From afaac75fcaa9f819c4cee3746adac02e11c2a36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Mi=C3=B1o?= Date: Wed, 10 Apr 2019 16:15:43 -0400 Subject: [PATCH] Feature: Choose IPFS gateway (#592) * use of ipfsgateway from GABA * handle ipfs gateway selection * snapshots * snapshots * reorder ipfs gateways list * only show available ipfs gateways * snapshots * bump gaba * handle current gateway down * snapshots --- app/components/UI/SelectComponent/index.js | 10 ++- .../__snapshots__/index.test.js.snap | 55 ++++++++++++++++ .../Views/AdvancedSettings/index.js | 66 +++++++++++++++++-- .../Views/AdvancedSettings/index.test.js | 9 ++- app/components/Views/Browser/index.js | 15 ++++- .../Settings/__snapshots__/index.test.js.snap | 2 +- app/util/general.js | 15 +++++ app/util/ipfs-gateways.json | 41 ++++++++++++ locales/en.json | 5 +- locales/es.json | 5 +- 10 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 app/util/general.js create mode 100644 app/util/ipfs-gateways.json diff --git a/app/components/UI/SelectComponent/index.js b/app/components/UI/SelectComponent/index.js index 1c8912f28a5..76aef3e384d 100644 --- a/app/components/UI/SelectComponent/index.js +++ b/app/components/UI/SelectComponent/index.js @@ -82,6 +82,10 @@ const styles = StyleSheet.create({ export default class SelectComponent extends Component { static propTypes = { + /** + * Default value to show + */ + defaultValue: PropTypes.string, /** * Label for the field */ @@ -137,10 +141,14 @@ export default class SelectComponent extends Component { }; getSelectedValue = () => { - const el = this.props.options && this.props.options.filter(o => o.value === this.props.selectedValue); + const { options, selectedValue, defaultValue } = this.props; + const el = options && options.filter(o => o.value === selectedValue); if (el.length && el[0].label) { return el[0].label; } + if (defaultValue) { + return defaultValue; + } return ''; }; diff --git a/app/components/Views/AdvancedSettings/__snapshots__/index.test.js.snap b/app/components/Views/AdvancedSettings/__snapshots__/index.test.js.snap index fa92f98e0ca..dea8ed649d7 100644 --- a/app/components/Views/AdvancedSettings/__snapshots__/index.test.js.snap +++ b/app/components/Views/AdvancedSettings/__snapshots__/index.test.js.snap @@ -313,6 +313,61 @@ exports[`AdvancedSettings should render correctly 1`] = ` + + + IPFS Gateway + + + Choose your preferred IPFS gateway. + + + + + { + componentDidMount = async () => { + await this.handleAvailableIpfsGateways(); this.mounted = true; // Workaround https://github.com/facebook/react-native/issues/9958 this.state.inputWidth && @@ -161,6 +179,26 @@ class AdvancedSettings extends Component { this.mounted = false; }; + handleAvailableIpfsGateways = async () => { + const ipfsGatewaysPromises = ipfsGateways.map(async ipfsGateway => { + const testUrl = ipfsGateway.value + HASH_TO_TEST + '#x-ipfs-companion-no-redirect'; + try { + const res = await timeoutFetch(testUrl); + const text = await res.text(); + const available = text.trim() === HASH_STRING.trim(); + ipfsGateway.available = available; + return ipfsGateway; + } catch (e) { + ipfsGateway.available = false; + return ipfsGateway; + } + }); + const ipfsGatewaysAvailability = await Promise.all(ipfsGatewaysPromises); + const onlineIpfsGateways = ipfsGatewaysAvailability.filter(ipfsGateway => ipfsGateway.available); + const sortedOnlineIpfsGateways = onlineIpfsGateways.sort((a, b) => a.key < b.key); + this.setState({ onlineIpfsGateways: sortedOnlineIpfsGateways }); + }; + displayResetAccountModal = () => { this.setState({ resetModalVisible: true }); }; @@ -258,9 +296,14 @@ class AdvancedSettings extends Component { } }; + setIpfsGateway = ipfsGateway => { + const { PreferencesController } = Engine.context; + PreferencesController.setIpfsGateway(ipfsGateway); + }; + render = () => { - const { showHexData } = this.props; - const { resetModalVisible } = this.state; + const { showHexData, ipfsGateway } = this.props; + const { resetModalVisible, onlineIpfsGateways } = this.state; return ( @@ -330,6 +373,20 @@ class AdvancedSettings extends Component { + + + {strings('app_settings.ipfs_gateway')} + {strings('app_settings.ipfs_gateway_desc')} + + + + {strings('app_settings.show_hex_data')} {strings('app_settings.hex_desc')} @@ -363,6 +420,7 @@ class AdvancedSettings extends Component { } const mapStateToProps = state => ({ + ipfsGateway: state.engine.backgroundState.PreferencesController.ipfsGateway, showHexData: state.settings.showHexData, fullState: state }); diff --git a/app/components/Views/AdvancedSettings/index.test.js b/app/components/Views/AdvancedSettings/index.test.js index 1191f73c03e..b6d33c019da 100644 --- a/app/components/Views/AdvancedSettings/index.test.js +++ b/app/components/Views/AdvancedSettings/index.test.js @@ -8,7 +8,14 @@ describe('AdvancedSettings', () => { it('should render correctly', () => { const initialState = { - settings: { showHexData: true } + settings: { showHexData: true }, + engine: { + backgroundState: { + PreferencesController: { + ipfsGateway: 'https://ipfs.io/ipfs/' + } + } + } }; const wrapper = shallow( diff --git a/app/components/Views/Browser/index.js b/app/components/Views/Browser/index.js index 7bf5b28850d..c2fe3c7d635 100644 --- a/app/components/Views/Browser/index.js +++ b/app/components/Views/Browser/index.js @@ -261,6 +261,10 @@ export class Browser extends Component { * Protocol string to append to URLs that have none */ defaultProtocol: PropTypes.string, + /** + * A string that of the chosen ipfs gateway + */ + ipfsGateway: PropTypes.string, /** * Object containing the information for the current transaction */ @@ -690,6 +694,7 @@ export class Browser extends Component { const sanitizedURL = hasProtocol ? url : `${this.props.defaultProtocol}${url}`; const urlObj = new URL(sanitizedURL); const { hostname, query, pathname } = urlObj; + const { ipfsGateway } = this.props; let ipfsContent = null; let currentEnsName = null; @@ -702,7 +707,7 @@ export class Browser extends Component { const urlObj = new URL(sanitizedURL); currentEnsName = urlObj.hostname; ipfsHash = ipfsContent - .replace(this.state.ipfsGateway, '') + .replace(ipfsGateway, '') .split('/') .shift(); } @@ -756,6 +761,8 @@ export class Browser extends Component { async handleIpfsContent(fullUrl, { hostname, pathname, query }) { const { provider } = Engine.context.NetworkController; + const { ipfsGateway } = this.props; + let ipfsHash; try { ipfsHash = await resolveEnsToIpfsContentId({ provider, name: hostname }); @@ -766,7 +773,7 @@ export class Browser extends Component { return null; } - const gatewayUrl = `${this.state.ipfsGateway}${ipfsHash}${pathname || '/'}${query || ''}`; + const gatewayUrl = `${ipfsGateway}${ipfsHash}${pathname || '/'}${query || ''}`; try { const response = await fetch(gatewayUrl, { method: 'HEAD' }); @@ -1025,6 +1032,7 @@ export class Browser extends Component { } onPageChange = ({ url }) => { + const { ipfsGateway } = this.props; if ((this.goingBack && url === 'about:blank') || (this.initialUrl === url && url === 'about:blank')) { this.goBackToHomepage(); return; @@ -1044,7 +1052,7 @@ export class Browser extends Component { data.inputValue = url; } else if (url.search(`${AppConstants.IPFS_OVERRIDE_PARAM}=false`) === -1) { data.inputValue = url.replace( - `${this.state.ipfsGateway}${this.state.ipfsHash}/`, + `${ipfsGateway}${this.state.ipfsHash}/`, `https://${this.state.currentEnsName}/` ); } else if (this.isENSUrl(url)) { @@ -1661,6 +1669,7 @@ export class Browser extends Component { const mapStateToProps = state => ({ approvedHosts: state.privacy.approvedHosts, bookmarks: state.bookmarks, + ipfsGateway: state.engine.backgroundState.PreferencesController.ipfsGateway, networkType: state.engine.backgroundState.NetworkController.provider.type, network: state.engine.backgroundState.NetworkController.network, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, diff --git a/app/components/Views/Settings/__snapshots__/index.test.js.snap b/app/components/Views/Settings/__snapshots__/index.test.js.snap index 144dda22b51..6967715194e 100644 --- a/app/components/Views/Settings/__snapshots__/index.test.js.snap +++ b/app/components/Views/Settings/__snapshots__/index.test.js.snap @@ -17,7 +17,7 @@ exports[`Settings should render correctly 1`] = ` title="General" /> diff --git a/app/util/general.js b/app/util/general.js new file mode 100644 index 00000000000..76b8e636450 --- /dev/null +++ b/app/util/general.js @@ -0,0 +1,15 @@ +/** + * Fetch that fails after timeout + * + * @param url - Url to fetch + * @param options - Options to send with the request + * @param timeout - Timeout to fail request + * + * @returns - Promise resolving the request + */ +export default function timeoutFetch(url, options, timeout = 500) { + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)) + ]); +} diff --git a/app/util/ipfs-gateways.json b/app/util/ipfs-gateways.json new file mode 100644 index 00000000000..9711e5420b2 --- /dev/null +++ b/app/util/ipfs-gateways.json @@ -0,0 +1,41 @@ +[ + { "value": "https://ipfs.io/ipfs/", "key": 0, "label": "https://ipfs.io/ipfs/" }, + { "value": "https://gateway.ipfs.io/ipfs/", "key": 1, "label": "https://gateway.ipfs.io/ipfs/" }, + { "value": "https://ipfs.infura.io/ipfs/", "key": 2, "label": "https://ipfs.infura.io/ipfs/" }, + { "value": "https://rx14.co.uk/ipfs/", "key": 3, "label": "https://rx14.co.uk/ipfs/" }, + { "value": "https://ninetailed.ninja/ipfs/", "key": 4, "label": "https://ninetailed.ninja/ipfs/" }, + { "value": "https://upload.global/ipfs/", "key": 5, "label": "https://upload.global/ipfs/" }, + { "value": "https://ipfs.jes.xxx/ipfs/", "key": 6, "label": "https://ipfs.jes.xxx/ipfs/" }, + { "value": "https://catalunya.network/ipfs/", "key": 7, "label": "https://catalunya.network/ipfs/" }, + { "value": "https://siderus.io/ipfs/", "key": 8, "label": "https://siderus.io/ipfs/" }, + { "value": "https://ipfs.eternum.io/ipfs/", "key": 9, "label": "https://ipfs.eternum.io/ipfs/" }, + { "value": "https://hardbin.com/ipfs/", "key": 10, "label": "https://hardbin.com/ipfs/" }, + { "value": "https://ipfs.macholibre.org/ipfs/", "key": 11, "label": "https://ipfs.macholibre.org/ipfs/" }, + { "value": "https://ipfs.works/ipfs/", "key": 12, "label": "https://ipfs.works/ipfs/" }, + { "value": "https://ipfs.wa.hle.rs/ipfs/", "key": 13, "label": "https://ipfs.wa.hle.rs/ipfs/" }, + { "value": "https://api.wisdom.sh/ipfs/", "key": 14, "label": "https://api.wisdom.sh/ipfs/" }, + { "value": "https://gateway.blocksec.com/ipfs/", "key": 15, "label": "https://gateway.blocksec.com/ipfs/" }, + { "value": "https://ipfs.renehsz.com/ipfs/", "key": 16, "label": "https://ipfs.renehsz.com/ipfs/" }, + { "value": "https://cloudflare-ipfs.com/ipfs/", "key": 17, "label": "https://cloudflare-ipfs.com/ipfs/" }, + { "value": "https://ipns.co/", "key": 18, "label": "https://ipns.co/" }, + { "value": "https://ipfs.netw0rk.io/ipfs/", "key": 19, "label": "https://ipfs.netw0rk.io/ipfs/" }, + { "value": "https://gateway.swedneck.xyz/ipfs/", "key": 20, "label": "https://gateway.swedneck.xyz/ipfs/" }, + { "value": "https://ipfs.mrh.io/ipfs/", "key": 21, "label": "https://ipfs.mrh.io/ipfs/" }, + { + "value": "https://gateway.originprotocol.com/ipfs/", + "key": 22, + "label": "https://gateway.originprotocol.com/ipfs/" + }, + { "value": "https://ipfs.dapps.earth/ipfs/", "key": 23, "label": "https://ipfs.dapps.earth/ipfs/" }, + { "value": "https://gateway.pinata.cloud/ipfs/", "key": 24, "label": "https://gateway.pinata.cloud/ipfs/" }, + { "value": "https://ipfs.doolta.com/ipfs/", "key": 25, "label": "https://ipfs.doolta.com/ipfs/" }, + { "value": "https://ipfs.sloppyta.co/ipfs/", "key": 26, "label": "https://ipfs.sloppyta.co/ipfs/" }, + { "value": "https://ipfs.busy.org/ipfs/", "key": 27, "label": "https://ipfs.busy.org/ipfs/" }, + { "value": "https://ipfs.greyh.at/ipfs/", "key": 28, "label": "https://ipfs.greyh.at/ipfs/" }, + { "value": "https://gateway.serph.network/ipfs/", "key": 29, "label": "https://gateway.serph.network/ipfs/" }, + { "value": "https://jorropo.ovh/ipfs/", "key": 30, "label": "https://jorropo.ovh/ipfs/" }, + { "value": "https://ipfs.deo.moe/ipfs/", "key": 31, "label": "https://ipfs.deo.moe/ipfs/" }, + { "value": "https://gateway.temporal.cloud/ipfs/", "key": 32, "label": "https://gateway.temporal.cloud/ipfs/" }, + { "value": "https://ipfs.fooock.com/ipfs/", "key": 33, "label": "https://ipfs.fooock.com/ipfs/" }, + { "value": "https://cdn.cwinfo.net/ipfs/", "key": 34, "label": "https://cdn.cwinfo.net/ipfs/" } +] diff --git a/locales/en.json b/locales/en.json index 169a1afd5f5..a69efd20efe 100644 --- a/locales/en.json +++ b/locales/en.json @@ -184,6 +184,9 @@ "title": "Settings", "current_conversion": "Base Currency", "current_language": "Current Language", + "ipfs_gateway": "IPFS Gateway", + "ipfs_gateway_down": "Your current IPFS gateway is down", + "ipfs_gateway_desc": "Choose your preferred IPFS gateway.", "search_engine": "Search Engine", "new_RPC_URL": "New RPC Network", "state_logs": "State Logs", @@ -225,7 +228,7 @@ "general_title": "General", "general_desc": "Currency conversion, primary currency, language", "advanced_title": "Advanced", - "advanced_desc": "Access developer features, reset account, setup testnets, sync with extension, state logs and custom RPC", + "advanced_desc": "Access developer features, reset account, setup testnets, sync with extension, state logs, IPFS gateway and custom RPC", "security_title": "Security & Privacy", "security_desc": "Privacy settings, private key and wallet seed phrase", "info_title": "About MetaMask", diff --git a/locales/es.json b/locales/es.json index dbd5a66be66..70298f1ae01 100644 --- a/locales/es.json +++ b/locales/es.json @@ -185,6 +185,9 @@ "current_conversion": "Conversión actual", "current_language": "Lenguaje Actual", "new_RPC_URL": "Nueva URL del RPC", + "ipfs_gateway": "IPFS Gateway", + "ipfs_gateway_down": "IPFS gateway actual está desconectado", + "ipfs_gateway_desc": "Elige tu IPFS gateway preferida.", "state_logs": "Logs de Estado", "reveal_seed_words": "Revelar Palabras de Semilla", "reset_account": "Reiniciar Cuenta", @@ -224,7 +227,7 @@ "general_title": "General", "general_desc": "Conversion, Moneda principal, lenguaje", "advanced_title": "Avanzado", - "advanced_desc": "Accede a opciones de desarrolador, reiniciar cuenta, sincronizar con extension, logs de estado, agregar redes de prueba y RPC personalizados", + "advanced_desc": "Accede a opciones de desarrolador, reiniciar cuenta, sincronizar con extension, logs de estado, agregar redes de prueba, IPFS gateway y RPC personalizados", "security_title": "Seguridad y privacidad", "security_desc": "Opciones de privacidad y frase semilla de la billetera", "info_title": "Acerca de MetaMask",