importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.NemFacadeimportNemFacadefromsymbolchain.ncimportAmountfromsymbolchain.nem.FeeCalculatorimport(calculate_mosaic_rental_fee,calculate_transaction_fee)fromsymbolchain.nem.NetworkimportNetworkTimestampNODE_URL=os.getenv('NODE_URL','http://libertalia.nemtest.net:7890')print(f'Using node {NODE_URL}')SIGNER_PRIVATE_KEY=os.getenv('SIGNER_PRIVATE_KEY','0000000000000000000000000000000000000000000000000000000000000000')signer_key_pair=NemFacade.KeyPair(PrivateKey(SIGNER_PRIVATE_KEY))facade=NemFacade('testnet')signer_address=facade.network.public_key_to_address(signer_key_pair.public_key)print(f'Signer address: {signer_address}')namespace_name=os.getenv('NAMESPACE','my_namespace')mosaic_name=os.getenv('MOSAIC',f'token_{int(time.time())}')mosaic_id=f'{namespace_name}:{mosaic_name}'print(f'Creating mosaic: {mosaic_id}')try:# Fetch current network timetime_path='/time-sync/network-time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())network_time=response_json['receiveTimeStamp']//1000print(f' Network time: {network_time} s since the nemesis block')# Derived fields from network timetimestamp=NetworkTimestamp(network_time)deadline=timestamp.add_hours(2)# Describe the levyLEVY_RECIPIENT=os.getenv('LEVY_RECIPIENT','TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4')levy={'transfer_fee_type':'absolute','recipient_address':LEVY_RECIPIENT,'mosaic_id':{'namespace_id':{'name':'nem'},'name':'xem'},'fee':1_000_000}levy_mosaic_id=levy['mosaic_id']print('Levy:')print(f' Type: {levy["transfer_fee_type"]}')print(f' Recipient: {levy["recipient_address"]}')print(f' Mosaic: {levy_mosaic_id["namespace_id"]["name"]}:'f'{levy_mosaic_id["name"]}')print(f' Fee: {levy["fee"]}')# Build the mosaic definition transactionrental_fee=calculate_mosaic_rental_fee()print(f' Mosaic creation fee: {rental_fee/1_000_000} XEM')transaction=facade.transaction_factory.create({'type':'mosaic_definition_transaction_v1','signer_public_key':signer_key_pair.public_key,'timestamp':timestamp.timestamp,'deadline':deadline.timestamp,'rental_fee_sink':'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC','rental_fee':rental_fee,'mosaic_definition':{'owner_public_key':signer_key_pair.public_key,'id':{'namespace_id':{'name':namespace_name},'name':mosaic_name},'description':'My tutorial mosaic with a levy','properties':[{'property_':{'name':b'divisibility','value':b'2'}},{'property_':{'name':b'initialSupply','value':b'1000'}},{'property_':{'name':b'supplyMutable','value':b'true'}},{'property_':{'name':b'transferable','value':b'true'}}],'levy':levy}})# Calculate and attach the transaction feefee=calculate_transaction_fee(transaction)transaction.fee=Amount(fee)print(f' Transaction fee: {fee/1_000_000} XEM')# Sign, announce and wait for confirmationsignature=facade.sign_transaction(signer_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)print('Built mosaic definition transaction:')print(json.dumps(transaction.to_json(),indent=2))announce_path='/transaction/announce'print(f'Announcing mosaic definition to {announce_path}')announce_request=urllib.request.Request(f'{NODE_URL}{announce_path}',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(announce_request)asresponse:announce_result=json.loads(response.read().decode())print(f' Result: {announce_result["message"]}')if'SUCCESS'==announce_result['message']:transaction_hash=facade.hash_transaction(transaction)status_path=f'/transaction/get?hash={transaction_hash}'print(f'Waiting for confirmation from {status_path}')is_confirmed=Falseforattemptinrange(120):try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asresponse:confirmed=json.loads(response.read().decode())height=confirmed['meta']['height']print(f'Transaction confirmed in block {height}')is_confirmed=Truebreakexcepturllib.error.HTTPError:print(' Transaction status: pending')time.sleep(1)ifnotis_confirmed:print('Confirmation took too long.')else:print(f'Transaction rejected: {announce_result["message"]}')# Retrieve the levydefinition_path=f'/mosaic/definition?mosaicId={mosaic_id}'print(f'Fetching mosaic information from {definition_path}')withurllib.request.urlopen(f'{NODE_URL}{definition_path}')asresponse:mosaic_info=json.loads(response.read().decode())levy_info=mosaic_info['levy']levy_mosaic_id=levy_info['mosaicId']levy_type='absolute'if1==levy_info['type']else'percentile'print('Levy information:')print(f' Type: {levy_type}')print(f' Recipient: {levy_info["recipient"]}')print(f' Mosaic: 'f'{levy_mosaic_id["namespaceId"]}:{levy_mosaic_id["name"]}')print(f' Fee: {levy_info["fee"]}')excepturllib.error.URLErrorase:print(e.reason)
import{PrivateKey}from'symbol-sdk';import{NemFacade,NetworkTimestamp,calculateMosaicRentalFee,calculateTransactionFee,models}from'symbol-sdk/nem';constNODE_URL=process.env.NODE_URL||'http://libertalia.nemtest.net:7890';console.log('Using node',NODE_URL);constSIGNER_PRIVATE_KEY=process.env.SIGNER_PRIVATE_KEY||'0000000000000000000000000000000000000000000000000000000000000000';constsignerKeyPair=newNemFacade.KeyPair(newPrivateKey(SIGNER_PRIVATE_KEY));constfacade=newNemFacade('testnet');constsignerAddress=facade.network.publicKeyToAddress(signerKeyPair.publicKey);console.log('Signer address:',signerAddress.toString());constnamespaceName=process.env.NAMESPACE||'my_namespace';constmosaicName=process.env.MOSAIC||`token_${Math.floor(Date.now()/1000)}`;constmosaicId=`${namespaceName}:${mosaicName}`;console.log('Creating mosaic:',mosaicId);try{// Fetch current network timeconsttimePath='/time-sync/network-time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();constnetworkTime=Math.floor(timeJSON.receiveTimeStamp/1000);console.log(' Network time:',networkTime,'s since the nemesis block');// Derived fields from network timeconsttimestamp=newNetworkTimestamp(networkTime);constdeadline=timestamp.addHours(2);// Describe the levyconstLEVY_RECIPIENT=process.env.LEVY_RECIPIENT||'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';constlevy={transferFeeType:'absolute',recipientAddress:LEVY_RECIPIENT,mosaicId:{namespaceId:{name:'nem'},name:'xem'},fee:1_000_000};console.log('Levy:');console.log(' Type:',levy.transferFeeType);console.log(' Recipient:',levy.recipientAddress);console.log(' Mosaic:',`${levy.mosaicId.namespaceId.name}:${levy.mosaicId.name}`);console.log(' Fee:',levy.fee);// Build the mosaic definition transactionconstrentalFee=calculateMosaicRentalFee();console.log(' Mosaic creation fee:',`${Number(rentalFee)/1_000_000} XEM`);consttransaction=facade.transactionFactory.create({type:'mosaic_definition_transaction_v1',signerPublicKey:signerKeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,rentalFeeSink:'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC',rentalFee,mosaicDefinition:{ownerPublicKey:signerKeyPair.publicKey.toString(),id:{namespaceId:{name:namespaceName},name:mosaicName},description:'My tutorial mosaic with a levy',properties:[{property:{name:'divisibility',value:'2'}},{property:{name:'initialSupply',value:'1000'}},{property:{name:'supplyMutable',value:'true'}},{property:{name:'transferable',value:'true'}}],levy}});// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction);transaction.fee=newmodels.Amount(fee);console.log(' Transaction fee:',`${Number(fee)/1_000_000} XEM`);// Sign, announce and wait for confirmationconstsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);console.log('Built mosaic definition transaction:');console.dir(transaction.toJson(),{colors:true});constannouncePath='/transaction/announce';console.log('Announcing mosaic definition to',announcePath);constannounceResponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'POST',headers:{'Content-Type':'application/json'},body:jsonPayload});constannounceResult=awaitannounceResponse.json();console.log(' Result:',announceResult.message);if('SUCCESS'===announceResult.message){consttransactionHash=facade.hashTransaction(transaction).toString();conststatusPath=`/transaction/get?hash=${transactionHash}`;console.log('Waiting for confirmation from',statusPath);letisConfirmed=false;for(letattempt=1;120>=attempt;++attempt){constresponse=awaitfetch(`${NODE_URL}${statusPath}`);if(response.ok){constconfirmed=awaitresponse.json();console.log('Transaction confirmed in block',confirmed.meta.height);isConfirmed=true;break;}console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}if(!isConfirmed)console.warn('Confirmation took too long.');}else{console.log('Transaction rejected:',announceResult.message);}// Retrieve the levyconstdefinitionPath=`/mosaic/definition?mosaicId=${mosaicId}`;console.log('Fetching mosaic information from',definitionPath);constdefinitionResponse=awaitfetch(`${NODE_URL}${definitionPath}`);constmosaicInfo=awaitdefinitionResponse.json();constlevyInfo=mosaicInfo.levy;constlevyMosaicId=levyInfo.mosaicId;constlevyType=1===levyInfo.type?'absolute':'percentile';console.log('Levy information:');console.log(' Type:',levyType);console.log(' Recipient:',levyInfo.recipient);console.log(' Mosaic:',`${levyMosaicId.namespaceId}:${levyMosaicId.name}`);console.log(' Fee:',levyInfo.fee);}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
The snippet reads the signer's private key from the SIGNER_PRIVATE_KEY environment variable, which defaults to a
test key if not set.
This account signs the transaction and becomes the owner of the mosaic, so it must also own the namespace that will
hold it.
The mosaic identifier is assembled from that namespace and a mosaic name.
See Name in the Textbook for the naming rules.
To avoid collisions across multiple runs of the tutorial, a timestamp is added to the mosaic name.
In practice, however, programs would use a fixed name for their mosaics.
You can force the tutorial to use fixed names through the NAMESPACE and MOSAIC environment variables.
Use a namespace owned by the signer
By default, the code uses the test account referenced by SIGNER_PRIVATE_KEY and a namespace named
my_namespace.
If you come from the Registering a Root Namespace tutorial, set the
SIGNER_PRIVATE_KEY and NAMESPACE environment variables to match the account and namespace you created there,
or any other namespace that the signer owns.
# Fetch current network timetime_path='/time-sync/network-time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())network_time=response_json['receiveTimeStamp']//1000print(f' Network time: {network_time} s since the nemesis block')# Derived fields from network timetimestamp=NetworkTimestamp(network_time)deadline=timestamp.add_hours(2)
// Fetch current network timeconsttimePath='/time-sync/network-time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();constnetworkTime=Math.floor(timeJSON.receiveTimeStamp/1000);console.log(' Network time:',networkTime,'s since the nemesis block');// Derived fields from network timeconsttimestamp=newNetworkTimestamp(networkTime);constdeadline=timestamp.addHours(2);
Network time is fetched from /time-sync/network-timeGET, and the transaction's timestamp and deadline fields
are derived from it, following the process described in the Transfer XEM tutorial.
// Describe the levyconstLEVY_RECIPIENT=process.env.LEVY_RECIPIENT||'TBULEAUG2CZQISUR442HWA6UAKGWIXHDABJVIPS4';constlevy={transferFeeType:'absolute',recipientAddress:LEVY_RECIPIENT,mosaicId:{namespaceId:{name:'nem'},name:'xem'},fee:1_000_000};console.log('Levy:');console.log(' Type:',levy.transferFeeType);console.log(' Recipient:',levy.recipientAddress);console.log(' Mosaic:',`${levy.mosaicId.namespaceId.name}:${levy.mosaicId.name}`);console.log(' Fee:',levy.fee);
The levy is a MosaicLevy structure with four fields:
: How the levy amount is calculated:
absolute: A fixed quantity charged on every transfer, regardless of the amount transferred.
percentile: A quantity proportional to the amount transferred.
This tutorial uses an absolute levy, so every transfer is charged the same amount.
: The account credited with the levy on every transfer.
It can be the mosaic creator or any other account.
: The mosaic in which the levy is paid.
This tutorial charges the levy in nem:xem, so senders pay in the network currency.
The levy can also be paid in the mosaic being defined itself.
Any other levy mosaic must already exist on the network and be
transferable.
: The levy amount.
For an absolute levy, it is expressed in the atomic units of the
levy mosaic.
Because nem:xem has a divisibility of 6, a value of 1'000'000 charges 1 XEM per transfer.
For a percentile levy, the fee is interpreted in basis points instead: a fee of 100 charges 1% of the
amount transferred.
See the percentile levy calculation in the Textbook for
the full rules.
// Build the mosaic definition transactionconstrentalFee=calculateMosaicRentalFee();console.log(' Mosaic creation fee:',`${Number(rentalFee)/1_000_000} XEM`);consttransaction=facade.transactionFactory.create({type:'mosaic_definition_transaction_v1',signerPublicKey:signerKeyPair.publicKey.toString(),timestamp:timestamp.timestamp,deadline:deadline.timestamp,rentalFeeSink:'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC',rentalFee,mosaicDefinition:{ownerPublicKey:signerKeyPair.publicKey.toString(),id:{namespaceId:{name:namespaceName},name:mosaicName},description:'My tutorial mosaic with a levy',properties:[{property:{name:'divisibility',value:'2'}},{property:{name:'initialSupply',value:'1000'}},{property:{name:'supplyMutable',value:'true'}},{property:{name:'transferable',value:'true'}}],levy}});// Calculate and attach the transaction feeconstfee=calculateTransactionFee(transaction);transaction.fee=newmodels.Amount(fee);console.log(' Transaction fee:',`${Number(fee)/1_000_000} XEM`);
A levy is part of the mosaic definition, so it is set with the same MosaicDefinitionTransactionV1 used in
Creating a Mosaic.
This tutorial reuses the same transaction, with the levy added to the field.
The creation fee is 10 XEM, and the transaction fee is a fixed 0.15 XEM, as
shown in the fee schedule.
# Sign, announce and wait for confirmationsignature=facade.sign_transaction(signer_key_pair,transaction)json_payload=facade.transaction_factory.attach_signature(transaction,signature)print('Built mosaic definition transaction:')print(json.dumps(transaction.to_json(),indent=2))announce_path='/transaction/announce'print(f'Announcing mosaic definition to {announce_path}')announce_request=urllib.request.Request(f'{NODE_URL}{announce_path}',data=json_payload.encode(),headers={'Content-Type':'application/json'},method='POST')withurllib.request.urlopen(announce_request)asresponse:announce_result=json.loads(response.read().decode())print(f' Result: {announce_result["message"]}')if'SUCCESS'==announce_result['message']:transaction_hash=facade.hash_transaction(transaction)status_path=f'/transaction/get?hash={transaction_hash}'print(f'Waiting for confirmation from {status_path}')is_confirmed=Falseforattemptinrange(120):try:withurllib.request.urlopen(f'{NODE_URL}{status_path}')asresponse:confirmed=json.loads(response.read().decode())height=confirmed['meta']['height']print(f'Transaction confirmed in block {height}')is_confirmed=Truebreakexcepturllib.error.HTTPError:print(' Transaction status: pending')time.sleep(1)ifnotis_confirmed:print('Confirmation took too long.')else:print(f'Transaction rejected: {announce_result["message"]}')
// Sign, announce and wait for confirmationconstsignature=facade.signTransaction(signerKeyPair,transaction);constjsonPayload=facade.transactionFactory.static.attachSignature(transaction,signature);console.log('Built mosaic definition transaction:');console.dir(transaction.toJson(),{colors:true});constannouncePath='/transaction/announce';console.log('Announcing mosaic definition to',announcePath);constannounceResponse=awaitfetch(`${NODE_URL}${announcePath}`,{method:'POST',headers:{'Content-Type':'application/json'},body:jsonPayload});constannounceResult=awaitannounceResponse.json();console.log(' Result:',announceResult.message);if('SUCCESS'===announceResult.message){consttransactionHash=facade.hashTransaction(transaction).toString();conststatusPath=`/transaction/get?hash=${transactionHash}`;console.log('Waiting for confirmation from',statusPath);letisConfirmed=false;for(letattempt=1;120>=attempt;++attempt){constresponse=awaitfetch(`${NODE_URL}${statusPath}`);if(response.ok){constconfirmed=awaitresponse.json();console.log('Transaction confirmed in block',confirmed.meta.height);isConfirmed=true;break;}console.log(' Transaction status: pending');awaitnewPromise(resolve=>{setTimeout(resolve,1000);});}if(!isConfirmed)console.warn('Confirmation took too long.');}else{console.log('Transaction rejected:',announceResult.message);}
The transaction is then signed, announced, and confirmed following the same process as in the
Transfer XEM tutorial.
// Retrieve the levyconstdefinitionPath=`/mosaic/definition?mosaicId=${mosaicId}`;console.log('Fetching mosaic information from',definitionPath);constdefinitionResponse=awaitfetch(`${NODE_URL}${definitionPath}`);constmosaicInfo=awaitdefinitionResponse.json();constlevyInfo=mosaicInfo.levy;constlevyMosaicId=levyInfo.mosaicId;constlevyType=1===levyInfo.type?'absolute':'percentile';console.log('Levy information:');console.log(' Type:',levyType);console.log(' Recipient:',levyInfo.recipient);console.log(' Mosaic:',`${levyMosaicId.namespaceId}:${levyMosaicId.name}`);console.log(' Fee:',levyInfo.fee);
To verify the mosaic with the levy was created, the code retrieves the mosaic definition from the
/mosaic/definitionGET endpoint, which returns the levy alongside the mosaic properties.
A levy in the response confirms that future transfers of the mosaic will be charged the levy.
The network rejects the transfer if the sender cannot cover both the transferred amount and the levy.
Because this levy is paid in nem:xem, the sender must also have enough XEM to cover both the levy and the transaction
fee.
If the levy is paid in another mosaic, the sender must also hold a sufficient balance of that mosaic.
Mosaic ID (line 3): The mosaic is identified by its fully qualified name, combining the namespace
my_namespace and a timestamped mosaic name.
Search for this name in the NEM testnet explorer to view the mosaic details.
Levy fields (lines 6-10): The levy to create, an absolute fee of 1'000'000 atomic units of nem:xem (1 XEM),
paid to the levy recipient on every transfer.
Levy in the transaction (lines 58-68): The levy is defined inside the mosaic definition.
The recipient address, the levy mosaic name, and the mosaic name are hex-encoded in the payload, while the fee of
this absolute levy is expressed in atomic units.
Verified levy (lines 82-85): The mosaic is retrieved from the network, confirming the levy type, its recipient,
the mosaic in which it is paid, and its amount.