From bb771528c663cea90c4d87fcfb5bf93493bdb0ae Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:49:18 -0400 Subject: [PATCH 01/64] feat(backend): add local payment quote migration --- ...40809171654_add_ilp_quote_details_table.js | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js diff --git a/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js b/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js new file mode 100644 index 0000000000..8811674c58 --- /dev/null +++ b/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js @@ -0,0 +1,130 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return ( + knex.schema + // Create new table with columns from "quotes" to migrate + .createTable('ilpQuoteDetails', function (table) { + table.uuid('id').notNullable().primary() + table.uuid('quoteId').notNullable().unique() + table.foreign('quoteId').references('quotes.id') + + table.bigInteger('maxPacketAmount').notNullable() + table.decimal('minExchangeRateNumerator', 64, 0).notNullable() + table.decimal('minExchangeRateDenominator', 64, 0).notNullable() + table.decimal('lowEstimatedExchangeRateNumerator', 64, 0).notNullable() + table + .decimal('lowEstimatedExchangeRateDenominator', 64, 0) + .notNullable() + table.decimal('highEstimatedExchangeRateNumerator', 64, 0).notNullable() + table + .decimal('highEstimatedExchangeRateDenominator', 64, 0) + .notNullable() + }) + .then(() => { + // Enable uuid_generate_v4 + return knex.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`) + }) + .then(() => { + // Migrate data from quotes to ilpQuoteDetails. + return knex.raw(` + INSERT INTO "ilpQuoteDetails" ( + id, + "quoteId", + "maxPacketAmount", + "minExchangeRateNumerator", + "minExchangeRateDenominator", + "lowEstimatedExchangeRateNumerator", + "lowEstimatedExchangeRateDenominator", + "highEstimatedExchangeRateNumerator", + "highEstimatedExchangeRateDenominator" + ) + SELECT + uuid_generate_v4(), + id AS "quoteId", + "maxPacketAmount", + "minExchangeRateNumerator", + "minExchangeRateDenominator", + "lowEstimatedExchangeRateNumerator", + "lowEstimatedExchangeRateDenominator", + "highEstimatedExchangeRateNumerator", + "highEstimatedExchangeRateDenominator" + FROM "quotes"; + `) + }) + .then(() => { + return knex.schema.alterTable('quotes', function (table) { + table.dropColumn('maxPacketAmount') + table.dropColumn('minExchangeRateNumerator') + table.dropColumn('minExchangeRateDenominator') + table.dropColumn('lowEstimatedExchangeRateNumerator') + table.dropColumn('lowEstimatedExchangeRateDenominator') + table.dropColumn('highEstimatedExchangeRateNumerator') + table.dropColumn('highEstimatedExchangeRateDenominator') + }) + }) + ) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema + .alterTable('quotes', function (table) { + // restore columns without not null constraint + table.bigInteger('maxPacketAmount') + table.decimal('minExchangeRateNumerator', 64, 0) + table.decimal('minExchangeRateDenominator', 64, 0) + table.decimal('lowEstimatedExchangeRateNumerator', 64, 0) + table.decimal('lowEstimatedExchangeRateDenominator', 64, 0) + table.decimal('highEstimatedExchangeRateNumerator', 64, 0) + table.decimal('highEstimatedExchangeRateDenominator', 64, 0) + }) + .then(() => { + // Migrate data back to quotes table from ilpQuote + return knex.raw(` + UPDATE "quotes" + SET + "maxPacketAmount" = "ilpQuoteDetails"."maxPacketAmount", + "minExchangeRateNumerator" = "ilpQuoteDetails"."minExchangeRateNumerator", + "minExchangeRateDenominator" = "ilpQuoteDetails"."minExchangeRateDenominator", + "lowEstimatedExchangeRateNumerator" = "ilpQuoteDetails"."lowEstimatedExchangeRateNumerator", + "lowEstimatedExchangeRateDenominator" = "ilpQuoteDetails"."lowEstimatedExchangeRateDenominator", + "highEstimatedExchangeRateNumerator" = "ilpQuoteDetails"."highEstimatedExchangeRateNumerator", + "highEstimatedExchangeRateDenominator" = "ilpQuoteDetails"."highEstimatedExchangeRateDenominator" + FROM "ilpQuoteDetails" + WHERE "quotes"."id" = "ilpQuoteDetails"."quoteId" + `) + }) + .then(() => { + // Apply the not null constraints after data insertion + return knex.schema.alterTable('quotes', function (table) { + table.bigInteger('maxPacketAmount').notNullable().alter() + table.decimal('minExchangeRateNumerator', 64, 0).notNullable().alter() + table.decimal('minExchangeRateDenominator', 64, 0).notNullable().alter() + table + .decimal('lowEstimatedExchangeRateNumerator', 64, 0) + .notNullable() + .alter() + table + .decimal('lowEstimatedExchangeRateDenominator', 64, 0) + .notNullable() + .alter() + table + .decimal('highEstimatedExchangeRateNumerator', 64, 0) + .notNullable() + .alter() + table + .decimal('highEstimatedExchangeRateDenominator', 64, 0) + .notNullable() + .alter() + }) + }) + .then(() => { + return knex.schema.dropTableIfExists('ilpQuoteDetails') + }) +} From fc06e24ec357ca7d3a991d41f6022de79220cd34 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:54:20 -0400 Subject: [PATCH 02/64] feat(backend): WIP seperate ILPModels, LocalQuote, BaseQuote models - trouble updating the services. this may not be the best way. also not sure I can prevent querying for non-local quotes on LocalQuote (and vice-versa for ILPQuote). - the idea behind seperate models was a firm Quote type (it has the ilp specific props or it doesnt instead of 1 type MAYBE having them) - typeguards could work instead but seemed messier. or maybe I can still have seperate quote service methods returning different types? --- ...40809171654_add_ilp_quote_details_table.js | 25 +++ .../backend/src/open_payments/quote/model.ts | 152 +++++++++++++----- .../src/open_payments/quote/service.ts | 26 +-- 3 files changed, 153 insertions(+), 50 deletions(-) diff --git a/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js b/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js index 8811674c58..19974a3b86 100644 --- a/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js +++ b/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js @@ -54,6 +54,26 @@ exports.up = function (knex) { FROM "quotes"; `) }) + // .then(() => { + // return knex.schema.alterTable('quotes', function (table) { + // table.enum('type', ['ILP', 'LOCAL']) + // }) + // }) + .then(() => { + // TODO: enum type. alteration to non-nullable complicated + // https://github.com/knex/knex/issues/1699 + return knex.schema.alterTable('quotes', function (table) { + table.string('type') + }) + }) + .then(() => { + return knex('quotes').update({ type: 'ILP' }) + }) + .then(() => { + return knex.schema.alterTable('quotes', function (table) { + table.string('type').notNullable().alter() + }) + }) .then(() => { return knex.schema.alterTable('quotes', function (table) { table.dropColumn('maxPacketAmount') @@ -124,6 +144,11 @@ exports.down = function (knex) { .alter() }) }) + .then(() => { + return knex.schema.alterTable('quotes', function (table) { + table.dropColumn('type') + }) + }) .then(() => { return knex.schema.dropTableIfExists('ilpQuoteDetails') }) diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index 39631f7240..1340f2f00d 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -10,21 +10,20 @@ import { Asset } from '../../asset/model' import { Quote as OpenPaymentsQuote } from '@interledger/open-payments' import { Fee } from '../../fee/model' -export class Quote extends WalletAddressSubresource { +export enum QuoteType { + ILP = 'ILP', + LOCAL = 'LOCAL' +} +export class BaseQuote extends WalletAddressSubresource { public static readonly tableName = 'quotes' public static readonly urlPath = '/quotes' static get virtualAttributes(): string[] { - return [ - 'debitAmount', - 'receiveAmount', - 'minExchangeRate', - 'lowEstimatedExchangeRate', - 'highEstimatedExchangeRate', - 'method' - ] + return ['debitAmount', 'receiveAmount', 'method'] } + public type!: QuoteType + // Asset id of the sender public assetId!: string public asset!: Asset @@ -52,6 +51,15 @@ export class Quote extends WalletAddressSubresource { to: 'fees.id' } } + // TODO: use or lose + // ilpQuoteDetails: { + // relation: Model.HasOneRelation, + // modelClass: IlpQuoteDetails, + // join: { + // from: 'quotes.id', + // to: 'ilp_quotes.quoteId' + // } + // } } } @@ -59,7 +67,7 @@ export class Quote extends WalletAddressSubresource { public receiver!: string - private debitAmountValue!: bigint + protected debitAmountValue!: bigint public getUrl(walletAddress: WalletAddress): string { const url = new URL(walletAddress.url) @@ -78,7 +86,7 @@ export class Quote extends WalletAddressSubresource { this.debitAmountValue = amount.value } - private receiveAmountValue!: bigint + protected receiveAmountValue!: bigint private receiveAmountAssetCode!: string private receiveAmountAssetScale!: number @@ -96,6 +104,74 @@ export class Quote extends WalletAddressSubresource { this.receiveAmountAssetScale = amount?.assetScale } + public get method(): 'ilp' { + return 'ilp' + } + + $formatJson(json: Pojo): Pojo { + json = super.$formatJson(json) + return { + id: json.id, + walletAddressId: json.walletAddressId, + receiver: json.receiver, + debitAmount: { + ...json.debitAmount, + value: json.debitAmount.value.toString() + }, + receiveAmount: { + ...json.receiveAmount, + value: json.receiveAmount.value.toString() + }, + createdAt: json.createdAt, + expiresAt: json.expiresAt.toISOString() + } + } + + public toOpenPaymentsType(walletAddress: WalletAddress): OpenPaymentsQuote { + return { + id: this.getUrl(walletAddress), + walletAddress: walletAddress.url, + receiveAmount: serializeAmount(this.receiveAmount), + debitAmount: serializeAmount(this.debitAmount), + receiver: this.receiver, + expiresAt: this.expiresAt.toISOString(), + createdAt: this.createdAt.toISOString(), + method: this.method + } + } +} + +export class LocalQuote extends BaseQuote { + $beforeInsert() { + this.type = QuoteType.LOCAL + } +} + +// TODO: rename to ilp quote +export class Quote extends BaseQuote { + static get virtualAttributes(): string[] { + return [ + ...super.virtualAttributes, + 'minExchangeRate', + 'lowEstimatedExchangeRate', + 'highEstimatedExchangeRate' + ] + } + + static get relationMappings() { + return { + ...super.relationMappings, + ilpQuoteDetails: { + relation: Model.HasOneRelation, + modelClass: IlpQuoteDetails, + join: { + from: 'quotes.id', + to: 'ilp_quotes.quoteId' + } + } + } + } + public maxPacketAmount!: bigint private minExchangeRateNumerator!: bigint private minExchangeRateDenominator!: bigint @@ -153,39 +229,37 @@ export class Quote extends WalletAddressSubresource { this.highEstimatedExchangeRateDenominator = value.b.value } - public get method(): 'ilp' { - return 'ilp' + $beforeInsert() { + this.type = QuoteType.ILP } +} - $formatJson(json: Pojo): Pojo { - json = super.$formatJson(json) - return { - id: json.id, - walletAddressId: json.walletAddressId, - receiver: json.receiver, - debitAmount: { - ...json.debitAmount, - value: json.debitAmount.value.toString() - }, - receiveAmount: { - ...json.receiveAmount, - value: json.receiveAmount.value.toString() - }, - createdAt: json.createdAt, - expiresAt: json.expiresAt.toISOString() - } +class IlpQuoteDetails extends Model { + static get tableName() { + return 'ilpQuoteDetails' } - public toOpenPaymentsType(walletAddress: WalletAddress): OpenPaymentsQuote { + static get relationMappings() { return { - id: this.getUrl(walletAddress), - walletAddress: walletAddress.url, - receiveAmount: serializeAmount(this.receiveAmount), - debitAmount: serializeAmount(this.debitAmount), - receiver: this.receiver, - expiresAt: this.expiresAt.toISOString(), - createdAt: this.createdAt.toISOString(), - method: this.method + quote: { + relation: Model.BelongsToOneRelation, + modelClass: Quote, + join: { + from: 'ilp_quote_details.quoteId', + to: 'quotes.id' + } + } } } + + public quoteId!: string + public maxPacketAmount!: bigint + public minExchangeRate!: Pay.Ratio + public lowEstimatedExchangeRate!: Pay.Ratio + public highEstimatedExchangeRate!: Pay.PositiveRatio + + // TODO: use or lose + // static get idColumn() { + // return 'quoteId'; + // } } diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 04d9df7f87..13bfa0bc3e 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -3,7 +3,7 @@ import * as Pay from '@interledger/pay' import { BaseService } from '../../shared/baseService' import { QuoteError, isQuoteError } from './errors' -import { Quote } from './model' +import { Quote, QuoteType } from './model' import { Amount } from '../amount' import { ReceiverService } from '../receiver/service' import { Receiver } from '../receiver/model' @@ -56,7 +56,7 @@ async function getQuote( ): Promise { return Quote.query(deps.knex) .get(options) - .withGraphFetched('[asset, fee, walletAddress]') + .withGraphFetched('[asset, fee, walletAddress, ilpQuoteDetails]') } interface QuoteOptionsBase { @@ -129,25 +129,29 @@ async function createQuote( return await Quote.transaction(deps.knex, async (trx) => { const createdQuote = await Quote.query(trx) - .insertAndFetch({ + .insertGraphAndFetch({ walletAddressId: options.walletAddressId, assetId: walletAddress.assetId, receiver: options.receiver, debitAmount: quote.debitAmount, receiveAmount: quote.receiveAmount, - maxPacketAmount: - MAX_INT64 < maxPacketAmount ? MAX_INT64 : maxPacketAmount, // Cap at MAX_INT64 because of postgres type limits. - minExchangeRate: quote.additionalFields.minExchangeRate as Pay.Ratio, - lowEstimatedExchangeRate: quote.additionalFields - .lowEstimatedExchangeRate as Pay.Ratio, - highEstimatedExchangeRate: quote.additionalFields - .highEstimatedExchangeRate as Pay.PositiveRatio, + type: QuoteType.ILP, + ilpQuoteDetails: { + maxPacketAmount: + MAX_INT64 < maxPacketAmount ? MAX_INT64 : maxPacketAmount, // Cap at MAX_INT64 because of postgres type limits. + minExchangeRate: quote.additionalFields + .minExchangeRate as Pay.Ratio, + lowEstimatedExchangeRate: quote.additionalFields + .lowEstimatedExchangeRate as Pay.Ratio, + highEstimatedExchangeRate: quote.additionalFields + .highEstimatedExchangeRate as Pay.PositiveRatio + }, expiresAt: new Date(0), // expiresAt is patched in finalizeQuote client: options.client, feeId: sendingFee?.id, estimatedExchangeRate: quote.estimatedExchangeRate }) - .withGraphFetched('[asset, fee, walletAddress]') + .withGraphFetched('[asset, fee, walletAddress, ilpQuoteDetails]') return await finalizeQuote( { From 257fd01e54c5de334a8d418bf9fc92b888ac041c Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:33:29 -0400 Subject: [PATCH 03/64] refactor(backend): change model/services to reflect optional ilp quote details - includes some WIP changes including gql field removal and handling missing ilp quote details --- ...40809171654_add_ilp_quote_details_table.js | 38 ++--- .../resolvers/outgoing_payment.test.ts | 12 +- .../src/graphql/resolvers/quote.test.ts | 8 +- .../backend/src/graphql/resolvers/quote.ts | 9 +- .../payment/outgoing/lifecycle.ts | 6 +- .../payment/outgoing/service.test.ts | 4 +- .../open_payments/payment/outgoing/service.ts | 12 +- .../open_payments/payment/outgoing/worker.ts | 2 +- .../backend/src/open_payments/quote/model.ts | 130 +++++++----------- .../src/open_payments/quote/service.test.ts | 18 ++- .../src/open_payments/quote/service.ts | 8 +- .../backend/src/payment-method/ilp/service.ts | 14 +- packages/backend/src/tests/quote.ts | 22 ++- 13 files changed, 126 insertions(+), 157 deletions(-) diff --git a/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js b/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js index 19974a3b86..23d7ad0e5c 100644 --- a/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js +++ b/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js @@ -22,6 +22,9 @@ exports.up = function (knex) { table .decimal('highEstimatedExchangeRateDenominator', 64, 0) .notNullable() + + table.timestamp('createdAt').defaultTo(knex.fn.now()) + table.timestamp('updatedAt').defaultTo(knex.fn.now()) }) .then(() => { // Enable uuid_generate_v4 @@ -54,25 +57,19 @@ exports.up = function (knex) { FROM "quotes"; `) }) - // .then(() => { - // return knex.schema.alterTable('quotes', function (table) { - // table.enum('type', ['ILP', 'LOCAL']) - // }) - // }) .then(() => { - // TODO: enum type. alteration to non-nullable complicated - // https://github.com/knex/knex/issues/1699 - return knex.schema.alterTable('quotes', function (table) { - table.string('type') - }) - }) - .then(() => { - return knex('quotes').update({ type: 'ILP' }) - }) - .then(() => { - return knex.schema.alterTable('quotes', function (table) { - table.string('type').notNullable().alter() - }) + // TODO: test this more thoroughly. + // Might need to seed in migration preceeding this? + // Cant simply withold htis migration. Application code will fail when trying + // to insert ... + return knex('quotes') + .whereNull('estimatedExchangeRate') + .update({ + estimatedExchangeRate: knex.raw('?? / ??', [ + 'lowEstimatedExchangeRateNumerator', + 'lowEstimatedExchangeRateDenominator' + ]) + }) }) .then(() => { return knex.schema.alterTable('quotes', function (table) { @@ -144,11 +141,6 @@ exports.down = function (knex) { .alter() }) }) - .then(() => { - return knex.schema.alterTable('quotes', function (table) { - table.dropColumn('type') - }) - }) .then(() => { return knex.schema.dropTableIfExists('ilpQuoteDetails') }) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index 71c51affcc..84b3ab0e8f 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -245,12 +245,12 @@ describe('OutgoingPayment Resolvers', (): void => { metadata, quote: { id: payment.quote.id, - maxPacketAmount: payment.quote.maxPacketAmount.toString(), - minExchangeRate: payment.quote.minExchangeRate.valueOf(), - lowEstimatedExchangeRate: - payment.quote.lowEstimatedExchangeRate.valueOf(), - highEstimatedExchangeRate: - payment.quote.highEstimatedExchangeRate.valueOf(), + // maxPacketAmount: payment.quote.maxPacketAmount.toString(), + // minExchangeRate: payment.quote.minExchangeRate.valueOf(), + // lowEstimatedExchangeRate: + // payment.quote.lowEstimatedExchangeRate.valueOf(), + // highEstimatedExchangeRate: + // payment.quote.highEstimatedExchangeRate.valueOf(), createdAt: payment.quote.createdAt.toISOString(), expiresAt: payment.quote.expiresAt.toISOString(), __typename: 'Quote' diff --git a/packages/backend/src/graphql/resolvers/quote.test.ts b/packages/backend/src/graphql/resolvers/quote.test.ts index 39efdac43c..4375900945 100644 --- a/packages/backend/src/graphql/resolvers/quote.test.ts +++ b/packages/backend/src/graphql/resolvers/quote.test.ts @@ -124,10 +124,10 @@ describe('Quote Resolvers', (): void => { assetScale: quote.receiveAmount.assetScale, __typename: 'Amount' }, - maxPacketAmount: quote.maxPacketAmount.toString(), - minExchangeRate: quote.minExchangeRate.valueOf(), - lowEstimatedExchangeRate: quote.lowEstimatedExchangeRate.valueOf(), - highEstimatedExchangeRate: quote.highEstimatedExchangeRate.valueOf(), + // maxPacketAmount: quote.maxPacketAmount.toString(), + // minExchangeRate: quote.minExchangeRate.valueOf(), + // lowEstimatedExchangeRate: quote.lowEstimatedExchangeRate.valueOf(), + // highEstimatedExchangeRate: quote.highEstimatedExchangeRate.valueOf(), createdAt: quote.createdAt.toISOString(), expiresAt: quote.expiresAt.toISOString(), __typename: 'Quote' diff --git a/packages/backend/src/graphql/resolvers/quote.ts b/packages/backend/src/graphql/resolvers/quote.ts index 638c0fecde..abf2ebcbbc 100644 --- a/packages/backend/src/graphql/resolvers/quote.ts +++ b/packages/backend/src/graphql/resolvers/quote.ts @@ -97,6 +97,7 @@ export const getWalletAddressQuotes: WalletAddressResolvers['quot } } +// TODO: update gql types (there is a pr pending for this) export function quoteToGraphql(quote: Quote): SchemaQuote { return { id: quote.id, @@ -104,10 +105,10 @@ export function quoteToGraphql(quote: Quote): SchemaQuote { receiver: quote.receiver, debitAmount: quote.debitAmount, receiveAmount: quote.receiveAmount, - maxPacketAmount: quote.maxPacketAmount, - minExchangeRate: quote.minExchangeRate.valueOf(), - lowEstimatedExchangeRate: quote.lowEstimatedExchangeRate.valueOf(), - highEstimatedExchangeRate: quote.highEstimatedExchangeRate.valueOf(), + maxPacketAmount: 0n, //quote.maxPacketAmount, + minExchangeRate: 0, //quote.minExchangeRate.valueOf(), + lowEstimatedExchangeRate: 0, //quote.lowEstimatedExchangeRate.valueOf(), + highEstimatedExchangeRate: 0, //quote.highEstimatedExchangeRate.valueOf(), createdAt: new Date(+quote.createdAt).toISOString(), expiresAt: new Date(+quote.expiresAt).toISOString() } diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index a825bd6515..c649e6fb19 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -109,11 +109,7 @@ function getAdjustedAmounts( // This is only an approximation of the true amount delivered due to exchange rate variance. Due to connection failures there isn't a reliable way to track that in sync with the amount sent (particularly within ILP payments) // eslint-disable-next-line no-case-declarations const amountDelivered = BigInt( - Math.ceil( - Number(alreadySentAmount) * - (payment.quote.estimatedExchangeRate || - payment.quote.lowEstimatedExchangeRate.valueOf()) - ) + Math.ceil(Number(alreadySentAmount) * payment.quote.estimatedExchangeRate) ) let maxReceiveAmount = payment.receiveAmount.value - amountDelivered diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 41085233d4..7f1a1064d5 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -778,8 +778,10 @@ describe('OutgoingPaymentService', (): void => { validDestination: false, method: 'ilp' }) + const pastDate = new Date() + pastDate.setMinutes(pastDate.getMinutes() - 5) await quote.$query(knex).patch({ - expiresAt: new Date() + expiresAt: pastDate }) await expect( outgoingPaymentService.create({ diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index bb44909ae6..a3a5393560 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -89,7 +89,7 @@ async function getOutgoingPayment( ): Promise { const outgoingPayment = await OutgoingPayment.query(deps.knex) .get(options) - .withGraphFetched('[quote.asset, walletAddress]') + .withGraphFetched('[quote.[asset, ilpQuoteDetails], walletAddress]') if (outgoingPayment) { return addSentAmount(deps, outgoingPayment) @@ -150,7 +150,7 @@ async function cancelOutgoingPayment( ...(options.reason ? { cancellationReason: options.reason } : {}) } }) - .withGraphFetched('[quote.asset, walletAddress]') + .withGraphFetched('[quote.[asset, ilpQuoteDetails], walletAddress]') return addSentAmount(deps, payment) }) @@ -208,7 +208,7 @@ async function createOutgoingPayment( state: OutgoingPaymentState.Funding, grantId }) - .withGraphFetched('[quote.asset, walletAddress]') + .withGraphFetched('[quote.[asset, ilpQuoteDetails], walletAddress]') if ( payment.walletAddressId !== payment.quote.walletAddressId || @@ -374,7 +374,7 @@ async function validateGrantAndAddSpentAmountsToPayment( .andWhereNot({ id: payment.id }) - .withGraphFetched('[quote.asset]') + .withGraphFetched('[quote.[asset, ilpQuoteDetails]]') if (grantPayments.length === 0) { return true @@ -451,7 +451,7 @@ async function fundPayment( const payment = await OutgoingPayment.query(trx) .findById(id) .forUpdate() - .withGraphFetched('[quote.asset]') + .withGraphFetched('[quote.[asset, ilpQuoteDetails]]') if (!payment) return FundingError.UnknownPayment if (payment.state !== OutgoingPaymentState.Funding) { return FundingError.WrongState @@ -495,7 +495,7 @@ async function getWalletAddressPage( ): Promise { const page = await OutgoingPayment.query(deps.knex) .list(options) - .withGraphFetched('[quote.asset, walletAddress]') + .withGraphFetched('[quote.[asset, ilpQuoteDetails], walletAddress]') const amounts = await deps.accountingService.getAccountsTotalSent( page.map((payment: OutgoingPayment) => payment.id) ) diff --git a/packages/backend/src/open_payments/payment/outgoing/worker.ts b/packages/backend/src/open_payments/payment/outgoing/worker.ts index 4c9e35cf60..b82754a7e4 100644 --- a/packages/backend/src/open_payments/payment/outgoing/worker.ts +++ b/packages/backend/src/open_payments/payment/outgoing/worker.ts @@ -55,7 +55,7 @@ async function getPendingPayment( [RETRY_BACKOFF_SECONDS, now] ) }) - .withGraphFetched('[walletAddress, quote.asset]') + .withGraphFetched('[walletAddress, quote.[asset, ilpQuoteDetails]]') return payments[0] } diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index 1340f2f00d..c179585fe2 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -9,12 +9,15 @@ import { import { Asset } from '../../asset/model' import { Quote as OpenPaymentsQuote } from '@interledger/open-payments' import { Fee } from '../../fee/model' +import { BaseModel } from '../../shared/baseModel' -export enum QuoteType { - ILP = 'ILP', - LOCAL = 'LOCAL' -} -export class BaseQuote extends WalletAddressSubresource { +// TODO: use or lose. could maybe be used as a typegaurd instead of checking that details +// field(s) are present +// export interface QuoteWithDetails extends Quote { +// ilpQuoteDetails: IlpQuoteDetails +// } + +export class Quote extends WalletAddressSubresource { public static readonly tableName = 'quotes' public static readonly urlPath = '/quotes' @@ -22,16 +25,16 @@ export class BaseQuote extends WalletAddressSubresource { return ['debitAmount', 'receiveAmount', 'method'] } - public type!: QuoteType - // Asset id of the sender public assetId!: string public asset!: Asset - public estimatedExchangeRate?: number + public estimatedExchangeRate!: number public feeId?: string public fee?: Fee + public ilpQuoteDetails?: IlpQuoteDetails + static get relationMappings() { return { ...super.relationMappings, @@ -50,16 +53,15 @@ export class BaseQuote extends WalletAddressSubresource { from: 'quotes.feeId', to: 'fees.id' } + }, + ilpQuoteDetails: { + relation: Model.HasOneRelation, + modelClass: IlpQuoteDetails, + join: { + from: 'ilpQuoteDetails.quoteId', + to: 'quotes.id' + } } - // TODO: use or lose - // ilpQuoteDetails: { - // relation: Model.HasOneRelation, - // modelClass: IlpQuoteDetails, - // join: { - // from: 'quotes.id', - // to: 'ilp_quotes.quoteId' - // } - // } } } @@ -67,7 +69,7 @@ export class BaseQuote extends WalletAddressSubresource { public receiver!: string - protected debitAmountValue!: bigint + private debitAmountValue!: bigint public getUrl(walletAddress: WalletAddress): string { const url = new URL(walletAddress.url) @@ -86,7 +88,7 @@ export class BaseQuote extends WalletAddressSubresource { this.debitAmountValue = amount.value } - protected receiveAmountValue!: bigint + private receiveAmountValue!: bigint private receiveAmountAssetCode!: string private receiveAmountAssetScale!: number @@ -104,6 +106,14 @@ export class BaseQuote extends WalletAddressSubresource { this.receiveAmountAssetScale = amount?.assetScale } + public get maxSourceAmount(): bigint { + return this.debitAmountValue + } + + public get minDeliveryAmount(): bigint { + return this.receiveAmountValue + } + public get method(): 'ilp' { return 'ilp' } @@ -141,53 +151,41 @@ export class BaseQuote extends WalletAddressSubresource { } } -export class LocalQuote extends BaseQuote { - $beforeInsert() { - this.type = QuoteType.LOCAL - } -} +export class IlpQuoteDetails extends BaseModel { + public static readonly tableName = 'ilpQuoteDetails' -// TODO: rename to ilp quote -export class Quote extends BaseQuote { static get virtualAttributes(): string[] { return [ - ...super.virtualAttributes, 'minExchangeRate', 'lowEstimatedExchangeRate', 'highEstimatedExchangeRate' ] } + public quoteId!: string + public quote?: Quote + + public maxPacketAmount!: bigint + public minExchangeRateNumerator!: bigint + public minExchangeRateDenominator!: bigint + public lowEstimatedExchangeRateNumerator!: bigint + public lowEstimatedExchangeRateDenominator!: bigint + public highEstimatedExchangeRateNumerator!: bigint + public highEstimatedExchangeRateDenominator!: bigint + static get relationMappings() { return { - ...super.relationMappings, - ilpQuoteDetails: { - relation: Model.HasOneRelation, - modelClass: IlpQuoteDetails, + quote: { + relation: Model.BelongsToOneRelation, + modelClass: Quote, join: { - from: 'quotes.id', - to: 'ilp_quotes.quoteId' + from: 'ilpQuoteDetails.quoteId', + to: 'quotes.id' } } } } - public maxPacketAmount!: bigint - private minExchangeRateNumerator!: bigint - private minExchangeRateDenominator!: bigint - private lowEstimatedExchangeRateNumerator!: bigint - private lowEstimatedExchangeRateDenominator!: bigint - private highEstimatedExchangeRateNumerator!: bigint - private highEstimatedExchangeRateDenominator!: bigint - - public get maxSourceAmount(): bigint { - return this.debitAmountValue - } - - public get minDeliveryAmount(): bigint { - return this.receiveAmountValue - } - public get minExchangeRate(): Pay.Ratio { return Pay.Ratio.of( Pay.Int.from(this.minExchangeRateNumerator) as Pay.PositiveInt, @@ -228,38 +226,4 @@ export class Quote extends BaseQuote { this.highEstimatedExchangeRateNumerator = value.a.value this.highEstimatedExchangeRateDenominator = value.b.value } - - $beforeInsert() { - this.type = QuoteType.ILP - } -} - -class IlpQuoteDetails extends Model { - static get tableName() { - return 'ilpQuoteDetails' - } - - static get relationMappings() { - return { - quote: { - relation: Model.BelongsToOneRelation, - modelClass: Quote, - join: { - from: 'ilp_quote_details.quoteId', - to: 'quotes.id' - } - } - } - } - - public quoteId!: string - public maxPacketAmount!: bigint - public minExchangeRate!: Pay.Ratio - public lowEstimatedExchangeRate!: Pay.Ratio - public highEstimatedExchangeRate!: Pay.PositiveRatio - - // TODO: use or lose - // static get idColumn() { - // return 'quoteId'; - // } } diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 4bddd700f5..1ba449505a 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -206,7 +206,9 @@ describe('QuoteService', (): void => { receiver: options.receiver, debitAmount: debitAmount || mockedQuote.debitAmount, receiveAmount: receiveAmount || mockedQuote.receiveAmount, - maxPacketAmount: BigInt('9223372036854775807'), + ilpQuoteDetails: { + maxPacketAmount: BigInt('9223372036854775807') + }, createdAt: expect.any(Date), updatedAt: expect.any(Date), expiresAt: new Date( @@ -291,7 +293,9 @@ describe('QuoteService', (): void => { expect(quote).toMatchObject({ ...options, - maxPacketAmount: BigInt('9223372036854775807'), + ilpQuoteDetails: { + maxPacketAmount: BigInt('9223372036854775807') + }, debitAmount: mockedQuote.debitAmount, receiveAmount: incomingAmount, createdAt: expect.any(Date), @@ -425,10 +429,12 @@ describe('QuoteService', (): void => { ).resolves.toMatchObject({ debitAmount: mockedQuote.debitAmount, receiveAmount: receiver.incomingAmount, - maxPacketAmount: BigInt('9223372036854775807'), - lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - minExchangeRate: Pay.Ratio.from(10 ** 20) + ilpQuoteDetails: { + maxPacketAmount: BigInt('9223372036854775807'), + lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), + highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), + minExchangeRate: Pay.Ratio.from(10 ** 20) + } }) }) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 13bfa0bc3e..51588acf1b 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -3,7 +3,7 @@ import * as Pay from '@interledger/pay' import { BaseService } from '../../shared/baseService' import { QuoteError, isQuoteError } from './errors' -import { Quote, QuoteType } from './model' +import { Quote } from './model' import { Amount } from '../amount' import { ReceiverService } from '../receiver/service' import { Receiver } from '../receiver/model' @@ -135,7 +135,6 @@ async function createQuote( receiver: options.receiver, debitAmount: quote.debitAmount, receiveAmount: quote.receiveAmount, - type: QuoteType.ILP, ilpQuoteDetails: { maxPacketAmount: MAX_INT64 < maxPacketAmount ? MAX_INT64 : maxPacketAmount, // Cap at MAX_INT64 because of postgres type limits. @@ -235,8 +234,7 @@ function calculateFixedSendQuoteAmounts( ): CalculateQuoteAmountsWithFeesResult { const fees = quote.fee?.calculate(quote.receiveAmount.value) ?? BigInt(0) - const estimatedExchangeRate = - quote.estimatedExchangeRate || quote.lowEstimatedExchangeRate.valueOf() + const { estimatedExchangeRate } = quote const exchangeAdjustedFees = BigInt( Math.ceil(Number(fees) * estimatedExchangeRate) @@ -370,5 +368,5 @@ async function getWalletAddressPage( ): Promise { return await Quote.query(deps.knex) .list(options) - .withGraphFetched('[asset, fee, walletAddress]') + .withGraphFetched('[asset, fee, walletAddress, ilpQuoteDetails]') } diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index 4974ffab24..4638567a28 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -217,12 +217,24 @@ async function pay( }) } + // TODO: prevent ilpQuoteDetails with better type instead of having to check and error? + // Or make better error. + // Is there a way to ensure that right here, outgoingPayment.quote is QuoteWithDetails? + // I dont think so... this is where the ambiguousness of our DB types surfaces and we have + // to do some explicit check like this. Even if we had table inheritance we'd have this problem + // so long as an outgoing payment could have either type of quote on them. To truly make this + // typesafe you might need to have seperate outgoing payment tables for each type of quote (which + // goes way too far) + if (!outgoingPayment.quote.ilpQuoteDetails) { + throw new Error('Missing ILP quote details') + } + const { lowEstimatedExchangeRate, highEstimatedExchangeRate, minExchangeRate, maxPacketAmount - } = outgoingPayment.quote + } = outgoingPayment.quote.ilpQuoteDetails const quote: Pay.Quote = { maxPacketAmount, diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index 40dfe78db1..e00a82808e 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -158,23 +158,14 @@ export async function createQuote( } } - const withGraphFetchedArray = ['asset', 'walletAddress'] + const withGraphFetchedArray = ['asset', 'walletAddress', 'ilpQuoteDetails'] if (withFee) { withGraphFetchedArray.push('fee') } const withGraphFetchedExpression = `[${withGraphFetchedArray.join(', ')}]` - const ilpData = { - lowEstimatedExchangeRate: Pay.Ratio.from(exchangeRate) as Pay.PositiveRatio, - highEstimatedExchangeRate: Pay.Ratio.from( - exchangeRate + 0.000000000001 - ) as Pay.PositiveRatio, - minExchangeRate: Pay.Ratio.from(exchangeRate * 0.99) as Pay.PositiveRatio, - maxPacketAmount: BigInt('9223372036854775807') - } - return await Quote.query() - .insertAndFetch({ + .insertGraphAndFetch({ walletAddressId, assetId: walletAddress.assetId, receiver: receiverUrl, @@ -183,7 +174,14 @@ export async function createQuote( estimatedExchangeRate: exchangeRate, expiresAt: new Date(Date.now() + config.quoteLifespan), client, - ...ilpData + ilpQuoteDetails: { + lowEstimatedExchangeRate: Pay.Ratio.from(exchangeRate), + highEstimatedExchangeRate: Pay.Ratio.from( + exchangeRate + 0.000000000001 + ) as unknown as Pay.PositiveRatio, + minExchangeRate: Pay.Ratio.from(exchangeRate * 0.99), + maxPacketAmount: BigInt('9223372036854775807') + } }) .withGraphFetched(withGraphFetchedExpression) } From 2e76ee2c6541b3f7e0fc5ede6a75e8e4f50d6028 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 14 Aug 2024 21:19:53 -0400 Subject: [PATCH 04/64] feat(backend): WIP local payment method with getQuote --- packages/backend/src/index.ts | 15 + .../src/payment-method/local/service.test.ts | 737 ++++++++++++++++++ .../src/payment-method/local/service.ts | 157 ++++ packages/backend/src/rates/service.ts | 5 + 4 files changed, 914 insertions(+) create mode 100644 packages/backend/src/payment-method/local/service.test.ts create mode 100644 packages/backend/src/payment-method/local/service.ts diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index bd72bba1fa..1f429f2262 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -51,6 +51,10 @@ import { createIlpPaymentService, ServiceDependencies as IlpPaymentServiceDependencies } from './payment-method/ilp/service' +import { + createLocalPaymentService, + ServiceDependencies as LocalPaymentServiceDependencies +} from './payment-method/local/service' import { createSPSPRoutes } from './payment-method/ilp/spsp/routes' import { createStreamCredentialsService } from './payment-method/ilp/stream-credentials/service' import { createRatesService } from './rates/service' @@ -444,6 +448,17 @@ export function initIocContainer( return createIlpPaymentService(serviceDependencies) }) + container.singleton('localPaymentService', async (deps) => { + const serviceDependencies: LocalPaymentServiceDependencies = { + logger: await deps.use('logger'), + knex: await deps.use('knex'), + config: await deps.use('config'), + ratesService: await deps.use('ratesService') + } + + return createLocalPaymentService(serviceDependencies) + }) + container.singleton('paymentMethodHandlerService', async (deps) => { return createPaymentMethodHandlerService({ logger: await deps.use('logger'), diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts new file mode 100644 index 0000000000..94f644e494 --- /dev/null +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -0,0 +1,737 @@ +import { LocalPaymentService } from './service' +import { initIocContainer } from '../../' +import { createTestApp, TestContainer } from '../../tests/app' +import { IAppConfig, Config } from '../../config/app' +import { IocContract } from '@adonisjs/fold' +import { AppServices } from '../../app' +import { createAsset } from '../../tests/asset' +import { createWalletAddress } from '../../tests/walletAddress' +import { Asset } from '../../asset/model' +import { StartQuoteOptions } from '../handler/service' +import { WalletAddress } from '../../open_payments/wallet_address/model' +import * as Pay from '@interledger/pay' + +import { createReceiver } from '../../tests/receiver' +import { mockRatesApi } from '../../tests/rates' +import { AccountingService } from '../../accounting/service' +import { truncateTables } from '../../tests/tableManager' + +const nock = (global as unknown as { nock: typeof import('nock') }).nock + +describe('IlpPaymentService', (): void => { + let deps: IocContract + let appContainer: TestContainer + let localPaymentService: LocalPaymentService + let accountingService: AccountingService + let config: IAppConfig + + const exchangeRatesUrl = 'https://example-rates.com' + + const assetMap: Record = {} + const walletAddressMap: Record = {} + + beforeAll(async (): Promise => { + deps = initIocContainer({ + ...Config, + exchangeRatesUrl, + exchangeRatesLifetime: 0 + }) + appContainer = await createTestApp(deps) + + config = await deps.use('config') + localPaymentService = await deps.use('localPaymentService') + accountingService = await deps.use('accountingService') + }) + + beforeEach(async (): Promise => { + assetMap['USD'] = await createAsset(deps, { + code: 'USD', + scale: 2 + }) + + assetMap['EUR'] = await createAsset(deps, { + code: 'EUR', + scale: 2 + }) + + walletAddressMap['USD'] = await createWalletAddress(deps, { + assetId: assetMap['USD'].id + }) + + walletAddressMap['EUR'] = await createWalletAddress(deps, { + assetId: assetMap['EUR'].id + }) + }) + + afterEach(async (): Promise => { + await truncateTables(appContainer.knex) + jest.restoreAllMocks() + + nock.cleanAll() + nock.abortPendingRequests() + nock.restore() + nock.activate() + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + + describe('getQuote', (): void => { + // test('calls rates service with correct base asset', async (): Promise => { + // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) + + // const options: StartQuoteOptions = { + // walletAddress: walletAddressMap['USD'], + // receiver: await createReceiver(deps, walletAddressMap['USD']), + // debitAmount: { + // assetCode: 'USD', + // assetScale: 2, + // value: 100n + // } + // } + + // const ratesService = await deps.use('ratesService') + // const ratesServiceSpy = jest.spyOn(ratesService, 'rates') + + // await ilpPaymentService.getQuote(options) + + // expect(ratesServiceSpy).toHaveBeenCalledWith('USD') + // ratesScope.done() + // }) + + // test('fails on rate service error', async (): Promise => { + // const ratesService = await deps.use('ratesService') + // jest + // .spyOn(ratesService, 'rates') + // .mockImplementation(() => Promise.reject(new Error('fail'))) + + // expect.assertions(4) + // try { + // await ilpPaymentService.getQuote({ + // walletAddress: walletAddressMap['USD'], + // receiver: await createReceiver(deps, walletAddressMap['USD']), + // debitAmount: { + // assetCode: 'USD', + // assetScale: 2, + // value: 100n + // } + // }) + // } catch (err) { + // expect(err).toBeInstanceOf(PaymentMethodHandlerError) + // expect((err as PaymentMethodHandlerError).message).toBe( + // 'Received error during ILP quoting' + // ) + // expect((err as PaymentMethodHandlerError).description).toBe( + // 'Could not get rates from service' + // ) + // expect((err as PaymentMethodHandlerError).retryable).toBe(false) + // } + // }) + + // test('returns all fields correctly', async (): Promise => { + // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) + + // const options: StartQuoteOptions = { + // walletAddress: walletAddressMap['USD'], + // receiver: await createReceiver(deps, walletAddressMap['USD']), + // debitAmount: { + // assetCode: 'USD', + // assetScale: 2, + // value: 100n + // } + // } + + // await expect(ilpPaymentService.getQuote(options)).resolves.toEqual({ + // receiver: options.receiver, + // walletAddress: options.walletAddress, + // debitAmount: { + // assetCode: 'USD', + // assetScale: 2, + // value: 100n + // }, + // receiveAmount: { + // assetCode: 'USD', + // assetScale: 2, + // value: 99n + // }, + // estimatedExchangeRate: expect.any(Number), + // additionalFields: { + // minExchangeRate: expect.any(Pay.Ratio), + // highEstimatedExchangeRate: expect.any(Pay.Ratio), + // lowEstimatedExchangeRate: expect.any(Pay.Ratio), + // maxPacketAmount: BigInt(Pay.Int.MAX_U64.toString()) + // } + // }) + // ratesScope.done() + // }) + + // test('uses receiver.incomingAmount if receiveAmount is not provided', async (): Promise => { + // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) + + // const incomingAmount = { + // assetCode: 'USD', + // assetScale: 2, + // value: 100n + // } + + // const options: StartQuoteOptions = { + // walletAddress: walletAddressMap['USD'], + // receiver: await createReceiver(deps, walletAddressMap['USD'], { + // incomingAmount + // }) + // } + + // const ilpStartQuoteSpy = jest.spyOn(Pay, 'startQuote') + + // await expect(ilpPaymentService.getQuote(options)).resolves.toMatchObject({ + // receiveAmount: { + // assetCode: 'USD', + // assetScale: 2, + // value: incomingAmount?.value + // } + // }) + + // expect(ilpStartQuoteSpy).toHaveBeenCalledWith( + // expect.objectContaining({ + // amountToDeliver: incomingAmount?.value + // }) + // ) + // ratesScope.done() + // }) + + // test('fails if slippage too high', async (): Promise => + // withConfigOverride( + // () => config, + // { slippage: 101 }, + // async () => { + // mockRatesApi(exchangeRatesUrl, () => ({})) + + // expect.assertions(4) + // try { + // await ilpPaymentService.getQuote({ + // walletAddress: walletAddressMap['USD'], + // receiver: await createReceiver(deps, walletAddressMap['USD']), + // debitAmount: { + // assetCode: 'USD', + // assetScale: 2, + // value: 100n + // } + // }) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Received error during ILP quoting' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // Pay.PaymentError.InvalidSlippage + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // } + // } + // )()) + + // test('throws if quote returns invalid maxSourceAmount', async (): Promise => { + // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) + + // const options: StartQuoteOptions = { + // walletAddress: walletAddressMap['USD'], + // receiver: await createReceiver(deps, walletAddressMap['USD']) + // } + + // jest.spyOn(Pay, 'startQuote').mockResolvedValueOnce({ + // maxSourceAmount: -1n + // } as Pay.Quote) + + // expect.assertions(4) + // try { + // await ilpPaymentService.getQuote(options) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Received error during ILP quoting' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // 'Maximum source amount of ILP quote is non-positive' + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // } + + // ratesScope.done() + // }) + + // test('throws if quote returns invalid minDeliveryAmount', async (): Promise => { + // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) + + // const options: StartQuoteOptions = { + // walletAddress: walletAddressMap['USD'], + // receiver: await createReceiver(deps, walletAddressMap['USD'], { + // incomingAmount: { + // assetCode: 'USD', + // assetScale: 2, + // value: 100n + // } + // }) + // } + + // jest.spyOn(Pay, 'startQuote').mockResolvedValueOnce({ + // maxSourceAmount: 1n, + // minDeliveryAmount: -1n + // } as Pay.Quote) + + // expect.assertions(5) + // try { + // await ilpPaymentService.getQuote(options) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Received error during ILP quoting' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // 'Minimum delivery amount of ILP quote is non-positive' + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // expect((error as PaymentMethodHandlerError).code).toBe( + // PaymentMethodHandlerErrorCode.QuoteNonPositiveReceiveAmount + // ) + // } + + // ratesScope.done() + // }) + + // test('throws if quote returns with a non-positive estimated delivery amount', async (): Promise => { + // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) + + // const options: StartQuoteOptions = { + // walletAddress: walletAddressMap['USD'], + // receiver: await createReceiver(deps, walletAddressMap['USD']) + // } + + // jest.spyOn(Pay, 'startQuote').mockResolvedValueOnce({ + // maxSourceAmount: 10n, + // highEstimatedExchangeRate: Pay.Ratio.from(0.099) + // } as Pay.Quote) + + // expect.assertions(5) + // try { + // await ilpPaymentService.getQuote(options) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Received error during ILP quoting' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // 'Estimated receive amount of ILP quote is non-positive' + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // expect((error as PaymentMethodHandlerError).code).toBe( + // PaymentMethodHandlerErrorCode.QuoteNonPositiveReceiveAmount + // ) + // } + + // ratesScope.done() + // }) + + describe('successfully gets ilp quote', (): void => { + describe.only('with incomingAmount', () => { + test.each` + incomingAssetCode | incomingAmountValue | debitAssetCode | expectedDebitAmount | exchangeRate | description + ${'EUR'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'cross currency, same rate'} + ${'EUR'} | ${100n} | ${'USD'} | ${111n} | ${0.9} | ${'cross currency, exchange rate < 1'} + ${'EUR'} | ${100n} | ${'USD'} | ${50n} | ${2.0} | ${'cross currency, exchange rate > 1'} + `( + // TODO: seperate test with this case (and dont mock the mockRatesApi). + // no `each` needed. + // test.each` + // incomingAssetCode | incomingAmountValue | debitAssetCode | expectedDebitAmount | exchangeRate | description + // ${'USD'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'same currency'} + // `( + '$description', + async ({ + incomingAssetCode, + incomingAmountValue, + debitAssetCode, + expectedDebitAmount, + exchangeRate + }): Promise => { + // TODO: investigate this further. + // - Is the expectedDebitAmount correct in these tests? + // - Is the mockRatesApi return different than ilp getQuote test (which is [incomingAmountAssetCode]: exchangeRate) + // because we do convertRatesToIlpPrices (which inverts) in ilp getQuote? (I think so...) + // - just convert the exchangeRate test arg instead of inversing here (0.9 -> 1.1., 2.0 -> .5 etc)? + // I started with the exchangeRates as they are simply because I copy/pasted from ilp getQuote tests + const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({ + [debitAssetCode]: 1 / exchangeRate + })) + + const receivingWalletAddress = walletAddressMap[incomingAssetCode] + const sendingWalletAddress = walletAddressMap[debitAssetCode] + + const options: StartQuoteOptions = { + walletAddress: sendingWalletAddress, + receiver: await createReceiver(deps, receivingWalletAddress), + receiveAmount: { + assetCode: receivingWalletAddress.asset.code, + assetScale: receivingWalletAddress.asset.scale, + value: incomingAmountValue + } + } + + const quote = await localPaymentService.getQuote(options) + + console.log('quote gotten in test', { quote }) + + expect(quote).toMatchObject({ + debitAmount: { + assetCode: sendingWalletAddress.asset.code, + assetScale: sendingWalletAddress.asset.scale, + value: expectedDebitAmount + }, + receiveAmount: { + assetCode: receivingWalletAddress.asset.code, + assetScale: receivingWalletAddress.asset.scale, + value: incomingAmountValue + } + }) + ratesScope.done() + } + ) + }) + + describe.only('with debitAmount', () => { + test.each` + debitAssetCode | debitAmountValue | incomingAssetCode | expectedReceiveAmount | exchangeRate | description + ${'EUR'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'cross currency, same rate'} + ${'USD'} | ${100n} | ${'EUR'} | ${90n} | ${0.9} | ${'cross currency, exchange rate < 1'} + ${'USD'} | ${100n} | ${'EUR'} | ${200n} | ${2.0} | ${'cross currency, exchange rate > 1'} + `( + // TODO: seperate test with this case (and dont mock the mockRatesApi). + // no `each` needed. + // test.each` + // debitAssetCode | debitAmountValue | incomingAssetCode | expectedReceiveAmount | exchangeRate | description + // ${'USD'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'same currency'} + // `( + '$description', + async ({ + incomingAssetCode, + debitAmountValue, + debitAssetCode, + expectedReceiveAmount, + exchangeRate + }): Promise => { + const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({ + [incomingAssetCode]: exchangeRate + })) + + const receivingWalletAddress = walletAddressMap[incomingAssetCode] + const sendingWalletAddress = walletAddressMap[debitAssetCode] + + const options: StartQuoteOptions = { + walletAddress: sendingWalletAddress, + receiver: await createReceiver(deps, receivingWalletAddress), + debitAmount: { + assetCode: sendingWalletAddress.asset.code, + assetScale: sendingWalletAddress.asset.scale, + value: debitAmountValue + } + } + + const quote = await localPaymentService.getQuote(options) + + expect(quote).toMatchObject({ + debitAmount: { + assetCode: sendingWalletAddress.asset.code, + assetScale: sendingWalletAddress.asset.scale, + value: debitAmountValue + }, + receiveAmount: { + assetCode: receivingWalletAddress.asset.code, + assetScale: receivingWalletAddress.asset.scale, + value: expectedReceiveAmount + } + }) + ratesScope.done() + } + ) + }) + }) + }) + + // describe('pay', (): void => { + // function mockIlpPay( + // overrideQuote: Partial, + // error?: Pay.PaymentError + // ): jest.SpyInstance< + // Promise, + // [options: Pay.PayOptions] + // > { + // return jest + // .spyOn(Pay, 'pay') + // .mockImplementationOnce(async (opts: Pay.PayOptions) => { + // const res = await Pay.pay({ + // ...opts, + // quote: { ...opts.quote, ...overrideQuote } + // }) + // if (error) res.error = error + // return res + // }) + // } + + // async function validateBalances( + // outgoingPayment: OutgoingPayment, + // incomingPayment: IncomingPayment, + // { + // amountSent, + // amountReceived + // }: { + // amountSent: bigint + // amountReceived: bigint + // } + // ) { + // await expect( + // accountingService.getTotalSent(outgoingPayment.id) + // ).resolves.toBe(amountSent) + // await expect( + // accountingService.getTotalReceived(incomingPayment.id) + // ).resolves.toEqual(amountReceived) + // } + + // test('successfully streams between accounts', async (): Promise => { + // const { incomingPayment, receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // await expect( + // ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 100n, + // finalReceiveAmount: 100n + // }) + // ).resolves.toBeUndefined() + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 100n, + // amountReceived: 100n + // }) + // }) + + // test('partially streams between accounts, then streams to completion', async (): Promise => { + // const { incomingPayment, receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // exchangeRate: 1, + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // mockIlpPay( + // { maxSourceAmount: 5n, minDeliveryAmount: 5n }, + // Pay.PaymentError.ClosedByReceiver + // ) + + // await expect( + // ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 100n, + // finalReceiveAmount: 100n + // }) + // ).rejects.toThrow(PaymentMethodHandlerError) + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 5n, + // amountReceived: 5n + // }) + + // await expect( + // ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 100n - 5n, + // finalReceiveAmount: 100n - 5n + // }) + // ).resolves.toBeUndefined() + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 100n, + // amountReceived: 100n + // }) + // }) + + // test('throws if invalid finalDebitAmount', async (): Promise => { + // const { incomingPayment, receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // expect.assertions(6) + // try { + // await ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 0n, + // finalReceiveAmount: 50n + // }) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Could not start ILP streaming' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // 'Invalid finalDebitAmount' + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // } + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 0n, + // amountReceived: 0n + // }) + // }) + + // test('throws if invalid finalReceiveAmount', async (): Promise => { + // const { incomingPayment, receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // expect.assertions(6) + // try { + // await ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 50n, + // finalReceiveAmount: 0n + // }) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Could not start ILP streaming' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // 'Invalid finalReceiveAmount' + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // } + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 0n, + // amountReceived: 0n + // }) + // }) + + // test('throws retryable ILP error', async (): Promise => { + // const { receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // mockIlpPay({}, Object.keys(retryableIlpErrors)[0] as Pay.PaymentError) + + // expect.assertions(4) + // try { + // await ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 50n, + // finalReceiveAmount: 50n + // }) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Received error during ILP pay' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // Object.keys(retryableIlpErrors)[0] + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(true) + // } + // }) + + // test('throws non-retryable ILP error', async (): Promise => { + // const { receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // const nonRetryableIlpError = Object.values(Pay.PaymentError).find( + // (error) => !retryableIlpErrors[error] + // ) + + // mockIlpPay({}, nonRetryableIlpError) + + // expect.assertions(4) + // try { + // await ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 50n, + // finalReceiveAmount: 50n + // }) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Received error during ILP pay' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // nonRetryableIlpError + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // } + // }) + // }) +}) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts new file mode 100644 index 0000000000..3db65779cf --- /dev/null +++ b/packages/backend/src/payment-method/local/service.ts @@ -0,0 +1,157 @@ +import { BaseService } from '../../shared/baseService' +import { + PaymentQuote, + PaymentMethodService, + StartQuoteOptions, + PayOptions +} from '../handler/service' +import { isConvertError, RatesService } from '../../rates/service' +import { IAppConfig } from '../../config/app' +import { + PaymentMethodHandlerError, + PaymentMethodHandlerErrorCode +} from '../handler/errors' +import { TelemetryService } from '../../telemetry/service' +import { Asset } from '../../rates/util' + +export interface LocalPaymentService extends PaymentMethodService {} + +export interface ServiceDependencies extends BaseService { + config: IAppConfig + ratesService: RatesService + telemetry?: TelemetryService +} + +export async function createLocalPaymentService( + deps_: ServiceDependencies +): Promise { + const deps: ServiceDependencies = { + ...deps_, + logger: deps_.logger.child({ service: 'LocalPaymentService' }) + } + + return { + getQuote: (quoteOptions) => getQuote(deps, quoteOptions), + pay: (payOptions) => pay(deps, payOptions) + } +} + +async function getQuote( + deps: ServiceDependencies, + options: StartQuoteOptions +): Promise { + const { receiver, debitAmount, receiveAmount } = options + + console.log('getting quote from', { receiver, debitAmount, receiveAmount }) + + let debitAmountValue: bigint + let receiveAmountValue: bigint + // let estimatedExchangeRate: number + + if (debitAmount) { + console.log('getting receiveAmount from debitAmount via convert') + debitAmountValue = debitAmount.value + const converted = await deps.ratesService.convert({ + sourceAmount: debitAmountValue, + sourceAsset: { + code: debitAmount.assetCode, + scale: debitAmount.assetScale + }, + destinationAsset: { code: receiver.assetCode, scale: receiver.assetScale } + }) + if (isConvertError(converted)) { + throw new PaymentMethodHandlerError( + 'Received error during local quoting', + { + description: 'Failed to convert debitAmount to receive amount', + retryable: false + } + ) + } + receiveAmountValue = converted + // estimatedExchangeRate = Number(debitAmountValue / receiveAmountValue) + } else if (receiveAmount) { + console.log('getting debitAmount from receiveAmount via convert') + receiveAmountValue = receiveAmount.value + const converted = await deps.ratesService.convert({ + sourceAmount: receiveAmountValue, + sourceAsset: { + code: receiveAmount.assetCode, + scale: receiveAmount.assetScale + }, + destinationAsset: { + code: options.walletAddress.asset.code, + scale: options.walletAddress.asset.scale + } + }) + if (isConvertError(converted)) { + throw new PaymentMethodHandlerError( + 'Received error during local quoting', + { + description: 'Failed to convert receiveAmount to debitAmount', + retryable: false + } + ) + } + debitAmountValue = converted + // estimatedExchangeRate = Number(receiveAmountValue / debitAmountValue) + // estimatedExchangeRate = Number(debitAmountValue / receiveAmountValue) + } else if (receiver.incomingAmount) { + console.log('getting debitAmount from receiver.incomingAmount via convert') + receiveAmountValue = receiver.incomingAmount.value + const converted = await deps.ratesService.convert({ + sourceAmount: receiveAmountValue, + sourceAsset: { + code: receiver.incomingAmount.assetCode, + scale: receiver.incomingAmount.assetScale + }, + destinationAsset: { + code: options.walletAddress.asset.code, + scale: options.walletAddress.asset.scale + } + }) + if (isConvertError(converted)) { + throw new PaymentMethodHandlerError( + 'Received error during local quoting', + { + description: + 'Failed to convert receiver.incomingAmount to debitAmount', + retryable: false + } + ) + } + debitAmountValue = converted + // estimatedExchangeRate = Number(receiveAmountValue / debitAmountValue) + // estimatedExchangeRate = Number(debitAmountValue / receiveAmountValue) + } else { + throw new PaymentMethodHandlerError('Received error during local quoting', { + description: 'No value provided to get quote from', + retryable: false + }) + } + + return { + receiver: options.receiver, + walletAddress: options.walletAddress, + // TODO: is this correct for both fixed send/receive? or needs to be flipped? + estimatedExchangeRate: Number(debitAmountValue / receiveAmountValue), + debitAmount: { + value: debitAmountValue, + assetCode: options.walletAddress.asset.code, + assetScale: options.walletAddress.asset.scale + }, + receiveAmount: { + value: receiveAmountValue, + assetCode: options.receiver.assetCode, + assetScale: options.receiver.assetScale + }, + additionalFields: {} + } +} + +async function pay( + deps: ServiceDependencies, + options: PayOptions +): Promise { + throw new Error('local pay not implemented') +} diff --git a/packages/backend/src/rates/service.ts b/packages/backend/src/rates/service.ts index 5a6ed7322e..36a8c8cbc4 100644 --- a/packages/backend/src/rates/service.ts +++ b/packages/backend/src/rates/service.ts @@ -54,14 +54,19 @@ class RatesServiceImpl implements RatesService { async convert( opts: Omit ): Promise { + console.log('convert called with', { opts }) const sameCode = opts.sourceAsset.code === opts.destinationAsset.code const sameScale = opts.sourceAsset.scale === opts.destinationAsset.scale if (sameCode && sameScale) return opts.sourceAmount if (sameCode) return convert({ exchangeRate: 1.0, ...opts }) const { rates } = await this.getRates(opts.sourceAsset.code) + console.log('convert got rates', { rates }) const destinationExchangeRate = rates[opts.destinationAsset.code] + console.log('convert got destinationExchangeRate', { + destinationExchangeRate + }) if (!destinationExchangeRate || !isValidPrice(destinationExchangeRate)) { return ConvertError.InvalidDestinationPrice } From 4fab5d89fb35d1cf2fe7db424cb4b6c4a1a1eeb8 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:50:11 -0400 Subject: [PATCH 05/64] feat(backend): add local payment method to payment method handler --- packages/backend/src/index.ts | 3 +- .../payment-method/handler/service.test.ts | 61 +++++++++++++++++++ .../src/payment-method/handler/service.ts | 13 ++-- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 1f429f2262..db0b4515af 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -463,7 +463,8 @@ export function initIocContainer( return createPaymentMethodHandlerService({ logger: await deps.use('logger'), knex: await deps.use('knex'), - ilpPaymentService: await deps.use('ilpPaymentService') + ilpPaymentService: await deps.use('ilpPaymentService'), + localPaymentService: await deps.use('localPaymentService') }) }) diff --git a/packages/backend/src/payment-method/handler/service.test.ts b/packages/backend/src/payment-method/handler/service.test.ts index 2a80feed5c..a28e20b2b4 100644 --- a/packages/backend/src/payment-method/handler/service.test.ts +++ b/packages/backend/src/payment-method/handler/service.test.ts @@ -15,12 +15,14 @@ import { createReceiver } from '../../tests/receiver' import { IlpPaymentService } from '../ilp/service' import { truncateTables } from '../../tests/tableManager' import { createOutgoingPaymentWithReceiver } from '../../tests/outgoingPayment' +import { LocalPaymentService } from '../local/service' describe('PaymentMethodHandlerService', (): void => { let deps: IocContract let appContainer: TestContainer let paymentMethodHandlerService: PaymentMethodHandlerService let ilpPaymentService: IlpPaymentService + let localPaymentService: LocalPaymentService beforeAll(async (): Promise => { deps = initIocContainer(Config) @@ -28,6 +30,7 @@ describe('PaymentMethodHandlerService', (): void => { paymentMethodHandlerService = await deps.use('paymentMethodHandlerService') ilpPaymentService = await deps.use('ilpPaymentService') + localPaymentService = await deps.use('localPaymentService') }) afterEach(async (): Promise => { @@ -64,6 +67,30 @@ describe('PaymentMethodHandlerService', (): void => { expect(ilpPaymentServiceGetQuoteSpy).toHaveBeenCalledWith(options) }) + test('calls lcaolPaymentService for local payment type', async (): Promise => { + const asset = await createAsset(deps) + const walletAddress = await createWalletAddress(deps, { + assetId: asset.id + }) + + const options: StartQuoteOptions = { + walletAddress, + receiver: await createReceiver(deps, walletAddress), + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 100n + } + } + + const localPaymentServiceGetQuoteSpy = jest + .spyOn(localPaymentService, 'getQuote') + .mockImplementationOnce(jest.fn()) + + await paymentMethodHandlerService.getQuote('LOCAL', options) + + expect(localPaymentServiceGetQuoteSpy).toHaveBeenCalledWith(options) + }) }) describe('pay', (): void => { @@ -101,5 +128,39 @@ describe('PaymentMethodHandlerService', (): void => { expect(ilpPaymentServicePaySpy).toHaveBeenCalledWith(options) }) + test('calls localPaymentService for local payment type', async (): Promise => { + const asset = await createAsset(deps) + const walletAddress = await createWalletAddress(deps, { + assetId: asset.id + }) + const { receiver, outgoingPayment } = + await createOutgoingPaymentWithReceiver(deps, { + sendingWalletAddress: walletAddress, + receivingWalletAddress: walletAddress, + method: 'ilp', + quoteOptions: { + debitAmount: { + assetCode: walletAddress.asset.code, + assetScale: walletAddress.asset.scale, + value: 100n + } + } + }) + + const options: PayOptions = { + receiver, + outgoingPayment, + finalDebitAmount: outgoingPayment.debitAmount.value, + finalReceiveAmount: outgoingPayment.receiveAmount.value + } + + const localPaymentServicePaySpy = jest + .spyOn(localPaymentService, 'pay') + .mockImplementationOnce(jest.fn()) + + await paymentMethodHandlerService.pay('LOCAL', options) + + expect(localPaymentServicePaySpy).toHaveBeenCalledWith(options) + }) }) }) diff --git a/packages/backend/src/payment-method/handler/service.ts b/packages/backend/src/payment-method/handler/service.ts index 670d46ac4a..c7bb2dfc0e 100644 --- a/packages/backend/src/payment-method/handler/service.ts +++ b/packages/backend/src/payment-method/handler/service.ts @@ -4,6 +4,7 @@ import { WalletAddress } from '../../open_payments/wallet_address/model' import { Receiver } from '../../open_payments/receiver/model' import { BaseService } from '../../shared/baseService' import { IlpPaymentService } from '../ilp/service' +import { LocalPaymentService } from '../local/service' export interface StartQuoteOptions { walletAddress: WalletAddress @@ -33,7 +34,7 @@ export interface PaymentMethodService { pay(payOptions: PayOptions): Promise } -export type PaymentMethod = 'ILP' +export type PaymentMethod = 'ILP' | 'LOCAL' export interface PaymentMethodHandlerService { getQuote( @@ -45,12 +46,14 @@ export interface PaymentMethodHandlerService { interface ServiceDependencies extends BaseService { ilpPaymentService: IlpPaymentService + localPaymentService: LocalPaymentService } export async function createPaymentMethodHandlerService({ logger, knex, - ilpPaymentService + ilpPaymentService, + localPaymentService }: ServiceDependencies): Promise { const log = logger.child({ service: 'PaymentMethodHandlerService' @@ -58,11 +61,13 @@ export async function createPaymentMethodHandlerService({ const deps: ServiceDependencies = { logger: log, knex, - ilpPaymentService + ilpPaymentService, + localPaymentService } const paymentMethods: { [key in PaymentMethod]: PaymentMethodService } = { - ILP: deps.ilpPaymentService + ILP: deps.ilpPaymentService, + LOCAL: deps.localPaymentService } return { From b17db75e271393f27ef256f4d18e4f2ac952bb17 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 15 Aug 2024 15:28:45 -0400 Subject: [PATCH 06/64] chore(backend): fix format --- .../src/payment-method/local/service.test.ts | 13 ++++++++----- .../backend/src/payment-method/local/service.ts | 10 +++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 94f644e494..54acaf9520 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -1,7 +1,10 @@ import { LocalPaymentService } from './service' import { initIocContainer } from '../../' import { createTestApp, TestContainer } from '../../tests/app' -import { IAppConfig, Config } from '../../config/app' +import { + // IAppConfig, + Config +} from '../../config/app' import { IocContract } from '@adonisjs/fold' import { AppServices } from '../../app' import { createAsset } from '../../tests/asset' @@ -9,11 +12,11 @@ import { createWalletAddress } from '../../tests/walletAddress' import { Asset } from '../../asset/model' import { StartQuoteOptions } from '../handler/service' import { WalletAddress } from '../../open_payments/wallet_address/model' -import * as Pay from '@interledger/pay' +// import * as Pay from '@interledger/pay' import { createReceiver } from '../../tests/receiver' import { mockRatesApi } from '../../tests/rates' -import { AccountingService } from '../../accounting/service' +// import { AccountingService } from '../../accounting/service' import { truncateTables } from '../../tests/tableManager' const nock = (global as unknown as { nock: typeof import('nock') }).nock @@ -22,8 +25,8 @@ describe('IlpPaymentService', (): void => { let deps: IocContract let appContainer: TestContainer let localPaymentService: LocalPaymentService - let accountingService: AccountingService - let config: IAppConfig + // let accountingService: AccountingService + // let config: IAppConfig const exchangeRatesUrl = 'https://example-rates.com' diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 3db65779cf..12e9866df6 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -8,11 +8,11 @@ import { import { isConvertError, RatesService } from '../../rates/service' import { IAppConfig } from '../../config/app' import { - PaymentMethodHandlerError, - PaymentMethodHandlerErrorCode + PaymentMethodHandlerError + // PaymentMethodHandlerErrorCode } from '../handler/errors' import { TelemetryService } from '../../telemetry/service' -import { Asset } from '../../rates/util' +// import { Asset } from '../../rates/util' export interface LocalPaymentService extends PaymentMethodService {} @@ -150,8 +150,8 @@ async function getQuote( } async function pay( - deps: ServiceDependencies, - options: PayOptions + _deps: ServiceDependencies, + _options: PayOptions ): Promise { throw new Error('local pay not implemented') } From 9ef7f948e423f12a22d83700cf7ada61ad8f0eaa Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 15 Aug 2024 21:42:42 -0400 Subject: [PATCH 07/64] feat(backend): stub in control payment handler service with receiver isLocal - stubs in isLocal variable in place of actualy receiver.isLocal property - updates/adds test. new test expectedly fails because there is no way to set the receiver to local yet. can implement after isLocal is added to receiver --- .../src/open_payments/quote/service.test.ts | 71 ++++++++++++++++++- .../src/open_payments/quote/service.ts | 67 +++++++++-------- .../src/payment-method/local/service.test.ts | 4 +- 3 files changed, 109 insertions(+), 33 deletions(-) diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 1ba449505a..4c4db21500 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -191,7 +191,7 @@ describe('QuoteService', (): void => { }) }) - jest + const getQuoteSpy = jest .spyOn(paymentMethodHandlerService, 'getQuote') .mockResolvedValueOnce(mockedQuote) @@ -201,6 +201,17 @@ describe('QuoteService', (): void => { }) assert.ok(!isQuoteError(quote)) + expect(getQuoteSpy).toHaveBeenCalledTimes(1) + expect(getQuoteSpy).toHaveBeenCalledWith( + 'ILP', + expect.objectContaining({ + walletAddress: sendingWalletAddress, + receiver: expect.anything(), + receiveAmount: options.receiveAmount, + debitAmount: options.debitAmount + }) + ) + expect(quote).toMatchObject({ walletAddressId: sendingWalletAddress.id, receiver: options.receiver, @@ -750,5 +761,63 @@ describe('QuoteService', (): void => { ).resolves.toEqual(QuoteError.NonPositiveReceiveAmount) }) }) + + describe('Local Receiver', (): void => { + test('Local receiver uses local payment method', async () => { + const incomingPayment = await createIncomingPayment(deps, { + walletAddressId: receivingWalletAddress.id, + incomingAmount + }) + + const options: CreateQuoteOptions = { + walletAddressId: sendingWalletAddress.id, + receiver: incomingPayment.getUrl(receivingWalletAddress), + method: 'ilp' + } + + const mockedQuote = mockQuote({ + receiver: (await receiverService.get( + incomingPayment.getUrl(receivingWalletAddress) + ))!, + walletAddress: sendingWalletAddress, + exchangeRate: 0.5, + debitAmountValue: debitAmount.value + }) + + const getQuoteSpy = jest + .spyOn(paymentMethodHandlerService, 'getQuote') + .mockResolvedValueOnce(mockedQuote) + + const quote = await quoteService.create(options) + assert.ok(!isQuoteError(quote)) + + expect(getQuoteSpy).toHaveBeenCalledTimes(1) + expect(getQuoteSpy).toHaveBeenCalledWith( + 'LOCAL', + expect.objectContaining({ + walletAddress: sendingWalletAddress, + receiver: expect.anything(), + receiveAmount: options.receiveAmount, + debitAmount: options.debitAmount + }) + ) + + expect(quote).toMatchObject({ + walletAddressId: sendingWalletAddress.id, + receiver: options.receiver, + debitAmount: mockedQuote.debitAmount, + receiveAmount: mockedQuote.receiveAmount, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + expiresAt: new Date(quote.createdAt.getTime() + config.quoteLifespan) + }) + + await expect( + quoteService.get({ + id: quote.id + }) + ).resolves.toEqual(quote) + }) + }) }) }) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 51588acf1b..51b2a67cc8 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -1,4 +1,4 @@ -import { TransactionOrKnex } from 'objection' +import { PartialModelGraph, TransactionOrKnex } from 'objection' import * as Pay from '@interledger/pay' import { BaseService } from '../../shared/baseService' @@ -113,43 +113,50 @@ async function createQuote( try { const receiver = await resolveReceiver(deps, options) - const quote = await deps.paymentMethodHandlerService.getQuote('ILP', { - walletAddress, - receiver, - receiveAmount: options.receiveAmount, - debitAmount: options.debitAmount - }) - - const maxPacketAmount = quote.additionalFields.maxPacketAmount as bigint + const isLocal = false + const quote = await deps.paymentMethodHandlerService.getQuote( + isLocal ? 'LOCAL' : 'ILP', + { + walletAddress, + receiver, + receiveAmount: options.receiveAmount, + debitAmount: options.debitAmount + } + ) const sendingFee = await deps.feeService.getLatestFee( walletAddress.assetId, FeeType.Sending ) + const graph: PartialModelGraph = { + 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 + } + + if (!isLocal) { + const maxPacketAmount = quote.additionalFields.maxPacketAmount as bigint + graph.ilpQuoteDetails = { + maxPacketAmount: + MAX_INT64 < maxPacketAmount ? MAX_INT64 : maxPacketAmount, // Cap at MAX_INT64 because of postgres type limits. + minExchangeRate: quote.additionalFields.minExchangeRate as Pay.Ratio, + lowEstimatedExchangeRate: quote.additionalFields + .lowEstimatedExchangeRate as Pay.Ratio, + highEstimatedExchangeRate: quote.additionalFields + .highEstimatedExchangeRate as Pay.PositiveRatio + } + } + return await Quote.transaction(deps.knex, async (trx) => { const createdQuote = await Quote.query(trx) - .insertGraphAndFetch({ - walletAddressId: options.walletAddressId, - assetId: walletAddress.assetId, - receiver: options.receiver, - debitAmount: quote.debitAmount, - receiveAmount: quote.receiveAmount, - ilpQuoteDetails: { - maxPacketAmount: - MAX_INT64 < maxPacketAmount ? MAX_INT64 : maxPacketAmount, // Cap at MAX_INT64 because of postgres type limits. - minExchangeRate: quote.additionalFields - .minExchangeRate as Pay.Ratio, - lowEstimatedExchangeRate: quote.additionalFields - .lowEstimatedExchangeRate as Pay.Ratio, - highEstimatedExchangeRate: quote.additionalFields - .highEstimatedExchangeRate as Pay.PositiveRatio - }, - expiresAt: new Date(0), // expiresAt is patched in finalizeQuote - client: options.client, - feeId: sendingFee?.id, - estimatedExchangeRate: quote.estimatedExchangeRate - }) + .insertGraphAndFetch(graph) .withGraphFetched('[asset, fee, walletAddress, ilpQuoteDetails]') return await finalizeQuote( diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 54acaf9520..07d56df898 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -41,9 +41,9 @@ describe('IlpPaymentService', (): void => { }) appContainer = await createTestApp(deps) - config = await deps.use('config') + // config = await deps.use('config') localPaymentService = await deps.use('localPaymentService') - accountingService = await deps.use('accountingService') + // accountingService = await deps.use('accountingService') }) beforeEach(async (): Promise => { From 320ee21b5015a940e48c9dc09e59e17f54bc06e5 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:16:21 -0400 Subject: [PATCH 08/64] feat(backend): local payment .pay --- .../backend/src/accounting/psql/service.ts | 2 +- packages/backend/src/accounting/service.ts | 2 +- packages/backend/src/app.ts | 2 + packages/backend/src/index.ts | 4 +- .../payment/outgoing/lifecycle.ts | 6 +- .../src/open_payments/quote/service.ts | 2 + .../src/payment-method/local/service.ts | 106 ++++++++++++++---- 7 files changed, 99 insertions(+), 25 deletions(-) diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index e427cd26b5..6da25ecb96 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -264,7 +264,7 @@ export async function createTransfer( debitAccount: accountMap[transfer.sourceAccountId], creditAccount: accountMap[transfer.destinationAccountId], amount: transfer.amount, - timeoutMs: BigInt(args.timeout * 1000) + timeoutMs: args.timeout ? BigInt(args.timeout * 1000) : undefined })) ) ) diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index bdbbc3c2fc..ba6adb1f85 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -65,7 +65,7 @@ export interface TransferOptions extends BaseTransfer { destinationAccount: LiquidityAccount sourceAmount: bigint destinationAmount?: bigint - timeout: number + timeout?: number } export interface Transaction { diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 4614e99380..e9059a1a2e 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -98,6 +98,7 @@ import { } from './open_payments/wallet_address/middleware' import { LoggingPlugin } from './graphql/plugin' +import { LocalPaymentService } from './payment-method/local/service' export interface AppContextData { logger: Logger container: AppContainer @@ -250,6 +251,7 @@ export interface AppServices { tigerBeetle?: Promise paymentMethodHandlerService: Promise ilpPaymentService: Promise + localPaymentService: Promise } export type AppContainer = IocContract diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index db0b4515af..ff13d7900c 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -453,7 +453,9 @@ export function initIocContainer( logger: await deps.use('logger'), knex: await deps.use('knex'), config: await deps.use('config'), - ratesService: await deps.use('ratesService') + ratesService: await deps.use('ratesService'), + accountingService: await deps.use('accountingService'), + incomingPaymentService: await deps.use('incomingPaymentService') } return createLocalPaymentService(serviceDependencies) diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index 676f3b10c6..f2772c131a 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -77,7 +77,10 @@ export async function handleSending( } const payStartTime = Date.now() - await deps.paymentMethodHandlerService.pay('ILP', { + // TODO: use receiver.isLocal + const isLocal = false + // const isLocal = true + await deps.paymentMethodHandlerService.pay(isLocal ? 'LOCAL' : 'ILP', { receiver, outgoingPayment: payment, finalDebitAmount: maxDebitAmount, @@ -91,6 +94,7 @@ export async function handleSending( deps.telemetry.incrementCounter('transactions_total', 1, { description: 'Count of funded transactions' }), + // TODO: exclude local payments from ilp pay time? deps.telemetry.recordHistogram('ilp_pay_time_ms', payDuration, { description: 'Time to complete an ILP payment' }), diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 51b2a67cc8..ac632e5183 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -113,7 +113,9 @@ async function createQuote( try { const receiver = await resolveReceiver(deps, options) + // TODO: use reciver.isLocal const isLocal = false + // const isLocal = true const quote = await deps.paymentMethodHandlerService.getQuote( isLocal ? 'LOCAL' : 'ILP', { diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 12e9866df6..2263eaa8d6 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -7,19 +7,28 @@ import { } from '../handler/service' import { isConvertError, RatesService } from '../../rates/service' import { IAppConfig } from '../../config/app' +import { PaymentMethodHandlerError } from '../handler/errors' import { - PaymentMethodHandlerError - // PaymentMethodHandlerErrorCode -} from '../handler/errors' -import { TelemetryService } from '../../telemetry/service' -// import { Asset } from '../../rates/util' + AccountingService, + LiquidityAccountType, + TransferOptions, + TransferType +} from '../../accounting/service' +import { + AccountAlreadyExistsError, + isTransferError, + TransferError +} from '../../accounting/errors' +import { InsufficientLiquidityError } from 'ilp-packet/dist/errors' +import { IncomingPaymentService } from '../../open_payments/payment/incoming/service' export interface LocalPaymentService extends PaymentMethodService {} export interface ServiceDependencies extends BaseService { config: IAppConfig ratesService: RatesService - telemetry?: TelemetryService + accountingService: AccountingService + incomingPaymentService: IncomingPaymentService } export async function createLocalPaymentService( @@ -42,14 +51,11 @@ async function getQuote( ): Promise { const { receiver, debitAmount, receiveAmount } = options - console.log('getting quote from', { receiver, debitAmount, receiveAmount }) - let debitAmountValue: bigint let receiveAmountValue: bigint // let estimatedExchangeRate: number if (debitAmount) { - console.log('getting receiveAmount from debitAmount via convert') debitAmountValue = debitAmount.value const converted = await deps.ratesService.convert({ sourceAmount: debitAmountValue, @@ -69,9 +75,7 @@ async function getQuote( ) } receiveAmountValue = converted - // estimatedExchangeRate = Number(debitAmountValue / receiveAmountValue) } else if (receiveAmount) { - console.log('getting debitAmount from receiveAmount via convert') receiveAmountValue = receiveAmount.value const converted = await deps.ratesService.convert({ sourceAmount: receiveAmountValue, @@ -94,10 +98,7 @@ async function getQuote( ) } debitAmountValue = converted - // estimatedExchangeRate = Number(receiveAmountValue / debitAmountValue) - // estimatedExchangeRate = Number(debitAmountValue / receiveAmountValue) } else if (receiver.incomingAmount) { - console.log('getting debitAmount from receiver.incomingAmount via convert') receiveAmountValue = receiver.incomingAmount.value const converted = await deps.ratesService.convert({ sourceAmount: receiveAmountValue, @@ -121,8 +122,6 @@ async function getQuote( ) } debitAmountValue = converted - // estimatedExchangeRate = Number(receiveAmountValue / debitAmountValue) - // estimatedExchangeRate = Number(debitAmountValue / receiveAmountValue) } else { throw new PaymentMethodHandlerError('Received error during local quoting', { description: 'No value provided to get quote from', @@ -133,8 +132,7 @@ async function getQuote( return { receiver: options.receiver, walletAddress: options.walletAddress, - // TODO: is this correct for both fixed send/receive? or needs to be flipped? - estimatedExchangeRate: Number(debitAmountValue / receiveAmountValue), + estimatedExchangeRate: Number(receiveAmountValue / debitAmountValue), debitAmount: { value: debitAmountValue, assetCode: options.walletAddress.asset.code, @@ -150,8 +148,74 @@ async function getQuote( } async function pay( - _deps: ServiceDependencies, - _options: PayOptions + deps: ServiceDependencies, + options: PayOptions ): Promise { - throw new Error('local pay not implemented') + const { outgoingPayment, receiver, finalDebitAmount, finalReceiveAmount } = + options + + // Cannot directly use receiver/receiver.incomingAccount for destinationAccount. + // createTransfer Expects LiquidityAccount (gets Peer in ilp). + const incomingPaymentId = receiver.incomingPayment.id.split('/').pop() + if (!incomingPaymentId) { + throw new PaymentMethodHandlerError('Received error during local payment', { + description: 'Incoming payment not found from receiver', + retryable: false + }) + } + const incomingPayment = await deps.incomingPaymentService.get({ + id: incomingPaymentId + }) + if (!incomingPayment) { + throw new PaymentMethodHandlerError('Received error during local payment', { + description: 'Incoming payment not found from receiver', + retryable: false + }) + } + + // TODO: refine this. hastily copied from rafiki/packages/backend/src/payment-method/ilp/connector/core/middleware/account.ts + // Necessary to avoid `UnknownDestinationAccount` error + try { + await deps.accountingService.createLiquidityAccount( + incomingPayment, + LiquidityAccountType.INCOMING + ) + deps.logger.debug( + { incomingPayment }, + 'Created liquidity account for local incoming payment' + ) + } catch (err) { + if (!(err instanceof AccountAlreadyExistsError)) { + deps.logger.error( + { incomingPayment, err }, + 'Failed to create liquidity account for local incoming payment' + ) + throw err + } + } + + const transferOptions: TransferOptions = { + sourceAccount: outgoingPayment, + destinationAccount: incomingPayment, + sourceAmount: finalDebitAmount, + destinationAmount: finalReceiveAmount, + transferType: TransferType.TRANSFER + } + + const trxOrError = + await deps.accountingService.createTransfer(transferOptions) + + if (isTransferError(trxOrError)) { + deps.logger.error( + { transferOptions, transferError: trxOrError }, + 'Could not create transfer' + ) + switch (trxOrError) { + case TransferError.InsufficientBalance: + case TransferError.InsufficientLiquidity: + throw new InsufficientLiquidityError(trxOrError) + default: + throw new Error('Unknown error while trying to create transfer') + } + } } From f3d5f5da7d59636fa05deadda98435ef8f6428f9 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:50:13 -0400 Subject: [PATCH 09/64] chore: rm comment --- packages/backend/src/graphql/resolvers/quote.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/graphql/resolvers/quote.ts b/packages/backend/src/graphql/resolvers/quote.ts index 62b5da7508..16bd2863e1 100644 --- a/packages/backend/src/graphql/resolvers/quote.ts +++ b/packages/backend/src/graphql/resolvers/quote.ts @@ -97,7 +97,6 @@ export const getWalletAddressQuotes: WalletAddressResolvers['quot } } -// TODO: update gql types (there is a pr pending for this) export function quoteToGraphql(quote: Quote): SchemaQuote { return { id: quote.id, From d795ce2e537f62a0b9fe9e488f15cb53fbf94221 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:10:52 -0400 Subject: [PATCH 10/64] chore: WIP debugging wrong sentAmount - for new local payment method service, sentAmount matches debitAmount, whereas is matches receive amount for local/remote ilp payments --- .../backend/src/accounting/psql/service.ts | 1 + packages/backend/src/accounting/service.ts | 35 ++++++++++++++ .../payment/outgoing/lifecycle.ts | 4 +- .../open_payments/payment/outgoing/service.ts | 1 + .../src/open_payments/quote/service.ts | 4 +- .../ilp/connector/core/middleware/balance.ts | 4 ++ .../ilp/connector/core/rafiki.ts | 1 + .../backend/src/payment-method/ilp/service.ts | 10 ++++ .../src/payment-method/local/service.ts | 48 ++++++++++++------- 9 files changed, 88 insertions(+), 20 deletions(-) diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index 6da25ecb96..54098d15c3 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -340,6 +340,7 @@ async function createAccountDeposit( type: LedgerTransferType.DEPOSIT } + // ilp - 617n (not final sentAmount) const { errors } = await createTransfers(deps, [transfer], trx) if (errors[0]) { diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index ba6adb1f85..1811d5211d 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -181,6 +181,41 @@ export async function createAccountToAccountTransfer( sourceAccount.asset.ledger === destinationAccount.asset.ledger ? buildSameAssetTransfers(transferArgs) : buildCrossAssetTransfers(transferArgs) + // transfersToCreateOrError + // for local payment method (on P2P > Create Outgoing Payment) + // - something not right here - shouldnt have the 110n one + // cloud-nine-backend-1 | { + // cloud-nine-backend-1 | transfersToCreateOrError: [ + // cloud-nine-backend-1 | { + // cloud-nine-backend-1 | sourceAccountId: '6c16f28c-85a9-4011-b693-b8a7cd8cc23b', + // cloud-nine-backend-1 | destinationAccountId: '549d4ef2-9c03-48e6-a734-a1eaab5a07ce', + // cloud-nine-backend-1 | amount: 500n, + // cloud-nine-backend-1 | ledger: 1, + // cloud-nine-backend-1 | transferType: 'TRANSFER' + // cloud-nine-backend-1 | }, + // cloud-nine-backend-1 | { + // cloud-nine-backend-1 | sourceAccountId: '6c16f28c-85a9-4011-b693-b8a7cd8cc23b', + // cloud-nine-backend-1 | destinationAccountId: '7c157fcd-3ea4-4cb4-bf3b-e498d9e749d3', + // cloud-nine-backend-1 | amount: 110n, + // cloud-nine-backend-1 | ledger: 1, + // cloud-nine-backend-1 | transferType: 'TRANSFER' + // cloud-nine-backend-1 | } + // cloud-nine-backend-1 | ] + // cloud-nine-backend-1 | } + + // for remote payment method (on P2P > Create Outgoing Payment) + // cloud-nine-backend-1 | transfersToCreateOrError: [ + // cloud-nine-backend-1 | { + // cloud-nine-backend-1 | sourceAccountId: '595aab3b-3a8d-4f42-af14-f7e359014a3c', + // cloud-nine-backend-1 | destinationAccountId: '0a07a341-a930-4d54-b6d7-d33faf9e4ff7', + // cloud-nine-backend-1 | amount: 500n, + // cloud-nine-backend-1 | ledger: 1, + // cloud-nine-backend-1 | transferType: 'TRANSFER' + // cloud-nine-backend-1 | } + // cloud-nine-backend-1 | ] + + // Why is there an extra transfer on the local one? is the extra a fee? (both values sum to equal debitAmount in any case) + // The extra 110n one is to a different destination too ... ? if (isTransferError(transfersToCreateOrError)) { return transfersToCreateOrError diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index f2772c131a..b2275b9357 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -78,8 +78,8 @@ export async function handleSending( const payStartTime = Date.now() // TODO: use receiver.isLocal - const isLocal = false - // const isLocal = true + // const isLocal = false + const isLocal = true await deps.paymentMethodHandlerService.pay(isLocal ? 'LOCAL' : 'ILP', { receiver, outgoingPayment: payment, diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index bbe5039df9..224c56445f 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -477,6 +477,7 @@ async function fundPayment( const error = await deps.accountingService.createDeposit({ id: transferId, account: payment, + // ilp - 617n (not final sentAmount) amount }) if (error) { diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index ac632e5183..af01e3f07d 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -114,8 +114,8 @@ async function createQuote( try { const receiver = await resolveReceiver(deps, options) // TODO: use reciver.isLocal - const isLocal = false - // const isLocal = true + // const isLocal = false + const isLocal = true const quote = await deps.paymentMethodHandlerService.getQuote( isLocal ? 'LOCAL' : 'ILP', { diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/balance.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/balance.ts index 512ef863d1..efb7446417 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/balance.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/balance.ts @@ -34,6 +34,10 @@ export function createBalanceMiddleware(): ILPMiddleware { return } + // in happy-path (non local p2p create outgoing payment) + // both source and destination amounts are 500n. In local payment + // method, one is 610 but should be 500. S + const sourceAmount = BigInt(amount) const destinationAmountOrError = await services.rates.convert({ sourceAmount, diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index 16e58f8e69..c72bc4778c 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -153,6 +153,7 @@ export class Rafiki { unfulfillable: boolean, rawPrepare: Buffer ): Promise { + // prepare amount = 500 const prepare = new ZeroCopyIlpPrepare(rawPrepare) const response = new IlpResponse() diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index 4638567a28..07d2e76cdb 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -238,7 +238,11 @@ async function pay( const quote: Pay.Quote = { maxPacketAmount, + // TODO: is paymentType controlling something in Pay.pay that we should be replicating in + // local payment method? paymentType: Pay.PaymentType.FixedDelivery, + // finalDebitAmount: 617n + // finalReceiveAmount: 500n maxSourceAmount: finalDebitAmount, minDeliveryAmount: finalReceiveAmount, lowEstimatedExchangeRate, @@ -247,12 +251,18 @@ async function pay( } const plugin = deps.makeIlpPlugin({ + // TODO: what is the outgoingPayment.quote.receiveAmountValue in local pay?? + // is it 500n or 610n? maybe outgoing payment quote is wrong? + // outgoingPayment.quote.receiveAmountValue: 500n + // outgoingPayment.quote.debitAmountValue: 617n sourceAccount: outgoingPayment }) const destination = receiver.toResolvedPayment() try { + // receipt.amountDelivered: 500n + // receipt.amountSent: 500n const receipt = await Pay.pay({ plugin, destination, quote }) if (receipt.error) { diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 2263eaa8d6..7ca2ca506d 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -21,6 +21,7 @@ import { } from '../../accounting/errors' import { InsufficientLiquidityError } from 'ilp-packet/dist/errors' import { IncomingPaymentService } from '../../open_payments/payment/incoming/service' +import { IncomingPaymentState } from '../../open_payments/payment/incoming/model' export interface LocalPaymentService extends PaymentMethodService {} @@ -173,35 +174,49 @@ async function pay( }) } - // TODO: refine this. hastily copied from rafiki/packages/backend/src/payment-method/ilp/connector/core/middleware/account.ts + // TODO: anything more needed from balance middleware? // Necessary to avoid `UnknownDestinationAccount` error - try { - await deps.accountingService.createLiquidityAccount( - incomingPayment, - LiquidityAccountType.INCOMING - ) - deps.logger.debug( - { incomingPayment }, - 'Created liquidity account for local incoming payment' - ) - } catch (err) { - if (!(err instanceof AccountAlreadyExistsError)) { - deps.logger.error( - { incomingPayment, err }, - 'Failed to create liquidity account for local incoming payment' + // TODO: remove incoming state check? perhaps only applies ilp account middleware where its checking many different things + if (incomingPayment.state === IncomingPaymentState.Pending) { + try { + await deps.accountingService.createLiquidityAccount( + incomingPayment, + LiquidityAccountType.INCOMING ) - throw err + deps.logger.debug( + { incomingPayment }, + 'Created liquidity account for local incoming payment' + ) + } catch (err) { + if (!(err instanceof AccountAlreadyExistsError)) { + deps.logger.error( + { incomingPayment, err }, + 'Failed to create liquidity account for local incoming payment' + ) + throw err + } } } const transferOptions: TransferOptions = { + // outgoingPayment.quote.debitAmount = 610n + // outgoingPayment.quote.receiveAmount = 500n + // ^ consistent with ilp payment method sourceAccount: outgoingPayment, destinationAccount: incomingPayment, + // finalDebitAmount: 610n + // finalReceiveAmount: 500n + // ^ consistent with ilp payment method sourceAmount: finalDebitAmount, destinationAmount: finalReceiveAmount, transferType: TransferType.TRANSFER } + // Here, createTransfer gets debitAmount = 610 and receiveAmount = 500n + // however, in ilp balance middleware its 500/500. In ilpPaymentService.pay, + // Pay.pay is called with the same sort of quote as here. debitAmount = 617 (higher due to slippage?) + // and receiveAmount = 500. There is some adjustment happening between Pay.pay (in Pay.pay?) + // (in ilpPaymentMethodService.pay) and createTransfer in the connector balance middleware const trxOrError = await deps.accountingService.createTransfer(transferOptions) @@ -218,4 +233,5 @@ async function pay( throw new Error('Unknown error while trying to create transfer') } } + await trxOrError.post() } From 251c60aa27abad1dcd91cad421ba3e5ff889dbcc Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:16:00 -0400 Subject: [PATCH 11/64] chore: rm comment --- packages/backend/src/payment-method/ilp/service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index 07d2e76cdb..b0df8ccc7c 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -251,8 +251,6 @@ async function pay( } const plugin = deps.makeIlpPlugin({ - // TODO: what is the outgoingPayment.quote.receiveAmountValue in local pay?? - // is it 500n or 610n? maybe outgoing payment quote is wrong? // outgoingPayment.quote.receiveAmountValue: 500n // outgoingPayment.quote.debitAmountValue: 617n sourceAccount: outgoingPayment From eca01f11ecec43b0a34eedd23586f27322b837ae Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:38:04 -0400 Subject: [PATCH 12/64] feat(backend): use receiver.isLocal to control payment method in quote/outgoing payment --- .../payment/outgoing/lifecycle.ts | 18 +++++++++--------- .../backend/src/open_payments/quote/service.ts | 7 ++----- .../src/payment-method/local/service.ts | 6 +++++- packages/backend/src/rates/service.ts | 5 ----- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index b2275b9357..655d2d9d8b 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -77,15 +77,15 @@ export async function handleSending( } const payStartTime = Date.now() - // TODO: use receiver.isLocal - // const isLocal = false - const isLocal = true - await deps.paymentMethodHandlerService.pay(isLocal ? 'LOCAL' : 'ILP', { - receiver, - outgoingPayment: payment, - finalDebitAmount: maxDebitAmount, - finalReceiveAmount: maxReceiveAmount - }) + await deps.paymentMethodHandlerService.pay( + receiver.isLocal ? 'LOCAL' : 'ILP', + { + receiver, + outgoingPayment: payment, + finalDebitAmount: maxDebitAmount, + finalReceiveAmount: maxReceiveAmount + } + ) const payEndTime = Date.now() if (deps.telemetry) { diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index af01e3f07d..1a900f6d81 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -113,11 +113,8 @@ async function createQuote( try { const receiver = await resolveReceiver(deps, options) - // TODO: use reciver.isLocal - // const isLocal = false - const isLocal = true const quote = await deps.paymentMethodHandlerService.getQuote( - isLocal ? 'LOCAL' : 'ILP', + receiver.isLocal ? 'LOCAL' : 'ILP', { walletAddress, receiver, @@ -143,7 +140,7 @@ async function createQuote( estimatedExchangeRate: quote.estimatedExchangeRate } - if (!isLocal) { + if (!receiver.isLocal) { const maxPacketAmount = quote.additionalFields.maxPacketAmount as bigint graph.ilpQuoteDetails = { maxPacketAmount: diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 7ca2ca506d..021cf73d09 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -206,7 +206,11 @@ async function pay( destinationAccount: incomingPayment, // finalDebitAmount: 610n // finalReceiveAmount: 500n - // ^ consistent with ilp payment method + // ^ consistent with amounts passed into ilpPaymentMethodService.pay and Pay.pay + // ^ inconsisten with transfer passed into createTrasnfer in balance middleware + // - that is sourceAmount: 500, destinationAmount: 500 + // - this transfer gets 500 from the request.prepare (formed from Pay.pay?). request.prepare + // is the sourceAmount then the destinationAmount is derived from it with rates.convert sourceAmount: finalDebitAmount, destinationAmount: finalReceiveAmount, transferType: TransferType.TRANSFER diff --git a/packages/backend/src/rates/service.ts b/packages/backend/src/rates/service.ts index 36a8c8cbc4..5a6ed7322e 100644 --- a/packages/backend/src/rates/service.ts +++ b/packages/backend/src/rates/service.ts @@ -54,19 +54,14 @@ class RatesServiceImpl implements RatesService { async convert( opts: Omit ): Promise { - console.log('convert called with', { opts }) const sameCode = opts.sourceAsset.code === opts.destinationAsset.code const sameScale = opts.sourceAsset.scale === opts.destinationAsset.scale if (sameCode && sameScale) return opts.sourceAmount if (sameCode) return convert({ exchangeRate: 1.0, ...opts }) const { rates } = await this.getRates(opts.sourceAsset.code) - console.log('convert got rates', { rates }) const destinationExchangeRate = rates[opts.destinationAsset.code] - console.log('convert got destinationExchangeRate', { - destinationExchangeRate - }) if (!destinationExchangeRate || !isValidPrice(destinationExchangeRate)) { return ConvertError.InvalidDestinationPrice } From 170bcc3b72e11bc264e349cd2ac0ae9a848ca8ed Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:03:31 -0400 Subject: [PATCH 13/64] fix(backend): added source amount --- localenv/cloud-nine-wallet/seed.yml | 6 + ...40809171654_add_ilp_quote_details_table.js | 1 + packages/backend/src/accounting/service.ts | 35 - packages/backend/src/fee/service.test.ts | 18 + packages/backend/src/fee/service.ts | 11 +- packages/backend/src/index.ts | 4 +- .../backend/src/open_payments/quote/model.ts | 1 + .../src/open_payments/quote/service.ts | 12 + .../src/payment-method/handler/service.ts | 2 + .../src/payment-method/local/service.test.ts | 661 ++++++++++-------- .../src/payment-method/local/service.ts | 64 +- 11 files changed, 493 insertions(+), 322 deletions(-) diff --git a/localenv/cloud-nine-wallet/seed.yml b/localenv/cloud-nine-wallet/seed.yml index eff7d687df..75ad0398be 100644 --- a/localenv/cloud-nine-wallet/seed.yml +++ b/localenv/cloud-nine-wallet/seed.yml @@ -46,6 +46,12 @@ accounts: path: accounts/broke brunoEnvVar: brokeWalletAddress assetCode: USD + - name: "Luca Rossi" + id: 63dcc665-d946-4263-ac27-d0da1eb08a83 + initialBalance: 50 + path: accounts/lrossi + brunoEnvVar: lrossiWalletAddressId + assetCode: EUR rates: EUR: MXN: 18.78 diff --git a/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js b/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js index 23d7ad0e5c..8b6fa6f680 100644 --- a/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js +++ b/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js @@ -80,6 +80,7 @@ exports.up = function (knex) { table.dropColumn('lowEstimatedExchangeRateDenominator') table.dropColumn('highEstimatedExchangeRateNumerator') table.dropColumn('highEstimatedExchangeRateDenominator') + table.bigInteger('sourceAmount').nullable() }) }) ) diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index 1811d5211d..ba6adb1f85 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -181,41 +181,6 @@ export async function createAccountToAccountTransfer( sourceAccount.asset.ledger === destinationAccount.asset.ledger ? buildSameAssetTransfers(transferArgs) : buildCrossAssetTransfers(transferArgs) - // transfersToCreateOrError - // for local payment method (on P2P > Create Outgoing Payment) - // - something not right here - shouldnt have the 110n one - // cloud-nine-backend-1 | { - // cloud-nine-backend-1 | transfersToCreateOrError: [ - // cloud-nine-backend-1 | { - // cloud-nine-backend-1 | sourceAccountId: '6c16f28c-85a9-4011-b693-b8a7cd8cc23b', - // cloud-nine-backend-1 | destinationAccountId: '549d4ef2-9c03-48e6-a734-a1eaab5a07ce', - // cloud-nine-backend-1 | amount: 500n, - // cloud-nine-backend-1 | ledger: 1, - // cloud-nine-backend-1 | transferType: 'TRANSFER' - // cloud-nine-backend-1 | }, - // cloud-nine-backend-1 | { - // cloud-nine-backend-1 | sourceAccountId: '6c16f28c-85a9-4011-b693-b8a7cd8cc23b', - // cloud-nine-backend-1 | destinationAccountId: '7c157fcd-3ea4-4cb4-bf3b-e498d9e749d3', - // cloud-nine-backend-1 | amount: 110n, - // cloud-nine-backend-1 | ledger: 1, - // cloud-nine-backend-1 | transferType: 'TRANSFER' - // cloud-nine-backend-1 | } - // cloud-nine-backend-1 | ] - // cloud-nine-backend-1 | } - - // for remote payment method (on P2P > Create Outgoing Payment) - // cloud-nine-backend-1 | transfersToCreateOrError: [ - // cloud-nine-backend-1 | { - // cloud-nine-backend-1 | sourceAccountId: '595aab3b-3a8d-4f42-af14-f7e359014a3c', - // cloud-nine-backend-1 | destinationAccountId: '0a07a341-a930-4d54-b6d7-d33faf9e4ff7', - // cloud-nine-backend-1 | amount: 500n, - // cloud-nine-backend-1 | ledger: 1, - // cloud-nine-backend-1 | transferType: 'TRANSFER' - // cloud-nine-backend-1 | } - // cloud-nine-backend-1 | ] - - // Why is there an extra transfer on the local one? is the extra a fee? (both values sum to equal debitAmount in any case) - // The extra 110n one is to a different destination too ... ? if (isTransferError(transfersToCreateOrError)) { return transfersToCreateOrError diff --git a/packages/backend/src/fee/service.test.ts b/packages/backend/src/fee/service.test.ts index e57068f7e3..a26ac12603 100644 --- a/packages/backend/src/fee/service.test.ts +++ b/packages/backend/src/fee/service.test.ts @@ -156,6 +156,24 @@ describe('Fee Service', (): void => { expect(latestFee).toBeUndefined() }) }) + describe('getById', (): void => { + it('should get fee by id', async (): Promise => { + const fee = await Fee.query().insertAndFetch({ + assetId: asset.id, + type: FeeType.Receiving, + basisPointFee: 100, + fixedFee: BigInt(100) + }) + + const foundFee = await feeService.getById(fee.id) + expect(foundFee).toMatchObject(fee) + }) + + it('should return undefined when not foudn fee by id', async (): Promise => { + const foundFee = await feeService.getById(v4()) + expect(foundFee).toBe(undefined) + }) + }) describe('Fee pagination', (): void => { getPageTests({ diff --git a/packages/backend/src/fee/service.ts b/packages/backend/src/fee/service.ts index 4979cb18a4..9fc24a4de1 100644 --- a/packages/backend/src/fee/service.ts +++ b/packages/backend/src/fee/service.ts @@ -21,6 +21,7 @@ export interface FeeService { sortOrder?: SortOrder ): Promise getLatestFee(assetId: string, type: FeeType): Promise + getById(feeId: string): Promise } type ServiceDependencies = BaseService @@ -43,7 +44,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), + getById: (feeId: string) => getById(deps, feeId) } } @@ -71,6 +73,13 @@ async function getLatestFee( .first() } +async function getById( + deps: ServiceDependencies, + feeId: string +): Promise { + return await Fee.query(deps.knex).findById(feeId) +} + async function createFee( deps: ServiceDependencies, { assetId, type, fee }: CreateOptions diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 6146c0889d..23eabdb8ea 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -451,7 +451,9 @@ export function initIocContainer( config: await deps.use('config'), ratesService: await deps.use('ratesService'), accountingService: await deps.use('accountingService'), - incomingPaymentService: await deps.use('incomingPaymentService') + incomingPaymentService: await deps.use('incomingPaymentService'), + // TODO: rm if saving base debitAmount on quote + feeService: await deps.use('feeService') } return createLocalPaymentService(serviceDependencies) diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index c179585fe2..d0dfb0d5da 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -34,6 +34,7 @@ export class Quote extends WalletAddressSubresource { public fee?: Fee public ilpQuoteDetails?: IlpQuoteDetails + public sourceAmount?: bigint static get relationMappings() { return { diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 1a900f6d81..f5105dc952 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -128,6 +128,8 @@ async function createQuote( FeeType.Sending ) + console.log({ sendingFee }) + const graph: PartialModelGraph = { walletAddressId: options.walletAddressId, assetId: walletAddress.assetId, @@ -357,7 +359,17 @@ async function finalizeQuote( ? calculateFixedSendQuoteAmounts(deps, quote, maxReceiveAmountValue) : calculateFixedDeliveryQuoteAmounts(deps, quote) + console.log({ + debitAmountValue, + receiveAmountValue, + 'quote.debitAmount.value': quote.debitAmount.value, + maxReceiveAmountValue + }) + const patchOptions = { + sourceAmount: maxReceiveAmountValue + ? receiveAmountValue + : quote.debitAmount.value, debitAmountValue, receiveAmountValue, expiresAt: calculateExpiry(deps, quote, receiver) diff --git a/packages/backend/src/payment-method/handler/service.ts b/packages/backend/src/payment-method/handler/service.ts index c7bb2dfc0e..2008a64af5 100644 --- a/packages/backend/src/payment-method/handler/service.ts +++ b/packages/backend/src/payment-method/handler/service.ts @@ -19,6 +19,8 @@ export interface PaymentQuote { debitAmount: Amount receiveAmount: Amount estimatedExchangeRate: number + // TODO: make this an amount like debitAmount? update db field, handle conversion to num/denom, etc. + // sourceAmount: bigint additionalFields: Record } diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 07d56df898..8807dff7ab 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -16,16 +16,19 @@ import { WalletAddress } from '../../open_payments/wallet_address/model' import { createReceiver } from '../../tests/receiver' import { mockRatesApi } from '../../tests/rates' -// import { AccountingService } from '../../accounting/service' +import { AccountingService } from '../../accounting/service' import { truncateTables } from '../../tests/tableManager' +import { createOutgoingPaymentWithReceiver } from '../../tests/outgoingPayment' +import { OutgoingPayment } from '../../open_payments/payment/outgoing/model' +import { IncomingPayment } from '../../open_payments/payment/incoming/model' const nock = (global as unknown as { nock: typeof import('nock') }).nock -describe('IlpPaymentService', (): void => { +describe('LocalPaymentService', (): void => { let deps: IocContract let appContainer: TestContainer let localPaymentService: LocalPaymentService - // let accountingService: AccountingService + let accountingService: AccountingService // let config: IAppConfig const exchangeRatesUrl = 'https://example-rates.com' @@ -43,7 +46,7 @@ describe('IlpPaymentService', (): void => { // config = await deps.use('config') localPaymentService = await deps.use('localPaymentService') - // accountingService = await deps.use('accountingService') + accountingService = await deps.use('accountingService') }) beforeEach(async (): Promise => { @@ -335,8 +338,8 @@ describe('IlpPaymentService', (): void => { // ratesScope.done() // }) - describe('successfully gets ilp quote', (): void => { - describe.only('with incomingAmount', () => { + describe('successfully gets local quote', (): void => { + describe('with incomingAmount', () => { test.each` incomingAssetCode | incomingAmountValue | debitAssetCode | expectedDebitAmount | exchangeRate | description ${'EUR'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'cross currency, same rate'} @@ -401,7 +404,7 @@ describe('IlpPaymentService', (): void => { ) }) - describe.only('with debitAmount', () => { + describe('with debitAmount', () => { test.each` debitAssetCode | debitAmountValue | incomingAssetCode | expectedReceiveAmount | exchangeRate | description ${'EUR'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'cross currency, same rate'} @@ -460,281 +463,371 @@ describe('IlpPaymentService', (): void => { }) }) - // describe('pay', (): void => { - // function mockIlpPay( - // overrideQuote: Partial, - // error?: Pay.PaymentError - // ): jest.SpyInstance< - // Promise, - // [options: Pay.PayOptions] - // > { - // return jest - // .spyOn(Pay, 'pay') - // .mockImplementationOnce(async (opts: Pay.PayOptions) => { - // const res = await Pay.pay({ - // ...opts, - // quote: { ...opts.quote, ...overrideQuote } - // }) - // if (error) res.error = error - // return res - // }) - // } - - // async function validateBalances( - // outgoingPayment: OutgoingPayment, - // incomingPayment: IncomingPayment, - // { - // amountSent, - // amountReceived - // }: { - // amountSent: bigint - // amountReceived: bigint - // } - // ) { - // await expect( - // accountingService.getTotalSent(outgoingPayment.id) - // ).resolves.toBe(amountSent) - // await expect( - // accountingService.getTotalReceived(incomingPayment.id) - // ).resolves.toEqual(amountReceived) - // } - - // test('successfully streams between accounts', async (): Promise => { - // const { incomingPayment, receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // await expect( - // ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 100n, - // finalReceiveAmount: 100n - // }) - // ).resolves.toBeUndefined() - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 100n, - // amountReceived: 100n - // }) - // }) - - // test('partially streams between accounts, then streams to completion', async (): Promise => { - // const { incomingPayment, receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // exchangeRate: 1, - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // mockIlpPay( - // { maxSourceAmount: 5n, minDeliveryAmount: 5n }, - // Pay.PaymentError.ClosedByReceiver - // ) - - // await expect( - // ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 100n, - // finalReceiveAmount: 100n - // }) - // ).rejects.toThrow(PaymentMethodHandlerError) - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 5n, - // amountReceived: 5n - // }) - - // await expect( - // ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 100n - 5n, - // finalReceiveAmount: 100n - 5n - // }) - // ).resolves.toBeUndefined() - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 100n, - // amountReceived: 100n - // }) - // }) - - // test('throws if invalid finalDebitAmount', async (): Promise => { - // const { incomingPayment, receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // expect.assertions(6) - // try { - // await ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 0n, - // finalReceiveAmount: 50n - // }) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Could not start ILP streaming' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // 'Invalid finalDebitAmount' - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // } - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 0n, - // amountReceived: 0n - // }) - // }) - - // test('throws if invalid finalReceiveAmount', async (): Promise => { - // const { incomingPayment, receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // expect.assertions(6) - // try { - // await ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 50n, - // finalReceiveAmount: 0n - // }) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Could not start ILP streaming' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // 'Invalid finalReceiveAmount' - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // } - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 0n, - // amountReceived: 0n - // }) - // }) - - // test('throws retryable ILP error', async (): Promise => { - // const { receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // mockIlpPay({}, Object.keys(retryableIlpErrors)[0] as Pay.PaymentError) - - // expect.assertions(4) - // try { - // await ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 50n, - // finalReceiveAmount: 50n - // }) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Received error during ILP pay' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // Object.keys(retryableIlpErrors)[0] - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(true) - // } - // }) - - // test('throws non-retryable ILP error', async (): Promise => { - // const { receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // const nonRetryableIlpError = Object.values(Pay.PaymentError).find( - // (error) => !retryableIlpErrors[error] - // ) - - // mockIlpPay({}, nonRetryableIlpError) - - // expect.assertions(4) - // try { - // await ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 50n, - // finalReceiveAmount: 50n - // }) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Received error during ILP pay' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // nonRetryableIlpError - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // } - // }) - // }) + describe('pay', (): void => { + // function mockIlpPay( + // overrideQuote: Partial, + // error?: Pay.PaymentError + // ): jest.SpyInstance< + // Promise, + // [options: Pay.PayOptions] + // > { + // return jest + // .spyOn(Pay, 'pay') + // .mockImplementationOnce(async (opts: Pay.PayOptions) => { + // const res = await Pay.pay({ + // ...opts, + // quote: { ...opts.quote, ...overrideQuote } + // }) + // if (error) res.error = error + // return res + // }) + // } + + async function validateBalances( + outgoingPayment: OutgoingPayment, + incomingPayment: IncomingPayment, + { + amountSent, + amountReceived + }: { + amountSent: bigint + amountReceived: bigint + } + ) { + await expect( + accountingService.getTotalSent(outgoingPayment.id) + ).resolves.toBe(amountSent) + await expect( + accountingService.getTotalReceived(incomingPayment.id) + ).resolves.toEqual(amountReceived) + } + + test('succesfully make local payment', async (): Promise => { + const { incomingPayment, receiver, outgoingPayment } = + await createOutgoingPaymentWithReceiver(deps, { + sendingWalletAddress: walletAddressMap['USD'], + receivingWalletAddress: walletAddressMap['USD'], + method: 'ilp', + quoteOptions: { + debitAmount: { + value: 100n, + assetScale: walletAddressMap['USD'].asset.scale, + assetCode: walletAddressMap['USD'].asset.code + } + } + }) + + const payResponse = await localPaymentService.pay({ + receiver, + outgoingPayment, + finalDebitAmount: 100n, + finalReceiveAmount: 100n + }) + + console.log({ payResponse }) + + expect(payResponse).toBe(undefined) + + await validateBalances(outgoingPayment, incomingPayment, { + amountSent: 100n, + amountReceived: 100n + }) + }) + + test.only('succesfully make local payment with fee', async (): Promise => { + // for this case, the underyling outgoing payment that gets created should have a quote that with amounts + // that look like: + // { + // "id": "a6a157d7-93ab-4104-b590-3cae00a30798", + // "walletAddressId": "9683a8bf-2a24-4dc1-853e-9d11d6681115", + // "receiver": "https://cloud-nine-wallet-backend/incoming-payments/c1617263-3b29-4d6b-9561-a5723b3e16ac", + // "debitAmount": { + // "value": "610", + // "assetCode": "USD", + // "assetScale": 2 + // }, + // "receiveAmount": { + // "value": "500", + // "assetCode": "USD", + // "assetScale": 2 + // }, + // "createdAt": "2024-08-21T17:45:07.227Z", + // "expiresAt": "2024-08-21T17:50:07.227Z" + // } + const { incomingPayment, receiver, outgoingPayment } = + await createOutgoingPaymentWithReceiver(deps, { + sendingWalletAddress: walletAddressMap['USD'], + receivingWalletAddress: walletAddressMap['USD'], + method: 'ilp', + quoteOptions: { + debitAmount: { + value: 610n, + assetScale: walletAddressMap['USD'].asset.scale, + assetCode: walletAddressMap['USD'].asset.code + } + } + }) + + console.log( + 'do the amounts/quote match the expected ones in the test code comment?', + { outgoingPayment } + ) + + expect(true).toBe(false) + + const payResponse = await localPaymentService.pay({ + receiver, + outgoingPayment, + finalDebitAmount: 100n, + finalReceiveAmount: 100n + }) + + console.log({ payResponse }) + + expect(payResponse).toBe(undefined) + + await validateBalances(outgoingPayment, incomingPayment, { + amountSent: 100n, + amountReceived: 100n + }) + }) + + // test('successfully streams between accounts', async (): Promise => { + // const { incomingPayment, receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // await expect( + // ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 100n, + // finalReceiveAmount: 100n + // }) + // ).resolves.toBeUndefined() + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 100n, + // amountReceived: 100n + // }) + // }) + + // test('partially streams between accounts, then streams to completion', async (): Promise => { + // const { incomingPayment, receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // exchangeRate: 1, + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // mockIlpPay( + // { maxSourceAmount: 5n, minDeliveryAmount: 5n }, + // Pay.PaymentError.ClosedByReceiver + // ) + + // await expect( + // ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 100n, + // finalReceiveAmount: 100n + // }) + // ).rejects.toThrow(PaymentMethodHandlerError) + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 5n, + // amountReceived: 5n + // }) + + // await expect( + // ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 100n - 5n, + // finalReceiveAmount: 100n - 5n + // }) + // ).resolves.toBeUndefined() + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 100n, + // amountReceived: 100n + // }) + // }) + + // test('throws if invalid finalDebitAmount', async (): Promise => { + // const { incomingPayment, receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // expect.assertions(6) + // try { + // await ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 0n, + // finalReceiveAmount: 50n + // }) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Could not start ILP streaming' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // 'Invalid finalDebitAmount' + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // } + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 0n, + // amountReceived: 0n + // }) + // }) + + // test('throws if invalid finalReceiveAmount', async (): Promise => { + // const { incomingPayment, receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // expect.assertions(6) + // try { + // await ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 50n, + // finalReceiveAmount: 0n + // }) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Could not start ILP streaming' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // 'Invalid finalReceiveAmount' + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // } + + // await validateBalances(outgoingPayment, incomingPayment, { + // amountSent: 0n, + // amountReceived: 0n + // }) + // }) + + // test('throws retryable ILP error', async (): Promise => { + // const { receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // mockIlpPay({}, Object.keys(retryableIlpErrors)[0] as Pay.PaymentError) + + // expect.assertions(4) + // try { + // await ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 50n, + // finalReceiveAmount: 50n + // }) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Received error during ILP pay' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // Object.keys(retryableIlpErrors)[0] + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(true) + // } + // }) + + // test('throws non-retryable ILP error', async (): Promise => { + // const { receiver, outgoingPayment } = + // await createOutgoingPaymentWithReceiver(deps, { + // sendingWalletAddress: walletAddressMap['USD'], + // receivingWalletAddress: walletAddressMap['USD'], + // method: 'ilp', + // quoteOptions: { + // debitAmount: { + // value: 100n, + // assetScale: walletAddressMap['USD'].asset.scale, + // assetCode: walletAddressMap['USD'].asset.code + // } + // } + // }) + + // const nonRetryableIlpError = Object.values(Pay.PaymentError).find( + // (error) => !retryableIlpErrors[error] + // ) + + // mockIlpPay({}, nonRetryableIlpError) + + // expect.assertions(4) + // try { + // await ilpPaymentService.pay({ + // receiver, + // outgoingPayment, + // finalDebitAmount: 50n, + // finalReceiveAmount: 50n + // }) + // } catch (error) { + // expect(error).toBeInstanceOf(PaymentMethodHandlerError) + // expect((error as PaymentMethodHandlerError).message).toBe( + // 'Received error during ILP pay' + // ) + // expect((error as PaymentMethodHandlerError).description).toBe( + // nonRetryableIlpError + // ) + // expect((error as PaymentMethodHandlerError).retryable).toBe(false) + // } + // }) + }) }) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 021cf73d09..1efed196e4 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -22,6 +22,8 @@ import { import { InsufficientLiquidityError } from 'ilp-packet/dist/errors' import { IncomingPaymentService } from '../../open_payments/payment/incoming/service' import { IncomingPaymentState } from '../../open_payments/payment/incoming/model' +import { FeeService } from '../../fee/service' +import { Fee } from '../../fee/model' export interface LocalPaymentService extends PaymentMethodService {} @@ -30,6 +32,7 @@ export interface ServiceDependencies extends BaseService { ratesService: RatesService accountingService: AccountingService incomingPaymentService: IncomingPaymentService + feeService: FeeService } export async function createLocalPaymentService( @@ -154,6 +157,11 @@ async function pay( ): Promise { const { outgoingPayment, receiver, finalDebitAmount, finalReceiveAmount } = options + if (!outgoingPayment.quote.sourceAmount) { + // TODO: handle this better. perhaps sourceAmount should not be nullable? + // If throwing an error, follow existing patterns + throw new Error('could do local pay, missing sourceAmount') + } // Cannot directly use receiver/receiver.incomingAccount for destinationAccount. // createTransfer Expects LiquidityAccount (gets Peer in ilp). @@ -198,6 +206,60 @@ async function pay( } } + console.log('pay fee', outgoingPayment.quote.fee) + console.log('pay feeid', outgoingPayment.quote.feeId) + + // let feeAmount: number | null + + // baseDebitAmount excludes fees + let sourceAmount = outgoingPayment.quote.sourceAmount //finalDebitAmount + + console.log({ finalDebitAmount, sourceAmount, finalReceiveAmount }) + + // if (outgoingPayment.quote.feeId) { + // const fee = await deps.feeService.getById(outgoingPayment.quote.feeId) + + // if (!fee) { + // throw new PaymentMethodHandlerError( + // 'Received error during local payment', + // { + // description: 'Quote fee could not be found by id', + // retryable: false + // } + // ) + // } + + // // TODO: store this on quote instead? + // // Original debit amount value excluding fees + // sourceAmount = BigInt( + // Math.floor( + // Number(finalDebitAmount - fee.fixedFee) / + // (1 + fee.basisPointFee / 10000) + // ) + // ) + // } + + // TODO: is this going to work for fixed send/delivery? + // At this stage does that concept still have an effect? in ilp pay I + // think its all fixed delivery here... + + // [x] Use finalDebitAmount - fees for sourceAmount and finalReceiveAmount for destinationAmount + // ilp pay is sending min of debitAmount/converted min delivery (finalReceiveAmount) + + // extra things to verify: + // - [X] before implenting this, ensure transfersToCreateOrError 110 amt destinationAccontId is the us asset account + // - IT DOES MATCH. the destinationAccontId for the 110 amt corresponds to USD asset id + // - 110 record had destinationAccountId: 7b628461-0d45-4cd5-9b2d-ec6de9fe8159 + // - ledgerAccount had 2 records with accountRef: 7b628461-0d45-4cd5-9b2d-ec6de9fe8159. + // 1 with type SETTLEMENT and another with type LIQUIDITY_ASSET + // - USD asset had id of 7b628461-0d45-4cd5-9b2d-ec6de9fe8159 + // - [ ] after implementing, ensure there are 3 transfers made for cross currency: + // - 1 outgoing payment to usd asset + // - 1 usd asset to fx asset + // - 1 fx asset to incoming payment + + // const fee = await deps.feeService.get(outgoingPayment.quote.feeId) + const transferOptions: TransferOptions = { // outgoingPayment.quote.debitAmount = 610n // outgoingPayment.quote.receiveAmount = 500n @@ -211,7 +273,7 @@ async function pay( // - that is sourceAmount: 500, destinationAmount: 500 // - this transfer gets 500 from the request.prepare (formed from Pay.pay?). request.prepare // is the sourceAmount then the destinationAmount is derived from it with rates.convert - sourceAmount: finalDebitAmount, + sourceAmount, destinationAmount: finalReceiveAmount, transferType: TransferType.TRANSFER } From 88618cee33daf48b2a301012da543d47370acf1a Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:08:54 -0400 Subject: [PATCH 14/64] fix(backend): p2p case (cross currency, local, fixed send) --- .../src/open_payments/quote/service.ts | 74 ++++++++++++++++++- .../src/payment-method/local/service.ts | 3 +- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index f5105dc952..8c626c3b75 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -240,6 +240,11 @@ function calculateFixedSendQuoteAmounts( quote: Quote, maxReceiveAmountValue: bigint ): CalculateQuoteAmountsWithFeesResult { + // TODO: rework to + // debitAmount = 500 + // sourceAmount/debitAmountMinusFees = 500 - (500 * 0.02 + 100) = 390 + // receiveAmount = floor(debitAmountMinusFees * estimatedExchangeRate) = floor(390 * 0.91) = 354 + const fees = quote.fee?.calculate(quote.receiveAmount.value) ?? BigInt(0) const { estimatedExchangeRate } = quote @@ -264,6 +269,7 @@ function calculateFixedSendQuoteAmounts( deps.logger.debug( { + 'quote.receiveAmount.value': quote.receiveAmount.value, debitAmountValue: quote.debitAmount.value, receiveAmountValue, fees, @@ -278,6 +284,70 @@ function calculateFixedSendQuoteAmounts( } } +// WIP: rework to +// debitAmount = 500 +// sourceAmount/debitAmountMinusFees = 500 - (500 * 0.02 + 100) = 390 +// receiveAmount = floor(debitAmountMinusFees * estimatedExchangeRate) = floor(390 * 0.91) = 354 + +// Problem: some quote tests are off by 1. changing calculate to floor/receive amount to floor (and every combo) didnt seem to fix it + +// function calculateFixedSendQuoteAmounts( +// deps: ServiceDependencies, +// quote: Quote, +// maxReceiveAmountValue: bigint +// ): CalculateQuoteAmountsWithFeesResult { +// // TODO: rework to +// // debitAmount = 500 +// // sourceAmount/debitAmountMinusFees = 500 - (500 * 0.02 + 100) = 390 +// // receiveAmount = floor(debitAmountMinusFees * estimatedExchangeRate) = floor(390 * 0.91) = 354 + +// const fees = quote.fee?.calculate(quote.debitAmount.value) ?? BigInt(0) +// const debitAmountMinusFees = quote.debitAmount.value - fees +// const { estimatedExchangeRate } = quote +// const receiveAmountValue = BigInt( +// Math.floor(Number(debitAmountMinusFees) * estimatedExchangeRate) +// ) + +// // console.log({ estimatedExchangeRate }) + +// // const exchangeAdjustedFees = BigInt( +// // Math.ceil(Number(fees) * estimatedExchangeRate) +// // ) +// // const receiveAmountValue = +// // BigInt(quote.receiveAmount.value) - exchangeAdjustedFees + +// if (receiveAmountValue <= BigInt(0)) { +// deps.logger.info( +// { fees, estimatedExchangeRate, receiveAmountValue }, +// 'Negative receive amount when calculating quote amount' +// ) +// throw QuoteError.NonPositiveReceiveAmount +// } + +// if (receiveAmountValue > maxReceiveAmountValue) { +// throw QuoteError.InvalidAmount +// } + +// deps.logger.debug( +// { +// 'quote.receiveAmount.value': quote.receiveAmount.value, +// debitAmountValue: quote.debitAmount.value, +// receiveAmountValue, +// fees +// // exchangeAdjustedFees +// }, +// 'Calculated fixed-send quote amount with fees' +// ) + +// // what is the debit amount that satisfies the receiveAmount after fees +// // - in my case, debitAmount of 500 includes fees. (answer is 390 in this case) + +// return { +// debitAmountValue: quote.debitAmount.value, +// receiveAmountValue +// } +// } + /** * Calculate fixed-delivery quote amounts: receiveAmount is locked, * add fees to the the debitAmount. @@ -368,7 +438,9 @@ async function finalizeQuote( const patchOptions = { sourceAmount: maxReceiveAmountValue - ? receiveAmountValue + ? // TODO: change fixed send to return the sourceAmount if I can get new calc working + quote.debitAmount.value - + (quote.fee?.calculate(quote.debitAmount.value) ?? 0n) : quote.debitAmount.value, debitAmountValue, receiveAmountValue, diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 1efed196e4..3d636bc189 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -136,7 +136,8 @@ async function getQuote( return { receiver: options.receiver, walletAddress: options.walletAddress, - estimatedExchangeRate: Number(receiveAmountValue / debitAmountValue), + estimatedExchangeRate: + Number(receiveAmountValue) / Number(debitAmountValue), debitAmount: { value: debitAmountValue, assetCode: options.walletAddress.asset.code, From 823c512748410c76d0147ecb9ac5e73072d16126 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 16 Sep 2024 10:22:57 -0400 Subject: [PATCH 15/64] fix: lint error --- packages/backend/src/payment-method/local/service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 3d636bc189..50fa78523b 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -23,7 +23,6 @@ import { InsufficientLiquidityError } from 'ilp-packet/dist/errors' import { IncomingPaymentService } from '../../open_payments/payment/incoming/service' import { IncomingPaymentState } from '../../open_payments/payment/incoming/model' import { FeeService } from '../../fee/service' -import { Fee } from '../../fee/model' export interface LocalPaymentService extends PaymentMethodService {} @@ -207,13 +206,10 @@ async function pay( } } - console.log('pay fee', outgoingPayment.quote.fee) - console.log('pay feeid', outgoingPayment.quote.feeId) - // let feeAmount: number | null // baseDebitAmount excludes fees - let sourceAmount = outgoingPayment.quote.sourceAmount //finalDebitAmount + const sourceAmount = outgoingPayment.quote.sourceAmount //finalDebitAmount console.log({ finalDebitAmount, sourceAmount, finalReceiveAmount }) From 2e9f043e4b7e47ab6673999efb3257eabfcc604c Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 16 Sep 2024 11:32:16 -0400 Subject: [PATCH 16/64] chore: rm logs --- packages/backend/src/open_payments/quote/service.ts | 9 --------- .../backend/src/payment-method/local/service.test.ts | 11 ----------- packages/backend/src/payment-method/local/service.ts | 2 -- 3 files changed, 22 deletions(-) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 8c626c3b75..9c40ec2a43 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -128,8 +128,6 @@ async function createQuote( FeeType.Sending ) - console.log({ sendingFee }) - const graph: PartialModelGraph = { walletAddressId: options.walletAddressId, assetId: walletAddress.assetId, @@ -429,13 +427,6 @@ async function finalizeQuote( ? calculateFixedSendQuoteAmounts(deps, quote, maxReceiveAmountValue) : calculateFixedDeliveryQuoteAmounts(deps, quote) - console.log({ - debitAmountValue, - receiveAmountValue, - 'quote.debitAmount.value': quote.debitAmount.value, - maxReceiveAmountValue - }) - const patchOptions = { sourceAmount: maxReceiveAmountValue ? // TODO: change fixed send to return the sourceAmount if I can get new calc working diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 8807dff7ab..d3226a2420 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -385,8 +385,6 @@ describe('LocalPaymentService', (): void => { const quote = await localPaymentService.getQuote(options) - console.log('quote gotten in test', { quote }) - expect(quote).toMatchObject({ debitAmount: { assetCode: sendingWalletAddress.asset.code, @@ -524,8 +522,6 @@ describe('LocalPaymentService', (): void => { finalReceiveAmount: 100n }) - console.log({ payResponse }) - expect(payResponse).toBe(undefined) await validateBalances(outgoingPayment, incomingPayment, { @@ -568,11 +564,6 @@ describe('LocalPaymentService', (): void => { } }) - console.log( - 'do the amounts/quote match the expected ones in the test code comment?', - { outgoingPayment } - ) - expect(true).toBe(false) const payResponse = await localPaymentService.pay({ @@ -582,8 +573,6 @@ describe('LocalPaymentService', (): void => { finalReceiveAmount: 100n }) - console.log({ payResponse }) - expect(payResponse).toBe(undefined) await validateBalances(outgoingPayment, incomingPayment, { diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 50fa78523b..78b76db8eb 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -211,8 +211,6 @@ async function pay( // baseDebitAmount excludes fees const sourceAmount = outgoingPayment.quote.sourceAmount //finalDebitAmount - console.log({ finalDebitAmount, sourceAmount, finalReceiveAmount }) - // if (outgoingPayment.quote.feeId) { // const fee = await deps.feeService.getById(outgoingPayment.quote.feeId) From 3fb02e5756bafd9457d526e7098b10ee18bf3223 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:59:26 -0400 Subject: [PATCH 17/64] fix: quote service test --- .../src/open_payments/quote/service.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 4c4db21500..bb6e26b4a3 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -35,6 +35,7 @@ import { PaymentMethodHandlerError, PaymentMethodHandlerErrorCode } from '../../payment-method/handler/errors' +import { Receiver } from '../receiver/model' describe('QuoteService', (): void => { let deps: IocContract @@ -46,6 +47,12 @@ describe('QuoteService', (): void => { let sendingWalletAddress: MockWalletAddress let receivingWalletAddress: MockWalletAddress let config: IAppConfig + let receiverGet: typeof receiverService.get + let receiverGetSpy: jest.SpyInstance< + Promise, + [url: string], + any + > const asset: AssetOptions = { scale: 9, @@ -93,6 +100,21 @@ describe('QuoteService', (): void => { assetId: destinationAssetId, mockServerPort: appContainer.openPaymentsPort }) + + // Make receivers non-local by default + receiverGet = receiverService.get + receiverGetSpy = jest + .spyOn(receiverService, 'get') + .mockImplementation(async (url: string) => { + // call original instead of receiverService.get to avoid infinite loop + const receiver = await receiverGet.call(receiverService, url) + if (receiver) { + // "as any" to circumvent "readonly" check (compile time only) to allow overriding "isLocal" here + ;(receiver.isLocal as any) = false + return receiver + } + return undefined + }) }) afterEach(async (): Promise => { @@ -763,6 +785,9 @@ describe('QuoteService', (): void => { }) describe('Local Receiver', (): void => { + beforeEach(() => { + receiverGetSpy.mockRestore() + }) test('Local receiver uses local payment method', async () => { const incomingPayment = await createIncomingPayment(deps, { walletAddressId: receivingWalletAddress.id, From 569f029045ea123a5ce2433dbf8f7550f18b5f56 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:12:45 -0400 Subject: [PATCH 18/64] fix: lint errors --- .../payment/outgoing/service.test.ts | 20 +++++++++++++++++++ .../src/open_payments/quote/service.test.ts | 2 ++ .../src/payment-method/local/service.ts | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 4d5ac90f8b..2b83d75dba 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -53,6 +53,7 @@ import { withConfigOverride } from '../../../tests/helpers' import { TelemetryService } from '../../../telemetry/service' import { getPageTests } from '../../../shared/baseModel.test' import { Pagination, SortOrder } from '../../../shared/baseModel' +import { ReceiverService } from '../../receiver/service' describe('OutgoingPaymentService', (): void => { let deps: IocContract @@ -72,6 +73,8 @@ describe('OutgoingPaymentService', (): void => { let amtDelivered: bigint let trx: Knex.Transaction let config: IAppConfig + let receiverService: ReceiverService + let receiverGet: typeof receiverService.get const asset: AssetOptions = { scale: 9, @@ -261,6 +264,7 @@ describe('OutgoingPaymentService', (): void => { telemetryService = (await deps.use('telemetry'))! config = await deps.use('config') knex = appContainer.knex + receiverService = await deps.use('receiverService') }) beforeEach(async (): Promise => { @@ -290,6 +294,22 @@ describe('OutgoingPaymentService', (): void => { receiver = incomingPayment.getUrl(receiverWalletAddress) amtDelivered = BigInt(0) + + // Make receivers non-local by default + receiverGet = receiverService.get + jest + .spyOn(receiverService, 'get') + .mockImplementation(async (url: string) => { + // call original instead of receiverService.get to avoid infinite loop + const receiver = await receiverGet.call(receiverService, url) + if (receiver) { + // "as any" to circumvent "readonly" check (compile time only) to allow overriding "isLocal" here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(receiver.isLocal as any) = false + return receiver + } + return undefined + }) }) afterEach(async (): Promise => { diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index bb6e26b4a3..086ee5bf58 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -51,6 +51,7 @@ describe('QuoteService', (): void => { let receiverGetSpy: jest.SpyInstance< Promise, [url: string], + // eslint-disable-next-line @typescript-eslint/no-explicit-any any > @@ -110,6 +111,7 @@ describe('QuoteService', (): void => { const receiver = await receiverGet.call(receiverService, url) if (receiver) { // "as any" to circumvent "readonly" check (compile time only) to allow overriding "isLocal" here + // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(receiver.isLocal as any) = false return receiver } diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 78b76db8eb..1bb3da62f4 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -155,8 +155,8 @@ async function pay( deps: ServiceDependencies, options: PayOptions ): Promise { - const { outgoingPayment, receiver, finalDebitAmount, finalReceiveAmount } = - options + // TODO: use finalDebitAmount instead of sourceAmount? pass sourceAmount in as finalDebitAmount? + const { outgoingPayment, receiver, finalReceiveAmount } = options if (!outgoingPayment.quote.sourceAmount) { // TODO: handle this better. perhaps sourceAmount should not be nullable? // If throwing an error, follow existing patterns From 318294437a56ed41bd2aeeac1b0f840e6d2652d5 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:15:44 -0400 Subject: [PATCH 19/64] refactor(backend): split migrations --- ...6181643_require_estimated_exchange_rate.js | 33 +++++++++ .../20240916181659_add_ilp_quote_details.js | 69 ++++++++++++++++++ .../20240916182716_drop_quote_ilp_fields.js | 73 +++++++++++++++++++ .../20240916185330_add_quote_source_amount.js | 19 +++++ 4 files changed, 194 insertions(+) create mode 100644 packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js create mode 100644 packages/backend/migrations/20240916181659_add_ilp_quote_details.js create mode 100644 packages/backend/migrations/20240916182716_drop_quote_ilp_fields.js create mode 100644 packages/backend/migrations/20240916185330_add_quote_source_amount.js diff --git a/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js b/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js new file mode 100644 index 0000000000..23fce59a3d --- /dev/null +++ b/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js @@ -0,0 +1,33 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ + +exports.up = function (knex) { + return knex("quotes") + .whereNull("estimatedExchangeRate") + .update({ + // TODO: vet this more... looks like the low* fields were (inadvertently?) + // made nullable when updating from bigint to decimal. If they are null + // anywhere then this wont work. + estimatedExchangeRate: knex.raw("?? / ??", [ + "lowEstimatedExchangeRateNumerator", + "lowEstimatedExchangeRateDenominator", + ]), + }) + .then(() => { + return knex.schema.table("quotes", (table) => { + table.decimal("estimatedExchangeRate", 20, 10).notNullable().alter(); + }); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.table("quotes", (table) => { + table.decimal("estimatedExchangeRate", 20, 10).nullable().alter(); + }); +}; diff --git a/packages/backend/migrations/20240916181659_add_ilp_quote_details.js b/packages/backend/migrations/20240916181659_add_ilp_quote_details.js new file mode 100644 index 0000000000..fec3636c39 --- /dev/null +++ b/packages/backend/migrations/20240916181659_add_ilp_quote_details.js @@ -0,0 +1,69 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return ( + knex.schema + // Create new table with columns from "quotes" to migrate + .createTable('ilpQuoteDetails', function (table) { + table.uuid('id').notNullable().primary() + table.uuid('quoteId').notNullable().unique() + table.foreign('quoteId').references('quotes.id') + + table.bigInteger('maxPacketAmount').notNullable() + table.decimal('minExchangeRateNumerator', 64, 0).notNullable() + table.decimal('minExchangeRateDenominator', 64, 0).notNullable() + table.decimal('lowEstimatedExchangeRateNumerator', 64, 0).notNullable() + table + .decimal('lowEstimatedExchangeRateDenominator', 64, 0) + .notNullable() + table.decimal('highEstimatedExchangeRateNumerator', 64, 0).notNullable() + table + .decimal('highEstimatedExchangeRateDenominator', 64, 0) + .notNullable() + + table.timestamp('createdAt').defaultTo(knex.fn.now()) + table.timestamp('updatedAt').defaultTo(knex.fn.now()) + }) + .then(() => { + return knex.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`) + }) + .then(() => { + return knex.raw(` + INSERT INTO "ilpQuoteDetails" ( + id, + "quoteId", + "maxPacketAmount", + "minExchangeRateNumerator", + "minExchangeRateDenominator", + "lowEstimatedExchangeRateNumerator", + "lowEstimatedExchangeRateDenominator", + "highEstimatedExchangeRateNumerator", + "highEstimatedExchangeRateDenominator" + ) + SELECT + uuid_generate_v4(), + id AS "quoteId", + "maxPacketAmount", + "minExchangeRateNumerator", + "minExchangeRateDenominator", + "lowEstimatedExchangeRateNumerator", + "lowEstimatedExchangeRateDenominator", + "highEstimatedExchangeRateNumerator", + "highEstimatedExchangeRateDenominator" + FROM "quotes"; + `) + }) + ) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.raw(`DROP EXTENSION IF EXISTS "uuid-ossp"`).then(() => { + return knex.schema.dropTableIfExists('ilpQuoteDetails') + }) +} diff --git a/packages/backend/migrations/20240916182716_drop_quote_ilp_fields.js b/packages/backend/migrations/20240916182716_drop_quote_ilp_fields.js new file mode 100644 index 0000000000..839371d366 --- /dev/null +++ b/packages/backend/migrations/20240916182716_drop_quote_ilp_fields.js @@ -0,0 +1,73 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.alterTable('quotes', function (table) { + table.dropColumn('maxPacketAmount') + table.dropColumn('minExchangeRateNumerator') + table.dropColumn('minExchangeRateDenominator') + table.dropColumn('lowEstimatedExchangeRateNumerator') + table.dropColumn('lowEstimatedExchangeRateDenominator') + table.dropColumn('highEstimatedExchangeRateNumerator') + table.dropColumn('highEstimatedExchangeRateDenominator') + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema + .alterTable('quotes', function (table) { + // restore columns without not null constraint + table.bigInteger('maxPacketAmount') + table.decimal('minExchangeRateNumerator', 64, 0) + table.decimal('minExchangeRateDenominator', 64, 0) + table.decimal('lowEstimatedExchangeRateNumerator', 64, 0) + table.decimal('lowEstimatedExchangeRateDenominator', 64, 0) + table.decimal('highEstimatedExchangeRateNumerator', 64, 0) + table.decimal('highEstimatedExchangeRateDenominator', 64, 0) + }) + .then(() => { + // Migrate data back to quotes table from ilpQuote + return knex.raw(` + UPDATE "quotes" + SET + "maxPacketAmount" = "ilpQuoteDetails"."maxPacketAmount", + "minExchangeRateNumerator" = "ilpQuoteDetails"."minExchangeRateNumerator", + "minExchangeRateDenominator" = "ilpQuoteDetails"."minExchangeRateDenominator", + "lowEstimatedExchangeRateNumerator" = "ilpQuoteDetails"."lowEstimatedExchangeRateNumerator", + "lowEstimatedExchangeRateDenominator" = "ilpQuoteDetails"."lowEstimatedExchangeRateDenominator", + "highEstimatedExchangeRateNumerator" = "ilpQuoteDetails"."highEstimatedExchangeRateNumerator", + "highEstimatedExchangeRateDenominator" = "ilpQuoteDetails"."highEstimatedExchangeRateDenominator" + FROM "ilpQuoteDetails" + WHERE "quotes"."id" = "ilpQuoteDetails"."quoteId" + `) + }) + // .then(() => { + // // Apply the not null constraints after data insertion + // return knex.schema.alterTable('quotes', function (table) { + // table.bigInteger('maxPacketAmount').notNullable().alter() + // table.decimal('minExchangeRateNumerator', 64, 0).notNullable().alter() + // table.decimal('minExchangeRateDenominator', 64, 0).notNullable().alter() + // table + // .decimal('lowEstimatedExchangeRateNumerator', 64, 0) + // .notNullable() + // .alter() + // table + // .decimal('lowEstimatedExchangeRateDenominator', 64, 0) + // .notNullable() + // .alter() + // table + // .decimal('highEstimatedExchangeRateNumerator', 64, 0) + // .notNullable() + // .alter() + // table + // .decimal('highEstimatedExchangeRateDenominator', 64, 0) + // .notNullable() + // .alter() + // }) + // }) +} diff --git a/packages/backend/migrations/20240916185330_add_quote_source_amount.js b/packages/backend/migrations/20240916185330_add_quote_source_amount.js new file mode 100644 index 0000000000..f86094ad9a --- /dev/null +++ b/packages/backend/migrations/20240916185330_add_quote_source_amount.js @@ -0,0 +1,19 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.alterTable('quotes', function (table) { + table.bigInteger('sourceAmount').nullable() + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.alterTable('quotes', function (table) { + table.dropColumn('sourceAmount') + }) +} From 366518800b0769ca3f8b373893da27339aa824ca Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:16:17 -0400 Subject: [PATCH 20/64] refactor(backend): rm migration that was split into many --- ...40809171654_add_ilp_quote_details_table.js | 148 ------------------ 1 file changed, 148 deletions(-) delete mode 100644 packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js diff --git a/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js b/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js deleted file mode 100644 index 8b6fa6f680..0000000000 --- a/packages/backend/migrations/20240809171654_add_ilp_quote_details_table.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ -exports.up = function (knex) { - return ( - knex.schema - // Create new table with columns from "quotes" to migrate - .createTable('ilpQuoteDetails', function (table) { - table.uuid('id').notNullable().primary() - table.uuid('quoteId').notNullable().unique() - table.foreign('quoteId').references('quotes.id') - - table.bigInteger('maxPacketAmount').notNullable() - table.decimal('minExchangeRateNumerator', 64, 0).notNullable() - table.decimal('minExchangeRateDenominator', 64, 0).notNullable() - table.decimal('lowEstimatedExchangeRateNumerator', 64, 0).notNullable() - table - .decimal('lowEstimatedExchangeRateDenominator', 64, 0) - .notNullable() - table.decimal('highEstimatedExchangeRateNumerator', 64, 0).notNullable() - table - .decimal('highEstimatedExchangeRateDenominator', 64, 0) - .notNullable() - - table.timestamp('createdAt').defaultTo(knex.fn.now()) - table.timestamp('updatedAt').defaultTo(knex.fn.now()) - }) - .then(() => { - // Enable uuid_generate_v4 - return knex.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`) - }) - .then(() => { - // Migrate data from quotes to ilpQuoteDetails. - return knex.raw(` - INSERT INTO "ilpQuoteDetails" ( - id, - "quoteId", - "maxPacketAmount", - "minExchangeRateNumerator", - "minExchangeRateDenominator", - "lowEstimatedExchangeRateNumerator", - "lowEstimatedExchangeRateDenominator", - "highEstimatedExchangeRateNumerator", - "highEstimatedExchangeRateDenominator" - ) - SELECT - uuid_generate_v4(), - id AS "quoteId", - "maxPacketAmount", - "minExchangeRateNumerator", - "minExchangeRateDenominator", - "lowEstimatedExchangeRateNumerator", - "lowEstimatedExchangeRateDenominator", - "highEstimatedExchangeRateNumerator", - "highEstimatedExchangeRateDenominator" - FROM "quotes"; - `) - }) - .then(() => { - // TODO: test this more thoroughly. - // Might need to seed in migration preceeding this? - // Cant simply withold htis migration. Application code will fail when trying - // to insert ... - return knex('quotes') - .whereNull('estimatedExchangeRate') - .update({ - estimatedExchangeRate: knex.raw('?? / ??', [ - 'lowEstimatedExchangeRateNumerator', - 'lowEstimatedExchangeRateDenominator' - ]) - }) - }) - .then(() => { - return knex.schema.alterTable('quotes', function (table) { - table.dropColumn('maxPacketAmount') - table.dropColumn('minExchangeRateNumerator') - table.dropColumn('minExchangeRateDenominator') - table.dropColumn('lowEstimatedExchangeRateNumerator') - table.dropColumn('lowEstimatedExchangeRateDenominator') - table.dropColumn('highEstimatedExchangeRateNumerator') - table.dropColumn('highEstimatedExchangeRateDenominator') - table.bigInteger('sourceAmount').nullable() - }) - }) - ) -} - -/** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ -exports.down = function (knex) { - return knex.schema - .alterTable('quotes', function (table) { - // restore columns without not null constraint - table.bigInteger('maxPacketAmount') - table.decimal('minExchangeRateNumerator', 64, 0) - table.decimal('minExchangeRateDenominator', 64, 0) - table.decimal('lowEstimatedExchangeRateNumerator', 64, 0) - table.decimal('lowEstimatedExchangeRateDenominator', 64, 0) - table.decimal('highEstimatedExchangeRateNumerator', 64, 0) - table.decimal('highEstimatedExchangeRateDenominator', 64, 0) - }) - .then(() => { - // Migrate data back to quotes table from ilpQuote - return knex.raw(` - UPDATE "quotes" - SET - "maxPacketAmount" = "ilpQuoteDetails"."maxPacketAmount", - "minExchangeRateNumerator" = "ilpQuoteDetails"."minExchangeRateNumerator", - "minExchangeRateDenominator" = "ilpQuoteDetails"."minExchangeRateDenominator", - "lowEstimatedExchangeRateNumerator" = "ilpQuoteDetails"."lowEstimatedExchangeRateNumerator", - "lowEstimatedExchangeRateDenominator" = "ilpQuoteDetails"."lowEstimatedExchangeRateDenominator", - "highEstimatedExchangeRateNumerator" = "ilpQuoteDetails"."highEstimatedExchangeRateNumerator", - "highEstimatedExchangeRateDenominator" = "ilpQuoteDetails"."highEstimatedExchangeRateDenominator" - FROM "ilpQuoteDetails" - WHERE "quotes"."id" = "ilpQuoteDetails"."quoteId" - `) - }) - .then(() => { - // Apply the not null constraints after data insertion - return knex.schema.alterTable('quotes', function (table) { - table.bigInteger('maxPacketAmount').notNullable().alter() - table.decimal('minExchangeRateNumerator', 64, 0).notNullable().alter() - table.decimal('minExchangeRateDenominator', 64, 0).notNullable().alter() - table - .decimal('lowEstimatedExchangeRateNumerator', 64, 0) - .notNullable() - .alter() - table - .decimal('lowEstimatedExchangeRateDenominator', 64, 0) - .notNullable() - .alter() - table - .decimal('highEstimatedExchangeRateNumerator', 64, 0) - .notNullable() - .alter() - table - .decimal('highEstimatedExchangeRateDenominator', 64, 0) - .notNullable() - .alter() - }) - }) - .then(() => { - return knex.schema.dropTableIfExists('ilpQuoteDetails') - }) -} From 5b8bad3dada965f1e4eefe404528aa6f4b04814e Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:21:45 -0400 Subject: [PATCH 21/64] WIP bruno requests for testing --- .../Create Outgoing Payment.bru | 72 +++++++++++++++++ .../Create Quote.bru | 71 +++++++++++++++++ ...ate Receiver (remote Incoming Payment).bru | 78 +++++++++++++++++++ .../Get Outgoing Payment.bru | 56 +++++++++++++ .../Create Outgoing Payment.bru | 72 +++++++++++++++++ .../Create Quote.bru | 71 +++++++++++++++++ ...ate Receiver (remote Incoming Payment).bru | 78 +++++++++++++++++++ .../Get Outgoing Payment.bru | 56 +++++++++++++ .../Create Outgoing Payment.bru | 72 +++++++++++++++++ .../Create Quote.bru | 70 +++++++++++++++++ ...ate Receiver (remote Incoming Payment).bru | 78 +++++++++++++++++++ .../Get Outgoing Payment.bru | 56 +++++++++++++ .../Create Outgoing Payment.bru | 72 +++++++++++++++++ .../Create Quote.bru | 71 +++++++++++++++++ ...ate Receiver -remote Incoming Payment-.bru | 73 +++++++++++++++++ .../Get Outgoing Payment.bru | 57 ++++++++++++++ .../Create Outgoing Payment.bru | 72 +++++++++++++++++ .../Create Quote.bru | 66 ++++++++++++++++ ...ate Receiver -remote Incoming Payment-.bru | 73 +++++++++++++++++ .../Get Outgoing Payment.bru | 57 ++++++++++++++ .../Create Outgoing Payment.bru | 72 +++++++++++++++++ .../Create Quote.bru | 71 +++++++++++++++++ ...ate Receiver -remote Incoming Payment-.bru | 73 +++++++++++++++++ .../Get Outgoing Payment.bru | 57 ++++++++++++++ .../Continuation Request.bru | 33 ++++++++ .../Create Incoming Payment.bru | 55 +++++++++++++ .../Create Outgoing Payment.bru | 48 ++++++++++++ .../Open Payments (local)/Create Quote.bru | 47 +++++++++++ .../Get Outgoing Payment.bru | 29 +++++++ .../Get receiver wallet address.bru | 50 ++++++++++++ .../Get sender wallet address.bru | 50 ++++++++++++ .../Grant Request Incoming Payment.bru | 45 +++++++++++ .../Grant Request Outgoing Payment.bru | 56 +++++++++++++ .../Grant Request Quote.bru | 46 +++++++++++ 34 files changed, 2103 insertions(+) create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Quote.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Receiver (remote Incoming Payment).bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Get Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Quote.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Receiver (remote Incoming Payment).bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Get Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Get Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Quote.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Receiver -remote Incoming Payment-.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Get Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Quote.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Receiver -remote Incoming Payment-.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Get Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Quote.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Receiver -remote Incoming Payment-.bru create mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Get Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Continuation Request.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Create Incoming Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Create Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Create Quote.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Get Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Get sender wallet address.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Outgoing Payment.bru create mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Quote.bru diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Outgoing Payment.bru new file mode 100644 index 0000000000..4bfde2f666 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Outgoing Payment.bru @@ -0,0 +1,72 @@ +meta { + name: Create Outgoing Payment + type: graphql + seq: 3 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { + createOutgoingPayment(input: $input) { + payment { + createdAt + error + metadata + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "quoteId": "{{quoteId}}" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); + } +} + +tests { + test("Outgoing Payment id is string", function() { + expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Quote.bru new file mode 100644 index 0000000000..46bd3ffc1d --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Quote.bru @@ -0,0 +1,71 @@ +meta { + name: Create Quote + type: graphql + seq: 2 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateQuote($input: CreateQuoteInput!) { + createQuote(input: $input) { + quote { + createdAt + expiresAt + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "receiver": "{{receiverId}}", + "debitAmount": { + "assetCode": "USD", + "assetScale": 2, + "value": 500 + } + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.loadWalletAddressIdsIntoVariables(); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("quoteId", body.data.createQuote.quote.id); + } +} + +tests { + test("Quote id is string", function() { + expect(bru.getEnvVar("quoteId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Receiver (remote Incoming Payment).bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Receiver (remote Incoming Payment).bru new file mode 100644 index 0000000000..c3b3ae34bd --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Receiver (remote Incoming Payment).bru @@ -0,0 +1,78 @@ +meta { + name: Create Receiver (remote Incoming Payment) + type: graphql + seq: 1 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateReceiver($input: CreateReceiverInput!) { + createReceiver(input: $input) { + receiver { + completed + createdAt + expiresAt + metadata + id + incomingAmount { + assetCode + assetScale + value + } + walletAddressUrl + receivedAmount { + assetCode + assetScale + value + } + updatedAt + } + } + } +} + +body:graphql:vars { + { + "input": { + "metadata": { + "description": "cross-currency" + }, + // "incomingAmount": { + // "assetCode": "EUR", + // "assetScale": 2, + // "value": 500 + // }, + "walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/lrossi" + } + } +} + +vars:pre-request { + signatureVersion: {{apiSignatureVersion}} + signatureSecret: {{apiSignatureSecret}} +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); + } +} + +tests { + test("Receiver id is string", function() { + expect(bru.getEnvVar("receiverId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Get Outgoing Payment.bru new file mode 100644 index 0000000000..3216293fbd --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Get Outgoing Payment.bru @@ -0,0 +1,56 @@ +meta { + name: Get Outgoing Payment + type: graphql + seq: 4 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetOutgoingPayment($id: String!) { + outgoingPayment(id: $id) { + createdAt + error + metadata + id + walletAddressId + quote { + id + } + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } +} + +body:graphql:vars { + { + "id": "{{outgoingPaymentId}}" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Outgoing Payment.bru new file mode 100644 index 0000000000..4bfde2f666 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Outgoing Payment.bru @@ -0,0 +1,72 @@ +meta { + name: Create Outgoing Payment + type: graphql + seq: 3 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { + createOutgoingPayment(input: $input) { + payment { + createdAt + error + metadata + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "quoteId": "{{quoteId}}" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); + } +} + +tests { + test("Outgoing Payment id is string", function() { + expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Quote.bru new file mode 100644 index 0000000000..78a99f8225 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Quote.bru @@ -0,0 +1,71 @@ +meta { + name: Create Quote + type: graphql + seq: 2 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateQuote($input: CreateQuoteInput!) { + createQuote(input: $input) { + quote { + createdAt + expiresAt + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "receiver": "{{receiverId}}", + "debitAmount": { + "assetCode": "EUR", + "assetScale": 2, + "value": 500 + } + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.loadWalletAddressIdsIntoVariables(); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("quoteId", body.data.createQuote.quote.id); + } +} + +tests { + test("Quote id is string", function() { + expect(bru.getEnvVar("quoteId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Receiver (remote Incoming Payment).bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Receiver (remote Incoming Payment).bru new file mode 100644 index 0000000000..4fae1c904b --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Receiver (remote Incoming Payment).bru @@ -0,0 +1,78 @@ +meta { + name: Create Receiver (remote Incoming Payment) + type: graphql + seq: 1 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateReceiver($input: CreateReceiverInput!) { + createReceiver(input: $input) { + receiver { + completed + createdAt + expiresAt + metadata + id + incomingAmount { + assetCode + assetScale + value + } + walletAddressUrl + receivedAmount { + assetCode + assetScale + value + } + updatedAt + } + } + } +} + +body:graphql:vars { + { + "input": { + "metadata": { + "description": "cross-currency" + }, + "incomingAmount": { + "assetCode": "EUR", + "assetScale": 2, + "value": 500 + }, + "walletAddressUrl": "https://happy-life-bank-backend/accounts/lars" + } + } +} + +vars:pre-request { + signatureVersion: {{apiSignatureVersion}} + signatureSecret: {{apiSignatureSecret}} +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); + } +} + +tests { + test("Receiver id is string", function() { + expect(bru.getEnvVar("receiverId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Get Outgoing Payment.bru new file mode 100644 index 0000000000..3216293fbd --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Get Outgoing Payment.bru @@ -0,0 +1,56 @@ +meta { + name: Get Outgoing Payment + type: graphql + seq: 4 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetOutgoingPayment($id: String!) { + outgoingPayment(id: $id) { + createdAt + error + metadata + id + walletAddressId + quote { + id + } + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } +} + +body:graphql:vars { + { + "id": "{{outgoingPaymentId}}" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Outgoing Payment.bru new file mode 100644 index 0000000000..4bfde2f666 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Outgoing Payment.bru @@ -0,0 +1,72 @@ +meta { + name: Create Outgoing Payment + type: graphql + seq: 3 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { + createOutgoingPayment(input: $input) { + payment { + createdAt + error + metadata + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "quoteId": "{{quoteId}}" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); + } +} + +tests { + test("Outgoing Payment id is string", function() { + expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru new file mode 100644 index 0000000000..73b598a469 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru @@ -0,0 +1,70 @@ +meta { + name: Create Quote + type: graphql + seq: 2 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateQuote($input: CreateQuoteInput!) { + createQuote(input: $input) { + quote { + createdAt + expiresAt + highEstimatedExchangeRate + id + lowEstimatedExchangeRate + maxPacketAmount + minExchangeRate + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "receiver": "{{receiverId}}" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.loadWalletAddressIdsIntoVariables(); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("quoteId", body.data.createQuote.quote.id); + } +} + +tests { + test("Quote id is string", function() { + expect(bru.getEnvVar("quoteId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru new file mode 100644 index 0000000000..03bc186665 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru @@ -0,0 +1,78 @@ +meta { + name: Create Receiver (remote Incoming Payment) + type: graphql + seq: 1 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateReceiver($input: CreateReceiverInput!) { + createReceiver(input: $input) { + receiver { + completed + createdAt + expiresAt + metadata + id + incomingAmount { + assetCode + assetScale + value + } + walletAddressUrl + receivedAmount { + assetCode + assetScale + value + } + updatedAt + } + } + } +} + +body:graphql:vars { + { + "input": { + "metadata": { + "description": "cross-currency" + }, + "incomingAmount": { + "assetCode": "EUR", + "assetScale": 2, + "value": 500 + }, + "walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/bhamchest" + } + } +} + +vars:pre-request { + signatureVersion: {{apiSignatureVersion}} + signatureSecret: {{apiSignatureSecret}} +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); + } +} + +tests { + test("Receiver id is string", function() { + expect(bru.getEnvVar("receiverId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Get Outgoing Payment.bru new file mode 100644 index 0000000000..3216293fbd --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Get Outgoing Payment.bru @@ -0,0 +1,56 @@ +meta { + name: Get Outgoing Payment + type: graphql + seq: 4 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetOutgoingPayment($id: String!) { + outgoingPayment(id: $id) { + createdAt + error + metadata + id + walletAddressId + quote { + id + } + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } +} + +body:graphql:vars { + { + "id": "{{outgoingPaymentId}}" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Outgoing Payment.bru new file mode 100644 index 0000000000..4bfde2f666 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Outgoing Payment.bru @@ -0,0 +1,72 @@ +meta { + name: Create Outgoing Payment + type: graphql + seq: 3 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { + createOutgoingPayment(input: $input) { + payment { + createdAt + error + metadata + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "quoteId": "{{quoteId}}" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); + } +} + +tests { + test("Outgoing Payment id is string", function() { + expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Quote.bru new file mode 100644 index 0000000000..26e82d0288 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Quote.bru @@ -0,0 +1,71 @@ +meta { + name: Create Quote + type: graphql + seq: 2 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateQuote($input: CreateQuoteInput!) { + createQuote(input: $input) { + quote { + createdAt + expiresAt + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "receiver": "{{receiverId}}", + "debitAmount": { + "assetCode": "USD", + "assetScale": 2, + "value": 500 + } + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.loadWalletAddressIdsIntoVariables(); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("quoteId", body.data.createQuote.quote.id); + } +} + +tests { + test("Quote id is string", function() { + expect(bru.getEnvVar("quoteId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Receiver -remote Incoming Payment-.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Receiver -remote Incoming Payment-.bru new file mode 100644 index 0000000000..20c36a034d --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Receiver -remote Incoming Payment-.bru @@ -0,0 +1,73 @@ +meta { + name: Create Receiver -remote Incoming Payment- + type: graphql + seq: 1 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateReceiver($input: CreateReceiverInput!) { + createReceiver(input: $input) { + receiver { + completed + createdAt + expiresAt + metadata + id + incomingAmount { + assetCode + assetScale + value + } + walletAddressUrl + receivedAmount { + assetCode + assetScale + value + } + updatedAt + } + } + } +} + +body:graphql:vars { + { + "input": { + "metadata": { + "description": "For lunch!" + }, + "incomingAmount": { + "assetCode": "USD", + "assetScale": 2, + "value": 500 + }, + "walletAddressUrl": "https://happy-life-bank-backend/accounts/pfry" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); + } +} + +tests { + test("Receiver id is string", function() { + expect(bru.getEnvVar("receiverId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Get Outgoing Payment.bru new file mode 100644 index 0000000000..cfca035df3 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Get Outgoing Payment.bru @@ -0,0 +1,57 @@ +meta { + name: Get Outgoing Payment + type: graphql + seq: 4 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetOutgoingPayment($id: String!) { + outgoingPayment(id: $id) { + createdAt + error + metadata + id + grantId + walletAddressId + quote { + id + } + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } +} + +body:graphql:vars { + { + "id": "{{outgoingPaymentId}}" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Outgoing Payment.bru new file mode 100644 index 0000000000..4bfde2f666 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Outgoing Payment.bru @@ -0,0 +1,72 @@ +meta { + name: Create Outgoing Payment + type: graphql + seq: 3 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { + createOutgoingPayment(input: $input) { + payment { + createdAt + error + metadata + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "quoteId": "{{quoteId}}" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); + } +} + +tests { + test("Outgoing Payment id is string", function() { + expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Quote.bru new file mode 100644 index 0000000000..4b293be810 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Quote.bru @@ -0,0 +1,66 @@ +meta { + name: Create Quote + type: graphql + seq: 2 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateQuote($input: CreateQuoteInput!) { + createQuote(input: $input) { + quote { + createdAt + expiresAt + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "receiver": "{{receiverId}}" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.loadWalletAddressIdsIntoVariables(); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("quoteId", body.data.createQuote.quote.id); + } +} + +tests { + test("Quote id is string", function() { + expect(bru.getEnvVar("quoteId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Receiver -remote Incoming Payment-.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Receiver -remote Incoming Payment-.bru new file mode 100644 index 0000000000..7af9c1b345 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Receiver -remote Incoming Payment-.bru @@ -0,0 +1,73 @@ +meta { + name: Create Receiver -remote Incoming Payment- + type: graphql + seq: 1 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateReceiver($input: CreateReceiverInput!) { + createReceiver(input: $input) { + receiver { + completed + createdAt + expiresAt + metadata + id + incomingAmount { + assetCode + assetScale + value + } + walletAddressUrl + receivedAmount { + assetCode + assetScale + value + } + updatedAt + } + } + } +} + +body:graphql:vars { + { + "input": { + "metadata": { + "description": "For lunch!" + }, + "incomingAmount": { + "assetCode": "USD", + "assetScale": 2, + "value": 500 + }, + "walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/bhamchest" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); + } +} + +tests { + test("Receiver id is string", function() { + expect(bru.getEnvVar("receiverId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Get Outgoing Payment.bru new file mode 100644 index 0000000000..cfca035df3 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Get Outgoing Payment.bru @@ -0,0 +1,57 @@ +meta { + name: Get Outgoing Payment + type: graphql + seq: 4 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetOutgoingPayment($id: String!) { + outgoingPayment(id: $id) { + createdAt + error + metadata + id + grantId + walletAddressId + quote { + id + } + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } +} + +body:graphql:vars { + { + "id": "{{outgoingPaymentId}}" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Outgoing Payment.bru new file mode 100644 index 0000000000..4bfde2f666 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Outgoing Payment.bru @@ -0,0 +1,72 @@ +meta { + name: Create Outgoing Payment + type: graphql + seq: 3 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { + createOutgoingPayment(input: $input) { + payment { + createdAt + error + metadata + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "quoteId": "{{quoteId}}" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); + } +} + +tests { + test("Outgoing Payment id is string", function() { + expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Quote.bru new file mode 100644 index 0000000000..108fde9f14 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Quote.bru @@ -0,0 +1,71 @@ +meta { + name: Create Quote + type: graphql + seq: 2 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateQuote($input: CreateQuoteInput!) { + createQuote(input: $input) { + quote { + createdAt + expiresAt + id + walletAddressId + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + } + } + } +} + +body:graphql:vars { + { + "input": { + "walletAddressId": "{{gfranklinWalletAddressId}}", + "receiver": "{{receiverId}}", + "debitAmount": { + "assetCode": "USD", + "assetScale": 2, + "value": 500 + } + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.loadWalletAddressIdsIntoVariables(); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("quoteId", body.data.createQuote.quote.id); + } +} + +tests { + test("Quote id is string", function() { + expect(bru.getEnvVar("quoteId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Receiver -remote Incoming Payment-.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Receiver -remote Incoming Payment-.bru new file mode 100644 index 0000000000..4738401863 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Receiver -remote Incoming Payment-.bru @@ -0,0 +1,73 @@ +meta { + name: Create Receiver -remote Incoming Payment- + type: graphql + seq: 1 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + mutation CreateReceiver($input: CreateReceiverInput!) { + createReceiver(input: $input) { + receiver { + completed + createdAt + expiresAt + metadata + id + incomingAmount { + assetCode + assetScale + value + } + walletAddressUrl + receivedAmount { + assetCode + assetScale + value + } + updatedAt + } + } + } +} + +body:graphql:vars { + { + "input": { + "metadata": { + "description": "For lunch!" + }, + // "incomingAmount": { + // "assetCode": "USD", + // "assetScale": 2, + // "value": 500 + // }, + "walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/bhamchest" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.data) { + bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); + } +} + +tests { + test("Receiver id is string", function() { + expect(bru.getEnvVar("receiverId")).to.be.a("string"); + }) +} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Get Outgoing Payment.bru new file mode 100644 index 0000000000..cfca035df3 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Get Outgoing Payment.bru @@ -0,0 +1,57 @@ +meta { + name: Get Outgoing Payment + type: graphql + seq: 4 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetOutgoingPayment($id: String!) { + outgoingPayment(id: $id) { + createdAt + error + metadata + id + grantId + walletAddressId + quote { + id + } + receiveAmount { + assetCode + assetScale + value + } + receiver + debitAmount { + assetCode + assetScale + value + } + sentAmount { + assetCode + assetScale + value + } + state + stateAttempts + } + } +} + +body:graphql:vars { + { + "id": "{{outgoingPaymentId}}" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Continuation Request.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Continuation Request.bru new file mode 100644 index 0000000000..01d39ebf3f --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Continuation Request.bru @@ -0,0 +1,33 @@ +meta { + name: Continuation Request + type: http + seq: 8 +} + +post { + url: {{senderOpenPaymentsAuthHost}}/continue/{{continueId}} + body: json + auth: none +} + +headers { + Authorization: GNAP {{continueToken}} +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.addSignatureHeaders(); +} + +script:post-response { + const scripts = require('./scripts'); + + scripts.storeTokenDetails(); +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Incoming Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Incoming Payment.bru new file mode 100644 index 0000000000..093a481bae --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Incoming Payment.bru @@ -0,0 +1,55 @@ +meta { + name: Create Incoming Payment + type: http + seq: 4 +} + +post { + url: {{receiverOpenPaymentsHost}}/incoming-payments + body: json + auth: none +} + +headers { + Authorization: GNAP {{accessToken}} +} + +body:json { + { + "walletAddress": "{{receiverWalletAddress}}", + "incomingAmount": { + "value": "100", + "assetCode": "{{receiverAssetCode}}", + "assetScale": {{receiverAssetScale}} + }, + "expiresAt": "{{tomorrow}}", + "metadata": { + "description": "Free Money!" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + bru.setEnvVar("tomorrow", (new Date(new Date().setDate(new Date().getDate() + 1))).toISOString()); + + scripts.addHostHeader(); + + await scripts.addSignatureHeaders(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.id) { + bru.setEnvVar("incomingPaymentId", body.id.split("/").pop()); + } + +} + +tests { + test("Status code is 201", function() { + expect(res.getStatus()).to.equal(201); + }); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Outgoing Payment.bru new file mode 100644 index 0000000000..6014dda378 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Outgoing Payment.bru @@ -0,0 +1,48 @@ +meta { + name: Create Outgoing Payment + type: http + seq: 9 +} + +post { + url: {{senderOpenPaymentsHost}}/outgoing-payments + body: json + auth: none +} + +headers { + Authorization: GNAP {{accessToken}} +} + +body:json { + { + "walletAddress": "{{senderWalletAddress}}", + "quoteId": "{{senderWalletAddress}}/quotes/{{quoteId}}", + "metadata": { + "description": "Free Money!" + } + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addHostHeader(); + + await scripts.addSignatureHeaders(); +} + +script:post-response { + const body = res.getBody(); + + if (body?.id) { + bru.setEnvVar("outgoingPaymentId", body.id.split("/").pop()); + } + +} + +tests { + test("Status code is 201", function() { + expect(res.getStatus()).to.equal(201); + }); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Quote.bru new file mode 100644 index 0000000000..a7708217f4 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Quote.bru @@ -0,0 +1,47 @@ +meta { + name: Create Quote + type: http + seq: 6 +} + +post { + url: {{senderOpenPaymentsHost}}/quotes + body: json + auth: none +} + +headers { + Authorization: GNAP {{accessToken}} +} + +body:json { + { + "walletAddress": "{{senderWalletAddress}}", + "receiver": "{{receiverOpenPaymentsHost}}/incoming-payments/{{incomingPaymentId}}", + "method": "ilp" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addHostHeader(); + + await scripts.addSignatureHeaders(); +} + +script:post-response { + const body = res.getBody(); + if (body?.id) { + bru.setEnvVar("quoteId", body.id.split("/").pop()); + bru.setEnvVar("quoteDebitAmount", JSON.stringify(body.debitAmount)) + bru.setEnvVar("quoteReceiveAmount", JSON.stringify(body.receiveAmount)) + } + +} + +tests { + test("Status code is 201", function() { + expect(res.getStatus()).to.equal(201); + }); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get Outgoing Payment.bru new file mode 100644 index 0000000000..4946e1b040 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get Outgoing Payment.bru @@ -0,0 +1,29 @@ +meta { + name: Get Outgoing Payment + type: http + seq: 10 +} + +get { + url: {{senderOpenPaymentsHost}}/outgoing-payments/{{outgoingPaymentId}} + body: none + auth: none +} + +headers { + Authorization: GNAP {{accessToken}} +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addHostHeader(); + + await scripts.addSignatureHeaders(); +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru new file mode 100644 index 0000000000..5b9b0277a9 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru @@ -0,0 +1,50 @@ +meta { + name: Get receiver wallet address + type: http + seq: 2 +} + +get { + url: {{receiverWalletAddress}} + body: none + auth: none +} + +headers { + Accept: application/json +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addHostHeader("receiverOpenPaymentsHost"); +} + +script:post-response { + const url = require('url') + + if (res.getStatus() !== 200) { + return + } + + const body = res.getBody() + bru.setEnvVar("receiverAssetCode", body?.assetCode) + bru.setEnvVar("receiverAssetScale", body?.assetScale) + + const authUrl = url.parse(body?.authServer) + if ( + authUrl.hostname.includes('cloud-nine-wallet') || + authUrl.hostname.includes('happy-life-bank') + ){ + const port = authUrl.hostname.includes('cloud-nine-wallet')? authUrl.port: Number(authUrl.port) + 1000 + bru.setEnvVar("receiverOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port ); + } else { + bru.setEnvVar("receiverOpenPaymentsAuthHost", body?.authServer); + } +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get sender wallet address.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get sender wallet address.bru new file mode 100644 index 0000000000..9665a40e32 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get sender wallet address.bru @@ -0,0 +1,50 @@ +meta { + name: Get sender wallet address + type: http + seq: 1 +} + +get { + url: {{senderWalletAddress}} + body: none + auth: none +} + +headers { + Accept: application/json +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addHostHeader("senderOpenPaymentsHost"); +} + +script:post-response { + const url = require('url') + + if (res.getStatus() !== 200) { + return + } + + const body = res.getBody() + bru.setEnvVar("senderAssetCode", body?.assetCode) + bru.setEnvVar("senderAssetScale", body?.assetScale) + + const authUrl = url.parse(body?.authServer) + if ( + authUrl.hostname.includes('cloud-nine-wallet') || + authUrl.hostname.includes('happy-life-bank') + ){ + const port = authUrl.hostname.includes('cloud-nine-wallet')? authUrl.port: Number(authUrl.port) + 1000 + bru.setEnvVar("senderOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port ); + } else { + bru.setEnvVar("senderOpenPaymentsAuthHost", body?.authServer); + } +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru new file mode 100644 index 0000000000..6335a518af --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru @@ -0,0 +1,45 @@ +meta { + name: Grant Request Incoming Payment + type: http + seq: 3 +} + +post { + url: {{receiverOpenPaymentsAuthHost}}/ + body: json + auth: none +} + +body:json { + { + "access_token": { + "access": [ + { + "type": "incoming-payment", + "actions": [ + "create", "read", "list", "complete" + ] + } + ] + }, + "client": "{{clientWalletAddress}}" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.addSignatureHeaders(); +} + +script:post-response { + const scripts = require('./scripts'); + + scripts.storeTokenDetails(); +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Outgoing Payment.bru new file mode 100644 index 0000000000..5be7a46476 --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Outgoing Payment.bru @@ -0,0 +1,56 @@ +meta { + name: Grant Request Outgoing Payment + type: http + seq: 7 +} + +post { + url: {{senderOpenPaymentsAuthHost}}/ + body: json + auth: none +} + +body:json { + { + "access_token": { + "access": [ + { + "type": "outgoing-payment", + "actions": [ + "create", "read", "list" + ], + "identifier": "{{senderWalletAddress}}", + "limits": { + "debitAmount": {{quoteDebitAmount}}, + "receiveAmount": {{quoteReceiveAmount}} + } + } + ] + }, + "client": "{{clientWalletAddress}}", + "interact": { + "start": [ + "redirect" + ] + } + } + +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.addSignatureHeaders(); +} + +script:post-response { + const scripts = require('./scripts'); + + scripts.storeTokenDetails(); +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Quote.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Quote.bru new file mode 100644 index 0000000000..3c0736670d --- /dev/null +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Quote.bru @@ -0,0 +1,46 @@ +meta { + name: Grant Request Quote + type: http + seq: 5 +} + +post { + url: {{senderOpenPaymentsAuthHost}}/ + body: json + auth: none +} + +body:json { + { + "access_token": { + "access": [ + { + "type": "quote", + "actions": [ + "create", "read" + ] + } + ] + }, + "client": "{{clientWalletAddress}}" + } + +} + +script:pre-request { + const scripts = require('./scripts'); + + await scripts.addSignatureHeaders(); +} + +script:post-response { + const scripts = require('./scripts'); + + scripts.storeTokenDetails(); +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} From b49e08eed7f01ee13ef4763a4d68a46ffda59596 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:47:50 -0400 Subject: [PATCH 22/64] feat(backend): start rm ilpQuoteDetail join on op where not used --- .../src/graphql/resolvers/liquidity.ts | 2 +- .../src/graphql/resolvers/outgoing_payment.ts | 2 +- .../open_payments/payment/outgoing/routes.ts | 2 +- .../payment/outgoing/service.test.ts | 18 +++++++++--------- .../open_payments/payment/outgoing/service.ts | 19 +++++++++++++++++++ .../wallet_address/middleware.ts | 2 +- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/liquidity.ts b/packages/backend/src/graphql/resolvers/liquidity.ts index ba434e31ea..a8bd8ca19a 100644 --- a/packages/backend/src/graphql/resolvers/liquidity.ts +++ b/packages/backend/src/graphql/resolvers/liquidity.ts @@ -539,7 +539,7 @@ export const createOutgoingPaymentWithdrawal: MutationResolvers[' const outgoingPaymentService = await ctx.container.use( 'outgoingPaymentService' ) - const outgoingPayment = await outgoingPaymentService.get({ + const outgoingPayment = await outgoingPaymentService.getWithIlpDetails({ id: outgoingPaymentId }) const webhookService = await ctx.container.use('webhookService') diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.ts index a9cdcb4403..44d73c9d5a 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.ts @@ -23,7 +23,7 @@ export const getOutgoingPayment: QueryResolvers['outgoingPayment' const outgoingPaymentService = await ctx.container.use( 'outgoingPaymentService' ) - const payment = await outgoingPaymentService.get({ + const payment = await outgoingPaymentService.getWithIlpDetails({ id: args.id }) if (!payment) { diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.ts b/packages/backend/src/open_payments/payment/outgoing/routes.ts index e39e0c3baf..ec1324d75e 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.ts @@ -52,7 +52,7 @@ async function getOutgoingPayment( deps: ServiceDependencies, ctx: ReadContext ): Promise { - const outgoingPayment = await deps.outgoingPaymentService.get({ + const outgoingPayment = await deps.outgoingPaymentService.getWithIlpDetails({ id: ctx.params.id, client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined }) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 2b83d75dba..78d364f5e1 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -110,7 +110,7 @@ describe('OutgoingPaymentService', (): void => { expectedError?: string ): Promise { await expect(outgoingPaymentService.processNext()).resolves.toBe(paymentId) - const payment = await outgoingPaymentService.get({ + const payment = await outgoingPaymentService.getWithIlpDetails({ id: paymentId }) if (!payment) throw 'no payment' @@ -333,7 +333,7 @@ describe('OutgoingPaymentService', (): void => { validDestination: false, method: 'ilp' }), - get: (options) => outgoingPaymentService.get(options), + get: (options) => outgoingPaymentService.getWithIlpDetails(options), list: (options) => outgoingPaymentService.getWalletAddressPage(options) }) }) @@ -355,7 +355,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(!isOutgoingPaymentError(payment)) await expect( - outgoingPaymentService.get({ + outgoingPaymentService.getWithIlpDetails({ id: payment.id, client }) @@ -371,7 +371,7 @@ describe('OutgoingPaymentService', (): void => { .spyOn(accountingService, 'getTotalSent') .mockResolvedValueOnce(undefined) await expect( - outgoingPaymentService.get({ + outgoingPaymentService.getWithIlpDetails({ id: payment.id, client }) @@ -517,7 +517,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(!isOutgoingPaymentError(payment)) await expect( - outgoingPaymentService.get({ + outgoingPaymentService.getWithIlpDetails({ id: payment.id, client }) @@ -836,7 +836,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( - outgoingPaymentService.get({ + outgoingPaymentService.getWithIlpDetails({ id: payment.id }) ).resolves.toEqual(payment) @@ -1806,7 +1806,7 @@ describe('OutgoingPaymentService', (): void => { state: OutgoingPaymentState.Sending }) - const after = await outgoingPaymentService.get({ + const after = await outgoingPaymentService.getWithIlpDetails({ id: payment.id }) expect(after?.state).toBe(OutgoingPaymentState.Sending) @@ -1822,7 +1822,7 @@ describe('OutgoingPaymentService', (): void => { }) ).resolves.toEqual(FundingError.InvalidAmount) - const after = await outgoingPaymentService.get({ + const after = await outgoingPaymentService.getWithIlpDetails({ id: payment.id }) expect(after?.state).toBe(OutgoingPaymentState.Funding) @@ -1841,7 +1841,7 @@ describe('OutgoingPaymentService', (): void => { }) ).resolves.toEqual(FundingError.WrongState) - const after = await outgoingPaymentService.get({ + const after = await outgoingPaymentService.getWithIlpDetails({ id: payment.id }) expect(after?.state).toBe(startState) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index a4bb961b69..e0979eb617 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' export interface OutgoingPaymentService extends WalletAddressSubresourceService { + getWithIlpDetails(options: GetOptions): Promise getPage(options?: GetPageOptions): Promise create( options: CreateOutgoingPaymentOptions @@ -78,6 +79,8 @@ export async function createOutgoingPaymentService( } return { get: (options) => getOutgoingPayment(deps, options), + getWithIlpDetails: (options) => + getOutgoingPaymentWithILPDetails(deps, options), getPage: (options) => getOutgoingPaymentsPage(deps, options), create: (options) => createOutgoingPayment(deps, options), cancel: (options) => cancelOutgoingPayment(deps, options), @@ -141,6 +144,22 @@ async function getOutgoingPaymentsPage( async function getOutgoingPayment( deps: ServiceDependencies, options: GetOptions +): Promise { + const outgoingPayment = await OutgoingPayment.query(deps.knex) + .get(options) + .withGraphFetched('[quote.asset, walletAddress]') + + if (outgoingPayment) { + return addSentAmount(deps, outgoingPayment) + } +} + +// TODO: Dont return the payment joined on the ilpQuoteDetails by default. +// - [X] replace every outgoingPaymentService.get with .getWithILPDetails +// - [ ] for each getWithILPDetails call, change to getOutgoingPayment (if able) and validate +async function getOutgoingPaymentWithILPDetails( + deps: ServiceDependencies, + options: GetOptions ): Promise { const outgoingPayment = await OutgoingPayment.query(deps.knex) .get(options) diff --git a/packages/backend/src/open_payments/wallet_address/middleware.ts b/packages/backend/src/open_payments/wallet_address/middleware.ts index 6a6601d9f9..073156b03f 100644 --- a/packages/backend/src/open_payments/wallet_address/middleware.ts +++ b/packages/backend/src/open_payments/wallet_address/middleware.ts @@ -64,7 +64,7 @@ export async function getWalletAddressUrlFromOutgoingPayment( const outgoingPaymentService = await ctx.container.use( 'outgoingPaymentService' ) - const outgoingPayment = await outgoingPaymentService.get({ + const outgoingPayment = await outgoingPaymentService.getWithIlpDetails({ id: ctx.params.id }) From eedacb52be95d111683ca488bc40f8a51145ae70 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:51:37 -0400 Subject: [PATCH 23/64] fix(backend): rm unecessary ilpQuoteDetail join --- packages/backend/src/graphql/resolvers/liquidity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/graphql/resolvers/liquidity.ts b/packages/backend/src/graphql/resolvers/liquidity.ts index a8bd8ca19a..ba434e31ea 100644 --- a/packages/backend/src/graphql/resolvers/liquidity.ts +++ b/packages/backend/src/graphql/resolvers/liquidity.ts @@ -539,7 +539,7 @@ export const createOutgoingPaymentWithdrawal: MutationResolvers[' const outgoingPaymentService = await ctx.container.use( 'outgoingPaymentService' ) - const outgoingPayment = await outgoingPaymentService.getWithIlpDetails({ + const outgoingPayment = await outgoingPaymentService.get({ id: outgoingPaymentId }) const webhookService = await ctx.container.use('webhookService') From 27a64b23784502dd9ed49cb989b52d65d5e55347 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:53:22 -0400 Subject: [PATCH 24/64] chore(backend): format --- ...6181643_require_estimated_exchange_rate.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js b/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js index 23fce59a3d..cd9d69d8df 100644 --- a/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js +++ b/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js @@ -4,30 +4,30 @@ */ exports.up = function (knex) { - return knex("quotes") - .whereNull("estimatedExchangeRate") + return knex('quotes') + .whereNull('estimatedExchangeRate') .update({ // TODO: vet this more... looks like the low* fields were (inadvertently?) // made nullable when updating from bigint to decimal. If they are null // anywhere then this wont work. - estimatedExchangeRate: knex.raw("?? / ??", [ - "lowEstimatedExchangeRateNumerator", - "lowEstimatedExchangeRateDenominator", - ]), + estimatedExchangeRate: knex.raw('?? / ??', [ + 'lowEstimatedExchangeRateNumerator', + 'lowEstimatedExchangeRateDenominator' + ]) }) .then(() => { - return knex.schema.table("quotes", (table) => { - table.decimal("estimatedExchangeRate", 20, 10).notNullable().alter(); - }); - }); -}; + return knex.schema.table('quotes', (table) => { + table.decimal('estimatedExchangeRate', 20, 10).notNullable().alter() + }) + }) +} /** * @param { import("knex").Knex } knex * @returns { Promise } */ exports.down = function (knex) { - return knex.schema.table("quotes", (table) => { - table.decimal("estimatedExchangeRate", 20, 10).nullable().alter(); - }); -}; + return knex.schema.table('quotes', (table) => { + table.decimal('estimatedExchangeRate', 20, 10).nullable().alter() + }) +} From 26d0539eb94c8b5f28d418430f9ac6135fa00d7e Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:02:34 -0400 Subject: [PATCH 25/64] fix(backend): dont join op on quote.ilpQuoteDetails on get --- .../backend/src/graphql/resolvers/outgoing_payment.ts | 2 +- .../src/open_payments/payment/outgoing/routes.ts | 2 +- .../src/open_payments/payment/outgoing/service.test.ts | 2 +- .../src/open_payments/payment/outgoing/service.ts | 10 +++++++--- .../src/open_payments/wallet_address/middleware.ts | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.ts index 44d73c9d5a..a9cdcb4403 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.ts @@ -23,7 +23,7 @@ export const getOutgoingPayment: QueryResolvers['outgoingPayment' const outgoingPaymentService = await ctx.container.use( 'outgoingPaymentService' ) - const payment = await outgoingPaymentService.getWithIlpDetails({ + const payment = await outgoingPaymentService.get({ id: args.id }) if (!payment) { diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.ts b/packages/backend/src/open_payments/payment/outgoing/routes.ts index ec1324d75e..e39e0c3baf 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.ts @@ -52,7 +52,7 @@ async function getOutgoingPayment( deps: ServiceDependencies, ctx: ReadContext ): Promise { - const outgoingPayment = await deps.outgoingPaymentService.getWithIlpDetails({ + const outgoingPayment = await deps.outgoingPaymentService.get({ id: ctx.params.id, client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined }) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 78d364f5e1..9d598f4329 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -110,7 +110,7 @@ describe('OutgoingPaymentService', (): void => { expectedError?: string ): Promise { await expect(outgoingPaymentService.processNext()).resolves.toBe(paymentId) - const payment = await outgoingPaymentService.getWithIlpDetails({ + const payment = await outgoingPaymentService.get({ id: paymentId }) if (!payment) throw 'no payment' diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index e0979eb617..b3e92d1a7d 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -154,9 +154,9 @@ async function getOutgoingPayment( } } -// TODO: Dont return the payment joined on the ilpQuoteDetails by default. -// - [X] replace every outgoingPaymentService.get with .getWithILPDetails -// - [ ] for each getWithILPDetails call, change to getOutgoingPayment (if able) and validate +// TODO: Remove this entirely after removing ilpQuoteDetails elsewhere? +// Think the only thing using it now are tests because the service create +// method is driving the expectation for ilpQuoteDetails async function getOutgoingPaymentWithILPDetails( deps: ServiceDependencies, options: GetOptions @@ -230,6 +230,10 @@ async function cancelOutgoingPayment( }) } +// TODO: when refactorting to remove implicit ilpQuoteDetails join, +// remove usage of outgoingPaymentService.getWithILPDetails where possible. Some +// of these (such as rafiki/packages/backend/src/open_payments/payment/outgoing/service.test.ts) +// are being compared with the create, thus they cant be removed until its removed in the create as well. async function createOutgoingPayment( deps: ServiceDependencies, options: CreateOutgoingPaymentOptions diff --git a/packages/backend/src/open_payments/wallet_address/middleware.ts b/packages/backend/src/open_payments/wallet_address/middleware.ts index 073156b03f..6a6601d9f9 100644 --- a/packages/backend/src/open_payments/wallet_address/middleware.ts +++ b/packages/backend/src/open_payments/wallet_address/middleware.ts @@ -64,7 +64,7 @@ export async function getWalletAddressUrlFromOutgoingPayment( const outgoingPaymentService = await ctx.container.use( 'outgoingPaymentService' ) - const outgoingPayment = await outgoingPaymentService.getWithIlpDetails({ + const outgoingPayment = await outgoingPaymentService.get({ id: ctx.params.id }) From 2723961a0836ce8625a33638b136a5efe08983e0 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:08:08 -0400 Subject: [PATCH 26/64] fix(backend): rm ilpQuoteDetails join on op cancel --- packages/backend/src/open_payments/payment/outgoing/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index b3e92d1a7d..2dc5a9b041 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -224,7 +224,7 @@ async function cancelOutgoingPayment( ...(options.reason ? { cancellationReason: options.reason } : {}) } }) - .withGraphFetched('[quote.[asset, ilpQuoteDetails], walletAddress]') + .withGraphFetched('[quote.asset, walletAddress]') return addSentAmount(deps, payment) }) From 08126c5abcbb8fb34d76044d5e2c6598bab9182c Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:15:22 -0400 Subject: [PATCH 27/64] fix(backend): rm unecessary join in op validate grant amount --- packages/backend/src/open_payments/payment/outgoing/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 2dc5a9b041..628dbf950e 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -450,7 +450,7 @@ async function validateGrantAndAddSpentAmountsToPayment( .andWhereNot({ id: payment.id }) - .withGraphFetched('[quote.[asset, ilpQuoteDetails]]') + .withGraphFetched('quote.asset') if (grantPayments.length === 0) { return true From 205447df560e5f354af6970c53866e7d878a2f56 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:18:30 -0400 Subject: [PATCH 28/64] fix(backend): rm join from fundPayment --- packages/backend/src/open_payments/payment/outgoing/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 628dbf950e..772c73c649 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -527,7 +527,7 @@ async function fundPayment( const payment = await OutgoingPayment.query(trx) .findById(id) .forUpdate() - .withGraphFetched('[quote.[asset, ilpQuoteDetails]]') + .withGraphFetched('quote.asset') if (!payment) return FundingError.UnknownPayment if (payment.state !== OutgoingPaymentState.Funding) { return FundingError.WrongState From 0471d8419a56628cff5a32d76f0cc26a4b81269e Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:19:41 -0400 Subject: [PATCH 29/64] fix(backend): rm unecessary join, unused method --- .../payment/outgoing/service.test.ts | 16 +++++------ .../open_payments/payment/outgoing/service.ts | 27 ++----------------- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 9d598f4329..2b83d75dba 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -333,7 +333,7 @@ describe('OutgoingPaymentService', (): void => { validDestination: false, method: 'ilp' }), - get: (options) => outgoingPaymentService.getWithIlpDetails(options), + get: (options) => outgoingPaymentService.get(options), list: (options) => outgoingPaymentService.getWalletAddressPage(options) }) }) @@ -355,7 +355,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(!isOutgoingPaymentError(payment)) await expect( - outgoingPaymentService.getWithIlpDetails({ + outgoingPaymentService.get({ id: payment.id, client }) @@ -371,7 +371,7 @@ describe('OutgoingPaymentService', (): void => { .spyOn(accountingService, 'getTotalSent') .mockResolvedValueOnce(undefined) await expect( - outgoingPaymentService.getWithIlpDetails({ + outgoingPaymentService.get({ id: payment.id, client }) @@ -517,7 +517,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(!isOutgoingPaymentError(payment)) await expect( - outgoingPaymentService.getWithIlpDetails({ + outgoingPaymentService.get({ id: payment.id, client }) @@ -836,7 +836,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( - outgoingPaymentService.getWithIlpDetails({ + outgoingPaymentService.get({ id: payment.id }) ).resolves.toEqual(payment) @@ -1806,7 +1806,7 @@ describe('OutgoingPaymentService', (): void => { state: OutgoingPaymentState.Sending }) - const after = await outgoingPaymentService.getWithIlpDetails({ + const after = await outgoingPaymentService.get({ id: payment.id }) expect(after?.state).toBe(OutgoingPaymentState.Sending) @@ -1822,7 +1822,7 @@ describe('OutgoingPaymentService', (): void => { }) ).resolves.toEqual(FundingError.InvalidAmount) - const after = await outgoingPaymentService.getWithIlpDetails({ + const after = await outgoingPaymentService.get({ id: payment.id }) expect(after?.state).toBe(OutgoingPaymentState.Funding) @@ -1841,7 +1841,7 @@ describe('OutgoingPaymentService', (): void => { }) ).resolves.toEqual(FundingError.WrongState) - const after = await outgoingPaymentService.getWithIlpDetails({ + const after = await outgoingPaymentService.get({ id: payment.id }) expect(after?.state).toBe(startState) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 772c73c649..6636d1ef88 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -45,7 +45,6 @@ import { FilterString } from '../../../shared/filters' export interface OutgoingPaymentService extends WalletAddressSubresourceService { - getWithIlpDetails(options: GetOptions): Promise getPage(options?: GetPageOptions): Promise create( options: CreateOutgoingPaymentOptions @@ -79,8 +78,6 @@ export async function createOutgoingPaymentService( } return { get: (options) => getOutgoingPayment(deps, options), - getWithIlpDetails: (options) => - getOutgoingPaymentWithILPDetails(deps, options), getPage: (options) => getOutgoingPaymentsPage(deps, options), create: (options) => createOutgoingPayment(deps, options), cancel: (options) => cancelOutgoingPayment(deps, options), @@ -154,22 +151,6 @@ async function getOutgoingPayment( } } -// TODO: Remove this entirely after removing ilpQuoteDetails elsewhere? -// Think the only thing using it now are tests because the service create -// method is driving the expectation for ilpQuoteDetails -async function getOutgoingPaymentWithILPDetails( - deps: ServiceDependencies, - options: GetOptions -): Promise { - const outgoingPayment = await OutgoingPayment.query(deps.knex) - .get(options) - .withGraphFetched('[quote.[asset, ilpQuoteDetails], walletAddress]') - - if (outgoingPayment) { - return addSentAmount(deps, outgoingPayment) - } -} - export interface BaseOptions { walletAddressId: string client?: string @@ -230,10 +211,6 @@ async function cancelOutgoingPayment( }) } -// TODO: when refactorting to remove implicit ilpQuoteDetails join, -// remove usage of outgoingPaymentService.getWithILPDetails where possible. Some -// of these (such as rafiki/packages/backend/src/open_payments/payment/outgoing/service.test.ts) -// are being compared with the create, thus they cant be removed until its removed in the create as well. async function createOutgoingPayment( deps: ServiceDependencies, options: CreateOutgoingPaymentOptions @@ -286,7 +263,7 @@ async function createOutgoingPayment( state: OutgoingPaymentState.Funding, grantId }) - .withGraphFetched('[quote.[asset, ilpQuoteDetails], walletAddress]') + .withGraphFetched('[quote.asset, walletAddress]') if ( payment.walletAddressId !== payment.quote.walletAddressId || @@ -572,7 +549,7 @@ async function getWalletAddressPage( ): Promise { const page = await OutgoingPayment.query(deps.knex) .list(options) - .withGraphFetched('[quote.[asset, ilpQuoteDetails], walletAddress]') + .withGraphFetched('[quote.asset, walletAddress]') const amounts = await deps.accountingService.getAccountsTotalSent( page.map((payment: OutgoingPayment) => payment.id) ) From 7b5142d59a89719b80d1b1c66c976068e00ef103 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:07:17 -0400 Subject: [PATCH 30/64] fix(backend): fetch ilpQuoteDetails where used instead of joining --- packages/backend/src/app.ts | 2 + packages/backend/src/index.ts | 11 ++- .../open_payments/payment/outgoing/worker.ts | 2 +- .../backend/src/open_payments/quote/model.ts | 6 -- .../ilp/quote-details/service.test.ts | 68 +++++++++++++++++++ .../ilp/quote-details/service.ts | 33 +++++++++ .../backend/src/payment-method/ilp/service.ts | 29 ++++---- 7 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 packages/backend/src/payment-method/ilp/quote-details/service.test.ts create mode 100644 packages/backend/src/payment-method/ilp/quote-details/service.ts diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index ccb62fc4a9..d7db2a45c0 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -101,6 +101,7 @@ import { LoggingPlugin } from './graphql/plugin' import { LocalPaymentService } from './payment-method/local/service' import { GrantService } from './open_payments/grant/service' import { AuthServerService } from './open_payments/authServer/service' +import { IlpQuoteDetailsService } from './payment-method/ilp/quote-details/service' export interface AppContextData { logger: Logger container: AppContainer @@ -256,6 +257,7 @@ export interface AppServices { paymentMethodHandlerService: Promise ilpPaymentService: Promise localPaymentService: Promise + ilpQuoteDetailsService: Promise } export type AppContainer = IocContract diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 23eabdb8ea..22415573a5 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -51,6 +51,7 @@ import { createIlpPaymentService, ServiceDependencies as IlpPaymentServiceDependencies } from './payment-method/ilp/service' +import { createIlpQuoteDetailsService } from './payment-method/ilp/quote-details/service' import { createLocalPaymentService, ServiceDependencies as LocalPaymentServiceDependencies @@ -428,13 +429,21 @@ export function initIocContainer( }) }) + container.singleton('ilpQuoteDetailsService', async (deps) => { + return await createIlpQuoteDetailsService({ + logger: await deps.use('logger'), + knex: await deps.use('knex') + }) + }) + container.singleton('ilpPaymentService', async (deps) => { const serviceDependencies: IlpPaymentServiceDependencies = { logger: await deps.use('logger'), knex: await deps.use('knex'), config: await deps.use('config'), makeIlpPlugin: await deps.use('makeIlpPlugin'), - ratesService: await deps.use('ratesService') + ratesService: await deps.use('ratesService'), + ilpQuoteDetailsService: await deps.use('ilpQuoteDetailsService') } if (config.enableTelemetry) { diff --git a/packages/backend/src/open_payments/payment/outgoing/worker.ts b/packages/backend/src/open_payments/payment/outgoing/worker.ts index e460527d93..0df698f960 100644 --- a/packages/backend/src/open_payments/payment/outgoing/worker.ts +++ b/packages/backend/src/open_payments/payment/outgoing/worker.ts @@ -66,7 +66,7 @@ async function getPendingPayment( [RETRY_BACKOFF_SECONDS, now] ) }) - .withGraphFetched('[walletAddress, quote.[asset, ilpQuoteDetails]]') + .withGraphFetched('[walletAddress, quote.asset]') return payments[0] } diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index d0dfb0d5da..92a8afb1d6 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -11,12 +11,6 @@ import { Quote as OpenPaymentsQuote } from '@interledger/open-payments' import { Fee } from '../../fee/model' import { BaseModel } from '../../shared/baseModel' -// TODO: use or lose. could maybe be used as a typegaurd instead of checking that details -// field(s) are present -// export interface QuoteWithDetails extends Quote { -// ilpQuoteDetails: IlpQuoteDetails -// } - export class Quote extends WalletAddressSubresource { public static readonly tableName = 'quotes' public static readonly urlPath = '/quotes' diff --git a/packages/backend/src/payment-method/ilp/quote-details/service.test.ts b/packages/backend/src/payment-method/ilp/quote-details/service.test.ts new file mode 100644 index 0000000000..bbd647a955 --- /dev/null +++ b/packages/backend/src/payment-method/ilp/quote-details/service.test.ts @@ -0,0 +1,68 @@ +import { IocContract } from '@adonisjs/fold' +import { AppServices } from '../../../app' +import { TestContainer, createTestApp } from '../../../tests/app' +import { initIocContainer } from '../../../' +import { Config } from '../../../config/app' +import { Knex } from 'knex' +import { truncateTables } from '../../../tests/tableManager' +import { createAsset } from '../../../tests/asset' +import { v4 } from 'uuid' +import { IlpQuoteDetailsService } from './service' +import { createQuote } from '../../../tests/quote' +import { createWalletAddress } from '../../../tests/walletAddress' + +describe('IlpQuoteDetails Service', (): void => { + let deps: IocContract + let appContainer: TestContainer + let knex: Knex + let ilpQuoteDetailsService: IlpQuoteDetailsService + + beforeAll(async (): Promise => { + deps = await initIocContainer(Config) + appContainer = await createTestApp(deps) + knex = await deps.use('knex') + ilpQuoteDetailsService = await deps.use('ilpQuoteDetailsService') + }) + afterEach(async (): Promise => { + await truncateTables(knex) + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + + describe('IlpQuoteDetails Service', (): void => { + describe('getById', (): void => { + it('should get ILP quote by id', async (): Promise => { + // const ilpQuote = await createIlpQuote(deps, asset.id, BigInt(1000)) + const asset = await createAsset(deps) + const { id: walletAddressId } = await createWalletAddress(deps, { + assetId: asset.id + }) + + const quote = await createQuote(deps, { + walletAddressId, + receiver: `http://wallet2.example/bob/incoming-payments/${v4()}`, + debitAmount: { + value: BigInt(56), + assetCode: asset.code, + assetScale: asset.scale + }, + method: 'ilp', + validDestination: false + }) + + const foundIlpQuote = await ilpQuoteDetailsService.getByQuoteId( + quote.id + ) + console.log({ foundIlpQuote }) + expect(foundIlpQuote).toBeDefined() + }) + + it('should return undefined when no ILP quote is found by id', async (): Promise => { + const foundIlpQuote = await ilpQuoteDetailsService.getByQuoteId(v4()) + expect(foundIlpQuote).toBe(undefined) + }) + }) + }) +}) diff --git a/packages/backend/src/payment-method/ilp/quote-details/service.ts b/packages/backend/src/payment-method/ilp/quote-details/service.ts new file mode 100644 index 0000000000..a26633d610 --- /dev/null +++ b/packages/backend/src/payment-method/ilp/quote-details/service.ts @@ -0,0 +1,33 @@ +import { TransactionOrKnex } from 'objection' +// TODO: move IlpQuoteDetails to this dir +import { IlpQuoteDetails } from '../../../open_payments/quote/model' +import { BaseService } from '../../../shared/baseService' + +export interface IlpQuoteDetailsService { + getByQuoteId(quoteId: string): Promise +} + +export interface ServiceDependencies extends BaseService { + knex: TransactionOrKnex +} + +export async function createIlpQuoteDetailsService( + deps_: ServiceDependencies +): Promise { + const deps = { + ...deps_, + logger: deps_.logger.child({ service: 'IlpQuoteDetailsService' }) + } + return { + getByQuoteId: (quoteId: string) => + getIlpQuoteDetailsByQuoteId(deps, quoteId) + } +} +async function getIlpQuoteDetailsByQuoteId( + deps: ServiceDependencies, + quoteId: string +): Promise { + return await IlpQuoteDetails.query(deps.knex) + .where('quoteId', quoteId) + .first() +} diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index b0df8ccc7c..855eca8462 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -15,6 +15,7 @@ import { PaymentMethodHandlerErrorCode } from '../handler/errors' import { TelemetryService } from '../../telemetry/service' +import { IlpQuoteDetailsService } from './quote-details/service' export interface IlpPaymentService extends PaymentMethodService {} @@ -23,6 +24,7 @@ export interface ServiceDependencies extends BaseService { ratesService: RatesService makeIlpPlugin: (options: IlpPluginOptions) => IlpPlugin telemetry?: TelemetryService + ilpQuoteDetailsService: IlpQuoteDetailsService } export async function createIlpPaymentService( @@ -217,16 +219,19 @@ async function pay( }) } - // TODO: prevent ilpQuoteDetails with better type instead of having to check and error? - // Or make better error. - // Is there a way to ensure that right here, outgoingPayment.quote is QuoteWithDetails? - // I dont think so... this is where the ambiguousness of our DB types surfaces and we have - // to do some explicit check like this. Even if we had table inheritance we'd have this problem - // so long as an outgoing payment could have either type of quote on them. To truly make this - // typesafe you might need to have seperate outgoing payment tables for each type of quote (which - // goes way too far) if (!outgoingPayment.quote.ilpQuoteDetails) { - throw new Error('Missing ILP quote details') + outgoingPayment.quote.ilpQuoteDetails = + await deps.ilpQuoteDetailsService.getByQuoteId(outgoingPayment.quote.id) + + if (!outgoingPayment.quote.ilpQuoteDetails) { + throw new PaymentMethodHandlerError( + 'Could not find required ILP Quote Details', + { + description: 'ILP Quote Details not found', + retryable: false + } + ) + } } const { @@ -238,11 +243,7 @@ async function pay( const quote: Pay.Quote = { maxPacketAmount, - // TODO: is paymentType controlling something in Pay.pay that we should be replicating in - // local payment method? paymentType: Pay.PaymentType.FixedDelivery, - // finalDebitAmount: 617n - // finalReceiveAmount: 500n maxSourceAmount: finalDebitAmount, minDeliveryAmount: finalReceiveAmount, lowEstimatedExchangeRate, @@ -251,8 +252,6 @@ async function pay( } const plugin = deps.makeIlpPlugin({ - // outgoingPayment.quote.receiveAmountValue: 500n - // outgoingPayment.quote.debitAmountValue: 617n sourceAccount: outgoingPayment }) From 773e4f2bf149808d2ab7f6efc119a6abf374d4db Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:14:30 -0400 Subject: [PATCH 31/64] chore(backend): move ilpquotedetails dir --- .../backend/src/open_payments/quote/model.ts | 81 +------------------ .../payment-method/ilp/quote-details/model.ts | 81 +++++++++++++++++++ .../ilp/quote-details/service.ts | 3 +- 3 files changed, 83 insertions(+), 82 deletions(-) create mode 100644 packages/backend/src/payment-method/ilp/quote-details/model.ts diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index 92a8afb1d6..48619ba080 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -1,6 +1,4 @@ import { Model, Pojo } from 'objection' -import * as Pay from '@interledger/pay' - import { Amount, serializeAmount } from '../amount' import { WalletAddress, @@ -9,7 +7,7 @@ import { import { Asset } from '../../asset/model' import { Quote as OpenPaymentsQuote } from '@interledger/open-payments' import { Fee } from '../../fee/model' -import { BaseModel } from '../../shared/baseModel' +import { IlpQuoteDetails } from '../../payment-method/ilp/quote-details/model' export class Quote extends WalletAddressSubresource { public static readonly tableName = 'quotes' @@ -145,80 +143,3 @@ export class Quote extends WalletAddressSubresource { } } } - -export class IlpQuoteDetails extends BaseModel { - public static readonly tableName = 'ilpQuoteDetails' - - static get virtualAttributes(): string[] { - return [ - 'minExchangeRate', - 'lowEstimatedExchangeRate', - 'highEstimatedExchangeRate' - ] - } - - public quoteId!: string - public quote?: Quote - - public maxPacketAmount!: bigint - public minExchangeRateNumerator!: bigint - public minExchangeRateDenominator!: bigint - public lowEstimatedExchangeRateNumerator!: bigint - public lowEstimatedExchangeRateDenominator!: bigint - public highEstimatedExchangeRateNumerator!: bigint - public highEstimatedExchangeRateDenominator!: bigint - - static get relationMappings() { - return { - quote: { - relation: Model.BelongsToOneRelation, - modelClass: Quote, - join: { - from: 'ilpQuoteDetails.quoteId', - to: 'quotes.id' - } - } - } - } - - public get minExchangeRate(): Pay.Ratio { - return Pay.Ratio.of( - Pay.Int.from(this.minExchangeRateNumerator) as Pay.PositiveInt, - Pay.Int.from(this.minExchangeRateDenominator) as Pay.PositiveInt - ) - } - - public set minExchangeRate(value: Pay.Ratio) { - this.minExchangeRateNumerator = value.a.value - this.minExchangeRateDenominator = value.b.value - } - - public get lowEstimatedExchangeRate(): Pay.Ratio { - return Pay.Ratio.of( - Pay.Int.from(this.lowEstimatedExchangeRateNumerator) as Pay.PositiveInt, - Pay.Int.from(this.lowEstimatedExchangeRateDenominator) as Pay.PositiveInt - ) - } - - public set lowEstimatedExchangeRate(value: Pay.Ratio) { - this.lowEstimatedExchangeRateNumerator = value.a.value - this.lowEstimatedExchangeRateDenominator = value.b.value - } - - // Note that the upper exchange rate bound is *exclusive*. - public get highEstimatedExchangeRate(): Pay.PositiveRatio { - const highEstimatedExchangeRate = Pay.Ratio.of( - Pay.Int.from(this.highEstimatedExchangeRateNumerator) as Pay.PositiveInt, - Pay.Int.from(this.highEstimatedExchangeRateDenominator) as Pay.PositiveInt - ) - if (!highEstimatedExchangeRate.isPositive()) { - throw new Error('high estimated exchange rate is not positive') - } - return highEstimatedExchangeRate - } - - public set highEstimatedExchangeRate(value: Pay.PositiveRatio) { - this.highEstimatedExchangeRateNumerator = value.a.value - this.highEstimatedExchangeRateDenominator = value.b.value - } -} diff --git a/packages/backend/src/payment-method/ilp/quote-details/model.ts b/packages/backend/src/payment-method/ilp/quote-details/model.ts new file mode 100644 index 0000000000..dbe47550b7 --- /dev/null +++ b/packages/backend/src/payment-method/ilp/quote-details/model.ts @@ -0,0 +1,81 @@ +import { Model } from 'objection' +import { Quote } from '../../../open_payments/quote/model' +import { BaseModel } from '../../../shared/baseModel' +import * as Pay from '@interledger/pay' + +export class IlpQuoteDetails extends BaseModel { + public static readonly tableName = 'ilpQuoteDetails' + + static get virtualAttributes(): string[] { + return [ + 'minExchangeRate', + 'lowEstimatedExchangeRate', + 'highEstimatedExchangeRate' + ] + } + + public quoteId!: string + public quote?: Quote + + public maxPacketAmount!: bigint + public minExchangeRateNumerator!: bigint + public minExchangeRateDenominator!: bigint + public lowEstimatedExchangeRateNumerator!: bigint + public lowEstimatedExchangeRateDenominator!: bigint + public highEstimatedExchangeRateNumerator!: bigint + public highEstimatedExchangeRateDenominator!: bigint + + static get relationMappings() { + return { + quote: { + relation: Model.BelongsToOneRelation, + modelClass: Quote, + join: { + from: 'ilpQuoteDetails.quoteId', + to: 'quotes.id' + } + } + } + } + + public get minExchangeRate(): Pay.Ratio { + return Pay.Ratio.of( + Pay.Int.from(this.minExchangeRateNumerator) as Pay.PositiveInt, + Pay.Int.from(this.minExchangeRateDenominator) as Pay.PositiveInt + ) + } + + public set minExchangeRate(value: Pay.Ratio) { + this.minExchangeRateNumerator = value.a.value + this.minExchangeRateDenominator = value.b.value + } + + public get lowEstimatedExchangeRate(): Pay.Ratio { + return Pay.Ratio.of( + Pay.Int.from(this.lowEstimatedExchangeRateNumerator) as Pay.PositiveInt, + Pay.Int.from(this.lowEstimatedExchangeRateDenominator) as Pay.PositiveInt + ) + } + + public set lowEstimatedExchangeRate(value: Pay.Ratio) { + this.lowEstimatedExchangeRateNumerator = value.a.value + this.lowEstimatedExchangeRateDenominator = value.b.value + } + + // Note that the upper exchange rate bound is *exclusive*. + public get highEstimatedExchangeRate(): Pay.PositiveRatio { + const highEstimatedExchangeRate = Pay.Ratio.of( + Pay.Int.from(this.highEstimatedExchangeRateNumerator) as Pay.PositiveInt, + Pay.Int.from(this.highEstimatedExchangeRateDenominator) as Pay.PositiveInt + ) + if (!highEstimatedExchangeRate.isPositive()) { + throw new Error('high estimated exchange rate is not positive') + } + return highEstimatedExchangeRate + } + + public set highEstimatedExchangeRate(value: Pay.PositiveRatio) { + this.highEstimatedExchangeRateNumerator = value.a.value + this.highEstimatedExchangeRateDenominator = value.b.value + } +} diff --git a/packages/backend/src/payment-method/ilp/quote-details/service.ts b/packages/backend/src/payment-method/ilp/quote-details/service.ts index a26633d610..3903b75b71 100644 --- a/packages/backend/src/payment-method/ilp/quote-details/service.ts +++ b/packages/backend/src/payment-method/ilp/quote-details/service.ts @@ -1,6 +1,5 @@ import { TransactionOrKnex } from 'objection' -// TODO: move IlpQuoteDetails to this dir -import { IlpQuoteDetails } from '../../../open_payments/quote/model' +import { IlpQuoteDetails } from './model' import { BaseService } from '../../../shared/baseService' export interface IlpQuoteDetailsService { From 0a3adf43d12e548371ff14a2e1a969a2f7bc255d Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:24:11 -0400 Subject: [PATCH 32/64] chore(backend): rm console.log --- .../backend/src/payment-method/ilp/quote-details/service.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/payment-method/ilp/quote-details/service.test.ts b/packages/backend/src/payment-method/ilp/quote-details/service.test.ts index bbd647a955..e9da98c24b 100644 --- a/packages/backend/src/payment-method/ilp/quote-details/service.test.ts +++ b/packages/backend/src/payment-method/ilp/quote-details/service.test.ts @@ -55,7 +55,6 @@ describe('IlpQuoteDetails Service', (): void => { const foundIlpQuote = await ilpQuoteDetailsService.getByQuoteId( quote.id ) - console.log({ foundIlpQuote }) expect(foundIlpQuote).toBeDefined() }) From 347f9b02aad5f935c8ea3b62b21c5112d79584ea Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:21:23 -0400 Subject: [PATCH 33/64] fix(backend): rm ilpQuoteDetails joins from quote service --- .../src/open_payments/quote/service.test.ts | 89 +++++++++++++------ .../src/open_payments/quote/service.ts | 13 +-- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 086ee5bf58..2a2558b43d 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -36,6 +36,7 @@ import { PaymentMethodHandlerErrorCode } from '../../payment-method/handler/errors' import { Receiver } from '../receiver/model' +import { IlpQuoteDetailsService } from '../../payment-method/ilp/quote-details/service' describe('QuoteService', (): void => { let deps: IocContract @@ -47,6 +48,7 @@ describe('QuoteService', (): void => { let sendingWalletAddress: MockWalletAddress let receivingWalletAddress: MockWalletAddress let config: IAppConfig + let ilpQuoteDetailsService: IlpQuoteDetailsService let receiverGet: typeof receiverService.get let receiverGetSpy: jest.SpyInstance< Promise, @@ -86,6 +88,7 @@ describe('QuoteService', (): void => { quoteService = await deps.use('quoteService') paymentMethodHandlerService = await deps.use('paymentMethodHandlerService') receiverService = await deps.use('receiverService') + ilpQuoteDetailsService = await deps.use('ilpQuoteDetailsService') }) beforeEach(async (): Promise => { @@ -145,8 +148,32 @@ describe('QuoteService', (): void => { withFee: true, method: 'ilp' }), - get: (options) => quoteService.get(options), - list: (options) => quoteService.getWalletAddressPage(options) + get: async (options) => { + const quote = await quoteService.get(options) + assert.ok(!isQuoteError(quote)) + + if (!quote) { + return + } + + quote.ilpQuoteDetails = await ilpQuoteDetailsService.getByQuoteId( + quote.id + ) + + return quote + }, + list: async (options) => { + const quotes = await quoteService.getWalletAddressPage(options) + + const quotesWithDetails = await Promise.all( + quotes.map(async (q) => { + q.ilpQuoteDetails = await ilpQuoteDetailsService.getByQuoteId(q.id) + return q + }) + ) + + return quotesWithDetails + } }) }) @@ -252,11 +279,13 @@ describe('QuoteService', (): void => { client: client || null }) - await expect( - quoteService.get({ - id: quote.id - }) - ).resolves.toEqual(quote) + const foundQuote = await quoteService.get({ + id: quote.id + }) + assert(foundQuote) + foundQuote.ilpQuoteDetails = + await ilpQuoteDetailsService.getByQuoteId(quote.id) + expect(foundQuote).toEqual(quote) } ) @@ -341,11 +370,13 @@ describe('QuoteService', (): void => { client: client || null }) - await expect( - quoteService.get({ - id: quote.id - }) - ).resolves.toEqual(quote) + const foundQuote = await quoteService.get({ + id: quote.id + }) + assert(foundQuote) + foundQuote.ilpQuoteDetails = + await ilpQuoteDetailsService.getByQuoteId(quote.id) + expect(foundQuote).toEqual(quote) } ) } @@ -455,21 +486,27 @@ describe('QuoteService', (): void => { .spyOn(paymentMethodHandlerService, 'getQuote') .mockResolvedValueOnce(mockedQuote) - await expect( - quoteService.create({ - walletAddressId: sendingWalletAddress.id, - receiver: receiver.incomingPayment!.id, - method: 'ilp' - }) - ).resolves.toMatchObject({ + const quote = await quoteService.create({ + walletAddressId: sendingWalletAddress.id, + receiver: receiver.incomingPayment!.id, + method: 'ilp' + }) + assert.ok(!isQuoteError(quote)) + + const ilpQuoteDetails = await ilpQuoteDetailsService.getByQuoteId( + quote.id + ) + + expect(quote).toMatchObject({ debitAmount: mockedQuote.debitAmount, - receiveAmount: receiver.incomingAmount, - ilpQuoteDetails: { - maxPacketAmount: BigInt('9223372036854775807'), - lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - minExchangeRate: Pay.Ratio.from(10 ** 20) - } + receiveAmount: receiver.incomingAmount + }) + + expect(ilpQuoteDetails).toMatchObject({ + maxPacketAmount: BigInt('9223372036854775807'), + lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), + highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), + minExchangeRate: Pay.Ratio.from(10 ** 20) }) }) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 9c40ec2a43..493d1c5ba5 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -56,7 +56,7 @@ async function getQuote( ): Promise { return Quote.query(deps.knex) .get(options) - .withGraphFetched('[asset, fee, walletAddress, ilpQuoteDetails]') + .withGraphFetched('[asset, fee, walletAddress]') } interface QuoteOptionsBase { @@ -113,8 +113,9 @@ async function createQuote( try { const receiver = await resolveReceiver(deps, options) + const paymentMethod = receiver.isLocal ? 'LOCAL' : 'ILP' const quote = await deps.paymentMethodHandlerService.getQuote( - receiver.isLocal ? 'LOCAL' : 'ILP', + paymentMethod, { walletAddress, receiver, @@ -140,7 +141,7 @@ async function createQuote( estimatedExchangeRate: quote.estimatedExchangeRate } - if (!receiver.isLocal) { + if (paymentMethod === 'ILP') { const maxPacketAmount = quote.additionalFields.maxPacketAmount as bigint graph.ilpQuoteDetails = { maxPacketAmount: @@ -156,7 +157,9 @@ async function createQuote( return await Quote.transaction(deps.knex, async (trx) => { const createdQuote = await Quote.query(trx) .insertGraphAndFetch(graph) - .withGraphFetched('[asset, fee, walletAddress, ilpQuoteDetails]') + .withGraphFetched('[asset, fee, walletAddress]') + + console.log({ createdQuote }) return await finalizeQuote( { @@ -449,5 +452,5 @@ async function getWalletAddressPage( ): Promise { return await Quote.query(deps.knex) .list(options) - .withGraphFetched('[asset, fee, walletAddress, ilpQuoteDetails]') + .withGraphFetched('[asset, fee, walletAddress]') } From 37d399e05e023a40bb1eb074b4038e3d5d770125 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:27:37 -0400 Subject: [PATCH 34/64] chore(backend): rm console.log --- packages/backend/src/open_payments/quote/service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 493d1c5ba5..6d781340ea 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -159,8 +159,6 @@ async function createQuote( .insertGraphAndFetch(graph) .withGraphFetched('[asset, fee, walletAddress]') - console.log({ createdQuote }) - return await finalizeQuote( { ...deps, From 78d42b68eb8bbc0d7f0dab2665f98607da5c5679 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:22:14 -0400 Subject: [PATCH 35/64] refactor(backend): rename sourceAmount to debitAmountMinusFees --- .../Create Quote.bru | 4 ---- .../Create Receiver (remote Incoming Payment).bru | 2 +- .../20240916185330_add_quote_source_amount.js | 4 ++-- packages/backend/src/open_payments/quote/model.ts | 2 +- packages/backend/src/open_payments/quote/service.ts | 4 ++-- packages/backend/src/payment-method/local/service.ts | 10 +++++----- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru index 73b598a469..4b293be810 100644 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru @@ -16,11 +16,7 @@ body:graphql { quote { createdAt expiresAt - highEstimatedExchangeRate id - lowEstimatedExchangeRate - maxPacketAmount - minExchangeRate walletAddressId receiveAmount { assetCode diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru index 03bc186665..da9b731cf6 100644 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru @@ -47,7 +47,7 @@ body:graphql:vars { "assetScale": 2, "value": 500 }, - "walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/bhamchest" + "walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/lrossi" } } } diff --git a/packages/backend/migrations/20240916185330_add_quote_source_amount.js b/packages/backend/migrations/20240916185330_add_quote_source_amount.js index f86094ad9a..646aa70043 100644 --- a/packages/backend/migrations/20240916185330_add_quote_source_amount.js +++ b/packages/backend/migrations/20240916185330_add_quote_source_amount.js @@ -4,7 +4,7 @@ */ exports.up = function (knex) { return knex.schema.alterTable('quotes', function (table) { - table.bigInteger('sourceAmount').nullable() + table.bigInteger('debitAmountMinusFees').nullable() }) } @@ -14,6 +14,6 @@ exports.up = function (knex) { */ exports.down = function (knex) { return knex.schema.alterTable('quotes', function (table) { - table.dropColumn('sourceAmount') + table.dropColumn('debitAmountMinusFees') }) } diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index 48619ba080..cc01498759 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -26,7 +26,7 @@ export class Quote extends WalletAddressSubresource { public fee?: Fee public ilpQuoteDetails?: IlpQuoteDetails - public sourceAmount?: bigint + public debitAmountMinusFees?: bigint static get relationMappings() { return { diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 6d781340ea..0ac569a885 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -429,8 +429,8 @@ async function finalizeQuote( : calculateFixedDeliveryQuoteAmounts(deps, quote) const patchOptions = { - sourceAmount: maxReceiveAmountValue - ? // TODO: change fixed send to return the sourceAmount if I can get new calc working + debitAmountMinusFees: maxReceiveAmountValue + ? // TODO: change fixed send to return the debitAmountMinusFees if I can get new calc working quote.debitAmount.value - (quote.fee?.calculate(quote.debitAmount.value) ?? 0n) : quote.debitAmount.value, diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 1bb3da62f4..937fbff661 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -155,12 +155,12 @@ async function pay( deps: ServiceDependencies, options: PayOptions ): Promise { - // TODO: use finalDebitAmount instead of sourceAmount? pass sourceAmount in as finalDebitAmount? + // TODO: use finalDebitAmount instead of debitAmountMinusFees? pass debitAmountMinusFees in as finalDebitAmount? const { outgoingPayment, receiver, finalReceiveAmount } = options - if (!outgoingPayment.quote.sourceAmount) { - // TODO: handle this better. perhaps sourceAmount should not be nullable? + if (!outgoingPayment.quote.debitAmountMinusFees) { + // TODO: handle this better. perhaps debitAmountMinusFees should not be nullable? // If throwing an error, follow existing patterns - throw new Error('could do local pay, missing sourceAmount') + throw new Error('could do local pay, missing debitAmountMinusFees') } // Cannot directly use receiver/receiver.incomingAccount for destinationAccount. @@ -209,7 +209,7 @@ async function pay( // let feeAmount: number | null // baseDebitAmount excludes fees - const sourceAmount = outgoingPayment.quote.sourceAmount //finalDebitAmount + const sourceAmount = outgoingPayment.quote.debitAmountMinusFees //finalDebitAmount // if (outgoingPayment.quote.feeId) { // const fee = await deps.feeService.getById(outgoingPayment.quote.feeId) From a96d6543ef67c862e9aa602fcb87a4def7d34f97 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:48:21 -0400 Subject: [PATCH 36/64] chore(backend): cleanup, rm unused fee method --- packages/backend/src/fee/service.test.ts | 18 ------ packages/backend/src/fee/service.ts | 11 +--- .../src/payment-method/local/service.ts | 64 +------------------ 3 files changed, 2 insertions(+), 91 deletions(-) diff --git a/packages/backend/src/fee/service.test.ts b/packages/backend/src/fee/service.test.ts index a26ac12603..e57068f7e3 100644 --- a/packages/backend/src/fee/service.test.ts +++ b/packages/backend/src/fee/service.test.ts @@ -156,24 +156,6 @@ describe('Fee Service', (): void => { expect(latestFee).toBeUndefined() }) }) - describe('getById', (): void => { - it('should get fee by id', async (): Promise => { - const fee = await Fee.query().insertAndFetch({ - assetId: asset.id, - type: FeeType.Receiving, - basisPointFee: 100, - fixedFee: BigInt(100) - }) - - const foundFee = await feeService.getById(fee.id) - expect(foundFee).toMatchObject(fee) - }) - - it('should return undefined when not foudn fee by id', async (): Promise => { - const foundFee = await feeService.getById(v4()) - expect(foundFee).toBe(undefined) - }) - }) describe('Fee pagination', (): void => { getPageTests({ diff --git a/packages/backend/src/fee/service.ts b/packages/backend/src/fee/service.ts index 9fc24a4de1..4979cb18a4 100644 --- a/packages/backend/src/fee/service.ts +++ b/packages/backend/src/fee/service.ts @@ -21,7 +21,6 @@ export interface FeeService { sortOrder?: SortOrder ): Promise getLatestFee(assetId: string, type: FeeType): Promise - getById(feeId: string): Promise } type ServiceDependencies = BaseService @@ -44,8 +43,7 @@ export async function createFeeService({ sortOrder = SortOrder.Desc ) => getFeesPage(deps, assetId, pagination, sortOrder), getLatestFee: (assetId: string, type: FeeType) => - getLatestFee(deps, assetId, type), - getById: (feeId: string) => getById(deps, feeId) + getLatestFee(deps, assetId, type) } } @@ -73,13 +71,6 @@ async function getLatestFee( .first() } -async function getById( - deps: ServiceDependencies, - feeId: string -): Promise { - return await Fee.query(deps.knex).findById(feeId) -} - async function createFee( deps: ServiceDependencies, { assetId, type, fee }: CreateOptions diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 937fbff661..7ff3bc53c7 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -206,78 +206,16 @@ async function pay( } } - // let feeAmount: number | null - - // baseDebitAmount excludes fees - const sourceAmount = outgoingPayment.quote.debitAmountMinusFees //finalDebitAmount - - // if (outgoingPayment.quote.feeId) { - // const fee = await deps.feeService.getById(outgoingPayment.quote.feeId) - - // if (!fee) { - // throw new PaymentMethodHandlerError( - // 'Received error during local payment', - // { - // description: 'Quote fee could not be found by id', - // retryable: false - // } - // ) - // } - - // // TODO: store this on quote instead? - // // Original debit amount value excluding fees - // sourceAmount = BigInt( - // Math.floor( - // Number(finalDebitAmount - fee.fixedFee) / - // (1 + fee.basisPointFee / 10000) - // ) - // ) - // } - - // TODO: is this going to work for fixed send/delivery? - // At this stage does that concept still have an effect? in ilp pay I - // think its all fixed delivery here... - - // [x] Use finalDebitAmount - fees for sourceAmount and finalReceiveAmount for destinationAmount - // ilp pay is sending min of debitAmount/converted min delivery (finalReceiveAmount) - - // extra things to verify: - // - [X] before implenting this, ensure transfersToCreateOrError 110 amt destinationAccontId is the us asset account - // - IT DOES MATCH. the destinationAccontId for the 110 amt corresponds to USD asset id - // - 110 record had destinationAccountId: 7b628461-0d45-4cd5-9b2d-ec6de9fe8159 - // - ledgerAccount had 2 records with accountRef: 7b628461-0d45-4cd5-9b2d-ec6de9fe8159. - // 1 with type SETTLEMENT and another with type LIQUIDITY_ASSET - // - USD asset had id of 7b628461-0d45-4cd5-9b2d-ec6de9fe8159 - // - [ ] after implementing, ensure there are 3 transfers made for cross currency: - // - 1 outgoing payment to usd asset - // - 1 usd asset to fx asset - // - 1 fx asset to incoming payment - - // const fee = await deps.feeService.get(outgoingPayment.quote.feeId) + const sourceAmount = outgoingPayment.quote.debitAmountMinusFees //finalDebitAmount? const transferOptions: TransferOptions = { - // outgoingPayment.quote.debitAmount = 610n - // outgoingPayment.quote.receiveAmount = 500n - // ^ consistent with ilp payment method sourceAccount: outgoingPayment, destinationAccount: incomingPayment, - // finalDebitAmount: 610n - // finalReceiveAmount: 500n - // ^ consistent with amounts passed into ilpPaymentMethodService.pay and Pay.pay - // ^ inconsisten with transfer passed into createTrasnfer in balance middleware - // - that is sourceAmount: 500, destinationAmount: 500 - // - this transfer gets 500 from the request.prepare (formed from Pay.pay?). request.prepare - // is the sourceAmount then the destinationAmount is derived from it with rates.convert sourceAmount, destinationAmount: finalReceiveAmount, transferType: TransferType.TRANSFER } - // Here, createTransfer gets debitAmount = 610 and receiveAmount = 500n - // however, in ilp balance middleware its 500/500. In ilpPaymentService.pay, - // Pay.pay is called with the same sort of quote as here. debitAmount = 617 (higher due to slippage?) - // and receiveAmount = 500. There is some adjustment happening between Pay.pay (in Pay.pay?) - // (in ilpPaymentMethodService.pay) and createTransfer in the connector balance middleware const trxOrError = await deps.accountingService.createTransfer(transferOptions) From 7941f0564a201b5ded068145a60cce585e11dfdd Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:49:01 -0400 Subject: [PATCH 37/64] test(backend): add local payment tests --- .../payment/outgoing/lifecycle.ts | 28 +- .../payment-method/handler/service.test.ts | 2 +- .../src/payment-method/local/service.test.ts | 895 +++++++----------- .../src/payment-method/local/service.ts | 108 ++- packages/backend/src/rates/service.ts | 6 +- packages/backend/src/tests/quote.ts | 1 + 6 files changed, 431 insertions(+), 609 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index 655d2d9d8b..e27fa4eeca 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -77,15 +77,33 @@ export async function handleSending( } const payStartTime = Date.now() - await deps.paymentMethodHandlerService.pay( - receiver.isLocal ? 'LOCAL' : 'ILP', - { + if (receiver.isLocal) { + if ( + !payment.quote.debitAmountMinusFees || + payment.quote.debitAmountMinusFees <= BigInt(0) + ) { + deps.logger.error( + { + debitAmountMinusFees: payment.quote.debitAmountMinusFees + }, + 'handleSending: quote.debitAmountMinusFees invalid' + ) + throw LifecycleError.BadState + } + await deps.paymentMethodHandlerService.pay('LOCAL', { + receiver, + outgoingPayment: payment, + finalDebitAmount: payment.quote.debitAmountMinusFees, + finalReceiveAmount: maxReceiveAmount + }) + } else { + await deps.paymentMethodHandlerService.pay('ILP', { receiver, outgoingPayment: payment, finalDebitAmount: maxDebitAmount, finalReceiveAmount: maxReceiveAmount - } - ) + }) + } const payEndTime = Date.now() if (deps.telemetry) { diff --git a/packages/backend/src/payment-method/handler/service.test.ts b/packages/backend/src/payment-method/handler/service.test.ts index a28e20b2b4..987f07354a 100644 --- a/packages/backend/src/payment-method/handler/service.test.ts +++ b/packages/backend/src/payment-method/handler/service.test.ts @@ -67,7 +67,7 @@ describe('PaymentMethodHandlerService', (): void => { expect(ilpPaymentServiceGetQuoteSpy).toHaveBeenCalledWith(options) }) - test('calls lcaolPaymentService for local payment type', async (): Promise => { + test('calls localPaymentService for local payment type', async (): Promise => { const asset = await createAsset(deps) const walletAddress = await createWalletAddress(deps, { assetId: asset.id diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index d3226a2420..2f52691ef1 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -21,6 +21,10 @@ import { truncateTables } from '../../tests/tableManager' import { createOutgoingPaymentWithReceiver } from '../../tests/outgoingPayment' import { OutgoingPayment } from '../../open_payments/payment/outgoing/model' import { IncomingPayment } from '../../open_payments/payment/incoming/model' +import { IncomingPaymentService } from '../../open_payments/payment/incoming/service' +import { errorToMessage, TransferError } from '../../accounting/errors' +import { PaymentMethodHandlerError } from '../handler/errors' +import { ConvertError } from '../../rates/service' const nock = (global as unknown as { nock: typeof import('nock') }).nock @@ -29,7 +33,7 @@ describe('LocalPaymentService', (): void => { let appContainer: TestContainer let localPaymentService: LocalPaymentService let accountingService: AccountingService - // let config: IAppConfig + let incomingPaymentService: IncomingPaymentService const exchangeRatesUrl = 'https://example-rates.com' @@ -44,9 +48,9 @@ describe('LocalPaymentService', (): void => { }) appContainer = await createTestApp(deps) - // config = await deps.use('config') localPaymentService = await deps.use('localPaymentService') accountingService = await deps.use('accountingService') + incomingPaymentService = await deps.use('incomingPaymentService') }) beforeEach(async (): Promise => { @@ -84,274 +88,180 @@ describe('LocalPaymentService', (): void => { }) describe('getQuote', (): void => { - // test('calls rates service with correct base asset', async (): Promise => { - // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) - - // const options: StartQuoteOptions = { - // walletAddress: walletAddressMap['USD'], - // receiver: await createReceiver(deps, walletAddressMap['USD']), - // debitAmount: { - // assetCode: 'USD', - // assetScale: 2, - // value: 100n - // } - // } - - // const ratesService = await deps.use('ratesService') - // const ratesServiceSpy = jest.spyOn(ratesService, 'rates') - - // await ilpPaymentService.getQuote(options) - - // expect(ratesServiceSpy).toHaveBeenCalledWith('USD') - // ratesScope.done() - // }) - - // test('fails on rate service error', async (): Promise => { - // const ratesService = await deps.use('ratesService') - // jest - // .spyOn(ratesService, 'rates') - // .mockImplementation(() => Promise.reject(new Error('fail'))) - - // expect.assertions(4) - // try { - // await ilpPaymentService.getQuote({ - // walletAddress: walletAddressMap['USD'], - // receiver: await createReceiver(deps, walletAddressMap['USD']), - // debitAmount: { - // assetCode: 'USD', - // assetScale: 2, - // value: 100n - // } - // }) - // } catch (err) { - // expect(err).toBeInstanceOf(PaymentMethodHandlerError) - // expect((err as PaymentMethodHandlerError).message).toBe( - // 'Received error during ILP quoting' - // ) - // expect((err as PaymentMethodHandlerError).description).toBe( - // 'Could not get rates from service' - // ) - // expect((err as PaymentMethodHandlerError).retryable).toBe(false) - // } - // }) - - // test('returns all fields correctly', async (): Promise => { - // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) - - // const options: StartQuoteOptions = { - // walletAddress: walletAddressMap['USD'], - // receiver: await createReceiver(deps, walletAddressMap['USD']), - // debitAmount: { - // assetCode: 'USD', - // assetScale: 2, - // value: 100n - // } - // } - - // await expect(ilpPaymentService.getQuote(options)).resolves.toEqual({ - // receiver: options.receiver, - // walletAddress: options.walletAddress, - // debitAmount: { - // assetCode: 'USD', - // assetScale: 2, - // value: 100n - // }, - // receiveAmount: { - // assetCode: 'USD', - // assetScale: 2, - // value: 99n - // }, - // estimatedExchangeRate: expect.any(Number), - // additionalFields: { - // minExchangeRate: expect.any(Pay.Ratio), - // highEstimatedExchangeRate: expect.any(Pay.Ratio), - // lowEstimatedExchangeRate: expect.any(Pay.Ratio), - // maxPacketAmount: BigInt(Pay.Int.MAX_U64.toString()) - // } - // }) - // ratesScope.done() - // }) - - // test('uses receiver.incomingAmount if receiveAmount is not provided', async (): Promise => { - // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) - - // const incomingAmount = { - // assetCode: 'USD', - // assetScale: 2, - // value: 100n - // } - - // const options: StartQuoteOptions = { - // walletAddress: walletAddressMap['USD'], - // receiver: await createReceiver(deps, walletAddressMap['USD'], { - // incomingAmount - // }) - // } - - // const ilpStartQuoteSpy = jest.spyOn(Pay, 'startQuote') - - // await expect(ilpPaymentService.getQuote(options)).resolves.toMatchObject({ - // receiveAmount: { - // assetCode: 'USD', - // assetScale: 2, - // value: incomingAmount?.value - // } - // }) - - // expect(ilpStartQuoteSpy).toHaveBeenCalledWith( - // expect.objectContaining({ - // amountToDeliver: incomingAmount?.value - // }) - // ) - // ratesScope.done() - // }) - - // test('fails if slippage too high', async (): Promise => - // withConfigOverride( - // () => config, - // { slippage: 101 }, - // async () => { - // mockRatesApi(exchangeRatesUrl, () => ({})) - - // expect.assertions(4) - // try { - // await ilpPaymentService.getQuote({ - // walletAddress: walletAddressMap['USD'], - // receiver: await createReceiver(deps, walletAddressMap['USD']), - // debitAmount: { - // assetCode: 'USD', - // assetScale: 2, - // value: 100n - // } - // }) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Received error during ILP quoting' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // Pay.PaymentError.InvalidSlippage - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // } - // } - // )()) - - // test('throws if quote returns invalid maxSourceAmount', async (): Promise => { - // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) - - // const options: StartQuoteOptions = { - // walletAddress: walletAddressMap['USD'], - // receiver: await createReceiver(deps, walletAddressMap['USD']) - // } - - // jest.spyOn(Pay, 'startQuote').mockResolvedValueOnce({ - // maxSourceAmount: -1n - // } as Pay.Quote) - - // expect.assertions(4) - // try { - // await ilpPaymentService.getQuote(options) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Received error during ILP quoting' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // 'Maximum source amount of ILP quote is non-positive' - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // } - - // ratesScope.done() - // }) - - // test('throws if quote returns invalid minDeliveryAmount', async (): Promise => { - // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) - - // const options: StartQuoteOptions = { - // walletAddress: walletAddressMap['USD'], - // receiver: await createReceiver(deps, walletAddressMap['USD'], { - // incomingAmount: { - // assetCode: 'USD', - // assetScale: 2, - // value: 100n - // } - // }) - // } - - // jest.spyOn(Pay, 'startQuote').mockResolvedValueOnce({ - // maxSourceAmount: 1n, - // minDeliveryAmount: -1n - // } as Pay.Quote) - - // expect.assertions(5) - // try { - // await ilpPaymentService.getQuote(options) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Received error during ILP quoting' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // 'Minimum delivery amount of ILP quote is non-positive' - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // expect((error as PaymentMethodHandlerError).code).toBe( - // PaymentMethodHandlerErrorCode.QuoteNonPositiveReceiveAmount - // ) - // } - - // ratesScope.done() - // }) - - // test('throws if quote returns with a non-positive estimated delivery amount', async (): Promise => { - // const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) - - // const options: StartQuoteOptions = { - // walletAddress: walletAddressMap['USD'], - // receiver: await createReceiver(deps, walletAddressMap['USD']) - // } - - // jest.spyOn(Pay, 'startQuote').mockResolvedValueOnce({ - // maxSourceAmount: 10n, - // highEstimatedExchangeRate: Pay.Ratio.from(0.099) - // } as Pay.Quote) - - // expect.assertions(5) - // try { - // await ilpPaymentService.getQuote(options) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Received error during ILP quoting' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // 'Estimated receive amount of ILP quote is non-positive' - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // expect((error as PaymentMethodHandlerError).code).toBe( - // PaymentMethodHandlerErrorCode.QuoteNonPositiveReceiveAmount - // ) - // } - - // ratesScope.done() - // }) + test('fails on unknown rate service error', async (): Promise => { + const ratesService = await deps.use('ratesService') + jest + .spyOn(ratesService, 'convert') + .mockImplementation(() => Promise.reject(new Error('fail'))) + + expect.assertions(4) + try { + await localPaymentService.getQuote({ + walletAddress: walletAddressMap['USD'], + receiver: await createReceiver(deps, walletAddressMap['USD']), + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 100n + } + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Received error during local quoting' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + 'Unknown error while attempting to convert rates' + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } + }) + + test('fails on rate service error', async (): Promise => { + const ratesService = await deps.use('ratesService') + jest + .spyOn(ratesService, 'convert') + .mockImplementation(() => + Promise.resolve(ConvertError.InvalidDestinationPrice) + ) + + expect.assertions(4) + try { + await localPaymentService.getQuote({ + walletAddress: walletAddressMap['USD'], + receiver: await createReceiver(deps, walletAddressMap['USD']), + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 100n + } + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Received error during local quoting' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + 'Failed to convert debitAmount to receive amount' + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } + }) + + test('returns all fields correctly', async (): Promise => { + const options: StartQuoteOptions = { + walletAddress: walletAddressMap['USD'], + receiver: await createReceiver(deps, walletAddressMap['USD']), + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 100n + } + } + + await expect(localPaymentService.getQuote(options)).resolves.toEqual({ + receiver: options.receiver, + walletAddress: options.walletAddress, + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 100n + }, + receiveAmount: { + assetCode: 'USD', + assetScale: 2, + value: 100n + }, + estimatedExchangeRate: 1, + additionalFields: {} + }) + }) + + test('fails if debit amount is non-positive', async (): Promise => { + expect.assertions(4) + try { + await localPaymentService.getQuote({ + walletAddress: walletAddressMap['USD'], + receiver: await createReceiver(deps, walletAddressMap['USD']), + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 0n + } + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Received error during local quoting' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + 'debit amount of local quote is non-positive' + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } + }) + test('fails if receive amount is non-positive', async (): Promise => { + const ratesService = await deps.use('ratesService') + jest + .spyOn(ratesService, 'convert') + .mockImplementation(() => Promise.resolve(100n)) + expect.assertions(4) + try { + await localPaymentService.getQuote({ + walletAddress: walletAddressMap['USD'], + receiver: await createReceiver(deps, walletAddressMap['USD']), + receiveAmount: { + assetCode: 'USD', + assetScale: 2, + value: 0n + } + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Received error during local quoting' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + 'receive amount of local quote is non-positive' + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } + }) + + test('uses receiver.incomingAmount if receiveAmount is not provided', async (): Promise => { + const incomingAmount = { + assetCode: 'USD', + assetScale: 2, + value: 100n + } + + const options: StartQuoteOptions = { + walletAddress: walletAddressMap['USD'], + receiver: await createReceiver(deps, walletAddressMap['USD'], { + incomingAmount + }) + } + + await expect( + localPaymentService.getQuote(options) + ).resolves.toMatchObject({ + receiveAmount: { + assetCode: 'USD', + assetScale: 2, + value: incomingAmount.value + } + }) + }) describe('successfully gets local quote', (): void => { describe('with incomingAmount', () => { test.each` incomingAssetCode | incomingAmountValue | debitAssetCode | expectedDebitAmount | exchangeRate | description + ${'USD'} | ${100n} | ${'USD'} | ${100n} | ${null} | ${'local currency'} ${'EUR'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'cross currency, same rate'} ${'EUR'} | ${100n} | ${'USD'} | ${111n} | ${0.9} | ${'cross currency, exchange rate < 1'} ${'EUR'} | ${100n} | ${'USD'} | ${50n} | ${2.0} | ${'cross currency, exchange rate > 1'} `( - // TODO: seperate test with this case (and dont mock the mockRatesApi). - // no `each` needed. - // test.each` - // incomingAssetCode | incomingAmountValue | debitAssetCode | expectedDebitAmount | exchangeRate | description - // ${'USD'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'same currency'} - // `( '$description', async ({ incomingAssetCode, @@ -360,15 +270,13 @@ describe('LocalPaymentService', (): void => { expectedDebitAmount, exchangeRate }): Promise => { - // TODO: investigate this further. - // - Is the expectedDebitAmount correct in these tests? - // - Is the mockRatesApi return different than ilp getQuote test (which is [incomingAmountAssetCode]: exchangeRate) - // because we do convertRatesToIlpPrices (which inverts) in ilp getQuote? (I think so...) - // - just convert the exchangeRate test arg instead of inversing here (0.9 -> 1.1., 2.0 -> .5 etc)? - // I started with the exchangeRates as they are simply because I copy/pasted from ilp getQuote tests - const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({ - [debitAssetCode]: 1 / exchangeRate - })) + let ratesScope + + if (incomingAssetCode !== debitAssetCode) { + ratesScope = mockRatesApi(exchangeRatesUrl, () => ({ + [debitAssetCode]: 1 / exchangeRate + })) + } const receivingWalletAddress = walletAddressMap[incomingAssetCode] const sendingWalletAddress = walletAddressMap[debitAssetCode] @@ -397,7 +305,7 @@ describe('LocalPaymentService', (): void => { value: incomingAmountValue } }) - ratesScope.done() + ratesScope && ratesScope.done() } ) }) @@ -405,16 +313,11 @@ describe('LocalPaymentService', (): void => { describe('with debitAmount', () => { test.each` debitAssetCode | debitAmountValue | incomingAssetCode | expectedReceiveAmount | exchangeRate | description + ${'USD'} | ${100n} | ${'USD'} | ${100n} | ${null} | ${'local currency'} ${'EUR'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'cross currency, same rate'} ${'USD'} | ${100n} | ${'EUR'} | ${90n} | ${0.9} | ${'cross currency, exchange rate < 1'} ${'USD'} | ${100n} | ${'EUR'} | ${200n} | ${2.0} | ${'cross currency, exchange rate > 1'} `( - // TODO: seperate test with this case (and dont mock the mockRatesApi). - // no `each` needed. - // test.each` - // debitAssetCode | debitAmountValue | incomingAssetCode | expectedReceiveAmount | exchangeRate | description - // ${'USD'} | ${100n} | ${'USD'} | ${100n} | ${1.0} | ${'same currency'} - // `( '$description', async ({ incomingAssetCode, @@ -423,9 +326,13 @@ describe('LocalPaymentService', (): void => { expectedReceiveAmount, exchangeRate }): Promise => { - const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({ - [incomingAssetCode]: exchangeRate - })) + let ratesScope + + if (debitAssetCode !== incomingAssetCode) { + ratesScope = mockRatesApi(exchangeRatesUrl, () => ({ + [incomingAssetCode]: exchangeRate + })) + } const receivingWalletAddress = walletAddressMap[incomingAssetCode] const sendingWalletAddress = walletAddressMap[debitAssetCode] @@ -454,7 +361,7 @@ describe('LocalPaymentService', (): void => { value: expectedReceiveAmount } }) - ratesScope.done() + ratesScope && ratesScope.done() } ) }) @@ -462,25 +369,6 @@ describe('LocalPaymentService', (): void => { }) describe('pay', (): void => { - // function mockIlpPay( - // overrideQuote: Partial, - // error?: Pay.PaymentError - // ): jest.SpyInstance< - // Promise, - // [options: Pay.PayOptions] - // > { - // return jest - // .spyOn(Pay, 'pay') - // .mockImplementationOnce(async (opts: Pay.PayOptions) => { - // const res = await Pay.pay({ - // ...opts, - // quote: { ...opts.quote, ...overrideQuote } - // }) - // if (error) res.error = error - // return res - // }) - // } - async function validateBalances( outgoingPayment: OutgoingPayment, incomingPayment: IncomingPayment, @@ -530,293 +418,158 @@ describe('LocalPaymentService', (): void => { }) }) - test.only('succesfully make local payment with fee', async (): Promise => { - // for this case, the underyling outgoing payment that gets created should have a quote that with amounts - // that look like: - // { - // "id": "a6a157d7-93ab-4104-b590-3cae00a30798", - // "walletAddressId": "9683a8bf-2a24-4dc1-853e-9d11d6681115", - // "receiver": "https://cloud-nine-wallet-backend/incoming-payments/c1617263-3b29-4d6b-9561-a5723b3e16ac", - // "debitAmount": { - // "value": "610", - // "assetCode": "USD", - // "assetScale": 2 - // }, - // "receiveAmount": { - // "value": "500", - // "assetCode": "USD", - // "assetScale": 2 - // }, - // "createdAt": "2024-08-21T17:45:07.227Z", - // "expiresAt": "2024-08-21T17:50:07.227Z" - // } - const { incomingPayment, receiver, outgoingPayment } = + test('throws error if incoming payment is not found', async (): Promise => { + const { receiver, outgoingPayment } = await createOutgoingPaymentWithReceiver(deps, { sendingWalletAddress: walletAddressMap['USD'], receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { debitAmount: { - value: 610n, + value: 100n, assetScale: walletAddressMap['USD'].asset.scale, assetCode: walletAddressMap['USD'].asset.code } } }) - expect(true).toBe(false) + jest.spyOn(incomingPaymentService, 'get').mockResolvedValueOnce(undefined) - const payResponse = await localPaymentService.pay({ - receiver, - outgoingPayment, - finalDebitAmount: 100n, - finalReceiveAmount: 100n - }) + expect.assertions(4) + try { + await localPaymentService.pay({ + receiver, + outgoingPayment, + finalDebitAmount: 100n, + finalReceiveAmount: 100n + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Received error during local payment' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + 'Incoming payment not found from receiver' + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } + }) - expect(payResponse).toBe(undefined) + test('throws InsufficientBalance when balance is insufficient', async (): Promise => { + const { receiver, outgoingPayment } = + await createOutgoingPaymentWithReceiver(deps, { + sendingWalletAddress: walletAddressMap['USD'], + receivingWalletAddress: walletAddressMap['USD'], + method: 'ilp', + quoteOptions: { + debitAmount: { + value: 100n, + assetScale: walletAddressMap['USD'].asset.scale, + assetCode: walletAddressMap['USD'].asset.code + } + } + }) - await validateBalances(outgoingPayment, incomingPayment, { - amountSent: 100n, - amountReceived: 100n - }) + jest + .spyOn(accountingService, 'createTransfer') + .mockResolvedValueOnce(TransferError.InsufficientBalance) + + expect.assertions(4) + try { + await localPaymentService.pay({ + receiver, + outgoingPayment, + finalDebitAmount: 100n, + finalReceiveAmount: 100n + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Received error during local payment' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + errorToMessage[TransferError.InsufficientBalance] + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } }) - // test('successfully streams between accounts', async (): Promise => { - // const { incomingPayment, receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // await expect( - // ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 100n, - // finalReceiveAmount: 100n - // }) - // ).resolves.toBeUndefined() - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 100n, - // amountReceived: 100n - // }) - // }) - - // test('partially streams between accounts, then streams to completion', async (): Promise => { - // const { incomingPayment, receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // exchangeRate: 1, - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // mockIlpPay( - // { maxSourceAmount: 5n, minDeliveryAmount: 5n }, - // Pay.PaymentError.ClosedByReceiver - // ) - - // await expect( - // ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 100n, - // finalReceiveAmount: 100n - // }) - // ).rejects.toThrow(PaymentMethodHandlerError) - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 5n, - // amountReceived: 5n - // }) - - // await expect( - // ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 100n - 5n, - // finalReceiveAmount: 100n - 5n - // }) - // ).resolves.toBeUndefined() - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 100n, - // amountReceived: 100n - // }) - // }) - - // test('throws if invalid finalDebitAmount', async (): Promise => { - // const { incomingPayment, receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // expect.assertions(6) - // try { - // await ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 0n, - // finalReceiveAmount: 50n - // }) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Could not start ILP streaming' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // 'Invalid finalDebitAmount' - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // } - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 0n, - // amountReceived: 0n - // }) - // }) - - // test('throws if invalid finalReceiveAmount', async (): Promise => { - // const { incomingPayment, receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // expect.assertions(6) - // try { - // await ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 50n, - // finalReceiveAmount: 0n - // }) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Could not start ILP streaming' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // 'Invalid finalReceiveAmount' - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // } - - // await validateBalances(outgoingPayment, incomingPayment, { - // amountSent: 0n, - // amountReceived: 0n - // }) - // }) - - // test('throws retryable ILP error', async (): Promise => { - // const { receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // mockIlpPay({}, Object.keys(retryableIlpErrors)[0] as Pay.PaymentError) - - // expect.assertions(4) - // try { - // await ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 50n, - // finalReceiveAmount: 50n - // }) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Received error during ILP pay' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // Object.keys(retryableIlpErrors)[0] - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(true) - // } - // }) - - // test('throws non-retryable ILP error', async (): Promise => { - // const { receiver, outgoingPayment } = - // await createOutgoingPaymentWithReceiver(deps, { - // sendingWalletAddress: walletAddressMap['USD'], - // receivingWalletAddress: walletAddressMap['USD'], - // method: 'ilp', - // quoteOptions: { - // debitAmount: { - // value: 100n, - // assetScale: walletAddressMap['USD'].asset.scale, - // assetCode: walletAddressMap['USD'].asset.code - // } - // } - // }) - - // const nonRetryableIlpError = Object.values(Pay.PaymentError).find( - // (error) => !retryableIlpErrors[error] - // ) - - // mockIlpPay({}, nonRetryableIlpError) - - // expect.assertions(4) - // try { - // await ilpPaymentService.pay({ - // receiver, - // outgoingPayment, - // finalDebitAmount: 50n, - // finalReceiveAmount: 50n - // }) - // } catch (error) { - // expect(error).toBeInstanceOf(PaymentMethodHandlerError) - // expect((error as PaymentMethodHandlerError).message).toBe( - // 'Received error during ILP pay' - // ) - // expect((error as PaymentMethodHandlerError).description).toBe( - // nonRetryableIlpError - // ) - // expect((error as PaymentMethodHandlerError).retryable).toBe(false) - // } - // }) + test('throws InsufficientLiquidityError when liquidity is insufficient', async (): Promise => { + const { receiver, outgoingPayment } = + await createOutgoingPaymentWithReceiver(deps, { + sendingWalletAddress: walletAddressMap['USD'], + receivingWalletAddress: walletAddressMap['USD'], + method: 'ilp', + quoteOptions: { + debitAmount: { + value: 100n, + assetScale: walletAddressMap['USD'].asset.scale, + assetCode: walletAddressMap['USD'].asset.code + } + } + }) + + jest + .spyOn(accountingService, 'createTransfer') + .mockResolvedValueOnce(TransferError.InsufficientLiquidity) + + expect.assertions(4) + try { + await localPaymentService.pay({ + receiver, + outgoingPayment, + finalDebitAmount: 100n, + finalReceiveAmount: 100n + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Received error during local payment' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + errorToMessage[TransferError.InsufficientLiquidity] + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } + }) + + test('throws generic error for unknown transfer error', async (): Promise => { + const { receiver, outgoingPayment } = + await createOutgoingPaymentWithReceiver(deps, { + sendingWalletAddress: walletAddressMap['USD'], + receivingWalletAddress: walletAddressMap['USD'], + method: 'ilp', + quoteOptions: { + debitAmount: { + value: 100n, + assetScale: walletAddressMap['USD'].asset.scale, + assetCode: walletAddressMap['USD'].asset.code + } + } + }) + + jest + .spyOn(accountingService, 'createTransfer') + .mockResolvedValueOnce('UnknownError' as any) + + expect.assertions(4) + try { + await localPaymentService.pay({ + receiver, + outgoingPayment, + finalDebitAmount: 100n, + finalReceiveAmount: 100n + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Received error during local payment' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + 'Unknown error while trying to create transfer' + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } + }) }) }) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 7ff3bc53c7..e09ea95966 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -5,9 +5,17 @@ import { StartQuoteOptions, PayOptions } from '../handler/service' -import { isConvertError, RatesService } from '../../rates/service' +import { + ConvertError, + isConvertError, + RateConvertOpts, + RatesService +} from '../../rates/service' import { IAppConfig } from '../../config/app' -import { PaymentMethodHandlerError } from '../handler/errors' +import { + PaymentMethodHandlerError, + PaymentMethodHandlerErrorCode +} from '../handler/errors' import { AccountingService, LiquidityAccountType, @@ -16,6 +24,7 @@ import { } from '../../accounting/service' import { AccountAlreadyExistsError, + errorToMessage, isTransferError, TransferError } from '../../accounting/errors' @@ -56,17 +65,39 @@ async function getQuote( let debitAmountValue: bigint let receiveAmountValue: bigint - // let estimatedExchangeRate: number + + const convert = async (opts: RateConvertOpts) => { + let converted: bigint | ConvertError + try { + converted = await deps.ratesService.convert(opts) + } catch (err) { + deps.logger.error( + { receiver, debitAmount, receiveAmount, err }, + 'Unknown error while attempting to convert rates' + ) + throw new PaymentMethodHandlerError( + 'Received error during local quoting', + { + description: 'Unknown error while attempting to convert rates', + retryable: false + } + ) + } + return converted + } if (debitAmount) { debitAmountValue = debitAmount.value - const converted = await deps.ratesService.convert({ + const converted = await convert({ sourceAmount: debitAmountValue, sourceAsset: { code: debitAmount.assetCode, scale: debitAmount.assetScale }, - destinationAsset: { code: receiver.assetCode, scale: receiver.assetScale } + destinationAsset: { + code: receiver.assetCode, + scale: receiver.assetScale + } }) if (isConvertError(converted)) { throw new PaymentMethodHandlerError( @@ -80,7 +111,7 @@ async function getQuote( receiveAmountValue = converted } else if (receiveAmount) { receiveAmountValue = receiveAmount.value - const converted = await deps.ratesService.convert({ + const converted = await convert({ sourceAmount: receiveAmountValue, sourceAsset: { code: receiveAmount.assetCode, @@ -103,7 +134,7 @@ async function getQuote( debitAmountValue = converted } else if (receiver.incomingAmount) { receiveAmountValue = receiver.incomingAmount.value - const converted = await deps.ratesService.convert({ + const converted = await convert({ sourceAmount: receiveAmountValue, sourceAsset: { code: receiver.incomingAmount.assetCode, @@ -132,6 +163,21 @@ async function getQuote( }) } + if (debitAmountValue <= BigInt(0)) { + throw new PaymentMethodHandlerError('Received error during local quoting', { + description: 'debit amount of local quote is non-positive', + retryable: false + }) + } + + if (receiveAmountValue <= BigInt(0)) { + throw new PaymentMethodHandlerError('Received error during local quoting', { + description: 'receive amount of local quote is non-positive', + retryable: false, + code: PaymentMethodHandlerErrorCode.QuoteNonPositiveReceiveAmount + }) + } + return { receiver: options.receiver, walletAddress: options.walletAddress, @@ -155,20 +201,13 @@ async function pay( deps: ServiceDependencies, options: PayOptions ): Promise { - // TODO: use finalDebitAmount instead of debitAmountMinusFees? pass debitAmountMinusFees in as finalDebitAmount? - const { outgoingPayment, receiver, finalReceiveAmount } = options - if (!outgoingPayment.quote.debitAmountMinusFees) { - // TODO: handle this better. perhaps debitAmountMinusFees should not be nullable? - // If throwing an error, follow existing patterns - throw new Error('could do local pay, missing debitAmountMinusFees') - } + const { outgoingPayment, receiver, finalReceiveAmount, finalDebitAmount } = + options - // Cannot directly use receiver/receiver.incomingAccount for destinationAccount. - // createTransfer Expects LiquidityAccount (gets Peer in ilp). const incomingPaymentId = receiver.incomingPayment.id.split('/').pop() if (!incomingPaymentId) { throw new PaymentMethodHandlerError('Received error during local payment', { - description: 'Incoming payment not found from receiver', + description: 'Failed to parse incoming payment on receiver', retryable: false }) } @@ -182,8 +221,6 @@ async function pay( }) } - // TODO: anything more needed from balance middleware? - // Necessary to avoid `UnknownDestinationAccount` error // TODO: remove incoming state check? perhaps only applies ilp account middleware where its checking many different things if (incomingPayment.state === IncomingPaymentState.Pending) { try { @@ -191,27 +228,28 @@ async function pay( incomingPayment, LiquidityAccountType.INCOMING ) - deps.logger.debug( - { incomingPayment }, - 'Created liquidity account for local incoming payment' - ) } catch (err) { if (!(err instanceof AccountAlreadyExistsError)) { deps.logger.error( { incomingPayment, err }, 'Failed to create liquidity account for local incoming payment' ) - throw err + throw new PaymentMethodHandlerError( + 'Received error during local payment', + { + description: + 'Unknown error while trying to create liquidity account', + retryable: false + } + ) } } } - const sourceAmount = outgoingPayment.quote.debitAmountMinusFees //finalDebitAmount? - const transferOptions: TransferOptions = { sourceAccount: outgoingPayment, destinationAccount: incomingPayment, - sourceAmount, + sourceAmount: finalDebitAmount, destinationAmount: finalReceiveAmount, transferType: TransferType.TRANSFER } @@ -227,9 +265,21 @@ async function pay( switch (trxOrError) { case TransferError.InsufficientBalance: case TransferError.InsufficientLiquidity: - throw new InsufficientLiquidityError(trxOrError) + throw new PaymentMethodHandlerError( + 'Received error during local payment', + { + description: errorToMessage[trxOrError], + retryable: false + } + ) default: - throw new Error('Unknown error while trying to create transfer') + throw new PaymentMethodHandlerError( + 'Received error during local payment', + { + description: 'Unknown error while trying to create transfer', + retryable: false + } + ) } } await trxOrError.post() diff --git a/packages/backend/src/rates/service.ts b/packages/backend/src/rates/service.ts index f4cae1f819..0af756bcd4 100644 --- a/packages/backend/src/rates/service.ts +++ b/packages/backend/src/rates/service.ts @@ -11,11 +11,11 @@ export interface Rates { rates: Record } +export type RateConvertOpts = Omit + export interface RatesService { rates(baseAssetCode: string): Promise - convert( - opts: Omit - ): Promise + convert(opts: RateConvertOpts): Promise } interface ServiceDependencies extends BaseService { diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index e00a82808e..a1996c3b9d 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -170,6 +170,7 @@ export async function createQuote( assetId: walletAddress.assetId, receiver: receiverUrl, debitAmount, + debitAmountMinusFees: debitAmount.value, receiveAmount, estimatedExchangeRate: exchangeRate, expiresAt: new Date(Date.now() + config.quoteLifespan), From 753a57be529c28e54f5f701a1d8a5c3ae157e1f9 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:50:46 -0400 Subject: [PATCH 38/64] chore: format --- packages/backend/src/payment-method/local/service.test.ts | 1 + packages/backend/src/payment-method/local/service.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 2f52691ef1..77f013cd0b 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -550,6 +550,7 @@ describe('LocalPaymentService', (): void => { jest .spyOn(accountingService, 'createTransfer') + // eslint-disable-next-line @typescript-eslint/no-explicit-any .mockResolvedValueOnce('UnknownError' as any) expect.assertions(4) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index e09ea95966..eba4d7215b 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -28,7 +28,6 @@ import { isTransferError, TransferError } from '../../accounting/errors' -import { InsufficientLiquidityError } from 'ilp-packet/dist/errors' import { IncomingPaymentService } from '../../open_payments/payment/incoming/service' import { IncomingPaymentState } from '../../open_payments/payment/incoming/model' import { FeeService } from '../../fee/service' From 8476615e591faefa005f63d9546277e11e9ca44d Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:53:20 -0400 Subject: [PATCH 39/64] fix(bruno): local open payments requests --- .../Open Payments (local)/Get receiver wallet address.bru | 3 ++- .../Open Payments (local)/Grant Request Incoming Payment.bru | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru index 5b9b0277a9..85b0b38d0e 100644 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{receiverWalletAddress}} + url: http://localhost:3000/accounts/bhamchest body: none auth: none } @@ -28,6 +28,7 @@ script:post-response { } const body = res.getBody() + bru.setEnvVar("receiverWalletAddress", "http://localhost:3000/accounts/bhamchest") bru.setEnvVar("receiverAssetCode", body?.assetCode) bru.setEnvVar("receiverAssetScale", body?.assetScale) diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru index 6335a518af..7fd03f2ee3 100644 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru +++ b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{receiverOpenPaymentsAuthHost}}/ + url: {{senderOpenPaymentsAuthHost}}/ body: json auth: none } From f8a979c99998529bdc5db5b9698b0786779a90c9 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:55:30 -0400 Subject: [PATCH 40/64] test(backend): add integration tests for local payments --- .../src/payment-method/local/service.ts | 6 +- test/integration/integration.test.ts | 807 +++++++++++------- .../testenv/cloud-nine-wallet/seed.yml | 6 + 3 files changed, 486 insertions(+), 333 deletions(-) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index eba4d7215b..dcf208b870 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -250,7 +250,11 @@ async function pay( destinationAccount: incomingPayment, sourceAmount: finalDebitAmount, destinationAmount: finalReceiveAmount, - transferType: TransferType.TRANSFER + transferType: TransferType.TRANSFER, + // TODO: no timeout? theoretically possible (perhaps even correct?) but need to + // update IncomingPayment state to COMPLETE some other way. trxOrError.post usually + // will (via onCredit) but post will return error instead because its already posted. + timeout: deps.config.tigerBeetleTwoPhaseTimeout } const trxOrError = diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index a54177bc7d..c8244cc4c6 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -11,7 +11,6 @@ jest.setTimeout(20_000) describe('Integration tests', (): void => { let c9: MockASE let hlb: MockASE - let testActions: TestActions beforeAll(async () => { try { @@ -23,8 +22,6 @@ describe('Integration tests', (): void => { // https://github.com/jestjs/jest/issues/2713 process.exit(1) } - - testActions = createTestActions({ sendingASE: c9, receivingASE: hlb }) }) afterAll(async () => { @@ -68,368 +65,514 @@ describe('Integration tests', (): void => { }) }) - // Series of requests depending on eachother describe('Flows', () => { - test('Open Payments with Continuation via Polling', async (): Promise => { - const { - grantRequestIncomingPayment, - createIncomingPayment, - grantRequestQuote, - createQuote, - grantRequestOutgoingPayment, - pollGrantContinue, - createOutgoingPayment, - getOutgoingPayment, - getPublicIncomingPayment - } = testActions.openPayments - const { consentInteraction } = testActions - - const receiverWalletAddressUrl = - 'https://happy-life-bank-test-backend:4100/accounts/pfry' - const senderWalletAddressUrl = - 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' - const amountValueToSend = '100' - - const receiverWalletAddress = await c9.opClient.walletAddress.get({ - url: receiverWalletAddressUrl - }) - expect(receiverWalletAddress.id).toBe(receiverWalletAddressUrl) + describe('Remote', () => { + let testActions: TestActions - const senderWalletAddress = await c9.opClient.walletAddress.get({ - url: senderWalletAddressUrl + beforeAll(async () => { + testActions = createTestActions({ sendingASE: c9, receivingASE: hlb }) }) - expect(senderWalletAddress.id).toBe(senderWalletAddressUrl) - let incomingPaymentGrant - try { - incomingPaymentGrant = await grantRequestIncomingPayment( - receiverWalletAddress - ) - } catch (err) { - console.log('ERROR: ', err) - throw err - } + test('Open Payments with Continuation via Polling', async (): Promise => { + const { + grantRequestIncomingPayment, + createIncomingPayment, + grantRequestQuote, + createQuote, + grantRequestOutgoingPayment, + pollGrantContinue, + createOutgoingPayment, + getOutgoingPayment, + getPublicIncomingPayment + } = testActions.openPayments + const { consentInteraction } = testActions + + const receiverWalletAddressUrl = + 'https://happy-life-bank-test-backend:4100/accounts/pfry' + const senderWalletAddressUrl = + 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' + const amountValueToSend = '100' - const incomingPayment = await createIncomingPayment( - receiverWalletAddress, - incomingPaymentGrant.access_token.value, - { amountValueToSend } - ) - const quoteGrant = await grantRequestQuote(senderWalletAddress) - const quote = await createQuote( - senderWalletAddress, - quoteGrant.access_token.value, - incomingPayment - ) - const outgoingPaymentGrant = await grantRequestOutgoingPayment( - senderWalletAddress, - { - debitAmount: quote.debitAmount, - receiveAmount: quote.receiveAmount - } - ) - await consentInteraction(outgoingPaymentGrant, senderWalletAddress) - const grantContinue = await pollGrantContinue(outgoingPaymentGrant) - const outgoingPayment = await createOutgoingPayment( - senderWalletAddress, - grantContinue, - { - metadata: {}, - quoteId: quote.id + const receiverWalletAddress = await c9.opClient.walletAddress.get({ + url: receiverWalletAddressUrl + }) + expect(receiverWalletAddress.id).toBe(receiverWalletAddressUrl) + + const senderWalletAddress = await c9.opClient.walletAddress.get({ + url: senderWalletAddressUrl + }) + expect(senderWalletAddress.id).toBe(senderWalletAddressUrl) + + let incomingPaymentGrant + try { + incomingPaymentGrant = await grantRequestIncomingPayment( + receiverWalletAddress + ) + } catch (err) { + console.log('ERROR: ', err) + throw err } - ) - const outgoingPayment_ = await getOutgoingPayment( - outgoingPayment.id, - grantContinue - ) - expect(outgoingPayment_.receiveAmount.value).toBe(amountValueToSend) - expect(outgoingPayment_.sentAmount.value).toBe(amountValueToSend) + const incomingPayment = await createIncomingPayment( + receiverWalletAddress, + incomingPaymentGrant.access_token.value, + { amountValueToSend } + ) + const quoteGrant = await grantRequestQuote(senderWalletAddress) + const quote = await createQuote( + senderWalletAddress, + quoteGrant.access_token.value, + incomingPayment + ) + const outgoingPaymentGrant = await grantRequestOutgoingPayment( + senderWalletAddress, + { + debitAmount: quote.debitAmount, + receiveAmount: quote.receiveAmount + } + ) + await consentInteraction(outgoingPaymentGrant, senderWalletAddress) + const grantContinue = await pollGrantContinue(outgoingPaymentGrant) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddress, + grantContinue, + { + metadata: {}, + quoteId: quote.id + } + ) + const outgoingPayment_ = await getOutgoingPayment( + outgoingPayment.id, + grantContinue + ) - await getPublicIncomingPayment(incomingPayment.id, amountValueToSend) + expect(outgoingPayment_.receiveAmount.value).toBe(amountValueToSend) + expect(outgoingPayment_.sentAmount.value).toBe(amountValueToSend) - const incomingPayment_ = await hlb.opClient.incomingPayment.getPublic({ - url: incomingPayment.id - }) - assert(incomingPayment_.receivedAmount) - expect(incomingPayment_.receivedAmount.value).toBe(amountValueToSend) - }) - test('Open Payments with Continuation via finish method', async (): Promise => { - const { - grantRequestIncomingPayment, - createIncomingPayment, - grantRequestQuote, - createQuote, - grantRequestOutgoingPayment, - grantContinue, - createOutgoingPayment, - getOutgoingPayment, - getPublicIncomingPayment - } = testActions.openPayments - const { consentInteractionWithInteractRef } = testActions - - const receiverWalletAddressUrl = - 'https://happy-life-bank-test-backend:4100/accounts/pfry' - const senderWalletAddressUrl = - 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' - const amountValueToSend = '100' - - const receiverWalletAddress = await c9.opClient.walletAddress.get({ - url: receiverWalletAddressUrl - }) - expect(receiverWalletAddress.id).toBe(receiverWalletAddressUrl) + await getPublicIncomingPayment(incomingPayment.id, amountValueToSend) - const senderWalletAddress = await c9.opClient.walletAddress.get({ - url: senderWalletAddressUrl + const incomingPayment_ = await hlb.opClient.incomingPayment.getPublic({ + url: incomingPayment.id + }) + assert(incomingPayment_.receivedAmount) + expect(incomingPayment_.receivedAmount.value).toBe(amountValueToSend) }) - expect(senderWalletAddress.id).toBe(senderWalletAddressUrl) + test('Open Payments with Continuation via finish method', async (): Promise => { + const { + grantRequestIncomingPayment, + createIncomingPayment, + grantRequestQuote, + createQuote, + grantRequestOutgoingPayment, + grantContinue, + createOutgoingPayment, + getOutgoingPayment, + getPublicIncomingPayment + } = testActions.openPayments + const { consentInteractionWithInteractRef } = testActions + + const receiverWalletAddressUrl = + 'https://happy-life-bank-test-backend:4100/accounts/pfry' + const senderWalletAddressUrl = + 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' + const amountValueToSend = '100' - const incomingPaymentGrant = await grantRequestIncomingPayment( - receiverWalletAddress - ) - const incomingPayment = await createIncomingPayment( - receiverWalletAddress, - incomingPaymentGrant.access_token.value, - { amountValueToSend } - ) - const quoteGrant = await grantRequestQuote(senderWalletAddress) - const quote = await createQuote( - senderWalletAddress, - quoteGrant.access_token.value, - incomingPayment - ) - const outgoingPaymentGrant = await grantRequestOutgoingPayment( - senderWalletAddress, - { - debitAmount: quote.debitAmount, - receiveAmount: quote.receiveAmount - }, - { - method: 'redirect', - uri: 'https://example.com', - nonce: '456' - } - ) - const interactRef = await consentInteractionWithInteractRef( - outgoingPaymentGrant, - senderWalletAddress - ) - const finalizedGrant = await grantContinue( - outgoingPaymentGrant, - interactRef - ) - const outgoingPayment = await createOutgoingPayment( - senderWalletAddress, - finalizedGrant, - { - metadata: {}, - quoteId: quote.id - } - ) - const outgoingPayment_ = await getOutgoingPayment( - outgoingPayment.id, - finalizedGrant - ) + const receiverWalletAddress = await c9.opClient.walletAddress.get({ + url: receiverWalletAddressUrl + }) + expect(receiverWalletAddress.id).toBe(receiverWalletAddressUrl) - expect(outgoingPayment_.receiveAmount.value).toBe(amountValueToSend) - expect(outgoingPayment_.sentAmount.value).toBe(amountValueToSend) + const senderWalletAddress = await c9.opClient.walletAddress.get({ + url: senderWalletAddressUrl + }) + expect(senderWalletAddress.id).toBe(senderWalletAddressUrl) - await getPublicIncomingPayment(incomingPayment.id, amountValueToSend) - }) - test('Open Payments without Quote', async (): Promise => { - const { - grantRequestIncomingPayment, - createIncomingPayment, - grantRequestOutgoingPayment, - pollGrantContinue, - createOutgoingPayment, - getOutgoingPayment - } = testActions.openPayments - const { consentInteraction } = testActions - - const receiverWalletAddressUrl = - 'https://happy-life-bank-test-backend:4100/accounts/pfry' - const senderWalletAddressUrl = - 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' - - const receiverWalletAddress = await c9.opClient.walletAddress.get({ - url: receiverWalletAddressUrl - }) - expect(receiverWalletAddress.id).toBe(receiverWalletAddressUrl) + const incomingPaymentGrant = await grantRequestIncomingPayment( + receiverWalletAddress + ) + const incomingPayment = await createIncomingPayment( + receiverWalletAddress, + incomingPaymentGrant.access_token.value, + { amountValueToSend } + ) + const quoteGrant = await grantRequestQuote(senderWalletAddress) + const quote = await createQuote( + senderWalletAddress, + quoteGrant.access_token.value, + incomingPayment + ) + const outgoingPaymentGrant = await grantRequestOutgoingPayment( + senderWalletAddress, + { + debitAmount: quote.debitAmount, + receiveAmount: quote.receiveAmount + }, + { + method: 'redirect', + uri: 'https://example.com', + nonce: '456' + } + ) + const interactRef = await consentInteractionWithInteractRef( + outgoingPaymentGrant, + senderWalletAddress + ) + const finalizedGrant = await grantContinue( + outgoingPaymentGrant, + interactRef + ) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddress, + finalizedGrant, + { + metadata: {}, + quoteId: quote.id + } + ) + const outgoingPayment_ = await getOutgoingPayment( + outgoingPayment.id, + finalizedGrant + ) + + expect(outgoingPayment_.receiveAmount.value).toBe(amountValueToSend) + expect(outgoingPayment_.sentAmount.value).toBe(amountValueToSend) - const senderWalletAddress = await c9.opClient.walletAddress.get({ - url: senderWalletAddressUrl + await getPublicIncomingPayment(incomingPayment.id, amountValueToSend) }) - expect(senderWalletAddress.id).toBe(senderWalletAddressUrl) + test('Open Payments without Quote', async (): Promise => { + const { + grantRequestIncomingPayment, + createIncomingPayment, + grantRequestOutgoingPayment, + pollGrantContinue, + createOutgoingPayment, + getOutgoingPayment + } = testActions.openPayments + const { consentInteraction } = testActions + + const receiverWalletAddressUrl = + 'https://happy-life-bank-test-backend:4100/accounts/pfry' + const senderWalletAddressUrl = + 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' - const debitAmount = { - assetCode: senderWalletAddress.assetCode, - assetScale: senderWalletAddress.assetScale, - value: '500' - } + const receiverWalletAddress = await c9.opClient.walletAddress.get({ + url: receiverWalletAddressUrl + }) + expect(receiverWalletAddress.id).toBe(receiverWalletAddressUrl) - const incomingPaymentGrant = await grantRequestIncomingPayment( - receiverWalletAddress - ) - const incomingPayment = await createIncomingPayment( - receiverWalletAddress, - incomingPaymentGrant.access_token.value - ) + const senderWalletAddress = await c9.opClient.walletAddress.get({ + url: senderWalletAddressUrl + }) + expect(senderWalletAddress.id).toBe(senderWalletAddressUrl) - const outgoingPaymentGrant = await grantRequestOutgoingPayment( - senderWalletAddress, - { - debitAmount, - receiveAmount: debitAmount + const debitAmount = { + assetCode: senderWalletAddress.assetCode, + assetScale: senderWalletAddress.assetScale, + value: '500' } - ) - await consentInteraction(outgoingPaymentGrant, senderWalletAddress) - const grantContinue = await pollGrantContinue(outgoingPaymentGrant) - const outgoingPayment = await createOutgoingPayment( - senderWalletAddress, - grantContinue, - { - incomingPayment: incomingPayment.id, - debitAmount + + const incomingPaymentGrant = await grantRequestIncomingPayment( + receiverWalletAddress + ) + const incomingPayment = await createIncomingPayment( + receiverWalletAddress, + incomingPaymentGrant.access_token.value + ) + + const outgoingPaymentGrant = await grantRequestOutgoingPayment( + senderWalletAddress, + { + debitAmount, + receiveAmount: debitAmount + } + ) + await consentInteraction(outgoingPaymentGrant, senderWalletAddress) + const grantContinue = await pollGrantContinue(outgoingPaymentGrant) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddress, + grantContinue, + { + incomingPayment: incomingPayment.id, + debitAmount + } + ) + + const outgoingPayment_ = await getOutgoingPayment( + outgoingPayment.id, + grantContinue + ) + + expect(outgoingPayment_.debitAmount).toMatchObject(debitAmount) + }) + test('Peer to Peer', async (): Promise => { + const { + createReceiver, + createQuote, + createOutgoingPayment, + getOutgoingPayment, + getIncomingPayment + } = testActions.admin + + const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( + 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' + ) + assert(senderWalletAddress?.walletAddressID) + const senderWalletAddressId = senderWalletAddress.walletAddressID + const value = '500' + const createReceiverInput = { + metadata: { + description: 'For lunch!' + }, + incomingAmount: { + assetCode: 'USD', + assetScale: 2, + value: value as unknown as bigint + }, + walletAddressUrl: + 'https://happy-life-bank-test-backend:4100/accounts/pfry' } - ) - const outgoingPayment_ = await getOutgoingPayment( - outgoingPayment.id, - grantContinue - ) + const receiver = await createReceiver(createReceiverInput) + const quote = await createQuote(senderWalletAddressId, receiver) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddressId, + quote + ) + const outgoingPayment_ = await getOutgoingPayment( + outgoingPayment.id, + value + ) + expect(outgoingPayment_.sentAmount.value).toBe(BigInt(value)) - expect(outgoingPayment_.debitAmount).toMatchObject(debitAmount) - }) - test('Peer to Peer', async (): Promise => { - const { - createReceiver, - createQuote, - createOutgoingPayment, - getOutgoingPayment, - getIncomingPayment - } = testActions.admin - - const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( - 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' - ) - assert(senderWalletAddress?.walletAddressID) - const senderWalletAddressId = senderWalletAddress.walletAddressID - const value = '500' - const createReceiverInput = { - metadata: { - description: 'For lunch!' - }, - incomingAmount: { - assetCode: 'USD', - assetScale: 2, - value: value as unknown as bigint - }, - walletAddressUrl: - 'https://happy-life-bank-test-backend:4100/accounts/pfry' - } + const incomingPaymentId = receiver.id.split('/').slice(-1)[0] + const incomingPayment = await getIncomingPayment(incomingPaymentId) + expect(incomingPayment.receivedAmount.value).toBe(BigInt(value)) + expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) + }) + test('Peer to Peer - Cross Currency', async (): Promise => { + const { + createReceiver, + createQuote, + createOutgoingPayment, + getOutgoingPayment, + getIncomingPayment + } = testActions.admin + + const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( + 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' + ) + assert(senderWalletAddress) + const senderAssetCode = senderWalletAddress.assetCode + const senderWalletAddressId = senderWalletAddress.walletAddressID + const value = '500' + const createReceiverInput = { + metadata: { + description: 'cross-currency' + }, + incomingAmount: { + assetCode: 'EUR', + assetScale: 2, + value: value as unknown as bigint + }, + walletAddressUrl: + 'https://happy-life-bank-test-backend:4100/accounts/lars' + } - const receiver = await createReceiver(createReceiverInput) - const quote = await createQuote(senderWalletAddressId, receiver) - const outgoingPayment = await createOutgoingPayment( - senderWalletAddressId, - quote - ) - const outgoingPayment_ = await getOutgoingPayment( - outgoingPayment.id, - value - ) - expect(outgoingPayment_.sentAmount.value).toBe(BigInt(value)) + const receiver = await createReceiver(createReceiverInput) + assert(receiver.incomingAmount) - const incomingPaymentId = receiver.id.split('/').slice(-1)[0] - const incomingPayment = await getIncomingPayment(incomingPaymentId) - expect(incomingPayment.receivedAmount.value).toBe(BigInt(value)) - expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) - }) - test('Peer to Peer - Cross Currency', async (): Promise => { - const { - createReceiver, - createQuote, - createOutgoingPayment, - getOutgoingPayment, - getIncomingPayment - } = testActions.admin - - const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( - 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' - ) - assert(senderWalletAddress) - const senderAssetCode = senderWalletAddress.assetCode - const senderWalletAddressId = senderWalletAddress.walletAddressID - const value = '500' - const createReceiverInput = { - metadata: { - description: 'cross-currency' - }, - incomingAmount: { + const quote = await createQuote(senderWalletAddressId, receiver) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddressId, + quote + ) + const completedOutgoingPayment = await getOutgoingPayment( + outgoingPayment.id, + value + ) + + const receiverAssetCode = receiver.incomingAmount.assetCode + const exchangeRate = + hlb.config.seed.rates[senderAssetCode][receiverAssetCode] + const fee = c9.config.seed.fees.find((fee: Fee) => fee.asset === 'USD') + + // Expected amounts depend on the configuration of asset codes, scale, exchange rate, and fees. + assert(receiverAssetCode === 'EUR') + assert(senderAssetCode === 'USD') + assert( + receiver.incomingAmount.assetScale === senderWalletAddress.assetScale + ) + assert(senderWalletAddress.assetScale === 2) + assert(exchangeRate === 0.91) + assert(fee) + assert(fee.fixed === 100) + assert(fee.basisPoints === 200) + assert(fee.asset === 'USD') + assert(fee.scale === 2) + expect(completedOutgoingPayment.receiveAmount).toMatchObject({ assetCode: 'EUR', assetScale: 2, - value: value as unknown as bigint - }, - walletAddressUrl: - 'https://happy-life-bank-test-backend:4100/accounts/lars' - } - - const receiver = await createReceiver(createReceiverInput) - assert(receiver.incomingAmount) - - const quote = await createQuote(senderWalletAddressId, receiver) - const outgoingPayment = await createOutgoingPayment( - senderWalletAddressId, - quote - ) - const completedOutgoingPayment = await getOutgoingPayment( - outgoingPayment.id, - value - ) - - const receiverAssetCode = receiver.incomingAmount.assetCode - const exchangeRate = - hlb.config.seed.rates[senderAssetCode][receiverAssetCode] - const fee = c9.config.seed.fees.find((fee: Fee) => fee.asset === 'USD') + value: 500n + }) + expect(completedOutgoingPayment.debitAmount).toMatchObject({ + assetCode: 'USD', + assetScale: 2, + value: 668n + }) + expect(completedOutgoingPayment.sentAmount).toMatchObject({ + assetCode: 'USD', + assetScale: 2, + value: 550n + }) - // Expected amounts depend on the configuration of asset codes, scale, exchange rate, and fees. - assert(receiverAssetCode === 'EUR') - assert(senderAssetCode === 'USD') - assert( - receiver.incomingAmount.assetScale === senderWalletAddress.assetScale - ) - assert(senderWalletAddress.assetScale === 2) - assert(exchangeRate === 0.91) - assert(fee) - assert(fee.fixed === 100) - assert(fee.basisPoints === 200) - assert(fee.asset === 'USD') - assert(fee.scale === 2) - expect(completedOutgoingPayment.receiveAmount).toMatchObject({ - assetCode: 'EUR', - assetScale: 2, - value: 500n + const incomingPaymentId = receiver.id.split('/').slice(-1)[0] + const incomingPayment = await getIncomingPayment(incomingPaymentId) + expect(incomingPayment.receivedAmount).toMatchObject({ + assetCode: 'EUR', + assetScale: 2, + value: 501n + }) + expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) }) - expect(completedOutgoingPayment.debitAmount).toMatchObject({ - assetCode: 'USD', - assetScale: 2, - value: 668n + }) + describe('Local', () => { + let testActions: TestActions + + beforeAll(async () => { + testActions = createTestActions({ sendingASE: c9, receivingASE: c9 }) }) - expect(completedOutgoingPayment.sentAmount).toMatchObject({ - assetCode: 'USD', - assetScale: 2, - value: 550n + + test('Peer to Peer', async (): Promise => { + const { + createReceiver, + createQuote, + createOutgoingPayment, + getOutgoingPayment, + getIncomingPayment + } = testActions.admin + + const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( + 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' + ) + assert(senderWalletAddress?.walletAddressID) + const senderWalletAddressId = senderWalletAddress.walletAddressID + const value = '500' + const createReceiverInput = { + metadata: { + description: 'For lunch!' + }, + incomingAmount: { + assetCode: 'USD', + assetScale: 2, + value: value as unknown as bigint + }, + walletAddressUrl: + 'https://cloud-nine-wallet-test-backend:3100/accounts/bhamchest' + } + + const receiver = await createReceiver(createReceiverInput) + const quote = await createQuote(senderWalletAddressId, receiver) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddressId, + quote + ) + const outgoingPayment_ = await getOutgoingPayment( + outgoingPayment.id, + value + ) + expect(outgoingPayment_.sentAmount.value).toBe(BigInt(value)) + + const incomingPaymentId = receiver.id.split('/').slice(-1)[0] + const incomingPayment = await getIncomingPayment(incomingPaymentId) + expect(incomingPayment.receivedAmount.value).toBe(BigInt(value)) + expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) }) - const incomingPaymentId = receiver.id.split('/').slice(-1)[0] - const incomingPayment = await getIncomingPayment(incomingPaymentId) - expect(incomingPayment.receivedAmount).toMatchObject({ - assetCode: 'EUR', - assetScale: 2, - value: 501n + test('Peer to Peer - Cross Currency', async (): Promise => { + const { + createReceiver, + createQuote, + createOutgoingPayment, + getOutgoingPayment, + getIncomingPayment + } = testActions.admin + + const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( + 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' + ) + assert(senderWalletAddress) + const senderAssetCode = senderWalletAddress.assetCode + const senderWalletAddressId = senderWalletAddress.walletAddressID + const value = '500' + const createReceiverInput = { + metadata: { + description: 'cross-currency' + }, + incomingAmount: { + assetCode: 'EUR', + assetScale: 2, + value: value as unknown as bigint + }, + walletAddressUrl: + 'https://cloud-nine-wallet-backend:3100/accounts/lrossi' + } + + const receiver = await createReceiver(createReceiverInput) + assert(receiver.incomingAmount) + + const quote = await createQuote(senderWalletAddressId, receiver) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddressId, + quote + ) + const completedOutgoingPayment = await getOutgoingPayment( + outgoingPayment.id, + value + ) + + const receiverAssetCode = receiver.incomingAmount.assetCode + const exchangeRate = + c9.config.seed.rates[senderAssetCode][receiverAssetCode] + const fee = c9.config.seed.fees.find((fee: Fee) => fee.asset === 'USD') + + // Expected amounts depend on the configuration of asset codes, scale, exchange rate, and fees. + assert(receiverAssetCode === 'EUR') + assert(senderAssetCode === 'USD') + assert( + receiver.incomingAmount.assetScale === senderWalletAddress.assetScale + ) + assert(senderWalletAddress.assetScale === 2) + assert(exchangeRate === 0.91) + assert(fee) + assert(fee.fixed === 100) + assert(fee.basisPoints === 200) + assert(fee.asset === 'USD') + assert(fee.scale === 2) + expect(completedOutgoingPayment.receiveAmount).toMatchObject({ + assetCode: 'EUR', + assetScale: 2, + value: 500n + }) + expect(completedOutgoingPayment.debitAmount).toMatchObject({ + assetCode: 'USD', + assetScale: 2, + value: 661n + }) + expect(completedOutgoingPayment.sentAmount).toMatchObject({ + assetCode: 'USD', + assetScale: 2, + value: 550n + }) + + const incomingPaymentId = receiver.id.split('/').slice(-1)[0] + const incomingPayment = await getIncomingPayment(incomingPaymentId) + expect(incomingPayment.receivedAmount).toMatchObject({ + assetCode: 'EUR', + assetScale: 2, + value: 500n + }) + expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) }) - expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) }) }) }) diff --git a/test/integration/testenv/cloud-nine-wallet/seed.yml b/test/integration/testenv/cloud-nine-wallet/seed.yml index cd55ae9b87..1b987fec45 100644 --- a/test/integration/testenv/cloud-nine-wallet/seed.yml +++ b/test/integration/testenv/cloud-nine-wallet/seed.yml @@ -40,6 +40,12 @@ accounts: path: accounts/wbdc postmanEnvVar: wbdcWalletAddress assetCode: USD + - name: "Luca Rossi" + id: 63dcc665-d946-4263-ac27-d0da1eb08a83 + initialBalance: 50 + path: accounts/lrossi + brunoEnvVar: lrossiWalletAddressId + assetCode: EUR rates: EUR: MXN: 18.78 From 5cee61822a48b8f98db6dd13ed5e346db93551ca Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:39:07 -0400 Subject: [PATCH 41/64] chore(backend): cleanup --- .../20240916182716_drop_quote_ilp_fields.js | 24 ------- packages/backend/src/index.ts | 4 +- .../open_payments/payment/outgoing/service.ts | 1 - .../src/open_payments/quote/service.ts | 72 +------------------ .../src/payment-method/handler/service.ts | 2 - .../ilp/connector/core/middleware/balance.ts | 4 -- .../ilp/connector/core/rafiki.ts | 1 - .../ilp/quote-details/service.test.ts | 1 - .../backend/src/payment-method/ilp/service.ts | 2 - .../src/payment-method/local/service.test.ts | 6 +- .../src/payment-method/local/service.ts | 1 - 11 files changed, 4 insertions(+), 114 deletions(-) diff --git a/packages/backend/migrations/20240916182716_drop_quote_ilp_fields.js b/packages/backend/migrations/20240916182716_drop_quote_ilp_fields.js index 839371d366..b2f50338db 100644 --- a/packages/backend/migrations/20240916182716_drop_quote_ilp_fields.js +++ b/packages/backend/migrations/20240916182716_drop_quote_ilp_fields.js @@ -46,28 +46,4 @@ exports.down = function (knex) { WHERE "quotes"."id" = "ilpQuoteDetails"."quoteId" `) }) - // .then(() => { - // // Apply the not null constraints after data insertion - // return knex.schema.alterTable('quotes', function (table) { - // table.bigInteger('maxPacketAmount').notNullable().alter() - // table.decimal('minExchangeRateNumerator', 64, 0).notNullable().alter() - // table.decimal('minExchangeRateDenominator', 64, 0).notNullable().alter() - // table - // .decimal('lowEstimatedExchangeRateNumerator', 64, 0) - // .notNullable() - // .alter() - // table - // .decimal('lowEstimatedExchangeRateDenominator', 64, 0) - // .notNullable() - // .alter() - // table - // .decimal('highEstimatedExchangeRateNumerator', 64, 0) - // .notNullable() - // .alter() - // table - // .decimal('highEstimatedExchangeRateDenominator', 64, 0) - // .notNullable() - // .alter() - // }) - // }) } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 8ae019d7bb..8de9b01277 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -454,9 +454,7 @@ export function initIocContainer( config: await deps.use('config'), ratesService: await deps.use('ratesService'), accountingService: await deps.use('accountingService'), - incomingPaymentService: await deps.use('incomingPaymentService'), - // TODO: rm if saving base debitAmount on quote - feeService: await deps.use('feeService') + incomingPaymentService: await deps.use('incomingPaymentService') } return createLocalPaymentService(serviceDependencies) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index d1eaaeddec..316124682b 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -532,7 +532,6 @@ async function fundPayment( const error = await deps.accountingService.createDeposit({ id: transferId, account: payment, - // ilp - 617n (not final sentAmount) amount }) if (error) { diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 0ac569a885..bc806ac31d 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -239,11 +239,7 @@ function calculateFixedSendQuoteAmounts( quote: Quote, maxReceiveAmountValue: bigint ): CalculateQuoteAmountsWithFeesResult { - // TODO: rework to - // debitAmount = 500 - // sourceAmount/debitAmountMinusFees = 500 - (500 * 0.02 + 100) = 390 - // receiveAmount = floor(debitAmountMinusFees * estimatedExchangeRate) = floor(390 * 0.91) = 354 - + // TODO: derive fee from debitAmount and convert that to receiveAmount const fees = quote.fee?.calculate(quote.receiveAmount.value) ?? BigInt(0) const { estimatedExchangeRate } = quote @@ -283,70 +279,6 @@ function calculateFixedSendQuoteAmounts( } } -// WIP: rework to -// debitAmount = 500 -// sourceAmount/debitAmountMinusFees = 500 - (500 * 0.02 + 100) = 390 -// receiveAmount = floor(debitAmountMinusFees * estimatedExchangeRate) = floor(390 * 0.91) = 354 - -// Problem: some quote tests are off by 1. changing calculate to floor/receive amount to floor (and every combo) didnt seem to fix it - -// function calculateFixedSendQuoteAmounts( -// deps: ServiceDependencies, -// quote: Quote, -// maxReceiveAmountValue: bigint -// ): CalculateQuoteAmountsWithFeesResult { -// // TODO: rework to -// // debitAmount = 500 -// // sourceAmount/debitAmountMinusFees = 500 - (500 * 0.02 + 100) = 390 -// // receiveAmount = floor(debitAmountMinusFees * estimatedExchangeRate) = floor(390 * 0.91) = 354 - -// const fees = quote.fee?.calculate(quote.debitAmount.value) ?? BigInt(0) -// const debitAmountMinusFees = quote.debitAmount.value - fees -// const { estimatedExchangeRate } = quote -// const receiveAmountValue = BigInt( -// Math.floor(Number(debitAmountMinusFees) * estimatedExchangeRate) -// ) - -// // console.log({ estimatedExchangeRate }) - -// // const exchangeAdjustedFees = BigInt( -// // Math.ceil(Number(fees) * estimatedExchangeRate) -// // ) -// // const receiveAmountValue = -// // BigInt(quote.receiveAmount.value) - exchangeAdjustedFees - -// if (receiveAmountValue <= BigInt(0)) { -// deps.logger.info( -// { fees, estimatedExchangeRate, receiveAmountValue }, -// 'Negative receive amount when calculating quote amount' -// ) -// throw QuoteError.NonPositiveReceiveAmount -// } - -// if (receiveAmountValue > maxReceiveAmountValue) { -// throw QuoteError.InvalidAmount -// } - -// deps.logger.debug( -// { -// 'quote.receiveAmount.value': quote.receiveAmount.value, -// debitAmountValue: quote.debitAmount.value, -// receiveAmountValue, -// fees -// // exchangeAdjustedFees -// }, -// 'Calculated fixed-send quote amount with fees' -// ) - -// // what is the debit amount that satisfies the receiveAmount after fees -// // - in my case, debitAmount of 500 includes fees. (answer is 390 in this case) - -// return { -// debitAmountValue: quote.debitAmount.value, -// receiveAmountValue -// } -// } - /** * Calculate fixed-delivery quote amounts: receiveAmount is locked, * add fees to the the debitAmount. @@ -430,7 +362,7 @@ async function finalizeQuote( const patchOptions = { debitAmountMinusFees: maxReceiveAmountValue - ? // TODO: change fixed send to return the debitAmountMinusFees if I can get new calc working + ? // TODO: change calculateFixedSendQuoteAmounts to return the debitAmountMinusFees if using new calculation quote.debitAmount.value - (quote.fee?.calculate(quote.debitAmount.value) ?? 0n) : quote.debitAmount.value, diff --git a/packages/backend/src/payment-method/handler/service.ts b/packages/backend/src/payment-method/handler/service.ts index 2008a64af5..c7bb2dfc0e 100644 --- a/packages/backend/src/payment-method/handler/service.ts +++ b/packages/backend/src/payment-method/handler/service.ts @@ -19,8 +19,6 @@ export interface PaymentQuote { debitAmount: Amount receiveAmount: Amount estimatedExchangeRate: number - // TODO: make this an amount like debitAmount? update db field, handle conversion to num/denom, etc. - // sourceAmount: bigint additionalFields: Record } diff --git a/packages/backend/src/payment-method/ilp/connector/core/middleware/balance.ts b/packages/backend/src/payment-method/ilp/connector/core/middleware/balance.ts index efb7446417..512ef863d1 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/middleware/balance.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/middleware/balance.ts @@ -34,10 +34,6 @@ export function createBalanceMiddleware(): ILPMiddleware { return } - // in happy-path (non local p2p create outgoing payment) - // both source and destination amounts are 500n. In local payment - // method, one is 610 but should be 500. S - const sourceAmount = BigInt(amount) const destinationAmountOrError = await services.rates.convert({ sourceAmount, diff --git a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts index 55e3af9cdc..6740d21886 100644 --- a/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts +++ b/packages/backend/src/payment-method/ilp/connector/core/rafiki.ts @@ -158,7 +158,6 @@ export class Rafiki { unfulfillable: boolean, rawPrepare: Buffer ): Promise { - // prepare amount = 500 const prepare = new ZeroCopyIlpPrepare(rawPrepare) const response = new IlpResponse() const telemetry = this.publicServer.context.services.telemetry diff --git a/packages/backend/src/payment-method/ilp/quote-details/service.test.ts b/packages/backend/src/payment-method/ilp/quote-details/service.test.ts index e9da98c24b..82bbdccccb 100644 --- a/packages/backend/src/payment-method/ilp/quote-details/service.test.ts +++ b/packages/backend/src/payment-method/ilp/quote-details/service.test.ts @@ -34,7 +34,6 @@ describe('IlpQuoteDetails Service', (): void => { describe('IlpQuoteDetails Service', (): void => { describe('getById', (): void => { it('should get ILP quote by id', async (): Promise => { - // const ilpQuote = await createIlpQuote(deps, asset.id, BigInt(1000)) const asset = await createAsset(deps) const { id: walletAddressId } = await createWalletAddress(deps, { assetId: asset.id diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index 39ad3d00d0..6142bc4193 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -256,8 +256,6 @@ async function pay( const destination = receiver.toResolvedPayment() try { - // receipt.amountDelivered: 500n - // receipt.amountSent: 500n const receipt = await Pay.pay({ plugin, destination, quote }) if (receipt.error) { diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 77f013cd0b..30130790f0 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -1,10 +1,7 @@ import { LocalPaymentService } from './service' import { initIocContainer } from '../../' import { createTestApp, TestContainer } from '../../tests/app' -import { - // IAppConfig, - Config -} from '../../config/app' +import { Config } from '../../config/app' import { IocContract } from '@adonisjs/fold' import { AppServices } from '../../app' import { createAsset } from '../../tests/asset' @@ -12,7 +9,6 @@ import { createWalletAddress } from '../../tests/walletAddress' import { Asset } from '../../asset/model' import { StartQuoteOptions } from '../handler/service' import { WalletAddress } from '../../open_payments/wallet_address/model' -// import * as Pay from '@interledger/pay' import { createReceiver } from '../../tests/receiver' import { mockRatesApi } from '../../tests/rates' diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index dcf208b870..68a3f9eb2b 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -39,7 +39,6 @@ export interface ServiceDependencies extends BaseService { ratesService: RatesService accountingService: AccountingService incomingPaymentService: IncomingPaymentService - feeService: FeeService } export async function createLocalPaymentService( From d41777e9116e14674ffdd08b0024ed2d60f7cdba Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:45:33 -0400 Subject: [PATCH 42/64] chore: restore old version of date definition in test --- .../src/open_payments/payment/outgoing/service.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 2b83d75dba..87251cb1ff 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -928,10 +928,8 @@ describe('OutgoingPaymentService', (): void => { validDestination: false, method: 'ilp' }) - const pastDate = new Date() - pastDate.setMinutes(pastDate.getMinutes() - 5) await quote.$query(knex).patch({ - expiresAt: pastDate + expiresAt: new Date() }) await expect( outgoingPaymentService.create({ From e0f988d4d9c571123ff4d3dadb9da99f54207ab8 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:52:57 -0400 Subject: [PATCH 43/64] chore: cleanup --- packages/backend/src/accounting/psql/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index 54098d15c3..6da25ecb96 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -340,7 +340,6 @@ async function createAccountDeposit( type: LedgerTransferType.DEPOSIT } - // ilp - 617n (not final sentAmount) const { errors } = await createTransfers(deps, [transfer], trx) if (errors[0]) { From 5d631bdd01d4cd129eac1ed3ebb4513b2376532f Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:00:48 -0400 Subject: [PATCH 44/64] fix: rm unused import --- packages/backend/src/payment-method/local/service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 68a3f9eb2b..11ce3584a1 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -30,7 +30,6 @@ import { } from '../../accounting/errors' import { IncomingPaymentService } from '../../open_payments/payment/incoming/service' import { IncomingPaymentState } from '../../open_payments/payment/incoming/model' -import { FeeService } from '../../fee/service' export interface LocalPaymentService extends PaymentMethodService {} From ececad2cfe053ba43bb11c484e8d9b24d76d94fb Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:12:41 -0400 Subject: [PATCH 45/64] test(integration): new case - p2p, fixed-send, local --- test/integration/integration.test.ts | 90 ++++++++++++++++++++-- test/integration/lib/test-actions/admin.ts | 26 +++---- 2 files changed, 92 insertions(+), 24 deletions(-) diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index c8244cc4c6..271898106a 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -333,7 +333,10 @@ describe('Integration tests', (): void => { } const receiver = await createReceiver(createReceiverInput) - const quote = await createQuote(senderWalletAddressId, receiver) + const quote = await createQuote({ + walletAddressId: senderWalletAddressId, + receiver: receiver.id + }) const outgoingPayment = await createOutgoingPayment( senderWalletAddressId, quote @@ -381,7 +384,10 @@ describe('Integration tests', (): void => { const receiver = await createReceiver(createReceiverInput) assert(receiver.incomingAmount) - const quote = await createQuote(senderWalletAddressId, receiver) + const quote = await createQuote({ + walletAddressId: senderWalletAddressId, + receiver: receiver.id + }) const outgoingPayment = await createOutgoingPayment( senderWalletAddressId, quote @@ -471,7 +477,10 @@ describe('Integration tests', (): void => { } const receiver = await createReceiver(createReceiverInput) - const quote = await createQuote(senderWalletAddressId, receiver) + const quote = await createQuote({ + walletAddressId: senderWalletAddressId, + receiver: receiver.id + }) const outgoingPayment = await createOutgoingPayment( senderWalletAddressId, quote @@ -488,6 +497,70 @@ describe('Integration tests', (): void => { expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) }) + test('Peer to Peer (Fixed Send)', async (): Promise => { + const { + createReceiver, + createQuote, + createOutgoingPayment, + getOutgoingPayment, + getIncomingPayment + } = testActions.admin + + const receiver = await createReceiver({ + metadata: { + description: 'For lunch!' + }, + walletAddressUrl: + 'https://cloud-nine-wallet-test-backend:3100/accounts/bhamchest' + }) + expect(receiver.id).toBeDefined() + + const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( + 'https://cloud-nine-wallet-test-backend:3100/accounts/gfranklin' + ) + assert(senderWalletAddress?.walletAddressID) + const senderWalletAddressId = senderWalletAddress.walletAddressID + + const createQuoteInput = { + walletAddressId: senderWalletAddressId, + receiver: receiver.id, + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: '500' as unknown as bigint + } + } + const quote = await createQuote(createQuoteInput) + expect(quote.id).toBeDefined() + + const outgoingPayment = await createOutgoingPayment( + senderWalletAddressId, + quote + ) + expect(outgoingPayment.id).toBeDefined() + + const outgoingPayment_ = await getOutgoingPayment( + outgoingPayment.id, + String(quote.receiveAmount.value) + ) + expect(outgoingPayment_.sentAmount.value).toBe( + BigInt(quote.receiveAmount.value) + ) + + expect(outgoingPayment_.state).toBe('COMPLETED') + + const incomingPaymentId = receiver.id.split('/').slice(-1)[0] + const incomingPayment = await getIncomingPayment(incomingPaymentId) + + expect(incomingPayment.receivedAmount).toMatchObject({ + assetCode: 'USD', + assetScale: 2, + value: BigInt(quote.receiveAmount.value) + }) + // TODO: fix bug where fixed-send (regardless of local/remote) is not completeing + // expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) + }) + test('Peer to Peer - Cross Currency', async (): Promise => { const { createReceiver, @@ -503,7 +576,6 @@ describe('Integration tests', (): void => { assert(senderWalletAddress) const senderAssetCode = senderWalletAddress.assetCode const senderWalletAddressId = senderWalletAddress.walletAddressID - const value = '500' const createReceiverInput = { metadata: { description: 'cross-currency' @@ -511,23 +583,25 @@ describe('Integration tests', (): void => { incomingAmount: { assetCode: 'EUR', assetScale: 2, - value: value as unknown as bigint + value: '500' as unknown as bigint }, walletAddressUrl: 'https://cloud-nine-wallet-backend:3100/accounts/lrossi' } - const receiver = await createReceiver(createReceiverInput) assert(receiver.incomingAmount) - const quote = await createQuote(senderWalletAddressId, receiver) + const quote = await createQuote({ + walletAddressId: senderWalletAddressId, + receiver: receiver.id + }) const outgoingPayment = await createOutgoingPayment( senderWalletAddressId, quote ) const completedOutgoingPayment = await getOutgoingPayment( outgoingPayment.id, - value + String(createReceiverInput.incomingAmount.value) ) const receiverAssetCode = receiver.incomingAmount.assetCode diff --git a/test/integration/lib/test-actions/admin.ts b/test/integration/lib/test-actions/admin.ts index 15dc6114d6..21ca18a1ec 100644 --- a/test/integration/lib/test-actions/admin.ts +++ b/test/integration/lib/test-actions/admin.ts @@ -5,7 +5,8 @@ import { OutgoingPayment, OutgoingPaymentState, CreateReceiverInput, - IncomingPayment + IncomingPayment, + CreateQuoteInput } from '../generated/graphql' import { MockASE } from '../mock-ase' import { pollCondition } from '../utils' @@ -17,8 +18,8 @@ interface AdminActionsDeps { } export interface AdminActions { - createReceiver(createReceiverInput: CreateReceiverInput): Promise - createQuote(senderWalletAddressId: string, receiver: Receiver): Promise + createReceiver(input: CreateReceiverInput): Promise + createQuote(input: CreateQuoteInput): Promise createOutgoingPayment( senderWalletAddressId: string, quote: Quote @@ -32,10 +33,8 @@ export interface AdminActions { export function createAdminActions(deps: AdminActionsDeps): AdminActions { return { - createReceiver: (createReceiverInput) => - createReceiver(deps, createReceiverInput), - createQuote: (senderWalletAddressId, receiver) => - createQuote(deps, senderWalletAddressId, receiver), + createReceiver: (input) => createReceiver(deps, input), + createQuote: (input) => createQuote(deps, input), createOutgoingPayment: (senderWalletAddressId, quote) => createOutgoingPayment(deps, senderWalletAddressId, quote), getIncomingPayment: (incomingPaymentId) => @@ -47,15 +46,14 @@ export function createAdminActions(deps: AdminActionsDeps): AdminActions { async function createReceiver( deps: AdminActionsDeps, - createReceiverInput: CreateReceiverInput + input: CreateReceiverInput ): Promise { const { receivingASE, sendingASE } = deps const handleWebhookEventSpy = jest.spyOn( receivingASE.integrationServer.webhookEventHandler, 'handleWebhookEvent' ) - const response = - await sendingASE.adminClient.createReceiver(createReceiverInput) + const response = await sendingASE.adminClient.createReceiver(input) assert(response.receiver) @@ -80,14 +78,10 @@ async function createReceiver( } async function createQuote( deps: AdminActionsDeps, - senderWalletAddressId: string, - receiver: Receiver + input: CreateQuoteInput ): Promise { const { sendingASE } = deps - const response = await sendingASE.adminClient.createQuote({ - walletAddressId: senderWalletAddressId, - receiver: receiver.id - }) + const response = await sendingASE.adminClient.createQuote(input) assert(response.quote) From 63011ae3905bb3e1132ea49f7a0027703bd279e1 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:16:59 -0400 Subject: [PATCH 46/64] chore(integration): rename test for consistency --- test/integration/integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index 271898106a..0406f58c03 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -497,7 +497,7 @@ describe('Integration tests', (): void => { expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) }) - test('Peer to Peer (Fixed Send)', async (): Promise => { + test('Peer to Peer - Fixed Send', async (): Promise => { const { createReceiver, createQuote, From bd12e30ce97e971790601124d740df862e6d2d8f Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 16 Oct 2024 10:05:48 -0400 Subject: [PATCH 47/64] fix(backend): throw error in pay if incoming payment is not pending --- .../src/payment-method/local/service.test.ts | 44 ++++++++++++++++++- .../src/payment-method/local/service.ts | 44 ++++++++++--------- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 30130790f0..417e1eee24 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -16,7 +16,10 @@ import { AccountingService } from '../../accounting/service' import { truncateTables } from '../../tests/tableManager' import { createOutgoingPaymentWithReceiver } from '../../tests/outgoingPayment' import { OutgoingPayment } from '../../open_payments/payment/outgoing/model' -import { IncomingPayment } from '../../open_payments/payment/incoming/model' +import { + IncomingPayment, + IncomingPaymentState +} from '../../open_payments/payment/incoming/model' import { IncomingPaymentService } from '../../open_payments/payment/incoming/service' import { errorToMessage, TransferError } from '../../accounting/errors' import { PaymentMethodHandlerError } from '../handler/errors' @@ -451,6 +454,45 @@ describe('LocalPaymentService', (): void => { } }) + test('throws error if incoming payment state is not pending', async (): Promise => { + const { receiver, outgoingPayment } = + await createOutgoingPaymentWithReceiver(deps, { + sendingWalletAddress: walletAddressMap['USD'], + receivingWalletAddress: walletAddressMap['USD'], + method: 'ilp', + quoteOptions: { + debitAmount: { + value: 100n, + assetScale: walletAddressMap['USD'].asset.scale, + assetCode: walletAddressMap['USD'].asset.code + } + } + }) + + jest.spyOn(incomingPaymentService, 'get').mockResolvedValueOnce({ + state: IncomingPaymentState.Processing + } as IncomingPayment) + + expect.assertions(4) + try { + await localPaymentService.pay({ + receiver, + outgoingPayment, + finalDebitAmount: 100n, + finalReceiveAmount: 100n + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Bad Incoming Payment State' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + `Incoming Payment state should be ${IncomingPaymentState.Pending}` + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } + }) + test('throws InsufficientBalance when balance is insufficient', async (): Promise => { const { receiver, outgoingPayment } = await createOutgoingPaymentWithReceiver(deps, { diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 11ce3584a1..598a520000 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -217,29 +217,31 @@ async function pay( retryable: false }) } + if (incomingPayment.state !== IncomingPaymentState.Pending) { + throw new PaymentMethodHandlerError('Bad Incoming Payment State', { + description: `Incoming Payment state should be ${IncomingPaymentState.Pending}`, + retryable: false + }) + } - // TODO: remove incoming state check? perhaps only applies ilp account middleware where its checking many different things - if (incomingPayment.state === IncomingPaymentState.Pending) { - try { - await deps.accountingService.createLiquidityAccount( - incomingPayment, - LiquidityAccountType.INCOMING + try { + await deps.accountingService.createLiquidityAccount( + incomingPayment, + LiquidityAccountType.INCOMING + ) + } catch (err) { + if (!(err instanceof AccountAlreadyExistsError)) { + deps.logger.error( + { incomingPayment, err }, + 'Failed to create liquidity account for local incoming payment' + ) + throw new PaymentMethodHandlerError( + 'Received error during local payment', + { + description: 'Unknown error while trying to create liquidity account', + retryable: false + } ) - } catch (err) { - if (!(err instanceof AccountAlreadyExistsError)) { - deps.logger.error( - { incomingPayment, err }, - 'Failed to create liquidity account for local incoming payment' - ) - throw new PaymentMethodHandlerError( - 'Received error during local payment', - { - description: - 'Unknown error while trying to create liquidity account', - retryable: false - } - ) - } } } From 0d69a3dc1dbbc385f4c92c42437e6be145f493fb Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:37:21 +0200 Subject: [PATCH 48/64] feat(backend): simplify migrations --- .../20240916181643_require_estimated_exchange_rate.js | 3 --- .../migrations/20240916181659_add_ilp_quote_details.js | 9 ++------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js b/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js index cd9d69d8df..c1c4c180ce 100644 --- a/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js +++ b/packages/backend/migrations/20240916181643_require_estimated_exchange_rate.js @@ -7,9 +7,6 @@ exports.up = function (knex) { return knex('quotes') .whereNull('estimatedExchangeRate') .update({ - // TODO: vet this more... looks like the low* fields were (inadvertently?) - // made nullable when updating from bigint to decimal. If they are null - // anywhere then this wont work. estimatedExchangeRate: knex.raw('?? / ??', [ 'lowEstimatedExchangeRateNumerator', 'lowEstimatedExchangeRateDenominator' diff --git a/packages/backend/migrations/20240916181659_add_ilp_quote_details.js b/packages/backend/migrations/20240916181659_add_ilp_quote_details.js index fec3636c39..1c6e6c3e7a 100644 --- a/packages/backend/migrations/20240916181659_add_ilp_quote_details.js +++ b/packages/backend/migrations/20240916181659_add_ilp_quote_details.js @@ -26,9 +26,6 @@ exports.up = function (knex) { table.timestamp('createdAt').defaultTo(knex.fn.now()) table.timestamp('updatedAt').defaultTo(knex.fn.now()) }) - .then(() => { - return knex.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`) - }) .then(() => { return knex.raw(` INSERT INTO "ilpQuoteDetails" ( @@ -43,7 +40,7 @@ exports.up = function (knex) { "highEstimatedExchangeRateDenominator" ) SELECT - uuid_generate_v4(), + gen_random_uuid(), id AS "quoteId", "maxPacketAmount", "minExchangeRateNumerator", @@ -63,7 +60,5 @@ exports.up = function (knex) { * @returns { Promise } */ exports.down = function (knex) { - return knex.raw(`DROP EXTENSION IF EXISTS "uuid-ossp"`).then(() => { - return knex.schema.dropTableIfExists('ilpQuoteDetails') - }) + return knex.schema.dropTableIfExists('ilpQuoteDetails') } From 31e6cc7c4982c1bc6b301c8176ad8e88f03ec960 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:40:31 +0200 Subject: [PATCH 49/64] chore(backend): clarify comment --- packages/backend/src/payment-method/local/service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 598a520000..48841c1328 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -251,9 +251,8 @@ async function pay( sourceAmount: finalDebitAmount, destinationAmount: finalReceiveAmount, transferType: TransferType.TRANSFER, - // TODO: no timeout? theoretically possible (perhaps even correct?) but need to - // update IncomingPayment state to COMPLETE some other way. trxOrError.post usually - // will (via onCredit) but post will return error instead because its already posted. + // TODO: remove timeout after implementing single phase transfer + // https://github.com/interledger/rafiki/issues/2629 timeout: deps.config.tigerBeetleTwoPhaseTimeout } From bb9d334bb284c73d85e73e4cc1cd42b84e0ecfd7 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:54:34 +0200 Subject: [PATCH 50/64] chore(auth): format --- packages/auth/src/graphql/schema.graphql | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/auth/src/graphql/schema.graphql b/packages/auth/src/graphql/schema.graphql index efe49d4f07..0511ff0add 100644 --- a/packages/auth/src/graphql/schema.graphql +++ b/packages/auth/src/graphql/schema.graphql @@ -16,10 +16,7 @@ type Query { ): GrantsConnection! "Fetch a specific grant by its ID." - grant( - "Unique identifier of the grant." - id: ID! - ): Grant! + grant("Unique identifier of the grant." id: ID!): Grant! } type Mutation { From b0897d99c5170e7c7e5f99ba3538f3888e6dce45 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:21:04 +0200 Subject: [PATCH 51/64] refactor(backend): use IlpQuoteDetails model directly in ilp payment method --- packages/backend/src/payment-method/ilp/service.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index 6142bc4193..1b03e2d719 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -16,6 +16,7 @@ import { } from '../handler/errors' import { TelemetryService } from '../../telemetry/service' import { IlpQuoteDetailsService } from './quote-details/service' +import { IlpQuoteDetails } from './quote-details/model' export interface IlpPaymentService extends PaymentMethodService {} @@ -218,8 +219,11 @@ async function pay( } if (!outgoingPayment.quote.ilpQuoteDetails) { - outgoingPayment.quote.ilpQuoteDetails = - await deps.ilpQuoteDetailsService.getByQuoteId(outgoingPayment.quote.id) + outgoingPayment.quote.ilpQuoteDetails = await IlpQuoteDetails.query( + deps.knex + ) + .where('quoteId', outgoingPayment.quote.id) + .first() if (!outgoingPayment.quote.ilpQuoteDetails) { throw new PaymentMethodHandlerError( From 2d8048a015a198d9d054d33f297fa7990c7351bf Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:31:45 +0200 Subject: [PATCH 52/64] refactor(backend): rm ilpQuoteDetails service --- packages/backend/src/app.ts | 2 - packages/backend/src/index.ts | 9 --- .../src/open_payments/quote/service.test.ts | 30 +++++---- .../ilp/quote-details/service.test.ts | 66 ------------------- .../ilp/quote-details/service.ts | 32 --------- .../backend/src/payment-method/ilp/service.ts | 2 - 6 files changed, 16 insertions(+), 125 deletions(-) delete mode 100644 packages/backend/src/payment-method/ilp/quote-details/service.test.ts delete mode 100644 packages/backend/src/payment-method/ilp/quote-details/service.ts diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 1bf0b0a41a..ec8014bf99 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -101,7 +101,6 @@ import { LoggingPlugin } from './graphql/plugin' import { LocalPaymentService } from './payment-method/local/service' import { GrantService } from './open_payments/grant/service' import { AuthServerService } from './open_payments/authServer/service' -import { IlpQuoteDetailsService } from './payment-method/ilp/quote-details/service' export interface AppContextData { logger: Logger container: AppContainer @@ -257,7 +256,6 @@ export interface AppServices { paymentMethodHandlerService: Promise ilpPaymentService: Promise localPaymentService: Promise - ilpQuoteDetailsService: Promise } export type AppContainer = IocContract diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 6cd94fc273..c0870d00f8 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -48,7 +48,6 @@ import { import { createHttpTokenService } from './payment-method/ilp/peer-http-token/service' import { createPeerService } from './payment-method/ilp/peer/service' import { createIlpPaymentService } from './payment-method/ilp/service' -import { createIlpQuoteDetailsService } from './payment-method/ilp/quote-details/service' import { createLocalPaymentService, ServiceDependencies as LocalPaymentServiceDependencies @@ -428,13 +427,6 @@ export function initIocContainer( }) }) - container.singleton('ilpQuoteDetailsService', async (deps) => { - return await createIlpQuoteDetailsService({ - logger: await deps.use('logger'), - knex: await deps.use('knex') - }) - }) - container.singleton('ilpPaymentService', async (deps) => { return await createIlpPaymentService({ logger: await deps.use('logger'), @@ -442,7 +434,6 @@ export function initIocContainer( config: await deps.use('config'), makeIlpPlugin: await deps.use('makeIlpPlugin'), ratesService: await deps.use('ratesService'), - ilpQuoteDetailsService: await deps.use('ilpQuoteDetailsService'), telemetry: await deps.use('telemetry') }) }) diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 2a2558b43d..dee858bd08 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -36,7 +36,7 @@ import { PaymentMethodHandlerErrorCode } from '../../payment-method/handler/errors' import { Receiver } from '../receiver/model' -import { IlpQuoteDetailsService } from '../../payment-method/ilp/quote-details/service' +import { IlpQuoteDetails } from '../../payment-method/ilp/quote-details/model' describe('QuoteService', (): void => { let deps: IocContract @@ -48,7 +48,6 @@ describe('QuoteService', (): void => { let sendingWalletAddress: MockWalletAddress let receivingWalletAddress: MockWalletAddress let config: IAppConfig - let ilpQuoteDetailsService: IlpQuoteDetailsService let receiverGet: typeof receiverService.get let receiverGetSpy: jest.SpyInstance< Promise, @@ -88,7 +87,6 @@ describe('QuoteService', (): void => { quoteService = await deps.use('quoteService') paymentMethodHandlerService = await deps.use('paymentMethodHandlerService') receiverService = await deps.use('receiverService') - ilpQuoteDetailsService = await deps.use('ilpQuoteDetailsService') }) beforeEach(async (): Promise => { @@ -156,9 +154,9 @@ describe('QuoteService', (): void => { return } - quote.ilpQuoteDetails = await ilpQuoteDetailsService.getByQuoteId( - quote.id - ) + quote.ilpQuoteDetails = await IlpQuoteDetails.query() + .where({ quoteId: quote.id }) + .first() return quote }, @@ -167,7 +165,9 @@ describe('QuoteService', (): void => { const quotesWithDetails = await Promise.all( quotes.map(async (q) => { - q.ilpQuoteDetails = await ilpQuoteDetailsService.getByQuoteId(q.id) + q.ilpQuoteDetails = await IlpQuoteDetails.query() + .where({ quoteId: q.id }) + .first() return q }) ) @@ -283,8 +283,9 @@ describe('QuoteService', (): void => { id: quote.id }) assert(foundQuote) - foundQuote.ilpQuoteDetails = - await ilpQuoteDetailsService.getByQuoteId(quote.id) + foundQuote.ilpQuoteDetails = await IlpQuoteDetails.query() + .where({ quoteId: quote.id }) + .first() expect(foundQuote).toEqual(quote) } ) @@ -374,8 +375,9 @@ describe('QuoteService', (): void => { id: quote.id }) assert(foundQuote) - foundQuote.ilpQuoteDetails = - await ilpQuoteDetailsService.getByQuoteId(quote.id) + foundQuote.ilpQuoteDetails = await IlpQuoteDetails.query() + .where({ quoteId: quote.id }) + .first() expect(foundQuote).toEqual(quote) } ) @@ -493,9 +495,9 @@ describe('QuoteService', (): void => { }) assert.ok(!isQuoteError(quote)) - const ilpQuoteDetails = await ilpQuoteDetailsService.getByQuoteId( - quote.id - ) + const ilpQuoteDetails = await IlpQuoteDetails.query() + .where({ quoteId: quote.id }) + .first() expect(quote).toMatchObject({ debitAmount: mockedQuote.debitAmount, diff --git a/packages/backend/src/payment-method/ilp/quote-details/service.test.ts b/packages/backend/src/payment-method/ilp/quote-details/service.test.ts deleted file mode 100644 index 82bbdccccb..0000000000 --- a/packages/backend/src/payment-method/ilp/quote-details/service.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { IocContract } from '@adonisjs/fold' -import { AppServices } from '../../../app' -import { TestContainer, createTestApp } from '../../../tests/app' -import { initIocContainer } from '../../../' -import { Config } from '../../../config/app' -import { Knex } from 'knex' -import { truncateTables } from '../../../tests/tableManager' -import { createAsset } from '../../../tests/asset' -import { v4 } from 'uuid' -import { IlpQuoteDetailsService } from './service' -import { createQuote } from '../../../tests/quote' -import { createWalletAddress } from '../../../tests/walletAddress' - -describe('IlpQuoteDetails Service', (): void => { - let deps: IocContract - let appContainer: TestContainer - let knex: Knex - let ilpQuoteDetailsService: IlpQuoteDetailsService - - beforeAll(async (): Promise => { - deps = await initIocContainer(Config) - appContainer = await createTestApp(deps) - knex = await deps.use('knex') - ilpQuoteDetailsService = await deps.use('ilpQuoteDetailsService') - }) - afterEach(async (): Promise => { - await truncateTables(knex) - }) - - afterAll(async (): Promise => { - await appContainer.shutdown() - }) - - describe('IlpQuoteDetails Service', (): void => { - describe('getById', (): void => { - it('should get ILP quote by id', async (): Promise => { - const asset = await createAsset(deps) - const { id: walletAddressId } = await createWalletAddress(deps, { - assetId: asset.id - }) - - const quote = await createQuote(deps, { - walletAddressId, - receiver: `http://wallet2.example/bob/incoming-payments/${v4()}`, - debitAmount: { - value: BigInt(56), - assetCode: asset.code, - assetScale: asset.scale - }, - method: 'ilp', - validDestination: false - }) - - const foundIlpQuote = await ilpQuoteDetailsService.getByQuoteId( - quote.id - ) - expect(foundIlpQuote).toBeDefined() - }) - - it('should return undefined when no ILP quote is found by id', async (): Promise => { - const foundIlpQuote = await ilpQuoteDetailsService.getByQuoteId(v4()) - expect(foundIlpQuote).toBe(undefined) - }) - }) - }) -}) diff --git a/packages/backend/src/payment-method/ilp/quote-details/service.ts b/packages/backend/src/payment-method/ilp/quote-details/service.ts deleted file mode 100644 index 3903b75b71..0000000000 --- a/packages/backend/src/payment-method/ilp/quote-details/service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TransactionOrKnex } from 'objection' -import { IlpQuoteDetails } from './model' -import { BaseService } from '../../../shared/baseService' - -export interface IlpQuoteDetailsService { - getByQuoteId(quoteId: string): Promise -} - -export interface ServiceDependencies extends BaseService { - knex: TransactionOrKnex -} - -export async function createIlpQuoteDetailsService( - deps_: ServiceDependencies -): Promise { - const deps = { - ...deps_, - logger: deps_.logger.child({ service: 'IlpQuoteDetailsService' }) - } - return { - getByQuoteId: (quoteId: string) => - getIlpQuoteDetailsByQuoteId(deps, quoteId) - } -} -async function getIlpQuoteDetailsByQuoteId( - deps: ServiceDependencies, - quoteId: string -): Promise { - return await IlpQuoteDetails.query(deps.knex) - .where('quoteId', quoteId) - .first() -} diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index 1b03e2d719..1b54d5251b 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -15,7 +15,6 @@ import { PaymentMethodHandlerErrorCode } from '../handler/errors' import { TelemetryService } from '../../telemetry/service' -import { IlpQuoteDetailsService } from './quote-details/service' import { IlpQuoteDetails } from './quote-details/model' export interface IlpPaymentService extends PaymentMethodService {} @@ -24,7 +23,6 @@ export interface ServiceDependencies extends BaseService { config: IAppConfig ratesService: RatesService makeIlpPlugin: (options: IlpPluginOptions) => IlpPlugin - ilpQuoteDetailsService: IlpQuoteDetailsService telemetry: TelemetryService } From c40db9e670082bc04a36a50e0d52ee4d03f4019c Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:56:31 -0400 Subject: [PATCH 53/64] fix(integration): wa typo --- test/integration/integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index 0406f58c03..d0d41cd3ae 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -586,7 +586,7 @@ describe('Integration tests', (): void => { value: '500' as unknown as bigint }, walletAddressUrl: - 'https://cloud-nine-wallet-backend:3100/accounts/lrossi' + 'https://cloud-nine-wallet-test-backend:3100/accounts/lrossi' } const receiver = await createReceiver(createReceiverInput) assert(receiver.incomingAmount) From 0dbb2d3a84798ccc69fa75a0f2fba0a55f7e0530 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:07:18 -0400 Subject: [PATCH 54/64] chore: rm bruno test examples --- .../Create Quote.bru | 71 ----------------- ...ate Receiver (remote Incoming Payment).bru | 78 ------------------- .../Get Outgoing Payment.bru | 56 ------------- .../Create Outgoing Payment.bru | 72 ----------------- .../Create Quote.bru | 71 ----------------- ...ate Receiver (remote Incoming Payment).bru | 78 ------------------- .../Get Outgoing Payment.bru | 56 ------------- .../Create Outgoing Payment.bru | 72 ----------------- ...ate Receiver (remote Incoming Payment).bru | 78 ------------------- .../Get Outgoing Payment.bru | 56 ------------- .../Create Outgoing Payment.bru | 0 .../Create Quote.bru | 0 ...ate Receiver -remote Incoming Payment-.bru | 0 .../Get Outgoing Payment.bru | 0 .../Create Outgoing Payment.bru | 72 ----------------- .../Create Quote.bru | 71 ----------------- ...ate Receiver -remote Incoming Payment-.bru | 73 ----------------- .../Create Outgoing Payment.bru | 72 ----------------- .../Create Quote.bru | 66 ---------------- .../Get Outgoing Payment.bru | 57 -------------- .../Create Outgoing Payment.bru | 72 ----------------- .../Create Quote.bru | 71 ----------------- ...ate Receiver -remote Incoming Payment-.bru | 73 ----------------- .../Get Outgoing Payment.bru | 57 -------------- .../Continuation Request.bru | 33 -------- .../Create Incoming Payment.bru | 55 ------------- .../Create Outgoing Payment.bru | 48 ------------ .../Open Payments (local)/Create Quote.bru | 47 ----------- .../Get Outgoing Payment.bru | 29 ------- .../Get receiver wallet address.bru | 51 ------------ .../Get sender wallet address.bru | 50 ------------ .../Grant Request Incoming Payment.bru | 45 ----------- .../Grant Request Outgoing Payment.bru | 56 ------------- .../Grant Request Quote.bru | 46 ----------- 34 files changed, 1832 deletions(-) delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Quote.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Receiver (remote Incoming Payment).bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Get Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Quote.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Receiver (remote Incoming Payment).bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Get Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Get Outgoing Payment.bru rename bruno/collections/Rafiki/Examples/Admin API - only locally/{Peer-to-Peer Cross Cur Payment (Local, Fixed Send) => Peer-to-Peer Local Payment}/Create Outgoing Payment.bru (100%) rename bruno/collections/Rafiki/Examples/Admin API - only locally/{Peer-to-Peer Cross Currency Payment (Local) => Peer-to-Peer Local Payment}/Create Quote.bru (100%) rename bruno/collections/Rafiki/Examples/Admin API - only locally/{Peer-to-Peer Payment (Local) => Peer-to-Peer Local Payment}/Create Receiver -remote Incoming Payment-.bru (100%) rename bruno/collections/Rafiki/Examples/Admin API - only locally/{Peer-to-Peer Payment (Fixed Send) => Peer-to-Peer Local Payment}/Get Outgoing Payment.bru (100%) delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Quote.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Receiver -remote Incoming Payment-.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Quote.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Get Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Quote.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Receiver -remote Incoming Payment-.bru delete mode 100644 bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Get Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Continuation Request.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Create Incoming Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Create Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Create Quote.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Get Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Get sender wallet address.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Outgoing Payment.bru delete mode 100644 bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Quote.bru diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Quote.bru deleted file mode 100644 index 46bd3ffc1d..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Quote.bru +++ /dev/null @@ -1,71 +0,0 @@ -meta { - name: Create Quote - type: graphql - seq: 2 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateQuote($input: CreateQuoteInput!) { - createQuote(input: $input) { - quote { - createdAt - expiresAt - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "receiver": "{{receiverId}}", - "debitAmount": { - "assetCode": "USD", - "assetScale": 2, - "value": 500 - } - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - await scripts.loadWalletAddressIdsIntoVariables(); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("quoteId", body.data.createQuote.quote.id); - } -} - -tests { - test("Quote id is string", function() { - expect(bru.getEnvVar("quoteId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Receiver (remote Incoming Payment).bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Receiver (remote Incoming Payment).bru deleted file mode 100644 index c3b3ae34bd..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Receiver (remote Incoming Payment).bru +++ /dev/null @@ -1,78 +0,0 @@ -meta { - name: Create Receiver (remote Incoming Payment) - type: graphql - seq: 1 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateReceiver($input: CreateReceiverInput!) { - createReceiver(input: $input) { - receiver { - completed - createdAt - expiresAt - metadata - id - incomingAmount { - assetCode - assetScale - value - } - walletAddressUrl - receivedAmount { - assetCode - assetScale - value - } - updatedAt - } - } - } -} - -body:graphql:vars { - { - "input": { - "metadata": { - "description": "cross-currency" - }, - // "incomingAmount": { - // "assetCode": "EUR", - // "assetScale": 2, - // "value": 500 - // }, - "walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/lrossi" - } - } -} - -vars:pre-request { - signatureVersion: {{apiSignatureVersion}} - signatureSecret: {{apiSignatureSecret}} -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); - } -} - -tests { - test("Receiver id is string", function() { - expect(bru.getEnvVar("receiverId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Get Outgoing Payment.bru deleted file mode 100644 index 3216293fbd..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Get Outgoing Payment.bru +++ /dev/null @@ -1,56 +0,0 @@ -meta { - name: Get Outgoing Payment - type: graphql - seq: 4 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - query GetOutgoingPayment($id: String!) { - outgoingPayment(id: $id) { - createdAt - error - metadata - id - walletAddressId - quote { - id - } - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } -} - -body:graphql:vars { - { - "id": "{{outgoingPaymentId}}" - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Outgoing Payment.bru deleted file mode 100644 index 4bfde2f666..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Outgoing Payment.bru +++ /dev/null @@ -1,72 +0,0 @@ -meta { - name: Create Outgoing Payment - type: graphql - seq: 3 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { - createOutgoingPayment(input: $input) { - payment { - createdAt - error - metadata - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "quoteId": "{{quoteId}}" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); - } -} - -tests { - test("Outgoing Payment id is string", function() { - expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Quote.bru deleted file mode 100644 index 78a99f8225..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Quote.bru +++ /dev/null @@ -1,71 +0,0 @@ -meta { - name: Create Quote - type: graphql - seq: 2 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateQuote($input: CreateQuoteInput!) { - createQuote(input: $input) { - quote { - createdAt - expiresAt - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "receiver": "{{receiverId}}", - "debitAmount": { - "assetCode": "EUR", - "assetScale": 2, - "value": 500 - } - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - await scripts.loadWalletAddressIdsIntoVariables(); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("quoteId", body.data.createQuote.quote.id); - } -} - -tests { - test("Quote id is string", function() { - expect(bru.getEnvVar("quoteId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Receiver (remote Incoming Payment).bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Receiver (remote Incoming Payment).bru deleted file mode 100644 index 4fae1c904b..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Create Receiver (remote Incoming Payment).bru +++ /dev/null @@ -1,78 +0,0 @@ -meta { - name: Create Receiver (remote Incoming Payment) - type: graphql - seq: 1 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateReceiver($input: CreateReceiverInput!) { - createReceiver(input: $input) { - receiver { - completed - createdAt - expiresAt - metadata - id - incomingAmount { - assetCode - assetScale - value - } - walletAddressUrl - receivedAmount { - assetCode - assetScale - value - } - updatedAt - } - } - } -} - -body:graphql:vars { - { - "input": { - "metadata": { - "description": "cross-currency" - }, - "incomingAmount": { - "assetCode": "EUR", - "assetScale": 2, - "value": 500 - }, - "walletAddressUrl": "https://happy-life-bank-backend/accounts/lars" - } - } -} - -vars:pre-request { - signatureVersion: {{apiSignatureVersion}} - signatureSecret: {{apiSignatureSecret}} -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); - } -} - -tests { - test("Receiver id is string", function() { - expect(bru.getEnvVar("receiverId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Get Outgoing Payment.bru deleted file mode 100644 index 3216293fbd..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Fixed Send)/Get Outgoing Payment.bru +++ /dev/null @@ -1,56 +0,0 @@ -meta { - name: Get Outgoing Payment - type: graphql - seq: 4 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - query GetOutgoingPayment($id: String!) { - outgoingPayment(id: $id) { - createdAt - error - metadata - id - walletAddressId - quote { - id - } - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } -} - -body:graphql:vars { - { - "id": "{{outgoingPaymentId}}" - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Outgoing Payment.bru deleted file mode 100644 index 4bfde2f666..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Outgoing Payment.bru +++ /dev/null @@ -1,72 +0,0 @@ -meta { - name: Create Outgoing Payment - type: graphql - seq: 3 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { - createOutgoingPayment(input: $input) { - payment { - createdAt - error - metadata - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "quoteId": "{{quoteId}}" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); - } -} - -tests { - test("Outgoing Payment id is string", function() { - expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru deleted file mode 100644 index da9b731cf6..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Receiver (remote Incoming Payment).bru +++ /dev/null @@ -1,78 +0,0 @@ -meta { - name: Create Receiver (remote Incoming Payment) - type: graphql - seq: 1 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateReceiver($input: CreateReceiverInput!) { - createReceiver(input: $input) { - receiver { - completed - createdAt - expiresAt - metadata - id - incomingAmount { - assetCode - assetScale - value - } - walletAddressUrl - receivedAmount { - assetCode - assetScale - value - } - updatedAt - } - } - } -} - -body:graphql:vars { - { - "input": { - "metadata": { - "description": "cross-currency" - }, - "incomingAmount": { - "assetCode": "EUR", - "assetScale": 2, - "value": 500 - }, - "walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/lrossi" - } - } -} - -vars:pre-request { - signatureVersion: {{apiSignatureVersion}} - signatureSecret: {{apiSignatureSecret}} -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); - } -} - -tests { - test("Receiver id is string", function() { - expect(bru.getEnvVar("receiverId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Get Outgoing Payment.bru deleted file mode 100644 index 3216293fbd..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Get Outgoing Payment.bru +++ /dev/null @@ -1,56 +0,0 @@ -meta { - name: Get Outgoing Payment - type: graphql - seq: 4 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - query GetOutgoingPayment($id: String!) { - outgoingPayment(id: $id) { - createdAt - error - metadata - id - walletAddressId - quote { - id - } - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } -} - -body:graphql:vars { - { - "id": "{{outgoingPaymentId}}" - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Outgoing Payment.bru similarity index 100% rename from bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Cur Payment (Local, Fixed Send)/Create Outgoing Payment.bru rename to bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Outgoing Payment.bru diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Quote.bru similarity index 100% rename from bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Cross Currency Payment (Local)/Create Quote.bru rename to bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Quote.bru diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Receiver -remote Incoming Payment-.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru similarity index 100% rename from bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Receiver -remote Incoming Payment-.bru rename to bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Get Outgoing Payment.bru similarity index 100% rename from bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Get Outgoing Payment.bru rename to bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Get Outgoing Payment.bru diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Outgoing Payment.bru deleted file mode 100644 index 4bfde2f666..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Outgoing Payment.bru +++ /dev/null @@ -1,72 +0,0 @@ -meta { - name: Create Outgoing Payment - type: graphql - seq: 3 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { - createOutgoingPayment(input: $input) { - payment { - createdAt - error - metadata - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "quoteId": "{{quoteId}}" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); - } -} - -tests { - test("Outgoing Payment id is string", function() { - expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Quote.bru deleted file mode 100644 index 26e82d0288..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Quote.bru +++ /dev/null @@ -1,71 +0,0 @@ -meta { - name: Create Quote - type: graphql - seq: 2 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateQuote($input: CreateQuoteInput!) { - createQuote(input: $input) { - quote { - createdAt - expiresAt - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "receiver": "{{receiverId}}", - "debitAmount": { - "assetCode": "USD", - "assetScale": 2, - "value": 500 - } - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - await scripts.loadWalletAddressIdsIntoVariables(); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("quoteId", body.data.createQuote.quote.id); - } -} - -tests { - test("Quote id is string", function() { - expect(bru.getEnvVar("quoteId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Receiver -remote Incoming Payment-.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Receiver -remote Incoming Payment-.bru deleted file mode 100644 index 20c36a034d..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Fixed Send)/Create Receiver -remote Incoming Payment-.bru +++ /dev/null @@ -1,73 +0,0 @@ -meta { - name: Create Receiver -remote Incoming Payment- - type: graphql - seq: 1 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateReceiver($input: CreateReceiverInput!) { - createReceiver(input: $input) { - receiver { - completed - createdAt - expiresAt - metadata - id - incomingAmount { - assetCode - assetScale - value - } - walletAddressUrl - receivedAmount { - assetCode - assetScale - value - } - updatedAt - } - } - } -} - -body:graphql:vars { - { - "input": { - "metadata": { - "description": "For lunch!" - }, - "incomingAmount": { - "assetCode": "USD", - "assetScale": 2, - "value": 500 - }, - "walletAddressUrl": "https://happy-life-bank-backend/accounts/pfry" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); - } -} - -tests { - test("Receiver id is string", function() { - expect(bru.getEnvVar("receiverId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Outgoing Payment.bru deleted file mode 100644 index 4bfde2f666..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Outgoing Payment.bru +++ /dev/null @@ -1,72 +0,0 @@ -meta { - name: Create Outgoing Payment - type: graphql - seq: 3 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { - createOutgoingPayment(input: $input) { - payment { - createdAt - error - metadata - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "quoteId": "{{quoteId}}" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); - } -} - -tests { - test("Outgoing Payment id is string", function() { - expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Quote.bru deleted file mode 100644 index 4b293be810..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Create Quote.bru +++ /dev/null @@ -1,66 +0,0 @@ -meta { - name: Create Quote - type: graphql - seq: 2 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateQuote($input: CreateQuoteInput!) { - createQuote(input: $input) { - quote { - createdAt - expiresAt - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "receiver": "{{receiverId}}" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - await scripts.loadWalletAddressIdsIntoVariables(); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("quoteId", body.data.createQuote.quote.id); - } -} - -tests { - test("Quote id is string", function() { - expect(bru.getEnvVar("quoteId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Get Outgoing Payment.bru deleted file mode 100644 index cfca035df3..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local)/Get Outgoing Payment.bru +++ /dev/null @@ -1,57 +0,0 @@ -meta { - name: Get Outgoing Payment - type: graphql - seq: 4 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - query GetOutgoingPayment($id: String!) { - outgoingPayment(id: $id) { - createdAt - error - metadata - id - grantId - walletAddressId - quote { - id - } - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } -} - -body:graphql:vars { - { - "id": "{{outgoingPaymentId}}" - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Outgoing Payment.bru deleted file mode 100644 index 4bfde2f666..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Outgoing Payment.bru +++ /dev/null @@ -1,72 +0,0 @@ -meta { - name: Create Outgoing Payment - type: graphql - seq: 3 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateOutgoingPayment($input: CreateOutgoingPaymentInput!) { - createOutgoingPayment(input: $input) { - payment { - createdAt - error - metadata - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "quoteId": "{{quoteId}}" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("outgoingPaymentId", body.data.createOutgoingPayment.payment.id); - } -} - -tests { - test("Outgoing Payment id is string", function() { - expect(bru.getEnvVar("outgoingPaymentId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Quote.bru deleted file mode 100644 index 108fde9f14..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Quote.bru +++ /dev/null @@ -1,71 +0,0 @@ -meta { - name: Create Quote - type: graphql - seq: 2 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateQuote($input: CreateQuoteInput!) { - createQuote(input: $input) { - quote { - createdAt - expiresAt - id - walletAddressId - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - } - } - } -} - -body:graphql:vars { - { - "input": { - "walletAddressId": "{{gfranklinWalletAddressId}}", - "receiver": "{{receiverId}}", - "debitAmount": { - "assetCode": "USD", - "assetScale": 2, - "value": 500 - } - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - await scripts.loadWalletAddressIdsIntoVariables(); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("quoteId", body.data.createQuote.quote.id); - } -} - -tests { - test("Quote id is string", function() { - expect(bru.getEnvVar("quoteId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Receiver -remote Incoming Payment-.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Receiver -remote Incoming Payment-.bru deleted file mode 100644 index 4738401863..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Create Receiver -remote Incoming Payment-.bru +++ /dev/null @@ -1,73 +0,0 @@ -meta { - name: Create Receiver -remote Incoming Payment- - type: graphql - seq: 1 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - mutation CreateReceiver($input: CreateReceiverInput!) { - createReceiver(input: $input) { - receiver { - completed - createdAt - expiresAt - metadata - id - incomingAmount { - assetCode - assetScale - value - } - walletAddressUrl - receivedAmount { - assetCode - assetScale - value - } - updatedAt - } - } - } -} - -body:graphql:vars { - { - "input": { - "metadata": { - "description": "For lunch!" - }, - // "incomingAmount": { - // "assetCode": "USD", - // "assetScale": 2, - // "value": 500 - // }, - "walletAddressUrl": "https://cloud-nine-wallet-backend/accounts/bhamchest" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.data) { - bru.setEnvVar("receiverId", body.data.createReceiver.receiver.id); - } -} - -tests { - test("Receiver id is string", function() { - expect(bru.getEnvVar("receiverId")).to.be.a("string"); - }) -} diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Get Outgoing Payment.bru deleted file mode 100644 index cfca035df3..0000000000 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Payment (Local, Fixed Send)/Get Outgoing Payment.bru +++ /dev/null @@ -1,57 +0,0 @@ -meta { - name: Get Outgoing Payment - type: graphql - seq: 4 -} - -post { - url: {{RafikiGraphqlHost}}/graphql - body: graphql - auth: none -} - -body:graphql { - query GetOutgoingPayment($id: String!) { - outgoingPayment(id: $id) { - createdAt - error - metadata - id - grantId - walletAddressId - quote { - id - } - receiveAmount { - assetCode - assetScale - value - } - receiver - debitAmount { - assetCode - assetScale - value - } - sentAmount { - assetCode - assetScale - value - } - state - stateAttempts - } - } -} - -body:graphql:vars { - { - "id": "{{outgoingPaymentId}}" - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addApiSignatureHeader(); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Continuation Request.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Continuation Request.bru deleted file mode 100644 index 01d39ebf3f..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Continuation Request.bru +++ /dev/null @@ -1,33 +0,0 @@ -meta { - name: Continuation Request - type: http - seq: 8 -} - -post { - url: {{senderOpenPaymentsAuthHost}}/continue/{{continueId}} - body: json - auth: none -} - -headers { - Authorization: GNAP {{continueToken}} -} - -script:pre-request { - const scripts = require('./scripts'); - - await scripts.addSignatureHeaders(); -} - -script:post-response { - const scripts = require('./scripts'); - - scripts.storeTokenDetails(); -} - -tests { - test("Status code is 200", function() { - expect(res.getStatus()).to.equal(200); - }); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Incoming Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Incoming Payment.bru deleted file mode 100644 index 093a481bae..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Incoming Payment.bru +++ /dev/null @@ -1,55 +0,0 @@ -meta { - name: Create Incoming Payment - type: http - seq: 4 -} - -post { - url: {{receiverOpenPaymentsHost}}/incoming-payments - body: json - auth: none -} - -headers { - Authorization: GNAP {{accessToken}} -} - -body:json { - { - "walletAddress": "{{receiverWalletAddress}}", - "incomingAmount": { - "value": "100", - "assetCode": "{{receiverAssetCode}}", - "assetScale": {{receiverAssetScale}} - }, - "expiresAt": "{{tomorrow}}", - "metadata": { - "description": "Free Money!" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - bru.setEnvVar("tomorrow", (new Date(new Date().setDate(new Date().getDate() + 1))).toISOString()); - - scripts.addHostHeader(); - - await scripts.addSignatureHeaders(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.id) { - bru.setEnvVar("incomingPaymentId", body.id.split("/").pop()); - } - -} - -tests { - test("Status code is 201", function() { - expect(res.getStatus()).to.equal(201); - }); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Outgoing Payment.bru deleted file mode 100644 index 6014dda378..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Outgoing Payment.bru +++ /dev/null @@ -1,48 +0,0 @@ -meta { - name: Create Outgoing Payment - type: http - seq: 9 -} - -post { - url: {{senderOpenPaymentsHost}}/outgoing-payments - body: json - auth: none -} - -headers { - Authorization: GNAP {{accessToken}} -} - -body:json { - { - "walletAddress": "{{senderWalletAddress}}", - "quoteId": "{{senderWalletAddress}}/quotes/{{quoteId}}", - "metadata": { - "description": "Free Money!" - } - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addHostHeader(); - - await scripts.addSignatureHeaders(); -} - -script:post-response { - const body = res.getBody(); - - if (body?.id) { - bru.setEnvVar("outgoingPaymentId", body.id.split("/").pop()); - } - -} - -tests { - test("Status code is 201", function() { - expect(res.getStatus()).to.equal(201); - }); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Quote.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Quote.bru deleted file mode 100644 index a7708217f4..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Create Quote.bru +++ /dev/null @@ -1,47 +0,0 @@ -meta { - name: Create Quote - type: http - seq: 6 -} - -post { - url: {{senderOpenPaymentsHost}}/quotes - body: json - auth: none -} - -headers { - Authorization: GNAP {{accessToken}} -} - -body:json { - { - "walletAddress": "{{senderWalletAddress}}", - "receiver": "{{receiverOpenPaymentsHost}}/incoming-payments/{{incomingPaymentId}}", - "method": "ilp" - } -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addHostHeader(); - - await scripts.addSignatureHeaders(); -} - -script:post-response { - const body = res.getBody(); - if (body?.id) { - bru.setEnvVar("quoteId", body.id.split("/").pop()); - bru.setEnvVar("quoteDebitAmount", JSON.stringify(body.debitAmount)) - bru.setEnvVar("quoteReceiveAmount", JSON.stringify(body.receiveAmount)) - } - -} - -tests { - test("Status code is 201", function() { - expect(res.getStatus()).to.equal(201); - }); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get Outgoing Payment.bru deleted file mode 100644 index 4946e1b040..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get Outgoing Payment.bru +++ /dev/null @@ -1,29 +0,0 @@ -meta { - name: Get Outgoing Payment - type: http - seq: 10 -} - -get { - url: {{senderOpenPaymentsHost}}/outgoing-payments/{{outgoingPaymentId}} - body: none - auth: none -} - -headers { - Authorization: GNAP {{accessToken}} -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addHostHeader(); - - await scripts.addSignatureHeaders(); -} - -tests { - test("Status code is 200", function() { - expect(res.getStatus()).to.equal(200); - }); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru deleted file mode 100644 index 85b0b38d0e..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get receiver wallet address.bru +++ /dev/null @@ -1,51 +0,0 @@ -meta { - name: Get receiver wallet address - type: http - seq: 2 -} - -get { - url: http://localhost:3000/accounts/bhamchest - body: none - auth: none -} - -headers { - Accept: application/json -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addHostHeader("receiverOpenPaymentsHost"); -} - -script:post-response { - const url = require('url') - - if (res.getStatus() !== 200) { - return - } - - const body = res.getBody() - bru.setEnvVar("receiverWalletAddress", "http://localhost:3000/accounts/bhamchest") - bru.setEnvVar("receiverAssetCode", body?.assetCode) - bru.setEnvVar("receiverAssetScale", body?.assetScale) - - const authUrl = url.parse(body?.authServer) - if ( - authUrl.hostname.includes('cloud-nine-wallet') || - authUrl.hostname.includes('happy-life-bank') - ){ - const port = authUrl.hostname.includes('cloud-nine-wallet')? authUrl.port: Number(authUrl.port) + 1000 - bru.setEnvVar("receiverOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port ); - } else { - bru.setEnvVar("receiverOpenPaymentsAuthHost", body?.authServer); - } -} - -tests { - test("Status code is 200", function() { - expect(res.getStatus()).to.equal(200); - }); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get sender wallet address.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Get sender wallet address.bru deleted file mode 100644 index 9665a40e32..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Get sender wallet address.bru +++ /dev/null @@ -1,50 +0,0 @@ -meta { - name: Get sender wallet address - type: http - seq: 1 -} - -get { - url: {{senderWalletAddress}} - body: none - auth: none -} - -headers { - Accept: application/json -} - -script:pre-request { - const scripts = require('./scripts'); - - scripts.addHostHeader("senderOpenPaymentsHost"); -} - -script:post-response { - const url = require('url') - - if (res.getStatus() !== 200) { - return - } - - const body = res.getBody() - bru.setEnvVar("senderAssetCode", body?.assetCode) - bru.setEnvVar("senderAssetScale", body?.assetScale) - - const authUrl = url.parse(body?.authServer) - if ( - authUrl.hostname.includes('cloud-nine-wallet') || - authUrl.hostname.includes('happy-life-bank') - ){ - const port = authUrl.hostname.includes('cloud-nine-wallet')? authUrl.port: Number(authUrl.port) + 1000 - bru.setEnvVar("senderOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port ); - } else { - bru.setEnvVar("senderOpenPaymentsAuthHost", body?.authServer); - } -} - -tests { - test("Status code is 200", function() { - expect(res.getStatus()).to.equal(200); - }); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru deleted file mode 100644 index 7fd03f2ee3..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Incoming Payment.bru +++ /dev/null @@ -1,45 +0,0 @@ -meta { - name: Grant Request Incoming Payment - type: http - seq: 3 -} - -post { - url: {{senderOpenPaymentsAuthHost}}/ - body: json - auth: none -} - -body:json { - { - "access_token": { - "access": [ - { - "type": "incoming-payment", - "actions": [ - "create", "read", "list", "complete" - ] - } - ] - }, - "client": "{{clientWalletAddress}}" - } -} - -script:pre-request { - const scripts = require('./scripts'); - - await scripts.addSignatureHeaders(); -} - -script:post-response { - const scripts = require('./scripts'); - - scripts.storeTokenDetails(); -} - -tests { - test("Status code is 200", function() { - expect(res.getStatus()).to.equal(200); - }); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Outgoing Payment.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Outgoing Payment.bru deleted file mode 100644 index 5be7a46476..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Outgoing Payment.bru +++ /dev/null @@ -1,56 +0,0 @@ -meta { - name: Grant Request Outgoing Payment - type: http - seq: 7 -} - -post { - url: {{senderOpenPaymentsAuthHost}}/ - body: json - auth: none -} - -body:json { - { - "access_token": { - "access": [ - { - "type": "outgoing-payment", - "actions": [ - "create", "read", "list" - ], - "identifier": "{{senderWalletAddress}}", - "limits": { - "debitAmount": {{quoteDebitAmount}}, - "receiveAmount": {{quoteReceiveAmount}} - } - } - ] - }, - "client": "{{clientWalletAddress}}", - "interact": { - "start": [ - "redirect" - ] - } - } - -} - -script:pre-request { - const scripts = require('./scripts'); - - await scripts.addSignatureHeaders(); -} - -script:post-response { - const scripts = require('./scripts'); - - scripts.storeTokenDetails(); -} - -tests { - test("Status code is 200", function() { - expect(res.getStatus()).to.equal(200); - }); -} diff --git a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Quote.bru b/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Quote.bru deleted file mode 100644 index 3c0736670d..0000000000 --- a/bruno/collections/Rafiki/Examples/Open Payments (local)/Grant Request Quote.bru +++ /dev/null @@ -1,46 +0,0 @@ -meta { - name: Grant Request Quote - type: http - seq: 5 -} - -post { - url: {{senderOpenPaymentsAuthHost}}/ - body: json - auth: none -} - -body:json { - { - "access_token": { - "access": [ - { - "type": "quote", - "actions": [ - "create", "read" - ] - } - ] - }, - "client": "{{clientWalletAddress}}" - } - -} - -script:pre-request { - const scripts = require('./scripts'); - - await scripts.addSignatureHeaders(); -} - -script:post-response { - const scripts = require('./scripts'); - - scripts.storeTokenDetails(); -} - -tests { - test("Status code is 200", function() { - expect(res.getStatus()).to.equal(200); - }); -} From 61b1010e2d76b149c93ea77ccd47293df2d0404b Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:17:08 -0400 Subject: [PATCH 55/64] refactor: mv debitAmountMinusFees to fee calc and clarify TODO --- .../src/open_payments/quote/service.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index bc806ac31d..335deee2f4 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -228,6 +228,7 @@ export async function resolveReceiver( interface CalculateQuoteAmountsWithFeesResult { receiveAmountValue: bigint debitAmountValue: bigint + debitAmountMinusFees: bigint } /** @@ -239,7 +240,7 @@ function calculateFixedSendQuoteAmounts( quote: Quote, maxReceiveAmountValue: bigint ): CalculateQuoteAmountsWithFeesResult { - // TODO: derive fee from debitAmount and convert that to receiveAmount + // 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 @@ -262,10 +263,15 @@ function calculateFixedSendQuoteAmounts( 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 @@ -275,6 +281,7 @@ function calculateFixedSendQuoteAmounts( return { debitAmountValue: quote.debitAmount.value, + debitAmountMinusFees, receiveAmountValue } } @@ -306,6 +313,7 @@ function calculateFixedDeliveryQuoteAmounts( return { debitAmountValue, + debitAmountMinusFees: quote.debitAmount.value, receiveAmountValue: quote.receiveAmount.value } } @@ -356,16 +364,13 @@ async function finalizeQuote( `Calculating ${maxReceiveAmountValue ? 'fixed-send' : 'fixed-delivery'} quote amount with fees` ) - const { debitAmountValue, receiveAmountValue } = maxReceiveAmountValue - ? calculateFixedSendQuoteAmounts(deps, quote, maxReceiveAmountValue) - : calculateFixedDeliveryQuoteAmounts(deps, quote) + const { debitAmountValue, debitAmountMinusFees, receiveAmountValue } = + maxReceiveAmountValue + ? calculateFixedSendQuoteAmounts(deps, quote, maxReceiveAmountValue) + : calculateFixedDeliveryQuoteAmounts(deps, quote) const patchOptions = { - debitAmountMinusFees: maxReceiveAmountValue - ? // TODO: change calculateFixedSendQuoteAmounts to return the debitAmountMinusFees if using new calculation - quote.debitAmount.value - - (quote.fee?.calculate(quote.debitAmount.value) ?? 0n) - : quote.debitAmount.value, + debitAmountMinusFees, debitAmountValue, receiveAmountValue, expiresAt: calculateExpiry(deps, quote, receiver) From 6c995d9a274adc2a86287e40882df2b76e8eea8c Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:19:31 -0400 Subject: [PATCH 56/64] Update bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru Co-authored-by: Max Kurapov --- .../Create Receiver -remote Incoming Payment-.bru | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru index 7af9c1b345..f72cbbd483 100644 --- a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru +++ b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru @@ -1,5 +1,5 @@ meta { - name: Create Receiver -remote Incoming Payment- + name: Create Receiver -local Incoming Payment- type: graphql seq: 1 } From 86f6db6d0b91ae06d27a8344395cac55b2bde15b Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:24:56 -0400 Subject: [PATCH 57/64] fix: make timeout required again Making optional depends on single phase transfer --- packages/backend/src/accounting/psql/service.ts | 2 +- packages/backend/src/accounting/service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/accounting/psql/service.ts b/packages/backend/src/accounting/psql/service.ts index 6da25ecb96..e427cd26b5 100644 --- a/packages/backend/src/accounting/psql/service.ts +++ b/packages/backend/src/accounting/psql/service.ts @@ -264,7 +264,7 @@ export async function createTransfer( debitAccount: accountMap[transfer.sourceAccountId], creditAccount: accountMap[transfer.destinationAccountId], amount: transfer.amount, - timeoutMs: args.timeout ? BigInt(args.timeout * 1000) : undefined + timeoutMs: BigInt(args.timeout * 1000) })) ) ) diff --git a/packages/backend/src/accounting/service.ts b/packages/backend/src/accounting/service.ts index ba6adb1f85..bdbbc3c2fc 100644 --- a/packages/backend/src/accounting/service.ts +++ b/packages/backend/src/accounting/service.ts @@ -65,7 +65,7 @@ export interface TransferOptions extends BaseTransfer { destinationAccount: LiquidityAccount sourceAmount: bigint destinationAmount?: bigint - timeout?: number + timeout: number } export interface Transaction { From 3fc69247e9838c124bdadebe0c726b1d16b888cf Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:22:59 -0400 Subject: [PATCH 58/64] feat: error when post fails in local pay --- .../src/payment-method/local/service.test.ts | 41 ++++++++++++++++++- .../src/payment-method/local/service.ts | 9 +++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 417e1eee24..3fce7e5b86 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -12,7 +12,7 @@ import { WalletAddress } from '../../open_payments/wallet_address/model' import { createReceiver } from '../../tests/receiver' import { mockRatesApi } from '../../tests/rates' -import { AccountingService } from '../../accounting/service' +import { AccountingService, Transaction } from '../../accounting/service' import { truncateTables } from '../../tests/tableManager' import { createOutgoingPaymentWithReceiver } from '../../tests/outgoingPayment' import { OutgoingPayment } from '../../open_payments/payment/outgoing/model' @@ -610,5 +610,44 @@ describe('LocalPaymentService', (): void => { expect((err as PaymentMethodHandlerError).retryable).toBe(false) } }) + + test('throws error when transfer post fails', async (): Promise => { + const { receiver, outgoingPayment } = + await createOutgoingPaymentWithReceiver(deps, { + sendingWalletAddress: walletAddressMap['USD'], + receivingWalletAddress: walletAddressMap['USD'], + method: 'ilp', + quoteOptions: { + debitAmount: { + value: 100n, + assetScale: walletAddressMap['USD'].asset.scale, + assetCode: walletAddressMap['USD'].asset.code + } + } + }) + + jest.spyOn(accountingService, 'createTransfer').mockResolvedValueOnce({ + post: () => Promise.resolve(TransferError.UnknownTransfer) + } as Transaction) + + expect.assertions(4) + try { + await localPaymentService.pay({ + receiver, + outgoingPayment, + finalDebitAmount: 100n, + finalReceiveAmount: 100n + }) + } catch (err) { + expect(err).toBeInstanceOf(PaymentMethodHandlerError) + expect((err as PaymentMethodHandlerError).message).toBe( + 'Received error during local payment' + ) + expect((err as PaymentMethodHandlerError).description).toBe( + errorToMessage[TransferError.UnknownTransfer] + ) + expect((err as PaymentMethodHandlerError).retryable).toBe(false) + } + }) }) }) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 48841c1328..93748aca56 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -284,5 +284,12 @@ async function pay( ) } } - await trxOrError.post() + const transferError = await trxOrError.post() + + if (isTransferError(transferError)) { + throw new PaymentMethodHandlerError('Received error during local payment', { + description: errorToMessage[transferError], + retryable: false + }) + } } From 6634b8f2f77a0a04c540e4720f5a7930de8c8528 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:28:00 -0400 Subject: [PATCH 59/64] refactor(backend): add optional quoteId to getQuote args - add runtime check to ilp payment method implementation requireing it --- .../src/open_payments/quote/service.ts | 19 ++++++++++ .../payment-method/handler/service.test.ts | 2 + .../src/payment-method/handler/service.ts | 1 + .../src/payment-method/ilp/service.test.ts | 37 +++++++++++++++++++ .../backend/src/payment-method/ilp/service.ts | 7 ++++ 5 files changed, 66 insertions(+) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 335deee2f4..f243914081 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -20,6 +20,7 @@ import { PaymentMethodHandlerError, PaymentMethodHandlerErrorCode } from '../../payment-method/handler/errors' +import { v4 as uuid } from 'uuid' const MAX_INT64 = BigInt('9223372036854775807') @@ -80,6 +81,22 @@ export type CreateQuoteOptions = | QuoteOptionsWithDebitAmount | QuoteOptionsWithReceiveAmount +// TODO: refactor +// - re-model +// - [ ] remove IlpQuoteDetails.quoteId FK relation so i can insert that first +// - [ ] remove relationship on Quote objection model (and ilp quote details?) +// - quote service +// - [X] make id for quote +// - [X] pass id into getQuote +// - [X] use id for quote create +// ... make payment method service changes +// - [ ] dont do anything with additionalFields +// - payment method service +// - [ ] take quoteId +// - [ ] insert ilpQuoteDetails at end of method and use the given quoteId +// - [ ] remove additionalFields if unused +// - new tests (in addition to updating tests based on above) +// - [ ] ilpQuoteDetails should be checked in payment method getQuote tests. not quote create, as currently async function createQuote( deps: ServiceDependencies, options: CreateQuoteOptions @@ -114,9 +131,11 @@ async function createQuote( try { const receiver = await resolveReceiver(deps, options) const paymentMethod = receiver.isLocal ? 'LOCAL' : 'ILP' + const quoteId = uuid() const quote = await deps.paymentMethodHandlerService.getQuote( paymentMethod, { + quoteId, walletAddress, receiver, receiveAmount: options.receiveAmount, diff --git a/packages/backend/src/payment-method/handler/service.test.ts b/packages/backend/src/payment-method/handler/service.test.ts index 987f07354a..0950feda7b 100644 --- a/packages/backend/src/payment-method/handler/service.test.ts +++ b/packages/backend/src/payment-method/handler/service.test.ts @@ -16,6 +16,7 @@ import { IlpPaymentService } from '../ilp/service' import { truncateTables } from '../../tests/tableManager' import { createOutgoingPaymentWithReceiver } from '../../tests/outgoingPayment' import { LocalPaymentService } from '../local/service' +import { v4 as uuid } from 'uuid' describe('PaymentMethodHandlerService', (): void => { let deps: IocContract @@ -50,6 +51,7 @@ describe('PaymentMethodHandlerService', (): void => { }) const options: StartQuoteOptions = { + quoteId: uuid(), walletAddress, receiver: await createReceiver(deps, walletAddress), debitAmount: { diff --git a/packages/backend/src/payment-method/handler/service.ts b/packages/backend/src/payment-method/handler/service.ts index c7bb2dfc0e..32486ef4cf 100644 --- a/packages/backend/src/payment-method/handler/service.ts +++ b/packages/backend/src/payment-method/handler/service.ts @@ -7,6 +7,7 @@ import { IlpPaymentService } from '../ilp/service' import { LocalPaymentService } from '../local/service' export interface StartQuoteOptions { + quoteId?: string walletAddress: WalletAddress debitAmount?: Amount receiveAmount?: Amount diff --git a/packages/backend/src/payment-method/ilp/service.test.ts b/packages/backend/src/payment-method/ilp/service.test.ts index 5aa742821c..d1d198f2df 100644 --- a/packages/backend/src/payment-method/ilp/service.test.ts +++ b/packages/backend/src/payment-method/ilp/service.test.ts @@ -23,6 +23,7 @@ import { AccountingService } from '../../accounting/service' import { IncomingPayment } from '../../open_payments/payment/incoming/model' import { truncateTables } from '../../tests/tableManager' import { createOutgoingPaymentWithReceiver } from '../../tests/outgoingPayment' +import { v4 as uuid } from 'uuid' const nock = (global as unknown as { nock: typeof import('nock') }).nock @@ -90,6 +91,7 @@ describe('IlpPaymentService', (): void => { const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) const options: StartQuoteOptions = { + quoteId: uuid(), walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']), debitAmount: { @@ -108,6 +110,32 @@ describe('IlpPaymentService', (): void => { ratesScope.done() }) + test('Throws if quoteId is not provided', async (): Promise => { + const options: StartQuoteOptions = { + walletAddress: walletAddressMap['USD'], + receiver: await createReceiver(deps, walletAddressMap['USD']), + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 100n + } + } + + expect.assertions(4) + try { + await ilpPaymentService.getQuote(options) + } catch (error) { + expect(error).toBeInstanceOf(PaymentMethodHandlerError) + expect((error as PaymentMethodHandlerError).message).toBe( + 'Received error during ILP quoting' + ) + expect((error as PaymentMethodHandlerError).description).toBe( + 'quoteId is required for ILP quotes' + ) + expect((error as PaymentMethodHandlerError).retryable).toBe(false) + } + }) + test('fails on rate service error', async (): Promise => { const ratesService = await deps.use('ratesService') jest @@ -117,6 +145,7 @@ describe('IlpPaymentService', (): void => { expect.assertions(4) try { await ilpPaymentService.getQuote({ + quoteId: uuid(), walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']), debitAmount: { @@ -141,6 +170,7 @@ describe('IlpPaymentService', (): void => { const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) const options: StartQuoteOptions = { + quoteId: uuid(), walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']), debitAmount: { @@ -184,6 +214,7 @@ describe('IlpPaymentService', (): void => { } const options: StartQuoteOptions = { + quoteId: uuid(), walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD'], { incomingAmount @@ -218,6 +249,7 @@ describe('IlpPaymentService', (): void => { expect.assertions(4) try { await ilpPaymentService.getQuote({ + quoteId: uuid(), walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']), debitAmount: { @@ -243,6 +275,7 @@ describe('IlpPaymentService', (): void => { const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) const options: StartQuoteOptions = { + quoteId: uuid(), walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']) } @@ -272,6 +305,7 @@ describe('IlpPaymentService', (): void => { const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) const options: StartQuoteOptions = { + quoteId: uuid(), walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD'], { incomingAmount: { @@ -311,6 +345,7 @@ describe('IlpPaymentService', (): void => { const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) const options: StartQuoteOptions = { + quoteId: uuid(), walletAddress: walletAddressMap['USD'], receiver: await createReceiver(deps, walletAddressMap['USD']) } @@ -371,6 +406,7 @@ describe('IlpPaymentService', (): void => { const sendingWalletAddress = walletAddressMap[debitAssetCode] const options: StartQuoteOptions = { + quoteId: uuid(), walletAddress: sendingWalletAddress, receiver: await createReceiver(deps, receivingWalletAddress), receiveAmount: { @@ -430,6 +466,7 @@ describe('IlpPaymentService', (): void => { const sendingWalletAddress = walletAddressMap[debitAssetCode] const options: StartQuoteOptions = { + quoteId: uuid(), walletAddress: sendingWalletAddress, receiver: await createReceiver(deps, receivingWalletAddress), debitAmount: { diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index 1b54d5251b..bdfad1627a 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -44,6 +44,13 @@ async function getQuote( deps: ServiceDependencies, options: StartQuoteOptions ): Promise { + if (!options.quoteId) { + throw new PaymentMethodHandlerError('Received error during ILP quoting', { + description: 'quoteId is required for ILP quotes', + retryable: false + }) + } + const rates = await deps.ratesService .rates(options.walletAddress.asset.code) .catch((_err: Error) => { From 6706557572dc878bfb7a388f35ae9bd3f9cda94f Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:45:22 -0400 Subject: [PATCH 60/64] refactor: rm ilp quote details out of quote service --- .../20240916181659_add_ilp_quote_details.js | 8 +- .../backend/src/open_payments/quote/model.ts | 10 -- .../src/open_payments/quote/service.test.ts | 98 ++++++------------- .../src/open_payments/quote/service.ts | 38 +++---- .../payment-method/ilp/quote-details/model.ts | 16 --- .../backend/src/payment-method/ilp/service.ts | 28 +++--- packages/backend/src/tests/quote.ts | 28 +++--- 7 files changed, 77 insertions(+), 149 deletions(-) diff --git a/packages/backend/migrations/20240916181659_add_ilp_quote_details.js b/packages/backend/migrations/20240916181659_add_ilp_quote_details.js index 1c6e6c3e7a..bd84708d2c 100644 --- a/packages/backend/migrations/20240916181659_add_ilp_quote_details.js +++ b/packages/backend/migrations/20240916181659_add_ilp_quote_details.js @@ -8,8 +8,12 @@ exports.up = function (knex) { // Create new table with columns from "quotes" to migrate .createTable('ilpQuoteDetails', function (table) { table.uuid('id').notNullable().primary() - table.uuid('quoteId').notNullable().unique() - table.foreign('quoteId').references('quotes.id') + + // quoteId is purposefully not a FK referencing quote.id + // this allows us to create ilpQuoteDetail before quotes in service of + // fully decoupling payment method/quote services. + // https://github.com/interledger/rafiki/pull/2857#discussion_r1825891327 + table.uuid('quoteId').notNullable().unique().index() table.bigInteger('maxPacketAmount').notNullable() table.decimal('minExchangeRateNumerator', 64, 0).notNullable() diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index cc01498759..3c6bd6d135 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -7,7 +7,6 @@ import { import { Asset } from '../../asset/model' import { Quote as OpenPaymentsQuote } from '@interledger/open-payments' import { Fee } from '../../fee/model' -import { IlpQuoteDetails } from '../../payment-method/ilp/quote-details/model' export class Quote extends WalletAddressSubresource { public static readonly tableName = 'quotes' @@ -25,7 +24,6 @@ export class Quote extends WalletAddressSubresource { public feeId?: string public fee?: Fee - public ilpQuoteDetails?: IlpQuoteDetails public debitAmountMinusFees?: bigint static get relationMappings() { @@ -46,14 +44,6 @@ export class Quote extends WalletAddressSubresource { from: 'quotes.feeId', to: 'fees.id' } - }, - ilpQuoteDetails: { - relation: Model.HasOneRelation, - modelClass: IlpQuoteDetails, - join: { - from: 'ilpQuoteDetails.quoteId', - to: 'quotes.id' - } } } } diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index dee858bd08..7011c0d38b 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -36,7 +36,6 @@ import { PaymentMethodHandlerErrorCode } from '../../payment-method/handler/errors' import { Receiver } from '../receiver/model' -import { IlpQuoteDetails } from '../../payment-method/ilp/quote-details/model' describe('QuoteService', (): void => { let deps: IocContract @@ -146,34 +145,8 @@ describe('QuoteService', (): void => { withFee: true, method: 'ilp' }), - get: async (options) => { - const quote = await quoteService.get(options) - assert.ok(!isQuoteError(quote)) - - if (!quote) { - return - } - - quote.ilpQuoteDetails = await IlpQuoteDetails.query() - .where({ quoteId: quote.id }) - .first() - - return quote - }, - list: async (options) => { - const quotes = await quoteService.getWalletAddressPage(options) - - const quotesWithDetails = await Promise.all( - quotes.map(async (q) => { - q.ilpQuoteDetails = await IlpQuoteDetails.query() - .where({ quoteId: q.id }) - .first() - return q - }) - ) - - return quotesWithDetails - } + get: (options) => quoteService.get(options), + list: (options) => quoteService.getWalletAddressPage(options) }) }) @@ -268,9 +241,6 @@ describe('QuoteService', (): void => { receiver: options.receiver, debitAmount: debitAmount || mockedQuote.debitAmount, receiveAmount: receiveAmount || mockedQuote.receiveAmount, - ilpQuoteDetails: { - maxPacketAmount: BigInt('9223372036854775807') - }, createdAt: expect.any(Date), updatedAt: expect.any(Date), expiresAt: new Date( @@ -279,14 +249,11 @@ describe('QuoteService', (): void => { client: client || null }) - const foundQuote = await quoteService.get({ - id: quote.id - }) - assert(foundQuote) - foundQuote.ilpQuoteDetails = await IlpQuoteDetails.query() - .where({ quoteId: quote.id }) - .first() - expect(foundQuote).toEqual(quote) + await expect( + quoteService.get({ + id: quote.id + }) + ).resolves.toEqual(quote) } ) @@ -358,9 +325,6 @@ describe('QuoteService', (): void => { expect(quote).toMatchObject({ ...options, - ilpQuoteDetails: { - maxPacketAmount: BigInt('9223372036854775807') - }, debitAmount: mockedQuote.debitAmount, receiveAmount: incomingAmount, createdAt: expect.any(Date), @@ -371,14 +335,11 @@ describe('QuoteService', (): void => { client: client || null }) - const foundQuote = await quoteService.get({ - id: quote.id - }) - assert(foundQuote) - foundQuote.ilpQuoteDetails = await IlpQuoteDetails.query() - .where({ quoteId: quote.id }) - .first() - expect(foundQuote).toEqual(quote) + await expect( + quoteService.get({ + id: quote.id + }) + ).resolves.toEqual(quote) } ) } @@ -458,6 +419,7 @@ describe('QuoteService', (): void => { } ) + // TODO: move test? Logic maybe better tests in ilp payment service getQuote test('creates a quote with large exchange rate amounts', async (): Promise => { const receiveAmountValue = 100n const receiver = await createReceiver(deps, receivingWalletAddress, { @@ -473,15 +435,15 @@ describe('QuoteService', (): void => { receiver, walletAddress: sendingWalletAddress, receiveAmountValue - }, - { - additionalFields: { - maxPacketAmount: Pay.Int.MAX_U64, - lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - minExchangeRate: Pay.Ratio.from(10 ** 20) - } } + // { + // additionalFields: { + // maxPacketAmount: Pay.Int.MAX_U64, + // lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), + // highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), + // minExchangeRate: Pay.Ratio.from(10 ** 20) + // } + // } ) jest @@ -495,21 +457,21 @@ describe('QuoteService', (): void => { }) assert.ok(!isQuoteError(quote)) - const ilpQuoteDetails = await IlpQuoteDetails.query() - .where({ quoteId: quote.id }) - .first() + // const ilpQuoteDetails = await IlpQuoteDetails.query() + // .where({ quoteId: quote.id }) + // .first() expect(quote).toMatchObject({ debitAmount: mockedQuote.debitAmount, receiveAmount: receiver.incomingAmount }) - expect(ilpQuoteDetails).toMatchObject({ - maxPacketAmount: BigInt('9223372036854775807'), - lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - minExchangeRate: Pay.Ratio.from(10 ** 20) - }) + // expect(ilpQuoteDetails).toMatchObject({ + // maxPacketAmount: BigInt('9223372036854775807'), + // lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), + // highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), + // minExchangeRate: Pay.Ratio.from(10 ** 20) + // }) }) test('fails on unknown wallet address', async (): Promise => { diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index f243914081..5b164e0727 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -148,34 +148,20 @@ async function createQuote( FeeType.Sending ) - const graph: PartialModelGraph = { - 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 - } - - if (paymentMethod === 'ILP') { - const maxPacketAmount = quote.additionalFields.maxPacketAmount as bigint - graph.ilpQuoteDetails = { - maxPacketAmount: - MAX_INT64 < maxPacketAmount ? MAX_INT64 : maxPacketAmount, // Cap at MAX_INT64 because of postgres type limits. - minExchangeRate: quote.additionalFields.minExchangeRate as Pay.Ratio, - lowEstimatedExchangeRate: quote.additionalFields - .lowEstimatedExchangeRate as Pay.Ratio, - highEstimatedExchangeRate: quote.additionalFields - .highEstimatedExchangeRate as Pay.PositiveRatio - } - } - return await Quote.transaction(deps.knex, async (trx) => { const createdQuote = await Quote.query(trx) - .insertGraphAndFetch(graph) + .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('[asset, fee, walletAddress]') return await finalizeQuote( diff --git a/packages/backend/src/payment-method/ilp/quote-details/model.ts b/packages/backend/src/payment-method/ilp/quote-details/model.ts index dbe47550b7..b0d8ed4ac3 100644 --- a/packages/backend/src/payment-method/ilp/quote-details/model.ts +++ b/packages/backend/src/payment-method/ilp/quote-details/model.ts @@ -1,5 +1,3 @@ -import { Model } from 'objection' -import { Quote } from '../../../open_payments/quote/model' import { BaseModel } from '../../../shared/baseModel' import * as Pay from '@interledger/pay' @@ -15,7 +13,6 @@ export class IlpQuoteDetails extends BaseModel { } public quoteId!: string - public quote?: Quote public maxPacketAmount!: bigint public minExchangeRateNumerator!: bigint @@ -25,19 +22,6 @@ export class IlpQuoteDetails extends BaseModel { public highEstimatedExchangeRateNumerator!: bigint public highEstimatedExchangeRateDenominator!: bigint - static get relationMappings() { - return { - quote: { - relation: Model.BelongsToOneRelation, - modelClass: Quote, - join: { - from: 'ilpQuoteDetails.quoteId', - to: 'quotes.id' - } - } - } - } - public get minExchangeRate(): Pay.Ratio { return Pay.Ratio.of( Pay.Int.from(this.minExchangeRateNumerator) as Pay.PositiveInt, diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index bdfad1627a..e7d98fd91e 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -223,22 +223,18 @@ async function pay( }) } - if (!outgoingPayment.quote.ilpQuoteDetails) { - outgoingPayment.quote.ilpQuoteDetails = await IlpQuoteDetails.query( - deps.knex - ) - .where('quoteId', outgoingPayment.quote.id) - .first() + const ilpQuoteDetails = await IlpQuoteDetails.query(deps.knex) + .where('quoteId', outgoingPayment.quote.id) + .first() - if (!outgoingPayment.quote.ilpQuoteDetails) { - throw new PaymentMethodHandlerError( - 'Could not find required ILP Quote Details', - { - description: 'ILP Quote Details not found', - retryable: false - } - ) - } + if (!ilpQuoteDetails) { + throw new PaymentMethodHandlerError( + 'Could not find required ILP Quote Details', + { + description: 'ILP Quote Details not found', + retryable: false + } + ) } const { @@ -246,7 +242,7 @@ async function pay( highEstimatedExchangeRate, minExchangeRate, maxPacketAmount - } = outgoingPayment.quote.ilpQuoteDetails + } = ilpQuoteDetails const quote: Pay.Quote = { maxPacketAmount, diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index a1996c3b9d..ad09987083 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -9,6 +9,8 @@ import { CreateQuoteOptions } from '../open_payments/quote/service' import { PaymentQuote } from '../payment-method/handler/service' import { WalletAddress } from '../open_payments/wallet_address/model' import { Receiver } from '../open_payments/receiver/model' +import { IlpQuoteDetails } from '../payment-method/ilp/quote-details/model' +import { v4 as uuid } from 'uuid' export type CreateTestQuoteOptions = CreateQuoteOptions & { exchangeRate?: number @@ -158,14 +160,26 @@ export async function createQuote( } } - const withGraphFetchedArray = ['asset', 'walletAddress', 'ilpQuoteDetails'] + const quoteId = uuid() + await IlpQuoteDetails.query().insert({ + quoteId, + lowEstimatedExchangeRate: Pay.Ratio.from(exchangeRate), + highEstimatedExchangeRate: Pay.Ratio.from( + exchangeRate + 0.000000000001 + ) as unknown as Pay.PositiveRatio, + minExchangeRate: Pay.Ratio.from(exchangeRate * 0.99), + maxPacketAmount: BigInt('9223372036854775807') + }) + + const withGraphFetchedArray = ['asset', 'walletAddress'] if (withFee) { withGraphFetchedArray.push('fee') } const withGraphFetchedExpression = `[${withGraphFetchedArray.join(', ')}]` return await Quote.query() - .insertGraphAndFetch({ + .insertAndFetch({ + id: quoteId, walletAddressId, assetId: walletAddress.assetId, receiver: receiverUrl, @@ -174,15 +188,7 @@ export async function createQuote( receiveAmount, estimatedExchangeRate: exchangeRate, expiresAt: new Date(Date.now() + config.quoteLifespan), - client, - ilpQuoteDetails: { - lowEstimatedExchangeRate: Pay.Ratio.from(exchangeRate), - highEstimatedExchangeRate: Pay.Ratio.from( - exchangeRate + 0.000000000001 - ) as unknown as Pay.PositiveRatio, - minExchangeRate: Pay.Ratio.from(exchangeRate * 0.99), - maxPacketAmount: BigInt('9223372036854775807') - } + client }) .withGraphFetched(withGraphFetchedExpression) } From 998ee257409928921e35cc8b3d4e493e2336660e Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:34:02 -0400 Subject: [PATCH 61/64] refactor: insert ilp quote details in ilp getQuote --- .../src/open_payments/quote/service.test.ts | 62 +------- .../src/open_payments/quote/service.ts | 53 +++---- .../src/payment-method/handler/service.ts | 14 +- .../src/payment-method/ilp/service.test.ts | 135 +++++++++++++++++- .../backend/src/payment-method/ilp/service.ts | 26 ++-- .../src/payment-method/local/service.test.ts | 3 +- .../src/payment-method/local/service.ts | 3 +- packages/backend/src/tests/quote.ts | 6 - 8 files changed, 179 insertions(+), 123 deletions(-) diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 7011c0d38b..9e9c8712bf 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -30,7 +30,6 @@ import { Asset } from '../../asset/model' import { PaymentMethodHandlerService } from '../../payment-method/handler/service' import { ReceiverService } from '../receiver/service' import { createReceiver } from '../../tests/receiver' -import * as Pay from '@interledger/pay' import { PaymentMethodHandlerError, PaymentMethodHandlerErrorCode @@ -233,7 +232,8 @@ describe('QuoteService', (): void => { receiver: expect.anything(), receiveAmount: options.receiveAmount, debitAmount: options.debitAmount - }) + }), + expect.anything() ) expect(quote).toMatchObject({ @@ -419,61 +419,6 @@ describe('QuoteService', (): void => { } ) - // TODO: move test? Logic maybe better tests in ilp payment service getQuote - test('creates a quote with large exchange rate amounts', async (): Promise => { - const receiveAmountValue = 100n - const receiver = await createReceiver(deps, receivingWalletAddress, { - incomingAmount: { - assetCode: receivingWalletAddress.asset.code, - assetScale: receivingWalletAddress.asset.scale, - value: receiveAmountValue - } - }) - - const mockedQuote = mockQuote( - { - receiver, - walletAddress: sendingWalletAddress, - receiveAmountValue - } - // { - // additionalFields: { - // maxPacketAmount: Pay.Int.MAX_U64, - // lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - // highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - // minExchangeRate: Pay.Ratio.from(10 ** 20) - // } - // } - ) - - jest - .spyOn(paymentMethodHandlerService, 'getQuote') - .mockResolvedValueOnce(mockedQuote) - - const quote = await quoteService.create({ - walletAddressId: sendingWalletAddress.id, - receiver: receiver.incomingPayment!.id, - method: 'ilp' - }) - assert.ok(!isQuoteError(quote)) - - // const ilpQuoteDetails = await IlpQuoteDetails.query() - // .where({ quoteId: quote.id }) - // .first() - - expect(quote).toMatchObject({ - debitAmount: mockedQuote.debitAmount, - receiveAmount: receiver.incomingAmount - }) - - // expect(ilpQuoteDetails).toMatchObject({ - // maxPacketAmount: BigInt('9223372036854775807'), - // lowEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - // highEstimatedExchangeRate: Pay.Ratio.from(10 ** 20), - // minExchangeRate: Pay.Ratio.from(10 ** 20) - // }) - }) - test('fails on unknown wallet address', async (): Promise => { await expect( quoteService.create({ @@ -827,7 +772,8 @@ describe('QuoteService', (): void => { receiver: expect.anything(), receiveAmount: options.receiveAmount, debitAmount: options.debitAmount - }) + }), + expect.anything() ) expect(quote).toMatchObject({ diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 5b164e0727..415f24da3b 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -1,5 +1,4 @@ -import { PartialModelGraph, TransactionOrKnex } from 'objection' -import * as Pay from '@interledger/pay' +import { TransactionOrKnex } from 'objection' import { BaseService } from '../../shared/baseService' import { QuoteError, isQuoteError } from './errors' @@ -22,8 +21,6 @@ import { } from '../../payment-method/handler/errors' import { v4 as uuid } from 'uuid' -const MAX_INT64 = BigInt('9223372036854775807') - export interface QuoteService extends WalletAddressSubresourceService { create(options: CreateQuoteOptions): Promise } @@ -81,22 +78,6 @@ export type CreateQuoteOptions = | QuoteOptionsWithDebitAmount | QuoteOptionsWithReceiveAmount -// TODO: refactor -// - re-model -// - [ ] remove IlpQuoteDetails.quoteId FK relation so i can insert that first -// - [ ] remove relationship on Quote objection model (and ilp quote details?) -// - quote service -// - [X] make id for quote -// - [X] pass id into getQuote -// - [X] use id for quote create -// ... make payment method service changes -// - [ ] dont do anything with additionalFields -// - payment method service -// - [ ] take quoteId -// - [ ] insert ilpQuoteDetails at end of method and use the given quoteId -// - [ ] remove additionalFields if unused -// - new tests (in addition to updating tests based on above) -// - [ ] ilpQuoteDetails should be checked in payment method getQuote tests. not quote create, as currently async function createQuote( deps: ServiceDependencies, options: CreateQuoteOptions @@ -132,23 +113,25 @@ async function createQuote( const receiver = await resolveReceiver(deps, options) const paymentMethod = receiver.isLocal ? 'LOCAL' : 'ILP' const quoteId = uuid() - const quote = await deps.paymentMethodHandlerService.getQuote( - paymentMethod, - { - quoteId, - walletAddress, - receiver, - receiveAmount: options.receiveAmount, - debitAmount: options.debitAmount - } - ) - - const sendingFee = await deps.feeService.getLatestFee( - walletAddress.assetId, - FeeType.Sending - ) return await Quote.transaction(deps.knex, async (trx) => { + const quote = await deps.paymentMethodHandlerService.getQuote( + paymentMethod, + { + quoteId, + walletAddress, + receiver, + receiveAmount: options.receiveAmount, + debitAmount: options.debitAmount + }, + trx + ) + + const sendingFee = await deps.feeService.getLatestFee( + walletAddress.assetId, + FeeType.Sending + ) + const createdQuote = await Quote.query(trx) .insertAndFetch({ id: quoteId, diff --git a/packages/backend/src/payment-method/handler/service.ts b/packages/backend/src/payment-method/handler/service.ts index 32486ef4cf..281007bee9 100644 --- a/packages/backend/src/payment-method/handler/service.ts +++ b/packages/backend/src/payment-method/handler/service.ts @@ -5,6 +5,7 @@ import { Receiver } from '../../open_payments/receiver/model' import { BaseService } from '../../shared/baseService' import { IlpPaymentService } from '../ilp/service' import { LocalPaymentService } from '../local/service' +import { Transaction } from 'objection' export interface StartQuoteOptions { quoteId?: string @@ -20,7 +21,6 @@ export interface PaymentQuote { debitAmount: Amount receiveAmount: Amount estimatedExchangeRate: number - additionalFields: Record } export interface PayOptions { @@ -31,7 +31,10 @@ export interface PayOptions { } export interface PaymentMethodService { - getQuote(quoteOptions: StartQuoteOptions): Promise + getQuote( + quoteOptions: StartQuoteOptions, + trx?: Transaction + ): Promise pay(payOptions: PayOptions): Promise } @@ -40,7 +43,8 @@ export type PaymentMethod = 'ILP' | 'LOCAL' export interface PaymentMethodHandlerService { getQuote( method: PaymentMethod, - quoteOptions: StartQuoteOptions + quoteOptions: StartQuoteOptions, + trx?: Transaction ): Promise pay(method: PaymentMethod, payOptions: PayOptions): Promise } @@ -72,8 +76,8 @@ export async function createPaymentMethodHandlerService({ } return { - getQuote: (method, quoteOptions) => - paymentMethods[method].getQuote(quoteOptions), + getQuote: (method, quoteOptions, trx) => + paymentMethods[method].getQuote(quoteOptions, trx), pay: (method, payOptions) => paymentMethods[method].pay(payOptions) } } diff --git a/packages/backend/src/payment-method/ilp/service.test.ts b/packages/backend/src/payment-method/ilp/service.test.ts index d1d198f2df..b2f37b7025 100644 --- a/packages/backend/src/payment-method/ilp/service.test.ts +++ b/packages/backend/src/payment-method/ilp/service.test.ts @@ -11,6 +11,8 @@ import { withConfigOverride } from '../../tests/helpers' import { StartQuoteOptions } from '../handler/service' import { WalletAddress } from '../../open_payments/wallet_address/model' import * as Pay from '@interledger/pay' +import { Ratio, Int, PaymentType } from '@interledger/pay' +import assert from 'assert' import { createReceiver } from '../../tests/receiver' import { mockRatesApi } from '../../tests/rates' @@ -24,6 +26,7 @@ import { IncomingPayment } from '../../open_payments/payment/incoming/model' import { truncateTables } from '../../tests/tableManager' import { createOutgoingPaymentWithReceiver } from '../../tests/outgoingPayment' import { v4 as uuid } from 'uuid' +import { IlpQuoteDetails } from './quote-details/model' const nock = (global as unknown as { nock: typeof import('nock') }).nock @@ -110,6 +113,130 @@ describe('IlpPaymentService', (): void => { ratesScope.done() }) + test('inserts ilpQuoteDetails', async (): Promise => { + const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) + const quoteId = uuid() + const options: StartQuoteOptions = { + quoteId, + walletAddress: walletAddressMap['USD'], + receiver: await createReceiver(deps, walletAddressMap['USD']), + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 100n + } + } + + const highEstimatedExchangeRate = Ratio.of(Int.ONE, Int.TWO) + const lowEstimatedExchangeRate = Ratio.from(0.5) + const minExchangeRate = Ratio.from(0.5) + + assert(highEstimatedExchangeRate) + assert(lowEstimatedExchangeRate) + assert(minExchangeRate) + + const mockIlpQuote = { + paymentType: PaymentType.FixedDelivery, + maxSourceAmount: BigInt(500), + minDeliveryAmount: BigInt(400), + highEstimatedExchangeRate, + lowEstimatedExchangeRate, + minExchangeRate, + maxPacketAmount: BigInt('9223372036854775807') + } + + jest.spyOn(Pay, 'startQuote').mockResolvedValue(mockIlpQuote) + + await ilpPaymentService.getQuote(options) + + const ilpQuoteDetails = await IlpQuoteDetails.query() + .where({ quoteId }) + .first() + + ilpQuoteDetails?.lowEstimatedExchangeRate + + expect(ilpQuoteDetails).toMatchObject({ + quoteId, + maxPacketAmount: mockIlpQuote.maxPacketAmount, + minExchangeRate: mockIlpQuote.minExchangeRate, + minExchangeRateNumerator: mockIlpQuote.minExchangeRate.a.toString(), + minExchangeRateDenominator: mockIlpQuote.minExchangeRate.b.toString(), + lowEstimatedExchangeRate: mockIlpQuote.lowEstimatedExchangeRate, + lowEstimatedExchangeRateNumerator: + mockIlpQuote.lowEstimatedExchangeRate.a.toString(), + lowEstimatedExchangeRateDenominator: + mockIlpQuote.lowEstimatedExchangeRate.b.toString(), + highEstimatedExchangeRate: mockIlpQuote.highEstimatedExchangeRate, + highEstimatedExchangeRateNumerator: + mockIlpQuote.highEstimatedExchangeRate.a.toString(), + highEstimatedExchangeRateDenominator: + mockIlpQuote.highEstimatedExchangeRate.b.toString() + }) + ratesScope.done() + }) + + test('creates a quote with large exchange rate amounts', async (): Promise => { + const ratesScope = mockRatesApi(exchangeRatesUrl, () => ({})) + const quoteId = uuid() + const options: StartQuoteOptions = { + quoteId, + walletAddress: walletAddressMap['USD'], + receiver: await createReceiver(deps, walletAddressMap['USD']), + debitAmount: { + assetCode: 'USD', + assetScale: 2, + value: 100n + } + } + + const highEstimatedExchangeRate = Ratio.of(Int.MAX_U64, Int.ONE) + const lowEstimatedExchangeRate = Ratio.from(10 ** 20) + const minExchangeRate = Ratio.from(0.5) + + assert(highEstimatedExchangeRate) + assert(lowEstimatedExchangeRate) + assert(minExchangeRate) + + const mockIlpQuote = { + paymentType: PaymentType.FixedDelivery, + maxSourceAmount: BigInt(500), + minDeliveryAmount: BigInt(400), + highEstimatedExchangeRate, + lowEstimatedExchangeRate, + minExchangeRate, + maxPacketAmount: BigInt('9223372036854775807') + } + + jest.spyOn(Pay, 'startQuote').mockResolvedValue(mockIlpQuote) + + await ilpPaymentService.getQuote(options) + + const ilpQuoteDetails = await IlpQuoteDetails.query() + .where({ quoteId }) + .first() + + ilpQuoteDetails?.lowEstimatedExchangeRate + + expect(ilpQuoteDetails).toMatchObject({ + quoteId, + maxPacketAmount: mockIlpQuote.maxPacketAmount, + minExchangeRate: mockIlpQuote.minExchangeRate, + minExchangeRateNumerator: mockIlpQuote.minExchangeRate.a.toString(), + minExchangeRateDenominator: mockIlpQuote.minExchangeRate.b.toString(), + lowEstimatedExchangeRate: mockIlpQuote.lowEstimatedExchangeRate, + lowEstimatedExchangeRateNumerator: + mockIlpQuote.lowEstimatedExchangeRate.a.toString(), + lowEstimatedExchangeRateDenominator: + mockIlpQuote.lowEstimatedExchangeRate.b.toString(), + highEstimatedExchangeRate: mockIlpQuote.highEstimatedExchangeRate, + highEstimatedExchangeRateNumerator: + mockIlpQuote.highEstimatedExchangeRate.a.toString(), + highEstimatedExchangeRateDenominator: + mockIlpQuote.highEstimatedExchangeRate.b.toString() + }) + ratesScope.done() + }) + test('Throws if quoteId is not provided', async (): Promise => { const options: StartQuoteOptions = { walletAddress: walletAddressMap['USD'], @@ -193,13 +320,7 @@ describe('IlpPaymentService', (): void => { assetScale: 2, value: 99n }, - estimatedExchangeRate: expect.any(Number), - additionalFields: { - minExchangeRate: expect.any(Pay.Ratio), - highEstimatedExchangeRate: expect.any(Pay.Ratio), - lowEstimatedExchangeRate: expect.any(Pay.Ratio), - maxPacketAmount: BigInt(Pay.Int.MAX_U64.toString()) - } + estimatedExchangeRate: expect.any(Number) }) ratesScope.done() }) diff --git a/packages/backend/src/payment-method/ilp/service.ts b/packages/backend/src/payment-method/ilp/service.ts index e7d98fd91e..c26fe1a58e 100644 --- a/packages/backend/src/payment-method/ilp/service.ts +++ b/packages/backend/src/payment-method/ilp/service.ts @@ -16,6 +16,9 @@ import { } from '../handler/errors' import { TelemetryService } from '../../telemetry/service' import { IlpQuoteDetails } from './quote-details/model' +import { Transaction } from 'objection' + +const MAX_INT64 = BigInt('9223372036854775807') export interface IlpPaymentService extends PaymentMethodService {} @@ -35,14 +38,15 @@ export async function createIlpPaymentService( } return { - getQuote: (quoteOptions) => getQuote(deps, quoteOptions), + getQuote: (quoteOptions, trx) => getQuote(deps, quoteOptions, trx), pay: (payOptions) => pay(deps, payOptions) } } async function getQuote( deps: ServiceDependencies, - options: StartQuoteOptions + options: StartQuoteOptions, + trx?: Transaction ): Promise { if (!options.quoteId) { throw new PaymentMethodHandlerError('Received error during ILP quoting', { @@ -157,6 +161,18 @@ async function getQuote( }) } + await IlpQuoteDetails.query(trx ?? deps.knex).insert({ + quoteId: options.quoteId, + lowEstimatedExchangeRate: ilpQuote.lowEstimatedExchangeRate, + highEstimatedExchangeRate: ilpQuote.highEstimatedExchangeRate, + minExchangeRate: ilpQuote.minExchangeRate, + maxPacketAmount: + // Cap at MAX_INT64 because of postgres type limits + MAX_INT64 < ilpQuote.maxPacketAmount + ? MAX_INT64 + : ilpQuote.maxPacketAmount + }) + return { receiver: options.receiver, walletAddress: options.walletAddress, @@ -170,12 +186,6 @@ async function getQuote( value: ilpQuote.minDeliveryAmount, assetCode: options.receiver.assetCode, assetScale: options.receiver.assetScale - }, - additionalFields: { - lowEstimatedExchangeRate: ilpQuote.lowEstimatedExchangeRate, - highEstimatedExchangeRate: ilpQuote.highEstimatedExchangeRate, - minExchangeRate: ilpQuote.minExchangeRate, - maxPacketAmount: ilpQuote.maxPacketAmount } } } finally { diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 3fce7e5b86..47e38aaa25 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -171,8 +171,7 @@ describe('LocalPaymentService', (): void => { assetScale: 2, value: 100n }, - estimatedExchangeRate: 1, - additionalFields: {} + estimatedExchangeRate: 1 }) }) diff --git a/packages/backend/src/payment-method/local/service.ts b/packages/backend/src/payment-method/local/service.ts index 93748aca56..9cf7b04221 100644 --- a/packages/backend/src/payment-method/local/service.ts +++ b/packages/backend/src/payment-method/local/service.ts @@ -189,8 +189,7 @@ async function getQuote( value: receiveAmountValue, assetCode: options.receiver.assetCode, assetScale: options.receiver.assetScale - }, - additionalFields: {} + } } } diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index ad09987083..bf6e844d28 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -50,12 +50,6 @@ export function mockQuote( : BigInt(Math.ceil(Number(args.debitAmountValue) * exchangeRate)) }, estimatedExchangeRate: exchangeRate, - additionalFields: { - maxPacketAmount: BigInt(Pay.Int.MAX_U64.toString()), - lowEstimatedExchangeRate: Pay.Ratio.from(exchangeRate ?? 1), - highEstimatedExchangeRate: Pay.Ratio.from(exchangeRate ?? 1), - minExchangeRate: Pay.Ratio.from(exchangeRate ?? 1) - }, ...overrides } } From a4cf57ad275c9dfd1d4f576bf4f8ba357a4927d6 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:49:33 -0400 Subject: [PATCH 62/64] fix(backend): payment handler test --- .../backend/src/payment-method/handler/service.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/payment-method/handler/service.test.ts b/packages/backend/src/payment-method/handler/service.test.ts index 0950feda7b..254963967c 100644 --- a/packages/backend/src/payment-method/handler/service.test.ts +++ b/packages/backend/src/payment-method/handler/service.test.ts @@ -67,7 +67,10 @@ describe('PaymentMethodHandlerService', (): void => { await paymentMethodHandlerService.getQuote('ILP', options) - expect(ilpPaymentServiceGetQuoteSpy).toHaveBeenCalledWith(options) + expect(ilpPaymentServiceGetQuoteSpy).toHaveBeenCalledWith( + options, + undefined + ) }) test('calls localPaymentService for local payment type', async (): Promise => { const asset = await createAsset(deps) @@ -91,7 +94,10 @@ describe('PaymentMethodHandlerService', (): void => { await paymentMethodHandlerService.getQuote('LOCAL', options) - expect(localPaymentServiceGetQuoteSpy).toHaveBeenCalledWith(options) + expect(localPaymentServiceGetQuoteSpy).toHaveBeenCalledWith( + options, + undefined + ) }) }) From 1835bc4dbe40bea6eaee0c20990f871ecc9298e0 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:54:37 -0500 Subject: [PATCH 63/64] chore(bruno): rename request --- ... Payment-.bru => Create Receiver -local Incoming Payment-.bru} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/{Create Receiver -remote Incoming Payment-.bru => Create Receiver -local Incoming Payment-.bru} (100%) diff --git a/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru b/bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -local Incoming Payment-.bru similarity index 100% rename from bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -remote Incoming Payment-.bru rename to bruno/collections/Rafiki/Examples/Admin API - only locally/Peer-to-Peer Local Payment/Create Receiver -local Incoming Payment-.bru From 7db833bf51dc31ea667f44fd479a717b639b3252 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:01:46 -0500 Subject: [PATCH 64/64] chore(integration): rm erroneous todo comment - comment indicated there was a bug where there was not. incoming payment not completing was expected because it doesnt have an amount and didnt expire --- test/integration/integration.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index d0d41cd3ae..d83df3e03f 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -557,8 +557,6 @@ describe('Integration tests', (): void => { assetScale: 2, value: BigInt(quote.receiveAmount.value) }) - // TODO: fix bug where fixed-send (regardless of local/remote) is not completeing - // expect(incomingPayment.state).toBe(IncomingPaymentState.Completed) }) test('Peer to Peer - Cross Currency', async (): Promise => {