From 89b1ca5f730be24e390c7f9b5bb7566110d976d2 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Sun, 23 Feb 2025 22:43:12 -0500 Subject: [PATCH 01/14] feat: cache fees --- packages/backend/src/fee/service.ts | 28 +++++++++++++++++++++++----- packages/backend/src/index.ts | 3 ++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/fee/service.ts b/packages/backend/src/fee/service.ts index 4979cb18a4..e5c6e64426 100644 --- a/packages/backend/src/fee/service.ts +++ b/packages/backend/src/fee/service.ts @@ -3,6 +3,7 @@ import { BaseService } from '../shared/baseService' import { FeeError } from './errors' import { Fee, FeeType } from './model' import { Pagination, SortOrder } from '../shared/baseModel' +import { CacheDataStore } from '../middleware/cache/data-stores' export interface CreateOptions { assetId: string @@ -23,17 +24,21 @@ export interface FeeService { getLatestFee(assetId: string, type: FeeType): Promise } -type ServiceDependencies = BaseService +interface ServiceDependencies extends BaseService { + feeCache: CacheDataStore +} export async function createFeeService({ logger, - knex + knex, + feeCache }: ServiceDependencies): Promise { const deps: ServiceDependencies = { logger: logger.child({ service: 'FeeService' }), - knex + knex, + feeCache } return { create: (options: CreateOptions) => createFee(deps, options), @@ -65,10 +70,20 @@ async function getLatestFee( assetId: string, type: FeeType ): Promise { - return await Fee.query(deps.knex) + const cachedFee = await deps.feeCache.get(`${assetId}${type}`) + + if (cachedFee) { + return cachedFee + } + + const latestFee = await Fee.query(deps.knex) .where({ assetId, type }) .orderBy('createdAt', 'desc') .first() + + if (latestFee) await deps.feeCache.set(`${assetId}${type}`, latestFee) + + return latestFee } async function createFee( @@ -86,12 +101,15 @@ async function createFee( } try { - return await Fee.query(deps.knex).insertAndFetch({ + const fee = await Fee.query(deps.knex).insertAndFetch({ assetId: assetId, type: type, basisPointFee: basisPoints, fixedFee: fixed }) + + await deps.feeCache.set(`${assetId}${type}`, fee) + return fee } catch (error) { if (error instanceof ForeignKeyViolationError) { return FeeError.UnknownAsset diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 91db346566..7267399a7e 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -419,7 +419,8 @@ export function initIocContainer( const knex = await deps.use('knex') return await createFeeService({ logger: logger, - knex: knex + knex: knex, + feeCache: createInMemoryDataStore(config.localCacheDuration) }) }) From 4de0a4ccbc6ccf21a561af7dbfd865dd6513ddd5 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Sun, 23 Feb 2025 22:45:02 -0500 Subject: [PATCH 02/14] feat: fix deadlocks - fixes deadlocks by ensuring we dont open new connections before resolving open transactions - forced knex connections to 1 to help find these cases --- .../open_payments/payment/outgoing/service.ts | 266 ++++++++++++- .../src/open_payments/quote/service.ts | 373 +++++++++++++++++- 2 files changed, 636 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 54ede1accb..fefbc53d67 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -44,6 +44,7 @@ import { Pagination, SortOrder } from '../../../shared/baseModel' import { FilterString } from '../../../shared/filters' import { IAppConfig } from '../../../config/app' import { AssetService } from '../../../asset/service' +import { Span, trace } from '@opentelemetry/api' export interface OutgoingPaymentService extends WalletAddressSubresourceService { @@ -83,7 +84,8 @@ export async function createOutgoingPaymentService( return { get: (options) => getOutgoingPayment(deps, options), getPage: (options) => getOutgoingPaymentsPage(deps, options), - create: (options) => createOutgoingPayment(deps, options), + // create: (options) => createOutgoingPayment(deps, options), + create: (options) => createOutgoingPayment2(deps, options), cancel: (options) => cancelOutgoingPayment(deps, options), fund: (options) => fundPayment(deps, options), processNext: () => worker.processPendingPayment(deps), @@ -454,6 +456,268 @@ async function createOutgoingPayment( } } +async function createOutgoingPayment2( + deps: ServiceDependencies, + options: CreateOutgoingPaymentOptions +): Promise { + const tracer = trace.getTracer('outgoing_payment_service_create') + + return tracer.startActiveSpan( + 'outgoingPaymentService.createOutgoingPayment', + async (span: Span) => { + const stopTimerOP = deps.telemetry.startTimer( + 'outgoing_payment_service_create_time_ms', + { + callName: 'OutgoingPaymentService:create', + description: 'Time to create an outgoing payment' + } + ) + const { walletAddressId } = options + let quoteId: string + + if (isCreateFromIncomingPayment(options)) { + const stopTimerQuote = deps.telemetry.startTimer( + 'outgoing_payment_service_create_quote_time_ms', + { + callName: 'QuoteService:create', + description: 'Time to create a quote in outgoing payment' + } + ) + const { debitAmount, incomingPayment } = options + const quoteOrError = await deps.quoteService.create({ + receiver: incomingPayment, + debitAmount, + method: 'ilp', + walletAddressId + }) + stopTimerQuote() + + if (isQuoteError(quoteOrError)) { + return quoteErrorToOutgoingPaymentError[quoteOrError] + } + quoteId = quoteOrError.id + } else { + quoteId = options.quoteId + } + + const grantId = options.grant?.id + try { + const stopTimerWA = deps.telemetry.startTimer( + 'outgoing_payment_service_getwalletaddress_time_ms', + { + callName: 'WalletAddressService:get', + description: 'Time to get wallet address in outgoing payment' + } + ) + const walletAddress = + await deps.walletAddressService.get(walletAddressId) + stopTimerWA() + if (!walletAddress) { + throw OutgoingPaymentError.UnknownWalletAddress + } + if (!walletAddress.isActive) { + throw OutgoingPaymentError.InactiveWalletAddress + } + + const quote = await deps.quoteService.get({ id: quoteId }) + if (!quote) { + return OutgoingPaymentError.UnknownQuote + } + + const asset = await deps.assetService.get(quote.assetId) + + const stopTimerReceiver = deps.telemetry.startTimer( + 'outgoing_payment_service_getreceiver_time_ms', + { + callName: 'ReceiverService:get', + description: 'Time to retrieve receiver in outgoing payment' + } + ) + + const receiver = await deps.receiverService.get(quote.receiver) + stopTimerReceiver() + if (!receiver) { + throw OutgoingPaymentError.InvalidQuote + } + const stopTimerPeer = deps.telemetry.startTimer( + 'outgoing_payment_service_getpeer_time_ms', + { + callName: 'PeerService:getByDestinationAddress', + description: 'Time to retrieve peer in outgoing payment' + } + ) + const peer = await deps.peerService.getByDestinationAddress( + receiver.ilpAddress + ) + stopTimerPeer() + + // TODO: Fixes use of trx. To avoid deadlock. + // Deadlock happened when we tried to open a new connection inside + // an Objection transaction block while at max connections already. + // This deadlocks because transaction waits for callback to finish, + // and query in callback waits for connection (ie the transaction + // to finish). Deadlock. + + // Must either: + // - move non-trx db calls OUT of the transaction + // - or use the trx in each db call + // + // Moving out is good but we need to make sure we can safely do that and not introduce data + // inconsistency. I gnerally opted for passing the trx in and not moving stuff out. Felt + // like it was safer and more straightforward fix. However, we should move anything we + // SAFELY can out of the transaction. + + // Moved several things outside transaction... shoudl double check its OK. + // IE, we have the quote id, cant we fetch it before inserting the outgoing payment? + // Also unblocks fetching otehr stuff like peer, asset. + + // *** 1. Begin transaction. fast + const payment = await OutgoingPayment.transaction(async (trx) => { + // return await OutgoingPayment.transaction(deps.knex, async (trx) => { + if (grantId) { + const stopTimerGrant = deps.telemetry.startTimer( + 'outgoing_payment_service_insertgrant_time_ms', + { + callName: 'OutgoingPaymentGrantModel:insert', + description: 'Time to insert grant in outgoing payment' + } + ) + await OutgoingPaymentGrant.query(trx) + .insert({ + id: grantId + }) + .onConflict('id') + .ignore() + stopTimerGrant() + } + const stopTimerInsertPayment = deps.telemetry.startTimer( + 'outgoing_payment_service_insertpayment_time_ms', + { + callName: 'OutgoingPayment.insert', + description: 'Time to insert payment in outgoing payment' + } + ) + + const payment = await OutgoingPayment.query(trx).insertAndFetch({ + id: quoteId, + walletAddressId: walletAddressId, + client: options.client, + metadata: options.metadata, + state: OutgoingPaymentState.Funding, + grantId + }) + payment.walletAddress = walletAddress + payment.quote = quote + if (asset) payment.quote.asset = asset + + stopTimerInsertPayment() + + if ( + payment.walletAddressId !== payment.quote.walletAddressId || + payment.quote.expiresAt.getTime() <= payment.createdAt.getTime() + ) { + throw OutgoingPaymentError.InvalidQuote + } + + if (options.grant) { + const stopTimerValidateGrant = deps.telemetry.startTimer( + 'outgoing_payment_service_validate_grant_time_ms', + { + callName: + 'OutgoingPaymentService:validateGrantAndAddSpentAmountsToPayment', + description: 'Time to validate a grant' + } + ) + const isValid = await validateGrantAndAddSpentAmountsToPayment( + deps, + payment, + options.grant, + trx, + options.callback, + options.grantLockTimeoutMs + ) + stopTimerValidateGrant() + if (!isValid) { + throw OutgoingPaymentError.InsufficientGrant + } + } + + const stopTimerPeerUpdate = deps.telemetry.startTimer( + 'outgoing_payment_service_patchpeer_time_ms', + { + callName: 'OutgoingPaymentModel:patch', + description: 'Time to patch peer in outgoing payment' + } + ) + if (peer) await payment.$query(trx).patch({ peerId: peer.id }) + stopTimerPeerUpdate() + + const stopTimerWebhook = deps.telemetry.startTimer( + 'outgoing_payment_service_webhook_event_time_ms', + { + callName: 'OutgoingPaymentService:sendWebhookEvent', + description: 'Time to add outgoing payment webhook event' + } + ) + await sendWebhookEvent( + deps, + payment, + OutgoingPaymentEventType.PaymentCreated, + trx + ) + stopTimerWebhook() + + return payment + }) + + const stopTimerAddAmount = deps.telemetry.startTimer( + 'outgoing_payment_service_add_sent_time_ms', + { + callName: 'OutgoingPaymentService:addSentAmount', + description: 'Time to add sent amount to outgoing payment' + } + ) + + const paymentWithSentAmount = await addSentAmount( + deps, + payment, + BigInt(0) + ) + + stopTimerAddAmount() + + return paymentWithSentAmount + } catch (err) { + if (err instanceof UniqueViolationError) { + if (err.constraint === 'outgoingPayments_pkey') { + return OutgoingPaymentError.InvalidQuote + } + } else if (err instanceof ForeignKeyViolationError) { + if (err.constraint === 'outgoingpayments_id_foreign') { + return OutgoingPaymentError.UnknownQuote + } else if ( + err.constraint === 'outgoingpayments_walletaddressid_foreign' + ) { + return OutgoingPaymentError.UnknownWalletAddress + } + } else if (isOutgoingPaymentError(err)) { + return err + } else if (err instanceof knex.KnexTimeoutError) { + deps.logger.error( + { grant: grantId }, + 'Could not create outgoing payment: grant locked' + ) + } + console.log('error when creating op', { err }) + throw err + } finally { + stopTimerOP() + span.end() + } + } + ) +} + function validateAccessLimits( payment: OutgoingPayment, limits: Limits diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 957c4065f4..8b68c9f8ed 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -14,7 +14,7 @@ import { import { PaymentMethodHandlerService } from '../../payment-method/handler/service' import { IAppConfig } from '../../config/app' import { FeeService } from '../../fee/service' -import { FeeType } from '../../fee/model' +import { Fee, FeeType } from '../../fee/model' import { PaymentMethodHandlerError, PaymentMethodHandlerErrorCode @@ -47,7 +47,8 @@ export async function createQuoteService( } return { get: (options) => getQuote(deps, options), - create: (options: CreateQuoteOptions) => createQuote(deps, options), + // create: (options: CreateQuoteOptions) => createQuote(deps, options), + create: (options: CreateQuoteOptions) => createQuote2(deps, options), getWalletAddressPage: (options) => getWalletAddressPage(deps, options) } } @@ -247,6 +248,222 @@ async function createQuote( } } +// - get fee before creation (getFeeById(sendingFee?.id)) +// - initialize obj for insert which is the current object but +// - call finalizeQuote with the object for insert and return the stuff thats currently patched (the amounts and expiry) +// - merge those things into the quote input and create the quote. + +interface UnfinalizedQuote { + id: string + walletAddressId: string + assetId: string + receiver: string + debitAmount: Amount + receiveAmount: Amount + client: string | undefined + feeId: string | undefined + fee: Fee | undefined + estimatedExchangeRate: number +} + +async function createQuote2( + deps: ServiceDependencies, + options: CreateQuoteOptions +): Promise { + const stopTimer = deps.telemetry.startTimer('quote_service_create_time_ms', { + callName: 'QuoteService:create', + description: 'Time to create a quote' + }) + if (options.debitAmount && options.receiveAmount) { + stopTimer() + return QuoteError.InvalidAmount + } + const walletAddress = await deps.walletAddressService.get( + options.walletAddressId + ) + if (!walletAddress) { + stopTimer() + return QuoteError.UnknownWalletAddress + } + if (!walletAddress.isActive) { + stopTimer() + return QuoteError.InactiveWalletAddress + } + if (options.debitAmount) { + if ( + options.debitAmount.value <= BigInt(0) || + options.debitAmount.assetCode !== walletAddress.asset.code || + options.debitAmount.assetScale !== walletAddress.asset.scale + ) { + stopTimer() + return QuoteError.InvalidAmount + } + } + if (options.receiveAmount) { + if (options.receiveAmount.value <= BigInt(0)) { + stopTimer() + return QuoteError.InvalidAmount + } + } + + try { + const stopTimerReceiver = deps.telemetry.startTimer( + 'quote_service_create_resolve_receiver_time_ms', + { + callName: 'QuoteService:resolveReceiver', + description: 'Time to resolve receiver' + } + ) + const receiver = await resolveReceiver(deps, options) + stopTimerReceiver() + + const paymentMethod = receiver.isLocal ? 'LOCAL' : 'ILP' + const quoteId = uuid() + + const stopTimerFee = deps.telemetry.startTimer( + 'quote_service_create_get_latest_fee_time_ms', + { + callName: 'FeeService:getLatestFee', + description: 'Time to getLatestFee' + } + ) + const sendingFee = await deps.feeService.getLatestFee( + walletAddress.assetId, + FeeType.Sending + ) + stopTimerFee() + + const stopTimerQuote = deps.telemetry.startTimer( + 'quote_service_create_get_quote_time_ms', + { + callName: 'PaymentMethodHandlerService:getQuote', + description: 'Time to getQuote' + } + ) + + // TODO: should getQuote happen inside trx? wasnt in main (was inside but not using trx). + // If so, in the getQuote method, need to not only pass into IlpQuoteDetails but also connector. + // Probably should have IlpQuoteDetails usingt he trx but not sure about the rest (just + // including the IlpQuoteDetails insert would prly require refactor to to that here) + const quote = await deps.paymentMethodHandlerService.getQuote( + paymentMethod, + { + quoteId, + walletAddress, + receiver, + receiveAmount: options.receiveAmount, + debitAmount: options.debitAmount + } + // trx + ) + stopTimerQuote() + + return await Quote.transaction(async (trx) => { + const stopTimerQuoteTrx = deps.telemetry.startTimer( + 'quote_service_create_get_quote_time_ms', + { + callName: 'QuoteService:create:transaction', + description: 'Time to complete quote transaction' + } + ) + // const stopTimerQuote = deps.telemetry.startTimer( + // 'quote_service_create_get_quote_time_ms', + // { + // callName: 'PaymentMethodHandlerService:getQuote', + // description: 'Time to getQuote' + // } + // ) + + // // TODO: rm getQuote from this trx and change to return IlpQuoteDetails + // // instead to take the connector network calls out of th trx? + + // console.log('calling paymentMethodHandlerService.getQuote wtih', { + // quoteId, + // walletAddress, + // receiver, + // receiveAmount: options.receiveAmount, + // debitAmount: options.debitAmount + // }) + // const quote = await deps.paymentMethodHandlerService.getQuote( + // paymentMethod, + // { + // quoteId, + // walletAddress, + // receiver, + // receiveAmount: options.receiveAmount, + // debitAmount: options.debitAmount + // }, + // trx + // ) + // console.log('getQuote finished') + // stopTimerQuote() + + const unfinalizedQuote: UnfinalizedQuote = { + id: quoteId, + walletAddressId: options.walletAddressId, + assetId: walletAddress.assetId, + receiver: options.receiver, + debitAmount: quote.debitAmount, + receiveAmount: quote.receiveAmount, + client: options.client, + feeId: sendingFee?.id, + fee: sendingFee, + estimatedExchangeRate: quote.estimatedExchangeRate + } + + const stopFinalize = deps.telemetry.startTimer( + 'quote_service_finalize_quote_ms', + { + callName: 'QuoteService:finalizedQuote', + description: 'Time to finalize quote' + } + ) + const patchOptions = await finalizeQuote2( + deps, + options, + unfinalizedQuote, + receiver + ) + stopFinalize() + + const stopQuoteCreate = deps.telemetry.startTimer( + 'quote_service_create_insert_time_ms', + { + callName: 'QuoteModel.insert', + description: 'Time to insert quote' + } + ) + const createdQuote = await Quote.query(trx).insertAndFetch({ + ...unfinalizedQuote, + ...patchOptions + }) + createdQuote.asset = walletAddress.asset + createdQuote.walletAddress = walletAddress + if (sendingFee) createdQuote.fee = sendingFee + + stopQuoteCreate() + stopTimerQuoteTrx() + return createdQuote + }) + } catch (err) { + if (isQuoteError(err)) { + return err + } + + if ( + err instanceof PaymentMethodHandlerError && + err.code === PaymentMethodHandlerErrorCode.QuoteNonPositiveReceiveAmount + ) { + return QuoteError.NonPositiveReceiveAmount + } + + deps.logger.error({ err }, 'error creating a quote') + throw err + } finally { + stopTimer() + } +} + export async function resolveReceiver( deps: ServiceDependencies, options: CreateQuoteOptions @@ -347,6 +564,57 @@ function calculateFixedSendQuoteAmounts( } } +function calculateFixedSendQuoteAmounts2( + deps: ServiceDependencies, + quote: UnfinalizedQuote, + maxReceiveAmountValue: bigint +): CalculateQuoteAmountsWithFeesResult { + // TODO: derive fee from debitAmount instead? Current behavior/tests may be wrong with basis point fees. + const fees = quote.fee?.calculate(quote.receiveAmount.value) ?? BigInt(0) + + const { estimatedExchangeRate } = quote + + const exchangeAdjustedFees = BigInt( + Math.ceil(Number(fees) * estimatedExchangeRate) + ) + const receiveAmountValue = + BigInt(quote.receiveAmount.value) - exchangeAdjustedFees + + if (receiveAmountValue <= BigInt(0)) { + deps.logger.info( + { fees, exchangeAdjustedFees, estimatedExchangeRate, receiveAmountValue }, + 'Negative receive amount when calculating quote amount' + ) + throw QuoteError.NonPositiveReceiveAmount + } + + if (receiveAmountValue > maxReceiveAmountValue) { + throw QuoteError.InvalidAmount + } + + const debitAmountMinusFees = + quote.debitAmount.value - + (quote.fee?.calculate(quote.debitAmount.value) ?? 0n) + + deps.logger.debug( + { + 'quote.receiveAmount.value': quote.receiveAmount.value, + debitAmountValue: quote.debitAmount.value, + debitAmountMinusFees, + receiveAmountValue, + fees, + exchangeAdjustedFees + }, + 'Calculated fixed-send quote amount with fees' + ) + + return { + debitAmountValue: quote.debitAmount.value, + debitAmountMinusFees, + receiveAmountValue + } +} + /** * Calculate fixed-delivery quote amounts: receiveAmount is locked, * add fees to the the debitAmount. @@ -397,6 +665,64 @@ function calculateExpiry( : quoteExpiry } +function calculateExpiry2( + deps: ServiceDependencies, + quote: UnfinalizedQuote, + receiver: Receiver +): Date { + const quoteExpiry = new Date( + // quote.createdAt.getTime() + deps.config.quoteLifespan + Date.now() + deps.config.quoteLifespan + ) + + const incomingPaymentExpiresEarlier = + receiver.incomingPayment?.expiresAt && + receiver.incomingPayment.expiresAt.getTime() < quoteExpiry.getTime() + + return incomingPaymentExpiresEarlier + ? receiver.incomingPayment!.expiresAt! + : quoteExpiry +} + +interface QuotePatchOptions { + debitAmountMinusFees: bigint + debitAmountValue: bigint + receiveAmountValue: bigint + expiresAt: Date +} + +/** + * Calculate fixed-delivery quote amounts: receiveAmount is locked, + * add fees to the the debitAmount. + */ +function calculateFixedDeliveryQuoteAmounts2( + deps: ServiceDependencies, + quote: UnfinalizedQuote +): CalculateQuoteAmountsWithFeesResult { + const fees = quote.fee?.calculate(quote.debitAmount.value) ?? BigInt(0) + + const debitAmountValue = BigInt(quote.debitAmount.value) + fees + + if (debitAmountValue <= BigInt(0)) { + deps.logger.info( + { fees, debitAmountValue }, + 'Received negative debitAmount receive amount when calculating quote amount' + ) + throw QuoteError.InvalidAmount + } + + deps.logger.debug( + { debitAmountValue, receiveAmountValue: quote.receiveAmount.value, fees }, + `Calculated fixed-delivery quote amount with fees` + ) + + return { + debitAmountValue, + debitAmountMinusFees: quote.debitAmount.value, + receiveAmountValue: quote.receiveAmount.value + } +} + async function finalizeQuote( deps: ServiceDependencies, options: CreateQuoteOptions, @@ -442,6 +768,49 @@ async function finalizeQuote( return quote } +async function finalizeQuote2( + deps: ServiceDependencies, + options: CreateQuoteOptions, + quote: UnfinalizedQuote, + receiver: Receiver +): Promise { + let maxReceiveAmountValue: bigint | undefined + + if (options.debitAmount) { + const receivingPaymentValue = + receiver.incomingAmount && receiver.receivedAmount + ? receiver.incomingAmount.value - receiver.receivedAmount.value + : undefined + maxReceiveAmountValue = + receivingPaymentValue && receivingPaymentValue < quote.receiveAmount.value + ? receivingPaymentValue + : quote.receiveAmount.value + } + + deps.logger.debug( + { + debitAmountValue: quote.debitAmount.value, + receiveAmountValue: quote.receiveAmount.value, + maxReceiveAmountValue + }, + `Calculating ${maxReceiveAmountValue ? 'fixed-send' : 'fixed-delivery'} quote amount with fees` + ) + + const { debitAmountValue, debitAmountMinusFees, receiveAmountValue } = + maxReceiveAmountValue + ? calculateFixedSendQuoteAmounts2(deps, quote, maxReceiveAmountValue) + : calculateFixedDeliveryQuoteAmounts2(deps, quote) + + const patchOptions = { + debitAmountMinusFees, + debitAmountValue, + receiveAmountValue, + expiresAt: calculateExpiry2(deps, quote, receiver) + } + + return patchOptions +} + async function getWalletAddressPage( deps: ServiceDependencies, options: ListOptions From cddcc34803260413ca712c1c9bd782db80390e31 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Sun, 23 Feb 2025 22:45:45 -0500 Subject: [PATCH 03/14] feat: add local payment test script --- .../scripts/create-local-outgoing-payments.js | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 test/performance/scripts/create-local-outgoing-payments.js diff --git a/test/performance/scripts/create-local-outgoing-payments.js b/test/performance/scripts/create-local-outgoing-payments.js new file mode 100644 index 0000000000..beebfcb03b --- /dev/null +++ b/test/performance/scripts/create-local-outgoing-payments.js @@ -0,0 +1,167 @@ +import http from 'k6/http' +import { fail } from 'k6' +import { createHMAC } from 'k6/crypto' +import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js' +import { canonicalize } from '../dist/json-canonicalize.bundle.js' + +export const options = { + vus: 50, + duration: '120s' + // vus: 1, + // iterations: 1 +} + +// const CLOUD_NINE_GQL_ENDPOINT = 'http://cloud-nine-wallet-backend:3001/graphql' +// const CLOUD_NINE_WALLET_ADDRESS = +// 'https://cloud-nine-wallet-backend/accounts/gfranklin' +// const HAPPY_LIFE_BANK_WALLET_ADDRESS = +// 'https://happy-life-bank-backend/accounts/pfry' + +const GQL_ENDPOINT = 'http://cloud-nine-wallet-backend:3001/graphql' +const SENDER_WALLET_ADDRESS = + 'https://cloud-nine-wallet-backend/accounts/gfranklin' +const RECEIVER_WALLET_ADDRESS = + 'https://cloud-nine-wallet-backend/accounts/bhamchest' +const SIGNATURE_SECRET = 'iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=' +const SIGNATURE_VERSION = '1' + +function generateSignedHeaders(requestPayload) { + const timestamp = Date.now() + const payload = `${timestamp}.${canonicalize(requestPayload)}` + const hmac = createHMAC('sha256', SIGNATURE_SECRET) + hmac.update(payload) + const digest = hmac.digest('hex') + + return { + 'Content-Type': 'application/json', + signature: `t=${timestamp}, v${SIGNATURE_VERSION}=${digest}, n=${uuidv4()}` + } +} + +function request(query) { + const headers = generateSignedHeaders(query) + const response = http.post(GQL_ENDPOINT, JSON.stringify(query), { + headers + }) + + if (response.status !== 200) { + fail(`GraphQL Request failed`) + } + return JSON.parse(response.body).data +} + +export function setup() { + const query = { + query: ` + query GetWalletAddresses { + walletAddresses { + edges { + node { + id + url + } + } + } + } + ` + } + + const data = request(query) + const c9WalletAddresses = data.walletAddresses.edges + const c9WalletAddress = c9WalletAddresses.find( + (edge) => edge.node.url === SENDER_WALLET_ADDRESS + ).node + if (!c9WalletAddress) { + fail(`could not find wallet address: ${SENDER_WALLET_ADDRESS}`) + } + + return { data: { c9WalletAddress } } +} + +// The function that defines VU logic. +// +// See https://grafana.com/docs/k6/latest/examples/get-started-with-k6/ to learn more +// about authoring k6 scripts. +// +export default function (data) { + const { + data: { c9WalletAddress } + } = data + + const createReceiverPayload = { + query: ` + mutation CreateReceiver($input: CreateReceiverInput!) { + createReceiver(input: $input) { + receiver { + id + } + } + } + `, + variables: { + input: { + expiresAt: null, + metadata: { + description: 'Hello my friend', + externalRef: null + }, + incomingAmount: { + assetCode: 'USD', + assetScale: 2, + value: 1002 + }, + walletAddressUrl: RECEIVER_WALLET_ADDRESS + } + } + } + + const createReceiverResponse = request(createReceiverPayload) + const receiver = createReceiverResponse.createReceiver.receiver + + const createQuotePayload = { + query: ` + mutation CreateQuote($input: CreateQuoteInput!) { + createQuote(input: $input) { + quote { + id + } + } + } + `, + variables: { + input: { + walletAddressId: c9WalletAddress.id, + receiveAmount: null, + receiver: receiver.id, + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 500 + } + } + } + } + + const createQuoteResponse = request(createQuotePayload) + const quote = createQuoteResponse.createQuote.quote + + const createOutgoingPaymentPayload = { + query: ` + mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { + createOutgoingPayment(input: $input) { + payment { + id + } + } + } + `, + variables: { + input: { + walletAddressId: c9WalletAddress.id, + quoteId: quote.id + } + } + } + + request(createOutgoingPaymentPayload) +} From 95a65e5cd7c656ba187dc6efeabb945629682422 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 24 Feb 2025 09:56:10 -0500 Subject: [PATCH 04/14] feat: use fee cache instead of withGraphFetched --- packages/backend/src/fee/service.ts | 21 ++++++++++- .../src/open_payments/quote/service.ts | 36 ++++++++++--------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/packages/backend/src/fee/service.ts b/packages/backend/src/fee/service.ts index e5c6e64426..8f75ecb23d 100644 --- a/packages/backend/src/fee/service.ts +++ b/packages/backend/src/fee/service.ts @@ -22,6 +22,7 @@ export interface FeeService { sortOrder?: SortOrder ): Promise getLatestFee(assetId: string, type: FeeType): Promise + get(id: string): Promise } interface ServiceDependencies extends BaseService { @@ -48,7 +49,8 @@ export async function createFeeService({ sortOrder = SortOrder.Desc ) => getFeesPage(deps, assetId, pagination, sortOrder), getLatestFee: (assetId: string, type: FeeType) => - getLatestFee(deps, assetId, type) + getLatestFee(deps, assetId, type), + get: (id: string) => getById(deps, id) } } @@ -65,6 +67,23 @@ async function getFeesPage( return await query } +async function getById( + deps: ServiceDependencies, + id: string +): Promise { + const cachedFee = await deps.feeCache.get(id) + + if (cachedFee) { + return cachedFee + } + + const fee = await Fee.query(deps.knex).findById(id) + + if (fee) await deps.feeCache.set(id, fee) + + return fee +} + async function getLatestFee( deps: ServiceDependencies, assetId: string, diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 8b68c9f8ed..3d05bc9826 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -57,9 +57,7 @@ async function getQuote( deps: ServiceDependencies, options: GetOptions ): Promise { - const quote = await Quote.query(deps.knex) - .get(options) - .withGraphFetched('fee') + const quote = await Quote.query(deps.knex).get(options) if (quote) { const asset = await deps.assetService.get(quote.assetId) if (asset) quote.asset = asset @@ -67,6 +65,10 @@ async function getQuote( quote.walletAddress = await deps.walletAddressService.get( quote.walletAddressId ) + + if (quote.feeId) { + quote.fee = await deps.feeService.get(quote.feeId) + } } return quote } @@ -187,20 +189,18 @@ async function createQuote( ) stopTimerFee() - const createdQuote = await Quote.query(trx) - .insertAndFetch({ - id: quoteId, - walletAddressId: options.walletAddressId, - assetId: walletAddress.assetId, - receiver: options.receiver, - debitAmount: quote.debitAmount, - receiveAmount: quote.receiveAmount, - expiresAt: new Date(0), // expiresAt is patched in finalizeQuote - client: options.client, - feeId: sendingFee?.id, - estimatedExchangeRate: quote.estimatedExchangeRate - }) - .withGraphFetched('fee') + const createdQuote = await Quote.query(trx).insertAndFetch({ + id: quoteId, + walletAddressId: options.walletAddressId, + assetId: walletAddress.assetId, + receiver: options.receiver, + debitAmount: quote.debitAmount, + receiveAmount: quote.receiveAmount, + expiresAt: new Date(0), // expiresAt is patched in finalizeQuote + client: options.client, + feeId: sendingFee?.id, + estimatedExchangeRate: quote.estimatedExchangeRate + }) const asset = await deps.assetService.get(createdQuote.assetId) if (asset) createdQuote.asset = asset @@ -208,6 +208,8 @@ async function createQuote( createdQuote.walletAddressId ) + createdQuote.fee = sendingFee + stopQuoteCreate() const stopFinalize = deps.telemetry.startTimer( From 815a770c286bde87d2d1e7b464ffeee68c3a71f4 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:06:39 -0500 Subject: [PATCH 05/14] chore: cleanup comments, test config --- .../scripts/create-local-outgoing-payments.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test/performance/scripts/create-local-outgoing-payments.js b/test/performance/scripts/create-local-outgoing-payments.js index beebfcb03b..2a5398fa15 100644 --- a/test/performance/scripts/create-local-outgoing-payments.js +++ b/test/performance/scripts/create-local-outgoing-payments.js @@ -5,18 +5,10 @@ import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js' import { canonicalize } from '../dist/json-canonicalize.bundle.js' export const options = { - vus: 50, + vus: 25, duration: '120s' - // vus: 1, - // iterations: 1 } -// const CLOUD_NINE_GQL_ENDPOINT = 'http://cloud-nine-wallet-backend:3001/graphql' -// const CLOUD_NINE_WALLET_ADDRESS = -// 'https://cloud-nine-wallet-backend/accounts/gfranklin' -// const HAPPY_LIFE_BANK_WALLET_ADDRESS = -// 'https://happy-life-bank-backend/accounts/pfry' - const GQL_ENDPOINT = 'http://cloud-nine-wallet-backend:3001/graphql' const SENDER_WALLET_ADDRESS = 'https://cloud-nine-wallet-backend/accounts/gfranklin' From 68d45808b951615fb25769393b68b38844786f3c Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:12:47 -0500 Subject: [PATCH 06/14] chore: format --- .../open_payments/payment/outgoing/service.ts | 447 +++++----- .../src/open_payments/quote/service.ts | 761 +++++++++--------- 2 files changed, 602 insertions(+), 606 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index fefbc53d67..961df81854 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -84,8 +84,7 @@ export async function createOutgoingPaymentService( return { get: (options) => getOutgoingPayment(deps, options), getPage: (options) => getOutgoingPaymentsPage(deps, options), - // create: (options) => createOutgoingPayment(deps, options), - create: (options) => createOutgoingPayment2(deps, options), + create: (options) => createOutgoingPayment(deps, options), cancel: (options) => cancelOutgoingPayment(deps, options), fund: (options) => fundPayment(deps, options), processNext: () => worker.processPendingPayment(deps), @@ -234,229 +233,229 @@ async function cancelOutgoingPayment( }) } -async function createOutgoingPayment( - deps: ServiceDependencies, - options: CreateOutgoingPaymentOptions -): Promise { - const stopTimerOP = deps.telemetry.startTimer( - 'outgoing_payment_service_create_time_ms', - { - callName: 'OutgoingPaymentService:create', - description: 'Time to create an outgoing payment' - } - ) - const { walletAddressId } = options - let quoteId: string - - if (isCreateFromIncomingPayment(options)) { - const stopTimerQuote = deps.telemetry.startTimer( - 'outgoing_payment_service_create_quote_time_ms', - { - callName: 'QuoteService:create', - description: 'Time to create a quote in outgoing payment' - } - ) - const { debitAmount, incomingPayment } = options - const quoteOrError = await deps.quoteService.create({ - receiver: incomingPayment, - debitAmount, - method: 'ilp', - walletAddressId - }) - stopTimerQuote() - - if (isQuoteError(quoteOrError)) { - return quoteErrorToOutgoingPaymentError[quoteOrError] - } - quoteId = quoteOrError.id - } else { - quoteId = options.quoteId - } - - const grantId = options.grant?.id - try { - return await OutgoingPayment.transaction(deps.knex, async (trx) => { - const stopTimerWA = deps.telemetry.startTimer( - 'outgoing_payment_service_getwalletaddress_time_ms', - { - callName: 'WalletAddressService:get', - description: 'Time to get wallet address in outgoing payment' - } - ) - const walletAddress = await deps.walletAddressService.get(walletAddressId) - stopTimerWA() - if (!walletAddress) { - throw OutgoingPaymentError.UnknownWalletAddress - } - if (!walletAddress.isActive) { - throw OutgoingPaymentError.InactiveWalletAddress - } - - if (grantId) { - const stopTimerGrant = deps.telemetry.startTimer( - 'outgoing_payment_service_insertgrant_time_ms', - { - callName: 'OutgoingPaymentGrantModel:insert', - description: 'Time to insert grant in outgoing payment' - } - ) - await OutgoingPaymentGrant.query(trx) - .insert({ - id: grantId - }) - .onConflict('id') - .ignore() - stopTimerGrant() - } - const stopTimerInsertPayment = deps.telemetry.startTimer( - 'outgoing_payment_service_insertpayment_time_ms', - { - callName: 'OutgoingPayment.insert', - description: 'Time to insert payment in outgoing payment' - } - ) - const payment = await OutgoingPayment.query(trx) - .insertAndFetch({ - id: quoteId, - walletAddressId: walletAddressId, - client: options.client, - metadata: options.metadata, - state: OutgoingPaymentState.Funding, - grantId - }) - .withGraphFetched('quote') - payment.walletAddress = await deps.walletAddressService.get( - payment.walletAddressId - ) - const asset = await deps.assetService.get(payment.quote.assetId) - if (asset) payment.quote.asset = asset - - stopTimerInsertPayment() - - if ( - payment.walletAddressId !== payment.quote.walletAddressId || - payment.quote.expiresAt.getTime() <= payment.createdAt.getTime() - ) { - throw OutgoingPaymentError.InvalidQuote - } - - if (options.grant) { - const stopTimerValidateGrant = deps.telemetry.startTimer( - 'outgoing_payment_service_validate_grant_time_ms', - { - callName: - 'OutgoingPaymentService:validateGrantAndAddSpentAmountsToPayment', - description: 'Time to validate a grant' - } - ) - const isValid = await validateGrantAndAddSpentAmountsToPayment( - deps, - payment, - options.grant, - trx, - options.callback, - options.grantLockTimeoutMs - ) - stopTimerValidateGrant() - if (!isValid) { - throw OutgoingPaymentError.InsufficientGrant - } - } - const stopTimerReceiver = deps.telemetry.startTimer( - 'outgoing_payment_service_getreceiver_time_ms', - { - callName: 'ReceiverService:get', - description: 'Time to retrieve receiver in outgoing payment' - } - ) - const receiver = await deps.receiverService.get(payment.receiver) - stopTimerReceiver() - if (!receiver) { - throw OutgoingPaymentError.InvalidQuote - } - const stopTimerPeer = deps.telemetry.startTimer( - 'outgoing_payment_service_getpeer_time_ms', - { - callName: 'PeerService:getByDestinationAddress', - description: 'Time to retrieve peer in outgoing payment' - } - ) - const peer = await deps.peerService.getByDestinationAddress( - receiver.ilpAddress - ) - stopTimerPeer() +// async function createOutgoingPayment( +// deps: ServiceDependencies, +// options: CreateOutgoingPaymentOptions +// ): Promise { +// const stopTimerOP = deps.telemetry.startTimer( +// 'outgoing_payment_service_create_time_ms', +// { +// callName: 'OutgoingPaymentService:create', +// description: 'Time to create an outgoing payment' +// } +// ) +// const { walletAddressId } = options +// let quoteId: string + +// if (isCreateFromIncomingPayment(options)) { +// const stopTimerQuote = deps.telemetry.startTimer( +// 'outgoing_payment_service_create_quote_time_ms', +// { +// callName: 'QuoteService:create', +// description: 'Time to create a quote in outgoing payment' +// } +// ) +// const { debitAmount, incomingPayment } = options +// const quoteOrError = await deps.quoteService.create({ +// receiver: incomingPayment, +// debitAmount, +// method: 'ilp', +// walletAddressId +// }) +// stopTimerQuote() + +// if (isQuoteError(quoteOrError)) { +// return quoteErrorToOutgoingPaymentError[quoteOrError] +// } +// quoteId = quoteOrError.id +// } else { +// quoteId = options.quoteId +// } + +// const grantId = options.grant?.id +// try { +// return await OutgoingPayment.transaction(deps.knex, async (trx) => { +// const stopTimerWA = deps.telemetry.startTimer( +// 'outgoing_payment_service_getwalletaddress_time_ms', +// { +// callName: 'WalletAddressService:get', +// description: 'Time to get wallet address in outgoing payment' +// } +// ) +// const walletAddress = await deps.walletAddressService.get(walletAddressId) +// stopTimerWA() +// if (!walletAddress) { +// throw OutgoingPaymentError.UnknownWalletAddress +// } +// if (!walletAddress.isActive) { +// throw OutgoingPaymentError.InactiveWalletAddress +// } + +// if (grantId) { +// const stopTimerGrant = deps.telemetry.startTimer( +// 'outgoing_payment_service_insertgrant_time_ms', +// { +// callName: 'OutgoingPaymentGrantModel:insert', +// description: 'Time to insert grant in outgoing payment' +// } +// ) +// await OutgoingPaymentGrant.query(trx) +// .insert({ +// id: grantId +// }) +// .onConflict('id') +// .ignore() +// stopTimerGrant() +// } +// const stopTimerInsertPayment = deps.telemetry.startTimer( +// 'outgoing_payment_service_insertpayment_time_ms', +// { +// callName: 'OutgoingPayment.insert', +// description: 'Time to insert payment in outgoing payment' +// } +// ) +// const payment = await OutgoingPayment.query(trx) +// .insertAndFetch({ +// id: quoteId, +// walletAddressId: walletAddressId, +// client: options.client, +// metadata: options.metadata, +// state: OutgoingPaymentState.Funding, +// grantId +// }) +// .withGraphFetched('quote') +// payment.walletAddress = await deps.walletAddressService.get( +// payment.walletAddressId +// ) +// const asset = await deps.assetService.get(payment.quote.assetId) +// if (asset) payment.quote.asset = asset + +// stopTimerInsertPayment() + +// if ( +// payment.walletAddressId !== payment.quote.walletAddressId || +// payment.quote.expiresAt.getTime() <= payment.createdAt.getTime() +// ) { +// throw OutgoingPaymentError.InvalidQuote +// } + +// if (options.grant) { +// const stopTimerValidateGrant = deps.telemetry.startTimer( +// 'outgoing_payment_service_validate_grant_time_ms', +// { +// callName: +// 'OutgoingPaymentService:validateGrantAndAddSpentAmountsToPayment', +// description: 'Time to validate a grant' +// } +// ) +// const isValid = await validateGrantAndAddSpentAmountsToPayment( +// deps, +// payment, +// options.grant, +// trx, +// options.callback, +// options.grantLockTimeoutMs +// ) +// stopTimerValidateGrant() +// if (!isValid) { +// throw OutgoingPaymentError.InsufficientGrant +// } +// } +// const stopTimerReceiver = deps.telemetry.startTimer( +// 'outgoing_payment_service_getreceiver_time_ms', +// { +// callName: 'ReceiverService:get', +// description: 'Time to retrieve receiver in outgoing payment' +// } +// ) +// const receiver = await deps.receiverService.get(payment.receiver) +// stopTimerReceiver() +// if (!receiver) { +// throw OutgoingPaymentError.InvalidQuote +// } +// const stopTimerPeer = deps.telemetry.startTimer( +// 'outgoing_payment_service_getpeer_time_ms', +// { +// callName: 'PeerService:getByDestinationAddress', +// description: 'Time to retrieve peer in outgoing payment' +// } +// ) +// const peer = await deps.peerService.getByDestinationAddress( +// receiver.ilpAddress +// ) +// stopTimerPeer() + +// const stopTimerPeerUpdate = deps.telemetry.startTimer( +// 'outgoing_payment_service_patchpeer_time_ms', +// { +// callName: 'OutgoingPaymentModel:patch', +// description: 'Time to patch peer in outgoing payment' +// } +// ) +// if (peer) await payment.$query(trx).patch({ peerId: peer.id }) +// stopTimerPeerUpdate() + +// const stopTimerWebhook = deps.telemetry.startTimer( +// 'outgoing_payment_service_webhook_event_time_ms', +// { +// callName: 'OutgoingPaymentService:sendWebhookEvent', +// description: 'Time to add outgoing payment webhook event' +// } +// ) +// await sendWebhookEvent( +// deps, +// payment, +// OutgoingPaymentEventType.PaymentCreated, +// trx +// ) +// stopTimerWebhook() + +// const stopTimerAddAmount = deps.telemetry.startTimer( +// 'outgoing_payment_service_add_sent_time_ms', +// { +// callName: 'OutgoingPaymentService:addSentAmount', +// description: 'Time to add sent amount to outgoing payment' +// } +// ) + +// const paymentWithSentAmount = await addSentAmount( +// deps, +// payment, +// BigInt(0) +// ) + +// stopTimerAddAmount() + +// return paymentWithSentAmount +// }) +// } catch (err) { +// if (err instanceof UniqueViolationError) { +// if (err.constraint === 'outgoingPayments_pkey') { +// return OutgoingPaymentError.InvalidQuote +// } +// } else if (err instanceof ForeignKeyViolationError) { +// if (err.constraint === 'outgoingpayments_id_foreign') { +// return OutgoingPaymentError.UnknownQuote +// } else if ( +// err.constraint === 'outgoingpayments_walletaddressid_foreign' +// ) { +// return OutgoingPaymentError.UnknownWalletAddress +// } +// } else if (isOutgoingPaymentError(err)) { +// return err +// } else if (err instanceof knex.KnexTimeoutError) { +// deps.logger.error( +// { grant: grantId }, +// 'Could not create outgoing payment: grant locked' +// ) +// } +// throw err +// } finally { +// stopTimerOP() +// } +// } - const stopTimerPeerUpdate = deps.telemetry.startTimer( - 'outgoing_payment_service_patchpeer_time_ms', - { - callName: 'OutgoingPaymentModel:patch', - description: 'Time to patch peer in outgoing payment' - } - ) - if (peer) await payment.$query(trx).patch({ peerId: peer.id }) - stopTimerPeerUpdate() - - const stopTimerWebhook = deps.telemetry.startTimer( - 'outgoing_payment_service_webhook_event_time_ms', - { - callName: 'OutgoingPaymentService:sendWebhookEvent', - description: 'Time to add outgoing payment webhook event' - } - ) - await sendWebhookEvent( - deps, - payment, - OutgoingPaymentEventType.PaymentCreated, - trx - ) - stopTimerWebhook() - - const stopTimerAddAmount = deps.telemetry.startTimer( - 'outgoing_payment_service_add_sent_time_ms', - { - callName: 'OutgoingPaymentService:addSentAmount', - description: 'Time to add sent amount to outgoing payment' - } - ) - - const paymentWithSentAmount = await addSentAmount( - deps, - payment, - BigInt(0) - ) - - stopTimerAddAmount() - - return paymentWithSentAmount - }) - } catch (err) { - if (err instanceof UniqueViolationError) { - if (err.constraint === 'outgoingPayments_pkey') { - return OutgoingPaymentError.InvalidQuote - } - } else if (err instanceof ForeignKeyViolationError) { - if (err.constraint === 'outgoingpayments_id_foreign') { - return OutgoingPaymentError.UnknownQuote - } else if ( - err.constraint === 'outgoingpayments_walletaddressid_foreign' - ) { - return OutgoingPaymentError.UnknownWalletAddress - } - } else if (isOutgoingPaymentError(err)) { - return err - } else if (err instanceof knex.KnexTimeoutError) { - deps.logger.error( - { grant: grantId }, - 'Could not create outgoing payment: grant locked' - ) - } - throw err - } finally { - stopTimerOP() - } -} - -async function createOutgoingPayment2( +async function createOutgoingPayment( deps: ServiceDependencies, options: CreateOutgoingPaymentOptions ): Promise { diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 3d05bc9826..61ebf48b6c 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -47,8 +47,7 @@ export async function createQuoteService( } return { get: (options) => getQuote(deps, options), - // create: (options: CreateQuoteOptions) => createQuote(deps, options), - create: (options: CreateQuoteOptions) => createQuote2(deps, options), + create: (options: CreateQuoteOptions) => createQuote(deps, options), getWalletAddressPage: (options) => getWalletAddressPage(deps, options) } } @@ -94,161 +93,161 @@ export type CreateQuoteOptions = | QuoteOptionsWithDebitAmount | QuoteOptionsWithReceiveAmount -async function createQuote( - deps: ServiceDependencies, - options: CreateQuoteOptions -): Promise { - const stopTimer = deps.telemetry.startTimer('quote_service_create_time_ms', { - callName: 'QuoteService:create', - description: 'Time to create a quote' - }) - if (options.debitAmount && options.receiveAmount) { - stopTimer() - return QuoteError.InvalidAmount - } - const walletAddress = await deps.walletAddressService.get( - options.walletAddressId - ) - if (!walletAddress) { - stopTimer() - return QuoteError.UnknownWalletAddress - } - if (!walletAddress.isActive) { - stopTimer() - return QuoteError.InactiveWalletAddress - } - if (options.debitAmount) { - if ( - options.debitAmount.value <= BigInt(0) || - options.debitAmount.assetCode !== walletAddress.asset.code || - options.debitAmount.assetScale !== walletAddress.asset.scale - ) { - stopTimer() - return QuoteError.InvalidAmount - } - } - if (options.receiveAmount) { - if (options.receiveAmount.value <= BigInt(0)) { - stopTimer() - return QuoteError.InvalidAmount - } - } - - try { - const stopTimerReceiver = deps.telemetry.startTimer( - 'quote_service_create_resolve_receiver_time_ms', - { - callName: 'QuoteService:resolveReceiver', - description: 'Time to resolve receiver' - } - ) - const receiver = await resolveReceiver(deps, options) - stopTimerReceiver() - - const paymentMethod = receiver.isLocal ? 'LOCAL' : 'ILP' - const quoteId = uuid() - - return await Quote.transaction(deps.knex, async (trx) => { - const stopQuoteCreate = deps.telemetry.startTimer( - 'quote_service_create_insert_time_ms', - { - callName: 'QuoteModel.insert', - description: 'Time to insert quote' - } - ) - const stopTimerQuote = deps.telemetry.startTimer( - 'quote_service_create_get_quote_time_ms', - { - callName: 'PaymentMethodHandlerService:getQuote', - description: 'Time to getQuote' - } - ) - const quote = await deps.paymentMethodHandlerService.getQuote( - paymentMethod, - { - quoteId, - walletAddress, - receiver, - receiveAmount: options.receiveAmount, - debitAmount: options.debitAmount - }, - trx - ) - stopTimerQuote() - - const stopTimerFee = deps.telemetry.startTimer( - 'quote_service_create_get_latest_fee_time_ms', - { - callName: 'FeeService:getLatestFee', - description: 'Time to getLatestFee' - } - ) - const sendingFee = await deps.feeService.getLatestFee( - walletAddress.assetId, - FeeType.Sending - ) - stopTimerFee() - - const createdQuote = await Quote.query(trx).insertAndFetch({ - id: quoteId, - walletAddressId: options.walletAddressId, - assetId: walletAddress.assetId, - receiver: options.receiver, - debitAmount: quote.debitAmount, - receiveAmount: quote.receiveAmount, - expiresAt: new Date(0), // expiresAt is patched in finalizeQuote - client: options.client, - feeId: sendingFee?.id, - estimatedExchangeRate: quote.estimatedExchangeRate - }) - const asset = await deps.assetService.get(createdQuote.assetId) - if (asset) createdQuote.asset = asset - - createdQuote.walletAddress = await deps.walletAddressService.get( - createdQuote.walletAddressId - ) - - createdQuote.fee = sendingFee - - stopQuoteCreate() - - const stopFinalize = deps.telemetry.startTimer( - 'quote_service_finalize_quote_ms', - { - callName: 'QuoteService:finalizedQuote', - description: 'Time to finalize quote' - } - ) - const finalizedQuote = await finalizeQuote( - { - ...deps, - knex: trx - }, - options, - createdQuote, - receiver - ) - stopFinalize() - return finalizedQuote - }) - } catch (err) { - if (isQuoteError(err)) { - return err - } - - if ( - err instanceof PaymentMethodHandlerError && - err.code === PaymentMethodHandlerErrorCode.QuoteNonPositiveReceiveAmount - ) { - return QuoteError.NonPositiveReceiveAmount - } - - deps.logger.error({ err }, 'error creating a quote') - throw err - } finally { - stopTimer() - } -} +// async function createQuote( +// deps: ServiceDependencies, +// options: CreateQuoteOptions +// ): Promise { +// const stopTimer = deps.telemetry.startTimer('quote_service_create_time_ms', { +// callName: 'QuoteService:create', +// description: 'Time to create a quote' +// }) +// if (options.debitAmount && options.receiveAmount) { +// stopTimer() +// return QuoteError.InvalidAmount +// } +// const walletAddress = await deps.walletAddressService.get( +// options.walletAddressId +// ) +// if (!walletAddress) { +// stopTimer() +// return QuoteError.UnknownWalletAddress +// } +// if (!walletAddress.isActive) { +// stopTimer() +// return QuoteError.InactiveWalletAddress +// } +// if (options.debitAmount) { +// if ( +// options.debitAmount.value <= BigInt(0) || +// options.debitAmount.assetCode !== walletAddress.asset.code || +// options.debitAmount.assetScale !== walletAddress.asset.scale +// ) { +// stopTimer() +// return QuoteError.InvalidAmount +// } +// } +// if (options.receiveAmount) { +// if (options.receiveAmount.value <= BigInt(0)) { +// stopTimer() +// return QuoteError.InvalidAmount +// } +// } + +// try { +// const stopTimerReceiver = deps.telemetry.startTimer( +// 'quote_service_create_resolve_receiver_time_ms', +// { +// callName: 'QuoteService:resolveReceiver', +// description: 'Time to resolve receiver' +// } +// ) +// const receiver = await resolveReceiver(deps, options) +// stopTimerReceiver() + +// const paymentMethod = receiver.isLocal ? 'LOCAL' : 'ILP' +// const quoteId = uuid() + +// return await Quote.transaction(deps.knex, async (trx) => { +// const stopQuoteCreate = deps.telemetry.startTimer( +// 'quote_service_create_insert_time_ms', +// { +// callName: 'QuoteModel.insert', +// description: 'Time to insert quote' +// } +// ) +// const stopTimerQuote = deps.telemetry.startTimer( +// 'quote_service_create_get_quote_time_ms', +// { +// callName: 'PaymentMethodHandlerService:getQuote', +// description: 'Time to getQuote' +// } +// ) +// const quote = await deps.paymentMethodHandlerService.getQuote( +// paymentMethod, +// { +// quoteId, +// walletAddress, +// receiver, +// receiveAmount: options.receiveAmount, +// debitAmount: options.debitAmount +// }, +// trx +// ) +// stopTimerQuote() + +// const stopTimerFee = deps.telemetry.startTimer( +// 'quote_service_create_get_latest_fee_time_ms', +// { +// callName: 'FeeService:getLatestFee', +// description: 'Time to getLatestFee' +// } +// ) +// const sendingFee = await deps.feeService.getLatestFee( +// walletAddress.assetId, +// FeeType.Sending +// ) +// stopTimerFee() + +// const createdQuote = await Quote.query(trx).insertAndFetch({ +// id: quoteId, +// walletAddressId: options.walletAddressId, +// assetId: walletAddress.assetId, +// receiver: options.receiver, +// debitAmount: quote.debitAmount, +// receiveAmount: quote.receiveAmount, +// expiresAt: new Date(0), // expiresAt is patched in finalizeQuote +// client: options.client, +// feeId: sendingFee?.id, +// estimatedExchangeRate: quote.estimatedExchangeRate +// }) +// const asset = await deps.assetService.get(createdQuote.assetId) +// if (asset) createdQuote.asset = asset + +// createdQuote.walletAddress = await deps.walletAddressService.get( +// createdQuote.walletAddressId +// ) + +// createdQuote.fee = sendingFee + +// stopQuoteCreate() + +// const stopFinalize = deps.telemetry.startTimer( +// 'quote_service_finalize_quote_ms', +// { +// callName: 'QuoteService:finalizedQuote', +// description: 'Time to finalize quote' +// } +// ) +// const finalizedQuote = await finalizeQuote( +// { +// ...deps, +// knex: trx +// }, +// options, +// createdQuote, +// receiver +// ) +// stopFinalize() +// return finalizedQuote +// }) +// } catch (err) { +// if (isQuoteError(err)) { +// return err +// } + +// if ( +// err instanceof PaymentMethodHandlerError && +// err.code === PaymentMethodHandlerErrorCode.QuoteNonPositiveReceiveAmount +// ) { +// return QuoteError.NonPositiveReceiveAmount +// } + +// deps.logger.error({ err }, 'error creating a quote') +// throw err +// } finally { +// stopTimer() +// } +// } // - get fee before creation (getFeeById(sendingFee?.id)) // - initialize obj for insert which is the current object but @@ -268,7 +267,7 @@ interface UnfinalizedQuote { estimatedExchangeRate: number } -async function createQuote2( +async function createQuote( deps: ServiceDependencies, options: CreateQuoteOptions ): Promise { @@ -360,93 +359,91 @@ async function createQuote2( ) stopTimerQuote() - return await Quote.transaction(async (trx) => { - const stopTimerQuoteTrx = deps.telemetry.startTimer( - 'quote_service_create_get_quote_time_ms', - { - callName: 'QuoteService:create:transaction', - description: 'Time to complete quote transaction' - } - ) - // const stopTimerQuote = deps.telemetry.startTimer( - // 'quote_service_create_get_quote_time_ms', - // { - // callName: 'PaymentMethodHandlerService:getQuote', - // description: 'Time to getQuote' - // } - // ) - - // // TODO: rm getQuote from this trx and change to return IlpQuoteDetails - // // instead to take the connector network calls out of th trx? - - // console.log('calling paymentMethodHandlerService.getQuote wtih', { - // quoteId, - // walletAddress, - // receiver, - // receiveAmount: options.receiveAmount, - // debitAmount: options.debitAmount - // }) - // const quote = await deps.paymentMethodHandlerService.getQuote( - // paymentMethod, - // { - // quoteId, - // walletAddress, - // receiver, - // receiveAmount: options.receiveAmount, - // debitAmount: options.debitAmount - // }, - // trx - // ) - // console.log('getQuote finished') - // stopTimerQuote() - - const unfinalizedQuote: UnfinalizedQuote = { - id: quoteId, - walletAddressId: options.walletAddressId, - assetId: walletAddress.assetId, - receiver: options.receiver, - debitAmount: quote.debitAmount, - receiveAmount: quote.receiveAmount, - client: options.client, - feeId: sendingFee?.id, - fee: sendingFee, - estimatedExchangeRate: quote.estimatedExchangeRate - } + const unfinalizedQuote: UnfinalizedQuote = { + id: quoteId, + walletAddressId: options.walletAddressId, + assetId: walletAddress.assetId, + receiver: options.receiver, + debitAmount: quote.debitAmount, + receiveAmount: quote.receiveAmount, + client: options.client, + feeId: sendingFee?.id, + fee: sendingFee, + estimatedExchangeRate: quote.estimatedExchangeRate + } - const stopFinalize = deps.telemetry.startTimer( - 'quote_service_finalize_quote_ms', - { - callName: 'QuoteService:finalizedQuote', - description: 'Time to finalize quote' - } - ) - const patchOptions = await finalizeQuote2( - deps, - options, - unfinalizedQuote, - receiver - ) - stopFinalize() - - const stopQuoteCreate = deps.telemetry.startTimer( - 'quote_service_create_insert_time_ms', - { - callName: 'QuoteModel.insert', - description: 'Time to insert quote' - } - ) - const createdQuote = await Quote.query(trx).insertAndFetch({ - ...unfinalizedQuote, - ...patchOptions - }) - createdQuote.asset = walletAddress.asset - createdQuote.walletAddress = walletAddress - if (sendingFee) createdQuote.fee = sendingFee - - stopQuoteCreate() - stopTimerQuoteTrx() - return createdQuote + const stopFinalize = deps.telemetry.startTimer( + 'quote_service_finalize_quote_ms', + { + callName: 'QuoteService:finalizedQuote', + description: 'Time to finalize quote' + } + ) + const patchOptions = await finalizeQuote2( + deps, + options, + unfinalizedQuote, + receiver + ) + stopFinalize() + + // const stopTimerQuoteTrx = deps.telemetry.startTimer( + // 'quote_service_create_get_quote_time_ms', + // { + // callName: 'QuoteService:create:transaction', + // description: 'Time to complete quote transaction' + // } + // ) + // const stopTimerQuote = deps.telemetry.startTimer( + // 'quote_service_create_get_quote_time_ms', + // { + // callName: 'PaymentMethodHandlerService:getQuote', + // description: 'Time to getQuote' + // } + // ) + + // // TODO: rm getQuote from this trx and change to return IlpQuoteDetails + // // instead to take the connector network calls out of th trx? + + // console.log('calling paymentMethodHandlerService.getQuote wtih', { + // quoteId, + // walletAddress, + // receiver, + // receiveAmount: options.receiveAmount, + // debitAmount: options.debitAmount + // }) + // const quote = await deps.paymentMethodHandlerService.getQuote( + // paymentMethod, + // { + // quoteId, + // walletAddress, + // receiver, + // receiveAmount: options.receiveAmount, + // debitAmount: options.debitAmount + // }, + // trx + // ) + // console.log('getQuote finished') + // stopTimerQuote() + + const stopQuoteCreate = deps.telemetry.startTimer( + 'quote_service_create_insert_time_ms', + { + callName: 'QuoteModel.insert', + description: 'Time to insert quote' + } + ) + const createdQuote = await Quote.query(deps.knex).insertAndFetch({ + ...unfinalizedQuote, + ...patchOptions }) + createdQuote.asset = walletAddress.asset + createdQuote.walletAddress = walletAddress + if (sendingFee) createdQuote.fee = sendingFee + + stopQuoteCreate() + // stopTimerQuoteTrx() + return createdQuote } catch (err) { if (isQuoteError(err)) { return err @@ -515,56 +512,56 @@ interface CalculateQuoteAmountsWithFeesResult { * Calculate fixed-send quote amounts: debitAmount is locked, * subtract fees (considering the exchange rate) from the receiveAmount. */ -function calculateFixedSendQuoteAmounts( - deps: ServiceDependencies, - quote: Quote, - maxReceiveAmountValue: bigint -): CalculateQuoteAmountsWithFeesResult { - // TODO: derive fee from debitAmount instead? Current behavior/tests may be wrong with basis point fees. - const fees = quote.fee?.calculate(quote.receiveAmount.value) ?? BigInt(0) - - const { estimatedExchangeRate } = quote - - const exchangeAdjustedFees = BigInt( - Math.ceil(Number(fees) * estimatedExchangeRate) - ) - const receiveAmountValue = - BigInt(quote.receiveAmount.value) - exchangeAdjustedFees - - if (receiveAmountValue <= BigInt(0)) { - deps.logger.info( - { fees, exchangeAdjustedFees, estimatedExchangeRate, receiveAmountValue }, - 'Negative receive amount when calculating quote amount' - ) - throw QuoteError.NonPositiveReceiveAmount - } - - if (receiveAmountValue > maxReceiveAmountValue) { - throw QuoteError.InvalidAmount - } - - const debitAmountMinusFees = - quote.debitAmount.value - - (quote.fee?.calculate(quote.debitAmount.value) ?? 0n) - - deps.logger.debug( - { - 'quote.receiveAmount.value': quote.receiveAmount.value, - debitAmountValue: quote.debitAmount.value, - debitAmountMinusFees, - receiveAmountValue, - fees, - exchangeAdjustedFees - }, - 'Calculated fixed-send quote amount with fees' - ) - - return { - debitAmountValue: quote.debitAmount.value, - debitAmountMinusFees, - receiveAmountValue - } -} +// function calculateFixedSendQuoteAmounts( +// deps: ServiceDependencies, +// quote: Quote, +// maxReceiveAmountValue: bigint +// ): CalculateQuoteAmountsWithFeesResult { +// // TODO: derive fee from debitAmount instead? Current behavior/tests may be wrong with basis point fees. +// const fees = quote.fee?.calculate(quote.receiveAmount.value) ?? BigInt(0) + +// const { estimatedExchangeRate } = quote + +// const exchangeAdjustedFees = BigInt( +// Math.ceil(Number(fees) * estimatedExchangeRate) +// ) +// const receiveAmountValue = +// BigInt(quote.receiveAmount.value) - exchangeAdjustedFees + +// if (receiveAmountValue <= BigInt(0)) { +// deps.logger.info( +// { fees, exchangeAdjustedFees, estimatedExchangeRate, receiveAmountValue }, +// 'Negative receive amount when calculating quote amount' +// ) +// throw QuoteError.NonPositiveReceiveAmount +// } + +// if (receiveAmountValue > maxReceiveAmountValue) { +// throw QuoteError.InvalidAmount +// } + +// const debitAmountMinusFees = +// quote.debitAmount.value - +// (quote.fee?.calculate(quote.debitAmount.value) ?? 0n) + +// deps.logger.debug( +// { +// 'quote.receiveAmount.value': quote.receiveAmount.value, +// debitAmountValue: quote.debitAmount.value, +// debitAmountMinusFees, +// receiveAmountValue, +// fees, +// exchangeAdjustedFees +// }, +// 'Calculated fixed-send quote amount with fees' +// ) + +// return { +// debitAmountValue: quote.debitAmount.value, +// debitAmountMinusFees, +// receiveAmountValue +// } +// } function calculateFixedSendQuoteAmounts2( deps: ServiceDependencies, @@ -621,51 +618,51 @@ function calculateFixedSendQuoteAmounts2( * Calculate fixed-delivery quote amounts: receiveAmount is locked, * add fees to the the debitAmount. */ -function calculateFixedDeliveryQuoteAmounts( - deps: ServiceDependencies, - quote: Quote -): CalculateQuoteAmountsWithFeesResult { - const fees = quote.fee?.calculate(quote.debitAmount.value) ?? BigInt(0) - - const debitAmountValue = BigInt(quote.debitAmount.value) + fees - - if (debitAmountValue <= BigInt(0)) { - deps.logger.info( - { fees, debitAmountValue }, - 'Received negative debitAmount receive amount when calculating quote amount' - ) - throw QuoteError.InvalidAmount - } - - deps.logger.debug( - { debitAmountValue, receiveAmountValue: quote.receiveAmount.value, fees }, - `Calculated fixed-delivery quote amount with fees` - ) - - return { - debitAmountValue, - debitAmountMinusFees: quote.debitAmount.value, - receiveAmountValue: quote.receiveAmount.value - } -} - -function calculateExpiry( - deps: ServiceDependencies, - quote: Quote, - receiver: Receiver -): Date { - const quoteExpiry = new Date( - quote.createdAt.getTime() + deps.config.quoteLifespan - ) - - const incomingPaymentExpiresEarlier = - receiver.incomingPayment?.expiresAt && - receiver.incomingPayment.expiresAt.getTime() < quoteExpiry.getTime() - - return incomingPaymentExpiresEarlier - ? receiver.incomingPayment!.expiresAt! - : quoteExpiry -} +// function calculateFixedDeliveryQuoteAmounts( +// deps: ServiceDependencies, +// quote: Quote +// ): CalculateQuoteAmountsWithFeesResult { +// const fees = quote.fee?.calculate(quote.debitAmount.value) ?? BigInt(0) + +// const debitAmountValue = BigInt(quote.debitAmount.value) + fees + +// if (debitAmountValue <= BigInt(0)) { +// deps.logger.info( +// { fees, debitAmountValue }, +// 'Received negative debitAmount receive amount when calculating quote amount' +// ) +// throw QuoteError.InvalidAmount +// } + +// deps.logger.debug( +// { debitAmountValue, receiveAmountValue: quote.receiveAmount.value, fees }, +// `Calculated fixed-delivery quote amount with fees` +// ) + +// return { +// debitAmountValue, +// debitAmountMinusFees: quote.debitAmount.value, +// receiveAmountValue: quote.receiveAmount.value +// } +// } + +// function calculateExpiry( +// deps: ServiceDependencies, +// quote: Quote, +// receiver: Receiver +// ): Date { +// const quoteExpiry = new Date( +// quote.createdAt.getTime() + deps.config.quoteLifespan +// ) + +// const incomingPaymentExpiresEarlier = +// receiver.incomingPayment?.expiresAt && +// receiver.incomingPayment.expiresAt.getTime() < quoteExpiry.getTime() + +// return incomingPaymentExpiresEarlier +// ? receiver.incomingPayment!.expiresAt! +// : quoteExpiry +// } function calculateExpiry2( deps: ServiceDependencies, @@ -725,50 +722,50 @@ function calculateFixedDeliveryQuoteAmounts2( } } -async function finalizeQuote( - deps: ServiceDependencies, - options: CreateQuoteOptions, - quote: Quote, - receiver: Receiver -): Promise { - let maxReceiveAmountValue: bigint | undefined - - if (options.debitAmount) { - const receivingPaymentValue = - receiver.incomingAmount && receiver.receivedAmount - ? receiver.incomingAmount.value - receiver.receivedAmount.value - : undefined - maxReceiveAmountValue = - receivingPaymentValue && receivingPaymentValue < quote.receiveAmount.value - ? receivingPaymentValue - : quote.receiveAmount.value - } - - deps.logger.debug( - { - debitAmountValue: quote.debitAmount.value, - receiveAmountValue: quote.receiveAmount.value, - maxReceiveAmountValue - }, - `Calculating ${maxReceiveAmountValue ? 'fixed-send' : 'fixed-delivery'} quote amount with fees` - ) - - const { debitAmountValue, debitAmountMinusFees, receiveAmountValue } = - maxReceiveAmountValue - ? calculateFixedSendQuoteAmounts(deps, quote, maxReceiveAmountValue) - : calculateFixedDeliveryQuoteAmounts(deps, quote) - - const patchOptions = { - debitAmountMinusFees, - debitAmountValue, - receiveAmountValue, - expiresAt: calculateExpiry(deps, quote, receiver) - } - - await quote.$query(deps.knex).patch(patchOptions) - - return quote -} +// async function finalizeQuote( +// deps: ServiceDependencies, +// options: CreateQuoteOptions, +// quote: Quote, +// receiver: Receiver +// ): Promise { +// let maxReceiveAmountValue: bigint | undefined + +// if (options.debitAmount) { +// const receivingPaymentValue = +// receiver.incomingAmount && receiver.receivedAmount +// ? receiver.incomingAmount.value - receiver.receivedAmount.value +// : undefined +// maxReceiveAmountValue = +// receivingPaymentValue && receivingPaymentValue < quote.receiveAmount.value +// ? receivingPaymentValue +// : quote.receiveAmount.value +// } + +// deps.logger.debug( +// { +// debitAmountValue: quote.debitAmount.value, +// receiveAmountValue: quote.receiveAmount.value, +// maxReceiveAmountValue +// }, +// `Calculating ${maxReceiveAmountValue ? 'fixed-send' : 'fixed-delivery'} quote amount with fees` +// ) + +// const { debitAmountValue, debitAmountMinusFees, receiveAmountValue } = +// maxReceiveAmountValue +// ? calculateFixedSendQuoteAmounts(deps, quote, maxReceiveAmountValue) +// : calculateFixedDeliveryQuoteAmounts(deps, quote) + +// const patchOptions = { +// debitAmountMinusFees, +// debitAmountValue, +// receiveAmountValue, +// expiresAt: calculateExpiry(deps, quote, receiver) +// } + +// await quote.$query(deps.knex).patch(patchOptions) + +// return quote +// } async function finalizeQuote2( deps: ServiceDependencies, From 456a0981749b49f69e9d9aedeb7fd063bd84e04e Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:35:14 -0500 Subject: [PATCH 07/14] chore: rm logs, commented out code --- .../open_payments/payment/outgoing/service.ts | 225 +----------------- 1 file changed, 3 insertions(+), 222 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 961df81854..35bf3999bc 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -233,228 +233,6 @@ async function cancelOutgoingPayment( }) } -// async function createOutgoingPayment( -// deps: ServiceDependencies, -// options: CreateOutgoingPaymentOptions -// ): Promise { -// const stopTimerOP = deps.telemetry.startTimer( -// 'outgoing_payment_service_create_time_ms', -// { -// callName: 'OutgoingPaymentService:create', -// description: 'Time to create an outgoing payment' -// } -// ) -// const { walletAddressId } = options -// let quoteId: string - -// if (isCreateFromIncomingPayment(options)) { -// const stopTimerQuote = deps.telemetry.startTimer( -// 'outgoing_payment_service_create_quote_time_ms', -// { -// callName: 'QuoteService:create', -// description: 'Time to create a quote in outgoing payment' -// } -// ) -// const { debitAmount, incomingPayment } = options -// const quoteOrError = await deps.quoteService.create({ -// receiver: incomingPayment, -// debitAmount, -// method: 'ilp', -// walletAddressId -// }) -// stopTimerQuote() - -// if (isQuoteError(quoteOrError)) { -// return quoteErrorToOutgoingPaymentError[quoteOrError] -// } -// quoteId = quoteOrError.id -// } else { -// quoteId = options.quoteId -// } - -// const grantId = options.grant?.id -// try { -// return await OutgoingPayment.transaction(deps.knex, async (trx) => { -// const stopTimerWA = deps.telemetry.startTimer( -// 'outgoing_payment_service_getwalletaddress_time_ms', -// { -// callName: 'WalletAddressService:get', -// description: 'Time to get wallet address in outgoing payment' -// } -// ) -// const walletAddress = await deps.walletAddressService.get(walletAddressId) -// stopTimerWA() -// if (!walletAddress) { -// throw OutgoingPaymentError.UnknownWalletAddress -// } -// if (!walletAddress.isActive) { -// throw OutgoingPaymentError.InactiveWalletAddress -// } - -// if (grantId) { -// const stopTimerGrant = deps.telemetry.startTimer( -// 'outgoing_payment_service_insertgrant_time_ms', -// { -// callName: 'OutgoingPaymentGrantModel:insert', -// description: 'Time to insert grant in outgoing payment' -// } -// ) -// await OutgoingPaymentGrant.query(trx) -// .insert({ -// id: grantId -// }) -// .onConflict('id') -// .ignore() -// stopTimerGrant() -// } -// const stopTimerInsertPayment = deps.telemetry.startTimer( -// 'outgoing_payment_service_insertpayment_time_ms', -// { -// callName: 'OutgoingPayment.insert', -// description: 'Time to insert payment in outgoing payment' -// } -// ) -// const payment = await OutgoingPayment.query(trx) -// .insertAndFetch({ -// id: quoteId, -// walletAddressId: walletAddressId, -// client: options.client, -// metadata: options.metadata, -// state: OutgoingPaymentState.Funding, -// grantId -// }) -// .withGraphFetched('quote') -// payment.walletAddress = await deps.walletAddressService.get( -// payment.walletAddressId -// ) -// const asset = await deps.assetService.get(payment.quote.assetId) -// if (asset) payment.quote.asset = asset - -// stopTimerInsertPayment() - -// if ( -// payment.walletAddressId !== payment.quote.walletAddressId || -// payment.quote.expiresAt.getTime() <= payment.createdAt.getTime() -// ) { -// throw OutgoingPaymentError.InvalidQuote -// } - -// if (options.grant) { -// const stopTimerValidateGrant = deps.telemetry.startTimer( -// 'outgoing_payment_service_validate_grant_time_ms', -// { -// callName: -// 'OutgoingPaymentService:validateGrantAndAddSpentAmountsToPayment', -// description: 'Time to validate a grant' -// } -// ) -// const isValid = await validateGrantAndAddSpentAmountsToPayment( -// deps, -// payment, -// options.grant, -// trx, -// options.callback, -// options.grantLockTimeoutMs -// ) -// stopTimerValidateGrant() -// if (!isValid) { -// throw OutgoingPaymentError.InsufficientGrant -// } -// } -// const stopTimerReceiver = deps.telemetry.startTimer( -// 'outgoing_payment_service_getreceiver_time_ms', -// { -// callName: 'ReceiverService:get', -// description: 'Time to retrieve receiver in outgoing payment' -// } -// ) -// const receiver = await deps.receiverService.get(payment.receiver) -// stopTimerReceiver() -// if (!receiver) { -// throw OutgoingPaymentError.InvalidQuote -// } -// const stopTimerPeer = deps.telemetry.startTimer( -// 'outgoing_payment_service_getpeer_time_ms', -// { -// callName: 'PeerService:getByDestinationAddress', -// description: 'Time to retrieve peer in outgoing payment' -// } -// ) -// const peer = await deps.peerService.getByDestinationAddress( -// receiver.ilpAddress -// ) -// stopTimerPeer() - -// const stopTimerPeerUpdate = deps.telemetry.startTimer( -// 'outgoing_payment_service_patchpeer_time_ms', -// { -// callName: 'OutgoingPaymentModel:patch', -// description: 'Time to patch peer in outgoing payment' -// } -// ) -// if (peer) await payment.$query(trx).patch({ peerId: peer.id }) -// stopTimerPeerUpdate() - -// const stopTimerWebhook = deps.telemetry.startTimer( -// 'outgoing_payment_service_webhook_event_time_ms', -// { -// callName: 'OutgoingPaymentService:sendWebhookEvent', -// description: 'Time to add outgoing payment webhook event' -// } -// ) -// await sendWebhookEvent( -// deps, -// payment, -// OutgoingPaymentEventType.PaymentCreated, -// trx -// ) -// stopTimerWebhook() - -// const stopTimerAddAmount = deps.telemetry.startTimer( -// 'outgoing_payment_service_add_sent_time_ms', -// { -// callName: 'OutgoingPaymentService:addSentAmount', -// description: 'Time to add sent amount to outgoing payment' -// } -// ) - -// const paymentWithSentAmount = await addSentAmount( -// deps, -// payment, -// BigInt(0) -// ) - -// stopTimerAddAmount() - -// return paymentWithSentAmount -// }) -// } catch (err) { -// if (err instanceof UniqueViolationError) { -// if (err.constraint === 'outgoingPayments_pkey') { -// return OutgoingPaymentError.InvalidQuote -// } -// } else if (err instanceof ForeignKeyViolationError) { -// if (err.constraint === 'outgoingpayments_id_foreign') { -// return OutgoingPaymentError.UnknownQuote -// } else if ( -// err.constraint === 'outgoingpayments_walletaddressid_foreign' -// ) { -// return OutgoingPaymentError.UnknownWalletAddress -// } -// } else if (isOutgoingPaymentError(err)) { -// return err -// } else if (err instanceof knex.KnexTimeoutError) { -// deps.logger.error( -// { grant: grantId }, -// 'Could not create outgoing payment: grant locked' -// ) -// } -// throw err -// } finally { -// stopTimerOP() -// } -// } - async function createOutgoingPayment( deps: ServiceDependencies, options: CreateOutgoingPaymentOptions @@ -606,6 +384,9 @@ async function createOutgoingPayment( grantId }) payment.walletAddress = walletAddress + // payment.walletAddress = await deps.walletAddressService.get( + // payment.walletAddressId + // ) // TODO: why cant I do payment.walletAddress = walletAddress? payment.quote = quote if (asset) payment.quote.asset = asset From 1b7f3530a4ee0163b1897a1432c6ecf79c55d397 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:02:43 -0500 Subject: [PATCH 08/14] fix(backend): out payment service tests - payment returned from createOutgoingPayment previously had walletAddress of undefined (coming from withGraphFetched(quote)). When assigning manually it included wallet address. Updated get methods to include wallet address on quote as well. --- .../src/open_payments/payment/outgoing/service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 35bf3999bc..56899e1c1b 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -160,6 +160,10 @@ async function getOutgoingPayment( outgoingPayment.walletAddress = await deps.walletAddressService.get( outgoingPayment.walletAddressId ) + outgoingPayment.quote.walletAddress = await deps.walletAddressService.get( + outgoingPayment.quote.walletAddressId + ) + const asset = await deps.assetService.get(outgoingPayment.quote.assetId) if (asset) outgoingPayment.quote.asset = asset @@ -384,9 +388,6 @@ async function createOutgoingPayment( grantId }) payment.walletAddress = walletAddress - // payment.walletAddress = await deps.walletAddressService.get( - // payment.walletAddressId - // ) // TODO: why cant I do payment.walletAddress = walletAddress? payment.quote = quote if (asset) payment.quote.asset = asset @@ -730,6 +731,9 @@ async function getWalletAddressPage( payment.walletAddress = await deps.walletAddressService.get( payment.walletAddressId ) + payment.quote.walletAddress = await deps.walletAddressService.get( + payment.quote.walletAddressId + ) const asset = await deps.assetService.get(payment.quote.assetId) if (asset) payment.quote.asset = asset } From 3164ed9385afd4fb19ed2ae0284218fabc0715db Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:17:00 -0500 Subject: [PATCH 09/14] test(backend): fix some quote tests --- .../src/open_payments/quote/service.test.ts | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 3840212c2e..13d2daa423 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -123,6 +123,7 @@ describe('QuoteService', (): void => { afterEach(async (): Promise => { jest.restoreAllMocks() + jest.useRealTimers() await truncateTables(knex) }) @@ -221,6 +222,10 @@ describe('QuoteService', (): void => { .spyOn(paymentMethodHandlerService, 'getQuote') .mockResolvedValueOnce(mockedQuote) + jest.useFakeTimers() + const now = Date.now() + jest.spyOn(global.Date, 'now').mockImplementation(() => now) + const quote = await quoteService.create({ ...options, client @@ -235,8 +240,7 @@ describe('QuoteService', (): void => { receiver: expect.anything(), receiveAmount: options.receiveAmount, debitAmount: options.debitAmount - }), - expect.anything() + }) ) expect(quote).toMatchObject({ @@ -246,9 +250,7 @@ describe('QuoteService', (): void => { receiveAmount: receiveAmount || mockedQuote.receiveAmount, createdAt: expect.any(Date), updatedAt: expect.any(Date), - expiresAt: new Date( - quote.createdAt.getTime() + config.quoteLifespan - ), + expiresAt: new Date(now + config.quoteLifespan), client: client || null }) @@ -320,6 +322,10 @@ describe('QuoteService', (): void => { .spyOn(paymentMethodHandlerService, 'getQuote') .mockResolvedValueOnce(mockedQuote) + jest.useFakeTimers() + const now = Date.now() + jest.spyOn(global.Date, 'now').mockImplementation(() => now) + const quote = await quoteService.create({ ...options, client @@ -332,9 +338,7 @@ describe('QuoteService', (): void => { receiveAmount: incomingAmount, createdAt: expect.any(Date), updatedAt: expect.any(Date), - expiresAt: new Date( - quote.createdAt.getTime() + config.quoteLifespan - ), + expiresAt: new Date(new Date(now + config.quoteLifespan)), client: client || null }) @@ -764,6 +768,10 @@ describe('QuoteService', (): void => { .spyOn(paymentMethodHandlerService, 'getQuote') .mockResolvedValueOnce(mockedQuote) + jest.useFakeTimers() + const now = Date.now() + jest.spyOn(global.Date, 'now').mockImplementation(() => now) + const quote = await quoteService.create(options) assert.ok(!isQuoteError(quote)) @@ -775,8 +783,7 @@ describe('QuoteService', (): void => { receiver: expect.anything(), receiveAmount: options.receiveAmount, debitAmount: options.debitAmount - }), - expect.anything() + }) ) expect(quote).toMatchObject({ @@ -786,7 +793,7 @@ describe('QuoteService', (): void => { receiveAmount: mockedQuote.receiveAmount, createdAt: expect.any(Date), updatedAt: expect.any(Date), - expiresAt: new Date(quote.createdAt.getTime() + config.quoteLifespan) + expiresAt: new Date(now + config.quoteLifespan) }) await expect( From 0ee97947975e9c275682f117b2884504cf744da6 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:34:44 -0500 Subject: [PATCH 10/14] chore: cleanup comments, join on fees again, fix test --- .../src/open_payments/quote/service.test.ts | 10 +- .../src/open_payments/quote/service.ts | 392 +----------------- 2 files changed, 24 insertions(+), 378 deletions(-) diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 13d2daa423..eb239231e3 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -377,7 +377,7 @@ describe('QuoteService', (): void => { test.each` expiryDate | description ${new Date(new Date().getTime() + Config.quoteLifespan - 2 * 60_000)} | ${"the incoming payment's expirataion date"} - ${new Date(new Date().getTime() + Config.quoteLifespan + 2 * 60_000)} | ${"the quotation's creation date plus its lifespan"} + ${new Date(new Date().getTime() + Config.quoteLifespan + 2 * 60_000)} | ${"the quote's creation date plus its lifespan"} `( 'sets expiry date to $description', async ({ expiryDate }): Promise => { @@ -406,11 +406,13 @@ describe('QuoteService', (): void => { .spyOn(paymentMethodHandlerService, 'getQuote') .mockResolvedValueOnce(mockedQuote) + jest.useFakeTimers() + const now = Date.now() + jest.spyOn(global.Date, 'now').mockImplementation(() => now) + const quote = await quoteService.create(options) assert.ok(!isQuoteError(quote)) - const maxExpiration = new Date( - quote.createdAt.getTime() + config.quoteLifespan - ) + const maxExpiration = new Date(now + config.quoteLifespan) expect(quote).toMatchObject({ walletAddressId: sendingWalletAddress.id, receiver: options.receiver, diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 61ebf48b6c..084e10dfc7 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -56,7 +56,9 @@ async function getQuote( deps: ServiceDependencies, options: GetOptions ): Promise { - const quote = await Quote.query(deps.knex).get(options) + const quote = await Quote.query(deps.knex) + .get(options) + .withGraphFetched('fee') if (quote) { const asset = await deps.assetService.get(quote.assetId) if (asset) quote.asset = asset @@ -64,10 +66,6 @@ async function getQuote( quote.walletAddress = await deps.walletAddressService.get( quote.walletAddressId ) - - if (quote.feeId) { - quote.fee = await deps.feeService.get(quote.feeId) - } } return quote } @@ -93,167 +91,6 @@ export type CreateQuoteOptions = | QuoteOptionsWithDebitAmount | QuoteOptionsWithReceiveAmount -// async function createQuote( -// deps: ServiceDependencies, -// options: CreateQuoteOptions -// ): Promise { -// const stopTimer = deps.telemetry.startTimer('quote_service_create_time_ms', { -// callName: 'QuoteService:create', -// description: 'Time to create a quote' -// }) -// if (options.debitAmount && options.receiveAmount) { -// stopTimer() -// return QuoteError.InvalidAmount -// } -// const walletAddress = await deps.walletAddressService.get( -// options.walletAddressId -// ) -// if (!walletAddress) { -// stopTimer() -// return QuoteError.UnknownWalletAddress -// } -// if (!walletAddress.isActive) { -// stopTimer() -// return QuoteError.InactiveWalletAddress -// } -// if (options.debitAmount) { -// if ( -// options.debitAmount.value <= BigInt(0) || -// options.debitAmount.assetCode !== walletAddress.asset.code || -// options.debitAmount.assetScale !== walletAddress.asset.scale -// ) { -// stopTimer() -// return QuoteError.InvalidAmount -// } -// } -// if (options.receiveAmount) { -// if (options.receiveAmount.value <= BigInt(0)) { -// stopTimer() -// return QuoteError.InvalidAmount -// } -// } - -// try { -// const stopTimerReceiver = deps.telemetry.startTimer( -// 'quote_service_create_resolve_receiver_time_ms', -// { -// callName: 'QuoteService:resolveReceiver', -// description: 'Time to resolve receiver' -// } -// ) -// const receiver = await resolveReceiver(deps, options) -// stopTimerReceiver() - -// const paymentMethod = receiver.isLocal ? 'LOCAL' : 'ILP' -// const quoteId = uuid() - -// return await Quote.transaction(deps.knex, async (trx) => { -// const stopQuoteCreate = deps.telemetry.startTimer( -// 'quote_service_create_insert_time_ms', -// { -// callName: 'QuoteModel.insert', -// description: 'Time to insert quote' -// } -// ) -// const stopTimerQuote = deps.telemetry.startTimer( -// 'quote_service_create_get_quote_time_ms', -// { -// callName: 'PaymentMethodHandlerService:getQuote', -// description: 'Time to getQuote' -// } -// ) -// const quote = await deps.paymentMethodHandlerService.getQuote( -// paymentMethod, -// { -// quoteId, -// walletAddress, -// receiver, -// receiveAmount: options.receiveAmount, -// debitAmount: options.debitAmount -// }, -// trx -// ) -// stopTimerQuote() - -// const stopTimerFee = deps.telemetry.startTimer( -// 'quote_service_create_get_latest_fee_time_ms', -// { -// callName: 'FeeService:getLatestFee', -// description: 'Time to getLatestFee' -// } -// ) -// const sendingFee = await deps.feeService.getLatestFee( -// walletAddress.assetId, -// FeeType.Sending -// ) -// stopTimerFee() - -// const createdQuote = await Quote.query(trx).insertAndFetch({ -// id: quoteId, -// walletAddressId: options.walletAddressId, -// assetId: walletAddress.assetId, -// receiver: options.receiver, -// debitAmount: quote.debitAmount, -// receiveAmount: quote.receiveAmount, -// expiresAt: new Date(0), // expiresAt is patched in finalizeQuote -// client: options.client, -// feeId: sendingFee?.id, -// estimatedExchangeRate: quote.estimatedExchangeRate -// }) -// const asset = await deps.assetService.get(createdQuote.assetId) -// if (asset) createdQuote.asset = asset - -// createdQuote.walletAddress = await deps.walletAddressService.get( -// createdQuote.walletAddressId -// ) - -// createdQuote.fee = sendingFee - -// stopQuoteCreate() - -// const stopFinalize = deps.telemetry.startTimer( -// 'quote_service_finalize_quote_ms', -// { -// callName: 'QuoteService:finalizedQuote', -// description: 'Time to finalize quote' -// } -// ) -// const finalizedQuote = await finalizeQuote( -// { -// ...deps, -// knex: trx -// }, -// options, -// createdQuote, -// receiver -// ) -// stopFinalize() -// return finalizedQuote -// }) -// } catch (err) { -// if (isQuoteError(err)) { -// return err -// } - -// if ( -// err instanceof PaymentMethodHandlerError && -// err.code === PaymentMethodHandlerErrorCode.QuoteNonPositiveReceiveAmount -// ) { -// return QuoteError.NonPositiveReceiveAmount -// } - -// deps.logger.error({ err }, 'error creating a quote') -// throw err -// } finally { -// stopTimer() -// } -// } - -// - get fee before creation (getFeeById(sendingFee?.id)) -// - initialize obj for insert which is the current object but -// - call finalizeQuote with the object for insert and return the stuff thats currently patched (the amounts and expiry) -// - merge those things into the quote input and create the quote. - interface UnfinalizedQuote { id: string walletAddressId: string @@ -355,7 +192,6 @@ async function createQuote( receiveAmount: options.receiveAmount, debitAmount: options.debitAmount } - // trx ) stopTimerQuote() @@ -379,7 +215,7 @@ async function createQuote( description: 'Time to finalize quote' } ) - const patchOptions = await finalizeQuote2( + const finalQuoteOptions = await finalizeQuote( deps, options, unfinalizedQuote, @@ -387,45 +223,6 @@ async function createQuote( ) stopFinalize() - // const stopTimerQuoteTrx = deps.telemetry.startTimer( - // 'quote_service_create_get_quote_time_ms', - // { - // callName: 'QuoteService:create:transaction', - // description: 'Time to complete quote transaction' - // } - // ) - // const stopTimerQuote = deps.telemetry.startTimer( - // 'quote_service_create_get_quote_time_ms', - // { - // callName: 'PaymentMethodHandlerService:getQuote', - // description: 'Time to getQuote' - // } - // ) - - // // TODO: rm getQuote from this trx and change to return IlpQuoteDetails - // // instead to take the connector network calls out of th trx? - - // console.log('calling paymentMethodHandlerService.getQuote wtih', { - // quoteId, - // walletAddress, - // receiver, - // receiveAmount: options.receiveAmount, - // debitAmount: options.debitAmount - // }) - // const quote = await deps.paymentMethodHandlerService.getQuote( - // paymentMethod, - // { - // quoteId, - // walletAddress, - // receiver, - // receiveAmount: options.receiveAmount, - // debitAmount: options.debitAmount - // }, - // trx - // ) - // console.log('getQuote finished') - // stopTimerQuote() - const stopQuoteCreate = deps.telemetry.startTimer( 'quote_service_create_insert_time_ms', { @@ -433,16 +230,16 @@ async function createQuote( description: 'Time to insert quote' } ) - const createdQuote = await Quote.query(deps.knex).insertAndFetch({ - ...unfinalizedQuote, - ...patchOptions - }) + const createdQuote = await Quote.query(deps.knex) + .insertAndFetch({ + ...unfinalizedQuote, + ...finalQuoteOptions + }) + .withGraphFetched('fee') createdQuote.asset = walletAddress.asset createdQuote.walletAddress = walletAddress - if (sendingFee) createdQuote.fee = sendingFee stopQuoteCreate() - // stopTimerQuoteTrx() return createdQuote } catch (err) { if (isQuoteError(err)) { @@ -508,62 +305,7 @@ interface CalculateQuoteAmountsWithFeesResult { debitAmountMinusFees: bigint } -/** - * Calculate fixed-send quote amounts: debitAmount is locked, - * subtract fees (considering the exchange rate) from the receiveAmount. - */ -// function calculateFixedSendQuoteAmounts( -// deps: ServiceDependencies, -// quote: Quote, -// maxReceiveAmountValue: bigint -// ): CalculateQuoteAmountsWithFeesResult { -// // TODO: derive fee from debitAmount instead? Current behavior/tests may be wrong with basis point fees. -// const fees = quote.fee?.calculate(quote.receiveAmount.value) ?? BigInt(0) - -// const { estimatedExchangeRate } = quote - -// const exchangeAdjustedFees = BigInt( -// Math.ceil(Number(fees) * estimatedExchangeRate) -// ) -// const receiveAmountValue = -// BigInt(quote.receiveAmount.value) - exchangeAdjustedFees - -// if (receiveAmountValue <= BigInt(0)) { -// deps.logger.info( -// { fees, exchangeAdjustedFees, estimatedExchangeRate, receiveAmountValue }, -// 'Negative receive amount when calculating quote amount' -// ) -// throw QuoteError.NonPositiveReceiveAmount -// } - -// if (receiveAmountValue > maxReceiveAmountValue) { -// throw QuoteError.InvalidAmount -// } - -// const debitAmountMinusFees = -// quote.debitAmount.value - -// (quote.fee?.calculate(quote.debitAmount.value) ?? 0n) - -// deps.logger.debug( -// { -// 'quote.receiveAmount.value': quote.receiveAmount.value, -// debitAmountValue: quote.debitAmount.value, -// debitAmountMinusFees, -// receiveAmountValue, -// fees, -// exchangeAdjustedFees -// }, -// 'Calculated fixed-send quote amount with fees' -// ) - -// return { -// debitAmountValue: quote.debitAmount.value, -// debitAmountMinusFees, -// receiveAmountValue -// } -// } - -function calculateFixedSendQuoteAmounts2( +function calculateFixedSendQuoteAmounts( deps: ServiceDependencies, quote: UnfinalizedQuote, maxReceiveAmountValue: bigint @@ -614,65 +356,12 @@ function calculateFixedSendQuoteAmounts2( } } -/** - * Calculate fixed-delivery quote amounts: receiveAmount is locked, - * add fees to the the debitAmount. - */ -// function calculateFixedDeliveryQuoteAmounts( -// deps: ServiceDependencies, -// quote: Quote -// ): CalculateQuoteAmountsWithFeesResult { -// const fees = quote.fee?.calculate(quote.debitAmount.value) ?? BigInt(0) - -// const debitAmountValue = BigInt(quote.debitAmount.value) + fees - -// if (debitAmountValue <= BigInt(0)) { -// deps.logger.info( -// { fees, debitAmountValue }, -// 'Received negative debitAmount receive amount when calculating quote amount' -// ) -// throw QuoteError.InvalidAmount -// } - -// deps.logger.debug( -// { debitAmountValue, receiveAmountValue: quote.receiveAmount.value, fees }, -// `Calculated fixed-delivery quote amount with fees` -// ) - -// return { -// debitAmountValue, -// debitAmountMinusFees: quote.debitAmount.value, -// receiveAmountValue: quote.receiveAmount.value -// } -// } - -// function calculateExpiry( -// deps: ServiceDependencies, -// quote: Quote, -// receiver: Receiver -// ): Date { -// const quoteExpiry = new Date( -// quote.createdAt.getTime() + deps.config.quoteLifespan -// ) - -// const incomingPaymentExpiresEarlier = -// receiver.incomingPayment?.expiresAt && -// receiver.incomingPayment.expiresAt.getTime() < quoteExpiry.getTime() - -// return incomingPaymentExpiresEarlier -// ? receiver.incomingPayment!.expiresAt! -// : quoteExpiry -// } - -function calculateExpiry2( +function calculateExpiry( deps: ServiceDependencies, quote: UnfinalizedQuote, receiver: Receiver ): Date { - const quoteExpiry = new Date( - // quote.createdAt.getTime() + deps.config.quoteLifespan - Date.now() + deps.config.quoteLifespan - ) + const quoteExpiry = new Date(Date.now() + deps.config.quoteLifespan) const incomingPaymentExpiresEarlier = receiver.incomingPayment?.expiresAt && @@ -694,7 +383,7 @@ interface QuotePatchOptions { * Calculate fixed-delivery quote amounts: receiveAmount is locked, * add fees to the the debitAmount. */ -function calculateFixedDeliveryQuoteAmounts2( +function calculateFixedDeliveryQuoteAmounts( deps: ServiceDependencies, quote: UnfinalizedQuote ): CalculateQuoteAmountsWithFeesResult { @@ -722,52 +411,7 @@ function calculateFixedDeliveryQuoteAmounts2( } } -// async function finalizeQuote( -// deps: ServiceDependencies, -// options: CreateQuoteOptions, -// quote: Quote, -// receiver: Receiver -// ): Promise { -// let maxReceiveAmountValue: bigint | undefined - -// if (options.debitAmount) { -// const receivingPaymentValue = -// receiver.incomingAmount && receiver.receivedAmount -// ? receiver.incomingAmount.value - receiver.receivedAmount.value -// : undefined -// maxReceiveAmountValue = -// receivingPaymentValue && receivingPaymentValue < quote.receiveAmount.value -// ? receivingPaymentValue -// : quote.receiveAmount.value -// } - -// deps.logger.debug( -// { -// debitAmountValue: quote.debitAmount.value, -// receiveAmountValue: quote.receiveAmount.value, -// maxReceiveAmountValue -// }, -// `Calculating ${maxReceiveAmountValue ? 'fixed-send' : 'fixed-delivery'} quote amount with fees` -// ) - -// const { debitAmountValue, debitAmountMinusFees, receiveAmountValue } = -// maxReceiveAmountValue -// ? calculateFixedSendQuoteAmounts(deps, quote, maxReceiveAmountValue) -// : calculateFixedDeliveryQuoteAmounts(deps, quote) - -// const patchOptions = { -// debitAmountMinusFees, -// debitAmountValue, -// receiveAmountValue, -// expiresAt: calculateExpiry(deps, quote, receiver) -// } - -// await quote.$query(deps.knex).patch(patchOptions) - -// return quote -// } - -async function finalizeQuote2( +async function finalizeQuote( deps: ServiceDependencies, options: CreateQuoteOptions, quote: UnfinalizedQuote, @@ -797,14 +441,14 @@ async function finalizeQuote2( const { debitAmountValue, debitAmountMinusFees, receiveAmountValue } = maxReceiveAmountValue - ? calculateFixedSendQuoteAmounts2(deps, quote, maxReceiveAmountValue) - : calculateFixedDeliveryQuoteAmounts2(deps, quote) + ? calculateFixedSendQuoteAmounts(deps, quote, maxReceiveAmountValue) + : calculateFixedDeliveryQuoteAmounts(deps, quote) const patchOptions = { debitAmountMinusFees, debitAmountValue, receiveAmountValue, - expiresAt: calculateExpiry2(deps, quote, receiver) + expiresAt: calculateExpiry(deps, quote, receiver) } return patchOptions From 86ed0f4596ab81c0aa87b00736ad2c56967345dc Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:39:20 -0500 Subject: [PATCH 11/14] chore: rm comment, restore comment --- .../open_payments/payment/outgoing/service.ts | 22 ------------------- .../src/open_payments/quote/service.ts | 4 ++++ 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 56899e1c1b..12d693ca6d 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -332,29 +332,7 @@ async function createOutgoingPayment( ) stopTimerPeer() - // TODO: Fixes use of trx. To avoid deadlock. - // Deadlock happened when we tried to open a new connection inside - // an Objection transaction block while at max connections already. - // This deadlocks because transaction waits for callback to finish, - // and query in callback waits for connection (ie the transaction - // to finish). Deadlock. - - // Must either: - // - move non-trx db calls OUT of the transaction - // - or use the trx in each db call - // - // Moving out is good but we need to make sure we can safely do that and not introduce data - // inconsistency. I gnerally opted for passing the trx in and not moving stuff out. Felt - // like it was safer and more straightforward fix. However, we should move anything we - // SAFELY can out of the transaction. - - // Moved several things outside transaction... shoudl double check its OK. - // IE, we have the quote id, cant we fetch it before inserting the outgoing payment? - // Also unblocks fetching otehr stuff like peer, asset. - - // *** 1. Begin transaction. fast const payment = await OutgoingPayment.transaction(async (trx) => { - // return await OutgoingPayment.transaction(deps.knex, async (trx) => { if (grantId) { const stopTimerGrant = deps.telemetry.startTimer( 'outgoing_payment_service_insertgrant_time_ms', diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 084e10dfc7..3f3b3606b4 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -305,6 +305,10 @@ interface CalculateQuoteAmountsWithFeesResult { debitAmountMinusFees: bigint } +/** + * Calculate fixed-send quote amounts: debitAmount is locked, + * subtract fees (considering the exchange rate) from the receiveAmount. + */ function calculateFixedSendQuoteAmounts( deps: ServiceDependencies, quote: UnfinalizedQuote, From 480876553e41ca98694c9f6ce527daeb1f20dc81 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:21:55 -0500 Subject: [PATCH 12/14] fix: quote, outgoing payment tests --- packages/backend/src/index.ts | 3 ++- .../open_payments/payment/outgoing/service.ts | 6 +++++- .../backend/src/open_payments/quote/model.ts | 2 +- .../src/open_payments/quote/service.ts | 20 ++++++++----------- packages/backend/src/tests/quote.ts | 5 ++++- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 7267399a7e..dae1f10903 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -514,7 +514,8 @@ export function initIocContainer( walletAddressService: await deps.use('walletAddressService'), quoteService: await deps.use('quoteService'), assetService: await deps.use('assetService'), - telemetry: await deps.use('telemetry') + telemetry: await deps.use('telemetry'), + feeService: await deps.use('feeService') }) }) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 12d693ca6d..bbbed972da 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -45,6 +45,7 @@ import { FilterString } from '../../../shared/filters' import { IAppConfig } from '../../../config/app' import { AssetService } from '../../../asset/service' import { Span, trace } from '@opentelemetry/api' +import { FeeService } from '../../../fee/service' export interface OutgoingPaymentService extends WalletAddressSubresourceService { @@ -72,6 +73,7 @@ export interface ServiceDependencies extends BaseService { quoteService: QuoteService assetService: AssetService telemetry: TelemetryService + feeService: FeeService } export async function createOutgoingPaymentService( @@ -304,6 +306,9 @@ async function createOutgoingPayment( if (!quote) { return OutgoingPaymentError.UnknownQuote } + if (quote.feeId) { + quote.fee = await deps.feeService.get(quote.feeId) + } const asset = await deps.assetService.get(quote.assetId) @@ -467,7 +472,6 @@ async function createOutgoingPayment( 'Could not create outgoing payment: grant locked' ) } - console.log('error when creating op', { err }) throw err } finally { stopTimerOP() diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index 3c6bd6d135..16483a954f 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -22,7 +22,7 @@ export class Quote extends WalletAddressSubresource { public estimatedExchangeRate!: number public feeId?: string - public fee?: Fee + public fee?: Fee | null public debitAmountMinusFees?: bigint diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 3f3b3606b4..a861a6ddbb 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -56,9 +56,7 @@ async function getQuote( deps: ServiceDependencies, options: GetOptions ): Promise { - const quote = await Quote.query(deps.knex) - .get(options) - .withGraphFetched('fee') + const quote = await Quote.query(deps.knex).get(options) if (quote) { const asset = await deps.assetService.get(quote.assetId) if (asset) quote.asset = asset @@ -230,14 +228,13 @@ async function createQuote( description: 'Time to insert quote' } ) - const createdQuote = await Quote.query(deps.knex) - .insertAndFetch({ - ...unfinalizedQuote, - ...finalQuoteOptions - }) - .withGraphFetched('fee') + const createdQuote = await Quote.query(deps.knex).insertAndFetch({ + ...unfinalizedQuote, + ...finalQuoteOptions + }) createdQuote.asset = walletAddress.asset createdQuote.walletAddress = walletAddress + createdQuote.fee = sendingFee stopQuoteCreate() return createdQuote @@ -462,9 +459,8 @@ async function getWalletAddressPage( deps: ServiceDependencies, options: ListOptions ): Promise { - const quotes = await Quote.query(deps.knex) - .list(options) - .withGraphFetched('fee') + const quotes = await Quote.query(deps.knex).list(options) + // .withGraphFetched('fee') for (const quote of quotes) { const asset = await deps.assetService.get(quote.assetId) if (asset) quote.asset = asset diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index 5992a66e8b..2bfc437ca7 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -171,7 +171,7 @@ export async function createQuote( } const withGraphFetchedExpression = `[${withGraphFetchedArray.join(', ')}]` - return Quote.query() + const quote = await Quote.query() .insertAndFetch({ id: quoteId, walletAddressId, @@ -185,4 +185,7 @@ export async function createQuote( client }) .withGraphFetched(withGraphFetchedExpression) + quote.fee = quote.fee ?? undefined + + return quote } From f0d010a16ad8cdf69157834faaeb982ae9e599b9 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 27 Feb 2025 10:31:57 -0500 Subject: [PATCH 13/14] chore: rm comment --- packages/backend/src/open_payments/quote/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index a861a6ddbb..675aae0804 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -460,7 +460,6 @@ async function getWalletAddressPage( options: ListOptions ): Promise { const quotes = await Quote.query(deps.knex).list(options) - // .withGraphFetched('fee') for (const quote of quotes) { const asset = await deps.assetService.get(quote.assetId) if (asset) quote.asset = asset From cdac8322e7c43731c722413d8c481dc78e252ae4 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:13:16 -0500 Subject: [PATCH 14/14] fix(performance): local test to use incoming payment, not receiver --- .../scripts/create-local-outgoing-payments.js | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/test/performance/scripts/create-local-outgoing-payments.js b/test/performance/scripts/create-local-outgoing-payments.js index 2a5398fa15..7cfe14f69f 100644 --- a/test/performance/scripts/create-local-outgoing-payments.js +++ b/test/performance/scripts/create-local-outgoing-payments.js @@ -60,14 +60,20 @@ export function setup() { const data = request(query) const c9WalletAddresses = data.walletAddresses.edges - const c9WalletAddress = c9WalletAddresses.find( + const senderWalletAddress = c9WalletAddresses.find( (edge) => edge.node.url === SENDER_WALLET_ADDRESS ).node - if (!c9WalletAddress) { + if (!senderWalletAddress) { fail(`could not find wallet address: ${SENDER_WALLET_ADDRESS}`) } + const receiverWalletAddress = c9WalletAddresses.find( + (edge) => edge.node.url === RECEIVER_WALLET_ADDRESS + ).node + if (!receiverWalletAddress) { + fail(`could not find wallet address: ${RECEIVER_WALLET_ADDRESS}`) + } - return { data: { c9WalletAddress } } + return { senderWalletAddress, receiverWalletAddress } } // The function that defines VU logic. @@ -76,15 +82,13 @@ export function setup() { // about authoring k6 scripts. // export default function (data) { - const { - data: { c9WalletAddress } - } = data + const { senderWalletAddress, receiverWalletAddress } = data - const createReceiverPayload = { + const createIncomingPaymentPayload = { query: ` - mutation CreateReceiver($input: CreateReceiverInput!) { - createReceiver(input: $input) { - receiver { + mutation CreateIncomingPayment($input: CreateIncomingPaymentInput!) { + createIncomingPayment(input: $input) { + payment { id } } @@ -93,22 +97,19 @@ export default function (data) { variables: { input: { expiresAt: null, - metadata: { - description: 'Hello my friend', - externalRef: null - }, incomingAmount: { assetCode: 'USD', assetScale: 2, value: 1002 }, - walletAddressUrl: RECEIVER_WALLET_ADDRESS + walletAddressId: receiverWalletAddress.id } } } - const createReceiverResponse = request(createReceiverPayload) - const receiver = createReceiverResponse.createReceiver.receiver + const createIncomingPaymentResponse = request(createIncomingPaymentPayload) + const incomingPayment = + createIncomingPaymentResponse.createIncomingPayment.payment const createQuotePayload = { query: ` @@ -122,9 +123,9 @@ export default function (data) { `, variables: { input: { - walletAddressId: c9WalletAddress.id, + walletAddressId: senderWalletAddress.id, receiveAmount: null, - receiver: receiver.id, + receiver: `https://cloud-nine-wallet-backend/incoming-payments/${incomingPayment.id}`, debitAmount: { assetCode: 'USD', assetScale: 2, @@ -149,7 +150,7 @@ export default function (data) { `, variables: { input: { - walletAddressId: c9WalletAddress.id, + walletAddressId: senderWalletAddress.id, quoteId: quote.id } }