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 @@ -2,6 +2,8 @@

## Unreleased (develop)

- 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.

## 4.49.0 (staging)

- added: Monero wallet import support
Expand Down
1 change: 0 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,6 @@ export default [

'src/plugins/gui/providers/bityProvider.ts',

'src/plugins/gui/providers/moonpayProvider.ts',
'src/plugins/gui/providers/mtpelerinProvider.ts',

'src/plugins/gui/providers/revolutProvider.ts',
Expand Down
7 changes: 5 additions & 2 deletions src/plugins/gui/providers/moonpayProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
RETURN_URL_PAYMENT,
validateExactRegion
} from './common'
import { signMoonpayUrl } from './moonpaySign'
const providerId = 'moonpay'
const storeId = 'com.moonpay'
const partnerIcon = 'moonpay_symbol_prp.png'
Expand Down Expand Up @@ -643,6 +644,7 @@ export const moonpayProvider: FiatProviderFactory = {
}
urlObj.set('query', queryObj)
console.log('Approving moonpay buy quote url=' + urlObj.href)
const signedUrl = await signMoonpayUrl(urlObj.href)
const handleBuyDeeplinkAsync = async (
link: unknown
): Promise<void> => {
Expand Down Expand Up @@ -695,7 +697,7 @@ export const moonpayProvider: FiatProviderFactory = {
handleBuyDeeplinkAsync(link).catch(() => {})
}
await showUi.openExternalWebView({
url: urlObj.href,
url: signedUrl,
providerId,
deeplinkHandler: handleBuyDeeplink
})
Expand All @@ -718,6 +720,7 @@ export const moonpayProvider: FiatProviderFactory = {
}
urlObj.set('query', queryObj)
console.log('Approving moonpay sell quote url=' + urlObj.href)
const signedUrl = await signMoonpayUrl(urlObj.href)

let inPayment = false

Expand Down Expand Up @@ -867,7 +870,7 @@ export const moonpayProvider: FiatProviderFactory = {
onUrlChangeAsync(uri).catch(() => {})
}
await showUi.openWebView({
url: urlObj.href,
url: signedUrl,
onUrlChange,
onClose: () => {}
})
Expand Down
28 changes: 28 additions & 0 deletions src/plugins/gui/providers/moonpaySign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { asObject, asString } from 'cleaners'

import { fetchInfo } from '../../../util/network'

const asMoonpaySignResponse = asObject({ signedUrl: asString })

/**
* Ask the info server to bind a Moonpay widget URL to the caller's public IP
* and sign it. Moonpay's on-ramp security upgrade refuses to load widget URLs
* that are not signed and IP-bound, so every buy/sell widget URL must be routed
* through here before it is opened.
*/
export const signMoonpayUrl = async (url: string): Promise<string> => {
const response = await fetchInfo(
'v1/moonpay/signUrl',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
},
10000
)
if (!response.ok) {
throw new Error(`Moonpay URL signing failed: ${response.status}`)
}
const reply = await response.json()
return asMoonpaySignResponse(reply).signedUrl
}
9 changes: 7 additions & 2 deletions src/plugins/ramps/moonpay/moonpayRampPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
RETURN_URL_PAYMENT,
validateExactRegion
} from '../../gui/providers/common'
import { signMoonpayUrl } from '../../gui/providers/moonpaySign'
import { addTokenToArray } from '../../gui/util/providerUtils'
import { rampDeeplinkManager } from '../rampDeeplinkHandler'
import type {
Expand Down Expand Up @@ -833,9 +834,10 @@ export const moonpayRampPlugin: RampPluginFactory = (
}
urlObj.set('query', queryObj)
console.log('Approving moonpay buy quote url=' + urlObj.href)
const signedUrl = await signMoonpayUrl(urlObj.href)

deeplinkToken = await openExternalWebView({
url: urlObj.href,
url: signedUrl,
deeplink: {
direction: 'buy',
providerId: pluginId,
Expand Down Expand Up @@ -997,9 +999,12 @@ export const moonpayRampPlugin: RampPluginFactory = (
let inPayment = false

const openWebView = async (): Promise<void> => {
// Re-sign on every open: IP-bound signatures must be fresh, and
// this path re-opens the widget on a failed/cancelled send.
const signedUrl = await signMoonpayUrl(urlObj.href)
await new Promise<void>((resolve, reject) => {
navigation.navigate('guiPluginWebView', {
url: urlObj.href,
url: signedUrl,
Comment thread
cursor[bot] marked this conversation as resolved.
onClose: () => {
resolve()
},
Expand Down
Loading