Skip to content
Open
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
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 86 additions & 1 deletion src/accounts/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,86 @@
export { getMinimumReserve, generateKeypair } from './keypair';
import { AccountInfo } from '../types/network';
import { HorizonSubmitError, ValidationError } from '../utils/errors';
import { isValidPublicKey } from '../utils/validation';

interface HorizonAccountResponse {
id: string;
sequence: string;
balances?: Array<{
asset_type?: string;
balance?: string;
}>;
signers?: Array<{
key: string;
weight: number;
}>;
thresholds?: {
low_threshold?: number;
med_threshold?: number;
high_threshold?: number;
};
}

const DEFAULT_HORIZON_URL = process.env.STELLAR_HORIZON_URL || 'https://horizon-testnet.stellar.org';

function buildMissingAccountInfo(publicKey: string): AccountInfo {
return {
accountId: publicKey,
balance: '0',
signers: [],
thresholds: {
low: 0,
medium: 0,
high: 0,
},
sequenceNumber: '0',
exists: false,
};
}

function mapAccountInfo(account: HorizonAccountResponse): AccountInfo {
const nativeBalance = account.balances?.find(balance => balance.asset_type === 'native')?.balance || '0';

return {
accountId: account.id,
balance: nativeBalance,
signers: (account.signers || []).map(signer => ({
publicKey: signer.key,
weight: signer.weight,
})),
thresholds: {
low: account.thresholds?.low_threshold || 0,
medium: account.thresholds?.med_threshold || 0,
high: account.thresholds?.high_threshold || 0,
},
sequenceNumber: account.sequence,
exists: true,
};
}

export async function verifyAccount(
publicKey: string,
horizonUrl: string = DEFAULT_HORIZON_URL,
): Promise<AccountInfo> {
if (!isValidPublicKey(publicKey)) {
throw new ValidationError('publicKey', 'Invalid Stellar public key');
}

let response: Response;

try {
response = await fetch(`${horizonUrl.replace(/\/$/, '')}/accounts/${publicKey}`);
} catch {
throw new HorizonSubmitError('network_error');
}

if (response.status === 404) {
return buildMissingAccountInfo(publicKey);
}

if (!response.ok) {
throw new HorizonSubmitError(`http_${response.status}`);
}

const account = (await response.json()) as HorizonAccountResponse;
return mapAccountInfo(account);
}
55 changes: 13 additions & 42 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,25 @@
import * as StellarSDK from '@stellar/stellar-sdk';
export { StellarSDK };

export const SDK_VERSION = '0.1.0';

// 1. Main class
export { StellarSDK } from './sdk';
export { StellarSDK as default } from './sdk';
export { verifyAccount } from './accounts';

// 2. Error classes
export {
SdkError,
ValidationError,
AccountNotFoundError,
EscrowNotFoundError,
InsufficientBalanceError,
HorizonSubmitError,
TransactionTimeoutError,
MonitorTimeoutError,
FriendbotError,
ConditionMismatchError,
} from './utils/errors';
export * from './utils/errors';
export * from './utils/validation';

// 3. Escrow types (canonical source for Signer + Thresholds)
export { EscrowStatus } from './types/escrow';
export type {
CreateEscrowParams,
Signer,
Thresholds,
EscrowAccount,
Distribution,
ReleaseParams,
ReleasedPayment,
ReleaseResult,
Percentage,
LockFundsParams,
LockResult,
} from './types/escrow';
export { EscrowStatus, asPercentage } from './types/escrow';

// 4. Network types (Signer + Thresholds excluded to avoid conflict)
export type { SDKConfig, KeypairResult, AccountInfo, BalanceInfo } from './types/network';

// 5. Transaction types
export type { SubmitResult, TransactionStatus, BuildParams, Operation } from './types/transaction';
export type { AccountInfo } from './types/network';

// 6. Standalone functions
export {
createEscrowAccount,
calculateStartingBalance,
lockCustodyFunds,
EscrowManager,
handleDispute,
anchorTrustHash,
verifyEventHash,
} from './escrow';
export { buildMultisigTransaction } from './transactions';
export { getMinimumReserve, generateKeypair } from './accounts';
// Default export for convenience
export default {
SDK_VERSION,
StellarSDK,
};
86 changes: 86 additions & 0 deletions tests/unit/accounts/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { verifyAccount } from '../../../src/accounts';
import { HorizonSubmitError, ValidationError } from '../../../src/utils/errors';

const VALID_PUBLIC_KEY = 'GADOPTER111111111111111111111111111111111111111111111111';
const MOCK_HORIZON_URL = 'http://mock-horizon.test';

describe('verifyAccount', () => {
const originalFetch = global.fetch;

afterEach(() => {
global.fetch = originalFetch;
jest.restoreAllMocks();
});

it('returns full account details when the account exists', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
id: VALID_PUBLIC_KEY,
sequence: '123456789',
balances: [
{ asset_type: 'credit_alphanum4', balance: '25.0000000' },
{ asset_type: 'native', balance: '100.5000000' },
],
signers: [
{ key: VALID_PUBLIC_KEY, weight: 1 },
{ key: 'GSIGNER11111111111111111111111111111111111111111111111111', weight: 2 },
],
thresholds: {
low_threshold: 1,
med_threshold: 2,
high_threshold: 3,
},
}),
}) as typeof fetch;

await expect(verifyAccount(VALID_PUBLIC_KEY, MOCK_HORIZON_URL)).resolves.toEqual({
accountId: VALID_PUBLIC_KEY,
balance: '100.5000000',
signers: [
{ publicKey: VALID_PUBLIC_KEY, weight: 1 },
{ publicKey: 'GSIGNER11111111111111111111111111111111111111111111111111', weight: 2 },
],
thresholds: {
low: 1,
medium: 2,
high: 3,
},
sequenceNumber: '123456789',
exists: true,
});

expect(global.fetch).toHaveBeenCalledWith(`${MOCK_HORIZON_URL}/accounts/${VALID_PUBLIC_KEY}`);
});

it('returns exists false for a non-existent account', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 404,
}) as typeof fetch;

await expect(verifyAccount(VALID_PUBLIC_KEY, MOCK_HORIZON_URL)).resolves.toEqual({
accountId: VALID_PUBLIC_KEY,
balance: '0',
signers: [],
thresholds: {
low: 0,
medium: 0,
high: 0,
},
sequenceNumber: '0',
exists: false,
});
});

it('throws ValidationError for an invalid key', async () => {
await expect(verifyAccount('BAD_KEY', MOCK_HORIZON_URL)).rejects.toBeInstanceOf(ValidationError);
});

it('throws HorizonSubmitError on network failure', async () => {
global.fetch = jest.fn().mockRejectedValue(new Error('socket hang up')) as typeof fetch;

await expect(verifyAccount(VALID_PUBLIC_KEY, MOCK_HORIZON_URL)).rejects.toBeInstanceOf(HorizonSubmitError);
});
});
44 changes: 44 additions & 0 deletions tests/unit/sdk-exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,50 @@ import defaultExport, {
EscrowStatus,
} from '../../src/index';

describe('SDK Exports', () => {
it('should export StellarSDK', () => {
expect(StellarSDK).toBeDefined();
});

it('should export all error classes', () => {
expect(SdkError).toBeDefined();
expect(ValidationError).toBeDefined();
expect(AccountNotFoundError).toBeDefined();
expect(EscrowNotFoundError).toBeDefined();
expect(InsufficientBalanceError).toBeDefined();
expect(HorizonSubmitError).toBeDefined();
expect(TransactionTimeoutError).toBeDefined();
expect(MonitorTimeoutError).toBeDefined();
expect(FriendbotError).toBeDefined();
expect(ConditionMismatchError).toBeDefined();
});

it('should export EscrowStatus enum', () => {
expect(EscrowStatus).toBeDefined();
expect(EscrowStatus.CREATED).toBe('CREATED');
});

it('should have a default export', () => {
expect(defaultExport).toBeDefined();
expect(defaultExport.SDK_VERSION).toBeDefined();
});
});

import defaultExport, {
StellarSDK,
SdkError,
ValidationError,
AccountNotFoundError,
EscrowNotFoundError,
InsufficientBalanceError,
HorizonSubmitError,
TransactionTimeoutError,
MonitorTimeoutError,
FriendbotError,
ConditionMismatchError,
EscrowStatus,
} from '../../src/index';

// Requirements 1.3
describe('StellarSDK named and default export identity', () => {
it('default export and named StellarSDK export are the same reference', () => {
Expand Down
Loading