diff --git a/i18n/locales/en/generic.json b/i18n/locales/en/generic.json index f43ac81e4..5f9b4b077 100644 --- a/i18n/locales/en/generic.json +++ b/i18n/locales/en/generic.json @@ -20,7 +20,7 @@ "cosigner-lacking-key": "Cannot add key pair as co-signer of an account, since no public key for the account to co-sign has been provided", "expected-json-response-error": "Expected {{url}} to return a JSON response. Content type was {{contentType}} instead.", "existing-account-error": "An account with that name does already exist.", - "fetch-account-data-error": "Cannot fetch account data of {{account}} from {{horizon}}", + "fetch-account-data-error": "Cannot fetch account data of {{account}}", "fetch-signature-requests-error": "Fetching signature requests failed: {{response}} \nService: {{service}}", "fetch-web-auth-challenge-error": "Cannot fetch web auth challenge", "http-request-error": "HTTP fetch failed: {{response}} \nService: {{service}}", diff --git a/i18n/locales/es/generic.json b/i18n/locales/es/generic.json index d824cc44e..ba4fea35e 100644 --- a/i18n/locales/es/generic.json +++ b/i18n/locales/es/generic.json @@ -19,7 +19,7 @@ "bio-auth-test-canceled": "Autenticación biométrica anulada", "expected-json-response-error": "Se esperaba que {{url}} responda en formato JSON. El tipo de contenido fue {{contentType}}.", "existing-account-error": "Ya existe otra cuenta con el mismo nombre.", - "fetch-account-data-error": "Imposible recuperar los datos de la cuenta de {{account}} desde {{horizon}}", + "fetch-account-data-error": "Imposible recuperar los datos de la cuenta de {{account}}", "fetch-web-auth-challenge-error": "Error al obtener el desafío de autenticación", "fetch-signature-requests-error": "Fallaron las solicitudes de recuperar firmas: {{response}} \nServicio: {{service}}", "invariant-violation-error": "Violación invariante: {{message}}", @@ -96,4 +96,4 @@ "user-interface": { "copied-to-clipboard": "Copiado a Portapapeles." } -} \ No newline at end of file +} diff --git a/i18n/locales/it/generic.json b/i18n/locales/it/generic.json index 4259ce46f..63e30ce07 100755 --- a/i18n/locales/it/generic.json +++ b/i18n/locales/it/generic.json @@ -16,7 +16,7 @@ "bio-auth-test-canceled": "Autenticazione biometrica annullata", "expected-json-response-error": "Previsto che {{url}} restituisse una risposta JSON. Il tipo di contenuto era invece {{contentType}}.", "existing-account-error": "Esiste già un account con quel nome.", - "fetch-account-data-error": "Impossibile recuperare i dati dell'account di {{account}} da {{horizon}}", + "fetch-account-data-error": "Impossibile recuperare i dati dell'account di {{account}}", "fetch-signature-requests-error": "Recupero delle richieste di firma non riuscito: {{response}} \nService: {{service}}", "invariant-violation-error": "Violazione invariante: {{message}}", "low-reserve-order-error": "Impossibile effettuare l'ordine perché il saldo XLM spendibile è troppo basso.", diff --git a/src/Account/components/AccountTransactions.tsx b/src/Account/components/AccountTransactions.tsx index 50667b3e5..a00b4bebe 100644 --- a/src/Account/components/AccountTransactions.tsx +++ b/src/Account/components/AccountTransactions.tsx @@ -6,7 +6,6 @@ import UpdateIcon from "@material-ui/icons/Update" import { Account } from "~App/contexts/accounts" import { SettingsContext } from "~App/contexts/settings" import { SignatureDelegationContext } from "~App/contexts/signatureDelegation" -import { useHorizonURLs } from "~Generic/hooks/stellar" import { useLiveRecentTransactions, useLiveAccountData, @@ -17,6 +16,7 @@ import { useLoadingState } from "~Generic/hooks/util" import * as routes from "~App/routes" import MainSelectionButton from "~Generic/components/MainSelectionButton" import { VerticalLayout } from "~Layout/components/Box" +import { getHorizonURLs } from "~Workers/net-worker/stellar-network" import FriendbotButton from "./FriendbotButton" import OfferList from "./OfferList" import { InteractiveSignatureRequestList } from "./SignatureRequestList" @@ -69,7 +69,7 @@ function AccountTransactions(props: { account: Account }) { const { account } = props const { t } = useTranslation() const accountData = useLiveAccountData(account.accountID, account.testnet) - const horizonURLs = useHorizonURLs(account.testnet) + const horizonURLs = getHorizonURLs(account.testnet) const isSmallScreen = useIsMobile() const [moreTxsLoadingState, handleMoreTxsFetch] = useLoadingState() const recentTxs = useLiveRecentTransactions(account.accountID, account.testnet) diff --git a/src/Account/components/OfferList.tsx b/src/Account/components/OfferList.tsx index eae9946bc..38cd02f53 100644 --- a/src/Account/components/OfferList.tsx +++ b/src/Account/components/OfferList.tsx @@ -1,7 +1,7 @@ import BigNumber from "big.js" import React from "react" import { Trans, useTranslation } from "react-i18next" -import { Operation, Server, ServerApi, Transaction } from "stellar-sdk" +import { Operation, ServerApi, Transaction } from "stellar-sdk" import ExpansionPanel from "@material-ui/core/ExpansionPanel" import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails" import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary" @@ -17,7 +17,6 @@ import { Account } from "~App/contexts/accounts" import { breakpoints } from "~App/theme" import { trackError } from "~App/contexts/notifications" import { ActionButton } from "~Generic/components/DialogActions" -import { useHorizon } from "~Generic/hooks/stellar" import { useLoadingState } from "~Generic/hooks/util" import { useLiveAccountData, useLiveAccountOffers, useOlderOffers } from "~Generic/hooks/stellar-subscriptions" import { useIsMobile } from "~Generic/hooks/userinterface" @@ -30,7 +29,6 @@ import TransactionSender from "~Transaction/components/TransactionSender" import { SingleBalance } from "./AccountBalances" function createDismissalTransaction( - horizon: Server, account: Account, accountData: AccountData, offer: ServerApi.OfferRecord @@ -49,7 +47,7 @@ function createDismissalTransaction( selling }) ], - { accountData, horizon, walletAccount: account } + { accountData, walletAccount: account } ) } else { return createTransaction( @@ -62,7 +60,7 @@ function createDismissalTransaction( selling }) ], - { accountData, horizon, walletAccount: account } + { accountData, walletAccount: account } ) } } @@ -216,7 +214,6 @@ const useStyles = makeStyles({ function OfferList(props: Props & { sendTransaction: (tx: Transaction) => Promise }) { const accountData = useLiveAccountData(props.account.accountID, props.account.testnet) const classes = useStyles() - const horizon = useHorizon(props.account.testnet) const offerHistory = useLiveAccountOffers(props.account.publicKey, props.account.testnet) const [moreTxsLoadingState, handleMoreTxsFetch] = useLoadingState() const fetchMoreOffers = useOlderOffers(props.account.publicKey, props.account.testnet) @@ -230,7 +227,7 @@ function OfferList(props: Props & { sendTransaction: (tx: Transaction) => Promis const onCancel = async (offer: ServerApi.OfferRecord) => { try { - const tx = await createDismissalTransaction(horizon, props.account, accountData, offer) + const tx = await createDismissalTransaction(props.account, accountData, offer) await props.sendTransaction(tx) } catch (error) { trackError(error) diff --git a/src/Account/components/TransactionList.tsx b/src/Account/components/TransactionList.tsx index daac9140e..f750abbc4 100644 --- a/src/Account/components/TransactionList.tsx +++ b/src/Account/components/TransactionList.tsx @@ -30,6 +30,7 @@ import { matchesRoute } from "~Generic/lib/routes" import MemoMessage from "~Transaction/components/MemoMessage" import TransactionReviewDialog from "~TransactionReview/components/TransactionReviewDialog" import { useOperationTitle } from "~TransactionReview/components/Operations" +import { getNetwork } from "~Workers/net-worker/stellar-network" import { SingleBalance } from "./AccountBalances" const dedupe = (array: T[]): T[] => Array.from(new Set(array)) @@ -405,7 +406,7 @@ export const TransactionListItem = React.memo(function TransactionListItem(props const { onOpenTransaction } = props const restoredTransaction = React.useMemo( - () => TransactionBuilder.fromXDR(props.transactionEnvelopeXdr, props.testnet ? Networks.TESTNET : Networks.PUBLIC), + () => TransactionBuilder.fromXDR(props.transactionEnvelopeXdr, getNetwork(props.testnet)), [props.testnet, props.transactionEnvelopeXdr] ) @@ -506,7 +507,7 @@ function TransactionList(props: TransactionListProps) { return null } - const network = props.account.testnet ? Networks.TESTNET : Networks.PUBLIC + const network = getNetwork(props.account.testnet) const txResponse = props.transactions.find(recentTx => recentTx.hash === openedTxHash) let tx = txResponse ? TransactionBuilder.fromXDR(txResponse.envelope_xdr, network) : null diff --git a/src/AccountSettings/components/AccountDeletionDialog.tsx b/src/AccountSettings/components/AccountDeletionDialog.tsx index 81a986184..531c4c69d 100644 --- a/src/AccountSettings/components/AccountDeletionDialog.tsx +++ b/src/AccountSettings/components/AccountDeletionDialog.tsx @@ -1,6 +1,6 @@ import React from "react" import { useTranslation } from "react-i18next" -import { Operation, Transaction, Server } from "stellar-sdk" +import { Operation, Transaction } from "stellar-sdk" import DialogContent from "@material-ui/core/DialogContent" import DialogContentText from "@material-ui/core/DialogContentText" import Switch from "@material-ui/core/Switch" @@ -90,7 +90,6 @@ interface Warning { interface AccountDeletionDialogProps { account: Account - horizon: Server onClose: () => void onDelete: () => void sendTransaction: (transaction: Transaction) => void @@ -98,7 +97,6 @@ interface AccountDeletionDialogProps { function AccountDeletionDialog(props: AccountDeletionDialogProps) { const accountData = useLiveAccountData(props.account.accountID, props.account.testnet) - const horizon = props.horizon const { accounts } = React.useContext(AccountsContext) const [mergeAccountEnabled, setMergeAccountEnabled] = React.useState(false) @@ -126,7 +124,7 @@ function AccountDeletionDialog(props: AccountDeletionDialogProps) { destination: selectedMergeAccount.publicKey }) ], - { accountData, horizon, walletAccount: props.account } + { accountData, walletAccount: props.account } ) await props.sendTransaction(transaction) diff --git a/src/App/bootstrap/context.tsx b/src/App/bootstrap/context.tsx index 474acb9e8..7f6c43f9a 100644 --- a/src/App/bootstrap/context.tsx +++ b/src/App/bootstrap/context.tsx @@ -4,20 +4,17 @@ import { CachingProviders } from "../contexts/caches" import { NotificationsProvider } from "../contexts/notifications" import { SettingsProvider } from "../contexts/settings" import { SignatureDelegationProvider } from "../contexts/signatureDelegation" -import { StellarProvider } from "../contexts/stellar" export function ContextProviders(props: { children: React.ReactNode }) { return ( - - - - - - {props.children} - - - - - + + + + + {props.children} + + + + ) } diff --git a/src/App/components/DesktopNotifications.tsx b/src/App/components/DesktopNotifications.tsx index 72b402892..9514585c0 100644 --- a/src/App/components/DesktopNotifications.tsx +++ b/src/App/components/DesktopNotifications.tsx @@ -3,7 +3,6 @@ import { TFunction } from "i18next" import React from "react" import { useTranslation } from "react-i18next" import { Asset, Horizon, ServerApi } from "stellar-sdk" -import { useHorizonURLs } from "~Generic/hooks/stellar" import { useLiveAccountEffects } from "~Generic/hooks/stellar-subscriptions" import { useRouter } from "~Generic/hooks/userinterface" import { useSingleton } from "~Generic/hooks/util" @@ -12,6 +11,7 @@ import { MultisigTransactionResponse } from "~Generic/lib/multisig-service" import { showNotification } from "~Platform/notifications" import { formatBalance } from "~Generic/lib/balances" import { OfferDetailsString } from "~TransactionReview/components/Operations" +import { getNetwork } from "~Workers/net-worker/stellar-network" import { NetWorker } from "~Workers/worker-controller" import { Account, AccountsContext } from "../contexts/accounts" import { trackError } from "../contexts/notifications" @@ -39,13 +39,7 @@ const isTradeEffect = (effect: ServerApi.EffectRecord): effect is TradeEffect => const isPaymentEffect = (effect: ServerApi.EffectRecord) => effect.type === "account_credited" || effect.type === "account_debited" -function createEffectHandlers( - router: ReturnType, - netWorker: NetWorker, - mainnetHorizonURLs: string[], - testnetHorizonURLs: string[], - t: TFunction -) { +function createEffectHandlers(router: ReturnType, netWorker: NetWorker, t: TFunction) { return { async handleTradeEffect(account: Account, effect: TradeEffect) { const buying = @@ -57,8 +51,8 @@ function createEffectHandlers( ? new Asset(effect.sold_asset_code, effect.sold_asset_issuer) : Asset.native() - const horizonURL = account.testnet ? testnetHorizonURLs : mainnetHorizonURLs - const openOffers = await netWorker.fetchAccountOpenOrders(horizonURL, account.accountID) + const network = getNetwork(account.testnet) + const openOffers = await netWorker.fetchAccountOpenOrders(account.accountID, network) const orderOnlyPartiallyExecuted = openOffers._embedded.records.find( offer => String(offer.id) === String(effect.offer_id) @@ -107,15 +101,11 @@ function DesktopNotifications() { const { accounts } = React.useContext(AccountsContext) const { subscribeToNewSignatureRequests } = React.useContext(SignatureDelegationContext) - const mainnetHorizonURLs = useHorizonURLs(false) - const testnetHorizonURLs = useHorizonURLs(true) const netWorker = useNetWorker() const router = useRouter() const { t } = useTranslation() - const effectHandlers = useSingleton(() => - createEffectHandlers(router, netWorker, mainnetHorizonURLs, testnetHorizonURLs, t) - ) + const effectHandlers = useSingleton(() => createEffectHandlers(router, netWorker, t)) const handleNewSignatureRequest = React.useCallback( (signatureRequest: MultisigTransactionResponse) => { diff --git a/src/App/contexts/stellar.tsx b/src/App/contexts/stellar.tsx deleted file mode 100644 index 1ccb4d444..000000000 --- a/src/App/contexts/stellar.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from "react" -import { useNetworkCacheReset } from "~Generic/hooks/stellar-subscriptions" -import { workers } from "~Workers/worker-controller" -import { trackError } from "./notifications" - -interface Props { - children: React.ReactNode -} - -interface ContextType { - isSelectionPending: boolean - pendingSelection: Promise - pubnetHorizonURLs: string[] - testnetHorizonURLs: string[] -} - -const initialHorizonSelection: Promise<[string[], string[]]> = (async () => { - const { netWorker } = await workers - - const pubnetHorizonURLs: string[] = Array.from( - new Set( - await Promise.all([ - "https://horizon.stellar.org", - netWorker.checkHorizonOrFailover("https://horizon.stellarx.com", "https://horizon.stellar.org"), - netWorker.checkHorizonOrFailover("https://horizon.stellar.lobstr.co", "https://horizon.stellar.org") - ]) - ) - ) - - const testnetHorizonURLs: string[] = [ - await netWorker.checkHorizonOrFailover( - "https://stellar-horizon-testnet.satoshipay.io/", - "https://horizon-testnet.stellar.org" - ) - ] - - return Promise.all([pubnetHorizonURLs, testnetHorizonURLs]) -})() - -initialHorizonSelection.catch(trackError) - -const initialValues: ContextType = { - isSelectionPending: true, - pendingSelection: initialHorizonSelection, - pubnetHorizonURLs: ["https://horizon.stellar.org"], - testnetHorizonURLs: ["https://stellar-horizon-testnet.satoshipay.io/"] -} - -const StellarContext = React.createContext(initialValues) - -export function StellarProvider(props: Props) { - const [contextValue, setContextValue] = React.useState(initialValues) - const resetNetworkCaches = useNetworkCacheReset() - - React.useEffect(() => { - let cancelled = false - - const init = async () => { - const { netWorker } = await workers - - setContextValue(prevState => ({ ...prevState, pendingSelection: initialHorizonSelection })) - const [pubnetHorizonURLs, testnetHorizonURLs] = await initialHorizonSelection - - if (!cancelled) { - setContextValue(prevState => ({ - isSelectionPending: false, - pendingSelection: prevState.pendingSelection, - pubnetHorizonURLs: - pubnetHorizonURLs !== prevState.pubnetHorizonURLs ? pubnetHorizonURLs : prevState.pubnetHorizonURLs, - testnetHorizonURLs: - testnetHorizonURLs !== prevState.testnetHorizonURLs ? testnetHorizonURLs : prevState.testnetHorizonURLs - })) - - if ( - pubnetHorizonURLs !== initialValues.pubnetHorizonURLs || - testnetHorizonURLs !== initialValues.testnetHorizonURLs - ) { - await netWorker.resetAllSubscriptions() - resetNetworkCaches() - } - - // tslint:disable-next-line no-console - console.debug(`Selected horizon servers:`, { pubnetHorizonURLs, testnetHorizonURLs }) - } - } - - if (navigator.onLine !== false) { - init().catch(trackError) - } - - const unsubscribe = () => { - cancelled = true - } - return unsubscribe - }, [resetNetworkCaches]) - - return {props.children} -} - -export { ContextType as StellarContextType, StellarContext } diff --git a/src/App/cordova/keystore.ts b/src/App/cordova/keystore.ts index bfa7f85e5..1b31b420d 100644 --- a/src/App/cordova/keystore.ts +++ b/src/App/cordova/keystore.ts @@ -1,6 +1,7 @@ import { KeyStore } from "key-store" -import { Transaction, Keypair, Networks } from "stellar-sdk" +import { Transaction, Keypair } from "stellar-sdk" import { Messages } from "~shared/ipc" +import { getNetwork } from "~Workers/net-worker/stellar-network" import { WrongPasswordError } from "~Generic/lib/errors" import { CommandHandlers, expose } from "./ipc" @@ -88,7 +89,7 @@ async function respondWithSignedTransaction( ) { try { const account = keyStore.getPublicKeyData(internalAccountID) - const networkPassphrase = account.testnet ? Networks.TESTNET : Networks.PUBLIC + const networkPassphrase = getNetwork(account.testnet) const transaction = new Transaction(transactionXDR, networkPassphrase) const privateKey = keyStore.getPrivateKeyData(internalAccountID, password).privateKey diff --git a/src/Assets/components/AddAssetDialog.tsx b/src/Assets/components/AddAssetDialog.tsx index 34ccb88e7..da8d3b33a 100644 --- a/src/Assets/components/AddAssetDialog.tsx +++ b/src/Assets/components/AddAssetDialog.tsx @@ -1,6 +1,6 @@ import React from "react" import { useTranslation } from "react-i18next" -import { Asset, AssetType, Horizon, Operation, Server, Transaction } from "stellar-sdk" +import { Asset, AssetType, Horizon, Operation, Transaction } from "stellar-sdk" import Dialog from "@material-ui/core/Dialog" import List from "@material-ui/core/List" import ListItem from "@material-ui/core/ListItem" @@ -228,7 +228,6 @@ const useAddAssetStyles = makeStyles({ interface AddAssetDialogProps { account: Account accountData: AccountData - horizon: Server hpadding: number itemHPadding: number onClose: () => void @@ -259,7 +258,6 @@ const AddAssetDialog = React.memo(function AddAssetDialog(props: AddAssetDialogP const operations = [Operation.changeTrust({ asset, limit: options.limit })] return createTransaction(operations, { accountData: props.accountData, - horizon: props.horizon, walletAccount: props.account }) } @@ -371,7 +369,6 @@ const AddAssetDialog = React.memo(function AddAssetDialog(props: AddAssetDialogP account={props.account} accountData={props.accountData} createAddAssetTransaction={createAddAssetTransaction} - horizon={props.horizon} onClose={closeCustomTrustlineDialog} sendTransaction={sendTransaction} txCreationPending={txCreationPending} @@ -385,9 +382,7 @@ const AddAssetDialog = React.memo(function AddAssetDialog(props: AddAssetDialogP function ConnectedAddAssetDialog(props: Omit) { return ( - {({ horizon, sendTransaction }) => ( - - )} + {({ sendTransaction }) => } ) } diff --git a/src/Assets/components/AssetDetailsActions.tsx b/src/Assets/components/AssetDetailsActions.tsx index 87ad4a90d..1656a809c 100644 --- a/src/Assets/components/AssetDetailsActions.tsx +++ b/src/Assets/components/AssetDetailsActions.tsx @@ -1,6 +1,6 @@ import React from "react" import { useTranslation } from "react-i18next" -import { Asset, Operation, Server, Transaction } from "stellar-sdk" +import { Asset, Operation, Transaction } from "stellar-sdk" import Dialog from "@material-ui/core/Dialog" import ClearIcon from "@material-ui/icons/Clear" import SwapHorizIcon from "@material-ui/icons/SwapHoriz" @@ -23,7 +23,6 @@ const dialogActionsBoxStyle: React.CSSProperties = { interface Props { account: Account asset: Asset - horizon: Server sendTransaction: SendTransaction } @@ -44,7 +43,6 @@ function AssetDetailsActions(props: Props) { const operations = [Operation.changeTrust({ asset, limit: options.limit })] return createTransaction(operations, { accountData, - horizon: props.horizon, walletAccount: props.account }) } @@ -110,9 +108,7 @@ function AssetDetailsActions(props: Props) { function ConnectedAssetDetailsActions(props: Omit) { return ( - {({ horizon, sendTransaction }) => ( - - )} + {({ sendTransaction }) => } ) } diff --git a/src/Assets/components/CustomTrustline.tsx b/src/Assets/components/CustomTrustline.tsx index 00203763e..3a8e6d389 100644 --- a/src/Assets/components/CustomTrustline.tsx +++ b/src/Assets/components/CustomTrustline.tsx @@ -1,6 +1,6 @@ import React from "react" import { useTranslation } from "react-i18next" -import { Asset, Server, Transaction } from "stellar-sdk" +import { Asset, Transaction } from "stellar-sdk" import useMediaQuery from "@material-ui/core/useMediaQuery" import TextField from "@material-ui/core/TextField" import VerifiedUserIcon from "@material-ui/icons/VerifiedUser" @@ -14,7 +14,6 @@ interface Props { account: Account accountData: AccountData createAddAssetTransaction: (asset: Asset, options: { limit?: string }) => any - horizon: Server onClose: () => void sendTransaction: (createTransactionToSend: () => Promise) => any txCreationPending: boolean diff --git a/src/Assets/components/RemoveTrustline.tsx b/src/Assets/components/RemoveTrustline.tsx index e203be892..94120d707 100644 --- a/src/Assets/components/RemoveTrustline.tsx +++ b/src/Assets/components/RemoveTrustline.tsx @@ -1,6 +1,6 @@ import React from "react" import { Trans, useTranslation } from "react-i18next" -import { Asset, Horizon, Operation, Server } from "stellar-sdk" +import { Asset, Horizon, Operation } from "stellar-sdk" import CloseIcon from "@material-ui/icons/Close" import DialogContent from "@material-ui/core/DialogContent" import DialogContentText from "@material-ui/core/DialogContentText" @@ -16,7 +16,6 @@ interface Props { account: Account accountData: AccountData asset: Asset - horizon: Server onClose: () => void onRemoved: () => void sendTransaction: SendTransaction @@ -33,7 +32,6 @@ const RemoveTrustlineDialog = React.memo(function RemoveTrustlineDialog(props: P const operations = [Operation.changeTrust({ asset: props.asset, limit: "0" })] const transaction = await createTransaction(operations, { accountData: props.accountData, - horizon: props.horizon, walletAccount: props.account }) setTxCreationPending(false) @@ -91,13 +89,8 @@ const RemoveTrustlineDialog = React.memo(function RemoveTrustlineDialog(props: P function ConnectedRemoveTrustlineDialog(props: Omit) { return ( - {({ horizon, sendTransaction }) => ( - + {({ sendTransaction }) => ( + )} ) diff --git a/src/Generic/hooks/_caches.ts b/src/Generic/hooks/_caches.ts index 5a8be705d..5be1b6de1 100644 --- a/src/Generic/hooks/_caches.ts +++ b/src/Generic/hooks/_caches.ts @@ -1,6 +1,6 @@ import { TransferServerInfo } from "@satoshipay/stellar-transfer" import { multicast, Observable, ObservableLike } from "observable-fns" -import { Asset, Horizon, ServerApi } from "stellar-sdk" +import { Asset, Horizon, Networks, ServerApi } from "stellar-sdk" import { trackError } from "~App/contexts/notifications" import { AccountData } from "../lib/account" import { FixedOrderbookRecord } from "../lib/orderbook" @@ -90,12 +90,12 @@ function createCache( return cache } -function createAccountCacheKey([horizonURLs, accountID]: readonly [string[], string]) { - return `${horizonURLs.map(url => `${url}:`)}${accountID}` +function createAccountCacheKey([network, accountID]: readonly [Networks, string]) { + return `${network.toString()}${accountID}` } -function createAssetPairCacheKey([horizonURLs, selling, buying]: readonly [string[], Asset, Asset]) { - return `${horizonURLs.map(url => `${url}:`)}${stringifyAsset(selling)}:${stringifyAsset(buying)}` +function createAssetPairCacheKey([network, selling, buying]: readonly [Networks, Asset, Asset]) { + return `${network.toString()}${stringifyAsset(selling)}:${stringifyAsset(buying)}` } export interface TransactionHistory { @@ -142,29 +142,29 @@ function areOffersNewer(prev: OfferHistory, next: OfferHistory) { return !prev || nextMaxTimestamp > prevMaxTimestamp } -export const accountDataCache = createCache( +export const accountDataCache = createCache( createAccountCacheKey ) export const accountHomeDomainCache = createCache< - readonly [string[], string], + readonly [Networks, string], [string] | [], AccountData["home_domain"] >(createAccountCacheKey) -export const accountOpenOrdersCache = createCache( +export const accountOpenOrdersCache = createCache( createAccountCacheKey, areOffersNewer ) export const accountTransactionsCache = createCache< - readonly [string[], string], + readonly [Networks, string], TransactionHistory, Horizon.TransactionResponse >(createAccountCacheKey, areTransactionsNewer) export const orderbookCache = createCache< - readonly [string[], Asset, Asset], + readonly [Networks, Asset, Asset], FixedOrderbookRecord, FixedOrderbookRecord >(createAssetPairCacheKey) diff --git a/src/Generic/hooks/stellar-subscriptions.ts b/src/Generic/hooks/stellar-subscriptions.ts index 53279e2ac..793c0cf70 100644 --- a/src/Generic/hooks/stellar-subscriptions.ts +++ b/src/Generic/hooks/stellar-subscriptions.ts @@ -8,7 +8,7 @@ import { createEmptyAccountData, AccountData } from "../lib/account" import { FixedOrderbookRecord } from "../lib/orderbook" import { stringifyAsset } from "../lib/stellar" import { mapSuspendables } from "../lib/suspense" -import { CollectionPage } from "~Workers/net-worker/stellar-network" +import { CollectionPage, getNetwork } from "~Workers/net-worker/stellar-network" import { accountDataCache, accountOpenOrdersCache, @@ -18,7 +18,6 @@ import { OfferHistory, TransactionHistory } from "./_caches" -import { useHorizonURLs } from "./stellar" import { useDebouncedState, useForceRerender } from "./util" import { useNetWorker } from "./workers" @@ -74,13 +73,13 @@ function applyAccountDataUpdate(prev: AccountData, next: AccountData): AccountDa } export function useLiveAccountDataSet(accountIDs: string[], testnet: boolean): AccountData[] { - const horizonURLs = useHorizonURLs(testnet) const netWorker = useNetWorker() + const network = getNetwork(testnet) const items = React.useMemo( () => accountIDs.map(accountID => { - const selector = [horizonURLs, accountID] as const + const selector = [network, accountID] as const const prepare = (account: Horizon.AccountResponse | null) => { return account ? { ...account, data_attr: account.data } : createEmptyAccountData(accountID) } @@ -89,7 +88,7 @@ export function useLiveAccountDataSet(accountIDs: string[], testnet: boolean): A get() { return ( accountDataCache.get(selector) || - accountDataCache.suspend(selector, () => netWorker.fetchAccountData(horizonURLs, accountID).then(prepare)) + accountDataCache.suspend(selector, () => netWorker.fetchAccountData(accountID, network).then(prepare)) ) }, set(updated: AccountData) { @@ -97,12 +96,12 @@ export function useLiveAccountDataSet(accountIDs: string[], testnet: boolean): A }, observe() { return accountDataCache.observe(selector, () => - netWorker.subscribeToAccount(horizonURLs, accountID).map(prepare) + netWorker.subscribeToAccount(accountID, network).map(prepare) ) } } }), - [accountIDs, horizonURLs, netWorker] + [accountIDs, network, netWorker] ) return useDataSubscriptions(applyAccountDataUpdate, items) @@ -118,18 +117,18 @@ function applyAccountOffersUpdate(prev: OfferHistory, next: ServerApi.OfferRecor } export function useLiveAccountOffers(accountID: string, testnet: boolean): OfferHistory { - const horizonURLs = useHorizonURLs(testnet) const netWorker = useNetWorker() + const network = getNetwork(testnet) const { get, set, observe } = React.useMemo(() => { - const selector = [horizonURLs, accountID] as const + const selector = [network, accountID] as const const limit = 10 return { get() { return ( accountOpenOrdersCache.get(selector) || accountOpenOrdersCache.suspend(selector, async () => { - const page = await netWorker.fetchAccountOpenOrders(horizonURLs, accountID, { limit, order: "desc" }) + const page = await netWorker.fetchAccountOpenOrders(accountID, network, { limit, order: "desc" }) const offers = page._embedded.records return { olderOffersAvailable: offers.length === limit, @@ -144,37 +143,37 @@ export function useLiveAccountOffers(accountID: string, testnet: boolean): Offer accountOpenOrdersCache.set(selector, { ...updated, olderOffersAvailable }) }, observe() { - return netWorker.subscribeToOpenOrders(horizonURLs, accountID) + return netWorker.subscribeToOpenOrders(accountID, network) } } - }, [accountID, horizonURLs, netWorker]) + }, [accountID, network, netWorker]) return useDataSubscription(applyAccountOffersUpdate, get, set, observe) } export function useOlderOffers(accountID: string, testnet: boolean) { const forceRerender = useForceRerender() - const horizonURLs = useHorizonURLs(testnet) const netWorker = useNetWorker() + const network = getNetwork(testnet) const fetchMoreOffers = React.useCallback( async function fetchMoreOffers() { let fetched: CollectionPage - const selector = [horizonURLs, accountID] as const + const selector = [network, accountID] as const const history = accountOpenOrdersCache.get(selector) const limit = 10 const prevOffers = history?.offers || [] if (prevOffers.length > 0) { - fetched = await netWorker.fetchAccountOpenOrders(horizonURLs, accountID, { + fetched = await netWorker.fetchAccountOpenOrders(accountID, network, { cursor: prevOffers[prevOffers.length - 1].paging_token, limit, order: "desc" }) } else { - fetched = await netWorker.fetchAccountOpenOrders(horizonURLs, accountID, { + fetched = await netWorker.fetchAccountOpenOrders(accountID, network, { limit, order: "desc" }) @@ -195,7 +194,7 @@ export function useOlderOffers(accountID: string, testnet: boolean) { // hacky… forceRerender() }, - [accountID, forceRerender, horizonURLs, netWorker] + [accountID, forceRerender, network, netWorker] ) return fetchMoreOffers @@ -205,19 +204,17 @@ type EffectHandler = (account: Account, effect: ServerApi.EffectRecord) => void export function useLiveAccountEffects(accounts: Account[], handler: EffectHandler) { const netWorker = useNetWorker() - const mainnetHorizonURLs = useHorizonURLs(false) - const testnetHorizonURLs = useHorizonURLs(true) React.useEffect(() => { const subscriptions = accounts.map(account => { - const horizonURLs = account.testnet ? testnetHorizonURLs : mainnetHorizonURLs - const observable = netWorker.subscribeToAccountEffects(horizonURLs, account.accountID) + const network = getNetwork(account.testnet) + const observable = netWorker.subscribeToAccountEffects(account.accountID, network) const subscription = observable.subscribe(effect => effect && handler(account, effect)) return subscription }) return () => subscriptions.forEach(subscription => subscription.unsubscribe()) - }, [accounts, handler, mainnetHorizonURLs, netWorker, testnetHorizonURLs]) + }, [accounts, handler, netWorker]) } function applyOrderbookUpdate(prev: FixedOrderbookRecord, next: FixedOrderbookRecord) { @@ -226,17 +223,17 @@ function applyOrderbookUpdate(prev: FixedOrderbookRecord, next: FixedOrderbookRe } export function useLiveOrderbook(selling: Asset, buying: Asset, testnet: boolean): FixedOrderbookRecord { - const horizonURLs = useHorizonURLs(testnet) const netWorker = useNetWorker() + const network = getNetwork(testnet) const { get, set, observe } = React.useMemo(() => { - const selector = [horizonURLs, selling, buying] as const + const selector = [network, selling, buying] as const return { get() { return ( orderbookCache.get(selector) || orderbookCache.suspend(selector, () => - netWorker.fetchOrderbookRecord(horizonURLs, stringifyAsset(selling), stringifyAsset(buying)) + netWorker.fetchOrderbookRecord(network, stringifyAsset(selling), stringifyAsset(buying)) ) ) }, @@ -244,11 +241,11 @@ export function useLiveOrderbook(selling: Asset, buying: Asset, testnet: boolean orderbookCache.set(selector, updated) }, observe() { - return netWorker.subscribeToOrderbook(horizonURLs, stringifyAsset(selling), stringifyAsset(buying)) + return netWorker.subscribeToOrderbook(network, stringifyAsset(selling), stringifyAsset(buying)) } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [stringifyAsset(buying), horizonURLs, netWorker, stringifyAsset(selling)]) + }, [stringifyAsset(buying), network, netWorker, stringifyAsset(selling)]) return useDataSubscription(applyOrderbookUpdate, get, set, observe) } @@ -272,19 +269,19 @@ function applyAccountTransactionsUpdate( } export function useLiveRecentTransactions(accountID: string, testnet: boolean): TransactionHistory { - const horizonURLs = useHorizonURLs(testnet) const netWorker = useNetWorker() + const network = getNetwork(testnet) const { get, set, observe } = React.useMemo(() => { const limit = 15 - const selector = [horizonURLs, accountID] as const + const selector = [network, accountID] as const return { get() { return ( accountTransactionsCache.get(selector) || accountTransactionsCache.suspend(selector, async () => { - const page = await netWorker.fetchAccountTransactions(horizonURLs, accountID, { + const page = await netWorker.fetchAccountTransactions(accountID, network, { emptyOn404: true, limit, order: "desc" @@ -303,38 +300,38 @@ export function useLiveRecentTransactions(accountID: string, testnet: boolean): accountTransactionsCache.set(selector, updated) }, observe() { - return netWorker.subscribeToAccountTransactions(horizonURLs, accountID) + return netWorker.subscribeToAccountTransactions(accountID, network) } } - }, [accountID, horizonURLs, netWorker]) + }, [accountID, network, netWorker]) return useDataSubscription(applyAccountTransactionsUpdate, get, set, observe) } export function useOlderTransactions(accountID: string, testnet: boolean) { const forceRerender = useForceRerender() - const horizonURLs = useHorizonURLs(testnet) const netWorker = useNetWorker() + const network = getNetwork(testnet) const fetchMoreTransactions = React.useCallback( async function fetchMoreTransactions() { let fetched: CollectionPage - const selector = [horizonURLs, accountID] as const + const selector = [network, accountID] as const const history = accountTransactionsCache.get(selector) const limit = 15 const prevTransactions = history?.transactions || [] if (prevTransactions.length > 0) { - fetched = await netWorker.fetchAccountTransactions(horizonURLs, accountID, { + fetched = await netWorker.fetchAccountTransactions(accountID, network, { emptyOn404: true, cursor: prevTransactions[prevTransactions.length - 1].paging_token, limit: 15, order: "desc" }) } else { - fetched = await netWorker.fetchAccountTransactions(horizonURLs, accountID, { + fetched = await netWorker.fetchAccountTransactions(accountID, network, { emptyOn404: true, limit, order: "desc" @@ -359,7 +356,7 @@ export function useOlderTransactions(accountID: string, testnet: boolean) { // hacky… forceRerender() }, - [accountID, forceRerender, horizonURLs, netWorker] + [accountID, forceRerender, network, netWorker] ) return fetchMoreTransactions diff --git a/src/Generic/hooks/stellar.ts b/src/Generic/hooks/stellar.ts index 3e710eda7..eb2643106 100644 --- a/src/Generic/hooks/stellar.ts +++ b/src/Generic/hooks/stellar.ts @@ -1,14 +1,13 @@ /* tslint:disable:no-string-literal */ import React from "react" -import { Asset, Networks, Server, Transaction, Horizon } from "stellar-sdk" +import { Asset, Networks, Transaction, Horizon } from "stellar-sdk" import { SigningKeyCacheContext, StellarAddressCacheContext, StellarAddressReverseCacheContext, WebAuthTokenCacheContext } from "~App/contexts/caches" -import { StellarContext } from "~App/contexts/stellar" import { workers } from "~Workers/worker-controller" import { StellarToml, StellarTomlCurrency } from "~shared/types/stellar-toml" import { createEmptyAccountData, AccountData } from "../lib/account" @@ -17,25 +16,7 @@ import * as StellarAddresses from "../lib/stellar-address" import { mapSuspendables } from "../lib/suspense" import { accountDataCache, accountHomeDomainCache, stellarTomlCache } from "./_caches" import { useNetWorker } from "./workers" - -/** @deprecated */ -export function useHorizon(testnet: boolean = false) { - const horizonURLs = useHorizonURLs(testnet) - const horizonURL = horizonURLs[0] - - return testnet ? new Server(horizonURL) : new Server(horizonURL) -} - -export function useHorizonURLs(testnet: boolean = false) { - const stellar = React.useContext(StellarContext) - - if (stellar.isSelectionPending) { - throw stellar.pendingSelection - } - - const horizonURLs = testnet ? stellar.testnetHorizonURLs : stellar.pubnetHorizonURLs - return horizonURLs -} +import { getNetwork } from "~Workers/net-worker/stellar-network" export function useFederationLookup() { const lookup = React.useContext(StellarAddressCacheContext) @@ -88,7 +69,7 @@ export function useWebAuth() { const manageDataOperation = transaction.operations.find(operation => operation.type === "manageData") const localPublicKey = manageDataOperation ? manageDataOperation.source : undefined - const network = testnet ? Networks.TESTNET : Networks.PUBLIC + const network = getNetwork(testnet) const txXdr = transaction .toEnvelope() .toXDR() @@ -133,17 +114,17 @@ export function useStellarToml(domain: string | undefined): StellarToml | undefi } export function useAccountData(accountID: string, testnet: boolean) { - const horizonURLs = useHorizonURLs(testnet) const netWorker = useNetWorker() + const network = getNetwork(testnet) - const selector = [horizonURLs, accountID] as const + const selector = [network, accountID] as const const cached = accountDataCache.get(selector) const prepare = (account: Horizon.AccountResponse | null): AccountData => account ? { ...account, data_attr: account.data } : createEmptyAccountData(accountID) if (!cached) { - accountDataCache.suspend(selector, () => netWorker.fetchAccountData(horizonURLs, accountID).then(prepare)) + accountDataCache.suspend(selector, () => netWorker.fetchAccountData(accountID, network).then(prepare)) } return cached || createEmptyAccountData(accountID) } @@ -156,14 +137,15 @@ export function useAccountHomeDomains( testnet: boolean, allowIncompleteResult?: boolean ): Array { - const horizonURLs = useHorizonURLs(testnet) const netWorker = useNetWorker() const [, setRerenderCounter] = React.useState(0) + const network = getNetwork(testnet) + const forceRerender = () => setRerenderCounter(counter => counter + 1) const fetchHomeDomain = async (accountID: string): Promise<[string] | []> => { - const accountData = await netWorker.fetchAccountData(horizonURLs, accountID) + const accountData = await netWorker.fetchAccountData(accountID, network) const homeDomain = accountData ? (accountData as any).home_domain : undefined if (homeDomain) { ;(testnet ? homeDomainCacheTestnet : homeDomainCachePubnet).save(accountID, homeDomain || null) @@ -176,7 +158,7 @@ export function useAccountHomeDomains( try { return mapSuspendables(accountIDs, accountID => { - const selector = [horizonURLs, accountID] as const + const selector = [network, accountID] as const return (accountHomeDomainCache.get(selector) || accountHomeDomainCache.suspend(selector, () => fetchHomeDomain(accountID)))[0] }) diff --git a/src/Generic/hooks/transfer-server.ts b/src/Generic/hooks/transfer-server.ts index 8aef37668..80c619b18 100644 --- a/src/Generic/hooks/transfer-server.ts +++ b/src/Generic/hooks/transfer-server.ts @@ -4,7 +4,8 @@ import { TransferServer, TransferServerInfo } from "@satoshipay/stellar-transfer" -import { Asset, Networks } from "stellar-sdk" +import { Asset } from "stellar-sdk" +import { getNetwork } from "~Workers/net-worker/stellar-network" import { mapSuspendables } from "../lib/suspense" import { transferInfosCache } from "./_caches" import { useAccountHomeDomains } from "./stellar" @@ -21,7 +22,7 @@ function withTimeout(promise: Promise, ms: number, message: string): Promi } async function initTransferServer(domain: string, testnet: boolean): Promise { - const network = testnet ? Networks.TESTNET : Networks.PUBLIC + const network = getNetwork(testnet) try { const transferServer = await withTimeout( diff --git a/src/Generic/lib/third-party-security.ts b/src/Generic/lib/third-party-security.ts index 759e9a0be..3a6eab000 100644 --- a/src/Generic/lib/third-party-security.ts +++ b/src/Generic/lib/third-party-security.ts @@ -1,4 +1,4 @@ -import { Server, Transaction, Horizon } from "stellar-sdk" +import { Transaction, Horizon, Networks } from "stellar-sdk" import { CustomError } from "./errors" import StellarGuardIcon from "~Icons/components/StellarGuard" import LobstrVaultIcon from "~Icons/components/LobstrVault" @@ -34,11 +34,10 @@ const services: ThirdPartySecurityService[] = [ } ] -export async function isThirdPartyProtected(horizon: Server, accountPubKey: string) { +export async function isThirdPartyProtected(accountPubKey: string, network: Networks) { const { netWorker } = await workers - const horizonURL = horizon.serverURL.toString() - const account = await netWorker.fetchAccountData(horizonURL, accountPubKey) + const account = await netWorker.fetchAccountData(accountPubKey, network) const signerKeys = (account?.signers || []).map(signer => signer.key) const enabledService = services.find(service => signerKeys.includes(service.publicKey)) diff --git a/src/Generic/lib/transaction.ts b/src/Generic/lib/transaction.ts index 12b6b64a6..7a20e89ac 100644 --- a/src/Generic/lib/transaction.ts +++ b/src/Generic/lib/transaction.ts @@ -4,7 +4,6 @@ import { Keypair, Memo, Operation, - Server, ServerApi, TransactionBuilder, Transaction, @@ -12,7 +11,8 @@ import { Networks } from "stellar-sdk" import { Account } from "~App/contexts/accounts" -import { workers } from "~Workers/worker-controller" +import { getNetwork } from "~Workers/net-worker/stellar-network" +import { workers, NetWorker } from "~Workers/worker-controller" import { WrongPasswordError, CustomError } from "./errors" import { applyTimeout } from "./promise" import { getAllSources, isNotFoundError } from "./stellar" @@ -62,15 +62,10 @@ export function hasSigned( ) } -async function accountExists(horizon: Server, publicKey: string) { +async function accountExists(publicKey: string, networker: NetWorker, network: Networks) { try { - const account = await horizon - .accounts() - .accountId(publicKey) - .call() - - // Hack to fix SatoshiPay horizons responding with status 200 and an empty object on non-existent accounts - return Object.keys(account).length > 0 + const account = await networker.fetchAccountData(publicKey, network) + return Boolean(account) } catch (error) { if (isNotFoundError(error)) { return false @@ -87,24 +82,23 @@ function selectTransactionTimeout(accountData: Pick - horizon: Server memo?: Memo | null minTransactionFee?: number walletAccount: Account } export async function createTransaction(operations: Array>, options: TxBlueprint) { - const { horizon, walletAccount } = options + const { walletAccount } = options const { netWorker } = await workers + const network = getNetwork(walletAccount.testnet) - const horizonURL = horizon.serverURL.toString() const timeout = selectTransactionTimeout(options.accountData) const [accountMetadata, timebounds] = await Promise.all([ - applyTimeout(netWorker.fetchAccountData(horizonURL, walletAccount.accountID, 10), 10000, () => + applyTimeout(netWorker.fetchAccountData(walletAccount.accountID, network, 10), 10000, () => fail(`Fetching source account data timed out`) ), - applyTimeout(netWorker.fetchTimebounds(horizonURL, timeout), 10000, () => + applyTimeout(netWorker.fetchTimebounds(timeout, network), 10000, () => fail(`Syncing time bounds with horizon timed out`) ) ] as const) @@ -114,7 +108,7 @@ export async function createTransaction(operations: Array>, o } const account = new StellarAccount(accountMetadata.id, accountMetadata.sequence) - const networkPassphrase = walletAccount.testnet ? Networks.TESTNET : Networks.PUBLIC + const networkPassphrase = getNetwork(walletAccount.testnet) const txFee = Math.max(options.minTransactionFee || 0, maximumFeeToSpend) const builder = new TransactionBuilder(account, { @@ -136,12 +130,13 @@ interface PaymentOperationBlueprint { amount: string asset: Asset destination: string - horizon: Server + testnet: boolean } -export async function createPaymentOperation(options: PaymentOperationBlueprint) { - const { amount, asset, destination, horizon } = options - const destinationAccountExists = await accountExists(horizon, destination) +export async function createPaymentOperation(options: PaymentOperationBlueprint, networker: NetWorker) { + const { amount, asset, destination, testnet } = options + const network = getNetwork(testnet) + const destinationAccountExists = await accountExists(destination, networker, network) if (!destinationAccountExists && !Asset.native().equals(options.asset)) { throw CustomError( @@ -167,9 +162,8 @@ export async function signTransaction(transaction: Transaction, walletAccount: A return signedTransaction } -export async function requiresRemoteSignatures(horizon: Server, transaction: Transaction, walletPublicKey: string) { +export async function requiresRemoteSignatures(network: Networks, transaction: Transaction, walletPublicKey: string) { const { netWorker } = await workers - const horizonURL = horizon.serverURL.toString() const sources = getAllSources(transaction) if (sources.length > 1) { @@ -178,7 +172,7 @@ export async function requiresRemoteSignatures(horizon: Server, transaction: Tra const accounts = await Promise.all( sources.map(async sourcePublicKey => { - const account = await netWorker.fetchAccountData(horizonURL, sourcePublicKey) + const account = await netWorker.fetchAccountData(sourcePublicKey, network) if (!account) { throw Error(`Could not fetch account metadata from horizon server: ${sourcePublicKey}`) } diff --git a/src/ManageSigners/components/ManageSignersDialog.tsx b/src/ManageSigners/components/ManageSignersDialog.tsx index 1e22ad601..7cc93c92c 100644 --- a/src/ManageSigners/components/ManageSignersDialog.tsx +++ b/src/ManageSigners/components/ManageSignersDialog.tsx @@ -168,8 +168,8 @@ interface ManageSignersDialogProps { function ManageSignersDialog(props: ManageSignersDialogProps) { return ( - {({ horizon, sendTransaction }) => ( - + {({ sendTransaction }) => ( + )} diff --git a/src/ManageSigners/components/MultisigEditorContext.tsx b/src/ManageSigners/components/MultisigEditorContext.tsx index c054bd405..a198db93a 100644 --- a/src/ManageSigners/components/MultisigEditorContext.tsx +++ b/src/ManageSigners/components/MultisigEditorContext.tsx @@ -1,5 +1,5 @@ import React from "react" -import { Server, Transaction } from "stellar-sdk" +import { Transaction } from "stellar-sdk" import { Account } from "~App/contexts/accounts" import { useSignersEditor, SignersUpdate } from "../hooks/useSignersEditor" import { MultisigPresets, SignersEditorState } from "../lib/editor" @@ -46,7 +46,6 @@ export const MultisigEditorContext = React.createContext void } diff --git a/src/ManageSigners/hooks/useSignersEditor.ts b/src/ManageSigners/hooks/useSignersEditor.ts index 407a9dc94..b1fb092ba 100644 --- a/src/ManageSigners/hooks/useSignersEditor.ts +++ b/src/ManageSigners/hooks/useSignersEditor.ts @@ -1,5 +1,5 @@ import React from "react" -import { Horizon, Operation, Server, Transaction, xdr } from "stellar-sdk" +import { Horizon, Operation, Transaction, xdr } from "stellar-sdk" import { trackError } from "~App/contexts/notifications" import { Account } from "~App/contexts/accounts" import { SettingsContext, SettingsContextType } from "~App/contexts/settings" @@ -10,7 +10,6 @@ import { initializeEditorState, SignersEditorState } from "../lib/editor" export interface SignersEditorOptions { account: Account - horizon: Server sendTransaction: (tx: Transaction) => void } @@ -99,7 +98,6 @@ export function useSignersEditor(options: SignersEditorOptions) { const tx = await createTransaction(operations, { accountData, - horizon: options.horizon, walletAccount: options.account }) diff --git a/src/Payment/components/PaymentDialog.tsx b/src/Payment/components/PaymentDialog.tsx index c16ccf89f..6b571f378 100644 --- a/src/Payment/components/PaymentDialog.tsx +++ b/src/Payment/components/PaymentDialog.tsx @@ -1,6 +1,6 @@ import React from "react" import { useTranslation } from "react-i18next" -import { Asset, Server, Transaction } from "stellar-sdk" +import { Asset, Transaction } from "stellar-sdk" import { Account } from "~App/contexts/accounts" import { trackError } from "~App/contexts/notifications" import { useLiveAccountData, useLiveAccountOffers } from "~Generic/hooks/stellar-subscriptions" @@ -18,7 +18,6 @@ import PaymentForm from "./PaymentForm" interface Props { account: Account accountData: AccountData - horizon: Server onClose: () => void openOrdersCount: number sendTransaction: (transaction: Transaction) => Promise @@ -31,10 +30,10 @@ function PaymentDialog(props: Props) { const [txCreationPending, setTxCreationPending] = React.useState(false) const handleSubmit = React.useCallback( - async (createTx: (horizon: Server, account: Account) => Promise) => { + async (createTx: (account: Account) => Promise) => { try { setTxCreationPending(true) - const tx = await createTx(props.horizon, props.account) + const tx = await createTx(props.account) setTxCreationPending(false) await sendTransaction(tx) } catch (error) { @@ -43,7 +42,7 @@ function PaymentDialog(props: Props) { setTxCreationPending(false) } }, - [props.account, props.horizon, sendTransaction] + [props.account, sendTransaction] ) const trustedAssets = React.useMemo(() => getAssetsFromBalances(props.accountData.balances) || [Asset.native()], [ @@ -89,11 +88,10 @@ function ConnectedPaymentDialog(props: Pick) { return ( - {({ horizon, sendTransaction }) => ( + {({ sendTransaction }) => ( diff --git a/src/Payment/components/PaymentForm.tsx b/src/Payment/components/PaymentForm.tsx index 0383dfa6e..c9957332a 100644 --- a/src/Payment/components/PaymentForm.tsx +++ b/src/Payment/components/PaymentForm.tsx @@ -3,7 +3,7 @@ import nanoid from "nanoid" import React from "react" import { Controller, useForm } from "react-hook-form" import { useTranslation } from "react-i18next" -import { Asset, Memo, MemoType, Server, Transaction } from "stellar-sdk" +import { Asset, Memo, MemoType, Transaction } from "stellar-sdk" import InputAdornment from "@material-ui/core/InputAdornment" import TextField from "@material-ui/core/TextField" import SendIcon from "@material-ui/icons/Send" @@ -11,6 +11,7 @@ import { Account } from "~App/contexts/accounts" import { AccountRecord, useWellKnownAccounts } from "~Generic/hooks/stellar-ecosystem" import { useFederationLookup } from "~Generic/hooks/stellar" import { useIsMobile, RefStateObject } from "~Generic/hooks/userinterface" +import { useNetWorker } from "~Generic/hooks/workers" import { AccountData } from "~Generic/lib/account" import { CustomError } from "~Generic/lib/errors" import { findMatchingBalanceLine, getAccountMinimumBalance, getSpendableBalance } from "~Generic/lib/stellar" @@ -333,13 +334,14 @@ interface Props { trustedAssets: Asset[] txCreationPending?: boolean onCancel: () => void - onSubmit: (createTx: (horizon: Server, account: Account) => Promise) => any + onSubmit: (createTx: (account: Account) => Promise) => any } function PaymentFormContainer(props: Props) { const { lookupFederationRecord } = useFederationLookup() + const networker = useNetWorker() - const createPaymentTx = async (horizon: Server, account: Account, formValues: ExtendedPaymentFormValues) => { + const createPaymentTx = async (account: Account, formValues: ExtendedPaymentFormValues) => { const asset = props.trustedAssets.find(trustedAsset => trustedAsset.equals(formValues.asset)) const federationRecord = formValues.destination.indexOf("*") > -1 ? await lookupFederationRecord(formValues.destination) : null @@ -361,24 +363,26 @@ function PaymentFormContainer(props: Props) { const isMultisigTx = props.accountData.signers.length > 1 - const payment = await createPaymentOperation({ - asset: asset || Asset.native(), - amount: replaceCommaWithDot(formValues.amount), - destination, - horizon - }) + const payment = await createPaymentOperation( + { + asset: asset || Asset.native(), + amount: replaceCommaWithDot(formValues.amount), + destination, + testnet: props.testnet + }, + networker + ) const tx = await createTransaction([payment], { accountData: props.accountData, memo: federationMemo.type !== "none" ? federationMemo : userMemo, minTransactionFee: isMultisigTx ? multisigMinimumFee : 0, - horizon, walletAccount: account }) return tx } const submitForm = (formValues: ExtendedPaymentFormValues) => { - props.onSubmit((horizon, account) => createPaymentTx(horizon, account, formValues)) + props.onSubmit(account => createPaymentTx(account, formValues)) } return diff --git a/src/Platform/ipc/web.ts b/src/Platform/ipc/web.ts index fef6cc8b8..6b2ce73fb 100644 --- a/src/Platform/ipc/web.ts +++ b/src/Platform/ipc/web.ts @@ -1,5 +1,6 @@ import { createStore, KeysData } from "key-store" -import { Networks, Keypair, Transaction } from "stellar-sdk" +import { Keypair, Transaction } from "stellar-sdk" +import { getNetwork } from "~Workers/net-worker/stellar-network" import { Messages } from "../../shared/ipc" import { WrongPasswordError } from "../../Generic/lib/errors" @@ -130,7 +131,7 @@ function initKeyStore() { function signTransaction(internalAccountID: string, transactionXDR: string, password: string) { try { const account = keyStore.getPublicKeyData(internalAccountID) - const networkPassphrase = account.testnet ? Networks.TESTNET : Networks.PUBLIC + const networkPassphrase = getNetwork(account.testnet) const transaction = new Transaction(transactionXDR, networkPassphrase) const privateKey = keyStore.getPrivateKeyData(internalAccountID, password).privateKey diff --git a/src/Trading/components/TradingDialog.tsx b/src/Trading/components/TradingDialog.tsx index 6d40a1063..fd2d30913 100644 --- a/src/Trading/components/TradingDialog.tsx +++ b/src/Trading/components/TradingDialog.tsx @@ -1,6 +1,6 @@ import React from "react" import { useTranslation } from "react-i18next" -import { Asset, Horizon, Server, Transaction } from "stellar-sdk" +import { Asset, Horizon, Transaction } from "stellar-sdk" import Box from "@material-ui/core/Box" import Typography from "@material-ui/core/Typography" import { Account } from "~App/contexts/accounts" @@ -25,7 +25,6 @@ import TradingForm from "./TradingForm" interface TradingDialogProps { account: Account - horizon: Server onClose: () => void sendTransaction: (transaction: Transaction) => void } diff --git a/src/Trading/components/TradingForm.tsx b/src/Trading/components/TradingForm.tsx index 3e712bd4a..d214a2048 100644 --- a/src/Trading/components/TradingForm.tsx +++ b/src/Trading/components/TradingForm.tsx @@ -20,7 +20,6 @@ import AssetSelector from "~Generic/components/AssetSelector" import { ActionButton, DialogActionsBox } from "~Generic/components/DialogActions" import { ReadOnlyTextfield } from "~Generic/components/FormFields" import Portal from "~Generic/components/Portal" -import { useHorizon } from "~Generic/hooks/stellar" import { useLiveOrderbook } from "~Generic/hooks/stellar-subscriptions" import { RefStateObject, useIsMobile } from "~Generic/hooks/userinterface" import { AccountData } from "~Generic/lib/account" @@ -105,7 +104,6 @@ function TradingForm(props: Props) { } }, [form, primaryAsset, props.initialPrimaryAsset]) - const horizon = useHorizon(props.account.testnet) const tradePair = useLiveOrderbook(primaryAsset || Asset.native(), secondaryAsset, props.account.testnet) const assets = React.useMemo(() => props.trustlines.map(balancelineToAsset), [props.trustlines]) @@ -197,7 +195,6 @@ function TradingForm(props: Props) { ], { accountData: props.accountData, - horizon, walletAccount: props.account } ) @@ -210,7 +207,6 @@ function TradingForm(props: Props) { }, [ form, effectivePrice, - horizon, primaryAsset, props.account, props.accountData, diff --git a/src/Transaction/components/TransactionSender.tsx b/src/Transaction/components/TransactionSender.tsx index 8c9d2ad23..8a1edb4d7 100644 --- a/src/Transaction/components/TransactionSender.tsx +++ b/src/Transaction/components/TransactionSender.tsx @@ -1,11 +1,10 @@ import { TFunction } from "i18next" import React from "react" import { Translation } from "react-i18next" -import { Networks, Server, Transaction } from "stellar-sdk" +import { Transaction } from "stellar-sdk" import Zoom from "@material-ui/core/Zoom" import { Account } from "~App/contexts/accounts" import { SettingsContext, SettingsContextType } from "~App/contexts/settings" -import { useHorizon } from "~Generic/hooks/stellar" import { useIsMobile } from "~Generic/hooks/userinterface" import { isWrongPasswordError, getErrorTranslation } from "~Generic/lib/errors" import { explainSubmissionErrorResponse } from "~Generic/lib/horizonErrors" @@ -18,6 +17,7 @@ import { ThirdPartySecurityService } from "~Generic/lib/third-party-security" import { workers } from "~Workers/worker-controller" +import { getNetwork } from "~Workers/net-worker/stellar-network" import TransactionReviewDialog from "~TransactionReview/components/TransactionReviewDialog" import SubmissionProgress, { SubmissionType } from "./SubmissionProgress" @@ -77,7 +77,6 @@ export type SendTransaction = ( ) => Promise interface RenderFunctionProps { - horizon: Server sendTransaction: SendTransaction } @@ -85,7 +84,6 @@ interface Props { account: Account completionCallbackDelay?: number forceClose?: boolean - horizon: Server settings: SettingsContextType t: TFunction children: (props: RenderFunctionProps) => React.ReactNode @@ -215,12 +213,12 @@ class TransactionSender extends React.Component { const { account, completionCallbackDelay = 1000, - horizon, onSubmissionCompleted = () => undefined, onSubmissionFailure } = this.props try { - const thirdPartySecurityService = await isThirdPartyProtected(horizon, account.accountID) + const network = getNetwork(this.props.account.testnet) + const thirdPartySecurityService = await isThirdPartyProtected(account.accountID, network) if (thirdPartySecurityService) { await this.submitTransactionToThirdPartyService(signedTx, thirdPartySecurityService) } else if ( @@ -233,7 +231,7 @@ class TransactionSender extends React.Component { // with master weight set to 0 --> request should be submitted to multisig service // although it does not require remote signatures this.state.signatureRequest?.status === "pending" || - (await requiresRemoteSignatures(horizon, signedTx, account.publicKey)) + (await requiresRemoteSignatures(network, signedTx, account.publicKey)) ) { await this.submitTransactionToMultisigService(signedTx, unsignedTx) } else { @@ -272,20 +270,18 @@ class TransactionSender extends React.Component { submitTransactionToHorizon = async (signedTransaction: Transaction) => { const { netWorker } = await workers - const network = this.props.account.testnet ? Networks.TESTNET : Networks.PUBLIC + const network = getNetwork(this.props.account.testnet) const txEnvelopeXdr = signedTransaction .toEnvelope() .toXDR() .toString("base64") - const promise = netWorker - .submitTransaction(String(this.props.horizon.serverURL), txEnvelopeXdr, network) - .then(response => { - if (response.status !== 200) { - throw explainSubmissionErrorResponse(response, this.props.t) - } - return response - }) + const promise = netWorker.submitTransaction(txEnvelopeXdr, network).then(response => { + if (response.status !== 200) { + throw explainSubmissionErrorResponse(response, this.props.t) + } + return response + }) this.setSubmissionPromise(promise) this.setState({ submissionType: SubmissionType.default }) @@ -364,7 +360,6 @@ class TransactionSender extends React.Component { } = this.state const content = this.props.children({ - horizon: this.props.horizon, sendTransaction: this.setTransaction }) @@ -402,10 +397,9 @@ class TransactionSender extends React.Component { } } -function TransactionSenderWithHorizon(props: Omit) { - const horizon = useHorizon(props.account.testnet) +function TransactionSenderWithHorizon(props: Omit) { const settings = React.useContext(SettingsContext) - return {t => } + return {t => } } export default React.memo(TransactionSenderWithHorizon) diff --git a/src/TransactionReview/stories/Dialogs.tsx b/src/TransactionReview/stories/Dialogs.tsx index 1df96f535..ad2247e26 100644 --- a/src/TransactionReview/stories/Dialogs.tsx +++ b/src/TransactionReview/stories/Dialogs.tsx @@ -1,11 +1,12 @@ import React from "react" import Button from "@material-ui/core/Button" import { storiesOf } from "@storybook/react" -import { Asset, Server, Transaction } from "stellar-sdk" -import TransactionReviewDialog from "../components/TransactionReviewDialog" +import { Asset, Transaction } from "stellar-sdk" import { Account, AccountsContext, AccountsProvider } from "~App/contexts/accounts" import { useLiveAccountData } from "~Generic/hooks/stellar-subscriptions" import { createPaymentOperation, createTransaction } from "~Generic/lib/transaction" +import { useNetWorker } from "~Generic/hooks/workers" +import TransactionReviewDialog from "../components/TransactionReviewDialog" interface DialogContainerProps { account: Account @@ -16,27 +17,30 @@ function DialogContainer(props: DialogContainerProps) { const [isOpen, setIsOpen] = React.useState(false) const [transaction, setTransaction] = React.useState(null) const accountData = useLiveAccountData(props.account.accountID, props.account.testnet) + const networker = useNetWorker() React.useEffect(() => { const createDemoTx = async () => { return createTransaction( [ - await createPaymentOperation({ - amount: "1", - asset: Asset.native(), - destination: "GA2CZKBI2C55WHALSTNPG54FOQCLC6Y4EIATZEIJOXWQPSEGN4CWAXFT", - horizon: new Server("https://horizon-testnet.stellar.org") - }) + await createPaymentOperation( + { + amount: "1", + asset: Asset.native(), + destination: "GA2CZKBI2C55WHALSTNPG54FOQCLC6Y4EIATZEIJOXWQPSEGN4CWAXFT", + testnet: props.account.testnet + }, + networker + ) ], { accountData, - horizon: new Server("https://horizon-testnet.stellar.org"), walletAccount: props.account } ) } createDemoTx().then(tx => setTransaction(tx)) - }, [accountData, props.account]) + }, [accountData, networker, props.account]) return ( <> diff --git a/src/TransferService/components/ConnectedTransferDialog.tsx b/src/TransferService/components/ConnectedTransferDialog.tsx index b7659079e..459423d36 100644 --- a/src/TransferService/components/ConnectedTransferDialog.tsx +++ b/src/TransferService/components/ConnectedTransferDialog.tsx @@ -21,7 +21,7 @@ function ConnectedTransferDialog(props: Pick - {({ horizon, sendTransaction }) => ( + {({ sendTransaction }) => ( } > - + )} diff --git a/src/TransferService/components/TransferDialog.tsx b/src/TransferService/components/TransferDialog.tsx index 0cb43a0fc..d07016b9f 100644 --- a/src/TransferService/components/TransferDialog.tsx +++ b/src/TransferService/components/TransferDialog.tsx @@ -1,5 +1,5 @@ import React from "react" -import { Asset, Server, Transaction } from "stellar-sdk" +import { Asset, Transaction } from "stellar-sdk" import { Account } from "~App/contexts/accounts" import { useTransferInfos } from "~Generic/hooks/transfer-server" import { useIsMobile, useDialogActions } from "~Generic/hooks/userinterface" @@ -39,7 +39,6 @@ const TransferContent = withFallback(PureTransferContent, ) export interface TransferDialogProps { account: Account accountData: AccountData - horizon: Server onClose: () => void sendTransaction: (transaction: Transaction) => Promise type: "deposit" | "withdrawal" diff --git a/src/TransferService/hooks/useDepositState.ts b/src/TransferService/hooks/useDepositState.ts index c2e535a26..030950a85 100644 --- a/src/TransferService/hooks/useDepositState.ts +++ b/src/TransferService/hooks/useDepositState.ts @@ -1,5 +1,5 @@ import BigNumber from "big.js" -import { Networks, Transaction } from "stellar-sdk" +import { Transaction } from "stellar-sdk" import { WebauthData } from "@satoshipay/stellar-sep-10" import { fetchTransferInfos, @@ -13,6 +13,7 @@ import { import { Account } from "~App/contexts/accounts" import { CustomError } from "~Generic/lib/errors" import { useWebAuth } from "~Generic/hooks/stellar" +import { getNetwork } from "~Workers/net-worker/stellar-network" import { Action, TransferStates } from "../util/statemachine" import { useTransferState } from "./useTransferState" import { parseAmount } from "../util/util" @@ -107,7 +108,7 @@ export function useDepositState(account: Account, closeDialog: () => void) { } else if (cachedAuthToken) { await requestDeposit(deposit, cachedAuthToken) } else { - const network = account.testnet ? Networks.TESTNET : Networks.PUBLIC + const network = getNetwork(account.testnet) const authChallenge = await WebAuth.fetchChallenge( webauth.endpointURL, webauth.signingKey, diff --git a/src/TransferService/hooks/useWithdrawalState.ts b/src/TransferService/hooks/useWithdrawalState.ts index f7baf0b7f..b12333643 100644 --- a/src/TransferService/hooks/useWithdrawalState.ts +++ b/src/TransferService/hooks/useWithdrawalState.ts @@ -1,5 +1,5 @@ import BigNumber from "big.js" -import { Horizon, Networks, Operation, Server, Transaction, xdr } from "stellar-sdk" +import { Horizon, Operation, Transaction, xdr } from "stellar-sdk" import { WebauthData } from "@satoshipay/stellar-sep-10" import { fetchTransferInfos, @@ -11,13 +11,14 @@ import { WithdrawalTransaction } from "@satoshipay/stellar-transfer" import { Account } from "~App/contexts/accounts" -import { useHorizonURLs, useWebAuth } from "~Generic/hooks/stellar" +import { useWebAuth } from "~Generic/hooks/stellar" import { CustomError } from "~Generic/lib/errors" import { useNetWorker } from "~Generic/hooks/workers" import { createTransaction } from "~Generic/lib/transaction" import { Action, TransferStates } from "../util/statemachine" import { useTransferState } from "./useTransferState" import { createMemo, parseAmount } from "../util/util" +import { getNetwork } from "~Workers/net-worker/stellar-network" function createWithdrawal(state: Omit): Withdrawal { const fields = { @@ -31,7 +32,6 @@ async function createWithdrawalTransaction( account: Account, accountData: Horizon.AccountResponse, amount: BigNumber, - horizon: Server, instructions: WithdrawalInstructionsSuccess, withdrawal: Withdrawal ): Promise { @@ -55,7 +55,6 @@ async function createWithdrawalTransaction( return createTransaction(operations, { accountData, - horizon, memo, walletAccount: account }) @@ -63,7 +62,7 @@ async function createWithdrawalTransaction( export function useWithdrawalState(account: Account, closeDialog: () => void) { const netWorker = useNetWorker() - const horizonURLs = useHorizonURLs(account.testnet) + const network = getNetwork(account.testnet) const WebAuth = useWebAuth() const { dispatch, machineState, transfer } = useTransferState(account, closeDialog) @@ -140,7 +139,6 @@ export function useWithdrawalState(account: Account, closeDialog: () => void) { } else if (cachedAuthToken) { await requestWithdrawal(withdrawal, cachedAuthToken) } else { - const network = account.testnet ? Networks.TESTNET : Networks.PUBLIC const authChallenge = await WebAuth.fetchChallenge( webauth.endpointURL, webauth.signingKey, @@ -156,18 +154,15 @@ export function useWithdrawalState(account: Account, closeDialog: () => void) { instructions: WithdrawalInstructionsSuccess, amount: BigNumber ) => { - const accountData = await netWorker.fetchAccountData(horizonURLs, account.accountID) - const horizonURL = horizonURLs[0] + const accountData = await netWorker.fetchAccountData(account.accountID, network) if (!accountData) { - throw CustomError( - "FetchAccountDataError", - `Cannot fetch account data of ${account.accountID} from ${horizonURL}`, - { account: account.accountID, horizon: horizonURL } - ) + throw CustomError("FetchAccountDataError", `Cannot fetch account data of ${account.accountID}`, { + account: account.accountID + }) } - return createWithdrawalTransaction(account, accountData, amount, new Server(horizonURL), instructions, withdrawal) + return createWithdrawalTransaction(account, accountData, amount, instructions, withdrawal) } const pollKYCStatus = async (withdrawal: Withdrawal, transferTxId: string, authToken?: string) => { diff --git a/src/Workers/net-worker/multisig.ts b/src/Workers/net-worker/multisig.ts index ed370d6bc..4c969682b 100644 --- a/src/Workers/net-worker/multisig.ts +++ b/src/Workers/net-worker/multisig.ts @@ -10,6 +10,7 @@ import { import { manageStreamConnection, whenBackOnline } from "~Generic/lib/stream" import { joinURL } from "~Generic/lib/url" import { raiseConnectionError, ServiceID } from "./errors" +import { getNetwork } from "./stellar-network" interface ServerSentEvent { data: string | string[] @@ -174,7 +175,7 @@ export async function shareTransaction( transactionXdr: string, signatureXdr: string ) { - const transaction = new Transaction(transactionXdr, testnet ? Networks.TESTNET : Networks.PUBLIC) + const transaction = new Transaction(transactionXdr, getNetwork(testnet)) const url = joinURL(serviceURL, "/transactions") const req = createSignatureRequestURI(transaction, { diff --git a/src/Workers/net-worker/stellar-network.ts b/src/Workers/net-worker/stellar-network.ts index 3e777597c..3ccb7703d 100644 --- a/src/Workers/net-worker/stellar-network.ts +++ b/src/Workers/net-worker/stellar-network.ts @@ -85,11 +85,10 @@ const identification = { "X-Client-Version": pkg.version } -const createAccountCacheKey = (horizonURLs: string[], accountID: string) => - `${horizonURLs.map(url => `${url}:`)}${accountID}` +const createAccountCacheKey = (accountID: string, network: Networks) => `${network.toString()}:${accountID}` // const createAccountCacheKey = (horizonURL: string, accountID: string) => `${horizonURL}:${accountID}` -const createOrderbookCacheKey = (horizonURLs: string[], sellingAsset: string, buyingAsset: string) => - `${horizonURLs.map(url => `${url}:`)}${sellingAsset}:${buyingAsset}` +const createOrderbookCacheKey = (network: Networks, sellingAsset: string, buyingAsset: string) => + `${network.toString()}:${sellingAsset}:${buyingAsset}` const debugHorizonSelection = DebugLogger("net-worker:select-horizon") const debugSubscriptionReset = DebugLogger("net-worker:reset-subscriptions") @@ -99,12 +98,18 @@ function delay(ms: number) { } let roundRobinIndex = 0 -function getRandomURL(horizonURLs: string[]) { +async function getRandomURL(network: Networks) { + const [mainnet, testnet] = await initialHorizonSelection + const horizonURLs = network === Networks.PUBLIC ? mainnet : testnet const url = horizonURLs[roundRobinIndex % horizonURLs.length] roundRobinIndex += 1 return url } +export function getNetwork(testnet: boolean) { + return testnet ? Networks.TESTNET : Networks.PUBLIC +} + function getFetchQueue(horizonURL: string): PromiseQueue { if (!fetchQueuesByHorizon.has(horizonURL)) { const fetchQueue = new PromiseQueue({ @@ -120,8 +125,8 @@ function getFetchQueue(horizonURL: string): PromiseQueue { return fetchQueuesByHorizon.get(horizonURL)! } -function getServiceID(horizonURL: string): ServiceID { - return /testnet/.test(horizonURL) ? ServiceID.HorizonTestnet : ServiceID.HorizonPublic +function getServiceID(network: Networks): ServiceID { + return network === Networks.TESTNET ? ServiceID.HorizonTestnet : ServiceID.HorizonPublic } function cachify( @@ -143,6 +148,47 @@ function cachify( } } +let testnetURLs: string[] = [] +let mainnetURLs: string[] = [] +let selectionPending = true + +const initialHorizonSelection: Promise<[string[], string[]]> = (async () => { + const pubnetHorizonURLs: string[] = Array.from( + new Set( + await Promise.all([ + "https://horizon.stellar.org", + checkHorizonOrFailover("https://horizon.stellarx.com", "https://horizon.stellar.org"), + checkHorizonOrFailover("https://horizon.stellar.lobstr.co", "https://horizon.stellar.org") + ]) + ) + ) + + const testnetHorizonURLs: string[] = [ + await checkHorizonOrFailover( + "https://stellar-horizon-testnet.satoshipay.io/", + "https://horizon-testnet.stellar.org" + ) + ] + + return Promise.all([pubnetHorizonURLs, testnetHorizonURLs]) +})() + +initialHorizonSelection + .then(result => { + mainnetURLs = result[0] + testnetURLs = result[1] + selectionPending = false + }) + // tslint:disable-next-line no-console + .catch(console.error) + +export function getHorizonURLs(testnet: boolean = false) { + if (selectionPending) { + throw initialHorizonSelection + } + return testnet ? testnetURLs : mainnetURLs +} + export async function checkHorizonOrFailover(primaryHorizonURL: string, secondaryHorizonURL: string) { const debug = debugHorizonSelection // Account ID of friendbot (account exists on pubnet, too) @@ -186,7 +232,8 @@ export function resetAllSubscriptions() { resetSubscriptions() } -export async function submitTransaction(horizonURL: string, txEnvelopeXdr: string, network: Networks) { +export async function submitTransaction(txEnvelopeXdr: string, network: Networks) { + const horizonURL = await getRandomURL(network) const fetchQueue = getFetchQueue(horizonURL) const url = new URL(`/transactions?${qs.stringify({ tx: txEnvelopeXdr })}`, horizonURL) @@ -209,7 +256,8 @@ export async function submitTransaction(horizonURL: string, txEnvelopeXdr: strin } } -async function waitForAccountDataUncached(horizonURL: string, accountID: string, shouldCancel?: () => boolean) { +async function waitForAccountDataUncached(accountID: string, network: Networks, shouldCancel?: () => boolean) { + const horizonURL = await getRandomURL(network) const fetchQueue = getFetchQueue(horizonURL) const debug = DebugLogger(`net-worker:wait-for-account:${accountID}`) @@ -247,16 +295,15 @@ async function waitForAccountDataUncached(horizonURL: string, accountID: string, } } -async function waitForAccountData(horizonURLs: string[], accountID: string, shouldCancel?: () => boolean) { +async function waitForAccountData(accountID: string, network: Networks, shouldCancel?: () => boolean) { // Cache promise to make sure we don't poll the same account twice simultaneously - const cacheKey = createAccountCacheKey(horizonURLs, accountID) + const cacheKey = createAccountCacheKey(accountID, network) const pending = accountDataWaitingCache.get(cacheKey) - const horizonURL = getRandomURL(horizonURLs) if (pending) { return pending } else { - const justStarted = waitForAccountDataUncached(horizonURL, accountID, shouldCancel) + const justStarted = waitForAccountDataUncached(accountID, network, shouldCancel) accountDataWaitingCache.set(cacheKey, justStarted) justStarted.then( () => accountDataWaitingCache.delete(cacheKey), @@ -266,11 +313,11 @@ async function waitForAccountData(horizonURLs: string[], accountID: string, shou } } -function subscribeToAccountEffectsUncached(horizonURLs: string[], accountID: string) { - const horizonURL = getRandomURL(horizonURLs) +function subscribeToAccountEffectsUncached(accountID: string, network: Networks) { + let horizonURL = "" const fetchQueue = getFetchQueue(horizonURL) const debug = DebugLogger(`net-worker:subscriptions:account-effects:${accountID}`) - const serviceID = getServiceID(horizonURL) + const serviceID = getServiceID(network) let latestCursor: string | undefined let latestEffectCreatedAt: string | undefined @@ -287,18 +334,19 @@ function subscribeToAccountEffectsUncached(horizonURLs: string[], accountID: str if (streamedUpdate) { return streamedUpdate } else { - const effect = await fetchLatestAccountEffect(horizonURL, accountID) + const effect = await fetchLatestAccountEffect(accountID, network) return effect || undefined } }, async init() { debug(`Subscribing to account effects…`) - let effect = await fetchLatestAccountEffect(horizonURL, accountID) + horizonURL = await getRandomURL(network) + let effect = await fetchLatestAccountEffect(accountID, network) if (!effect) { debug(`Waiting for account to be created on the network…`) - await waitForAccountData(horizonURLs, accountID) - effect = await fetchLatestAccountEffect(horizonURL, accountID) + await waitForAccountData(accountID, network) + effect = await fetchLatestAccountEffect(accountID, network) } latestCursor = effect ? effect.paging_token : latestCursor @@ -363,14 +411,14 @@ export const subscribeToAccountEffects = cachify( createAccountCacheKey ) -function subscribeToAccountUncached(horizonURLs: string[], accountID: string) { +function subscribeToAccountUncached(accountID: string, network: Networks) { const debug = DebugLogger(`net-worker:subscriptions:account:${accountID}`) - const horizonURL = getRandomURL(horizonURLs) - const serviceID = getServiceID(horizonURL) + let horizonURL = "" + const serviceID = getServiceID(network) let latestSnapshot: string | undefined - const cacheKey = createAccountCacheKey(horizonURLs, accountID) + const cacheKey = createAccountCacheKey(accountID, network) const createSnapshot = (accountData: Horizon.AccountResponse) => JSON.stringify([accountData.sequence, accountData.balances]) @@ -386,18 +434,20 @@ function subscribeToAccountUncached(horizonURLs: string[], accountID: string) { }, async fetchUpdate() { debug(`Fetching update…`) - const accountData = await fetchAccountData(horizonURLs, accountID) + const accountData = await fetchAccountData(accountID, network) return accountData || undefined }, async init() { debug(`Subscribing to account meta data updates…`) const lastKnownAccountData = accountDataCache.get(cacheKey) + horizonURL = await getRandomURL(network) + if (lastKnownAccountData) { latestSnapshot = createSnapshot(lastKnownAccountData) return lastKnownAccountData } else { - const { accountData: initialAccountData } = await waitForAccountData(horizonURLs, accountID) + const { accountData: initialAccountData } = await waitForAccountData(accountID, network) accountDataCache.set(cacheKey, initialAccountData) // Don't set `latestSnapshot` yet or the value will initially not be emitted @@ -420,7 +470,7 @@ function subscribeToAccountUncached(horizonURLs: string[], accountID: string) { } return merge( // Update whenever we receive an account effect push notification - subscribeToAccountEffects(horizonURLs, accountID).pipe(map(() => fetchAccountData(horizonURLs, accountID))), + subscribeToAccountEffects(accountID, network).pipe(map(() => fetchAccountData(accountID, network))), // Update on new optimistic updates accountDataUpdates.observe().pipe( map(handleNewOptimisticUpdate), @@ -430,7 +480,7 @@ function subscribeToAccountUncached(horizonURLs: string[], accountID: string) { Observable.from([0]).pipe( map(async () => { await delay(1000) - return fetchAccountData(horizonURLs, accountID) + return fetchAccountData(accountID, network) }) ) ) @@ -442,13 +492,13 @@ function subscribeToAccountUncached(horizonURLs: string[], accountID: string) { export const subscribeToAccount = cachify(accountSubscriptionCache, subscribeToAccountUncached, createAccountCacheKey) -function subscribeToAccountTransactionsUncached(horizonURLs: string[], accountID: string) { +function subscribeToAccountTransactionsUncached(accountID: string, network: Networks) { const debug = DebugLogger(`net-worker:subscriptions:account-transactions:${accountID}`) let latestCursor: string | undefined const fetchInitial = async () => { - const page = await fetchAccountTransactions(horizonURLs, accountID, { + const page = await fetchAccountTransactions(accountID, network, { limit: 1, order: "desc" }) @@ -464,10 +514,10 @@ function subscribeToAccountTransactionsUncached(horizonURLs: string[], accountID debug(`Fetching latest transactions…`) if (latestCursor) { - const page = await fetchAccountTransactions(horizonURLs, accountID, { cursor: latestCursor, limit: 10 }) + const page = await fetchAccountTransactions(accountID, network, { cursor: latestCursor, limit: 10 }) return [page, "asc"] as const } else { - const page = await fetchAccountTransactions(horizonURLs, accountID, { limit: 10, order: "desc" }) + const page = await fetchAccountTransactions(accountID, network, { limit: 10, order: "desc" }) return [page, "desc"] as const } }, @@ -483,7 +533,7 @@ function subscribeToAccountTransactionsUncached(horizonURLs: string[], accountID }) return multicast( - subscribeToAccountEffects(horizonURLs, accountID).pipe( + subscribeToAccountEffects(accountID, network).pipe( flatMap(async function*(): AsyncIterableIterator { for (let i = 0; i < 3; i++) { const [page, order] = await fetchLatestTxs() @@ -513,10 +563,10 @@ export const subscribeToAccountTransactions = cachify( createAccountCacheKey ) -function subscribeToOpenOrdersUncached(horizonURLs: string[], accountID: string) { +function subscribeToOpenOrdersUncached(accountID: string, network: Networks) { const debug = DebugLogger(`net-worker:subscriptions:account-orders:${accountID}`) - const horizonURL = getRandomURL(horizonURLs) - const serviceID = getServiceID(horizonURL) + let horizonURL = "" + const serviceID = getServiceID(network) let latestCursor: string | undefined let latestSet: ServerApi.OfferRecord[] = [] @@ -526,7 +576,7 @@ function subscribeToOpenOrdersUncached(horizonURLs: string[], accountID: string) // Don't use latest cursor as we want to fetch all open orders // (otherwise we could not handle order deletions) - const page = await fetchAccountOpenOrders(horizonURLs, accountID, { order: "desc" }) + const page = await fetchAccountOpenOrders(accountID, network, { order: "desc" }) return page._embedded.records } @@ -549,6 +599,7 @@ function subscribeToOpenOrdersUncached(horizonURLs: string[], accountID: string) fetchUpdate, async init() { debug(`Subscribing to open orders…`) + horizonURL = await getRandomURL(network) const records = await fetchUpdate() if (records.length > 0) { @@ -580,7 +631,7 @@ function subscribeToOpenOrdersUncached(horizonURLs: string[], accountID: string) // unreliable and the account effects stream only indicates a trade // happening, not the creation/cancellation of one return merge( - subscribeToAccountEffects(horizonURLs, accountID).pipe(map(() => fetchUpdate())), + subscribeToAccountEffects(accountID, network).pipe(map(() => fetchUpdate())), offerUpdates.observe().pipe(map(handleNewOptimisticUpdate)) ) } @@ -627,7 +678,7 @@ function createEmptyOrderbookRecord(base: Asset, counter: Asset): ServerApi.Orde } } -function subscribeToOrderbookUncached(horizonURLs: string[], sellingAsset: string, buyingAsset: string) { +function subscribeToOrderbookUncached(network: Networks, sellingAsset: string, buyingAsset: string) { const debug = DebugLogger(`net-worker:subscriptions:orderbook:${buyingAsset}-${sellingAsset}`) const buying = parseAssetID(buyingAsset) @@ -638,13 +689,13 @@ function subscribeToOrderbookUncached(horizonURLs: string[], sellingAsset: strin return Observable.from([createEmptyOrderbookRecord(buying, buying)]) } - const horizonURL = getRandomURL(horizonURLs) + let horizonURL = "" const createURL = () => String(new URL(`/order_book?${qs.stringify({ ...query, cursor: "now" })}`, horizonURL)) - const fetchUpdate = () => fetchOrderbookRecord(horizonURLs, sellingAsset, buyingAsset) + const fetchUpdate = () => fetchOrderbookRecord(network, sellingAsset, buyingAsset) let latestKnownSnapshot = "" const fetchQueue = getFetchQueue(horizonURL) - const serviceID = getServiceID(horizonURL) + const serviceID = getServiceID(network) // TODO: Optimize - Make UpdateT = ValueT & { [$snapshot]: string } @@ -658,6 +709,7 @@ function subscribeToOrderbookUncached(horizonURLs: string[], sellingAsset: strin fetchUpdate, async init() { debug(`Subscribing to order book…`) + horizonURL = await getRandomURL(network) const record = await fetchUpdate() latestKnownSnapshot = JSON.stringify(record) return record @@ -704,11 +756,11 @@ export interface PaginationOptions { } export async function fetchAccountData( - horizonURLs: string | string[], accountID: string, + network: Networks, priority: number = 2 ): Promise<(Horizon.AccountResponse & { home_domain?: string | undefined }) | null> { - const horizonURL = Array.isArray(horizonURLs) ? getRandomURL(horizonURLs) : horizonURLs + const horizonURL = await getRandomURL(network) const fetchQueue = getFetchQueue(horizonURL) const url = new URL(`/accounts/${accountID}?${qs.stringify(identification)}`, horizonURL) const response = await fetchQueue.add(() => fetch(String(url)), { priority }) @@ -721,7 +773,8 @@ export async function fetchAccountData( return optimisticallyUpdateAccountData(horizonURL, accountData) } -export async function fetchLatestAccountEffect(horizonURL: string, accountID: string) { +export async function fetchLatestAccountEffect(accountID: string, network: Networks) { + const horizonURL = await getRandomURL(network) const fetchQueue = getFetchQueue(horizonURL) const url = new URL( `/accounts/${accountID}/effects?${qs.stringify({ @@ -746,11 +799,11 @@ export interface FetchTransactionsOptions extends PaginationOptions { } export async function fetchAccountTransactions( - horizonURLs: string[], accountID: string, + network: Networks, options: FetchTransactionsOptions = {} ): Promise> { - const horizonURL = getRandomURL(horizonURLs) + const horizonURL = await getRandomURL(network) const fetchQueue = getFetchQueue(horizonURL) const pagination = { cursor: options.cursor, @@ -785,12 +838,8 @@ export async function fetchAccountTransactions( return collection } -export async function fetchAccountOpenOrders( - horizonURLs: string[], - accountID: string, - options: PaginationOptions = {} -) { - const horizonURL = getRandomURL(horizonURLs) +export async function fetchAccountOpenOrders(accountID: string, network: Networks, options: PaginationOptions = {}) { + const horizonURL = await getRandomURL(network) const fetchQueue = getFetchQueue(horizonURL) const url = new URL(`/accounts/${accountID}/offers?${qs.stringify({ ...identification, ...options })}`, horizonURL) @@ -816,11 +865,11 @@ export async function fetchFeeStats(horizonURL: string): Promise { return response.json() } -export async function fetchOrderbookRecord(horizonURLs: string[], sellingAsset: string, buyingAsset: string) { +export async function fetchOrderbookRecord(network: Networks, sellingAsset: string, buyingAsset: string) { if (buyingAsset === sellingAsset) { return createEmptyOrderbookRecord(parseAssetID(buyingAsset), parseAssetID(buyingAsset)) } - const horizonURL = getRandomURL(horizonURLs) + const horizonURL = await getRandomURL(network) const fetchQueue = getFetchQueue(horizonURL) const query = createOrderbookQuery(parseAssetID(sellingAsset), parseAssetID(buyingAsset)) const url = new URL(`/order_book?${qs.stringify({ ...identification, ...query })}`, horizonURL) @@ -829,7 +878,8 @@ export async function fetchOrderbookRecord(horizonURLs: string[], sellingAsset: return parseJSONResponse(response) } -export async function fetchTimebounds(horizonURL: string, timeout: number) { +export async function fetchTimebounds(timeout: number, network: Networks) { + const horizonURL = await getRandomURL(network) const fetchQueue = getFetchQueue(horizonURL) const horizon = new Server(horizonURL)