diff --git a/package-lock.json b/package-lock.json index cf06e411..877e8a78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@multiversx/sdk-core", - "version": "14.1.0-beta.0", + "version": "14.1.0-beta.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@multiversx/sdk-core", - "version": "14.1.0-beta.0", + "version": "14.1.0-beta.1", "license": "MIT", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", diff --git a/package.json b/package.json index ddc442e9..90ff967e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-core", - "version": "14.1.0-beta.0", + "version": "14.1.0-beta.1", "description": "MultiversX SDK for JavaScript and TypeScript", "author": "MultiversX", "homepage": "https://multiversx.com", diff --git a/src/multisig/multisigController.spec.ts b/src/multisig/multisigController.spec.ts new file mode 100644 index 00000000..501678a9 --- /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.funcionName, "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 00000000..61053f87 --- /dev/null +++ b/src/multisig/multisigController.ts @@ -0,0 +1,837 @@ +import BigNumber from "bignumber.js"; +import { Abi } from "../abi"; +import { + Address, + BaseControllerInput, + IAccount, + Transaction, + TransactionsFactoryConfig, + TransactionWatcher, +} from "../core"; +import { INetworkProvider } from "../networkProviders/interface"; +import { SmartContractController } from "../smartContracts"; +import { MultisigTransactionsFactory } from "./multisigTransactionsFactory"; +import { MultisigTransactionsOutcomeParser } from "./multisigTransactionsOutcomeParser"; +import * as resources from "./resources"; + +export class MultisigController extends SmartContractController { + private transactionAwaiter: TransactionWatcher; + private multisigFactory: MultisigTransactionsFactory; + private multisigParser: MultisigTransactionsOutcomeParser; + + constructor(options: { chainID: string; networkProvider: INetworkProvider; abi: Abi }) { + super(options); + this.abi = options.abi; + 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 }); + } + + /** + * Creates a transaction for deploying a new multisig contract + */ + async createTransactionForMultisigDeploy( + sender: IAccount, + nonce: bigint, + options: resources.DeployMultisigContractInput & BaseControllerInput, + ): Promise { + const transaction = this.multisigFactory.createTransactionForMultisigDeploy(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; + } + + /** + * Gets quorum for specific multisig + */ + async getQuorum(options: { mutisigAddress: string }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getQuorum", + arguments: [], + }); + return Number(response[0].toString()); + } + + /** + * Gets number of board members for specific multisig + */ + async getNumBoardMembers(options: { mutisigAddress: string }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getNumBoardMembers", + arguments: [], + }); + + return Number(response[0].toString()); + } + + /** + * Gets number of groups for specific multisig + */ + async getNumGroups(options: { mutisigAddress: string }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getNumGroups", + arguments: [], + }); + + return Number(response[0].toString()); + } + + /** + * Gets number of proposers for specific multisig + */ + async getNumProposers(options: { mutisigAddress: string }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getNumProposers", + arguments: [], + }); + + return Number(response[0].toString()); + } + + /** + * Gets action group for specific multisig + */ + async getActionGroup(options: { mutisigAddress: string; groupId: number }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionGroup", + arguments: [options.groupId], + }); + + return response[0].map((n: BigNumber) => Number(n.toString())); + } + + /** + * Gets last group action id specific multisig + */ + async getLastGroupActionId(options: { mutisigAddress: string }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getLastGroupActionId", + arguments: [], + }); + + return Number(response[0].toString()); + } + + /** + * Gets last action index specific multisig + */ + async getActionLastIndex(options: { mutisigAddress: string }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionLastIndex", + arguments: [], + }); + + return Number(response[0].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 response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "signed", + arguments: [Address.newFromBech32(options.userAddress), options.actionId], + }); + + return response[0]; + } + + /** + * Returns `true` (`1`) if `getActionValidSignerCount >= getQuorum`. + */ + async quorumReached(options: { mutisigAddress: string; actionId: number }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "quorumReached", + arguments: [options.actionId], + }); + + return response[0]; + } + + /** + * Lists all users that can sign actions. + */ + async getAllBoardMembers(options: { mutisigAddress: string }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getAllBoardMembers", + arguments: [], + }); + + return response[0].map((address: Address) => address?.toBech32()); + } + + /** + * Lists all proposers that are not board members. + */ + async getAllProposers(options: { mutisigAddress: string }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getAllProposers", + arguments: [], + }); + + return response[0].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 response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "userRole", + arguments: [Address.newFromBech32(options.userAddress)], + }); + const userRole = response[0].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 response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionData", + arguments: [options.actionId], + }); + const result = this.mapResponseToAction(response[0].valueOf()); + return result; + } + + /** + * Gets all pending actions. + */ + async getPendingActionFullInfo(options: { mutisigAddress: string }): Promise { + const response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getPendingActionFullInfo", + arguments: [], + }); + + const result: resources.FullMultisigAction[] = []; + const actions = response[0]; + 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).toBech32()), + }); + } + 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.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 response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionSignerCount", + arguments: [options.actionId], + }); + + return response[0]; + } + + /** + * 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 response = await this.query({ + contract: Address.newFromBech32(options.mutisigAddress), + function: "getActionValidSignerCount", + arguments: [options.actionId], + }); + + return Number(response[0].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; + } + + /** + * Awaits the completion of a propose add board member action + */ + async awaitCompletedProposeAddBoardMember(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(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; + } + + /** + * Awaits the completion of a propose add proposer action + */ + async awaitCompletedProposeAddProposer(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(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; + } + + /** + * Awaits the completion of a propose remove user action + */ + async awaitCompletedProposeRemoveUser(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(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; + } + + /** + * Awaits the completion of a propose change quorum action + */ + async awaitCompletedProposeChangeQuorum(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; + } + + /** + * Awaits the completion of a sign action + */ + async awaitCompletedSignAction(txHash: string): Promise { + await this.transactionAwaiter.awaitCompleted(txHash); + } + + /** + * 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; + } + + /** + * Awaits the completion of a perform action + */ + 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; + } + + /** + * Awaits the completion of an unsign action + */ + async awaitCompletedUnsignAction(txHash: string): Promise { + await this.transactionAwaiter.awaitCompleted(txHash); + } + + /** + * 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; + } + + /** + * Awaits the completion of a discard action + */ + async awaitCompletedDiscardAction(txHash: string): Promise { + await this.transactionAwaiter.awaitCompleted(txHash); + } + + /** + * 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; + } + + /** + * Awaits the completion of a propose transfer execute action + */ + async awaitCompletedDepositExecute(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(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; + } + + /** + * Awaits the completion of a propose transfer execute action + */ + async awaitCompletedProposeTransferExecute(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(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; + } + + /** + * Awaits the completion of a propose transfer execute ESDT action + */ + async awaitCompletedProposeTransferExecuteEsdt(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(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; + } + + /** + * Awaits the completion of a propose async call action + */ + async awaitCompletedProposeAsyncCall(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(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; + } + + /** + * Awaits the completion of a propose Contract deploy from source action + */ + async awaitCompletedProposeContractDeployFromSource(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(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; + } + + /** + * Awaits the completion of a propose Contract upgrade from source action + */ + async awaitCompletedProposeContractUpgradeFromSource(txHash: string): Promise { + const transaction = await this.transactionAwaiter.awaitCompleted(txHash); + return this.multisigParser.parseProposeAction(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; + } + + /** + * Awaits the completion of a sign batch action + */ + async awaitCompletedSignBatch(txHash: string): Promise { + await this.transactionAwaiter.awaitCompleted(txHash); + } + + /** + * 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; + } + + /** + * Awaits the completion of a sign and perform action + */ + async awaitCompletedSignAndPerform(txHash: string): Promise { + await this.transactionAwaiter.awaitCompleted(txHash); + } + + /** + * 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; + } + + /** + * Awaits the completion of an unsign for outdated board members action + */ + async awaitCompletedUnsignForOutdatedBoardMembers(txHash: string): Promise { + await this.transactionAwaiter.awaitCompleted(txHash); + } + + /** + * 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; + } + + /** + * Awaits the completion of a perform action batch + */ + async awaitCompletedPerformActionBatch(txHash: string): Promise { + await this.transactionAwaiter.awaitCompleted(txHash); + } + + /** + * 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; + } + + /** + * Awaits the completion of a discard batch action + */ + async awaitCompletedDiscardBatch(txHash: string): Promise { + await this.transactionAwaiter.awaitCompleted(txHash); + } + + 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 index 73827a6b..8b20a99b 100644 --- a/src/multisig/multisigTransactionsFactory.spec.ts +++ b/src/multisig/multisigTransactionsFactory.spec.ts @@ -290,7 +290,7 @@ describe("test multisig transactions factory", function () { "erd1qqqqqqqqqqqqqpgq0cjuum0t436gmp446wf3yz43avp2gm2czeus8mctaf", ); - const transaction = factory.createTransactionForProposeSCDeployFromSource(senderAddress, { + const transaction = factory.createTransactionForProposeContractDeployFromSource(senderAddress, { multisigContract: multisigContractAddress, gasLimit: 5000000n, amount: amount, @@ -318,7 +318,7 @@ describe("test multisig transactions factory", function () { "erd1qqqqqqqqqqqqqpgq0cjuum0t436gmp446wf3yz43avp2gm2czeus8mctaf", ); - const transaction = factory.createTransactionForProposeSCUpgradeFromSource(senderAddress, { + const transaction = factory.createTransactionForProposeContractUpgradeFromSource(senderAddress, { multisigContract: multisigContractAddress, gasLimit: 5000000n, scAddress: multisigContractAddress, diff --git a/src/multisig/multisigTransactionsFactory.ts b/src/multisig/multisigTransactionsFactory.ts index d0870a12..1ecd4e2e 100644 --- a/src/multisig/multisigTransactionsFactory.ts +++ b/src/multisig/multisigTransactionsFactory.ts @@ -28,7 +28,7 @@ import * as resources from "./resources"; export class MultisigTransactionsFactory extends SmartContractTransactionsFactory { private readonly argSerializer: ArgSerializer; - constructor(options: { config: TransactionsFactoryConfig; abi?: Abi }) { + constructor(options: { config: TransactionsFactoryConfig; abi: Abi }) { super(options); this.argSerializer = new ArgSerializer(); } @@ -265,9 +265,9 @@ export class MultisigTransactionsFactory extends SmartContractTransactionsFactor /** * Proposes deploying a smart contract from source */ - createTransactionForProposeSCDeployFromSource( + createTransactionForProposeContractDeployFromSource( sender: Address, - options: resources.ProposeSCDeployFromSourceInput, + options: resources.ProposeContractDeployFromSourceInput, ): Transaction { const dataParts = [ "proposeSCDeployFromSource", @@ -289,9 +289,9 @@ export class MultisigTransactionsFactory extends SmartContractTransactionsFactor /** * Proposes upgrading a smart contract from source */ - createTransactionForProposeSCUpgradeFromSource( + createTransactionForProposeContractUpgradeFromSource( sender: Address, - options: resources.ProposeSCUpgradeFromSourceInput, + options: resources.ProposeContractUpgradeFromSourceInput, ): Transaction { const dataParts = [ "proposeSCUpgradeFromSource", diff --git a/src/multisig/multisigTransactionsOutcomeParser.ts b/src/multisig/multisigTransactionsOutcomeParser.ts new file mode 100644 index 00000000..d595bf4f --- /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/resources.ts b/src/multisig/resources.ts index d71f4673..1b06a9aa 100644 --- a/src/multisig/resources.ts +++ b/src/multisig/resources.ts @@ -1,5 +1,5 @@ import { Abi } from "../abi"; -import { TokenTransfer } from "../core"; +import { Token, TokenTransfer } from "../core"; import { Address } from "../core/address"; import { CodeMetadata } from "../core/codeMetadata"; @@ -70,14 +70,14 @@ export type ProposeAsyncCallInput = MultisigContractInput & { abi?: Abi; }; -export type ProposeSCDeployFromSourceInput = MultisigContractInput & { +export type ProposeContractDeployFromSourceInput = MultisigContractInput & { amount: bigint; source: Address; codeMetadata: CodeMetadata; arguments: string[]; }; -export type ProposeSCUpgradeFromSourceInput = MultisigContractInput & { +export type ProposeContractUpgradeFromSourceInput = MultisigContractInput & { scAddress: Address; amount: bigint; source: Address; @@ -101,6 +101,168 @@ 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?: number; + funcionName: string; + arguments: Uint8Array[]; + + constructor(data: any) { + super(); + this.type = MultisigActionEnum.SendTransferExecuteEgld; + this.receiver = data.to; + this.amount = data.egld_amount; + this.optionalGasLimit = data.opt_gas_limit; + this.funcionName = data.endpoint_name.toString(); + this.arguments = data.arguments; + } +} +export class SendTransferExecuteEsdt extends MultisigAction { + receiver: Address; + tokens: TokenTransfer[]; + optionalGasLimit?: number; + 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 = data.opt_gas_limit; + + this.funcionName = Buffer.from(data.endpoint_name.toString(), "hex").toString(); + this.arguments = data.arguments; + } +} + +export class SendAsyncCall extends MultisigAction { + receiver: Address; + amount: bigint; + optionalGasLimit?: number; + funcionName: string; + arguments: Uint8Array[]; + + constructor(data: any) { + super(); + this.type = MultisigActionEnum.SendAsyncCall; + this.receiver = data.to; + this.amount = data.egld_amount; + this.optionalGasLimit = data.opt_gas_limit; + 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; diff --git a/src/smartContracts/smartContractController.ts b/src/smartContracts/smartContractController.ts index cd370bbc..59efe28f 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();