Skip to content

Commit e0d5c52

Browse files
committed
feat: add wallet vanity command for generating vanity addresses
1 parent ec6955f commit e0d5c52

3 files changed

Lines changed: 112 additions & 1 deletion

File tree

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,30 @@ xrpl wallet verify-message "I own this account" 48F3E8... ED01FA...
14761476
# Valid
14771477
```
14781478
1479+
#### `wallet vanity`
1480+
1481+
Generate a vanity address matching a prefix and/or suffix pattern. Uses brute-force search.
1482+
1483+
> **WARNING:** Vanity addresses should NOT be used for mainnet accounts holding real value.
1484+
1485+
```bash
1486+
xrpl wallet vanity [--starts-with <pattern>] [--ends-with <pattern>] [--algorithm <ed25519|secp256k1>]
1487+
```
1488+
1489+
| Option | Type | Default | Description |
1490+
|--------|------|---------|-------------|
1491+
| `--starts-with` | `string` | — | Prefix pattern to match after the leading `r` (case-insensitive regex) |
1492+
| `--ends-with` | `string` | — | Suffix pattern to match (case-insensitive regex) |
1493+
| `--algorithm` | `string` | `"ed25519"` | Key algorithm: `ed25519` or `secp256k1` |
1494+
1495+
At least one of `--starts-with` or `--ends-with` is required.
1496+
1497+
```bash
1498+
xrpl wallet vanity --starts-with Nick
1499+
xrpl wallet vanity --ends-with XRP
1500+
xrpl wallet vanity --starts-with N --ends-with XRP
1501+
```
1502+
14791503
#### `wallet import`
14801504
14811505
Import a wallet into an encrypted local keystore. Supports seeds, BIP39 mnemonics (with optional passphrase and derivation path), and interactive input. Secrets are encrypted with scrypt + AES-128-CTR.
@@ -2046,7 +2070,6 @@ Contributions are welcome! Please:
20462070
Features that aren't planned for the initial release but worth exploring later:
20472071
20482072
- **`watch` mode** — poll a command at an interval and stream changes, e.g. `xrpl watch balance rAddr --interval 5s`. Useful for scripting and monitoring.
2049-
- **`wallet vanity`** — generate a vanity address matching a prefix/suffix pattern (e.g. `xrpl wallet vanity --starts-with rXRP`).
20502073
- **Ledger hardware wallet signing** — sign transactions using a connected Ledger device, as an alternative to providing a seed or keyfile.
20512074
- **`--private-key` option** — accept a raw hex private key directly for signing, in addition to seeds. Requires algorithm detection or an `--algorithm` flag to derive the public key.
20522075
- **Niche developer utilities** — expose more from xrpl.js sub-packages: RFC1751 mnemonic encode/decode, seed encoding/decoding (`encodeSeed`/`decodeSeed`), raw keypair derivation (`deriveKeypair`), account ID encoding from hex public key, and low-level binary codec operations.

src/commands/wallet/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { registerImport } from "./import.js";
77
import { registerList } from "./list.js";
88
import { registerRemove } from "./remove.js";
99
import { registerSignMessage } from "./sign-message.js";
10+
import { registerVanity } from "./vanity.js";
1011
import { registerVerifyMessage } from "./verify-message.js";
1112

1213
export function registerWalletCommands(program: Command) {
@@ -22,5 +23,6 @@ export function registerWalletCommands(program: Command) {
2223
registerList(wallet, program);
2324
registerRemove(wallet, program);
2425
registerSignMessage(wallet, program);
26+
registerVanity(wallet, program);
2527
registerVerifyMessage(wallet, program);
2628
}

src/commands/wallet/vanity.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { type Command, Option } from "commander";
2+
import { Wallet } from "xrpl";
3+
import { info, output } from "../../lib/output.js";
4+
import type { GlobalOptions } from "../../lib/types.js";
5+
import { formatWallet, parseAlgorithm } from "./shared.js";
6+
7+
function toRegex(pattern: string, template: string, label: string): RegExp {
8+
try {
9+
return new RegExp(template.replace("{}", pattern), "i");
10+
} catch {
11+
throw new Error(`Invalid regex in ${label}: ${pattern}`);
12+
}
13+
}
14+
15+
function buildMatcher(
16+
startsWith?: string,
17+
endsWith?: string,
18+
): (address: string) => boolean {
19+
const startsRe = startsWith
20+
? toRegex(startsWith, "^r{}", "--starts-with")
21+
: null;
22+
const endsRe = endsWith ? toRegex(endsWith, "{}$", "--ends-with") : null;
23+
24+
return (address: string) => {
25+
if (startsRe && !startsRe.test(address)) return false;
26+
if (endsRe && !endsRe.test(address)) return false;
27+
return true;
28+
};
29+
}
30+
31+
async function vanityWallet(
32+
opts: { algorithm: string; startsWith?: string; endsWith?: string },
33+
globalOpts: GlobalOptions,
34+
) {
35+
if (!opts.startsWith && !opts.endsWith) {
36+
throw new Error("At least one of --starts-with or --ends-with is required");
37+
}
38+
39+
const matches = buildMatcher(opts.startsWith, opts.endsWith);
40+
const algorithm = parseAlgorithm(opts.algorithm);
41+
42+
info(
43+
"WARNING: Vanity addresses should NOT be used for mainnet accounts holding real value.\n",
44+
globalOpts,
45+
);
46+
info("Searching for matching address...", globalOpts);
47+
48+
let attempts = 0;
49+
let wallet: Wallet;
50+
51+
do {
52+
wallet = Wallet.generate(algorithm);
53+
attempts++;
54+
if (attempts % 10000 === 0) {
55+
info(` ${attempts.toLocaleString()} attempts...`, globalOpts);
56+
}
57+
} while (!matches(wallet.address));
58+
59+
info(`Found after ${attempts.toLocaleString()} attempts.\n`, globalOpts);
60+
output(formatWallet(wallet), globalOpts);
61+
}
62+
63+
export function registerVanity(wallet: Command, program: Command) {
64+
wallet
65+
.command("vanity")
66+
.description(
67+
"Generate a vanity address matching a pattern (NOT safe for mainnet use)",
68+
)
69+
.option(
70+
"--starts-with <pattern>",
71+
"Prefix pattern to match after the leading 'r' (case-insensitive regex)",
72+
)
73+
.option(
74+
"--ends-with <pattern>",
75+
"Suffix pattern to match (case-insensitive regex)",
76+
)
77+
.addOption(
78+
new Option("--algorithm <algorithm>", "Key algorithm")
79+
.choices(["ed25519", "secp256k1"])
80+
.default("ed25519"),
81+
)
82+
.action(
83+
(opts: { algorithm: string; startsWith?: string; endsWith?: string }) =>
84+
vanityWallet(opts, program.opts() as GlobalOptions),
85+
);
86+
}

0 commit comments

Comments
 (0)