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: 0 additions & 1 deletion apps/api/src/config/env.server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const envSchema = z.object({
])
.optional(),
INFURA_API_KEY: z.string(),
MERCURYO_SECRET_KEY: z.string(),
ALCHEMY_API_KEYS: z.string(),
COINGECKO_API_KEY: z.string(),
CRYPTOCOMPARE_API_KEYS: z.string(),
Expand Down
139 changes: 2 additions & 137 deletions apps/portfolio/src/app/_actions.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,18 @@
'use server'

import crypto from 'crypto'
import { match } from 'ts-pattern'

import { serverEnv } from '../config/env.server.mjs'
import { getAPIClient } from '../data/api'

import type { NetworkType } from '@status-im/wallet/data'

export type Provider = 'mercuryo' | 'moonpay'
export type Provider = 'moonpay'

type ApiInput = {
name: Provider
asset?: string
network?: string
address?: string
}

function getChainIdFromCode(chainCode: string): number | undefined {
return match(chainCode)
.with('ETHEREUM', () => 1)
.otherwise(() => undefined)
}

function convertIpfsUriToHttp(uri: string): string {
if (uri.startsWith('ipfs://')) {
const cid = uri.replace('ipfs://', '')
return `https://ipfs.io/ipfs/${cid}`
}
return uri
}

export async function handleCryptoOnRamp(input: ApiInput) {
const { name, network, asset, address } = input
const { name, address } = input

// Redirect to the provider's website
let redirectUrl = ''
Expand All @@ -45,124 +25,9 @@ export async function handleCryptoOnRamp(input: ApiInput) {
throw new Error('Address is required')
}

if (name === 'mercuryo') {
const baseUrl = `https://exchange.mercuryo.io/?type=buy&network=${network}&currency=${asset}&address=${address}&hide_address=false&fix_address=true&widget_id=6a7eb330-2b09-49b7-8fd3-1c77cfb6cd47`

const signature = crypto
.createHmac('sha512', serverEnv.MERCURYO_SECRET_KEY)
.update(new URL(baseUrl).search)
.digest('hex')

redirectUrl = `${baseUrl}&signature=${encodeURIComponent(signature)}`
}

return { url: redirectUrl }
}

type Icons = {
png: string
relative: { png: string; svg: string }
svg: string
}

type CryptoCurrency = {
contract: string
currency: string
network: string
network_label: string
show_network_icon: boolean
}

type Response = {
data: {
config: {
base: Record<string, string>
crypto_currencies: CryptoCurrency[]
default_networks: Record<string, string>
display_options: Record<
string,
{ fullname: string; total_digits: number; display_digits: number }
>
has_withdrawal_fee: Record<string, boolean>
icons: Record<string, Icons>
networks: Record<string, { name: string; icons: Icons }>
}
crypto: string[]
fiat: string[]
}
status: number
}

export type Currency = {
contract_address: string
code: string
label: string
network: string
imageUrl: string
}

export const getSupportedCurrencies = async (): Promise<Currency[]> => {
// Fetch the data from Mercuryo and cache it for 24 hours
const response = await fetch('https://api.mercuryo.io/v1.6/lib/currencies', {
next: {
revalidate: 86400, // 24 hours
},
})

const data: Response = await response.json()

if (data.status !== 200) {
throw new Error('Failed to fetch data from Mercuryo')
}

const supportedCurrencies = data.data.config.crypto_currencies
.filter(currency => {
// Get the chain ID from the network (e.g., 'ETHEREUM', 'ARBITRUM')
const chainId = getChainIdFromCode(currency.network)

if (currency.currency === 'ETH' && !!chainId) {
return true
}

// Find matching Mercuryo token by contract address and network chain ID.
return ([] as any).some((uniswapToken: any) => {
// Check if token is either Ethereum or the contract address matches
return (
chainId === uniswapToken.chainId &&
uniswapToken.address.toLowerCase() === currency.contract.toLowerCase()
)
})
})
.map(currency => {
const tokenFromUniswap = ([] as any).find((uniswapToken: any) => {
return (
uniswapToken.address.toLowerCase() === currency.contract.toLowerCase()
)
})

// If the currency is ETH, return the Ether object because it's not in the Uniswap tokens list
if (currency.currency === 'ETH') {
return {
contract_address: currency.contract,
code: currency.currency,
label: 'Ether',
network: currency.network,
imageUrl: '/images/tokens/eth.png',
}
}

return {
contract_address: tokenFromUniswap?.address || '',
code: tokenFromUniswap?.symbol || '',
label: tokenFromUniswap?.name || '',
network: currency.network || '',
imageUrl: convertIpfsUriToHttp(tokenFromUniswap?.imageUrl || ''),
}
})

return supportedCurrencies
}

export async function getAccountsData(
addresses: string[],
networks: NetworkType[]
Expand Down
136 changes: 111 additions & 25 deletions apps/portfolio/src/app/_components/buy-crypto-drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,135 @@
'use client'

import {
BuyCryptoDrawer as BuyCryptoDrawerBase,
type Provider,
} from '@status-im/wallet/components'
import { useState } from 'react'

import { Avatar, useToast } from '@status-im/components'
import { FeesIcon } from '@status-im/icons/12'
import { ExternalIcon } from '@status-im/icons/20'
import NextImage from 'next/image'

import { handleCryptoOnRamp } from '../_actions'
import { useCurrentAccount } from '../_hooks/use-current-account'
import * as Drawer from './drawer'

import type { Provider } from '../_actions'

// Todo: pass images to cloudinary?
const PROVIDERS = [
{
name: 'moonpay',
description: 'The new standard for fiat to crypto',
fee: '1% - 4.5%',
image: '/images/providers/moonpay.png',
},
] as const

type Props = {
children: React.ReactElement
}

export const BuyCryptoDrawer = (props: Props) => {
const { children } = props
const [open, setOpen] = useState(false)

const account = useCurrentAccount()
const toast = useToast()

const handleProviderSelect = async (
provider: Provider,
network: string,
asset?: string
) => {
return handleCryptoOnRamp({
name: provider,
network,
address: account.address,
asset: asset || 'USD',
})
}
const handleProviderSelect = async (provider: Provider) => {
const openProviderUrl = (url: string) => {
const newTab = window.open(url, '_blank', 'noopener,noreferrer')
if (!newTab) {
toast.negative(
'Unable to open a new tab. Please check your browser settings.'
)
}
}

const handleOpenTab = (url: string) => {
window.open(url, '_blank', 'noopener,noreferrer')
try {
const data = await handleCryptoOnRamp({
name: provider,
address: account.address,
})

openProviderUrl(data.url)
} catch {
toast.negative(
'Unable to open a new tab. Please check your browser settings.'
)
}
}

if (!account) {
return null
}

return (
<BuyCryptoDrawerBase
account={account}
onProviderSelect={handleProviderSelect}
onOpenTab={handleOpenTab}
>
{children}
</BuyCryptoDrawerBase>
<Drawer.Root modal open={open} onOpenChange={setOpen}>
<Drawer.Trigger asChild>{children}</Drawer.Trigger>

<Drawer.Content className="p-3">
<Drawer.Header className="sticky top-1 flex flex-col gap-2 bg-white-60 backdrop-blur-[20px]">
<Drawer.Title className="!pb-0 pl-1">Buy crypto</Drawer.Title>

<div className="flex items-center gap-2">
<div
className="inline-flex h-6 items-center gap-1 rounded-8 border bg-neutral-10 pl-px pr-2"
data-customisation={account.color}
>
<div className="rounded-6 bg-white-100">
<Avatar
type="account"
name={account.name}
emoji={account.emoji}
size="20"
bgOpacity="20"
/>
</div>
<span className="text-13 font-medium text-neutral-100">
{account.name}
</span>
</div>
</div>
</Drawer.Header>

<Drawer.Body className="relative flex flex-col overflow-clip">
<div className="mt-2 flex flex-col gap-0.5 rounded-16 border border-neutral-10 bg-neutral-2.5 p-1">
{PROVIDERS.map(provider => (
<button
key={provider.name}
className="flex w-full cursor-pointer items-center justify-between gap-4 rounded-12 px-2 py-1 transition-colors hover:bg-neutral-5"
onClick={() => handleProviderSelect(provider.name)}
>
<div className="flex items-center gap-2">
<NextImage
src={provider.image}
alt={provider.name}
width={32}
height={32}
className="size-8 rounded-full"
/>
<div className="flex flex-col">
<div className="flex flex-col items-start">
<div className="text-15 font-600 capitalize">
{provider.name}
</div>
<div className="text-13 text-neutral-50">
{provider.description}
</div>
</div>
</div>
</div>

<div className="flex items-center gap-4">
<div className="flex items-center gap-1 text-13 font-500">
<FeesIcon /> {provider.fee}
</div>
<ExternalIcon />
</div>
</button>
))}
</div>
</Drawer.Body>
</Drawer.Content>
</Drawer.Root>
)
}
1 change: 0 additions & 1 deletion apps/portfolio/src/config/env.server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export const envSchema = z.object({
COINGECKO_API_KEY: z.string(),
CRYPTOCOMPARE_API_KEYS: z.string(),
PORT: z.coerce.number().optional(),
MERCURYO_SECRET_KEY: z.string(),
})

const result = envSchema.safeParse(process.env)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ Status Software only shares the above personal data when the user explicitly nav

For more information about the on-ramp providers, their APIs and widgets, and privacy policies, please see below:

#### Mercuryo

- [https://uk.mercuryo.io/on-off-ramps/](https://uk.mercuryo.io/on-off-ramps/)
- [https://help.mercuryo.io/hc/en-gb/articles/14833411947037-How-does-the-Mercuryo-widget-work-for-on-ramp](https://help.mercuryo.io/hc/en-gb/articles/14833411947037-How-does-the-Mercuryo-widget-work-for-on-ramp)
- [https://help.mercuryo.io/hc/en-gb/articles/14495463995805-How-does-Mercuryo-keep-my-information-safe](https://help.mercuryo.io/hc/en-gb/articles/14495463995805-How-does-Mercuryo-keep-my-information-safe)

#### Moonpay

- [https://www.moonpay.com/en-gb](https://www.moonpay.com/en-gb)
Expand Down
4 changes: 2 additions & 2 deletions apps/status.app/content/help/wallet/buy-crypto.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ In Status Wallet, you can buy crypto with fiat currencies such as USD, EUR and o

### Are crypto purchases handled by Status?

No. When you're buying crypto in Status, you're buying from our partners through their sites. If you have any issues or questions, please contact the support team of the partner you bought crypto from (Mercuryo, Ramp or MoonPay).
No. When you're buying crypto in Status, you're buying from our partners through their sites. If you have any issues or questions, please contact the support team of the partner you bought crypto from (Ramp or MoonPay).

### Why did I receive a different amount than I expected?

An asset exchange rate is always changing and fluctuating, and blockchain transactions take some time to be processed. If the network is particularly busy while you're placing your order, it can take longer for your transaction to go through.

This can sometimes lead to you receiving more or less than the amount you were expecting. If you have any issues or questions, please contact the support team of the partner you bought crypto from (Mercuryo, Ramp or MoonPay).
This can sometimes lead to you receiving more or less than the amount you were expecting. If you have any issues or questions, please contact the support team of the partner you bought crypto from (Ramp or MoonPay).

### Does Status charge any fees for buying crypto?

Expand Down
1 change: 0 additions & 1 deletion apps/wallet/wxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export default defineConfig({
INFURA_API_KEY: 'test',
CRYPTOCOMPARE_API_KEYS: 'test',
COINGECKO_API_KEY: 'test',
MERCURYO_SECRET_KEY: 'test',
VERCEL: 'test',
VERCEL_ENV: 'test',
},
Expand Down
Loading
Loading