Repository: hyperledger/firefly
Pull request #1095 (2022-11-07 22:14:56)

Add endpoint for deploying contracts
Signed-off-by: Nicko Guyer <nicko.guyer@kaleido.io>


Patch:
diff --git a/docs/reference/types/event.md b/docs/reference/types/event.md
index 2e5c20b2c..6fbc38dbd 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`:<br/>`"transaction_submitted"`<br/>`"message_confirmed"`<br/>`"message_rejected"`<br/>`"datatype_confirmed"`<br/>`"identity_confirmed"`<br/>`"identity_updated"`<br/>`"token_pool_confirmed"`<br/>`"token_pool_op_failed"`<br/>`"token_transfer_confirmed"`<br/>`"token_transfer_op_failed"`<br/>`"token_approval_confirmed"`<br/>`"token_approval_op_failed"`<br/>`"contract_interface_confirmed"`<br/>`"contract_api_confirmed"`<br/>`"blockchain_event_received"`<br/>`"blockchain_invoke_op_succeeded"`<br/>`"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`:<br/>`"transaction_submitted"`<br/>`"message_confirmed"`<br/>`"message_rejected"`<br/>`"datatype_confirmed"`<br/>`"identity_confirmed"`<br/>`"identity_updated"`<br/>`"token_pool_confirmed"`<br/>`"token_pool_op_failed"`<br/>`"token_transfer_confirmed"`<br/>`"token_transfer_op_failed"`<br/>`"token_approval_confirmed"`<br/>`"token_approval_op_failed"`<br/>`"contract_interface_confirmed"`<br/>`"contract_api_confirmed"`<br/>`"blockchain_event_received"`<br/>`"blockchain_invoke_op_succeeded"`<br/>`"blockchain_invoke_op_failed"`<br/>`"blockchain_contract_deploy_op_succeeded"`<br/>`"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 cfd55a5ce..3e1defe59 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`:<br/>`"definition"`<br/>`"broadcast"`<br/>`"private"`<br/>`"groupinit"`<br/>`"transfer_broadcast"`<br/>`"transfer_private"` |
-| `txtype` | The type of transaction used to order/deliver this message | `FFEnum`:<br/>`"none"`<br/>`"unpinned"`<br/>`"batch_pin"`<br/>`"network_action"`<br/>`"token_pool"`<br/>`"token_transfer"`<br/>`"contract_invoke"`<br/>`"token_approval"`<br/>`"data_publish"` |
+| `txtype` | The type of transaction used to order/deliver this message | `FFEnum`:<br/>`"none"`<br/>`"unpinned"`<br/>`"batch_pin"`<br/>`"network_action"`<br/>`"token_pool"`<br/>`"token_transfer"`<br/>`"contract_deploy"`<br/>`"contract_invoke"`<br/>`"token_approval"`<br/>`"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 c182b7c7a..d9dc7d54e 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`:<br/>`"blockchain_pin_batch"`<br/>`"blockchain_network_action"`<br/>`"blockchain_invoke"`<br/>`"sharedstorage_upload_batch"`<br/>`"sharedstorage_upload_blob"`<br/>`"sharedstorage_upload_value"`<br/>`"sharedstorage_download_batch"`<br/>`"sharedstorage_download_blob"`<br/>`"dataexchange_send_batch"`<br/>`"dataexchange_send_blob"`<br/>`"token_create_pool"`<br/>`"token_activate_pool"`<br/>`"token_transfer"`<br/>`"token_approval"` |
+| `type` | The type of the operation | `FFEnum`:<br/>`"blockchain_pin_batch"`<br/>`"blockchain_network_action"`<br/>`"blockchain_deploy"`<br/>`"blockchain_invoke"`<br/>`"sharedstorage_upload_batch"`<br/>`"sharedstorage_upload_blob"`<br/>`"sharedstorage_upload_value"`<br/>`"sharedstorage_download_batch"`<br/>`"sharedstorage_download_blob"`<br/>`"dataexchange_send_batch"`<br/>`"dataexchange_send_blob"`<br/>`"token_create_pool"`<br/>`"token_activate_pool"`<br/>`"token_transfer"`<br/>`"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 d7a20531b..87b931491 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`:<br/>`"none"`<br/>`"unpinned"`<br/>`"batch_pin"`<br/>`"network_action"`<br/>`"token_pool"`<br/>`"token_transfer"`<br/>`"contract_invoke"`<br/>`"token_approval"`<br/>`"data_publish"` |
+| `type` | The type of the FireFly transaction | `FFEnum`:<br/>`"none"`<br/>`"unpinned"`<br/>`"batch_pin"`<br/>`"network_action"`<br/>`"token_pool"`<br/>`"token_transfer"`<br/>`"contract_deploy"`<br/>`"contract_invoke"`<br/>`"token_approval"`<br/>`"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 a82c1428b..d8ca72764 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 000000000..d2d101345
--- /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/routes.go b/internal/apiserver/routes.go
index 30514d745..3a43852b6 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/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go
index 95ec90159..5ccf8716d 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/fabric/fabric.go b/internal/blockchain/fabric/fabric.go
index 1c3527d6f..d18478394 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/contracts/manager.go b/internal/contracts/manager.go
index 42288c977..9207fefe4 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/operations.go b/internal/contracts/operations.go
index 889860c99..25823586a 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/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go
index 5ba86caf8..dea99d3da 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 af1d6b79b..4597b015f 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 903b43453..6895f2182 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 ba5691087..0ac1162bf 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 a7bc3908d..c559d074b 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/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go
index a1c5a5e90..e143c2dd0 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 0b0125bbb..bd38194a5 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 06b48037f..af8601621 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 08b12900f..943050535 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 26c44cce4..80642144d 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 69a6289c9..c3f2988b0 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 daf82405f..b96e2d726 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 c6824b7d4..589aa1984 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 7ab5b54a0..d107b0a8a 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


Test patch:
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 000000000..85ec40835
--- /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/swagger_generate_test.go b/internal/apiserver/swagger_generate_test.go
index ca69b07c2..a313a2ab1 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_test.go b/internal/blockchain/ethereum/ethereum_test.go
index 0369b0585..d7e265562 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_test.go b/internal/blockchain/fabric/fabric_test.go
index e28ab9717..3e63d6122 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_test.go b/internal/contracts/manager_test.go
index cab669030..ec844ca17 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_test.go b/internal/contracts/operations_test.go
index d37932d0b..5972dd0ea 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/syncasync/sync_async_bridge_test.go b/internal/syncasync/sync_async_bridge_test.go
index c00f11886..b7cb0d515 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)