From d3470c47fef8d1654069d3c89771f3a3f7d67012 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Wed, 12 Oct 2022 16:41:22 +0200 Subject: [PATCH 01/56] feat(open-payments-client): initial POC of open-payments-client --- packages/open-payments-client/package.json | 23 + packages/open-payments-client/src/index.ts | 76 +++ packages/open-payments-client/src/types.ts | 708 ++++++++++++++++++++ packages/open-payments-client/tsconfig.json | 11 + 4 files changed, 818 insertions(+) create mode 100644 packages/open-payments-client/package.json create mode 100644 packages/open-payments-client/src/index.ts create mode 100644 packages/open-payments-client/src/types.ts create mode 100644 packages/open-payments-client/tsconfig.json diff --git a/packages/open-payments-client/package.json b/packages/open-payments-client/package.json new file mode 100644 index 0000000000..f99a55f866 --- /dev/null +++ b/packages/open-payments-client/package.json @@ -0,0 +1,23 @@ +{ + "name": "open-payments-client", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "pnpm clean && tsc --build tsconfig.json", + "clean": "rm -fr dist/", + "prepack": "pnpm build", + "generate": "npx openapi-typescript https://raw.githubusercontent.com/interledger/open-payments/main/open-api-spec.yaml --output src/types.ts" + }, + "devDependencies": { + "openapi-typescript-codegen": "^0.23.0", + "typescript": "^4.2.4" + }, + "dependencies": { + "axios": "^1.1.2" + } +} diff --git a/packages/open-payments-client/src/index.ts b/packages/open-payments-client/src/index.ts new file mode 100644 index 0000000000..0ef80025b0 --- /dev/null +++ b/packages/open-payments-client/src/index.ts @@ -0,0 +1,76 @@ +import { components } from './types' +import axios, { AxiosInstance } from 'axios' + +type IncomingPayment = components['schemas']['incoming-payment'] + +interface CreateOpenPaymentClientArgs { + timeout?: number +} + +interface GetArgs { + url: string + accessToken: string +} + +interface OpenPaymentsClient { + incomingPayment: { + get(args: GetArgs): Promise + } +} + +const get = async (axios: AxiosInstance, args: GetArgs): Promise => { + const { url, accessToken } = args + + const headers = { + 'Content-Type': 'application/json' + } + + if (accessToken) { + headers['Authorization'] = `GNAP ${accessToken}` + // TODO: https://github.com/interledger/rafiki/issues/587 + headers['Signature'] = 'TODO' + headers['Signature-Input'] = 'TODO' + } + + const { data } = await axios.get(url, { + headers + }) + + return data +} + +const getIncomingPayment = async ( + axios: AxiosInstance, + args: GetArgs +): Promise => { + return get(axios, args) +} + +const createClient = ( + args: CreateOpenPaymentClientArgs +): OpenPaymentsClient => { + const { timeout } = args + + const axiosInstance = axios.create({ + timeout + }) + + return { + incomingPayment: { + get: (getArgs: GetArgs) => getIncomingPayment(axiosInstance, getArgs) + } + } +} + +const openPaymentsClient = createClient({ + timeout: 5_000 +}) + +const incomingPayment = await openPaymentsClient.incomingPayment.get({ + url: '', + accessToken: '' +}) + + +incomingPayment. +export { IncomingPayment, OpenPaymentsClient, createClient } diff --git a/packages/open-payments-client/src/types.ts b/packages/open-payments-client/src/types.ts new file mode 100644 index 0000000000..30fa869425 --- /dev/null +++ b/packages/open-payments-client/src/types.ts @@ -0,0 +1,708 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/": { + /** + * Retrieve the public information of the Payment Pointer. + * + * This end-point should be open to anonymous requests as it allows clients to verify a Payment Pointer URL and get the basic information required to construct new transactions and discover the grant request URL. + * + * The content should be slow changing and cacheable for long periods. Servers SHOULD use cache control headers. + */ + get: operations["get-payment-pointer"]; + }; + "/connections/{id}": { + /** + * *NB* Use server url specific to this path. + * + * Fetch new connection credentials for an ILP STREAM connection. + * + * A connection is an ephemeral resource that is created to accommodate new incoming payments. + * + * A new set of credential will be generated each time this API is called. + */ + get: operations["get-ilp-stream-connection"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; + "/incoming-payments": { + /** List all incoming payments on the payment pointer */ + get: operations["list-incoming-payments"]; + /** + * A client MUST create an **incoming payment** resource before it is possible to send any payments to the payment pointer. + * + * All of the input parameters are _optional_. + */ + post: operations["create-incoming-payment"]; + }; + /** Create a new outgoing payment at the payment pointer. */ + "/outgoing-payments": { + /** List all outgoing payments on the payment pointer */ + get: operations["list-outgoing-payments"]; + /** An **outgoing payment** is a sub-resource of a payment pointer. It represents a payment from the payment pointer. */ + post: operations["create-outgoing-payment"]; + }; + /** Create a new quote at the payment pointer. */ + "/quotes": { + /** A **quote** is a sub-resource of a payment pointer. It represents a quote for a payment from the payment pointer. */ + post: operations["create-quote"]; + }; + "/incoming-payments/{id}": { + /** A client can fetch the latest state of an incoming payment to determine the amount received into the payment pointer. */ + get: operations["get-incoming-payment"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; + "/incoming-payments/{id}/complete": { + /** + * A client with the appropriate permissions MAY mark a non-expired **incoming payment** as `completed` if it has not yet received `incomingAmount`. + * + * This indicates to the receiving account provider that it can begin any post processing of the payment such as generating account statements or notifying the account holder of the completed payment. + */ + post: operations["complete-incoming-payment"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; + "/outgoing-payments/{id}": { + /** A client can fetch the latest state of an outgoing payment. */ + get: operations["get-outgoing-payment"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; + "/quotes/{id}": { + /** A client can fetch the latest state of a quote. */ + get: operations["get-quote"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; +} + +export interface components { + schemas: { + /** + * Payment Pointer + * @description A **payment pointer** resource is the root of the API and contains the public details of the financial account represented by the Payment Pointer that is also the service endpoint URL. + */ + "payment-pointer": { + /** + * Format: uri + * @description The URL identifying the incoming payment. + */ + id: string; + /** @description A public name for the account. This should be set by the account holder with their provider to provide a hint to counterparties as to the identity of the account holder. */ + publicName?: string; + /** @description The asset code of the account. */ + assetCode: components["schemas"]["assetCode"]; + assetScale: components["schemas"]["assetScale"]; + /** + * Format: uri + * @description The URL of the authorization server endpoint for getting grants and access tokens for this payment pointer. + */ + authServer: string; + }; + /** + * ILP Stream Connection + * @description An **ILP STREAM Connection** is an endpoint that returns unique STREAM connection credentials to establish a STREAM connection to the underlying account. + */ + "ilp-stream-connection": { + /** + * Format: uri + * @description The URL identifying the endpoint. + */ + id: string; + /** @description The ILP address to use when establishing a STREAM connection. */ + ilpAddress: string; + /** @description The base64 url-encoded shared secret to use when establishing a STREAM connection. */ + sharedSecret: string; + /** @description The asset code of the amount. */ + assetCode: components["schemas"]["assetCode"]; + /** @description The scale of the amount. */ + assetScale: components["schemas"]["assetScale"]; + }; + /** + * Incoming Payment + * @description An **incoming payment** resource represents a payment that will be, is currently being, or has been received by the account. + */ + "incoming-payment": { + /** + * Format: uri + * @description The URL identifying the incoming payment. + */ + id: string; + /** + * Format: uri + * @description The URL of the payment pointer this payment is being made into. + */ + paymentPointer: string; + /** + * @description Describes whether the incoming payment has completed receiving fund. + * @default false + */ + completed: boolean; + /** @description The maximum amount that should be paid into the payment pointer under this incoming payment. */ + incomingAmount?: components["schemas"]["amount"]; + /** @description The total amount that has been paid into the payment pointer under this incoming payment. */ + receivedAmount: components["schemas"]["amount"]; + /** + * Format: date-time + * @description The date and time when payments under this incoming payment will no longer be accepted. + */ + expiresAt?: string; + /** @description Human readable description of the incoming payment that will be visible to the account holder. */ + description?: string; + /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. */ + externalRef?: string; + /** + * Format: date-time + * @description The date and time when the incoming payment was created. + */ + createdAt: string; + /** + * Format: date-time + * @description The date and time when the incoming payment was updated. + */ + updatedAt: string; + }; + /** + * Incoming Payment with Connection + * @description An **incoming payment** resource with the Interledger STREAM Connection to use to pay into the payment pointer under this incoming payment. + */ + "incoming-payment-with-connection": components["schemas"]["incoming-payment"] & { + ilpStreamConnection?: components["schemas"]["ilp-stream-connection"]; + }; + /** + * Incoming Payment with Connection + * @description An **incoming payment** resource with the url for the Interledger STREAM Connection resource to use to pay into the payment pointer under this incoming payment. + */ + "incoming-payment-with-connection-url": components["schemas"]["incoming-payment"] & { + /** + * Format: uri + * @description Endpoint that returns unique STREAM connection credentials to establish a STREAM connection to the underlying account. + */ + ilpStreamConnection?: string; + }; + /** + * Outgoing Payment + * @description An **outgoing payment** resource represents a payment that will be, is currently being, or has previously been, sent from the payment pointer. + */ + "outgoing-payment": { + /** + * Format: uri + * @description The URL identifying the outgoing payment. + */ + id: string; + /** + * Format: uri + * @description The URL of the payment pointer from which this payment is sent. + */ + paymentPointer: string; + /** + * Format: uri + * @description The URL of the quote defining this payment's amounts. + */ + quoteId?: string; + /** + * @description Describes whether the payment failed to send its full amount. + * @default false + */ + failed?: boolean; + /** @description The URL of the incoming payment or ILP STREAM Connection that is being paid. */ + receiver: components["schemas"]["receiver"]; + /** @description The total amount that should be received by the receiver when this outgoing payment has been paid. */ + receiveAmount: components["schemas"]["amount"]; + /** @description The total amount that should be sent when this outgoing payment has been paid. */ + sendAmount: components["schemas"]["amount"]; + /** @description The total amount that has been sent under this outgoing payment. */ + sentAmount: components["schemas"]["amount"]; + /** @description Human readable description of the outgoing payment that will be visible to the account holder and shared with the receiver. */ + description?: string; + /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ + externalRef?: string; + /** + * Format: date-time + * @description The date and time when the outgoing payment was created. + */ + createdAt: string; + /** + * Format: date-time + * @description The date and time when the outgoing payment was updated. + */ + updatedAt: string; + }; + /** + * Quote + * @description A **quote** resource represents the quoted amount details with which an Outgoing Payment may be created. + */ + quote: { + /** + * Format: uri + * @description The URL identifying the quote. + */ + id: string; + /** + * Format: uri + * @description The URL of the payment pointer from which this quote's payment would be sent. + */ + paymentPointer: string; + /** @description The URL of the incoming payment or ILP Stream Connection that would be paid. */ + receiver: components["schemas"]["receiver"]; + /** @description The total amount that should be received by the receiver. */ + receiveAmount: components["schemas"]["amount"]; + /** @description The total amount that should be sent by the sender. */ + sendAmount: components["schemas"]["amount"]; + /** @description The date and time when the calculated `sendAmount` is no longer valid. */ + expiresAt?: string; + /** + * Format: date-time + * @description The date and time when the quote was created. + */ + createdAt: string; + }; + /** + * Amount + * @description All amounts in open payments are represented as a value and an asset code and scale. + * + * The `value` is an unsigned 64-bit integer amount, represented as a string. + * + * The `assetCode` is a code that indicates the underlying asset. In most cases this SHOULD be a 3-character ISO 4217 currency code. + * + * The `assetScale` indicates how the `value` has been scaled relative to the natural scale of the asset. For example, an `value` of `"1234"` with an `assetScale` of `2` represents an amount of 12.34. + */ + amount: { + /** + * Format: uint64 + * @description The amount, scaled by the given scale. + */ + value: string; + /** @description The asset code of the amount. */ + assetCode: components["schemas"]["assetCode"]; + /** @description The scale of the amount. */ + assetScale: components["schemas"]["assetScale"]; + }; + /** + * Asset code + * @description This SHOULD be an ISO4217 currency code. + */ + assetCode: string; + /** + * Asset scale + * @description The scale of amounts denoted in the corresponding asset code. + */ + assetScale: number; + /** + * Receiver + * Format: uri + * @description The URL of the incoming payment or ILP STREAM connection that is being paid. + */ + receiver: string; + /** @description Pagination parameters */ + pagination: + | components["schemas"]["forward-pagination"] + | components["schemas"]["backward-pagination"]; + /** @description Forward pagination parameters */ + "forward-pagination": { + /** @description The number of items to return. */ + first?: number; + /** @description The cursor key to list from. */ + cursor?: string; + }; + /** @description Backward pagination parameters */ + "backward-pagination": { + /** @description The number of items to return. */ + last?: number; + /** @description The cursor key to list from. */ + cursor: string; + }; + "page-info": { + /** @description Cursor corresponding to the first element in the result array. */ + startCursor: string; + /** @description Cursor corresponding to the last element in the result array. */ + endCursor: string; + /** @description Describes whether the data set has further entries. */ + hasNextPage: boolean; + /** @description Describes whether the data set has previous entries. */ + hasPreviousPage: boolean; + }; + }; + responses: { + /** Authorization required */ + 401: unknown; + /** Forbidden */ + 403: unknown; + }; + parameters: { + /** @description Sub-resource identifier */ + id: string; + /** @description The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + signature: string; + /** @description The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "signature-input": string; + }; +} + +export interface operations { + /** + * Retrieve the public information of the Payment Pointer. + * + * This end-point should be open to anonymous requests as it allows clients to verify a Payment Pointer URL and get the basic information required to construct new transactions and discover the grant request URL. + * + * The content should be slow changing and cacheable for long periods. Servers SHOULD use cache control headers. + */ + "get-payment-pointer": { + responses: { + /** Payment Pointer Found */ + 200: { + content: { + "application/json": components["schemas"]["payment-pointer"]; + }; + }; + /** Payment Pointer Not Found */ + 404: unknown; + }; + }; + /** + * *NB* Use server url specific to this path. + * + * Fetch new connection credentials for an ILP STREAM connection. + * + * A connection is an ephemeral resource that is created to accommodate new incoming payments. + * + * A new set of credential will be generated each time this API is called. + */ + "get-ilp-stream-connection": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + responses: { + /** Connection Found */ + 200: { + content: { + "application/json": components["schemas"]["ilp-stream-connection"]; + }; + }; + /** Connection Not Found */ + 404: unknown; + }; + }; + /** List all incoming payments on the payment pointer */ + "list-incoming-payments": { + parameters: { + query: { + /** Pagination parameters */ + pagination?: components["schemas"]["pagination"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + pagination?: components["schemas"]["page-info"]; + result?: components["schemas"]["incoming-payment-with-connection-url"][]; + }; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + }; + /** + * A client MUST create an **incoming payment** resource before it is possible to send any payments to the payment pointer. + * + * All of the input parameters are _optional_. + */ + "create-incoming-payment": { + parameters: { + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Incoming Payment Created */ + 201: { + content: { + "application/json": components["schemas"]["incoming-payment-with-connection"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + /** + * A subset of the incoming payments schema is accepted as input to create a new incoming payment. + * + * The `incomingAmount` must use the same `assetCode` and `assetScale` as the payment pointer. + */ + requestBody: { + content: { + "application/json": { + /** @description The maximum amount that should be paid into the payment pointer under this incoming payment. */ + incomingAmount?: components["schemas"]["amount"]; + /** + * Format: date-time + * @description The date and time when payments into the incoming payment must no longer be accepted. + */ + expiresAt?: string; + /** @description Human readable description of the incoming payment that will be visible to the account holder. */ + description?: string; + /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ + externalRef?: string; + }; + }; + }; + }; + /** List all outgoing payments on the payment pointer */ + "list-outgoing-payments": { + parameters: { + query: { + /** Pagination parameters */ + pagination?: components["schemas"]["pagination"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + pagination?: components["schemas"]["page-info"]; + result?: components["schemas"]["outgoing-payment"][]; + }; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + }; + /** An **outgoing payment** is a sub-resource of a payment pointer. It represents a payment from the payment pointer. */ + "create-outgoing-payment": { + parameters: { + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Outgoing Payment Created */ + 201: { + content: { + "application/json": components["schemas"]["outgoing-payment"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + /** + * A subset of the outgoing payments schema is accepted as input to create a new outgoing payment. + * + * The `sendAmount` must use the same `assetCode` and `assetScale` as the payment pointer. + */ + requestBody: { + content: { + "application/json": { + /** + * Format: uri + * @description The URL of the quote defining this payment's amounts. + */ + quoteId: string; + /** @description Human readable description of the outgoing payment that will be visible to the account holder and shared with the receiver. */ + description?: string; + /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ + externalRef?: string; + }; + }; + }; + }; + /** A **quote** is a sub-resource of a payment pointer. It represents a quote for a payment from the payment pointer. */ + "create-quote": { + parameters: { + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Quote Created */ + 201: { + content: { + "application/json": components["schemas"]["quote"]; + }; + }; + /** No amount was provided and no amount could be inferred from the receiver. */ + 400: unknown; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + /** + * A subset of the quotes schema is accepted as input to create a new quote. + * + * The quote must be created with a (`sendAmount` xor `receiveAmount`) unless the `receiver` is an Incoming Payment which has an `incomingAmount`. + */ + requestBody: { + content: { + "application/json": { + receiver: components["schemas"]["receiver"]; + receiveAmount?: components["schemas"]["amount"]; + sendAmount?: components["schemas"]["amount"]; + }; + }; + }; + }; + /** A client can fetch the latest state of an incoming payment to determine the amount received into the payment pointer. */ + "get-incoming-payment": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Incoming Payment Found */ + 200: { + content: { + "application/json": components["schemas"]["incoming-payment-with-connection"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + /** Incoming Payment Not Found */ + 404: unknown; + }; + }; + /** + * A client with the appropriate permissions MAY mark a non-expired **incoming payment** as `completed` if it has not yet received `incomingAmount`. + * + * This indicates to the receiving account provider that it can begin any post processing of the payment such as generating account statements or notifying the account holder of the completed payment. + */ + "complete-incoming-payment": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["incoming-payment"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + /** Incoming Payment Not Found */ + 404: unknown; + }; + }; + /** A client can fetch the latest state of an outgoing payment. */ + "get-outgoing-payment": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Outgoing Payment Found */ + 200: { + content: { + "application/json": components["schemas"]["outgoing-payment"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + /** Outgoing Payment Not Found */ + 404: unknown; + }; + }; + /** A client can fetch the latest state of a quote. */ + "get-quote": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Quote Found */ + 200: { + content: { + "application/json": components["schemas"]["quote"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + /** Quote Not Found */ + 404: unknown; + }; + }; +} + +export interface external {} diff --git a/packages/open-payments-client/tsconfig.json b/packages/open-payments-client/tsconfig.json new file mode 100644 index 0000000000..c7e406b567 --- /dev/null +++ b/packages/open-payments-client/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts"] +} From 7f968c3c71bfb24a97636ec6d33a53bf0e8d58b4 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Wed, 12 Oct 2022 16:48:49 +0200 Subject: [PATCH 02/56] feat(open-payments-client): cleanup --- packages/open-payments-client/src/index.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/open-payments-client/src/index.ts b/packages/open-payments-client/src/index.ts index 0ef80025b0..cb95ab0299 100644 --- a/packages/open-payments-client/src/index.ts +++ b/packages/open-payments-client/src/index.ts @@ -27,7 +27,6 @@ const get = async (axios: AxiosInstance, args: GetArgs): Promise => { if (accessToken) { headers['Authorization'] = `GNAP ${accessToken}` - // TODO: https://github.com/interledger/rafiki/issues/587 headers['Signature'] = 'TODO' headers['Signature-Input'] = 'TODO' } @@ -62,15 +61,4 @@ const createClient = ( } } -const openPaymentsClient = createClient({ - timeout: 5_000 -}) - -const incomingPayment = await openPaymentsClient.incomingPayment.get({ - url: '', - accessToken: '' -}) - - -incomingPayment. export { IncomingPayment, OpenPaymentsClient, createClient } From 79e7cd7cf8dc30f1988c30fe9c317cc9f987649a Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Thu, 13 Oct 2022 17:54:16 +0200 Subject: [PATCH 03/56] feat(open-payments): remove open-payments-client folder --- packages/open-payments-client/package.json | 23 - packages/open-payments-client/src/index.ts | 64 -- packages/open-payments-client/src/types.ts | 708 -------------------- packages/open-payments-client/tsconfig.json | 11 - 4 files changed, 806 deletions(-) delete mode 100644 packages/open-payments-client/package.json delete mode 100644 packages/open-payments-client/src/index.ts delete mode 100644 packages/open-payments-client/src/types.ts delete mode 100644 packages/open-payments-client/tsconfig.json diff --git a/packages/open-payments-client/package.json b/packages/open-payments-client/package.json deleted file mode 100644 index f99a55f866..0000000000 --- a/packages/open-payments-client/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "open-payments-client", - "version": "1.0.0", - "description": "", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "pnpm clean && tsc --build tsconfig.json", - "clean": "rm -fr dist/", - "prepack": "pnpm build", - "generate": "npx openapi-typescript https://raw.githubusercontent.com/interledger/open-payments/main/open-api-spec.yaml --output src/types.ts" - }, - "devDependencies": { - "openapi-typescript-codegen": "^0.23.0", - "typescript": "^4.2.4" - }, - "dependencies": { - "axios": "^1.1.2" - } -} diff --git a/packages/open-payments-client/src/index.ts b/packages/open-payments-client/src/index.ts deleted file mode 100644 index cb95ab0299..0000000000 --- a/packages/open-payments-client/src/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { components } from './types' -import axios, { AxiosInstance } from 'axios' - -type IncomingPayment = components['schemas']['incoming-payment'] - -interface CreateOpenPaymentClientArgs { - timeout?: number -} - -interface GetArgs { - url: string - accessToken: string -} - -interface OpenPaymentsClient { - incomingPayment: { - get(args: GetArgs): Promise - } -} - -const get = async (axios: AxiosInstance, args: GetArgs): Promise => { - const { url, accessToken } = args - - const headers = { - 'Content-Type': 'application/json' - } - - if (accessToken) { - headers['Authorization'] = `GNAP ${accessToken}` - headers['Signature'] = 'TODO' - headers['Signature-Input'] = 'TODO' - } - - const { data } = await axios.get(url, { - headers - }) - - return data -} - -const getIncomingPayment = async ( - axios: AxiosInstance, - args: GetArgs -): Promise => { - return get(axios, args) -} - -const createClient = ( - args: CreateOpenPaymentClientArgs -): OpenPaymentsClient => { - const { timeout } = args - - const axiosInstance = axios.create({ - timeout - }) - - return { - incomingPayment: { - get: (getArgs: GetArgs) => getIncomingPayment(axiosInstance, getArgs) - } - } -} - -export { IncomingPayment, OpenPaymentsClient, createClient } diff --git a/packages/open-payments-client/src/types.ts b/packages/open-payments-client/src/types.ts deleted file mode 100644 index 30fa869425..0000000000 --- a/packages/open-payments-client/src/types.ts +++ /dev/null @@ -1,708 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - "/": { - /** - * Retrieve the public information of the Payment Pointer. - * - * This end-point should be open to anonymous requests as it allows clients to verify a Payment Pointer URL and get the basic information required to construct new transactions and discover the grant request URL. - * - * The content should be slow changing and cacheable for long periods. Servers SHOULD use cache control headers. - */ - get: operations["get-payment-pointer"]; - }; - "/connections/{id}": { - /** - * *NB* Use server url specific to this path. - * - * Fetch new connection credentials for an ILP STREAM connection. - * - * A connection is an ephemeral resource that is created to accommodate new incoming payments. - * - * A new set of credential will be generated each time this API is called. - */ - get: operations["get-ilp-stream-connection"]; - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - }; - }; - "/incoming-payments": { - /** List all incoming payments on the payment pointer */ - get: operations["list-incoming-payments"]; - /** - * A client MUST create an **incoming payment** resource before it is possible to send any payments to the payment pointer. - * - * All of the input parameters are _optional_. - */ - post: operations["create-incoming-payment"]; - }; - /** Create a new outgoing payment at the payment pointer. */ - "/outgoing-payments": { - /** List all outgoing payments on the payment pointer */ - get: operations["list-outgoing-payments"]; - /** An **outgoing payment** is a sub-resource of a payment pointer. It represents a payment from the payment pointer. */ - post: operations["create-outgoing-payment"]; - }; - /** Create a new quote at the payment pointer. */ - "/quotes": { - /** A **quote** is a sub-resource of a payment pointer. It represents a quote for a payment from the payment pointer. */ - post: operations["create-quote"]; - }; - "/incoming-payments/{id}": { - /** A client can fetch the latest state of an incoming payment to determine the amount received into the payment pointer. */ - get: operations["get-incoming-payment"]; - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - }; - }; - "/incoming-payments/{id}/complete": { - /** - * A client with the appropriate permissions MAY mark a non-expired **incoming payment** as `completed` if it has not yet received `incomingAmount`. - * - * This indicates to the receiving account provider that it can begin any post processing of the payment such as generating account statements or notifying the account holder of the completed payment. - */ - post: operations["complete-incoming-payment"]; - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - }; - }; - "/outgoing-payments/{id}": { - /** A client can fetch the latest state of an outgoing payment. */ - get: operations["get-outgoing-payment"]; - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - }; - }; - "/quotes/{id}": { - /** A client can fetch the latest state of a quote. */ - get: operations["get-quote"]; - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - }; - }; -} - -export interface components { - schemas: { - /** - * Payment Pointer - * @description A **payment pointer** resource is the root of the API and contains the public details of the financial account represented by the Payment Pointer that is also the service endpoint URL. - */ - "payment-pointer": { - /** - * Format: uri - * @description The URL identifying the incoming payment. - */ - id: string; - /** @description A public name for the account. This should be set by the account holder with their provider to provide a hint to counterparties as to the identity of the account holder. */ - publicName?: string; - /** @description The asset code of the account. */ - assetCode: components["schemas"]["assetCode"]; - assetScale: components["schemas"]["assetScale"]; - /** - * Format: uri - * @description The URL of the authorization server endpoint for getting grants and access tokens for this payment pointer. - */ - authServer: string; - }; - /** - * ILP Stream Connection - * @description An **ILP STREAM Connection** is an endpoint that returns unique STREAM connection credentials to establish a STREAM connection to the underlying account. - */ - "ilp-stream-connection": { - /** - * Format: uri - * @description The URL identifying the endpoint. - */ - id: string; - /** @description The ILP address to use when establishing a STREAM connection. */ - ilpAddress: string; - /** @description The base64 url-encoded shared secret to use when establishing a STREAM connection. */ - sharedSecret: string; - /** @description The asset code of the amount. */ - assetCode: components["schemas"]["assetCode"]; - /** @description The scale of the amount. */ - assetScale: components["schemas"]["assetScale"]; - }; - /** - * Incoming Payment - * @description An **incoming payment** resource represents a payment that will be, is currently being, or has been received by the account. - */ - "incoming-payment": { - /** - * Format: uri - * @description The URL identifying the incoming payment. - */ - id: string; - /** - * Format: uri - * @description The URL of the payment pointer this payment is being made into. - */ - paymentPointer: string; - /** - * @description Describes whether the incoming payment has completed receiving fund. - * @default false - */ - completed: boolean; - /** @description The maximum amount that should be paid into the payment pointer under this incoming payment. */ - incomingAmount?: components["schemas"]["amount"]; - /** @description The total amount that has been paid into the payment pointer under this incoming payment. */ - receivedAmount: components["schemas"]["amount"]; - /** - * Format: date-time - * @description The date and time when payments under this incoming payment will no longer be accepted. - */ - expiresAt?: string; - /** @description Human readable description of the incoming payment that will be visible to the account holder. */ - description?: string; - /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. */ - externalRef?: string; - /** - * Format: date-time - * @description The date and time when the incoming payment was created. - */ - createdAt: string; - /** - * Format: date-time - * @description The date and time when the incoming payment was updated. - */ - updatedAt: string; - }; - /** - * Incoming Payment with Connection - * @description An **incoming payment** resource with the Interledger STREAM Connection to use to pay into the payment pointer under this incoming payment. - */ - "incoming-payment-with-connection": components["schemas"]["incoming-payment"] & { - ilpStreamConnection?: components["schemas"]["ilp-stream-connection"]; - }; - /** - * Incoming Payment with Connection - * @description An **incoming payment** resource with the url for the Interledger STREAM Connection resource to use to pay into the payment pointer under this incoming payment. - */ - "incoming-payment-with-connection-url": components["schemas"]["incoming-payment"] & { - /** - * Format: uri - * @description Endpoint that returns unique STREAM connection credentials to establish a STREAM connection to the underlying account. - */ - ilpStreamConnection?: string; - }; - /** - * Outgoing Payment - * @description An **outgoing payment** resource represents a payment that will be, is currently being, or has previously been, sent from the payment pointer. - */ - "outgoing-payment": { - /** - * Format: uri - * @description The URL identifying the outgoing payment. - */ - id: string; - /** - * Format: uri - * @description The URL of the payment pointer from which this payment is sent. - */ - paymentPointer: string; - /** - * Format: uri - * @description The URL of the quote defining this payment's amounts. - */ - quoteId?: string; - /** - * @description Describes whether the payment failed to send its full amount. - * @default false - */ - failed?: boolean; - /** @description The URL of the incoming payment or ILP STREAM Connection that is being paid. */ - receiver: components["schemas"]["receiver"]; - /** @description The total amount that should be received by the receiver when this outgoing payment has been paid. */ - receiveAmount: components["schemas"]["amount"]; - /** @description The total amount that should be sent when this outgoing payment has been paid. */ - sendAmount: components["schemas"]["amount"]; - /** @description The total amount that has been sent under this outgoing payment. */ - sentAmount: components["schemas"]["amount"]; - /** @description Human readable description of the outgoing payment that will be visible to the account holder and shared with the receiver. */ - description?: string; - /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ - externalRef?: string; - /** - * Format: date-time - * @description The date and time when the outgoing payment was created. - */ - createdAt: string; - /** - * Format: date-time - * @description The date and time when the outgoing payment was updated. - */ - updatedAt: string; - }; - /** - * Quote - * @description A **quote** resource represents the quoted amount details with which an Outgoing Payment may be created. - */ - quote: { - /** - * Format: uri - * @description The URL identifying the quote. - */ - id: string; - /** - * Format: uri - * @description The URL of the payment pointer from which this quote's payment would be sent. - */ - paymentPointer: string; - /** @description The URL of the incoming payment or ILP Stream Connection that would be paid. */ - receiver: components["schemas"]["receiver"]; - /** @description The total amount that should be received by the receiver. */ - receiveAmount: components["schemas"]["amount"]; - /** @description The total amount that should be sent by the sender. */ - sendAmount: components["schemas"]["amount"]; - /** @description The date and time when the calculated `sendAmount` is no longer valid. */ - expiresAt?: string; - /** - * Format: date-time - * @description The date and time when the quote was created. - */ - createdAt: string; - }; - /** - * Amount - * @description All amounts in open payments are represented as a value and an asset code and scale. - * - * The `value` is an unsigned 64-bit integer amount, represented as a string. - * - * The `assetCode` is a code that indicates the underlying asset. In most cases this SHOULD be a 3-character ISO 4217 currency code. - * - * The `assetScale` indicates how the `value` has been scaled relative to the natural scale of the asset. For example, an `value` of `"1234"` with an `assetScale` of `2` represents an amount of 12.34. - */ - amount: { - /** - * Format: uint64 - * @description The amount, scaled by the given scale. - */ - value: string; - /** @description The asset code of the amount. */ - assetCode: components["schemas"]["assetCode"]; - /** @description The scale of the amount. */ - assetScale: components["schemas"]["assetScale"]; - }; - /** - * Asset code - * @description This SHOULD be an ISO4217 currency code. - */ - assetCode: string; - /** - * Asset scale - * @description The scale of amounts denoted in the corresponding asset code. - */ - assetScale: number; - /** - * Receiver - * Format: uri - * @description The URL of the incoming payment or ILP STREAM connection that is being paid. - */ - receiver: string; - /** @description Pagination parameters */ - pagination: - | components["schemas"]["forward-pagination"] - | components["schemas"]["backward-pagination"]; - /** @description Forward pagination parameters */ - "forward-pagination": { - /** @description The number of items to return. */ - first?: number; - /** @description The cursor key to list from. */ - cursor?: string; - }; - /** @description Backward pagination parameters */ - "backward-pagination": { - /** @description The number of items to return. */ - last?: number; - /** @description The cursor key to list from. */ - cursor: string; - }; - "page-info": { - /** @description Cursor corresponding to the first element in the result array. */ - startCursor: string; - /** @description Cursor corresponding to the last element in the result array. */ - endCursor: string; - /** @description Describes whether the data set has further entries. */ - hasNextPage: boolean; - /** @description Describes whether the data set has previous entries. */ - hasPreviousPage: boolean; - }; - }; - responses: { - /** Authorization required */ - 401: unknown; - /** Forbidden */ - 403: unknown; - }; - parameters: { - /** @description Sub-resource identifier */ - id: string; - /** @description The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - signature: string; - /** @description The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "signature-input": string; - }; -} - -export interface operations { - /** - * Retrieve the public information of the Payment Pointer. - * - * This end-point should be open to anonymous requests as it allows clients to verify a Payment Pointer URL and get the basic information required to construct new transactions and discover the grant request URL. - * - * The content should be slow changing and cacheable for long periods. Servers SHOULD use cache control headers. - */ - "get-payment-pointer": { - responses: { - /** Payment Pointer Found */ - 200: { - content: { - "application/json": components["schemas"]["payment-pointer"]; - }; - }; - /** Payment Pointer Not Found */ - 404: unknown; - }; - }; - /** - * *NB* Use server url specific to this path. - * - * Fetch new connection credentials for an ILP STREAM connection. - * - * A connection is an ephemeral resource that is created to accommodate new incoming payments. - * - * A new set of credential will be generated each time this API is called. - */ - "get-ilp-stream-connection": { - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - }; - responses: { - /** Connection Found */ - 200: { - content: { - "application/json": components["schemas"]["ilp-stream-connection"]; - }; - }; - /** Connection Not Found */ - 404: unknown; - }; - }; - /** List all incoming payments on the payment pointer */ - "list-incoming-payments": { - parameters: { - query: { - /** Pagination parameters */ - pagination?: components["schemas"]["pagination"]; - }; - header: { - /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "Signature-Input": components["parameters"]["signature-input"]; - /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - Signature: components["parameters"]["signature"]; - }; - }; - responses: { - /** OK */ - 200: { - content: { - "application/json": { - pagination?: components["schemas"]["page-info"]; - result?: components["schemas"]["incoming-payment-with-connection-url"][]; - }; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - }; - }; - /** - * A client MUST create an **incoming payment** resource before it is possible to send any payments to the payment pointer. - * - * All of the input parameters are _optional_. - */ - "create-incoming-payment": { - parameters: { - header: { - /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "Signature-Input": components["parameters"]["signature-input"]; - /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - Signature: components["parameters"]["signature"]; - }; - }; - responses: { - /** Incoming Payment Created */ - 201: { - content: { - "application/json": components["schemas"]["incoming-payment-with-connection"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - }; - /** - * A subset of the incoming payments schema is accepted as input to create a new incoming payment. - * - * The `incomingAmount` must use the same `assetCode` and `assetScale` as the payment pointer. - */ - requestBody: { - content: { - "application/json": { - /** @description The maximum amount that should be paid into the payment pointer under this incoming payment. */ - incomingAmount?: components["schemas"]["amount"]; - /** - * Format: date-time - * @description The date and time when payments into the incoming payment must no longer be accepted. - */ - expiresAt?: string; - /** @description Human readable description of the incoming payment that will be visible to the account holder. */ - description?: string; - /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ - externalRef?: string; - }; - }; - }; - }; - /** List all outgoing payments on the payment pointer */ - "list-outgoing-payments": { - parameters: { - query: { - /** Pagination parameters */ - pagination?: components["schemas"]["pagination"]; - }; - header: { - /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "Signature-Input": components["parameters"]["signature-input"]; - /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - Signature: components["parameters"]["signature"]; - }; - }; - responses: { - /** OK */ - 200: { - content: { - "application/json": { - pagination?: components["schemas"]["page-info"]; - result?: components["schemas"]["outgoing-payment"][]; - }; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - }; - }; - /** An **outgoing payment** is a sub-resource of a payment pointer. It represents a payment from the payment pointer. */ - "create-outgoing-payment": { - parameters: { - header: { - /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "Signature-Input": components["parameters"]["signature-input"]; - /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - Signature: components["parameters"]["signature"]; - }; - }; - responses: { - /** Outgoing Payment Created */ - 201: { - content: { - "application/json": components["schemas"]["outgoing-payment"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - }; - /** - * A subset of the outgoing payments schema is accepted as input to create a new outgoing payment. - * - * The `sendAmount` must use the same `assetCode` and `assetScale` as the payment pointer. - */ - requestBody: { - content: { - "application/json": { - /** - * Format: uri - * @description The URL of the quote defining this payment's amounts. - */ - quoteId: string; - /** @description Human readable description of the outgoing payment that will be visible to the account holder and shared with the receiver. */ - description?: string; - /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ - externalRef?: string; - }; - }; - }; - }; - /** A **quote** is a sub-resource of a payment pointer. It represents a quote for a payment from the payment pointer. */ - "create-quote": { - parameters: { - header: { - /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "Signature-Input": components["parameters"]["signature-input"]; - /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - Signature: components["parameters"]["signature"]; - }; - }; - responses: { - /** Quote Created */ - 201: { - content: { - "application/json": components["schemas"]["quote"]; - }; - }; - /** No amount was provided and no amount could be inferred from the receiver. */ - 400: unknown; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - }; - /** - * A subset of the quotes schema is accepted as input to create a new quote. - * - * The quote must be created with a (`sendAmount` xor `receiveAmount`) unless the `receiver` is an Incoming Payment which has an `incomingAmount`. - */ - requestBody: { - content: { - "application/json": { - receiver: components["schemas"]["receiver"]; - receiveAmount?: components["schemas"]["amount"]; - sendAmount?: components["schemas"]["amount"]; - }; - }; - }; - }; - /** A client can fetch the latest state of an incoming payment to determine the amount received into the payment pointer. */ - "get-incoming-payment": { - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - header: { - /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "Signature-Input": components["parameters"]["signature-input"]; - /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - Signature: components["parameters"]["signature"]; - }; - }; - responses: { - /** Incoming Payment Found */ - 200: { - content: { - "application/json": components["schemas"]["incoming-payment-with-connection"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - /** Incoming Payment Not Found */ - 404: unknown; - }; - }; - /** - * A client with the appropriate permissions MAY mark a non-expired **incoming payment** as `completed` if it has not yet received `incomingAmount`. - * - * This indicates to the receiving account provider that it can begin any post processing of the payment such as generating account statements or notifying the account holder of the completed payment. - */ - "complete-incoming-payment": { - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - header: { - /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "Signature-Input": components["parameters"]["signature-input"]; - /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - Signature: components["parameters"]["signature"]; - }; - }; - responses: { - /** OK */ - 200: { - content: { - "application/json": components["schemas"]["incoming-payment"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - /** Incoming Payment Not Found */ - 404: unknown; - }; - }; - /** A client can fetch the latest state of an outgoing payment. */ - "get-outgoing-payment": { - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - header: { - /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "Signature-Input": components["parameters"]["signature-input"]; - /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - Signature: components["parameters"]["signature"]; - }; - }; - responses: { - /** Outgoing Payment Found */ - 200: { - content: { - "application/json": components["schemas"]["outgoing-payment"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - /** Outgoing Payment Not Found */ - 404: unknown; - }; - }; - /** A client can fetch the latest state of a quote. */ - "get-quote": { - parameters: { - path: { - /** Sub-resource identifier */ - id: components["parameters"]["id"]; - }; - header: { - /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ - "Signature-Input": components["parameters"]["signature-input"]; - /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ - Signature: components["parameters"]["signature"]; - }; - }; - responses: { - /** Quote Found */ - 200: { - content: { - "application/json": components["schemas"]["quote"]; - }; - }; - 401: components["responses"]["401"]; - 403: components["responses"]["403"]; - /** Quote Not Found */ - 404: unknown; - }; - }; -} - -export interface external {} diff --git a/packages/open-payments-client/tsconfig.json b/packages/open-payments-client/tsconfig.json deleted file mode 100644 index c7e406b567..0000000000 --- a/packages/open-payments-client/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "lib": ["ES2020"], - "outDir": "./dist", - "rootDir": "./src", - "declaration": true - }, - "include": ["src/**/*"], - "exclude": ["**/*.test.ts"] -} From 32e2a315a5d250847e78cb909a451bf8cda14b1a Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Thu, 13 Oct 2022 17:54:40 +0200 Subject: [PATCH 04/56] feat(open-payments): make open-payments folder and use script for type generation --- packages/open-payments/config.js | 5 + packages/open-payments/package.json | 25 + .../open-payments/scripts/generate-types.js | 19 + packages/open-payments/src/client.ts | 57 ++ packages/open-payments/src/generated/types.ts | 708 ++++++++++++++++++ packages/open-payments/src/index.ts | 2 + packages/open-payments/src/types.ts | 4 + packages/open-payments/tsconfig.json | 11 + 8 files changed, 831 insertions(+) create mode 100644 packages/open-payments/config.js create mode 100644 packages/open-payments/package.json create mode 100644 packages/open-payments/scripts/generate-types.js create mode 100644 packages/open-payments/src/client.ts create mode 100644 packages/open-payments/src/generated/types.ts create mode 100644 packages/open-payments/src/index.ts create mode 100644 packages/open-payments/src/types.ts create mode 100644 packages/open-payments/tsconfig.json diff --git a/packages/open-payments/config.js b/packages/open-payments/config.js new file mode 100644 index 0000000000..f277636b12 --- /dev/null +++ b/packages/open-payments/config.js @@ -0,0 +1,5 @@ +export default { + OPEN_PAYMENTS_OPEN_API_URL: + 'https://raw.githubusercontent.com/interledger/open-payments/main/open-api-spec.yaml', + DEFAULT_REQUEST_TIMEOUT: 3_000 +} diff --git a/packages/open-payments/package.json b/packages/open-payments/package.json new file mode 100644 index 0000000000..4881c517a9 --- /dev/null +++ b/packages/open-payments/package.json @@ -0,0 +1,25 @@ +{ + "name": "open-payments", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "pnpm clean && tsc --build tsconfig.json", + "clean": "rm -fr dist/", + "prepack": "pnpm build", + "generate:types": "node scripts/generate-types.js" + }, + "devDependencies": { + "@types/node": "^18.7.12", + "openapi-typescript": "^5.4.1", + "typescript": "^4.2.4" + }, + "dependencies": { + "axios": "^1.1.2" + } +} diff --git a/packages/open-payments/scripts/generate-types.js b/packages/open-payments/scripts/generate-types.js new file mode 100644 index 0000000000..0ff22c9f2d --- /dev/null +++ b/packages/open-payments/scripts/generate-types.js @@ -0,0 +1,19 @@ +import fs from 'fs' +import openapiTS from 'openapi-typescript' +import config from '../config.js' +;(async () => { + try { + const output = await openapiTS(new URL(config.OPEN_PAYMENTS_OPEN_API_URL)) + const fileName = 'src/generated/types.ts' + + fs.writeFile(fileName, output, (error) => { + if (error) { + console.log(`Error when writing types to ${fileName}`, { error }) + } + }) + } catch (error) { + console.log('Error when generating types', { + error + }) + } +})() diff --git a/packages/open-payments/src/client.ts b/packages/open-payments/src/client.ts new file mode 100644 index 0000000000..83bcd264e4 --- /dev/null +++ b/packages/open-payments/src/client.ts @@ -0,0 +1,57 @@ +import { ILPStreamConnection, IncomingPayment } from './types' +import axios, { AxiosInstance } from 'axios' +import config from '../config.js' + +interface CreateOpenPaymentClientArgs { + timeout?: number +} + +interface GetArgs { + url: string + accessToken: string +} + +export interface OpenPaymentsClient { + incomingPayment: { + get(args: GetArgs): Promise + } + ilpStreamConnection: { + get(args: GetArgs): Promise + } +} + +const get = async (axios: AxiosInstance, args: GetArgs): Promise => { + const { url, accessToken } = args + + const { data } = await axios.get(url, { + headers: accessToken + ? { + Authorization: `GNAP ${accessToken}`, + Signature: 'TODO', + 'Signature-Input': 'TODO' + } + : {} + }) + + return data +} + +export const createClient = ( + args?: CreateOpenPaymentClientArgs +): OpenPaymentsClient => { + const defaultTimeout = config['DEFAULT_REQUEST_TIMEOUT'] || 3_000 + const axiosInstance = axios.create({ + timeout: args.timeout || defaultTimeout + }) + + axiosInstance.defaults.headers.common['Content-Type'] = 'application/json' + + return { + incomingPayment: { + get: (args: GetArgs) => get(axios, args) + }, + ilpStreamConnection: { + get: (args: GetArgs) => get(axios, args) + } + } +} diff --git a/packages/open-payments/src/generated/types.ts b/packages/open-payments/src/generated/types.ts new file mode 100644 index 0000000000..30fa869425 --- /dev/null +++ b/packages/open-payments/src/generated/types.ts @@ -0,0 +1,708 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/": { + /** + * Retrieve the public information of the Payment Pointer. + * + * This end-point should be open to anonymous requests as it allows clients to verify a Payment Pointer URL and get the basic information required to construct new transactions and discover the grant request URL. + * + * The content should be slow changing and cacheable for long periods. Servers SHOULD use cache control headers. + */ + get: operations["get-payment-pointer"]; + }; + "/connections/{id}": { + /** + * *NB* Use server url specific to this path. + * + * Fetch new connection credentials for an ILP STREAM connection. + * + * A connection is an ephemeral resource that is created to accommodate new incoming payments. + * + * A new set of credential will be generated each time this API is called. + */ + get: operations["get-ilp-stream-connection"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; + "/incoming-payments": { + /** List all incoming payments on the payment pointer */ + get: operations["list-incoming-payments"]; + /** + * A client MUST create an **incoming payment** resource before it is possible to send any payments to the payment pointer. + * + * All of the input parameters are _optional_. + */ + post: operations["create-incoming-payment"]; + }; + /** Create a new outgoing payment at the payment pointer. */ + "/outgoing-payments": { + /** List all outgoing payments on the payment pointer */ + get: operations["list-outgoing-payments"]; + /** An **outgoing payment** is a sub-resource of a payment pointer. It represents a payment from the payment pointer. */ + post: operations["create-outgoing-payment"]; + }; + /** Create a new quote at the payment pointer. */ + "/quotes": { + /** A **quote** is a sub-resource of a payment pointer. It represents a quote for a payment from the payment pointer. */ + post: operations["create-quote"]; + }; + "/incoming-payments/{id}": { + /** A client can fetch the latest state of an incoming payment to determine the amount received into the payment pointer. */ + get: operations["get-incoming-payment"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; + "/incoming-payments/{id}/complete": { + /** + * A client with the appropriate permissions MAY mark a non-expired **incoming payment** as `completed` if it has not yet received `incomingAmount`. + * + * This indicates to the receiving account provider that it can begin any post processing of the payment such as generating account statements or notifying the account holder of the completed payment. + */ + post: operations["complete-incoming-payment"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; + "/outgoing-payments/{id}": { + /** A client can fetch the latest state of an outgoing payment. */ + get: operations["get-outgoing-payment"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; + "/quotes/{id}": { + /** A client can fetch the latest state of a quote. */ + get: operations["get-quote"]; + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + }; +} + +export interface components { + schemas: { + /** + * Payment Pointer + * @description A **payment pointer** resource is the root of the API and contains the public details of the financial account represented by the Payment Pointer that is also the service endpoint URL. + */ + "payment-pointer": { + /** + * Format: uri + * @description The URL identifying the incoming payment. + */ + id: string; + /** @description A public name for the account. This should be set by the account holder with their provider to provide a hint to counterparties as to the identity of the account holder. */ + publicName?: string; + /** @description The asset code of the account. */ + assetCode: components["schemas"]["assetCode"]; + assetScale: components["schemas"]["assetScale"]; + /** + * Format: uri + * @description The URL of the authorization server endpoint for getting grants and access tokens for this payment pointer. + */ + authServer: string; + }; + /** + * ILP Stream Connection + * @description An **ILP STREAM Connection** is an endpoint that returns unique STREAM connection credentials to establish a STREAM connection to the underlying account. + */ + "ilp-stream-connection": { + /** + * Format: uri + * @description The URL identifying the endpoint. + */ + id: string; + /** @description The ILP address to use when establishing a STREAM connection. */ + ilpAddress: string; + /** @description The base64 url-encoded shared secret to use when establishing a STREAM connection. */ + sharedSecret: string; + /** @description The asset code of the amount. */ + assetCode: components["schemas"]["assetCode"]; + /** @description The scale of the amount. */ + assetScale: components["schemas"]["assetScale"]; + }; + /** + * Incoming Payment + * @description An **incoming payment** resource represents a payment that will be, is currently being, or has been received by the account. + */ + "incoming-payment": { + /** + * Format: uri + * @description The URL identifying the incoming payment. + */ + id: string; + /** + * Format: uri + * @description The URL of the payment pointer this payment is being made into. + */ + paymentPointer: string; + /** + * @description Describes whether the incoming payment has completed receiving fund. + * @default false + */ + completed: boolean; + /** @description The maximum amount that should be paid into the payment pointer under this incoming payment. */ + incomingAmount?: components["schemas"]["amount"]; + /** @description The total amount that has been paid into the payment pointer under this incoming payment. */ + receivedAmount: components["schemas"]["amount"]; + /** + * Format: date-time + * @description The date and time when payments under this incoming payment will no longer be accepted. + */ + expiresAt?: string; + /** @description Human readable description of the incoming payment that will be visible to the account holder. */ + description?: string; + /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. */ + externalRef?: string; + /** + * Format: date-time + * @description The date and time when the incoming payment was created. + */ + createdAt: string; + /** + * Format: date-time + * @description The date and time when the incoming payment was updated. + */ + updatedAt: string; + }; + /** + * Incoming Payment with Connection + * @description An **incoming payment** resource with the Interledger STREAM Connection to use to pay into the payment pointer under this incoming payment. + */ + "incoming-payment-with-connection": components["schemas"]["incoming-payment"] & { + ilpStreamConnection?: components["schemas"]["ilp-stream-connection"]; + }; + /** + * Incoming Payment with Connection + * @description An **incoming payment** resource with the url for the Interledger STREAM Connection resource to use to pay into the payment pointer under this incoming payment. + */ + "incoming-payment-with-connection-url": components["schemas"]["incoming-payment"] & { + /** + * Format: uri + * @description Endpoint that returns unique STREAM connection credentials to establish a STREAM connection to the underlying account. + */ + ilpStreamConnection?: string; + }; + /** + * Outgoing Payment + * @description An **outgoing payment** resource represents a payment that will be, is currently being, or has previously been, sent from the payment pointer. + */ + "outgoing-payment": { + /** + * Format: uri + * @description The URL identifying the outgoing payment. + */ + id: string; + /** + * Format: uri + * @description The URL of the payment pointer from which this payment is sent. + */ + paymentPointer: string; + /** + * Format: uri + * @description The URL of the quote defining this payment's amounts. + */ + quoteId?: string; + /** + * @description Describes whether the payment failed to send its full amount. + * @default false + */ + failed?: boolean; + /** @description The URL of the incoming payment or ILP STREAM Connection that is being paid. */ + receiver: components["schemas"]["receiver"]; + /** @description The total amount that should be received by the receiver when this outgoing payment has been paid. */ + receiveAmount: components["schemas"]["amount"]; + /** @description The total amount that should be sent when this outgoing payment has been paid. */ + sendAmount: components["schemas"]["amount"]; + /** @description The total amount that has been sent under this outgoing payment. */ + sentAmount: components["schemas"]["amount"]; + /** @description Human readable description of the outgoing payment that will be visible to the account holder and shared with the receiver. */ + description?: string; + /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ + externalRef?: string; + /** + * Format: date-time + * @description The date and time when the outgoing payment was created. + */ + createdAt: string; + /** + * Format: date-time + * @description The date and time when the outgoing payment was updated. + */ + updatedAt: string; + }; + /** + * Quote + * @description A **quote** resource represents the quoted amount details with which an Outgoing Payment may be created. + */ + quote: { + /** + * Format: uri + * @description The URL identifying the quote. + */ + id: string; + /** + * Format: uri + * @description The URL of the payment pointer from which this quote's payment would be sent. + */ + paymentPointer: string; + /** @description The URL of the incoming payment or ILP Stream Connection that would be paid. */ + receiver: components["schemas"]["receiver"]; + /** @description The total amount that should be received by the receiver. */ + receiveAmount: components["schemas"]["amount"]; + /** @description The total amount that should be sent by the sender. */ + sendAmount: components["schemas"]["amount"]; + /** @description The date and time when the calculated `sendAmount` is no longer valid. */ + expiresAt?: string; + /** + * Format: date-time + * @description The date and time when the quote was created. + */ + createdAt: string; + }; + /** + * Amount + * @description All amounts in open payments are represented as a value and an asset code and scale. + * + * The `value` is an unsigned 64-bit integer amount, represented as a string. + * + * The `assetCode` is a code that indicates the underlying asset. In most cases this SHOULD be a 3-character ISO 4217 currency code. + * + * The `assetScale` indicates how the `value` has been scaled relative to the natural scale of the asset. For example, an `value` of `"1234"` with an `assetScale` of `2` represents an amount of 12.34. + */ + amount: { + /** + * Format: uint64 + * @description The amount, scaled by the given scale. + */ + value: string; + /** @description The asset code of the amount. */ + assetCode: components["schemas"]["assetCode"]; + /** @description The scale of the amount. */ + assetScale: components["schemas"]["assetScale"]; + }; + /** + * Asset code + * @description This SHOULD be an ISO4217 currency code. + */ + assetCode: string; + /** + * Asset scale + * @description The scale of amounts denoted in the corresponding asset code. + */ + assetScale: number; + /** + * Receiver + * Format: uri + * @description The URL of the incoming payment or ILP STREAM connection that is being paid. + */ + receiver: string; + /** @description Pagination parameters */ + pagination: + | components["schemas"]["forward-pagination"] + | components["schemas"]["backward-pagination"]; + /** @description Forward pagination parameters */ + "forward-pagination": { + /** @description The number of items to return. */ + first?: number; + /** @description The cursor key to list from. */ + cursor?: string; + }; + /** @description Backward pagination parameters */ + "backward-pagination": { + /** @description The number of items to return. */ + last?: number; + /** @description The cursor key to list from. */ + cursor: string; + }; + "page-info": { + /** @description Cursor corresponding to the first element in the result array. */ + startCursor: string; + /** @description Cursor corresponding to the last element in the result array. */ + endCursor: string; + /** @description Describes whether the data set has further entries. */ + hasNextPage: boolean; + /** @description Describes whether the data set has previous entries. */ + hasPreviousPage: boolean; + }; + }; + responses: { + /** Authorization required */ + 401: unknown; + /** Forbidden */ + 403: unknown; + }; + parameters: { + /** @description Sub-resource identifier */ + id: string; + /** @description The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + signature: string; + /** @description The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "signature-input": string; + }; +} + +export interface operations { + /** + * Retrieve the public information of the Payment Pointer. + * + * This end-point should be open to anonymous requests as it allows clients to verify a Payment Pointer URL and get the basic information required to construct new transactions and discover the grant request URL. + * + * The content should be slow changing and cacheable for long periods. Servers SHOULD use cache control headers. + */ + "get-payment-pointer": { + responses: { + /** Payment Pointer Found */ + 200: { + content: { + "application/json": components["schemas"]["payment-pointer"]; + }; + }; + /** Payment Pointer Not Found */ + 404: unknown; + }; + }; + /** + * *NB* Use server url specific to this path. + * + * Fetch new connection credentials for an ILP STREAM connection. + * + * A connection is an ephemeral resource that is created to accommodate new incoming payments. + * + * A new set of credential will be generated each time this API is called. + */ + "get-ilp-stream-connection": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + }; + responses: { + /** Connection Found */ + 200: { + content: { + "application/json": components["schemas"]["ilp-stream-connection"]; + }; + }; + /** Connection Not Found */ + 404: unknown; + }; + }; + /** List all incoming payments on the payment pointer */ + "list-incoming-payments": { + parameters: { + query: { + /** Pagination parameters */ + pagination?: components["schemas"]["pagination"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + pagination?: components["schemas"]["page-info"]; + result?: components["schemas"]["incoming-payment-with-connection-url"][]; + }; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + }; + /** + * A client MUST create an **incoming payment** resource before it is possible to send any payments to the payment pointer. + * + * All of the input parameters are _optional_. + */ + "create-incoming-payment": { + parameters: { + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Incoming Payment Created */ + 201: { + content: { + "application/json": components["schemas"]["incoming-payment-with-connection"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + /** + * A subset of the incoming payments schema is accepted as input to create a new incoming payment. + * + * The `incomingAmount` must use the same `assetCode` and `assetScale` as the payment pointer. + */ + requestBody: { + content: { + "application/json": { + /** @description The maximum amount that should be paid into the payment pointer under this incoming payment. */ + incomingAmount?: components["schemas"]["amount"]; + /** + * Format: date-time + * @description The date and time when payments into the incoming payment must no longer be accepted. + */ + expiresAt?: string; + /** @description Human readable description of the incoming payment that will be visible to the account holder. */ + description?: string; + /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ + externalRef?: string; + }; + }; + }; + }; + /** List all outgoing payments on the payment pointer */ + "list-outgoing-payments": { + parameters: { + query: { + /** Pagination parameters */ + pagination?: components["schemas"]["pagination"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": { + pagination?: components["schemas"]["page-info"]; + result?: components["schemas"]["outgoing-payment"][]; + }; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + }; + /** An **outgoing payment** is a sub-resource of a payment pointer. It represents a payment from the payment pointer. */ + "create-outgoing-payment": { + parameters: { + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Outgoing Payment Created */ + 201: { + content: { + "application/json": components["schemas"]["outgoing-payment"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + /** + * A subset of the outgoing payments schema is accepted as input to create a new outgoing payment. + * + * The `sendAmount` must use the same `assetCode` and `assetScale` as the payment pointer. + */ + requestBody: { + content: { + "application/json": { + /** + * Format: uri + * @description The URL of the quote defining this payment's amounts. + */ + quoteId: string; + /** @description Human readable description of the outgoing payment that will be visible to the account holder and shared with the receiver. */ + description?: string; + /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ + externalRef?: string; + }; + }; + }; + }; + /** A **quote** is a sub-resource of a payment pointer. It represents a quote for a payment from the payment pointer. */ + "create-quote": { + parameters: { + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Quote Created */ + 201: { + content: { + "application/json": components["schemas"]["quote"]; + }; + }; + /** No amount was provided and no amount could be inferred from the receiver. */ + 400: unknown; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + }; + /** + * A subset of the quotes schema is accepted as input to create a new quote. + * + * The quote must be created with a (`sendAmount` xor `receiveAmount`) unless the `receiver` is an Incoming Payment which has an `incomingAmount`. + */ + requestBody: { + content: { + "application/json": { + receiver: components["schemas"]["receiver"]; + receiveAmount?: components["schemas"]["amount"]; + sendAmount?: components["schemas"]["amount"]; + }; + }; + }; + }; + /** A client can fetch the latest state of an incoming payment to determine the amount received into the payment pointer. */ + "get-incoming-payment": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Incoming Payment Found */ + 200: { + content: { + "application/json": components["schemas"]["incoming-payment-with-connection"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + /** Incoming Payment Not Found */ + 404: unknown; + }; + }; + /** + * A client with the appropriate permissions MAY mark a non-expired **incoming payment** as `completed` if it has not yet received `incomingAmount`. + * + * This indicates to the receiving account provider that it can begin any post processing of the payment such as generating account statements or notifying the account holder of the completed payment. + */ + "complete-incoming-payment": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["incoming-payment"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + /** Incoming Payment Not Found */ + 404: unknown; + }; + }; + /** A client can fetch the latest state of an outgoing payment. */ + "get-outgoing-payment": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Outgoing Payment Found */ + 200: { + content: { + "application/json": components["schemas"]["outgoing-payment"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + /** Outgoing Payment Not Found */ + 404: unknown; + }; + }; + /** A client can fetch the latest state of a quote. */ + "get-quote": { + parameters: { + path: { + /** Sub-resource identifier */ + id: components["parameters"]["id"]; + }; + header: { + /** The Signature-Input field is a Dictionary structured field containing the metadata for one or more message signatures generated from components within the HTTP message. Each member describes a single message signature. The member's key is the label that uniquely identifies the message signature within the context of the HTTP message. The member's value is the serialization of the covered components Inner List plus all signature metadata parameters identified by the label. The following components MUST be included: - "@method" - "@target-uri" - "authorization" When the message contains a request body, the covered components MUST also include the following: - "content-digest" The keyid parameter of the signature MUST be set to the kid value of the JWK. See [ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-4.1) for more details. */ + "Signature-Input": components["parameters"]["signature-input"]; + /** The signature generated based on the Signature-Input, using the signing algorithm specified in the "alg" field of the JWK. */ + Signature: components["parameters"]["signature"]; + }; + }; + responses: { + /** Quote Found */ + 200: { + content: { + "application/json": components["schemas"]["quote"]; + }; + }; + 401: components["responses"]["401"]; + 403: components["responses"]["403"]; + /** Quote Not Found */ + 404: unknown; + }; + }; +} + +export interface external {} diff --git a/packages/open-payments/src/index.ts b/packages/open-payments/src/index.ts new file mode 100644 index 0000000000..8e8c16c9bf --- /dev/null +++ b/packages/open-payments/src/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export { createClient, OpenPaymentsClient } from './client' diff --git a/packages/open-payments/src/types.ts b/packages/open-payments/src/types.ts new file mode 100644 index 0000000000..a282d9b169 --- /dev/null +++ b/packages/open-payments/src/types.ts @@ -0,0 +1,4 @@ +import { components } from './generated/types' + +export type IncomingPayment = components['schemas']['incoming-payment'] +export type ILPStreamConnection = components['schemas']['ilp-stream-connection'] diff --git a/packages/open-payments/tsconfig.json b/packages/open-payments/tsconfig.json new file mode 100644 index 0000000000..b6b5f0ce92 --- /dev/null +++ b/packages/open-payments/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "lib": ["ES2020"], + "outDir": "./dist", + // "rootDir": "./src", + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts"] +} From 050ebefd97e8b7181d077a2c59633b2e896abb99 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Thu, 13 Oct 2022 17:55:37 +0200 Subject: [PATCH 05/56] feat(open-payments): fix rootDir --- packages/open-payments/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/open-payments/tsconfig.json b/packages/open-payments/tsconfig.json index b6b5f0ce92..c7e406b567 100644 --- a/packages/open-payments/tsconfig.json +++ b/packages/open-payments/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "lib": ["ES2020"], "outDir": "./dist", - // "rootDir": "./src", + "rootDir": "./src", "declaration": true }, "include": ["src/**/*"], From 989302902ed8dd61afdabb56733fc09638f54eb4 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Fri, 14 Oct 2022 14:35:26 +0200 Subject: [PATCH 06/56] feat(open-payments): downgrading generation lib, updating how script is ran --- packages/open-payments/package.json | 9 ++++----- .../scripts/{generate-types.js => generate-types.ts} | 4 ++-- packages/open-payments/src/client.ts | 6 +++--- packages/open-payments/{config.js => src/config.ts} | 0 packages/open-payments/src/generated/types.ts | 10 ++-------- packages/open-payments/tsconfig.json | 2 +- 6 files changed, 12 insertions(+), 19 deletions(-) rename packages/open-payments/scripts/{generate-types.js => generate-types.ts} (77%) rename packages/open-payments/{config.js => src/config.ts} (100%) diff --git a/packages/open-payments/package.json b/packages/open-payments/package.json index 4881c517a9..ea1c7a6dae 100644 --- a/packages/open-payments/package.json +++ b/packages/open-payments/package.json @@ -1,10 +1,8 @@ { "name": "open-payments", - "version": "1.0.0", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", - "type": "module", "files": [ "dist/**/*" ], @@ -12,12 +10,13 @@ "build": "pnpm clean && tsc --build tsconfig.json", "clean": "rm -fr dist/", "prepack": "pnpm build", - "generate:types": "node scripts/generate-types.js" + "generate:types": "npx ts-node scripts/generate-types.ts" }, "devDependencies": { "@types/node": "^18.7.12", - "openapi-typescript": "^5.4.1", - "typescript": "^4.2.4" + "openapi-typescript": "^4.5.0", + "ts-node": "^10.7.0", + "typescript": "^4.3.0" }, "dependencies": { "axios": "^1.1.2" diff --git a/packages/open-payments/scripts/generate-types.js b/packages/open-payments/scripts/generate-types.ts similarity index 77% rename from packages/open-payments/scripts/generate-types.js rename to packages/open-payments/scripts/generate-types.ts index 0ff22c9f2d..27c978a180 100644 --- a/packages/open-payments/scripts/generate-types.js +++ b/packages/open-payments/scripts/generate-types.ts @@ -1,9 +1,9 @@ import fs from 'fs' import openapiTS from 'openapi-typescript' -import config from '../config.js' +import config from '../src/config' ;(async () => { try { - const output = await openapiTS(new URL(config.OPEN_PAYMENTS_OPEN_API_URL)) + const output = await openapiTS(config.OPEN_PAYMENTS_OPEN_API_URL) const fileName = 'src/generated/types.ts' fs.writeFile(fileName, output, (error) => { diff --git a/packages/open-payments/src/client.ts b/packages/open-payments/src/client.ts index 83bcd264e4..84d030d271 100644 --- a/packages/open-payments/src/client.ts +++ b/packages/open-payments/src/client.ts @@ -1,6 +1,6 @@ import { ILPStreamConnection, IncomingPayment } from './types' import axios, { AxiosInstance } from 'axios' -import config from '../config.js' +import config from './config' interface CreateOpenPaymentClientArgs { timeout?: number @@ -39,9 +39,9 @@ const get = async (axios: AxiosInstance, args: GetArgs): Promise => { export const createClient = ( args?: CreateOpenPaymentClientArgs ): OpenPaymentsClient => { - const defaultTimeout = config['DEFAULT_REQUEST_TIMEOUT'] || 3_000 + const defaultTimeout = config.DEFAULT_REQUEST_TIMEOUT || 3_000 const axiosInstance = axios.create({ - timeout: args.timeout || defaultTimeout + timeout: args.timeout ?? defaultTimeout }) axiosInstance.defaults.headers.common['Content-Type'] = 'application/json' diff --git a/packages/open-payments/config.js b/packages/open-payments/src/config.ts similarity index 100% rename from packages/open-payments/config.js rename to packages/open-payments/src/config.ts diff --git a/packages/open-payments/src/generated/types.ts b/packages/open-payments/src/generated/types.ts index 30fa869425..95973a957a 100644 --- a/packages/open-payments/src/generated/types.ts +++ b/packages/open-payments/src/generated/types.ts @@ -157,10 +157,7 @@ export interface components { * @description The URL of the payment pointer this payment is being made into. */ paymentPointer: string; - /** - * @description Describes whether the incoming payment has completed receiving fund. - * @default false - */ + /** @description Describes whether the incoming payment has completed receiving fund. */ completed: boolean; /** @description The maximum amount that should be paid into the payment pointer under this incoming payment. */ incomingAmount?: components["schemas"]["amount"]; @@ -224,10 +221,7 @@ export interface components { * @description The URL of the quote defining this payment's amounts. */ quoteId?: string; - /** - * @description Describes whether the payment failed to send its full amount. - * @default false - */ + /** @description Describes whether the payment failed to send its full amount. */ failed?: boolean; /** @description The URL of the incoming payment or ILP STREAM Connection that is being paid. */ receiver: components["schemas"]["receiver"]; diff --git a/packages/open-payments/tsconfig.json b/packages/open-payments/tsconfig.json index c7e406b567..cf98f1692c 100644 --- a/packages/open-payments/tsconfig.json +++ b/packages/open-payments/tsconfig.json @@ -7,5 +7,5 @@ "declaration": true }, "include": ["src/**/*"], - "exclude": ["**/*.test.ts"] + "exclude": ["**/*.test.ts", "src/scripts/*"] } From c15fdbc418b695aa317c6c21a893bd48675f3150 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Fri, 14 Oct 2022 14:36:49 +0200 Subject: [PATCH 07/56] feat(open-payments): update default config usage --- packages/open-payments/src/client.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/open-payments/src/client.ts b/packages/open-payments/src/client.ts index 84d030d271..0e882b8bf5 100644 --- a/packages/open-payments/src/client.ts +++ b/packages/open-payments/src/client.ts @@ -39,9 +39,8 @@ const get = async (axios: AxiosInstance, args: GetArgs): Promise => { export const createClient = ( args?: CreateOpenPaymentClientArgs ): OpenPaymentsClient => { - const defaultTimeout = config.DEFAULT_REQUEST_TIMEOUT || 3_000 const axiosInstance = axios.create({ - timeout: args.timeout ?? defaultTimeout + timeout: args.timeout ?? config.DEFAULT_REQUEST_TIMEOUT }) axiosInstance.defaults.headers.common['Content-Type'] = 'application/json' From 130e3105c1105f556a10afcad2cc7097baf53252 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Fri, 14 Oct 2022 14:47:23 +0200 Subject: [PATCH 08/56] feat(open-payments): update pnpm-lock.yaml --- pnpm-lock.yaml | 164 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 147 insertions(+), 17 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b35775d3bc..cff2e1ee27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,10 +50,10 @@ importers: '@koa/router': ^12.0.0 '@types/jest': ^28.1.7 '@types/koa': 2.13.5 - '@types/koa-bodyparser': ^4.3.7 - '@types/koa-session': ^5.10.6 '@types/koa__cors': ^3.1.1 '@types/koa__router': ^8.0.11 + '@types/koa-bodyparser': ^4.3.7 + '@types/koa-session': ^5.10.6 '@types/uuid': ^8.3.4 ajv: ^8.11.0 axios: ^0.27.2 @@ -98,10 +98,10 @@ importers: devDependencies: '@types/jest': 28.1.7 '@types/koa': 2.13.5 - '@types/koa-bodyparser': 4.3.7 - '@types/koa-session': 5.10.6 '@types/koa__cors': 3.3.0 '@types/koa__router': 8.0.11 + '@types/koa-bodyparser': 4.3.7 + '@types/koa-session': 5.10.6 '@types/uuid': 8.3.4 jest-openapi: 0.14.2 nock: 13.2.9 @@ -129,9 +129,9 @@ importers: '@types/bcrypt': ^5.0.0 '@types/jest': ^28.1.7 '@types/koa': 2.13.5 - '@types/koa-bodyparser': ^4.3.7 '@types/koa__cors': ^3.0.2 '@types/koa__router': ^8.0.11 + '@types/koa-bodyparser': ^4.3.7 '@types/lodash': ^4.14.184 '@types/luxon': ^3.0.0 '@types/react': ^18.0.17 @@ -170,6 +170,7 @@ importers: objection: ^3.0.1 objection-db-errors: ^1.1.2 oer-utils: 5.1.3-alpha.1 + open-payments: workspace:../open-payments openapi: workspace:../openapi openapi-types: ^12.0.0 pg: ^8.6.0 @@ -222,6 +223,7 @@ importers: objection: 3.0.1_knex@0.95.15 objection-db-errors: 1.1.2_objection@3.0.1 oer-utils: 5.1.3-alpha.1 + open-payments: link:../open-payments openapi: link:../openapi pg: 8.7.3 pino: 8.4.2 @@ -236,9 +238,9 @@ importers: '@graphql-codegen/typescript-resolvers': 2.7.3_graphql@16.6.0 '@types/jest': 28.1.7 '@types/koa': 2.13.5 - '@types/koa-bodyparser': 4.3.7 '@types/koa__cors': 3.3.0 '@types/koa__router': 8.0.11 + '@types/koa-bodyparser': 4.3.7 '@types/lodash': 4.14.184 '@types/luxon': 3.0.0 '@types/react': 18.0.17 @@ -305,6 +307,21 @@ importers: eslint: 8.22.0 typescript: 4.7.4 + packages/open-payments: + specifiers: + '@types/node': ^18.7.12 + axios: ^1.1.2 + openapi-typescript: ^4.5.0 + ts-node: ^10.7.0 + typescript: ^4.3.0 + dependencies: + axios: 1.1.2 + devDependencies: + '@types/node': 18.7.13 + openapi-typescript: 4.5.0 + ts-node: 10.9.1_ieummqxttktzud32hpyrer46t4 + typescript: 4.8.4 + packages/openapi: specifiers: '@apidevtools/json-schema-ref-parser': ^9.0.9 @@ -325,7 +342,7 @@ importers: dependencies: '@apidevtools/json-schema-ref-parser': 9.0.9 ajv: 8.11.0 - ajv-formats: 2.1.1_ajv@8.11.0 + ajv-formats: 2.1.1 openapi-default-setter: 12.0.0 openapi-request-coercer: 12.0.0 openapi-request-validator: 12.0.0 @@ -533,6 +550,7 @@ packages: /@babel/code-frame/7.18.6: resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} engines: {node: '>=6.9.0'} + requiresBuild: true dependencies: '@babel/highlight': 7.18.6 dev: true @@ -2169,7 +2187,7 @@ packages: '@types/node': 18.7.6 chalk: 4.1.2 cosmiconfig: 7.0.1 - cosmiconfig-typescript-loader: 2.0.2_nhjjlp4mdozido4npc63jtte4a + cosmiconfig-typescript-loader: 2.0.2_hxbm2pyqntnffeffb7yzpexbtu lodash: 4.17.21 resolve-from: 5.0.0 typescript: 4.7.4 @@ -4496,10 +4514,8 @@ packages: indent-string: 4.0.0 dev: true - /ajv-formats/2.1.1_ajv@8.11.0: + /ajv-formats/2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 peerDependenciesMeta: ajv: optional: true @@ -4658,9 +4674,9 @@ packages: '@koa/cors': 3.3.0 '@types/accepts': 1.3.5 '@types/koa': 2.13.5 + '@types/koa__cors': 3.3.0 '@types/koa-bodyparser': 4.3.7 '@types/koa-compose': 3.2.5 - '@types/koa__cors': 3.3.0 accepts: 1.3.8 apollo-server-core: 3.10.1_graphql@16.6.0 apollo-server-types: 3.6.2_graphql@16.6.0 @@ -4958,6 +4974,16 @@ packages: - debug dev: false + /axios/1.1.2: + resolution: {integrity: sha512-bznQyETwElsXl2RK7HLLwb5GPpOLlycxHCtrpDR/4RqqBzjARaOTo3jz4IgtntWUYee7Ne4S8UHd92VCuzPaWA==} + dependencies: + follow-redirects: 1.15.1 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query/2.2.0: resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==} dev: true @@ -5812,8 +5838,8 @@ packages: engines: {node: '>=10'} hasBin: true dependencies: - JSONStream: 1.3.5 is-text-path: 1.0.1 + JSONStream: 1.3.5 lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 @@ -5886,12 +5912,11 @@ packages: '@iarna/toml': 2.2.5 dev: true - /cosmiconfig-typescript-loader/2.0.2_nhjjlp4mdozido4npc63jtte4a: + /cosmiconfig-typescript-loader/2.0.2_hxbm2pyqntnffeffb7yzpexbtu: resolution: {integrity: sha512-KmE+bMjWMXJbkWCeY4FJX/npHuZPNr9XF9q9CIQ/bpFwi1qHfCmSiKarrCcRa0LO4fWjk93pVoeRtJAkTGcYNw==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@types/node': '*' - cosmiconfig: '>=7' typescript: '>=3' dependencies: '@types/node': 18.7.6 @@ -7662,6 +7687,10 @@ packages: type-fest: 0.20.2 dev: true + /globalyzer/0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + dev: true + /globby/10.0.0: resolution: {integrity: sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==} engines: {node: '>=8'} @@ -7687,6 +7716,10 @@ packages: merge2: 1.4.1 slash: 3.0.0 + /globrex/0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + dev: true + /got/11.8.5: resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==} engines: {node: '>=10.19.0'} @@ -7945,6 +7978,13 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true + /hosted-git-info/3.0.8: + resolution: {integrity: sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + /hosted-git-info/4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -9897,6 +9937,24 @@ packages: yargs-parser: 20.2.9 dev: true + /meow/9.0.0: + resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==} + engines: {node: '>=10'} + dependencies: + '@types/minimist': 1.2.2 + camelcase-keys: 6.2.2 + decamelize: 1.2.0 + decamelize-keys: 1.1.0 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + dev: true + /merge-descriptors/1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} @@ -10234,6 +10292,12 @@ packages: engines: {node: '>=4'} hasBin: true + /mime/3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: true + /mimic-fn/2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -10721,7 +10785,7 @@ packages: resolution: {integrity: sha512-z0K3fRViKbamg80RJgNLzLZik2QBvXVfHgtpK4nGlTQ7qgn06PApFzF1Arc4Fb3aAB26BFBQGzjrojT58XjA3A==} dependencies: ajv: 8.11.0 - ajv-formats: 2.1.1_ajv@8.11.0 + ajv-formats: 2.1.1 content-type: 1.0.4 openapi-jsonschema-parameters: 12.0.0 openapi-types: 12.0.0 @@ -10746,7 +10810,7 @@ packages: resolution: {integrity: sha512-5wpFKMoEbUcjiqo16jIen3Cb2+oApSnYZpWn8WQdRO2q/dNQZZl8Pz6ESwCriiyU5AK4i5ZI6+7O3bHQr6+6+g==} dependencies: ajv: 8.11.0 - ajv-formats: 2.1.1_ajv@8.11.0 + ajv-formats: 2.1.1 lodash.merge: 4.6.2 openapi-types: 9.3.1 dev: true @@ -10758,6 +10822,24 @@ packages: resolution: {integrity: sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==} dev: true + /openapi-typescript/4.5.0: + resolution: {integrity: sha512-++gWZLTKmbZP608JHMerllAs84HzULWfVjfH7otkWBLrKxUvzHMFqI6R4JSW1LoNDZnS4KKiRTZW66Fxyp6z4Q==} + engines: {node: '>= 12.0.0', npm: '>= 7.0.0'} + hasBin: true + dependencies: + hosted-git-info: 3.0.8 + js-yaml: 4.1.0 + kleur: 4.1.5 + meow: 9.0.0 + mime: 3.0.0 + node-fetch: 2.6.7 + prettier: 2.7.1 + slash: 3.0.0 + tiny-glob: 0.2.9 + transitivePeerDependencies: + - encoding + dev: true + /openapi-validator/0.14.2: resolution: {integrity: sha512-bgRQLZoxmECTjRxfpyMorad1ll58biUdV+31ALsHW2gRzdtMscI4Qm/wuhG8HsDUMGQkVLQYzUgJijNGKD65Og==} dependencies: @@ -11285,6 +11367,10 @@ packages: forwarded: 0.2.0 ipaddr.js: 1.9.1 + /proxy-from-env/1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /pump/2.0.1: resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} dependencies: @@ -12516,6 +12602,13 @@ packages: engines: {node: '>=8'} dev: false + /tiny-glob/0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + dev: true + /title-case/3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} dependencies: @@ -12737,6 +12830,37 @@ packages: yn: 3.1.1 dev: true + /ts-node/10.9.1_ieummqxttktzud32hpyrer46t4: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 18.7.13 + acorn: 8.8.0 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.8.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-node/9.1.1_typescript@4.7.4: resolution: {integrity: sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==} engines: {node: '>=10.0.0'} @@ -12863,6 +12987,12 @@ packages: hasBin: true dev: true + /typescript/4.8.4: + resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /ua-parser-js/0.7.31: resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==} dev: true From e067011f996dc2ce7f15fc9b9aedbffe55172e12 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Fri, 14 Oct 2022 14:51:01 +0200 Subject: [PATCH 09/56] feat(open-payments): update pnpm-lock.yaml --- pnpm-lock.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cff2e1ee27..dd45873785 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,7 +170,6 @@ importers: objection: ^3.0.1 objection-db-errors: ^1.1.2 oer-utils: 5.1.3-alpha.1 - open-payments: workspace:../open-payments openapi: workspace:../openapi openapi-types: ^12.0.0 pg: ^8.6.0 @@ -223,7 +222,6 @@ importers: objection: 3.0.1_knex@0.95.15 objection-db-errors: 1.1.2_objection@3.0.1 oer-utils: 5.1.3-alpha.1 - open-payments: link:../open-payments openapi: link:../openapi pg: 8.7.3 pino: 8.4.2 From dc288785bfbe9278584fe7cb36154c0b2fdca565 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Fri, 14 Oct 2022 15:28:51 +0200 Subject: [PATCH 10/56] feat(open-payments): adding tests --- packages/open-payments/jest.config.js | 17 ++++++ packages/open-payments/package.json | 4 +- packages/open-payments/src/client.test.ts | 71 +++++++++++++++++++++++ packages/open-payments/src/client.ts | 21 +++++-- 4 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 packages/open-payments/jest.config.js create mode 100644 packages/open-payments/src/client.test.ts diff --git a/packages/open-payments/jest.config.js b/packages/open-payments/jest.config.js new file mode 100644 index 0000000000..a2667d704f --- /dev/null +++ b/packages/open-payments/jest.config.js @@ -0,0 +1,17 @@ +'use strict' +// eslint-disable-next-line @typescript-eslint/no-var-requires +const baseConfig = require('../../jest.config.base.js') +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageName = require('./package.json').name + +module.exports = { + ...baseConfig, + clearMocks: true, + roots: [`/packages/${packageName}`], + testRegex: `(packages/${packageName}/.*/__tests__/.*|\\.(test|spec))\\.tsx?$`, + moduleDirectories: [`node_modules`, `packages/${packageName}/node_modules`], + modulePaths: [`/packages/${packageName}/src/`], + id: packageName, + displayName: packageName, + rootDir: '../..' +} diff --git a/packages/open-payments/package.json b/packages/open-payments/package.json index ea1c7a6dae..6e6f697c38 100644 --- a/packages/open-payments/package.json +++ b/packages/open-payments/package.json @@ -9,11 +9,13 @@ "scripts": { "build": "pnpm clean && tsc --build tsconfig.json", "clean": "rm -fr dist/", + "generate:types": "npx ts-node scripts/generate-types.ts", "prepack": "pnpm build", - "generate:types": "npx ts-node scripts/generate-types.ts" + "test": "jest --passWithNoTests" }, "devDependencies": { "@types/node": "^18.7.12", + "nock": "^13.2.9", "openapi-typescript": "^4.5.0", "ts-node": "^10.7.0", "typescript": "^4.3.0" diff --git a/packages/open-payments/src/client.test.ts b/packages/open-payments/src/client.test.ts new file mode 100644 index 0000000000..e270fbb5ea --- /dev/null +++ b/packages/open-payments/src/client.test.ts @@ -0,0 +1,71 @@ +import { createAxiosInstance, get } from './client' +import nock from 'nock' + +describe('open-payments', (): void => { + describe('createAxiosInstance', (): void => { + test('sets timeout properly', async (): Promise => { + expect(createAxiosInstance({ timeout: 1000 }).defaults.timeout).toBe(1000) + }) + test('sets Content-Type header properly', async (): Promise => { + expect( + createAxiosInstance().defaults.headers.common['Content-Type'] + ).toBe('application/json') + }) + }) + + describe('get', (): void => { + const axiosInstance = createAxiosInstance() + const baseUrl = 'http://localhost:1000' + + beforeEach(() => { + jest.spyOn(axiosInstance, 'get') + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + test('sets headers properly if accessToken provided', async (): Promise => { + nock(baseUrl) + .get('/incoming-payment') + .reply(200, () => ({ + validReceiver: 0 + })) + + await get(axiosInstance, { + url: `${baseUrl}/incoming-payment`, + accessToken: 'accessToken' + }) + + expect(axiosInstance.get).toHaveBeenCalledWith( + `${baseUrl}/incoming-payment`, + { + headers: { + Authorization: 'GNAP accessToken', + Signature: 'TODO', + 'Signature-Input': 'TODO' + } + } + ) + }) + + test('sets headers properly if accessToken is not provided', async (): Promise => { + nock(baseUrl) + .get('/incoming-payment') + .reply(200, () => ({ + validReceiver: 0 + })) + + await get(axiosInstance, { + url: `${baseUrl}/incoming-payment` + }) + + expect(axiosInstance.get).toHaveBeenCalledWith( + `${baseUrl}/incoming-payment`, + { + headers: {} + } + ) + }) + }) +}) diff --git a/packages/open-payments/src/client.ts b/packages/open-payments/src/client.ts index 0e882b8bf5..b6ff930712 100644 --- a/packages/open-payments/src/client.ts +++ b/packages/open-payments/src/client.ts @@ -8,7 +8,7 @@ interface CreateOpenPaymentClientArgs { interface GetArgs { url: string - accessToken: string + accessToken?: string } export interface OpenPaymentsClient { @@ -20,7 +20,10 @@ export interface OpenPaymentsClient { } } -const get = async (axios: AxiosInstance, args: GetArgs): Promise => { +export const get = async ( + axios: AxiosInstance, + args: GetArgs +): Promise => { const { url, accessToken } = args const { data } = await axios.get(url, { @@ -36,15 +39,23 @@ const get = async (axios: AxiosInstance, args: GetArgs): Promise => { return data } -export const createClient = ( +export const createAxiosInstance = ( args?: CreateOpenPaymentClientArgs -): OpenPaymentsClient => { +): AxiosInstance => { const axiosInstance = axios.create({ - timeout: args.timeout ?? config.DEFAULT_REQUEST_TIMEOUT + timeout: args?.timeout ?? config.DEFAULT_REQUEST_TIMEOUT }) axiosInstance.defaults.headers.common['Content-Type'] = 'application/json' + return axiosInstance +} + +export const createClient = ( + args?: CreateOpenPaymentClientArgs +): OpenPaymentsClient => { + const axios = createAxiosInstance(args) + return { incomingPayment: { get: (args: GetArgs) => get(axios, args) From 39b561b12c651bcc7585589b16ed29660ecb573a Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Fri, 14 Oct 2022 15:30:03 +0200 Subject: [PATCH 11/56] feat(open-payments): update pnpm-lock.yaml --- pnpm-lock.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd45873785..e663d96185 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -309,6 +309,7 @@ importers: specifiers: '@types/node': ^18.7.12 axios: ^1.1.2 + nock: ^13.2.9 openapi-typescript: ^4.5.0 ts-node: ^10.7.0 typescript: ^4.3.0 @@ -316,6 +317,7 @@ importers: axios: 1.1.2 devDependencies: '@types/node': 18.7.13 + nock: 13.2.9 openapi-typescript: 4.5.0 ts-node: 10.9.1_ieummqxttktzud32hpyrer46t4 typescript: 4.8.4 From 9393e47393b9b7de15812f57897b7ba9e4f6ad79 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Fri, 14 Oct 2022 15:35:36 +0200 Subject: [PATCH 12/56] feat(open-payments): simplify test --- packages/open-payments/src/client.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/open-payments/src/client.test.ts b/packages/open-payments/src/client.test.ts index e270fbb5ea..da502deaea 100644 --- a/packages/open-payments/src/client.test.ts +++ b/packages/open-payments/src/client.test.ts @@ -21,10 +21,6 @@ describe('open-payments', (): void => { jest.spyOn(axiosInstance, 'get') }) - afterEach(() => { - jest.clearAllMocks() - }) - test('sets headers properly if accessToken provided', async (): Promise => { nock(baseUrl) .get('/incoming-payment') From 3c3a0b76d997cf3ac020f3f36418c4a71ca20ef1 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Fri, 14 Oct 2022 15:42:14 +0200 Subject: [PATCH 13/56] feat(open-payments): updating workflows --- .github/labeler.yml | 2 ++ .github/workflows/lint_test_build.yml | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index 67fd85dc75..5b9ea5507c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -5,6 +5,7 @@ 'pkg: auth': packages/auth/**/* 'pkg: openapi': packages/openapi/**/* 'pkg: map': packages/mock-account-provider/**/* +'pkg: open-payments': packages/open-payments/**/* # Add 'type: documentation' label to any change to *.md files 'type: ci': @@ -22,6 +23,7 @@ - packages/frontend/src/**/* - packages/auth/src/**/* - packages/openapi/src/**/* + - packages/open-payments/src/**/* # Add 'type: tests' label to any change to *.test.ts files 'type: tests': packages/**/*.test.ts diff --git a/.github/workflows/lint_test_build.yml b/.github/workflows/lint_test_build.yml index 221296abcb..9a55454eec 100644 --- a/.github/workflows/lint_test_build.yml +++ b/.github/workflows/lint_test_build.yml @@ -57,6 +57,15 @@ jobs: - uses: ./.github/workflows/rafiki/env-setup - run: pnpm --filter openapi test + open-payments: + runs-on: ubuntu-latest + needs: checkout + timeout-minutes: 5 + steps: + - uses: actions/checkout@v2 + - uses: ./.github/workflows/rafiki/env-setup + - run: pnpm --filter open-payments test + build: runs-on: ubuntu-latest timeout-minutes: 5 @@ -65,6 +74,7 @@ jobs: - frontend - auth - openapi + - open-payments steps: - uses: actions/checkout@v2 - uses: ./.github/workflows/rafiki/env-setup From 120dcae03cc70ce8a065a602cec55b7b527b309a Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Fri, 14 Oct 2022 17:50:20 +0200 Subject: [PATCH 14/56] feat(open-payments): pin the open api spec to the most recent commit --- packages/open-payments/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/open-payments/src/config.ts b/packages/open-payments/src/config.ts index f277636b12..ed62374b63 100644 --- a/packages/open-payments/src/config.ts +++ b/packages/open-payments/src/config.ts @@ -1,5 +1,5 @@ export default { OPEN_PAYMENTS_OPEN_API_URL: - 'https://raw.githubusercontent.com/interledger/open-payments/main/open-api-spec.yaml', + 'https://raw.githubusercontent.com/interledger/open-payments/4c873dba89164decffbe84905d12f1d4ec045389/open-api-spec.yaml', DEFAULT_REQUEST_TIMEOUT: 3_000 } From 80d985617d72843412d0de9065fda6701ffd8826 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Tue, 18 Oct 2022 14:29:08 +0200 Subject: [PATCH 15/56] feat(open-payments): adding openapi validation --- packages/open-payments/src/client.test.ts | 67 ------------------- packages/open-payments/src/client.ts | 67 ------------------- .../src/client/ilp-stream-connection.ts | 19 ++++++ .../src/client/incoming-payment.ts | 19 ++++++ packages/open-payments/src/client/index.ts | 36 ++++++++++ packages/open-payments/src/client/requests.ts | 60 +++++++++++++++++ packages/open-payments/src/index.ts | 2 +- packages/open-payments/src/types.ts | 5 +- 8 files changed, 139 insertions(+), 136 deletions(-) delete mode 100644 packages/open-payments/src/client.test.ts delete mode 100644 packages/open-payments/src/client.ts create mode 100644 packages/open-payments/src/client/ilp-stream-connection.ts create mode 100644 packages/open-payments/src/client/incoming-payment.ts create mode 100644 packages/open-payments/src/client/index.ts create mode 100644 packages/open-payments/src/client/requests.ts diff --git a/packages/open-payments/src/client.test.ts b/packages/open-payments/src/client.test.ts deleted file mode 100644 index da502deaea..0000000000 --- a/packages/open-payments/src/client.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { createAxiosInstance, get } from './client' -import nock from 'nock' - -describe('open-payments', (): void => { - describe('createAxiosInstance', (): void => { - test('sets timeout properly', async (): Promise => { - expect(createAxiosInstance({ timeout: 1000 }).defaults.timeout).toBe(1000) - }) - test('sets Content-Type header properly', async (): Promise => { - expect( - createAxiosInstance().defaults.headers.common['Content-Type'] - ).toBe('application/json') - }) - }) - - describe('get', (): void => { - const axiosInstance = createAxiosInstance() - const baseUrl = 'http://localhost:1000' - - beforeEach(() => { - jest.spyOn(axiosInstance, 'get') - }) - - test('sets headers properly if accessToken provided', async (): Promise => { - nock(baseUrl) - .get('/incoming-payment') - .reply(200, () => ({ - validReceiver: 0 - })) - - await get(axiosInstance, { - url: `${baseUrl}/incoming-payment`, - accessToken: 'accessToken' - }) - - expect(axiosInstance.get).toHaveBeenCalledWith( - `${baseUrl}/incoming-payment`, - { - headers: { - Authorization: 'GNAP accessToken', - Signature: 'TODO', - 'Signature-Input': 'TODO' - } - } - ) - }) - - test('sets headers properly if accessToken is not provided', async (): Promise => { - nock(baseUrl) - .get('/incoming-payment') - .reply(200, () => ({ - validReceiver: 0 - })) - - await get(axiosInstance, { - url: `${baseUrl}/incoming-payment` - }) - - expect(axiosInstance.get).toHaveBeenCalledWith( - `${baseUrl}/incoming-payment`, - { - headers: {} - } - ) - }) - }) -}) diff --git a/packages/open-payments/src/client.ts b/packages/open-payments/src/client.ts deleted file mode 100644 index b6ff930712..0000000000 --- a/packages/open-payments/src/client.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ILPStreamConnection, IncomingPayment } from './types' -import axios, { AxiosInstance } from 'axios' -import config from './config' - -interface CreateOpenPaymentClientArgs { - timeout?: number -} - -interface GetArgs { - url: string - accessToken?: string -} - -export interface OpenPaymentsClient { - incomingPayment: { - get(args: GetArgs): Promise - } - ilpStreamConnection: { - get(args: GetArgs): Promise - } -} - -export const get = async ( - axios: AxiosInstance, - args: GetArgs -): Promise => { - const { url, accessToken } = args - - const { data } = await axios.get(url, { - headers: accessToken - ? { - Authorization: `GNAP ${accessToken}`, - Signature: 'TODO', - 'Signature-Input': 'TODO' - } - : {} - }) - - return data -} - -export const createAxiosInstance = ( - args?: CreateOpenPaymentClientArgs -): AxiosInstance => { - const axiosInstance = axios.create({ - timeout: args?.timeout ?? config.DEFAULT_REQUEST_TIMEOUT - }) - - axiosInstance.defaults.headers.common['Content-Type'] = 'application/json' - - return axiosInstance -} - -export const createClient = ( - args?: CreateOpenPaymentClientArgs -): OpenPaymentsClient => { - const axios = createAxiosInstance(args) - - return { - incomingPayment: { - get: (args: GetArgs) => get(axios, args) - }, - ilpStreamConnection: { - get: (args: GetArgs) => get(axios, args) - } - } -} diff --git a/packages/open-payments/src/client/ilp-stream-connection.ts b/packages/open-payments/src/client/ilp-stream-connection.ts new file mode 100644 index 0000000000..fe8db4b767 --- /dev/null +++ b/packages/open-payments/src/client/ilp-stream-connection.ts @@ -0,0 +1,19 @@ +import { AxiosInstance } from 'axios' +import { OpenAPI, HttpMethod } from 'openapi' +import { getPath, ILPStreamConnection } from '../types' +import { GetArgs, get } from './requests' + +export const getILPStreamConnection = async ( + axios: AxiosInstance, + openApi: OpenAPI, + args: GetArgs +): Promise => { + return get( + axios, + args, + openApi.createResponseValidator({ + path: getPath('/connections/{id}'), + method: HttpMethod.GET + }) + ) +} diff --git a/packages/open-payments/src/client/incoming-payment.ts b/packages/open-payments/src/client/incoming-payment.ts new file mode 100644 index 0000000000..6303ad8fa8 --- /dev/null +++ b/packages/open-payments/src/client/incoming-payment.ts @@ -0,0 +1,19 @@ +import { AxiosInstance } from 'axios' +import { OpenAPI, HttpMethod } from 'openapi' +import { IncomingPayment, getPath } from '../types' +import { GetArgs, get } from './requests' + +export const getIncomingPayment = async ( + axios: AxiosInstance, + openApi: OpenAPI, + args: GetArgs +): Promise => { + return get( + axios, + args, + openApi.createResponseValidator({ + path: getPath('/incoming-payments/{id}'), + method: HttpMethod.GET + }) + ) +} diff --git a/packages/open-payments/src/client/index.ts b/packages/open-payments/src/client/index.ts new file mode 100644 index 0000000000..ad47e9aa3f --- /dev/null +++ b/packages/open-payments/src/client/index.ts @@ -0,0 +1,36 @@ +import { createOpenAPI } from 'openapi' + +import { ILPStreamConnection, IncomingPayment } from '../types' +import config from '../config' +import { getIncomingPayment } from './incoming-payment' +import { getILPStreamConnection } from './ilp-stream-connection' +import { createAxiosInstance, GetArgs } from './requests' + +export interface CreateOpenPaymentClientArgs { + timeout?: number +} + +export interface OpenPaymentsClient { + incomingPayment: { + get(args: GetArgs): Promise + } + ilpStreamConnection: { + get(args: GetArgs): Promise + } +} + +export const createClient = async ( + args?: CreateOpenPaymentClientArgs +): Promise => { + const axios = createAxiosInstance(args) + const openApi = await createOpenAPI(config.OPEN_PAYMENTS_OPEN_API_URL) + + return { + incomingPayment: { + get: (args: GetArgs) => getIncomingPayment(axios, openApi, args) + }, + ilpStreamConnection: { + get: (args: GetArgs) => getILPStreamConnection(axios, openApi, args) + } + } +} diff --git a/packages/open-payments/src/client/requests.ts b/packages/open-payments/src/client/requests.ts new file mode 100644 index 0000000000..909214dbc7 --- /dev/null +++ b/packages/open-payments/src/client/requests.ts @@ -0,0 +1,60 @@ +import axios, { AxiosInstance } from 'axios' +import { ValidateFunction } from 'openapi' +import { CreateOpenPaymentClientArgs } from '.' +import config from '../config' + +export interface GetArgs { + url: string + accessToken?: string +} + +export const get = async ( + axios: AxiosInstance, + args: GetArgs, + responseValidator: ValidateFunction +): Promise => { + const { url, accessToken } = args + + try { + const { data } = await axios.get(url, { + headers: accessToken + ? { + Authorization: `GNAP ${accessToken}`, + Signature: 'TODO', + 'Signature-Input': 'TODO' + } + : {} + }) + + if (!responseValidator(data)) { + const errorMessage = 'Failed to validate OpenApi response' + console.log(errorMessage, { + url, + data: JSON.stringify(data) + }) + + throw new Error(errorMessage) + } + + return data + } catch (error) { + console.log('Error when making Open Payments GET request', { + errorMessage: error?.message, + url + }) + + throw error + } +} + +export const createAxiosInstance = ( + args?: CreateOpenPaymentClientArgs +): AxiosInstance => { + const axiosInstance = axios.create({ + timeout: args?.timeout ?? config.DEFAULT_REQUEST_TIMEOUT + }) + + axiosInstance.defaults.headers.common['Content-Type'] = 'application/json' + + return axiosInstance +} diff --git a/packages/open-payments/src/index.ts b/packages/open-payments/src/index.ts index 8e8c16c9bf..0cc9edc2a2 100644 --- a/packages/open-payments/src/index.ts +++ b/packages/open-payments/src/index.ts @@ -1,2 +1,2 @@ -export * from './types' +export { IncomingPayment, ILPStreamConnection } from './types' export { createClient, OpenPaymentsClient } from './client' diff --git a/packages/open-payments/src/types.ts b/packages/open-payments/src/types.ts index a282d9b169..82dd47c3c9 100644 --- a/packages/open-payments/src/types.ts +++ b/packages/open-payments/src/types.ts @@ -1,4 +1,7 @@ -import { components } from './generated/types' +import { components, paths as Paths } from './generated/types' + +export const getPath =

(path: P): string => + path as string export type IncomingPayment = components['schemas']['incoming-payment'] export type ILPStreamConnection = components['schemas']['ilp-stream-connection'] From 218f1f0119237b03bc5cb65c437f40f516b4e219 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Tue, 18 Oct 2022 14:29:20 +0200 Subject: [PATCH 16/56] feat(open-payments): adding openapi validation --- packages/open-payments/package.json | 3 ++- pnpm-lock.yaml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/open-payments/package.json b/packages/open-payments/package.json index 6e6f697c38..2caaef6075 100644 --- a/packages/open-payments/package.json +++ b/packages/open-payments/package.json @@ -21,6 +21,7 @@ "typescript": "^4.3.0" }, "dependencies": { - "axios": "^1.1.2" + "axios": "^1.1.2", + "openapi": "workspace:../openapi" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e663d96185..68a7a7d685 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,11 +310,13 @@ importers: '@types/node': ^18.7.12 axios: ^1.1.2 nock: ^13.2.9 + openapi: workspace:../openapi openapi-typescript: ^4.5.0 ts-node: ^10.7.0 typescript: ^4.3.0 dependencies: axios: 1.1.2 + openapi: link:../openapi devDependencies: '@types/node': 18.7.13 nock: 13.2.9 From d7c203c5d910a573421039634b3e92733c01dd11 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Tue, 18 Oct 2022 14:29:37 +0200 Subject: [PATCH 17/56] feat(open-payments): adding tests --- .../src/client/ilp-stream-connection.test.ts | 37 ++++++++ .../src/client/incoming-payment.test.ts | 37 ++++++++ .../open-payments/src/client/requests.test.ts | 86 +++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 packages/open-payments/src/client/ilp-stream-connection.test.ts create mode 100644 packages/open-payments/src/client/incoming-payment.test.ts create mode 100644 packages/open-payments/src/client/requests.test.ts diff --git a/packages/open-payments/src/client/ilp-stream-connection.test.ts b/packages/open-payments/src/client/ilp-stream-connection.test.ts new file mode 100644 index 0000000000..957cb4b694 --- /dev/null +++ b/packages/open-payments/src/client/ilp-stream-connection.test.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { getILPStreamConnection } from './ilp-stream-connection' +import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' +import { createAxiosInstance } from './requests' +import config from '../config' + +jest.mock('./requests', () => ({ + ...jest.requireActual('./requests'), + get: jest.fn() +})) + +describe('ilp-stream-connection', (): void => { + let openApi: OpenAPI + + beforeAll(async () => { + openApi = await createOpenAPI(config.OPEN_PAYMENTS_OPEN_API_URL) + }) + + const axiosInstance = createAxiosInstance() + + describe('getIncomingPayment', (): void => { + test('calls createResponseValidator properly', async (): Promise => { + const createResponseValidatorSpy = jest.spyOn( + openApi, + 'createResponseValidator' + ) + + await getILPStreamConnection(axiosInstance, openApi, { + url: 'http://localhost:1000/incoming-payment' + }) + expect(createResponseValidatorSpy).toHaveBeenCalledWith({ + path: '/connections/{id}', + method: HttpMethod.GET + }) + }) + }) +}) diff --git a/packages/open-payments/src/client/incoming-payment.test.ts b/packages/open-payments/src/client/incoming-payment.test.ts new file mode 100644 index 0000000000..ac29c328ef --- /dev/null +++ b/packages/open-payments/src/client/incoming-payment.test.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { getIncomingPayment } from './incoming-payment' +import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' +import { createAxiosInstance } from './requests' +import config from '../config' + +jest.mock('./requests', () => ({ + ...jest.requireActual('./requests'), + get: jest.fn() +})) + +describe('incoming-payment', (): void => { + let openApi: OpenAPI + + beforeAll(async () => { + openApi = await createOpenAPI(config.OPEN_PAYMENTS_OPEN_API_URL) + }) + + const axiosInstance = createAxiosInstance() + + describe('getIncomingPayment', (): void => { + test('calls createResponseValidator properly', async (): Promise => { + const createResponseValidatorSpy = jest.spyOn( + openApi, + 'createResponseValidator' + ) + + await getIncomingPayment(axiosInstance, openApi, { + url: 'http://localhost:1000/incoming-payment' + }) + expect(createResponseValidatorSpy).toHaveBeenCalledWith({ + path: '/incoming-payments/{id}', + method: HttpMethod.GET + }) + }) + }) +}) diff --git a/packages/open-payments/src/client/requests.test.ts b/packages/open-payments/src/client/requests.test.ts new file mode 100644 index 0000000000..08f911f336 --- /dev/null +++ b/packages/open-payments/src/client/requests.test.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { createAxiosInstance, get } from './requests' +import nock from 'nock' + +describe('requests', (): void => { + jest.spyOn(console, 'log').mockImplementation(() => {}) + + describe('createAxiosInstance', (): void => { + test('sets timeout properly', async (): Promise => { + expect(createAxiosInstance({ timeout: 1000 }).defaults.timeout).toBe(1000) + }) + test('sets Content-Type header properly', async (): Promise => { + expect( + createAxiosInstance().defaults.headers.common['Content-Type'] + ).toBe('application/json') + }) + }) + + describe('get', (): void => { + const axiosInstance = createAxiosInstance() + const baseUrl = 'http://localhost:1000' + const successfulValidator = (data: unknown): data is unknown => true + const failedValidator = (data: unknown): data is unknown => false + + beforeAll(() => { + jest.spyOn(axiosInstance, 'get') + }) + + test('sets headers properly if accessToken provided', async (): Promise => { + nock(baseUrl).get('/incoming-payment').reply(200) + + await get( + axiosInstance, + { + url: `${baseUrl}/incoming-payment`, + accessToken: 'accessToken' + }, + successfulValidator + ) + + expect(axiosInstance.get).toHaveBeenCalledWith( + `${baseUrl}/incoming-payment`, + { + headers: { + Authorization: 'GNAP accessToken', + Signature: 'TODO', + 'Signature-Input': 'TODO' + } + } + ) + }) + + test('sets headers properly if accessToken is not provided', async (): Promise => { + nock(baseUrl).get('/incoming-payment').reply(200) + + await get( + axiosInstance, + { + url: `${baseUrl}/incoming-payment` + }, + successfulValidator + ) + + expect(axiosInstance.get).toHaveBeenCalledWith( + `${baseUrl}/incoming-payment`, + { + headers: {} + } + ) + }) + + test('throws if response validator function fails', async (): Promise => { + nock(baseUrl).get('/incoming-payment').reply(200) + + await expect( + get( + axiosInstance, + { + url: `${baseUrl}/incoming-payment` + }, + failedValidator + ) + ).rejects.toThrow('Failed to validate OpenApi response') + }) + }) +}) From dfc07cb7a9e76753e7efa25b5744542c917a0077 Mon Sep 17 00:00:00 2001 From: "maxkurapov@gmail.com" Date: Tue, 18 Oct 2022 14:33:31 +0200 Subject: [PATCH 18/56] feat(open-payments): correcting test --- packages/open-payments/src/client/ilp-stream-connection.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/open-payments/src/client/ilp-stream-connection.test.ts b/packages/open-payments/src/client/ilp-stream-connection.test.ts index 957cb4b694..f347ad6af1 100644 --- a/packages/open-payments/src/client/ilp-stream-connection.test.ts +++ b/packages/open-payments/src/client/ilp-stream-connection.test.ts @@ -18,7 +18,7 @@ describe('ilp-stream-connection', (): void => { const axiosInstance = createAxiosInstance() - describe('getIncomingPayment', (): void => { + describe('getILPStreamConnection', (): void => { test('calls createResponseValidator properly', async (): Promise => { const createResponseValidatorSpy = jest.spyOn( openApi, From bc0ea85b5c8f651d4d820ab5c24ac501d87c0f70 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Fri, 21 Oct 2022 13:22:05 +0200 Subject: [PATCH 19/56] feat(backend): import open-payments package --- packages/backend/package.json | 1 + pnpm-lock.yaml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/backend/package.json b/packages/backend/package.json index cc55363d1b..3f5f75759c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -81,6 +81,7 @@ "objection-db-errors": "^1.1.2", "oer-utils": "5.1.3-alpha.1", "openapi": "workspace:../openapi", + "open-payments": "workspace:../open-payments", "pg": "^8.6.0", "pino": "^8.4.2", "pino-pretty": "^9.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68a7a7d685..a961938862 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,7 @@ importers: objection: ^3.0.1 objection-db-errors: ^1.1.2 oer-utils: 5.1.3-alpha.1 + open-payments: workspace:../open-payments openapi: workspace:../openapi openapi-types: ^12.0.0 pg: ^8.6.0 @@ -222,6 +223,7 @@ importers: objection: 3.0.1_knex@0.95.15 objection-db-errors: 1.1.2_objection@3.0.1 oer-utils: 5.1.3-alpha.1 + open-payments: link:../open-payments openapi: link:../openapi pg: 8.7.3 pino: 8.4.2 From 43335f69b89ca46d59d3e177ac1add2daa9a060c Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Fri, 21 Oct 2022 20:07:07 +0200 Subject: [PATCH 20/56] feat(open-payments): use updated RS spec --- packages/open-payments/src/config.ts | 2 +- packages/open-payments/src/generated/types.ts | 247 +++++++++++++----- packages/open-payments/src/types.ts | 3 +- 3 files changed, 189 insertions(+), 63 deletions(-) diff --git a/packages/open-payments/src/config.ts b/packages/open-payments/src/config.ts index ed62374b63..7c16ca7088 100644 --- a/packages/open-payments/src/config.ts +++ b/packages/open-payments/src/config.ts @@ -1,5 +1,5 @@ export default { OPEN_PAYMENTS_OPEN_API_URL: - 'https://raw.githubusercontent.com/interledger/open-payments/4c873dba89164decffbe84905d12f1d4ec045389/open-api-spec.yaml', + 'https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/RS/openapi.yaml', DEFAULT_REQUEST_TIMEOUT: 3_000 } diff --git a/packages/open-payments/src/generated/types.ts b/packages/open-payments/src/generated/types.ts index 95973a957a..d3398c3d86 100644 --- a/packages/open-payments/src/generated/types.ts +++ b/packages/open-payments/src/generated/types.ts @@ -14,6 +14,10 @@ export interface paths { */ get: operations["get-payment-pointer"]; }; + "/jwks.json": { + /** Retrieve the public keys of the Payment Pointer. */ + get: operations["get-payment-pointer-keys"]; + }; "/connections/{id}": { /** * *NB* Use server url specific to this path. @@ -114,15 +118,21 @@ export interface components { id: string; /** @description A public name for the account. This should be set by the account holder with their provider to provide a hint to counterparties as to the identity of the account holder. */ publicName?: string; - /** @description The asset code of the account. */ - assetCode: components["schemas"]["assetCode"]; - assetScale: components["schemas"]["assetScale"]; + assetCode: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["assetCode"]; + assetScale: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["assetScale"]; /** * Format: uri * @description The URL of the authorization server endpoint for getting grants and access tokens for this payment pointer. */ authServer: string; }; + /** + * JSON Web Key Set document + * @description A JSON Web Key Set document according to [rfc7517](https://datatracker.ietf.org/doc/html/rfc7517) listing the keys associated with this payment pointer. These keys are used to sign requests made by this payment pointer. + */ + "json-web-key-set": { + keys: components["schemas"]["json-web-key"][]; + }; /** * ILP Stream Connection * @description An **ILP STREAM Connection** is an endpoint that returns unique STREAM connection credentials to establish a STREAM connection to the underlying account. @@ -137,10 +147,8 @@ export interface components { ilpAddress: string; /** @description The base64 url-encoded shared secret to use when establishing a STREAM connection. */ sharedSecret: string; - /** @description The asset code of the amount. */ - assetCode: components["schemas"]["assetCode"]; - /** @description The scale of the amount. */ - assetScale: components["schemas"]["assetScale"]; + assetCode: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["assetCode"]; + assetScale: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["assetScale"]; }; /** * Incoming Payment @@ -160,9 +168,9 @@ export interface components { /** @description Describes whether the incoming payment has completed receiving fund. */ completed: boolean; /** @description The maximum amount that should be paid into the payment pointer under this incoming payment. */ - incomingAmount?: components["schemas"]["amount"]; + incomingAmount?: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; /** @description The total amount that has been paid into the payment pointer under this incoming payment. */ - receivedAmount: components["schemas"]["amount"]; + receivedAmount: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; /** * Format: date-time * @description The date and time when payments under this incoming payment will no longer be accepted. @@ -224,13 +232,13 @@ export interface components { /** @description Describes whether the payment failed to send its full amount. */ failed?: boolean; /** @description The URL of the incoming payment or ILP STREAM Connection that is being paid. */ - receiver: components["schemas"]["receiver"]; + receiver: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["receiver"]; /** @description The total amount that should be received by the receiver when this outgoing payment has been paid. */ - receiveAmount: components["schemas"]["amount"]; + receiveAmount: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; /** @description The total amount that should be sent when this outgoing payment has been paid. */ - sendAmount: components["schemas"]["amount"]; + sendAmount: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; /** @description The total amount that has been sent under this outgoing payment. */ - sentAmount: components["schemas"]["amount"]; + sentAmount: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; /** @description Human readable description of the outgoing payment that will be visible to the account holder and shared with the receiver. */ description?: string; /** @description A reference that can be used by external systems to reconcile this payment with their systems. E.g. An invoice number. (Optional) */ @@ -261,12 +269,9 @@ export interface components { * @description The URL of the payment pointer from which this quote's payment would be sent. */ paymentPointer: string; - /** @description The URL of the incoming payment or ILP Stream Connection that would be paid. */ - receiver: components["schemas"]["receiver"]; - /** @description The total amount that should be received by the receiver. */ - receiveAmount: components["schemas"]["amount"]; - /** @description The total amount that should be sent by the sender. */ - sendAmount: components["schemas"]["amount"]; + receiver: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["receiver"]; + receiveAmount: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; + sendAmount: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; /** @description The date and time when the calculated `sendAmount` is no longer valid. */ expiresAt?: string; /** @@ -275,43 +280,6 @@ export interface components { */ createdAt: string; }; - /** - * Amount - * @description All amounts in open payments are represented as a value and an asset code and scale. - * - * The `value` is an unsigned 64-bit integer amount, represented as a string. - * - * The `assetCode` is a code that indicates the underlying asset. In most cases this SHOULD be a 3-character ISO 4217 currency code. - * - * The `assetScale` indicates how the `value` has been scaled relative to the natural scale of the asset. For example, an `value` of `"1234"` with an `assetScale` of `2` represents an amount of 12.34. - */ - amount: { - /** - * Format: uint64 - * @description The amount, scaled by the given scale. - */ - value: string; - /** @description The asset code of the amount. */ - assetCode: components["schemas"]["assetCode"]; - /** @description The scale of the amount. */ - assetScale: components["schemas"]["assetScale"]; - }; - /** - * Asset code - * @description This SHOULD be an ISO4217 currency code. - */ - assetCode: string; - /** - * Asset scale - * @description The scale of amounts denoted in the corresponding asset code. - */ - assetScale: number; - /** - * Receiver - * Format: uri - * @description The URL of the incoming payment or ILP STREAM connection that is being paid. - */ - receiver: string; /** @description Pagination parameters */ pagination: | components["schemas"]["forward-pagination"] @@ -340,6 +308,18 @@ export interface components { /** @description Describes whether the data set has previous entries. */ hasPreviousPage: boolean; }; + /** + * Ed25519 Public Key + * @description A JWK representation of an Ed25519 Public Key + */ + "json-web-key": { + kid: string; + use?: "sig"; + kty: "OKP"; + crv: "Ed25519"; + /** @description The base64 url-encoded public key. */ + x: string; + }; }; responses: { /** Authorization required */ @@ -377,6 +357,19 @@ export interface operations { 404: unknown; }; }; + /** Retrieve the public keys of the Payment Pointer. */ + "get-payment-pointer-keys": { + responses: { + /** JWKS Document Found */ + 200: { + content: { + "application/json": components["schemas"]["json-web-key-set"]; + }; + }; + /** JWKS Document Not Found */ + 404: unknown; + }; + }; /** * *NB* Use server url specific to this path. * @@ -465,7 +458,7 @@ export interface operations { content: { "application/json": { /** @description The maximum amount that should be paid into the payment pointer under this incoming payment. */ - incomingAmount?: components["schemas"]["amount"]; + incomingAmount?: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; /** * Format: date-time * @description The date and time when payments into the incoming payment must no longer be accepted. @@ -578,9 +571,9 @@ export interface operations { requestBody: { content: { "application/json": { - receiver: components["schemas"]["receiver"]; - receiveAmount?: components["schemas"]["amount"]; - sendAmount?: components["schemas"]["amount"]; + receiver: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["receiver"]; + receiveAmount?: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; + sendAmount?: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; }; }; }; @@ -699,4 +692,136 @@ export interface operations { }; } -export interface external {} +export interface external { + "https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml": { + paths: {}; + components: { + schemas: { + /** @description A description of the rights associated with this access token. */ + access: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["access-item"][]; + /** @description The access associated with the access token is described using objects that each contain multiple dimensions of access. */ + "access-item": + | external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["access-incoming"] + | external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["access-outgoing"] + | external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["access-quote"]; + /** access-incoming */ + "access-incoming": { + /** @description The type of resource request as a string. This field defines which other fields are allowed in the request object. */ + type: "incoming-payment"; + /** @description The types of actions the client instance will take at the RS as an array of strings. */ + actions: ( + | "create" + | "complete" + | "read" + | "read-all" + | "list" + | "list-all" + )[]; + /** + * Format: uri + * @description A string identifier indicating a specific resource at the RS. + */ + identifier?: string; + }; + /** access-outgoing */ + "access-outgoing": { + /** @description The type of resource request as a string. This field defines which other fields are allowed in the request object. */ + type: "outgoing-payment"; + /** @description The types of actions the client instance will take at the RS as an array of strings. */ + actions: ("create" | "read" | "read-all" | "list" | "list-all")[]; + /** + * Format: uri + * @description A string identifier indicating a specific resource at the RS. + */ + identifier: string; + limits?: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["limits-outgoing"]; + }; + /** access-quote */ + "access-quote": { + /** @description The type of resource request as a string. This field defines which other fields are allowed in the request object. */ + type: "quote"; + /** @description The types of actions the client instance will take at the RS as an array of strings. */ + actions: ("create" | "read" | "read-all")[]; + }; + /** + * amount + * @description All amounts are maxima, i.e. multiple payments can be created under a grant as long as the total amounts of these payments do not exceed the maximum amount per interval as specified in the grant. + */ + amount: { + /** + * Format: uint64 + * @description The value is an unsigned 64-bit integer amount, represented as a string. + */ + value: string; + assetCode: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["assetCode"]; + assetScale: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["assetScale"]; + }; + /** + * Asset code + * @description The assetCode is a code that indicates the underlying asset. This SHOULD be an ISO4217 currency code. + */ + assetCode: string; + /** + * Asset scale + * @description The scale of amounts denoted in the corresponding asset code. + */ + assetScale: number; + /** + * Interval + * @description [ISO8601 repeating interval](https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals) + */ + interval: string; + /** + * key + * @description A key presented by value MUST be a public key. + */ + key: { + /** @description The form of proof that the client instance will use when presenting the key. */ + proof: "httpsig"; + /** @description The public key and its properties represented as a JSON Web Key [[RFC7517](https://datatracker.ietf.org/doc/html/rfc7517)]. */ + jwk: { + /** @description The cryptographic algorithm family used with the key. The only allowed value is `EdDSA`. */ + alg: "EdDSA"; + /** @description A Key ID can be used to match a specific key. */ + kid: string; + /** @description The Key Type. The only allowed value is `OKP`. */ + kty: "OKP"; + /** @description The intended use of the key. */ + use?: "sig"; + /** @description The cryptographic curve used with the key. The only allowed value is `Ed25519`. */ + crv: "Ed25519"; + /** @description Public key encoded using the `base64url` encoding. */ + x: string; + /** @description Array of allowed operations this key may be used for. */ + key_ops?: ("sign" | "verify")[]; + /** @description UNIX timestamp indicating the earliest this key may be used. */ + nbf?: number; + /** @description UNIX timestamp indicating the latest this key may be used. */ + exp?: number; + /** @description The revocation status of the key. */ + revoked?: boolean; + }; + }; + /** + * limits-outgoing + * @description Open Payments specific property that defines the limits under which outgoing payments can be created. + */ + "limits-outgoing": Partial & { + receiver?: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["receiver"]; + sendAmount?: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; + receiveAmount?: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["amount"]; + interval?: external["https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/shared/schemas.yaml"]["components"]["schemas"]["interval"]; + }; + "list-actions": "list" | "list-all"; + "read-actions": "read" | "read-all"; + /** + * Receiver + * Format: uri + * @description The URL of the incoming payment or ILP STREAM connection that is being paid. + */ + receiver: string; + }; + }; + operations: {}; + }; +} diff --git a/packages/open-payments/src/types.ts b/packages/open-payments/src/types.ts index a282d9b169..7f204b10c1 100644 --- a/packages/open-payments/src/types.ts +++ b/packages/open-payments/src/types.ts @@ -1,4 +1,5 @@ import { components } from './generated/types' -export type IncomingPayment = components['schemas']['incoming-payment'] +export type IncomingPayment = + components['schemas']['incoming-payment-with-connection'] export type ILPStreamConnection = components['schemas']['ilp-stream-connection'] From b7e1282098f7df5ae323b489ffa958ccb95825db Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Mon, 24 Oct 2022 13:53:49 +0200 Subject: [PATCH 21/56] feat(open-payments): build open api package on build step --- packages/open-payments/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/open-payments/package.json b/packages/open-payments/package.json index 2caaef6075..60464f9807 100644 --- a/packages/open-payments/package.json +++ b/packages/open-payments/package.json @@ -7,7 +7,8 @@ "dist/**/*" ], "scripts": { - "build": "pnpm clean && tsc --build tsconfig.json", + "build:deps": "pnpm --filter openapi build", + "build": "pnpm build:deps && pnpm clean && tsc --build tsconfig.json", "clean": "rm -fr dist/", "generate:types": "npx ts-node scripts/generate-types.ts", "prepack": "pnpm build", From f8c184654716b80fc43876e9bef9e98a3c3d53b5 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Mon, 24 Oct 2022 13:55:40 +0200 Subject: [PATCH 22/56] feat(open-payments): build open api package on build step --- packages/open-payments/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/open-payments/package.json b/packages/open-payments/package.json index 2caaef6075..60464f9807 100644 --- a/packages/open-payments/package.json +++ b/packages/open-payments/package.json @@ -7,7 +7,8 @@ "dist/**/*" ], "scripts": { - "build": "pnpm clean && tsc --build tsconfig.json", + "build:deps": "pnpm --filter openapi build", + "build": "pnpm build:deps && pnpm clean && tsc --build tsconfig.json", "clean": "rm -fr dist/", "generate:types": "npx ts-node scripts/generate-types.ts", "prepack": "pnpm build", From e11ec65f35a8caa282d5f757d6f7a6af5b7de7ba Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Mon, 24 Oct 2022 14:01:18 +0200 Subject: [PATCH 23/56] feat(open-payments): building open-api package during workflow --- .github/workflows/lint_test_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint_test_build.yml b/.github/workflows/lint_test_build.yml index 9a55454eec..c5848f174e 100644 --- a/.github/workflows/lint_test_build.yml +++ b/.github/workflows/lint_test_build.yml @@ -64,6 +64,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: ./.github/workflows/rafiki/env-setup + - run: pnpm --filter openapi build - run: pnpm --filter open-payments test build: From 0dad54d6da56ded62df39f5c3d495150f8da1d42 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Mon, 24 Oct 2022 14:04:45 +0200 Subject: [PATCH 24/56] Revert "feat(open-payments): building open-api package during workflow" This reverts commit e11ec65f35a8caa282d5f757d6f7a6af5b7de7ba. --- .github/workflows/lint_test_build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/lint_test_build.yml b/.github/workflows/lint_test_build.yml index c5848f174e..9a55454eec 100644 --- a/.github/workflows/lint_test_build.yml +++ b/.github/workflows/lint_test_build.yml @@ -64,7 +64,6 @@ jobs: steps: - uses: actions/checkout@v2 - uses: ./.github/workflows/rafiki/env-setup - - run: pnpm --filter openapi build - run: pnpm --filter open-payments test build: From 25d26171aae1047545fa2a3e98286c073616381c Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Mon, 24 Oct 2022 14:06:24 +0200 Subject: [PATCH 25/56] feat(open-payments): building open-api package during workflow --- .github/workflows/lint_test_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint_test_build.yml b/.github/workflows/lint_test_build.yml index 9a55454eec..c5848f174e 100644 --- a/.github/workflows/lint_test_build.yml +++ b/.github/workflows/lint_test_build.yml @@ -64,6 +64,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: ./.github/workflows/rafiki/env-setup + - run: pnpm --filter openapi build - run: pnpm --filter open-payments test build: From 4552188adb0cdf69780a8196a345c772f15d0fa7 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Mon, 24 Oct 2022 16:50:29 +0200 Subject: [PATCH 26/56] feat(open-payments): update naming --- packages/open-payments/src/client/requests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/open-payments/src/client/requests.ts b/packages/open-payments/src/client/requests.ts index 909214dbc7..6dbb5c474f 100644 --- a/packages/open-payments/src/client/requests.ts +++ b/packages/open-payments/src/client/requests.ts @@ -11,7 +11,7 @@ export interface GetArgs { export const get = async ( axios: AxiosInstance, args: GetArgs, - responseValidator: ValidateFunction + openApiResponseValidator: ValidateFunction ): Promise => { const { url, accessToken } = args @@ -26,7 +26,7 @@ export const get = async ( : {} }) - if (!responseValidator(data)) { + if (!openApiResponseValidator(data)) { const errorMessage = 'Failed to validate OpenApi response' console.log(errorMessage, { url, From 85db13611f153ec1cd5fde078d0c2aea1f139e7b Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Tue, 25 Oct 2022 14:24:58 +0200 Subject: [PATCH 27/56] feat(open-payments): instantiate validator functions once --- .../src/client/ilp-stream-connection.test.ts | 20 ++++--------- .../src/client/ilp-stream-connection.ts | 22 ++++++++------ .../src/client/incoming-payment.test.ts | 20 ++++--------- .../src/client/incoming-payment.ts | 22 ++++++++------ packages/open-payments/src/client/index.ts | 29 +++++++++---------- 5 files changed, 49 insertions(+), 64 deletions(-) diff --git a/packages/open-payments/src/client/ilp-stream-connection.test.ts b/packages/open-payments/src/client/ilp-stream-connection.test.ts index f347ad6af1..17a617fe8e 100644 --- a/packages/open-payments/src/client/ilp-stream-connection.test.ts +++ b/packages/open-payments/src/client/ilp-stream-connection.test.ts @@ -1,14 +1,9 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { getILPStreamConnection } from './ilp-stream-connection' +import { createILPStreamConnectionRoutes } from './ilp-stream-connection' import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' import { createAxiosInstance } from './requests' import config from '../config' -jest.mock('./requests', () => ({ - ...jest.requireActual('./requests'), - get: jest.fn() -})) - describe('ilp-stream-connection', (): void => { let openApi: OpenAPI @@ -18,17 +13,12 @@ describe('ilp-stream-connection', (): void => { const axiosInstance = createAxiosInstance() - describe('getILPStreamConnection', (): void => { + describe('createILPStreamConnectionRoutes', (): void => { test('calls createResponseValidator properly', async (): Promise => { - const createResponseValidatorSpy = jest.spyOn( - openApi, - 'createResponseValidator' - ) + jest.spyOn(openApi, 'createResponseValidator') - await getILPStreamConnection(axiosInstance, openApi, { - url: 'http://localhost:1000/incoming-payment' - }) - expect(createResponseValidatorSpy).toHaveBeenCalledWith({ + createILPStreamConnectionRoutes(axiosInstance, openApi) + expect(openApi.createResponseValidator).toHaveBeenCalledWith({ path: '/connections/{id}', method: HttpMethod.GET }) diff --git a/packages/open-payments/src/client/ilp-stream-connection.ts b/packages/open-payments/src/client/ilp-stream-connection.ts index fe8db4b767..43d8370c5f 100644 --- a/packages/open-payments/src/client/ilp-stream-connection.ts +++ b/packages/open-payments/src/client/ilp-stream-connection.ts @@ -3,17 +3,21 @@ import { OpenAPI, HttpMethod } from 'openapi' import { getPath, ILPStreamConnection } from '../types' import { GetArgs, get } from './requests' -export const getILPStreamConnection = async ( +export interface ILPStreamConnectionRoutes { + get(args: GetArgs): Promise +} + +export const createILPStreamConnectionRoutes = ( axios: AxiosInstance, - openApi: OpenAPI, - args: GetArgs -): Promise => { - return get( - axios, - args, - openApi.createResponseValidator({ + openApi: OpenAPI +): ILPStreamConnectionRoutes => { + const getILPStreamConnectionValidator = + openApi.createResponseValidator({ path: getPath('/connections/{id}'), method: HttpMethod.GET }) - ) + + return { + get: (args: GetArgs) => get(axios, args, getILPStreamConnectionValidator) + } } diff --git a/packages/open-payments/src/client/incoming-payment.test.ts b/packages/open-payments/src/client/incoming-payment.test.ts index ac29c328ef..add7ec5c4e 100644 --- a/packages/open-payments/src/client/incoming-payment.test.ts +++ b/packages/open-payments/src/client/incoming-payment.test.ts @@ -1,14 +1,9 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import { getIncomingPayment } from './incoming-payment' +import { createIncomingPaymentRoutes } from './incoming-payment' import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' import { createAxiosInstance } from './requests' import config from '../config' -jest.mock('./requests', () => ({ - ...jest.requireActual('./requests'), - get: jest.fn() -})) - describe('incoming-payment', (): void => { let openApi: OpenAPI @@ -18,17 +13,12 @@ describe('incoming-payment', (): void => { const axiosInstance = createAxiosInstance() - describe('getIncomingPayment', (): void => { + describe('createIncomingPaymentRoutes', (): void => { test('calls createResponseValidator properly', async (): Promise => { - const createResponseValidatorSpy = jest.spyOn( - openApi, - 'createResponseValidator' - ) + jest.spyOn(openApi, 'createResponseValidator') - await getIncomingPayment(axiosInstance, openApi, { - url: 'http://localhost:1000/incoming-payment' - }) - expect(createResponseValidatorSpy).toHaveBeenCalledWith({ + createIncomingPaymentRoutes(axiosInstance, openApi) + expect(openApi.createResponseValidator).toHaveBeenCalledWith({ path: '/incoming-payments/{id}', method: HttpMethod.GET }) diff --git a/packages/open-payments/src/client/incoming-payment.ts b/packages/open-payments/src/client/incoming-payment.ts index 6303ad8fa8..b827238cba 100644 --- a/packages/open-payments/src/client/incoming-payment.ts +++ b/packages/open-payments/src/client/incoming-payment.ts @@ -3,17 +3,21 @@ import { OpenAPI, HttpMethod } from 'openapi' import { IncomingPayment, getPath } from '../types' import { GetArgs, get } from './requests' -export const getIncomingPayment = async ( +export interface IncomingPaymentRoutes { + get(args: GetArgs): Promise +} + +export const createIncomingPaymentRoutes = ( axios: AxiosInstance, - openApi: OpenAPI, - args: GetArgs -): Promise => { - return get( - axios, - args, - openApi.createResponseValidator({ + openApi: OpenAPI +): IncomingPaymentRoutes => { + const getIncomingPaymentValidator = + openApi.createResponseValidator({ path: getPath('/incoming-payments/{id}'), method: HttpMethod.GET }) - ) + + return { + get: (args: GetArgs) => get(axios, args, getIncomingPaymentValidator) + } } diff --git a/packages/open-payments/src/client/index.ts b/packages/open-payments/src/client/index.ts index ad47e9aa3f..7d6f225d43 100644 --- a/packages/open-payments/src/client/index.ts +++ b/packages/open-payments/src/client/index.ts @@ -1,22 +1,23 @@ import { createOpenAPI } from 'openapi' -import { ILPStreamConnection, IncomingPayment } from '../types' import config from '../config' -import { getIncomingPayment } from './incoming-payment' -import { getILPStreamConnection } from './ilp-stream-connection' -import { createAxiosInstance, GetArgs } from './requests' +import { + createIncomingPaymentRoutes, + IncomingPaymentRoutes +} from './incoming-payment' +import { + createILPStreamConnectionRoutes, + ILPStreamConnectionRoutes +} from './ilp-stream-connection' +import { createAxiosInstance } from './requests' export interface CreateOpenPaymentClientArgs { timeout?: number } export interface OpenPaymentsClient { - incomingPayment: { - get(args: GetArgs): Promise - } - ilpStreamConnection: { - get(args: GetArgs): Promise - } + incomingPayment: IncomingPaymentRoutes + ilpStreamConnection: ILPStreamConnectionRoutes } export const createClient = async ( @@ -26,11 +27,7 @@ export const createClient = async ( const openApi = await createOpenAPI(config.OPEN_PAYMENTS_OPEN_API_URL) return { - incomingPayment: { - get: (args: GetArgs) => getIncomingPayment(axios, openApi, args) - }, - ilpStreamConnection: { - get: (args: GetArgs) => getILPStreamConnection(axios, openApi, args) - } + incomingPayment: createIncomingPaymentRoutes(axios, openApi), + ilpStreamConnection: createILPStreamConnectionRoutes(axios, openApi) } } From dc4aa0fd4f718955292c03e05db0f20d11a66410 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Tue, 25 Oct 2022 14:26:20 +0200 Subject: [PATCH 28/56] chore(openapi): add docs for usage --- packages/openapi/README.md | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/openapi/README.md b/packages/openapi/README.md index 9387c5f328..074cf7d703 100644 --- a/packages/openapi/README.md +++ b/packages/openapi/README.md @@ -1,5 +1,7 @@ # OpenAPI 3.1 Validator +This package exposes functionality to validate requests and responses according to a given OpenAPI 3.1 schema. + ## Local Development ### Building @@ -15,3 +17,45 @@ From the monorepo root directory: ```shell pnpm --filter openapi test ``` + +## Usage + +First, instantiate an `OpenAPI` validator object with a reference to your OpenAPI spec: + +```ts +const openApi = await createOpenAPI(OPEN_API_URL) +``` + +Then, responses and requests validators can be created and used as such: + +```ts +const validateRequest = openApi.createRequestValidator({ + path: '/resource/{id}', + method: HttpMethod.GET +}) + +validateRequest(data) // true or false + +const validateResponse = openApi.createResponseValidator({ + path: '/resource/{id}', + method: HttpMethod.GET +}) + +validateResponse(data) // true or false +``` + +> **Note** +> The underlying response & request validator [packages](https://github.com/kogosoftwarellc/open-api/tree/master/packages) use the [Ajv schema validator](https://ajv.js.org) library. When a request and a validator is created, a `new Ajv()` instance is also created. However, Avj [recommends](https://ajv.js.org/guide/managing-schemas.html#compiling-during-initialization) instantiating once at initialization. This means validators (`openApi.createRequestValidator` and `openApi.createResponseValidator`) should also be instantiated once during the lifecycle of the applcation to avoid any issues. + +Likewise, you can validate both requests and responses in a middleware, using the `createValidatorMiddleware` method: + +```ts +const openApi = await createOpenAPI(OPEN_API_URL) +const router = new SomeRouter() +router.get( + '/example', + createValidatorMiddleware(openApi, { + path: '/example', + method: HttpMethod.GET +}) +``` From c927291c718e4ede720c104afef15fb60fe9e71c Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Tue, 25 Oct 2022 14:37:47 +0200 Subject: [PATCH 29/56] chore(openapi): update docs --- packages/openapi/README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/openapi/README.md b/packages/openapi/README.md index 074cf7d703..d7ec561302 100644 --- a/packages/openapi/README.md +++ b/packages/openapi/README.md @@ -26,7 +26,7 @@ First, instantiate an `OpenAPI` validator object with a reference to your OpenAP const openApi = await createOpenAPI(OPEN_API_URL) ``` -Then, responses and requests validators can be created and used as such: +Then, validate requests and responses as such: ```ts const validateRequest = openApi.createRequestValidator({ @@ -45,17 +45,23 @@ validateResponse(data) // true or false ``` > **Note** -> The underlying response & request validator [packages](https://github.com/kogosoftwarellc/open-api/tree/master/packages) use the [Ajv schema validator](https://ajv.js.org) library. When a request and a validator is created, a `new Ajv()` instance is also created. However, Avj [recommends](https://ajv.js.org/guide/managing-schemas.html#compiling-during-initialization) instantiating once at initialization. This means validators (`openApi.createRequestValidator` and `openApi.createResponseValidator`) should also be instantiated once during the lifecycle of the applcation to avoid any issues. +> +> The underlying response & request validator [packages](https://github.com/kogosoftwarellc/open-api/tree/master/packages) use the [Ajv schema validator](https://ajv.js.org) library. Each time validators are created via `createRequestValidator` and `createResponseValidator`, a `new Ajv()` instance is also [created](https://github.com/kogosoftwarellc/open-api/blob/master/packages/openapi-response-validator/index.ts). Since Ajv [recommends](https://ajv.js.org/guide/managing-schemas.html#compiling-during-initialization) instantiating once at initialization, these validators should also be instantiated just once during the lifecycle of the application to avoid any issues. -Likewise, you can validate both requests and responses in a middleware, using the `createValidatorMiddleware` method: + + +
+ +Likewise, you can validate both requests and responses inside a middleware method, using `createValidatorMiddleware`: ```ts const openApi = await createOpenAPI(OPEN_API_URL) const router = new SomeRouter() router.get( - '/example', + '/resource/{id}', createValidatorMiddleware(openApi, { - path: '/example', - method: HttpMethod.GET -}) + path: '/resource/{id}', + method: HttpMethod.GET + }) +) ``` From 3ed946b98048fa5d36f3b8e4ab891419a165289c Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Tue, 25 Oct 2022 14:39:32 +0200 Subject: [PATCH 30/56] chore(openapi): prettify docs --- packages/openapi/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/openapi/README.md b/packages/openapi/README.md index d7ec561302..13a4e06902 100644 --- a/packages/openapi/README.md +++ b/packages/openapi/README.md @@ -48,8 +48,6 @@ validateResponse(data) // true or false > > The underlying response & request validator [packages](https://github.com/kogosoftwarellc/open-api/tree/master/packages) use the [Ajv schema validator](https://ajv.js.org) library. Each time validators are created via `createRequestValidator` and `createResponseValidator`, a `new Ajv()` instance is also [created](https://github.com/kogosoftwarellc/open-api/blob/master/packages/openapi-response-validator/index.ts). Since Ajv [recommends](https://ajv.js.org/guide/managing-schemas.html#compiling-during-initialization) instantiating once at initialization, these validators should also be instantiated just once during the lifecycle of the application to avoid any issues. - -
Likewise, you can validate both requests and responses inside a middleware method, using `createValidatorMiddleware`: @@ -58,10 +56,10 @@ Likewise, you can validate both requests and responses inside a middleware metho const openApi = await createOpenAPI(OPEN_API_URL) const router = new SomeRouter() router.get( - '/resource/{id}', - createValidatorMiddleware(openApi, { - path: '/resource/{id}', - method: HttpMethod.GET - }) + '/resource/{id}', + createValidatorMiddleware(openApi, { + path: '/resource/{id}', + method: HttpMethod.GET + }) ) ``` From 347075a1435449b352bb4eae8ba51c5f3585bcef Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Tue, 25 Oct 2022 20:06:02 +0200 Subject: [PATCH 31/56] chore(openapi): update docs --- packages/openapi/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/openapi/README.md b/packages/openapi/README.md index 13a4e06902..102b4f7d07 100644 --- a/packages/openapi/README.md +++ b/packages/openapi/README.md @@ -23,7 +23,7 @@ pnpm --filter openapi test First, instantiate an `OpenAPI` validator object with a reference to your OpenAPI spec: ```ts -const openApi = await createOpenAPI(OPEN_API_URL) +const openApi = await createOpenAPI(OPEN_API_URL_OR_FILE_PATH) ``` Then, validate requests and responses as such: @@ -34,14 +34,14 @@ const validateRequest = openApi.createRequestValidator({ method: HttpMethod.GET }) -validateRequest(data) // true or false +validateRequest(request) // true or false const validateResponse = openApi.createResponseValidator({ path: '/resource/{id}', method: HttpMethod.GET }) -validateResponse(data) // true or false +validateResponse(response.body) // true or false ``` > **Note** From 90bc6b020ee7790e448f3dba805841859b90de03 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Tue, 25 Oct 2022 21:00:03 +0200 Subject: [PATCH 32/56] feat(open-payments): adding logger as dependency --- packages/open-payments/package.json | 3 ++- .../src/client/ilp-stream-connection.test.ts | 4 ++- .../src/client/ilp-stream-connection.ts | 12 +++++---- .../src/client/incoming-payment.test.ts | 8 +++++- .../src/client/incoming-payment.ts | 12 +++++---- packages/open-payments/src/client/index.ts | 25 +++++++++++++++---- .../open-payments/src/client/requests.test.ts | 9 ++++--- packages/open-payments/src/client/requests.ts | 11 ++++---- packages/open-payments/src/test/helpers.ts | 5 ++++ packages/open-payments/tsconfig.json | 2 +- pnpm-lock.yaml | 2 ++ 11 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 packages/open-payments/src/test/helpers.ts diff --git a/packages/open-payments/package.json b/packages/open-payments/package.json index 60464f9807..31a38e9909 100644 --- a/packages/open-payments/package.json +++ b/packages/open-payments/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "axios": "^1.1.2", - "openapi": "workspace:../openapi" + "openapi": "workspace:../openapi", + "pino": "^8.4.2" } } diff --git a/packages/open-payments/src/client/ilp-stream-connection.test.ts b/packages/open-payments/src/client/ilp-stream-connection.test.ts index 17a617fe8e..dffb6078b5 100644 --- a/packages/open-payments/src/client/ilp-stream-connection.test.ts +++ b/packages/open-payments/src/client/ilp-stream-connection.test.ts @@ -3,6 +3,7 @@ import { createILPStreamConnectionRoutes } from './ilp-stream-connection' import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' import { createAxiosInstance } from './requests' import config from '../config' +import { silentLogger } from '../test/helpers' describe('ilp-stream-connection', (): void => { let openApi: OpenAPI @@ -12,12 +13,13 @@ describe('ilp-stream-connection', (): void => { }) const axiosInstance = createAxiosInstance() + const logger = silentLogger describe('createILPStreamConnectionRoutes', (): void => { test('calls createResponseValidator properly', async (): Promise => { jest.spyOn(openApi, 'createResponseValidator') - createILPStreamConnectionRoutes(axiosInstance, openApi) + createILPStreamConnectionRoutes({ axiosInstance, openApi, logger }) expect(openApi.createResponseValidator).toHaveBeenCalledWith({ path: '/connections/{id}', method: HttpMethod.GET diff --git a/packages/open-payments/src/client/ilp-stream-connection.ts b/packages/open-payments/src/client/ilp-stream-connection.ts index 43d8370c5f..f36f0c9025 100644 --- a/packages/open-payments/src/client/ilp-stream-connection.ts +++ b/packages/open-payments/src/client/ilp-stream-connection.ts @@ -1,5 +1,5 @@ -import { AxiosInstance } from 'axios' -import { OpenAPI, HttpMethod } from 'openapi' +import { HttpMethod } from 'openapi' +import { ClientDeps } from '.' import { getPath, ILPStreamConnection } from '../types' import { GetArgs, get } from './requests' @@ -8,9 +8,10 @@ export interface ILPStreamConnectionRoutes { } export const createILPStreamConnectionRoutes = ( - axios: AxiosInstance, - openApi: OpenAPI + clientDeps: ClientDeps ): ILPStreamConnectionRoutes => { + const { axiosInstance, openApi, logger } = clientDeps + const getILPStreamConnectionValidator = openApi.createResponseValidator({ path: getPath('/connections/{id}'), @@ -18,6 +19,7 @@ export const createILPStreamConnectionRoutes = ( }) return { - get: (args: GetArgs) => get(axios, args, getILPStreamConnectionValidator) + get: (args: GetArgs) => + get({ axiosInstance, logger }, args, getILPStreamConnectionValidator) } } diff --git a/packages/open-payments/src/client/incoming-payment.test.ts b/packages/open-payments/src/client/incoming-payment.test.ts index add7ec5c4e..02d4967527 100644 --- a/packages/open-payments/src/client/incoming-payment.test.ts +++ b/packages/open-payments/src/client/incoming-payment.test.ts @@ -3,6 +3,7 @@ import { createIncomingPaymentRoutes } from './incoming-payment' import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' import { createAxiosInstance } from './requests' import config from '../config' +import { silentLogger } from '../test/helpers' describe('incoming-payment', (): void => { let openApi: OpenAPI @@ -12,12 +13,17 @@ describe('incoming-payment', (): void => { }) const axiosInstance = createAxiosInstance() + const logger = silentLogger describe('createIncomingPaymentRoutes', (): void => { test('calls createResponseValidator properly', async (): Promise => { jest.spyOn(openApi, 'createResponseValidator') - createIncomingPaymentRoutes(axiosInstance, openApi) + createIncomingPaymentRoutes({ + axiosInstance, + openApi, + logger + }) expect(openApi.createResponseValidator).toHaveBeenCalledWith({ path: '/incoming-payments/{id}', method: HttpMethod.GET diff --git a/packages/open-payments/src/client/incoming-payment.ts b/packages/open-payments/src/client/incoming-payment.ts index b827238cba..b787fff3c6 100644 --- a/packages/open-payments/src/client/incoming-payment.ts +++ b/packages/open-payments/src/client/incoming-payment.ts @@ -1,5 +1,5 @@ -import { AxiosInstance } from 'axios' -import { OpenAPI, HttpMethod } from 'openapi' +import { HttpMethod } from 'openapi' +import { ClientDeps } from '.' import { IncomingPayment, getPath } from '../types' import { GetArgs, get } from './requests' @@ -8,9 +8,10 @@ export interface IncomingPaymentRoutes { } export const createIncomingPaymentRoutes = ( - axios: AxiosInstance, - openApi: OpenAPI + clientDeps: ClientDeps ): IncomingPaymentRoutes => { + const { axiosInstance, openApi, logger } = clientDeps + const getIncomingPaymentValidator = openApi.createResponseValidator({ path: getPath('/incoming-payments/{id}'), @@ -18,6 +19,7 @@ export const createIncomingPaymentRoutes = ( }) return { - get: (args: GetArgs) => get(axios, args, getIncomingPaymentValidator) + get: (args: GetArgs) => + get({ axiosInstance, logger }, args, getIncomingPaymentValidator) } } diff --git a/packages/open-payments/src/client/index.ts b/packages/open-payments/src/client/index.ts index 7d6f225d43..bc57d2f191 100644 --- a/packages/open-payments/src/client/index.ts +++ b/packages/open-payments/src/client/index.ts @@ -1,5 +1,5 @@ -import { createOpenAPI } from 'openapi' - +import { createOpenAPI, OpenAPI } from 'openapi' +import createLogger, { Logger, LevelWithSilent as LogLevel } from 'pino' import config from '../config' import { createIncomingPaymentRoutes, @@ -10,9 +10,18 @@ import { ILPStreamConnectionRoutes } from './ilp-stream-connection' import { createAxiosInstance } from './requests' +import { AxiosInstance } from 'axios' export interface CreateOpenPaymentClientArgs { timeout?: number + logger?: Logger + loggerLevel?: LogLevel +} + +export interface ClientDeps { + axiosInstance: AxiosInstance + openApi: OpenAPI + logger: Logger } export interface OpenPaymentsClient { @@ -23,11 +32,17 @@ export interface OpenPaymentsClient { export const createClient = async ( args?: CreateOpenPaymentClientArgs ): Promise => { - const axios = createAxiosInstance(args) + const axiosInstance = createAxiosInstance(args) const openApi = await createOpenAPI(config.OPEN_PAYMENTS_OPEN_API_URL) + const logger = + args.logger ?? + createLogger({ + level: args.loggerLevel ?? 'info' + }) + const deps = { axiosInstance, openApi, logger } return { - incomingPayment: createIncomingPaymentRoutes(axios, openApi), - ilpStreamConnection: createILPStreamConnectionRoutes(axios, openApi) + incomingPayment: createIncomingPaymentRoutes(deps), + ilpStreamConnection: createILPStreamConnectionRoutes(deps) } } diff --git a/packages/open-payments/src/client/requests.test.ts b/packages/open-payments/src/client/requests.test.ts index 08f911f336..b68bebf93d 100644 --- a/packages/open-payments/src/client/requests.test.ts +++ b/packages/open-payments/src/client/requests.test.ts @@ -1,9 +1,10 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { createAxiosInstance, get } from './requests' import nock from 'nock' +import { silentLogger } from '../test/helpers' describe('requests', (): void => { - jest.spyOn(console, 'log').mockImplementation(() => {}) + const logger = silentLogger describe('createAxiosInstance', (): void => { test('sets timeout properly', async (): Promise => { @@ -30,7 +31,7 @@ describe('requests', (): void => { nock(baseUrl).get('/incoming-payment').reply(200) await get( - axiosInstance, + { axiosInstance, logger }, { url: `${baseUrl}/incoming-payment`, accessToken: 'accessToken' @@ -54,7 +55,7 @@ describe('requests', (): void => { nock(baseUrl).get('/incoming-payment').reply(200) await get( - axiosInstance, + { axiosInstance, logger }, { url: `${baseUrl}/incoming-payment` }, @@ -74,7 +75,7 @@ describe('requests', (): void => { await expect( get( - axiosInstance, + { axiosInstance, logger }, { url: `${baseUrl}/incoming-payment` }, diff --git a/packages/open-payments/src/client/requests.ts b/packages/open-payments/src/client/requests.ts index 6dbb5c474f..96fcec4936 100644 --- a/packages/open-payments/src/client/requests.ts +++ b/packages/open-payments/src/client/requests.ts @@ -1,6 +1,6 @@ import axios, { AxiosInstance } from 'axios' import { ValidateFunction } from 'openapi' -import { CreateOpenPaymentClientArgs } from '.' +import { ClientDeps, CreateOpenPaymentClientArgs } from '.' import config from '../config' export interface GetArgs { @@ -9,14 +9,15 @@ export interface GetArgs { } export const get = async ( - axios: AxiosInstance, + clientDeps: Pick, args: GetArgs, openApiResponseValidator: ValidateFunction ): Promise => { + const { axiosInstance, logger } = clientDeps const { url, accessToken } = args try { - const { data } = await axios.get(url, { + const { data } = await axiosInstance.get(url, { headers: accessToken ? { Authorization: `GNAP ${accessToken}`, @@ -28,7 +29,7 @@ export const get = async ( if (!openApiResponseValidator(data)) { const errorMessage = 'Failed to validate OpenApi response' - console.log(errorMessage, { + logger.error(errorMessage, { url, data: JSON.stringify(data) }) @@ -38,7 +39,7 @@ export const get = async ( return data } catch (error) { - console.log('Error when making Open Payments GET request', { + logger.error('Error when making Open Payments GET request', { errorMessage: error?.message, url }) diff --git a/packages/open-payments/src/test/helpers.ts b/packages/open-payments/src/test/helpers.ts new file mode 100644 index 0000000000..b657490d1d --- /dev/null +++ b/packages/open-payments/src/test/helpers.ts @@ -0,0 +1,5 @@ +import createLogger from 'pino' + +export const silentLogger = createLogger({ + level: 'silent' +}) diff --git a/packages/open-payments/tsconfig.json b/packages/open-payments/tsconfig.json index cf98f1692c..700f4d60dc 100644 --- a/packages/open-payments/tsconfig.json +++ b/packages/open-payments/tsconfig.json @@ -7,5 +7,5 @@ "declaration": true }, "include": ["src/**/*"], - "exclude": ["**/*.test.ts", "src/scripts/*"] + "exclude": ["**/*.test.ts", "src/scripts/*", "src/test/*"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4b2580a37..f2e11e9940 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,11 +312,13 @@ importers: nock: ^13.2.9 openapi: workspace:../openapi openapi-typescript: ^4.5.0 + pino: ^8.4.2 ts-node: ^10.7.0 typescript: ^4.3.0 dependencies: axios: 1.1.2 openapi: link:../openapi + pino: 8.4.2 devDependencies: '@types/node': 18.7.13 nock: 13.2.9 From 3129f2aed55163fda092c6f396687c0fdce920a0 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Tue, 25 Oct 2022 21:36:06 +0200 Subject: [PATCH 33/56] feat(open-payments): updating logging & error logic --- .../src/client/ilp-stream-connection.test.ts | 5 ++-- .../src/client/incoming-payment.test.ts | 5 ++-- packages/open-payments/src/client/index.ts | 14 +++++----- .../open-payments/src/client/requests.test.ts | 10 ++++--- packages/open-payments/src/client/requests.ts | 26 +++++++------------ packages/open-payments/src/config.ts | 2 +- packages/open-payments/src/test/helpers.ts | 3 +++ 7 files changed, 31 insertions(+), 34 deletions(-) diff --git a/packages/open-payments/src/client/ilp-stream-connection.test.ts b/packages/open-payments/src/client/ilp-stream-connection.test.ts index dffb6078b5..c22839783c 100644 --- a/packages/open-payments/src/client/ilp-stream-connection.test.ts +++ b/packages/open-payments/src/client/ilp-stream-connection.test.ts @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { createILPStreamConnectionRoutes } from './ilp-stream-connection' import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' -import { createAxiosInstance } from './requests' import config from '../config' -import { silentLogger } from '../test/helpers' +import { defaultAxiosInstance, silentLogger } from '../test/helpers' describe('ilp-stream-connection', (): void => { let openApi: OpenAPI @@ -12,7 +11,7 @@ describe('ilp-stream-connection', (): void => { openApi = await createOpenAPI(config.OPEN_PAYMENTS_OPEN_API_URL) }) - const axiosInstance = createAxiosInstance() + const axiosInstance = defaultAxiosInstance const logger = silentLogger describe('createILPStreamConnectionRoutes', (): void => { diff --git a/packages/open-payments/src/client/incoming-payment.test.ts b/packages/open-payments/src/client/incoming-payment.test.ts index 02d4967527..764025f562 100644 --- a/packages/open-payments/src/client/incoming-payment.test.ts +++ b/packages/open-payments/src/client/incoming-payment.test.ts @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { createIncomingPaymentRoutes } from './incoming-payment' import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' -import { createAxiosInstance } from './requests' import config from '../config' -import { silentLogger } from '../test/helpers' +import { defaultAxiosInstance, silentLogger } from '../test/helpers' describe('incoming-payment', (): void => { let openApi: OpenAPI @@ -12,7 +11,7 @@ describe('incoming-payment', (): void => { openApi = await createOpenAPI(config.OPEN_PAYMENTS_OPEN_API_URL) }) - const axiosInstance = createAxiosInstance() + const axiosInstance = defaultAxiosInstance const logger = silentLogger describe('createIncomingPaymentRoutes', (): void => { diff --git a/packages/open-payments/src/client/index.ts b/packages/open-payments/src/client/index.ts index bc57d2f191..306b3d7865 100644 --- a/packages/open-payments/src/client/index.ts +++ b/packages/open-payments/src/client/index.ts @@ -13,9 +13,8 @@ import { createAxiosInstance } from './requests' import { AxiosInstance } from 'axios' export interface CreateOpenPaymentClientArgs { - timeout?: number + requestTimeoutMs?: number logger?: Logger - loggerLevel?: LogLevel } export interface ClientDeps { @@ -32,13 +31,12 @@ export interface OpenPaymentsClient { export const createClient = async ( args?: CreateOpenPaymentClientArgs ): Promise => { - const axiosInstance = createAxiosInstance(args) + const axiosInstance = createAxiosInstance({ + requestTimeoutMs: + args?.requestTimeoutMs ?? config.DEFAULT_REQUEST_TIMEOUT_MS + }) const openApi = await createOpenAPI(config.OPEN_PAYMENTS_OPEN_API_URL) - const logger = - args.logger ?? - createLogger({ - level: args.loggerLevel ?? 'info' - }) + const logger = args?.logger ?? createLogger() const deps = { axiosInstance, openApi, logger } return { diff --git a/packages/open-payments/src/client/requests.test.ts b/packages/open-payments/src/client/requests.test.ts index b68bebf93d..e32128f2d5 100644 --- a/packages/open-payments/src/client/requests.test.ts +++ b/packages/open-payments/src/client/requests.test.ts @@ -8,17 +8,21 @@ describe('requests', (): void => { describe('createAxiosInstance', (): void => { test('sets timeout properly', async (): Promise => { - expect(createAxiosInstance({ timeout: 1000 }).defaults.timeout).toBe(1000) + expect( + createAxiosInstance({ requestTimeoutMs: 1000 }).defaults.timeout + ).toBe(1000) }) test('sets Content-Type header properly', async (): Promise => { expect( - createAxiosInstance().defaults.headers.common['Content-Type'] + createAxiosInstance({ requestTimeoutMs: 0 }).defaults.headers.common[ + 'Content-Type' + ] ).toBe('application/json') }) }) describe('get', (): void => { - const axiosInstance = createAxiosInstance() + const axiosInstance = createAxiosInstance({ requestTimeoutMs: 0 }) const baseUrl = 'http://localhost:1000' const successfulValidator = (data: unknown): data is unknown => true const failedValidator = (data: unknown): data is unknown => false diff --git a/packages/open-payments/src/client/requests.ts b/packages/open-payments/src/client/requests.ts index 96fcec4936..bc7448f3fa 100644 --- a/packages/open-payments/src/client/requests.ts +++ b/packages/open-payments/src/client/requests.ts @@ -1,7 +1,6 @@ -import axios, { AxiosInstance } from 'axios' +import axios, { AxiosError, AxiosInstance } from 'axios' import { ValidateFunction } from 'openapi' -import { ClientDeps, CreateOpenPaymentClientArgs } from '.' -import config from '../config' +import { ClientDeps } from '.' export interface GetArgs { url: string @@ -29,30 +28,25 @@ export const get = async ( if (!openApiResponseValidator(data)) { const errorMessage = 'Failed to validate OpenApi response' - logger.error(errorMessage, { - url, - data: JSON.stringify(data) - }) + logger.error({ data: JSON.stringify(data), url }, errorMessage) throw new Error(errorMessage) } return data } catch (error) { - logger.error('Error when making Open Payments GET request', { - errorMessage: error?.message, - url - }) + const errorMessage = 'Error when making Open Payments GET request' + logger.error({ errorMessage: error?.message, url }, errorMessage) - throw error + throw new Error(errorMessage) } } -export const createAxiosInstance = ( - args?: CreateOpenPaymentClientArgs -): AxiosInstance => { +export const createAxiosInstance = (args: { + requestTimeoutMs: number +}): AxiosInstance => { const axiosInstance = axios.create({ - timeout: args?.timeout ?? config.DEFAULT_REQUEST_TIMEOUT + timeout: args.requestTimeoutMs }) axiosInstance.defaults.headers.common['Content-Type'] = 'application/json' diff --git a/packages/open-payments/src/config.ts b/packages/open-payments/src/config.ts index 7c16ca7088..d3c58e27b9 100644 --- a/packages/open-payments/src/config.ts +++ b/packages/open-payments/src/config.ts @@ -1,5 +1,5 @@ export default { OPEN_PAYMENTS_OPEN_API_URL: 'https://raw.githubusercontent.com/interledger/open-payments/7bb2e6a03d7dfe7ecb0553afb6c70741317bb489/openapi/RS/openapi.yaml', - DEFAULT_REQUEST_TIMEOUT: 3_000 + DEFAULT_REQUEST_TIMEOUT_MS: 5_000 } diff --git a/packages/open-payments/src/test/helpers.ts b/packages/open-payments/src/test/helpers.ts index b657490d1d..cb35e7fa62 100644 --- a/packages/open-payments/src/test/helpers.ts +++ b/packages/open-payments/src/test/helpers.ts @@ -1,5 +1,8 @@ import createLogger from 'pino' +import { createAxiosInstance } from '../client/requests' export const silentLogger = createLogger({ level: 'silent' }) + +export const defaultAxiosInstance = createAxiosInstance({ requestTimeoutMs: 0 }) From 05b6cfb6736c95481aa35b4bdc79317d6959f3b5 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 26 Oct 2022 00:35:45 +0200 Subject: [PATCH 34/56] feat(open-payments): remove unnecessary imports --- packages/open-payments/src/client/index.ts | 2 +- packages/open-payments/src/client/requests.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/open-payments/src/client/index.ts b/packages/open-payments/src/client/index.ts index 306b3d7865..557cb01170 100644 --- a/packages/open-payments/src/client/index.ts +++ b/packages/open-payments/src/client/index.ts @@ -1,5 +1,5 @@ import { createOpenAPI, OpenAPI } from 'openapi' -import createLogger, { Logger, LevelWithSilent as LogLevel } from 'pino' +import createLogger, { Logger } from 'pino' import config from '../config' import { createIncomingPaymentRoutes, diff --git a/packages/open-payments/src/client/requests.ts b/packages/open-payments/src/client/requests.ts index bc7448f3fa..aeb903a636 100644 --- a/packages/open-payments/src/client/requests.ts +++ b/packages/open-payments/src/client/requests.ts @@ -1,4 +1,4 @@ -import axios, { AxiosError, AxiosInstance } from 'axios' +import axios, { AxiosInstance } from 'axios' import { ValidateFunction } from 'openapi' import { ClientDeps } from '.' From f6eb6a8796cbfa380953975767e4bd4a610f45fb Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 26 Oct 2022 00:49:40 +0200 Subject: [PATCH 35/56] feat(open-payments): update error handling & test --- packages/open-payments/src/client/requests.test.ts | 2 +- packages/open-payments/src/client/requests.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/open-payments/src/client/requests.test.ts b/packages/open-payments/src/client/requests.test.ts index e32128f2d5..29c251916f 100644 --- a/packages/open-payments/src/client/requests.test.ts +++ b/packages/open-payments/src/client/requests.test.ts @@ -85,7 +85,7 @@ describe('requests', (): void => { }, failedValidator ) - ).rejects.toThrow('Failed to validate OpenApi response') + ).rejects.toThrow(/Failed to validate OpenApi response/) }) }) }) diff --git a/packages/open-payments/src/client/requests.ts b/packages/open-payments/src/client/requests.ts index aeb903a636..233cb1354f 100644 --- a/packages/open-payments/src/client/requests.ts +++ b/packages/open-payments/src/client/requests.ts @@ -35,8 +35,10 @@ export const get = async ( return data } catch (error) { - const errorMessage = 'Error when making Open Payments GET request' - logger.error({ errorMessage: error?.message, url }, errorMessage) + const errorMessage = `Error when making Open Payments GET request: ${ + error?.message ? error.message : 'Unknown error' + }` + logger.error({ url }, errorMessage) throw new Error(errorMessage) } From 3306129a4102743767639f77a48b8e89bca6a41c Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 26 Oct 2022 14:13:51 +0200 Subject: [PATCH 36/56] feat(backend): wip receiver service --- .../src/open_payments/receiver/service.ts | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 packages/backend/src/open_payments/receiver/service.ts diff --git a/packages/backend/src/open_payments/receiver/service.ts b/packages/backend/src/open_payments/receiver/service.ts new file mode 100644 index 0000000000..929fbf516b --- /dev/null +++ b/packages/backend/src/open_payments/receiver/service.ts @@ -0,0 +1,284 @@ +import { Counter, ResolvedPayment } from '@interledger/pay' +import base64url from 'base64url' + +import { Amount, parseAmount } from '../amount' + +import { + ConnectionBase, + ConnectionJSON, + ConnectionService +} from '../connection/service' +import { IncomingPaymentJSON } from '../payment/incoming/model' +import { PaymentPointerService } from '../payment_pointer/service' +import { ReadContext } from '../../app' +import { AssetOptions } from '../../asset/service' +import { BaseService } from '../../shared/baseService' + +import { + OpenPaymentsClient, + IncomingPayment, + ILPStreamConnection +} from 'open-payments' +import { IncomingPaymentService } from '../payment/incoming/service' +import { PaymentPointer } from '../payment_pointer/model' + +export class Receiver extends ConnectionBase { + static fromConnection(connection: ConnectionJSON): Receiver { + return new this(connection) + } + + static fromIncomingPayment( + incomingPayment: IncomingPayment + ): Receiver | undefined { + if (incomingPayment.completed) { + return undefined + } + if (typeof incomingPayment.ilpStreamConnection !== 'object') { + return undefined + } + if ( + incomingPayment.expiresAt && + new Date(incomingPayment.expiresAt).getTime() <= Date.now() + ) { + return undefined + } + const receivedAmount = parseAmount(incomingPayment.receivedAmount) + const incomingAmount = incomingPayment.incomingAmount + ? parseAmount(incomingPayment.incomingAmount) + : undefined + + return new this( + incomingPayment.ilpStreamConnection, + incomingAmount?.value, + receivedAmount.value + ) + } + + private constructor( + connection: ConnectionJSON, + private readonly incomingAmountValue?: bigint, + private readonly receivedAmountValue?: bigint + ) { + super( + connection.ilpAddress, + base64url.toBuffer(connection.sharedSecret), + connection.assetCode, + connection.assetScale + ) + } + + public get asset(): AssetOptions { + return { + code: this.assetCode, + scale: this.assetScale + } + } + + public get incomingAmount(): Amount | undefined { + if (this.incomingAmountValue) { + return { + value: this.incomingAmountValue, + assetCode: this.assetCode, + assetScale: this.assetScale + } + } + return undefined + } + + public get receivedAmount(): Amount | undefined { + if (this.receivedAmountValue !== undefined) { + return { + value: this.receivedAmountValue, + assetCode: this.assetCode, + assetScale: this.assetScale + } + } + return undefined + } + + public toResolvedPayment(): ResolvedPayment { + return { + destinationAsset: this.asset, + destinationAddress: this.ilpAddress, + sharedSecret: this.sharedSecret, + requestCounter: Counter.from(0) + } + } +} + +export interface ReceiverService { + receiver: { + get(url: string): Promise + } +} + +interface ServiceDependencies extends BaseService { + accessToken: string + connectionService: ConnectionService + incomingPaymentService: IncomingPaymentService + openPaymentsUrl: string + paymentPointerService: PaymentPointerService + openPaymentsClient: OpenPaymentsClient +} + +export async function createReceiverService( + deps_: ServiceDependencies +): Promise { + const log = deps_.logger.child({ + service: 'ReceiverService' + }) + const deps: ServiceDependencies = { + ...deps_, + logger: log + } + + return { + receiver: { + get: (url) => getReceiver(deps, url) + } + } +} + +async function getLocalConnection( + deps: ServiceDependencies, + url: string +): Promise { + const incomingPayment = await deps.incomingPaymentService.getByConnection( + url.slice(-36) + ) + + if (!incomingPayment) { + return + } + + const connection = deps.connectionService.get(incomingPayment) + + return connection ? connection.toJSON() : undefined +} + +async function getConnection( + deps: ServiceDependencies, + url: string +): Promise { + try { + if (url.startsWith(`${deps.openPaymentsUrl}/connections/`)) { + return await getLocalConnection(deps, url) + } + + return await deps.openPaymentsClient.ilpStreamConnection.get({ + url, + accessToken: deps.accessToken + }) + } catch (_) { + return undefined + } +} + +const INCOMING_PAYMENT_URL_REGEX = + /(?^(.)+)\/incoming-payments\/(?(.){36}$)/ + +async function getLocalIncomingPayment({ + deps, + id, + paymentPointer +}: { + deps: ServiceDependencies + id: string + paymentPointer: PaymentPointer +}): Promise { + const incomingPayment = await deps.incomingPaymentService.get({ + id, + paymentPointerId: paymentPointer.id + }) + + const connection = deps.connectionService.get(incomingPayment) + + return { + ...incomingPayment.toJSON(), + ilpStreamConnection: connection.toJSON() + } +} + +async function getIncomingPayment( + deps: ServiceDependencies, + url: string +): Promise { + try { + const match = url.match(INCOMING_PAYMENT_URL_REGEX)?.groups + if (!match) { + return undefined + } + // Check if this is a local payment pointer + const paymentPointer = await deps.paymentPointerService.getByUrl( + match.paymentPointerUrl + ) + if (paymentPointer) { + const incoming + await deps.incomingPaymentRoutes.get(ctx) + return ctx.body as IncomingPaymentJSON + } + const incomingPayment = await deps.openPaymentsClient.incomingPayment.get({ + url, + accessToken: deps.accessToken + }) + if (!isValidIncomingPayment(incomingPayment)) { + throw new Error('unreachable') + } + + return incomingPayment + } catch (_) { + return undefined + } +} + +const CONNECTION_URL_REGEX = /\/connections\/(.){36}$/ + +async function getReceiver( + deps: ServiceDependencies, + url: string +): Promise { + if (url.match(CONNECTION_URL_REGEX)) { + const connection = await getConnection(deps, url) + if (connection) { + return Receiver.fromConnection(connection) + } + } else { + const incomingPayment = await getIncomingPayment(deps, url) + if (incomingPayment) { + return Receiver.fromIncomingPayment(incomingPayment) + } + } +} + +// Validate referential integrity, which cannot be represented in OpenAPI +function isValidIncomingPayment( + payment: IncomingPayment +): payment is IncomingPayment { + if (payment.incomingAmount) { + const incomingAmount = parseAmount(payment.incomingAmount) + const receivedAmount = parseAmount(payment.receivedAmount) + if ( + incomingAmount.assetCode !== receivedAmount.assetCode || + incomingAmount.assetScale !== receivedAmount.assetScale + ) { + return false + } + if (incomingAmount.value < receivedAmount.value) { + return false + } + if (incomingAmount.value === receivedAmount.value && !payment.completed) { + return false + } + } + if (typeof payment.ilpStreamConnection === 'object') { + if ( + payment.ilpStreamConnection.assetCode !== + payment.receivedAmount.assetCode || + payment.ilpStreamConnection.assetScale !== + payment.receivedAmount.assetScale + ) { + return false + } + } + return true +} From 9535ee2386e892a9e231947b4f755d4f709f79a6 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Thu, 27 Oct 2022 00:20:47 +0200 Subject: [PATCH 37/56] feat(backend): adding open-payments as a build dependency --- packages/backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index c205cdf384..4bb54e9319 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,7 +4,7 @@ "test": "jest --passWithNoTests --maxWorkers=50%", "knex": "knex", "generate": "graphql-codegen --config codegen.yml", - "build:deps": "pnpm --filter openapi build && pnpm --filter auth build", + "build:deps": "pnpm --filter openapi build && pnpm --filter auth build && pnpm --filter open-payments build", "build": "pnpm build:deps && tsc --build tsconfig.json && pnpm copy-files", "clean": "rm -fr dist/", "copy-files": "cp src/graphql/schema.graphql dist/graphql/", From bcda9c85d2a71bf00f3349b2555b3b486ca191d0 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Thu, 27 Oct 2022 00:20:58 +0200 Subject: [PATCH 38/56] feat(backend): organizing models, and adding receiver service logic --- .../src/open_payments/client/service.ts | 2 +- .../src/open_payments/connection/model.ts | 74 ++++++ .../src/open_payments/connection/service.ts | 64 +---- .../open_payments/payment/incoming/model.ts | 27 +- .../open_payments/payment/incoming/routes.ts | 3 +- .../src/open_payments/receiver/model.ts | 99 ++++++++ .../src/open_payments/receiver/service.ts | 230 ++++++------------ 7 files changed, 284 insertions(+), 215 deletions(-) create mode 100644 packages/backend/src/open_payments/connection/model.ts create mode 100644 packages/backend/src/open_payments/receiver/model.ts diff --git a/packages/backend/src/open_payments/client/service.ts b/packages/backend/src/open_payments/client/service.ts index 5ef57dfe92..5b95903e8f 100644 --- a/packages/backend/src/open_payments/client/service.ts +++ b/packages/backend/src/open_payments/client/service.ts @@ -7,7 +7,7 @@ import { URL } from 'url' import { Amount, parseAmount } from '../amount' import { ConnectionRoutes } from '../connection/routes' -import { ConnectionBase, ConnectionJSON } from '../connection/service' +import { ConnectionBase, ConnectionJSON } from '../connection/model' import { IncomingPaymentJSON } from '../payment/incoming/model' import { IncomingPaymentRoutes } from '../payment/incoming/routes' import { PaymentPointerService } from '../payment_pointer/service' diff --git a/packages/backend/src/open_payments/connection/model.ts b/packages/backend/src/open_payments/connection/model.ts new file mode 100644 index 0000000000..53d592b12f --- /dev/null +++ b/packages/backend/src/open_payments/connection/model.ts @@ -0,0 +1,74 @@ +import { StreamCredentials } from '@interledger/stream-receiver' +import base64url from 'base64url' +import { IlpAddress } from 'ilp-packet' +import { ILPStreamConnection } from 'open-payments' +import { IncomingPayment } from '../payment/incoming/model' + +export type ConnectionJSON = { + id: string + ilpAddress: IlpAddress + sharedSecret: string + assetCode: string + assetScale: number +} + +export abstract class ConnectionBase { + protected constructor( + public readonly ilpAddress: IlpAddress, + public readonly sharedSecret: Buffer, + public readonly assetCode: string, + public readonly assetScale: number + ) {} +} + +export class Connection extends ConnectionBase { + static fromPayment(options: { + payment: IncomingPayment + credentials: StreamCredentials + openPaymentsUrl: string + }): Connection { + return new this( + options.payment.connectionId, + options.openPaymentsUrl, + options.credentials.ilpAddress, + options.credentials.sharedSecret, + options.payment.asset.code, + options.payment.asset.scale + ) + } + + private constructor( + public readonly id: string, + private readonly openPaymentsUrl: string, + ilpAddress: IlpAddress, + sharedSecret: Buffer, + assetCode: string, + assetScale: number + ) { + super(ilpAddress, sharedSecret, assetCode, assetScale) + } + + public get url(): string { + return `${this.openPaymentsUrl}/connections/${this.id}` + } + + public toJSON(): ConnectionJSON { + return { + id: this.url, + ilpAddress: this.ilpAddress, + sharedSecret: base64url(this.sharedSecret), + assetCode: this.assetCode, + assetScale: this.assetScale + } + } + + public toOpenPaymentsType(): ILPStreamConnection { + return { + id: this.url, + ilpAddress: this.ilpAddress, + sharedSecret: base64url(this.sharedSecret), + assetCode: this.assetCode, + assetScale: this.assetScale + } + } +} diff --git a/packages/backend/src/open_payments/connection/service.ts b/packages/backend/src/open_payments/connection/service.ts index 6e2242461e..50ce11852f 100644 --- a/packages/backend/src/open_payments/connection/service.ts +++ b/packages/backend/src/open_payments/connection/service.ts @@ -1,68 +1,8 @@ -import base64url from 'base64url' -import { IlpAddress } from 'ilp-packet' -import { StreamCredentials, StreamServer } from '@interledger/stream-receiver' +import { StreamServer } from '@interledger/stream-receiver' import { BaseService } from '../../shared/baseService' import { IncomingPayment } from '../payment/incoming/model' - -export type ConnectionJSON = { - id: string - ilpAddress: IlpAddress - sharedSecret: string - assetCode: string - assetScale: number -} - -export abstract class ConnectionBase { - protected constructor( - public readonly ilpAddress: IlpAddress, - public readonly sharedSecret: Buffer, - public readonly assetCode: string, - public readonly assetScale: number - ) {} -} - -export class Connection extends ConnectionBase { - static fromPayment(options: { - payment: IncomingPayment - credentials: StreamCredentials - openPaymentsUrl: string - }): Connection { - return new this( - options.payment.connectionId, - options.openPaymentsUrl, - options.credentials.ilpAddress, - options.credentials.sharedSecret, - options.payment.asset.code, - options.payment.asset.scale - ) - } - - private constructor( - public readonly id: string, - private readonly openPaymentsUrl: string, - ilpAddress: IlpAddress, - sharedSecret: Buffer, - assetCode: string, - assetScale: number - ) { - super(ilpAddress, sharedSecret, assetCode, assetScale) - } - - public get url(): string { - return `${this.openPaymentsUrl}/connections/${this.id}` - } - - public toJSON(): ConnectionJSON { - return { - id: this.url, - ilpAddress: this.ilpAddress, - sharedSecret: base64url(this.sharedSecret), - assetCode: this.assetCode, - assetScale: this.assetScale - } - } -} +import { Connection } from './model' export interface ConnectionService { get(payment: IncomingPayment): Connection | undefined diff --git a/packages/backend/src/open_payments/payment/incoming/model.ts b/packages/backend/src/open_payments/payment/incoming/model.ts index 7f4b141134..51fc533dbe 100644 --- a/packages/backend/src/open_payments/payment/incoming/model.ts +++ b/packages/backend/src/open_payments/payment/incoming/model.ts @@ -2,7 +2,7 @@ import { Model, ModelOptions, Pojo, QueryContext } from 'objection' import { v4 as uuid } from 'uuid' import { Amount, AmountJSON } from '../../amount' -import { ConnectionJSON } from '../../connection/service' +import { Connection, ConnectionJSON } from '../../connection/model' import { PaymentPointer, PaymentPointerSubresource @@ -11,6 +11,7 @@ import { Asset } from '../../../asset/model' import { LiquidityAccount, OnCreditOptions } from '../../../accounting/service' import { ConnectorAccount } from '../../../connector/core/rafiki' import { WebhookEvent } from '../../../webhook/model' +import { IncomingPayment as OpenPaymentsIncomingPayment } from 'open-payments' export enum IncomingPaymentEventType { IncomingPaymentExpired = 'incoming_payment.expired', @@ -237,6 +238,30 @@ export class IncomingPayment } return payment } + + public toOpenPaymentsType({ + ilpStreamConnection + }: { + ilpStreamConnection: Connection + }): OpenPaymentsIncomingPayment { + return { + id: this.id, + paymentPointer: this.paymentPointer.url, + incomingAmount: { + ...this.incomingAmount, + value: this.incomingAmount.value.toString() + }, + receivedAmount: { + ...this.receivedAmount, + value: this.receivedAmount.value.toString() + }, + completed: this.completed, + createdAt: this.createdAt.toISOString(), + updatedAt: this.updatedAt.toISOString(), + expiresAt: this.expiresAt.toISOString(), + ilpStreamConnection: ilpStreamConnection.toOpenPaymentsType() + } + } } // TODO: disallow undefined diff --git a/packages/backend/src/open_payments/payment/incoming/routes.ts b/packages/backend/src/open_payments/payment/incoming/routes.ts index cfbf71b680..b4ad3a2c6f 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.ts @@ -16,7 +16,8 @@ import { } from './errors' import { AmountJSON, parseAmount } from '../../amount' import { listSubresource } from '../../payment_pointer/routes' -import { ConnectionJSON, ConnectionService } from '../../connection/service' +import { ConnectionJSON } from '../../connection/model' +import { ConnectionService } from '../../connection/service' // Don't allow creating an incoming payment too far out. Incoming payments with no payments before they expire are cleaned up, since incoming payments creation is unauthenticated. // TODO what is a good default value for this? diff --git a/packages/backend/src/open_payments/receiver/model.ts b/packages/backend/src/open_payments/receiver/model.ts new file mode 100644 index 0000000000..71a55df2d5 --- /dev/null +++ b/packages/backend/src/open_payments/receiver/model.ts @@ -0,0 +1,99 @@ +import { Counter, ResolvedPayment } from '@interledger/pay' +import base64url from 'base64url' + +import { Amount, parseAmount } from '../amount' +import { AssetOptions } from '../../asset/service' +import { + IncomingPayment as OpenPaymentsIncomingPayment, + ILPStreamConnection as OpenPaymentsConnection +} from 'open-payments' +import { ConnectionBase } from '../connection/model' +import { isValidIlpAddress } from 'ilp-packet' + +export class Receiver extends ConnectionBase { + static fromConnection(connection: OpenPaymentsConnection): Receiver { + return new this(connection) + } + + static fromIncomingPayment( + incomingPayment: OpenPaymentsIncomingPayment + ): Receiver | undefined { + if (incomingPayment.completed) { + return undefined + } + if (typeof incomingPayment.ilpStreamConnection !== 'object') { + return undefined + } + if ( + incomingPayment.expiresAt && + new Date(incomingPayment.expiresAt).getTime() <= Date.now() + ) { + return undefined + } + const receivedAmount = parseAmount(incomingPayment.receivedAmount) + const incomingAmount = incomingPayment.incomingAmount + ? parseAmount(incomingPayment.incomingAmount) + : undefined + + return new this( + incomingPayment.ilpStreamConnection, + incomingAmount?.value, + receivedAmount.value + ) + } + + private constructor( + connection: OpenPaymentsConnection, + private readonly incomingAmountValue?: bigint, + private readonly receivedAmountValue?: bigint + ) { + if (!isValidIlpAddress(connection.ilpAddress)) { + throw new Error('Connection has invalid destination address') + } + + super( + connection.ilpAddress, + base64url.toBuffer(connection.sharedSecret), + connection.assetCode, + connection.assetScale + ) + } + + public get asset(): AssetOptions { + return { + code: this.assetCode, + scale: this.assetScale + } + } + + public get incomingAmount(): Amount | undefined { + if (this.incomingAmountValue) { + return { + value: this.incomingAmountValue, + assetCode: this.assetCode, + assetScale: this.assetScale + } + } + return undefined + } + + public get receivedAmount(): Amount | undefined { + if (this.receivedAmountValue !== undefined) { + return { + value: this.receivedAmountValue, + assetCode: this.assetCode, + assetScale: this.assetScale + } + } + return undefined + } + + public toResolvedPayment(): ResolvedPayment { + return { + destinationAsset: this.asset, + destinationAddress: this.ilpAddress, + sharedSecret: this.sharedSecret, + requestCounter: Counter.from(0) + } + } +} diff --git a/packages/backend/src/open_payments/receiver/service.ts b/packages/backend/src/open_payments/receiver/service.ts index 929fbf516b..2b54f436fa 100644 --- a/packages/backend/src/open_payments/receiver/service.ts +++ b/packages/backend/src/open_payments/receiver/service.ts @@ -1,115 +1,22 @@ -import { Counter, ResolvedPayment } from '@interledger/pay' -import base64url from 'base64url' +import { parseAmount } from '../amount' -import { Amount, parseAmount } from '../amount' +import { ConnectionService } from '../connection/service' -import { - ConnectionBase, - ConnectionJSON, - ConnectionService -} from '../connection/service' -import { IncomingPaymentJSON } from '../payment/incoming/model' import { PaymentPointerService } from '../payment_pointer/service' -import { ReadContext } from '../../app' -import { AssetOptions } from '../../asset/service' import { BaseService } from '../../shared/baseService' import { OpenPaymentsClient, - IncomingPayment, - ILPStreamConnection + IncomingPayment as OpenPaymentsIncomingPayment, + ILPStreamConnection as OpenPaymentsConnection } from 'open-payments' import { IncomingPaymentService } from '../payment/incoming/service' import { PaymentPointer } from '../payment_pointer/model' +import { Receiver } from './model' -export class Receiver extends ConnectionBase { - static fromConnection(connection: ConnectionJSON): Receiver { - return new this(connection) - } - - static fromIncomingPayment( - incomingPayment: IncomingPayment - ): Receiver | undefined { - if (incomingPayment.completed) { - return undefined - } - if (typeof incomingPayment.ilpStreamConnection !== 'object') { - return undefined - } - if ( - incomingPayment.expiresAt && - new Date(incomingPayment.expiresAt).getTime() <= Date.now() - ) { - return undefined - } - const receivedAmount = parseAmount(incomingPayment.receivedAmount) - const incomingAmount = incomingPayment.incomingAmount - ? parseAmount(incomingPayment.incomingAmount) - : undefined - - return new this( - incomingPayment.ilpStreamConnection, - incomingAmount?.value, - receivedAmount.value - ) - } - - private constructor( - connection: ConnectionJSON, - private readonly incomingAmountValue?: bigint, - private readonly receivedAmountValue?: bigint - ) { - super( - connection.ilpAddress, - base64url.toBuffer(connection.sharedSecret), - connection.assetCode, - connection.assetScale - ) - } - - public get asset(): AssetOptions { - return { - code: this.assetCode, - scale: this.assetScale - } - } - - public get incomingAmount(): Amount | undefined { - if (this.incomingAmountValue) { - return { - value: this.incomingAmountValue, - assetCode: this.assetCode, - assetScale: this.assetScale - } - } - return undefined - } - - public get receivedAmount(): Amount | undefined { - if (this.receivedAmountValue !== undefined) { - return { - value: this.receivedAmountValue, - assetCode: this.assetCode, - assetScale: this.assetScale - } - } - return undefined - } - - public toResolvedPayment(): ResolvedPayment { - return { - destinationAsset: this.asset, - destinationAddress: this.ilpAddress, - sharedSecret: this.sharedSecret, - requestCounter: Counter.from(0) - } - } -} - +// A receiver is resolved from an incoming payment or a connection export interface ReceiverService { - receiver: { - get(url: string): Promise - } + get(url: string): Promise } interface ServiceDependencies extends BaseService { @@ -121,6 +28,10 @@ interface ServiceDependencies extends BaseService { openPaymentsClient: OpenPaymentsClient } +const CONNECTION_URL_REGEX = /\/connections\/(.){36}$/ +const INCOMING_PAYMENT_URL_REGEX = + /(?^(.)+)\/incoming-payments\/(?(.){36}$)/ + export async function createReceiverService( deps_: ServiceDependencies ): Promise { @@ -133,8 +44,23 @@ export async function createReceiverService( } return { - receiver: { - get: (url) => getReceiver(deps, url) + get: (url) => getReceiver(deps, url) + } +} + +async function getReceiver( + deps: ServiceDependencies, + url: string +): Promise { + if (url.match(CONNECTION_URL_REGEX)) { + const connection = await getConnection(deps, url) + if (connection) { + return Receiver.fromConnection(connection) + } + } else { + const incomingPayment = await getIncomingPayment(deps, url) + if (incomingPayment) { + return Receiver.fromIncomingPayment(incomingPayment) } } } @@ -142,7 +68,7 @@ export async function createReceiverService( async function getLocalConnection( deps: ServiceDependencies, url: string -): Promise { +): Promise { const incomingPayment = await deps.incomingPaymentService.getByConnection( url.slice(-36) ) @@ -153,13 +79,17 @@ async function getLocalConnection( const connection = deps.connectionService.get(incomingPayment) - return connection ? connection.toJSON() : undefined + if (!connection) { + return + } + + return connection.toOpenPaymentsType() } async function getConnection( deps: ServiceDependencies, url: string -): Promise { +): Promise { try { if (url.startsWith(`${deps.openPaymentsUrl}/connections/`)) { return await getLocalConnection(deps, url) @@ -174,55 +104,47 @@ async function getConnection( } } -const INCOMING_PAYMENT_URL_REGEX = - /(?^(.)+)\/incoming-payments\/(?(.){36}$)/ - -async function getLocalIncomingPayment({ - deps, - id, - paymentPointer -}: { - deps: ServiceDependencies - id: string - paymentPointer: PaymentPointer -}): Promise { - const incomingPayment = await deps.incomingPaymentService.get({ - id, - paymentPointerId: paymentPointer.id - }) - - const connection = deps.connectionService.get(incomingPayment) +function parseIncomingPaymentUrl( + url: string +): { id: string; paymentPointerUrl: string } | undefined { + const match = url.match(INCOMING_PAYMENT_URL_REGEX)?.groups + if (!match || !match.paymentPointerUrl || !match.id) { + return undefined + } return { - ...incomingPayment.toJSON(), - ilpStreamConnection: connection.toJSON() + id: match.id, + paymentPointerUrl: match.paymentPointerUrl } } async function getIncomingPayment( deps: ServiceDependencies, url: string -): Promise { +): Promise { try { - const match = url.match(INCOMING_PAYMENT_URL_REGEX)?.groups - if (!match) { + const urlParseResult = parseIncomingPaymentUrl(url) + if (!urlParseResult) { return undefined } // Check if this is a local payment pointer const paymentPointer = await deps.paymentPointerService.getByUrl( - match.paymentPointerUrl + urlParseResult.paymentPointerUrl ) if (paymentPointer) { - const incoming - await deps.incomingPaymentRoutes.get(ctx) - return ctx.body as IncomingPaymentJSON + return getLocalIncomingPayment({ + deps, + id: urlParseResult.id, + paymentPointer + }) } + const incomingPayment = await deps.openPaymentsClient.incomingPayment.get({ url, accessToken: deps.accessToken }) if (!isValidIncomingPayment(incomingPayment)) { - throw new Error('unreachable') + throw new Error('Invalid incoming payment') } return incomingPayment @@ -231,29 +153,37 @@ async function getIncomingPayment( } } -const CONNECTION_URL_REGEX = /\/connections\/(.){36}$/ +async function getLocalIncomingPayment({ + deps, + id, + paymentPointer +}: { + deps: ServiceDependencies + id: string + paymentPointer: PaymentPointer +}): Promise { + const incomingPayment = await deps.incomingPaymentService.get({ + id, + paymentPointerId: paymentPointer.id + }) -async function getReceiver( - deps: ServiceDependencies, - url: string -): Promise { - if (url.match(CONNECTION_URL_REGEX)) { - const connection = await getConnection(deps, url) - if (connection) { - return Receiver.fromConnection(connection) - } - } else { - const incomingPayment = await getIncomingPayment(deps, url) - if (incomingPayment) { - return Receiver.fromIncomingPayment(incomingPayment) - } + if (!incomingPayment) { + return undefined } + + const connection = deps.connectionService.get(incomingPayment) + + if (!connection) { + return undefined + } + + return incomingPayment.toOpenPaymentsType({ ilpStreamConnection: connection }) } // Validate referential integrity, which cannot be represented in OpenAPI function isValidIncomingPayment( - payment: IncomingPayment -): payment is IncomingPayment { + payment: OpenPaymentsIncomingPayment +): payment is OpenPaymentsIncomingPayment { if (payment.incomingAmount) { const incomingAmount = parseAmount(payment.incomingAmount) const receivedAmount = parseAmount(payment.receivedAmount) From ad74758cfcbfcf6e04d40b508cc569df1fd58d0e Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Thu, 27 Oct 2022 01:23:50 +0200 Subject: [PATCH 39/56] feat(backend): start using receiver service --- packages/backend/src/app.ts | 2 ++ packages/backend/src/index.ts | 23 +++++++++++++++++-- .../open_payments/payment/outgoing/service.ts | 6 ++--- .../src/open_payments/quote/service.ts | 7 +++--- .../src/open_payments/receiver/service.ts | 14 +++++------ 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 130d749513..a55b7ce329 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -50,6 +50,7 @@ import { createValidatorMiddleware, HttpMethod, isHttpMethod } from 'openapi' import { ClientKeysService } from './clientKeys/service' import { ClientService } from './clients/service' import { GrantReferenceService } from './open_payments/grantReference/service' +import { OpenPaymentsClient } from 'open-payments' export interface AppContextData { logger: Logger @@ -154,6 +155,7 @@ export interface AppServices { clientService: Promise clientKeysService: Promise grantReferenceService: Promise + openPaymentsClient: Promise } export type AppContainer = IocContract diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index e2efa9d029..a83e243801 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -37,11 +37,13 @@ import { createConnectorService } from './connector' import { createSessionService } from './session/service' import { createApiKeyService } from './apiKey/service' import { createOpenAPI } from 'openapi' +import { createClient as createOpenPaymentsClient } from 'open-payments' import { createConnectionService } from './open_payments/connection/service' import { createConnectionRoutes } from './open_payments/connection/routes' import { createClientKeysService } from './clientKeys/service' import { createClientService } from './clients/service' import { createGrantReferenceService } from './open_payments/grantReference/service' +import { createReceiverService } from './open_payments/receiver/service' BigInt.prototype.toJSON = function () { return this.toString() @@ -113,6 +115,10 @@ export function initIocContainer( const config = await deps.use('config') return await createOpenAPI(config.authServerSpec) }) + container.singleton('openPaymentsClient', async (deps) => { + const logger = await deps.use('logger') + return createOpenPaymentsClient({ logger }) + }) /** * Add services to the container. @@ -229,6 +235,19 @@ export function initIocContainer( connectionService: await deps.use('connectionService') }) }) + container.singleton('receiverService', async (deps) => { + const config = await deps.use('config') + return await createReceiverService({ + logger: await deps.use('logger'), + // TODO: https://github.com/interledger/rafiki/issues/583 + accessToken: config.devAccessToken, + connectionService: await deps.use('connectionService'), + incomingPaymentService: await deps.use('incomingPaymentService'), + openPaymentsUrl: config.openPaymentsUrl, + paymentPointerService: await deps.use('paymentPointerService'), + openPaymentsClient: await deps.use('openPaymentsClient') + }) + }) container.singleton('openPaymentsClientService', async (deps) => { const config = await deps.use('config') return await createOpenPaymentsClientService({ @@ -288,7 +307,7 @@ export function initIocContainer( logger: await deps.use('logger'), knex: await deps.use('knex'), makeIlpPlugin: await deps.use('makeIlpPlugin'), - clientService: await deps.use('openPaymentsClientService'), + receiverService: await deps.use('receiverService'), paymentPointerService: await deps.use('paymentPointerService'), ratesService: await deps.use('ratesService') }) @@ -308,7 +327,7 @@ export function initIocContainer( logger: await deps.use('logger'), knex: await deps.use('knex'), accountingService: await deps.use('accountingService'), - clientService: await deps.use('openPaymentsClientService'), + receiverService: await deps.use('receiverService'), grantReferenceService: await deps.use('grantReferenceService'), makeIlpPlugin: await deps.use('makeIlpPlugin'), peerService: await deps.use('peerService') diff --git a/packages/backend/src/open_payments/payment/outgoing/service.ts b/packages/backend/src/open_payments/payment/outgoing/service.ts index e5066e33ca..d6adbaf135 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.ts @@ -15,7 +15,7 @@ import { import { AccountingService } from '../../../accounting/service' import { PeerService } from '../../../peer/service' import { Grant, AccessLimits, getInterval } from '../../auth/grant' -import { OpenPaymentsClientService } from '../../client/service' +import { ReceiverService } from '../../receiver/service' import { GetOptions, ListOptions } from '../../payment_pointer/model' import { PaymentPointerSubresourceService } from '../../payment_pointer/service' import { IlpPlugin, IlpPluginOptions } from '../../../shared/ilp_plugin' @@ -43,7 +43,7 @@ export interface OutgoingPaymentService export interface ServiceDependencies extends BaseService { knex: TransactionOrKnex accountingService: AccountingService - clientService: OpenPaymentsClientService + receiverService: ReceiverService peerService: PeerService grantReferenceService: GrantReferenceService makeIlpPlugin: (options: IlpPluginOptions) => IlpPlugin @@ -126,7 +126,7 @@ async function createOutgoingPayment( throw OutgoingPaymentError.InsufficientGrant } } - const receiver = await deps.clientService.receiver.get(payment.receiver) + const receiver = await deps.receiverService.get(payment.receiver) if (!receiver) { throw OutgoingPaymentError.InvalidQuote } diff --git a/packages/backend/src/open_payments/quote/service.ts b/packages/backend/src/open_payments/quote/service.ts index 2ab12f0faf..8b9a535ed0 100644 --- a/packages/backend/src/open_payments/quote/service.ts +++ b/packages/backend/src/open_payments/quote/service.ts @@ -8,7 +8,8 @@ import { BaseService } from '../../shared/baseService' import { QuoteError, isQuoteError } from './errors' import { Quote } from './model' import { Amount, parseAmount } from '../amount' -import { OpenPaymentsClientService, Receiver } from '../client/service' +import { ReceiverService } from '../receiver/service' +import { Receiver } from '../receiver/model' import { PaymentPointer, GetOptions, @@ -34,7 +35,7 @@ export interface ServiceDependencies extends BaseService { quoteLifespan: number // milliseconds signatureSecret?: string signatureVersion: number - clientService: OpenPaymentsClientService + receiverService: ReceiverService paymentPointerService: PaymentPointerService ratesService: RatesService makeIlpPlugin: (options: IlpPluginOptions) => IlpPlugin @@ -167,7 +168,7 @@ export async function resolveReceiver( deps: ServiceDependencies, options: CreateQuoteOptions ): Promise { - const receiver = await deps.clientService.receiver.get(options.receiver) + const receiver = await deps.receiverService.get(options.receiver) if (!receiver) { throw QuoteError.InvalidReceiver } diff --git a/packages/backend/src/open_payments/receiver/service.ts b/packages/backend/src/open_payments/receiver/service.ts index 2b54f436fa..41d15ff902 100644 --- a/packages/backend/src/open_payments/receiver/service.ts +++ b/packages/backend/src/open_payments/receiver/service.ts @@ -1,15 +1,13 @@ -import { parseAmount } from '../amount' - -import { ConnectionService } from '../connection/service' - -import { PaymentPointerService } from '../payment_pointer/service' -import { BaseService } from '../../shared/baseService' - import { OpenPaymentsClient, IncomingPayment as OpenPaymentsIncomingPayment, ILPStreamConnection as OpenPaymentsConnection } from 'open-payments' + +import { parseAmount } from '../amount' +import { ConnectionService } from '../connection/service' +import { PaymentPointerService } from '../payment_pointer/service' +import { BaseService } from '../../shared/baseService' import { IncomingPaymentService } from '../payment/incoming/service' import { PaymentPointer } from '../payment_pointer/model' import { Receiver } from './model' @@ -132,7 +130,7 @@ async function getIncomingPayment( urlParseResult.paymentPointerUrl ) if (paymentPointer) { - return getLocalIncomingPayment({ + return await getLocalIncomingPayment({ deps, id: urlParseResult.id, paymentPointer From df3bcf4a0f589588378e439717575b7f3bf376f6 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Thu, 27 Oct 2022 01:48:03 +0200 Subject: [PATCH 40/56] feat(backend): start using receiver service --- .../backend/src/open_payments/payment/outgoing/lifecycle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts index 306f098910..953e88ec53 100644 --- a/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts +++ b/packages/backend/src/open_payments/payment/outgoing/lifecycle.ts @@ -8,7 +8,7 @@ import { PaymentEventType } from './model' import { ServiceDependencies } from './service' -import { Receiver } from '../../client/service' +import { Receiver } from '../../receiver/model' import { IlpPlugin } from '../../../shared/ilp_plugin' // "payment" is locked by the "deps.knex" transaction. @@ -19,7 +19,7 @@ export async function handleSending( ): Promise { if (!payment.quote) throw LifecycleError.MissingQuote - const receiver = await deps.clientService.receiver.get(payment.receiver) + const receiver = await deps.receiverService.get(payment.receiver) // TODO: Query Tigerbeetle transfers by code to distinguish sending debits from withdrawals const amountSent = await deps.accountingService.getTotalSent(payment.id) From eaa4e87a181820971cea58c7d075e44b826a34d2 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Fri, 28 Oct 2022 13:41:52 +0200 Subject: [PATCH 41/56] feat(backend): add open-payments build as a test workflow step --- .github/workflows/lint_test_build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint_test_build.yml b/.github/workflows/lint_test_build.yml index c5848f174e..a3f45429b2 100644 --- a/.github/workflows/lint_test_build.yml +++ b/.github/workflows/lint_test_build.yml @@ -27,6 +27,7 @@ jobs: - uses: ./.github/workflows/rafiki/env-setup - run: pnpm --filter openapi build - run: pnpm --filter auth build + - run: pnpm --filter open-payments build - run: pnpm --filter backend test frontend: From 6c65e96b76338bd248b67d7caa5f8534aef7922e Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Fri, 28 Oct 2022 13:42:21 +0200 Subject: [PATCH 42/56] feat(backend): add initial test for receiver service --- .../open_payments/receiver/service.test.ts | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 packages/backend/src/open_payments/receiver/service.test.ts diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts new file mode 100644 index 0000000000..32d4b513c9 --- /dev/null +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -0,0 +1,188 @@ +import { IocContract } from '@adonisjs/fold' +import { faker } from '@faker-js/faker' +import axios from 'axios' +import { Knex } from 'knex' +import nock from 'nock' +import { URL } from 'url' +import { v4 as uuid } from 'uuid' + +import { ReceiverService } from './service' +import { createTestApp, TestContainer } from '../../tests/app' +import { Config } from '../../config/app' +import { initIocContainer } from '../..' +import { AppServices } from '../../app' +import { createIncomingPayment } from '../../tests/incomingPayment' +import { createPaymentPointer } from '../../tests/paymentPointer' +import { truncateTables } from '../../tests/tableManager' + +describe('Receiver Service', (): void => { + let deps: IocContract + let appContainer: TestContainer + let receiverService: ReceiverService + let knex: Knex + + const CONNECTION_PATH = 'connections' + const INCOMING_PAYMENT_PATH = 'incoming-payments' + + beforeAll(async (): Promise => { + deps = initIocContainer(Config) + appContainer = await createTestApp(deps) + receiverService = await deps.use('receiverService') + knex = await deps.use('knex') + }) + + afterEach(async (): Promise => { + jest.useRealTimers() + await truncateTables(knex) + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + describe.each` + local | description + ${true} | ${'local'} + ${false} | ${'remote'} + `('get - $description', ({ local }): void => { + describe.each` + urlPath | description + ${CONNECTION_PATH} | ${'connection'} + ${INCOMING_PAYMENT_PATH} | ${'incoming payment'} + `('$description', ({ urlPath }): void => { + if (urlPath === CONNECTION_PATH) { + test('resolves connection from Open Payments server', async (): Promise => { + const paymentPointer = await createPaymentPointer(deps) + const { connectionId } = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + const localUrl = `${Config.openPaymentsUrl}/${urlPath}/${connectionId}` + const remoteUrl = new URL( + `${faker.internet.url()}/${urlPath}/${connectionId}` + ) + const scope = nock(remoteUrl.origin) + .get(remoteUrl.pathname) + .reply(200, function () { + return axios + .get(localUrl, { + headers: this.req.headers + }) + .then((res) => res.data) + }) + + await expect( + receiverService.get(local ? localUrl : remoteUrl.href) + ).resolves.toMatchObject({ + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale, + incomingAmount: undefined, + receivedAmount: undefined, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer) + }) + expect(local).not.toEqual(scope.isDone()) + }) + if (local) { + test('returns undefined for unknown connection', async (): Promise => { + await expect( + receiverService.get( + `${Config.openPaymentsUrl}/${urlPath}/${uuid()}` + ) + ).resolves.toBeUndefined() + }) + } + } else { + test.each` + incomingAmount | description | externalRef + ${undefined} | ${undefined} | ${undefined} + ${BigInt(123)} | ${'Test'} | ${'#123'} + `( + 'resolves incoming payment from Open Payments server', + async ({ + incomingAmount, + description, + externalRef + }): Promise => { + const paymentPointer = await createPaymentPointer(deps, { + mockServerPort: appContainer.openPaymentsPort + }) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id, + description, + incomingAmount: incomingAmount && { + value: incomingAmount, + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale + }, + externalRef + }) + let spy: jest.SpyInstance + if (!local) { + const paymentPointerService = await deps.use( + 'paymentPointerService' + ) + spy = jest + .spyOn(paymentPointerService, 'getByUrl') + .mockResolvedValueOnce(undefined) + } + const receiver = await receiverService.get(incomingPayment.url) + if (!local) { + expect(spy).toHaveBeenCalledWith(paymentPointer.url) + } + expect(local).not.toEqual(paymentPointer.scope.isDone()) + expect(receiver).toMatchObject({ + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale, + incomingAmount: incomingPayment.incomingAmount, + receivedAmount: incomingPayment.receivedAmount, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer) + }) + } + ) + if (local) { + test('returns undefined for unknown incoming payment', async (): Promise => { + const paymentPointer = await createPaymentPointer(deps) + await expect( + receiverService.get(`${paymentPointer.url}/${urlPath}/${uuid()}`) + ).resolves.toBeUndefined() + }) + } + } + if (!local) { + test.each` + statusCode + ${404} + ${500} + `( + 'returns undefined for unsuccessful request ($statusCode)', + async ({ statusCode }): Promise => { + const receiverUrl = new URL( + `${faker.internet.url()}/${urlPath}/${uuid()}` + ) + const scope = nock(receiverUrl.origin) + .get(receiverUrl.pathname) + .reply(statusCode) + await expect( + receiverService.get(receiverUrl.href) + ).resolves.toBeUndefined() + scope.done() + } + ) + test(`returns undefined for invalid response`, async (): Promise => { + const receiverUrl = new URL( + `${faker.internet.url()}/${urlPath}/${uuid()}` + ) + const scope = nock(receiverUrl.origin) + .get(receiverUrl.pathname) + .reply(200, () => ({ + validReceiver: 0 + })) + await expect( + receiverService.get(receiverUrl.href) + ).resolves.toBeUndefined() + scope.done() + }) + } + }) + }) +}) From d997e79ba8402161cf2b6b5974311771872dd6f3 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Fri, 28 Oct 2022 20:06:21 +0200 Subject: [PATCH 43/56] feat(backend): update ilpStreamConnection fetch --- packages/backend/src/open_payments/receiver/service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend/src/open_payments/receiver/service.ts b/packages/backend/src/open_payments/receiver/service.ts index 41d15ff902..29c122e266 100644 --- a/packages/backend/src/open_payments/receiver/service.ts +++ b/packages/backend/src/open_payments/receiver/service.ts @@ -94,8 +94,7 @@ async function getConnection( } return await deps.openPaymentsClient.ilpStreamConnection.get({ - url, - accessToken: deps.accessToken + url }) } catch (_) { return undefined From 2a8cbe95394edb1324c910fd9a3e338d90d3bc7a Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Tue, 1 Nov 2022 12:05:58 +0100 Subject: [PATCH 44/56] feat(backend): removing incoming payment validation from receiver service --- .../src/open_payments/receiver/service.ts | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/packages/backend/src/open_payments/receiver/service.ts b/packages/backend/src/open_payments/receiver/service.ts index 29c122e266..b116d698b6 100644 --- a/packages/backend/src/open_payments/receiver/service.ts +++ b/packages/backend/src/open_payments/receiver/service.ts @@ -4,7 +4,6 @@ import { ILPStreamConnection as OpenPaymentsConnection } from 'open-payments' -import { parseAmount } from '../amount' import { ConnectionService } from '../connection/service' import { PaymentPointerService } from '../payment_pointer/service' import { BaseService } from '../../shared/baseService' @@ -136,15 +135,10 @@ async function getIncomingPayment( }) } - const incomingPayment = await deps.openPaymentsClient.incomingPayment.get({ + return await deps.openPaymentsClient.incomingPayment.get({ url, accessToken: deps.accessToken }) - if (!isValidIncomingPayment(incomingPayment)) { - throw new Error('Invalid incoming payment') - } - - return incomingPayment } catch (_) { return undefined } @@ -176,36 +170,3 @@ async function getLocalIncomingPayment({ return incomingPayment.toOpenPaymentsType({ ilpStreamConnection: connection }) } - -// Validate referential integrity, which cannot be represented in OpenAPI -function isValidIncomingPayment( - payment: OpenPaymentsIncomingPayment -): payment is OpenPaymentsIncomingPayment { - if (payment.incomingAmount) { - const incomingAmount = parseAmount(payment.incomingAmount) - const receivedAmount = parseAmount(payment.receivedAmount) - if ( - incomingAmount.assetCode !== receivedAmount.assetCode || - incomingAmount.assetScale !== receivedAmount.assetScale - ) { - return false - } - if (incomingAmount.value < receivedAmount.value) { - return false - } - if (incomingAmount.value === receivedAmount.value && !payment.completed) { - return false - } - } - if (typeof payment.ilpStreamConnection === 'object') { - if ( - payment.ilpStreamConnection.assetCode !== - payment.receivedAmount.assetCode || - payment.ilpStreamConnection.assetScale !== - payment.receivedAmount.assetScale - ) { - return false - } - } - return true -} From 6ea72057ac40d3a778b476f122205fb0071e9037 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Tue, 1 Nov 2022 12:06:38 +0100 Subject: [PATCH 45/56] feat(backend): start of receiver service tests --- .../open_payments/receiver/service.test.ts | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index 32d4b513c9..54e37f9329 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -14,20 +14,24 @@ import { AppServices } from '../../app' import { createIncomingPayment } from '../../tests/incomingPayment' import { createPaymentPointer } from '../../tests/paymentPointer' import { truncateTables } from '../../tests/tableManager' +import { ConnectionService } from '../connection/service' +import { OpenPaymentsClient } from 'open-payments' describe('Receiver Service', (): void => { let deps: IocContract let appContainer: TestContainer let receiverService: ReceiverService + let openPaymentsClient: OpenPaymentsClient let knex: Knex - const CONNECTION_PATH = 'connections' const INCOMING_PAYMENT_PATH = 'incoming-payments' + const CONNECTION_PATH = 'connections' beforeAll(async (): Promise => { deps = initIocContainer(Config) appContainer = await createTestApp(deps) receiverService = await deps.use('receiverService') + openPaymentsClient = await deps.use('openPaymentsClient') knex = await deps.use('knex') }) @@ -39,7 +43,71 @@ describe('Receiver Service', (): void => { afterAll(async (): Promise => { await appContainer.shutdown() }) - describe.each` + + describe('get', () => { + describe('connections', () => { + const CONNECTION_PATH = 'connections' + + test('resolves local connection', async () => { + const paymentPointer = await createPaymentPointer(deps) + const { connectionId } = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + const localUrl = `${Config.openPaymentsUrl}/${CONNECTION_PATH}/${connectionId}` + + await expect(receiverService.get(localUrl)).resolves.toMatchObject({ + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale, + incomingAmount: undefined, + receivedAmount: undefined, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer) + }) + }) + + test('resolves remote connection', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + + const remoteUrl = new URL( + `${faker.internet.url()}/${CONNECTION_PATH}/${ + incomingPayment.connectionId + }` + ) + const scope = nock(remoteUrl.origin) + .get(remoteUrl.pathname) + .reply(200, async function () { + const connectionService: ConnectionService = await deps.use( + 'connectionService' + ) + + return connectionService.get(incomingPayment) + }) + + const clientGetConnectionSpy = jest.spyOn( + openPaymentsClient, + 'ilpStreamConnection', + 'get' + ) + + await expect( + receiverService.get(remoteUrl.href) + ).resolves.toMatchObject({ + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale, + incomingAmount: undefined, + receivedAmount: undefined, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer) + }) + expect(clientGetConnectionSpy).toHaveBeenCalledWith(remoteUrl.href) + }) + }) + }) + + describe.skip.each` local | description ${true} | ${'local'} ${false} | ${'remote'} From 042c8fd26f1068a214c8ba015218d61d399940e3 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 2 Nov 2022 13:16:51 +0100 Subject: [PATCH 46/56] feat(backend): adding logs in service --- .../backend/src/open_payments/receiver/service.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/open_payments/receiver/service.ts b/packages/backend/src/open_payments/receiver/service.ts index b116d698b6..491cb0a551 100644 --- a/packages/backend/src/open_payments/receiver/service.ts +++ b/packages/backend/src/open_payments/receiver/service.ts @@ -95,7 +95,12 @@ async function getConnection( return await deps.openPaymentsClient.ilpStreamConnection.get({ url }) - } catch (_) { + } catch (error) { + deps.logger.error( + { errorMessage: error?.message }, + 'Could not get connection' + ) + return undefined } } @@ -139,7 +144,11 @@ async function getIncomingPayment( url, accessToken: deps.accessToken }) - } catch (_) { + } catch (error) { + deps.logger.error( + { errorMessage: error?.message }, + 'Could not get incoming payment' + ) return undefined } } From 08c2708008220bcf58999208c9694ca0983fdcd9 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 2 Nov 2022 13:17:28 +0100 Subject: [PATCH 47/56] feat(backend): adding tests for service --- .../open_payments/receiver/service.test.ts | 291 ++++++++---------- 1 file changed, 127 insertions(+), 164 deletions(-) diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index 54e37f9329..6184a8c344 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -1,8 +1,5 @@ import { IocContract } from '@adonisjs/fold' -import { faker } from '@faker-js/faker' -import axios from 'axios' import { Knex } from 'knex' -import nock from 'nock' import { URL } from 'url' import { v4 as uuid } from 'uuid' @@ -16,6 +13,7 @@ import { createPaymentPointer } from '../../tests/paymentPointer' import { truncateTables } from '../../tests/tableManager' import { ConnectionService } from '../connection/service' import { OpenPaymentsClient } from 'open-payments' +import { PaymentPointerService } from '../payment_pointer/service' describe('Receiver Service', (): void => { let deps: IocContract @@ -23,15 +21,16 @@ describe('Receiver Service', (): void => { let receiverService: ReceiverService let openPaymentsClient: OpenPaymentsClient let knex: Knex - - const INCOMING_PAYMENT_PATH = 'incoming-payments' - const CONNECTION_PATH = 'connections' + let connectionService: ConnectionService + let paymentPointerService: PaymentPointerService beforeAll(async (): Promise => { deps = initIocContainer(Config) appContainer = await createTestApp(deps) receiverService = await deps.use('receiverService') openPaymentsClient = await deps.use('openPaymentsClient') + connectionService = await deps.use('connectionService') + paymentPointerService = await deps.use('paymentPointerService') knex = await deps.use('knex') }) @@ -49,12 +48,17 @@ describe('Receiver Service', (): void => { const CONNECTION_PATH = 'connections' test('resolves local connection', async () => { - const paymentPointer = await createPaymentPointer(deps) + const paymentPointer = await createPaymentPointer(deps, { + mockServerPort: Config.openPaymentsPort + }) const { connectionId } = await createIncomingPayment(deps, { paymentPointerId: paymentPointer.id }) + const localUrl = `${Config.openPaymentsUrl}/${CONNECTION_PATH}/${connectionId}` + console.log({ paymentPointer, localUrl }) + await expect(receiverService.get(localUrl)).resolves.toMatchObject({ assetCode: paymentPointer.asset.code, assetScale: paymentPointer.asset.scale, @@ -72,25 +76,14 @@ describe('Receiver Service', (): void => { }) const remoteUrl = new URL( - `${faker.internet.url()}/${CONNECTION_PATH}/${ - incomingPayment.connectionId - }` + `${paymentPointer.url}/${CONNECTION_PATH}/${incomingPayment.connectionId}` ) - const scope = nock(remoteUrl.origin) - .get(remoteUrl.pathname) - .reply(200, async function () { - const connectionService: ConnectionService = await deps.use( - 'connectionService' - ) - return connectionService.get(incomingPayment) - }) - - const clientGetConnectionSpy = jest.spyOn( - openPaymentsClient, - 'ilpStreamConnection', - 'get' - ) + const clientGetConnectionSpy = jest + .spyOn(openPaymentsClient.ilpStreamConnection, 'get') + .mockImplementationOnce(async () => + connectionService.get(incomingPayment).toOpenPaymentsType() + ) await expect( receiverService.get(remoteUrl.href) @@ -102,155 +95,125 @@ describe('Receiver Service', (): void => { ilpAddress: expect.any(String), sharedSecret: expect.any(Buffer) }) - expect(clientGetConnectionSpy).toHaveBeenCalledWith(remoteUrl.href) + expect(clientGetConnectionSpy).toHaveBeenCalledWith({ + url: remoteUrl.href + }) }) - }) - }) - describe.skip.each` - local | description - ${true} | ${'local'} - ${false} | ${'remote'} - `('get - $description', ({ local }): void => { - describe.each` - urlPath | description - ${CONNECTION_PATH} | ${'connection'} - ${INCOMING_PAYMENT_PATH} | ${'incoming payment'} - `('$description', ({ urlPath }): void => { - if (urlPath === CONNECTION_PATH) { - test('resolves connection from Open Payments server', async (): Promise => { - const paymentPointer = await createPaymentPointer(deps) - const { connectionId } = await createIncomingPayment(deps, { - paymentPointerId: paymentPointer.id - }) - const localUrl = `${Config.openPaymentsUrl}/${urlPath}/${connectionId}` - const remoteUrl = new URL( - `${faker.internet.url()}/${urlPath}/${connectionId}` + test('returns undefined for unknown connection', async (): Promise => { + const paymentPointer = await createPaymentPointer(deps) + + await expect( + receiverService.get( + `${paymentPointer.url}/${CONNECTION_PATH}/${uuid()}` ) - const scope = nock(remoteUrl.origin) - .get(remoteUrl.pathname) - .reply(200, function () { - return axios - .get(localUrl, { - headers: this.req.headers - }) - .then((res) => res.data) - }) + ).resolves.toBeUndefined() + }) + }) - await expect( - receiverService.get(local ? localUrl : remoteUrl.href) - ).resolves.toMatchObject({ - assetCode: paymentPointer.asset.code, - assetScale: paymentPointer.asset.scale, - incomingAmount: undefined, - receivedAmount: undefined, - ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer) - }) - expect(local).not.toEqual(scope.isDone()) + describe('incoming payments', () => { + const INCOMING_PAYMENT_PATH = 'incoming-payments' + + test('resolves local incoming payment', async () => { + const paymentPointer = await createPaymentPointer(deps, { + mockServerPort: Config.openPaymentsPort }) - if (local) { - test('returns undefined for unknown connection', async (): Promise => { - await expect( - receiverService.get( - `${Config.openPaymentsUrl}/${urlPath}/${uuid()}` - ) - ).resolves.toBeUndefined() - }) - } - } else { - test.each` - incomingAmount | description | externalRef - ${undefined} | ${undefined} | ${undefined} - ${BigInt(123)} | ${'Test'} | ${'#123'} - `( - 'resolves incoming payment from Open Payments server', - async ({ - incomingAmount, - description, - externalRef - }): Promise => { - const paymentPointer = await createPaymentPointer(deps, { - mockServerPort: appContainer.openPaymentsPort - }) - const incomingPayment = await createIncomingPayment(deps, { - paymentPointerId: paymentPointer.id, - description, - incomingAmount: incomingAmount && { - value: incomingAmount, - assetCode: paymentPointer.asset.code, - assetScale: paymentPointer.asset.scale - }, - externalRef - }) - let spy: jest.SpyInstance - if (!local) { - const paymentPointerService = await deps.use( - 'paymentPointerService' - ) - spy = jest - .spyOn(paymentPointerService, 'getByUrl') - .mockResolvedValueOnce(undefined) - } - const receiver = await receiverService.get(incomingPayment.url) - if (!local) { - expect(spy).toHaveBeenCalledWith(paymentPointer.url) - } - expect(local).not.toEqual(paymentPointer.scope.isDone()) - expect(receiver).toMatchObject({ - assetCode: paymentPointer.asset.code, - assetScale: paymentPointer.asset.scale, - incomingAmount: incomingPayment.incomingAmount, - receivedAmount: incomingPayment.receivedAmount, - ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer) - }) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id, + incomingAmount: { + value: BigInt(5), + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale } - ) - if (local) { - test('returns undefined for unknown incoming payment', async (): Promise => { - const paymentPointer = await createPaymentPointer(deps) - await expect( - receiverService.get(`${paymentPointer.url}/${urlPath}/${uuid()}`) - ).resolves.toBeUndefined() - }) - } - } - if (!local) { - test.each` - statusCode - ${404} - ${500} - `( - 'returns undefined for unsuccessful request ($statusCode)', - async ({ statusCode }): Promise => { - const receiverUrl = new URL( - `${faker.internet.url()}/${urlPath}/${uuid()}` - ) - const scope = nock(receiverUrl.origin) - .get(receiverUrl.pathname) - .reply(statusCode) - await expect( - receiverService.get(receiverUrl.href) - ).resolves.toBeUndefined() - scope.done() + }) + + await expect( + receiverService.get(incomingPayment.url) + ).resolves.toMatchObject({ + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale, + incomingAmount: incomingPayment.incomingAmount, + receivedAmount: incomingPayment.receivedAmount, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer) + }) + }) + + test('resolves remote incoming payment', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id, + incomingAmount: { + value: BigInt(5), + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale } - ) - test(`returns undefined for invalid response`, async (): Promise => { - const receiverUrl = new URL( - `${faker.internet.url()}/${urlPath}/${uuid()}` + }) + + const clientGetIncomingPaymentSpy = jest + .spyOn(openPaymentsClient.incomingPayment, 'get') + .mockImplementationOnce(async () => + incomingPayment.toOpenPaymentsType({ + ilpStreamConnection: connectionService.get(incomingPayment) + }) + ) + + jest + .spyOn(paymentPointerService, 'getByUrl') + .mockResolvedValueOnce(undefined) + + await expect( + receiverService.get(incomingPayment.url) + ).resolves.toMatchObject({ + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale, + incomingAmount: incomingPayment.incomingAmount, + receivedAmount: incomingPayment.receivedAmount, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer) + }) + expect(clientGetIncomingPaymentSpy).toHaveBeenCalledWith({ + url: incomingPayment.url, + accessToken: expect.any(String) + }) + }) + + test('returns undefined for unknown incoming payment', async (): Promise => { + const paymentPointer = await createPaymentPointer(deps, { + mockServerPort: Config.openPaymentsPort + }) + + await expect( + receiverService.get( + `${paymentPointer.url}/${INCOMING_PAYMENT_PATH}/${uuid()}` ) - const scope = nock(receiverUrl.origin) - .get(receiverUrl.pathname) - .reply(200, () => ({ - validReceiver: 0 - })) - await expect( - receiverService.get(receiverUrl.href) - ).resolves.toBeUndefined() - scope.done() + ).resolves.toBeUndefined() + }) + + test('returns undefined when fetching remote incoming payment throws', async (): Promise => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id }) - } + + const clientGetIncomingPaymentSpy = jest + .spyOn(openPaymentsClient.incomingPayment, 'get') + .mockImplementationOnce(async () => { + throw new Error('Could not get incoming payment') + }) + + jest + .spyOn(paymentPointerService, 'getByUrl') + .mockResolvedValueOnce(undefined) + + await expect( + receiverService.get(incomingPayment.url) + ).resolves.toBeUndefined() + expect(clientGetIncomingPaymentSpy).toHaveBeenCalledWith({ + url: incomingPayment.url, + accessToken: expect.any(String) + }) + }) }) }) }) From 691a95efed0dfbceff61d347d532179cf16425ec Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 2 Nov 2022 13:18:00 +0100 Subject: [PATCH 48/56] feat(backend): updating methods for models --- .../src/open_payments/payment/incoming/model.ts | 10 ++++++---- packages/backend/src/open_payments/receiver/model.ts | 3 --- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/open_payments/payment/incoming/model.ts b/packages/backend/src/open_payments/payment/incoming/model.ts index 51fc533dbe..8ffb19be27 100644 --- a/packages/backend/src/open_payments/payment/incoming/model.ts +++ b/packages/backend/src/open_payments/payment/incoming/model.ts @@ -247,10 +247,12 @@ export class IncomingPayment return { id: this.id, paymentPointer: this.paymentPointer.url, - incomingAmount: { - ...this.incomingAmount, - value: this.incomingAmount.value.toString() - }, + incomingAmount: this.incomingAmount + ? { + ...this.incomingAmount, + value: this.incomingAmount.value.toString() + } + : undefined, receivedAmount: { ...this.receivedAmount, value: this.receivedAmount.value.toString() diff --git a/packages/backend/src/open_payments/receiver/model.ts b/packages/backend/src/open_payments/receiver/model.ts index 71a55df2d5..a1ca7a060b 100644 --- a/packages/backend/src/open_payments/receiver/model.ts +++ b/packages/backend/src/open_payments/receiver/model.ts @@ -21,9 +21,6 @@ export class Receiver extends ConnectionBase { if (incomingPayment.completed) { return undefined } - if (typeof incomingPayment.ilpStreamConnection !== 'object') { - return undefined - } if ( incomingPayment.expiresAt && new Date(incomingPayment.expiresAt).getTime() <= Date.now() From 581d7f7c446d9f271a94e390a7c98031a7f64fae Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 2 Nov 2022 23:27:40 +0100 Subject: [PATCH 49/56] feat(backend): updating logic for receiver model, adding tests --- .../src/open_payments/receiver/model.test.ts | 135 ++++++++++++++++++ .../src/open_payments/receiver/model.ts | 39 +++-- 2 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 packages/backend/src/open_payments/receiver/model.test.ts diff --git a/packages/backend/src/open_payments/receiver/model.test.ts b/packages/backend/src/open_payments/receiver/model.test.ts new file mode 100644 index 0000000000..89663f3898 --- /dev/null +++ b/packages/backend/src/open_payments/receiver/model.test.ts @@ -0,0 +1,135 @@ +import { IocContract } from '@adonisjs/fold' +import { Knex } from 'knex' + +import { createTestApp, TestContainer } from '../../tests/app' +import { Config } from '../../config/app' +import { initIocContainer } from '../..' +import { AppServices } from '../../app' +import { createIncomingPayment } from '../../tests/incomingPayment' +import { createPaymentPointer } from '../../tests/paymentPointer' +import { truncateTables } from '../../tests/tableManager' +import { ConnectionService } from '../connection/service' +import { Receiver } from './model' +import { IncomingPaymentState } from '../payment/incoming/model' + +describe('Receiver Model', (): void => { + let deps: IocContract + let appContainer: TestContainer + let knex: Knex + let connectionService: ConnectionService + + beforeAll(async (): Promise => { + deps = initIocContainer(Config) + appContainer = await createTestApp(deps) + connectionService = await deps.use('connectionService') + knex = await deps.use('knex') + }) + + afterEach(async (): Promise => { + jest.useRealTimers() + await truncateTables(knex) + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + + describe('fromIncomingPayment', () => { + test('creates receiver', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + + const receiver = Receiver.fromIncomingPayment( + incomingPayment.toOpenPaymentsType({ + ilpStreamConnection: connectionService.get(incomingPayment) + }) + ) + + expect(receiver.asset).toStrictEqual({ + code: incomingPayment.asset.code, + scale: incomingPayment.asset.scale + }) + expect(receiver.incomingAmount).toBeUndefined() + expect(receiver.receivedAmount).toStrictEqual({ + assetCode: incomingPayment.asset.code, + assetScale: incomingPayment.asset.scale, + value: BigInt(0) + }) + expect(receiver.ilpAddress).toEqual(expect.any(String)) + expect(receiver.sharedSecret).toEqual(expect.any(Buffer)) + }) + + test('fails to create receiver if payment completed', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + + incomingPayment.state = IncomingPaymentState.Completed + + const receiver = Receiver.fromIncomingPayment( + incomingPayment.toOpenPaymentsType({ + ilpStreamConnection: connectionService.get(incomingPayment) + }) + ) + + expect(receiver).toBeUndefined() + }) + + test('fails to create receiver if payment expired', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + + incomingPayment.expiresAt = new Date(Date.now() - 1) + + const receiver = Receiver.fromIncomingPayment( + incomingPayment.toOpenPaymentsType({ + ilpStreamConnection: connectionService.get(incomingPayment) + }) + ) + + expect(receiver).toBeUndefined() + }) + }) + + describe('fromConnection', () => { + test('creates receiver', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + + const receiver = Receiver.fromConnection( + connectionService.get(incomingPayment).toOpenPaymentsType() + ) + + expect(receiver.asset).toStrictEqual({ + code: incomingPayment.asset.code, + scale: incomingPayment.asset.scale + }) + expect(receiver.incomingAmount).toBeUndefined() + expect(receiver.receivedAmount).toBeUndefined() + expect(receiver.ilpAddress).toEqual(expect.any(String)) + expect(receiver.sharedSecret).toEqual(expect.any(Buffer)) + }) + + test('returns undefined if invalid ilpAdress', async () => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + + const connection = connectionService + .get(incomingPayment) + .toOpenPaymentsType() + + connection.ilpAddress = 'not base 64 encoded' + + expect(Receiver.fromConnection(connection)).toBeUndefined() + }) + }) +}) diff --git a/packages/backend/src/open_payments/receiver/model.ts b/packages/backend/src/open_payments/receiver/model.ts index a1ca7a060b..1af8317794 100644 --- a/packages/backend/src/open_payments/receiver/model.ts +++ b/packages/backend/src/open_payments/receiver/model.ts @@ -8,11 +8,16 @@ import { ILPStreamConnection as OpenPaymentsConnection } from 'open-payments' import { ConnectionBase } from '../connection/model' -import { isValidIlpAddress } from 'ilp-packet' +import { IlpAddress, isValidIlpAddress } from 'ilp-packet' + +interface OpenPaymentsConnectionWithIlpAddress + extends Omit { + ilpAddress: IlpAddress +} export class Receiver extends ConnectionBase { static fromConnection(connection: OpenPaymentsConnection): Receiver { - return new this(connection) + return this.fromOpenPaymentsConnection(connection) } static fromIncomingPayment( @@ -27,27 +32,43 @@ export class Receiver extends ConnectionBase { ) { return undefined } - const receivedAmount = parseAmount(incomingPayment.receivedAmount) const incomingAmount = incomingPayment.incomingAmount ? parseAmount(incomingPayment.incomingAmount) : undefined + const receivedAmount = parseAmount(incomingPayment.receivedAmount) - return new this( + return this.fromOpenPaymentsConnection( incomingPayment.ilpStreamConnection, incomingAmount?.value, receivedAmount.value ) } - private constructor( + private static fromOpenPaymentsConnection( connection: OpenPaymentsConnection, - private readonly incomingAmountValue?: bigint, - private readonly receivedAmountValue?: bigint - ) { + incomingAmountValue?: bigint, + receivedAmountValue?: bigint + ): Receiver | undefined { if (!isValidIlpAddress(connection.ilpAddress)) { - throw new Error('Connection has invalid destination address') + return undefined + } + + const validConnection = { + id: connection.id, + assetCode: connection.assetCode, + assetScale: connection.assetScale, + sharedSecret: connection.sharedSecret, + ilpAddress: connection.ilpAddress as IlpAddress } + return new this(validConnection, incomingAmountValue, receivedAmountValue) + } + + private constructor( + connection: OpenPaymentsConnectionWithIlpAddress, + private readonly incomingAmountValue?: bigint, + private readonly receivedAmountValue?: bigint + ) { super( connection.ilpAddress, base64url.toBuffer(connection.sharedSecret), From acba42e5432c67bf35953ff5a67f0532233f04de Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 2 Nov 2022 23:29:06 +0100 Subject: [PATCH 50/56] feat(backend): remove log from test --- packages/backend/src/open_payments/receiver/service.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index 6184a8c344..1627e96e2c 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -57,8 +57,6 @@ describe('Receiver Service', (): void => { const localUrl = `${Config.openPaymentsUrl}/${CONNECTION_PATH}/${connectionId}` - console.log({ paymentPointer, localUrl }) - await expect(receiverService.get(localUrl)).resolves.toMatchObject({ assetCode: paymentPointer.asset.code, assetScale: paymentPointer.asset.scale, From e9b41c48c49eee7a1f0a6cc000a4fd51d3495976 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 2 Nov 2022 23:32:35 +0100 Subject: [PATCH 51/56] feat(backend): remove type assertion in model --- .../src/open_payments/receiver/model.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/open_payments/receiver/model.ts b/packages/backend/src/open_payments/receiver/model.ts index 1af8317794..228bcc4dd4 100644 --- a/packages/backend/src/open_payments/receiver/model.ts +++ b/packages/backend/src/open_payments/receiver/model.ts @@ -49,19 +49,23 @@ export class Receiver extends ConnectionBase { incomingAmountValue?: bigint, receivedAmountValue?: bigint ): Receiver | undefined { - if (!isValidIlpAddress(connection.ilpAddress)) { - return undefined - } + const ilpAddress = connection.ilpAddress - const validConnection = { - id: connection.id, - assetCode: connection.assetCode, - assetScale: connection.assetScale, - sharedSecret: connection.sharedSecret, - ilpAddress: connection.ilpAddress as IlpAddress + if (!isValidIlpAddress(ilpAddress)) { + return undefined } - return new this(validConnection, incomingAmountValue, receivedAmountValue) + return new this( + { + id: connection.id, + assetCode: connection.assetCode, + assetScale: connection.assetScale, + sharedSecret: connection.sharedSecret, + ilpAddress + }, + incomingAmountValue, + receivedAmountValue + ) } private constructor( From 39ebc2b3c78595bb4f66b63cc309f39855a1bc58 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 2 Nov 2022 23:45:51 +0100 Subject: [PATCH 52/56] feat(backend): remove openPaymentsClientService --- packages/backend/src/index.ts | 15 +- .../src/open_payments/client/service.test.ts | 195 ----------- .../src/open_payments/client/service.ts | 313 ------------------ packages/backend/src/tests/outgoingPayment.ts | 6 +- packages/backend/src/tests/quote.ts | 4 +- 5 files changed, 6 insertions(+), 527 deletions(-) delete mode 100644 packages/backend/src/open_payments/client/service.test.ts delete mode 100644 packages/backend/src/open_payments/client/service.ts diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index fd7bb24a87..920a5aab39 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -24,7 +24,7 @@ import { createAssetService } from './asset/service' import { createAccountingService } from './accounting/service' import { createPeerService } from './peer/service' import { createAuthService } from './open_payments/auth/service' -import { createOpenPaymentsClientService } from './open_payments/client/service' + import { createPaymentPointerService } from './open_payments/payment_pointer/service' import { createSPSPRoutes } from './spsp/routes' import { createPaymentPointerKeyRoutes } from './paymentPointerKey/routes' @@ -247,19 +247,6 @@ export function initIocContainer( openPaymentsClient: await deps.use('openPaymentsClient') }) }) - container.singleton('openPaymentsClientService', async (deps) => { - const config = await deps.use('config') - return await createOpenPaymentsClientService({ - logger: await deps.use('logger'), - // TODO: https://github.com/interledger/rafiki/issues/583 - accessToken: config.devAccessToken, - connectionRoutes: await deps.use('connectionRoutes'), - incomingPaymentRoutes: await deps.use('incomingPaymentRoutes'), - openApi: await deps.use('openApi'), - openPaymentsUrl: config.openPaymentsUrl, - paymentPointerService: await deps.use('paymentPointerService') - }) - }) container.singleton('ratesService', async (deps) => { const config = await deps.use('config') diff --git a/packages/backend/src/open_payments/client/service.test.ts b/packages/backend/src/open_payments/client/service.test.ts deleted file mode 100644 index db78080147..0000000000 --- a/packages/backend/src/open_payments/client/service.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { IocContract } from '@adonisjs/fold' -import { faker } from '@faker-js/faker' -import axios from 'axios' -import { Knex } from 'knex' -import nock from 'nock' -import { URL } from 'url' -import { v4 as uuid } from 'uuid' - -import { OpenPaymentsClientService } from './service' -import { createTestApp, TestContainer } from '../../tests/app' -import { Config } from '../../config/app' -import { initIocContainer } from '../..' -import { AppServices } from '../../app' -import { createIncomingPayment } from '../../tests/incomingPayment' -import { createPaymentPointer } from '../../tests/paymentPointer' -import { truncateTables } from '../../tests/tableManager' - -describe('Open Payments Client Service', (): void => { - let deps: IocContract - let appContainer: TestContainer - let clientService: OpenPaymentsClientService - let knex: Knex - - const CONNECTION_PATH = 'connections' - const INCOMING_PAYMENT_PATH = 'incoming-payments' - - beforeAll(async (): Promise => { - deps = await initIocContainer(Config) - appContainer = await createTestApp(deps) - clientService = await deps.use('openPaymentsClientService') - knex = await deps.use('knex') - }) - - afterEach(async (): Promise => { - jest.useRealTimers() - await truncateTables(knex) - }) - - afterAll(async (): Promise => { - await appContainer.shutdown() - }) - describe.each` - local | description - ${true} | ${'local'} - ${false} | ${'remote'} - `('receiver.get - $description', ({ local }): void => { - describe.each` - urlPath | description - ${CONNECTION_PATH} | ${'connection'} - ${INCOMING_PAYMENT_PATH} | ${'incoming payment'} - `('$description', ({ urlPath }): void => { - if (urlPath === CONNECTION_PATH) { - test('resolves connection from Open Payments server', async (): Promise => { - const paymentPointer = await createPaymentPointer(deps) - const { connectionId } = await createIncomingPayment(deps, { - paymentPointerId: paymentPointer.id - }) - const localUrl = `${Config.openPaymentsUrl}/${urlPath}/${connectionId}` - const remoteUrl = new URL( - `${faker.internet.url()}/${urlPath}/${connectionId}` - ) - const scope = nock(remoteUrl.origin) - .get(remoteUrl.pathname) - .reply(200, function () { - return axios - .get(localUrl, { - headers: this.req.headers - }) - .then((res) => res.data) - }) - - await expect( - clientService.receiver.get(local ? localUrl : remoteUrl.href) - ).resolves.toMatchObject({ - assetCode: paymentPointer.asset.code, - assetScale: paymentPointer.asset.scale, - incomingAmount: undefined, - receivedAmount: undefined, - ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer), - expiresAt: - urlPath === INCOMING_PAYMENT_PATH ? expect.any(Date) : undefined - }) - expect(local).not.toEqual(scope.isDone()) - }) - if (local) { - test('returns undefined for unknown connection', async (): Promise => { - await expect( - clientService.receiver.get( - `${Config.openPaymentsUrl}/${urlPath}/${uuid()}` - ) - ).resolves.toBeUndefined() - }) - } - } else { - test.each` - incomingAmount | description | externalRef - ${undefined} | ${undefined} | ${undefined} - ${BigInt(123)} | ${'Test'} | ${'#123'} - `( - 'resolves incoming payment from Open Payments server', - async ({ - incomingAmount, - description, - externalRef - }): Promise => { - const paymentPointer = await createPaymentPointer(deps, { - mockServerPort: appContainer.openPaymentsPort - }) - const incomingPayment = await createIncomingPayment(deps, { - paymentPointerId: paymentPointer.id, - description, - incomingAmount: incomingAmount && { - value: incomingAmount, - assetCode: paymentPointer.asset.code, - assetScale: paymentPointer.asset.scale - }, - externalRef - }) - let spy: jest.SpyInstance - if (!local) { - const paymentPointerService = await deps.use( - 'paymentPointerService' - ) - spy = jest - .spyOn(paymentPointerService, 'getByUrl') - .mockResolvedValueOnce(undefined) - } - const receiver = await clientService.receiver.get( - incomingPayment.url - ) - if (!local) { - expect(spy).toHaveBeenCalledWith(paymentPointer.url) - } - expect(local).not.toEqual(paymentPointer.scope.isDone()) - expect(receiver).toMatchObject({ - assetCode: paymentPointer.asset.code, - assetScale: paymentPointer.asset.scale, - incomingAmount: incomingPayment.incomingAmount, - receivedAmount: incomingPayment.receivedAmount, - ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer), - expiresAt: incomingPayment.expiresAt - }) - } - ) - if (local) { - test('returns undefined for unknown incoming payment', async (): Promise => { - const paymentPointer = await createPaymentPointer(deps) - await expect( - clientService.receiver.get( - `${paymentPointer.url}/${urlPath}/${uuid()}` - ) - ).resolves.toBeUndefined() - }) - } - } - if (!local) { - test.each` - statusCode - ${404} - ${500} - `( - 'returns undefined for unsuccessful request ($statusCode)', - async ({ statusCode }): Promise => { - const receiverUrl = new URL( - `${faker.internet.url()}/${urlPath}/${uuid()}` - ) - const scope = nock(receiverUrl.origin) - .get(receiverUrl.pathname) - .reply(statusCode) - await expect( - clientService.receiver.get(receiverUrl.href) - ).resolves.toBeUndefined() - scope.done() - } - ) - test(`returns undefined for invalid response`, async (): Promise => { - const receiverUrl = new URL( - `${faker.internet.url()}/${urlPath}/${uuid()}` - ) - const scope = nock(receiverUrl.origin) - .get(receiverUrl.pathname) - .reply(200, () => ({ - validReceiver: 0 - })) - await expect( - clientService.receiver.get(receiverUrl.href) - ).resolves.toBeUndefined() - scope.done() - }) - } - }) - }) -}) diff --git a/packages/backend/src/open_payments/client/service.ts b/packages/backend/src/open_payments/client/service.ts deleted file mode 100644 index ef3a6543fe..0000000000 --- a/packages/backend/src/open_payments/client/service.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { Counter, ResolvedPayment } from '@interledger/pay' -import { createMockContext } from '@shopify/jest-koa-mocks' -import axios, { AxiosRequestHeaders, AxiosResponse } from 'axios' -import base64url from 'base64url' -import { OpenAPI, HttpMethod, ResponseValidator } from 'openapi' -import { URL } from 'url' - -import { Amount, parseAmount } from '../amount' -import { ConnectionRoutes } from '../connection/routes' -import { ConnectionBase, ConnectionJSON } from '../connection/model' -import { IncomingPaymentJSON } from '../payment/incoming/model' -import { IncomingPaymentRoutes } from '../payment/incoming/routes' -import { PaymentPointerService } from '../payment_pointer/service' -import { ReadContext } from '../../app' -import { AssetOptions } from '../../asset/service' -import { BaseService } from '../../shared/baseService' - -const REQUEST_TIMEOUT = 5_000 // millseconds - -export class Receiver extends ConnectionBase { - static fromConnection(connection: ConnectionJSON): Receiver { - return new this(connection) - } - - static fromIncomingPayment( - incomingPayment: IncomingPaymentJSON - ): Receiver | undefined { - if (incomingPayment.completed) { - return undefined - } - if (typeof incomingPayment.ilpStreamConnection !== 'object') { - return undefined - } - const expiryDate = incomingPayment.expiresAt - ? new Date(incomingPayment.expiresAt) - : undefined - if (expiryDate && expiryDate.getTime() <= Date.now()) { - return undefined - } - const receivedAmount = parseAmount(incomingPayment.receivedAmount) - const incomingAmount = incomingPayment.incomingAmount - ? parseAmount(incomingPayment.incomingAmount) - : undefined - - return new this( - incomingPayment.ilpStreamConnection, - incomingAmount?.value, - receivedAmount.value, - expiryDate - ) - } - - private constructor( - connection: ConnectionJSON, - private readonly incomingAmountValue?: bigint, - private readonly receivedAmountValue?: bigint, - public readonly expiresAt?: Date - ) { - super( - connection.ilpAddress, - base64url.toBuffer(connection.sharedSecret), - connection.assetCode, - connection.assetScale - ) - } - - public get asset(): AssetOptions { - return { - code: this.assetCode, - scale: this.assetScale - } - } - - public get incomingAmount(): Amount | undefined { - if (this.incomingAmountValue) { - return { - value: this.incomingAmountValue, - assetCode: this.assetCode, - assetScale: this.assetScale - } - } - return undefined - } - - public get receivedAmount(): Amount | undefined { - if (this.receivedAmountValue !== undefined) { - return { - value: this.receivedAmountValue, - assetCode: this.assetCode, - assetScale: this.assetScale - } - } - return undefined - } - - public toResolvedPayment(): ResolvedPayment { - return { - destinationAsset: this.asset, - destinationAddress: this.ilpAddress, - sharedSecret: this.sharedSecret, - requestCounter: Counter.from(0) - } - } -} - -export interface OpenPaymentsClientService { - receiver: { - get(url: string): Promise - } -} - -interface ServiceDependencies extends BaseService { - accessToken: string - connectionRoutes: ConnectionRoutes - incomingPaymentRoutes: IncomingPaymentRoutes - openApi: OpenAPI - openPaymentsUrl: string - paymentPointerService: PaymentPointerService - validateConnection: ResponseValidator - validateIncomingPayment: ResponseValidator -} - -export async function createOpenPaymentsClientService( - deps_: Omit< - ServiceDependencies, - 'validateConnection' | 'validateIncomingPayment' - > -): Promise { - const log = deps_.logger.child({ - service: 'OpenPaymentsClientService' - }) - const deps: ServiceDependencies = { - ...deps_, - logger: log, - validateConnection: deps_.openApi.createResponseValidator({ - path: '/connections/{id}', - method: HttpMethod.GET - }), - validateIncomingPayment: - deps_.openApi.createResponseValidator({ - path: '/incoming-payments/{id}', - method: HttpMethod.GET - }) - } - return { - receiver: { - get: (url) => getReceiver(deps, url) - } - } -} - -async function getResource({ - url, - accessToken, - expectedStatus = 200 -}: { - url: string - accessToken?: string - expectedStatus?: number -}): Promise { - const requestUrl = new URL(url) - if (process.env.NODE_ENV === 'development') { - requestUrl.protocol = 'http' - } - const headers: AxiosRequestHeaders = { - 'Content-Type': 'application/json' - } - if (accessToken) { - headers['Authorization'] = `GNAP ${accessToken}` - // TODO: https://github.com/interledger/rafiki/issues/587 - headers['Signature'] = 'TODO' - headers['Signature-Input'] = 'TODO' - } - return await axios.get(requestUrl.href, { - headers, - timeout: REQUEST_TIMEOUT, - validateStatus: (status) => status === expectedStatus - }) -} - -const createReadContext = (params: { id: string }): ReadContext => - createMockContext({ - headers: { Accept: 'application/json' }, - method: 'GET', - customProperties: { - params - } - }) as ReadContext - -async function getConnection( - deps: ServiceDependencies, - url: string -): Promise { - try { - // Check if this is a local incoming payment connection - if (url.startsWith(`${deps.openPaymentsUrl}/connections/`)) { - const ctx = createReadContext({ - id: url.slice(-36) - }) - await deps.connectionRoutes.get(ctx) - return ctx.body as ConnectionJSON - } - const { status, data } = await getResource({ - url - }) - if ( - !deps.validateConnection({ - status, - body: data - }) - ) { - throw new Error('unreachable') - } - return data - } catch (_) { - return undefined - } -} - -const INCOMING_PAYMENT_URL_REGEX = - /(?^(.)+)\/incoming-payments\/(?(.){36}$)/ - -async function getIncomingPayment( - deps: ServiceDependencies, - url: string -): Promise { - try { - const match = url.match(INCOMING_PAYMENT_URL_REGEX)?.groups - if (!match) { - return undefined - } - // Check if this is a local payment pointer - const paymentPointer = await deps.paymentPointerService.getByUrl( - match.paymentPointerUrl - ) - if (paymentPointer) { - const ctx = createReadContext({ - id: match.id - }) - ctx.paymentPointer = paymentPointer - await deps.incomingPaymentRoutes.get(ctx) - return ctx.body as IncomingPaymentJSON - } - const { status, data } = await getResource({ - url, - accessToken: deps.accessToken - }) - if ( - !deps.validateIncomingPayment({ - status, - body: data - }) || - !isValidIncomingPayment(data) - ) { - throw new Error('unreachable') - } - return data - } catch (_) { - return undefined - } -} - -const CONNECTION_URL_REGEX = /\/connections\/(.){36}$/ - -async function getReceiver( - deps: ServiceDependencies, - url: string -): Promise { - if (url.match(CONNECTION_URL_REGEX)) { - const connection = await getConnection(deps, url) - if (connection) { - return Receiver.fromConnection(connection) - } - } else { - const incomingPayment = await getIncomingPayment(deps, url) - if (incomingPayment) { - return Receiver.fromIncomingPayment(incomingPayment) - } - } -} - -// Validate referential integrity, which cannot be represented in OpenAPI -function isValidIncomingPayment( - payment: IncomingPaymentJSON -): payment is IncomingPaymentJSON { - if (payment.incomingAmount) { - const incomingAmount = parseAmount(payment.incomingAmount) - const receivedAmount = parseAmount(payment.receivedAmount) - if ( - incomingAmount.assetCode !== receivedAmount.assetCode || - incomingAmount.assetScale !== receivedAmount.assetScale - ) { - return false - } - if (incomingAmount.value < receivedAmount.value) { - return false - } - if (incomingAmount.value === receivedAmount.value && !payment.completed) { - return false - } - } - if (typeof payment.ilpStreamConnection === 'object') { - if ( - payment.ilpStreamConnection.assetCode !== - payment.receivedAmount.assetCode || - payment.ilpStreamConnection.assetScale !== - payment.receivedAmount.assetScale - ) { - return false - } - } - return true -} diff --git a/packages/backend/src/tests/outgoingPayment.ts b/packages/backend/src/tests/outgoingPayment.ts index 94dfee9c22..4385dd480f 100644 --- a/packages/backend/src/tests/outgoingPayment.ts +++ b/packages/backend/src/tests/outgoingPayment.ts @@ -4,7 +4,7 @@ import { IocContract } from '@adonisjs/fold' import { createQuote, CreateTestQuoteOptions } from './quote' import { AppServices } from '../app' -import { Receiver } from '../open_payments/client/service' +import { Receiver } from '../open_payments/receiver/model' import { isOutgoingPaymentError } from '../open_payments/payment/outgoing/errors' import { OutgoingPayment } from '../open_payments/payment/outgoing/model' import { CreateOutgoingPaymentOptions } from '../open_payments/payment/outgoing/service' @@ -18,11 +18,11 @@ export async function createOutgoingPayment( ): Promise { const quote = await createQuote(deps, options) const outgoingPaymentService = await deps.use('outgoingPaymentService') - const clientService = await deps.use('openPaymentsClientService') + const receiverService = await deps.use('receiverService') if (options.validDestination === false) { const streamServer = await deps.use('streamServer') const { ilpAddress, sharedSecret } = streamServer.generateCredentials() - jest.spyOn(clientService.receiver, 'get').mockResolvedValueOnce( + jest.spyOn(receiverService, 'get').mockResolvedValueOnce( Receiver.fromConnection({ id: options.receiver, ilpAddress, diff --git a/packages/backend/src/tests/quote.ts b/packages/backend/src/tests/quote.ts index a2addd1971..4100a6623c 100644 --- a/packages/backend/src/tests/quote.ts +++ b/packages/backend/src/tests/quote.ts @@ -35,8 +35,8 @@ export async function createQuote( const config = await deps.use('config') let receiveAsset: AssetOptions | undefined if (validDestination) { - const clientService = await deps.use('openPaymentsClientService') - const receiver = await clientService.receiver.get(receiverUrl) + const receiverService = await deps.use('receiverService') + const receiver = await receiverService.get(receiverUrl) assert.ok(receiver) assert.ok(receiver.incomingAmount || receiveAmount || sendAmount) if (receiveAmount) { From 1bebc3124763eb1b62f506f4d55dd395fb98d702 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 2 Nov 2022 23:57:10 +0100 Subject: [PATCH 53/56] feat(backend): merge in expiresAt addition --- .../src/open_payments/receiver/model.test.ts | 2 ++ .../src/open_payments/receiver/model.ts | 22 ++++++++++++------- .../open_payments/receiver/service.test.ts | 12 ++++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/backend/src/open_payments/receiver/model.test.ts b/packages/backend/src/open_payments/receiver/model.test.ts index 89663f3898..7d7886e5f5 100644 --- a/packages/backend/src/open_payments/receiver/model.test.ts +++ b/packages/backend/src/open_payments/receiver/model.test.ts @@ -59,6 +59,7 @@ describe('Receiver Model', (): void => { }) expect(receiver.ilpAddress).toEqual(expect.any(String)) expect(receiver.sharedSecret).toEqual(expect.any(Buffer)) + expect(receiver.expiresAt).toEqual(incomingPayment.expiresAt) }) test('fails to create receiver if payment completed', async () => { @@ -115,6 +116,7 @@ describe('Receiver Model', (): void => { expect(receiver.receivedAmount).toBeUndefined() expect(receiver.ilpAddress).toEqual(expect.any(String)) expect(receiver.sharedSecret).toEqual(expect.any(Buffer)) + expect(receiver.expiresAt).toBeUndefined() }) test('returns undefined if invalid ilpAdress', async () => { diff --git a/packages/backend/src/open_payments/receiver/model.ts b/packages/backend/src/open_payments/receiver/model.ts index 228bcc4dd4..51a49f44ac 100644 --- a/packages/backend/src/open_payments/receiver/model.ts +++ b/packages/backend/src/open_payments/receiver/model.ts @@ -26,12 +26,14 @@ export class Receiver extends ConnectionBase { if (incomingPayment.completed) { return undefined } - if ( - incomingPayment.expiresAt && - new Date(incomingPayment.expiresAt).getTime() <= Date.now() - ) { + const expiresAt = incomingPayment.expiresAt + ? new Date(incomingPayment.expiresAt) + : undefined + + if (expiresAt && expiresAt.getTime() <= Date.now()) { return undefined } + const incomingAmount = incomingPayment.incomingAmount ? parseAmount(incomingPayment.incomingAmount) : undefined @@ -40,14 +42,16 @@ export class Receiver extends ConnectionBase { return this.fromOpenPaymentsConnection( incomingPayment.ilpStreamConnection, incomingAmount?.value, - receivedAmount.value + receivedAmount.value, + expiresAt ) } private static fromOpenPaymentsConnection( connection: OpenPaymentsConnection, incomingAmountValue?: bigint, - receivedAmountValue?: bigint + receivedAmountValue?: bigint, + expiresAt?: Date ): Receiver | undefined { const ilpAddress = connection.ilpAddress @@ -64,14 +68,16 @@ export class Receiver extends ConnectionBase { ilpAddress }, incomingAmountValue, - receivedAmountValue + receivedAmountValue, + expiresAt ) } private constructor( connection: OpenPaymentsConnectionWithIlpAddress, private readonly incomingAmountValue?: bigint, - private readonly receivedAmountValue?: bigint + private readonly receivedAmountValue?: bigint, + public readonly expiresAt?: Date ) { super( connection.ilpAddress, diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index 1627e96e2c..3a1e92d14f 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -63,7 +63,8 @@ describe('Receiver Service', (): void => { incomingAmount: undefined, receivedAmount: undefined, ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer) + sharedSecret: expect.any(Buffer), + expiresAt: undefined }) }) @@ -91,7 +92,8 @@ describe('Receiver Service', (): void => { incomingAmount: undefined, receivedAmount: undefined, ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer) + sharedSecret: expect.any(Buffer), + expiresAt: undefined }) expect(clientGetConnectionSpy).toHaveBeenCalledWith({ url: remoteUrl.href @@ -133,7 +135,8 @@ describe('Receiver Service', (): void => { incomingAmount: incomingPayment.incomingAmount, receivedAmount: incomingPayment.receivedAmount, ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer) + sharedSecret: expect.any(Buffer), + expiresAt: expect.any(Date) }) }) @@ -168,7 +171,8 @@ describe('Receiver Service', (): void => { incomingAmount: incomingPayment.incomingAmount, receivedAmount: incomingPayment.receivedAmount, ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer) + sharedSecret: expect.any(Buffer), + expiresAt: expect.any(Date) }) expect(clientGetIncomingPaymentSpy).toHaveBeenCalledWith({ url: incomingPayment.url, From d277827bf626ab67c64091ea280fe2b9f82518eb Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Thu, 3 Nov 2022 00:29:07 +0100 Subject: [PATCH 54/56] chore(backend): update order of tsc building --- tsconfig.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index cc949a8fb2..a816dfc8f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,9 @@ { "path": "packages/openapi" }, + { + "path": "packages/open-payments" + }, { "path": "packages/auth" }, @@ -11,9 +14,6 @@ }, { "path": "packages/frontend" - }, - { - "path": "packages/open-payments" } ], "files": [], From d51d3d26ee36692a1642a5d993a075a4e3ec8c91 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Fri, 4 Nov 2022 16:02:29 +0100 Subject: [PATCH 55/56] feat(backend): remove jest-koa-mocks package --- packages/backend/package.json | 1 - pnpm-lock.yaml | 26 +------------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 4bb54e9319..9df1026d49 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -54,7 +54,6 @@ "@koa/cors": "^3.1.0", "@koa/router": "^12.0.0", "@poppinss/file-generator": "^1.0.2", - "@shopify/jest-koa-mocks": "^5.0.1", "@types/bcrypt": "^5.0.0", "add": "^2.0.6", "ajv": "^8.11.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13ddd6c1e8..d73da29fa0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,7 +126,6 @@ importers: '@koa/cors': ^3.1.0 '@koa/router': ^12.0.0 '@poppinss/file-generator': ^1.0.2 - '@shopify/jest-koa-mocks': ^5.0.1 '@types/bcrypt': ^5.0.0 '@types/jest': ^28.1.7 '@types/koa': 2.13.5 @@ -198,7 +197,6 @@ importers: '@koa/cors': 3.3.0 '@koa/router': 12.0.0 '@poppinss/file-generator': 1.0.2 - '@shopify/jest-koa-mocks': 5.0.1 '@types/bcrypt': 5.0.0 add: 2.0.6 ajv: 8.11.0 @@ -3541,16 +3539,6 @@ packages: resolution: {integrity: sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==} dev: true - /@shopify/jest-koa-mocks/5.0.1: - resolution: {integrity: sha512-4YskS9q8+TEHNoyopmuoy2XyhInyqeOl7CF5ShJs19sm6m0EA/jGGvgf/osv2PeTfuf42/L2G9CzWUSg49yTSg==} - engines: {node: ^14.17.0 || >=16.0.0} - dependencies: - koa: 2.13.4 - node-mocks-http: 1.11.0 - transitivePeerDependencies: - - supports-color - dev: false - /@sinclair/typebox/0.24.28: resolution: {integrity: sha512-dgJd3HLOkLmz4Bw50eZx/zJwtBq65nms3N9VBYu5LTjJ883oBFkTyXRlCB/ZGGwqYpJJHA5zW2Ibhl5ngITfow==} dev: true @@ -4541,18 +4529,6 @@ packages: optional: true dependencies: ajv: 8.11.0 - dev: false - - /ajv-formats/2.1.1_ajv@8.11.0: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - dependencies: - ajv: 8.11.0 - dev: true /ajv/6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -10802,7 +10778,7 @@ packages: resolution: {integrity: sha512-5wpFKMoEbUcjiqo16jIen3Cb2+oApSnYZpWn8WQdRO2q/dNQZZl8Pz6ESwCriiyU5AK4i5ZI6+7O3bHQr6+6+g==} dependencies: ajv: 8.11.0 - ajv-formats: 2.1.1_ajv@8.11.0 + ajv-formats: 2.1.1 lodash.merge: 4.6.2 openapi-types: 9.3.1 dev: true From f3558d8d3f6db1e0c549679db3a43e922b4d12b6 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Fri, 4 Nov 2022 16:03:09 +0100 Subject: [PATCH 56/56] feat(backend): address feedback, add tests --- .../open_payments/payment/incoming/model.ts | 12 +- .../src/open_payments/receiver/model.test.ts | 32 +++-- .../open_payments/receiver/service.test.ts | 109 +++++++++++++----- 3 files changed, 99 insertions(+), 54 deletions(-) diff --git a/packages/backend/src/open_payments/payment/incoming/model.ts b/packages/backend/src/open_payments/payment/incoming/model.ts index 8ffb19be27..8afcaa6add 100644 --- a/packages/backend/src/open_payments/payment/incoming/model.ts +++ b/packages/backend/src/open_payments/payment/incoming/model.ts @@ -1,7 +1,7 @@ import { Model, ModelOptions, Pojo, QueryContext } from 'objection' import { v4 as uuid } from 'uuid' -import { Amount, AmountJSON } from '../../amount' +import { Amount, AmountJSON, serializeAmount } from '../../amount' import { Connection, ConnectionJSON } from '../../connection/model' import { PaymentPointer, @@ -248,15 +248,9 @@ export class IncomingPayment id: this.id, paymentPointer: this.paymentPointer.url, incomingAmount: this.incomingAmount - ? { - ...this.incomingAmount, - value: this.incomingAmount.value.toString() - } + ? serializeAmount(this.incomingAmount) : undefined, - receivedAmount: { - ...this.receivedAmount, - value: this.receivedAmount.value.toString() - }, + receivedAmount: serializeAmount(this.receivedAmount), completed: this.completed, createdAt: this.createdAt.toISOString(), updatedAt: this.updatedAt.toISOString(), diff --git a/packages/backend/src/open_payments/receiver/model.test.ts b/packages/backend/src/open_payments/receiver/model.test.ts index 7d7886e5f5..35420582b6 100644 --- a/packages/backend/src/open_payments/receiver/model.test.ts +++ b/packages/backend/src/open_payments/receiver/model.test.ts @@ -47,19 +47,15 @@ describe('Receiver Model', (): void => { }) ) - expect(receiver.asset).toStrictEqual({ - code: incomingPayment.asset.code, - scale: incomingPayment.asset.scale - }) - expect(receiver.incomingAmount).toBeUndefined() - expect(receiver.receivedAmount).toStrictEqual({ + expect(receiver).toEqual({ assetCode: incomingPayment.asset.code, assetScale: incomingPayment.asset.scale, - value: BigInt(0) + incomingAmountValue: undefined, + receivedAmountValue: BigInt(0), + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer), + expiresAt: incomingPayment.expiresAt }) - expect(receiver.ilpAddress).toEqual(expect.any(String)) - expect(receiver.sharedSecret).toEqual(expect.any(Buffer)) - expect(receiver.expiresAt).toEqual(incomingPayment.expiresAt) }) test('fails to create receiver if payment completed', async () => { @@ -108,15 +104,15 @@ describe('Receiver Model', (): void => { connectionService.get(incomingPayment).toOpenPaymentsType() ) - expect(receiver.asset).toStrictEqual({ - code: incomingPayment.asset.code, - scale: incomingPayment.asset.scale + expect(receiver).toEqual({ + assetCode: incomingPayment.asset.code, + assetScale: incomingPayment.asset.scale, + incomingAmountValue: undefined, + receivedAmountValue: undefined, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer), + expiresAt: undefined }) - expect(receiver.incomingAmount).toBeUndefined() - expect(receiver.receivedAmount).toBeUndefined() - expect(receiver.ilpAddress).toEqual(expect.any(String)) - expect(receiver.sharedSecret).toEqual(expect.any(Buffer)) - expect(receiver.expiresAt).toBeUndefined() }) test('returns undefined if invalid ilpAdress', async () => { diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index 3a1e92d14f..03781651b2 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -57,7 +57,12 @@ describe('Receiver Service', (): void => { const localUrl = `${Config.openPaymentsUrl}/${CONNECTION_PATH}/${connectionId}` - await expect(receiverService.get(localUrl)).resolves.toMatchObject({ + const clientGetConnectionSpy = jest.spyOn( + openPaymentsClient.ilpStreamConnection, + 'get' + ) + + await expect(receiverService.get(localUrl)).resolves.toEqual({ assetCode: paymentPointer.asset.code, assetScale: paymentPointer.asset.scale, incomingAmount: undefined, @@ -66,6 +71,7 @@ describe('Receiver Service', (): void => { sharedSecret: expect.any(Buffer), expiresAt: undefined }) + expect(clientGetConnectionSpy).not.toHaveBeenCalled() }) test('resolves remote connection', async () => { @@ -84,9 +90,7 @@ describe('Receiver Service', (): void => { connectionService.get(incomingPayment).toOpenPaymentsType() ) - await expect( - receiverService.get(remoteUrl.href) - ).resolves.toMatchObject({ + await expect(receiverService.get(remoteUrl.href)).resolves.toEqual({ assetCode: paymentPointer.asset.code, assetScale: paymentPointer.asset.scale, incomingAmount: undefined, @@ -100,7 +104,7 @@ describe('Receiver Service', (): void => { }) }) - test('returns undefined for unknown connection', async (): Promise => { + test('returns undefined for unknown local connection', async (): Promise => { const paymentPointer = await createPaymentPointer(deps) await expect( @@ -109,6 +113,51 @@ describe('Receiver Service', (): void => { ) ).resolves.toBeUndefined() }) + + test('returns undefined for unknown remote connection', async (): Promise => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + const remoteUrl = new URL( + `${paymentPointer.url}/${CONNECTION_PATH}/${incomingPayment.connectionId}` + ) + + const clientGetConnectionSpy = jest + .spyOn(openPaymentsClient.ilpStreamConnection, 'get') + .mockResolvedValueOnce(undefined) + + await expect( + receiverService.get(remoteUrl.href) + ).resolves.toBeUndefined() + expect(clientGetConnectionSpy).toHaveBeenCalledWith({ + url: remoteUrl.href + }) + }) + + test('returns undefined when fetching remote connection throws', async (): Promise => { + const paymentPointer = await createPaymentPointer(deps) + const incomingPayment = await createIncomingPayment(deps, { + paymentPointerId: paymentPointer.id + }) + + const remoteUrl = new URL( + `${paymentPointer.url}/${CONNECTION_PATH}/${incomingPayment.connectionId}` + ) + + const clientGetConnectionSpy = jest + .spyOn(openPaymentsClient.ilpStreamConnection, 'get') + .mockImplementationOnce(async () => { + throw new Error('Could not get connection') + }) + + await expect( + receiverService.get(remoteUrl.href) + ).resolves.toBeUndefined() + expect(clientGetConnectionSpy).toHaveBeenCalledWith({ + url: remoteUrl.href + }) + }) }) describe('incoming payments', () => { @@ -127,17 +176,23 @@ describe('Receiver Service', (): void => { } }) - await expect( - receiverService.get(incomingPayment.url) - ).resolves.toMatchObject({ - assetCode: paymentPointer.asset.code, - assetScale: paymentPointer.asset.scale, - incomingAmount: incomingPayment.incomingAmount, - receivedAmount: incomingPayment.receivedAmount, - ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer), - expiresAt: expect.any(Date) - }) + const clientGetIncomingPaymentSpy = jest.spyOn( + openPaymentsClient.ilpStreamConnection, + 'get' + ) + + await expect(receiverService.get(incomingPayment.url)).resolves.toEqual( + { + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale, + incomingAmountValue: incomingPayment.incomingAmount.value, + receivedAmountValue: incomingPayment.receivedAmount.value, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer), + expiresAt: expect.any(Date) + } + ) + expect(clientGetIncomingPaymentSpy).not.toHaveBeenCalled() }) test('resolves remote incoming payment', async () => { @@ -163,17 +218,17 @@ describe('Receiver Service', (): void => { .spyOn(paymentPointerService, 'getByUrl') .mockResolvedValueOnce(undefined) - await expect( - receiverService.get(incomingPayment.url) - ).resolves.toMatchObject({ - assetCode: paymentPointer.asset.code, - assetScale: paymentPointer.asset.scale, - incomingAmount: incomingPayment.incomingAmount, - receivedAmount: incomingPayment.receivedAmount, - ilpAddress: expect.any(String), - sharedSecret: expect.any(Buffer), - expiresAt: expect.any(Date) - }) + await expect(receiverService.get(incomingPayment.url)).resolves.toEqual( + { + assetCode: paymentPointer.asset.code, + assetScale: paymentPointer.asset.scale, + incomingAmountValue: incomingPayment.incomingAmount.value, + receivedAmountValue: incomingPayment.receivedAmount.value, + ilpAddress: expect.any(String), + sharedSecret: expect.any(Buffer), + expiresAt: expect.any(Date) + } + ) expect(clientGetIncomingPaymentSpy).toHaveBeenCalledWith({ url: incomingPayment.url, accessToken: expect.any(String)