Skip to content
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

Merged
merged 50 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e0942aa
Feat: add kyc to profile storage
0oM4R Oct 15, 2024
2f8e5eb
Feat: WIP adding KYC UI
0oM4R Oct 16, 2024
4805e7f
Feat: suppot TFGrid-KYC in gridclient
0oM4R Oct 16, 2024
c6f9e07
chore: export KYC client
0oM4R Oct 16, 2024
154c17f
Chore:
0oM4R Oct 16, 2024
ef66fde
chore : use urlJoin
0oM4R Oct 16, 2024
3d14655
refactor: retrive address from mnemoinic
0oM4R Oct 16, 2024
6632a56
chore: add api prefix as const
0oM4R Oct 17, 2024
e955e6a
Merge branch 'development' of github.com:threefoldtech/tfgrid-sdk-ts …
0oM4R Oct 21, 2024
bdcb5ce
Merge branch 'development_kyc' of github.com:threefoldtech/tfgrid-sdk…
0oM4R Oct 21, 2024
2af3b4d
prepare KYC client
0oM4R Oct 21, 2024
d11860f
WIP: itegrate the kyc client in dashboard
0oM4R Oct 21, 2024
7579268
chore: update status response
0oM4R Oct 22, 2024
96cc450
Feat: handle idenfy iframe
0oM4R Oct 22, 2024
34aa6fc
chore: create kyc store
0oM4R Oct 22, 2024
6ce858d
chore: use kyc in env
0oM4R Oct 22, 2024
6ea1b4b
chore: use kyc store
0oM4R Oct 22, 2024
f03cf67
WIP: kyc style
0oM4R Oct 22, 2024
96ad9b5
Style: enhance kyc dialog loading
0oM4R Oct 22, 2024
1873a50
chore: add kyc to gridclient
0oM4R Oct 22, 2024
c93b84e
chore pass kyc url in gridclient options
0oM4R Oct 22, 2024
f571351
chore: handle domain post/prefix
0oM4R Oct 22, 2024
1255404
WIP: add kyc errors
0oM4R Oct 23, 2024
70c00af
kyc: add token errors
0oM4R Oct 23, 2024
5f20410
finalize errors
0oM4R Oct 23, 2024
60266c2
add kyc client to twindeployment handler
0oM4R Oct 23, 2024
d5d33ee
apply error handling
0oM4R Oct 23, 2024
74df586
add requirekyc
0oM4R Oct 23, 2024
8917e32
update tooltip
0oM4R Oct 23, 2024
b7d9b4d
Merge branch 'development' of github.com:threefoldtech/tfgrid-sdk-ts …
0oM4R Oct 23, 2024
50bb804
cleanup
0oM4R Oct 23, 2024
02afab2
cleanup
0oM4R Oct 23, 2024
647f653
cleanup
0oM4R Oct 23, 2024
2f3a69d
style: add icon, remove check
0oM4R Oct 23, 2024
15852f6
UX: enhance token error handling
0oM4R Oct 23, 2024
0ed4767
chore: use enum status code
0oM4R Oct 24, 2024
583212d
rephrase client error message
0oM4R Oct 24, 2024
a74b9d4
fix typo
0oM4R Oct 24, 2024
23e194e
chore: use new status endpoint
0oM4R Oct 27, 2024
5d7fd30
Fix:
0oM4R Oct 28, 2024
5a28b4f
chore: enhance error messages
0oM4R Oct 29, 2024
be22124
Refactor:
0oM4R Oct 29, 2024
395f606
Refactor:
0oM4R Oct 29, 2024
3096bfa
fix: headers, enahnce error msg
0oM4R Oct 29, 2024
d95d163
refactor:
0oM4R Oct 30, 2024
02a4fc0
WIP: handle diffrent networks
0oM4R Oct 30, 2024
9758bd9
docs: add kyc to build docs
0oM4R Oct 30, 2024
38539a3
refactor:
0oM4R Oct 30, 2024
5ad3c2f
feat: add isHealty to kyc client
0oM4R Oct 30, 2024
f8ffea0
chore: remove kyc check on createNameContract
0oM4R Oct 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/grid_client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { validateMnemonic } from "bip39";
import * as PATH from "path";
import urlJoin from "url-join";

import { Graphql } from "./clients";
import { Graphql, KYC as kycClient } from "./clients";
import { TFClient } from "./clients/tf-grid/client";
import { ClientOptions, GridClientConfig, NetworkEnv } from "./config";
import { migrateKeysEncryption, send, toHexSeed } from "./helpers";
Expand All @@ -24,6 +24,7 @@ class GridClient {
config: GridClientConfig;
rmbClient: RMBClient;
tfclient: TFClient;
kycClient: kycClient;
/**The `MachinesModule` class provides methods to interact with machine operations.*/
machines: modules.machines;
k8s: modules.k8s;
Expand Down Expand Up @@ -139,7 +140,7 @@ class GridClient {
this.clientOptions.keypairType,
this.clientOptions.keepReconnectingToChain,
);

this.kycClient = new kycClient(urls.KYC, this.clientOptions.mnemonic, this.clientOptions.keypairType);
this.rmbClient = new RMBClient(
urls.substrate,
urls.relay,
Expand Down Expand Up @@ -190,6 +191,7 @@ class GridClient {
twinId: this.twinId,
seed: this.clientOptions.seed,
deploymentTimeoutMinutes: this.clientOptions.deploymentTimeoutMinutes!,
kycURL: urls.KYC,
};
for (const module of Object.getOwnPropertyNames(modules).filter(item => typeof modules[item] === "function")) {
if (module.includes("Model")) {
Expand Down Expand Up @@ -245,6 +247,8 @@ class GridClient {
substrate: substrateURL || `wss://tfchain.${base}/ws`,
graphql: graphqlURL || `https://graphql.${base}/graphql`,
activation: activationURL || `https://activation.${base}/activation/activate`,
//TODO handle kyc url
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Contributor Author

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

KYC: `kyc1.gent01.dev.grid.tf`,
};

return urls;
Expand Down
1 change: 1 addition & 0 deletions packages/grid_client/src/clients/index.ts
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";
195 changes: 195 additions & 0 deletions packages/grid_client/src/clients/kyc/client.ts
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>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unknown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
What do you suggest?

}

/**
* 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"):
Copy link
Contributor

@amiraabouhadid amiraabouhadid Oct 23, 2024

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 address and challenge so we need to make sure about the error message context.

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);
}
}
}
}
2 changes: 2 additions & 0 deletions packages/grid_client/src/clients/kyc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./client";
export * from "./types";
90 changes: 90 additions & 0 deletions packages/grid_client/src/clients/kyc/types.ts
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",
}
2 changes: 2 additions & 0 deletions packages/grid_client/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class GridClientConfig {
twinId: number;
seed?: string;
deploymentTimeoutMinutes: number;
kycURL: string;
}
class ClientOptions {
constructor(
Expand All @@ -49,6 +50,7 @@ class ClientOptions {
public activationURL?: string,
public deploymentTimeoutMinutes?: number,
public keepReconnectingToChain?: boolean,
public KycURL?: string,
) {}
}

Expand Down
18 changes: 18 additions & 0 deletions packages/grid_client/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ function convertObjectToQueryString(obj: Record<string, any>): string {

return queryString;
}
/**
* Converts a string message to its hexadecimal representation.
* @param message - The message to convert.
* @returns The hexadecimal representation of the message.
*/
function stringToHex(message: string): string {
return Buffer.from(message).toString("hex");
}
/**
* Converts a hexadecimal string to a byte array.
* @param hex - The hexadecimal string to convert.
* @returns {Uint8Array} The byte array representation of the hexadecimal string.
*/
function bytesFromHex(hex: string): Uint8Array {
return new Uint8Array(Buffer.from(hex, "hex"));
}

export {
generateString,
Expand All @@ -95,4 +111,6 @@ export {
generateRandomHexSeed,
zeroPadding,
convertObjectToQueryString,
stringToHex,
bytesFromHex,
};
Loading
Loading