import { objectiveId , Objective , OpenChannel , CloseChannel , SharedObjective , SubmitChallenge , DefundChannel , unreachable , } from '@statechannels/wallet-core' ; import { Model , TransactionOrKnex } from 'objection' ; import _ from 'lodash' ; import { WaitingFor } from '../objectives/objective-manager' ; function extractReferencedChannels ( objective ) { switch ( objective . type ) { case 'OpenChannel' : case 'CloseChannel' : case 'VirtuallyFund' : case 'SubmitChallenge' : case 'DefundChannel' : return [ objective . data . targetChannelId ] ; case 'FundGuarantor' : return [ objective . data . guarantorId ] ; case 'FundLedger' : case 'CloseLedger' : return [ objective . data . ledgerId ] ; default : return [ ] ; } } export type ObjectiveStatus = 'pending' | 'approved' | 'rejected' | 'failed' | 'succeeded' ; /**
 * A WalletObjective is a wire objective with a status, waitingFor state-string, timestamps and an objectiveId
 *
 * Limited to 'OpenChannel', 'CloseChannel', 'SubmitChallenge' and 'DefundChannel' which are the only objectives
 * that are currently supported by the server wallet
 *
 */ type ExtraProperties = { objectiveId : string ; status : ObjectiveStatus ; waitingFor : WaitingFor ; createdAt : Date ; progressLastMadeAt : Date ; } ; export type WalletObjective < O extends SupportedObjective = SupportedObjective > = O & ExtraProperties ; export function isSharedObjective ( objective ) { return objective . type === 'OpenChannel' || objective . type === 'CloseChannel' ; } type SupportedObjective = OpenChannel | CloseChannel | SubmitChallenge | DefundChannel ; export function isSupportedObjective ( o ) { return ( o . type === 'OpenChannel' || o . type === 'CloseChannel' || o . type === 'SubmitChallenge' || o . type === 'DefundChannel' ) ; } export const toWireObjective = ( dbObj ) : SharedObjective => { if ( dbObj . type === 'SubmitChallenge' || dbObj . type === 'DefundChannel' ) { throw new Error ( 'SubmitChallenge and DefundChannel objectives are not supported as wire objectives' ) ; } /**
     * This switch statement is unfortunate but is necessary to avoid a typescript error.
     * It seems that the typescript error is the result of:
     * - The parameter dbObj is a union type.
     * - The return type is a union type.
     * - The return value is constructed by creating an object with type, data, and participants fields.
     * - Typescript considers a return object with "type" OpenChannel and "data" of CloseChannel objective to be a possible return type.
     * - The above object does not belong to the union return type.
     */ switch ( dbObj . type ) { case 'OpenChannel' : return { type : dbObj . type , data : dbObj . data , participants : dbObj . participants , } ; case 'CloseChannel' : return { type : dbObj . type , data : dbObj . data , participants : dbObj . participants , } ; default : unreachable ( dbObj ) ; } } ; export class ObjectiveChannelModel extends Model { readonly objectiveId ! : string ; readonly channelId ! : string ; static tableName = 'objectives_channels' ; static get idColumn ( ) : string [ ] { return [ 'objectiveId' , 'channelId' ] ; } } export class ObjectiveModel extends Model { readonly objectiveId ! : ExtraProperties [ 'objectiveId' ] ; readonly status ! : ExtraProperties [ 'status' ] ; readonly type ! : SupportedObjective [ 'type' ] ; readonly data ! : SupportedObjective [ 'data' ] ; readonly waitingFor ! : ExtraProperties [ 'waitingFor' ] ; createdAt ! : Date ; progressLastMadeAt ! : Date ; static tableName = 'objectives' ; static get idColumn ( ) : string [ ] { return [ 'objectiveId' ] ; } static get relationMappings ( ) { const { Channel } = require ( './channel' ) ; return { objectivesChannels : { relation : Model . ManyToManyRelation , modelClass : Channel , join : { from : 'objectives.objectiveId' , through : { from : 'objectives_channels.objectiveId' , to : 'objectives_channels.channelId' , } , to : 'channels.channelId' , } , } , } ; } static async insert < O extends SupportedObjective = SupportedObjective > ( objectiveToBeStored , tx , approvalStatus ? , waitingFor ? ) { const id = objectiveId ( objectiveToBeStored ) ; return tx . transaction ( async ( trx ) => { const query = ObjectiveModel . query ( trx ) . insert ( { objectiveId : id , status : approvalStatus ?? 'pending' , type : objectiveToBeStored . type , data : objectiveToBeStored . data , createdAt : new Date ( ) , progressLastMadeAt : new Date ( ) , waitingFor : waitingFor ?? 'approval' , } ) . returning ( '*' ) . first ( ) ; const override = _ . pickBy ( { status : approvalStatus , waitingFor } , _ . identity ) ; const model = _ . isEmpty ( override ) ? await query . onConflict ( 'objectiveId' ) . ignore ( ) : await query . onConflict ( 'objectiveId' ) . merge ( override ) ; await Promise . all ( extractReferencedChannels ( objectiveToBeStored ) . map ( async ( value ) => ObjectiveChannelModel . query ( trx ) . insert ( { objectiveId : id , channelId : value } ) . onConflict ( [ 'objectiveId' , 'channelId' ] ) . ignore ( ) ) ) ; return model . toObjective < WalletObjective < O > > ( ) ; } ) ; } static async forId ( objectiveId , tx ) { const model = await ObjectiveModel . query ( tx ) . findById ( objectiveId ) ; return model . toObjective ( ) ; } static async forIds ( objectiveIds , tx ) { const model = await ObjectiveModel . query ( tx ) . findByIds ( objectiveIds ) ; return model . map ( m => m . toObjective ( ) ) ; } static async approve ( objectiveId , tx ) { return ( await ObjectiveModel . query ( tx ) . findById ( objectiveId ) . patch ( { status : 'approved' } ) . returning ( '*' ) . first ( ) ) . toObjective ( ) ; } static async succeed ( objectiveId , tx ) { return ObjectiveModel . query ( tx ) . findById ( objectiveId ) . patch ( { status : 'succeeded' } ) . returning ( '*' ) . first ( ) ; } static async failed ( objectiveId , tx ) { return ObjectiveModel . query ( tx ) . findById ( objectiveId ) . patch ( { status : 'failed' } ) . returning ( '*' ) . first ( ) ; } static async updateProgressDate ( objectiveId , tx ) { return ( await ObjectiveModel . query ( tx ) . findById ( objectiveId ) . patch ( { progressLastMadeAt : new Date ( Date . now ( ) ) } ) . returning ( '*' ) . first ( ) ) . toObjective ( ) ; } static async updateWaitingFor ( objectiveId , waitingFor , tx ) { return ( await ObjectiveModel . query ( tx ) . findById ( objectiveId ) . patch ( { progressLastMadeAt : new Date ( ) , waitingFor } ) . returning ( '*' ) . first ( ) ) . toObjective ( ) ; } static async approvedObjectiveIds ( targetChannelIds , tx ) { return ( await this . forChannelIds ( targetChannelIds , tx ) ) . filter ( x => x . status === 'approved' ) . map ( x => x . objectiveId ) ; } static async forChannelIds ( targetChannelIds , tx ) { const objectiveIds = ( await ObjectiveChannelModel . query ( tx ) . column ( 'objectiveId' ) . select ( ) . whereIn ( 'channelId' , targetChannelIds ) ) . map ( oc => oc . objectiveId ) ; return ( await ObjectiveModel . query ( tx ) . findByIds ( objectiveIds ) ) . map ( m => m . toObjective ( ) ) ; } toObjective < O extends SupportedObjective = SupportedObjective > ( ) { const o = { type : this . type , data : this . data , participants : [ ] as SupportedObjective [ 'participants' ] , } as O ; const walletObjective = { ... o , objectiveId : this . objectiveId , status : this . status , createdAt : this . createdAt , progressLastMadeAt : this . progressLastMadeAt , waitingFor : this . waitingFor , } ; if ( isSupportedObjective ( o ) ) { return walletObjective as WalletObjective < O > ; } throw Error ( 'unsupported' ) ; } }