From d49cae9f4e13134e721cb42973e98acbff60e781 Mon Sep 17 00:00:00 2001 From: Tom Beynon Date: Sun, 12 Jan 2025 23:41:10 +0000 Subject: [PATCH 1/5] Implement retries on all API requests --- src/components/Voting.js | 2 +- src/utils/Network.mjs | 3 +- src/utils/RestClient.mjs | 87 ++++++++++++++++++++++------------------ 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/components/Voting.js b/src/components/Voting.js index 2253437..772d115 100644 --- a/src/components/Voting.js +++ b/src/components/Voting.js @@ -83,7 +83,7 @@ function Voting(props) { const { clearExisting } = opts || {} try { - let newProposals = await network.restClient.getProposals() + let newProposals = await network.restClient.getProposals({ timeout: 10000 }) newProposals = await mapSync(newProposals.map(el => { return async () => { return await Proposal(el) diff --git a/src/utils/Network.mjs b/src/utils/Network.mjs index 9744d0c..f214852 100644 --- a/src/utils/Network.mjs +++ b/src/utils/Network.mjs @@ -131,10 +131,9 @@ class Network { this.gasModifier = this.data.gasModifier || 1.5 } - async connect(opts) { + async connect() { try { this.restClient = await RestClient(this.chain.chainId, this.restUrl, { - connectTimeout: opts?.timeout, apiVersions: this.chain.apiVersions }) this.restUrl = this.restClient.restUrl diff --git a/src/utils/RestClient.mjs b/src/utils/RestClient.mjs index f315963..fdf31e2 100644 --- a/src/utils/RestClient.mjs +++ b/src/utils/RestClient.mjs @@ -8,9 +8,13 @@ import { sleep } from "@cosmjs/utils"; const RestClient = async (chainId, restUrls, opts) => { const config = _.merge({ - connectTimeout: 10000, + timeout: 5000, + retries: 2, + apiVersions: {} }, opts) - const restUrl = await findAvailableUrl(restUrls, { timeout: config.connectTimeout }) + const restUrl = await findAvailableUrl(restUrls, { timeout: 10000 }) + const client = axios.create({ baseURL: restUrl, timeout: config.timeout }); + axiosRetry(client, { retries: config.retries, shouldResetTimeout: true }); function getAllValidators(pageSize, opts, pageCallback) { return getAllPages((nextKey) => { @@ -33,9 +37,9 @@ const RestClient = async (chainId, restUrls, opts) => { searchParams.append("pagination.limit", pageSize); if (nextKey) searchParams.append("pagination.key", nextKey); - return axios + return client .get( - apiUrl('staking', `validators?${searchParams.toString()}`), { + apiPath('staking', `validators?${searchParams.toString()}`), { timeout: opts.timeout || 10000, }) .then((res) => res.data); @@ -59,8 +63,8 @@ const RestClient = async (chainId, restUrls, opts) => { if (nextKey) searchParams.append("pagination.key", nextKey); - return axios - .get(apiUrl('staking', `validators/${validatorAddress}/delegations?${searchParams.toString()}`), opts) + return client + .get(apiPath('staking', `validators/${validatorAddress}/delegations?${searchParams.toString()}`), opts) .then((res) => res.data); } @@ -69,8 +73,8 @@ const RestClient = async (chainId, restUrls, opts) => { const searchParams = new URLSearchParams(); if (nextKey) searchParams.append("pagination.key", nextKey); - return axios - .get(apiUrl('bank', `balances/${address}?${searchParams.toString()}`), opts) + return client + .get(apiPath('bank', `balances/${address}?${searchParams.toString()}`), opts) .then((res) => res.data) }).then((pages) => { const result = pages.map((el) => el.balances).flat() @@ -85,8 +89,8 @@ const RestClient = async (chainId, restUrls, opts) => { } function getDelegations(address) { - return axios - .get(apiUrl('staking', `delegations/${address}`)) + return client + .get(apiPath('staking', `delegations/${address}`)) .then((res) => res.data) .then((result) => { const delegations = result.delegation_responses.reduce( @@ -98,8 +102,8 @@ const RestClient = async (chainId, restUrls, opts) => { } function getRewards(address, opts) { - return axios - .get(apiUrl('distribution', `delegators/${address}/rewards`), opts) + return client + .get(apiPath('distribution', `delegators/${address}/rewards`), opts) .then((res) => res.data) .then((result) => { const rewards = result.rewards.reduce( @@ -111,8 +115,8 @@ const RestClient = async (chainId, restUrls, opts) => { } function getCommission(validatorAddress, opts) { - return axios - .get(apiUrl('distribution', `validators/${validatorAddress}/commission`), opts) + return client + .get(apiPath('distribution', `validators/${validatorAddress}/commission`), opts) .then((res) => res.data) .then((result) => { return result.commission; @@ -127,8 +131,8 @@ const RestClient = async (chainId, restUrls, opts) => { if (nextKey) searchParams.append("pagination.key", nextKey); - return axios - .get(apiUrl('gov', `proposals?${searchParams.toString()}`), opts) + return client + .get(apiPath('gov', `proposals?${searchParams.toString()}`), opts) .then((res) => res.data); }).then((pages) => { return pages.map(el => el.proposals).flat(); @@ -136,14 +140,14 @@ const RestClient = async (chainId, restUrls, opts) => { } function getProposalTally(proposal_id, opts) { - return axios - .get(apiUrl('gov', `proposals/${proposal_id}/tally`), opts) + return client + .get(apiPath('gov', `proposals/${proposal_id}/tally`), opts) .then((res) => res.data); } function getProposalVote(proposal_id, address, opts) { - return axios - .get(apiUrl('gov', `proposals/${proposal_id}/votes/${address}`), opts) + return client + .get(apiPath('gov', `proposals/${proposal_id}/votes/${address}`), opts) .then((res) => res.data); } @@ -155,8 +159,8 @@ const RestClient = async (chainId, restUrls, opts) => { if (nextKey) searchParams.append("pagination.key", nextKey); - return axios - .get(apiUrl('authz', `grants/grantee/${grantee}?${searchParams.toString()}`), opts) + return client + .get(apiPath('authz', `grants/grantee/${grantee}?${searchParams.toString()}`), opts) .then((res) => res.data); }, pageCallback).then((pages) => { return pages.map(el => el.grants).flat(); @@ -171,8 +175,8 @@ const RestClient = async (chainId, restUrls, opts) => { if (nextKey) searchParams.append("pagination.key", nextKey); - return axios - .get(apiUrl('authz', `grants/granter/${granter}?${searchParams.toString()}`), opts) + return client + .get(apiPath('authz', `grants/granter/${granter}?${searchParams.toString()}`), opts) .then((res) => res.data); }, pageCallback).then((pages) => { return pages.map(el => el.grants).flat(); @@ -185,8 +189,8 @@ const RestClient = async (chainId, restUrls, opts) => { searchParams.append("grantee", grantee); if (granter) searchParams.append("granter", granter); - return axios - .get(apiUrl('authz', `grants?${searchParams.toString()}`), opts) + return client + .get(apiPath('authz', `grants?${searchParams.toString()}`), opts) .then((res) => res.data) .then((result) => { return result.grants; @@ -194,8 +198,8 @@ const RestClient = async (chainId, restUrls, opts) => { } function getWithdrawAddress(address, opts) { - return axios - .get(apiUrl('distribution', `delegators/${address}/withdraw_address`)) + return client + .get(apiPath('distribution', `delegators/${address}/withdraw_address`)) .then((res) => res.data) .then((result) => { return result.withdraw_address; @@ -214,18 +218,19 @@ const RestClient = async (chainId, restUrls, opts) => { } if (order) searchParams.append('order_by', order); - const client = axios.create({ baseURL: restUrl }); - axiosRetry(client, { retries: retries || 0, shouldResetTimeout: true, retryCondition: (e) => true }); - return client.get(apiPath('tx', `txs?${searchParams.toString()}`), opts).then((res) => res.data); + return client.get(apiPath('tx', `txs?${searchParams.toString()}`), { + 'axios-retry': { retries: retries }, + ...opts + }).then((res) => res.data); } function getTransaction(txHash) { - return axios.get(apiUrl('tx', `txs/${txHash}`)).then((res) => res.data); + return client.get(apiPath('tx', `txs/${txHash}`)).then((res) => res.data); } function getAccount(address) { - return axios - .get(apiUrl('auth', `accounts/${address}`)) + return client + .get(apiPath('auth', `accounts/${address}`)) .then((res) => res.data.account) .then((value) => { if(!value) throw new Error('Failed to fetch account, please try again') @@ -275,12 +280,12 @@ const RestClient = async (chainId, restUrls, opts) => { }; function simulate(params){ - return axios.post(apiUrl('tx', `simulate`), params) + return client.post(apiPath('tx', `simulate`), params) .then((res) => res.data) } function broadcast(params){ - return axios.post(apiUrl('tx', `txs`), params) + return client.post(apiPath('tx', `txs`), params) .then((res) => parseTxResult(res.data.tx_response)) } @@ -370,10 +375,13 @@ const RestClient = async (chainId, restUrls, opts) => { async function getLatestBlock(opts){ const { timeout } = opts || {} - const url = opts?.url || restUrl + let blockClient = client + if(opts?.url){ + blockClient = axios.create({ baseURL: opts.url }); + } const path = opts?.path || apiPath('/base/tendermint', 'blocks/latest') try { - return await axios.get(url + path, { timeout }) + return await blockClient.get(path, { timeout }) .then((res) => res.data) } catch (error) { const fallback = '/blocks/latest' @@ -389,8 +397,7 @@ const RestClient = async (chainId, restUrls, opts) => { } function apiPath(type, path){ - const versions = config.apiVersions || {} - const version = versions[type] || 'v1beta1' + const version = config.apiVersions[type] || 'v1beta1' return `/cosmos/${type}/${version}/${path}` } From 6fa36265f8c92fc3ee26bc930e1e3c2e0a7cc4e8 Mon Sep 17 00:00:00 2001 From: Tom Beynon Date: Sun, 12 Jan 2025 23:56:03 +0000 Subject: [PATCH 2/5] Handle non-JSON IPFS response and batch requests --- src/components/Voting.js | 2 +- src/utils/Proposal.mjs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Voting.js b/src/components/Voting.js index 772d115..4032e74 100644 --- a/src/components/Voting.js +++ b/src/components/Voting.js @@ -88,7 +88,7 @@ function Voting(props) { return async () => { return await Proposal(el) } - })) + }), 5) setError() setProposals(sortProposals(newProposals)) diff --git a/src/utils/Proposal.mjs b/src/utils/Proposal.mjs index f898b62..1624876 100644 --- a/src/utils/Proposal.mjs +++ b/src/utils/Proposal.mjs @@ -36,9 +36,12 @@ const Proposal = async (data) => { }else{ ipfsUrl = `https://ipfs.io/ipfs/${metadata}` } - metadata = await axios.get(ipfsUrl, { timeout: 5000 }).then(res => res.data) - title = metadata.title - description = metadata.summary || metadata.description || metadata.details + response = await axios.get(ipfsUrl, { timeout: 5000 }) + if(response.headers['content-type'] === 'application/json'){ + metadata = response.data + title = metadata.title + description = metadata.summary || metadata.description || metadata.details + } } catch (e) { console.log(e) } From 0fe28df14dcddb561acd37877642c8038c17e83f Mon Sep 17 00:00:00 2001 From: Tom Beynon Date: Mon, 13 Jan 2025 00:13:12 +0000 Subject: [PATCH 3/5] Remove unused apiUrl method and increase timeout on post requests --- src/utils/RestClient.mjs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/utils/RestClient.mjs b/src/utils/RestClient.mjs index fdf31e2..4159db0 100644 --- a/src/utils/RestClient.mjs +++ b/src/utils/RestClient.mjs @@ -280,12 +280,12 @@ const RestClient = async (chainId, restUrls, opts) => { }; function simulate(params){ - return client.post(apiPath('tx', `simulate`), params) + return client.post(apiPath('tx', `simulate`), params, { timeout: 30000 }) .then((res) => res.data) } function broadcast(params){ - return client.post(apiPath('tx', `txs`), params) + return client.post(apiPath('tx', `txs`), params, { timeout: 30000 }) .then((res) => parseTxResult(res.data.tx_response)) } @@ -392,10 +392,6 @@ const RestClient = async (chainId, restUrls, opts) => { } } - function apiUrl(type, path){ - return restUrl + apiPath(type, path) - } - function apiPath(type, path){ const version = config.apiVersions[type] || 'v1beta1' return `/cosmos/${type}/${version}/${path}` From 3879baab41f1456bb73641052d21bf686532491f Mon Sep 17 00:00:00 2001 From: Tom Beynon Date: Mon, 13 Jan 2025 13:11:08 +0000 Subject: [PATCH 4/5] Always retry requests other than broadcast --- src/utils/RestClient.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/RestClient.mjs b/src/utils/RestClient.mjs index 4159db0..9627bc3 100644 --- a/src/utils/RestClient.mjs +++ b/src/utils/RestClient.mjs @@ -14,7 +14,7 @@ const RestClient = async (chainId, restUrls, opts) => { }, opts) const restUrl = await findAvailableUrl(restUrls, { timeout: 10000 }) const client = axios.create({ baseURL: restUrl, timeout: config.timeout }); - axiosRetry(client, { retries: config.retries, shouldResetTimeout: true }); + axiosRetry(client, { retries: config.retries, shouldResetTimeout: true, retryCondition: () => true }); function getAllValidators(pageSize, opts, pageCallback) { return getAllPages((nextKey) => { @@ -285,7 +285,7 @@ const RestClient = async (chainId, restUrls, opts) => { } function broadcast(params){ - return client.post(apiPath('tx', `txs`), params, { timeout: 30000 }) + return client.post(apiPath('tx', `txs`), params, { timeout: 30000, 'axios-retry': { retries: 0 } }) .then((res) => parseTxResult(res.data.tx_response)) } From b27f2ffa651668d8a24550be084debba6f73eb79 Mon Sep 17 00:00:00 2001 From: Tom Beynon Date: Tue, 14 Jan 2025 20:56:23 +0000 Subject: [PATCH 5/5] Don't retry when polling for transaction after broadcast --- src/utils/RestClient.mjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/utils/RestClient.mjs b/src/utils/RestClient.mjs index 9627bc3..118c65e 100644 --- a/src/utils/RestClient.mjs +++ b/src/utils/RestClient.mjs @@ -219,13 +219,17 @@ const RestClient = async (chainId, restUrls, opts) => { if (order) searchParams.append('order_by', order); return client.get(apiPath('tx', `txs?${searchParams.toString()}`), { - 'axios-retry': { retries: retries }, + 'axios-retry': { retries: retries ?? config.retries }, ...opts }).then((res) => res.data); } - function getTransaction(txHash) { - return client.get(apiPath('tx', `txs/${txHash}`)).then((res) => res.data); + function getTransaction(txHash, _opts) { + const { retries, ...opts } = _opts; + return client.get(apiPath('tx', `txs/${txHash}`), { + 'axios-retry': { retries: retries ?? config.retries }, + ...opts + }).then((res) => res.data); } function getAccount(address) { @@ -305,7 +309,7 @@ const RestClient = async (chainId, restUrls, opts) => { } await sleep(pollIntervalMs); try { - const response = await getTransaction(txId); + const response = await getTransaction(txId, { retries: 0 }); const result = parseTxResult(response.tx_response) return result } catch {