From 5809202cac8945d21d80c8fe58bd0ebf78fb0285 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Mon, 16 Oct 2023 09:40:28 -0700 Subject: [PATCH] feat: add wallet address to subresource requests (#305) --- .changeset/clean-mice-sparkle.md | 5 + openapi/resource-server.yaml | 28 ++++++ openapi/schemas.yaml | 5 + packages/open-payments/src/client/grant.ts | 10 +- .../src/client/incoming-payment.test.ts | 97 ++++++++++++------- .../src/client/incoming-payment.ts | 33 ++++--- packages/open-payments/src/client/index.ts | 11 ++- .../src/client/outgoing-payment.test.ts | 89 +++++++++++------ .../src/client/outgoing-payment.ts | 25 +++-- .../open-payments/src/client/quote.test.ts | 20 ++-- packages/open-payments/src/client/quote.ts | 17 ++-- packages/open-payments/src/client/token.ts | 14 +-- .../openapi/generated/auth-server-types.ts | 6 ++ .../generated/resource-server-types.ts | 17 ++++ packages/open-payments/src/types.ts | 1 + packages/openapi/src/middleware.test.ts | 11 ++- 16 files changed, 263 insertions(+), 126 deletions(-) create mode 100644 .changeset/clean-mice-sparkle.md diff --git a/.changeset/clean-mice-sparkle.md b/.changeset/clean-mice-sparkle.md new file mode 100644 index 00000000..6ad5938d --- /dev/null +++ b/.changeset/clean-mice-sparkle.md @@ -0,0 +1,5 @@ +--- +'@interledger/open-payments': minor +--- + +Added "walletAddress" field to resource POST request bodies, added "wallet-address" query parameter to resource GET requests. diff --git a/openapi/resource-server.yaml b/openapi/resource-server.yaml index 37e1dfe5..ac42d585 100644 --- a/openapi/resource-server.yaml +++ b/openapi/resource-server.yaml @@ -137,6 +137,8 @@ paths: type: object additionalProperties: false properties: + walletAddress: + $ref: ./schemas.yaml#/components/schemas/walletAddress incomingAmount: $ref: ./schemas.yaml#/components/schemas/amount description: The maximum amount that should be paid into the wallet address under this incoming payment. @@ -151,6 +153,7 @@ paths: examples: Create incoming payment for $25 to pay invoice INV2022-02-0137: value: + walletAddress: 'https://openpayments.guide/alice/' incomingAmount: value: '2500' assetCode: USD @@ -275,6 +278,7 @@ paths: $ref: '#/components/responses/403' description: List all incoming payments on the wallet address parameters: + - $ref: '#/components/parameters/wallet-address' - $ref: '#/components/parameters/cursor' - $ref: '#/components/parameters/first' - $ref: '#/components/parameters/last' @@ -328,12 +332,15 @@ paths: examples: Create an outgoing payment based on a quote: value: + walletAddress: 'https://ilp.rafiki.money/alice/' quoteId: 'https://ilp.rafiki.money/quotes/ab03296b-0c8b-4776-b94e-7ee27d868d4d' metadata: externalRef: INV2022-02-0137 schema: type: object properties: + walletAddress: + $ref: ./schemas.yaml#/components/schemas/walletAddress quoteId: type: string format: uri @@ -344,6 +351,7 @@ paths: description: Additional metadata associated with the outgoing payment. (Optional) required: - quoteId + - walletAddress additionalProperties: false description: |- A subset of the outgoing payments schema is accepted as input to create a new outgoing payment. @@ -482,6 +490,7 @@ paths: $ref: '#/components/responses/403' description: List all outgoing payments on the wallet address parameters: + - $ref: '#/components/parameters/wallet-address' - $ref: '#/components/parameters/cursor' - $ref: '#/components/parameters/first' - $ref: '#/components/parameters/last' @@ -531,10 +540,12 @@ paths: examples: Create quote for an `receiver` that is an Incoming Payment with an `incomingAmount`: value: + walletAddress: 'https://ilp.rafiki.money/alice' receiver: 'https://ilp.rafiki.money/incoming-payments/37a0d0ee-26dc-4c66-89e0-01fbf93156f7' method: ilp Create fixed-send amount quote for $25: value: + walletAddress: 'https://ilp.rafiki.money/alice' receiver: 'https://ilp.rafiki.money/incoming-payments/37a0d0ee-26dc-4c66-89e0-01fbf93156f7' method: ilp debitAmount: @@ -543,6 +554,7 @@ paths: assetScale: 2 Create fixed-receive amount quote for $25: value: + walletAddress: 'https://ilp.rafiki.money/alice' receiver: 'https://ilp.rafiki.money/incoming-payments/37a0d0ee-26dc-4c66-89e0-01fbf93156f7' method: ilp receiveAmount: @@ -553,16 +565,21 @@ paths: oneOf: - description: Create quote for an `receiver` that is an Incoming Payment with an `incomingAmount` properties: + walletAddress: + $ref: ./schemas.yaml#/components/schemas/walletAddress receiver: $ref: ./schemas.yaml#/components/schemas/receiver method: $ref: '#/components/schemas/payment-method' required: + - walletAddress - receiver - method additionalProperties: false - description: Create a quote with a fixed-receive amount properties: + walletAddress: + $ref: ./schemas.yaml#/components/schemas/walletAddress receiver: $ref: ./schemas.yaml#/components/schemas/receiver method: @@ -571,12 +588,15 @@ paths: description: The fixed amount that would be paid into the receiving wallet address given a successful outgoing payment. $ref: ./schemas.yaml#/components/schemas/amount required: + - walletAddress - receiver - method - receiveAmount additionalProperties: false - description: Create a quote with a fixed-send amount properties: + walletAddress: + $ref: ./schemas.yaml#/components/schemas/walletAddress receiver: $ref: ./schemas.yaml#/components/schemas/receiver method: @@ -585,6 +605,7 @@ paths: description: The fixed amount that would be sent from the sending wallet address given a successful outgoing payment. $ref: ./schemas.yaml#/components/schemas/amount required: + - walletAddress - receiver - method - debitAmount @@ -1282,6 +1303,13 @@ components: type: string description: Sub-resource identifier required: true + wallet-address: + name: wallet-address + in: query + schema: + type: string + description: 'URL of a wallet address hosted by a Rafiki instance.' + required: true signature: name: Signature in: header diff --git a/openapi/schemas.yaml b/openapi/schemas.yaml index 0216faa4..fc0bce44 100644 --- a/openapi/schemas.yaml +++ b/openapi/schemas.yaml @@ -46,3 +46,8 @@ components: examples: - 'https://ilp.rafiki.money/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c' - 'https://ilp.rafiki.money/connections/016da9d5-c9a4-4c80-a354-86b915a04ff8' + walletAddress: + title: Wallet Address + type: string + description: 'URL of a wallet address hosted by a Rafiki instance.' + format: uri diff --git a/packages/open-payments/src/client/grant.ts b/packages/open-payments/src/client/grant.ts index 6be99e6a..420f2d07 100644 --- a/packages/open-payments/src/client/grant.ts +++ b/packages/open-payments/src/client/grant.ts @@ -1,6 +1,6 @@ import { HttpMethod } from '@interledger/openapi' import { - ResourceRequestArgs, + GrantOrTokenRequestArgs, RouteDeps, UnauthenticatedResourceRequestArgs } from '.' @@ -23,10 +23,10 @@ export interface GrantRoutes { args: Omit ): Promise continue( - postArgs: ResourceRequestArgs, + postArgs: GrantOrTokenRequestArgs, args: GrantContinuationRequest ): Promise - cancel(postArgs: ResourceRequestArgs): Promise + cancel(postArgs: GrantOrTokenRequestArgs): Promise } export const createGrantRoutes = (deps: GrantRouteDeps): GrantRoutes => { @@ -62,7 +62,7 @@ export const createGrantRoutes = (deps: GrantRouteDeps): GrantRoutes => { requestGrantValidator ), continue: ( - { url, accessToken }: ResourceRequestArgs, + { url, accessToken }: GrantOrTokenRequestArgs, args: GrantContinuationRequest ) => post( @@ -74,7 +74,7 @@ export const createGrantRoutes = (deps: GrantRouteDeps): GrantRoutes => { }, continueGrantValidator ), - cancel: ({ url, accessToken }: ResourceRequestArgs) => + cancel: ({ url, accessToken }: GrantOrTokenRequestArgs) => deleteRequest( deps, { diff --git a/packages/open-payments/src/client/incoming-payment.test.ts b/packages/open-payments/src/client/incoming-payment.test.ts index bcc86f07..bcf773d2 100644 --- a/packages/open-payments/src/client/incoming-payment.test.ts +++ b/packages/open-payments/src/client/incoming-payment.test.ts @@ -54,14 +54,14 @@ describe('incoming-payment', (): void => { test('returns incoming payment if passes validation', async (): Promise => { const incomingPayment = mockIncomingPaymentWithPaymentMethods() - nock(walletAddress) + nock(serverAddress) .get('/incoming-payments/1') .reply(200, incomingPayment) const result = await getIncomingPayment( { axiosInstance, logger }, { - url: `${walletAddress}/incoming-payments/1`, + url: `${serverAddress}/incoming-payments/1`, accessToken }, openApiValidators.successfulValidator @@ -83,7 +83,7 @@ describe('incoming-payment', (): void => { } }) - nock(walletAddress) + nock(serverAddress) .get('/incoming-payments/1') .reply(200, incomingPayment) @@ -94,7 +94,7 @@ describe('incoming-payment', (): void => { logger }, { - url: `${walletAddress}/incoming-payments/1`, + url: `${serverAddress}/incoming-payments/1`, accessToken }, openApiValidators.successfulValidator @@ -105,8 +105,9 @@ describe('incoming-payment', (): void => { test('throws if incoming payment does not pass open api validation', async (): Promise => { const incomingPayment = mockIncomingPaymentWithPaymentMethods() - nock(walletAddress) + nock(serverAddress) .get('/incoming-payments/1') + .query({ 'wallet-address': walletAddress }) .reply(200, incomingPayment) await expect( @@ -116,7 +117,7 @@ describe('incoming-payment', (): void => { logger }, { - url: `${walletAddress}/incoming-payments/1`, + url: `${serverAddress}/incoming-payments/1`, accessToken }, openApiValidators.failedValidator @@ -179,15 +180,16 @@ describe('incoming-payment', (): void => { metadata }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .post('/incoming-payments') .reply(200, incomingPayment) const result = await createIncomingPayment( { axiosInstance, logger }, - { walletAddress, accessToken }, + { url: serverAddress, accessToken }, openApiValidators.successfulValidator, { + walletAddress, incomingAmount, expiresAt, metadata @@ -212,16 +214,16 @@ describe('incoming-payment', (): void => { completed: false }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .post('/incoming-payments') .reply(200, incomingPayment) await expect( createIncomingPayment( { axiosInstance, logger }, - { walletAddress, accessToken }, + { url: serverAddress, accessToken }, openApiValidators.successfulValidator, - {} + { walletAddress } ) ).rejects.toThrowError() scope.done() @@ -230,16 +232,16 @@ describe('incoming-payment', (): void => { test('throws if the created incoming payment does not pass open api validation', async (): Promise => { const incomingPayment = mockIncomingPaymentWithPaymentMethods() - const scope = nock(walletAddress) + const scope = nock(serverAddress) .post('/incoming-payments') .reply(200, incomingPayment) await expect( createIncomingPayment( { axiosInstance, logger }, - { walletAddress, accessToken }, + { url: serverAddress, accessToken }, openApiValidators.failedValidator, - {} + { walletAddress } ) ).rejects.toThrowError() scope.done() @@ -275,7 +277,7 @@ describe('incoming-payment', (): void => { completed: false }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .post(`/incoming-payments/${incomingPayment.id}/complete`) .reply(200, incomingPayment) @@ -283,7 +285,7 @@ describe('incoming-payment', (): void => { completeIncomingPayment( { axiosInstance, logger }, { - url: `${walletAddress}/incoming-payments/${incomingPayment.id}`, + url: `${serverAddress}/incoming-payments/${incomingPayment.id}`, accessToken }, openApiValidators.successfulValidator @@ -298,7 +300,7 @@ describe('incoming-payment', (): void => { completed: true }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .post(`/incoming-payments/${incomingPayment.id}/complete`) .reply(200, incomingPayment) @@ -306,7 +308,7 @@ describe('incoming-payment', (): void => { completeIncomingPayment( { axiosInstance, logger }, { - url: `${walletAddress}/incoming-payments/${incomingPayment.id}`, + url: `${serverAddress}/incoming-payments/${incomingPayment.id}`, accessToken }, openApiValidators.failedValidator @@ -332,9 +334,10 @@ describe('incoming-payment', (): void => { result: Array(first).fill(mockIncomingPayment()) }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/incoming-payments') .query({ + 'wallet-address': walletAddress, ...(first ? { first } : {}), ...(cursor ? { cursor } : {}) }) @@ -346,11 +349,13 @@ describe('incoming-payment', (): void => { logger }, { + url: serverAddress, walletAddress, accessToken }, openApiValidators.successfulValidator, { + 'wallet-address': walletAddress, first, cursor } @@ -375,9 +380,10 @@ describe('incoming-payment', (): void => { result: Array(last).fill(mockIncomingPayment()) }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/incoming-payments') .query({ + 'wallet-address': walletAddress, ...(last ? { last } : {}), cursor }) @@ -389,11 +395,13 @@ describe('incoming-payment', (): void => { logger }, { + url: serverAddress, walletAddress, accessToken }, openApiValidators.successfulValidator, { + 'wallet-address': walletAddress, last, cursor } @@ -424,8 +432,9 @@ describe('incoming-payment', (): void => { result: [incomingPayment] }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/incoming-payments') + .query({ 'wallet-address': walletAddress }) .reply(200, incomingPaymentPaginationResult) await expect( @@ -435,6 +444,7 @@ describe('incoming-payment', (): void => { logger }, { + url: serverAddress, walletAddress, accessToken }, @@ -449,14 +459,15 @@ describe('incoming-payment', (): void => { const incomingPaymentPaginationResult = mockIncomingPaymentPaginationResult() - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/incoming-payments') + .query({ 'wallet-address': walletAddress }) .reply(200, incomingPaymentPaginationResult) await expect( listIncomingPayment( { axiosInstance, logger }, - { walletAddress, accessToken }, + { url: serverAddress, walletAddress, accessToken }, openApiValidators.failedValidator ) ).rejects.toThrowError() @@ -637,7 +648,7 @@ describe('incoming-payment', (): void => { const mockResponseValidator = ({ path, method }) => path === '/incoming-payments/{id}' && method === HttpMethod.GET - const url = `${walletAddress}/incoming-payments/1` + const url = `${serverAddress}/incoming-payments/1` jest .spyOn(openApi, 'createResponseValidator') @@ -659,7 +670,10 @@ describe('incoming-payment', (): void => { axiosInstance, logger }, - { url, accessToken }, + { + url, + accessToken + }, true ) }) @@ -670,7 +684,7 @@ describe('incoming-payment', (): void => { const mockResponseValidator = ({ path, method }) => path === '/incoming-payments/{id}' && method === HttpMethod.GET - const url = `${walletAddress}/incoming-payments/1` + const url = `${serverAddress}/incoming-payments/1` jest .spyOn(openApi, 'createResponseValidator') @@ -687,7 +701,7 @@ describe('incoming-payment', (): void => { openApi, axiosInstance, logger - }).getPublic({ accessToken, url }) + }).getPublic({ url }) expect(getSpy).toHaveBeenCalledWith( { @@ -709,7 +723,7 @@ describe('incoming-payment', (): void => { mockIncomingPaymentPaginationResult({ result: [mockIncomingPayment()] }) - const url = `${walletAddress}${getRSPath('/incoming-payments')}` + const url = `${serverAddress}${getRSPath('/incoming-payments')}` jest .spyOn(openApi, 'createResponseValidator') @@ -724,14 +738,20 @@ describe('incoming-payment', (): void => { openApi, axiosInstance, logger - }).list({ walletAddress, accessToken }) + }).list({ url: serverAddress, walletAddress, accessToken }) expect(getSpy).toHaveBeenCalledWith( { axiosInstance, logger }, - { url, accessToken }, + { + url, + accessToken, + queryParams: { + 'wallet-address': walletAddress + } + }, true ) }) @@ -742,8 +762,9 @@ describe('incoming-payment', (): void => { const mockResponseValidator = ({ path, method }) => path === '/incoming-payments' && method === HttpMethod.POST - const url = `${walletAddress}/incoming-payments` + const url = `${serverAddress}/incoming-payments` const incomingPaymentCreateArgs = { + walletAddress, description: 'Invoice', incomingAmount: { assetCode: 'USD', assetScale: 2, value: '10' } } @@ -761,7 +782,10 @@ describe('incoming-payment', (): void => { openApi, axiosInstance, logger - }).create({ walletAddress, accessToken }, incomingPaymentCreateArgs) + }).create( + { url: serverAddress, accessToken }, + incomingPaymentCreateArgs + ) expect(postSpy).toHaveBeenCalledWith( { @@ -780,7 +804,7 @@ describe('incoming-payment', (): void => { path === '/incoming-payments/{id}/complete' && method === HttpMethod.POST - const incomingPaymentUrl = `${walletAddress}/incoming-payments/1` + const incomingPaymentUrl = `${serverAddress}/incoming-payments/1` jest .spyOn(openApi, 'createResponseValidator') @@ -802,7 +826,10 @@ describe('incoming-payment', (): void => { axiosInstance, logger }, - { url: `${incomingPaymentUrl}/complete`, accessToken }, + { + url: `${incomingPaymentUrl}/complete`, + accessToken + }, true ) }) @@ -814,7 +841,7 @@ describe('incoming-payment', (): void => { const mockResponseValidator = ({ path, method }) => path === '/incoming-payments/{id}' && method === HttpMethod.GET - const url = `${walletAddress}/incoming-payments/1` + const url = `${serverAddress}/incoming-payments/1` jest .spyOn(openApi, 'createResponseValidator') diff --git a/packages/open-payments/src/client/incoming-payment.ts b/packages/open-payments/src/client/incoming-payment.ts index b64bcbdf..36c847d5 100644 --- a/packages/open-payments/src/client/incoming-payment.ts +++ b/packages/open-payments/src/client/incoming-payment.ts @@ -1,8 +1,8 @@ import { HttpMethod, ResponseValidator } from '@interledger/openapi' import { BaseDeps, - CollectionRequestArgs, ResourceRequestArgs, + CollectionRequestArgs, RouteDeps, UnauthenticatedResourceRequestArgs } from '.' @@ -21,9 +21,11 @@ type AnyIncomingPayment = IncomingPayment | IncomingPaymentWithPaymentMethods export interface IncomingPaymentRoutes { get(args: ResourceRequestArgs): Promise - getPublic(args: ResourceRequestArgs): Promise + getPublic( + args: UnauthenticatedResourceRequestArgs + ): Promise create( - args: CollectionRequestArgs, + args: ResourceRequestArgs, createArgs: CreateIncomingPaymentArgs ): Promise complete(args: ResourceRequestArgs): Promise @@ -75,17 +77,16 @@ export const createIncomingPaymentRoutes = ( args, getIncomingPaymentOpenApiValidator ), - getPublic: (args: ResourceRequestArgs) => { + getPublic: (args: UnauthenticatedResourceRequestArgs) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { accessToken, ...argsWithoutAccessToken } = args return getPublicIncomingPayment( { axiosInstance, logger }, - argsWithoutAccessToken, + args, getPublicIncomingPaymentOpenApiValidator ) }, create: ( - requestArgs: CollectionRequestArgs, + requestArgs: ResourceRequestArgs, createArgs: CreateIncomingPaymentArgs ) => createIncomingPayment( @@ -145,7 +146,9 @@ export const getIncomingPayment = async ( const incomingPayment = await get( { axiosInstance, logger }, - args, + { + ...args + }, validateOpenApiResponse ) @@ -173,13 +176,13 @@ export const getPublicIncomingPayment = async ( export const createIncomingPayment = async ( deps: BaseDeps, - requestArgs: CollectionRequestArgs, + requestArgs: ResourceRequestArgs, validateOpenApiResponse: ResponseValidator, createArgs: CreateIncomingPaymentArgs ) => { const { axiosInstance, logger } = deps - const { walletAddress, accessToken } = requestArgs - const url = `${walletAddress}${getRSPath('/incoming-payments')}` + const { url: baseUrl, accessToken } = requestArgs + const url = `${baseUrl}${getRSPath('/incoming-payments')}` const incomingPayment = await post( { axiosInstance, logger }, @@ -235,16 +238,18 @@ export const listIncomingPayment = async ( pagination?: PaginationArgs ) => { const { axiosInstance, logger } = deps - const { accessToken, walletAddress } = args + const { url: baseUrl, accessToken, walletAddress } = args - const url = `${walletAddress}${getRSPath('/incoming-payments')}` + const url = `${baseUrl}${getRSPath('/incoming-payments')}` const incomingPayments = await get( { axiosInstance, logger }, { url, accessToken, - ...(pagination ? { queryParams: { ...pagination } } : {}) + ...(pagination + ? { queryParams: { ...pagination, 'wallet-address': walletAddress } } + : { queryParams: { 'wallet-address': walletAddress } }) }, validateOpenApiResponse ) diff --git a/packages/open-payments/src/client/index.ts b/packages/open-payments/src/client/index.ts index 987b9ddf..a0737faf 100644 --- a/packages/open-payments/src/client/index.ts +++ b/packages/open-payments/src/client/index.ts @@ -45,7 +45,7 @@ export interface UnauthenticatedResourceRequestArgs { * * For example, if the requested resource is an incoming payment: * ``` - * https://openpayments.guide/alice/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c` + * https://openpayments.guide/incoming-payments/08394f02-7b7b-45e2-b645-51d04e7c330c` * ``` */ url: string @@ -60,11 +60,18 @@ interface AuthenticatedRequestArgs { */ accessToken: string } + +export interface GrantOrTokenRequestArgs + extends UnauthenticatedResourceRequestArgs, + AuthenticatedRequestArgs {} + export interface ResourceRequestArgs extends UnauthenticatedResourceRequestArgs, AuthenticatedRequestArgs {} -export interface CollectionRequestArgs extends AuthenticatedRequestArgs { +export interface CollectionRequestArgs + extends UnauthenticatedResourceRequestArgs, + AuthenticatedRequestArgs { /** * The wallet address URL of the requested collection. * diff --git a/packages/open-payments/src/client/outgoing-payment.test.ts b/packages/open-payments/src/client/outgoing-payment.test.ts index d3f83bd7..ab195c94 100644 --- a/packages/open-payments/src/client/outgoing-payment.test.ts +++ b/packages/open-payments/src/client/outgoing-payment.test.ts @@ -38,20 +38,21 @@ describe('outgoing-payment', (): void => { const axiosInstance = defaultAxiosInstance const logger = silentLogger const walletAddress = `http://localhost:1000/.well-known/pay` + const serverAddress = 'http://localhost:1000' const openApiValidators = mockOpenApiResponseValidators() describe('getOutgoingPayment', (): void => { test('returns outgoing payment if passes validation', async (): Promise => { const outgoingPayment = mockOutgoingPayment() - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/outgoing-payments/1') .reply(200, outgoingPayment) const result = await getOutgoingPayment( { axiosInstance, logger }, { - url: `${walletAddress}/outgoing-payments/1`, + url: `${serverAddress}/outgoing-payments/1`, accessToken: 'accessToken' }, openApiValidators.successfulValidator @@ -74,7 +75,7 @@ describe('outgoing-payment', (): void => { } }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/outgoing-payments/1') .reply(200, outgoingPayment) @@ -82,7 +83,7 @@ describe('outgoing-payment', (): void => { getOutgoingPayment( { axiosInstance, logger }, { - url: `${walletAddress}/outgoing-payments/1`, + url: `${serverAddress}/outgoing-payments/1`, accessToken: 'accessToken' }, openApiValidators.successfulValidator @@ -94,7 +95,7 @@ describe('outgoing-payment', (): void => { test('throws if outgoing payment does not pass open api validation', async (): Promise => { const outgoingPayment = mockOutgoingPayment() - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/outgoing-payments/1') .reply(200, outgoingPayment) @@ -102,7 +103,7 @@ describe('outgoing-payment', (): void => { getOutgoingPayment( { axiosInstance, logger }, { - url: `${walletAddress}/outgoing-payments/1`, + url: `${serverAddress}/outgoing-payments/1`, accessToken: 'accessToken' }, openApiValidators.failedValidator @@ -127,9 +128,10 @@ describe('outgoing-payment', (): void => { result: Array(first).fill(mockOutgoingPayment()) }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/outgoing-payments') .query({ + 'wallet-address': walletAddress, ...(first ? { first } : {}), ...(cursor ? { cursor } : {}) }) @@ -138,11 +140,13 @@ describe('outgoing-payment', (): void => { const result = await listOutgoingPayments( { axiosInstance, logger }, { + url: serverAddress, walletAddress, accessToken: 'accessToken' }, openApiValidators.successfulValidator, { + 'wallet-address': walletAddress, first, cursor } @@ -166,9 +170,13 @@ describe('outgoing-payment', (): void => { result: Array(last).fill(mockOutgoingPayment()) }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/outgoing-payments') - .query({ ...(last ? { last } : {}), cursor }) + .query({ + 'wallet-address': walletAddress, + ...(last ? { last } : {}), + cursor + }) .reply(200, outgoingPaymentPaginationResult) const result = await listOutgoingPayments( @@ -177,11 +185,13 @@ describe('outgoing-payment', (): void => { logger }, { + url: serverAddress, walletAddress, accessToken: 'accessToken' }, openApiValidators.successfulValidator, { + 'wallet-address': walletAddress, last, cursor } @@ -211,8 +221,9 @@ describe('outgoing-payment', (): void => { result: [invalidOutgoingPayment] }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/outgoing-payments') + .query({ 'wallet-address': walletAddress }) .reply(200, outgoingPaymentPaginationResult) await expect( @@ -222,6 +233,7 @@ describe('outgoing-payment', (): void => { logger }, { + url: serverAddress, walletAddress, accessToken: 'accessToken' }, @@ -235,8 +247,9 @@ describe('outgoing-payment', (): void => { const outgoingPaymentPaginationResult = mockOutgoingPaymentPaginationResult() - const scope = nock(walletAddress) + const scope = nock(serverAddress) .get('/outgoing-payments') + .query({ 'wallet-address': walletAddress }) .reply(200, outgoingPaymentPaginationResult) await expect( @@ -246,6 +259,7 @@ describe('outgoing-payment', (): void => { logger }, { + url: serverAddress, walletAddress, accessToken: 'accessToken' }, @@ -257,7 +271,7 @@ describe('outgoing-payment', (): void => { }) describe('createOutgoingPayment', (): void => { - const quoteId = `${walletAddress}/quotes/${uuid()}` + const quoteId = `${serverAddress}/quotes/${uuid()}` test.each` metadata @@ -269,20 +283,21 @@ describe('outgoing-payment', (): void => { metadata }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .post('/outgoing-payments') .reply(200, outgoingPayment) const result = await createOutgoingPayment( { axiosInstance, logger }, { - walletAddress, + url: serverAddress, accessToken: 'accessToken' }, openApiValidators.successfulValidator, { quoteId, - metadata + metadata, + walletAddress } ) expect(result).toEqual(outgoingPayment) @@ -303,7 +318,7 @@ describe('outgoing-payment', (): void => { } }) - const scope = nock(walletAddress) + const scope = nock(serverAddress) .post('/outgoing-payments') .reply(200, outgoingPayment) @@ -311,12 +326,13 @@ describe('outgoing-payment', (): void => { createOutgoingPayment( { axiosInstance, logger }, { - walletAddress, + url: serverAddress, accessToken: 'accessToken' }, openApiValidators.successfulValidator, { - quoteId: uuid() + quoteId: uuid(), + walletAddress } ) ).rejects.toThrowError() @@ -326,7 +342,7 @@ describe('outgoing-payment', (): void => { test('throws if outgoing payment does not pass open api validation', async (): Promise => { const outgoingPayment = mockOutgoingPayment() - const scope = nock(walletAddress) + const scope = nock(serverAddress) .post('/outgoing-payments') .reply(200, outgoingPayment) @@ -337,12 +353,13 @@ describe('outgoing-payment', (): void => { logger }, { - walletAddress, + url: serverAddress, accessToken: 'accessToken' }, openApiValidators.failedValidator, { - quoteId: uuid() + quoteId: uuid(), + walletAddress } ) ).rejects.toThrowError() @@ -443,7 +460,7 @@ describe('outgoing-payment', (): void => { const mockResponseValidator = ({ path, method }) => path === '/outgoing-payments/{id}' && method === HttpMethod.GET - const url = `${walletAddress}/outgoing-payments/1` + const url = `${serverAddress}/outgoing-payments/1` jest .spyOn(openApi, 'createResponseValidator') @@ -465,7 +482,10 @@ describe('outgoing-payment', (): void => { axiosInstance, logger }, - { url, accessToken: 'accessToken' }, + { + url, + accessToken: 'accessToken' + }, true ) }) @@ -480,7 +500,7 @@ describe('outgoing-payment', (): void => { mockOutgoingPaymentPaginationResult({ result: [mockOutgoingPayment()] }) - const url = `${walletAddress}/outgoing-payments` + const url = `${serverAddress}/outgoing-payments` jest .spyOn(openApi, 'createResponseValidator') @@ -495,14 +515,24 @@ describe('outgoing-payment', (): void => { openApi, axiosInstance, logger - }).list({ walletAddress, accessToken: 'accessToken' }) + }).list({ + url: serverAddress, + walletAddress, + accessToken: 'accessToken' + }) expect(getSpy).toHaveBeenCalledWith( { axiosInstance, logger }, - { url, accessToken: 'accessToken' }, + { + url, + accessToken: 'accessToken', + queryParams: { + 'wallet-address': walletAddress + } + }, true ) }) @@ -513,9 +543,10 @@ describe('outgoing-payment', (): void => { const mockResponseValidator = ({ path, method }) => path === '/outgoing-payments' && method === HttpMethod.POST - const url = `${walletAddress}/outgoing-payments` + const url = `${serverAddress}/outgoing-payments` const outgoingPaymentCreateArgs = { - quoteId: uuid() + quoteId: uuid(), + walletAddress } jest @@ -532,7 +563,7 @@ describe('outgoing-payment', (): void => { axiosInstance, logger }).create( - { walletAddress, accessToken: 'accessToken' }, + { url: serverAddress, accessToken: 'accessToken' }, outgoingPaymentCreateArgs ) diff --git a/packages/open-payments/src/client/outgoing-payment.ts b/packages/open-payments/src/client/outgoing-payment.ts index 76d1038a..48301d4e 100644 --- a/packages/open-payments/src/client/outgoing-payment.ts +++ b/packages/open-payments/src/client/outgoing-payment.ts @@ -1,8 +1,8 @@ import { HttpMethod, ResponseValidator } from '@interledger/openapi' import { BaseDeps, - CollectionRequestArgs, ResourceRequestArgs, + CollectionRequestArgs, RouteDeps } from '.' import { @@ -21,7 +21,7 @@ export interface OutgoingPaymentRoutes { pagination?: PaginationArgs ): Promise create( - requestArgs: CollectionRequestArgs, + requestArgs: ResourceRequestArgs, createArgs: CreateOutgoingPaymentArgs ): Promise } @@ -64,7 +64,7 @@ export const createOutgoingPaymentRoutes = ( pagination ), create: ( - requestArgs: CollectionRequestArgs, + requestArgs: ResourceRequestArgs, createArgs: CreateOutgoingPaymentArgs ) => createOutgoingPayment( @@ -86,7 +86,10 @@ export const getOutgoingPayment = async ( const outgoingPayment = await get( { axiosInstance, logger }, - { url, accessToken }, + { + url, + accessToken + }, validateOpenApiResponse ) @@ -105,13 +108,13 @@ export const getOutgoingPayment = async ( export const createOutgoingPayment = async ( deps: BaseDeps, - requestArgs: CollectionRequestArgs, + requestArgs: ResourceRequestArgs, validateOpenApiResponse: ResponseValidator, createArgs: CreateOutgoingPaymentArgs ) => { const { axiosInstance, logger } = deps - const { walletAddress, accessToken } = requestArgs - const url = `${walletAddress}${getRSPath('/outgoing-payments')}` + const { url: baseUrl, accessToken } = requestArgs + const url = `${baseUrl}${getRSPath('/outgoing-payments')}` const outgoingPayment = await post( { axiosInstance, logger }, @@ -139,15 +142,17 @@ export const listOutgoingPayments = async ( pagination?: PaginationArgs ) => { const { axiosInstance, logger } = deps - const { accessToken, walletAddress } = requestArgs - const url = `${walletAddress}${getRSPath('/outgoing-payments')}` + const { url: baseUrl, accessToken, walletAddress } = requestArgs + const url = `${baseUrl}${getRSPath('/outgoing-payments')}` const outgoingPayments = await get( { axiosInstance, logger }, { url, accessToken, - ...(pagination ? { queryParams: { ...pagination } } : {}) + ...(pagination + ? { queryParams: { ...pagination, 'wallet-address': walletAddress } } + : { queryParams: { 'wallet-address': walletAddress } }) }, validateOpenApiResponse ) diff --git a/packages/open-payments/src/client/quote.test.ts b/packages/open-payments/src/client/quote.test.ts index 7d5421c8..10cea754 100644 --- a/packages/open-payments/src/client/quote.test.ts +++ b/packages/open-payments/src/client/quote.test.ts @@ -75,25 +75,25 @@ describe('quote', (): void => { describe('createQuote', (): void => { test('returns the quote if it passes open api validation', async (): Promise => { - const scope = nock(walletAddress).post(`/quotes`).reply(200, quote) + const scope = nock(baseUrl).post(`/quotes`).reply(200, quote) const result = await createQuote( { axiosInstance, logger }, { - walletAddress, + url: baseUrl, accessToken }, openApiValidators.successfulValidator, - { receiver: quote.receiver, method: 'ilp' } + { receiver: quote.receiver, method: 'ilp', walletAddress } ) expect(result).toStrictEqual(quote) scope.done() }) test('throws if quote does not pass open api validation', async (): Promise => { - const scope = nock(walletAddress).post(`/quotes`).reply(200, quote) + const scope = nock(baseUrl).post(`/quotes`).reply(200, quote) await expect(() => createQuote( { @@ -101,11 +101,11 @@ describe('quote', (): void => { logger }, { - walletAddress, + url: baseUrl, accessToken }, openApiValidators.failedValidator, - { receiver: quote.receiver, method: 'ilp' } + { receiver: quote.receiver, method: 'ilp', walletAddress } ) ).rejects.toThrowError() scope.done() @@ -161,7 +161,7 @@ describe('quote', (): void => { const postSpy = jest .spyOn(requestors, 'post') .mockResolvedValueOnce(quote) - const url = `${walletAddress}${getRSPath('/quotes')}` + const url = `${baseUrl}${getRSPath('/quotes')}` await createQuoteRoutes({ openApi, @@ -169,10 +169,10 @@ describe('quote', (): void => { logger }).create( { - walletAddress, + url: baseUrl, accessToken }, - { receiver: quote.receiver, method: 'ilp' } + { receiver: quote.receiver, method: 'ilp', walletAddress } ) expect(postSpy).toHaveBeenCalledWith( @@ -183,7 +183,7 @@ describe('quote', (): void => { { url, accessToken, - body: { receiver: quote.receiver, method: 'ilp' } + body: { receiver: quote.receiver, method: 'ilp', walletAddress } }, true ) diff --git a/packages/open-payments/src/client/quote.ts b/packages/open-payments/src/client/quote.ts index 13f2deeb..275f9272 100644 --- a/packages/open-payments/src/client/quote.ts +++ b/packages/open-payments/src/client/quote.ts @@ -1,17 +1,12 @@ import { HttpMethod, ResponseValidator } from '@interledger/openapi' -import { - ResourceRequestArgs, - CollectionRequestArgs, - BaseDeps, - RouteDeps -} from '.' +import { ResourceRequestArgs, BaseDeps, RouteDeps } from '.' import { CreateQuoteArgs, getRSPath, Quote } from '../types' import { get, post } from './requests' export interface QuoteRoutes { get(args: ResourceRequestArgs): Promise create( - createArgs: CollectionRequestArgs, + createArgs: ResourceRequestArgs, createQuoteArgs: CreateQuoteArgs ): Promise } @@ -33,7 +28,7 @@ export const createQuoteRoutes = (deps: RouteDeps): QuoteRoutes => { get: (args: ResourceRequestArgs) => getQuote({ axiosInstance, logger }, args, getQuoteOpenApiValidator), create: ( - createArgs: CollectionRequestArgs, + createArgs: ResourceRequestArgs, createQuoteArgs: CreateQuoteArgs ) => createQuote( @@ -63,13 +58,13 @@ export const getQuote = async ( export const createQuote = async ( deps: BaseDeps, - createArgs: CollectionRequestArgs, + createArgs: ResourceRequestArgs, validateOpenApiResponse: ResponseValidator, createQuoteArgs: CreateQuoteArgs ) => { const { axiosInstance, logger } = deps - const { accessToken, walletAddress } = createArgs - const url = `${walletAddress}${getRSPath('/quotes')}` + const { accessToken, url: baseUrl } = createArgs + const url = `${baseUrl}${getRSPath('/quotes')}` const quote = await post( { axiosInstance, logger }, diff --git a/packages/open-payments/src/client/token.ts b/packages/open-payments/src/client/token.ts index aafea3df..ea4a8729 100644 --- a/packages/open-payments/src/client/token.ts +++ b/packages/open-payments/src/client/token.ts @@ -1,16 +1,16 @@ import { HttpMethod, ResponseValidator } from '@interledger/openapi' -import { ResourceRequestArgs, RouteDeps } from '.' +import { GrantOrTokenRequestArgs, RouteDeps } from '.' import { getASPath, AccessToken } from '../types' import { deleteRequest, post } from './requests' export interface TokenRoutes { - rotate(args: ResourceRequestArgs): Promise - revoke(args: ResourceRequestArgs): Promise + rotate(args: GrantOrTokenRequestArgs): Promise + revoke(args: GrantOrTokenRequestArgs): Promise } export const rotateToken = async ( deps: RouteDeps, - args: ResourceRequestArgs, + args: GrantOrTokenRequestArgs, validateOpenApiResponse: ResponseValidator ) => { const { axiosInstance, logger } = deps @@ -31,7 +31,7 @@ export const rotateToken = async ( export const revokeToken = async ( deps: RouteDeps, - args: ResourceRequestArgs, + args: GrantOrTokenRequestArgs, validateOpenApiResponse: ResponseValidator ) => { const { axiosInstance, logger } = deps @@ -63,9 +63,9 @@ export const createTokenRoutes = (deps: RouteDeps): TokenRoutes => { }) return { - rotate: (args: ResourceRequestArgs) => + rotate: (args: GrantOrTokenRequestArgs) => rotateToken(deps, args, rotateTokenValidator), - revoke: (args: ResourceRequestArgs) => + revoke: (args: GrantOrTokenRequestArgs) => revokeToken(deps, args, revokeTokenValidator) } } diff --git a/packages/open-payments/src/openapi/generated/auth-server-types.ts b/packages/open-payments/src/openapi/generated/auth-server-types.ts index 68c05a25..3a2e6c65 100644 --- a/packages/open-payments/src/openapi/generated/auth-server-types.ts +++ b/packages/open-payments/src/openapi/generated/auth-server-types.ts @@ -340,6 +340,12 @@ export interface external { * @description The URL of the incoming payment or ILP STREAM connection that is being paid. */ receiver: string; + /** + * Wallet Address + * Format: uri + * @description URL of a wallet address hosted by a Rafiki instance. + */ + walletAddress: string; }; }; operations: {}; 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 3a23ed59..7f4e2858 100644 --- a/packages/open-payments/src/openapi/generated/resource-server-types.ts +++ b/packages/open-payments/src/openapi/generated/resource-server-types.ts @@ -302,6 +302,8 @@ export interface components { last: number; /** @description Sub-resource identifier */ id: string; + /** @description URL of a wallet address hosted by a Rafiki instance. */ + "wallet-address": string; /** @description The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ signature: components["parameters"]["optional-signature"]; /** @description The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization". When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ @@ -350,6 +352,8 @@ export interface operations { "list-incoming-payments": { parameters: { query: { + /** URL of a wallet address hosted by a Rafiki instance. */ + "wallet-address": components["parameters"]["wallet-address"]; /** The cursor key to list from. */ cursor?: components["parameters"]["cursor"]; /** The number of items to return after the cursor. */ @@ -413,6 +417,7 @@ export interface operations { requestBody: { content: { "application/json": { + walletAddress?: external["schemas.yaml"]["components"]["schemas"]["walletAddress"]; /** @description The maximum amount that should be paid into the wallet address under this incoming payment. */ incomingAmount?: external["schemas.yaml"]["components"]["schemas"]["amount"]; /** @@ -430,6 +435,8 @@ export interface operations { "list-outgoing-payments": { parameters: { query: { + /** URL of a wallet address hosted by a Rafiki instance. */ + "wallet-address": components["parameters"]["wallet-address"]; /** The cursor key to list from. */ cursor?: components["parameters"]["cursor"]; /** The number of items to return after the cursor. */ @@ -490,6 +497,7 @@ export interface operations { requestBody: { content: { "application/json": { + walletAddress: external["schemas.yaml"]["components"]["schemas"]["walletAddress"]; /** * Format: uri * @description The URL of the quote defining this payment's amounts. @@ -532,16 +540,19 @@ export interface operations { content: { "application/json": | { + walletAddress: external["schemas.yaml"]["components"]["schemas"]["walletAddress"]; receiver: external["schemas.yaml"]["components"]["schemas"]["receiver"]; method: components["schemas"]["payment-method"]; } | { + walletAddress: external["schemas.yaml"]["components"]["schemas"]["walletAddress"]; receiver: external["schemas.yaml"]["components"]["schemas"]["receiver"]; method: components["schemas"]["payment-method"]; /** @description The fixed amount that would be paid into the receiving wallet address given a successful outgoing payment. */ receiveAmount: external["schemas.yaml"]["components"]["schemas"]["amount"]; } | { + walletAddress: external["schemas.yaml"]["components"]["schemas"]["walletAddress"]; receiver: external["schemas.yaml"]["components"]["schemas"]["receiver"]; method: components["schemas"]["payment-method"]; /** @description The fixed amount that would be sent from the sending wallet address given a successful outgoing payment. */ @@ -698,6 +709,12 @@ export interface external { * @description The URL of the incoming payment or ILP STREAM connection that is being paid. */ receiver: string; + /** + * Wallet Address + * Format: uri + * @description URL of a wallet address hosted by a Rafiki instance. + */ + walletAddress: string; }; }; operations: {}; diff --git a/packages/open-payments/src/types.ts b/packages/open-payments/src/types.ts index eedadf3a..c9495b4c 100644 --- a/packages/open-payments/src/types.ts +++ b/packages/open-payments/src/types.ts @@ -51,6 +51,7 @@ export type JWK = RSComponents['schemas']['json-web-key'] export type JWKS = RSComponents['schemas']['json-web-key-set'] export type Quote = RSComponents['schemas']['quote'] type QuoteArgsBase = { + walletAddress: RSOperations['create-quote']['requestBody']['content']['application/json']['walletAddress'] receiver: RSOperations['create-quote']['requestBody']['content']['application/json']['receiver'] method: RSComponents['schemas']['payment-method'] } diff --git a/packages/openapi/src/middleware.test.ts b/packages/openapi/src/middleware.test.ts index f7482884..cefaec06 100644 --- a/packages/openapi/src/middleware.test.ts +++ b/packages/openapi/src/middleware.test.ts @@ -37,6 +37,7 @@ export function createContext( const PATH = '/incoming-payments' const SPEC = path.resolve(__dirname, '../../../openapi/resource-server.yaml') +const WALLET_ADDRESS = 'https://openpayments.guide/alice' describe('OpenAPI Validator', (): void => { let openApi: OpenAPI @@ -75,7 +76,7 @@ describe('OpenAPI Validator', (): void => { const ctx = createContext( { headers: { Accept: 'application/json' }, - url: `${PATH}?first=${first}` + url: `${PATH}?first=${first}&wallet-address=${WALLET_ADDRESS}` }, {} ) @@ -88,7 +89,7 @@ describe('OpenAPI Validator', (): void => { const ctx = createContext( { headers: { Accept: 'application/json' }, - url: `${PATH}?first=NaN` + url: `${PATH}?first=NaN&wallet-address=${WALLET_ADDRESS}` }, {} ) @@ -145,6 +146,8 @@ describe('OpenAPI Validator', (): void => { ) addTestSignatureHeaders(ctx) ctx.request.body = body + ? { ...body, walletAddress: WALLET_ADDRESS } + : body await expect(validatePostMiddleware(ctx, next)).rejects.toMatchObject({ status: 400, message @@ -161,10 +164,12 @@ describe('OpenAPI Validator', (): void => { {} ) addTestSignatureHeaders(ctx) + ctx.request.query = { 'wallet-address': WALLET_ADDRESS } const next = jest.fn().mockImplementation(() => { expect(ctx.request.query).toEqual({ first: 10, - last: 10 + last: 10, + 'wallet-address': WALLET_ADDRESS }) ctx.response.body = {} })