From 3f801e377d0e9d028234dfd4acf4aa95d3a06423 Mon Sep 17 00:00:00 2001 From: Nicolas Ferro Date: Fri, 23 Sep 2022 18:38:40 +0200 Subject: [PATCH] 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 --- .gitignore | 3 + app/scripts/lib/buy-url.js | 9 ++- app/scripts/lib/buy-url.test.js | 4 +- shared/constants/network.ts | 66 +++++++++++-------- .../app/deposit-popover/deposit-popover.js | 14 +++- .../app/wallet-overview/token-overview.js | 7 +- ui/helpers/utils/moonpay.js | 19 ++++++ ui/helpers/utils/moonpay.test.js | 33 ++++++++++ ui/selectors/selectors.js | 11 ++++ 9 files changed, 129 insertions(+), 37 deletions(-) create mode 100644 ui/helpers/utils/moonpay.js create mode 100644 ui/helpers/utils/moonpay.test.js diff --git a/.gitignore b/.gitignore index c86fb82db..1d8d1f276 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ notes.txt # TypeScript tsout/ + +# Test results +test-results/ \ No newline at end of file diff --git a/app/scripts/lib/buy-url.js b/app/scripts/lib/buy-url.js index 5a4f2dadf..079d54c39 100644 --- a/app/scripts/lib/buy-url.js +++ b/app/scripts/lib/buy-url.js @@ -11,6 +11,7 @@ import { MOONPAY_API_KEY, COINBASEPAY_API_KEY, } from '../constants/on-ramp'; +import { formatMoonpaySymbol } from '../../../ui/helpers/utils/moonpay'; const fetchWithTimeout = getFetchWithTimeout(); @@ -75,15 +76,17 @@ const createTransakUrl = (walletAddress, chainId, symbol) => { * * @param {string} walletAddress - Destination address * @param {string} chainId - Current chain ID + * @param {string|undefined} symbol - Token symbol to buy * @returns String */ -const createMoonPayUrl = async (walletAddress, chainId) => { +const createMoonPayUrl = async (walletAddress, chainId, symbol) => { const { moonPay: { defaultCurrencyCode, showOnlyCurrencies } = {} } = BUYABLE_CHAINS_MAP[chainId]; const moonPayQueryParams = new URLSearchParams({ apiKey: MOONPAY_API_KEY, walletAddress, - defaultCurrencyCode, + defaultCurrencyCode: + formatMoonpaySymbol(symbol, chainId) || defaultCurrencyCode, showOnlyCurrencies, }); const queryParams = new URLSearchParams({ @@ -159,7 +162,7 @@ export default async function getBuyUrl({ chainId, address, service, symbol }) { case 'transak': return createTransakUrl(address, chainId, symbol); case 'moonpay': - return createMoonPayUrl(address, chainId); + return createMoonPayUrl(address, chainId, symbol); case 'coinbase': return createCoinbasePayUrl(address, chainId, symbol); case 'metamask-faucet': diff --git a/app/scripts/lib/buy-url.test.js b/app/scripts/lib/buy-url.test.js index 188153f6c..2af812ffb 100644 --- a/app/scripts/lib/buy-url.test.js +++ b/app/scripts/lib/buy-url.test.js @@ -117,11 +117,11 @@ describe('buy-url', () => { 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`, + 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' }); 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(); }); diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 45a7d7d03..d09d5e0e6 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -23,11 +23,6 @@ type CurrencySymbol = typeof CURRENCY_SYMBOLS[keyof typeof CURRENCY_SYMBOLS]; */ type SupportedCurrencySymbol = 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; /** * 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'. @@ -40,7 +35,7 @@ export type TestNetworkCurrencySymbol = * inform the MoonPay API which network the user is attempting to onramp into. * 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 @@ -49,25 +44,20 @@ type MoonPayNetworkAbbreviation = 'bsc' | 'cchain' | 'polygon'; type MoonPayChainSettings = { /** * What should the default onramp currency be, for example 'eth' on 'mainnet' - * This type matches a single LowercaseCurrencySymbol or a - * LowercaseCurrencySymbol and a MoonPayNetworkAbbreviation joined by a '_'. + * This type matches a single SupportedCurrencySymbol or a + * SupportedCurrencySymbol and a MoonPayNetworkAbbreviation joined by a '_'. */ defaultCurrencyCode: - | LowercaseCurrencySymbol - | `${LowercaseCurrencySymbol}_${MoonPayNetworkAbbreviation}`; + | SupportedCurrencySymbol + | `${SupportedCurrencySymbol}_${MoonPayNetworkAbbreviation}`; /** * We must also configure all possible onramp currencies we wish to support. - * This type matches 1 to 3 LowercaseCurrencySymbols, joined by ','. It also - * matches 1 or 2 LowercaseCurrencySymbols with a - * MoonPayNetworkAbbreviation joined by a '_', and concatenated with ','. + * This type matches either an array of SupportedCurrencySymbol or + * an array of SupportedCurrencySymbol and a MoonPayNetworkAbbreviation joined by a '_'. */ showOnlyCurrencies: - | `${LowercaseCurrencySymbol}` - | `${LowercaseCurrencySymbol},${LowercaseCurrencySymbol}` - | `${LowercaseCurrencySymbol},${LowercaseCurrencySymbol},${LowercaseCurrencySymbol}` - | `${LowercaseCurrencySymbol},${LowercaseCurrencySymbol},${LowercaseCurrencySymbol},${LowercaseCurrencySymbol}` - | `${LowercaseCurrencySymbol}_${MoonPayNetworkAbbreviation}` - | `${LowercaseCurrencySymbol}_${MoonPayNetworkAbbreviation},${LowercaseCurrencySymbol}_${MoonPayNetworkAbbreviation}`; + | SupportedCurrencySymbol[] + | `${SupportedCurrencySymbol}_${MoonPayNetworkAbbreviation}`[]; }; /** @@ -318,6 +308,7 @@ const SUPPORTED_CURRENCY_SYMBOLS = { ASM: 'ASM', AUCTION: 'AUCTION', AXS: 'AXS', + AVAX: 'AVAX', BADGER: 'BADGER', BAL: 'BAL', BAND: 'BAND', @@ -351,6 +342,7 @@ const SUPPORTED_CURRENCY_SYMBOLS = { GTH: 'GTH', HEX: 'HEX', IOTX: 'IOTX', + IMX: 'IMX', JASMY: 'JASMY', KEEP: 'KEEP', KNC: 'KNC', @@ -371,6 +363,7 @@ const SUPPORTED_CURRENCY_SYMBOLS = { NU: 'NU', OGN: 'OGN', OMG: 'OMG', + ORN: 'ORN', OXT: 'OXT', PAX: 'PAX', PERP: 'PERP', @@ -685,8 +678,17 @@ export const BUYABLE_CHAINS_MAP: { SUPPORTED_CURRENCY_SYMBOLS.YLD, ], moonPay: { - defaultCurrencyCode: 'eth', - showOnlyCurrencies: 'eth,usdt,usdc,dai', + defaultCurrencyCode: SUPPORTED_CURRENCY_SYMBOLS.ETH, + 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: { srn: 'ethereum', @@ -821,8 +823,11 @@ export const BUYABLE_CHAINS_MAP: { SUPPORTED_CURRENCY_SYMBOLS.BUSD, ], moonPay: { - defaultCurrencyCode: 'bnb_bsc', - showOnlyCurrencies: 'bnb_bsc,busd_bsc', + defaultCurrencyCode: `${SUPPORTED_CURRENCY_SYMBOLS.BNB}_BSC`, + showOnlyCurrencies: [ + `${SUPPORTED_CURRENCY_SYMBOLS.BNB}_BSC`, + `${SUPPORTED_CURRENCY_SYMBOLS.BUSD}_BSC`, + ], }, }, [CHAIN_IDS.POLYGON]: { @@ -835,8 +840,11 @@ export const BUYABLE_CHAINS_MAP: { SUPPORTED_CURRENCY_SYMBOLS.DAI, ], moonPay: { - defaultCurrencyCode: 'matic_polygon', - showOnlyCurrencies: 'matic_polygon,usdc_polygon', + defaultCurrencyCode: `${SUPPORTED_CURRENCY_SYMBOLS.BNB}_POLYGON`, + showOnlyCurrencies: [ + `${SUPPORTED_CURRENCY_SYMBOLS.MATIC}_POLYGON`, + `${SUPPORTED_CURRENCY_SYMBOLS.USDC}_POLYGON`, + ], }, wyre: { srn: 'matic', @@ -848,8 +856,8 @@ export const BUYABLE_CHAINS_MAP: { network: 'avaxcchain', transakCurrencies: [SUPPORTED_CURRENCY_SYMBOLS.AVALANCHE], moonPay: { - defaultCurrencyCode: 'avax_cchain', - showOnlyCurrencies: 'avax_cchain', + defaultCurrencyCode: `${SUPPORTED_CURRENCY_SYMBOLS.AVAX}_CCHAIN`, + showOnlyCurrencies: [`${SUPPORTED_CURRENCY_SYMBOLS.AVAX}_CCHAIN`], }, wyre: { srn: 'avalanche', @@ -867,8 +875,8 @@ export const BUYABLE_CHAINS_MAP: { network: 'celo', transakCurrencies: [SUPPORTED_CURRENCY_SYMBOLS.CELO], moonPay: { - defaultCurrencyCode: 'celo', - showOnlyCurrencies: 'celo', + defaultCurrencyCode: SUPPORTED_CURRENCY_SYMBOLS.CELO, + showOnlyCurrencies: [SUPPORTED_CURRENCY_SYMBOLS.CELO], }, }, }; diff --git a/ui/components/app/deposit-popover/deposit-popover.js b/ui/components/app/deposit-popover/deposit-popover.js index 0a0a814ba..c5ddc363d 100644 --- a/ui/components/app/deposit-popover/deposit-popover.js +++ b/ui/components/app/deposit-popover/deposit-popover.js @@ -28,6 +28,7 @@ import { getIsBuyableCoinbasePayChain, getIsBuyableCoinbasePayToken, getIsBuyableTransakToken, + getIsBuyableMoonpayToken, } from '../../../selectors/selectors'; import OnRampItem from './on-ramp-item'; @@ -53,6 +54,9 @@ const DepositPopover = ({ onClose, token }) => { const isTokenBuyableTransak = useSelector((state) => getIsBuyableTransakToken(state, token?.symbol), ); + const isTokenBuyableMoonpay = useSelector((state) => + getIsBuyableMoonpayToken(state, token?.symbol), + ); const networkName = NETWORK_TO_NAME_MAP[chainId]; const symbol = token @@ -77,7 +81,9 @@ const DepositPopover = ({ onClose, token }) => { ); }; const toMoonPay = () => { - dispatch(buy({ service: 'moonpay', address, chainId })); + dispatch( + buy({ service: 'moonpay', address, chainId, symbol: token?.symbol }), + ); }; const toWyre = () => { dispatch(buy({ service: 'wyre', address, chainId })); @@ -153,7 +159,11 @@ const DepositPopover = ({ onClose, token }) => { }); toMoonPay(); }} - hide={isTokenDeposit || !isBuyableMoonPayChain} + hide={ + isTokenDeposit + ? !isBuyableMoonPayChain || !isTokenBuyableMoonpay + : !isBuyableMoonPayChain + } /> { const isTokenBuyableTransak = useSelector((state) => getIsBuyableTransakToken(state, token.symbol), ); - const isBuyable = isTokenBuyableCoinbasePay || isTokenBuyableTransak; + const isTokenBuyableMoonpay = useSelector((state) => + getIsBuyableMoonpayToken(state, token.symbol), + ); + const isBuyable = + isTokenBuyableCoinbasePay || isTokenBuyableTransak || isTokenBuyableMoonpay; useEffect(() => { if (token.isERC721 && process.env.COLLECTIBLES_V1) { diff --git a/ui/helpers/utils/moonpay.js b/ui/helpers/utils/moonpay.js new file mode 100644 index 000000000..67686d741 --- /dev/null +++ b/ui/helpers/utils/moonpay.js @@ -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; +}; diff --git a/ui/helpers/utils/moonpay.test.js b/ui/helpers/utils/moonpay.test.js new file mode 100644 index 000000000..bf13f500d --- /dev/null +++ b/ui/helpers/utils/moonpay.test.js @@ -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'); + }); + }); +}); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 961bf339f..78ad6d79d 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -63,6 +63,7 @@ import { } from '../ducks/app/app'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import { hexToDecimal } from '../../shared/lib/metamask-controller-utils'; +import { formatMoonpaySymbol } from '../helpers/utils/moonpay'; ///: BEGIN:ONLY_INCLUDE_IN(flask) import { SNAPS_VIEW_ROUTE } from '../helpers/constants/routes'; 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) { const chainId = getCurrentChainId(state); return Boolean(BUYABLE_CHAINS_MAP?.[chainId]?.moonPay);