Skip to content

Commit 360c936

Browse files
aziolekmike-code
andauthored
OCT-1284 Create an IPFS implementation with client failover (#26)
Co-authored-by: Michał Kluczek <michal@wildland.io>
1 parent cae3c89 commit 360c936

File tree

15 files changed

+131
-55
lines changed

15 files changed

+131
-55
lines changed

.github/workflows/tpl-deploy-app.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ env:
139139
TESTNET_RPC_URL: "${{ secrets.TESTNET_RPC_URL }}"
140140
ETHERSCAN_API_KEY: "${{ secrets.ETHERSCAN_API_KEY }}"
141141
VITE_ALCHEMY_ID: "${{ secrets.VITE_ALCHEMY_ID }}"
142-
IPFS_GATEWAY: "${{ vars.IPFS_GATEWAY }}"
143142
# ----------------------------------------------------------------------------
144143
# CI/CD
145144
GCP_DOCKER_IMAGES_REGISTRY_SERVICE_ACCOUNT: "${{ secrets.GCP_DOCKER_IMAGES_REGISTRY_SERVICE_ACCOUNT }}"

ci/argocd/templates/octant-application.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ spec:
1515
namespace: $DEPLOYMENT_ID
1616
sources:
1717
- repoURL: 'https://gitlab.com/api/v4/projects/48137258/packages/helm/devel'
18-
targetRevision: 0.2.37
18+
targetRevision: 0.2.38
1919
chart: octant
2020
helm:
2121
parameters:
@@ -33,8 +33,6 @@ spec:
3333
value: '$NETWORK_NAME'
3434
- name: 'webClient.hideCurrentProjectsOutsideAW'
3535
value: 'false'
36-
- name: 'webClient.ipfsGateway'
37-
value: '$IPFS_GATEWAY'
3836
## Graph Node
3937
- name: graphNode.graph.env.NETWORK
4038
value: '$NETWORK_NAME'

client/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Ensure that the `.env` file is present. See `.env.template`.
1010

1111
1. `VITE_NETWORK` sets network used by the application. Supported values are 'Local', 'Mainnet', 'Sepolia'. Whenever different value is set, app uses 'Sepolia' network config.
1212
2. `VITE_JSON_RPC_ENDPOINT`: when provided, app uses first JSON RPC provided with this endpint. When it's not provided, app uses alchemy provider first.
13-
3. `areCurrentEpochsProjectsHiddenOutsideAllocationWindow` when set to 'true' makes current epoch's projects hidden when allocation window is closed.
13+
3. `VITE_ARE_CURRENT_EPOCHS_PROJECTS_HIDDEN_OUTSIDE_ALLOCATION_WINDOW` when set to 'true' makes current epoch's projects hidden when allocation window is closed.
14+
4. `VITE_IPFS_GATEWAYS` is an array of URLs separated by strings sorted by priority with providers the app should try to fetch the data about projects from. When fetching from the last fails client shows error toast message. Each URL should end with a forward slash (`/`).
1415

1516
`yanr generate-abi-typings` is used to generate typings for proposals ABIs that we have in codebase. In these typings custom adjustments are added, e.g. in some places `string` is wrongly instead of `BigInt`. Linter is also disabled there. Since ABIs do not change, this command doesn't need to rerun.
1617

client/cypress/e2e/proposal.cy.ts

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,30 @@ const getButtonAddToAllocate = (): Chainable<any> => {
1212
return proposalView.find('[data-test=ProposalListItemHeader__ButtonAddToAllocate]');
1313
};
1414

15+
const checkProposalItemElements = (): Chainable<any> => {
16+
cy.get('[data-test^=ProposalsView__ProposalsListItem').first().click();
17+
const proposalView = cy.get('[data-test=ProposalListItem').first();
18+
proposalView.get('[data-test=ProposalListItemHeader__Img]').should('be.visible');
19+
proposalView.get('[data-test=ProposalListItemHeader__name]').should('be.visible');
20+
getButtonAddToAllocate().should('be.visible');
21+
proposalView.get('[data-test=ProposalListItemHeader__Button]').should('be.visible');
22+
proposalView.get('[data-test=ProposalListItem__Description]').should('be.visible');
23+
24+
cy.get('[data-test=ProposalListItem__Donors]')
25+
.first()
26+
.scrollIntoView({ offset: { left: 0, top: 100 } });
27+
28+
cy.get('[data-test=ProposalListItem__Donors]').first().should('be.visible');
29+
cy.get('[data-test=ProposalListItem__Donors__DonorsHeader__count]')
30+
.first()
31+
.should('be.visible')
32+
.should('have.text', '0');
33+
return cy
34+
.get('[data-test=ProposalListItem__Donors__noDonationsYet]')
35+
.first()
36+
.should('be.visible');
37+
};
38+
1539
Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => {
1640
describe(`proposal: ${device}`, { viewportHeight, viewportWidth }, () => {
1741
let proposalNames: string[] = [];
@@ -40,27 +64,25 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) =>
4064
});
4165

4266
it('entering proposal view renders all its elements', () => {
43-
cy.get('[data-test^=ProposalsView__ProposalsListItem').first().click();
44-
const proposalView = cy.get('[data-test=ProposalListItem').first();
45-
proposalView.get('[data-test=ProposalListItemHeader__Img]').should('be.visible');
46-
proposalView.get('[data-test=ProposalListItemHeader__name]').should('be.visible');
47-
getButtonAddToAllocate().should('be.visible');
48-
proposalView.get('[data-test=ProposalListItemHeader__Button]').should('be.visible');
49-
proposalView.get('[data-test=ProposalListItem__Description]').should('be.visible');
50-
51-
cy.get('[data-test=ProposalListItem__Donors]')
52-
.first()
53-
.scrollIntoView({ offset: { left: 0, top: 100 } });
54-
55-
cy.get('[data-test=ProposalListItem__Donors]').first().should('be.visible');
56-
cy.get('[data-test=ProposalListItem__Donors__DonorsHeader__count]')
57-
.first()
58-
.should('be.visible')
59-
.should('have.text', '0');
60-
return cy
61-
.get('[data-test=ProposalListItem__Donors__noDonationsYet]')
62-
.first()
63-
.should('be.visible');
67+
checkProposalItemElements();
68+
});
69+
70+
it('entering proposal view renders all its elements with fallback IPFS provider', () => {
71+
cy.intercept('GET', '**/ipfs/**', req => {
72+
if (req.url.includes('infura')) {
73+
req.destroy();
74+
}
75+
});
76+
77+
checkProposalItemElements();
78+
});
79+
80+
it('entering proposal view renders all its elements with fallback IPFS provider', () => {
81+
cy.intercept('GET', '**/ipfs/**', req => {
82+
req.destroy();
83+
});
84+
85+
cy.get('[data-test=Toast--ipfsMessage').should('be.visible');
6486
});
6587

6688
it('entering proposal view allows to add it to allocation and remove, triggering change of the icon, change of the number in navbar', () => {

client/src/api/calls/proposals.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
import env from 'env';
22
import apiService from 'services/apiService';
33

4+
async function getFirstValid(arrayUrls: string[], baseUri: string, index: number): Promise<any> {
5+
return apiService
6+
.get(`${arrayUrls[index]}${baseUri}`)
7+
.then(({ data }) => ({
8+
data,
9+
ipfsGatewayUsed: arrayUrls[index],
10+
}))
11+
.catch(e => {
12+
if (index < arrayUrls.length - 1) {
13+
return getFirstValid(arrayUrls, baseUri, index + 1);
14+
}
15+
throw e;
16+
});
17+
}
18+
419
export function apiGetProposal(baseUri: string): Promise<any> {
5-
const { ipfsGateway } = env;
6-
return apiService.get(`${ipfsGateway}${baseUri}`).then(({ data }) => data);
20+
const { ipfsGateways } = env;
21+
22+
return getFirstValid(ipfsGateways.split(','), baseUri, 0).then(({ data }) => data);
723
}

client/src/components/Allocation/AllocationItem/AllocationItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const AllocationItem: FC<AllocationItemProps> = ({
5050
const isGweiRange = (individualReward && individualReward < GWEI_5) ?? false;
5151

5252
const [isInputFocused, setIsInputFocused] = useState(false);
53-
const { ipfsGateway } = env;
53+
const { ipfsGateways } = env;
5454
const { isConnected } = useAccount();
5555
const { data: currentEpoch } = useCurrentEpoch();
5656
const { isFetching: isFetchingRewardsThreshold } = useProposalRewardsThreshold();
@@ -225,7 +225,7 @@ const AllocationItem: FC<AllocationItemProps> = ({
225225
<Img
226226
className={styles.image}
227227
dataTest="ProposalItem__imageProfile"
228-
src={`${ipfsGateway}${profileImageSmall}`}
228+
sources={ipfsGateways.split(',').map(element => `${element}${profileImageSmall}`)}
229229
/>
230230
<div className={styles.nameAndRewards}>
231231
<div className={styles.name}>{name}</div>

client/src/components/Metrics/MetricsProjectsListItem/MetricsProjectsListItem.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import React, { FC, memo } from 'react';
22

3+
import Img from 'components/ui/Img/Img';
34
import env from 'env';
45
import useProposalsIpfs from 'hooks/queries/useProposalsIpfs';
56

67
import styles from './MetricsProjectsListItem.module.scss';
78
import MetricsProjectsListItemProps from './types';
89

910
const MetricsProjectsListItem: FC<MetricsProjectsListItemProps> = ({ address, epoch, value }) => {
10-
const { ipfsGateway } = env;
11+
const { ipfsGateways } = env;
1112
const { data: proposalsIpfs } = useProposalsIpfs([address], epoch);
1213

1314
const image = proposalsIpfs.at(0)?.profileImageSmall;
1415
const name = proposalsIpfs.at(0)?.name;
1516

1617
return (
1718
<div className={styles.root}>
18-
<img alt="project logo" className={styles.image} src={`${ipfsGateway}${image}`} />
19+
<Img
20+
alt="project logo"
21+
className={styles.image}
22+
sources={ipfsGateways.split(',').map(element => `${element}${image}`)}
23+
/>
1924
<div className={styles.name}>{name}</div>
2025
<div className={styles.value}>{value}</div>
2126
</div>

client/src/components/Proposal/ProposalListItemHeader/ProposalListItemHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const ProposalListItemHeader: FC<ProposalListItemHeaderProps> = ({
2626
website,
2727
epoch,
2828
}) => {
29-
const { ipfsGateway } = env;
29+
const { ipfsGateways } = env;
3030
const { i18n } = useTranslation('translation', { keyPrefix: 'views.proposal' });
3131
const { epoch: epochUrl } = useParams();
3232
const { data: userAllocations } = useUserAllocations(epoch);
@@ -82,7 +82,7 @@ const ProposalListItemHeader: FC<ProposalListItemHeaderProps> = ({
8282
<Img
8383
className={styles.imageProfile}
8484
dataTest="ProposalListItemHeader__Img"
85-
src={`${ipfsGateway}${profileImageSmall}`}
85+
sources={ipfsGateways.split(',').map(element => `${element}${profileImageSmall}`)}
8686
/>
8787
<div className={styles.actionsWrapper}>
8888
<Tooltip

client/src/components/Proposals/ProposalsListItem/ProposalsListItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const ProposalsListItem: FC<ProposalsListItemProps> = ({
2525
epoch,
2626
proposalIpfsWithRewards,
2727
}) => {
28-
const { ipfsGateway } = env;
28+
const { ipfsGateways } = env;
2929
const { address, isLoadingError, profileImageSmall, name, introDescription } =
3030
proposalIpfsWithRewards;
3131
const navigate = useNavigate();
@@ -97,7 +97,7 @@ const ProposalsListItem: FC<ProposalsListItemProps> = ({
9797
? 'ProposalsListItem__imageProfile--archive'
9898
: 'ProposalsListItem__imageProfile'
9999
}
100-
src={`${ipfsGateway}${profileImageSmall}`}
100+
sources={ipfsGateways.split(',').map(element => `${element}${profileImageSmall}`)}
101101
/>
102102
{((isAllocatedTo && isArchivedProposal) || !isArchivedProposal) && (
103103
<ButtonAddToAllocate

client/src/components/shared/ProjectAllocationDetailRow/ProjectAllocationDetailRow.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const ProjectAllocationDetailRow: FC<ProjectAllocationDetailRowProps> = ({
1616
amount,
1717
epoch,
1818
}) => {
19-
const { ipfsGateway } = env;
19+
const { ipfsGateways } = env;
2020
const {
2121
data: { displayCurrency, isCryptoMainValueDisplay },
2222
} = useSettingsStore(({ data }) => ({
@@ -39,7 +39,9 @@ const ProjectAllocationDetailRow: FC<ProjectAllocationDetailRowProps> = ({
3939
<div className={styles.imageAndName}>
4040
<Img
4141
className={styles.image}
42-
src={`${ipfsGateway}${proposalIpfs[0].profileImageSmall!}`}
42+
sources={ipfsGateways
43+
.split(',')
44+
.map(element => `${element}${proposalIpfs[0].profileImageSmall!}`)}
4345
/>
4446
<div className={styles.name}>{proposalIpfs[0].name}</div>
4547
</div>

0 commit comments

Comments
 (0)