A multisignature account, 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:
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.NemFacadeimportNemFacadefromsymbolchain.ncimportAmountfromsymbolchain.nem.FeeCalculatorimportcalculate_transaction_feefromsymbolchain.nem.NetworkimportNetworkTimestampNODE_URL=os.getenv('NODE_URL','http://libertalia.nemtest.net:7890')print(f'Using node {NODE_URL}')facade=NemFacade('testnet')KEY_TEMPLATE='0'*63+'{}'# Set up the keys for the multisig account and its two cosignatoriesMULTISIG_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=[]foriinrange(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})')# Helper function to announce a transactiondefannounce_transaction(payload,label):announce_path='/transaction/announce'print(f'Announcing {label} to {announce_path}')request=urllib.request.Request(f'{NODE_URL}{announce_path}',data=payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(request)asannounce_response:result=json.loads(announce_response.read().decode())print(f' Result: {result["message"]}')returnresult['message']# Helper function to wait for transaction confirmationdefwait_for_confirmation(tx_hash,label):status_path=f'/transaction/get?hash={tx_hash}'print(f'Waiting for {label} confirmation from {status_path}')is_confirmed=Falsefor_inrange(120):try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asstatus_response:confirmed=json.loads(status_response.read().decode())height=confirmed['meta']['height']print(f'{label} confirmed in block {height}')is_confirmed=Truebreakexcepturllib.error.HTTPError:print(' Transaction status: pending')time.sleep(1)ifnotis_confirmed:print(f'{label} confirmation took too long.')# Returns the cosignatory addresses of the provided multisig# account, or an empty list if the account is not multisigdefget_multisig_cosignatories(address):account_path=f'/account/get?address={address}'print(f'Getting cosignatories from {account_path}')url=f'{NODE_URL}{account_path}'withurllib.request.urlopen(url)asaccount_response:account_info=json.loads(account_response.read().decode())found_cosignatories=[cosignatory['address']forcosignatoryinaccount_info['meta']['cosignatories']]ifnotfound_cosignatories:print(' Response: No cosignatories')return[]print(f' Response: {found_cosignatories}')returnfound_cosignatories# Returns a transaction that turns a regular account into a multisigdefmultisig_enable_transaction(tx_timestamp,tx_deadline,approval_delta):# Create a multisig account modification transaction# that adds the cosignatoriesmodifications=[{'modification':{'modification_type':'add_cosignatory','cosignatory_public_key':key_pair.public_key}}forkey_pairincosignatory_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})# Calculate and attach the transaction feefee=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))# Sign the transaction with the multisig's keysignature=facade.sign_transaction(multisig_key_pair,transaction)facade.transaction_factory.attach_signature(transaction,signature)returntransaction# Returns a transaction that removes one cosignatory from the multisigdefmultisig_removal_transaction(tx_timestamp,tx_deadline,removed_key_pair,approval_delta):# Create a multisig account modification transaction# that removes a single cosignatoryinner_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}}]})# Wrap the modification in a multisig transactioninner_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)})# Calculate and attach the transaction feefee=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))# Sign the transaction with the cosignatory's keysignature=facade.sign_transaction(cosignatory_key_pairs[0],transaction)facade.transaction_factory.attach_signature(transaction,signature)returntransactiontry:# Fetch current network timetime_path='/time-sync/network-time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())network_time=response_json['receiveTimeStamp']//1000print(f' Network time: {network_time} s since the nemesis block')# Derived fields from network timetimestamp=NetworkTimestamp(network_time)deadline=timestamp.add_hours(2)# Get current state of the multisig account and decide which# operation to performcosignatories=get_multisig_cosignatories(multisig_address)iflen(cosignatories)==0:# Enable the multisigtransactions=[multisig_enable_transaction(timestamp,deadline,1)]else:# Disable the multisigtransactions=[multisig_removal_transaction(timestamp,deadline,cosignatory_key_pairs[1],0),multisig_removal_transaction(timestamp,deadline,cosignatory_key_pairs[0],-1)]# Announce each transaction and wait for confirmationforsigned_transactionintransactions: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')breakwait_for_confirmation(transaction_hash,'transaction')exceptExceptionase:print(e)
import{PrivateKey}from'symbol-sdk';import{NemFacade,NetworkTimestamp,calculateTransactionFee,models}from'symbol-sdk/nem';constNODE_URL=process.env.NODE_URL||'http://libertalia.nemtest.net:7890';console.log('Using node',NODE_URL);constfacade=newNemFacade('testnet');constKEY_PREFIX='0'.repeat(63);// Set up the keys for the multisig account and its two cosignatoriesconstMULTISIG_PRIVATE_KEY=process.env.MULTISIG_PRIVATE_KEY||(`${KEY_PREFIX}1`);constmultisigKeyPair=newNemFacade.KeyPair(newPrivateKey(MULTISIG_PRIVATE_KEY));constmultisigAddress=facade.network.publicKeyToAddress(multisigKeyPair.publicKey);console.log(`Multisig address: ${multisigAddress}`,`(public key ${multisigKeyPair.publicKey})`);constcosignatoryKeyPairs=[];for(leti=0;2>i;i++){constCOSIGNATORY_PRIVATE_KEY=process.env[`COSIGNATORY${i}_PRIVATE_KEY`]||(KEY_PREFIX+String(i+2));constkeyPair=newNemFacade.KeyPair(newPrivateKey(COSIGNATORY_PRIVATE_KEY));cosignatoryKeyPairs.push(keyPair);constaddr=facade.network.publicKeyToAddress(keyPair.publicKey);console.log(`Cosignatory ${i} address: ${addr}`,`(public key ${keyPair.publicKey})`);}// Helper function to announce a transactionasyncfunctionannounceTransaction(payload,label){constannouncePath='/transaction/announce';console.log(`Announcing ${label} to ${announcePath}`);constannounceResponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'POST',headers:{'Content-Type':'application/json'},body:payload});constresult=awaitannounceResponse.json();console.log(' Result:',result.message);returnresult.message;}// Helper function to wait for transaction confirmationasyncfunctionwaitForConfirmation(transactionHash,label){conststatusPath=`/transaction/get?hash=${transactionHash}`;console.log(`Waiting for ${label} confirmation from`,statusPath);letisConfirmed=false;for(letattempt=1;120>=attempt;++attempt){constresponse=awaitfetch(`${NODE_URL}${statusPath}`);if(response.ok){constconfirmed=awaitresponse.json();console.log(`${label} confirmed in block`,confirmed.meta.height);isConfirmed=true;break;}console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}if(!isConfirmed)console.warn(`${label} confirmation took too long.`);}// Returns the cosignatory addresses of the provided multisig// account, or an empty list if the account is not multisigasyncfunctiongetMultisigCosignatories(address){constaccountPath=`/account/get?address=${address}`;console.log(`Getting cosignatories from ${accountPath}`);constresponse=awaitfetch(`${NODE_URL}${accountPath}`);constaccountInfo=awaitresponse.json();constfoundCosignatories=accountInfo.meta.cosignatories.map(cosignatory=>cosignatory.address);if(0===foundCosignatories.length){console.log(' Response: No cosignatories');return[];}console.log(' Response:',JSON.stringify(foundCosignatories));returnfoundCosignatories;}// Returns a transaction that turns a regular account into a multisigfunctionmultisigEnableTransaction(timestamp,deadline,approvalDelta){// Create a multisig account modification transaction// that adds the cosignatoriesconstmodifications=cosignatoryKeyPairs.map(keyPair=>({modification:{modificationType:'add_cosignatory',cosignatoryPublicKey:keyPair.publicKey.toString()}}));consttransaction=facade.transactionFactory.create({type:'multisig_account_modification_transaction_v2',// This is the account that will be turned into a multisigsignerPublicKey:multisigKeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,// Change of the number of cosignatures// required to approve transactionsminApprovalDelta:approvalDelta,modifications});// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction);transaction.fee=newmodels.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));// Sign the transaction with the multisig's keyconstsignature=facade.signTransaction(multisigKeyPair,transaction);facade.transactionFactory.static.attachSignature(transaction,signature);returntransaction;}// Returns a transaction that removes one cosignatory from the multisigfunctionmultisigRemovalTransaction(timestamp,deadline,removedKeyPair,approvalDelta){// Create a multisig account modification transaction// that removes a single cosignatoryconstinnerTransaction=facade.transactionFactory.create({type:'multisig_account_modification_transaction_v2',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,// Change of the number of cosignatures// required to approve transactionsminApprovalDelta:approvalDelta,modifications:[{modification:{modificationType:'delete_cosignatory',cosignatoryPublicKey:removedKeyPair.publicKey.toString()}}]});// Wrap the modification in a multisig transactionconstinnerFee=calculateTransactionFee(innerTransaction);innerTransaction.fee=newmodels.Amount(innerFee);consttransaction=facade.transactionFactory.create({type:'multisig_transaction_v1',// This is the cosignatory that initiates the removalsignerPublicKey:cosignatoryKeyPairs[0].publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,innerTransaction:facade.transactionFactory.static.toNonVerifiableTransaction(innerTransaction)});// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction);transaction.fee=newmodels.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));// Sign the transaction with the cosignatory's keyconstsignature=facade.signTransaction(cosignatoryKeyPairs[0],transaction);facade.transactionFactory.static.attachSignature(transaction,signature);returntransaction;}try{// Fetch current network timeconsttimePath='/time-sync/network-time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();constnetworkTime=Math.floor(timeJSON.receiveTimeStamp/1000);console.log(' Network time:',networkTime,'s since the nemesis block');// Derived fields from network timeconsttimestamp=newNetworkTimestamp(networkTime);constdeadline=timestamp.addHours(2);// Get current state of the multisig account and decide// which operation to performconstcosignatories=awaitgetMultisigCosignatories(multisigAddress);lettransactions;if(0===cosignatories.length){// Enable the multisigtransactions=[multisigEnableTransaction(timestamp,deadline,1)];}else{// Disable the multisigtransactions=[multisigRemovalTransaction(timestamp,deadline,cosignatoryKeyPairs[1],0),multisigRemovalTransaction(timestamp,deadline,cosignatoryKeyPairs[0],-1)];}// Announce each transaction and wait for confirmationfor(constsignedTransactionoftransactions){consttransactionHash=facade.hashTransaction(signedTransaction).toString();console.log('Built transaction with hash:',transactionHash);constjsonPayload=facade.transactionFactory.static.toJson(signedTransaction);constresult=awaitannounceTransaction(jsonPayload,'transaction');if('SUCCESS'!==result){console.log('Transaction rejected');break;}awaitwaitForConfirmation(transactionHash,'transaction');}}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
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 tutorial.
The remaining helper functions are described in the sections below.
Depending on whether the account is already configured as a multisig,
transactions are created to enable or disable it as appropriate.
Finally, the transactions are announced and confirmed.
KEY_TEMPLATE='0'*63+'{}'# Set up the keys for the multisig account and its two cosignatoriesMULTISIG_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=[]foriinrange(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})')
constKEY_PREFIX='0'.repeat(63);// Set up the keys for the multisig account and its two cosignatoriesconstMULTISIG_PRIVATE_KEY=process.env.MULTISIG_PRIVATE_KEY||(`${KEY_PREFIX}1`);constmultisigKeyPair=newNemFacade.KeyPair(newPrivateKey(MULTISIG_PRIVATE_KEY));constmultisigAddress=facade.network.publicKeyToAddress(multisigKeyPair.publicKey);console.log(`Multisig address: ${multisigAddress}`,`(public key ${multisigKeyPair.publicKey})`);constcosignatoryKeyPairs=[];for(leti=0;2>i;i++){constCOSIGNATORY_PRIVATE_KEY=process.env[`COSIGNATORY${i}_PRIVATE_KEY`]||(KEY_PREFIX+String(i+2));constkeyPair=newNemFacade.KeyPair(newPrivateKey(COSIGNATORY_PRIVATE_KEY));cosignatoryKeyPairs.push(keyPair);constaddr=facade.network.publicKeyToAddress(keyPair.publicKey);console.log(`Cosignatory ${i} address: ${addr}`,`(public key ${keyPair.publicKey})`);}
The tutorial requires three separate accounts.
Their private keys 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 key pair and address of each account for later use.
# Fetch current network timetime_path='/time-sync/network-time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())network_time=response_json['receiveTimeStamp']//1000print(f' Network time: {network_time} s since the nemesis block')# Derived fields from network timetimestamp=NetworkTimestamp(network_time)deadline=timestamp.add_hours(2)
// Fetch current network timeconsttimePath='/time-sync/network-time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();constnetworkTime=Math.floor(timeJSON.receiveTimeStamp/1000);console.log(' Network time:',networkTime,'s since the nemesis block');// Derived fields from network timeconsttimestamp=newNetworkTimestamp(networkTime);constdeadline=timestamp.addHours(2);
Network time is fetched from /time-sync/network-timeGET, and the transactions' timestamp and deadline fields
are derived from it, following the process described in the Transfer XEM tutorial.
# Returns the cosignatory addresses of the provided multisig# account, or an empty list if the account is not multisigdefget_multisig_cosignatories(address):account_path=f'/account/get?address={address}'print(f'Getting cosignatories from {account_path}')url=f'{NODE_URL}{account_path}'withurllib.request.urlopen(url)asaccount_response:account_info=json.loads(account_response.read().decode())found_cosignatories=[cosignatory['address']forcosignatoryinaccount_info['meta']['cosignatories']]ifnotfound_cosignatories:print(' Response: No cosignatories')return[]print(f' Response: {found_cosignatories}')returnfound_cosignatories
// Returns the cosignatory addresses of the provided multisig// account, or an empty list if the account is not multisigasyncfunctiongetMultisigCosignatories(address){constaccountPath=`/account/get?address=${address}`;console.log(`Getting cosignatories from ${accountPath}`);constresponse=awaitfetch(`${NODE_URL}${accountPath}`);constaccountInfo=awaitresponse.json();constfoundCosignatories=accountInfo.meta.cosignatories.map(cosignatory=>cosignatory.address);if(0===foundCosignatories.length){console.log(' Response: No cosignatories');return[];}console.log(' Response:',JSON.stringify(foundCosignatories));returnfoundCosignatories;}
This helper retrieves the list of current cosignatories for a given address using the /account/getGET endpoint.
If it returns an empty list, the account is not currently configured as a multisig account.
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.
# Get current state of the multisig account and decide which# operation to performcosignatories=get_multisig_cosignatories(multisig_address)iflen(cosignatories)==0:# Enable the multisigtransactions=[multisig_enable_transaction(timestamp,deadline,1)]else:# Disable the multisigtransactions=[multisig_removal_transaction(timestamp,deadline,cosignatory_key_pairs[1],0),multisig_removal_transaction(timestamp,deadline,cosignatory_key_pairs[0],-1)]
// Get current state of the multisig account and decide// which operation to performconstcosignatories=awaitgetMultisigCosignatories(multisigAddress);lettransactions;if(0===cosignatories.length){// Enable the multisigtransactions=[multisigEnableTransaction(timestamp,deadline,1)];}else{// Disable the multisigtransactions=[multisigRemovalTransaction(timestamp,deadline,cosignatoryKeyPairs[1],0),multisigRemovalTransaction(timestamp,deadline,cosignatoryKeyPairs[0],-1)];}
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.
# Returns a transaction that turns a regular account into a multisigdefmultisig_enable_transaction(tx_timestamp,tx_deadline,approval_delta):# Create a multisig account modification transaction# that adds the cosignatoriesmodifications=[{'modification':{'modification_type':'add_cosignatory','cosignatory_public_key':key_pair.public_key}}forkey_pairincosignatory_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})
// Returns a transaction that turns a regular account into a multisigfunctionmultisigEnableTransaction(timestamp,deadline,approvalDelta){// Create a multisig account modification transaction// that adds the cosignatoriesconstmodifications=cosignatoryKeyPairs.map(keyPair=>({modification:{modificationType:'add_cosignatory',cosignatoryPublicKey:keyPair.publicKey.toString()}}));consttransaction=facade.transactionFactory.create({type:'multisig_account_modification_transaction_v2',// This is the account that will be turned into a multisigsignerPublicKey:multisigKeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,// Change of the number of cosignatures// required to approve transactionsminApprovalDelta:approvalDelta,modifications});
All changes to the multisig configuration of an account, including adding or removing cosignatories,
are performed using a MultisigAccountModificationTransactionV2.
: public key of the account whose multisig configuration will be modified.
and : The values computed in the network time step.
: 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.
: list of changes to the account's cosignatories.
Each modification adds or removes one cosignatory, identified by its public key.
In this case, two add_cosignatory modifications add the cosignatories prepared during the
setup phase.
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
# Calculate and attach the transaction feefee=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))
// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction);transaction.fee=newmodels.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));
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.
# Sign the transaction with the multisig's keysignature=facade.sign_transaction(multisig_key_pair,transaction)facade.transaction_factory.attach_signature(transaction,signature)returntransaction
// Sign the transaction with the multisig's keyconstsignature=facade.signTransaction(multisigKeyPair,transaction);facade.transactionFactory.static.attachSignature(transaction,signature);returntransaction;
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.
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 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.
# Returns a transaction that removes one cosignatory from the multisigdefmultisig_removal_transaction(tx_timestamp,tx_deadline,removed_key_pair,approval_delta):# Create a multisig account modification transaction# that removes a single cosignatoryinner_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}}]})
// Returns a transaction that removes one cosignatory from the multisigfunctionmultisigRemovalTransaction(timestamp,deadline,removedKeyPair,approvalDelta){// Create a multisig account modification transaction// that removes a single cosignatoryconstinnerTransaction=facade.transactionFactory.create({type:'multisig_account_modification_transaction_v2',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,// Change of the number of cosignatures// required to approve transactionsminApprovalDelta:approvalDelta,modifications:[{modification:{modificationType:'delete_cosignatory',cosignatoryPublicKey:removedKeyPair.publicKey.toString()}}]});
This helper builds a MultisigAccountModificationTransactionV2 that removes a cosignatory.
It takes the cosignatory to remove and the approval delta to apply as parameters.
is set to the multisig account's public key because its configuration is being
modified.
# Wrap the modification in a multisig transactioninner_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)})
// Wrap the modification in a multisig transactionconstinnerFee=calculateTransactionFee(innerTransaction);innerTransaction.fee=newmodels.Amount(innerFee);consttransaction=facade.transactionFactory.create({type:'multisig_transaction_v1',// This is the cosignatory that initiates the removalsignerPublicKey:cosignatoryKeyPairs[0].publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,innerTransaction:facade.transactionFactory.static.toNonVerifiableTransaction(innerTransaction)});
Since a multisig account cannot sign transactions on its own, each modification is wrapped in a
MultisigTransactionV1.
The inner modification transaction is converted with so it can be
embedded in the wrapping multisig transaction.
# Calculate and attach the transaction feefee=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))
// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction);transaction.fee=newmodels.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));
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.
Both fees are deducted from the multisig account.
Cosignatories never pay fees for the transactions they initiate on behalf of a multisig.
# Sign the transaction with the cosignatory's keysignature=facade.sign_transaction(cosignatory_key_pairs[0],transaction)facade.transaction_factory.attach_signature(transaction,signature)returntransaction
// Sign the transaction with the cosignatory's keyconstsignature=facade.signTransaction(cosignatoryKeyPairs[0],transaction);facade.transactionFactory.static.attachSignature(transaction,signature);returntransaction;
Finally, the multisig transaction is signed by a cosignatory.
In this case, both multisig transactions are initiated by , 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.
# Announce each transaction and wait for confirmationforsigned_transactionintransactions: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')breakwait_for_confirmation(transaction_hash,'transaction')
// Announce each transaction and wait for confirmationfor(constsignedTransactionoftransactions){consttransactionHash=facade.hashTransaction(signedTransaction).toString();console.log('Built transaction with hash:',transactionHash);constjsonPayload=facade.transactionFactory.static.toJson(signedTransaction);constresult=awaitannounceTransaction(jsonPayload,'transaction');if('SUCCESS'!==result){console.log('Transaction rejected');break;}awaitwaitForConfirmation(transactionHash,'transaction');}
The final step is to announce the transactions and wait for their confirmation, as described in the
Transfer XEM 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.
Using node http://libertalia.nemtest.net:7890
Multisig address: TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6 (public key D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2)
Cosignatory 0 address: TAWOQNIMCCFO6MT7JLLFER746HKBBUVU7KQUSDJX (public key AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3)
Cosignatory 1 address: TC7BQFXISQEOPN2PCPPPOM3V4R3XDPNVEHEQLID4 (public key 26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23)
Fetching current network time from /time-sync/network-time
Network time: 357324204 s since the nemesis block
Getting cosignatories from /account/get?address=TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6
Response: ['TC7BQFXISQEOPN2PCPPPOM3V4R3XDPNVEHEQLID4', 'TAWOQNIMCCFO6MT7JLLFER746HKBBUVU7KQUSDJX']
Transaction fee: 0.65 XEM
Disabling the multisig with the multisig transaction:
{
"type": 4100,
"version": 1,
"network": 152,
"timestamp": 357324204,
"signer_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3",
"signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"fee": "150000",
"deadline": 357331404,
"inner_transaction": {
"type": 4097,
"version": 2,
"network": 152,
"timestamp": 357324204,
"signer_public_key": "D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2",
"signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"fee": "500000",
"deadline": 357331404,
"modifications": [
{
"modification": {
"modification_type": 2,
"cosignatory_public_key": "26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23"
}
}
],
"min_approval_delta": 0
},
"cosignatures": []
}
Transaction fee: 0.65 XEM
Disabling the multisig with the multisig transaction:
{
"type": 4100,
"version": 1,
"network": 152,
"timestamp": 357324204,
"signer_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3",
"signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"fee": "150000",
"deadline": 357331404,
"inner_transaction": {
"type": 4097,
"version": 2,
"network": 152,
"timestamp": 357324204,
"signer_public_key": "D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2",
"signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"fee": "500000",
"deadline": 357331404,
"modifications": [
{
"modification": {
"modification_type": 2,
"cosignatory_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3"
}
}
],
"min_approval_delta": -1
},
"cosignatures": []
}
Built transaction with hash: E66D3B5D36D7711C5A3C0D99C502258345E086241DF7CCD810DFB4A0ED8CC90D
Announcing transaction to /transaction/announce
Result: SUCCESS
Waiting for transaction confirmation from /transaction/get?hash=E66D3B5D36D7711C5A3C0D99C502258345E086241DF7CCD810DFB4A0ED8CC90D
Transaction status: pending
Transaction status: pending
...
transaction confirmed in block 715438
Built transaction with hash: 70CAE1EEF8432A834C0E4EBE3A1EB6A1774F4AE42939E05AE9F120A9AF456051
Announcing transaction to /transaction/announce
Result: SUCCESS
Waiting for transaction confirmation from /transaction/get?hash=70CAE1EEF8432A834C0E4EBE3A1EB6A1774F4AE42939E05AE9F120A9AF456051
Transaction status: pending
Transaction status: pending
...
transaction confirmed in block 715439
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.