📺

XRPレジャーのトランザクション結果をプレビューする

2024/07/23に公開

XRP Ledger

XRP Ledgerは、分散型のパブリックブロックチェーンです。XRP Ledgerは、DEX機能やNFT機能をはじめとした様々なネイティブ機能を備えており、日々進化を続けています。

https://xrpl.org/ja/

XRPLのトランザクション

XRPLにおいてユーザ/アカウントが送信するトランザクションは次のようなJSON形式で表されます。

{
  "TransactionType": "Payment",
  "Account": "r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59",
  "Destination": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
  "Amount": "1000000",
  "Flags": 2147483648,
  "LastLedgerSequence": 8804620,
  "Fee": "12",
  "Sequence": 1
}

トランザクションのうち、AccountSetやNFTokenMintなどの一部のTransactionTypeの実行結果はある程度予測可能ですが、特にDEXやAMMなどを利用する一部のトランザクションの実行結果は予測が難しくなっています。

トランザクションのプレビュー

現在XLS-69dとしてSimulate RPCが提案されており、RPC送信時点のレジャーデータを利用しトランザクションの実行結果をプレビューすることが可能となります。

https://github.com/XRPLF/XRPL-Standards/pull/207

このRPCではトランザクションの成功/失敗だけでなく、実行によりどのようにレジャーデータが変更されたかを表すメタデータも取得することが可能です。

ただし、RPC実行時と実際のトランザクション送信時の時間差やその他の要因により、このRPCによるプレビュー内容は実際にトランザクションを送信する際の結果と異なる可能性があるため注意が必要です。

試してみる

本記事執筆時点でSimulate RPCは現在開発中であり、テストネット等でもまだ利用出来ませんが、ローカルでビルドし試してみることが可能です。

https://github.com/XRPLF/rippled/pull/5069

ビルド方法については割愛します。

Simulate RPCはリクエストパラメータにcommand: "simulate"を指定、そしてtx_jsonにトランザクションのJSONを指定します。

tx_jsonフィールドに指定するトランザクション情報ではFeeSequenceなどの自動入力可能なフィールドは省略することが可能です。

以下はSimulate RPCのリクエスト例(WebSocket)です。

{
  command: 'simulate',
  tx_json: {
    TransactionType: 'Payment',
    Account: "rQQQrUdN1cLdNmxH4dHfKgmX5P4kf3ZrM",
    Destination: "r9fVvKgMMPkBHQ3n28sifxi22zKphwkf8u"
    Amount: "1000000"
  },
}

AccountSetトランザクション

最もシンプルなAccountSetトランザクションをプレビューしてみます。

https://xrpl.org/ja/docs/references/protocol/transactions/types/accountset/

import { Client, ECDSA, Wallet } from 'xrpl'

const client = new Client('ws://localhost:6006')

const account = Wallet.fromSecret('snoPBrXtMeMyMHUVTgbuqAfg1SUTb', { algorithm: ECDSA.secp256k1 }) // genesis account

const main = async () => {
  await client.connect()

  const response = await client.request({
    command: 'simulate',
    tx_json: {
      TransactionType: 'AccountSet',
      Account: account.address,
    },
  })

  const { tx_json, meta, engine_result } = response.result

  console.log(tx_json)
  console.log(JSON.stringify(meta, null, 2))
}

main()

処理されたtx_json(幾つかの不足フィールドは自動入力されます。)

tx_json
{
  Account: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
  Fee: '10',
  Sequence: 1,
  SigningPubKey: '',
  TransactionType: 'AccountSet',
  TxnSignature: '',
  hash: '2209360D0FBB6D2370F6F576255032B15AAD5688584828C68EC081D12E545CE1'
}

処理されたトランザクションのメタデータ

meta
{
  "AffectedNodes": [
    {
      "ModifiedNode": {
        "FinalFields": {
          "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
          "Balance": "99999999999999990",
          "Flags": 0,
          "OwnerCount": 0,
          "Sequence": 2
        },
        "LedgerEntryType": "AccountRoot",
        "LedgerIndex": "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8",
        "PreviousFields": {
          "Balance": "100000000000000000",
          "Sequence": 1
        }
      }
    }
  ],
  "TransactionIndex": 0,
  "TransactionResult": "tesSUCCESS"
}

DEX取引を行うOfferCreateトランザクション

複雑な処理を行うトランザクションの1つであり、DEXを使ったトークンの売買を行えるOfferCreateトランザクションをプレビューしてみます。

https://xrpl.org/ja/docs/references/protocol/transactions/types/offercreate/

今回の例では取引板が1つのみのため計算は可能ですが、メインネットでは取引板には数多くの注文が存在するため、実際にどれほどが約定し、どのような結果になるかは予測が難しいです。
しかし、Simulate RPCを利用することで簡単にこれらの結果を予測することが可能になるのです。

import { Client, ECDSA, Wallet } from 'xrpl'

const client = new Client('ws://localhost:6006')

const account = Wallet.fromSecret('snoPBrXtMeMyMHUVTgbuqAfg1SUTb', { algorithm: ECDSA.secp256k1 }) // genesis account

const issuer = Wallet.fromSecret('sEdTpvPK4rbWSoQyfApQt4TkeJAWkeC')

const prepare = async () => {
  // issuerアカウントの有効化
  await client.submit(
    {
      TransactionType: 'Payment',
      Account: account.address,
      Destination: issuer.address,
      Amount: '100000000',
    },
    { wallet: account },
  )
  await client.request({ command: 'ledger_accept' })

  // issuerによるUSD/XRPの注文作成
  await client.submit(
    {
      TransactionType: 'OfferCreate',
      Account: issuer.address,
      TakerPays: '60000000',
      TakerGets: {
        currency: 'USD',
        issuer: issuer.address,
        value: '100',
      },
    },
    { wallet: issuer },
  )
  await client.request({ command: 'ledger_accept' })
}

const main = async () => {
  await client.connect()
  await prepare()

  const response = await client.request({
    command: 'simulate',
    tx_json: {
      TransactionType: 'OfferCreate',
      Account: account.address,
      TakerGets: '600000',
      TakerPays: {
        currency: 'USD',
        issuer: issuer.address,
        value: '1',
      },
    },
  })

  const { tx_json, meta, engine_result } = response.result

  console.log(tx_json)
  console.log(JSON.stringify(meta, null, 2))
}

main()
tx_json
{
  Account: 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh',
  Fee: '10',
  Sequence: 2,
  SigningPubKey: '',
  TakerGets: '600000',
  TakerPays: {
    currency: 'USD',
    issuer: 'rtyqWbUwB3aH25dkvM2m2x22nepD3zvb3',
    value: '1'
  },
  TransactionType: 'OfferCreate',
  TxnSignature: '',
  hash: '5598FFE6E73299212AE2DFB277174B2B8F9522B77655EEA1149AEFFD2C8F9AF3'
}
meta
{
  "AffectedNodes": [
    {
      "ModifiedNode": {
        "FinalFields": {
          "Account": "rtyqWbUwB3aH25dkvM2m2x22nepD3zvb3",
          "Balance": "100599988",
          "Flags": 0,
          "OwnerCount": 1,
          "Sequence": 4
        },
        "LedgerEntryType": "AccountRoot",
        "LedgerIndex": "1A3CCA1775ECD91A53B3B8AEB1FE9F3FED70C86BF2406C2FEB65E6E1AA55229F",
        "PreviousFields": {
          "Balance": "99999988"
        },
        "PreviousTxnID": "68EEB4DE72AFA91C91A91E0A1FCF7AED37DAC77CE9F7A7FEC8FEEEA879AD65C7",
        "PreviousTxnLgrSeq": 4
      }
    },
    {
      "ModifiedNode": {
        "FinalFields": {
          "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
          "Balance": "99999999899399978",
          "Flags": 0,
          "OwnerCount": 1,
          "Sequence": 3
        },
        "LedgerEntryType": "AccountRoot",
        "LedgerIndex": "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8",
        "PreviousFields": {
          "Balance": "99999999899999988",
          "OwnerCount": 0,
          "Sequence": 2
        },
        "PreviousTxnID": "C8E9E3CA95CCF6491644C59B2255731703AD97317884CB39071350F5D716C601",
        "PreviousTxnLgrSeq": 3
      }
    },
    {
      "ModifiedNode": {
        "FinalFields": {
          "Flags": 0,
          "Owner": "rtyqWbUwB3aH25dkvM2m2x22nepD3zvb3",
          "RootIndex": "848B8D3D67D7FD255708A2D18D039F8E12096061666B2CE28A4AB6B13794067D"
        },
        "LedgerEntryType": "DirectoryNode",
        "LedgerIndex": "848B8D3D67D7FD255708A2D18D039F8E12096061666B2CE28A4AB6B13794067D"
      }
    },
    {
      "ModifiedNode": {
        "FinalFields": {
          "Account": "rtyqWbUwB3aH25dkvM2m2x22nepD3zvb3",
          "BookDirectory": "E3AA7E0781A80DD7DEB69927BF83488DB977F387D83321CC5A1550F7DCA70000",
          "BookNode": "0",
          "Flags": 0,
          "OwnerNode": "0",
          "Sequence": 3,
          "TakerGets": {
            "currency": "USD",
            "issuer": "rtyqWbUwB3aH25dkvM2m2x22nepD3zvb3",
            "value": "99"
          },
          "TakerPays": "59400000"
        },
        "LedgerEntryType": "Offer",
        "LedgerIndex": "A976EA16B7A0F1B067957B2F161E6EC697E92B81782922AA980C62CCA9BC4A1D",
        "PreviousFields": {
          "TakerGets": {
            "currency": "USD",
            "issuer": "rtyqWbUwB3aH25dkvM2m2x22nepD3zvb3",
            "value": "100"
          },
          "TakerPays": "60000000"
        },
        "PreviousTxnID": "68EEB4DE72AFA91C91A91E0A1FCF7AED37DAC77CE9F7A7FEC8FEEEA879AD65C7",
        "PreviousTxnLgrSeq": 4
      }
    },
    {
      "CreatedNode": {
        "LedgerEntryType": "DirectoryNode",
        "LedgerIndex": "D8120FC732737A2CF2E9968FDF3797A43B457F2A81AA06D2653171A1EA635204",
        "NewFields": {
          "Owner": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
          "RootIndex": "D8120FC732737A2CF2E9968FDF3797A43B457F2A81AA06D2653171A1EA635204"
        }
      }
    },
    {
      "CreatedNode": {
        "LedgerEntryType": "RippleState",
        "LedgerIndex": "E63B0DCAFB5380FE24DC472A0FA156D66AF42D5CECA3DA2D21471FCA94F2D75B",
        "NewFields": {
          "Balance": {
            "currency": "USD",
            "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji",
            "value": "-1"
          },
          "Flags": 3276800,
          "HighLimit": {
            "currency": "USD",
            "issuer": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
            "value": "0"
          },
          "LowLimit": {
            "currency": "USD",
            "issuer": "rtyqWbUwB3aH25dkvM2m2x22nepD3zvb3",
            "value": "0"
          }
        }
      }
    }
  ],
  "TransactionIndex": 0,
  "TransactionResult": "tesSUCCESS"
}

AffectedNodesオブジェクトを見てみると、新規トラストラインが作成されたり、アカウントの残高が増減したりしていることが確認できます。

まとめ

Simulate RPCはトランザクションのプレビューを行うための非常に便利なRPCです。本記事で説明したDEX取引だけでなく、AMMプールへの入出金や開発中のBatchトランザクションなどでも利用することができ、複雑化する処理も簡単に予測することが可能です。

また、新機能開発時にも非常に有用であり、同一のレジャーデータを手動テスト実行毎に毎回確認する必要がなくなったり、特定条件下でのトランザクションの挙動を反復的に確認することが可能となります。

興味を持たれた方はXRP Ledger開発者のDiscordチャンネルへ是非お越しください!
日本語チャンネルもありますので、英語ができなくても大丈夫です!
https://xrpldevs.org

また、XRPL JapanのDiscordサーバもありますので、こちらもぜひご参加ください!
https://discord.gg/invite/xrpljapan

私のX/Twitterアカウントはこちら!

Discussion