'use strict' ; import * as vscode from 'vscode' ; import * as path from 'path' ; import * as fs from 'fs-extra' ; import { IBlockchainQuickPickItem , UserInputUtil } from './UserInputUtil' ; import { FabricGatewayConnectionManager } from '../fabric/FabricGatewayConnectionManager' ; import { TransactionTreeItem } from '../explorer/model/TransactionTreeItem' ; import { Reporter } from '../util/Reporter' ; import { VSCodeBlockchainOutputAdapter } from '../logging/VSCodeBlockchainOutputAdapter' ; import { ExtensionCommands } from '../../ExtensionCommands' ; import { VSCodeBlockchainDockerOutputAdapter } from '../logging/VSCodeBlockchainDockerOutputAdapter' ; import { InstantiatedTreeItem } from '../explorer/model/InstantiatedTreeItem' ; import { IFabricGatewayConnection , LogType , FabricGatewayRegistryEntry , FabricEnvironmentRegistryEntry , FabricEnvironmentRegistry , EnvironmentType } from 'ibm-blockchain-platform-common' ; import { FabricDebugConfigurationProvider } from '../debug/FabricDebugConfigurationProvider' ; import { GlobalState , ExtensionData } from '../util/GlobalState' ; interface ITransactionData { transactionName : string ; transactionLabel ? : string ; arguments : string [ ] ; transientData : any ; } function validateTxnDataObject ( txnDataObject ) { let args ; let transientData ; if ( txnDataObject . arguments ) { args = txnDataObject . arguments . map ( ( arg ) => typeof arg !== 'string' ? JSON . stringify ( arg ) : arg ) ; } else { args = [ ] ; } transientData = txnDataObject . transientData || { } ; const keys = Array . from ( Object . keys ( transientData ) ) ; if ( keys . length > 0 ) { keys . forEach ( ( key ) => { transientData [ key ] = Buffer . from ( transientData [ key ] ) ; } ) ; } return { ... txnDataObject , arguments : args , transientData } ; } async function createArgs ( transactionObject ) { let argsString ; if ( transactionObject && transactionObject . args ) { if ( Array . isArray ( transactionObject . args ) ) { return transactionObject . args ; } argsString = transactionObject . args ; } else { argsString = await UserInputUtil . showInputBox ( 'optional: What are the arguments to the transaction, (e.g. ["arg1", "arg2"])' , '[]' ) ; } if ( argsString === undefined ) { throw new Error ( 'transaction cancelled' ) ; } else if ( argsString === '' ) { return [ ] ; } else { argsString = argsString . trim ( ) ; if ( ! argsString . startsWith ( '[' ) || ! argsString . endsWith ( ']' ) ) { throw new Error ( 'transaction arguments should be in the format ["arg1", {"arg2key" : "arg2value"}]' ) ; } const args = JSON . parse ( argsString ) ; args . forEach ( ( arg , index ) => { if ( ( typeof arg !== 'string' ) ) { args [ index ] = JSON . stringify ( arg ) ; } } ) ; return args ; } } export async function submitTransaction ( evaluate , treeItem ? , channelName ? , smartContract ? , transactionObject ? ) { const outputAdapter = VSCodeBlockchainOutputAdapter . instance ( ) ; let action ; let actioning ; let actioned ; if ( evaluate ) { action = 'evaluate' ; actioning = 'evaluating' ; actioned = 'evaluated' ; } else { action = 'submit' ; actioning = 'submitting' ; actioned = 'submitted' ; } outputAdapter . log ( LogType . INFO , undefined , `${ action }Transaction` ) ; let transactionName ; let namespace ; let args ; let transientData ; let peerTargetNames = [ ] ; let peerTargetMessage = '' ; let gatewayRegistryEntry ; let errorMessage ; let connection ; if ( transactionObject ) { channelName = transactionObject . channelName ; smartContract = transactionObject . smartContract ; transactionName = transactionObject . transactionName ; namespace = transactionObject . namespace ; } else if ( treeItem instanceof TransactionTreeItem ) { smartContract = treeItem . chaincodeName ; transactionName = treeItem . name ; channelName = treeItem . channelName ; namespace = treeItem . contractName ; } else { if ( ! treeItem && ! channelName && ! smartContract ) { if ( ! FabricGatewayConnectionManager . instance ( ) . getConnection ( ) ) { await vscode . commands . executeCommand ( ExtensionCommands . CONNECT_TO_GATEWAY ) ; if ( ! FabricGatewayConnectionManager . instance ( ) . getConnection ( ) ) { return ; } } const chosenChannel = await UserInputUtil . showChannelFromGatewayQuickPickBox ( `Choose a channel to select a smart contract from` ) ; if ( ! chosenChannel ) { return ; } channelName = chosenChannel . label ; const chosenSmartContract = await UserInputUtil . showClientInstantiatedSmartContractsQuickPick ( `Choose a smart contract to ${ action } a transaction from` , channelName ) ; if ( ! chosenSmartContract ) { return ; } smartContract = chosenSmartContract . data . name ; } else if ( treeItem instanceof InstantiatedTreeItem ) { channelName = treeItem . channels [ 0 ] . label ; smartContract = treeItem . name ; } const chosenTransaction = await UserInputUtil . showTransactionQuickPick ( `Choose a transaction to ${ action }` , smartContract , channelName ) ; if ( ! chosenTransaction ) { return ; } else { transactionName = chosenTransaction . data . name ; namespace = chosenTransaction . data . contract ; } } const debugSession = vscode . debug . activeDebugSession ; if ( debugSession && debugSession . configuration . debugEvent === FabricDebugConfigurationProvider . debugEvent ) { const connected = await FabricDebugConfigurationProvider . connectToGateway ( ) ; if ( ! connected ) { return ; } } gatewayRegistryEntry = await FabricGatewayConnectionManager . instance ( ) . getGatewayRegistryEntry ( ) ; let associatedTestData ; if ( gatewayRegistryEntry . transactionDataDirectories ) { associatedTestData = gatewayRegistryEntry . transactionDataDirectories . find ( ( item ) => { return item . chaincodeName === smartContract && item . channelName === channelName ; } ) ; } if ( associatedTestData ) { if ( transactionObject ) { if ( transactionObject . txDataFile ) { const validatedTxnDataObject = validateTxnDataObject ( { transactionName , arguments : transactionObject . args , transientData : transactionObject . transientData } ) ; args = validatedTxnDataObject . arguments ; transientData = validatedTxnDataObject . transientData ; } } else { const testDataDirPath = associatedTestData . transactionDataPath ; const quickPickItems = [ ] ; const allFiles = await fs . readdir ( testDataDirPath ) ; const txDataFiles = allFiles . filter ( ( file ) => file . endsWith ( '.txdata' ) ) ; if ( txDataFiles . length > 0 ) { for ( const file of txDataFiles ) { try { const fileJson = await fs . readJSON ( path . join ( testDataDirPath , file ) ) ; fileJson . forEach ( ( txn ) => { if ( txn . transactionName === transactionName ) { quickPickItems . push ( { label : file , description : txn . transactionLabel || '' , data : txn } ) ; } } ) ; } catch ( error ) { errorMessage = `Error with transaction file ${ file }: ${ error . message }` ; outputAdapter . log ( LogType . ERROR , errorMessage ) ; } } if ( quickPickItems . length > 0 ) { quickPickItems . push ( { label : 'No (manual entry)' , description : '' , data : undefined } ) ; const chosenTransaction = await UserInputUtil . showQuickPickItem ( 'Do you want to provide a file of transaction data for this transaction?' , quickPickItems , false ) as IBlockchainQuickPickItem ; if ( ! chosenTransaction ) { return ; } else if ( chosenTransaction . label !== 'No (manual entry)' ) { const txnDataObject = validateTxnDataObject ( chosenTransaction . data ) ; args = txnDataObject . arguments ; transientData = txnDataObject . transientData ; } } } } } if ( args === undefined ) { try { args = await createArgs ( transactionObject ) ; } catch ( error ) { errorMessage = `Error with transaction arguments: ${ error . message }` ; outputAdapter . log ( LogType . ERROR , errorMessage ) ; return transactionObject ? errorMessage : undefined ; } } if ( ! transientData ) { try { let transientDataString ; if ( transactionObject ) { transientDataString = transactionObject . transientData ; } else { transientDataString = await UserInputUtil . showInputBox ( 'optional: What is the transient data for the transaction, e.g. {"key": "value"}' , '{}' ) ; } if ( transientDataString === undefined ) { return ; } else if ( transientDataString !== '' && transientDataString !== '{}' ) { transientDataString = transientDataString . trim ( ) ; if ( ! transientDataString . startsWith ( '{' ) || ! transientDataString . endsWith ( '}' ) ) { throw new Error ( 'transient data should be in the format {"key": "value"}' ) ; } transientData = JSON . parse ( transientDataString ) ; const keys = Array . from ( Object . keys ( transientData ) ) ; keys . forEach ( ( key ) => { transientData [ key ] = Buffer . from ( transientData [ key ] ) ; } ) ; } } catch ( error ) { errorMessage = `Error with transaction transient data: ${ error . message }` ; outputAdapter . log ( LogType . ERROR , errorMessage ) ; return transactionObject ? errorMessage : undefined ; } } connection = FabricGatewayConnectionManager . instance ( ) . getConnection ( ) ; const channelPeerInfo = await connection . getChannelPeersInfo ( channelName ) ; let selectPeers ; if ( transactionObject ) { peerTargetNames = transactionObject . peerTargetNames ; peerTargetMessage = peerTargetNames . length ? ` to peers ${ peerTargetNames }` : '' ; } else { if ( channelPeerInfo . length === 0 ) { outputAdapter . log ( LogType . ERROR , `No channel peers available to target` ) ; return ; } else if ( channelPeerInfo . length === 1 ) { selectPeers = UserInputUtil . DEFAULT ; peerTargetNames . push ( channelPeerInfo [ 0 ] . name ) ; } else { selectPeers = await UserInputUtil . showQuickPick ( 'Select a peer-targeting policy for this transaction' , [ UserInputUtil . DEFAULT , UserInputUtil . CUSTOM ] ) as string ; } if ( ! selectPeers ) { return ; } else if ( selectPeers === UserInputUtil . CUSTOM ) { const peerTargets = await UserInputUtil . showChannelPeersQuickPick ( channelPeerInfo ) ; if ( ! peerTargets || peerTargets . length === 0 ) { return ; } else { peerTargetNames = peerTargets . map ( ( peer ) => { return peer . data ; } ) ; peerTargetMessage = ` to peers ${ peerTargetNames }` ; } } } return await vscode . window . withProgress ( { location : vscode . ProgressLocation . Notification , title : 'IBM Blockchain Platform Extension' , cancellable : false } , async ( progress ) => { try { progress . report ( { message : `${ actioning } transaction ${ transactionName }` } ) ; if ( args . length === 0 ) { outputAdapter . log ( LogType . INFO , undefined , `${ actioning } transaction ${ transactionName } with no args on channel ${ channelName }${ peerTargetMessage }` ) ; } else { outputAdapter . log ( LogType . INFO , undefined , `${ actioning } transaction ${ transactionName } with args ${ args } on channel ${ channelName }${ peerTargetMessage }` ) ; } if ( gatewayRegistryEntry . fromEnvironment ) { const environmentEntry = await FabricEnvironmentRegistry . instance ( ) . get ( gatewayRegistryEntry . fromEnvironment ) ; if ( environmentEntry . environmentType === EnvironmentType . LOCAL_MICROFAB_ENVIRONMENT ) { VSCodeBlockchainDockerOutputAdapter . instance ( environmentEntry . name ) . show ( ) ; } } let result ; if ( evaluate ) { result = await connection . submitTransaction ( smartContract , transactionName , channelName , args , namespace , transientData , true , peerTargetNames ) ; } else { result = await connection . submitTransaction ( smartContract , transactionName , channelName , args , namespace , transientData , false , peerTargetNames ) ; } Reporter . instance ( ) . sendTelemetryEvent ( `${ action } transaction` ) ; let message ; if ( result === undefined ) { message = `No value returned from ${ transactionName }` ; } else { message = `Returned value from ${ transactionName }: ${ result }` ; } outputAdapter . log ( LogType . SUCCESS , `Successfully ${ actioned } transaction` , message ) ; outputAdapter . show ( ) ; const extensionData = GlobalState . get ( ) ; if ( ! evaluate && ! extensionData . shownFirstSubmissionSurveyURL ) { const surveyMessage = 'Congratulations on submitting your first transaction! We appreciate your feedback to help improve the extension' ; const surveyCallToAction = 'Open survey' ; const surveyURL = 'https://www.surveygizmo.com/s3/5561397/VSCodeExt-Popup-Link' ; await GlobalState . update ( { ... extensionData , shownFirstSubmissionSurveyURL : true } ) ; UserInputUtil . showExternalLinkButton ( surveyMessage , surveyCallToAction , surveyURL ) ; } if ( transactionObject ) { return message ; } } catch ( error ) { errorMessage = `Error ${ actioning } transaction: ${ error . message }` ; outputAdapter . log ( LogType . ERROR , errorMessage ) ; if ( error . endorsements ) { for ( const endorsement of error . endorsements ) { const endorsementError = `Endorsement failed with: ${ endorsement . message }` ; outputAdapter . log ( LogType . ERROR , endorsementError ) ; errorMessage += `\n${ endorsementError }` ; } } outputAdapter . show ( ) ; if ( transactionObject ) { return errorMessage ; } } } ) ; }