# WebSocketを使用した着信ペイメントの監視 このチュートリアルでは、[WebSocket `rippled` API](/ja/docs/references/http-websocket-apis)を使用して、着信[ペイメント](/ja/docs/concepts/payment-types)を監視する方法を説明します。すべてのXRP Ledgerトランザクションは公開されているため、誰もが任意のアドレスへの着信ペイメントを監視できます。 WebSocketは、クライアントとサーバが1つの接続を確立し、その接続を経由して両方向にメッセージを送信するモデルに従います。この接続は、明示的に閉じる(または接続に障害が発生する)まで続きます。これは、リクエストごとにクライアントが新しい接続を開いて閉じるHTTPベースのAPIモデル(JSON-RPCやRESTful APIなど)とは対照的です[¹](#footnote-1)。 このページの例はJavaScriptを使用しているため、Webブラウザーでネイティブに実行できます。JavaScriptで開発している場合は、[JavaScript向けxrpl.jsライブラリ](https://js.xrpl.org/)も利用すると、一部の作業を簡素化できます。このチュートリアルでは、xrpl.jsを使用できないその他のプログラミング言語にステップを適合できるよう、xrpl.jsを使用 *しない* でトランザクションを監視する方法を説明します。 ## 前提条件 - このページの例では、すべての主要な最新ブラウザーで使用できるJavaScriptおよびWebSocketプロトコルを使用しています。JavaScriptにある程度習熟し、WebSocketクライアントを使用する他のプログラミング言語の専門知識があれば、選択する言語に手順を適合させながら進めていくことができます。 - 安定したインターネット接続と`rippled`サーバへアクセスが必要です。埋め込まれている例では、Rippleの公開サーバのプールに接続します。[独自の`rippled`サーバを運用](/ja/docs/infrastructure/installation)する場合は、ローカルでそのサーバに接続することもできます。 - 丸め方によるエラーを発生させることなくXRPの価値を適切に処理するには、64ビット符号なし整数で計算できる数値タイプを使用できる必要があります。このチュートリアルの例では、[big.js](https://github.com/MikeMcl/big.js/)を使用しています。[トークン](/ja/docs/concepts/tokens)を使用する場合は、さらに高い精度が求められます。詳細は、[通貨の精度](/ja/docs/references/protocol/data-types/currency-formats#xrp%E3%81%AE%E7%B2%BE%E5%BA%A6)をご覧ください。 script script script // Helper stuff for this interactive tutorial specifically function writeToConsole(console_selector, message) { let write_msg = "
" + message + "
" $(console_selector).find(".placeholder").remove() $(console_selector).append(write_msg) // TODO: JSON pretty-printing, maybe w/ multiple input args? } ## 1. XRP Ledgerへの接続 着信ペイメントを監視する最初のステップとして、XRP Ledger、つまり`rippled`サーバに接続します。 以下のJavaScriptコードでは、Rippleの公開サーバのクラスターの1つに接続します。その後、コンソールにメッセージを記録し、[pingメソッド](/ja/docs/references/http-websocket-apis/public-api-methods/utility-methods/ping)を使用してリクエストを送信します。次に、サーバ側からのメッセージを受信するときに、ハンドラーを設定してコンソールに再度メッセージを記録します。 ```js 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](/resources/dev-tools/xrp-faucets)上にあるRippleの公開APIサーバの1つに対して、安全な接続(`wss://`)を開きます。代わりにデフォルトの構成を使用してローカルで運用している`rippled`サーバに接続するには、最初の行に以下を使用して、ローカルのポート**6006**で *安全ではない* 接続(`ws://`)を開きます。 ```js const socket = new WebSocket('ws://localhost:6006') ``` デフォルトでは、ローカル`rippled`サーバに接続することで、インターネット上の公開サーバに接続する際に使用できる[パブリックメソッド](/ja/docs/references/http-websocket-apis/public-api-methods)以外に、すべての[管理メソッド](/ja/docs/references/http-websocket-apis/admin-api-methods)と、[server_info](/ja/docs/references/http-websocket-apis/public-api-methods/server-info-methods/server_info)などの一部のレスポンスに含まれる管理者専用データを利用できます。 例: Connect Connect Connection status: Not connected h5 Console: div span (Log is empty) script let socket; $("#connect-socket-button").click((event) => { socket = new WebSocket('wss://s.altnet.rippletest.net:51233') socket.addEventListener('open', (event) => { // This callback runs when the connection is open writeToConsole("#monitor-console-connect", "Connected!") $("#connection-status").text("Connected") const command = { "id": "on_open_ping_1", "command": "ping" } socket.send(JSON.stringify(command)) complete_step("Connect") $("#connect-button").prop("disabled", "disabled") $("#enable_dispatcher").prop("disabled",false) }) socket.addEventListener('close', (event) => { $("#connection-status").text("Disconnected") $("#connect-button").prop("disabled", false) }) socket.addEventListener('message', (event) => { writeToConsole("#monitor-console-connect", "Got message from server: " + JSON.stringify(event.data)) }) }) ## 2. ハンドラーへの着信メッセージのディスパッチ WebSocket接続では、複数のメッセージをどちらの方向にも送信することが可能で、リクエストとレスポンスの間に厳密な1:1の相互関係がないため、各着信メッセージに対応する処理を識別する必要があります。この処理をコーディングする際の優れたモデルとして、「ディスパッチャー」関数の設定が挙げられます。この関数は着信メッセージを読み取り、各メッセージを正しいコードのパスに中継して処理します。メッセージを適切にディスパッチできるように、`rippled`サーバでは、すべてのWebSocketメッセージで`type`フィールドを使用できます。 - クライアント側からのリクエストへの直接のレスポンスとなるメッセージの場合、`type`は文字列の`response`です。この場合、サーバは以下も提供します。 - このレスポンスに対するリクエストで指定された`id`に一致する`id`フィールド(レスポンスが順序どおりに到着しない可能性があるため、これは重要です)。 - APIがリクエストの処理に成功したかどうかを示す`status`フィールド。文字列値`success`は、[成功したレスポンス](/ja/docs/references/http-websocket-apis/api-conventions/response-formatting)を示します。文字列値`error`は、[エラー](/ja/docs/references/http-websocket-apis/api-conventions/error-formatting)を示します。 トランザクションを送信する際、WebSocketメッセージの先頭にある`success`の`status`は、必ずしもトランザクション自体が成功したことを意味しません。これは、サーバによってリクエストが理解されたということのみを示します。トランザクションの実際の結果を確認するには、[トランザクションの結果の確認](/ja/docs/concepts/transactions/finality-of-results/look-up-transaction-results)をご覧ください。 - [サブスクリプション](/ja/docs/references/http-websocket-apis/public-api-methods/subscription-methods/subscribe)からのフォローアップメッセージの場合、`type`は、新しいトランザクション、レジャーまたは検証の通知など、フォローアップメッセージのタイプを示します。または継続している[pathfindingリクエスト](/ja/docs/references/http-websocket-apis/public-api-methods/path-and-order-book-methods/path_find)のフォローアップを示します。クライアントがこれらのメッセージを受信するのは、それらをサブスクライブしている場合のみです。 [JavaScript向けxrpl.js](https://js.xrpl.org/)は、デフォルトでこのステップに対応しています。すべての非同期APIリクエストはPromiseを使用してレスポンスを提供します。また[`.on(event, callback)`メソッド](https://js.xrpl.org/classes/Client.html#on)を使用して、ストリームをリッスンできます。 以下のJavaScriptコードでは、APIリクエストを便利な非同期[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises)に変換するヘルパー関数を定義し、他のタイプのメッセージをグローバルハンドラーにマップするインターフェイスを設定します。 ```js 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() ``` Dispatch Messages Enable Dispatcher Ping! h5 Responses div span (Log is empty) script const AWAITING = {} const handleResponse = function(data) { if (!data.hasOwnProperty("id")) { writeToConsole("#monitor-console-ping", "Got response event without ID:", data) return } if (AWAITING.hasOwnProperty(data.id)) { AWAITING[data.id].resolve(data) } else { writeToConsole("#monitor-console-ping", "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 resolveFunc; AWAITING[options.id] = new Promise((resolve, reject) => { // Save the resolve func to be called by the handleResponse function later resolveFunc = resolve try { // Use the socket opened in the previous example... socket.send(JSON.stringify(options)) } catch(error) { reject(error) } }) AWAITING[options.id].resolve = resolveFunc return AWAITING[options.id] } const WS_HANDLERS = { "response": handleResponse } $("#enable_dispatcher").click((clickEvent) => { 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 { writeToConsole("#monitor-console-ping", "Unhandled message from server: " + event) } }) complete_step("Dispatch Messages") $("#dispatch_ping").prop("disabled", false) $("#tx_subscribe").prop("disabled", false) }) async function pingpong() { const response = await api_request({command: "ping"}) writeToConsole("#monitor-console-ping", "Pong! " + JSON.stringify(response)) } $("#dispatch_ping").click((event) => { pingpong() }) ## 3. アカウントのサブスクライブ トランザクションがアカウントに影響を及ぼすたびに即座に通知を取得するには、[subscribeメソッド](/ja/docs/references/http-websocket-apis/public-api-methods/subscription-methods/subscribe)を使用してアカウントをサブスクライブします。実際には、このアカウントはあなた自身のアカウントでなくてもかまいません。すべてのトランザクションは公開されているため、任意のアカウントで、または複数のアカウントでもサブスクライブできます。 1つ以上のアカウントをサブスクライブした場合、指定したアカウントのいずれかに何らかの影響を及ぼす各検証済みトランザクションについて、`"type": "transaction"`が含まれるメッセージがサーバから送信されます。これを確認するには、トランザクションメッセージで`"validated": true`を探します。 以下のコードサンプルは、Test Net Faucetの送信側アドレスをサブスクライブします。このコードサンプルでは、前のステップのディスパッチャーにハンドラーを追加することで、該当する各トランザクションのメッセージを記録します。 ```js 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](/resources/dev-tools/tx-sender)を開くことと、サブスクライブしているアドレスへのトランザクションの送信を試みます。 Subscribe Test Net Address: Subscribe h5 Transactions div span (Log is empty) script async function do_subscribe() { const sub_address = $("#subscribe_address").val() const sub_response = await api_request({ command:"subscribe", accounts: [sub_address] }) if (sub_response.status === "success") { writeToConsole("#monitor-console-subscribe", "Successfully subscribed!") } else { writeToConsole("#monitor-console-subscribe", "Error subscribing: " + JSON.stringify(sub_response)) } } $("#tx_subscribe").click((event) => { do_subscribe() complete_step("Subscribe") $("#tx_read").prop("disabled", false) }) const log_tx = function(tx) { writeToConsole("#monitor-console-subscribe", tx.transaction.TransactionType + " transaction sent by " + tx.transaction.Account + "
  Result: " + tx.meta.TransactionResult + " in ledger " + tx.ledger_index + "
  Validated? " + tx.validated) } WS_HANDLERS["transaction"] = log_tx ## 4. 着信ペイメントの読み取り アカウントをサブスクライブすると、 *アカウントへのすべてのトランザクションとアカウントからのすべてのトランザクション* 、および *アカウントに間接的に影響を及ぼすトランザクション* に関するメッセージが表示されます。この例として、[トークン](/ja/docs/concepts/tokens)の取引があります。アカウントが着信ペイメントを受け取った日時を認識することを目的とする場合、トランザクションストリームを絞り込んで、実際に支払われた額に基づいて支払いを処理する必要があります。以下の情報を探します。 - **`validated`フィールド**は、トランザクションの結果が[最終的である](/ja/docs/concepts/transactions/finality-of-results)ことを示します。これは、`accounts`をサブスクライブする場合に常に当てはまりますが、`accounts_proposed`または`transactions_proposed`ストリーム *も* サブスクライブしている場合は、サーバは未確認のトランザクションに関して同様のメッセージを同じ接続で送信します。予防策として、`validated`フィールドを常に確認することをお勧めします。 - **`meta.TransactionResult`フィールド**は、[トランザクションの結果](/ja/docs/references/protocol/transactions/transaction-results)です。結果が`tesSUCCESS`でない場合は、トランザクションは失敗したため、値を送信できません。 - **`transaction.Account`** フィールドはトランザクションの送信元です。他の人が送信したトランザクションのみを探している場合は、このフィールドがあなたのアドレスと一致するトランザクションを無視できます(自身に対するクロスカレンシー支払いが *可能である* 点に注意してください)。 - **`transaction.TransactionType`フィールド**はトランザクションのタイプです。アカウントに通貨を送金できる可能性があるトランザクションのタイプは以下のとおりです。 - **[Paymentトランザクション](/ja/docs/references/protocol/transactions/types/payment)** はXRPまたは[トークン](/ja/docs/concepts/tokens)を送金できます。受取人のアドレスを含んでいる`transaction.Destination`フィールドによってこれらを絞り込み、必ず`meta.delivered_amount`を使用して実際に支払われた額を確認します。XRPの額は、[文字列のフォーマットで記述されます](/ja/docs/references/protocol/data-types/basic-data-types#%E9%80%9A%E8%B2%A8%E9%A1%8D%E3%81%AE%E6%8C%87%E5%AE%9A)。 代わりに`transaction.Amount`フィールドを使用すると、[Partial Paymentの悪用](/ja/docs/concepts/payment-types/partial-payments#partial-payment%E3%81%AE%E6%82%AA%E7%94%A8)に対して脆弱になる可能性があります。不正使用者はこの悪用を行ってあなたをだまし、あなたが支払ったよりも多くの金額を交換または引き出すことができます。 - **[CheckCashトランザクション](/ja/docs/references/protocol/transactions/types/checkcash)**では、アカウントは別のアカウントの[CheckCreateトランザクション](/ja/docs/references/protocol/transactions/types/checkcreate)によって承認された金額を受け取ることができます。**CheckCashトランザクション**のメタデータを確認すると、アカウントが受け取った通貨の額を確認できます。 - **[EscrowFinishトランザクション](/ja/docs/references/protocol/transactions/types/escrowfinish)** は、以前の[EscrowCreateトランザクション](/ja/docs/references/protocol/transactions/types/escrowcreate)によって作成された[Escrow](/ja/docs/concepts/payment-types/escrow)を終了することでXRPを送金できます。**EscrowFinishトランザクション**のメタデータを確認すると、escrowからXRPを受け取ったアカウントと、その額を確認できます。 - **[OfferCreateトランザクション](/ja/docs/references/protocol/transactions/types/offercreate)** はアカウントがXRP Ledgerの[分散型取引所](/ja/docs/concepts/tokens/decentralized-exchange)で以前発行したオファーを消費することで、XRPまたはトークンを送金できます。オファーを発行しないと、この方法で金額を受け取ることはできません。メタデータを確認して、アカウントが受け取った通貨(この情報がある場合)と、金額を確認します。 - **[PaymentChannelClaimトランザクション](/ja/docs/references/protocol/transactions/types/paymentchannelclaim)** では、[Payment Channel](/ja/docs/concepts/payment-types/payment-channels)からXRPを送金できます。メタデータを確認して、トランザクションからXRPを受け取ったアカウント(この情報がある場合)を確認します。 - **[PaymentChannelFundトランザクション](/ja/docs/references/protocol/transactions/types/paymentchannelfund)** は、閉鎖された(期限切れの)Payment Channelから送金元にXRPを返金することができます。 - **`meta`フィールド**には、1つまたは複数の通貨の種類とその正確な金額、その送金先などを示す[トランザクションメタデータ](/ja/docs/references/protocol/transactions/metadata)が示されています。トランザクションメタデータを理解する方法の詳細は、[トランザクションの結果の確認](/ja/docs/concepts/transactions/finality-of-results/look-up-transaction-results)をご覧ください。 以下のサンプルコードは、上に示したすべてのトランザクションのタイプのトランザクションメタデータを確認し、アカウントが受け取った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 { // Wrap the existing "transaction" handler to do the old thing and also // do the CountXRPReceived thing const sub_address = $("#subscribe_address").val() const old_handler = WS_HANDLERS["transaction"] const new_handler = function(data) { old_handler(data) CountXRPReceived(data, sub_address) } WS_HANDLERS["transaction"] = new_handler // Disable the button so you can't stack up multiple levels of the new handler $("#tx_read").prop("disabled", "disabled") complete_step("Read Payments") }) ## 次のステップ - [トランザクションの結果の確認](/ja/docs/concepts/transactions/finality-of-results/look-up-transaction-results)で、トランザクションの実行内容を確認し、適切に対応するソフトウェアを構築します。 - あなた自身のアドレスから[XRPの送金](/ja/docs/tutorials/how-tos/send-xrp)を試します。 - [Escrow](/ja/docs/concepts/payment-types/escrow)、[Checks](/ja/docs/concepts/payment-types/checks)または[Payment Channel](/ja/docs/concepts/payment-types/payment-channels)のような高度なタイプのトランザクションの監視と着信通知へのレスポンスを試します。 ## その他のプログラミング言語 多くのプログラミング言語には、WebSocket接続を使用して、データの送受信を行うためのライブラリが用意されています。JavaScript以外の言語で`rippled`のWebSocket APIとの通信を効率良く始めるには、同様な機能を利用している以下の例を参考にしてください。 Go package main // Connect to the XRPL Ledger using websocket and subscribe to an account // translation from the JavaScript example to Go // https://xrpl.org/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 { panic(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 { panic(err) } // read from the websocket _, message, err := c.ReadMessage() if err != nil { panic(err) } // 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 { panic(err) } select { case <-done: case <-time.After(time.Second): } return } } } 目的のプログラミング言語の例がない場合があります。このページの最上部にある「GitHubで編集する」リンクをクリックして、作成したサンプルコードを提供してください。 ## 脚注 [1.](#from-footnote-1)実際には、HTTPベースのAPIを何度も呼び出す場合、クライアントとサーバは複数のリクエストとレスポンスを処理する際に同じ接続を再利用できます。この方法は、[HTTP永続接続、またはキープアライブ](https://en.wikipedia.org/wiki/HTTP_persistent_connection)と呼ばれます。開発の観点から見ると、基本となる接続が新しい場合でも、再利用される場合でも、HTTPベースのAPIを使用するコードは同じです。 ## 関連項目 - **コンセプト:** - [トランザクション](/ja/docs/concepts/transactions) - [結果のファイナリティー](/ja/docs/concepts/transactions/finality-of-results) - トランザクションの成功また失敗が最終的なものとなるタイミングを判断する方法(簡単な説明: トランザクションが検証済みレジャーにある場合は、その結果とメタデータは最終的なものです)。 - **チュートリアル:** - [信頼できるトランザクションの送信](/ja/docs/concepts/transactions/reliable-transaction-submission) - [トランザクションの結果の確認](/ja/docs/concepts/transactions/finality-of-results/look-up-transaction-results) - **リファレンス:** - [トランザクションのタイプ](/ja/docs/references/protocol/transactions/types) - [トランザクションのメタデータ](/ja/docs/references/protocol/transactions/metadata) - メタデータフォーマットとメタデータに表示されるフィールドの概要 - [トランザクションの結果](/ja/docs/references/protocol/transactions/transaction-results) - トランザクションのすべての結果コードを掲載した表一覧