Skip to content

Commit

Permalink
Transaction peer targeting (close IBM-Blockchain#1514) (IBM-Blockchai…
Browse files Browse the repository at this point in the history
…n#1530)

Signed-off-by: Jake Turner <jaketurner25@live.com>
  • Loading branch information
Jakeeyturner authored and cazfletch committed Oct 15, 2019
1 parent 650498b commit efb2c5c
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 55 deletions.
2 changes: 2 additions & 0 deletions cucumber/helpers/gatewayHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ export class GatewayHelper {
}
this.userInputUtilHelper.inputBoxStub.withArgs('optional: What is the transient data for the transaction, e.g. {"key": "value"}', '{}').resolves(transientData);

this.userInputUtilHelper.showQuickPickStub.withArgs('Select a peer-targeting policy for this transaction', [UserInputUtil.DEFAULT, UserInputUtil.CUSTOM]).resolves(UserInputUtil.DEFAULT);

if (evaluate) {
await vscode.commands.executeCommand(ExtensionCommands.EVALUATE_TRANSACTION);
} else {
Expand Down
7 changes: 5 additions & 2 deletions extension/commands/UserInputUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export class UserInputUtil {
static readonly OPEN_IN_NEW_WINDOW: string = 'Open in new window';
static readonly YES: string = 'Yes';
static readonly NO: string = 'No';
static readonly DEFAULT: string = 'Default';
static readonly CUSTOM: string = 'Custom';

static readonly OVERWRITE_FILE: string = 'Overwrite file';
static readonly SKIP_FILE: string = 'Skip file';
static readonly FORCE_FILES: string = 'Force all files to overwrite';
Expand All @@ -82,10 +85,10 @@ export class UserInputUtil {
static readonly ADD_GATEWAY_FROM_ENVIRONMENT: string = 'Create a gateway from a Fabric environment';
static readonly ADD_GATEWAY_FROM_CCP: string = 'Create a gateway from a connection profile';

public static async showQuickPick(prompt: string, items: string[]): Promise<string> {
public static async showQuickPick(prompt: string, items: string[], canPickMany: boolean = false): Promise<string | string[]> {
const quickPickOptions: vscode.QuickPickOptions = {
ignoreFocusOut: true,
canPickMany: false,
canPickMany: canPickMany,
placeHolder: prompt
};

Expand Down
2 changes: 1 addition & 1 deletion extension/commands/addGatewayCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function addGateway(): Promise<{} | void> {

const items: string[] = [UserInputUtil.ADD_GATEWAY_FROM_CCP, UserInputUtil.ADD_GATEWAY_FROM_ENVIRONMENT];

const gatewayMethod: string = await UserInputUtil.showQuickPick('Choose a method to add a gateway', items);
const gatewayMethod: string = await UserInputUtil.showQuickPick('Choose a method to add a gateway', items) as string;

if (!gatewayMethod) {
return;
Expand Down
4 changes: 2 additions & 2 deletions extension/commands/associateIdentityWithNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export async function associateIdentityWithNode(replace: boolean = false, enviro
if (node.type === FabricNodeType.CERTIFICATE_AUTHORITY && node.enroll_id && node.enroll_secret) {
const enrollNew: string = 'Use ID and secret to enroll a new identity';
const chooseExisting: string = 'Choose an existing identity';
const chosenMethod: string = await UserInputUtil.showQuickPick('The JSON for this certificate authority includes an enrollment ID and secret...', [enrollNew, chooseExisting]);
const chosenMethod: string = await UserInputUtil.showQuickPick('The JSON for this certificate authority includes an enrollment ID and secret...', [enrollNew, chooseExisting]) as string;
if (!chosenMethod) {
return;
}
Expand Down Expand Up @@ -173,7 +173,7 @@ export async function associateIdentityWithNode(replace: boolean = false, enviro
});

items.push('No');
const nodeName: string = await UserInputUtil.showQuickPick('Do you want to associate the same identity with another node?', items);
const nodeName: string = await UserInputUtil.showQuickPick('Do you want to associate the same identity with another node?', items) as string;

if (!nodeName || nodeName === 'No') {
askAgain = false;
Expand Down
30 changes: 26 additions & 4 deletions extension/commands/submitTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { VSCodeBlockchainDockerOutputAdapter } from '../logging/VSCodeBlockchain
import { InstantiatedTreeItem } from '../explorer/model/InstantiatedTreeItem';
import { FabricGatewayRegistryEntry } from '../registries/FabricGatewayRegistryEntry';
import { FabricRuntimeUtil } from '../fabric/FabricRuntimeUtil';
import { IFabricClientConnection } from '../fabric/IFabricClientConnection';

export async function submitTransaction(evaluate: boolean, treeItem?: InstantiatedTreeItem | TransactionTreeItem, channelName?: string, smartContract?: string): Promise<void> {
const outputAdapter: VSCodeBlockchainOutputAdapter = VSCodeBlockchainOutputAdapter.instance();
Expand Down Expand Up @@ -120,6 +121,27 @@ export async function submitTransaction(evaluate: boolean, treeItem?: Instantiat
return;
}

const selectPeers: string = await UserInputUtil.showQuickPick('Select a peer-targeting policy for this transaction', [UserInputUtil.DEFAULT, UserInputUtil.CUSTOM]) as string;

let peerTargetNames: string[] = [];
let peerTargetMessage: string = '';

const connection: IFabricClientConnection = FabricConnectionManager.instance().getConnection();

if (!selectPeers) {
return;
} else if (selectPeers === UserInputUtil.CUSTOM) {
const channelPeerNames: string[] = await connection.getChannelPeerNames(channelName);

peerTargetNames = await UserInputUtil.showQuickPick('Select the peers to send the transaction to', channelPeerNames, true) as string[];

if (!peerTargetNames || peerTargetNames.length === 0) {
return;
} else {
peerTargetMessage = ` to peers ${peerTargetNames}`;
}
}

await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'IBM Blockchain Platform Extension',
Expand All @@ -129,9 +151,9 @@ export async function submitTransaction(evaluate: boolean, treeItem?: Instantiat
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}`);
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}`);
outputAdapter.log(LogType.INFO, undefined, `${actioning} transaction ${transactionName} with args ${args} on channel ${channelName}${peerTargetMessage}`);
}

const gatewayRegistyrEntry: FabricGatewayRegistryEntry = FabricConnectionManager.instance().getGatewayRegistryEntry();
Expand All @@ -141,10 +163,10 @@ export async function submitTransaction(evaluate: boolean, treeItem?: Instantiat

let result: string | undefined;
if (evaluate) {
result = await FabricConnectionManager.instance().getConnection().submitTransaction(smartContract, transactionName, channelName, args, namespace, transientData, true);
result = await connection.submitTransaction(smartContract, transactionName, channelName, args, namespace, transientData, true, peerTargetNames);

} else {
result = await FabricConnectionManager.instance().getConnection().submitTransaction(smartContract, transactionName, channelName, args, namespace, transientData);
result = await connection.submitTransaction(smartContract, transactionName, channelName, args, namespace, transientData, false, peerTargetNames);
}

Reporter.instance().sendTelemetryEvent(`${action} transaction`);
Expand Down
10 changes: 8 additions & 2 deletions extension/fabric/FabricClientConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { FabricWallet } from './FabricWallet';
import { ExtensionUtil } from '../util/ExtensionUtil';
import { IFabricClientConnection } from './IFabricClientConnection';
import { Network, Contract, Transaction } from 'fabric-network';
import * as Client from 'fabric-client';

export class FabricClientConnection extends FabricConnection implements IFabricClientConnection {

Expand Down Expand Up @@ -66,16 +67,21 @@ export class FabricClientConnection extends FabricConnection implements IFabricC
}
}

public async submitTransaction(chaincodeName: string, transactionName: string, channel: string, args: Array<string>, namespace: string, transientData: { [key: string]: Buffer }, evaluate?: boolean): Promise<string | undefined> {
public async submitTransaction(chaincodeName: string, transactionName: string, channelName: string, args: Array<string>, namespace: string, transientData: { [key: string]: Buffer }, evaluate?: boolean, peerTargetNames: string[] = []): Promise<string | undefined> {

const network: Network = await this.gateway.getNetwork(channel);
const network: Network = await this.gateway.getNetwork(channelName);
const smartContract: Contract = network.getContract(chaincodeName, namespace);

const transaction: Transaction = smartContract.createTransaction(transactionName);
if (transientData) {
transaction.setTransient(transientData);
}

if (peerTargetNames && peerTargetNames.length > 0) {
const peerTargets: Client.ChannelPeer[] = await this.getChannelPeers(channelName, peerTargetNames);
transaction.setEndorsingPeers(peerTargets);
}

let response: Buffer;
if (evaluate) {
response = await transaction.evaluate(...args);
Expand Down
40 changes: 39 additions & 1 deletion extension/fabric/FabricConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
'use strict';

import * as Client from 'fabric-client';
import { Gateway, GatewayOptions, FileSystemWallet } from 'fabric-network';
import { Gateway, GatewayOptions, FileSystemWallet, Network } from 'fabric-network';
import { OutputAdapter } from '../logging/OutputAdapter';
import { ConsoleOutputAdapter } from '../logging/ConsoleOutputAdapter';
import { FabricWallet } from './FabricWallet';
Expand Down Expand Up @@ -136,6 +136,26 @@ export abstract class FabricConnection {
}
}

public async getChannelPeerNames(channelName: string): Promise<string[]> {
try {
const network: Network = await this.gateway.getNetwork(channelName);
const channel: Client.Channel = network.getChannel();
const channelPeers: Client.ChannelPeer[] = channel.getChannelPeers();

const peerNames: string[] = [];

for (const peer of channelPeers) {
const name: string = peer.getName();
peerNames.push(name);
}

return peerNames;

} catch (error) {
throw new Error(`Unable to get channel peer names: ${error.message}`);
}
}

protected async connectInner(connectionProfile: object, wallet: FileSystemWallet, identityName: string, timeout: number): Promise<void> {

this.discoveryAsLocalhost = this.hasLocalhostURLs(connectionProfile);
Expand Down Expand Up @@ -175,6 +195,24 @@ export abstract class FabricConnection {
throw lastError;
}

protected async getChannelPeers(channelName: string, peerNames: string[]): Promise<Client.ChannelPeer[]> {
try {
const network: Network = await this.gateway.getNetwork(channelName);
const channel: Client.Channel = network.getChannel();

const channelPeers: Client.ChannelPeer[] = [];

for (const name of peerNames) {
const peer: Client.ChannelPeer = channel.getChannelPeer(name);
channelPeers.push(peer);
}

return channelPeers;
} catch (error) {
throw new Error(`Unable to get channel peers: ${error.message}`);
}
}

private isLocalhostURL(url: string): boolean {
const parsedURL: URL = new URL(url);
const localhosts: string[] = [
Expand Down
4 changes: 3 additions & 1 deletion extension/fabric/IFabricClientConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ export interface IFabricClientConnection {

getAllChannelsForPeer(peerName: string): Promise<Array<string>>;

getChannelPeerNames(channelName: string): Promise<string[]>;

getInstantiatedChaincode(channelName: string): Promise<Array<{name: string, version: string}>>;

isIBPConnection(): boolean;

getMetadata(instantiatedChaincodeName: string, channel: string): Promise<any>;

submitTransaction(chaincodeName: string, transactionName: string, channel: string, args: Array<string>, namespace: string, transientData: {[key: string]: Buffer}, evaluate?: boolean): Promise<string | undefined>;
submitTransaction(chaincodeName: string, transactionName: string, channel: string, args: Array<string>, namespace: string, transientData: {[key: string]: Buffer}, evaluate?: boolean, peerTargetNames?: string[]): Promise<string | undefined>;

}
50 changes: 19 additions & 31 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1361,8 +1361,8 @@
},
"optionalDependencies": {
"fabric-ca-client": "1.4.5-snapshot.69",
"fabric-client": "1.4.5-snapshot.65",
"fabric-network": "1.4.5-snapshot.64"
"fabric-client": "1.4.5-snapshot.68",
"fabric-network": "1.4.5-snapshot.72"
},
"nativeDependencies": [
"grpc"
Expand Down
20 changes: 17 additions & 3 deletions test/commands/UserInputUtil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2291,14 +2291,28 @@ describe('UserInputUtil', () => {
});

describe('showQuickPick', () => {
it('should show the items passed in', async () => {
it('should be able to pick an item', async () => {
const items: string[] = ['itemOne', 'itemTwo'];
quickPickStub.resolves('itemOne');

const result: string = await UserInputUtil.showQuickPick('choose an item', items);
const prompt: string = 'choose an item';

const result: string = await UserInputUtil.showQuickPick(prompt, items) as string;
result.should.equal('itemOne');

quickPickStub.should.have.been.calledWith(items, sinon.match.any);
quickPickStub.should.have.been.calledWith(items, {ignoreFocusOut: true, canPickMany: false, placeHolder: prompt});
});

it('should be able to pick multiple items', async () => {
const items: string[] = ['itemOne', 'itemTwo'];
quickPickStub.resolves(items);

const prompt: string = 'choose an item';

const result: string[] = await UserInputUtil.showQuickPick(prompt, items, true) as string[];
result.should.deep.equal(items);

quickPickStub.should.have.been.calledWith(items, {ignoreFocusOut: true, canPickMany: true, placeHolder: prompt});
});
});

Expand Down
Loading

0 comments on commit efb2c5c

Please sign in to comment.