diff --git a/examples/contracts/multisig-full.abi.json b/examples/contracts/multisig-full.abi.json index f0fdca90..74a448a7 100644 --- a/examples/contracts/multisig-full.abi.json +++ b/examples/contracts/multisig-full.abi.json @@ -14,20 +14,20 @@ ], "outputs": [] }, + "upgradeConstructor": { + "inputs": [], + "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": ["*"], + "payableInTokens": [ + "*" + ], "inputs": [], "outputs": [] }, @@ -48,7 +48,9 @@ "outputs": [] }, { - "docs": ["Discard all the actions with the given IDs"], + "docs": [ + "Discard all the actions with the given IDs" + ], "name": "discardBatch", "mutability": "mutable", "inputs": [ @@ -61,7 +63,9 @@ "outputs": [] }, { - "docs": ["Minimum number of signatures needed to perform any action."], + "docs": [ + "Minimum number of signatures needed to perform any action." + ], "name": "getQuorum", "mutability": "readonly", "inputs": [], @@ -394,7 +398,9 @@ ] }, { - "docs": ["Used by board members to sign actions."], + "docs": [ + "Used by board members to sign actions." + ], "name": "sign", "mutability": "mutable", "inputs": [ @@ -406,7 +412,9 @@ "outputs": [] }, { - "docs": ["Sign all the actions in the given batch"], + "docs": [ + "Sign all the actions in the given batch" + ], "name": "signBatch", "mutability": "mutable", "inputs": [ @@ -460,7 +468,9 @@ "outputs": [] }, { - "docs": ["Unsign all actions with the given IDs"], + "docs": [ + "Unsign all actions with the given IDs" + ], "name": "unsignBatch", "mutability": "mutable", "inputs": [ @@ -548,7 +558,9 @@ ] }, { - "docs": ["Perform all the actions in the given batch"], + "docs": [ + "Perform all the actions in the given batch" + ], "name": "performBatch", "mutability": "mutable", "inputs": [ @@ -563,7 +575,9 @@ "name": "dnsRegister", "onlyOwner": true, "mutability": "mutable", - "payableInTokens": ["EGLD"], + "payableInTokens": [ + "EGLD" + ], "inputs": [ { "name": "dns_address", @@ -599,7 +613,9 @@ "multi_result": true } ], - "labels": ["multisig-external-view"], + "labels": [ + "multisig-external-view" + ], "allow_multiple_var_args": true }, { @@ -622,10 +638,14 @@ "type": "UserRole" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { - "docs": ["Lists all users that can sign actions."], + "docs": [ + "Lists all users that can sign actions." + ], "name": "getAllBoardMembers", "mutability": "readonly", "inputs": [], @@ -635,10 +655,14 @@ "multi_result": true } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { - "docs": ["Lists all proposers that are not board members."], + "docs": [ + "Lists all proposers that are not board members." + ], "name": "getAllProposers", "mutability": "readonly", "inputs": [], @@ -648,10 +672,14 @@ "multi_result": true } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { - "docs": ["Serialized action data of an action with index."], + "docs": [ + "Serialized action data of an action with index." + ], "name": "getActionData", "mutability": "readonly", "inputs": [ @@ -665,7 +693,9 @@ "type": "Action" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { "docs": [ @@ -686,7 +716,9 @@ "type": "List
" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { "docs": [ @@ -706,7 +738,9 @@ "type": "u32" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { "docs": [ @@ -729,7 +763,9 @@ "type": "u32" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] } ], "events": [ @@ -1120,7 +1156,9 @@ }, "ActionFullInfo": { "type": "struct", - "docs": ["Not used internally, just to retrieve results via endpoint."], + "docs": [ + "Not used internally, just to retrieve results via endpoint." + ], "fields": [ { "name": "action_id", diff --git a/multiversx_sdk/core/transaction_events_parser_test.py b/multiversx_sdk/core/transaction_events_parser_test.py index 72662f93..e9fff99e 100644 --- a/multiversx_sdk/core/transaction_events_parser_test.py +++ b/multiversx_sdk/core/transaction_events_parser_test.py @@ -285,6 +285,8 @@ def test_multisig_start_perform_action(): '__discriminant__': 5 }, ), - signers=[Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th").get_public_key(), - Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx").get_public_key()] + signers=[ + Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th").get_public_key(), + Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx").get_public_key() + ] ) diff --git a/multiversx_sdk/entrypoints/config.py b/multiversx_sdk/entrypoints/config.py index b1896803..ed817f91 100644 --- a/multiversx_sdk/entrypoints/config.py +++ b/multiversx_sdk/entrypoints/config.py @@ -1,6 +1,13 @@ from dataclasses import dataclass +@dataclass +class LocalnetEntrypointConfig: + network_provider_url = "http://localhost:7950" + network_provider_kind = "proxy" + chain_id = "localnet" + + @dataclass class TestnetEntrypointConfig: network_provider_url = "https://testnet-api.multiversx.com" diff --git a/multiversx_sdk/entrypoints/entrypoints.py b/multiversx_sdk/entrypoints/entrypoints.py index 8cc8c0c8..66c40f0b 100644 --- a/multiversx_sdk/entrypoints/entrypoints.py +++ b/multiversx_sdk/entrypoints/entrypoints.py @@ -14,6 +14,7 @@ from multiversx_sdk.delegation.delegation_transactions_factory import \ DelegationTransactionsFactory from multiversx_sdk.entrypoints.config import (DevnetEntrypointConfig, + LocalnetEntrypointConfig, MainnetEntrypointConfig, TestnetEntrypointConfig) from multiversx_sdk.entrypoints.errors import InvalidNetworkProviderKindError @@ -142,28 +143,29 @@ def create_transfers_transactions_factory(self) -> TransferTransactionsFactory: return TransferTransactionsFactory(TransactionsFactoryConfig(self.chain_id)) +class LocalnetEntrypoint(NetworkEntrypoint): + def __init__(self, url: Optional[str] = None, kind: Optional[str] = None) -> None: + url = url or LocalnetEntrypointConfig.network_provider_url + kind = kind or LocalnetEntrypointConfig.network_provider_kind + super().__init__(url, kind, LocalnetEntrypointConfig.chain_id) + + class TestnetEntrypoint(NetworkEntrypoint): def __init__(self, url: Optional[str] = None, kind: Optional[str] = None) -> None: url = url or TestnetEntrypointConfig.network_provider_url - kind = kind or TestnetEntrypointConfig.network_provider_kind - super().__init__(url, kind, TestnetEntrypointConfig.chain_id) class DevnetEntrypoint(NetworkEntrypoint): def __init__(self, url: Optional[str] = None, kind: Optional[str] = None) -> None: url = url or DevnetEntrypointConfig.network_provider_url - kind = kind or DevnetEntrypointConfig.network_provider_kind - super().__init__(url, kind, DevnetEntrypointConfig.chain_id) class MainnetEntrypoint(NetworkEntrypoint): def __init__(self, url: Optional[str] = None, kind: Optional[str] = None) -> None: url = url or MainnetEntrypointConfig.network_provider_url - kind = kind or MainnetEntrypointConfig.network_provider_kind - super().__init__(url, kind, MainnetEntrypointConfig.chain_id) diff --git a/multiversx_sdk/entrypoints/entrypoints_multisig_test.py b/multiversx_sdk/entrypoints/entrypoints_multisig_test.py new file mode 100644 index 00000000..2dc89671 --- /dev/null +++ b/multiversx_sdk/entrypoints/entrypoints_multisig_test.py @@ -0,0 +1,274 @@ +import time +from pathlib import Path + +import pytest + +from multiversx_sdk.abi.abi import Abi +from multiversx_sdk.controllers.multisig_v2_resources import ( + ProposeAsyncCallInput, ProposeSCDeployFromSourceInput, + ProposeTransferExecuteInput, SCDeployFromSource, SendAsyncCall, UserRole) +from multiversx_sdk.core.address import Address +from multiversx_sdk.core.code_metadata import CodeMetadata +from multiversx_sdk.facades.account import Account +from multiversx_sdk.facades.entrypoints import DevnetEntrypoint + +testutils = Path(__file__).parent.parent / "testutils" + + +class TestEntrypoint: + entrypoint = DevnetEntrypoint() + alice_pem = testutils / "testwallets" / "alice.pem" + grace_pem = testutils / "testwallets" / "grace.pem" + + @pytest.mark.networkInteraction + def test_multisig_flow(self): + abi_multisig = Abi.load(testutils / "testdata" / "multisig-full.abi.json") + abi_adder = Abi.load(testutils / "testdata" / "adder.abi.json") + bytecode_path_multisig = testutils / "testdata" / "multisig-full.wasm" + contract_to_copy_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqsuxsgykwm6r3s5apct2g5a2rcpe7kw0ed8ssf6h9f6") + controller_multisig = self.entrypoint.create_multisig_v2_controller(abi_multisig) + controller_adder = self.entrypoint.create_smart_contract_controller(abi_adder) + + # Alice and Grace are the (initial) board members. + alice = Account.new_from_pem(self.alice_pem) + grace = Account.new_from_pem(self.grace_pem) + alice.nonce = self.entrypoint.recall_account_nonce(alice.address) + grace.nonce = self.entrypoint.recall_account_nonce(grace.address) + + # Deploy the multisig contract. + transaction = controller_multisig.create_transaction_for_deploy( + sender=alice, + nonce=alice.nonce, + bytecode=bytecode_path_multisig, + gas_limit=100_000_000, + quorum=2, + board=[alice.address, grace.address] + ) + + alice.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + multisig_address = controller_multisig.await_completed_deploy(transaction_hash) + print("Multisig address:", multisig_address) + + role_alice = controller_multisig.get_user_role(multisig_address, alice.address) + role_grace = controller_multisig.get_user_role(multisig_address, grace.address) + assert role_alice == UserRole.BOARD_MEMBER + assert role_grace == UserRole.BOARD_MEMBER + + board_members = controller_multisig.get_all_board_members(multisig_address) + assert board_members == [alice.address, grace.address] + + # Alice proposes a deploy of the adder contract. + transaction = controller_multisig.create_transaction_for_propose_deploy_contract_from_source( + sender=alice, + nonce=alice.nonce, + contract=multisig_address, + gas_limit=30_000_000, + input=ProposeSCDeployFromSourceInput( + native_transfer_amount=0, + contract_to_copy=contract_to_copy_address, + code_metadata=CodeMetadata(), + arguments=[7], + abi=abi_adder + ) + ) + + alice.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + action_id = controller_multisig.await_completed_execute_propose_any(transaction_hash) + print("Action ID (deploy the adder contract):", action_id) + + # Grace signs the action. + transaction = controller_multisig.create_transaction_for_sign( + sender=grace, + nonce=grace.nonce, + contract=multisig_address, + gas_limit=30_000_000, + action_id=action_id + ) + + grace.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + self.entrypoint.await_completed_transaction(transaction_hash) + + # Query information about the action. + signer_count = controller_multisig.get_action_signer_count(multisig_address, action_id) + valid_signer_count = controller_multisig.get_action_valid_signer_count(multisig_address, action_id) + signers = controller_multisig.get_action_signers(multisig_address, action_id) + + assert signer_count == 2 + assert valid_signer_count == 2 + assert signers == [alice.address, grace.address] + + [action_full_info] = controller_multisig.get_pending_actions_full_info(multisig_address) + print("Action full info:", action_full_info) + assert action_full_info.action_id == action_id + assert action_full_info.group_id == 0 + assert action_full_info.signers == [alice.address, grace.address] + assert action_full_info.action_data.discriminant == 8 + assert isinstance(action_full_info.action_data, SCDeployFromSource) + assert action_full_info.action_data.amount == 0 + assert action_full_info.action_data.source == contract_to_copy_address + assert action_full_info.action_data.code_metadata == CodeMetadata().serialize() + assert action_full_info.action_data.arguments == [bytes([7])] + + # Alice performs the action. + transaction = controller_multisig.create_transaction_for_perform_action( + sender=alice, + nonce=alice.nonce, + contract=multisig_address, + gas_limit=30_000_000, + action_id=action_id + ) + + alice.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + adder_address = controller_multisig.await_completed_execute_perform(transaction_hash) + print("Adder address:", adder_address) + assert adder_address is not None + + # Query the adder contract. + [value] = controller_adder.query_contract( + contract=adder_address, + function="getSum", + arguments=[] + ) + + print("Value of adder::getSum():", value) + assert value == 7 + + # Alice proposes to add a value to the adder contract. + transaction = controller_multisig.create_transaction_for_propose_async_call( + sender=alice, + nonce=alice.nonce, + contract=multisig_address, + gas_limit=30_000_000, + input=ProposeAsyncCallInput.new_for_transfer_execute( + to=adder_address, + native_transfer_amount=0, + token_transfers=[], + function="add", + arguments=[7], + abi=abi_adder, + ) + ) + + alice.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + action_id = controller_multisig.await_completed_execute_propose_any(transaction_hash) + print("Action ID (call adder::add()):", action_id) + + # Grace signs the action. + transaction = controller_multisig.create_transaction_for_sign( + sender=grace, + nonce=grace.nonce, + contract=multisig_address, + gas_limit=30_000_000, + action_id=action_id + ) + + grace.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + self.entrypoint.await_completed_transaction(transaction_hash) + + [action_full_info] = controller_multisig.get_pending_actions_full_info(multisig_address) + print("Action full info:", action_full_info) + assert action_full_info.action_id == action_id + assert action_full_info.group_id == 0 + assert action_full_info.signers == [alice.address, grace.address] + assert action_full_info.action_data.discriminant == 7 + assert isinstance(action_full_info.action_data, SendAsyncCall) + assert action_full_info.action_data.data.to == adder_address + assert action_full_info.action_data.data.egld_amount == 0 + assert action_full_info.action_data.data.endpoint_name == b"add" + assert action_full_info.action_data.data.arguments == [bytes([7])] + assert action_full_info.action_data.data.opt_gas_limit is None + + # Alice performs the action. + transaction = controller_multisig.create_transaction_for_perform_action( + sender=alice, + nonce=alice.nonce, + contract=multisig_address, + gas_limit=30_000_000, + action_id=action_id + ) + + alice.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + _ = controller_multisig.await_completed_execute_perform(transaction_hash) + + # Query the adder contract. + [value] = controller_adder.query_contract( + contract=adder_address, + function="getSum", + arguments=[] + ) + + print("Value of adder::getSum():", value) + assert value == 14 + + # Alice proposes to add a value to the adder contract (with transfer and execute). + transaction = controller_multisig.create_transaction_for_propose_transfer_execute( + sender=alice, + nonce=alice.nonce, + contract=multisig_address, + gas_limit=30_000_000, + input=ProposeTransferExecuteInput.new_for_transfer_execute( + to=adder_address, + native_transfer_amount=0, + function="add", + arguments=[7], + abi=abi_adder, + ) + ) + + alice.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + action_id = controller_multisig.await_completed_execute_propose_any(transaction_hash) + print("Action ID (call adder::add()):", action_id) + + # Grace signs the action. + transaction = controller_multisig.create_transaction_for_sign( + sender=grace, + nonce=grace.nonce, + contract=multisig_address, + gas_limit=30_000_000, + action_id=action_id + ) + + grace.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + self.entrypoint.await_completed_transaction(transaction_hash) + + # Alice performs the action. + transaction = controller_multisig.create_transaction_for_perform_action( + sender=alice, + nonce=alice.nonce, + contract=multisig_address, + gas_limit=30_000_000, + action_id=action_id + ) + + alice.nonce += 1 + + transaction_hash = self.entrypoint.send_transaction(transaction) + _ = controller_multisig.await_completed_execute_perform(transaction_hash) + + # Query the adder contract. + [value] = controller_adder.query_contract( + contract=adder_address, + function="getSum", + arguments=[] + ) + + print("Value of adder::getSum():", value) + assert value == 21 diff --git a/multiversx_sdk/multisig/multisig_v2_controller.py b/multiversx_sdk/multisig/multisig_v2_controller.py new file mode 100644 index 00000000..d1c00364 --- /dev/null +++ b/multiversx_sdk/multisig/multisig_v2_controller.py @@ -0,0 +1,725 @@ +from pathlib import Path +from typing import Any, List, Optional, Protocol, Union + +from multiversx_sdk.abi.abi import Abi +from multiversx_sdk.core.address import Address +from multiversx_sdk.core.config import LibraryConfig +from multiversx_sdk.core.interfaces import IAccount +from multiversx_sdk.core.tokens import TokenTransfer +from multiversx_sdk.core.transaction import Transaction +from multiversx_sdk.core.transaction_computer import TransactionComputer +from multiversx_sdk.core.transaction_on_network import TransactionOnNetwork +from multiversx_sdk.core.transactions_factory_config import \ + TransactionsFactoryConfig +from multiversx_sdk.multisig.multisig_v2_resources import ( + ActionFullInfo, ProposeAsyncCallInput, ProposeSCDeployFromSourceInput, + ProposeSCUpgradeFromSourceInput, ProposeSyncCallInput, + ProposeTransferExecuteEsdtInput, ProposeTransferExecuteInput, UserRole) +from multiversx_sdk.network_providers.resources import AwaitingOptions +from multiversx_sdk.smart_contracts import ( + ParsedSmartContractCallOutcome, SmartContractController, + SmartContractQuery, SmartContractQueryResponse, + SmartContractTransactionsFactory, SmartContractTransactionsOutcomeParser) + + +class INetworkProvider(Protocol): + def query_contract(self, query: SmartContractQuery) -> SmartContractQueryResponse: + ... + + def await_transaction_completed(self, transaction_hash: Union[str, bytes], options: Optional[AwaitingOptions] = None) -> TransactionOnNetwork: + ... + + +class MultisigV2Controller: + def __init__(self, chain_id: str, network_provider: INetworkProvider, abi: Abi, address_hrp: Optional[str] = None) -> None: + self.network_provider = network_provider + self.factory = SmartContractTransactionsFactory( + TransactionsFactoryConfig(chain_id), + abi=abi + ) + self.parser = SmartContractTransactionsOutcomeParser(abi=abi) + self.transaction_computer = TransactionComputer() + self.smart_contract_controller = SmartContractController( + chain_id=chain_id, + network_provider=network_provider, + abi=abi + ) + self.address_hrp = address_hrp if address_hrp else LibraryConfig.default_address_hrp + + def create_transaction_for_deploy(self, + sender: IAccount, + nonce: int, + bytecode: Union[Path, bytes], + gas_limit: int, + quorum: int, + board: list[Address], + is_upgradeable: bool = True, + is_readable: bool = True, + is_payable: bool = False, + is_payable_by_contract: bool = True) -> Transaction: + transaction = self.factory.create_transaction_for_deploy( + sender=sender.address, + bytecode=bytecode, + gas_limit=gas_limit, + arguments=[quorum, board], + is_upgradeable=is_upgradeable, + is_readable=is_readable, + is_payable=is_payable, + is_payable_by_sc=is_payable_by_contract, + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def parse_deploy(self, transaction_on_network: TransactionOnNetwork) -> Address: + outcome = self.parser.parse_deploy(transaction_on_network) + return outcome.contracts[0].address + + def await_completed_deploy(self, tx_hash: str) -> Address: + transaction = self.network_provider.await_transaction_completed( + tx_hash) + return self.parse_deploy(transaction) + + def create_transaction_for_deposit(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + native_transfer_amount: int = 0, + token_transfers: Optional[List[TokenTransfer]] = None) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="deposit", + gas_limit=gas_limit, + arguments=[], + native_transfer_amount=native_transfer_amount, + token_transfers=token_transfers or [] + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_discard_action(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + action_id: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="discardAction", + gas_limit=gas_limit, + arguments=[action_id], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_discard_batch(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + actions_ids: list[int]) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="discardBatch", + gas_limit=gas_limit, + arguments=[actions_ids], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def get_quorum(self, contract: Address) -> int: + [value] = self.smart_contract_controller.query( + contract=contract, + function="getQuorum", + arguments=[], + ) + + return value + + def get_num_board_members(self, contract: Address) -> int: + [value] = self.smart_contract_controller.query( + contract=contract, + function="getNumBoardMembers", + arguments=[], + ) + + return value + + def get_num_groups(self, contract: Address) -> int: + [value] = self.smart_contract_controller.query( + contract=contract, + function="getNumGroups", + arguments=[], + ) + + return value + + def get_num_proposers(self, contract: Address) -> int: + [value] = self.smart_contract_controller.query( + contract=contract, + function="getNumProposers", + arguments=[], + ) + + return value + + def get_action_group(self, contract: Address, group_id: int) -> list[int]: + values = self.smart_contract_controller.query( + contract=contract, + function="getActionGroup", + arguments=[group_id], + ) + + return values + + def get_last_group_action_id(self, contract: Address) -> int: + [value] = self.smart_contract_controller.query( + contract=contract, + function="getLastGroupActionId", + arguments=[], + ) + + return value + + def get_action_last_index(self, contract: Address) -> int: + [value] = self.smart_contract_controller.query( + contract=contract, + function="getActionLastIndex", + arguments=[], + ) + + return value + + def create_transaction_for_propose_add_board_member(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + board_member: Address,) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeAddBoardMember", + gas_limit=gas_limit, + arguments=[board_member], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_add_proposer(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + proposer: Address) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeAddProposer", + gas_limit=gas_limit, + arguments=[proposer], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_remove_user(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + user: Address) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeRemoveUser", + gas_limit=gas_limit, + arguments=[user], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_change_quorum(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + new_quorum: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeChangeQuorum", + gas_limit=gas_limit, + arguments=[new_quorum], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_transfer_execute(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + input: ProposeTransferExecuteInput) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeTransferExecute", + gas_limit=gas_limit, + arguments=[input.to, input.egld_amount, + input.opt_gas_limit, input.function_call], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_transfer_execute_esdt(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + input: ProposeTransferExecuteEsdtInput) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeTransferExecuteEsdt", + gas_limit=gas_limit, + arguments=[input.to, input.tokens, + input.opt_gas_limit, input.function_call], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_async_call(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + input: ProposeAsyncCallInput) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeAsyncCall", + gas_limit=gas_limit, + arguments=[input.to, input.egld_amount, + input.opt_gas_limit, input.function_call], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_sync_call(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + input: ProposeSyncCallInput) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeSyncCall", + gas_limit=gas_limit, + arguments=[input.to, input.egld_amount, + input.opt_gas_limit, input.function_call], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_deploy_contract_from_source(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + input: ProposeSCDeployFromSourceInput) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeSCDeployFromSource", + gas_limit=gas_limit, + arguments=[input.amount, input.source, + input.code_metadata, input.arguments], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_upgrade_contract_from_source(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + input: ProposeSCUpgradeFromSourceInput) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="proposeSCUpgradeFromSource", + gas_limit=gas_limit, + arguments=[input.sc_address, input.amount, + input.source, input.code_metadata, input.arguments], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_propose_batch(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + actions: list[Any]) -> Transaction: + raise NotImplementedError("Not implemented yet") + + def create_transaction_for_sign(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + action_id: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="sign", + gas_limit=gas_limit, + arguments=[action_id], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_sign_batch(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + group_id: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="signBatch", + gas_limit=gas_limit, + arguments=[group_id], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_sign_and_perform(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + action_id: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="signAndPerform", + gas_limit=gas_limit, + arguments=[action_id], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_sign_batch_and_perform(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + group_id: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="signBatchAndPerform", + gas_limit=gas_limit, + arguments=[group_id], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_unsign(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + action_id: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="unsign", + gas_limit=gas_limit, + arguments=[action_id], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_unsign_batch(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + group_id: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="unsignBatch", + gas_limit=gas_limit, + arguments=[group_id], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def is_signed_by(self, contract: Address, user: Address, action_id: int) -> bool: + [value] = self.smart_contract_controller.query( + contract=contract, + function="signed", + arguments=[user, action_id], + ) + + return value + + def create_transaction_for_unsign_for_outdated_board_members(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + action_id: int, + outdated_board_members: list[int]) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="unsignForOutdatedBoardMembers", + gas_limit=gas_limit, + arguments=[action_id, outdated_board_members], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def is_quorum_reached(self, contract: Address, action_id: int) -> bool: + [value] = self.smart_contract_controller.query( + contract=contract, + function="quorumReached", + arguments=[action_id], + ) + + return value + + def create_transaction_for_perform_action(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + action_id: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="performAction", + gas_limit=gas_limit, + arguments=[action_id], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def create_transaction_for_perform_batch(self, + sender: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + group_id: int) -> Transaction: + transaction = self.factory.create_transaction_for_execute( + sender=sender.address, + contract=contract, + function="performBatch", + gas_limit=gas_limit, + arguments=[group_id], + ) + + transaction.nonce = nonce + transaction.signature = sender.sign( + self.transaction_computer.compute_bytes_for_signing(transaction)) + + return transaction + + def get_pending_actions_full_info(self, contract: Address) -> list[ActionFullInfo]: + [values] = self.smart_contract_controller.query( + contract=contract, + function="getPendingActionFullInfo", + # For now, we don't support the `opt_range` argument. + arguments=[None], + ) + + actions = [ActionFullInfo.new_from_object(value) for value in values] + return actions + + def get_user_role(self, contract: Address, user: Address) -> UserRole: + [value] = self.smart_contract_controller.query( + contract=contract, + function="userRole", + arguments=[user], + ) + + return UserRole(int(value)) + + def get_all_board_members(self, contract: Address) -> list[Address]: + [public_keys] = self.smart_contract_controller.query( + contract=contract, + function="getAllBoardMembers", + arguments=[], + ) + + return [Address(value, self.address_hrp) for value in public_keys] + + def get_all_proposers(self, contract: Address) -> list[Address]: + [public_keys] = self.smart_contract_controller.query( + contract=contract, + function="getAllProposers", + arguments=[], + ) + + return [Address(value, self.address_hrp) for value in public_keys] + + def get_action_data(self, contract: Address, action_id: int) -> list[Address]: + [value] = self.smart_contract_controller.query( + contract=contract, + function="getActionData", + arguments=[action_id], + ) + + return value + + def get_action_signers(self, contract: Address, action_id: int) -> list[Address]: + [public_keys] = self.smart_contract_controller.query( + contract=contract, + function="getActionSigners", + arguments=[action_id], + ) + + return [Address(value, self.address_hrp) for value in public_keys] + + def get_action_signer_count(self, contract: Address, action_id: int) -> int: + [value] = self.smart_contract_controller.query( + contract=contract, + function="getActionValidSignerCount", + arguments=[action_id], + ) + + return value + + def get_action_valid_signer_count(self, contract: Address, action_id: int) -> int: + [value] = self.smart_contract_controller.query( + contract=contract, + function="getActionSignerCount", + arguments=[action_id], + ) + + return value + + def parse_execute_propose_any(self, transaction_on_network: TransactionOnNetwork) -> int: + outcome = self.parser.parse_execute(transaction_on_network) + self._raise_for_return_code_in_outcome(outcome) + [value] = outcome.values + return value + + def await_completed_execute_propose_any(self, tx_hash: str) -> int: + transaction = self.network_provider.await_transaction_completed( + tx_hash) + return self.parse_execute_propose_any(transaction) + + def parse_execute_perform(self, transaction_on_network: TransactionOnNetwork) -> Optional[Address]: + outcome = self.parser.parse_execute(transaction_on_network) + self._raise_for_return_code_in_outcome(outcome) + [value] = outcome.values + return Address(value, self.address_hrp) if value else None + + def await_completed_execute_perform(self, tx_hash: str) -> Optional[Address]: + transaction = self.network_provider.await_transaction_completed( + tx_hash) + return self.parse_execute_perform(transaction) + + # TODO: maybe move to the generic outcome parser, just like we did in the query controller? + def _raise_for_return_code_in_outcome(self, outcome: ParsedSmartContractCallOutcome): + is_ok = outcome.return_code == "ok" + if not is_ok: + raise Exception(outcome.return_code, outcome.return_message) diff --git a/multiversx_sdk/multisig/multisig_v2_resources.py b/multiversx_sdk/multisig/multisig_v2_resources.py new file mode 100644 index 00000000..c4eaf563 --- /dev/null +++ b/multiversx_sdk/multisig/multisig_v2_resources.py @@ -0,0 +1,436 @@ + +from enum import Enum +from typing import Any, Optional + +from multiversx_sdk.abi.abi import Abi +from multiversx_sdk.core.address import Address +from multiversx_sdk.core.code_metadata import CodeMetadata +from multiversx_sdk.core.constants import ARGS_SEPARATOR +from multiversx_sdk.core.tokens import TokenTransfer +from multiversx_sdk.core.transactions_factory_config import \ + TransactionsFactoryConfig +from multiversx_sdk.smart_contracts.smart_contract_transactions_factory import \ + SmartContractTransactionsFactory + + +class ProposeTransferExecuteInput: + def __init__(self, + to: Address, + native_transfer_amount: int, + function_call: list[bytes], + opt_gas_limit: Optional[int] = None,) -> None: + + self.to = to + self.egld_amount = native_transfer_amount + self.function_call = function_call + self.opt_gas_limit = opt_gas_limit + + @classmethod + def new_for_native_transfer(cls, + to: Address, + native_transfer_amount: int, + gas_limit: Optional[int] = None): + return cls(to, native_transfer_amount, [], gas_limit) + + @classmethod + def new_for_transfer_execute(cls, + to: Address, + native_transfer_amount: int, + function: str, + arguments: list[Any], + gas_limit: Optional[int] = None, + abi: Optional[Abi] = None): + arguments = abi.encode_endpoint_input_parameters(function, arguments) if abi else arguments + function_call = [function, *arguments] + return cls(to, native_transfer_amount, function_call, gas_limit) + + +class ProposeTransferExecuteEsdtInput: + def __init__(self, + to: Address, + tokens: list['EsdtTokenPayment'], + function_call: list[bytes], + opt_gas_limit: Optional[int] = None) -> None: + self.to = to + self.tokens = tokens + self.function_call = function_call + self.opt_gas_limit = opt_gas_limit + + @classmethod + def new_for_transfer(cls, + to: Address, + token_transfers: list[TokenTransfer], + gas_limit: Optional[int] = None): + tokens = [EsdtTokenPayment(token.token.identifier, token.token.nonce, + token.amount) for token in token_transfers] + return cls(to, tokens, [], gas_limit) + + @classmethod + def new_for_transfer_execute(cls, + to: Address, + token_transfers: list[TokenTransfer], + function: str, + arguments: list[Any], + gas_limit: Optional[int] = None, + abi: Optional[Abi] = None): + # Since multisig requires the execution (but not the transfers) to be encoded as variadic in "function_call", + # we leverage the transactions factory to achieve this (followed by splitting the data). + transactions_factory = SmartContractTransactionsFactory( + TransactionsFactoryConfig(""), abi=abi) + transaction = transactions_factory.create_transaction_for_execute( + sender=Address.empty(), + contract=Address.empty(), + function=function, + gas_limit=0, + arguments=arguments) + + tokens = [EsdtTokenPayment(token.token.identifier, token.token.nonce, + token.amount) for token in token_transfers] + function_call_parts = transaction.data.split(ARGS_SEPARATOR.encode()) + function_name = function_call_parts[0] + function_arguments = [bytes.fromhex(item.decode()) for item in function_call_parts[1:]] + function_call = [function_name, *function_arguments] + return cls(to, tokens, function_call, gas_limit) + + +class ProposeAsyncCallInput: + def __init__(self, + to: Address, + egld_amount: int, + function_call: list[bytes], + opt_gas_limit: Optional[int] = None) -> None: + self.to = to + self.egld_amount = egld_amount + self.function_call = function_call + self.opt_gas_limit = opt_gas_limit + + @classmethod + def new_for_transfer(cls, + to: Address, + token_transfers: list[TokenTransfer], + gas_limit: Optional[int] = None): + # Since multisig requires the transfer to be encoded as variadic in "function_call", + # we leverage the transactions factory to achieve this (followed by splitting the data). + transactions_factory = TransferTransactionsFactory(TransactionsFactoryConfig("")) + transaction = transactions_factory.create_transaction_for_transfer( + sender=Address.empty(), + receiver=Address.empty(), + # Multisig wasn't designed to work with EGLD within MultiESDTNFT. + native_amount=0, + token_transfers=token_transfers) + + function_call_parts = transaction.data.split(ARGS_SEPARATOR.encode()) + function_name = function_call_parts[0] + function_arguments = [bytes.fromhex(item.decode()) for item in function_call_parts[1:]] + function_call = [function_name, *function_arguments] + return cls(to, 0, function_call, gas_limit) + + @classmethod + def new_for_transfer_execute(cls, + to: Address, + native_transfer_amount: int, + token_transfers: list[TokenTransfer], + function: str, + arguments: list[Any], + gas_limit: Optional[int] = None, + abi: Optional[Abi] = None): + # Since multisig requires the transfer & execute to be encoded as variadic in "function_call", + # we leverage the transactions factory to achieve this (followed by splitting the data). + transactions_factory = SmartContractTransactionsFactory( + TransactionsFactoryConfig(""), abi=abi) + transaction = transactions_factory.create_transaction_for_execute( + sender=Address.empty(), + contract=Address.empty(), + function=function, + gas_limit=0, + arguments=arguments, + # Multisig wasn't designed to work with EGLD within MultiESDTNFT. + native_transfer_amount=0, + token_transfers=token_transfers) + + function_call_parts = transaction.data.split(ARGS_SEPARATOR.encode()) + function_name = function_call_parts[0] + function_arguments = [bytes.fromhex(item.decode()) for item in function_call_parts[1:]] + function_call = [function_name, *function_arguments] + return cls(to, native_transfer_amount, function_call, gas_limit) + + +class ProposeSyncCallInput(ProposeAsyncCallInput): + pass + + +class EsdtTokenPayment: + def __init__(self, token_identifier: str, token_nonce: int, amount: int) -> None: + self.token_identifier = token_identifier + self.token_nonce = token_nonce + self.amount = amount + + +class ProposeSCDeployFromSourceInput: + def __init__(self, + native_transfer_amount: int, + contract_to_copy: Address, + code_metadata: CodeMetadata, + arguments: list[Any], + abi: Optional[Abi] = None) -> None: + self.amount = native_transfer_amount + self.source = contract_to_copy + self.code_metadata = code_metadata.serialize() + self.arguments = abi.encode_constructor_input_parameters(arguments) if abi else arguments + + +class ProposeSCUpgradeFromSourceInput: + def __init__(self, + contract_to_upgrade: Address, + native_transfer_amount: int, + contract_to_copy: Address, + code_metadata: CodeMetadata, + arguments: list[Any], + abi: Optional[Abi] = None) -> None: + self.sc_address = contract_to_upgrade + self.amount = native_transfer_amount + self.source = contract_to_copy + self.code_metadata = code_metadata.serialize() + self.arguments = abi.encode_upgrade_constructor_input_parameters( + arguments) if abi else arguments + + +class UserRole(Enum): + NONE = 0 + PROPOSER = 1 + BOARD_MEMBER = 2 + + +class ActionFullInfo: + def __init__(self, + action_id: int, + group_id: int, + action_data: 'Action', + signers: list[Address]) -> None: + self.action_id = action_id + self.group_id = group_id + self.action_data = action_data + self.signers = signers + + @classmethod + def new_from_object(cls, object: Any) -> 'ActionFullInfo': + signers = [Address(value, DEFAULT_ADDRESS_HRP) for value in object.signers] + action_data = create_action_from_object(object.action_data) + return cls(object.action_id, object.group_id, action_data, signers) + + def __repr__(self) -> str: + return f"ActionFullInfo(action_id={self.action_id}, group_id={self.group_id}, action_data={self.action_data}, signers={self.signers})" + + +def create_action_from_object(object: Any) -> 'Action': + discriminant = int(object) + + if discriminant == AddBoardMember.discriminant: + return AddBoardMember.new_from_object(object) + elif discriminant == AddProposer.discriminant: + return AddProposer.new_from_object(object) + elif discriminant == RemoveUser.discriminant: + return RemoveUser.new_from_object(object) + elif discriminant == ChangeQuorum.discriminant: + return ChangeQuorum.new_from_object(object) + elif discriminant == SendTransferExecuteEgld.discriminant: + return SendTransferExecuteEgld.new_from_object(object) + elif discriminant == SendTransferExecuteEsdt.discriminant: + return SendTransferExecuteEsdt.new_from_object(object) + elif discriminant == SendAsyncCall.discriminant: + return SendAsyncCall.new_from_object(object) + elif discriminant == SCDeployFromSource.discriminant: + return SCDeployFromSource.new_from_object(object) + elif discriminant == SCUpgradeFromSource.discriminant: + return SCUpgradeFromSource.new_from_object(object) + else: + raise ValueError(f"Unknown action discriminant: {discriminant}") + + +class Action: + discriminant: int + + def __init__(self) -> None: + pass + + def __repr__(self) -> str: + return self.__class__.__name__ + + +class AddBoardMember(Action): + discriminant = 1 + + def __init__(self, address: Address) -> None: + self.address = address + + @classmethod + def new_from_object(cls, object: Any) -> 'AddBoardMember': + field_0 = getattr(object, "0") + public_key = field_0 + address = Address(public_key, DEFAULT_ADDRESS_HRP) + return cls(address) + + +class AddProposer(Action): + discriminant = 2 + + def __init__(self, address: Address) -> None: + self.address = address + + @classmethod + def new_from_object(cls, object: Any) -> 'AddProposer': + field_0 = getattr(object, "0") + public_key = field_0 + address = Address(public_key, DEFAULT_ADDRESS_HRP) + return cls(address) + + +class RemoveUser(Action): + discriminant = 3 + + def __init__(self, address: Address) -> None: + self.address = address + + @classmethod + def new_from_object(cls, object: Any) -> 'RemoveUser': + field_0 = getattr(object, "0") + public_key = field_0 + address = Address(public_key, DEFAULT_ADDRESS_HRP) + return cls(address) + + +class ChangeQuorum(Action): + discriminant = 4 + + def __init__(self, quorum: int) -> None: + self.quorum = quorum + + @classmethod + def new_from_object(cls, object: Any) -> 'ChangeQuorum': + field_0 = getattr(object, "0") + quorum = field_0 + return cls(quorum) + + +class SendTransferExecuteEgld(Action): + discriminant = 5 + + def __init__(self, data: 'CallActionData') -> None: + self.data = data + + @classmethod + def new_from_object(cls, object: Any) -> 'SendTransferExecuteEgld': + field_0 = getattr(object, "0") + data = CallActionData.new_from_object(field_0) + return cls(data) + + +class CallActionData: + def __init__(self, + to: Address, + egld_amount: int, + endpoint_name: str, + arguments: list[bytes], + opt_gas_limit: Optional[int] = None): + self.to = to + self.egld_amount = egld_amount + self.endpoint_name = endpoint_name + self.arguments = arguments + self.opt_gas_limit = opt_gas_limit + + @classmethod + def new_from_object(cls, object: Any) -> 'CallActionData': + to = Address(object.to, DEFAULT_ADDRESS_HRP) + egld_amount = object.egld_amount + endpoint_name = object.endpoint_name + arguments = object.arguments + opt_gas_limit = object.opt_gas_limit + return cls(to, egld_amount, endpoint_name, arguments, opt_gas_limit) + + +class SendTransferExecuteEsdt(Action): + discriminant = 6 + + def __init__(self, data: 'EsdtTransferExecuteData') -> None: + self.data = data + + @classmethod + def new_from_object(cls, object: Any) -> 'SendTransferExecuteEsdt': + field_0 = getattr(object, "0") + data = EsdtTransferExecuteData.new_from_object(field_0) + return cls(data) + + +class EsdtTransferExecuteData: + def __init__(self, + to: Address, + tokens: list[EsdtTokenPayment], + opt_gas_limit: Optional[int], + endpoint_name: bytes, + arguments: list[bytes]) -> None: + self.to = to + self.tokens = tokens + self.opt_gas_limit = opt_gas_limit + self.endpoint_name = endpoint_name + self.arguments = arguments + + @classmethod + def new_from_object(cls, object: Any) -> 'EsdtTransferExecuteData': + to = Address(object.to, DEFAULT_ADDRESS_HRP) + tokens = [EsdtTokenPayment(token.token_identifier, token.token_nonce, + token.amount) for token in object.tokens] + opt_gas_limit = object.opt_gas_limit + endpoint_name = object.endpoint_name + arguments = object.arguments + return cls(to, tokens, opt_gas_limit, endpoint_name, arguments) + + +class SendAsyncCall(Action): + discriminant = 7 + + def __init__(self, data: 'CallActionData') -> None: + self.data = data + + @classmethod + def new_from_object(cls, object: Any) -> 'SendAsyncCall': + field_0 = getattr(object, "0") + data = CallActionData.new_from_object(field_0) + return cls(data) + + +class SCDeployFromSource(Action): + discriminant = 8 + + def __init__(self, amount: int, source: Address, code_metadata: bytes, arguments: list[bytes]) -> None: + self.amount = amount + self.source = source + self.code_metadata = code_metadata + self.arguments = arguments + + @classmethod + def new_from_object(cls, object: Any) -> 'SCDeployFromSource': + amount = object.amount + source = Address(object.source, DEFAULT_ADDRESS_HRP) + code_metadata = object.code_metadata + arguments = object.arguments + return cls(amount, source, code_metadata, arguments) + + +class SCUpgradeFromSource(Action): + discriminant = 9 + + def __init__(self, sc_address: Address, amount: int, source: Address, code_metadata: bytes, arguments: list[bytes]) -> None: + self.sc_address = sc_address + self.amount = amount + self.source = source + self.code_metadata = code_metadata + self.arguments = arguments + + @classmethod + def new_from_object(cls, object: Any) -> 'SCUpgradeFromSource': + sc_address = Address(object.sc_address, DEFAULT_ADDRESS_HRP) + amount = object.amount + source = Address(object.source, DEFAULT_ADDRESS_HRP) + code_metadata = object.code_metadata + arguments = object.arguments + return cls(sc_address, amount, source, code_metadata, arguments) diff --git a/multiversx_sdk/testutils/testdata/multisig-full.abi.json b/multiversx_sdk/testutils/testdata/multisig-full.abi.json index f0fdca90..74a448a7 100644 --- a/multiversx_sdk/testutils/testdata/multisig-full.abi.json +++ b/multiversx_sdk/testutils/testdata/multisig-full.abi.json @@ -14,20 +14,20 @@ ], "outputs": [] }, + "upgradeConstructor": { + "inputs": [], + "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": ["*"], + "payableInTokens": [ + "*" + ], "inputs": [], "outputs": [] }, @@ -48,7 +48,9 @@ "outputs": [] }, { - "docs": ["Discard all the actions with the given IDs"], + "docs": [ + "Discard all the actions with the given IDs" + ], "name": "discardBatch", "mutability": "mutable", "inputs": [ @@ -61,7 +63,9 @@ "outputs": [] }, { - "docs": ["Minimum number of signatures needed to perform any action."], + "docs": [ + "Minimum number of signatures needed to perform any action." + ], "name": "getQuorum", "mutability": "readonly", "inputs": [], @@ -394,7 +398,9 @@ ] }, { - "docs": ["Used by board members to sign actions."], + "docs": [ + "Used by board members to sign actions." + ], "name": "sign", "mutability": "mutable", "inputs": [ @@ -406,7 +412,9 @@ "outputs": [] }, { - "docs": ["Sign all the actions in the given batch"], + "docs": [ + "Sign all the actions in the given batch" + ], "name": "signBatch", "mutability": "mutable", "inputs": [ @@ -460,7 +468,9 @@ "outputs": [] }, { - "docs": ["Unsign all actions with the given IDs"], + "docs": [ + "Unsign all actions with the given IDs" + ], "name": "unsignBatch", "mutability": "mutable", "inputs": [ @@ -548,7 +558,9 @@ ] }, { - "docs": ["Perform all the actions in the given batch"], + "docs": [ + "Perform all the actions in the given batch" + ], "name": "performBatch", "mutability": "mutable", "inputs": [ @@ -563,7 +575,9 @@ "name": "dnsRegister", "onlyOwner": true, "mutability": "mutable", - "payableInTokens": ["EGLD"], + "payableInTokens": [ + "EGLD" + ], "inputs": [ { "name": "dns_address", @@ -599,7 +613,9 @@ "multi_result": true } ], - "labels": ["multisig-external-view"], + "labels": [ + "multisig-external-view" + ], "allow_multiple_var_args": true }, { @@ -622,10 +638,14 @@ "type": "UserRole" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { - "docs": ["Lists all users that can sign actions."], + "docs": [ + "Lists all users that can sign actions." + ], "name": "getAllBoardMembers", "mutability": "readonly", "inputs": [], @@ -635,10 +655,14 @@ "multi_result": true } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { - "docs": ["Lists all proposers that are not board members."], + "docs": [ + "Lists all proposers that are not board members." + ], "name": "getAllProposers", "mutability": "readonly", "inputs": [], @@ -648,10 +672,14 @@ "multi_result": true } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { - "docs": ["Serialized action data of an action with index."], + "docs": [ + "Serialized action data of an action with index." + ], "name": "getActionData", "mutability": "readonly", "inputs": [ @@ -665,7 +693,9 @@ "type": "Action" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { "docs": [ @@ -686,7 +716,9 @@ "type": "List
" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { "docs": [ @@ -706,7 +738,9 @@ "type": "u32" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] }, { "docs": [ @@ -729,7 +763,9 @@ "type": "u32" } ], - "labels": ["multisig-external-view"] + "labels": [ + "multisig-external-view" + ] } ], "events": [ @@ -1120,7 +1156,9 @@ }, "ActionFullInfo": { "type": "struct", - "docs": ["Not used internally, just to retrieve results via endpoint."], + "docs": [ + "Not used internally, just to retrieve results via endpoint." + ], "fields": [ { "name": "action_id", diff --git a/multiversx_sdk/testutils/testdata/multisig-full.wasm b/multiversx_sdk/testutils/testdata/multisig-full.wasm new file mode 100644 index 00000000..c27d8197 Binary files /dev/null and b/multiversx_sdk/testutils/testdata/multisig-full.wasm differ