Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extra currency support #362

Merged
merged 11 commits into from
Jan 20, 2025
7 changes: 0 additions & 7 deletions .github/workflows/ipad-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion apps/extension/src/provider/tonconnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export const getDeviceInfo = (): DeviceInfo => {
'SendTransaction',
{
name: 'SendTransaction',
maxMessages: 4
maxMessages: 4,
extraCurrenciesSupported: true
}
]
};
Expand Down
37 changes: 22 additions & 15 deletions apps/twa/src/components/transfer/SendNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -249,7 +250,6 @@ const SendContent: FC<{
confirm: confirmRef
}[view];


const assetAmount = useMemo(() => {
if (!amountViewState?.token || !amountViewState?.coinValue) {
return null;
Expand All @@ -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 (
Expand Down Expand Up @@ -348,6 +348,7 @@ export const TwaSendNotification: FC<PropsWithChildren> = ({ children }) => {
const { data: jettons } = useJettonList();

const { mutateAsync: getAccountAsync, reset } = useGetToAccount();
const wallet = useActiveWallet();

const sdk = useAppSdk();
const track = useAnalyticsTrack();
Expand All @@ -371,15 +372,21 @@ export const TwaSendNotification: FC<PropsWithChildren> = ({ 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 });
};

Expand Down
63 changes: 56 additions & 7 deletions packages/core/src/entries/crypto/asset/ton-asset.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}
}
2 changes: 2 additions & 0 deletions packages/core/src/entries/tonConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -169,6 +170,7 @@ export enum SEND_TRANSACTION_ERROR_CODES {
export type SendTransactionFeature = {
name: 'SendTransaction';
maxMessages: number;
extraCurrenciesSupported?: boolean;
};

export type SendTransactionFeatureDeprecated = 'SendTransaction';
Expand Down
70 changes: 70 additions & 0 deletions packages/core/src/service/ton-blockchain/encoder/encoder-base.ts
Original file line number Diff line number Diff line change
@@ -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
};
}
}
Original file line number Diff line number Diff line change
@@ -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<WalletOutgoingMessage> => {
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<WalletOutgoingMessage> => {
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<WalletOutgoingMessage> => {
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
};
};
}
Loading
Loading