From ba7578f4d0fd6683659d221db16d4f58d5b24667 Mon Sep 17 00:00:00 2001 From: zero Date: Fri, 24 Jul 2026 20:42:09 +0100 Subject: [PATCH 1/2] [mkdocs] feat: add config multisig tutorial [mkdocs] fix: flow as other tutorials [mkdocs] fix: flow as other tutorials --- mkdocs/config/mkdocs.en.yml | 1 + .../en/devbook/accounts/configure-multisig.md | 269 ++++++++++++++++++ .../devbook/accounts/configure_multisig.mjs | 237 +++++++++++++++ .../devbook/accounts/configure_multisig.py | 228 +++++++++++++++ .../accounts/configure_multisig_disable.log | 88 ++++++ .../accounts/configure_multisig_enable.log | 43 +++ 6 files changed, 866 insertions(+) create mode 100644 mkdocs/pages/en/devbook/accounts/configure-multisig.md create mode 100644 mkdocs/snippets/devbook/accounts/configure_multisig.mjs create mode 100644 mkdocs/snippets/devbook/accounts/configure_multisig.py create mode 100644 mkdocs/snippets/devbook/accounts/configure_multisig_disable.log create mode 100644 mkdocs/snippets/devbook/accounts/configure_multisig_enable.log diff --git a/mkdocs/config/mkdocs.en.yml b/mkdocs/config/mkdocs.en.yml index a150ba78c..82f09ebd1 100644 --- a/mkdocs/config/mkdocs.en.yml +++ b/mkdocs/config/mkdocs.en.yml @@ -86,6 +86,7 @@ nav: - devbook/accounts/create-from-mnemonic.md - devbook/accounts/testnet-faucet.md - devbook/accounts/query-balance.md + - devbook/accounts/configure-multisig.md - Transactions: - devbook/transactions/transfer-xem.md - devbook/transactions/transfer-mosaics.md diff --git a/mkdocs/pages/en/devbook/accounts/configure-multisig.md b/mkdocs/pages/en/devbook/accounts/configure-multisig.md new file mode 100644 index 000000000..da01e2220 --- /dev/null +++ b/mkdocs/pages/en/devbook/accounts/configure-multisig.md @@ -0,0 +1,269 @@ +--- +title: Configure a Multisig +tutorial_level: advanced +--- + +# Configuring a Multisignature Account + +A , also called _multisig_, cannot initiate transactions on its own. +Instead, it relies on _cosignatory_ accounts to create transactions and sign them on its behalf. + +This tutorial shows how to convert a regular account into a multisig account that requires approval from one of two +cosignatories. +If the account is already multisig, the tutorial instead demonstrates how to remove the cosignatories and revert the +account to a regular account. + +The multisignature structure used in this tutorial is shown below: + +```dot +digraph "Multisignature Tree" { + rankdir="BT"; + node [fontsize=12]; + "Multisignature Account"; + "Cosignatory 0"; + "Cosignatory 1"; + + "Cosignatory 0" -> "Multisignature Account"; + "Cosignatory 1" -> "Multisignature Account"; +} +``` + +## Prerequisites + +Before you start, make sure to: + +* Set up your development environment. + See [Setting Up a Development Environment](../start/setup.md). +* Create 3 : one to turn into a multisig, and the other two to act as cosignatories. + You can do this either [from code](./create-from-private-key.md) or + [by using a wallet](../../userbook/wallet/create-account.md). +* Obtain for the account being converted into a multisig to pay for the transaction fees. + See [Getting Testnet Funds from the Faucet](./testnet-faucet.md). + +Additionally, review the [Transfer XEM](../transactions/transfer-xem.md) tutorial to understand how transactions are +announced and confirmed. + +## Full Code + +{% import 'tutorial.jinja2' as tutorial with context %} + +{{ tutorial.code_full_tagged('devbook/accounts/configure_multisig', ['py', 'js']) }} + +## Code Explanation + +The code defines two helper functions, for announcing a transaction and waiting for its confirmation. +For details on how these work, see the [Transfer XEM](../transactions/transfer-xem.md) tutorial. +The remaining helper functions are described in the sections below. + +The tutorial then proceeds to [set up the required keys](#setting-up-the-accounts), +[fetch the current network time](#fetching-network-time), and +[detect the current configuration](#determining-the-multisig-operation) of the multisig account. + +Depending on whether the account is already configured as a multisig, +transactions are created to [enable](#enabling-the-multisig) or [disable](#disabling-the-multisig) it as appropriate. +Finally, the transactions are [announced and confirmed](#submitting-the-transactions). + +### Setting Up the Accounts + +{{ tutorial.code_snippet_tagged('step-1') }} + +The tutorial requires three separate accounts. +Their can be provided through environment variables. +If not set, default values are used: + +| Environment Variable | Default value | Purpose | +|----------------------------|---------------|----------------------------| +| `MULTISIG_PRIVATE_KEY` | `0000..0001` | Multisig account | +| `COSIGNATORY0_PRIVATE_KEY` | `0000..0002` | First cosignatory account | +| `COSIGNATORY1_PRIVATE_KEY` | `0000..0003` | Second cosignatory account | + +Each private key is a 64-character hexadecimal string. + +The multisig account must hold enough funds to pay the transaction fees. +If the default values are used, this account may already be funded. + +The snippet above derives and stores the and of each account for later use. + +### Fetching Network Time + +{{ tutorial.code_snippet_tagged('step-2') }} + +Network time is fetched from , and the transactions' `timestamp` and `deadline` fields +are derived from it, following the process described in the [Transfer XEM](../transactions/transfer-xem.md) tutorial. + +### Determining the Multisig Operation + +{{ tutorial.code_snippet_tagged('step-3') }} + +This helper retrieves the list of current cosignatories for a given address using the endpoint. +If it returns an empty list, the account is not currently configured as a multisig account. + +{{ tutorial.code_snippet_tagged('step-4') }} + +The returned cosignatories determine whether the account is configured as a multisig account, and therefore whether to +create the transactions to enable or disable multisig. + +The functions that build them and the delta values they use are described in the next two sections. + +### Enabling the Multisig + +{{ tutorial.code_snippet_tagged('step-5') }} + +All changes to the multisig configuration of an account, including adding or removing cosignatories, +are performed using a . + +The transaction specifies: + +* {{ tutorial.var('type') }}: Multisig configuration changes use the type + . + +* {{ tutorial.var('signer_public_key') }}: of the account whose multisig configuration will be modified. + +* {{ tutorial.var('timestamp') }} and {{ tutorial.var('deadline') }}: The values computed in the network time step. + +* {{ tutorial.var('min_approval_delta') }}: difference between the _desired value_ and the _current value_ of the + number of cosignatures required to approve transactions from the multisig account. + + In this case, the account is initially a regular account, so the current number of required cosignatures is `0`. + To convert it into a multisig account that requires one signature from one of its cosignatories, + the delta is set to `1`. + + The delta value can be negative to _reduce_ the current value, as shown in the next section. + +* {{ tutorial.var('modifications') }}: list of changes to the account's cosignatories. + Each modification adds or removes one cosignatory, identified by its . + + In this case, two `add_cosignatory` modifications add the cosignatories prepared during the + [setup phase](#setting-up-the-accounts). + +!!! note "Safety measures" + + The protocol includes safety mechanisms that help prevent locking an account into an invalid state. + Transactions that would result in an invalid multisig configuration are rejected with an error. + For example, when: + + * The number of cosignatories is lower than the number of required cosignatures + * An account that is already a cosignatory is added + * An account that is not a cosignatory is removed + * More than one cosignatory is removed in a single transaction + * A multisig account is added as a cosignatory + +{{ tutorial.code_snippet_tagged('step-6') }} + +The transaction fee is calculated with and attached to the transaction. +Multisig account modification transactions pay a fixed transaction fee of 0.5 XEM, as shown in the +[fee schedule](../../textbook/transactions.md#fee-schedule). + +{{ tutorial.code_snippet_tagged('step-7') }} + +Finally, the transaction is signed. +In this case, only the signature of the account being converted into a multisig is required. +The cosignatories do not sign the conversion transaction. + +!!! info "From now on, cosignatories must initiate transactions" + + Once an account has multisig enabled, its own signature is no longer accepted. + Any transaction sent from that account, such as a transfer or a further multisig modification, + must instead be initiated and signed by its cosignatories, as shown in the next section. + +### Disabling the Multisig + +Disabling a multisig configuration requires removing all cosignatories. +The process is similar to enabling it, with two key differences: +cosignatories must be removed one by one, and the multisig account itself cannot sign the transactions. + +{{ tutorial.code_snippet_tagged('step-8') }} + +This helper builds a that removes a cosignatory. +It takes the cosignatory to remove and the approval delta to apply as parameters. +{{ tutorial.var('signer_public_key') }} is set to the multisig account's public key because its configuration is being +modified. + +As shown in [Determining the Multisig Operation](#determining-the-multisig-operation), the helper is called twice. + +The first call removes {{ tutorial.var('cosignatory_key_pairs[1]') }} with an approval delta of `0`, because one +cosignatory still remains. + +The second removes the remaining cosignatory with an approval delta of `-1`, reducing the approval requirement from `1` +back to `0`. + +{{ tutorial.code_snippet_tagged('step-9') }} + +Since a multisig account cannot sign transactions on its own, each modification is wrapped in a +. + +The inner modification transaction is converted with so it can be +embedded in the wrapping multisig transaction. + +{{ tutorial.code_snippet_tagged('step-10') }} + +Both the inner transaction and the wrapper pay a transaction fee: 0.5 XEM for the modification and 0.15 XEM for the +multisig wrapper, as shown in the [fee schedule](../../textbook/transactions.md#fee-schedule). +Both fees are deducted from the multisig account. +Cosignatories never pay fees for the transactions they initiate on behalf of a multisig. + +{{ tutorial.code_snippet_tagged('step-11') }} + +Finally, the multisig transaction is signed by a cosignatory. +In this case, both multisig transactions are initiated by {{ tutorial.var('cosignatory_key_pairs[0]') }}, whose +signature alone is enough to approve them without additional cosignatures. + +The cosignatories could also have been removed in the opposite order. +The only difference would be which cosignatory initiates and signs each transaction. + +### Submitting the Transactions + +{{ tutorial.code_snippet_tagged('step-12') }} + +The final step is to announce the transactions and wait for their confirmation, as described in the +[Transfer XEM](../transactions/transfer-xem.md) tutorial. + +When disabling the multisig, the two multisig transactions are announced sequentially. +The code waits for the first transaction to be confirmed before announcing the second one, because the second removal is +only valid once the first one has been processed. + +## Output + +The output shown below corresponds to two typical runs of the program. + +=== ":material-plus-thick: Enabling the Multisig" + + ```text linenums="1" hl_lines="2-4 8 24 30 34" + --8<-- 'devbook/accounts/configure_multisig_enable.log' + ``` + + Key points in the output: + + * **Lines 2-4**: Addresses and public keys of all involved accounts. + * **Line 8** (`Response: No cosignatories`): No cosignatories are currently configured. + * **Lines 24 and 30** (`cosignatory_public_key`): Public keys of the cosignatories that will be added. + * **Line 34** (`"min_approval_delta": 1`): The number of required cosignatures will be increased by one. + +=== ":material-minus-thick: Disabling the Multisig" + + ```text linenums="1" hl_lines="2-4 8 29-37 61-69" + --8<-- 'devbook/accounts/configure_multisig_disable.log' + ``` + + Key points in the output: + + * **Lines 2-4**: Addresses and public keys of all involved accounts. + * **Line 8** (`Response: [ ... ]`): Existing cosignatories have been detected. + * **Lines 29-37** (First multisig transaction): The number of required cosignatures will remain unchanged and one + existing cosignatory will be removed. + * **Lines 61-69** (Second multisig transaction): The number of required cosignatures will be decreased by one and + the last remaining cosignatory will be removed. + +The transaction hashes shown in the output can be used to look up the transactions in the +[NEM testnet explorer](https://testnet.nem.fyi/). + +## Conclusion + +This tutorial showed how to: + +| Step | Related documentation | +|------------------------------------------------------------------------------------|------------------------------------------------| +| [Retrieve the current multisig configuration](#determining-the-multisig-operation) | | +| [Enable a multisig account](#enabling-the-multisig) | | +| [Disable a multisig account](#disabling-the-multisig) | | +| Wrap a modification in a multisig transaction | | diff --git a/mkdocs/snippets/devbook/accounts/configure_multisig.mjs b/mkdocs/snippets/devbook/accounts/configure_multisig.mjs new file mode 100644 index 000000000..f6cf2c9b1 --- /dev/null +++ b/mkdocs/snippets/devbook/accounts/configure_multisig.mjs @@ -0,0 +1,237 @@ +import { PrivateKey } from 'symbol-sdk'; +import { + NemFacade, + NetworkTimestamp, + calculateTransactionFee, + models +} from 'symbol-sdk/nem'; + +const NODE_URL = process.env.NODE_URL || + 'http://libertalia.nemtest.net:7890'; +console.log('Using node', NODE_URL); + +const facade = new NemFacade('testnet'); +// [>step-1] +const KEY_PREFIX = '0'.repeat(63); + +// Set up the keys for the multisig account and its two cosignatories +const MULTISIG_PRIVATE_KEY = process.env.MULTISIG_PRIVATE_KEY || ( + `${KEY_PREFIX}1`); +const multisigKeyPair = new NemFacade.KeyPair( + new PrivateKey(MULTISIG_PRIVATE_KEY)); +const multisigAddress = facade.network.publicKeyToAddress( + multisigKeyPair.publicKey); +console.log(`Multisig address: ${multisigAddress}`, + `(public key ${multisigKeyPair.publicKey})`); + +const cosignatoryKeyPairs = []; +for (let i = 0; 2 > i; i++) { + const COSIGNATORY_PRIVATE_KEY = + process.env[`COSIGNATORY${i}_PRIVATE_KEY`] || ( + KEY_PREFIX + String(i + 2)); + const keyPair = new NemFacade.KeyPair( + new PrivateKey(COSIGNATORY_PRIVATE_KEY)); + cosignatoryKeyPairs.push(keyPair); + const addr = facade.network.publicKeyToAddress(keyPair.publicKey); + console.log(`Cosignatory ${i} address: ${addr}`, + `(public key ${keyPair.publicKey})`); +} +// [= attempt; ++attempt) { + const response = await fetch(`${NODE_URL}${statusPath}`); + if (response.ok) { + const confirmed = await response.json(); + console.log(`${label} confirmed in block`, + confirmed.meta.height); + isConfirmed = true; + break; + } + console.log(' Transaction status: pending'); + await new Promise(resolve => { setTimeout(resolve, 1000); }); + } + if (!isConfirmed) + console.warn(`${label} confirmation took too long.`); +} + +// Returns the cosignatory addresses of the provided multisig [>step-3] +// account, or an empty list if the account is not multisig +async function getMultisigCosignatories(address) { + const accountPath = `/account/get?address=${address}`; + console.log(`Getting cosignatories from ${accountPath}`); + const response = await fetch(`${NODE_URL}${accountPath}`); + const accountInfo = await response.json(); + const foundCosignatories = accountInfo.meta.cosignatories + .map(cosignatory => cosignatory.address); + if (0 === foundCosignatories.length) { + console.log(' Response: No cosignatories'); + return []; + } + console.log(' Response:', JSON.stringify(foundCosignatories)); + return foundCosignatories; +} +// [step-5] +// Returns a transaction that turns a regular account into a multisig +function multisigEnableTransaction(timestamp, deadline, approvalDelta) { + // Create a multisig account modification transaction + // that adds the cosignatories + const modifications = cosignatoryKeyPairs.map(keyPair => ({ + modification: { + modificationType: 'add_cosignatory', + cosignatoryPublicKey: keyPair.publicKey.toString() + } + })); + const transaction = facade.transactionFactory.create({ + type: 'multisig_account_modification_transaction_v2', + // This is the account that will be turned into a multisig + signerPublicKey: multisigKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + // Change of the number of cosignatures + // required to approve transactions + minApprovalDelta: approvalDelta, + modifications + }); + // [step-6] + const fee = calculateTransactionFee(transaction); + transaction.fee = new models.Amount(fee); + console.log(` Transaction fee: ${Number(fee) / 1_000_000} XEM`); + console.log( + 'Enabling the multisig with the modification transaction:'); + console.log(JSON.stringify(transaction.toJson(), null, 2)); + // [step-7] + const signature = facade.signTransaction( + multisigKeyPair, transaction); + facade.transactionFactory.static.attachSignature( + transaction, signature); + return transaction; // [step-8] +// Returns a transaction that removes one cosignatory from the multisig +function multisigRemovalTransaction(timestamp, deadline, + removedKeyPair, approvalDelta) { + // Create a multisig account modification transaction + // that removes a single cosignatory + const innerTransaction = facade.transactionFactory.create({ + type: 'multisig_account_modification_transaction_v2', + // This is the multisig account that will be modified + signerPublicKey: multisigKeyPair.publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + // Change of the number of cosignatures + // required to approve transactions + minApprovalDelta: approvalDelta, + modifications: [ + { + modification: { + modificationType: 'delete_cosignatory', + cosignatoryPublicKey: + removedKeyPair.publicKey.toString() + } + } + ] + }); + // [step-9] + const innerFee = calculateTransactionFee(innerTransaction); + innerTransaction.fee = new models.Amount(innerFee); + const transaction = facade.transactionFactory.create({ + type: 'multisig_transaction_v1', + // This is the cosignatory that initiates the removal + signerPublicKey: cosignatoryKeyPairs[0].publicKey.toString(), + timestamp: timestamp.timestamp, + deadline: deadline.timestamp, + innerTransaction: facade.transactionFactory.static + .toNonVerifiableTransaction(innerTransaction) + }); + // [step-10] + const fee = calculateTransactionFee(transaction); + transaction.fee = new models.Amount(fee); + console.log(' Transaction fee:', + `${Number(innerFee + fee) / 1_000_000} XEM`); + console.log( + 'Disabling the multisig with the multisig transaction:'); + console.log(JSON.stringify(transaction.toJson(), null, 2)); + // [step-11] + const signature = facade.signTransaction( + cosignatoryKeyPairs[0], transaction); + facade.transactionFactory.static + .attachSignature(transaction, signature); + return transaction; // [step-2] + const timePath = '/time-sync/network-time'; + console.log('Fetching current network time from', timePath); + const timeResponse = await fetch(`${NODE_URL}${timePath}`); + const timeJSON = await timeResponse.json(); + const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000); + console.log(' Network time:', networkTime, + 's since the nemesis block'); + + // Derived fields from network time + const timestamp = new NetworkTimestamp(networkTime); + const deadline = timestamp.addHours(2); + // [step-4] + // which operation to perform + const cosignatories = await getMultisigCosignatories(multisigAddress); + let transactions; + if (0 === cosignatories.length) { + // Enable the multisig + transactions = [multisigEnableTransaction( + timestamp, deadline, 1)]; + } else { + // Disable the multisig + transactions = [ + multisigRemovalTransaction( + timestamp, deadline, cosignatoryKeyPairs[1], 0), + multisigRemovalTransaction( + timestamp, deadline, cosignatoryKeyPairs[0], -1) + ]; + } + // [step-12] + for (const signedTransaction of transactions) { + const transactionHash = facade.hashTransaction(signedTransaction) + .toString(); + console.log('Built transaction with hash:', transactionHash); + const jsonPayload = facade.transactionFactory.static + .toJson(signedTransaction); + const result = await announceTransaction( + jsonPayload, 'transaction'); + if ('SUCCESS' !== result) { + console.log('Transaction rejected'); + break; + } + await waitForConfirmation(transactionHash, 'transaction'); + } + // [step-1] +KEY_TEMPLATE = '0' * 63 + '{}' + +# Set up the keys for the multisig account and its two cosignatories +MULTISIG_PRIVATE_KEY = os.getenv( + 'MULTISIG_PRIVATE_KEY', KEY_TEMPLATE.format(1)) +multisig_key_pair = NemFacade.KeyPair(PrivateKey(MULTISIG_PRIVATE_KEY)) +multisig_address = facade.network.public_key_to_address( + multisig_key_pair.public_key) +print(f'Multisig address: {multisig_address} ' + f'(public key {multisig_key_pair.public_key})') + +cosignatory_key_pairs = [] +for i in range(2): + COSIGNATORY_PRIVATE_KEY = os.getenv( + f'COSIGNATORY{i}_PRIVATE_KEY', KEY_TEMPLATE.format(i + 2)) + key_pair = NemFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY)) + cosignatory_key_pairs.append(key_pair) + addr = facade.network.public_key_to_address(key_pair.public_key) + print(f'Cosignatory {i} address: ' + f'{addr} (public key {key_pair.public_key})') # [step-3] +# account, or an empty list if the account is not multisig +def get_multisig_cosignatories(address): + account_path = f'/account/get?address={address}' + print(f'Getting cosignatories from {account_path}') + url = f'{NODE_URL}{account_path}' + with urllib.request.urlopen(url) as account_response: + account_info = json.loads(account_response.read().decode()) + found_cosignatories = [ + cosignatory['address'] + for cosignatory in account_info['meta']['cosignatories'] + ] + if not found_cosignatories: + print(' Response: No cosignatories') + return [] + print(f' Response: {found_cosignatories}') + return found_cosignatories # [step-5] +# Returns a transaction that turns a regular account into a multisig +def multisig_enable_transaction(tx_timestamp, tx_deadline, + approval_delta): + # Create a multisig account modification transaction + # that adds the cosignatories + modifications = [ + {'modification': { + 'modification_type': 'add_cosignatory', + 'cosignatory_public_key': key_pair.public_key + }} + for key_pair in cosignatory_key_pairs + ] + transaction = facade.transaction_factory.create({ + 'type': 'multisig_account_modification_transaction_v2', + # This is the account that will be turned into a multisig + 'signer_public_key': multisig_key_pair.public_key, + 'timestamp': tx_timestamp.timestamp, + 'deadline': tx_deadline.timestamp, + # Change of the number of cosignatures + # required to approve transactions + 'min_approval_delta': approval_delta, + 'modifications': modifications + }) + # [step-6] + fee = calculate_transaction_fee(transaction) + transaction.fee = Amount(fee) + print(f' Transaction fee: {fee / 1_000_000} XEM') + print('Enabling the multisig with the modification transaction:') + print(json.dumps(transaction.to_json(), indent=2)) + # [step-7] + signature = facade.sign_transaction(multisig_key_pair, transaction) + facade.transaction_factory.attach_signature(transaction, signature) + return transaction # [step-8] +# Returns a transaction that removes one cosignatory from the multisig +def multisig_removal_transaction(tx_timestamp, tx_deadline, + removed_key_pair, approval_delta): + # Create a multisig account modification transaction + # that removes a single cosignatory + inner_transaction = facade.transaction_factory.create({ + 'type': 'multisig_account_modification_transaction_v2', + # This is the multisig account that will be modified + 'signer_public_key': multisig_key_pair.public_key, + 'timestamp': tx_timestamp.timestamp, + 'deadline': tx_deadline.timestamp, + # Change of the number of cosignatures + # required to approve transactions + 'min_approval_delta': approval_delta, + 'modifications': [ + {'modification': { + 'modification_type': 'delete_cosignatory', + 'cosignatory_public_key': removed_key_pair.public_key + }} + ] + }) + # [step-9] + inner_fee = calculate_transaction_fee(inner_transaction) + inner_transaction.fee = Amount(inner_fee) + transaction = facade.transaction_factory.create({ + 'type': 'multisig_transaction_v1', + # This is the cosignatory that initiates the removal + 'signer_public_key': cosignatory_key_pairs[0].public_key, + 'timestamp': tx_timestamp.timestamp, + 'deadline': tx_deadline.timestamp, + 'inner_transaction': + facade.transaction_factory.to_non_verifiable_transaction( + inner_transaction) + }) + # [step-10] + fee = calculate_transaction_fee(transaction) + transaction.fee = Amount(fee) + print(f' Transaction fee: {(inner_fee + fee) / 1_000_000} XEM') + print('Disabling the multisig with the multisig transaction:') + print(json.dumps(transaction.to_json(), indent=2)) + # [step-11] + signature = facade.sign_transaction( + cosignatory_key_pairs[0], transaction) + facade.transaction_factory.attach_signature(transaction, signature) + return transaction # [step-2] + time_path = '/time-sync/network-time' + print(f'Fetching current network time from {time_path}') + with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response: + response_json = json.loads(response.read().decode()) + network_time = response_json['receiveTimeStamp'] // 1000 + print(f' Network time: {network_time} s since the nemesis block') + + # Derived fields from network time + timestamp = NetworkTimestamp(network_time) + deadline = timestamp.add_hours(2) + # [step-4] + # operation to perform + cosignatories = get_multisig_cosignatories(multisig_address) + if len(cosignatories) == 0: + # Enable the multisig + transactions = [multisig_enable_transaction( + timestamp, deadline, 1)] + else: + # Disable the multisig + transactions = [ + multisig_removal_transaction( + timestamp, deadline, cosignatory_key_pairs[1], 0), + multisig_removal_transaction( + timestamp, deadline, cosignatory_key_pairs[0], -1) + ] + # [step-12] + for signed_transaction in transactions: + transaction_hash = facade.hash_transaction(signed_transaction) + print(f'Built transaction with hash: {transaction_hash}') + json_payload = facade.transaction_factory.to_json( + signed_transaction) + announce_result = announce_transaction( + json_payload, 'transaction') + if 'SUCCESS' != announce_result: + print('Transaction rejected') + break + wait_for_confirmation(transaction_hash, 'transaction') + # [ Date: Mon, 3 Aug 2026 12:00:01 +0200 Subject: [PATCH 2/2] [mkdocs] reviewer feedback --- .../pages/en/devbook/accounts/configure-multisig.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mkdocs/pages/en/devbook/accounts/configure-multisig.md b/mkdocs/pages/en/devbook/accounts/configure-multisig.md index da01e2220..9ddd6dc3e 100644 --- a/mkdocs/pages/en/devbook/accounts/configure-multisig.md +++ b/mkdocs/pages/en/devbook/accounts/configure-multisig.md @@ -98,6 +98,17 @@ are derived from it, following the process described in the [Transfer XEM](../tr This helper retrieves the list of current cosignatories for a given address using the endpoint. If it returns an empty list, the account is not currently configured as a multisig account. +!!! warning "Check the existing multisig configuration" + + For simplicity, the tutorial assumes that if the list of cosignatories is _not_ empty, then the account is a + multisig configured by the tutorial itself. + + If the configuration is not the expected one, for example, because the cosignatories are different, + the removal transactions will be rejected. + + Applications should always check the current configuration before trying to modify it, including the full list of + cosignatories and the minimum number of signatures required. + {{ tutorial.code_snippet_tagged('step-4') }} The returned cosignatories determine whether the account is configured as a multisig account, and therefore whether to