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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the SDK semver policy, deprecation
| `@wraith-protocol/sdk/chains/solana` | Solana stealth address crypto (ed25519) |
| `@wraith-protocol/sdk/chains/ckb` | CKB (Nervos) stealth address crypto (secp256k1) |

> React Native support is documented in `docs/guides/react-native-setup.mdx` and the companion example at `examples/react-native-stellar`.

## Agent Client

The root export provides `Wraith` and `WraithAgent` — a lightweight HTTP client for the Wraith managed TEE platform.
Expand Down
93 changes: 93 additions & 0 deletions docs/guides/react-native-setup.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: React Native Setup
---

# React Native Setup for `@wraith-protocol/sdk`

This guide explains how to use `@wraith-protocol/sdk/chains/stellar` in Expo React Native apps, including the minimal polyfills required for RN environments.

## Why this is necessary

React Native does not provide all browser or Node globals by default. The Stellar chain code in `@wraith-protocol/sdk` requires:

- `crypto.getRandomValues` for ed25519 key generation via `@noble/curves`
- `TextEncoder` / `TextDecoder` for deterministic hashing and serialization
- `atob` / `btoa` for some Stellar SDK compatibility paths
- `Buffer` if the app imports `@stellar/stellar-sdk` directly

## What was fixed in the SDK

The SDK now includes the following compatibility improvements:

- `pubKeyToStellarAddress()` now uses a native StrKey encoder instead of `@stellar/stellar-sdk`, reducing runtime dependence on Node-style buffers.
- A helper export is exposed from the package root:
- `installReactNativePolyfills()`
- Root package metadata now includes `main`, `module`, and `react-native` fields to improve Metro resolution.

## Recommended React Native setup

### 1. Install runtime dependencies

```bash
pnpm add expo expo-crypto react-native-get-random-values buffer @stellar/stellar-sdk
```

### 2. Import polyfills before using the SDK

In your app entry point (`App.tsx` or `index.ts`):

```ts
import 'react-native-get-random-values';
import { Buffer } from 'buffer';

if (globalThis.Buffer == null) {
(globalThis as any).Buffer = Buffer;
}

if (typeof globalThis.atob === 'undefined') {
globalThis.atob = (input: string) => {
return Buffer.from(input, 'base64').toString('binary');
};
}

if (typeof globalThis.btoa === 'undefined') {
globalThis.btoa = (input: string) => {
return Buffer.from(input, 'binary').toString('base64');
};
}
```

Then import the SDK and optionally install the package helper after app polyfills are in place:

```ts
import { installReactNativePolyfills } from '@wraith-protocol/sdk';
import {
deriveStealthKeys,
generateStealthAddress,
scanAnnouncements,
} from '@wraith-protocol/sdk/chains/stellar';

installReactNativePolyfills();
```

## Example application

A full example is available at `examples/react-native-stellar` in this repository.

It demonstrates:

- deriving stealth keys from a 64-byte signature
- generating a one-time Stellar stealth address
- scanning a fixture announcement

## Root cause breakdown

- `crypto.getRandomValues` missing: belongs in app entry point / runtime polyfill package
- `Buffer` missing: belongs in app entry point / runtime polyfill package
- `atob` / `btoa` missing: belongs in app entry point / runtime polyfill package
- `StrKey` runtime dependency on `@stellar/stellar-sdk`: fixed in SDK itself by adding a pure JS Stellar address encoder

## Notes

- New Expo SDK 51+ and Hermes 0.74+ include `TextEncoder` / `TextDecoder`, but the package also now includes a fallback polyfill helper for older RN engines.
- This guide is intentionally minimal: the SDK itself is now cross-platform out of the box for Stellar core cryptography, with only standard RN polyfills required in the app.
92 changes: 92 additions & 0 deletions examples/react-native-stellar/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import './polyfills';
import React, { useMemo } from 'react';
import { SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native';
import {
deriveStealthKeys,
generateStealthAddress,
scanAnnouncements,
} from '@wraith-protocol/sdk/chains/stellar';

function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}

const sampleSignature = new Uint8Array(Array.from({ length: 64 }, (_, i) => i + 1));

const announcementsFixture = (stealthAddress: string, ephemeralPubKey: Uint8Array, viewTag: number) => [
{
schemeId: 1,
stealthAddress,
caller: 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF',
ephemeralPubKey: bytesToHex(ephemeralPubKey),
metadata: `0x${bytesToHex(new Uint8Array([viewTag]))}`,
},
];

export default function App() {
const { keys, stealth, matches } = useMemo(() => {
const keys = deriveStealthKeys(sampleSignature);
const stealth = generateStealthAddress(keys.spendingPubKey, keys.viewingPubKey, new Uint8Array(32).fill(0x42));
const announcements = announcementsFixture(stealth.stealthAddress, stealth.ephemeralPubKey, stealth.viewTag);
const matches = scanAnnouncements(announcements, keys.viewingKey, keys.spendingPubKey, keys.spendingScalar);
return { keys, stealth, matches };
}, []);

return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.title}>Wraith React Native Stellar Example</Text>
<View style={styles.card}>
<Text style={styles.heading}>Derived Keys</Text>
<Text style={styles.value}>Spending public key: {bytesToHex(keys.spendingPubKey)}</Text>
<Text style={styles.value}>Viewing public key: {bytesToHex(keys.viewingPubKey)}</Text>
</View>
<View style={styles.card}>
<Text style={styles.heading}>Generated Stealth Address</Text>
<Text style={styles.value}>{stealth.stealthAddress}</Text>
<Text style={styles.value}>View Tag: {stealth.viewTag}</Text>
</View>
<View style={styles.card}>
<Text style={styles.heading}>Scan Result</Text>
<Text style={styles.value}>Matches found: {matches.length}</Text>
<Text style={styles.value}>{matches.length > 0 ? matches[0].stealthAddress : 'none'}</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0A1222',
},
content: {
padding: 20,
},
title: {
fontSize: 24,
fontWeight: '700',
color: '#ffffff',
marginBottom: 20,
},
card: {
marginBottom: 16,
padding: 16,
backgroundColor: '#111C38',
borderRadius: 12,
},
heading: {
fontSize: 18,
fontWeight: '700',
color: '#E0E7FF',
marginBottom: 8,
},
value: {
color: '#C1C9FF',
marginBottom: 4,
fontSize: 14,
},
});
9 changes: 9 additions & 0 deletions examples/react-native-stellar/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"expo": {
"name": "Wraith Stellar React Native Example",
"slug": "wraith-stellar-react-native",
"sdkVersion": "51.0.0",
"platforms": ["ios", "android"],
"assetBundlePatterns": ["**/*"]
}
}
23 changes: 23 additions & 0 deletions examples/react-native-stellar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "wraith-sdk-react-native-stellar",
"private": true,
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios"
},
"dependencies": {
"@wraith-protocol/sdk": "file:../..",
"@stellar/stellar-sdk": "^13.1.0",
"buffer": "^6.0.3",
"expo": "~51.0.0",
"expo-crypto": "~14.2.0",
"react": "18.3.1",
"react-native": "0.72.4",
"react-native-get-random-values": "^1.0.0"
},
"devDependencies": {
"typescript": "^5.7.0"
}
}
48 changes: 48 additions & 0 deletions examples/react-native-stellar/polyfills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'react-native-get-random-values';
import { Buffer } from 'buffer';

if (typeof globalThis.Buffer === 'undefined') {
(globalThis as any).Buffer = Buffer;
}

if (typeof globalThis.atob === 'undefined') {
globalThis.atob = (input: string) => {
const base64 = input.replace(/=+$/, '');
let str = '';
let bc = 0;
let bs = 0;
let buffer;
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

for (let idx = 0; (buffer = base64.charAt(idx++)); ) {
const code = chars.indexOf(buffer);
if (code === -1) continue;
bs = (bs << 6) | code;
bc += 6;
if (bc >= 8) {
bc -= 8;
str += String.fromCharCode((bs >> bc) & 0xff);
}
}

return str;
};
}

if (typeof globalThis.btoa === 'undefined') {
globalThis.btoa = (input: string) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let str = input;
let output = '';

for (let block = 0, charCode, idx = 0, map = chars; str.charAt(idx | 0) || ((map = '='), idx % 1); ) {
charCode = str.charCodeAt((idx += 3 / 4));
if (charCode > 0xff) {
throw new Error('Failed to execute btoa: The string to be encoded contains characters outside of the Latin1 range.');
}
output += map.charAt((block = (block << 8) | charCode) >> ((3.5 - (idx % 1)) * 8) & 0x3f);
}

return output;
};
}
9 changes: 9 additions & 0 deletions examples/react-native-stellar/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"skipLibCheck": true,
"isolatedModules": true
},
"include": ["**/*"]
}
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"name": "@wraith-protocol/sdk",
"version": "1.4.5",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"react-native": "./dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -18,6 +21,11 @@
"import": "./dist/chains/stellar/index.js",
"require": "./dist/chains/stellar/index.cjs"
},
"./compat/react-native": {
"types": "./dist/compat/react-native.d.ts",
"import": "./dist/compat/react-native.js",
"require": "./dist/compat/react-native.cjs"
},
"./chains/solana": {
"types": "./dist/chains/solana/index.d.ts",
"import": "./dist/chains/solana/index.js",
Expand Down
1 change: 1 addition & 0 deletions src/chains/stellar/announcements.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Announcement } from './types';
import { bytesToHex } from './utils';
import { getDeployment } from './deployments';
import { Address, xdr } from '@stellar/stellar-sdk';

let stellarSdkPromise: Promise<typeof import('@stellar/stellar-sdk')> | undefined;

Expand Down
52 changes: 49 additions & 3 deletions src/chains/stellar/scalar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ed25519 } from '@noble/curves/ed25519';
import { sha512 } from '@noble/hashes/sha512';
import { sha256 } from '@noble/hashes/sha256';
import { StrKey } from '@stellar/stellar-sdk';

/**
* ed25519 group order used to reduce Stellar stealth scalars.
Expand Down Expand Up @@ -126,6 +125,42 @@ export function deriveStealthPubKey(spendingPubKey: Uint8Array, hashScalar: bigi
return stealthPoint.toRawBytes();
}

const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const STELLAR_PUBLIC_KEY_VERSION = 6 << 3;

function crc16Xmodem(bytes: Uint8Array): number {
let crc = 0x0000;
for (const b of bytes) {
crc ^= b << 8;
for (let i = 0; i < 8; i++) {
crc = (crc & 0x8000) !== 0 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
}
}
return crc;
}

function base32Encode(bytes: Uint8Array): string {
let bits = 0;
let value = 0;
let output = '';

for (const byte of bytes) {
value = (value << 8) | byte;
bits += 8;

while (bits >= 5) {
output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
bits -= 5;
}
}

if (bits > 0) {
output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
}

return output;
}

/**
* Converts a 32-byte ed25519 public key into a Stellar `G...` address.
*
Expand All @@ -146,8 +181,19 @@ export function deriveStealthPubKey(spendingPubKey: Uint8Array, hashScalar: bigi
* @see {@link deriveStealthPubKey}
*/
export function pubKeyToStellarAddress(pubKeyBytes: Uint8Array): string {
// StrKey typings expect Buffer, but Uint8Array works at runtime
return (StrKey as any).encodeEd25519PublicKey(pubKeyBytes);
if (pubKeyBytes.length !== 32) {
throw new Error(`Expected 32-byte ed25519 public key, got ${pubKeyBytes.length}`);
}

const payload = new Uint8Array(1 + pubKeyBytes.length + 2);
payload[0] = STELLAR_PUBLIC_KEY_VERSION;
payload.set(pubKeyBytes, 1);

const checksum = crc16Xmodem(payload.subarray(0, 33));
payload[33] = checksum & 0xff;
payload[34] = (checksum >> 8) & 0xff;

return base32Encode(payload);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/compat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { installReactNativePolyfills } from './react-native';
Loading