Skip to content
Merged
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
7 changes: 2 additions & 5 deletions packages/wallet/backend/src/quote/controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Amount, Quote } from '@/rafiki/backend/generated/graphql'
import { validate } from '@/shared/validate'
import type { NextFunction, Request } from 'express'
import { QuoteService } from './service'
Expand All @@ -7,12 +6,10 @@ import { Controller, toSuccessResponse } from '@shared/backend'
import { QuoteResponse } from '@wallet/shared'

interface IQuoteController {
create: Controller<Quote>
create: Controller<QuoteResponse>
}

export type QuoteWithFees = Quote & {
fee?: Amount
}
export type QuoteWithFees = QuoteResponse

export class QuoteController implements IQuoteController {
constructor(private quoteService: QuoteService) {}
Expand Down
107 changes: 60 additions & 47 deletions packages/wallet/backend/src/quote/service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { AccountService } from '@/account/service'
import { IncomingPaymentService } from '@/incomingPayment/service'
import { WalletAddress } from '@/walletAddress/model'
import { Quote } from '@/rafiki/backend/generated/graphql'
import { RafikiClient } from '@/rafiki/rafiki-client'
import { incomingPaymentRegexp, transformBalance } from '@/utils/helpers'
import { WalletAddressService } from '@/walletAddress/service'
import { QuoteWithFees } from './controller'
import { RatesService } from '@/rates/service'
import { BadRequest } from '@shared/backend'
import { AssetService } from '@/asset/service'
import { RhyzaAdminClient } from '@/rhyza/admin-client'
import { randomUUID } from 'crypto'

interface IQuoteService {
create: (params: CreateQuoteParams) => Promise<Quote>
create: (params: CreateQuoteParams) => Promise<QuoteWithFees>
}

type CreateQuoteParams = {
Expand All @@ -33,11 +32,10 @@ type ConvertParams = {
export class QuoteService implements IQuoteService {
constructor(
private accountService: AccountService,
private rafikiClient: RafikiClient,
private incomingPaymentService: IncomingPaymentService,
private ratesService: RatesService,
private walletAddressService: WalletAddressService,
private assetService: AssetService
private rhyzaAdminClient: RhyzaAdminClient
) {}

async create(params: CreateQuoteParams): Promise<QuoteWithFees> {
Expand All @@ -49,49 +47,48 @@ export class QuoteService implements IQuoteService {
throw new BadRequest('Invalid wallet address')
}

const { assetCode } = await this.accountService.findAccountById(
const { assetCode, assetScale } = await this.accountService.findAccountById(
existingWalletAddress.accountId,
params.userId
)

let asset = await this.assetService.getAssetByCode(assetCode)

const account = await this.accountService.getAccountById(
params.userId,
existingWalletAddress.accountId
)
const balance = account.balance

const originalValue = BigInt((params.amount * 10 ** asset.scale).toFixed())
const originalValue = BigInt((params.amount * 10 ** assetScale).toFixed())

if (Number(balance) < originalValue) {
throw new BadRequest('Not enough funds in account')
}

let value = originalValue

let paymentUrl = params.receiver

const isIncomingPayment = incomingPaymentRegexp.test(params.receiver)

let assetDetails
if (isIncomingPayment) {
const payment =
await this.incomingPaymentService.getExternalPayment(paymentUrl)
const payment = await this.incomingPaymentService.getExternalPayment(
params.receiver
)
assetDetails = {
assetCode: payment.receivedAmount.assetCode,
assetScale: payment.receivedAmount.assetScale
}
} else {
const walletAddress =
await this.walletAddressService.getExternalWalletAddress(paymentUrl)
await this.walletAddressService.getExternalWalletAddress(
params.receiver
)
assetDetails = {
assetCode: walletAddress.assetCode,
assetScale: walletAddress.assetScale
}
}

if (params.isReceive && assetDetails.assetCode !== asset.code) {
if (params.isReceive && assetDetails.assetCode !== assetCode) {
let convertedValue = await this.convert({
from: assetCode,
to: assetDetails.assetCode,
Expand All @@ -114,69 +111,85 @@ export class QuoteService implements IQuoteService {
}

value = convertedValue

asset = {
code: assetDetails.assetCode,
scale: assetDetails.assetScale
}
}

if (!isIncomingPayment) {
paymentUrl = await this.incomingPaymentService.createReceiver({
amount: params.isReceive ? value : null,
asset: {
code: assetDetails.assetCode,
scale: assetDetails.assetScale
},
walletAddressUrl: params.receiver,
description: params.description,
let paymentIntentType:
| { paymentIntentId: string }
| { paymentIntentUrl: string }

if (isIncomingPayment) {
paymentIntentType = { paymentIntentUrl: params.receiver }
} else {
const { id } = await this.rhyzaAdminClient.createPaymentIntent({
walletAddress: params.receiver,
incomingAmount: params.isReceive ? Number(value) : undefined,
expiresAt: params.isReceive
? undefined
: new Date(Date.now() + 1000 * 15),
vopNonce: params.vopNonce
: new Date(Date.now() + 1000 * 15).toISOString(),
metadata:
params.description || params.vopNonce
? { description: params.description, vopNonce: params.vopNonce }
: undefined
})
paymentIntentType = { paymentIntentId: id }
}

const amountParams = {
assetCode: asset.code,
assetScale: asset.scale,
value
}
const amountParams = params.isReceive
? { receiveAmount: value.toString() }
: { debitAmount: value.toString() }

const quote = await this.rafikiClient.createQuote({
walletAddressId: params.walletAddressId,
receiveAmount: params.isReceive ? amountParams : undefined,
receiver: paymentUrl,
debitAmount: params.isReceive ? undefined : amountParams
const rhyzaQuote = await this.rhyzaAdminClient.createQuote({
id: randomUUID(),
walletAddress: existingWalletAddress.url,
...amountParams,
...paymentIntentType
})

if (!rhyzaQuote.debitAmount || !rhyzaQuote.receiveAmount) {
throw new BadRequest('Could not calculate quote')
}

const quote: QuoteWithFees = {
id: rhyzaQuote.id,
debitAmount: {
assetCode,
assetScale,
value: rhyzaQuote.debitAmount
},
receiveAmount: {
...assetDetails,
value: rhyzaQuote.receiveAmount
}
}

return this.addConversionInfo(
quote,
params.isReceive ? originalValue : undefined
)
}

private async addConversionInfo(
quote: Quote,
quote: QuoteWithFees,
originalValue?: bigint
): Promise<QuoteWithFees | Quote> {
): Promise<QuoteWithFees> {
if (quote.receiveAmount.assetCode === quote.debitAmount.assetCode) {
return quote
}
const value = await this.convert({
from: quote.receiveAmount.assetCode,
to: quote.debitAmount.assetCode,
amount: quote.receiveAmount.value
amount: BigInt(quote.receiveAmount.value)
})

const feeInSenderCurrency = BigInt(quote.debitAmount.value) - value

return {
...quote,
fee: {
value: originalValue
value: (originalValue
? BigInt(quote.debitAmount.value) - originalValue
: feeInSenderCurrency,
: feeInSenderCurrency
).toString(),
assetScale: quote.debitAmount.assetScale,
assetCode: quote.debitAmount.assetCode
}
Expand Down
40 changes: 38 additions & 2 deletions packages/wallet/backend/src/rhyza/admin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
PaymentMetadata,
WalletAddress,
Payment,
Pagination
Pagination,
Quote
} from '@/rhyza/types'

interface AssetCreateResponse {
Expand All @@ -25,7 +26,7 @@ interface WalletAddressCreateResponse {

export interface CreatePaymentIntentArgs {
walletAddress: string
incomingAmount: number
incomingAmount?: number
expiresAt?: string
metadata?: PaymentMetadata
}
Expand Down Expand Up @@ -84,6 +85,23 @@ export interface GetPaymentIntentResponse {
destinationIlpAddress?: string
}

type QuoteAmount =
| { debitAmount: string; receiveAmount?: never }
| { receiveAmount: string; debitAmount?: never }

type QuoteCreate = {
id: string
walletAddress: string
} & QuoteAmount

export type QuoteCreateFromPaymentIntentId = QuoteCreate & {
paymentIntentId: string
}

export type QuoteCreateFromPaymentIntentUrl = QuoteCreate & {
paymentIntentUrl: string
}

export class RhyzaAdminClient {
constructor(private http: HttpClient) {}

Expand Down Expand Up @@ -161,4 +179,22 @@ export class RhyzaAdminClient {
`/wallet-addresses/${walletAddressId}/keys/${keyId}/revoke`
)
}

async createQuote(
args: QuoteCreateFromPaymentIntentId | QuoteCreateFromPaymentIntentUrl
): Promise<Quote> {
const response = await this.http.post<Quote>('/quotes', args)
return {
createdAt: response.createdAt,
id: response.id,
paymentIntentId: response.paymentIntentId,
senderWalletAddress: response.senderWalletAddress,
senderWalletAddressId: response.senderWalletAddressId,
state: response.state,
debitAmount: response.debitAmount,
exchangeRate: response.exchangeRate,
expiresAt: response.expiresAt,
receiveAmount: response.receiveAmount
}
}
}
16 changes: 16 additions & 0 deletions packages/wallet/backend/src/rhyza/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface WalletAddress {

export interface PaymentMetadata {
description?: string
vopNonce?: string
}

export type GrantState =
Expand Down Expand Up @@ -88,3 +89,18 @@ export interface Pagination {
hasNextPage: boolean
hasPreviousPage: boolean
}

export type QuoteState = 'PENDING' | 'FAILED' | 'COMPLETED'

export interface Quote {
createdAt: string
id: string
paymentIntentId: string
senderWalletAddress: string
senderWalletAddressId: string
state: QuoteState
debitAmount?: string
exchangeRate?: number
expiresAt?: string
receiveAmount?: string
}
Loading
Loading