Skip to content

Commit

Permalink
merge development
Browse files Browse the repository at this point in the history
  • Loading branch information
0oM4R committed Oct 31, 2024
2 parents 8667aa0 + c572637 commit 5b4536f
Show file tree
Hide file tree
Showing 94 changed files with 1,590 additions and 387 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/grid_client_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ jobs:
sudo sed -i -- 's/Peers\: \[\]/Peers: [\n'"$PEERS"']/g' /etc/yggdrasil/yggdrasil.conf
sudo systemctl stop yggdrasil
sudo systemctl start yggdrasil
- name: Install Mycelium
run: |
sudo apt-get update
wget https://github.com/threefoldtech/mycelium/releases/download/v0.5.6/mycelium-x86_64-unknown-linux-musl.tar.gz
tar -xvf mycelium-x86_64-unknown-linux-musl.tar.gz
mv mycelium /usr/local/bin
sudo mycelium --peers tcp://188.40.132.242:9651 quic://185.69.166.8:9651 --tun-name utun9 &
- name: Generate SSH Key
run: |
ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
Expand Down
2 changes: 1 addition & 1 deletion packages/grid_client/scripts/multiple_vms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async function main() {
await getDeployment(grid3, name);

// //Uncomment the line below to cancel the deployment
await cancel(grid3, name);
// await cancel(grid3, name);

await grid3.disconnect();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ async function main() {
mru: 4, // GB
sru: 10,
farmId: 1,
availableFor: grid3.twinId,
};

const vms: MachinesModel = {
Expand Down
7 changes: 5 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,7 @@ class GridClient {
substrate: substrateURL || `wss://tfchain.${base}/ws`,
graphql: graphqlURL || `https://graphql.${base}/graphql`,
activation: activationURL || `https://activation.${base}/activation/activate`,
KYC: `https://kyc.${base}`,
};

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";
198 changes: 198 additions & 0 deletions packages/grid_client/src/clients/kyc/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { Keyring } from "@polkadot/keyring";
import { KeyringPair } from "@polkadot/keyring/types";
import { waitReady } from "@polkadot/wasm-crypto";
import { InsufficientBalanceError, KycBaseError, KycErrors, ValidationError } from "@threefold/types";
import axios, { AxiosError, HttpStatusCode } from "axios";
import { Buffer } from "buffer";
import urlJoin from "url-join";

import { bytesFromHex, formatErrorMessage, KeypairType, 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 = this.apiDomain.replace(/https?:\/\//, "").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>> {
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>;
}

/**
* Throws specific KYC-related errors based on the provided error .
* @param {Error} error - The error to evaluate.
* @param {string} errorMessage - The error message to evaluate.
* @throws {KycErrors | KycBaseError} If the error message indicates a malformed address.
*/
private throwKycError(error: Error, messagePrefix: string) {
if (!(error instanceof AxiosError)) return new KycBaseError(error.message);
const { response, status } = error as AxiosError;
if (response?.data) error.message = (response?.data as { error: string })?.error || error.message;
const errorMessage = formatErrorMessage(messagePrefix, error);
switch (true) {
case status === HttpStatusCode.BadRequest:
return new KycErrors.BadRequest(`${errorMessage}. Please contact support.`);

case status === HttpStatusCode.Unauthorized:
return new KycErrors.Unauthorized(`${errorMessage}. Please contact support.`);

case status === HttpStatusCode.NotFound:
return new KycErrors.Unverified(errorMessage);

case status === HttpStatusCode.TooManyRequests:
return new KycErrors.RateLimit(errorMessage);

case status === HttpStatusCode.Conflict:
return new KycErrors.AlreadyVerified(errorMessage);

case status === HttpStatusCode.PaymentRequired:
return new InsufficientBalanceError(errorMessage);

default:
return new KycBaseError(errorMessage);
}
}

/**
* 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 axios.get(urlJoin("https://", this.apiDomain, API_PREFIX, "data"), { headers });
} catch (error) {
throw this.throwKycError(error, "Failed to get authentication data from KYC service.");
}
}

/**
* Retrieves the current verification status.
*
* @returns {Promise<KycStatus>} A promise that resolves to the verification status.
* @throws {KycErrors | 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 axios.get(urlJoin("https://", this.apiDomain, API_PREFIX, "status"), {
params: { client_id: this.address },
})
).data;
if (!res.result.status)
throw new KycErrors.InvalidResponse("Failed to get status due to: Response does not contain status field");
return res.result.status;
} catch (error) {
if (error instanceof AxiosError && error.status === HttpStatusCode.NotFound) return KycStatus.unverified;
throw this.throwKycError(error, "Failed to get authentication status from KYC service.");
}
}
/**
* 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 axios.post(urlJoin("https://", this.apiDomain, API_PREFIX, "token"), "", { headers })).data;
if (!res.result.authToken)
throw new KycErrors.InvalidResponse("Failed to get token due to: Response does not contain authToken field");
return res.result.authToken;
} catch (error) {
throw this.throwKycError(error, "Failed to get auth token from KYC service.");
}
}

/**
* Checks the health status of the KYC service.
*
* @returns {Promise<boolean>} A promise that resolves to `true` if the service is healthy, otherwise `false`.
*
* @throws Will return `false` if there is an error during the request.
*/
async isHealthy(): Promise<boolean> {
try {
const res = await axios.get(urlJoin("https://", this.apiDomain, API_PREFIX, "health"));
const { status } = res.data;
if (status !== `Healthy`) return false;
return true;
} catch (error) {
return false;
}
}
}
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",
}
Loading

0 comments on commit 5b4536f

Please sign in to comment.