-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: suppot TFGrid-KYC #3531
Feat: suppot TFGrid-KYC #3531
Changes from 39 commits
e0942aa
2f8e5eb
4805e7f
c6f9e07
154c17f
ef66fde
3d14655
6632a56
e955e6a
bdcb5ce
2af3b4d
d11860f
7579268
96cc450
34aa6fc
6ce858d
6ea1b4b
f03cf67
96ad9b5
1873a50
c93b84e
f571351
1255404
70c00af
5f20410
60266c2
d5d33ee
74df586
8917e32
b7d9b4d
50bb804
02afab2
647f653
2f3a69d
15852f6
0ed4767
583212d
a74b9d4
23e194e
5d7fd30
5a28b4f
be22124
395f606
3096bfa
d95d163
02a4fc0
9758bd9
38539a3
5ad3c2f
f8ffea0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from "./tf-grid"; | ||
export * from "./graphql"; | ||
export * from "./rmb"; | ||
export * from "./kyc"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
import { Keyring } from "@polkadot/keyring"; | ||
import { KeyringPair } from "@polkadot/keyring/types"; | ||
import { waitReady } from "@polkadot/wasm-crypto"; | ||
import { InsufficientBalanceError, KycBaseError, KycErrors, RequestError, ValidationError } from "@threefold/types"; | ||
import { HttpStatusCode } from "axios"; | ||
import { Buffer } from "buffer"; | ||
import urlJoin from "url-join"; | ||
|
||
import { bytesFromHex, formatErrorMessage, KeypairType, send, stringToHex } from "../.."; | ||
import { KycHeaders, KycStatus, VerificationDataResponse } from "./types"; | ||
const API_PREFIX = "/api/v1/"; | ||
/** | ||
* The KYC class provides methods to interact with a TFGid KYC (Know Your Customer) service. | ||
* It allows fetching verification data, status, and token by preparing necessary headers | ||
* and sending requests to the specified API domain. | ||
* | ||
* @class KYC | ||
* @example | ||
* ```typescript | ||
* const kyc = new KYC("https://api.example.com", KeypairType.sr25519, "mnemonic"); | ||
* const data = await kyc.data(); | ||
* const status = await kyc.status(); | ||
* const token = await kyc.getToken(); | ||
* ``` | ||
* @param {string} apiDomain - The API domain for the KYC service. | ||
* @param {KeypairType} [keypairType=KeypairType.sr25519] - The type of keypair to use. | ||
* @param {string} mnemonic - The mnemonic for generating the keypair. | ||
* @method data - Fetches the verification data from the KYC service. | ||
* @method status - Fetches the verification status from the KYC service. | ||
* @method getToken - Fetches the token from the KYC service. | ||
*/ | ||
export class KYC { | ||
private keypair: KeyringPair; | ||
public address: string; | ||
/** | ||
* Creates an instance of KYC. | ||
* @param apiDomain - The API domain for the TFGrid KYC service. | ||
* @param keypairType - The type of keypair to use (default is sr25519). | ||
* @param mnemonic - The mnemonic for generating the keypair. | ||
*/ | ||
constructor( | ||
public apiDomain: string, | ||
private mnemonic: string, | ||
public keypairType: KeypairType = KeypairType.sr25519, | ||
) { | ||
if (mnemonic === "") { | ||
throw new ValidationError("mnemonic is required"); | ||
} | ||
/** The api domain should not contains any prefix or postfix */ | ||
this.apiDomain = apiDomain.replace("https://", ""); | ||
this.apiDomain = apiDomain.replace("http://", ""); | ||
this.apiDomain = apiDomain.replace("/", ""); | ||
} | ||
|
||
/** | ||
* Setup the keypair and the address | ||
* | ||
* @returns {Promise<void>} | ||
* @private | ||
*/ | ||
private async setupKeyring() { | ||
const keyring = new Keyring({ type: this.keypairType }); | ||
await waitReady(); | ||
this.keypair = keyring.addFromUri(this.mnemonic); | ||
this.address = this.keypair.address; | ||
} | ||
|
||
/** | ||
* Prepares the headers required for TFGrid KYC requests. | ||
* | ||
* This method generates a set of headers that include a timestamp-based challenge, | ||
* a signed challenge using the user's key, and other necessary information. | ||
* | ||
* @returns {Promise<Record<string, string>>} A promise that resolves to an object containing the prepared headers. | ||
* | ||
*/ | ||
private async prepareHeaders(): Promise<Record<string, string>> { | ||
0oM4R marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!this.keypair) await this.setupKeyring(); | ||
const timestamp = Date.now(); | ||
const challenge = stringToHex(`${this.apiDomain}:${timestamp}`); | ||
const signedChallenge = this.keypair.sign(bytesFromHex(challenge)); | ||
const signedMsgHex = Buffer.from(signedChallenge).toString("hex"); | ||
|
||
const headers: KycHeaders = { | ||
"content-type": "application/json", | ||
"X-Client-ID": this.address, | ||
"X-Challenge": challenge, | ||
"X-Signature": signedMsgHex, | ||
}; | ||
return headers as unknown as Record<string, string>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unknown? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes we have to convert the type to record so we have to use it ref |
||
} | ||
|
||
/** | ||
* Throws specific KYC-related errors based on the provided error message. | ||
* | ||
* @param errorMessage - The error message to evaluate. | ||
* @throws {KycErrors.KycInvalidAddressError} If the error message indicates a malformed address. | ||
* @throws {KycErrors.KycInvalidChallengeError} If the error message indicates a malformed or bad challenge. | ||
* @throws {KycErrors.KycInvalidSignatureError} If the error message indicates a malformed or bad signature. | ||
* @returns {undefined} If the error message does not match any known error patterns. | ||
*/ | ||
private ThrowHeadersRelatedError(errorMessage: string) { | ||
switch (true) { | ||
case errorMessage.includes("malformed address"): | ||
throw new KycErrors.KycInvalidAddressError(errorMessage); | ||
case errorMessage.includes("malformed challenge") || errorMessage.includes("bad challenge"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since malformed is in all of these error messages, then why not just check for .includes("address"), .includes("challenge"), etc? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the idea is those errors for handling corrupted headers. service may have another error that contains |
||
throw new KycErrors.KycInvalidChallengeError(errorMessage); | ||
case errorMessage.includes("malformed signature") || errorMessage.includes("bad signature"): | ||
throw new KycErrors.KycInvalidSignatureError(errorMessage); | ||
default: | ||
return undefined; | ||
} | ||
xmonader marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Fetches the verification data from the API. | ||
* | ||
* @returns {Promise<VerificationDataResponse>} A promise that resolves to the verification data response. | ||
* @throws {KycErrors.TFGridKycError | KycBaseError} If there is an issue with fetching the data. | ||
*/ | ||
async data(): Promise<VerificationDataResponse> { | ||
try { | ||
const headers = await this.prepareHeaders(); | ||
return await send("GET", urlJoin("https://", this.apiDomain, API_PREFIX, "data"), "", headers); | ||
} catch (error) { | ||
const messagePrefix = "Failed to get authentication data from KYC service"; | ||
const errorMessage = formatErrorMessage(messagePrefix, error); | ||
const statusCode = (error as RequestError).statusCode; | ||
this.ThrowHeadersRelatedError(errorMessage); | ||
if (statusCode === 404) throw new KycErrors.KycUnverifiedError(errorMessage); | ||
throw new KycBaseError(errorMessage); | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves the current verification status. | ||
* | ||
* @returns {Promise<KycStatus>} A promise that resolves to the verification status. | ||
* @throws {KycErrors.TFGridKycError | KycBaseError} If there is an issue with fetching the status data. | ||
*/ | ||
async status(): Promise<KycStatus> { | ||
try { | ||
if (!this.keypair) await this.setupKeyring(); | ||
const res = await send( | ||
"GET", | ||
urlJoin("https://", this.apiDomain, API_PREFIX, "status", `?client_id=${this.address}`), | ||
"", | ||
{ "Content-Type": "application/json" }, | ||
); | ||
if (!res.result.status) | ||
throw new KycErrors.KycInvalidResponseError( | ||
"Failed to get status due to: Response does not contain status field", | ||
); | ||
return res.result.status; | ||
} catch (error) { | ||
if (error instanceof RequestError && error.statusCode === HttpStatusCode.NotFound) return KycStatus.unverified; | ||
const messagePrefix = "Failed to get authentication status from KYC service"; | ||
const errorMessage = formatErrorMessage(messagePrefix, error); | ||
this.ThrowHeadersRelatedError(errorMessage); | ||
throw new KycBaseError(errorMessage); | ||
} | ||
} | ||
/** | ||
* Retrieves a token data from the KYC service API. | ||
* | ||
* @returns {Promise<string>} A promise that resolves to a string representing the idenify auth token . | ||
* @throws {KycErrors.TFGridKycError | KycBaseError} If there is an issue with fetching the token data. | ||
*/ | ||
async getToken(): Promise<string> { | ||
try { | ||
const headers = await this.prepareHeaders(); | ||
const res = await send("POST", urlJoin("https://", this.apiDomain, API_PREFIX, "token"), "", headers); | ||
if (!res.result.authToken) | ||
throw new KycErrors.KycInvalidResponseError( | ||
"Failed to get token due to: Response does not contain authToken field", | ||
); | ||
return res.result.authToken; | ||
} catch (error) { | ||
const messagePrefix = "Failed to get auth token from KYC service"; | ||
const errorMessage = formatErrorMessage(messagePrefix, error); | ||
const statusCode = (error as RequestError).statusCode; | ||
this.ThrowHeadersRelatedError(errorMessage); | ||
switch (true) { | ||
case statusCode === HttpStatusCode.TooManyRequests: | ||
throw new KycErrors.KycRateLimitError(errorMessage); | ||
case errorMessage.includes("already verified"): | ||
throw new KycErrors.KycAlreadyVerifiedError(errorMessage); | ||
case errorMessage.includes("balance"): | ||
throw new InsufficientBalanceError(errorMessage); | ||
default: | ||
throw new KycBaseError(errorMessage); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./client"; | ||
export * from "./types"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/** | ||
* KYC service token response type | ||
*/ | ||
export interface TokenResponse { | ||
authToken: string; | ||
clientId: string; | ||
digitString: string; | ||
expiryTime: number; | ||
message: string; | ||
scanRef: string; | ||
sessionLength: number; | ||
tokenType: string; | ||
} | ||
/** | ||
* @interface VerificationStatusResponse | ||
* KYC service token response type | ||
*/ | ||
export interface VerificationStatusResponse { | ||
autoDocument: string; | ||
autoFace: string; | ||
clientId: string; | ||
fraudTags: string[]; | ||
manualDocument: string; | ||
manualFace: string; | ||
mismatchTags: string[]; | ||
scanRef: string; | ||
status: string; | ||
} | ||
|
||
export interface VerificationDataResponse { | ||
additionalData: object; | ||
address: string; | ||
addressVerification: object; | ||
ageEstimate: string; | ||
authority: string; | ||
birthPlace: string; | ||
clientId: string; | ||
clientIpProxyRiskLevel: string; | ||
docBirthName: string; | ||
docDateOfIssue: string; | ||
docDob: string; | ||
docExpiry: string; | ||
docFirstName: string; | ||
docIssuingCountry: string; | ||
docLastName: string; | ||
docNationality: string; | ||
docNumber: string; | ||
docPersonalCode: string; | ||
docSex: string; | ||
docTemporaryAddress: string; | ||
docType: string; | ||
driverLicenseCategory: string; | ||
duplicateDocFaces: string[]; | ||
duplicateFaces: string[]; | ||
fullName: string; | ||
manuallyDataChanged: boolean; | ||
mothersMaidenName: string; | ||
orgAddress: string; | ||
orgAuthority: string; | ||
orgBirthName: string; | ||
orgBirthPlace: string; | ||
orgFirstName: string; | ||
orgLastName: string; | ||
orgMothersMaidenName: string; | ||
orgNationality: string; | ||
orgTemporaryAddress: string; | ||
scanRef: string; | ||
selectedCountry: string; | ||
} | ||
|
||
/** | ||
* Interface representing the headers required for KYC (Know Your Customer) requests. | ||
* | ||
* @property {string} content-type - The MIME type of the request body. | ||
* @property {string} X-Client-ID - TF chian address | ||
* @property {string} X-Challenge - hex-encoded message `{api-domain}:{timestamp}`. | ||
* @property {string} X-Signature - hex-encoded sr25519|ed25519 signature. | ||
*/ | ||
export interface KycHeaders { | ||
"content-type": string; | ||
"X-Client-ID": string; | ||
"X-Challenge": string; | ||
"X-Signature": string; | ||
} | ||
export enum KycStatus { | ||
unverified = "UNVERIFIED", | ||
verified = "VERIFIED", | ||
rejected = "REJECTED", | ||
pending = "PENDING", | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needed to be handled later, on supporting kyc stacks