diff --git a/clients/validator/src/caches/mixnodes.ts b/clients/validator/src/caches/mixnodes.ts index 9b1cfb100a..ddac197727 100644 --- a/clients/validator/src/caches/mixnodes.ts +++ b/clients/validator/src/caches/mixnodes.ts @@ -1,7 +1,7 @@ -import {MixNodeBond, PagedMixnodeResponse} from "../types"; -import { INetClient } from "../net-client" -import {IQueryClient} from "../query-client"; -import {VALIDATOR_API_MIXNODES, VALIDATOR_API_PORT} from "../index"; +import { MixNodeBond, PagedMixnodeResponse } from "../types"; +import { INetClient } from "../net-client"; +import { IQueryClient } from "../query-client"; +import { VALIDATOR_API_MIXNODES, VALIDATOR_API_PORT } from "../index"; import axios from "axios"; export { MixnodesCache }; @@ -13,48 +13,52 @@ export { MixnodesCache }; * available for querying. * */ export default class MixnodesCache { - mixNodes: MixNodeBond[] - client: INetClient | IQueryClient - perPage: number + mixNodes: MixNodeBond[]; + client: INetClient | IQueryClient; + perPage: number; - constructor(client: INetClient | IQueryClient, perPage: number) { - this.client = client; - this.mixNodes = []; - this.perPage = perPage; + constructor(client: INetClient | IQueryClient, perPage: number) { + this.client = client; + this.mixNodes = []; + this.perPage = perPage; + } + + /// Makes repeated requests to assemble a full list of nodes. + /// Requests continue to be make as long as `shouldMakeAnotherRequest()` + // returns true. + async refreshMixNodes(contractAddress: string): Promise { + let newMixnodes: MixNodeBond[] = []; + let response: PagedMixnodeResponse; + let next: string | undefined = undefined; + for (;;) { + response = await this.client.getMixNodes( + contractAddress, + this.perPage, + next + ); + newMixnodes = newMixnodes.concat(response.nodes); + next = response.start_next_after; + // if `start_next_after` is not set, we're done + if (!next) { + break; + } } - /// Makes repeated requests to assemble a full list of nodes. - /// Requests continue to be make as long as `shouldMakeAnotherRequest()` - // returns true. - async refreshMixNodes(contractAddress: string): Promise { - let newMixnodes: MixNodeBond[] = []; - let response: PagedMixnodeResponse; - let next: string | undefined = undefined; - for (;;) { - response = await this.client.getMixNodes(contractAddress, this.perPage, next); - newMixnodes = newMixnodes.concat(response.nodes) - next = response.start_next_after; - // if `start_next_after` is not set, we're done - if (!next) { - break - } - } + this.mixNodes = newMixnodes; + return this.mixNodes; + } - this.mixNodes = newMixnodes - return this.mixNodes; + /// Makes requests to assemble a full list of mixnodes from validator-api + async refreshValidatorAPIMixNodes(urls: string[]): Promise { + for (const url of urls) { + const validator_api_url = new URL(url); + validator_api_url.port = VALIDATOR_API_PORT; + validator_api_url.pathname += VALIDATOR_API_MIXNODES; + const response = await axios.get(validator_api_url.toString()); + if (response.status == 200) { + return response.data; + } } - - /// Makes requests to assemble a full list of mixnodes from validator-api - async refreshValidatorAPIMixNodes(urls: string[]): Promise { - for (const url of urls) { - const validator_api_url = new URL(url); - validator_api_url.port = VALIDATOR_API_PORT; - validator_api_url.pathname += VALIDATOR_API_MIXNODES; - const response = await axios.get(validator_api_url.toString()); - if (response.status == 200) { - return response.data; - } - } - throw new Error("None of the provided validators seem to be alive") - } -} \ No newline at end of file + throw new Error("None of the provided validators seem to be alive"); + } +} diff --git a/clients/validator/src/cli.ts b/clients/validator/src/cli.ts index b515fdaf3a..937de26336 100644 --- a/clients/validator/src/cli.ts +++ b/clients/validator/src/cli.ts @@ -4,15 +4,28 @@ import inquirer from "inquirer"; const VALIDATOR_URLS: string[] = [ "https://testnet-milhon-validator1.nymtech.net", - "https://testnet-milhon-validator2.nymtech.net", + // "https://testnet-milhon-validator2.nymtech.net", // <-- val 2 doesnt work apparently. ]; const DENOM = "punk"; const MOCK_MNEMONIC = "vault risk throw flat garlic pretty clay senior birth correct panic floor around pen horror mail entry arrest zoo devote message evoke street total"; +// ^^ addr: punk10dxwmqjy72s9nkm9x9pluyn6pyx0gkptjhs4k9 +// curr balance: 899999747 + +// const MOCK_MNEMONIC = +// "oil once motion cute crawl patch happy wave donkey zoo retreat matrix emerge adult very universe aware error snap credit actress couple upset engine"; +// ^^ addr: punk1yzr7gtmtlfd0s7s9wpexhteeu05y4xlcvh65eh +// curr balance: 5045 UPUNK + +// const MOCK_MNEMONIC = +// "sample menu edit midnight guard review call record horn antenna stairs awkward fringe document during amazing twelve wise wide escape matter betray staff someone"; +// ^^ addr: punk1wn8lwxe5hvdtx60c6p7ekskmu75agwfrslf0qs +// curr balance: + type AccountType = { addr: string; client: any; - mnemonic: string; + mnemonic?: string; }; function validatorCli() { // define funcs to be used in CLI switch-case @@ -43,6 +56,75 @@ function validatorCli() { restartApp(); } + function sendFundsMenu() { + inquirer + .prompt([ + { + name: "recipient", + type: "input", + message: "please enter the receipient:", + }, + { + name: "amount", + type: "input", + message: "please enter the amount (UPUNK):", + }, + ]) + .then(async ({ recipient, amount }) => { + const { addr, client } = state; + console.log( + `🔥 Hold Tight - Sending ${amount}UPUNK to ${recipient} 🚀` + ); + + const res = await client.send(addr, recipient, [ + { + denom: "upunk", + amount: amount, + }, + ]); + console.log("Funds Transfer Response:", res); + restartApp(); + }); + } + + async function delegateGateway() { + console.log( + "unfortunately - gateway delegation is switched off at the moment." + ); + startTransactionMenu(); + // const id = "punk1yzr7gtmtlfd0s7s9wpexhteeu05y4xlcvh65eh"; + // const gatewayID = "EQhjPpUuy4i1u87nfQMW21WiBT5mJk4dcq4ju7Vct7cB"; + // const coin = { + // denom: "upunk", + // amount: "101", + // }; + // const res = await state.client.delegateToMixnode(gatewayID, coin); + // console.log("delegateMixnode ==> ", res); + } + + async function delegateMixnode() { + const mixNodeID = "2cFpCe7yP79CcuRpf6JBRdJaSp7JF5YcA5SHi8JVm1d2"; + // const mixNodeID = "2Vrr7s2peGiWsPh6xY3ZFEMDRmMNv8xLBUtV5XMyQLSB"; + const coin = { + denom: "upunk", + amount: "1001", + }; + const res = await state.client.delegateToMixnode(mixNodeID, coin); + console.log("delegate to mixnode response: ", res); + } + async function findMinimumMixnodeBond() { + const res = await state.client.minimumMixnodeBond(); + console.log("res is back ", res); + } + + async function bondMixnode() { + state.client.bondMixnode(); + } + + async function checkOwnsMixnodes() { + const res = await state.client.ownsMixNode(); + console.log("owns mixnode? ", res); + } function startTransactionMenu() { inquirer .prompt([ @@ -50,19 +132,88 @@ function validatorCli() { type: "list", name: "task", message: "What now?", - choices: ["send_funds"], + choices: [ + "send_funds", + "get_mixnodes", + "refresh_mixnodes", + "refresh_val_api_mixnodes", + "min_mixn_bond", + "bond_mixnode", + "delegate_mixnode", + "delegate_gateway", + "check_owns_mixnode", + ], }, ]) .then(({ task }) => { switch (task) { case "send_funds": - console.log("sending funds from ", state.addr); + sendFundsMenu(); + break; + case "get_mixnodes": + getMixnodes(); + break; + case "refresh_mixnodes": + refreshMixnodes(); + break; + case "refresh_val_api_mixnodes": + refreshValApiMixnodes(); + break; + case "min_mixn_bond": + findMinimumMixnodeBond(); + break; + case "bond_mixnode": + bondMixnode(); + break; + case "delegate_gateway": + delegateGateway(); + break; + case "delegate_mixnode": + delegateMixnode(); + break; + case "check_owns_mixnode": + checkOwnsMixnodes(); break; default: return null; } }); } + + function queryUserAccount() { + inquirer + .prompt([ + { + type: "input", + name: "query_user", + message: "Please enter the public address of user you wish to query", + }, + ]) + .then(async ({ query_user }) => { + let response = ""; + try { + const client = await ValidatorClient.connectForQuery( + query_user, + VALIDATOR_URLS, + DENOM + ); + const balance = await client.getBalance(query_user); + response = `User ${query_user} has a balance of ${balance?.amount}${balance?.denom}`; + console.log(response); + return validatorCli(); + } catch (error) { + console.log("error back ", error); + return validatorCli(); + } + }); + } + + async function refreshMixnodes() { + const res = await state.client.refreshMixNodes( + "punk1yksauczytk60x5cejaras8w6nwf7r772n3kwkp" + ); + console.log("done:", res); + } function connectAccount() { inquirer .prompt([ @@ -80,7 +231,6 @@ function validatorCli() { "punk" ); - console.log("Decryped address:", addr); const client = await ValidatorClient.connect( addr, MOCK_MNEMONIC, @@ -97,8 +247,9 @@ function validatorCli() { const balance = await client.getBalance(addr); console.log(`connected to validator, our address is ${client.address}`); console.log("connected to validator", client.urls[0]); - console.log("📫 Your address:", addr); - console.log("💰 Your balance:", balance); + console.log( + `💰 Your balance is ${balance?.amount}${balance?.denom.toUpperCase()}` + ); startTransactionMenu(); }) @@ -106,19 +257,40 @@ function validatorCli() { console.log("error: ", err); }); } - + function buildAWallet() { + inquirer + .prompt([ + { + message: "enter your mnemonic to build wallet:", + type: "input", + name: "mnemonic", + }, + ]) + .then(async ({ mnemonic }) => { + const res = await ValidatorClient.buildWallet(mnemonic, DENOM); + console.log("Build_Wallet Response: ", res); + }); + } + async function refreshValApiMixnodes() { + const res = await state.client.refreshValidatorAPIMixNodes(); + console.log("res is back: ", res); + } + function getMixnodes() { + const res = state.client.mixNodesCache; + console.log("Mixnodes", res); + } // app provides a list of possible tasks inquirer .prompt([ { type: "list", name: "task", - message: "So...What would you like to do today?", + message: "Yo, What would you like to do today?", choices: [ "create_account", "connect_account", - "get_mixnodes", - "send_funds", + "build_wallet", + "query_user", ], }, ]) @@ -130,6 +302,12 @@ function validatorCli() { case "connect_account": connectAccount(); break; + case "build_wallet": + buildAWallet(); + break; + case "query_user": + queryUserAccount(); + break; default: return null; } @@ -137,6 +315,3 @@ function validatorCli() { } validatorCli(); -// if it's get mixnodes, return all mixnodes -// if it's create an account -// if it's send funds from one address to another diff --git a/clients/validator/src/index.ts b/clients/validator/src/index.ts index 25b404ade6..a62e69c1cf 100644 --- a/clients/validator/src/index.ts +++ b/clients/validator/src/index.ts @@ -80,6 +80,13 @@ export default class ValidatorClient { this.denom = "u" + prefix; } + /** + * @param contractAddress the user's contract address eg. `punk23o85698370891702470413487` + * @param mnemonic A mnemonic string from which to generate a public/private keypair. + * @param urls the validator URLs in either array of string format. + * @param prefix the denom eg. `punk` + * @returns user's instance of the Validator Client. + */ // allows also entering 'string' by itself for backwards compatibility static async connect( contractAddress: string, @@ -126,6 +133,14 @@ export default class ValidatorClient { throw new Error("None of the provided validators seem to be alive"); } + /** + * This method is the same as connect() but doesnt require a mnemonic + * as you cannot transfer/withdraw once connected. It is effectively ReadOnly. + * @param contractAddress the user's contract address eg. `punk23o85698370891702470413487` + * @param urls the validator URLs in either array of string format. + * @param prefix the denom eg. `punk` + * @returns user's instance of the Validator Client. + */ // allows also entering 'string' by itself for backwards compatibility static async connectForQuery( contractAddress: string, @@ -164,6 +179,10 @@ export default class ValidatorClient { throw new Error("None of the provided validators seem to be alive"); } + /** + * @param urls the validator URLs in either array of string format. + * @returns a shuffled array of validator URLs. + */ private static ensureArray(urls: string | string[]): string[] { let validatorsUrls: string[] = []; if (typeof urls === "string") { @@ -184,8 +203,12 @@ export default class ValidatorClient { return validatorsUrls; } - // an error adapter function that upon an error attempts to switch currently used validator to the next one available - // note that it ALWAYS throws an error + /** + * Error adapter function that - upon an error - attempts to switch currently used validator to the next one available + * note that it ALWAYS throws an error + * @param error Error thrown by async/netw requests in other methods within this class. + * @returns a thrown error, as normal. + */ async handleRequestFailure(error: Error): Promise { // don't bother doing any fancy validator switches if we only have 1 validator to choose from if (this.urls.length > 1) { @@ -208,12 +231,21 @@ export default class ValidatorClient { throw error; } } - + /** + * changes the client's validator to the new validator passed in. + * @param newUrl the URL of the new/alternative validator. + * @returns void + */ private async changeValidator(newUrl: string): Promise { console.log("Changing validator to", newUrl); return await this.client.changeValidator(newUrl); } + /** + * shuffles the array + * @param arr the URL of the new/alternative validator. + * @returns array + */ // adapted from https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array/6274381#6274381 static shuffleArray(arr: T[]): T[] { for (let i = arr.length - 1; i > 0; i--) { @@ -223,6 +255,12 @@ export default class ValidatorClient { return arr; } + /** + * utility function that moves the first element (Val url) to the back + * of the array again. + * @param arr + * @returns void + */ // It is responsibility of the caller to ensure the input array is non-empty private static moveArrayHeadToBack(arr: T[]) { const head = arr.shift(); @@ -265,6 +303,7 @@ export default class ValidatorClient { } /** + * effectively decodes a mnemonic phrase into a public punk address. * @param mnemonic A mnemonic from which to generate a public/private keypair. * @returns the address for this client wallet */ @@ -277,6 +316,12 @@ export default class ValidatorClient { return address; } + /** + * async func to build/create a NYM wallet. + * @param mnemonic + * @param prefix + * @returns Promise + */ static async buildWallet( mnemonic: string, prefix: string @@ -285,12 +330,22 @@ export default class ValidatorClient { return DirectSecp256k1HdWallet.fromMnemonic(mnemonic, signerOptions); } + /** + * this method returns a promise which returns the amount/denom of a given user + * @param address the user's public contract address eg. `punk23o85698370891702470413487` + * @returns user's instance of the Validator Client. + */ getBalance(address: string): Promise { return this.client .getBalance(address, this.denom) .catch((err) => this.handleRequestFailure(err)); } + /** + * Func calls the client (instance of INetClient / NetClient) method `getStateParams(addr)` + * Used in minimumMixnodeBond() and minimumGatewayBond() + * @returns Promisified State Params. + */ async getStateParams(): Promise { return this.client .getStateParams(this.contractAddress) @@ -305,9 +360,9 @@ export default class ValidatorClient { * TODO: We will want to put this puppy on a timer, but for the moment we can * just get things strung together and refresh it manually. */ - refreshMixNodes(): Promise { + async refreshMixNodes(arg: string): Promise { return this.mixNodesCache - .refreshMixNodes(this.contractAddress) + .refreshMixNodes(arg) .catch((err) => this.handleRequestFailure(err)); } @@ -319,7 +374,7 @@ export default class ValidatorClient { * TODO: We will want to put this puppy on a timer, but for the moment we can * just get things strung together and refresh it manually. */ - refreshValidatorAPIMixNodes(): Promise { + async refreshValidatorAPIMixNodes(): Promise { return this.mixNodesCache .refreshValidatorAPIMixNodes(this.urls) .catch((err) => this.handleRequestFailure(err)); @@ -397,6 +452,7 @@ export default class ValidatorClient { amount: Coin ): Promise { if (this.client instanceof NetClient) { + console.log("args mixID ", mixIdentity, " amount ", amount); const result = await this.client .executeContract( this.client.clientAddress, @@ -747,6 +803,10 @@ export default class ValidatorClient { /** * Send funds multiple times from one address to another in a single block. + * @param senderAddress + * @param data + * @param memo + * @returns result */ async sendMultiple( senderAddress: string,