diff --git a/apps/wallet/src/features/Send/InputTransaction/TokensTab.tsx b/apps/wallet/src/features/Send/InputTransaction/TokensTab.tsx index c96cc2e63..8f20ec6cd 100644 --- a/apps/wallet/src/features/Send/InputTransaction/TokensTab.tsx +++ b/apps/wallet/src/features/Send/InputTransaction/TokensTab.tsx @@ -6,6 +6,7 @@ import { Button, Select, Text, View } from '@walless/gui'; import type { TokenDocument } from '@walless/store'; import CheckedInput from 'components/CheckedInput'; import { NavButton } from 'components/NavButton'; +import { keyState } from 'state/keys'; import { useTokens } from 'utils/hooks'; import { checkValidAddress } from 'utils/transaction'; @@ -89,6 +90,9 @@ export const TokensTab: FC = ({ onContinue }) => { useEffect(() => { if (type === 'token' && token) { txActions.update({ network: token.network }); + const publicKeys = Array.from(keyState.map.values()); + const key = publicKeys.find((k) => k.network === network); + if (key) txActions.update({ sender: key._id }); } }, [type, token]); diff --git a/apps/wallet/src/features/Send/internal.ts b/apps/wallet/src/features/Send/internal.ts index a29d4501d..3d480404e 100644 --- a/apps/wallet/src/features/Send/internal.ts +++ b/apps/wallet/src/features/Send/internal.ts @@ -19,17 +19,21 @@ import { ModalId } from 'modals/types'; import { solMint } from 'utils/constants'; import { useSnapshot } from 'utils/hooks'; import { - createAndSendSolanaTransaction, createAndSendSuiTransaction, - getGasilonFee, - getSolanaTransactionFee, + createAndSendTezosTransaction, getSuiTransactionFee, hasEnoughBalanceToMakeTx, } from 'utils/transaction'; -import { constructSuiSendTokenTransaction } from 'utils/transaction/construct'; +import { createAndSendSolanaTransaction } from 'utils/transaction'; +import { constructSuiSendTokenTransaction } from 'utils/transaction'; +import { + getGasilonFee, + getSolanaTransactionFee, +} from 'utils/transaction/solana/send/fee'; import type { SolanaSendTransaction, SuiSendTransaction, + TezosSendTokenTransaction, } from 'utils/transaction/types'; import { proxy } from 'valtio'; @@ -143,6 +147,18 @@ export const txActions = { break; } + // case Networks.tezos: { + // txActions.update({ feeLoading: true }); + // const genericTransaction = await createGenericTransaction(); + // const { connection } = engine.getContext(Networks.tezos); + // const fee = await getTezosTransactionFee( + // genericTransaction as TezosSendTokenTransaction, + // connection, + // ); + // txActions.update({ feeAmount: fee, feeLoading: false }); + // break; + // } + default: { txActions.update({ feeAmount: 0 }); break; @@ -203,6 +219,27 @@ export const txActions = { }); onComplete?.(); } + } else if (txContext.tx.network === Networks.tezos) { + const payload = { + sender: genericTransaction?.sender, + receiver: genericTransaction?.receiver, + amount: genericTransaction?.amount, + token: genericTransaction?.token, + } as TezosSendTokenTransaction; + + const res = await createAndSendTezosTransaction(payload, passcode); + + if (res.responseCode === ResponseCode.WRONG_PASSCODE) { + showError({ errorText: 'Passcode is NOT matched' }); + } else if (res.responseCode === ResponseCode.SUCCESS) { + const signature = res.signatureString; + txActions.update({ signature }); + txActions.update({ time: new Date() }); + txActions.update({ + status: res.isSuccessful ? 'success' : 'failed', + }); + onComplete?.(); + } } return true; @@ -268,6 +305,16 @@ const createGenericTransaction = async () => { } else { throw new Error('Transfer Sui NFT not supported'); } + } else if (network === Networks.tezos) { + const { token } = txContext.tx as TokenTransactionContext; + transaction = { + sender, + receiver, + token, + amount: parseInt(amount) || 0, + network, + type, + } as TezosSendTokenTransaction; } else { throw new Error('Unsupported network'); } @@ -275,6 +322,7 @@ const createGenericTransaction = async () => { const hasEnoughBalance = await hasEnoughBalanceToMakeTx(transaction); if (!hasEnoughBalance) { + console.log('100'); showError({ errorText: 'Insufficient balance to send' }); return; } @@ -340,3 +388,7 @@ export type SuiTransactionContext = SuiTokenTransactionContext & SuiCollectibleTransactionContext & { signature?: string; }; + +export type TezosTransactionContext = TokenTransactionContext & { + signature?: string; +}; diff --git a/apps/wallet/src/features/Swap/ConfirmPasscode/index.tsx b/apps/wallet/src/features/Swap/ConfirmPasscode/index.tsx index a6a4382ba..482b2504e 100644 --- a/apps/wallet/src/features/Swap/ConfirmPasscode/index.tsx +++ b/apps/wallet/src/features/Swap/ConfirmPasscode/index.tsx @@ -11,8 +11,9 @@ import type { SolanaContext } from 'engine/runners'; import { showError } from 'modals/Error'; import assets from 'utils/assets'; import { nativeModules } from 'utils/native'; -import { constructSwapTransaction, getAliasedMint } from 'utils/transaction'; import { signAndSendTransaction } from 'utils/transaction/solana'; +import { getAliasedMint } from 'utils/transaction/solana/swap'; +import { constructSwapTransaction } from 'utils/transaction/solana/swap'; import { swapActions, swapContext } from '../context'; diff --git a/apps/wallet/src/features/Swap/InputSwap/ToToken.tsx b/apps/wallet/src/features/Swap/InputSwap/ToToken.tsx index c77e3e646..78098a2fc 100644 --- a/apps/wallet/src/features/Swap/InputSwap/ToToken.tsx +++ b/apps/wallet/src/features/Swap/InputSwap/ToToken.tsx @@ -7,7 +7,7 @@ import { useDebouncedCallback } from 'use-debounce'; import { parseWithDecimals } from 'utils/format'; import type { JupiterToken } from 'utils/hooks'; import { useSnapshot } from 'utils/hooks'; -import { getAliasedMint, getSwapQuote } from 'utils/transaction'; +import { getAliasedMint, getSwapQuote } from 'utils/transaction/solana/swap'; import type { SwapContext } from '../context'; import { swapActions, swapContext } from '../context'; diff --git a/apps/wallet/src/features/Swap/context.ts b/apps/wallet/src/features/Swap/context.ts index 08daab7a7..5a233aaf9 100644 --- a/apps/wallet/src/features/Swap/context.ts +++ b/apps/wallet/src/features/Swap/context.ts @@ -4,8 +4,9 @@ import { AnimateDirections, BindDirections, modalActions } from '@walless/gui'; import type { TokenDocument } from '@walless/store'; import { ModalId } from 'modals/types'; import type { JupiterToken } from 'utils/hooks'; -import type { SwapQuote } from 'utils/transaction'; -import { checkValidAddress, getAliasedMint } from 'utils/transaction'; +import { checkValidAddress } from 'utils/transaction'; +import type { SwapQuote } from 'utils/transaction/solana/swap'; +import { getAliasedMint } from 'utils/transaction/solana/swap'; import { proxy } from 'valtio'; import SelectFromToken from './Select/SelectFromToken'; diff --git a/apps/wallet/src/utils/transaction/aptos/index.ts b/apps/wallet/src/utils/transaction/aptos/index.ts new file mode 100644 index 000000000..d1ab99c82 --- /dev/null +++ b/apps/wallet/src/utils/transaction/aptos/index.ts @@ -0,0 +1 @@ +export * from './send'; diff --git a/apps/wallet/src/utils/transaction/aptos/send/index.ts b/apps/wallet/src/utils/transaction/aptos/send/index.ts new file mode 100644 index 000000000..d03ea6908 --- /dev/null +++ b/apps/wallet/src/utils/transaction/aptos/send/index.ts @@ -0,0 +1,91 @@ +import { Networks, RequestType, ResponseCode } from '@walless/core'; +import type { ResponsePayload } from '@walless/messaging'; +import { aptos, utils } from '@walless/network'; +import { engine } from 'engine'; +import type { AptosContext } from 'engine/runners'; +import { storage } from 'utils/storage'; + +export const sendAptosTransaction = async ( + transaction: aptos.AptosCoinPayload | aptos.AptosTokenPayload, + passcode: string, +) => { + const res = {} as ResponsePayload; + + let privateKey; + try { + privateKey = await utils.getPrivateKey(storage, Networks.aptos, passcode); + } catch { + res.responseCode = ResponseCode.WRONG_PASSCODE; + return res; + } + + const isCoinTransaction = !('creator' in transaction); + + const { provider } = engine.getContext(Networks.aptos); + if (isCoinTransaction) { + res.signatureString = await aptos.handleTransferCoin( + provider, + privateKey, + transaction, + ); + } else { + res.signatureString = await aptos.handleTransferToken( + provider, + privateKey, + transaction, + ); + } + + res.responseCode = ResponseCode.SUCCESS; + + return res; +}; +export const handleAptosOnChainAction = async ({ + passcode, + type, + payload, +}: { + passcode: string; + type: RequestType; + payload: unknown; +}) => { + const res = {} as ResponsePayload; + + let privateKey; + try { + privateKey = await utils.getPrivateKey(storage, Networks.aptos, passcode); + } catch { + res.responseCode = ResponseCode.WRONG_PASSCODE; + return res; + } + + try { + const { provider } = engine.getContext(Networks.aptos); + switch (type) { + case RequestType.UPDATE_DIRECT_TRANSFER_ON_APTOS: + res.signatureString = await aptos.handleUpdateDirectTransfer( + provider, + privateKey, + payload as aptos.AptosDirectTransferPayload, + ); + break; + + case RequestType.CLAIM_TOKEN_ON_APTOS: + res.signatureString = await aptos.handleClaimToken( + provider, + privateKey, + payload as aptos.AptosClaimTokenPayload, + ); + break; + + default: + break; + } + + res.responseCode = ResponseCode.SUCCESS; + } catch { + res.responseCode = ResponseCode.ERROR; + } + + return res; +}; diff --git a/apps/wallet/src/utils/transaction/aptos/send/index.web.ts b/apps/wallet/src/utils/transaction/aptos/send/index.web.ts new file mode 100644 index 000000000..d615587ca --- /dev/null +++ b/apps/wallet/src/utils/transaction/aptos/send/index.web.ts @@ -0,0 +1,20 @@ +import type { RequestType } from '@walless/core'; +import { sendRequest } from 'bridge'; + +export const handleAptosOnChainAction = async ({ + passcode, + payload, + type, +}: { + passcode: string; + type: RequestType; + payload: unknown; +}) => { + const res = await sendRequest({ + type, + transaction: JSON.stringify(payload), + passcode, + }); + + return res; +}; diff --git a/apps/wallet/src/utils/transaction/fee.ts b/apps/wallet/src/utils/transaction/fee.ts index 45b52f045..3c57bffdb 100644 --- a/apps/wallet/src/utils/transaction/fee.ts +++ b/apps/wallet/src/utils/transaction/fee.ts @@ -1,32 +1,17 @@ -import type { GasCostSummary } from '@mysten/sui.js/client'; -import type { TransactionBlock } from '@mysten/sui.js/transactions'; -import { SUI_DECIMALS } from '@mysten/sui.js/utils'; -import { ACCOUNT_SIZE, getAssociatedTokenAddressSync } from '@solana/spl-token'; -import type { VersionedTransaction } from '@solana/web3.js'; -import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; -import type { SolanaCollectible, SolanaToken } from '@walless/core'; -import { logger, Networks } from '@walless/core'; -import { solana } from '@walless/network'; -import { withSetComputeUnitPrice } from '@walless/network/src/solana'; -import type { NftDocument, TokenDocument } from '@walless/store'; -import base58 from 'bs58'; +import { Networks } from '@walless/core'; import { engine } from 'engine'; -import type { SolanaContext, SuiContext } from 'engine/runners'; -import { environment } from 'utils/config'; +import type { SuiContext } from 'engine/runners'; import { solMint } from 'utils/constants'; -import { - constructSolanaSendNftTransaction, - constructSolanaSendTokenTransaction, - constructSuiSendTokenTransaction, -} from './construct'; -import { getGasilonConfig } from './gasilon'; +import { getGasilonFee, getSolanaTransactionFee } from './solana'; +import { constructSuiSendTokenTransaction, getSuiTransactionFee } from './sui'; import type { SendNftTransaction, SendTokenTransaction, SolanaSendNftTransaction, SolanaSendTokenTransaction, SuiSendTransaction, + TezosSendTokenTransaction, } from './types'; export const hasEnoughBalanceToMakeTx = async ( @@ -35,7 +20,8 @@ export const hasEnoughBalanceToMakeTx = async ( | SendNftTransaction | SolanaSendTokenTransaction | SolanaSendNftTransaction - | SuiSendTransaction, + | SuiSendTransaction + | TezosSendTokenTransaction, ) => { const { network } = initTransaction; @@ -85,150 +71,12 @@ export const hasEnoughBalanceToMakeTx = async ( return false; } } + case Networks.tezos: { + const tezosTransaction = initTransaction as TezosSendTokenTransaction; + return tezosTransaction.token.balance > tezosTransaction.amount; + } default: { return false; } } }; - -export type GetSolanaFeeConfig = { - type: 'token' | 'nft'; - sender: string; - receiver: string; - nft?: NftDocument; - token?: TokenDocument; - tokenForFee: TokenDocument; -}; - -export const getSolanaTransactionFee = async ( - initTransaction: GetSolanaFeeConfig, -): Promise => { - const { type, sender, receiver, token, nft } = initTransaction; - - let transaction; - if (type === 'token') { - if (!token) throw Error("require token when type is 'token'"); - transaction = await constructSolanaSendTokenTransaction({ - sender, - receiver, - token, - amount: 0, - }); - } else { - if (!nft) throw Error("require nft when type is 'nft'"); - transaction = await constructSolanaSendNftTransaction({ - sender, - receiver, - nft, - amount: 0, - }); - } - - const { connection } = engine.getContext(Networks.solana); - - if (!transaction) return 0; - transaction = await withSetComputeUnitPrice(transaction, connection); - - const message = (transaction as VersionedTransaction).message; - const transactionFeePromise = connection - .getFeeForMessage(message) - .then((res) => res.value || 0); - - const rentFeePromise = (async () => { - const { receiver } = initTransaction; - let mint; - if (initTransaction.type === 'token') { - const { token } = initTransaction as SolanaSendTokenTransaction; - if (token.mint === solMint) return 0; - mint = token.mint; - } else { - const { nft } = initTransaction as SolanaSendNftTransaction; - mint = nft.mint; - } - - const receiverATAddress = getAssociatedTokenAddressSync( - new PublicKey(mint), - new PublicKey(receiver), - ); - - const receiverATAccount = - await connection.getAccountInfo(receiverATAddress); - - if (!receiverATAccount) { - return await connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE); - } - - return 0; - })(); - - const [txFee, rentFee] = await Promise.all([ - transactionFeePromise, - rentFeePromise, - ]); - - return (txFee + rentFee) / LAMPORTS_PER_SOL; -}; - -export type GetGasilonFeeConfig = { - sender: string; - receiver: string; - mint: string; - feeMint: string; -}; - -export const getGasilonFee = async ({ - sender, - receiver, - mint, - feeMint, -}: GetGasilonFeeConfig): Promise => { - const config = await getGasilonConfig(); - if (!config) return 0; - - const { connection } = engine.getContext(Networks.solana); - const transaction = await solana.constructGasilonTransaction(connection, { - sender: new PublicKey(sender), - receiver: new PublicKey(receiver), - feePayer: new PublicKey(config.feePayer), - amount: 0, // mock amount for get gee - fee: 0, // mock amount for get gee - mint: new PublicKey(mint), - feeMint: new PublicKey(feeMint), - }); - - try { - const res = await fetch(`${environment.GASILON_ENDPOINT}/solana/getFee`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - transaction: base58.encode(transaction.serialize()), - }), - }); - - const data = await res.json(); - - return data.totalByFeeToken; - } catch (error) { - logger.error('failed to get gasilon transaction fee', error); - return 0; - } -}; - -export const getSuiTransactionFee = async (txb: TransactionBlock) => { - const { client } = engine.getContext(Networks.sui); - const builtTxb = await txb.build({ client }).catch(console.log); - if (!builtTxb) throw new Error('unable to build txb'); - const dryrunResult = await client.dryRunTransactionBlock({ - transactionBlock: builtTxb, - }); - - return calculateSuiGas(dryrunResult?.effects.gasUsed); -}; - -const calculateSuiGas = (gas: GasCostSummary): number => { - const gasBigInt = - BigInt(gas.computationCost) + - BigInt(gas.storageCost) - - BigInt(gas.storageRebate); - return Number(gasBigInt) / 10 ** SUI_DECIMALS; -}; diff --git a/apps/wallet/src/utils/transaction/index.ts b/apps/wallet/src/utils/transaction/index.ts index d3a9c85a3..207da1fde 100644 --- a/apps/wallet/src/utils/transaction/index.ts +++ b/apps/wallet/src/utils/transaction/index.ts @@ -1,5 +1,8 @@ +export * from './aptos'; export * from './common'; export * from './fee'; export * from './gasilon'; -export * from './send'; -export * from './swap'; +export * from './solana'; +export * from './sui'; +export * from './tezos'; +export * from './types'; diff --git a/apps/wallet/src/utils/transaction/send.ts b/apps/wallet/src/utils/transaction/send.ts deleted file mode 100644 index 7f33b3712..000000000 --- a/apps/wallet/src/utils/transaction/send.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { PublicKey } from '@solana/web3.js'; -import { Networks, RequestType, ResponseCode } from '@walless/core'; -import type { ResponsePayload } from '@walless/messaging'; -import { aptos, solana, sui, utils } from '@walless/network'; -import { engine } from 'engine'; -import type { AptosContext, SolanaContext, SuiContext } from 'engine/runners'; -import { environment } from 'utils/config'; -import { solMint } from 'utils/constants'; -import { storage } from 'utils/storage'; - -import { - constructSolanaSendNftTransaction, - constructSolanaSendTokenTransaction, - constructSuiSendTokenTransaction, -} from './construct'; -import { getGasilonConfig } from './gasilon'; -import type { - SolanaSendNftTransaction, - SolanaSendTokenTransaction, - SolanaSendTransaction, - SuiSendTokenTransaction, -} from './types'; - -export const createAndSendSolanaTransaction = async ( - initTransaction: SolanaSendTokenTransaction | SolanaSendNftTransaction, - passcode: string, -) => { - const { type } = initTransaction; - let transaction; - if (type === 'token') { - transaction = await constructSolanaSendTokenTransaction( - initTransaction as SolanaSendTokenTransaction, - ); - } else { - transaction = await constructSolanaSendNftTransaction( - initTransaction as SolanaSendNftTransaction, - ); - } - if (!transaction) throw Error('failed to construct transaction'); - transaction = await solana.withSetComputeUnitPrice(transaction); - - const res = {} as ResponsePayload; - let privateKey; - try { - privateKey = await utils.getPrivateKey(storage, Networks.solana, passcode); - } catch { - res.responseCode = ResponseCode.WRONG_PASSCODE; - return res; - } - - const { tokenForFee } = initTransaction as SolanaSendTransaction; - const isGasilon = tokenForFee && tokenForFee.mint !== solMint; - if (isGasilon) { - const { sender, tokenForFee, fee } = initTransaction; - const config = await getGasilonConfig(); - if (!config) throw Error('Gasilon is not available'); - transaction = await solana.withGasilon(transaction, { - sender: new PublicKey(sender), - feeAmount: Math.round(fee * 10 ** tokenForFee.decimals), - feeMint: new PublicKey(tokenForFee.mint), - feePayer: new PublicKey(config.feePayer), - }); - - const gasilonEndpoint = environment.GASILON_ENDPOINT; - res.signatureString = await solana.signAndSendGasilonTransaction( - gasilonEndpoint, - transaction, - privateKey, - ); - } else { - const { connection } = engine.getContext(Networks.solana); - res.signatureString = await solana.signAndSendTransaction( - connection, - // TODO: support legacy transaction - transaction as never, - privateKey, - ); - } - res.responseCode = ResponseCode.SUCCESS; - - return res; -}; - -export const sendAptosTransaction = async ( - transaction: aptos.AptosCoinPayload | aptos.AptosTokenPayload, - passcode: string, -) => { - const res = {} as ResponsePayload; - - let privateKey; - try { - privateKey = await utils.getPrivateKey(storage, Networks.aptos, passcode); - } catch { - res.responseCode = ResponseCode.WRONG_PASSCODE; - return res; - } - - const isCoinTransaction = !('creator' in transaction); - - const { provider } = engine.getContext(Networks.aptos); - if (isCoinTransaction) { - res.signatureString = await aptos.handleTransferCoin( - provider, - privateKey, - transaction, - ); - } else { - res.signatureString = await aptos.handleTransferToken( - provider, - privateKey, - transaction, - ); - } - - res.responseCode = ResponseCode.SUCCESS; - - return res; -}; -export const handleAptosOnChainAction = async ({ - passcode, - type, - payload, -}: { - passcode: string; - type: RequestType; - payload: unknown; -}) => { - const res = {} as ResponsePayload; - - let privateKey; - try { - privateKey = await utils.getPrivateKey(storage, Networks.aptos, passcode); - } catch { - res.responseCode = ResponseCode.WRONG_PASSCODE; - return res; - } - - try { - const { provider } = engine.getContext(Networks.aptos); - switch (type) { - case RequestType.UPDATE_DIRECT_TRANSFER_ON_APTOS: - res.signatureString = await aptos.handleUpdateDirectTransfer( - provider, - privateKey, - payload as aptos.AptosDirectTransferPayload, - ); - break; - - case RequestType.CLAIM_TOKEN_ON_APTOS: - res.signatureString = await aptos.handleClaimToken( - provider, - privateKey, - payload as aptos.AptosClaimTokenPayload, - ); - break; - - default: - break; - } - - res.responseCode = ResponseCode.SUCCESS; - } catch { - res.responseCode = ResponseCode.ERROR; - } - - return res; -}; - -export const createAndSendSuiTransaction = async ( - initTransaction: SuiSendTokenTransaction, - passcode: string, -) => { - const res = {} as ResponsePayload; - let privateKey: Uint8Array; - try { - privateKey = await utils.getPrivateKey(storage, Networks.sui, passcode); - } catch { - res.responseCode = ResponseCode.WRONG_PASSCODE; - return res; - } - - const { client } = engine.getContext(Networks.sui); - const transactionBlock = await constructSuiSendTokenTransaction( - client, - initTransaction, - ); - const response = await sui.signAndExecuteTransaction( - client, - transactionBlock.serialize(), - privateKey, - ); - res.signatureString = response.transaction?.txSignatures; - res.responseCode = ResponseCode.SUCCESS; - - return res; -}; diff --git a/apps/wallet/src/utils/transaction/solana.ts b/apps/wallet/src/utils/transaction/solana.ts deleted file mode 100644 index 6a879a4c7..000000000 --- a/apps/wallet/src/utils/transaction/solana.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { VersionedTransaction } from '@solana/web3.js'; -import { Networks, ResponseCode } from '@walless/core'; -import type { ResponsePayload } from '@walless/messaging'; -import { solana, utils } from '@walless/network'; -import { engine } from 'engine'; -import type { SolanaContext } from 'engine/runners'; -import { storage } from 'utils/storage'; - -export const signAndSendTransaction = async ( - transaction: VersionedTransaction, - passcode: string, - options?: solana.SignAndSendOptions, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - timeout?: number, -): Promise => { - const res = {} as ResponsePayload; - let privateKey; - try { - privateKey = await utils.getPrivateKey(storage, Networks.solana, passcode); - } catch { - res.responseCode = ResponseCode.WRONG_PASSCODE; - return res; - } - - const { connection } = engine.getContext(Networks.solana); - res.signatureString = await solana.signAndSendTransaction( - connection, - transaction, - privateKey, - options, - ); - - return res; -}; diff --git a/apps/wallet/src/utils/transaction/solana.web.ts b/apps/wallet/src/utils/transaction/solana.web.ts deleted file mode 100644 index 38349f11d..000000000 --- a/apps/wallet/src/utils/transaction/solana.web.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { VersionedTransaction } from '@solana/web3.js'; -import { RequestType } from '@walless/core'; -import type { ResponsePayload } from '@walless/messaging'; -import type { solana } from '@walless/network'; -import { sendRequest } from 'bridge'; -import { encode } from 'bs58'; - -export const signAndSendTransaction = async ( - transaction: VersionedTransaction, - passcode: string, - options?: solana.SignAndSendOptions, - timeout?: number, -): Promise => { - const res = await sendRequest( - { - type: RequestType.SIGN_SEND_TRANSACTION_ON_SOLANA, - transaction: encode(transaction.serialize()), - passcode, - options, - }, - timeout, - ); - - return res as ResponsePayload; -}; diff --git a/apps/wallet/src/utils/transaction/solana/index.ts b/apps/wallet/src/utils/transaction/solana/index.ts new file mode 100644 index 000000000..e852f5234 --- /dev/null +++ b/apps/wallet/src/utils/transaction/solana/index.ts @@ -0,0 +1,2 @@ +export * from './send'; +export * from './swap'; diff --git a/apps/wallet/src/utils/transaction/construct.ts b/apps/wallet/src/utils/transaction/solana/send/construct.ts similarity index 78% rename from apps/wallet/src/utils/transaction/construct.ts rename to apps/wallet/src/utils/transaction/solana/send/construct.ts index 238ad2583..eae05e0ea 100644 --- a/apps/wallet/src/utils/transaction/construct.ts +++ b/apps/wallet/src/utils/transaction/solana/send/construct.ts @@ -1,17 +1,14 @@ -import type { SuiClient } from '@mysten/sui.js/client'; import type { VersionedTransaction } from '@solana/web3.js'; import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; import { Networks } from '@walless/core'; -import { solana, sui } from '@walless/network'; +import { solana } from '@walless/network'; import { engine } from 'engine'; import type { SolanaContext } from 'engine/runners'; import { solMint } from 'utils/constants'; - import type { SolanaSendNftTransaction, SolanaSendTokenTransaction, - SuiSendTokenTransaction, -} from './types'; +} from 'utils/transaction/types'; export const constructSolanaSendTokenTransaction = async ( transaction: Omit< @@ -66,19 +63,3 @@ export const constructSolanaSendNftTransaction = async ( return transaction; }; - -export const constructSuiSendTokenTransaction = async ( - client: SuiClient, - initTransaction: SuiSendTokenTransaction, -) => { - const { tokenForFee, amount, receiver, sender, token } = initTransaction; - return sui.constructSuiTransactionBlock( - client, - token.coinType, - token.decimals, - tokenForFee.coinType, - amount, - receiver, - sender, - ); -}; diff --git a/apps/wallet/src/utils/transaction/solana/send/fee.ts b/apps/wallet/src/utils/transaction/solana/send/fee.ts new file mode 100644 index 000000000..5c876a1d6 --- /dev/null +++ b/apps/wallet/src/utils/transaction/solana/send/fee.ts @@ -0,0 +1,147 @@ +import { PublicKey } from '@metaplex-foundation/js'; +import { ACCOUNT_SIZE, getAssociatedTokenAddressSync } from '@solana/spl-token'; +import type { VersionedTransaction } from '@solana/web3.js'; +import { LAMPORTS_PER_SOL } from '@solana/web3.js'; +import type { SolanaCollectible, SolanaToken } from '@walless/core'; +import { logger, Networks } from '@walless/core'; +import { solana } from '@walless/network'; +import { withSetComputeUnitPrice } from '@walless/network/src/solana'; +import type { NftDocument, TokenDocument } from '@walless/store'; +import base58 from 'bs58'; +import { engine } from 'engine'; +import type { SolanaContext } from 'engine/runners'; +import { environment } from 'utils/config'; +import { solMint } from 'utils/constants'; +import { getGasilonConfig } from 'utils/transaction/gasilon'; +import type { + SolanaSendNftTransaction, + SolanaSendTokenTransaction, +} from 'utils/transaction/types'; + +import { + constructSolanaSendNftTransaction, + constructSolanaSendTokenTransaction, +} from './construct'; + +export type GetSolanaFeeConfig = { + type: 'token' | 'nft'; + sender: string; + receiver: string; + nft?: NftDocument; + token?: TokenDocument; + tokenForFee: TokenDocument; +}; + +export const getSolanaTransactionFee = async ( + initTransaction: GetSolanaFeeConfig, +): Promise => { + const { type, sender, receiver, token, nft } = initTransaction; + + let transaction; + if (type === 'token') { + if (!token) throw Error("require token when type is 'token'"); + transaction = await constructSolanaSendTokenTransaction({ + sender, + receiver, + token, + amount: 0, + }); + } else { + if (!nft) throw Error("require nft when type is 'nft'"); + transaction = await constructSolanaSendNftTransaction({ + sender, + receiver, + nft, + amount: 0, + }); + } + + const { connection } = engine.getContext(Networks.solana); + + if (!transaction) return 0; + transaction = await withSetComputeUnitPrice(transaction, connection); + + const message = (transaction as VersionedTransaction).message; + const transactionFeePromise = connection + .getFeeForMessage(message) + .then((res) => res.value || 0); + + const rentFeePromise = (async () => { + const { receiver } = initTransaction; + let mint; + if (initTransaction.type === 'token') { + const { token } = initTransaction as SolanaSendTokenTransaction; + if (token.mint === solMint) return 0; + mint = token.mint; + } else { + const { nft } = initTransaction as SolanaSendNftTransaction; + mint = nft.mint; + } + + const receiverATAddress = getAssociatedTokenAddressSync( + new PublicKey(mint), + new PublicKey(receiver), + ); + + const receiverATAccount = + await connection.getAccountInfo(receiverATAddress); + + if (!receiverATAccount) { + return await connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE); + } + + return 0; + })(); + + const [txFee, rentFee] = await Promise.all([ + transactionFeePromise, + rentFeePromise, + ]); + + return (txFee + rentFee) / LAMPORTS_PER_SOL; +}; + +export type GetGasilonFeeConfig = { + sender: string; + receiver: string; + mint: string; + feeMint: string; +}; + +export const getGasilonFee = async ({ + sender, + receiver, + mint, + feeMint, +}: GetGasilonFeeConfig): Promise => { + const config = await getGasilonConfig(); + if (!config) return 0; + + const { connection } = engine.getContext(Networks.solana); + const transaction = await solana.constructGasilonTransaction(connection, { + sender: new PublicKey(sender), + receiver: new PublicKey(receiver), + feePayer: new PublicKey(config.feePayer), + amount: 0, // mock amount for get gee + fee: 0, // mock amount for get gee + mint: new PublicKey(mint), + feeMint: new PublicKey(feeMint), + }); + + try { + const res = await fetch(`${environment.GASILON_ENDPOINT}/solana/getFee`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + transaction: base58.encode(transaction.serialize()), + }), + }); + + const data = await res.json(); + + return data.totalByFeeToken; + } catch (error) { + logger.error('failed to get gasilon transaction fee', error); + return 0; + } +}; diff --git a/apps/wallet/src/utils/transaction/solana/send/index.ts b/apps/wallet/src/utils/transaction/solana/send/index.ts new file mode 100644 index 000000000..9d9413d05 --- /dev/null +++ b/apps/wallet/src/utils/transaction/solana/send/index.ts @@ -0,0 +1,111 @@ +import { PublicKey } from '@metaplex-foundation/js'; +import type { VersionedTransaction } from '@solana/web3.js'; +import { Networks, ResponseCode } from '@walless/core'; +import type { ResponsePayload } from '@walless/messaging'; +import { solana, utils } from '@walless/network'; +import { engine } from 'engine'; +import type { SolanaContext } from 'engine/runners'; +import { environment } from 'utils/config'; +import { solMint } from 'utils/constants'; +import { storage } from 'utils/storage'; +import { getGasilonConfig } from 'utils/transaction/gasilon'; +import type { + SolanaSendNftTransaction, + SolanaSendTokenTransaction, + SolanaSendTransaction, +} from 'utils/transaction/types'; + +import { + constructSolanaSendNftTransaction, + constructSolanaSendTokenTransaction, +} from './construct'; + +export const createAndSendSolanaTransaction = async ( + initTransaction: SolanaSendTokenTransaction | SolanaSendNftTransaction, + passcode: string, +) => { + const { type } = initTransaction; + let transaction; + if (type === 'token') { + transaction = await constructSolanaSendTokenTransaction( + initTransaction as SolanaSendTokenTransaction, + ); + } else { + transaction = await constructSolanaSendNftTransaction( + initTransaction as SolanaSendNftTransaction, + ); + } + if (!transaction) throw Error('failed to construct transaction'); + transaction = await solana.withSetComputeUnitPrice(transaction); + + const res = {} as ResponsePayload; + let privateKey; + try { + privateKey = await utils.getPrivateKey(storage, Networks.solana, passcode); + } catch { + res.responseCode = ResponseCode.WRONG_PASSCODE; + return res; + } + + const { tokenForFee } = initTransaction as SolanaSendTransaction; + const isGasilon = tokenForFee && tokenForFee.mint !== solMint; + if (isGasilon) { + const { sender, tokenForFee, fee } = initTransaction; + const config = await getGasilonConfig(); + if (!config) throw Error('Gasilon is not available'); + transaction = await solana.withGasilon(transaction, { + sender: new PublicKey(sender), + feeAmount: Math.round(fee * 10 ** tokenForFee.decimals), + feeMint: new PublicKey(tokenForFee.mint), + feePayer: new PublicKey(config.feePayer), + }); + + const gasilonEndpoint = environment.GASILON_ENDPOINT; + res.signatureString = await solana.signAndSendGasilonTransaction( + gasilonEndpoint, + transaction, + privateKey, + ); + } else { + const { connection } = engine.getContext(Networks.solana); + res.signatureString = await solana.signAndSendTransaction( + connection, + // TODO: support legacy transaction + transaction as never, + privateKey, + ); + } + res.responseCode = ResponseCode.SUCCESS; + + return res; +}; + +export const signAndSendTransaction = async ( + transaction: VersionedTransaction, + passcode: string, + options?: solana.SignAndSendOptions, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + timeout?: number, +): Promise => { + const res = {} as ResponsePayload; + let privateKey; + try { + privateKey = await utils.getPrivateKey(storage, Networks.solana, passcode); + } catch { + res.responseCode = ResponseCode.WRONG_PASSCODE; + return res; + } + + const { connection } = engine.getContext(Networks.solana); + res.signatureString = await solana.signAndSendTransaction( + connection, + transaction, + privateKey, + options, + ); + + return res; +}; + +export * from './construct'; +export * from './fee'; diff --git a/apps/wallet/src/utils/transaction/send.web.ts b/apps/wallet/src/utils/transaction/solana/send/index.web.ts similarity index 61% rename from apps/wallet/src/utils/transaction/send.web.ts rename to apps/wallet/src/utils/transaction/solana/send/index.web.ts index 6282f9622..b679ae22f 100644 --- a/apps/wallet/src/utils/transaction/send.web.ts +++ b/apps/wallet/src/utils/transaction/solana/send/index.web.ts @@ -1,26 +1,24 @@ -import { PublicKey } from '@solana/web3.js'; -import { Networks, RequestType, ResponseCode } from '@walless/core'; +import { PublicKey } from '@metaplex-foundation/js'; +import type { VersionedTransaction } from '@solana/web3.js'; +import { Networks, RequestType } from '@walless/core'; import type { ResponsePayload } from '@walless/messaging'; -import { solana, sui, utils } from '@walless/network'; +import { solana } from '@walless/network'; import { sendRequest } from 'bridge'; import { encode } from 'bs58'; import { engine } from 'engine'; -import type { SolanaContext, SuiContext } from 'engine/runners'; +import type { SolanaContext } from 'engine/runners'; import { solMint } from 'utils/constants'; -import { storage } from 'utils/storage'; +import { getGasilonConfig } from 'utils/transaction/gasilon'; +import type { + SolanaSendNftTransaction, + SolanaSendTokenTransaction, + SolanaSendTransaction, +} from 'utils/transaction/types'; import { constructSolanaSendNftTransaction, constructSolanaSendTokenTransaction, - constructSuiSendTokenTransaction, } from './construct'; -import { getGasilonConfig } from './gasilon'; -import type { - SolanaSendNftTransaction, - SolanaSendTokenTransaction, - SolanaSendTransaction, - SuiSendTokenTransaction, -} from './types'; export const createAndSendSolanaTransaction = async ( initTransaction: SolanaSendTokenTransaction | SolanaSendNftTransaction, @@ -78,49 +76,24 @@ export const createAndSendSolanaTransaction = async ( } }; -export const handleAptosOnChainAction = async ({ - passcode, - payload, - type, -}: { - passcode: string; - type: RequestType; - payload: unknown; -}) => { - const res = await sendRequest({ - type, - transaction: JSON.stringify(payload), - passcode, - }); - - return res; -}; - -export const createAndSendSuiTransaction = async ( - initTransaction: SuiSendTokenTransaction, +export const signAndSendTransaction = async ( + transaction: VersionedTransaction, passcode: string, -) => { - const res = {} as ResponsePayload; - let privateKey: Uint8Array; - try { - privateKey = await utils.getPrivateKey(storage, Networks.sui, passcode); - } catch { - res.responseCode = ResponseCode.WRONG_PASSCODE; - return res; - } - - const { client } = engine.getContext(Networks.sui); - const transactionBlock = await constructSuiSendTokenTransaction( - client, - initTransaction, - ); - const response = await sui.signAndExecuteTransaction( - client, - transactionBlock.serialize(), - privateKey, + options?: solana.SignAndSendOptions, + timeout?: number, +): Promise => { + const res = await sendRequest( + { + type: RequestType.SIGN_SEND_TRANSACTION_ON_SOLANA, + transaction: encode(transaction.serialize()), + passcode, + options, + }, + timeout, ); - res.signatureString = response.transaction?.txSignatures; - res.responseCode = ResponseCode.SUCCESS; - return res; + return res as ResponsePayload; }; + +export * from './construct'; +export * from './fee'; diff --git a/apps/wallet/src/utils/transaction/swap/index.ts b/apps/wallet/src/utils/transaction/solana/swap/index.ts similarity index 100% rename from apps/wallet/src/utils/transaction/swap/index.ts rename to apps/wallet/src/utils/transaction/solana/swap/index.ts diff --git a/apps/wallet/src/utils/transaction/swap/jupiter.ts b/apps/wallet/src/utils/transaction/solana/swap/jupiter.ts similarity index 100% rename from apps/wallet/src/utils/transaction/swap/jupiter.ts rename to apps/wallet/src/utils/transaction/solana/swap/jupiter.ts diff --git a/apps/wallet/src/utils/transaction/swap/types.ts b/apps/wallet/src/utils/transaction/solana/swap/types.ts similarity index 100% rename from apps/wallet/src/utils/transaction/swap/types.ts rename to apps/wallet/src/utils/transaction/solana/swap/types.ts diff --git a/apps/wallet/src/utils/transaction/sui/index.ts b/apps/wallet/src/utils/transaction/sui/index.ts new file mode 100644 index 000000000..d1ab99c82 --- /dev/null +++ b/apps/wallet/src/utils/transaction/sui/index.ts @@ -0,0 +1 @@ +export * from './send'; diff --git a/apps/wallet/src/utils/transaction/sui/send/construct.ts b/apps/wallet/src/utils/transaction/sui/send/construct.ts new file mode 100644 index 000000000..ea53a899b --- /dev/null +++ b/apps/wallet/src/utils/transaction/sui/send/construct.ts @@ -0,0 +1,19 @@ +import type { SuiClient } from '@mysten/sui.js/dist/cjs/client'; +import { sui } from '@walless/network'; +import type { SuiSendTokenTransaction } from 'utils/transaction/types'; + +export const constructSuiSendTokenTransaction = async ( + client: SuiClient, + initTransaction: SuiSendTokenTransaction, +) => { + const { tokenForFee, amount, receiver, sender, token } = initTransaction; + return sui.constructSuiTransactionBlock( + client, + token.coinType, + token.decimals, + tokenForFee.coinType, + amount, + receiver, + sender, + ); +}; diff --git a/apps/wallet/src/utils/transaction/sui/send/fee.ts b/apps/wallet/src/utils/transaction/sui/send/fee.ts new file mode 100644 index 000000000..a9364c143 --- /dev/null +++ b/apps/wallet/src/utils/transaction/sui/send/fee.ts @@ -0,0 +1,25 @@ +import type { GasCostSummary } from '@mysten/sui.js/client'; +import type { TransactionBlock } from '@mysten/sui.js/transactions'; +import { SUI_DECIMALS } from '@mysten/sui.js/utils'; +import { Networks } from '@walless/core'; +import { engine } from 'engine'; +import type { SuiContext } from 'engine/runners'; + +const calculateSuiGas = (gas: GasCostSummary): number => { + const gasBigInt = + BigInt(gas.computationCost) + + BigInt(gas.storageCost) - + BigInt(gas.storageRebate); + return Number(gasBigInt) / 10 ** SUI_DECIMALS; +}; + +export const getSuiTransactionFee = async (txb: TransactionBlock) => { + const { client } = engine.getContext(Networks.sui); + const builtTxb = await txb.build({ client }).catch(console.log); + if (!builtTxb) throw new Error('unable to build txb'); + const dryrunResult = await client.dryRunTransactionBlock({ + transactionBlock: builtTxb, + }); + + return calculateSuiGas(dryrunResult?.effects.gasUsed); +}; diff --git a/apps/wallet/src/utils/transaction/sui/send/index.ts b/apps/wallet/src/utils/transaction/sui/send/index.ts new file mode 100644 index 000000000..9dd3fd4d1 --- /dev/null +++ b/apps/wallet/src/utils/transaction/sui/send/index.ts @@ -0,0 +1,41 @@ +import { Networks, ResponseCode } from '@walless/core'; +import type { ResponsePayload } from '@walless/messaging'; +import { sui, utils } from '@walless/network'; +import { engine } from 'engine'; +import type { SuiContext } from 'engine/runners'; +import { storage } from 'utils/storage'; +import type { SuiSendTokenTransaction } from 'utils/transaction/types'; + +import { constructSuiSendTokenTransaction } from './construct'; + +export const createAndSendSuiTransaction = async ( + initTransaction: SuiSendTokenTransaction, + passcode: string, +) => { + const res = {} as ResponsePayload; + let privateKey: Uint8Array; + try { + privateKey = await utils.getPrivateKey(storage, Networks.sui, passcode); + } catch { + res.responseCode = ResponseCode.WRONG_PASSCODE; + return res; + } + + const { client } = engine.getContext(Networks.sui); + const transactionBlock = await constructSuiSendTokenTransaction( + client, + initTransaction, + ); + const response = await sui.signAndExecuteTransaction( + client, + transactionBlock.serialize(), + privateKey, + ); + res.signatureString = response.transaction?.txSignatures; + res.responseCode = ResponseCode.SUCCESS; + + return res; +}; + +export * from './construct'; +export * from './fee'; diff --git a/apps/wallet/src/utils/transaction/sui/send/index.web.ts b/apps/wallet/src/utils/transaction/sui/send/index.web.ts new file mode 100644 index 000000000..9dd3fd4d1 --- /dev/null +++ b/apps/wallet/src/utils/transaction/sui/send/index.web.ts @@ -0,0 +1,41 @@ +import { Networks, ResponseCode } from '@walless/core'; +import type { ResponsePayload } from '@walless/messaging'; +import { sui, utils } from '@walless/network'; +import { engine } from 'engine'; +import type { SuiContext } from 'engine/runners'; +import { storage } from 'utils/storage'; +import type { SuiSendTokenTransaction } from 'utils/transaction/types'; + +import { constructSuiSendTokenTransaction } from './construct'; + +export const createAndSendSuiTransaction = async ( + initTransaction: SuiSendTokenTransaction, + passcode: string, +) => { + const res = {} as ResponsePayload; + let privateKey: Uint8Array; + try { + privateKey = await utils.getPrivateKey(storage, Networks.sui, passcode); + } catch { + res.responseCode = ResponseCode.WRONG_PASSCODE; + return res; + } + + const { client } = engine.getContext(Networks.sui); + const transactionBlock = await constructSuiSendTokenTransaction( + client, + initTransaction, + ); + const response = await sui.signAndExecuteTransaction( + client, + transactionBlock.serialize(), + privateKey, + ); + res.signatureString = response.transaction?.txSignatures; + res.responseCode = ResponseCode.SUCCESS; + + return res; +}; + +export * from './construct'; +export * from './fee'; diff --git a/apps/wallet/src/utils/transaction/tezos/index.ts b/apps/wallet/src/utils/transaction/tezos/index.ts new file mode 100644 index 000000000..d1ab99c82 --- /dev/null +++ b/apps/wallet/src/utils/transaction/tezos/index.ts @@ -0,0 +1 @@ +export * from './send'; diff --git a/apps/wallet/src/utils/transaction/tezos/send/fee.ts b/apps/wallet/src/utils/transaction/tezos/send/fee.ts new file mode 100644 index 000000000..56f28acd0 --- /dev/null +++ b/apps/wallet/src/utils/transaction/tezos/send/fee.ts @@ -0,0 +1,13 @@ +import type { TezosToolkit } from '@taquito/taquito'; +import type { TezosSendTokenTransaction } from 'utils/transaction/types'; + +import { getTransferParams } from './internal'; +export const getTezosTransactionFee = async ( + payload: TezosSendTokenTransaction, + connection: TezosToolkit, +) => { + const transferParams = await getTransferParams(payload, connection); + const estmtn = await connection.estimate.transfer(transferParams); + + return estmtn.suggestedFeeMutez; +}; diff --git a/apps/wallet/src/utils/transaction/tezos/send/index.ts b/apps/wallet/src/utils/transaction/tezos/send/index.ts new file mode 100644 index 000000000..4d71becc2 --- /dev/null +++ b/apps/wallet/src/utils/transaction/tezos/send/index.ts @@ -0,0 +1,46 @@ +import { importKey } from '@taquito/signer'; +import { Networks, ResponseCode } from '@walless/core'; +import type { ResponsePayload } from '@walless/messaging'; +import { utils } from '@walless/network'; +import { encode } from 'bs58'; +import { engine } from 'engine'; +import type { TezosContext } from 'engine/runners'; +import { storage } from 'utils/storage'; +import type { TezosSendTokenTransaction } from 'utils/transaction/types'; + +import { getTransferParams } from './internal'; + +export const createAndSendTezosTransaction = async ( + payload: TezosSendTokenTransaction, + passcode: string, +) => { + const { connection } = engine.getContext(Networks.tezos); + + const res = {} as ResponsePayload; + + let privateKey; + try { + const decodedPrivateKey = await utils.getPrivateKey( + storage, + Networks.tezos, + passcode, + ); + privateKey = encode(decodedPrivateKey); + } catch { + res.responseCode = ResponseCode.WRONG_PASSCODE; + return res; + } + + await importKey(connection, privateKey); + + const transferParams = await getTransferParams(payload, connection); + + const tx = await connection.wallet.transfer(transferParams).send(); + + res.signatureString = tx.opHash; + res.isSuccessful = (await tx.confirmation())?.completed; + + res.responseCode = ResponseCode.SUCCESS; + + return res; +}; diff --git a/apps/wallet/src/utils/transaction/tezos/send/internal.ts b/apps/wallet/src/utils/transaction/tezos/send/internal.ts new file mode 100644 index 000000000..6bdd168f2 --- /dev/null +++ b/apps/wallet/src/utils/transaction/tezos/send/internal.ts @@ -0,0 +1,62 @@ +import type { TezosToolkit } from '@taquito/taquito'; +import { OpKind } from '@taquito/taquito'; +import { TezosTokenTypes } from '@walless/core'; +import { XTZ } from 'engine/runners/tezos/token'; +import type { TezosSendTokenTransaction } from 'utils/transaction/types'; + +export const getTransferParams = async ( + { + sender, + receiver, + amount, + token, + }: Omit, + connection: TezosToolkit, +) => { + if (token.contract.address === XTZ.contract.address) { + return { + amount: amount, + to: receiver, + }; + } + + const contract = await connection.contract.at(token.contract.address); + + if (token.tokenType === TezosTokenTypes.FA2) { + return { + kind: OpKind.TRANSACTION, + to: contract.address, + amount: 0, + parameter: { + entrypoint: 'transfer', + value: [ + { + prim: 'Pair', + args: [ + { string: sender }, + [ + { + prim: 'Pair', + args: [ + { string: receiver }, + { + prim: 'Pair', + args: [ + { int: token.tokenId }, + { int: (amount * 10 ** token.decimals).toFixed() }, + ], + }, + ], + }, + ], + ], + }, + ], + }, + }; + } + + return contract.methods + .transfer(sender, receiver, amount * 10 ** token.decimals) + .toTransferParams(); +}; diff --git a/apps/wallet/src/utils/transaction/types.ts b/apps/wallet/src/utils/transaction/types.ts index c586c76e3..247e7e815 100644 --- a/apps/wallet/src/utils/transaction/types.ts +++ b/apps/wallet/src/utils/transaction/types.ts @@ -5,6 +5,7 @@ import type { SolanaCollectible, SolanaToken, SuiToken, + TezosToken, Token, } from '@walless/core'; import type { NftDocument, TokenDocument } from '@walless/store'; @@ -54,3 +55,5 @@ export type SuiSendTokenTransaction = SendTokenTransaction & }; export type SuiSendTransaction = SuiSendTokenTransaction; + +export type TezosSendTokenTransaction = SendTokenTransaction; diff --git a/packages/core/utils/assets.ts b/packages/core/utils/assets.ts index 4f6f14c8e..32f8a5bcf 100644 --- a/packages/core/utils/assets.ts +++ b/packages/core/utils/assets.ts @@ -83,8 +83,8 @@ export type SuiToken = Token & { // There are 2 standards of tokens in Tezos, they are FA1.2 and FA2 export enum TezosTokenTypes { - FA12 = 'FA1.2', - FA2 = 'FA2', + FA12 = 'fa1.2', + FA2 = 'fa2', } export type TezosContract = {