From 0640ccbb9b1b061ef201ae5e759c3126bca560ef Mon Sep 17 00:00:00 2001
From: TomerHFB <158162596+TomerHFB@users.noreply.github.com>
Date: Thu, 12 Dec 2024 21:06:35 +0200
Subject: [PATCH] feat: :sparkles: add jetton withdrawal support
---
.../WithdrawModal/CreateTransaction/index.tsx | 19 +-
.../components/WithdrawModal/index.tsx | 4 +-
apps/recovery-relay/context/Workspace.tsx | 10 +-
apps/recovery-relay/lib/defaultRPCs.ts | 6 +
.../lib/wallets/Jetton/index.ts | 186 ++++++++++++++++++
apps/recovery-relay/lib/wallets/TON/index.ts | 7 +-
apps/recovery-relay/lib/wallets/index.ts | 4 +
apps/recovery-relay/lib/wallets/types.ts | 1 +
.../WithdrawModal/SignTransaction/index.tsx | 4 +-
.../renderer/context/Workspace.tsx | 10 +-
.../renderer/lib/wallets/Jetton/index.ts | 72 +++++++
.../renderer/lib/wallets/TON/index.ts | 3 +-
.../renderer/lib/wallets/index.ts | 16 ++
packages/asset-config/config/patches.ts | 50 +++--
packages/asset-config/data/globalAssets.ts | 118 +++++++++++
packages/asset-config/util.ts | 8 +
packages/e2e-tests/tests.ts | 3 +
.../useBaseWorkspace/reduceDerivations.ts | 2 +-
packages/shared/lib/validateAddress.ts | 3 +
packages/wallet-derivation/index.ts | 1 +
.../wallets/chains/Jetton.ts | 8 +
.../wallet-derivation/wallets/chains/index.ts | 5 +
yarn.lock | 8 +-
23 files changed, 517 insertions(+), 31 deletions(-)
create mode 100644 apps/recovery-relay/lib/wallets/Jetton/index.ts
create mode 100644 apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
create mode 100644 packages/wallet-derivation/wallets/chains/Jetton.ts
diff --git a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
index 13753b1d..b0f9cf86 100644
--- a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
@@ -22,6 +22,7 @@ import { useWorkspace } from '../../../context/Workspace';
import { Derivation, AccountData } from '../../../lib/wallets';
import { LateInitConnectedWallet } from '../../../lib/wallets/LateInitConnectedWallet';
import { useSettings } from '../../../context/Settings';
+import { Jetton } from '../../../lib/wallets/Jetton';
const logger = getLogger(LOGGER_NAME_RELAY);
@@ -149,7 +150,7 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
const fromAddress = values.fromAddress ?? defaultValues.fromAddress;
- const derivation = wallet?.derivations?.get(fromAddress);
+ const derivation = wallet?.derivations?.get(`${asset?.id}-${fromAddress}`); // fix for token support
// TODO: Show both original balance and adjusted balance in create tx UI
@@ -166,6 +167,11 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
throw new Error(`No RPC Url for: ${derivation?.assetId}`);
}
if (rpcUrl !== null) derivation!.setRPCUrl(rpcUrl);
+ if (asset.address && asset.protocol === 'TON') {
+ (derivation as Jetton).setTokenAddress(asset.address);
+ (derivation as Jetton).setDecimals(asset.decimals);
+ }
+
return await derivation!.prepare?.(toAddress, values.memo);
},
onSuccess: (prepare: AccountData) => {
@@ -421,6 +427,10 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
theme.palette.error.main}>
Insufficient balance for transaction
+ ) : prepareQuery.data?.insufficientFeeBalance === true && prepareQuery.data?.insufficientBalance !== true ? (
+ theme.palette.error.main}>
+ Insufficient fee asset balance for token transaction
+
) : (
''
)}
@@ -430,7 +440,12 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
type='submit'
disabled={
!prepareQuery.data?.balance ||
- (prepareQuery.data && prepareQuery.data?.insufficientBalance !== undefined && prepareQuery.data.insufficientBalance)
+ (prepareQuery.data &&
+ prepareQuery.data?.insufficientBalance !== undefined &&
+ prepareQuery.data.insufficientBalance) ||
+ (prepareQuery.data &&
+ prepareQuery.data?.insufficientFeeBalance !== undefined &&
+ prepareQuery.data.insufficientFeeBalance)
}
>
Prepare Transaction
diff --git a/apps/recovery-relay/components/WithdrawModal/index.tsx b/apps/recovery-relay/components/WithdrawModal/index.tsx
index bb4b1533..f42edb70 100644
--- a/apps/recovery-relay/components/WithdrawModal/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/index.tsx
@@ -127,7 +127,9 @@ export const WithdrawModal = () => {
.get(inboundRelayParams?.accountId)
?.wallets.get(inboundRelayParams?.signedTx.assetId);
- const derivation = wallet?.derivations?.get(inboundRelayParams?.signedTx.from);
+ const derivation = wallet?.derivations?.get(
+ `${inboundRelayParams?.signedTx.assetId}-${inboundRelayParams?.signedTx.from}`,
+ ); // fix for token support
const rpcUrl = getAssetURL(derivation?.assetId ?? '', RPCs);
if (rpcUrl === undefined) {
diff --git a/apps/recovery-relay/context/Workspace.tsx b/apps/recovery-relay/context/Workspace.tsx
index 88dab046..7a44abf6 100644
--- a/apps/recovery-relay/context/Workspace.tsx
+++ b/apps/recovery-relay/context/Workspace.tsx
@@ -14,6 +14,7 @@ import { getAssetConfig } from '@fireblocks/asset-config';
import packageJson from '../package.json';
import { WalletClasses, Derivation } from '../lib/wallets';
import { LOGGER_NAME_RELAY } from '@fireblocks/recovery-shared/constants';
+import { isTransferableToken } from '@fireblocks/asset-config/util';
type DistributiveOmit = T extends any ? Omit : never;
@@ -61,7 +62,6 @@ const getInboundRelayWalletIds = (inboundRelayParams?: RelayRequestParams) => {
return null;
}
- logger.info('Inbound Relay params', inboundRelayParams);
let ret;
switch (inboundRelayParams.action) {
case 'import':
@@ -116,6 +116,14 @@ export const WorkspaceProvider = ({ children }: Props) => {
app: 'relay',
relayBaseUrl: 'fireblocks-recovery:/',
deriveWallet: (input) => {
+ if (isTransferableToken(input.assetId)) {
+ if (input.assetId in WalletClasses) {
+ return new WalletClasses[input.assetId as keyof typeof WalletClasses](input, 0);
+ } else {
+ throw new Error(`Unsupported token: ${input.assetId}`);
+ }
+ }
+
const nativeAssetId = (getAssetConfig(input.assetId)?.nativeAsset ?? input.assetId) as keyof typeof WalletClasses;
if (nativeAssetId in WalletClasses) {
diff --git a/apps/recovery-relay/lib/defaultRPCs.ts b/apps/recovery-relay/lib/defaultRPCs.ts
index aa211042..cdacbf68 100644
--- a/apps/recovery-relay/lib/defaultRPCs.ts
+++ b/apps/recovery-relay/lib/defaultRPCs.ts
@@ -168,4 +168,10 @@ export const defaultRPCs: Record<
enabled: true,
allowedEmptyValue: false,
},
+ Jetton: {
+ url: 'https://toncenter.com/api/v2/jsonRPC',
+ name: 'The Open Network',
+ enabled: true,
+ allowedEmptyValue: false,
+ },
};
diff --git a/apps/recovery-relay/lib/wallets/Jetton/index.ts b/apps/recovery-relay/lib/wallets/Jetton/index.ts
new file mode 100644
index 00000000..1f20373b
--- /dev/null
+++ b/apps/recovery-relay/lib/wallets/Jetton/index.ts
@@ -0,0 +1,186 @@
+import { Ton as BaseTon } from '@fireblocks/wallet-derivation';
+import { JettonMaster, TonClient, WalletContractV4 } from '@ton/ton';
+import { Address, beginCell, Cell, fromNano, toNano } from '@ton/core';
+import { AccountData } from '../types';
+import { defaultTonWalletV4R2code } from '../TON/tonParams';
+import axios from 'axios';
+import { LateInitConnectedWallet } from '../LateInitConnectedWallet';
+
+export class Jetton extends BaseTon implements LateInitConnectedWallet {
+ public memo: string | undefined;
+ public tokenAddress: string | undefined;
+ public decimals: number | undefined;
+
+ public setTokenAddress(address: string) {
+ this.tokenAddress = address;
+ }
+
+ public setDecimals(decimals: number) {
+ this.decimals = decimals;
+ }
+
+ public updateDataEndpoint(memo?: string): void {
+ this.memo = memo;
+ }
+
+ public getLateInitLabel(): string {
+ throw new Error('Method not implemented.');
+ }
+
+ public rpcURL: string | undefined;
+
+ public setRPCUrl(url: string): void {
+ this.rpcURL = url;
+ }
+
+ private client: TonClient | undefined;
+
+ private init() {
+ this.client = new TonClient({
+ endpoint: this.rpcURL!,
+ });
+ }
+
+ private tonWallet = WalletContractV4.create({ publicKey: Buffer.from(this.publicKey.replace('0x', ''), 'hex'), workchain: 0 });
+
+ private async getContractAddress(): Promise {
+ const jettonMasterAddress = Address.parse(this.tokenAddress!);
+ const walletAddress = Address.parse(this.address);
+ const jettonMaster = this?.client?.open(JettonMaster.create(jettonMasterAddress));
+ return await jettonMaster?.getWalletAddress(walletAddress);
+ }
+
+ public async getBalance(): Promise {
+ if (this.client) {
+ if (!this.tokenAddress) {
+ this.relayLogger.error('TON Jettons: Jetton token address unavailable');
+ throw new Error('TON Jettons: Jetton token address unavailable');
+ }
+
+ const contractAddress = await this.getContractAddress();
+ if (!contractAddress) {
+ this.relayLogger.error(`TON Jettons: wallet's contract address unavailable`);
+ throw new Error(`TON Jettons: wallet's contract address unavailable`);
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ const { stack } = await this.client.runMethod(contractAddress, 'get_wallet_data');
+ const normalizingFactor = 10 ** this.decimals!;
+
+ return stack.readNumber() / normalizingFactor;
+ } else {
+ this.relayLogger.error('TON Jettons: Client failed to initialize');
+ throw new Error('TON Jettons: Client failed to initialize');
+ }
+ }
+
+ public async broadcastTx(tx: string): Promise {
+ try {
+ // init the TonClient
+ this.init();
+
+ // parse the tx back to Ton Cell
+ const body = Cell.fromBoc(Buffer.from(tx, 'base64'))[0];
+ const pubKey = Buffer.from(this.publicKey.replace('0x', ''), 'hex');
+ const externalMessage = beginCell()
+ .storeUint(0b10, 2)
+ .storeUint(0, 2)
+ .storeAddress(Address.parse(this.address))
+ .storeCoins(0);
+
+ const seqno = await this.getSeqno();
+ if (seqno === 0) {
+ // for the fist transaction we initialize a state init struct which consists of init struct and code
+ externalMessage
+ .storeBit(1) // We have State Init
+ .storeBit(1) // We store State Init as a reference
+ .storeRef(await this.createStateInit(pubKey)); // Store State Init as a reference
+ } else {
+ externalMessage.storeBit(0); // We don't have state init
+ }
+ const finalExternalMessage = externalMessage.storeBit(1).storeRef(body).endCell();
+
+ if (this.client) {
+ // broadcast Tx and calc TxHash
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ await this.client.sendFile(finalExternalMessage.toBoc());
+ const txHash = finalExternalMessage.hash().toString('hex');
+ this.relayLogger.debug(`Jetton: Tx broadcasted: ${txHash}`);
+ return txHash;
+ } else {
+ throw new Error('Jetton: Client failed to initialize');
+ }
+ } catch (e) {
+ this.relayLogger.error(`Jetton: Error broadcasting tx: ${e}`);
+ if (axios.isAxiosError(e)) {
+ this.relayLogger.error(`Axios error: ${e.message}\n${e.response?.data}`);
+ }
+ throw e;
+ }
+ }
+
+ public async prepare(): Promise {
+ // init the TonClient
+ this.init();
+
+ const jettonBalance = await this.getBalance();
+ const contract = this.client!.open(this.tonWallet);
+ const tonBalance = await contract.getBalance();
+
+ // fee for token tx is hardcoded to 0.1 TON
+ const feeRate = Number(toNano(0.1));
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+
+ // get seqno of the wallet, set it as exrtaParams
+ const seqno = await this.getSeqno();
+
+ // get the contract address of the wallet
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ const contractAddress = await this.getContractAddress();
+
+ // set extraParams
+ const extraParams = new Map();
+ extraParams.set('seqno', seqno);
+ extraParams.set('contract-address', contractAddress?.toString({ bounceable: true, testOnly: false }));
+ extraParams.set('decimals', this.decimals);
+
+ const preperedData = {
+ balance: jettonBalance,
+ memo: this.memo,
+ feeRate,
+ extraParams,
+ insufficientBalance: jettonBalance <= 0,
+ insufficientFeeBalance: tonBalance < feeRate,
+ } as AccountData;
+
+ return preperedData;
+ }
+ private async getSeqno() {
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ return await this.client!.open(this.tonWallet).getSeqno();
+ }
+
+ private async createStateInit(pubKey: Buffer): Promise {
+ // the initial data cell our contract will hold. Wallet V4 has an extra value for plugins in the end
+ const dataCell = beginCell()
+ .storeUint(await this.getSeqno(), 32) // Seqno 0 for the first tx
+ .storeUint(698983191, 32) // Subwallet ID -> https://docs.ton.org/v3/guidelines/smart-contracts/howto/wallet#subwallet-ids
+ .storeBuffer(pubKey)
+ .storeBit(0) // only for Wallet V4
+ .endCell();
+
+ // we take a boiler place already made WalletV4R2 code
+ const codeCell = Cell.fromBoc(Buffer.from(defaultTonWalletV4R2code, 'base64'))[0];
+ const stateInit = beginCell()
+ .storeBit(0) // No split_depth
+ .storeBit(0) // No special
+ .storeBit(1) // We have code
+ .storeRef(codeCell)
+ .storeBit(1) // We have data
+ .storeRef(dataCell)
+ .storeBit(0) // No library
+ .endCell();
+ return stateInit;
+ }
+}
diff --git a/apps/recovery-relay/lib/wallets/TON/index.ts b/apps/recovery-relay/lib/wallets/TON/index.ts
index 4372429e..e403ce6f 100644
--- a/apps/recovery-relay/lib/wallets/TON/index.ts
+++ b/apps/recovery-relay/lib/wallets/TON/index.ts
@@ -1,6 +1,6 @@
import { Ton as BaseTon } from '@fireblocks/wallet-derivation';
import { TonClient, WalletContractV4 } from '@ton/ton';
-import { beginCell, Cell, fromNano } from '@ton/core';
+import { beginCell, Cell, fromNano, toNano } from '@ton/core';
import { AccountData } from '../types';
import { defaultTonWalletV4R2code } from './tonParams';
import axios from 'axios';
@@ -35,7 +35,6 @@ export class Ton extends BaseTon implements LateInitConnectedWallet {
public async getBalance(): Promise {
if (this.client) {
- await new Promise((resolve) => setTimeout(resolve, 2000));
const contract = this.client.open(this.tonWallet);
return Number(fromNano(await contract.getBalance()));
} else {
@@ -91,7 +90,7 @@ export class Ton extends BaseTon implements LateInitConnectedWallet {
const balance = await this.getBalance();
// fee for regular tx is hardcoded to 0.02 TON
- const feeRate = 0.02;
+ const feeRate = Number(toNano(0.02));
await new Promise((resolve) => setTimeout(resolve, 2000));
// get seqno of the wallet, set it as exrtaParams
@@ -104,7 +103,7 @@ export class Ton extends BaseTon implements LateInitConnectedWallet {
memo: this.memo,
feeRate,
extraParams,
- insufficientBalance: balance < 0.005,
+ insufficientBalance: Number(toNano(balance)) - feeRate < Number(toNano(0.005)), // 0.005 is minimum amount for transfer
} as AccountData;
return preperedData;
diff --git a/apps/recovery-relay/lib/wallets/index.ts b/apps/recovery-relay/lib/wallets/index.ts
index 4c109e90..e29ad57e 100644
--- a/apps/recovery-relay/lib/wallets/index.ts
+++ b/apps/recovery-relay/lib/wallets/index.ts
@@ -37,6 +37,7 @@ import { Algorand } from './ALGO';
import { Celestia } from './CELESTIA';
import { CoreDAO } from './EVM/CORE_COREDAO';
import { Ton } from './TON';
+import { Jetton } from './Jetton';
export { ConnectedWallet } from './ConnectedWallet';
export const WalletClasses = {
@@ -121,6 +122,9 @@ export const WalletClasses = {
CELESTIA_TEST: Celestia,
TON: Ton,
TON_TEST: Ton,
+ USDT_TON: Jetton,
+ NOTCOIN_TON: Jetton,
+ DOGS_TON: Jetton,
} as const;
type WalletClass = (typeof WalletClasses)[keyof typeof WalletClasses];
diff --git a/apps/recovery-relay/lib/wallets/types.ts b/apps/recovery-relay/lib/wallets/types.ts
index 55ba2442..2afd0cef 100644
--- a/apps/recovery-relay/lib/wallets/types.ts
+++ b/apps/recovery-relay/lib/wallets/types.ts
@@ -28,6 +28,7 @@ export type AccountData = {
extraParams?: Map;
endpoint?: string;
insufficientBalance?: boolean;
+ insufficientFeeBalance?: boolean;
};
export type TxPayload = {
diff --git a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx
index afd3db5a..cd2e1077 100644
--- a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx
+++ b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx
@@ -73,7 +73,9 @@ export const SignTransaction = ({ txId, account, asset, inboundRelayParams }: Pr
const { to, amount, misc } = unsignedTx;
- const derivation = account.wallets.get(asset.id)?.derivations.get(unsignedTx.from);
+ const derivation = account.wallets
+ .get(asset.id)
+ ?.derivations.get(`${inboundRelayParams?.unsignedTx.assetId}-${inboundRelayParams?.unsignedTx.from}`);
if (!derivation) {
throw new Error('Derivation not found');
diff --git a/apps/recovery-utility/renderer/context/Workspace.tsx b/apps/recovery-utility/renderer/context/Workspace.tsx
index 6b39ac4e..f901a940 100644
--- a/apps/recovery-utility/renderer/context/Workspace.tsx
+++ b/apps/recovery-utility/renderer/context/Workspace.tsx
@@ -17,6 +17,7 @@ import { handleRelayUrl } from '../lib/ipc/handleRelayUrl';
import { SigningWallet } from '../lib/wallets/SigningWallet';
import { useSettings } from './Settings';
import { LOGGER_NAME_UTILITY } from '@fireblocks/recovery-shared/constants';
+import { isTransferableToken } from '@fireblocks/asset-config/util';
type DistributiveOmit = T extends any ? Omit : never;
@@ -65,7 +66,14 @@ export const WorkspaceProvider = ({ children }: Props) => {
app: 'utility',
relayBaseUrl,
deriveWallet: (input) => {
- const nativeAssetId = (getAssetConfig(input.assetId)?.nativeAsset ?? input.assetId) as keyof typeof WalletClasses;
+ let transferableToken = false;
+ const config = getAssetConfig(input.assetId);
+ if (config?.address && isTransferableToken(input.assetId)) {
+ transferableToken = true;
+ }
+ const nativeAssetId = (
+ transferableToken ? input.assetId : config?.nativeAsset ?? input.assetId
+ ) as keyof typeof WalletClasses;
logger.info('Deriving native asset', nativeAssetId);
diff --git a/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts b/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
new file mode 100644
index 00000000..4328439d
--- /dev/null
+++ b/apps/recovery-utility/renderer/lib/wallets/Jetton/index.ts
@@ -0,0 +1,72 @@
+import { Ton as BaseTon, Input } from '@fireblocks/wallet-derivation';
+import { SigningWallet } from '../SigningWallet';
+import { Address, beginCell, Cell, SendMode } from '@ton/core';
+import { GenerateTxInput, TxPayload } from '../types';
+
+export class Jetton extends BaseTon implements SigningWallet {
+ constructor(input: Input) {
+ super(input);
+ }
+
+ public async generateTx({ to, amount, feeRate, memo, extraParams }: GenerateTxInput): Promise {
+ // check for jetton extra params (contract address, seqno and decimals)
+ if (!extraParams?.get('contract-address') || !extraParams?.get('decimals') || !extraParams?.get('seqno')) {
+ throw new Error('Jetton: Missing jetton parameters');
+ }
+
+ const JettonTransferOpcode = 0x0f8a7ea5;
+ const decimals = extraParams?.get('decimals');
+ const normalizingFactor = 10 ** decimals;
+ const amountToWithdraw = amount * normalizingFactor; // amount is the wallet balance
+
+ let internalMessageMemo = undefined;
+ // create the tx payload
+ const internalMessageBody = beginCell()
+ .storeUint(JettonTransferOpcode, 32) // opcode for jetton transfer
+ .storeUint(0, 64) // query id
+ .storeCoins(amountToWithdraw) // jetton balance
+ .storeAddress(Address.parse(to)) // tx destination
+ .storeAddress(Address.parse(this.address)) // excess fees sent back to native ton wallet
+ .storeBit(0); // no custom payload
+ if (memo) {
+ internalMessageMemo = beginCell().storeUint(0, 32).storeStringTail(memo).endCell();
+ internalMessageBody
+ .storeCoins(1) // forward amount - if >0, will send notification message
+ .storeBit(1) // we store forwardPayload as a reference
+ .storeRef(internalMessageMemo)
+ .endCell();
+ } else {
+ internalMessageBody
+ .storeCoins(0) // no memo added
+ .storeBit(0)
+ .endCell();
+ }
+ const sendMode = SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS;
+
+ const contractAddress = extraParams?.get('contract-address');
+ const internalMessage = beginCell()
+ .storeUint(0x18, 6) // bounceable tx
+ .storeAddress(Address.parse(contractAddress)) //wallet Jetton contract address
+ .storeCoins(BigInt(feeRate!))
+ .storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) // We store 1 that means we have body as a reference
+ .storeRef(internalMessageBody)
+ .endCell();
+ const toSign = beginCell()
+ .storeUint(698983191, 32) // Subwallet ID -> https://docs.ton.org/v3/guidelines/smart-contracts/howto/wallet#subwallet-ids
+ .storeUint(Math.floor(Date.now() / 1e3) + 600, 32) // Transaction expiration time, +600 = 10 minute
+ .storeUint(extraParams?.get('seqno'), 32) // store seqno
+ .storeUint(0, 8)
+ .storeUint(sendMode, 8)
+ .storeRef(internalMessage) // store our internalMessage as a reference
+ .endCell();
+
+ const signMessage = toSign.toBoc().toString('base64');
+ const signData = toSign.hash();
+
+ const signature = Buffer.from(await this.sign(Uint8Array.from(signData))).toString('base64');
+ const unsignedTx = Cell.fromBase64(signMessage).asBuilder();
+
+ const body = beginCell().storeBuffer(Buffer.from(signature, 'base64')).storeBuilder(unsignedTx).endCell();
+ return { tx: body.toBoc().toString('base64') };
+ }
+}
diff --git a/apps/recovery-utility/renderer/lib/wallets/TON/index.ts b/apps/recovery-utility/renderer/lib/wallets/TON/index.ts
index 3608fa2a..ddb6ae67 100644
--- a/apps/recovery-utility/renderer/lib/wallets/TON/index.ts
+++ b/apps/recovery-utility/renderer/lib/wallets/TON/index.ts
@@ -10,8 +10,7 @@ export class Ton extends BaseTon implements SigningWallet {
public async generateTx({ to, amount, feeRate, memo, extraParams }: GenerateTxInput): Promise {
// calculate the amount to withdraw
- const fee = BigInt(toNano(feeRate!)); // feeRate as BigInt in nano
- const amountToWithdraw = BigInt(toNano(amount)) - fee; // amount is the wallet balance
+ const amountToWithdraw = BigInt(toNano(amount)) - BigInt(feeRate!);
let internalMessageMemo = undefined;
if (memo) {
internalMessageMemo = beginCell().storeUint(0, 32).storeStringTail(memo).endCell();
diff --git a/apps/recovery-utility/renderer/lib/wallets/index.ts b/apps/recovery-utility/renderer/lib/wallets/index.ts
index 59a5a3e1..956cc6da 100644
--- a/apps/recovery-utility/renderer/lib/wallets/index.ts
+++ b/apps/recovery-utility/renderer/lib/wallets/index.ts
@@ -21,6 +21,7 @@ import { Algorand } from './ALGO';
import { Bitcoin, BitcoinSV, LiteCoin, Dash, ZCash, Doge } from './BTC';
import { Celestia } from './CELESTIA';
import { Ton } from './TON';
+import { Jetton } from './Jetton';
const fillEVMs = () => {
const evms = Object.keys(assets).reduce(
@@ -39,6 +40,18 @@ const fillEVMs = () => {
return evms;
};
+// const fillJettons = () => {
+// const jettons = Object.keys(assets).reduce(
+// (o, assetId) => ({
+// ...o,
+// [assets[assetId].id]: assets[assetId].protocol === 'TON' && assets[assetId].address ? Ton : undefined,
+// }),
+// {},
+// ) as any;
+// Object.keys(jettons).forEach((key) => (jettons[key] === undefined ? delete jettons[key] : {}));
+// return jettons;
+// };
+
export { SigningWallet as BaseWallet } from './SigningWallet';
export const WalletClasses = {
@@ -98,6 +111,9 @@ export const WalletClasses = {
HBAR_TEST: Hedera,
TON: Ton,
TON_TEST: Ton,
+ USDT_TON: Jetton,
+ NOTCOIN_TON: Jetton,
+ DOGS_TON: Jetton,
...fillEVMs(),
} as const;
diff --git a/packages/asset-config/config/patches.ts b/packages/asset-config/config/patches.ts
index 2a4c4252..e70c2348 100644
--- a/packages/asset-config/config/patches.ts
+++ b/packages/asset-config/config/patches.ts
@@ -24,6 +24,13 @@ const getSolanaExplorer: ExplorerUrlBuilder = (cluster?: string) => (type) => (v
return `${baseUrl}?cluster=${cluster}`;
};
+const getTonExplorerUrl: ExplorerUrlBuilder = (baseUrl) => (type) => (value) => {
+ if (type === 'tx') {
+ return `${baseUrl}/transaction/${value}`;
+ }
+
+ return `${baseUrl}/${value}`;
+};
const evm = (baseExplorerUrl: string, rpcUrl?: string, transfer = true): NativeAssetPatch => ({
derive: true,
@@ -344,13 +351,7 @@ export const nativeAssetPatches: NativeAssetPatches = {
segwit: false,
minBalance: true,
memo: true,
- getExplorerUrl: (type) => (value) => {
- if (type === 'tx') {
- return `https://tonviewer.com/transaction/${value}`;
- }
-
- return `https://tonviewer.com/${value}`;
- },
+ getExplorerUrl: getTonExplorerUrl('https://tonviewer.com'),
},
TON_TEST: {
derive: true,
@@ -359,12 +360,33 @@ export const nativeAssetPatches: NativeAssetPatches = {
segwit: false,
minBalance: true,
memo: true,
- getExplorerUrl: (type) => (value) => {
- if (type === 'tx') {
- return `https://testnet.tonviewer.com/transaction/${value}`;
- }
-
- return `https://testnet.tonviewer.com/${value}`;
- },
+ getExplorerUrl: getTonExplorerUrl('https://testnet/tonviewer.com'),
+ },
+ DOGS_TON: {
+ derive: true,
+ transfer: true,
+ utxo: false,
+ segwit: false,
+ minBalance: true,
+ memo: true,
+ getExplorerUrl: getTonExplorerUrl('https://tonviewer.com'),
+ },
+ USDT_TON: {
+ derive: true,
+ transfer: true,
+ utxo: false,
+ segwit: false,
+ minBalance: true,
+ memo: true,
+ getExplorerUrl: getTonExplorerUrl('https://tonviewer.com'),
+ },
+ NOTCOIN_TON: {
+ derive: true,
+ transfer: true,
+ utxo: false,
+ segwit: false,
+ minBalance: true,
+ memo: true,
+ getExplorerUrl: getTonExplorerUrl('https://tonviewer.com'),
},
};
diff --git a/packages/asset-config/data/globalAssets.ts b/packages/asset-config/data/globalAssets.ts
index daf3e646..3e38c599 100644
--- a/packages/asset-config/data/globalAssets.ts
+++ b/packages/asset-config/data/globalAssets.ts
@@ -2022,6 +2022,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'BUIDL_ETH_X3N7',
+ symbol: 'BUIDL',
+ name: 'BlackRock USD Institutional Digital Liquidity Fund',
+ decimals: 6,
+ address: '0x7712c34205737192402172409a8F7ccef8aA2AEc',
+ nativeAsset: 'ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'BURGER_BSC',
symbol: 'BURGER',
@@ -2198,6 +2208,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'CAT_B71VABWJ_BY1A',
+ symbol: 'CAT',
+ name: 'Simons Cat',
+ decimals: 18,
+ address: '0x6894CDe390a3f51155ea41Ed24a33A4827d3063D',
+ nativeAsset: 'BNB_BSC',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'CAVE_SOL',
symbol: 'CAVE',
@@ -3594,6 +3614,16 @@ export const globalAssets = [
protocol: 'BTC',
testnet: true,
},
+ {
+ id: 'DOGS_TON',
+ symbol: 'DOGS',
+ name: 'Dogs (Ton)',
+ decimals: 9,
+ address: 'EQCvxJy4eG8hyHBFsZ7eePxrRsUQSFE_jpptRAYBmcG_DOGS',
+ nativeAsset: 'TON',
+ protocol: 'TON',
+ testnet: false,
+ },
{
id: 'DOPEX',
symbol: 'DOPEX',
@@ -4445,6 +4475,15 @@ export const globalAssets = [
protocol: 'SOL',
testnet: false,
},
+ {
+ id: 'FASTEX_BAHAMUT',
+ symbol: 'FTN',
+ name: 'Fastex Bahamut',
+ decimals: 18,
+ nativeAsset: 'FASTEX_BAHAMUT',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'FB_ATHENA_TEST',
symbol: 'FB_ATHENA_TEST',
@@ -7160,6 +7199,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'NOTCOIN_TON',
+ symbol: 'NOT',
+ name: 'Notcoin (Ton)',
+ decimals: 9,
+ address: 'EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT',
+ nativeAsset: 'TON',
+ protocol: 'TON',
+ testnet: false,
+ },
{
id: 'NOTE_ETH',
symbol: 'NOTE',
@@ -8343,6 +8392,15 @@ export const globalAssets = [
protocol: 'SOL',
testnet: false,
},
+ {
+ id: 'REDBELLY_TEST',
+ symbol: 'RBNT',
+ name: 'Redbelly Test',
+ decimals: 18,
+ nativeAsset: 'REDBELLY_TEST',
+ protocol: 'ETH',
+ testnet: true,
+ },
{
id: 'REEF',
symbol: 'REEF',
@@ -10175,6 +10233,26 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'TOKEN_BSC_RLDP',
+ symbol: 'TOKEN',
+ name: 'TokenFi',
+ decimals: 9,
+ address: '0x4507cEf57C46789eF8d1a19EA45f4216bae2B528',
+ nativeAsset: 'BNB_BSC',
+ protocol: 'ETH',
+ testnet: false,
+ },
+ {
+ id: 'TOKEN_ETH_JIKQ',
+ symbol: 'TOKEN',
+ name: 'TokenFi',
+ decimals: 9,
+ address: '0x4507cEf57C46789eF8d1a19EA45f4216bae2B528',
+ nativeAsset: 'ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'TON',
symbol: 'TON',
@@ -10762,6 +10840,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: true,
},
+ {
+ id: 'USDC_BASECHAIN_ETH_5I5C',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ decimals: 6,
+ address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
+ nativeAsset: 'BASECHAIN_ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'USDC_BSC',
symbol: 'USDC',
@@ -11015,6 +11103,16 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'USDT_TON',
+ symbol: 'USDT',
+ name: 'Tether USD (Ton)',
+ decimals: 6,
+ address: 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs',
+ nativeAsset: 'TON',
+ protocol: 'TON',
+ testnet: false,
+ },
{
id: 'USDT2_AVAX',
symbol: 'USDT',
@@ -11330,6 +11428,26 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+ {
+ id: 'WCT_B6QT1TZK_TZBJ',
+ symbol: 'WCT',
+ name: 'WalletConnect',
+ decimals: 18,
+ address: '0x61cc6aF18C351351148815c5F4813A16DEe7A7E4',
+ nativeAsset: 'ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
+ {
+ id: 'WCT_B7K5S6PF_H7HU',
+ symbol: 'WCT',
+ name: 'WalletConnect',
+ decimals: 18,
+ address: '0xeF4461891DfB3AC8572cCf7C794664A8DD927945',
+ nativeAsset: 'ETH-OPT',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'WDGLD',
symbol: 'WDGLD',
diff --git a/packages/asset-config/util.ts b/packages/asset-config/util.ts
index 9c2f34c7..4f116367 100644
--- a/packages/asset-config/util.ts
+++ b/packages/asset-config/util.ts
@@ -42,6 +42,14 @@ export const isExplorerUrl = (url: string) => {
export const getAssetConfig = (assetId?: string) => (isAssetId(assetId) ? assets[assetId] : undefined);
+export const isTransferableToken = (assetId?: string) => {
+ if (!isAssetId) return false;
+ const assetConfig = assets[assetId!];
+
+ // Is this a non-native asset, has a contract / address that represents it (i.e token) and we enabled the transfer option
+ return assetConfig.nativeAsset !== assetConfig.id && assetConfig.address && assetConfig.transfer;
+};
+
export const getNetworkProtocol = (assetId: string) => getAssetConfig(assetId)?.protocol;
export const getNativeAssetConfig = (assetId?: ID) =>
diff --git a/packages/e2e-tests/tests.ts b/packages/e2e-tests/tests.ts
index 30afd326..35fa8939 100644
--- a/packages/e2e-tests/tests.ts
+++ b/packages/e2e-tests/tests.ts
@@ -100,6 +100,9 @@ const nativeMainnetAssets = [
// { assetId: 'XTZ' },
// { assetId: 'ZEC' },
// { assetId: 'TON' },
+ // { assetId: 'USDT_TON' },
+ // { assetId: 'NOTCOIN_TON' },
+ // { assetId: 'DOGS_TON' },
];
export const testAssets: AssetTestConfig[] = [...nativeTestnetAssets, ...nativeMainnetAssets];
diff --git a/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts b/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts
index 738014f0..d9e4b5ee 100644
--- a/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts
+++ b/packages/shared/hooks/useBaseWorkspace/reduceDerivations.ts
@@ -126,7 +126,7 @@ export const reduceDerivations = (input: Deri
wallet.derivations.delete(address);
}
- wallet.derivations.set(derivation.address, derivation);
+ wallet.derivations.set(`${derivation.assetId}-${derivation.address}`, derivation); // fix for token support
}
// Handle legacy + Segwit derivations
diff --git a/packages/shared/lib/validateAddress.ts b/packages/shared/lib/validateAddress.ts
index fb74d847..71a68e4a 100644
--- a/packages/shared/lib/validateAddress.ts
+++ b/packages/shared/lib/validateAddress.ts
@@ -104,6 +104,9 @@ export class AddressValidator {
return this.validateTERRA(address);
case 'TON':
case 'TON_TEST':
+ case 'USDT_TON':
+ case 'DOGS_TON':
+ case 'NOTCOIN_TON':
return this.validateTON(address);
default:
logger.error(`Unsupported networkProtocol for address validation ${validatorReference}`);
diff --git a/packages/wallet-derivation/index.ts b/packages/wallet-derivation/index.ts
index 39c51d65..3347587a 100644
--- a/packages/wallet-derivation/index.ts
+++ b/packages/wallet-derivation/index.ts
@@ -10,6 +10,7 @@ export const deriveWallet = (input: Input): BaseWallet => {
const WalletInstance = getWallet(nativeAssetId);
// TODO: defaultCoinType should be unnecessary here, fix types / class inheritance
+ //@ts-ignore
const wallet = new WalletInstance(input, 0);
return wallet;
diff --git a/packages/wallet-derivation/wallets/chains/Jetton.ts b/packages/wallet-derivation/wallets/chains/Jetton.ts
new file mode 100644
index 00000000..1eace884
--- /dev/null
+++ b/packages/wallet-derivation/wallets/chains/Jetton.ts
@@ -0,0 +1,8 @@
+import { Input } from '../../types';
+import { Ton } from './TON';
+
+export class Jetton extends Ton {
+ constructor(input: Input, protected tokenAddress: string) {
+ super(input);
+ }
+}
diff --git a/packages/wallet-derivation/wallets/chains/index.ts b/packages/wallet-derivation/wallets/chains/index.ts
index af267037..5976cf32 100644
--- a/packages/wallet-derivation/wallets/chains/index.ts
+++ b/packages/wallet-derivation/wallets/chains/index.ts
@@ -26,6 +26,7 @@ import { ZCash } from './ZEC';
import { Hedera } from './HBAR';
import { Celestia } from './TIA';
import { Ton } from './TON';
+import { Jetton } from './Jetton';
export const getWallet = (assetId: string) => {
const asset = assets[assetId];
@@ -75,6 +76,10 @@ export const getWallet = (assetId: string) => {
case 'TON':
case 'TON_TEST':
return Ton;
+ case 'USDT_TON':
+ case 'NOTCOIN_TON':
+ case 'DOGS_TON':
+ return Jetton;
// ECDSA
case 'ATOM_COS':
diff --git a/yarn.lock b/yarn.lock
index 261e7a3c..58a7b791 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3254,7 +3254,7 @@
rxjs "^7.8.1"
tslib "^2.5.3"
-"@polkadot/keyring@^12.2.1", "@polkadot/keyring@^12.3.1":
+"@polkadot/keyring@12.3.2", "@polkadot/keyring@^12.2.1", "@polkadot/keyring@^12.3.1":
version "12.3.2"
resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.3.2.tgz#112a0c28816a1f47edad6260dc94222c29465a54"
integrity sha512-NTdtDeI0DP9l/45hXynNABeP5VB8piw5YR+CbUxK2e36xpJWVXwbcOepzslg5ghE9rs8UKJb30Z/HqTU4sBY0Q==
@@ -3440,7 +3440,7 @@
"@polkadot/wasm-util" "7.2.1"
tslib "^2.5.0"
-"@polkadot/wasm-crypto@^7.2.1":
+"@polkadot/wasm-crypto@7.2.1", "@polkadot/wasm-crypto@^7.2.1":
version "7.2.1"
resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.1.tgz#db671dcb73f1646dc13478b5ffc3be18c64babe1"
integrity sha512-SA2+33S9TAwGhniKgztVN6pxUKpGfN4Tre/eUZGUfpgRkT92wIUT2GpGWQE+fCCqGQgADrNiBcwt6XwdPqMQ4Q==
@@ -3467,7 +3467,7 @@
"@polkadot/x-global" "12.3.2"
tslib "^2.5.3"
-"@polkadot/x-fetch@^12.3.1":
+"@polkadot/x-fetch@12.3.2", "@polkadot/x-fetch@^12.3.1":
version "12.3.2"
resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.3.2.tgz#7e8d2113268e792dd5d1b259ef13839c6aa77996"
integrity sha512-3IEuZ5S+RI/t33NsdPLIIa5COfDCfpUW2sbaByEczn75aD1jLqJZSEDwiBniJ2osyNd4uUxBf6e5jw7LAZeZJg==
@@ -3507,7 +3507,7 @@
"@polkadot/x-global" "12.3.2"
tslib "^2.5.3"
-"@polkadot/x-ws@^12.3.1":
+"@polkadot/x-ws@12.3.2", "@polkadot/x-ws@^12.3.1":
version "12.3.2"
resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.3.2.tgz#422559dfbdaac4c965d5e1b406b6cc4529214f94"
integrity sha512-yM9Z64pLNlHpJE43+Xtr+iUXmYpFFY5u5hrke2PJt13O48H8f9Vb9cRaIh94appLyICoS0aekGhDkGH+MCspBA==
|