importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.scimportAmountfromsymbolchain.symbol.FeeCalculatorimportcalculate_transaction_feefromsymbolchain.symbol.NetworkimportNetworkTimestampNODE_URL=os.getenv('NODE_URL','https://reference.symboltest.net:3001')print(f'Using node {NODE_URL}')facade=SymbolFacade('testnet')KEY_TEMPLATE='0'*63+'{}'# Setup the keys for the multisig account and its two cosignatoriesMULTISIG_PRIVATE_KEY=os.getenv('MULTISIG_PRIVATE_KEY',KEY_TEMPLATE.format(1))multisig_key_pair=SymbolFacade.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=[]cosignatory_addresses=[]foriinrange(2):COSIGNATORY_PRIVATE_KEY=os.getenv(f'COSIGNATORY{i}_PRIVATE_KEY',KEY_TEMPLATE.format(i+2))key_pair=SymbolFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))cosignatory_key_pairs.append(key_pair)addr=facade.network.public_key_to_address(key_pair.public_key)cosignatory_addresses.append(addr)print(f'Cosignatory {i} address: 'f'{addr} (public key {key_pair.public_key})')# Helper function to announce a transactiondefannounce_transaction(payload,label):print(f'Announcing {label} to /transactions')request=urllib.request.Request(f'{NODE_URL}/transactions',data=payload.encode(),headers={'Content-Type':'application/json'},method='PUT')withurllib.request.urlopen(request)asannounce_response:print(f' Response: {announce_response.read().decode()}')# Helper function to wait for transaction confirmationdefwait_for_confirmation(tx_hash,label):print(f'Waiting for {label} confirmation...')forattemptinrange(60):time.sleep(1)try:url=f'{NODE_URL}/transactionStatus/{tx_hash}'withurllib.request.urlopen(url)asconfirm_response:status=json.loads(confirm_response.read().decode())print(f' Transaction status: {status["group"]}')ifstatus['group']=='confirmed':print(f'{label} confirmed in {attempt} seconds')returnifstatus['group']=='failed':raiseRuntimeError(f'{label} failed: {status["code"]}')excepturllib.error.HTTPError:print(' Transaction status: unknown')raiseTimeoutError(f'{label} not confirmed after 60 seconds')# Returns the cosignatory addresses of the provided multisig account,# or an empty list if the account is not multisig or has never been useddefget_multisig_cosignatories(address):multisig_path=f'/account/{address}/multisig'print(f'Getting cosignatories from {multisig_path}')try:url=f'{NODE_URL}{multisig_path}'withurllib.request.urlopen(url)asmultisig_response:status=json.loads(multisig_response.read().decode())found_cosignatories=(status['multisig']['cosignatoryAddresses'])print(f' Response: {found_cosignatories}')returnfound_cosignatoriesexcepturllib.error.HTTPError:# The address has never been usedprint(' Response: No cosignatories')return[]# Returns a transaction that turns a regular account into a multisigdefmultisig_enable_transaction():# Create an embedded multisig account modification transaction# that adds two cosignatoriesembedded_transaction=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the account that will be turned into a multisig'signer_public_key':multisig_key_pair.public_key,# Increment of the number of signatures required for approvals'min_approval_delta':1,# Increment of the number of signatures required for removals'min_removal_delta':1,'address_additions':cosignatory_addresses})# Build the aggregate transactionembedded_transactions=[embedded_transaction]transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3',# This is the account that will pay for this transaction'signer_public_key':multisig_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions})# Reserve space for two cosignatures# and calculate fee for the final transaction sizetransaction.fee=Amount(calculate_transaction_fee(transaction,fee_multiplier,len(cosignatory_key_pairs)))print('Enabling the multisig with the aggregate transaction:')print(json.dumps(transaction.to_json(),indent=2))# Sign the aggregate transaction with the multisig's signaturefacade.transaction_factory.attach_signature(transaction,facade.sign_transaction(multisig_key_pair,transaction))# Append signatures from all cosignatoriesforcosignatory_key_pairincosignatory_key_pairs:transaction.cosignatures.append(facade.cosign_transaction(cosignatory_key_pair,transaction))returntransaction# Returns a transaction that turns a multisig into a regular accountdefmultisig_disable_transaction():# Create two embedded multisig account modification transactions# because cosignatories must be removed one by oneembedded_transaction_1=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the multisig account that will be modified'signer_public_key':multisig_key_pair.public_key,# Keep required signatures unchanged for this step'min_approval_delta':0,'min_removal_delta':0,'address_deletions':[cosignatory_addresses[1]]})embedded_transaction_2=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the multisig account that will be modified'signer_public_key':multisig_key_pair.public_key,# Decrease required signatures after final removal'min_approval_delta':-1,'min_removal_delta':-1,'address_deletions':[cosignatory_addresses[0]]})# Build the aggregate transactionembedded_transactions=[embedded_transaction_1,embedded_transaction_2]transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3',# This is the account that will pay for all transactions'signer_public_key':cosignatory_key_pairs[0].public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions})# Calculate fee for the final transaction size# (No need to reserve space for cosignatures, as there are none)transaction.fee=Amount(calculate_transaction_fee(transaction,fee_multiplier))print('Disabling the multisig with the aggregate transaction:')print(json.dumps(transaction.to_json(),indent=2))# Sign the aggregate transaction using the first cosigner's signaturefacade.transaction_factory.attach_signature(transaction,facade.sign_transaction(cosignatory_key_pairs[0],transaction))returntransactiontry:# Fetch current network timetime_path='/node/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())receive_timestamp=(response_json['communicationTimestamps']['receiveTimestamp'])timestamp=NetworkTimestamp(int(receive_timestamp))print(f' Network time: {timestamp.timestamp} ms since nemesis')# Fetch recommended feesfee_path='/network/fees/transaction'print(f'Fetching recommended fees from {fee_path}')withurllib.request.urlopen(f'{NODE_URL}{fee_path}')asresponse:response_json=json.loads(response.read().decode())median_multiplier=response_json['medianFeeMultiplier']minimum_multiplier=response_json['minFeeMultiplier']fee_multiplier=max(median_multiplier,minimum_multiplier)print(f' Fee multiplier: {fee_multiplier}')# Get current state of the multisig account and decide which# operation to performcosignatories=get_multisig_cosignatories(multisig_address)iflen(cosignatories)==0:# Enable the multisigagg_transaction=multisig_enable_transaction()else:# Disable the multisigagg_transaction=multisig_disable_transaction()json_payload=facade.transaction_factory.to_json(agg_transaction)# Announce and wait for confirmationagg_transaction_hash=facade.hash_transaction(agg_transaction)print(f'Built aggregate transaction with hash: {agg_transaction_hash}')announce_transaction(json_payload,'aggregate transaction')wait_for_confirmation(agg_transaction_hash,'aggregate transaction')exceptExceptionase:print(e)
import{PrivateKey}from'symbol-sdk';import{KeyPair,NetworkTimestamp,SymbolFacade,SymbolTransactionFactory,calculateTransactionFee,models}from'symbol-sdk/symbol';constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';console.log('Using node',NODE_URL);constfacade=newSymbolFacade('testnet');constKEY_PREFIX='0'.repeat(63);// Setup the keys for the multisig account and its two cosignatoriesconstMULTISIG_PRIVATE_KEY=process.env.MULTISIG_PRIVATE_KEY||(`${KEY_PREFIX}1`);constmultisigKeyPair=newKeyPair(newPrivateKey(MULTISIG_PRIVATE_KEY));constmultisigAddress=facade.network.publicKeyToAddress(multisigKeyPair.publicKey);console.log(`Multisig address: ${multisigAddress}`,`(public key ${multisigKeyPair.publicKey})`);constcosignatoryKeyPairs=[];constcosignatoryAddresses=[];for(leti=0;2>i;i++){constCOSIGNATORY_PRIVATE_KEY=process.env[`COSIGNATORY${i}_PRIVATE_KEY`]||(KEY_PREFIX+String(i+2));constkp=newKeyPair(newPrivateKey(COSIGNATORY_PRIVATE_KEY));cosignatoryKeyPairs.push(kp);constaddr=facade.network.publicKeyToAddress(kp.publicKey);cosignatoryAddresses.push(addr);console.log(`Cosignatory ${i} address: ${addr}`,`(public key ${kp.publicKey})`);}// Helper function to announce a transactionasyncfunctionannounceTransaction(payload,label){console.log(`Announcing ${label} to /transactions`);constresponse=awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:payload});console.log(' Response:',awaitresponse.text());}// Helper function to wait for transaction confirmationasyncfunctionwaitForConfirmation(transactionHash,label){console.log(`Waiting for ${label} confirmation...`);for(letattempt=0;60>attempt;attempt++){awaitnewPromise(resolve=>{setTimeout(resolve,1000);});try{constresponse=awaitfetch(`${NODE_URL}/transactionStatus/${transactionHash}`);conststatus=awaitresponse.json();console.log(' Transaction status:',status.group);if('confirmed'===status.group){console.log(`${label} confirmed in`,attempt,'seconds');return;}if('failed'===status.group)thrownewError(`${label} failed: ${status.code}`);}catch(e){if(e.message.includes('failed'))throwe;console.log(' Transaction status: unknown');}}thrownewError(`${label} not confirmed after 60 seconds`);}// Returns the cosignatory addresses of the provided multisig account,// or an empty list if the account is not multisig or has never been usedasyncfunctiongetMultisigCosignatories(address){constmultisigPath=`/account/${address}/multisig`;console.log(`Getting cosignatories from ${multisigPath}`);constresponse=awaitfetch(`${NODE_URL}${multisigPath}`);if(!response.ok){console.log(' Response: No cosignatories');return[];}constjson=awaitresponse.json();constcosignatories=json.multisig.cosignatoryAddresses;console.log(' Response:',JSON.stringify(cosignatories));returncosignatories;}// Returns a transaction that turns a regular account into a multisigfunctionmultisigEnableTransaction(timestamp,feeMultiplier){// Create an embedded multisig account modification transaction// that adds two cosignatoriesconstembeddedTransaction=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the account that will be turned into a multisigsignerPublicKey:multisigKeyPair.publicKey,// Delta of the number of signatures required for approvalsminApprovalDelta:1,// Delta of the number of signatures required for removalsminRemovalDelta:1,addressAdditions:cosignatoryAddresses});// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction];consttransaction=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',// This is the account that will pay for this transactionsignerPublicKey:multisigKeyPair.publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(embeddedTransactions),transactions:embeddedTransactions});// Reserve space for two cosignatures// and calculate fee for the final transaction sizetransaction.fee=newmodels.Amount(calculateTransactionFee(transaction,feeMultiplier,cosignatoryKeyPairs.length));console.log('Enabling the multisig with the aggregate transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));// Sign the aggregate transaction with the multisig's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(multisigKeyPair,transaction));// Append signatures from all cosignatoriesfor(constcosignatoryKeyPairofcosignatoryKeyPairs){transaction.cosignatures.push(facade.cosignTransaction(cosignatoryKeyPair,transaction));}returntransaction;}// Returns a transaction that turns a multisig into a regular accountfunctionmultisigDisableTransaction(timestamp,feeMultiplier){// Create two embedded multisig account modification transactions// because cosignatories must be removed one by oneconstembeddedTransaction1=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey,// Keep required signatures unchanged for this stepminApprovalDelta:0,minRemovalDelta:0,addressDeletions:[cosignatoryAddresses[1]]});constembeddedTransaction2=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey,// Decrease required signatures after final removalminApprovalDelta:-1,minRemovalDelta:-1,addressDeletions:[cosignatoryAddresses[0]]});// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];consttransaction=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',// This is the account that will pay for this transactionsignerPublicKey:cosignatoryKeyPairs[0].publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(embeddedTransactions),transactions:embeddedTransactions});// Calculate fee for the final transaction size// (No need to reserve space for cosignatures, as there are none)transaction.fee=newmodels.Amount(calculateTransactionFee(transaction,feeMultiplier));console.log('Disabling the multisig with the aggregate transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));// Sign the aggregate with the first cosigner's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPairs[0],transaction));returntransaction;}try{// Fetch current network timeconsttimePath='/node/time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();consttimestamp=newNetworkTimestamp(timeJSON.communicationTimestamps.receiveTimestamp);console.log(' Network time:',timestamp.timestamp,'ms since nemesis');// Fetch recommended feesconstfeePath='/network/fees/transaction';console.log('Fetching recommended fees from',feePath);constfeeResponse=awaitfetch(`${NODE_URL}${feePath}`);constfeeJSON=awaitfeeResponse.json();constmedianMultiplier=feeJSON.medianFeeMultiplier;constminimumMultiplier=feeJSON.minFeeMultiplier;constfeeMultiplier=Math.max(medianMultiplier,minimumMultiplier);console.log(' Fee multiplier:',feeMultiplier);// Get current state of the multisig account and decide which// operation to performconstcosignatories=awaitgetMultisigCosignatories(multisigAddress);lettransaction;if(0===cosignatories.length){// Enable the multisigtransaction=multisigEnableTransaction(timestamp,feeMultiplier);}else{// Disable the multisigtransaction=multisigDisableTransaction(timestamp,feeMultiplier);}constpayload=SymbolTransactionFactory.toJson(transaction);// Announce and wait for confirmationconsttransactionHash=facade.hashTransaction(transaction).toString();console.log('Built aggregate transaction with hash:',transactionHash);awaitannounceTransaction(payload,'aggregate transaction');awaitwaitForConfirmation(transactionHash,'aggregate transaction');}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
//JAVA 21+//DEPS org.symbol:symbol-sdk:3.3.1importjava.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;importjava.util.Map;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.symbol.sdk.CryptoTypes;importorg.symbol.sdk.facade.SymbolFacade;importorg.symbol.sdk.symbol.Address;importorg.symbol.sdk.symbol.FeeCalculator;importorg.symbol.sdk.symbol.KeyPair;importorg.symbol.sdk.symbol.NetworkTimestamp;importorg.symbol.sdk.symbol.SymbolTransactionFactory;importorg.symbol.sdk.symbol.models.*;finalclassConfigureMultisig{privatestaticfinalObjectMapperJSON_MAPPER=newObjectMapper();privatestaticfinalHttpClientHTTP_CLIENT=HttpClient.newHttpClient();privatestaticfinalStringKEY_PREFIX="0".repeat(63);privatefinalStringnodeUrl=System.getenv().getOrDefault("NODE_URL","https://reference.symboltest.net:3001");privatefinalSymbolFacadefacade=newSymbolFacade("testnet");privateKeyPairmultisigKeyPair;privateAddressmultisigAddress;privatefinalList<KeyPair>cosignatoryKeyPairs=newArrayList<>();privatefinalList<Address>cosignatoryAddresses=newArrayList<>();privatevoidannounceTransaction(finalStringpayload,finalStringlabel)throwsIOException,InterruptedException{System.out.printf("Announcing %s to /transactions%n",label);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+"/transactions")).header("Content-Type","application/json").PUT(HttpRequest.BodyPublishers.ofString(payload)).build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());System.out.printf(" Response: %s%n",response.body());}privatevoidwaitForConfirmation(finalStringtransactionHash,finalStringlabel)throwsIOException,InterruptedException{System.out.printf("Waiting for %s confirmation...%n",label);for(intattempt=0;60>attempt;++attempt){Thread.sleep(1000);try{finalStringstatusPath="/transactionStatus/"+transactionHash;finalHttpRequeststatusRequest=HttpRequest.newBuilder(URI.create(nodeUrl+statusPath)).GET().build();finalHttpResponse<String>statusResponse=HTTP_CLIENT.send(statusRequest,BodyHandlers.ofString());finalJsonNodestatus=JSON_MAPPER.readTree(statusResponse.body());finalStringgroup=status.get("group").asText();System.out.printf(" Transaction status: %s%n",group);if("confirmed".equals(group)){System.out.printf("%s confirmed in %d seconds%n",label,attempt);return;}if("failed".equals(group))thrownewIOException(String.format("%s failed: %s",label,status.get("code").asText()));}catch(finalIOExceptionex){if(ex.getMessage().contains("failed"))throwex;System.out.println(" Transaction status: unknown");}}thrownewIOException(String.format("%s not confirmed after 60 seconds",label));}// Returns the cosignatory addresses of the provided multisig account,// or an empty array if the account is not multisig or has// never been usedprivateJsonNodegetMultisigCosignatories(finalAddressaddress)throwsIOException,InterruptedException{finalStringmultisigPath=String.format("/account/%s/multisig",address);System.out.printf("Getting cosignatories from %s%n",multisigPath);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+multisigPath)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100){System.out.println(" Response: No cosignatories");returnJSON_MAPPER.createArrayNode();}finalJsonNodecosignatories=JSON_MAPPER.readTree(response.body()).get("multisig").get("cosignatoryAddresses");System.out.printf(" Response: %s%n",cosignatories);returncosignatories;}// Returns a transaction that turns a regular account into a multisigprivateTransactionmultisigEnableTransaction(finalNetworkTimestamptimestamp,finallongfeeMultiplier)throwsIOException{// Create an embedded multisig account modification transaction// that adds two cosignatoriesfinalEmbeddedTransactionembeddedTransaction=facade.transactionFactory.createEmbedded(Map.of("type","multisig_account_modification_transaction_v1","signerPublicKey",multisigKeyPair.getPublicKey(),"minApprovalDelta",1,"minRemovalDelta",1,// Cast one value to infer Map<String, Object>,// as expected by the SDK."addressAdditions",(Object)cosignatoryAddresses));// Build the aggregate transactionfinalList<EmbeddedTransaction>embeddedTransactions=List.of(embeddedTransaction);finalvartransaction=(AggregateCompleteTransactionV3)facade.transactionFactory.create(Map.of("type","aggregate_complete_transaction_v3","signerPublicKey",multisigKeyPair.getPublicKey(),"deadline",timestamp.addHours(2).timestamp,"transactionsHash",SymbolFacade.hashEmbeddedTransactions(embeddedTransactions),// Cast one value to infer Map<String, Object>,// as expected by the SDK."transactions",(Object)embeddedTransactions));finalintcosignatureCount=cosignatoryKeyPairs.size();transaction.setFee(newAmount(FeeCalculator.calculateTransactionFee(transaction,feeMultiplier,cosignatureCount)));System.out.println("Enabling the multisig with the aggregate transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));// Sign the aggregate transaction with the multisig's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(multisigKeyPair,transaction));// Append signatures from all cosignatoriesfinalList<Cosignature>cosignatures=newArrayList<>();for(finalKeyPaircosignatoryKeyPair:cosignatoryKeyPairs)cosignatures.add(facade.cosignTransaction(cosignatoryKeyPair,transaction));transaction.setCosignatures(cosignatures);returntransaction;}// Returns a transaction that turns a multisig into a regular accountprivateTransactionmultisigDisableTransaction(finalNetworkTimestamptimestamp,finallongfeeMultiplier)throwsIOException{// Create two embedded multisig account modification transactions// because cosignatories must be removed one by onefinalEmbeddedTransactionembeddedTransaction1=facade.transactionFactory.createEmbedded(Map.of("type","multisig_account_modification_transaction_v1","signerPublicKey",multisigKeyPair.getPublicKey(),"minApprovalDelta",0,"minRemovalDelta",0,// Cast one value to infer Map<String, Object>,// as expected by the SDK."addressDeletions",(Object)List.of(cosignatoryAddresses.get(1))));finalEmbeddedTransactionembeddedTransaction2=facade.transactionFactory.createEmbedded(Map.of("type","multisig_account_modification_transaction_v1","signerPublicKey",multisigKeyPair.getPublicKey(),"minApprovalDelta",-1,"minRemovalDelta",-1,// Cast one value to infer Map<String, Object>,// as expected by the SDK."addressDeletions",(Object)List.of(cosignatoryAddresses.get(0))));// Build the aggregate transactionfinalList<EmbeddedTransaction>embeddedTransactions=List.of(embeddedTransaction1,embeddedTransaction2);finalvartransaction=(AggregateCompleteTransactionV3)facade.transactionFactory.create(Map.of("type","aggregate_complete_transaction_v3","signerPublicKey",cosignatoryKeyPairs.get(0).getPublicKey(),"deadline",timestamp.addHours(2).timestamp,"transactionsHash",SymbolFacade.hashEmbeddedTransactions(embeddedTransactions),// Cast one value to infer Map<String, Object>,// as expected by the SDK."transactions",(Object)embeddedTransactions));transaction.setFee(newAmount(FeeCalculator.calculateTransactionFee(transaction,feeMultiplier)));System.out.println("Disabling the multisig with the aggregate transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPairs.get(0),transaction));returntransaction;}publicstaticvoidmain(finalString[]args){newConfigureMultisig().run();}privatevoidrun(){System.out.printf("Using node %s%n",nodeUrl);finalStringmultisigPrivateKey=System.getenv().getOrDefault("MULTISIG_PRIVATE_KEY",KEY_PREFIX+"1");multisigKeyPair=newKeyPair(newCryptoTypes.PrivateKey(multisigPrivateKey));multisigAddress=facade.network.publicKeyToAddress(multisigKeyPair.getPublicKey());System.out.printf("Multisig address: %s (public key %s)%n",multisigAddress,multisigKeyPair.getPublicKey());for(inti=0;2>i;++i){finalStringcosignatoryPrivateKey=System.getenv().getOrDefault(String.format("COSIGNATORY%d_PRIVATE_KEY",i),KEY_PREFIX+(i+2));finalKeyPairkeyPair=newKeyPair(newCryptoTypes.PrivateKey(cosignatoryPrivateKey));cosignatoryKeyPairs.add(keyPair);finalAddressaddress=facade.network.publicKeyToAddress(keyPair.getPublicKey());cosignatoryAddresses.add(address);System.out.printf("Cosignatory %d address: %s (public key %s)%n",i,address,keyPair.getPublicKey());}try{// Fetch current network timefinalStringtimePath="/node/time";System.out.printf("Fetching current network time from %s%n",timePath);finalHttpRequesttimeRequest=HttpRequest.newBuilder(URI.create(nodeUrl+timePath)).GET().build();finalHttpResponse<String>timeResponse=HTTP_CLIENT.send(timeRequest,BodyHandlers.ofString());finalJsonNodetimeJson=JSON_MAPPER.readTree(timeResponse.body());finalNetworkTimestamptimestamp=newNetworkTimestamp(timeJson.get("communicationTimestamps").get("receiveTimestamp").asLong());System.out.printf(" Network time: %dms since nemesis%n",timestamp.timestamp);// Fetch recommended feesfinalStringfeePath="/network/fees/transaction";System.out.printf("Fetching recommended fees from %s%n",feePath);finalHttpRequestfeeRequest=HttpRequest.newBuilder(URI.create(nodeUrl+feePath)).GET().build();finalHttpResponse<String>feeResponse=HTTP_CLIENT.send(feeRequest,BodyHandlers.ofString());finalJsonNodefeeJson=JSON_MAPPER.readTree(feeResponse.body());finallongfeeMultiplier=Math.max(feeJson.get("medianFeeMultiplier").asLong(),feeJson.get("minFeeMultiplier").asLong());System.out.printf(" Fee multiplier: %d%n",feeMultiplier);// Get current state of the multisig account and decide which// operation to performfinalJsonNodecosignatories=getMultisigCosignatories(multisigAddress);finalTransactiontransaction;if(cosignatories.isEmpty())transaction=multisigEnableTransaction(timestamp,feeMultiplier);elsetransaction=multisigDisableTransaction(timestamp,feeMultiplier);finalStringpayload=SymbolTransactionFactory.toJson(transaction);// Announce and wait for confirmationfinalStringtransactionHash=facade.hashTransaction(transaction).toString();System.out.printf("Built aggregate transaction with hash: %s%n",transactionHash);announceTransaction(payload,"aggregate transaction");waitForConfirmation(transactionHash,"aggregate transaction");}catch(finalExceptionex){System.out.printf("%s%n",ex.getMessage());}}}
KEY_TEMPLATE='0'*63+'{}'# Setup the keys for the multisig account and its two cosignatoriesMULTISIG_PRIVATE_KEY=os.getenv('MULTISIG_PRIVATE_KEY',KEY_TEMPLATE.format(1))multisig_key_pair=SymbolFacade.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=[]cosignatory_addresses=[]foriinrange(2):COSIGNATORY_PRIVATE_KEY=os.getenv(f'COSIGNATORY{i}_PRIVATE_KEY',KEY_TEMPLATE.format(i+2))key_pair=SymbolFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))cosignatory_key_pairs.append(key_pair)addr=facade.network.public_key_to_address(key_pair.public_key)cosignatory_addresses.append(addr)print(f'Cosignatory {i} address: 'f'{addr} (public key {key_pair.public_key})')
constKEY_PREFIX='0'.repeat(63);// Setup the keys for the multisig account and its two cosignatoriesconstMULTISIG_PRIVATE_KEY=process.env.MULTISIG_PRIVATE_KEY||(`${KEY_PREFIX}1`);constmultisigKeyPair=newKeyPair(newPrivateKey(MULTISIG_PRIVATE_KEY));constmultisigAddress=facade.network.publicKeyToAddress(multisigKeyPair.publicKey);console.log(`Multisig address: ${multisigAddress}`,`(public key ${multisigKeyPair.publicKey})`);constcosignatoryKeyPairs=[];constcosignatoryAddresses=[];for(leti=0;2>i;i++){constCOSIGNATORY_PRIVATE_KEY=process.env[`COSIGNATORY${i}_PRIVATE_KEY`]||(KEY_PREFIX+String(i+2));constkp=newKeyPair(newPrivateKey(COSIGNATORY_PRIVATE_KEY));cosignatoryKeyPairs.push(kp);constaddr=facade.network.publicKeyToAddress(kp.publicKey);cosignatoryAddresses.push(addr);console.log(`Cosignatory ${i} address: ${addr}`,`(public key ${kp.publicKey})`);}
# Fetch current network timetime_path='/node/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())receive_timestamp=(response_json['communicationTimestamps']['receiveTimestamp'])timestamp=NetworkTimestamp(int(receive_timestamp))print(f' Network time: {timestamp.timestamp} ms since nemesis')# Fetch recommended feesfee_path='/network/fees/transaction'print(f'Fetching recommended fees from {fee_path}')withurllib.request.urlopen(f'{NODE_URL}{fee_path}')asresponse:response_json=json.loads(response.read().decode())median_multiplier=response_json['medianFeeMultiplier']minimum_multiplier=response_json['minFeeMultiplier']fee_multiplier=max(median_multiplier,minimum_multiplier)print(f' Fee multiplier: {fee_multiplier}')
// Fetch current network timefinalStringtimePath="/node/time";System.out.printf("Fetching current network time from %s%n",timePath);finalHttpRequesttimeRequest=HttpRequest.newBuilder(URI.create(nodeUrl+timePath)).GET().build();finalHttpResponse<String>timeResponse=HTTP_CLIENT.send(timeRequest,BodyHandlers.ofString());finalJsonNodetimeJson=JSON_MAPPER.readTree(timeResponse.body());finalNetworkTimestamptimestamp=newNetworkTimestamp(timeJson.get("communicationTimestamps").get("receiveTimestamp").asLong());System.out.printf(" Network time: %dms since nemesis%n",timestamp.timestamp);// Fetch recommended feesfinalStringfeePath="/network/fees/transaction";System.out.printf("Fetching recommended fees from %s%n",feePath);finalHttpRequestfeeRequest=HttpRequest.newBuilder(URI.create(nodeUrl+feePath)).GET().build();finalHttpResponse<String>feeResponse=HTTP_CLIENT.send(feeRequest,BodyHandlers.ofString());finalJsonNodefeeJson=JSON_MAPPER.readTree(feeResponse.body());finallongfeeMultiplier=Math.max(feeJson.get("medianFeeMultiplier").asLong(),feeJson.get("minFeeMultiplier").asLong());System.out.printf(" Fee multiplier: %d%n",feeMultiplier);
# Returns the cosignatory addresses of the provided multisig account,# or an empty list if the account is not multisig or has never been useddefget_multisig_cosignatories(address):multisig_path=f'/account/{address}/multisig'print(f'Getting cosignatories from {multisig_path}')try:url=f'{NODE_URL}{multisig_path}'withurllib.request.urlopen(url)asmultisig_response:status=json.loads(multisig_response.read().decode())found_cosignatories=(status['multisig']['cosignatoryAddresses'])print(f' Response: {found_cosignatories}')returnfound_cosignatoriesexcepturllib.error.HTTPError:# The address has never been usedprint(' Response: No cosignatories')return[]
// Returns the cosignatory addresses of the provided multisig account,// or an empty list if the account is not multisig or has never been usedasyncfunctiongetMultisigCosignatories(address){constmultisigPath=`/account/${address}/multisig`;console.log(`Getting cosignatories from ${multisigPath}`);constresponse=awaitfetch(`${NODE_URL}${multisigPath}`);if(!response.ok){console.log(' Response: No cosignatories');return[];}constjson=awaitresponse.json();constcosignatories=json.multisig.cosignatoryAddresses;console.log(' Response:',JSON.stringify(cosignatories));returncosignatories;}
// Returns the cosignatory addresses of the provided multisig account,// or an empty array if the account is not multisig or has// never been usedprivateJsonNodegetMultisigCosignatories(finalAddressaddress)throwsIOException,InterruptedException{finalStringmultisigPath=String.format("/account/%s/multisig",address);System.out.printf("Getting cosignatories from %s%n",multisigPath);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+multisigPath)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100){System.out.println(" Response: No cosignatories");returnJSON_MAPPER.createArrayNode();}finalJsonNodecosignatories=JSON_MAPPER.readTree(response.body()).get("multisig").get("cosignatoryAddresses");System.out.printf(" Response: %s%n",cosignatories);returncosignatories;}
# Get current state of the multisig account and decide which# operation to performcosignatories=get_multisig_cosignatories(multisig_address)iflen(cosignatories)==0:# Enable the multisigagg_transaction=multisig_enable_transaction()else:# Disable the multisigagg_transaction=multisig_disable_transaction()json_payload=facade.transaction_factory.to_json(agg_transaction)
// Get current state of the multisig account and decide which// operation to performconstcosignatories=awaitgetMultisigCosignatories(multisigAddress);lettransaction;if(0===cosignatories.length){// Enable the multisigtransaction=multisigEnableTransaction(timestamp,feeMultiplier);}else{// Disable the multisigtransaction=multisigDisableTransaction(timestamp,feeMultiplier);}constpayload=SymbolTransactionFactory.toJson(transaction);
// Get current state of the multisig account and decide which// operation to performfinalJsonNodecosignatories=getMultisigCosignatories(multisigAddress);finalTransactiontransaction;if(cosignatories.isEmpty())transaction=multisigEnableTransaction(timestamp,feeMultiplier);elsetransaction=multisigDisableTransaction(timestamp,feeMultiplier);finalStringpayload=SymbolTransactionFactory.toJson(transaction);
# Create an embedded multisig account modification transaction# that adds two cosignatoriesembedded_transaction=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the account that will be turned into a multisig'signer_public_key':multisig_key_pair.public_key,# Increment of the number of signatures required for approvals'min_approval_delta':1,# Increment of the number of signatures required for removals'min_removal_delta':1,'address_additions':cosignatory_addresses})
// Create an embedded multisig account modification transaction// that adds two cosignatoriesconstembeddedTransaction=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the account that will be turned into a multisigsignerPublicKey:multisigKeyPair.publicKey,// Delta of the number of signatures required for approvalsminApprovalDelta:1,// Delta of the number of signatures required for removalsminRemovalDelta:1,addressAdditions:cosignatoryAddresses});
// Create an embedded multisig account modification transaction// that adds two cosignatoriesfinalEmbeddedTransactionembeddedTransaction=facade.transactionFactory.createEmbedded(Map.of("type","multisig_account_modification_transaction_v1","signerPublicKey",multisigKeyPair.getPublicKey(),"minApprovalDelta",1,"minRemovalDelta",1,// Cast one value to infer Map<String, Object>,// as expected by the SDK."addressAdditions",(Object)cosignatoryAddresses));
# Build the aggregate transactionembedded_transactions=[embedded_transaction]transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3',# This is the account that will pay for this transaction'signer_public_key':multisig_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions})# Reserve space for two cosignatures# and calculate fee for the final transaction sizetransaction.fee=Amount(calculate_transaction_fee(transaction,fee_multiplier,len(cosignatory_key_pairs)))print('Enabling the multisig with the aggregate transaction:')print(json.dumps(transaction.to_json(),indent=2))
// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction];consttransaction=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',// This is the account that will pay for this transactionsignerPublicKey:multisigKeyPair.publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(embeddedTransactions),transactions:embeddedTransactions});// Reserve space for two cosignatures// and calculate fee for the final transaction sizetransaction.fee=newmodels.Amount(calculateTransactionFee(transaction,feeMultiplier,cosignatoryKeyPairs.length));console.log('Enabling the multisig with the aggregate transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));
// Build the aggregate transactionfinalList<EmbeddedTransaction>embeddedTransactions=List.of(embeddedTransaction);finalvartransaction=(AggregateCompleteTransactionV3)facade.transactionFactory.create(Map.of("type","aggregate_complete_transaction_v3","signerPublicKey",multisigKeyPair.getPublicKey(),"deadline",timestamp.addHours(2).timestamp,"transactionsHash",SymbolFacade.hashEmbeddedTransactions(embeddedTransactions),// Cast one value to infer Map<String, Object>,// as expected by the SDK."transactions",(Object)embeddedTransactions));finalintcosignatureCount=cosignatoryKeyPairs.size();transaction.setFee(newAmount(FeeCalculator.calculateTransactionFee(transaction,feeMultiplier,cosignatureCount)));System.out.println("Enabling the multisig with the aggregate transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));
# Sign the aggregate transaction with the multisig's signaturefacade.transaction_factory.attach_signature(transaction,facade.sign_transaction(multisig_key_pair,transaction))# Append signatures from all cosignatoriesforcosignatory_key_pairincosignatory_key_pairs:transaction.cosignatures.append(facade.cosign_transaction(cosignatory_key_pair,transaction))
// Sign the aggregate transaction with the multisig's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(multisigKeyPair,transaction));// Append signatures from all cosignatoriesfor(constcosignatoryKeyPairofcosignatoryKeyPairs){transaction.cosignatures.push(facade.cosignTransaction(cosignatoryKeyPair,transaction));}
// Sign the aggregate transaction with the multisig's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(multisigKeyPair,transaction));// Append signatures from all cosignatoriesfinalList<Cosignature>cosignatures=newArrayList<>();for(finalKeyPaircosignatoryKeyPair:cosignatoryKeyPairs)cosignatures.add(facade.cosignTransaction(cosignatoryKeyPair,transaction));transaction.setCosignatures(cosignatures);
# Create two embedded multisig account modification transactions# because cosignatories must be removed one by oneembedded_transaction_1=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the multisig account that will be modified'signer_public_key':multisig_key_pair.public_key,# Keep required signatures unchanged for this step'min_approval_delta':0,'min_removal_delta':0,'address_deletions':[cosignatory_addresses[1]]})embedded_transaction_2=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the multisig account that will be modified'signer_public_key':multisig_key_pair.public_key,# Decrease required signatures after final removal'min_approval_delta':-1,'min_removal_delta':-1,'address_deletions':[cosignatory_addresses[0]]})
// Create two embedded multisig account modification transactions// because cosignatories must be removed one by oneconstembeddedTransaction1=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey,// Keep required signatures unchanged for this stepminApprovalDelta:0,minRemovalDelta:0,addressDeletions:[cosignatoryAddresses[1]]});constembeddedTransaction2=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey,// Decrease required signatures after final removalminApprovalDelta:-1,minRemovalDelta:-1,addressDeletions:[cosignatoryAddresses[0]]});
// Create two embedded multisig account modification transactions// because cosignatories must be removed one by onefinalEmbeddedTransactionembeddedTransaction1=facade.transactionFactory.createEmbedded(Map.of("type","multisig_account_modification_transaction_v1","signerPublicKey",multisigKeyPair.getPublicKey(),"minApprovalDelta",0,"minRemovalDelta",0,// Cast one value to infer Map<String, Object>,// as expected by the SDK."addressDeletions",(Object)List.of(cosignatoryAddresses.get(1))));finalEmbeddedTransactionembeddedTransaction2=facade.transactionFactory.createEmbedded(Map.of("type","multisig_account_modification_transaction_v1","signerPublicKey",multisigKeyPair.getPublicKey(),"minApprovalDelta",-1,"minRemovalDelta",-1,// Cast one value to infer Map<String, Object>,// as expected by the SDK."addressDeletions",(Object)List.of(cosignatoryAddresses.get(0))));
# Build the aggregate transactionembedded_transactions=[embedded_transaction_1,embedded_transaction_2]transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3',# This is the account that will pay for all transactions'signer_public_key':cosignatory_key_pairs[0].public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions})# Calculate fee for the final transaction size# (No need to reserve space for cosignatures, as there are none)transaction.fee=Amount(calculate_transaction_fee(transaction,fee_multiplier))print('Disabling the multisig with the aggregate transaction:')print(json.dumps(transaction.to_json(),indent=2))# Sign the aggregate transaction using the first cosigner's signaturefacade.transaction_factory.attach_signature(transaction,facade.sign_transaction(cosignatory_key_pairs[0],transaction))
// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];consttransaction=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',// This is the account that will pay for this transactionsignerPublicKey:cosignatoryKeyPairs[0].publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(embeddedTransactions),transactions:embeddedTransactions});// Calculate fee for the final transaction size// (No need to reserve space for cosignatures, as there are none)transaction.fee=newmodels.Amount(calculateTransactionFee(transaction,feeMultiplier));console.log('Disabling the multisig with the aggregate transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));// Sign the aggregate with the first cosigner's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPairs[0],transaction));
// Build the aggregate transactionfinalList<EmbeddedTransaction>embeddedTransactions=List.of(embeddedTransaction1,embeddedTransaction2);finalvartransaction=(AggregateCompleteTransactionV3)facade.transactionFactory.create(Map.of("type","aggregate_complete_transaction_v3","signerPublicKey",cosignatoryKeyPairs.get(0).getPublicKey(),"deadline",timestamp.addHours(2).timestamp,"transactionsHash",SymbolFacade.hashEmbeddedTransactions(embeddedTransactions),// Cast one value to infer Map<String, Object>,// as expected by the SDK."transactions",(Object)embeddedTransactions));transaction.setFee(newAmount(FeeCalculator.calculateTransactionFee(transaction,feeMultiplier)));System.out.println("Disabling the multisig with the aggregate transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPairs.get(0),transaction));
# Announce and wait for confirmationagg_transaction_hash=facade.hash_transaction(agg_transaction)print(f'Built aggregate transaction with hash: {agg_transaction_hash}')announce_transaction(json_payload,'aggregate transaction')wait_for_confirmation(agg_transaction_hash,'aggregate transaction')
// Announce and wait for confirmationconsttransactionHash=facade.hashTransaction(transaction).toString();console.log('Built aggregate transaction with hash:',transactionHash);awaitannounceTransaction(payload,'aggregate transaction');awaitwaitForConfirmation(transactionHash,'aggregate transaction');
// Announce and wait for confirmationfinalStringtransactionHash=facade.hashTransaction(transaction).toString();System.out.printf("Built aggregate transaction with hash: %s%n",transactionHash);announceTransaction(payload,"aggregate transaction");waitForConfirmation(transactionHash,"aggregate transaction");