diff --git a/bruno/collections/Rafiki/Examples/Open Payments Without Quote/Get receiver wallet address.bru b/bruno/collections/Rafiki/Examples/Open Payments Without Quote/Get receiver wallet address.bru index 6f6ee08f1d..5df49cfc71 100644 --- a/bruno/collections/Rafiki/Examples/Open Payments Without Quote/Get receiver wallet address.bru +++ b/bruno/collections/Rafiki/Examples/Open Payments Without Quote/Get receiver wallet address.bru @@ -37,7 +37,7 @@ script:post-response { authUrl.hostname.includes('happy-life-bank') ){ const port = authUrl.hostname.includes('cloud-nine-wallet')? authUrl.port: Number(authUrl.port) + 1000 - bru.setEnvVar("receiverOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path ); + bru.setEnvVar("receiverOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path); } else { bru.setEnvVar("receiverOpenPaymentsAuthHost", body?.authServer); } diff --git a/bruno/collections/Rafiki/Examples/Open Payments Without Quote/Get sender wallet address.bru b/bruno/collections/Rafiki/Examples/Open Payments Without Quote/Get sender wallet address.bru index 4e32ad9bdd..4ed35375d7 100644 --- a/bruno/collections/Rafiki/Examples/Open Payments Without Quote/Get sender wallet address.bru +++ b/bruno/collections/Rafiki/Examples/Open Payments Without Quote/Get sender wallet address.bru @@ -38,7 +38,7 @@ script:post-response { authUrl.hostname.includes('happy-life-bank') ){ const port = authUrl.hostname.includes('cloud-nine-wallet')? authUrl.port: Number(authUrl.port) + 1000 - bru.setEnvVar("senderOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path ); + bru.setEnvVar("senderOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path); } else { bru.setEnvVar("senderOpenPaymentsAuthHost", body?.authServer); } diff --git a/bruno/collections/Rafiki/Examples/Open Payments/Get sender wallet address.bru b/bruno/collections/Rafiki/Examples/Open Payments/Get sender wallet address.bru index 6b7c2ae061..7f52d5e87c 100644 --- a/bruno/collections/Rafiki/Examples/Open Payments/Get sender wallet address.bru +++ b/bruno/collections/Rafiki/Examples/Open Payments/Get sender wallet address.bru @@ -37,7 +37,7 @@ script:post-response { authUrl.hostname.includes('happy-life-bank') ){ const port = authUrl.hostname.includes('cloud-nine-wallet')? authUrl.port: Number(authUrl.port) + 1000 - bru.setEnvVar("senderOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path ); + bru.setEnvVar("senderOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path); } else { bru.setEnvVar("senderOpenPaymentsAuthHost", body?.authServer); } diff --git a/bruno/collections/Rafiki/Examples/Web Monetization/Get receiver wallet address.bru b/bruno/collections/Rafiki/Examples/Web Monetization/Get receiver wallet address.bru index 6fc5d5f28c..41033ba257 100644 --- a/bruno/collections/Rafiki/Examples/Web Monetization/Get receiver wallet address.bru +++ b/bruno/collections/Rafiki/Examples/Web Monetization/Get receiver wallet address.bru @@ -38,7 +38,7 @@ script:post-response { authUrl.hostname.includes('happy-life-bank') ){ const port = authUrl.hostname.includes('cloud-nine-wallet')? authUrl.port: Number(authUrl.port) + 1000 - bru.setEnvVar("receiverOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path ); + bru.setEnvVar("receiverOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path); } else { bru.setEnvVar("receiverOpenPaymentsAuthHost", body?.authServer); } diff --git a/bruno/collections/Rafiki/Examples/Web Monetization/Get sender wallet address.bru b/bruno/collections/Rafiki/Examples/Web Monetization/Get sender wallet address.bru index 6b7c2ae061..7f52d5e87c 100644 --- a/bruno/collections/Rafiki/Examples/Web Monetization/Get sender wallet address.bru +++ b/bruno/collections/Rafiki/Examples/Web Monetization/Get sender wallet address.bru @@ -37,7 +37,7 @@ script:post-response { authUrl.hostname.includes('happy-life-bank') ){ const port = authUrl.hostname.includes('cloud-nine-wallet')? authUrl.port: Number(authUrl.port) + 1000 - bru.setEnvVar("senderOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path ); + bru.setEnvVar("senderOpenPaymentsAuthHost", authUrl.protocol + '//localhost:' + port + authUrl.path); } else { bru.setEnvVar("senderOpenPaymentsAuthHost", body?.authServer); } diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 7ce6b6ce2e..57a2ac3fc1 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1010,6 +1010,8 @@ export type OutgoingPayment = BasePayment & Model & { state: OutgoingPaymentState; /** Number of attempts made to send an outgoing payment. */ stateAttempts: Scalars['Int']['output']; + /** Tenant ID of the outgoing payment. */ + tenantId?: Maybe; /** Unique identifier of the wallet address under which the outgoing payment was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -1253,6 +1255,7 @@ export type QueryOutgoingPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1263,6 +1266,7 @@ export type QueryPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1355,6 +1359,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2386,6 +2392,7 @@ export type OutgoingPaymentResolvers; state?: Resolver; stateAttempts?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2495,6 +2502,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/migrations/20241208214023_add_tenant_id_to_quote.js b/packages/backend/migrations/20241208214023_add_tenant_id_to_quote.js new file mode 100644 index 0000000000..2f16118d46 --- /dev/null +++ b/packages/backend/migrations/20241208214023_add_tenant_id_to_quote.js @@ -0,0 +1,32 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema + .alterTable('quotes', (table) => { + table.uuid('tenantId').references('tenants.id').index() + }) + .then(() => { + return knex.raw( + `UPDATE "quotes" SET "tenantId" = (SELECT id from "tenants" LIMIT 1)` + ) + }) + .then(() => { + return knex.schema.alterTable('quotes', (table) => { + table.uuid('tenantId').notNullable().alter() + }) + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return Promise.all([ + knex.schema.alterTable('quotes', function (table) { + table.dropColumn('tenantId') + }) + ]) +} diff --git a/packages/backend/migrations/20241223104532_add_tenant_id_to_outgoing_payments.js b/packages/backend/migrations/20241223104532_add_tenant_id_to_outgoing_payments.js new file mode 100644 index 0000000000..34092bbb49 --- /dev/null +++ b/packages/backend/migrations/20241223104532_add_tenant_id_to_outgoing_payments.js @@ -0,0 +1,32 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema + .alterTable('outgoingPayments', (table) => { + table.uuid('tenantId').references('tenants.id').index() + }) + .then(() => { + return knex.raw( + `UPDATE "outgoingPayments" SET "tenantId" = (SELECT id from "tenants" LIMIT 1)` + ) + }) + .then(() => { + return knex.schema.alterTable('outgoingPayments', (table) => { + table.uuid('tenantId').notNullable().alter() + }) + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return Promise.all([ + knex.schema.alterTable('outgoingPayments', function (table) { + table.dropColumn('tenantId') + }) + ]) +} diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 701cfad6ce..cec3286b87 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -550,7 +550,7 @@ export class App { // POST /outgoing-payment // Create outgoing payment router.post>( - '/outgoing-payments', + '/:tenantId/outgoing-payments', createValidatorMiddleware< ContextType> >( @@ -577,7 +577,7 @@ export class App { DefaultState, SignedCollectionContext >( - '/outgoing-payments', + '/:tenantId/outgoing-payments', createValidatorMiddleware< ContextType> >( @@ -601,7 +601,7 @@ export class App { // POST /quotes // Create quote router.post>( - '/quotes', + '/:tenantId/quotes', createValidatorMiddleware< ContextType> >( @@ -672,7 +672,7 @@ export class App { // GET /outgoing-payments/{id} // Read outgoing payment router.get( - '/outgoing-payments/:id', + '/:tenantId/outgoing-payments/:id', createValidatorMiddleware>( resourceServerSpec, { @@ -694,7 +694,7 @@ export class App { // GET /quotes/{id} // Read quote router.get( - '/quotes/:id', + '/:tenantId/quotes/:id', createValidatorMiddleware>( resourceServerSpec, { diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 47c62f1646..8926972df1 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -5517,6 +5517,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "Tenant ID of the outgoing payment.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "walletAddressId", "description": "Unique identifier of the wallet address under which the outgoing payment was created.", @@ -6728,6 +6740,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -6817,6 +6841,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -7521,6 +7557,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "tenantId", + "description": "Unique identifier of the tenant under which the quote was created.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "walletAddressId", "description": "Unique identifier of the wallet address under which the quote was created.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 7ce6b6ce2e..57a2ac3fc1 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1010,6 +1010,8 @@ export type OutgoingPayment = BasePayment & Model & { state: OutgoingPaymentState; /** Number of attempts made to send an outgoing payment. */ stateAttempts: Scalars['Int']['output']; + /** Tenant ID of the outgoing payment. */ + tenantId?: Maybe; /** Unique identifier of the wallet address under which the outgoing payment was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -1253,6 +1255,7 @@ export type QueryOutgoingPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1263,6 +1266,7 @@ export type QueryPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1355,6 +1359,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2386,6 +2392,7 @@ export type OutgoingPaymentResolvers; state?: Resolver; stateAttempts?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2495,6 +2502,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/resolvers/combined_payments.test.ts b/packages/backend/src/graphql/resolvers/combined_payments.test.ts index 869aab33d1..2f3ac34706 100644 --- a/packages/backend/src/graphql/resolvers/combined_payments.test.ts +++ b/packages/backend/src/graphql/resolvers/combined_payments.test.ts @@ -56,6 +56,7 @@ describe('Payment', (): void => { const client = 'client-test' const outgoingPayment = await createOutgoingPayment(deps, { + tenantId: Config.operatorTenantId, walletAddressId: outWalletAddressId, client: client, method: 'ilp', @@ -165,6 +166,7 @@ describe('Payment', (): void => { const client = 'client-test-type-wallet-address' const outgoingPayment = await createOutgoingPayment(deps, { + tenantId: Config.operatorTenantId, walletAddressId: outWalletAddressId, client: client, method: 'ilp', @@ -176,6 +178,7 @@ describe('Payment', (): void => { assetId: asset.id }) await createOutgoingPayment(deps, { + tenantId: Config.operatorTenantId, walletAddressId: outWalletAddressId2, client: client, method: 'ilp', diff --git a/packages/backend/src/graphql/resolvers/liquidity.test.ts b/packages/backend/src/graphql/resolvers/liquidity.test.ts index fc02751476..457099cb36 100644 --- a/packages/backend/src/graphql/resolvers/liquidity.test.ts +++ b/packages/backend/src/graphql/resolvers/liquidity.test.ts @@ -1743,13 +1743,15 @@ describe('Liquidity Resolvers', (): void => { ) describe('Event Liquidity', (): void => { + let tenantId: string let walletAddress: WalletAddress let incomingPayment: IncomingPayment let payment: OutgoingPayment beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId + tenantId }) const walletAddressId = walletAddress.id incomingPayment = await createIncomingPayment(deps, { @@ -1763,6 +1765,7 @@ describe('Liquidity Resolvers', (): void => { tenantId: Config.operatorTenantId }) payment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, method: 'ilp', receiver: `${Config.openPaymentsUrl}/incoming-payments/${uuid()}`, @@ -2176,6 +2179,7 @@ describe('Liquidity Resolvers', (): void => { tenantId: Config.operatorTenantId }) outgoingPayment = await createOutgoingPayment(deps, { + tenantId: Config.operatorTenantId, walletAddressId, method: 'ilp', receiver: `${ diff --git a/packages/backend/src/graphql/resolvers/liquidity.ts b/packages/backend/src/graphql/resolvers/liquidity.ts index e402a8f8ac..14e93fcdd0 100644 --- a/packages/backend/src/graphql/resolvers/liquidity.ts +++ b/packages/backend/src/graphql/resolvers/liquidity.ts @@ -12,7 +12,7 @@ import { OutgoingPaymentResolvers, PaymentResolvers } from '../generated/graphql' -import { ApolloContext } from '../../app' +import { ApolloContext, TenantedApolloContext } from '../../app' import { fundingErrorToMessage, fundingErrorToCode, @@ -350,7 +350,7 @@ export type DepositEventType = OutgoingPaymentDepositType const isDepositEventType = (o: any): o is DepositEventType => Object.values(DepositEventType).includes(o) -export const depositEventLiquidity: MutationResolvers['depositEventLiquidity'] = +export const depositEventLiquidity: MutationResolvers['depositEventLiquidity'] = async ( parent, args, @@ -377,6 +377,7 @@ export const depositEventLiquidity: MutationResolvers['depositEve ) const paymentOrErr = await outgoingPaymentService.fund({ id: event.data.id, + tenantId: ctx.tenant.id, amount: BigInt(event.data.debitAmount.value), transferId: event.id }) @@ -434,7 +435,7 @@ export const withdrawEventLiquidity: MutationResolvers['withdrawE } } -export const depositOutgoingPaymentLiquidity: MutationResolvers['depositOutgoingPaymentLiquidity'] = +export const depositOutgoingPaymentLiquidity: MutationResolvers['depositOutgoingPaymentLiquidity'] = async ( parent, args, @@ -478,6 +479,7 @@ export const depositOutgoingPaymentLiquidity: MutationResolvers[' }) const paymentOrErr = await outgoingPaymentService.fund({ id: outgoingPaymentId, + tenantId: ctx.tenant.id, amount: BigInt(event.data.debitAmount.value), transferId: event.id }) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index 27d4aa8730..a4698992dc 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -68,6 +68,7 @@ describe('OutgoingPayment Resolvers', (): void => { const createPayment = async ( options: { + tenantId: string walletAddressId: string metadata?: Record }, @@ -94,12 +95,14 @@ describe('OutgoingPayment Resolvers', (): void => { describe('Query.outgoingPayment', (): void => { let payment: OutgoingPaymentModel + let tenantId: string let walletAddressId: string beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId walletAddressId = ( await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) ).id @@ -109,6 +112,7 @@ describe('OutgoingPayment Resolvers', (): void => { getClient: () => appContainer.apolloClient, createModel: () => createPayment({ + tenantId, walletAddressId }), pagedQuery: 'outgoingPayments' @@ -136,16 +140,16 @@ describe('OutgoingPayment Resolvers', (): void => { beforeEach(async (): Promise => { const firstReceiverWalletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) const secondWalletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) const secondReceiverWalletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) @@ -165,6 +169,7 @@ describe('OutgoingPayment Resolvers', (): void => { }) receiver = incomingPayment.getUrl(firstReceiverWalletAddress) firstOutgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, receiver, method: 'ilp', @@ -177,6 +182,7 @@ describe('OutgoingPayment Resolvers', (): void => { }) const secondReceiver = secondIncomingPayment.getUrl(secondWalletAddress) secondOutgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: secondWalletAddress.id, receiver: secondReceiver, method: 'ilp', @@ -332,11 +338,14 @@ describe('OutgoingPayment Resolvers', (): void => { const grantId = uuid() const { id: walletAddressId } = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) - const payment = await createPayment({ walletAddressId }, grantId) + const payment = await createPayment( + { tenantId, walletAddressId }, + grantId + ) const query = await appContainer.apolloClient .query({ @@ -372,10 +381,10 @@ describe('OutgoingPayment Resolvers', (): void => { beforeEach(async (): Promise => { const { id: walletAddressId } = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) - payment = await createPayment({ walletAddressId, metadata }) + payment = await createPayment({ tenantId, walletAddressId, metadata }) }) // Query with each payment state with and without an error @@ -554,17 +563,26 @@ describe('OutgoingPayment Resolvers', (): void => { }) describe('Mutation.createOutgoingPayment', (): void => { + let tenantId: string const metadata = { description: 'rent', externalRef: '202201' } + beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId + }) + test('success (metadata)', async (): Promise => { const { id: walletAddressId } = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) - const payment = await createPayment({ walletAddressId, metadata }) + const payment = await createPayment({ + tenantId, + walletAddressId, + metadata + }) const createSpy = jest .spyOn(outgoingPaymentService, 'create') @@ -595,7 +613,7 @@ describe('OutgoingPayment Resolvers', (): void => { (query): OutgoingPaymentResponse => query.data?.createOutgoingPayment ) - expect(createSpy).toHaveBeenCalledWith(input) + expect(createSpy).toHaveBeenCalledWith({ ...input, tenantId }) expect(query.payment?.id).toBe(payment.id) expect(query.payment?.state).toBe(SchemaPaymentState.Funding) }) @@ -643,7 +661,7 @@ describe('OutgoingPayment Resolvers', (): void => { }) ) } - expect(createSpy).toHaveBeenCalledWith(input) + expect(createSpy).toHaveBeenCalledWith({ ...input, tenantId }) }) test('internal server error', async (): Promise => { @@ -689,19 +707,27 @@ describe('OutgoingPayment Resolvers', (): void => { }) ) } - expect(createSpy).toHaveBeenCalledWith(input) + expect(createSpy).toHaveBeenCalledWith({ ...input, tenantId }) }) }) describe('Mutation.createOutgoingPaymentFromIncomingPayment', (): void => { + let tenantId: string const mockIncomingPaymentUrl = `https://${faker.internet.domainName()}/incoming-payments/${uuid()}` + beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId + }) + test('create', async (): Promise => { const walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) - const payment = await createPayment({ walletAddressId: walletAddress.id }) + const payment = await createPayment({ + tenantId, + walletAddressId: walletAddress.id + }) const createSpy = jest .spyOn(outgoingPaymentService, 'create') @@ -738,7 +764,7 @@ describe('OutgoingPayment Resolvers', (): void => { query.data?.createOutgoingPaymentFromIncomingPayment ) - expect(createSpy).toHaveBeenCalledWith(input) + expect(createSpy).toHaveBeenCalledWith({ ...input, tenantId }) expect(query.payment?.id).toBe(payment.id) expect(query.payment?.state).toBe(SchemaPaymentState.Funding) }) @@ -791,7 +817,7 @@ describe('OutgoingPayment Resolvers', (): void => { }) ) } - expect(createSpy).toHaveBeenCalledWith(input) + expect(createSpy).toHaveBeenCalledWith({ ...input, tenantId }) }) test('unknown error', async (): Promise => { @@ -842,19 +868,23 @@ describe('OutgoingPayment Resolvers', (): void => { }) ) } - expect(createSpy).toHaveBeenCalledWith(input) + expect(createSpy).toHaveBeenCalledWith({ ...input, tenantId }) }) }) describe('Mutation.cancelOutgoingPayment', (): void => { let payment: OutgoingPaymentModel beforeEach(async () => { + const tenantId = Config.operatorTenantId const walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) - payment = await createPayment({ walletAddressId: walletAddress.id }) + payment = await createPayment({ + tenantId, + walletAddressId: walletAddress.id + }) }) const reasons: (string | undefined)[] = [undefined, 'Not enough balance'] @@ -897,7 +927,10 @@ describe('OutgoingPayment Resolvers', (): void => { query.data?.cancelOutgoingPayment ) - expect(cancelSpy).toHaveBeenCalledWith(input) + expect(cancelSpy).toHaveBeenCalledWith({ + ...input, + tenantId: payment.quote.tenantId + }) expect(mutationResponse.payment).toEqual({ __typename: 'OutgoingPayment', id: input.id, @@ -956,18 +989,23 @@ describe('OutgoingPayment Resolvers', (): void => { }) ) } - expect(cancelSpy).toHaveBeenCalledWith(input) + expect(cancelSpy).toHaveBeenCalledWith({ + ...input, + tenantId: payment.quote.tenantId + }) } ) }) describe('Wallet address outgoingPayments', (): void => { let walletAddressId: string + let tenantId: string beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId walletAddressId = ( await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) ).id @@ -977,6 +1015,7 @@ describe('OutgoingPayment Resolvers', (): void => { getClient: () => appContainer.apolloClient, createModel: () => createPayment({ + tenantId, walletAddressId }), pagedQuery: 'outgoingPayments', diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.ts index a9cdcb4403..7eb85b86ee 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.ts @@ -12,19 +12,20 @@ import { errorToCode } from '../../open_payments/payment/outgoing/errors' import { OutgoingPayment } from '../../open_payments/payment/outgoing/model' -import { ApolloContext } from '../../app' +import { TenantedApolloContext } from '../../app' import { getPageInfo } from '../../shared/pagination' import { Pagination, SortOrder } from '../../shared/baseModel' import { GraphQLError } from 'graphql' import { GraphQLErrorCode } from '../errors' -export const getOutgoingPayment: QueryResolvers['outgoingPayment'] = +export const getOutgoingPayment: QueryResolvers['outgoingPayment'] = async (parent, args, ctx): Promise => { const outgoingPaymentService = await ctx.container.use( 'outgoingPaymentService' ) const payment = await outgoingPaymentService.get({ - id: args.id + id: args.id, + tenantId: ctx.isOperator ? undefined : ctx.tenant.id }) if (!payment) { throw new GraphQLError('payment does not exist', { @@ -36,7 +37,7 @@ export const getOutgoingPayment: QueryResolvers['outgoingPayment' return paymentToGraphql(payment) } -export const getOutgoingPayments: QueryResolvers['outgoingPayments'] = +export const getOutgoingPayments: QueryResolvers['outgoingPayments'] = async ( parent, args, @@ -45,10 +46,11 @@ export const getOutgoingPayments: QueryResolvers['outgoingPayment const outgoingPaymentService = await ctx.container.use( 'outgoingPaymentService' ) - const { filter, sortOrder, ...pagination } = args + const { tenantId, filter, sortOrder, ...pagination } = args const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc const getPageFn = (pagination_: Pagination, sortOrder_?: SortOrder) => outgoingPaymentService.getPage({ + tenantId: ctx.isOperator ? tenantId : ctx.tenant.id, pagination: pagination_, filter, sortOrder: sortOrder_ @@ -71,7 +73,7 @@ export const getOutgoingPayments: QueryResolvers['outgoingPayment } } -export const cancelOutgoingPayment: MutationResolvers['cancelOutgoingPayment'] = +export const cancelOutgoingPayment: MutationResolvers['cancelOutgoingPayment'] = async ( parent, args, @@ -81,9 +83,21 @@ export const cancelOutgoingPayment: MutationResolvers['cancelOutg 'outgoingPaymentService' ) - const outgoingPaymentOrError = await outgoingPaymentService.cancel( - args.input - ) + const tenantId = ctx.tenant.id + if (!tenantId) + throw new GraphQLError( + `Assignment to the specified tenant is not permitted`, + { + extensions: { + code: GraphQLErrorCode.BadUserInput + } + } + ) + + const outgoingPaymentOrError = await outgoingPaymentService.cancel({ + tenantId, + ...args.input + }) if (isOutgoingPaymentError(outgoingPaymentOrError)) { throw new GraphQLError(errorToMessage[outgoingPaymentOrError], { @@ -98,7 +112,7 @@ export const cancelOutgoingPayment: MutationResolvers['cancelOutg } } -export const createOutgoingPayment: MutationResolvers['createOutgoingPayment'] = +export const createOutgoingPayment: MutationResolvers['createOutgoingPayment'] = async ( parent, args, @@ -107,9 +121,21 @@ export const createOutgoingPayment: MutationResolvers['createOutg const outgoingPaymentService = await ctx.container.use( 'outgoingPaymentService' ) - const outgoingPaymentOrError = await outgoingPaymentService.create( - args.input - ) + + const tenantId = ctx.tenant.id + if (!tenantId) + throw new GraphQLError( + `Assignment to the specified tenant is not permitted`, + { + extensions: { + code: GraphQLErrorCode.BadUserInput + } + } + ) + const outgoingPaymentOrError = await outgoingPaymentService.create({ + tenantId, + ...args.input + }) if (isOutgoingPaymentError(outgoingPaymentOrError)) { throw new GraphQLError(errorToMessage[outgoingPaymentOrError], { extensions: { @@ -122,7 +148,7 @@ export const createOutgoingPayment: MutationResolvers['createOutg } } -export const createOutgoingPaymentFromIncomingPayment: MutationResolvers['createOutgoingPaymentFromIncomingPayment'] = +export const createOutgoingPaymentFromIncomingPayment: MutationResolvers['createOutgoingPaymentFromIncomingPayment'] = async ( parent, args, @@ -131,10 +157,20 @@ export const createOutgoingPaymentFromIncomingPayment: MutationResolvers['outgoingPayments'] = +export const getWalletAddressOutgoingPayments: WalletAddressResolvers['outgoingPayments'] = async ( parent, args, @@ -167,17 +203,20 @@ export const getWalletAddressOutgoingPayments: WalletAddressResolvers outgoingPaymentService.getWalletAddressPage({ walletAddressId: parent.id as string, pagination, - sortOrder + sortOrder, + tenantId }), page: outgoingPayments, sortOrder: order @@ -208,6 +247,7 @@ export function paymentToGraphql( metadata: payment.metadata, createdAt: new Date(+payment.createdAt).toISOString(), quote: quoteToGraphql(payment.quote), - grantId: payment.grantId + grantId: payment.grantId, + tenantId: payment.tenantId } } diff --git a/packages/backend/src/graphql/resolvers/quote.test.ts b/packages/backend/src/graphql/resolvers/quote.test.ts index 5132b5292b..246f7ac486 100644 --- a/packages/backend/src/graphql/resolvers/quote.test.ts +++ b/packages/backend/src/graphql/resolvers/quote.test.ts @@ -28,6 +28,7 @@ describe('Quote Resolvers', (): void => { let appContainer: TestContainer let quoteService: QuoteService let asset: Asset + let tenantId: string const receivingWalletAddress = 'http://wallet2.example/bob' const receiver = `${receivingWalletAddress}/incoming-payments/${uuid()}` @@ -39,6 +40,7 @@ describe('Quote Resolvers', (): void => { }) beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId asset = await createAsset(deps) }) @@ -56,6 +58,7 @@ describe('Quote Resolvers', (): void => { walletAddressId: string ): Promise => { return await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount: { @@ -71,7 +74,7 @@ describe('Quote Resolvers', (): void => { describe('Query.quote', (): void => { test('success', async (): Promise => { const { id: walletAddressId } = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) const quote = await createWalletAddressQuote(walletAddressId) @@ -140,7 +143,9 @@ describe('Quote Resolvers', (): void => { } } `, - variables: { quoteId: uuid() } + variables: { + quoteId: uuid() + } }) } catch (error) { expect(error).toBeInstanceOf(ApolloError) @@ -190,7 +195,7 @@ describe('Quote Resolvers', (): void => { `('$type', async ({ withAmount, receiveAmount }): Promise => { const amount = withAmount ? debitAmount : undefined const { id: walletAddressId } = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) const input = { @@ -206,6 +211,7 @@ describe('Quote Resolvers', (): void => { .mockImplementationOnce(async (opts) => { quote = await createQuote(deps, { ...opts, + tenantId, validDestination: false }) return quote @@ -226,7 +232,11 @@ describe('Quote Resolvers', (): void => { }) .then((query): QuoteResponse => query.data?.createQuote) - expect(createSpy).toHaveBeenCalledWith({ ...input, method: 'ilp' }) + expect(createSpy).toHaveBeenCalledWith({ + ...input, + tenantId, + method: 'ilp' + }) expect(query.quote?.id).toBe(quote?.id) }) @@ -292,7 +302,11 @@ describe('Quote Resolvers', (): void => { }) ) } - expect(createSpy).toHaveBeenCalledWith({ ...input, method: 'ilp' }) + expect(createSpy).toHaveBeenCalledWith({ + ...input, + tenantId, + method: 'ilp' + }) }) }) @@ -302,7 +316,7 @@ describe('Quote Resolvers', (): void => { beforeEach(async (): Promise => { walletAddressId = ( await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) ).id diff --git a/packages/backend/src/graphql/resolvers/quote.ts b/packages/backend/src/graphql/resolvers/quote.ts index 16bd2863e1..f506cf221b 100644 --- a/packages/backend/src/graphql/resolvers/quote.ts +++ b/packages/backend/src/graphql/resolvers/quote.ts @@ -11,21 +11,22 @@ import { errorToMessage } from '../../open_payments/quote/errors' import { Quote } from '../../open_payments/quote/model' -import { ApolloContext } from '../../app' +import { TenantedApolloContext } from '../../app' import { getPageInfo } from '../../shared/pagination' import { Pagination, SortOrder } from '../../shared/baseModel' import { CreateQuoteOptions } from '../../open_payments/quote/service' import { GraphQLError } from 'graphql' import { GraphQLErrorCode } from '../errors' -export const getQuote: QueryResolvers['quote'] = async ( +export const getQuote: QueryResolvers['quote'] = async ( parent, args, ctx ): Promise => { const quoteService = await ctx.container.use('quoteService') const quote = await quoteService.get({ - id: args.id + id: args.id, + tenantId: ctx.tenant.id }) if (!quote) { throw new GraphQLError('quote does not exist', { @@ -37,10 +38,21 @@ export const getQuote: QueryResolvers['quote'] = async ( return quoteToGraphql(quote) } -export const createQuote: MutationResolvers['createQuote'] = +export const createQuote: MutationResolvers['createQuote'] = async (parent, args, ctx): Promise => { const quoteService = await ctx.container.use('quoteService') + const tenantId = ctx.tenant.id + if (!tenantId) + throw new GraphQLError( + `Assignment to the specified tenant is not permitted`, + { + extensions: { + code: GraphQLErrorCode.BadUserInput + } + } + ) const options: CreateQuoteOptions = { + tenantId, walletAddressId: args.input.walletAddressId, receiver: args.input.receiver, method: 'ilp' @@ -61,7 +73,7 @@ export const createQuote: MutationResolvers['createQuote'] = } } -export const getWalletAddressQuotes: WalletAddressResolvers['quotes'] = +export const getWalletAddressQuotes: WalletAddressResolvers['quotes'] = async (parent, args, ctx): Promise => { if (!parent.id) { throw new GraphQLError('missing wallet address id', { @@ -73,17 +85,20 @@ export const getWalletAddressQuotes: WalletAddressResolvers['quot const quoteService = await ctx.container.use('quoteService') const { sortOrder, ...pagination } = args const order = sortOrder === 'ASC' ? SortOrder.Asc : SortOrder.Desc + const tenantId = ctx.isOperator ? undefined : ctx.tenant.id const quotes = await quoteService.getWalletAddressPage({ walletAddressId: parent.id, pagination, - sortOrder: order + sortOrder: order, + tenantId }) const pageInfo = await getPageInfo({ getPage: (pagination: Pagination, sortOrder?: SortOrder) => quoteService.getWalletAddressPage({ walletAddressId: parent.id as string, pagination, - sortOrder + sortOrder, + tenantId }), page: quotes, sortOrder: order @@ -100,6 +115,7 @@ export const getWalletAddressQuotes: WalletAddressResolvers['quot export function quoteToGraphql(quote: Quote): SchemaQuote { return { id: quote.id, + tenantId: quote.tenantId, walletAddressId: quote.walletAddressId, receiver: quote.receiver, debitAmount: quote.debitAmount, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 69eab4330e..4f7543f027 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -97,6 +97,8 @@ type Query { sortOrder: SortOrder "Filter outgoing payments based on specific criteria such as receiver, wallet address ID, or state." filter: OutgoingPaymentFilter + "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature." + tenantId: String ): OutgoingPaymentConnection! "Fetch an Open Payments incoming payment by its ID." @@ -135,6 +137,8 @@ type Query { sortOrder: SortOrder "Filter payment events based on specific criteria such as payment type or wallet address ID." filter: PaymentFilter + "Unique identifier of the tenant associated with the wallet address. Optional, if not provided, the tenantId will be obtained from the signature." + tenantId: String ): PaymentConnection! "Fetch a paginated list of accounting transfers for a given account." @@ -1021,6 +1025,8 @@ type OutgoingPayment implements BasePayment & Model { createdAt: String! "Unique identifier of the grant under which the outgoing payment was created." grantId: String + "Tenant ID of the outgoing payment." + tenantId: String } enum OutgoingPaymentState { @@ -1144,6 +1150,8 @@ type QuoteEdge { type Quote { "Unique identifier of the quote." id: ID! + "Unique identifier of the tenant under which the quote was created." + tenantId: ID! "Unique identifier of the wallet address under which the quote was created." walletAddressId: ID! "Wallet address URL of the receiver." diff --git a/packages/backend/src/open_payments/payment/combined/service.test.ts b/packages/backend/src/open_payments/payment/combined/service.test.ts index 6e6d663eab..60251049ec 100644 --- a/packages/backend/src/open_payments/payment/combined/service.test.ts +++ b/packages/backend/src/open_payments/payment/combined/service.test.ts @@ -27,6 +27,7 @@ describe('Combined Payment Service', (): void => { let appContainer: TestContainer let knex: Knex let combinedPaymentService: CombinedPaymentService + let tenantId: string let sendAsset: Asset let sendWalletAddressId: string let receiveAsset: Asset @@ -37,6 +38,7 @@ describe('Combined Payment Service', (): void => { appContainer = await createTestApp(deps) knex = appContainer.knex combinedPaymentService = await deps.use('combinedPaymentService') + tenantId = Config.operatorTenantId }) beforeEach(async (): Promise => { @@ -70,6 +72,7 @@ describe('Combined Payment Service', (): void => { const receiverUrl = incomingPayment.getUrl(receiveWalletAddress) const outgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: sendWalletAddressId, method: 'ilp', receiver: receiverUrl, diff --git a/packages/backend/src/open_payments/payment/outgoing/model.ts b/packages/backend/src/open_payments/payment/outgoing/model.ts index 130e9391b6..302d8ba72e 100644 --- a/packages/backend/src/open_payments/payment/outgoing/model.ts +++ b/packages/backend/src/open_payments/payment/outgoing/model.ts @@ -15,6 +15,7 @@ import { OutgoingPayment as OpenPaymentsOutgoingPayment, OutgoingPaymentWithSpentAmounts } from '@interledger/open-payments' +import { Tenant } from '../../../tenants/model' export class OutgoingPaymentGrant extends DbErrors(Model) { public static get modelPaths(): string[] { @@ -108,7 +109,7 @@ export class OutgoingPayment public getUrl(walletAddress: WalletAddress): string { const url = new URL(walletAddress.url) - return `${url.origin}${OutgoingPayment.urlPath}/${this.id}` + return `${url.origin}/${this.tenantId}${OutgoingPayment.urlPath}/${this.id}` } public get asset(): Asset { @@ -125,6 +126,8 @@ export class OutgoingPayment // Outgoing peer public peerId?: string + public tenantId!: string + static get relationMappings() { return { ...super.relationMappings, @@ -135,6 +138,14 @@ export class OutgoingPayment from: 'outgoingPayments.id', to: 'quotes.id' } + }, + tenant: { + relation: Model.BelongsToOneRelation, + modelClass: Tenant, + join: { + from: 'outgoingPayments.tenantId', + to: 'tenants.id' + } } } } 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 270586fc22..98a744f1e3 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts @@ -41,6 +41,7 @@ describe('Outgoing Payment Routes', (): void => { let outgoingPaymentService: OutgoingPaymentService let walletAddress: WalletAddress let baseUrl: string + let tenantId: string const receivingWalletAddress = `https://wallet.example/${uuid()}` @@ -51,6 +52,7 @@ describe('Outgoing Payment Routes', (): void => { }): Promise => { return await createOutgoingPayment(deps, { ...options, + tenantId: Config.operatorTenantId, walletAddressId: walletAddress.id, method: 'ilp', receiver: `${receivingWalletAddress}/incoming-payments/${uuid()}`, @@ -77,8 +79,9 @@ describe('Outgoing Payment Routes', (): void => { beforeEach(async (): Promise => { const asset = await createAsset(deps) + tenantId = Config.operatorTenantId walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) baseUrl = new URL(walletAddress.url).origin @@ -117,7 +120,7 @@ describe('Outgoing Payment Routes', (): void => { get: (ctx) => outgoingPaymentRoutes.get(ctx), getBody: (outgoingPayment) => { return { - id: `${baseUrl}/outgoing-payments/${outgoingPayment.id}`, + id: `${baseUrl}/${tenantId}/outgoing-payments/${outgoingPayment.id}`, walletAddress: walletAddress.url, receiver: outgoingPayment.receiver, quoteId: outgoingPayment.quote.getUrl(walletAddress), @@ -137,7 +140,7 @@ describe('Outgoing Payment Routes', (): void => { type SetupContextOptions = UnionOmit< CreateOutgoingPaymentOptions, - 'walletAddressId' + 'walletAddressId' | 'tenantId' > describe('create', (): void => { @@ -152,6 +155,9 @@ describe('Outgoing Payment Routes', (): void => { url: `/outgoing-payments`, body: options }, + params: { + tenantId + }, walletAddress, client: options.client, grant: options.grant @@ -185,6 +191,7 @@ describe('Outgoing Payment Routes', (): void => { CreateOutgoingPaymentBaseOptions, 'walletAddressId' > = { + tenantId, client, grant, metadata @@ -192,7 +199,7 @@ describe('Outgoing Payment Routes', (): void => { if (createFrom === CreateFrom.Quote) { options = { ...options, - quoteId: `${baseUrl}/quotes/${payment.quote.id}` + quoteId: `${baseUrl}/${payment.quote.tenantId}/quotes/${payment.quote.id}` } as CreateFromQuote } else { assert(createFrom === CreateFrom.IncomingPayment) @@ -215,6 +222,7 @@ describe('Outgoing Payment Routes', (): void => { ).resolves.toBeUndefined() let expectedCreateOptions: CreateOutgoingPaymentBaseOptions = { + tenantId, walletAddressId: walletAddress.id, metadata, client, @@ -243,7 +251,7 @@ describe('Outgoing Payment Routes', (): void => { .split('/') .pop() expect(ctx.response.body).toEqual({ - id: `${baseUrl}/outgoing-payments/${outgoingPaymentId}`, + id: `${baseUrl}/${tenantId}/outgoing-payments/${outgoingPaymentId}`, walletAddress: walletAddress.url, receiver: payment.receiver, quoteId: @@ -284,8 +292,9 @@ describe('Outgoing Payment Routes', (): void => { 'returns error on %s', async (error): Promise => { const quoteId = uuid() + const tenantId = Config.operatorTenantId const ctx = setup({ - quoteId: `${baseUrl}/quotes/${quoteId}` + quoteId: `${baseUrl}/${tenantId}/quotes/${quoteId}` }) const createSpy = jest .spyOn(outgoingPaymentService, 'create') @@ -303,7 +312,8 @@ describe('Outgoing Payment Routes', (): void => { expect(createSpy).toHaveBeenCalledWith({ walletAddressId: walletAddress.id, - quoteId + quoteId, + tenantId }) } ) diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.ts b/packages/backend/src/open_payments/payment/outgoing/routes.ts index e39e0c3baf..5ba4644fa7 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.ts @@ -54,6 +54,7 @@ async function getOutgoingPayment( ): Promise { const outgoingPayment = await deps.outgoingPaymentService.get({ id: ctx.params.id, + tenantId: ctx.params.tenantId, client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined }) @@ -98,6 +99,7 @@ async function createOutgoingPayment( ): Promise { const { body } = ctx.request const baseOptions: OutgoingPaymentCreateBaseOptions = { + tenantId: ctx.params.tenantId, walletAddressId: ctx.walletAddress.id, metadata: body.metadata, client: ctx.client, @@ -148,7 +150,13 @@ async function listOutgoingPayments( ): Promise { await listSubresource({ ctx, - getWalletAddressPage: deps.outgoingPaymentService.getWalletAddressPage, + getWalletAddressPage: async ({ walletAddressId, pagination, client }) => + deps.outgoingPaymentService.getWalletAddressPage({ + walletAddressId, + pagination, + client, + tenantId: ctx.params.tenantId + }), toBody: (payment) => outgoingPaymentToBody(ctx.walletAddress, payment) }) } 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 667009087e..05007973d3 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -54,6 +54,7 @@ import { TelemetryService } from '../../../telemetry/service' import { getPageTests } from '../../../shared/baseModel.test' import { Pagination, SortOrder } from '../../../shared/baseModel' import { ReceiverService } from '../../receiver/service' +import { WalletAddressService } from '../../wallet_address/service' describe('OutgoingPaymentService', (): void => { let deps: IocContract @@ -62,9 +63,11 @@ describe('OutgoingPaymentService', (): void => { let accountingService: AccountingService let paymentMethodHandlerService: PaymentMethodHandlerService let quoteService: QuoteService + let walletAddressService: WalletAddressService let telemetryService: TelemetryService let knex: Knex let assetId: string + let tenantId: string let walletAddressId: string let incomingPayment: IncomingPayment let receiverWalletAddress: MockWalletAddress @@ -262,6 +265,7 @@ describe('OutgoingPaymentService', (): void => { accountingService = await deps.use('accountingService') paymentMethodHandlerService = await deps.use('paymentMethodHandlerService') quoteService = await deps.use('quoteService') + walletAddressService = await deps.use('walletAddressService') telemetryService = (await deps.use('telemetry'))! config = await deps.use('config') knex = appContainer.knex @@ -269,17 +273,18 @@ describe('OutgoingPaymentService', (): void => { }) beforeEach(async (): Promise => { + tenantId = config.operatorTenantId const { id: sendAssetId } = await createAsset(deps, asset) assetId = sendAssetId const walletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId: sendAssetId }) walletAddressId = walletAddress.id client = walletAddress.url const { id: destinationAssetId } = await createAsset(deps, destinationAsset) receiverWalletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId: destinationAssetId, mockServerPort: appContainer.openPaymentsPort }) @@ -330,6 +335,7 @@ describe('OutgoingPaymentService', (): void => { getTests({ createModel: ({ client }) => createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -345,6 +351,7 @@ describe('OutgoingPaymentService', (): void => { describe('get', (): void => { test('throws error if cannot find liquidity account for SENDING payment', async () => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -352,6 +359,7 @@ describe('OutgoingPaymentService', (): void => { }) const payment = await outgoingPaymentService.create({ + tenantId, walletAddressId, quoteId: quote.id, client @@ -366,6 +374,7 @@ describe('OutgoingPaymentService', (): void => { ).resolves.toEqual(payment) await expect( outgoingPaymentService.fund({ + tenantId, id: payment.id, amount: payment.debitAmount.value, transferId: uuid() @@ -389,6 +398,7 @@ describe('OutgoingPaymentService', (): void => { getPageTests({ createModel: () => createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -412,11 +422,11 @@ describe('OutgoingPaymentService', (): void => { let otherOutgoingPayment: OutgoingPayment beforeEach(async (): Promise => { otherSenderWalletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId }) otherReceiverWalletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId }) const incomingPayment = await createIncomingPayment(deps, { @@ -426,6 +436,7 @@ describe('OutgoingPaymentService', (): void => { otherReceiver = incomingPayment.getUrl(otherReceiverWalletAddress) outgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -439,6 +450,7 @@ describe('OutgoingPaymentService', (): void => { }) otherOutgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: otherSenderWalletAddress.id, client, receiver: otherReceiver, @@ -512,6 +524,7 @@ describe('OutgoingPaymentService', (): void => { describe('getWalletAddressPage', (): void => { test('throws error if cannot find liquidity account for SENDING payment', async () => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -519,6 +532,7 @@ describe('OutgoingPaymentService', (): void => { }) const payment = await outgoingPaymentService.create({ + tenantId, walletAddressId, client, quoteId: quote.id @@ -534,6 +548,7 @@ describe('OutgoingPaymentService', (): void => { await expect( outgoingPaymentService.fund({ id: payment.id, + tenantId, amount: payment.debitAmount.value, transferId: uuid() }) @@ -585,6 +600,7 @@ describe('OutgoingPaymentService', (): void => { * 4. Based on state, check the result */ const outgoingPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -601,6 +617,7 @@ describe('OutgoingPaymentService', (): void => { const response = await outgoingPaymentService.cancel({ id: outgoingPayment.id, + tenantId, reason }) @@ -639,6 +656,7 @@ describe('OutgoingPaymentService', (): void => { const quoteSpy = jest.spyOn(quoteService, 'create') const payment = await outgoingPaymentService.create({ + tenantId, walletAddressId, debitAmount, incomingPayment: incomingPaymentUrl @@ -646,6 +664,7 @@ describe('OutgoingPaymentService', (): void => { expect(!isOutgoingPaymentError(payment)).toBeTruthy() expect(quoteSpy).toHaveBeenCalledWith({ + tenantId, walletAddressId, receiver: incomingPaymentUrl, debitAmount, @@ -679,6 +698,7 @@ describe('OutgoingPaymentService', (): void => { }) const options: CreateOutgoingPaymentOptions = { + tenantId, walletAddressId: receiverWalletAddress.id, debitAmount, incomingPayment: incomingPayment.toOpenPaymentsTypeWithMethods( @@ -724,6 +744,7 @@ describe('OutgoingPaymentService', (): void => { }) const options: CreateOutgoingPaymentOptions = { + tenantId, walletAddressId: receiverWalletAddress.id, debitAmount, incomingPayment: incomingPayment.toOpenPaymentsTypeWithMethods( @@ -761,6 +782,7 @@ describe('OutgoingPaymentService', (): void => { .mockImplementationOnce(async () => quoteCreateResponse) const payment = await outgoingPaymentService.create({ + tenantId, walletAddressId, debitAmount, incomingPayment: incomingPaymentUrl @@ -769,6 +791,7 @@ describe('OutgoingPaymentService', (): void => { expect(isOutgoingPaymentError(payment)).toBeTruthy() expect(payment).toBe(quoteCreateResponse) expect(quoteSpy).toHaveBeenCalledWith({ + tenantId, walletAddressId, receiver: incomingPaymentUrl, debitAmount, @@ -810,12 +833,14 @@ describe('OutgoingPaymentService', (): void => { const peerService = await deps.use('peerService') const peer = await createPeer(deps) const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, method: 'ilp' }) const options = { + tenantId, walletAddressId, client, quoteId: quote.id, @@ -873,23 +898,59 @@ describe('OutgoingPaymentService', (): void => { it('fails to create on unknown wallet address', async () => { const { id: quoteId } = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, validDestination: false, method: 'ilp' }) + const unknownWalletAddressId = uuid() + jest.spyOn(walletAddressService, 'get').mockResolvedValueOnce(undefined) await expect( outgoingPaymentService.create({ - walletAddressId: uuid(), + tenantId, + walletAddressId: unknownWalletAddressId, quoteId }) ).resolves.toEqual(OutgoingPaymentError.UnknownWalletAddress) + expect(walletAddressService.get).toHaveBeenCalledTimes(1) + expect(walletAddressService.get).toHaveBeenCalledWith( + unknownWalletAddressId, + tenantId + ) + }) + + it('fails to create on unknown tenant id', async () => { + const { id: quoteId } = await createQuote(deps, { + tenantId, + walletAddressId, + receiver, + debitAmount, + validDestination: false, + method: 'ilp' + }) + + const unknownTenandId = uuid() + jest.spyOn(walletAddressService, 'get').mockResolvedValueOnce(undefined) + await expect( + outgoingPaymentService.create({ + tenantId: unknownTenandId, + walletAddressId, + quoteId + }) + ).resolves.toEqual(OutgoingPaymentError.UnknownWalletAddress) + expect(walletAddressService.get).toHaveBeenCalledTimes(1) + expect(walletAddressService.get).toHaveBeenCalledWith( + walletAddressId, + unknownTenandId + ) }) it('fails to create on unknown quote', async () => { await expect( outgoingPaymentService.create({ + tenantId, walletAddressId, quoteId: uuid() }) @@ -898,6 +959,7 @@ describe('OutgoingPaymentService', (): void => { it('fails to create on "consumed" quote', async () => { const { quote } = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver, @@ -906,6 +968,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId, client, quoteId: quote.id @@ -915,6 +978,7 @@ describe('OutgoingPaymentService', (): void => { it('fails to create on invalid quote wallet address', async () => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -923,6 +987,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId: receiverWalletAddress.id, quoteId: quote.id }) @@ -931,6 +996,7 @@ describe('OutgoingPaymentService', (): void => { it('fails to create on expired quote', async () => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -942,6 +1008,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId, quoteId: quote.id }) @@ -955,6 +1022,7 @@ describe('OutgoingPaymentService', (): void => { `fails to create on $state quote receiver`, async ({ state }): Promise => { const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -967,6 +1035,7 @@ describe('OutgoingPaymentService', (): void => { }) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId, quoteId: quote.id }) @@ -976,6 +1045,7 @@ describe('OutgoingPaymentService', (): void => { test('fails to create on inactive wallet address', async () => { const { id: quoteId } = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -983,7 +1053,7 @@ describe('OutgoingPaymentService', (): void => { method: 'ilp' }) const walletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId + tenantId }) const walletAddressUpdated = await WalletAddress.query( knex @@ -991,6 +1061,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(!walletAddressUpdated.isActive) await expect( outgoingPaymentService.create({ + tenantId, walletAddressId: walletAddress.id, quoteId }) @@ -1007,6 +1078,7 @@ describe('OutgoingPaymentService', (): void => { const quotes = await Promise.all( [0, 1].map(async (_) => { return await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -1016,6 +1088,7 @@ describe('OutgoingPaymentService', (): void => { ) const options = quotes.map((quote) => { return { + tenantId, walletAddressId, client, quoteId: quote.id, @@ -1058,12 +1131,14 @@ describe('OutgoingPaymentService', (): void => { let interval: string beforeEach(async (): Promise => { quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, method: 'ilp' }) options = { + tenantId, walletAddressId, quoteId: quote.id, metadata: { @@ -1093,6 +1168,7 @@ describe('OutgoingPaymentService', (): void => { receiver } const quote = await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -1193,6 +1269,7 @@ describe('OutgoingPaymentService', (): void => { value: BigInt(190) } const firstPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver: `${ @@ -1280,6 +1357,7 @@ describe('OutgoingPaymentService', (): void => { value: BigInt(7) } const firstPayment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, client, receiver: `${ @@ -1336,6 +1414,7 @@ describe('OutgoingPaymentService', (): void => { await expect( outgoingPaymentService.fund({ id: payment.id, + tenantId, amount: payment.debitAmount.value, transferId: uuid() }) @@ -1363,6 +1442,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(incomingPayment.walletAddress) const createdPayment = await setup({ + tenantId, receiver: incomingPayment.getUrl(incomingPayment.walletAddress), receiveAmount, method: 'ilp' @@ -1395,6 +1475,7 @@ describe('OutgoingPaymentService', (): void => { .spyOn(telemetryService!, 'incrementCounter') .mockImplementation(() => Promise.resolve()) const createdPayment = await setup({ + tenantId, receiver, debitAmount, method: 'ilp' @@ -1445,6 +1526,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(incomingPayment.walletAddress) const createdPayment = await setup({ + tenantId, receiver: incomingPayment.getUrl(incomingPayment.walletAddress), receiveAmount, method: 'ilp' @@ -1480,6 +1562,7 @@ describe('OutgoingPaymentService', (): void => { const spyCounter = jest.spyOn(telemetryService, 'incrementCounter') const createdPayment = await setup({ + tenantId, receiver, debitAmount, receiveAmount, @@ -1534,6 +1617,7 @@ describe('OutgoingPaymentService', (): void => { assert.ok(incomingPayment.receivedAmount?.assetScale) const createdPayment = await setup({ + tenantId, receiver: incomingPayment.getUrl(incomingPayment.walletAddress), receiveAmount, method: 'ilp' @@ -1556,6 +1640,7 @@ describe('OutgoingPaymentService', (): void => { test('COMPLETED (with incoming payment initially partially paid)', async (): Promise => { const createdPayment = await setup( { + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1593,6 +1678,7 @@ describe('OutgoingPaymentService', (): void => { test('SENDING -> FAILED (partial payment then retryable Pay error)', async (): Promise => { const createdPayment = await setup({ + tenantId, receiver, debitAmount, method: 'ilp' @@ -1643,6 +1729,7 @@ describe('OutgoingPaymentService', (): void => { test('FAILED (non-retryable error)', async (): Promise => { const createdPayment = await setup({ + tenantId, receiver, debitAmount, method: 'ilp' @@ -1674,6 +1761,7 @@ describe('OutgoingPaymentService', (): void => { test('SENDING→COMPLETED (partial payment, resume, complete)', async (): Promise => { const createdPayment = await setup({ + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1705,6 +1793,7 @@ describe('OutgoingPaymentService', (): void => { test('COMPLETED (already fully paid)', async (): Promise => { const createdPayment = await setup( { + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1736,6 +1825,7 @@ describe('OutgoingPaymentService', (): void => { test('COMPLETED (already fully paid)', async (): Promise => { const { id: paymentId } = await setup( { + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1761,6 +1851,7 @@ describe('OutgoingPaymentService', (): void => { test('FAILED (source asset changed)', async (): Promise => { const { id: paymentId } = await setup( { + tenantId, receiver, receiveAmount, method: 'ilp' @@ -1785,6 +1876,7 @@ describe('OutgoingPaymentService', (): void => { }) test('FAILED (destination asset changed)', async (): Promise => { const createdPayment = await setup({ + tenantId, receiver, debitAmount, method: 'ilp' @@ -1813,6 +1905,7 @@ describe('OutgoingPaymentService', (): void => { beforeEach(async (): Promise => { payment = await createOutgoingPayment(deps, { + tenantId, walletAddressId, receiver, debitAmount, @@ -1831,6 +1924,7 @@ describe('OutgoingPaymentService', (): void => { await expect( outgoingPaymentService.fund({ id: uuid(), + tenantId, amount: quoteAmount, transferId: uuid() }) @@ -1841,6 +1935,7 @@ describe('OutgoingPaymentService', (): void => { await expect( outgoingPaymentService.fund({ id: payment.id, + tenantId, amount: quoteAmount, transferId: uuid() }) @@ -1860,6 +1955,7 @@ describe('OutgoingPaymentService', (): void => { await expect( outgoingPaymentService.fund({ id: payment.id, + tenantId, amount: quoteAmount - BigInt(1), transferId: uuid() }) @@ -1879,6 +1975,7 @@ describe('OutgoingPaymentService', (): void => { await expect( outgoingPaymentService.fund({ id: payment.id, + tenantId, amount: quoteAmount, transferId: uuid() }) diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index 54ede1accb..8e16eb3b74 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -101,6 +101,7 @@ interface GetPageOptions { pagination?: Pagination filter?: OutgoingPaymentFilter sortOrder?: SortOrder + tenantId?: string } async function getOutgoingPaymentsPage( @@ -153,11 +154,17 @@ async function getOutgoingPayment( options: GetOptions ): Promise { const outgoingPayment = await OutgoingPayment.query(deps.knex) + .modify((query) => { + if (options.tenantId) { + query.where({ tenantId: options.tenantId }) + } + }) .get(options) .withGraphFetched('quote') if (outgoingPayment) { outgoingPayment.walletAddress = await deps.walletAddressService.get( - outgoingPayment.walletAddressId + outgoingPayment.walletAddressId, + outgoingPayment.tenantId ) const asset = await deps.assetService.get(outgoingPayment.quote.assetId) if (asset) outgoingPayment.quote.asset = asset @@ -167,6 +174,7 @@ async function getOutgoingPayment( } export interface BaseOptions { + tenantId: string walletAddressId: string client?: string grant?: Grant @@ -184,6 +192,7 @@ export interface CreateFromIncomingPayment extends BaseOptions { export type CancelOutgoingPaymentOptions = { id: string + tenantId: string reason?: string } @@ -201,10 +210,15 @@ async function cancelOutgoingPayment( deps: ServiceDependencies, options: CancelOutgoingPaymentOptions ): Promise { - const { id } = options + const { id, tenantId } = options return deps.knex.transaction(async (trx) => { - let payment = await OutgoingPayment.query(trx).findById(id).forUpdate() + let payment = await OutgoingPayment.query(trx) + .findOne({ + id, + tenantId + }) + .forUpdate() if (!payment) return OutgoingPaymentError.UnknownPayment if (payment.state !== OutgoingPaymentState.Funding) { @@ -243,7 +257,7 @@ async function createOutgoingPayment( description: 'Time to create an outgoing payment' } ) - const { walletAddressId } = options + const { walletAddressId, tenantId } = options let quoteId: string if (isCreateFromIncomingPayment(options)) { @@ -256,6 +270,7 @@ async function createOutgoingPayment( ) const { debitAmount, incomingPayment } = options const quoteOrError = await deps.quoteService.create({ + tenantId, receiver: incomingPayment, debitAmount, method: 'ilp', @@ -281,7 +296,10 @@ async function createOutgoingPayment( description: 'Time to get wallet address in outgoing payment' } ) - const walletAddress = await deps.walletAddressService.get(walletAddressId) + const walletAddress = await deps.walletAddressService.get( + walletAddressId, + tenantId + ) stopTimerWA() if (!walletAddress) { throw OutgoingPaymentError.UnknownWalletAddress @@ -316,6 +334,7 @@ async function createOutgoingPayment( const payment = await OutgoingPayment.query(trx) .insertAndFetch({ id: quoteId, + tenantId, walletAddressId: walletAddressId, client: options.client, metadata: options.metadata, @@ -621,17 +640,21 @@ async function validateGrantAndAddSpentAmountsToPayment( export interface FundOutgoingPaymentOptions { id: string + tenantId: string amount: bigint transferId: string } async function fundPayment( deps: ServiceDependencies, - { id, amount, transferId }: FundOutgoingPaymentOptions + { id, tenantId, amount, transferId }: FundOutgoingPaymentOptions ): Promise { return await deps.knex.transaction(async (trx) => { const payment = await OutgoingPayment.query(trx) - .findById(id) + .findOne({ + id, + tenantId + }) .forUpdate() .withGraphFetched('quote') if (!payment) return FundingError.UnknownPayment @@ -680,11 +703,17 @@ async function getWalletAddressPage( options: ListOptions ): Promise { const page = await OutgoingPayment.query(deps.knex) + .modify((query) => { + if (options.tenantId) { + query.where({ tenantId: options.tenantId }) + } + }) .list(options) .withGraphFetched('quote') for (const payment of page) { payment.walletAddress = await deps.walletAddressService.get( - payment.walletAddressId + payment.walletAddressId, + payment.tenantId ) const asset = await deps.assetService.get(payment.quote.assetId) if (asset) payment.quote.asset = asset diff --git a/packages/backend/src/open_payments/quote/model.ts b/packages/backend/src/open_payments/quote/model.ts index 3c6bd6d135..a05352e228 100644 --- a/packages/backend/src/open_payments/quote/model.ts +++ b/packages/backend/src/open_payments/quote/model.ts @@ -7,6 +7,7 @@ import { import { Asset } from '../../asset/model' import { Quote as OpenPaymentsQuote } from '@interledger/open-payments' import { Fee } from '../../fee/model' +import { Tenant } from '../../tenants/model' export class Quote extends WalletAddressSubresource { public static readonly tableName = 'quotes' @@ -26,6 +27,8 @@ export class Quote extends WalletAddressSubresource { public debitAmountMinusFees?: bigint + public tenantId!: string + static get relationMappings() { return { ...super.relationMappings, @@ -44,6 +47,14 @@ export class Quote extends WalletAddressSubresource { from: 'quotes.feeId', to: 'fees.id' } + }, + tenant: { + relation: Model.BelongsToOneRelation, + modelClass: Tenant, + join: { + from: 'quotes.tenantId', + to: 'tenants.id' + } } } } @@ -56,7 +67,7 @@ export class Quote extends WalletAddressSubresource { public getUrl(walletAddress: WalletAddress): string { const url = new URL(walletAddress.url) - return `${url.origin}${Quote.urlPath}/${this.id}` + return `${url.origin}/${this.tenantId}${Quote.urlPath}/${this.id}` } public get debitAmount(): Amount { diff --git a/packages/backend/src/open_payments/quote/routes.test.ts b/packages/backend/src/open_payments/quote/routes.test.ts index 791b4f48b3..3c161305fb 100644 --- a/packages/backend/src/open_payments/quote/routes.test.ts +++ b/packages/backend/src/open_payments/quote/routes.test.ts @@ -30,6 +30,7 @@ describe('Quote Routes', (): void => { let quoteRoutes: QuoteRoutes let walletAddress: WalletAddress let baseUrl: string + let tenantId: string const receiver = `https://wallet2.example/incoming-payments/${uuid()}` const asset = randomAsset() @@ -40,13 +41,16 @@ describe('Quote Routes', (): void => { } const createWalletAddressQuote = async ({ + tenantId, walletAddressId, client }: { + tenantId: string walletAddressId: string client?: string }): Promise => { return await createQuote(deps, { + tenantId, walletAddressId, receiver, debitAmount: { @@ -72,12 +76,13 @@ describe('Quote Routes', (): void => { }) beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId const { id: assetId } = await createAsset(deps, { code: debitAmount.assetCode, scale: debitAmount.assetScale }) walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId }) baseUrl = new URL(walletAddress.url).origin @@ -96,13 +101,14 @@ describe('Quote Routes', (): void => { getWalletAddress: async () => walletAddress, createModel: async ({ client }) => createWalletAddressQuote({ + tenantId, walletAddressId: walletAddress.id, client }), get: (ctx) => quoteRoutes.get(ctx), getBody: (quote) => { return { - id: `${baseUrl}/quotes/${quote.id}`, + id: `${baseUrl}/${quote.tenantId}/quotes/${quote.id}`, walletAddress: walletAddress.url, receiver: quote.receiver, debitAmount: serializeAmount(quote.debitAmount), @@ -130,6 +136,9 @@ describe('Quote Routes', (): void => { method: 'POST', url: `/quotes` }, + params: { + tenantId + }, walletAddress, client }) @@ -195,6 +204,7 @@ describe('Quote Routes', (): void => { }) await expect(quoteRoutes.create(ctx)).resolves.toBeUndefined() expect(quoteSpy).toHaveBeenCalledWith({ + tenantId, walletAddressId: walletAddress.id, receiver, debitAmount: options.debitAmount && { @@ -216,7 +226,7 @@ describe('Quote Routes', (): void => { .pop() assert.ok(quote) expect(ctx.response.body).toEqual({ - id: `${baseUrl}/quotes/${quoteId}`, + id: `${baseUrl}/${tenantId}/quotes/${quoteId}`, walletAddress: walletAddress.url, receiver: quote.receiver, debitAmount: { @@ -254,6 +264,7 @@ describe('Quote Routes', (): void => { }) await expect(quoteRoutes.create(ctx)).resolves.toBeUndefined() expect(quoteSpy).toHaveBeenCalledWith({ + tenantId, walletAddressId: walletAddress.id, receiver, client, @@ -267,7 +278,7 @@ describe('Quote Routes', (): void => { .pop() assert.ok(quote) expect(ctx.response.body).toEqual({ - id: `${baseUrl}/quotes/${quoteId}`, + id: `${baseUrl}/${tenantId}/quotes/${quoteId}`, walletAddress: walletAddress.url, receiver: options.receiver, debitAmount: { diff --git a/packages/backend/src/open_payments/quote/routes.ts b/packages/backend/src/open_payments/quote/routes.ts index d0069280fc..86212743c8 100644 --- a/packages/backend/src/open_payments/quote/routes.ts +++ b/packages/backend/src/open_payments/quote/routes.ts @@ -38,7 +38,8 @@ async function getQuote( ): Promise { const quote = await deps.quoteService.get({ id: ctx.params.id, - client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined + client: ctx.accessAction === AccessAction.Read ? ctx.client : undefined, + tenantId: ctx.params.tenantId }) if (!quote) { @@ -73,7 +74,9 @@ async function createQuote( ctx: CreateContext ): Promise { const { body } = ctx.request + const { tenantId } = ctx.params const options: CreateQuoteOptions = { + tenantId, walletAddressId: ctx.walletAddress.id, receiver: body.receiver, client: ctx.client, diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index 7bd4917c11..ef91ab9568 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -35,12 +35,14 @@ import { PaymentMethodHandlerErrorCode } from '../../payment-method/handler/errors' import { Receiver } from '../receiver/model' +import { WalletAddressService } from '../wallet_address/service' describe('QuoteService', (): void => { let deps: IocContract let appContainer: TestContainer let quoteService: QuoteService let paymentMethodHandlerService: PaymentMethodHandlerService + let walletAddressService: WalletAddressService let receiverService: ReceiverService let knex: Knex let sendingWalletAddress: MockWalletAddress @@ -53,6 +55,7 @@ describe('QuoteService', (): void => { // eslint-disable-next-line @typescript-eslint/no-explicit-any any > + let tenantId: string const asset: AssetOptions = { scale: 9, @@ -87,21 +90,23 @@ describe('QuoteService', (): void => { config = await deps.use('config') quoteService = await deps.use('quoteService') paymentMethodHandlerService = await deps.use('paymentMethodHandlerService') + walletAddressService = await deps.use('walletAddressService') receiverService = await deps.use('receiverService') }) beforeEach(async (): Promise => { + tenantId = config.operatorTenantId const { id: sendAssetId } = await createAsset(deps, { code: debitAmount.assetCode, scale: debitAmount.assetScale }) sendingWalletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId: sendAssetId }) const { id: destinationAssetId } = await createAsset(deps, destinationAsset) receivingWalletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId: destinationAssetId, mockServerPort: appContainer.openPaymentsPort }) @@ -137,6 +142,7 @@ describe('QuoteService', (): void => { getTests({ createModel: ({ client }) => createQuote(deps, { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: `${receivingWalletAddress.url}/incoming-payments/${uuid()}`, debitAmount: { @@ -182,6 +188,7 @@ describe('QuoteService', (): void => { tenantId: Config.operatorTenantId }) options = { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: incomingPayment.getUrl(receivingWalletAddress), method: 'ilp' @@ -257,6 +264,7 @@ describe('QuoteService', (): void => { await expect( quoteService.get({ + tenantId, id: quote.id }) ).resolves.toEqual(quote) @@ -343,6 +351,7 @@ describe('QuoteService', (): void => { await expect( quoteService.get({ + tenantId, id: quote.id }) ).resolves.toEqual(quote) @@ -387,6 +396,7 @@ describe('QuoteService', (): void => { tenantId: Config.operatorTenantId }) const options: CreateQuoteOptions = { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: incomingPayment.getUrl(receivingWalletAddress), receiveAmount, @@ -425,21 +435,52 @@ describe('QuoteService', (): void => { }) } ) + test('fails on unknown tenant id', async (): Promise => { + const walletAddress = await createWalletAddress(deps, { + tenantId + }) + const unknownTenantId = uuid() + + jest.spyOn(walletAddressService, 'get').mockResolvedValueOnce(undefined) + await expect( + quoteService.create({ + tenantId: unknownTenantId, + walletAddressId: walletAddress.id, + receiver: `${receivingWalletAddress.url}/incoming-payments/${uuid()}`, + debitAmount, + method: 'ilp' + }) + ).resolves.toEqual(QuoteError.UnknownWalletAddress) + expect(walletAddressService.get).toHaveBeenCalledTimes(1) + expect(walletAddressService.get).toHaveBeenCalledWith( + walletAddress.id, + unknownTenantId + ) + }) test('fails on unknown wallet address', async (): Promise => { + const unknownWalletAddressId = uuid() + jest.spyOn(walletAddressService, 'get').mockResolvedValueOnce(undefined) + await expect( quoteService.create({ - walletAddressId: uuid(), + tenantId, + walletAddressId: unknownWalletAddressId, receiver: `${receivingWalletAddress.url}/incoming-payments/${uuid()}`, debitAmount, method: 'ilp' }) ).resolves.toEqual(QuoteError.UnknownWalletAddress) + expect(walletAddressService.get).toHaveBeenCalledTimes(1) + expect(walletAddressService.get).toHaveBeenCalledWith( + unknownWalletAddressId, + tenantId + ) }) test('fails on inactive wallet address', async () => { const walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId + tenantId }) const walletAddressUpdated = await WalletAddress.query( knex @@ -447,6 +488,7 @@ describe('QuoteService', (): void => { assert.ok(!walletAddressUpdated.isActive) await expect( quoteService.create({ + tenantId, walletAddressId: walletAddress.id, receiver: `${receivingWalletAddress.url}/incoming-payments/${uuid()}`, debitAmount, @@ -458,6 +500,7 @@ describe('QuoteService', (): void => { test('fails on invalid receiver', async (): Promise => { await expect( quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: `${receivingWalletAddress.url}/incoming-payments/${uuid()}`, debitAmount, @@ -480,6 +523,7 @@ describe('QuoteService', (): void => { await expect( quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, method: 'ilp', @@ -509,6 +553,7 @@ describe('QuoteService', (): void => { tenantId: Config.operatorTenantId }) const options: CreateQuoteOptions = { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: incomingPayment.getUrl(receivingWalletAddress), method: 'ilp' @@ -532,11 +577,11 @@ describe('QuoteService', (): void => { scale: 2 }) sendingWalletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId: asset.id }) receivingWalletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId: asset.id }) }) @@ -582,6 +627,7 @@ describe('QuoteService', (): void => { .mockResolvedValueOnce(mockedQuote) const quote = await quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, method: 'ilp' @@ -620,6 +666,7 @@ describe('QuoteService', (): void => { await expect( quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, method: 'ilp' @@ -644,11 +691,11 @@ describe('QuoteService', (): void => { scale: 2 }) sendingWalletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId: sendAsset.id }) receivingWalletAddress = await createWalletAddress(deps, { - tenantId: config.operatorTenantId, + tenantId, assetId: receiveAsset.id }) }) @@ -692,6 +739,7 @@ describe('QuoteService', (): void => { .mockResolvedValueOnce(mockedQuote) const quote = await quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, debitAmount: { @@ -735,6 +783,7 @@ describe('QuoteService', (): void => { await expect( quoteService.create({ + tenantId, walletAddressId: sendingWalletAddress.id, receiver: receiver.incomingPayment!.id, debitAmount: { @@ -760,6 +809,7 @@ describe('QuoteService', (): void => { }) const options: CreateQuoteOptions = { + tenantId, walletAddressId: sendingWalletAddress.id, receiver: incomingPayment.getUrl(receivingWalletAddress), method: 'ilp' @@ -805,6 +855,7 @@ describe('QuoteService', (): void => { await expect( quoteService.get({ + tenantId, id: quote.id }) ).resolves.toEqual(quote) diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 957c4065f4..e274c43b5e 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -57,6 +57,11 @@ async function getQuote( options: GetOptions ): Promise { const quote = await Quote.query(deps.knex) + .modify((query) => { + if (options.tenantId) { + query.where({ tenantId: options.tenantId }) + } + }) .get(options) .withGraphFetched('fee') if (quote) { @@ -64,13 +69,15 @@ async function getQuote( if (asset) quote.asset = asset quote.walletAddress = await deps.walletAddressService.get( - quote.walletAddressId + quote.walletAddressId, + quote.tenantId ) } return quote } interface QuoteOptionsBase { + tenantId: string walletAddressId: string receiver: string method: 'ilp' @@ -104,7 +111,8 @@ async function createQuote( return QuoteError.InvalidAmount } const walletAddress = await deps.walletAddressService.get( - options.walletAddressId + options.walletAddressId, + options.tenantId ) if (!walletAddress) { stopTimer() @@ -189,6 +197,7 @@ async function createQuote( const createdQuote = await Quote.query(trx) .insertAndFetch({ id: quoteId, + tenantId: options.tenantId, walletAddressId: options.walletAddressId, assetId: walletAddress.assetId, receiver: options.receiver, @@ -447,6 +456,11 @@ async function getWalletAddressPage( options: ListOptions ): Promise { const quotes = await Quote.query(deps.knex) + .modify((query) => { + if (options.tenantId) { + query.where({ tenantId: options.tenantId }) + } + }) .list(options) .withGraphFetched('fee') for (const quote of quotes) { @@ -454,7 +468,8 @@ async function getWalletAddressPage( if (asset) quote.asset = asset quote.walletAddress = await deps.walletAddressService.get( - quote.walletAddressId + quote.walletAddressId, + quote.tenantId ) } return quotes diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index 0596c7cf3d..2077703d1c 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -424,7 +424,6 @@ describe('Receiver Service', (): void => { incomingPaymentService, 'create' ) - const receiver = await receiverService.create({ walletAddressUrl: walletAddress.id, incomingAmount, diff --git a/packages/backend/src/open_payments/wallet_address/middleware.test.ts b/packages/backend/src/open_payments/wallet_address/middleware.test.ts index 72d0b0dfc5..3ae2438ec1 100644 --- a/packages/backend/src/open_payments/wallet_address/middleware.test.ts +++ b/packages/backend/src/open_payments/wallet_address/middleware.test.ts @@ -31,6 +31,9 @@ import { OutgoingPaymentService } from '../payment/outgoing/service' import { Quote } from '../quote/model' import { IncomingPayment } from '../payment/incoming/model' import { OutgoingPayment } from '../payment/outgoing/model' +import { createOutgoingPayment } from '../../tests/outgoingPayment' +import { createAsset } from '../../tests/asset' +import { AssetOptions } from '../../asset/service' describe('Wallet Address Middleware', (): void => { let deps: IocContract @@ -226,6 +229,58 @@ describe('Wallet Address Middleware', (): void => { expect(next).toHaveBeenCalled() }) + test('throws error if could not find existing quote for mismatched tenantId', async () => { + const tenantId = Config.operatorTenantId + + const asset: AssetOptions = { + scale: 9, + code: 'USD' + } + const { id: sendAssetId } = await createAsset(deps, asset) + const walletAddress = await createWalletAddress(deps, { + tenantId, + assetId: sendAssetId + }) + + const existingQuoteId = ( + await createOutgoingPayment(deps, { + tenantId: Config.operatorTenantId, + walletAddressId: walletAddress.id, + method: 'ilp', + receiver: `${ + Config.openPaymentsUrl + }/${crypto.randomUUID()}/incoming-payments/${crypto.randomUUID()}`, + debitAmount: { + value: BigInt(456), + assetCode: walletAddress.asset.code, + assetScale: walletAddress.asset.scale + }, + validDestination: false + }) + ).quote.id + + const ctx: WalletAddressUrlContext = createContext( + { headers: { Accept: 'application/json' } }, + { + id: existingQuoteId, + tenantId: crypto.randomUUID() + } + ) + + ctx.container = deps + const next = jest.fn() + + expect.assertions(3) + try { + await getWalletAddressUrlFromQuote(ctx, next) + } catch (err) { + assert(err instanceof OpenPaymentsServerRouteError) + expect(err.status).toBe(401) + expect(err.message).toBe('Unauthorized') + expect(next).not.toHaveBeenCalled() + } + }) + test('throws error if could not find quote', async (): Promise => { const ctx: WalletAddressUrlContext = createContext( { @@ -290,6 +345,58 @@ describe('Wallet Address Middleware', (): void => { expect(next).toHaveBeenCalled() }) + test('throws error if could not find existing outgoing payment for mismatched tenantId', async () => { + const tenantId = Config.operatorTenantId + + const asset: AssetOptions = { + scale: 9, + code: 'USD' + } + const { id: sendAssetId } = await createAsset(deps, asset) + const walletAddress = await createWalletAddress(deps, { + tenantId, + assetId: sendAssetId + }) + + const existingPaymentId = ( + await createOutgoingPayment(deps, { + tenantId: Config.operatorTenantId, + walletAddressId: walletAddress.id, + method: 'ilp', + receiver: `${ + Config.openPaymentsUrl + }/${crypto.randomUUID()}/incoming-payments/${crypto.randomUUID()}`, + debitAmount: { + value: BigInt(456), + assetCode: walletAddress.asset.code, + assetScale: walletAddress.asset.scale + }, + validDestination: false + }) + ).id + + const ctx: WalletAddressUrlContext = createContext( + { headers: { Accept: 'application/json' } }, + { + id: existingPaymentId, + tenantId: crypto.randomUUID() + } + ) + + ctx.container = deps + const next = jest.fn() + + expect.assertions(3) + try { + await getWalletAddressUrlFromOutgoingPayment(ctx, next) + } catch (err) { + assert(err instanceof OpenPaymentsServerRouteError) + expect(err.status).toBe(401) + expect(err.message).toBe('Unauthorized') + expect(next).not.toHaveBeenCalled() + } + }) + test('throws error if could not find outgoing payment', async (): Promise => { const ctx: WalletAddressUrlContext = createContext( { diff --git a/packages/backend/src/open_payments/wallet_address/middleware.ts b/packages/backend/src/open_payments/wallet_address/middleware.ts index 2404966244..49fa7c3f21 100644 --- a/packages/backend/src/open_payments/wallet_address/middleware.ts +++ b/packages/backend/src/open_payments/wallet_address/middleware.ts @@ -66,7 +66,8 @@ export async function getWalletAddressUrlFromOutgoingPayment( 'outgoingPaymentService' ) const outgoingPayment = await outgoingPaymentService.get({ - id: ctx.params.id + id: ctx.params.id, + tenantId: ctx.params.tenantId }) if (!outgoingPayment?.walletAddress) { @@ -86,7 +87,8 @@ export async function getWalletAddressUrlFromQuote( ) { const quoteService = await ctx.container.use('quoteService') const quote = await quoteService.get({ - id: ctx.params.id + id: ctx.params.id, + tenantId: ctx.params.tenantId }) if (!quote?.walletAddress) { diff --git a/packages/backend/src/payment-method/handler/service.test.ts b/packages/backend/src/payment-method/handler/service.test.ts index 6398f06b6f..5bcfaf36ea 100644 --- a/packages/backend/src/payment-method/handler/service.test.ts +++ b/packages/backend/src/payment-method/handler/service.test.ts @@ -45,9 +45,10 @@ describe('PaymentMethodHandlerService', (): void => { describe('getQuote', (): void => { test('calls ilpPaymentService for ILP payment type', async (): Promise => { + const tenantId = Config.operatorTenantId const asset = await createAsset(deps) const walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) @@ -74,9 +75,10 @@ describe('PaymentMethodHandlerService', (): void => { ) }) test('calls localPaymentService for local payment type', async (): Promise => { + const tenantId = Config.operatorTenantId const asset = await createAsset(deps) const walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) @@ -105,9 +107,10 @@ describe('PaymentMethodHandlerService', (): void => { describe('pay', (): void => { test('calls ilpPaymentService for ILP payment type', async (): Promise => { + const tenantId = Config.operatorTenantId const asset = await createAsset(deps) const walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) const { receiver, outgoingPayment } = @@ -116,6 +119,7 @@ describe('PaymentMethodHandlerService', (): void => { receivingWalletAddress: walletAddress, method: 'ilp', quoteOptions: { + tenantId, debitAmount: { assetCode: walletAddress.asset.code, assetScale: walletAddress.asset.scale, @@ -140,9 +144,10 @@ describe('PaymentMethodHandlerService', (): void => { expect(ilpPaymentServicePaySpy).toHaveBeenCalledWith(options) }) test('calls localPaymentService for local payment type', async (): Promise => { + const tenantId = Config.operatorTenantId const asset = await createAsset(deps) const walletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) const { receiver, outgoingPayment } = @@ -151,6 +156,7 @@ describe('PaymentMethodHandlerService', (): void => { receivingWalletAddress: walletAddress, method: 'ilp', quoteOptions: { + tenantId, debitAmount: { assetCode: walletAddress.asset.code, assetScale: walletAddress.asset.scale, diff --git a/packages/backend/src/payment-method/ilp/service.test.ts b/packages/backend/src/payment-method/ilp/service.test.ts index db5dda2be6..48e59a88c4 100644 --- a/packages/backend/src/payment-method/ilp/service.test.ts +++ b/packages/backend/src/payment-method/ilp/service.test.ts @@ -36,6 +36,7 @@ describe('IlpPaymentService', (): void => { let ilpPaymentService: IlpPaymentService let accountingService: AccountingService let config: IAppConfig + let tenantId: string const exchangeRatesUrl = 'https://example-rates.com' @@ -56,6 +57,7 @@ describe('IlpPaymentService', (): void => { }) beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId assetMap['USD'] = await createAsset(deps, { code: 'USD', scale: 2 @@ -67,12 +69,12 @@ describe('IlpPaymentService', (): void => { }) walletAddressMap['USD'] = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: assetMap['USD'].id }) walletAddressMap['EUR'] = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: assetMap['EUR'].id }) }) @@ -669,6 +671,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -699,6 +702,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, exchangeRate: 1, debitAmount: { value: 100n, @@ -749,6 +753,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -789,6 +794,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -829,6 +835,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -866,6 +873,7 @@ describe('IlpPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index 6af3327f49..ee2afc5d2b 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -33,6 +33,7 @@ describe('LocalPaymentService', (): void => { let localPaymentService: LocalPaymentService let accountingService: AccountingService let incomingPaymentService: IncomingPaymentService + let tenantId: string const exchangeRatesUrl = 'https://example-rates.com' @@ -53,6 +54,7 @@ describe('LocalPaymentService', (): void => { }) beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId assetMap['USD'] = await createAsset(deps, { code: 'USD', scale: 2 @@ -69,17 +71,17 @@ describe('LocalPaymentService', (): void => { }) walletAddressMap['USD'] = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: assetMap['USD'].id }) walletAddressMap['USD_9'] = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: assetMap['USD_9'].id }) walletAddressMap['EUR'] = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: assetMap['EUR'].id }) }) @@ -410,6 +412,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -440,6 +443,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -477,6 +481,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -516,6 +521,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -555,6 +561,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -594,6 +601,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, @@ -634,6 +642,7 @@ describe('LocalPaymentService', (): void => { receivingWalletAddress: walletAddressMap['USD'], method: 'ilp', quoteOptions: { + tenantId, debitAmount: { value: 100n, assetScale: walletAddressMap['USD'].asset.scale, diff --git a/packages/backend/src/shared/pagination.test.ts b/packages/backend/src/shared/pagination.test.ts index f2bc6ddd0c..85d3df3e01 100644 --- a/packages/backend/src/shared/pagination.test.ts +++ b/packages/backend/src/shared/pagination.test.ts @@ -74,6 +74,7 @@ describe('Pagination', (): void => { }) describe('getPageInfo', (): void => { describe('wallet address resources', (): void => { + let tenantId: string let defaultWalletAddress: WalletAddress let secondaryWalletAddress: WalletAddress let debitAmount: Amount @@ -83,13 +84,14 @@ describe('Pagination', (): void => { outgoingPaymentService = await deps.use('outgoingPaymentService') quoteService = await deps.use('quoteService') + tenantId = Config.operatorTenantId const asset = await createAsset(deps) defaultWalletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) secondaryWalletAddress = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId, + tenantId, assetId: asset.id }) debitAmount = { @@ -175,6 +177,7 @@ describe('Pagination', (): void => { const paymentIds: string[] = [] for (let i = 0; i < num; i++) { const payment = await createOutgoingPayment(deps, { + tenantId, walletAddressId: defaultWalletAddress.id, receiver: secondaryWalletAddress.url, method: 'ilp', @@ -232,6 +235,7 @@ describe('Pagination', (): void => { const quoteIds: string[] = [] for (let i = 0; i < num; i++) { const quote = await createQuote(deps, { + tenantId, walletAddressId: defaultWalletAddress.id, receiver: secondaryWalletAddress.url, debitAmount, diff --git a/packages/backend/src/tests/combinedPayment.ts b/packages/backend/src/tests/combinedPayment.ts index b7e96b117a..fbae2901b7 100644 --- a/packages/backend/src/tests/combinedPayment.ts +++ b/packages/backend/src/tests/combinedPayment.ts @@ -58,6 +58,7 @@ export async function createCombinedPayment( tenantId: receiveWalletAddress.tenantId }) : await createOutgoingPayment(deps, { + tenantId: Config.operatorTenantId, walletAddressId: sendWalletAddressId, method: 'ilp', receiver: `${Config.openPaymentsUrl}/${uuid()}`, diff --git a/packages/backend/src/tests/outgoingPayment.ts b/packages/backend/src/tests/outgoingPayment.ts index c618af026f..82afb05147 100644 --- a/packages/backend/src/tests/outgoingPayment.ts +++ b/packages/backend/src/tests/outgoingPayment.ts @@ -25,6 +25,7 @@ export async function createOutgoingPayment( options: CreateTestQuoteAndOutgoingPaymentOptions ): Promise { const quoteOptions: CreateTestQuoteOptions = { + tenantId: options.tenantId, walletAddressId: options.walletAddressId, client: options.client, receiver: options.receiver, @@ -88,7 +89,7 @@ interface CreateOutgoingPaymentWithReceiverArgs { quoteOptions?: Partial< Pick< CreateTestQuoteAndOutgoingPaymentOptions, - 'debitAmount' | 'receiveAmount' | 'exchangeRate' + 'debitAmount' | 'receiveAmount' | 'exchangeRate' | 'tenantId' > > sendingWalletAddress: WalletAddress @@ -134,6 +135,7 @@ export async function createOutgoingPaymentWithReceiver( ) const outgoingPayment = await createOutgoingPayment(deps, { + tenantId: args.sendingWalletAddress.tenantId, walletAddressId: args.sendingWalletAddress.id, method: args.method, receiver: receiver.incomingPayment!.id!, @@ -144,6 +146,7 @@ export async function createOutgoingPaymentWithReceiver( const outgoingPaymentService = await deps.use('outgoingPaymentService') await outgoingPaymentService.fund({ id: outgoingPayment.id, + tenantId: args.sendingWalletAddress.tenantId, amount: outgoingPayment.debitAmount.value, transferId: uuid() }) diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index 5992a66e8b..c49022a21d 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -57,6 +57,7 @@ export function mockQuote( export async function createQuote( deps: IocContract, { + tenantId, walletAddressId, receiver: receiverUrl, debitAmount, @@ -70,7 +71,10 @@ export async function createQuote( }: CreateTestQuoteOptions ): Promise { const walletAddressService = await deps.use('walletAddressService') - const walletAddress = await walletAddressService.get(walletAddressId) + const walletAddress = await walletAddressService.get( + walletAddressId, + tenantId + ) if (!walletAddress) { throw new Error('wallet not found') } @@ -174,6 +178,7 @@ export async function createQuote( return Quote.query() .insertAndFetch({ id: quoteId, + tenantId, walletAddressId, assetId: walletAddress.assetId, receiver: receiverUrl, diff --git a/packages/backend/src/webhook/service.test.ts b/packages/backend/src/webhook/service.test.ts index c0337a821d..d2d55f09e3 100644 --- a/packages/backend/src/webhook/service.test.ts +++ b/packages/backend/src/webhook/service.test.ts @@ -110,6 +110,7 @@ describe('Webhook Service', (): void => { }) describe('Get Webhook Event by account id and types', (): void => { + let tenantId: string let walletAddressIn: WalletAddress let walletAddressOut: WalletAddress let incomingPaymentIds: string[] @@ -117,11 +118,12 @@ describe('Webhook Service', (): void => { let events: WebhookEvent[] = [] beforeEach(async (): Promise => { + tenantId = Config.operatorTenantId walletAddressIn = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId + tenantId }) walletAddressOut = await createWalletAddress(deps, { - tenantId: Config.operatorTenantId + tenantId }) incomingPaymentIds = [ ( @@ -141,6 +143,7 @@ describe('Webhook Service', (): void => { ( await createOutgoingPayment(deps, { method: 'ilp', + tenantId, walletAddressId: walletAddressOut.id, receiver: '', validDestination: false @@ -149,6 +152,7 @@ describe('Webhook Service', (): void => { ( await createOutgoingPayment(deps, { method: 'ilp', + tenantId, walletAddressId: walletAddressOut.id, receiver: '', validDestination: false diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index b39d8afd2e..660cf0f59e 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1010,6 +1010,8 @@ export type OutgoingPayment = BasePayment & Model & { state: OutgoingPaymentState; /** Number of attempts made to send an outgoing payment. */ stateAttempts: Scalars['Int']['output']; + /** Tenant ID of the outgoing payment. */ + tenantId?: Maybe; /** Unique identifier of the wallet address under which the outgoing payment was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -1253,6 +1255,7 @@ export type QueryOutgoingPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1263,6 +1266,7 @@ export type QueryPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1355,6 +1359,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2386,6 +2392,7 @@ export type OutgoingPaymentResolvers; state?: Resolver; stateAttempts?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2495,6 +2502,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 7ce6b6ce2e..57a2ac3fc1 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1010,6 +1010,8 @@ export type OutgoingPayment = BasePayment & Model & { state: OutgoingPaymentState; /** Number of attempts made to send an outgoing payment. */ stateAttempts: Scalars['Int']['output']; + /** Tenant ID of the outgoing payment. */ + tenantId?: Maybe; /** Unique identifier of the wallet address under which the outgoing payment was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -1253,6 +1255,7 @@ export type QueryOutgoingPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1263,6 +1266,7 @@ export type QueryPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1355,6 +1359,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2386,6 +2392,7 @@ export type OutgoingPaymentResolvers; state?: Resolver; stateAttempts?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2495,6 +2502,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 7ce6b6ce2e..57a2ac3fc1 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1010,6 +1010,8 @@ export type OutgoingPayment = BasePayment & Model & { state: OutgoingPaymentState; /** Number of attempts made to send an outgoing payment. */ stateAttempts: Scalars['Int']['output']; + /** Tenant ID of the outgoing payment. */ + tenantId?: Maybe; /** Unique identifier of the wallet address under which the outgoing payment was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -1253,6 +1255,7 @@ export type QueryOutgoingPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1263,6 +1266,7 @@ export type QueryPaymentsArgs = { first?: InputMaybe; last?: InputMaybe; sortOrder?: InputMaybe; + tenantId?: InputMaybe; }; @@ -1355,6 +1359,8 @@ export type Quote = { receiveAmount: Amount; /** Wallet address URL of the receiver. */ receiver: Scalars['String']['output']; + /** Unique identifier of the tenant under which the quote was created. */ + tenantId: Scalars['ID']['output']; /** Unique identifier of the wallet address under which the quote was created. */ walletAddressId: Scalars['ID']['output']; }; @@ -2386,6 +2392,7 @@ export type OutgoingPaymentResolvers; state?: Resolver; stateAttempts?: Resolver; + tenantId?: Resolver, ParentType, ContextType>; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2495,6 +2502,7 @@ export type QuoteResolvers; receiveAmount?: Resolver; receiver?: Resolver; + tenantId?: Resolver; walletAddressId?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/test-actions/open-payments.ts b/test/integration/lib/test-actions/open-payments.ts index 567041fe5e..b789e2af20 100644 --- a/test/integration/lib/test-actions/open-payments.ts +++ b/test/integration/lib/test-actions/open-payments.ts @@ -12,13 +12,7 @@ import { isPendingGrant } from '@interledger/open-payments' import { MockASE } from '../mock-ase' -import { - UnionOmit, - poll, - pollCondition, - wait, - urlWithoutTenantId -} from '../utils' +import { UnionOmit, poll, pollCondition, wait } from '../utils' import { WebhookEventType } from 'mock-account-service-lib' import { CreateOutgoingPaymentArgs, @@ -218,7 +212,7 @@ async function createQuote( const { sendingASE } = deps return await sendingASE.opClient.quote.create( { - url: urlWithoutTenantId(senderWalletAddress.resourceServer), + url: senderWalletAddress.resourceServer, accessToken }, { @@ -326,7 +320,7 @@ async function createOutgoingPayment( const outgoingPayment = await sendingASE.opClient.outgoingPayment.create( { - url: urlWithoutTenantId(senderWalletAddress.resourceServer), + url: senderWalletAddress.resourceServer, accessToken: grantContinue.access_token.value }, {