Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-privkey-prefix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@offckb/cli": patch
---

fix(cli): standardize private key inputs and fix 0x prefix parsing error (#422)
5 changes: 3 additions & 2 deletions src/sdk/ckb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// to replace lumos with ccc

import { ccc, ClientPublicMainnet, ClientPublicTestnet, OutPointLike, Script } from '@ckb-ccc/core';
import { isValidNetworkString } from '../util/validator';
import { isValidNetworkString, normalizePrivKey } from '../util/validator';
import { networks } from './network';
import { buildCCCDevnetKnownScripts } from '../scripts/private';
import { Migration } from '../deploy/migration';
Expand Down Expand Up @@ -81,7 +81,8 @@ export class CKB {
}

buildSigner(privateKey: HexString) {
const signer = new ccc.SignerCkbPrivateKey(this.client, privateKey);
const normalizedKey = normalizePrivKey(privateKey);
const signer = new ccc.SignerCkbPrivateKey(this.client, normalizedKey);
return signer;
}

Expand Down
40 changes: 40 additions & 0 deletions src/util/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,43 @@ export function isValidVersion(version: unknown): boolean {
// Test the version against the regex
return versionRegex.test(version);
}

export function normalizePrivKey(privKey: string): string {
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This normalization/validation logic is now on the buildSigner hot-path for multiple CLI commands, but there are no tests covering common accepted inputs (with/without 0x, with quotes/whitespace) and rejection cases (bad hex, wrong length). Adding a small unit test suite for normalizePrivKey would help prevent regressions and ensure the error messages stay user-friendly.

Copilot uses AI. Check for mistakes.
// Trim surrounding whitespaces
let key = privKey ? privKey.trim() : '';

if (!key) {
throw new Error('Private key is required.');
}

// Strip surrounding quotes
if (key.startsWith('"') && key.endsWith('"')) {
key = key.slice(1, -1);
}
if (key.startsWith("'") && key.endsWith("'")) {
key = key.slice(1, -1);
}

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After stripping surrounding quotes, the value is not re-trimmed. Inputs like '"0x... "' (common when copying from .env) will retain inner whitespace and fail the hex-only regex. Consider trimming again after quote removal (and/or before validation) so quoted keys with accidental inner spaces normalize successfully.

Suggested change
// Trim again to normalize whitespace that was inside surrounding quotes
key = key.trim();

Copilot uses AI. Check for mistakes.
// Trim again to normalize whitespace that was inside surrounding quotes
key = key.trim();

// Remove standard 0x/0X prefix if it exists manually for normalization
if (/^0x/i.test(key)) {
key = key.slice(2);
}

// Validate only hex characters are left
if (!/^[0-9a-fA-F]+$/.test(key)) {
throw new Error('Invalid private key: contains non-hexadecimal characters.');
}

// Enforce exactly 32 bytes length
if (key.length !== 64) {
throw new Error(
`Invalid private key length: expected 32 bytes (64 hex characters), but got ${key.length} characters (excluding 0x prefix).`,
);
}

// Return the formally strictly padded ckb format `0x` string
return '0x' + key;
}
46 changes: 46 additions & 0 deletions tests/validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { normalizePrivKey } from '../src/util/validator';

describe('normalizePrivKey', () => {
const validHex64 = '1234567812345678123456781234567812345678123456781234567812345678';

it('should throw an error for empty or formatting values', () => {
expect(() => normalizePrivKey('')).toThrow('Private key is required.');
expect(() => normalizePrivKey(' ')).toThrow('Private key is required.');
});

it('should accept a 64-character hex string without 0x prefix', () => {
const key = validHex64;
expect(normalizePrivKey(key)).toBe('0x' + key);
});

it('should accept a 64-character hex string with 0x prefix', () => {
const key = '0x' + validHex64;
expect(normalizePrivKey(key)).toBe('0x' + validHex64);
});

it('should accept a 64-character hex string with 0X prefix (case-insensitive)', () => {
const key = '0X' + validHex64;
expect(normalizePrivKey(key)).toBe('0x' + validHex64);
});

it('should correctly trim spaces inside quotes', () => {
const key = `' 0x${validHex64} '`;
expect(normalizePrivKey(key)).toBe('0x' + validHex64);

const key2 = `" 0X${validHex64} "`;
expect(normalizePrivKey(key2)).toBe('0x' + validHex64);
});

it('should throw an error if the key contains non-hexadecimal characters', () => {
const invalidHex = validHex64.slice(0, -1) + 'G'; // Replace last char with 'G' (invalid hex)
expect(() => normalizePrivKey(invalidHex)).toThrow('Invalid private key: contains non-hexadecimal characters.');
});

it('should throw an error if the key length is incorrect', () => {
const shortKey = validHex64.slice(0, 62);
expect(() => normalizePrivKey(shortKey)).toThrow('Invalid private key length');

const longKey = validHex64 + '12';
expect(() => normalizePrivKey(longKey)).toThrow('Invalid private key length');
});
});
Loading