1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 09:23:21 +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": {
"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": {
"message": "Buy $1 with Transak",
"description": "$1 represents the cypto symbol to be purchased"
@ -617,6 +624,9 @@
"continue": {
"message": "Continue"
},
"continueToMoonPay": {
"message": "Continue to MoonPay"
},
"continueToTransak": {
"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 MOONPAY_API_KEY = 'pk_live_WbCpe6PxSIcGPCSd6lKCbJNRht7uy'; // Publishable key.

View File

@ -12,7 +12,7 @@ import {
} from '../../../shared/constants/network';
import { SECOND } from '../../../shared/constants/time';
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);
@ -67,6 +67,47 @@ const createTransakUrl = (walletAddress, chainId) => {
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
*
@ -89,6 +130,8 @@ export default async function getBuyUrl({ chainId, address, service }) {
return await createWyrePurchaseUrl(address);
case 'transak':
return createTransakUrl(address, chainId);
case 'moonpay':
return createMoonPayUrl(address, chainId);
case 'metamask-faucet':
return 'https://faucet.metamask.io/';
case 'rinkeby-faucet':

View File

@ -9,7 +9,7 @@ import {
ETH_SYMBOL,
BUYABLE_CHAINS_MAP,
} 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 getBuyUrl from './buy-url';
@ -114,4 +114,35 @@ describe('buy-url', () => {
const kovanUrl = await getBuyUrl(KOVAN);
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
// default crypto currency for the network
const BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME = 'ethereum';
export const BUYABLE_CHAINS_MAP = {
[MAINNET_CHAIN_ID]: {
nativeCurrency: ETH_SYMBOL,
network: BUYABLE_CHAIN_ETHEREUM_NETWORK_NAME,
transakCurrencies: [ETH_SYMBOL, 'USDT', 'USDC', 'DAI'],
moonPay: {
defaultCurrencyCode: 'eth',
showOnlyCurrencies: 'eth,usdt,usdc,dai',
},
},
[ROPSTEN_CHAIN_ID]: {
nativeCurrency: ETH_SYMBOL,
@ -211,16 +216,28 @@ export const BUYABLE_CHAINS_MAP = {
nativeCurrency: BNB_SYMBOL,
network: 'bsc',
transakCurrencies: [BNB_SYMBOL, 'BUSD'],
moonPay: {
defaultCurrencyCode: 'bnb_bsc',
showOnlyCurrencies: 'bnb_bsc,busd_bsc',
},
},
[POLYGON_CHAIN_ID]: {
nativeCurrency: MATIC_SYMBOL,
network: 'polygon',
transakCurrencies: [MATIC_SYMBOL, 'USDT', 'USDC', 'DAI'],
moonPay: {
defaultCurrencyCode: 'matic_polygon',
showOnlyCurrencies: 'matic_polygon,usdc_polygon',
},
},
[AVALANCHE_CHAIN_ID]: {
nativeCurrency: AVALANCHE_SYMBOL,
network: 'avaxcchain',
transakCurrencies: [AVALANCHE_SYMBOL],
moonPay: {
defaultCurrencyCode: 'avax_cchain',
showOnlyCurrencies: 'avax_cchain',
},
},
[FANTOM_CHAIN_ID]: {
nativeCurrency: FANTOM_SYMBOL,
@ -231,5 +248,9 @@ export const BUYABLE_CHAINS_MAP = {
nativeCurrency: CELO_SYMBOL,
network: 'celo',
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';
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 =
'https://swap.metaswap-dev.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) };
} else if (url.match(/chromeextensionmm/u)) {
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)) {
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';

View File

@ -5,6 +5,7 @@ import {
BUYABLE_CHAINS_MAP,
} from '../../../../../shared/constants/network';
import Button from '../../../ui/button';
import LogoMoonPay from '../../../ui/logo/logo-moonpay';
export default class DepositEtherModal extends Component {
static contextTypes = {
@ -17,8 +18,10 @@ export default class DepositEtherModal extends Component {
isTestnet: PropTypes.bool.isRequired,
isMainnet: PropTypes.bool.isRequired,
isBuyableTransakChain: PropTypes.bool.isRequired,
isBuyableMoonPayChain: PropTypes.bool.isRequired,
toWyre: PropTypes.func.isRequired,
toTransak: PropTypes.func.isRequired,
toMoonPay: PropTypes.func.isRequired,
address: PropTypes.string.isRequired,
toFaucet: PropTypes.func.isRequired,
hideWarning: PropTypes.func.isRequired,
@ -93,11 +96,13 @@ export default class DepositEtherModal extends Component {
chainId,
toWyre,
toTransak,
toMoonPay,
address,
toFaucet,
isTestnet,
isMainnet,
isBuyableTransakChain,
isBuyableMoonPayChain,
} = this.props;
const { t } = this.context;
const networkName = NETWORK_TO_NAME_MAP[chainId];
@ -122,31 +127,6 @@ export default class DepositEtherModal extends Component {
</div>
<div className="page-container__content">
<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({
logo: (
<div
@ -172,6 +152,50 @@ export default class DepositEtherModal extends Component {
},
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({
logo: (
<img

View File

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

View File

@ -26,6 +26,10 @@
display: flex;
justify-content: center;
align-items: center;
&--moonpay {
height: 30px;
}
}
&__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', () => {
const cleanFeatureFlagApiCache = () => {
setStorageItem(
'cachedFetch:https://api2.metaswap.codefi.network/featureFlags',
'cachedFetch:https://swap.metaswap.codefi.network/featureFlags',
null,
);
};
@ -47,7 +47,7 @@ describe('Ducks - Swaps', () => {
featureFlagsResponse,
replyWithError = false,
} = {}) => {
const apiNock = nock('https://api2.metaswap.codefi.network').get(
const apiNock = nock('https://swap.metaswap.codefi.network').get(
'/featureFlags',
);
if (replyWithError) {

View File

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

View File

@ -672,6 +672,11 @@ export function getIsBuyableTransakChain(state) {
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) {
const nativeCurrency = getNativeCurrency(state).toUpperCase();
return NATIVE_CURRENCY_TOKEN_IMAGE_MAP[nativeCurrency];

View File

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