import * as _ from 'lodash' ; import Dexie , { Transaction , TransactionMode } from 'dexie' ; import { Objective , DomainBudget , ChannelStoredData , AssetBudget , BN } from '@statechannels/wallet-core' ; import { unreachable } from '../utils' ; import { logger } from '../logger' ; import { ChannelStoreEntry } from './channel-store-entry' ; import { DBBackend , ObjectStores , TXMode } from '.' ; const STORES = [ ObjectStores . budgets , ObjectStores . channels , ObjectStores . ledgers , ObjectStores . nonces , ObjectStores . objectives , ObjectStores . privateKeys , ObjectStores . destinationAddress ] ; export class Backend implements DBBackend { private _db : Dexie ; constructor ( ) { if ( ! indexedDB ) { logger . error ( "Your browser doesn't support a stable version of IndexedDB." ) ; } } param cleanSlate param databaseName public async initialize ( cleanSlate , databaseName ) { const createdDB = await this . create ( databaseName ) ; if ( cleanSlate ) await Promise . all ( STORES . map ( this . clear . bind ( this ) ) ) ; return createdDB ; } private async create ( databaseName ) { this . _db = new Dexie ( databaseName , { indexedDB } ) ; this . _db . version ( 3 ) . stores ( _ . reduce ( STORES . map ( s => ( { [ s ] : '' } ) ) , _ . merge ) ) . upgrade ( tx => { const numberify = n => Number ( BN . from ( n ) ) ; tx . table ( ObjectStores . channels ) . each ( ( { key : channelId , value } ) => { const { challengeDuration , channelNonce } = value . channelConstants ; value . channelConstants . challengeDuration = numberify ( challengeDuration ) ; value . channelConstants . channelNonce = numberify ( channelNonce ) ; value . stateVariables . map ( s => ( s . turnNum = numberify ( s . turnNum ) ) ) ; this . setChannel ( channelId , value ) ; } ) ; tx . table ( ObjectStores . nonces ) . each ( ( { key , value } ) => this . setNonce ( key , numberify ( value ) ) ) ; } ) ; } public async clear ( storeName ) { return this . _db [ storeName ] ?. clear ( ) ; } public async channels ( ) { const channelData = ( await this . getAll ( ObjectStores . channels , true ) ) as { key : string ; value : ChannelStoredData ; } [ ] ; const channels = { } ; channelData . forEach ( cd => { channels [ cd . key ] = ChannelStoreEntry . fromJson ( cd . value ) ; } ) ; return channels ; } public async objectives ( ) { return this . getAll ( ObjectStores . objectives ) ; } public async nonces ( ) { const nonces = await this . getAll ( ObjectStores . nonces ) ; for ( const key in nonces ) { nonces [ key ] = nonces [ key ] ?? - 1 ; } return nonces ; } public async privateKeys ( ) { return this . getAll ( ObjectStores . privateKeys ) ; } public async ledgers ( ) { return this . getAll ( ObjectStores . ledgers ) ; } public async getBudget ( key ) { const budget = await this . get ( ObjectStores . budgets , key ) ; if ( ! budget ) return budget ; return { ... budget , forAsset : _ . mapValues ( budget . forAsset , ( assetBudget ) => ( { asset : assetBudget . asset , availableReceiveCapacity : BN . from ( assetBudget . availableReceiveCapacity ) , availableSendCapacity : BN . from ( assetBudget . availableSendCapacity ) , channels : assetBudget . channels } ) ) } ; } public async setBudget ( key , value ) { const result = await this . put ( ObjectStores . budgets , value , key ) ; return result . value ; } public async deleteBudget ( key ) { return this . delete ( ObjectStores . budgets , key ) ; } public async getChannel ( key ) { const channel = await this . get ( ObjectStores . channels , key ) ; return channel && ChannelStoreEntry . fromJson ( channel ) ; } public async getObjective ( key ) { return this . get ( ObjectStores . objectives , key ) ; } public async getNonce ( key ) { return ( await this . get ( ObjectStores . nonces , key ) ) ?? - 1 ; } public async getPrivateKey ( key ) { return this . get ( ObjectStores . privateKeys , key ) ; } public async getLedger ( key ) { return this . get ( ObjectStores . ledgers , key ) ; } public async getDestinationAddress ( ) { return this . get ( ObjectStores . destinationAddress , 0 ) ; } public async setDestinationAddress ( address ) { return this . put ( ObjectStores . destinationAddress , address , 0 ) ; } public async setPrivateKey ( signingAddress , privateKey ) { return this . put ( ObjectStores . privateKeys , privateKey , signingAddress ) ; } public async setChannel ( key , value ) { return this . put ( ObjectStores . channels , value , key ) ; } public async setLedger ( key , value ) { return this . put ( ObjectStores . ledgers , value , key ) ; } public async setNonce ( key , value ) { await this . put ( ObjectStores . nonces , value , key ) ; return await this . _db [ ObjectStores . nonces ] . get ( key ) ; } public async setObjective ( key , value ) { return this . put ( ObjectStores . objectives , value , Number ( key ) ) as Promise < Objective > ; } public get transactionOngoing ( ) { return ! ! Dexie . currentTransaction ; } public async transaction < T , S extends ObjectStores > ( mode , stores , callback ) { let dexieMode ; switch ( mode ) { case 'readwrite' : dexieMode = 'rw' ; break ; case 'readonly' : dexieMode = 'r' ; break ; default : return unreachable ( mode ) ; } try { return await this . _db . transaction ( dexieMode , stores , callback ) ; } catch ( error ) { logger . error ( { error : error . message ?? error , store : await this . dump ( ) , callback : callback . toString ( ) . slice ( 0 , 200 ) + '...}' } , 'Transaction error' ) ; throw error ; } } param storeName param asArray private async getAll ( storeName , asArray = false ) { return asArray ? this . _db [ storeName ] . toArray ( ) : _ . mapValues ( _ . keyBy ( await this . _db [ storeName ] . toArray ( ) , 'key' ) , 'value' ) ; } param storeName param key private async get ( storeName , key ) { try { return ( await this . _db [ storeName ] . get ( key ) ) ?. value ; } catch ( e ) { if ( /NotFoundError:/ . test ( e . message ) ) { logger . error ( 'Attempting invalid access to store %s' , storeName ) ; } throw e ; } } param storeName param value param key private async put ( storeName , value , key ) { await this . _db [ storeName ] . put ( { key , value } , key ) ; return this . _db [ storeName ] . get ( key ) ; } param storeName param key returns private async delete ( storeName , key ) { return this . _db [ storeName ] . delete ( key ) ; } private async dump ( ) { return this . _db . transaction ( 'r!' , STORES , async ( ) => _ . reduce ( await Promise . all ( STORES . map ( async ( store ) => ( { [ store ] : await this . _db . table ( store ) . toArray ( ) } ) ) ) , _ . merge ) ) ; } }