初級
モザイクの回収(リボーク)
revokable(回収可能)フラグを設定して作成された モザイク は、作成者が任意のアカウントからモザイクを回収し、自身の アカウント 残高に戻すことができます。これは、契約条項の執行、未使用トークンの回収、または誤った配布の修正に役立ちます。
このチュートリアルでは、他のアカウントからモザイクを回収する方法を説明します。
前提条件
開始する前に、以下を確認してください。
さらに、トランザクションがどのようにアナウンスされ承認されるかを理解するために、転送トランザクション チュートリアルを復習しておいてください 。
回収可能性の詳細については、テキストブックの 回収可能性 を参照してください。
完全なコード
import json
import os
import time
import urllib.request
from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.sc import Amount
from symbolchain.symbol.Network import NetworkTimestamp
NODE_URL = os . getenv ( 'NODE_URL' , 'https://reference.symboltest.net:3001' )
print ( f 'Using node { NODE_URL } ' )
# Helper function to fetch account mosaic balances
def get_account_mosaics ( address ):
account_path = f '/accounts/ { address } '
print ( f 'Fetching account information from { account_path } ' )
with urllib . request . urlopen ( f ' { NODE_URL }{ account_path } ' ) as resp :
resp_json = json . loads ( resp . read () . decode ())
return resp_json [ 'account' ][ 'mosaics' ]
SIGNER_PRIVATE_KEY = os . getenv ( 'SIGNER_PRIVATE_KEY' ,
'0000000000000000000000000000000000000000000000000000000000000000' )
signer_key_pair = SymbolFacade . KeyPair ( PrivateKey ( SIGNER_PRIVATE_KEY ))
facade = SymbolFacade ( 'testnet' )
signer_address = facade . network . public_key_to_address (
signer_key_pair . public_key )
print ( f 'Signer address: { signer_address } ' )
SOURCE_ADDRESS = os . getenv ( 'SOURCE_ADDRESS' ,
'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA' )
print ( f 'Source address: { SOURCE_ADDRESS } ' )
MOSAIC_ID_HEX = os . getenv ( 'MOSAIC_ID' , '7aed3d514c986941' )
mosaic_id = int ( MOSAIC_ID_HEX , 16 )
print ( f 'Mosaic ID: { mosaic_id } (0x { MOSAIC_ID_HEX } )' )
try :
# Fetch current network time
time_path = '/node/time'
print ( f 'Fetching current network time from { time_path } ' )
with urllib . request . urlopen ( f ' { NODE_URL }{ time_path } ' ) as response :
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 fees
fee_path = '/network/fees/transaction'
print ( f 'Fetching recommended fees from { fee_path } ' )
with urllib . request . urlopen ( f ' { NODE_URL }{ fee_path } ' ) as response :
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 } ' )
# --- CHECKING INITIAL BALANCE ---
print ( ' \n --- Checking initial balance ---' )
mosaics = get_account_mosaics ( SOURCE_ADDRESS )
for mosaic in mosaics :
if mosaic [ 'id' ] == MOSAIC_ID_HEX . upper ():
print ( f ' Mosaic ID: { mosaic [ "id" ] } ,'
f ' Amount: { mosaic [ "amount" ] } ' )
# --- REVOKING MOSAIC ---
print ( ' \n --- Revoking mosaic ---' )
revoke_tx = facade . transaction_factory . create ({
'type' : 'mosaic_supply_revocation_transaction_v1' ,
'signer_public_key' : signer_key_pair . public_key ,
'deadline' : timestamp . add_hours ( 2 ) . timestamp ,
'source_address' : SOURCE_ADDRESS ,
'mosaic' : {
'mosaic_id' : mosaic_id ,
'amount' : 7_00
}
})
revoke_tx . fee = Amount ( fee_multiplier * revoke_tx . size )
# Sign and generate final payload
signature = facade . sign_transaction (
signer_key_pair , revoke_tx )
json_payload = facade . transaction_factory . attach_signature (
revoke_tx , signature )
print ( 'Built mosaic revocation transaction:' )
print ( json . dumps ( revoke_tx . to_json (), indent = 2 ))
# Announce transaction
revoke_hash = facade . hash_transaction ( revoke_tx )
print ( f 'Transaction hash: { revoke_hash } ' )
print ( 'Announcing mosaic revocation to /transactions' )
request = urllib . request . Request (
f ' { NODE_URL } /transactions' ,
data = json_payload . encode (),
headers = { 'Content-Type' : 'application/json' },
method = 'PUT'
)
with urllib . request . urlopen ( request ) as response :
print ( f ' Response: { response . read () . decode () } ' )
# Wait for confirmation
print ( 'Waiting for mosaic revocation confirmation...' )
for attempt in range ( 60 ):
time . sleep ( 1 )
try :
status_url = f ' { NODE_URL } /transactionStatus/ { revoke_hash } '
with urllib . request . urlopen ( status_url ) as response :
status = json . loads ( response . read () . decode ())
print ( f ' Transaction status: { status [ "group" ] } ' )
if status [ 'group' ] == 'confirmed' :
print ( 'Mosaic revocation confirmed in' ,
attempt , 'seconds' )
break
if status [ 'group' ] == 'failed' :
raise RuntimeError (
f 'Mosaic revocation failed: { status [ "code" ] } ' )
except urllib . error . HTTPError :
print ( ' Transaction status: unknown' )
# --- VERIFYING REVOCATION ---
print ( ' \n --- Verifying revocation ---' )
mosaics = get_account_mosaics ( SOURCE_ADDRESS )
for mosaic in mosaics :
if mosaic [ 'id' ] == MOSAIC_ID_HEX . upper ():
print ( f ' Mosaic ID: { mosaic [ "id" ] } ,'
f ' Amount: { mosaic [ "amount" ] } ' )
except Exception as e :
print ( e )
Download source
import { PrivateKey } from 'symbol-sdk' ;
import {
NetworkTimestamp ,
SymbolFacade ,
models
} from 'symbol-sdk/symbol' ;
const NODE_URL = process . env . NODE_URL ||
'https://reference.symboltest.net:3001' ;
console . log ( 'Using node' , NODE_URL );
// Helper function to fetch account mosaic balances
async function getAccountMosaics ( address ) {
const accountPath = `/accounts/ ${ address } ` ;
console . log ( 'Fetching account information from' , accountPath );
const response = await fetch ( ` ${ NODE_URL }${ accountPath } ` );
const responseJSON = await response . json ();
return responseJSON . account . mosaics ;
}
const SIGNER_PRIVATE_KEY = process . env . SIGNER_PRIVATE_KEY ||
'0000000000000000000000000000000000000000000000000000000000000000' ;
const signerKeyPair = new SymbolFacade . KeyPair (
new PrivateKey ( SIGNER_PRIVATE_KEY ));
const facade = new SymbolFacade ( 'testnet' );
const signerAddress =
facade . network . publicKeyToAddress ( signerKeyPair . publicKey );
console . log ( 'Signer address:' , signerAddress . toString ());
const SOURCE_ADDRESS = process . env . SOURCE_ADDRESS ||
'TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA' ;
console . log ( 'Source address:' , SOURCE_ADDRESS );
const MOSAIC_ID_HEX = process . env . MOSAIC_ID ||
'7aed3d514c986941' ;
const mosaicId = BigInt ( `0x ${ MOSAIC_ID_HEX } ` );
console . log (
`Mosaic ID: ${ mosaicId } (0x ${ MOSAIC_ID_HEX } )` );
try {
// Fetch current network time
const timePath = '/node/time' ;
console . log ( 'Fetching current network time from' , timePath );
const timeResponse = await fetch ( ` ${ NODE_URL }${ timePath } ` );
const timeJSON = await timeResponse . json ();
const timestamp = new NetworkTimestamp (
timeJSON . communicationTimestamps . receiveTimestamp );
console . log ( ' Network time:' ,
timestamp . timestamp , 'ms since nemesis' );
// Fetch recommended fees
const feePath = '/network/fees/transaction' ;
console . log ( 'Fetching recommended fees from' , feePath );
const feeResponse = await fetch ( ` ${ NODE_URL }${ feePath } ` );
const feeJSON = await feeResponse . json ();
const medianMultiplier = feeJSON . medianFeeMultiplier ;
const minimumMultiplier = feeJSON . minFeeMultiplier ;
const feeMultiplier = Math . max ( medianMultiplier , minimumMultiplier );
console . log ( ' Fee multiplier:' , feeMultiplier );
// --- CHECKING INITIAL BALANCE ---
console . log ( '\n--- Checking initial balance ---' );
let mosaics = await getAccountMosaics ( SOURCE_ADDRESS );
for ( const mosaic of mosaics ) {
if ( mosaic . id === MOSAIC_ID_HEX . toUpperCase ()) {
console . log ( ` Mosaic ID: ${ mosaic . id } ,` +
` Amount: ${ mosaic . amount } ` );
}
}
// --- REVOKING MOSAIC ---
console . log ( '\n--- Revoking mosaic ---' );
const revokeTx = facade . transactionFactory . create ({
type : 'mosaic_supply_revocation_transaction_v1' ,
signerPublicKey : signerKeyPair . publicKey . toString (),
deadline : timestamp . addHours ( 2 ). timestamp ,
sourceAddress : SOURCE_ADDRESS ,
mosaic : {
mosaicId ,
amount : 7 _00n
}
});
revokeTx . fee = new models . Amount ( feeMultiplier * revokeTx . size );
// Sign and generate final payload
const signature = facade . signTransaction ( signerKeyPair , revokeTx );
const jsonPayload = facade . transactionFactory . static . attachSignature (
revokeTx , signature );
console . log ( 'Built mosaic revocation transaction:' );
console . dir ( revokeTx . toJson (), { colors : true });
// Announce transaction
const revokeHash = facade . hashTransaction ( revokeTx ). toString ();
console . log ( 'Transaction hash:' , revokeHash );
console . log ( 'Announcing mosaic revocation to /transactions' );
const announceResponse = await fetch ( ` ${ NODE_URL } /transactions` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' },
body : jsonPayload
});
console . log ( ' Response:' , await announceResponse . text ());
// Wait for confirmation
console . log ( 'Waiting for mosaic revocation confirmation...' );
const statusPath = `/transactionStatus/ ${ revokeHash } ` ;
for ( let attempt = 1 ; 60 >= attempt ; ++ attempt ) {
await new Promise ( resolve => { setTimeout ( resolve , 1000 ); });
const response = await fetch ( ` ${ NODE_URL }${ statusPath } ` );
if ( response . ok ) {
const status = await response . json ();
console . log ( ' Transaction status:' , status . group );
if ( 'confirmed' === status . group ) {
console . log ( 'Transaction confirmed in' , attempt ,
'seconds' );
break ;
}
if ( 'failed' === status . group ) {
console . log ( 'Transaction failed:' , status . code );
break ;
}
} else {
console . log ( ' Transaction status: unknown | Cause:' ,
response . status
);
}
if ( 60 === attempt )
console . warn ( 'Confirmation took too long.' );
}
// --- VERIFYING REVOCATION ---
console . log ( '\n--- Verifying revocation ---' );
mosaics = await getAccountMosaics ( SOURCE_ADDRESS );
for ( const mosaic of mosaics ) {
if ( mosaic . id === MOSAIC_ID_HEX . toUpperCase ()) {
console . log ( ` Mosaic ID: ${ mosaic . id } ,` +
` Amount: ${ mosaic . amount } ` );
}
}
} catch ( e ) {
console . error ( e . message );
}
Download source
コード解説
アカウントの設定
このスニペットは、署名者の秘密鍵を SIGNER_PRIVATE_KEY 環境変数から読み取ります。設定されていない場合はテストキーがデフォルトとして使用されます 。
署名者のアドレは公開鍵から派生します 。
このアカウントは、 revokable フラグを持つモザイクの元の作成者である必要があります 。
SOURCE_ADDRESS 環境変数は、モザイクユニットが回収されるアカウントのアドレスを指定します 。
MOSAIC_ID 環境変数は、回収するモザイクの16進数識別子を指定します 。
アカウントが保持しているモザイクを一覧表示するには、アカウント残高の照会 を参照してください 。
ネットワーク時間と手数料の取得
転送トランザクション チュートリアルで説明されているプロセスに従い、ネットワーク時間と推奨手数料をそれぞれ /node/time GET および /network/fees/transaction GET から取得します 。
初期残高の確認
回収を行う前に、ヘルパー関数 get_account_mosaics が /accounts/{accountId} GET エンドポイントからソースアカウントの対象モザイクの現在残高を取得します 。
これにより、回収後の結果と比較するための基準が得られます 。
回収トランザクションの構築
revoke_tx = facade . transaction_factory . create ({
'type' : 'mosaic_supply_revocation_transaction_v1' ,
'signer_public_key' : signer_key_pair . public_key ,
'deadline' : timestamp . add_hours ( 2 ) . timestamp ,
'source_address' : SOURCE_ADDRESS ,
'mosaic' : {
'mosaic_id' : mosaic_id ,
'amount' : 7_00
}
})
revoke_tx . fee = Amount ( fee_multiplier * revoke_tx . size )
const revokeTx = facade . transactionFactory . create ({
type : 'mosaic_supply_revocation_transaction_v1' ,
signerPublicKey : signerKeyPair . publicKey . toString (),
deadline : timestamp . addHours ( 2 ). timestamp ,
sourceAddress : SOURCE_ADDRESS ,
mosaic : {
mosaicId ,
amount : 7 _00n
}
});
revokeTx . fee = new models . Amount ( feeMultiplier * revokeTx . size );
回収トランザクションは、ソースアカウントからモザイクユニットを回収し、作成者の残高に戻します 。
部分的な回収
数量はソースアカウントの全残高と一致する必要はありません 。
単一のトランザクションで、ソースの現在の保有量までの任意の数量を回収できます 。
回収の送信
# Sign and generate final payload
signature = facade . sign_transaction (
signer_key_pair , revoke_tx )
json_payload = facade . transaction_factory . attach_signature (
revoke_tx , signature )
print ( 'Built mosaic revocation transaction:' )
print ( json . dumps ( revoke_tx . to_json (), indent = 2 ))
# Announce transaction
revoke_hash = facade . hash_transaction ( revoke_tx )
print ( f 'Transaction hash: { revoke_hash } ' )
print ( 'Announcing mosaic revocation to /transactions' )
request = urllib . request . Request (
f ' { NODE_URL } /transactions' ,
data = json_payload . encode (),
headers = { 'Content-Type' : 'application/json' },
method = 'PUT'
)
with urllib . request . urlopen ( request ) as response :
print ( f ' Response: { response . read () . decode () } ' )
// Sign and generate final payload
const signature = facade . signTransaction ( signerKeyPair , revokeTx );
const jsonPayload = facade . transactionFactory . static . attachSignature (
revokeTx , signature );
console . log ( 'Built mosaic revocation transaction:' );
console . dir ( revokeTx . toJson (), { colors : true });
// Announce transaction
const revokeHash = facade . hashTransaction ( revokeTx ). toString ();
console . log ( 'Transaction hash:' , revokeHash );
console . log ( 'Announcing mosaic revocation to /transactions' );
const announceResponse = await fetch ( ` ${ NODE_URL } /transactions` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' },
body : jsonPayload
});
console . log ( ' Response:' , await announceResponse . text ());
回収トランザクションは、転送トランザクションの作成 と同じプロセスに従って署名され、アナウンスされます 。
コードはその後、ステータスが confirmed に変わるまで /transactionStatus/{hash} GET エンドポイントをポーリングして、トランザクションが承認されるのを待ちます 。
回収の検証
回収を検証するために、ヘルパー関数 get_account_mosaics がソースアカウントの残高を再度取得します 。
残高は 初期残高 よりも回収量分だけ少なくなっているはずです 。
出力
以下に示す出力は、プログラムの典型的な実行結果に対応しています 。
Using node https://reference.symboltest.net:3001
Signer address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Source address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Mosaic ID: 8857803461494335809 (0x7aed3d514c986941)
Fetching current network time from /node/time
Network time: 104783268507 ms since nemesis
Fetching recommended fees from /network/fees/transaction
Fee multiplier: 100
--- Checking initial balance ---
Fetching account information from /accounts/TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Mosaic ID: 7AED3D514C986941, Amount: 1000
--- Revoking mosaic ---
Built mosaic revocation transaction:
{
"signature": "225719859B43C8B9FCB04432E83FA95258C15A64B60974754DC2E4CAF9E58110995F2ECB5552049C3F79632FFAF19F4B90247012A7777E79D3AF0A17507A9F0D",
"signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
"version": 1,
"network": 152,
"type": 17229,
"fee": "16800",
"deadline": "104790468507",
"source_address": "987D075454716222F609929E883174AD8C996D5828C938BC",
"mosaic": {
"mosaic_id": "8857803461494335809",
"amount": "700"
}
}
Transaction hash: C0E59A5E36FC50CC5BDF8A16EABAB791611737B64E3590D43576374B803771E6
Announcing mosaic revocation to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for mosaic revocation confirmation...
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: unconfirmed
Transaction status: confirmed
Mosaic revocation confirmed in 8 seconds
--- Verifying revocation ---
Fetching account information from /accounts/TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA
Mosaic ID: 7AED3D514C986941, Amount: 300
出力の主なポイント:
モザイクID (4行目): モザイクID 8857803461494335809 (0x7aed3d514c986941) は、回収対象のモザイクを識別します 。
初期残高 (12行目): 回収前、ソースアカウントはそのモザイクを 1000 絶対単位保持しています 。
ソースアドレス (24行目): source_address フィールドは、ユニットが回収されるアカウントを識別します 。
これは、3行目に示されている Base32 アドレス の16進数エンコード形式です 。
回収量 (26-27行目): mosaic オブジェクトは、10進数形式のモザイクIDと数量 700 を指定しています 。
10進数の値は、4行目に示されている16進数IDに対応します 。
検証された残高 (47行目): 回収後、ソースアカウントの残高は 300 になっており、 700 絶対単位が正常に回収されたことが確認されました 。
出力に印刷されたトランザクション ハッシュ を使用して、 Symbol Testnet Explorer でトランザクションを検索できます 。
結論
このチュートリアルでは、以下の方法を説明しました。