Skip to content

XRP Transaction Flow (Current Implementation)

This document describes the as-built transaction flow for the XRP integration as of PR #635. It maps each step to the concrete Go files and interfaces that implement it.

For the common multi-wallet architecture shared across all chains, see docs/transaction-flow.md. For the future architecture proposal, see architecture-2026.md.


Overview

All network communication uses WebSocket directly to a rippled node. The former gRPC adapter (apps/xrpl-grpc-server) has been fully removed.

The flow spans three wallet processes communicating via files (transferred offline):

Watch Wallet (online)          Sign/Keygen Wallet (offline)       Watch Wallet (online)
        │                               │                                  │
  Step 1: Create                  Step 2: Sign                      Step 3: Submit
  unsigned tx                     (offline, no network)             signed tx
        │                               │                                  │
  ──► file ──────────────────────────► file ────────────────────────────► XRP network

Transport Layer

ConnectionTypePortUsed by
Public WebSocketWebSocket6006All read/submit operations (PublicXRP)
Admin WebSocketWebSocket6005ledger_accept standalone mode only (AdminXRP)

PublicXRP and AdminXRP are separate structs in internal/infrastructure/api/xrp/public/ and admin/ respectively. Both embed the corresponding low-level RPC client from pkg/chains/xrp/rpc/. Construction is via NewPublicXRPFromCoinType() and NewAdminXRPFromCoinType() in connection.go.


Step 1 — Watch Wallet: Create Unsigned Transaction

Entry point: internal/interface-adapters/cli/watch/api/xrp/Use case: internal/application/usecase/watch/xrp/create_transaction.go

Interfaces used

InterfaceFileMethod
apixrp.AccountInfoProviderports/api/xrp/account_info.goGetBalance()
apixrp.TransactionPreparerports/api/xrp/interface.goCreateRawTransaction()

Implementation path

createTransactionUseCase.Execute()

    ├─ accountInfo.GetBalance(senderAddr)
    │       └─ WSClient: account_info → reads XRP balance

    └─ txPreparer.CreateRawTransaction(sender, receiver, amount, instructions)
            └─ WSClient.PrepareTransaction()
                    │  1. xrprpc.AccountInfo() → WebSocket account_info
                    │     → fetches Sequence + LedgerCurrentIndex
                    │  2. Builds dtoxrp.TxInput:
                    │       TransactionType: "Payment"
                    │       Account:            sender address
                    │       Destination:        receiver address
                    │       Amount:             drops (amount × 1_000_000)
                    │       Fee:                "12" (minimum, overridable via Instructions)
                    │       Sequence:           from account_info
                    │       LastLedgerSequence: LedgerCurrentIndex + MaxLedgerVersionOffset
                    └─ Returns (*dtoxrp.TxInput, rawTxJSON string, error)

Output file format

The unsigned transaction is written to a JSON file via txFileRepo.WriteXRPJSONFile():

storage/xrp/tx/deposit_unsigned_<txID>_0.json

File content (XRPTransactionFile schema in ports/file/):

json
{
  "transactions": [
    {
      "uuid": "<uuid-v7>",
      "sender_account_type": "client",
      "sender_account": "r...",
      "unsigned_data": { "TransactionType": "Payment", "Account": "r...", ... },
      "required_signatures": 1,
      "signature_count": 0,
      "is_complete": false
    }
  ]
}

Step 2 — Sign/Keygen Wallet: Sign Transaction (Offline)

Entry point: internal/interface-adapters/cli/sign/ or keygen/Use case: internal/application/usecase/sign/xrp/sign_transaction.go (keygen uses the equivalent at internal/application/usecase/keygen/xrp/sign_transaction.go)

Interface used

InterfaceFileMethods
apixrp.TransactionSignerports/api/xrp/transaction_signer.goSignTransaction(), SignTransactionNative()

Implementation path

signTransactionUseCase.Sign()

    ├─ txFileRepo.ReadXRPJSONFile(filePath)
    │       └─ reads unsigned JSON file from disk

    ├─ txFile.Validate()
    │       └─ checks invariants (non-empty, valid fields)

    └─ for each tx entry:

            ├─ xrpAccountKeyRepo.GetSecret(senderAccountType, senderAccount)
            │       └─ reads seed/secret from local DB (never logged)

            └─ xrp.SignTransactionNative(ctx, &tx.UnsignedData, secret, isMultiSig, existingBlob)
                    └─ (*XRP).SignTransactionNative()  [xrpapi_tx.go]
                            └─ xrpsigner.NewPeersystSigner().SignTransactionNative()
                                    │  Uses Peersyst/xrpl-go library
                                    │  ZERO network calls — fully offline
                                    │  Single-sig:  wallet.Sign()
                                    │  Multi-sig:   wallet.Multisign() + blob accumulation
                                    └─ Returns (txHash string, txBlob string, error)

Multi-signature accumulation

For M-of-N signing, each signer calls SignTransactionNative with the previous signer's txBlob passed as existingSignedBlob. The library appends the new Signer entry to the existing Signers array rather than replacing it. Signing is fully serial and offline.

Output file format

The signed result is written back as JSON:

storage/xrp/tx/deposit_signed_<txID>_<signedCount>.json

The signed_blob field of each completed transaction entry is populated with the hex-encoded signed transaction blob ready for submission.


Step 3 — Watch Wallet: Submit and Confirm

Entry point: internal/interface-adapters/cli/watch/Use case: internal/application/usecase/watch/xrp/send_transaction.go

Interfaces used

InterfaceFileMethods
apixrp.TransactionSubmitterports/api/xrp/transaction_submitter.goSubmitTransaction(), GetTransaction()
apixrp.LedgerPollerports/api/xrp/transaction_submitter.goLedgerCurrent()
apixrp.LedgerAdvancer (optional)ports/api/xrp/transaction_submitter.goLedgerAccept()

Ledger Validation — Plan C Architecture

WaitValidation was removed from TransactionSubmitter. The use case manages the ledger polling loop itself (Plan C):

  • LedgerPollerPublicXRP.LedgerCurrent() — polls current ledger index (always available)
  • LedgerAdvancerAdminXRP.LedgerAccept() — advances ledger in standalone mode (optional, may be nil)

The local xrpSendTxClient interface in send_transaction.go composes both:

go
type xrpSendTxClient interface {
    apixrp.TransactionSubmitter
    apixrp.LedgerPoller
}

ledgerAdv apixrp.LedgerAdvancer is a separate optional field (nil in production without admin).

Implementation path

sendTransactionUseCase.Execute()

    ├─ txFileRepo.ReadFileSlice(filePath)
    │       └─ reads signed tx file lines: "uuid,txHash,txBlob"

    └─ for each entry (concurrent goroutines):

            ├─ xrper.SubmitTransaction(ctx, txBlob)
            │       └─ PublicXRP.SubmitTransaction()
            │               └─ xrprpc.Submit() → WebSocket submit
            │                  Returns SentTx{ResultCode, TxJSON{Hash, LastLedgerSequence}, ...}
            │                  Checks ResultCode contains "tesSUCCESS"

            ├─ use case: waitValidation(ctx, sentTx.TxJSON.LastLedgerSequence)
            │       │  Polls xrper.LedgerCurrent() up to 30 times × 1s
            │       │  If ledgerAdv != nil: calls LedgerAccept() between polls (standalone)
            │       └─ Returns when currentLedger >= LastLedgerSequence

            └─ xrper.GetTransaction(ctx, txHash, earliestLedgerVersion)
                    └─ PublicXRP.GetTransaction()
                            └─ xrprpc.GetTx() → WebSocket tx
                               Confirms Meta.TransactionResult and Validated flag

Port Interface Summary

apixrp.XRPPublicClient (focused — used everywhere except DI admin setup)
    embeds:
        XRPPublicer         → public queries (AccountChannels, AccountInfo, ServerInfo)
        TransactionSubmitter→ SubmitTransaction, GetTransaction
        TransactionSigner   → SignTransaction, SignTransactionNative
        LedgerPoller        → LedgerCurrent
        AccountInfoProvider → GetAccountInfo, GetBalance
        TransactionPreparer → CreateRawTransaction
        CoinTypeProvider, Closer, ChainConfigProvider

apixrp.XRPAdminClient (focused — used only for keygen + standalone ledger)
    embeds:
        XRPAdminer          → admin keygen ops (ValidationCreate, WalletPropose)
        LedgerAdvancer      → LedgerAccept
        CoinTypeProvider, Closer, ChainConfigProvider

apixrp.XRPer (deprecated — kept for backward compatibility only)
    embeds: XRPAdminClient + XRPPublicClient (effectively the union)

Use cases depend only on the focused interfaces:

Use caseInterface dependency
createTransactionUseCaseAccountInfoProvider, TransactionPreparer
signTransactionUseCase (sign/keygen)TransactionSigner
sendTransactionUseCaseTransactionSubmitter + LedgerPoller (via local xrpSendTxClient), optional LedgerAdvancer
monitorTransactionUseCaseAccountInfoProvider
generateKeyUseCase (keygen)XRPAdminClient (needs WalletPropose)

Removed Functionality (gRPC era)

The following operations were implemented via apps/xrpl-grpc-server and have been removed as of PR #632. They have no WebSocket-based replacement in the current codebase:

OperationFormer fileStatus
GenerateAddress / GenerateXAddressxrpapi_address.goRemoved
SetRegularKey transactionxrpapi_tx_account.goRemoved (DI panics)
SignerListSet transactionxrpapi_tx_account.goRemoved (DI panics)
Escrow transactionsxrpapi_tx_escrow.goRemoved
NFToken transactionsxrpapi_tx_nftoken.goRemoved
Payment channel transactionsxrpapi_tx_payment_channel.goRemoved
CombineTransaction (multisig aggregation via gRPC)xrpapi_tx.goRemoved

Multi-signature signing is still supported via SignTransactionNative (signature accumulation in the sign wallet), but the gRPC-based CombineTransaction aggregation path is gone.


Key Files Reference

FileRole
internal/infrastructure/api/xrp/public/client.goPublicXRP struct (implements XRPPublicClient)
internal/infrastructure/api/xrp/public/transaction.goPrepareTransaction, SubmitTransaction, GetTransaction, LedgerCurrent
internal/infrastructure/api/xrp/public/sign.goSignTransaction, SignTransactionNative
internal/infrastructure/api/xrp/public/account.goGetAccountInfo, GetBalance
internal/infrastructure/api/xrp/admin/client.goAdminXRP struct (implements XRPAdminClient)
internal/infrastructure/api/xrp/admin/keygen.goWalletPropose, ValidationCreate
internal/infrastructure/api/xrp/connection.goFactory functions NewPublicXRPFromCoinType, NewAdminXRPFromCoinType
internal/infrastructure/api/xrp/signer/PeersystSigner — offline signing via Peersyst/xrpl-go
internal/application/ports/api/xrp/interface.goAll interfaces: XRPPublicClient, XRPAdminClient, TransactionSigner, TransactionSubmitter, LedgerPoller, LedgerAdvancer, and others
internal/application/usecase/watch/xrp/create_transaction.goStep 1: build unsigned tx
internal/application/usecase/sign/xrp/sign_transaction.goStep 2: sign (sign wallet)
internal/application/usecase/keygen/xrp/sign_transaction.goStep 2: sign (keygen wallet)
internal/application/usecase/watch/xrp/send_transaction.goStep 3: submit + confirm (Plan C ledger polling)