From be5e7eda6ef8ff89222ae7c89a0bce00f826ae66 Mon Sep 17 00:00:00 2001 From: Dragos Date: Wed, 20 Sep 2023 11:54:56 +0300 Subject: [PATCH] feat(payments): create external receiver --- .../backend/src/incomingPayment/service.ts | 7 +- .../src/rafiki/auth/generated/graphql.ts | 8 ++- .../src/rafiki/backend/generated/graphql.ts | 69 ++++++++++++++++++- .../backend/request/receiver.request.ts | 28 ++++++++ .../wallet/backend/src/rafiki/controller.ts | 4 +- .../backend/src/rafiki/rafiki-client.ts | 52 +++++++++++++- .../wallet/backend/src/rafiki/validation.ts | 8 +-- 7 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 packages/wallet/backend/src/rafiki/backend/request/receiver.request.ts diff --git a/packages/wallet/backend/src/incomingPayment/service.ts b/packages/wallet/backend/src/incomingPayment/service.ts index ccaa8a161..5b3d1782f 100644 --- a/packages/wallet/backend/src/incomingPayment/service.ts +++ b/packages/wallet/backend/src/incomingPayment/service.ts @@ -1,5 +1,5 @@ import { AccountService } from '@/account/service' -import { BadRequest, NotFound } from '@/errors' +import { NotFound } from '@/errors' import { PaymentDetails } from '@/incomingPayment/controller' import { PaymentPointer } from '@/paymentPointer/model' import { @@ -152,8 +152,11 @@ export class IncomingPaymentService implements IIncomingPaymentService { const existingPaymentPointer = await PaymentPointer.query().findOne({ url: params.paymentPointerUrl ?? '' }) + if (!existingPaymentPointer) { - throw new BadRequest('Invalid payment pointer') + const response = await this.deps.rafikiClient.createReceiver(params) + // id is the incoming payment url + return response.id } const response = await this.createIncomingPaymentTransactions({ diff --git a/packages/wallet/backend/src/rafiki/auth/generated/graphql.ts b/packages/wallet/backend/src/rafiki/auth/generated/graphql.ts index 8c35d6d59..d31c3f1c4 100644 --- a/packages/wallet/backend/src/rafiki/auth/generated/graphql.ts +++ b/packages/wallet/backend/src/rafiki/auth/generated/graphql.ts @@ -32,8 +32,13 @@ export type Access = Model & { type: Scalars['String']['output']; }; +export type FilterGrantState = { + in?: InputMaybe>; + notIn?: InputMaybe>; +}; + export type FilterString = { - in: Array; + in?: InputMaybe>; }; export type Grant = Model & { @@ -58,6 +63,7 @@ export type GrantEdge = { export type GrantFilter = { identifier?: InputMaybe; + state?: InputMaybe; }; export enum GrantState { diff --git a/packages/wallet/backend/src/rafiki/backend/generated/graphql.ts b/packages/wallet/backend/src/rafiki/backend/generated/graphql.ts index f6de872f1..d40b987c4 100644 --- a/packages/wallet/backend/src/rafiki/backend/generated/graphql.ts +++ b/packages/wallet/backend/src/rafiki/backend/generated/graphql.ts @@ -96,6 +96,13 @@ export type AssetsConnection = { pageInfo: PageInfo; }; +export type BasePayment = { + createdAt: Scalars['String']['output']; + id: Scalars['ID']['output']; + metadata?: Maybe; + paymentPointerId: Scalars['ID']['output']; +}; + export type CreateAssetInput = { /** [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217), e.g. `USD` */ code: Scalars['String']['input']; @@ -315,7 +322,7 @@ export type HttpOutgoingInput = { endpoint: Scalars['String']['input']; }; -export type IncomingPayment = Model & { +export type IncomingPayment = BasePayment & Model & { __typename?: 'IncomingPayment'; /** Date-time of creation */ createdAt: Scalars['String']['output']; @@ -596,7 +603,7 @@ export type MutationResponse = { success: Scalars['Boolean']['output']; }; -export type OutgoingPayment = Model & { +export type OutgoingPayment = BasePayment & Model & { __typename?: 'OutgoingPayment'; /** Date-time of creation */ createdAt: Scalars['String']['output']; @@ -665,6 +672,39 @@ export type PageInfo = { startCursor?: Maybe; }; +export type Payment = BasePayment & Model & { + __typename?: 'Payment'; + /** Date-time of creation */ + createdAt: Scalars['String']['output']; + /** Payment id */ + id: Scalars['ID']['output']; + /** Additional metadata associated with the payment. */ + metadata?: Maybe; + /** Id of the payment pointer under which this payment was created */ + paymentPointerId: Scalars['ID']['output']; + /** Either the IncomingPaymentState or OutgoingPaymentState according to type */ + state: Scalars['String']['output']; + /** Type of payment */ + type: PaymentType; +}; + +export type PaymentConnection = { + __typename?: 'PaymentConnection'; + edges: Array; + pageInfo: PageInfo; +}; + +export type PaymentEdge = { + __typename?: 'PaymentEdge'; + cursor: Scalars['String']['output']; + node: Payment; +}; + +export type PaymentFilter = { + paymentPointerId?: InputMaybe; + type?: InputMaybe; +}; + export type PaymentPointer = Model & { __typename?: 'PaymentPointer'; /** Asset of the payment pointer */ @@ -682,7 +722,7 @@ export type PaymentPointer = Model & { /** List of quotes created at this payment pointer */ quotes?: Maybe; /** Status of the payment pointer */ - status?: Maybe; + status: PaymentPointerStatus; /** Payment Pointer URL */ url: Scalars['String']['output']; }; @@ -763,6 +803,11 @@ export type PaymentPointersConnection = { pageInfo: PageInfo; }; +export enum PaymentType { + Incoming = 'INCOMING', + Outgoing = 'OUTGOING' +} + export type Peer = Model & { __typename?: 'Peer'; /** Asset of peering relationship */ @@ -816,6 +861,8 @@ export type Query = { paymentPointer?: Maybe; /** Fetch a page of payment pointers. */ paymentPointers: PaymentPointersConnection; + /** Fetch a page of combined payments */ + payments: PaymentConnection; /** Fetch a peer */ peer?: Maybe; /** Fetch a page of peers. */ @@ -863,6 +910,15 @@ export type QueryPaymentPointersArgs = { }; +export type QueryPaymentsArgs = { + after?: InputMaybe; + before?: InputMaybe; + filter?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + export type QueryPeerArgs = { id: Scalars['String']['input']; }; @@ -1183,3 +1239,10 @@ export type GetQuoteQueryVariables = Exact<{ export type GetQuoteQuery = { __typename?: 'Query', quote?: { __typename?: 'Quote', id: string, paymentPointerId: string, receiver: string, maxPacketAmount: bigint, minExchangeRate: number, lowEstimatedExchangeRate: number, highEstimatedExchangeRate: number, createdAt: string, expiresAt: string, sendAmount: { __typename?: 'Amount', value: bigint, assetCode: string, assetScale: number }, receiveAmount: { __typename?: 'Amount', value: bigint, assetCode: string, assetScale: number } } | null }; + +export type CreateReceiverMutationVariables = Exact<{ + input: CreateReceiverInput; +}>; + + +export type CreateReceiverMutation = { __typename?: 'Mutation', createReceiver: { __typename?: 'CreateReceiverResponse', code: string, message?: string | null, success: boolean, receiver?: { __typename?: 'Receiver', createdAt: string, metadata?: any | null, expiresAt?: string | null, id: string, paymentPointerUrl: string, incomingAmount?: { __typename?: 'Amount', assetCode: string, assetScale: number, value: bigint } | null, receivedAmount: { __typename?: 'Amount', assetCode: string, assetScale: number, value: bigint } } | null } }; diff --git a/packages/wallet/backend/src/rafiki/backend/request/receiver.request.ts b/packages/wallet/backend/src/rafiki/backend/request/receiver.request.ts new file mode 100644 index 000000000..db623ef15 --- /dev/null +++ b/packages/wallet/backend/src/rafiki/backend/request/receiver.request.ts @@ -0,0 +1,28 @@ +import { gql } from 'graphql-request' + +export const createReceiverMutation = gql` + mutation CreateReceiverMutation($input: CreateReceiverInput!) { + createReceiver(input: $input) { + code + message + receiver { + createdAt + metadata + expiresAt + id + incomingAmount { + assetCode + assetScale + value + } + paymentPointerUrl + receivedAmount { + assetCode + assetScale + value + } + } + success + } + } +` diff --git a/packages/wallet/backend/src/rafiki/controller.ts b/packages/wallet/backend/src/rafiki/controller.ts index 3b81ad2f5..083080014 100644 --- a/packages/wallet/backend/src/rafiki/controller.ts +++ b/packages/wallet/backend/src/rafiki/controller.ts @@ -3,7 +3,7 @@ import { Logger } from 'winston' import { RatesResponse, RatesService } from '../rates/service' import { validate } from '../shared/validate' import { Quote, RafikiService } from './service' -import { quoteSchmea, ratesSchema, webhookSchema } from './validation' +import { quoteSchema, ratesSchema, webhookSchema } from './validation' interface IRafikiController { createQuote: ( @@ -27,7 +27,7 @@ export class RafikiController implements IRafikiController { constructor(private deps: RafikiControllerDependencies) {} createQuote = async (req: Request, res: Response, next: NextFunction) => { try { - const { body } = await validate(quoteSchmea, req) + const { body } = await validate(quoteSchema, req) const result = await this.deps.rafikiService.createQuote(body) res.status(201).json(result) } catch (e) { diff --git a/packages/wallet/backend/src/rafiki/rafiki-client.ts b/packages/wallet/backend/src/rafiki/rafiki-client.ts index 83268a7af..17336e2e9 100644 --- a/packages/wallet/backend/src/rafiki/rafiki-client.ts +++ b/packages/wallet/backend/src/rafiki/rafiki-client.ts @@ -20,6 +20,9 @@ import { CreateQuoteInput, CreateQuoteMutation, CreateQuoteMutationVariables, + CreateReceiverInput, + CreateReceiverMutation, + CreateReceiverMutationVariables, DepositLiquidityMutation, DepositLiquidityMutationVariables, GetAssetQuery, @@ -33,6 +36,7 @@ import { OutgoingPayment, QueryAssetsArgs, Quote, + Receiver, RevokePaymentPointerKeyMutation, RevokePaymentPointerKeyMutationVariables, UpdatePaymentPointerInput, @@ -64,6 +68,7 @@ import { createQuoteMutation, getQuoteQuery } from './backend/request/quote.request' +import { createReceiverMutation } from '@/rafiki/backend/request/receiver.request' interface IRafikiClient { createAsset(code: string, scale: number): Promise @@ -77,14 +82,22 @@ interface RafikiClientDependencies { gqlClient: GraphQLClient } -export type CreateIncomingPaymentParams = { - paymentPointerId: string +type PaymentParams = { amount: bigint | null asset: Pick description?: string expiresAt?: Date - accountId: string } + +export type CreateIncomingPaymentParams = { + paymentPointerId: string + accountId: string +} & PaymentParams + +export type CreateReceiverParams = { + paymentPointerUrl: string +} & PaymentParams + export class RafikiClient implements IRafikiClient { constructor(private deps: RafikiClientDependencies) {} @@ -153,6 +166,39 @@ export class RafikiClient implements IRafikiClient { return paymentResponse.payment } + public async createReceiver(params: CreateReceiverParams): Promise { + const input: CreateReceiverInput = { + paymentPointerUrl: params.paymentPointerUrl, + metadata: { + description: params.description + }, + expiresAt: params.expiresAt?.toISOString(), + ...(params.amount && { + incomingAmount: { + value: params.amount, + assetCode: params.asset.code, + assetScale: params.asset.scale + } + }) + } + const { createReceiver: paymentResponse } = + await this.deps.gqlClient.request< + CreateReceiverMutation, + CreateReceiverMutationVariables + >(createReceiverMutation, { + input + }) + + if (!paymentResponse.success) { + throw new Error(paymentResponse.message ?? 'Empty result') + } + if (!paymentResponse.receiver) { + throw new Error('Unable to fetch created payment pointer') + } + + return paymentResponse.receiver as Receiver + } + public async withdrawLiqudity(eventId: string) { const response = await this.deps.gqlClient.request< WithdrawLiquidityMutation, diff --git a/packages/wallet/backend/src/rafiki/validation.ts b/packages/wallet/backend/src/rafiki/validation.ts index 3bf7c3e8d..5d3e1a2c7 100644 --- a/packages/wallet/backend/src/rafiki/validation.ts +++ b/packages/wallet/backend/src/rafiki/validation.ts @@ -18,19 +18,13 @@ export const webhookSchema = z.object({ }) }) -export const amountSchema = z.object({ - value: z.bigint(), - assetCode: z.string(), - assetScale: z.number() -}) - const quoteAmountSchema = z.object({ value: z.coerce.bigint(), assetCode: z.string(), assetScale: z.number() }) -export const quoteSchmea = z.object({ +export const quoteSchema = z.object({ body: z.object({ id: z.string(), paymentType: z.nativeEnum(PaymentType),