Skip to content

Commit 487c350

Browse files
committed
fix(openclaw): strip quotes from mnemonic import, show word count in errors
1 parent db2aa30 commit 487c350

4 files changed

Lines changed: 94 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.11.2] - 2026-04-02
6+
7+
### Fixed
8+
9+
- Strip surrounding quotes from mnemonic in `/x_wallet setup import` (Telegram copy-paste)
10+
- Show actual word count in mnemonic validation errors
11+
512
## [0.11.1] - 2026-04-02
613

714
### Fixed
@@ -37,6 +44,7 @@ All notable changes to this project will be documented in this file.
3744
- Strip `outputSchema` from proxied MCP tool definitions
3845
- Downgrade @modelcontextprotocol/sdk to 1.27.1 for compatibility
3946

47+
[0.11.2]: https://github.com/cascade-protocol/x402-proxy/compare/v0.11.1...v0.11.2
4048
[0.11.1]: https://github.com/cascade-protocol/x402-proxy/compare/v0.11.0...v0.11.1
4149
[0.11.0]: https://github.com/cascade-protocol/x402-proxy/compare/v0.10.12...v0.11.0
4250
[0.10.12]: https://github.com/cascade-protocol/x402-proxy/releases/tag/v0.10.12

packages/x402-proxy/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "x402-proxy",
33
"private": true,
4-
"version": "0.11.1",
4+
"version": "0.11.2",
55
"description": "curl for x402 paid APIs. Auto-pays any endpoint on Base, Solana, and Tempo.",
66
"type": "module",
77
"sideEffects": false,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it } from "vitest";
2+
import { parseMnemonicImport } from "./commands.js";
3+
4+
const VALID_12 =
5+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
6+
const VALID_24 =
7+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
8+
9+
describe("parseMnemonicImport", () => {
10+
it("accepts valid 12-word mnemonic as bare words", () => {
11+
const result = parseMnemonicImport(VALID_12.split(" "));
12+
expect(result).toEqual({ mnemonic: VALID_12 });
13+
});
14+
15+
it("accepts valid 24-word mnemonic as bare words", () => {
16+
const result = parseMnemonicImport(VALID_24.split(" "));
17+
expect(result).toEqual({ mnemonic: VALID_24 });
18+
});
19+
20+
it("strips surrounding double quotes (Telegram copy-paste)", () => {
21+
const result = parseMnemonicImport([`"${VALID_12}"`]);
22+
expect(result).toEqual({ mnemonic: VALID_12 });
23+
});
24+
25+
it("strips surrounding single quotes", () => {
26+
const result = parseMnemonicImport([`'${VALID_12}'`]);
27+
expect(result).toEqual({ mnemonic: VALID_12 });
28+
});
29+
30+
it("strips surrounding smart quotes", () => {
31+
const result = parseMnemonicImport([`\u201C${VALID_12}\u201D`]);
32+
expect(result).toEqual({ mnemonic: VALID_12 });
33+
});
34+
35+
it("handles quoted mnemonic split across args by Telegram", () => {
36+
// Telegram may split: ['"abandon', 'abandon', ..., 'about"']
37+
const words = VALID_12.split(" ");
38+
words[0] = `"${words[0]}`;
39+
words[words.length - 1] = `${words[words.length - 1]}"`;
40+
const result = parseMnemonicImport(words);
41+
expect(result).toEqual({ mnemonic: VALID_12 });
42+
});
43+
44+
it("rejects wrong word count with count in error", () => {
45+
const result = parseMnemonicImport("one two three four five six seven eight".split(" "));
46+
expect("error" in result && result.error).toContain("got 8");
47+
});
48+
49+
it("rejects 16-word input", () => {
50+
const sixteenWords = VALID_24.split(" ").slice(0, 16);
51+
const result = parseMnemonicImport(sixteenWords);
52+
expect("error" in result && result.error).toContain("got 16");
53+
});
54+
55+
it("rejects invalid BIP-39 words with correct count", () => {
56+
const invalid =
57+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon zzzzz";
58+
const result = parseMnemonicImport(invalid.split(" "));
59+
expect("error" in result && result.error).toContain("Invalid BIP-39");
60+
});
61+
62+
it("rejects empty input", () => {
63+
const result = parseMnemonicImport([]);
64+
expect("error" in result && result.error).toContain("got 0");
65+
});
66+
});

packages/x402-proxy/src/openclaw/commands.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -203,23 +203,32 @@ function handleHistory(histPath: string, page: number): { text: string } {
203203
return { text: lines.join("\n") };
204204
}
205205

206+
/** Parse mnemonic from import args, stripping surrounding quotes. Returns error string or clean mnemonic. */
207+
export function parseMnemonicImport(parts: string[]): { mnemonic: string } | { error: string } {
208+
const raw = parts.join(" ").replace(/^["'""\u201C\u201D]+|["'""\u201C\u201D]+$/g, "");
209+
const words = raw.split(/\s+/).filter(Boolean);
210+
if (words.length !== 12 && words.length !== 24) {
211+
return {
212+
error: `Mnemonic must be 12 or 24 words (got ${words.length}).\nUsage: \`/x_wallet setup import word1 word2 ... word24\``,
213+
};
214+
}
215+
const mnemonic = words.join(" ");
216+
if (!isValidMnemonic(mnemonic)) {
217+
return { error: "Invalid BIP-39 mnemonic. Check the words and try again." };
218+
}
219+
return { mnemonic };
220+
}
221+
206222
async function handleSetup(ctx: CommandContext, parts: string[]): Promise<{ text: string }> {
207223
const action = parts[0]?.toLowerCase();
208224

209225
let mnemonic: string | null = null;
210226
if (action === "generate") {
211227
mnemonic = generateMnemonic();
212228
} else if (action === "import") {
213-
const words = parts.slice(1);
214-
if (words.length !== 12 && words.length !== 24) {
215-
return {
216-
text: "Mnemonic must be 12 or 24 words.\nUsage: `/x_wallet setup import word1 word2 ... word24`",
217-
};
218-
}
219-
mnemonic = words.join(" ");
220-
if (!isValidMnemonic(mnemonic)) {
221-
return { text: "Invalid BIP-39 mnemonic. Check the words and try again." };
222-
}
229+
const result = parseMnemonicImport(parts.slice(1));
230+
if ("error" in result) return { text: result.error };
231+
mnemonic = result.mnemonic;
223232
}
224233

225234
if (!mnemonic) {

0 commit comments

Comments
 (0)