diff --git a/docs/reference/types/event.md b/docs/reference/types/event.md
index 2e5c20b2c6..6fbc38dbd4 100644
--- a/docs/reference/types/event.md
+++ b/docs/reference/types/event.md
@@ -41,7 +41,7 @@ nav_order: 2
|------------|-------------|------|
| `id` | The UUID assigned to this event by your local FireFly node | [`UUID`](simpletypes#uuid) |
| `sequence` | A sequence indicating the order in which events are delivered to your application. Assure to be unique per event in your local FireFly database (unlike the created timestamp) | `int64` |
-| `type` | All interesting activity in FireFly is emitted as a FireFly event, of a given type. The 'type' combined with the 'reference' can be used to determine how to process the event within your application | `FFEnum`:
`"transaction_submitted"`
`"message_confirmed"`
`"message_rejected"`
`"datatype_confirmed"`
`"identity_confirmed"`
`"identity_updated"`
`"token_pool_confirmed"`
`"token_pool_op_failed"`
`"token_transfer_confirmed"`
`"token_transfer_op_failed"`
`"token_approval_confirmed"`
`"token_approval_op_failed"`
`"contract_interface_confirmed"`
`"contract_api_confirmed"`
`"blockchain_event_received"`
`"blockchain_invoke_op_succeeded"`
`"blockchain_invoke_op_failed"` |
+| `type` | All interesting activity in FireFly is emitted as a FireFly event, of a given type. The 'type' combined with the 'reference' can be used to determine how to process the event within your application | `FFEnum`:
`"transaction_submitted"`
`"message_confirmed"`
`"message_rejected"`
`"datatype_confirmed"`
`"identity_confirmed"`
`"identity_updated"`
`"token_pool_confirmed"`
`"token_pool_op_failed"`
`"token_transfer_confirmed"`
`"token_transfer_op_failed"`
`"token_approval_confirmed"`
`"token_approval_op_failed"`
`"contract_interface_confirmed"`
`"contract_api_confirmed"`
`"blockchain_event_received"`
`"blockchain_invoke_op_succeeded"`
`"blockchain_invoke_op_failed"`
`"blockchain_contract_deploy_op_succeeded"`
`"blockchain_contract_deploy_op_failed"` |
| `namespace` | The namespace of the event. Your application must subscribe to events within a namespace | `string` |
| `reference` | The UUID of an resource that is the subject of this event. The event type determines what type of resource is referenced, and whether this field might be unset | [`UUID`](simpletypes#uuid) |
| `correlator` | For message events, this is the 'header.cid' field from the referenced message. For certain other event types, a secondary object is referenced such as a token pool | [`UUID`](simpletypes#uuid) |
diff --git a/docs/reference/types/message.md b/docs/reference/types/message.md
index cfd55a5ce1..3e1defe598 100644
--- a/docs/reference/types/message.md
+++ b/docs/reference/types/message.md
@@ -72,7 +72,7 @@ nav_order: 15
| `id` | The UUID of the message. Unique to each message | [`UUID`](simpletypes#uuid) |
| `cid` | The correlation ID of the message. Set this when a message is a response to another message | [`UUID`](simpletypes#uuid) |
| `type` | The type of the message | `FFEnum`:
`"definition"`
`"broadcast"`
`"private"`
`"groupinit"`
`"transfer_broadcast"`
`"transfer_private"` |
-| `txtype` | The type of transaction used to order/deliver this message | `FFEnum`:
`"none"`
`"unpinned"`
`"batch_pin"`
`"network_action"`
`"token_pool"`
`"token_transfer"`
`"contract_invoke"`
`"token_approval"`
`"data_publish"` |
+| `txtype` | The type of transaction used to order/deliver this message | `FFEnum`:
`"none"`
`"unpinned"`
`"batch_pin"`
`"network_action"`
`"token_pool"`
`"token_transfer"`
`"contract_deploy"`
`"contract_invoke"`
`"token_approval"`
`"data_publish"` |
| `author` | The DID of identity of the submitter | `string` |
| `key` | The on-chain signing key used to sign the transaction | `string` |
| `created` | The creation time of the message | [`FFTime`](simpletypes#fftime) |
diff --git a/docs/reference/types/operation.md b/docs/reference/types/operation.md
index c182b7c7ac..d9dc7d54e8 100644
--- a/docs/reference/types/operation.md
+++ b/docs/reference/types/operation.md
@@ -47,7 +47,7 @@ nav_order: 7
| `id` | The UUID of the operation | [`UUID`](simpletypes#uuid) |
| `namespace` | The namespace of the operation | `string` |
| `tx` | The UUID of the FireFly transaction the operation is part of | [`UUID`](simpletypes#uuid) |
-| `type` | The type of the operation | `FFEnum`:
`"blockchain_pin_batch"`
`"blockchain_network_action"`
`"blockchain_invoke"`
`"sharedstorage_upload_batch"`
`"sharedstorage_upload_blob"`
`"sharedstorage_upload_value"`
`"sharedstorage_download_batch"`
`"sharedstorage_download_blob"`
`"dataexchange_send_batch"`
`"dataexchange_send_blob"`
`"token_create_pool"`
`"token_activate_pool"`
`"token_transfer"`
`"token_approval"` |
+| `type` | The type of the operation | `FFEnum`:
`"blockchain_pin_batch"`
`"blockchain_network_action"`
`"blockchain_deploy"`
`"blockchain_invoke"`
`"sharedstorage_upload_batch"`
`"sharedstorage_upload_blob"`
`"sharedstorage_upload_value"`
`"sharedstorage_download_batch"`
`"sharedstorage_download_blob"`
`"dataexchange_send_batch"`
`"dataexchange_send_blob"`
`"token_create_pool"`
`"token_activate_pool"`
`"token_transfer"`
`"token_approval"` |
| `status` | The current status of the operation | `OpStatus` |
| `plugin` | The plugin responsible for performing the operation | `string` |
| `input` | The input to this operation | [`JSONObject`](simpletypes#jsonobject) |
diff --git a/docs/reference/types/transaction.md b/docs/reference/types/transaction.md
index d7a20531b5..87b931491f 100644
--- a/docs/reference/types/transaction.md
+++ b/docs/reference/types/transaction.md
@@ -40,7 +40,7 @@ nav_order: 6
|------------|-------------|------|
| `id` | The UUID of the FireFly transaction | [`UUID`](simpletypes#uuid) |
| `namespace` | The namespace of the FireFly transaction | `string` |
-| `type` | The type of the FireFly transaction | `FFEnum`:
`"none"`
`"unpinned"`
`"batch_pin"`
`"network_action"`
`"token_pool"`
`"token_transfer"`
`"contract_invoke"`
`"token_approval"`
`"data_publish"` |
+| `type` | The type of the FireFly transaction | `FFEnum`:
`"none"`
`"unpinned"`
`"batch_pin"`
`"network_action"`
`"token_pool"`
`"token_transfer"`
`"contract_deploy"`
`"contract_invoke"`
`"token_approval"`
`"data_publish"` |
| `created` | The time the transaction was created on this node. Note the transaction is individually created with the same UUID on each participant in the FireFly transaction | [`FFTime`](simpletypes#fftime) |
| `blockchainIds` | The blockchain transaction ID, in the format specific to the blockchain involved in the transaction. Not all FireFly transactions include a blockchain. FireFly transactions are extensible to support multiple blockchain transactions | `string[]` |
diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml
index a82c1428b8..d8ca727643 100644
--- a/docs/swagger/swagger.yaml
+++ b/docs/swagger/swagger.yaml
@@ -673,6 +673,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -746,6 +747,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -1978,6 +1980,207 @@ paths:
description: ""
tags:
- Default Namespace
+ /contracts/deploy:
+ post:
+ description: Deploy a new smart contract
+ operationId: postContractDeploy
+ parameters:
+ - description: When true the HTTP request blocks until the message is confirmed
+ in: query
+ name: confirm
+ schema:
+ example: "true"
+ type: string
+ - description: Server-side request timeout (milliseconds, or set a custom suffix
+ like 10s)
+ in: header
+ name: Request-Timeout
+ schema:
+ default: 2m0s
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ contract:
+ description: The smart contract to deploy. This should be pre-compiled
+ if required by the blockchain connector
+ definition:
+ description: The definition of the smart contract
+ input:
+ description: An optional array of inputs passed to the smart contract's
+ constructor, if applicable
+ items:
+ description: An optional array of inputs passed to the smart contract's
+ constructor, if applicable
+ type: array
+ key:
+ description: The blockchain signing key that will be used to deploy
+ the contract. Defaults to the first signing key of the organization
+ that operates the node
+ type: string
+ options:
+ additionalProperties:
+ description: A map of named inputs that will be passed through
+ to the blockchain connector
+ description: A map of named inputs that will be passed through to
+ the blockchain connector
+ type: object
+ type: object
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ properties:
+ created:
+ description: The time the operation was created
+ format: date-time
+ type: string
+ error:
+ description: Any error reported back from the plugin for this
+ operation
+ type: string
+ id:
+ description: The UUID of the operation
+ format: uuid
+ type: string
+ input:
+ additionalProperties:
+ description: The input to this operation
+ description: The input to this operation
+ type: object
+ namespace:
+ description: The namespace of the operation
+ type: string
+ output:
+ additionalProperties:
+ description: Any output reported back from the plugin for this
+ operation
+ description: Any output reported back from the plugin for this
+ operation
+ type: object
+ plugin:
+ description: The plugin responsible for performing the operation
+ type: string
+ retry:
+ description: If this operation was initiated as a retry to a previous
+ operation, this field points to the UUID of the operation being
+ retried
+ format: uuid
+ type: string
+ status:
+ description: The current status of the operation
+ type: string
+ tx:
+ description: The UUID of the FireFly transaction the operation
+ is part of
+ format: uuid
+ type: string
+ type:
+ description: The type of the operation
+ enum:
+ - blockchain_pin_batch
+ - blockchain_network_action
+ - blockchain_deploy
+ - blockchain_invoke
+ - sharedstorage_upload_batch
+ - sharedstorage_upload_blob
+ - sharedstorage_upload_value
+ - sharedstorage_download_batch
+ - sharedstorage_download_blob
+ - dataexchange_send_batch
+ - dataexchange_send_blob
+ - token_create_pool
+ - token_activate_pool
+ - token_transfer
+ - token_approval
+ type: string
+ updated:
+ description: The last update time of the operation
+ format: date-time
+ type: string
+ type: object
+ description: Success
+ "202":
+ content:
+ application/json:
+ schema:
+ properties:
+ created:
+ description: The time the operation was created
+ format: date-time
+ type: string
+ error:
+ description: Any error reported back from the plugin for this
+ operation
+ type: string
+ id:
+ description: The UUID of the operation
+ format: uuid
+ type: string
+ input:
+ additionalProperties:
+ description: The input to this operation
+ description: The input to this operation
+ type: object
+ namespace:
+ description: The namespace of the operation
+ type: string
+ output:
+ additionalProperties:
+ description: Any output reported back from the plugin for this
+ operation
+ description: Any output reported back from the plugin for this
+ operation
+ type: object
+ plugin:
+ description: The plugin responsible for performing the operation
+ type: string
+ retry:
+ description: If this operation was initiated as a retry to a previous
+ operation, this field points to the UUID of the operation being
+ retried
+ format: uuid
+ type: string
+ status:
+ description: The current status of the operation
+ type: string
+ tx:
+ description: The UUID of the FireFly transaction the operation
+ is part of
+ format: uuid
+ type: string
+ type:
+ description: The type of the operation
+ enum:
+ - blockchain_pin_batch
+ - blockchain_network_action
+ - blockchain_deploy
+ - blockchain_invoke
+ - sharedstorage_upload_batch
+ - sharedstorage_upload_blob
+ - sharedstorage_upload_value
+ - sharedstorage_download_batch
+ - sharedstorage_download_blob
+ - dataexchange_send_batch
+ - dataexchange_send_blob
+ - token_create_pool
+ - token_activate_pool
+ - token_transfer
+ - token_approval
+ type: string
+ updated:
+ description: The last update time of the operation
+ format: date-time
+ type: string
+ type: object
+ description: Success
+ default:
+ description: ""
+ tags:
+ - Default Namespace
/contracts/interfaces:
get:
description: Gets a list of contract interfaces that have been published
@@ -3305,6 +3508,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -3378,6 +3582,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -4978,6 +5183,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -5796,6 +6002,8 @@ paths:
- blockchain_event_received
- blockchain_invoke_op_succeeded
- blockchain_invoke_op_failed
+ - blockchain_contract_deploy_op_succeeded
+ - blockchain_contract_deploy_op_failed
type: string
type: object
type: array
@@ -5906,6 +6114,8 @@ paths:
- blockchain_event_received
- blockchain_invoke_op_succeeded
- blockchain_invoke_op_failed
+ - blockchain_contract_deploy_op_succeeded
+ - blockchain_contract_deploy_op_failed
type: string
type: object
description: Success
@@ -7380,6 +7590,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -7623,6 +7834,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -7929,6 +8141,8 @@ paths:
- blockchain_event_received
- blockchain_invoke_op_succeeded
- blockchain_invoke_op_failed
+ - blockchain_contract_deploy_op_succeeded
+ - blockchain_contract_deploy_op_failed
type: string
type: object
type: array
@@ -7995,6 +8209,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -8101,6 +8316,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -8208,6 +8424,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -8336,6 +8553,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -8512,6 +8730,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -8625,6 +8844,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -8759,6 +8979,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -8931,6 +9152,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -9124,6 +9346,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -10021,6 +10244,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -10094,6 +10318,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -11509,6 +11734,214 @@ paths:
description: ""
tags:
- Non-Default Namespace
+ /namespaces/{ns}/contracts/deploy:
+ post:
+ description: Deploy a new smart contract
+ operationId: postContractDeployNamespace
+ parameters:
+ - description: The namespace which scopes this request
+ in: path
+ name: ns
+ required: true
+ schema:
+ example: default
+ type: string
+ - description: When true the HTTP request blocks until the message is confirmed
+ in: query
+ name: confirm
+ schema:
+ example: "true"
+ type: string
+ - description: Server-side request timeout (milliseconds, or set a custom suffix
+ like 10s)
+ in: header
+ name: Request-Timeout
+ schema:
+ default: 2m0s
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ contract:
+ description: The smart contract to deploy. This should be pre-compiled
+ if required by the blockchain connector
+ definition:
+ description: The definition of the smart contract
+ input:
+ description: An optional array of inputs passed to the smart contract's
+ constructor, if applicable
+ items:
+ description: An optional array of inputs passed to the smart contract's
+ constructor, if applicable
+ type: array
+ key:
+ description: The blockchain signing key that will be used to deploy
+ the contract. Defaults to the first signing key of the organization
+ that operates the node
+ type: string
+ options:
+ additionalProperties:
+ description: A map of named inputs that will be passed through
+ to the blockchain connector
+ description: A map of named inputs that will be passed through to
+ the blockchain connector
+ type: object
+ type: object
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ properties:
+ created:
+ description: The time the operation was created
+ format: date-time
+ type: string
+ error:
+ description: Any error reported back from the plugin for this
+ operation
+ type: string
+ id:
+ description: The UUID of the operation
+ format: uuid
+ type: string
+ input:
+ additionalProperties:
+ description: The input to this operation
+ description: The input to this operation
+ type: object
+ namespace:
+ description: The namespace of the operation
+ type: string
+ output:
+ additionalProperties:
+ description: Any output reported back from the plugin for this
+ operation
+ description: Any output reported back from the plugin for this
+ operation
+ type: object
+ plugin:
+ description: The plugin responsible for performing the operation
+ type: string
+ retry:
+ description: If this operation was initiated as a retry to a previous
+ operation, this field points to the UUID of the operation being
+ retried
+ format: uuid
+ type: string
+ status:
+ description: The current status of the operation
+ type: string
+ tx:
+ description: The UUID of the FireFly transaction the operation
+ is part of
+ format: uuid
+ type: string
+ type:
+ description: The type of the operation
+ enum:
+ - blockchain_pin_batch
+ - blockchain_network_action
+ - blockchain_deploy
+ - blockchain_invoke
+ - sharedstorage_upload_batch
+ - sharedstorage_upload_blob
+ - sharedstorage_upload_value
+ - sharedstorage_download_batch
+ - sharedstorage_download_blob
+ - dataexchange_send_batch
+ - dataexchange_send_blob
+ - token_create_pool
+ - token_activate_pool
+ - token_transfer
+ - token_approval
+ type: string
+ updated:
+ description: The last update time of the operation
+ format: date-time
+ type: string
+ type: object
+ description: Success
+ "202":
+ content:
+ application/json:
+ schema:
+ properties:
+ created:
+ description: The time the operation was created
+ format: date-time
+ type: string
+ error:
+ description: Any error reported back from the plugin for this
+ operation
+ type: string
+ id:
+ description: The UUID of the operation
+ format: uuid
+ type: string
+ input:
+ additionalProperties:
+ description: The input to this operation
+ description: The input to this operation
+ type: object
+ namespace:
+ description: The namespace of the operation
+ type: string
+ output:
+ additionalProperties:
+ description: Any output reported back from the plugin for this
+ operation
+ description: Any output reported back from the plugin for this
+ operation
+ type: object
+ plugin:
+ description: The plugin responsible for performing the operation
+ type: string
+ retry:
+ description: If this operation was initiated as a retry to a previous
+ operation, this field points to the UUID of the operation being
+ retried
+ format: uuid
+ type: string
+ status:
+ description: The current status of the operation
+ type: string
+ tx:
+ description: The UUID of the FireFly transaction the operation
+ is part of
+ format: uuid
+ type: string
+ type:
+ description: The type of the operation
+ enum:
+ - blockchain_pin_batch
+ - blockchain_network_action
+ - blockchain_deploy
+ - blockchain_invoke
+ - sharedstorage_upload_batch
+ - sharedstorage_upload_blob
+ - sharedstorage_upload_value
+ - sharedstorage_download_batch
+ - sharedstorage_download_blob
+ - dataexchange_send_batch
+ - dataexchange_send_blob
+ - token_create_pool
+ - token_activate_pool
+ - token_transfer
+ - token_approval
+ type: string
+ updated:
+ description: The last update time of the operation
+ format: date-time
+ type: string
+ type: object
+ description: Success
+ default:
+ description: ""
+ tags:
+ - Non-Default Namespace
/namespaces/{ns}/contracts/interfaces:
get:
description: Gets a list of contract interfaces that have been published
@@ -12878,6 +13311,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -12951,6 +13385,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -14628,6 +15063,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -15488,6 +15924,8 @@ paths:
- blockchain_event_received
- blockchain_invoke_op_succeeded
- blockchain_invoke_op_failed
+ - blockchain_contract_deploy_op_succeeded
+ - blockchain_contract_deploy_op_failed
type: string
type: object
type: array
@@ -15605,6 +16043,8 @@ paths:
- blockchain_event_received
- blockchain_invoke_op_succeeded
- blockchain_invoke_op_failed
+ - blockchain_contract_deploy_op_succeeded
+ - blockchain_contract_deploy_op_failed
type: string
type: object
description: Success
@@ -17149,6 +17589,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -17399,6 +17840,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -17719,6 +18161,8 @@ paths:
- blockchain_event_received
- blockchain_invoke_op_succeeded
- blockchain_invoke_op_failed
+ - blockchain_contract_deploy_op_succeeded
+ - blockchain_contract_deploy_op_failed
type: string
type: object
type: array
@@ -17792,6 +18236,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -17943,6 +18388,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -18056,6 +18502,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -18190,6 +18637,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -18373,6 +18821,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -18486,6 +18935,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -18620,6 +19070,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -18799,6 +19250,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -18992,6 +19444,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -20862,6 +21315,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -20966,6 +21420,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -21075,6 +21530,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -24045,6 +24501,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -24484,6 +24941,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -25701,6 +26159,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -26170,6 +26629,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -26300,6 +26760,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -26492,6 +26953,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -28590,6 +29052,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -28687,6 +29150,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -28789,6 +29253,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
@@ -31654,6 +32119,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -32071,6 +32537,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -33253,6 +33720,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -33708,6 +34176,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -33831,6 +34300,7 @@ paths:
- network_action
- token_pool
- token_transfer
+ - contract_deploy
- contract_invoke
- token_approval
- data_publish
@@ -34009,6 +34479,7 @@ paths:
enum:
- blockchain_pin_batch
- blockchain_network_action
+ - blockchain_deploy
- blockchain_invoke
- sharedstorage_upload_batch
- sharedstorage_upload_blob
diff --git a/internal/apiserver/route_post_contract_deploy.go b/internal/apiserver/route_post_contract_deploy.go
new file mode 100644
index 0000000000..d2d1013455
--- /dev/null
+++ b/internal/apiserver/route_post_contract_deploy.go
@@ -0,0 +1,52 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apiserver
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/hyperledger/firefly-common/pkg/ffapi"
+ "github.com/hyperledger/firefly/internal/coremsgs"
+ "github.com/hyperledger/firefly/internal/orchestrator"
+ "github.com/hyperledger/firefly/pkg/core"
+)
+
+var postContractDeploy = &ffapi.Route{
+ Name: "postContractDeploy",
+ Path: "contracts/deploy",
+ Method: http.MethodPost,
+ PathParams: nil,
+ QueryParams: []*ffapi.QueryParam{
+ {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ },
+ Description: coremsgs.APIEndpointsPostContractDeploy,
+ JSONInputValue: func() interface{} { return &core.ContractDeployRequest{} },
+ JSONOutputValue: func() interface{} { return &core.Operation{} },
+ JSONOutputCodes: []int{http.StatusOK, http.StatusAccepted},
+ Extensions: &coreExtensions{
+ EnabledIf: func(or orchestrator.Orchestrator) bool {
+ return or.Contracts() != nil
+ },
+ CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) {
+ waitConfirm := strings.EqualFold(r.QP["confirm"], "true")
+ r.SuccessStatus = syncRetcode(waitConfirm)
+ req := r.Input.(*core.ContractDeployRequest)
+ return cr.or.Contracts().DeployContract(cr.ctx, req, waitConfirm)
+ },
+ },
+}
diff --git a/internal/apiserver/route_post_contract_deploy_test.go b/internal/apiserver/route_post_contract_deploy_test.go
new file mode 100644
index 0000000000..85ec40835f
--- /dev/null
+++ b/internal/apiserver/route_post_contract_deploy_test.go
@@ -0,0 +1,49 @@
+// Copyright © 2022 Kaleido, Inc.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apiserver
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/hyperledger/firefly/mocks/contractmocks"
+ "github.com/hyperledger/firefly/pkg/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+func TestPostContractDeploy(t *testing.T) {
+ o, r := newTestAPIServer()
+ o.On("Authorize", mock.Anything, mock.Anything).Return(nil)
+ mcm := &contractmocks.Manager{}
+ o.On("Contracts").Return(mcm)
+ input := core.Datatype{}
+ var buf bytes.Buffer
+ json.NewEncoder(&buf).Encode(&input)
+ req := httptest.NewRequest("POST", "/api/v1/namespaces/ns1/contracts/deploy", &buf)
+ req.Header.Set("Content-Type", "application/json; charset=utf-8")
+ res := httptest.NewRecorder()
+
+ mcm.On("DeployContract", mock.Anything, mock.MatchedBy(func(req *core.ContractDeployRequest) bool {
+ return true
+ }), false).Return("banana", nil)
+ r.ServeHTTP(res, req)
+
+ assert.Equal(t, 202, res.Result().StatusCode)
+}
diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go
index 30514d745e..3a43852b6b 100644
--- a/internal/apiserver/routes.go
+++ b/internal/apiserver/routes.go
@@ -127,6 +127,7 @@ var routes = append(
postContractAPIQuery,
postContractAPIListeners,
postContractInterfaceGenerate,
+ postContractDeploy,
postContractInvoke,
postContractQuery,
postData,
diff --git a/internal/apiserver/swagger_generate_test.go b/internal/apiserver/swagger_generate_test.go
index ca69b07c2a..a313a2ab11 100644
--- a/internal/apiserver/swagger_generate_test.go
+++ b/internal/apiserver/swagger_generate_test.go
@@ -44,7 +44,7 @@ func TestDownloadSwaggerYAML(t *testing.T) {
defer s.Close()
res, err := http.Get(fmt.Sprintf("http://%s/api/swagger.yaml", s.Listener.Addr()))
- assert.NoError(t, err)
+ assert.Nil(t, err)
b, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, 200, res.StatusCode, string(b))
doc, err := openapi3.NewLoader().LoadFromData(b)
diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go
index 95ec901593..5ccf8716de 100644
--- a/internal/blockchain/ethereum/ethereum.go
+++ b/internal/blockchain/ethereum/ethereum.go
@@ -545,6 +545,10 @@ func (e *Ethereum) buildEthconnectRequestBody(ctx context.Context, messageType,
if signingKey != "" {
body["from"] = signingKey
}
+ return e.applyOptions(ctx, body, options)
+}
+
+func (e *Ethereum) applyOptions(ctx context.Context, body, options map[string]interface{}) (map[string]interface{}, error) {
for k, v := range options {
// Set the new field if it's not already set. Do not allow overriding of existing fields
if _, ok := body[k]; !ok {
@@ -679,6 +683,45 @@ func (e *Ethereum) SubmitNetworkAction(ctx context.Context, nsOpID string, signi
return e.invokeContractMethod(ctx, ethLocation.Address, signingKey, method, nsOpID, input, nil)
}
+func (e *Ethereum) DeployContract(ctx context.Context, nsOpID, signingKey string, definition, contract *fftypes.JSONAny, input []interface{}, options map[string]interface{}) error {
+ if e.metrics.IsMetricsEnabled() {
+ e.metrics.BlockchainContractDeployment()
+ }
+ headers := EthconnectMessageHeaders{
+ Type: "DeployContract",
+ ID: nsOpID,
+ }
+ body := map[string]interface{}{
+ "headers": headers,
+ "from": signingKey,
+ "params": input,
+ "definition": definition,
+ "contract": contract,
+ }
+ if signingKey != "" {
+ body["from"] = signingKey
+ }
+ body, err := e.applyOptions(ctx, body, options)
+ if err != nil {
+ return err
+ }
+
+ client := e.fftmClient
+ if client == nil {
+ client = e.client
+ }
+ var resErr ethError
+ res, err := client.R().
+ SetContext(ctx).
+ SetBody(body).
+ SetError(&resErr).
+ Post("/")
+ if err != nil || !res.IsSuccess() {
+ return wrapError(ctx, &resErr, res, err)
+ }
+ return nil
+}
+
func (e *Ethereum) InvokeContract(ctx context.Context, nsOpID string, signingKey string, location *fftypes.JSONAny, method *fftypes.FFIMethod, input map[string]interface{}, options map[string]interface{}) error {
ethereumLocation, err := parseContractLocation(ctx, location)
if err != nil {
diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go
index 0369b0585c..d7e2655624 100644
--- a/internal/blockchain/ethereum/ethereum_test.go
+++ b/internal/blockchain/ethereum/ethereum_test.go
@@ -85,6 +85,7 @@ func newTestEthereum() (*Ethereum, func()) {
mm := &metricsmocks.Manager{}
mm.On("IsMetricsEnabled").Return(true)
mm.On("BlockchainTransaction", mock.Anything, mock.Anything).Return(nil)
+ mm.On("BlockchainContractDeployment", mock.Anything, mock.Anything).Return(nil)
mm.On("BlockchainQuery", mock.Anything, mock.Anything).Return(nil)
e := &Ethereum{
ctx: ctx,
@@ -2225,6 +2226,102 @@ func TestHandleMessageContractEventError(t *testing.T) {
em.AssertExpectations(t)
}
+func TestDeployContractOK(t *testing.T) {
+ e, cancel := newTestEthereum()
+ defer cancel()
+ httpmock.ActivateNonDefault(e.client.GetClient())
+ defer httpmock.DeactivateAndReset()
+ signingKey := ethHexFormatB32(fftypes.NewRandB32())
+ input := []interface{}{
+ float64(1),
+ "1000000000000000000000000",
+ }
+ options := map[string]interface{}{
+ "customOption": "customValue",
+ }
+ definitionBytes, err := json.Marshal([]interface{}{})
+ contractBytes, err := json.Marshal("0x123456")
+ assert.NoError(t, err)
+ httpmock.RegisterResponder("POST", `http://localhost:12345/`,
+ func(req *http.Request) (*http.Response, error) {
+ var body map[string]interface{}
+ json.NewDecoder(req.Body).Decode(&body)
+ params := body["params"].([]interface{})
+ headers := body["headers"].(map[string]interface{})
+ assert.Equal(t, "DeployContract", headers["type"])
+ assert.Equal(t, float64(1), params[0])
+ assert.Equal(t, "1000000000000000000000000", params[1])
+ assert.Equal(t, body["customOption"].(string), "customValue")
+ return httpmock.NewJsonResponderOrPanic(200, "")(req)
+ })
+ err = e.DeployContract(context.Background(), "", signingKey, fftypes.JSONAnyPtrBytes(definitionBytes), fftypes.JSONAnyPtrBytes(contractBytes), input, options)
+ assert.NoError(t, err)
+}
+
+func TestDeployContractInvalidOption(t *testing.T) {
+ e, cancel := newTestEthereum()
+ defer cancel()
+ httpmock.ActivateNonDefault(e.client.GetClient())
+ defer httpmock.DeactivateAndReset()
+ signingKey := ethHexFormatB32(fftypes.NewRandB32())
+ input := []interface{}{
+ float64(1),
+ "1000000000000000000000000",
+ }
+ options := map[string]interface{}{
+ "contract": "not really a contract",
+ }
+ definitionBytes, err := json.Marshal([]interface{}{})
+ contractBytes, err := json.Marshal("0x123456")
+ assert.NoError(t, err)
+ httpmock.RegisterResponder("POST", `http://localhost:12345/`,
+ func(req *http.Request) (*http.Response, error) {
+ var body map[string]interface{}
+ json.NewDecoder(req.Body).Decode(&body)
+ params := body["params"].([]interface{})
+ headers := body["headers"].(map[string]interface{})
+ assert.Equal(t, "DeployContract", headers["type"])
+ assert.Equal(t, float64(1), params[0])
+ assert.Equal(t, "1000000000000000000000000", params[1])
+ assert.Equal(t, body["customOption"].(string), "customValue")
+ return httpmock.NewJsonResponderOrPanic(400, "pop")(req)
+ })
+ err = e.DeployContract(context.Background(), "", signingKey, fftypes.JSONAnyPtrBytes(definitionBytes), fftypes.JSONAnyPtrBytes(contractBytes), input, options)
+ assert.Regexp(t, "FF10398", err)
+}
+
+func TestDeployContractError(t *testing.T) {
+ e, cancel := newTestEthereum()
+ defer cancel()
+ httpmock.ActivateNonDefault(e.client.GetClient())
+ defer httpmock.DeactivateAndReset()
+ signingKey := ethHexFormatB32(fftypes.NewRandB32())
+ input := []interface{}{
+ float64(1),
+ "1000000000000000000000000",
+ }
+ options := map[string]interface{}{
+ "customOption": "customValue",
+ }
+ definitionBytes, err := json.Marshal([]interface{}{})
+ contractBytes, err := json.Marshal("0x123456")
+ assert.NoError(t, err)
+ httpmock.RegisterResponder("POST", `http://localhost:12345/`,
+ func(req *http.Request) (*http.Response, error) {
+ var body map[string]interface{}
+ json.NewDecoder(req.Body).Decode(&body)
+ params := body["params"].([]interface{})
+ headers := body["headers"].(map[string]interface{})
+ assert.Equal(t, "DeployContract", headers["type"])
+ assert.Equal(t, float64(1), params[0])
+ assert.Equal(t, "1000000000000000000000000", params[1])
+ assert.Equal(t, body["customOption"].(string), "customValue")
+ return httpmock.NewJsonResponderOrPanic(400, "pop")(req)
+ })
+ err = e.DeployContract(context.Background(), "", signingKey, fftypes.JSONAnyPtrBytes(definitionBytes), fftypes.JSONAnyPtrBytes(contractBytes), input, options)
+ assert.Regexp(t, "FF10111", err)
+}
+
func TestInvokeContractOK(t *testing.T) {
e, cancel := newTestEthereum()
defer cancel()
diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go
index 1c3527d6f2..d184783947 100644
--- a/internal/blockchain/fabric/fabric.go
+++ b/internal/blockchain/fabric/fabric.go
@@ -718,6 +718,10 @@ func (f *Fabric) buildFabconnectRequestBody(ctx context.Context, channel, chainc
return body, nil
}
+func (f *Fabric) DeployContract(ctx context.Context, nsOpID, signingKey string, definition, contract *fftypes.JSONAny, input []interface{}, options map[string]interface{}) error {
+ return i18n.NewError(ctx, coremsgs.MsgNotSupportedByBlockchainPlugin)
+}
+
func (f *Fabric) InvokeContract(ctx context.Context, nsOpID string, signingKey string, location *fftypes.JSONAny, method *fftypes.FFIMethod, input map[string]interface{}, options map[string]interface{}) error {
fabricOnChainLocation, err := parseContractLocation(ctx, location)
if err != nil {
diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go
index e28ab97177..3e63d61221 100644
--- a/internal/blockchain/fabric/fabric_test.go
+++ b/internal/blockchain/fabric/fabric_test.go
@@ -1992,6 +1992,38 @@ func TestInvokeContractOK(t *testing.T) {
assert.NoError(t, err)
}
+func TestDeployContractOK(t *testing.T) {
+ e, cancel := newTestFabric()
+ defer cancel()
+ httpmock.ActivateNonDefault(e.client.GetClient())
+ defer httpmock.DeactivateAndReset()
+ signingKey := fftypes.NewRandB32().String()
+ input := []interface{}{
+ float64(1),
+ "1000000000000000000000000",
+ }
+ options := map[string]interface{}{
+ "contract": "not really a contract",
+ }
+ definitionBytes, err := json.Marshal([]interface{}{})
+ contractBytes, err := json.Marshal("0x123456")
+ assert.NoError(t, err)
+ httpmock.RegisterResponder("POST", `http://localhost:12345/`,
+ func(req *http.Request) (*http.Response, error) {
+ var body map[string]interface{}
+ json.NewDecoder(req.Body).Decode(&body)
+ params := body["params"].([]interface{})
+ headers := body["headers"].(map[string]interface{})
+ assert.Equal(t, "DeployContract", headers["type"])
+ assert.Equal(t, float64(1), params[0])
+ assert.Equal(t, "1000000000000000000000000", params[1])
+ assert.Equal(t, body["customOption"].(string), "customValue")
+ return httpmock.NewJsonResponderOrPanic(400, "pop")(req)
+ })
+ err = e.DeployContract(context.Background(), "", signingKey, fftypes.JSONAnyPtrBytes(definitionBytes), fftypes.JSONAnyPtrBytes(contractBytes), input, options)
+ assert.Regexp(t, "FF10429", err)
+}
+
func TestInvokeContractBadSchema(t *testing.T) {
e, cancel := newTestFabric()
defer cancel()
diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go
index 42288c977b..9207fefe48 100644
--- a/internal/contracts/manager.go
+++ b/internal/contracts/manager.go
@@ -44,6 +44,7 @@ type Manager interface {
GetFFIs(ctx context.Context, filter database.AndFilter) ([]*fftypes.FFI, *database.FilterResult, error)
ResolveFFI(ctx context.Context, ffi *fftypes.FFI) error
+ DeployContract(ctx context.Context, req *core.ContractDeployRequest, waitConfirm bool) (interface{}, error)
InvokeContract(ctx context.Context, req *core.ContractCallRequest, waitConfirm bool) (interface{}, error)
InvokeContractAPI(ctx context.Context, apiName, methodPath string, req *core.ContractCallRequest, waitConfirm bool) (interface{}, error)
GetContractAPI(ctx context.Context, httpServerURL, apiName string) (*core.ContractAPI, error)
@@ -98,6 +99,7 @@ func NewContractManager(ctx context.Context, ns string, di database.Plugin, bi b
om.RegisterHandler(ctx, cm, []core.OpType{
core.OpTypeBlockchainInvoke,
+ core.OpTypeBlockchainContractDeploy,
})
return cm, nil
@@ -176,12 +178,58 @@ func (cm *contractManager) writeInvokeTransaction(ctx context.Context, req *core
cm.namespace,
txid,
core.OpTypeBlockchainInvoke)
- if err = addBlockchainInvokeInputs(op, req); err == nil {
+ if err = addBlockchainReqInputs(op, req); err == nil {
err = cm.operations.AddOrReuseOperation(ctx, op)
}
return op, err
}
+func (cm *contractManager) writeDeployTransaction(ctx context.Context, req *core.ContractDeployRequest) (*core.Operation, error) {
+ txid, err := cm.txHelper.SubmitNewTransaction(ctx, core.TransactionTypeContractDeploy)
+ if err != nil {
+ return nil, err
+ }
+
+ op := core.NewOperation(
+ cm.blockchain,
+ cm.namespace,
+ txid,
+ core.OpTypeBlockchainContractDeploy)
+ if err = addBlockchainReqInputs(op, req); err == nil {
+ err = cm.operations.AddOrReuseOperation(ctx, op)
+ }
+ return op, err
+}
+
+func (cm *contractManager) DeployContract(ctx context.Context, req *core.ContractDeployRequest, waitConfirm bool) (res interface{}, err error) {
+ req.Key, err = cm.identity.NormalizeSigningKey(ctx, req.Key, identity.KeyNormalizationBlockchainPlugin)
+ if err != nil {
+ return nil, err
+ }
+
+ var op *core.Operation
+ err = cm.database.RunAsGroup(ctx, func(ctx context.Context) (err error) {
+ op, err = cm.writeDeployTransaction(ctx, req)
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ send := func(ctx context.Context) error {
+ _, err := cm.operations.RunOperation(ctx, opBlockchainContractDeploy(op, req))
+ return err
+ }
+ if waitConfirm {
+ return cm.syncasync.WaitForDeployOperation(ctx, op.ID, send)
+ }
+ err = send(ctx)
+ return op, err
+}
+
func (cm *contractManager) InvokeContract(ctx context.Context, req *core.ContractCallRequest, waitConfirm bool) (res interface{}, err error) {
req.Key, err = cm.identity.NormalizeSigningKey(ctx, req.Key, identity.KeyNormalizationBlockchainPlugin)
if err != nil {
diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go
index cab669030d..ec844ca171 100644
--- a/internal/contracts/manager_test.go
+++ b/internal/contracts/manager_test.go
@@ -18,6 +18,7 @@ package contracts
import (
"context"
+ "errors"
"fmt"
"testing"
"time"
@@ -1317,6 +1318,125 @@ func TestGetFFIs(t *testing.T) {
assert.NoError(t, err)
}
+func TestDeployContract(t *testing.T) {
+ cm := newTestContractManager()
+ mim := cm.identity.(*identitymanagermocks.Manager)
+ mdi := cm.database.(*databasemocks.Plugin)
+ mth := cm.txHelper.(*txcommonmocks.Helper)
+ mom := cm.operations.(*operationmocks.Manager)
+ signingKey := "0x2468"
+ req := &core.ContractDeployRequest{
+ Key: signingKey,
+ Definition: fftypes.JSONAnyPtr("[]"),
+ Contract: fftypes.JSONAnyPtr("\"0x123456\""),
+ Input: []interface{}{"one", "two", "three"},
+ }
+
+ mth.On("SubmitNewTransaction", mock.Anything, core.TransactionTypeContractDeploy).Return(fftypes.NewUUID(), nil)
+ mim.On("NormalizeSigningKey", mock.Anything, signingKey, identity.KeyNormalizationBlockchainPlugin).Return("key-resolved", nil)
+ mom.On("AddOrReuseOperation", mock.Anything, mock.MatchedBy(func(op *core.Operation) bool {
+ return op.Namespace == "ns1" && op.Type == core.OpTypeBlockchainContractDeploy && op.Plugin == "mockblockchain"
+ })).Return(nil)
+ mom.On("RunOperation", mock.Anything, mock.MatchedBy(func(op *core.PreparedOperation) bool {
+ data := op.Data.(blockchainContractDeployData)
+ return op.Type == core.OpTypeBlockchainContractDeploy && data.Request == req
+ })).Return(nil, nil)
+
+ _, err := cm.DeployContract(context.Background(), req, false)
+
+ assert.NoError(t, err)
+
+ mth.AssertExpectations(t)
+ mim.AssertExpectations(t)
+ mdi.AssertExpectations(t)
+ mom.AssertExpectations(t)
+}
+
+func TestDeployContractSync(t *testing.T) {
+ cm := newTestContractManager()
+ mim := cm.identity.(*identitymanagermocks.Manager)
+ mdi := cm.database.(*databasemocks.Plugin)
+ mth := cm.txHelper.(*txcommonmocks.Helper)
+ mom := cm.operations.(*operationmocks.Manager)
+ sam := cm.syncasync.(*syncasyncmocks.Bridge)
+ signingKey := "0x2468"
+ req := &core.ContractDeployRequest{
+ Key: signingKey,
+ Definition: fftypes.JSONAnyPtr("[]"),
+ Contract: fftypes.JSONAnyPtr("\"0x123456\""),
+ Input: []interface{}{"one", "two", "three"},
+ }
+
+ mth.On("SubmitNewTransaction", mock.Anything, core.TransactionTypeContractDeploy).Return(fftypes.NewUUID(), nil)
+ mim.On("NormalizeSigningKey", mock.Anything, signingKey, identity.KeyNormalizationBlockchainPlugin).Return("key-resolved", nil)
+ mom.On("AddOrReuseOperation", mock.Anything, mock.MatchedBy(func(op *core.Operation) bool {
+ return op.Namespace == "ns1" && op.Type == core.OpTypeBlockchainContractDeploy && op.Plugin == "mockblockchain"
+ })).Return(nil)
+
+ sam.On("WaitForDeployOperation", mock.Anything, mock.Anything, mock.Anything).Return(&core.Operation{Status: core.OpStatusSucceeded}, nil)
+
+ _, err := cm.DeployContract(context.Background(), req, true)
+
+ assert.NoError(t, err)
+
+ mth.AssertExpectations(t)
+ mim.AssertExpectations(t)
+ mdi.AssertExpectations(t)
+ mom.AssertExpectations(t)
+}
+
+func TestDeployContractNormalizeSigningKeyFail(t *testing.T) {
+ cm := newTestContractManager()
+ mim := cm.identity.(*identitymanagermocks.Manager)
+ mdi := cm.database.(*databasemocks.Plugin)
+ mth := cm.txHelper.(*txcommonmocks.Helper)
+ mom := cm.operations.(*operationmocks.Manager)
+ signingKey := "0x2468"
+ req := &core.ContractDeployRequest{
+ Key: signingKey,
+ Definition: fftypes.JSONAnyPtr("[]"),
+ Contract: fftypes.JSONAnyPtr("\"0x123456\""),
+ Input: []interface{}{"one", "two", "three"},
+ }
+
+ mim.On("NormalizeSigningKey", mock.Anything, signingKey, identity.KeyNormalizationBlockchainPlugin).Return("", errors.New("pop"))
+ _, err := cm.DeployContract(context.Background(), req, false)
+
+ assert.Regexp(t, "pop", err)
+
+ mth.AssertExpectations(t)
+ mim.AssertExpectations(t)
+ mdi.AssertExpectations(t)
+ mom.AssertExpectations(t)
+}
+
+func TestDeployContractSubmitNewTransactionFail(t *testing.T) {
+ cm := newTestContractManager()
+ mim := cm.identity.(*identitymanagermocks.Manager)
+ mdi := cm.database.(*databasemocks.Plugin)
+ mth := cm.txHelper.(*txcommonmocks.Helper)
+ mom := cm.operations.(*operationmocks.Manager)
+ signingKey := "0x2468"
+ req := &core.ContractDeployRequest{
+ Key: signingKey,
+ Definition: fftypes.JSONAnyPtr("[]"),
+ Contract: fftypes.JSONAnyPtr("\"0x123456\""),
+ Input: []interface{}{"one", "two", "three"},
+ }
+
+ mth.On("SubmitNewTransaction", mock.Anything, core.TransactionTypeContractDeploy).Return(nil, errors.New("pop"))
+ mim.On("NormalizeSigningKey", mock.Anything, signingKey, identity.KeyNormalizationBlockchainPlugin).Return("key-resolved", nil)
+
+ _, err := cm.DeployContract(context.Background(), req, false)
+
+ assert.Regexp(t, "pop", err)
+
+ mth.AssertExpectations(t)
+ mim.AssertExpectations(t)
+ mdi.AssertExpectations(t)
+ mom.AssertExpectations(t)
+}
+
func TestInvokeContract(t *testing.T) {
cm := newTestContractManager()
mim := cm.identity.(*identitymanagermocks.Manager)
diff --git a/internal/contracts/operations.go b/internal/contracts/operations.go
index 889860c99c..25823586ab 100644
--- a/internal/contracts/operations.go
+++ b/internal/contracts/operations.go
@@ -30,7 +30,11 @@ type blockchainInvokeData struct {
Request *core.ContractCallRequest `json:"request"`
}
-func addBlockchainInvokeInputs(op *core.Operation, req *core.ContractCallRequest) (err error) {
+type blockchainContractDeployData struct {
+ Request *core.ContractDeployRequest `json:"request"`
+}
+
+func addBlockchainReqInputs(op *core.Operation, req interface{}) (err error) {
var reqJSON []byte
if reqJSON, err = json.Marshal(req); err == nil {
err = json.Unmarshal(reqJSON, &op.Input)
@@ -38,6 +42,15 @@ func addBlockchainInvokeInputs(op *core.Operation, req *core.ContractCallRequest
return err
}
+func retrieveBlockchainDeployInputs(ctx context.Context, op *core.Operation) (*core.ContractDeployRequest, error) {
+ var req core.ContractDeployRequest
+ s := op.Input.String()
+ if err := json.Unmarshal([]byte(s), &req); err != nil {
+ return nil, i18n.WrapError(ctx, err, i18n.MsgJSONObjectParseFailed, s)
+ }
+ return &req, nil
+}
+
func retrieveBlockchainInvokeInputs(ctx context.Context, op *core.Operation) (*core.ContractCallRequest, error) {
var req core.ContractCallRequest
s := op.Input.String()
@@ -55,7 +68,12 @@ func (cm *contractManager) PrepareOperation(ctx context.Context, op *core.Operat
return nil, err
}
return opBlockchainInvoke(op, req), nil
-
+ case core.OpTypeBlockchainContractDeploy:
+ req, err := retrieveBlockchainDeployInputs(ctx, op)
+ if err != nil {
+ return nil, err
+ }
+ return opBlockchainContractDeploy(op, req), nil
default:
return nil, i18n.NewError(ctx, coremsgs.MsgOperationNotSupported, op.Type)
}
@@ -67,14 +85,18 @@ func (cm *contractManager) RunOperation(ctx context.Context, op *core.PreparedOp
req := data.Request
return nil, false, cm.blockchain.InvokeContract(ctx, op.NamespacedIDString(), req.Key, req.Location, req.Method, req.Input, req.Options)
+ case blockchainContractDeployData:
+ req := data.Request
+ return nil, false, cm.blockchain.DeployContract(ctx, op.NamespacedIDString(), req.Key, req.Definition, req.Contract, req.Input, req.Options)
default:
return nil, false, i18n.NewError(ctx, coremsgs.MsgOperationDataIncorrect, op.Data)
}
}
func (cm *contractManager) OnOperationUpdate(ctx context.Context, op *core.Operation, update *core.OperationUpdate) error {
- // Special handling for OpTypeBlockchainInvoke, which writes an event when it succeeds or fails
- if op.Type == core.OpTypeBlockchainInvoke {
+ // Special handling for blockchain operations, which writes an event when it succeeds or fails
+ switch op.Type {
+ case core.OpTypeBlockchainInvoke:
if update.Status == core.OpStatusSucceeded {
event := core.NewEvent(core.EventTypeBlockchainInvokeOpSucceeded, op.Namespace, op.ID, op.Transaction, "")
if err := cm.database.InsertEvent(ctx, event); err != nil {
@@ -87,6 +109,19 @@ func (cm *contractManager) OnOperationUpdate(ctx context.Context, op *core.Opera
return err
}
}
+ case core.OpTypeBlockchainContractDeploy:
+ if update.Status == core.OpStatusSucceeded {
+ event := core.NewEvent(core.EventTypeBlockchainContractDeployOpSucceeded, op.Namespace, op.ID, op.Transaction, "")
+ if err := cm.database.InsertEvent(ctx, event); err != nil {
+ return err
+ }
+ }
+ if update.Status == core.OpStatusFailed {
+ event := core.NewEvent(core.EventTypeBlockchainContractDeployOpFailed, op.Namespace, op.ID, op.Transaction, "")
+ if err := cm.database.InsertEvent(ctx, event); err != nil {
+ return err
+ }
+ }
}
return nil
}
@@ -100,3 +135,13 @@ func opBlockchainInvoke(op *core.Operation, req *core.ContractCallRequest) *core
Data: blockchainInvokeData{Request: req},
}
}
+
+func opBlockchainContractDeploy(op *core.Operation, req *core.ContractDeployRequest) *core.PreparedOperation {
+ return &core.PreparedOperation{
+ ID: op.ID,
+ Namespace: op.Namespace,
+ Plugin: op.Plugin,
+ Type: op.Type,
+ Data: blockchainContractDeployData{Request: req},
+ }
+}
diff --git a/internal/contracts/operations_test.go b/internal/contracts/operations_test.go
index d37932d0ba..5972dd0ea7 100644
--- a/internal/contracts/operations_test.go
+++ b/internal/contracts/operations_test.go
@@ -46,7 +46,7 @@ func TestPrepareAndRunBlockchainInvoke(t *testing.T) {
"value": "1",
},
}
- err := addBlockchainInvokeInputs(op, req)
+ err := addBlockchainReqInputs(op, req)
assert.NoError(t, err)
mbi := cm.blockchain.(*blockchainmocks.Plugin)
@@ -68,6 +68,39 @@ func TestPrepareAndRunBlockchainInvoke(t *testing.T) {
mbi.AssertExpectations(t)
}
+func TestPrepareAndRunBlockchainContractDeploy(t *testing.T) {
+ cm := newTestContractManager()
+
+ op := &core.Operation{
+ Type: core.OpTypeBlockchainContractDeploy,
+ ID: fftypes.NewUUID(),
+ Namespace: "ns1",
+ }
+ signingKey := "0x2468"
+ req := &core.ContractDeployRequest{
+ Key: signingKey,
+ Definition: fftypes.JSONAnyPtr("[]"),
+ Contract: fftypes.JSONAnyPtr("\"0x123456\""),
+ Input: []interface{}{"one", "two", "three"},
+ }
+ err := addBlockchainReqInputs(op, req)
+ assert.NoError(t, err)
+
+ mbi := cm.blockchain.(*blockchainmocks.Plugin)
+ mbi.On("DeployContract", context.Background(), "ns1:"+op.ID.String(), signingKey, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+ po, err := cm.PrepareOperation(context.Background(), op)
+ assert.NoError(t, err)
+ assert.Equal(t, req, po.Data.(blockchainContractDeployData).Request)
+
+ _, complete, err := cm.RunOperation(context.Background(), po)
+
+ assert.False(t, complete)
+ assert.NoError(t, err)
+
+ mbi.AssertExpectations(t)
+}
+
func TestPrepareOperationNotSupported(t *testing.T) {
cm := newTestContractManager()
@@ -77,6 +110,18 @@ func TestPrepareOperationNotSupported(t *testing.T) {
assert.Regexp(t, "FF10371", err)
}
+func TestPrepareOperationBlockchainDeployBadInput(t *testing.T) {
+ cm := newTestContractManager()
+
+ op := &core.Operation{
+ Type: core.OpTypeBlockchainContractDeploy,
+ Input: fftypes.JSONObject{"input": "bad"},
+ }
+
+ _, err := cm.PrepareOperation(context.Background(), op)
+ assert.Regexp(t, "FF00127", err)
+}
+
func TestPrepareOperationBlockchainInvokeBadInput(t *testing.T) {
cm := newTestContractManager()
@@ -107,6 +152,50 @@ func TestOperationUpdate(t *testing.T) {
assert.NoError(t, err)
}
+func TestOperationUpdateDeploySucceed(t *testing.T) {
+ cm := newTestContractManager()
+
+ op := &core.Operation{
+ ID: fftypes.NewUUID(),
+ Type: core.OpTypeBlockchainContractDeploy,
+ }
+ update := &core.OperationUpdate{
+ Status: core.OpStatusSucceeded,
+ }
+
+ mdi := cm.database.(*databasemocks.Plugin)
+ mdi.On("InsertEvent", context.Background(), mock.MatchedBy(func(event *core.Event) bool {
+ return event.Type == core.EventTypeBlockchainContractDeployOpSucceeded && *event.Reference == *op.ID
+ })).Return(fmt.Errorf("pop"))
+
+ err := cm.OnOperationUpdate(context.Background(), op, update)
+ assert.EqualError(t, err, "pop")
+
+ mdi.AssertExpectations(t)
+}
+
+func TestOperationUpdateDeployFail(t *testing.T) {
+ cm := newTestContractManager()
+
+ op := &core.Operation{
+ ID: fftypes.NewUUID(),
+ Type: core.OpTypeBlockchainContractDeploy,
+ }
+ update := &core.OperationUpdate{
+ Status: core.OpStatusFailed,
+ }
+
+ mdi := cm.database.(*databasemocks.Plugin)
+ mdi.On("InsertEvent", context.Background(), mock.MatchedBy(func(event *core.Event) bool {
+ return event.Type == core.EventTypeBlockchainContractDeployOpFailed && *event.Reference == *op.ID
+ })).Return(fmt.Errorf("pop"))
+
+ err := cm.OnOperationUpdate(context.Background(), op, update)
+ assert.EqualError(t, err, "pop")
+
+ mdi.AssertExpectations(t)
+}
+
func TestOperationUpdateInvokeSucceed(t *testing.T) {
cm := newTestContractManager()
diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go
index 5ba86caf80..dea99d3da5 100644
--- a/internal/coremsgs/en_api_translations.go
+++ b/internal/coremsgs/en_api_translations.go
@@ -143,6 +143,7 @@ var (
APIEndpointsGetVerifierByHash = ffm("api.endpoints.getVerifierByHash", "Gets a verifier by its hash")
APIEndpointsGetVerifiers = ffm("api.endpoints.getVerifiers", "Gets a list of verifiers")
APIEndpointsPatchUpdateIdentity = ffm("api.endpoints.patchUpdateIdentity", "Updates an identity")
+ APIEndpointsPostContractDeploy = ffm("api.endpoints.postContractDeploy", "Deploy a new smart contract")
APIEndpointsPostContractAPIInvoke = ffm("api.endpoints.postContractAPIInvoke", "Invokes a method on a smart contract API. Performs a blockchain transaction.")
APIEndpointsPostContractAPIQuery = ffm("api.endpoints.postContractAPIQuery", "Queries a method on a smart contract API. Performs a read-only query.")
APIEndpointsPostContractInterfaceGenerate = ffm("api.endpoints.postContractInterfaceGenerate", "A convenience method to convert a blockchain specific smart contract format into a FireFly Interface format. The specific blockchain plugin in use must support this functionality.")
diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go
index af1d6b79b2..4597b015fc 100644
--- a/internal/coremsgs/en_error_messages.go
+++ b/internal/coremsgs/en_error_messages.go
@@ -267,4 +267,5 @@ var (
MsgCacheConfigKeyMismatchInternal = ffe("FF10426", "could not initialize cache - '%s' and '%s' do not have identical prefix, mismatching prefixes are: '%s','%s'")
MsgCacheUnexpectedSizeKeyNameInternal = ffe("FF10427", "could not initialize cache - '%s' is not an expected size configuration key suffix. Expected values are: 'size', 'limit'")
MsgUnknownVerifierType = ffe("FF10428", "Unknown verifier type", 400)
+ MsgNotSupportedByBlockchainPlugin = ffe("FF10429", "Not supported by blockchain plugin", 400)
)
diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go
index 903b43453d..6895f21820 100644
--- a/internal/coremsgs/en_struct_descriptions.go
+++ b/internal/coremsgs/en_struct_descriptions.go
@@ -593,6 +593,13 @@ var (
TransactionStatusDetailsError = ffm("TransactionStatusDetails.error", "If an error occurred related to the detail entry, it is included here")
TransactionStatusDetailsInfo = ffm("TransactionStatusDetails.info", "Output details for this entry")
+ // ContractDeployRequest field descriptions
+ ContractDeployRequestKey = ffm("ContractDeployRequest.key", "The blockchain signing key that will be used to deploy the contract. Defaults to the first signing key of the organization that operates the node")
+ ContractDeployRequestInput = ffm("ContractDeployRequest.input", "An optional array of inputs passed to the smart contract's constructor, if applicable")
+ ContractDeployRequestDefinition = ffm("ContractDeployRequest.definition", "The definition of the smart contract")
+ ContractDeployRequestContract = ffm("ContractDeployRequest.contract", "The smart contract to deploy. This should be pre-compiled if required by the blockchain connector")
+ ContractDeployRequestOptions = ffm("ContractDeployRequest.options", "A map of named inputs that will be passed through to the blockchain connector")
+
// ContractCallRequest field descriptions
ContractCallRequestType = ffm("ContractCallRequest.type", "Invocations cause transactions on the blockchain. Whereas queries simply execute logic in your local node to query data at a given current/historical block")
ContractCallRequestInterface = ffm("ContractCallRequest.interface", "The UUID of a method within a pre-configured FireFly interface (FFI) definition for a smart contract. Required if the 'method' is omitted. Also see Contract APIs as a way to configure a dedicated API for your FFI, including all methods and an OpenAPI/Swagger interface")
diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go
index ba56910872..0ac1162bfb 100644
--- a/internal/metrics/metrics.go
+++ b/internal/metrics/metrics.go
@@ -35,6 +35,7 @@ type Manager interface {
MessageConfirmed(msg *core.Message, eventType fftypes.FFEnum)
TransferSubmitted(transfer *core.TokenTransfer)
TransferConfirmed(transfer *core.TokenTransfer)
+ BlockchainContractDeployment()
BlockchainTransaction(location, methodName string)
BlockchainQuery(location, methodName string)
BlockchainEvent(location, signature string)
@@ -129,6 +130,10 @@ func (mm *metricsManager) TransferConfirmed(transfer *core.TokenTransfer) {
}
}
+func (mm *metricsManager) BlockchainContractDeployment() {
+ BlockchainTransactionsCounter.WithLabelValues("", "").Inc()
+}
+
func (mm *metricsManager) BlockchainTransaction(location, methodName string) {
BlockchainTransactionsCounter.WithLabelValues(location, methodName).Inc()
}
diff --git a/internal/syncasync/sync_async_bridge.go b/internal/syncasync/sync_async_bridge.go
index a7bc3908d9..c559d074b4 100644
--- a/internal/syncasync/sync_async_bridge.go
+++ b/internal/syncasync/sync_async_bridge.go
@@ -57,6 +57,8 @@ type Bridge interface {
WaitForTokenApproval(ctx context.Context, id *fftypes.UUID, send SendFunction) (*core.TokenApproval, error)
// WaitForInvokeOperation waits for an operation with the supplied ID
WaitForInvokeOperation(ctx context.Context, id *fftypes.UUID, send SendFunction) (*core.Operation, error)
+ // WaitForDeployOperation waits for an operation with the supplied ID
+ WaitForDeployOperation(ctx context.Context, id *fftypes.UUID, send SendFunction) (*core.Operation, error)
}
// Sender interface may be implemented by other types that wish to provide generic sync/async capabilities
@@ -78,6 +80,7 @@ const (
tokenTransferConfirm
tokenApproveConfirm
invokeOperationConfirm
+ deployOperationConfirm
)
type inflightRequest struct {
@@ -433,7 +436,7 @@ func (sa *syncAsyncBridge) handleApprovalOpFailedEvent(event *core.EventDelivery
return nil
}
-func (sa *syncAsyncBridge) handleOperationSuccededEvent(event *core.EventDelivery) error {
+func (sa *syncAsyncBridge) handleOperationSucceededEvent(event *core.EventDelivery) error {
// See if this is a failure of an inflight invoke operation
inflight := sa.getInFlight(event.Namespace, invokeOperationConfirm, event.Reference)
if inflight == nil {
@@ -450,6 +453,40 @@ func (sa *syncAsyncBridge) handleOperationSuccededEvent(event *core.EventDeliver
return nil
}
+func (sa *syncAsyncBridge) handleContractDeployOperationSucceededEvent(event *core.EventDelivery) error {
+ // See if this is a failure of an inflight invoke operation
+ inflight := sa.getInFlight(event.Namespace, deployOperationConfirm, event.Reference)
+ if inflight == nil {
+ return nil
+ }
+
+ op, err := sa.getOperationFromEvent(event)
+ if err != nil || op == nil {
+ return err
+ }
+
+ go sa.resolveSuccessfulOperation(inflight, "deploy", op)
+
+ return nil
+}
+
+func (sa *syncAsyncBridge) handleContractDeployOperationFailedEvent(event *core.EventDelivery) error {
+ // See if this is a failure of an inflight deploy operation
+ inflight := sa.getInFlight(event.Namespace, deployOperationConfirm, event.Reference)
+ if inflight == nil {
+ return nil
+ }
+
+ op, err := sa.getOperationFromEvent(event)
+ if err != nil || op == nil {
+ return err
+ }
+
+ go sa.resolveFailedOperation(inflight, "deploy", op)
+
+ return nil
+}
+
func (sa *syncAsyncBridge) handleOperationFailedEvent(event *core.EventDelivery) error {
// See if this is a failure of an inflight invoke operation
inflight := sa.getInFlight(event.Namespace, invokeOperationConfirm, event.Reference)
@@ -506,10 +543,16 @@ func (sa *syncAsyncBridge) eventCallback(event *core.EventDelivery) error {
return sa.handleApprovalOpFailedEvent(event)
case core.EventTypeBlockchainInvokeOpSucceeded:
- return sa.handleOperationSuccededEvent(event)
+ return sa.handleOperationSucceededEvent(event)
case core.EventTypeBlockchainInvokeOpFailed:
return sa.handleOperationFailedEvent(event)
+
+ case core.EventTypeBlockchainContractDeployOpSucceeded:
+ return sa.handleContractDeployOperationSucceededEvent(event)
+
+ case core.EventTypeBlockchainContractDeployOpFailed:
+ return sa.handleContractDeployOperationFailedEvent(event)
}
return nil
@@ -660,3 +703,11 @@ func (sa *syncAsyncBridge) WaitForInvokeOperation(ctx context.Context, id *fftyp
}
return reply.(*core.Operation), err
}
+
+func (sa *syncAsyncBridge) WaitForDeployOperation(ctx context.Context, id *fftypes.UUID, send SendFunction) (*core.Operation, error) {
+ reply, err := sa.sendAndWait(ctx, sa.namespace, id, deployOperationConfirm, send)
+ if err != nil {
+ return nil, err
+ }
+ return reply.(*core.Operation), err
+}
diff --git a/internal/syncasync/sync_async_bridge_test.go b/internal/syncasync/sync_async_bridge_test.go
index c00f118868..b7cb0d5151 100644
--- a/internal/syncasync/sync_async_bridge_test.go
+++ b/internal/syncasync/sync_async_bridge_test.go
@@ -255,6 +255,8 @@ func TestEventCallbackNotInflight(t *testing.T) {
core.EventTypeIdentityConfirmed,
core.EventTypeBlockchainInvokeOpSucceeded,
core.EventTypeBlockchainInvokeOpFailed,
+ core.EventTypeBlockchainContractDeployOpSucceeded,
+ core.EventTypeBlockchainContractDeployOpFailed,
} {
err := sa.eventCallback(&core.EventDelivery{
EnrichedEvent: core.EnrichedEvent{
@@ -1423,6 +1425,144 @@ func TestAwaitIdentityFail(t *testing.T) {
assert.Regexp(t, "pop", err)
}
+func TestAwaitDeployOpSucceeded(t *testing.T) {
+
+ sa, cancel := newTestSyncAsyncBridge(t)
+ defer cancel()
+
+ requestID := fftypes.NewUUID()
+ op := &core.Operation{
+ ID: requestID,
+ Status: core.OpStatusSucceeded,
+ }
+
+ mse := sa.sysevents.(*systemeventmocks.EventInterface)
+ mse.On("AddSystemEventListener", "ns1", mock.Anything).Return(nil)
+
+ mom := sa.operations.(*operationmocks.Manager)
+ mom.On("GetOperationByIDCached", sa.ctx, op.ID).Return(op, nil)
+
+ ret, err := sa.WaitForDeployOperation(sa.ctx, requestID, func(ctx context.Context) error {
+ go func() {
+ sa.eventCallback(&core.EventDelivery{
+ EnrichedEvent: core.EnrichedEvent{
+ Event: core.Event{
+ ID: fftypes.NewUUID(),
+ Type: core.EventTypeBlockchainContractDeployOpSucceeded,
+ Reference: requestID,
+ Namespace: "ns1",
+ },
+ },
+ })
+ }()
+ return nil
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, ret, op)
+}
+
+func TestAwaitDeployOpSucceededLookupFail(t *testing.T) {
+
+ sa, cancel := newTestSyncAsyncBridge(t)
+ defer cancel()
+
+ requestID := fftypes.NewUUID()
+ sa.inflight = map[string]map[fftypes.UUID]*inflightRequest{
+ "ns1": {
+ *requestID: &inflightRequest{
+ reqType: deployOperationConfirm,
+ },
+ },
+ }
+
+ mse := sa.sysevents.(*systemeventmocks.EventInterface)
+ mse.On("AddSystemEventListener", "ns1", mock.Anything).Return(nil)
+
+ mom := sa.operations.(*operationmocks.Manager)
+ mom.On("GetOperationByIDCached", sa.ctx, requestID).Return(nil, fmt.Errorf("pop"))
+
+ err := sa.eventCallback(&core.EventDelivery{
+ EnrichedEvent: core.EnrichedEvent{
+ Event: core.Event{
+ ID: fftypes.NewUUID(),
+ Type: core.EventTypeBlockchainContractDeployOpSucceeded,
+ Reference: requestID,
+ Namespace: "ns1",
+ },
+ },
+ })
+ assert.EqualError(t, err, "pop")
+}
+
+func TestAwaitDeployOpFailed(t *testing.T) {
+
+ sa, cancel := newTestSyncAsyncBridge(t)
+ defer cancel()
+
+ requestID := fftypes.NewUUID()
+ op := &core.Operation{
+ ID: requestID,
+ Status: core.OpStatusFailed,
+ Error: "pop",
+ }
+
+ mse := sa.sysevents.(*systemeventmocks.EventInterface)
+ mse.On("AddSystemEventListener", "ns1", mock.Anything).Return(nil)
+
+ mom := sa.operations.(*operationmocks.Manager)
+ mom.On("GetOperationByIDCached", sa.ctx, op.ID).Return(op, nil)
+
+ _, err := sa.WaitForDeployOperation(sa.ctx, requestID, func(ctx context.Context) error {
+ go func() {
+ sa.eventCallback(&core.EventDelivery{
+ EnrichedEvent: core.EnrichedEvent{
+ Event: core.Event{
+ ID: fftypes.NewUUID(),
+ Type: core.EventTypeBlockchainContractDeployOpFailed,
+ Reference: requestID,
+ Namespace: "ns1",
+ },
+ },
+ })
+ }()
+ return nil
+ })
+ assert.EqualError(t, err, "pop")
+}
+
+func TestAwaitDeployOpFailedLookupFail(t *testing.T) {
+
+ sa, cancel := newTestSyncAsyncBridge(t)
+ defer cancel()
+
+ requestID := fftypes.NewUUID()
+ sa.inflight = map[string]map[fftypes.UUID]*inflightRequest{
+ "ns1": {
+ *requestID: &inflightRequest{
+ reqType: deployOperationConfirm,
+ },
+ },
+ }
+
+ mse := sa.sysevents.(*systemeventmocks.EventInterface)
+ mse.On("AddSystemEventListener", "ns1", mock.Anything).Return(nil)
+
+ mom := sa.operations.(*operationmocks.Manager)
+ mom.On("GetOperationByIDCached", sa.ctx, requestID).Return(nil, fmt.Errorf("pop"))
+
+ err := sa.eventCallback(&core.EventDelivery{
+ EnrichedEvent: core.EnrichedEvent{
+ Event: core.Event{
+ ID: fftypes.NewUUID(),
+ Type: core.EventTypeBlockchainContractDeployOpFailed,
+ Reference: requestID,
+ Namespace: "ns1",
+ },
+ },
+ })
+ assert.EqualError(t, err, "pop")
+}
+
func TestAwaitInvokeOpSucceeded(t *testing.T) {
sa, cancel := newTestSyncAsyncBridge(t)
diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go
index a1c5a5e900..e143c2dd0b 100644
--- a/mocks/blockchainmocks/plugin.go
+++ b/mocks/blockchainmocks/plugin.go
@@ -89,6 +89,20 @@ func (_m *Plugin) DeleteContractListener(ctx context.Context, subscription *core
return r0
}
+// DeployContract provides a mock function with given fields: ctx, nsOpID, signingKey, definition, contract, input, options
+func (_m *Plugin) DeployContract(ctx context.Context, nsOpID string, signingKey string, definition *fftypes.JSONAny, contract *fftypes.JSONAny, input []interface{}, options map[string]interface{}) error {
+ ret := _m.Called(ctx, nsOpID, signingKey, definition, contract, input, options)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(context.Context, string, string, *fftypes.JSONAny, *fftypes.JSONAny, []interface{}, map[string]interface{}) error); ok {
+ r0 = rf(ctx, nsOpID, signingKey, definition, contract, input, options)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
// GenerateEventSignature provides a mock function with given fields: ctx, event
func (_m *Plugin) GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) string {
ret := _m.Called(ctx, event)
@@ -366,13 +380,13 @@ func (_m *Plugin) Start() error {
return r0
}
-// SubmitBatchPin provides a mock function with given fields: ctx, nsOpID, remtoeNamespace, signingKey, batch, location
-func (_m *Plugin) SubmitBatchPin(ctx context.Context, nsOpID string, remtoeNamespace string, signingKey string, batch *blockchain.BatchPin, location *fftypes.JSONAny) error {
- ret := _m.Called(ctx, nsOpID, remtoeNamespace, signingKey, batch, location)
+// SubmitBatchPin provides a mock function with given fields: ctx, nsOpID, networkNamespace, signingKey, batch, location
+func (_m *Plugin) SubmitBatchPin(ctx context.Context, nsOpID string, networkNamespace string, signingKey string, batch *blockchain.BatchPin, location *fftypes.JSONAny) error {
+ ret := _m.Called(ctx, nsOpID, networkNamespace, signingKey, batch, location)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, *blockchain.BatchPin, *fftypes.JSONAny) error); ok {
- r0 = rf(ctx, nsOpID, remtoeNamespace, signingKey, batch, location)
+ r0 = rf(ctx, nsOpID, networkNamespace, signingKey, batch, location)
} else {
r0 = ret.Error(0)
}
diff --git a/mocks/contractmocks/manager.go b/mocks/contractmocks/manager.go
index 0b0125bbbb..bd38194a5f 100644
--- a/mocks/contractmocks/manager.go
+++ b/mocks/contractmocks/manager.go
@@ -79,6 +79,29 @@ func (_m *Manager) DeleteContractListenerByNameOrID(ctx context.Context, nameOrI
return r0
}
+// DeployContract provides a mock function with given fields: ctx, req, waitConfirm
+func (_m *Manager) DeployContract(ctx context.Context, req *core.ContractDeployRequest, waitConfirm bool) (interface{}, error) {
+ ret := _m.Called(ctx, req, waitConfirm)
+
+ var r0 interface{}
+ if rf, ok := ret.Get(0).(func(context.Context, *core.ContractDeployRequest, bool) interface{}); ok {
+ r0 = rf(ctx, req, waitConfirm)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(interface{})
+ }
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func(context.Context, *core.ContractDeployRequest, bool) error); ok {
+ r1 = rf(ctx, req, waitConfirm)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
// GenerateFFI provides a mock function with given fields: ctx, generationRequest
func (_m *Manager) GenerateFFI(ctx context.Context, generationRequest *fftypes.FFIGenerationRequest) (*fftypes.FFI, error) {
ret := _m.Called(ctx, generationRequest)
diff --git a/mocks/metricsmocks/manager.go b/mocks/metricsmocks/manager.go
index 06b48037f4..af86016210 100644
--- a/mocks/metricsmocks/manager.go
+++ b/mocks/metricsmocks/manager.go
@@ -21,6 +21,11 @@ func (_m *Manager) AddTime(id string) {
_m.Called(id)
}
+// BlockchainContractDeployment provides a mock function with given fields:
+func (_m *Manager) BlockchainContractDeployment() {
+ _m.Called()
+}
+
// BlockchainEvent provides a mock function with given fields: location, signature
func (_m *Manager) BlockchainEvent(location string, signature string) {
_m.Called(location, signature)
diff --git a/mocks/syncasyncmocks/bridge.go b/mocks/syncasyncmocks/bridge.go
index 08b12900f1..9430505358 100644
--- a/mocks/syncasyncmocks/bridge.go
+++ b/mocks/syncasyncmocks/bridge.go
@@ -25,6 +25,29 @@ func (_m *Bridge) Init(sysevents system.EventInterface) {
_m.Called(sysevents)
}
+// WaitForDeployOperation provides a mock function with given fields: ctx, id, send
+func (_m *Bridge) WaitForDeployOperation(ctx context.Context, id *fftypes.UUID, send syncasync.SendFunction) (*core.Operation, error) {
+ ret := _m.Called(ctx, id, send)
+
+ var r0 *core.Operation
+ if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, syncasync.SendFunction) *core.Operation); ok {
+ r0 = rf(ctx, id, send)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*core.Operation)
+ }
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, syncasync.SendFunction) error); ok {
+ r1 = rf(ctx, id, send)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
+
// WaitForIdentity provides a mock function with given fields: ctx, id, send
func (_m *Bridge) WaitForIdentity(ctx context.Context, id *fftypes.UUID, send syncasync.SendFunction) (*core.Identity, error) {
ret := _m.Called(ctx, id, send)
diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go
index 26c44cce4e..80642144dd 100644
--- a/pkg/blockchain/plugin.go
+++ b/pkg/blockchain/plugin.go
@@ -64,6 +64,9 @@ type Plugin interface {
// SubmitNetworkAction writes a special "BatchPin" event which signals the plugin to take an action
SubmitNetworkAction(ctx context.Context, nsOpID, signingKey string, action core.NetworkActionType, location *fftypes.JSONAny) error
+ // DeployContract submits a new transaction to deploy a new instance of a smart contract
+ DeployContract(ctx context.Context, nsOpID, signingKey string, definition, contract *fftypes.JSONAny, input []interface{}, options map[string]interface{}) error
+
// InvokeContract submits a new transaction to be executed by custom on-chain logic
InvokeContract(ctx context.Context, nsOpID, signingKey string, location *fftypes.JSONAny, method *fftypes.FFIMethod, input map[string]interface{}, options map[string]interface{}) error
diff --git a/pkg/core/contracts.go b/pkg/core/contracts.go
index 69a6289c9c..c3f2988b03 100644
--- a/pkg/core/contracts.go
+++ b/pkg/core/contracts.go
@@ -42,6 +42,14 @@ type ContractCallRequest struct {
Options map[string]interface{} `ffstruct:"ContractCallRequest" json:"options"`
}
+type ContractDeployRequest struct {
+ Key string `ffstruct:"ContractDeployRequest" json:"key,omitempty"`
+ Input []interface{} `ffstruct:"ContractDeployRequest" json:"input"`
+ Definition *fftypes.JSONAny `ffstruct:"ContractDeployRequest" json:"definition"`
+ Contract *fftypes.JSONAny `ffstruct:"ContractDeployRequest" json:"contract"`
+ Options map[string]interface{} `ffstruct:"ContractDeployRequest" json:"options"`
+}
+
type ContractURLs struct {
OpenAPI string `ffstruct:"ContractURLs" json:"openapi"`
UI string `ffstruct:"ContractURLs" json:"ui"`
diff --git a/pkg/core/event.go b/pkg/core/event.go
index daf82405f3..b96e2d7263 100644
--- a/pkg/core/event.go
+++ b/pkg/core/event.go
@@ -57,6 +57,10 @@ var (
EventTypeBlockchainInvokeOpSucceeded = fftypes.FFEnumValue("eventtype", "blockchain_invoke_op_succeeded")
// EventTypeBlockchainInvokeOpFailed occurs when a blockchain "invoke" request has failed
EventTypeBlockchainInvokeOpFailed = fftypes.FFEnumValue("eventtype", "blockchain_invoke_op_failed")
+ // EventTypeBlockchainContractDeployOpSucceeded occurs when a contract deployment request has succeeded
+ EventTypeBlockchainContractDeployOpSucceeded = fftypes.FFEnumValue("eventtype", "blockchain_contract_deploy_op_succeeded")
+ // EventTypeBlockchainContractDeployOpFailed occurs when a contract deployment request has failed
+ EventTypeBlockchainContractDeployOpFailed = fftypes.FFEnumValue("eventtype", "blockchain_contract_deploy_op_failed")
)
// Event is an activity in the system, delivered reliably to applications, that indicates something has happened in the network
diff --git a/pkg/core/operation.go b/pkg/core/operation.go
index c6824b7d47..589aa19846 100644
--- a/pkg/core/operation.go
+++ b/pkg/core/operation.go
@@ -35,6 +35,8 @@ var (
OpTypeBlockchainPinBatch = fftypes.FFEnumValue("optype", "blockchain_pin_batch")
// OpTypeBlockchainNetworkAction is an administrative action on a multiparty blockchain network
OpTypeBlockchainNetworkAction = fftypes.FFEnumValue("optype", "blockchain_network_action")
+ // OpTypeBlockchainContractDeploy is a smart contract deploy
+ OpTypeBlockchainContractDeploy = fftypes.FFEnumValue("optype", "blockchain_deploy")
// OpTypeBlockchainInvoke is a smart contract invoke
OpTypeBlockchainInvoke = fftypes.FFEnumValue("optype", "blockchain_invoke")
// OpTypeSharedStorageUploadBatch is a shared storage operation to upload broadcast data
diff --git a/pkg/core/transaction.go b/pkg/core/transaction.go
index 7ab5b54a03..d107b0a8a8 100644
--- a/pkg/core/transaction.go
+++ b/pkg/core/transaction.go
@@ -35,6 +35,8 @@ var (
TransactionTypeTokenPool = fftypes.FFEnumValue("txtype", "token_pool")
// TransactionTypeTokenTransfer represents a token transfer
TransactionTypeTokenTransfer = fftypes.FFEnumValue("txtype", "token_transfer")
+ // TransactionTypeContractDeploy is a smart contract deployment
+ TransactionTypeContractDeploy = fftypes.FFEnumValue("txtype", "contract_deploy")
// TransactionTypeContractInvoke is a smart contract invoke
TransactionTypeContractInvoke = fftypes.FFEnumValue("txtype", "contract_invoke")
// TransactionTypeTokenTransfer represents a token approval