Smart Contracts

Vendia Share makes it easy to share data and files among participants in a Uni. But sometimes sharing data alone isn't enough. Smart Contracts allow users to take action on data in a prescribed way, as data changes across a Uni, without having to build or maintain a complex eventing infrastructure.

Smart contracts can be used for various purposes. Examples of smart contract usage include:

  • Integrity Constraints: Smart contracts can be used to enforce restrictions or limitations, such as ensuring that the balance across several related accounts never drops below a minimum threshold or ensuring that two flight segments aren't booked closer together than 45 minutes between the arrival of the first and the departure of the second. Making this calculation a smart contract ensures that the data in the Uni can adhere to policy constraints, regardless of its provenance. Participants don't have to just trust that other participants "got it right" when they updated one or more values over time.

  • Derived (Computed) Values: Smart contracts can also be used to create data values that are derived from other information. For example, a Uni may be used to store sales information, in which case a smart contract can be used to accurately calculate sales tax and the total amount, rather than requiring every node to maintain a redundant implementation of the tax calculation.

  • Third-party System Integration: Because a smart contract can be any code that the parties in the Uni agree is legitimate to use for their shared purposes, it can do things like contact third party systems, retrieve information stored outside the Uni, etc. Smart contracts can also be used to update third-party systems based on the data in the Uni.

When to Use Smart Contracts

Not all data actions need to be captured in a smart contract. The following questions can help determine if a smart contract is indicated:

  • Does the action affect the data in the Uni?: If the goal is to add computed values, enforce Uni-wide constraints, or act on behalf of all participants, then a smart contract is likely to be required.

  • Is the result of the action stored in the Uni?: If a computation's output is used to update or add to the Uni's data, then a smart contract may be appropriate.

Comparison to Ethereum Smart Contracts

If you are more familiar with Ethereum Smart Contracts there are some key differences and similarities between the two that are important to understand.

FeatureEthereumVendia
NamingSmart contracts reside at a specific address on the Ethereum blockchain and are read and executed using its addressSmart contracts are named following Vendia's vrn format
ImmutabilitySmart contracts have their complete bytecode included on the Ethereum blockchainVendia only invokes smart contract resources that are guaranteed by the cloud provider to be immutable. Further, Vendia guarantees the smart contract data is immutable for a given revisionId
Updating/DeprecatingSmart contracts on the Ethereum block chain are forever on the blockchain to be executed. It is common to create smart contracts that route to other smart contracts as a way to update a smart contract while preserving its wallet address and balance.Smart contracts can be updated using the updateVendia_Contract API. For removing access to older revisionIds, see Invoking specific revisionIds.
Programming languageSolidity and Vyper are the most common language choices for Ethereum smart contracts, with other Ethereum specific languages additionally available.Vendia Smart Contracts can be written in any language that is supported by AWS Lambda. For a full list of supported languages go to the AWS Lambda documentation here.
Accessing external dataPossible through oraclesThe backing resource (AWS Lambda function) has access to anything your function has access to (e.g. private Amazon Relational Database Service instance, public API endpoint, etc)

What is in a Vendia Smart Contract?

A Vendia Smart Contract contains the following fields:

field namedescriptionsourcerequired
nameThe name of the smart contract. Must pass the regex: [a-zA-Z0-9-_]40YouYes
descriptionA description of what the smart contract doesYouNo
revisionIdThe revisionId tracks the tuple of the (name, resource.uri, inputQuery, outputMutation) fields. The value is only changed when one of those fields have been updatedVendiaN/A
resource.uriThe backing resource for the smart contract. Currently only AWS Lambda functions are supportedYouYes
resource.cspThe cloud service provider for the backing resourceVendiaN/A
resource.metadataA list of metadata fields from the backing resourceVendiaN/A
inputQueryA stringified graphql query run prior to invoking the backing function that retrieves data from the uniYouNo
outputMutationA stringified graphql mutation run after the backing function completes that updates the world state based on the results of the backing resourceYouYes

How Vendia Smart Contracts Work

On many blockchain platforms, smart contracts have to be executed by all nodes in parallel - a costly and redundant approach. Vendia only requires executing a smart contract once, which also frees developers from having to ensure that the code in the contract is idempotent and replayable. This allows for freedom of language choice: On Vendia, smart contracts can be written in literally any language (though sticking to one of the built-in ones does make things a little simpler). Vendia permits flexible use of non-idempotent calculations, including random number generators, time of date, arbitrary API calls, and more. Not all Unis and participants may elect to support those features in the smart contracts they use, but they're available if desired.

Vendia Share expresses smart contracts as AWS Lambda functions. Importantly, these functions must be versioned. Versioning a Lambda function makes it immutable - not even the owner of the function can change its code or configuration. This immutability allows the function to be executed with cross-participant trust, because the function has the same "meaning" regardless of who its owner might be.

Vendia Share executes a smart contract in several steps:

  1. Create the input payload from world state. When a smart contract is invoked, an invoke payload is generated by combining the results of static invoke arguments with the results from running the inputQuery defined on the Vendia Smart Contract. See The Invoke Payload for more details. A ContractTask record is generated at the time of invocation. This ContractTask record can be referenced by an ID generated as the output of the invocation.

  2. The Lambda function representing the contract is invoked, using the values generated in step one as the arguments.

  3. The result of the function is captured. The ContractTask record created during the invocation of the contract is updated to reflect the result of the contract execution.

The values passed to a function are computed in the same block in which the smart contract invocation is processed. However, since functions can run for up to 14 minutes, transaction processing does not wait for contracts to complete. The results of a contract will be applied asynchronously, once they become available.

What is a Vendia ContractTask?

When a Vendia Smart Contract is executed, a ContractTask record is created. This record is updated during the execution of the Vendia Smart Contract to reflect the status of the contract execution. The record can be queried to keep track of the status of the contract being invoked by using the ID({transaction{_id}}) provided at the time of the contract invocation.

Result of a Contract invocation:

{
  "data": {
    "invokeVendia_Contract": {
      "transaction": {
        "_id": "018878d2-f3b9-9e42-06cc-b7a8dfbeed69"
      }
    }
  }
}

What is in a Vendia ContractTask?

A Vendia Smart Contract Task contains the following fields:

field namedescription
startTimeThe timestamp when the smart contract was invoked
completionTimeThe timestamp when the smart contract invocation was completed
statusThe current status of the smart contract invocation
invokedByThe node that invoked the smart contract
contractIdThe smart contract that was invoked
revisionIdThe revisionId for the smart contract that was invoked
rawInputThe raw input that was passed by the customer during the invocation. This includes the invokeArgs, queryArgs, and the invocationId field values.
transactionIdsA list of transaction IDs generated by the output mutation of a successful smart contract execution
error.errorTypeThe type of error that has occurred
error.errorDetailsThe details of the error that has occurred

The following fields in the Smart Contract Task record are erasable:

  • rawInput
  • error.errorDetails

ContractTask Execution Details

When the ContractTask record is created, the status field is set to INVOKING and the following fields are set to null.

  • startTime
  • completionTime
  • error
  • transactionIds
  • revisionId
{
  "startTime": null,
  "completionTime": null,
  "invokedBy": "N0-us-east-2",
  "rawInput": "{\"queryArgs\": \"{\\\"id\\\": \\\"a-very-real-shipment-id\\\"}\"}",
  "revisionId": null,
  "status": "INVOKING",
  "transactionIds": null,
  "contractId": "vrn:MyTestNode:smart-contract:update-delivery-status",
  "error": null,
  "_id": "0188787f-7ec4-affc-97b3-1f24b7d27812"
}

Once the contract execution has completed, the ContractTask record is updated according to the following possibilities:

  • Rejected: If the smart contract makes the decision not to execute the output mutation, it can include the field x-vendia-status in the return of the smart contract. The value of this field should be set to REJECTED(the only currently supported value). If this key-value pair is set, the status field of the ContractTask record will be updated to REJECTED.

    {"x-vendia-status": "REJECTED"}
    
    • If the above JSON is returned by contract executing function, the ContractTask will be updated to resemble the following:
{
  "startTime": "2023-05-25T22:26:27.535011+00:00",
  "completionTime": "2023-05-25T22:26:27.831082+00:00",
  "invokedBy": "N0-us-east-2",
  "rawInput": "{\"queryArgs\": \"{\\\"id\\\": \\\"a-very-real-shipment-id\\\"}\"}",
  "revisionId": "2617b6c55f4059cd70101cae92e271d5",
  "status": "REJECTED",
  "transactionIds": null,
  "contractId": "vrn:MyTestNode:smart-contract:update-delivery-status",
  "error": null,
  "_id": "01885505-3e43-c699-d4ee-1e58892ceb02"
},
  • If in addition, a field containing the string x-vendia-status-details is present in the return of the smart contract, the error structure will be updated as follows:

    • errorType will be set to REJECTED

    • errorDetails will be set to the value of the x-vendia-status-details field

      {"x-vendia-status": "REJECTED", "x-vendia-status-details": "This contract was rejected because XYZ"}
      
    • If the above JSON is returned by contract executing function, the ContractTask will be updated to resemble the following:

    {
      "startTime": "2023-05-25T22:26:19.165201+00:00",
      "completionTime": "2023-05-25T22:26:19.476396+00:00",
      "invokedBy": "N0-us-east-2",
      "rawInput": "{\"queryArgs\": \"{\\\"id\\\": \\\"a-very-real-shipment-id\\\"}\"}",
      "revisionId": "475d70ffe793319fb682562a5c6d12ce",
      "status": "ERROR",
      "transactionIds": null,
      "contractId": "vrn:MyTestNode:smart-contract:update-delivery-status",
      "error": {
        "errorType": "REJECTED",
        "errorDetails": "This contract was rejected because XYZ"
      },
      "_id": "01885505-1d62-80fc-9741-2ceb2a6caa07"
    },
    
  • Error: A number of different error types are supported by the ContractTask type.

    • Permissions: In the event that a smart contract cannot be accessed due to a lack of permissions, the errorType will be set to PERMISSIONS, and any corresponding details will be accessible via errorDetails.
    {
      "startTime": "2023-05-25T22:26:04.756037+00:00",
      "completionTime": "2023-05-25T22:26:10.991708+00:00",
      "invokedBy": "N0-us-east-2",
      "rawInput": "{\"queryArgs\": \"{\\\"id\\\": \\\"a-very-real-shipment-id\\\"}\"}",
      "revisionId": "feabdda6be2cf5cad3cfc3bfaa017bc8",
      "status": "ERROR",
      "transactionIds": null,
      "contractId": "vrn:MyTestNode:smart-contract:update-delivery-status",
      "error": {
        "errorType": "PERMISSIONS",
        "errorDetails": "Error invoking smart contract. Ensure the Lambda Function arn:aws:lambda:us-west-2:123456789012:function:my-lambda-function:1 has the correct permissions."
      },
      "_id": "01885504-dcb3-43f5-0fc3-760c03e230e4"
    },
    
    • Internal Error: In the event that Vendia encounters an unexpected error while executing the smart contract or output mutation, the errorType will be set to INTERNAL_ERROR.
    {
      "startTime": "2023-06-01T21:04:44.153306+00:00",
      "completionTime": "2023-06-01T21:04:44.520127+00:00",
      "invokedBy": "N0-us-east-2",
      "rawInput": "{\"queryArgs\": \"{\\\"id\\\": \\\"a-very-real-shipment-id\\\"}\"}",
      "revisionId": "330d387b73e4bdb13001b453b21a4866",
      "status": "ERROR",
      "transactionIds": null,
      "contractId": "vrn:MyTestNode:smart-contract:update-delivery-status",
      "error": {
        "errorType": "INTERNAL_ERROR",
        "errorDetails": null
      },
      "_id": "018878c6-f035-d8de-7acb-5dcadf399531"
    },
    
    • Contract: In the event that a smart contract returns a FunctionError(an error is raised in the function code), the errorType will be set to CONTRACT, and any corresponding details will be accessible via errorDetails. A stacktrace will be included in the errorDetails field if applicable.
    {
      "startTime": "2023-06-01T21:04:44.153306+00:00",
      "completionTime": "2023-06-01T21:04:44.520127+00:00",
      "invokedBy": "N0-us-east-2",
      "rawInput": "{\"queryArgs\": \"{\\\"id\\\": \\\"a-very-real-shipment-id\\\"}\"}",
      "revisionId": "330d387b73e4bdb13001b453b21a4866",
      "status": "ERROR",
      "transactionIds": null,
      "contractId": "vrn:MyTestNode:smart-contract:update-delivery-status",
      "error": {
        "errorType": "CONTRACT",
        "errorDetails": "[\"  File \\\"/var/task/index.py\\\", line 3, in handler\\n    return globals()[\\\"permissions_error\\\"](event, context)\\n\"]"
      },
      "_id": "018878c6-f035-d8de-7acb-5dcadf399531"
    },
    
    • Throttle: In the event that the smart contract invocation is throttled, the errorType will be set to THROTTLE, and any corresponding details will be accessible via errorDetails.
    {
      "startTime": "2023-05-25T22:26:04.756037+00:00",
      "completionTime": "2023-05-25T22:26:10.991708+00:00",
      "invokedBy": "N0-us-east-2",
      "rawInput": "{\"queryArgs\": \"{\\\"id\\\": \\\"a-very-real-shipment-id\\\"}\"}",
      "revisionId": "feabdda6be2cf5cad3cfc3bfaa017bc8",
      "status": "ERROR",
      "transactionIds": null,
      "contractId": "vrn:MyTestNode:smart-contract:update-delivery-status",
      "error": {
        "errorType": "THROTTLE",
        "errorDetails": "Throttled invoking smart contract. Ensure the Lambda Function arn:aws:lambda:us-west-2:123456789012:function:my-lambda-function:1 has the correct concurrency settings and limits."
      },
      "_id": "01885504-dcb3-43f5-0fc3-760c03e230e4"
    },
    
    • Result Mutation: If the output mutation fails due to a GraphQL error, the errorType will be set to RESULT_MUTATION, and any corresponding details will be accessible via errorDetails.
    {
      "startTime": "2023-05-25T22:26:19.165201+00:00",
      "completionTime": "2023-05-25T22:26:19.476396+00:00",
      "invokedBy": "N0-us-east-2",
      "rawInput": "{\"queryArgs\": \"{\\\"id\\\": \\\"a-very-real-shipment-id\\\"}\"}",
      "revisionId": "475d70ffe793319fb682562a5c6d12ce",
      "status": "ERROR",
      "transactionIds": null,
      "contractId": "vrn:MyTestNode:smart-contract:update-delivery-status",
      "error": {
        "errorType": "RESULT_MUTATION",
        "errorDetails": "This contract has run into XYZ issue."
      },
      "_id": "01885505-1d62-80fc-9741-2ceb2a6caa07"
    },
    
    • Success: The smart contract execution has been completed successfully. The startTime, completionTime, revisionId, and transactionIds fields should now be populated.
    {
      "startTime": "2023-06-01T21:04:33.433505+00:00",
      "completionTime": "2023-06-01T21:04:33.599285+00:00",
      "invokedBy": "N1-us-west-2",
      "rawInput": "{\"queryArgs\": \"{\\\"id\\\": \\\"a-very-real-shipment-id\\\"}\"}",
      "revisionId": "4e37a8a55a88878603fed5a2dc5ad559",
      "status": "SUCCESS",
      "transactionIds": [
        "018878c6-c900-4319-7ca5-2eb55a4e4485"
      ],
      "contractId": "vrn:MyTestNode:smart-contract:update-delivery-status",
      "error": null,
      "_id": "018878c6-c581-6eba-6eaa-19e73b26ed0b"
    },
    

ContractTask Visibility Permissions

The following permissions will be followed by the ContractTask record:

  • All nodes will have access to the following fields:

    • status
    • contractId
    • revisionId
    • transactionIds
  • The node that invokes the smart contract will have access to the following fields:

    • status
    • contractId
    • revisionId
    • transactionIds
    • error.errorType
  • The node that owns the smart contract resource will have access all of the fields in the ContractTask record

Linking ContractTask Records to Blocks

The mutations generated by a successful smart contract invocation can be found by calling the listVendia_BlockItems API and filtering on the transaction ids stored in the ContractTask record.

query blocksQuery {
  listVendia_BlockItems(filter: {_transactionsItem: {_id: {eq: "123"}}}) {
    Vendia_BlockItems {
      transactions {
        _id
      }
      _id
      blockHash
    }
  }
}

Tracking smart contract latency

The latency from the time a smart contract is invoked to the time the output mutation begins to be applied can be tracked using the following query.

query contractQuery {
  getVendia_ContractTask(id: "01872afa-5348-e31f-0b97-3ae2f49f3f5e") {
    ... on Vendia_ContractInvocation {
      startTime
      completionTime
      transactionIds
    }
  }
  listVendia_BlockItems(
    filter: {_transactionsItem: {_id: {eq: "01872afa-56a3-217c-5957-ff5a7ed7037f"}}}
  ) {
    Vendia_BlockItems {
      blockId
      transactions {
        submissionTime
        _id
      }
      commitTime
    }
  }
}

The above query produces the following output:

{
  "data": {
    "getVendia_ContractTask": {
      "startTime": "2023-03-29T01:27:41.878482+00:00",
      "completionTime": "2023-03-29T01:27:42.059641+00:00",
      "transactionIds": [
        "01872afa-56a3-217c-5957-ff5a7ed7037f"
      ]
    },
    "listVendia_BlockItems": {
      "Vendia_BlockItems": [
        {
          "blockId": "000000000000101",
          "transactions": [
            {
              "submissionTime": "2023-03-29T01:27:41.987254+00:00",
              "_id": "01872afa-56a3-217c-5957-ff5a7ed7037f"
            }
          ],
          "commitTime": "2023-03-29T01:27:42.250060934+00:00"
        }
      ]
    }
  }
}
  • getVendia_ContractTask
    • startTime: When Vendia started to invoke the smart contract
    • completionTime: When Vendia completed invoking the smart contract and submitting the output mutation
  • listVendia_BlockItems
    • transactions → submissionTime: When this transaction was submitted to be applied
    • commitTime: When the transactions in this block started to be applied

Versioning

Smart contracts have two modes of versioning. The first type uses Vendia’s standard object versioning. Whenever any field of a smart contract is updated, the version number will increment by one, and a version update is recorded. Retrieving specific versions of a function can be done buy using the getVendia_Contract API and passing in the version you want.

The second versioning schema happens less frequently, updating of the revisionId. The revisionId tracks any changes to three properties of a smart contract object that change the underlying behavior of what a smart contract does. These fields are inputQuery, outputMutation, and resource.uri. When calling the invokeVendia_Contract API with a specific revisionId parameter, you are guaranteed to be running an immutable grouping of (inputQuery, outputMutation, resource.uri) is the exact properties of the Smart Contract.

Vendia Smart Contract Function Deployment and Permissions

The Lambda function supporting a smart contract is a customer-owned resource. As such it is deployed to an AWS Account outside of Vendia. This allows you to retain complete control over the function configuration, code, versioning, and permissions.

So that Vendia can securely invoke your lambda function you will need to set up a resource policy on your Lambda function. The specific permissions you will need to grant are lambda:GetFunctionConfiguration and lambda:InvokeFunction. The former permissions are used to get the Lambda function's metadata for validating it passes Share's requirements, while the latter is required to invoke the Lambda function itself.

To ensure that Lambda function has not changed between smart contract invocations we do not support $LATEST. This means you will need need to utilize lambda versioning. For each new Lambda version that is created, you need to explicitly re-attach the resource policies that are defined below. AWS does not carry over the the resource policies from $LATEST to your new version. As such, you will also need to ensure that :<version> is appended onto the end of each lambda arn. e.g. arn:aws:lambda:us-east-2:123456789012:function:my-function:1

Determining the Vendia Smart Contract Role

To ensure only your node can invoke your Vendia Smart Contract, a special AWS role is created per-node that is used to retrieve metadata and invoke your Vendia Smart Contract's resource. To find this role, you can use either the UI or the share CLI.

Share CLI

share uni get --uni <name-of-uni>
Example
share uni get --uni loonies-twonies.unis.vendia.net

Current logged in user "user@domain.com".
Getting loonies-twonies.unis.vendia.net info...
┌─────────────────────┐
│   Uni Information   │
└─────────────────────┘
Uni Name:    loonies-twonies.unis.vendia.net
Uni Status:  RUNNING
Node Count:  1
Node Info:
└─ ⬢ NodeOne
   ├─ name: NodeOne
   ├─ status: RUNNING
   └─ resources:
      ├─ graphqlApi
      │  ├─ httpsUrl https://some-url.com/graphql/
      │  ├─ apiKey MY_API_KY
      │  └─ websocketUrl wss://some-url.com/graphql
      ├─ smartContracts
      │  └─ aws_Role arn:aws:iam::123456789012:role/loonies-twonies_NodeOne_0e4e6c4cf9d7ed_SmartContractRole
      ├─ aws_AsyncIngressQueue
      │  ├─ url https://sqs.us-west-2.amazonaws.com/1234567889012/ingressQ_loonies-twonies_NodeOne
      │  └─ name ingressQ_loonies-twonies_NodeOne
      ├─ aws_FileStorage
      │  ├─ arn arn:aws:s3:::loonies-twonies-1-nodeone-some-bucket
      │  └─ name loonies-twonies-1-nodeone-some-bucket
      ├─ aws_BlockNotifications
      │  └─ arn arn:aws:sns:us-west-2:123456789012:loonies-twonies-1-NodeOne-BlockTopicSOME_ID
      ├─ aws_DeadLetterNotifications
      │  └─ arn arn:aws:sns:us-west-2:123456789012:loonies-twonies-1-NodeOne-DeadLetterTopicSOME_ID
      └─ aws_Cognito
         ├─ userPoolId null
         ├─ userPoolClientId null
         └─ identityPoolId null

or more succinctly if you have jq installed:

share uni get --uni loonies-twonies.unis.vendia.net --json | jq '.nodes[] | { "node_name": .name, "smart_contract_role_arn": .resources.smartContracts.aws_Role }'

{
  "node_name": "NodeOne",
  "smart_contract_role_arn": "loonies-twonies_NodeOne_0e4e6c4cf9d7ed_SmartContractRole"
}

Share UI

Select the node where you are going to create the Vendia Smart Contract, and the Smart Contract Role should be visible under the resources section.

Smart Contract Role on the UI

Adding the Permissions

The fastest way to add the required permissions for your AWS Lambda function is via the AWS CLI. The following code block provides an example of the CLI commands necessary.

aws lambda add-permission --region <lambda-function-region> --function-name <your-lambda-resource-arn> --action lambda:InvokeFunction --statement-id AllowVendiaInvokeFunction --principal <smart-contract-role-arn>

aws lambda add-permission --region <lambda-function-region> --function-name <your-lambda-resource-arn> --action lambda:GetFunctionConfiguration --statement-id AllowVendiaGetFunctionConfiguration --principal <smart-contract-role-arn>

For example, if your AWS Lambda function arn is arn:aws:lambda:us-west-2:123456789012:function:my-lambda-function:1 and the Vendia Smart Contract role arn is arn:aws:iam::102930495678:role/loonies-twonie_NodeOne_6f87c1fc2943bf_SmartContractRole, your commands would be:

aws lambda add-permission --region us-west-2 --function-name arn:aws:lambda:us-west-2:123456789012:function:my-lambda-function:1 --action lambda:InvokeFunction --statement-id AllowVendiaInvokeFunction --principal arn:aws:iam::102930495678:role/loonies-twonie_NodeOne_6f87c1fc2943bf_SmartContractRole

aws lambda add-permission --region us-west-2 --function-name arn:aws:lambda:us-west-2:123456789012:function:my-lambda-function:1 --action lambda:GetFunctionConfiguration --statement-id AllowVendiaGetFunctionConfiguration --principal arn:aws:iam::102930495678:role/loonies-twonie_NodeOne_6f87c1fc2943bf_SmartContractRole

Invoking the Vendia Smart Contract

To invoke a Vendia Smart Contract, you use the invokeVendia_Contract API.

field namedescriptionrequired
idThe id for the Vendia Smart Contract you want to invokeYes
revisionIdThe version identifier tracking the current state of the tuple (inputQuery, outputMutation, resource.uri). Can be used to guarantee the specific revision of a Vendia Smart Contract to be invoked.No
input.invocationIdIf invocationId is not supplied, Vendia generates one. This invocationId is passed to the Vendia Smart Contract as part of the input payload. This can be used by to pass through request ids or trace ids through your system.No
input.queryArgsA stringified json that contains the variable map is passed to the inputQuery defined on your Vendia Smart ContractIf the smart contract has an inputQuery defined, the query args passed in the invoke request must be included and must match the query args defined by customers in the inputQuery. If inputQuery is not defined, this field is unused.
input.invokeArgsA stringified json that is passed directly to your AWS Lambda function resourceNo

Invoking specific revisionIds

For Vendia Smart Contracts, the node that created the Smart Contract can invoke any revisionId. All other nodes in the Uni are only able to invoke the latest revisionId on the Smart Contract.

A Vendia Smart Contract Example

Building on the inventory track and trace quickstart, smart contracts can be used to check external systems before marking a shipment as delivered. The "Orders" and "Shipments" data models both have a delivered(boolean) property but instead of directly mutating that state, the delivering party can use a smart contract to create a confirmation step for the recipient before a delivered=True state is written to the world state.

A smart contract can be used to check with off-chain systems before putting the data into the ledger permanently. Before introducing our contract we can take a look at the state of the world:

query Statuses {
  list_WarehouseItems {
    Warehouse {
      city
      code
      companyName
    }
  }
  list_ShipmentItems {
    Shipment {
      created
      delivered
      destinationWarehouse
      lastUpdated
      id
      location
      orderId
      originWarehouse
    }
  }
  list_OrderItems {
    Order {
      delivered
      retailerWarehouseCode
      manufacturerWarehouseCode
      orderId
    }
  }
}

First, we create the smart contract with the below mutation that retrieves the order information for a specific shipment and then updates the status of the delivery using the result of the backing resource. Let's break down how that works!

  • inputQuery defines a query where we retrieve the up-to-date data about a specific shipment.
  • resource.uri points at the lambda function version, in this example arn:aws:lambda:us-west-2:123456789012:function:ContractEnforcement:9, will be passed the result of the inputQuery. The function could be querying a separate backend API to retrieve the order status of the shipment.
  • outputMutation defines a mutation that should be run where the inputs come from the result of the resource function. This mutation updates the world state to mark the shipment's delivery status
mutation createConfirmDeliveryContract {
  addVendia_Contract(
    input: {
      name: "update-delivery-status",
      resource: { uri: "arn:aws:lambda:us-west-2:123456789012:function:ContractEnforcement:9" }, 
      description: "a smart contract that updates the delivery status of a shipment",
      inputQuery: "query shipmentDetails($id: ID!) { getShipment(id: $id) { _id orderId destinationWarehouse }}" 
      outputMutation: "mutation m($id: ID!, $delivered: Boolean, $lastUpdated: String, $orderId: String) { updateShipment(id: $id, input: { delivered: $delivered, lastUpdated: $lastUpdated, orderId: $orderId }, syncMode: ASYNC) { transaction { _id } } }" 
    },
    syncMode: ASYNC
  ) {
  transaction {
    _id
    _owner
    transactionId
    version
    submissionTime
    }
  }
}

Once the function is created, we will want to invoke it! We can do this for a specific shipment by using the invokeVendia_Contract api. In the following example, we are invoking the Vendia Smart Contract we create above in a node named "MyTestNode", and running it on the shipment id a-very-real-shipment-id.

mutation invokeSmartContract {
  invokeVendia_Contract(
    input: {
      id: "vrn:MyTestNode:smart-contract:update-delivery-status",
      input: {
        queryArgs: "{\"id\": \"a-very-real-shipment-id\"}",
      }
    }
  ) {
    result {
      _id
      _owner
      submission_time
      transactionId
    }
  }
}

When invoked, the backing Lambda function will receive a JSON payload containing the result of running your inputQuery, any static arguments passed in the invokeArgs field, and an invocationId. For our example, the inputQuery returns the details of a specific shipment.

Example JSON that is sent to the backing Lambda function
{
  "queryResults": {
    "shipmentDetails": {
      "_id": "a-very-real-shipment-id",
      "orderId": "order782",
      "destinationWarehouse": "SEA-52"
    }
  },
  "invokeArgs": {},
  "invocationId": "01FPES7CKM6EEEW2F8B155K0TK"
}

This is passed to the backing Lambda function, where the business logic begins to run. Below, we are taking the incoming shipment details stored in the Uni, reaching out to an external API, and returning the status of the delivery.

Example AWS Lambda function logic
import json
from datetime import datetime, timezone

def _get_status_of_delivery(order_id: str, warehouse_id: str) -> bool:
  """Get the delivery status of an order"""

  # Here, we can reach out to a backend database or API, get the delivery status, and return the result
  return True

def lambda_handler(event, context):
    # printing out the event is useful for development, but you may not want to
    # do this for customer data
    print(json.dumps(event, sort_keys=True))
    # read the incoming arguments to get information about the order
    shipment_details = event["queryResults"]["shipmentDetails"]

    warehouse_id = shipment_details.get("_id")
    order_id = shipment_details.get('orderId')

    delivery_received = _get_status_of_delivery(warehouse_id, order_id)

    return {
        "id": warehouse_id,
        "delivered": delivery_received,
        "lastUpdated": datetime.now(timezone=timezone.utc).isoformat(),
        "orderId": order_id,
    }

Once the function returns, the response of the Lambda function is passed in as is to the outputMutation mutation as variables (see Graphql Variable definitions for more on how variables work with GraphQL mutations).

The call to the smart contract and the mutation result will be saved to the ledger, making it easy to resolve any future disputes and audit the usage of the smart contract. Supporting Lambda execution makes it easy to integrate any external system into your Uni's consensus process and leave a trail of decisions auditable by any node.

Smart Contract Reference

Schema

For reference, here is the full JSON schema for a contract expression. This can also be reviewed in GraphQL format from your node's GraphQL Explorer.

"Contract": {
  "description": "Smart Contracts",
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "name": {
        "type": "string",
        "pattern": "[a-zA-Z0-9\\-_\\.]+"
      },
      "description": {
        "type": "string",
        "maxLength": 256
      },
      "revisionId": {
        "type": "string",
        "readOnly": true
      },
      "resource": {
        "type": "object",
        "properties": {
          "uri": {
            "type": "string"
          },
          "csp": {
            "type": "string",
            "enum": [
              "aws"
            ],
            "readOnly": true
          },
          "metadata": {
            "type": "array",
            "readOnly": true,
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "value": {
                  "type": "string"
                }
              }
            }
          }
        },
        "required": [
          "uri"
        ]
      },
      "inputQuery": {
        "type": "string"
      },
      "outputMutation": {
        "type": "string"
      }
    },
    "required": [
      "name",
      "resource",
      "outputMutation"
    ]
  },
  "uniqueItems": true
}

Vendia Smart Contract APIs

Mutations

Add
addVendia_Contract(
  input: {
    name: String!,
    description: String,
    inputQuery: String,
    outputMutation: String!,
    resource: {uri: String!}
  },
  aclInput: Vendia_Acls_Input_,
  syncMode: Vendia_SyncMode
) {
  transaction {
    _id
    _owner
    submissionTime
    transactionId
    version
  }
 result {
    _id
    _owner     
    name
    description
    inputQuery
    outputMutation
    resource
 }
}
Update
updateVendia_Contract(id: ID!
  input: {
    description: String,
    inputQuery: String,
    outputMutation: String,
    resource: {uri: String}
  },
  aclInput: Vendia_Acls_Input_,
  syncMode: Vendia_SyncMode
) {
  transaction {
    _id
    _owner
    submissionTime
    transactionId
    version
  }
 result {
    _id
    _owner     
    name
    description
    inputQuery
    outputMutation
    resource
 }
}
Remove
removeVendia_Contract(id: ID!
  condition: Vendia_Contract_ConditionInput_,
  syncMode: Vendia_SyncMode
) {
  transaction {
    _id
    _owner
    submissionTime
    transactionId
    version
  }
}
Invoke
invokeVendia_Contract(id: ID!
  revisionId: String,
  input: {
    invocationId: String,
    queryArgs: String,
    invokeArgs: String,
    syncMode: Vendia_SyncMode
  },

) {
  result {
    _id
    _owner
    submissionTime
    transactionId
    version
  }
  error
}

Queries

Get
getVendia_Contract(id: ID!, version: int) {
  Vendia_Contract_PartialUnion
}
List Contracts
listVendia_ContractItems(filter: Vendia_Contract_FilterInput_, limit: int, nextToken: String) {
  [Vendia_Contract_PartialUnion]
  nextToken
}
List Contract Versions
listVendia_ContractVersions(id: ID!, filter: Vendia_Contract_FilterInput_, limit: int, nextToken: String) {
  Vendia_Version
  nextToken
}

Limits

fieldlimit
Smart Contract name40 characters
Resource timeout14 minutes
Number of queries in the inputQuery field10
Number of mutation in the outputMutation field10
Show table of contents
Edit this page