diff --git a/package-lock.json b/package-lock.json index f43b82a49..1f358a9fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@multiversx/sdk-core", - "version": "14.1.2", + "version": "14.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@multiversx/sdk-core", - "version": "14.1.2", + "version": "14.2.0", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", diff --git a/package.json b/package.json index d884719fb..5a2df1f1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-core", - "version": "14.1.2", + "version": "14.2.0", "description": "MultiversX SDK for JavaScript and TypeScript", "author": "MultiversX", "homepage": "https://multiversx.com", diff --git a/src/abi/smartContract.ts b/src/abi/smartContract.ts index 7282430be..5c991eef7 100644 --- a/src/abi/smartContract.ts +++ b/src/abi/smartContract.ts @@ -19,14 +19,7 @@ import { } from "./interface"; import { NativeSerializer } from "./nativeSerializer"; import { Query } from "./query"; -import { EndpointDefinition, TypedValue } from "./typesystem"; - -interface IAbi { - constructorDefinition: EndpointDefinition; - - getEndpoints(): EndpointDefinition[]; - getEndpoint(name: string | ContractFunction): EndpointDefinition; -} +import { Abi, EndpointDefinition, TypedValue } from "./typesystem"; /** * * @deprecated component. Use "SmartContractTransactionsFactory" or "SmartContractController", instead. @@ -35,7 +28,7 @@ interface IAbi { */ export class SmartContract implements ISmartContract { private address: Address = Address.empty(); - private abi?: IAbi; + private abi?: Abi; /** * This object contains a function for each endpoint defined by the contract. @@ -55,7 +48,7 @@ export class SmartContract implements ISmartContract { /** * Create a SmartContract object by providing its address on the Network. */ - constructor(options: { address?: Address; abi?: IAbi } = {}) { + constructor(options: { address?: Address; abi?: Abi } = {}) { this.address = options.address || Address.empty(); this.abi = options.abi; @@ -105,13 +98,16 @@ export class SmartContract implements ISmartContract { return this.address; } - private getAbi(): IAbi { + private getAbi(): Abi { guardValueIsSet("abi", this.abi); return this.abi!; } getEndpoint(name: string | ContractFunction): EndpointDefinition { - return this.getAbi().getEndpoint(name); + if (typeof name === "string") { + return this.getAbi().getEndpoint(name); + } + return this.getAbi().getEndpoint(name.name); } /** diff --git a/src/core/constants.ts b/src/core/constants.ts index 765ed156c..b9dbfa59c 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -14,6 +14,7 @@ export const VM_TYPE_WASM_VM = new Uint8Array([0x05, 0x00]); export const CONTRACT_DEPLOY_ADDRESS_HEX = "0000000000000000000000000000000000000000000000000000000000000000"; export const DELEGATION_MANAGER_SC_ADDRESS_HEX = "000000000000000000010000000000000000000000000000000000000004ffff"; export const ESDT_CONTRACT_ADDRESS_HEX = "000000000000000000010000000000000000000000000000000000000002ffff"; +export const GOVERNANCE_CONTRACT_ADDRESS_HEX = "000000000000000000010000000000000000000000000000000000000003ffff"; export const DEFAULT_MESSAGE_VERSION = 1; export const MESSAGE_PREFIX = "\x17Elrond Signed Message:\n"; diff --git a/src/core/transactionsFactoryConfig.ts b/src/core/transactionsFactoryConfig.ts index d898fbb00..a7b38372f 100644 --- a/src/core/transactionsFactoryConfig.ts +++ b/src/core/transactionsFactoryConfig.ts @@ -44,6 +44,12 @@ export class TransactionsFactoryConfig { gasLimitNftChangeToDynamic: bigint; gasLimitUpdateTokenId: bigint; gasLimitRegisterDynamic: bigint; + gasLimitForProposal: bigint; + gasLimitForVote: bigint; + gasLimitForClosingProposal: bigint; + gasLimitForClearProposals: bigint; + gasLimitForChangeConfig: bigint; + gasLimitForClaimAccumulatedFees: bigint; constructor(options: { chainID: string }) { // General-purpose configuration @@ -100,5 +106,13 @@ export class TransactionsFactoryConfig { // Configuration for smart contract operations this.gasLimitClaimDeveloperRewards = 6000000n; this.gasLimitChangeOwnerAddress = 6000000n; + + // Configuration for governance operations + this.gasLimitForProposal = 50_000_000n; + this.gasLimitForVote = 5_000_000n; + this.gasLimitForClosingProposal = 50_000_000n; + this.gasLimitForClearProposals = 50_000_000n; + this.gasLimitForChangeConfig = 50_000_000n; + this.gasLimitForClaimAccumulatedFees = 1_000_000n; } } diff --git a/src/entrypoints/entrypoints.ts b/src/entrypoints/entrypoints.ts index 9431e71b0..eb4cd1232 100644 --- a/src/entrypoints/entrypoints.ts +++ b/src/entrypoints/entrypoints.ts @@ -12,6 +12,8 @@ import { TransactionWatcher, } from "../core"; import { DelegationController, DelegationTransactionsFactory } from "../delegation"; +import { MultisigTransactionsFactory } from "../multisig"; +import { MultisigController } from "../multisig/multisigController"; import { ApiNetworkProvider, ProxyNetworkProvider } from "../networkProviders"; import { INetworkProvider } from "../networkProviders/interface"; import { SmartContractTransactionsFactory } from "../smartContracts"; @@ -180,6 +182,17 @@ export class NetworkEntrypoint { config: new TransactionsFactoryConfig({ chainID: this.chainId }), }); } + + createMultisigController(abi: Abi): MultisigController { + return new MultisigController({ chainID: this.chainId, networkProvider: this.networkProvider, abi: abi }); + } + + createMultisigTransactionsFactory(abi: Abi): MultisigTransactionsFactory { + return new MultisigTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: this.chainId }), + abi: abi, + }); + } } export class TestnetEntrypoint extends NetworkEntrypoint { diff --git a/src/governance/governanceController.spec.ts b/src/governance/governanceController.spec.ts new file mode 100644 index 000000000..c972655aa --- /dev/null +++ b/src/governance/governanceController.spec.ts @@ -0,0 +1,227 @@ +import { assert } from "chai"; +import { Account } from "../accounts"; +import { SmartContractQueryResponse } from "../core"; +import { Address } from "../core/address"; +import { ProxyNetworkProvider } from "../networkProviders"; +import { b64TopicsToBytes, MockNetworkProvider } from "../testutils"; +import { KeyPair, UserSecretKey } from "../wallet"; +import { GovernanceController } from "./governanceController"; +import { Vote } from "./resources"; + +describe("test governance controller", function () { + const chainID = "D"; + const controller = new GovernanceController({ + chainID: chainID, + networkProvider: new ProxyNetworkProvider("https://devnet-gateway.multiversx.com"), + }); + + const commitHash = "1db734c0315f9ec422b88f679ccfe3e0197b9d67"; + const governanceAddress = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla"; + + const aliceBech32 = "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"; + const secretKey = UserSecretKey.fromString("413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"); + const keypair = new KeyPair(secretKey); + const alice = Account.newFromKeypair(keypair); + + it("should create transaction for creating new proposal", async function () { + const expectedData = `proposal@${Buffer.from(commitHash).toString("hex")}@0a@0f`; + + const transaction = await controller.createTransactionForNewProposal(alice, alice.getNonceThenIncrement(), { + commitHash: commitHash, + startVoteEpoch: 10, + endVoteEpoch: 15, + nativeTokenAmount: 1000_000000000000000000n, + }); + + assert.equal(transaction.sender.toBech32(), aliceBech32); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 1000_000000000000000000n); + assert.equal(transaction.chainID, chainID); + assert.equal(transaction.gasLimit, 50_192_500n); + assert.equal(transaction.data.toString(), expectedData); + }); + + it("should create transaction for voting", async function () { + const transaction = await controller.createTransactionForVoting(alice, alice.getNonceThenIncrement(), { + proposalNonce: 1, + vote: Vote.YES, + }); + + assert.equal(transaction.sender.toBech32(), aliceBech32); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, chainID); + assert.equal(transaction.gasLimit, 5_171_000n); + assert.equal(transaction.data.toString(), "vote@01@796573"); + }); + + it("should create transaction for closing proposal", async function () { + const transaction = await controller.createTransactionForClosingProposal(alice, alice.getNonceThenIncrement(), { + proposalNonce: 1, + }); + + assert.equal(transaction.sender.toBech32(), aliceBech32); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, chainID); + assert.equal(transaction.gasLimit, 50_074_000n); + assert.equal(transaction.data.toString(), "closeProposal@01"); + }); + + it("should create transaction for clearing ended proposals", async function () { + const expectedData = + "clearEndedProposals@0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8"; + + const transaction = await controller.createTransactionForClearingEndedProposals( + alice, + alice.getNonceThenIncrement(), + { + proposers: [ + alice.address, + Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + ], + }, + ); + + assert.equal(transaction.sender.toBech32(), aliceBech32); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, chainID); + assert.equal(transaction.gasLimit, 150_273_500n); + assert.equal(transaction.data.toString(), expectedData); + }); + + it("should create transaction for claiming accumulated fees", async function () { + const transaction = await controller.createTransactionForClaimingAccumulatedFees( + alice, + alice.getNonceThenIncrement(), + {}, + ); + + assert.equal(transaction.sender.toBech32(), aliceBech32); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, chainID); + assert.equal(transaction.gasLimit, 1_080_000n); + assert.equal(transaction.data.toString(), "claimAccumulatedFees"); + }); + + it("should create transaction for changing config", async function () { + const expectedData = + "changeConfig@31303030303030303030303030303030303030303030@3130303030303030303030303030303030303030@35303030@33303030@36303030"; + + const transaction = await controller.createTransactionForChangingConfig(alice, alice.getNonceThenIncrement(), { + proposalFee: 1000000000000000000000n, + lastProposalFee: 10000000000000000000n, + minQuorum: 5000, + minVetoThreshold: 3000, + minPassThreshold: 6000, + }); + + assert.equal(transaction.sender.toBech32(), aliceBech32); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, chainID); + assert.equal(transaction.gasLimit, 50_237_500n); + assert.equal(transaction.data.toString(), expectedData); + }); + + it("should get voting power", async function () { + const provider = new MockNetworkProvider(); + const controller = new GovernanceController({ + chainID: chainID, + networkProvider: provider, + }); + + provider.mockQueryContractOnFunction( + "viewVotingPower", + new SmartContractQueryResponse({ + returnDataParts: [Buffer.from("878678326eac900000", "hex")], + returnCode: "ok", + returnMessage: "", + function: "viewVotingPower", + }), + ); + + const votingPower = await controller.getVotingPower(alice.address); + assert.equal(votingPower, 2500_000000000000000000n); + }); + + it("should get config", async function () { + const provider = new MockNetworkProvider(); + const controller = new GovernanceController({ + chainID: chainID, + networkProvider: provider, + }); + + provider.mockQueryContractOnFunction( + "viewConfig", + new SmartContractQueryResponse({ + returnDataParts: [ + Buffer.from("1000000000000000000000"), + Buffer.from("0.2000"), + Buffer.from("0.5000"), + Buffer.from("0.3300"), + Buffer.from("1"), + ], + returnCode: "ok", + returnMessage: "", + function: "viewConfig", + }), + ); + + const config = await controller.getConfig(); + assert.equal(config.proposalFee, 1000_000000000000000000n); + assert.equal(config.minQuorum, 0.2); + assert.equal(config.minPassThreshold, 0.5); + assert.equal(config.minVetoThreshold, 0.33); + assert.equal(config.lastProposalNonce, 1); + }); + + it("should get proposal", async function () { + const provider = new MockNetworkProvider(); + const controller = new GovernanceController({ + chainID: chainID, + networkProvider: provider, + }); + + provider.mockQueryContractOnFunction( + "viewProposal", + new SmartContractQueryResponse({ + returnDataParts: b64TopicsToBytes([ + "NjXJrcXeoAAA", + "MWRiNzM0YzAzMTVmOWVjNDIyYjg4ZjY3OWNjZmUzZTAxOTdiOWQ2Nw==", + "AQ==", + "ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE=", + "NQ==", + "Nw==", + "", + "", + "", + "", + "", + "ZmFsc2U=", + "ZmFsc2U=", + ]), + returnCode: "ok", + returnMessage: "", + function: "viewProposal", + }), + ); + + const proposal = await controller.getProposal(1); + assert.equal(proposal.cost, 1000_000000000000000000n); + assert.equal(proposal.commitHash, "1db734c0315f9ec422b88f679ccfe3e0197b9d67"); + assert.equal(proposal.nonce, 1); + assert.equal(proposal.issuer.toBech32(), aliceBech32); + assert.equal(proposal.startVoteEpoch, 53); + assert.equal(proposal.endVoteEpoch, 55); + assert.equal(proposal.quorumStake, 0n); + assert.equal(proposal.numYesVotes, 0n); + assert.equal(proposal.numNoVotes, 0n); + assert.equal(proposal.numVetoVotes, 0n); + assert.equal(proposal.numAbstainVotes, 0n); + assert.equal(proposal.isClosed, false); + assert.equal(proposal.isPassed, false); + }); +}); diff --git a/src/governance/governanceController.ts b/src/governance/governanceController.ts new file mode 100644 index 000000000..c6eb02f74 --- /dev/null +++ b/src/governance/governanceController.ts @@ -0,0 +1,292 @@ +import { AddressType, AddressValue, ArgSerializer, BigUIntType, BigUIntValue, StringType } from "../abi"; +import { + Address, + BaseController, + BaseControllerInput, + IAccount, + LibraryConfig, + Transaction, + TransactionOnNetwork, + TransactionsFactoryConfig, + TransactionWatcher, +} from "../core"; +import { GOVERNANCE_CONTRACT_ADDRESS_HEX } from "../core/constants"; +import { INetworkProvider } from "../networkProviders"; +import { SmartContractController } from "../smartContracts"; +import { GovernanceTransactionsFactory } from "./governanceTransactionsFactory"; +import { GovernanceTransactionsOutcomeParser } from "./governanceTransactionsOutcomeParser"; +import { + ChangeConfigInput, + ClearEndedProposalsInput, + CloseProposalInput, + CloseProposalOutcome, + DelegatedVoteInfo, + GovernanceConfig, + NewProposalInput, + NewProposalOutcome, + ProposalInfo, + VoteOutcome, + VoteProposalInput, +} from "./resources"; + +export class GovernanceController extends BaseController { + private readonly governanceFactory: GovernanceTransactionsFactory; + private readonly parser: GovernanceTransactionsOutcomeParser; + private readonly smartContractController: SmartContractController; + private readonly governanceContract: Address; + private readonly transactionAwaiter: TransactionWatcher; + private readonly addressHrp: string; + private readonly serializer: ArgSerializer; + + constructor(options: { chainID: string; networkProvider: INetworkProvider; addressHrp?: string }) { + super(); + this.governanceFactory = new GovernanceTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: options.chainID }), + }); + this.smartContractController = new SmartContractController({ + chainID: options.chainID, + networkProvider: options.networkProvider, + }); + this.addressHrp = options.addressHrp ?? LibraryConfig.DefaultAddressHrp; + this.parser = new GovernanceTransactionsOutcomeParser({ addressHrp: this.addressHrp }); + this.governanceContract = Address.newFromHex(GOVERNANCE_CONTRACT_ADDRESS_HEX, this.addressHrp); + this.transactionAwaiter = new TransactionWatcher(options.networkProvider); + this.serializer = new ArgSerializer(); + } + + async createTransactionForNewProposal( + sender: IAccount, + nonce: bigint, + options: NewProposalInput & BaseControllerInput, + ): Promise { + const transaction = this.governanceFactory.createTransactionForNewProposal(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + this.setVersionAndOptionsForGuardian(transaction); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + parseNewProposal(transaction: TransactionOnNetwork): NewProposalOutcome[] { + return this.parser.parseNewProposal(transaction); + } + + async awaitCompletedProposeProposal(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseNewProposal(transaction); + } + + async createTransactionForVoting( + sender: IAccount, + nonce: bigint, + options: VoteProposalInput & BaseControllerInput, + ): Promise { + const transaction = this.governanceFactory.createTransactionForVoting(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + this.setVersionAndOptionsForGuardian(transaction); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + parseVote(transaction: TransactionOnNetwork): VoteOutcome[] { + return this.parser.parseVote(transaction); + } + + async awaitCompletedVote(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseVote(transaction); + } + + async createTransactionForClosingProposal( + sender: IAccount, + nonce: bigint, + options: CloseProposalInput & BaseControllerInput, + ): Promise { + const transaction = this.governanceFactory.createTransactionForClosingProposal(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + this.setVersionAndOptionsForGuardian(transaction); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + parseCloseProposal(transaction: TransactionOnNetwork): CloseProposalOutcome[] { + return this.parser.parseCloseProposal(transaction); + } + + async awaitCompletedCloseProposal(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseCloseProposal(transaction); + } + + async createTransactionForClearingEndedProposals( + sender: IAccount, + nonce: bigint, + options: ClearEndedProposalsInput & BaseControllerInput, + ): Promise { + const transaction = this.governanceFactory.createTransactionForClearingEndedProposals(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + this.setVersionAndOptionsForGuardian(transaction); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForClaimingAccumulatedFees( + sender: IAccount, + nonce: bigint, + options: BaseControllerInput, + ): Promise { + const transaction = this.governanceFactory.createTransactionForClaimingAccumulatedFees(sender.address); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + this.setVersionAndOptionsForGuardian(transaction); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async createTransactionForChangingConfig( + sender: IAccount, + nonce: bigint, + options: ChangeConfigInput & BaseControllerInput, + ): Promise { + const transaction = this.governanceFactory.createTransactionForChangingConfig(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + this.setVersionAndOptionsForGuardian(transaction); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + async getVotingPower(address: Address): Promise { + const result = await this.smartContractController.query({ + contract: this.governanceContract, + function: "viewVotingPower", + arguments: [new AddressValue(address)], + }); + + const votingPower = { type: new BigUIntType() }; + const data = this.serializer.buffersToValues(result, [votingPower]); + return BigInt(data[0].valueOf().toFixed()); + } + + async getConfig(): Promise { + const result = await this.smartContractController.query({ + contract: this.governanceContract, + function: "viewConfig", + arguments: [], + }); + + const proposalFee = BigInt(Buffer.from(result[0]).toString()); + const minQuorum = Number(Buffer.from(result[1]).toString()); + const minPassThreshold = Number(Buffer.from(result[2]).toString()); + const minVetoThreshold = Number(Buffer.from(result[3]).toString()); + const lastProposalNonce = Number(Buffer.from(result[4]).toString()); + + return { + proposalFee, + minQuorum, + minPassThreshold, + minVetoThreshold, + lastProposalNonce, + }; + } + + async getProposal(proposalNonce: number): Promise { + const result = await this.smartContractController.query({ + contract: this.governanceContract, + function: "viewProposal", + arguments: [new BigUIntValue(proposalNonce)], + }); + + const proposalCost = { type: new BigUIntType() }; + const commitHash = { type: new StringType() }; + const nonce = { type: new BigUIntType() }; + const issuer = { type: new AddressType() }; + const startVoteEpoch = { type: new BigUIntType() }; + const endVoteEpoch = { type: new BigUIntType() }; + const quorumStake = { type: new BigUIntType() }; + const numVotesYes = { type: new BigUIntType() }; + const numVotesNo = { type: new BigUIntType() }; + const numVotesVeto = { type: new BigUIntType() }; + const numVotesAbstain = { type: new BigUIntType() }; + + const data = this.serializer.buffersToValues(result.slice(0, 11), [ + proposalCost, + commitHash, + nonce, + issuer, + startVoteEpoch, + endVoteEpoch, + quorumStake, + numVotesYes, + numVotesNo, + numVotesVeto, + numVotesAbstain, + ]); + + const isClosed = Buffer.from(result[11]).toString() === "true"; + const isPassed = Buffer.from(result[12]).toString() === "true"; + + return { + cost: BigInt(data[0].valueOf().toFixed()), + commitHash: data[1].valueOf(), + nonce: Number(data[2].valueOf().toString()), + issuer: data[3].valueOf(), + startVoteEpoch: Number(data[4].valueOf().toString()), + endVoteEpoch: Number(data[5].valueOf().toString()), + quorumStake: BigInt(data[6].valueOf().toFixed()), + numYesVotes: BigInt(data[7].valueOf().toFixed()), + numNoVotes: BigInt(data[8].valueOf().toFixed()), + numVetoVotes: BigInt(data[9].valueOf().toFixed()), + numAbstainVotes: BigInt(data[10].valueOf().toFixed()), + isClosed: isClosed, + isPassed: isPassed, + }; + } + + async getDelegatedVoteInfo(): Promise { + const result = await this.smartContractController.query({ + contract: this.governanceContract, + function: "viewDelegatedVoteInfo", + arguments: [], + }); + + const usedStake = BigInt(Buffer.from(result[0]).toString()); + const usedPower = BigInt(Buffer.from(result[1]).toString()); + const totalStake = BigInt(Buffer.from(result[2]).toString()); + const totalPower = BigInt(Buffer.from(result[3]).toString()); + + return { + usedStake, + usedPower, + totalStake, + totalPower, + }; + } +} diff --git a/src/governance/governanceTransactionsFactory.spec.ts b/src/governance/governanceTransactionsFactory.spec.ts new file mode 100644 index 000000000..bdc9911fc --- /dev/null +++ b/src/governance/governanceTransactionsFactory.spec.ts @@ -0,0 +1,108 @@ +import { assert } from "chai"; +import { Address } from "../core/address"; +import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig"; +import { GovernanceTransactionsFactory } from "./governanceTransactionsFactory"; +import { Vote } from "./resources"; + +describe("test governance transactions factory", function () { + const config = new TransactionsFactoryConfig({ + chainID: "D", + }); + const factory = new GovernanceTransactionsFactory({ config }); + + const commitHash = "1db734c0315f9ec422b88f679ccfe3e0197b9d67"; + const alice = Address.newFromBech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + const governanceAddress = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqrlllsrujgla"; + + it("should create transaction for creating new proposal", function () { + const expectedData = `proposal@${Buffer.from(commitHash).toString("hex")}@0a@0f`; + + const transaction = factory.createTransactionForNewProposal(alice, { + commitHash: commitHash, + startVoteEpoch: 10, + endVoteEpoch: 15, + nativeTokenAmount: 1000_000000000000000000n, + }); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 1000_000000000000000000n); + assert.equal(transaction.chainID, config.chainID); + assert.equal(transaction.gasLimit, 50_192_500n); + assert.equal(transaction.data.toString(), expectedData); + }); + + it("should create transaction for voting", function () { + const transaction = factory.createTransactionForVoting(alice, { + proposalNonce: 1, + vote: Vote.YES, + }); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, config.chainID); + assert.equal(transaction.gasLimit, 5_171_000n); + assert.equal(transaction.data.toString(), "vote@01@796573"); + }); + + it("should create transaction for closing proposal", function () { + const transaction = factory.createTransactionForClosingProposal(alice, { + proposalNonce: 1, + }); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, config.chainID); + assert.equal(transaction.gasLimit, 50_074_000n); + assert.equal(transaction.data.toString(), "closeProposal@01"); + }); + + it("should create transaction for clearing ended proposals", function () { + const expectedData = + "clearEndedProposals@0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8"; + + const transaction = factory.createTransactionForClearingEndedProposals(alice, { + proposers: [alice, Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx")], + }); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, config.chainID); + assert.equal(transaction.gasLimit, 150_273_500n); + assert.equal(transaction.data.toString(), expectedData); + }); + + it("should create transaction for claiming accumulated fees", function () { + const transaction = factory.createTransactionForClaimingAccumulatedFees(alice); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, config.chainID); + assert.equal(transaction.gasLimit, 1_080_000n); + assert.equal(transaction.data.toString(), "claimAccumulatedFees"); + }); + + it("should create transaction for changing config", function () { + const expectedData = + "changeConfig@31303030303030303030303030303030303030303030@3130303030303030303030303030303030303030@35303030@33303030@36303030"; + + const transaction = factory.createTransactionForChangingConfig(alice, { + proposalFee: 1000000000000000000000n, + lastProposalFee: 10000000000000000000n, + minQuorum: 5000, + minVetoThreshold: 3000, + minPassThreshold: 6000, + }); + + assert.equal(transaction.sender.toBech32(), alice.toBech32()); + assert.equal(transaction.receiver.toBech32(), governanceAddress); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, config.chainID); + assert.equal(transaction.gasLimit, 50_237_500n); + assert.equal(transaction.data.toString(), expectedData); + }); +}); diff --git a/src/governance/governanceTransactionsFactory.ts b/src/governance/governanceTransactionsFactory.ts new file mode 100644 index 000000000..7eb32e2bb --- /dev/null +++ b/src/governance/governanceTransactionsFactory.ts @@ -0,0 +1,133 @@ +import { ArgSerializer, BigUIntValue, StringValue } from "../abi"; +import { Address, Transaction, TransactionsFactoryConfig } from "../core"; +import { GOVERNANCE_CONTRACT_ADDRESS_HEX } from "../core/constants"; +import { TransactionBuilder } from "../core/transactionBuilder"; +import { + ChangeConfigInput, + ClearEndedProposalsInput, + CloseProposalInput, + NewProposalInput, + VoteProposalInput, +} from "./resources"; + +interface IConfig { + chainID: string; + addressHrp: string; + minGasLimit: bigint; + gasLimitPerByte: bigint; + gasLimitForProposal: bigint; + gasLimitForVote: bigint; + gasLimitForClosingProposal: bigint; + gasLimitForClearProposals: bigint; + gasLimitForChangeConfig: bigint; + gasLimitForClaimAccumulatedFees: bigint; +} + +const EXTRA_GAS_LIMIT_FOR_VOTING = 100_000n; + +export class GovernanceTransactionsFactory { + private readonly config: IConfig; + private readonly argSerializer: ArgSerializer; + private readonly governanceContract: Address; + + constructor(options: { config: TransactionsFactoryConfig }) { + this.config = options.config; + this.argSerializer = new ArgSerializer(); + this.governanceContract = Address.newFromHex(GOVERNANCE_CONTRACT_ADDRESS_HEX, this.config.addressHrp); + } + + createTransactionForNewProposal(sender: Address, options: NewProposalInput): Transaction { + const args = [ + new StringValue(options.commitHash), + new BigUIntValue(options.startVoteEpoch), + new BigUIntValue(options.endVoteEpoch), + ]; + const dataParts = ["proposal", ...this.argSerializer.valuesToStrings(args)]; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: this.governanceContract, + dataParts: dataParts, + gasLimit: this.config.gasLimitForProposal, + addDataMovementGas: true, + amount: options.nativeTokenAmount, + }).build(); + } + + createTransactionForVoting(sender: Address, options: VoteProposalInput): Transaction { + const args = [new BigUIntValue(options.proposalNonce), new StringValue(options.vote.valueOf())]; + const dataParts = ["vote", ...this.argSerializer.valuesToStrings(args)]; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: this.governanceContract, + dataParts: dataParts, + gasLimit: this.config.gasLimitForVote + EXTRA_GAS_LIMIT_FOR_VOTING, + addDataMovementGas: true, + }).build(); + } + + createTransactionForClosingProposal(sender: Address, options: CloseProposalInput): Transaction { + const args = [new BigUIntValue(options.proposalNonce)]; + const dataParts = ["closeProposal", ...this.argSerializer.valuesToStrings(args)]; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: this.governanceContract, + dataParts: dataParts, + gasLimit: this.config.gasLimitForClosingProposal, + addDataMovementGas: true, + }).build(); + } + + createTransactionForClearingEndedProposals(sender: Address, options: ClearEndedProposalsInput): Transaction { + const dataParts = ["clearEndedProposals", ...options.proposers.map((address) => address.toHex())]; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: this.governanceContract, + dataParts: dataParts, + gasLimit: + this.config.gasLimitForClearProposals + + BigInt(options.proposers.length) * this.config.gasLimitForClearProposals, + addDataMovementGas: true, + }).build(); + } + + createTransactionForClaimingAccumulatedFees(sender: Address): Transaction { + const dataParts = ["claimAccumulatedFees"]; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: this.governanceContract, + dataParts: dataParts, + gasLimit: this.config.gasLimitForClaimAccumulatedFees, + addDataMovementGas: true, + }).build(); + } + + createTransactionForChangingConfig(sender: Address, options: ChangeConfigInput): Transaction { + const args = [ + new StringValue(options.proposalFee.toString()), + new StringValue(options.lastProposalFee.toString()), + new StringValue(options.minQuorum.toString()), + new StringValue(options.minVetoThreshold.toString()), + new StringValue(options.minPassThreshold.toString()), + ]; + const dataParts = ["changeConfig", ...this.argSerializer.valuesToStrings(args)]; + + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: this.governanceContract, + dataParts: dataParts, + gasLimit: this.config.gasLimitForChangeConfig, + addDataMovementGas: true, + }).build(); + } +} diff --git a/src/governance/governanceTransactionsOutcomeParser.spec.ts b/src/governance/governanceTransactionsOutcomeParser.spec.ts new file mode 100644 index 000000000..de29b6c06 --- /dev/null +++ b/src/governance/governanceTransactionsOutcomeParser.spec.ts @@ -0,0 +1,94 @@ +import { assert } from "chai"; +import { TransactionEvent, TransactionLogs, TransactionOnNetwork } from "../core"; +import { Address } from "../core/address"; +import { b64TopicsToBytes } from "../testutils"; +import { GovernanceTransactionsOutcomeParser } from "./governanceTransactionsOutcomeParser"; + +describe("test multisig transactions outcome parser", function () { + const parser = new GovernanceTransactionsOutcomeParser({}); + + it("should parse transaction for creating new proposal", function () { + const commitHash = "1db734c0315f9ec422b88f679ccfe3e0197b9d67"; + + const proposalEvent = new TransactionEvent({ + address: Address.empty(), + identifier: "proposal", + topics: [new Uint8Array([0x01]), Buffer.from(commitHash), Buffer.from("5"), Buffer.from("7")], + }); + const logs = new TransactionLogs({ events: [proposalEvent] }); + + const transaction = new TransactionOnNetwork({ logs: logs }); + + const outcome = parser.parseNewProposal(transaction); + assert.equal(outcome.length, 1); + assert.equal(outcome[0].proposalNonce, 1); + assert.equal(outcome[0].commitHash, commitHash); + assert.equal(outcome[0].startVoteEpoch, 53); + assert.equal(outcome[0].endVoteEpoch, 55); + }); + + it("should parse transaction for voting", function () { + const encodedTopics = ["AQ==", "eWVz", "BlpNol0wFsAAAA==", "BlpNol0wFsAAAA=="]; + + const voteEvent = new TransactionEvent({ + address: Address.empty(), + identifier: "vote", + topics: b64TopicsToBytes(encodedTopics), + }); + const logs = new TransactionLogs({ events: [voteEvent] }); + + const transaction = new TransactionOnNetwork({ logs: logs }); + + const outcome = parser.parseVote(transaction); + assert.equal(outcome.length, 1); + assert.equal(outcome[0].proposalNonce, 1); + assert.equal(outcome[0].vote, "yes"); + assert.equal(outcome[0].totalStake, 30000_000000000000000000n); + assert.equal(outcome[0].votingPower, 30000_000000000000000000n); + }); + + it("should parse transaction for delegating vote", function () { + const encodedTopics = [ + "AQ==", + "YWJzdGFpbg==", + "a3Qc0P1f8raaWzOVkcJbHHxHOx2+LI6S8CM9aV+W6KY=", + "Ah4Z4Mm6skAAAA==", + "Ah4Z4Mm6skAAAA==", + ]; + + const voteEvent = new TransactionEvent({ + address: Address.empty(), + identifier: "delegateVote", + topics: b64TopicsToBytes(encodedTopics), + }); + const logs = new TransactionLogs({ events: [voteEvent] }); + + const transaction = new TransactionOnNetwork({ logs: logs }); + + const outcome = parser.parseDelegateVote(transaction); + assert.equal(outcome.length, 1); + assert.equal(outcome[0].proposalNonce, 1); + assert.equal(outcome[0].vote, "abstain"); + assert.equal(outcome[0].voter.toBech32(), "erd1dd6pe58atletdxjmxw2ersjmr37ywwcahckgayhsyv7kjhukaznqx2mzqf"); + assert.equal(outcome[0].userStake, 10000_000000000000000000n); + assert.equal(outcome[0].votingPower, 10000_000000000000000000n); + }); + + it("should parse transaction for closing proposal", function () { + const encodedTopics = ["ZDVkMjRhYTY1ZWY5OWM3NDcxMjkxMmZkOGJiMmE1MDVjY2RmMDYyYw==", "dHJ1ZQ=="]; + + const voteEvent = new TransactionEvent({ + address: Address.empty(), + identifier: "closeProposal", + topics: b64TopicsToBytes(encodedTopics), + }); + const logs = new TransactionLogs({ events: [voteEvent] }); + + const transaction = new TransactionOnNetwork({ logs: logs }); + + const outcome = parser.parseCloseProposal(transaction); + assert.equal(outcome.length, 1); + assert.equal(outcome[0].commitHash, "d5d24aa65ef99c74712912fd8bb2a505ccdf062c"); + assert.equal(outcome[0].passed, true); + }); +}); diff --git a/src/governance/governanceTransactionsOutcomeParser.ts b/src/governance/governanceTransactionsOutcomeParser.ts new file mode 100644 index 000000000..0bea641dd --- /dev/null +++ b/src/governance/governanceTransactionsOutcomeParser.ts @@ -0,0 +1,136 @@ +import { AddressType, ArgSerializer, BigUIntType, StringType } from "../abi"; +import { Address, ErrParseTransactionOutcome, LibraryConfig, TransactionEvent, TransactionOnNetwork } from "../core"; +import { findEventsByIdentifier } from "../transactionsOutcomeParsers"; +import { CloseProposalOutcome, DelegateVoteOutcome, NewProposalOutcome, VoteOutcome } from "./resources"; + +export class GovernanceTransactionsOutcomeParser { + private addressHrp: string; + private serializer: ArgSerializer; + + constructor(options: { addressHrp?: string }) { + this.addressHrp = options.addressHrp ?? LibraryConfig.DefaultAddressHrp; + this.serializer = new ArgSerializer(); + } + + parseNewProposal(transactionOnNetwork: TransactionOnNetwork): NewProposalOutcome[] { + this.ensureNoError(transactionOnNetwork.logs.events); + + const events = findEventsByIdentifier(transactionOnNetwork, "proposal"); + + const proposalNonce = { type: new BigUIntType() }; + const commitHash = { type: new StringType() }; + const startVoteEpoch = { type: new BigUIntType() }; + const endVoteEpoch = { type: new BigUIntType() }; + + const outcome: NewProposalOutcome[] = []; + for (const event of events) { + const data = this.serializer.buffersToValues( + event.topics.map((topic) => Buffer.from(topic)), + [proposalNonce, commitHash, startVoteEpoch, endVoteEpoch], + ); + + outcome.push({ + proposalNonce: data[0].valueOf(), + commitHash: data[1].valueOf(), + startVoteEpoch: data[2].valueOf(), + endVoteEpoch: data[3].valueOf(), + }); + } + + return outcome; + } + + parseVote(transactionOnNetwork: TransactionOnNetwork): VoteOutcome[] { + this.ensureNoError(transactionOnNetwork.logs.events); + + const events = findEventsByIdentifier(transactionOnNetwork, "vote"); + + const proposalToVote = { type: new BigUIntType() }; + const vote = { type: new StringType() }; + const totalStake = { type: new BigUIntType() }; + const votingPower = { type: new BigUIntType() }; + + const outcome: VoteOutcome[] = []; + for (const event of events) { + const data = this.serializer.buffersToValues( + event.topics.map((topic) => Buffer.from(topic)), + [proposalToVote, vote, totalStake, votingPower], + ); + + outcome.push({ + proposalNonce: Number(data[0].toString()), + vote: data[1].valueOf(), + totalStake: BigInt(data[2].valueOf().toFixed()), + votingPower: BigInt(data[3].valueOf().toFixed()), + }); + } + + return outcome; + } + + parseDelegateVote(transactionOnNetwork: TransactionOnNetwork): DelegateVoteOutcome[] { + this.ensureNoError(transactionOnNetwork.logs.events); + + const events = findEventsByIdentifier(transactionOnNetwork, "delegateVote"); + + const proposalToVote = { type: new BigUIntType() }; + const vote = { type: new StringType() }; + const voter = { type: new AddressType() }; + const userStake = { type: new BigUIntType() }; + const votingPower = { type: new BigUIntType() }; + + const outcome: DelegateVoteOutcome[] = []; + for (const event of events) { + const data = this.serializer.buffersToValues( + event.topics.map((topic) => Buffer.from(topic)), + [proposalToVote, vote, voter, userStake, votingPower], + ); + + outcome.push({ + proposalNonce: Number(data[0].toString()), + vote: data[1].valueOf(), + voter: new Address(data[2].valueOf().getPublicKey(), this.addressHrp), + userStake: BigInt(data[3].valueOf().toFixed()), + votingPower: BigInt(data[4].valueOf().toFixed()), + }); + } + + return outcome; + } + + parseCloseProposal(transactionOnNetwork: TransactionOnNetwork): CloseProposalOutcome[] { + this.ensureNoError(transactionOnNetwork.logs.events); + + const events = findEventsByIdentifier(transactionOnNetwork, "closeProposal"); + + const outcome: CloseProposalOutcome[] = []; + for (const event of events) { + const commitHash = Buffer.from(event.topics[0]).toString(); + const passed = Buffer.from(event.topics[1]).toString() === "true"; + + outcome.push({ + commitHash: commitHash, + passed: passed, + }); + } + + return outcome; + } + + private ensureNoError(transactionEvents: TransactionEvent[]) { + for (const event of transactionEvents) { + if (event.identifier == "signalError") { + const data = Buffer.from(event.additionalData[0]?.toString().slice(1)).toString() || ""; + const message = this.decodeTopicAsString(event.topics[1]); + + throw new ErrParseTransactionOutcome( + `encountered signalError: ${message} (${Buffer.from(data, "hex").toString()})`, + ); + } + } + } + + private decodeTopicAsString(topic: Uint8Array): string { + return Buffer.from(topic).toString(); + } +} diff --git a/src/governance/index.ts b/src/governance/index.ts new file mode 100644 index 000000000..8fd19e1ab --- /dev/null +++ b/src/governance/index.ts @@ -0,0 +1,4 @@ +export * from "./governanceController"; +export * from "./governanceTransactionsFactory"; +export * from "./governanceTransactionsOutcomeParser"; +export * from "./resources"; diff --git a/src/governance/resources.ts b/src/governance/resources.ts new file mode 100644 index 000000000..34c4440d1 --- /dev/null +++ b/src/governance/resources.ts @@ -0,0 +1,94 @@ +import { Address } from "../core"; + +export type NewProposalInput = { + commitHash: string; + startVoteEpoch: number; + endVoteEpoch: number; + nativeTokenAmount: bigint; +}; + +export type VoteProposalInput = { + proposalNonce: number; + vote: Vote; +}; + +export enum Vote { + YES = "yes", + NO = "no", + ABSTAIN = "abstain", + VETO = "veto", +} + +export type CloseProposalInput = { + proposalNonce: number; +}; + +export type ClearEndedProposalsInput = { + proposers: Address[]; +}; + +export type ChangeConfigInput = { + proposalFee: bigint; + lastProposalFee: bigint; + minQuorum: number; + minVetoThreshold: number; + minPassThreshold: number; +}; + +export type NewProposalOutcome = { + proposalNonce: number; + commitHash: string; + startVoteEpoch: number; + endVoteEpoch: number; +}; + +export type VoteOutcome = { + proposalNonce: number; + vote: string; + totalStake: bigint; + votingPower: bigint; +}; + +export type DelegateVoteOutcome = { + proposalNonce: number; + vote: string; + voter: Address; + userStake: bigint; + votingPower: bigint; +}; + +export type CloseProposalOutcome = { + commitHash: string; + passed: boolean; +}; + +export type GovernanceConfig = { + proposalFee: bigint; + minQuorum: number; + minPassThreshold: number; + minVetoThreshold: number; + lastProposalNonce: number; +}; + +export type ProposalInfo = { + cost: bigint; + commitHash: string; + nonce: number; + issuer: Address; + startVoteEpoch: number; + endVoteEpoch: number; + quorumStake: bigint; + numYesVotes: bigint; + numNoVotes: bigint; + numVetoVotes: bigint; + numAbstainVotes: bigint; + isClosed: boolean; + isPassed: boolean; +}; + +export type DelegatedVoteInfo = { + usedStake: bigint; + usedPower: bigint; + totalStake: bigint; + totalPower: bigint; +}; diff --git a/src/index.ts b/src/index.ts index b9e1ef27e..74f3941d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,8 @@ export * from "./accounts"; export * from "./core"; export * from "./delegation"; export * from "./entrypoints"; +export * from "./governance"; +export * from "./multisig"; export * from "./networkProviders"; export * from "./smartContracts"; export * from "./tokenManagement"; diff --git a/src/multisig/index.ts b/src/multisig/index.ts new file mode 100644 index 000000000..7af23fc96 --- /dev/null +++ b/src/multisig/index.ts @@ -0,0 +1,3 @@ +export * from "./multisigTransactionsFactory"; +export * from "./proposeTransferExecuteContractInput"; +export * from "./resources"; diff --git a/src/multisig/multisigController.spec.ts b/src/multisig/multisigController.spec.ts new file mode 100644 index 000000000..addcb1b6f --- /dev/null +++ b/src/multisig/multisigController.spec.ts @@ -0,0 +1,619 @@ +import { assert } from "chai"; +import { Address, CodeMetadata, SmartContractQueryResponse } from "../core"; +import { loadAbiRegistry, MockNetworkProvider } from "../testutils"; +import { MultisigController } from "./multisigController"; +import * as resources from "./resources"; + +describe("test multisig controller query methods", () => { + const mockMultisigAddress: string = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6"; + const mockBoardMemberAddress = "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"; + const mockProposerAddress = "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"; + let networkProvider = new MockNetworkProvider(); + let controller: MultisigController; + + beforeEach(async function () { + networkProvider = new MockNetworkProvider(); + controller = new MultisigController({ + chainID: "D", + networkProvider: networkProvider, + abi: await loadAbiRegistry("src/testdata/multisig-full.abi.json"), + }); + }); + + it("getQuorum returns the quorum value", async function () { + networkProvider.mockQueryContractOnFunction( + "getQuorum", + new SmartContractQueryResponse({ + function: "getQuorum", + returnDataParts: [Buffer.from("03", "hex")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getQuorum({ mutisigAddress: mockMultisigAddress }); + + assert.equal(result, 3); + }); + + it("getNumBoardMembers returns the number of board members", async function () { + networkProvider.mockQueryContractOnFunction( + "getNumBoardMembers", + new SmartContractQueryResponse({ + function: "getNumBoardMembers", + returnDataParts: [Buffer.from("02", "hex")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + const result = await controller.getNumBoardMembers({ mutisigAddress: mockMultisigAddress }); + + assert.equal(result, 2); + }); + + it("queries and returns the number of groups", async function () { + networkProvider.mockQueryContractOnFunction( + "getNumGroups", + new SmartContractQueryResponse({ + function: "getNumGroups", + returnDataParts: [Buffer.from("05", "hex")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getNumGroups({ mutisigAddress: mockMultisigAddress }); + + assert.equal(result, 5); + }); + + it("getNumProposers returns the number of proposers", async function () { + networkProvider.mockQueryContractOnFunction( + "getNumProposers", + new SmartContractQueryResponse({ + function: "getNumProposers", + returnDataParts: [Buffer.from("04", "hex")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getNumProposers({ mutisigAddress: mockMultisigAddress }); + + assert.equal(result, 4); + }); + + it("getActionGroup returns the action group ID", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionGroup", + new SmartContractQueryResponse({ + function: "getActionGroup", + returnDataParts: [Buffer.from("02", "hex")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionGroup({ + mutisigAddress: mockMultisigAddress, + groupId: 5, + }); + assert.equal(result.length, 1); + assert.equal(result[0], 2); + }); + + it("getLastGroupActionId returns the last group action ID", async function () { + networkProvider.mockQueryContractOnFunction( + "getLastGroupActionId", + new SmartContractQueryResponse({ + function: "getLastGroupActionId", + returnDataParts: [Buffer.from("07", "hex")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getLastGroupActionId({ + mutisigAddress: mockMultisigAddress, + }); + + assert.equal(result, 7); + }); + + it("getActionLastIndex returns the last action ID", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionLastIndex", + new SmartContractQueryResponse({ + function: "getActionLastIndex", + returnDataParts: [Buffer.from("42", "hex")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionLastIndex({ + mutisigAddress: mockMultisigAddress, + }); + + assert.equal(result, 0x42); + }); + + it("hasSignedAction returns whether user has signed action", async function () { + networkProvider.mockQueryContractOnFunction( + "signed", + new SmartContractQueryResponse({ + function: "signed", + returnDataParts: [Buffer.from("01", "hex")], // 1 = true + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.hasSignedAction({ + mutisigAddress: mockMultisigAddress, + userAddress: mockBoardMemberAddress, + actionId: 42, + }); + + assert.isTrue(result); + + it("returns false when user has not signed", async function () { + networkProvider.mockQueryContractOnFunction( + "signed", + new SmartContractQueryResponse({ + function: "signed", + returnDataParts: [Buffer.from("00", "hex")], // 0 = false + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.hasSignedAction({ + mutisigAddress: mockMultisigAddress, + userAddress: mockProposerAddress, + actionId: 42, + }); + + assert.isFalse(result); + }); + }); + + it("quorumReached returns false when quorum reached", async function () { + networkProvider.mockQueryContractOnFunction( + "quorumReached", + new SmartContractQueryResponse({ + function: "quorumReached", + returnDataParts: [Buffer.from("01", "hex")], // 1 = true + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.quorumReached({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + assert.isTrue(result); + + it("quorumReached returns false when quorum not reached", async function () { + networkProvider.mockQueryContractOnFunction( + "quorumReached", + new SmartContractQueryResponse({ + function: "quorumReached", + returnDataParts: [Buffer.from("00", "hex")], // 0 = false + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.quorumReached({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + assert.isFalse(result); + }); + }); + + it("getUserRole returns the user role", async function () { + networkProvider.mockQueryContractOnFunction( + "userRole", + new SmartContractQueryResponse({ + function: "userRole", + returnDataParts: [Buffer.from("01", "hex")], // 1 = PROPOSER, for example + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getUserRole({ + mutisigAddress: mockMultisigAddress, + userAddress: mockBoardMemberAddress, + }); + + assert.equal(result, "Proposer"); // 1 could be proposer member role + }); + + it("getAllBoardMembers returns all board members as address array", async function () { + networkProvider.mockQueryContractOnFunction( + "getAllBoardMembers", + new SmartContractQueryResponse({ + function: "getAllBoardMembers", + returnDataParts: [ + Buffer.from("ATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE=", "base64"), + Buffer.from("gEnWOeWmmA0c0jkqvM5BApzadKFWNSOiAvCWQcwmGPg=", "base64"), + ], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getAllBoardMembers({ + mutisigAddress: mockMultisigAddress, + }); + + assert.equal(result.length, 2); + assert.equal(result[0], "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"); + assert.equal(result[1], "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + }); + + it("getAllProposers returns all proposers as address array", async function () { + const address1 = Buffer.from(Address.newFromBech32(mockBoardMemberAddress).toHex(), "hex"); + const address2 = Buffer.from(Address.newFromBech32(mockProposerAddress).toHex(), "hex"); + + networkProvider.mockQueryContractOnFunction( + "getAllProposers", + new SmartContractQueryResponse({ + function: "getAllProposers", + returnDataParts: [address1, address2], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getAllProposers({ + mutisigAddress: mockMultisigAddress, + }); + + assert.equal(result.length, 2); + assert.equal(result[0], mockBoardMemberAddress); + assert.equal(result[1], mockProposerAddress); + }); + + it("getActionData returns the action data as SendTransferExecuteEgld", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionData", + new SmartContractQueryResponse({ + function: "getActionData", + returnDataParts: [ + Buffer.from( + "0500000000000000000500d006f73c4221216fa679bc559005584c4f1160e569e1000000012a0000000003616464000000010000000107", + "hex", + ), + ], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionData({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + const mappedRes = result as resources.SendTransferExecuteEgld; + assert.equal(mappedRes.receiver.toBech32(), "erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60"); + assert.equal(mappedRes.functionName, "add"); + assert.equal(mappedRes.amount, 42n); + }); + + it("getPendingActionFullInfo returns all the actions pending", async function () { + networkProvider.mockQueryContractOnFunction( + "getPendingActionFullInfo", + new SmartContractQueryResponse({ + function: "getPendingActionFullInfo", + returnDataParts: [ + Buffer.from( + "AAAAAQAAAAAFgEnWOeWmmA0c0jkqvM5BApzadKFWNSOiAvCWQcwmGPgAAAAIDeC2s6dkAAABAAAAAAF9eEAAAAAAAAAAAAAAAAEBOUcu/2iGdxqYLzCD2l1CHyTCkYHmOIgijcgcpg1p4Q==", + "base64", + ), + Buffer.from( + "AAAAAgAAAAAHAAAAAAAAAAAFAHjSljKssVmYAD9hXQpRJhNT2AQdPhMAAAAIDeC2s6dkAAABAAAAAAOThwAAAAABCgAAAAIAAAABDQAAAAENAAAAAQE5Ry7/aIZ3GpgvMIPaXUIfJMKRgeY4iCKNyBymDWnh", + "base64", + ), + Buffer.from( + "AAAAAwAAAAAGAAAAAAAAAAAFAEm/+WO9+j6gJxM2IJXfMuPXCOrM/FcAAAABAAAADEFMSUNFLTU2MjdmMQAAAAAAAAAAAAAAAAEAAAAAAExLQAAAABQ2NDY5NzM3NDcyNjk2Mjc1NzQ2NQAAAAAAAAABATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE=", + "base64", + ), + Buffer.from( + "AAAABAAAAAAGAAAAAAAAAAAFAEm/+WO9+j6gJxM2IJXfMuPXCOrM/FcAAAABAAAADEFMSUNFLTU2MjdmMQAAAAAAAAAAAAAAAQoBAAAAAABMS0AAAAAUNjQ2OTczNzQ3MjY5NjI3NTc0NjUAAAAAAAAAAQE5Ry7/aIZ3GpgvMIPaXUIfJMKRgeY4iCKNyBymDWnh", + "base64", + ), + Buffer.from( + "AAAABgAAAAACgEnWOeWmmA0c0jkqvM5BApzadKFWNSOiAvCWQcwmGPgAAAABATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE=", + "base64", + ), + Buffer.from( + "AAAABwAAAAAIAAAAB7GivC7FAAAAAAAAAAAAAAUAhw0EEs7ehxhTocLUinVDwHPrOflp4QUAAAAAAQAAAAEHAAAAAQE5Ry7/aIZ3GpgvMIPaXUIfJMKRgeY4iCKNyBymDWnh", + "base64", + ), + Buffer.from( + "AAAACAAAAAAJAAAAAAAAAAAFAH4lzm3rrHSNhrXTkxIKsesCpG1YFnkAAAAHsaK8LsUAAAAAAAAAAAAABQBqvRw6N5TaAWArhVrAPngh5mOOyBZ5BQAAAAAAAAAAAQE5Ry7/aIZ3GpgvMIPaXUIfJMKRgeY4iCKNyBymDWnh", + "base64", + ), + Buffer.from("AAAACQAAAAAEAAAAAgAAAAEBOUcu/2iGdxqYLzCD2l1CHyTCkYHmOIgijcgcpg1p4Q==", "base64"), + Buffer.from( + "AAAACgAAAAADgEnWOeWmmA0c0jkqvM5BApzadKFWNSOiAvCWQcwmGPgAAAABATlHLv9ohncamC8wg9pdQh8kwpGB5jiIIo3IHKYNaeE=", + "base64", + ), + ], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getPendingActionFullInfo({ + mutisigAddress: mockMultisigAddress, + }); + + assert.equal(result.length, 9); + }); + + // TODO: I'll do this on a future branch + it.skip("getActionData returns the action data as SendAsyncCall", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionData", + new SmartContractQueryResponse({ + function: "getActionData", + returnDataParts: [ + Buffer.from( + "BwAAAAAAAAAABQB40pYyrLFZmAA/YV0KUSYTU9gEHT4TAAAACA3gtrOnZAAAAQAAAAADk4cAAAAAAQoAAAACAAAAAQ0AAAABDQ==", + "base64", + ), + ], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionData({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + const mappedRes = result as resources.SendAsyncCall; + assert.equal(mappedRes.receiver.toBech32(), "erd1qqqqqqqqqqqqqpgq0rffvv4vk9vesqplv9ws55fxzdfaspqa8cfszy2hms"); + assert.equal(mappedRes.funcionName, "add"); + assert.equal(mappedRes.amount, 0n); + }); + + it("getActionData returns the action data as SendTransferExecuteEsdt", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionData", + new SmartContractQueryResponse({ + function: "getActionData", + returnDataParts: [ + Buffer.from( + "BgAAAAAAAAAABQBJv/ljvfo+oCcTNiCV3zLj1wjqzPxXAAAAAQAAAAxBTElDRS01NjI3ZjEAAAAAAAAAAAAAAAEKAQAAAAAATEtAAAAAFDY0Njk3Mzc0NzI2OTYyNzU3NDY1AAAAAA==", + "base64", + ), + ], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionData({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + const mappedRes = result as resources.SendTransferExecuteEsdt; + + assert.equal(mappedRes.receiver.toBech32(), "erd1qqqqqqqqqqqqqpgqfxlljcaalgl2qfcnxcsftheju0ts36kvl3ts3qkewe"); + assert.equal(mappedRes.funcionName, "distribute"); + }); + + it("getActionData returns the action data as AddBoardMember", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionData", + new SmartContractQueryResponse({ + function: "getActionData", + returnDataParts: [Buffer.from("AYBJ1jnlppgNHNI5KrzOQQKc2nShVjUjogLwlkHMJhj4", "base64")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionData({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + const mappedRes = result as resources.AddBoardMember; + + assert.equal(mappedRes.address.toBech32(), "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + }); + + it("getActionData returns the action data as AddProposer", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionData", + new SmartContractQueryResponse({ + function: "getActionData", + returnDataParts: [Buffer.from("AYBJ1jnlppgNHNI5KrzOQQKc2nShVjUjogLwlkHMJhj4", "base64")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionData({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + const mappedRes = result as resources.AddProposer; + + assert.equal(mappedRes.address.toBech32(), "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + }); + + it("getActionData returns the action data as SCDeployFromSource", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionData", + new SmartContractQueryResponse({ + function: "getActionData", + returnDataParts: [ + Buffer.from( + "CAAAAAexorwuxQAAAAAAAAAAAAAFAIcNBBLO3ocYU6HC1Ip1Q8Bz6zn5aeEFAAAAAAEAAAABBw==", + "base64", + ), + ], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionData({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + const mappedRes = result as resources.SCDeployFromSource; + + assert.equal( + mappedRes.sourceContract.toBech32(), + "erd1qqqqqqqqqqqqqpgqsuxsgykwm6r3s5apct2g5a2rcpe7kw0ed8ssf6h9f6", + ); + assert.equal(mappedRes.amount.toString(), "50000000000000000"); + assert.deepEqual(mappedRes.codeMetadata, new CodeMetadata(true, true, false)); + }); + + it("getActionData returns the action data as SCUpgradeFromSource", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionData", + new SmartContractQueryResponse({ + function: "getActionData", + returnDataParts: [ + Buffer.from( + "CQAAAAAAAAAABQB+Jc5t66x0jYa105MSCrHrAqRtWBZ5AAAAB7GivC7FAAAAAAAAAAAAAAUAar0cOjeU2gFgK4VawD54IeZjjsgWeQUAAAAAAA==", + "base64", + ), + ], + returnCode: "ok", + returnMessage: "ok", + }), + ); + const amount = BigInt(50000000000000000); // 0.05 EGLD + const metadata = new CodeMetadata(true, true, false); + const sourceContract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqd273cw3hjndqzcpts4dvq0ncy8nx8rkgzeusnefvaq"); + + const result = await controller.getActionData({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + const mappedRes = result as resources.SCUpgradeFromSource; + + assert.equal(mappedRes.sourceContract.toBech32(), sourceContract.toBech32()); + assert.equal(mappedRes.amount, amount); + assert.deepEqual(mappedRes.codeMetadata, metadata); + }); + + it("getActionData returns the action data as ChangeQuorum", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionData", + new SmartContractQueryResponse({ + function: "getActionData", + returnDataParts: [Buffer.from("BAAAAAI=", "base64")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + const result = await controller.getActionData({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + const mappedRes = result as resources.ChangeQuorum; + + assert.equal(mappedRes.quorum, 2); + }); + + it("getActionData returns the action data as RemoveUser", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionData", + new SmartContractQueryResponse({ + function: "getActionData", + returnDataParts: [Buffer.from("A4BJ1jnlppgNHNI5KrzOQQKc2nShVjUjogLwlkHMJhj4", "base64")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + const result = await controller.getActionData({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + const mappedRes = result as resources.RemoveUser; + + assert.equal(mappedRes.address.toBech32(), "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + }); + + it("getActionSigners returns the action signers as address array", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionSigners", + new SmartContractQueryResponse({ + function: "getActionSigners", + returnDataParts: [ + Buffer.from( + "8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba", + "hex", + ), + ], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionSigners({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + assert.equal(result.length, 2); + assert.equal(result[0], mockBoardMemberAddress); + assert.equal(result[1], mockProposerAddress); + }); + + it("getActionSignerCount returns the number of signers that signed an action", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionSignerCount", + new SmartContractQueryResponse({ + function: "getActionSignerCount", + returnDataParts: [Buffer.from("04", "hex")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionSignerCount({ mutisigAddress: mockMultisigAddress, actionId: 42 }); + + assert.equal(result, 4); + }); + + it("getActionValidSignerCount returns the number of signers that signed an action and are still boardMembers", async function () { + networkProvider.mockQueryContractOnFunction( + "getActionValidSignerCount", + new SmartContractQueryResponse({ + function: "getActionValidSignerCount", + returnDataParts: [Buffer.from("04", "hex")], + returnCode: "ok", + returnMessage: "ok", + }), + ); + + const result = await controller.getActionValidSignerCount({ + mutisigAddress: mockMultisigAddress, + actionId: 42, + }); + + assert.equal(result, 4); + }); +}); diff --git a/src/multisig/multisigController.ts b/src/multisig/multisigController.ts new file mode 100644 index 000000000..a2efb38c5 --- /dev/null +++ b/src/multisig/multisigController.ts @@ -0,0 +1,725 @@ +import BigNumber from "bignumber.js"; +import { Abi } from "../abi"; +import { + Address, + BaseController, + BaseControllerInput, + IAccount, + Transaction, + TransactionOnNetwork, + TransactionsFactoryConfig, + TransactionWatcher, +} from "../core"; +import { INetworkProvider } from "../networkProviders/interface"; +import { SmartContractController, SmartContractDeployOutcome } from "../smartContracts"; +import { MultisigTransactionsFactory } from "./multisigTransactionsFactory"; +import { MultisigTransactionsOutcomeParser } from "./multisigTransactionsOutcomeParser"; +import * as resources from "./resources"; + +export class MultisigController extends BaseController { + private transactionAwaiter: TransactionWatcher; + private multisigFactory: MultisigTransactionsFactory; + private multisigParser: MultisigTransactionsOutcomeParser; + private smartContractController: SmartContractController; + + constructor(options: { chainID: string; networkProvider: INetworkProvider; abi: Abi }) { + super(); + this.transactionAwaiter = new TransactionWatcher(options.networkProvider); + this.multisigFactory = new MultisigTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: options.chainID }), + abi: options.abi, + }); + this.multisigParser = new MultisigTransactionsOutcomeParser({ abi: options.abi }); + this.smartContractController = new SmartContractController({ + chainID: options.chainID, + networkProvider: options.networkProvider, + abi: options.abi, + }); + } + + /** + * Creates a transaction for deploying a new multisig contract + */ + async createTransactionForDeploy( + sender: IAccount, + nonce: bigint, + options: resources.DeployMultisigContractInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForDeploy(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + parseDeploy(transactionOnNetwork: TransactionOnNetwork): SmartContractDeployOutcome { + return this.multisigParser.parseDeploy(transactionOnNetwork); + } + + async awaitCompletedDeploy(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.parseDeploy(transaction); + } + + /** + * Gets quorum for specific multisig + */ + async getQuorum(options: { mutisigAddress: string }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getQuorum", + arguments: [], + }); + return Number(value.toString()); + } + + /** + * Gets number of board members for specific multisig + */ + async getNumBoardMembers(options: { mutisigAddress: string }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getNumBoardMembers", + arguments: [], + }); + return Number(value.toString()); + } + + /** + * Gets number of groups for specific multisig + */ + async getNumGroups(options: { mutisigAddress: string }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getNumGroups", + arguments: [], + }); + + return Number(value.toString()); + } + + /** + * Gets number of proposers for specific multisig + */ + async getNumProposers(options: { mutisigAddress: string }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getNumProposers", + arguments: [], + }); + + return Number(value.toString()); + } + + /** + * Gets action group for specific multisig + */ + async getActionGroup(options: { mutisigAddress: string; groupId: number }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionGroup", + arguments: [options.groupId], + }); + + return value.map((n: BigNumber) => Number(n.toString())); + } + + /** + * Gets last group action id specific multisig + */ + async getLastGroupActionId(options: { mutisigAddress: string }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getLastGroupActionId", + arguments: [], + }); + + return Number(value.toString()); + } + + /** + * Gets last action index specific multisig + */ + async getActionLastIndex(options: { mutisigAddress: string }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionLastIndex", + arguments: [], + }); + + return Number(value.toString()); + } + + /** + * Returns `true` (`1`) if the user has signed the action. + * Does not check whether or not the user is still a board member and the signature valid. + */ + async hasSignedAction(options: { + mutisigAddress: string; + userAddress: string; + actionId: number; + }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "signed", + arguments: [Address.newFromBech32(options.userAddress), options.actionId], + }); + + return value; + } + + /** + * Returns `true` (`1`) if `getActionValidSignerCount >= getQuorum`. + */ + async quorumReached(options: { mutisigAddress: string; actionId: number }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "quorumReached", + arguments: [options.actionId], + }); + + return value; + } + + /** + * Lists all users that can sign actions. + */ + async getAllBoardMembers(options: { mutisigAddress: string }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getAllBoardMembers", + arguments: [], + }); + + return value.map((address: Address) => address?.toBech32()); + } + + /** + * Lists all proposers that are not board members. + */ + async getAllProposers(options: { mutisigAddress: string }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getAllProposers", + arguments: [], + }); + + return value.map((address: Address) => address?.toBech32()); + } + /** + * "Indicates user rights.", + * `0` = no rights,", + * `1` = can propose, but not sign, + * `2` = can propose and sign. + */ + async getUserRole(options: { mutisigAddress: string; userAddress: string }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "userRole", + arguments: [Address.newFromBech32(options.userAddress)], + }); + const userRole = value.valueOf().name as keyof typeof resources.UserRoleEnum; + return resources.UserRoleEnum[userRole]; + } + + /** + * Serialized action data of an action with index. + */ + async getActionData(options: { mutisigAddress: string; actionId: number }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionData", + arguments: [options.actionId], + }); + const result = this.mapResponseToAction(value.valueOf()); + return result; + } + + /** + * Gets all pending actions. + */ + async getPendingActionFullInfo(options: { mutisigAddress: string }): Promise { + const [actions] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getPendingActionFullInfo", + arguments: [], + }); + + const result: resources.FullMultisigAction[] = []; + for (let action = 0; action < actions.length; action++) { + const element = actions[action]; + result.push({ + actionId: Number(element.action_id.toString()), + groupId: Number(element.group_id.toString()), + actionData: this.mapResponseToAction(element.action_data.valueOf()), + signers: element.signers.map((address: Address) => new Address(address)), + }); + } + return result; + } + + /** + * Gets addresses of all users who signed an action. + * Does not check if those users are still board members or not, so the result may contain invalid signers. + */ + async getActionSigners(options: { mutisigAddress: string; actionId: number }): Promise { + const response = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionSigners", + arguments: [options.actionId], + }); + const addresses: any = response.valueOf(); + return addresses[0]; + } + + /** + * Gets addresses of all users who signed an action and are still board members. + * All these signatures are currently valid. + */ + async getActionSignerCount(options: { mutisigAddress: string; actionId: number }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionSignerCount", + arguments: [options.actionId], + }); + + return value; + } + + /** + * Gets addresses of all users who signed an action and are still board members. + * All these signatures are currently valid. + */ + async getActionValidSignerCount(options: { mutisigAddress: string; actionId: number }): Promise { + const [value] = await this.smartContractController.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionValidSignerCount", + arguments: [options.actionId], + }); + + return Number(value.toString()); + } + + /** + * Creates a transaction for proposing to add a board member + */ + async createTransactionForProposeAddBoardMember( + sender: IAccount, + nonce: bigint, + options: resources.ProposeAddBoardMemberInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForProposeAddBoardMember(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for proposing to add a proposer + */ + async createTransactionForProposeAddProposer( + sender: IAccount, + nonce: bigint, + options: resources.ProposeAddProposerInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForProposeAddProposer(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for proposing to remove a user + */ + async createTransactionForProposeRemoveUser( + sender: IAccount, + nonce: bigint, + options: resources.ProposeRemoveUserInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForProposeRemoveUser(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for proposing to change quorum + */ + async createTransactionForProposeChangeQuorum( + sender: IAccount, + nonce: bigint, + options: resources.ProposeChangeQuorumInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForProposeChangeQuorum(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + parseProposeAction(transaction: TransactionOnNetwork): number { + return this.multisigParser.parseProposeAction(transaction); + } + + async awaitCompletedProposeAction(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(transaction); + } + + /** + * Creates a transaction for signing an action + */ + async createTransactionForSignAction( + sender: IAccount, + nonce: bigint, + options: resources.ActionInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForSignAction(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for performing an action + */ + async createTransactionForPerformAction( + sender: IAccount, + nonce: bigint, + options: resources.ActionInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForPerformAction(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + parsePerformAction(transaction: TransactionOnNetwork): Address | undefined { + return this.multisigParser.parsePerformAction(transaction); + } + + async awaitCompletedPerformAction(txHash: string): Promise
{ + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parsePerformAction(transaction); + } + + /** + * Creates a transaction for unsigning an action + */ + async createTransactionForUnsignAction( + sender: IAccount, + nonce: bigint, + options: resources.ActionInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForUnsign(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for discarding an action + */ + async createTransactionForDiscardAction( + sender: IAccount, + nonce: bigint, + options: resources.ActionInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForDiscardAction(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for deposit native token or tokens + */ + async createTransactionForDeposit( + sender: IAccount, + nonce: bigint, + options: resources.DepositExecuteInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForDeposit(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for proposing to transfer EGLD and execute a smart contract call + */ + async createTransactionForProposeTransferExecute( + sender: IAccount, + nonce: bigint, + options: resources.ProposeTransferExecuteInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForProposeTransferExecute(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for proposing to transfer ESDT tokens and execute a smart contract call + */ + async createTransactionForProposeTransferExecuteEsdt( + sender: IAccount, + nonce: bigint, + options: resources.ProposeTransferExecuteEsdtInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForProposeTransferExecuteEsdt( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for proposing an async call to another contract + */ + async createTransactionForProposeAsyncCall( + sender: IAccount, + nonce: bigint, + options: resources.ProposeAsyncCallInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForProposeAsyncCall(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for proposing to deploy a smart contract from source + */ + async createTransactionForProposeContractDeployFromSource( + sender: IAccount, + nonce: bigint, + options: resources.ProposeContractDeployFromSourceInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForProposeContractDeployFromSource( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for proposing to upgrade a smart contract from source + */ + async createTransactionForProposeContractUpgradeFromSource( + sender: IAccount, + nonce: bigint, + options: resources.ProposeContractUpgradeFromSourceInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForProposeContractUpgradeFromSource( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for signing a batch of actions + */ + async createTransactionForSignBatch( + sender: IAccount, + nonce: bigint, + options: resources.GroupInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForSignBatch(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for signing and performing an action in one step + */ + async createTransactionForSignAndPerform( + sender: IAccount, + nonce: bigint, + options: resources.ActionInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForSignAndPerform(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for unsigning for outdated board members + */ + async createTransactionForUnsignForOutdatedBoardMembers( + sender: IAccount, + nonce: bigint, + options: resources.UnsignForOutdatedBoardMembersInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForUnsignForOutdatedBoardMembers( + sender.address, + options, + ); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for performing a batch of actions + */ + async createTransactionForPerformBatch( + sender: IAccount, + nonce: bigint, + options: resources.GroupInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForPerformBatch(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + /** + * Creates a transaction for discarding a batch of actions + */ + async createTransactionForDiscardBatch( + sender: IAccount, + nonce: bigint, + options: resources.DiscardBatchInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForDiscardBatch(sender.address, options); + + transaction.guardian = options.guardian ?? Address.empty(); + transaction.relayer = options.relayer ?? Address.empty(); + transaction.nonce = nonce; + this.setTransactionGasOptions(transaction, options); + transaction.signature = await sender.signTransaction(transaction); + + return transaction; + } + + private mapResponseToAction = (responseData: any): resources.MultisigAction => { + const { name, fields } = responseData; + switch (name) { + case resources.MultisigActionEnum.Nothing: + return new resources.MultisigAction(); + case resources.MultisigActionEnum.AddBoardMember: + return new resources.AddBoardMember(fields[0]); + case resources.MultisigActionEnum.AddProposer: + return new resources.AddProposer(fields[0]); + case resources.MultisigActionEnum.RemoveUser: + return new resources.RemoveUser(fields[0]); + case resources.MultisigActionEnum.ChangeQuorum: + return new resources.ChangeQuorum(fields[0]); + case resources.MultisigActionEnum.SendTransferExecuteEgld: + return new resources.SendTransferExecuteEgld(fields[0]); + case resources.MultisigActionEnum.SendTransferExecuteEsdt: + return new resources.SendTransferExecuteEsdt(fields[0]); + case resources.MultisigActionEnum.SendAsyncCall: + return new resources.SendAsyncCall(fields[0]); + case resources.MultisigActionEnum.SCDeployFromSource: + return new resources.SCDeployFromSource(fields); + case resources.MultisigActionEnum.SCUpgradeFromSource: + return new resources.SCUpgradeFromSource(fields); + default: + throw new Error(`Unknown action type: ${name}`); + } + }; +} diff --git a/src/multisig/multisigTransactionsFactory.spec.ts b/src/multisig/multisigTransactionsFactory.spec.ts new file mode 100644 index 000000000..068a05ddd --- /dev/null +++ b/src/multisig/multisigTransactionsFactory.spec.ts @@ -0,0 +1,583 @@ +import { assert } from "chai"; +import { Abi, AddressValue, BigUIntValue, Code, U32Value, VariadicValue } from "../abi"; +import { CodeMetadata, Token, TokenTransfer } from "../core"; +import { Address } from "../core/address"; +import { Transaction } from "../core/transaction"; +import { TransactionsFactoryConfig } from "../core/transactionsFactoryConfig"; +import { loadAbiRegistry, loadContractCode } from "../testutils"; +import { MultisigTransactionsFactory } from "./multisigTransactionsFactory"; + +describe("test multisig transactions factory", function () { + const config = new TransactionsFactoryConfig({ + chainID: "D", + }); + + let bytecode: Code; + let abi: Abi; + let adderAbi: Abi; + let esdtSafeAbi: Abi; + let factory: MultisigTransactionsFactory; + before(async function () { + bytecode = await loadContractCode("src/testdata/multisig-full.wasm"); + abi = await loadAbiRegistry("src/testdata/multisig-full.abi.json"); + adderAbi = await loadAbiRegistry("src/testdata/adder.abi.json"); + esdtSafeAbi = await loadAbiRegistry("src/testdata/esdt-safe.abi.json"); + + factory = new MultisigTransactionsFactory({ + config: config, + abi: abi, + }); + }); + + it("should create transaction for deploy multisig contract", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const boardMemberOne = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const boardMemberTwo = Address.newFromBech32("erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8"); + + const board = [boardMemberOne, boardMemberTwo]; + + const transaction = factory.createTransactionForDeploy(senderAddress, { + bytecode: bytecode.valueOf(), + gasLimit: 5000000n, + quorum: 2, + board, + }); + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu"); + assert.equal(transaction.value, 0n); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + Buffer.from(transaction.data), + Buffer.from( + `${bytecode}@0500@0504@02@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@b2a11555ce521e4944e09ab17549d85b487dcd26c84b5017a39e31a3670889ba`, + ), + ); + }); + + it("should create transaction for propose add board member", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const boardMember = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForProposeAddBoardMember(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + boardMember: boardMember, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + transaction.data.toString(), + "proposeAddBoardMember@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", + ); + }); + + it("should create transaction for propose add proposer", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const proposer = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForProposeAddProposer(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + proposer: proposer, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + transaction.data.toString(), + "proposeAddProposer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", + ); + }); + + it("should create transaction for propose remove user", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const userAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForProposeRemoveUser(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + userAddress: userAddress, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + transaction.data.toString(), + "proposeRemoveUser@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8", + ); + }); + + it("should create transaction for propose change quorum", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForProposeChangeQuorum(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + newQuorum: 3, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual(transaction.data.toString(), "proposeChangeQuorum@03"); + }); + + it("should create transaction for propose transfer execute", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const destinationContract = Address.newFromBech32( + "erd1qqqqqqqqqqqqqpgq0rffvv4vk9vesqplv9ws55fxzdfaspqa8cfszy2hms", + ); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqpgq6kurkz43xq8t35kx9p8rvyz5kpxe9g7qd8ssefqjw8", + ); + const amount = 1000000000000000000n; // 1 EGLD + const transaction = factory.createTransactionForProposeTransferExecute(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + nativeTokenAmount: amount, + to: destinationContract, + functionName: "add", + functionArguments: [7], + optGasLimit: 5000000n, + abi: adderAbi, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + transaction.data.toString(), + "proposeTransferExecute@0000000000000000050078d29632acb15998003f615d0a51261353d8041d3e13@0de0b6b3a7640000@0100000000004c4b40@616464@07", + ); + }); + + it("should create transaction for propose transfer execute ESDT", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const destinationContract = Address.newFromBech32( + "erd1qqqqqqqqqqqqqpgqfxlljcaalgl2qfcnxcsftheju0ts36kvl3ts3qkewe", + ); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const token = new Token({ + identifier: "ALICE-5627f1", + }); + const tokenTransfer = new TokenTransfer({ token: token, amount: 10n }); + + const transaction = factory.createTransactionForProposeTransferExecuteEsdt(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + to: destinationContract, + tokens: [tokenTransfer], + functionName: "distribute", + functionArguments: [], + optGasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + transaction.data.toString(), + "proposeTransferExecuteEsdt@0000000000000000050049bff963bdfa3ea02713362095df32e3d708eaccfc57@0000000c414c4943452d3536323766310000000000000000000000010a@0100000000004c4b40@3634363937333734373236393632373537343635", + ); + }); + + it("should create transaction for propose async call", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const destinationContract = Address.newFromBech32( + "erd1qqqqqqqqqqqqqpgq0rffvv4vk9vesqplv9ws55fxzdfaspqa8cfszy2hms", + ); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqpgq6kurkz43xq8t35kx9p8rvyz5kpxe9g7qd8ssefqjw8", + ); + const transaction = factory.createTransactionForProposeAsyncCall(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + nativeTransferAmount: 0n, + to: destinationContract, + functionName: "add", + functionArguments: [7], + tokenTransfers: [], + abi: adderAbi, + optGasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.equal( + transaction.data.toString(), + "proposeAsyncCall@0000000000000000050078d29632acb15998003f615d0a51261353d8041d3e13@@4c4b40@616464@07", + ); + }); + + it("should create transaction for deposit the expected amount of egld", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + + const transaction = factory.createTransactionForDeposit(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + nativeTokenAmount: 1n, + tokenTransfers: [], + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.equal(transaction.value, 1n); + assert.deepEqual(transaction.data.toString(), "deposit"); + }); + + it("should create transaction for deposit esdt token", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const token = new Token({ + identifier: "ALICE-5627f1", + }); + const tokenTransfer = new TokenTransfer({ token: token, amount: 100n }); + + const transaction = factory.createTransactionForDeposit(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + nativeTokenAmount: 0n, + tokenTransfers: [tokenTransfer], + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.equal(transaction.value, 0n); + assert.deepEqual(transaction.data.toString(), "ESDTTransfer@414c4943452d353632376631@64@6465706f736974"); + }); + + it("should create transaction for propose SC deploy from source when abi is passed", function () { + const amount = BigInt(50000000000000000); // 0.05 EGLD + const metadata = new CodeMetadata(true, true, false); + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const sourceContract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqsuxsgykwm6r3s5apct2g5a2rcpe7kw0ed8ssf6h9f6"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqpgq0cjuum0t436gmp446wf3yz43avp2gm2czeus8mctaf", + ); + + const transaction = factory.createTransactionForProposeContractDeployFromSource(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + amount: amount, + source: sourceContract, + codeMetadata: metadata, + arguments: ["7"], + abi: adderAbi, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + transaction.data.toString(), + "proposeSCDeployFromSource@b1a2bc2ec50000@000000000000000005007e25ce6debac748d86b5d393120ab1eb02a46d581679@0500@07", + ); + }); + + it("should create transaction for propose SC deploy from source when no abi is passed", function () { + const amount = BigInt(50000000000000000); // 0.05 EGLD + const metadata = new CodeMetadata(true, true, false); + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const sourceContract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqsuxsgykwm6r3s5apct2g5a2rcpe7kw0ed8ssf6h9f6"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqpgq0cjuum0t436gmp446wf3yz43avp2gm2czeus8mctaf", + ); + + const transaction = factory.createTransactionForProposeContractDeployFromSource(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + amount: amount, + source: sourceContract, + codeMetadata: metadata, + arguments: [new BigUIntValue(7n)], + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + transaction.data.toString(), + "proposeSCDeployFromSource@b1a2bc2ec50000@000000000000000005007e25ce6debac748d86b5d393120ab1eb02a46d581679@0500@07", + ); + }); + + it("should create transaction for propose SC upgrade from source when abi is passed", function () { + const amount = BigInt(50000000000000000); // 0.05 EGLD + const metadata = new CodeMetadata(true, true, false); + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const sourceContract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqd273cw3hjndqzcpts4dvq0ncy8nx8rkgzeusnefvaq"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqpgq0cjuum0t436gmp446wf3yz43avp2gm2czeus8mctaf", + ); + + const transaction = factory.createTransactionForProposeContractUpgradeFromSource(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + scAddress: multisigContractAddress, + amount: amount, + source: sourceContract, + codeMetadata: metadata, + arguments: [ + 2, + "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "erd1qqqqqqqqqqqqqpgqsuxsgykwm6r3s5apct2g5a2rcpe7kw0ed8ssf6h9f6", + ], + abi: esdtSafeAbi, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + transaction.data.toString(), + "proposeSCUpgradeFromSource@000000000000000005007e25ce6debac748d86b5d393120ab1eb02a46d581679@b1a2bc2ec50000@000000000000000005006abd1c3a3794da01602b855ac03e7821e6638ec81679@0500@02@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@00000000000000000500870d0412cede871853a1c2d48a7543c073eb39f969e1", + ); + }); + + it("should create transaction for propose SC upgrade from source when no abi is passed", function () { + const amount = BigInt(50000000000000000); // 0.05 EGLD + const metadata = new CodeMetadata(true, true, false); + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const sourceContract = Address.newFromBech32("erd1qqqqqqqqqqqqqpgqd273cw3hjndqzcpts4dvq0ncy8nx8rkgzeusnefvaq"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqpgq0cjuum0t436gmp446wf3yz43avp2gm2czeus8mctaf", + ); + + const transaction = factory.createTransactionForProposeContractUpgradeFromSource(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + scAddress: multisigContractAddress, + amount: amount, + source: sourceContract, + codeMetadata: metadata, + arguments: [ + new U32Value(2n), + VariadicValue.fromItems( + new AddressValue( + Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"), + ), + new AddressValue( + Address.newFromBech32("erd1qqqqqqqqqqqqqpgqsuxsgykwm6r3s5apct2g5a2rcpe7kw0ed8ssf6h9f6"), + ), + ), + ], + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual( + transaction.data.toString(), + "proposeSCUpgradeFromSource@000000000000000005007e25ce6debac748d86b5d393120ab1eb02a46d581679@b1a2bc2ec50000@000000000000000005006abd1c3a3794da01602b855ac03e7821e6638ec81679@0500@02@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@00000000000000000500870d0412cede871853a1c2d48a7543c073eb39f969e1", + ); + }); + + it("should create transaction for sign action", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForSignAction(senderAddress, { + multisigContract: multisigContractAddress, + actionId: 42, + gasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual(transaction.data.toString(), "sign@2a"); + }); + + it("should create transaction for sign batch", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForSignBatch(senderAddress, { + multisigContract: multisigContractAddress, + groupId: 5, + gasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual(transaction.data.toString(), "signBatch@05"); + }); + + it("should create transaction for sign and perform", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForSignAndPerform(senderAddress, { + multisigContract: multisigContractAddress, + actionId: 42, + gasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual(transaction.data.toString(), "signAndPerform@2a"); + }); + + it("should create transaction for unsign", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForUnsign(senderAddress, { + multisigContract: multisigContractAddress, + actionId: 42, + gasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.deepEqual(transaction.data.toString(), "unsign@2a"); + }); + + it("should create transaction for unsign for outdated board members", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForUnsignForOutdatedBoardMembers(senderAddress, { + multisigContract: multisigContractAddress, + actionId: 42, + outdatedBoardMembers: [1, 3, 5], + gasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.deepEqual(transaction.data.toString(), "unsignForOutdatedBoardMembers@2a@01@03@05"); + }); + + it("should create transaction for perform action", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForPerformAction(senderAddress, { + multisigContract: multisigContractAddress, + actionId: 42, + gasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.deepEqual(transaction.data.toString(), "performAction@2a"); + }); + + it("should create transaction for perform batch", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForPerformBatch(senderAddress, { + multisigContract: multisigContractAddress, + groupId: 5, + gasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.deepEqual(transaction.data.toString(), "performBatch@05"); + }); + + it("should create transaction for discard action", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForDiscardAction(senderAddress, { + multisigContract: multisigContractAddress, + actionId: 322, + gasLimit: 5000000n, + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual(transaction.data.toString(), "discardAction@0142"); + }); + + it("should create transaction for discard batch", function () { + const senderAddress = Address.newFromBech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"); + + const multisigContractAddress = Address.newFromBech32( + "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6", + ); + const transaction = factory.createTransactionForDiscardBatch(senderAddress, { + multisigContract: multisigContractAddress, + gasLimit: 5000000n, + actionIds: [24, 25], + }); + + assert.instanceOf(transaction, Transaction); + assert.equal(transaction.sender.toBech32(), senderAddress.toBech32()); + assert.equal(transaction.receiver.toBech32(), multisigContractAddress.toBech32()); + assert.equal(transaction.chainID, config.chainID); + assert.deepEqual(transaction.data.toString(), "discardBatch@18@19"); + }); +}); diff --git a/src/multisig/multisigTransactionsFactory.ts b/src/multisig/multisigTransactionsFactory.ts new file mode 100644 index 000000000..c2997776f --- /dev/null +++ b/src/multisig/multisigTransactionsFactory.ts @@ -0,0 +1,433 @@ +import { + Abi, + AddressValue, + ArgSerializer, + BigUIntValue, + BytesValue, + CodeMetadataValue, + EndpointDefinition, + isTyped, + NativeSerializer, + OptionType, + OptionValue, + TypedValue, + U32Value, + U64Type, + U64Value, + VariadicValue, +} from "../abi"; +import { Err, TokenComputer, TransactionsFactoryConfig } from "../core"; +import { Address } from "../core/address"; +import { Transaction } from "../core/transaction"; +import { TransactionBuilder } from "../core/transactionBuilder"; +import { SmartContractTransactionsFactory } from "../smartContracts"; +import { ProposeTransferExecuteContractInput } from "./proposeTransferExecuteContractInput"; +import * as resources from "./resources"; + +interface IConfig { + chainID: string; + addressHrp: string; + minGasLimit: bigint; + gasLimitPerByte: bigint; +} + +/** + * Use this class to create multisig related transactions like creating a new multisig contract, + * proposing actions, signing actions, and performing actions. + */ +export class MultisigTransactionsFactory { + private readonly argSerializer: ArgSerializer; + private readonly smartContractFactory: SmartContractTransactionsFactory; + private readonly config: IConfig; + private readonly abi: Abi; + + constructor(options: { config: TransactionsFactoryConfig; abi: Abi }) { + this.config = options.config; + this.abi = options.abi; + this.argSerializer = new ArgSerializer(); + this.smartContractFactory = new SmartContractTransactionsFactory(options); + } + + /** + * Creates a transaction to deploy a new multisig contract + */ + createTransactionForDeploy(sender: Address, options: resources.DeployMultisigContractInput): Transaction { + const boardAddresses: AddressValue[] = options.board.map((addr) => new AddressValue(addr)); + const args = [new U32Value(options.quorum), VariadicValue.fromItems(...boardAddresses)]; + + return this.smartContractFactory.createTransactionForDeploy(sender, { + bytecode: options.bytecode, + gasLimit: options.gasLimit, + isUpgradeable: options.isUpgradeable, + isReadable: options.isReadable, + isPayable: options.isPayable, + isPayableBySmartContract: options.isPayableBySmartContract, + arguments: args, + }); + } + + /** + * Proposes adding a new board member + */ + createTransactionForProposeAddBoardMember( + sender: Address, + options: resources.ProposeAddBoardMemberInput, + ): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "proposeAddBoardMember", + gasLimit: options.gasLimit, + arguments: [options.boardMember], + }); + } + + /** + * Proposes adding a new proposer + */ + createTransactionForProposeAddProposer(sender: Address, options: resources.ProposeAddProposerInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "proposeAddProposer", + gasLimit: options.gasLimit, + arguments: [options.proposer], + }); + } + + /** + * Proposes removing a user (board member or proposer) + */ + createTransactionForProposeRemoveUser(sender: Address, options: resources.ProposeRemoveUserInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "proposeRemoveUser", + gasLimit: options.gasLimit, + arguments: [options.userAddress], + }); + } + + /** + * Proposes changing the quorum (minimum signatures required) + */ + createTransactionForProposeChangeQuorum(sender: Address, options: resources.ProposeChangeQuorumInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "proposeChangeQuorum", + gasLimit: options.gasLimit, + arguments: [options.newQuorum], + }); + } + + /** + * Proposes a transaction that will transfer EGLD and/or execute a function + */ + createTransactionForProposeTransferExecute( + sender: Address, + options: resources.ProposeTransferExecuteInput, + ): Transaction { + const gasOption = new U64Value(options.optGasLimit ?? 0n); + const input = ProposeTransferExecuteContractInput.newFromTransferExecuteInput({ + multisig: options.multisigContract, + to: options.to, + functionName: options.functionName, + arguments: options.functionArguments, + abi: options.abi, + }); + + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "proposeTransferExecute", + gasLimit: options.gasLimit, + arguments: [ + new AddressValue(options.to), + new BigUIntValue(options.nativeTokenAmount), + new OptionValue(new OptionType(new U64Type()), gasOption), + VariadicValue.fromItems(...input.functionCall.map((value) => new BytesValue(value))), + ], + }); + } + + /** + * Proposes a transaction that will transfer EGLD and/or execute a function + */ + createTransactionForDeposit(sender: Address, options: resources.DepositExecuteInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "deposit", + gasLimit: options.gasLimit, + arguments: [], + nativeTransferAmount: options.nativeTokenAmount, + tokenTransfers: options.tokenTransfers, + }); + } + + /** + * Proposes a transaction that will transfer ESDT tokens and/or execute a function + */ + createTransactionForProposeTransferExecuteEsdt( + sender: Address, + options: resources.ProposeTransferExecuteEsdtInput, + ): Transaction { + const input = ProposeTransferExecuteContractInput.newFromTransferExecuteInput({ + multisig: options.multisigContract, + to: options.to, + functionName: options.functionName, + arguments: options.functionArguments, + abi: options.abi, + }); + + const tokenPayments: resources.EsdtTokenPayment[] = this.mapTokenPayments(options); + const dataParts = [ + "proposeTransferExecuteEsdt", + ...this.argSerializer.valuesToStrings( + NativeSerializer.nativeToTypedValues( + [options.to, tokenPayments, options.optGasLimit, VariadicValue.fromItems(...input.functionCall)], + this.abi.getEndpoint("proposeTransferExecuteEsdt"), + ), + ), + ]; + return new TransactionBuilder({ + config: this.config, + sender: sender, + receiver: options.multisigContract, + dataParts: dataParts, + gasLimit: options.gasLimit, + addDataMovementGas: false, + }).build(); + } + + private mapTokenPayments(options: resources.ProposeTransferExecuteEsdtInput): resources.EsdtTokenPayment[] { + const tokenComputer = new TokenComputer(); + const tokens = []; + for (const token of options.tokens) { + tokens.push({ + token_identifier: tokenComputer.extractIdentifierFromExtendedIdentifier(token.token.identifier), + token_nonce: token.token.nonce, + amount: token.amount, + }); + } + return tokens; + } + + /** + * Proposes an async call to another contract + */ + createTransactionForProposeAsyncCall(sender: Address, options: resources.ProposeAsyncCallInput): Transaction { + const input = ProposeTransferExecuteContractInput.newFromProposeAsyncCallInput({ + multisig: options.multisigContract, + to: options.to, + tokenTransfers: options.tokenTransfers, + functionName: options.functionName, + arguments: options.functionArguments, + abi: options.abi, + }); + + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "proposeAsyncCall", + gasLimit: options.gasLimit, + arguments: [ + new AddressValue(options.to), + new BigUIntValue(options.nativeTransferAmount), + new BigUIntValue(options.optGasLimit ?? 0n), + VariadicValue.fromItems(...input.functionCall.map((value) => new BytesValue(value))), + ], + }); + } + + /** + * Proposes deploying a smart contract from source + */ + createTransactionForProposeContractDeployFromSource( + sender: Address, + options: resources.ProposeContractDeployFromSourceInput, + ): Transaction { + let args: TypedValue[] = this.argsToTypedValues(options.arguments, options.abi?.constructorDefinition); + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "proposeSCDeployFromSource", + gasLimit: options.gasLimit, + arguments: [ + new BigUIntValue(options.amount), + new AddressValue(options.multisigContract), + new CodeMetadataValue(options.codeMetadata), + VariadicValue.fromItems(...args), + ], + }); + } + + /** + * Proposes upgrading a smart contract from source + */ + createTransactionForProposeContractUpgradeFromSource( + sender: Address, + options: resources.ProposeContractUpgradeFromSourceInput, + ): Transaction { + let args: TypedValue[] = this.argsToTypedValues(options.arguments, options.abi?.constructorDefinition); + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "proposeSCUpgradeFromSource", + gasLimit: options.gasLimit, + arguments: [ + new AddressValue(options.multisigContract), + new BigUIntValue(options.amount), + new AddressValue(options.source), + new CodeMetadataValue(options.codeMetadata), + VariadicValue.fromItems(...args), + ], + }); + } + + /** + * Signs an action (by a board member) + */ + createTransactionForSignAction(sender: Address, options: resources.ActionInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "sign", + gasLimit: options.gasLimit, + arguments: [options.actionId], + }); + } + + /** + * Signs all actions in a batch + */ + createTransactionForSignBatch(sender: Address, options: resources.GroupInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "signBatch", + gasLimit: options.gasLimit, + arguments: [options.groupId], + }); + } + + /** + * Signs and performs an action in one transaction + */ + createTransactionForSignAndPerform(sender: Address, options: resources.ActionInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "signAndPerform", + gasLimit: options.gasLimit, + arguments: [options.actionId], + }); + } + + /** + * Signs and performs all actions in a batch + */ + createTransactionForSignBatchAndPerform(sender: Address, options: resources.GroupInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "signBatchAndPerform", + gasLimit: options.gasLimit, + arguments: [options.groupId], + }); + } + + /** + * Withdraws signature from an action + */ + createTransactionForUnsign(sender: Address, options: resources.ActionInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "unsign", + gasLimit: options.gasLimit, + arguments: [options.actionId], + }); + } + + /** + * Withdraws signatures from all actions in a batch + */ + createTransactionForUnsignBatch(sender: Address, options: resources.GroupInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "unsignBatch", + gasLimit: options.gasLimit, + arguments: [options.groupId], + }); + } + + /** + * Removes signatures from outdated board members + */ + createTransactionForUnsignForOutdatedBoardMembers( + sender: Address, + options: resources.UnsignForOutdatedBoardMembersInput, + ): Transaction { + const outdatedBoardMembers: U32Value[] = options.outdatedBoardMembers.map((id) => new U32Value(id)); + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "unsignForOutdatedBoardMembers", + gasLimit: options.gasLimit, + arguments: [new U32Value(options.actionId), VariadicValue.fromItems(...outdatedBoardMembers)], + }); + } + + /** + * Performs an action that has reached quorum + */ + createTransactionForPerformAction(sender: Address, options: resources.ActionInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "performAction", + gasLimit: options.gasLimit, + arguments: [options.actionId], + }); + } + + /** + * Performs all actions in a batch that have reached quorum + */ + createTransactionForPerformBatch(sender: Address, options: resources.GroupInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "performBatch", + gasLimit: options.gasLimit, + arguments: [options.groupId], + }); + } + + /** + * Discards an action that is no longer needed + */ + createTransactionForDiscardAction(sender: Address, options: resources.ActionInput): Transaction { + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "discardAction", + gasLimit: options.gasLimit, + arguments: [options.actionId], + }); + } + + /** + * Discards all actions in the provided list + */ + createTransactionForDiscardBatch(sender: Address, options: resources.DiscardBatchInput): Transaction { + const actionIdsArgs = options.actionIds.map((id) => new U32Value(id)); + return this.smartContractFactory.createTransactionForExecute(sender, { + contract: options.multisigContract, + function: "discardBatch", + gasLimit: options.gasLimit, + arguments: [VariadicValue.fromItems(...actionIdsArgs)], + }); + } + + protected argsToTypedValues(args: any[], endpoint?: EndpointDefinition): TypedValue[] { + if (endpoint) { + const typedArgs = NativeSerializer.nativeToTypedValues(args, endpoint); + return typedArgs; + } + + if (this.areArgsOfTypedValue(args)) { + return args; + } + + throw new Err("Can't convert args to TypedValues"); + } + + private areArgsOfTypedValue(args: any[]): boolean { + return args.every((arg) => isTyped(arg)); + } +} diff --git a/src/multisig/multisigTransactionsOutcomeParser.ts b/src/multisig/multisigTransactionsOutcomeParser.ts new file mode 100644 index 000000000..d595bf4f3 --- /dev/null +++ b/src/multisig/multisigTransactionsOutcomeParser.ts @@ -0,0 +1,48 @@ +import { Abi } from "../abi"; +import { Address, TransactionOnNetwork } from "../core"; +import { SmartContractDeployOutcome } from "../smartContracts/resources"; +import { SmartContractTransactionsOutcomeParser } from "../transactionsOutcomeParsers"; + +/** + * Parses the outcome of multisig contract operations + */ +export class MultisigTransactionsOutcomeParser { + private parser: SmartContractTransactionsOutcomeParser; + private readonly abi: Abi; + + constructor(options: { abi: Abi }) { + this.abi = options?.abi; + this.parser = new SmartContractTransactionsOutcomeParser({ abi: this.abi }); + } + + /** + * Parses the outcome of creating a new multisig contract + * @param transactionOnNetwork The completed transaction + * @returns An array of objects containing the new contract addresses + */ + parseDeploy(transactionOnNetwork: TransactionOnNetwork): SmartContractDeployOutcome { + return this.parser.parseDeploy({ transactionOnNetwork }); + } + + /** + * Parses the outcome of a multisig action proposal + * @param transactionOnNetwork The completed transaction + * @returns The action ID that was created + */ + parseProposeAction(transactionOnNetwork: TransactionOnNetwork): number { + const result = this.parser.parseExecute({ transactionOnNetwork }); + + return result.values[0]; + } + + /** + * Parses the outcome of a multisig action proposal + * @param transactionOnNetwork The completed transaction + * @returns In case of scDeploy returns address else undefined + */ + parsePerformAction(transactionOnNetwork: TransactionOnNetwork): Address | undefined { + const result = this.parser.parseExecute({ transactionOnNetwork }); + + return result.values[0]; + } +} diff --git a/src/multisig/proposeTransferExecuteContractInput.ts b/src/multisig/proposeTransferExecuteContractInput.ts new file mode 100644 index 000000000..6c89b6cbc --- /dev/null +++ b/src/multisig/proposeTransferExecuteContractInput.ts @@ -0,0 +1,88 @@ +import { Abi, BytesValue } from "../abi"; +import { Address, TokenTransfer, Transaction, TransactionsFactoryConfig } from "../core"; +import { ARGUMENTS_SEPARATOR } from "../core/constants"; +import { utf8ToHex } from "../core/utils.codec"; +import { SmartContractTransactionsFactory } from "../smartContracts"; + +export class ProposeTransferExecuteContractInput { + multisigContract: Address; + to: Address; + gasLimit?: bigint; + functionCall: any[]; + + constructor(options: { multisigContract: Address; to: Address; gasLimit?: bigint; functionCall: any[] }) { + this.multisigContract = options.multisigContract; + this.to = options.to; + this.gasLimit = options.gasLimit; + this.functionCall = options.functionCall; + } + + static newFromTransferExecuteInput(options: { + multisig: Address; + to: Address; + functionName: string; + arguments: any[]; + optGasLimit?: bigint; + abi?: Abi; + }): ProposeTransferExecuteContractInput { + const transactionsFactory = new SmartContractTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: "" }), + abi: options.abi, + }); + const transaction = transactionsFactory.createTransactionForExecute(Address.empty(), { + contract: Address.empty(), + function: options.functionName, + gasLimit: 0n, + arguments: options.arguments, + nativeTransferAmount: 0n, + }); + + const functionCall = ProposeTransferExecuteContractInput.getFunctionCall(transaction); + + return new ProposeTransferExecuteContractInput({ + multisigContract: options.multisig, + to: options.to, + functionCall: functionCall, + gasLimit: options.optGasLimit, + }); + } + + static newFromProposeAsyncCallInput(options: { + multisig: Address; + to: Address; + tokenTransfers: TokenTransfer[]; + functionName: string; + arguments: any[]; + optGasLimit?: bigint; + abi?: Abi; + }): ProposeTransferExecuteContractInput { + const transactionsFactory = new SmartContractTransactionsFactory({ + config: new TransactionsFactoryConfig({ chainID: "" }), + abi: options.abi, + }); + const transaction = transactionsFactory.createTransactionForExecute(Address.empty(), { + contract: Address.empty(), + function: options.functionName, + gasLimit: 0n, + arguments: options.arguments, + nativeTransferAmount: 0n, + }); + + const functionCall = ProposeTransferExecuteContractInput.getFunctionCall(transaction); + + return new ProposeTransferExecuteContractInput({ + multisigContract: options.multisig, + to: options.to, + functionCall: functionCall, + gasLimit: options.optGasLimit, + }); + } + + private static getFunctionCall(transaction: Transaction) { + const functionCallParts = Buffer.from(transaction.data).toString().split(ARGUMENTS_SEPARATOR); + const functionName = functionCallParts[0]; + const functionArguments = functionCallParts.slice(1).map((part) => part.valueOf()); + const functionCall = [new BytesValue(Buffer.from(utf8ToHex(functionName))), ...functionArguments]; + return functionCall; + } +} diff --git a/src/multisig/resources.ts b/src/multisig/resources.ts new file mode 100644 index 000000000..2e3b0c872 --- /dev/null +++ b/src/multisig/resources.ts @@ -0,0 +1,280 @@ +import { Abi } from "../abi"; +import { Token, TokenTransfer } from "../core"; +import { Address } from "../core/address"; +import { CodeMetadata } from "../core/codeMetadata"; + +export type DeployMultisigContractInput = { + quorum: number; + board: Address[]; + bytecode: Uint8Array; + isUpgradeable?: boolean; + isReadable?: boolean; + isPayable?: boolean; + isPayableBySmartContract?: boolean; + gasLimit: bigint; +}; + +export type MultisigContractInput = { + multisigContract: Address; + gasLimit: bigint; +}; + +export type ProposeAddBoardMemberInput = MultisigContractInput & { + boardMember: Address; +}; + +export type ProposeAddProposerInput = MultisigContractInput & { + proposer: Address; +}; + +export type ProposeRemoveUserInput = MultisigContractInput & { + userAddress: Address; +}; + +export type ProposeChangeQuorumInput = MultisigContractInput & { + newQuorum: number; +}; + +export type ProposeTransferExecuteInput = MultisigContractInput & { + to: Address; + nativeTokenAmount: bigint; + optGasLimit?: bigint; + functionName: string; + functionArguments: any[]; + abi?: Abi; +}; + +export type DepositExecuteInput = MultisigContractInput & { + nativeTokenAmount: bigint; + gasLimit?: bigint; + tokenTransfers: TokenTransfer[]; +}; + +export type ProposeTransferExecuteEsdtInput = MultisigContractInput & { + to: Address; + tokens: any[]; + optGasLimit?: bigint; + functionName: string; + functionArguments: any[]; + abi?: Abi; +}; + +export type ProposeAsyncCallInput = MultisigContractInput & { + multisigContract: Address; + to: Address; + nativeTransferAmount: bigint; + tokenTransfers: TokenTransfer[]; + functionName: string; + functionArguments: any[]; + optGasLimit?: bigint; + abi?: Abi; +}; + +export type ProposeContractDeployFromSourceInput = MultisigContractInput & { + amount: bigint; + source: Address; + codeMetadata: CodeMetadata; + arguments: any[]; + abi?: Abi; +}; + +export type ProposeContractUpgradeFromSourceInput = MultisigContractInput & { + scAddress: Address; + amount: bigint; + source: Address; + codeMetadata: CodeMetadata; + arguments: any[]; + abi?: Abi; +}; + +export type ActionInput = MultisigContractInput & { + actionId: number; +}; + +export type GroupInput = MultisigContractInput & { + groupId: number; +}; + +export type UnsignForOutdatedBoardMembersInput = ActionInput & { + outdatedBoardMembers: number[]; +}; + +export type DiscardBatchInput = MultisigContractInput & { + actionIds: number[]; +}; + +export enum UserRoleEnum { + None = "None", + Proposer = "Proposer", + BoardMember = "BoardMember", +} + +export enum MultisigActionEnum { + Nothing = "Nothing", + AddBoardMember = "AddBoardMember", + AddProposer = "AddProposer", + RemoveUser = "RemoveUser", + ChangeQuorum = "ChangeQuorum", + SendTransferExecuteEgld = "SendTransferExecuteEgld", + SendTransferExecuteEsdt = "SendTransferExecuteEsdt", + SendAsyncCall = "SendAsyncCall", + SCDeployFromSource = "SCDeployFromSource", + SCUpgradeFromSource = "SCUpgradeFromSource", +} + +export class MultisigAction { + public type: MultisigActionEnum = MultisigActionEnum.Nothing; +} + +export type FullMultisigAction = { + actionId: number; + groupId: number; + signers: Address[]; + actionData: MultisigAction; +}; + +export class AddBoardMember extends MultisigAction { + public address: Address; + constructor(address: Address) { + super(); + this.type = MultisigActionEnum.AddBoardMember; + this.address = address; + } +} +export class AddProposer extends MultisigAction { + public address: Address; + + constructor(address: Address) { + super(); + this.type = MultisigActionEnum.AddProposer; + this.address = address; + } +} +export class RemoveUser extends MultisigAction { + public type: MultisigActionEnum = MultisigActionEnum.RemoveUser; + public address: Address; + + constructor(address: Address) { + super(); + this.type = MultisigActionEnum.RemoveUser; + this.address = address; + } +} + +export class ChangeQuorum extends MultisigAction { + public quorum: number; + + constructor(quorum: number) { + super(); + this.type = MultisigActionEnum.ChangeQuorum; + this.quorum = quorum; + } +} + +export class SendTransferExecuteEgld extends MultisigAction { + receiver: Address; + amount: bigint; + optionalGasLimit?: bigint; + functionName: string; + arguments: Uint8Array[]; + + constructor(data: any) { + super(); + this.type = MultisigActionEnum.SendTransferExecuteEgld; + this.receiver = data.to; + this.amount = BigInt(data.egld_amount?.toFixed() ?? 0); + this.optionalGasLimit = BigInt(data.opt_gas_limit?.toFixed() ?? 0); + this.functionName = data.endpoint_name.toString(); + this.arguments = data.arguments; + } +} +export class SendTransferExecuteEsdt extends MultisigAction { + receiver: Address; + tokens: TokenTransfer[]; + optionalGasLimit?: bigint; + funcionName: string; + arguments: Uint8Array[]; + + constructor(data: any) { + super(); + this.type = MultisigActionEnum.SendTransferExecuteEsdt; + this.receiver = data.to; + this.tokens = data.tokens.map( + (token: { token_identifier: string; nonce: bigint; amount: bigint }) => + new TokenTransfer({ + token: new Token({ identifier: token.token_identifier, nonce: token.nonce }), + amount: token.amount, + }), + ); + this.optionalGasLimit = BigInt(data.opt_gas_limit.toFixed()); + + this.funcionName = Buffer.from(data.endpoint_name.toString(), "hex").toString(); + this.arguments = data.arguments; + } +} + +export class SendAsyncCall extends MultisigAction { + receiver: Address; + amount: bigint; + optionalGasLimit?: bigint; + funcionName: string; + arguments: Uint8Array[]; + + constructor(data: any) { + super(); + this.type = MultisigActionEnum.SendAsyncCall; + this.receiver = data.to; + this.amount = BigInt(data.egld_amount?.toFixed() ?? 0); + this.optionalGasLimit = BigInt(data.opt_gas_limit?.toFixed() ?? 0); + this.funcionName = data.endpoint_name.toString(); + this.arguments = data.arguments; + } +} + +export class SCDeployFromSource extends MultisigAction { + sourceContract: Address; + amount: bigint; + codeMetadata: CodeMetadata; + arguments: Uint8Array[]; + + constructor(data: any) { + super(); + this.type = MultisigActionEnum.SCDeployFromSource; + this.sourceContract = data[1]; + this.amount = data[0]; + this.codeMetadata = data[2]; + this.arguments = data[3]; + } +} + +export class SCUpgradeFromSource extends MultisigAction { + sourceContract: Address; + destinationContract: Address; + amount: bigint; + codeMetadata: CodeMetadata; + arguments: Uint8Array[]; + + constructor(data: any) { + super(); + this.type = MultisigActionEnum.SCUpgradeFromSource; + this.destinationContract = data[0]; + this.amount = data[1]; + this.sourceContract = data[2]; + this.codeMetadata = data[3]; + this.arguments = data[4]; + } +} + +export type CallActionData = { + receiver: Address; + amount: bigint; + optionalGasLimit?: number | null; + functionName: Uint8Array; + arguments: Uint8Array[]; +}; + +export type EsdtTokenPayment = { + token_identifier: any; + token_nonce: any; + amount: any; +}; diff --git a/src/networkProviders/contractQueryResponse.ts b/src/networkProviders/contractQueryResponse.ts deleted file mode 100644 index 40da09129..000000000 --- a/src/networkProviders/contractQueryResponse.ts +++ /dev/null @@ -1,50 +0,0 @@ -import BigNumber from "bignumber.js"; -import { MaxUint64AsBigNumber } from "./constants"; - -export class ContractQueryResponse { - returnData: string[]; - returnCode: string; - returnMessage: string; - gasUsed: number; - - constructor(init?: Partial) { - this.returnData = init?.returnData || []; - this.returnCode = init?.returnCode || ""; - this.returnMessage = init?.returnMessage || ""; - this.gasUsed = init?.gasUsed || 0; - } - - /** - * Constructs a QueryResponse object from a HTTP response (as returned by the provider). - */ - static fromHttpResponse(payload: any): ContractQueryResponse { - let returnData = payload["returnData"] || payload["ReturnData"]; - let returnCode = payload["returnCode"] || payload["ReturnCode"]; - let returnMessage = payload["returnMessage"] || payload["ReturnMessage"]; - let gasRemaining = new BigNumber(payload["gasRemaining"] || payload["GasRemaining"] || 0); - let gasUsed = MaxUint64AsBigNumber.minus(gasRemaining).toNumber(); - - return new ContractQueryResponse({ - returnData: returnData, - returnCode: returnCode, - returnMessage: returnMessage, - gasUsed: gasUsed, - }); - } - - getReturnDataParts(): Buffer[] { - return this.returnData.map((item) => Buffer.from(item || "", "base64")); - } - - /** - * Converts the object to a pretty, plain JavaScript object. - */ - toJSON(): object { - return { - returnData: this.returnData, - returnCode: this.returnCode, - returnMessage: this.returnMessage, - gasUsed: this.gasUsed.valueOf(), - }; - } -} diff --git a/src/networkProviders/contractResults.ts b/src/networkProviders/contractResults.ts deleted file mode 100644 index 38c057f75..000000000 --- a/src/networkProviders/contractResults.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Address } from "../core/address"; -import { TransactionLogs } from "../core/transactionLogs"; - -export class ContractResults { - readonly items: ContractResultItem[]; - - constructor(items: ContractResultItem[]) { - this.items = items; - - this.items.sort(function (a: ContractResultItem, b: ContractResultItem) { - return a.nonce.valueOf() - b.nonce.valueOf(); - }); - } - - static fromProxyHttpResponse(results: any[]): ContractResults { - const items = results.map((item) => ContractResultItem.fromProxyHttpResponse(item)); - return new ContractResults(items); - } - - static fromApiHttpResponse(results: any[]): ContractResults { - let items = results.map((item) => ContractResultItem.fromApiHttpResponse(item)); - return new ContractResults(items); - } -} - -export class ContractResultItem { - hash: string = ""; - nonce: number = 0; - value: string = ""; - receiver: Address = Address.empty(); - sender: Address = Address.empty(); - data: string = ""; - previousHash: string = ""; - originalHash: string = ""; - gasLimit: number = 0; - gasPrice: number = 0; - callType: number = 0; - returnMessage: string = ""; - logs: TransactionLogs = new TransactionLogs(); - - constructor(init?: Partial) { - Object.assign(this, init); - } - - static fromProxyHttpResponse(response: any): ContractResultItem { - const item = ContractResultItem.fromHttpResponse(response); - return item; - } - - static fromApiHttpResponse(response: any): ContractResultItem { - let item = ContractResultItem.fromHttpResponse(response); - - item.data = Buffer.from(item.data, "base64").toString(); - item.callType = Number(item.callType); - - return item; - } - - private static fromHttpResponse(response: any): ContractResultItem { - let item = new ContractResultItem(); - - item.hash = response.hash; - item.nonce = Number(response.nonce || 0); - item.value = (response.value || 0).toString(); - item.receiver = new Address(response.receiver); - item.sender = new Address(response.sender); - item.previousHash = response.prevTxHash; - item.originalHash = response.originalTxHash; - item.gasLimit = Number(response.gasLimit || 0); - item.gasPrice = Number(response.gasPrice || 0); - item.data = response.data || ""; - item.callType = response.callType; - item.returnMessage = response.returnMessage; - - item.logs = TransactionLogs.fromHttpResponse(response.logs || {}); - - return item; - } -} diff --git a/src/networkProviders/index.ts b/src/networkProviders/index.ts index 97f5dffa9..3e11b80a4 100644 --- a/src/networkProviders/index.ts +++ b/src/networkProviders/index.ts @@ -2,9 +2,6 @@ export { ApiNetworkProvider } from "./apiNetworkProvider"; export * from "./interface"; export { ProxyNetworkProvider } from "./proxyNetworkProvider"; -export { ContractQueryResponse } from "./contractQueryResponse"; -export { ContractResultItem, ContractResults } from "./contractResults"; - export * from "./accounts"; export * from "./blocks"; export { NetworkConfig } from "./networkConfig"; diff --git a/src/smartContracts/smartContractController.ts b/src/smartContracts/smartContractController.ts index cf02ecfc8..d1e610eb7 100644 --- a/src/smartContracts/smartContractController.ts +++ b/src/smartContracts/smartContractController.ts @@ -20,11 +20,11 @@ import * as resources from "./resources"; import { SmartContractTransactionsFactory } from "./smartContractTransactionsFactory"; export class SmartContractController extends BaseController { - private factory: SmartContractTransactionsFactory; + protected factory: SmartContractTransactionsFactory; private parser: SmartContractTransactionsOutcomeParser; private transactionWatcher: TransactionWatcher; private networkProvider: INetworkProvider; - private abi?: Abi; + protected abi?: Abi; constructor(options: { chainID: string; networkProvider: INetworkProvider; abi?: Abi }) { super(); diff --git a/src/smartContracts/smartContractTransactionsFactory.ts b/src/smartContracts/smartContractTransactionsFactory.ts index 18e9e522a..83136bd2f 100644 --- a/src/smartContracts/smartContractTransactionsFactory.ts +++ b/src/smartContracts/smartContractTransactionsFactory.ts @@ -1,4 +1,4 @@ -import { ArgSerializer, ContractFunction, EndpointDefinition, isTyped, NativeSerializer } from "../abi"; +import { Abi, ArgSerializer, EndpointDefinition, isTyped, NativeSerializer } from "../abi"; import { Address, CodeMetadata } from "../core"; import { CONTRACT_DEPLOY_ADDRESS_HEX, @@ -23,24 +23,17 @@ interface IConfig { gasLimitChangeOwnerAddress: bigint; } -interface IAbi { - constructorDefinition: EndpointDefinition; - upgradeConstructorDefinition?: EndpointDefinition; - - getEndpoint(name: string | ContractFunction): EndpointDefinition; -} - /** * Use this class to create transactions to deploy, call or upgrade a smart contract. */ export class SmartContractTransactionsFactory { private readonly config: IConfig; - private readonly abi?: IAbi; + private readonly abi?: Abi; private readonly tokenComputer: TokenComputer; private readonly dataArgsBuilder: TokenTransfersDataBuilder; private readonly contractDeployAddress: Address; - constructor(options: { config: IConfig; abi?: IAbi }) { + constructor(options: { config: IConfig; abi?: Abi }) { this.config = options.config; this.abi = options.abi; this.tokenComputer = new TokenComputer(); @@ -202,7 +195,7 @@ export class SmartContractTransactionsFactory { }).build(); } - private argsToDataParts(args: any[], endpoint?: EndpointDefinition): string[] { + protected argsToDataParts(args: any[], endpoint?: EndpointDefinition): string[] { if (endpoint) { const typedArgs = NativeSerializer.nativeToTypedValues(args, endpoint); return new ArgSerializer().valuesToStrings(typedArgs); diff --git a/src/testdata/multisig-full.abi.json b/src/testdata/multisig-full.abi.json index 401de7cc0..6e9657dce 100644 --- a/src/testdata/multisig-full.abi.json +++ b/src/testdata/multisig-full.abi.json @@ -1,1304 +1,1279 @@ { - "buildInfo": { - "rustc": { - "version": "1.71.0-nightly", - "commitHash": "a2b1646c597329d0a25efa3889b66650f65de1de", - "commitDate": "2023-05-25", - "channel": "Nightly", - "short": "rustc 1.71.0-nightly (a2b1646c5 2023-05-25)" - }, - "contractCrate": { - "name": "multisig", - "version": "1.0.0", - "gitVersion": "v0.45.2.1-reproducible-169-g37d970c" - }, - "framework": { - "name": "multiversx-sc", - "version": "0.47.2" - } - }, - "docs": [ - "Multi-signature smart contract implementation.", - "Acts like a wallet that needs multiple signers for any action performed.", - "See the readme file for more detailed documentation." - ], "name": "Multisig", "constructor": { - "inputs": [ - { - "name": "quorum", - "type": "u32" - }, - { - "name": "board", - "type": "variadic
", - "multi_arg": true - } - ], - "outputs": [] - }, - "endpoints": [ - { - "name": "upgrade", - "mutability": "mutable", - "inputs": [], - "outputs": [] - }, - { - "docs": [ - "Allows the contract to receive funds even if it is marked as unpayable in the protocol." - ], - "name": "deposit", - "mutability": "mutable", - "payableInTokens": [ - "*" - ], - "inputs": [], - "outputs": [] - }, - { - "docs": [ - "Clears storage pertaining to an action that is no longer supposed to be executed.", - "Any signatures that the action received must first be removed, via `unsign`.", - "Otherwise this endpoint would be prone to abuse." - ], - "name": "discardAction", - "mutability": "mutable", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [] - }, - { - "docs": [ - "Discard all the actions with the given IDs" - ], - "name": "discardBatch", - "mutability": "mutable", - "inputs": [ - { - "name": "action_ids", - "type": "variadic", - "multi_arg": true - } - ], - "outputs": [] - }, - { - "docs": [ - "Minimum number of signatures needed to perform any action." - ], - "name": "getQuorum", - "mutability": "readonly", - "inputs": [], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "docs": [ - "Denormalized board member count.", - "It is kept in sync with the user list by the contract." - ], - "name": "getNumBoardMembers", - "mutability": "readonly", - "inputs": [], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "name": "getNumGroups", - "mutability": "readonly", - "inputs": [], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "docs": [ - "Denormalized proposer count.", - "It is kept in sync with the user list by the contract." - ], - "name": "getNumProposers", - "mutability": "readonly", - "inputs": [], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "name": "getActionGroup", - "mutability": "readonly", - "inputs": [ - { - "name": "group_id", - "type": "u32" - } - ], - "outputs": [ - { - "type": "variadic", - "multi_result": true - } - ] - }, - { - "name": "getLastGroupActionId", - "mutability": "readonly", - "inputs": [], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "docs": [ - "The index of the last proposed action.", - "0 means that no action was ever proposed yet." - ], - "name": "getActionLastIndex", - "mutability": "readonly", - "inputs": [], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "docs": [ - "Initiates board member addition process.", - "Can also be used to promote a proposer to board member." - ], - "name": "proposeAddBoardMember", - "mutability": "mutable", - "inputs": [ - { - "name": "board_member_address", - "type": "Address" - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "docs": [ - "Initiates proposer addition process..", - "Can also be used to demote a board member to proposer." - ], - "name": "proposeAddProposer", - "mutability": "mutable", - "inputs": [ - { - "name": "proposer_address", - "type": "Address" - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "docs": [ - "Removes user regardless of whether it is a board member or proposer." - ], - "name": "proposeRemoveUser", - "mutability": "mutable", - "inputs": [ - { - "name": "user_address", - "type": "Address" - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "name": "proposeChangeQuorum", - "mutability": "mutable", - "inputs": [ - { - "name": "new_quorum", - "type": "u32" - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "docs": [ - "Propose a transaction in which the contract will perform a transfer-execute call.", - "Can send EGLD without calling anything.", - "Can call smart contract endpoints directly.", - "Doesn't really work with builtin functions." - ], - "name": "proposeTransferExecute", - "mutability": "mutable", - "inputs": [ - { - "name": "to", - "type": "Address" - }, - { - "name": "egld_amount", - "type": "BigUint" - }, - { - "name": "opt_gas_limit", - "type": "Option" - }, - { - "name": "function_call", - "type": "variadic", - "multi_arg": true - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "name": "proposeTransferExecuteEsdt", - "mutability": "mutable", - "inputs": [ - { - "name": "to", - "type": "Address" - }, - { - "name": "tokens", - "type": "List" - }, - { - "name": "opt_gas_limit", - "type": "Option" - }, - { - "name": "function_call", - "type": "variadic", - "multi_arg": true - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "docs": [ - "Propose a transaction in which the contract will perform an async call call.", - "Can call smart contract endpoints directly.", - "Can use ESDTTransfer/ESDTNFTTransfer/MultiESDTTransfer to send tokens, while also optionally calling endpoints.", - "Works well with builtin functions.", - "Cannot simply send EGLD directly without calling anything." - ], - "name": "proposeAsyncCall", - "mutability": "mutable", - "inputs": [ - { - "name": "to", - "type": "Address" - }, - { - "name": "egld_amount", - "type": "BigUint" - }, - { - "name": "opt_gas_limit", - "type": "Option" - }, - { - "name": "function_call", - "type": "variadic", - "multi_arg": true - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "name": "proposeSCDeployFromSource", - "mutability": "mutable", - "inputs": [ - { - "name": "amount", - "type": "BigUint" - }, - { - "name": "source", - "type": "Address" - }, - { - "name": "code_metadata", - "type": "CodeMetadata" - }, - { - "name": "arguments", - "type": "variadic", - "multi_arg": true - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "name": "proposeSCUpgradeFromSource", - "mutability": "mutable", - "inputs": [ - { - "name": "sc_address", - "type": "Address" - }, - { - "name": "amount", - "type": "BigUint" - }, - { - "name": "source", - "type": "Address" - }, - { - "name": "code_metadata", - "type": "CodeMetadata" - }, - { - "name": "arguments", - "type": "variadic", - "multi_arg": true - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "name": "proposeBatch", - "mutability": "mutable", - "inputs": [ - { - "name": "actions", - "type": "variadic", - "multi_arg": true - } - ], - "outputs": [ - { - "type": "u32" - } - ] - }, - { - "docs": [ - "Used by board members to sign actions." - ], - "name": "sign", - "mutability": "mutable", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [] - }, + "inputs": [ { - "docs": [ - "Sign all the actions in the given batch" - ], - "name": "signBatch", - "mutability": "mutable", - "inputs": [ - { - "name": "group_id", - "type": "u32" - } - ], - "outputs": [] + "name": "quorum", + "type": "u32" }, { - "name": "signAndPerform", - "mutability": "mutable", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [ - { - "type": "optional
", - "multi_result": true - } - ] - }, - { - "name": "signBatchAndPerform", - "mutability": "mutable", - "inputs": [ - { - "name": "group_id", - "type": "u32" - } - ], - "outputs": [] - }, - { - "docs": [ - "Board members can withdraw their signatures if they no longer desire for the action to be executed.", - "Actions that are left with no valid signatures can be then deleted to free up storage." - ], - "name": "unsign", - "mutability": "mutable", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [] - }, - { - "docs": [ - "Unsign all actions with the given IDs" - ], - "name": "unsignBatch", - "mutability": "mutable", - "inputs": [ - { - "name": "group_id", - "type": "u32" - } - ], - "outputs": [] - }, - { - "docs": [ - "Returns `true` (`1`) if the user has signed the action.", - "Does not check whether or not the user is still a board member and the signature valid." - ], - "name": "signed", - "mutability": "readonly", - "inputs": [ - { - "name": "user", - "type": "Address" - }, - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [ - { - "type": "bool" - } - ] - }, - { - "name": "unsignForOutdatedBoardMembers", - "mutability": "mutable", - "inputs": [ - { - "name": "action_id", - "type": "u32" - }, - { - "name": "outdated_board_members", - "type": "variadic", - "multi_arg": true - } - ], - "outputs": [] - }, - { - "docs": [ - "Returns `true` (`1`) if `getActionValidSignerCount >= getQuorum`." - ], - "name": "quorumReached", - "mutability": "readonly", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [ - { - "type": "bool" - } - ] - }, - { - "docs": [ - "Proposers and board members use this to launch signed actions." - ], - "name": "performAction", - "mutability": "mutable", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [ - { - "type": "optional
", - "multi_result": true - } - ] - }, - { - "docs": [ - "Perform all the actions in the given batch" - ], - "name": "performBatch", - "mutability": "mutable", - "inputs": [ - { - "name": "group_id", - "type": "u32" - } - ], - "outputs": [] - }, - { - "name": "dnsRegister", - "onlyOwner": true, - "mutability": "mutable", - "payableInTokens": [ - "EGLD" - ], - "inputs": [ - { - "name": "dns_address", - "type": "Address" - }, - { - "name": "name", - "type": "bytes" - } - ], - "outputs": [] - }, - { - "docs": [ - "Iterates through all actions and retrieves those that are still pending.", - "Serialized full action data:", - "- the action id", - "- the serialized action data", - "- (number of signers followed by) list of signer addresses." - ], - "name": "getPendingActionFullInfo", - "mutability": "readonly", - "inputs": [ - { - "name": "opt_range", - "type": "optional>", - "multi_arg": true - } - ], - "outputs": [ - { - "type": "variadic", - "multi_result": true - } - ], - "labels": [ - "multisig-external-view" - ], - "allow_multiple_var_args": true - }, - { - "docs": [ - "Indicates user rights.", - "`0` = no rights,", - "`1` = can propose, but not sign,", - "`2` = can propose and sign." - ], - "name": "userRole", - "mutability": "readonly", - "inputs": [ - { - "name": "user", - "type": "Address" - } - ], - "outputs": [ - { - "type": "UserRole" - } - ], - "labels": [ - "multisig-external-view" - ] - }, - { - "docs": [ - "Lists all users that can sign actions." - ], - "name": "getAllBoardMembers", - "mutability": "readonly", - "inputs": [], - "outputs": [ - { - "type": "variadic
", - "multi_result": true - } - ], - "labels": [ - "multisig-external-view" - ] - }, - { - "docs": [ - "Lists all proposers that are not board members." - ], - "name": "getAllProposers", - "mutability": "readonly", - "inputs": [], - "outputs": [ - { - "type": "variadic
", - "multi_result": true - } - ], - "labels": [ - "multisig-external-view" - ] - }, - { - "docs": [ - "Serialized action data of an action with index." - ], - "name": "getActionData", - "mutability": "readonly", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [ - { - "type": "Action" - } - ], - "labels": [ - "multisig-external-view" - ] - }, - { - "docs": [ - "Gets addresses of all users who signed an action.", - "Does not check if those users are still board members or not,", - "so the result may contain invalid signers." - ], - "name": "getActionSigners", - "mutability": "readonly", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [ - { - "type": "List
" - } - ], - "labels": [ - "multisig-external-view" - ] - }, - { - "docs": [ - "Gets addresses of all users who signed an action and are still board members.", - "All these signatures are currently valid." - ], - "name": "getActionSignerCount", - "mutability": "readonly", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [ - { - "type": "u32" - } - ], - "labels": [ - "multisig-external-view" - ] - }, - { - "docs": [ - "It is possible for board members to lose their role.", - "They are not automatically removed from all actions when doing so,", - "therefore the contract needs to re-check every time when actions are performed.", - "This function is used to validate the signers before performing an action.", - "It also makes it easy to check before performing an action." - ], - "name": "getActionValidSignerCount", - "mutability": "readonly", - "inputs": [ - { - "name": "action_id", - "type": "u32" - } - ], - "outputs": [ - { - "type": "u32" - } - ], - "labels": [ - "multisig-external-view" - ] + "name": "board", + "type": "variadic
", + "multi_arg": true } + ], + "outputs": [] + }, + "upgradeConstructor": { + "inputs": [], + "outputs": [] + }, + "endpoints": [ + { + "docs": [ + "Allows the contract to receive funds even if it is marked as unpayable in the protocol." + ], + "name": "deposit", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [], + "outputs": [] + }, + { + "docs": [ + "Clears storage pertaining to an action that is no longer supposed to be executed.", + "Any signatures that the action received must first be removed, via `unsign`.", + "Otherwise this endpoint would be prone to abuse." + ], + "name": "discardAction", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Discard all the actions with the given IDs" + ], + "name": "discardBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "action_ids", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "docs": [ + "Minimum number of signatures needed to perform any action." + ], + "name": "getQuorum", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Denormalized board member count.", + "It is kept in sync with the user list by the contract." + ], + "name": "getNumBoardMembers", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "getNumGroups", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Denormalized proposer count.", + "It is kept in sync with the user list by the contract." + ], + "name": "getNumProposers", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "getActionGroup", + "mutability": "readonly", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "variadic", + "multi_result": true + } + ] + }, + { + "name": "getLastGroupActionId", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "The index of the last proposed action.", + "0 means that no action was ever proposed yet." + ], + "name": "getActionLastIndex", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Initiates board member addition process.", + "Can also be used to promote a proposer to board member." + ], + "name": "proposeAddBoardMember", + "mutability": "mutable", + "inputs": [ + { + "name": "board_member_address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Initiates proposer addition process..", + "Can also be used to demote a board member to proposer." + ], + "name": "proposeAddProposer", + "mutability": "mutable", + "inputs": [ + { + "name": "proposer_address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Removes user regardless of whether it is a board member or proposer." + ], + "name": "proposeRemoveUser", + "mutability": "mutable", + "inputs": [ + { + "name": "user_address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeChangeQuorum", + "mutability": "mutable", + "inputs": [ + { + "name": "new_quorum", + "type": "u32" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Propose a transaction in which the contract will perform a transfer-execute call.", + "Can send EGLD without calling anything.", + "Can call smart contract endpoints directly.", + "Doesn't really work with builtin functions." + ], + "name": "proposeTransferExecute", + "mutability": "mutable", + "inputs": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "egld_amount", + "type": "BigUint" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "function_call", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeTransferExecuteEsdt", + "mutability": "mutable", + "inputs": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "tokens", + "type": "List" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "function_call", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Propose a transaction in which the contract will perform an async call call.", + "Can call smart contract endpoints directly.", + "Can use ESDTTransfer/ESDTNFTTransfer/MultiESDTTransfer to send tokens, while also optionally calling endpoints.", + "Works well with builtin functions.", + "Cannot simply send EGLD directly without calling anything." + ], + "name": "proposeAsyncCall", + "mutability": "mutable", + "inputs": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "egld_amount", + "type": "BigUint" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "function_call", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeSCDeployFromSource", + "mutability": "mutable", + "inputs": [ + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "source", + "type": "Address" + }, + { + "name": "code_metadata", + "type": "CodeMetadata" + }, + { + "name": "arguments", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeSCUpgradeFromSource", + "mutability": "mutable", + "inputs": [ + { + "name": "sc_address", + "type": "Address" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "source", + "type": "Address" + }, + { + "name": "code_metadata", + "type": "CodeMetadata" + }, + { + "name": "arguments", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "actions", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Used by board members to sign actions." + ], + "name": "sign", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Sign all the actions in the given batch" + ], + "name": "signBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "name": "signAndPerform", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "optional
", + "multi_result": true + } + ] + }, + { + "name": "signBatchAndPerform", + "mutability": "mutable", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Board members can withdraw their signatures if they no longer desire for the action to be executed.", + "Actions that are left with no valid signatures can be then deleted to free up storage." + ], + "name": "unsign", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Unsign all actions with the given IDs" + ], + "name": "unsignBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Returns `true` (`1`) if the user has signed the action.", + "Does not check whether or not the user is still a board member and the signature valid." + ], + "name": "signed", + "mutability": "readonly", + "inputs": [ + { + "name": "user", + "type": "Address" + }, + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "unsignForOutdatedBoardMembers", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + }, + { + "name": "outdated_board_members", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "docs": [ + "Returns `true` (`1`) if `getActionValidSignerCount >= getQuorum`." + ], + "name": "quorumReached", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "docs": [ + "Proposers and board members use this to launch signed actions." + ], + "name": "performAction", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "optional
", + "multi_result": true + } + ] + }, + { + "docs": [ + "Perform all the actions in the given batch" + ], + "name": "performBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "name": "dnsRegister", + "onlyOwner": true, + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "dns_address", + "type": "Address" + }, + { + "name": "name", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "docs": [ + "Iterates through all actions and retrieves those that are still pending.", + "Serialized full action data:", + "- the action id", + "- the serialized action data", + "- (number of signers followed by) list of signer addresses." + ], + "name": "getPendingActionFullInfo", + "mutability": "readonly", + "inputs": [ + { + "name": "opt_range", + "type": "optional>", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "variadic", + "multi_result": true + } + ], + "labels": [ + "multisig-external-view" + ], + "allow_multiple_var_args": true + }, + { + "docs": [ + "Indicates user rights.", + "`0` = no rights,", + "`1` = can propose, but not sign,", + "`2` = can propose and sign." + ], + "name": "userRole", + "mutability": "readonly", + "inputs": [ + { + "name": "user", + "type": "Address" + } + ], + "outputs": [ + { + "type": "UserRole" + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Lists all users that can sign actions." + ], + "name": "getAllBoardMembers", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Lists all proposers that are not board members." + ], + "name": "getAllProposers", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Serialized action data of an action with index." + ], + "name": "getActionData", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "Action" + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Gets addresses of all users who signed an action.", + "Does not check if those users are still board members or not,", + "so the result may contain invalid signers." + ], + "name": "getActionSigners", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "List
" + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Gets addresses of all users who signed an action and are still board members.", + "All these signatures are currently valid." + ], + "name": "getActionSignerCount", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "u32" + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "It is possible for board members to lose their role.", + "They are not automatically removed from all actions when doing so,", + "therefore the contract needs to re-check every time when actions are performed.", + "This function is used to validate the signers before performing an action.", + "It also makes it easy to check before performing an action." + ], + "name": "getActionValidSignerCount", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "u32" + } + ], + "labels": [ + "multisig-external-view" + ] + } ], "events": [ - { - "identifier": "asyncCallSuccess", - "inputs": [ - { - "name": "results", - "type": "variadic", - "indexed": true - } - ] - }, - { - "identifier": "asyncCallError", - "inputs": [ - { - "name": "err_code", - "type": "u32", - "indexed": true - }, - { - "name": "err_message", - "type": "bytes", - "indexed": true - } - ] - }, - { - "identifier": "startPerformAction", - "inputs": [ - { - "name": "data", - "type": "ActionFullInfo" - } - ] - }, - { - "identifier": "performChangeUser", - "inputs": [ - { - "name": "action_id", - "type": "u32", - "indexed": true - }, - { - "name": "changed_user", - "type": "Address", - "indexed": true - }, - { - "name": "old_role", - "type": "UserRole", - "indexed": true - }, - { - "name": "new_role", - "type": "UserRole", - "indexed": true - } - ] - }, - { - "identifier": "performChangeQuorum", - "inputs": [ - { - "name": "action_id", - "type": "u32", - "indexed": true - }, - { - "name": "new_quorum", - "type": "u32", - "indexed": true - } - ] - }, - { - "identifier": "performAsyncCall", - "inputs": [ - { - "name": "action_id", - "type": "u32", - "indexed": true - }, - { - "name": "to", - "type": "Address", - "indexed": true - }, - { - "name": "egld_value", - "type": "BigUint", - "indexed": true - }, - { - "name": "gas", - "type": "u64", - "indexed": true - }, - { - "name": "endpoint", - "type": "bytes", - "indexed": true - }, - { - "name": "arguments", - "type": "variadic", - "indexed": true - } - ] - }, - { - "identifier": "performTransferExecuteEgld", - "inputs": [ - { - "name": "action_id", - "type": "u32", - "indexed": true - }, - { - "name": "to", - "type": "Address", - "indexed": true - }, - { - "name": "egld_value", - "type": "BigUint", - "indexed": true - }, - { - "name": "gas", - "type": "u64", - "indexed": true - }, - { - "name": "endpoint", - "type": "bytes", - "indexed": true - }, - { - "name": "arguments", - "type": "variadic", - "indexed": true - } - ] - }, - { - "identifier": "performTransferExecuteEsdt", - "inputs": [ - { - "name": "action_id", - "type": "u32", - "indexed": true - }, - { - "name": "to", - "type": "Address", - "indexed": true - }, - { - "name": "tokens", - "type": "List", - "indexed": true - }, - { - "name": "gas", - "type": "u64", - "indexed": true - }, - { - "name": "endpoint", - "type": "bytes", - "indexed": true - }, - { - "name": "arguments", - "type": "variadic", - "indexed": true - } - ] - }, - { - "identifier": "performDeployFromSource", - "inputs": [ - { - "name": "action_id", - "type": "u32", - "indexed": true - }, - { - "name": "egld_value", - "type": "BigUint", - "indexed": true - }, - { - "name": "source_address", - "type": "Address", - "indexed": true - }, - { - "name": "code_metadata", - "type": "CodeMetadata", - "indexed": true - }, - { - "name": "gas", - "type": "u64", - "indexed": true - }, - { - "name": "arguments", - "type": "variadic", - "indexed": true - } - ] - }, - { - "identifier": "performUpgradeFromSource", - "inputs": [ - { - "name": "action_id", - "type": "u32", - "indexed": true - }, - { - "name": "target_address", - "type": "Address", - "indexed": true - }, - { - "name": "egld_value", - "type": "BigUint", - "indexed": true - }, - { - "name": "source_address", - "type": "Address", - "indexed": true - }, - { - "name": "code_metadata", - "type": "CodeMetadata", - "indexed": true - }, - { - "name": "gas", - "type": "u64", - "indexed": true - }, - { - "name": "arguments", - "type": "variadic", - "indexed": true - } - ] - } + { + "identifier": "asyncCallSuccess", + "inputs": [ + { + "name": "results", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "asyncCallError", + "inputs": [ + { + "name": "err_code", + "type": "u32", + "indexed": true + }, + { + "name": "err_message", + "type": "bytes", + "indexed": true + } + ] + }, + { + "identifier": "startPerformAction", + "inputs": [ + { + "name": "data", + "type": "ActionFullInfo" + } + ] + }, + { + "identifier": "performChangeUser", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "changed_user", + "type": "Address", + "indexed": true + }, + { + "name": "old_role", + "type": "UserRole", + "indexed": true + }, + { + "name": "new_role", + "type": "UserRole", + "indexed": true + } + ] + }, + { + "identifier": "performChangeQuorum", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "new_quorum", + "type": "u32", + "indexed": true + } + ] + }, + { + "identifier": "performAsyncCall", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "to", + "type": "Address", + "indexed": true + }, + { + "name": "egld_value", + "type": "BigUint", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "endpoint", + "type": "bytes", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "performTransferExecuteEgld", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "to", + "type": "Address", + "indexed": true + }, + { + "name": "egld_value", + "type": "BigUint", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "endpoint", + "type": "bytes", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "performTransferExecuteEsdt", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "to", + "type": "Address", + "indexed": true + }, + { + "name": "tokens", + "type": "List", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "endpoint", + "type": "bytes", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "performDeployFromSource", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "egld_value", + "type": "BigUint", + "indexed": true + }, + { + "name": "source_address", + "type": "Address", + "indexed": true + }, + { + "name": "code_metadata", + "type": "CodeMetadata", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "performUpgradeFromSource", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "target_address", + "type": "Address", + "indexed": true + }, + { + "name": "egld_value", + "type": "BigUint", + "indexed": true + }, + { + "name": "source_address", + "type": "Address", + "indexed": true + }, + { + "name": "code_metadata", + "type": "CodeMetadata", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + } ], "esdtAttributes": [], "hasCallback": true, "types": { - "Action": { - "type": "enum", - "variants": [ - { - "name": "Nothing", - "discriminant": 0 - }, - { - "name": "AddBoardMember", - "discriminant": 1, - "fields": [ - { - "name": "0", - "type": "Address" - } - ] - }, - { - "name": "AddProposer", - "discriminant": 2, - "fields": [ - { - "name": "0", - "type": "Address" - } - ] - }, - { - "name": "RemoveUser", - "discriminant": 3, - "fields": [ - { - "name": "0", - "type": "Address" - } - ] - }, - { - "name": "ChangeQuorum", - "discriminant": 4, - "fields": [ - { - "name": "0", - "type": "u32" - } - ] - }, - { - "name": "SendTransferExecuteEgld", - "discriminant": 5, - "fields": [ - { - "name": "0", - "type": "CallActionData" - } - ] - }, - { - "name": "SendTransferExecuteEsdt", - "discriminant": 6, - "fields": [ - { - "name": "0", - "type": "EsdtTransferExecuteData" - } - ] - }, - { - "name": "SendAsyncCall", - "discriminant": 7, - "fields": [ - { - "name": "0", - "type": "CallActionData" - } - ] - }, - { - "name": "SCDeployFromSource", - "discriminant": 8, - "fields": [ - { - "name": "amount", - "type": "BigUint" - }, - { - "name": "source", - "type": "Address" - }, - { - "name": "code_metadata", - "type": "CodeMetadata" - }, - { - "name": "arguments", - "type": "List" - } - ] - }, - { - "name": "SCUpgradeFromSource", - "discriminant": 9, - "fields": [ - { - "name": "sc_address", - "type": "Address" - }, - { - "name": "amount", - "type": "BigUint" - }, - { - "name": "source", - "type": "Address" - }, - { - "name": "code_metadata", - "type": "CodeMetadata" - }, - { - "name": "arguments", - "type": "List" - } - ] - } + "Action": { + "type": "enum", + "variants": [ + { + "name": "Nothing", + "discriminant": 0 + }, + { + "name": "AddBoardMember", + "discriminant": 1, + "fields": [ + { + "name": "0", + "type": "Address" + } ] - }, - "ActionFullInfo": { - "type": "struct", - "docs": [ - "Not used internally, just to retrieve results via endpoint." - ], + }, + { + "name": "AddProposer", + "discriminant": 2, "fields": [ - { - "name": "action_id", - "type": "u32" - }, - { - "name": "group_id", - "type": "u32" - }, - { - "name": "action_data", - "type": "Action" - }, - { - "name": "signers", - "type": "List
" - } + { + "name": "0", + "type": "Address" + } ] - }, - "ActionStatus": { - "type": "enum", - "variants": [ - { - "name": "Available", - "discriminant": 0 - }, - { - "name": "Aborted", - "discriminant": 1 - } + }, + { + "name": "RemoveUser", + "discriminant": 3, + "fields": [ + { + "name": "0", + "type": "Address" + } ] - }, - "CallActionData": { - "type": "struct", + }, + { + "name": "ChangeQuorum", + "discriminant": 4, "fields": [ - { - "name": "to", - "type": "Address" - }, - { - "name": "egld_amount", - "type": "BigUint" - }, - { - "name": "opt_gas_limit", - "type": "Option" - }, - { - "name": "endpoint_name", - "type": "bytes" - }, - { - "name": "arguments", - "type": "List" - } + { + "name": "0", + "type": "u32" + } ] - }, - "EsdtTokenPayment": { - "type": "struct", + }, + { + "name": "SendTransferExecuteEgld", + "discriminant": 5, "fields": [ - { - "name": "token_identifier", - "type": "TokenIdentifier" - }, - { - "name": "token_nonce", - "type": "u64" - }, - { - "name": "amount", - "type": "BigUint" - } + { + "name": "0", + "type": "CallActionData" + } ] - }, - "EsdtTransferExecuteData": { - "type": "struct", + }, + { + "name": "SendTransferExecuteEsdt", + "discriminant": 6, "fields": [ - { - "name": "to", - "type": "Address" - }, - { - "name": "tokens", - "type": "List" - }, - { - "name": "opt_gas_limit", - "type": "Option" - }, - { - "name": "endpoint_name", - "type": "bytes" - }, - { - "name": "arguments", - "type": "List" - } + { + "name": "0", + "type": "EsdtTransferExecuteData" + } ] - }, - "UserRole": { - "type": "enum", - "variants": [ - { - "name": "None", - "discriminant": 0 - }, - { - "name": "Proposer", - "discriminant": 1 - }, - { - "name": "BoardMember", - "discriminant": 2 - } + }, + { + "name": "SendAsyncCall", + "discriminant": 7, + "fields": [ + { + "name": "0", + "type": "CallActionData" + } ] - } + }, + { + "name": "SCDeployFromSource", + "discriminant": 8, + "fields": [ + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "source", + "type": "Address" + }, + { + "name": "code_metadata", + "type": "CodeMetadata" + }, + { + "name": "arguments", + "type": "List" + } + ] + }, + { + "name": "SCUpgradeFromSource", + "discriminant": 9, + "fields": [ + { + "name": "sc_address", + "type": "Address" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "source", + "type": "Address" + }, + { + "name": "code_metadata", + "type": "CodeMetadata" + }, + { + "name": "arguments", + "type": "List" + } + ] + } + ] + }, + "ActionFullInfo": { + "type": "struct", + "docs": [ + "Not used internally, just to retrieve results via endpoint." + ], + "fields": [ + { + "name": "action_id", + "type": "u32" + }, + { + "name": "group_id", + "type": "u32" + }, + { + "name": "action_data", + "type": "Action" + }, + { + "name": "signers", + "type": "List
" + } + ] + }, + "ActionStatus": { + "type": "enum", + "variants": [ + { + "name": "Available", + "discriminant": 0 + }, + { + "name": "Aborted", + "discriminant": 1 + } + ] + }, + "CallActionData": { + "type": "struct", + "fields": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "egld_amount", + "type": "BigUint" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "endpoint_name", + "type": "bytes" + }, + { + "name": "arguments", + "type": "List" + } + ] + }, + "EsdtTokenPayment": { + "type": "struct", + "fields": [ + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, + { + "name": "token_nonce", + "type": "u64" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + }, + "EsdtTransferExecuteData": { + "type": "struct", + "fields": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "tokens", + "type": "List" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "endpoint_name", + "type": "bytes" + }, + { + "name": "arguments", + "type": "List" + } + ] + }, + "UserRole": { + "type": "enum", + "variants": [ + { + "name": "None", + "discriminant": 0 + }, + { + "name": "Proposer", + "discriminant": 1 + }, + { + "name": "BoardMember", + "discriminant": 2 + } + ] + } } -} + } \ No newline at end of file