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_feeNODE_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(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.create_transaction_from_descriptor({'type':'multisig_account_modification_transaction_v2',# Change of the number of cosignatures# required to approve transactions'min_approval_delta':approval_delta,'modifications':modifications},# This is the account that will be turned into a multisigmultisig_key_pair.public_key,0,2*60*60)# 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(removed_key_pair,approval_delta):# Create a multisig account modification transaction# that removes a single cosignatoryinner_transaction=facade.create_transaction_from_descriptor({'type':'multisig_account_modification_transaction_v2',# 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}}]},# This is the multisig account that will be modifiedmultisig_key_pair.public_key,0,2*60*60)# Wrap the modification in a multisig transactioninner_fee=calculate_transaction_fee(inner_transaction)inner_transaction.fee=Amount(inner_fee)transaction=facade.create_transaction_from_descriptor({'type':'multisig_transaction_v1','inner_transaction':facade.transaction_factory.to_non_verifiable_transaction(inner_transaction)},# This is the cosignatory that initiates the removalcosignatory_key_pairs[0].public_key,0,2*60*60)# 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:# 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(1)]else:# Disable the multisigtransactions=[multisig_removal_transaction(cosignatory_key_pairs[1],0),multisig_removal_transaction(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')exceptExceptionaserror:print(error)
import{PrivateKey}from'symbol-sdk';import{NemFacade,calculateTransactionFee,descriptors,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}`);constresponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'POST',headers:{'Content-Type':'application/json'},body:payload});if(!response.ok)thrownewError(`HTTP ${response.status}`);constresult=awaitresponse.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){console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}else{constconfirmed=awaitresponse.json();console.log(`${label} confirmed in block`,confirmed.meta.height);isConfirmed=true;break;}}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(approvalDelta){// Create a multisig account modification transaction// that adds the cosignatoriesconstmodifications=cosignatoryKeyPairs.map(keyPair=>newdescriptors.SizePrefixedMultisigAccountModificationDescriptor(newdescriptors.MultisigAccountModificationDescriptor(models.MultisigAccountModificationType.ADD_COSIGNATORY,keyPair.publicKey)));consttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.MultisigAccountModificationTransactionV2Descriptor(// Change of the number of cosignatures// required to approve transactionsapprovalDelta,modifications),// This is the account that will be turned into a multisigmultisigKeyPair.publicKey,0n,2*60*60);// 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(removedKeyPair,approvalDelta){// Create a multisig account modification transaction// that removes a single cosignatoryconstinnerTransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.MultisigAccountModificationTransactionV2Descriptor(// Change of the number of cosignatures// required to approve transactionsapprovalDelta,[newdescriptors.SizePrefixedMultisigAccountModificationDescriptor(newdescriptors.MultisigAccountModificationDescriptor(models.MultisigAccountModificationType.DELETE_COSIGNATORY,removedKeyPair.publicKey))]),// This is the multisig account that will be modifiedmultisigKeyPair.publicKey,0n,2*60*60);// Wrap the modification in a multisig transactionconstinnerFee=calculateTransactionFee(innerTransaction);innerTransaction.fee=newmodels.Amount(innerFee);consttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.MultisigTransactionV1Descriptor(facade.transactionFactory.static.toNonVerifiableTransaction(innerTransaction)),// This is the cosignatory that initiates the removalcosignatoryKeyPairs[0].publicKey,0n,2*60*60);// 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{// Get current state of the multisig account and decide// which operation to performconstcosignatories=awaitgetMultisigCosignatories(multisigAddress);lettransactions;if(0===cosignatories.length){// Enable the multisigtransactions=[multisigEnableTransaction(1)];}else{// Disable the multisigtransactions=[multisigRemovalTransaction(cosignatoryKeyPairs[1],0),multisigRemovalTransaction(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');}
//JAVA 21+//DEPS org.symbol:symbol-sdk:3.3.3importjava.io.IOException;importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;importjava.net.http.HttpResponse.BodyHandlers;importjava.util.ArrayList;importjava.util.List;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.symbol.sdk.CryptoTypes;importorg.symbol.sdk.facade.NemFacade;importorg.symbol.sdk.nem.Address;importorg.symbol.sdk.nem.FeeCalculator;importorg.symbol.sdk.nem.KeyPair;importorg.symbol.sdk.nem.NemTransactionFactory;importorg.symbol.sdk.nem.descriptors.*;importorg.symbol.sdk.nem.models.Amount;importorg.symbol.sdk.nem.models.MultisigAccountModificationType;importorg.symbol.sdk.nem.models.Transaction;publicfinalclassConfigureMultisig{privatestaticfinalObjectMapperJSON_MAPPER=newObjectMapper();privatestaticfinalHttpClientHTTP_CLIENT=HttpClient.newHttpClient();privatefinalStringnodeUrl=System.getenv().getOrDefault("NODE_URL","http://libertalia.nemtest.net:7890");privatefinalNemFacadefacade=newNemFacade("testnet");// Helper method to announce a transactionprivateStringannounceTransaction(finalStringpayload,finalStringlabel)throwsIOException,InterruptedException{finalStringannouncePath="/transaction/announce";System.out.printf("Announcing %s to %s%n",label,announcePath);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+announcePath)).header("Content-Type","application/json").POST(HttpRequest.BodyPublishers.ofString(payload)).build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100)thrownewIOException("HTTP "+response.statusCode());finalStringresult=JSON_MAPPER.readTree(response.body()).get("message").asText();System.out.printf(" Result: %s%n",result);returnresult;}// Helper method to wait for transaction confirmationprivatevoidwaitForConfirmation(finalStringtransactionHash,finalStringlabel)throwsIOException,InterruptedException{finalStringstatusPath=String.format("/transaction/get?hash=%s",transactionHash);System.out.printf("Waiting for %s confirmation from %s%n",label,statusPath);booleanisConfirmed=false;for(intattempt=1;120>=attempt;++attempt){finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+statusPath)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100){System.out.println(" Transaction status: pending");Thread.sleep(1000);}else{finalJsonNodeconfirmed=JSON_MAPPER.readTree(response.body());System.out.printf("%s confirmed in block %s%n",label,confirmed.get("meta").get("height").asText());isConfirmed=true;break;}}if(!isConfirmed)System.out.printf("%s confirmation took too long.%n",label);}publicstaticvoidmain(finalString[]args){try{newConfigureMultisig().run();}catch(finalExceptionex){System.out.println(null==ex.getMessage()?ex.toString():ex.getMessage());}}privatevoidrun()throwsIOException,InterruptedException{System.out.printf("Using node %s%n",nodeUrl);finalStringkeyPrefix="0".repeat(63);finalStringmultisigPrivateKey=System.getenv().getOrDefault("MULTISIG_PRIVATE_KEY",keyPrefix+"1");finalKeyPairmultisigKeyPair=newKeyPair(newCryptoTypes.PrivateKey(multisigPrivateKey));finalAddressmultisigAddress=facade.network.publicKeyToAddress(multisigKeyPair.getPublicKey());System.out.printf("Multisig address: %s (public key %s)%n",multisigAddress,multisigKeyPair.getPublicKey());finalList<KeyPair>cosignatoryKeyPairs=newArrayList<>();for(inti=0;2>i;++i){finalStringprivateKey=System.getenv().getOrDefault(String.format("COSIGNATORY%d_PRIVATE_KEY",i),keyPrefix+(i+2));finalKeyPairkeyPair=newKeyPair(newCryptoTypes.PrivateKey(privateKey));cosignatoryKeyPairs.add(keyPair);finalAddressaddress=facade.network.publicKeyToAddress(keyPair.getPublicKey());System.out.printf("Cosignatory %d address: %s (public key %s)%n",i,address,keyPair.getPublicKey());}// Get the current state of the multisig accountfinalList<String>cosignatories=getMultisigCosignatories(multisigAddress);// Decide which operation to performfinalList<Transaction>transactions;if(cosignatories.isEmpty()){transactions=List.of(multisigEnableTransaction(multisigKeyPair,cosignatoryKeyPairs,1));}else{transactions=List.of(multisigRemovalTransaction(multisigKeyPair,cosignatoryKeyPairs,cosignatoryKeyPairs.get(1),0),multisigRemovalTransaction(multisigKeyPair,cosignatoryKeyPairs,cosignatoryKeyPairs.get(0),-1));}// Announce each transaction and wait for confirmationfor(finalTransactiontransaction:transactions){finalStringhash=facade.hashTransaction(transaction).toString();System.out.printf("Built transaction with hash: %s%n",hash);finalStringresult=announceTransaction(NemTransactionFactory.toJson(transaction),"transaction");if(!"SUCCESS".equals(result)){System.out.println("Transaction rejected");break;}waitForConfirmation(hash,"transaction");}}// Returns the cosignatory addresses of the provided multisig// account, or an empty list if the account is not multisigprivateList<String>getMultisigCosignatories(finalAddressaddress)throwsIOException,InterruptedException{finalStringpath="/account/get?address="+address;System.out.printf("Getting cosignatories from %s%n",path);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+path)).GET().build();finalJsonNodefound=JSON_MAPPER.readTree(HTTP_CLIENT.send(request,BodyHandlers.ofString()).body()).get("meta").get("cosignatories");finalList<String>result=newArrayList<>();for(finalJsonNodecosignatory:found)result.add(cosignatory.get("address").asText());if(result.isEmpty())System.out.println(" Response: No cosignatories");elseSystem.out.printf(" Response: %s%n",result);returnresult;}// Returns a transaction that turns a regular account into a multisigprivateTransactionmultisigEnableTransaction(finalKeyPairmultisigKeyPair,finalList<KeyPair>cosignatoryKeyPairs,finalintapprovalDelta)throwsIOException{finalList<SizePrefixedMultisigAccountModificationDescriptor>modifications=newArrayList<>();for(finalKeyPairkeyPair:cosignatoryKeyPairs)modifications.add(newSizePrefixedMultisigAccountModificationDescriptor(newMultisigAccountModificationDescriptor(MultisigAccountModificationType.ADD_COSIGNATORY,keyPair.getPublicKey())));finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newMultisigAccountModificationTransactionV2Descriptor(approvalDelta,modifications),multisigKeyPair.getPublicKey(),0,2*60*60);// Calculate and attach the transaction feefinallongfee=FeeCalculator.calculateTransactionFee(transaction);transaction.setFee(newAmount(fee));System.out.printf(" Transaction fee: %s XEM%n",(double)fee/1_000_000);System.out.println("Enabling the multisig with the modification transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));// Sign the transaction with the multisig's keyNemTransactionFactory.attachSignature(transaction,facade.signTransaction(multisigKeyPair,transaction));returntransaction;}// Returns a transaction that removes one cosignatory from the multisigprivateTransactionmultisigRemovalTransaction(finalKeyPairmultisigKeyPair,finalList<KeyPair>cosignatoryKeyPairs,finalKeyPairremovedKeyPair,finalintapprovalDelta)throwsIOException{finalSizePrefixedMultisigAccountModificationDescriptorremoval=newSizePrefixedMultisigAccountModificationDescriptor(newMultisigAccountModificationDescriptor(MultisigAccountModificationType.DELETE_COSIGNATORY,removedKeyPair.getPublicKey()));finalTransactioninnerTransaction=facade.createTransactionFromTypedDescriptor(newMultisigAccountModificationTransactionV2Descriptor(approvalDelta,List.of(removal)),multisigKeyPair.getPublicKey(),0,2*60*60);// Wrap the modification in a multisig transactionfinallonginnerFee=FeeCalculator.calculateTransactionFee(innerTransaction);innerTransaction.setFee(newAmount(innerFee));finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newMultisigTransactionV1Descriptor(NemTransactionFactory.toNonVerifiableTransaction(innerTransaction),List.of()),cosignatoryKeyPairs.get(0).getPublicKey(),0,2*60*60);// Calculate and attach the transaction feefinallongfee=FeeCalculator.calculateTransactionFee(transaction);transaction.setFee(newAmount(fee));System.out.printf(" Transaction fee: %s XEM%n",(double)(innerFee+fee)/1_000_000);System.out.println("Disabling the multisig with the multisig transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));// Sign the transaction with the cosignatory's keyNemTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPairs.get(0),transaction));returntransaction;}}
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})`);}
# 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;}
// Returns the cosignatory addresses of the provided multisig// account, or an empty list if the account is not multisigprivateList<String>getMultisigCosignatories(finalAddressaddress)throwsIOException,InterruptedException{finalStringpath="/account/get?address="+address;System.out.printf("Getting cosignatories from %s%n",path);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+path)).GET().build();finalJsonNodefound=JSON_MAPPER.readTree(HTTP_CLIENT.send(request,BodyHandlers.ofString()).body()).get("meta").get("cosignatories");finalList<String>result=newArrayList<>();for(finalJsonNodecosignatory:found)result.add(cosignatory.get("address").asText());if(result.isEmpty())System.out.println(" Response: No cosignatories");elseSystem.out.printf(" Response: %s%n",result);returnresult;}
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(1)]else:# Disable the multisigtransactions=[multisig_removal_transaction(cosignatory_key_pairs[1],0),multisig_removal_transaction(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(1)];}else{// Disable the multisigtransactions=[multisigRemovalTransaction(cosignatoryKeyPairs[1],0),multisigRemovalTransaction(cosignatoryKeyPairs[0],-1)];}
// Decide which operation to performfinalList<Transaction>transactions;if(cosignatories.isEmpty()){transactions=List.of(multisigEnableTransaction(multisigKeyPair,cosignatoryKeyPairs,1));}else{transactions=List.of(multisigRemovalTransaction(multisigKeyPair,cosignatoryKeyPairs,cosignatoryKeyPairs.get(1),0),multisigRemovalTransaction(multisigKeyPair,cosignatoryKeyPairs,cosignatoryKeyPairs.get(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(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.create_transaction_from_descriptor({'type':'multisig_account_modification_transaction_v2',# Change of the number of cosignatures# required to approve transactions'min_approval_delta':approval_delta,'modifications':modifications},# This is the account that will be turned into a multisigmultisig_key_pair.public_key,0,2*60*60)
// Returns a transaction that turns a regular account into a multisigfunctionmultisigEnableTransaction(approvalDelta){// Create a multisig account modification transaction// that adds the cosignatoriesconstmodifications=cosignatoryKeyPairs.map(keyPair=>newdescriptors.SizePrefixedMultisigAccountModificationDescriptor(newdescriptors.MultisigAccountModificationDescriptor(models.MultisigAccountModificationType.ADD_COSIGNATORY,keyPair.publicKey)));consttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.MultisigAccountModificationTransactionV2Descriptor(// Change of the number of cosignatures// required to approve transactionsapprovalDelta,modifications),// This is the account that will be turned into a multisigmultisigKeyPair.publicKey,0n,2*60*60);
// Returns a transaction that turns a regular account into a multisigprivateTransactionmultisigEnableTransaction(finalKeyPairmultisigKeyPair,finalList<KeyPair>cosignatoryKeyPairs,finalintapprovalDelta)throwsIOException{finalList<SizePrefixedMultisigAccountModificationDescriptor>modifications=newArrayList<>();for(finalKeyPairkeyPair:cosignatoryKeyPairs)modifications.add(newSizePrefixedMultisigAccountModificationDescriptor(newMultisigAccountModificationDescriptor(MultisigAccountModificationType.ADD_COSIGNATORY,keyPair.getPublicKey())));finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newMultisigAccountModificationTransactionV2Descriptor(approvalDelta,modifications),multisigKeyPair.getPublicKey(),0,2*60*60);
All changes to the multisig configuration of an account, including adding or removing cosignatories,
are performed using a MultisigAccountModificationTransactionV2.
: 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.
The facade adds the account whose multisig configuration will be modified as signer and derives the timestamp and
deadline from the two-hour deadline duration.
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));
// Calculate and attach the transaction feefinallongfee=FeeCalculator.calculateTransactionFee(transaction);transaction.setFee(newAmount(fee));System.out.printf(" Transaction fee: %s XEM%n",(double)fee/1_000_000);System.out.println("Enabling the multisig with the modification transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));
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;
// Sign the transaction with the multisig's keyNemTransactionFactory.attachSignature(transaction,facade.signTransaction(multisigKeyPair,transaction));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(removed_key_pair,approval_delta):# Create a multisig account modification transaction# that removes a single cosignatoryinner_transaction=facade.create_transaction_from_descriptor({'type':'multisig_account_modification_transaction_v2',# 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}}]},# This is the multisig account that will be modifiedmultisig_key_pair.public_key,0,2*60*60)
// Returns a transaction that removes one cosignatory from the multisigfunctionmultisigRemovalTransaction(removedKeyPair,approvalDelta){// Create a multisig account modification transaction// that removes a single cosignatoryconstinnerTransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.MultisigAccountModificationTransactionV2Descriptor(// Change of the number of cosignatures// required to approve transactionsapprovalDelta,[newdescriptors.SizePrefixedMultisigAccountModificationDescriptor(newdescriptors.MultisigAccountModificationDescriptor(models.MultisigAccountModificationType.DELETE_COSIGNATORY,removedKeyPair.publicKey))]),// This is the multisig account that will be modifiedmultisigKeyPair.publicKey,0n,2*60*60);
// Returns a transaction that removes one cosignatory from the multisigprivateTransactionmultisigRemovalTransaction(finalKeyPairmultisigKeyPair,finalList<KeyPair>cosignatoryKeyPairs,finalKeyPairremovedKeyPair,finalintapprovalDelta)throwsIOException{finalSizePrefixedMultisigAccountModificationDescriptorremoval=newSizePrefixedMultisigAccountModificationDescriptor(newMultisigAccountModificationDescriptor(MultisigAccountModificationType.DELETE_COSIGNATORY,removedKeyPair.getPublicKey()));finalTransactioninnerTransaction=facade.createTransactionFromTypedDescriptor(newMultisigAccountModificationTransactionV2Descriptor(approvalDelta,List.of(removal)),multisigKeyPair.getPublicKey(),0,2*60*60);
This helper builds a MultisigAccountModificationTransactionV2 that removes a cosignatory.
It takes the cosignatory to remove and the approval delta to apply as parameters.
The multisig account's public key is passed as the signer 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.create_transaction_from_descriptor({'type':'multisig_transaction_v1','inner_transaction':facade.transaction_factory.to_non_verifiable_transaction(inner_transaction)},# This is the cosignatory that initiates the removalcosignatory_key_pairs[0].public_key,0,2*60*60)
// Wrap the modification in a multisig transactionconstinnerFee=calculateTransactionFee(innerTransaction);innerTransaction.fee=newmodels.Amount(innerFee);consttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.MultisigTransactionV1Descriptor(facade.transactionFactory.static.toNonVerifiableTransaction(innerTransaction)),// This is the cosignatory that initiates the removalcosignatoryKeyPairs[0].publicKey,0n,2*60*60);
// Wrap the modification in a multisig transactionfinallonginnerFee=FeeCalculator.calculateTransactionFee(innerTransaction);innerTransaction.setFee(newAmount(innerFee));finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newMultisigTransactionV1Descriptor(NemTransactionFactory.toNonVerifiableTransaction(innerTransaction),List.of()),cosignatoryKeyPairs.get(0).getPublicKey(),0,2*60*60);
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));
// Calculate and attach the transaction feefinallongfee=FeeCalculator.calculateTransactionFee(transaction);transaction.setFee(newAmount(fee));System.out.printf(" Transaction fee: %s XEM%n",(double)(innerFee+fee)/1_000_000);System.out.println("Disabling the multisig with the multisig transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));
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;
// Sign the transaction with the cosignatory's keyNemTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPairs.get(0),transaction));returntransaction;
Finally, each multisig transaction is signed by the cosignatory that initiates it, whose public key is passed to the
facade as the wrapper's signer.
Here, both removals are initiated and signed by .
In this case, a single signature is enough because this multisig requires only one cosignature.
In stricter configurations, a removal requires approval from additional cosignatories, like any other transaction,
although the signature of the cosignatory being removed never counts toward the requirement.
The removal of the last remaining cosignatory is a special case.
Only cosignatories can sign transactions on behalf of the multisig account, so the last cosignatory signs its own
removal, as the second transaction in this tutorial shows.
The cosignatories could also have been removed in the opposite order.
The only difference would be which cosignatory initiates and signs each transaction.
Disabling other configurations
If the removal transactions are rejected, the account may have been configured differently from this tutorial's
default, such as a 2-of-2 multisig instead of a 1-of-2.
Check the number of required cosignatures in the minCosignatories field returned by /account/getGET, and
adjust the removal transactions as needed.
For example, to disable a 2-of-2 multisig:
Remove Cosignatory 1 with min_approval_delta set to -1,
because the account cannot keep requiring two cosignatures once a single cosignatory remains.
Once confirmed, remove Cosignatory 0 with min_approval_delta set to -1.
In this case, Cosignatory 0 can still sign both removals, since the signature of the cosignatory being removed
never counts toward the required approvals.
# 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');}
// Announce each transaction and wait for confirmationfor(finalTransactiontransaction:transactions){finalStringhash=facade.hashTransaction(transaction).toString();System.out.printf("Built transaction with hash: %s%n",hash);finalStringresult=announceTransaction(NemTransactionFactory.toJson(transaction),"transaction");if(!"SUCCESS".equals(result)){System.out.println("Transaction rejected");break;}waitForConfirmation(hash,"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.
Lines 2-4: Addresses and public keys of all involved accounts.
Line 6 (Response: [ ... ]): Existing cosignatories have been detected.
Lines 27-35 (First multisig transaction): The number of required cosignatures will remain unchanged and one
existing cosignatory will be removed.
Lines 59-67 (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.