Skip to content

Commit

Permalink
feat: Rapyd zod validation (#1077)
Browse files Browse the repository at this point in the history
* rapyd interaction zod validation

* naming

* remove async where direct promise is returned

* fixes

* more validation fixes

* convert rapyd types to zod schema

* convert rapyd types to zod schema

* Fix RapydWalletSchema

* Fix showing error message

---------

Co-authored-by: beniaminmunteanu <beniamin.munteanu@xpsol.ro>
  • Loading branch information
rico191013 and beniaminmunteanu committed Jan 19, 2024
1 parent dccc2a3 commit a0c18ba
Show file tree
Hide file tree
Showing 10 changed files with 684 additions and 444 deletions.
2 changes: 1 addition & 1 deletion packages/wallet/backend/src/rafiki/rafiki-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export class RafikiClient implements IRafikiClient {
}

if (!createQuote.quote) {
throw new Error('Unable to fetch created quote')
throw new Error(createQuote.message || 'Unable to create Quote')
}

return createQuote.quote
Expand Down
11 changes: 8 additions & 3 deletions packages/wallet/backend/src/rapyd/controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { NextFunction, Request } from 'express'
import { AccountService } from '@/account/service'
import { WalletAddressService } from '@/walletAddress/service'
import { validate } from '@/shared/validate'
import { User } from '@/user/model'
import { getRandomValues } from 'crypto'
import { NextFunction, Request } from 'express'
import { Options, RapydService } from './service'
import { kycSchema, profileSchema, walletSchema } from './validation'
import { kycSchema, profileSchema, walletSchema } from './schemas'
import { User } from '@/user/model'
import {
RapydDocumentType,
RapydIdentityResponse,
RapydWallet
} from './schemas'

interface IRapydController {
getCountryNames: ControllerFunction<Options[]>
Expand Down
177 changes: 131 additions & 46 deletions packages/wallet/backend/src/rapyd/rapyd-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,51 @@ import { Env } from '@/config/env'
import { User } from '@/user/model'
import RandExp from 'randexp'
import { BadRequest } from '@/errors'
import { AnyZodObject, z, ZodEffects, ZodTypeAny } from 'zod'
import {
CompletePayoutRequest,
CompletePayoutResponse,
CompletePayoutResponseSchema,
PayoutListMethodResponseSchema,
PayoutMethodResponse,
PayoutRequiredFieldsResponse,
PayoutRequiredFieldsResponseSchema,
RapydAccountBalance,
RapydCountry,
RapydCountryListSchema,
RapydDepositRequest,
RapydDepositResponse,
RapydDepositResponseSchema,
RapydDocumentsTypeSchema,
RapydDocumentType,
RapydHoldRequest,
RapydHoldResponse,
RapydHoldResponseSchema,
RapydIdentityRequest,
RapydIdentityResponse,
RapydIdentityResponseSchema,
RapydListAccountBalanceSchema,
RapydProfile,
RapydReleaseRequest,
RapydReleaseResponse,
RapydReleaseResponseSchema,
RapydResponse,
RapydSetTransferResponse,
RapydSetTransferResponseRequest,
RapydSetTransferResponseSchema,
RapydTransferRequest,
RapydWallet,
RapydWalletSchema,
RequiredFields,
SimulateBankTransferToWalletRequest,
SimulateBankTransferToWalletResponse,
VirtualAccountRequest,
VirtualAccountResponse,
VirtualAccountResponseSchema,
WithdrawFundsFromAccountResponse,
WithdrawFundsFromAccountResponseSchema
} from './schemas'
import { validateRapydResponse } from './schemas'

interface IRapydClient {
createWallet(wallet: RapydWallet): Promise<RapydResponse<RapydWallet>>
Expand Down Expand Up @@ -77,39 +122,42 @@ export class RapydClient implements IRapydClient {
public createWallet(
wallet: RapydWallet
): Promise<RapydResponse<RapydWallet>> {
return this.post<RapydResponse<RapydWallet>>('user', JSON.stringify(wallet))
return this.post('user', JSON.stringify(wallet), RapydWalletSchema)
}

public updateProfile(
profile: RapydProfile
): Promise<RapydResponse<RapydWallet>> {
return this.put<RapydResponse<RapydWallet>>('user', JSON.stringify(profile))
return this.put('user', JSON.stringify(profile), RapydWalletSchema)
}

public verifyIdentity(
req: RapydIdentityRequest
): Promise<RapydResponse<RapydIdentityResponse>> {
return this.post<RapydResponse<RapydIdentityResponse>>(
return this.post(
'identities',
JSON.stringify(req)
JSON.stringify(req),
RapydIdentityResponseSchema
)
}

public depositLiquidity(
req: RapydDepositRequest
): Promise<RapydResponse<RapydDepositResponse>> {
return this.post<RapydResponse<RapydDepositResponse>>(
return this.post(
'account/deposit',
JSON.stringify(req)
JSON.stringify(req),
RapydDepositResponseSchema
)
}

public holdLiquidity(
req: RapydHoldRequest
): Promise<RapydResponse<RapydHoldResponse>> {
return this.post<RapydResponse<RapydHoldResponse>>(
return this.post(
'account/balance/hold',
JSON.stringify(req)
JSON.stringify(req),
RapydHoldResponseSchema
)
}

Expand All @@ -118,9 +166,10 @@ export class RapydClient implements IRapydClient {
isRetry: boolean = false
): Promise<RapydResponse<RapydReleaseResponse>> {
try {
return await this.post<RapydResponse<RapydReleaseResponse>>(
return this.post(
'account/balance/release',
JSON.stringify(req)
JSON.stringify(req),
RapydReleaseResponseSchema
)
} catch (err) {
if (
Expand All @@ -145,26 +194,29 @@ export class RapydClient implements IRapydClient {
public getDocumentTypes(
country: string
): Promise<RapydResponse<RapydDocumentType[]>> {
return this.get<RapydResponse<RapydDocumentType[]>>(
`identities/types?country=${country}`
)
return this.get(
`identities/types?country=${country}`,
RapydDocumentsTypeSchema
) as Promise<RapydResponse<RapydDocumentType[]>>
}

public issueVirtualAccount(
req: VirtualAccountRequest
): Promise<RapydResponse<VirtualAccountResponse>> {
return this.post<RapydResponse<VirtualAccountResponse>>(
return this.post(
'issuing/bankaccounts',
JSON.stringify(req)
JSON.stringify(req),
VirtualAccountResponseSchema
)
}

public simulateBankTransferToWallet(
req: SimulateBankTransferToWalletRequest
): Promise<RapydResponse<SimulateBankTransferToWalletResponse>> {
return this.post<RapydResponse<SimulateBankTransferToWalletResponse>>(
return this.post(
'issuing/bankaccounts/bankaccounttransfertobankaccount',
JSON.stringify(req)
JSON.stringify(req),
VirtualAccountResponseSchema
)
}

Expand All @@ -173,9 +225,11 @@ export class RapydClient implements IRapydClient {
isRetry: boolean = false
): Promise<RapydResponse<RapydSetTransferResponse>> {
try {
const transferResponse = await this.post<
RapydResponse<RapydSetTransferResponse>
>('account/transfer', JSON.stringify(req))
const transferResponse = await this.post(
'account/transfer',
JSON.stringify(req),
RapydSetTransferResponseSchema
)

return await this.setTransferResponse({
id: transferResponse.data.id,
Expand Down Expand Up @@ -204,13 +258,16 @@ export class RapydClient implements IRapydClient {
public getAccountsBalance(
walletId: string
): Promise<RapydResponse<RapydAccountBalance[]>> {
return this.get<RapydResponse<RapydAccountBalance[]>>(
`user/${walletId}/accounts`
)
return this.get(
`user/${walletId}/accounts`,
RapydListAccountBalanceSchema
) as Promise<RapydResponse<RapydAccountBalance[]>>
}

public getCountryNames(): Promise<RapydResponse<RapydCountry[]>> {
return this.get<RapydResponse<RapydCountry[]>>('data/countries')
return this.get('data/countries', RapydCountryListSchema) as Promise<
RapydResponse<RapydCountry[]>
>
}

public async withdrawFundsFromAccount(
Expand Down Expand Up @@ -271,9 +328,11 @@ export class RapydClient implements IRapydClient {
withdrawReq
)

const payout = await this.post<
RapydResponse<WithdrawFundsFromAccountResponse>
>('payouts', JSON.stringify(withdrawReq))
const payout = await this.post(
'payouts',
JSON.stringify(withdrawReq),
WithdrawFundsFromAccountResponseSchema
)

if (payout.status.status !== 'SUCCESS') {
throw new Error(
Expand All @@ -299,9 +358,10 @@ export class RapydClient implements IRapydClient {
private async getPayoutMethodTypes(
assetCode: string
): Promise<PayoutMethodResponse> {
const response: RapydResponse<PayoutMethodResponse[]> = await this.get(
`payouts/supported_types?payout_currency=${assetCode}&limit=1`
)
const response = (await this.get(
`payouts/supported_types?payout_currency=${assetCode}&limit=1`,
PayoutListMethodResponseSchema
)) as RapydResponse<PayoutMethodResponse[]>

if (response.status.status !== 'SUCCESS') {
throw new Error(
Expand Down Expand Up @@ -334,16 +394,18 @@ export class RapydClient implements IRapydClient {
): Promise<RapydResponse<CompletePayoutResponse>> {
return this.post(
`payouts/complete/${req.payout}/${req.amount}`,
JSON.stringify(req)
JSON.stringify(req),
CompletePayoutResponseSchema
)
}

private setTransferResponse(
req: RapydSetTransferResponseRequest
): Promise<RapydResponse<RapydSetTransferResponse>> {
return this.post<RapydResponse<RapydSetTransferResponse>>(
return this.post(
'account/transfer/response',
JSON.stringify(req)
JSON.stringify(req),
RapydSetTransferResponseSchema
)
}

Expand All @@ -364,16 +426,27 @@ export class RapydClient implements IRapydClient {
}
}

private get<T>(url: string) {
return this.request<T>('get', url)
private get<T extends AnyZodObject | ZodEffects<AnyZodObject> | ZodTypeAny>(
url: string,
zodResponseSchema?: T
) {
return this.request('get', url, undefined, zodResponseSchema)
}

private post<T>(url: string, body: string) {
return this.request<T>('post', url, body)
private post<T extends AnyZodObject | ZodEffects<AnyZodObject>>(
url: string,
body: string,
zodResponseSchema?: T
) {
return this.request<T>('post', url, body, zodResponseSchema)
}

private put<T>(url: string, body: string) {
return this.request<T>('put', url, body)
private put<T extends AnyZodObject | ZodEffects<AnyZodObject>>(
url: string,
body: string,
zodResponseSchema: T
) {
return this.request('put', url, body, zodResponseSchema)
}

private calcSignature(params: CalculateSignatureParams): string {
Expand Down Expand Up @@ -416,19 +489,30 @@ export class RapydClient implements IRapydClient {
}
}

private async request<T = unknown>(
private async request<
T extends AnyZodObject | ZodEffects<AnyZodObject> | ZodTypeAny
>(
method: 'get' | 'post' | 'put',
url: string,
body?: string
) {
body?: string,
zodResponseSchema?: T
): Promise<RapydResponse<z.infer<T>>> {
const headers = this.getRapydRequestHeader(method, url, body ?? '')
try {
const res = await axios<T>({
const res = await axios<z.infer<T>>({
method,
url: `${this.env.RAPYD_API}/${url}`,
...(body && { data: body }),
headers
})

if (zodResponseSchema) {
await validateRapydResponse(
zodResponseSchema as AnyZodObject | ZodEffects<AnyZodObject>,
res.data.data
)
}

return res.data
} catch (e) {
if (e instanceof AxiosError) {
Expand Down Expand Up @@ -460,9 +544,10 @@ export class RapydClient implements IRapydClient {
}

const response: RapydResponse<PayoutRequiredFieldsResponse> =
await this.get(
`payouts/${args.payoutMethodType}/details?sender_country=${args.senderCountry}&sender_currency=${args.senderCurrency}&beneficiary_country=${args.beneficiaryCountry}&payout_currency=${args.payoutCurrency}&sender_entity_type=${args.senderEntityType}&beneficiary_entity_type=${args.beneficiaryEntityType}&payout_amount=${args.payoutAmount}`
)
(await this.get(
`payouts/${args.payoutMethodType}/details?sender_country=${args.senderCountry}&sender_currency=${args.senderCurrency}&beneficiary_country=${args.beneficiaryCountry}&payout_currency=${args.payoutCurrency}&sender_entity_type=${args.senderEntityType}&beneficiary_entity_type=${args.beneficiaryEntityType}&payout_amount=${args.payoutAmount}`,
PayoutRequiredFieldsResponseSchema
)) as RapydResponse<PayoutRequiredFieldsResponse>

if (response.status.status !== 'SUCCESS') {
throw new Error(
Expand Down
Loading

0 comments on commit a0c18ba

Please sign in to comment.