This guide documents breaking changes to @wraith-protocol/sdk and how to update your code.
Before working through the manual steps below, try the codemod -- it automates the mechanical parts of each breaking change (import updates, message-matching error handlers, the React Native polyfill call) so you don't have to grep-and-sed by hand.
npx @wraith-protocol/codemod v1 ./srcRun it with --dry --print first if you want to preview the diff without
writing anything:
npx @wraith-protocol/codemod v1 ./src --dry --printIt's safe to run more than once -- files that are already migrated, or that don't match a known pattern, are left untouched.
What it handles automatically:
- Rewrites
catch (e) { if (e.message.includes('...')) }message-matching intoe instanceof <TypedError>checks, and adds the required import (see Error Handling below). - Inserts the
installReactNativePolyfills()call and import into React Native entry files that need it (see React Native below).
What still needs a manual look: the codemod only rewrites .message.includes(...)
checks against message fragments it recognizes as belonging to a specific
@wraith-protocol/sdk error class. If your code matches against custom or
already-changed message text, or combines multiple .message.includes(...)
checks with ||/&& in a single condition, review those call sites by hand
using the reference below. The Stellar cryptographic audit fixes require no
code changes at all (automated or manual) -- see that section for details.
Source lives in packages/codemod, including the fixture
pre/post pairs each transform is tested against.
Rationale: The SDK now exports a hierarchy of typed error classes, allowing programmatic error handling without brittle error.message string matching. This change improves maintainability and enables richer error context like expectedLength, actualLength, code, and auto-generated docsLink.
Before (1.4.x):
try {
const sig = '0x...'; // Wrong length
deriveStealthKeys(sig);
} catch (e) {
if (e.message.includes('Expected 65-byte signature')) {
console.log('Signature length mismatch');
}
}After (1.5.0+):
import { InvalidSignatureError, KeyDerivationFailedError } from '@wraith-protocol/sdk';
try {
const sig = '0x...'; // Wrong length
deriveStealthKeys(sig);
} catch (e) {
if (e instanceof InvalidSignatureError) {
console.log(`Expected ${e.context.expectedLength}, got ${e.context.actualLength}`);
console.log(`Docs: ${e.docsLink}`);
} else if (e instanceof KeyDerivationFailedError) {
console.log(`Key derivation failed: ${e.context.reason}`);
}
}Affected Error Types:
All cryptographic and network operations may now throw typed exceptions. Import and use these base or specific error classes:
- Base classes:
WraithError,WraithInputError,WraithCryptoError,WraithNetworkError,WraithContractError,WraithBuilderError - Input validation:
InvalidMetaAddressError,InvalidNameError,InvalidSignatureError,InvalidScalarError - Cryptography:
KeyDerivationFailedError,ViewTagMismatchError,ECDHFailedError - Network:
RPCRequestError,RPCRetryExhaustedError,RPCTimeoutError,RetentionExceededError - Contracts:
NameNotFoundError,NameAlreadyRegisteredError,InsufficientAuthError,ContractRevertError - Builders:
InsufficientBalanceError,UnsupportedAssetError
All are exported from the SDK root:
import {
WraithError,
InvalidMetaAddressError,
// ... other error types
} from '@wraith-protocol/sdk';Rationale: The independent cryptographic audit of the Stellar chain module identified two medium-severity findings in scalar handling that required immediate fixes:
- Zero-scalar rejection: Derived scalars must never be zero (security constraint).
- Signature verification: Stellar transaction signatures must be verified with the correct public key.
These fixes are cryptographically sound but behaviorally breaking if your code relied on edge-case scalars.
Before (1.4.x):
import { scanAnnouncements } from '@wraith-protocol/sdk/chains/stellar';
const matched = scanAnnouncements(announcements, viewingKey, spendingPubKey, spendingScalar);
// All announcements with matching view tags were included, even if (spendingScalar + hashScalar) % L === 0After (1.5.0+):
import { scanAnnouncements } from '@wraith-protocol/sdk/chains/stellar';
const matched = scanAnnouncements(announcements, viewingKey, spendingPubKey, spendingScalar);
// Announcements that would result in stealthPrivateScalar <= 0 are silently skipped
// This is extremely rare (probability ~1 in 2^255) but now cryptographically correctMigration: No code change required unless you were explicitly handling or testing zero-scalar edge cases. If you have test fixtures that generate announcements with zero-scalar outcomes, regenerate them.
Before (1.4.x):
const result = checkStealthAddress(ephemeralPubKey, viewingKey, spendingPubKey, viewTag);
// View tag was computed from shared secret derived from (viewingKey ECDH ephemeralPubKey)After (1.5.0+):
const result = checkStealthAddress(ephemeralPubKey, viewingKey, spendingPubKey, viewTag);
// View tag is now computed from the ephemeralPubKey and viewingPubKey directly (prefilter optimization)
// Functionally identical, but 1.5–2x faster for large announcement batchesMigration: No code change required. The function signature is identical; only the internal computation is optimized. Existing callers and test vectors remain valid.
Rationale: React Native environments lack native crypto APIs and WebAssembly support. The SDK now provides opt-in polyfills instead of automatically patching globals, which avoids surprising bundlers and test environments.
Before (1.4.x):
// On React Native, crypto functions "just worked" because the SDK automatically
// installed polyfills when imported. This surprised some bundlers.
import { scanAnnouncements } from '@wraith-protocol/sdk/chains/stellar';
const matched = scanAnnouncements(...);After (1.5.0+):
// React Native apps MUST call this once at app startup
import { installReactNativePolyfills } from '@wraith-protocol/sdk';
installReactNativePolyfills();
// Now use the SDK normally
import { scanAnnouncements } from '@wraith-protocol/sdk/chains/stellar';
const matched = scanAnnouncements(...);Where to Call:
Add the polyfill installation early in your app entry point (before any crypto imports):
Expo + TypeScript:
// App.tsx
import { installReactNativePolyfills } from '@wraith-protocol/sdk';
// Call this once at app start, before importing Stellar/Solana modules
installReactNativePolyfills();
export default function App() {
// ... your app
}React Native (Bare Workflow):
// index.ts (or index.js)
import { installReactNativePolyfills } from '@wraith-protocol/sdk';
installReactNativePolyfills();
import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('MyApp', () => App);If Not Using React Native:
No change needed. The polyfill function is a no-op in Node.js and browser environments.
createHorizonClient() and createRpcClient() used to wait for a response indefinitely. Each attempt now fails after 10 seconds without response headers or 30 seconds in total, and is retried or failed over like a network error. When every attempt fails, the client throws RPCRetryExhaustedError with an RPCTimeoutError on its cause.
Pass longer timeouts for calls that are slow on purpose, such as Horizon transaction submission, or 0 to turn a timeout off:
const horizon = createHorizonClient({
horizonUrl: 'https://horizon.stellar.org',
timeouts: { connectMs: 0, requestMs: 0 }, // the pre-2.0 behaviour
});See Stellar Horizon and RPC request timeouts.
The SDK now includes a ScannerPool utility for efficient multichain scanning. While this is an additive feature (no breaking changes), be aware of the new export:
import { ScannerPool } from '@wraith-protocol/sdk';
// Scan multiple chains in parallel with bounded concurrency
const pool = new ScannerPool({ concurrency: 4 });
const results = await pool.scan([
{
chain: 'ethereum',
announcements,
viewingKey,
spendingPubKey,
spendingScalar,
},
{
chain: 'stellar',
announcements,
viewingKey,
spendingPubKey,
spendingScalar,
},
]);This is fully backward-compatible; existing code using individual chain scan functions continues to work.
For detailed documentation on each error type, including context fields and resolution steps, see docs/errors.md.
InvalidMetaAddressError
import { InvalidMetaAddressError } from '@wraith-protocol/sdk';
try {
const decoded = decodeStealthMetaAddress('invalid-format');
} catch (e) {
if (e instanceof InvalidMetaAddressError) {
console.log(`Invalid meta-address: ${e.context.metaAddress}`);
console.log(`Reason: ${e.context.reason}`);
// Check prefix ('st:eth:', 'st:xlm:', etc.) and total length
}
}InvalidSignatureError
import { InvalidSignatureError } from '@wraith-protocol/sdk';
try {
const keys = deriveStealthKeys(signature);
} catch (e) {
if (e instanceof InvalidSignatureError) {
console.log(`Expected: ${e.context.expectedLength} bytes`);
console.log(`Got: ${e.context.actualLength} bytes`);
// Ensure signature is exactly 65 bytes (EVM) or 64 bytes (Stellar)
}
}KeyDerivationFailedError
import { KeyDerivationFailedError } from '@wraith-protocol/sdk';
try {
const keys = deriveStealthKeys(signature);
} catch (e) {
if (e instanceof KeyDerivationFailedError) {
console.log(`Key derivation failed: ${e.context.reason}`);
// Usually means signature format is invalid or cryptographic operation failed
}
}If you have tests that mock errors or check error messages:
Before:
test('rejects invalid signature', async () => {
expect(() => deriveStealthKeys('0x...')).toThrow(/Expected 65-byte signature/);
});After:
import { InvalidSignatureError } from '@wraith-protocol/sdk';
test('rejects invalid signature', async () => {
expect(() => deriveStealthKeys('0x...')).toThrow(InvalidSignatureError);
});- Error Documentation: https://docs.wraith.dev/sdk/errors
- GitHub Issues: https://github.com/wraith-protocol/sdk/issues
- Contributing: See CONTRIBUTING.md for the semver policy and deprecation rules.