An API facilitating a distributed heterogeneous pairwise matchmaking service utilising Digital Catapult's Sequence (SQNC) ledger-based solution
Use a .env
at root of the repository to set values for the environment variables defined in .env
file.
variable | required | default | description |
---|---|---|---|
PORT | N | 3000 |
The port for the API to listen on |
LOG_LEVEL | N | debug |
Logging level. Valid values are [trace , debug , info , warn , error , fatal ] |
ENVIRONMENT_VAR | N | example |
An environment specific variable |
DB_PORT | N | 5432 |
The port for the database |
DB_HOST | Y | - | The database hostname / host |
DB_NAME | N | sqnc-matchmaker-api |
The database name |
DB_USERNAME | Y | - | The database username |
DB_PASSWORD | Y | - | The database password |
IDENTITY_SERVICE_HOST | Y | - | Hostname of the sqnc-identity-service |
IDENTITY_SERVICE_PORT | N | 3000 |
Port of the sqnc-identity-service |
NODE_HOST | Y | - | The hostname of the sqnc-node the API should connect to |
NODE_PORT | N | 9944 |
The port of the sqnc-node the API should connect to |
LOG_LEVEL | N | info |
Logging level. Valid values are [trace , debug , info , warn , error , fatal ] |
USER_URI | Y | - | The Substrate URI representing the private key to use when making sqnc-node transactions |
IPFS_HOST | Y | - | Hostname of the IPFS node to use for metadata storage |
IPFS_PORT | N | 5001 |
Port of the IPFS node to use for metadata storage |
WATCHER_POLL_PERIOD_MS | N | 10000 |
Number of ms between polling of service state |
WATCHER_TIMEOUT_MS | N | 2000 |
Timeout period in ms for service state |
API_SWAGGER_BG_COLOR | N | #fafafa |
CSS _color* val for UI bg ( try: e4f2f3 , e7f6e6 or f8dddd ) |
API_SWAGGER_TITLE | N | IdentityAPI |
String used to customise the title of the html page |
API_SWAGGER_HEADING | N | IdentityService |
String used to customise the H2 heading |
IDP_CLIENT_ID | Y | - | OAuth2 client-id to use when validating authentication headers |
IDP_PUBLIC_URL_PREFIX | Y | - | URL prefix to apply to access the IDP endpoints from the public internet |
IDP_INTERNAL_URL_PREFIX | Y | - | URL prefix to apply to access the IDP endpoints from within the Sequence deployment's network |
IDP_TOKEN_PATH | N | /token |
Path to append to the appropriate prefix to determine the OAuth2 token endpoint |
IDP_JWKS_PATH | N | /certs |
Path to append to the appropriate prefix to determine the OAuth2 JWKS endpoint |
# start dependencies
docker compose up -d
# install packages
npm i
# run migrations
npm run db:migrate
# put process flows on-chain
npm run flows
# start service in dev mode. In order to start in full - npm start"
npm run dev
View OpenAPI documentation for all routes with Swagger:
localhost:3000/swagger/
before performing any database interactions like clean/migrate make sure you have database running e.g. docker-compose up -d or any local instance if not using docker
# running migrations
npm run db:migrate
# creating new migration
## install npx globally
npm i -g knex
## make new migration with some prefixes
npx knex migrate:make --knexfile src/lib/db/knexfile.ts attachment-table
Unit tests are executed by calling:
npm run test:unit
Integration tests require the test dependency services be brought up using docker:
# start dependencies
docker compose -f ./docker-compose-test.yml up -d
# install packages
npm ci
# run migrations
npm run db:migrate
# put process flows on-chain
npm run flows
Integration tests are then executed by calling:
npm run test
To ensure integrity of data within transactions (and therefore on chain), it's possible to define custom processes that validate transactions. More info.
Process flows covering this API's transactions are in processFlows.json
. The file is an array of process flows that can be supplied to the sqnc-process-management
CLI for creating processes on chain:
npm run flows
sqnc-matchmaker-api
provides a RESTful OpenAPI-based interface for third parties and front-ends to interact with the Sequence
(SQNC) system. The design prioritises:
- RESTful design principles:
- all endpoints describing discrete operations on path derived entities.
- use of HTTP verbs to describe whether state is modified, whether the action is idempotent etc.
- HTTP response codes indicating the correct status of the request.
- HTTP response bodies including the details of a query response or details about the entity being created/modified.
- Simplicity of structure. The API should be easily understood by a third party and traversable.
- Simplicity of usage:
- all APIs that take request bodies taking a JSON structured request with the exception of attachment upload (which is idiomatically represented as a multipart form).
- all APIs which return a body returning a JSON structured response (again with the exception of attachments.
- Abstraction of the underlying DLT components. This means no token Ids, no block numbers etc.
- Conflict free identifiers. All identifiers must be conflict free as updates can come from third party organisations.
The API is authenticated and should be accessed with an OAuth2 JWT Bearer token obtained following the OAuth2 client-credentials flow against the deployment's identity-provider.
These are the top level physical concepts in the system. They are the top level RESTful path segments. Note that different states of an entity will NOT be represented as different top level entities.
v1/demandA
v1/demandB
v1/match2
Note the meaning in the API of demandA
and demandB
are abstract and use-case dependent. For example in the case of a logistics matching service where one provider has an order
to be moved and another has some capacity
to move orders one might represent an order
as a demandA
and a capacity
as a demandB
. Interpretation of these labels is entirely by convention.
Additionally, there is the attachment
entity which returns an id
to be used when preparing entity updates to attach files.
Entity queries allow the API user to list those entities (including a query) and to get a specific entity. For demandA
for example:
GET /v1/demandA
- get all demandAsGET /v1/demandA/{demandAId}
- get a demandA by ID
Allows the creation of an initial local state for an entity. Note this is essentially just to establish an internal identifier for the entity and the state is not shared across the blockchain network at this point.
POST /v1/demandB
Allows different kind of updates to be prepared and applied to an entity. For example, a demandB
or match2
must be submitted via a creation
action.
-
POST /v1/demandB/{demandBId}/creation
- create a creationcreation
transaction and send it to the blockchain. -
GET /v1/demandB/{demandBId}/creation
- list a demandB'screation
transactions and their status. -
GET /v1/demandB/{demandBId}/creation/{creationId}
- get the details of a demandBcreation
transaction. -
POST/v1/match2/{match2Id}/cancellation
- submits cancellation request and creates a transaction -
GET /v1/match2/{match2Id}/cancellation
- retrieves all cancellation transactions for specificmatch2Id
-
GET /v1/match2/{match2Id}/cancellation/{cancellationId}
- retrieves a specific cancellation transaction
The last top level entity attachment
, which accepts a multipart/form-data
payload for uploading a file or application/json
for uploading JSON as a file. This will return an id
that can then be used when preparing entity updates to attach files.
POST /v1/attachment
- upload a file.GET /v1/attachment
- list attachments.GET /v1/attachment/{attachmentId}
- download an attachment.
Run docker compose -f docker-compose-3-persona.yml up -d
to start the required dependencies to fully demo sqnc-matchmaker-api
.
The demo involves three personas: MemberA
, MemberB
and an Optimiser
. Each persona has a set of sqnc
services:
- sqnc-matchmaker-api (+ PostgreSQL)
- sqnc-identity-service (+ PostgreSQL)
- sqnc-node
There is also a single ipfs
node for file storage and keycloak
instance used as the identity provider with a realm (member-a
, member-b
, optimiser
) configured for each persona.
Container names are prefixed with the persona e.g. member-a-node
. Services are networked so that only the sqnc-node
instances communicate cross-persona. Each persona uses a substrate
well-known identity for their sqnc-node
:
"MemberA": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", // alice
"MemberB": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", // bob
"Optimiser": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y", // charlie
The docker compose
automatically adds process flows using MemberA
. Process flows validate transactions that affect the chain.
To generate an authentication token for a given persona we will need to perform the client credentials flow against the correct keycloak
realm. If using the swagger
interface for each API this can be done by clicking Authorize
and passing the client_id
sequence
and the client_secret
secret
. All other API calls through the swagger will now pass with the correct authorization header. If you would prefer to interact with the API from the command line you can generate an authentication token, for example with MemberA, with:
curl -X POST \
'http://localhost:3080/realms/member-a/protocol/openid-connect/token' \
-H 'content-type: application/x-www-form-urlencoded' \
-d grant_type=client_credentials \
-d client_id=sequence \
-d client_secret=secret
This will return a JSON response with the property access_token
containing the JWT access token which will look something like eyJhbGci...iPeDl3Fg
. API calls can then be conducted by passing this as a bearer token in an authorization
header. For curl this is done with an argument like -H 'authorization: bearer eyJhbGci...iPeDl3Fg'
.
Before transacting, aliases (a human-friendly names) can be set for the pre-configured node addresses using each persona's sqnc-identity-service
. The value for alias doesn't matter, it just needs some value e.g. self
. For example, to set the self address for MemberA
, you can either use the identity service swagger or run (remembering to put in a valid authorisation token):
curl -X 'PUT' \
'http://localhost:8001/v1/members/5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'authorization: bearer eyJhbGci...iPeDl3Fg' \
-d '{
"alias": "self"
}'
Each persona's identity service:
By default, if no alias is set, the alias is the same as the node address.
The environment is now ready to run through a demo scenario using each persona's matchmaker APIs:
Note the meaning in the API of demandA
and demandB
are abstract and use-case dependent. For example in the case of a logistics matching service where one provider has an order
to be moved and another has some capacity
to move orders one might represent an order
as a demandA
and a capacity
as a demandB
.
MemberA
wants to create ademandA
, which includes a parameters file that details the parameters of the available demandA they have. The parameters file will be used byOptimiser
when matchingdemandA
with ademandB
. FirstMemberA
must upload this parameters file to their local database withPOST /v1/attachment
.- They use the returned
id
forparametersAttachmentId
in the request body toPOST /v1/demandA
. At this point, thedemandA
only exists in theMemberA
database. - When
MemberA
is ready for thedemandA
to exist on chain theyPOST /v1/demandA/{demandAId}/creation
. - Putting something on chain creates a local
transaction
database entry which records the status of block finalisation. Every route that puts something on chain returns a transactionid
. These routes also have an accompanyingGET
route that returns all of the transactions of that transaction type e.g.GET /v1/demandA/{demandAId}/creation
returns the details of alldemandA
creation transactions. The transactionid
can be supplied toGET /v1/demandA/{demandAId}/creation/{creationId}
to get that specific transaction. AlternativelyGET /v1/transaction
can be used to get all transactions of any type. - Once the block has finalised, The indexers running on
MemberB
andOptimiser
'ssqnc-matchmaker-api
will process the block containing the newdemandA
and update their local databases (assuming theirsqnc-node
instance is running and connected). They will be able to see the newdemandA
withGET /v1/demandA
. MemberB
creates ademandB
) in a similar manner to creating ademandA
. It includes a parameters file to describe the parameters of theirdemandB
.- When
MemberB
is ready for thedemandB
to exist on chain theyPOST /v1/demandB/{id}/creation
. Optimiser
can now create amatch2
that matches a singledemandA
with a singledemandB
. They supply their localid
fordemandA
anddemandB
.- When
Optimiser
is ready for thematch2
to exist on chain theyPOST /v1/match2/{id}/proposal
. - Either
MemberA
orMemberB
can accept thematch2
withPOST /v1/match2/{id}/accept
. It doesn't matter which member accepts first. - Once the second member accepts, we have a successful match! The
match2
state changes toacceptedFinal
anddemandA
+demandB
state moves toallocated
. These demands can no longer be used in a newmatch2
.
To clear chain and database state, delete the volumes e.g. docker compose -f docker-compose-3-persona.yml down -v
.
The previous scenario covers a 'happy path' where every member accepts each step without issue. There are also routes for communicating when something has gone wrong.
At any time a demandA
or demandB
can be commented on by any member by POST
ing a single attachment to POST /v1/demandA/{id}/comment
or POST /v1/demandB/{id}/comment
. The attachment is a file that informs the owner of the demand about anything (e.g. an issue, correction) related to the demand. A demand can be commented on multiple times and it does not change the demand's state.
At any time after a match2
is proposed
, and before it reaches acceptedFinal
state, any of its members can reject the match2
using POST /v1/match2/{id}/rejection
. Once a match2
is rejected, it is permanently closed.
At any time after a match2
is in acceptedFinal
state either memberA
or memberB
can cancel the match2
using POST /v1/match2/{id}/cancellation
. Once a match2
is cancelled, it is permanently closed. However, optimiser won't be able to cancel a match2