import { currentAccount , EthApi , requireAccount } from "@joincivil/ethapi" ; import { CivilErrors , estimateRawHex , getDefaultFromBlock , hashContent , hashPersonalMessage , is0x0Address , is0x0Hash , prepareNewsroomMessage , prepareUserFriendlyNewsroomMessage , promisify , recoverSigner , NewsroomRoles , } from "@joincivil/utils" ; import * as Debug from "debug" ; import { addHexPrefix , bufferToHex , setLengthLeft , toBuffer } from "ethereumjs-util" ; import { Observable } from "rxjs" ; import { TransactionReceipt , TransactionConfig , TransactionConfig as SendOptions , Transaction } from "web3-core" ; import * as zlib from "zlib" ; import { ContentProvider } from "../content/contentprovider" ; import { BigNumber , parseEther , ApprovedRevision , CharterContent , ContentId , EthAddress , EthContentHeader , Hex , NewsroomContent , NewsroomData , NewsroomWrapper , RevisionId , StorageHeader , TxHash , Uri , } from "@joincivil/typescript-types" ; import { CivilTransactionReceipt , TwoStepEthTransaction } from "../types" ; import { BaseWrapper } from "./basewrapper" ; import { NewsroomMultisigProxy } from "./generated/multisig/newsroom" ; import { MultiSigWallet as MultisigEvents } from "./generated/wrappers/multi_sig_wallet" ; import { Newsroom as Events , NewsroomContract } from "./generated/wrappers/newsroom" ; import { NewsroomFactory , NewsroomFactoryContract } from "./generated/wrappers/newsroom_factory" ; import { CreateNewsroomInGroupContract } from "./generated/wrappers/create_newsroom_in_group" ; import { MultisigProxyTransaction } from "./multisig/basemultisigproxy" ; import { Multisig } from "./multisig/multisig" ; import { MultisigTransaction } from "./multisig/multisigtransaction" ; import { createTwoStepSimple , createTwoStepTransaction , findEvent , findEventOrThrow , findEvents , } from "./utils/contracts" ; const deflate = promisify < Buffer > ( zlib . deflate ) ; const debug = Debug ( "civil:newsroom" ) ; const findContentId = ( receipt ) => new BigNumber ( findEventOrThrow < Events . Logs . ContentPublished > ( receipt , Events . Events . ContentPublished ) . returnValues . contentId ) . toNumber ( ) ; const findRevisionId = ( receipt ) => new BigNumber ( findEventOrThrow < Events . Logs . RevisionUpdated > ( receipt , Events . Events . RevisionUpdated ) . returnValues . revisionId ) . toNumber ( ) ; /**
 * A Newsroom can be thought of an organizational unit with a sole goal of providing content
 * in an organized way.
 *
 * Newsroom is controlled by access-control pattern, with multiple roles (see [[NewsroomRoles]]) that allow governing
 * what get's published and what isn't.
 *
 * All of the logic exists on the Ethereum network, with the data of articles living outside.
 * The smart-contracts doesn't limit _where_ the articles lives, as it accepts an URI with a schema.
 * And so articles can live in IPFS, Filecoin, Whisper, or just a simple HTTP server.
 * Right now the only supported systems are HTTP and [[InMemoryProvider]] for debugging purpouses
 */ export class Newsroom extends BaseWrapper < NewsroomContract > { public static async deployTrusted ( ethApi , contentProvider , newsroomName , charterUri = "" , charterHash = "" ) { if ( ( charterUri . length === 0 ) !== ( charterHash . length === 0 ) ) { throw new Error ( "Both charter URI and Hash need to be set, or both empty" ) ; } const account = await requireAccount ( ethApi ) . toPromise ( ) ; const txData = { from : account } ; const factory = await CreateNewsroomInGroupContract . singletonTrusted ( ethApi ) ; if ( ! factory ) { throw new Error ( CivilErrors . UnsupportedNetwork ) ; } return createTwoStepTransaction ( ethApi , await factory . create . sendTransactionAsync ( newsroomName , charterUri , charterHash , [ account ] , ethApi . toBigNumber ( 1 ) , txData ) , async ( factoryReceipt ) => { return Newsroom . fromFactoryReceipt ( factoryReceipt , ethApi , contentProvider ) ; } ) ; } public static async fromFactoryReceipt ( factoryReceipt , ethApi , contentProvider ) { const factory = await CreateNewsroomInGroupContract . singletonTrusted ( ethApi ) ; const newsroomFactory = await NewsroomFactoryContract . singletonTrusted ( ethApi ) ; if ( ! factory ) { throw new Error ( CivilErrors . UnsupportedNetwork ) ; } const createdNewsroom = findEvents < NewsroomFactory . Logs . ContractInstantiation > ( factoryReceipt , NewsroomFactory . Events . ContractInstantiation ) . find ( log => log . address === newsroomFactory ! . address ) ; if ( ! createdNewsroom ) { throw new Error ( "No Newsroom created during deployment through factory" ) ; } const contract = NewsroomContract . atUntrusted ( ethApi , createdNewsroom . args . instantiation ) ; const multisigProxy = await NewsroomMultisigProxy . create ( ethApi , contract ) ; const defaultBlock = getDefaultFromBlock ( await ethApi . network ( ) ) ; return new Newsroom ( ethApi , contentProvider , contract , multisigProxy , defaultBlock ) ; } public static async estimateDeployTrusted ( ethApi , newsroomName , charterUri = "" , charterHash = "" ) { const account = await requireAccount ( ethApi ) . toPromise ( ) ; const txData = { from : account } ; const factory = await CreateNewsroomInGroupContract . singletonTrusted ( ethApi ) ; if ( ! factory ) { throw new Error ( CivilErrors . UnsupportedNetwork ) ; } return factory . create . estimateGasAsync ( newsroomName , charterUri , charterHash , [ account ] , ethApi . toBigNumber ( 1 ) , txData ) ; } public static async deployNonMultisigTrusted ( ethApi , contentProvider , newsroomName , charterUri = "" , charterHash = "" ) { if ( ( charterUri . length === 0 ) !== ( charterHash . length === 0 ) ) { throw new Error ( "Both charter URI and Hash need to be set, or both empty" ) ; } const from = await currentAccount ( ethApi ) ; const txData = { from : from ! } ; return createTwoStepTransaction ( ethApi , await NewsroomContract . deployTrusted . sendTransactionAsync ( ethApi , newsroomName , charterUri , charterHash , txData ) , async ( receipt ) => Newsroom . atUntrusted ( ethApi , contentProvider , receipt . contractAddress ! ) ) ; } public static async atUntrusted ( ethApi , contentProvider , address ) { const instance = NewsroomContract . atUntrusted ( ethApi , address ) ; const multisigProxy = await NewsroomMultisigProxy . create ( ethApi , instance ) ; const defaultBlock = getDefaultFromBlock ( await ethApi . network ( ) ) ; return new Newsroom ( ethApi , contentProvider , instance , multisigProxy , defaultBlock ) ; } public static async recoverArchiveTx ( tx ) { const inflate = promisify < string > ( zlib . inflate ) ; const txDataLength = parseInt ( addHexPrefix ( tx . input . substr ( tx . input . length - 16 ) ) , 16 ) ; const content = addHexPrefix ( tx . input . substring ( txDataLength , tx . input . length - 16 ) ) ; return ( await inflate ( toBuffer ( content ) ) ) . toString ( ) ; } private multisigProxy : NewsroomMultisigProxy ; private contentProvider : ContentProvider ; private constructor ( ethApi , contentProvider , instance , multisigProxy , defaultBlock ) { super ( ethApi , instance , defaultBlock ) ; this . contentProvider = contentProvider ; this . multisigProxy = multisigProxy ; } public editors ( ) { return this . instance . RoleAddedStream ( { role : NewsroomRoles . Editor } , { fromBlock : this . defaultBlock } ) . map ( e => e . returnValues . grantee ) . concatFilter ( async ( e ) => this . isEditor ( e ) ) ; } param fromBlock returns public content ( fromBlock = this . defaultBlock ) { return this . instance . ContentPublishedStream ( { } , { fromBlock } ) . map ( e => new BigNumber ( e . returnValues . contentId ) ) . concatMap ( this . loadContentHeader . bind ( this ) ) ; } param contentId param fromBlock returns public revisions ( contentId ? , fromBlock = this . defaultBlock ) { const myContentId = contentId ? this . ethApi . toBigNumber ( contentId ) : undefined ; return this . instance . RevisionUpdatedStream ( { contentId : myContentId } , { fromBlock } ) . concatMap ( async ( e ) => { const contentHeader = await this . loadContentHeader ( new BigNumber ( e . returnValues . contentId ) , new BigNumber ( e . returnValues . revisionId ) ) ; return { blockNumber : e . blockNumber , transactionHash : e . transactionHash , ... contentHeader , } ; } ) ; } param fromBlock returns public nameChanges ( fromBlock = this . defaultBlock ) { return this . instance . NameChangedStream ( { } , { fromBlock } ) . map ( e => e . returnValues . newName ) ; } /**
     * Returns NewsroomWrapper (address + data) for this newsroom
     */ public async getNewsroomWrapper ( ) { const data = await this . getNewsroomData ( ) ; return { address : this . instance . address , data , } ; } /**
     * Returns NewsroomData for this newsroom
     */ public async getNewsroomData ( ) { const name = await this . getName ( ) ; const owner = await this . instance . owner . callAsync ( ) ; const owners = await this . owners ( ) ; const charterHeader = await this . getCharterHeader ( ) ; return { name , owner , owners , charterHeader , } ; } /**
     * Returns a list of Board of Directors with superuser powers over this
     * newsroom.
     */ public async owners ( ) { return this . multisigProxy . owners ( ) ; } public async addOwner ( owner ) { const address = await this . multisigProxy . getMultisigAddress ( ) ; const contract = await Multisig . atUntrusted ( this . ethApi , address ! ) ; return contract . addOwner ( owner ) ; } public async estimateAddOwner ( owner ) { const address = await this . multisigProxy . getMultisigAddress ( ) ; const contract = await Multisig . atUntrusted ( this . ethApi , address ! ) ; return contract . estimateAddOwner ( owner ) ; } /**
     * Returns the address of the multisig that controls this newsroom if one is defined
     */ public async getMultisigAddress ( ) { return this . multisigProxy . getMultisigAddress ( ) ; } param eth param to public async transferEthFromMultisig ( eth , to ) { const wei = parseEther ( eth ) ; const address = await this . multisigProxy . getMultisigAddress ( ) ; const multisig = await Multisig . atUntrusted ( this . ethApi , address ! ) ; return multisig . submitTransaction ( to , wei , "0x" ) ; } param address throws public async isOwner ( address ? ) { let who = address ; if ( ! who ) { who = await requireAccount ( this . ethApi ) . toPromise ( ) ; } return this . multisigProxy . isOwner ( who ) ; } param address throws public async isEditor ( address ? ) { let who = address ; if ( ! who ) { who = await requireAccount ( this . ethApi ) . toPromise ( ) ; } return this . instance . hasRole . callAsync ( who , NewsroomRoles . Editor ) ; } param address throws public async hasEditorCapabilities ( address ? ) { if ( await this . isOwner ( address ) ) { return true ; } let who = address ; if ( ! who ) { who = await requireAccount ( this . ethApi ) . toPromise ( ) ; } return this . instance . hasRole . callAsync ( who , NewsroomRoles . Editor ) ; } public async getArticleHeader ( articleId ) { return this . loadContentHeader ( articleId ) ; } public async getCharterHeader ( ) { return this . getArticleHeader ( new BigNumber ( 0 ) ) ; } throws public async getCharter ( ) { const charterData = await this . loadArticle ( new BigNumber ( 0 ) ) ; if ( ! charterData ) { return charterData ; } if ( typeof charterData . content !== "object" ) { try { charterData . content = JSON . parse ( charterData . content ) ; } catch ( e ) { debug ( `Charter content not in expected format: ${ charterData }` , e ) ; throw CivilErrors . MalformedCharter ; } } return charterData as CharterContent ; } param articleId public async loadArticle ( articleId ) { const header = await this . loadContentHeader ( articleId ) ; if ( header . contentHash && ! is0x0Hash ( header . contentHash ) ) { return this . resolveContent ( header ) ; } else { return undefined ; } } param header public async resolveContent ( header ) { try { const content = await this . contentProvider . get ( header ) ; return { ... header , content , } ; } catch ( e ) { debug ( `Resolving Content failed for EthContentHeader: ${ header }` , e ) ; return ; } } param articleId public async loadContentHeader ( contentId , revisionId ? ) { let revision = revisionId ; if ( ! revision ) { revision = new BigNumber ( await this . instance . revisionCount . callAsync ( new BigNumber ( contentId ) . toString ( ) ) ) . sub ( new BigNumber ( 1 ) . toString ( ) ) ; } const myContentId = this . ethApi . toBigNumber ( contentId ) ; let contentHash ; let uri ; let timestamp ; let author ; let signature ; let myRevisionId ; if ( revision ) { myRevisionId = this . ethApi . toBigNumber ( revision ) ; [ contentHash , uri , timestamp , author , signature ] = await this . instance . getRevision . callAsync ( myContentId , myRevisionId ! ) ; } else { [ contentHash , uri , timestamp , author , signature ] = await this . instance . getContent . callAsync ( myContentId . toNumber ( ) ) ; } return { contentId : myContentId . toNumber ( ) , revisionId : myRevisionId ? new BigNumber ( myRevisionId ) . toNumber ( ) : 0 , timestamp : new Date ( parseInt ( timestamp , 10 ) * 1000 ) , uri , contentHash , author , signature , verifySignature : ( ) => { if ( is0x0Address ( author ) ) { return false ; } const message = prepareNewsroomMessage ( this . address , contentHash ) ; const hashedPersonal = hashPersonalMessage ( message ) ; const recovered = recoverSigner ( { signature , messageHash : hashedPersonal . messageHash } ) ; if ( recovered !== author ) { throw new Error ( `The signature for ${ contentId } is invalid` ) ; } return true ; } , } ; } public async getName ( ) { return this . instance . name . callAsync ( ) ; } param actor param role throws throws public async addRole ( actor , role ) { if ( await this . isOwner ( ) ) { return this . multisigProxy . addRole . sendTransactionAsync ( actor , role ) ; } await this . requireEditor ( ) ; return createTwoStepSimple ( this . ethApi , await this . instance . addRole . sendTransactionAsync ( actor , role ) ) ; } param actor param role throws throws public async removeRole ( actor , role ) { if ( await this . isOwner ( ) ) { return this . multisigProxy . removeRole . sendTransactionAsync ( actor , role ) ; } await this . requireEditor ( ) ; return createTwoStepSimple ( this . ethApi , await this . instance . removeRole . sendTransactionAsync ( actor , role ) ) ; } public async removeOwner ( actor ) { await this . requireOwner ( ) ; const address = await this . getMultisigAddress ( ) ; const contract = await Multisig . atUntrusted ( this . ethApi , address ! ) ; return contract . removeOwner ( actor ) ; } param newName throws throws public async setName ( newName ) { await this . requireOwner ( ) ; return this . multisigProxy . setName . sendTransactionAsync ( newName ) ; } public txArchiveForContentId ( contentId , revisionId ) { const myContentId = this . ethApi . toBigNumber ( contentId ) ; const myRevisionId = this . ethApi . toBigNumber ( revisionId ) ; return this . instance . RevisionUpdatedStream ( { contentId : myContentId , revisionId : myRevisionId } , { fromBlock : this . defaultBlock } ) . concatMap ( async ( item ) => { const transaction = await this . ethApi . getTransaction ( item . transactionHash ) ; return Newsroom . recoverArchiveTx ( transaction ) ; } ) ; } public async estimatePublishURIAndHash ( uriOrContent , hash , author = "0x" , signature = "0x" , archive ? ) { const uriForEstimate = archive ? "self-tx:1.0" : uriOrContent ; const data = await this . instance . publishContent . getRaw ( uriForEstimate , hash , author , signature , { gas : 0 } ) ; if ( ! ( await this . isEditor ( ) ) && ( await this . isOwner ( ) ) ) { if ( archive ) { return this . estimateFromDataMultiSig ( data , uriOrContent ) ; } else { return this . estimateFromDataMultiSig ( data ) ; } } else { const baseGas = await this . instance . publishContent . estimateGasAsync ( uriForEstimate , hash , author , signature , { } ) ; let additionalGas = 0 ; if ( archive ) { additionalGas = await this . estimateFromContent ( uriOrContent , data . data ! . length ) ; } return baseGas + additionalGas ; } } public async estimateUpdateURIAndHash ( contentId , uriOrContent , hash , signature = "0x" , archive ? ) { const uriForEstimate = archive ? "self-tx:1.0" : uriOrContent ; const data = await this . instance . updateRevision . getRaw ( this . ethApi . toBigNumber ( contentId ) , uriForEstimate , hash , signature , { gas : 0 } ) ; if ( ! ( await this . isEditor ( ) ) && ( await this . isOwner ( ) ) ) { if ( archive ) { return this . estimateFromDataMultiSig ( data , uriOrContent ) ; } else { return this . estimateFromDataMultiSig ( data ) ; } } else { const baseGas = await this . instance . updateRevision . estimateGasAsync ( this . ethApi . toBigNumber ( contentId ) , uriForEstimate , hash , signature , { } ) ; let additionalGas = 0 ; if ( archive ) { additionalGas = await this . estimateFromContent ( uriOrContent , data . data ! . length ) ; } return baseGas + additionalGas ; } } public async estimateFromDataMultiSig ( data , content ? ) { const address = await this . multisigProxy . getMultisigAddress ( ) ; const contract = await Multisig . atUntrusted ( this . ethApi , address ! ) ; const baseGas = await contract . estimateTransaction ( address ! , this . ethApi . toBigNumber ( 0 ) , data . data ! ) ; let additionalGas = 0 ; if ( content ) { const multiSigData = await contract . getRawTransaction ( address ! , this . ethApi . toBigNumber ( 0 ) , data . data ! ) ; additionalGas = await this . estimateFromContent ( content , multiSigData . data ! . length ) ; } return baseGas + additionalGas ; } public async estimateFromContent ( content , dataLength ) { const revision = typeof content === "string" ? content : JSON . stringify ( content ) ; const buffer = await deflate ( revision ) ; const hex = bufferToHex ( buffer ) ; const length = bufferToHex ( setLengthLeft ( toBuffer ( dataLength ) , 8 ) ) ; const extra = hex . substr ( 2 ) + length . substr ( 2 ) ; return estimateRawHex ( extra ) ; } public async publishURIAndHash ( uri , hash , author = "0x" , signature = "0x" ) { if ( ! ( await this . isEditor ( ) ) && ( await this . isOwner ( ) ) ) { return this . twoStepOrMulti ( await this . multisigProxy . publishContent . sendTransactionAsync ( uri , hash , author , signature ) , findContentId ) ; } else { await this . requireEditor ( ) ; return createTwoStepTransaction ( this . ethApi , await this . instance . publishContent . sendTransactionAsync ( uri , hash , author , signature ) , findContentId ) ; } } public async updateRevisionURIAndHash ( contentId , uri , hash , signature = "0x" ) { if ( contentId === 0 ) { await this . requireOwner ( ) ; return this . twoStepOrMulti ( await this . multisigProxy . updateRevision . sendTransactionAsync ( this . ethApi . toBigNumber ( contentId ) , uri , hash , signature ) , findRevisionId ) ; } else if ( ! ( await this . isEditor ( ) ) && ( await this . isOwner ( ) ) ) { return this . twoStepOrMulti ( await this . multisigProxy . updateRevision . sendTransactionAsync ( this . ethApi . toBigNumber ( contentId ) , uri , hash , signature ) , findRevisionId ) ; } else { await this . requireEditor ( ) ; return createTwoStepTransaction ( this . ethApi , await this . instance . updateRevision . sendTransactionAsync ( this . ethApi . toBigNumber ( contentId ) , uri , hash , signature ) , findRevisionId ) ; } } public async addArchiveToMultisig ( data , hex , gas ) { const multiSigAddress = await this . multisigProxy . getMultisigAddress ( ) ; const contract = await Multisig . atUntrusted ( this . ethApi , multiSigAddress ! ) ; const multiSigTxData = await contract . getRawTransaction ( this . address , this . ethApi . toBigNumber ( 0 ) , data . data ! ) ; const length = bufferToHex ( setLengthLeft ( toBuffer ( multiSigTxData . data ! . length ) , 8 ) ) ; const extra = hex . substr ( 2 ) + length . substr ( 2 ) ; multiSigTxData . gas = gas ; multiSigTxData . data = multiSigTxData . data + extra ; return multiSigTxData ; } public async publishWithArchive ( content , hash , author = "0x" , signature = "0x" ) { const revision = typeof content === "string" ? content : JSON . stringify ( content ) ; const gas = await this . estimatePublishURIAndHash ( content , hash , author , signature , true ) ; const data = await this . instance . publishContent . getRaw ( "self-tx:1.0" , hash , author , signature , { gas } ) ; const buffer = await deflate ( revision ) ; const hex = bufferToHex ( buffer ) ; if ( ! ( await this . isEditor ( ) ) && ( await this . isOwner ( ) ) ) { const multiSigData = await this . addArchiveToMultisig ( data , hex , gas ) ; return createTwoStepTransaction ( this . ethApi , await this . ethApi . sendTransaction ( multiSigData ) , findContentId ) ; } else { await this . requireEditor ( ) ; const txData = await this . instance . publishContent . getRaw ( "self-tx:1.0" , hash , author , signature , { gas } ) ; const length = bufferToHex ( setLengthLeft ( toBuffer ( txData . data ! . length ) , 8 ) ) ; const extra = hex . substr ( 2 ) + length . substr ( 2 ) ; txData . data = txData . data + extra ; txData . gas = gas ; return createTwoStepTransaction ( this . ethApi , await this . ethApi . sendTransaction ( txData ) , findContentId ) ; } } public async updateRevisionURIAndHashWithArchive ( contentId , content , hash , signature = "0x" ) { const revision = typeof content === "string" ? content : JSON . stringify ( content ) ; const gas = await this . estimateUpdateURIAndHash ( contentId , content , hash , signature , true ) ; const data = await this . instance . updateRevision . getRaw ( this . ethApi . toBigNumber ( contentId ) , "self-tx:1.0" , hash , signature , { gas } ) ; const buffer = await deflate ( revision ) ; const hex = bufferToHex ( buffer ) ; if ( ! ( await this . isEditor ( ) ) && ( await this . isOwner ( ) ) ) { const multiSigData = await this . addArchiveToMultisig ( data , hex , gas ) ; return createTwoStepTransaction ( this . ethApi , await this . ethApi . sendTransaction ( multiSigData ) , findRevisionId ) ; } else { await this . requireEditor ( ) ; const txData = await this . instance . updateRevision . getRaw ( this . ethApi . toBigNumber ( contentId ) , "self-tx:1.0" , hash , signature , { gas } ) ; const length = bufferToHex ( setLengthLeft ( toBuffer ( txData . data ! . length ) , 8 ) ) ; const extra = hex . substr ( 2 ) + length . substr ( 2 ) ; txData . data = txData . data + extra ; txData . gas = gas ; return createTwoStepTransaction ( this . ethApi , await this . ethApi . sendTransaction ( txData ) , findRevisionId ) ; } } public async contentIdFromTxHash ( txHash ) { const publishReceipt = await this . ethApi . awaitReceipt < CivilTransactionReceipt > ( txHash ) ; return findContentId ( publishReceipt ) ; } param content param signedData returns public async publishContent ( content , signedData ? ) { const { storageHeader , author , signature } = await this . uploadToStorage ( content , signedData ) ; if ( this . isOwner ( ) ) { return this . twoStepOrMulti ( await this . multisigProxy . publishContent . sendTransactionAsync ( storageHeader . uri , storageHeader . contentHash ! , author , signature ) , findContentId ) ; } else { await this . requireEditor ( ) ; return createTwoStepTransaction ( this . ethApi , await this . instance . publishContent . sendTransactionAsync ( storageHeader . uri , storageHeader . contentHash ! , author , signature ) , findContentId ) ; } } public async revisionFromTxHash ( txHash ) { const revisionReceipt = await this . ethApi . awaitReceipt < CivilTransactionReceipt > ( txHash ) ; return findRevisionId ( revisionReceipt ) ; } param contentId param content param signedData public async updateRevision ( contentId , content , signedData ? ) { const { storageHeader , signature } = await this . uploadToStorage ( content , signedData ) ; if ( this . isOwner ( ) ) { return this . twoStepOrMulti ( await this . multisigProxy . updateRevision . sendTransactionAsync ( this . ethApi . toBigNumber ( contentId ) , storageHeader . uri , storageHeader . contentHash ! , signature ) , findRevisionId ) ; } else { await this . requireEditor ( ) ; return createTwoStepTransaction ( this . ethApi , await this . instance . updateRevision . sendTransactionAsync ( this . ethApi . toBigNumber ( contentId ) , storageHeader . uri , storageHeader . contentHash ! , signature ) , findRevisionId ) ; } } param contentId param revisionId param signedData public async signRevision ( contentId , revisionId , signedData ) { const contentHeader = await this . loadContentHeader ( new BigNumber ( contentId ) , revisionId ) ; this . verifyApprovedRevision ( contentHeader , signedData ) ; if ( ! is0x0Address ( contentHeader . author ) && contentHeader . author !== signedData . author ) { throw new Error ( CivilErrors . MalformedParams ) ; } if ( this . isOwner ( ) ) { return this . twoStepOrMulti ( await this . multisigProxy . signRevision . sendTransactionAsync ( this . ethApi . toBigNumber ( contentId ) , this . ethApi . toBigNumber ( revisionId ) , signedData . author , signedData . signature ) , receipt => receipt ) ; } else { await this . requireEditor ( ) ; return createTwoStepSimple ( this . ethApi , await this . instance . signRevision . sendTransactionAsync ( this . ethApi . toBigNumber ( contentId ) , this . ethApi . toBigNumber ( revisionId ) , signedData . author , signedData . signature ) ) ; } } param content returns public async approveByAuthor ( content ) { const author = await requireAccount ( this . ethApi ) . toPromise ( ) ; const contentHash = hashContent ( content ) ; const message = prepareNewsroomMessage ( this . address , contentHash ) ; const { signature } = await this . ethApi . signMessage ( message , author ) ; return { author , contentHash , signature , date : new Date ( ) . toISOString ( ) , newsroomAddress : this . address , } ; } public async approveByAuthorPersonalSign ( contentHash ) { const author = await requireAccount ( this . ethApi ) . toPromise ( ) ; const name = await this . getName ( ) ; const message = prepareUserFriendlyNewsroomMessage ( this . address , contentHash , name ) ; const { signature } = await this . ethApi . signMessage ( message , author ) ; const date = new Date ( ) . toISOString ( ) ; return { author , contentHash , signature , date , newsroomAddress : this . address , } ; } private async requireEditor ( ) { await this . requireRole ( NewsroomRoles . Editor ) ; } private async requireRole ( role ) { const account = await requireAccount ( this . ethApi ) . toPromise ( ) ; if ( ( await this . instance . owner . callAsync ( ) ) !== account ) { if ( ! ( await this . instance . hasRole . callAsync ( account , role ) ) ) { throw new Error ( CivilErrors . NoPrivileges ) ; } } } private async requireOwner ( ) { if ( ! ( await this . isOwner ( ) ) ) { throw new Error ( CivilErrors . NoPrivileges ) ; } } private verifyApprovedRevision ( storageHeader , signedData ) { if ( signedData . newsroomAddress !== this . address ) { throw new Error ( CivilErrors . MalformedParams ) ; } if ( storageHeader . contentHash !== signedData . contentHash ) { throw new Error ( CivilErrors . NoPrivileges ) ; } } private async uploadToStorage ( content , signedData ? ) { const storageHeader = await this . contentProvider . put ( content ) ; let author = "0x" ; let signature = "0x" ; if ( signedData ) { this . verifyApprovedRevision ( storageHeader , signedData ) ; author = signedData . author ; signature = signedData . signature ; } return { storageHeader , author , signature , } ; } /**
     * Used when the transaction might go through a multisig wallet
     * In that case, more confirmations might be needed from other owners
     */ private twoStepOrMulti < T > ( tx , transformation ) { return createTwoStepTransaction ( this . ethApi , tx . txHash , async ( receipt ) => { if ( tx . isProxied && ! findEvent < MultisigEvents . Logs . Execution > ( receipt , MultisigEvents . Events . Execution ) ) { const transactionId = await tx . proxiedId ! ( ) ; return this . multisigProxy . requireMultisig ( ) . transaction ( transactionId ) ; } return transformation ( receipt ) ; } ) ; } }