diff --git a/packages/backend/migrations/20230918113102_rename_payment_pointer_tables.js b/packages/backend/migrations/20230918113102_rename_payment_pointer_tables.js index 7e4ebf8582..2a305dbd05 100644 --- a/packages/backend/migrations/20230918113102_rename_payment_pointer_tables.js +++ b/packages/backend/migrations/20230918113102_rename_payment_pointer_tables.js @@ -40,27 +40,21 @@ exports.up = function (knex) { table.dropIndex(['paymentPointerId', 'createdAt', 'id']) table.renameColumn('paymentPointerId', 'walletAddressId') table.foreign('walletAddressId').references('walletAddresses.id') - table.index(['walletAddressId']) - table.index(['createdAt']) - table.index(['id']) + table.index(['walletAddressId', 'createdAt', 'id']) }), knex.schema.alterTable('incomingPayments', function (table) { table.dropForeign(['paymentPointerId']) table.dropIndex(['paymentPointerId', 'createdAt', 'id']) table.renameColumn('paymentPointerId', 'walletAddressId') table.foreign('walletAddressId').references('walletAddresses.id') - table.index(['walletAddressId']) - table.index(['createdAt']) - table.index(['id']) + table.index(['walletAddressId', 'createdAt', 'id']) }), knex.schema.alterTable('outgoingPayments', function (table) { table.dropForeign(['paymentPointerId']) table.dropIndex(['paymentPointerId', 'createdAt', 'id']) table.renameColumn('paymentPointerId', 'walletAddressId') table.foreign('walletAddressId').references('walletAddresses.id') - table.index(['walletAddressId']) - table.index(['createdAt']) - table.index(['id']) + table.index(['walletAddressId', 'createdAt', 'id']) }), knex('webhookEvents') .update({ @@ -113,7 +107,7 @@ exports.down = function (knex) { 'ALTER INDEX "walletAddresses_pkey" RENAME TO "paymentPointers_pkey"' ), knex.raw( - 'ALTER INDEX "walletAddresses_url_index" RENAME TO "paymentPointers_url_index"' + 'ALTER INDEX "walletaddresses_url_index" RENAME TO "paymentpointers_url_index"' ), knex.raw( 'ALTER INDEX "walletaddresses_processat_index" RENAME TO "paymentpointers_processat_index"' @@ -122,11 +116,10 @@ exports.down = function (knex) { 'ALTER TABLE "paymentPointers" DROP CONSTRAINT "walletaddresses_url_unique"' ), knex.raw( - 'ALTER TABLE "paymentPointers" DROP CONSTRAINT "walletaddreses_assetid_foreign"' + 'ALTER TABLE "paymentPointers" DROP CONSTRAINT "walletaddresses_assetid_foreign"' ), knex.schema.renameTable('walletAddressKeys', 'paymentPointerKeys'), knex.schema.alterTable('paymentPointerKeys', function (table) { - table.dropForeign(['walletAddressId']) table.renameColumn('walletAddressId', 'paymentPointerId') table.foreign('paymentPointerId').references('paymentPointers.id') }), @@ -134,7 +127,7 @@ exports.down = function (knex) { 'ALTER INDEX "walletAddressKeys_pkey" RENAME TO "paymentPointerKeys_pkey"' ), knex.raw( - 'ALTER TABLE "paymentpointerKeys" DROP CONSTRAINT "walletaddresskeys_paymentpointerid_foreign"' + 'ALTER TABLE "paymentPointerKeys" DROP CONSTRAINT "walletaddresskeys_walletaddressid_foreign"' ), knex.schema.alterTable('quotes', function (table) { table.dropForeign(['walletAddressId']) diff --git a/packages/backend/package.json b/packages/backend/package.json index a88d18e432..638b99ecc7 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -53,7 +53,7 @@ "@graphql-tools/load": "^8.0.0", "@graphql-tools/schema": "^10.0.0", "@interledger/http-signature-utils": "1.1.0", - "@interledger/open-payments": "5.0.0", + "@interledger/open-payments": "5.2.0", "@interledger/openapi": "1.0.3", "@interledger/pay": "0.4.0-alpha.9", "@interledger/stream-receiver": "^0.3.3-alpha.3", diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 8b02ec391b..f8425c5d6c 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -132,10 +132,14 @@ export type HttpSigContext = AppContext & { client: string } -// Wallet address subresources export type HttpSigWithAuthenticatedStatusContext = HttpSigContext & AuthenticatedStatusContext +// Wallet address subresources +interface GetCollectionQuery { + 'wallet-address': string +} + type CollectionRequest = Omit< WalletAddressContext['request'], 'body' @@ -371,7 +375,7 @@ export class App { // POST /incoming-payments // Create incoming payment router.post>( - WALLET_ADDRESS_PATH + '/incoming-payments', + '/incoming-payments', createWalletAddressMiddleware(), createValidatorMiddleware< ContextType> @@ -389,16 +393,18 @@ export class App { // GET /incoming-payments // List incoming payments - router.get( - WALLET_ADDRESS_PATH + '/incoming-payments', + router.get< + DefaultState, + SignedCollectionContext + >( + '/incoming-payments', createWalletAddressMiddleware(), - createValidatorMiddleware>( - resourceServerSpec, - { - path: '/incoming-payments', - method: HttpMethod.GET - } - ), + createValidatorMiddleware< + ContextType> + >(resourceServerSpec, { + path: '/incoming-payments', + method: HttpMethod.GET + }), createTokenIntrospectionMiddleware({ requestType: AccessType.IncomingPayment, requestAction: RequestAction.List @@ -410,7 +416,7 @@ export class App { // POST /outgoing-payment // Create outgoing payment router.post>( - WALLET_ADDRESS_PATH + '/outgoing-payments', + '/outgoing-payments', createWalletAddressMiddleware(), createValidatorMiddleware< ContextType> @@ -428,16 +434,18 @@ export class App { // GET /outgoing-payment // List outgoing payments - router.get( - WALLET_ADDRESS_PATH + '/outgoing-payments', + router.get< + DefaultState, + SignedCollectionContext + >( + '/outgoing-payments', createWalletAddressMiddleware(), - createValidatorMiddleware>( - resourceServerSpec, - { - path: '/outgoing-payments', - method: HttpMethod.GET - } - ), + createValidatorMiddleware< + ContextType> + >(resourceServerSpec, { + path: '/outgoing-payments', + method: HttpMethod.GET + }), createTokenIntrospectionMiddleware({ requestType: AccessType.OutgoingPayment, requestAction: RequestAction.List @@ -449,7 +457,7 @@ export class App { // POST /quotes // Create quote router.post>( - WALLET_ADDRESS_PATH + '/quotes', + '/quotes', createWalletAddressMiddleware(), createValidatorMiddleware< ContextType> @@ -468,8 +476,7 @@ export class App { // GET /incoming-payments/{id} // Read incoming payment router.get( - WALLET_ADDRESS_PATH + '/incoming-payments/:id', - createWalletAddressMiddleware(), + '/incoming-payments/:id', createValidatorMiddleware< ContextType >(resourceServerSpec, { @@ -488,8 +495,7 @@ export class App { // POST /incoming-payments/{id}/complete // Complete incoming payment router.post( - WALLET_ADDRESS_PATH + '/incoming-payments/:id/complete', - createWalletAddressMiddleware(), + '/incoming-payments/:id/complete', createValidatorMiddleware>( resourceServerSpec, { @@ -508,8 +514,7 @@ export class App { // GET /outgoing-payments/{id} // Read outgoing payment router.get( - WALLET_ADDRESS_PATH + '/outgoing-payments/:id', - createWalletAddressMiddleware(), + '/outgoing-payments/:id', createValidatorMiddleware>( resourceServerSpec, { @@ -528,7 +533,7 @@ export class App { // GET /quotes/{id} // Read quote router.get( - WALLET_ADDRESS_PATH + '/quotes/:id', + '/quotes/:id', createWalletAddressMiddleware(), createValidatorMiddleware>( resourceServerSpec, 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 455cc259f0..3e536fa136 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.test.ts @@ -297,7 +297,7 @@ describe('Incoming Payment Routes', (): void => { describe('get unauthenticated incoming payment', (): void => { test('Can get incoming payment with public fields', async (): Promise => { const incomingPayment = await createIncomingPayment(deps, { - paymentPointerId: paymentPointer.id, + walletAddressId: walletAddress.id, expiresAt, incomingAmount, metadata @@ -312,7 +312,7 @@ describe('Incoming Payment Routes', (): void => { params: { id: incomingPayment.id }, - paymentPointer + walletAddress }) ctx.authenticated = false diff --git a/packages/backend/src/open_payments/payment/incoming/routes.ts b/packages/backend/src/open_payments/payment/incoming/routes.ts index 188afae0af..895843d5cb 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.ts @@ -81,7 +81,7 @@ async function getIncomingPaymentPublic( const incomingPayment = await deps.incomingPaymentService.get({ id: ctx.params.id, client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined, - paymentPointerId: ctx.paymentPointer.id + walletAddressId: ctx.walletAddress.id }) ctx.body = incomingPayment?.toPublicOpenPaymentsType() } catch (err) { @@ -105,16 +105,17 @@ async function getIncomingPaymentPrivate( } catch (err) { ctx.throw(500, 'Error trying to get incoming payment') } - if (!incomingPayment) return ctx.throw(404) + if (!incomingPayment || !incomingPayment.walletAddress) return ctx.throw(404) const connection = deps.connectionService.get(incomingPayment) ctx.body = incomingPaymentToBody( - ctx.walletAddress, + incomingPayment.walletAddress, incomingPayment, connection ) } export type CreateBody = { + walletAddress: string expiresAt?: string incomingAmount?: AmountJSON metadata?: Record @@ -146,10 +147,14 @@ async function createIncomingPayment( ) } + if (!incomingPaymentOrError.walletAddress) { + ctx.throw(404) + } + ctx.status = 201 const connection = deps.connectionService.get(incomingPaymentOrError) ctx.body = incomingPaymentToBody( - ctx.walletAddress, + incomingPaymentOrError.walletAddress, incomingPaymentOrError, connection ) @@ -174,7 +179,12 @@ async function completeIncomingPayment( errorToMessage[incomingPaymentOrError] ) } - ctx.body = incomingPaymentToBody(ctx.walletAddress, incomingPaymentOrError) + + if (!incomingPaymentOrError.walletAddress) { + ctx.throw(404) + } + + ctx.body = incomingPaymentToBody(incomingPaymentOrError.walletAddress, incomingPaymentOrError) } async function listIncomingPayments( diff --git a/packages/backend/src/open_payments/payment/incoming_remote/service.ts b/packages/backend/src/open_payments/payment/incoming_remote/service.ts index 4d38be5c0f..1eabea33a7 100644 --- a/packages/backend/src/open_payments/payment/incoming_remote/service.ts +++ b/packages/backend/src/open_payments/payment/incoming_remote/service.ts @@ -66,10 +66,11 @@ async function create( try { return await deps.openPaymentsClient.incomingPayment.create( { - walletAddress: walletAddressUrl, + url: walletAddressUrl, accessToken: grantOrError.accessToken }, { + walletAddress: walletAddressUrl, incomingAmount: args.incomingAmount ? serializeAmount(args.incomingAmount) : undefined, diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.ts b/packages/backend/src/open_payments/payment/outgoing/routes.ts index 62be310455..51ac82a9b8 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.ts @@ -47,17 +47,20 @@ async function getOutgoingPayment( try { outgoingPayment = await deps.outgoingPaymentService.get({ id: ctx.params.id, - client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined, - walletAddressId: ctx.walletAddress.id + client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined }) } catch (_) { ctx.throw(500, 'Error trying to get outgoing payment') } - if (!outgoingPayment) return ctx.throw(404) - ctx.body = outgoingPaymentToBody(ctx.walletAddress, outgoingPayment) + if (!outgoingPayment || !outgoingPayment.walletAddress) return ctx.throw(404) + ctx.body = outgoingPaymentToBody( + outgoingPayment.walletAddress, + outgoingPayment + ) } export type CreateBody = { + walletAddress: string quoteId: string metadata?: Record } diff --git a/packages/backend/src/open_payments/quote/routes.ts b/packages/backend/src/open_payments/quote/routes.ts index 816b415a72..46c23941ae 100644 --- a/packages/backend/src/open_payments/quote/routes.ts +++ b/packages/backend/src/open_payments/quote/routes.ts @@ -37,11 +37,10 @@ async function getQuote( ): Promise { const quote = await deps.quoteService.get({ id: ctx.params.id, - client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined, - walletAddressId: ctx.walletAddress.id + client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined }) - if (!quote) return ctx.throw(404) - ctx.body = quoteToBody(ctx.walletAddress, quote) + if (!quote || !quote.walletAddress) return ctx.throw(404) + ctx.body = quoteToBody(quote.walletAddress, quote) } interface CreateBodyBase { diff --git a/packages/backend/src/open_payments/wallet_address/middleware.ts b/packages/backend/src/open_payments/wallet_address/middleware.ts index c7ac829d36..c9db313b96 100644 --- a/packages/backend/src/open_payments/wallet_address/middleware.ts +++ b/packages/backend/src/open_payments/wallet_address/middleware.ts @@ -1,11 +1,28 @@ import { AppContext } from '../../app' +import { CreateBody as IncomingCreateBody } from '../../open_payments/payment/incoming/routes' +import { CreateBody as OutgoingCreateBody } from '../../open_payments/payment/outgoing/routes' + +type CreateBody = IncomingCreateBody | OutgoingCreateBody export function createWalletAddressMiddleware() { return async ( ctx: AppContext, next: () => Promise ): Promise => { - ctx.walletAddressUrl = `https://${ctx.request.host}/${ctx.params.walletAddressPath}` + if ( + ctx.path === '/incoming-payments' || + ctx.path === '/outgoing-payments' + ) { + if (ctx.method === 'GET') { + ctx.walletAddressUrl = ctx.query['wallet-address'] as string + } else if (ctx.method === 'POST') { + ctx.walletAddressUrl = (ctx.request.body as CreateBody).walletAddress + } else { + ctx.throw(401) + } + } else { + ctx.walletAddressUrl = `https://${ctx.request.host}/${ctx.params.walletAddressPath}` + } const config = await ctx.container.use('config') if (ctx.walletAddressUrl !== config.walletAddressUrl) { const walletAddressService = await ctx.container.use( diff --git a/packages/backend/src/shared/pagination.ts b/packages/backend/src/shared/pagination.ts index 8af77ea58e..205afb523f 100644 --- a/packages/backend/src/shared/pagination.ts +++ b/packages/backend/src/shared/pagination.ts @@ -1,7 +1,9 @@ -import { PaginationArgs } from '@interledger/open-payments' +import { PaginationArgs as OpenPaymentsPaginationArgs } from '@interledger/open-payments' import { BaseModel, PageInfo, Pagination } from './baseModel' +type PaginationArgs = Omit + export function parsePaginationQueryParameters({ first, last, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d286819948..a5913a954c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,8 +303,8 @@ importers: specifier: 1.1.0 version: 1.1.0 '@interledger/open-payments': - specifier: 5.0.0 - version: 5.0.0 + specifier: 5.2.0 + version: 5.2.0 '@interledger/openapi': specifier: 1.0.3 version: 1.0.3 @@ -4415,6 +4415,21 @@ packages: - supports-color dev: false + /@interledger/open-payments@5.2.0: + resolution: {integrity: sha512-Rt1ypzDkEimLky2QoJ1cTPkjr0esxGwzoj7rTLwNiT9B5SY30O+dVXhw3AYa3dK4FDwGgDQOQHJlPEHwzrXZng==} + dependencies: + '@interledger/http-signature-utils': 1.1.0 + '@interledger/openapi': 1.2.0 + axios: 1.5.1 + base64url: 3.0.1 + http-message-signatures: 0.1.2 + pino: 8.16.0 + uuid: 9.0.1 + transitivePeerDependencies: + - debug + - supports-color + dev: false + /@interledger/openapi@1.0.3: resolution: {integrity: sha512-Xx/Me2tj8DhHpQR9cVjioX/DAUmQnaXWjt8hEVPIru4pDSMHCGG0RmpU2dV+aIiVmkDM2OvJUfKoLGsPVStqrA==} dependencies: diff --git a/postman/collections/Interledger.json b/postman/collections/Interledger.json index 1ff5e31e5a..5cbb3338e2 100644 --- a/postman/collections/Interledger.json +++ b/postman/collections/Interledger.json @@ -1530,7 +1530,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"incomingAmount\": {\n \"value\": \"2000\",\n \"assetCode\": \"USD\",\n \"assetScale\": 2\n },\n \"expiresAt\": \"{{tomorrow}}\",\n \"metadata\": {\n \"description\": \"Incoming Payment on Own Account\",\n \"externalRef\": \"INV-001\"\n }\n}", + "raw": "{\n \"walletAddress\": {{pfryWalletAddress}},\n \"incomingAmount\": {\n \"value\": \"2000\",\n \"assetCode\": \"USD\",\n \"assetScale\": 2\n },\n \"expiresAt\": \"{{tomorrow}}\",\n \"metadata\": {\n \"description\": \"Incoming Payment on Own Account\",\n \"externalRef\": \"INV-001\"\n }\n}", "options": { "raw": { "language": "json" @@ -1538,10 +1538,12 @@ } }, "url": { - "raw": "{{pfryWalletAddress}}/incoming-payments", + "raw": "http://localhost:4000/incoming-payments", + "protocol": "http", "host": [ - "{{pfryWalletAddress}}" + "localhost" ], + "port": "4000", "path": [ "incoming-payments" ] @@ -1593,10 +1595,12 @@ } ], "url": { - "raw": "{{pfryWalletAddress}}/incoming-payments/{{incomingPaymentId}}", + "raw": "http://localhost:4000/incoming-payments/{{incomingPaymentId}}", + "protocol": "http", "host": [ - "{{pfryWalletAddress}}" + "localhost" ], + "port": "4000", "path": [ "incoming-payments", "{{incomingPaymentId}}" @@ -1650,10 +1654,12 @@ } ], "url": { - "raw": "{{pfryPaymentPointer}}/incoming-payments/{{incomingPaymentId}}", + "raw": "http://localhost:4000/incoming-payments/{{incomingPaymentId}}", + "protocol": "http", "host": [ - "{{pfryPaymentPointer}}" + "localhost" ], + "port": "4000", "path": [ "incoming-payments", "{{incomingPaymentId}}" @@ -1706,22 +1712,28 @@ } ], "url": { - "raw": "{{pfryWalletAddress}}/incoming-payments?first=10", + "raw": "http://localhost:4000/incoming-payments?first=10&wallet-address={{pfryWalletAddress}}", + "protocol": "http", "host": [ - "{{pfryWalletAddress}}" + "localhost" ], + "port": "4000", "path": [ "incoming-payments" ], "query": [ + { + "key": "first", + "value": "10" + }, { "key": "cursor", "value": "ea3bf38f-2719-4473-a0f7-4ba967d1d81b", "disabled": true }, { - "key": "first", - "value": "10" + "key": "wallet-address", + "value": "{{pfryWalletAddress}}" } ] } @@ -1758,11 +1770,22 @@ "value": "happy-life-bank-backend" } ], + "body": { + "mode": "raw", + "raw": "{\n \"walletAddress\": {{pfryWalletAddress}}\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{pfryWalletAddress}}/incoming-payments/{{incomingPaymentId}}/complete", + "raw": "http://localhost:4000/incoming-payments/{{incomingPaymentId}}/complete", + "protocol": "http", "host": [ - "{{pfryWalletAddress}}" + "localhost" ], + "port": "4000", "path": [ "incoming-payments", "{{incomingPaymentId}}", @@ -1826,7 +1849,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"receiver\": \"https://happy-life-bank-backend/accounts/pfry/incoming-payments/{{incomingPaymentId}}\",\n \"debitAmount\": {\n \"value\": \"1000\",\n \"assetCode\": \"USD\",\n \"assetScale\": 2\n }\n}", + "raw": "{\n \"walletAddress\": {{gfranklinWalletAddress}},\n \"receiver\": \"https://happy-life-bank-backend/accounts/pfry/incoming-payments/{{incomingPaymentId}}\",\n \"debitAmount\": {\n \"value\": \"1000\",\n \"assetCode\": \"USD\",\n \"assetScale\": 2\n }\n}", "options": { "raw": { "language": "json" @@ -1834,10 +1857,12 @@ } }, "url": { - "raw": "{{gfranklinWalletAddress}}/quotes", + "raw": "http://localhost:3000/quotes", + "protocol": "http", "host": [ - "{{gfranklinWalletAddress}}" + "localhost" ], + "port": "3000", "path": [ "quotes" ] @@ -1889,10 +1914,12 @@ } ], "url": { - "raw": "{{gfranklinWalletAddress}}/quotes/{{quoteId}}", + "raw": "http://localhost:3000/quotes/{{quoteId}}", + "protocol": "http", "host": [ - "{{gfranklinWalletAddress}}" + "localhost" ], + "port": "3000", "path": [ "quotes", "{{quoteId}}" @@ -1955,7 +1982,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"quoteId\": \"{{gfranklinWalletAddress}}/quotes/{{quoteId}}\",\n \"description\": \"yolo\",\n \"externalRef\": \"INV-001\"\n}", + "raw": "{\n \"walletAddress\": {{gfranklinWalletAddress}},\n \"quoteId\": \"{{gfranklinWalletAddress}}/quotes/{{quoteId}}\",\n \"description\": \"yolo\",\n \"externalRef\": \"INV-001\"\n}", "options": { "raw": { "language": "json" @@ -1963,10 +1990,12 @@ } }, "url": { - "raw": "{{gfranklinWalletAddress}}/outgoing-payments", + "raw": "http://localhost:3000/outgoing-payments", + "protocol": "http", "host": [ - "{{gfranklinWalletAddress}}" + "localhost" ], + "port": "3000", "path": [ "outgoing-payments" ] @@ -2018,10 +2047,12 @@ } ], "url": { - "raw": "{{gfranklinWalletAddress}}/outgoing-payments/{{outgoingPaymentId}}", + "raw": "http://localhost:3000/outgoing-payments/{{outgoingPaymentId}}", + "protocol": "http", "host": [ - "{{gfranklinWalletAddress}}" + "localhost" ], + "port": "3000", "path": [ "outgoing-payments", "{{outgoingPaymentId}}" @@ -2074,10 +2105,12 @@ } ], "url": { - "raw": "{{gfranklinWalletAddress}}/outgoing-payments?first=2", + "raw": "http://localhost:3000/outgoing-payments?first=2&wallet-address={{gfranklinWalletAddress}}", + "protocol": "http", "host": [ - "{{gfranklinWalletAddress}}" + "localhost" ], + "port": "3000", "path": [ "outgoing-payments" ], @@ -2090,6 +2123,10 @@ "key": "cursor", "value": "ea3bf38f-2719-4473-a0f7-4ba967d1d81b", "disabled": true + }, + { + "key": "wallet-address", + "value": "{{gfranklinWalletAddress}}" } ] }