Skip to content
Closed
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 .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pnpm format:check
npm run format:check
25 changes: 18 additions & 7 deletions src/components/AutoSign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,11 @@ function HorizenAutoSign() {
}

function StellarAutoSign() {
const { isConnected, address, signMessage } = useStellarWallet();
const { isConnected, address, network, signMessage } = useStellarWallet();
const { stellarKeys, setStellarKeys, setStellarMetaAddress, clearStellar } = useStealthKeys();
const prompted = useRef<string | null>(null);
const signature = useRef<Uint8Array | null>(null);
const lastAddress = useRef<string | null>(null);
const [ready, setReady] = useState(false);
const isLoading = useRef(false);

Expand All @@ -99,15 +101,22 @@ function StellarAutoSign() {
if (!ready || !address) return;
if (stellarKeys) return;
if (isLoading.current) return;
if (prompted.current === address) return;
if (lastAddress.current !== address) {
prompted.current = null;
signature.current = null;
lastAddress.current = address;
}
const identity = `${address}:${network.id}`;
if (prompted.current === identity) return;

prompted.current = address;
prompted.current = identity;
isLoading.current = true;

(async () => {
try {
const signature = await signMessage(STELLAR_SIGNING_MESSAGE);
const keys = deriveStellarKeys(signature);
const signedMessage = signature.current || (await signMessage(STELLAR_SIGNING_MESSAGE));
signature.current = signedMessage;
const keys = deriveStellarKeys(signedMessage);
const meta = encodeStellarMeta(keys.spendingPubKey, keys.viewingPubKey);
setStellarKeys(keys);
setStellarMetaAddress(meta);
Expand All @@ -117,15 +126,17 @@ function StellarAutoSign() {
isLoading.current = false;
}
})();
}, [ready, address, stellarKeys, signMessage, setStellarKeys, setStellarMetaAddress]);
}, [ready, address, network.id, stellarKeys, signMessage, setStellarKeys, setStellarMetaAddress]);

useEffect(() => {
if (!isConnected) {
prompted.current = null;
signature.current = null;
lastAddress.current = null;
setReady(false);
clearStellar();
}
}, [isConnected, clearStellar]);
}, [isConnected, address, clearStellar]);

return null;
}
Expand Down
41 changes: 19 additions & 22 deletions src/components/StellarReceive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { useStealthKeys } from '@/context/StealthKeysContext';
import { useStellarWallet } from '@/context/StellarWalletContext';
import { CopyButton } from '@/components/CopyButton';
import { stellarTxUrl, stellarAddrUrl } from '@/lib/explorer';
import { STELLAR_NETWORK } from '@/config';

const ANNOUNCER_CONTRACT = 'CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL';
const REGISTRY_CONTRACT = 'CC2LAUCXYOPJ4DV4CYXNXYAXRDVOTMAWFF76W4WFD5OVQBD6TN4PYYJ5';
Expand Down Expand Up @@ -144,6 +143,7 @@ function StellarStealthRow({
match: MatchedAnnouncement;
onWithdrawn: () => void;
}) {
const { network } = useStellarWallet();
const [balance, setBalance] = useState<string | null>(null);
const [loadingBal, setLoadingBal] = useState(true);
const [dest, setDest] = useState('');
Expand All @@ -157,7 +157,7 @@ function StellarStealthRow({
useEffect(() => {
(async () => {
try {
const res = await fetch(`${STELLAR_NETWORK.horizonUrl}/accounts/${match.stealthAddress}`);
const res = await fetch(`${network.horizonUrl}/accounts/${match.stealthAddress}`);
if (!res.ok) {
setBalance('0');
return;
Expand All @@ -171,16 +171,16 @@ function StellarStealthRow({
setLoadingBal(false);
}
})();
}, [match.stealthAddress]);
}, [match.stealthAddress, network.horizonUrl]);

const handleWithdraw = async () => {
if (!dest) return;
setError('');
setWithdrawing(true);

try {
const horizonUrl = STELLAR_NETWORK.horizonUrl;
const networkPassphrase = STELLAR_NETWORK.networkPassphrase;
const horizonUrl = network.horizonUrl;
const networkPassphrase = network.networkPassphrase;

const res = await fetch(`${horizonUrl}/accounts/${match.stealthAddress}`);
if (!res.ok) throw new Error('Account not found');
Expand Down Expand Up @@ -244,7 +244,7 @@ function StellarStealthRow({
</span>
<div className="mt-0.5 flex items-center gap-2">
<a
href={stellarAddrUrl(match.stealthAddress)}
href={stellarAddrUrl(match.stealthAddress, network)}
target="_blank"
rel="noopener noreferrer"
className="block truncate font-mono text-xs text-primary underline"
Expand Down Expand Up @@ -300,7 +300,7 @@ function StellarStealthRow({
<span className="font-mono text-[10px] text-on-surface-variant">
Withdrawn —{' '}
<a
href={stellarTxUrl(withdrawHash)}
href={stellarTxUrl(withdrawHash, network)}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
Expand Down Expand Up @@ -336,7 +336,7 @@ function StellarStealthRow({
}

export function StellarReceive() {
const { address, isConnected, signMessage, signTransaction } = useStellarWallet();
const { address, isConnected, network, signMessage, signTransaction } = useStellarWallet();
const { stellarKeys, stellarMetaAddress, setStellarKeys, setStellarMetaAddress } =
useStealthKeys();

Expand All @@ -357,8 +357,8 @@ export function StellarReceive() {
(async () => {
try {
const { rpc: rpcMod } = await import('@stellar/stellar-sdk');
const soroban = new rpcMod.Server(STELLAR_NETWORK.rpcUrl);
const networkPassphrase = STELLAR_NETWORK.networkPassphrase;
const soroban = new rpcMod.Server(network.rpcUrl);
const networkPassphrase = network.networkPassphrase;

const accountResponse = await soroban.getAccount(address);
const sourceAccount = new Account(
Expand Down Expand Up @@ -386,7 +386,7 @@ export function StellarReceive() {
// Not registered or contract not available
}
})();
}, [address]);
}, [address, network]);

const registered = isAlreadyRegistered || isRegSuccess;

Expand All @@ -412,8 +412,8 @@ export function StellarReceive() {
setError('');
try {
const { rpc: rpcMod } = await import('@stellar/stellar-sdk');
const soroban = new rpcMod.Server(STELLAR_NETWORK.rpcUrl);
const networkPassphrase = STELLAR_NETWORK.networkPassphrase;
const soroban = new rpcMod.Server(network.rpcUrl);
const networkPassphrase = network.networkPassphrase;

const accountResponse = await soroban.getAccount(address);
const sourceAccount = new Account(
Expand Down Expand Up @@ -482,17 +482,14 @@ export function StellarReceive() {
} finally {
setIsRegistering(false);
}
}, [stellarKeys, address, signTransaction]);
}, [stellarKeys, address, network, signTransaction]);

const scanPayments = useCallback(async () => {
if (!stellarKeys) return;
setIsScanning(true);
setError('');
try {
const announcements = await fetchAnnouncementEvents(
STELLAR_NETWORK.rpcUrl,
ANNOUNCER_CONTRACT,
);
const announcements = await fetchAnnouncementEvents(network.rpcUrl, ANNOUNCER_CONTRACT);
const results = scanAnnouncements(
announcements,
stellarKeys.viewingKey,
Expand All @@ -506,13 +503,13 @@ export function StellarReceive() {
} finally {
setIsScanning(false);
}
}, [stellarKeys]);
}, [stellarKeys, network.rpcUrl]);

if (!isConnected) {
return (
<section className="flex flex-col gap-3">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Stellar Testnet / XLM
{network.name} / XLM
</span>
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
Receive
Expand All @@ -528,7 +525,7 @@ export function StellarReceive() {
<section className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Stellar Testnet / XLM
{network.name} / XLM
</span>
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
Receive
Expand Down Expand Up @@ -578,7 +575,7 @@ export function StellarReceive() {
<>
{' — '}
<a
href={stellarTxUrl(regHash)}
href={stellarTxUrl(regHash, network)}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
Expand Down
19 changes: 9 additions & 10 deletions src/components/StellarSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@ import {
} from '@wraith-protocol/sdk/chains/stellar';
import { useStellarWallet } from '@/context/StellarWalletContext';
import { stellarTxUrl, stellarAddrUrl } from '@/lib/explorer';
import { STELLAR_NETWORK } from '@/config';
import { CopyButton } from '@/components/CopyButton';

const ANNOUNCER_CONTRACT = 'CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL';

export function StellarSend() {
const { address, isConnected, signTransaction } = useStellarWallet();
const { address, isConnected, network, signTransaction } = useStellarWallet();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [error, setError] = useState('');
Expand Down Expand Up @@ -56,8 +55,8 @@ export function StellarSend() {
const result = generateStealthAddress(decoded.spendingPubKey, decoded.viewingPubKey);
setStealthResult(result);

const horizonUrl = STELLAR_NETWORK.horizonUrl;
const networkPassphrase = STELLAR_NETWORK.networkPassphrase;
const horizonUrl = network.horizonUrl;
const networkPassphrase = network.networkPassphrase;

const accountRes = await fetch(`${horizonUrl}/accounts/${address}`);
if (!accountRes.ok) throw new Error('Failed to load sender account');
Expand Down Expand Up @@ -112,7 +111,7 @@ export function StellarSend() {
// Announce via Soroban (best-effort)
try {
const { rpc: rpcMod } = await import('@stellar/stellar-sdk');
const soroban = new rpcMod.Server(STELLAR_NETWORK.rpcUrl);
const soroban = new rpcMod.Server(network.rpcUrl);
const announcerContract = new Contract(ANNOUNCER_CONTRACT);

const freshRes = await fetch(`${horizonUrl}/accounts/${address}`);
Expand Down Expand Up @@ -156,7 +155,7 @@ export function StellarSend() {
} finally {
setIsPending(false);
}
}, [address, recipient, amount, signTransaction]);
}, [address, recipient, amount, network, signTransaction]);

const reset = () => {
setRecipient('');
Expand All @@ -180,7 +179,7 @@ export function StellarSend() {
return (
<section className="flex flex-col gap-3">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Stellar Testnet / XLM
{network.name} / XLM
</span>
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
Send
Expand All @@ -196,7 +195,7 @@ export function StellarSend() {
<section className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Stellar Testnet / XLM
{network.name} / XLM
</span>
<h1 className="font-heading text-[28px] font-bold uppercase tracking-tight text-on-surface">
Send
Expand Down Expand Up @@ -295,7 +294,7 @@ export function StellarSend() {
</span>
<div className="mt-0.5 flex items-center gap-2">
<a
href={stellarAddrUrl(stealthResult.stealthAddress)}
href={stellarAddrUrl(stealthResult.stealthAddress, network)}
target="_blank"
rel="noopener noreferrer"
className="block truncate font-mono text-xs text-primary underline"
Expand All @@ -313,7 +312,7 @@ export function StellarSend() {
</span>
<div className="mt-0.5 flex items-center gap-2">
<a
href={stellarTxUrl(txHash)}
href={stellarTxUrl(txHash, network)}
target="_blank"
rel="noopener noreferrer"
className="block truncate font-mono text-xs text-primary underline"
Expand Down
25 changes: 22 additions & 3 deletions src/components/WalletConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ function HorizenButton() {
}

function FreighterButton() {
const { address, isConnected, connect, disconnect } = useStellarWallet();
const { address, isConnected, status, installUrl, connect, disconnect, retryInstall } =
useStellarWallet();

if (isConnected && address) {
return (
Expand All @@ -50,9 +51,27 @@ function FreighterButton() {
);
}

if (status === 'not-installed') {
return (
<a
href={installUrl}
target="_blank"
rel="noopener noreferrer"
onClick={retryInstall}
className={btnBase}
>
Install Freighter
</a>
);
}

return (
<button onClick={connect} className={btnBase}>
Connect Freighter
<button
onClick={() => void connect().catch(() => undefined)}
disabled={status === 'checking'}
className={btnBase}
>
{status === 'needs-approval' ? 'Approve in Freighter' : 'Connect Freighter'}
</button>
);
}
Expand Down
46 changes: 40 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,48 @@ export const wagmiConfig = getDefaultConfig({
},
});

export const STELLAR_NETWORK = {
name: 'Stellar Testnet',
networkPassphrase: 'Test SDF Network ; September 2015',
rpcUrl: 'https://soroban-testnet.stellar.org',
horizonUrl: 'https://horizon-testnet.stellar.org',
explorerUrl: 'https://stellar.expert/explorer/testnet',
export const STELLAR_NETWORKS = {
mainnet: {
id: 'mainnet',
freighterName: 'PUBLIC',
name: 'Stellar Mainnet',
networkPassphrase: 'Public Global Stellar Network ; September 2015',
rpcUrl: 'https://soroban-rpc.mainnet.stellar.gateway.fm',
horizonUrl: 'https://horizon.stellar.org',
explorerUrl: 'https://stellar.expert/explorer/public',
},
futurenet: {
id: 'futurenet',
freighterName: 'FUTURENET',
name: 'Stellar Futurenet',
networkPassphrase: 'Test SDF Future Network ; October 2022',
rpcUrl: 'https://rpc-futurenet.stellar.org',
horizonUrl: 'https://horizon-futurenet.stellar.org',
explorerUrl: 'https://stellar.expert/explorer/futurenet',
},
testnet: {
id: 'testnet',
freighterName: 'TESTNET',
name: 'Stellar Testnet',
networkPassphrase: 'Test SDF Network ; September 2015',
rpcUrl: 'https://soroban-testnet.stellar.org',
horizonUrl: 'https://horizon-testnet.stellar.org',
explorerUrl: 'https://stellar.expert/explorer/testnet',
},
} as const;

export type StellarNetworkId = keyof typeof STELLAR_NETWORKS;
export type StellarNetwork = (typeof STELLAR_NETWORKS)[StellarNetworkId];

export const STELLAR_NETWORK = STELLAR_NETWORKS.testnet;

export function getStellarNetwork(network: string): StellarNetwork {
const normalized = network.toUpperCase();
if (normalized === 'PUBLIC' || normalized === 'MAINNET') return STELLAR_NETWORKS.mainnet;
if (normalized === 'FUTURENET') return STELLAR_NETWORKS.futurenet;
return STELLAR_NETWORKS.testnet;
}

export const SOLANA_NETWORK = {
name: 'Solana Devnet',
rpcUrl: 'https://api.devnet.solana.com',
Expand Down
Loading