Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## Unreleased (develop)

- added: `AGENT_TEST_MODE` config setting and iOS debug-build `EdgeTestAnimations` switch that quiet the app for automated UI tests
- added: MoonPay payee contact in the transaction details payee picker
- fixed: A MoonPay sale no longer shows a swap provider's name, such as "MoonPay Trade (DeFi)", as its title
- fixed: Recognize promotions that only set a preferred exchange in the exchange settings, which now say the promotion is choosing it and offer to remove it, instead of showing a list whose taps were saved but silently overridden for the promotion's whole window

## 4.52.0 (staging)
Expand Down
105 changes: 105 additions & 0 deletions src/__tests__/actions/CategoriesActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
EdgeMetadata,
EdgeTransaction
} from 'edge-core-js'
import { sprintf } from 'sprintf-js'

import {
getPluginIdIcon,
Expand All @@ -18,10 +19,16 @@ import { edgeLight } from '../../theme/variables/edgeLight'
const BITCOIN_WALLET_ID = 'bitcoin-wallet-id'
const RECIPIENT_ADDRESS = 'bc1qrecipientaddressthepayeecontrols'
const DEPOSIT_ADDRESS = '13e6qqcAZCgApTDyMNG8brru4PmtjbReUd'
const SWAP_PROVIDER_NAME = 'MoonPay Trade (DeFi)'

// Only the fields `getTxActionDisplayInfo` actually reads:
const account = {
currencyWallets: {},
swapConfig: {
mptradedefi: {
swapInfo: { pluginId: 'mptradedefi', displayName: SWAP_PROVIDER_NAME }
}
},
currencyConfig: {
bitcoin: {
currencyInfo: { currencyCode: 'BTC' },
Expand Down Expand Up @@ -199,6 +206,104 @@ describe('getTxActionDisplayInfo, the parent network-fee row', () => {
})
})

/**
* A sale as the MoonPay ramp plugin leaves it: a send to the provider's
* deposit address carrying a fiat action, and no name of its own.
*/
const makeFiatSellTx = (metadata?: EdgeMetadata): EdgeTransaction => {
return {
txid: 'txid',
tokenId: null,
currencyCode: 'BTC',
nativeAmount: '-563825',
isSend: true,
metadata,
assetAction: { assetActionType: 'sell' },
spendTargets: [{ publicAddress: DEPOSIT_ADDRESS, nativeAmount: '563825' }],
savedAction: {
actionType: 'fiat',
orderId: 'f8a1c2d4',
isEstimate: true,
fiatPlugin: { providerId: 'moonpay', providerDisplayName: 'MoonPay' },
payinAddress: DEPOSIT_ADDRESS,
fiatAsset: { fiatCurrencyCode: 'iso:USD', fiatAmount: '471.04' },
cryptoAsset: {
pluginId: 'bitcoin',
tokenId: null,
nativeAmount: '563825'
}
}
} as unknown as EdgeTransaction
}

describe('getTxActionDisplayInfo, fiat sale titles', () => {
const sellTitle = sprintf(lstrings.transaction_details_sold_1s, 'Bitcoin')

it('titles a sale by its asset', () => {
const { mergedData } = getTxActionDisplayInfo(
makeFiatSellTx(),
account,
bitcoinWallet
)
expect(mergedData.name).toBe(sellTitle)
})

it('outranks a stored swap provider name on a sale', () => {
const { mergedData, userData } = getTxActionDisplayInfo(
makeFiatSellTx({ name: SWAP_PROVIDER_NAME }),
account,
bitcoinWallet
)
expect(mergedData.name).toBe(sellTitle)
// The stored name itself is left alone:
expect(userData.name).toBe(SWAP_PROVIDER_NAME)
})

it('outranks a stored swap provider name whose plugin is not loaded', () => {
const { mergedData } = getTxActionDisplayInfo(
makeFiatSellTx({ name: SWAP_PROVIDER_NAME }),
{ ...account, swapConfig: {} } as unknown as EdgeAccount,
bitcoinWallet
)
expect(mergedData.name).toBe(sellTitle)
})

it('outranks a loaded swap provider name that has no contact', () => {
const houdiniAccount = {
...account,
swapConfig: {
houdini: {
swapInfo: { pluginId: 'houdini', displayName: 'HoudiniSwap' }
}
}
} as unknown as EdgeAccount
const { mergedData } = getTxActionDisplayInfo(
makeFiatSellTx({ name: 'HoudiniSwap' }),
houdiniAccount,
bitcoinWallet
)
expect(mergedData.name).toBe(sellTitle)
})

it('lets any other stored name stand on a sale', () => {
const { mergedData } = getTxActionDisplayInfo(
makeFiatSellTx({ name: 'MoonPay' }),
account,
bitcoinWallet
)
expect(mergedData.name).toBe('MoonPay')
})

it('lets a stored swap provider name stand on a swap', () => {
const { mergedData } = getTxActionDisplayInfo(
makeSwapSendTx({ metadata: { name: SWAP_PROVIDER_NAME } }),
account,
bitcoinWallet
)
expect(mergedData.name).toBe(SWAP_PROVIDER_NAME)
})
})

describe('getPluginIdIcon', () => {
it('follows the theme and fits a single-color logo', () => {
expect(getPluginIdIcon('houdini', edgeDark)).toEqual({
Expand Down
20 changes: 19 additions & 1 deletion src/actions/CategoriesActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { sprintf } from 'sprintf-js'

import { showError } from '../components/services/AirshipInstance'
import { EDGE_CONTENT_SERVER_URI } from '../constants/CdnConstants'
import { SWAP_PROVIDER_CONTACT_NAMES } from '../constants/MerchantContacts'
import { TX_ACTION_LABEL_MAP } from '../constants/txActionConstants'
import { lstrings } from '../locales/strings'
import type { ThunkAction } from '../types/reduxTypes'
Expand Down Expand Up @@ -726,9 +727,26 @@ export const getTxActionDisplayInfo = (
// by any route would otherwise display exactly what the flow conceals.
const isPrivateSend = action?.actionType === 'swapSend' && action.privacy

// A fiat buy or sell is never a swap, so its title also outranks a stored
// name that belongs to a swap provider: the row would otherwise present the
// sale as a trade on that exchange. The contact list is checked beside the
// loaded plugins because the payee picker offers a provider's name whether
// or not its plugin loaded.
const storedName = metadata?.name
const isSwapNameOnFiat =
action?.actionType === 'fiat' &&
storedName != null &&
(SWAP_PROVIDER_CONTACT_NAMES.has(storedName) ||
Object.values(account.swapConfig).some(
swapConfig => swapConfig.swapInfo.displayName === storedName
))

const mergedData: EdgeMetadata = {
name:
!isPrivateSend && metadata?.name != null && metadata.name.length > 0
!isPrivateSend &&
!isSwapNameOnFiat &&
metadata?.name != null &&
metadata.name.length > 0
? metadata.name
: savedData.name,
category:
Expand Down
98 changes: 71 additions & 27 deletions src/constants/MerchantContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { EDGE_CONTENT_SERVER_URI } from './CdnConstants'
export interface MerchantContact {
displayName: string
thumbnailPath: string
/** True for a swap, bridge or DEX provider. */
isSwapProvider?: boolean
}

export const MERCHANT_CONTACTS: MerchantContact[] = [
{
displayName: '0x Gasless Swap',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/0xgasless.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/0xgasless.png`,
isSwapProvider: true
},
{
displayName: 'Bitrefill',
Expand All @@ -20,103 +23,130 @@ export const MERCHANT_CONTACTS: MerchantContact[] = [
},
{
displayName: 'Bridgeless',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/bridgeless.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/bridgeless.png`,
isSwapProvider: true
},
{
displayName: 'Change NOW',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/changenow.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/changenow.png`,
isSwapProvider: true
},
{
displayName: 'ChangeHero',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/changehero.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/changehero.png`,
isSwapProvider: true
},
{
displayName: 'Changelly',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/changelly.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/changelly.png`,
isSwapProvider: true
},
{
displayName: 'IBC Transfer',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/cosmosibc.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/cosmosibc.png`,
isSwapProvider: true
},
{
displayName: 'Fantom/Sonic Bridge',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/fantomsonicupgrade.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/fantomsonicupgrade.png`,
isSwapProvider: true
},
{
displayName: 'EOS Network',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/eos-logo-solo-64.png`
},
{
displayName: 'Exolix',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/exolix-logo.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/exolix-logo.png`,
isSwapProvider: true
},
{
displayName: 'Fox Exchange',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/foxEchange.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/foxEchange.png`,
isSwapProvider: true
},
{
displayName: 'Godex',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/godex.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/godex.png`,
isSwapProvider: true
},
{
displayName: 'LetsExchange',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/letsexchange-logo.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/letsexchange-logo.png`,
isSwapProvider: true
},
{
displayName: 'LI.FI',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/lifi.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/lifi.png`,
isSwapProvider: true
},
{
displayName: 'MoonPay',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/moonpay_symbol_prp.png`
},
{
displayName: 'MoonPay Trade (Centralized)',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/exchangeIcons/mptrade/icon.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/exchangeIcons/mptrade/icon.png`,
isSwapProvider: true
},
{
displayName: 'MoonPay Trade (DeFi)',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/exchangeIcons/mptradedefi/icon.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/exchangeIcons/mptradedefi/icon.png`,
isSwapProvider: true
},
{
displayName: 'ShapeShift',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/shapeshift.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/shapeshift.png`,
isSwapProvider: true
},
{
displayName: 'SideShift.ai',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/sideshift-logo.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/sideshift-logo.png`,
isSwapProvider: true
},
{
displayName: 'Simplex',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/simplex.png`
},
{
displayName: 'Swapuz',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/swapuz.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/swapuz.png`,
isSwapProvider: true
},
{
displayName: 'Switchain',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/switchain.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/switchain.png`,
isSwapProvider: true
},
{
displayName: 'Maya Protocol',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/mayaprotocol.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/mayaprotocol.png`,
isSwapProvider: true
},
{
displayName: 'Thorchain',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/thorchain.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/thorchain.png`,
isSwapProvider: true
},
{
displayName: 'Thorchain DEX Aggregator',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/thorchain.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/thorchain.png`,
isSwapProvider: true
},
{
displayName: 'Totle',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/totle-logo.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/totle-logo.png`,
isSwapProvider: true
},
{
displayName: 'Unizen',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/unizen.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/unizen.png`,
isSwapProvider: true
},
{
displayName: 'Velodrome',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/velodrome.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/velodrome.png`,
isSwapProvider: true
},
{
displayName: 'Visa® Prepaid Card',
Expand All @@ -128,18 +158,32 @@ export const MERCHANT_CONTACTS: MerchantContact[] = [
},
{
displayName: 'Xgram',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/xgram.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/xgram.png`,
isSwapProvider: true
},
{
displayName: 'XRP DEX',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/xrpdex.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/xrpdex.png`,
isSwapProvider: true
},
{
displayName: 'Rango Exchange',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/rango.png`
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/rango.png`,
isSwapProvider: true
},
{
displayName: 'Phaze Gift Card',
thumbnailPath: `${EDGE_CONTENT_SERVER_URI}/phaze.png`
}
]

/**
* Display names of the contacts that are swap providers. Unlike
* `account.swapConfig`, this set still knows a provider whose plugin failed
* to load or has been retired.
*/
export const SWAP_PROVIDER_CONTACT_NAMES = new Set(
MERCHANT_CONTACTS.filter(contact => contact.isSwapProvider === true).map(
contact => contact.displayName
)
)
Loading