diff --git a/packages/backend/src/graphql/resolvers/receiver.test.ts b/packages/backend/src/graphql/resolvers/receiver.test.ts index 3739fc6380..53da11c425 100644 --- a/packages/backend/src/graphql/resolvers/receiver.test.ts +++ b/packages/backend/src/graphql/resolvers/receiver.test.ts @@ -6,7 +6,10 @@ import { AppServices } from '../../app' import { initIocContainer } from '../..' import { Config } from '../../config/app' import { Amount, serializeAmount } from '../../open_payments/amount' -import { mockIncomingPayment, mockPaymentPointer } from 'open-payments' +import { + mockIncomingPaymentWithConnection, + mockPaymentPointer +} from 'open-payments' import { CreateReceiverResponse } from '../generated/graphql' import { ReceiverService } from '../../open_payments/receiver/service' import { Receiver } from '../../open_payments/receiver/model' @@ -49,7 +52,7 @@ describe('Receiver Resolver', (): void => { incomingAmount }): Promise => { const receiver = Receiver.fromIncomingPayment( - mockIncomingPayment({ + mockIncomingPaymentWithConnection({ id: `${paymentPointer.id}/incoming-payments/${uuid()}`, paymentPointer: paymentPointer.id, description, diff --git a/packages/backend/src/open_payments/connection/model.ts b/packages/backend/src/open_payments/connection/model.ts index 4dc8c726a0..0fc2eafaf7 100644 --- a/packages/backend/src/open_payments/connection/model.ts +++ b/packages/backend/src/open_payments/connection/model.ts @@ -4,14 +4,6 @@ import { IlpAddress } from 'ilp-packet' import { ILPStreamConnection } from 'open-payments' import { IncomingPayment } from '../payment/incoming/model' -export type ConnectionJSON = { - id: string - ilpAddress: IlpAddress - sharedSecret: string - assetCode: string - assetScale: number -} - export abstract class ConnectionBase { protected constructor( public readonly ilpAddress: IlpAddress, @@ -55,16 +47,6 @@ export class Connection extends ConnectionBase { return `${this.openPaymentsUrl}/connections/${this.id}` } - public toJSON(): ConnectionJSON { - return { - id: this.url, - ilpAddress: this.ilpAddress, - sharedSecret: base64url(this.sharedSecret), - assetCode: this.assetCode, - assetScale: this.assetScale - } - } - public toOpenPaymentsType(): ILPStreamConnection { return { id: this.url, diff --git a/packages/backend/src/open_payments/connection/routes.ts b/packages/backend/src/open_payments/connection/routes.ts index 80f0f1cc80..2f4570e07c 100644 --- a/packages/backend/src/open_payments/connection/routes.ts +++ b/packages/backend/src/open_payments/connection/routes.ts @@ -31,5 +31,5 @@ async function getConnection( ): Promise { const connection = deps.connectionService.get(ctx.incomingPayment) if (!connection) return ctx.throw(404) - ctx.body = connection.toJSON() + ctx.body = connection.toOpenPaymentsType() } diff --git a/packages/backend/src/open_payments/connection/service.test.ts b/packages/backend/src/open_payments/connection/service.test.ts index ca30596049..3497964dd5 100644 --- a/packages/backend/src/open_payments/connection/service.test.ts +++ b/packages/backend/src/open_payments/connection/service.test.ts @@ -57,7 +57,7 @@ describe('Connection Service', (): void => { expect(connection.url).toEqual( `${Config.openPaymentsUrl}/connections/${incomingPayment.connectionId}` ) - expect(connection.toJSON()).toEqual({ + expect(connection.toOpenPaymentsType()).toEqual({ id: connection.url, ilpAddress: connection.ilpAddress, sharedSecret: base64url(connection.sharedSecret || ''), diff --git a/packages/backend/src/open_payments/payment/incoming/model.test.ts b/packages/backend/src/open_payments/payment/incoming/model.test.ts new file mode 100644 index 0000000000..1c64715c7f --- /dev/null +++ b/packages/backend/src/open_payments/payment/incoming/model.test.ts @@ -0,0 +1,127 @@ +import { IocContract } from '@adonisjs/fold' +import { createTestApp, TestContainer } from '../../../tests/app' +import { Config, IAppConfig } from '../../../config/app' +import { initIocContainer } from '../../..' +import { AppServices } from '../../../app' +import { createIncomingPayment } from '../../../tests/incomingPayment' +import { createPaymentPointer } from '../../../tests/paymentPointer' +import { truncateTables } from '../../../tests/tableManager' +import { Connection } from '../../connection/model' +import { serializeAmount } from '../../amount' +import { IlpAddress } from 'ilp-packet' +import { IncomingPayment } from './model' + +describe('Incoming Payment Model', (): void => { + let deps: IocContract + let appContainer: TestContainer + let config: IAppConfig + + beforeAll(async (): Promise => { + deps = initIocContainer(Config) + appContainer = await createTestApp(deps) + config = await deps.use('config') + }) + + afterEach(async (): Promise => { + jest.useRealTimers() + await truncateTables(appContainer.knex) + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + + describe('toOpenPaymentsType', () => { + test('returns incoming payment without connection provided', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id, + description: 'my payment' + }) + + expect(incomingPayment.toOpenPaymentsType(paymentPointer)).toEqual({ + id: `${paymentPointer.url}${IncomingPayment.urlPath}/${incomingPayment.id}`, + paymentPointer: paymentPointer.url, + completed: incomingPayment.completed, + receivedAmount: serializeAmount(incomingPayment.receivedAmount), + incomingAmount: incomingPayment.incomingAmount + ? serializeAmount(incomingPayment.incomingAmount) + : undefined, + expiresAt: incomingPayment.expiresAt.toISOString(), + description: incomingPayment.description ?? undefined, + externalRef: incomingPayment.externalRef ?? undefined, + updatedAt: incomingPayment.updatedAt.toISOString(), + createdAt: incomingPayment.createdAt.toISOString() + }) + }) + + test('returns incoming payment with connection as string', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id, + description: 'my payment' + }) + + const connection = `${config.openPaymentsUrl}/connections/${incomingPayment.connectionId}` + + expect( + incomingPayment.toOpenPaymentsType(paymentPointer, connection) + ).toEqual({ + id: `${paymentPointer.url}${IncomingPayment.urlPath}/${incomingPayment.id}`, + paymentPointer: paymentPointer.url, + completed: incomingPayment.completed, + receivedAmount: serializeAmount(incomingPayment.receivedAmount), + incomingAmount: incomingPayment.incomingAmount + ? serializeAmount(incomingPayment.incomingAmount) + : undefined, + expiresAt: incomingPayment.expiresAt.toISOString(), + description: incomingPayment.description ?? undefined, + externalRef: incomingPayment.externalRef ?? undefined, + updatedAt: incomingPayment.updatedAt.toISOString(), + createdAt: incomingPayment.createdAt.toISOString(), + ilpStreamConnection: connection + }) + }) + + test('returns incoming payment with connection as object', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id, + description: 'my payment' + }) + + const connection = Connection.fromPayment({ + payment: incomingPayment, + openPaymentsUrl: config.openPaymentsUrl, + credentials: { + ilpAddress: 'test.ilp' as IlpAddress, + sharedSecret: Buffer.from('') + } + }) + + expect( + incomingPayment.toOpenPaymentsType(paymentPointer, connection) + ).toEqual({ + id: `${paymentPointer.url}${IncomingPayment.urlPath}/${incomingPayment.id}`, + paymentPointer: paymentPointer.url, + completed: incomingPayment.completed, + receivedAmount: serializeAmount(incomingPayment.receivedAmount), + incomingAmount: incomingPayment.incomingAmount + ? serializeAmount(incomingPayment.incomingAmount) + : undefined, + expiresAt: incomingPayment.expiresAt.toISOString(), + description: incomingPayment.description ?? undefined, + externalRef: incomingPayment.externalRef ?? undefined, + updatedAt: incomingPayment.updatedAt.toISOString(), + createdAt: incomingPayment.createdAt.toISOString(), + ilpStreamConnection: { + id: `${config.openPaymentsUrl}/connections/${incomingPayment.connectionId}`, + ilpAddress: 'test.ilp', + sharedSecret: expect.any(String), + assetCode: incomingPayment.asset.code, + assetScale: incomingPayment.asset.scale + } + }) + }) + }) +}) diff --git a/packages/backend/src/open_payments/payment/incoming/model.ts b/packages/backend/src/open_payments/payment/incoming/model.ts index b75f4c5c04..c310120aba 100644 --- a/packages/backend/src/open_payments/payment/incoming/model.ts +++ b/packages/backend/src/open_payments/payment/incoming/model.ts @@ -1,8 +1,8 @@ -import { Model, ModelOptions, Pojo, QueryContext } from 'objection' +import { Model, ModelOptions, QueryContext } from 'objection' import { v4 as uuid } from 'uuid' import { Amount, AmountJSON, serializeAmount } from '../../amount' -import { Connection, ConnectionJSON } from '../../connection/model' +import { Connection } from '../../connection/model' import { PaymentPointer, PaymentPointerSubresource @@ -11,7 +11,11 @@ import { Asset } from '../../../asset/model' import { LiquidityAccount, OnCreditOptions } from '../../../accounting/service' import { ConnectorAccount } from '../../../connector/core/rafiki' import { WebhookEvent } from '../../../webhook/model' -import { IncomingPayment as OpenPaymentsIncomingPayment } from 'open-payments' +import { + IncomingPayment as OpenPaymentsIncomingPayment, + IncomingPaymentWithConnection as OpenPaymentsIncomingPaymentWithConnection, + IncomingPaymentWithConnectionUrl as OpenPaymentsIncomingPaymentWithConnectionUrl +} from 'open-payments' export enum IncomingPaymentEventType { IncomingPaymentExpired = 'incoming_payment.expired', @@ -80,7 +84,6 @@ export class IncomingPayment } } - public paymentPointer!: PaymentPointer public description?: string public expiresAt!: Date public state!: IncomingPaymentState @@ -127,8 +130,8 @@ export class IncomingPayment this.receivedAmountValue = amount.value } - public get url(): string { - return `${this.paymentPointer.url}${IncomingPayment.urlPath}/${this.id}` + public getUrl(paymentPointer: PaymentPointer): string { + return `${paymentPointer.url}${IncomingPayment.urlPath}/${this.id}` } public async onCredit({ @@ -211,67 +214,60 @@ export class IncomingPayment } } - $formatJson(json: Pojo): Pojo { - json = super.$formatJson(json) - const payment: Pojo = { - id: json.id, - receivedAmount: { - ...json.receivedAmount, - value: json.receivedAmount.value.toString() - }, - completed: json.completed, - createdAt: json.createdAt, - updatedAt: json.updatedAt, - expiresAt: json.expiresAt.toISOString() - } - if (json.incomingAmount) { - payment.incomingAmount = { - ...json.incomingAmount, - value: json.incomingAmount.value.toString() - } - } - if (json.description) { - payment.description = json.description - } - if (json.externalRef) { - payment.externalRef = json.externalRef - } - return payment - } - - public toOpenPaymentsType({ - ilpStreamConnection - }: { + public toOpenPaymentsType( + paymentPointer: PaymentPointer + ): OpenPaymentsIncomingPayment + public toOpenPaymentsType( + paymentPointer: PaymentPointer, ilpStreamConnection: Connection - }): OpenPaymentsIncomingPayment { - return { - id: this.url, - paymentPointer: this.paymentPointer.url, + ): OpenPaymentsIncomingPaymentWithConnection + public toOpenPaymentsType( + paymentPointer: PaymentPointer, + ilpStreamConnection: string + ): OpenPaymentsIncomingPaymentWithConnectionUrl + public toOpenPaymentsType( + paymentPointer: PaymentPointer, + ilpStreamConnection?: Connection | string + ): + | OpenPaymentsIncomingPaymentWithConnection + | OpenPaymentsIncomingPaymentWithConnectionUrl + + public toOpenPaymentsType( + paymentPointer: PaymentPointer, + ilpStreamConnection?: Connection | string + ): + | OpenPaymentsIncomingPayment + | OpenPaymentsIncomingPaymentWithConnection + | OpenPaymentsIncomingPaymentWithConnectionUrl { + const baseIncomingPayment: OpenPaymentsIncomingPayment = { + id: this.getUrl(paymentPointer), + paymentPointer: paymentPointer.url, incomingAmount: this.incomingAmount ? serializeAmount(this.incomingAmount) : undefined, receivedAmount: serializeAmount(this.receivedAmount), completed: this.completed, + description: this.description ?? undefined, + externalRef: this.externalRef ?? undefined, createdAt: this.createdAt.toISOString(), updatedAt: this.updatedAt.toISOString(), - expiresAt: this.expiresAt.toISOString(), + expiresAt: this.expiresAt.toISOString() + } + + if (!ilpStreamConnection) { + return baseIncomingPayment + } + + if (typeof ilpStreamConnection === 'string') { + return { + ...baseIncomingPayment, + ilpStreamConnection + } + } + + return { + ...baseIncomingPayment, ilpStreamConnection: ilpStreamConnection.toOpenPaymentsType() } } } - -// TODO: disallow undefined -// https://github.com/interledger/rafiki/issues/594 -export type IncomingPaymentJSON = { - id: string - paymentPointer: string - incomingAmount?: AmountJSON - receivedAmount: AmountJSON - completed: boolean - description?: string - externalRef?: string - createdAt: string - updatedAt: string - expiresAt?: string - ilpStreamConnection?: ConnectionJSON | string -} diff --git a/packages/backend/src/open_payments/payment/incoming/routes.test.ts b/packages/backend/src/open_payments/payment/incoming/routes.test.ts index 40410ea4b5..644b0726ac 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.test.ts @@ -82,31 +82,33 @@ describe('Incoming Payment Routes', (): void => { externalRef }), get: (ctx) => incomingPaymentRoutes.get(ctx), - getBody: (incomingPayment, list) => ({ - id: incomingPayment.url, - paymentPointer: paymentPointer.url, - completed: false, - incomingAmount: - incomingPayment.incomingAmount && - serializeAmount(incomingPayment.incomingAmount), - description: incomingPayment.description, - expiresAt: incomingPayment.expiresAt.toISOString(), - createdAt: incomingPayment.createdAt.toISOString(), - updatedAt: incomingPayment.updatedAt.toISOString(), - receivedAmount: serializeAmount(incomingPayment.receivedAmount), - externalRef: '#123', - ilpStreamConnection: list - ? `${config.openPaymentsUrl}/connections/${incomingPayment.connectionId}` - : { - id: `${config.openPaymentsUrl}/connections/${incomingPayment.connectionId}`, - ilpAddress: expect.stringMatching( - /^test\.rafiki\.[a-zA-Z0-9_-]{95}$/ - ), - sharedSecret: expect.stringMatching(/^[a-zA-Z0-9-_]{43}$/), - assetCode: incomingPayment.receivedAmount.assetCode, - assetScale: incomingPayment.receivedAmount.assetScale - } - }), + getBody: (incomingPayment, list) => { + return { + id: incomingPayment.getUrl(paymentPointer), + paymentPointer: paymentPointer.url, + completed: false, + incomingAmount: + incomingPayment.incomingAmount && + serializeAmount(incomingPayment.incomingAmount), + description: incomingPayment.description, + expiresAt: incomingPayment.expiresAt.toISOString(), + createdAt: incomingPayment.createdAt.toISOString(), + updatedAt: incomingPayment.updatedAt.toISOString(), + receivedAmount: serializeAmount(incomingPayment.receivedAmount), + externalRef: '#123', + ilpStreamConnection: list + ? `${config.openPaymentsUrl}/connections/${incomingPayment.connectionId}` + : { + id: `${config.openPaymentsUrl}/connections/${incomingPayment.connectionId}`, + ilpAddress: expect.stringMatching( + /^test\.rafiki\.[a-zA-Z0-9_-]{95}$/ + ), + sharedSecret: expect.stringMatching(/^[a-zA-Z0-9-_]{43}$/), + assetCode: incomingPayment.receivedAmount.assetCode, + assetScale: incomingPayment.receivedAmount.assetScale + } + } + }, list: (ctx) => incomingPaymentRoutes.list(ctx), urlPath: IncomingPayment.urlPath }) @@ -263,7 +265,7 @@ describe('Incoming Payment Routes', (): void => { await expect(incomingPaymentRoutes.complete(ctx)).resolves.toBeUndefined() expect(ctx.response).toSatisfyApiSpec() expect(ctx.body).toEqual({ - id: incomingPayment.url, + id: incomingPayment.getUrl(paymentPointer), paymentPointer: paymentPointer.url, incomingAmount: { value: '123', diff --git a/packages/backend/src/open_payments/payment/incoming/routes.ts b/packages/backend/src/open_payments/payment/incoming/routes.ts index 439a155b28..8175670f7b 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.ts @@ -1,4 +1,3 @@ -import { AccessAction } from 'open-payments' import { Logger } from 'pino' import { ReadContext, @@ -8,7 +7,7 @@ import { } from '../../../app' import { IAppConfig } from '../../../config/app' import { IncomingPaymentService } from './service' -import { IncomingPayment, IncomingPaymentJSON } from './model' +import { IncomingPayment } from './model' import { errorToCode, errorToMessage, @@ -17,8 +16,15 @@ import { } from './errors' import { AmountJSON, parseAmount } from '../../amount' import { listSubresource } from '../../payment_pointer/routes' -import { ConnectionJSON } from '../../connection/model' +import { Connection } from '../../connection/model' import { ConnectionService } from '../../connection/service' +import { + AccessAction, + IncomingPayment as OpenPaymentsIncomingPayment, + IncomingPaymentWithConnection as OpenPaymentsIncomingPaymentWithConnection, + IncomingPaymentWithConnectionUrl as OpenPaymentsIncomingPaymentWithConnectionUrl +} from 'open-payments' +import { PaymentPointer } from '../../payment_pointer/model' // Don't allow creating an incoming payment too far out. Incoming payments with no payments before they expire are cleaned up, since incoming payments creation is unauthenticated. // TODO what is a good default value for this? @@ -70,7 +76,11 @@ async function getIncomingPayment( } if (!incomingPayment) return ctx.throw(404) const connection = deps.connectionService.get(incomingPayment) - ctx.body = incomingPaymentToBody(deps, incomingPayment, connection?.toJSON()) + ctx.body = incomingPaymentToBody( + ctx.paymentPointer, + incomingPayment, + connection + ) } export type CreateBody = { @@ -112,9 +122,9 @@ async function createIncomingPayment( ctx.status = 201 const connection = deps.connectionService.get(incomingPaymentOrError) ctx.body = incomingPaymentToBody( - deps, + ctx.paymentPointer, incomingPaymentOrError, - connection?.toJSON() + connection ) } @@ -137,7 +147,7 @@ async function completeIncomingPayment( errorToMessage[incomingPaymentOrError] ) } - ctx.body = incomingPaymentToBody(deps, incomingPaymentOrError) + ctx.body = incomingPaymentToBody(ctx.paymentPointer, incomingPaymentOrError) } async function listIncomingPayments( @@ -150,7 +160,7 @@ async function listIncomingPayments( getPaymentPointerPage: deps.incomingPaymentService.getPaymentPointerPage, toBody: (payment) => incomingPaymentToBody( - deps, + ctx.paymentPointer, payment, deps.connectionService.getUrl(payment) ) @@ -159,19 +169,13 @@ async function listIncomingPayments( ctx.throw(500, 'Error trying to list incoming payments') } } - function incomingPaymentToBody( - deps: ServiceDependencies, + paymentPointer: PaymentPointer, incomingPayment: IncomingPayment, - ilpStreamConnection?: ConnectionJSON | string -): IncomingPaymentJSON { - const body = { - ...incomingPayment.toJSON(), - id: incomingPayment.url, - paymentPointer: incomingPayment.paymentPointer.url - } as unknown as IncomingPaymentJSON - if (ilpStreamConnection) { - body.ilpStreamConnection = ilpStreamConnection - } - return body + ilpStreamConnection?: Connection | string +): + | OpenPaymentsIncomingPayment + | OpenPaymentsIncomingPaymentWithConnection + | OpenPaymentsIncomingPaymentWithConnectionUrl { + return incomingPayment.toOpenPaymentsType(paymentPointer, ilpStreamConnection) } diff --git a/packages/backend/src/open_payments/payment/incoming_remote/service.test.ts b/packages/backend/src/open_payments/payment/incoming_remote/service.test.ts index 67f1635484..dd78e6e9ef 100644 --- a/packages/backend/src/open_payments/payment/incoming_remote/service.test.ts +++ b/packages/backend/src/open_payments/payment/incoming_remote/service.test.ts @@ -11,10 +11,10 @@ import { AuthenticatedClient as OpenPaymentsClient, AccessAction, AccessType, - mockIncomingPayment, mockInteractiveGrant, mockNonInteractiveGrant, - mockPaymentPointer + mockPaymentPointer, + mockIncomingPaymentWithConnection } from 'open-payments' import { GrantService } from '../../grant/service' import { RemoteIncomingPaymentError } from './errors' @@ -90,7 +90,7 @@ describe('Remote Incoming Payment Service', (): void => { ${undefined} | ${undefined} | ${undefined} | ${undefined} ${amount} | ${new Date(Date.now() + 30_000)} | ${'Test incoming payment'} | ${'#123'} `('creates remote incoming payment ($#)', async (args): Promise => { - const mockedIncomingPayment = mockIncomingPayment({ + const mockedIncomingPayment = mockIncomingPaymentWithConnection({ ...args, paymentPointerUrl: paymentPointer.id }) @@ -178,7 +178,7 @@ describe('Remote Incoming Payment Service', (): void => { ${undefined} | ${undefined} | ${undefined} | ${undefined} ${amount} | ${new Date(Date.now() + 30_000)} | ${'Test incoming payment'} | ${'#123'} `('creates remote incoming payment ($#)', async (args): Promise => { - const mockedIncomingPayment = mockIncomingPayment({ + const mockedIncomingPayment = mockIncomingPaymentWithConnection({ ...args, paymentPointerUrl: paymentPointer.id }) diff --git a/packages/backend/src/open_payments/payment/outgoing/model.ts b/packages/backend/src/open_payments/payment/outgoing/model.ts index 8dcd358db1..ebdab291f5 100644 --- a/packages/backend/src/open_payments/payment/outgoing/model.ts +++ b/packages/backend/src/open_payments/payment/outgoing/model.ts @@ -1,13 +1,17 @@ -import { Model, ModelOptions, Pojo, QueryContext } from 'objection' +import { Model, ModelOptions, QueryContext } from 'objection' import { DbErrors } from 'objection-db-errors' import { LiquidityAccount } from '../../../accounting/service' import { Asset } from '../../../asset/model' import { ConnectorAccount } from '../../../connector/core/rafiki' -import { PaymentPointerSubresource } from '../../payment_pointer/model' +import { + PaymentPointerSubresource, + PaymentPointer +} from '../../payment_pointer/model' import { Quote } from '../../quote/model' -import { Amount, AmountJSON } from '../../amount' +import { Amount, AmountJSON, serializeAmount } from '../../amount' import { WebhookEvent } from '../../../webhook/model' +import { OutgoingPayment as OpenPaymentsOutgoingPayment } from 'open-payments' export class OutgoingPaymentGrant extends DbErrors(Model) { public static get modelPaths(): string[] { @@ -68,10 +72,18 @@ export class OutgoingPayment return this.quote.assetId } + public getUrl(paymentPointer: PaymentPointer): string { + return `${paymentPointer.url}${OutgoingPayment.urlPath}/${this.id}` + } + public get asset(): Asset { return this.quote.asset } + public get failed(): boolean { + return this.state === OutgoingPaymentState.Failed + } + // Outgoing peer public peerId?: string @@ -144,28 +156,22 @@ export class OutgoingPayment return data } - $formatJson(json: Pojo): Pojo { - json = super.$formatJson(json) + public toOpenPaymentsType( + paymentPointer: PaymentPointer + ): OpenPaymentsOutgoingPayment { return { - id: json.id, - state: json.state, - receiver: json.receiver, - sendAmount: { - ...json.sendAmount, - value: json.sendAmount.value.toString() - }, - sentAmount: { - ...json.sentAmount, - value: json.sentAmount.value.toString() - }, - receiveAmount: { - ...json.receiveAmount, - value: json.receiveAmount.value.toString() - }, - description: json.description, - externalRef: json.externalRef, - createdAt: json.createdAt, - updatedAt: json.updatedAt + id: this.getUrl(paymentPointer), + paymentPointer: paymentPointer.url, + quoteId: this.quote?.getUrl(paymentPointer) ?? undefined, + receiveAmount: serializeAmount(this.receiveAmount), + sendAmount: serializeAmount(this.sendAmount), + sentAmount: serializeAmount(this.sentAmount), + receiver: this.receiver, + failed: this.failed, + externalRef: this.externalRef ?? undefined, + description: this.description ?? undefined, + createdAt: this.createdAt.toISOString(), + updatedAt: this.updatedAt.toISOString() } } } @@ -236,16 +242,3 @@ export class PaymentEvent extends WebhookEvent { public type!: PaymentEventType public data!: PaymentData } - -export type OutgoingPaymentJSON = { - id: string - paymentPointer: string - receiver: string - sendAmount: AmountJSON - sentAmount: AmountJSON - receiveAmount: AmountJSON - description: string | null - externalRef: string | null - createdAt: string - updatedAt: string -} diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts index a7f154b55b..2b949c5a16 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts @@ -100,19 +100,22 @@ describe('Outgoing Payment Routes', (): void => { return outgoingPayment }, get: (ctx) => outgoingPaymentRoutes.get(ctx), - getBody: (outgoingPayment) => ({ - id: `${paymentPointer.url}/outgoing-payments/${outgoingPayment.id}`, - paymentPointer: paymentPointer.url, - receiver: outgoingPayment.receiver, - sendAmount: serializeAmount(outgoingPayment.sendAmount), - sentAmount: serializeAmount(outgoingPayment.sentAmount), - receiveAmount: serializeAmount(outgoingPayment.receiveAmount), - description: outgoingPayment.description, - externalRef: outgoingPayment.externalRef, - failed, - createdAt: outgoingPayment.createdAt.toISOString(), - updatedAt: outgoingPayment.updatedAt.toISOString() - }), + getBody: (outgoingPayment) => { + return { + id: `${paymentPointer.url}/outgoing-payments/${outgoingPayment.id}`, + paymentPointer: paymentPointer.url, + receiver: outgoingPayment.receiver, + quoteId: outgoingPayment.quote.getUrl(paymentPointer), + sendAmount: serializeAmount(outgoingPayment.sendAmount), + sentAmount: serializeAmount(outgoingPayment.sentAmount), + receiveAmount: serializeAmount(outgoingPayment.receiveAmount), + description: outgoingPayment.description, + externalRef: outgoingPayment.externalRef, + failed, + createdAt: outgoingPayment.createdAt.toISOString(), + updatedAt: outgoingPayment.updatedAt.toISOString() + } + }, list: (ctx) => outgoingPaymentRoutes.list(ctx), urlPath: OutgoingPayment.urlPath }) @@ -201,6 +204,7 @@ describe('Outgoing Payment Routes', (): void => { id: `${paymentPointer.url}/outgoing-payments/${outgoingPaymentId}`, paymentPointer: paymentPointer.url, receiver: payment.receiver, + quoteId: options.quoteId, sendAmount: { ...payment.sendAmount, value: payment.sendAmount.value.toString() diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.ts b/packages/backend/src/open_payments/payment/outgoing/routes.ts index 21e3c529a4..9884292cab 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.ts @@ -1,11 +1,14 @@ -import { AccessAction } from 'open-payments' import { Logger } from 'pino' import { ReadContext, CreateContext, ListContext } from '../../../app' import { IAppConfig } from '../../../config/app' import { OutgoingPaymentService } from './service' import { isOutgoingPaymentError, errorToCode, errorToMessage } from './errors' -import { OutgoingPayment, OutgoingPaymentState } from './model' +import { OutgoingPayment } from './model' import { listSubresource } from '../../payment_pointer/routes' +import { + AccessAction, + OutgoingPayment as OpenPaymentsOutgoingPayment +} from 'open-payments' import { PaymentPointer } from '../../payment_pointer/model' interface ServiceDependencies { @@ -50,8 +53,7 @@ async function getOutgoingPayment( ctx.throw(500, 'Error trying to get outgoing payment') } if (!outgoingPayment) return ctx.throw(404) - const body = outgoingPaymentToBody(deps, outgoingPayment, ctx.paymentPointer) - ctx.body = body + ctx.body = outgoingPaymentToBody(ctx.paymentPointer, outgoingPayment) } export type CreateBody = { @@ -85,8 +87,7 @@ async function createOutgoingPayment( return ctx.throw(errorToCode[paymentOrErr], errorToMessage[paymentOrErr]) } ctx.status = 201 - const res = outgoingPaymentToBody(deps, paymentOrErr, ctx.paymentPointer) - ctx.body = res + ctx.body = outgoingPaymentToBody(ctx.paymentPointer, paymentOrErr) } async function listOutgoingPayments( @@ -97,8 +98,7 @@ async function listOutgoingPayments( await listSubresource({ ctx, getPaymentPointerPage: deps.outgoingPaymentService.getPaymentPointerPage, - toBody: (payment) => - outgoingPaymentToBody(deps, payment, ctx.paymentPointer) + toBody: (payment) => outgoingPaymentToBody(ctx.paymentPointer, payment) }) } catch (_) { ctx.throw(500, 'Error trying to list outgoing payments') @@ -106,17 +106,8 @@ async function listOutgoingPayments( } function outgoingPaymentToBody( - deps: ServiceDependencies, - outgoingPayment: OutgoingPayment, - paymentPointer: PaymentPointer -) { - return Object.fromEntries( - Object.entries({ - ...outgoingPayment.toJSON(), - id: `${paymentPointer.url}/outgoing-payments/${outgoingPayment.id}`, - paymentPointer: paymentPointer.url, - state: null, - failed: outgoingPayment.state === OutgoingPaymentState.Failed - }).filter(([_, v]) => v != null) - ) + paymentPointer: PaymentPointer, + outgoingPayment: OutgoingPayment +): OpenPaymentsOutgoingPayment { + return outgoingPayment.toOpenPaymentsType(paymentPointer) } 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 6fa087e86d..b2827bdd11 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -245,11 +245,10 @@ describe('OutgoingPaymentService', (): void => { beforeEach(async (): Promise => { const { id: sendAssetId } = await createAsset(deps, asset) - paymentPointerId = ( - await createPaymentPointer(deps, { - assetId: sendAssetId - }) - ).id + const paymentPointer = await createPaymentPointer(deps, { + assetId: sendAssetId + }) + paymentPointerId = paymentPointer.id const { id: destinationAssetId } = await createAsset(deps, destinationAsset) receiverPaymentPointer = await createPaymentPointer(deps, { assetId: destinationAssetId, @@ -266,7 +265,7 @@ describe('OutgoingPaymentService', (): void => { incomingPayment = await createIncomingPayment(deps, { paymentPointerId: receiverPaymentPointer.id }) - receiver = incomingPayment.url + receiver = incomingPayment.getUrl(receiverPaymentPointer) amtDelivered = BigInt(0) }) @@ -855,10 +854,14 @@ describe('OutgoingPaymentService', (): void => { assetScale: receiverPaymentPointer.asset.scale } }) + const fetchedReceiver = connectionService.getUrl(incomingPayment) assert.ok(fetchedReceiver) + assert.ok(incomingPayment.paymentPointer) const paymentId = await setup({ - receiver: toConnection ? fetchedReceiver : incomingPayment.url, + receiver: toConnection + ? fetchedReceiver + : incomingPayment.getUrl(incomingPayment.paymentPointer), receiveAmount }) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index f8c8c36edb..4a37f971da 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -293,7 +293,7 @@ async function validateGrant( payment: grantPayment }) ) { - if (grantPayment.state === OutgoingPaymentState.Failed) { + if (grantPayment.failed) { const totalSent = await deps.accountingService.getTotalSent( grantPayment.id ) diff --git a/packages/backend/src/open_payments/payment_pointer/routes.ts b/packages/backend/src/open_payments/payment_pointer/routes.ts index e095e9a016..2059a63d47 100644 --- a/packages/backend/src/open_payments/payment_pointer/routes.ts +++ b/packages/backend/src/open_payments/payment_pointer/routes.ts @@ -66,10 +66,7 @@ export const listSubresource = async ({ ) const result = { pagination: pageInfo, - result: page.map((item: M) => { - item.paymentPointer = ctx.paymentPointer - return toBody(item) - }) + result: page.map((item: M) => toBody(item)) } ctx.body = result } diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index 1efc525e82..1b70de7b10 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -1,9 +1,13 @@ import { Model, Pojo } from 'objection' import * as Pay from '@interledger/pay' -import { Amount, AmountJSON } from '../amount' -import { PaymentPointerSubresource } from '../payment_pointer/model' +import { Amount, serializeAmount } from '../amount' +import { + PaymentPointer, + PaymentPointerSubresource +} from '../payment_pointer/model' import { Asset } from '../../asset/model' +import { Quote as OpenPaymentsQuote } from 'open-payments' export class Quote extends PaymentPointerSubresource { public static readonly tableName = 'quotes' @@ -43,6 +47,10 @@ export class Quote extends PaymentPointerSubresource { private sendAmountValue!: bigint + public getUrl(paymentPointer: PaymentPointer): string { + return `${paymentPointer.url}${Quote.urlPath}/${this.id}` + } + public get sendAmount(): Amount { return { value: this.sendAmountValue, @@ -148,14 +156,16 @@ export class Quote extends PaymentPointerSubresource { expiresAt: json.expiresAt.toISOString() } } -} -export type QuoteJSON = { - id: string - paymentPointerId: string - receiver: string - sendAmount: AmountJSON - receiveAmount: AmountJSON - createdAt: string - expiresAt: string + public toOpenPaymentsType(paymentPointer: PaymentPointer): OpenPaymentsQuote { + return { + id: this.getUrl(paymentPointer), + paymentPointer: paymentPointer.url, + receiveAmount: serializeAmount(this.receiveAmount), + sendAmount: serializeAmount(this.sendAmount), + receiver: this.receiver, + expiresAt: this.expiresAt.toISOString(), + createdAt: this.createdAt.toISOString() + } + } } diff --git a/packages/backend/src/open_payments/quote/routes.test.ts b/packages/backend/src/open_payments/quote/routes.test.ts index 4088916bbe..9c78b95f28 100644 --- a/packages/backend/src/open_payments/quote/routes.test.ts +++ b/packages/backend/src/open_payments/quote/routes.test.ts @@ -96,15 +96,17 @@ describe('Quote Routes', (): void => { client }), get: (ctx) => quoteRoutes.get(ctx), - getBody: (quote) => ({ - id: `${paymentPointer.url}/quotes/${quote.id}`, - paymentPointer: paymentPointer.url, - receiver: quote.receiver, - sendAmount: serializeAmount(quote.sendAmount), - receiveAmount: serializeAmount(quote.receiveAmount), - createdAt: quote.createdAt.toISOString(), - expiresAt: quote.expiresAt.toISOString() - }), + getBody: (quote) => { + return { + id: `${paymentPointer.url}/quotes/${quote.id}`, + paymentPointer: paymentPointer.url, + receiver: quote.receiver, + sendAmount: serializeAmount(quote.sendAmount), + receiveAmount: serializeAmount(quote.receiveAmount), + createdAt: quote.createdAt.toISOString(), + expiresAt: quote.expiresAt.toISOString() + } + }, urlPath: Quote.urlPath }) }) diff --git a/packages/backend/src/open_payments/quote/routes.ts b/packages/backend/src/open_payments/quote/routes.ts index 8495477499..d327497a99 100644 --- a/packages/backend/src/open_payments/quote/routes.ts +++ b/packages/backend/src/open_payments/quote/routes.ts @@ -6,6 +6,7 @@ import { CreateQuoteOptions, QuoteService } from './service' import { isQuoteError, errorToCode, errorToMessage } from './errors' import { Quote } from './model' import { AmountJSON, parseAmount } from '../amount' +import { Quote as OpenPaymentsQuote } from 'open-payments' import { PaymentPointer } from '../payment_pointer/model' interface ServiceDependencies { @@ -40,8 +41,7 @@ async function getQuote( paymentPointerId: ctx.paymentPointer.id }) if (!quote) return ctx.throw(404) - const body = quoteToBody(deps, quote, ctx.paymentPointer) - ctx.body = body + ctx.body = quoteToBody(ctx.paymentPointer, quote) } interface CreateBodyBase { @@ -81,8 +81,7 @@ async function createQuote( } ctx.status = 201 - const res = quoteToBody(deps, quoteOrErr, ctx.paymentPointer) - ctx.body = res + ctx.body = quoteToBody(ctx.paymentPointer, quoteOrErr) } catch (err) { if (isQuoteError(err)) { return ctx.throw(errorToCode[err], errorToMessage[err]) @@ -93,16 +92,8 @@ async function createQuote( } function quoteToBody( - deps: ServiceDependencies, - quote: Quote, - paymentPointer: PaymentPointer -) { - return Object.fromEntries( - Object.entries({ - ...quote.toJSON(), - id: `${paymentPointer.url}/quotes/${quote.id}`, - paymentPointer: paymentPointer.url, - paymentPointerId: undefined - }).filter(([_, v]) => v != null) - ) + paymentPointer: PaymentPointer, + quote: Quote +): OpenPaymentsQuote { + return quote.toOpenPaymentsType(paymentPointer) } diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 0eadf95b66..8de6155c3c 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -247,7 +247,7 @@ describe('QuoteService', (): void => { paymentPointerId, receiver: toConnection ? connectionService.getUrl(incomingPayment) - : incomingPayment.url + : incomingPayment.getUrl(receivingPaymentPointer) } if (sendAmount) options.sendAmount = sendAmount if (receiveAmount) options.receiveAmount = receiveAmount @@ -575,7 +575,7 @@ describe('QuoteService', (): void => { }) const options: CreateQuoteOptions = { paymentPointerId, - receiver: incomingPayment.url, + receiver: incomingPayment.getUrl(receivingPaymentPointer), receiveAmount } const expected: ExpectedQuote = { @@ -651,13 +651,12 @@ describe('QuoteService', (): void => { `( 'fails to create $description', async ({ sendAmount, receiveAmount }): Promise => { + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: receivingPaymentPointer.id + }) const options: CreateQuoteOptions = { paymentPointerId, - receiver: ( - await createIncomingPayment(deps, { - paymentPointerId: receivingPaymentPointer.id - }) - ).url + receiver: incomingPayment.getUrl(receivingPaymentPointer) } if (sendAmount) options.sendAmount = sendAmount if (receiveAmount) options.receiveAmount = receiveAmount @@ -672,14 +671,14 @@ describe('QuoteService', (): void => { jest .spyOn(ratesService, 'prices') .mockImplementation(() => Promise.reject(new Error('fail'))) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: receivingPaymentPointer.id + }) + await expect( quoteService.create({ paymentPointerId, - receiver: ( - await createIncomingPayment(deps, { - paymentPointerId: receivingPaymentPointer.id - }) - ).url, + receiver: incomingPayment.getUrl(receivingPaymentPointer), sendAmount }) ).rejects.toThrow('missing prices') diff --git a/packages/backend/src/open_payments/receiver/model.test.ts b/packages/backend/src/open_payments/receiver/model.test.ts index d2cbcf70d0..bfa9af0ca0 100644 --- a/packages/backend/src/open_payments/receiver/model.test.ts +++ b/packages/backend/src/open_payments/receiver/model.test.ts @@ -44,9 +44,7 @@ describe('Receiver Model', (): void => { assert(connection instanceof Connection) const receiver = Receiver.fromIncomingPayment( - incomingPayment.toOpenPaymentsType({ - ilpStreamConnection: connection - }) + await incomingPayment.toOpenPaymentsType(paymentPointer, connection) ) expect(receiver).toEqual({ @@ -55,8 +53,8 @@ describe('Receiver Model', (): void => { ilpAddress: expect.any(String), sharedSecret: expect.any(Buffer), incomingPayment: { - id: incomingPayment.url, - paymentPointer: incomingPayment.paymentPointer.url, + id: incomingPayment.getUrl(paymentPointer), + paymentPointer: paymentPointer.url, updatedAt: incomingPayment.updatedAt, createdAt: incomingPayment.createdAt, completed: incomingPayment.completed, @@ -77,12 +75,11 @@ describe('Receiver Model', (): void => { const connection = connectionService.get(incomingPayment) assert(connection instanceof Connection) + const openPaymentsIncomingPayment = + await incomingPayment.toOpenPaymentsType(paymentPointer, connection) + expect(() => - Receiver.fromIncomingPayment( - incomingPayment.toOpenPaymentsType({ - ilpStreamConnection: connection - }) - ) + Receiver.fromIncomingPayment(openPaymentsIncomingPayment) ).toThrow('Cannot create receiver from completed incoming payment') }) @@ -96,12 +93,11 @@ describe('Receiver Model', (): void => { const connection = connectionService.get(incomingPayment) assert(connection instanceof Connection) + const openPaymentsIncomingPayment = + await incomingPayment.toOpenPaymentsType(paymentPointer, connection) + expect(() => - Receiver.fromIncomingPayment( - incomingPayment.toOpenPaymentsType({ - ilpStreamConnection: connection - }) - ) + Receiver.fromIncomingPayment(openPaymentsIncomingPayment) ).toThrow('Cannot create receiver from expired incoming payment') }) @@ -115,12 +111,11 @@ describe('Receiver Model', (): void => { assert(connection instanceof Connection) ;(connection.ilpAddress as string) = 'not base 64 encoded' + const openPaymentsIncomingPayment = + await incomingPayment.toOpenPaymentsType(paymentPointer, connection) + expect(() => - Receiver.fromIncomingPayment( - incomingPayment.toOpenPaymentsType({ - ilpStreamConnection: connection - }) - ) + Receiver.fromIncomingPayment(openPaymentsIncomingPayment) ).toThrow('Invalid ILP address on stream connection') }) }) diff --git a/packages/backend/src/open_payments/receiver/model.ts b/packages/backend/src/open_payments/receiver/model.ts index f183f0b0fd..541f9efdee 100644 --- a/packages/backend/src/open_payments/receiver/model.ts +++ b/packages/backend/src/open_payments/receiver/model.ts @@ -4,7 +4,7 @@ import base64url from 'base64url' import { Amount, parseAmount } from '../amount' import { AssetOptions } from '../../asset/service' import { - IncomingPayment as OpenPaymentsIncomingPayment, + IncomingPaymentWithConnection as OpenPaymentsIncomingPaymentWithConnection, ILPStreamConnection as OpenPaymentsConnection } from 'open-payments' import { ConnectionBase } from '../connection/model' @@ -17,7 +17,7 @@ interface OpenPaymentsConnectionWithIlpAddress type ReceiverIncomingPayment = Readonly< Omit< - OpenPaymentsIncomingPayment, + OpenPaymentsIncomingPaymentWithConnection, | 'ilpStreamConnection' | 'expiresAt' | 'receivedAmount' @@ -51,7 +51,7 @@ export class Receiver extends ConnectionBase { } static fromIncomingPayment( - incomingPayment: OpenPaymentsIncomingPayment + incomingPayment: OpenPaymentsIncomingPaymentWithConnection ): Receiver { if (!incomingPayment.ilpStreamConnection) { throw new Error('Missing stream connection on incoming payment') diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index dd2908890d..0bca137283 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -7,10 +7,10 @@ import { AccessAction, IncomingPayment as OpenPaymentsIncomingPayment, PaymentPointer as OpenPaymentsPaymentPointer, - mockIncomingPayment, mockPaymentPointer, NonInteractiveGrant, - GrantRequest + GrantRequest, + mockIncomingPaymentWithConnection } from 'open-payments' import { URL } from 'url' import { v4 as uuid } from 'uuid' @@ -193,26 +193,26 @@ describe('Receiver Service', (): void => { 'get' ) - await expect(receiverService.get(incomingPayment.url)).resolves.toEqual( - { - assetCode: incomingPayment.receivedAmount.assetCode, - assetScale: incomingPayment.receivedAmount.assetScale, - ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer), - incomingPayment: { - id: incomingPayment.url, - paymentPointer: incomingPayment.paymentPointer.url, - completed: incomingPayment.completed, - receivedAmount: incomingPayment.receivedAmount, - incomingAmount: incomingPayment.incomingAmount, - description: incomingPayment.description || undefined, - externalRef: incomingPayment.externalRef || undefined, - expiresAt: incomingPayment.expiresAt, - updatedAt: new Date(incomingPayment.updatedAt), - createdAt: new Date(incomingPayment.createdAt) - } + await expect( + receiverService.get(incomingPayment.getUrl(paymentPointer)) + ).resolves.toEqual({ + assetCode: incomingPayment.receivedAmount.assetCode, + assetScale: incomingPayment.receivedAmount.assetScale, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer), + incomingPayment: { + id: incomingPayment.getUrl(paymentPointer), + paymentPointer: paymentPointer.url, + completed: incomingPayment.completed, + receivedAmount: incomingPayment.receivedAmount, + incomingAmount: incomingPayment.incomingAmount, + description: incomingPayment.description || undefined, + externalRef: incomingPayment.externalRef || undefined, + expiresAt: incomingPayment.expiresAt, + updatedAt: new Date(incomingPayment.updatedAt), + createdAt: new Date(incomingPayment.createdAt) } - ) + }) expect(clientGetIncomingPaymentSpy).not.toHaveBeenCalled() }) @@ -263,7 +263,7 @@ describe('Receiver Service', (): void => { paymentPointer = mockPaymentPointer({ authServer }) - incomingPayment = mockIncomingPayment({ + incomingPayment = mockIncomingPaymentWithConnection({ id: `${paymentPointer.id}/incoming-payments/${uuid()}`, paymentPointer: paymentPointer.id }) @@ -455,7 +455,7 @@ describe('Receiver Service', (): void => { expiresAt, incomingAmount }): Promise => { - const incomingPayment = mockIncomingPayment({ + const incomingPayment = mockIncomingPaymentWithConnection({ description, externalRef, expiresAt, diff --git a/packages/backend/src/open_payments/receiver/service.ts b/packages/backend/src/open_payments/receiver/service.ts index 3de999b5a1..70b5aa3a76 100644 --- a/packages/backend/src/open_payments/receiver/service.ts +++ b/packages/backend/src/open_payments/receiver/service.ts @@ -131,9 +131,7 @@ async function createLocalIncomingPayment( throw new Error(errorMessage) } - return incomingPaymentOrError.toOpenPaymentsType({ - ilpStreamConnection: connection - }) + return incomingPaymentOrError.toOpenPaymentsType(paymentPointer, connection) } async function getReceiver( @@ -276,7 +274,7 @@ async function getLocalIncomingPayment({ return undefined } - return incomingPayment.toOpenPaymentsType({ ilpStreamConnection: connection }) + return incomingPayment.toOpenPaymentsType(paymentPointer, connection) } async function getIncomingPaymentGrant( diff --git a/packages/open-payments/src/client/incoming-payment.test.ts b/packages/open-payments/src/client/incoming-payment.test.ts index 90acd2fb42..a9cd22c98e 100644 --- a/packages/open-payments/src/client/incoming-payment.test.ts +++ b/packages/open-payments/src/client/incoming-payment.test.ts @@ -14,6 +14,8 @@ import { mockILPStreamConnection, mockIncomingPayment, mockIncomingPaymentPaginationResult, + mockIncomingPaymentWithConnection, + mockIncomingPaymentWithConnectionUrl, mockOpenApiResponseValidators, silentLogger } from '../test/helpers' @@ -48,7 +50,7 @@ describe('incoming-payment', (): void => { describe('getIncomingPayment', (): void => { test('returns incoming payment if passes validation', async (): Promise => { - const incomingPayment = mockIncomingPayment() + const incomingPayment = mockIncomingPaymentWithConnection() nock(paymentPointer) .get('/incoming-payments/1') @@ -66,7 +68,7 @@ describe('incoming-payment', (): void => { }) test('throws if incoming payment does not pass validation', async (): Promise => { - const incomingPayment = mockIncomingPayment({ + const incomingPayment = mockIncomingPaymentWithConnection({ incomingAmount: { assetCode: 'USD', assetScale: 2, @@ -99,7 +101,7 @@ describe('incoming-payment', (): void => { }) test('throws if incoming payment does not pass open api validation', async (): Promise => { - const incomingPayment = mockIncomingPayment() + const incomingPayment = mockIncomingPaymentWithConnection() nock(paymentPointer) .get('/incoming-payments/1') @@ -134,7 +136,7 @@ describe('incoming-payment', (): void => { description, externalRef }): Promise => { - const incomingPayment = mockIncomingPayment({ + const incomingPayment = mockIncomingPaymentWithConnection({ incomingAmount, expiresAt, description, @@ -169,7 +171,7 @@ describe('incoming-payment', (): void => { value: '10' } - const incomingPayment = mockIncomingPayment({ + const incomingPayment = mockIncomingPaymentWithConnection({ incomingAmount: amount, receivedAmount: amount, completed: false @@ -191,7 +193,7 @@ describe('incoming-payment', (): void => { }) test('throws if the created incoming payment does not pass open api validation', async (): Promise => { - const incomingPayment = mockIncomingPayment() + const incomingPayment = mockIncomingPaymentWithConnection() const scope = nock(paymentPointer) .post('/incoming-payments') @@ -292,7 +294,7 @@ describe('incoming-payment', (): void => { async ({ first, cursor }): Promise => { const incomingPaymentPaginationResult = mockIncomingPaymentPaginationResult({ - result: Array(first).fill(mockIncomingPayment()) + result: Array(first).fill(mockIncomingPaymentWithConnectionUrl()) }) const scope = nock(paymentPointer) @@ -335,7 +337,7 @@ describe('incoming-payment', (): void => { async ({ last, cursor }): Promise => { const incomingPaymentPaginationResult = mockIncomingPaymentPaginationResult({ - result: Array(last).fill(mockIncomingPayment()) + result: Array(last).fill(mockIncomingPaymentWithConnectionUrl()) }) const scope = nock(paymentPointer) @@ -369,7 +371,7 @@ describe('incoming-payment', (): void => { }) test('throws if an incoming payment does not pass validation', async (): Promise => { - const incomingPayment = mockIncomingPayment({ + const incomingPayment = mockIncomingPaymentWithConnectionUrl({ incomingAmount: { assetCode: 'USD', assetScale: 2, @@ -531,7 +533,7 @@ describe('incoming-payment', (): void => { assetCode: 'CAD' }) - const incomingPayment = mockIncomingPayment({ + const incomingPayment = mockIncomingPaymentWithConnection({ incomingAmount: { assetCode: 'USD', assetScale: 2, @@ -556,7 +558,7 @@ describe('incoming-payment', (): void => { assetScale: 1 }) - const incomingPayment = mockIncomingPayment({ + const incomingPayment = mockIncomingPaymentWithConnection({ incomingAmount: { assetCode: 'USD', assetScale: 2, @@ -658,7 +660,7 @@ describe('incoming-payment', (): void => { const getSpy = jest .spyOn(requestors, 'get') - .mockResolvedValueOnce(mockIncomingPayment()) + .mockResolvedValueOnce(mockIncomingPaymentWithConnection()) await createIncomingPaymentRoutes({ openApi, @@ -684,7 +686,7 @@ describe('incoming-payment', (): void => { const incomingPaymentPaginationResult = mockIncomingPaymentPaginationResult({ - result: [mockIncomingPayment()] + result: [mockIncomingPaymentWithConnectionUrl()] }) const url = `${paymentPointer}${getRSPath('/incoming-payments')}` @@ -732,7 +734,9 @@ describe('incoming-payment', (): void => { const postSpy = jest .spyOn(requestors, 'post') - .mockResolvedValueOnce(mockIncomingPayment(incomingPaymentCreateArgs)) + .mockResolvedValueOnce( + mockIncomingPaymentWithConnection(incomingPaymentCreateArgs) + ) await createIncomingPaymentRoutes({ openApi, diff --git a/packages/open-payments/src/client/incoming-payment.ts b/packages/open-payments/src/client/incoming-payment.ts index 772bd35197..6eeed35d99 100644 --- a/packages/open-payments/src/client/incoming-payment.ts +++ b/packages/open-payments/src/client/incoming-payment.ts @@ -10,16 +10,23 @@ import { getRSPath, CreateIncomingPaymentArgs, PaginationArgs, - IncomingPaymentPaginationResult + IncomingPaymentPaginationResult, + IncomingPaymentWithConnectionUrl, + IncomingPaymentWithConnection } from '../types' import { get, post } from './requests' +type AnyIncomingPayment = + | IncomingPayment + | IncomingPaymentWithConnection + | IncomingPaymentWithConnectionUrl + export interface IncomingPaymentRoutes { - get(args: ResourceRequestArgs): Promise + get(args: ResourceRequestArgs): Promise create( args: CollectionRequestArgs, createArgs: CreateIncomingPaymentArgs - ): Promise + ): Promise complete(args: ResourceRequestArgs): Promise list( args: CollectionRequestArgs, @@ -33,7 +40,7 @@ export const createIncomingPaymentRoutes = ( const { axiosInstance, openApi, logger } = deps const getIncomingPaymentOpenApiValidator = - openApi.createResponseValidator({ + openApi.createResponseValidator({ path: getRSPath('/incoming-payments/{id}'), method: HttpMethod.GET }) @@ -92,7 +99,7 @@ export const createIncomingPaymentRoutes = ( export const getIncomingPayment = async ( deps: BaseDeps, args: ResourceRequestArgs, - validateOpenApiResponse: ResponseValidator + validateOpenApiResponse: ResponseValidator ) => { const { axiosInstance, logger } = deps const { url } = args @@ -215,9 +222,9 @@ export const listIncomingPayment = async ( return incomingPayments } -export const validateIncomingPayment = ( - payment: IncomingPayment -): IncomingPayment => { +export const validateIncomingPayment = ( + payment: T +): T => { if (payment.incomingAmount) { const { incomingAmount, receivedAmount } = payment if ( @@ -239,10 +246,12 @@ export const validateIncomingPayment = ( } if ( - !payment.ilpStreamConnection || - payment.ilpStreamConnection.assetCode !== + 'ilpStreamConnection' in payment && + typeof payment.ilpStreamConnection === 'object' && + (payment.ilpStreamConnection.assetCode !== payment.receivedAmount.assetCode || - payment.ilpStreamConnection.assetScale !== payment.receivedAmount.assetScale + payment.ilpStreamConnection.assetScale !== + payment.receivedAmount.assetScale) ) { throw new Error( 'Stream connection asset information does not match incoming payment asset information' @@ -253,8 +262,8 @@ export const validateIncomingPayment = ( } export const validateCreatedIncomingPayment = ( - payment: IncomingPayment -): IncomingPayment => { + payment: IncomingPaymentWithConnection +): IncomingPaymentWithConnection => { const { receivedAmount, completed } = payment if (BigInt(receivedAmount.value) !== BigInt(0)) { diff --git a/packages/open-payments/src/index.ts b/packages/open-payments/src/index.ts index e4aec97194..1cb9457589 100644 --- a/packages/open-payments/src/index.ts +++ b/packages/open-payments/src/index.ts @@ -2,7 +2,10 @@ export { GrantRequest, GrantContinuationRequest, IncomingPayment, + IncomingPaymentWithConnection, + IncomingPaymentWithConnectionUrl, ILPStreamConnection, + Quote, OutgoingPayment, InteractiveGrant, NonInteractiveGrant, @@ -26,6 +29,8 @@ export { mockILPStreamConnection, mockPaymentPointer, mockIncomingPayment, + mockIncomingPaymentWithConnection, + mockIncomingPaymentWithConnectionUrl, mockOutgoingPayment, mockIncomingPaymentPaginationResult, mockOutgoingPaymentPaginationResult, diff --git a/packages/open-payments/src/test/helpers.ts b/packages/open-payments/src/test/helpers.ts index 3376fa4aa6..fa2116c9c8 100644 --- a/packages/open-payments/src/test/helpers.ts +++ b/packages/open-payments/src/test/helpers.ts @@ -14,7 +14,9 @@ import { JWK, AccessToken, Quote, - IncomingPaymentPaginationResult + IncomingPaymentPaginationResult, + IncomingPaymentWithConnection, + IncomingPaymentWithConnectionUrl } from '../types' import base64url from 'base64url' import { v4 as uuid } from 'uuid' @@ -81,7 +83,7 @@ export const mockPaymentPointer = ( export const mockILPStreamConnection = ( overrides?: Partial ): ILPStreamConnection => ({ - id: uuid(), + id: `https://example.com/.well-known/pay/connections/${uuid()}`, sharedSecret: base64url('sharedSecret'), ilpAddress: 'test.ilpAddress', assetCode: 'USD', @@ -93,7 +95,7 @@ export const mockIncomingPayment = ( overrides?: Partial ): IncomingPayment => ({ id: `https://example.com/.well-known/pay/incoming-payments/${uuid()}`, - paymentPointer: 'paymentPointer', + paymentPointer: 'https://example.com/.well-known/pay', completed: false, incomingAmount: { assetCode: 'USD', @@ -107,7 +109,22 @@ export const mockIncomingPayment = ( }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), - ilpStreamConnection: mockILPStreamConnection(), + ...overrides +}) + +export const mockIncomingPaymentWithConnection = ( + overrides?: Partial +): IncomingPaymentWithConnection => ({ + ...mockIncomingPayment(), + ilpStreamConnection: mockILPStreamConnection(overrides?.ilpStreamConnection), + ...overrides +}) + +export const mockIncomingPaymentWithConnectionUrl = ( + overrides?: Partial +): IncomingPaymentWithConnectionUrl => ({ + ...mockIncomingPayment(), + ilpStreamConnection: mockILPStreamConnection().id, ...overrides }) @@ -134,8 +151,8 @@ export const mockIncomingPaymentPaginationResult = ( export const mockOutgoingPayment = ( overrides?: Partial ): OutgoingPayment => ({ - id: uuid(), - paymentPointer: 'paymentPointer', + id: `https://example.com/.well-known/pay/outgoing-payments/${uuid()}`, + paymentPointer: 'https://example.com/.well-known/pay', failed: false, sendAmount: { assetCode: 'USD', @@ -269,9 +286,9 @@ export const mockAccessToken = ( }) export const mockQuote = (overrides?: Partial): Quote => ({ - id: uuid(), - receiver: `receiver`, - paymentPointer: 'paymentPointer', + id: `https://example.com/.well-known/pay/quotes/${uuid()}`, + receiver: 'https://example.com/.well-known/peer', + paymentPointer: 'https://example.com/.well-known/pay', sendAmount: { value: '100', assetCode: 'USD', diff --git a/packages/open-payments/src/types.ts b/packages/open-payments/src/types.ts index 37d522a983..473a534039 100644 --- a/packages/open-payments/src/types.ts +++ b/packages/open-payments/src/types.ts @@ -12,11 +12,16 @@ import { export const getRSPath =

(path: P): string => path as string -export type IncomingPayment = + +export type IncomingPayment = RSComponents['schemas']['incoming-payment'] +export type IncomingPaymentWithConnection = RSComponents['schemas']['incoming-payment-with-connection'] +export type IncomingPaymentWithConnectionUrl = + RSComponents['schemas']['incoming-payment-with-connection-url'] export type CreateIncomingPaymentArgs = RSOperations['create-incoming-payment']['requestBody']['content']['application/json'] -export type IncomingPaymentPaginationResult = PaginationResult +export type IncomingPaymentPaginationResult = + PaginationResult export type ILPStreamConnection = RSComponents['schemas']['ilp-stream-connection'] export type OutgoingPayment = RSComponents['schemas']['outgoing-payment']