diff --git a/package.json b/package.json index c02cedbdf..11c5a38f0 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@xchainjs/xchain-crypto": "^0.2.6", "@xchainjs/xchain-doge": "^0.1.2", "@xchainjs/xchain-ethereum": "^0.23.3", - "@xchainjs/xchain-litecoin": "^0.7.2", + "@xchainjs/xchain-litecoin": "^0.8.0-alpha.1", "@xchainjs/xchain-terra": "^0.1.0-alpha.2", "@xchainjs/xchain-thorchain": "^0.22.2", "@xchainjs/xchain-util": "^0.6.0", diff --git a/src/main/api/ledger/bitcoin/transaction.ts b/src/main/api/ledger/bitcoin/transaction.ts index 49b83311f..1febbd56b 100644 --- a/src/main/api/ledger/bitcoin/transaction.ts +++ b/src/main/api/ledger/bitcoin/transaction.ts @@ -7,8 +7,9 @@ import { BaseAmount } from '@xchainjs/xchain-util' import * as Bitcoin from 'bitcoinjs-lib' import * as E from 'fp-ts/lib/Either' +import { getSochainUrl } from '../../../../shared/api/sochain' import { LedgerError, LedgerErrorId, Network } from '../../../../shared/api/types' -import { getHaskoinApiUrl, getSochainUrl } from '../../../../shared/bitcoin/client' +import { getHaskoinApiUrl } from '../../../../shared/bitcoin/client' import { toClientNetwork } from '../../../../shared/utils/client' import { isError } from '../../../../shared/utils/guard' import { getDerivationPath } from './common' diff --git a/src/main/api/ledger/litecoin/transaction.ts b/src/main/api/ledger/litecoin/transaction.ts new file mode 100644 index 000000000..b6fe7427e --- /dev/null +++ b/src/main/api/ledger/litecoin/transaction.ts @@ -0,0 +1,104 @@ +import AppBTC from '@ledgerhq/hw-app-btc' +import { Transaction } from '@ledgerhq/hw-app-btc/lib/types' +import Transport from '@ledgerhq/hw-transport' +import { Address, FeeRate, TxHash } from '@xchainjs/xchain-client' +import { broadcastTx, buildTx } from '@xchainjs/xchain-litecoin' +import { BaseAmount } from '@xchainjs/xchain-util' +import * as Bitcoin from 'bitcoinjs-lib' +import * as E from 'fp-ts/lib/Either' + +import { getSochainUrl } from '../../../../shared/api/sochain' +import { LedgerError, LedgerErrorId, Network } from '../../../../shared/api/types' +import { getNodeAuth, getNodeUrl } from '../../../../shared/litecoin/client' +import { toClientNetwork } from '../../../../shared/utils/client' +import { isError } from '../../../../shared/utils/guard' +import { getDerivationPath } from './common' + +/** + * Sends LTC tx using Ledger + */ +export const send = async ({ + transport, + network, + sender, + recipient, + amount, + feeRate, + memo, + walletIndex +}: { + transport: Transport + network: Network + sender?: Address + recipient: Address + amount: BaseAmount + feeRate: FeeRate + memo?: string + walletIndex: number +}): Promise> => { + if (!sender) { + return E.left({ + errorId: LedgerErrorId.GET_ADDRESS_FAILED, + msg: `Getting sender address using Ledger failed` + }) + } + + try { + const app = new AppBTC(transport) + const clientNetwork = toClientNetwork(network) + const derivePath = getDerivationPath(walletIndex, clientNetwork) + + const { psbt, utxos } = await buildTx({ + amount, + recipient, + memo, + feeRate, + sender, + network: clientNetwork, + sochainUrl: getSochainUrl(), + withTxHex: true + }) + + const inputs: Array<[Transaction, number, string | null, number | null]> = utxos.map(({ txHex, hash, index }) => { + if (!txHex) { + throw Error(`Missing 'txHex' for UTXO (txHash ${hash})`) + } + const utxoTx = Bitcoin.Transaction.fromHex(txHex) + const splittedTx = app.splitTransaction(txHex, utxoTx.hasWitnesses()) + return [splittedTx, index, null, null] + }) + + const associatedKeysets: string[] = inputs.map((_) => derivePath) + + const newTxHex = psbt.data.globalMap.unsignedTx.toBuffer().toString('hex') + const newTx: Transaction = app.splitTransaction(newTxHex, true) + + const outputScriptHex = app.serializeTransactionOutputs(newTx).toString('hex') + + const txHex = await app.createPaymentTransactionNew({ + inputs, + associatedKeysets, + outputScriptHex, + segwit: true, + useTrustedInputForSegwit: true, + additionals: ['bech32'] + }) + + const nodeUrl = getNodeUrl(network) + const auth = getNodeAuth() + const txHash = await broadcastTx({ txHex, nodeUrl, auth }) + + if (!txHash) { + return E.left({ + errorId: LedgerErrorId.INVALID_RESPONSE, + msg: `Post request to send LTC transaction using Ledger failed` + }) + } + return E.right(txHash) + } catch (error) { + return E.left({ + errorId: LedgerErrorId.SEND_TX_FAILED, + msg: isError(error) ? error?.message ?? error.toString() : `${error}` + }) + } +} diff --git a/src/main/api/ledger/transaction.ts b/src/main/api/ledger/transaction.ts index ebb2b381c..10911e9b3 100644 --- a/src/main/api/ledger/transaction.ts +++ b/src/main/api/ledger/transaction.ts @@ -1,6 +1,6 @@ import TransportNodeHidSingleton from '@ledgerhq/hw-transport-node-hid-singleton' import { TxHash } from '@xchainjs/xchain-client' -import { BNBChain, BTCChain, THORChain } from '@xchainjs/xchain-util' +import { BNBChain, BTCChain, LTCChain, THORChain } from '@xchainjs/xchain-util' import * as E from 'fp-ts/Either' import { IPCLedgerDepositTxParams, IPCLedgerSendTxParams } from '../../../shared/api/io' @@ -8,6 +8,7 @@ import { LedgerError, LedgerErrorId } from '../../../shared/api/types' import { isError } from '../../../shared/utils/guard' import * as BNB from './binance/transaction' import * as BTC from './bitcoin/transaction' +import * as LTC from './litecoin/transaction' import * as THOR from './thorchain/transaction' export const sendTx = async ({ @@ -59,6 +60,18 @@ export const sendTx = async ({ walletIndex }) break + case LTCChain: + res = await LTC.send({ + transport, + network, + sender, + recipient, + amount, + feeRate, + memo, + walletIndex + }) + break default: res = E.left({ errorId: LedgerErrorId.NOT_IMPLEMENTED, diff --git a/src/renderer/components/swap/Swap.tsx b/src/renderer/components/swap/Swap.tsx index 844aa70a3..498635ab6 100644 --- a/src/renderer/components/swap/Swap.tsx +++ b/src/renderer/components/swap/Swap.tsx @@ -38,7 +38,7 @@ import { to1e8BaseAmount, isChainAsset } from '../../helpers/assetHelper' -import { getChainAsset, isBtcChain, isEthChain } from '../../helpers/chainHelper' +import { getChainAsset, isBtcChain, isEthChain, isLtcChain } from '../../helpers/chainHelper' import { unionAssets } from '../../helpers/fp/array' import { eqAsset, eqBaseAmount, eqOAsset, eqOApproveParams, eqAddress, eqOAddress } from '../../helpers/fp/eq' import { sequenceSOption, sequenceTOption } from '../../helpers/fpHelpers' @@ -373,7 +373,7 @@ export const Swap = ({ () => FP.pipe( oSourceAsset, - O.map(({ chain }) => isBtcChain(chain) && useSourceAssetLedger), + O.map(({ chain }) => (isBtcChain(chain) || isLtcChain(chain)) && useSourceAssetLedger), O.getOrElse(() => false) ), [useSourceAssetLedger, oSourceAsset] @@ -1358,7 +1358,7 @@ export const Swap = ({ from={oSourcePoolAsset} to={oTargetPoolAsset} disableSlippage={disableSlippage} - disableSlippageMsg={intl.formatMessage({ id: 'swap.slip.tolerance.btc-ledger-disabled.info' })} + disableSlippageMsg={intl.formatMessage({ id: 'swap.slip.tolerance.ledger-disabled.info' })} /> diff --git a/src/renderer/components/wallet/txs/send/SendFormLTC.tsx b/src/renderer/components/wallet/txs/send/SendFormLTC.tsx index 16522d29e..f9782aa1e 100644 --- a/src/renderer/components/wallet/txs/send/SendFormLTC.tsx +++ b/src/renderer/components/wallet/txs/send/SendFormLTC.tsx @@ -11,7 +11,8 @@ import { baseAmount, baseToAsset, bn, - formatAssetAmountCurrency + formatAssetAmountCurrency, + LTCChain } from '@xchainjs/xchain-util' import { Row, Form } from 'antd' import { RadioChangeEvent } from 'antd/lib/radio' @@ -21,6 +22,7 @@ import * as O from 'fp-ts/lib/Option' import { useIntl } from 'react-intl' import { Network } from '../../../../../shared/api/types' +import { isKeystoreWallet, isLedgerWallet } from '../../../../../shared/utils/guard' import { WalletType } from '../../../../../shared/wallet/types' import { ZERO_BASE_AMOUNT, ZERO_BN } from '../../../../const' import { useSubscriptionState } from '../../../../hooks/useSubscriptionState' @@ -30,7 +32,7 @@ import { AddressValidation, GetExplorerTxUrl, OpenExplorerTxUrl, WalletBalances import { FeesWithRatesRD } from '../../../../services/litecoin/types' import { ValidatePasswordHandler } from '../../../../services/wallet/types' import { WalletBalance } from '../../../../services/wallet/types' -import { WalletPasswordConfirmationModal } from '../../../modal/confirmation' +import { LedgerConfirmationModal, WalletPasswordConfirmationModal } from '../../../modal/confirmation' import * as StyledR from '../../../shared/form/Radio.styles' import { MaxBalanceButton } from '../../../uielements/button/MaxBalanceButton' import { UIFeesRD } from '../../../uielements/fees' @@ -269,16 +271,10 @@ export const SendFormLTC: React.FC = (props): JSX.Element => { [intl, maxAmount] ) - // State for visibility of Modal to confirm tx - const [showPwModal, setShowPwModal] = useState(false) - // Send tx start time const [sendTxStartTime, setSendTxStartTime] = useState(0) const submitTx = useCallback(() => { - // close PW modal - setShowPwModal(false) - setSendTxStartTime(Date.now()) subscribeSendTxState( @@ -305,19 +301,40 @@ export const SendFormLTC: React.FC = (props): JSX.Element => { selectedFeeOption ]) - const renderPwModal = useMemo( - () => - showPwModal ? ( + const [showConfirmationModal, setShowConfirmationModal] = useState(false) + + const renderConfirmationModal = useMemo(() => { + const onSuccessHandler = () => { + setShowConfirmationModal(false) + submitTx() + } + const onCloseHandler = () => { + setShowConfirmationModal(false) + } + + if (isLedgerWallet(walletType)) { + return ( + + ) + } else if (isKeystoreWallet(walletType)) { + return ( setShowPwModal(false)} + onSuccess={onSuccessHandler} + onClose={onCloseHandler} validatePassword$={validatePassword$} /> - ) : ( - <> - ), - [submitTx, showPwModal, validatePassword$] - ) + ) + } else { + return null + } + }, [intl, network, submitTx, showConfirmationModal, validatePassword$, walletType]) const renderTxModal = useMemo( () => @@ -405,7 +422,7 @@ export const SendFormLTC: React.FC = (props): JSX.Element => { // Default value for RadioGroup of feeOptions feeRate: DEFAULT_FEE_OPTION }} - onFinish={() => setShowPwModal(true)} + onFinish={() => setShowConfirmationModal(true)} labelCol={{ span: 24 }}> @@ -446,7 +463,7 @@ export const SendFormLTC: React.FC = (props): JSX.Element => { - {renderPwModal} + {showConfirmationModal && renderConfirmationModal} {renderTxModal} ) diff --git a/src/renderer/const.ts b/src/renderer/const.ts index 7f9f69d4c..494e927ea 100644 --- a/src/renderer/const.ts +++ b/src/renderer/const.ts @@ -264,4 +264,4 @@ export const ASYM_DEPOSIT_TOOL_URL: Record = { } // @asgdx-team: Extend list whenever another ledger app will be supported -export const SUPPORTED_LEDGER_APPS: Chain[] = [THORChain, BNBChain, BTCChain] +export const SUPPORTED_LEDGER_APPS: Chain[] = [THORChain, BNBChain, BTCChain, LTCChain] diff --git a/src/renderer/hooks/useLiquidityProviders.ts b/src/renderer/hooks/useLiquidityProviders.ts index 97cdccbdd..0d53b8ab1 100644 --- a/src/renderer/hooks/useLiquidityProviders.ts +++ b/src/renderer/hooks/useLiquidityProviders.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import * as RD from '@devexperts/remote-data-ts' import { Address } from '@xchainjs/xchain-client' @@ -7,6 +7,7 @@ import * as A from 'fp-ts/lib/Array' import * as FP from 'fp-ts/lib/function' import * as O from 'fp-ts/lib/Option' import { useObservableState } from 'observable-hooks' +import * as RxOp from 'rxjs/operators' import { Network } from '../../shared/api/types' import { useThorchainContext } from '../contexts/ThorchainContext' @@ -36,7 +37,19 @@ export const useLiquidityProviders = ({ }) => { const { getLiquidityProviders } = useThorchainContext() - const [providers] = useObservableState(() => getLiquidityProviders({ asset, network }), RD.initial) + const [providers, networkUpdated] = useObservableState( + (network$) => + FP.pipe( + network$, + RxOp.distinctUntilChanged(), + RxOp.switchMap((network) => getLiquidityProviders({ asset, network })) + ), + RD.initial + ) + + // `networkUpdated` needs to be called whenever network has been updated + // to update `useObservableState` properly to push latest `network` into `getLiquidityProviders` + useEffect(() => networkUpdated(network), [network, networkUpdated]) /** * Gets liquidity provider data by given RUNE + asset address diff --git a/src/renderer/i18n/de/swap.ts b/src/renderer/i18n/de/swap.ts index 3cfcf30fe..c907af4a2 100644 --- a/src/renderer/i18n/de/swap.ts +++ b/src/renderer/i18n/de/swap.ts @@ -12,8 +12,8 @@ const swap: SwapMessages = { 'swap.slip.tolerance': 'Slippage-Toleranz', 'swap.slip.tolerance.info': 'Je höher die Prozentangabe, je höher akzeptierst Du ein Slippage. Mehr Slippage bedeutet zugleich ein größerer Spielraum zur Abdeckung der geschätzten Gebühren, um fehlgeschlagene Swaps zu vermeiden.', - 'swap.slip.tolerance.btc-ledger-disabled.info': - 'Das Auswählen der Slippage-Toleranz ist deaktiviert aufgrund technischer Probleme mit Ledger und BTC.', + 'swap.slip.tolerance.ledger-disabled.info': + 'Slippage-Toleranz ist deaktiviert aufgrund technischer Probleme mit Ledger.', 'swap.errors.amount.balanceShouldCoverChainFee': 'Transaktionsgebühr in Höhe von {fee} ist nicht über Dein Guthaben {balance} gedeckt.', 'swap.errors.amount.outputShouldCoverChainFee': diff --git a/src/renderer/i18n/en/swap.ts b/src/renderer/i18n/en/swap.ts index 45c8993b9..89ac3efce 100644 --- a/src/renderer/i18n/en/swap.ts +++ b/src/renderer/i18n/en/swap.ts @@ -12,8 +12,8 @@ const swap: SwapMessages = { 'swap.slip.tolerance': 'Slippage tolerance', 'swap.slip.tolerance.info': 'The higher the percentage, the more slippage you will accept. More slippage includes also a wider range for covering estimated fees to avoid aborted swaps.', - 'swap.slip.tolerance.btc-ledger-disabled.info': - 'Selecting slippage tolerance has been disabled due technical issues with Ledger and BTC.', + 'swap.slip.tolerance.ledger-disabled.info': + 'Selecting slippage tolerance has been disabled due technical issues with Ledger.', 'swap.errors.amount.balanceShouldCoverChainFee': 'Transaction fee {fee} needs to be covered by your balance (currently {balance}).', 'swap.errors.amount.outputShouldCoverChainFee': diff --git a/src/renderer/i18n/fr/swap.ts b/src/renderer/i18n/fr/swap.ts index afb1a0326..263ec3b21 100644 --- a/src/renderer/i18n/fr/swap.ts +++ b/src/renderer/i18n/fr/swap.ts @@ -12,8 +12,8 @@ const swap: SwapMessages = { 'swap.slip.tolerance': 'Tolérance de slippage', 'swap.slip.tolerance.info': "Plus le pourcentage est élevé, plus vous acceptez de slippage. Ceci inclus également un écart plus important pour couvrir les frais estimés, afin d'éviter les échanges avortés.", - 'swap.slip.tolerance.btc-ledger-disabled.info': - 'Selecting slippage tolerance has been disabled due technical issues with Ledger and BTC. - FR', + 'swap.slip.tolerance.ledger-disabled.info': + 'Selecting slippage tolerance has been disabled due technical issues with Ledger. - FR', 'swap.errors.amount.balanceShouldCoverChainFee': '{fee} de frais de transaction doivent être couverts par votre solde (actuellement {balance}).', 'swap.errors.amount.outputShouldCoverChainFee': diff --git a/src/renderer/i18n/ru/swap.ts b/src/renderer/i18n/ru/swap.ts index e84ba58b8..006f280d5 100644 --- a/src/renderer/i18n/ru/swap.ts +++ b/src/renderer/i18n/ru/swap.ts @@ -12,8 +12,8 @@ const swap: SwapMessages = { 'swap.slip.tolerance': 'Допуск по проскальзыванию', 'swap.slip.tolerance.info': 'Чем выше процент, тем большее проскальзывание вы допускаете. Большее проскальзывание включает также более широкий диапазон расчёта комиссий во избежание прерывания обмена.', - 'swap.slip.tolerance.btc-ledger-disabled.info': - 'Selecting slippage tolerance has been disabled due technical issues with Ledger and BTC. - RU', + 'swap.slip.tolerance.ledger-disabled.info': + 'Selecting slippage tolerance has been disabled due technical issues with Ledger. - RU', 'swap.errors.amount.balanceShouldCoverChainFee': 'Комиссия транзакции {fee} дожна покрываться вашим балансом (сейчас {balance}).', 'swap.errors.amount.outputShouldCoverChainFee': diff --git a/src/renderer/i18n/types.ts b/src/renderer/i18n/types.ts index 0a09802c4..f2aa5d2d6 100644 --- a/src/renderer/i18n/types.ts +++ b/src/renderer/i18n/types.ts @@ -293,7 +293,7 @@ type SwapMessageKey = | 'swap.slip.title' | 'swap.slip.tolerance' | 'swap.slip.tolerance.info' - | 'swap.slip.tolerance.btc-ledger-disabled.info' + | 'swap.slip.tolerance.ledger-disabled.info' | 'swap.state.pending' | 'swap.state.success' | 'swap.state.error' diff --git a/src/renderer/services/bitcoin/common.ts b/src/renderer/services/bitcoin/common.ts index b52ab8773..4cadad53e 100644 --- a/src/renderer/services/bitcoin/common.ts +++ b/src/renderer/services/bitcoin/common.ts @@ -7,7 +7,8 @@ import * as Rx from 'rxjs' import { Observable } from 'rxjs' import * as RxOp from 'rxjs/operators' -import { getHaskoinApiUrl, getSochainUrl } from '../../../shared/bitcoin/client' +import { getSochainUrl } from '../../../shared/api/sochain' +import { getHaskoinApiUrl } from '../../../shared/bitcoin/client' import { isError } from '../../../shared/utils/guard' import { clientNetwork$ } from '../app/service' import * as C from '../clients' diff --git a/src/renderer/services/chain/transaction/common.ts b/src/renderer/services/chain/transaction/common.ts index dfada0d01..ac71550cf 100644 --- a/src/renderer/services/chain/transaction/common.ts +++ b/src/renderer/services/chain/transaction/common.ts @@ -110,7 +110,7 @@ export const sendTx$ = ({ msg: error?.message ?? error.toString() })), liveData.chain(({ rates }) => { - return LTC.sendTx({ recipient, amount, asset, memo, feeRate: rates[feeOption], walletIndex }) + return LTC.sendTx({ walletType, recipient, amount, feeRate: rates[feeOption], memo, walletIndex, sender }) }) ) } diff --git a/src/renderer/services/litecoin/balances.ts b/src/renderer/services/litecoin/balances.ts index 748aa91f4..e4d1ba034 100644 --- a/src/renderer/services/litecoin/balances.ts +++ b/src/renderer/services/litecoin/balances.ts @@ -22,4 +22,7 @@ const reloadBalances = () => { const balances$ = (walletType: WalletType, walletIndex: number): C.WalletBalancesLD => C.balances$({ client$, trigger$: reloadBalances$, walletType, walletIndex, walletBalanceType: 'all' }) -export { balances$, reloadBalances, reloadBalances$, resetReloadBalances } +// State of balances loaded by Client and Address +const getBalanceByAddress$ = C.balancesByAddress$({ client$, trigger$: reloadBalances$, walletBalanceType: 'all' }) + +export { balances$, getBalanceByAddress$, reloadBalances, reloadBalances$, resetReloadBalances } diff --git a/src/renderer/services/litecoin/common.ts b/src/renderer/services/litecoin/common.ts index f1001bbb4..6b0751d5f 100644 --- a/src/renderer/services/litecoin/common.ts +++ b/src/renderer/services/litecoin/common.ts @@ -1,12 +1,13 @@ import * as RD from '@devexperts/remote-data-ts' -import { Client, NodeAuth } from '@xchainjs/xchain-litecoin' +import { Client } from '@xchainjs/xchain-litecoin' import { LTCChain } from '@xchainjs/xchain-util' import * as FP from 'fp-ts/function' import * as O from 'fp-ts/Option' import * as Rx from 'rxjs' import * as RxOp from 'rxjs/operators' -import { envOrDefault } from '../../../shared/utils/env' +import { getSochainUrl } from '../../../shared/api/sochain' +import { getNodeAuth, getNodeUrl } from '../../../shared/litecoin/client' import { isError } from '../../../shared/utils/guard' import { clientNetwork$ } from '../app/service' import * as C from '../clients' @@ -14,17 +15,6 @@ import { keystoreService } from '../wallet/keystore' import { getPhrase } from '../wallet/util' import { Client$, ClientState$, ClientState } from './types' -const LTC_NODE_TESTNET_URL = envOrDefault( - process.env.REACT_APP_LTC_NODE_TESTNET_URL, - 'https://testnet.ltc.thorchain.info' -) -const LTC_NODE_MAINNET_URL = envOrDefault(process.env.REACT_APP_LTC_NODE_MAINNET_URL, 'https://ltc.thorchain.info') - -const NODE_AUTH: NodeAuth = { - password: envOrDefault(process.env.REACT_APP_LTC_NODE_PASSWORD, 'password'), - username: envOrDefault(process.env.REACT_APP_LTC_NODE_USERNAME, 'thorchain') -} - /** * Stream to create an observable `LitecoinClient` depending on existing phrase in keystore * @@ -41,12 +31,12 @@ const clientState$: ClientState$ = FP.pipe( getPhrase(keystore), O.map((phrase) => { try { - const nodeUrl = network === 'mainnet' ? LTC_NODE_MAINNET_URL : LTC_NODE_TESTNET_URL const client = new Client({ network, phrase, - nodeUrl, - nodeAuth: NODE_AUTH + nodeUrl: getNodeUrl(network), + nodeAuth: getNodeAuth(), + sochainUrl: getSochainUrl() }) return RD.success(client) } catch (error) { diff --git a/src/renderer/services/litecoin/index.ts b/src/renderer/services/litecoin/index.ts index 8c970c469..c4d21612c 100644 --- a/src/renderer/services/litecoin/index.ts +++ b/src/renderer/services/litecoin/index.ts @@ -1,9 +1,10 @@ -import { reloadBalances, balances$, reloadBalances$, resetReloadBalances } from './balances' +import { network$ } from '../app/service' +import { reloadBalances, balances$, getBalanceByAddress$, reloadBalances$, resetReloadBalances } from './balances' import { client$, clientState$, address$, addressUI$, explorerUrl$ } from './common' import { createFeesService } from './fees' import { createTransactionService } from './transaction' -const { txs$, tx$, txStatus$, subscribeTx, resetTx, sendTx, txRD$ } = createTransactionService(client$) +const { txs$, tx$, txStatus$, subscribeTx, resetTx, sendTx, txRD$ } = createTransactionService(client$, network$) const { reloadFees, fees$, feesWithRates$, reloadFeesWithRates } = createFeesService(client$) export { @@ -14,6 +15,7 @@ export { explorerUrl$, reloadBalances, balances$, + getBalanceByAddress$, reloadBalances$, resetReloadBalances, txs$, diff --git a/src/renderer/services/litecoin/transaction.ts b/src/renderer/services/litecoin/transaction.ts index cea4e0520..2ed304c05 100644 --- a/src/renderer/services/litecoin/transaction.ts +++ b/src/renderer/services/litecoin/transaction.ts @@ -1,5 +1,70 @@ +import * as RD from '@devexperts/remote-data-ts' +import { TxHash } from '@xchainjs/xchain-client' +import { AssetLTC, LTCChain } from '@xchainjs/xchain-util' +import * as E from 'fp-ts/lib/Either' +import * as FP from 'fp-ts/lib/function' +import * as Rx from 'rxjs' +import * as RxOp from 'rxjs/operators' + +import { IPCLedgerSendTxParams, ipcLedgerSendTxParamsIO } from '../../../shared/api/io' +import { LedgerError, Network } from '../../../shared/api/types' +import { isLedgerWallet } from '../../../shared/utils/guard' +import { Network$ } from '../app/types' import * as C from '../clients' -import { Client$ } from './types' +import { TxHashLD, ErrorId } from '../wallet/types' +import { Client$, SendTxParams } from './types' import { TransactionService } from './types' -export const createTransactionService: (client$: Client$) => TransactionService = C.createTransactionService +export const createTransactionService = (client$: Client$, network$: Network$): TransactionService => { + const common = C.createTransactionService(client$) + + const sendLedgerTx = ({ network, params }: { network: Network; params: SendTxParams }): TxHashLD => { + const { amount, sender, recipient, memo, walletIndex, feeRate } = params + const sendLedgerTxParams: IPCLedgerSendTxParams = { + chain: LTCChain, + asset: AssetLTC, + network, + amount, + sender, + feeRate, + recipient, + memo, + walletIndex + } + const encoded = ipcLedgerSendTxParamsIO.encode(sendLedgerTxParams) + + return FP.pipe( + Rx.from(window.apiHDWallet.sendLedgerTx(encoded)), + RxOp.switchMap( + FP.flow( + E.fold( + ({ msg }) => + Rx.of( + RD.failure({ + errorId: ErrorId.SEND_LEDGER_TX, + msg: `Sending Ledger LTC tx failed. (${msg})` + }) + ), + (txHash) => Rx.of(RD.success(txHash)) + ) + ) + ), + RxOp.startWith(RD.pending) + ) + } + + const sendTx = (params: SendTxParams): TxHashLD => + FP.pipe( + network$, + RxOp.switchMap((network) => { + if (isLedgerWallet(params.walletType)) return sendLedgerTx({ network, params }) + + return common.sendTx(params) + }) + ) + + return { + ...common, + sendTx + } +} diff --git a/src/renderer/services/litecoin/types.ts b/src/renderer/services/litecoin/types.ts index 06f4ab4d2..1c4bd6405 100644 --- a/src/renderer/services/litecoin/types.ts +++ b/src/renderer/services/litecoin/types.ts @@ -1,8 +1,9 @@ import * as RD from '@devexperts/remote-data-ts' -import { FeesWithRates } from '@xchainjs/xchain-client' +import { Address, FeesWithRates } from '@xchainjs/xchain-client' import { Client } from '@xchainjs/xchain-litecoin' -import { Asset, BaseAmount } from '@xchainjs/xchain-util' +import { BaseAmount } from '@xchainjs/xchain-util' +import { WalletType } from '../../../shared/wallet/types' import { LiveData } from '../../helpers/rx/liveData' import { Memo } from '../chain/types' import * as C from '../clients' @@ -21,11 +22,12 @@ export type FeesService = C.FeesService & { } export type SendTxParams = { + walletType: WalletType + sender?: Address recipient: string amount: BaseAmount - asset: Asset - memo?: string feeRate: number + memo?: string walletIndex: number } diff --git a/src/renderer/services/wallet/balances.ts b/src/renderer/services/wallet/balances.ts index df9b21be5..6ac807efd 100644 --- a/src/renderer/services/wallet/balances.ts +++ b/src/renderer/services/wallet/balances.ts @@ -349,6 +349,15 @@ export const createBalancesService = ({ })) ) + /** + * LTC Ledger balances + */ + const ltcLedgerChainBalance$: ChainBalance$ = ledgerChainBalance$({ + chain: LTCChain, + walletBalanceType: 'all', + getBalanceByAddress$: LTC.getBalanceByAddress$ + }) + /** * Transforms BCH balances into `ChainBalances` */ @@ -496,12 +505,12 @@ export const createBalancesService = ({ Rx.combineLatest( filterEnabledChains({ THOR: [thorChainBalance$, thorLedgerChainBalance$], - // for BTC we store + // for BTC we store `confirmed` or `all` (confirmed + unconfirmed) balances BTC: [btcChainBalance$, btcChainBalanceConfirmed$, btcLedgerChainBalance$, btcLedgerChainBalanceConfirmed$], BCH: [bchChainBalance$], ETH: [ethChainBalance$], BNB: [bnbChainBalance$, bnbLedgerChainBalance$], - LTC: [ltcBalance$], + LTC: [ltcBalance$, ltcLedgerChainBalance$], DOGE: [dogeChainBalance$] }) ), diff --git a/src/shared/api/sochain.ts b/src/shared/api/sochain.ts new file mode 100644 index 000000000..67bffcd8d --- /dev/null +++ b/src/shared/api/sochain.ts @@ -0,0 +1,3 @@ +import { envOrDefault } from '../utils/env' + +export const getSochainUrl = (): string => envOrDefault(process.env.REACT_APP_SOCHAIN_URL, 'https://sochain.com/api/v2') diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index 2e01ebe4f..6a4edf332 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -106,6 +106,11 @@ export type LedgerBTCTxInfo = Pick & { sender: Address } +export type LedgerLTCTxInfo = Pick & { + feeRate: FeeRate + sender: Address +} + export type LedgerTxParams = LedgerTHORTxParams | LedgerBNBTxParams export type IPCLedgerAdddressParams = { chain: Chain; network: Network; walletIndex: number } diff --git a/src/shared/bitcoin/client.ts b/src/shared/bitcoin/client.ts index e10e95230..92d37485f 100644 --- a/src/shared/bitcoin/client.ts +++ b/src/shared/bitcoin/client.ts @@ -14,5 +14,3 @@ export const getHaskoinApiUrl = (): ClientUrl => { mainnet: APP_HASKOIN_MAINNET_URL } } - -export const getSochainUrl = (): string => envOrDefault(process.env.REACT_APP_SOCHAIN_URL, 'https://sochain.com/api/v2') diff --git a/src/shared/litecoin/client.ts b/src/shared/litecoin/client.ts new file mode 100644 index 000000000..e9bd2b398 --- /dev/null +++ b/src/shared/litecoin/client.ts @@ -0,0 +1,16 @@ +import { NodeAuth } from '@xchainjs/xchain-litecoin' + +import { Network } from '../api/types' +import { envOrDefault } from '../utils/env' + +export const getNodeUrl = (network: Network): string => { + const testnetUrl = envOrDefault(process.env.REACT_APP_LTC_NODE_TESTNET_URL, 'https://testnet.ltc.thorchain.info') + const mainnetUrl = envOrDefault(process.env.REACT_APP_LTC_NODE_MAINNET_URL, 'https://ltc.thorchain.info') + + return network === 'testnet' ? testnetUrl : mainnetUrl +} + +export const getNodeAuth = (): NodeAuth => ({ + password: envOrDefault(process.env.REACT_APP_LTC_NODE_PASSWORD, 'password'), + username: envOrDefault(process.env.REACT_APP_LTC_NODE_USERNAME, 'thorchain') +}) diff --git a/yarn.lock b/yarn.lock index 2dc305c12..ecc0b0f88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4656,10 +4656,10 @@ resolved "https://registry.yarnpkg.com/@xchainjs/xchain-ethereum/-/xchain-ethereum-0.23.3.tgz#086c987eb009564ca890445f1e51c0ca28584957" integrity sha512-E53jYeiqeU8Plcx/Pyd1wA556zZbMnawIiSWPXlX8H+JkM1ETPO7ajNiWnRlf3bj9Igrql+sYSbHZIEpSnmSoA== -"@xchainjs/xchain-litecoin@^0.7.2": - version "0.7.2" - resolved "https://registry.yarnpkg.com/@xchainjs/xchain-litecoin/-/xchain-litecoin-0.7.2.tgz#3d1f6cbfd271f7214aa29f3488cc88e69858ab86" - integrity sha512-NSq1K1Wnmq1L7buRUICkF4GPw3cVQj2iALJa0ZQpradH6NM6UUf6BqFqIerjFZugiF2lxBQa+rqsqZ7vEAtfuQ== +"@xchainjs/xchain-litecoin@^0.8.0-alpha.1": + version "0.8.0-alpha.1" + resolved "https://registry.yarnpkg.com/@xchainjs/xchain-litecoin/-/xchain-litecoin-0.8.0-alpha.1.tgz#8b77cf135ea1f2d95209eab3216d93cd207ccde4" + integrity sha512-p5idwYUi35WMwjZ7MaY2yU6zvXZgP0tD8bGMh8wdb3r8iTM9AaSmQsXstzgCIzREnbirf8KR07LLJ29OyNkWQA== "@xchainjs/xchain-terra@^0.1.0-alpha.2": version "0.1.0-alpha.2"