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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist
node_modules
reference
pnpm-lock.yaml
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ See [MIGRATING.md](./MIGRATING.md) for breaking changes and migration steps when
| `@wraith-protocol/sdk/chains/stellar` | Stellar stealth address crypto (ed25519) |
| `@wraith-protocol/sdk/chains/solana` | Solana stealth address crypto (ed25519) |
| `@wraith-protocol/sdk/chains/ckb` | CKB (Nervos) stealth address crypto (secp256k1) |
| `@wraith-protocol/sdk/vault` | Browser-only passphrase vault for short-lived keys |
| `@wraith-protocol/sdk/vault` | Browser-only passphrase vault for short-lived keys |

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

Expand Down
2 changes: 1 addition & 1 deletion examples/react-native-stellar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@stellar/stellar-sdk": "^13.1.0",
"buffer": "^6.0.3",
"expo": "~51.0.0",
"expo-crypto": "~14.2.0",
"expo-crypto": "~13.0.2",
"react": "18.3.1",
"react-native": "0.72.4",
"react-native-get-random-values": "^1.0.0"
Expand Down
12 changes: 12 additions & 0 deletions examples/react-stellar-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stellar Stealth App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
25 changes: 25 additions & 0 deletions examples/react-stellar-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "react-stellar-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"@wraith-protocol/sdk": "workspace:*",
"@wraith-protocol/sdk-react": "workspace:*",
"@stellar/stellar-sdk": "^13.1.0"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.3",
"vite": "^5.4.1"
}
}
38 changes: 38 additions & 0 deletions examples/react-stellar-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import {
useStellarStealthKeys,
useStellarBalance,
useStellarName,
} from '@wraith-protocol/sdk-react';

function App() {
const { keys, generate } = useStellarStealthKeys();
const { balance, loading: balLoading } = useStellarBalance(
'GAXPQRUTZQOXGBF3NBBWY43K5YUTYMMW3SBRV3L4YJ6ZJWWX4VHYQQX4',
);
const { address, loading: nameLoading } = useStellarName('alice.wraith');

return (
<div style={{ padding: '20px', fontFamily: 'system-ui' }}>
<h1>Stellar Stealth App</h1>

<div style={{ marginBottom: '20px' }}>
<h2>Stealth Keys</h2>
<button onClick={() => generate(new Uint8Array(32).fill(1))}>Generate Mock Keys</button>
{keys && <p style={{ color: 'green' }}>Keys generated successfully!</p>}
</div>

<div style={{ marginBottom: '20px' }}>
<h2>Balance</h2>
<p>Balance: {balLoading ? 'Loading...' : `${balance} XLM`}</p>
</div>

<div style={{ marginBottom: '20px' }}>
<h2>Name Resolution</h2>
<p>alice.wraith resolved address: {nameLoading ? 'Loading...' : address || 'Not found'}</p>
</div>
</div>
);
}

export default App;
9 changes: 9 additions & 0 deletions examples/react-stellar-app/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
15 changes: 15 additions & 0 deletions examples/react-stellar-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src"]
}
7 changes: 7 additions & 0 deletions examples/react-stellar-app/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});
39 changes: 39 additions & 0 deletions packages/sdk-react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@wraith-protocol/sdk-react",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"test": "vitest run",
"test:watch": "vitest"
},
"peerDependencies": {
"@stellar/stellar-sdk": "^13.1.0",
"@wraith-protocol/sdk": "workspace:*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"tsup": "^8.0.0",
"typescript": "^5.0.0",
"vitest": "^3.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/jest-dom": "^6.0.0",
"jsdom": "^24.0.0"
}
}
10 changes: 10 additions & 0 deletions packages/sdk-react/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
declare module '@wraith-protocol/sdk/chains/stellar' {
export const deriveStealthKeys: any;
export const fetchAnnouncements: any;
export const buildStealthPayment: any;
export const getDeployment: any;
export type StealthKeys = any;
export type FetchAnnouncementsOptions = any;
export type Announcement = any;
export type BuildStealthPaymentOptions = any;
}
141 changes: 141 additions & 0 deletions packages/sdk-react/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { useState, useCallback, useEffect } from 'react';
import {
deriveStealthKeys,
fetchAnnouncements,
buildStealthPayment,
getDeployment,
type StealthKeys,
type FetchAnnouncementsOptions,
type Announcement,
type BuildStealthPaymentOptions,
} from '@wraith-protocol/sdk/chains/stellar';
import { Horizon } from '@stellar/stellar-sdk';

/** Hook to generate and manage stealth keys */
export function useStellarStealthKeys() {
const [keys, setKeys] = useState<StealthKeys | null>(null);

const generate = useCallback((signature: Uint8Array) => {
const derived = deriveStealthKeys(signature);
setKeys(derived);
return derived;
}, []);

return { keys, generate };
}

/** Hook to scan for announcements */
export function useStellarAnnouncementScan() {
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [scanning, setScanning] = useState(false);
const [error, setError] = useState<Error | null>(null);

const scan = useCallback(async (options: FetchAnnouncementsOptions) => {
setScanning(true);
setError(null);
try {
const result = await fetchAnnouncements(options);
setAnnouncements(result.announcements);
return result.announcements;
} catch (err: any) {
setError(err);
throw err;
} finally {
setScanning(false);
}
}, []);

return { announcements, scanning, error, scan };
}

/** Hook to send stealth payments */
export function useStellarSendStealthPayment() {
const [building, setBuilding] = useState(false);
const [error, setError] = useState<Error | null>(null);

const build = useCallback(async (options: BuildStealthPaymentOptions) => {
setBuilding(true);
setError(null);
try {
const result = await buildStealthPayment(options);
return result;
} catch (err: any) {
setError(err);
throw err;
} finally {
setBuilding(false);
}
}, []);

return { building, error, build };
}

/** Hook to get stellar balance */
export function useStellarBalance(publicKey: string, rpcUrl = 'https://horizon.stellar.org') {
const [balance, setBalance] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

useEffect(() => {
if (!publicKey) return;
let mounted = true;
setLoading(true);

const server = new Horizon.Server(rpcUrl);
server
.loadAccount(publicKey)
.then((acc) => {
if (!mounted) return;
const xlmBalance = acc.balances.find((b: any) => b.asset_type === 'native');
setBalance(xlmBalance ? xlmBalance.balance : '0');
})
.catch(() => {
if (mounted) setBalance('0');
})
.finally(() => {
if (mounted) setLoading(false);
});

return () => {
mounted = false;
};
}, [publicKey, rpcUrl]);

return { balance, loading };
}

/** Hook to resolve a Wraith name on Stellar */
export function useStellarName(name: string | undefined, chain = 'testnet') {
const [address, setAddress] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
if (!name) return;
let mounted = true;

async function resolve() {
setLoading(true);
setError(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));
if (!mounted) return;
setAddress(null); // Return null or resolved meta-address
} catch (err: any) {
if (mounted) setError(err);
} finally {
if (mounted) setLoading(false);
}
}

resolve();
return () => {
mounted = false;
};
}, [name, chain]);

return { address, loading, error };
}
1 change: 1 addition & 0 deletions packages/sdk-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './hooks';
62 changes: 62 additions & 0 deletions packages/sdk-react/test/hooks.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// @vitest-environment jsdom
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { useStellarStealthKeys, useStellarBalance } from '../src/hooks';
import { deriveStealthKeys } from '@wraith-protocol/sdk/chains/stellar';

vi.mock('@wraith-protocol/sdk/chains/stellar', async () => {
const original: any = await vi.importActual('@wraith-protocol/sdk/chains/stellar');
return {
...original,
deriveStealthKeys: vi.fn(() => ({ viewPrivateKey: 'vpk', spendPrivateKey: 'spk' })),
fetchAnnouncements: vi.fn(() => Promise.resolve({ announcements: [] })),
buildStealthPayment: vi.fn(() => Promise.resolve({})),
getDeployment: vi.fn(() => ({ rpcUrl: 'https://rpc', contracts: { names: 'foo' } })),
};
});

vi.mock('@stellar/stellar-sdk', async () => {
const original: any = await vi.importActual('@stellar/stellar-sdk');
return {
...original,
Horizon: {
Server: vi.fn().mockImplementation(() => ({
loadAccount: vi.fn().mockResolvedValue({
balances: [{ asset_type: 'native', balance: '10.5' }],
}),
})),
},
};
});

describe('useStellarStealthKeys', () => {
it('generates keys correctly', () => {
const { result } = renderHook(() => useStellarStealthKeys());

expect(result.current.keys).toBeNull();

act(() => {
result.current.generate(new Uint8Array([1, 2, 3]));
});

expect(result.current.keys).toEqual({ viewPrivateKey: 'vpk', spendPrivateKey: 'spk' });
expect(deriveStealthKeys).toHaveBeenCalled();
});
});

describe('useStellarBalance', () => {
it('fetches balance correctly', async () => {
const { result } = renderHook(() => useStellarBalance('GDQ...'));

expect(result.current.loading).toBe(true);
expect(result.current.balance).toBeNull();

// wait for effect to resolve
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});

expect(result.current.loading).toBe(false);
expect(result.current.balance).toBe('10.5');
});
});
1 change: 1 addition & 0 deletions packages/sdk-react/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
Loading