sdk_client/src/services.ts
2024-05-28 11:43:57 +02:00

906 lines
33 KiB
TypeScript

import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage, UnknownMessage } from '../dist/pkg/sdk_client';
import IndexedDB from './database'
import { WebSocketClient } from './websockets';
class Services {
private static instance: Services;
private sdkClient: any;
private current_process: string | null = null;
private websocketConnection: WebSocketClient[] = [];
private sp_address: string | null = null;
// Private constructor to prevent direct instantiation from outside
private constructor() {}
// Method to access the singleton instance of Services
public static async getInstance(): Promise<Services> {
if (!Services.instance) {
Services.instance = new Services();
await Services.instance.init();
}
return Services.instance;
}
// The init method is now part of the instance, and should only be called once
private async init(): Promise<void> {
this.sdkClient = await import("../dist/pkg/sdk_client");
this.sdkClient.setup();
await this.updateProcesses();
}
public async addWebsocketConnection(url: string): Promise<void> {
const services = await Services.getInstance();
const newClient = new WebSocketClient(url, services);
if (!services.websocketConnection.includes(newClient)) {
services.websocketConnection.push(newClient);
}
}
public async isNewUser(): Promise<boolean> {
let isNew = false;
try {
const indexedDB = await IndexedDB.getInstance();
const db = await indexedDB.getDb();
let userListObject = await indexedDB.getAll<User>(db, indexedDB.getStoreList().AnkUser);
if (userListObject.length == 0) {
isNew = true;
}
} catch (error) {
console.error("Failed to retrieve isNewUser :", error);
}
return isNew;
}
public async displayCreateId(): Promise<void> {
const services = await Services.getInstance();
await services.createIdInjectHtml();
services.attachSubmitListener("form4nk", (event) => services.createId(event));
services.attachClickListener("displayrecover", services.displayRecover);
await services.displayProcess();
}
public async displaySendMessage(): Promise<void> {
const services = await Services.getInstance();
await services.injectHtml('Messaging');
services.attachSubmitListener("form4nk", (event) => services.sendMessage(event));
// const ourAddress = document.getElementById('our_address');
// if (ourAddress) {
// ourAddress.innerHTML = `<strong>Our Address:</strong> ${this.sp_address}`
// }
// services.attachClickListener("displaysendmessage", services.displaySendMessage);
// await services.displayProcess();
}
public async sendMessage(event: Event): Promise<void> {
event.preventDefault();
const services = await Services.getInstance();
let availableAmt: number = 0;
// check available amount
try {
availableAmt = await services.sdkClient.get_available_amount_for_user(true);
} catch (error) {
console.error('Failed to get available amount');
return;
}
if (availableAmt < 2000) {
try {
await services.obtainTokenWithFaucet();
} catch (error) {
console.error('Failed to obtain faucet token:', error);
return;
}
}
const spAddressElement = document.getElementById("sp_address") as HTMLInputElement;
const messageElement = document.getElementById("message") as HTMLInputElement;
if (!spAddressElement || !messageElement) {
console.error("One or more elements not found");
return;
}
const recipientSpAddress = spAddressElement.value;
const message = messageElement.value;
const msg_payload: UnknownMessage = {sender: this.sp_address!, message: message};
let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload);
if (notificationInfo) {
let networkMsg = notificationInfo.new_network_msg;
console.debug(networkMsg);
const connection = await services.pickWebsocketConnectionRandom();
const flag: AnkFlag = "Unknown";
try {
// send message (transaction in envelope)
await services.updateMessages(networkMsg);
connection?.sendMessage(flag, networkMsg.ciphertext!);
} catch (error) {
throw error;
}
// add peers list
// add processes list
}
}
public async createId(event: Event): Promise<void> {
event.preventDefault();
// verify we don't already have an user
const services = await Services.getInstance();
try {
let user = await services.getUserInfo();
if (user) {
console.error("User already exists, please recover");
return;
}
} catch (error) {
throw error;
}
const passwordElement = document.getElementById("password") as HTMLInputElement;
const processElement = document.getElementById("selectProcess") as HTMLSelectElement;
if (!passwordElement || !processElement) {
throw 'One or more elements not found';
}
const password = passwordElement.value;
this.current_process = processElement.value;
// console.log("JS password: " + password + " process: " + this.current_process);
// To comment if test
// if (!Services.instance.isPasswordValid(password)) return;
const label = null;
const birthday_signet = 50000;
const birthday_main = 500000;
let createUserReturn: createUserReturn;
try {
createUserReturn = services.sdkClient.create_user(password, label, birthday_main, birthday_signet, this.current_process);
} catch (error) {
throw error;
}
let user = createUserReturn.user;
// const shares = user.shares;
// send the shares on the network
const revokeData = user.revoke_data;
if (!revokeData) {
throw 'Failed to get revoke data from wasm';
}
// user.shares = [];
user.revoke_data = null;
try {
const indexedDb = await IndexedDB.getInstance();
const db = await indexedDb.getDb();
await indexedDb.writeObject(db, indexedDb.getStoreList().AnkUser, user, null);
} catch (error) {
throw `Failed to write user object: ${error}`;
}
try {
await services.obtainTokenWithFaucet();
} catch (error) {
throw error;
}
await services.displayRevokeImage(new Uint8Array(revokeData));
}
public async displayRecover(): Promise<void> {
const services = await Services.getInstance();
await services.recoverInjectHtml();
services.attachSubmitListener("form4nk", (event) => services.recover(event));
services.attachClickListener("displaycreateid", services.displayCreateId);
services.attachClickListener("displayrevoke", services.displayRevoke);
services.attachClickListener("submitButtonRevoke", services.revoke);
await services.displayProcess();
}
public async recover(event: Event) {
event.preventDefault();
const passwordElement = document.getElementById("password") as HTMLInputElement;
const processElement = document.getElementById("selectProcess") as HTMLSelectElement;
if (!passwordElement || !processElement) {
console.error("One or more elements not found");
return;
}
const password = passwordElement.value;
const process = processElement.value;
// console.log("JS password: " + password + " process: " + process);
// To comment if test
// if (!Services.instance.isPasswordValid(password)) return;
// Get user in db
const services = await Services.getInstance();
try {
const user = await services.getUserInfo();
if (user) {
services.sdkClient.login_user(password, user.pre_id, user.recover_data, user.shares, user.outputs);
this.sp_address = services.sdkClient.get_recover_address();
if (this.sp_address) {
console.info('Using sp_address:', this.sp_address);
await services.obtainTokenWithFaucet();
}
}
} catch (error) {
console.error(error);
}
console.info(this.sp_address);
// TODO: check blocks since last_scan and update outputs
await services.displaySendMessage();
}
public async displayRevokeImage(revokeData: Uint8Array): Promise<void> {
const services = await Services.getInstance();
await services.revokeImageInjectHtml();
services.attachClickListener("displayupdateanid", services.displayUpdateAnId);
let imageBytes = await services.getRecoverImage('assets/4nk_revoke.jpg');
if (imageBytes != null) {
var elem = document.getElementById("revoke") as HTMLAnchorElement;
if (elem != null) {
let imageWithData = services.sdkClient.add_data_to_image(imageBytes, revokeData, true);
const blob = new Blob([imageWithData], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
// Set the href attribute for download
elem.href = url;
elem.download = 'revoke_4NK.jpg';
}
}
}
private async getRecoverImage(imageUrl:string): Promise<Uint8Array|null> {
let imageBytes = null;
try {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
imageBytes = new Uint8Array(arrayBuffer);
} catch (error) {
console.error("Failed to get image : "+imageUrl, error);
}
return imageBytes;
}
public async displayRevoke(): Promise<void> {
const services = await Services.getInstance();
await services.revokeInjectHtml();
services.attachClickListener("displayrecover", Services.instance.displayRecover);
services.attachSubmitListener("form4nk", Services.instance.revoke);
}
public async revoke(event: Event): Promise<void> {
event.preventDefault();
console.log("JS revoke click ");
// TODO
alert("revoke click to do ...");
}
public async displayUpdateAnId() {
const services = await Services.getInstance();
await services.updateIdInjectHtml();
services.attachSubmitListener("form4nk", services.updateAnId);
}
public async parseNetworkMessage(raw: string, feeRate: number): Promise<CachedMessage> {
const services = await Services.getInstance();
try {
const msg: CachedMessage = services.sdkClient.parse_network_msg(raw, feeRate);
return msg;
} catch (error) {
throw error;
}
}
public async updateAnId(event: Event): Promise<void> {
event.preventDefault();
// TODO get values
const firstNameElement = 'firstName';
const lastNameElement = 'lastName';
const firstName = document.getElementById(firstNameElement) as HTMLInputElement;
const lastName = document.getElementById(lastNameElement) as HTMLInputElement;
console.log("JS updateAnId submit ");
// TODO
alert("updateAnId submit to do ... Name : "+firstName.value + " "+lastName.value);
// TODO Mock add user member to process
}
public async displayProcess(): Promise<void> {
const services = await Services.getInstance();
const processList = await services.getAllProcess();
const selectProcess = document.getElementById("selectProcess");
if (selectProcess) {
processList.forEach((process) => {
let child = new Option(process.name, process.name);
if (!selectProcess.contains(child)) {
selectProcess.appendChild(child);
}
})
}
}
public async addProcess(process: Process): Promise<void> {
try {
const indexedDB = await IndexedDB.getInstance();
const db = await indexedDB.getDb();
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null);
} catch (error) {
console.log('addProcess failed: ',error);
}
}
public async getAllProcess(): Promise<Process[]> {
try {
const indexedDB = await IndexedDB.getInstance();
const db = await indexedDB.getDb();
let processListObject = await indexedDB.getAll<Process>(db, indexedDB.getStoreList().AnkProcess);
return processListObject;
} catch (error) {
console.log('getAllProcess failed: ',error);
return [];
}
}
public async updateOwnedOutputsForUser(): Promise<void> {
const services = await Services.getInstance();
let latest_outputs: outputs_list;
try {
latest_outputs = services.sdkClient.get_outpoints_for_user();
} catch (error) {
console.error(error);
return;
}
try {
let user = await services.getUserInfo();
if (user) {
user.outputs = latest_outputs;
// console.warn(user);
await services.updateUser(user);
}
} catch (error) {
console.error(error);
}
}
public async getAllProcessForUser(pre_id: string): Promise<Process[]> {
const services = await Services.getInstance();
let user: User;
let userProcessList: Process[] = [];
try {
const indexedDB = await IndexedDB.getInstance();
const db = await indexedDB.getDb();
user = await indexedDB.getObject<User>(db, indexedDB.getStoreList().AnkUser, pre_id);
} catch (error) {
console.error('getAllUserProcess failed: ',error);
return [];
}
try {
const processListObject = await services.getAllProcess();
processListObject.forEach(async (processObject) => {
if (processObject.members.includes(user.pre_id)) {
userProcessList.push(processObject);
}
})
} catch (error) {
console.error('getAllUserProcess failed: ',error);
return [];
}
return userProcessList;
}
public async getProcessByName(name: string): Promise<Process | null> {
console.log('getProcessByName name: '+name);
const indexedDB = await IndexedDB.getInstance();
const db = await indexedDB.getDb();
const process = await indexedDB.getFirstMatchWithIndex<Process>(db, indexedDB.getStoreList().AnkProcess, 'by_name', name);
console.log('getProcessByName process: '+process);
return process;
}
public async updateMessages(message: CachedMessage): Promise<void> {
const indexedDb = await IndexedDB.getInstance();
const db = await indexedDb.getDb();
try {
await indexedDb.setObject(db, indexedDb.getStoreList().AnkMessages, message, null);
} catch (error) {
throw error;
}
}
public async removeMessage(id: number): Promise<void> {
const indexedDb = await IndexedDB.getInstance();
const db = await indexedDb.getDb();
try {
await indexedDb.rmObject(db, indexedDb.getStoreList().AnkMessages, id);
} catch (error) {
throw error;
}
}
public async updateProcesses(): Promise<void> {
const services = await Services.getInstance();
const processList: Process[] = services.sdkClient.get_processes();
processList.forEach(async (process: Process) => {
const indexedDB = await IndexedDB.getInstance();
const db = await indexedDB.getDb();
try {
const processStore = await indexedDB.getObject<Process>(db, indexedDB.getStoreList().AnkProcess, process.id);
if (!processStore) {
await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null);
}
} catch (error) {
console.error('Error while writing process', process.name, 'to indexedDB:', error);
}
})
}
public attachClickListener(elementId: string, callback: (event: Event) => void): void {
const element = document.getElementById(elementId);
element?.removeEventListener("click", callback);
element?.addEventListener("click", callback);
}
public attachSubmitListener(elementId: string, callback: (event: Event) => void): void {
const element = document.getElementById(elementId);
element?.removeEventListener("submit", callback);
element?.addEventListener("submit", callback);
}
public async revokeInjectHtml() {
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
container.innerHTML =
` <div class='card'>
<div class='side-by-side'>
<h3>Revoke an Id</h3>
<div>
<a href='#' id='displayrecover'>Recover</a>
</div>
</div>
<form id='form4nk' action='#'>
<label for='password'>Password :</label>
<input type='password' id='password' />
<hr/>
<div class='image-container'>
<label class='image-label'>Revoke image</label>
<img src='assets/revoke.jpeg' alt='' />
</div>
<hr/>
<button type='submit' id='submitButton' class='recover bg-primary'>Revoke</button>
</form>
</div>
`;
}
public async revokeImageInjectHtml() {
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
container.innerHTML =
`<div class='card'>
<div class='side-by-side'>
<h3>Revoke image</h3>
<div><a href='#' id='displayupdateanid'>Update an Id</a></div>
</div>
</div>
<div class='card-revoke'>
<a href='#' download='revoke_4NK.jpg' id='revoke'>
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'>
<path
d='M246.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3V320c0 17.7 14.3 32 32 32s32-14.3 32-32V109.3l73.4 73.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-128-128zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 53 43 96 96 96H352c53 0 96-43 96-96V352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V352z'
/>
</svg>
</a>
<div class='image-container'>
<img src='assets/4nk_revoke.jpg' alt='' />
</div>
</div>`;
}
public async recoverInjectHtml() {
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
const services = await Services.getInstance();
await services.updateProcesses();
container.innerHTML =
`<div class='card'>
<div class='side-by-side'>
<h3>Recover my Id</h3>
<div><a href='#'>Processes</a></div>
</div>
<form id='form4nk' action='#'>
<label for='password'>Password :</label>
<input type='password' id='password' />
<input type='hidden' id='currentpage' value='recover' />
<select id='selectProcess' class='custom-select'></select><hr/>
<div class='side-by-side'>
<button type='submit' id='submitButton' class='recover bg-primary'>Recover</button>
<div>
<a href='#' id='displaycreateid'>Create an Id</a>
</div>
</div><hr/>
<a href='#' id='displayrevoke' class='btn'>Revoke</a>
</form><br/>
<div id='passwordalert' class='passwordalert'></div>
</div>`;
}
public async createIdInjectHtml() {
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
container.innerHTML =
`<div class='card'>
<div class='side-by-side'>
<h3>Create an Id</h3>
<div><a href='#'>Processes</a></div>
</div>
<form id='form4nk' action='#'>
<label for='password'>Password :</label>
<input type='password' id='password' /><hr/>
<input type='hidden' id='currentpage' value='creatid' />
<select id='selectProcess' class='custom-select'></select><hr/>
<div class='side-by-side'>
<button type='submit' id='submitButton' class='bg-primary'>Create</button>
<div>
<a href='#' id='displayrecover'>Recover</a>
</div>
</div>
</form><br/>
<div id='passwordalert' class='passwordalert'></div>
</div>`;
}
public async updateIdInjectHtml() {
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
container.innerHTML =
`<body>
<div class='container'>
<div>
<h3>Update an Id</h3>
</div>
<hr />
<form id='form4nk' action='#'>
<label for='firstName'>First Name:</label>
<input type='text' id='firstName' name='firstName' required />
<label for='lastName'>Last Name:</label>
<input type='text' id='lastName' name='lastName' required />
<label for='Birthday'>Birthday:</label>
<input type='date' id='Birthday' name='birthday' />
<label for='file'>File:</label>
<input type='file' id='fileInput' name='file' />
<label>Third parties:</label>
<div id='sp-address-block'>
<div class='side-by-side'>
<input
type='text'
name='sp-address'
id='sp-address'
placeholder='sp address'
form='no-form'
/>
<button
type='button'
class='circle-btn bg-secondary'
id='add-sp-address-btn'
>
+
</button>
</div>
</div>
<div class='div-text-area'>
<textarea
name='bio'
id=''
cols='30'
rows='10'
placeholder='Bio'
></textarea>
</div>
<button type='submit' class='bg-primary'>Update</button>
</form>
</div>
</body>`;
}
public async injectHtml(processName: string) {
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
const services = await Services.getInstance();
// do we have all processes in db?
const knownProcesses = await services.getAllProcess();
const processesFromNetwork: Process[] = services.sdkClient.get_processes();
const processToAdd = processesFromNetwork.filter(processFromNetwork => !knownProcesses.some(knownProcess => knownProcess.id === processFromNetwork.id));
processToAdd.forEach(async p => {
await services.addProcess(p);
})
// get the process we need
const process = await services.getProcessByName(processName);
if (process) {
container.innerHTML = process.html;
} else {
console.error("No process ", processName);
}
}
// public async getCurrentProcess(): Promise<string> {
// let currentProcess = "";
// try {
// const indexedDB = await IndexedDB.getInstance();
// const db = indexedDB.getDb();
// currentProcess = await indexedDB.getObject<string>(db, indexedDB.getStoreList().AnkSession, Services.CURRENT_PROCESS);
// } catch (error) {
// console.error("Failed to retrieve currentprocess object :", error);
// }
// return currentProcess;
// }
public isPasswordValid(password: string) {
var alertElem = document.getElementById("passwordalert");
var success = true;
var strength = 0;
if (password.match(/[a-z]+/)) {
var strength = 0;
strength += 1;
}
if (password.match(/[A-Z]+/)) {
strength += 1;
}
if (password.match(/[0-9]+/)) {
strength += 1;
}
if (password.match(/[$@#&!]+/)) {
strength += 1;
}
if (alertElem !== null) {
// TODO Passer à 18
if (password.length < 4) {
alertElem.innerHTML = "Password size is < 4";
success = false;
} else {
if (password.length > 30) {
alertElem.innerHTML = "Password size is > 30";
success = false;
} else {
if (strength < 4) {
alertElem.innerHTML = "Password need [a-z] [A-Z] [0-9]+ [$@#&!]+";
success = false;
}
}
}
}
return success;
}
private async pickWebsocketConnectionRandom(): Promise<WebSocketClient | null> {
const services = await Services.getInstance();
const websockets = services.websocketConnection;
if (websockets.length === 0) {
console.error("No websocket connection available at the moment");
return null;
} else {
const random = Math.floor(Math.random() * websockets.length);
return websockets[random];
}
}
public async obtainTokenWithFaucet(): Promise<void> {
const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom();
if (!connection) {
throw 'no available relay connections';
}
let cachedMsg: CachedMessage;
try {
const flag: AnkFlag = 'Faucet';
cachedMsg = services.sdkClient.create_faucet_msg();
if (cachedMsg.commitment && cachedMsg.recipient) {
let faucetMsg: FaucetMessage = {
sp_address: cachedMsg.recipient,
commitment: cachedMsg.commitment,
}
connection.sendMessage(flag, JSON.stringify(faucetMsg));
}
} catch (error) {
throw `Failed to obtain tokens with relay ${connection.getUrl()}: ${error}`;
}
try {
await services.updateMessages(cachedMsg);
} catch (error) {
throw error;
}
}
public async updateUser(user: User): Promise<void> {
try {
const indexedDB = await IndexedDB.getInstance();
const db = await indexedDB.getDb();
await indexedDB.setObject(db, indexedDB.getStoreList().AnkUser, user, null);
} catch (error) {
throw error;
}
}
public async getUserInfo(): Promise<User | null> {
try {
const indexedDB = await IndexedDB.getInstance();
const db = await indexedDB.getDb();
let user = await indexedDB.getAll<User>(db, indexedDB.getStoreList().AnkUser);
// This should never happen
if (user.length > 1) {
throw "Multiple users in db";
} else {
let res = user.pop();
if (res === undefined) {
return null;
} else {
return res;
}
}
} catch (error) {
throw error;
}
}
public async answer_confirmation_message(msg: CachedMessage): Promise<void> {
const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom();
if (!connection) {
throw new Error("No connection to relay");
}
let user: User;
try {
let possibleUser = await services.getUserInfo();
if (!possibleUser) {
throw new Error("No user loaded, please first create a new user or login");
} else {
user = possibleUser;
}
} catch (error) {
throw error;
}
let notificationInfo: createTransactionReturn;
try {
const feeRate = 1;
notificationInfo = services.sdkClient.answer_confirmation_transaction(msg.id, feeRate);
} catch (error) {
throw new Error(`Failed to create confirmation transaction: ${error}`);
}
const flag: AnkFlag = "NewTx";
const newTxMsg: NewTxMessage = {
'transaction': notificationInfo.transaction,
'tweak_data': null
}
connection.sendMessage(flag, JSON.stringify(newTxMsg));
await services.updateMessages(notificationInfo.new_network_msg);
return;
}
public async confirm_sender_address(msg: CachedMessage): Promise<void> {
const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom();
if (!connection) {
throw new Error("No connection to relay");
}
let user: User;
try {
let possibleUser = await services.getUserInfo();
if (!possibleUser) {
throw new Error("No user loaded, please first create a new user or login");
} else {
user = possibleUser;
}
} catch (error) {
throw error;
}
let notificationInfo: createTransactionReturn;
try {
const feeRate = 1;
notificationInfo = services.sdkClient.create_confirmation_transaction(msg.id, feeRate);
} catch (error) {
throw new Error(`Failed to create confirmation transaction: ${error}`);
}
const flag: AnkFlag = "NewTx";
const newTxMsg: NewTxMessage = {
'transaction': notificationInfo.transaction,
'tweak_data': null
}
connection.sendMessage(flag, JSON.stringify(newTxMsg));
await services.updateMessages(notificationInfo.new_network_msg);
return;
}
public async notify_address_for_message(sp_address: string, message: UnknownMessage): Promise<createTransactionReturn> {
const services = await Services.getInstance();
const connection = await services.pickWebsocketConnectionRandom();
if (!connection) {
throw 'No available connection';
}
try {
const feeRate = 1;
let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate);
const flag: AnkFlag = "NewTx";
const newTxMsg: NewTxMessage = {
'transaction': notificationInfo.transaction,
'tweak_data': null
}
connection.sendMessage(flag, JSON.stringify(newTxMsg));
console.info('Successfully sent notification transaction');
return notificationInfo;
} catch (error) {
throw 'Failed to create notification transaction:", error';
}
}
}
export default Services;