diff --git a/packages/wallet/wallet-toolbox/src/storage/remoting/StorageClient.ts b/packages/wallet/wallet-toolbox/src/storage/remoting/StorageClient.ts index dd8e8382c..03f3d2a21 100644 --- a/packages/wallet/wallet-toolbox/src/storage/remoting/StorageClient.ts +++ b/packages/wallet/wallet-toolbox/src/storage/remoting/StorageClient.ts @@ -1,46 +1,10 @@ import { - AbortActionArgs, - AbortActionResult, - InternalizeActionArgs, - ListActionsResult, - ListCertificatesResult, - ListOutputsResult, - RelinquishCertificateArgs, - RelinquishOutputArgs, WalletInterface, - AuthFetch, - Validation, WalletLoggerInterface } from '@bsv/sdk' -import { - AuthId, - FindCertificatesArgs, - FindOutputBasketsArgs, - FindOutputsArgs, - FindProvenTxReqsArgs, - ProcessSyncChunkResult, - RequestSyncChunkArgs, - StorageCreateActionResult, - StorageInternalizeActionResult, - StorageProcessActionArgs, - StorageProcessActionResults, - SyncChunk, - UpdateProvenTxReqWithNewProvenTxArgs, - UpdateProvenTxReqWithNewProvenTxResult, - WalletStorageProvider -} from '../../sdk/WalletStorage.interfaces' -import { TableSettings } from '../schema/tables/TableSettings' -import { WERR_INVALID_OPERATION } from '../../sdk/WERR_errors' -import { WalletServices } from '../../sdk/WalletServices.interfaces' -import { TableUser } from '../schema/tables/TableUser' -import { TableSyncState } from '../schema/tables/TableSyncState' -import { TableCertificateX } from '../schema/tables/TableCertificate' -import { TableOutputBasket } from '../schema/tables/TableOutputBasket' -import { TableOutput } from '../schema/tables/TableOutput' -import { TableProvenTxReq } from '../schema/tables/TableProvenTxReq' -import { EntityTimeStamp } from '../../sdk/types' import { WalletErrorFromJson } from '../../sdk/WalletErrorFromJson' import { logWalletError } from '../../WalletLogger' +import { StorageClientBase } from './StorageClientBase' /** * `StorageClient` implements the `WalletStorageProvider` interface which allows it to @@ -56,27 +20,9 @@ import { logWalletError } from '../../WalletLogger' * * For details of the API implemented, follow the "See also" link for the `WalletStorageProvider` interface. */ -export class StorageClient implements WalletStorageProvider { - readonly endpointUrl: string - private readonly authClient: AuthFetch - private nextId = 1 - - // Track ephemeral (in-memory) "settings" if you wish to align with isAvailable() checks - public settings?: TableSettings - +export class StorageClient extends StorageClientBase { constructor (wallet: WalletInterface, endpointUrl: string) { - this.authClient = new AuthFetch(wallet) - this.endpointUrl = endpointUrl - } - - /** - * The `StorageClient` implements the `WalletStorageProvider` interface. - * It does not implement the lower level `StorageProvider` interface. - * - * @returns false - */ - isStorageProvider (): boolean { - return false + super(wallet, endpointUrl) } /// /////////////////////////////////////////////////////////////////////////// @@ -88,7 +34,7 @@ export class StorageClient implements WalletStorageProvider { * @param method The WalletStorage method name to call. * @param params The array of parameters to pass to the method in order. */ - private async rpcCall(method: string, params: unknown[]): Promise { + protected async rpcCall(method: string, params: unknown[]): Promise { const logger: WalletLoggerInterface | undefined = params[1]?.['logger'] try { @@ -147,420 +93,4 @@ export class StorageClient implements WalletStorageProvider { } } } - - /** - * @returns true once storage `TableSettings` have been retreived from remote storage. - */ - isAvailable (): boolean { - // We'll just say "yes" if we have settings - return !(this.settings == null) - } - - /** - * @returns remote storage `TableSettings` if they have been retreived by `makeAvailable`. - * @throws WERR_INVALID_OPERATION if `makeAvailable` has not yet been called. - */ - getSettings (): TableSettings { - if (this.settings == null) { - throw new WERR_INVALID_OPERATION('call makeAvailable at least once before getSettings') - } - return this.settings - } - - /** - * Must be called prior to making use of storage. - * Retreives `TableSettings` from remote storage provider. - * @returns remote storage `TableSettings` - */ - async makeAvailable (): Promise { - if (this.settings == null) { - this.settings = await this.rpcCall('makeAvailable', []) - } - return this.settings - } - - /// /////////////////////////////////////////////////////////////////////////// - // - // Implementation of all WalletStorage interface methods - // They are simple pass-thrus to rpcCall - // - // IMPORTANT: The parameter ordering must match exactly as in your interface. - /// /////////////////////////////////////////////////////////////////////////// - - /** - * Called to cleanup resources when no further use of this object will occur. - */ - async destroy (): Promise { - return await this.rpcCall('destroy', []) - } - - /** - * Requests schema migration to latest. - * Typically remote storage will ignore this request. - * @param storageName Unique human readable name for remote storage if it does not yet exist. - * @param storageIdentityKey Unique identity key for remote storage if it does not yet exist. - * @returns current schema migration identifier - */ - async migrate (storageName: string, storageIdentityKey: string): Promise { - return await this.rpcCall('migrate', [storageName]) - } - - /** - * Remote storage does not offer `Services` to remote clients. - * @throws WERR_INVALID_OPERATION - */ - getServices (): WalletServices { - // Typically, the client would not store or retrieve "Services" from a remote server. - // The "services" in local in-memory usage is a no-op or your own approach: - throw new WERR_INVALID_OPERATION( - 'getServices() not implemented in remote client. This method typically is not used remotely.' - ) - } - - /** - * Ignored. Remote storage cannot share `Services` with remote clients. - */ - setServices (v: WalletServices): void { - // Typically no-op for remote client - // Because "services" are usually local definitions to the Storage. - } - - /** - * Storage level processing for wallet `internalizeAction`. - * Updates internalized outputs in remote storage. - * Triggers proof validation of containing transaction. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Original wallet `internalizeAction` arguments. - * @returns `internalizeAction` results - */ - async internalizeAction (auth: AuthId, args: InternalizeActionArgs): Promise { - return await this.rpcCall('internalizeAction', [auth, args]) - } - - /** - * Storage level processing for wallet `createAction`. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Validated extension of original wallet `createAction` arguments. - * @returns `StorageCreateActionResults` supporting additional wallet processing to yield `createAction` results. - */ - async createAction (auth: AuthId, args: Validation.ValidCreateActionArgs): Promise { - return await this.rpcCall('createAction', [auth, args]) - } - - /** - * Storage level processing for wallet `createAction` and `signAction`. - * - * Handles remaining storage tasks once a fully signed transaction has been completed. This is common to both `createAction` and `signAction`. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `StorageProcessActionArgs` convey completed signed transaction to storage. - * @returns `StorageProcessActionResults` supporting final wallet processing to yield `createAction` or `signAction` results. - */ - async processAction (auth: AuthId, args: StorageProcessActionArgs): Promise { - return await this.rpcCall('processAction', [auth, args]) - } - - /** - * Aborts an action by `reference` string. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args original wallet `abortAction` args. - * @returns `abortAction` result. - */ - async abortAction (auth: AuthId, args: AbortActionArgs): Promise { - return await this.rpcCall('abortAction', [auth, args]) - } - - /** - * Used to both find and initialize a new user by identity key. - * It is up to the remote storage whether to allow creation of new users by this method. - * @param identityKey of the user. - * @returns `TableUser` for the user and whether a new user was created. - */ - async findOrInsertUser (identityKey): Promise<{ user: TableUser, isNew: boolean }> { - return await this.rpcCall<{ user: TableUser, isNew: boolean }>('findOrInsertUser', [identityKey]) - } - - /** - * Used to both find and insert a `TableSyncState` record for the user to track wallet data replication across storage providers. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param storageName the name of the remote storage being sync'd - * @param storageIdentityKey the identity key of the remote storage being sync'd - * @returns `TableSyncState` and whether a new record was created. - */ - async findOrInsertSyncStateAuth ( - auth: AuthId, - storageIdentityKey: string, - storageName: string - ): Promise<{ syncState: TableSyncState, isNew: boolean }> { - const r = await this.rpcCall<{ syncState: TableSyncState, isNew: boolean }>('findOrInsertSyncStateAuth', [ - auth, - storageIdentityKey, - storageName - ]) - r.syncState = this.validateEntity(r.syncState, ['when']) - return r - } - - /** - * Inserts a new certificate with fields and keyring into remote storage. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param certificate the certificate to insert. - * @returns record Id of the inserted `TableCertificate` record. - */ - async insertCertificateAuth (auth: AuthId, certificate: TableCertificateX): Promise { - const r = await this.rpcCall('insertCertificateAuth', [auth, certificate]) - return r - } - - /** - * Storage level processing for wallet `listActions`. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Validated extension of original wallet `listActions` arguments. - * @returns `listActions` results. - */ - async listActions (auth: AuthId, vargs: Validation.ValidListActionsArgs): Promise { - const r = await this.rpcCall('listActions', [auth, vargs]) - return r - } - - /** - * Storage level processing for wallet `listOutputs`. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Validated extension of original wallet `listOutputs` arguments. - * @returns `listOutputs` results. - */ - async listOutputs (auth: AuthId, vargs: Validation.ValidListOutputsArgs): Promise { - const r = await this.rpcCall('listOutputs', [auth, vargs]) - return r - } - - /** - * Storage level processing for wallet `listCertificates`. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Validated extension of original wallet `listCertificates` arguments. - * @returns `listCertificates` results. - */ - async listCertificates (auth: AuthId, vargs: Validation.ValidListCertificatesArgs): Promise { - const r = await this.rpcCall('listCertificates', [auth, vargs]) - return r - } - - /** - * Find user certificates, optionally with fields. - * - * This certificate retrieval method supports internal wallet operations. - * Field values are stored and retrieved encrypted. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `FindCertificatesArgs` determines which certificates to retrieve and whether to include fields. - * @returns array of certificates matching args. - */ - async findCertificatesAuth (auth: AuthId, args: FindCertificatesArgs): Promise { - const r = await this.rpcCall('findCertificatesAuth', [auth, args]) - this.validateEntities(r) - if (args.includeFields) { - for (const c of r) { - if (c.fields != null) this.validateEntities(c.fields) - } - } - return r - } - - /** - * Find output baskets. - * - * This retrieval method supports internal wallet operations. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `FindOutputBasketsArgs` determines which baskets to retrieve. - * @returns array of output baskets matching args. - */ - async findOutputBasketsAuth (auth: AuthId, args: FindOutputBasketsArgs): Promise { - const r = await this.rpcCall('findOutputBaskets', [auth, args]) - this.validateEntities(r) - return r - } - - /** - * Find outputs. - * - * This retrieval method supports internal wallet operations. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `FindOutputsArgs` determines which outputs to retrieve. - * @returns array of outputs matching args. - */ - async findOutputsAuth (auth: AuthId, args: FindOutputsArgs): Promise { - const r = await this.rpcCall('findOutputsAuth', [auth, args]) - this.validateEntities(r) - return r - } - - /** - * Find requests for transaction proofs. - * - * This retrieval method supports internal wallet operations. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `FindProvenTxReqsArgs` determines which proof requests to retrieve. - * @returns array of proof requests matching args. - */ - async findProvenTxReqs (args: FindProvenTxReqsArgs): Promise { - const r = await this.rpcCall('findProvenTxReqs', [args]) - this.validateEntities(r) - return r - } - - /** - * Relinquish a certificate. - * - * For storage supporting replication records must be kept of deletions. Therefore certificates are marked as deleted - * when relinquished, and no longer returned by `listCertificates`, but are still retained by storage. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args original wallet `relinquishCertificate` args. - */ - async relinquishCertificate (auth: AuthId, args: RelinquishCertificateArgs): Promise { - return await this.rpcCall('relinquishCertificate', [auth, args]) - } - - /** - * Relinquish an output. - * - * Relinquishing an output removes the output from whatever basket was tracking it. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args original wallet `relinquishOutput` args. - */ - async relinquishOutput (auth: AuthId, args: RelinquishOutputArgs): Promise { - return await this.rpcCall('relinquishOutput', [auth, args]) - } - - /** - * Process a "chunk" of replication data for the user. - * - * The normal data flow is for the active storage to push backups as a sequence of data chunks to backup storage providers. - * - * @param args a copy of the replication request args that initiated the sequence of data chunks. - * @param chunk the current data chunk to process. - * @returns whether processing is done, counts of inserts and udpates, and related progress tracking properties. - */ - async processSyncChunk (args: RequestSyncChunkArgs, chunk: SyncChunk): Promise { - const r = await this.rpcCall('processSyncChunk', [args, chunk]) - return r - } - - /** - * Request a "chunk" of replication data for a specific user and storage provider. - * - * The normal data flow is for the active storage to push backups as a sequence of data chunks to backup storage providers. - * Also supports recovery where non-active storage can attempt to merge available data prior to becoming active. - * - * @param args that identify the non-active storage which will receive replication data and constrains the replication process. - * @returns the next "chunk" of replication data - */ - async getSyncChunk (args: RequestSyncChunkArgs): Promise { - const r = await this.rpcCall('getSyncChunk', [args]) - if (r.certificateFields != null) r.certificateFields = this.validateEntities(r.certificateFields) - if (r.certificates != null) r.certificates = this.validateEntities(r.certificates) - if (r.commissions != null) r.commissions = this.validateEntities(r.commissions) - if (r.outputBaskets != null) r.outputBaskets = this.validateEntities(r.outputBaskets) - if (r.outputTagMaps != null) r.outputTagMaps = this.validateEntities(r.outputTagMaps) - if (r.outputTags != null) r.outputTags = this.validateEntities(r.outputTags) - if (r.outputs != null) r.outputs = this.validateEntities(r.outputs) - if (r.provenTxReqs != null) r.provenTxReqs = this.validateEntities(r.provenTxReqs) - if (r.provenTxs != null) r.provenTxs = this.validateEntities(r.provenTxs) - if (r.transactions != null) r.transactions = this.validateEntities(r.transactions) - if (r.txLabelMaps != null) r.txLabelMaps = this.validateEntities(r.txLabelMaps) - if (r.txLabels != null) r.txLabels = this.validateEntities(r.txLabels) - if (r.user != null) r.user = this.validateEntity(r.user) - return r - } - - /** - * Handles the data received when a new transaction proof is found in response to an outstanding request for proof data: - * - * - Creates a new `TableProvenTx` record. - * - Notifies all user transaction records of the new status. - * - Updates the proof request record to 'completed' status which enables delayed deletion. - * - * @param args proof request and new transaction proof data - * @returns results of updates - */ - async updateProvenTxReqWithNewProvenTx ( - args: UpdateProvenTxReqWithNewProvenTxArgs - ): Promise { - const r = await this.rpcCall('updateProvenTxReqWithNewProvenTx', [args]) - return r - } - - /** - * Ensures up-to-date wallet data replication to all configured backup storage providers, - * then promotes one of the configured backups to active, - * demoting the current active to new backup. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param newActiveStorageIdentityKey which must be a currently configured backup storage provider. - */ - async setActive (auth: AuthId, newActiveStorageIdentityKey: string): Promise { - return await this.rpcCall('setActive', [auth, newActiveStorageIdentityKey]) - } - - validateDate (date: Date | string | number): Date { - let r: Date - if (date instanceof Date) r = date - else r = new Date(date) - return r - } - - /** - * Helper to force uniform behavior across database engines. - * Use to process all individual records with time stamps retreived from database. - */ - validateEntity(entity: T, dateFields?: string[]): T { - entity.created_at = this.validateDate(entity.created_at) - entity.updated_at = this.validateDate(entity.updated_at) - if (dateFields != null) { - for (const df of dateFields) { - if (entity[df]) entity[df] = this.validateDate(entity[df]) - } - } - for (const key of Object.keys(entity)) { - const val = entity[key] - if (val === null) { - entity[key] = undefined - } else if (val instanceof Uint8Array) { - entity[key] = Array.from(val) - } - } - return entity - } - - /** - * Helper to force uniform behavior across database engines. - * Use to process all arrays of records with time stamps retreived from database. - * @returns input `entities` array with contained values validated. - */ - validateEntities(entities: T[], dateFields?: string[]): T[] { - for (let i = 0; i < entities.length; i++) { - entities[i] = this.validateEntity(entities[i], dateFields) - } - return entities - } } diff --git a/packages/wallet/wallet-toolbox/src/storage/remoting/StorageClientBase.ts b/packages/wallet/wallet-toolbox/src/storage/remoting/StorageClientBase.ts new file mode 100644 index 000000000..11d1bb045 --- /dev/null +++ b/packages/wallet/wallet-toolbox/src/storage/remoting/StorageClientBase.ts @@ -0,0 +1,463 @@ +import { + AbortActionArgs, + AbortActionResult, + InternalizeActionArgs, + ListActionsResult, + ListCertificatesResult, + ListOutputsResult, + RelinquishCertificateArgs, + RelinquishOutputArgs, + WalletInterface, + AuthFetch, + Validation +} from '@bsv/sdk' +import { + AuthId, + FindCertificatesArgs, + FindOutputBasketsArgs, + FindOutputsArgs, + FindProvenTxReqsArgs, + ProcessSyncChunkResult, + RequestSyncChunkArgs, + StorageCreateActionResult, + StorageInternalizeActionResult, + StorageProcessActionArgs, + StorageProcessActionResults, + SyncChunk, + UpdateProvenTxReqWithNewProvenTxArgs, + UpdateProvenTxReqWithNewProvenTxResult, + WalletStorageProvider +} from '../../sdk/WalletStorage.interfaces' +import { TableSettings } from '../schema/tables/TableSettings' +import { WERR_INVALID_OPERATION } from '../../sdk/WERR_errors' +import { WalletServices } from '../../sdk/WalletServices.interfaces' +import { TableUser } from '../schema/tables/TableUser' +import { TableSyncState } from '../schema/tables/TableSyncState' +import { TableCertificateX } from '../schema/tables/TableCertificate' +import { TableOutputBasket } from '../schema/tables/TableOutputBasket' +import { TableOutput } from '../schema/tables/TableOutput' +import { TableProvenTxReq } from '../schema/tables/TableProvenTxReq' +import { EntityTimeStamp } from '../../sdk/types' +import { validateDate, validateEntity, validateEntities, validateSyncChunkEntities } from './entityValidationHelpers' + +/** + * Abstract base class shared by `StorageClient` and `StorageMobile`. + * + * Contains all `WalletStorageProvider` method implementations and entity-validation + * helpers. Subclasses only need to provide `rpcCall`, which differs between + * the full (logger-aware) and mobile (lightweight) variants. + */ +export abstract class StorageClientBase implements WalletStorageProvider { + readonly endpointUrl: string + protected readonly authClient: AuthFetch + protected nextId = 1 + + // Track ephemeral (in-memory) "settings" if you wish to align with isAvailable() checks + public settings?: TableSettings + + constructor (wallet: WalletInterface, endpointUrl: string) { + this.authClient = new AuthFetch(wallet) + this.endpointUrl = endpointUrl + } + + /** + * The `StorageClient` implements the `WalletStorageProvider` interface. + * It does not implement the lower level `StorageProvider` interface. + * + * @returns false + */ + isStorageProvider (): boolean { + return false + } + + /** + * Make a JSON-RPC call to the remote server. + * Implemented differently by each subclass (with or without logger support). + * @param method The WalletStorage method name to call. + * @param params The array of parameters to pass to the method in order. + */ + protected abstract rpcCall(method: string, params: unknown[]): Promise + + /** + * @returns true once storage `TableSettings` have been retreived from remote storage. + */ + isAvailable (): boolean { + // We'll just say "yes" if we have settings + return !(this.settings == null) + } + + /** + * @returns remote storage `TableSettings` if they have been retreived by `makeAvailable`. + * @throws WERR_INVALID_OPERATION if `makeAvailable` has not yet been called. + */ + getSettings (): TableSettings { + if (this.settings == null) { + throw new WERR_INVALID_OPERATION('call makeAvailable at least once before getSettings') + } + return this.settings + } + + /** + * Must be called prior to making use of storage. + * Retreives `TableSettings` from remote storage provider. + * @returns remote storage `TableSettings` + */ + async makeAvailable (): Promise { + if (this.settings == null) { + this.settings = await this.rpcCall('makeAvailable', []) + } + return this.settings + } + + /// /////////////////////////////////////////////////////////////////////////// + // + // Implementation of all WalletStorage interface methods + // They are simple pass-thrus to rpcCall + // + // IMPORTANT: The parameter ordering must match exactly as in your interface. + /// /////////////////////////////////////////////////////////////////////////// + + /** + * Called to cleanup resources when no further use of this object will occur. + */ + async destroy (): Promise { + return await this.rpcCall('destroy', []) + } + + /** + * Requests schema migration to latest. + * Typically remote storage will ignore this request. + * @param storageName Unique human readable name for remote storage if it does not yet exist. + * @param storageIdentityKey Unique identity key for remote storage if it does not yet exist. + * @returns current schema migration identifier + */ + async migrate (storageName: string, storageIdentityKey: string): Promise { + return await this.rpcCall('migrate', [storageName]) + } + + /** + * Remote storage does not offer `Services` to remote clients. + * @throws WERR_INVALID_OPERATION + */ + getServices (): WalletServices { + // Typically, the client would not store or retrieve "Services" from a remote server. + // The "services" in local in-memory usage is a no-op or your own approach: + throw new WERR_INVALID_OPERATION( + 'getServices() not implemented in remote client. This method typically is not used remotely.' + ) + } + + /** + * Ignored. Remote storage cannot share `Services` with remote clients. + */ + setServices (v: WalletServices): void { + // Typically no-op for remote client + // Because "services" are usually local definitions to the Storage. + } + + /** + * Storage level processing for wallet `internalizeAction`. + * Updates internalized outputs in remote storage. + * Triggers proof validation of containing transaction. + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args Original wallet `internalizeAction` arguments. + * @returns `internalizeAction` results + */ + async internalizeAction (auth: AuthId, args: InternalizeActionArgs): Promise { + return await this.rpcCall('internalizeAction', [auth, args]) + } + + /** + * Storage level processing for wallet `createAction`. + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args Validated extension of original wallet `createAction` arguments. + * @returns `StorageCreateActionResults` supporting additional wallet processing to yield `createAction` results. + */ + async createAction (auth: AuthId, args: Validation.ValidCreateActionArgs): Promise { + return await this.rpcCall('createAction', [auth, args]) + } + + /** + * Storage level processing for wallet `createAction` and `signAction`. + * + * Handles remaining storage tasks once a fully signed transaction has been completed. This is common to both `createAction` and `signAction`. + * + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args `StorageProcessActionArgs` convey completed signed transaction to storage. + * @returns `StorageProcessActionResults` supporting final wallet processing to yield `createAction` or `signAction` results. + */ + async processAction (auth: AuthId, args: StorageProcessActionArgs): Promise { + return await this.rpcCall('processAction', [auth, args]) + } + + /** + * Aborts an action by `reference` string. + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args original wallet `abortAction` args. + * @returns `abortAction` result. + */ + async abortAction (auth: AuthId, args: AbortActionArgs): Promise { + return await this.rpcCall('abortAction', [auth, args]) + } + + /** + * Used to both find and initialize a new user by identity key. + * It is up to the remote storage whether to allow creation of new users by this method. + * @param identityKey of the user. + * @returns `TableUser` for the user and whether a new user was created. + */ + async findOrInsertUser (identityKey): Promise<{ user: TableUser, isNew: boolean }> { + return await this.rpcCall<{ user: TableUser, isNew: boolean }>('findOrInsertUser', [identityKey]) + } + + /** + * Used to both find and insert a `TableSyncState` record for the user to track wallet data replication across storage providers. + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param storageName the name of the remote storage being sync'd + * @param storageIdentityKey the identity key of the remote storage being sync'd + * @returns `TableSyncState` and whether a new record was created. + */ + async findOrInsertSyncStateAuth ( + auth: AuthId, + storageIdentityKey: string, + storageName: string + ): Promise<{ syncState: TableSyncState, isNew: boolean }> { + const r = await this.rpcCall<{ syncState: TableSyncState, isNew: boolean }>('findOrInsertSyncStateAuth', [ + auth, + storageIdentityKey, + storageName + ]) + r.syncState = validateEntity(r.syncState, ['when']) + return r + } + + /** + * Inserts a new certificate with fields and keyring into remote storage. + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param certificate the certificate to insert. + * @returns record Id of the inserted `TableCertificate` record. + */ + async insertCertificateAuth (auth: AuthId, certificate: TableCertificateX): Promise { + const r = await this.rpcCall('insertCertificateAuth', [auth, certificate]) + return r + } + + /** + * Storage level processing for wallet `listActions`. + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args Validated extension of original wallet `listActions` arguments. + * @returns `listActions` results. + */ + async listActions (auth: AuthId, vargs: Validation.ValidListActionsArgs): Promise { + const r = await this.rpcCall('listActions', [auth, vargs]) + return r + } + + /** + * Storage level processing for wallet `listOutputs`. + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args Validated extension of original wallet `listOutputs` arguments. + * @returns `listOutputs` results. + */ + async listOutputs (auth: AuthId, vargs: Validation.ValidListOutputsArgs): Promise { + const r = await this.rpcCall('listOutputs', [auth, vargs]) + return r + } + + /** + * Storage level processing for wallet `listCertificates`. + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args Validated extension of original wallet `listCertificates` arguments. + * @returns `listCertificates` results. + */ + async listCertificates (auth: AuthId, vargs: Validation.ValidListCertificatesArgs): Promise { + const r = await this.rpcCall('listCertificates', [auth, vargs]) + return r + } + + /** + * Find user certificates, optionally with fields. + * + * This certificate retrieval method supports internal wallet operations. + * Field values are stored and retrieved encrypted. + * + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args `FindCertificatesArgs` determines which certificates to retrieve and whether to include fields. + * @returns array of certificates matching args. + */ + async findCertificatesAuth (auth: AuthId, args: FindCertificatesArgs): Promise { + const r = await this.rpcCall('findCertificatesAuth', [auth, args]) + validateEntities(r) + if (args.includeFields) { + for (const c of r) { + if (c.fields != null) validateEntities(c.fields) + } + } + return r + } + + /** + * Find output baskets. + * + * This retrieval method supports internal wallet operations. + * + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args `FindOutputBasketsArgs` determines which baskets to retrieve. + * @returns array of output baskets matching args. + */ + async findOutputBasketsAuth (auth: AuthId, args: FindOutputBasketsArgs): Promise { + const r = await this.rpcCall('findOutputBaskets', [auth, args]) + validateEntities(r) + return r + } + + /** + * Find outputs. + * + * This retrieval method supports internal wallet operations. + * + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args `FindOutputsArgs` determines which outputs to retrieve. + * @returns array of outputs matching args. + */ + async findOutputsAuth (auth: AuthId, args: FindOutputsArgs): Promise { + const r = await this.rpcCall('findOutputsAuth', [auth, args]) + validateEntities(r) + return r + } + + /** + * Find requests for transaction proofs. + * + * This retrieval method supports internal wallet operations. + * + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args `FindProvenTxReqsArgs` determines which proof requests to retrieve. + * @returns array of proof requests matching args. + */ + async findProvenTxReqs (args: FindProvenTxReqsArgs): Promise { + const r = await this.rpcCall('findProvenTxReqs', [args]) + validateEntities(r) + return r + } + + /** + * Relinquish a certificate. + * + * For storage supporting replication records must be kept of deletions. Therefore certificates are marked as deleted + * when relinquished, and no longer returned by `listCertificates`, but are still retained by storage. + * + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args original wallet `relinquishCertificate` args. + */ + async relinquishCertificate (auth: AuthId, args: RelinquishCertificateArgs): Promise { + return await this.rpcCall('relinquishCertificate', [auth, args]) + } + + /** + * Relinquish an output. + * + * Relinquishing an output removes the output from whatever basket was tracking it. + * + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param args original wallet `relinquishOutput` args. + */ + async relinquishOutput (auth: AuthId, args: RelinquishOutputArgs): Promise { + return await this.rpcCall('relinquishOutput', [auth, args]) + } + + /** + * Process a "chunk" of replication data for the user. + * + * The normal data flow is for the active storage to push backups as a sequence of data chunks to backup storage providers. + * + * @param args a copy of the replication request args that initiated the sequence of data chunks. + * @param chunk the current data chunk to process. + * @returns whether processing is done, counts of inserts and udpates, and related progress tracking properties. + */ + async processSyncChunk (args: RequestSyncChunkArgs, chunk: SyncChunk): Promise { + const r = await this.rpcCall('processSyncChunk', [args, chunk]) + return r + } + + /** + * Request a "chunk" of replication data for a specific user and storage provider. + * + * The normal data flow is for the active storage to push backups as a sequence of data chunks to backup storage providers. + * Also supports recovery where non-active storage can attempt to merge available data prior to becoming active. + * + * @param args that identify the non-active storage which will receive replication data and constrains the replication process. + * @returns the next "chunk" of replication data + */ + async getSyncChunk (args: RequestSyncChunkArgs): Promise { + const r = await this.rpcCall('getSyncChunk', [args]) + return validateSyncChunkEntities(r) + } + + /** + * Handles the data received when a new transaction proof is found in response to an outstanding request for proof data: + * + * - Creates a new `TableProvenTx` record. + * - Notifies all user transaction records of the new status. + * - Updates the proof request record to 'completed' status which enables delayed deletion. + * + * @param args proof request and new transaction proof data + * @returns results of updates + */ + async updateProvenTxReqWithNewProvenTx ( + args: UpdateProvenTxReqWithNewProvenTxArgs + ): Promise { + const r = await this.rpcCall('updateProvenTxReqWithNewProvenTx', [args]) + return r + } + + /** + * Ensures up-to-date wallet data replication to all configured backup storage providers, + * then promotes one of the configured backups to active, + * demoting the current active to new backup. + * + * @param auth Identifies client by identity key and the storage identity key of their currently active storage. + * This must match the `AuthFetch` identity securing the remote conneciton. + * @param newActiveStorageIdentityKey which must be a currently configured backup storage provider. + */ + async setActive (auth: AuthId, newActiveStorageIdentityKey: string): Promise { + return await this.rpcCall('setActive', [auth, newActiveStorageIdentityKey]) + } + + /** @see {@link validateDate} */ + validateDate (date: Date | string | number): Date { return validateDate(date) } + + /** + * Helper to force uniform behavior across database engines. + * Use to process all individual records with time stamps retreived from database. + * @see {@link validateEntity} + */ + validateEntity(entity: T, dateFields?: string[]): T { + return validateEntity(entity, dateFields) + } + + /** + * Helper to force uniform behavior across database engines. + * Use to process all arrays of records with time stamps retreived from database. + * @returns input `entities` array with contained values validated. + * @see {@link validateEntities} + */ + validateEntities(entities: T[], dateFields?: string[]): T[] { + return validateEntities(entities, dateFields) + } +} diff --git a/packages/wallet/wallet-toolbox/src/storage/remoting/StorageMobile.ts b/packages/wallet/wallet-toolbox/src/storage/remoting/StorageMobile.ts index ceafb83ce..bd2f3457c 100644 --- a/packages/wallet/wallet-toolbox/src/storage/remoting/StorageMobile.ts +++ b/packages/wallet/wallet-toolbox/src/storage/remoting/StorageMobile.ts @@ -1,79 +1,21 @@ -import { - AbortActionArgs, - AbortActionResult, - InternalizeActionArgs, - ListActionsResult, - ListCertificatesResult, - ListOutputsResult, - RelinquishCertificateArgs, - RelinquishOutputArgs, - WalletInterface, - AuthFetch, - Validation -} from '@bsv/sdk' -import { - AuthId, - FindCertificatesArgs, - FindOutputBasketsArgs, - FindOutputsArgs, - FindProvenTxReqsArgs, - ProcessSyncChunkResult, - RequestSyncChunkArgs, - StorageCreateActionResult, - StorageInternalizeActionResult, - StorageProcessActionArgs, - StorageProcessActionResults, - SyncChunk, - UpdateProvenTxReqWithNewProvenTxArgs, - UpdateProvenTxReqWithNewProvenTxResult, - WalletStorageProvider -} from '../../sdk/WalletStorage.interfaces' -import { WERR_INVALID_OPERATION } from '../../sdk/WERR_errors' -import { WalletServices } from '../../sdk/WalletServices.interfaces' -import { TableSettings } from '../schema/tables/TableSettings' -import { TableUser } from '../schema/tables/TableUser' -import { TableSyncState } from '../schema/tables/TableSyncState' -import { TableCertificateX } from '../schema/tables/TableCertificate' -import { TableOutputBasket } from '../schema/tables/TableOutputBasket' -import { TableOutput } from '../schema/tables/TableOutput' -import { TableProvenTxReq } from '../schema/tables/TableProvenTxReq' -import { EntityTimeStamp } from '../../sdk/types' +import { WalletInterface } from '@bsv/sdk' +import { StorageClientBase } from './StorageClientBase' /** - * `StorageClient` implements the `WalletStorageProvider` interface which allows it to + * `StorageClient` (mobile variant) implements the `WalletStorageProvider` interface which allows it to * serve as a BRC-100 wallet's active storage. * * Internally, it uses JSON-RPC over HTTPS to make requests of a remote server. * Typically this server uses the `StorageServer` class to implement the service. * - * The `AuthFetch` component is used to secure and authenticate the requests to the remote server. - * - * `AuthFetch` is initialized with a BRC-100 wallet which establishes the identity of - * the party making requests of the remote service. + * This mobile variant omits the full logger support present in `StorageClient` to keep + * the bundle lean for mobile / browser environments. * * For details of the API implemented, follow the "See also" link for the `WalletStorageProvider` interface. */ -export class StorageClient implements WalletStorageProvider { - readonly endpointUrl: string - private readonly authClient: AuthFetch - private nextId = 1 - - // Track ephemeral (in-memory) "settings" if you wish to align with isAvailable() checks - public settings?: TableSettings - +export class StorageClient extends StorageClientBase { constructor (wallet: WalletInterface, endpointUrl: string) { - this.authClient = new AuthFetch(wallet) - this.endpointUrl = endpointUrl - } - - /** - * The `StorageClient` implements the `WalletStorageProvider` interface. - * It does not implement the lower level `StorageProvider` interface. - * - * @returns false - */ - isStorageProvider (): boolean { - return false + super(wallet, endpointUrl) } /// /////////////////////////////////////////////////////////////////////////// @@ -85,7 +27,7 @@ export class StorageClient implements WalletStorageProvider { * @param method The WalletStorage method name to call. * @param params The array of parameters to pass to the method in order. */ - private async rpcCall(method: string, params: unknown[]): Promise { + protected async rpcCall(method: string, params: unknown[]): Promise { try { const id = this.nextId++ const body = { @@ -125,420 +67,4 @@ export class StorageClient implements WalletStorageProvider { throw error_ } } - - /** - * @returns true once storage `TableSettings` have been retreived from remote storage. - */ - isAvailable (): boolean { - // We'll just say "yes" if we have settings - return !(this.settings == null) - } - - /** - * @returns remote storage `TableSettings` if they have been retreived by `makeAvailable`. - * @throws WERR_INVALID_OPERATION if `makeAvailable` has not yet been called. - */ - getSettings (): TableSettings { - if (this.settings == null) { - throw new WERR_INVALID_OPERATION('call makeAvailable at least once before getSettings') - } - return this.settings - } - - /** - * Must be called prior to making use of storage. - * Retreives `TableSettings` from remote storage provider. - * @returns remote storage `TableSettings` - */ - async makeAvailable (): Promise { - if (this.settings == null) { - this.settings = await this.rpcCall('makeAvailable', []) - } - return this.settings - } - - /// /////////////////////////////////////////////////////////////////////////// - // - // Implementation of all WalletStorage interface methods - // They are simple pass-thrus to rpcCall - // - // IMPORTANT: The parameter ordering must match exactly as in your interface. - /// /////////////////////////////////////////////////////////////////////////// - - /** - * Called to cleanup resources when no further use of this object will occur. - */ - async destroy (): Promise { - return await this.rpcCall('destroy', []) - } - - /** - * Requests schema migration to latest. - * Typically remote storage will ignore this request. - * @param storageName Unique human readable name for remote storage if it does not yet exist. - * @param storageIdentityKey Unique identity key for remote storage if it does not yet exist. - * @returns current schema migration identifier - */ - async migrate (storageName: string, storageIdentityKey: string): Promise { - return await this.rpcCall('migrate', [storageName]) - } - - /** - * Remote storage does not offer `Services` to remote clients. - * @throws WERR_INVALID_OPERATION - */ - getServices (): WalletServices { - // Typically, the client would not store or retrieve "Services" from a remote server. - // The "services" in local in-memory usage is a no-op or your own approach: - throw new WERR_INVALID_OPERATION( - 'getServices() not implemented in remote client. This method typically is not used remotely.' - ) - } - - /** - * Ignored. Remote storage cannot share `Services` with remote clients. - */ - setServices (v: WalletServices): void { - // Typically no-op for remote client - // Because "services" are usually local definitions to the Storage. - } - - /** - * Storage level processing for wallet `internalizeAction`. - * Updates internalized outputs in remote storage. - * Triggers proof validation of containing transaction. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Original wallet `internalizeAction` arguments. - * @returns `internalizeAction` results - */ - async internalizeAction (auth: AuthId, args: InternalizeActionArgs): Promise { - return await this.rpcCall('internalizeAction', [auth, args]) - } - - /** - * Storage level processing for wallet `createAction`. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Validated extension of original wallet `createAction` arguments. - * @returns `StorageCreateActionResults` supporting additional wallet processing to yield `createAction` results. - */ - async createAction (auth: AuthId, args: Validation.ValidCreateActionArgs): Promise { - return await this.rpcCall('createAction', [auth, args]) - } - - /** - * Storage level processing for wallet `createAction` and `signAction`. - * - * Handles remaining storage tasks once a fully signed transaction has been completed. This is common to both `createAction` and `signAction`. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `StorageProcessActionArgs` convey completed signed transaction to storage. - * @returns `StorageProcessActionResults` supporting final wallet processing to yield `createAction` or `signAction` results. - */ - async processAction (auth: AuthId, args: StorageProcessActionArgs): Promise { - return await this.rpcCall('processAction', [auth, args]) - } - - /** - * Aborts an action by `reference` string. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args original wallet `abortAction` args. - * @returns `abortAction` result. - */ - async abortAction (auth: AuthId, args: AbortActionArgs): Promise { - return await this.rpcCall('abortAction', [auth, args]) - } - - /** - * Used to both find and initialize a new user by identity key. - * It is up to the remote storage whether to allow creation of new users by this method. - * @param identityKey of the user. - * @returns `TableUser` for the user and whether a new user was created. - */ - async findOrInsertUser (identityKey): Promise<{ user: TableUser, isNew: boolean }> { - return await this.rpcCall<{ user: TableUser, isNew: boolean }>('findOrInsertUser', [identityKey]) - } - - /** - * Used to both find and insert a `TableSyncState` record for the user to track wallet data replication across storage providers. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param storageName the name of the remote storage being sync'd - * @param storageIdentityKey the identity key of the remote storage being sync'd - * @returns `TableSyncState` and whether a new record was created. - */ - async findOrInsertSyncStateAuth ( - auth: AuthId, - storageIdentityKey: string, - storageName: string - ): Promise<{ syncState: TableSyncState, isNew: boolean }> { - const r = await this.rpcCall<{ syncState: TableSyncState, isNew: boolean }>('findOrInsertSyncStateAuth', [ - auth, - storageIdentityKey, - storageName - ]) - r.syncState = this.validateEntity(r.syncState, ['when']) - return r - } - - /** - * Inserts a new certificate with fields and keyring into remote storage. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param certificate the certificate to insert. - * @returns record Id of the inserted `TableCertificate` record. - */ - async insertCertificateAuth (auth: AuthId, certificate: TableCertificateX): Promise { - const r = await this.rpcCall('insertCertificateAuth', [auth, certificate]) - return r - } - - /** - * Storage level processing for wallet `listActions`. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Validated extension of original wallet `listActions` arguments. - * @returns `listActions` results. - */ - async listActions (auth: AuthId, vargs: Validation.ValidListActionsArgs): Promise { - const r = await this.rpcCall('listActions', [auth, vargs]) - return r - } - - /** - * Storage level processing for wallet `listOutputs`. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Validated extension of original wallet `listOutputs` arguments. - * @returns `listOutputs` results. - */ - async listOutputs (auth: AuthId, vargs: Validation.ValidListOutputsArgs): Promise { - const r = await this.rpcCall('listOutputs', [auth, vargs]) - return r - } - - /** - * Storage level processing for wallet `listCertificates`. - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args Validated extension of original wallet `listCertificates` arguments. - * @returns `listCertificates` results. - */ - async listCertificates (auth: AuthId, vargs: Validation.ValidListCertificatesArgs): Promise { - const r = await this.rpcCall('listCertificates', [auth, vargs]) - return r - } - - /** - * Find user certificates, optionally with fields. - * - * This certificate retrieval method supports internal wallet operations. - * Field values are stored and retrieved encrypted. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `FindCertificatesArgs` determines which certificates to retrieve and whether to include fields. - * @returns array of certificates matching args. - */ - async findCertificatesAuth (auth: AuthId, args: FindCertificatesArgs): Promise { - const r = await this.rpcCall('findCertificatesAuth', [auth, args]) - this.validateEntities(r) - if (args.includeFields) { - for (const c of r) { - if (c.fields != null) this.validateEntities(c.fields) - } - } - return r - } - - /** - * Find output baskets. - * - * This retrieval method supports internal wallet operations. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `FindOutputBasketsArgs` determines which baskets to retrieve. - * @returns array of output baskets matching args. - */ - async findOutputBasketsAuth (auth: AuthId, args: FindOutputBasketsArgs): Promise { - const r = await this.rpcCall('findOutputBaskets', [auth, args]) - this.validateEntities(r) - return r - } - - /** - * Find outputs. - * - * This retrieval method supports internal wallet operations. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `FindOutputsArgs` determines which outputs to retrieve. - * @returns array of outputs matching args. - */ - async findOutputsAuth (auth: AuthId, args: FindOutputsArgs): Promise { - const r = await this.rpcCall('findOutputsAuth', [auth, args]) - this.validateEntities(r) - return r - } - - /** - * Find requests for transaction proofs. - * - * This retrieval method supports internal wallet operations. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args `FindProvenTxReqsArgs` determines which proof requests to retrieve. - * @returns array of proof requests matching args. - */ - async findProvenTxReqs (args: FindProvenTxReqsArgs): Promise { - const r = await this.rpcCall('findProvenTxReqs', [args]) - this.validateEntities(r) - return r - } - - /** - * Relinquish a certificate. - * - * For storage supporting replication records must be kept of deletions. Therefore certificates are marked as deleted - * when relinquished, and no longer returned by `listCertificates`, but are still retained by storage. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args original wallet `relinquishCertificate` args. - */ - async relinquishCertificate (auth: AuthId, args: RelinquishCertificateArgs): Promise { - return await this.rpcCall('relinquishCertificate', [auth, args]) - } - - /** - * Relinquish an output. - * - * Relinquishing an output removes the output from whatever basket was tracking it. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param args original wallet `relinquishOutput` args. - */ - async relinquishOutput (auth: AuthId, args: RelinquishOutputArgs): Promise { - return await this.rpcCall('relinquishOutput', [auth, args]) - } - - /** - * Process a "chunk" of replication data for the user. - * - * The normal data flow is for the active storage to push backups as a sequence of data chunks to backup storage providers. - * - * @param args a copy of the replication request args that initiated the sequence of data chunks. - * @param chunk the current data chunk to process. - * @returns whether processing is done, counts of inserts and udpates, and related progress tracking properties. - */ - async processSyncChunk (args: RequestSyncChunkArgs, chunk: SyncChunk): Promise { - const r = await this.rpcCall('processSyncChunk', [args, chunk]) - return r - } - - /** - * Request a "chunk" of replication data for a specific user and storage provider. - * - * The normal data flow is for the active storage to push backups as a sequence of data chunks to backup storage providers. - * Also supports recovery where non-active storage can attempt to merge available data prior to becoming active. - * - * @param args that identify the non-active storage which will receive replication data and constrains the replication process. - * @returns the next "chunk" of replication data - */ - async getSyncChunk (args: RequestSyncChunkArgs): Promise { - const r = await this.rpcCall('getSyncChunk', [args]) - if (r.certificateFields != null) r.certificateFields = this.validateEntities(r.certificateFields) - if (r.certificates != null) r.certificates = this.validateEntities(r.certificates) - if (r.commissions != null) r.commissions = this.validateEntities(r.commissions) - if (r.outputBaskets != null) r.outputBaskets = this.validateEntities(r.outputBaskets) - if (r.outputTagMaps != null) r.outputTagMaps = this.validateEntities(r.outputTagMaps) - if (r.outputTags != null) r.outputTags = this.validateEntities(r.outputTags) - if (r.outputs != null) r.outputs = this.validateEntities(r.outputs) - if (r.provenTxReqs != null) r.provenTxReqs = this.validateEntities(r.provenTxReqs) - if (r.provenTxs != null) r.provenTxs = this.validateEntities(r.provenTxs) - if (r.transactions != null) r.transactions = this.validateEntities(r.transactions) - if (r.txLabelMaps != null) r.txLabelMaps = this.validateEntities(r.txLabelMaps) - if (r.txLabels != null) r.txLabels = this.validateEntities(r.txLabels) - if (r.user != null) r.user = this.validateEntity(r.user) - return r - } - - /** - * Handles the data received when a new transaction proof is found in response to an outstanding request for proof data: - * - * - Creates a new `TableProvenTx` record. - * - Notifies all user transaction records of the new status. - * - Updates the proof request record to 'completed' status which enables delayed deletion. - * - * @param args proof request and new transaction proof data - * @returns results of updates - */ - async updateProvenTxReqWithNewProvenTx ( - args: UpdateProvenTxReqWithNewProvenTxArgs - ): Promise { - const r = await this.rpcCall('updateProvenTxReqWithNewProvenTx', [args]) - return r - } - - /** - * Ensures up-to-date wallet data replication to all configured backup storage providers, - * then promotes one of the configured backups to active, - * demoting the current active to new backup. - * - * @param auth Identifies client by identity key and the storage identity key of their currently active storage. - * This must match the `AuthFetch` identity securing the remote conneciton. - * @param newActiveStorageIdentityKey which must be a currently configured backup storage provider. - */ - async setActive (auth: AuthId, newActiveStorageIdentityKey: string): Promise { - return await this.rpcCall('setActive', [auth, newActiveStorageIdentityKey]) - } - - validateDate (date: Date | string | number): Date { - let r: Date - if (date instanceof Date) r = date - else r = new Date(date) - return r - } - - /** - * Helper to force uniform behavior across database engines. - * Use to process all individual records with time stamps retreived from database. - */ - validateEntity(entity: T, dateFields?: string[]): T { - entity.created_at = this.validateDate(entity.created_at) - entity.updated_at = this.validateDate(entity.updated_at) - if (dateFields != null) { - for (const df of dateFields) { - if (entity[df]) entity[df] = this.validateDate(entity[df]) - } - } - for (const key of Object.keys(entity)) { - const val = entity[key] - if (val === null) { - entity[key] = undefined - } else if (val instanceof Uint8Array) { - entity[key] = Array.from(val) - } - } - return entity - } - - /** - * Helper to force uniform behavior across database engines. - * Use to process all arrays of records with time stamps retreived from database. - * @returns input `entities` array with contained values validated. - */ - validateEntities(entities: T[], dateFields?: string[]): T[] { - for (let i = 0; i < entities.length; i++) { - entities[i] = this.validateEntity(entities[i], dateFields) - } - return entities - } } diff --git a/packages/wallet/wallet-toolbox/src/storage/remoting/StorageServer.ts b/packages/wallet/wallet-toolbox/src/storage/remoting/StorageServer.ts index 30afbb01a..bb870c783 100644 --- a/packages/wallet/wallet-toolbox/src/storage/remoting/StorageServer.ts +++ b/packages/wallet/wallet-toolbox/src/storage/remoting/StorageServer.ts @@ -14,6 +14,7 @@ import { StorageProvider } from '../StorageProvider' import { WERR_UNAUTHORIZED } from '../../sdk/WERR_errors' import { SyncChunk } from '../../sdk/WalletStorage.interfaces' import { EntityTimeStamp } from '../../sdk/types' +import { validateDate, validateEntity, validateEntities, validateSyncChunkEntities } from './entityValidationHelpers' import { WalletError } from '../../sdk/WalletError' import { logWalletError } from '../../WalletLogger' @@ -192,19 +193,7 @@ export class StorageServer { await this.validateParam0(params, req) // const args: RequestSyncChunkArgs = params[0] const r: SyncChunk = params[1] - if (r.certificateFields != null) r.certificateFields = this.validateEntities(r.certificateFields) - if (r.certificates != null) r.certificates = this.validateEntities(r.certificates) - if (r.commissions != null) r.commissions = this.validateEntities(r.commissions) - if (r.outputBaskets != null) r.outputBaskets = this.validateEntities(r.outputBaskets) - if (r.outputTagMaps != null) r.outputTagMaps = this.validateEntities(r.outputTagMaps) - if (r.outputTags != null) r.outputTags = this.validateEntities(r.outputTags) - if (r.outputs != null) r.outputs = this.validateEntities(r.outputs) - if (r.provenTxReqs != null) r.provenTxReqs = this.validateEntities(r.provenTxReqs) - if (r.provenTxs != null) r.provenTxs = this.validateEntities(r.provenTxs) - if (r.transactions != null) r.transactions = this.validateEntities(r.transactions) - if (r.txLabelMaps != null) r.txLabelMaps = this.validateEntities(r.txLabelMaps) - if (r.txLabels != null) r.txLabels = this.validateEntities(r.txLabels) - if (r.user != null) r.user = this.validateEntity(r.user) + validateSyncChunkEntities(r) } break default: @@ -301,45 +290,25 @@ export class StorageServer { } } - validateDate (date: Date | string | number): Date { - let r: Date - if (date instanceof Date) r = date - else r = new Date(date) - return r - } + /** @see {@link validateDate} */ + validateDate (date: Date | string | number): Date { return validateDate(date) } /** * Helper to force uniform behavior across database engines. * Use to process all individual records with time stamps retreived from database. + * @see {@link validateEntity} */ validateEntity(entity: T, dateFields?: string[]): T { - entity.created_at = this.validateDate(entity.created_at) - entity.updated_at = this.validateDate(entity.updated_at) - if (dateFields != null) { - for (const df of dateFields) { - if (entity[df]) entity[df] = this.validateDate(entity[df]) - } - } - for (const key of Object.keys(entity)) { - const val = entity[key] - if (val === null) { - entity[key] = undefined - } else if (Buffer.isBuffer(val)) { - entity[key] = Array.from(val) - } - } - return entity + return validateEntity(entity, dateFields) } /** * Helper to force uniform behavior across database engines. * Use to process all arrays of records with time stamps retreived from database. * @returns input `entities` array with contained values validated. + * @see {@link validateEntities} */ validateEntities(entities: T[], dateFields?: string[]): T[] { - for (let i = 0; i < entities.length; i++) { - entities[i] = this.validateEntity(entities[i], dateFields) - } - return entities + return validateEntities(entities, dateFields) } } diff --git a/packages/wallet/wallet-toolbox/src/storage/remoting/entityValidationHelpers.ts b/packages/wallet/wallet-toolbox/src/storage/remoting/entityValidationHelpers.ts new file mode 100644 index 000000000..cc572e03f --- /dev/null +++ b/packages/wallet/wallet-toolbox/src/storage/remoting/entityValidationHelpers.ts @@ -0,0 +1,74 @@ +import { SyncChunk } from '../../sdk/WalletStorage.interfaces' +import { EntityTimeStamp } from '../../sdk/types' + +/** + * Shared entity-validation helpers used by both client-side storage remoting + * (StorageClientBase / StorageMobile) and the server-side StorageServer. + * + * These helpers normalise records returned from remote calls or database queries: + * - Coerce date strings / timestamps to `Date` objects. + * - Replace `null` values with `undefined`. + * - Replace `Uint8Array` / `Buffer` values with plain `number[]` arrays. + */ + +export function validateDate (date: Date | string | number): Date { + if (date instanceof Date) return date + return new Date(date) +} + +/** + * Force uniform behaviour across database engines. + * Use to process all individual records with timestamps retrieved from database. + */ +export function validateEntity(entity: T, dateFields?: string[]): T { + entity.created_at = validateDate(entity.created_at) + entity.updated_at = validateDate(entity.updated_at) + if (dateFields != null) { + for (const df of dateFields) { + if (entity[df]) entity[df] = validateDate(entity[df]) + } + } + for (const key of Object.keys(entity)) { + const val = entity[key] + if (val === null) { + entity[key] = undefined + } else if (val instanceof Uint8Array) { + entity[key] = Array.from(val) + } + } + return entity +} + +/** + * Force uniform behaviour across database engines. + * Use to process all arrays of records with timestamps retrieved from database. + * @returns input `entities` array with contained values validated. + */ +export function validateEntities(entities: T[], dateFields?: string[]): T[] { + if (!Array.isArray(entities)) return entities + for (let i = 0; i < entities.length; i++) { + entities[i] = validateEntity(entities[i], dateFields) + } + return entities +} + +/** + * Validate all entity arrays within a `SyncChunk` received from a remote storage call. + * Normalises timestamps, nulls, and binary fields in-place. + */ +export function validateSyncChunkEntities (r: SyncChunk): SyncChunk { + if (r.certificateFields != null) r.certificateFields = validateEntities(r.certificateFields) + if (r.certificates != null) r.certificates = validateEntities(r.certificates) + if (r.commissions != null) r.commissions = validateEntities(r.commissions) + if (r.outputBaskets != null) r.outputBaskets = validateEntities(r.outputBaskets) + if (r.outputTagMaps != null) r.outputTagMaps = validateEntities(r.outputTagMaps) + if (r.outputTags != null) r.outputTags = validateEntities(r.outputTags) + if (r.outputs != null) r.outputs = validateEntities(r.outputs) + if (r.provenTxReqs != null) r.provenTxReqs = validateEntities(r.provenTxReqs) + if (r.provenTxs != null) r.provenTxs = validateEntities(r.provenTxs) + if (r.transactions != null) r.transactions = validateEntities(r.transactions) + if (r.txLabelMaps != null) r.txLabelMaps = validateEntities(r.txLabelMaps) + if (r.txLabels != null) r.txLabels = validateEntities(r.txLabels) + if (r.user != null) r.user = validateEntity(r.user) + return r +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97290cdfb..d0b3f40f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -727,6 +727,9 @@ importers: '@types/node': specifier: ^22.5.0 version: 22.19.17 + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.5.4 version: 5.9.3 @@ -17829,7 +17832,6 @@ snapshots: get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 - optional: true get-uri@6.0.5: dependencies: @@ -21684,8 +21686,7 @@ snapshots: resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: - optional: true + resolve-pkg-maps@1.0.0: {} resolve-pkg@2.0.0: dependencies: @@ -23011,7 +23012,6 @@ snapshots: get-tsconfig: 4.14.0 optionalDependencies: fsevents: 2.3.3 - optional: true tsyringe@4.10.0: dependencies: