WebSocketを使用した着信ペイメントの監視

このチュートリアルでは、WebSocket rippled APIを使用して、着信ペイメントを監視する方法を説明します。すべてのXRP Ledgerトランザクションは公開されているため、誰もが任意のアドレスへの着信ペイメントを監視できます。

WebSocketは、クライアントとサーバーが1つの接続を確立し、その接続を経由して両方向にメッセージを送信するモデルに従います。この接続は、明示的に閉じる(または接続に障害が発生する)まで続きます。これは、要求ごとにクライアントが新しい接続を開いて閉じるHTTPベースのAPIモデル(JSON-RPCやRESTful APIなど)とは対照的です¹

ヒント: このページの例はJavaScriptを使用しているため、Webブラウザーでネイティブに実行できます。JavaScriptで開発している場合は、JavaScript向けRippleAPIライブラリも利用すると、一部の作業を簡素化できます。このチュートリアルでは、RippleAPIを使用できないその他のプログラミング言語にステップを適合できるよう、RippleAPIを使用 しない でトランザクションを監視する方法を説明します。

前提条件

  • このページの例では、すべての主要な最新ブラウザーで使用できるJavaScriptおよびWebSocketプロトコルを使用しています。JavaScriptにある程度習熟し、WebSocketクライアントを使用する他のプログラミング言語の専門知識があれば、選択する言語に手順を適合させながら進めていくことができます。
  • 安定したインターネット接続とrippledサーバーへアクセスが必要です。埋め込まれている例では、Rippleの公開サーバーのプールに接続します。独自のrippledサーバーを運用する場合は、ローカルでそのサーバーに接続することもできます。
  • 丸め方によるエラーを発生させることなくXRPの価値を適切に処理するには、64ビット符号なし整数で計算できる数値タイプを使用できる必要があります。このチュートリアルの例では、big.js を使用しています。発行済み通貨を使用する場合は、さらに高い精度が求められます。詳細は、通貨の精度を参照してください。

1. XRP Ledgerへの接続

着信ペイメントを監視する最初のステップとして、XRP Ledger、つまりrippledサーバーに接続します。

以下のJavaScriptコードでは、Rippleの公開サーバーのクラスターの1つに接続します。その後、コンソールにメッセージを記録し、pingメソッドを使用して要求を送信します。次に、サーバー側からのメッセージを受信するときに、ハンドラーを設定してコンソールに再度メッセージを記録します。

const socket = new WebSocket('wss://s.altnet.rippletest.net:51233')
socket.addEventListener('open', (event) => {
  // This callback runs when the connection is open
  console.log("Connected!")
  const command = {
    "id": "on_open_ping_1",
    "command": "ping"
  }
  socket.send(JSON.stringify(command))
})
socket.addEventListener('message', (event) => {
  console.log('Got message from server:', event.data)
})
socket.addEventListener('close', (event) => {
  // Use this event to detect when you have become disconnected
  // and respond appropriately.
  console.log('Disconnected...')
})

上記の例では、Test Net上にあるRippleの公開APIサーバーの1つに対して、安全な接続(wss://)を開きます。代わりにデフォルトの構成を使用してローカルで運用しているrippledサーバーに接続するには、最初の行に以下を使用して、ローカルのポート6006安全ではない 接続(ws://)を開きます。

const socket = new WebSocket('ws://localhost:6006')

ヒント: デフォルトでは、ローカルrippledサーバーに接続することで、インターネット上の公開サーバーに接続する際に使用できるパブリックメソッド以外に、すべての管理メソッドと、server_infoなどの一部の応答に含まれる管理者専用データを利用できます。

例:

Connection status: Not connected
Console:
(Log is empty)

2. ハンドラーへの着信メッセージのディスパッチ

WebSocket接続では、複数のメッセージをどちらの方向にも送信することが可能で、要求と応答の間に厳密な1:1の相互関係がないため、各着信メッセージに対応する処理を識別する必要があります。この処理をコーディングする際の優れたモデルとして、「ディスパッチャー」関数の設定が挙げられます。この関数は着信メッセージを読み取り、各メッセージを正しいコードのパスに中継して処理します。メッセージを適切にディスパッチできるように、rippledサーバーでは、すべてのWebSocketメッセージでtypeフィールドを使用できます。

  • クライアント側からの要求への直接の応答となるメッセージの場合、typeは文字列のresponseです。この場合、サーバーは以下も提供します。

  • この応答に対する要求で指定されたidに一致するidフィールド(応答が順序どおりに到着しない可能性があるため、これは重要です)。

  • APIが要求の処理に成功したかどうかを示すstatusフィールド。文字列値successは、成功した応答を示します。文字列値errorは、エラーを示します。

    警告: トランザクションを送信する際、WebSocketメッセージの先頭にあるsuccessstatusは、必ずしもトランザクション自体が成功したことを意味しません。これは、サーバーによって要求が理解されたということのみを示します。トランザクションの実際の結果を確認するには、トランザクションの結果の確認を参照してください。

  • サブスクリプションからのフォローアップメッセージの場合、typeは、新しいトランザクション、レジャーまたは検証の通知など、フォローアップメッセージのタイプを示します。または継続しているpathfinding要求のフォローアップを示します。クライアントがこれらのメッセージを受信するのは、それらをサブスクライブしている場合のみです。

ヒント: JavaScript向けRippleAPIは、デフォルトでこのステップに対応しています。すべての非同期API要求はPromiseを使用して応答を提供します。また.on(event, callback)メソッドを使用して、ストリームをリッスンできます。

以下のJavaScriptコードでは、API要求を便利な非同期Promise に変換するヘルパー関数を定義し、他のタイプのメッセージをグローバルハンドラーにマップするインターフェイスを設定します。

const AWAITING = {}
const handleResponse = function(data) {
  if (!data.hasOwnProperty("id")) {
    console.error("Got response event without ID:", data)
    return
  }
  if (AWAITING.hasOwnProperty(data.id)) {
    AWAITING[data.id].resolve(data)
  } else {
    console.error("Response to un-awaited request w/ ID " + data.id)
  }
}

let autoid_n = 0
function api_request(options) {
  if (!options.hasOwnProperty("id")) {
    options.id = "autoid_" + (autoid_n++)
  }

  let resolveHolder;
  AWAITING[options.id] = new Promise((resolve, reject) => {
    // Save the resolve func to be called by the handleResponse function later
    resolveHolder = resolve
    try {
      // Use the socket opened in the previous example...
      socket.send(JSON.stringify(options))
    } catch(error) {
      reject(error)
    }
  })
  AWAITING[options.id].resolve = resolveHolder;
  return AWAITING[options.id]
}

const WS_HANDLERS = {
  "response": handleResponse
  // Fill this out with your handlers in the following format:
  // "type": function(event) { /* handle event of this type */ }
}
socket.addEventListener('message', (event) => {
  const parsed_data = JSON.parse(event.data)
  if (WS_HANDLERS.hasOwnProperty(parsed_data.type)) {
    // Call the mapped handler
    WS_HANDLERS[parsed_data.type](parsed_data)
  } else {
    console.log("Unhandled message from server", event)
  }
})

// Demonstrate api_request functionality
async function pingpong() {
  console.log("Ping...")
  const response = await api_request({command: "ping"})
  console.log("Pong!", response)
}
pingpong()
Responses
(Log is empty)

3. アカウントのサブスクライブ

トランザクションがアカウントに影響を及ぼすたびに即座に通知を取得するには、subscribeメソッドを使用してアカウントをサブスクライブします。実際には、このアカウントはあなた自身のアカウントでなくてもかまいません。すべてのトランザクションは公開されているため、任意のアカウントで、または複数のアカウントでもサブスクライブできます。

1つ以上のアカウントをサブスクライブした場合、指定したアカウントのいずれかに何らかの影響を及ぼす各検証済みトランザクションについて、"type": "transaction"が含まれるメッセージがサーバーから送信されます。これを確認するには、トランザクションメッセージで"validated": trueを探します。

以下のコードサンプルは、Test Net Faucetの送信側アドレスをサブスクライブします。このコードサンプルでは、前のステップのディスパッチャーにハンドラーを追加することで、該当する各トランザクションのメッセージを記録します。

async function do_subscribe() {
  const sub_response = await api_request({
    command:"subscribe",
    accounts: ["rUCzEr6jrEyMpjhs4wSdQdz4g8Y382NxfM"]
  })
  if (sub_response.status === "success") {
    console.log("Successfully subscribed!")
  } else {
    console.error("Error subscribing: ", sub_response)
  }
}
do_subscribe()

const log_tx = function(tx) {
  console.log(tx.transaction.TransactionType + " transaction sent by " +
              tx.transaction.Account +
              "\n  Result: " + tx.meta.TransactionResult +
              " in ledger " + tx.ledger_index +
              "\n  Validated? " + tx.validated)
}
WS_HANDLERS["transaction"] = log_tx

以下の例では、別のウィンドウまたは別のデバイスでTransaction Senderを開くことと、サブスクライブしているアドレスへのトランザクションの送信を試みます。

Transactions
(Log is empty)

4. 着信ペイメントの読み取り

アカウントをサブスクライブすると、 アカウントへのすべてのトランザクションとアカウントからのすべてのトランザクション 、および アカウントに間接的に影響を及ぼすトランザクション に関するメッセージが表示されます。この例として、発行済み通貨の取引があります。アカウントが着信ペイメントを受け取った日時を認識することを目的とする場合、トランザクションストリームを絞り込んで、実際に支払われた額に基づいて支払いを処理する必要があります。以下の情報を探します。

  • validatedフィールドは、トランザクションの結果が最終的であることを示します。これは、accountsをサブスクライブする場合に常に当てはまりますが、accounts_proposedまたはtransactions_proposedストリーム サブスクライブしている場合は、サーバーは未確認のトランザクションに関して同様のメッセージを同じ接続で送信します。予防策として、validatedフィールドを常に確認することをお勧めします。

  • meta.TransactionResultフィールドは、トランザクションの結果です。結果がtesSUCCESSでない場合は、トランザクションは失敗したため、値を送信できません。

  • transaction.Account フィールドはトランザクションの送信元です。他の人が送信したトランザクションのみを探している場合は、このフィールドがあなたのアドレスと一致するトランザクションを無視できます(自身に対する複数通貨間の支払いが 可能である 点に注意してください)。

  • transaction.TransactionTypeフィールドはトランザクションのタイプです。アカウントに通貨を送金できる可能性があるトランザクションのタイプは以下のとおりです。

  • Paymentトランザクション はXRPまたは発行済み通貨を送金できます。受取人のアドレスを含んでいるtransaction.Destinationフィールドによってこれらを絞り込み、必ずmeta.delivered_amountを使用して実際に支払われた額を確認します。XRPの額は、文字列のフォーマットで記述されます

    警告: 代わりにtransaction.Amountフィールドを使用すると、Partial Paymentの悪用に対して脆弱になる可能性があります。不正使用者はこの悪用を行ってあなたをだまし、あなたが支払ったよりも多くの金額を交換または引き出すことができます。

  • CheckCashトランザクション では、アカウントは別のアカウントのCheckCreateトランザクションによって承認された金額を受け取ることができます。CheckCashトランザクションのメタデータを確認すると、アカウントが受け取った通貨の額を確認できます。

  • EscrowFinishトランザクション は、以前のEscrowCreateトランザクションによって作成されたEscrowを終了することでXRPを送金できます。EscrowFinishトランザクションのメタデータを確認すると、escrowからXRPを受け取ったアカウントと、その額を確認できます。

  • OfferCreateトランザクション はアカウントがXRP Ledgerの分散型取引所で以前発行したオファーを消費することで、XRPまたは発行済み通貨を送金できます。オファーを発行しないと、この方法で金額を受け取ることはできません。メタデータを確認して、アカウントが受け取った通貨(この情報がある場合)と、金額を確認します。

  • PaymentChannelClaimトランザクション では、Payment ChannelからXRPを送金できます。メタデータを確認して、トランザクションからXRPを受け取ったアカウント(この情報がある場合)を確認します。

  • PaymentChannelFundトランザクション は、閉鎖された(期限切れの)Payment Channelから送金元にXRPを返金することができます。

  • metaフィールドには、1つまたは複数の通貨の種類とその正確な金額、その送金先などを示すトランザクションメタデータが示されています。トランザクションメタデータを理解する方法の詳細は、トランザクションの結果の確認を参照してください。

以下のサンプルコードは、上に示したすべてのトランザクションのタイプのトランザクションメタデータを確認し、アカウントが受け取ったXRPの金額をレポートします。

function CountXRPDifference(affected_nodes, address) {
  // Helper to find an account in an AffectedNodes array and see how much
  // its balance changed, if at all. Fortunately, each account appears at most
  // once in the AffectedNodes array, so we can return as soon as we find it.

  // Note: this reports the net balance change. If the address is the sender,
  // the transaction cost is deducted and combined with XRP sent/received

  for (let i=0; i<affected_nodes.length; i++) {
    if ((affected_nodes[i].hasOwnProperty("ModifiedNode"))) {
      // modifies an existing ledger entry
      let ledger_entry = affected_nodes[i].ModifiedNode
      if (ledger_entry.LedgerEntryType === "AccountRoot" &&
          ledger_entry.FinalFields.Account === address) {
        if (!ledger_entry.PreviousFields.hasOwnProperty("Balance")) {
          console.log("XRP balance did not change.")
        }
        // Balance is in PreviousFields, so it changed. Time for
        // high-precision math!
        const old_balance = new Big(ledger_entry.PreviousFields.Balance)
        const new_balance = new Big(ledger_entry.FinalFields.Balance)
        const diff_in_drops = new_balance.minus(old_balance)
        const xrp_amount = diff_in_drops.div(1e6)
        if (xrp_amount.gte(0)) {
          console.log("Received " + xrp_amount.toString() + " XRP.")
          return
        } else {
          console.log("Spent " + xrp_amount.abs().toString() + " XRP.")
          return
        }
      }
    } else if ((affected_nodes[i].hasOwnProperty("CreatedNode"))) {
      // created a ledger entry. maybe the account just got funded?
      let ledger_entry = affected_nodes[i].CreatedNode
      if (ledger_entry.LedgerEntryType === "AccountRoot" &&
          ledger_entry.NewFields.Account === address) {
        const balance_drops = new Big(ledger_entry.NewFields.Balance)
        const xrp_amount = balance_drops.div(1e6)
        console.log("Received " + xrp_amount.toString() + " XRP (account funded).")
        return
      }
    } // accounts cannot be deleted at this time, so we ignore DeletedNode
  }

  console.log("Did not find address in affected nodes.")
  return
}

function CountXRPReceived(tx, address) {
  if (tx.meta.TransactionResult !== "tesSUCCESS") {
    console.log("Transaction failed.")
    return
  }
  if (tx.transaction.TransactionType === "Payment") {
    if (tx.transaction.Destination !== address) {
      console.log("Not the destination of this payment.")
      return
    }
    if (typeof tx.meta.delivered_amount === "string") {
      const amount_in_drops = new Big(tx.meta.delivered_amount)
      const xrp_amount = amount_in_drops.div(1e6)
      console.log("Received " + xrp_amount.toString() + " XRP.")
      return
    } else {
      console.log("Received non-XRP currency.")
      return
    }
  } else if (["PaymentChannelClaim", "PaymentChannelFund", "OfferCreate",
          "CheckCash", "EscrowFinish"].includes(
          tx.transaction.TransactionType)) {
    CountXRPDifference(tx.meta.AffectedNodes, address)
  } else {
    console.log("Not a currency-delivering transaction type (" +
                tx.transaction.TransactionType + ").")
  }
}
Transactions
(Log is empty)

次のステップ

その他のプログラミング言語

多くのプログラミング言語には、WebSocket接続を使用して、データの送受信を行うためのライブラリが用意されています。JavaScript以外の言語でrippledのWebSocket APIとの通信を効率良く始めるには、同様な機能を利用している以下の例を参考にしてください。

package main

// Connect to the XRPL Ledger using websocket and subscribe to an account
// translation from the JavaScript example to Go
// https://developers.ripple.com/monitor-incoming-payments-with-websocket.html
// This example uses the Gorilla websocket library to create a websocket client
// install: go get github.com/gorilla/websocket

import (
    "encoding/json"
    "flag"
    "log"
    "net/url"
    "os"
    "os/signal"
    "time"

    "github.com/gorilla/websocket"
)

// websocket address
var addr = flag.String("addr", "s.altnet.rippletest.net:51233", "http service address")

// Payload object
type message struct {
    Command  string   `json:"command"`
    Accounts []string `json:"accounts"`
}

func main() {
    flag.Parse()
    log.SetFlags(0)

    var m message

    // check for interrupts and cleanly close the connection
    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)

    u := url.URL{Scheme: "ws", Host: *addr, Path: "/"}
    log.Printf("connecting to %s", u.String())

    // make the connection
    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    if err != nil {
        log.Fatal("dial:", err)
    }
    // on exit close
    defer c.Close()

    done := make(chan struct{})

    // send a subscribe command and a target XRPL account
    m.Command = "subscribe"
    m.Accounts = append(m.Accounts, "rUCzEr6jrEyMpjhs4wSdQdz4g8Y382NxfM")

    // struct to JSON marshalling
    msg, _ := json.Marshal(m)
    // write to the websocket
    err = c.WriteMessage(websocket.TextMessage, []byte(string(msg)))
    if err != nil {
        log.Println("write:", err)
        return
    }

    // read from the websocket
    _, message, err := c.ReadMessage()
    if err != nil {
        log.Println("read:", err)
        return
    }
    // print the response from the XRP Ledger
    log.Printf("recv: %s", message)

    // handle interrupt
    for {
        select {
        case <-done:
            return
        case <-interrupt:
            log.Println("interrupt")

            // Cleanly close the connection by sending a close message and then
            // waiting (with timeout) for the server to close the connection.
            err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
            if err != nil {
                log.Println("write close:", err)
                return
            }
            select {
            case <-done:
            case <-time.After(time.Second):
            }
            return
        }
    }
}

ヒント: 目的のプログラミング言語の例がない場合があります。このページの最上部にある「GitHubで編集する」リンクをクリックして、作成したサンプルコードを提供してください。

脚注

1.実際には、HTTPベースのAPIを何度も呼び出す場合、クライアントとサーバーは複数の要求と応答を処理する際に同じ接続を再利用できます。この方法は、HTTP永続接続、またはキープアライブ と呼ばれます。開発の観点から見ると、基本となる接続が新しい場合でも、再利用される場合でも、HTTPベースのAPIを使用するコードは同じです。

関連項目