diff --git a/src/client.ts b/src/client.ts index ac2c2fc..05c34ab 100644 --- a/src/client.ts +++ b/src/client.ts @@ -91,7 +91,7 @@ export class VeriTixClient extends EventEmitter { public readonly batch: BatchModule; private server!: SorobanRpc.Server; - private readonly keypair: Keypair | undefined; + protected readonly keypair: Keypair | undefined; private connected = false; /** Cache for getCurrentLedger — { sequence, fetchedAt } */ @@ -394,7 +394,10 @@ export class VeriTixClient extends EventEmitter { */ async simulate(method: string, args: xdr.ScVal[]): Promise { if (!this.connected) { - throw new Error('VeriTixClient: call connect() before simulate()'); + throw new VeriTixError( + VeriTixErrorCode.ClientNotConnected, + 'VeriTixClient: call connect() before simulate()' + ); } try { @@ -448,7 +451,10 @@ export class VeriTixClient extends EventEmitter { */ async getCurrentLedger(): Promise { if (!this.connected || !this.server) { - throw new Error('VeriTixClient: call connect() before using module methods'); + throw new VeriTixError( + VeriTixErrorCode.ClientNotConnected, + 'VeriTixClient: call connect() before using module methods' + ); } const now = Date.now(); if ( @@ -473,7 +479,10 @@ export class VeriTixClient extends EventEmitter { */ async watchTransaction(hash: string, options?: WatchOptions): Promise { if (!this.connected || !this.server) { - throw new Error('VeriTixClient: call connect() before using module methods'); + throw new VeriTixError( + VeriTixErrorCode.ClientNotConnected, + 'VeriTixClient: call connect() before using module methods' + ); } const intervalMs = options?.intervalMs ?? 2_000; const timeoutMs = options?.timeoutMs ?? 60_000; @@ -518,7 +527,10 @@ export class VeriTixClient extends EventEmitter { */ async getContractMetadata(): Promise { if (!this.connected || !this.server) { - throw new Error('VeriTixClient: call connect() before using module methods'); + throw new VeriTixError( + VeriTixErrorCode.ClientNotConnected, + 'VeriTixClient: call connect() before using module methods' + ); } const [name, symbol, decimal, totalSupply] = await Promise.all([ this.token.name(), @@ -595,12 +607,13 @@ export class VeriTixClient extends EventEmitter { return new Proxy({} as SorobanRpc.Server, { get: (_target, prop) => { if (!this.connected || !this.server) { - throw new Error( - `VeriTixClient: call connect() before using module methods (attempted access to server.${String(prop)})`, + throw new VeriTixError( + VeriTixErrorCode.ClientNotConnected, + `VeriTixClient: call connect() before using module methods (attempted access to server.${String(prop)})` ); } return (this.server as unknown as Record)[prop]; }, }); } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 758ad95..5344560 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,10 @@ // Client // --------------------------------------------------------------------------- export { VeriTixClient } from './client'; +export { VeriTixClientExtended } from './client-extended'; +export { createSafeToJSON, createSafeInspect } from './client-security'; +export { VeriTixSDK } from './namespace'; +export { createFromFreighter } from './modules/freighter-factory'; // NOTE: WatchOptions was previously exported from './client' (deprecated). // The canonical export is from './types/index' below. // @deprecated importing WatchOptions from './client' — use './types/index' directly. @@ -31,6 +35,7 @@ export type { ContractMetadata, EscrowRecord, TicketEscrowParams, + BatchSettlementResult, SplitRecord, SplitRecipient, DisputeRecord, @@ -52,6 +57,13 @@ export { SplitterModule } from './modules/splitter'; export { RecurringModule } from './modules/recurring'; export { AdminModule } from './modules/admin'; export { BatchModule } from './modules/batch'; +export { EventsService } from './modules/events-service'; +export { EventDashboard } from './modules/events-dashboard'; +export { EventGalleryService } from './modules/event-gallery.service'; +export { RevenueAnalyticsModule } from './modules/analytics/revenue-analytics.service'; +export { TicketAnalyticsModule } from './modules/analytics/ticket-analytics.service'; +export { CollaboratorModule } from './modules/collaborator/collaborator.service'; +export { TicketPurchaseModule } from './modules/ticket/ticket-purchase.service'; // Module param types export type { MintParams, BurnParams, TransferParams, ApproveParams } from './modules/token'; @@ -60,6 +72,17 @@ export type { OpenDisputeParams, ResolveDisputeParams } from './modules/dispute' export type { CreateSplitParams } from './modules/splitter'; export type { SetupRecurringParams } from './modules/recurring'; export type { BatchMintEntry, BatchTransferEntry } from './modules/batch'; +// New module types +export type { Event, EventFilter } from './modules/events-service'; +export type { DashboardMetrics } from './modules/events-dashboard'; +export type { RevenuePeriod, TicketSale } from './modules/analytics/revenue-analytics.service'; +export type { TicketPeriod, ExportFormat, TicketOrder } from './modules/analytics/ticket-analytics.service'; +export type { Collaborator, CollaboratorUpdate } from './modules/collaborator/collaborator.service'; +export type { BillingDetails, AddressDetails, PurchaseRequest, Receipt } from './modules/ticket/ticket-purchase.service'; + +// Module constants +export { TRANSACTION_CHARGE_RATE } from './modules/analytics/revenue-analytics.service'; +export { MAX_COLLABORATORS_PER_EVENT } from './modules/collaborator/collaborator.service'; // --------------------------------------------------------------------------- // Errors @@ -83,6 +106,30 @@ export { assertValidAddress, } from './utils/network'; +// --------------------------------------------------------------------------- +// ScVal conversion helpers +// --------------------------------------------------------------------------- +export { + addressToScVal, + bigintToScVal, + boolToScVal, + stringToScVal, + scValToString, + scValToBigint, + scValToBoolean, + scValToNumber, +} from './utils/scval'; + +// --------------------------------------------------------------------------- +// XDR struct parsers +// --------------------------------------------------------------------------- +export { + parseEscrowRecord, + parseSplitRecord, + parseDisputeRecord, + parseRecurringRecord, +} from './utils/parsers'; + // --------------------------------------------------------------------------- // Format helpers // --------------------------------------------------------------------------- @@ -96,4 +143,4 @@ export { buildContractCall, simulateTransaction, submitTransaction, -} from './utils/transaction'; +} from './utils/transaction'; \ No newline at end of file diff --git a/src/modules/collaborator/collaborator.service.ts b/src/modules/collaborator/collaborator.service.ts index 25d6c94..d6d6262 100644 --- a/src/modules/collaborator/collaborator.service.ts +++ b/src/modules/collaborator/collaborator.service.ts @@ -1,3 +1,5 @@ +import { VeriTixError, VeriTixErrorCode } from '../../utils/errors'; + /** A person collaborating on an event. */ export interface Collaborator { id: string; @@ -19,12 +21,15 @@ export class CollaboratorModule { public add(collaborator: Collaborator): Collaborator { if (this.collaborators.has(collaborator.id)) { - throw new Error(`Collaborator ${collaborator.id} already exists.`); + throw new VeriTixError( + VeriTixErrorCode.CollaboratorAlreadyExists, + `Collaborator ${collaborator.id} already exists.` + ); } if (this.listByEvent(collaborator.eventId).length >= MAX_COLLABORATORS_PER_EVENT) { - throw new Error( - `Event ${collaborator.eventId} already has the maximum of ` + - `${MAX_COLLABORATORS_PER_EVENT} collaborators.`, + throw new VeriTixError( + VeriTixErrorCode.MaxCollaboratorsReached, + `Event ${collaborator.eventId} already has the maximum of ${MAX_COLLABORATORS_PER_EVENT} collaborators.` ); } this.collaborators.set(collaborator.id, collaborator); @@ -46,7 +51,10 @@ export class CollaboratorModule { public update(id: string, changes: CollaboratorUpdate): Collaborator { const existing = this.collaborators.get(id); if (!existing) { - throw new Error(`Collaborator ${id} not found.`); + throw new VeriTixError( + VeriTixErrorCode.CollaboratorNotFound, + `Collaborator ${id} not found.` + ); } const updated: Collaborator = { ...existing, ...changes }; this.collaborators.set(id, updated); @@ -56,4 +64,4 @@ export class CollaboratorModule { public remove(id: string): boolean { return this.collaborators.delete(id); } -} +} \ No newline at end of file diff --git a/src/modules/freighter-factory.ts b/src/modules/freighter-factory.ts index abd7757..f380567 100644 --- a/src/modules/freighter-factory.ts +++ b/src/modules/freighter-factory.ts @@ -1,9 +1,15 @@ // Factory for creating VeriTixClient from Freighter wallet +import { VeriTixClient } from '../client'; +import { VeriTixError, VeriTixErrorCode } from '../utils/errors'; + export async function createFromFreighter(network: string = 'testnet') { try { const freighter = (window as any).freighter; if (!freighter) { - throw new Error('Freighter wallet not found'); + throw new VeriTixError( + VeriTixErrorCode.FreighterNotFound, + 'Freighter wallet not found' + ); } const publicKey = await freighter.getPublicKey(); return new VeriTixClient({ @@ -15,4 +21,4 @@ export async function createFromFreighter(network: string = 'testnet') { console.error('Failed to connect to Freighter:', error); throw error; } -} +} \ No newline at end of file diff --git a/src/modules/splitter.ts b/src/modules/splitter.ts index 5ece0ec..18a63bf 100644 --- a/src/modules/splitter.ts +++ b/src/modules/splitter.ts @@ -53,7 +53,10 @@ export class SplitterModule { // TODO: implement void this.config; void this.server; - throw new Error('SplitterModule.getSplit: not implemented'); + throw new VeriTixError( + VeriTixErrorCode.NotImplemented, + 'SplitterModule.getSplit: not implemented' + ); } /** @@ -232,7 +235,10 @@ export class SplitterModule { // TODO: build & submit contract call void simulateTransaction; void submitTransaction; - throw new Error('SplitterModule.createSplit: not implemented'); + throw new VeriTixError( + VeriTixErrorCode.NotImplemented, + 'SplitterModule.createSplit: not implemented' + ); } /** @@ -381,7 +387,12 @@ export class SplitterModule { * ``` */ async distribute(_id: bigint): Promise { - throw new Error('SplitterModule.distribute: not implemented'); + void this.server; + void _id; + throw new VeriTixError( + VeriTixErrorCode.NotImplemented, + 'SplitterModule.distribute: not implemented' + ); } /** @@ -415,4 +426,4 @@ export class SplitterModule { return { distributed, failed }; } -} +} \ No newline at end of file diff --git a/src/modules/ticket/ticket-purchase.service.ts b/src/modules/ticket/ticket-purchase.service.ts index bd387ba..9a1a58a 100644 --- a/src/modules/ticket/ticket-purchase.service.ts +++ b/src/modules/ticket/ticket-purchase.service.ts @@ -1,3 +1,5 @@ +import { VeriTixError, VeriTixErrorCode } from '../../utils/errors'; + /** Billing contact captured at checkout. */ export interface BillingDetails { fullName: string; @@ -38,12 +40,15 @@ export class TicketPurchaseModule { /** Confirms a purchase against remaining availability and stores a receipt. */ public purchase(orderId: string, request: PurchaseRequest, available: number): Receipt { if (!Number.isInteger(request.quantity) || request.quantity < 1) { - throw new Error('Ticket quantity must be a positive integer.'); + throw new VeriTixError( + VeriTixErrorCode.InvalidAmount, + 'Ticket quantity must be a positive integer.' + ); } if (request.quantity > available) { - throw new Error( - `Only ${available} ticket(s) remain for event ${request.eventId}; ` + - `${request.quantity} requested.`, + throw new VeriTixError( + VeriTixErrorCode.InsufficientBalance, + `Only ${available} ticket(s) remain for event ${request.eventId}; ${request.quantity} requested.` ); } const receipt: Receipt = { @@ -60,4 +65,4 @@ export class TicketPurchaseModule { public getReceipt(orderId: string): Receipt | undefined { return this.receipts.get(orderId); } -} +} \ No newline at end of file diff --git a/src/modules/user.ts b/src/modules/user.ts index 7a4d1d2..5f1c649 100644 --- a/src/modules/user.ts +++ b/src/modules/user.ts @@ -8,6 +8,7 @@ */ import { SorobanRpc, Keypair } from '@stellar/stellar-sdk'; +import { VeriTixError, VeriTixErrorCode } from '../utils/errors'; import type { NetworkConfig, TransactionResult } from '../types/index'; /** @@ -49,7 +50,10 @@ export class AdminModule { void this.config; void this.server; void this.keypair; - throw new Error('AdminModule.setAdmin: not implemented'); + throw new VeriTixError( + VeriTixErrorCode.NotImplemented, + 'AdminModule.setAdmin: not implemented' + ); } // ------------------------------------------------------------------------- @@ -66,7 +70,11 @@ export class AdminModule { */ async freeze(_address: string): Promise { // TODO: implement - throw new Error('AdminModule.freeze: not implemented'); + void _address; + throw new VeriTixError( + VeriTixErrorCode.NotImplemented, + 'AdminModule.freeze: not implemented' + ); } /** @@ -78,7 +86,11 @@ export class AdminModule { */ async unfreeze(_address: string): Promise { // TODO: implement - throw new Error('AdminModule.unfreeze: not implemented'); + void _address; + throw new VeriTixError( + VeriTixErrorCode.NotImplemented, + 'AdminModule.unfreeze: not implemented' + ); } // ------------------------------------------------------------------------- @@ -96,7 +108,12 @@ export class AdminModule { */ async clawback(_from: string, _amount: bigint): Promise { // TODO: implement - throw new Error('AdminModule.clawback: not implemented'); + void _from; + void _amount; + throw new VeriTixError( + VeriTixErrorCode.NotImplemented, + 'AdminModule.clawback: not implemented' + ); } // ------------------------------------------------------------------------- @@ -112,7 +129,10 @@ export class AdminModule { */ async pause(): Promise { // TODO: implement - throw new Error('AdminModule.pause: not implemented'); + throw new VeriTixError( + VeriTixErrorCode.NotImplemented, + 'AdminModule.pause: not implemented' + ); } /** @@ -123,6 +143,9 @@ export class AdminModule { */ async unpause(): Promise { // TODO: implement - throw new Error('AdminModule.unpause: not implemented'); + throw new VeriTixError( + VeriTixErrorCode.NotImplemented, + 'AdminModule.unpause: not implemented' + ); } -} +} \ No newline at end of file diff --git a/src/utils/errors.ts b/src/utils/errors.ts index f108cec..e7c3eb0 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -96,6 +96,20 @@ export enum VeriTixErrorCode { WatchTimeout = 'WATCH_TIMEOUT', /** Transaction was rejected by the network */ TransactionFailed = 'TRANSACTION_FAILED', + /** Feature or method is not yet implemented */ + NotImplemented = 'NOT_IMPLEMENTED', + /** Collaborator not found */ + CollaboratorNotFound = 'COLLABORATOR_NOT_FOUND', + /** Collaborator already exists */ + CollaboratorAlreadyExists = 'COLLABORATOR_ALREADY_EXISTS', + /** Maximum number of collaborators reached for event */ + MaxCollaboratorsReached = 'MAX_COLLABORATORS_REACHED', + /** Freighter wallet not found in browser */ + FreighterNotFound = 'FREIGHTER_NOT_FOUND', + /** Client not connected - call connect() first */ + ClientNotConnected = 'CLIENT_NOT_CONNECTED', + /** Invalid input parameter */ + InvalidInput = 'INVALID_INPUT', } // --------------------------------------------------------------------------- @@ -276,4 +290,4 @@ function buildMessage(code: VeriTixErrorCode, rawStr: string): string { [VeriTixErrorCode.TransactionFailed]: 'Transaction was rejected by the Stellar network.', }; return messages[code]; -} +} \ No newline at end of file diff --git a/src/utils/parsers.ts b/src/utils/parsers.ts index eec0d8f..2f28d59 100644 --- a/src/utils/parsers.ts +++ b/src/utils/parsers.ts @@ -7,6 +7,7 @@ * matching the field names used in the VeriTix Soroban contract structs. */ import { xdr, scValToNative } from '@stellar/stellar-sdk'; +import { VeriTixError, VeriTixErrorCode } from './errors'; import type { EscrowRecord, SplitRecord, @@ -34,8 +35,9 @@ import { */ function scMapToRecord(val: xdr.ScVal): Map { if (val.switch() !== xdr.ScValType.scvMap()) { - throw new Error( - `Expected ScvMap, got ScVal type: ${val.switch().name}`, + throw new VeriTixError( + VeriTixErrorCode.InvalidInput, + `Expected ScvMap, got ScVal type: ${val.switch().name}` ); } const map = new Map(); @@ -50,11 +52,15 @@ function scMapToRecord(val: xdr.ScVal): Map { * Retrieves a required field from an `ScvMap` record. * * @throws {Error} if the field is absent. + * @internal */ function getField(map: Map, field: string): xdr.ScVal { const val = map.get(field); if (val === undefined) { - throw new Error(`Missing required field "${field}" in ScvMap`); + throw new VeriTixError( + VeriTixErrorCode.InvalidInput, + `Missing required field "${field}" in ScvMap` + ); } return val; } @@ -110,7 +116,10 @@ export function parseSplitRecord(val: xdr.ScVal): SplitRecord { const recipientsVal = getField(map, 'recipients'); if (recipientsVal.switch() !== xdr.ScValType.scvVec()) { - throw new Error('Field "recipients" must be an ScvVec'); + throw new VeriTixError( + VeriTixErrorCode.InvalidInput, + 'Field "recipients" must be an ScvVec' + ); } const recipients: SplitRecipient[] = (recipientsVal.vec() ?? []).map((item) => { @@ -214,9 +223,10 @@ const DISPUTE_STATUS_MAP: Record = { function parseDisputeStatus(raw: string): DisputeStatus { const status = DISPUTE_STATUS_MAP[raw]; if (!status) { - throw new Error( - `Unknown DisputeStatus value: "${raw}". Expected one of: ${Object.keys(DISPUTE_STATUS_MAP).join(', ')}`, + throw new VeriTixError( + VeriTixErrorCode.InvalidInput, + `Unknown DisputeStatus value: "${raw}". Expected one of: ${Object.keys(DISPUTE_STATUS_MAP).join(', ')}` ); } return status; -} +} \ No newline at end of file diff --git a/src/utils/scval.ts b/src/utils/scval.ts index b83736e..d60fb6d 100644 --- a/src/utils/scval.ts +++ b/src/utils/scval.ts @@ -11,6 +11,7 @@ import { scValToNative, xdr, } from '@stellar/stellar-sdk'; +import { VeriTixError, VeriTixErrorCode } from './errors'; // --------------------------------------------------------------------------- // TypeScript → ScVal @@ -63,8 +64,9 @@ export function stringToScVal(value: string): xdr.ScVal { export function scValToString(val: xdr.ScVal): string { const native = scValToNative(val); if (typeof native !== 'string') { - throw new Error( - `Expected ScVal of type String or Symbol, got switch: ${val.switch().name}`, + throw new VeriTixError( + VeriTixErrorCode.InvalidInput, + `Expected ScVal of type String or Symbol, got switch: ${val.switch().name}` ); } return native; @@ -74,27 +76,29 @@ export function scValToString(val: xdr.ScVal): string { * Extracts a `bigint` from an `ScVal` of any integer type * (`i64`, `u64`, `i128`, `u128`, `i256`, `u256`). * - * @throws {Error} if the native value is not a `bigint` or `number`. + * @throws {VeriTixError} if the native value is not a `bigint` or `number`. */ export function scValToBigint(val: xdr.ScVal): bigint { const native = scValToNative(val); if (typeof native === 'bigint') return native; if (typeof native === 'number') return BigInt(native); - throw new Error( - `Expected ScVal to be a numeric type, got switch: ${val.switch().name}`, + throw new VeriTixError( + VeriTixErrorCode.InvalidInput, + `Expected ScVal to be a numeric type, got switch: ${val.switch().name}` ); } /** * Extracts a `boolean` from an `ScVal` of type `Bool`. * - * @throws {Error} if the `ScVal` is not a boolean type. + * @throws {VeriTixError} if the `ScVal` is not a boolean type. */ export function scValToBoolean(val: xdr.ScVal): boolean { const native = scValToNative(val); if (typeof native !== 'boolean') { - throw new Error( - `Expected ScVal of type Bool, got switch: ${val.switch().name}`, + throw new VeriTixError( + VeriTixErrorCode.InvalidInput, + `Expected ScVal of type Bool, got switch: ${val.switch().name}` ); } return native; @@ -106,13 +110,14 @@ export function scValToBoolean(val: xdr.ScVal): boolean { * > **Warning:** values exceeding `Number.MAX_SAFE_INTEGER` will lose * > precision. For large amounts, prefer {@link scValToBigint}. * - * @throws {Error} if the native value is not numeric. + * @throws {VeriTixError} if the native value is not numeric. */ export function scValToNumber(val: xdr.ScVal): number { const native = scValToNative(val); if (typeof native === 'number') return native; if (typeof native === 'bigint') return Number(native); - throw new Error( - `Expected ScVal to be a numeric type, got switch: ${val.switch().name}`, + throw new VeriTixError( + VeriTixErrorCode.InvalidInput, + `Expected ScVal to be a numeric type, got switch: ${val.switch().name}` ); -} +} \ No newline at end of file