Code samples come in two flavors with very different conventions. Identify which you're writing first.
| Flavor | Folder pattern | Audience | Priority |
|---|---|---|---|
| Tutorial | <verb>-<thing>/main.go (e.g., create-loan-broker/main.go) | A dev reading & learning the protocol | Clarity 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 folder | Speed over clarity |
If a file isn't clearly one or the other, prompt the user for clarity.
gofmt-formatted (tabs, standard layout)
- Folder/binary names:
kebab-case(e.g.,create-loan-broker/) - Variables:
camelCasewith 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 embeddedBaseTxsubstruct. - Setup JSON keys:
camelCase(loanBroker,credentialIssuer,mptID,vaultID,loanBrokerID)
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.md is the entry point for a reader running the samples.
- Title:
# <Topic> Examples (Go) - One-sentence description listing what the directory demonstrates
## Setupsection with the note "All commands should be run from thisgo/directory." and ago mod tidyfenced block- 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
shblock withgo run ./<verb>-<thing> - One-sentence summary of what the command will output
- Fenced
shblock showing actual expected console output (real addresses, tx IDs, JSON dumps — captured from a successful sample code run)
---separator between tutorial sections
The expected-output blocks document the golden path. Update them when a command's output format changes.
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.
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 }WebSocket client — github.com/Peersyst/xrpl-go/xrpl/websocket. Always wrap with defer client.Disconnect() right after NewClient so the connection closes on any exit path.
- Multi-line
// IMPORTANT:header explaining what the command demonstrates and any preconditions (e.g., "uses an existing account that has a PRIVATE vault") package main+ imports- 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)
}- (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)- (Optional) If the tutorial funds its own wallets instead of loading them from setup data, add
WithFaucetProviderto the client config in step 3 and fund wallets afterclient.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)
}- Tutorial code steps.
- 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
SubmitTxAndWaitand handle results by checking fortesSUCCESSand 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 } }
RPC client — github.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).
- Use goroutines + buffered channels for fan-out parallelism (not
errgrouporsync.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
TicketCreatewithTicketCount: N, then setSequence: 0andTicketSequence: ...on theBaseTxof 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 }
- Top comment: single line,
// Setup script for <topic> tutorialsabovepackage 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 silentlycontinueor_ = err.
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)
}