From 9d2d70a067892f1a03f8fb3dc736af692eca79cf Mon Sep 17 00:00:00 2001 From: Chris Bygrave Date: Thu, 1 Jun 2023 15:57:07 +0100 Subject: [PATCH] Add support for optionally publishing token pools, interfaces, and APIs Signed-off-by: Chris Bygrave --- server/package-lock.json | 14 ++--- server/package.json | 2 +- server/src/controllers/contracts.ts | 52 +++++++++++-------- server/src/controllers/tokens.ts | 22 ++++---- server/src/interfaces.ts | 12 +++++ server/src/utils.ts | 29 +++++++++-- server/test/contracts.template.test.ts | 42 +++++++++++++++ server/test/contracts.test.ts | 30 ++++++----- server/test/tokens.template.test.ts | 24 +++++++++ server/test/tokens.test.ts | 15 +++--- .../Forms/Contracts/DefineInterfaceForm.tsx | 38 ++++++++++++-- .../Contracts/RegisterContractApiForm.tsx | 38 +++++++++++++- ui/src/components/Forms/Tokens/PoolForm.tsx | 32 +++++++++++- ui/src/translations/en.json | 4 ++ 14 files changed, 284 insertions(+), 70 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index cd2e22a..1f16f93 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.3", "license": "ISC", "dependencies": { - "@hyperledger/firefly-sdk": "^1.2.3", + "@hyperledger/firefly-sdk": "^1.2.10", "body-parser": "^1.20.0", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", @@ -855,9 +855,9 @@ "dev": true }, "node_modules/@hyperledger/firefly-sdk": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@hyperledger/firefly-sdk/-/firefly-sdk-1.2.3.tgz", - "integrity": "sha512-icJYoztHF33EuPHbniypLyteJj6kfa/rei52YPvMbM6TIPNNPx3BA/w1jCRr7/uGVLsJoL/9uu2cBA718POuIw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@hyperledger/firefly-sdk/-/firefly-sdk-1.2.10.tgz", + "integrity": "sha512-YKA4AcIo/yRghQMVVnImDGarrKofnYFfXHPOW0dLnAbsQUM18L7tux4/rjNrNd0lEpxxJCy1MwajF7+mzjuyiw==", "dependencies": { "axios": "^0.26.1", "form-data": "^4.0.0", @@ -11355,9 +11355,9 @@ "dev": true }, "@hyperledger/firefly-sdk": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@hyperledger/firefly-sdk/-/firefly-sdk-1.2.3.tgz", - "integrity": "sha512-icJYoztHF33EuPHbniypLyteJj6kfa/rei52YPvMbM6TIPNNPx3BA/w1jCRr7/uGVLsJoL/9uu2cBA718POuIw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@hyperledger/firefly-sdk/-/firefly-sdk-1.2.10.tgz", + "integrity": "sha512-YKA4AcIo/yRghQMVVnImDGarrKofnYFfXHPOW0dLnAbsQUM18L7tux4/rjNrNd0lEpxxJCy1MwajF7+mzjuyiw==", "requires": { "axios": "^0.26.1", "form-data": "^4.0.0", diff --git a/server/package.json b/server/package.json index 8d3a77c..5195428 100644 --- a/server/package.json +++ b/server/package.json @@ -26,7 +26,7 @@ }, "homepage": "https://github.com/hyperledger/firefly-sandbox#readme", "dependencies": { - "@hyperledger/firefly-sdk": "^1.2.3", + "@hyperledger/firefly-sdk": "^1.2.10", "body-parser": "^1.20.0", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", diff --git a/server/src/controllers/contracts.ts b/server/src/controllers/contracts.ts index cadf5b8..8a71ca7 100644 --- a/server/src/controllers/contracts.ts +++ b/server/src/controllers/contracts.ts @@ -10,7 +10,7 @@ import { } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { getFireflyClient } from '../clients/fireflySDKWrapper'; -import { formatTemplate, quoteAndEscape as q } from '../utils'; +import { formatTemplate, getFireflyOptions, quoteAndEscape as q } from '../utils'; import { AsyncResponse, ContractAPI, @@ -51,7 +51,7 @@ export class ContractsController { input: { abi: body.schema }, }) : body.schema; - const result = await firefly.createContractInterface(ffi); + const result = await firefly.createContractInterface(ffi, getFireflyOptions(body.publish)); return { type: 'message', id: result.message }; } @@ -65,16 +65,19 @@ export class ContractsController { ): Promise { const firefly = getFireflyClient(namespace); // See ContractsTemplateController and keep template code up to date. - const api = await firefly.createContractAPI({ - name: body.name, - interface: { - name: body.interfaceName, - version: body.interfaceVersion, - }, - location: { - address: body.address, + const api = await firefly.createContractAPI( + { + name: body.name, + interface: { + name: body.interfaceName, + version: body.interfaceVersion, + }, + location: { + address: body.address, + }, }, - }); + getFireflyOptions(body.publish), + ); return { type: 'message', id: api.message }; } @@ -88,17 +91,20 @@ export class ContractsController { ): Promise { const firefly = getFireflyClient(namespace); // See ContractsTemplateController and keep template code up to date. - const api = await firefly.createContractAPI({ - name: body.name, - interface: { - name: body.interfaceName, - version: body.interfaceVersion, - }, - location: { - chaincode: body.chaincode, - channel: body.channel, + const api = await firefly.createContractAPI( + { + name: body.name, + interface: { + name: body.interfaceName, + version: body.interfaceVersion, + }, + location: { + chaincode: body.chaincode, + channel: body.channel, + }, }, - }); + getFireflyOptions(body.publish), + ); return { type: 'message', id: api.message }; } @@ -213,7 +219,7 @@ export class ContractsTemplateController { abi: <%= ${q('schema', { isObject: true, truncate: true })} %>, }, })<% } else { %><%= ${q('schema', { isObject: true, truncate: true })} %><% } %>; - const result = await firefly.createContractInterface(ffi); + const result = await firefly.createContractInterface(ffi<% if (publish !== undefined) { %>, { publish: <%= publish %> }<% }%>); return { type: 'message', id: result.message }; `); } @@ -232,7 +238,7 @@ export class ContractsTemplateController { chaincode: <%= ${q('chaincode')} %>, channel: <%= ${q('channel')} %>,<% } %> }, - }); + }<% if (publish !== undefined) { %>, { publish: <%= publish %> }<% }%>); return { type: 'message', id: api.message }; `); } diff --git a/server/src/controllers/tokens.ts b/server/src/controllers/tokens.ts index 3f2e9e2..b208ab9 100644 --- a/server/src/controllers/tokens.ts +++ b/server/src/controllers/tokens.ts @@ -18,6 +18,7 @@ import { formatTemplate, FormDataSchema, getBroadcastMessageBody, + getFireflyOptions, getPrivateMessageBody, mapPool, quoteAndEscape as q, @@ -70,15 +71,18 @@ export class TokensController { ): Promise { const firefly = getFireflyClient(namespace); // See TokensTemplateController and keep template code up to date. - const pool = await firefly.createTokenPool({ - name: body.name, - symbol: body.symbol, - type: body.type, - config: { - address: body.address, - blockNumber: body.blockNumber, + const pool = await firefly.createTokenPool( + { + name: body.name, + symbol: body.symbol, + type: body.type, + config: { + address: body.address, + blockNumber: body.blockNumber, + }, }, - }); + getFireflyOptions(body.publish), + ); return { type: 'token_pool', id: pool.id }; } @@ -291,7 +295,7 @@ export class TokensTemplateController { <% print('address: ' + ${q('address')} + ',') } %> blockNumber: <%= ${q('blockNumber')} %>, } - }); + }<% if (publish !== undefined) { %>, { publish: <%= publish %> }<% }%>); return { type: 'token_pool', id: pool.id }; `); } diff --git a/server/src/interfaces.ts b/server/src/interfaces.ts index b9fd3ad..5222da2 100644 --- a/server/src/interfaces.ts +++ b/server/src/interfaces.ts @@ -153,6 +153,10 @@ export class TokenPoolInput { @IsString() @IsOptional() blockNumber?: string; + + @IsBoolean() + @IsOptional() + publish?: boolean; } export class TokenPool extends TokenPoolInput { @@ -241,6 +245,10 @@ export class ContractInterface { @IsDefined() @JSONSchema({ type: 'object' }) schema: any; + + @IsBoolean() + @IsOptional() + publish?: boolean; } export class ContractInterfaceEvent { @@ -269,6 +277,10 @@ export class ContractAPI { @IsString() @IsOptional() chaincode?: string; + + @IsBoolean() + @IsOptional() + publish?: boolean; } export class ContractAPIURLs { diff --git a/server/src/utils.ts b/server/src/utils.ts index 603a31b..3889729 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -4,7 +4,11 @@ import { getMetadataArgsStorage, RoutingControllersOptions } from 'routing-contr import { OpenAPI, routingControllersToSpec } from 'routing-controllers-openapi'; import { WebSocketServer } from 'ws'; import { validationMetadatasToSchemas } from 'class-validator-jsonschema'; -import { FireFlyDataRequest, FireFlyTokenPoolResponse } from '@hyperledger/firefly-sdk'; +import { + FireFlyCreateOptions, + FireFlyDataRequest, + FireFlyTokenPoolResponse, +} from '@hyperledger/firefly-sdk'; import stripIndent = require('strip-indent'); import { BroadcastValue, PrivateValue } from './interfaces'; @@ -20,6 +24,13 @@ export enum FF_MESSAGES { GROUP_INIT = 'groupinit', } +export function getFireflyOptions(publish?: boolean): FireFlyCreateOptions { + if (publish === undefined) { + return {}; + } + return { publish: publish }; +} + export function genOpenAPI(options: RoutingControllersOptions) { return routingControllersToSpec(getMetadataArgsStorage(), options, { info: { @@ -111,25 +122,33 @@ export function quoteAndEscape(varName: string, options?: QuoteOptions) { return result; } -export function getBroadcastMessageBody(body: BroadcastValue, blobId?: string, messageType?: FF_MESSAGES) { +export function getBroadcastMessageBody( + body: BroadcastValue, + blobId?: string, + messageType?: FF_MESSAGES, +) { const dataBody = blobId ? { id: blobId } : getMessageBody(body); return { header: { tag: body.tag || undefined, topics: body.topic ? [body.topic] : undefined, - type: messageType || undefined + type: messageType || undefined, }, data: [dataBody], }; } -export function getPrivateMessageBody(body: PrivateValue, blobId?: string, messageType?: FF_MESSAGES) { +export function getPrivateMessageBody( + body: PrivateValue, + blobId?: string, + messageType?: FF_MESSAGES, +) { const dataBody = blobId ? { id: blobId } : getMessageBody(body); return { header: { tag: body.tag || undefined, topics: body.topic ? [body.topic] : undefined, - type: messageType || undefined + type: messageType || undefined, }, group: { members: body.recipients.map((r) => ({ identity: r })), diff --git a/server/test/contracts.template.test.ts b/server/test/contracts.template.test.ts index 98c4ddd..e1452d1 100644 --- a/server/test/contracts.template.test.ts +++ b/server/test/contracts.template.test.ts @@ -17,6 +17,7 @@ describe('Templates: Smart Contracts', () => { name: 'simple-storage', version: '1.0', schema: [{ name: 'method1' }, { name: 'event1' }], + publish: undefined, }), ).toBe( formatTemplate(` @@ -36,6 +37,7 @@ describe('Templates: Smart Contracts', () => { compiled({ format: 'ffi', schema: { methods: [{ name: 'method1' }] }, + publish: undefined, }), ).toBe( formatTemplate(` @@ -44,6 +46,20 @@ describe('Templates: Smart Contracts', () => { return { type: 'message', id: result.message }; `), ); + + expect( + compiled({ + format: 'ffi', + schema: { methods: [{ name: 'method1' }] }, + publish: true, + }), + ).toBe( + formatTemplate(` + const ffi = {"methods" ... ethod1"}]}; + const result = await firefly.createContractInterface(ffi, { publish: true }); + return { type: 'message', id: result.message }; + `), + ); }); }); @@ -60,6 +76,7 @@ describe('Templates: Smart Contracts', () => { interfaceName: 'simple-storage', interfaceVersion: '1.0', address: '0x123', + publish: undefined, }), ).toBe( formatTemplate(` @@ -76,6 +93,30 @@ describe('Templates: Smart Contracts', () => { return { type: 'message', id: api.message }; `), ); + + expect( + compiled({ + name: 'api1', + interfaceName: 'simple-storage', + interfaceVersion: '1.0', + address: '0x123', + publish: false, + }), + ).toBe( + formatTemplate(` + const api = await firefly.createContractAPI({ + name: 'api1', + interface: { + name: 'simple-storage', + version: '1.0', + }, + location: { + address: '0x123', + }, + }, { publish: false }); + return { type: 'message', id: api.message }; + `), + ); }); }); @@ -94,6 +135,7 @@ describe('Templates: Smart Contracts', () => { chaincode: 'chaincode123', channel: 'channel123', address: undefined, + publish: undefined, }), ).toBe( formatTemplate(` diff --git a/server/test/contracts.test.ts b/server/test/contracts.test.ts index 20cbe6b..999f9f5 100644 --- a/server/test/contracts.test.ts +++ b/server/test/contracts.test.ts @@ -41,7 +41,7 @@ describe('Smart Contracts', () => { .expect(202) .expect({ type: 'message', id: 'msg1' }); - expect(mockFireFly.createContractInterface).toHaveBeenCalledWith(req.schema); + expect(mockFireFly.createContractInterface).toHaveBeenCalledWith(req.schema, {}); }); test('Create contract interface from ABI', async () => { @@ -71,7 +71,7 @@ describe('Smart Contracts', () => { version: '1.0', input: { abi: req.schema }, }); - expect(mockFireFly.createContractInterface).toHaveBeenCalledWith(int); + expect(mockFireFly.createContractInterface).toHaveBeenCalledWith(int, {}); }); test('Create contract API', async () => { @@ -94,11 +94,14 @@ describe('Smart Contracts', () => { .expect(202) .expect({ type: 'message', id: 'msg1' }); - expect(mockFireFly.createContractAPI).toHaveBeenCalledWith({ - interface: { name: 'my-contract', version: '1.0' }, - location: { address: '0x123' }, - name: 'my-api', - }); + expect(mockFireFly.createContractAPI).toHaveBeenCalledWith( + { + interface: { name: 'my-contract', version: '1.0' }, + location: { address: '0x123' }, + name: 'my-api', + }, + {}, + ); }); test('Create contract API with Fabric', async () => { @@ -122,11 +125,14 @@ describe('Smart Contracts', () => { .expect(202) .expect({ type: 'message', id: 'msg1' }); - expect(mockFireFly.createContractAPI).toHaveBeenCalledWith({ - interface: { name: 'my-contract', version: '1.0' }, - location: { chaincode: 'chaincode', channel: '0x123' }, - name: 'my-api-fabric', - }); + expect(mockFireFly.createContractAPI).toHaveBeenCalledWith( + { + interface: { name: 'my-contract', version: '1.0' }, + location: { chaincode: 'chaincode', channel: '0x123' }, + name: 'my-api-fabric', + }, + {}, + ); }); test('Get contract Interfaces', async () => { diff --git a/server/test/tokens.template.test.ts b/server/test/tokens.template.test.ts index 45775ce..af7dde7 100644 --- a/server/test/tokens.template.test.ts +++ b/server/test/tokens.template.test.ts @@ -17,6 +17,7 @@ describe('Templates: Tokens', () => { type: 'fungible', address: undefined, blockNumber: '0', + publish: undefined, }), ).toBe( formatTemplate(` @@ -31,6 +32,29 @@ describe('Templates: Tokens', () => { return { type: 'token_pool', id: pool.id }; `), ); + + expect( + compiled({ + name: 'pool1', + symbol: 'P1', + type: 'fungible', + address: undefined, + blockNumber: '0', + publish: true, + }), + ).toBe( + formatTemplate(` + const pool = await firefly.createTokenPool({ + name: 'pool1', + symbol: 'P1', + type: 'fungible', + config: { + blockNumber: '0', + } + }, { publish: true }); + return { type: 'token_pool', id: pool.id }; + `), + ); }); }); diff --git a/server/test/tokens.test.ts b/server/test/tokens.test.ts index 09f86c1..805de3b 100644 --- a/server/test/tokens.test.ts +++ b/server/test/tokens.test.ts @@ -53,12 +53,15 @@ describe('Tokens', () => { .expect(202) .expect({ type: 'token_pool', id: 'pool1' }); - expect(mockFireFly.createTokenPool).toHaveBeenCalledWith({ - name: 'my-pool', - symbol: 'P1', - type: 'fungible', - config: {}, - }); + expect(mockFireFly.createTokenPool).toHaveBeenCalledWith( + { + name: 'my-pool', + symbol: 'P1', + type: 'fungible', + config: {}, + }, + {}, + ); }); test('Mint tokens', async () => { diff --git a/ui/src/components/Forms/Contracts/DefineInterfaceForm.tsx b/ui/src/components/Forms/Contracts/DefineInterfaceForm.tsx index 864276a..3ed857f 100644 --- a/ui/src/components/Forms/Contracts/DefineInterfaceForm.tsx +++ b/ui/src/components/Forms/Contracts/DefineInterfaceForm.tsx @@ -1,5 +1,8 @@ import { + Checkbox, FormControl, + FormControlLabel, + FormHelperText, Grid, InputLabel, MenuItem, @@ -63,13 +66,18 @@ const DEFAULT_ABI_SCHEMA = [ ]; export const DefineInterfaceForm: React.FC = () => { - const { blockchainPlugin, setJsonPayload, setPayloadMissingFields } = - useContext(ApplicationContext); + const { + blockchainPlugin, + multiparty, + setJsonPayload, + setPayloadMissingFields, + } = useContext(ApplicationContext); const { t } = useTranslation(); const theme = useTheme(); const [interfaceFormat, setInterfaceFormat] = useState('ffi'); const [name, setName] = useState(''); + const [publish, setPublish] = useState(true); const [schema, setSchema] = useState(DEFAULT_FFI_SCHEMA); const { formID } = useContext(FormContext); const [schemaString, setSchemaString] = useState( @@ -90,8 +98,9 @@ export const DefineInterfaceForm: React.FC = () => { name, version, schema, + publish: multiparty ? publish : undefined, }); - }, [interfaceFormat, schema, name, version, formID]); + }, [interfaceFormat, schema, name, version, formID, multiparty, publish]); useEffect(() => { blockchainPlugin === BLOCKCHAIN_TYPE.FABRIC && setInterfaceFormats(['ffi']); @@ -181,6 +190,29 @@ export const DefineInterfaceForm: React.FC = () => { }} /> + {/* Publish */} + {multiparty && ( + + + + { + setPublish(!publish); + }} + /> + } + label={t('publishToNetwork')} + /> + + {t('publishContractInterfaceHelperText')} + + + + + )} ); diff --git a/ui/src/components/Forms/Contracts/RegisterContractApiForm.tsx b/ui/src/components/Forms/Contracts/RegisterContractApiForm.tsx index 6670030..e8765c2 100644 --- a/ui/src/components/Forms/Contracts/RegisterContractApiForm.tsx +++ b/ui/src/components/Forms/Contracts/RegisterContractApiForm.tsx @@ -1,5 +1,7 @@ import { + Checkbox, FormControl, + FormControlLabel, FormHelperText, Grid, InputLabel, @@ -22,8 +24,12 @@ import { fetchCatcher } from '../../../utils/fetches'; import { isValidFFName, isValidAddress } from '../../../utils/regex'; export const RegisterContractApiForm: React.FC = () => { - const { blockchainPlugin, setJsonPayload, setPayloadMissingFields } = - useContext(ApplicationContext); + const { + blockchainPlugin, + multiparty, + setJsonPayload, + setPayloadMissingFields, + } = useContext(ApplicationContext); const { formID } = useContext(FormContext); const { reportFetchError } = useContext(SnackbarContext); const { t } = useTranslation(); @@ -33,6 +39,7 @@ export const RegisterContractApiForm: React.FC = () => { >([]); const [contractInterfaceIdx, setContractInterfaceIdx] = useState(0); const [name, setName] = useState(''); + const [publish, setPublish] = useState(true); const [nameError, setNameError] = useState(''); const [chaincode, setChaincode] = useState(''); const [channel, setChannel] = useState(''); @@ -64,6 +71,7 @@ export const RegisterContractApiForm: React.FC = () => { interfaceVersion: contractInterfaces[contractInterfaceIdx]?.version || '', address: contractAddress, + publish: multiparty ? publish : undefined, }); } else { setJsonPayload({ @@ -74,6 +82,7 @@ export const RegisterContractApiForm: React.FC = () => { chaincode: chaincode, channel: channel, address: '', + publish: multiparty ? publish : undefined, }); } }, [ @@ -84,6 +93,8 @@ export const RegisterContractApiForm: React.FC = () => { contractAddress, formID, blockchainPlugin, + multiparty, + publish, ]); useEffect(() => { @@ -210,6 +221,29 @@ export const RegisterContractApiForm: React.FC = () => { )} + {/* Publish */} + {multiparty && ( + + + + { + setPublish(!publish); + }} + /> + } + label={t('publishToNetwork')} + /> + + {t('publishContractAPIHelperText')} + + + + + )} ); diff --git a/ui/src/components/Forms/Tokens/PoolForm.tsx b/ui/src/components/Forms/Tokens/PoolForm.tsx index f052239..cd49674 100644 --- a/ui/src/components/Forms/Tokens/PoolForm.tsx +++ b/ui/src/components/Forms/Tokens/PoolForm.tsx @@ -1,5 +1,8 @@ import { + Checkbox, FormControl, + FormControlLabel, + FormHelperText, Grid, InputLabel, MenuItem, @@ -15,12 +18,13 @@ import { DEFAULT_SPACING } from '../../../theme'; export const PoolForm: React.FC = () => { const { t } = useTranslation(); - const { setJsonPayload, setPayloadMissingFields } = + const { multiparty, setJsonPayload, setPayloadMissingFields } = useContext(ApplicationContext); const { formID } = useContext(FormContext); const [name, setName] = useState(''); const [symbol, setSymbol] = useState(''); const [address, setAddress] = useState(); + const [publish, setPublish] = useState(true); const [blockNumber, setBlockNumber] = useState('0'); const [type, setType] = useState<'fungible' | 'nonfungible'>('fungible'); @@ -38,8 +42,9 @@ export const PoolForm: React.FC = () => { type, address, blockNumber, + publish: multiparty ? publish : undefined, }); - }, [name, symbol, type, address, formID, blockNumber]); + }, [name, symbol, type, address, formID, blockNumber, multiparty, publish]); const handleNameChange = (event: React.ChangeEvent) => { if (event.target.value.indexOf(' ') < 0) { @@ -128,6 +133,29 @@ export const PoolForm: React.FC = () => { /> + {/* Publish */} + {multiparty && ( + + + + { + setPublish(!publish); + }} + /> + } + label={t('publishToNetwork')} + /> + + {t('publishTokenPoolHelperText')} + + + + + )} ); diff --git a/ui/src/translations/en.json b/ui/src/translations/en.json index 8726050..3ae128d 100644 --- a/ui/src/translations/en.json +++ b/ui/src/translations/en.json @@ -154,6 +154,10 @@ "privateShortInfo": "Sends a message to a restricted set of parties", "privateTitle": "Send a Private Message", "protocolID": "Protocol ID", + "publishToNetwork": "Publish", + "publishContractAPIHelperText": "Controls whether to automatically publish the contract API to the network.", + "publishContractInterfaceHelperText": "Controls whether to automatically publish the contract interface to the network.", + "publishTokenPoolHelperText": "Controls whether to automatically publish the token pool to the network.", "recipients": "Recipients", "referenceID": "Reference ID", "refreshConnection": "Refresh Connection",