diff --git a/README.md b/README.md index c4294f1..834cc7b 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ logic lives now. - `errors` — `VeroError` with stable, switchable `VeroErrorCode`s - `network` — network config and HTTPS-enforcing endpoint validation - `rpc` — RPC client with failover, health tracking, and origin-safe URL building +- `account` — Horizon account loader, data-entry helpers, and stroop-safe balances - `contract` — typed read/write wrappers for the Vero core contract entrypoints Wallet adapters and higher-level transaction builders are tracked as open diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs index f69d1ae..51981b4 100644 --- a/scripts/smoke-test.mjs +++ b/scripts/smoke-test.mjs @@ -18,19 +18,25 @@ const REQUIRED_EXPORTS = [ 'MAINNET', 'retry', 'defaultIsRetryable', + 'AccountLoader', + 'AccountDataKey', ]; const DECLARATION_CHECKS = [ - ['cjs', 'index.d.ts', /export \* from '\.\/(types|errors|network|rpc|nonce)\/index\.js'/], + ['cjs', 'index.d.ts', /export \* from '\.\/(types|errors|network|rpc|nonce|account)\/index\.js'/], ['cjs', 'index.d.ts', /export \* from '\.\/resilience\/backoff\.js'/], + ['cjs', 'index.d.ts', "export * from './account/index.js'"], ['cjs', 'errors/index.d.ts', 'VeroError'], ['cjs', 'rpc/index.d.ts', 'RpcClient'], ['cjs', 'nonce/index.d.ts', 'NonceManager'], - ['esm', 'index.d.ts', /export \* from '\.\/(types|errors|network|rpc|nonce)\/index\.js'/], + ['cjs', 'account/index.d.ts', 'AccountLoader'], + ['esm', 'index.d.ts', /export \* from '\.\/(types|errors|network|rpc|nonce|account)\/index\.js'/], ['esm', 'index.d.ts', /export \* from '\.\/resilience\/backoff\.js'/], + ['esm', 'index.d.ts', "export * from './account/index.js'"], ['esm', 'errors/index.d.ts', 'VeroError'], ['esm', 'rpc/index.d.ts', 'RpcClient'], ['esm', 'nonce/index.d.ts', 'NonceManager'], + ['esm', 'account/index.d.ts', 'AccountLoader'], ]; function assert(condition, message) { diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts new file mode 100644 index 0000000..38d6297 --- /dev/null +++ b/src/__tests__/index.test.ts @@ -0,0 +1,77 @@ +/** + * Root barrel regression (#67). + * + * Account tests imported `../index` (the account-local barrel), so the + * module could ship in `dist` and still be unreachable from + * `@vero-protocol/sdk`. This file imports the package root and asserts + * every module directory is re-exported, so a new module cannot be + * omitted the same way. + */ + +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; +import { + AccountDataKey, + AccountLoader, + DataKey, + EventCursor, + NonceManager, + retry, + Role, + RpcClient, + validateUrl, + VeroError, +} from '../index'; + +/** + * `contract` re-exports `Task`, `Vote`, and `SubmitResult`, so + * `export * from './contract'` collides with `./types`. Resolving that + * is outside #67. + */ +const ROOT_BARREL_EXCEPTIONS = new Set(['contract']); + +describe('package root barrel', () => { + it('exposes AccountLoader for import { AccountLoader } from "@vero-protocol/sdk"', () => { + expect(AccountLoader).toBeDefined(); + expect(typeof AccountLoader).toBe('function'); + }); + + it('keeps protocol DataKey distinct from AccountDataKey', () => { + expect(DataKey.reputation).toBe('vero_reputation'); + expect(AccountDataKey.Reputation).toBe('reputation'); + expect(DataKey).not.toBe(AccountDataKey); + }); + + it('re-exports every module directory', () => { + const srcDir = join(__dirname, '..'); + const barrel = readFileSync(join(srcDir, 'index.ts'), 'utf8'); + const moduleDirs = readdirSync(srcDir).filter((name) => { + const full = join(srcDir, name); + return statSync(full).isDirectory() && name !== '__tests__'; + }); + + for (const name of ROOT_BARREL_EXCEPTIONS) { + expect(moduleDirs).toContain(name); + } + + const missing = moduleDirs.filter((name) => { + if (ROOT_BARREL_EXCEPTIONS.has(name)) { + return false; + } + return !barrel.includes(`from './${name}`); + }); + + expect(missing).toEqual([]); + }); + + it('resolves a representative symbol from each public module', () => { + expect(Role).toBeDefined(); + expect(VeroError).toBeDefined(); + expect(validateUrl).toBeDefined(); + expect(RpcClient).toBeDefined(); + expect(NonceManager).toBeDefined(); + expect(retry).toBeDefined(); + expect(EventCursor).toBeDefined(); + expect(AccountLoader).toBeDefined(); + }); +}); diff --git a/src/account/__tests__/data.test.ts b/src/account/__tests__/data.test.ts index c829ec3..fa6c1ac 100644 --- a/src/account/__tests__/data.test.ts +++ b/src/account/__tests__/data.test.ts @@ -3,7 +3,7 @@ */ import { - DataKey, + AccountDataKey, readDataEntry, getReputation, getMetadata, @@ -80,7 +80,7 @@ describe('Account Data', () => { }); it('getReputation returns null for an entry failing the validity check (#70)', () => { - const data = { [DataKey.Reputation]: '!!!not base64!!!' }; + const data = { [AccountDataKey.Reputation]: '!!!not base64!!!' }; expect(getReputation(data)).toBeNull(); }); }); @@ -88,7 +88,7 @@ describe('Account Data', () => { describe('getReputation', () => { it('should parse valid reputation data', () => { const data = { - [DataKey.Reputation]: Buffer.from( + [AccountDataKey.Reputation]: Buffer.from( JSON.stringify({ score: 250, metadata: { contributions: 5 } }) ).toString('base64'), }; @@ -106,7 +106,7 @@ describe('Account Data', () => { it('should handle invalid reputation JSON gracefully', () => { const data = { - [DataKey.Reputation]: Buffer.from('invalid json').toString('base64'), + [AccountDataKey.Reputation]: Buffer.from('invalid json').toString('base64'), }; const result = getReputation(data); expect(result).toBeNull(); @@ -114,7 +114,7 @@ describe('Account Data', () => { it('should handle reputation with missing score', () => { const data = { - [DataKey.Reputation]: Buffer.from( + [AccountDataKey.Reputation]: Buffer.from( JSON.stringify({ metadata: { contributions: 5 } }) ).toString('base64'), }; @@ -136,7 +136,7 @@ describe('Account Data', () => { for (const { score, tier } of testCases) { const data = { - [DataKey.Reputation]: Buffer.from( + [AccountDataKey.Reputation]: Buffer.from( JSON.stringify({ score }) ).toString('base64'), }; @@ -149,7 +149,7 @@ describe('Account Data', () => { describe('getMetadata', () => { it('should parse valid metadata', () => { const data = { - [DataKey.Metadata]: Buffer.from( + [AccountDataKey.Metadata]: Buffer.from( JSON.stringify({ name: 'test', version: 1 }) ).toString('base64'), }; @@ -167,7 +167,7 @@ describe('Account Data', () => { it('should handle invalid metadata JSON', () => { const data = { - [DataKey.Metadata]: Buffer.from('invalid json').toString('base64'), + [AccountDataKey.Metadata]: Buffer.from('invalid json').toString('base64'), }; const result = getMetadata(data); expect(result).toBeNull(); @@ -175,7 +175,7 @@ describe('Account Data', () => { it('should handle malformed metadata base64', () => { const data = { - [DataKey.Metadata]: 'not-valid-base64!', + [AccountDataKey.Metadata]: 'not-valid-base64!', }; const result = getMetadata(data); expect(result).toBeNull(); @@ -185,7 +185,7 @@ describe('Account Data', () => { describe('getProfile', () => { it('should parse valid profile', () => { const data = { - [DataKey.Profile]: Buffer.from( + [AccountDataKey.Profile]: Buffer.from( JSON.stringify({ name: 'testuser', email: 'test@example.com' }) ).toString('base64'), }; @@ -203,7 +203,7 @@ describe('Account Data', () => { it('should handle invalid profile JSON', () => { const data = { - [DataKey.Profile]: Buffer.from('invalid json').toString('base64'), + [AccountDataKey.Profile]: Buffer.from('invalid json').toString('base64'), }; const result = getProfile(data); expect(result).toBeNull(); diff --git a/src/account/__tests__/index.test.ts b/src/account/__tests__/index.test.ts index f06edf0..035f0e0 100644 --- a/src/account/__tests__/index.test.ts +++ b/src/account/__tests__/index.test.ts @@ -9,7 +9,7 @@ describe('Account module index', () => { // Check that all expected exports exist expect(account.AccountLoader).toBeDefined(); expect(account.accountLoader).toBeDefined(); - expect(account.DataKey).toBeDefined(); + expect(account.AccountDataKey).toBeDefined(); expect(account.readDataEntry).toBeDefined(); expect(account.getReputation).toBeDefined(); expect(account.getMetadata).toBeDefined(); diff --git a/src/account/data.ts b/src/account/data.ts index 2bf4c5d..ad8b814 100644 --- a/src/account/data.ts +++ b/src/account/data.ts @@ -8,15 +8,18 @@ import { AccountDataEntry, ReputationData } from './types.js'; /** - * Data key constants + * Horizon account data-entry names. + * + * Named `AccountDataKey` so it does not collide with the contract storage + * `DataKey` in `src/types` when both are re-exported from the package root. */ -export const DataKey = { +export const AccountDataKey = { Reputation: 'reputation', Metadata: 'metadata', Profile: 'profile', } as const; -export type DataKey = typeof DataKey[keyof typeof DataKey]; +export type AccountDataKey = typeof AccountDataKey[keyof typeof AccountDataKey]; /** * Strip trailing base64 padding so two equivalent encodings compare equal. @@ -70,7 +73,7 @@ export function readDataEntry( * @returns The reputation data, or null if not found */ export function getReputation(data: Record): ReputationData | null { - const entry = readDataEntry(data, DataKey.Reputation); + const entry = readDataEntry(data, AccountDataKey.Reputation); if (!entry || !entry.isValid) { return null; } @@ -105,7 +108,7 @@ function getReputationTier(score: number): 'bronze' | 'silver' | 'gold' | 'plati * Read metadata from account data. */ export function getMetadata(data: Record): Record | null { - const entry = readDataEntry(data, DataKey.Metadata); + const entry = readDataEntry(data, AccountDataKey.Metadata); if (!entry || !entry.isValid) { return null; } @@ -121,7 +124,7 @@ export function getMetadata(data: Record): Record): Record | null { - const entry = readDataEntry(data, DataKey.Profile); + const entry = readDataEntry(data, AccountDataKey.Profile); if (!entry || !entry.isValid) { return null; } diff --git a/src/account/index.ts b/src/account/index.ts index 76b48ec..8acc9e0 100644 --- a/src/account/index.ts +++ b/src/account/index.ts @@ -13,7 +13,7 @@ export { AccountLoader, accountLoader } from './loader.js'; // Data export { - DataKey, + AccountDataKey, readDataEntry, getReputation, getMetadata, diff --git a/src/index.ts b/src/index.ts index 8de3cc6..80faefb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,3 +14,4 @@ export * from './rpc/index.js'; export * from './nonce/index.js'; export * from './resilience/backoff.js'; export * from './events/index.js'; +export * from './account/index.js';