Ability to buy tokens with Moonpay (#15924)

* Ability to buy tokens with Moonpay

* fix for test cases failing

* updated description for MoonPayChainSettings type

* removed test results
feature/default_network_editable
Nicolas Ferro 2 years ago committed by GitHub
parent d520fc57cb
commit 3f801e377d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 9
      app/scripts/lib/buy-url.js
  3. 4
      app/scripts/lib/buy-url.test.js
  4. 66
      shared/constants/network.ts
  5. 14
      ui/components/app/deposit-popover/deposit-popover.js
  6. 7
      ui/components/app/wallet-overview/token-overview.js
  7. 19
      ui/helpers/utils/moonpay.js
  8. 33
      ui/helpers/utils/moonpay.test.js
  9. 11
      ui/selectors/selectors.js

3
.gitignore vendored

@ -53,3 +53,6 @@ notes.txt
# TypeScript # TypeScript
tsout/ tsout/
# Test results
test-results/

@ -11,6 +11,7 @@ import {
MOONPAY_API_KEY, MOONPAY_API_KEY,
COINBASEPAY_API_KEY, COINBASEPAY_API_KEY,
} from '../constants/on-ramp'; } from '../constants/on-ramp';
import { formatMoonpaySymbol } from '../../../ui/helpers/utils/moonpay';
const fetchWithTimeout = getFetchWithTimeout(); const fetchWithTimeout = getFetchWithTimeout();
@ -75,15 +76,17 @@ const createTransakUrl = (walletAddress, chainId, symbol) => {
* *
* @param {string} walletAddress - Destination address * @param {string} walletAddress - Destination address
* @param {string} chainId - Current chain ID * @param {string} chainId - Current chain ID
* @param {string|undefined} symbol - Token symbol to buy
* @returns String * @returns String
*/ */
const createMoonPayUrl = async (walletAddress, chainId) => { const createMoonPayUrl = async (walletAddress, chainId, symbol) => {
const { moonPay: { defaultCurrencyCode, showOnlyCurrencies } = {} } = const { moonPay: { defaultCurrencyCode, showOnlyCurrencies } = {} } =
BUYABLE_CHAINS_MAP[chainId]; BUYABLE_CHAINS_MAP[chainId];
const moonPayQueryParams = new URLSearchParams({ const moonPayQueryParams = new URLSearchParams({
apiKey: MOONPAY_API_KEY, apiKey: MOONPAY_API_KEY,
walletAddress, walletAddress,
defaultCurrencyCode, defaultCurrencyCode:
formatMoonpaySymbol(symbol, chainId) || defaultCurrencyCode,
showOnlyCurrencies, showOnlyCurrencies,
}); });
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
@ -159,7 +162,7 @@ export default async function getBuyUrl({ chainId, address, service, symbol }) {
case 'transak': case 'transak':
return createTransakUrl(address, chainId, symbol); return createTransakUrl(address, chainId, symbol);
case 'moonpay': case 'moonpay':
return createMoonPayUrl(address, chainId); return createMoonPayUrl(address, chainId, symbol);
case 'coinbase': case 'coinbase':
return createCoinbasePayUrl(address, chainId, symbol); return createCoinbasePayUrl(address, chainId, symbol);
case 'metamask-faucet': case 'metamask-faucet':

@ -117,11 +117,11 @@ describe('buy-url', () => {
nock(SWAPS_API_V2_BASE_URL) nock(SWAPS_API_V2_BASE_URL)
.get(`/moonpaySign/?${queryParams}`) .get(`/moonpaySign/?${queryParams}`)
.reply(200, { .reply(200, {
url: `https://buy.moonpay.com/?apiKey=${MOONPAY_API_KEY}&walletAddress=${MAINNET.address}&defaultCurrencyCode=${defaultCurrencyCode}&showOnlyCurrencies=eth%2Cusdt%2Cusdc%2Cdai&signature=laefTlgkESEc2hv8AZEH9F25VjLEJUADY27D6MccE54%3D`, url: `https://buy.moonpay.com/?apiKey=${MOONPAY_API_KEY}&walletAddress=${MAINNET.address}&defaultCurrencyCode=${defaultCurrencyCode}&showOnlyCurrencies=${showOnlyCurrencies}&signature=laefTlgkESEc2hv8AZEH9F25VjLEJUADY27D6MccE54%3D`,
}); });
const moonPayUrl = await getBuyUrl({ ...MAINNET, service: 'moonpay' }); const moonPayUrl = await getBuyUrl({ ...MAINNET, service: 'moonpay' });
expect(moonPayUrl).toStrictEqual( expect(moonPayUrl).toStrictEqual(
`https://buy.moonpay.com/?apiKey=${MOONPAY_API_KEY}&walletAddress=${MAINNET.address}&defaultCurrencyCode=${defaultCurrencyCode}&showOnlyCurrencies=eth%2Cusdt%2Cusdc%2Cdai&signature=laefTlgkESEc2hv8AZEH9F25VjLEJUADY27D6MccE54%3D`, `https://buy.moonpay.com/?apiKey=${MOONPAY_API_KEY}&walletAddress=${MAINNET.address}&defaultCurrencyCode=${defaultCurrencyCode}&showOnlyCurrencies=${showOnlyCurrencies}&signature=laefTlgkESEc2hv8AZEH9F25VjLEJUADY27D6MccE54%3D`,
); );
nock.cleanAll(); nock.cleanAll();
}); });

@ -23,11 +23,6 @@ type CurrencySymbol = typeof CURRENCY_SYMBOLS[keyof typeof CURRENCY_SYMBOLS];
*/ */
type SupportedCurrencySymbol = type SupportedCurrencySymbol =
typeof SUPPORTED_CURRENCY_SYMBOLS[keyof typeof SUPPORTED_CURRENCY_SYMBOLS]; typeof SUPPORTED_CURRENCY_SYMBOLS[keyof typeof SUPPORTED_CURRENCY_SYMBOLS];
/**
* For certain specific situations we need the above type, but with all symbols
* in lowercase format.
*/
type LowercaseCurrencySymbol = Lowercase<CurrencySymbol>;
/** /**
* Test networks have special symbols that combine the network name and 'ETH' * Test networks have special symbols that combine the network name and 'ETH'
* so that they are distinct from mainnet and other networks that use 'ETH'. * so that they are distinct from mainnet and other networks that use 'ETH'.
@ -40,7 +35,7 @@ export type TestNetworkCurrencySymbol =
* inform the MoonPay API which network the user is attempting to onramp into. * inform the MoonPay API which network the user is attempting to onramp into.
* This type reflects those possible values. * This type reflects those possible values.
*/ */
type MoonPayNetworkAbbreviation = 'bsc' | 'cchain' | 'polygon'; type MoonPayNetworkAbbreviation = 'BSC' | 'CCHAIN' | 'POLYGON';
/** /**
* MoonPay requires some settings that are configured per network that it is * MoonPay requires some settings that are configured per network that it is
@ -49,25 +44,20 @@ type MoonPayNetworkAbbreviation = 'bsc' | 'cchain' | 'polygon';
type MoonPayChainSettings = { type MoonPayChainSettings = {
/** /**
* What should the default onramp currency be, for example 'eth' on 'mainnet' * What should the default onramp currency be, for example 'eth' on 'mainnet'
* This type matches a single LowercaseCurrencySymbol or a * This type matches a single SupportedCurrencySymbol or a
* LowercaseCurrencySymbol and a MoonPayNetworkAbbreviation joined by a '_'. * SupportedCurrencySymbol and a MoonPayNetworkAbbreviation joined by a '_'.
*/ */
defaultCurrencyCode: defaultCurrencyCode:
| LowercaseCurrencySymbol | SupportedCurrencySymbol
| `${LowercaseCurrencySymbol}_${MoonPayNetworkAbbreviation}`; | `${SupportedCurrencySymbol}_${MoonPayNetworkAbbreviation}`;
/** /**
* We must also configure all possible onramp currencies we wish to support. * We must also configure all possible onramp currencies we wish to support.
* This type matches 1 to 3 LowercaseCurrencySymbols, joined by ','. It also * This type matches either an array of SupportedCurrencySymbol or
* matches 1 or 2 LowercaseCurrencySymbols with a * an array of SupportedCurrencySymbol and a MoonPayNetworkAbbreviation joined by a '_'.
* MoonPayNetworkAbbreviation joined by a '_', and concatenated with ','.
*/ */
showOnlyCurrencies: showOnlyCurrencies:
| `${LowercaseCurrencySymbol}` | SupportedCurrencySymbol[]
| `${LowercaseCurrencySymbol},${LowercaseCurrencySymbol}` | `${SupportedCurrencySymbol}_${MoonPayNetworkAbbreviation}`[];
| `${LowercaseCurrencySymbol},${LowercaseCurrencySymbol},${LowercaseCurrencySymbol}`
| `${LowercaseCurrencySymbol},${LowercaseCurrencySymbol},${LowercaseCurrencySymbol},${LowercaseCurrencySymbol}`
| `${LowercaseCurrencySymbol}_${MoonPayNetworkAbbreviation}`
| `${LowercaseCurrencySymbol}_${MoonPayNetworkAbbreviation},${LowercaseCurrencySymbol}_${MoonPayNetworkAbbreviation}`;
}; };
/** /**
@ -318,6 +308,7 @@ const SUPPORTED_CURRENCY_SYMBOLS = {
ASM: 'ASM', ASM: 'ASM',
AUCTION: 'AUCTION', AUCTION: 'AUCTION',
AXS: 'AXS', AXS: 'AXS',
AVAX: 'AVAX',
BADGER: 'BADGER', BADGER: 'BADGER',
BAL: 'BAL', BAL: 'BAL',
BAND: 'BAND', BAND: 'BAND',
@ -351,6 +342,7 @@ const SUPPORTED_CURRENCY_SYMBOLS = {
GTH: 'GTH', GTH: 'GTH',
HEX: 'HEX', HEX: 'HEX',
IOTX: 'IOTX', IOTX: 'IOTX',
IMX: 'IMX',
JASMY: 'JASMY', JASMY: 'JASMY',
KEEP: 'KEEP', KEEP: 'KEEP',
KNC: 'KNC', KNC: 'KNC',
@ -371,6 +363,7 @@ const SUPPORTED_CURRENCY_SYMBOLS = {
NU: 'NU', NU: 'NU',
OGN: 'OGN', OGN: 'OGN',
OMG: 'OMG', OMG: 'OMG',
ORN: 'ORN',
OXT: 'OXT', OXT: 'OXT',
PAX: 'PAX', PAX: 'PAX',
PERP: 'PERP', PERP: 'PERP',
@ -685,8 +678,17 @@ export const BUYABLE_CHAINS_MAP: {
SUPPORTED_CURRENCY_SYMBOLS.YLD, SUPPORTED_CURRENCY_SYMBOLS.YLD,
], ],
moonPay: { moonPay: {
defaultCurrencyCode: 'eth', defaultCurrencyCode: SUPPORTED_CURRENCY_SYMBOLS.ETH,
showOnlyCurrencies: 'eth,usdt,usdc,dai', showOnlyCurrencies: [
SUPPORTED_CURRENCY_SYMBOLS.ETH,
SUPPORTED_CURRENCY_SYMBOLS.USDT,
SUPPORTED_CURRENCY_SYMBOLS.USDC,
SUPPORTED_CURRENCY_SYMBOLS.DAI,
SUPPORTED_CURRENCY_SYMBOLS.MATIC,
SUPPORTED_CURRENCY_SYMBOLS.ORN,
SUPPORTED_CURRENCY_SYMBOLS.WETH,
SUPPORTED_CURRENCY_SYMBOLS.IMX,
],
}, },
wyre: { wyre: {
srn: 'ethereum', srn: 'ethereum',
@ -821,8 +823,11 @@ export const BUYABLE_CHAINS_MAP: {
SUPPORTED_CURRENCY_SYMBOLS.BUSD, SUPPORTED_CURRENCY_SYMBOLS.BUSD,
], ],
moonPay: { moonPay: {
defaultCurrencyCode: 'bnb_bsc', defaultCurrencyCode: `${SUPPORTED_CURRENCY_SYMBOLS.BNB}_BSC`,
showOnlyCurrencies: 'bnb_bsc,busd_bsc', showOnlyCurrencies: [
`${SUPPORTED_CURRENCY_SYMBOLS.BNB}_BSC`,
`${SUPPORTED_CURRENCY_SYMBOLS.BUSD}_BSC`,
],
}, },
}, },
[CHAIN_IDS.POLYGON]: { [CHAIN_IDS.POLYGON]: {
@ -835,8 +840,11 @@ export const BUYABLE_CHAINS_MAP: {
SUPPORTED_CURRENCY_SYMBOLS.DAI, SUPPORTED_CURRENCY_SYMBOLS.DAI,
], ],
moonPay: { moonPay: {
defaultCurrencyCode: 'matic_polygon', defaultCurrencyCode: `${SUPPORTED_CURRENCY_SYMBOLS.BNB}_POLYGON`,
showOnlyCurrencies: 'matic_polygon,usdc_polygon', showOnlyCurrencies: [
`${SUPPORTED_CURRENCY_SYMBOLS.MATIC}_POLYGON`,
`${SUPPORTED_CURRENCY_SYMBOLS.USDC}_POLYGON`,
],
}, },
wyre: { wyre: {
srn: 'matic', srn: 'matic',
@ -848,8 +856,8 @@ export const BUYABLE_CHAINS_MAP: {
network: 'avaxcchain', network: 'avaxcchain',
transakCurrencies: [SUPPORTED_CURRENCY_SYMBOLS.AVALANCHE], transakCurrencies: [SUPPORTED_CURRENCY_SYMBOLS.AVALANCHE],
moonPay: { moonPay: {
defaultCurrencyCode: 'avax_cchain', defaultCurrencyCode: `${SUPPORTED_CURRENCY_SYMBOLS.AVAX}_CCHAIN`,
showOnlyCurrencies: 'avax_cchain', showOnlyCurrencies: [`${SUPPORTED_CURRENCY_SYMBOLS.AVAX}_CCHAIN`],
}, },
wyre: { wyre: {
srn: 'avalanche', srn: 'avalanche',
@ -867,8 +875,8 @@ export const BUYABLE_CHAINS_MAP: {
network: 'celo', network: 'celo',
transakCurrencies: [SUPPORTED_CURRENCY_SYMBOLS.CELO], transakCurrencies: [SUPPORTED_CURRENCY_SYMBOLS.CELO],
moonPay: { moonPay: {
defaultCurrencyCode: 'celo', defaultCurrencyCode: SUPPORTED_CURRENCY_SYMBOLS.CELO,
showOnlyCurrencies: 'celo', showOnlyCurrencies: [SUPPORTED_CURRENCY_SYMBOLS.CELO],
}, },
}, },
}; };

@ -28,6 +28,7 @@ import {
getIsBuyableCoinbasePayChain, getIsBuyableCoinbasePayChain,
getIsBuyableCoinbasePayToken, getIsBuyableCoinbasePayToken,
getIsBuyableTransakToken, getIsBuyableTransakToken,
getIsBuyableMoonpayToken,
} from '../../../selectors/selectors'; } from '../../../selectors/selectors';
import OnRampItem from './on-ramp-item'; import OnRampItem from './on-ramp-item';
@ -53,6 +54,9 @@ const DepositPopover = ({ onClose, token }) => {
const isTokenBuyableTransak = useSelector((state) => const isTokenBuyableTransak = useSelector((state) =>
getIsBuyableTransakToken(state, token?.symbol), getIsBuyableTransakToken(state, token?.symbol),
); );
const isTokenBuyableMoonpay = useSelector((state) =>
getIsBuyableMoonpayToken(state, token?.symbol),
);
const networkName = NETWORK_TO_NAME_MAP[chainId]; const networkName = NETWORK_TO_NAME_MAP[chainId];
const symbol = token const symbol = token
@ -77,7 +81,9 @@ const DepositPopover = ({ onClose, token }) => {
); );
}; };
const toMoonPay = () => { const toMoonPay = () => {
dispatch(buy({ service: 'moonpay', address, chainId })); dispatch(
buy({ service: 'moonpay', address, chainId, symbol: token?.symbol }),
);
}; };
const toWyre = () => { const toWyre = () => {
dispatch(buy({ service: 'wyre', address, chainId })); dispatch(buy({ service: 'wyre', address, chainId }));
@ -153,7 +159,11 @@ const DepositPopover = ({ onClose, token }) => {
}); });
toMoonPay(); toMoonPay();
}} }}
hide={isTokenDeposit || !isBuyableMoonPayChain} hide={
isTokenDeposit
? !isBuyableMoonPayChain || !isTokenBuyableMoonpay
: !isBuyableMoonPayChain
}
/> />
<OnRampItem <OnRampItem

@ -21,6 +21,7 @@ import {
getIsSwapsChain, getIsSwapsChain,
getIsBuyableCoinbasePayToken, getIsBuyableCoinbasePayToken,
getIsBuyableTransakToken, getIsBuyableTransakToken,
getIsBuyableMoonpayToken,
} from '../../../selectors/selectors'; } from '../../../selectors/selectors';
import BuyIcon from '../../ui/icon/overview-buy-icon.component'; import BuyIcon from '../../ui/icon/overview-buy-icon.component';
@ -59,7 +60,11 @@ const TokenOverview = ({ className, token }) => {
const isTokenBuyableTransak = useSelector((state) => const isTokenBuyableTransak = useSelector((state) =>
getIsBuyableTransakToken(state, token.symbol), getIsBuyableTransakToken(state, token.symbol),
); );
const isBuyable = isTokenBuyableCoinbasePay || isTokenBuyableTransak; const isTokenBuyableMoonpay = useSelector((state) =>
getIsBuyableMoonpayToken(state, token.symbol),
);
const isBuyable =
isTokenBuyableCoinbasePay || isTokenBuyableTransak || isTokenBuyableMoonpay;
useEffect(() => { useEffect(() => {
if (token.isERC721 && process.env.COLLECTIBLES_V1) { if (token.isERC721 && process.env.COLLECTIBLES_V1) {

@ -0,0 +1,19 @@
import {
BUYABLE_CHAINS_MAP,
CHAIN_IDS,
} from '../../../shared/constants/network';
export const formatMoonpaySymbol = (symbol, chainId = CHAIN_IDS.MAINNET) => {
if (!symbol) {
return symbol;
}
let _symbol = symbol;
if (chainId === CHAIN_IDS.POLYGON || chainId === CHAIN_IDS.BSC) {
_symbol = `${_symbol}_${BUYABLE_CHAINS_MAP?.[
chainId
]?.network.toUpperCase()}`;
} else if (chainId === CHAIN_IDS.AVALANCHE) {
_symbol = `${_symbol}_CCHAIN`;
}
return _symbol;
};

@ -0,0 +1,33 @@
import { CHAIN_IDS } from '../../../shared/constants/network';
import { formatMoonpaySymbol } from './moonpay';
describe('Moonpay Utils', () => {
describe('formatMoonpaySymbol', () => {
it('should return the same input if falsy input is provided', () => {
expect(formatMoonpaySymbol()).toBe(undefined);
expect(formatMoonpaySymbol(null)).toBe(null);
expect(formatMoonpaySymbol('')).toBe('');
});
it('should return the symbol in uppercase if no chainId is provided', () => {
const result = formatMoonpaySymbol('ETH');
expect(result).toStrictEqual('ETH');
});
it('should return the symbol in uppercase if chainId is different than Avalanche/BSC/Polygon', () => {
const result = formatMoonpaySymbol('ETH', CHAIN_IDS.MAINNET);
expect(result).toStrictEqual('ETH');
const result2 = formatMoonpaySymbol('CELO', CHAIN_IDS.CELO);
expect(result2).toStrictEqual('CELO');
});
it('should return the symbol in uppercase with the network name if chainId is Avalanche/BSC/Polygon', () => {
const result = formatMoonpaySymbol('BNB', CHAIN_IDS.BSC);
expect(result).toStrictEqual('BNB_BSC');
const result2 = formatMoonpaySymbol('MATIC', CHAIN_IDS.POLYGON);
expect(result2).toStrictEqual('MATIC_POLYGON');
const result3 = formatMoonpaySymbol('AVAX', CHAIN_IDS.AVALANCHE);
expect(result3).toStrictEqual('AVAX_CCHAIN');
});
});
});

@ -63,6 +63,7 @@ import {
} from '../ducks/app/app'; } from '../ducks/app/app';
import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils';
import { hexToDecimal } from '../../shared/lib/metamask-controller-utils'; import { hexToDecimal } from '../../shared/lib/metamask-controller-utils';
import { formatMoonpaySymbol } from '../helpers/utils/moonpay';
///: BEGIN:ONLY_INCLUDE_IN(flask) ///: BEGIN:ONLY_INCLUDE_IN(flask)
import { SNAPS_VIEW_ROUTE } from '../helpers/constants/routes'; import { SNAPS_VIEW_ROUTE } from '../helpers/constants/routes';
import { getPermissionSubjects } from './permissions'; import { getPermissionSubjects } from './permissions';
@ -716,6 +717,16 @@ export function getIsBuyableTransakToken(state, symbol) {
); );
} }
export function getIsBuyableMoonpayToken(state, symbol) {
const chainId = getCurrentChainId(state);
const _symbol = formatMoonpaySymbol(symbol, chainId);
return Boolean(
BUYABLE_CHAINS_MAP?.[chainId]?.moonPay.showOnlyCurrencies?.includes(
_symbol,
),
);
}
export function getIsBuyableMoonPayChain(state) { export function getIsBuyableMoonPayChain(state) {
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
return Boolean(BUYABLE_CHAINS_MAP?.[chainId]?.moonPay); return Boolean(BUYABLE_CHAINS_MAP?.[chainId]?.moonPay);

Loading…
Cancel
Save