Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Choose IPFS gateway #592

Merged
merged 12 commits into from
Apr 10, 2019
10 changes: 9 additions & 1 deletion app/components/UI/SelectComponent/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 '';
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,61 @@ exports[`AdvancedSettings should render correctly 1`] = `
</View>
</View>
</View>
<View
style={
Array [
Object {
"marginTop": 50,
},
]
}
>
<Text
style={
Object {
"color": "#000000",
"fontFamily": "Roboto",
"fontSize": 20,
"fontWeight": "400",
"lineHeight": 20,
}
}
>
IPFS Gateway
</Text>
<Text
style={
Object {
"color": "#4d4d4d",
"fontFamily": "Roboto",
"fontSize": 14,
"fontWeight": "400",
"lineHeight": 20,
"marginTop": 12,
}
}
>
Choose your preferred IPFS gateway.
</Text>
<View
style={
Object {
"borderColor": "#d2d8dd",
"borderRadius": 5,
"borderWidth": 2,
"marginTop": 16,
}
}
>
<SelectComponent
defaultValue="Your current IPFS gateway is down"
label="IPFS Gateway"
onValueChange={[Function]}
options={Array []}
selectedValue="https://ipfs.io/ipfs/"
/>
</View>
</View>
<View
style={
Object {
Expand Down
66 changes: 62 additions & 4 deletions app/components/Views/AdvancedSettings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import { Buffer } from 'buffer';
import Logger from '../../../util/Logger';
import { isprivateConnection } from '../../../util/networks';
import URL from 'url-parse';
import ipfsGateways from '../../../util/ipfs-gateways.json';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd fetch the list from github (https://raw.githubusercontent.com/ipfs/public-gateway-checker/master/gateways.json) so we get always the latest, assuming there should be more and more in the future

import SelectComponent from '../../UI/SelectComponent';
import timeoutFetch from '../../../util/general';

const HASH_TO_TEST = 'Qmaisz6NMhDB51cCvNWa1GMS7LU1pAxdF4Ld6Ft9kZEP2a';
const HASH_STRING = 'Hello from IPFS Gateway Checker';

const styles = StyleSheet.create({
wrapper: {
Expand Down Expand Up @@ -103,6 +109,12 @@ const styles = StyleSheet.create({
textAlign: 'center',
marginBottom: 20
},
picker: {
borderColor: colors.lightGray,
borderRadius: 5,
borderWidth: 2,
marginTop: 16
},
inner: {
paddingBottom: 48
},
Expand All @@ -120,6 +132,10 @@ const styles = StyleSheet.create({
*/
class AdvancedSettings extends Component {
static propTypes = {
/**
* A string that of the chosen ipfs gateway
*/
ipfsGateway: PropTypes.string,
/**
/* navigation object required to push new views
*/
Expand All @@ -145,10 +161,12 @@ class AdvancedSettings extends Component {
resetModalVisible: false,
rpcUrl: undefined,
warningRpcUrl: '',
inputWidth: Platform.OS === 'android' ? '99%' : undefined
inputWidth: Platform.OS === 'android' ? '99%' : undefined,
onlineIpfsGateways: []
};

componentDidMount = () => {
componentDidMount = async () => {
await this.handleAvailableIpfsGateways();
this.mounted = true;
// Workaround https://github.com/facebook/react-native/issues/9958
this.state.inputWidth &&
Expand All @@ -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 });
};
Expand Down Expand Up @@ -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 (
<SafeAreaView style={baseStyles.flexGrow}>
<KeyboardAwareScrollView style={styles.wrapper} resetScrollToCoords={{ x: 0, y: 0 }}>
Expand Down Expand Up @@ -330,6 +373,20 @@ class AdvancedSettings extends Component {
</View>
</View>
</View>

<View style={[styles.setting]}>
<Text style={styles.title}>{strings('app_settings.ipfs_gateway')}</Text>
<Text style={styles.desc}>{strings('app_settings.ipfs_gateway_desc')}</Text>
<View style={styles.picker}>
<SelectComponent
selectedValue={ipfsGateway}
defaultValue={strings('app_settings.ipfs_gateway_down')}
onValueChange={this.setIpfsGateway}
label={strings('app_settings.ipfs_gateway')}
options={onlineIpfsGateways}
/>
</View>
</View>
<View style={styles.setting}>
<Text style={styles.title}>{strings('app_settings.show_hex_data')}</Text>
<Text style={styles.desc}>{strings('app_settings.hex_desc')}</Text>
Expand Down Expand Up @@ -363,6 +420,7 @@ class AdvancedSettings extends Component {
}

const mapStateToProps = state => ({
ipfsGateway: state.engine.backgroundState.PreferencesController.ipfsGateway,
showHexData: state.settings.showHexData,
fullState: state
});
Expand Down
9 changes: 8 additions & 1 deletion app/components/Views/AdvancedSettings/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 12 additions & 3 deletions app/components/Views/Browser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
Expand Down Expand Up @@ -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 });
Expand All @@ -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' });
Expand Down Expand Up @@ -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;
Expand All @@ -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)) {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ exports[`Settings should render correctly 1`] = `
title="General"
/>
<SettingsDrawer
description="Access developer features, reset account, setup testnets, sync with extension, state logs and custom RPC"
description="Access developer features, reset account, setup testnets, sync with extension, state logs, IPFS gateway and custom RPC"
onPress={[Function]}
title="Advanced"
/>
Expand Down
15 changes: 15 additions & 0 deletions app/util/general.js
Original file line number Diff line number Diff line change
@@ -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))
]);
}
41 changes: 41 additions & 0 deletions app/util/ipfs-gateways.json
Original file line number Diff line number Diff line change
@@ -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/" }
]
5 changes: 4 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Loading