import {BN} from '@statechannels/wallet-core'; import {Model, TransactionOrKnex} from 'objection'; import {Uint256, Destination, Bytes32} from '../type-aliases'; export type LedgerRequestStatus = | 'queued' | 'cancelled' | 'pending' // Request added to DB to be handled by ProcessLedgerQueue | 'succeeded' // Ledger update became supported and thus request succeeded | 'insufficient-funds' | 'inconsistent' // channel already exists but has a different total | 'failed'; // if the ledger closes, or there's a protocol error export interface LedgerRequestType { ledgerChannelId: Destination; channelToBeFunded: Destination; status: LedgerRequestStatus; amountA: Uint256; amountB: Uint256; lastSeenAgreedState: number | null; missedOpportunityCount: number; type: 'fund' | 'defund'; } export interface RichLedgerRequest extends LedgerRequestType { isPending: boolean; isQueued: boolean; isFund: boolean; isDefund: boolean; totalAmount: Uint256; // TODO: This should be removed. // This is included for compatibility. It is only used in one place, // when throwing an error. $id: any; } export class LedgerRequest extends Model implements LedgerRequestType { channelToBeFunded!: LedgerRequestType['channelToBeFunded']; ledgerChannelId!: LedgerRequestType['ledgerChannelId']; status!: LedgerRequestType['status']; type!: LedgerRequestType['type']; amountA!: Uint256; amountB!: Uint256; missedOpportunityCount!: number; lastSeenAgreedState!: number | null; static tableName = 'ledger_requests'; static get idColumn(): string[] { return ['channelToBeFunded', 'type']; } static async getRequest( channelToBeFunded: Bytes32, type: 'fund' | 'defund', tx: TransactionOrKnex ): Promise { return LedgerRequest.query(tx).findById([channelToBeFunded, type]); } static async setRequest( request: Omit & { missedOpportunityCount?: number; lastSeenAgreedState?: null | number; }, tx: TransactionOrKnex ): Promise { return LedgerRequest.query(tx).insert({ ...request, missedOpportunityCount: request.missedOpportunityCount || 0, lastSeenAgreedState: request.lastSeenAgreedState || null, }); } async markAsFailed(tx: TransactionOrKnex): Promise { await LedgerRequest.query(tx) .findById([this.channelToBeFunded, this.type]) .patch({status: 'failed'}); } static async setRequestStatus( channelToBeFunded: Bytes32, type: 'fund' | 'defund', status: LedgerRequestStatus, tx: TransactionOrKnex ): Promise { await LedgerRequest.query(tx).findById([channelToBeFunded, type]).patch({status}); } static async getActiveRequests( ledgerChannelId: string, tx: TransactionOrKnex ): Promise { return LedgerRequest.query(tx) .select() .where({ledgerChannelId, status: 'pending'}) .orWhere({ledgerChannelId, status: 'queued'}); } static async ledgersWithNewRequestsIds(tx: TransactionOrKnex): Promise { return ( await LedgerRequest.query(tx).column('ledgerChannelId').whereNull('lastSeenAgreedState') ).map(lr => lr.ledgerChannelId); } static async saveAll(requests: LedgerRequest[], tx: TransactionOrKnex): Promise { // TODO: can we do a batch update? for (const request of requests) { await LedgerRequest.query(tx) .findById([request.channelToBeFunded, request.type]) .update(request); } } static async requestLedgerFunding( channelToBeFunded: Destination, ledgerChannelId: Destination, amountA: Uint256, // amount to be removed from/added to participant 0's balance amountB: Uint256, // amount to be removed from/added to participant 0's balance tx: TransactionOrKnex ): Promise { await this.setRequest( { ledgerChannelId, status: 'queued', channelToBeFunded, amountA, amountB, type: 'fund', }, tx ); } static async requestLedgerDefunding( channelToBeFunded: Destination, ledgerChannelId: Destination, amountA: Uint256, // amount to be removed from/added to participant 0's balance amountB: Uint256, // amount to be removed from/added to participant 0's balance tx: TransactionOrKnex ): Promise { await this.setRequest( { ledgerChannelId, status: 'queued', channelToBeFunded, amountA, amountB, type: 'defund', }, tx ); } public get isFund(): boolean { return this.type === 'fund'; } public get isDefund(): boolean { return this.type === 'defund'; } public get isQueued(): boolean { return this.status === 'queued'; } public get isPending(): boolean { return this.status === 'pending'; } public get isActive(): boolean { return this.isQueued || this.isPending; } public get isTerminal(): boolean { return !this.isActive; } static async markLedgerRequestsSuccessful( requests: Destination[], type: 'fund' | 'defund', tx: TransactionOrKnex ): Promise { await Promise.all( requests.map(req => LedgerRequest.setRequestStatus(req, type, 'succeeded', tx)) ); } public get totalAmount(): Uint256 { return BN.add(this.amountA, this.amountB); } }