From a1a475e4c88e52ca8fb1d004151a17f3efb39daf Mon Sep 17 00:00:00 2001 From: Amira <56790126+amiraabouhadid@users.noreply.github.com> Date: Sun, 24 Sep 2023 10:41:57 +0300 Subject: [PATCH] add dao module in tfchain client and tfgrid client (#1089) * add dao module in tfchain client and tfgrid client * add dao module * add DaoVoteModel * add dao to tfgrid modules * add dao vote and get scripts * add vote to tfchain module in gridclient * Update config.json * fix naming * remove type any * remove unnecessary file * remove dao as it is on parent class * remove unnecessary import * fix types * fix farmId type * remove hex2a method * remove nonce * add init in vote script * fix hash type, add min(1) to farmId * remove nowblock any type * change hash function parameter * add return types * add patchExtrinsic * fix hashes type * update types casting, add apply() to vote call --- .../grid_client/scripts/get_dao_proposals.ts | 10 ++ .../scripts/tfchain/vote_dao_proposal.ts | 17 ++ packages/grid_client/src/client.ts | 1 + packages/grid_client/src/modules/dao.ts | 22 +++ packages/grid_client/src/modules/index.ts | 1 + packages/grid_client/src/modules/models.ts | 17 +- packages/grid_client/src/modules/tfchain.ts | 20 +++ packages/tfchain_client/src/client.ts | 4 + packages/tfchain_client/src/dao.ts | 151 ++++++++++++++++++ packages/tfchain_client/src/index.ts | 1 + 10 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 packages/grid_client/scripts/get_dao_proposals.ts create mode 100644 packages/grid_client/scripts/tfchain/vote_dao_proposal.ts create mode 100644 packages/grid_client/src/modules/dao.ts create mode 100644 packages/tfchain_client/src/dao.ts diff --git a/packages/grid_client/scripts/get_dao_proposals.ts b/packages/grid_client/scripts/get_dao_proposals.ts new file mode 100644 index 0000000000..0550031e52 --- /dev/null +++ b/packages/grid_client/scripts/get_dao_proposals.ts @@ -0,0 +1,10 @@ +import { getClient } from "./client_loader"; + +async function main() { + const grid3 = await getClient(); + const proposals = await grid3.dao.get(); + console.log(proposals); + await grid3.disconnect(); +} + +main(); diff --git a/packages/grid_client/scripts/tfchain/vote_dao_proposal.ts b/packages/grid_client/scripts/tfchain/vote_dao_proposal.ts new file mode 100644 index 0000000000..8670a5b319 --- /dev/null +++ b/packages/grid_client/scripts/tfchain/vote_dao_proposal.ts @@ -0,0 +1,17 @@ +import { getClient } from "../client_loader"; + +async function main() { + const grid3 = await getClient(); + await grid3.tfchain.init({ name: "harby", secret: grid3.clientOptions.mnemonic }); + await grid3.tfchain.vote({ + name: "harby", + address: "5FWW1F7XHaiRgPEqJdkv9nVgz94AVKXkTKNyfbLcY4rqpaNM", + farmId: 246, + hash: "0xa539b59dcf7ba10764a49c9effb88aea400d3c20f0071c3b85494423079757fe", + approve: true, + }); + + await grid3.disconnect(); +} + +main(); diff --git a/packages/grid_client/src/client.ts b/packages/grid_client/src/client.ts index 1c265ea71d..750468cfc3 100644 --- a/packages/grid_client/src/client.ts +++ b/packages/grid_client/src/client.ts @@ -41,6 +41,7 @@ class GridClient { farmerbot: modules.farmerbot; farms: modules.farms; networks: modules.networks; + dao: modules.dao; bridge: modules.bridge; modules: string[] = []; diff --git a/packages/grid_client/src/modules/dao.ts b/packages/grid_client/src/modules/dao.ts new file mode 100644 index 0000000000..05c9cf0254 --- /dev/null +++ b/packages/grid_client/src/modules/dao.ts @@ -0,0 +1,22 @@ +import { TFClient } from "../clients/tf-grid/client"; +import { GridClientConfig } from "../config"; +import { expose } from "../helpers/expose"; +import { validateInput } from "../helpers/validator"; +import { DaoVoteModel } from "./models"; +class Dao { + client: TFClient; + constructor(config: GridClientConfig) { + this.client = new TFClient(config.substrateURL, config.mnemonic, config.storeSecret, config.keypairType); + } + @expose + @validateInput + async get() { + return await this.client.dao.get(); + } + @expose + @validateInput + async vote(options: DaoVoteModel) { + return (await this.client.dao.vote(options)).apply(); + } +} +export { Dao as dao }; diff --git a/packages/grid_client/src/modules/index.ts b/packages/grid_client/src/modules/index.ts index e886b0fef2..3f81550bb6 100644 --- a/packages/grid_client/src/modules/index.ts +++ b/packages/grid_client/src/modules/index.ts @@ -1,4 +1,5 @@ export * from "./k8s"; +export * from "./dao"; export * from "./machines"; export * from "./models"; export * from "./zdb"; diff --git a/packages/grid_client/src/modules/models.ts b/packages/grid_client/src/modules/models.ts index 26d1902354..fb8923f5ed 100644 --- a/packages/grid_client/src/modules/models.ts +++ b/packages/grid_client/src/modules/models.ts @@ -378,7 +378,12 @@ class KVStoreRemoveModel { class KVStoreBatchRemoveModel { @Expose() @ArrayNotEmpty() @IsString({ each: true }) keys: string[]; } - +class DaoVoteModel { + @Expose() @IsString() @IsNotEmpty() address: string; + @Expose() @IsInt() @IsNotEmpty() @Min(1) farmId: number; + @Expose() @IsBoolean() approve: boolean; + @Expose() @IsString() @IsNotEmpty() hash: string; +} class BalanceGetModel { @Expose() @IsString() @IsNotEmpty() address: string; } @@ -423,7 +428,13 @@ class TfchainWalletInitModel { class TfchainWalletBalanceByAddressModel { @Expose() @IsString() @IsNotEmpty() address: string; } - +class TfchainDaoVoteModel { + @Expose() @IsString() @IsNotEmpty() @IsAlphanumeric() @MaxLength(NameLength) name: string; + @Expose() @IsString() @IsNotEmpty() address: string; + @Expose() @IsInt() @IsNotEmpty() @Min(1) farmId: number; + @Expose() @IsBoolean() approve: boolean; + @Expose() @IsString() @IsNotEmpty() hash: string; +} class TfchainWalletTransferModel { @Expose() @IsString() @IsNotEmpty() @IsAlphanumeric() @MaxLength(NameLength) name: string; @Expose() @IsString() @IsNotEmpty() address_dest: string; @@ -665,6 +676,7 @@ export { AlgorandCreateTransactionModel, AlgorandTransferModel, DiskModel, + DaoVoteModel, NetworkModel, MachineModel, MachinesModel, @@ -740,6 +752,7 @@ export { TfchainWalletBalanceByAddressModel, TfchainWalletTransferModel, TfchainCreateModel, + TfchainDaoVoteModel, WalletMessageSignModel, BlockchainCreateModel, BlockchainCreateResultModel, diff --git a/packages/grid_client/src/modules/tfchain.ts b/packages/grid_client/src/modules/tfchain.ts index 1535b01035..30f712737c 100644 --- a/packages/grid_client/src/modules/tfchain.ts +++ b/packages/grid_client/src/modules/tfchain.ts @@ -22,6 +22,7 @@ import { BlockchainListResultModel, BlockchainSignModel, TfchainCreateModel, + TfchainDaoVoteModel, TfchainWalletBalanceByAddressModel, TfchainWalletInitModel, TfchainWalletTransferModel, @@ -221,6 +222,25 @@ class TFChain implements blockchainInterface { throw Error(`Could not complete transfer transaction: ${e}`); } } + @expose + @validateInput + async vote(options: TfchainDaoVoteModel) { + const mnemonics = await this.getMnemonics(options.name); + const sourceClient = new TFClient(this.substrateURL, mnemonics, this.storeSecret, this.keypairType); + await sourceClient.connect(); + try { + await ( + await sourceClient.dao.vote({ + address: options.address, + farmId: options.farmId, + hash: options.hash, + approve: options.approve, + }) + ).apply(); + } catch (e) { + throw Error(`Could not complete transfer transaction: ${e}`); + } + } @expose @validateInput diff --git a/packages/tfchain_client/src/client.ts b/packages/tfchain_client/src/client.ts index 2154bc6239..a27c821483 100644 --- a/packages/tfchain_client/src/client.ts +++ b/packages/tfchain_client/src/client.ts @@ -11,6 +11,7 @@ import { validateMnemonic } from "bip39"; import { Balances, QueryBalances } from "./balances"; import { Contracts, QueryContracts } from "./contracts"; +import { Dao, QueryDao } from "./dao"; import { QueryFarms } from "./farms"; import { KVStore } from "./kvstore"; import { Nodes, QueryNodes } from "./nodes"; @@ -44,6 +45,7 @@ class QueryClient { api: ApiPromise; contracts: QueryContracts = new QueryContracts(this); balances: QueryBalances = new QueryBalances(this); + dao: QueryDao = new QueryDao(this); farms: QueryFarms = new QueryFarms(this); tftBridge: QueryBridge = new QueryBridge(this); tftPrice: QueryTFTPrice = new QueryTFTPrice(this); @@ -228,8 +230,10 @@ class Client extends QueryClient { termsAndConditions: TermsAndConditions = new TermsAndConditions(this); kvStore: KVStore = new KVStore(this); twins: Twins = new Twins(this); + dao: Dao = new Dao(this); tftBridge: Bridge = new Bridge(this); + declare url: string; mnemonicOrSecret?: string; keypairType: KeypairType; diff --git a/packages/tfchain_client/src/dao.ts b/packages/tfchain_client/src/dao.ts new file mode 100644 index 0000000000..66fb294253 --- /dev/null +++ b/packages/tfchain_client/src/dao.ts @@ -0,0 +1,151 @@ +import moment from "moment"; + +import { Client, QueryClient } from "./client"; +import { checkConnection } from "./utils"; +interface Proposal { + threshold: number; + ayes: AyesAndNayes[]; + nayes: AyesAndNayes[]; + vetos: number; + end: moment.Moment; + hash: string; + action: string; + description: string; + link: string; + ayesProgress: number; + nayesProgress: number; +} +interface AyesAndNayes { + farmId: number; + weight: number; +} +interface ProposalRemark { + args: { remark: string }; +} +interface DaoProposal { + description: string; + link: string; +} +interface ProposalVotes { + ayes: AyesAndNayes[]; + nays: AyesAndNayes[]; + threshold: number; + vetos: number; + end: number; +} +export interface Proposals { + active: Proposal[]; + inactive: Proposal[]; +} +export interface DaoVoteOptions { + address: string; + farmId: number; + hash: string; + approve: boolean; +} + +class QueryDao { + constructor(public client: QueryClient) { + this.client = client; + } + + @checkConnection + async get(): Promise { + const hashesJson = await this.client.api.query.dao.proposalList(); + const hashes = hashesJson.toPrimitive() as string[]; + const activeProposals: Proposal[] = []; + const inactiveProposals: Proposal[] = []; + for await (const hash of hashes) { + const daoProposal = await this.getDaoProposal(hash); + const proposal = await this.getProposal(hash); + const proposalVotes = await this.getProposalVotes(hash); + + const nowBlock: number = +(await this.client.api.query.system.number()); + const timeUntilEnd = (proposalVotes.end - nowBlock) * 6; + if (proposal && daoProposal) { + if (proposalVotes.end < nowBlock) { + inactiveProposals.push({ + threshold: proposalVotes.threshold, + ayes: proposalVotes.ayes, //[{farmId: number, weight: number}] + nayes: proposalVotes.nays, + vetos: proposalVotes.vetos, + end: moment().add(timeUntilEnd, "second"), + hash: hash, + action: proposal.args.remark, + description: daoProposal.description, + link: daoProposal.link, + ayesProgress: this.getProgress(proposalVotes.ayes, proposalVotes.nays, true), + nayesProgress: this.getProgress(proposalVotes.ayes, proposalVotes.nays, false), + }); + } else { + activeProposals.push({ + threshold: proposalVotes.threshold, + ayes: proposalVotes.ayes, //[{farmId: number, weight: number}] + nayes: proposalVotes.nays, + vetos: proposalVotes.vetos, + end: moment().add(timeUntilEnd, "second"), + hash: hash, + action: proposal.args.remark, + description: daoProposal.description, + link: daoProposal.link, + ayesProgress: this.getProgress(proposalVotes.ayes, proposalVotes.nays, true), + nayesProgress: this.getProgress(proposalVotes.ayes, proposalVotes.nays, false), + }); + } + } + } + return { active: activeProposals, inactive: inactiveProposals }; + } + private getVotesWithWeights(votes: AyesAndNayes[]): number { + return votes.reduce((total: number, vote: AyesAndNayes) => { + return total + vote.weight; + }, 0); + } + private getProgress(ayes: AyesAndNayes[], nayes: AyesAndNayes[], typeAye: boolean): number { + const totalAyeWeight = ayes ? this.getVotesWithWeights(ayes) : 0; + const totalNayeWeight = nayes ? this.getVotesWithWeights(nayes) : 0; + const total = totalAyeWeight + totalNayeWeight; + if (total > 0) { + if (typeAye) { + return (totalAyeWeight / total) * 100; + } + return (totalNayeWeight / total) * 100; + } + return 0; + } + @checkConnection + private async getDaoProposal(hash: string): Promise { + const proposalJson = await this.client.api.query.dao.proposals(hash); + const proposal: DaoProposal = proposalJson.toPrimitive() as unknown as DaoProposal; + return proposal; + } + + @checkConnection + private async getProposal(hash: string): Promise { + try { + const proposalJson = await this.client.api.query.dao.proposalOf(hash); + const proposalRemark: ProposalRemark = proposalJson.toPrimitive() as unknown as ProposalRemark; + return proposalRemark; + } catch (error) { + console.warn("Couldn't decode a proposal"); + } + } + @checkConnection + private async getProposalVotes(hash: string): Promise { + const votesJson = await this.client.api.query.dao.voting(hash); + const proposalVotes: ProposalVotes = votesJson.toPrimitive() as unknown as ProposalVotes; + return proposalVotes; + } +} +class Dao extends QueryDao { + constructor(public client: Client) { + super(client); + this.client = client; + } + @checkConnection + async vote(options: DaoVoteOptions) { + const extrinsic = this.client.api.tx.dao.vote(options.farmId, options.hash, options.approve); + return this.client.patchExtrinsic(extrinsic); + } +} +export { Dao, QueryDao }; diff --git a/packages/tfchain_client/src/index.ts b/packages/tfchain_client/src/index.ts index 1e27cfb05f..0b780cc085 100644 --- a/packages/tfchain_client/src/index.ts +++ b/packages/tfchain_client/src/index.ts @@ -1,4 +1,5 @@ export * from "./balances"; +export * from "./dao"; export * from "./client"; export * from "./contracts"; export * from "./farms";