The owner of a mosaic can restrict which accounts are allowed to transact with it.
The conditions are called mosaic restrictions and are defined in two parts:
An account can transact with the mosaic only if its assigned values satisfy all the mosaic's global conditions.
This tutorial requires a preexisting mosaic created with the restrictable
flag.
If the mosaic does not yet define any global restriction, the tutorial creates one with the configuration:
Key
Value
Relation
security_level
1
greater-or-equal
This configuration means that the mosaic can only be used by accounts whose security_level restriction value is
greater than or equal to 1.
The tutorial then assigns this key to a test account, or toggles its value between 1 and 0 if it already exists,
and attempts to transfer the mosaic from its owner account to the test account.
As a result, every other run of the program fails with a restriction violation error.
Because configuring restrictions requires several transactions, the tutorial bundles them into a single
complete aggregate transaction.
This avoids waiting for each transaction to be confirmed individually.
Difference with Account Restrictions
Symbol also supports account restrictions, which are defined at the account level rather than
at the mosaic 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.requestimporthashlibfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.scimportAmountfromsymbolchain.symbol.NetworkimportNetworkTimestampNODE_URL=os.environ.get('NODE_URL','https://reference.symboltest.net:3001')print(f'Using node {NODE_URL}')# 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)asresponse:print(f' Response: {response.read().decode()}')# Helper function to wait for transaction confirmationdefwait_for_confirmation(transaction_hash,label):print(f'Waiting for {label} confirmation...')forattemptinrange(60):time.sleep(1)try:url=f'{NODE_URL}/transactionStatus/{transaction_hash}'withurllib.request.urlopen(url)asresponse:status=json.loads(response.read().decode())print(f' Transaction status: {status["group"]}')ifstatus['group']=='confirmed':print(f'{label} confirmed in {attempt} seconds')returnifstatus['group']=='failed':raiseException(f'{label} failed: {status["code"]}')excepturllib.error.HTTPError:print(' Transaction status: unknown')raiseException(f'{label} not confirmed after 60 seconds')# Returns a filtered list of restrictions currently applied to the mosaic# matching the given restriction keydefget_mosaic_restrictions(query,key):restrictions_path=f'/restrictions/mosaic?{query}'print(f' Getting restrictions from {restrictions_path}')res=[]try:url=f'{NODE_URL}{restrictions_path}'withurllib.request.urlopen(url)asresponse:status=json.loads(response.read().decode())data=status['data']iflen(data)>0:# Look at the first returned restrictionrlist=data[0]['mosaicRestrictionEntry']['restrictions']# Filter by keyres=[rforrinrlistifint(r['key'])==key]excepturllib.error.HTTPError:# The mosaic has no restrictions applied to this keypassprint(f' Response: {res}')returnresdefget_mosaic_global_restrictions(mosaic_id,key):returnget_mosaic_restrictions(f'mosaicId={mosaic_id:X}&entryType=1',key)defget_mosaic_address_restrictions(mosaic_id,address,key):returnget_mosaic_restrictions(f'mosaicId={mosaic_id:X}&entryType=0&targetAddress={address}',key)# Returns a transaction enabling a mosaic's global restrictiondefglobal_restriction_enable_transaction():transaction=facade.transaction_factory.create_embedded({'type':'mosaic_global_restriction_transaction_v1','signer_public_key':owner_key_pair.public_key,'mosaic_id':mosaic_id,'reference_mosaic_id':0,'restriction_key':restriction_key,'previous_restriction_type':0,'previous_restriction_value':0,'new_restriction_type':'ge','new_restriction_value':1})print(json.dumps(transaction.to_json(),indent=2))returntransaction# Returns a transaction setting an address restriction's valuedefaddress_restriction_set_value(prev_value,new_value,address):transaction=facade.transaction_factory.create_embedded({'type':'mosaic_address_restriction_transaction_v1','signer_public_key':owner_key_pair.public_key,'mosaic_id':mosaic_id,'restriction_key':restriction_key,'previous_restriction_value':prev_value,'new_restriction_value':new_value,'target_address':address})print(json.dumps(transaction.to_json(),indent=2))returntransactionfacade=SymbolFacade('testnet')OWNER_PRIVATE_KEY=os.getenv('OWNER_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000000')owner_key_pair=SymbolFacade.KeyPair(PrivateKey(OWNER_PRIVATE_KEY))owner_address=facade.network.public_key_to_address(owner_key_pair.public_key)print(f'Owner address: {owner_address}')target_address=os.getenv('TARGET_ADDRESS','TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA')print(f'Target address: {target_address}')mosaic_id=int(os.getenv('MOSAIC_ID','6A383620F5C7A5B2'),16)print(f'Mosaic ID: 0x{mosaic_id:08X}')restriction_name=os.getenv('RESTRICTION_NAME','security_level')hasher=hashlib.sha3_256()hasher.update(restriction_name.encode('utf8'))restriction_key=int.from_bytes(hasher.digest()[:4])print(f'Restriction name: "{restriction_name}"'f' (key: 0x{restriction_key:08X})')try:# 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_mult=response_json['medianFeeMultiplier']minimum_mult=response_json['minFeeMultiplier']fee_mult=max(median_mult,minimum_mult)print(f' Fee multiplier: {fee_mult}')# Enable global restriction if requiredtransactions=[]print("Checking if the global restriction is enabled:")global_restrictions=get_mosaic_global_restrictions(mosaic_id,restriction_key)iflen(global_restrictions)==0:# Enable the global restrictionprint('+ Enabling global restriction')transactions.append(global_restriction_enable_transaction())# Enable the address restrictionprint('+ Authorizing owner account')transactions.append(address_restriction_set_value(0xFFFFFFFF_FFFFFFFF,1,owner_address))# Toggle target address restrictionprint("Checking if target account is authorized:")address_restrictions=get_mosaic_address_restrictions(mosaic_id,target_address,restriction_key)prev_value=0xFFFFFFFF_FFFFFFFFiflen(address_restrictions)>0:prev_value=int(address_restrictions[0]['value'])ifprev_value!=1:# Enable the address restrictionprint('+ Authorizing target account')transactions.append(address_restriction_set_value(prev_value,1,target_address))else:# Disable the address restrictionprint('+ Deauthorizing target account')transactions.append(address_restriction_set_value(prev_value,0,target_address))# Build an aggregate transactionprint('Bundling',len(transactions),'transaction(s) in an aggregate')transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3','signer_public_key':owner_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(transactions),'transactions':transactions})transaction.fee=Amount(fee_mult*transaction.size)# Sign, announce and wait for confirmationpayload=facade.transaction_factory.attach_signature(transaction,facade.sign_transaction(owner_key_pair,transaction))transaction_hash=facade.hash_transaction(transaction)announce_transaction(payload,'aggregate')wait_for_confirmation(transaction_hash,'aggregate')# Try to transfer the mosaic to the target addresstransaction=facade.transaction_factory.create({'type':'transfer_transaction_v1','signer_public_key':owner_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'recipient_address':target_address,'mosaics':[{'mosaic_id':mosaic_id,'amount':1}]})transaction.fee=Amount(fee_mult*transaction.size)payload=facade.transaction_factory.attach_signature(transaction,facade.sign_transaction(owner_key_pair,transaction))transaction_hash=facade.hash_transaction(transaction)print('\nAttempting transfer to the target account')announce_transaction(payload,'test transfer')wait_for_confirmation(transaction_hash,'test transfer')exceptExceptionase:print(e)
import{PrivateKey}from'symbol-sdk';import{KeyPair,SymbolTransactionFactory,models,NetworkTimestamp,SymbolFacade}from'symbol-sdk/symbol';importcryptofrom'crypto';constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';console.log('Using node',NODE_URL);// 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;attempt<60;attempt++){awaitnewPromise(resolve=>setTimeout(resolve,1000));try{constresponse=awaitfetch(`${NODE_URL}/transactionStatus/${transactionHash}`);conststatus=awaitresponse.json();console.log(' Transaction status:',status.group);if(status.group==='confirmed'){console.log(`${label} confirmed in`,attempt,'seconds');return;}if(status.group==='failed'){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 a filtered list of restrictions currently applied to the mosaic// matching the given restriction keyasyncfunctiongetMosaicRestrictions(query,key){constrestrictionsPath=`/restrictions/mosaic?${query}`;console.log(` Getting restrictions from ${restrictionsPath}`);letres=[];try{constresponse=awaitfetch(`${NODE_URL}${restrictionsPath}`);conststatus=awaitresponse.json();constdata=status.data;if(data.length>0){// Look at the first returned restrictionconstrlist=data[0].mosaicRestrictionEntry.restrictions;// Filter by keyres=rlist.filter(r=>BigInt(r.key)===key);}}catch{// The mosaic has no restrictions applied to this key}console.log(' Response:',res);returnres;}functiongetMosaicGlobalRestrictions(mosaicId,key){returngetMosaicRestrictions(`mosaicId=${mosaicId.toString(16)}&entryType=1`,key);}functiongetMosaicAddressRestrictions(mosaicId,address,key){returngetMosaicRestrictions(`mosaicId=${mosaicId.toString(16)}&entryType=0`+`&targetAddress=${address}`,key);}// Returns a transaction enabling a mosaic's global restrictionfunctionglobalRestrictionEnableTransaction(){consttransaction=facade.transactionFactory.createEmbedded({type:'mosaic_global_restriction_transaction_v1',signerPublicKey:ownerKeyPair.publicKey,mosaicId,referenceMosaicId:0n,restrictionKey,previousRestrictionType:0,previousRestrictionValue:0n,newRestrictionType:'ge',newRestrictionValue:1n});console.dir(transaction.toJson(),{colors:true,depth:null});returntransaction;}// Returns a transaction setting an address restriction's valuefunctionaddressRestrictionSetValue(prevValue,newValue,address){consttransaction=facade.transactionFactory.createEmbedded({type:'mosaic_address_restriction_transaction_v1',signerPublicKey:ownerKeyPair.publicKey,mosaicId,restrictionKey,previousRestrictionValue:prevValue,newRestrictionValue:newValue,targetAddress:address});console.dir(transaction.toJson(),{colors:true,depth:null});returntransaction;}constfacade=newSymbolFacade('testnet');constOWNER_PRIVATE_KEY=process.env.OWNER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constownerKeyPair=newKeyPair(newPrivateKey(OWNER_PRIVATE_KEY));constownerAddress=facade.network.publicKeyToAddress(ownerKeyPair.publicKey);console.log(`Owner address: ${ownerAddress}`);consttargetAddress=process.env.TARGET_ADDRESS||'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA';console.log(`Target address: ${targetAddress}`);constmosaicId=BigInt('0x'+(process.env.MOSAIC_ID||'6A383620F5C7A5B2'));console.log(`Mosaic ID: 0x${mosaicId.toString(16).toUpperCase()}`);constrestrictionName=process.env.RESTRICTION_NAME||'security_level';constdigest=crypto.createHash('sha3-256').update(Buffer.from(restrictionName,'utf8')).digest();constrestrictionKey=BigInt(digest.readUInt32BE(0));console.log(`Restriction name: "${restrictionName}" (key: 0x${restrictionKey.toString(16).toUpperCase().padStart(8,'0')})`);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();constmedianMult=feeJSON.medianFeeMultiplier;constminimumMult=feeJSON.minFeeMultiplier;constfeeMult=Math.max(medianMult,minimumMult);console.log(' Fee multiplier:',feeMult);// Enable global restriction if requiredconsttransactions=[];console.log('Checking if the global restriction is enabled:');constglobalRestrictions=awaitgetMosaicGlobalRestrictions(mosaicId,restrictionKey);if(globalRestrictions.length===0){// Enable the global restrictionconsole.log('+ Enabling global restriction');transactions.push(globalRestrictionEnableTransaction());// Enable the address restrictionconsole.log('+ Authorizing owner account');transactions.push(addressRestrictionSetValue(0xFFFFFFFFFFFFFFFFn,1n,ownerAddress.toString()));}// Toggle target address restrictionconsole.log('Checking if target account is authorized:');constaddressRestrictions=awaitgetMosaicAddressRestrictions(mosaicId,targetAddress,restrictionKey);letprevValue=0xFFFFFFFFFFFFFFFFn;if(addressRestrictions.length>0)prevValue=BigInt(addressRestrictions[0].value);if(prevValue!==1n){// Enable the address restrictionconsole.log('+ Authorizing target account');transactions.push(addressRestrictionSetValue(prevValue,1n,targetAddress));}else{// Disable the address restrictionconsole.log('+ Deauthorizing target account');transactions.push(addressRestrictionSetValue(prevValue,0n,targetAddress));}// Build an aggregate transactionconsole.log('Bundling',transactions.length,'transaction(s) in an aggregate');constaggregate=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',signerPublicKey:ownerKeyPair.publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(transactions),transactions});aggregate.fee=newmodels.Amount(feeMult*aggregate.size);// Sign, announce and wait for confirmationletpayload=SymbolTransactionFactory.attachSignature(aggregate,facade.signTransaction(ownerKeyPair,aggregate));lethash=facade.hashTransaction(aggregate).toString();awaitannounceTransaction(payload,'aggregate');awaitwaitForConfirmation(hash,'aggregate');// Try to transfer the mosaic to the target addressconsttransfer=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:ownerKeyPair.publicKey,deadline:timestamp.addHours(2).timestamp,recipientAddress:targetAddress,mosaics:[{mosaicId,amount:1n}]});transfer.fee=newmodels.Amount(feeMult*transfer.size);payload=SymbolTransactionFactory.attachSignature(transfer,facade.signTransaction(ownerKeyPair,transfer));hash=facade.hashTransaction(transfer).toString();console.log('\nAttempting transfer to the target account');awaitannounceTransaction(payload,'test transfer');awaitwaitForConfirmation(hash,'test transfer');}catch(e){console.error(e.message);}
The code begins by defining several 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.
the owner account, which controls the mosaic and is responsible for configuring its restrictions.
Its private key can be provided through the OWNER_PRIVATE_KEY environment variable as a 64-character hexadecimal
string.
the target account, which will later receive authorization to transact with the mosaic.
Its address can be provided through the TARGET_ADDRESS environment variable as a Symbol testnet address.
the mosaic identifier, read from MOSAIC_ID as 16 hexadecimal characters.
the restriction name, read from RESTRICTION_NAME as a string.
the corresponding restriction key, derived from the restriction name by hashing it with SHA3-256 and taking the
first four bytes of the hash.
This approach allows applications to use human-readable names while producing deterministic keys.
Any 32-bit number can also be used directly as a restriction key.
If any of these values is not provided through an environment variable, a default value is used.
The owner account must hold sufficient funds to announce transactions.
If the default one is used, it may already be funded.
# 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_mult=response_json['medianFeeMultiplier']minimum_mult=response_json['minFeeMultiplier']fee_mult=max(median_mult,minimum_mult)print(f' Fee multiplier: {fee_mult}')
# Enable global restriction if requiredtransactions=[]print("Checking if the global restriction is enabled:")global_restrictions=get_mosaic_global_restrictions(mosaic_id,restriction_key)iflen(global_restrictions)==0:# Enable the global restrictionprint('+ Enabling global restriction')transactions.append(global_restriction_enable_transaction())# Enable the address restrictionprint('+ Authorizing owner account')transactions.append(address_restriction_set_value(0xFFFFFFFF_FFFFFFFF,1,owner_address))
// Enable global restriction if requiredconsttransactions=[];console.log('Checking if the global restriction is enabled:');constglobalRestrictions=awaitgetMosaicGlobalRestrictions(mosaicId,restrictionKey);if(globalRestrictions.length===0){// Enable the global restrictionconsole.log('+ Enabling global restriction');transactions.push(globalRestrictionEnableTransaction());// Enable the address restrictionconsole.log('+ Authorizing owner account');transactions.push(addressRestrictionSetValue(0xFFFFFFFFFFFFFFFFn,1n,ownerAddress.toString()));}
The code first checks whether the mosaic already defines a global restriction for the configured key:
defget_mosaic_restrictions(query,key):restrictions_path=f'/restrictions/mosaic?{query}'print(f' Getting restrictions from {restrictions_path}')res=[]try:url=f'{NODE_URL}{restrictions_path}'withurllib.request.urlopen(url)asresponse:status=json.loads(response.read().decode())data=status['data']iflen(data)>0:# Look at the first returned restrictionrlist=data[0]['mosaicRestrictionEntry']['restrictions']# Filter by keyres=[rforrinrlistifint(r['key'])==key]excepturllib.error.HTTPError:# The mosaic has no restrictions applied to this keypassprint(f' Response: {res}')returnresdefget_mosaic_global_restrictions(mosaic_id,key):returnget_mosaic_restrictions(f'mosaicId={mosaic_id:X}&entryType=1',key)
asyncfunctiongetMosaicRestrictions(query,key){constrestrictionsPath=`/restrictions/mosaic?${query}`;console.log(` Getting restrictions from ${restrictionsPath}`);letres=[];try{constresponse=awaitfetch(`${NODE_URL}${restrictionsPath}`);conststatus=awaitresponse.json();constdata=status.data;if(data.length>0){// Look at the first returned restrictionconstrlist=data[0].mosaicRestrictionEntry.restrictions;// Filter by keyres=rlist.filter(r=>BigInt(r.key)===key);}}catch{// The mosaic has no restrictions applied to this key}console.log(' Response:',res);returnres;}functiongetMosaicGlobalRestrictions(mosaicId,key){returngetMosaicRestrictions(`mosaicId=${mosaicId.toString(16)}&entryType=1`,key);}
This is done by querying /restrictions/mosaicGET and filtering by mosaicId and entryType=1, which selects
global restrictions.
The returned entries are then filtered to keep only those involving the selected restriction_key.
If no restriction is found, one is created by adding two transactions to the list of transactions to announce:
a mosaic global restriction transaction defining the restriction condition.
See the MosaicGlobalRestrictionTransactionV1 serialization table for details about each of its fields.
The restriction created in this tutorial requires the value associated with the key
security_level to be greater than or equal to 1.
a mosaic address restriction transaction authorizing the owner account.
See the MosaicAddressRestrictionTransactionV1 serialization table for details about each of its fields.
The code assigns the value 1 to the owner's security_level so the owner account can continue transacting with
its own mosaic.
Note
For simplicity, the tutorial assumes that if no global restriction exists, the owner account
also has no address restriction.
For this reason 0xFFFFFFFF_FFFFFFFF is used as the previous value, indicating that no value
was previously set.
A more robust implementation should first query the owner's restriction state and use the
appropriate previous value, as demonstrated below for the target account.
defget_mosaic_restrictions(query,key):restrictions_path=f'/restrictions/mosaic?{query}'print(f' Getting restrictions from {restrictions_path}')res=[]try:url=f'{NODE_URL}{restrictions_path}'withurllib.request.urlopen(url)asresponse:status=json.loads(response.read().decode())data=status['data']iflen(data)>0:# Look at the first returned restrictionrlist=data[0]['mosaicRestrictionEntry']['restrictions']# Filter by keyres=[rforrinrlistifint(r['key'])==key]excepturllib.error.HTTPError:# The mosaic has no restrictions applied to this keypassprint(f' Response: {res}')returnresdefget_mosaic_global_restrictions(mosaic_id,key):returnget_mosaic_restrictions(f'mosaicId={mosaic_id:X}&entryType=1',key)defget_mosaic_address_restrictions(mosaic_id,address,key):returnget_mosaic_restrictions(f'mosaicId={mosaic_id:X}&entryType=0&targetAddress={address}',key)
asyncfunctiongetMosaicRestrictions(query,key){constrestrictionsPath=`/restrictions/mosaic?${query}`;console.log(` Getting restrictions from ${restrictionsPath}`);letres=[];try{constresponse=awaitfetch(`${NODE_URL}${restrictionsPath}`);conststatus=awaitresponse.json();constdata=status.data;if(data.length>0){// Look at the first returned restrictionconstrlist=data[0].mosaicRestrictionEntry.restrictions;// Filter by keyres=rlist.filter(r=>BigInt(r.key)===key);}}catch{// The mosaic has no restrictions applied to this key}console.log(' Response:',res);returnres;}functiongetMosaicGlobalRestrictions(mosaicId,key){returngetMosaicRestrictions(`mosaicId=${mosaicId.toString(16)}&entryType=1`,key);}functiongetMosaicAddressRestrictions(mosaicId,address,key){returngetMosaicRestrictions(`mosaicId=${mosaicId.toString(16)}&entryType=0`+`&targetAddress=${address}`,key);}
As in the global restriction case, the current value is obtained by querying /restrictions/mosaicGET and filtering
by mosaicId, targetAddress, and entryType=0, which selects address restrictions.
The returned entries are then filtered to keep only those involving the selected restriction_key.
Depending on the current value of the restriction for the target account,
a transaction is created that authorizes or deauthorizes the account.
This transaction is added to the list of transactions to announce.
print("Checking if target account is authorized:")address_restrictions=get_mosaic_address_restrictions(mosaic_id,target_address,restriction_key)prev_value=0xFFFFFFFF_FFFFFFFFiflen(address_restrictions)>0:prev_value=int(address_restrictions[0]['value'])ifprev_value!=1:# Enable the address restrictionprint('+ Authorizing target account')transactions.append(address_restriction_set_value(prev_value,1,target_address))else:# Disable the address restrictionprint('+ Deauthorizing target account')transactions.append(address_restriction_set_value(prev_value,0,target_address))
console.log('Checking if target account is authorized:');constaddressRestrictions=awaitgetMosaicAddressRestrictions(mosaicId,targetAddress,restrictionKey);letprevValue=0xFFFFFFFFFFFFFFFFn;if(addressRestrictions.length>0)prevValue=BigInt(addressRestrictions[0].value);if(prevValue!==1n){// Enable the address restrictionconsole.log('+ Authorizing target account');transactions.push(addressRestrictionSetValue(prevValue,1n,targetAddress));}else{// Disable the address restrictionconsole.log('+ Deauthorizing target account');transactions.push(addressRestrictionSetValue(prevValue,0n,targetAddress));}
If the account does not yet have a restriction value, or the value is not 1, the code assigns the value 1,
authorizing it to use the mosaic.
If the account already has the value 1, the code replaces it with 0, revoking the authorization.
Running the tutorial repeatedly therefore alternates between authorizing and deauthorizing the
target account.
Only the first restriction in the returned list is examined, because, after filtering by restriction_key,
the list is either empty or contains a single entry.
All configuration transactions created above are bundled into a single complete aggregate transaction,
so the user does not need to wait for them to be confirmed individually.
print('Bundling',len(transactions),'transaction(s) in an aggregate')transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3','signer_public_key':owner_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(transactions),'transactions':transactions})transaction.fee=Amount(fee_mult*transaction.size)
console.log('Bundling',transactions.length,'transaction(s) in an aggregate');constaggregate=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',signerPublicKey:ownerKeyPair.publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(transactions),transactions});aggregate.fee=newmodels.Amount(feeMult*aggregate.size);
Only the aggregate transaction pays fees, so embedded transactions do not use the fee field.
transaction=facade.transaction_factory.create({'type':'transfer_transaction_v1','signer_public_key':owner_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'recipient_address':target_address,'mosaics':[{'mosaic_id':mosaic_id,'amount':1}]})transaction.fee=Amount(fee_mult*transaction.size)payload=facade.transaction_factory.attach_signature(transaction,facade.sign_transaction(owner_key_pair,transaction))transaction_hash=facade.hash_transaction(transaction)print('\nAttempting transfer to the target account')announce_transaction(payload,'test transfer')wait_for_confirmation(transaction_hash,'test transfer')
consttransfer=facade.transactionFactory.create({type:'transfer_transaction_v1',signerPublicKey:ownerKeyPair.publicKey,deadline:timestamp.addHours(2).timestamp,recipientAddress:targetAddress,mosaics:[{mosaicId,amount:1n}]});transfer.fee=newmodels.Amount(feeMult*transfer.size);payload=SymbolTransactionFactory.attachSignature(transfer,facade.signTransaction(ownerKeyPair,transfer));hash=facade.hashTransaction(transfer).toString();console.log('\nAttempting transfer to the target account');awaitannounceTransaction(payload,'test transfer');awaitwaitForConfirmation(hash,'test transfer');
If the target account currently satisfies the restriction (security_level ≥ 1),
the transfer is confirmed successfully.
If the restriction value was toggled to 0, the transaction fails with an Account_Unauthorized error.
Running the tutorial multiple times therefore alternates between successful and failing transfers,
demonstrating how mosaic restrictions control which accounts are allowed to transact with the mosaic.
Using node https://reference.symboltest.net:3001
Owner address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Target address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Mosaic ID: 0x147893385B36E34D
Restriction name: "security_level" (key: 0xC1D01F88)
Fetching current network time from /node/time
Network time: 105560304329 ms since nemesis
Fetching recommended fees from /network/fees/transaction
Fee multiplier: 100
Checking if the global restriction is enabled:
Getting restrictions from /restrictions/mosaic?mosaicId=147893385B36E34D&entryType=1
Response: []
+ Enabling global restriction
{
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 16721,
"mosaic_id": "1475090748221612877",
"reference_mosaic_id": "0",
"restriction_key": "3251642248",
"previous_restriction_value": "0",
"new_restriction_value": "1",
"previous_restriction_type": 0,
"new_restriction_type": 6
}
+ Authorizing owner account
{
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 16977,
"mosaic_id": "1475090748221612877",
"restriction_key": "3251642248",
"previous_restriction_value": "18446744073709551615",
"new_restriction_value": "1",
"target_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9"
}
Checking if target account is authorized:
Getting restrictions from /restrictions/mosaic?mosaicId=147893385B36E34D&entryType=0&targetAddress=TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Response: []
+ Authorizing target account
{
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 16977,
"mosaic_id": "1475090748221612877",
"restriction_key": "3251642248",
"previous_restriction_value": "18446744073709551615",
"new_restriction_value": "1",
"target_address": "987D075454716222F609929E883174AD8C996D5828C938BC"
}
Bundling 3 transaction(s) in an aggregate
Announcing aggregate to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for aggregate confirmation...
Transaction status: unconfirmed
Transaction status: unconfirmed
...
Transaction status: confirmed
aggregate confirmed in 14 seconds
Attempting transfer to the target account
Announcing test transfer to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for test transfer confirmation...
Transaction status: unconfirmed
Transaction status: unconfirmed
...
Transaction status: confirmed
test transfer confirmed in 21 seconds
Key points in the output:
Lines 2-3: Addresses of the involved accounts.
Line 4: The mosaic being restricted.
Line 5: The restriction name and its corresponding key.
Line 12 (Response: []): The mosaic currently has no global restrictions.
Line 13: The transaction configuring the mosaic restriction.
It includes the mosaic ID (in decimal), the restriction key (in decimal), the restriction value (1), and
the restriction condition (6, which corresponds to the greater-or-equalMosaicRestrictionType)
Line 27: The transaction authorizing the owner account.
It includes the mosaic ID (in decimal), the restriction key (in decimal), and the necessary restriction
value (1).
Line 41 (Response: []): The target account is currently unauthorized because it has no value associated with
the restriction key.
Line 42: The transaction authorizing the target account.
It includes the mosaic ID (in decimal), the restriction key (in decimal), and the necessary restriction
value (1).
Line 72 (test transfer confirmed): The test transaction succeeded because both accounts satisfy the
restriction and are therefore authorized.
Using node https://reference.symboltest.net:3001
Owner address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Target address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Mosaic ID: 0x147893385B36E34D
Restriction name: "security_level" (key: 0xC1D01F88)
Fetching current network time from /node/time
Network time: 105560429335 ms since nemesis
Fetching recommended fees from /network/fees/transaction
Fee multiplier: 100
Checking if the global restriction is enabled:
Getting restrictions from /restrictions/mosaic?mosaicId=147893385B36E34D&entryType=1
Response: [{'key': '3251642248', 'restriction': {'referenceMosaicId': '0000000000000000', 'restrictionValue': '1', 'restrictionType': 6}}]
Checking if target account is authorized:
Getting restrictions from /restrictions/mosaic?mosaicId=147893385B36E34D&entryType=0&targetAddress=TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Response: [{'key': '3251642248', 'value': '1'}]
+ Deauthorizing target account
{
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 16977,
"mosaic_id": "1475090748221612877",
"restriction_key": "3251642248",
"previous_restriction_value": "1",
"new_restriction_value": "0",
"target_address": "987D075454716222F609929E883174AD8C996D5828C938BC"
}
Bundling 1 transaction(s) in an aggregate
Announcing aggregate to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for aggregate confirmation...
Transaction status: unconfirmed
Transaction status: unconfirmed
...
Transaction status: confirmed
aggregate confirmed in 19 seconds
Attempting transfer to the target account
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_RestrictionMosaic_Account_Unauthorized
Key points in the output:
Lines 2-3: Addresses of the involved accounts.
Line 4: The mosaic being restricted.
Line 5: The restriction name and its corresponding key.
Line 12 (Response: [ ... ]): Existing restrictions are detected.
Line 15 (Response: [ ... ]): The target account has a restriction value of 1, meaning it is authorized.
Line 16: The transaction deauthorizing the target account.
It includes the mosaic ID (in decimal), the restriction key (in decimal), and the necessary restriction
value (0).
Line 43 (test transfer failed): The test transaction failed because the target account no longer satisfies
the restriction, as expected.
The transaction hashes shown in the output can be used to look up the transactions in the
Symbol Testnet Explorer.