diff --git a/.changeset/short-sloths-sin.md b/.changeset/short-sloths-sin.md new file mode 100644 index 00000000..771a13b0 --- /dev/null +++ b/.changeset/short-sloths-sin.md @@ -0,0 +1,5 @@ +--- +'@interledger/open-payments': minor +--- + +Adds incomingPayment get method to unauthenticated client diff --git a/openapi/resource-server.yaml b/openapi/resource-server.yaml index 4930a600..37e1dfe5 100644 --- a/openapi/resource-server.yaml +++ b/openapi/resource-server.yaml @@ -954,7 +954,7 @@ components: assetCode: USD assetScale: 2 properties: - receiveAmount: + receivedAmount: $ref: ./schemas.yaml#/components/schemas/amount unresolvedProperites: false outgoing-payment: diff --git a/packages/open-payments/README.md b/packages/open-payments/README.md index a594a0e3..735efd10 100644 --- a/packages/open-payments/README.md +++ b/packages/open-payments/README.md @@ -34,7 +34,7 @@ This package exports two clients, an `UnauthenticatedClient` and an `Authenticat ### `UnauthenticatedClient` This client allows making requests to access publicly available resources, without needing authentication. -The three available resources are [Wallet Addresses](https://docs.openpayments.guide/reference/get-wallet-address), [Wallet Address Keys](https://docs.openpayments.guide/reference/get-wallet-address-keys), and [ILP Stream Connections](https://docs.openpayments.guide/reference/get-ilp-stream-connection). +The available resources are [Wallet Addresses](https://docs.openpayments.guide/reference/get-wallet-address), [Wallet Address Keys](https://docs.openpayments.guide/reference/get-wallet-address-keys), and the public version of [Incoming Payments](https://docs.openpayments.guide/reference/get-incoming-payment). ```ts import { createUnauthenticatedClient } from '@interledger/open-payments' @@ -47,6 +47,10 @@ const client = await createUnauthenticatedClient({ const walletAddress = await client.walletAddress.get({ url: 'https://cloud-nine-wallet/alice' }) + +const incomingPayment = await client.walletAddress.get({ + url: 'https://cloud-nine-wallet/incoming-payment/' +}) ``` ### `AuthenticatedClient` diff --git a/packages/open-payments/src/client/incoming-payment.test.ts b/packages/open-payments/src/client/incoming-payment.test.ts index 99b64490..bcc86f07 100644 --- a/packages/open-payments/src/client/incoming-payment.test.ts +++ b/packages/open-payments/src/client/incoming-payment.test.ts @@ -6,7 +6,9 @@ import { getIncomingPayment, validateCompletedIncomingPayment, validateCreatedIncomingPayment, - validateIncomingPayment + validateIncomingPayment, + createUnauthenticatedIncomingPaymentRoutes, + getPublicIncomingPayment } from './incoming-payment' import { OpenAPI, HttpMethod, createOpenAPI } from '@interledger/openapi' import { @@ -15,6 +17,7 @@ import { mockIncomingPaymentPaginationResult, mockIncomingPaymentWithPaymentMethods, mockOpenApiResponseValidators, + mockPublicIncomingPayment, silentLogger } from '../test/helpers' import nock from 'nock' @@ -122,6 +125,46 @@ describe('incoming-payment', (): void => { }) }) + describe('getPublicIncomingPayment', (): void => { + test('returns incoming payment if passes validation', async (): Promise => { + const publicIncomingPayment = mockPublicIncomingPayment() + + nock(walletAddress) + .get('/incoming-payments/1') + .reply(200, publicIncomingPayment) + + const result = await getPublicIncomingPayment( + { axiosInstance, logger }, + { + url: `${walletAddress}/incoming-payments/1` + }, + openApiValidators.successfulValidator + ) + expect(result).toStrictEqual(publicIncomingPayment) + }) + + test('throws if incoming payment does not pass open api validation', async (): Promise => { + const publicIncomingPayment = mockPublicIncomingPayment() + + nock(walletAddress) + .get('/incoming-payments/1') + .reply(200, publicIncomingPayment) + + await expect( + getPublicIncomingPayment( + { + axiosInstance, + logger + }, + { + url: `${walletAddress}/incoming-payments/1` + }, + openApiValidators.failedValidator + ) + ).rejects.toThrowError() + }) + }) + describe('createIncomingPayment', (): void => { test.each` incomingAmount | expiresAt | metadata @@ -622,6 +665,41 @@ describe('incoming-payment', (): void => { }) }) + describe('getPublic', (): void => { + test('calls getPublic method with correct validator', async (): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/incoming-payments/{id}' && method === HttpMethod.GET + + const url = `${walletAddress}/incoming-payments/1` + + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const publicIncomingPayment = mockPublicIncomingPayment() + + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(publicIncomingPayment) + + await createIncomingPaymentRoutes({ + openApi, + axiosInstance, + logger + }).getPublic({ accessToken, url }) + + expect(getSpy).toHaveBeenCalledWith( + { + axiosInstance, + logger + }, + { url }, + true + ) + }) + }) + describe('list', (): void => { test('calls get method with correct validator', async (): Promise => { const mockResponseValidator = ({ path, method }) => @@ -730,4 +808,40 @@ describe('incoming-payment', (): void => { }) }) }) + describe('unauthenticated routes', (): void => { + describe('get', (): void => { + test('calls get method with correct validator', async (): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/incoming-payments/{id}' && method === HttpMethod.GET + + const url = `${walletAddress}/incoming-payments/1` + + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const publicIncomingPayment = mockPublicIncomingPayment() + + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(publicIncomingPayment) + + await createUnauthenticatedIncomingPaymentRoutes({ + openApi, + axiosInstance, + logger + }).get({ url }) + + expect(getSpy).toHaveBeenCalledWith( + { + axiosInstance, + logger + }, + { url }, + true + ) + }) + }) + }) }) diff --git a/packages/open-payments/src/client/incoming-payment.ts b/packages/open-payments/src/client/incoming-payment.ts index 057e5ac3..b64bcbdf 100644 --- a/packages/open-payments/src/client/incoming-payment.ts +++ b/packages/open-payments/src/client/incoming-payment.ts @@ -3,7 +3,8 @@ import { BaseDeps, CollectionRequestArgs, ResourceRequestArgs, - RouteDeps + RouteDeps, + UnauthenticatedResourceRequestArgs } from '.' import { IncomingPayment, @@ -11,6 +12,7 @@ import { CreateIncomingPaymentArgs, PaginationArgs, IncomingPaymentPaginationResult, + PublicIncomingPayment, IncomingPaymentWithPaymentMethods } from '../types' import { get, post } from './requests' @@ -19,6 +21,7 @@ type AnyIncomingPayment = IncomingPayment | IncomingPaymentWithPaymentMethods export interface IncomingPaymentRoutes { get(args: ResourceRequestArgs): Promise + getPublic(args: ResourceRequestArgs): Promise create( args: CollectionRequestArgs, createArgs: CreateIncomingPaymentArgs @@ -41,6 +44,12 @@ export const createIncomingPaymentRoutes = ( method: HttpMethod.GET }) + const getPublicIncomingPaymentOpenApiValidator = + openApi.createResponseValidator({ + path: getRSPath('/incoming-payments/{id}'), + method: HttpMethod.GET + }) + const createIncomingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/incoming-payments'), @@ -66,6 +75,15 @@ export const createIncomingPaymentRoutes = ( args, getIncomingPaymentOpenApiValidator ), + getPublic: (args: ResourceRequestArgs) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { accessToken, ...argsWithoutAccessToken } = args + return getPublicIncomingPayment( + { axiosInstance, logger }, + argsWithoutAccessToken, + getPublicIncomingPaymentOpenApiValidator + ) + }, create: ( requestArgs: CollectionRequestArgs, createArgs: CreateIncomingPaymentArgs @@ -92,6 +110,31 @@ export const createIncomingPaymentRoutes = ( } } +export interface UnauthenticatedIncomingPaymentRoutes { + get(args: UnauthenticatedResourceRequestArgs): Promise +} + +export const createUnauthenticatedIncomingPaymentRoutes = ( + deps: RouteDeps +): UnauthenticatedIncomingPaymentRoutes => { + const { axiosInstance, openApi, logger } = deps + + const getPublicIncomingPaymentOpenApiValidator = + openApi.createResponseValidator({ + path: getRSPath('/incoming-payments/{id}'), + method: HttpMethod.GET + }) + + return { + get: (args: UnauthenticatedResourceRequestArgs) => + getPublicIncomingPayment( + { axiosInstance, logger }, + args, + getPublicIncomingPaymentOpenApiValidator + ) + } +} + export const getIncomingPayment = async ( deps: BaseDeps, args: ResourceRequestArgs, @@ -119,6 +162,15 @@ export const getIncomingPayment = async ( } } +export const getPublicIncomingPayment = async ( + deps: BaseDeps, + args: UnauthenticatedResourceRequestArgs, + validateOpenApiResponse: ResponseValidator +) => { + const { axiosInstance, logger } = deps + return await get({ axiosInstance, logger }, args, validateOpenApiResponse) +} + export const createIncomingPayment = async ( deps: BaseDeps, requestArgs: CollectionRequestArgs, diff --git a/packages/open-payments/src/client/index.ts b/packages/open-payments/src/client/index.ts index 716745b9..987b9ddf 100644 --- a/packages/open-payments/src/client/index.ts +++ b/packages/open-payments/src/client/index.ts @@ -5,7 +5,9 @@ import createLogger, { Logger } from 'pino' import config from '../config' import { createIncomingPaymentRoutes, - IncomingPaymentRoutes + createUnauthenticatedIncomingPaymentRoutes, + IncomingPaymentRoutes, + UnauthenticatedIncomingPaymentRoutes } from './incoming-payment' import { createWalletAddressRoutes, @@ -107,6 +109,7 @@ export interface CreateUnauthenticatedClientArgs { export interface UnauthenticatedClient { walletAddress: WalletAddressRoutes + incomingPayment: UnauthenticatedIncomingPaymentRoutes } /** @@ -124,6 +127,11 @@ export const createUnauthenticatedClient = async ( axiosInstance, openApi: resourceServerOpenApi, logger + }), + incomingPayment: createUnauthenticatedIncomingPaymentRoutes({ + axiosInstance, + openApi: resourceServerOpenApi, + logger }) } } diff --git a/packages/open-payments/src/openapi/generated/resource-server-types.ts b/packages/open-payments/src/openapi/generated/resource-server-types.ts index d1c95758..3a23ed59 100644 --- a/packages/open-payments/src/openapi/generated/resource-server-types.ts +++ b/packages/open-payments/src/openapi/generated/resource-server-types.ts @@ -179,7 +179,7 @@ export interface components { * @description An **incoming payment** resource with public details. */ "public-incoming-payment": { - receiveAmount?: external["schemas.yaml"]["components"]["schemas"]["amount"]; + receivedAmount?: external["schemas.yaml"]["components"]["schemas"]["amount"]; }; /** * Outgoing Payment diff --git a/packages/open-payments/src/test/helpers.ts b/packages/open-payments/src/test/helpers.ts index 8b34dba3..589a68e0 100644 --- a/packages/open-payments/src/test/helpers.ts +++ b/packages/open-payments/src/test/helpers.ts @@ -15,7 +15,8 @@ import { PendingGrant, Grant, IncomingPaymentWithPaymentMethods, - IlpPaymentMethod + IlpPaymentMethod, + PublicIncomingPayment } from '../types' import { v4 as uuid } from 'uuid' import { ResponseValidator } from '@interledger/openapi' @@ -122,6 +123,17 @@ export const mockIncomingPaymentWithPaymentMethods = ( ...overrides }) +export const mockPublicIncomingPayment = ( + overrides?: Partial +): PublicIncomingPayment => ({ + receivedAmount: { + assetCode: 'USD', + assetScale: 2, + value: '0' + }, + ...overrides +}) + export const mockIlpPaymentMethod = ( overrides?: Partial ): IlpPaymentMethod => ({ diff --git a/packages/open-payments/src/types.ts b/packages/open-payments/src/types.ts index d8bcec61..eedadf3a 100644 --- a/packages/open-payments/src/types.ts +++ b/packages/open-payments/src/types.ts @@ -13,6 +13,8 @@ export const getRSPath =

(path: P): string => path as string export type IncomingPayment = RSComponents['schemas']['incoming-payment'] +export type PublicIncomingPayment = + RSComponents['schemas']['public-incoming-payment'] export type IlpPaymentMethod = RSComponents['schemas']['ilp-payment-method'] type PaymentMethods = IlpPaymentMethod