This tutorial demonstrates how to restrict an account's outgoing transactions
so that it can only send transactions to a single authorized address.
If the restriction is already enabled, the tutorial instead demonstrates how to remove it.
After enabling or disabling the restriction, a test transfer transaction to an unauthorized address is announced,
showing how the network rejects it.
Difference with Mosaic Restrictions
Symbol also supports mosaic restrictions, which are defined at the mosaic level rather than
at the account level as shown in this tutorial.
These are distinct mechanisms.
They are configured using different transaction types and operate under different rules.
However, account restrictions can limit which mosaics an account may interact with, and
mosaic restrictions can limit which accounts may interact with a mosaic.
The conceptual overlap is therefore a common source of confusion.
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportAddress,SymbolFacadefromsymbolchain.scimportAccountRestrictionFlagsNODE_URL=os.getenv('NODE_URL','https://reference.symboltest.net:3001')print(f'Using node {NODE_URL}')facade=SymbolFacade('testnet')SIGNER_PRIVATE_KEY=os.getenv('SIGNER_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000000')signer_key_pair=SymbolFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))signer_address=facade.network.public_key_to_address(signer_key_pair.public_key)print(f'Signer address: {signer_address}')auth_address=Address('TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA')print(f'Authorized address: {auth_address}')# 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 list of restrictions currently applied to the accountdefget_account_restrictions(address):restrictions_path=f'/restrictions/account/{address}'print(f'Getting restrictions from {restrictions_path}')try:url=f'{NODE_URL}{restrictions_path}'withurllib.request.urlopen(url)asrestr_response:status=json.loads(restr_response.read().decode())found_restr=status['accountRestrictions']['restrictions']print(f' Response: {found_restr}')returnfound_restrexcepturllib.error.HTTPError:# The address has never been usedprint(' Response: No restrictions found')return[]# Returns a transaction that restricts an accountdefrestriction_enable_transaction():enable_transaction=facade.create_transaction_from_descriptor({'type':'account_address_restriction_transaction_v1',# Allow only OUTGOING transactions to the authorized ADDRESS'restriction_flags':AccountRestrictionFlags.ADDRESS|AccountRestrictionFlags.OUTGOING,# This is the only authorized outgoing address'restriction_additions':[auth_address]},signer_key_pair.public_key,fee_multiplier,2*60*60)print('Enabling the restriction with transaction:')print(json.dumps(enable_transaction.to_json(),indent=2))returnenable_transaction# Returns a transaction that removes a restriction from an accountdefrestriction_disable_transaction(restriction):disable_transaction=facade.create_transaction_from_descriptor({'type':'account_address_restriction_transaction_v1',# Lift restrictions for OUTGOING ADDRESSES'restriction_flags':AccountRestrictionFlags.ADDRESS|AccountRestrictionFlags.OUTGOING,# Remove all addresses currently restricted'restriction_deletions':[Address.from_decoded_address_hex_string(addr)foraddrinrestriction['values']]},signer_key_pair.public_key,fee_multiplier,2*60*60)print('Disabling the restriction with transaction:')print(json.dumps(disable_transaction.to_json(),indent=2))returndisable_transactiontry:# 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 restriction and decide which# operation to performrestrictions=get_account_restrictions(signer_address)iflen(restrictions)==0:# Enable the restrictionprint('\n--- Enabling restriction ---')agg_transaction=restriction_enable_transaction()else:# Disable the restrictionprint('\n--- Disabling restriction ---')agg_transaction=restriction_disable_transaction(restrictions[0])# Sign, announce and wait for confirmationjson_payload=facade.transaction_factory.attach_signature(agg_transaction,facade.sign_transaction(signer_key_pair,agg_transaction))transaction_hash=facade.hash_transaction(agg_transaction)announce_transaction(json_payload,'restriction transaction')wait_for_confirmation(transaction_hash,'restriction transaction')# Try a dummy transfer to a random address with no mosaicstransaction=facade.create_transaction_from_descriptor({'type':'transfer_transaction_v1','recipient_address':Address('TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y')},signer_key_pair.public_key,fee_multiplier,2*60*60)json_payload=facade.transaction_factory.attach_signature(transaction,facade.sign_transaction(signer_key_pair,transaction))transaction_hash=facade.hash_transaction(transaction)print('\n--- Attempting transfer to unauthorized address ---')announce_transaction(json_payload,'test transfer')wait_for_confirmation(transaction_hash,'test transfer')exceptExceptionase:print(e)
import{PrivateKey}from'symbol-sdk';import{Address,KeyPair,SymbolFacade,descriptors,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');constSIGNER_PRIVATE_KEY=process.env.SIGNER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constsignerKeyPair=newKeyPair(newPrivateKey(SIGNER_PRIVATE_KEY));constsignerAddress=facade.network.publicKeyToAddress(signerKeyPair.publicKey);console.log(`Signer address: ${signerAddress}`);constauthAddress=newAddress('TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA');console.log(`Authorized address: ${authAddress}`);// 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);});constresponse=awaitfetch(`${NODE_URL}/transactionStatus/${transactionHash}`);if(!response.ok){if(404===response.status){console.log(' Transaction status: unknown');continue;}thrownewError(`HTTP ${response.status}`);}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}`);}thrownewError(`${label} not confirmed after 60 seconds`);}// Returns the list of restrictions currently applied to the accountasyncfunctiongetAccountRestrictions(address){constrestrictionsPath=`/restrictions/account/${address}`;console.log(`Getting restrictions from ${restrictionsPath}`);constresponse=awaitfetch(`${NODE_URL}${restrictionsPath}`);if(!response.ok){console.log(' Response: No restrictions found');return[];}constjson=awaitresponse.json();constrestrictions=json.accountRestrictions.restrictions;console.log(' Response:',restrictions);returnrestrictions;}// Returns a transaction that restricts an accountfunctionrestrictionEnableTransaction(feeMultiplier){consttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.AccountAddressRestrictionTransactionV1Descriptor(// Allow only OUTGOING transactions to the authorized ADDRESSmodels.AccountRestrictionFlags.ADDRESS.value|models.AccountRestrictionFlags.OUTGOING.value,// This is the only authorized outgoing address[authAddress],undefined),signerKeyPair.publicKey,feeMultiplier,2*60*60);console.log('Enabling the restriction with transaction:');console.dir(transaction.toJson(),{colors:true,depth:null});returntransaction;}// Returns a transaction that removes a restriction from an accountfunctionrestrictionDisableTransaction(feeMultiplier,restriction){consttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.AccountAddressRestrictionTransactionV1Descriptor(// Lift restrictions for OUTGOING ADDRESSESmodels.AccountRestrictionFlags.ADDRESS.value|models.AccountRestrictionFlags.OUTGOING.value,undefined,// Remove all addresses currently restrictedrestriction.values.map(hex=>Address.fromDecodedAddressHexString(hex))),signerKeyPair.publicKey,feeMultiplier,2*60*60);console.log('Disabling the restriction with transaction:');console.dir(transaction.toJson(),{colors:true,depth:null});returntransaction;}try{// 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 restriction and decide which// operation to performconstrestrictions=awaitgetAccountRestrictions(signerAddress);lettransaction;if(0===restrictions.length){// Enable the restrictionconsole.log('\n--- Enabling restriction ---');transaction=restrictionEnableTransaction(feeMultiplier);}else{// Disable the restrictionconsole.log('\n--- Disabling restriction ---');transaction=restrictionDisableTransaction(feeMultiplier,restrictions[0]);}// Sign, announce and wait for confirmationletpayload=facade.transactionFactory.static.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));lethash=facade.hashTransaction(transaction).toString();awaitannounceTransaction(payload,'restriction transaction');awaitwaitForConfirmation(hash,'restriction transaction');// Try a dummy transfer to a random address with no mosaicstransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV1Descriptor(newAddress('TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y'),undefined,undefined),signerKeyPair.publicKey,feeMultiplier,2*60*60);payload=facade.transactionFactory.static.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));hash=facade.hashTransaction(transaction).toString();console.log('\n--- Attempting transfer to unauthorized address ---');awaitannounceTransaction(payload,'test transfer');awaitwaitForConfirmation(hash,'test transfer');}catch(e){console.error(e.message);}
//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;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.KeyPair;importorg.symbol.sdk.symbol.SymbolTransactionFactory;importorg.symbol.sdk.symbol.descriptors.*;importorg.symbol.sdk.symbol.models.*;publicfinalclassAccountRestrictions{privatestaticfinalObjectMapperJSON_MAPPER=newObjectMapper();privatestaticfinalHttpClientHTTP_CLIENT=HttpClient.newHttpClient();privatefinalStringnodeUrl=System.getenv().getOrDefault("NODE_URL","https://reference.symboltest.net:3001");privatefinalSymbolFacadefacade=newSymbolFacade("testnet");privateKeyPairsignerKeyPair;privateAddresssignerAddress;privatefinalAddressauthAddress=newAddress("TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA");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);finalStringstatusPath="/transactionStatus/"+transactionHash;finalHttpRequeststatusRequest=HttpRequest.newBuilder(URI.create(nodeUrl+statusPath)).GET().build();finalHttpResponse<String>statusResponse=HTTP_CLIENT.send(statusRequest,BodyHandlers.ofString());if(404==statusResponse.statusCode()){System.out.println(" Transaction status: unknown");continue;}if(2!=statusResponse.statusCode()/100)thrownewIOException("HTTP "+statusResponse.statusCode());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()));}thrownewIOException(String.format("%s not confirmed after 60 seconds",label));}// Returns the restrictions currently applied to the accountprivateJsonNodegetAccountRestrictions(finalAddressaddress)throwsIOException,InterruptedException{finalStringrestrictionsPath=String.format("/restrictions/account/%s",address);System.out.printf("Getting restrictions from %s%n",restrictionsPath);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+restrictionsPath)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100){System.out.println(" Response: No restrictions found");returnJSON_MAPPER.createArrayNode();}finalJsonNoderestrictions=JSON_MAPPER.readTree(response.body()).get("accountRestrictions").get("restrictions");System.out.printf(" Response: %s%n",restrictions);returnrestrictions;}// Returns a transaction that restricts an accountprivateTransactionrestrictionEnableTransaction(finallongfeeMultiplier)throwsIOException{finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newAccountAddressRestrictionTransactionV1Descriptor(newAccountRestrictionFlags(AccountRestrictionFlags.ADDRESS.value|AccountRestrictionFlags.OUTGOING.value),List.of(authAddress),null),signerKeyPair.getPublicKey(),feeMultiplier,2*60*60);System.out.println("Enabling the restriction with transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));returntransaction;}// Returns a transaction that removes a restriction from an accountprivateTransactionrestrictionDisableTransaction(finallongfeeMultiplier,finalJsonNoderestriction)throwsIOException{finalList<Address>restrictionDeletions=newArrayList<>();for(finalJsonNodevalue:restriction.get("values"))restrictionDeletions.add(Address.fromDecodedAddressHexString(value.asText()));finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newAccountAddressRestrictionTransactionV1Descriptor(newAccountRestrictionFlags(AccountRestrictionFlags.ADDRESS.value|AccountRestrictionFlags.OUTGOING.value),null,restrictionDeletions),signerKeyPair.getPublicKey(),feeMultiplier,2*60*60);System.out.println("Disabling the restriction with transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));returntransaction;}publicstaticvoidmain(finalString[]args){try{newAccountRestrictions().run();}catch(finalExceptionex){System.out.println(null==ex.getMessage()?ex.toString():ex.getMessage());}}privatevoidrun()throwsIOException,InterruptedException{System.out.printf("Using node %s%n",nodeUrl);finalStringprivateKeyString=System.getenv().getOrDefault("SIGNER_PRIVATE_KEY","0".repeat(64));signerKeyPair=newKeyPair(newCryptoTypes.PrivateKey(privateKeyString));signerAddress=facade.network.publicKeyToAddress(signerKeyPair.getPublicKey());System.out.printf("Signer address: %s%n",signerAddress);System.out.printf("Authorized address: %s%n",authAddress);// 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 restriction and decide which// operation to performfinalJsonNoderestrictions=getAccountRestrictions(signerAddress);Transactiontransaction;if(restrictions.isEmpty()){System.out.println("\n--- Enabling restriction ---");transaction=restrictionEnableTransaction(feeMultiplier);}else{System.out.println("\n--- Disabling restriction ---");transaction=restrictionDisableTransaction(feeMultiplier,restrictions.get(0));}// Sign, announce and wait for confirmationStringpayload=SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));Stringhash=facade.hashTransaction(transaction).toString();announceTransaction(payload,"restriction transaction");waitForConfirmation(hash,"restriction transaction");// Try a dummy transfer to a random address with no mosaicstransaction=facade.createTransactionFromTypedDescriptor(newTransferTransactionV1Descriptor(newAddress("TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y"),null,null),signerKeyPair.getPublicKey(),feeMultiplier,2*60*60);payload=SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));hash=facade.hashTransaction(transaction).toString();System.out.println("\n--- Attempting transfer to unauthorized address ---");announceTransaction(payload,"test transfer");waitForConfirmation(hash,"test transfer");}}
The code begins by defining two helper functions.
For details on how transactions are announced and how their confirmation is tracked, refer to the
Transfer transaction tutorial.
The remaining helper functions are described in the sections below.
An account can only configure restrictions on itself, so this tutorial requires a single private key.
The private key can be provided through the SIGNER_PRIVATE_KEY environment variable
(as a 64-character hexadecimal string).
If it is not provided, a default value is used.
The account must hold sufficient funds to announce transactions.
If the default key is used, the corresponding account may already be funded.
At this stage, the authorized address is also configured.
The restriction will later limit outgoing transactions to this address only.
The following function retrieves the current account restrictions applied to a given address using the
/restrictions/account/{address}GET endpoint.
If no restrictions are configured, the function returns an empty list.
# Returns the list of restrictions currently applied to the accountdefget_account_restrictions(address):restrictions_path=f'/restrictions/account/{address}'print(f'Getting restrictions from {restrictions_path}')try:url=f'{NODE_URL}{restrictions_path}'withurllib.request.urlopen(url)asrestr_response:status=json.loads(restr_response.read().decode())found_restr=status['accountRestrictions']['restrictions']print(f' Response: {found_restr}')returnfound_restrexcepturllib.error.HTTPError:# The address has never been usedprint(' Response: No restrictions found')return[]
// Returns the list of restrictions currently applied to the accountasyncfunctiongetAccountRestrictions(address){constrestrictionsPath=`/restrictions/account/${address}`;console.log(`Getting restrictions from ${restrictionsPath}`);constresponse=awaitfetch(`${NODE_URL}${restrictionsPath}`);if(!response.ok){console.log(' Response: No restrictions found');return[];}constjson=awaitresponse.json();constrestrictions=json.accountRestrictions.restrictions;console.log(' Response:',restrictions);returnrestrictions;}
privateJsonNodegetAccountRestrictions(finalAddressaddress)throwsIOException,InterruptedException{finalStringrestrictionsPath=String.format("/restrictions/account/%s",address);System.out.printf("Getting restrictions from %s%n",restrictionsPath);finalHttpRequestrequest=HttpRequest.newBuilder(URI.create(nodeUrl+restrictionsPath)).GET().build();finalHttpResponse<String>response=HTTP_CLIENT.send(request,BodyHandlers.ofString());if(2!=response.statusCode()/100){System.out.println(" Response: No restrictions found");returnJSON_MAPPER.createArrayNode();}finalJsonNoderestrictions=JSON_MAPPER.readTree(response.body()).get("accountRestrictions").get("restrictions");System.out.printf(" Response: %s%n",restrictions);returnrestrictions;}
The returned list is then evaluated to determine the tutorial's execution path.
Based on its contents, the appropriate configuration transaction is constructed,
either to enable or to remove the restriction.
# Get current state of the restriction and decide which# operation to performrestrictions=get_account_restrictions(signer_address)iflen(restrictions)==0:# Enable the restrictionprint('\n--- Enabling restriction ---')agg_transaction=restriction_enable_transaction()else:# Disable the restrictionprint('\n--- Disabling restriction ---')agg_transaction=restriction_disable_transaction(restrictions[0])
// Get current state of the restriction and decide which// operation to performconstrestrictions=awaitgetAccountRestrictions(signerAddress);lettransaction;if(0===restrictions.length){// Enable the restrictionconsole.log('\n--- Enabling restriction ---');transaction=restrictionEnableTransaction(feeMultiplier);}else{// Disable the restrictionconsole.log('\n--- Disabling restriction ---');transaction=restrictionDisableTransaction(feeMultiplier,restrictions[0]);}
If multiple restrictions are configured on the account, only the first one returned by the endpoint is removed.
This situation should not occur in this tutorial.
# Returns a transaction that restricts an accountdefrestriction_enable_transaction():enable_transaction=facade.create_transaction_from_descriptor({'type':'account_address_restriction_transaction_v1',# Allow only OUTGOING transactions to the authorized ADDRESS'restriction_flags':AccountRestrictionFlags.ADDRESS|AccountRestrictionFlags.OUTGOING,# This is the only authorized outgoing address'restriction_additions':[auth_address]},signer_key_pair.public_key,fee_multiplier,2*60*60)print('Enabling the restriction with transaction:')print(json.dumps(enable_transaction.to_json(),indent=2))returnenable_transaction
// Returns a transaction that restricts an accountfunctionrestrictionEnableTransaction(feeMultiplier){consttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.AccountAddressRestrictionTransactionV1Descriptor(// Allow only OUTGOING transactions to the authorized ADDRESSmodels.AccountRestrictionFlags.ADDRESS.value|models.AccountRestrictionFlags.OUTGOING.value,// This is the only authorized outgoing address[authAddress],undefined),signerKeyPair.publicKey,feeMultiplier,2*60*60);console.log('Enabling the restriction with transaction:');console.dir(transaction.toJson(),{colors:true,depth:null});returntransaction;}
privateTransactionrestrictionEnableTransaction(finallongfeeMultiplier)throwsIOException{finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newAccountAddressRestrictionTransactionV1Descriptor(newAccountRestrictionFlags(AccountRestrictionFlags.ADDRESS.value|AccountRestrictionFlags.OUTGOING.value),List.of(authAddress),null),signerKeyPair.getPublicKey(),feeMultiplier,2*60*60);System.out.println("Enabling the restriction with transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));returntransaction;}
ADDRESS specifies that the restriction applies to addresses.
Other possible scopes are MOSAIC_ID and TRANSACTION_TYPE.
OUTGOING specifies that only outgoing transactions are affected.
Incoming transaction restrictions can be configured independently by omitting this flag.
By default, the listed values form an allowlist.
Only the specified addresses are allowed.
To configure the restriction in blocklist mode, where the listed addresses are forbidden,
include the BLOCK flag.
: list of addresses (or mosaic IDs, or transaction types) to be
added to the restriction.
In this case, the list contains only the authorized address.
# Returns a transaction that removes a restriction from an accountdefrestriction_disable_transaction(restriction):disable_transaction=facade.create_transaction_from_descriptor({'type':'account_address_restriction_transaction_v1',# Lift restrictions for OUTGOING ADDRESSES'restriction_flags':AccountRestrictionFlags.ADDRESS|AccountRestrictionFlags.OUTGOING,# Remove all addresses currently restricted'restriction_deletions':[Address.from_decoded_address_hex_string(addr)foraddrinrestriction['values']]},signer_key_pair.public_key,fee_multiplier,2*60*60)print('Disabling the restriction with transaction:')print(json.dumps(disable_transaction.to_json(),indent=2))returndisable_transaction
// Returns a transaction that removes a restriction from an accountfunctionrestrictionDisableTransaction(feeMultiplier,restriction){consttransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.AccountAddressRestrictionTransactionV1Descriptor(// Lift restrictions for OUTGOING ADDRESSESmodels.AccountRestrictionFlags.ADDRESS.value|models.AccountRestrictionFlags.OUTGOING.value,undefined,// Remove all addresses currently restrictedrestriction.values.map(hex=>Address.fromDecodedAddressHexString(hex))),signerKeyPair.publicKey,feeMultiplier,2*60*60);console.log('Disabling the restriction with transaction:');console.dir(transaction.toJson(),{colors:true,depth:null});returntransaction;}
privateTransactionrestrictionDisableTransaction(finallongfeeMultiplier,finalJsonNoderestriction)throwsIOException{finalList<Address>restrictionDeletions=newArrayList<>();for(finalJsonNodevalue:restriction.get("values"))restrictionDeletions.add(Address.fromDecodedAddressHexString(value.asText()));finalTransactiontransaction=facade.createTransactionFromTypedDescriptor(newAccountAddressRestrictionTransactionV1Descriptor(newAccountRestrictionFlags(AccountRestrictionFlags.ADDRESS.value|AccountRestrictionFlags.OUTGOING.value),null,restrictionDeletions),signerKeyPair.getPublicKey(),feeMultiplier,2*60*60);System.out.println("Disabling the restriction with transaction:");System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(transaction.toJson()));returntransaction;}
receives the transaction's descriptor, signer public key,
fee multiplier, and deadline duration.
The same values used when enabling the restriction are provided again.
The addresses currently configured in the restriction are supplied in the
field so they can be removed from the configuration.
The method converts the hexadecimal string format returned by the REST API
into the address representation expected when constructing a transaction.
# Sign, announce and wait for confirmationjson_payload=facade.transaction_factory.attach_signature(agg_transaction,facade.sign_transaction(signer_key_pair,agg_transaction))transaction_hash=facade.hash_transaction(agg_transaction)announce_transaction(json_payload,'restriction transaction')wait_for_confirmation(transaction_hash,'restriction transaction')
// Sign, announce and wait for confirmationletpayload=facade.transactionFactory.static.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));lethash=facade.hashTransaction(transaction).toString();awaitannounceTransaction(payload,'restriction transaction');awaitwaitForConfirmation(hash,'restriction transaction');
// Sign, announce and wait for confirmationStringpayload=SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));Stringhash=facade.hashTransaction(transaction).toString();announceTransaction(payload,"restriction transaction");waitForConfirmation(hash,"restriction transaction");
# Try a dummy transfer to a random address with no mosaicstransaction=facade.create_transaction_from_descriptor({'type':'transfer_transaction_v1','recipient_address':Address('TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y')},signer_key_pair.public_key,fee_multiplier,2*60*60)json_payload=facade.transaction_factory.attach_signature(transaction,facade.sign_transaction(signer_key_pair,transaction))transaction_hash=facade.hash_transaction(transaction)print('\n--- Attempting transfer to unauthorized address ---')announce_transaction(json_payload,'test transfer')wait_for_confirmation(transaction_hash,'test transfer')
// Try a dummy transfer to a random address with no mosaicstransaction=facade.createTransactionFromTypedDescriptor(newdescriptors.TransferTransactionV1Descriptor(newAddress('TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y'),undefined,undefined),signerKeyPair.publicKey,feeMultiplier,2*60*60);payload=facade.transactionFactory.static.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));hash=facade.hashTransaction(transaction).toString();console.log('\n--- Attempting transfer to unauthorized address ---');awaitannounceTransaction(payload,'test transfer');awaitwaitForConfirmation(hash,'test transfer');
// Try a dummy transfer to a random address with no mosaicstransaction=facade.createTransactionFromTypedDescriptor(newTransferTransactionV1Descriptor(newAddress("TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y"),null,null),signerKeyPair.getPublicKey(),feeMultiplier,2*60*60);payload=SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));hash=facade.hashTransaction(transaction).toString();System.out.println("\n--- Attempting transfer to unauthorized address ---");announceTransaction(payload,"test transfer");waitForConfirmation(hash,"test transfer");
If the restriction has been enabled, the transfer fails with an Address_Interaction_Prohibited error.
If the restriction has been removed, the transfer is confirmed successfully.
The restriction configuration transaction and the test transfer are announced and confirmed independently.
Each requires its own confirmation, which may increase the total execution time.
The process could be optimized by embedding both transactions in a single aggregate transaction
and announcing them together.
Using node https://reference.symboltest.net:3001
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Authorized address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Fetching recommended fees from /network/fees/transaction
Fee multiplier: 100
Getting restrictions from /restrictions/account/TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Response: No restrictions found
--- Enabling restriction ---
Enabling the restriction with transaction:
{
"signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 16720,
"fee": "16000",
"deadline": "104105847293",
"restriction_flags": 16385,
"restriction_additions": [
"987D075454716222F609929E883174AD8C996D5828C938BC"
],
"restriction_deletions": []
}
Announcing restriction transaction to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for restriction transaction confirmation...
Transaction status: unconfirmed
Transaction status: unconfirmed
...
Transaction status: confirmed
restriction transaction confirmed in 6 seconds
--- Attempting transfer to unauthorized address ---
Announcing test transfer to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for test transfer confirmation...
Transaction status: failed
test transfer failed: Failure_RestrictionAccount_Address_Interaction_Prohibited
Key points in the output:
Lines 2-3: Addresses of the involved accounts.
Line 7 (Response: No restrictions found): No restrictions are currently configured.
Line 19 ("restriction_flags": 16385): 0x4001 corresponds to the combination of ADDRESS and OUTGOING.
Line 20-22 ("restriction_additions"): List of allowed addresses, in decoded hexadecimal format.
The value corresponds to the address shown in line 3.
Line 39 (test transfer failed): The unauthorized recipient address results in an
Address_Interaction_Prohibited error, as expected.