Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ examples/ Runnable usage examples
- Run `npm run lint` before pushing — this type-checks the full source tree.
- Match the naming and file conventions already present in `src/`.
- Keep functions focused and add JSDoc comments for any exported API.
- Do not introduce new runtime dependencies without discussion in the issue first.
- Do not introduce new dependencies without following the [Dependency Review Standards](./docs/dependency-review.md) and discussing in the issue first.

---

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ npm install @axionvera/pocketpay-sdk
- [Release Checklist](./docs/release-checklist.md) - Pre-release verification steps for maintainers
- [Architecture Decision Records](./docs/adr/) - Records of significant SDK design decisions and their rationale
- [Support Policy](./docs/support-policy.md) - Supported runtimes, versions, network status, and maintenance expectations
- [Dependency Review Standards](./docs/dependency-review.md) - Guidelines for evaluating, adding, and updating SDK dependencies
- [Changelog](./CHANGELOG.md) - Track changes across SDK versions

## Package Root Imports
Expand Down
19 changes: 19 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,22 @@ interface ValidationError {
```

Consumers should branch on `err.code`; codes are part of the public contract, messages are not. See `docs/getting-started.md` for a form-integration example.

## Wallet Import API

### `importWallet(secretKey)`
Imports an existing wallet from a Stellar secret key (S...).
- **Throws**: `PocketPayError` (`INVALID_SECRET_KEY`) with typed validation reason (`not_a_string`, `missing`, `invalid_prefix`, `invalid_length`, `invalid_format`).

### `safeImportWallet(secretKey)`
Safely attempts to import a wallet without throwing.
- **Returns**: `PocketPayResult<WalletKeypair>` (`{ ok: true, value }` or `{ ok: false, error }`).

### `enhancedImportWallet(secretKey)`
Imports a wallet and returns an enhanced result with warnings and recovery hints.
- **Returns**: `EnhancedPocketPayResult<WalletKeypair>`.

### `safeEnhancedImportWallet(secretKey)`
Non-throwing wrapper for `enhancedImportWallet`.
- **Returns**: `EnhancedPocketPayResult<WalletKeypair>`.

2 changes: 1 addition & 1 deletion docs/dependency-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ Before merging any dependency change, confirm that:
- No new high or critical security advisories are introduced.
- The dependency license is compatible with MIT.

See also [Security Best Practices](./security.md) and the [Contributing Guide](../CONTRIBUTING.md).
See also [Security Best Practices](./security.md) and the [Contributing Guide](../CONTRIBUTING.md).
3 changes: 2 additions & 1 deletion docs/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,9 @@ The enhanced pattern is currently applied to two pilot operations:
|:---|:---|:---|:---|
| `sendXLM` | `enhancedSendXLM` | `HIGH_FEE_RATIO` (fee > 10% of amount) | `fund_account`, `check_input`, `retry` |
| `getBalance` | `enhancedGetBalance` | `ZERO_NATIVE_BALANCE`, `MANY_ASSETS` (> 20) | `fund_account`, `check_network`, `check_input` |
| `importWallet` | `enhancedImportWallet` | None | `check_input` |

Each pilot also has a non-throwing variant: `safeEnhancedSendXLM` and `safeEnhancedGetBalance`.
Each pilot also has a non-throwing variant: `safeEnhancedSendXLM`, `safeEnhancedGetBalance`, and `safeEnhancedImportWallet`.

### Building Your Own Enhanced Results

Expand Down
37 changes: 37 additions & 0 deletions docs/wallet-import-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,43 @@ Do not send a secret key to customer support or ask a user to paste one into a
support ticket. Diagnose wallet-import problems with public keys, sanitized
error codes, and non-sensitive device or transaction metadata.

## Validation & Safe Error Handling

When importing secret keys, the SDK performs strict local validation prior to any key derivation or SDK operations:

- **Type Check**: Non-string values throw `PocketPayError` with validation reason `not_a_string`.
- **Presence Check**: Empty or whitespace-only strings throw `PocketPayError` with validation reason `missing`.
- **Prefix Check**: Keys must start with `'S'`. Non-matching values throw `PocketPayError` with validation reason `invalid_prefix`.
- **Length Check**: Keys must be exactly 56 characters. Incorrect lengths throw `PocketPayError` with validation reason `invalid_length`.
- **Format Check**: Keys with invalid strkey payloads or checksums throw `PocketPayError` with validation reason `invalid_format`.

### Redaction Guarantee
Secret key inputs are **never** attached to `error.validation.value` or raw error messages. Sanitized error messages give clear guidance without echoing key material.

### Safe & Enhanced Import Helpers

In addition to `importWallet(secretKey)`, the SDK provides non-throwing and enriched wrappers:

```typescript
import { safeImportWallet, enhancedImportWallet } from '@axionvera/pocketpay-sdk';

// 1. Non-throwing result wrapper
const result = safeImportWallet(userInputKey);
if (result.ok) {
console.log('Wallet imported:', result.value.publicKey);
} else {
console.error('Import failed [code]:', result.error.code);
}

// 2. Enhanced wrapper with recovery hints
const enhanced = enhancedImportWallet(userInputKey);
if (!enhanced.ok) {
enhanced.recoveryHints?.forEach(hint => {
console.log('Recovery action:', hint.action, hint.message);
});
}
```

## Integration Checklist

- Secret keys never appear in logs, errors, analytics, or crash reports.
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export type { ResultWarning, RecoveryHint } from './errors';
export {
createWallet,
importWallet,
safeImportWallet,
enhancedImportWallet,
safeEnhancedImportWallet,
getPublicKey,
getBalance,
getBalanceOrUnfunded,
Expand Down
67 changes: 60 additions & 7 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,74 @@ export function validatePublicKey(publicKey: string): boolean {
}
}

export function validateSecretKey(secretKey: string): boolean {
export function validateSecretKey(secretKey: unknown): boolean {
if (typeof secretKey !== 'string') {
throw new PocketPayError(
'Invalid Stellar secret key: secret key must be a string',
'INVALID_SECRET_KEY',
{
validation: {
field: 'secretKey',
reason: 'not_a_string',
},
},
);
}

if (!secretKey || secretKey.trim().length === 0) {
throw new PocketPayError(
'Invalid Stellar secret key: secret key cannot be empty',
'INVALID_SECRET_KEY',
{
validation: {
field: 'secretKey',
reason: 'missing',
},
},
);
}

const trimmed = secretKey.trim();

if (!trimmed.startsWith('S')) {
throw new PocketPayError(
"Invalid Stellar secret key: secret key must start with 'S'",
'INVALID_SECRET_KEY',
{
validation: {
field: 'secretKey',
reason: 'invalid_prefix',
},
},
);
}

if (trimmed.length !== 56) {
throw new PocketPayError(
`Invalid Stellar secret key: secret key must be 56 characters long (got ${trimmed.length})`,
'INVALID_SECRET_KEY',
{
validation: {
field: 'secretKey',
reason: 'invalid_length',
},
},
);
}

try {
StellarSDK.Keypair.fromSecret(secretKey);
StellarSDK.Keypair.fromSecret(trimmed);
return true;
} catch {
throw new PocketPayError(
'Invalid Stellar secret key',
'Invalid Stellar secret key: failed strkey checksum or payload verification',
'INVALID_SECRET_KEY',
{
validation: {
field: 'secretKey',
reason: 'invalid_format'
// Do NOT include value (secret!)
}
}
reason: 'invalid_format',
},
},
);
}
}
Expand Down
89 changes: 86 additions & 3 deletions src/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,97 @@ export function createWallet(): WalletKeypair {
*/
export function importWallet(secretKey: string): WalletKeypair {
validateSecretKey(secretKey);
const kp = StellarSDK.Keypair.fromSecret(secretKey);
return { publicKey: kp.publicKey(), secretKey: kp.secret() };
const trimmed = secretKey.trim();
try {
const kp = StellarSDK.Keypair.fromSecret(trimmed);
return { publicKey: kp.publicKey(), secretKey: kp.secret() };
} catch (error) {
if (error instanceof PocketPayError) {
throw error;
}
throw new PocketPayError(
'Invalid Stellar secret key',
'INVALID_SECRET_KEY',
{
validation: {
field: 'secretKey',
reason: 'invalid_format',
},
cause: error instanceof Error ? error : undefined,
},
);
}
}

/** Derives the public key from a secret key. */
export function getPublicKey(secretKey: string): string {
validateSecretKey(secretKey);
return StellarSDK.Keypair.fromSecret(secretKey).publicKey();
const trimmed = secretKey.trim();
return StellarSDK.Keypair.fromSecret(trimmed).publicKey();
}

/**
* Safely imports a wallet from a secret key without throwing.
*
* @param secretKey - Stellar secret key (S...) to import
* @returns A {@link PocketPayResult} with either the {@link WalletKeypair} or a {@link PocketPayError}
*/
export function safeImportWallet(secretKey: string): PocketPayResult<WalletKeypair> {
try {
const wallet = importWallet(secretKey);
return toSuccessResult(wallet);
} catch (error) {
const pocketErr =
error instanceof PocketPayError
? error
: wrapError(error, 'Failed to import wallet', 'INVALID_SECRET_KEY');
return toFailureResult(pocketErr);
}
}

/**
* Imports an existing wallet with an enriched result containing warnings and recovery hints.
*
* @param secretKey - Stellar secret key (S...) to import
* @returns An {@link EnhancedPocketPayResult} with the imported wallet or failure details & recovery hints
*/
export function enhancedImportWallet(
secretKey: string,
): EnhancedPocketPayResult<WalletKeypair> {
const warnings: ResultWarning[] = [];
const recoveryHints: RecoveryHint[] = [];

try {
const wallet = importWallet(secretKey);
return toEnhancedSuccessResult(wallet, warnings, recoveryHints);
} catch (error) {
const pocketErr =
error instanceof PocketPayError
? error
: wrapError(error, 'Failed to import wallet', 'INVALID_SECRET_KEY');

if (pocketErr.code === 'INVALID_SECRET_KEY') {
recoveryHints.push({
action: 'check_input',
message: 'Ensure the secret key is a valid 56-character Stellar secret key starting with S.',
retryable: false,
});
}

return toEnhancedFailureResult(pocketErr, warnings, recoveryHints);
}
}

/**
* Non-throwing wrapper for {@link enhancedImportWallet}.
*
* @param secretKey - Stellar secret key (S...) to import
* @returns An enriched result that never throws
*/
export function safeEnhancedImportWallet(
secretKey: string,
): EnhancedPocketPayResult<WalletKeypair> {
return enhancedImportWallet(secretKey);
}

/**
Expand Down
Loading