Skip to content

Commit ebfc30f

Browse files
committed
refactor: deduplicate code, remove dead exports, clean up internals
- Extract shared walletFromMnemonic into src/lib/mnemonic.ts (was duplicated in wallet.ts and wallet/import.ts) - Replace hand-rolled toRippleTime with xrpl's isoTimeToRippleTime in escrow/create.ts - Remove dead decryptSeed/decryptKeypair (superseded by decryptSecret) - Remove unused fail(), nonNegativeInt, memo schema - Un-export internal-only functions: getKeystoreDir, applyTxOptions, validateHex, Config interface - Remove unused Memo re-export from memo.ts - Remove unused EXIT_CODES import from output.ts - Update keystore tests to use decryptSecret
1 parent 6a5a132 commit ebfc30f

12 files changed

Lines changed: 69 additions & 152 deletions

File tree

src/commands/escrow/create.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Command } from "commander";
2-
import { type EscrowCreate, xrpToDrops } from "xrpl";
2+
import { type EscrowCreate, isoTimeToRippleTime, xrpToDrops } from "xrpl";
33
import { withClient } from "../../lib/client.js";
44
import { resolve } from "../../lib/config.js";
55
import {
@@ -48,11 +48,7 @@ async function escrowCreate(opts: EscrowCreateOpts, globalOpts: GlobalOptions) {
4848
function toRippleTime(value: string): number {
4949
const num = Number(value);
5050
if (Number.isFinite(num) && num > 0) return num;
51-
const ms = Date.parse(value);
52-
if (Number.isNaN(ms))
53-
throw new Error(`Invalid time "${value}". Use ISO 8601 or Ripple epoch.`);
54-
// Ripple epoch starts 2000-01-01T00:00:00Z = 946684800 Unix
55-
return Math.floor(ms / 1000) - 946684800;
51+
return isoTimeToRippleTime(value);
5652
}
5753

5854
export function registerEscrowCreate(program: Command) {

src/commands/wallet/import.ts

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import { existsSync, readFileSync } from "node:fs";
2-
import { HDKey } from "@scure/bip32";
3-
import { mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
4-
import { wordlist } from "@scure/bip39/wordlists/english.js";
51
import type { Command } from "commander";
62
import { Wallet } from "xrpl";
73
import {
84
encryptKeypair,
95
encryptSeed,
106
saveKeystore,
117
} from "../../lib/keystore.js";
8+
import { walletFromMnemonic } from "../../lib/mnemonic.js";
129
import { output } from "../../lib/output.js";
1310
import { prompt } from "../../lib/prompt.js";
1411
import type { GlobalOptions } from "../../lib/types.js";
@@ -26,39 +23,6 @@ interface ImportOpts {
2623
mnemonicIndex?: number;
2724
}
2825

29-
function walletFromMnemonic(
30-
mnemonic: string,
31-
opts: ImportOpts,
32-
): { wallet: Wallet; publicKey: string; privateKey: string } {
33-
if (!validateMnemonic(mnemonic, wordlist)) {
34-
throw new Error("Invalid BIP39 mnemonic phrase.");
35-
}
36-
37-
const passphrase = opts.mnemonicPassphrase ?? "";
38-
const seed = mnemonicToSeedSync(mnemonic, passphrase);
39-
const masterNode = HDKey.fromMasterSeed(seed);
40-
41-
let derivationPath = opts.mnemonicDerivationPath ?? DEFAULT_DERIVATION_PATH;
42-
if (opts.mnemonicIndex != null) {
43-
const parts = derivationPath.split("/");
44-
parts[parts.length - 1] = String(opts.mnemonicIndex);
45-
derivationPath = parts.join("/");
46-
}
47-
48-
const node = masterNode.derive(derivationPath);
49-
if (!node.publicKey || !node.privateKey) {
50-
throw new Error(
51-
`Unable to derive keys from mnemonic at path ${derivationPath}`,
52-
);
53-
}
54-
55-
const publicKey = Buffer.from(node.publicKey).toString("hex").toUpperCase();
56-
const privateKey = `00${Buffer.from(node.privateKey).toString("hex").toUpperCase()}`;
57-
const wallet = new Wallet(publicKey, privateKey);
58-
59-
return { wallet, publicKey, privateKey };
60-
}
61-
6226
async function importWallet(
6327
name: string,
6428
opts: ImportOpts,
@@ -81,13 +45,8 @@ async function importWallet(
8145
const password = await getPassword(opts);
8246

8347
if (hasMnemonic) {
84-
const mnemonicInput = opts.mnemonic!;
85-
const mnemonic = existsSync(mnemonicInput)
86-
? readFileSync(mnemonicInput, "utf-8").trim()
87-
: mnemonicInput;
88-
8948
const { wallet, publicKey, privateKey } = walletFromMnemonic(
90-
mnemonic,
49+
opts.mnemonic!,
9150
opts,
9251
);
9352
keystore = encryptKeypair(

src/lib/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { join } from "node:path";
44
import { parse } from "smol-toml";
55
import { xrplAddress } from "./schemas.js";
66

7-
export interface Config {
7+
interface Config {
88
network?: string;
99
rpc_url?: string;
1010
aliases: Record<string, string>;

src/lib/keystore.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const SCRYPT_R = 8;
4646
const SCRYPT_P = 1;
4747
const DKLEN = 32;
4848

49-
export function getKeystoreDir(): string {
49+
function getKeystoreDir(): string {
5050
const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
5151
return join(xdgConfig, "xrpl", "keystores");
5252
}
@@ -151,29 +151,6 @@ export function encryptKeypair(
151151
};
152152
}
153153

154-
export function decryptSeed(keystore: KeystoreFile, password: string): string {
155-
if (keystore.type !== "seed" && keystore.type != null) {
156-
throw new Error(
157-
`Keystore "${keystore.name}" is not a seed-based keystore (type: ${keystore.type}).`,
158-
);
159-
}
160-
return decrypt(keystore.crypto, password);
161-
}
162-
163-
export function decryptKeypair(
164-
keystore: KeystoreFile,
165-
password: string,
166-
): { publicKey: string; privateKey: string } {
167-
const raw = decrypt(keystore.crypto, password);
168-
if (keystore.type === "keypair") {
169-
return JSON.parse(raw) as { publicKey: string; privateKey: string };
170-
}
171-
// Legacy: type is "seed" or missing — return seed so caller can handle it
172-
throw new Error(
173-
`Keystore "${keystore.name}" is not a keypair-based keystore (type: ${keystore.type ?? "seed"}).`,
174-
);
175-
}
176-
177154
export function decryptSecret(
178155
keystore: KeystoreFile,
179156
password: string,

src/lib/memo.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { Memo } from "xrpl";
22

3-
export type { Memo };
4-
53
const MEMO_RE = /^(hex:)?([^:]+):(hex:)?(.+)$/s;
64

75
function encodeField(raw: string, isHex: boolean): string {

src/lib/mnemonic.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { existsSync, readFileSync } from "node:fs";
2+
import { HDKey } from "@scure/bip32";
3+
import { mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
4+
import { wordlist } from "@scure/bip39/wordlists/english.js";
5+
import { Wallet } from "xrpl";
6+
7+
interface MnemonicOpts {
8+
mnemonicPassphrase?: string;
9+
mnemonicDerivationPath?: string;
10+
mnemonicIndex?: number;
11+
}
12+
13+
const DEFAULT_DERIVATION_PATH = "m/44'/144'/0'/0/0";
14+
15+
/**
16+
* Resolve a mnemonic input (file path or inline phrase) and derive a wallet.
17+
*/
18+
export function walletFromMnemonic(
19+
mnemonicInput: string,
20+
opts: MnemonicOpts,
21+
): { wallet: Wallet; publicKey: string; privateKey: string } {
22+
const mnemonic = existsSync(mnemonicInput)
23+
? readFileSync(mnemonicInput, "utf-8").trim()
24+
: mnemonicInput;
25+
26+
if (!validateMnemonic(mnemonic, wordlist)) {
27+
throw new Error("Invalid BIP39 mnemonic phrase.");
28+
}
29+
30+
const passphrase = opts.mnemonicPassphrase ?? "";
31+
const seed = mnemonicToSeedSync(mnemonic, passphrase);
32+
const masterNode = HDKey.fromMasterSeed(seed);
33+
34+
let derivationPath = opts.mnemonicDerivationPath ?? DEFAULT_DERIVATION_PATH;
35+
if (opts.mnemonicIndex != null) {
36+
const parts = derivationPath.split("/");
37+
parts[parts.length - 1] = String(opts.mnemonicIndex);
38+
derivationPath = parts.join("/");
39+
}
40+
41+
const node = masterNode.derive(derivationPath);
42+
if (!node.publicKey || !node.privateKey) {
43+
throw new Error(
44+
`Unable to derive keys from mnemonic at path ${derivationPath}`,
45+
);
46+
}
47+
48+
const publicKey = Buffer.from(node.publicKey).toString("hex").toUpperCase();
49+
const privateKey = `00${Buffer.from(node.privateKey).toString("hex").toUpperCase()}`;
50+
return { wallet: new Wallet(publicKey, privateKey), publicKey, privateKey };
51+
}

src/lib/output.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as tty from "node:tty";
22
import type { GlobalOptions } from "./types.js";
3-
import { EXIT_CODES } from "./types.js";
43

54
const isTTY = tty.isatty(1);
65

@@ -46,11 +45,3 @@ export function info(message: string, opts: GlobalOptions): void {
4645
console.error(message);
4746
}
4847
}
49-
50-
export function fail(
51-
message: string,
52-
code: number = EXIT_CODES.GENERAL_ERROR,
53-
): never {
54-
console.error(`Error: ${message}`);
55-
process.exit(code);
56-
}

src/lib/schemas.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@ export const positiveInt = z
2424
.int()
2525
.min(1, "Must be a positive integer");
2626

27-
export const nonNegativeInt = z
28-
.number()
29-
.int()
30-
.min(0, "Must be a non-negative integer");
31-
3227
export const intRange = (min: number, max: number) =>
3328
z
3429
.number()
@@ -98,13 +93,6 @@ export const signerEntry = z.string().transform((entry, ctx) => {
9893
return { SignerEntry: { Account: address, SignerWeight: weight } };
9994
});
10095

101-
export const memo = z
102-
.string()
103-
.regex(
104-
/^(hex:)?[^:]+(hex:)?.+$/s,
105-
'Invalid memo format. Expected "type:data" (e.g. "type:interchain_transfer").',
106-
);
107-
10896
export const tokenSpec = z
10997
.string()
11098
.refine(

src/lib/tx.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export async function calcSignersFee(
3838
* Apply shared transaction options (--prepare, --signers, --memo, --ticket)
3939
* to a transaction object before signing/submitting.
4040
*/
41-
export function applyTxOptions(tx: UnsignedTx, txOpts: TxOptions): UnsignedTx {
41+
function applyTxOptions(tx: UnsignedTx, txOpts: TxOptions): UnsignedTx {
4242
if (txOpts.memo?.length) {
4343
tx.Memos = parseMemos(txOpts.memo);
4444
}

src/lib/validate.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
hexString,
33
intRange,
44
mptIssuanceId,
5-
nonNegativeInt,
65
positiveInt,
76
txHash,
87
xrpAmount,
@@ -12,7 +11,7 @@ function label(name: string, value: unknown): string {
1211
return `Invalid ${name}: "${value}".`;
1312
}
1413

15-
export function validateHex(value: string, length: number, name = "hex"): void {
14+
function validateHex(value: string, length: number, name = "hex"): void {
1615
const r = hexString(length).safeParse(value);
1716
if (!r.success)
1817
throw new Error(`${label(name, value)} ${r.error.issues[0].message}`);
@@ -46,12 +45,6 @@ export function validatePositiveInt(value: number, name = "value"): void {
4645
throw new Error(`${label(name, value)} ${r.error.issues[0].message}`);
4746
}
4847

49-
export function validateNonNegativeInt(value: number, name = "value"): void {
50-
const r = nonNegativeInt.safeParse(value);
51-
if (!r.success)
52-
throw new Error(`${label(name, value)} ${r.error.issues[0].message}`);
53-
}
54-
5548
export function validateRange(
5649
value: number,
5750
min: number,

0 commit comments

Comments
 (0)