This folder contains an API written in Java Spring Boot. This API provides read-only endpoints for querying the details of a "Daml wallet", such as account details and balances. The "wallet" refers to assets held by a Daml party, modeled using Daml Finance. It is not restricted to any particular type of asset. The only requirement is that the assets implement the Daml Finance interfaces, with the following caveats:
- The settlement
Batch
IDs should be unique for each set of settlementrequestors
. - The settlement
Instruction
s should use a contract key (which is of theInstructionKey
type).
The API provides read-only endpoints (see below for more detail). It is not strictly a RESTful API, but it uses HTTP and requests/responses are in JSON format. The requests and responses are encoded using the Daml-lf JSON encoding, which is the same encoding used by the Daml JSON API. The API provides a significantly faster way to query wallet information than would otherwise be possible using only the Daml gRPC or JSON APIs.
The Spring Boot API does not send requests directly to the ledger API. Instead, it reads from a PostgreSQL database. The database is populated using Scribe, an application provided by the Participant Query Store (PQS) product. Please refer to the PQS documentation for more information. Scribe continuously streams events from the ledger and writes them to the database.
A client-side library, written in Typescript, is provided which can call the API from the browser or backend application.
This is available under typescript-client
directory. This provides a strongly-typed interface for interacting with
the API.
The type definitions of API requests and responses are written as Daml datatypes under daml/types
. Note - this is just
an internal implementation detail of the project, and this Daml code is not used directly. It is only used as a single
source of truth for the API request/response schemas. As part of the build, the
Codegen is used to generate Java code for the API and Typescript code for
the client. The types are fully compatible with Daml Finance as Daml Finance types/interface definitions are used in
the Daml code.
In the context of Canton, each participant needs to host their own instance of the Wallet Views database and API. There is a one-to-one relationship between the database and the participant node, as it acts as an indexed view of the state of the Daml ledger (visible to the participant) and its transaction history.
In the future, the topology may be changed to use the Daml 3.0 application architecture: Scribe would read from a separate participant using a party that is an observer of the wallet users' contracts. The wallet users could still use their own participant for submitting transactions, thereby maintaining a greater level of control over their assets, while delegating the operation of PQS and API to a service provider.
The API expects a ledger API user access token to be supplied with every request, even if the underlying participant does not use authentication. The token must be provided through the HTTP header:
Authorization: Bearer your-token
The API does not support party-specific tokens. Internally, the API will use this token to get the user rights of the
user from the participant node's UserManagementService. If the request is rejected by the participant with the gRPC
status UNAUTHENTICATED
or the user does not exist on the participant, then the caller will receive HTTP status 401
Unauthorized.
Before reading this section you should have a working knowledge of the fundamentals of the Daml Finance library (
accounts, holdings, settlement and instruments). Any request fields marked as optional below must still be included in
the JSON request body but with value set to null
(as opposed to not including the field at all). This is a known issue
due to the usage of the gson
library for decoding the Daml data types. The necessary conversions from JSON to Daml
types are not natively supported in the Daml java libraries at the time of writing (refer to this
issue).
The following endpoints are provided the API:
- URL:
/wallet-views/v1/accounts
- Method:
POST
- Content-Type:
application/json
- Content:
{
"custodian": "Acme::abc123...", // Only returns accounts which use this custodian - optional
"owner": "Alice::abc123..." // Only returns accounts owned by this party - optional
"id": { // Only returns accounts with this ID - optional
"unpack": "1"
}
}
Note: at least one of custodian or owner is required.
- actAs or readAs permissions for the account owner; or
- actAs or readAs permissions for the account custodian if provided.
- Content-Type:
application/json
- Content:
{
"accounts": [
{
"cid": "abc123...", // Contract ID of the Daml Finance Account
"view": { // Interface view of the Daml Finance Account
"custodian": "Acme::abc123...",
"owner": "Alice::abc123...",
"id": {
"unpack": "1"
},
"description": "...",
"holdingFactoryCid": "...",
"controllers": {
"outgoing": {
"map": [
["Alice::abc123...", {}],
["Bob::abc123...", {}]
]
},
"incoming": {
"map": [
["Charlie::abc123...", {}],
]
}
}
}
}
]
}
Note: currently the API only supports returning active Account
s (no archived Account
s will be returned).
This endpoint lists account OpenOffer
contracts as defined in the account-onboarding
folder at the base of this
repository.
- URL:
/wallet-views/v1/account-open-offers
- Method:
POST
- Content-Type:
application/json
- Content:
{} // No filters are currently implemented
- N/A (only returns data which is visible to parties that the user has readAs or actAs rights for).
- Content-Type:
application/json
- Content:
{
"accountOpenOffers": [ // Zero or more open offers
{
"cid": "abc123...", // Contract ID of OpenOffer
"view": { // Interface view of the OpenOffer
"custodian": "Acme::abc123...",
"ownerIncomingControlled": true,
"ownerOutgoingControlled": false,
"additionalControllers": {
"outgoing": {
"map": [
["Alice::abc123...", {}],
["Bob::abc123...", {}]
]
},
"incoming": {
"map": [
["Charlie::abc123...", {}],
]
}
},
"permittedOwners": { // Optional
"map": [
["David::abc123...", {}],
]
},
"accountFactoryCid": "abc123...",
"holdingFactoryCid": "abc123...",
"description": "..."
},
"create": {
// When the offer was created
"offset": "...",
"effectiveTime": "2023-01-01T04:30:23.123456Z"
}
}
]
}
- URL:
/wallet-views/v1/balance
- Method:
POST
- Content-Type:
application/json
- Content:
{
"account": { // Returns balances for this account
"owner": "Alice::abc123...",
"custodian": "Custodian::abc123...",
"id": {
"unpack": "1"
}
}
}
- actAs or readAs permissions for the account owner; or
- actAs or readAs permissions for the account custodian.
- Content-Type:
application/json
- Content:
{
"balances": [
{
"account": {
"owner": "Alice::abc123...",
"custodian": "Acme::abc123...",
"id": {
"unpack": "1"
}
},
"instrument": {
"depository": "Depository::abc123...",
"issuer": "Issuer1::abc123...",
"id": {
"unpack": "Coin1"
},
"version": "1"
},
"unlocked": "999.0", // Sum of holdings which are locked
"locked": "1.0" // Sum of holdings which are unlocked
},
{
"account": {
"owner": "Alice::abc123...",
"custodian": "Acme::abc123...",
"id": {
"unpack": "1"
}
},
"instrument": {
"depository": "Depository::abc123...",
"issuer": "Issuer2::abc123...",
"id": {
"unpack": "Coin2"
},
"version": "1"
},
"unlocked": "0.0", // Sum of holdings which are locked
"locked": "1.0" // Sum of holdings which are unlocked
}
]
}
- URL:
/wallet-views/v1/settlements
- Method:
POST
- Content-Type:
application/json
- Content:
{
"batchId": { // Optional. If provided, only returns settlements with this batch ID
"unpack": "batch1"
},
"before": "0000...a", // Optional. If provided, only returns transactions that occurred before this (absolute) ledger
// offset. If not provided, transactions are returned starting from the most recent.
"limit": 20 // Optional. If provided, it will only return a maximum of this many transactions. If not provided, the API
// will only return up to `walletviews.max-transactions-response-size` (application property)
}
- N/A (only returns data which is visible to parties that the user has readAs or actAs rights for).
- Content-Type:
application/json
- Content:
{
"settlements": [ // Zero or more Daml Finance `Batch` settlements
{
"execution": { // Ledger offset and ledger time at which the `Batch` was settled - optional
"offset": "...",
"effectiveTime": "1970-01-01T00:00:00Z"
},
"batchCid": "...", // Optional contract ID of the Batch contract (if the contract is visible to the caller)
"batchId": {
"unpack": "batch1"
},
"contextId": {
"unpack": "context1" // Optional context ID of the Batch contract (if the contract is visible to the caller)
},
"description": "...", // Optional description of the Batch contract (if the contract is visible to the caller)
"witness": { // Ledger offset and ledger time at which the caller was first informed of the settlement
"offset": "...",
"effectiveTime": "1970-01-01T00:00:00Z"
},
"requestors": {
"map": [
[
"Alice::abc123...",
{}
]
]
},
"settlers": {
"map": [
[
"Bob::abc123...",
{}
]
]
},
"steps": [ // List of 1 or more steps in the Batch (only those for which the caller can see the Instruction contract)
{
"instructionId": {
"unpack": "0"
},
"instructionCid": "...", // Contract ID of the most recently active instruction contract for this instruction ID in the Batch
"allocation": {
// One of:
"tag": "Unallocated",
"value": {}
// ... or ...
"tag": "Pledge",
"value": "abc123...", // Contract Id of pledged holding
// ... or ...
"tag": "CreditReceiver",
"value": {},
// ... or ...
"tag": "SettleOffledger",
"value": {},
// ... or ...
"tag": "PassThroughFrom",
"value": {
"_1": { // AccountKey
"owner": "Alice::abc123...",
"custodian": "Acme::abc123...",
"id": {
"unpack": "1"
}
},
"_2": { // InstructionKey
"requestors": {
"map": [
[
"Alice::abc123...",
{}
]
]
},
"batchId": {
"unpack": "batch1"
},
"id": {
"unpack": "1"
}
}
}
},
"approval": {
// One of:
"tag": "Unapproved",
"value": {}
// ... or ...
"tag": "TakeDelivery",
"value": { // AccountKey
"owner": "Alice::abc123...",
"custodian": "Acme::abc123...",
"id": {
"unpack": "1"
}
},
// ... or ...
"tag": "DebitSender",
"value": {},
// ... or ...
"tag": "SettleOffledgerAcknowledge",
"value": {},
// ... or ...
"tag": "PassThroughTo",
"value": {
"_1": { // AccountKey
"owner": "Alice::abc123...",
"custodian": "Acme::abc123...",
"id": {
"unpack": "1"
}
},
"_2": { // InstructionKey
"requestors": {
"map": [
[
"Alice::abc123...",
{}
]
]
},
"batchId": {
"unpack": "batch1"
},
"id": {
"unpack": "1"
}
}
}
},
"routedStep": {
"sender": "Charlie::abc123...",
"receiver": "David::abc123...",
"custodian": "Custodian::abc123...",
"quantity": {
"unit": {
"depository": "Custodian::abc123...",
"issuer": "Issuer::abc123...",
"id": {
"unpack": "1"
},
"version": "Coin1"
},
"amount": "100.0"
}
}
}
]
}
]
}
The response will include all Batch
es and Instruction
es visible to the API caller.
You can use the ledger offset of the last transaction in the list to call the endpoint again, using this value for the
before
field.
The API response is currently missing an attribute showing if/when the Batch
was cancelled. This should be added in
future.
The API assumes that the batch key is a unique identifier for each Batch
, where the batch key is defined as the
combination of batch ID and the set of requesting parties. This applies even for Batch
es which are already archived
on the ledger. The Batch
es and Instruction
s are grouped together by the batch key. This requires the requesting
parties to use a trusted supplier of unique identifiers from off the ledger.
This endpoint is useful if you need to get specific Holding
contracts and use them within workflows, such as transfers
or DvP.
- URL:
/wallet-views/v1/holdings
- Method:
POST
- Content-Type:
application/json
- Content:
{
"account": { // Account to filter by (note: all fields within this account object are optional)
"owner": "Alice::abc123...",
"custodian": "Custodian::abc123...",
"id": {
"unpack": "1"
}
},
"instrument": { // Instrument to filter by (note: all fields with this instrument object are mandatory)
"depository": "Depository::abc123...",
"issuer": "Issuer1::abc123...",
"id": {
"unpack": "Coin1"
},
"version": "1"
}
}
- N/A (only returns data which is visible to parties that the user has readAs or actAs rights for).
- Content-Type:
application/json
- Content:
{
"result": [ // Zero or more Holdings
{
"cid": "abc123...", // Contract ID of the Holding
"view": { // Interface view of the Daml Finance Holding (Base interface)
"account": {
"custodian": "Acme::abc123...",
"owner": "Alice::abc123...",
"id": {
"unpack": "1"
}
},
"instrument": {
"depository": "Depository::abc123...",
"issuer": "Issuer1::abc123...",
"id": {
"unpack": "Coin1"
},
"version": "1"
},
"amount": "999.0",
"lock": { // Optional
"lockers": {
"map": [
["Alice::abc123...", {}],
["Bob::abc123...", {}]
]
},
"lockType": "Semaphore" // "Semaphore" or "Reentrant"
}
},
"create": {
"offset": "...",
"effectiveTime": "2023-01-01T04:30:23.123456Z"
}
}
]
}
Note: only active contracts are returned.
- URL:
/wallet-views/v1/instruments
- Method:
POST
- Content-Type:
application/json
- Content:
{
"depository": "Depository1::abc123...", // Instrument depository - optional
"issuer": "Issuer::abc123...", // Instrument issuer
"id": { // Instrument ID - optional
"unpack": "Coin1"
},
"version": "1" // Instrument version - optional
}
- N/A (only returns data that is visible to parties that the user has readAs or actAs rights for).
- Content-Type:
application/json
- Content:
{
"result": [ // Zero or more Instruments
{
"cid": "abc123...", // Contract ID of the Instrument
"tokenView": { // Interface view of the Instrument if it is a Token Instrument
"instrument": {
"depository": "Depository::abc123...",
"issuer": "Issuer1::abc123...",
"id": {
"unpack": "Coin1"
},
"version": "1"
},
"description": "...",
"validAsOf": "2023-01-01T04:30:23.123456Z"
},
"pbaView": {
// Interface view of the Instrument if it is a PartyBoundAttributes Instrument (defined in `pbt` folder in the
// base of this repository)
"instrument": {
"depository": "Depository::abc123...",
"issuer": "Issuer1::abc123...",
"id": {
"unpack": "Coin1"
},
"version": "1"
},
"description": "...",
"validAsOf": "2023-01-01T04:30:23.123456Z",
"owner": "Alice::abc123...",
"attributes": [
["k1", "v1"],
["k2", "v2"]
]
}
}
]
}
List Issuer
contracts as defined in the issuer-onboarding
folder at the base of this repository.
- URL:
/wallet-views/v1/issuers
- Method:
POST
- Content-Type:
application/json
- Content:
{
"depository": "Depository1::abc123...", // Optional
"issuer": "Issuer::abc123...", // Optional
}
- N/A (only returns data which is visible to parties that the user has readAs or actAs rights for).
- Content-Type:
application/json
- Content:
{
"result": [ // Zero or more Issuers
{
"token": { // Details of Token issuer (optional as other issuer types may be added in future e.g. Bond Issuer)
"cid": "abc123...", // Contract ID of the Token Issuer
"view": {
"depository": "Depository1::abc123...",
"issuer": "Issuer::abc123...",
"instrumentFactoryCid": "abc123..."
}
}
}
]
}
Then the API can be compiled using:
make compile-wallet-views
The above command will download and build all required dependencies. Do not try to compile without using make
as these
dependencies may not be updated.
To build an executable JAR:
make build-wallet-views
The TypeScript client can be built using:
make build-wallet-views-ts-client
In order to run Scribe you need to upload the required DAR files to the ledger first. You can either use the
dops
CLI tool (refer to operations
folder in the base of this repository) as follows:
# Make sure to export required environment variables to connect to your participant before running `dops` (see
# the documentation for more detail)
dops upload-dar
or you can directly upload the DAR file found at .build/synfini-wallet-views-types.dar
using the daml
assistant
or ledger API. This DAR file is automatically built by make compile-wallet-views
or make build-wallet-views
.
You may want to allocate one or more parties on the participant which will be used to stream all required contracts from the ledger. This party or parties will need to be a stakeholder on the desired contracts. Filtering data by party this way can be useful to reduce the amount of data stored in the DB. If, in the future we migrate to a Daml 3.0 architecture, then this streaming party/parties can be allocated on a separate participant. Alternatively, you can stream data for all parties on your participant.
Create a Postgres database. Currently, only Postgres version 16 has been tested.
There are several utility PSQL functions that are used by the API. You need to deploy these by executing the provided
file wallet-views/java/src/main/resources/db/functions.sql
.
The Scribe process (long-running) must be started. The following positional arguments must be provided:
./scribe.jar pipeline ledger postgres-document
Note: the data source must be set to TransactionStream
(the default). You may need to add other CLI options to connect
to your participant and database. Please consult the PQS manual for more information. You will need to set the
--pipeline-filter-parties
option if you allocated specific parties for streaming data.
Create an application.properties
file by copying the example from src/main/resources/application.properties
. The
following properties can be modified as required but the others should not be altered:
Property | Description |
---|---|
spring.datasource.url |
JDBC URL of the Postgres database |
spring.datasource.username |
Username used to write the data from the ledger into Postgres database. Also used to create the database schema if required. |
spring.datasource.password |
Password of the above Postgres user |
walletviews.ledger-host |
Host of the participant node's gRPC API |
walletviews.ledger-port |
Port of the participant node's gRPC API |
walletviews.ledger-plaintext |
Use true for plaintext connections to the participant node's gRPC API, otherwise TLS will be used |
walletviews.max-transactions-response-size |
Maximum number of batch settlements the API will return in a single response when no limit is provided in the request |
Next, run make build-wallet-views
. This will generate a JAR file under the wallet-views/java/target
directory. You
can launch the API by running
java \
-jar wallet-views/java/target/wallet-views-<version>.jar \
--spring.config.name=application \
--spring.config.location=file:<directory containing application.properties>
This will start the API on port 8080 running over a plaintext connection. TLS support is not currently implemented.
To run functional test cases on the API, make sure you export the SCRIBE_LOCATION
environment variable to point
to where you have installed the Scribe JAR file. Then run the below command:
make test-wallet-views
This will:
- Start a local Daml sandbox on an available port.
- Run Scribe to stream events from the sandbox into a Postgres instance managed by Test Containers.
- Test that the API correctly returns data based on what commands have been submitted to the ledger API.
- Tear down the sandbox and wallet views application at the end of the test suite.
Note: in future the JAR file should be replaced by a containerized version of Scribe.
To run specific test cases for the wallet backend, you need to export an environment variable for this:
export TEST_WALLET_VIEWS_ARGS="-Dtest=IntegrationTest#yourTestMethod"
make test-wallet-views
The test cases have configurable timeouts for waiting for the sandbox and scribe components to start. They can be set as follows:
export TEST_WALLET_VIEWS_ARGS="-Dwalletviews.test.sandbox-start-timeout-seconds=60 -Dwalletviews.test.scribe-start-timeout-seconds=30"
make test-wallet-views
There is a set of integration tests for the TypeScript client. They can be run using:
make test-wallet-views-ts-client
This will:
- Start a local Daml sandbox on an available port.
- Run PQS (Scribe) to write events from the sandbox into a Postgres instance created using
docker compose
on an available port. - Perform basic tests to make sure the TypeScript client can retrieve data from the API.
- Tear down the sandbox and wallet views application at the end of the test suite.
- Stop the Postgres docker container.