Skip to content

feat: An account takes its address from its key, and a record answers one field at a time - #44

Open
willemneal wants to merge 3 commits into
feat/cli-streams-tickets-batchfrom
feat/cli-account-from-key
Open

willemneal wants to merge 3 commits into
feat/cli-streams-tickets-batchfrom
feat/cli-account-from-key

Conversation

@willemneal

Copy link
Copy Markdown
Member

Stacked on #43. Review the last three commits.

What prompted it

You cannot add an account with a key. And furthermore there is no way to list the parts of the key e.g. xrpl key show --address alice. xrpl accounts add --key alice should also work, currently it complains about a missing --address but that can be found via alice. It is good that accounts and keys are separate but most of the time an account will use just one key corresponding to its private key.

Both halves were real, and the second was the cause of the first. The address was always in the key record; there was no way to read one field out of a record without a JSON parser. So account add asked for an address the CLI already knew, and the ceremony script in demo/ opened with the proof:

"$XRPL" key generate "$name" --algorithm "$algorithm" >/dev/null
address=$("$XRPL" key show "$name" --json | jq -r .classic_address)
"$XRPL" account add "$name" --address "$address" … --key "$name" --default-signer "$name"

It is now:

"$XRPL" key generate "$name" --algorithm "$algorithm" >/dev/null
"$XRPL" account add "$name" --network-id "$EXPECT_NETWORK_ID" --key "$name" >/dev/null
address=$("$XRPL" account show "$name" --address)

account add --key supplies the address

--address is optional when --key is given, taken from the key records' cached classic_address, and disclosed on stderr — address r…, from key alice — for the same reason an inherited --account says so.

It is a default, not a rule. --address still wins when given, because a regular key and a signer-list member each derive to their own address and not to the account's; that is the whole reason keys is a list rather than a field. Three refusals rather than guesses:

  • keys that derive to different addresses, with each spelled out — guessing wrong records an account that signs for someone else
  • neither an --address nor a --key
  • the same --key twice, which would otherwise record an account with one key that cannot tell it has one

And --force will not change an alias's address by omission. Adding a key is the obvious reason to re-run add --force, and until --address became optional it had to be restated on every such run, so an alias could not silently change identity. It still cannot.

key show and account show answer one field

xrpl key show alice --address        # --public-key  --algorithm  --source
xrpl account show alice --keys       # --address  --network-id  --tag  --default-signer

One value, bare on stdout, nothing else — --json | jq -r .classic_address is just --address. Mutually exclusive, and each conflicts with --json.

A field set to nothing prints nothing and says why on stderr, so $(xrpl account show alice --tag) comes back empty rather than holding the word none. --default-signer is the exception and fails instead: an account with no key, or with several and no default, has no answer rather than an empty one.

account show also names the effective default signer. An account with one key has no default_signer field and that key is still the one the record names — the commonest record there is, and the block used to say nothing at all about it. An account with several and no default is named too, because that is the record a person most needs to be told to choose for.

Two fixes found on the way

  • xrpl key ls | head -1 ended in a Rust panic. The tx stages were moved onto the closed-pipe handling when tx new | head -1 was found to panic; the record listings were not, and they have the same reader.
  • SignerError::Unavailable spliced two sentences. Every construction site passes a full clause, and the variant wrapped them in one of its own: Signer alice is watch-only: it has a public key and no secret is unavailable on this machine.

One thing this does not do

AccountRecord::signer_key — the resolution that makes a lone key an account's default — had no caller outside its own unit tests, and still has none in the signing path. tx sign takes --sign-with <key-id> and reads no account record, so account show --default-signer answers which key this account names and that value is what you hand --sign-with. An early draft of this PR claimed the two resolved the same way; they do not, and the docs now say so rather than the reverse. Making tx sign resolve through the record is the obvious next step and is a separate change — it would need to decide what it resolves from without breaking the invariant that the unlock happens before the first transaction is read.

Verification

  • 299 tests pass, including 30 in tests/records.rs — 11 new, covering every refusal and every part flag against the --json record
  • cargo clippy --all-targets clean; CLI.md regenerated
  • the full token ceremony runs green end-to-end against a live rippleci/xrpld --standalone, with all six exit codes still asserted
  • three review agents (behaviour, output contract, house style) raised 14 findings against the first draft; all 14 are addressed here, and the highest-severity one is the tx sign claim above

🤖 Generated with Claude Code

willemneal and others added 3 commits September 24, 2026 13:50
The `tx` stages were moved onto `output::write_line`, which treats a closed
pipe as a normal ending, when `xrpl tx new … | head -1` was found to panic.
The record listings were not, and they have the same reader: `key ls | head`,
`account ls | grep -q`, anything that stops reading once it has what it asked
for. `head` closing the pipe is the reader doing its job, not an error.

`raw_line`'s doc said it was for `server subscribe` alone. It is for any output
that is a line rather than a document, which is what these listings are, so the
doc now says that instead of naming its first caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every site that builds `SignerError::Unavailable` passes a full clause naming
the signer and the reason, and the variant wrapped them in one of its own:
`Signer {0} is unavailable on this machine`. So a watch-only key read back as
`Signer alice is watch-only: it has a public key and no secret is unavailable
on this machine`, and the credential-store timeout — a paragraph of advice —
read worse.

The payload is the sentence. `Error::Signer` already prefixes "Signer error",
and it is the exit code, not the prose, that tells this apart from a backend
failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… one field at a time

`xrpl account add` required `--address`. An account's address derives from its
original master public key, so when that key is the one being recorded the
address is already on disk — and requiring it anyway meant every caller ran
`xrpl key show <id> --json | jq -r .classic_address` first. The ceremony script
in `demo/` did exactly that, which is the tell.

`--address` is now optional when `--key` is given, and is taken from the key
records. It is a default and not a rule: `--address` still wins when given,
because a regular key and a signer-list member each derive to their own address
and not to the account's, which is the whole reason keys are a list rather than
a field. Keys that derive to different addresses are refused with both spelled
out, rather than guessed at — guessing wrong records an account that signs for
someone else. `--force` will not change an alias's address by omission either:
adding a key is the obvious reason to re-run `add --force`, and until now the
address had to be restated on every such run.

The other half is reading a field back out. `key show` and `account show` grew
part flags — `--address`, `--public-key`, `--algorithm`, `--source` on one,
`--address`, `--network-id`, `--tag`, `--keys`, `--default-signer` on the other
— each writing that one value bare on stdout and nothing else, so a script
needs no JSON parser. A field set to nothing prints nothing and says so on
stderr, which keeps `$(xrpl account show alice --tag)` empty rather than holding
the word "none". `--default-signer` is the exception: an account with no key,
or with several and no default, has no answer rather than an empty one.

`account show` also names the effective default signer. An account with a single
key has no `default_signer` field and that key is still the one it names — the
commonest record there is — and the block used to say nothing at all about it.
An account with several and no default is named too, since that is the record a
person most needs to be told to choose for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant