コンテンツへスキップ
最終更新:

XRPL Go Code Sample Conventions

Code samples come in two flavors with very different conventions. Identify which you're writing first.

FlavorFolder patternAudiencePriority
Tutorial<verb>-<thing>/main.go (e.g., create-loan-broker/main.go)A dev reading & learning the protocolClarity over speed
Setup<topic>-setup/main.go (e.g., lending-setup/main.go)A dev who never opens this file — runs to prep network data (accounts, tokens, etc.) for all tutorials in the subject folderSpeed over clarity

If a file isn't clearly one or the other, prompt the user for clarity.

Style

Formatting

  • gofmt-formatted (tabs, standard layout)

Naming

  • Folder/binary names: kebab-case (e.g., create-loan-broker/)
  • Variables: camelCase with acronyms uppercased — loanBrokerWallet, mptID, vaultID, loanBrokerID, credIssuerWallet
  • Transaction struct fields: native XRPL PascalCase (Account, VaultID, ManagementFeeRate) — matches both Go's exported-field rule and the XRPL wire format. Common fields (Account, Sequence, Fee, TicketSequence) go in the embedded BaseTx substruct.
  • Setup JSON keys: camelCase (loanBroker, credentialIssuer, mptID, vaultID, loanBrokerID)

Structure

Folder layout

Each code sample lives at _code-samples/<topic>/go/. Every command is its own kebab-case subdir containing one main.go:

_code-samples/<topic>/go/
├── README.md
├── go.mod
├── go.sum                       # Auto-generated by `go mod tidy`; gitignored
├── <topic>-setup/
│   └── main.go                  # Optional — runs once to prep network state
├── <topic>-setup.json           # Auto-generated by the setup script; gitignored
└── <verb>-<thing>/
    └── main.go                  # Tutorial commands (one per user action)

Run any command with go run ./<verb>-<thing> from the language root directory.

README

README.md is the entry point for a reader running the samples.

  1. Title: # <Topic> Examples (Go)
  2. One-sentence description listing what the directory demonstrates
  3. ## Setup section with the note "All commands should be run from this go/ directory." and a go mod tidy fenced block
  4. One ## section per tutorial command, in the order a reader should run them:
  • Heading describes the action (e.g., ## Create a Loan Broker), not the folder name
  • Fenced sh block with go run ./<verb>-<thing>
  • One-sentence summary of what the command will output
  • Fenced sh block showing actual expected console output (real addresses, tx IDs, JSON dumps — captured from a successful sample code run)
  1. --- separator between tutorial sections

The expected-output blocks document the golden path. Update them when a command's output format changes.

go.mod

One go.mod per sample at the language root. Pin the xrpl-go version:

module github.com/XRPLF

go 1.24.3

require github.com/Peersyst/xrpl-go v<latest-stable>

go mod tidy populates the indirect dependency block at the bottom — that block is auto-managed and shouldn't be hand-edited.

Pointer helper

Any main.go that sets optional pointer fields includes this helper near the top of the file:

// ptr is a helper that returns a pointer to the given value,
// used for setting optional transaction fields in Go.
func ptr[T any](v T) *T { return &v }

Tutorial files

WebSocket clientgithub.com/Peersyst/xrpl-go/xrpl/websocket. Always wrap with defer client.Disconnect() right after NewClient so the connection closes on any exit path.

Structure

  1. Multi-line // IMPORTANT: header explaining what the command demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault")
  2. package main + imports
  3. Connect to the network:
// Connect to the network ----------------------
client := websocket.NewClient(
	websocket.NewClientConfig().
		WithHost("wss://s.devnet.rippletest.net:51233"),
)
defer client.Disconnect()

if err := client.Connect(); err != nil {
	panic(err)
}
  1. (Optional) If the tutorial is using setup data:
// Check for setup data; run lending-setup if missing
if _, err := os.Stat("lending-setup.json"); os.IsNotExist(err) {
	fmt.Printf("\n=== Lending tutorial data doesn't exist. Running setup script... ===\n\n")
	cmd := exec.Command("go", "run", "./lending-setup")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		panic(err)
	}
}

// Load preconfigured accounts and VaultID
data, err := os.ReadFile("lending-setup.json")
if err != nil {
	panic(err)
}
var setup map[string]any
if err := json.Unmarshal(data, &setup); err != nil {
	panic(err)
}

// You can replace these values with your own
loanBrokerWallet, err := wallet.FromSecret(setup["loanBroker"].(map[string]any)["seed"].(string))
if err != nil {
	panic(err)
}
vaultID := setup["vaultID"].(string)
  1. (Optional) If the tutorial funds its own wallets instead of loading them from setup data, add WithFaucetProvider to the client config in step 3 and fund wallets after client.Connect():
// In step 3, extend the client config chain:
client := websocket.NewClient(
	websocket.NewClientConfig().
		WithHost("wss://s.devnet.rippletest.net:51233").
		WithFaucetProvider(faucet.NewDevnetFaucetProvider()),
)

// After client.Connect():
testWallet, err := wallet.New(crypto.ED25519())
if err != nil {
	panic(err)
}
if err := client.FundWallet(&testWallet); err != nil {
	panic(err)
}
  1. Tutorial code steps.

Tutorial code step guide

  • Before each major step, add a comment and print a section banner.
  • Build transactions as model structs, call .Flatten(), and print before submitting:
    // Prepare LoanBrokerSet transaction ----------------------
    fmt.Printf("\n=== Preparing LoanBrokerSet transaction ===\n\n")
    mgmtFeeRate := types.InterestRate(1000)
    loanBrokerSetTx := transaction.LoanBrokerSet{
    	BaseTx: transaction.BaseTx{
    		Account: loanBrokerWallet.ClassicAddress,
    	},
    	VaultID:           vaultID,
    	ManagementFeeRate: &mgmtFeeRate,
    }
    
    // Flatten() converts the struct to a map and adds the TransactionType field
    flatLoanBrokerSetTx := loanBrokerSetTx.Flatten()
    loanBrokerSetTxJSON, _ := json.MarshalIndent(flatLoanBrokerSetTx, "", "  ")
    fmt.Printf("%s\n", string(loanBrokerSetTxJSON))
  • Submit with SubmitTxAndWait and handle results by checking for tesSUCCESS and exiting on failure:
    // Submit, sign, and wait for validation ----------------------
    fmt.Printf("\n=== Submitting LoanBrokerSet transaction ===\n\n")
    loanBrokerSetResponse, err := client.SubmitTxAndWait(flatLoanBrokerSetTx, &wstypes.SubmitOptions{
    	Autofill: true,
    	Wallet:   &loanBrokerWallet,
    })
    if err != nil {
    	panic(err)
    }
    
    if loanBrokerSetResponse.Meta.TransactionResult != "tesSUCCESS" {
    	fmt.Printf("Error: Unable to create loan broker: %s\n", loanBrokerSetResponse.Meta.TransactionResult)
    	os.Exit(1)
    }
    fmt.Printf("Loan broker created successfully!\n")
  • Extract metadata relevant to the tutorial:
    // Extract loan broker information from the transaction result
    fmt.Printf("\n=== Loan Broker Information ===\n\n")
    for _, node := range loanBrokerSetResponse.Meta.AffectedNodes {
    	if node.CreatedNode != nil && node.CreatedNode.LedgerEntryType == "LoanBroker" {
    		fmt.Printf("LoanBroker ID: %s\n", node.CreatedNode.LedgerIndex)
    		fmt.Printf("LoanBroker Pseudo-Account Address: %s\n", node.CreatedNode.NewFields["Account"])
    		break
    	}
    }

Setup files

RPC clientgithub.com/Peersyst/xrpl-go/xrpl/rpc with a faucet provider. Setup uses RPC, not WebSocket: xrpl-go's WS client is built on gorilla/websocket, which doesn't allow concurrent writes on a single connection.

cfg, err := rpc.NewClientConfig(
	"https://s.devnet.rippletest.net:51234",
	rpc.WithFaucetProvider(faucet.NewDevnetFaucetProvider()),
)
if err != nil {
	panic(err)
}
client := rpc.NewClient(cfg)

submitOpts := func(w *wallet.Wallet) *rpctypes.SubmitOptions {
	return &rpctypes.SubmitOptions{Autofill: true, Wallet: w}
}

RPC endpoints: Devnet (https://s.devnet.rippletest.net:51234) or Testnet (https://s.altnet.rippletest.net:51234).

Speed-first patterns when possible

  • Use goroutines + buffered channels for fan-out parallelism (not errgroup or sync.WaitGroup)
  • Each goroutine handles one independent task — often a single transaction, sometimes a multi-step pipeline wrapped in a helper closure
  • When fanning out parallel transactions from the same account, create tickets first via TicketCreate with TicketCount: N, then set Sequence: 0 and TicketSequence: ... on the BaseTx of each parallel tx
  • xrpl-go doesn't include a fund-and-wait helper, use this:
    // Create and fund wallets concurrently
    createAndFund := func(ch chan<- wallet.Wallet) {
    	w, err := wallet.New(crypto.ED25519())
    	if err != nil {
    		panic(err)
    	}
    	if err := client.FundWallet(&w); err != nil {
    		panic(err)
    	}
    	// Poll until account is validated on ledger
    	funded := false
    	for range 20 {
    		_, err := client.Request(&account.InfoRequest{
    			Account:     w.GetAddress(),
    			LedgerIndex: common.Validated,
    		})
    		if err == nil {
    			funded = true
    			break
    		}
    		time.Sleep(time.Second)
    	}
    	if !funded {
    		panic("Issue funding account: " + w.GetAddress().String())
    	}
    	ch <- w
    }

Setup code guide

  • Top comment: single line, // Setup script for <topic> tutorials above package main
  • Only output is a carriage-return progress indicator: fmt.Print("Setting up tutorial: N/D\r") between phases, where N is the step number and D is the total steps
  • No === Section === banners, no transaction dumps — the reader never sees this file's output beyond the progress counter
  • Section comments in code are short: // Section description (no dash visual)
  • Use panic(err) on every error path — setup is fail-fast, and a panic surfaces the failing line clearly. Don't silently continue or _ = err.

Output file

At the end, write all data the tutorials will need. Use an anonymous struct with json:"camelCase" tags so field order is preserved:

setupData := struct {
	Description  string `json:"description"`
	LoanBroker   any    `json:"loanBroker"`
	DomainID     string `json:"domainID"`
	MptID        string `json:"mptID"`
	VaultID      string `json:"vaultID"`
	LoanBrokerID string `json:"loanBrokerID"`
}{ ... }

jsonData, err := json.MarshalIndent(setupData, "", "  ")
if err != nil {
	panic(err)
}
if err := os.WriteFile("lending-setup.json", jsonData, 0644); err != nil {
	panic(err)
}