diff --git a/.github/workflows/ipad-build.yaml b/.github/workflows/ipad-build.yaml index 824ada9f1..ad6dbc563 100644 --- a/.github/workflows/ipad-build.yaml +++ b/.github/workflows/ipad-build.yaml @@ -109,13 +109,6 @@ jobs: APPLE_PROFILE_NAME: GitHub CI/CD iPad BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} - - name: Upload logs to artifacts - uses: actions/upload-artifact@v3 - if: failure() - with: - name: gum-logs - path: /Users/runner/Library/Logs/gym/*.log - - name: Summary run: | echo '### Successful iPad build 🚀🚀🚀' >> $GITHUB_STEP_SUMMARY diff --git a/apps/extension/src/provider/tonconnect.ts b/apps/extension/src/provider/tonconnect.ts index d9a382ac9..710c70d68 100644 --- a/apps/extension/src/provider/tonconnect.ts +++ b/apps/extension/src/provider/tonconnect.ts @@ -52,7 +52,8 @@ export const getDeviceInfo = (): DeviceInfo => { 'SendTransaction', { name: 'SendTransaction', - maxMessages: 4 + maxMessages: 4, + extraCurrenciesSupported: true } ] }; diff --git a/apps/twa/src/components/transfer/SendNotifications.tsx b/apps/twa/src/components/transfer/SendNotifications.tsx index 60a01b8f7..458efe163 100644 --- a/apps/twa/src/components/transfer/SendNotifications.tsx +++ b/apps/twa/src/components/transfer/SendNotifications.tsx @@ -33,9 +33,9 @@ import { useAppSdk } from '@tonkeeper/uikit/dist/hooks/appSdk'; import { openIosKeyboard } from '@tonkeeper/uikit/dist/hooks/ios'; import { useTranslation } from '@tonkeeper/uikit/dist/hooks/translation'; import { useJettonList } from '@tonkeeper/uikit/dist/state/jetton'; -import { useActiveTronWallet, useTronBalances } from "@tonkeeper/uikit/dist/state/tron/tron"; +import { useActiveTronWallet, useTronBalances } from '@tonkeeper/uikit/dist/state/tron/tron'; import BigNumber from 'bignumber.js'; -import { FC, PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { FC, PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import styled from 'styled-components'; import { FavoriteView, useFavoriteNotification } from './FavoriteNotification'; @@ -51,8 +51,9 @@ import { RecipientTwaHeaderBlock } from './SendNotificationHeader'; import { useAnalyticsTrack } from '@tonkeeper/uikit/dist/hooks/amplitude'; -import { TRON_USDT_ASSET } from "@tonkeeper/core/dist/entries/crypto/asset/constants"; -import { seeIfValidTronAddress } from "@tonkeeper/core/dist/utils/common"; +import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { seeIfValidTonAddress, seeIfValidTronAddress } from '@tonkeeper/core/dist/utils/common'; +import { useActiveWallet } from '@tonkeeper/uikit/dist/state/wallet'; const Body = styled.div` padding: 0 16px 16px; @@ -249,7 +250,6 @@ const SendContent: FC<{ confirm: confirmRef }[view]; - const assetAmount = useMemo(() => { if (!amountViewState?.token || !amountViewState?.coinValue) { return null; @@ -270,8 +270,8 @@ const SendContent: FC<{ } } else { acceptBlockchains = activeTronWallet - ? [BLOCKCHAIN_NAME.TON, BLOCKCHAIN_NAME.TRON] - : [BLOCKCHAIN_NAME.TON]; + ? [BLOCKCHAIN_NAME.TON, BLOCKCHAIN_NAME.TRON] + : [BLOCKCHAIN_NAME.TON]; } return ( @@ -348,6 +348,7 @@ export const TwaSendNotification: FC = ({ children }) => { const { data: jettons } = useJettonList(); const { mutateAsync: getAccountAsync, reset } = useGetToAccount(); + const wallet = useActiveWallet(); const sdk = useAppSdk(); const track = useAnalyticsTrack(); @@ -371,15 +372,21 @@ export const TwaSendNotification: FC = ({ children }) => { return; } - if (transfer.address) { - getAccountAsync({ address: transfer.address }).then(account => { - setTonTransfer(makeTransferInitData(transfer, account, jettons)); + getAccountAsync({ address: wallet.rawAddress }).then(fromAccount => { + if (transfer.address && seeIfValidTonAddress(transfer.address)) { + getAccountAsync({ address: transfer.address }).then(toAccount => { + setTonTransfer( + makeTransferInitData(transfer, fromAccount, toAccount, jettons) + ); + setOpen(true); + }); + } else { + setTonTransfer({ + initAmountState: makeTransferInitAmountState(transfer, fromAccount, jettons) + }); setOpen(true); - }); - } else { - setTonTransfer({ initAmountState: makeTransferInitAmountState(transfer, jettons) }); - setOpen(true); - } + } + }); track('send_open', { from: transfer.from }); }; diff --git a/packages/core/src/entries/crypto/asset/ton-asset.ts b/packages/core/src/entries/crypto/asset/ton-asset.ts index 662afd008..143e2fb3a 100644 --- a/packages/core/src/entries/crypto/asset/ton-asset.ts +++ b/packages/core/src/entries/crypto/asset/ton-asset.ts @@ -1,35 +1,80 @@ import { Address } from '@ton/core'; -import { JettonBalance, JettonsBalances } from '../../../tonApiV2'; +import { Account, ExtraCurrency, JettonBalance, JettonsBalances } from '../../../tonApiV2'; import { BLOCKCHAIN_NAME } from '../../crypto'; import { BasicAsset, packAssetId } from './basic-asset'; import { TON_ASSET } from './constants'; import { AssetAmount } from './asset-amount'; import { TronAsset } from './tron-asset'; +import { seeIfValidTonAddress } from '../../../utils/common'; -export type TonAssetAddress = Address | 'TON'; +export type TonAssetAddress = TonAsset['address']; export function isTon(address: TonAssetAddress): address is 'TON' { return address === 'TON'; } -export interface TonAssetIdentification { - address: Address | 'TON'; +export interface TonMainAsset { + address: 'TON'; blockchain: BLOCKCHAIN_NAME.TON; } -export interface TonAsset extends BasicAsset, TonAssetIdentification {} +export interface TonExtraCurrencyAsset { + address: string; + blockchain: BLOCKCHAIN_NAME.TON; +} + +export interface TonJettonAsset { + address: Address; + blockchain: BLOCKCHAIN_NAME.TON; +} + +export type TonAssetIdentification = TonMainAsset | TonExtraCurrencyAsset | TonJettonAsset; + +export type TonAsset = BasicAsset & TonAssetIdentification; export function tonAssetAddressToString(address: TonAsset['address']): string { return typeof address === 'string' ? address : address.toRawString(); } export function tonAssetAddressFromString(address: string): TonAsset['address'] { - return address === 'TON' ? address : Address.parse(address); + return seeIfValidTonAddress(address) ? Address.parse(address) : address; } export function assetAddressToString(address: TonAsset['address'] | TronAsset['address']): string { return typeof address === 'string' ? address : address.toRawString(); } +export function extraBalanceToTonAsset(extraBalance: ExtraCurrency): TonAsset { + return { + id: String(extraBalance.preview.id), + symbol: extraBalance.preview.symbol, + name: extraBalance.preview.symbol, + decimals: extraBalance.preview.decimals, + address: extraBalance.preview.symbol, + blockchain: BLOCKCHAIN_NAME.TON, + image: extraBalance.preview.image + }; +} + +export function tokenToTonAsset( + token: string, + info: Account | undefined, + jettons: JettonsBalances +): TonAsset { + if (token === 'TON') { + return TON_ASSET; + } + + if (seeIfValidTonAddress(token)) { + return jettonToTonAsset(token, jettons); + } + + const extra = info?.extraBalance?.find(item => item.preview.symbol === token); + if (!extra) { + throw new Error(`Extra currency ${extra} not found`); + } + return extraBalanceToTonAsset(extra); +} + export function jettonToTonAsset(address: string, jettons: JettonsBalances): TonAsset { if (address === 'TON') { return TON_ASSET; @@ -75,5 +120,9 @@ export function legacyTonAssetId( if (tonAsset.address === 'TON') { return 'TON'; } - return options?.userFriendly ? tonAsset.address.toString() : tonAsset.address.toRawString(); + if (Address.isAddress(tonAsset.address)) { + return options?.userFriendly ? tonAsset.address.toString() : tonAsset.address.toRawString(); + } else { + return tonAsset.address; + } } diff --git a/packages/core/src/entries/tonConnect.ts b/packages/core/src/entries/tonConnect.ts index e024290dc..6455a9f9d 100644 --- a/packages/core/src/entries/tonConnect.ts +++ b/packages/core/src/entries/tonConnect.ts @@ -26,6 +26,7 @@ export interface TonConnectTransactionPayloadMessage { amount: string | number; payload?: string; // base64 cell stateInit?: string; // base64 cell + extra_currencies?: [{ id: number; value: string }]; } export type TonConnectAccount = { @@ -169,6 +170,7 @@ export enum SEND_TRANSACTION_ERROR_CODES { export type SendTransactionFeature = { name: 'SendTransaction'; maxMessages: number; + extraCurrenciesSupported?: boolean; }; export type SendTransactionFeatureDeprecated = 'SendTransaction'; diff --git a/packages/core/src/service/ton-blockchain/encoder/encoder-base.ts b/packages/core/src/service/ton-blockchain/encoder/encoder-base.ts new file mode 100644 index 000000000..a3a827b71 --- /dev/null +++ b/packages/core/src/service/ton-blockchain/encoder/encoder-base.ts @@ -0,0 +1,70 @@ +import { Address } from '@ton/core/dist/address/Address'; +import { Cell } from '@ton/core/dist/boc/Cell'; +import { Dictionary } from '@ton/core/dist/dict/Dictionary'; +import type { CurrencyCollection } from '@ton/core/dist/types/CurrencyCollection'; +import type { MessageRelaxed } from '@ton/core/dist/types/MessageRelaxed'; +import type { StateInit } from '@ton/core/dist/types/StateInit'; +import BigNumber from 'bignumber.js'; + +export abstract class EncoderBase { + private getOtherDict = () => { + return Dictionary.empty(Dictionary.Keys.Uint(32), Dictionary.Values.BigVarUint(5)); + }; + + protected currencyValue(src: { + amount: string | number; + extraCurrencies: + | { + id: number; + value: string; + }[] + | undefined; + }): CurrencyCollection { + const coins = BigInt(src.amount); + + if (!src.extraCurrencies) { + return { coins }; + } + + const other = this.getOtherDict(); + + for (let extra of src.extraCurrencies) { + other.set(extra.id, BigInt(extra.value)); + } + + return { coins, other }; + } + + protected extraCurrencyValue(src: { id: number; weiAmount: BigNumber }): CurrencyCollection { + const other = this.getOtherDict(); + + other.set(src.id, BigInt(src.weiAmount.toFixed(0))); + + return { coins: BigInt('0'), other }; + } + + protected internalMessage(src: { + to: Address; + value: CurrencyCollection; + bounce: boolean; + init?: StateInit; + body?: Cell; + }): MessageRelaxed { + return { + info: { + type: 'internal', + dest: src.to, + value: src.value, + bounce: src.bounce, + ihrDisabled: true, + bounced: false, + ihrFee: 0n, + forwardFee: 0n, + createdAt: 0, + createdLt: 0n + }, + init: src.init ?? undefined, + body: src.body ?? Cell.EMPTY + }; + } +} diff --git a/packages/core/src/service/ton-blockchain/encoder/extra-currency-encoder.ts b/packages/core/src/service/ton-blockchain/encoder/extra-currency-encoder.ts new file mode 100644 index 000000000..83337242e --- /dev/null +++ b/packages/core/src/service/ton-blockchain/encoder/extra-currency-encoder.ts @@ -0,0 +1,84 @@ +import { Address, SendMode } from '@ton/core'; +import { userInputAddressIsBounceable } from '../utils'; +import BigNumber from 'bignumber.js'; +import { APIConfig } from '../../../entries/apis'; +import { MessagePayloadParam, serializePayload, WalletOutgoingMessage } from './types'; +import { EncoderBase } from './encoder-base'; + +export class ExtraCurrencyEncoder extends EncoderBase { + constructor(private readonly api: APIConfig, private readonly _walletAddress: string) { + super(); + } + + encodeTransfer = async ( + transfer: + | { + id: number; + to: string; + weiAmount: BigNumber; + payload?: MessagePayloadParam; + } + | { + id: number; + to: string; + weiAmount: BigNumber; + bounce: boolean; + payload?: MessagePayloadParam; + }[] + ): Promise => { + if (Array.isArray(transfer)) { + return this.encodeMultiTransfer(transfer); + } else { + return this.encodeSingleTransfer(transfer); + } + }; + + private encodeSingleTransfer = async ({ + id, + to, + weiAmount, + payload + }: { + id: number; + to: string; + weiAmount: BigNumber; + payload?: MessagePayloadParam; + }): Promise => { + const message = this.internalMessage({ + to: Address.parse(to), + bounce: await userInputAddressIsBounceable(this.api, to), + value: this.extraCurrencyValue({ id, weiAmount }), + body: serializePayload(payload) + }); + + return { + messages: [message], + sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS + }; + }; + + private encodeMultiTransfer = async ( + transfers: { + id: number; + to: string; + weiAmount: BigNumber; + bounce: boolean; + payload?: MessagePayloadParam; + }[] + ): Promise => { + return { + messages: transfers.map(transfer => + this.internalMessage({ + to: Address.parse(transfer.to), + bounce: transfer.bounce, + value: this.extraCurrencyValue({ + id: transfer.id, + weiAmount: transfer.weiAmount + }), + body: serializePayload(transfer.payload) + }) + ), + sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS + }; + }; +} diff --git a/packages/core/src/service/ton-blockchain/encoder/ton-connect-encoder.ts b/packages/core/src/service/ton-blockchain/encoder/ton-connect-encoder.ts index a06badf99..cc6a68ebe 100644 --- a/packages/core/src/service/ton-blockchain/encoder/ton-connect-encoder.ts +++ b/packages/core/src/service/ton-blockchain/encoder/ton-connect-encoder.ts @@ -6,9 +6,12 @@ import { TON_CONNECT_MSG_VARIANTS_ID, TonConnectTransactionPayload } from '../../../entries/tonConnect'; +import { EncoderBase } from './encoder-base'; -export class TonConnectEncoder { - constructor(private readonly api: APIConfig, private readonly walletAddress: string) {} +export class TonConnectEncoder extends EncoderBase { + constructor(private readonly api: APIConfig, private readonly walletAddress: string) { + super(); + } encodeTransfer = async ( transfer: TonConnectTransactionPayload & { @@ -30,10 +33,13 @@ export class TonConnectEncoder { sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, messages: await Promise.all( messages.map(async item => - internal({ + this.internalMessage({ to: Address.parse(item.address), bounce: await tonConnectAddressIsBounceable(this.api, item.address), - value: BigInt(item.amount), + value: this.currencyValue({ + amount: item.amount, + extraCurrencies: item.extra_currencies + }), init: toStateInit(item.stateInit), body: item.payload ? Cell.fromBase64(item.payload) : undefined }) diff --git a/packages/core/src/service/ton-blockchain/ton-asset-transaction.service.ts b/packages/core/src/service/ton-blockchain/ton-asset-transaction.service.ts index 8ffce374c..2fc96d6db 100644 --- a/packages/core/src/service/ton-blockchain/ton-asset-transaction.service.ts +++ b/packages/core/src/service/ton-blockchain/ton-asset-transaction.service.ts @@ -12,6 +12,8 @@ import { getTonEstimationTonFee, TonEstimation } from '../../entries/send'; import { isStandardTonWallet, TonContract } from '../../entries/wallet'; import { MessagePayloadParam } from './encoder/types'; import { assertMessagesNumberSupported } from './utils'; +import { seeIfValidTonAddress } from '../../utils/common'; +import { ExtraCurrencyEncoder } from './encoder/extra-currency-encoder'; export type TransferParams = | { @@ -31,6 +33,9 @@ export class TonAssetTransactionService { constructor(private readonly api: APIConfig, private readonly wallet: TonContract) {} async estimate(sender: Sender, params: TransferParams): Promise { + if (this.isExtraCurrency(params)) { + return this.estimateExtraCurrency(sender, params); + } if (this.isJettonTransfer(params)) { return this.estimateJetton(sender, params); } else { @@ -38,6 +43,43 @@ export class TonAssetTransactionService { } } + private async estimateExtraCurrency( + sender: Sender, + params: TransferParams + ): Promise { + // await this.checkTransferPossibility(sender, params); // TODO: Extra Currency validation + + if (Array.isArray(params)) { + if (sender instanceof LedgerMessageSender) { + throw new Error('Ledger multisend is not supported.'); + } else { + return sender.estimate( + await new ExtraCurrencyEncoder(this.api, this.wallet.rawAddress).encodeTransfer( + params.map(p => ({ + ...p, + weiAmount: p.amount.weiAmount, + id: Number(p.amount.asset.id) + })) + ) + ); + } + } else { + if (sender instanceof LedgerMessageSender) { + throw new Error('Ledger extra currency transfer is not supported.'); // TODO: Extra Currency - check ledger + } else { + return sender.estimate( + await new ExtraCurrencyEncoder(this.api, this.wallet.rawAddress).encodeTransfer( + { + ...params, + weiAmount: params.amount.weiAmount, + id: Number(params.amount.asset.id) + } + ) + ); + } + } + } + private async estimateTon(sender: Sender, params: TransferParams) { await this.checkTransferPossibility(sender, params); @@ -94,12 +136,54 @@ export class TonAssetTransactionService { } async send(sender: Sender, estimation: TonEstimation, params: TransferParams) { + if (this.isExtraCurrency(params)) { + return this.sendExtraCurrency(sender, estimation, params); + } if (this.isJettonTransfer(params)) { await this.sendJetton(sender, estimation, params); } else { await this.sendTon(sender, estimation, params); } } + private async sendExtraCurrency( + sender: Sender, + estimation: TonEstimation, + params: TransferParams + ) { + // await this.checkTransferPossibility(sender, params, estimation); // TODO: Extra Currency validation + + if (Array.isArray(params)) { + if (sender instanceof LedgerMessageSender) { + throw new Error('Ledger multisend is not supported.'); + } else { + return sender.send( + await new ExtraCurrencyEncoder(this.api, this.wallet.rawAddress).encodeTransfer( + params.map(p => ({ + ...p, + weiAmount: p.amount.weiAmount, + id: Number(p.amount.asset.id) + })) + ) + ); + } + } else { + if (sender instanceof LedgerMessageSender) { + return ( + await sender.tonTransfer({ ...params, weiAmount: params.amount.weiAmount }) + ).send(); + } else { + return sender.send( + await new ExtraCurrencyEncoder(this.api, this.wallet.rawAddress).encodeTransfer( + { + ...params, + weiAmount: params.amount.weiAmount, + id: Number(params.amount.asset.id) + } + ) + ); + } + } + } private async sendTon(sender: Sender, estimation: TonEstimation, params: TransferParams) { await this.checkTransferPossibility(sender, params, estimation); @@ -182,6 +266,14 @@ export class TonAssetTransactionService { }; } + private isExtraCurrency(params: TransferParams) { + const token = Array.isArray(params) + ? params[0].amount.asset.address + : params.amount.asset.address; + + return token !== TON_ASSET.address && !seeIfValidTonAddress(token.toString()); + } + private isJettonTransfer(params: TransferParams) { return Array.isArray(params) ? params[0].amount.asset.id !== TON_ASSET.id diff --git a/packages/core/src/service/tonConnect/connectService.ts b/packages/core/src/service/tonConnect/connectService.ts index 52cdf4299..8e5c3a471 100644 --- a/packages/core/src/service/tonConnect/connectService.ts +++ b/packages/core/src/service/tonConnect/connectService.ts @@ -161,7 +161,8 @@ export const getDeviceInfo = (appVersion: string, maxMessages: number): DeviceIn 'SendTransaction', { name: 'SendTransaction', - maxMessages: maxMessages + maxMessages: maxMessages, + extraCurrenciesSupported: true } ] }; diff --git a/packages/uikit/src/components/activity/EmptyActivity.tsx b/packages/uikit/src/components/activity/EmptyActivity.tsx index 7d8f67603..3faa3ff75 100644 --- a/packages/uikit/src/components/activity/EmptyActivity.tsx +++ b/packages/uikit/src/components/activity/EmptyActivity.tsx @@ -10,7 +10,6 @@ import { ArrowDownIcon, PlusIcon } from '../Icon'; import { HideOnReview } from '../ios/HideOnReview'; const EmptyBody = styled.div` - margin-top: -64px; flex-grow: 1; display: flex; flex-direction: column; diff --git a/packages/uikit/src/components/activity/ton/ActivityNotification.tsx b/packages/uikit/src/components/activity/ton/ActivityNotification.tsx index b4410da6a..e82b04dcc 100644 --- a/packages/uikit/src/components/activity/ton/ActivityNotification.tsx +++ b/packages/uikit/src/components/activity/ton/ActivityNotification.tsx @@ -5,6 +5,7 @@ import { ErrorActivityNotification } from '../NotificationCommon'; import { AuctionBidActionDetails, DomainRenewActionDetails, + ExtraCurrencyTransferNotification, SmartContractExecActionDetails, TonTransferActionNotification } from './TonActivityActionDetails'; @@ -84,6 +85,8 @@ const ActivityContentTon: FC = props => { return ; case 'NftPurchase': return ; + case 'ExtraCurrencyTransfer': + return ; case 'Unknown': return ; default: { diff --git a/packages/uikit/src/components/activity/ton/TonActivityAction.tsx b/packages/uikit/src/components/activity/ton/TonActivityAction.tsx index 3c8799ea6..a9ef1529a 100644 --- a/packages/uikit/src/components/activity/ton/TonActivityAction.tsx +++ b/packages/uikit/src/components/activity/ton/TonActivityAction.tsx @@ -85,6 +85,56 @@ const TonTransferAction: FC<{ ); }; +const ExtraCurrencyTransferAction: FC<{ + action: Action; + date: string; + isScam: boolean; +}> = ({ action, date, isScam }) => { + const wallet = useActiveWallet(); + const { extraCurrencyTransfer } = action; + const network = useActiveTonNetwork(); + + const format = useFormatCoinValue(); + + if (!extraCurrencyTransfer) { + return ; + } + + if (extraCurrencyTransfer.recipient.address === wallet.rawAddress) { + return ( + + ); + } + return ( + + ); +}; + export const SmartContractExecAction: FC<{ action: Action; date: string; @@ -248,6 +298,8 @@ export const ActivityAction: FC<{ return ; case 'DomainRenew': return ; + case 'ExtraCurrencyTransfer': + return ; case 'Unknown': return {t('txActions_signRaw_types_unknownTransaction')}; default: { diff --git a/packages/uikit/src/components/activity/ton/TonActivityActionDetails.tsx b/packages/uikit/src/components/activity/ton/TonActivityActionDetails.tsx index 6b5e8fbd2..baefb92fd 100644 --- a/packages/uikit/src/components/activity/ton/TonActivityActionDetails.tsx +++ b/packages/uikit/src/components/activity/ton/TonActivityActionDetails.tsx @@ -1,5 +1,10 @@ import { CryptoCurrency } from '@tonkeeper/core/dist/entries/crypto'; -import { AccountEvent, ActionStatusEnum, TonTransferAction } from '@tonkeeper/core/dist/tonApiV2'; +import { + AccountEvent, + ActionStatusEnum, + ExtraCurrencyTransferAction, + TonTransferAction +} from '@tonkeeper/core/dist/tonApiV2'; import { formatDecimals } from '@tonkeeper/core/dist/utils/balance'; import { FC } from 'react'; import { useFormatCoinValue } from '../../../hooks/balance'; @@ -167,3 +172,68 @@ export const SmartContractExecActionDetails: FC = ({ action, timesta ); }; + +const ExtraCurrencyTransferActionContent: FC<{ + extraCurrencyTransfer: ExtraCurrencyTransferAction; + timestamp: number; + event: AccountEvent; + isScam: boolean; + status?: ActionStatusEnum; +}> = ({ extraCurrencyTransfer, timestamp, event, isScam, status }) => { + const wallet = useActiveWallet(); + const { data } = useRate(extraCurrencyTransfer.currency.symbol); + const { fiatAmount } = useFormatFiat( + data, + formatDecimals(extraCurrencyTransfer.amount, extraCurrencyTransfer.currency.decimals) + ); + + const kind = + extraCurrencyTransfer.recipient.address === wallet.rawAddress ? 'received' : 'send'; + + return ( + + + + {kind === 'received' && ( + + )} + {kind === 'send' && ( + + )} + + + + + + ); +}; + +export const ExtraCurrencyTransferNotification: FC = ({ + action, + timestamp, + event, + isScam +}) => { + const { extraCurrencyTransfer } = action; + if (!extraCurrencyTransfer) { + return ; + } + return ( + + ); +}; diff --git a/packages/uikit/src/components/desktop/history/ton/HistoryAction.tsx b/packages/uikit/src/components/desktop/history/ton/HistoryAction.tsx index d8d83292d..d95e7631d 100644 --- a/packages/uikit/src/components/desktop/history/ton/HistoryAction.tsx +++ b/packages/uikit/src/components/desktop/history/ton/HistoryAction.tsx @@ -1,7 +1,10 @@ import { FC } from 'react'; import { Action } from '@tonkeeper/core/dist/tonApiV2'; -import { TonTransferDesktopAction } from './TonTransferDesktopAction'; +import { + ExtraCurrencyTransferDesktopAction, + TonTransferDesktopAction +} from './TonTransferDesktopAction'; import { NftPurchaseDesktopAction, NftTransferDesktopAction } from './NftDesktopActions'; import { JettonBurnDesktopAction, @@ -56,6 +59,8 @@ export const HistoryAction: FC<{ return ; case 'DomainRenew': return ; + case 'ExtraCurrencyTransfer': + return ; case 'Unknown': return ; default: { diff --git a/packages/uikit/src/components/desktop/history/ton/TonTransferDesktopAction.tsx b/packages/uikit/src/components/desktop/history/ton/TonTransferDesktopAction.tsx index 28a995da9..88e984dce 100644 --- a/packages/uikit/src/components/desktop/history/ton/TonTransferDesktopAction.tsx +++ b/packages/uikit/src/components/desktop/history/ton/TonTransferDesktopAction.tsx @@ -60,3 +60,50 @@ export const TonTransferDesktopAction: FC<{ ); }; + +export const ExtraCurrencyTransferDesktopAction: FC<{ + action: Action; + isScam: boolean; +}> = ({ action, isScam }) => { + const wallet = useActiveWallet(); + const { extraCurrencyTransfer } = action; + + if (!extraCurrencyTransfer) { + return ; + } + + if (eqAddresses(extraCurrencyTransfer.recipient.address, wallet.rawAddress)) { + return ( + <> + + + + + + + + ); + } + return ( + <> + + + + + + + + ); +}; diff --git a/packages/uikit/src/components/desktop/multi-send/MultiSendConfirmNotification.tsx b/packages/uikit/src/components/desktop/multi-send/MultiSendConfirmNotification.tsx index b15462c1b..4316ef23e 100644 --- a/packages/uikit/src/components/desktop/multi-send/MultiSendConfirmNotification.tsx +++ b/packages/uikit/src/components/desktop/multi-send/MultiSendConfirmNotification.tsx @@ -4,7 +4,7 @@ import { Image, ImageMock } from '../../transfer/Confirm'; import { MultiSendForm } from '../../../state/multiSend'; import { TonAsset } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import styled from 'styled-components'; -import { useAssetImage } from '../../../state/asset'; +import { useTonAssetImage, useAssetImage } from '../../../state/asset'; import { Body1, Body2, Body2Class, Body3, Body3Class, Label2, Num2 } from '../../Text'; import { useRate } from '../../../state/rates'; import { useAppContext } from '../../../hooks/appContext'; diff --git a/packages/uikit/src/components/desktop/multi-send/MultiSendReceiversNotification.tsx b/packages/uikit/src/components/desktop/multi-send/MultiSendReceiversNotification.tsx index 1efb3df15..d3b8a49cb 100644 --- a/packages/uikit/src/components/desktop/multi-send/MultiSendReceiversNotification.tsx +++ b/packages/uikit/src/components/desktop/multi-send/MultiSendReceiversNotification.tsx @@ -7,7 +7,10 @@ import { MultiSendFormTokenized } from '../../../hooks/blockchain/useSendMultiTr import { formatFiatCurrency, formatter } from '../../../hooks/balance'; import { useAppContext } from '../../../hooks/appContext'; import { useRate } from '../../../state/rates'; -import { TonAsset } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; +import { + TonAsset, + tonAssetAddressToString +} from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { shiftedDecimals } from '@tonkeeper/core/dist/utils/balance'; import { useTranslation } from '../../../hooks/translation'; @@ -79,7 +82,7 @@ const ReceiversTable: FC<{ form: MultiSendFormTokenized; asset: TonAsset }> = ({ }; const { fiat } = useAppContext(); - const { data: rate } = useRate(asset.address === 'TON' ? 'TON' : asset.address.toRawString()); + const { data: rate } = useRate(tonAssetAddressToString(asset.address)); const fiatToString = (weiAmount: MultiSendFormTokenized['rows'][number]['weiAmount']) => { return formatFiatCurrency( diff --git a/packages/uikit/src/components/home/AccountView.tsx b/packages/uikit/src/components/home/AccountView.tsx index 15fd9c487..53c0b7f99 100644 --- a/packages/uikit/src/components/home/AccountView.tsx +++ b/packages/uikit/src/components/home/AccountView.tsx @@ -1,5 +1,9 @@ import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; -import { formatAddress, formatTransferUrl } from '@tonkeeper/core/dist/utils/common'; +import { + formatAddress, + formatTransferUrl, + seeIfValidTonAddress +} from '@tonkeeper/core/dist/utils/common'; import { FC, useRef, useState } from 'react'; import { QRCode } from 'react-qrcode-logo'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; @@ -25,7 +29,7 @@ import { useIsActiveWalletWatchOnly } from '../../state/wallet'; import { AccountBadge } from '../account/AccountBadge'; -import { useAssetImage } from '../../state/asset'; +import { useTonAssetImage } from '../../state/asset'; import { TON_ASSET, TRON_TRX_ASSET, @@ -164,7 +168,7 @@ const ReceiveTon: FC<{ jetton?: string }> = ({ jetton }) => { const { t } = useTranslation(); const network = useActiveTonNetwork(); - const assetImage = useAssetImage({ + const assetImage = useTonAssetImage({ blockchain: BLOCKCHAIN_NAME.TON, address: jetton ? Address.parse(jetton) : TON_ASSET.address }); @@ -310,10 +314,7 @@ export const ReceiveContent: FC<{ {isTon ? ( ) : ( diff --git a/packages/uikit/src/components/home/Jettons.tsx b/packages/uikit/src/components/home/Jettons.tsx index 13964aa6e..fedea0d29 100644 --- a/packages/uikit/src/components/home/Jettons.tsx +++ b/packages/uikit/src/components/home/Jettons.tsx @@ -24,6 +24,7 @@ import { import { useJettonList } from '../../state/jetton'; import { eqAddresses } from '@tonkeeper/core/dist/utils/address'; import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { Address } from '@ton/core'; export interface TonAssetData { info: Account; diff --git a/packages/uikit/src/components/transfer/ConfirmView.tsx b/packages/uikit/src/components/transfer/ConfirmView.tsx index 11f45a7d4..e352cc388 100644 --- a/packages/uikit/src/components/transfer/ConfirmView.tsx +++ b/packages/uikit/src/components/transfer/ConfirmView.tsx @@ -18,7 +18,7 @@ import { useAppContext } from '../../hooks/appContext'; import { useAppSdk } from '../../hooks/appSdk'; import { formatFiatCurrency } from '../../hooks/balance'; import { useTranslation } from '../../hooks/translation'; -import { useAssetAmountFiatEquivalent, useAssetImage } from '../../state/asset'; +import { useAssetAmountFiatEquivalent, useTonAssetImage, useAssetImage } from '../../state/asset'; import { CheckmarkCircleIcon, ChevronLeftIcon, ExclamationMarkCircleIcon } from '../Icon'; import { Gap } from '../Layout'; import { ListBlock } from '../List'; diff --git a/packages/uikit/src/components/transfer/SendNotifications.tsx b/packages/uikit/src/components/transfer/SendNotifications.tsx index b7aabf4ee..d70b8eef1 100644 --- a/packages/uikit/src/components/transfer/SendNotifications.tsx +++ b/packages/uikit/src/components/transfer/SendNotifications.tsx @@ -55,7 +55,8 @@ import { useIsActiveAccountMultisig } from '../../state/multisig'; import { ConfirmMultisigNewTransferView } from './ConfirmMultisigNewTransferView'; import { useAnalyticsTrack } from '../../hooks/amplitude'; import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; -import { seeIfValidTronAddress } from '@tonkeeper/core/dist/utils/common'; +import { seeIfValidTonAddress, seeIfValidTronAddress } from '@tonkeeper/core/dist/utils/common'; +import { useActiveWallet } from '../../state/wallet'; const SendContent: FC<{ onClose: () => void; @@ -426,6 +427,7 @@ const SendActionNotification = () => { const [chain, setChain] = useState(undefined); const [tonTransfer, setTonTransfer] = useState(undefined); const { data: jettons } = useJettonList(); + const wallet = useActiveWallet(); const { mutateAsync: getAccountAsync, reset } = useGetToAccount(); const sdk = useAppSdk(); @@ -448,15 +450,21 @@ const SendActionNotification = () => { return; } - if (transfer.address) { - getAccountAsync({ address: transfer.address }).then(account => { - setTonTransfer(makeTransferInitData(transfer, account, jettons)); + getAccountAsync({ address: wallet.rawAddress }).then(fromAccount => { + if (transfer.address && seeIfValidTonAddress(transfer.address)) { + getAccountAsync({ address: transfer.address }).then(toAccount => { + setTonTransfer( + makeTransferInitData(transfer, fromAccount, toAccount, jettons) + ); + setOpen(true); + }); + } else { + setTonTransfer({ + initAmountState: makeTransferInitAmountState(transfer, fromAccount, jettons) + }); setOpen(true); - }); - } else { - setTonTransfer({ initAmountState: makeTransferInitAmountState(transfer, jettons) }); - setOpen(true); - } + } + }); track('send_open', { from: transfer.from }); }; diff --git a/packages/uikit/src/components/transfer/amountView/AmountView.tsx b/packages/uikit/src/components/transfer/amountView/AmountView.tsx index 7db2dc3cb..b03a2023a 100644 --- a/packages/uikit/src/components/transfer/amountView/AmountView.tsx +++ b/packages/uikit/src/components/transfer/amountView/AmountView.tsx @@ -2,7 +2,8 @@ import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; import { TonAsset, jettonToTonAsset, - legacyTonAssetId + legacyTonAssetId, + tokenToTonAsset } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { RecipientData } from '@tonkeeper/core/dist/entries/send'; import { isNumeric } from '@tonkeeper/core/dist/utils/send'; @@ -139,7 +140,7 @@ export const AmountView: FC<{ if (!jettons) return; dispatch({ kind: 'select', - payload: { token: jettonToTonAsset(address, jettons) } + payload: { token: tokenToTonAsset(address, info, jettons) } }); }, [dispatch, jettons] diff --git a/packages/uikit/src/components/transfer/amountView/AssetSelect.tsx b/packages/uikit/src/components/transfer/amountView/AssetSelect.tsx index 2ee06795a..926318066 100644 --- a/packages/uikit/src/components/transfer/amountView/AssetSelect.tsx +++ b/packages/uikit/src/components/transfer/amountView/AssetSelect.tsx @@ -87,6 +87,32 @@ const AssetDropDown: FC<{ ) : undefined} + {info?.extraBalance?.map(item => { + return ( + { + setJetton(item.preview.symbol); + onClose(); + }} + > + + + + {item.preview.symbol} + {format(item.amount, item.preview.decimals)} + + + {item.preview.symbol === jetton ? ( + + + + ) : undefined} + + + ); + })} {jettons.balances.map(item => { return ( { @@ -405,7 +406,7 @@ export const makeTransferInitData = ( done: toAccount.memoRequired ? tonTransfer.text !== '' && tonTransfer.text !== null : true }; - const initAmountState = makeTransferInitAmountState(tonTransfer, jettons); + const initAmountState = makeTransferInitAmountState(tonTransfer, fromAccount, jettons); return { initRecipient, @@ -415,10 +416,15 @@ export const makeTransferInitData = ( export const makeTransferInitAmountState = ( transfer: Pick, + account: Account | undefined, jettons: JettonsBalances | undefined ): Partial => { try { - const token = jettonToTonAsset(transfer.jetton || 'TON', jettons || { balances: [] }); + const token = tokenToTonAsset( + transfer.jetton || 'TON', + account, + jettons || { balances: [] } + ); if (!transfer.amount) { return { @@ -440,5 +446,4 @@ export const makeTransferInitAmountState = ( } catch { return {}; } - return {}; }; diff --git a/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx b/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx index 5cd7f010f..de3dde6df 100644 --- a/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx +++ b/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx @@ -40,6 +40,7 @@ import { useActiveTronWallet, useTronBalances } from '../../state/tron/tron'; import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; import { BorderSmallResponsive } from '../../components/shared/Styles'; import { useSendTransferNotification } from '../../components/modals/useSendTransferNotification'; +import { seeIfValidTonAddress } from '@tonkeeper/core/dist/utils/common'; export const DesktopCoinPage = () => { const navigate = useNavigate(); @@ -229,29 +230,41 @@ const CoinInfo: FC<{ token: string }> = ({ token }) => { }; } - const jettonBalance = assets.ton.jettons.balances.find(b => - eqAddresses(b.jetton.address, token) - ); + if (seeIfValidTonAddress(token)) { + const jettonBalance = assets.ton.jettons.balances.find(b => + eqAddresses(b.jetton.address, token) + ); - if (!jettonBalance) { - return undefined; + if (!jettonBalance) { + return undefined; + } + + const amount = jettonBalance.balance; + + return { + image: jettonBalance.jetton.image, + symbol: jettonBalance.jetton.symbol, + amount: format(amount, jettonBalance.jetton.decimals), + fiatAmount: formatFiatCurrency( + fiat, + jettonBalance.price + ? shiftedDecimals( + jettonBalance.balance, + jettonBalance.jetton.decimals + ).multipliedBy(toTokenRate(jettonBalance.price, fiat).prices) + : 0 + ) + }; } - const amount = jettonBalance.balance; + const extra = assets.ton.info.extraBalance?.find(item => item.preview.symbol === token); + if (!extra) return undefined; return { - image: jettonBalance.jetton.image, - symbol: jettonBalance.jetton.symbol, - amount: format(amount, jettonBalance.jetton.decimals), - fiatAmount: formatFiatCurrency( - fiat, - jettonBalance.price - ? shiftedDecimals( - jettonBalance.balance, - jettonBalance.jetton.decimals - ).multipliedBy(toTokenRate(jettonBalance.price, fiat).prices) - : 0 - ) + image: extra.preview.image, + symbol: extra.preview.symbol, + amount: format(extra.amount, extra.preview.decimals), + fiatAmount: formatFiatCurrency(fiat, 0) // TODO: Extra Currency Rates }; }, [assets, format, rate, fiat]); @@ -317,8 +330,12 @@ const CoinPage: FC<{ token: string }> = ({ token }) => { return t('Toncoin'); } - return assets.ton.jettons.balances.find(b => eqAddresses(b.jetton.address, token))?.jetton - .symbol; + if (seeIfValidTonAddress(decodeURIComponent(token))) { + return assets.ton.jettons.balances.find(b => eqAddresses(b.jetton.address, token)) + ?.jetton.symbol; + } else { + return token; + } }, [assets, t, token]); return ( diff --git a/packages/uikit/src/pages/coin/Coin.tsx b/packages/uikit/src/pages/coin/Coin.tsx index 9a7b5093f..5022ccc44 100644 --- a/packages/uikit/src/pages/coin/Coin.tsx +++ b/packages/uikit/src/pages/coin/Coin.tsx @@ -1,10 +1,12 @@ -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { AppRoute } from '../../libs/routes'; import { JettonContent } from './Jetton'; import { TonPage } from './Ton'; import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; import { TronUsdtContent } from './TronUsdt'; +import { seeIfValidTonAddress } from '@tonkeeper/core/dist/utils/common'; +import { ExtraCurrencyPage } from './ExtraCurrency'; const CoinPage = () => { const navigate = useNavigate(); @@ -23,7 +25,11 @@ const CoinPage = () => { } else if (name === 'ton') { return ; } else { - return ; + if (seeIfValidTonAddress(decodeURIComponent(name))) { + return ; + } else { + return ; + } } }; diff --git a/packages/uikit/src/pages/coin/ExtraCurrency.tsx b/packages/uikit/src/pages/coin/ExtraCurrency.tsx new file mode 100644 index 000000000..018707ab0 --- /dev/null +++ b/packages/uikit/src/pages/coin/ExtraCurrency.tsx @@ -0,0 +1,55 @@ +import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; +import { ExtraCurrency } from '@tonkeeper/core/dist/tonApiV2'; +import { formatDecimals } from '@tonkeeper/core/dist/utils/balance'; +import { FC, useMemo, useRef } from 'react'; +import { InnerBody } from '../../components/Body'; +import { CoinSkeletonPage } from '../../components/Skeleton'; +import { SubHeader } from '../../components/SubHeader'; +import { HomeActions } from '../../components/home/TonActions'; +import { CoinInfo } from '../../components/jettons/Info'; +import { useFormatBalance } from '../../hooks/balance'; +import { useTranslation } from '../../hooks/translation'; +import { useFormatFiat, useRate } from '../../state/rates'; +import { useWalletAccountInfo } from '../../state/wallet'; +import { MobileAssetHistory } from './Jetton'; + +const ItemHeader: FC<{ extra: ExtraCurrency }> = ({ extra }) => { + const amount = useMemo(() => formatDecimals(extra.amount, extra.preview.decimals), [extra]); + const total = useFormatBalance(amount); + + const { data } = useRate(extra.preview.symbol); + const { fiatAmount } = useFormatFiat(data, amount); + + return ( + + ); +}; + +export const ExtraCurrencyPage: FC<{ name: string }> = ({ name }) => { + const { t } = useTranslation(); + const ref = useRef(null); + + const { data: info } = useWalletAccountInfo(); + + const extra = info?.extraBalance?.find(item => item.preview.symbol === name); + + if (!extra) { + return ; + } + + return ( + <> + + + + + + + + ); +}; diff --git a/packages/uikit/src/state/activity.ts b/packages/uikit/src/state/activity.ts index 23ee17019..1303c3c13 100644 --- a/packages/uikit/src/state/activity.ts +++ b/packages/uikit/src/state/activity.ts @@ -10,9 +10,9 @@ import { useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { atom, useAtom } from '../libs/atom'; import { QueryKey } from '../libs/queryKey'; import { useGlobalPreferences, useMutateGlobalPreferences } from './global-preferences'; -import { seeIfTonTransfer } from './ton/tonActivity'; +import { seeIfExtraCurrencyTransfer, seeIfTonTransfer } from './ton/tonActivity'; import { useActiveApi, useActiveWallet } from './wallet'; -import { debounce } from '@tonkeeper/core/dist/utils/common'; +import { debounce, seeIfValidTonAddress } from '@tonkeeper/core/dist/utils/common'; import { useTwoFAWalletConfig } from './two-fa'; import { useActiveTronWallet, useTronApi } from './tron/tron'; import { APIConfig } from '@tonkeeper/core/dist/entries/apis'; @@ -325,7 +325,14 @@ async function fetchTonActivity({ initiator: onlyInitiator ? onlyInitiator : undefined }); } else { - if (assetTonApiId! === 'TON') { + if (seeIfValidTonAddress(assetTonApiId)) { + tonActivity = await new AccountsApi(api.tonApiV2).getAccountJettonHistoryByID({ + accountId: wallet.rawAddress, + jettonId: assetTonApiId!, + limit: 20, + beforeLt: pageParam + }); + } else if (assetTonApiId === 'TON') { tonActivity = await new AccountsApi(api.tonApiV2).getAccountEvents({ accountId: wallet.rawAddress, limit: 20, @@ -339,11 +346,17 @@ async function fetchTonActivity({ return event.actions.length > 0; }); } else { - tonActivity = await new AccountsApi(api.tonApiV2).getAccountJettonHistoryByID({ + tonActivity = await new AccountsApi(api.tonApiV2).getAccountEvents({ accountId: wallet.rawAddress, - jettonId: assetTonApiId!, limit: 20, - beforeLt: pageParam + beforeLt: pageParam, + subjectOnly: true, + initiator: onlyInitiator ? onlyInitiator : undefined + }); + + tonActivity.events = tonActivity.events.filter(event => { + event.actions = event.actions.filter(seeIfExtraCurrencyTransfer(assetTonApiId)); + return event.actions.length > 0; }); } } diff --git a/packages/uikit/src/state/asset.ts b/packages/uikit/src/state/asset.ts index 7cbbd8fb4..70a9937ce 100644 --- a/packages/uikit/src/state/asset.ts +++ b/packages/uikit/src/state/asset.ts @@ -35,6 +35,7 @@ import { useTronBalances } from './tron/tron'; import { useAccountsState, useActiveAccount, useWalletAccountInfo } from './wallet'; import { Network } from '@tonkeeper/core/dist/entries/network'; import { getNetworkByAccount } from '@tonkeeper/core/dist/entries/account'; +import { seeIfValidTonAddress } from '@tonkeeper/core/dist/utils/common'; export function useUserAssetBalance< T extends AssetIdentification = AssetIdentification, @@ -51,11 +52,17 @@ export function useUserAssetBalance< if (asset.address === 'TON') { isLoading = tonWalletInfo.isLoading; data = tonWalletInfo?.data?.balance || '0'; - } else { + } else if (Address.isAddress(asset.address)) { isLoading = jettons.isLoading; data = jettons.data?.balances.find(i => i.jetton.address === legacyTonAssetId(asset)) ?.balance || '0'; + } else { + isLoading = tonWalletInfo.isLoading; + const extra = tonWalletInfo.data?.extraBalance?.find( + item => item.preview.symbol === asset.address + ); + data = extra?.amount || '0'; } if (isBasicAsset(asset)) { data = new AssetAmount({ asset, weiAmount: data }); @@ -77,7 +84,11 @@ export function useUserAssetBalance< }; } -export function useAssetImage({ blockchain, address }: AssetIdentification): string | undefined { +export function useAssetImage(asset: Asset): string | undefined { + return asset.image; +} + +export function useTonAssetImage({ blockchain, address }: AssetIdentification): string | undefined { const id = packAssetId(blockchain, address); const { data: jettons } = useJettonList(); @@ -85,10 +96,6 @@ export function useAssetImage({ blockchain, address }: AssetIdentification): str return 'https://wallet.tonkeeper.com/img/toncoin.svg'; } - if (id === TRON_USDT_ASSET.id) { - return TRON_USDT_ASSET.image; - } - if (typeof address === 'string') { throw new Error('Unexpected address'); } @@ -300,6 +307,20 @@ export function useAssetsDistribution(maxGropusNumber = 10) { }) ); + if (assets.ton.info.extraBalance) { + for (let extra of assets.ton.info.extraBalance) { + // TODO: Extra Currency token rate + const dist: Omit = { + fiatBalance: new BigNumber(0), + meta: convertJettonToTokenMeta( + { isNative: true, balance: Number(extra.amount) }, + 0 + ) + }; + tokensOmited.push(dist); + } + } + const total = tokensOmited.reduce( (acc, t) => t.fiatBalance.plus(acc), new BigNumber(0) diff --git a/packages/uikit/src/state/home.ts b/packages/uikit/src/state/home.ts index 6e54d2255..785184a53 100644 --- a/packages/uikit/src/state/home.ts +++ b/packages/uikit/src/state/home.ts @@ -11,10 +11,15 @@ import { TRON_TRX_ASSET, TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; -import { jettonToTonAssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; +import { + extraBalanceToTonAsset, + jettonToTonAssetAmount, + TonAsset +} from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { packAssetId } from '@tonkeeper/core/dist/entries/crypto/asset/basic-asset'; import { useTronBalances } from './tron/tron'; import { assertUnreachable } from '@tonkeeper/core/dist/utils/types'; +import { Address } from '@ton/core'; export const useAssets = () => { const wallet = useActiveWallet(); @@ -25,6 +30,7 @@ export const useAssets = () => { const assets = useMemo(() => { if (!info || !jettons || tronBalances === undefined) return undefined; + return { ton: { info, jettons: jettons ?? { balances: [] } }, tron: tronBalances @@ -44,6 +50,17 @@ export const useAllChainsAssets = () => { const result: AssetAmount[] = [ new AssetAmount({ asset: TON_ASSET, weiAmount: assets.ton.info.balance }) ]; + + if (assets.ton.info.extraBalance) { + for (let extra of assets.ton.info.extraBalance) { + const asset = new AssetAmount({ + weiAmount: new BigNumber(extra.amount), + asset: extraBalanceToTonAsset(extra) + }); + result.push(asset); + } + } + config.pinnedTokens.forEach(p => { if (p === TRON_USDT_ASSET.address && assets.tron) { result.push(assets.tron.usdt); @@ -85,10 +102,19 @@ export const useAssetWeiBalance = (asset: AssetIdentification) => { if (asset.address === 'TON') { return new BigNumber(assets.ton.info.balance); } - const jAddress = asset.address.toRawString(); - return new BigNumber( - assets.ton.jettons.balances.find(i => i.jetton.address === jAddress)?.balance || '0' - ); + if (Address.isAddress(asset.address)) { + const jAddress = asset.address.toRawString(); + return new BigNumber( + assets.ton.jettons.balances.find(i => i.jetton.address === jAddress)?.balance || + '0' + ); + } else { + return new BigNumber( + assets.ton.info.extraBalance?.find( + item => item.preview.symbol === asset.address + )?.amount || '0' + ); + } } else if (asset.blockchain === BLOCKCHAIN_NAME.TRON) { if (asset.address === TRON_USDT_ASSET.address) { return assets.tron?.usdt.weiAmount || new BigNumber(0); diff --git a/packages/uikit/src/state/multiSend.ts b/packages/uikit/src/state/multiSend.ts index 52ab468ea..e16c34495 100644 --- a/packages/uikit/src/state/multiSend.ts +++ b/packages/uikit/src/state/multiSend.ts @@ -150,13 +150,16 @@ export const useParseCsvListMutation = () => { let token = TON_ASSET; if (crypto && !isTon(crypto)) { + if (!Address.isAddress(crypto)) { + throw new Error('Unable to get token info for extra currency.'); + } const response = await new JettonsApi(api.tonApiV2).getJettonInfo({ accountId: crypto.toRawString() }); token = { address: crypto, - image: response.metadata.image, + image: response.preview, blockchain: BLOCKCHAIN_NAME.TON, name: response.metadata.name, symbol: response.metadata.symbol, diff --git a/packages/uikit/src/state/swap/useCalculatedSwap.ts b/packages/uikit/src/state/swap/useCalculatedSwap.ts index 29321a837..5bfec5377 100644 --- a/packages/uikit/src/state/swap/useCalculatedSwap.ts +++ b/packages/uikit/src/state/swap/useCalculatedSwap.ts @@ -6,7 +6,7 @@ import { } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; import type { SwapService } from '@tonkeeper/core/dist/swapsApi'; -import { JettonsApi } from '@tonkeeper/core/dist/tonApiV2'; +import { AccountsApi, JettonsApi } from '@tonkeeper/core/dist/tonApiV2'; import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; import { packAssetId } from '@tonkeeper/core/dist/entries/crypto/asset/basic-asset'; import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; @@ -27,6 +27,7 @@ import { APIConfig } from '@tonkeeper/core/dist/entries/apis'; import { atom, useAtom } from '../../libs/atom'; import { useSwapsConfig } from './useSwapsConfig'; import { useActiveApi } from '../wallet'; +import { add } from '@amplitude/analytics-browser'; export type BasicCalculatedTrade = { from: AssetAmount; @@ -183,7 +184,7 @@ export function useCalculatedSwap() { } const toTradeAssetId = (address: TonAssetAddress) => { - return isTon(address) ? 'ton' : address.toRawString(); + return isTon(address) ? 'ton' : Address.isAddress(address) ? address.toRawString() : address; }; const fromTradeAssetId = (address: string): TonAssetAddress => { @@ -301,19 +302,23 @@ const getAsset = async (api: APIConfig, address: TonAssetAddress): Promise - ({ - symbol: response.metadata.symbol, - decimals: Number(response.metadata.decimals), - name: response.metadata.name, - blockchain: BLOCKCHAIN_NAME.TON, - address, - id: packAssetId(BLOCKCHAIN_NAME.TON, address), - image: response.metadata.image - } as const) - ); - swapAssetsCache.set(address, p); - return p; + if (Address.isAddress(address)) { + const tonapi = new JettonsApi(api.tonApiV2); + const p = tonapi.getJettonInfo({ accountId: address.toRawString() }).then( + response => + ({ + symbol: response.metadata.symbol, + decimals: Number(response.metadata.decimals), + name: response.metadata.name, + blockchain: BLOCKCHAIN_NAME.TON, + address, + id: packAssetId(BLOCKCHAIN_NAME.TON, address), + image: response.preview + } as const) + ); + swapAssetsCache.set(address, p); + return p; + } else { + throw new Error('Unable to get asset info for extra currency.'); + } }; diff --git a/packages/uikit/src/state/swap/useSwapAssets.ts b/packages/uikit/src/state/swap/useSwapAssets.ts index fe7abc65a..234019c1b 100644 --- a/packages/uikit/src/state/swap/useSwapAssets.ts +++ b/packages/uikit/src/state/swap/useSwapAssets.ts @@ -180,7 +180,7 @@ export const useSwapCustomTokenSearch = () => { const tonAsset: TonAsset = { address, - image: response.metadata.image, + image: response.preview, blockchain: BLOCKCHAIN_NAME.TON, name: response.metadata.name, symbol: response.metadata.symbol, diff --git a/packages/uikit/src/state/ton/tonActivity.ts b/packages/uikit/src/state/ton/tonActivity.ts index 2e168c7dd..331cfb377 100644 --- a/packages/uikit/src/state/ton/tonActivity.ts +++ b/packages/uikit/src/state/ton/tonActivity.ts @@ -43,3 +43,10 @@ export const groupAndFilterTonActivityItems = ( pageParams: [] }; }; + +export const seeIfExtraCurrencyTransfer = (symbol: string) => (action: Action) => { + return ( + action.type === 'ExtraCurrencyTransfer' && + action.extraCurrencyTransfer?.currency.symbol === symbol + ); +};