diff --git a/package-lock.json b/package-lock.json index 2fa5621..f3be0d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1494,6 +1495,7 @@ "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1574,6 +1576,7 @@ "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", @@ -1785,6 +1788,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2211,6 +2215,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2810,6 +2815,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3872,6 +3878,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -5885,6 +5892,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6106,6 +6114,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/accounts/index.ts b/src/accounts/index.ts index 3afe6c3..c8fc8fb 100644 --- a/src/accounts/index.ts +++ b/src/accounts/index.ts @@ -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 { + 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); +} diff --git a/src/index.ts b/src/index.ts index 083567c..4d1d8a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, +}; diff --git a/tests/unit/accounts/index.test.ts b/tests/unit/accounts/index.test.ts new file mode 100644 index 0000000..728dd70 --- /dev/null +++ b/tests/unit/accounts/index.test.ts @@ -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); + }); +}); diff --git a/tests/unit/sdk-exports.test.ts b/tests/unit/sdk-exports.test.ts index 16c9602..9c20578 100644 --- a/tests/unit/sdk-exports.test.ts +++ b/tests/unit/sdk-exports.test.ts @@ -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', () => {