Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
09ae070
feat: feat: implement Stellar asset trustline manager and balance (#72)
Timmmytunner Aug 31, 2026
cbef681
feat: feat: implement Stellar asset trustline manager and balance (#72)
Timmmytunner Aug 31, 2026
03db7c9
feat: feat: implement Stellar asset trustline manager and balance (#72)
Timmmytunner Aug 31, 2026
8dabd4f
feat: feat: implement Stellar asset trustline manager and balance (#72)
Timmmytunner Aug 31, 2026
bfd62dd
feat: feat: implement Stellar asset trustline manager and balance (#72)
Timmmytunner Aug 31, 2026
02227a1
feat: feat: implement Stellar asset trustline manager and balance (#72)
Timmmytunner Aug 31, 2026
08e00b5
feat: feat: implement Stellar asset trustline manager and balance (#72)
Timmmytunner Aug 31, 2026
c5986fe
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
ef9b484
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
0ba683b
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
837b8a5
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
efff901
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
20e622c
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
9410eb6
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
88e43b8
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
91c8051
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
804cf61
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
30aded0
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
047faa4
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
ddb7b32
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
1414297
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
0ef2df2
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
03ccdf3
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
31aa7f0
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
fb89feb
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
af236f7
fix(ci): resolve failing checks for #72
Timmmytunner Aug 31, 2026
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
166 changes: 146 additions & 20 deletions src/features/transactions/useXdrSigner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import { useState, useCallback, useEffect } from 'react';
import * as freighter from '@stellar/freighter-api';
import { Account, Asset, Networks, Operation, TransactionBuilder } from '@stellar/stellar-sdk';

export const STELLAR_HORIZON_URL =
process.env.NEXT_PUBLIC_STELLAR_HORIZON_URL || 'https://horizon-testnet.stellar.org';
export const STELLAR_NETWORK_PASSPHRASE =
process.env.NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE || Networks.TESTNET;

export interface Trustline {
assetCode: string;
issuer: string;
balance: string;
limit?: string;
}

export interface AddTrustlineResult {
signedXdr: string;
hash: string;
}

export interface UseXdrSignerResult {
activeKey: string | null;
Expand All @@ -11,6 +29,17 @@ export interface UseXdrSignerResult {
signXdr: (xdr: string, networkPassphrase?: string) => Promise<string | null>;
disconnectWallet: () => void;
setActiveKey: (key: string | null) => void;
getAccountBalances: (publicKey?: string) => Promise<Trustline[]>;
buildTrustlineXdr: (assetCode: string, issuer: string, limit?: string, publicKey?: string) => Promise<string | null>;
addTrustline: (assetCode: string, issuer: string, limit?: string, publicKey?: string) => Promise<AddTrustlineResult | null>;
}

async function fetchAccount(publicKey: string): Promise<any> {
const res = await fetch(`${STELLAR_HORIZON_URL}/accounts/${publicKey}`);
if (!res.ok) {
throw new Error(`Failed to fetch account from Horizon: ${res.statusText}`);
}
return res.json();
}

export function useXdrSigner(): UseXdrSignerResult {
Expand All @@ -24,13 +53,14 @@ export function useXdrSigner(): UseXdrSignerResult {
const checkAvailability = async () => {
try {
if (typeof freighter.isConnected === 'function') {
const res = await freighter.isConnected();
const available = typeof res === 'boolean' ? res : Boolean(res?.isConnected);
const res = await freighter.isConnected() as { isConnected?: boolean } | boolean;
const available = typeof res === 'boolean' ? res : Boolean(res);
setIsFreighterAvailable(available);
if (available && typeof freighter.getAddress === 'function') {
const info = await freighter.getAddress();
if (info?.address) {
setActiveKey(info.address);
const info = await freighter.getAddress() as { address?: string; publicKey?: string };
const pubKey = info?.address || info?.publicKey;
if (pubKey) {
setActiveKey(pubKey);
setIsConnected(true);
}
}
Expand All @@ -47,24 +77,27 @@ export function useXdrSigner(): UseXdrSignerResult {
setError(null);
try {
if (typeof freighter.requestAccess === 'function') {
const res = await freighter.requestAccess();
const res = await freighter.requestAccess() as string | { address?: string; publicKey?: string };
let pubKey: string | null = null;
if (typeof res === 'string' && res) {
setActiveKey(res);
setIsConnected(true);
return res;
} else if (res && typeof res === 'object' && 'address' in res && res.address) {
const addr = (res as { address: string }).address;
setActiveKey(addr);
pubKey = res;
} else if (res && typeof res === 'object') {
const obj = res as { address?: string; publicKey?: string };
pubKey = obj.address || obj.publicKey || null;
}
if (pubKey) {
setActiveKey(pubKey);
setIsConnected(true);
return addr;
return pubKey;
}
}
if (typeof freighter.getAddress === 'function') {
const info = await freighter.getAddress();
if (info?.address) {
setActiveKey(info.address);
const info = await freighter.getAddress() as { address?: string; publicKey?: string };
const pubKey = info?.address || info?.publicKey;
if (pubKey) {
setActiveKey(pubKey);
setIsConnected(true);
return info.address;
return pubKey;
}
}
setError('Freighter browser extension was not detected. Please install Freighter or select a key manually.');
Expand All @@ -85,12 +118,12 @@ export function useXdrSigner(): UseXdrSignerResult {
try {
if (typeof freighter.signTransaction === 'function') {
const res = await freighter.signTransaction(xdr, {
networkPassphrase: networkPassphrase || 'Test SDF Network ; September 2015',
});
networkPassphrase: networkPassphrase || STELLAR_NETWORK_PASSPHRASE,
}) as string | { signedTxXdr?: string };
if (typeof res === 'string') {
return res;
} else if (res && typeof res === 'object' && 'signedTxXdr' in res) {
return res.signedTxXdr;
return res.signedTxXdr ?? null;
}
}
throw new Error('Freighter signing operation failed or extension not connected.');
Expand All @@ -105,6 +138,96 @@ export function useXdrSigner(): UseXdrSignerResult {
[]
);

const getAccountBalances = useCallback(async (publicKey?: string): Promise<Trustline[]> => {
const key = publicKey || activeKey;
if (!key) {
setError('Wallet is not connected. Cannot fetch balances.');
return [];
}
try {
const account = await fetchAccount(key);
const balances = account.balances || [];
return balances.map((balance: any) => ({
assetCode: balance.asset_code || 'XLM',
issuer: balance.asset_issuer || '',
balance: balance.balance,
limit: balance.limit,
}));
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Failed to fetch account balances.';
setError(msg);
return [];
}
}, [activeKey]);

const buildTrustlineXdr = useCallback(
async (assetCode: string, issuer: string, limit?: string, publicKey?: string): Promise<string | null> => {
const key = publicKey || activeKey;
if (!key) {
setError('Wallet is not connected. Cannot build trustline transaction.');
return null;
}
try {
const account = await fetchAccount(key);
const asset = new Asset(assetCode, issuer);
const accountObj = new Account(key, account.sequence);
const transaction = new TransactionBuilder(accountObj, {
fee: '100',
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(Operation.changeTrust({ asset, limit: limit || undefined }))
.setTimeout(180)
.build();
return transaction.toXDR();
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Failed to build trustline transaction.';
setError(msg);
return null;
}
},
[activeKey]
);

const addTrustline = useCallback(
async (assetCode: string, issuer: string, limit?: string, publicKey?: string): Promise<AddTrustlineResult | null> => {
const key = publicKey || activeKey;
if (!key) {
setError('Wallet is not connected. Cannot add trustline.');
return null;
}
try {
const xdr = await buildTrustlineXdr(assetCode, issuer, limit, key);
if (!xdr) {
return null;
}
const signedXdr = await signXdr(xdr);
if (!signedXdr) {
return null;
}

const body = new URLSearchParams({ tx: signedXdr });
const res = await fetch(`${STELLAR_HORIZON_URL}/transactions`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});

if (!res.ok) {
const errorText = await res.text();
throw new Error(`Transaction submission failed (${res.status}): ${errorText}`);
}

const data = await res.json();
return { signedXdr, hash: data.hash };
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Failed to add trustline.';
setError(msg);
return null;
}
},
[activeKey, buildTrustlineXdr, signXdr]
);

const disconnectWallet = useCallback(() => {
setActiveKey(null);
setIsConnected(false);
Expand All @@ -121,5 +244,8 @@ export function useXdrSigner(): UseXdrSignerResult {
signXdr,
disconnectWallet,
setActiveKey,
getAccountBalances,
buildTrustlineXdr,
addTrustline,
};
}
1 change: 1 addition & 0 deletions src/features/wallet/trustline-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import { z } from 'zod';export const stellarPublicKeySchema = z.string().regex(/^G[2-9A-J-NP-Z]{55}$/);export const assetCodeSchema = z.string().min(1).max(12).regex(/^[a-zA-Z0-9]{1,12}$/);export const trustlineSchema = z.object({assetCode: assetCodeSchema,assetIssuer: stellarPublicKeySchema.optional(),balance: z.string().optional(),limit: z.string().optional()});export type StellarPublicKey = z.infer<typeof stellarPublicKeySchema>;export type AssetCode = z.infer<typeof assetCodeSchema>;export type Trustline = z.infer<typeof trustlineSchema>;
85 changes: 85 additions & 0 deletions src/features/wallet/useTrustlineManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useState, useEffect, useCallback } from 'react';
import { toast } from 'sonner';
import { useWalletStore } from '/stores/wallet';
import {
getSupportedAssetStatuses,
buildTrustlineTransaction,
signAndSubmitTransaction,
SupportedAssetStatus,
} from '../../services/stellar';

export function useTrustlineManager() {
const publicKey = useWalletStore((state: any) => state.publicKey);
const isConnected = useWalletStore((state: any) => state.isConnected);

const [assets, setAssets] = useState<SupportedAssetStatus[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);

const fetchAssets = useCallback(async () => {
if (!publicKey) {
setAssets([]);
return;
}

setLoading(true);
setError(null);

try {
const statuses = await getSupportedAssetStatuses(publicKey);
setAssets(statuses);
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to fetch asset balances';
setError(message);
toast.error(message);
} finally {
setLoading(false);
}
}, [publicKey]);

useEffect(() {
fetchAssets();
}, [fetchAssets]);

const addTrustline = useCallback(
async (assetCode: string, assetIssuer: string) => {
if (!publicKey) {
toast.error('Wallet not connected');
return;
}

const existing = assets.find(
(a) => a.assetCode === assetCode && a.issuer === assetIssuer
);
if (existing?.trustlineActive) {
toast.info('Trustline already exists');
return;
}

setIsSubmitting(true);
try {
const xdr = await buildTrustlineTransaction(publicKey, assetCode, assetIssuer);
await signAndSubmitTransaction(xdr);
toast.success('Trustline added successfully');
await fetchAssets();
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to add trustline';
toast.error(message);
} finally {
setIsSubmitting(false);
}
},
[publicKey, assets, fetchAssets]
);

return {
assets,
loading,
error,
isSubmitting,
isConnected,
addTrustline,
refresh: fetchAssets,
};
}
Loading
Loading