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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,25 @@ try {
}
```

### Stellar `.wraith` Name Resolution

`resolveStellarName` simulates `resolve(name)` against the configured Soroban `wraith-names` contract and decodes the 64-byte `(spendingPubKey || viewingPubKey)` payload into a validated `st:xlm:...` stealth meta-address. Both `@wraith-protocol/sdk-react` and `@wraith-protocol/sdk-svelte` delegate `useStellarName` to this core resolver with `AbortSignal` cancellation and typed errors.

- **Name normalization (`normalizeStellarName`)**: Strips a trailing `.wraith` suffix and validates `3..=32` characters, lowercase ASCII alphanumeric labels (`a-z`, `0-9`), and at most one subdomain level (`sub.parent`). Malformed inputs reject before any RPC call with `InvalidNameError`.
- **Network semantics**: Accepts `'testnet'` or `'stellar'` (pointing to the configured Stellar testnet deployment). Requesting an unconfigured network or a mismatched `networkPassphrase` rejects with `WalletWrongNetworkError`.
- **Cache semantics**: Positive resolutions (`st:xlm:...`) are cached in memory for `DEFAULT_STELLAR_NAME_TTL_MS` (5 minutes) keyed by `network:rpcUrl:contractId:normalizedName`. Unregistered names (`null`) and errors are never cached so newly registered names resolve immediately. Pass `{ bypassCache: true }`, `{ ttlMs: 0 }`, or call `clearStellarNameCache()` to force a fresh Soroban RPC read.
- **Cancellation**: Pass `{ signal }` to cancel an in-flight simulation; aborted requests reject with `RPCAbortedError`.

```ts
import { resolveStellarName, clearStellarNameCache } from '@wraith-protocol/sdk/chains/stellar';

const metaAddress = await resolveStellarName('alice.wraith', 'testnet', {
bypassCache: false,
signal: controller.signal,
});
// => "st:xlm:..." or null if unregistered
```

## CKB (Nervos) Stealth Addresses

Stealth address cryptography for CKB using secp256k1 with blake2b hashing. CKB uses a UTXO-based Cell model where the Cell itself is the announcement. No separate announcer contract needed.
Expand Down
29 changes: 29 additions & 0 deletions etc/sdk-stellar.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ export function checkStealthAddress(ephemeralPubKey: Uint8Array, viewingKey: Uin
// @public
export function clearAssetMetadataCache(): void;

// @public
export function clearStellarNameCache(): void;

// @public
export function computeAnnouncementViewTag(ephemeralPubKey: Uint8Array, viewingPubKey: Uint8Array): number;

Expand Down Expand Up @@ -325,6 +328,9 @@ export const DEFAULT_BASE_FEE = 100;
// @public
export const DEFAULT_BATCH_SENDER_THRESHOLD = 10;

// @public
export const DEFAULT_STELLAR_NAME_TTL_MS: number;

// @public
export const DEPLOYMENTS: Record<string, StellarChainDeployment>;

Expand Down Expand Up @@ -438,6 +444,9 @@ export function getAssetMetadataResult(contractId: string, network?: Network, op
// @public
export function getDeployment(chain: string): StellarChainDeployment;

// @public
export function getStellarNameDefaultTtl(): number;

// @public
export const HASH_MEMO_BYTES = 32;

Expand Down Expand Up @@ -552,6 +561,9 @@ export const META_ADDRESS_PREFIX = "st:xlm:";
// @public
export type Network = 'testnet' | 'mainnet';

// @public
export function normalizeStellarName(name: string): string;

// Warning: (ae-internal-missing-underscore) The name "parseAnnouncementEvent" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal
Expand Down Expand Up @@ -581,6 +593,20 @@ export interface RequestTimeouts {
requestMs?: number;
}

// @public
export function resolveStellarName(name: string, chain?: string, options?: ResolveStellarNameOptions): Promise<string | null>;

// @public
export interface ResolveStellarNameOptions {
bypassCache?: boolean;
contractId?: string;
networkPassphrase?: string;
rpcUrl?: string;
signal?: AbortSignal;
throwOnNotFound?: boolean;
ttlMs?: number;
}

// @public (undocumented)
export class RetentionExceededError extends Error {
constructor(requestedLedger: number, oldestAvailableLedger: number);
Expand Down Expand Up @@ -677,6 +703,9 @@ export const SCHEME_ID_V2 = 2;
// @public
export function seedToScalar(seed: Uint8Array): bigint;

// @public
export function setStellarNameDefaultTtl(ttlMs: number): void;

// @public
export function signStellarTransaction(transactionHash: Uint8Array, stealthScalar: bigint, stealthPubKey: Uint8Array): Uint8Array;

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@
"name": "Stellar ESM (import *)",
"path": "dist/chains/stellar/index.js",
"import": "*",
"limit": "31 KB"
"limit": "33 KB"
},
{
"name": "Stellar CJS (require)",
"path": "dist/chains/stellar/index.cjs",
"limit": "39.5 KB"
"limit": "41 KB"
}
],
"dependencies": {
Expand Down
82 changes: 68 additions & 14 deletions packages/sdk-react/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
deriveStealthKeys,
deriveStealthKeysFromSigner,
buildStealthPayment,
getDeployment,
resolveStellarName,
type StealthKeys,
type StellarStealthSigner,
type BuildStealthPaymentOptions,
type ResolveStellarNameOptions,
} from '@wraith-protocol/sdk/chains/stellar';
import { Horizon } from '@stellar/stellar-sdk';
import { getUnknownErrorMessage } from '@wraith-protocol/sdk';
import { getPlatform, setPlatform, type Platform } from './platform';

export { getPlatform, setPlatform };
Expand Down Expand Up @@ -48,8 +50,8 @@ export function useStellarSendStealthPayment() {
try {
const result = await buildStealthPayment(options);
return result;
} catch (err: any) {
setError(err);
} catch (err) {
setError(err instanceof Error ? err : new Error(getUnknownErrorMessage(err)));
throw err;
} finally {
setBuilding(false);
Expand All @@ -74,7 +76,7 @@ export function useStellarBalance(publicKey: string, rpcUrl = 'https://horizon.s
.loadAccount(publicKey)
.then((acc) => {
if (!mounted) return;
const xlmBalance = acc.balances.find((b: any) => b.asset_type === 'native');
const xlmBalance = acc.balances.find((balance) => balance.asset_type === 'native');
setBalance(xlmBalance ? xlmBalance.balance : '0');
})
.catch(() => {
Expand All @@ -92,29 +94,69 @@ export function useStellarBalance(publicKey: string, rpcUrl = 'https://horizon.s
return { balance, loading };
}

export function useStellarName(
name: string | undefined,
chain?: string,
options?: ResolveStellarNameOptions,
): { address: string | null; loading: boolean; error: Error | null };
/** Hook to resolve a Wraith name on Stellar */
export function useStellarName(name: string | undefined, chain = 'testnet') {
export function useStellarName(name: string | undefined, chain = 'stellar') {
const [address, setAddress] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// eslint-disable-next-line prefer-rest-params
const options = arguments[2] as ResolveStellarNameOptions | undefined;
const rpcUrl = options?.rpcUrl;
const contractId = options?.contractId;
const networkPassphrase = options?.networkPassphrase;
const bypassCache = options?.bypassCache;
const ttlMs = options?.ttlMs;
const throwOnNotFound = options?.throwOnNotFound;
const callerSignal = options?.signal;

useEffect(() => {
if (!name) return;
if (name === undefined) {
setAddress(null);
setLoading(false);
setError(null);
return;
}
let mounted = true;
const controller = new AbortController();
const onCallerAbort = () => controller.abort(callerSignal?.reason);

if (callerSignal) {
if (callerSignal.aborted) {
controller.abort(callerSignal.reason);
} else {
callerSignal.addEventListener('abort', onCallerAbort, { once: true });
}
}

async function resolve() {
setLoading(true);
setError(null);
setAddress(null);
try {
const deployment = getDeployment(chain);
// Stub for resolving name using the names contract.
// Integrators will fill in with actual logic if they need it,
// or this can be updated once the resolve logic is added to the SDK.
await new Promise((resolve) => setTimeout(resolve, 500));
const resolved = await resolveStellarName(name!, chain, {
rpcUrl,
contractId,
networkPassphrase,
bypassCache,
ttlMs,
throwOnNotFound,
signal: controller.signal,
});
if (mounted && resolved !== null) {
setAddress(resolved);
return;
}
if (!mounted) return;
setAddress(null); // Return null or resolved meta-address
} catch (err: any) {
if (mounted) setError(err);
} catch (err) {
if (mounted) {
setError(err instanceof Error ? err : new Error(getUnknownErrorMessage(err)));
}
} finally {
if (mounted) setLoading(false);
}
Expand All @@ -123,8 +165,20 @@ export function useStellarName(name: string | undefined, chain = 'testnet') {
resolve();
return () => {
mounted = false;
callerSignal?.removeEventListener('abort', onCallerAbort);
controller.abort();
};
}, [name, chain]);
}, [
name,
chain,
rpcUrl,
contractId,
networkPassphrase,
bypassCache,
ttlMs,
throwOnNotFound,
callerSignal,
]);

return { address, loading, error };
}
Loading
Loading