diff --git a/README.md b/README.md index cdfd4f8..ea24818 100644 --- a/README.md +++ b/README.md @@ -70,25 +70,29 @@ The following methods are available with this SDK: - [Response](#response-8) - [Tron Network](#tron-network) - [Response](#response-9) + - [Ton Network](#ton-network) + - [Response](#response-10) - [Encryptions](#encryptions) - [Encrypt Private Key](#encrypt-private-key) - - [Response](#response-10) - - [Decrypt Encrypted JSON](#decrypt-encrypted-json) - [Response](#response-11) + - [Decrypt Encrypted JSON](#decrypt-encrypted-json) + - [Response](#response-12) - [Token Info](#token-info) - [Get ERC20 Token Info](#get-erc20-token-info) - - [Response](#response-12) - - [Get SPL Token Info](#get-spl-token-info) - [Response](#response-13) - - [Get Waves Token Info](#get-waves-token-info) + - [Get SPL Token Info](#get-spl-token-info) - [Response](#response-14) - - [Get TRC20 Token Info](#get-tron-token-info) + - [Get Waves Token Info](#get-waves-token-info) - [Response](#response-15) + - [Get TRC20 Token Info](#get-tron-token-info) + - [Response](#response-16) + - [Get Jetton Token Info](#get-ton-jetton-token-info) + - [Response](#response-17) - [Smart Contract Call](#smart-contract-call) - [Ethereum network](#ethereum-network-1) - [Waves network](#waves-network-1) - [Tron network](#tron-network-1) - - [Response](#response-17) + - [Response](#response-18) - [Want to contribute?](#want-to-contribute) ### Generate mnemonic @@ -142,6 +146,13 @@ const wallet = await multichainWallet.createWallet({ network: 'tron', }); +// Creating a Ton wallet +const wallet = await multichainWallet.createWallet({ + cluster: 'testnet', // Can also be mainnet, + network: 'ton', + walletVersion: "v4R2", // Can also be W5 or delete (default v4R2) +}); + ``` #### Response @@ -202,6 +213,16 @@ const data = await multichainWallet.getBalance({ address: 'TDdHvW9nU1JaX1P7roYtDvjErTTR17GPJJ', rpcUrl: 'https://nile.trongrid.io', }); + +// Get the Ton balance of an address. +const data = await multichainWallet.getBalance({ + network: 'ton', + address: "0QBG61c4TjKqyiELE3CrxqSVHL6Lkl4D28K1A9oB8G13sXDG", + privateKey: "0808b63ccb05a826085acdb68465e04f428fee6781ed7f770fbe785e32ff56941aab493f5d1c5e94c0fd4a04934c988998c3803110180d26baa940fe602f60bc", + apiKey: "xxxxxxxxxxxxxxxxxxxxxxx", // optional for 10 request per second + rpcUrl: "https://testnet.toncenter.com/api/v2/jsonRPC", + walletVersion: "v4R2", // Can also be W5 or delete (default v4R2) +}); ``` #### Tokens @@ -238,6 +259,17 @@ const data = await multichainWallet.getBalance({ rpcUrl: 'https://nile.trongrid.io', tokenAddress: 'TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj', }); + +// Get the balance of a token on ton. +const data = await multichainWallet.getBalance({ + network: 'ton', + address: "0QBG61c4TjKqyiELE3CrxqSVHL6Lkl4D28K1A9oB8G13sXDG", + rpcUrl: "https://testnet.toncenter.com/api/v2/jsonRPC", + tokenAddress: 'kQB519C3IXFgCr4qKj6QrtaB9Pm3Sawr-Gonlo3O0cKL_I03', + privateKey: "0808b63ccb05a826085acdb68465e04f428fee6781ed7f770fbe785e32ff56941aab493f5d1c5e94c0fd4a04934c988998c3803110180d26baa940fe602f60bc", + apiKey: "xxxxxxxxxxxxxxxxxxxxxxx", // optional for 10 request per second + walletVersion: "v4R2", // Can also be W5 or delete (default v4R2) +}); ``` #### Response @@ -299,6 +331,14 @@ const wallet = multichainWallet.generateWalletFromMnemonic({ 'mushroom deliver work spray hire nuclear wrong deputy march six midnight outside motor differ adult', network: 'tron', }); + +// Generate a Ton wallet from mnemonic. +const wallet = multichainWallet.generateWalletFromMnemonic({ + mnemonic: 'august series long climb rain arrest unfair pill share funny rubber easy pulse vibrant settle inflict castle winter left duty you flower problem betray', // support 12 and 24 mnemonic + network: 'ton', + walletVersion: "v4R2", // Can also be W5 or delete (default v4R2) + cluster: "testnet", // Can also be mainnet, +}); ``` #### Response @@ -342,6 +382,14 @@ const address = multichainWallet.getAddressFromPrivateKey({ 'fa01dc6efd5fd64e4897aadf255ae715cf34138c7ada5f6a7efb0bdd0bd9c8c4', network: 'tron', }); + +// Get the address from the private key on the Tron network. +const address = multichainWallet.getAddressFromPrivateKey({ + privateKey: + '0808b63ccb05a826085acdb68465e04f428fee6781ed7f770fbe785e32ff56941aab493f5d1c5e94c0fd4a04934c988998c3803110180d26baa940fe602f60bc', + network: 'ton', + walletVersion: "v4R2", // Can also be W5 or delete (default v4R2) +}); ``` #### Response @@ -391,6 +439,16 @@ const receipt = await multichainWallet.getTransaction({ network: 'tron', rpcUrl: 'https://nile.trongrid.io', }); + +// Get the transaction receipt on Ton network. +const receipt = await multichainWallet.getTransaction({ + hash: '7a74b1e743f4251079afcb898f678c997820febb714103da417b2dd1370f968c', + network: 'ton', + rpcUrl: "https://testnet.toncenter.com/api/v2/jsonRPC", + apiKey: "xxxxxxxxxxxxxxxxxxxxxxx", // optional for 10 request per second + address: "0QBG61c4TjKqyiELE3CrxqSVHL6Lkl4D28K1A9oB8G13sXDG", // need address from documentation + logicalTime: '39457313000003', // need LT from documentation +}); ``` #### Response @@ -624,6 +682,46 @@ const transfer = await multichainWallet.transfer({ } ``` +#### Ton Network + +Allows for the transfer of TON and Jetton tokens. + +```javascript +// Transferring TON from v4 address to w5 or another. +const transfer = await multichainWallet.transfer({ + recipientAddress: "0QBh77kXMw-M-kg4L6EzP_6Jlx5xTh7mRbkM-zDjngphErgc", + amount: 0.1, + memo: "Sending Ton to Wallet W5", + network: 'ton', + apiKey: "xxxxxxxxxxxxxxxxxxxxxxx", // optional for 10 request per second + rpcUrl: "https://testnet.toncenter.com/api/v2/jsonRPC", + privateKey: "0808b63ccb05a826085acdb68465e04f428fee6781ed7f770fbe785e32ff56941aab493f5d1c5e94c0fd4a04934c988998c3803110180d26baa940fe602f60bc", + walletVersion: "v4R2", // Can also be W5 or delete (default v4R2) +}); + +// Transferring TRC20 tokens from one address to another. +const transfer = await multichainWallet.transfer({ + recipientAddress: "0QBh77kXMw-M-kg4L6EzP_6Jlx5xTh7mRbkM-zDjngphErgc", + amount: 0.1, + memo: "Sending Ton to Wallet W5", + network: 'ton', + apiKey: "xxxxxxxxxxxxxxxxxxxxxxx", // optional for 10 request per second + rpcUrl: "https://testnet.toncenter.com/api/v2/jsonRPC", + privateKey: "0808b63ccb05a826085acdb68465e04f428fee6781ed7f770fbe785e32ff56941aab493f5d1c5e94c0fd4a04934c988998c3803110180d26baa940fe602f60bc", + tokenAddress: 'kQB519C3IXFgCr4qKj6QrtaB9Pm3Sawr-Gonlo3O0cKL_I03', + walletVersion: "v4R2", // Can also be W5 or delete (default v4R2) +}); +``` + +#### Response + +```javascript +{ + transactionHash + ..object; +} +``` + ### Encryptions #### Encrypt Private Key @@ -777,6 +875,32 @@ const info = await multichainWallet.getTokenInfo({ } ``` +#### Get Ton Jetton Token Info + +Allows for fetching Ton token info + +```javascript +const info = await multichainWallet.getTokenInfo({ + address: 'kQB519C3IXFgCr4qKj6QrtaB9Pm3Sawr-Gonlo3O0cKL_I03', + network: 'ton', + apiKey: "xxxxxxxxxxxxxxxxxxxxxxx", // optional for 10 request per second + rpcUrl: "https://testnet.toncenter.com/api/v2/jsonRPC", +}); +``` + +#### Response + +```javascript +{ + name: 'Testnet Hipo Staked TON', + symbol: 'thTON', + address: 'kQB519C3IXFgCr4qKj6QrtaB9Pm3Sawr-Gonlo3O0cKL_I03', + decimals: null, + logoUrl: "https://app.hipo.finance/thton.png" + totalSupply: '132492602000022' +} +``` + ### Smart Contract Call This can be used to make custom smart contract interaction by specifying the contract ABI and function types. diff --git a/package.json b/package.json index 9309c6c..c49cce5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "test:sol": "tsdx test --testPathPattern=solana", "test:tron": "tsdx test --testPathPattern=tron", "test:waves": "tsdx test --testPathPattern=waves", + "test:ton": "tsdx test --testPathPattern=ton", "lint": "tsdx lint", "prepare": "tsdx build", "size": "size-limit", @@ -87,6 +88,9 @@ "@bitgo/utxo-lib": "https://github.com/ren-forks/bitgo-utxo-lib#b848585e65b42c48b98c207e72d7d3006c9a5da0", "@solana/spl-token": "^0.2.0", "@solana/web3.js": "^1.39.1", + "@ton/core": "^0.61.0", + "@ton/crypto": "^3.3.0", + "@ton/ton": "^15.3.1", "@truffle/hdwallet-provider": "^2.0.5", "@waves/ts-lib-crypto": "^1.4.4-beta.1", "@waves/waves-transactions": "^4.2.10", diff --git a/src/common/helpers/tonHelper.ts b/src/common/helpers/tonHelper.ts new file mode 100644 index 0000000..99e654c --- /dev/null +++ b/src/common/helpers/tonHelper.ts @@ -0,0 +1,455 @@ +import provider from '../utils/ton'; +import { + WalletContractV4, + WalletContractV5R1, + JettonMaster, + JettonWallet, + TonClient, + OpenedContract, + SendMode +} from '@ton/ton'; +import { + mnemonicNew, + keyPairFromSeed, + keyPairFromSecretKey, + mnemonicToHDSeed, + mnemonicToPrivateKey, + deriveEd25519Path, +} from '@ton/crypto'; +import { + Address, + fromNano, + toNano, + internal, + beginCell, + Cell, +} from "@ton/core"; + +import { derivePath } from 'ed25519-hd-key'; +import { mnemonicToSeed } from 'bip39'; + +import { + CreateWalletPayload, + GenerateWalletFromMnemonicPayload, + GetAddressFromPrivateKeyPayload, + GetTransactionPayload, + BalancePayload, + TransferPayload, + TonWalletVersion, + IGetTokenInfoPayload, + ITokenInfo, + IResponse, +} from '../utils/types'; + +import { successResponse } from '../utils'; + +interface GetContract { + rpcUrl?: string; + privateKey: string; + walletVersion?: TonWalletVersion; + apiKey?: string; +} + +const WALLET_VERSIONS: Record = { + v4R2: WalletContractV4, + W5: WalletContractV5R1 +}; + +/** + * Waits for a transaction hash after sending a transfer. + * @param contract - Opened wallet contract. + * @param address - Wallet address to check. + * @param connection - TON client connection. + * @param prevSeqno - Previous seqno value. + * @param retries - Number of retries before giving up. + * @param delay - Delay between retries (ms). + * @returns Transaction hash in hex format, or null if not found. + */ +async function waitTxHash( + contract: OpenedContract, + address: Address, + connection: TonClient, + prevSeqno: number, + retries = 15, + delay = 2000 +): Promise { + for (let i = 0; i < retries; i++) { + const newSeqno = await contract.getSeqno().catch(() => prevSeqno); + if (newSeqno > prevSeqno) { + const txs = await connection.getTransactions(address, { limit: 1 }); + if (txs.length > 0) { + return txs[0].hash().toString("hex"); + } + } + await new Promise(res => setTimeout(res, delay)); + } + return null; +} + +/** + * Parses a raw TON transaction object into a simplified structure. + * @param tx - Raw transaction. + * @returns Parsed transaction object or null. + */ +function parseTransaction(tx: any) { + if (!tx) return null; + + const inMsg = tx.inMessage?.info; + const outMsgs: any[] = []; + + if (tx.outMessages) { + for (const [, msg] of tx.outMessages) { + outMsgs.push({ + from: msg.info?.src?.toString(), + to: msg.info?.dest?.toString(), + value: msg.info?.value?.coins?.toString(), + body: msg.body?.toString(), + }); + } + } + + return { + hash: tx.hash ? tx.hash().toString("hex") : undefined, + lt: tx.lt?.toString(), + utime: tx.now, + from: inMsg?.src?.toString() || tx.address.toString(), + to: inMsg?.dest?.toString() || null, + value: inMsg?.value?.coins?.toString() || "0", + fee: tx.totalFees?.coins?.toString() || "0", + success: !tx.description?.aborted, + messages: outMsgs, + }; +} + +const getConnection = (rpcUrl?: string, apiKey?: string) => provider(rpcUrl, apiKey); + +/** + * Initializes a wallet contract from a private key. + * Deploys wallet if not already deployed. + */ +const getContract = async ({ privateKey, rpcUrl, apiKey, walletVersion = 'v4R2' }: GetContract) => { + const connection = getConnection(rpcUrl, apiKey); + + const WalletClass = WALLET_VERSIONS[walletVersion]; + if (!WalletClass) throw new Error(`Unsupported wallet version: ${walletVersion}`); + + const keyPair = keyPairFromSecretKey(Buffer.from(privateKey, 'hex')); + const wallet = WalletClass.create({ publicKey: keyPair.publicKey, workchain: 0 }); + const contract = connection.open(wallet); + + const seqno = await contract.getSeqno().catch(() => 0); + const isDeployed = await connection.isContractDeployed(wallet.address); + + if (isDeployed) { + return { keyPair, wallet, contract, seqno, connection }; + } + + let sendMode; + if (walletVersion === "W5") { + sendMode = SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS; + } + + await contract.sendTransfer({ + seqno, + secretKey: keyPair.secretKey, + sendMode, + messages: [ + internal({ + to: wallet.address, + value: toNano("0.01"), // minimal 0.01 TON + bounce: false, + body: 'Activate wallet with @ton/ton', + }), + ], + }); + + return { keyPair, wallet, contract, seqno, connection }; +}; + +/** + * Creates a new wallet with a fresh mnemonic and keypair. + */ +const createWallet = async ({ cluster, walletVersion = 'v4R2' }: CreateWalletPayload): Promise => { + const mnemonic = await mnemonicNew(24); + const seed = await mnemonicToHDSeed(mnemonic); + const derived = await deriveEd25519Path(seed, [44, 607, 0]); + const keyPair = keyPairFromSeed(derived); + + const WalletClass = WALLET_VERSIONS[walletVersion]; + if (!WalletClass) throw new Error(`Unsupported wallet version: ${walletVersion}`); + + const wallet = WalletClass.create({ publicKey: keyPair.publicKey, workchain: 0 }); + const address = wallet.address.toString({ + testOnly: cluster !== 'mainnet', + bounceable: false, + urlSafe: true + }); + + return successResponse({ + address, + privateKey: keyPair.secretKey.toString('hex'), + mnemonic: mnemonic.join(' ') + }); +}; + +/** + * Generates a wallet from a mnemonic (12 or 24 words). + */ +const generateWalletFromMnemonic = async ({ mnemonic, cluster, walletVersion = 'v4R2' }: GenerateWalletFromMnemonicPayload): Promise => { + const WalletClass = WALLET_VERSIONS[walletVersion]; + if (!WalletClass) throw new Error(`Unsupported wallet version: ${walletVersion}`); + + const mnemonics = Array.isArray(mnemonic) ? mnemonic : mnemonic.trim().split(/\s+/); + + let keyPair, privateKeys; + + if (mnemonics.length === 12) { + const seed = await mnemonicToSeed(mnemonics.join(' ')); + const derived = derivePath("m/44'/607'/0'", seed.toString('hex')); + keyPair = keyPairFromSeed(derived.key); + privateKeys = keyPair.secretKey.toString('hex'); + } else if (mnemonics.length === 24) { + keyPair = await mnemonicToPrivateKey(mnemonics); + privateKeys = keyPair.secretKey.toString('hex'); + } else { + throw new Error('Mnemonic must be 12 or 24 words'); + } + + const wallet = WalletClass.create({ publicKey: keyPair.publicKey, workchain: 0 }); + const address = wallet.address.toString({ + testOnly: cluster !== 'mainnet', + bounceable: false, + urlSafe: true + }); + + return successResponse({ + address, + privateKey: privateKeys, + mnemonic: mnemonics.join(' ') + }); +}; + +/** + * Derives a wallet address from a private key. + */ +const getAddressFromPrivateKey = ({ privateKey, walletVersion = 'v4R2' }: GetAddressFromPrivateKeyPayload): IResponse => { + const WalletClass = WALLET_VERSIONS[walletVersion]; + if (!WalletClass) throw new Error(`Unsupported wallet version: ${walletVersion}`); + + const keyPair = keyPairFromSecretKey(Buffer.from(privateKey, 'hex')); + const wallet = WalletClass.create({ publicKey: keyPair.publicKey, workchain: 0 }); + + return successResponse({ + address: wallet.address.toString({ bounceable: false, urlSafe: true }), + }); +}; + +/** + * Retrieves wallet balance (TON or Jetton). + */ +const getBalance = async (args: BalancePayload): Promise => { + if (!args.privateKey) throw new Error("privateKey is required for getBalance"); + + const walletVersion = args.walletVersion || 'v4R2'; + + const { wallet, contract, connection } = await getContract({ + privateKey: args.privateKey, + rpcUrl: args.rpcUrl, + apiKey: args.apiKey, + walletVersion + }); + + if (args.tokenAddress) { + const master = connection.open(JettonMaster.create(Address.parse(args.tokenAddress))); + const jettonWalletAddress = await master.getWalletAddress(wallet.address); + const jettonWallet = connection.open(JettonWallet.create(jettonWalletAddress)); + const balance = await jettonWallet.getBalance(); + + return successResponse({ + balance: fromNano(balance), + tokenAddress: args.tokenAddress + }); + } + + const balance = await contract.getBalance(); + return successResponse({ balance: fromNano(balance) }); +}; + +/** + * Sends a transfer of TON or Jetton tokens. + */ +const transfer = async (args: TransferPayload): Promise => { + try { + const walletVersion = args.walletVersion || "v4R2"; + + const { keyPair, seqno, contract, connection, wallet } = await getContract({ + privateKey: args.privateKey, + rpcUrl: args.rpcUrl, + apiKey: args.apiKey, + walletVersion, + }); + + const amount = toNano(args.amount); + const messages: any[] = []; + + const memoPayload = beginCell() + .storeUint(0, 32) + .storeStringTail(args.memo ?? "") + .endCell(); + + if (args.tokenAddress) { + const master = connection.open(JettonMaster.create(Address.parse(args.tokenAddress))); + const jettonWalletAddress = await master.getWalletAddress(wallet.address); + const jettonWallet = connection.open(JettonWallet.create(jettonWalletAddress)); + + const gasFee = toNano(args.gasPrice ?? "0.1"); + const forwardGas = toNano(args.forwardGas ?? "0"); + + const payload: Cell = beginCell() + .storeUint(0xf8a7ea5, 32) // jetton_transfer opcode + .storeUint(0, 64) // query id + .storeCoins(amount) + .storeAddress(Address.parse(args.recipientAddress)) + .storeAddress(wallet.address) + .storeBit(false) + .storeCoins(forwardGas) + .storeBit(1) + .storeRef(memoPayload) + .endCell(); + + messages.push(internal({ + to: jettonWallet.address, + value: gasFee, + body: payload, + bounce: true, + })); + } else { + const recipientAddr = Address.parse(args.recipientAddress); + const isDeployed = await connection.isContractDeployed(recipientAddr); + + messages.push(internal({ + to: args.recipientAddress, + value: amount, + body: memoPayload, + bounce: isDeployed, + })); + } + + const finalCell = await contract.createTransfer({ + seqno, + secretKey: keyPair.secretKey, + messages, + }); + + await connection.sendExternalMessage(wallet, finalCell); + const transactionHash = await waitTxHash(contract, wallet.address, connection, seqno); + + return successResponse({ + transactionHash, + to: args.recipientAddress, + amount: fromNano(amount), + tokenAddress: args.tokenAddress ?? null, + forwardGas: args.forwardGas ?? null, + }); + } catch (error) { + throw error; + } +}; + +/** + * Retrieves a transaction by address and logical time. + */ +const getTransaction = async (args: GetTransactionPayload): Promise => { + const connection = getConnection(args.rpcUrl, args.apiKey); + try { + if (!args.address || !args.logicalTime) { + throw new Error("address and logicalTime are required for getTransaction"); + } + + const hashBase64 = /^[0-9a-fA-F]+$/.test(args.hash) + ? Buffer.from(args.hash, "hex").toString("base64") + : args.hash; + + const tx = await connection.getTransaction(Address.parse(args.address), args.logicalTime, hashBase64); + const parsed = parseTransaction(tx); + + return successResponse({ trx: parsed }); + } catch (error) { + throw error; + } +}; + +/** + * Recursively parses a TON Cell to extract all UTF-8 strings. + */ +function parseAllCellStrings(cell: Cell): string[] { + const results: string[] = []; + + function traverse(c: Cell) { + const slice = c.beginParse(); + const bytes: number[] = []; + + try { + while (true) { + bytes.push(Number(slice.loadUint(8))); + } + } catch { } + + if (bytes.length > 0) { + if (bytes[0] === 0) bytes.shift(); + const str = Buffer.from(bytes).toString("utf8").trim(); + + if (/^[\x20-\x7E]+$/.test(str) && str.length > 0) { + results.push(str); + } + } + + for (const ref of c.refs) { + traverse(ref); + } + } + + traverse(cell); + return results; +} + +/** + * Retrieves token (Jetton) metadata information. + */ +const getTokenInfo = async (args: IGetTokenInfoPayload): Promise => { + try { + const connection = getConnection(args.rpcUrl, args.apiKey,); + const master = connection.open(JettonMaster.create(Address.parse(args.address))); + const onchainData = await master.getJettonData(); + + if (!onchainData) { + throw new Error("Token metadata not found"); + } + + const metaUrl = parseAllCellStrings(onchainData.content); + const [logoUrl, name, symbol, decimalsStr] = metaUrl; + + const data: ITokenInfo = { + name, + symbol, + address: args.address, + decimals: decimalsStr ? Number(decimalsStr) : 9, + logoUrl, + totalSupply: onchainData.totalSupply.toString(), + }; + + return successResponse({ ...data }); + } catch (error) { + throw error; + } +}; + +export default { + createWallet, + generateWalletFromMnemonic, + getAddressFromPrivateKey, + getBalance, + transfer, + getTransaction, + getTokenInfo, +}; diff --git a/src/common/utils/ton.ts b/src/common/utils/ton.ts new file mode 100644 index 0000000..94772bf --- /dev/null +++ b/src/common/utils/ton.ts @@ -0,0 +1,10 @@ +import { TonClient } from '@ton/ton'; + +const provider = (rpcUrl?: string, apiKey?: string) => { + return new TonClient({ + endpoint: rpcUrl as string, + apiKey: apiKey as string + }); +}; + +export default provider; diff --git a/src/common/utils/types.ts b/src/common/utils/types.ts index 32004e3..48b2f59 100644 --- a/src/common/utils/types.ts +++ b/src/common/utils/types.ts @@ -3,6 +3,7 @@ import ethereumHelper from '../../common/helpers/ethereumHelper'; import solanaHelper from '../../common/helpers/solanaHelper'; import wavesHelper from '../../common/helpers/wavesHelper'; import tronHelper from '../../common/helpers/tronHelper'; +import tonHelper from '../../common/helpers/tonHelper'; export type Network = | 'ethereum' @@ -10,15 +11,17 @@ export type Network = | 'tron' | 'waves' | 'bitcoin' - | 'bitcoin-testnet'; + | 'bitcoin-testnet' + | 'ton'; export type NetworkHelper = { [key in T]: - | typeof bitcoinHelper - | typeof ethereumHelper - | typeof solanaHelper - | typeof wavesHelper - | typeof tronHelper; + | typeof bitcoinHelper + | typeof ethereumHelper + | typeof solanaHelper + | typeof wavesHelper + | typeof tronHelper + | typeof tonHelper; }; export interface TransferPayload { @@ -36,6 +39,9 @@ export interface TransferPayload { fee?: number; // defaults to 10000 feeLimit?: number; subtractFee?: boolean; // defaults to false + walletVersion?: TonWalletVersion; + forwardGas?: number | string; // for ton jetton + memo?: string; // for memo tag ton } export interface BalancePayload { @@ -44,22 +50,28 @@ export interface BalancePayload { rpcUrl?: string; apiKey?: string; tokenAddress?: string; + walletVersion?: TonWalletVersion; + privateKey?: string; } export interface CreateWalletPayload { derivationPath?: string; cluster?: string; + walletVersion?: TonWalletVersion; network: Network; } export interface GetAddressFromPrivateKeyPayload { privateKey: string; network: Network; + walletVersion?: TonWalletVersion; } export interface GetTransactionPayload { rpcUrl?: string; apiKey?: string; + address?: string; + logicalTime?: string; hash: string; network: Network; } @@ -68,6 +80,7 @@ export interface GenerateWalletFromMnemonicPayload { mnemonic: string; derivationPath?: string; cluster?: string; + walletVersion?: TonWalletVersion; network: Network; } @@ -133,6 +146,8 @@ export interface ISmartContractCallPayload { privateKey?: string; } +export type TonWalletVersion = 'v4R2' | 'W5'; + export interface INetworkHelper { getAddressFromPrivateKey: (args: GetAddressFromPrivateKeyPayload) => IResponse; generateWalletFromMnemonic: (args: GenerateWalletFromMnemonicPayload) => IResponse; diff --git a/src/services/wallet/index.ts b/src/services/wallet/index.ts index 79bd8d8..858765f 100644 --- a/src/services/wallet/index.ts +++ b/src/services/wallet/index.ts @@ -5,6 +5,7 @@ import ethereumHelper from '../../common/helpers/ethereumHelper'; import solanaHelper from '../../common/helpers/solanaHelper'; import wavesHelper from '../../common/helpers/wavesHelper'; import tronHelper from '../../common/helpers/tronHelper'; +import tonHelper from '../../common/helpers/tonHelper'; import { TransferPayload, @@ -31,6 +32,7 @@ const networkHelpers: Record = { waves: wavesHelper, bitcoin: bitcoinHelper, 'bitcoin-testnet': bitcoinHelper, + ton: tonHelper, }; /** @@ -61,6 +63,7 @@ const supportedFeatures: Record = { 'bitcoin-testnet': [...baseFeatures], waves: [...baseFeatures, 'getTokenInfo', 'smartContractCall'], tron: [...baseFeatures, 'getTokenInfo', 'smartContractCall'], + ton: [...baseFeatures, 'getTokenInfo'], }; /** diff --git a/test/wallet.ton.test.ts b/test/wallet.ton.test.ts new file mode 100644 index 0000000..50b4a96 --- /dev/null +++ b/test/wallet.ton.test.ts @@ -0,0 +1,284 @@ +import { + createWallet, + generateMnemonic, + generateWalletFromMnemonic, + getAddressFromPrivateKey, + getBalance, + transfer, + getTransaction, + getTokenInfo, +} from '../src'; + +describe('MultichainCryptoWallet Ton tests', () => { + const tonCenterApiKey = '45b1c87ea1c172aa473658931cb2b58c52e1c8d9aa904e5fd8e2937eff29c013'; + const tonCenterRpc = "https://testnet.toncenter.com/api/v2/jsonRPC"; + // Generate From Tonkeeper app + const mnemonic24 = "august series long climb rain arrest unfair pill share funny rubber easy pulse vibrant settle inflict castle winter left duty you flower problem betray"; + const expectedAddress24v4 = "0QBG61c4TjKqyiELE3CrxqSVHL6Lkl4D28K1A9oB8G13sXDG"; + const expectedAddress24v5 = "0QBh77kXMw-M-kg4L6EzP_6Jlx5xTh7mRbkM-zDjngphErgc"; + const privateKey24 = "0808b63ccb05a826085acdb68465e04f428fee6781ed7f770fbe785e32ff56941aab493f5d1c5e94c0fd4a04934c988998c3803110180d26baa940fe602f60bc" + // Generate From Trustwallet or SDK + const mnemonic12 = "gallery follow round rookie rely pupil expect auto wonder stairs protect connect"; + const expectedAddress12v4 = "UQAa9bnNsobIO5PwrLr7oKiLHk2GBzBNEAIYIjL_FMpwP7M-"; + const expectedAddress12v5 = "UQAqnG6hJtc6Vp_T6rezHHQG_d1v4HCwuvQDM4VKYrlwvon3"; + const privateKey12 = "d5eb32f5eea03e446eee06401d2de6cc146a9768c8c614a298d556e86773d3edcaab63276f86daa2d3fba585f712c146eb2aa1494aad61535810a31eaf245b6a" + + it('generateMnemonic', () => { + const mnemonic = generateMnemonic(); + + expect(typeof mnemonic).toBe('string'); + }); + + it('createWallet V4', async () => { + const wallet = await createWallet({ + cluster: 'testnet', + network: 'ton', + }); + + expect(typeof wallet).toBe('object'); + expect(wallet).toHaveProperty('address'); + expect(wallet).toHaveProperty('privateKey'); + expect(wallet).toHaveProperty('mnemonic'); + }); + + it('createWallet W5', async () => { + const wallet = await createWallet({ + walletVersion: "W5", + cluster: 'testnet', + network: 'ton', + }); + + expect(typeof wallet).toBe('object'); + expect(wallet).toHaveProperty('address'); + expect(wallet).toHaveProperty('privateKey'); + expect(wallet).toHaveProperty('mnemonic'); + }); + + it('generateWalletFromMnemonic 12 V4', async () => { + const wallet = await generateWalletFromMnemonic({ + mnemonic: mnemonic12, + walletVersion: "v4R2", + cluster: "mainnet", + network: 'ton', + }); + + expect(typeof wallet).toBe('object'); + expect(wallet).toHaveProperty('address'); + expect(wallet).toHaveProperty('privateKey'); + expect(wallet).toHaveProperty('mnemonic'); + expect(wallet.address).toBe(expectedAddress12v4); + }); + + it('generateWalletFromMnemonic 12 W5', async () => { + const wallet = await generateWalletFromMnemonic({ + mnemonic: mnemonic12, + walletVersion: "W5", + cluster: "mainnet", + network: 'ton', + }); + + expect(typeof wallet).toBe('object'); + expect(wallet).toHaveProperty('address'); + expect(wallet).toHaveProperty('privateKey'); + expect(wallet).toHaveProperty('mnemonic'); + expect(wallet.address).toBe(expectedAddress12v5); + }); + + it('generateWalletFromMnemonic 24 V4', async () => { + const wallet = await generateWalletFromMnemonic({ + mnemonic: mnemonic24, + walletVersion: "v4R2", + cluster: "testnet", + network: 'ton', + }); + + expect(typeof wallet).toBe('object'); + expect(wallet).toHaveProperty('address'); + expect(wallet).toHaveProperty('privateKey'); + expect(wallet).toHaveProperty('mnemonic'); + expect(wallet.address).toBe(expectedAddress24v4); + }); + + it('generateWalletFromMnemonic 24 W5', async () => { + const wallet = await generateWalletFromMnemonic({ + mnemonic: mnemonic24, + walletVersion: "W5", + cluster: "testnet", + network: 'ton', + }); + + expect(typeof wallet).toBe('object'); + expect(wallet).toHaveProperty('address'); + expect(wallet).toHaveProperty('privateKey'); + expect(wallet).toHaveProperty('mnemonic'); + expect(wallet.address).toBe(expectedAddress24v5); + }); + + it('getAddressFromPrivateKey12 V4', () => { + const address = getAddressFromPrivateKey({ + privateKey: privateKey12, + walletVersion: "v4R2", + network: 'ton', + }); + + expect(typeof address).toBe('object'); + expect(address).toHaveProperty('address'); + expect(typeof address.address).toBe('string'); + }); + + it('getAddressFromPrivateKey12 W5', () => { + const address = getAddressFromPrivateKey({ + privateKey: privateKey12, + walletVersion: "W5", + network: 'ton', + }); + + expect(typeof address).toBe('object'); + expect(address).toHaveProperty('address'); + expect(typeof address.address).toBe('string'); + }); + + it('getAddressFromPrivateKey24 V4', () => { + const address = getAddressFromPrivateKey({ + privateKey: privateKey24, + walletVersion: "v4R2", + network: 'ton', + }); + + expect(typeof address).toBe('object'); + expect(address).toHaveProperty('address'); + expect(typeof address.address).toBe('string'); + }); + + it('getAddressFromPrivateKey24 W5', () => { + const address = getAddressFromPrivateKey({ + privateKey: privateKey24, + walletVersion: "W5", + network: 'ton', + }); + + expect(typeof address).toBe('object'); + expect(address).toHaveProperty('address'); + expect(typeof address.address).toBe('string'); + }); + + it('getBalance native TON V4', async () => { + const data = await getBalance({ + address: expectedAddress24v4, + privateKey: privateKey24, + apiKey: tonCenterApiKey, + network: 'ton', + rpcUrl: tonCenterRpc, + walletVersion: "v4R2" + }); + + expect(typeof data).toBe('object'); + expect(data).toHaveProperty('balance'); + expect(typeof data.balance).toBe('string'); + }); + + it('getBalance native TON V5', async () => { + const data = await getBalance({ + address: expectedAddress24v5, + privateKey: privateKey24, + apiKey: tonCenterApiKey, + network: 'ton', + rpcUrl: tonCenterRpc, + walletVersion: "W5", + }); + + expect(typeof data).toBe('object'); + expect(data).toHaveProperty('balance'); + expect(typeof data.balance).toBe('string'); + }); + + it('getBalance testnet Jetton token', async () => { + const data = await getBalance({ + address: expectedAddress24v4, + privateKey: privateKey24, + apiKey: tonCenterApiKey, + tokenAddress: 'kQB519C3IXFgCr4qKj6QrtaB9Pm3Sawr-Gonlo3O0cKL_I03', + network: 'ton', + rpcUrl: tonCenterRpc, + }); + + expect(typeof data).toBe('object'); + expect(data).toHaveProperty('balance'); + expect(typeof data.balance).toBe('string'); + }); + + it('transfer TON V4', async () => { + const response = await transfer({ + recipientAddress: expectedAddress24v5, + amount: 0.1, + memo: "Sending Ton to Wallet W5", + network: 'ton', + apiKey: tonCenterApiKey, + rpcUrl: tonCenterRpc, + privateKey: privateKey24, + }); + + expect(typeof response).toBe('object'); + }); + + it('transfer TON W5', async () => { + const response = await transfer({ + recipientAddress: expectedAddress24v4, + amount: 0.1, + memo: "Sending Ton to Wallet V4", + network: 'ton', + apiKey: tonCenterApiKey, + rpcUrl: tonCenterRpc, + privateKey: privateKey24, + walletVersion: "W5", + }); + + expect(typeof response).toBe('object'); + }); + + it('transfer Jetton Token on Ton Testnet', async () => { + const response = await transfer({ + recipientAddress: expectedAddress24v5, + tokenAddress: 'kQB519C3IXFgCr4qKj6QrtaB9Pm3Sawr-Gonlo3O0cKL_I03', + amount: 0.1, + forwardGas: 0, // remove for default Jetton Only + network: 'ton', + apiKey: tonCenterApiKey, + rpcUrl: tonCenterRpc, + privateKey: privateKey24, + }); + + expect(typeof response).toBe('object'); + }); + + it('getTransaction from hash', async () => { + const receipt = await getTransaction({ + apiKey: tonCenterApiKey, + rpcUrl: tonCenterRpc, + address: expectedAddress24v5, + hash: '7a74b1e743f4251079afcb898f678c997820febb714103da417b2dd1370f968c', + logicalTime: '39457313000003', + network: 'ton', + }); + + expect(typeof receipt).toBe('object'); + }); + + it('getTokenInfo (Hipo Testnet Coin)', async () => { + const tokenInfo = await getTokenInfo({ + address: 'kQB519C3IXFgCr4qKj6QrtaB9Pm3Sawr-Gonlo3O0cKL_I03', + network: 'ton', + apiKey: tonCenterApiKey, + rpcUrl: tonCenterRpc, + }); + + expect(tokenInfo).toBeDefined(); + expect(typeof tokenInfo).toBe('object'); + expect(tokenInfo).toHaveProperty('name'); + expect(tokenInfo).toHaveProperty('symbol'); + expect(tokenInfo).toHaveProperty('address'); + expect(tokenInfo).toHaveProperty('decimals'); + expect(tokenInfo).toHaveProperty('logoUrl'); + expect(tokenInfo).toHaveProperty('totalSupply'); + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0201e56..350af95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1906,6 +1906,40 @@ dependencies: defer-to-connect "^2.0.1" +"@ton/core@^0.61.0": + version "0.61.0" + resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.61.0.tgz#09b37801cb2f5a942020fcc992be1e99f4b16689" + integrity sha512-0qyVfP2dDue2bq80ydXggo2MlufcmzuFk6G94qRrZxvyQ3NSe4UeBTeRf1gQmN7tywgTsX2gS61e4yvJrlUu4Q== + dependencies: + symbol.inspect "1.0.1" + +"@ton/crypto-primitives@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz#8c9277c250b59aae3c819e0d6bd61e44d998e9ca" + integrity sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow== + dependencies: + jssha "3.2.0" + +"@ton/crypto@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@ton/crypto/-/crypto-3.3.0.tgz#019103df6540fbc1d8102979b4587bc85ff9779e" + integrity sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA== + dependencies: + "@ton/crypto-primitives" "2.1.0" + jssha "3.2.0" + tweetnacl "1.0.3" + +"@ton/ton@^15.3.1": + version "15.3.1" + resolved "https://registry.yarnpkg.com/@ton/ton/-/ton-15.3.1.tgz#c20688b27eb8ce8474610843804a7599679c38a2" + integrity sha512-+UuvbE0o0VIU/0W90STO+emRIDr3Vs39LdbX5ySm/Ra+RQJSiH0KX6TDOFqWDmD2Wzk4/zw21KwSiZ6Xjk8zlw== + dependencies: + axios "^1.6.7" + dataloader "^2.0.0" + symbol.inspect "1.0.1" + teslabot "^1.3.0" + zod "^3.21.4" + "@truffle/hdwallet-provider@^2.0.5": version "2.1.15" resolved "https://registry.yarnpkg.com/@truffle/hdwallet-provider/-/hdwallet-provider-2.1.15.tgz#fbf8e19d112db81b109ebc06ac6d9d42124b512c" @@ -2749,6 +2783,15 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^1.6.7: + version "1.12.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7" + integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.4" + proxy-from-env "^1.1.0" + axios@^1.8.3: version "1.8.4" resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447" @@ -3827,6 +3870,11 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" +dataloader@^2.0.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.3.tgz#42d10b4913515f5b37c6acedcb4960d6ae1b1517" + integrity sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -5518,6 +5566,17 @@ form-data@^4.0.0: es-set-tostringtag "^2.1.0" mime-types "^2.1.12" +form-data@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -7203,6 +7262,11 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" +jssha@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jssha/-/jssha-3.2.0.tgz#88ec50b866dd1411deaddbe6b3e3692e4c710f16" + integrity sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q== + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -9850,6 +9914,11 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +symbol.inspect@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/symbol.inspect/-/symbol.inspect-1.0.1.tgz#e13125b8038c4996eb0dfa1567332ad4dcd0763f" + integrity sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ== + table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" @@ -9890,6 +9959,11 @@ terser@^4.6.2: source-map "~0.6.1" source-map-support "~0.5.12" +teslabot@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/teslabot/-/teslabot-1.5.0.tgz#70f544516699ca5f696d8ae94f3d12cd495d5cd6" + integrity sha512-e2MmELhCgrgZEGo7PQu/6bmYG36IDH+YrBI1iGm6jovXkeDIGa3pZ2WSqRjzkuw2vt1EqfkZoV5GpXgqL8QJVg== + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -11120,3 +11194,8 @@ yargs@^15.3.1: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^18.1.2" + +zod@^3.21.4: + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==