import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase' import { flags } from '@oclif/command' import chalk from 'chalk' import { createTypeFromConstructor } from '@joystream/types' import { BagId } from '@joystream/types/storage' import ExitCodes from '../../ExitCodes' import { formatBalance } from '@polkadot/util' import BN from 'bn.js' export default class DeleteChannelCommand extends ContentDirectoryCommandBase { static description = 'Delete the channel and optionally all associated data objects.' static flags = { channelId: flags.integer({ char: 'c', required: true, description: 'ID of the Channel', }), force: flags.boolean({ char: 'f', default: false, description: 'Force-remove all associated channel data objects', }), } async getDataObjectsInfoFromQueryNode(channelId: number): Promise<[string, BN][]> { const dataObjects = await this.getQNApi().dataObjectsByBagId(`dynamic:channel:${channelId}`) if (dataObjects.length) { this.log('Following data objects are still associated with the channel:') dataObjects.forEach((o) => { let parentStr = '' if ('video' in o.type && o.type.video) { parentStr = ` (video: ${o.type.video.id})` } this.log(`- ${o.id} - ${o.type.__typename}${parentStr}`) }) } return dataObjects.map((o) => [o.id, new BN(o.deletionPrize)]) } async getDataObjectsInfoFromChain(channelId: number): Promise<[string, BN][]> { const dataObjects = await this.getApi().dataObjectsInBag( createTypeFromConstructor(BagId, { Dynamic: { Channel: channelId } }) ) if (dataObjects.length) { const dataObjectIds = dataObjects.map(([id]) => id.toString()) this.log(`Following data objects are still associated with the channel: ${dataObjectIds.join(', ')}`) } return dataObjects.map(([id, o]) => [id.toString(), o.deletion_prize]) } async run(): Promise { const { flags: { channelId, force }, } = this.parse(DeleteChannelCommand) // Context const channel = await this.getApi().channelById(channelId) const [actor, address] = await this.getChannelOwnerActor(channel) if (channel.num_videos.toNumber()) { this.error( `This channel still has ${channel.num_videos.toNumber()} associated video(s)!\n` + `Delete the videos first using ${chalk.magentaBright('content:deleteVideo')} command` ) } const dataObjectsInfo = this.isQueryNodeUriSet() ? await this.getDataObjectsInfoFromQueryNode(channelId) : await this.getDataObjectsInfoFromChain(channelId) if (dataObjectsInfo.length) { if (!force) { this.error(`Cannot remove associated data objects unless ${chalk.magentaBright('--force')} flag is used`, { exit: ExitCodes.InvalidInput, }) } const deletionPrize = dataObjectsInfo.reduce((sum, [, prize]) => sum.add(prize), new BN(0)) this.log( `Data objects deletion prize of ${chalk.cyanBright( formatBalance(deletionPrize) )} will be transferred to ${chalk.magentaBright(address)}` ) } await this.requireConfirmation( `Are you sure you want to remove channel ${chalk.magentaBright(channelId.toString())}${ force ? ' and all associated data objects' : '' }?` ) await this.sendAndFollowNamedTx(await this.getDecodedPair(address), 'content', 'deleteChannel', [ actor, channelId, force ? dataObjectsInfo.length : 0, ]) } }