コンテンツにスキップ

トランザクションのステータスを監視する⚓︎

初級

NEM ネットワークへ トランザクション をアナウンスした後、トランザクションは ブロック に含まれるまで未承認のままです。

ステータスの変化を監視することは、トランザクションの承認または失敗に応答できる、応答性の高いアプリケーションを構築するために重要です。

このチュートリアルでは、承認されるまでトランザクションのステータスをポーリングする方法、未承認トランザクションプール で承認を待っているか確認する方法、そしていつまでも承認されないと判断する方法を説明します。

このような監視は通常、XEM を送信する チュートリアルに示すように、トランザクションが承認されることを確認するため、アナウンス直後に行います。

本番環境ではポーリングは推奨されません

このチュートリアルでは説明のためにポーリングでトランザクションのステータスを確認しますが、本番アプリケーションに推奨される方法ではありません。

WebSocket を使えば、API を繰り返し呼び出すオーバーヘッドなしに、より応答性の高い解決策を実現できます。

前提条件⚓︎

このチュートリアルでは、SDK を必要とせずに NEM REST API を使用します。 HTTP リクエストを送信する方法だけが必要です。

完全なコード⚓︎

import json
import os
import time
import urllib.error
import urllib.request

NODE_URL = os.getenv("NODE_URL", "http://libertalia.nemtest.net:7890")
print(f'Using node {NODE_URL}')


# Transaction hash to monitor
transaction_hash = os.getenv(
    "TRANSACTION_HASH",
    "AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703C0B")
# Signer's address
signer_address = os.getenv(
    "SIGNER_ADDRESS",
    "TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP")
# Transaction signature
transaction_signature = os.getenv(
    "TRANSACTION_SIGNATURE",
    "99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70F52F089BD651DF"
    "A7D4991639A48B810EFD98C45060D7AD9AE57FDA37F58561459DCE8D0A747F02")

print(f"Monitoring transaction: {transaction_hash}")


def get_confirmation_height(tx_hash):
    """
    Query /transaction/get once to check for confirmation.

    Args:
        tx_hash: hash of the transaction to check

    Returns:
        The height of the block containing the transaction, or None
        if the transaction is not confirmed yet
    """
    url = f"{NODE_URL}/transaction/get?hash={tx_hash}"
    try:
        with urllib.request.urlopen(url) as response:
            confirmed = json.loads(response.read().decode())
            return confirmed["meta"]["height"]
    except urllib.error.HTTPError as err:
        if err.status != 400:
            raise
        return None


def is_in_unconfirmed_pool(signature, address):
    """
    Check whether a transaction with the given signature is in the
    address's unconfirmed pool.
    """
    path = f"/account/unconfirmedTransactions?address={address}"
    with urllib.request.urlopen(f"{NODE_URL}{path}") as response:
        pool = json.loads(response.read().decode())["data"]

    target = signature.lower()
    return any(
        entry["transaction"]["signature"].lower() == target
        for entry in pool
    )


def wait_for_confirmation(
    tx_hash, max_attempts=120, wait_seconds=1
):
    """
    Check for confirmation repeatedly until the transaction is confirmed
    or the attempts run out.

    Args:
        tx_hash: hash of the transaction to monitor
        max_attempts: maximum polling attempts
        wait_seconds: seconds to wait between attempts

    Returns:
        True if the transaction was confirmed, False otherwise
    """
    print("\nWaiting for transaction confirmation")
    for attempt in range(1, max_attempts + 1):
        time.sleep(wait_seconds)
        height = get_confirmation_height(tx_hash)
        status = f"confirmed in block {height}" if height else "pending"
        print(f"  Attempt {attempt}: {status}")
        if height:
            return True
    return False


try:
    block_height = get_confirmation_height(transaction_hash)
    if block_height:
        print(f"\nTransaction confirmed in block {block_height}")
    elif not is_in_unconfirmed_pool(transaction_signature,
            signer_address):
        print("\nTransaction not found")
    elif wait_for_confirmation(transaction_hash):
        print("\nTransaction confirmed!")
    else:
        print("\nTransaction not confirmed within the polling window")
except urllib.error.URLError as err:
    print(f"\nCould not reach the node: {err.reason}")

Download source

const NODE_URL = process.env.NODE_URL ||
    'http://libertalia.nemtest.net:7890';
console.log('Using node', NODE_URL);


// Transaction hash to monitor.
const transactionHash = process.env.TRANSACTION_HASH ||
    'AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703C0B';
// Signer's address.
const signerAddress = process.env.SIGNER_ADDRESS ||
    'TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP';
// Transaction signature.
const transactionSignature = process.env.TRANSACTION_SIGNATURE ||
    '99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70F52F089BD651DF' +
    'A7D4991639A48B810EFD98C45060D7AD9AE57FDA37F58561459DCE8D0A747F02';

console.log(`Monitoring transaction: ${transactionHash}`);


/**
 * Query /transaction/get once to check for confirmation.
 * @param {string} txHash - hash of the transaction to check
 * @returns {number|null} height of the block containing the
 *   transaction, or null if it is not confirmed yet
 */
async function getConfirmationHeight(txHash) {
    const url = `${NODE_URL}/transaction/get?hash=${txHash}`;
    const response = await fetch(url);
    if (response.ok) {
        const confirmed = await response.json();
        return confirmed.meta.height;
    }
    if (400 !== response.status)
        throw new Error(`Unexpected status: ${response.status}`);
    return null;
}


/**
 * Check whether a transaction with the given signature is in the
 * address's unconfirmed pool.
 * @param {string} signature - hex signature of the monitored transaction
 * @param {string} address - signer's address
 * @returns {boolean} true if the signature is in the signer's pool
 */
async function isInUnconfirmedPool(signature, address) {
    const path = `/account/unconfirmedTransactions?address=${address}`;
    const response = await fetch(`${NODE_URL}${path}`);
    const pool = (await response.json()).data;

    const target = signature.toLowerCase();
    return pool.some(
        entry => entry.transaction.signature.toLowerCase() === target
    );
}


/**
 * Check for confirmation repeatedly until the transaction is
 * confirmed or the attempts run out.
 * @param {string} txHash - hash of the transaction to monitor
 * @param {number} maxAttempts - maximum polling attempts
 * @param {number} waitSeconds - seconds to wait between attempts
 * @returns {boolean} true if the transaction was confirmed
 */
async function waitForConfirmation(
    txHash,
    maxAttempts = 120,
    waitSeconds = 1
) {
    console.log('\nWaiting for transaction confirmation');
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        await new Promise(resolve => {
            setTimeout(resolve, waitSeconds * 1000);
        });
        const height = await getConfirmationHeight(txHash);
        const status =
            height ? `confirmed in block ${height}` : 'pending';
        console.log(`  Attempt ${attempt}: ${status}`);
        if (height)
            return true;
    }
    return false;
}


try {
    const blockHeight = await getConfirmationHeight(transactionHash);
    if (blockHeight)
        console.log(`\nTransaction confirmed in block ${blockHeight}`);
    else if (!(await isInUnconfirmedPool(transactionSignature,
        signerAddress)))
        console.log('\nTransaction not found');
    else if (await waitForConfirmation(transactionHash))
        console.log('\nTransaction confirmed!');
    else
        console.log('\nConfirmation timed out');
} catch (error) {
    console.log(`\nCould not reach the node: ${error.message}`);
}

Download source

スニペットでは、NODE_URL 環境変数を使って NEM API ノードを指定します。 値が指定されていない場合は、デフォルト値を使用します。

チュートリアルでは、まず次の再利用可能な関数を定義します。

  • : トランザクションが ハーベスティング によって承認され、すでにブロックチェーンの一部になっているか確認します。
  • : トランザクションが 未承認トランザクションプール で承認を待っているかを返します。
  • : トランザクションが承認されるか試行回数に達するまで、承認の確認を繰り返します。

その後、すべてを組み合わせる に示すように、これらを一緒に呼び出してトランザクションを監視します。

コードの説明⚓︎

トランザクションハッシュ、アドレス、署名を取得する⚓︎

# Transaction hash to monitor
transaction_hash = os.getenv(
    "TRANSACTION_HASH",
    "AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703C0B")
# Signer's address
signer_address = os.getenv(
    "SIGNER_ADDRESS",
    "TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP")
# Transaction signature
transaction_signature = os.getenv(
    "TRANSACTION_SIGNATURE",
    "99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70F52F089BD651DF"
    "A7D4991639A48B810EFD98C45060D7AD9AE57FDA37F58561459DCE8D0A747F02")
// Transaction hash to monitor.
const transactionHash = process.env.TRANSACTION_HASH ||
    'AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703C0B';
// Signer's address.
const signerAddress = process.env.SIGNER_ADDRESS ||
    'TBONKWCOWBZYZB2I5JD3LSDBQVBYHB757VN3SKPP';
// Transaction signature.
const transactionSignature = process.env.TRANSACTION_SIGNATURE ||
    '99B1850FADDB964112D030AA0A5C9F8B5B1B6B992B407D9C70F52F089BD651DF' +
    'A7D4991639A48B810EFD98C45060D7AD9AE57FDA37F58561459DCE8D0A747F02';

トランザクションを監視するには、署名後に生成されるハッシュが必要です。 ハッシュは NEM ネットワーク上でトランザクションを一意に識別します。

スニペットでは、署名者のアドレストランザクションの署名 も読み取ります。 承認を検出するためにどちらも必要ではありませんが、未承認トランザクションプール でトランザクションが承認を待っているか確認するために使います。

スニペットではサンプル値を使用します。 TRANSACTION_HASHSIGNER_ADDRESSTRANSACTION_SIGNATURE 環境変数を設定すれば、値を上書きできます。 3 つの値はすべて、XEM を送信する チュートリアルに示すように、トランザクションへの署名時に生成されます。

承認を確認する⚓︎

def get_confirmation_height(tx_hash):
    """
    Query /transaction/get once to check for confirmation.

    Args:
        tx_hash: hash of the transaction to check

    Returns:
        The height of the block containing the transaction, or None
        if the transaction is not confirmed yet
    """
    url = f"{NODE_URL}/transaction/get?hash={tx_hash}"
    try:
        with urllib.request.urlopen(url) as response:
            confirmed = json.loads(response.read().decode())
            return confirmed["meta"]["height"]
    except urllib.error.HTTPError as err:
        if err.status != 400:
            raise
        return None
/**
 * Query /transaction/get once to check for confirmation.
 * @param {string} txHash - hash of the transaction to check
 * @returns {number|null} height of the block containing the
 *   transaction, or null if it is not confirmed yet
 */
async function getConfirmationHeight(txHash) {
    const url = `${NODE_URL}/transaction/get?hash=${txHash}`;
    const response = await fetch(url);
    if (response.ok) {
        const confirmed = await response.json();
        return confirmed.meta.height;
    }
    if (400 !== response.status)
        throw new Error(`Unexpected status: ${response.status}`);
    return null;
}

関数は、トランザクションハッシュを指定して /transaction/get GET を照会し、トランザクションが承認されたか確認します。

トランザクションがブロックに含まれると、このエンドポイントは meta.height のブロックの高さとともに内容を返し、関数はその高さを返します。

それ以外の場合、エンドポイントは HTTP 400(「Hash was not found in cache」)を返し、関数は高さを返しません。これはトランザクションが承認されていないことを意味します。 承認されていないトランザクションは 未承認トランザクションプール で承認を待っている可能性があり、次の関数が確認します。

ハッシュ検索の有効期間は短いです

/transaction/get GET はデフォルトで 36 時間保持されるキャッシュから読み取ります。 検索はデフォルトで有効ですが、ノード運用者は無効化したり保持期間を変更したりできます。

保持期間より古いトランザクションハッシュを照会すると、トランザクションが実際には承認済みでも HTTP 400 エラーが返ります。

そのため、この保持期間より後に検索する可能性のあるトランザクションをアナウンスするときは、ハッシュとともに承認ブロックの高さを保存してください。 そうすれば、/block/at/public POST エンドポイントでブロックからトランザクションを直接取得できます。

それ以外の場合は、/account/transfers/all GET で署名者の完全な履歴をページングするか、ブロックチェーンをブロックごとに検索してトランザクションを見つける必要があります。

未承認プールを確認する⚓︎

def is_in_unconfirmed_pool(signature, address):
    """
    Check whether a transaction with the given signature is in the
    address's unconfirmed pool.
    """
    path = f"/account/unconfirmedTransactions?address={address}"
    with urllib.request.urlopen(f"{NODE_URL}{path}") as response:
        pool = json.loads(response.read().decode())["data"]

    target = signature.lower()
    return any(
        entry["transaction"]["signature"].lower() == target
        for entry in pool
    )
/**
 * Check whether a transaction with the given signature is in the
 * address's unconfirmed pool.
 * @param {string} signature - hex signature of the monitored transaction
 * @param {string} address - signer's address
 * @returns {boolean} true if the signature is in the signer's pool
 */
async function isInUnconfirmedPool(signature, address) {
    const path = `/account/unconfirmedTransactions?address=${address}`;
    const response = await fetch(`${NODE_URL}${path}`);
    const pool = (await response.json()).data;

    const target = signature.toLowerCase();
    return pool.some(
        entry => entry.transaction.signature.toLowerCase() === target
    );
}

は署名者のアドレスを使って /account/unconfirmedTransactions GET を照会し、監視対象のトランザクションが承認待ちのリストに含まれているか報告します。

無効なトランザクションはプールに入りません

検証 に失敗したトランザクションは未承認プールに入りません。受信ノードは、XEM を送信する チュートリアルに示すように、アナウンス時にすぐ拒否します。

上記のエンドポイントレスポンスには各エントリのハッシュがないため、関数は 署名 で照合します。 トランザクションの署名は一意で、すべてのプールエントリの transaction.signature に含まれます。 マルチシグトランザクションでは、内部トランザクションではなく、アナウンスされたラッパーの署名です。

関数は次を返します。

  • : トランザクションはまだ未承認プールにあり、ブロックに含まれるのを待っています。
  • : トランザクションはレスポンスにありません。 署名者が使用したノードにまだ到着していない、すでに承認された、プールから削除された、レスポンスから除外されたなどの原因が考えられます。

    レスポンスは 25 トランザクションに制限されます

    エンドポイントは、アドレスに関係する最新 25 件までのトランザクションを返します。 受信トランザクションもこの上限に数えられるため、混雑したアカウントでは、監視対象のトランザクションが未承認プールに存在していてもレスポンスに含まれない可能性があります。

承認を待つ⚓︎

def wait_for_confirmation(
    tx_hash, max_attempts=120, wait_seconds=1
):
    """
    Check for confirmation repeatedly until the transaction is confirmed
    or the attempts run out.

    Args:
        tx_hash: hash of the transaction to monitor
        max_attempts: maximum polling attempts
        wait_seconds: seconds to wait between attempts

    Returns:
        True if the transaction was confirmed, False otherwise
    """
    print("\nWaiting for transaction confirmation")
    for attempt in range(1, max_attempts + 1):
        time.sleep(wait_seconds)
        height = get_confirmation_height(tx_hash)
        status = f"confirmed in block {height}" if height else "pending"
        print(f"  Attempt {attempt}: {status}")
        if height:
            return True
    return False
/**
 * Check for confirmation repeatedly until the transaction is
 * confirmed or the attempts run out.
 * @param {string} txHash - hash of the transaction to monitor
 * @param {number} maxAttempts - maximum polling attempts
 * @param {number} waitSeconds - seconds to wait between attempts
 * @returns {boolean} true if the transaction was confirmed
 */
async function waitForConfirmation(
    txHash,
    maxAttempts = 120,
    waitSeconds = 1
) {
    console.log('\nWaiting for transaction confirmation');
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        await new Promise(resolve => {
            setTimeout(resolve, waitSeconds * 1000);
        });
        const height = await getConfirmationHeight(txHash);
        const status =
            height ? `confirmed in block ${height}` : 'pending';
        console.log(`  Attempt ${attempt}: ${status}`);
        if (height)
            return true;
    }
    return false;
}

関数は、トランザクションが承認されるか、2 分が経過する(タイムアウトは設定可能)まで、毎秒 を呼び出します。

確認で承認が報告されると、関数はすぐ を返します。 試行回数を使い切ると、代わりに を返します。 これは、ポーリング期間内に承認されなかったことを意味するだけで、失敗したことを意味しません。ただし、このケースはまれです。

すべてを組み合わせる⚓︎

try:
    block_height = get_confirmation_height(transaction_hash)
    if block_height:
        print(f"\nTransaction confirmed in block {block_height}")
    elif not is_in_unconfirmed_pool(transaction_signature,
            signer_address):
        print("\nTransaction not found")
    elif wait_for_confirmation(transaction_hash):
        print("\nTransaction confirmed!")
    else:
        print("\nTransaction not confirmed within the polling window")
except urllib.error.URLError as err:
    print(f"\nCould not reach the node: {err.reason}")
try {
    const blockHeight = await getConfirmationHeight(transactionHash);
    if (blockHeight)
        console.log(`\nTransaction confirmed in block ${blockHeight}`);
    else if (!(await isInUnconfirmedPool(transactionSignature,
        signerAddress)))
        console.log('\nTransaction not found');
    else if (await waitForConfirmation(transactionHash))
        console.log('\nTransaction confirmed!');
    else
        console.log('\nConfirmation timed out');
} catch (error) {
    console.log(`\nCould not reach the node: ${error.message}`);
}

スニペットは を呼び出して、トランザクションがすでに承認されているか確認することから始めます。

承認済みトランザクションも取り消される可能性があります

承認済みトランザクションはブロックに含まれていますが、まだ不可逆ではありません。 後続ブロックが 書き換え制限 を超えるのに十分な数だけ追加されるまでは、ロールバック が発生する可能性があります。

トランザクションがまだブロックの一部でなければ、 が未承認プールを検索します。 承認済みでもプール内でもないトランザクションは、見つからないと報告されます。

トランザクションが未承認プールで待機している場合にだけ、スニペットは を呼び出し、承認されるかポーリング期間が終わるまで確認します。

拒否または期限切れだけが失敗を意味します

トランザクションが決して承認されないと知る方法は 2 つだけです。アナウンス時に拒否されたか、deadline が過ぎた場合です。

あるノードの未承認プールからトランザクションが消えたからといって、失敗したとは限りません。 各ノードは独自のプールを管理しており、別のピアがまだ保持して最終的に承認する可能性があります。 例えば、ノードが空のプールで再起動したり、プール容量を管理するときに古いトランザクションを削除したりすることがあります。

アナウンスの拒否はすぐ返されるため、最終的な判定は deadline です。 ネットワーク時刻 が deadline を過ぎると、トランザクションをブロックに含めることはできなくなり、代わりのトランザクションをアナウンスしても安全です。

トランザクションの構築 で選択した deadline と、/time-sync/network-time GET が返すネットワーク時刻を比較してください。

出力⚓︎

以下の出力は、新しくアナウンスしたトランザクションを監視した場合の実行例です。

Using node http://libertalia.nemtest.net:7890
Monitoring transaction: AE0B2142DFB75C9C126442EF612944E926BCE63FA34B353CDE409E2E87703C0B

Waiting for transaction confirmation
  Attempt 1: pending
  Attempt 2: pending
  Attempt 3: pending
  Attempt 4: pending
  Attempt 5: pending
  Attempt 6: confirmed in block 652601

Transaction confirmed!

出力の要点は次のとおりです。

  • トランザクションハッシュ(2 行目): 監視するトランザクションのハッシュで、ネットワーク上でそのトランザクションを一意に識別します。
  • ポーリング開始(4 行目): トランザクションがまだブロックチェーンに存在せず、未承認プールで待機していることが見つかったため、ポーリングを開始します。
  • ポーリングの試行(5~9 行目): トランザクションがブロックに含まれるのを待つ間、各試行で pending と表示します。
  • 承認(10 行目): 最終的にポーリングでブロック 652601 に含まれたことが報告されます。
  • 最終結果(12 行目): トランザクションが承認され、監視が終了します。

試行回数と時間は、ネットワーク状態とブロック生成速度によって変わります。

ネットワーク側からトランザクションを確認するには、NEM テストネットエクスプローラー にアクセスしてトランザクションハッシュを検索してください。

まとめ⚓︎

このチュートリアルでは、次の方法を説明しました。

手順 関連ドキュメント
承認を確認する /transaction/get GET
未承認プールを確認する /account/unconfirmedTransactions GET
承認を待つ /transaction/get GET
トランザクションが決して承認されない時点を検出する /time-sync/network-time GET

次のステップ⚓︎

本番アプリケーションでは、次の改善を検討してください。

  • 書き換え制限を過ぎるまで待つ。 後続ブロックが十分に追加されるまで、承認済みトランザクションもロールバックされる可能性があります。 実用上のしきい値については 書き換え制限 を参照してください。
  • 複数ノードを照会する。 複数の ノード でステータスを確認し、信頼性を高めて単一ノードの問題からシステムを保護します。
  • WebSocket を使う: ポーリングを WebSocket サブスクリプションに置き換え、API を繰り返し呼び出さずにリアルタイムで更新します。 トランザクションフローをリッスンする WebSocket チュートリアルを参照してください。