1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Add a new fiat onboarding option via MoonPay (#13934)

This commit is contained in:
Daniel 2022-03-21 10:26:52 +01:00 committed by GitHub
parent 64d35458b0
commit 95f830438a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 261 additions and 40 deletions

View File

@ -411,6 +411,13 @@
"buy": { "buy": {
"message": "Buy" "message": "Buy"
}, },
"buyCryptoWithMoonPay": {
"message": "Buy $1 with MoonPay",
"description": "$1 represents the cypto symbol to be purchased"
},
"buyCryptoWithMoonPayDescription": {
"message": "MoonPay supports popular payment methods, including Visa, Mastercard, Apple / Google / Samsung Pay, and bank transfers in 146+ countries. Tokens deposit into your MetaMask account."
},
"buyCryptoWithTransak": { "buyCryptoWithTransak": {
"message": "Buy $1 with Transak", "message": "Buy $1 with Transak",
"description": "$1 represents the cypto symbol to be purchased" "description": "$1 represents the cypto symbol to be purchased"
@ -617,6 +624,9 @@
"continue": { "continue": {
"message": "Continue" "message": "Continue"
}, },
"continueToMoonPay": {
"message": "Continue to MoonPay"
},
"continueToTransak": { "continueToTransak": {
"message": "Continue to Transak" "message": "Continue to Transak"
}, },

View File

@ -1 +1,2 @@
export const TRANSAK_API_KEY = '25ac1309-a49b-4411-b20e-5e56c61a5b1c'; // It's a public key, which will be included in a URL for Transak. export const TRANSAK_API_KEY = '25ac1309-a49b-4411-b20e-5e56c61a5b1c'; // It's a public key, which will be included in a URL for Transak.
export const MOONPAY_API_KEY = 'pk_live_WbCpe6PxSIcGPCSd6lKCbJNRht7uy'; // Publishable key.

View File

@ -12,7 +12,7 @@ import {
} from '../../../shared/constants/network'; } from '../../../shared/constants/network';
import { SECOND } from '../../../shared/constants/time'; import { SECOND } from '../../../shared/constants/time';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
import { TRANSAK_API_KEY } from '../constants/on-ramp'; import { TRANSAK_API_KEY, MOONPAY_API_KEY } from '../constants/on-ramp';
const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
@ -67,6 +67,47 @@ const createTransakUrl = (walletAddress, chainId) => {
return `https://global.transak.com/?${queryParams}`; return `https://global.transak.com/?${queryParams}`;
}; };
/**
* Create a MoonPay Checkout URL.
*
* @param {string} walletAddress - Destination address
* @param {string} chainId - Current chain ID
* @returns String
*/
const createMoonPayUrl = async (walletAddress, chainId) => {
const {
moonPay: { defaultCurrencyCode, showOnlyCurrencies } = {},
} = BUYABLE_CHAINS_MAP[chainId];
const moonPayQueryParams = new URLSearchParams({
apiKey: MOONPAY_API_KEY,
walletAddress,
defaultCurrencyCode,
showOnlyCurrencies,
});
const queryParams = new URLSearchParams({
url: `https://buy.moonpay.com?${moonPayQueryParams}`,
context: 'extension',
});
const moonPaySignUrl = `${SWAPS_API_V2_BASE_URL}/moonpaySign/?${queryParams}`;
try {
const response = await fetchWithTimeout(moonPaySignUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
const parsedResponse = await response.json();
if (response.ok && parsedResponse.url) {
return parsedResponse.url;
}
log.warn('Failed to create a MoonPay purchase URL', parsedResponse);
} catch (err) {
log.warn('Failed to create a MoonPay purchase URL', err);
}
return '';
};
/** /**
* Gives the caller a url at which the user can acquire eth, depending on the network they are in * Gives the caller a url at which the user can acquire eth, depending on the network they are in
* *
@ -89,6 +130,8 @@ export default async function getBuyUrl({ chainId, address, service }) {
return await createWyrePurchaseUrl(address); return await createWyrePurchaseUrl(address);
case 'transak': case 'transak':
return createTransakUrl(address, chainId); return createTransakUrl(address, chainId);
case 'moonpay':
return createMoonPayUrl(address, chainId);
case 'metamask-faucet': case 'metamask-faucet':
return 'https://faucet.metamask.io/'; return 'https://faucet.metamask.io/';
case 'rinkeby-faucet': case 'rinkeby-faucet':

View File

@ -9,7 +9,7 @@ import {
ETH_SYMBOL, ETH_SYMBOL,
BUYABLE_CHAINS_MAP, BUYABLE_CHAINS_MAP,
} from '../../../shared/constants/network'; } from '../../../shared/constants/network';
import { TRANSAK_API_KEY } from '../constants/on-ramp'; import { TRANSAK_API_KEY, MOONPAY_API_KEY } from '../constants/on-ramp';
import { SWAPS_API_V2_BASE_URL } from '../../../shared/constants/swaps'; import { SWAPS_API_V2_BASE_URL } from '../../../shared/constants/swaps';
import getBuyUrl from './buy-url'; import getBuyUrl from './buy-url';
@ -114,4 +114,35 @@ describe('buy-url', () => {
const kovanUrl = await getBuyUrl(KOVAN); const kovanUrl = await getBuyUrl(KOVAN);
expect(kovanUrl).toStrictEqual('https://github.com/kovan-testnet/faucet'); expect(kovanUrl).toStrictEqual('https://github.com/kovan-testnet/faucet');
}); });
it('returns a MoonPay url with a prefilled wallet address for the Ethereum network', async () => {
const {
moonPay: { defaultCurrencyCode, showOnlyCurrencies } = {},
} = BUYABLE_CHAINS_MAP[MAINNET.chainId];
const moonPayQueryParams = new URLSearchParams({
apiKey: MOONPAY_API_KEY,
walletAddress: MAINNET.address,
defaultCurrencyCode,
showOnlyCurrencies,
});
const queryParams = new URLSearchParams({
url: `https://buy.moonpay.com?${moonPayQueryParams}`,
context: 'extension',
});
nock(SWAPS_API_V2_BASE_URL)
.get(`/moonpaySign/?${queryParams}`)
.reply(200, {
url: `https://buy.moonpay.com/?apiKey=${MOONPAY_API_KEY}&walletAddress=${MAINNET.address}&defaultCurrencyCode=${defaultCurrencyCode}&showOnlyCurrencies=eth%2Cusdt%2Cusdc%2Cdai&signature=laefTlgkESEc2hv8AZEH9F25VjLEJUADY27D6MccE54%3D`,
});
const moonPayUrl = await getBuyUrl({ ...MAINNET, service: 'moonpay' });
expect(moonPayUrl).toStrictEqual(
`https://buy.moonpay.com/?apiKey=${MOONPAY_API_KEY}&walletAddress=${MAINNET.address}&defaultCurrencyCode=${defaultCurrencyCode}&showOnlyCurrencies=eth%2Cusdt%2Cusdc%2Cdai&signature=laefTlgkESEc2hv8AZEH9F25VjLEJUADY27D6MccE54%3D`,
);
nock.cleanAll();
});
it('returns an empty string if generating a MoonPay url fails', async () => {
const moonPayUrl = await getBuyUrl({ ...MAINNET, service: 'moonpay' });
expect(moonPayUrl).toStrictEqual('');
});
}); });

View File

@ -185,11 +185,16 @@ export const IPFS_DEFAULT_GATEWAY_URL = 'dweb.link';
// The first item in transakCurrencies must be the // The first item in transakCurrencies must be the
// default crypto currency for the network // default crypto currency for the network
const BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME = 'ethereum'; const BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME = 'ethereum';
export const BUYABLE_CHAINS_MAP = { export const BUYABLE_CHAINS_MAP = {
[MAINNET_CHAIN_ID]: { [MAINNET_CHAIN_ID]: {
nativeCurrency: ETH_SYMBOL, nativeCurrency: ETH_SYMBOL,
network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME, network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME,
transakCurrencies: [ETH_SYMBOL, 'USDT', 'USDC', 'DAI'], transakCurrencies: [ETH_SYMBOL, 'USDT', 'USDC', 'DAI'],
moonPay: {
defaultCurrencyCode: 'eth',
showOnlyCurrencies: 'eth,usdt,usdc,dai',
},
}, },
[ROPSTEN_CHAIN_ID]: { [ROPSTEN_CHAIN_ID]: {
nativeCurrency: ETH_SYMBOL, nativeCurrency: ETH_SYMBOL,
@ -211,16 +216,28 @@ export const BUYABLE_CHAINS_MAP = {
nativeCurrency: BNB_SYMBOL, nativeCurrency: BNB_SYMBOL,
network: 'bsc', network: 'bsc',
transakCurrencies: [BNB_SYMBOL, 'BUSD'], transakCurrencies: [BNB_SYMBOL, 'BUSD'],
moonPay: {
defaultCurrencyCode: 'bnb_bsc',
showOnlyCurrencies: 'bnb_bsc,busd_bsc',
},
}, },
[POLYGON_CHAIN_ID]: { [POLYGON_CHAIN_ID]: {
nativeCurrency: MATIC_SYMBOL, nativeCurrency: MATIC_SYMBOL,
network: 'polygon', network: 'polygon',
transakCurrencies: [MATIC_SYMBOL, 'USDT', 'USDC', 'DAI'], transakCurrencies: [MATIC_SYMBOL, 'USDT', 'USDC', 'DAI'],
moonPay: {
defaultCurrencyCode: 'matic_polygon',
showOnlyCurrencies: 'matic_polygon,usdc_polygon',
},
}, },
[AVALANCHE_CHAIN_ID]: { [AVALANCHE_CHAIN_ID]: {
nativeCurrency: AVALANCHE_SYMBOL, nativeCurrency: AVALANCHE_SYMBOL,
network: 'avaxcchain', network: 'avaxcchain',
transakCurrencies: [AVALANCHE_SYMBOL], transakCurrencies: [AVALANCHE_SYMBOL],
moonPay: {
defaultCurrencyCode: 'avax_cchain',
showOnlyCurrencies: 'avax_cchain',
},
}, },
[FANTOM_CHAIN_ID]: { [FANTOM_CHAIN_ID]: {
nativeCurrency: FANTOM_SYMBOL, nativeCurrency: FANTOM_SYMBOL,
@ -231,5 +248,9 @@ export const BUYABLE_CHAINS_MAP = {
nativeCurrency: CELO_SYMBOL, nativeCurrency: CELO_SYMBOL,
network: 'celo', network: 'celo',
transakCurrencies: [CELO_SYMBOL], transakCurrencies: [CELO_SYMBOL],
moonPay: {
defaultCurrencyCode: 'celo',
showOnlyCurrencies: 'celo',
},
}, },
}; };

View File

@ -101,7 +101,7 @@ export const WAVAX_CONTRACT_ADDRESS =
const SWAPS_TESTNET_CHAIN_ID = '0x539'; const SWAPS_TESTNET_CHAIN_ID = '0x539';
export const SWAPS_API_V2_BASE_URL = 'https://api2.metaswap.codefi.network'; export const SWAPS_API_V2_BASE_URL = 'https://swap.metaswap.codefi.network';
export const SWAPS_DEV_API_V2_BASE_URL = export const SWAPS_DEV_API_V2_BASE_URL =
'https://swap.metaswap-dev.codefi.network'; 'https://swap.metaswap-dev.codefi.network';
export const GAS_API_BASE_URL = 'https://gas-api.metaswap.codefi.network'; export const GAS_API_BASE_URL = 'https://gas-api.metaswap.codefi.network';

View File

@ -48,7 +48,7 @@ async function setupFetchMocking(driver) {
return { json: async () => clone(mockResponses.gasPricesBasic) }; return { json: async () => clone(mockResponses.gasPricesBasic) };
} else if (url.match(/chromeextensionmm/u)) { } else if (url.match(/chromeextensionmm/u)) {
return { json: async () => clone(mockResponses.metametrics) }; return { json: async () => clone(mockResponses.metametrics) };
} else if (url.match(/^https:\/\/(api2\.metaswap\.codefi\.network)/u)) { } else if (url.match(/^https:\/\/(swap\.metaswap\.codefi\.network)/u)) {
if (url.match(/featureFlags$/u)) { if (url.match(/featureFlags$/u)) {
return { json: async () => clone(mockResponses.swaps.featureFlags) }; return { json: async () => clone(mockResponses.swaps.featureFlags) };
} }

View File

@ -1,2 +1,2 @@
export const METASWAP_BASE_URL = 'https://api2.metaswap.codefi.network'; export const METASWAP_BASE_URL = 'https://swap.metaswap.codefi.network';
export const GAS_API_URL = 'https://gas-api.metaswap.codefi.network'; export const GAS_API_URL = 'https://gas-api.metaswap.codefi.network';

View File

@ -5,6 +5,7 @@ import {
BUYABLE_CHAINS_MAP, BUYABLE_CHAINS_MAP,
} from '../../../../../shared/constants/network'; } from '../../../../../shared/constants/network';
import Button from '../../../ui/button'; import Button from '../../../ui/button';
import LogoMoonPay from '../../../ui/logo/logo-moonpay';
export default class DepositEtherModal extends Component { export default class DepositEtherModal extends Component {
static contextTypes = { static contextTypes = {
@ -17,8 +18,10 @@ export default class DepositEtherModal extends Component {
isTestnet: PropTypes.bool.isRequired, isTestnet: PropTypes.bool.isRequired,
isMainnet: PropTypes.bool.isRequired, isMainnet: PropTypes.bool.isRequired,
isBuyableTransakChain: PropTypes.bool.isRequired, isBuyableTransakChain: PropTypes.bool.isRequired,
isBuyableMoonPayChain: PropTypes.bool.isRequired,
toWyre: PropTypes.func.isRequired, toWyre: PropTypes.func.isRequired,
toTransak: PropTypes.func.isRequired, toTransak: PropTypes.func.isRequired,
toMoonPay: PropTypes.func.isRequired,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
toFaucet: PropTypes.func.isRequired, toFaucet: PropTypes.func.isRequired,
hideWarning: PropTypes.func.isRequired, hideWarning: PropTypes.func.isRequired,
@ -93,11 +96,13 @@ export default class DepositEtherModal extends Component {
chainId, chainId,
toWyre, toWyre,
toTransak, toTransak,
toMoonPay,
address, address,
toFaucet, toFaucet,
isTestnet, isTestnet,
isMainnet, isMainnet,
isBuyableTransakChain, isBuyableTransakChain,
isBuyableMoonPayChain,
} = this.props; } = this.props;
const { t } = this.context; const { t } = this.context;
const networkName = NETWORK_TO_NAME_MAP[chainId]; const networkName = NETWORK_TO_NAME_MAP[chainId];
@ -122,31 +127,6 @@ export default class DepositEtherModal extends Component {
</div> </div>
<div className="page-container__content"> <div className="page-container__content">
<div className="deposit-ether-modal__buy-rows"> <div className="deposit-ether-modal__buy-rows">
{this.renderRow({
logo: (
<div
className="deposit-ether-modal__logo"
style={{
backgroundImage: "url('./images/wyre.svg')",
height: '40px',
}}
/>
),
title: t('buyWithWyre'),
text: t('buyWithWyreDescription'),
buttonLabel: t('continueToWyre'),
onButtonClick: () => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Deposit Ether',
name: 'Click buy Ether via Wyre',
},
});
toWyre(address);
},
hide: !isMainnet,
})}
{this.renderRow({ {this.renderRow({
logo: ( logo: (
<div <div
@ -172,6 +152,50 @@ export default class DepositEtherModal extends Component {
}, },
hide: !isBuyableTransakChain, hide: !isBuyableTransakChain,
})} })}
{this.renderRow({
logo: (
<LogoMoonPay className="deposit-ether-modal__logo--moonpay" />
),
title: t('buyCryptoWithMoonPay', [symbol]),
text: t('buyCryptoWithMoonPayDescription', [symbol]),
buttonLabel: t('continueToMoonPay'),
onButtonClick: () => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Deposit tokens',
name: 'Click buy tokens via MoonPay',
},
});
toMoonPay(address, chainId);
},
hide: !isBuyableMoonPayChain,
})}
{this.renderRow({
logo: (
<div
className="deposit-ether-modal__logo"
style={{
backgroundImage: "url('./images/wyre.svg')",
height: '40px',
}}
/>
),
title: t('buyWithWyre'),
text: t('buyWithWyreDescription'),
buttonLabel: t('continueToWyre'),
onButtonClick: () => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Deposit Ether',
name: 'Click buy Ether via Wyre',
},
});
toWyre(address);
},
hide: !isMainnet,
})}
{this.renderRow({ {this.renderRow({
logo: ( logo: (
<img <img

View File

@ -11,6 +11,7 @@ import {
getCurrentChainId, getCurrentChainId,
getSelectedAddress, getSelectedAddress,
getIsBuyableTransakChain, getIsBuyableTransakChain,
getIsBuyableMoonPayChain,
} from '../../../../selectors/selectors'; } from '../../../../selectors/selectors';
import DepositEtherModal from './deposit-ether-modal.component'; import DepositEtherModal from './deposit-ether-modal.component';
@ -21,6 +22,7 @@ function mapStateToProps(state) {
isMainnet: getIsMainnet(state), isMainnet: getIsMainnet(state),
address: getSelectedAddress(state), address: getSelectedAddress(state),
isBuyableTransakChain: getIsBuyableTransakChain(state), isBuyableTransakChain: getIsBuyableTransakChain(state),
isBuyableMoonPayChain: getIsBuyableMoonPayChain(state),
}; };
} }
@ -32,6 +34,9 @@ function mapDispatchToProps(dispatch) {
toTransak: (address, chainId) => { toTransak: (address, chainId) => {
dispatch(buyEth({ service: 'transak', address, chainId })); dispatch(buyEth({ service: 'transak', address, chainId }));
}, },
toMoonPay: (address, chainId) => {
dispatch(buyEth({ service: 'moonpay', address, chainId }));
},
hideModal: () => { hideModal: () => {
dispatch(hideModal()); dispatch(hideModal());
}, },

View File

@ -26,6 +26,10 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
&--moonpay {
height: 30px;
}
} }
&__buy-row { &__buy-row {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,11 @@
import React from 'react';
import { renderWithProvider } from '../../../../test/jest';
import LogoMoonPay from './logo-moonpay';
describe('LogoMoonPay', () => {
it('renders the LogoMoonPay component', () => {
const { container } = renderWithProvider(<LogoMoonPay />);
expect(container).toMatchSnapshot();
});
});

View File

@ -34,7 +34,7 @@ describe('Ducks - Swaps', () => {
describe('fetchSwapsLivenessAndFeatureFlags', () => { describe('fetchSwapsLivenessAndFeatureFlags', () => {
const cleanFeatureFlagApiCache = () => { const cleanFeatureFlagApiCache = () => {
setStorageItem( setStorageItem(
'cachedFetch:https://api2.metaswap.codefi.network/featureFlags', 'cachedFetch:https://swap.metaswap.codefi.network/featureFlags',
null, null,
); );
}; };
@ -47,7 +47,7 @@ describe('Ducks - Swaps', () => {
featureFlagsResponse, featureFlagsResponse,
replyWithError = false, replyWithError = false,
} = {}) => { } = {}) => {
const apiNock = nock('https://api2.metaswap.codefi.network').get( const apiNock = nock('https://swap.metaswap.codefi.network').get(
'/featureFlags', '/featureFlags',
); );
if (replyWithError) { if (replyWithError) {

View File

@ -94,7 +94,7 @@ describe('Swaps Util', () => {
}, },
}; };
it('should fetch trade info on prod', async () => { it('should fetch trade info on prod', async () => {
nock('https://api2.metaswap.codefi.network') nock('https://swap.metaswap.codefi.network')
.get('/networks/1/trades') .get('/networks/1/trades')
.query(true) .query(true)
.reply(200, MOCK_TRADE_RESPONSE_2); .reply(200, MOCK_TRADE_RESPONSE_2);
@ -120,7 +120,7 @@ describe('Swaps Util', () => {
describe('fetchTokens', () => { describe('fetchTokens', () => {
beforeAll(() => { beforeAll(() => {
nock('https://api2.metaswap.codefi.network') nock('https://swap.metaswap.codefi.network')
.persist() .persist()
.get('/networks/1/tokens') .get('/networks/1/tokens')
.reply(200, TOKENS); .reply(200, TOKENS);
@ -139,7 +139,7 @@ describe('Swaps Util', () => {
describe('fetchAggregatorMetadata', () => { describe('fetchAggregatorMetadata', () => {
beforeAll(() => { beforeAll(() => {
nock('https://api2.metaswap.codefi.network') nock('https://swap.metaswap.codefi.network')
.persist() .persist()
.get('/networks/1/aggregatorMetadata') .get('/networks/1/aggregatorMetadata')
.reply(200, AGGREGATOR_METADATA); .reply(200, AGGREGATOR_METADATA);
@ -158,7 +158,7 @@ describe('Swaps Util', () => {
describe('fetchTopAssets', () => { describe('fetchTopAssets', () => {
beforeAll(() => { beforeAll(() => {
nock('https://api2.metaswap.codefi.network') nock('https://swap.metaswap.codefi.network')
.persist() .persist()
.get('/networks/1/topAssets') .get('/networks/1/topAssets')
.reply(200, TOP_ASSETS); .reply(200, TOP_ASSETS);

View File

@ -672,6 +672,11 @@ export function getIsBuyableTransakChain(state) {
return Boolean(BUYABLE_CHAINS_MAP?.[chainId]?.transakCurrencies); return Boolean(BUYABLE_CHAINS_MAP?.[chainId]?.transakCurrencies);
} }
export function getIsBuyableMoonPayChain(state) {
const chainId = getCurrentChainId(state);
return Boolean(BUYABLE_CHAINS_MAP?.[chainId]?.moonPay);
}
export function getNativeCurrencyImage(state) { export function getNativeCurrencyImage(state) {
const nativeCurrency = getNativeCurrency(state).toUpperCase(); const nativeCurrency = getNativeCurrency(state).toUpperCase();
return NATIVE_CURRENCY_TOKEN_IMAGE_MAP[nativeCurrency]; return NATIVE_CURRENCY_TOKEN_IMAGE_MAP[nativeCurrency];

View File

@ -2102,10 +2102,12 @@ export function showSendTokenPage() {
export function buyEth(opts) { export function buyEth(opts) {
return async (dispatch) => { return async (dispatch) => {
const url = await getBuyUrl(opts); const url = await getBuyUrl(opts);
if (url) {
global.platform.openTab({ url }); global.platform.openTab({ url });
dispatch({ dispatch({
type: actionConstants.BUY_ETH, type: actionConstants.BUY_ETH,
}); });
}
}; };
} }