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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- added: Sign Message option in the wallet list menu for Bitcoin-family wallets, letting users prove self-hosted wallet ownership to exchanges by signing an exchange-provided message.
- added: `edge://buy` and `edge://sell` deep links (and their `https://deep.edge.app` equivalents) that open the buy/sell flow, optionally pinning a provider and payment method to the top of the quote options for that visit.
- added: Provider priority in the buy/sell options for affiliated accounts, configured through the info server promo card data.
- changed: MoonPay iOS buys open through a Private Relay interstitial check, so IP-match enforcement cannot lock out iCloud Private Relay users; any interstitial failure falls back to the existing bound flow.
- changed: Target Android 16 (API level 36), which Google Play requires for app updates submitted after Aug 30, 2026. Predictive back is opted out of for now, since React Native 0.79 cannot handle it, so the back button behaves exactly as it did before.
- changed: Sign MoonPay buy/sell widget URLs and bind them to the customer's IP via the info server, for MoonPay's on-ramp IP-matching security upgrade.
- changed: Style the entire "Already have an account? Sign in" line in the getting-started USP carousel with the tertiary link color, not just "Sign in".
Expand Down
107 changes: 107 additions & 0 deletions src/__tests__/plugins/gui/providers/moonpaySign.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals'

import { ENV } from '../../../../env'
import {
fetchMoonpayInterstitialUrl,
signMoonpayUrl
} from '../../../../plugins/gui/providers/moonpaySign'
import { fetchInfo, fetchWaterfall } from '../../../../util/network'

jest.mock('../../../../util/network', () => ({
fetchInfo: jest.fn(),
fetchWaterfall: jest.fn()
}))

jest.mock('../../../../env', () => ({
ENV: { MOONPAY_RELAY_CHECK_SIGN_PROXY: undefined }
}))

const mockedFetchInfo = fetchInfo as jest.MockedFunction<typeof fetchInfo>
const mockedFetchWaterfall = fetchWaterfall as jest.MockedFunction<
typeof fetchWaterfall
>

const widgetUrl = 'https://buy.moonpay.com/?apiKey=pk_live_key'

const jsonResponse = (body: unknown, ok: boolean = true): any => ({
ok,
status: ok ? 200 : 500,
json: async () => body
})

describe('signMoonpayUrl', () => {
beforeEach(() => {
jest.clearAllMocks()
ENV.MOONPAY_RELAY_CHECK_SIGN_PROXY = undefined
})

it('posts the url and returns the signed url', async () => {
mockedFetchInfo.mockResolvedValue(
jsonResponse({ signedUrl: 'https://buy.moonpay.com/?signed=1' })
)
const result = await signMoonpayUrl(widgetUrl)
expect(result).toBe('https://buy.moonpay.com/?signed=1')
const [path, options] = mockedFetchInfo.mock.calls[0]
expect(path).toBe('v1/moonpay/signUrl')
expect(JSON.parse(options?.body as string)).toEqual({ url: widgetUrl })
})

it('throws the signing error on a non-OK response', async () => {
mockedFetchInfo.mockResolvedValue(jsonResponse({}, false))
await expect(signMoonpayUrl(widgetUrl)).rejects.toThrow(
'Moonpay URL signing failed: 500'
)
})
})

describe('fetchMoonpayInterstitialUrl', () => {
beforeEach(() => {
jest.clearAllMocks()
ENV.MOONPAY_RELAY_CHECK_SIGN_PROXY = undefined
})

it('posts relayCheck: true and returns the interstitial url', async () => {
mockedFetchInfo.mockResolvedValue(
jsonResponse({
interstitialUrl: 'https://info1.edge.app/v1/moonpay/relayCheck?token=t'
})
)
const result = await fetchMoonpayInterstitialUrl(widgetUrl)
expect(result).toBe('https://info1.edge.app/v1/moonpay/relayCheck?token=t')
const [path, options] = mockedFetchInfo.mock.calls[0]
expect(path).toBe('v1/moonpay/signUrl')
expect(JSON.parse(options?.body as string)).toEqual({
url: widgetUrl,
relayCheck: true
})
expect(mockedFetchWaterfall).not.toHaveBeenCalled()
})

it('throws on an empty interstitial url so the caller falls back to bound signing', async () => {
mockedFetchInfo.mockResolvedValue(jsonResponse({ interstitialUrl: '' }))
await expect(fetchMoonpayInterstitialUrl(widgetUrl)).rejects.toThrow(
'empty interstitial URL'
)
})

it('throws the relay-check error on a non-OK response', async () => {
mockedFetchInfo.mockResolvedValue(jsonResponse({}, false))
await expect(fetchMoonpayInterstitialUrl(widgetUrl)).rejects.toThrow(
'Moonpay relay check failed: 500'
)
})

it('routes through the trimmed dev sign proxy with the same timeout', async () => {
ENV.MOONPAY_RELAY_CHECK_SIGN_PROXY = 'http://192.0.2.10:8009/'
mockedFetchWaterfall.mockResolvedValue(
jsonResponse({ interstitialUrl: 'http://192.0.2.10:8009/v1/x' })
)
const result = await fetchMoonpayInterstitialUrl(widgetUrl)
expect(result).toBe('http://192.0.2.10:8009/v1/x')
const [servers, path, , timeout] = mockedFetchWaterfall.mock.calls[0]
expect(servers).toEqual(['http://192.0.2.10:8009'])
expect(path).toBe('v1/moonpay/signUrl')
expect(timeout).toBe(10000)
expect(mockedFetchInfo).not.toHaveBeenCalled()
})
})
Loading
Loading