Skip to content

Commit 81f3532

Browse files
authored
Use fetch queue for SSE reconnects & tx creation requests (#1127)
* Use fetch queue on SSE reconnects & for tx creation requests * Make balances load quicker * Replace more (last?) instances of requests without queue
1 parent b4abb47 commit 81f3532

File tree

5 files changed

+159
-108
lines changed

5 files changed

+159
-108
lines changed

src/Generic/lib/stellar.ts

-60
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,6 @@ import fetch from "isomorphic-fetch"
33
import { xdr, Asset, Horizon, Keypair, NotFoundError, Server, ServerApi, Transaction } from "stellar-sdk"
44
import { AssetRecord } from "../hooks/stellar-ecosystem"
55
import { AccountData } from "./account"
6-
import { joinURL } from "./url"
7-
import { CustomError } from "./errors"
8-
9-
export interface SmartFeePreset {
10-
capacityTrigger: number
11-
maxFee: number
12-
percentile: number
13-
}
14-
15-
interface FeeStatsDetails {
16-
max: string
17-
min: string
18-
mode: string
19-
p10: string
20-
p20: string
21-
p30: string
22-
p40: string
23-
p50: string
24-
p60: string
25-
p70: string
26-
p80: string
27-
p90: string
28-
p95: string
29-
p99: string
30-
}
31-
32-
// See <https://www.stellar.org/developers/horizon/reference/endpoints/fee-stats.html>
33-
interface FeeStats {
34-
last_ledger: string
35-
last_ledger_base_fee: string
36-
ledger_capacity_usage: string
37-
fee_charged: FeeStatsDetails
38-
max_fee: FeeStatsDetails
39-
}
406

417
const MAX_INT64 = "9223372036854775807"
428

@@ -91,32 +57,6 @@ export function stringifyAsset(assetOrTrustline: Asset | Horizon.BalanceLine) {
9157
}
9258
}
9359

94-
async function fetchFeeStats(horizon: Server): Promise<FeeStats> {
95-
const url = joinURL(getHorizonURL(horizon), "/fee_stats")
96-
const response = await fetch(url)
97-
98-
if (!response.ok) {
99-
throw CustomError("RequestFailedError", `Request to ${url} failed with status code ${response.status}`, {
100-
target: url,
101-
status: response.status
102-
})
103-
}
104-
return response.json()
105-
}
106-
107-
export async function selectSmartTransactionFee(horizon: Server, preset: SmartFeePreset): Promise<number> {
108-
const feeStats = await fetchFeeStats(horizon)
109-
const capacityUsage = Number.parseFloat(feeStats.ledger_capacity_usage)
110-
const percentileFees = feeStats.fee_charged
111-
112-
const smartFee =
113-
capacityUsage > preset.capacityTrigger
114-
? Number.parseInt((percentileFees as any)[`p${preset.percentile}`] || feeStats.fee_charged.mode, 10)
115-
: Number.parseInt(feeStats.fee_charged.min, 10)
116-
117-
return Math.min(smartFee, preset.maxFee)
118-
}
119-
12060
export async function friendbotTopup(horizonURL: string, publicKey: string) {
12161
const horizonMetadata = await (await fetch(horizonURL)).json()
12262
const friendBotHref = horizonMetadata._links.friendbot.href.replace(/\{\?.*/, "")

src/Generic/lib/third-party-security.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Server, Transaction, Horizon } from "stellar-sdk"
22
import { CustomError } from "./errors"
33
import StellarGuardIcon from "~Icons/components/StellarGuard"
44
import LobstrVaultIcon from "~Icons/components/LobstrVault"
5+
import { workers } from "~Workers/worker-controller"
56

67
export interface ThirdPartySecurityService {
78
endpoints: {
@@ -34,8 +35,11 @@ const services: ThirdPartySecurityService[] = [
3435
]
3536

3637
export async function isThirdPartyProtected(horizon: Server, accountPubKey: string) {
37-
const account = await horizon.loadAccount(accountPubKey)
38-
const signerKeys = account.signers.map(signer => signer.key)
38+
const { netWorker } = await workers
39+
const horizonURL = horizon.serverURL.toString()
40+
41+
const account = await netWorker.fetchAccountData(horizonURL, accountPubKey)
42+
const signerKeys = (account?.signers || []).map(signer => signer.key)
3943

4044
const enabledService = services.find(service => signerKeys.includes(service.publicKey))
4145
return enabledService

src/Generic/lib/transaction.ts

+48-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
Account as StellarAccount,
23
Asset,
34
Keypair,
45
Memo,
@@ -13,7 +14,14 @@ import {
1314
import { Account } from "~App/contexts/accounts"
1415
import { WrongPasswordError, CustomError } from "./errors"
1516
import { applyTimeout } from "./promise"
16-
import { getAllSources, isNotFoundError, isSignedByAnyOf, selectSmartTransactionFee, SmartFeePreset } from "./stellar"
17+
import { getAllSources, isNotFoundError, isSignedByAnyOf } from "./stellar"
18+
import { workers } from "~Workers/worker-controller"
19+
20+
interface SmartFeePreset {
21+
capacityTrigger: number
22+
maxFee: number
23+
percentile: number
24+
}
1725

1826
// See <https://github.com/stellar/go/issues/926>
1927
const highFeePreset: SmartFeePreset = {
@@ -72,9 +80,20 @@ async function accountExists(horizon: Server, publicKey: string) {
7280
}
7381
}
7482

75-
async function selectTransactionFeeWithFallback(horizon: Server, fallbackFee: number) {
83+
async function selectTransactionFeeWithFallback(horizonURL: string, preset: SmartFeePreset, fallbackFee: number) {
7684
try {
77-
return await selectSmartTransactionFee(horizon, highFeePreset)
85+
const { netWorker } = await workers
86+
const feeStats = await netWorker.fetchFeeStats(horizonURL)
87+
88+
const capacityUsage = Number.parseFloat(feeStats.ledger_capacity_usage)
89+
const percentileFees = feeStats.fee_charged
90+
91+
const smartFee =
92+
capacityUsage > preset.capacityTrigger
93+
? Number.parseInt((percentileFees as any)[`p${preset.percentile}`] || feeStats.fee_charged.mode, 10)
94+
: Number.parseInt(feeStats.fee_charged.min, 10)
95+
96+
return Math.min(smartFee, preset.maxFee)
7897
} catch (error) {
7998
// Don't show error notification, since our horizon's endpoint is non-functional anyway
8099
// tslint:disable-next-line no-console
@@ -98,17 +117,27 @@ interface TxBlueprint {
98117

99118
export async function createTransaction(operations: Array<xdr.Operation<any>>, options: TxBlueprint) {
100119
const { horizon, walletAccount } = options
120+
const { netWorker } = await workers
121+
101122
const fallbackFee = 10000
123+
const horizonURL = horizon.serverURL.toString()
102124
const timeout = selectTransactionTimeout(options.accountData)
103125

104-
const [account, smartTxFee, timebounds] = await Promise.all([
105-
applyTimeout(horizon.loadAccount(walletAccount.publicKey), 10000, () =>
126+
const [accountMetadata, smartTxFee, timebounds] = await Promise.all([
127+
applyTimeout(netWorker.fetchAccountData(horizonURL, walletAccount.publicKey), 10000, () =>
106128
fail(`Fetching source account data timed out`)
107129
),
108-
applyTimeout(selectTransactionFeeWithFallback(horizon, fallbackFee), 5000, () => fallbackFee),
109-
applyTimeout(horizon.fetchTimebounds(timeout), 10000, () => fail(`Syncing time bounds with horizon timed out`))
110-
])
130+
applyTimeout(selectTransactionFeeWithFallback(horizonURL, highFeePreset, fallbackFee), 5000, () => fallbackFee),
131+
applyTimeout(netWorker.fetchTimebounds(horizonURL, timeout), 10000, () =>
132+
fail(`Syncing time bounds with horizon timed out`)
133+
)
134+
] as const)
111135

136+
if (!accountMetadata) {
137+
throw Error(`Failed to query account from horizon server: ${walletAccount.publicKey}`)
138+
}
139+
140+
const account = new StellarAccount(accountMetadata.id, accountMetadata.sequence)
112141
const networkPassphrase = walletAccount.testnet ? Networks.TESTNET : Networks.PUBLIC
113142
const txFee = Math.max(smartTxFee, options.minTransactionFee || 0)
114143

@@ -163,13 +192,23 @@ export async function signTransaction(transaction: Transaction, walletAccount: A
163192
}
164193

165194
export async function requiresRemoteSignatures(horizon: Server, transaction: Transaction, walletPublicKey: string) {
195+
const { netWorker } = await workers
196+
const horizonURL = horizon.serverURL.toString()
166197
const sources = getAllSources(transaction)
167198

168199
if (sources.length > 1) {
169200
return true
170201
}
171202

172-
const accounts = await Promise.all(sources.map(sourcePublicKey => horizon.loadAccount(sourcePublicKey)))
203+
const accounts = await Promise.all(
204+
sources.map(async sourcePublicKey => {
205+
const account = await netWorker.fetchAccountData(horizonURL, sourcePublicKey)
206+
if (!account) {
207+
throw Error(`Could not fetch account metadata from horizon server: ${sourcePublicKey}`)
208+
}
209+
return account
210+
})
211+
)
173212

174213
return accounts.some(account => {
175214
const thisWalletSigner = account.signers.find(signer => signer.key === walletPublicKey)

src/Workers/lib/event-source.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { handleConnectionState } from "./connection"
22

33
const watchdogIntervalTime = 15_000
44

5-
function createReconnectDelay(options: { delay: number }): () => Promise<void> {
5+
function createReconnectDelay(options: { initialDelay: number }): () => Promise<void> {
6+
let delay = options.initialDelay
67
let lastConnectionAttemptTime = 0
78

89
const networkBackOnline = () => {
@@ -21,14 +22,17 @@ function createReconnectDelay(options: { delay: number }): () => Promise<void> {
2122
}
2223

2324
return async function delayReconnect() {
24-
const justConnectedBefore = Date.now() - lastConnectionAttemptTime < options.delay
25-
const waitUntil = Date.now() + options.delay
25+
const justConnectedBefore = Date.now() - lastConnectionAttemptTime < delay
26+
const waitUntil = Date.now() + delay
2627

2728
await networkBackOnline()
2829

2930
if (justConnectedBefore) {
30-
// Reconnect immediately (skip await) if last reconnection is long ago
3131
await timeReached(waitUntil)
32+
delay = Math.min(delay * 1.5, options.initialDelay * 8)
33+
} else {
34+
// Reconnect immediately (skip await) if last reconnection is long ago
35+
delay = options.initialDelay
3236
}
3337

3438
lastConnectionAttemptTime = Date.now()
@@ -41,7 +45,11 @@ interface SSEHandlers {
4145
onMessage?(event: MessageEvent): void
4246
}
4347

44-
export function createReconnectingSSE(createURL: () => string, handlers: SSEHandlers) {
48+
export function createReconnectingSSE(
49+
createURL: () => string,
50+
handlers: SSEHandlers,
51+
queueRequest: (task: () => any) => Promise<any>
52+
) {
4553
let currentlySubscribed = false
4654
let delayReconnect: () => Promise<void>
4755

@@ -97,21 +105,20 @@ export function createReconnectingSSE(createURL: () => string, handlers: SSEHand
97105
handlers.onStreamError(error)
98106
}
99107

100-
delayReconnect().then(
101-
() => subscribe(),
102-
unexpectedError => {
108+
delayReconnect()
109+
.then(() => queueRequest(() => subscribe()))
110+
.catch(unexpectedError => {
103111
if (handlers.onUnexpectedError) {
104112
handlers.onUnexpectedError(unexpectedError)
105113
}
106-
}
107-
)
114+
})
108115
}
109116

110117
currentlySubscribed = true
111118
}
112119

113120
const setup = async () => {
114-
delayReconnect = createReconnectDelay({ delay: 1000 })
121+
delayReconnect = createReconnectDelay({ initialDelay: 1000 })
115122
subscribe()
116123
}
117124

0 commit comments

Comments
 (0)