Remove navigator (#18141)

* remove navigator

* remove all apart from assistant

* remove navigator from assistant

* remove navigator from release procedure

* remove mentions of navigator completely
This commit is contained in:
mziolekda 2024-01-12 12:32:57 +01:00 committed by GitHub
parent 6f3bb7f8ef
commit bde7632208
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
316 changed files with 7 additions and 40141 deletions

View File

@ -5,6 +5,5 @@ node_modules/
compiler/daml-extension/node_modules/
language-support/ts/node_modules/
language-support/ts/packages/node_modules/
navigator/frontend/node_modules/
compatibility/

View File

@ -21,10 +21,6 @@ bazel-daml
bazel-out
bazel-testlogs
# ignore generated code
navigator/frontend/src/api/Queries.ts
navigator/frontend/src/ui-core/src/api/Queries.ts
# ignore canton code drop
canton
canton-3x

View File

@ -55,7 +55,6 @@ NOTICES @garyverhaegen-da @dasormeter
# Application Runtime
/ledger-service/ @filmackay
/navigator/ @filmackay
/runtime-components/ @filmackay
/triggers/service/ @filmackay

View File

@ -732,15 +732,6 @@ yarn_install(
yarn_lock = "//compiler/daml-extension:yarn.lock",
)
# TODO use fine-grained managed dependency
yarn_install(
name = "navigator_frontend_deps",
args = ["--frozen-lockfile"],
package_json = "//navigator/frontend:package.json",
symlink_node_modules = False,
yarn_lock = "//navigator/frontend:yarn.lock",
)
# Weve had a bunch of problems with typescript rules on Windows.
# Therefore weve disabled them completely for now.
# Since we need to @load stuff in @language_support_ts_deps

View File

@ -160,14 +160,10 @@ commandParser = subparser $ fold
startCmd = do
sandboxPortM <- sandboxPortOpt "sandbox-port" "Port number for the sandbox"
shouldOpenBrowser <- flagYesNoAuto "open-browser" True "Open the browser after navigator" idm
shouldStartNavigator <- flagYesNoAuto' "start-navigator" "Start navigator as part of daml start. Can be set to true or false. Defaults to true." idm
navigatorPort <- navigatorPortOption
jsonApiPortM <- jsonApiPortOpt "json-api-port" "Port that the HTTP JSON API should listen on or 'none' to disable it"
onStartM <- optional (option str (long "on-start" <> metavar "COMMAND" <> help "Command to run once sandbox and navigator are running."))
onStartM <- optional (option str (long "on-start" <> metavar "COMMAND" <> help "Command to run once sandbox is running."))
shouldWaitForSignal <- flagYesNoAuto "wait-for-signal" True "Wait for Ctrl+C or interrupt after starting servers." idm
sandboxOptions <- many (strOption (long "sandbox-option" <> metavar "SANDBOX_OPTION" <> help "Pass option to sandbox"))
navigatorOptions <- many (strOption (long "navigator-option" <> metavar "NAVIGATOR_OPTION" <> help "Pass option to navigator"))
scriptOptions <- many (strOption (long "script-option" <> metavar "SCRIPT_OPTION" <> help "Pass option to Daml script interpreter"))
shutdownStdinClose <- stdinCloseOpt
sandboxPortSpec <- sandboxCantonPortSpecOpt
@ -183,12 +179,6 @@ commandParser = subparser $ fold
domainAdminApiSpec <- sandboxPortOpt "sandbox-domain-admin-port" "Port number for the canton domain admin API (--sandbox-canton only)"
pure SandboxCantonPortSpec {..}
navigatorPortOption = NavigatorPort <$> option auto
(long "navigator-port"
<> metavar "PORT_NUM"
<> value 7500
<> help "Port number for navigator (default is 7500).")
deployCmdInfo = mconcat
[ progDesc $ concat
[ "Deploy the current Daml project to a remote Daml ledger. "

View File

@ -5,10 +5,7 @@ module DA.Daml.Helper.Start
, withJar
, withSandbox
, withNavigator
, StartOptions(..)
, NavigatorPort(..)
, SandboxPort(..)
, SandboxPortSpec(..)
, toSandboxPortSpec
@ -29,9 +26,6 @@ import System.FilePath
import System.Process.Typed
import System.IO.Extra
import System.Info.Extra
import Web.Browser
import Options.Applicative.Extended (YesNoAuto, determineAutoM)
import DA.Daml.Helper.Codegen
import DA.Daml.Helper.Ledger
@ -48,15 +42,8 @@ toSandboxPortSpec n
| otherwise = Just (SpecifiedPort (SandboxPort n))
newtype SandboxPort = SandboxPort { unSandboxPort :: Int }
newtype NavigatorPort = NavigatorPort Int
newtype JsonApiPort = JsonApiPort { unJsonApiPort :: Int }
navigatorPortNavigatorArgs :: NavigatorPort -> [String]
navigatorPortNavigatorArgs (NavigatorPort p) = ["--port", show p]
navigatorURL :: NavigatorPort -> String
navigatorURL (NavigatorPort p) = "http://localhost:" <> show p
-- | Use SandboxPortSpec to determine a sandbox port number.
-- This is racy thanks to getFreePort, but there's no good alternative at the moment.
getPortForSandbox :: SandboxPortSpec -> Maybe SandboxPortSpec -> IO Int
@ -92,19 +79,6 @@ withSandbox StartOptions{..} darPath sandboxArgs kont =
runLedgerUploadDar (sandboxLedgerFlags sandboxPort) (Just darPath)
kont ph (SandboxPort sandboxPort)
withNavigator :: SandboxPort -> NavigatorPort -> [String] -> (Process () () () -> IO a) -> IO a
withNavigator (SandboxPort sandboxPort) navigatorPort args a = do
let navigatorArgs = concat
[ ["navigator", "server", "localhost", show sandboxPort]
, navigatorPortNavigatorArgs navigatorPort
, args
]
withSdkJar navigatorArgs "navigator-logback.xml" $ \ph -> do
putStrLn "Waiting for navigator to start: "
waitForHttpServer 240 (unsafeProcessHandle ph) (putStr "." *> threadDelay 500000)
(navigatorURL navigatorPort) []
a ph
waitForJsonApi :: Process () () () -> JsonApiPort -> IO ()
waitForJsonApi sandboxPh (JsonApiPort jsonApiPort) = do
putStrLn "Waiting for JSON API to start: "
@ -121,14 +95,10 @@ withOptsFromProjectConfig fieldName cliOpts projectConfig = do
data StartOptions = StartOptions
{ sandboxPortM :: Maybe SandboxPortSpec
, shouldOpenBrowser :: Bool
, shouldStartNavigator :: YesNoAuto
, navigatorPort :: NavigatorPort
, jsonApiPortM :: Maybe JsonApiPort
, onStartM :: Maybe String
, shouldWaitForSignal :: Bool
, sandboxOptions :: [String]
, navigatorOptions :: [String]
, scriptOptions :: [String]
, sandboxPortSpec :: !SandboxCantonPortSpec
}
@ -147,13 +117,7 @@ runStart startOptions@StartOptions{..} =
mbInitScript :: Maybe String <-
requiredE "Failed to parse init-script" $
queryProjectConfig ["init-script"] projectConfig
shouldStartNavigator :: Bool <-
determineAutoM (fmap (fromMaybe True) $
requiredE "Failed to parse start-navigator" $
queryProjectConfig ["start-navigator"] projectConfig)
shouldStartNavigator
sandboxOpts <- withOptsFromProjectConfig "sandbox-options" sandboxOptions projectConfig
navigatorOpts <- withOptsFromProjectConfig "navigator-options" navigatorOptions projectConfig
scriptOpts <- withOptsFromProjectConfig "script-options" scriptOptions projectConfig
doBuild
doCodegen projectConfig
@ -178,19 +142,13 @@ runStart startOptions@StartOptions{..} =
runProcess_ procScript
doRunInitScript
listenForKeyPress projectConfig darPath sandboxPort doRunInitScript
withNavigator' shouldStartNavigator sandboxPh sandboxPort navigatorPort navigatorOpts $ \navigatorPh -> do
do
whenJust onStartM $ \onStart -> runProcess_ (shell onStart)
when (shouldStartNavigator && shouldOpenBrowser) $
void $ openBrowser (navigatorURL navigatorPort)
whenJust jsonApiPortM $ \jsonApiPort -> waitForJsonApi sandboxPh jsonApiPort
when shouldWaitForSignal $
void $ waitAnyCancel =<< mapM (async . waitExitCode) [navigatorPh,sandboxPh]
void $ waitAnyCancel =<< mapM (async . waitExitCode) [sandboxPh]
where
withNavigator' shouldStartNavigator sandboxPh =
if shouldStartNavigator
then withNavigator
else (\_ _ _ f -> f sandboxPh)
doCodegen projectConfig =
forM_ [minBound :: Lang .. maxBound :: Lang] $ \lang -> do
mbOutputPath :: Maybe String <-
@ -235,14 +193,3 @@ runStart startOptions@StartOptions{..} =
reloadInstructions
| isWindows = "\nPress 'r' + 'Enter' to re-build and upload the package to the sandbox.\nPress 'Ctrl-C' to quit."
| otherwise = "\nPress 'r' to re-build and upload the package to the sandbox.\nPress 'Ctrl-C' to quit."
withSdkJar
:: [String]
-- ^ Commands passed to the assistant and the SDK JAR.
-> FilePath
-- ^ File name of the logback config.
-> (Process () () () -> IO a)
-> IO a
withSdkJar args logbackConf f = do
logbackArg <- getLogbackArg (damlSdkJarFolder </> logbackConf)
withJar damlSdkJar [logbackArg] args f

View File

@ -10,9 +10,7 @@ load("@os_info//:os_info.bzl", "is_windows")
load(":util.bzl", "deps")
scala_deps = [
"@maven//:org_apache_pekko_pekko_http_spray_json",
"@maven//:com_typesafe_scala_logging_scala_logging",
"@maven//:io_spray_spray_json",
]
scala_runtime_deps = [
@ -34,8 +32,6 @@ da_scala_binary(
resources = glob(["src/main/resources/**/*"]),
scala_deps = scala_deps,
scala_runtime_deps = scala_runtime_deps,
# Navigator resources trigger this incorrectly
unused_dependency_checker_mode = "off",
visibility = ["//visibility:public"],
runtime_deps = runtime_deps,
deps = deps("ce"),
@ -49,8 +45,6 @@ da_scala_binary(
scala_deps = scala_deps,
scala_runtime_deps = scala_runtime_deps,
tags = ["ee-jar-license"],
# Navigator resources trigger this incorrectly
unused_dependency_checker_mode = "off",
visibility = ["//visibility:public"],
runtime_deps = runtime_deps + [
"@maven//:com_oracle_database_jdbc_ojdbc8",

View File

@ -8,7 +8,6 @@ import com.daml.lf.engine.script.{ScriptMain => Script}
import com.daml.lf.engine.trigger.{RunnerMain => Trigger}
import com.daml.lf.engine.trigger.{ServiceMain => TriggerService}
import com.daml.auth.middleware.oauth2.{Main => Oauth2Middleware}
import com.daml.navigator.{NavigatorBackend => Navigator}
import com.daml.script.export.{Main => Export}
object SdkMain {
@ -22,7 +21,6 @@ object SdkMain {
case "codegen" => Codegen.main(rest)
case "trigger-service" => TriggerService.main(rest)
case "oauth2-middleware" => Oauth2Middleware.main(rest)
case "navigator" => Navigator.main(rest)
case _ => sys.exit(1)
}
}

View File

@ -5,11 +5,8 @@ def deps(edition):
return [
"//daml-script/runner:script-runner-lib",
"//language-support/codegen-main:codegen-main-lib",
"//navigator/backend:navigator-library",
"//daml-script/export",
"//triggers/runner:trigger-runner-lib",
"//triggers/service:trigger-service-binary-{}".format(edition),
"//triggers/service/auth:oauth2-middleware",
"//navigator/backend:backend-resources",
"//navigator/backend:frontend-resources",
]

View File

@ -382,7 +382,6 @@ argWhitelist = S.fromList
, "clean"
, "damlc", "ide", "license", "package", "docs", "visual", "visual-web", "inspect-dar", "validate-dar", "doctest", "lint"
, "sandbox", "INFO", "TRACE", "DEBUG", "WARN", "ERROR"
, "navigator", "server", "console", "dump-graphql-schema", "create-config", "static", "simulated", "wallclock"
, "ledger", "list-parties", "allocate-parties", "upload-dar", "fetch-dar"
, "codegen", "java", "js"
, "deploy"

View File

@ -141,7 +141,6 @@ damlStart tmpDir = do
let startProc =
(shell $ unwords
[ "daml start"
, "--start-navigator=no"
, "--sandbox-port", show $ ledger ports
, "--sandbox-admin-api-port", show $ admin ports
, "--sandbox-domain-public-port", show $ domainPublic ports
@ -413,23 +412,6 @@ damlStartTests getDamlStart =
manager <- newManager defaultManagerSettings
resp <- httpLbs req manager
responseBody resp @?= "{\"status\":\"pass\"}"
subtest "Navigator startup" $ do
DamlStartResource {projDir, sandboxPort} <- getDamlStart
navigatorPort :: Int <- fromIntegral <$> getFreePort
-- This test just checks that navigator starts up and returns a 200 response.
-- Nevertheless this would have caught a few issues on rules_nodejs upgrades
-- where we got a 404 instead.
withDamlServiceIn projDir "navigator"
[ "server"
, "localhost"
, show sandboxPort
, "--port"
, show navigatorPort
] $ \ ph -> do
waitForHttpServer 240 ph
(threadDelay 500000)
("http://localhost:" <> show navigatorPort)
[]
subtest "hot reload" $ do
DamlStartResource {projDir, jsonApiPort, startStdin, stdoutChan, alice, aliceHeaders} <- getDamlStart

View File

@ -35,7 +35,6 @@ da_scala_library(
"//daml-script:__subpackages__",
"//language-support:__subpackages__",
"//ledger-service:__subpackages__",
"//navigator:__subpackages__",
],
deps = [
"//daml-lf/archive:daml_lf_archive_reader",

View File

@ -45,10 +45,6 @@ packages/daml-manual/reference/file-structure.html -> /daml/reference/file-struc
packages/daml-manual/reference/expressions.html -> /daml/reference/expressions.html
packages/sdk-docs-introduction/index.html -> /getting-started/index.html
packages/da-docs-example-collateral/index.html -> /examples/collateral/index.html
packages/navigator-docs/index.html -> /tools/navigator/index.html
packages/navigator-docs/database.html -> /tools/navigator/database.html
packages/navigator-docs/backend-licenses.html -> /
packages/navigator-docs/frontend-licenses.html -> /
packages/quickstart/index.html -> /getting-started/index.html
packages/da-docs-ledger-model/ledger-integrity.html -> /concepts/ledger-model/ledger-integrity.html
packages/da-docs-ledger-model/index.html -> /concepts/ledger-model/index.html

View File

@ -1,385 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
import { DamlLfValue } from '@da/ui-core';
export const version = {
schema: 'navigator-config',
major: 2,
minor: 0,
};
export const customViews = (userId, party, role) => ({
issued_ious: {
type: "table-view",
title: "Issued Ious",
source: {
type: "contracts",
filter: [
{
field: "argument.issuer",
value: party,
},
{
field: "template.id",
value: "Iou:Iou",
}
],
search: "",
sort: [
{
field: "id",
direction: "ASCENDING"
}
]
},
columns: [
{
key: "id",
title: "Contract ID",
createCell: ({rowData}) => ({
type: "text",
value: rowData.id
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.owner",
title: "Owner",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).owner
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.currency",
title: "Currency",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).currency
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.amount",
title: "Amount",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).amount
}),
sortable: true,
width: 80,
weight: 3,
alignment: "left"
}
]
},
owned_ious: {
type: "table-view",
title: "Owned Ious",
source: {
type: "contracts",
filter: [
{
field: "argument.owner",
value: party,
},
{
field: "template.id",
value: "Iou:Iou",
}
],
search: "",
sort: [
{
field: "id",
direction: "ASCENDING"
}
]
},
columns: [
{
key: "id",
title: "Contract ID",
createCell: ({rowData}) => ({
type: "text",
value: rowData.id
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.issuer",
title: "Issuer",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).issuer
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.currency",
title: "Currency",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).currency
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.amount",
title: "Amount",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).amount
}),
sortable: true,
width: 80,
weight: 3,
alignment: "left"
}
]
},
transfers: {
type: "table-view",
title: "Iou Transfers",
source: {
type: "contracts",
filter: [
{
field: "template.id",
value: "Iou:IouTransfer",
}
],
search: "",
sort: [
{
field: "id",
direction: "ASCENDING"
}
]
},
columns: [
{
key: "id",
title: "Contract ID",
createCell: ({rowData}) => ({
type: "text",
value: rowData.id
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.iou.owner",
title: "Sender",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).iou.owner
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.iou.newOwner",
title: "Receiver",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).newOwner
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.iou.issuer",
title: "Issuer",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).iou.issuer
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.iou.currency",
title: "Currency",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).iou.currency
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.iou.amount",
title: "Amount",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).iou.amount
}),
sortable: true,
width: 80,
weight: 3,
alignment: "left"
}
]
},
trades: {
type: "table-view",
title: "Trades",
source: {
type: "contracts",
filter: [
{
field: "template.id",
value: "IouTrade:IouTrade@",
}
],
search: "",
sort: [
{
field: "id",
direction: "ASCENDING"
}
]
},
columns: [
{
key: "id",
title: "Contract ID",
createCell: ({rowData}) => ({
type: "text",
value: rowData.id
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.buyer",
title: "Buyer",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).buyer
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.seller",
title: "Seller",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).seller
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.baseIssuer",
title: "Base Issuer",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).baseIssuer
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.baseCurrency",
title: "Base Currency",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).baseCurrency
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.baseAmount",
title: "Base Amount",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).baseAmount
}),
sortable: true,
width: 80,
weight: 3,
alignment: "left"
},
{
key: "argument.quoteIssuer",
title: "Quote Issuer",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).quoteIssuer
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.quoteCurrency",
title: "Quote Currency",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).quoteCurrency
}),
sortable: true,
width: 80,
weight: 0,
alignment: "left"
},
{
key: "argument.quoteAmount",
title: "Quote Amount",
createCell: ({rowData}) => ({
type: "text",
value: DamlLfValue.toJSON(rowData.argument).quoteAmount
}),
sortable: true,
width: 80,
weight: 3,
alignment: "left"
},
]
}
})

2
fmt.sh
View File

@ -175,7 +175,7 @@ if ! diff .bazelrc compatibility/.bazelrc >/dev/null; then
diff -u .bazelrc compatibility/.bazelrc
fi
# check akka is not used as dependency except in navigator_maven and deprecated_maven
# check akka is not used as dependency except in deprecated_maven
for f in $(ls *_install*.json | egrep -v "deprecated"); do
if grep -q akka $f; then
echo $f contains a dependency to akka

View File

@ -1 +0,0 @@
**/src/main/resources/frontend/

View File

@ -1,7 +0,0 @@
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
alias(
name = "navigator-binary",
actual = "//navigator/backend:navigator-binary",
)

View File

@ -1,39 +0,0 @@
.PHONY: build test start-frontend start-ui-core
default: build
build:
bazel build //navigator/...
test:
bazel test //navigator/...
start-backend:
$(MAKE) -C backend start
start-frontend:
$(MAKE) -C frontend start
start-ui-core:
$(MAKE) -C frontend start-ui-core
# Updates licensing documentation.
# This documentation includes licensing info for each library that we use.
update-license-docs:
(cd docs/licenses && ./extract-sbt.py)
(cd docs/licenses && ./extract-js.py)
# CI: Makes sure the frontend code is nicely formatted.
lint:
$(MAKE) -C frontend lint
# CI: Makes sure every file has a license header.
check-licenses:
dade-copyright-headers check navigator
# CI: Makes sure licensing documentation is up to date.
check-license-docs:
(cd docs/licenses && ./extract-sbt.py check)
(cd docs/licenses && ./extract-js.py check)

View File

@ -1,77 +0,0 @@
Navigator
=========
The *Navigator* is a web-app that connects to any Digital Asset ledger and
allows the user to inspect contracts, create contracts, and exercise choices.
The Navigator can be used in development mode (see below) or packaged into a
"fat" JAR that includes the compiled frontend assets for distribution.
Navigator architecture
----------------------
To learn more about developing the different parts of the Navigator see:
- [Navigator Frontend README](./frontend)
- [Navigator Backend README](./backend)
Building Navigator
------------------
To build a "fat" JAR of the Navigator that includes the pre-compiled front-end
assets, run:
```bash
bazel build //navigator/backend:navigator-binary_distribute.jar
```
This produces a "fat" JAR `bazel-bin/navigator/backend/navigator-binary_distribute.jar` which can be run with:
```bash
java -jar bazel-bin/navigator/backend/navigator-binary_distribute.jar
```
Notable things in the Navigator build:
### backend/src/test/resources/schema.graphql
Manually written, must be consistent with `backend/src/main/scala/com/digitalasset/navigator/graphql/GraphQLSchema.scala`. Consistency is checked in a test.
### frontend/src/**/api/Queries.ts
Generated from `backend/src/test/resources/schema.graphql` with an external codegen tool.
Currently, these files are checked in and updated with `make update-graphql-types`.
### frontend bundled code
Code from `frontend/src/**/*.ts*`, compiled using TypeScript, and bundled with Webpack.
Output includes:
- `bundle-[hash].js`: bundled frontend code, name uses content hasing.
- `browsercheck-[hash].js`: tiny module for checking browser compatibility, name uses content hasing.
- Several image and font files, referenced by the above modules. File names use content hashing.
- `index.html`: Single page application main entry, references the above modules.
Note: Browsers are instructed never to cache `index.html`, and indefinitely cache all other files. This is why content hashing is used.
### backend binary
Scala binary, compiled as a fat JAR.
Code from `backend/src/**/*.scala`, bundled frontend code is copied to `backend/src/main/resources/frontend`.
### backend version
The version is included as resource files in the Navigator fat jar.
This is to reduce rebuild times when the version changes.
### frontend development build
For developing frontend code, `webpack-dev-server` is used. This serves the current frontend code on a separate port, and does:
- Watch `*.ts` files for changes
- Perform incremental builds
- Send a push notification to the browser, automatically reloading the page when the build is finished.
- Forward network requests to a different port, where a Navigator backend is expected to run.
This is orders of magnitude faster than what the current Bazel build offers, so it is desirable to keep the `webpack-dev-server` setup working.
Note, the browser is instructed to cache assets based on the SDK version.
During development this is too aggressive and you will need to manually refresh to see updates to the front-end.

View File

@ -1 +0,0 @@
target/

View File

@ -1,212 +0,0 @@
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
load(
"//bazel_tools:scala.bzl",
"da_scala_binary",
"da_scala_library",
"da_scala_test_suite",
)
navigator_scalacopts = [
"-Xelide-below",
"CONFIG",
]
# All frontend resource files.
# These come in a manually created JAR file, this rule is just wrapping it in
# a java_import, so that it is a valid target for the 'resources' property of
# scala_binary.
java_import(
name = "frontend-resources",
jars = [
"//navigator/frontend:frontend.jar",
],
visibility = ["//visibility:public"],
)
# Static backend resources.
java_library(
name = "backend-resources",
resources = glob(["src/main/resources/**/*"]),
visibility = ["//visibility:public"],
)
da_scala_library(
name = "navigator-library",
srcs = glob([
"src/main/scala/**/*.scala",
]),
scala_deps = [
"@maven//:com_chuusai_shapeless",
"@maven//:com_github_pureconfig_pureconfig_core",
"@maven//:com_github_pureconfig_pureconfig_generic",
"@maven//:com_github_scopt_scopt",
"@maven//:org_apache_pekko_pekko_actor",
"@maven//:org_apache_pekko_pekko_http",
"@maven//:org_apache_pekko_pekko_http_core",
"@maven//:org_apache_pekko_pekko_http_spray_json",
"@maven//:org_apache_pekko_pekko_stream",
"@maven//:com_typesafe_scala_logging_scala_logging",
"@maven//:io_spray_spray_json",
"@maven//:org_sangria_graphql_sangria_ast",
"@maven//:org_sangria_graphql_sangria_core",
"@maven//:org_sangria_graphql_sangria_derivation",
"@maven//:org_sangria_graphql_sangria_parser",
"@maven//:org_sangria_graphql_sangria_marshalling_api",
"@maven//:org_scalaz_scalaz_core",
"@maven//:org_tpolecat_doobie_core",
"@maven//:org_tpolecat_doobie_free",
"@maven//:org_typelevel_cats_core",
"@maven//:org_typelevel_cats_effect",
"@maven//:org_typelevel_cats_free",
"@maven//:org_typelevel_cats_kernel",
],
scala_runtime_deps = [
"@maven//:org_apache_pekko_pekko_slf4j",
],
scalacopts = navigator_scalacopts,
visibility = ["//visibility:public"],
runtime_deps = [
"@maven//:ch_qos_logback_logback_classic",
"@maven//:org_xerial_sqlite_jdbc",
],
deps = [
"//daml-assistant/scala-daml-project-config",
"//daml-lf/api-type-signature",
"//daml-lf/archive:daml_lf_archive_reader",
"//daml-lf/archive:daml_lf_dev_archive_proto_java",
"//daml-lf/data",
"//daml-lf/language",
"//daml-lf/transaction",
"//language-support/scala/bindings",
"//ledger-service/lf-value-json",
"//ledger/ledger-api-client",
"//ledger/ledger-api-common",
"//ledger/ledger-api-domain",
"//libs-scala/build-info",
"//libs-scala/grpc-utils",
"//libs-scala/nonempty",
"//libs-scala/rs-grpc-bridge",
"//libs-scala/rs-grpc-pekko",
"//libs-scala/scala-utils",
"//observability/tracing",
"@maven//:com_typesafe_config",
"@maven//:io_grpc_grpc_netty",
"@maven//:io_netty_netty_handler",
"@maven//:org_slf4j_slf4j_api",
],
)
da_scala_library(
name = "navigator-tests-library",
srcs = glob(
["src/test/**/*.scala"],
exclude = [
"src/test/**/*Spec.scala",
"src/test/**/*Test.scala",
],
),
scala_deps = [
"@maven//:org_scalaz_scalaz_core",
],
scala_runtime_deps = [
"@maven//:org_apache_pekko_pekko_stream_testkit",
],
scalacopts = navigator_scalacopts,
runtime_deps = [
"@maven//:ch_qos_logback_logback_classic",
"@maven//:io_grpc_grpc_netty",
"@maven//:org_xerial_sqlite_jdbc",
],
deps = [
":navigator-library",
"//daml-lf/api-type-signature",
"//daml-lf/data",
"//daml-lf/transaction",
"//language-support/scala/bindings",
"//ledger-service/lf-value-json",
],
)
da_scala_binary(
name = "navigator-binary",
main_class = "com.daml.navigator.NavigatorBackend",
tags = [
"maven_coordinates=com.daml:navigator:__VERSION__",
"no_scala_version_suffix",
],
visibility = [
"//visibility:public",
],
deps = [
":backend-resources",
":frontend-resources",
":navigator-library",
],
)
# Static test resources.
java_library(
name = "test-resources",
resources = glob(["src/test/resources/**/*"]),
)
da_scala_test_suite(
name = "navigator-scala-tests",
size = "large",
srcs = glob([
"src/test/**/*Spec.scala",
"src/test/**/*Test.scala",
]),
scala_deps = [
"@maven//:org_apache_pekko_pekko_actor",
"@maven//:org_apache_pekko_pekko_http",
"@maven//:org_apache_pekko_pekko_http_core",
"@maven//:org_apache_pekko_pekko_http_spray_json",
"@maven//:org_apache_pekko_pekko_http_testkit",
"@maven//:org_apache_pekko_pekko_stream",
"@maven//:com_typesafe_scala_logging_scala_logging",
"@maven//:io_spray_spray_json",
"@maven//:org_sangria_graphql_sangria_ast",
"@maven//:org_sangria_graphql_sangria_core",
"@maven//:org_sangria_graphql_sangria_derivation",
"@maven//:org_sangria_graphql_sangria_parser",
"@maven//:org_sangria_graphql_sangria_marshalling_api",
"@maven//:org_scalacheck_scalacheck",
"@maven//:org_scalatest_scalatest_core",
"@maven//:org_scalatest_scalatest_matchers_core",
"@maven//:org_scalatest_scalatest_shouldmatchers",
"@maven//:org_scalatest_scalatest_wordspec",
"@maven//:org_scalatestplus_scalacheck_1_15",
"@maven//:org_scalaz_scalaz_core",
],
scalacopts = navigator_scalacopts,
tags = ["cpu:4"],
deps = [
":navigator-library",
":navigator-tests-library",
":test-resources",
"//bazel_tools/runfiles:scala_runfiles",
"//daml-lf/api-type-signature",
"//daml-lf/data",
"//daml-lf/transaction",
"//language-support/scala/bindings",
"//ledger-service/lf-value-json",
"//ledger/ledger-api-client",
"//ledger/ledger-api-common",
"//ledger/ledger-api-domain",
"//libs-scala/build-info",
"//libs-scala/ledger-resources",
"//libs-scala/logging-entries",
"//libs-scala/ports",
"//libs-scala/resources",
"//libs-scala/rs-grpc-bridge",
"//libs-scala/testing-utils",
"//libs-scala/timer-utils",
"//test-common/canton/it-lib",
"@maven//:com_typesafe_config",
],
)
exports_files(["src/main/resources/logback.xml"])

View File

@ -1,13 +0,0 @@
.PHONY: build test start
default: build
build:
bazel build //navigator/backend/...
test:
bazel test //navigator/backend/...
report-licenses:
sbt dumpLicenseReport
cp target/license-reports/navigator-licenses.csv $(OUT)

View File

@ -1,246 +0,0 @@
Navigator Backend
=================
The Navigator backend is a Scala application providing
- a web server that exposes a GraphQL API with some predefined endpoints,
such as returning all visible contracts.
- a platform client that reacts to events happening in the platform
- a simple sign-in API
Usage
-----
The Navigator backend is written in Scala, making heavy use of
[Pekko](http://pekko.io/) and [Sangria](http://sangria-graphql.org/) (for
GraphQL). It uses the [Scala Build Tool](http://www.scala-sbt.org/).
We can build and run a basic the backend using the following commands:
```bash
# Build a distribution archive ("fat jar"):
bazel build //navigator/backend:navigator-binary_distribute.jar
# Run without arguments to show usage:
java -jar bazel-bin/navigator/backend/navigator-binary_distribute.jar --help
# Create a dummy configuration file
cat << EOF > navigator.conf
users {
OPERATOR: {
party=OPERATOR
}
}
EOF
# Start the web server
java -jar bazel-bin/navigator/backend/navigator-binary_distribute.jar server -c navigator.conf
```
If you start the server and the configuration file doesn't exist, the server will
write a configuration template and quit. You can then edit the configuration file
and start again the server.
Custom UI backends
------------------
## Extend the `UIBackend` base class
The base class for UI backends is the
[`UIBackend`](src/main/scala/com/digitalasset/navigator/backend/UIBackend.scala),
a Scala class that implements a ledger indexer and a web-server which exposes a
number of default GraphQL endpoints and a simple Session API. It also allows has
hooks for adding custom GraphQL endpoints.
To create a basic UI backend, you can extend `UIBackend` without any custom
additions:
```scala
object DefaultUIBackend extends UIBackend {
override def customEndpoints: Set[CustomEndpoint[_]] = Set()
override val defaultConfigFile: Path = Paths.get("my-app.conf")
override def applicationInfo: ApplicationInfo = ApplicationInfo(...)
override def banner: Option[String] = Some(...)
}
```
You can then run your app with this as the main class.
## Custom endpoints
A custom endpoint is an extra endpoint for a UI backend that binds a name to
some data that can be represented in GraphQL and a function to calculate that
data. For example:
```scala
object ExampleUIBackend extends UIBackend {
override def customEndpoints: Set[CustomEndpoint[_]] = Set(contractsIdEndpoint)
final case class TemplateId(id: String)
final case class ContractProjection(id: String, template: TemplateId, argument: RecordArgument)
/** Endpoing for the contract ids */
private val contractsIdEndpoint = new CustomEndpoint[ContractProjection] {
/** The endpoint to be used as GraphQL top-level for the data served by this */
override def endpointName: String = "contract_id"
/** For each contract in the ledger, extract contract id and template id */
override def calculate(ledger: Ledger): Seq[ContractProjection] =
ledger.allContracts().map(contract =>
ContractProjection(contract.idString, TemplateId(contract.template.idString), contract.argument))
}
}
```
This adds an endpoint called `contract_id` that returns a list of contract
projections that include the contract ID, the template ID, and the contract
arguments. You would then pass this custom endpoint to a UI backend subclass:
There are two components of a `CustomEndpoint`:
- The `endpointName` is the name used to bind this custom endpoint to the root
of the GraphQL Schema. Note that each endpoint must have a unique
`endpointName` and that default `endpointName`s -- `contracts` or `templates`
for example -- cannot be used in custom endpoints either.
- The `calculate` function is used to extract the data to serve from the
[`Ledger`](src/main/scala/com/digitalasset/ui/backend/model/Ledger.scala).
This function must create a sequence of instances of a scala *case class*.
The endpoint `contract_id` becomes available in the root of the graphql schema:
```graphql
type ContractProjection {
id: String!
template: TemplateId!
argument: Value!
}
type TemplateId {
id: String!
}
type Query {
contract_id: [ContractProjection!]!
}
```
The schema is automatically generated by the UI backend by following the
structure of a "row" case class. For instance `TemplateId` is a nested structure
inside `ContractProjection` in the Scala code and for this reason it will be a
nested structure in the GraphQL Schema too.
## Automatic generation of GraphQL Schemas for custom endpoints
The code for the GraphQL endpoint is automatically generated at compile time by
the UI backend. This means that there are some limitations in what can be
exposed as result of the `calculate` function. Specifically:
- The name of the "row" model case class must be a unique name. Don't use
`Contract` or `Template` or any other name already used in the [`GraphQL
Schema`](src/main/scala/com/digitalasset/ui/backend/graphql/GraphQLSchema.scala).
For example, you can't rename `ContractProjection` to `Contract`.
- The main "row" class must be modelled as a Scala case class and it must
contain fields that are either a case class or Scala primitives. If something
is not supported, you won't be able to compile your code.
If some type is not supported by the generator, it is possible to add support
for it by providing either an instance of the typeclass
[`GraphQLLeaf`](src/main/scala/com/digitalasset/ui/backend/graphqless/GraphQLLeaf.scala)
for simple types, e.g. `String` or `Int`, or an instance of the typeclass
[`GraphQLObject`](src/main/scala/com/digitalasset/ui/backend/graphqless/GraphQLObject.scala)
for complex types which have fields.
Common tasks
------------
## How do I query the Backend without a frontend?
The UI backend exposes a [GraphiQL](https://github.com/graphql/graphiql) page to
let the developer run queries interactively. You can find it at the address
`<address_of_the_navigator_backend>/graphql`. Note the "Docs" link in the upper
right corner and also that you have to be logged in order to run queries. You
can either use the frontend in the same browser to login or you can login
manually via command line as follows:
- Send a JSON POST request with the `userId` and make note of
the cookie:
```bash
> curl -H "Content-Type: application/json" -d'{ "userId":"PARTY" }' localhost:4000/api/session/ -i
HTTP/1.1 200 OK
Set-Cookie: session-id=8b4601d4-7113-407b-9b81-7fd5b213a96b; Path=/
Server: pekko-http/1.0.0
Date: Tue, 13 Jun 2017 16:47:38 GMT
Content-Type: application/json
Content-Length: 92
{"type":"session","user":{"id":"BANK1","role":"bank","party":"BANK1","canAdvanceTime":true}}
```
- Open the GraphiQL page in a browser (`localhost:4000/graphql` for example).
- Write `javascript:document.cookie="session-id=<the_id_recceived>"` in the location bar.
- You can now use the GraphiQL to query your data.
We plan to add an option to run the server in unauthenticated mode so that these steps become unnecessary.
Session API
-----------
In addition to the GraphQL endpoint exposing data, the user needs to act as
some party. This is to know which party's "view" on the ledger to expose. The
backend supplies the frontend with a list of available users.
The chosen user is set in a cookie and therefore
persists across reloads.
```typescript
type UserId = string;
type Party = string;
type Role = string;
type User = {
id: UserId;
party: Party;
canAdvanceTime: boolean;
role?: Role;
};
type Status = Session | SignIn;
type Session = { type: 'session'; user: User; }
type SignIn = {
type: 'sign-in';
method: SignInMethod;
error?: 'invalid-credentials';
}
type SignInMethod = SignInSelect
type SignInSelect = { type: 'select', users: UserId[] }
```
```bash
# Get current session or sign in
GET /session/ => Status
# Sign in
POST /session/ -d'{ userId: UserId }' => Status
# Sign out
DELETE /session/ => SignIn
```
Architecture notes
------------------
This section contains short notes for anyone that wishes to make changes to the UI backend library.
- The backend defines its own internal representation of ledger objects
- The backend communicates with the ledger via a JSON-based "ledger API"
- The backend communicates with the frontend via a JSON-based "frontend API"
- These are the relevant source files:
- `model/Model`: Defines the internal representations of ledger objects
- `model/Util`: (SDaml package) -> (internal model)
- `graphql/JsonType`: (internal model) <-> (frontend API JSON format)
- `store/platform/PlatformSubscriber`: (ledger API JSON format) <-> (internal model)

View File

@ -1 +0,0 @@
.package-database

View File

@ -1,44 +0,0 @@
### Navigator Testing Scenarios
The scenario(s) contained in this directory are useful to quickly spin up the sandbox and navigator from `HEAD` and visually inspect the navigator UI or perform other tests.
As an example, assuming your current working directory is the project root, you can:
1. open a terminal
1. change your working directory to `navigator/backend/scenarios/rental`
1. build the project
daml build
1. start the sandbox and run a scenario
bazel run --run_under="cd $PWD && " \
//ledger/sandbox-classic:sandbox-classic-binary -- --scenario Main:example dist/rental.dar
1. open another terminal
1. change your working directory to `navigator/backend/scenarios/rental`
1. start the navigator
bazel run --run_under="cd $PWD && " \
//navigator/backend:navigator-binary -- server
1. open a browser
1. go to `localhost:4000`
1. play around
#### Usage with TLS
If you want to use a secure channel (e.g.: to test against an authenticated sandbox), you can use the test certificates in ``//test-common/test-certificates``.
To use them, run the sandbox as follows:
bazel build //test-common/test-certificates
bazel run --run_under="cd $PWD && " \
//ledger/sandbox-classic:sandbox-classic-binary -- --scenario Main:example dist/rental.dar
--pem $PWD/bazel-bin/test-common/test-certificates/server.pem --crt $PWD/bazel-bin/test-common/test-certificates/server.crt --cacrt $PWD/bazel-bin/test-common/test-certificates/ca.crt
And run navigator as follows:
bazel run --run_under="cd $PWD && " \
//navigator/backend:navigator-binary -- server \
--pem $PWD/bazel-bin/test-common/test-certificates/client.pem --crt $PWD/bazel-bin/test-common/test-certificates/client.crt --cacrt $PWD/bazel-bin/test-common/test-certificates/ca.crt

View File

@ -1,18 +0,0 @@
# Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
sdk-version: 0.0.0
name: rental
source: daml/Main.daml
init-script: Main:example
parties:
- Betina_Beakley
- Scrooge_McDuck
- OPERATOR
version: 1.0.0
dependencies:
- daml-prim
- daml-stdlib
- daml-script
sandbox-options:
- --static-time

View File

@ -1,317 +0,0 @@
-- Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
{-# LANGUAGE ApplicativeDo #-}
module Main where
import DA.Date
import DA.Time
import Daml.Script
--------------------------------------------------------------------------------
-- Basic example
--------------------------------------------------------------------------------
template RightOfUseAgreement
with
landlord: Party
tenant: Party
address: Text
expirationDate: Date
where
signatory landlord
signatory tenant
key (tenant, address): (Party, Text)
maintainer key._1
agreement
show landlord <> " promises to let " <>
show tenant <> " live at " <> show address <>
" until " <> show expirationDate
template RightOfUseOffer
with
landlord: Party
tenant: Party
address: Text
expirationDate: Date
where
signatory landlord
observer tenant
choice Accept: ContractId RightOfUseAgreement
controller tenant
do
create RightOfUseAgreement with landlord; tenant; address; expirationDate
--------------------------------------------------------------------------------
-- Helpers for creating lots of contracts
--------------------------------------------------------------------------------
{-
explode10 : Text -> List Text
explode10 t = [
t <> "0",
t <> "1",
t <> "2",
t <> "3",
t <> "4",
t <> "5",
t <> "6",
t <> "7",
t <> "8",
t <> "9"
]
explode10s : List Text -> List Text
explode10s ts = concat (map explode10 ts)
oneString: List Text = cons "" nil
manyStrings = explode10s (explode10s (explode10s oneString))
pass : RelTime -> Script ()
pass d = do
t <- getTime
setTime (addRelTime t d)
createCounter : Party -> Text -> Script ()
createCounter p i = do
submit p do (createCmd Counter with index = i; owner = p)
pass 1
return ()
createManyCounters : Party -> Script ()
createManyCounters p = do
mapS (\i -> createCounter p i) manyStrings
return ()
-}
template Counter
with
index: Text
owner: Party
where
signatory owner
choice CArchive: ()
controller owner
do
return ()
choice Recreate: ContractId Counter
controller owner
do
create Counter with index; owner
createCounters : Int -> Text -> Party -> Commands ()
createCounters 0 t p = do
createCmd Counter with index = t; owner = p
pure ()
createCounters i t p = do
createCounters (i-1) (t <> "0") p
createCounters (i-1) (t <> "1") p
createCounters (i-1) (t <> "2") p
createCounters (i-1) (t <> "3") p
createCounters (i-1) (t <> "4") p
createCounters (i-1) (t <> "5") p
createCounters (i-1) (t <> "6") p
createCounters (i-1) (t <> "7") p
createCounters (i-1) (t <> "8") p
createCounters (i-1) (t <> "9") p
pure ()
-- Creates 10^i contracts
createManyCounters : Int -> Party -> Script ()
createManyCounters i p = submit p $ createCounters i "" p
--------------------------------------------------------------------------------
-- Types
--------------------------------------------------------------------------------
-- Stdlib types
data UserMaybe a = UserNothing | UserJust a deriving (Eq, Show);
data UserEither a b = UserLeft a | UserRight b deriving (Eq, Show);
-- Nested types
data Record0 a = Record0
with
fprimitive: a
flistR: [Record1 a]
flistV: [UserMaybe a]
flistL: [[a]]
fvariantR: UserMaybe (Record1 a)
fvariantV: UserMaybe (UserMaybe a)
fvariantL: UserMaybe ([a])
frecord: Record1 a
deriving (Eq, Show)
data Record1 a = Record1
with
fprimitive: a
flistR: [Record2 a]
flistV: [UserMaybe a]
flistL: [[a]]
fvariantR: UserMaybe (Record2 a)
fvariantV: UserMaybe (UserMaybe a)
fvariantL: UserMaybe ([a])
frecord: Record2 a
deriving (Eq, Show)
data Record2 a = Record2
with
fprimitive: a
flistR: [Record3 a]
flistV: [UserMaybe a]
flistL: [[a]]
fvariantR: UserMaybe (Record3 a)
fvariantV: UserMaybe (UserMaybe a)
fvariantL: UserMaybe ([a])
frecord: Record3 a
deriving (Eq, Show)
data Record3 a = Record3
with
fprimitive: a
deriving (Eq, Show)
-- Example values
valDate = date 1970 Jan 1
valTime = time valDate 0 0 0
valListT: [Text] = ["A"]
valMaybeT: UserMaybe Text = UserJust "A"
valRecord3T: Record3 Text = Record3 with
fprimitive = "A"
valRecord2T: Record2 Text = Record2 with
fprimitive = "A"
flistR = [valRecord3T]
flistV = [valMaybeT]
flistL = [valListT]
fvariantR = UserJust valRecord3T
fvariantV = UserJust valMaybeT
fvariantL = UserJust valListT
frecord = valRecord3T
valRecord1T: Record1 Text = Record1 with
fprimitive = "A"
flistR = [valRecord2T]
flistV = [valMaybeT]
flistL = [valListT]
fvariantR = UserJust valRecord2T
fvariantV = UserJust valMaybeT
fvariantL = UserJust valListT
frecord = valRecord2T
valRecord0T: Record0 Text = Record0 with
fprimitive = "A"
flistR = [valRecord1T]
flistV = [valMaybeT]
flistL = [valListT]
fvariantR = UserJust valRecord1T
fvariantV = UserJust valMaybeT
fvariantL = UserJust valListT
frecord = valRecord1T
template ParameterShowcase
with
foperator: Party
finteger: Int
fdecimal: Decimal
ftext: Text
fbool: Bool
ftime: Time
--freltime: RelTime
fdate: Date
fnestedTypes: Record0 Text
fintegerList: [Int]
fmaybeInteger: UserMaybe Int
foptionInteger: Optional Int
where
signatory foperator
choice SCArchive: ()
controller foperator
do
return ()
--------------------------------------------------------------------------------
-- Non-serializable types
--------------------------------------------------------------------------------
-- Function
data UnserializableF = UnserializableF {f : Int -> Int}
-- Tuple
data UnserializableT = UnserializableT {x : (Int, Int)}
-- Forall (Daml 1.2)
data Box = Box (forall a. a -> a)
--------------------------------------------------------------------------------
-- Recursive types
--------------------------------------------------------------------------------
data RTree a = Leaf a | Node {left: RTree a, right: RTree a} deriving (Eq, Show)
data RList a = Nil | Cons {value: a, next: RList a} deriving (Eq, Show)
template TList
with
owner: Party
value: RList Text
where
signatory owner
choice Append: ContractId TList
with
newValue : Text
controller owner
do
create TList with owner; value = Cons {value=newValue, next=value}
createRList : Int -> Text -> RList Text
createRList 0 t = Nil
createRList i t = Cons {value=(t<>show i), next=(createRList (i-1) t)}
--------------------------------------------------------------------------------
-- Script
--------------------------------------------------------------------------------
example = do
betina_Beakley <- allocatePartyWithHint "Betina_Beakley" (PartyIdHint "Betina_Beakley")
scrooge_McDuck <- allocatePartyWithHint "Scrooge_McDuck" (PartyIdHint "Scrooge_McDuck")
operator <- allocatePartyWithHint "OPERATOR" (PartyIdHint "OPERATOR")
-- Betina_Beakley offers Scrooge_McDuck room at McDuck Manor
offer <- submit betina_Beakley do
createCmd RightOfUseOffer with
landlord = betina_Beakley
tenant = scrooge_McDuck
address = "McDuck Manor, Duckburg"
expirationDate = valDate
-- Scrooge_McDuck accepts the offer, which creates a RightOfUseAgreement
submit scrooge_McDuck do exerciseCmd offer Accept
-- Operator creates 10^3 contracts
createManyCounters 3 operator
-- Complex contract
submit operator do
createCmd ParameterShowcase with
foperator = operator
finteger = 10
fdecimal = 10.0
ftext = "ABC"
fbool = True
ftime = valTime
--freltime = seconds 120
fdate = valDate
fnestedTypes = valRecord0T
fintegerList = [1, 2, 3]
fmaybeInteger = UserJust 10
foptionInteger = Some 10
-- Long recursive value
submit operator do
createCmd TList with
owner = operator
value = createRList 45 "X"

View File

@ -1,11 +0,0 @@
users {
Betina_Beakley {
party=Betina_Beakley
}
Scrooge_McDuck {
party=Scrooge_McDuck
}
OPERATOR {
party=OPERATOR
}
}

View File

@ -1,8 +0,0 @@
pekko {
loggers = ["org.apache.pekko.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
logging-filter = "org.apache.pekko.event.slf4j.Slf4jLoggingFilter"
}
ui.backend {
}

View File

@ -1,151 +0,0 @@
<!--
* LICENSE AGREEMENT For GraphiQL software
*
* Facebook, Inc. (“Facebook”) owns all right, title and interest, including all
* intellectual property and other proprietary rights, in and to the GraphiQL
* software. Subject to your compliance with these terms, you are hereby granted a
* non-exclusive, worldwide, royalty-free copyright license to (1) use and copy the
* GraphiQL software; and (2) reproduce and distribute the GraphiQL software as
* part of your own software (“Your Software”). Facebook reserves all rights not
* expressly granted to you in this license agreement.
*
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. IN NO
* EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICES, DIRECTORS OR EMPLOYEES BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* You will include in Your Software (e.g., in the file(s), documentation or other
* materials accompanying your software): (1) the disclaimer set forth above; (2)
* this sentence; and (3) the following copyright notice:
*
* Copyright (c) 2015, Facebook, Inc. All rights reserved.
-->
<!DOCTYPE html>
<html>
<head>
<style>
body {
height: 100%;
margin: 0;
width: 100%;
overflow: hidden;
}
#graphiql {
height: 100vh;
}
</style>
<link rel="stylesheet" href="//cdn.jsdelivr.net/graphiql/0.8.0/graphiql.css" />
<script src="//cdn.jsdelivr.net/es6-promise/4.0.5/es6-promise.auto.min.js"></script>
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/react/15.3.2/react.min.js"></script>
<script src="//cdn.jsdelivr.net/react/15.3.2/react-dom.min.js"></script>
<script src="//cdn.jsdelivr.net/graphiql/0.8.0/graphiql.min.js"></script>
</head>
<body>
<div id="graphiql">Loading...</div>
<script>
/**
* This GraphiQL example illustrates how to use some of GraphiQL's props
* in order to enable reading and updating the URL parameters, making
* link sharing of queries a little bit easier.
*
* This is only one example of this kind of feature, GraphiQL exposes
* various React params to enable interesting integrations.
*/
// Parse the search string to get url parameters.
var search = window.location.search;
var parameters = {};
search.substr(1).split('&').forEach(function (entry) {
var eq = entry.indexOf('=');
if (eq >= 0) {
parameters[decodeURIComponent(entry.slice(0, eq))] =
decodeURIComponent(entry.slice(eq + 1));
}
});
// if variables was provided, try to format it.
if (parameters.variables) {
try {
parameters.variables =
JSON.stringify(JSON.parse(parameters.variables), null, 2);
} catch (e) {
// Do nothing, we want to display the invalid JSON as a string, rather
// than present an error.
}
}
// When the query and variables string is edited, update the URL bar so
// that it can be easily shared
function onEditQuery(newQuery) {
parameters.query = newQuery;
updateURL();
}
function onEditVariables(newVariables) {
parameters.variables = newVariables;
updateURL();
}
function onEditOperationName(newOperationName) {
parameters.operationName = newOperationName;
updateURL();
}
function updateURL() {
var newSearch = '?' + Object.keys(parameters).filter(function (key) {
return Boolean(parameters[key]);
}).map(function (key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(parameters[key]);
}).join('&');
history.replaceState(null, null, newSearch);
}
// Defines a GraphQL fetcher using the fetch API.
function graphQLFetcher(graphQLParams) {
return fetch('/api/graphql', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(graphQLParams),
credentials: 'include',
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}
// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
query: parameters.query,
variables: parameters.variables,
operationName: parameters.operationName,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName
}),
document.getElementById('graphiql')
);
</script>
</body>
</html>

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
<target>System.err</target>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>navigator.log</file>
<param name="Append" value="false" />
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %mdc%n</pattern>
</encoder>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
</appender>
<!-- Some important messages are written to stderr -->
<logger name="user-facing-logs" level="INFO" additivity="true">
<appender-ref ref="STDERR"/>
</logger>
<!-- All log messages are written asynchronously to a file -->
<root level="INFO">
<appender-ref ref="ASYNC"/>
</root>
</configuration>

View File

@ -1 +0,0 @@
org.slf4j.simpleLogger.defaultLogLevel = debug

View File

@ -1,17 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import spray.json.{DefaultJsonProtocol, RootJsonFormat}
case class ApplicationInfo(
id: String,
name: String,
version: String,
)
trait ApplicationInfoJsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val applicationInfoFormat: RootJsonFormat[ApplicationInfo] = jsonFormat3(ApplicationInfo)
}

View File

@ -1,98 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import org.apache.pekko.actor.ActorRef
import org.apache.pekko.http.scaladsl.model.StatusCode
import org.apache.pekko.http.scaladsl.model.StatusCodes._
import com.daml.navigator.graphql._
import com.daml.navigator.graphql.SprayMarshallers._
import com.daml.navigator.model.PartyState
import com.daml.navigator.store.Store.StoreException
import com.typesafe.scalalogging.LazyLogging
import sangria.ast.Document
import sangria.execution._
import sangria.parser.QueryParser
import sangria.renderer.SchemaRenderer
import sangria.schema.Schema
import spray.json._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
case class ParseResult(ast: Document, operationName: Option[String], variables: JsValue)
/** Provides a way of executing GraphQL queries.
*/
trait GraphQLHandler {
def schema: Schema[GraphQLContext, Unit]
def parse(request: String): Try[ParseResult]
def parse(request: JsValue): Try[ParseResult]
def executeQuery(parsed: ParseResult, party: PartyState): Future[(StatusCode, JsValue)]
def renderSchema: String
}
object GraphQLHandler {
type ParseQuery = JsValue => Try[ParseResult]
type ExecuteQuery = (ParseResult, PartyState) => Future[(StatusCode, JsValue)]
type CustomEndpoints = Set[CustomEndpoint[_]]
}
case class DefaultGraphQLHandler(
customEndpoints: GraphQLHandler.CustomEndpoints,
platformStore: Option[ActorRef],
)(implicit
executionContext: ExecutionContext
) extends GraphQLHandler
with LazyLogging {
def schema: Schema[GraphQLContext, Unit] = new GraphQLSchema(customEndpoints).QuerySchema
def parse(request: String): Try[ParseResult] =
Try(request.parseJson).flatMap(parse)
def parse(request: JsValue): Try[ParseResult] =
for {
fields <- Try(request.asJsObject.fields)
JsString(query) <- Try(fields("query"))
operationName = fields.get("operationName").collect { case JsString(value) =>
value
}
vars: JsValue = fields.get("variables") match {
case Some(obj: JsObject) => obj
case _ => JsObject.empty
}
ast <- QueryParser.parse(query)
} yield ParseResult(ast, operationName, vars)
def executeQuery(parsed: ParseResult, party: PartyState): Future[(StatusCode, JsValue)] = {
platformStore.fold[Future[(StatusCode, JsValue)]](
Future.successful(InternalServerError -> JsString("Platform store not available"))
)(store => {
val context = GraphQLContext(party, store)
Executor
.execute(
schema,
parsed.ast,
context,
variables = parsed.variables,
operationName = parsed.operationName,
exceptionHandler = ExceptionHandler { case (_, StoreException(message)) =>
HandledException(message)
},
)
.map(OK -> _)
.recover {
case error: QueryAnalysisError =>
logger.warn(s"GraphQL analysis error ${error.getMessage}.")
BadRequest -> error.resolveError
case error: ErrorWithResolver =>
logger.error("Failed to execute GraphQL query", error)
InternalServerError -> error.resolveError
}
})
}
def renderSchema: String = SchemaRenderer.renderSchema(schema)
}

View File

@ -1,123 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit
import org.apache.pekko.actor.ActorRef
import org.apache.pekko.pattern.ask
import org.apache.pekko.util.Timeout
import com.daml.navigator.config.Arguments
import com.daml.navigator.store.Store._
import spray.json._
import DefaultJsonProtocol._
import com.daml.navigator.store.Store
import com.daml.navigator.time.TimeProviderType
import scala.concurrent.{ExecutionContext, Future}
/** Provides a way of getting debug information about the application state.
* The information is returned in an opaque JSON format,
* and is useful for displaying debug information in the frontend.
*/
trait InfoHandler {
def getInfo: Future[JsValue]
}
case class DefaultInfoHandler(arguments: Arguments, platformStore: ActorRef)(implicit
executionContext: ExecutionContext
) extends InfoHandler {
private case class Info(arguments: Arguments, appInfo: Either[String, ApplicationStateInfo])
private implicit object argumentsWriter extends RootJsonWriter[Arguments] {
override def write(obj: Arguments): JsValue = JsObject(
"port" -> obj.port.toJson,
"assets" -> obj.assets.toJson,
"time" -> TimeProviderType.write(obj.time).toJson,
"configFile" -> obj.configFile.map(p => p.toString).toJson,
"tlsConfig" -> obj.tlsConfig
.fold[JsValue](JsNull)(c =>
JsObject(
"enabled" -> c.enabled.toJson,
"privateKeyFile" -> c.privateKeyFile.fold[JsValue](JsNull)(f => JsString(f.toString)),
"certChainFile" -> c.certChainFile.fold[JsValue](JsNull)(f => JsString(f.toString)),
"trustCollectionFile" -> c.trustCollectionFile.fold[JsValue](JsNull)(f =>
JsString(f.toString)
),
)
),
)
}
private implicit object actorInfoWriter extends RootJsonWriter[PartyActorInfo] {
override def write(obj: PartyActorInfo): JsValue = obj match {
case _: PartyActorStarting => JsString("Starting")
case _: PartyActorStarted => JsString("Started")
case info: PartyActorFailed => JsString(s"Failed: ${info.error.getMessage}")
}
}
private implicit object actorResponseWriter extends RootJsonWriter[PartyActorResponse] {
override def write(obj: PartyActorResponse): JsValue = obj match {
case PartyActorRunning(info) => info.toJson
case Store.PartyActorUnresponsive => JsString("Unresponsive")
}
}
private implicit object appInfoWriter extends RootJsonWriter[ApplicationStateInfo] {
override def write(obj: ApplicationStateInfo): JsValue = obj match {
case info: ApplicationStateConnecting =>
JsObject(
"status" -> JsString("Connecting"),
"platformHost" -> info.platformHost.toJson,
"platformPort" -> info.platformPort.toJson,
"tls" -> info.tls.toJson,
"applicationId" -> info.applicationId.toJson,
)
case info: ApplicationStateConnected =>
JsObject(
"status" -> JsString("Connected"),
"platformHost" -> info.platformHost.toJson,
"platformPort" -> info.platformPort.toJson,
"tls" -> info.tls.toJson,
"applicationId" -> info.applicationId.toJson,
"ledgerId" -> info.ledgerId.toJson,
"ledgerTime" -> JsObject(
"time" -> DateTimeFormatter.ISO_INSTANT
.format(info.ledgerTime.time.getCurrentTime)
.toJson,
"type" -> TimeProviderType.write(info.ledgerTime.`type`).toJson,
),
"partyActors" -> JsObject(
info.partyActors.map { case (p, s) => p -> s.toJson }.toMap
),
)
case info: ApplicationStateFailed =>
JsObject(
"status" -> JsString("Failed"),
"platformHost" -> info.platformHost.toJson,
"platformPort" -> info.platformPort.toJson,
"tls" -> info.tls.toJson,
"applicationId" -> info.applicationId.toJson,
"error" -> info.error.toString.toJson,
)
}
}
private implicit object infoWriter extends RootJsonWriter[Info] {
override def write(obj: Info): JsValue = JsObject(
"arguments" -> obj.arguments.toJson,
"appInfo" -> obj.appInfo.fold(_.toJson, _.toJson),
)
}
private def getStoreInfo: Future[ApplicationStateInfo] = {
implicit val actorTimeout: Timeout = Timeout(5, TimeUnit.SECONDS)
(platformStore ? GetApplicationStateInfo).mapTo[ApplicationStateInfo]
}
def getInfo: Future[JsValue] = {
getStoreInfo
.map(i => Info(arguments, Right(i)))
.recover[Info] { case e: Throwable => Info(arguments, Left(e.getMessage)) }
.map(i => i.toJson)
}
}

View File

@ -1,43 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import java.nio.file.{Files, Paths}
import java.util.UUID
import org.apache.pekko.http.scaladsl.model.StatusCodes
import org.apache.pekko.http.scaladsl.server.Directives._
import org.apache.pekko.http.scaladsl.server.Route
import com.daml.buildinfo.BuildInfo
object NavigatorBackend extends UIBackend {
private val configFile = "frontend-config.js"
override def customEndpoints: Set[CustomEndpoint[_]] = Set()
override def customRoutes: List[Route] = List(frontendConfigRoute)
override def applicationInfo: ApplicationInfo = ApplicationInfo(
id = s"Navigator-${UUID.randomUUID().toString}",
name = "Navigator",
version = BuildInfo.Version,
)
override def banner: Option[String] =
Some(
raw""" _ __ _ __
| / |/ /__ __ __(_)__ ____ _/ /____ ____
| / / _ `/ |/ / / _ `/ _ `/ __/ _ \/ __/
|/_/|_/\_,_/|___/_/\_, /\_,_/\__/\___/_/
| /___/
|Version """.stripMargin + applicationInfo.version
)
/** Frontend config file */
private val frontendConfigRoute: Route = {
path("api" / "config") {
if (Files.exists(Paths.get(configFile)))
getFromFile(configFile)
else
complete(StatusCodes.NotFound)
}
}
}

View File

@ -1,51 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import scala.reflect.ClassTag
import scala.util.{Failure, Success, Try}
/** A reading failure */
final case class ReadFailed(from: String)
/** A simple `Read` typeclass */
sealed trait Read[To] {
/** Convert `from` to a value of type `To`
*
* @return `Right` wrapping an instance of `To` in case of success in reading, a `Left` wrapping a `ReadFailed`
* otherwise
*/
def from(from: String): Either[ReadFailed, To]
}
object Read {
def apply[To](implicit readTo: Read[To]): Read[To] = readTo
/** Utility function equivalent to calling `new Read` but more convenient */
def fromFunction[To](f: String => Either[ReadFailed, To]): Read[To] =
new Read[To] {
override def from(from: String): Either[ReadFailed, To] = f(from)
}
/** A failure while reading that wraps the failure inside a `Left` */
def fail[To](implicit classTag: ClassTag[To]): Either[ReadFailed, To] =
Left(ReadFailed(classTag.runtimeClass.getSimpleName))
/** Catches the exceptions thrown while read a value of type `To` from a `String`
*/
def fromUnsafeFunction[To](f: String => To)(implicit classTag: ClassTag[To]): Read[To] =
fromFunction[To] { str =>
Try(f(str)) match {
case Success(str) => Right(str)
case Failure(_) => Read.fail[To](classTag)
}
}
implicit val readString: Read[String] = Read.fromFunction[String](str => Right(str))
implicit val readBoolean: Read[Boolean] = Read.fromUnsafeFunction[Boolean](_.toBoolean)
implicit val readInt: Read[Int] = Read.fromUnsafeFunction[Int](_.toInt)
implicit val readFloat: Read[Float] = Read.fromUnsafeFunction[Float](_.toFloat)
implicit val readDouble: Read[Double] = Read.fromUnsafeFunction[Double](_.toDouble)
}

View File

@ -1,282 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import com.daml.navigator.model.PartyState
import scalaz.Tag
import scala.collection.immutable
import spray.json._
import scala.reflect.ClassTag
final case class User(
id: String,
party: PartyState,
// TODO: where is `role` used? frontend has some references, but doesn't seem to impact anything?
role: Option[String] = None,
canAdvanceTime: Boolean = true,
)
sealed trait SignInError
case object InvalidCredentials extends SignInError
case object NotConnected extends SignInError
case object Unresponsive extends SignInError
case object Unknown extends SignInError
sealed abstract class Status
final case class Session(user: User) extends Status
final case class SignIn(method: SignInMethod, error: Option[SignInError] = None) extends Status
sealed abstract class SignInMethod
final case class SignInSelect(userIds: Set[String]) extends SignInMethod
case class LoginRequest(userId: String)
/** Maintains session information for browser clients. */
object Session {
private var sessions: immutable.Map[String, Session] = immutable.Map()
def current(sessionId: String): Option[Session] = sessions.get(sessionId)
def open(
sessionId: String,
userId: String,
userRole: Option[String],
state: PartyState,
): Session = {
val user = Session(User(userId, state, userRole))
sessions += sessionId -> user
user
}
def close(sessionId: String): Unit = {
sessions -= sessionId
}
// method for debugging and testing purposes
private[navigator] def clean(): Unit = synchronized {
this.sessions = immutable.Map()
}
}
object SessionJsonProtocol extends DefaultJsonProtocol {
val typeFieldName = "type"
val userFieldName = "user"
val usersFieldName = "users"
val methodFieldName = "method"
val errorFieldName = "error"
val idFieldName = "id"
val roleFieldName = "role"
val partyFieldName = "party"
val canAdvanceTimeFieldName = "canAdvanceTime"
val sessionType = JsString("session")
val selectType = JsString("select")
val signInType = JsString("sign-in")
implicit object partyWriter extends RootJsonWriter[PartyState] {
override def write(obj: PartyState): JsValue = Tag.unwrap(obj.name).toJson
}
implicit object userWriter extends RootJsonWriter[User] {
override def write(obj: User): JsValue =
obj.role match {
case None =>
JsObject(
idFieldName -> obj.id.toJson,
partyFieldName -> obj.party.toJson,
canAdvanceTimeFieldName -> obj.canAdvanceTime.toJson,
)
case Some(role) =>
JsObject(
idFieldName -> obj.id.toJson,
roleFieldName -> role.toJson,
partyFieldName -> obj.party.toJson,
canAdvanceTimeFieldName -> obj.canAdvanceTime.toJson,
)
}
}
implicit object loginRequestFormat extends RootJsonFormat[LoginRequest] {
override def write(obj: LoginRequest): JsValue =
JsObject("userId" -> obj.userId.toJson)
override def read(json: JsValue): LoginRequest = {
val obj =
json.asJsObject(errorMsg =
s"LoginRequest should be an object but ${json.getClass.getCanonicalName} found"
)
obj.fields.get("userId") match {
case Some(JsString(userId)) => LoginRequest(userId)
case _ =>
throw DeserializationException(
s"LoginRequest should contain a field called 'userId' of type String, got $json"
)
}
}
}
implicit object sessionWriter extends RootJsonWriter[Session] {
override def write(obj: Session): JsValue = {
JsObject(
typeFieldName -> sessionType,
userFieldName -> obj.user.toJson,
)
}
}
implicit object statusWriter extends RootJsonWriter[Status] {
override def write(obj: Status): JsValue =
obj match {
case session: Session => sessionWriter.write(session)
case signIn: SignIn => signInFormat.write(signIn)
}
}
implicit object signInSelectJsonFormat extends RootJsonFormat[SignInSelect] {
def fromFields(fields: Map[String, JsValue]): SignInSelect =
fields.get(usersFieldName) match {
case None =>
throw DeserializationException(
s"Cannot decode an instance of " +
s"${SignInSelect.getClass.getCanonicalName} because the field $usersFieldName is missing"
)
case Some(rawUserIds) =>
SignInSelect(userIds = rawUserIds.convertTo[Set[String]])
}
override def read(json: JsValue): SignInSelect =
deserialize2(json, selectType)(fromFields)
override def write(obj: SignInSelect): JsValue =
JsObject(
typeFieldName -> selectType,
usersFieldName -> obj.userIds.toJson,
)
}
implicit object signInMethodFormat extends JsonFormat[SignInMethod] {
override def read(json: JsValue): SignInMethod =
deserialize(json) {
case (`selectType`, fields) =>
signInSelectJsonFormat.fromFields(fields)
case _ => throw DeserializationException(s"Unknown sign in method")
}
override def write(obj: SignInMethod): JsValue = obj match {
case select: SignInSelect =>
select.toJson
}
}
implicit object signInFormat extends RootJsonFormat[SignIn] {
override def write(obj: SignIn): JsValue =
obj.error.fold(
JsObject(
typeFieldName -> signInType,
methodFieldName -> obj.method.toJson,
)
)(error =>
JsObject(
typeFieldName -> signInType,
methodFieldName -> obj.method.toJson,
errorFieldName -> JsString(error match {
case InvalidCredentials => "invalid-credentials"
case NotConnected => "not-connected"
case Unresponsive => "unresponsive"
case Unknown => "unknown-error"
}),
)
)
def fromFields(fields: Map[String, JsValue]): SignIn = {
(fields.get(methodFieldName), fields.get(errorFieldName)) match {
case (None, _) =>
throw DeserializationException(
s"Cannot decode an instance of " +
s"${SignIn.getClass.getCanonicalName} because the field $methodFieldName is missing"
)
case (Some(rawMethod), maybeError) =>
val method = signInMethodFormat.read(rawMethod)
SignIn(
method,
maybeError.map {
case JsString("invalid-credentials") => InvalidCredentials
case JsString("not-connected") => NotConnected
case JsString("unresponsive") => Unresponsive
case _ => Unknown
},
)
}
}
override def read(json: JsValue): SignIn =
deserialize2(json, signInType)(fromFields)
}
/** Convert a json object following our encoding to an instance of `T` by using a function from the 'type' and the
* fields in the json object to the type `T`. Throws `DeserializationException` if the conversion is not possible.
*
* Our encoding of objects follows the rules:
* - the encoded json must be a `JsObject`
* - the encoded json must have a field `'type'` with the name of the type encoded. `T` must match it.
*/
private def deserialize[T](
json: JsValue
)(f: (JsString, Map[String, JsValue]) => T)(implicit tag: ClassTag[T]): T = {
val jsObject =
json
.asJsObject(errorMsg =
s"JSON object required to deserialize an instance of ${tag.runtimeClass.getCanonicalName}"
)
val typeValue = fieldOrThrow(jsObject, typeFieldName)(jsValueAsJsString)
f(typeValue, jsObject.fields)
}
/** Similar to [[deserialize]] but checks that the fields 'type' has the expected type.
*/
private def deserialize2[T](json: JsValue, typeExpected: JsString)(
f: Map[String, JsValue] => T
)(implicit tag: ClassTag[T]): T = {
deserialize[T](json) { case (typeStr, fields) =>
if (typeExpected != typeStr) {
throw new DeserializationException(
s"Cannot deserialize an instance of ${tag.runtimeClass.getCanonicalName} because the wrong type" +
s"${typeStr.prettyPrint} has been found instead of the expected type ${typeExpected.prettyPrint}"
)
} else {
f(fields)
}
}
}
/** Tries to retrieve a field from a `jsObject` and convert it to `T` if `f` is given. Throws
* `DeserializationException` if the `jsObject` doesn't contain the field
*/
private def fieldOrThrow[T](jsObject: JsObject, fieldName: String)(
f: JsValue => T
)(implicit tag: ClassTag[T]): T =
jsObject.fields
.get(fieldName)
.map(f)
.getOrElse(
throw DeserializationException(
s"JSON object with a '$fieldName' field is required to deserialize an " +
s"instance of ${tag.runtimeClass.getCanonicalName}. jsObject found ${jsObject.compactPrint}"
)
)
/** Convert the jsValue to a `JsString` if jsValue is a `JsString`, otherwise throws `DeserializationException`
*/
private def jsValueAsJsString(jsValue: JsValue): JsString =
jsValue match {
case js: JsString => js
case _ =>
throw DeserializationException(
s"JsString expected, but found ${jsValue.compactPrint} of type " +
jsValue.getClass.getCanonicalName
)
}
}

View File

@ -1,403 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import java.nio.file.{Files, Path, Paths}
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.stream.Collectors
import org.apache.pekko.actor.{ActorSystem, Cancellable}
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.model.HttpHeader
import org.apache.pekko.http.scaladsl.model.StatusCodes._
import org.apache.pekko.http.scaladsl.model.headers.CacheDirectives.{
`max-age`,
`no-cache`,
immutableDirective,
}
import org.apache.pekko.http.scaladsl.model.headers.{Cookie, EntityTag, HttpCookie, `Cache-Control`}
import org.apache.pekko.http.scaladsl.server.Directives._
import org.apache.pekko.http.scaladsl.server.Route
import org.apache.pekko.http.scaladsl.settings.RoutingSettings
import org.apache.pekko.http.scaladsl.settings.ServerSettings
import org.apache.pekko.pattern.ask
import org.apache.pekko.util.Timeout
import com.daml.grpc.GrpcException
import com.daml.navigator.SessionJsonProtocol._
import com.daml.navigator.config._
import com.daml.navigator.graphql.GraphQLContext
import com.daml.navigator.graphqless.GraphQLObject
import com.daml.navigator.model.{Ledger, PackageRegistry, PartyState}
import com.daml.navigator.store.Store
import com.daml.navigator.store.Store._
import com.daml.navigator.store.platform.PlatformStore
import com.daml.scalautil.Statement.discard
import com.typesafe.scalalogging.LazyLogging
import org.slf4j.LoggerFactory
import sangria.schema._
import spray.json._
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.util.control.NonFatal
import scala.util.{Failure, Success, Try}
/** Base abstract class for UI backends.
*
* A new UI backend can be implemented by extending [[UIBackend]] and by providing
* the [[customEndpoints]], [[customRoutes]], [[applicationInfo]] definitions.
*/
abstract class UIBackend extends LazyLogging with ApplicationInfoJsonSupport {
def customEndpoints: Set[CustomEndpoint[_]]
def customRoutes: List[Route]
def applicationInfo: ApplicationInfo
def banner: Option[String] = None
private[this] def userFacingLogger = LoggerFactory.getLogger("user-facing-logs")
/** Allow subclasses to customize config file name, but don't require it
* (supply a default)
*/
def defaultConfigFile: Path = Paths.get("ui-backend.conf")
private[navigator] def getRoute(
system: ActorSystem,
arguments: Arguments,
graphQL: GraphQLHandler,
info: InfoHandler,
getAppState: () => Future[ApplicationStateInfo],
): Route = {
def openSession(userId: String, userRole: Option[String], state: PartyState): Route = {
val sessionId = UUID.randomUUID().toString
setCookie(HttpCookie("session-id", sessionId, path = Some("/"))) {
complete(Session.open(sessionId, userId, userRole, state))
}
}
def findSession(httpHeader: HttpHeader): Option[(String, Session)] = httpHeader match {
case Cookie(cookies) =>
cookies
.filter(_.name == "session-id")
.flatMap(cookiePair =>
Session.current(cookiePair.value).map(cookiePair.value -> _).toList
)
.headOption
case _ =>
None
}
def signIn(): Route =
onSuccess(getAppState()) {
case ApplicationStateConnecting(_, _, _, _) =>
complete(SignIn(SignInSelect(Set.empty), Some(NotConnected)))
case ApplicationStateConnected(_, _, _, _, _, _, partyActors) =>
complete(SignIn(SignInSelect(partyActors.keySet), None))
case ApplicationStateFailed(_, _, _, _, _) =>
complete(SignIn(SignInSelect(Set.empty), Some(Unknown)))
}
// A resource with content that may change.
// Users can quickly switch between Navigator versions, so we don't want to cache this resource for any amount of time.
// Note: The server still uses E-Tags, so repeated fetches will complete with a "304: Not Modified".
def mutableResource = respondWithHeader(`Cache-Control`(`no-cache`))
// Add an ETag with value equal to the version used to build the application.
// Use as a cheap ETag for resources that may change between builds.
def versionETag = conditional(EntityTag(applicationInfo.version))
// A resource that is never going to change. Its path must use some kind of content hash.
// Such a resource may be cached indefinitely.
def immutableResource =
respondWithHeaders(`Cache-Control`(`max-age`(31536000), immutableDirective))
val noFileGetConditional = RoutingSettings(system).withFileGetConditional(false)
def withoutFileETag = withSettings(noFileGetConditional)
optionalHeaderValue(findSession) { session =>
{
// Custom routes
customRoutes.foldLeft[Route](reject)(_ ~ _) ~
// Built-in API routes
pathPrefix("api") {
path("about") {
complete(applicationInfo)
} ~
path("session"./) {
get {
session match {
case Some((_, session)) => complete(session)
case None => signIn()
}
} ~
post {
entity(as[LoginRequest]) { request =>
onSuccess(getAppState()) {
case ApplicationStateConnecting(_, _, _, _) =>
complete(SignIn(SignInSelect(Set.empty), Some(NotConnected)))
case ApplicationStateConnected(_, _, _, _, _, _, partyActors) =>
partyActors.get(request.userId) match {
case Some(resp) =>
resp match {
case PartyActorRunning(info) =>
openSession(request.userId, info.state.userRole, info.state)
case Store.PartyActorUnresponsive =>
complete(
SignIn(SignInSelect(partyActors.keySet), Some(Unresponsive))
)
}
case None =>
logger.error(
s"Attempt to signin with non-existent user ${request.userId}"
)
complete(
SignIn(SignInSelect(partyActors.keySet), Some(InvalidCredentials))
)
}
case ApplicationStateFailed(
_,
_,
_,
_,
GrpcException.PERMISSION_DENIED(),
) =>
logger.warn("Attempt to sign in without valid token")
complete(SignIn(SignInSelect(Set.empty), Some(InvalidCredentials)))
case ApplicationStateFailed(_, _, _, _, _) =>
complete(SignIn(SignInSelect(Set.empty), Some(Unknown)))
}
}
} ~
delete {
deleteCookie("session-id", path = "/") {
session match {
case Some((sessionId, sessionUser)) =>
Session.close(sessionId)
logger.info(s"Logged out user '${sessionUser.user.id}'")
case None =>
logger.error("Cannot delete session without session-id, cookie not found")
}
signIn()
}
}
} ~
path("info") {
mutableResource { complete(info.getInfo) }
} ~
path("graphql") {
get {
versionETag { mutableResource { getFromResource("graphiql.html") } }
} ~
post {
authorize(session.isDefined) {
entity(as[JsValue]) { request =>
logger.debug(s"Handling GraphQL query: $request")
graphQL.parse(request) match {
case Success(parsed) =>
complete(graphQL.executeQuery(parsed, session.get._2.user.party))
case Failure(error) =>
logger.debug("Cannot execute query: " + error.getMessage)
complete((BadRequest, JsObject("error" -> JsString(error.getMessage))))
}
}
}
}
}
} ~ {
arguments.assets match {
case None =>
// Serve assets from resource directory at /assets
pathPrefix("assets") {
// Webpack makes sure all files use content hashing
immutableResource { withoutFileETag { getFromResourceDirectory("frontend") } }
} ~
// Serve index on root and anything else to allow History API to behave
// as expected on reloading.
versionETag {
mutableResource { withoutFileETag { getFromResource("frontend/index.html") } }
}
case Some(folder) =>
// Serve assets under /assets
pathPrefix("assets") {
mutableResource { getFromDirectory(folder) }
} ~
// Serve index on root and anything else to allow History API to behave
// as expected on reloading.
mutableResource { getFromFile(folder + "/index.html") }
}
}
}
}
}
// Factored out for integration tests
private[navigator] def setup(arguments: Arguments, config: Config)(implicit
system: ActorSystem
) = {
import system.dispatcher
// Read from the access token file or crash
val token =
arguments.accessTokenFile.map { path =>
try {
Files.readAllLines(path).stream.collect(Collectors.joining("\n"))
} catch {
case NonFatal(e) =>
throw new RuntimeException(s"Unable to read the access token from $path", e)
}
}
val store = system.actorOf(
PlatformStore.props(
arguments.participantHost,
arguments.participantPort,
arguments.tlsConfig,
token,
arguments.time,
applicationInfo,
arguments.ledgerInboundMessageSizeMax,
arguments.enableUserManagement,
)
)
// TODO: usermgmt switching: for now we just poll both user and party mgmt
// If no parties are specified, we periodically poll from the party management service.
// If parties are specified, we only use those. This allows users to use custom display names
// if they are non-unique or use only a subset of parties for performance reasons.
// Currently, we subscribe to all available parties. We could change that to do it lazily only on login
// but given that Navigator is only a development tool that might not be worth the complexity.
val partyRefresh: Option[Cancellable] =
if (config.users.isEmpty || arguments.ignoreProjectParties) {
Some(
system.scheduler
.scheduleWithFixedDelay(Duration.Zero, 1.seconds, store, UpdateUsersOrParties)
)
} else {
config.users.foreach { case (displayName, config) =>
store ! Subscribe(
displayName,
config.party,
config.role,
config.useDatabase,
)
}
None
}
def graphQL: GraphQLHandler = DefaultGraphQLHandler(customEndpoints, Some(store))
def info: InfoHandler = DefaultInfoHandler(arguments, store)
val getAppState: () => Future[ApplicationStateInfo] = () => {
implicit val actorTimeout: Timeout = Timeout(5, TimeUnit.SECONDS)
(store ? GetApplicationStateInfo).mapTo[ApplicationStateInfo]
}
(graphQL, info, store, getAppState, partyRefresh)
}
private[navigator] def runServer(arguments: Arguments, config: Config): Unit = {
banner.foreach(println)
implicit val system: ActorSystem = ActorSystem("da-ui-backend")
val (graphQL, info, store @ _, getAppState, partyRefresh) = setup(arguments, config)
val stopServer = if (arguments.startWebServer) {
val binding = Http()
.newServerAt("0.0.0.0", arguments.port)
.withSettings(ServerSettings(system).withTransparentHeadRequests(true))
.bind(getRoute(system, arguments, graphQL, info, getAppState))
logger.info(s"DA UI backend server listening on port ${arguments.port}")
println(s"Frontend running at http://localhost:${arguments.port}.")
() => Try(Await.result(binding, 10.seconds).unbind())
} else { () =>
()
}
val stopPekko = () => Try(Await.result(system.terminate(), 10.seconds))
discard {
sys.addShutdownHook {
// Stop the web server, then the Pekko system consuming the ledger API
stopServer()
partyRefresh.foreach(_.cancel())
stopPekko()
()
}
}
}
private def dumpGraphQLSchema(): Unit = {
import ExecutionContext.Implicits.global
def graphQL: GraphQLHandler = DefaultGraphQLHandler(customEndpoints, None)
scala.Console.out.println(graphQL.renderSchema)
}
final def main(rawArgs: Array[String]): Unit =
Arguments.parse(rawArgs, defaultConfigFile) foreach run
private def run(args: Arguments): Unit = {
val navigatorConfigFile =
args.configFile.fold[ConfigOption](DefaultConfig(defaultConfigFile))(ExplicitConfig(_))
args.command match {
case ShowUsage =>
Arguments.showUsage(defaultConfigFile)
case DumpGraphQLSchema =>
dumpGraphQLSchema()
case CreateConfig =>
userFacingLogger.info(
s"Creating a configuration template file at ${navigatorConfigFile.path.toAbsolutePath()}"
)
Config.writeTemplateToPath(navigatorConfigFile.path, args.useDatabase)
case RunServer =>
Config.load(navigatorConfigFile, args.useDatabase) match {
case Left(ConfigNotFound(_)) =>
val message =
s"""No configuration file found!
|Please specify a configuration file and restart ${applicationInfo.name}.
|Config file path was '$navigatorConfigFile'.
|Hint: use the create-config command to create a sample config file.""".stripMargin
userFacingLogger.error(message)
sys.error("No configuration file found.")
case Left(ConfigParseFailed(message)) =>
userFacingLogger.error(s"Configuration file could not be parsed: $message")
sys.error(message)
case Left(ConfigInvalid(message)) =>
userFacingLogger.error(s"Configuration file is invalid: $message")
sys.error(message)
case Right(config) =>
runServer(args, config)
}
}
}
}
/** A custom endpoint for a backend to serve data of type `T`.
*
* Note that two `CustomEndpoint`s are considered equal if their `endpointName`s are the same to simplify
* registering new `CustomEndpoint`s and validate them
*/
abstract class CustomEndpoint[T](implicit tGraphQL: GraphQLObject[T]) {
/** The endpoint to be used as GraphQL top-level for the data served by this */
def endpointName: String
/** Calculate the data to serve within this endpoint */
def calculate(ledger: Ledger, templates: PackageRegistry): Seq[T]
final def endpoint: Field[GraphQLContext, Unit] = {
val listOfTType: ListType[T] = ListType(tGraphQL.to[GraphQLContext])
val resolve: Context[GraphQLContext, Unit] => Action[GraphQLContext, Seq[T]] =
(context: Context[GraphQLContext, Unit]) =>
calculate(context.ctx.ledger, context.ctx.templates)
Field(endpointName, listOfTType, resolve = resolve)
}
final override def hashCode: Int = endpointName.hashCode
final override def equals(obj: scala.Any): Boolean = obj match {
case that: CustomEndpoint[_] => this.endpointName equals that.endpointName
case _ => false
}
override def toString: String = endpointName
}

View File

@ -1,210 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.config
import java.io.File
import java.nio.file.{Path, Paths}
import com.daml.ledger.api.tls.TlsConfiguration
import com.daml.navigator.time.TimeProviderType
import scopt.{OptionDef, OptionParser}
import scala.util.Try
sealed abstract class Command
case object ShowUsage extends Command
case object RunServer extends Command
case object CreateConfig extends Command
case object DumpGraphQLSchema extends Command
case class Arguments(
port: Int = 4000,
assets: Option[String] = None,
command: Command = ShowUsage,
time: TimeProviderType = TimeProviderType.Auto,
participantHost: String = "localhost",
participantPort: Int = 6865,
tlsConfig: Option[TlsConfiguration] = None,
accessTokenFile: Option[Path] = None,
configFile: Option[Path] = None,
startWebServer: Boolean = false,
useDatabase: Boolean = false,
ledgerInboundMessageSizeMax: Int = 50 * 1024 * 1024, // 50 MiB
ignoreProjectParties: Boolean = false,
enableUserManagement: Boolean = true,
)
trait ArgumentsHelper { self: OptionParser[Arguments] =>
def hostname: OptionDef[String, Arguments] =
arg[String]("<host>")
.text(
s"hostname or IP address of the Ledger API server (default ${Arguments.default.participantHost})"
)
.optional()
.action((ip, arguments) =>
arguments.copy(
participantHost = ip
)
)
def port: OptionDef[Int, Arguments] =
arg[Int]("<port>")
.text(s"port number of the Ledger API server (default ${Arguments.default.participantPort})")
.optional()
.action((port, arguments) =>
arguments.copy(
participantPort = port
)
)
}
object Arguments {
val default = Arguments()
private def validatePath(path: String, message: String): Either[String, Unit] = {
val readable = Try(Paths.get(path).toFile.canRead).getOrElse(false)
if (readable) Right(()) else Left(message)
}
private val crtConfig = (path: String, arguments: Arguments) =>
arguments.copy(
tlsConfig =
arguments.tlsConfig.fold(Some(TlsConfiguration(true, Some(new File(path)), None, None)))(
c => Some(c.copy(certChainFile = Some(new File(path))))
)
)
private def argumentParser(defaultConfigFile: Path) =
new OptionParser[Arguments]("navigator") with ArgumentsHelper {
help("help").abbr("h").text("prints this usage text")
opt[Int]("port")
.text(s"port number on which the server should listen (default ${Arguments.default.port})")
.action((port, arguments) => arguments.copy(port = port))
opt[String]("assets")
.text("folder where frontend assets are available")
.action((assets, arguments) => arguments.copy(assets = Some(assets)))
opt[String]("access-token-file")
.text(
s"provide the path from which the access token will be read, required to interact with an authenticated ledger, no default"
)
.action((path, arguments) => arguments.copy(accessTokenFile = Some(Paths.get(path))))
.optional()
opt[String]('c', "config-file")
.text(s"set the configuration file default: ${defaultConfigFile}")
.action((path, arguments) => arguments.copy(configFile = Some(Paths.get(path))))
opt[TimeProviderType]('t', "time")
.text(s"Time provider. Valid values are: ${TimeProviderType.acceptedValues
.mkString(", ")}. Default: ${Arguments.default.time.name}")
.action((t, arguments) => arguments.copy(time = t))
// TODO: the 4 following TLS options can be defined by TlsConfigurationCli instead
opt[String]("pem")
.optional()
.text("TLS: The pem file to be used as the private key.")
.validate(path => validatePath(path, "The file specified via --pem does not exist"))
.action((path, arguments) =>
arguments.copy(tlsConfig =
arguments.tlsConfig.fold(
Some(TlsConfiguration(true, None, Some(new File(path)), None))
)(c => Some(c.copy(privateKeyFile = Some(new File(path)))))
)
)
opt[String]("crt")
.optional()
.text("TLS: The crt file to be used as the cert chain. Required for client authentication.")
.validate(path => validatePath(path, "The file specified via --crt does not exist"))
.action(crtConfig)
opt[String]("cacrt")
.optional()
.text("TLS: The crt file to be used as the trusted root CA.")
.validate(path => validatePath(path, "The file specified via --cacrt does not exist"))
.action((path, arguments) =>
arguments.copy(tlsConfig =
arguments.tlsConfig.fold(
Some(TlsConfiguration(true, None, None, Some(new File(path))))
)(c => Some(c.copy(trustCollectionFile = Some(new File(path)))))
)
)
opt[Unit]("tls")
.optional()
.text("TLS: Enable tls. This is redundant if --pem, --crt or --cacrt are set")
.action((_, arguments) =>
arguments.copy(tlsConfig =
arguments.tlsConfig.fold(Some(TlsConfiguration(true, None, None, None)))(Some(_))
)
)
opt[Unit]("database")
.hidden()
.text("EXPERIMENTAL: use an SQLite data store")
.action((_, arguments) =>
arguments.copy(
useDatabase = true
)
)
opt[Int]("ledger-api-inbound-message-size-max")
.text(
s"Maximum message size in bytes from the ledger API. Default is ${Arguments.default.ledgerInboundMessageSizeMax}."
)
.valueName("<bytes>")
.validate(x => Either.cond(x > 0, (), "Buffer size must be positive"))
.action((ledgerInboundMessageSizeMax, arguments) => {
arguments.copy(
ledgerInboundMessageSizeMax = ledgerInboundMessageSizeMax
)
})
opt[Unit]("ignore-project-parties")
.hidden()
.optional()
.text(
"Ignore the parties specified in the project configuration file and query the ledger for parties instead."
)
.action((_, arguments) => arguments.copy(ignoreProjectParties = true))
opt[Boolean]("feature-user-management")
.optional()
.text(
"By default, the login screen is now populated by quering the user mgmt service. Disable to query party mgmt instead (the pre-2.0 default)."
)
.action((enabled, arguments) => arguments.copy(enableUserManagement = enabled))
cmd("server")
.text("serve data from platform")
.action((_, arguments) =>
arguments.copy(
command = RunServer,
startWebServer = true,
)
)
.children(hostname, port)
cmd("dump-graphql-schema")
.text("Early Access (Labs). Dumps the full GraphQL schema to stdout")
.action((_, arguments) => arguments.copy(command = DumpGraphQLSchema))
cmd("create-config")
.text("Creates a template configuration file")
.action((_, arguments) => arguments.copy(command = CreateConfig))
}
def parse(args: Array[String], defaultConfigFile: Path): Option[Arguments] =
this.argumentParser(defaultConfigFile).parse(args, Arguments.default)
def showUsage(defaultConfigFile: Path): Unit = {
val parser = this.argumentParser(defaultConfigFile)
parser.displayToOut(parser.usage)
}
}

View File

@ -1,167 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.config
import java.nio.file.StandardOpenOption._
import java.nio.file.{Files, Path}
import com.daml.assistant.config.{
ProjectConfig,
ConfigLoadError => SdkConfigLoadError,
ConfigMissing => SdkConfigMissing,
ConfigParseError => SdkConfigParseError,
}
import com.daml.ledger.api.refinements.ApiTypes
import com.typesafe.config.{ConfigFactory, ConfigRenderOptions}
import org.slf4j.LoggerFactory
import pureconfig.{ConfigConvert, ConfigSource, ConfigWriter}
import pureconfig.generic.auto._
import scalaz.Tag
import scala.annotation.nowarn
final case class UserConfig(party: ApiTypes.Party, role: Option[String], useDatabase: Boolean)
/* The configuration has an empty map as default list of users because you can login as party too */
final case class Config(users: Map[String, UserConfig] = Map.empty[String, UserConfig]) {
def userIds: Set[String] = users.keySet
def roles: Set[String] = users.values.view.flatMap(_.role.toList).toSet
}
sealed abstract class ConfigReadError extends Product with Serializable {
def reason: String
}
final case class ConfigNotFound(reason: String) extends ConfigReadError
final case class ConfigInvalid(reason: String) extends ConfigReadError
final case class ConfigParseFailed(reason: String) extends ConfigReadError
sealed abstract class ConfigOption {
def path: Path
}
final case class DefaultConfig(path: Path) extends ConfigOption
final case class ExplicitConfig(path: Path) extends ConfigOption
object Config {
private[this] val logger = LoggerFactory.getLogger(this.getClass)
private[this] def userFacingLogger = LoggerFactory.getLogger("user-facing-logs")
def load(configOpt: ConfigOption, useDatabase: Boolean): Either[ConfigReadError, Config] = {
configOpt match {
case ExplicitConfig(configFile) =>
// If users specified a config file explicitly, we ignore the SDK config.
loadNavigatorConfig(configFile, useDatabase)
case DefaultConfig(configFile) =>
loadSdkConfig(useDatabase).left
.flatMap {
case ConfigNotFound(_) =>
logger.info("SDK config does not exist. Falling back to Navigator config file.")
loadNavigatorConfig(configFile, useDatabase) match {
case Left(ConfigNotFound(_)) =>
logger.info("No config file found. Using default config")
Right(Config())
case r => r
}
case e: ConfigReadError =>
logger.warn(s"SDK config exists, but is not usable: ${e.reason}")
Left(e)
}
}
}
def loadNavigatorConfig(
configFile: Path,
useDatabase: Boolean,
): Either[ConfigReadError, Config] = {
@nowarn(
"msg=local val userConfigConvert .* is never used"
) // false positive; macro uses aren't seen
implicit val userConfigConvert: ConfigConvert[UserConfig] =
mkUserConfigConvert(useDatabase = useDatabase)
if (Files.exists(configFile)) {
logger.info(s"Loading Navigator config file from $configFile")
val config = ConfigFactory.parseFileAnySyntax(configFile.toAbsolutePath.toFile)
ConfigSource
.fromConfig(config)
.load[Config]
.left
.map(e => ConfigParseFailed(e.toList.mkString(", ")))
} else {
Left(ConfigNotFound(s"File $configFile not found"))
}
}
def loadSdkConfig(useDatabase: Boolean): Either[ConfigReadError, Config] = {
val partiesE = for {
projectConfigPath <- ProjectConfig.projectConfigPath()
_ = logger.info(s"Loading SDK config file from $projectConfigPath")
projectConfig <- ProjectConfig.loadFromFile(projectConfigPath)
result <- projectConfig.parties
} yield result
partiesE match {
case Right(Some(parties)) =>
Right(
Config(
parties
.map(p => p -> UserConfig(ApiTypes.Party(p), None, useDatabase))
.toMap
)
)
case Right(None) =>
// Pick up parties from party management service
Right(Config())
case Left(SdkConfigMissing(reason)) =>
Left(ConfigNotFound(reason))
case Left(SdkConfigParseError(reason)) =>
val message = s"Found a SDK project config file, but it could not be parsed: $reason."
Left(ConfigParseFailed(message))
case Left(SdkConfigLoadError(reason)) =>
val message = s"Found a SDK project config file, but it could not be loaded: $reason."
Left(ConfigParseFailed(message))
}
}
def template(useDatabase: Boolean): Config =
Config(
Map(
"OPERATOR" -> UserConfig(ApiTypes.Party("party"), None, useDatabase)
)
)
def writeTemplateToPath(configFile: Path, useDatabase: Boolean): Unit = {
@nowarn(
"msg=local val userConfigConvert .* is never used"
) // false positive; macro uses aren't seen
implicit val userConfigConvert: ConfigConvert[UserConfig] = mkUserConfigConvert(
useDatabase = useDatabase
)
val config = ConfigWriter[Config].to(template(useDatabase))
val cro = ConfigRenderOptions
.defaults()
.setComments(false)
.setOriginComments(false)
.setFormatted(true)
.setJson(false)
Files.write(configFile, config.render(cro).getBytes, CREATE_NEW)
()
}
final case class UserConfigHelper(password: Option[String], party: String, role: Option[String])
private[this] def mkUserConfigConvert(useDatabase: Boolean): ConfigConvert[UserConfig] =
implicitly[ConfigConvert[UserConfigHelper]].xmap(
helper => {
helper.password.foreach { _ =>
userFacingLogger.warn(s"password field set for user ${helper.party} is deprecated")
}
UserConfig(ApiTypes.Party(helper.party), helper.role, useDatabase)
},
conf => UserConfigHelper(None, Tag.unwrap(conf.party), conf.role),
)
}

View File

@ -1,127 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.data
import java.time.Instant
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.json.ApiCodecVerbose
import com.daml.lf.value.json.ApiCodecCompressed
import ApiCodecCompressed.JsonImplicits._
import com.daml.navigator.model._
import scala.util.{Failure, Try}
import scalaz.syntax.tag._
import spray.json._
final case class CommandRow(
id: String,
index: Long,
workflowId: String,
platformTime: String,
subclassType: String,
template: Option[String],
recordArgument: Option[String],
contractId: Option[String],
interfaceId: Option[String],
choice: Option[String],
argumentValue: Option[String],
) {
def toCommand(types: PackageRegistry): Try[Command] = {
subclassType match {
case "CreateCommand" =>
(for {
tp <- Try(template.get)
tid <- Try(parseOpaqueIdentifier(tp).get)
recArgJson <- Try(recordArgument.get)
anyArg <- Try(
ApiCodecCompressed
.jsValueToApiValue(recArgJson.parseJson, tid, types.damlLfDefDataType _)
)
recArg <- Try(anyArg.asInstanceOf[ApiRecord])
} yield {
CreateCommand(
ApiTypes.CommandId(id),
index,
ApiTypes.WorkflowId(workflowId),
Instant.parse(platformTime),
tid,
recArg,
)
}).recoverWith { case e: Throwable =>
Failure(
DeserializationFailed(s"Failed to deserialize CreateCommand from row: $this. Error: $e")
)
}
case "ExerciseCommand" =>
(for {
tp <- Try(template.get)
tid <- Try(parseOpaqueIdentifier(tp).get)
iidOp <- Try(interfaceId.map(parseOpaqueIdentifier(_).get))
t <- Try(types.template(tid).get)
cId <- Try(contractId.get)
ch <- Try(choice.get)
_ <- Try(t.choices.find(_.name.unwrap == ch).get)
argJson <- Try(argumentValue.get)
arg <- Try(ApiCodecVerbose.jsValueToApiValue(argJson.parseJson))
} yield {
ExerciseCommand(
ApiTypes.CommandId(id),
index,
ApiTypes.WorkflowId(workflowId),
Instant.parse(platformTime),
ApiTypes.ContractId(cId),
tid,
iidOp,
ApiTypes.Choice(ch),
arg,
)
}).recoverWith { case e: Throwable =>
Failure(
DeserializationFailed(
s"Failed to deserialize ExerciseCommand from row: $this. Error: $e"
)
)
}
case _ => Failure(DeserializationFailed(s"unknown subclass type for Command: $subclassType"))
}
}
}
object CommandRow {
def fromCommand(c: Command): CommandRow = {
c match {
case c: CreateCommand =>
CommandRow(
c.id.unwrap,
c.index,
c.workflowId.unwrap,
c.platformTime.toString,
"CreateCommand",
Some(c.template.asOpaqueString),
Some(c.argument.toJson.compactPrint),
None,
None,
None,
None,
)
case e: ExerciseCommand =>
CommandRow(
c.id.unwrap,
c.index,
c.workflowId.unwrap,
c.platformTime.toString,
"ExerciseCommand",
Some(e.template.asOpaqueString),
None,
Some(e.contract.unwrap),
e.interfaceId.map(_.asOpaqueString),
Some(e.choice.unwrap),
Some(ApiCodecVerbose.apiValueToJsValue(e.argument).compactPrint),
)
}
}
}

View File

@ -1,97 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.data
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.model._
import scala.util.{Failure, Success, Try}
import scalaz.syntax.tag._
final case class CommandStatusRow(
commandId: String,
isCompleted: Boolean,
subclassType: String,
code: Option[String],
details: Option[String],
transactionId: Option[String],
) {
def toCommandStatus(
transactionById: ApiTypes.TransactionId => Try[Option[Transaction]]
): Try[CommandStatus] = {
subclassType match {
case "CommandStatusWaiting" =>
Success(CommandStatusWaiting())
case "CommandStatusError" =>
(for {
c <- code
d <- details
} yield {
CommandStatusError(c, d)
}).fold[Try[CommandStatus]](
Failure(
DeserializationFailed(s"Failed to deserialize CommandStatusError from row: $this")
)
)(
Success(_)
)
case "CommandStatusSuccess" =>
transactionId.map { tId =>
transactionById(ApiTypes.TransactionId(tId))
} match {
case Some(Success(Some(tx: Transaction))) => Success(CommandStatusSuccess(tx))
case Some(Failure(e)) =>
Failure(
RecordNotFound(
s"Failed to load transaction $transactionId for CommandStatus with commandId: $commandId. Exception: ${e.getMessage}"
)
)
case Some(Success(None)) =>
Failure(
RecordNotFound(
s"Failed to load transaction $transactionId for CommandStatus with commandId: $commandId"
)
)
case None =>
Failure(
DeserializationFailed(s"TransactionId is missing for CommandStatusSuccess row: $this")
)
}
case "CommandStatusUnknown" =>
Success(CommandStatusUnknown())
case s => Failure(DeserializationFailed(s"unknown subclass type for CommandStatus: $s"))
}
}
}
object CommandStatusRow {
def fromCommandStatus(commandId: ApiTypes.CommandId, cs: CommandStatus): CommandStatusRow = {
cs match {
case w: CommandStatusWaiting =>
CommandStatusRow(commandId.unwrap, w.isCompleted, "CommandStatusWaiting", None, None, None)
case e: CommandStatusError =>
CommandStatusRow(
commandId.unwrap,
e.isCompleted,
"CommandStatusError",
Some(e.code),
Some(e.details),
None,
)
case s: CommandStatusSuccess =>
CommandStatusRow(
commandId.unwrap,
s.isCompleted,
"CommandStatusSuccess",
None,
None,
Some(s.tx.id.unwrap),
)
case u: CommandStatusUnknown =>
CommandStatusRow(commandId.unwrap, u.isCompleted, "CommandStatusUnknown", None, None, None)
}
}
}

View File

@ -1,67 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.data
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.lf.value.json.ApiCodecCompressed
import ApiCodecCompressed.JsonImplicits._
import com.daml.navigator.json.ModelCodec.JsonImplicits._
import com.daml.navigator.model._
import scala.util.{Failure, Try}
import scalaz.syntax.tag._
import spray.json._
final case class ContractRow(
id: String,
templateId: String,
archiveTransactionId: Option[String],
argument: String,
agreementText: Option[String],
signatories: String,
observers: String,
key: Option[String],
) {
def toContract(types: PackageRegistry): Try[Contract] = {
(for {
id <- Try(ApiTypes.ContractId(id))
tid <- Try(parseOpaqueIdentifier(templateId).get)
template <- Try(types.template(tid).get)
recArgAny <- Try(
ApiCodecCompressed.jsValueToApiValue(argument.parseJson, tid, types.damlLfDefDataType _)
)
recArg <- Try(recArgAny.asInstanceOf[ApiRecord])
sig <- Try(signatories.parseJson.convertTo[List[ApiTypes.Party]])
obs <- Try(signatories.parseJson.convertTo[List[ApiTypes.Party]])
key <- Try(
key.map(
_.parseJson.convertTo[ApiValue](
ApiCodecCompressed.apiValueJsonReader(template.key.get, types.damlLfDefDataType _)
)
)
)
} yield {
Contract(id, template, recArg, agreementText, sig, obs, key)
}).recoverWith { case e: Throwable =>
Failure(DeserializationFailed(s"Failed to deserialize Contract from row: $this. Error: $e"))
}
}
}
object ContractRow {
def fromContract(c: Contract): ContractRow = {
ContractRow(
c.id.unwrap,
c.template.id.asOpaqueString,
None,
c.argument.toJson.compactPrint,
c.agreementText,
c.signatories.toJson.compactPrint,
c.observers.toJson.compactPrint,
c.key.map(_.toJson.compactPrint),
)
}
}

View File

@ -1,472 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.data
import java.sql.DriverManager
import java.util.concurrent.Executors.newWorkStealingPool
import cats.effect.{Blocker, ContextShift, IO}
import cats.implicits._
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.model._
import com.typesafe.scalalogging.LazyLogging
import doobie._
import doobie.implicits._
import scalaz.syntax.tag._
import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success, Try}
/** This class is responsible for running the queries
* and make the transformation between Scala and data store types
*/
class DatabaseActions extends LazyLogging {
/** Uncomment the log handler to enable query logging
*/
// implicit private val lh: LogHandler = doobie.util.log.LogHandler.jdkLogHandler
implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
// How many transactions can be executed in parallel.
// 256 comes from https://github.com/scala/scala/blob/v2.12.12/src/library/scala/concurrent/impl/ExecutionContextImpl.scala#L115-L116
private val maxConnections = 256
/** Initializing a new database.
* Every :memory: database is distinct from every other.
* So, opening two database connections each with the filename
* ":memory:" will create two independent in-memory databases.
* See https://www.sqlite.org/inmemorydb.html
*/
private val xa = Transactor.fromConnection[IO](
DriverManager.getConnection("jdbc:sqlite::memory:"),
Blocker liftExecutorService newWorkStealingPool(maxConnections),
)
/** Creating the tables when initializing the DatabaseActions object
*/
(Queries.createContractTable.update.run *>
Queries.contractIdIndex.update.run *>
Queries.contractIsActive.update.run *>
Queries.contractTemplateIdIsActive.update.run *>
Queries.createEventTable.update.run *>
Queries.eventIdIndex.update.run *>
Queries.eventTransactionIdParentId.update.run *>
Queries.eventContractIdSubclass.update.run *>
Queries.createTransactionTable.update.run *>
Queries.transactionIdIndex.update.run *>
Queries.createCommandStatusTable.update.run *>
Queries.commandStatusCommandIdIndex.update.run *>
Queries.createCommandTable.update.run *>
Queries.commandIdIndex.update.run)
.transact(xa)
.unsafeRunSync()
def schema(): Try[String] = {
Try(
Queries
.schema()
.query[Option[String]]
.to[List]
.transact(xa)
.unsafeRunSync()
.flatMap(_.toList)
.mkString("\n")
)
}
private def exec: PreparedStatementIO[SqlQueryResult] = {
// Read the specified columns from the resultset.
def readAll(cols: List[Int]): ResultSetIO[List[List[Object]]] =
readOne(cols).whileM[List](HRS.next)
// Take a list of column offsets and read a parallel list of values.
def readOne(cols: List[Int]): ResultSetIO[List[Object]] =
cols.traverse(FRS.getObject)
for {
md <- HPS.getMetaData
cols = (1 to md.getColumnCount).toList
colNames = cols.map(md.getColumnName)
data <- HPS.executeQuery(readAll(cols))
} yield SqlQueryResult(colNames, data.map(_.map(Option(_).map(_.toString).getOrElse("null"))))
}
/** Returns the given value, logging failures */
private def logErrors[T](result: Try[T]): Try[T] = {
result.failed.foreach { t =>
logger.error("Error executing database action, Navigator may be in a corrupted state", t)
}
result
}
def runQuery(query: String): Try[SqlQueryResult] = logErrors {
Try(Queries.query(query).execWith(exec).transact(xa).unsafeRunSync())
}
def insertEvent(event: Event): Try[Int] = logErrors {
Try {
Queries
.insertEvent(EventRow.fromEvent(event))
.update
.run
.transact(xa)
.unsafeRunSync()
}
}
def eventById(id: ApiTypes.EventId, types: PackageRegistry): Try[Option[Event]] = logErrors {
Try {
Queries
.eventById(id.unwrap)
.query[EventRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.headOption
.map { data =>
data.toEvent(types)
}
}.flatMap(_.sequence)
}
def eventsByParentId(parentId: ApiTypes.EventId, types: PackageRegistry): Try[List[Event]] =
logErrors {
Try {
Queries
.eventsByParentId(parentId.unwrap)
.query[EventRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.map { data =>
data.toEvent(types)
}
}.flatMap(_.sequence)
}
def createEventByContractId(
id: ApiTypes.ContractId,
types: PackageRegistry,
): Try[ContractCreated] = logErrors {
Try {
Queries
.eventByTypeAndContractId("ContractCreated", id.unwrap)
.query[EventRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.headOption
.map(data => (data.toEvent(types), data)) match {
case Some((Success(c: ContractCreated), _)) => Success(c)
case Some((_, data)) =>
Failure(DeserializationFailed(s"Failed to deserialize row as ContractCreated: $data"))
case None => Failure(RecordNotFound(s"Create event not found for contractId: $id"))
}
}.flatten
}
def archiveEventByContractId(
id: ApiTypes.ContractId,
types: PackageRegistry,
): Try[Option[ChoiceExercised]] = logErrors {
Try {
Queries
.eventByTypeAndContractId("ContractArchived", id.unwrap)
.query[EventRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.headOption
.map { data =>
data.toEvent(types) match {
case Success(a: ChoiceExercised) => Success(a)
case _ =>
Failure(DeserializationFailed(s"Failed to deserialize row as ChoiceExercised: $data"))
}
}
}.flatMap(_.sequence)
}
def choiceExercisedEventByContractById(
id: ApiTypes.ContractId,
types: PackageRegistry,
): Try[List[ChoiceExercised]] = logErrors {
Try {
Queries
.eventByTypeAndContractId("ChoiceExercised", id.unwrap)
.query[EventRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.map { data =>
data.toEvent(types) match {
case Success(e: ChoiceExercised) => Success(e)
case _ =>
Failure(DeserializationFailed(s"Failed to deserialize row as ChoiceExercised: $data"))
}
}
}.flatMap(_.sequence)
}
def insertTransaction(tx: Transaction): Try[Int] = logErrors {
Try {
Queries
.insertTransaction(TransactionRow.fromTransaction(tx))
.update
.run
.transact(xa)
.unsafeRunSync()
}
}
def transactionById(
id: ApiTypes.TransactionId,
types: PackageRegistry,
): Try[Option[Transaction]] = logErrors {
Try {
Queries
.topLevelEventsByTransactionId(id.unwrap)
.query[EventRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.map { data =>
data.toEvent(types)
}
.sequence
.map { events =>
Queries
.transactionById(id.unwrap)
.query[TransactionRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.headOption
.map { data =>
data.toTransaction(events)
}
}
}.flatten
}
def lastTransaction(types: PackageRegistry): Try[Option[Transaction]] = logErrors {
Try {
val txData = Queries
.lastTransaction()
.query[TransactionRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.headOption
txData.map { tx =>
Queries
.topLevelEventsByTransactionId(tx.id)
.query[EventRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.map { data =>
data.toEvent(types)
}
.sequence
.map { events =>
tx.toTransaction(events)
}
}
}.flatMap(_.sequence)
}
def upsertCommandStatus(commandId: ApiTypes.CommandId, cs: CommandStatus): Try[Int] = logErrors {
Try {
Queries
.upsertCommandStatus(CommandStatusRow.fromCommandStatus(commandId, cs))
.update
.run
.transact(xa)
.unsafeRunSync()
}
}
def updateCommandStatus(commandId: ApiTypes.CommandId, cs: CommandStatus): Try[Int] = logErrors {
Try {
Queries
.updateCommandStatus(CommandStatusRow.fromCommandStatus(commandId, cs))
.update
.run
.transact(xa)
.unsafeRunSync()
}
}
def commandStatusByCommandId(
commandId: ApiTypes.CommandId,
types: PackageRegistry,
): Try[Option[CommandStatus]] = logErrors {
Try {
Queries
.commandStatusByCommandId(commandId.unwrap)
.query[CommandStatusRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.headOption
.map { data =>
data.toCommandStatus(transactionById(_, types))
}
}.flatMap(_.sequence)
}
def insertCommand(cmd: Command): Try[Int] = logErrors {
Try {
Queries
.insertCommand(CommandRow.fromCommand(cmd))
.update
.run
.transact(xa)
.unsafeRunSync()
}
}
def commandById(id: ApiTypes.CommandId, types: PackageRegistry): Try[Option[Command]] =
logErrors {
Try {
Queries
.commandById(id.unwrap)
.query[CommandRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.headOption
.map(_.toCommand(types))
}.flatMap(_.sequence)
}
def allCommands(types: PackageRegistry): Try[List[Command]] = logErrors {
Try {
Queries
.allCommands()
.query[CommandRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.map(_.toCommand(types))
}.flatMap(_.sequence)
}
def insertContract(contract: Contract): Try[Int] = logErrors {
Try {
Queries
.insertContract(ContractRow.fromContract(contract))
.update
.run
.transact(xa)
.unsafeRunSync()
}
}
def archiveContract(
contractId: ApiTypes.ContractId,
archiveTransactionId: ApiTypes.TransactionId,
): Try[Int] = logErrors {
Try {
Queries
.archiveContract(contractId.unwrap, archiveTransactionId.unwrap)
.update
.run
.transact(xa)
.unsafeRunSync()
}
}
def contractCount(): Try[Int] = logErrors {
Try {
Queries
.contractCount()
.query[Int]
.unique
.transact(xa)
.unsafeRunSync()
}
}
def activeContractCount(): Try[Int] = logErrors {
Try {
Queries
.activeContractCount()
.query[Int]
.unique
.transact(xa)
.unsafeRunSync()
}
}
def contract(id: ApiTypes.ContractId, types: PackageRegistry): Try[Option[Contract]] = logErrors {
Try {
Queries
.contract(id.unwrap)
.query[ContractRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.headOption
.map(_.toContract(types))
}.flatMap(_.sequence)
}
def contracts(types: PackageRegistry): Try[List[Contract]] = logErrors {
Try {
Queries.contracts
.query[ContractRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.map(_.toContract(types))
}.flatMap(_.sequence)
}
def activeContracts(types: PackageRegistry): Try[List[Contract]] = logErrors {
Try {
Queries.activeContracts
.query[ContractRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.map(_.toContract(types))
}.flatMap(_.sequence)
}
def contractsForTemplate(tId: DamlLfIdentifier, types: PackageRegistry): Try[List[Contract]] =
logErrors {
Try {
Queries
.contractsForTemplate(tId.asOpaqueString)
.query[ContractRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.map(_.toContract(types))
}.flatMap(_.sequence)
}
def activeContractsForTemplate(
tId: DamlLfIdentifier,
types: PackageRegistry,
): Try[List[Contract]] = logErrors {
Try {
Queries
.activeContractsForTemplate(tId.asOpaqueString)
.query[ContractRow]
.to[List]
.transact(xa)
.unsafeRunSync()
.map(_.toContract(types))
}.flatMap(_.sequence)
}
}

View File

@ -1,176 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.data
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.lf.value.json.ApiCodecCompressed
import ApiCodecCompressed.JsonImplicits._
import com.daml.navigator.json.ModelCodec.JsonImplicits._
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
import com.daml.navigator.model._
import scala.util.{Failure, Try}
import scalaz.syntax.tag._
import spray.json._
final case class EventRow(
id: String,
transactionId: String,
workflowId: String,
parentId: Option[String],
contractId: String,
witnessParties: String,
subclassType: String,
templateId: Option[String],
recordArgument: Option[String],
choice: Option[String],
argumentValue: Option[String],
actingParties: Option[String],
isConsuming: Option[Boolean],
agreementText: Option[String],
signatories: String,
observers: String,
key: Option[String],
) {
def toEvent(types: PackageRegistry): Try[Event] = {
subclassType match {
case "ContractCreated" =>
(for {
wp <- Try(witnessParties.parseJson.convertTo[List[ApiTypes.Party]])
sig <- Try(signatories.parseJson.convertTo[List[ApiTypes.Party]])
obs <- Try(observers.parseJson.convertTo[List[ApiTypes.Party]])
tpStr <- Try(templateId.get)
tp <- Try(parseOpaqueIdentifier(tpStr).get)
recArgJson <- Try(recordArgument.get)
recArgAny <- Try(
ApiCodecCompressed
.jsValueToApiValue(recArgJson.parseJson, tp, types.damlLfDefDataType _)
)
recArg <- Try(recArgAny.asInstanceOf[ApiRecord])
template <- types
.template(tp)
.fold[Try[Template]](
Failure(new RuntimeException(s"No template in package registry with identifier $tp"))
)(Try(_))
key <- Try(
key.map(
_.parseJson.convertTo[ApiValue](
ApiCodecCompressed.apiValueJsonReader(template.key.get, types.damlLfDefDataType _)
)
)
)
} yield {
ContractCreated(
ApiTypes.EventId(id),
parentId.map(ApiTypes.EventId(_)),
ApiTypes.TransactionId(transactionId),
wp,
ApiTypes.WorkflowId(workflowId),
ApiTypes.ContractId(contractId),
tp,
recArg,
agreementText,
sig,
obs,
key,
)
}).recoverWith { case e: Throwable =>
Failure(
DeserializationFailed(
s"Failed to deserialize ContractCreated from row: $this. Error: $e"
)
)
}
case "ChoiceExercised" =>
(for {
wp <- Try(witnessParties.parseJson.convertTo[List[ApiTypes.Party]])
chc <- Try(choice.get)
argJson <- Try(argumentValue.get)
tp <- Try(templateId.get)
tid <- Try(parseOpaqueIdentifier(tp).get)
t <- Try(types.template(tid).get)
choiceType <- Try(
t.choices.find(c => ApiTypes.Choice.unwrap(c.name) == chc).get.parameter
)
arg <- Try(
ApiCodecCompressed
.jsValueToApiValue(argJson.parseJson, choiceType, types.damlLfDefDataType _)
)
apJson <- Try(actingParties.get)
ap <- Try(apJson.parseJson.convertTo[List[ApiTypes.Party]])
consuming <- Try(isConsuming.get)
} yield {
ChoiceExercised(
ApiTypes.EventId(id),
parentId.map(ApiTypes.EventId(_)),
ApiTypes.TransactionId(transactionId),
wp,
ApiTypes.WorkflowId(workflowId),
ApiTypes.ContractId(contractId),
tid,
ApiTypes.Choice(chc),
arg,
ap,
consuming,
)
}).recoverWith { case e: Throwable =>
Failure(
DeserializationFailed(
s"Failed to deserialize ChoiceExercised from row: $this. Error: $e"
)
)
}
case s => Failure(DeserializationFailed(s"unknown subclass type for Event: $s"))
}
}
}
object EventRow {
def fromEvent(event: Event): EventRow = {
event match {
case c: ContractCreated =>
EventRow(
c.id.unwrap,
c.transactionId.unwrap,
c.workflowId.unwrap,
c.parentId.map(_.unwrap),
c.contractId.unwrap,
c.witnessParties.toJson.compactPrint,
"ContractCreated",
Some(c.templateId.asOpaqueString),
Some(c.argument.toJson.compactPrint),
None,
None,
None,
None,
c.agreementText,
c.signatories.toJson.compactPrint,
c.observers.toJson.compactPrint,
c.key.map(_.toJson.compactPrint),
)
case e: ChoiceExercised =>
EventRow(
e.id.unwrap,
e.transactionId.unwrap,
e.workflowId.unwrap,
e.parentId.map(_.unwrap),
e.contractId.unwrap,
e.witnessParties.toJson.compactPrint,
"ChoiceExercised",
Some(e.templateId.asOpaqueString),
None,
Some(e.choice.unwrap),
Some(e.argument.toJson.compactPrint),
Some(e.actingParties.toJson.compactPrint),
Some(e.consuming),
None,
"[]",
"[]",
None,
)
}
}
}

View File

@ -1,275 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.data
import doobie._
import doobie.implicits._
object Queries {
private def createIndex(table: String, columns: List[String], name: String): Fragment =
Fragment.const(s"CREATE INDEX $name ON ${table} (${columns.mkString(", ")})")
val createContractTable: Fragment = sql"""
CREATE TABLE
contract
(
id TEXT PRIMARY KEY NOT NULL,
template_id TEXT NOT NULL,
archive_transaction_id TEXT DEFAULT NULL,
argument JSON NOT NULL,
agreement_text TEXT DEFAULT NULL,
signatories JSON DEFAULT NULL,
observers JSON DEFAULT NULL,
contract_key JSON DEFAULT NULL
)
"""
val contractIdIndex = createIndex("contract", List("id"), "contract_id_idx")
val contractIsActive =
createIndex("contract", List("archive_transaction_id"), "contract_active_idx")
val contractTemplateIdIsActive = createIndex(
"contract",
List("template_id", "archive_transaction_id"),
"contract_tmplt_active_idx",
)
val createEventTable: Fragment = sql"""
CREATE TABLE
event
(
id TEXT PRIMARY KEY NOT NULL,
transaction_id TEXT NOT NULL,
workflow_id TEXT NOT NULL,
parent_id TEXT DEFAULT NULL,
contract_id TEXT NOT NULL,
witness_parties JSON NOT NULL,
subclass_type TEXT NOT NULL,
template_id TEXT DEFAULT NULL,
record_argument JSON DEFAULT NULL,
choice TEXT DEFAULT NULL,
argument_value JSON DEFAULT NULL,
acting_parties JSON DEFAULT NULL,
is_consuming INTEGER DEFAULT NULL,
agreement_text TEXT DEFAULT NULL,
signatories JSON DEFAULT NULL,
observers JSON DEFAULT NULL,
contract_key JSON DEFAULT NULL
)
"""
val eventIdIndex = createIndex("event", List("id"), "event_id_idx")
val eventTransactionIdParentId =
createIndex("event", List("transaction_id", "parent_id"), "event_tx_id_idx")
val eventContractIdSubclass =
createIndex("event", List("contract_id", "subclass_type"), "event_cid_idx")
val createTransactionTable: Fragment = sql"""
CREATE TABLE
transactions
(
seq INTEGER PRIMARY KEY AUTOINCREMENT,
id TEXT UNIQUE NOT NULL,
command_id TEXT DEFAULT NULL,
effective_at TEXT NOT NULL,
offset TEXT NOT NULL
)
"""
val transactionIdIndex = createIndex("transactions", List("id"), "tx_id_idx")
val createCommandStatusTable: Fragment = sql"""
CREATE TABLE
command_status
(
command_id TEXT PRIMARY KEY NOT NULL,
is_completed INTEGER NOT NULL,
subclass_type TEXT NOT NULL,
code TEXT DEFAULT NULL,
details TEXT DEFAULT NULL,
transaction_id TEXT DEFAULT NULL
)
"""
val commandStatusCommandIdIndex =
createIndex("command_status", List("command_id"), "cmd_st_id_idx")
val createCommandTable: Fragment = sql"""
CREATE TABLE
command
(
id TEXT PRIMARY KEY NOT NULL,
idx INTEGER UNIQUE NOT NULL,
workflow_id TEXT NOT NULL,
platform_time TEXT NOT NULL,
subclass_type TEXT NOT NULL,
template TEXT DEFAULT NULL,
record_argument JSON DEFAULT NULL,
contract_id TEXT DEFAULT NULL,
choice TEXT DEFAULT NULL,
argument_value JSON DEFAULT NULL
)
"""
val commandIdIndex = createIndex("command", List("id"), "cmd_id_idx")
def schema(): Fragment =
sql"""SELECT sql FROM sqlite_master
ORDER BY tbl_name, type DESC, name
"""
def query(query: String): Fragment = {
Fragment.const(query)
}
def insertEvent(row: EventRow): Fragment =
sql"""
INSERT INTO
event
(id, transaction_id, workflow_id, parent_id, contract_id, witness_parties, subclass_type,
template_id, record_argument, choice, argument_value, acting_parties, is_consuming, agreement_text, signatories, observers, contract_key)
VALUES
(${row.id}, ${row.transactionId}, ${row.workflowId}, ${row.parentId}, ${row.contractId}, ${row.witnessParties}, ${row.subclassType},
${row.templateId}, ${row.recordArgument}, ${row.choice}, ${row.argumentValue}, ${row.actingParties}, ${row.isConsuming}, ${row.agreementText}, ${row.signatories}, ${row.observers}, ${row.key})
"""
def eventById(id: String): Fragment =
sql"""
SELECT * FROM event WHERE id = $id LIMIT 1
"""
def eventsByParentId(id: String): Fragment =
sql"""
SELECT * FROM event WHERE parent_id = $id
"""
def topLevelEventsByTransactionId(id: String): Fragment =
sql"""
SELECT * FROM event WHERE transaction_id = $id AND parent_id is NULL
"""
def eventByTypeAndContractId(subclassType: String, contractId: String): Fragment =
sql"""
SELECT * FROM event WHERE subclass_type = $subclassType and contract_id = $contractId
"""
def insertTransaction(row: TransactionRow): Fragment =
sql"""
INSERT INTO
transactions
(id, command_id, effective_at, offset)
VALUES
(${row.id}, ${row.commandId}, ${row.effectiveAt}, ${row.offset})
"""
def transactionById(id: String): Fragment =
sql"""
SELECT * FROM transactions WHERE id = $id LIMIT 1
"""
def lastTransaction(): Fragment =
sql"""
SELECT * FROM transactions ORDER BY seq DESC LIMIT 1
"""
def upsertCommandStatus(row: CommandStatusRow): Fragment =
sql"""
INSERT INTO
command_status
(command_id, is_completed, subclass_type, code, details, transaction_id)
VALUES
(${row.commandId}, ${row.isCompleted}, ${row.subclassType}, ${row.code}, ${row.details}, ${row.transactionId})
ON CONFLICT(command_id)
DO UPDATE
SET is_completed = ${row.isCompleted},
subclass_type = ${row.subclassType},
code = ${row.code},
details = ${row.details},
transaction_id = ${row.transactionId}
"""
def updateCommandStatus(row: CommandStatusRow): Fragment =
sql"""
UPDATE command_status
SET is_completed = ${row.isCompleted},
subclass_type = ${row.subclassType},
code = ${row.code},
details = ${row.details},
transaction_id = ${row.transactionId}
WHERE command_id = ${row.commandId}
"""
def commandStatusByCommandId(commandId: String): Fragment =
sql"""
SELECT * FROM command_status WHERE command_id = $commandId LIMIT 1
"""
def insertCommand(row: CommandRow): Fragment =
sql"""
INSERT INTO
command
(id, idx, workflow_id, platform_time, subclass_type, template, record_argument, contract_id, choice, argument_value)
VALUES
(${row.id}, ${row.index}, ${row.workflowId}, ${row.platformTime}, ${row.subclassType}, ${row.template}, ${row.recordArgument}, ${row.contractId}, ${row.choice}, ${row.argumentValue})
"""
def commandById(id: String): Fragment =
sql"""
SELECT * FROM command WHERE id = $id LIMIT 1
"""
def allCommands(): Fragment =
sql"""
SELECT * FROM command
"""
def insertContract(row: ContractRow): Fragment =
sql"""
INSERT INTO
contract
(id, template_id, archive_transaction_id, argument, agreement_text, signatories, observers, contract_key)
VALUES
(${row.id}, ${row.templateId}, ${row.archiveTransactionId}, ${row.argument}, ${row.agreementText}, ${row.signatories}, ${row.observers}, ${row.key})
"""
def archiveContract(contractId: String, archiveTransactionId: String): Fragment =
sql"""
UPDATE contract SET archive_transaction_id = $archiveTransactionId WHERE id = $contractId
"""
def contractCount(): Fragment =
sql"""
SELECT COUNT(*) FROM contract
"""
def activeContractCount(): Fragment =
sql"""
SELECT COUNT(*) FROM contract WHERE archive_transaction_id IS NULL
"""
def contract(id: String): Fragment =
sql"""
SELECT * FROM contract WHERE id = $id LIMIT 1
"""
def contracts: Fragment =
sql"""
SELECT * FROM contract
"""
def activeContracts: Fragment =
sql"""
SELECT * FROM contract WHERE archive_transaction_id IS NULL
"""
def contractsForTemplate(tId: String): Fragment =
sql"""
SELECT * FROM contract WHERE template_id = $tId
"""
def activeContractsForTemplate(tId: String): Fragment =
sql"""
SELECT * FROM contract WHERE template_id = $tId and archive_transaction_id IS NULL
"""
}

View File

@ -1,43 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.data
import java.time.Instant
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.model.{Event, Transaction}
import scalaz.syntax.tag._
final case class TransactionRow(
autoInc: Option[Int],
id: String,
commandId: Option[String],
effectiveAt: String,
offset: String,
) {
def toTransaction(events: List[Event]): Transaction = {
Transaction(
ApiTypes.TransactionId(id),
commandId.map(ApiTypes.CommandId(_)),
Instant.parse(effectiveAt),
offset,
events,
)
}
}
object TransactionRow {
def fromTransaction(tx: Transaction): TransactionRow = {
TransactionRow(
None,
tx.id.unwrap,
tx.commandId.map(_.unwrap),
tx.effectiveAt.toString,
tx.offset,
)
}
}

View File

@ -1,60 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.dotnot
/** A zipper for a property-like structure, such as dot-notation
*
* For instance, given the property `foo.bar.baz.doh`, a cursor `PropertyCursor(List("foo"), "bar", List("baz", "doh"))`
* is pointing at the `"bar"` part of the property.
*
* @param reversedBefore the part of the property prior to the cursor
* @param current the piece of the property currently pointed by the cursor
* @param after the part of the property after the cursor
*/
final case class PropertyCursor(
reversedBefore: List[String],
current: String,
after: List[String],
) {
/** @return whether [[current]] is the last */
def isLast: Boolean = after.isEmpty
/** @return a cursor pointing at the previous part of the property, if possible */
def prev: Option[PropertyCursor] =
reversedBefore match {
case Nil => None
case head :: tail => Some(PropertyCursor(tail, head, current :: after))
}
/** @return a cursor pointing at the next part of the property, if possible */
def next: Option[PropertyCursor] =
after match {
case Nil => None
case head :: tail => Some(PropertyCursor(current :: reversedBefore, head, tail))
}
/** Change the part pointed by the cursor */
def withCurrent(current: String): PropertyCursor =
this.copy(current = current)
/** Change the part of the property after the cursor */
def withAfter(newAfter: List[String]): PropertyCursor =
this.copy(after = newAfter)
override def toString: String =
(reversedBefore.reverse ++ (("[" + current + "]") +: after)).mkString(".")
def ensureLast[A](target: String)(r: => A): Either[DotNotFailure, A] =
if (isLast) Right(r) else Left(MustBeLastPart(target, this, current))
}
object PropertyCursor {
/** @return a [[PropertyCursor]] created by splitting the input on dot */
def fromString(str: String): PropertyCursor = {
val Array(current, after @ _*) = str.split("\\.", -1)
PropertyCursor(List.empty, current, after.toList)
}
}

View File

@ -1,260 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
/** dotnot(ation) is a simple library to implement a string-based dot-notation system
* to access properties. A property is something in dot notation `branch1.branch2.leaf`
* similar to the dot-notation in scala to reference a field.
*
* The idea is to represent all the possible paths from the root of the model down
* to the leafs with a nice DSL.
*
* Naming:
* - `Tree`: the model is seen as a `Tree` where each portion can either be a `Leaf` or a `Branch`
* - `Leaf`: a node of the Tree that contains only one value (e.g. a primitive field, like `Int`)
* - `Branch`: a non-leaf part of the model (e.g. an object with fields)
* - `root`: the entry point and root of the Tree
* - `path` or `property`: the list of all the pieces of the `Tree` from the `root` to a `leaf`
* - `dotnot handler for k`: the set of all the paths and actions to be performed on an instance of `k`
*/
package dotnot {
sealed trait DotNotFailure {
def cursor: PropertyCursor
def value: String
}
final case class CannotBeEmpty(target: String, cursor: PropertyCursor, value: String)
extends DotNotFailure
final case class MustBeLastPart(target: String, cursor: PropertyCursor, value: String)
extends DotNotFailure
final case class MustNotBeLastPart(target: String, cursor: PropertyCursor, value: String)
extends DotNotFailure
final case class TypeCoercionFailure(
target: String,
expectedType: String,
cursor: PropertyCursor,
value: String,
) extends DotNotFailure
final case class UnknownProperty(target: String, cursor: PropertyCursor, value: String)
extends DotNotFailure
final case class MatchNotFoundForValue(target: String, cursor: PropertyCursor, value: String)
extends DotNotFailure
final case class UnknownType(name: String, cursor: PropertyCursor, value: String)
extends DotNotFailure
final case class NameMatcherToAction[T, R, C](matcher: NameMatcher, action: Action[T, R, C])
final case class ValueMatcherToAction[T, R, C](matcher: ValueMatcher, action: Action[T, R, C])
/** Utility for debugging purposes, implements an equalsTo function with a toString defined */
final case class equalsTo[T](t: T) extends Function[T, Boolean] {
def apply(t2: T): Boolean = t == t2
override def toString: String = s"""_ == $t"""
}
final case object constTrue extends Function[String, Boolean] {
def apply(s: String): Boolean = true
override def toString: String = s"""_ => true"""
}
sealed trait OnTreeBase[T, R, C] {
def target: String
def onTree: OnTreeReady[T, R, C]
def onLeafP(nameMatcher: NameMatcher): OnLeaf[T, R, C] =
OnLeaf[T, R, C](onTree, nameMatcher)
def onLeaf(name: String): OnLeaf[T, R, C] =
onLeafP(equalsTo(name))
/** Match a branch by predicate on the name and delegate handling the subtree from that branch
*
* @param nameMatcher matcher of the branch name
* @param project the projection from the parent model of type `T` to the child model
* `P` where `P` is the type of the field of an instance of `T`
* @param delegate the dotnot handler for the subtree
*/
def onBranchP[P](
nameMatcher: NameMatcher,
project: T => P,
delegate: OnTreeReady[P, R, C],
): OnTreeReady[T, R, C] = {
val action: (T, PropertyCursor, String, C) => Either[DotNotFailure, R] =
(t: T, cursor: PropertyCursor, value: String, context: C) => {
cursor.next match {
case None =>
Left(MustNotBeLastPart(target, cursor, value))
case Some(nextCursor) =>
val p = project(t)
delegate.run(p, nextCursor, value, context)
}
}
val nameMatcherToAction = NameMatcherToAction[T, R, C](nameMatcher, action)
onTree.copy(nameMatcherToActions = onTree.nameMatcherToActions :+ nameMatcherToAction)
}
/** Same as [[onBranchP]] but match the branch by name */
def onBranch[P](
name: String,
project: T => P,
delegate: OnTreeReady[P, R, C],
): OnTreeReady[T, R, C] =
if (name == "*") {
onBranchP[P](constTrue, project, delegate)
} else {
onBranchP[P](equalsTo(name), project, delegate)
}
/** Given a collection, match one the first element `e` with `getName(e)` equal to
* the value currently pointed by the cursor and then delegate handling that element
* to `delegate`
*
* @param getName a function to read the name of `E`
* @param delegate the dotnot handler for `e`
*/
def onElements[E](getName: E => String, delegate: OnTreeReady[E, R, C])(implicit
ev: T <:< Iterable[E]
): OnTreeReady[T, R, C] = {
val action: (T, PropertyCursor, String, C) => Either[DotNotFailure, R] =
(t: T, cursor: PropertyCursor, value: String, context: C) => {
cursor.next match {
case None =>
Left(MustNotBeLastPart(target, cursor, value))
case Some(nextCursor) =>
t.find(getName(_).toLowerCase == cursor.current.toLowerCase) match {
case None =>
Left(UnknownProperty(target, cursor, value))
case Some(e) =>
delegate.run(e, nextCursor, value, context)
}
}
}
val nameMatcherToAction = NameMatcherToAction[T, R, C](constTrue, action)
onTree.copy(nameMatcherToActions = onTree.nameMatcherToActions :+ nameMatcherToAction)
}
def run(t: T, cursor: PropertyCursor, value: String, context: C): Either[DotNotFailure, R] = {
onTree.nameMatcherToActions
.find(_.matcher(cursor.current))
.map(_.action)
.orElse(onTree.default) match {
case None =>
Left(UnknownProperty(target, cursor, value))
case Some(action) =>
action(t, cursor, value, context)
}
}
}
final case class OnTree[T, R, C](target: String) extends OnTreeBase[T, R, C] {
def onTree: OnTreeReady[T, R, C] =
OnTreeReady[T, R, C](target, Vector(), None)
}
final case class OnTreeReady[T, R, C](
target: String,
nameMatcherToActions: Vector[NameMatcherToAction[T, R, C]],
default: Option[Action[T, R, C]],
) extends OnTreeBase[T, R, C] {
override def onTree: OnTreeReady[T, R, C] = this
}
sealed trait OnLeafBase[T, R, C] {
def onTreeOld: OnTreeReady[T, R, C]
def leafMatcher: NameMatcher
def valueMatcherToActions: Vector[ValueMatcherToAction[T, R, C]]
def default: Option[Action[T, R, C]]
def onValueP(valueMatcher: ValueMatcher): OnValue[T, R, C] =
OnValue[T, R, C](
OnLeafReady(onTreeOld, leafMatcher, valueMatcherToActions, default),
valueMatcher,
)
def onValue(value: String): OnValue[T, R, C] =
onValueP(equalsTo(value))
def onAnyValue: OnValue[T, R, C] =
onValueP(constTrue)
}
final case class OnLeaf[T, R, C](onTreeOld: OnTreeReady[T, R, C], leafMatcher: NameMatcher)
extends OnLeafBase[T, R, C] {
override def valueMatcherToActions: Vector[ValueMatcherToAction[T, R, C]] = Vector()
override def default: Option[Action[T, R, C]] = None
}
final case class OnLeafReady[T, R, C](
onTreeOld: OnTreeReady[T, R, C],
leafMatcher: NameMatcher,
valueMatcherToActions: Vector[ValueMatcherToAction[T, R, C]],
default: Option[Action[T, R, C]],
) extends OnLeafBase[T, R, C]
with OnTreeBase[T, R, C] {
override def target: String = onTreeOld.target
private def nameAction: NameMatcherToAction[T, R, C] = {
val action: Action[T, R, C] = (t: T, cursor: PropertyCursor, value: String, context: C) => {
valueMatcherToActions.find(_.matcher(value)).map(_.action).orElse(default) match {
case None =>
Left(MatchNotFoundForValue(onTreeOld.target, cursor, value))
case Some(action) =>
action(t, cursor, value, context)
}
}
NameMatcherToAction[T, R, C](leafMatcher, action)
}
private def onTreeUpdated: OnTreeReady[T, R, C] =
onTreeOld.copy(nameMatcherToActions = onTreeOld.nameMatcherToActions :+ nameAction)
override def onTree: OnTreeReady[T, R, C] = onTreeUpdated
}
final case class OnValue[T, R, C](onLeaf: OnLeafReady[T, R, C], valueMatcher: ValueMatcher) {
def perform[K](f: (T, K) => R)(implicit readK: Read[K]): OnLeafReady[T, R, C] = {
val action: Action[T, R, C] = (t: T, cursor: PropertyCursor, value: String, _: C) => {
if (cursor.isLast) {
Right(f(t, readK.from(value).toOption.get))
} else {
Left(MustBeLastPart(onLeaf.onTreeOld.target, cursor, value))
}
}
val valueMatcherToAction = ValueMatcherToAction(valueMatcher, action)
onLeaf.copy(valueMatcherToActions = onLeaf.valueMatcherToActions :+ valueMatcherToAction)
}
def const(r: R): OnLeafReady[T, R, C] =
perform[String]((_: T, _: String) => r)
}
}
package object dotnot {
type NameMatcher = String => Boolean
type ValueMatcher = String => Boolean
type Action[T, R, C] = (T, PropertyCursor, String, C) => Either[DotNotFailure, R]
/** Create a dot-notation handler for a type `T` with name `name` and with return
* type from the leafs of type `R`
*/
def root[T, R, C](name: String): OnTree[T, R, C] =
OnTree[T, R, C](name)
/** Utility method that creates a "opaque" dot-notation handler, which is
* a handler that bypass the library and uses the given function `f` to
* access the underlying structure of type `T` and to produce a result of
* type `R`.
*
* Useful for when dotnot is not useful
*/
def opaque[T, R, C](name: String)(f: Action[T, R, C]): OnTreeReady[T, R, C] =
OnTreeReady[T, R, C](name, Vector(), Some(f))
}

View File

@ -1,921 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.graphql
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.concurrent.TimeUnit
import org.apache.pekko.actor.ActorRef
import org.apache.pekko.pattern.ask
import org.apache.pekko.util.Timeout
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.CustomEndpoint
import com.daml.navigator.model._
import com.daml.navigator.query._
import com.daml.navigator.store.Store._
import com.daml.navigator.time.{TimeProviderType, TimeProviderWithType}
import com.daml.navigator.model.converter.GenericConversionError
import sangria.ast.StringValue
import sangria.macros.derive.GraphQLDeprecated
import sangria.schema.InputObjectType.DefaultInput
import sangria.schema._
import sangria.validation.{ValueCoercionViolation, Violation}
import scalaz.Tag
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
case class GraphQLContext(party: PartyState, store: ActorRef) {
def ledger: Ledger = party.ledger
def templates: PackageRegistry = party.packageRegistry
}
case class LedgerTime(time: TimeProviderWithType, id: String = "CURRENT")
case class UserFacingError(message: String)
extends Throwable
with sangria.execution.UserFacingError {
override def getMessage: String = message
}
private object Implicits {
implicit final class `ExtendedInterfaceType Ops`[Ctx, Val](
private val self: InterfaceType[Ctx, Val]
) extends AnyVal {
// Hack around https://github.com/scala/bug/issues/11662 which
// otherwise results in the implicit PossibleObject constructor
// being ambiguous due to the also implicit apply method.
def withPossibleObjectTypes(possible: () => List[ObjectType[Ctx, _]]) =
self.withPossibleTypes(() => possible().map(new PossibleObject[Ctx, Val](_)))
}
}
/** Schema definition for the UI backend GraphQL API. */
@SuppressWarnings(
Array("org.wartremover.warts.JavaSerializable", "org.wartremover.warts.Serializable")
)
final class GraphQLSchema(customEndpoints: Set[CustomEndpoint[_]]) {
import Implicits._
implicit private val actorTimeout: Timeout = Timeout(60, TimeUnit.SECONDS)
implicit private val executionContext: ExecutionContext = ExecutionContext.global
// ------------------------------------------------------------------------------------------------------------------
// Scalar and enum types
// ------------------------------------------------------------------------------------------------------------------
val DirectionType = EnumType(
"Direction",
values = List(
EnumValue("ASCENDING", value = SortDirection.ASCENDING),
EnumValue("DESCENDING", value = SortDirection.DESCENDING),
),
)
val TimeTypeType = EnumType(
"TimeType",
values = List(
EnumValue("static", value = TimeProviderType.Static),
EnumValue("wallclock", value = TimeProviderType.WallClock),
EnumValue("simulated", value = TimeProviderType.Simulated),
),
)
val SortingType = ObjectType(
"Sorting",
fields[Unit, SortCriterion](
Field("field", StringType, resolve = _.value.field),
Field("direction", DirectionType, resolve = _.value.direction),
),
)
val TimeType: ScalarType[Instant] = ScalarType[Instant](
"Time",
coerceOutput = (instant, _) => DateTimeFormatter.ISO_INSTANT.format(instant),
coerceUserInput = {
case s: String => parseInstant(s)
case _ => Left(InstantCoercionViolation)
},
coerceInput = {
case StringValue(s, _, _, _, _) => parseInstant(s)
case _ => Left(InstantCoercionViolation)
},
)
val PartyType = StringAliasType("Party")
val OffsetType = StringAliasType("LedgerOffset")
val CommandIdType: ScalarType[ApiTypes.CommandId] = StringAliasType.tagged("CommandId")
// ------------------------------------------------------------------------------------------------------------------
// Composite types
// ------------------------------------------------------------------------------------------------------------------
val LedgerTimeType = ObjectType(
"LedgerTime",
fields[Unit, TimeProviderWithType](
Field("id", IDType, resolve = _ => "CURRENT"),
Field("time", TimeType, resolve = _.value.time.getCurrentTime),
Field("type", TimeTypeType, resolve = _.value.`type`),
),
)
// noinspection ForwardReference
val NodeType: InterfaceType[GraphQLContext, Node[_]] = InterfaceType[GraphQLContext, Node[_]](
"Node",
fields[GraphQLContext, Node[_]](
Field("id", IDType, resolve = _.value.idString)
),
).withPossibleObjectTypes(() =>
List(
TransactionType,
TemplateType,
ContractType,
CreateCommandType,
ExerciseCommandType,
CreatedEventType,
ExercisedEventType,
DamlLfDefDataTypeType,
)
)
// noinspection ForwardReference
val DamlLfNodeType: InterfaceType[GraphQLContext, DamlLfNode] = InterfaceType(
"DamlLfNode",
fields[GraphQLContext, DamlLfNode](
Field("id", IDType, resolve = _.value.idString),
Field("package", StringType, resolve = _.value.id.packageId),
Field("module", StringType, resolve = _.value.id.qualifiedName.module.toString()),
Field("name", StringType, resolve = _.value.id.qualifiedName.name.toString()),
),
).withPossibleObjectTypes(() =>
List(
DamlLfDefDataTypeType,
TemplateType,
)
)
val ChoiceType = ObjectType(
"Choice",
fields[Unit, Choice](
Field(
"name",
StringType,
resolve = context => Tag.unwrap[String, ApiTypes.ChoiceTag](context.value.name),
),
Field("parameter", JsonType.DamlLfTypeType, resolve = _.value.parameter),
Field("consuming", BooleanType, resolve = _.value.consuming),
Field(
"inheritedInterface",
OptionType(StringType),
resolve = _.value.inheritedInterface.map(_.asOpaqueString),
),
),
)
// noinspection ForwardReference
val TemplateType: ObjectType[GraphQLContext, Template] = ObjectType(
"Template",
interfaces[GraphQLContext, Template](NodeType, DamlLfNodeType),
() =>
fields[GraphQLContext, Template](
Field("id", IDType, resolve = _.value.idString),
Field("topLevelDecl", StringType, resolve = _.value.topLevelDecl),
Field("parameter", JsonType.DamlLfTypeType, resolve = _.value.parameter),
Field(
"parameterDef",
DamlLfDefDataTypeType,
resolve = context =>
context.ctx.templates
.damlLfDefDataType(context.value.id)
.map(DamlLfDefDataTypeBoxed(context.value.id, _))
.get,
),
Field("choices", ListType(ChoiceType), resolve = _.value.choices),
Field(
"contracts",
ContractPaginationType,
arguments =
SearchArg :: FilterArg :: IncludeArchivedArg :: CountArg :: StartArg :: SortArg :: Nil,
resolve = context =>
buildTemplateContractPager(context).fetch(context.ctx.ledger, context.ctx.templates),
),
Field(
"implementedInterfaces",
ListType(StringType),
resolve = _.value.implementedInterfaces.toList.map(_.asOpaqueString),
),
),
)
val TemplateEdgeType = ObjectType(
"TemplateEdge",
fields[GraphQLContext, Template](
Field("node", TemplateType, resolve = _.value),
Field("cursor", StringType, resolve = _.value.idString),
),
)
val TemplatePaginationType = ObjectType(
"TemplatePagination",
fields[GraphQLContext, Page[Template]](
Field("beforeCount", IntType, resolve = _.value.offset),
Field("totalCount", IntType, resolve = _.value.total),
Field("sortings", OptionType(ListType(SortingType)), resolve = _.value.sortedLike),
Field("edges", ListType(TemplateEdgeType), resolve = _.value.rows),
),
)
// noinspection ForwardReference
val EventType: InterfaceType[GraphQLContext, Event] = InterfaceType(
"Event",
() =>
fields[GraphQLContext, Event](
Field("id", IDType, resolve = _.value.idString),
Field(
"parent",
OptionType(EventType),
resolve = context =>
context.value.parentId.flatMap(pid =>
context.ctx.ledger.event(pid, context.ctx.templates)
),
),
Field(
"transaction",
TransactionType,
resolve = context =>
context.ctx.ledger.transaction(context.value.transactionId, context.ctx.templates).get,
),
Field(
"witnessParties",
ListType(PartyType),
resolve = _.value.witnessParties.map(Tag.unwrap),
),
Field(
"workflowId",
StringType,
resolve = context => Tag.unwrap[String, ApiTypes.WorkflowIdTag](context.value.workflowId),
),
),
).withPossibleObjectTypes(() => List(CreatedEventType, ExercisedEventType))
// noinspection ForwardReference
val CreatedEventType: ObjectType[GraphQLContext, ContractCreated] = ObjectType(
"CreatedEvent",
interfaces[GraphQLContext, ContractCreated](NodeType, EventType),
() =>
fields[GraphQLContext, ContractCreated](
Field(
"transaction",
TransactionType,
resolve = context =>
context.ctx.ledger.transaction(context.value.transactionId, context.ctx.templates).get,
),
Field(
"contract",
ContractType,
resolve = context =>
context.ctx.ledger.contract(context.value.contractId, context.ctx.templates).get,
),
Field("argument", JsonType.ApiRecordType, resolve = _.value.argument),
),
)
// noinspection ForwardReference
val ExercisedEventType: ObjectType[GraphQLContext, ChoiceExercised] = ObjectType(
"ExercisedEvent",
interfaces[GraphQLContext, ChoiceExercised](NodeType, EventType),
() =>
fields[GraphQLContext, ChoiceExercised](
Field(
"transaction",
TransactionType,
resolve = context =>
context.ctx.ledger.transaction(context.value.transactionId, context.ctx.templates).get,
),
Field(
"contract",
ContractType,
resolve = context =>
context.ctx.ledger.contract(context.value.contractId, context.ctx.templates).get,
),
Field(
"choice",
StringType,
resolve = context => Tag.unwrap[String, ApiTypes.ChoiceTag](context.value.choice),
),
Field("argument", JsonType.ApiValueType, resolve = _.value.argument),
Field(
"actingParties",
ListType(PartyType),
resolve = _.value.actingParties.map(Tag.unwrap),
),
Field("consuming", BooleanType, resolve = _.value.consuming),
Field(
"children",
ListType(EventType),
resolve =
context => context.ctx.ledger.childEvents(context.value.id, context.ctx.templates),
),
),
)
val TransactionType: ObjectType[GraphQLContext, Transaction] = ObjectType(
"Transaction",
interfaces[GraphQLContext, Transaction](NodeType),
() =>
fields[GraphQLContext, Transaction](
Field("id", IDType, resolve = _.value.idString),
Field("offset", OffsetType, resolve = context => context.value.offset),
Field("effectiveAt", TimeType, resolve = _.value.effectiveAt),
Field("commandId", OptionType(CommandIdType), resolve = _.value.commandId),
Field("events", ListType(EventType), resolve = _.value.events),
),
)
// noinspection ForwardReference
val ContractType = ObjectType(
"Contract",
interfaces[GraphQLContext, Contract](NodeType),
() =>
fields[GraphQLContext, Contract](
Field("id", IDType, resolve = _.value.idString),
Field("template", TemplateType, resolve = _.value.template),
Field(
"createEvent",
CreatedEventType,
resolve =
context => context.ctx.ledger.createEventOf(context.value, context.ctx.templates),
),
Field(
"archiveEvent",
OptionType(ExercisedEventType),
resolve =
context => context.ctx.ledger.archiveEventOf(context.value, context.ctx.templates),
),
Field(
"exerciseEvents",
ListType(ExercisedEventType),
resolve =
context => context.ctx.ledger.exercisedEventsOf(context.value, context.ctx.templates),
),
Field("argument", JsonType.ApiRecordType, resolve = _.value.argument),
Field("agreementText", OptionType(StringType), resolve = _.value.agreementText),
Field("signatories", ListType(StringType), resolve = _.value.signatories.map(Tag.unwrap)),
Field("observers", ListType(StringType), resolve = _.value.observers.map(Tag.unwrap)),
Field("key", OptionType(JsonType.ApiValueType), resolve = _.value.key),
),
)
val ContractEdgeType = ObjectType(
"ContractEdge",
fields[Unit, Contract](
Field("node", ContractType, resolve = _.value),
Field("cursor", StringType, resolve = _.value.idString),
),
)
val ContractPaginationType = ObjectType(
"ContractPagination",
fields[Unit, Page[Contract]](
Field("beforeCount", IntType, resolve = _.value.offset),
Field("totalCount", IntType, resolve = _.value.total),
Field("sortings", OptionType(ListType(SortingType)), resolve = _.value.sortedLike),
Field("edges", ListType(ContractEdgeType), resolve = _.value.rows),
),
)
val FilterCriterionType = InputObjectType(
"FilterCriterion",
List(
InputField("field", StringType),
InputField("value", StringType),
),
)
val SortCriterionType = InputObjectType(
"SortCriterion",
List(
InputField("field", StringType),
InputField("direction", DirectionType),
),
)
// noinspection ForwardReference
val CommandStatusType: InterfaceType[GraphQLContext, CommandStatus] = InterfaceType(
"CommandStatus",
() =>
fields[GraphQLContext, CommandStatus](
Field("completed", BooleanType, resolve = _.value.isCompleted)
),
).withPossibleObjectTypes(() =>
List(
CommandStatusErrorType,
CommandStatusWaitingType,
CommandStatusSuccessType,
CommandStatusUnknownType,
)
)
val CommandStatusErrorType: ObjectType[GraphQLContext, CommandStatusError] = ObjectType(
"CommandStatusError",
interfaces[GraphQLContext, CommandStatusError](CommandStatusType),
() =>
fields[GraphQLContext, CommandStatusError](
Field("code", StringType, resolve = _.value.code),
Field("details", StringType, resolve = _.value.details),
),
)
val CommandStatusWaitingType: ObjectType[GraphQLContext, CommandStatusWaiting] = ObjectType(
"CommandStatusWaiting",
interfaces[GraphQLContext, CommandStatusWaiting](CommandStatusType),
() =>
fields[GraphQLContext, CommandStatusWaiting](
),
)
val CommandStatusUnknownType: ObjectType[GraphQLContext, CommandStatusUnknown] = ObjectType(
"CommandStatusUnknown",
interfaces[GraphQLContext, CommandStatusUnknown](CommandStatusType),
() =>
fields[GraphQLContext, CommandStatusUnknown](
),
)
val CommandStatusSuccessType: ObjectType[GraphQLContext, CommandStatusSuccess] = ObjectType(
"CommandStatusSuccess",
interfaces[GraphQLContext, CommandStatusSuccess](CommandStatusType),
() =>
fields[GraphQLContext, CommandStatusSuccess](
Field("transaction", TransactionType, resolve = _.value.tx)
),
)
// noinspection ForwardReference
val CommandType: InterfaceType[GraphQLContext, Command] = InterfaceType(
"Command",
() =>
fields[GraphQLContext, Command](
Field("id", IDType, resolve = _.value.idString),
Field("index", IntType, resolve = _.value.index.toInt),
Field(
"workflowId",
IDType,
resolve = context => Tag.unwrap[String, ApiTypes.WorkflowIdTag](context.value.workflowId),
),
Field("platformTime", TimeType, resolve = _.value.platformTime),
Field(
"status",
CommandStatusType,
resolve = context =>
context.ctx.ledger
.statusOf(context.value.id, context.ctx.templates)
.getOrElse(CommandStatusUnknown()),
),
),
).withPossibleObjectTypes(() => List(CreateCommandType, ExerciseCommandType))
val CreateCommandType: ObjectType[GraphQLContext, CreateCommand] = ObjectType(
"CreateCommand",
interfaces[GraphQLContext, CreateCommand](NodeType, CommandType),
() =>
fields[GraphQLContext, CreateCommand](
Field(
"template",
OptionType(TemplateType),
resolve = context => context.ctx.templates.template(context.value.template),
),
Field("templateId", StringType, resolve = context => context.value.template.asOpaqueString),
Field("argument", JsonType.ApiRecordType, resolve = _.value.argument),
),
)
val ExerciseCommandType: ObjectType[GraphQLContext, ExerciseCommand] = ObjectType(
"ExerciseCommand",
interfaces[GraphQLContext, ExerciseCommand](NodeType, CommandType),
() =>
fields[GraphQLContext, ExerciseCommand](
Field(
"contract",
OptionType(ContractType),
resolve =
context => context.ctx.ledger.contract(context.value.contract, context.ctx.templates),
),
Field(
"contractId",
StringType,
resolve = context => Tag.unwrap[String, ApiTypes.ContractIdTag](context.value.contract),
),
Field(
"interfaceId",
OptionType(StringType),
resolve = _.value.interfaceId.map(_.asOpaqueString),
),
Field(
"choice",
StringType,
resolve = context => Tag.unwrap[String, ApiTypes.ChoiceTag](context.value.choice),
),
Field("argument", JsonType.ApiValueType, resolve = _.value.argument),
),
)
val CommandEdgeType = ObjectType(
"CommandEdge",
fields[GraphQLContext, Command](
Field("node", CommandType, resolve = _.value),
Field("cursor", StringType, resolve = _.value.idString),
),
)
val CommandPaginationType = ObjectType(
"CommandPagination",
fields[GraphQLContext, Page[Command]](
Field("beforeCount", IntType, resolve = _.value.offset),
Field("totalCount", IntType, resolve = _.value.total),
Field("sortings", OptionType(ListType(SortingType)), resolve = _.value.sortedLike),
Field("edges", ListType(CommandEdgeType), resolve = _.value.rows),
),
)
// noinspection ForwardReference
val DamlLfDefDataTypeType: ObjectType[GraphQLContext, DamlLfDefDataTypeBoxed] = ObjectType(
"DamlLfDefDataType",
interfaces[GraphQLContext, DamlLfDefDataTypeBoxed](NodeType, DamlLfNodeType),
() =>
fields[GraphQLContext, DamlLfDefDataTypeBoxed](
Field("dataType", JsonType.DamlLfDataTypeType, resolve = _.value.value.dataType),
Field("typeVars", ListType(StringType), resolve = _.value.value.typeVars),
Field(
"dependencies",
ListType(DamlLfDefDataTypeType),
arguments = DepthArg :: Nil,
resolve = context =>
context.ctx.templates
.typeDependencies(context.value.value, context.arg(DepthArg).getOrElse(Int.MaxValue))
.toList
.map(t => DamlLfDefDataTypeBoxed(t._1, t._2)),
),
),
)
val DamlLfFieldWithTypeType = ObjectType(
"DamlLfFieldWithType",
fields[GraphQLContext, DamlLfFieldWithType](
Field("name", StringType, resolve = _.value._1),
Field("type", JsonType.DamlLfTypeType, resolve = _.value._2),
),
)
// ------------------------------------------------------------------------------------------------------------------
// Queries
// ------------------------------------------------------------------------------------------------------------------
val TypeArg = Argument("typename", StringType)
val IDArg = Argument("id", IDType)
val IDListArg = Argument("ids", ListInputType(IDType))
@GraphQLDeprecated("use filter intead")
val SearchArg = Argument("search", OptionInputType(StringType))
val FilterArg = Argument("filter", OptionInputType(ListInputType(FilterCriterionType)))
val CountArg = Argument("count", OptionInputType(IntType))
val StartArg = Argument("start", OptionInputType(StringType))
val SortArg = Argument("sort", OptionInputType(ListInputType(SortCriterionType)))
val IncludeArchivedArg = Argument("includeArchived", OptionInputType(BooleanType))
val DeclarationArg = Argument("topLevelDecl", StringType)
val DepthArg = Argument("depth", OptionInputType(IntType))
val QueryType = ObjectType(
"Query",
fields[GraphQLContext, Unit](
Field(
"parties",
ListType(PartyType),
arguments = SearchArg :: Nil,
resolve = context =>
(context.ctx.store ? GetParties(context.arg(SearchArg).getOrElse("")))
.mapTo[PartyList]
.map(response => Tag.unsubst(response.parties)),
),
Field(
"ledgerTime",
LedgerTimeType,
resolve = context =>
(context.ctx.store ? ReportCurrentTime)
.mapTo[Try[TimeProviderWithType]]
.map(t => t.get),
),
Field(
"latestTransaction",
OptionType(TransactionType),
resolve = context => context.ctx.ledger.latestTransaction(context.ctx.templates),
),
Field(
"node",
OptionType(NodeType),
arguments = TypeArg :: IDArg :: Nil,
resolve = context =>
context.arg(TypeArg) match {
case ContractType.name =>
context.ctx.ledger
.contract(ApiTypes.ContractId(context.arg(IDArg)), context.ctx.templates)
case TemplateType.name =>
context.ctx.templates.templateByStringId(TemplateStringId(context.arg(IDArg)))
case CommandType.name =>
context.ctx.ledger
.command(ApiTypes.CommandId(context.arg(IDArg)), context.ctx.templates)
case EventType.name =>
context.ctx.ledger.event(ApiTypes.EventId(context.arg(IDArg)), context.ctx.templates)
case TransactionType.name =>
context.ctx.ledger
.transaction(ApiTypes.TransactionId(context.arg(IDArg)), context.ctx.templates)
case DamlLfDefDataTypeType.name =>
for {
id <- parseOpaqueIdentifier(context.arg(IDArg))
ddt <- context.ctx.templates.damlLfDefDataType(id)
} yield DamlLfDefDataTypeBoxed(id, ddt)
case _ => None
},
),
Field(
"template",
ListType(TemplateType),
arguments = DeclarationArg :: Nil,
resolve = context => context.ctx.templates.templatesByName(context.arg(DeclarationArg)),
),
Field(
"nodes",
ListType(NodeType),
arguments = TypeArg :: IDListArg :: Nil,
resolve = context => {
val ledger = context.ctx.ledger
val ids = context.arg(IDListArg).toSet
context.arg(TypeArg) match {
case ContractType.name =>
ids
.map(id => ApiTypes.ContractId(id.toString))
.flatMap(ledger.contract(_, context.ctx.templates).toList)
.toSeq
case TemplateType.name =>
ids
.map(id => TemplateStringId(id.toString))
.flatMap(context.ctx.templates.templateByStringId(_).toList)
.toSeq
case CommandType.name =>
ids
.map(id => ApiTypes.CommandId(id.toString))
.flatMap(context.ctx.ledger.command(_, context.ctx.templates).toList)
.toSeq
case EventType.name =>
ids
.map(id => ApiTypes.EventId(id.toString))
.flatMap(context.ctx.ledger.event(_, context.ctx.templates).toList)
.toSeq
case TransactionType.name =>
ids
.map(id => ApiTypes.TransactionId(id.toString))
.flatMap(context.ctx.ledger.transaction(_, context.ctx.templates).toList)
.toSeq
case _ => Seq.empty[Node[_]]
}
},
),
Field(
"templates",
TemplatePaginationType,
arguments = SearchArg :: FilterArg :: CountArg :: StartArg :: SortArg :: Nil,
resolve =
context => buildTemplatePager(context).fetch(context.ctx.ledger, context.ctx.templates),
),
Field(
"contracts",
ContractPaginationType,
arguments =
SearchArg :: FilterArg :: IncludeArchivedArg :: CountArg :: StartArg :: SortArg :: Nil,
resolve =
context => buildContractPager(context).fetch(context.ctx.ledger, context.ctx.templates),
),
Field(
"commands",
CommandPaginationType,
arguments = SearchArg :: FilterArg :: CountArg :: StartArg :: SortArg :: Nil,
resolve =
context => buildCommandPager(context).fetch(context.ctx.ledger, context.ctx.templates),
),
Field(
"commandStatus",
OptionType(CommandStatusType),
arguments = IDArg :: Nil,
resolve = context =>
context.ctx.ledger
.statusOf(ApiTypes.CommandId(context.arg(IDArg)), context.ctx.templates)
.getOrElse(CommandStatusUnknown()),
),
) ++ customEndpoints.map(_.endpoint),
)
// ------------------------------------------------------------------------------------------------------------------
// Mutations
// ------------------------------------------------------------------------------------------------------------------
val TimeArgument = Argument("time", TimeType)
val TemplateIdArgument = Argument("templateId", IDType)
val ContractIdArgument = Argument("contractId", IDType)
val ChoiceIdArgument = Argument("choiceId", IDType)
val InterfaceIdArgument = Argument("interfaceId", OptionInputType(IDType))
val AnyArgument = Argument("argument", OptionInputType(JsonType.ApiValueType))
val MutationType = ObjectType(
"Mutation",
fields[GraphQLContext, Unit](
Field(
"advanceTime",
LedgerTimeType,
arguments = TimeArgument :: Nil,
resolve = context =>
(context.ctx.store ? AdvanceTime(context.arg(TimeArgument)))
.mapTo[Try[TimeProviderWithType]]
.map(t => t.get),
),
Field(
"create",
CommandIdType,
arguments = TemplateIdArgument :: AnyArgument :: Nil,
resolve = context => {
val command = CreateContract(
context.ctx.party,
TemplateStringId(context.arg(TemplateIdArgument)),
context.arg(AnyArgument).collect({ case r: ApiRecord => r }).orNull,
)
wrapError((context.ctx.store ? command).mapTo[Try[ApiTypes.CommandId]])
},
),
Field(
"exercise",
CommandIdType,
arguments =
ContractIdArgument :: InterfaceIdArgument :: ChoiceIdArgument :: AnyArgument :: Nil,
resolve = context => {
val command = ExerciseChoice(
context.ctx.party,
ApiTypes.ContractId(context.arg(ContractIdArgument)),
context.arg(InterfaceIdArgument).map(InterfaceStringId(_)),
ApiTypes.Choice(context.arg(ChoiceIdArgument)),
context.arg(AnyArgument).orNull,
)
wrapError((context.ctx.store ? command).mapTo[Try[ApiTypes.CommandId]])
},
),
),
)
val QuerySchema = Schema(QueryType, Some(MutationType))
case object InstantCoercionViolation extends ValueCoercionViolation("ISO-8601 timestamp expected")
private def buildContractPager(context: Context[GraphQLContext, _]): Pager[Contract] =
buildContractPager(
context,
if (shouldIncludeArchived(context)) AllContractsPager else ActiveContractsPager,
)
private def buildTemplateContractPager(
context: Context[GraphQLContext, Template]
): Pager[Contract] =
buildContractPager(
context,
if (shouldIncludeArchived(context)) {
new TemplateContractPager(context.value)
} else {
new ActiveTemplateContractPager(context.value)
},
)
private def shouldIncludeArchived(context: Context[_, _]): Boolean =
context.arg(IncludeArchivedArg).getOrElse(false)
def contractSearchToFilter(search: String): FilterCriterionBase =
OrFilterCriterion(
List(
FilterCriterion("id", search),
FilterCriterion("template.id", search),
FilterCriterion("template.topLevelDecl", search),
)
)
private def buildContractPager(
context: Context[GraphQLContext, _],
base: Pager[Contract],
): Pager[Contract] = {
val ps = context.ctx.templates.damlLfDefDataType(_)
val searched = context
.arg(SearchArg)
.map(arg => new ContractFilter(contractSearchToFilter(arg), ps, base))
.getOrElse(base)
val filtered = context
.arg(FilterArg)
.map(arg => new ContractFilter(filterCriteria(arg), ps, searched))
.getOrElse(searched)
val sorted = context
.arg(SortArg)
.map(arg => new ContractSorter(sortCriteria(arg), ps, filtered))
.getOrElse(filtered)
buildNodePager(context, sorted)
}
def templateSearchToFilter(search: String): FilterCriterionBase =
OrFilterCriterion(List(FilterCriterion("id", search), FilterCriterion("topLevelDecl", search)))
private def buildTemplatePager(
context: Context[GraphQLContext, _],
base: Pager[Template] = TemplatePager,
): Pager[Template] = {
val ps = context.ctx.templates.damlLfDefDataType(_)
val searched = context
.arg(SearchArg)
.map(arg => new TemplateFilter(templateSearchToFilter(arg), ps, base))
.getOrElse(base)
val filtered = context
.arg(FilterArg)
.map(arg => new TemplateFilter(filterCriteria(arg), ps, searched))
.getOrElse(searched)
val sorted = context
.arg(SortArg)
.map(arg => new TemplateSorter(sortCriteria(arg), ps, filtered))
.getOrElse(filtered)
buildNodePager(context, sorted)
}
def commandSearchToFilter(search: String): FilterCriterionBase =
OrFilterCriterion(List(FilterCriterion("id", search), FilterCriterion("workflowId", search)))
private def buildCommandPager(
context: Context[GraphQLContext, _],
base: Pager[Command] = CommandPager,
): Pager[Command] = {
val ps = context.ctx.templates.damlLfDefDataType(_)
val searched = context
.arg(SearchArg)
.map(arg => new CommandFilter(commandSearchToFilter(arg), ps, base))
.getOrElse(base)
val filtered = context
.arg(FilterArg)
.map(arg => new CommandFilter(filterCriteria(arg), ps, searched))
.getOrElse(searched)
val sorted = context
.arg(SortArg)
.map(arg => new CommandSorter(sortCriteria(arg), ps, filtered))
.getOrElse(filtered)
buildNodePager(context, sorted)
}
private def buildNodePager[N <: Node[_]](context: Context[_, _], sorted: Pager[N]): Pager[N] = {
val shifted = context.arg(StartArg).map(new ShiftingPager(_, sorted)).getOrElse(sorted)
val bounded = context.arg(CountArg).map(new BoundingPager(_, shifted)).getOrElse(shifted)
bounded
}
def sortCriteria(arg: Seq[DefaultInput]): List[SortCriterion] =
arg
.map(input =>
SortCriterion(
input("field").toString,
SortDirection.withName(input("direction").toString),
)
)
.toList
def filterCriteria(arg: Seq[DefaultInput]): AndFilterCriterion =
AndFilterCriterion(
arg.view
.map(input =>
FilterCriterion(
input("field").toString,
input("value").toString,
)
)
.toList
)
private def parseInstant(s: String): Either[Violation, Instant] = Try(Instant.parse(s)) match {
case Success(instant) => Right(instant)
case Failure(_) => Left(InstantCoercionViolation)
}
private def mapError(error: Throwable): Throwable = error match {
case GenericConversionError(message) => UserFacingError(message)
case _ => error
}
private def wrapError[T](value: Try[T]): Try[T] = {
value.recoverWith({ case e: Throwable => Failure(mapError(e)) })
}
private def wrapError[T](value: Future[Try[T]]): Future[T] = {
value
.recoverWith({ case e: Throwable => Future.failed(mapError(e)) })
.flatMap(v => Future.fromTry(wrapError(v)))
}
}

View File

@ -1,105 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.graphql
import com.daml.navigator.model._
import com.daml.navigator.json.ApiCodecVerbose.JsonImplicits._
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
import sangria.ast
import sangria.schema._
import sangria.validation.ValueCoercionViolation
import spray.json._
/** Custom GraphQL scalar type for raw JSON values.
*
* The custom ScalarType is necessary to deliver raw JSON values from the platform to the client
* through the Sangria GraphQL mechanism. We currently use such raw JSON values for contract arguments
* and choice parameters. Their internal structure varies enough that they do not lend themselves well
* to GraphQL queries, and transmitting them in full has no significant effects on performance (<100kB).
* There is still the minor inconvenience that we include a transfer format in our model but this may
* change when we introduce a unified approach to generate all transfer data formats and their
* marshalling/unmarshalling.
*
* Adapted from https://gist.github.com/OlegIlyenko/5b96f4b54f656aac226d3c4bc33fd2a6
*/
@SuppressWarnings(
Array(
"org.wartremover.warts.Product",
"org.wartremover.warts.Serializable",
"org.wartremover.warts.Any",
)
)
object JsonType {
case object JsonCoercionViolation extends ValueCoercionViolation("Not valid JSON")
// TODO: handle exceptions
private def coerceJsonInput[T](
v: sangria.ast.Value
)(implicit fmt: JsonFormat[T]): Either[sangria.validation.Violation, T] = v match {
case ast.StringValue(jsonStr, _, _, _, _) =>
jsonStr.parseJson match {
case jsValue: JsValue => Right(jsValue.convertTo[T])
case _ => Left(JsonCoercionViolation)
}
case _ =>
Left(JsonCoercionViolation)
}
private def coerceUserJsonInput[T](
v: Any
)(implicit fmt: JsonFormat[T]): Either[sangria.validation.Violation, T] = v match {
case jsValue: JsValue => Right(jsValue.convertTo[T])
case _ => Left(JsonCoercionViolation)
}
private def newScalarType[T](name: String)(implicit fmt: JsonFormat[T]): ScalarType[T] =
ScalarType(
name,
coerceOutput = (value, _) => value.toJson,
coerceUserInput = coerceUserJsonInput[T],
coerceInput = coerceJsonInput[T],
)
// ------------------------------------------------------------------------------------------------------------------
// Untyped JSON values???
// ------------------------------------------------------------------------------------------------------------------
def apply(name: String, description: Option[String] = None): ScalarType[JsValue] =
ScalarType[JsValue](
name,
description = description,
coerceOutput = (value, _) => value,
coerceUserInput = {
case v: String => Right(JsString(v): JsValue)
case v: Boolean => Right(JsBoolean(v))
case v: Int => Right(JsNumber(v))
case v: Long => Right(JsNumber(v))
case v: Float => Right(JsNumber(v.toDouble): JsValue)
case v: Double => Right(JsNumber(v): JsValue)
case v: BigInt => Right(JsNumber(v))
case v: BigDecimal => Right(JsNumber(v))
case v: JsValue => Right(v)
},
coerceInput = {
case ast.StringValue(jsonStr, _, _, _, _) =>
Right(jsonStr.parseJson)
case _ =>
Left(JsonCoercionViolation)
},
)
// ------------------------------------------------------------------------------------------------------------------
// Ledger API values
// ------------------------------------------------------------------------------------------------------------------
val ApiValueType: ScalarType[ApiValue] = newScalarType[ApiValue]("DamlLfValue")
val ApiRecordType: ScalarType[ApiRecord] = newScalarType[ApiRecord]("DamlLfValueRecord")
// ------------------------------------------------------------------------------------------------------------------
// Daml-LF types
// ------------------------------------------------------------------------------------------------------------------
val DamlLfTypeType: ScalarType[DamlLfType] = newScalarType[DamlLfType]("DamlLfType")
val DamlLfDataTypeType: ScalarType[DamlLfDataType] =
newScalarType[DamlLfDataType]("DamlLfDataType")
}

View File

@ -1,137 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.graphql
import sangria.marshalling._
import spray.json._
import scala.util.Try
/** Custom marshallers and unmarshallers for dealing with raw JSON values.
*
* The declared implicit objects replace the ones you would usually import from `sangria.marshalling.sprayJson`.
* In fact they are largely identical to those except for the additional support for non-scalar JSON values.
*
* The entire object is only relevant as long as we use raw JSON values in the model.
*
* Adapted from https://gist.github.com/OlegIlyenko/5b96f4b54f656aac226d3c4bc33fd2a6
*/
@SuppressWarnings(Array("org.wartremover.warts.Serializable"))
object SprayMarshallers {
implicit object CustomSprayJsonResultMarshaller extends ResultMarshaller {
type Node = JsValue
type MapBuilder = ArrayMapBuilder[Node]
def emptyMapNode(keys: Seq[String]) = new ArrayMapBuilder[Node](keys)
def addMapNodeElem(
builder: MapBuilder,
key: String,
value: Node,
optional: Boolean,
): ArrayMapBuilder[JsValue] = builder.add(key, value)
def mapNode(builder: MapBuilder) = JsObject(builder.toMap)
def mapNode(keyValues: Seq[(String, JsValue)]) = JsObject(keyValues: _*)
def arrayNode(values: Vector[JsValue]) = JsArray(values)
def optionalArrayNodeValue(value: Option[JsValue]): JsValue = value match {
case Some(v) => v
case None => nullNode
}
def scalarNode(value: Any, typeName: String, info: Set[ScalarValueInfo]): JsValue =
value match {
case v: String => JsString(v)
case v: Boolean => JsBoolean(v)
case v: Int => JsNumber(v)
case v: Long => JsNumber(v)
case v: Float => JsNumber(v.toDouble)
case v: Double => JsNumber(v)
case v: BigInt => JsNumber(v)
case v: BigDecimal => JsNumber(v)
case v: JsValue => v
case v => throw new IllegalArgumentException("Unsupported scalar value: " + v.toString)
}
def enumNode(value: String, typeName: String) = JsString(value)
def nullNode = JsNull
def renderCompact(node: JsValue): String = node.compactPrint
def renderPretty(node: JsValue): String = node.prettyPrint
}
implicit object SprayJsonMarshallerForType extends ResultMarshallerForType[JsValue] {
val marshaller = CustomSprayJsonResultMarshaller
}
implicit object CustomSprayJsonInputUnmarshaller extends InputUnmarshaller[JsValue] {
def getRootMapValue(node: JsValue, key: String): Option[JsValue] =
node.asInstanceOf[JsObject].fields get key
def isListNode(node: JsValue): Boolean = node.isInstanceOf[JsArray]
def getListValue(node: JsValue): Seq[JsValue] = node.asInstanceOf[JsArray].elements
def isMapNode(node: JsValue): Boolean = node.isInstanceOf[JsObject]
def getMapValue(node: JsValue, key: String): Option[JsValue] =
node.asInstanceOf[JsObject].fields get key
def getMapKeys(node: JsValue): Iterable[String] = node.asInstanceOf[JsObject].fields.keys
def isDefined(node: JsValue): Boolean = node != JsNull
def getScalarValue(node: JsValue): Any = node match {
case JsBoolean(b) => b
case JsNumber(d) => d.toBigIntExact getOrElse d
case JsString(s) => s
case n => n
}
def getScalaScalarValue(node: JsValue): Any = getScalarValue(node)
def isEnumNode(node: JsValue): Boolean = node.isInstanceOf[JsString]
def isScalarNode(node: JsValue) = true
def isVariableNode(node: JsValue) = false
def getVariableName(node: JsValue) =
throw new IllegalArgumentException("variables are not supported")
def render(node: JsValue): String = node.compactPrint
}
private object CustomSprayJsonToInput extends ToInput[JsValue, JsValue] {
def toInput(value: JsValue): (JsValue, CustomSprayJsonInputUnmarshaller.type) =
(value, CustomSprayJsonInputUnmarshaller)
}
private object CustomSprayJsonFromInput extends FromInput[JsValue] {
val marshaller = CustomSprayJsonResultMarshaller
def fromResult(node: marshaller.Node): JsValue = node
}
implicit def sprayJsonToInput[T <: JsValue]: ToInput[T, JsValue] =
CustomSprayJsonToInput.asInstanceOf[ToInput[T, JsValue]]
implicit def sprayJsonFromInput[T <: JsValue]: FromInput[T] =
CustomSprayJsonFromInput.asInstanceOf[FromInput[T]]
implicit def sprayJsonWriterToInput[T: JsonWriter]: ToInput[T, JsValue] =
new ToInput[T, JsValue] {
def toInput(value: T): (JsValue, CustomSprayJsonInputUnmarshaller.type) =
implicitly[JsonWriter[T]].write(value) -> CustomSprayJsonInputUnmarshaller
}
implicit def sprayJsonReaderFromInput[T: JsonReader]: FromInput[T] =
new FromInput[T] {
val marshaller = CustomSprayJsonResultMarshaller
def fromResult(node: marshaller.Node): T =
try implicitly[JsonReader[T]].read(node)
catch {
case e: DeserializationException => throw InputParsingError(Vector(e.msg))
}
}
implicit object CustomSprayJsonInputParser extends InputParser[JsValue] {
def parse(str: String) = Try(str.parseJson)
}
}

View File

@ -1,44 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.graphql
import sangria.ast.StringValue
import sangria.schema.ScalarType
import sangria.validation.StringCoercionViolation
import scalaz.{@@, Tag}
/** GraphQL alias type for string value. */
@SuppressWarnings(Array("org.wartremover.warts.Any"))
object StringAliasType {
def apply(name: String, description: Option[String] = None): ScalarType[String] =
ScalarType(
name,
description = description,
coerceOutput = sangria.schema.valueOutput,
coerceUserInput = {
case s: String => Right(s)
case _ => Left(StringCoercionViolation)
},
coerceInput = {
case StringValue(s, _, _, _, _) => Right(s)
case _ => Left(StringCoercionViolation)
},
)
def tagged[T](name: String, description: Option[String] = None): ScalarType[String @@ T] =
ScalarType(
name,
description = description,
coerceOutput = (value, _) => Tag.unwrap(value),
coerceUserInput = {
case s: String => Right(Tag.of[T](s))
case _ => Left(StringCoercionViolation)
},
coerceInput = {
case StringValue(s, _, _, _, _) => Right(Tag.of[T](s))
case _ => Left(StringCoercionViolation)
},
)
}

View File

@ -1,61 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.graphqless
import sangria.schema.{Context, Field}
import shapeless.{::, HList, HNil, LabelledGeneric, Lazy, Witness}
import shapeless.labelled.FieldType
import shapeless.ops.record.Selector
trait GraphQLFields[T, Repr] {
def fields[Ctx]: List[Field[Ctx, T]]
}
object GraphQLFields {
def apply[T, Repr](implicit graphQLFields: GraphQLFields[T, Repr]): GraphQLFields[T, Repr] =
graphQLFields
}
@SuppressWarnings(Array("org.wartremover.warts.Any"))
trait DerivedGraphQLFields {
def mkField[Ctx, C, Repr <: HList, K <: Symbol, V](implicit
kw: Witness.Aux[K],
vField: Lazy[GraphQLOutputType[V]],
gen: LabelledGeneric.Aux[C, Repr],
selector: Selector.Aux[Repr, K, V],
): Field[Ctx, C] =
Field(
name = kw.value.name,
fieldType = vField.value.outputType[Ctx],
resolve = (context: Context[Ctx, C]) => selector(gen.to(context.value)),
)
implicit def singleFieldGraphQLField[C, Repr <: HList, K <: Symbol, V](implicit
kw: Witness.Aux[K],
vField: Lazy[GraphQLOutputType[V]],
gen: LabelledGeneric.Aux[C, Repr],
selector: Selector.Aux[Repr, K, V],
): GraphQLFields[C, FieldType[K, V] :: HNil] = new GraphQLFields[C, FieldType[K, V] :: HNil] {
override def fields[Ctx]: List[Field[Ctx, C]] =
List(mkField[Ctx, C, Repr, K, V])
}
implicit def productGraphQLField[C, Repr <: HList, K <: Symbol, V, T <: HList](implicit
kw: Witness.Aux[K],
vField: Lazy[GraphQLOutputType[V]],
tGraphQLField: Lazy[GraphQLFields[C, T]],
gen: LabelledGeneric.Aux[C, Repr],
selector: Selector.Aux[Repr, K, V],
): GraphQLFields[C, FieldType[K, V] :: T] = new GraphQLFields[C, FieldType[K, V] :: T] {
override def fields[Ctx]: List[Field[Ctx, C]] = {
val head = mkField[Ctx, C, Repr, K, V]
val tail = tGraphQLField.value.fields[Ctx]
head +: tail
}
}
}
object DerivedGraphQLFields extends DerivedGraphQLFields

View File

@ -1,32 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.graphqless
import sangria.schema._
trait GraphQLLeaf[T] {
def to: OutputType[T] with LeafType with Named
}
object GraphQLLeaf {
def apply[T](implicit toGraphQLLeaf: GraphQLLeaf[T]): GraphQLLeaf[T] = toGraphQLLeaf
def instance[T](toImpl: OutputType[T] with LeafType with Named): GraphQLLeaf[T] =
new GraphQLLeaf[T] {
override def to: OutputType[T] with LeafType with Named = toImpl
}
}
trait PrimitiveGraphQLLeaf {
implicit val bigDecimalGraphQLLeaf: GraphQLLeaf[BigDecimal] =
GraphQLLeaf.instance[BigDecimal](BigDecimalType)
implicit val bigIntTypeGraphQLLeaf: GraphQLLeaf[BigInt] = GraphQLLeaf.instance[BigInt](BigIntType)
implicit val booleanToGraphQLLeaf: GraphQLLeaf[Boolean] =
GraphQLLeaf.instance[Boolean](BooleanType)
implicit val doubleToGraphQLLeaf: GraphQLLeaf[Double] = GraphQLLeaf.instance[Double](FloatType)
implicit val intToGraphQLLeaf: GraphQLLeaf[Int] = GraphQLLeaf.instance[Int](IntType)
implicit val longToGraphQLLeaf: GraphQLLeaf[Long] = GraphQLLeaf.instance[Long](LongType)
implicit val stringToGraphQLLeaf: GraphQLLeaf[String] = GraphQLLeaf.instance[String](StringType)
}
object PrimitiveGraphQLLeaf extends PrimitiveGraphQLLeaf

View File

@ -1,39 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.graphqless
import sangria.schema._
import shapeless._
import scala.annotation.nowarn
import scala.reflect.ClassTag
trait GraphQLObject[T] {
def to[Ctx]: ObjectType[Ctx, T]
}
object GraphQLObject {
def apply[T](implicit graphQLObject: GraphQLObject[T]): GraphQLObject[T] = graphQLObject
}
trait DerivedGraphQLObject {
@nowarn("msg=parameter generic .*is never used") // used to calculate Repr0 tparam only
implicit def caseClassGraphQLObject[C, Repr0 <: HList](implicit
classTag: ClassTag[C],
generic: LabelledGeneric.Aux[C, Repr0],
graphQLFields: GraphQLFields[C, Repr0],
): GraphQLObject[C] = new GraphQLObject[C] {
override def to[Ctx]: ObjectType[Ctx, C] = {
val objectName = classTag.runtimeClass.getSimpleName
val reprFields = graphQLFields.fields[Ctx]
ObjectType(
name = objectName,
fields = reprFields,
)
}
}
}
object DerivedGraphQLObject extends DerivedGraphQLObject

View File

@ -1,34 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.graphqless
import sangria.schema.OutputType
trait GraphQLOutputType[T] {
def outputType[Ctx]: OutputType[T]
}
object GraphQLOutputType {
def apply[T](implicit graphQLOutputType: GraphQLOutputType[T]): GraphQLOutputType[T] =
graphQLOutputType
}
trait DerivedGraphQLOutputType {
implicit def graphQLLeafToGraphQLOutputType[T](implicit
graphQLLeaf: GraphQLLeaf[T]
): GraphQLOutputType[T] = new GraphQLOutputType[T] {
override def outputType[Ctx]: OutputType[T] =
graphQLLeaf.to
}
implicit def graphQLObjectToGraphQLOutputType[T](implicit
graphQLObject: GraphQLObject[T]
): GraphQLOutputType[T] = new GraphQLOutputType[T] {
override def outputType[Ctx]: OutputType[T] =
graphQLObject.to[Ctx]
}
}
object DerivedGraphQLOutputType extends DerivedGraphQLOutputType

View File

@ -1,10 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
package object graphqless
extends DerivedGraphQLObject
with DerivedGraphQLFields
with PrimitiveGraphQLLeaf
with DerivedGraphQLOutputType

View File

@ -1,276 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.json
import com.daml.lf.data.{Decimal => LfDecimal, FrontStack, ImmArray, Ref, SortedLookupList}
import com.daml.navigator.{model => Model}
import com.daml.lf.value.{Value => V}
import com.daml.lf.value.json.ApiValueImplicits._
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
import com.daml.navigator.json.Util._
import com.daml.navigator.model.DamlLfIdentifier
import spray.json._
/** A verbose encoding of API values.
*
* The encoded values include type information and can be losslessly encoded.
*/
object ApiCodecVerbose {
// ------------------------------------------------------------------------------------------------------------------
// Constants used in the encoding
// ------------------------------------------------------------------------------------------------------------------
private[this] final val propType: String = "type"
private[this] final val propValue: String = "value"
private[this] final val propLabel: String = "label"
private[this] final val propFields: String = "fields"
private[this] final val propId: String = "id"
private[this] final val propConstructor: String = "constructor"
private[this] final val tagText: String = "text"
private[this] final val tagInt64: String = "int64"
private[this] final val tagNumeric: String = "numeric"
private[this] final val tagBool: String = "bool"
private[this] final val tagContractId: String = "contractid"
private[this] final val tagTimestamp: String = "timestamp"
private[this] final val tagDate: String = "date"
private[this] final val tagParty: String = "party"
private[this] final val tagUnit: String = "unit"
private[this] final val tagOptional: String = "optional"
private[this] final val tagList: String = "list"
private[this] final val tagTextMap: String = "textmap"
private[this] final val tagGenMap: String = "genmap"
private[this] final val tagRecord: String = "record"
private[this] final val tagVariant: String = "variant"
private[this] final val tagEnum: String = "enum"
// ------------------------------------------------------------------------------------------------------------------
// Encoding
// ------------------------------------------------------------------------------------------------------------------
def apiValueToJsValue(value: Model.ApiValue): JsValue = value match {
case v: Model.ApiRecord => apiRecordToJsValue(v)
case v: Model.ApiVariant => apiVariantToJsValue(v)
case v: V.ValueEnum => apiEnumToJsValue(v)
case v: Model.ApiList => apiListToJsValue(v)
case V.ValueText(v) => JsObject(propType -> JsString(tagText), propValue -> JsString(v))
case V.ValueInt64(v) =>
JsObject(propType -> JsString(tagInt64), propValue -> JsString((v: Long).toString))
case V.ValueNumeric(v) =>
JsObject(propType -> JsString(tagNumeric), propValue -> JsString(v.toUnscaledString))
case V.ValueBool(v) => JsObject(propType -> JsString(tagBool), propValue -> JsBoolean(v))
case V.ValueContractId(v) =>
JsObject(propType -> JsString(tagContractId), propValue -> JsString(v.coid))
case v: V.ValueTimestamp =>
JsObject(propType -> JsString(tagTimestamp), propValue -> JsString(v.toIso8601))
case v: V.ValueDate =>
JsObject(propType -> JsString(tagDate), propValue -> JsString(v.toIso8601))
case V.ValueParty(v) =>
JsObject(propType -> JsString(tagParty), propValue -> JsString(v))
case V.ValueUnit => JsObject(propType -> JsString(tagUnit))
case V.ValueOptional(None) => JsObject(propType -> JsString(tagOptional), propValue -> JsNull)
case V.ValueOptional(Some(v)) =>
JsObject(propType -> JsString(tagOptional), propValue -> apiValueToJsValue(v))
case v: Model.ApiMap => apiTextMapToJsValue(v)
case v: Model.ApiGenMap => apiGenMapToJsValue(v)
}
def apiListToJsValue(value: Model.ApiList): JsValue =
JsObject(
propType -> JsString(tagList),
propValue -> JsArray(value.values.map(apiValueToJsValue).toImmArray.toSeq: _*),
)
def apiTextMapToJsValue(value: Model.ApiMap): JsValue =
JsObject(
propType -> JsString(tagTextMap),
propValue -> JsArray(value.value.toImmArray.toSeq.toVector.map { case (k, v) =>
JsObject("key" -> JsString(k), "value" -> apiValueToJsValue(v))
}),
)
def apiGenMapToJsValue(value: Model.ApiGenMap): JsValue =
JsObject(
propType -> JsString(tagGenMap),
propValue -> JsArray(value.entries.toSeq.toVector.map { case (k, v) =>
JsObject("key" -> apiValueToJsValue(k), "value" -> apiValueToJsValue(v))
}),
)
def apiVariantToJsValue(value: Model.ApiVariant): JsValue =
JsObject(
propType -> JsString(tagVariant),
propId -> value.tycon.map(_.toJson).getOrElse(JsNull),
propConstructor -> JsString(value.variant),
propValue -> apiValueToJsValue(value.value),
)
def apiEnumToJsValue(value: V.ValueEnum): JsValue =
JsObject(
propType -> JsString(tagEnum),
propId -> value.tycon.map(_.toJson).getOrElse(JsNull),
propConstructor -> JsString(value.value),
)
def apiRecordToJsValue(value: Model.ApiRecord): JsValue =
JsObject(
propType -> JsString(tagRecord),
propId -> value.tycon.map(_.toJson).getOrElse(JsNull),
propFields -> JsArray(value.fields.toSeq.zipWithIndex.map { case ((oflabel, fvalue), ix) =>
JsObject(
propLabel -> JsString(oflabel getOrElse (ix: Int).toString),
propValue -> apiValueToJsValue(fvalue),
)
}.toVector),
)
// ------------------------------------------------------------------------------------------------------------------
// Decoding
// ------------------------------------------------------------------------------------------------------------------
private[this] def jsValueToApiRecordField(value: JsValue): Model.ApiRecordField = {
val label = strField(value, propLabel, "ApiRecordField")
val avalue = jsValueToApiValue(anyField(value, propValue, "ApiRecordField"))
(Some(assertDE(Ref.Name fromString label)), avalue)
}
def jsValueToApiValue(value: JsValue): Model.ApiValue =
strField(value, propType, "ApiValue") match {
case `tagRecord` => jsValueToApiRecord(value)
case `tagVariant` => jsValueToApiVariant(value)
case `tagEnum` => jsValueToApiEnum(value)
case `tagList` =>
V.ValueList(arrayField(value, propValue, "ApiList").map(jsValueToApiValue).to(FrontStack))
case `tagText` => V.ValueText(strField(value, propValue, "ApiText"))
case `tagInt64` => V.ValueInt64(strField(value, propValue, "ApiInt64").toLong)
case `tagNumeric` =>
V.ValueNumeric(assertDE(LfDecimal fromString strField(value, propValue, "ApiNumeric")))
case `tagBool` => V.ValueBool(boolField(value, propValue, "ApiBool"))
case `tagContractId` =>
V.ValueContractId(
assertDE(V.ContractId.fromString(strField(value, propValue, "ApiContractId")))
)
case `tagTimestamp` =>
V.ValueTimestamp.fromIso8601(strField(value, propValue, "ApiTimestamp"))
case `tagDate` => V.ValueDate.fromIso8601(strField(value, propValue, "ApiDate"))
case `tagParty` =>
V.ValueParty(assertDE(Ref.Party fromString strField(value, propValue, "ApiParty")))
case `tagUnit` => V.ValueUnit
case `tagOptional` =>
anyField(value, propValue, "ApiOptional") match {
case JsNull => V.ValueNone
case v => V.ValueOptional(Some(jsValueToApiValue(v)))
}
case `tagTextMap` =>
V.ValueTextMap(
SortedLookupList
.fromImmArray(
arrayField(value, propValue, "ApiMap").view.map(jsValueToMapEntry).to(ImmArray)
)
.fold(
err => deserializationError(s"Can't read ${value.prettyPrint} as ApiValue, $err'"),
identity,
)
)
case `tagGenMap` =>
V.ValueGenMap(
arrayField(value, propValue, "ApiGenMap").view.map(jsValueToGenMapEntry).to(ImmArray)
)
case t =>
deserializationError(s"Can't read ${value.prettyPrint} as ApiValue, unknown type '$t'")
}
def jsValueToApiRecord(value: JsValue): Model.ApiRecord =
strField(value, propType, "ApiRecord") match {
case `tagRecord` =>
V.ValueRecord(
asObject(value, "ApiRecord").fields
.get(propId)
.flatMap(_.convertTo[Option[DamlLfIdentifier]]),
arrayField(value, propFields, "ApiRecord").map(jsValueToApiRecordField).to(ImmArray),
)
case t =>
deserializationError(
s"Can't read ${value.prettyPrint} as ApiRecord, type '$t' is not a record"
)
}
def jsValueToMapEntry(value: JsValue): (String, Model.ApiValue) = {
val translation = value match {
case JsObject(map) =>
for {
key <- map.get("key").collect { case JsString(s) => s }
value <- map.get("value").map(jsValueToApiValue)
} yield key -> value
case _ => None
}
translation.getOrElse(deserializationError(s"Can't read ${value.prettyPrint} as a map entry"))
}
def jsValueToGenMapEntry(value: JsValue): (Model.ApiValue, Model.ApiValue) = {
val translation = value match {
case JsObject(genMap) =>
for {
key <- genMap.get("key").map(jsValueToApiValue)
value <- genMap.get("value").map(jsValueToApiValue)
} yield key -> value
case _ => None
}
translation.getOrElse(deserializationError(s"Can't read ${value.prettyPrint} as a map entry"))
}
def jsValueToApiVariant(value: JsValue): Model.ApiVariant =
strField(value, propType, "ApiVariant") match {
case `tagVariant` =>
V.ValueVariant(
asObject(value, "ApiVariant").fields
.get(propId)
.flatMap(_.convertTo[Option[DamlLfIdentifier]]),
assertDE(Ref.Name fromString strField(value, propConstructor, "ApiVariant")),
jsValueToApiValue(anyField(value, propValue, "ApiVariant")),
)
case t =>
deserializationError(
s"Can't read ${value.prettyPrint} as ApiVariant, type '$t' is not a variant"
)
}
def jsValueToApiEnum(value: JsValue): V.ValueEnum =
strField(value, propType, "ApiEnum") match {
case `tagEnum` =>
V.ValueEnum(
asObject(value, "ApiEnum").fields
.get(propId)
.flatMap(_.convertTo[Option[DamlLfIdentifier]]),
assertDE(Ref.Name fromString strField(value, propConstructor, "ApiEnum")),
)
case t =>
deserializationError(
s"Can't read ${value.prettyPrint} as ApiEnum, type '$t' is not a enum"
)
}
// ------------------------------------------------------------------------------------------------------------------
// Implicits that can be imported for .parseJson and .toJson functions
// ------------------------------------------------------------------------------------------------------------------
object JsonImplicits extends DefaultJsonProtocol {
implicit object ApiValueJsonFormat extends RootJsonFormat[Model.ApiValue] {
def write(v: Model.ApiValue): JsValue = apiValueToJsValue(v)
def read(v: JsValue): Model.ApiValue = jsValueToApiValue(v)
}
implicit object ApiRecordJsonFormat extends RootJsonFormat[Model.ApiRecord] {
def write(v: Model.ApiRecord): JsValue = apiRecordToJsValue(v)
def read(v: JsValue): Model.ApiRecord = jsValueToApiRecord(v)
}
implicit object ApiVariantJsonFormat extends RootJsonFormat[Model.ApiVariant] {
def write(v: Model.ApiVariant): JsValue = apiVariantToJsValue(v)
def read(v: JsValue): Model.ApiVariant = jsValueToApiVariant(v)
}
}
private[this] def assertDE[A](ea: Either[String, A]): A =
ea.fold(deserializationError(_), identity)
}

View File

@ -1,258 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.json
import com.daml.lf.data.{Ref => DamlLfRef, Numeric => DamlLfNumeric}
import com.daml.navigator.json.Util._
import com.daml.navigator.{model => Model}
import spray.json._
/** An encoding of Daml-LF types.
*
* The types are encoded as-is (no type variables substitution, no type reference resolution).
*/
object DamlLfCodec {
// ------------------------------------------------------------------------------------------------------------------
// Constants used in the encoding
// ------------------------------------------------------------------------------------------------------------------
private[this] final val propType: String = "type"
private[this] final val propValue: String = "value"
private[this] final val propName: String = "name"
private[this] final val propModule: String = "module"
private[this] final val propPackage: String = "package"
private[this] final val propArgs: String = "args"
private[this] final val propVars: String = "vars"
private[this] final val propFields: String = "fields"
private[this] final val propConstructors: String = "constructors"
private[this] final val propScale: String = "scale"
private[this] final val tagTypeCon: String = "typecon"
private[this] final val tagTypeVar: String = "typevar"
private[this] final val tagTypePrim: String = "primitive"
private[this] final val tagTypeList: String = "list"
private[this] final val tagTypeBool: String = "bool"
private[this] final val tagTypeNumeric: String = "numeric"
private[this] final val tagTypeInt64: String = "int64"
private[this] final val tagTypeContractId: String = "contractid"
private[this] final val tagTypeDate: String = "date"
private[this] final val tagTypeParty: String = "party"
private[this] final val tagTypeText: String = "text"
private[this] final val tagTypeTimestamp: String = "timestamp"
private[this] final val tagTypeUnit: String = "unit"
private[this] final val tagTypeRecord: String = "record"
private[this] final val tagTypeVariant: String = "variant"
private[this] final val tagTypeEnum: String = "enum"
private[this] final val tagTypeOptional: String = "optional"
private[this] final val tagTypeTextMap: String = "textmap"
private[this] final val tagTypeGenMap: String = "genmap"
// ------------------------------------------------------------------------------------------------------------------
// Encoding
// ------------------------------------------------------------------------------------------------------------------
def damlLfTypeToJsValue(value: Model.DamlLfType): JsValue = value match {
case typeCon: Model.DamlLfTypeCon => damlLfTypeConToJsValue(typeCon)
case typePrim: Model.DamlLfTypePrim => damlLfPrimToJsValue(typePrim)
case typeVar: Model.DamlLfTypeVar => damlLfTypeVarToJsValue(typeVar)
case typeNum: Model.DamlLfTypeNumeric => damlLfNumericToJsValue(typeNum)
}
def damlLfTypeConToJsValue(value: Model.DamlLfTypeCon): JsValue = {
val id =
Model.DamlLfIdentifier(value.name.identifier.packageId, value.name.identifier.qualifiedName)
JsObject(
propType -> JsString(tagTypeCon),
propName -> damlLfIdentifierToJsValue(id),
propArgs -> JsArray(value.typArgs.map(damlLfTypeToJsValue).toVector),
)
}
def damlLfTypeVarToJsValue(value: Model.DamlLfTypeVar): JsValue = JsObject(
propType -> JsString(tagTypeVar),
propName -> JsString(value.name),
)
def damlLfPrimToJsValue(value: Model.DamlLfTypePrim): JsValue = JsObject(
propType -> JsString(tagTypePrim),
propName -> damlLfPrimTypeToJsValue(value.typ),
propArgs -> JsArray(value.typArgs.map(damlLfTypeToJsValue).toVector),
)
def damlLfPrimTypeToJsValue(value: Model.DamlLfPrimType): JsString = value match {
case Model.DamlLfPrimType.List => JsString(tagTypeList)
case Model.DamlLfPrimType.ContractId => JsString(tagTypeContractId)
case Model.DamlLfPrimType.Bool => JsString(tagTypeBool)
case Model.DamlLfPrimType.Int64 => JsString(tagTypeInt64)
case Model.DamlLfPrimType.Date => JsString(tagTypeDate)
case Model.DamlLfPrimType.Party => JsString(tagTypeParty)
case Model.DamlLfPrimType.Text => JsString(tagTypeText)
case Model.DamlLfPrimType.Timestamp => JsString(tagTypeTimestamp)
case Model.DamlLfPrimType.Optional => JsString(tagTypeOptional)
case Model.DamlLfPrimType.TextMap => JsString(tagTypeTextMap)
case Model.DamlLfPrimType.GenMap => JsString(tagTypeGenMap)
case Model.DamlLfPrimType.Unit => JsString(tagTypeUnit)
}
def damlLfNumericToJsValue(typeNum: Model.DamlLfTypeNumeric): JsValue = JsObject(
propType -> JsString(tagTypeNumeric),
propScale -> JsNumber(typeNum.scale),
)
def damlLfIdentifierToJsValue(value: Model.DamlLfIdentifier): JsValue = JsObject(
propName -> JsString(value.qualifiedName.name.toString()),
propModule -> JsString(value.qualifiedName.module.toString()),
propPackage -> JsString(value.packageId),
)
def damlLfDataTypeToJsValue(value: Model.DamlLfDataType): JsValue = value match {
case r: Model.DamlLfRecord =>
JsObject(
propType -> JsString(tagTypeRecord),
propFields -> JsArray(
r.fields
.map(f => JsObject(propName -> JsString(f._1), propValue -> damlLfTypeToJsValue(f._2)))
.toVector
),
)
case v: Model.DamlLfVariant =>
JsObject(
propType -> JsString(tagTypeVariant),
propFields -> JsArray(
v.fields
.map(f => JsObject(propName -> JsString(f._1), propValue -> damlLfTypeToJsValue(f._2)))
.toVector
),
)
case e: Model.DamlLfEnum =>
JsObject(
propType -> JsString(tagTypeEnum),
propConstructors -> JsArray(e.constructors.map(JsString(_)).toVector),
)
}
def damlLfDefDataTypeToJsValue(value: Model.DamlLfDefDataType): JsValue = JsObject(
propType -> damlLfDataTypeToJsValue(value.dataType),
propVars -> JsArray(value.typeVars.map(JsString(_)).toVector),
)
// ------------------------------------------------------------------------------------------------------------------
// Decoding
// ------------------------------------------------------------------------------------------------------------------
def jsValueToDamlLfType(value: JsValue): Model.DamlLfType =
strField(value, propType, "DamlLfType") match {
case `tagTypeCon` =>
Model.DamlLfTypeCon(
Model.DamlLfTypeConName(
jsValueToDamlLfIdentifier(anyField(value, propName, "DamlLfTypeCon"))
),
Model.DamlLfImmArraySeq(
arrayField(value, propArgs, "DamlLfTypeCon").map(jsValueToDamlLfType): _*
),
)
case `tagTypeVar` => Model.DamlLfTypeVar(nameField(value, propName, "DamlLfTypeVar"))
case `tagTypePrim` =>
Model.DamlLfTypePrim(
jsValueToDamlLfPrimType(strField(value, propName, "DamlLfTypePrim")),
Model.DamlLfImmArraySeq(
arrayField(value, propArgs, "DamlLfTypePrim").map(jsValueToDamlLfType): _*
),
)
case `tagTypeNumeric` =>
DamlLfNumeric.Scale
.fromLong(intField(value, propScale, "DamlLfTypeNumeric"))
.fold[Model.DamlLfTypeNumeric](deserializationError(_), Model.DamlLfTypeNumeric)
}
def jsValueToDamlLfPrimType(value: String): Model.DamlLfPrimType = value match {
case `tagTypeList` => Model.DamlLfPrimType.List
case `tagTypeContractId` => Model.DamlLfPrimType.ContractId
case `tagTypeBool` => Model.DamlLfPrimType.Bool
case `tagTypeInt64` => Model.DamlLfPrimType.Int64
case `tagTypeDate` => Model.DamlLfPrimType.Date
case `tagTypeParty` => Model.DamlLfPrimType.Party
case `tagTypeText` => Model.DamlLfPrimType.Text
case `tagTypeTimestamp` => Model.DamlLfPrimType.Timestamp
case `tagTypeUnit` => Model.DamlLfPrimType.Unit
case `tagTypeOptional` => Model.DamlLfPrimType.Optional
case `tagTypeTextMap` => Model.DamlLfPrimType.TextMap
}
def jsValueToDamlLfDataType(value: JsValue): Model.DamlLfDataType =
strField(value, propType, "DamlLfDefDataType") match {
case `tagTypeRecord` =>
val fields = arrayField(value, propFields, "DamlLfRecord")
Model.DamlLfRecord(
Model.DamlLfImmArraySeq(
fields.map(f =>
(
nameField(f, propName, "DamlLfRecord"),
jsValueToDamlLfType(anyField(f, propValue, "DamlLfRecord")),
)
): _*
)
)
case `tagTypeVariant` =>
val constructors = arrayField(value, propFields, "DamlLfVariant")
Model.DamlLfVariant(
Model.DamlLfImmArraySeq(
constructors.map(f =>
(
nameField(f, propName, "DamlLfVariant"),
jsValueToDamlLfType(anyField(f, propValue, "DamlLfVariant")),
)
): _*
)
)
case `tagTypeEnum` =>
val constructors = arrayField(value, propConstructors, "DamlLfEnum")
Model.DamlLfEnum(Model.DamlLfImmArraySeq(constructors: _*).map(asName(_, "DamlLfEnum")))
case t =>
deserializationError(
s"Can't read ${value.prettyPrint} as DamlLfDataType, unknown type '$t'"
)
}
def jsValueToDamlLfDefDataType(value: JsValue): Model.DamlLfDefDataType = {
val vars =
arrayField(value, propVars, "DamlLfDefDataType").map(v => asName(v, "DamlLfDefDataType"))
val dataType = jsValueToDamlLfDataType(anyField(value, propType, "DamlLfDefDataType"))
Model.DamlLfDefDataType(Model.DamlLfImmArraySeq(vars: _*), dataType)
}
def jsValueToDamlLfIdentifier(value: JsValue): Model.DamlLfIdentifier =
Model.DamlLfIdentifier(
DamlLfRef.PackageId.assertFromString(strField(value, propPackage, "DamlLfIdentifier")),
Model.DamlLfQualifiedName(
Model.DamlLfDottedName.assertFromString(strField(value, propModule, "DamlLfIdentifier")),
Model.DamlLfDottedName.assertFromString(strField(value, propName, "DamlLfIdentifier")),
),
)
// ------------------------------------------------------------------------------------------------------------------
// Implicits that can be imported for .parseJson and .toJson functions
// ------------------------------------------------------------------------------------------------------------------
object JsonImplicits extends DefaultJsonProtocol {
implicit object DamlLfTypeJsonFormat extends RootJsonFormat[Model.DamlLfType] {
def write(v: Model.DamlLfType): JsValue = damlLfTypeToJsValue(v)
def read(v: JsValue): Model.DamlLfType = jsValueToDamlLfType(v)
}
implicit object DamlLfDataTypeJsonFormat extends RootJsonFormat[Model.DamlLfDataType] {
def write(v: Model.DamlLfDataType): JsValue = damlLfDataTypeToJsValue(v)
def read(v: JsValue): Model.DamlLfDataType = jsValueToDamlLfDataType(v)
}
implicit object DamlLfDefDataTypeJsonFormat extends RootJsonFormat[Model.DamlLfDefDataType] {
def write(v: Model.DamlLfDefDataType): JsValue = damlLfDefDataTypeToJsValue(v)
def read(v: JsValue): Model.DamlLfDefDataType = jsValueToDamlLfDefDataType(v)
}
implicit object DamlLfIdentifierJsonFormat extends RootJsonFormat[Model.DamlLfIdentifier] {
def write(v: Model.DamlLfIdentifier): JsValue = damlLfIdentifierToJsValue(v)
def read(v: JsValue): Model.DamlLfIdentifier = jsValueToDamlLfIdentifier(v)
}
}
}

View File

@ -1,96 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.json
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.model._
import com.daml.navigator.json.Util._
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
import spray.json._
/** An encoding of Model types.
*/
object ModelCodec {
// ------------------------------------------------------------------------------------------------------------------
// Constants used in the encoding
// ------------------------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------------------------
// Encoding
// ------------------------------------------------------------------------------------------------------------------
/*
def eventToJsValue(event: Event): JsValue = {
import JsonImplicits._
val fields: List[JsField] = event match {
case c: ContractCreated => c.toJson.asJsObject.fields.toList :+ propType -> JsString(contractCreatedTag)
case a: ContractArchived => a.toJson.asJsObject.fields.toList :+ propType -> JsString(contractArchivedTag)
case e: ChoiceExercised => e.toJson.asJsObject.fields.toList :+ propType -> JsString(choiceExercisedTag)
}
JsObject(fields: _*)
}
*/
// ------------------------------------------------------------------------------------------------------------------
// Decoding
// ------------------------------------------------------------------------------------------------------------------
/*
def jsValueToEvent(value: JsValue): Event = {
import JsonImplicits._
strField(value, propType, "Event") match {
case `contractCreatedTag` => value.convertTo[ContractCreated]
case `contractArchivedTag` => value.convertTo[ContractArchived]
case `choiceExercisedTag` => value.convertTo[ChoiceExercised]
case subclassType => deserializationError(s"Unknown Event type: $subclassType")
}
}
*/
// ------------------------------------------------------------------------------------------------------------------
// Implicits that can be imported to convert from and to JSON
// ------------------------------------------------------------------------------------------------------------------
object JsonImplicits extends DefaultJsonProtocol {
implicit object ContractIdJsonFormat extends RootJsonFormat[ApiTypes.ContractId] {
def write(value: ApiTypes.ContractId): JsValue = JsString(ApiTypes.ContractId.unwrap(value))
def read(value: JsValue): ApiTypes.ContractId =
ApiTypes.ContractId(asString(value, "ContractId"))
}
implicit object EventIdJsonFormat extends RootJsonFormat[ApiTypes.EventId] {
def write(value: ApiTypes.EventId): JsValue = JsString(ApiTypes.EventId.unwrap(value))
def read(value: JsValue): ApiTypes.EventId = ApiTypes.EventId(asString(value, "EventId"))
}
implicit object TransactionIdJsonFormat extends RootJsonFormat[ApiTypes.TransactionId] {
def write(value: ApiTypes.TransactionId): JsValue =
JsString(ApiTypes.TransactionId.unwrap(value))
def read(value: JsValue): ApiTypes.TransactionId =
ApiTypes.TransactionId(asString(value, "TransactionId"))
}
implicit object WorkflowIdJsonFormat extends RootJsonFormat[ApiTypes.WorkflowId] {
def write(value: ApiTypes.WorkflowId): JsValue = JsString(ApiTypes.WorkflowId.unwrap(value))
def read(value: JsValue): ApiTypes.WorkflowId =
ApiTypes.WorkflowId(asString(value, "WorkflowId"))
}
implicit object ChoiceJsonFormat extends RootJsonFormat[ApiTypes.Choice] {
def write(value: ApiTypes.Choice): JsValue = JsString(ApiTypes.Choice.unwrap(value))
def read(value: JsValue): ApiTypes.Choice = ApiTypes.Choice(asString(value, "Choice"))
}
implicit object PartyJsonFormat extends RootJsonFormat[ApiTypes.Party] {
def write(value: ApiTypes.Party): JsValue = JsString(ApiTypes.Party.unwrap(value))
def read(value: JsValue): ApiTypes.Party = ApiTypes.Party(asString(value, "Party"))
}
implicit val PartyListJsonFormat: RootJsonFormat[List[ApiTypes.Party]] =
listFormat[ApiTypes.Party]
implicit val choiceFormat: RootJsonFormat[Choice] = jsonFormat5(Choice.apply)
implicit val templateFormat: RootJsonFormat[Template] = jsonFormat4(Template.apply)
}
}

View File

@ -1,97 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.json
import com.daml.lf.data.Ref
import spray.json._
/** JSON encoding utils
*/
object Util {
def strField(obj: JsValue, name: String, as: String): String =
asObject(obj, as).fields.get(name) match {
case Some(JsString(v)) =>
v
case Some(_) =>
deserializationError(s"Can't read ${obj.prettyPrint} as $as, field '$name' is not a string")
case None =>
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
}
def nameField(obj: JsValue, name: String, as: String): Ref.Name =
Ref.Name
.fromString(strField(obj, name, as))
.fold(
err => deserializationError(s"Can't read ${obj.prettyPrint} as $as, $err"),
identity,
)
def intField(obj: JsValue, name: String, as: String): Long =
asObject(obj, as).fields.get(name) match {
case Some(JsNumber(v)) => v.toLongExact
case Some(_) =>
deserializationError(
s"Can't read ${obj.prettyPrint} as $as, field '$name' is not an integer"
)
case None =>
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
}
def boolField(obj: JsValue, name: String, as: String): Boolean =
asObject(obj, as).fields.get(name) match {
case Some(JsBoolean(v)) => v
case Some(_) =>
deserializationError(
s"Can't read ${obj.prettyPrint} as $as, field '$name' is not a boolean"
)
case None =>
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
}
def arrayField(obj: JsValue, name: String, as: String): List[JsValue] =
asObject(obj, as).fields.get(name) match {
case Some(JsArray(v)) => v.toList
case Some(_) =>
deserializationError(s"Can't read ${obj.prettyPrint} as $as, field '$name' is not an array")
case None =>
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
}
def objectField(obj: JsValue, name: String, as: String): Map[String, JsValue] =
asObject(obj, as).fields.get(name) match {
case Some(JsObject(v)) => v
case Some(_) =>
deserializationError(s"Can't read ${obj.prettyPrint} as $as, field '$name' is not an array")
case None =>
deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
}
def anyField(obj: JsValue, name: String, as: String): JsValue =
asObject(obj, as).fields.get(name) match {
case Some(v: JsValue) => v
case _ => deserializationError(s"Can't read ${obj.prettyPrint} as $as, missing field '$name'")
}
def asObject(value: JsValue, as: String): JsObject = value match {
case obj: JsObject => obj
case _ =>
deserializationError(s"Can't read ${value.prettyPrint} as $as, value is not an object")
}
def asString(value: JsValue, as: String): String = value match {
case JsString(s) => s
case _ => deserializationError(s"Can't read ${value.prettyPrint} as $as, value is not a string")
}
def asName(value: JsValue, as: String): Ref.Name = value match {
case JsString(v) =>
Ref.Name
.fromString(v)
.fold(
err => deserializationError(s"Can't read ${value.prettyPrint} as $as, $err"),
identity,
)
case _ => deserializationError(s"Can't read ${value.prettyPrint} as $as, value is not a string")
}
}

View File

@ -1,18 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model
sealed trait DatabaseError extends Throwable
final case class NoDatabaseUsed() extends DatabaseError {
override def getMessage: String = "No database is used."
}
final case class DeserializationFailed(msg: String) extends DatabaseError {
override def getMessage: String = s"Deserialization failed while reading from the db: $msg"
}
final case class RecordNotFound(msg: String) extends DatabaseError {
override def getMessage: String = s"Record not found: $msg"
}

View File

@ -1,361 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.data.DatabaseActions
import com.typesafe.scalalogging.LazyLogging
import scala.collection.immutable.LazyList
import scala.util.{Failure, Success, Try}
/** In-memory projection of ledger events. */
case class Ledger(
private val forParty: ApiTypes.Party,
private val lastTransaction: Option[Transaction],
private val useDatabase: Boolean,
private val transactionById: Map[ApiTypes.TransactionId, Transaction] = Map.empty,
private val eventById: Map[ApiTypes.EventId, Event] = Map.empty,
private val contractById: Map[ApiTypes.ContractId, Contract] = Map.empty,
private val activeContractById: Map[ApiTypes.ContractId, Contract] = Map.empty,
private val createEventByContractId: Map[ApiTypes.ContractId, ContractCreated] = Map.empty,
private val archiveEventByContractId: Map[ApiTypes.ContractId, ChoiceExercised] = Map.empty,
private val choiceExercisedEventByContractById: Map[ApiTypes.ContractId, List[
ChoiceExercised
]] = Map.empty,
private val contractsByTemplateId: Map[DamlLfIdentifier, Set[Contract]] = Map.empty,
private val commandById: Map[ApiTypes.CommandId, Command] = Map.empty,
private val statusByCommandId: Map[ApiTypes.CommandId, CommandStatus] = Map.empty,
private val db: DatabaseActions = new DatabaseActions,
) extends LazyLogging {
private def logErrorAndDefaultTo[A](result: Try[A], default: A): A = {
result match {
case Success(a) => a
case Failure(e) =>
logger.error(e.getMessage)
default
}
}
def latestTransaction(types: PackageRegistry): Option[Transaction] = {
if (useDatabase) {
logErrorAndDefaultTo(db.lastTransaction(types), None)
} else {
lastTransaction
}
}
def withCommandStatus(commandId: ApiTypes.CommandId, result: CommandStatus): Ledger = {
if (useDatabase) {
db.upsertCommandStatus(commandId, result)
this
} else {
copy(
statusByCommandId = statusByCommandId + (commandId -> result)
)
}
}
def withCommand(command: Command): Ledger = {
if (useDatabase) {
db.insertCommand(command)
db.upsertCommandStatus(command.id, CommandStatusWaiting())
this
} else {
copy(
commandById = commandById + (command.id -> command),
statusByCommandId = statusByCommandId + (command.id -> CommandStatusWaiting()),
)
}
}
def withTransaction(tx: Transaction, packageRegistry: PackageRegistry): Ledger = {
val ledger0 = this
.withLatestTransaction(tx)
.withTransactionResult(tx)
tx.events.foldLeft(ledger0) { (ledger, event) =>
ledger.withEvent(event, packageRegistry)
}
}
private def withLatestTransaction(tx: Transaction): Ledger = {
if (useDatabase) {
db.insertTransaction(tx)
this
} else {
copy(
lastTransaction = Some(tx),
transactionById = transactionById + (tx.id -> tx),
)
}
}
private def withTransactionResult(tx: Transaction): Ledger = {
// Note: only store the status for commands submitted by this client
if (useDatabase) {
tx.commandId.foreach(db.updateCommandStatus(_, CommandStatusSuccess(tx)))
this
} else {
tx.commandId
.filter(commandById.contains)
.fold(this) { commandId =>
copy(statusByCommandId = statusByCommandId + (commandId -> CommandStatusSuccess(tx)))
}
}
}
private def withEvent(event: Event, packageRegistry: PackageRegistry): Ledger =
event match {
case event: ContractCreated =>
packageRegistry.template(event.templateId).fold(this) { template =>
val contract = Contract(
event.contractId,
template,
event.argument,
event.agreementText,
event.signatories,
event.observers,
event.key,
)
withContractCreatedInEvent(contract, event)
}
case event: ChoiceExercised =>
withChoiceExercisedInEvent(event.contractId, event)
}
private def withContractCreatedInEvent(contract: Contract, event: ContractCreated): Ledger = {
val isStakeHolder =
contract.signatories.contains(forParty) || contract.observers.contains(forParty)
if (useDatabase) {
if (isStakeHolder) db.insertContract(contract)
db.insertEvent(event)
this
} else {
val contractUpdated =
if (isStakeHolder)
copy(
contractById = contractById + (contract.id -> contract),
activeContractById = activeContractById + (contract.id -> contract),
contractsByTemplateId = contractsByTemplateIdWith(contract),
createEventByContractId = createEventByContractId + (contract.id -> event),
)
else
this
contractUpdated.copy(eventById = eventById + (event.id -> event))
}
}
private def contractsByTemplateIdWith(contract: Contract) = {
val templateId = contract.template.id
val entryWithContract =
templateId -> (contractsByTemplateId.getOrElse(templateId, Set.empty) + contract)
contractsByTemplateId + entryWithContract
}
private def withChoiceExercisedInEvent(
contractId: ApiTypes.ContractId,
event: ChoiceExercised,
): Ledger = {
if (useDatabase) {
if (event.consuming) {
db.archiveContract(contractId, event.transactionId)
}
db.insertEvent(event)
this
} else {
val prevExercises: List[ChoiceExercised] =
choiceExercisedEventByContractById.getOrElse(contractId, List.empty)
copy(
archiveEventByContractId =
if (event.consuming) archiveEventByContractId + (contractId -> event)
else archiveEventByContractId,
activeContractById =
if (event.consuming) activeContractById - contractId else activeContractById,
choiceExercisedEventByContractById =
choiceExercisedEventByContractById + (contractId -> (event :: prevExercises)),
eventById = eventById + (event.id -> event),
)
}
}
def allContractsCount: Int = {
if (useDatabase) {
logErrorAndDefaultTo(db.contractCount(), 0)
} else {
contractById.size
}
}
def activeContractsCount: Int = {
if (useDatabase) {
logErrorAndDefaultTo(db.activeContractCount(), 0)
} else {
activeContractById.size
}
}
def contract(id: ApiTypes.ContractId, types: PackageRegistry): Option[Contract] = {
if (useDatabase) {
logErrorAndDefaultTo(db.contract(id, types), None)
} else {
contractById.get(id)
}
}
def event(id: ApiTypes.EventId, types: PackageRegistry): Option[Event] = {
if (useDatabase) {
logErrorAndDefaultTo(db.eventById(id, types), None)
} else {
eventById.get(id)
}
}
def childEvents(id: ApiTypes.EventId, types: PackageRegistry): List[Event] = {
if (useDatabase) {
logErrorAndDefaultTo(db.eventsByParentId(id, types), List.empty)
} else {
(for {
ev <- eventById.get(id)
tx <- transactionById.get(ev.transactionId)
} yield {
tx.events.filter(_.parentId.contains(id))
}).getOrElse(List.empty)
}
}
def transaction(id: ApiTypes.TransactionId, types: PackageRegistry): Option[Transaction] = {
if (useDatabase) {
logErrorAndDefaultTo(db.transactionById(id, types), None)
} else {
transactionById.get(id)
}
}
def command(id: ApiTypes.CommandId, types: PackageRegistry): Option[Command] = {
if (useDatabase) {
logErrorAndDefaultTo(db.commandById(id, types), None)
} else {
commandById.get(id)
}
}
def commandStatus(id: ApiTypes.CommandId, types: PackageRegistry): Option[CommandStatus] = {
if (useDatabase) {
logErrorAndDefaultTo(db.commandStatusByCommandId(id, types), None)
} else {
statusByCommandId.get(id)
}
}
def createEventOf(contract: Contract, types: PackageRegistry): Try[ContractCreated] = {
if (useDatabase) {
db.createEventByContractId(contract.id, types)
} else {
Success(createEventByContractId(contract.id))
}
}
def archiveEventOf(contract: Contract, types: PackageRegistry): Option[ChoiceExercised] = {
if (useDatabase) {
logErrorAndDefaultTo(db.archiveEventByContractId(contract.id, types), None)
} else {
archiveEventByContractId.get(contract.id)
}
}
def exercisedEventsOf(contract: Contract, types: PackageRegistry): List[ChoiceExercised] = {
if (useDatabase) {
logErrorAndDefaultTo(db.choiceExercisedEventByContractById(contract.id, types), List.empty)
} else {
choiceExercisedEventByContractById.getOrElse(contract.id, List.empty)
}
}
def allContracts(types: PackageRegistry): LazyList[Contract] = {
if (useDatabase) {
logErrorAndDefaultTo(db.contracts(types), List.empty[Contract]).to(LazyList)
} else {
contractById.values.to(LazyList)
}
}
def activeContracts(types: PackageRegistry): LazyList[Contract] = {
if (useDatabase) {
logErrorAndDefaultTo(db.activeContracts(types), List.empty[Contract]).to(LazyList)
} else {
activeContractById.values.to(LazyList)
}
}
def activeTemplateContractsOf(template: Template, types: PackageRegistry): LazyList[Contract] = {
if (useDatabase) {
logErrorAndDefaultTo(
db.activeContractsForTemplate(template.id, types),
List.empty[Contract],
).to(LazyList)
} else {
templateContractsOf(template, types).filter(contract =>
activeContractById.contains(contract.id)
)
}
}
def templateContractsOf(template: Template, types: PackageRegistry): LazyList[Contract] = {
if (useDatabase) {
logErrorAndDefaultTo(
db.contractsForTemplate(template.id, types),
List.empty[Contract],
).to(LazyList)
} else {
contractsByTemplateId.getOrElse(template.id, Set.empty).to(LazyList)
}
}
def allCommands(types: PackageRegistry): LazyList[Command] = {
if (useDatabase) {
logErrorAndDefaultTo(db.allCommands(types), List.empty[Command]).to(LazyList)
} else {
commandById.values.to(LazyList)
}
}
def statusOf(commandId: ApiTypes.CommandId, types: PackageRegistry): Option[CommandStatus] = {
if (useDatabase) {
logErrorAndDefaultTo(db.commandStatusByCommandId(commandId, types), None)
} else {
statusByCommandId.get(commandId)
}
}
def databaseSchema(): Try[String] =
if (useDatabase) {
db.schema()
} else {
Failure(NoDatabaseUsed())
}
def runQuery(query: String): Try[SqlQueryResult] =
if (useDatabase) {
db.runQuery(query)
} else {
Failure(NoDatabaseUsed())
}
// Deprecated
def resultOf(commandId: ApiTypes.CommandId, types: PackageRegistry): Option[Result] =
for {
status <- statusOf(commandId, types)
result <- status match {
case _: CommandStatusWaiting => None
case cmd: CommandStatusSuccess => Some(Result(commandId, Right(cmd.tx)))
case cmd: CommandStatusError =>
Some(Result(commandId, Left(new Error(cmd.code, cmd.details, ""))))
case _: CommandStatusUnknown =>
Some(Result(commandId, Left(new Error("INTERNAL", "Command status unknown", ""))))
}
} yield result
}

View File

@ -1,218 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model
import java.time.Instant
import com.daml.lf.data.{Ref => DamlLfRef}
import com.daml.ledger.api.refinements.ApiTypes
import scalaz.{@@, Tag}
// ------------------------------------------------------------------------------------------------
// Node
// ------------------------------------------------------------------------------------------------
/** Identifiable entity. */
sealed trait Node[T] {
def id: T
def idString: String
}
sealed trait TaggedNode[IdTag] extends Node[String @@ IdTag] {
def id: String @@ IdTag
override def idString: String = Tag.unwrap(id)
}
sealed trait DamlLfNode extends Node[DamlLfIdentifier] {
def id: DamlLfIdentifier
override def idString: String = id.asOpaqueString
}
// ------------------------------------------------------------------------------------------------
// Commands
// ------------------------------------------------------------------------------------------------
// Status of a command - waiting, failed, succeeded, or unknown
sealed trait CommandStatus {
def isCompleted: Boolean
}
// Waiting for command completion
final case class CommandStatusWaiting() extends CommandStatus {
def isCompleted = false
}
// Completed with an error
final case class CommandStatusError(code: String, details: String) extends CommandStatus {
def isCompleted = true
}
// Completed successfully
final case class CommandStatusSuccess(tx: Transaction) extends CommandStatus {
def isCompleted = true
}
// Command status tracking failed, status unknown
final case class CommandStatusUnknown() extends CommandStatus {
def isCompleted = false
}
sealed trait Command extends TaggedNode[ApiTypes.CommandIdTag] {
/** Order in which the command was submitted. */
def index: Long
def workflowId: ApiTypes.WorkflowId
def platformTime: Instant
}
final case class CreateCommand(
id: ApiTypes.CommandId,
index: Long,
workflowId: ApiTypes.WorkflowId,
platformTime: Instant,
template: DamlLfIdentifier,
argument: ApiRecord,
) extends Command
/** @param template
* The template of the given contract. Not required for the ledger API, but we keep
* this denormalized information so that it's easier to serialize/deserialize the
* choice argument.
*/
final case class ExerciseCommand(
id: ApiTypes.CommandId,
index: Long,
workflowId: ApiTypes.WorkflowId,
platformTime: Instant,
contract: ApiTypes.ContractId,
template: DamlLfIdentifier,
interfaceId: Option[DamlLfIdentifier],
choice: ApiTypes.Choice,
argument: ApiValue,
) extends Command
case class Error(code: String, details: String, parameters: String)
final case class Result(id: ApiTypes.CommandId, errorOrTx: Either[Error, Transaction])
extends TaggedNode[ApiTypes.CommandIdTag]
// ------------------------------------------------------------------------------------------------
// Daml Package
// ------------------------------------------------------------------------------------------------
case class DamlLfPackage(
id: DamlLfRef.PackageId,
typeDefs: Map[DamlLfIdentifier, DamlLfDefDataType],
templates: Map[DamlLfIdentifier, Template],
interfaces: Map[DamlLfIdentifier, Interface],
)
/** A boxed DefDataType that also includes the ID of the type.
* This is useful for the GraphQL schema.
*/
final case class DamlLfDefDataTypeBoxed(id: DamlLfIdentifier, value: DamlLfDefDataType)
extends DamlLfNode
// ------------------------------------------------------------------------------------------------
// Transactions
// ------------------------------------------------------------------------------------------------
/** Transaction. */
final case class Transaction(
id: ApiTypes.TransactionId,
commandId: Option[ApiTypes.CommandId],
effectiveAt: Instant,
offset: String,
events: List[Event],
) extends TaggedNode[ApiTypes.TransactionIdTag]
// ------------------------------------------------------------------------------------------------
// Events
// ------------------------------------------------------------------------------------------------
sealed trait Event extends TaggedNode[ApiTypes.EventIdTag] {
def workflowId: ApiTypes.WorkflowId
/** Id of the parent event in the transaction tree. */
def parentId: Option[ApiTypes.EventId]
/** Id of the transaction tree containing this event. */
def transactionId: ApiTypes.TransactionId
/** Determines which parties are notified of this event. */
def witnessParties: List[ApiTypes.Party]
}
final case class ContractCreated(
id: ApiTypes.EventId,
parentId: Option[ApiTypes.EventId],
transactionId: ApiTypes.TransactionId,
witnessParties: List[ApiTypes.Party],
workflowId: ApiTypes.WorkflowId,
contractId: ApiTypes.ContractId,
templateId: DamlLfIdentifier,
argument: ApiRecord,
agreementText: Option[String],
signatories: List[ApiTypes.Party],
observers: List[ApiTypes.Party],
key: Option[ApiValue],
) extends Event
final case class ChoiceExercised(
id: ApiTypes.EventId,
parentId: Option[ApiTypes.EventId],
transactionId: ApiTypes.TransactionId,
witnessParties: List[ApiTypes.Party],
workflowId: ApiTypes.WorkflowId,
contractId: ApiTypes.ContractId,
templateId: DamlLfIdentifier,
choice: ApiTypes.Choice,
argument: ApiValue,
actingParties: List[ApiTypes.Party],
consuming: Boolean,
) extends Event
// ------------------------------------------------------------------------------------------------
// Contract
// ------------------------------------------------------------------------------------------------
/** Active contract. */
final case class Contract(
id: ApiTypes.ContractId,
template: Template,
argument: ApiRecord,
agreementText: Option[String],
signatories: List[ApiTypes.Party],
observers: List[ApiTypes.Party],
key: Option[ApiValue],
) extends TaggedNode[ApiTypes.ContractIdTag]
// ------------------------------------------------------------------------------------------------
// Template
// ------------------------------------------------------------------------------------------------
/** Template for instantiating contracts. */
final case class Template(
id: DamlLfIdentifier,
choices: List[Choice],
key: Option[DamlLfType],
implementedInterfaces: Set[DamlLfIdentifier],
) extends DamlLfNode {
def topLevelDecl: String = id.qualifiedName.toString()
def parameter: DamlLfTypeCon = DamlLfTypeCon(DamlLfTypeConName(id), DamlLfImmArraySeq())
}
/** Interfaces. */
final case class Interface(
id: DamlLfIdentifier,
choices: List[Choice],
) extends DamlLfNode
/** Template choice. */
case class Choice(
name: ApiTypes.Choice,
parameter: DamlLfType,
returnType: DamlLfType,
consuming: Boolean,
inheritedInterface: Option[DamlLfIdentifier] = None,
)

View File

@ -1,184 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model
import com.daml.navigator.{model => Model}
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.lf.{typesig => DamlLfIface}
import com.daml.lf.data.{Ref => DamlLfRef}
/** Manages a set of known Daml-LF packages. */
case class PackageRegistry(
private val packageState: PackageState = PackageState(Map.empty),
// These are just projections from `packageState` for performance. `packageState` is the source of truth
private val packages: Map[DamlLfRef.PackageId, DamlLfPackage] = Map.empty,
private val templates: Map[DamlLfIdentifier, Template] = Map.empty,
private val typeDefs: Map[DamlLfIdentifier, DamlLfDefDataType] = Map.empty,
private val interfaces: Map[DamlLfIdentifier, Interface] = Map.empty,
) {
// TODO (#13969) ignores inherited choices; interfaces aren't handled at all
private[this] def template(
packageId: DamlLfRef.PackageId,
qname: DamlLfQualifiedName,
t: DamlLfIface.DefTemplate[DamlLfIface.Type],
): Template = Template(
DamlLfIdentifier(packageId, qname),
t.tChoices.resolvedChoices.toList.flatMap { case (choiceName, resolvedChoices) =>
resolvedChoices.map { case (interfaceIdOption, templateChoice) =>
choice(choiceName, templateChoice, interfaceIdOption)
}
},
t.key,
t.implementedInterfaces.toSet,
)
private[this] def interface(
packageId: DamlLfRef.PackageId,
qname: DamlLfQualifiedName,
interface: DamlLfIface.DefInterface[DamlLfIface.Type],
): Interface = {
Interface(
DamlLfIdentifier(packageId, qname),
interface.choices.toList.map(c => choice(c._1, c._2)),
)
}
private[this] def choice(
name: String,
c: DamlLfIface.TemplateChoice[DamlLfIface.Type],
inheritedInterface: Option[DamlLfIdentifier] = None,
): Model.Choice = Model.Choice(
ApiTypes.Choice(name),
c.param,
c.returnType,
c.consuming,
inheritedInterface,
)
def withPackages(interfaces: List[DamlLfIface.PackageSignature]): PackageRegistry = {
val newPackageStore = packageState.append(interfaces.map(p => p.packageId -> p).toMap)
val newPackages = newPackageStore.packages.values.map { p =>
val typeDefs = p.typeDecls.map { case (qname, td) =>
DamlLfIdentifier(p.packageId, qname) -> td.`type`
}
val templates = p.typeDecls.collect {
case (qname, DamlLfIface.PackageSignature.TypeDecl.Template(r @ _, t)) =>
DamlLfIdentifier(p.packageId, qname) -> template(p.packageId, qname, t)
}
val interfaces = p.interfaces.map { case (qname, defInterface) =>
DamlLfIdentifier(p.packageId, qname) -> interface(p.packageId, qname, defInterface)
}
p.packageId -> DamlLfPackage(p.packageId, typeDefs, templates, interfaces)
}.toMap
copy(
packages = newPackages,
templates = newPackages.flatMap(_._2.templates),
typeDefs = newPackages.flatMap(_._2.typeDefs),
interfaces = newPackages.flatMap(_._2.interfaces),
)
}
// ------------------------------------------------------------------------------------------------------------------
// Packages
// ------------------------------------------------------------------------------------------------------------------
def packageCount: Int =
packages.size
def pack(id: DamlLfRef.PackageId): Option[DamlLfPackage] =
packages.get(id)
def allPackages(): LazyList[DamlLfPackage] =
packages.values.to(LazyList)
// ------------------------------------------------------------------------------------------------------------------
// Templates
// ------------------------------------------------------------------------------------------------------------------
def template(id: DamlLfIdentifier): Option[Template] =
templates.get(id)
def templateByIdentifier(id: ApiTypes.TemplateId): Option[Template] = {
val damlId = ApiTypes.TemplateId.unwrap(id).asDaml
templates.get(damlId)
}
def templateByStringId(id: TemplateStringId): Option[Template] = {
parseOpaqueIdentifier(TemplateStringId.unwrap(id)).flatMap(lfid => templates.get(lfid))
}
def templateCount: Int =
templates.size
def allTemplates(): LazyList[Template] =
templates.values.to(LazyList)
def templatesByName(topLevelDecl: String): Seq[Template] =
templates.toList
.filter(t => t._2.topLevelDecl == topLevelDecl)
.map(_._2)
// ------------------------------------------------------------------------------------------------------------------
// Types
// ------------------------------------------------------------------------------------------------------------------
def damlLfDefDataType(id: DamlLfIdentifier): Option[DamlLfDefDataType] =
typeDefs.get(id)
/** Returns a list of all user defined types required to evaluate the given user defined type.
* maxDepth defines the maximum depth of instantiate() calls (i.e., recursive type lookups)
*/
def typeDependencies(
typ: DamlLfDefDataType,
maxDepth: Int = Int.MaxValue,
): Map[DamlLfIdentifier, DamlLfDefDataType] = {
def foldType(
typ: DamlLfType,
deps: Map[DamlLfIdentifier, DamlLfDefDataType],
instantiatesRemaining: Int,
): Map[DamlLfIdentifier, DamlLfDefDataType] = {
typ match {
case DamlLfTypeVar(_) | DamlLfTypeNumeric(_) => deps
case DamlLfTypePrim(_, vars) =>
vars.foldLeft(deps)((r, v) => foldType(v, r, instantiatesRemaining))
case DamlLfTypeCon(name, vars) =>
deps.get(name.identifier) match {
// Dependency already added
case Some(_) => deps
// New dependency
case None =>
if (instantiatesRemaining > 0) {
damlLfDefDataType(name.identifier).fold(deps)(ddt => {
val r1 = deps + (name.identifier -> ddt)
val r2 = foldDataType(ddt, r1, instantiatesRemaining - 1)
vars.foldLeft(r2)((r, v) => foldType(v, r, instantiatesRemaining - 1))
})
} else {
deps
}
}
}
}
def foldDataType(
ddt: DamlLfDefDataType,
deps: Map[DamlLfIdentifier, DamlLfDefDataType],
instantiatesRemaining: Int,
): Map[DamlLfIdentifier, DamlLfDefDataType] = {
ddt.dataType match {
case DamlLfRecord(fields) =>
fields.foldLeft(deps)((r, field) => foldType(field._2, r, instantiatesRemaining))
case DamlLfVariant(fields) =>
fields.foldLeft(deps)((r, field) => foldType(field._2, r, instantiatesRemaining))
case DamlLfEnum(_) =>
deps
}
}
foldDataType(typ, Map.empty, maxDepth)
}
}

View File

@ -1,40 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model
import com.daml.lf.data.Ref
import com.daml.lf.typesig
import scala.collection.immutable.Map
final case class PackageState(packages: PackageState.PackageStore) {
import PackageState.PackageStore
def append(diff: PackageStore): PackageState = {
val newPackages = resolveChoicesIn(
appendAndResolveRetroactiveInterfaces(diff)
)
copy(packages = newPackages)
}
private[this] def appendAndResolveRetroactiveInterfaces(diff: PackageStore): PackageStore = {
def lookupIf(packageStore: PackageStore, pkId: Ref.PackageId) =
packageStore
.get(pkId)
.map((_, { newSig: typesig.PackageSignature => packageStore.updated(pkId, newSig) }))
val (packageStore2, diffElems) =
typesig.PackageSignature.resolveRetroImplements(packages, diff.values.toSeq)(lookupIf)
packageStore2 ++ diffElems.view.map(p => (p.packageId, p))
}
private[this] def resolveChoicesIn(diff: PackageStore): PackageStore = {
def lookupIf(pkgId: Ref.PackageId) = (packages get pkgId) orElse (diff get pkgId)
val findIface = typesig.PackageSignature.findInterface(Function unlift lookupIf)
diff.transform((_, iface) => iface resolveChoicesAndFailOnUnresolvableChoices findIface)
}
}
object PackageState {
type PackageStore = Map[String, typesig.PackageSignature]
}

View File

@ -1,68 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model
import java.util.concurrent.atomic.AtomicReference
import com.daml.lf.{typesig => DamlLfIface}
import com.daml.ledger.api.refinements.ApiTypes
import scala.collection.immutable.LazyList
import scalaz.Tag
import java.net.URLEncoder
case class State(ledger: Ledger, packageRegistry: PackageRegistry)
/** A DA party and its ledger view(s). */
class PartyState(
val name: ApiTypes.Party,
// A named role specified in our config. The role is an arbitrary string chosen by the user. It gets passed to the frontend, which can display a custom theme or custom views depending on the role.
val userRole: Option[String] = None,
val useDatabase: Boolean = false,
) {
def actorName: String =
"party-" + URLEncoder.encode(ApiTypes.Party.unwrap(name), "UTF-8")
private val stateRef: AtomicReference[State] = new AtomicReference(
State(Ledger(name, None, useDatabase), new PackageRegistry)
)
def ledger: Ledger = stateRef.get.ledger
def packageRegistry: PackageRegistry = stateRef.get.packageRegistry
def addLatestTransaction(tx: Transaction): Unit = {
stateRef.updateAndGet(state =>
state.copy(ledger = state.ledger.withTransaction(tx, packageRegistry))
)
()
}
def addCommand(cmd: Command): Unit = {
stateRef.updateAndGet(state => state.copy(ledger = state.ledger.withCommand(cmd)))
()
}
def addCommandStatus(id: ApiTypes.CommandId, status: CommandStatus): Unit = {
stateRef.updateAndGet(state => state.copy(ledger = state.ledger.withCommandStatus(id, status)))
()
}
def addPackages(packs: List[DamlLfIface.PackageSignature]): Unit = {
stateRef.updateAndGet(state =>
state.copy(packageRegistry = packageRegistry.withPackages(packs))
)
()
}
override def hashCode(): Int = Tag.unwrap(name).hashCode()
override def equals(obj: scala.Any): Boolean = obj match {
case that: PartyState => Tag.unwrap(this.name) equals Tag.unwrap(that.name)
case _ => false
}
override def toString: String = "Party(" + name.toString + ")"
def contracts(): LazyList[Contract] = this.ledger.allContracts(this.packageRegistry)
}

View File

@ -1,6 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model
final case class SqlQueryResult(columnNames: List[String], rows: List[List[String]])

View File

@ -1,23 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model.converter
import com.daml.navigator.model.DamlLfIdentifier
sealed trait ConversionError extends Throwable
/** Conversion failed because a type is missing. */
final case class TypeNotFoundError(id: DamlLfIdentifier) extends ConversionError {
override def getMessage: String = s"Type $id not found"
}
/** A required field is missing. */
final case class RequiredFieldDoesNotExistError(name: String) extends ConversionError {
override def getMessage: String = s"Required field $name is missing"
}
/** Generic conversion error, with a human readable error string. */
final case class GenericConversionError(error: String) extends ConversionError {
override def getMessage: String = error
}

View File

@ -1,32 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model.converter
case object Converter {
/** Returns the sequence of values, or the last error encountered */
def sequence[E, T](xs: Seq[Either[E, T]]): Either[E, List[T]] =
xs.foldRight(Right(Nil): Either[E, List[T]]) { (e, acc) =>
for (xs <- acc; x <- e) yield x :: xs
}
/** Returns the value of a required protobuf3 field, or RequiredFieldDoesNotExistError if it doesn't exist. */
def checkExists[T](
fieldName: String,
maybeElement: Option[T],
): Either[ConversionError, T] =
maybeElement match {
case Some(element) => Right(element)
case None => Left(RequiredFieldDoesNotExistError(fieldName))
}
def checkExists[T](
maybeElement: Option[T],
error: ConversionError,
): Either[ConversionError, T] =
maybeElement match {
case Some(element) => Right(element)
case None => Left(error)
}
}

View File

@ -1,529 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model.converter
import java.time.Instant
import com.daml.lf.data.Ref
import com.daml.lf.typesig
import com.daml.lf.value.{Value => V}
import com.daml.ledger.api.{v1 => V1}
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.ledger.api.validation.NoLoggingValueValidator.{validateRecord, validateValue}
import com.daml.navigator.{model => Model}
import com.daml.navigator.model.{
DamlLfIdentifier,
IdentifierApiConversions,
IdentifierDamlConversions,
}
import com.daml.platform.participant.util.LfEngineToApi.{lfValueToApiRecord, lfValueToApiValue}
import scalaz.Tag
import scalaz.syntax.bifunctor._
import scalaz.syntax.traverse._
import scalaz.syntax.std.option._
import scalaz.std.either._
import scalaz.std.option._
case object LedgerApiV1 {
// ------------------------------------------------------------------------------------------------------------------
// Types
// ------------------------------------------------------------------------------------------------------------------
case class Context(party: ApiTypes.Party, templates: Model.PackageRegistry)
private type Result[X] = Either[ConversionError, X]
// ------------------------------------------------------------------------------------------------------------------
// Read methods (V1 -> Model)
// ------------------------------------------------------------------------------------------------------------------
/*
def readTransaction(tx: V1.transaction.Transaction, ctx: Context): Result[Model.Transaction] = {
for {
events <- Converter.sequence(tx.events.map(ev =>
readEvent(ev, ApiTypes.TransactionId(tx.transactionId), ctx, List.empty, ApiTypes.WorkflowId(tx.workflowId), None)))
effectiveAt <- Converter.checkExists("Transaction.effectiveAt", tx.effectiveAt)
offset <- readLedgerOffset(tx.offset)
} yield {
Model.Transaction(
id = ApiTypes.TransactionId(tx.transactionId),
commandId = if (tx.commandId.isEmpty) None else Some(ApiTypes.CommandId(tx.commandId)),
effectiveAt = Instant.ofEpochSecond(effectiveAt.seconds, effectiveAt.nanos),
offset = offset,
events = events
)
}
}
private def readEvent(
event: V1.event.Event,
transactionId: ApiTypes.TransactionId,
ctx: Context,
parentWitnessParties: List[ApiTypes.Party],
workflowId: ApiTypes.WorkflowId,
parentId: Option[ApiTypes.EventId] = None
): Result[Model.Event] = {
event match {
case V1.event.Event(V1.event.Event.Event.Created(ev)) =>
readEventCreated(ev, transactionId, parentWitnessParties, workflowId, parentId, ctx)
case V1.event.Event(V1.event.Event.Event.Exercised(ev)) =>
// This case should be removed from the protobuf, Transactions never contain Exercised events
Left(GenericConversionError("Exercised event found in GetTransactions"))
case V1.event.Event(V1.event.Event.Event.Archived(ev)) =>
readEventArchived(ev, transactionId, parentWitnessParties, workflowId, parentId, ctx)
case V1.event.Event(V1.event.Event.Event.Empty) =>
Left(RequiredFieldDoesNotExistError("Event.value"))
}
}
private def readEventArchived(
event: V1.event.ArchivedEvent,
transactionId: ApiTypes.TransactionId,
parentWitnessParties: List[ApiTypes.Party],
workflowId: ApiTypes.WorkflowId,
parentId: Option[ApiTypes.EventId],
ctx: Context
): Result[Model.Event] = {
val witnessParties = parentWitnessParties ++ ApiTypes.Party.subst(event.witnessParties)
Right(
Model.ContractArchived(
id = ApiTypes.EventId(event.eventId),
parentId = parentId,
transactionId = transactionId,
witnessParties = witnessParties,
workflowId = workflowId,
contractId = ApiTypes.ContractId(event.contractId)
)
)
}
*/
def readTransactionTree(
tx: V1.transaction.TransactionTree,
ctx: Context,
): Result[Model.Transaction] = {
for {
events <- Converter
.sequence(
tx.rootEventIds
.map(evid =>
readTreeEvent(
tx.eventsById(evid),
ApiTypes.TransactionId(tx.transactionId),
tx.eventsById,
ctx,
ApiTypes.WorkflowId(tx.workflowId),
None,
)
)
)
.map(_.flatten)
effectiveAt <- Converter.checkExists("Transaction.effectiveAt", tx.effectiveAt)
offset <- readLedgerOffset(tx.offset)
} yield {
Model.Transaction(
id = ApiTypes.TransactionId(tx.transactionId),
commandId = if (tx.commandId.isEmpty) None else Some(ApiTypes.CommandId(tx.commandId)),
effectiveAt = Instant.ofEpochSecond(effectiveAt.seconds, effectiveAt.nanos.toLong),
offset = offset,
events = events,
)
}
}
private def readTreeEvent(
event: V1.transaction.TreeEvent,
transactionId: ApiTypes.TransactionId,
eventsById: Map[String, V1.transaction.TreeEvent],
ctx: Context,
workflowId: ApiTypes.WorkflowId,
parentId: Option[ApiTypes.EventId],
): Result[List[Model.Event]] = {
event match {
case V1.transaction.TreeEvent(V1.transaction.TreeEvent.Kind.Created(ev)) =>
readEventCreated(ev, transactionId, workflowId, parentId, ctx).map(List(_))
case V1.transaction.TreeEvent(V1.transaction.TreeEvent.Kind.Exercised(ev)) =>
readEventExercised(ev, transactionId, eventsById, workflowId, parentId, ctx)
case V1.transaction.TreeEvent(V1.transaction.TreeEvent.Kind.Empty) =>
Left(RequiredFieldDoesNotExistError("TreeEvent.value"))
}
}
private def getTemplate(
id: Model.DamlLfIdentifier,
ctx: Context,
): Result[Model.Template] =
ctx.templates
.template(id)
.map(Right(_))
.getOrElse(Left(TypeNotFoundError(id)))
private def readLedgerOffset(offset: String): Result[String] = {
// Ledger offset may change to become a number in the future
// Try(BigInt(offset)).toEither
// .left.map(t => GenericConversionError(s"Could not parse ledger offset '$offset'"))
Right(offset)
}
private def readEventCreated(
event: V1.event.CreatedEvent,
transactionId: ApiTypes.TransactionId,
workflowId: ApiTypes.WorkflowId,
parentId: Option[ApiTypes.EventId],
ctx: Context,
): Result[Model.Event] = {
val witnessParties = ApiTypes.Party.subst(event.witnessParties.toList)
val signatories = ApiTypes.Party.subst(event.signatories.toList)
val observers = ApiTypes.Party.subst(event.observers.toList)
for {
templateId <- Converter.checkExists("CreatedEvent.templateId", event.templateId)
templateIdentifier = templateId.asDaml
template <- getTemplate(templateIdentifier, ctx)
arguments <- Converter.checkExists("CreatedEvent.arguments", event.createArguments)
arg <- readRecordArgument(arguments, templateIdentifier, ctx)
keyResult = event.contractKey
.traverse(k => readArgument(k, template.key.get, ctx))
key <- keyResult
} yield Model.ContractCreated(
id = ApiTypes.EventId(event.eventId),
parentId = parentId,
transactionId = transactionId,
witnessParties = witnessParties,
workflowId = workflowId,
contractId = ApiTypes.ContractId(event.contractId),
templateId = templateIdentifier,
argument = arg,
agreementText = event.agreementText,
signatories = signatories,
observers = observers,
key = key,
)
}
private def readEventExercised(
event: V1.event.ExercisedEvent,
transactionId: ApiTypes.TransactionId,
eventsById: Map[String, V1.transaction.TreeEvent],
workflowId: ApiTypes.WorkflowId,
parentId: Option[ApiTypes.EventId],
ctx: Context,
): Result[List[Model.Event]] = {
val witnessParties = ApiTypes.Party.subst(event.witnessParties.toList)
for {
templateId <- Converter.checkExists("ExercisedEvent.templateId", event.templateId)
templateIdentifier = templateId.asDaml
template <- getTemplate(templateId.asDaml, ctx)
argument <- Converter.checkExists("ExercisedEvent.arguments", event.choiceArgument)
choice <- Converter.checkExists(
template.choices.find(c => ApiTypes.Choice.unwrap(c.name) == event.choice),
GenericConversionError(s"Choice '${event.choice}' not found"),
)
modelArgument <- readArgument(argument, choice.parameter, ctx)
children <- Converter
.sequence(
event.childEventIds
.map(childId =>
readTreeEvent(
eventsById(childId),
transactionId,
eventsById,
ctx,
workflowId,
Some(ApiTypes.EventId(event.eventId)),
)
)
)
.map(_.flatten)
} yield Model.ChoiceExercised(
id = ApiTypes.EventId(event.eventId),
parentId = parentId,
transactionId = transactionId,
witnessParties = witnessParties,
workflowId = workflowId,
contractId = ApiTypes.ContractId(event.contractId),
templateId = templateIdentifier,
choice = ApiTypes.Choice(event.choice),
argument = modelArgument,
consuming = event.consuming,
actingParties = event.actingParties.map(ApiTypes.Party(_)).toList,
) :: children
}
private def readRecordArgument(
value: V1.value.Record,
typId: Model.DamlLfIdentifier,
ctx: Context,
): Result[Model.ApiRecord] =
for {
lfr <- validateRecord(value).leftMap(sre => GenericConversionError(sre.getMessage))
cidMapped <- lfr match {
case r: Model.ApiRecord => Right(r)
case v => Left(GenericConversionError(s"validating record produced non-record $v"))
}
filled <- fillInRecordTI(
cidMapped,
Model.DamlLfTypeCon(Model.DamlLfTypeConName(typId), Model.DamlLfImmArraySeq()),
ctx,
)
} yield filled
private def fillInRecordTI(
value: Model.ApiRecord,
typ: Model.DamlLfType,
ctx: Context,
): Result[Model.ApiRecord] =
for {
typeCon <- asTypeCon(typ, value)
ddt <- ctx.templates
.damlLfDefDataType(typeCon.name.identifier)
.toRight(GenericConversionError(s"Unknown type ${typeCon.name.identifier}"))
dt <- typeCon.instantiate(ddt) match {
case r @ typesig.Record(_) => Right(r)
case typesig.Variant(_) | typesig.Enum(_) =>
Left(GenericConversionError(s"Record expected"))
}
fields <- value.fields zip dt.fields.toImmArray traverseU {
case ((von, vv), (tn, fieldType)) =>
for {
_ <- von.cata(
vn =>
Either.cond(
(vn: String) == (tn: String),
(),
GenericConversionError(s"field order mismatch: expected $tn, got $vn"),
),
Right(()),
)
newVv <- fillInTypeInfo(vv, fieldType, ctx)
} yield (Some(tn), newVv)
}
} yield V.ValueRecord(Some(typeCon.name.identifier), fields)
private def fillInListTI(
list: Model.ApiList,
typ: Model.DamlLfType,
ctx: Context,
): Result[Model.ApiList] =
for {
elementType <- typ match {
case Model.DamlLfTypePrim(Model.DamlLfPrimType.List, Seq(t)) =>
Right(t)
case _ => Left(GenericConversionError(s"Cannot read $list as $typ"))
}
values <- list.values traverse (fillInTypeInfo(_, elementType, ctx))
} yield V.ValueList(values)
private def fillInTextMapTI(
textMap: Model.ApiMap,
typ: Model.DamlLfType,
ctx: Context,
): Result[Model.ApiMap] =
for {
elementType <- typ match {
case Model.DamlLfTypePrim(Model.DamlLfPrimType.TextMap, Seq(t)) =>
Right(t)
case _ => Left(GenericConversionError(s"Cannot read $textMap as $typ"))
}
values <- textMap.value traverse (fillInTypeInfo(_, elementType, ctx))
} yield V.ValueTextMap(values)
private def fillInGenMapTI(
genMap: Model.ApiGenMap,
typ: Model.DamlLfType,
ctx: Context,
): Result[Model.ApiGenMap] =
for {
types <- typ match {
case Model.DamlLfTypePrim(Model.DamlLfPrimType.GenMap, Seq(kT, vT)) =>
Right((kT, vT))
case _ => Left(GenericConversionError(s"Cannot read $genMap as $typ"))
}
(keyType, valueType) = types
values <- genMap.entries.toSeq traverse { case (k, v) =>
for {
key <- fillInTypeInfo(k, keyType, ctx)
value <- fillInTypeInfo(v, valueType, ctx)
} yield key -> value
}
} yield V.ValueGenMap(values.toImmArray)
private def fillInOptionalTI(
opt: Model.ApiOptional,
typ: Model.DamlLfType,
ctx: Context,
): Result[Model.ApiOptional] =
for {
optType <- typ match {
case Model.DamlLfTypePrim(Model.DamlLfPrimType.Optional, Seq(t)) =>
Right(t)
case _ => Left(GenericConversionError(s"Cannot read $opt as $typ"))
}
value <- opt.value traverse (fillInTypeInfo(_, optType, ctx))
} yield V.ValueOptional(value)
private def asTypeCon(
typ: Model.DamlLfType,
selector: Model.ApiValue,
): Result[Model.DamlLfTypeCon] =
typ match {
case t @ Model.DamlLfTypeCon(_, _) => Right(t)
case _ => Left(GenericConversionError(s"Cannot read $selector as $typ"))
}
private def fillInVariantTI(
variant: Model.ApiVariant,
typ: Model.DamlLfType,
ctx: Context,
): Result[Model.ApiVariant] =
for {
typeCon <- asTypeCon(typ, variant)
ddt <- ctx.templates
.damlLfDefDataType(typeCon.name.identifier)
.toRight(GenericConversionError(s"Unknown type ${typeCon.name.identifier}"))
dt <- typeCon.instantiate(ddt) match {
case v @ typesig.Variant(_) => Right(v)
case typesig.Record(_) | typesig.Enum(_) =>
Left(GenericConversionError(s"Variant expected"))
}
constructor = variant.variant
choice <- dt.fields
.collectFirst { case (`constructor`, cargTyp) => cargTyp }
.toRight(GenericConversionError(s"Unknown enum constructor $constructor"))
value = variant.value
argument <- fillInTypeInfo(value, choice, ctx)
} yield variant.copy(tycon = Some(typeCon.name.identifier), value = argument)
private def fillInEnumTI(
enumeration: V.ValueEnum,
typ: Model.DamlLfType,
): Result[V.ValueEnum] =
for {
typeCon <- asTypeCon(typ, enumeration)
} yield enumeration.copy(tycon = Some(typeCon.name.identifier))
private def readArgument(
value: V1.value.Value,
typ: Model.DamlLfType,
ctx: Context,
): Result[Model.ApiValue] =
validateValue(value)
.leftMap(sre => GenericConversionError(sre.getMessage))
.flatMap(vv => fillInTypeInfo(vv, typ, ctx))
/** Add `tycon`s and record field names where absent. */
private def fillInTypeInfo(
value: Model.ApiValue,
typ: Model.DamlLfType,
ctx: Context,
): Result[Model.ApiValue] =
value match {
case v: V.ValueEnum => fillInEnumTI(v, typ)
case _: V.ValueCidlessLeaf | _: V.ValueContractId => Right(value)
case v: Model.ApiOptional => fillInOptionalTI(v, typ, ctx)
case v: Model.ApiMap => fillInTextMapTI(v, typ, ctx)
case v: Model.ApiGenMap => fillInGenMapTI(v, typ, ctx)
case v: Model.ApiList => fillInListTI(v, typ, ctx)
case v: Model.ApiRecord => fillInRecordTI(v, typ, ctx)
case v: Model.ApiVariant => fillInVariantTI(v, typ, ctx)
}
// ------------------------------------------------------------------------------------------------------------------
// Write methods (Model -> V1)
// ------------------------------------------------------------------------------------------------------------------
def writeArgument(value: Model.ApiValue): Result[V1.value.Value] =
lfValueToApiValue(verbose = true, value) leftMap GenericConversionError
def writeRecordArgument(value: Model.ApiRecord): Result[V1.value.Record] =
lfValueToApiRecord(verbose = true, value) leftMap GenericConversionError
/** Write a composite command consisting of just the given command */
def writeCommands(
party: Model.PartyState,
command: Model.Command,
ledgerId: String,
applicationId: Ref.LedgerString,
): Result[V1.commands.Commands] = {
for {
ledgerCommand <- writeCommand(party, command)
} yield {
V1.commands.Commands(
ledgerId,
Tag.unwrap(command.workflowId),
applicationId,
Tag.unwrap(command.id),
Tag.unwrap(party.name),
List(ledgerCommand),
)
}
}
def writeCommand(
party: Model.PartyState,
command: Model.Command,
): Result[V1.commands.Command] = {
command match {
case cmd: Model.CreateCommand =>
writeCreateContract(party, cmd.template, cmd.argument)
case cmd: Model.ExerciseCommand =>
writeExerciseChoice(party, cmd.contract, cmd.interfaceId, cmd.choice, cmd.argument)
}
}
def writeCreateContract(
party: Model.PartyState,
templateId: Model.DamlLfIdentifier,
value: Model.ApiRecord,
): Result[V1.commands.Command] = {
for {
template <- Converter.checkExists(
party.packageRegistry.template(templateId),
GenericConversionError(s"Template '$templateId' not found"),
)
argument <- writeRecordArgument(value)
} yield {
V1.commands.Command(
V1.commands.Command.Command.Create(
V1.commands.CreateCommand(
Some(template.id.asApi),
Some(argument),
)
)
)
}
}
def writeExerciseChoice(
party: Model.PartyState,
contractId: ApiTypes.ContractId,
interfaceId: Option[DamlLfIdentifier],
choiceId: ApiTypes.Choice,
value: Model.ApiValue,
): Result[V1.commands.Command] = {
for {
contract <- Converter.checkExists(
party.ledger.contract(contractId, party.packageRegistry),
GenericConversionError(s"Contract '${Tag.unwrap(contractId)}' not found"),
)
_ <- Converter.checkExists(
contract.template.choices.find(c => c.name == choiceId),
GenericConversionError(s"Choice '${Tag.unwrap(choiceId)}' not found"),
)
argument <- writeArgument(value)
} yield {
V1.commands.Command(
V1.commands.Command.Command.Exercise(
V1.commands.ExerciseCommand(
interfaceId.orElse(Some(contract.template.id)).map(_.asApi),
Tag.unwrap(contractId),
Tag.unwrap(choiceId),
Some(argument),
)
)
)
}
}
}

View File

@ -1,116 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import scalaz.{@@, Tag}
import com.daml.lf.{data => DamlLfData}
import com.daml.lf.data.{Ref => DamlLfRef}
import com.daml.lf.{typesig => DamlLfIface}
import com.daml.lf.value.json.NavigatorModelAliases
import com.daml.ledger.api.{v1 => ApiV1}
import com.daml.ledger.api.refinements.ApiTypes
package object model extends NavigatorModelAliases[String] {
/** An opaque identifier used for templates.
* Templates are usually identified using a composite type (see [[DamlLfIdentifier]]).
*/
type TemplateStringId = String @@ TemplateStringIdTag
val TemplateStringId = Tag.of[TemplateStringIdTag]
type InterfaceStringId = String @@ InterfaceStringIdTag
val InterfaceStringId = Tag.of[InterfaceStringIdTag]
// ----------------------------------------------------------------------------------------------
// Types used in the ledger API
// ----------------------------------------------------------------------------------------------
type EventId = ApiTypes.EventId
type ContractId = ApiTypes.ContractId
type TemplateId = ApiTypes.TemplateId
type Party = DamlLfRef.Party
type CommandId = ApiTypes.CommandId
type WorkflowId = ApiTypes.WorkflowId
// ----------------------------------------------------------------------------------------------
// Types used in Daml-LF
// ----------------------------------------------------------------------------------------------
/** A dot-separated list of strings */
type DamlLfDottedName = DamlLfRef.DottedName
val DamlLfDottedName = DamlLfRef.DottedName
/** A qualified name, referencing entities from the same Daml-LF package */
type DamlLfQualifiedName = DamlLfRef.QualifiedName
val DamlLfQualifiedName = DamlLfRef.QualifiedName
type DamlLfTypeConNameOrPrimType = DamlLfIface.TypeConNameOrPrimType
type DamlLfImmArraySeq[T] = DamlLfData.ImmArray.ImmArraySeq[T]
val DamlLfImmArraySeq = DamlLfData.ImmArray.ImmArraySeq
type DamlLfImmArray[T] = DamlLfData.ImmArray[T]
val DamlLfImmArray = DamlLfData.ImmArray
type DamlLfFieldWithType = DamlLfIface.FieldWithType
// ----------------------------------------------------------------------------------------------
// Conversion between API Identifier, Daml-LF Identifier, and String
// ----------------------------------------------------------------------------------------------
implicit class IdentifierApiConversions(val id: ApiV1.value.Identifier) extends AnyVal {
def asDaml: DamlLfRef.Identifier =
DamlLfRef.Identifier(
DamlLfRef.PackageId.assertFromString(id.packageId),
DamlLfRef.QualifiedName(
DamlLfRef.DottedName.assertFromString(id.moduleName),
DamlLfRef.DottedName.assertFromString(id.entityName),
),
)
/** An opaque unique string for this identifier */
def asOpaqueString: String = id.asDaml.asOpaqueString
}
implicit class IdentifierDamlConversions(val id: DamlLfRef.Identifier) extends AnyVal {
def asApi: ApiV1.value.Identifier =
ApiV1.value.Identifier(
id.packageId,
id.qualifiedName.module.toString(),
id.qualifiedName.name.toString(),
)
/** An opaque unique string for this identifier */
def asOpaqueString: String =
opaqueIdentifier(id.qualifiedName.toString, id.packageId)
}
private[this] def opaqueIdentifier(qualifiedName: String, packageId: String): String =
s"$qualifiedName@$packageId"
private[this] val opaqueIdentifierRegex = "([^@]*)@([^@]*)".r
def parseOpaqueIdentifier(id: String): Option[DamlLfRef.Identifier] = {
id match {
case opaqueIdentifierRegex(qualifiedName, packageId) =>
Some(
DamlLfRef.Identifier(
DamlLfRef.PackageId.assertFromString(packageId),
DamlLfRef.QualifiedName.assertFromString(qualifiedName),
)
)
case _ =>
None
}
}
def parseTemplateOpaqueIdentifier(id: TemplateStringId): Option[DamlLfRef.Identifier] =
parseOpaqueIdentifier(TemplateStringId.unwrap(id))
def parseInterfaceOpaqueIdentifier(id: InterfaceStringId): Option[DamlLfRef.Identifier] =
parseOpaqueIdentifier(InterfaceStringId.unwrap(id))
}
package model {
sealed trait TemplateStringIdTag
sealed trait InterfaceStringIdTag
}

View File

@ -1,68 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.query
import com.daml.navigator.dotnot.{OnTreeReady, PropertyCursor}
import com.daml.navigator.model._
import com.daml.navigator.query.filter._
import com.typesafe.scalalogging.LazyLogging
sealed abstract class FilteringPager[N <: Node[_]](
criterion: FilterCriterionBase,
filter: OnTreeReady[N, Boolean, DamlLfTypeLookup],
ps: DamlLfTypeLookup,
) extends PagerDecorator[N]
with LazyLogging {
final override def decorate(page: Page[N], ledger: Ledger): Page[N] = {
val included = page.rows.filter(isIncluded)
page.copy(total = included.size, rows = included)
}
private[query] def isIncluded(node: N): Boolean = {
def loop(criterion: FilterCriterionBase): Boolean = {
criterion match {
case AndFilterCriterion(criteria) =>
criteria.forall(loop)
case OrFilterCriterion(criteria) =>
criteria.exists(loop)
case criterion: FilterCriterion =>
matchCriterion(node)(criterion)
}
}
loop(criterion)
}
private[query] def matchCriterion(node: N)(criterion: FilterCriterion): Boolean = {
val cursor = PropertyCursor.fromString(criterion.field)
filter.run(node, cursor, criterion.value, ps) match {
case Left(failure) =>
logger.error(
s"Cannot match $node and " +
s"criterion $criterion: $failure. Excluding it."
)
false
case Right(isMatching) =>
isMatching
}
}
}
final class ContractFilter(
criterion: FilterCriterionBase,
ps: DamlLfTypeLookup,
val delegate: Pager[Contract],
) extends FilteringPager[Contract](criterion, contractFilter, ps)
final class TemplateFilter(
criterion: FilterCriterionBase,
ps: DamlLfTypeLookup,
val delegate: Pager[Template],
) extends FilteringPager[Template](criterion, templateFilter, ps)
final class CommandFilter(
criterion: FilterCriterionBase,
ps: DamlLfTypeLookup,
val delegate: Pager[Command],
) extends FilteringPager[Command](criterion, commandFilter, ps)

View File

@ -1,82 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.query
import com.daml.navigator.model._
case class Page[N <: Node[_]](offset: Int, total: Int, rows: Seq[N], sortedLike: Seq[SortCriterion])
@SuppressWarnings(Array("org.wartremover.warts.Enumeration"))
object SortDirection extends Enumeration {
val ASCENDING, DESCENDING = Value
}
case class SortCriterion(field: String, direction: SortDirection.Value)
sealed abstract class FilterCriterionBase
final case class OrFilterCriterion(filters: List[FilterCriterionBase]) extends FilterCriterionBase
final case class AndFilterCriterion(filters: List[FilterCriterionBase]) extends FilterCriterionBase
final case class FilterCriterion(field: String, value: String) extends FilterCriterionBase
trait Pager[N <: Node[_]] {
def fetch(ledger: Ledger, templates: PackageRegistry): Page[N]
}
final object ActiveContractsPager extends Pager[Contract] {
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Contract] =
Page(0, ledger.activeContractsCount, ledger.activeContracts(templates), Seq.empty)
}
final object AllContractsPager extends Pager[Contract] {
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Contract] =
Page(0, ledger.allContractsCount, ledger.allContracts(templates), Seq.empty)
}
final object TemplatePager extends Pager[Template] {
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Template] =
Page(0, templates.templateCount, templates.allTemplates(), Seq.empty)
}
final class TemplateContractPager(template: Template) extends Pager[Contract] {
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Contract] = {
val contracts = ledger.templateContractsOf(template, templates)
Page(0, contracts.size, contracts, Seq.empty)
}
}
final class ActiveTemplateContractPager(template: Template) extends Pager[Contract] {
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Contract] = {
val contracts = ledger.activeTemplateContractsOf(template, templates)
Page(0, contracts.size, contracts, Seq.empty)
}
}
final object CommandPager extends Pager[Command] {
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[Command] = {
val commands = ledger.allCommands(templates)
Page(0, commands.size, commands, Seq.empty)
}
}
trait PagerDecorator[N <: Node[_]] extends Pager[N] {
override def fetch(ledger: Ledger, templates: PackageRegistry): Page[N] =
decorate(delegate.fetch(ledger, templates), ledger)
def delegate: Pager[N]
def decorate(page: Page[N], ledger: Ledger): Page[N]
}
class BoundingPager[N <: Node[_]](size: Int, val delegate: Pager[N]) extends PagerDecorator[N] {
override def decorate(page: Page[N], ledger: Ledger): Page[N] =
page.copy(rows = page.rows.take(size))
}
class ShiftingPager[N <: Node[_]](previous: String, val delegate: Pager[N])
extends PagerDecorator[N] {
override def decorate(page: Page[N], ledger: Ledger): Page[N] = {
val shiftedRows = page.rows.dropWhile(_.id != previous).drop(1)
page.copy(offset = page.rows.size - shiftedRows.size, rows = shiftedRows)
}
}

View File

@ -1,88 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.query
import com.daml.navigator.dotnot.{OnTreeReady, PropertyCursor}
import com.daml.navigator.model._
import com.daml.navigator.query.project._
import com.typesafe.scalalogging.LazyLogging
import scala.annotation.tailrec
sealed abstract class SortingPager[N <: Node[_]](
criteria: List[SortCriterion],
project: OnTreeReady[N, ProjectValue, DamlLfTypeLookup],
ps: DamlLfTypeLookup,
) extends PagerDecorator[N]
with LazyLogging {
final override def decorate(page: Page[N], ledger: Ledger): Page[N] =
page.copy(rows = sort(page.rows), sortedLike = criteria)
def sort(rows: Seq[N]): Seq[N] =
rows.sortBy(projectAll)(ordering)
private val ordering =
new Ordering[List[Option[ProjectValue]]] {
val optionProjectValueOrdering = Ordering[Option[ProjectValue]]
override def compare(l1: List[Option[ProjectValue]], l2: List[Option[ProjectValue]]): Int = {
@tailrec
def loop(
l1: List[Option[ProjectValue]],
l2: List[Option[ProjectValue]],
criteria: List[SortCriterion],
): Int = {
l1 match {
case Nil => 0
case x1 :: xs1 =>
val x2 :: xs2 = l2
val c :: cs = criteria
optionProjectValueOrdering.compare(x1, x2) match {
case 0 => loop(xs1, xs2, cs)
case x =>
c.direction match {
case SortDirection.DESCENDING => x * (-1)
case SortDirection.ASCENDING => x
}
}
}
}
loop(l1, l2, criteria)
}
}
private def projectAll(node: N): List[Option[ProjectValue]] =
criteria.map(project(node))
private def project(node: N)(criterion: SortCriterion): Option[ProjectValue] = {
val cursor = PropertyCursor.fromString(criterion.field)
val x = project.run(node, cursor, "", ps)
x match {
case Left(failure) =>
logger.error(s"Cannot project $node with criterion $criterion: $failure. Using None.")
None
case Right(value) =>
Some(value)
}
}
}
final class ContractSorter(
val criteria: List[SortCriterion],
ps: DamlLfTypeLookup,
val delegate: Pager[Contract],
) extends SortingPager[Contract](criteria, contractProject, ps)
final class TemplateSorter(
val criteria: List[SortCriterion],
ps: DamlLfTypeLookup,
val delegate: Pager[Template],
) extends SortingPager[Template](criteria, templateProject, ps)
final class CommandSorter(
val criteria: List[SortCriterion],
ps: DamlLfTypeLookup,
val delegate: Pager[Command],
) extends SortingPager[Command](criteria, commandProject, ps)

View File

@ -1,339 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.query
import com.daml.lf.value.json.ApiValueImplicits._
import com.daml.lf.value.{Value => V}
import com.daml.navigator.dotnot._
import com.daml.navigator.model._
import scalaz.Tag
import scalaz.syntax.tag._
import scala.annotation.nowarn
import scala.util.{Failure, Success, Try}
package object filter {
object checkContained extends ((String, String) => Boolean) {
override def apply(v1: String, v2: String): Boolean =
v1.toLowerCase contains v2.toLowerCase
override def toString(): String =
"target.contains(value)"
}
object checkOptionalContained extends ((Option[String], String) => Boolean) {
override def apply(v1: Option[String], v2: String): Boolean =
v1.exists(_.toLowerCase contains v2.toLowerCase)
override def toString(): String =
"target.exists(_.contains(value))"
}
def checkParameter(
rootParam: DamlLfType,
cursor: PropertyCursor,
expectedValue: String,
ps: DamlLfTypeLookup,
): Either[DotNotFailure, Boolean] = {
@annotation.tailrec
def loop(parameter: DamlLfType, cursor: PropertyCursor): Either[DotNotFailure, Boolean] =
parameter match {
case tc: DamlLfTypeCon =>
val nextOrResult =
(ps(tc.name.identifier).map(tc.instantiate(_)), cursor.next) match {
case (Some(DamlLfRecord(fields)), Some(nextCursor)) =>
fields
.collectFirst { case (nextCursor.current, fType) =>
fType -> nextCursor
}
.toLeft(false)
case (Some(DamlLfVariant(fields)), Some(nextCursor)) =>
fields
.collectFirst { case (nextCursor.current, fType) =>
fType -> nextCursor
}
.toLeft(false)
case (Some(DamlLfEnum(constructors)), _) =>
Right(constructors.exists(checkContained(_, expectedValue)))
case (None, _) | (_, None) =>
Right(false)
}
nextOrResult match {
case Right(r) => Right(r)
case Left((typ, nextCursor)) => loop(typ, nextCursor)
}
case DamlLfTypeVar(name) => Right(checkContained(name, expectedValue))
case DamlLfTypePrim(DamlLfPrimType.Bool, _) => Right(checkContained("bool", expectedValue))
case DamlLfTypeNumeric(_) => Right(checkContained("decimal", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.Int64, _) =>
Right(checkContained("int64", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.Date, _) => Right(checkContained("date", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.Text, _) => Right(checkContained("text", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.Party, _) =>
Right(checkContained("party", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.Timestamp, _) =>
Right(checkContained("timestamp", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.Unit, _) => Right(checkContained("unit", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.List, _) => Right(checkContained("list", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.ContractId, _) =>
Right(checkContained("contractid", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.Optional, _) =>
Right(checkContained("optional", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.TextMap, _) =>
Right(checkContained("textmap", expectedValue))
case DamlLfTypePrim(DamlLfPrimType.GenMap, _) =>
Right(checkContained("genmap", expectedValue))
}
loop(rootParam, cursor.prev.get)
}
def checkOptionalValue(
rootArgument: Option[ApiValue],
cursor: PropertyCursor,
expectedValue: String,
ps: DamlLfTypeLookup,
): Either[DotNotFailure, Boolean] =
rootArgument.fold[Either[DotNotFailure, Boolean]](Right(false))(
checkValue(_, cursor, expectedValue, ps)
)
@nowarn("msg=parameter ps .* is never used") // conform to opaque's signature
def checkValue(
rootArgument: ApiValue,
cursor: PropertyCursor,
expectedValue: String,
ps: DamlLfTypeLookup,
): Either[DotNotFailure, Boolean] = {
@annotation.tailrec
def loop(argument: ApiValue, cursor: PropertyCursor): Either[DotNotFailure, Boolean] =
argument match {
case V.ValueContractId(value) =>
cursor.ensureLast("ContractId")(checkContained(value.coid, expectedValue))
case V.ValueInt64(value) =>
cursor.ensureLast("Int64")(checkContained(value.toString, expectedValue))
case V.ValueNumeric(value) =>
cursor.ensureLast("Numeric")(checkContained(value.toUnscaledString, expectedValue))
case V.ValueText(value) =>
cursor.ensureLast("Text")(checkContained(value, expectedValue))
case V.ValueParty(value) =>
cursor.ensureLast("Party")(checkContained(value, expectedValue))
case V.ValueBool(value) =>
cursor.ensureLast("Bool")(checkContained(value.toString, expectedValue))
case V.ValueUnit =>
cursor.ensureLast("Unit")(expectedValue == "")
case t: V.ValueTimestamp =>
cursor.ensureLast("Timestamp")(checkContained(t.toIso8601, expectedValue))
case t: V.ValueDate =>
cursor.ensureLast("Date")(checkContained(t.toIso8601, expectedValue))
case V.ValueRecord(_, fields) =>
cursor.next match {
case None => Right(false)
case Some(nextCursor) =>
val current: String = nextCursor.current
fields.toSeq.collectFirst { case (Some(`current`), value) => value } match {
case Some(nextField) => loop(nextField, nextCursor)
case None => Right(false)
}
}
case V.ValueVariant(_, constructor, value) =>
cursor.next match {
case None => Right(false)
case Some(nextCursor) =>
nextCursor.current match {
case "__constructor" => Right(checkContained(constructor, expectedValue))
case "__value" => loop(value, nextCursor)
case `constructor` => loop(value, nextCursor)
case _ => Right(false)
}
}
case V.ValueEnum(_, constructor) =>
cursor.next match {
case None => Right(false)
case Some(nextCursor) =>
nextCursor.current match {
case "__constructor" => Right(checkContained(constructor, expectedValue))
case _ => Right(false)
}
}
case V.ValueList(elements) =>
cursor.next match {
case None => Right(false)
case Some(nextCursor) =>
Try(nextCursor.current.toInt) match {
case Success(index) => loop(elements.slowApply(index), nextCursor)
case Failure(_) =>
Left(TypeCoercionFailure("list index", "int", cursor, cursor.current))
}
}
case V.ValueOptional(optValue) =>
(cursor.next, optValue) match {
case (None, None) => Right(expectedValue == "None")
case (None, Some(_)) => Right(expectedValue == "Some")
case (Some(nextCursor), Some(value)) if nextCursor.current == "Some" =>
loop(value, nextCursor)
case (Some(nextCursor), None) if nextCursor.current == "None" => Right(true)
case (Some(_), _) => Right(false)
}
case V.ValueTextMap(textMap) =>
cursor.next match {
case None => Right(false)
case Some(nextCursor) =>
textMap.toImmArray.toSeq.collectFirst {
case (k, v) if k == nextCursor.current => v
} match {
case None => Right(false)
case Some(v) => loop(v, nextCursor)
}
}
case V.ValueGenMap(entries) =>
cursor.next match {
case None =>
Right(false)
case Some(nextCursor) =>
Try(nextCursor.current.toInt) match {
case Success(index) =>
nextCursor.next match {
case Some(nextNextCursor) if nextNextCursor.current == "key" =>
loop(entries(index)._1, nextNextCursor)
case Some(nextNextCursor) if nextNextCursor.current == "value" =>
loop(entries(index)._2, nextNextCursor)
case Some(_) => Left(UnknownProperty("genmap", nextCursor, expectedValue))
case None =>
Right(false)
}
case Failure(_) =>
Left(TypeCoercionFailure("GenMap index", "int", cursor, cursor.current))
}
}
}
loop(rootArgument, cursor.prev.get)
}
lazy val parameterFilter =
opaque[DamlLfType, Boolean, DamlLfTypeLookup]("parameter")((t, c, e, p) =>
checkParameter(t, c, e, p)
)
lazy val parameterIdFilter =
opaque[DamlLfIdentifier, Boolean, DamlLfTypeLookup]("parameter")((id, c, e, p) =>
checkParameter(DamlLfTypeCon(DamlLfTypeConName(id), DamlLfImmArraySeq()), c, e, p)
)
lazy val argumentFilter =
opaque[ApiValue, Boolean, DamlLfTypeLookup]("argument")(checkValue)
lazy val keyFilter =
opaque[Option[ApiValue], Boolean, DamlLfTypeLookup]("key")(checkOptionalValue)
lazy val templateFilter =
root[Template, Boolean, DamlLfTypeLookup]("template")
.onLeaf("id")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((contract, id) => checkContained(contract.idString, id))
.onLeaf("topLevelDecl")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((template, topLevelDecl) =>
checkContained(template.topLevelDecl, topLevelDecl)
)
.onBranch("parameter", _.id, parameterIdFilter)
.onBranch("choices", _.choices, choicesFilter)
// .onStar(check all fields)
lazy val contractFilter =
root[Contract, Boolean, DamlLfTypeLookup]("contract")
.onLeaf("id")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((contract, id) => checkContained(contract.id.unwrap, id.toLowerCase))
.onBranch("template", _.template, templateFilter)
.onBranch("argument", _.argument, argumentFilter)
.onBranch("key", _.key, keyFilter)
.onLeaf("agreementText")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((contract, agree) => checkOptionalContained(contract.agreementText, agree))
.onTree
.onLeaf("signatories")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((contract, signatory) =>
contract.signatories.map(Tag.unwrap).exists(checkContained(_, signatory))
)
.onTree
.onLeaf("observers")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((contract, observer) =>
contract.observers.map(Tag.unwrap).exists(checkContained(_, observer))
)
.onTree
// .onStar(check all fields)
lazy val choicesFilter =
root[Seq[Choice], Boolean, DamlLfTypeLookup]("choices")
.onElements[Choice](choice => Tag.unwrap(choice.name), choiceFilter)
lazy val choiceFilter =
root[Choice, Boolean, DamlLfTypeLookup]("choice")
.onLeaf("name")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((choice, name) => checkContained(Tag.unwrap(choice.name), name.toLowerCase))
.onLeaf("consuming")
.onAnyValue
.perform[Boolean]((choice, consuming) => choice.consuming == consuming)
.onBranch("parameter", _.parameter, parameterFilter)
// .onStar(check all fields)
.onTree
lazy val commandFilter =
root[Command, Boolean, DamlLfTypeLookup]("command")
.onLeaf("id")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((command, id) => checkContained(command.id.unwrap, id.toLowerCase))
.onLeaf("index")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((command, index) =>
checkContained(command.index.toString, index.toLowerCase)
)
.onLeaf("platformTime")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((command, time) =>
checkContained(command.platformTime.toString, time.toLowerCase)
)
.onLeaf("workflowId")
.onValue("*")
.const(true)
.onAnyValue
.perform[String]((command, id) => checkContained(command.workflowId.unwrap, id.toLowerCase))
.onTree
// .onStar(check all fields)
}

View File

@ -1,303 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.query
import com.daml.lf.value.json.ApiValueImplicits._
import com.daml.lf.value.{Value => V}
import com.daml.navigator.dotnot._
import com.daml.navigator.model._
import scalaz.Tag
import scalaz.syntax.tag._
import scala.annotation.nowarn
import scala.util.{Failure, Success, Try}
@SuppressWarnings(Array("org.wartremover.warts.Product", "org.wartremover.warts.Serializable"))
object project {
/** ADT of primitives used as result of a projection over contracts or templates */
sealed abstract class ProjectValue
final case class StringValue(value: String) extends ProjectValue
final case class NumberValue(value: BigDecimal) extends ProjectValue
final case class BooleanValue(value: Boolean) extends ProjectValue
implicit val projectValueOrdering: Ordering[ProjectValue] = new Ordering[ProjectValue] {
override def compare(x: ProjectValue, y: ProjectValue): Int =
x match {
case StringValue(s) =>
y match {
case StringValue(s2) => s compareTo s2
case _ => 1
}
case NumberValue(n) =>
y match {
case NumberValue(n2) => n compare n2
case _: StringValue => -1
case _: BooleanValue => 1
}
case BooleanValue(b) =>
y match {
case BooleanValue(b2) => b compareTo b2
case _ => -1
}
}
}
def checkParameter(
rootParam: DamlLfType,
cursor: PropertyCursor,
value: String,
ps: DamlLfTypeLookup,
): Either[DotNotFailure, ProjectValue] = {
@annotation.tailrec
def loop(
parameter: DamlLfType,
cursor: PropertyCursor,
ps: DamlLfTypeLookup,
): Either[DotNotFailure, ProjectValue] =
parameter match {
case tc: DamlLfTypeCon =>
val tyCon = tc.name.identifier
ps(tyCon) match {
case Some(ddt) =>
val next = tc.instantiate(ddt) match {
case DamlLfEnum(_) =>
Right(StringValue(tyCon.toString))
case DamlLfRecord(fields) =>
Left("record" -> fields)
case DamlLfVariant(constructors) =>
Left("variant" -> constructors)
}
next match {
case Left((description, fields)) =>
cursor.next match {
case Some(nextCursor) =>
fields.collectFirst { case (nextCursor.current, fieldType) =>
fieldType
} match {
case Some(nextType) =>
loop(nextType, nextCursor, ps)
case None =>
Left(UnknownProperty(description, nextCursor, value))
}
case None =>
Left(MustNotBeLastPart("DataType", cursor, value))
}
case Right(result) =>
Right(result)
}
case None =>
Left(UnknownType(tyCon.toString, cursor, value))
}
case DamlLfTypeVar(name) => Right(StringValue(name))
case DamlLfTypePrim(DamlLfPrimType.Bool, _) => Right(StringValue("bool"))
case DamlLfTypeNumeric(_) => Right(StringValue("decimal"))
case DamlLfTypePrim(DamlLfPrimType.Int64, _) => Right(StringValue("int64"))
case DamlLfTypePrim(DamlLfPrimType.Date, _) => Right(StringValue("date"))
case DamlLfTypePrim(DamlLfPrimType.Text, _) => Right(StringValue("text"))
case DamlLfTypePrim(DamlLfPrimType.Party, _) => Right(StringValue("party"))
case DamlLfTypePrim(DamlLfPrimType.Timestamp, _) => Right(StringValue("timestamp"))
case DamlLfTypePrim(DamlLfPrimType.Unit, _) => Right(StringValue("unit"))
case DamlLfTypePrim(DamlLfPrimType.List, _) => Right(StringValue("list"))
case DamlLfTypePrim(DamlLfPrimType.ContractId, _) => Right(StringValue("contractid"))
case DamlLfTypePrim(DamlLfPrimType.Optional, _) => Right(StringValue("optional"))
case DamlLfTypePrim(DamlLfPrimType.TextMap, _) => Right(StringValue("textmap"))
case DamlLfTypePrim(DamlLfPrimType.GenMap, _) => Right(StringValue("genmap"))
}
loop(rootParam, cursor.prev.get, ps)
}
def checkOptionalValue(
rootArgument: Option[ApiValue],
cursor: PropertyCursor,
expectedValue: String,
ps: DamlLfTypeLookup,
): Either[DotNotFailure, ProjectValue] =
rootArgument.fold[Either[DotNotFailure, ProjectValue]](Right(StringValue("")))(
checkValue(_, cursor, expectedValue, ps)
)
@nowarn("msg=parameter ps .* is never used") // conforms to `opaque`'s signature
def checkValue(
rootArgument: ApiValue,
cursor: PropertyCursor,
expectedValue: String,
ps: DamlLfTypeLookup,
): Either[DotNotFailure, ProjectValue] = {
@annotation.tailrec
def loop(argument: ApiValue, cursor: PropertyCursor): Either[DotNotFailure, ProjectValue] =
argument match {
case V.ValueContractId(value) => cursor.ensureLast("contractid")(StringValue(value.coid))
case V.ValueInt64(value) => cursor.ensureLast("int64")(NumberValue(value))
case V.ValueNumeric(value) =>
cursor.ensureLast("numeric")(StringValue(value.toUnscaledString))
case V.ValueText(value) => cursor.ensureLast("text")(StringValue(value))
case V.ValueParty(value) => cursor.ensureLast("party")(StringValue(value))
case V.ValueBool(value) => cursor.ensureLast("bool")(BooleanValue(value))
case V.ValueUnit => cursor.ensureLast("unit")(StringValue(""))
case t: V.ValueTimestamp => cursor.ensureLast("timestamp")(StringValue(t.toIso8601))
case t: V.ValueDate => cursor.ensureLast("date")(StringValue(t.toIso8601))
case V.ValueRecord(_, fields) =>
cursor.next match {
case None => Left(MustNotBeLastPart("record", cursor, expectedValue))
case Some(nextCursor) =>
val current: String = nextCursor.current
fields.toSeq.collectFirst { case (Some(`current`), value) => value } match {
case Some(nextField) => loop(nextField, nextCursor)
case None => Left(UnknownProperty("record", nextCursor, expectedValue))
}
}
case V.ValueVariant(_, constructor, value) =>
cursor.next match {
case None => Left(MustNotBeLastPart("variant", cursor, expectedValue))
case Some(nextCursor) =>
nextCursor.current match {
case "__constructor" => Right(StringValue(constructor))
case "__value" => loop(value, nextCursor)
case `constructor` => loop(value, nextCursor)
case _ => Left(UnknownProperty("variant", nextCursor, expectedValue))
}
}
case V.ValueEnum(_, constructor) =>
cursor.next match {
case None => Left(MustNotBeLastPart("enum", cursor, expectedValue))
case Some(nextCursor) =>
nextCursor.current match {
case "__constructor" => Right(StringValue(constructor))
case _ => Left(UnknownProperty("enum", nextCursor, expectedValue))
}
}
case V.ValueOptional(optValue) =>
(cursor.next, optValue) match {
case (None, None) => Right(StringValue("None"))
case (None, Some(_)) => Right(StringValue("Some"))
case (Some(nextCursor), Some(value)) if nextCursor.current == "Some" =>
loop(value, nextCursor)
case (Some(nextCursor), None) if nextCursor.current == "None" => Right(StringValue(""))
case (Some(nextCursor), _) =>
Left(UnknownProperty("optional", nextCursor, expectedValue))
}
case V.ValueList(elements) =>
cursor.next match {
case None => Left(MustNotBeLastPart("list", cursor, expectedValue))
case Some(nextCursor) =>
Try(nextCursor.current.toInt) match {
case Success(index) => loop(elements.slowApply(index), nextCursor)
case Failure(_) =>
Left(TypeCoercionFailure("list index", "int", cursor, cursor.current))
}
}
case V.ValueTextMap(textMap) =>
cursor.next match {
case None => Left(MustNotBeLastPart("textmap", cursor, expectedValue))
case Some(nextCursor) =>
textMap.toImmArray.toSeq.collectFirst {
case (k, v) if k == nextCursor.current => v
} match {
case Some(v) => loop(v, nextCursor)
case None =>
Left(UnknownProperty(nextCursor.current, nextCursor, expectedValue))
}
}
case V.ValueGenMap(entries) =>
cursor.next match {
case None =>
Left(MustNotBeLastPart("genmap", cursor, expectedValue))
case Some(nextCursor) =>
Try(nextCursor.current.toInt) match {
case Success(index) =>
nextCursor.next match {
case Some(nextNextCursor) if nextNextCursor.current == "key" =>
loop(entries(index)._1, nextNextCursor)
case Some(nextNextCursor) if nextNextCursor.current == "value" =>
loop(entries(index)._2, nextNextCursor)
case Some(_) => Left(UnknownProperty("genmap", nextCursor, expectedValue))
case None =>
Left(UnknownProperty(nextCursor.current, nextCursor, expectedValue))
}
case Failure(_) =>
Left(TypeCoercionFailure("GenMap index", "int", cursor, cursor.current))
}
}
}
loop(rootArgument, cursor.prev.get)
}
lazy val parameterProject =
opaque[DamlLfType, ProjectValue, DamlLfTypeLookup]("parameter")((t, c, e, p) =>
checkParameter(t, c, e, p)
)
lazy val parameterIdProject =
opaque[DamlLfIdentifier, ProjectValue, DamlLfTypeLookup]("parameter")((id, c, e, p) =>
checkParameter(DamlLfTypeCon(DamlLfTypeConName(id), DamlLfImmArraySeq()), c, e, p)
)
lazy val argumentProject =
opaque[ApiValue, ProjectValue, DamlLfTypeLookup]("argument")(checkValue)
lazy val keyProject =
opaque[Option[ApiValue], ProjectValue, DamlLfTypeLookup]("key")(checkOptionalValue)
lazy val templateProject =
root[Template, ProjectValue, DamlLfTypeLookup]("template")
.onLeaf("id")
.onAnyValue
.perform[String]((contract, _) => StringValue(contract.idString))
.onBranch("parameter", _.id, parameterIdProject)
.onBranch("choices", _.choices, choicesProject)
lazy val contractProject =
root[Contract, ProjectValue, DamlLfTypeLookup]("contract")
.onLeaf("id")
.onAnyValue
.perform[String]((contract, _) => StringValue(contract.id.unwrap))
.onBranch("template", _.template, templateProject)
.onBranch("argument", _.argument, argumentProject)
.onBranch("key", _.key, keyProject)
.onLeaf("agreementText")
.onAnyValue
.perform[String]((contract, _) => StringValue(contract.agreementText.getOrElse("")))
.onLeaf("signatories")
.onAnyValue
.perform[String]((contract, _) => StringValue(contract.signatories.mkString))
.onLeaf("observers")
.onAnyValue
.perform[String]((contract, _) => StringValue(contract.observers.mkString))
.onTree
lazy val choicesProject =
root[Seq[Choice], ProjectValue, DamlLfTypeLookup]("choices")
.onElements[Choice](choice => Tag.unwrap(choice.name), choiceProject)
lazy val choiceProject =
root[Choice, ProjectValue, DamlLfTypeLookup]("choice")
.onLeaf("name")
.onAnyValue
.perform[String]((choice, _) => StringValue(Tag.unwrap(choice.name)))
.onLeaf("consuming")
.onAnyValue
.perform[String]((choice, _) => BooleanValue(choice.consuming))
.onBranch("parameter", _.parameter, parameterProject)
.onTree
lazy val commandProject =
root[Command, ProjectValue, DamlLfTypeLookup]("command")
.onLeaf("id")
.onAnyValue
.perform[String]((command, _) => StringValue(Tag.unwrap(command.id)))
.onLeaf("index")
.onAnyValue
.perform[String]((command, _) => NumberValue(command.index))
.onLeaf("workflowId")
.onAnyValue
.perform[String]((command, _) => StringValue(Tag.unwrap(command.workflowId)))
.onLeaf("platformTime")
.onAnyValue
.perform[String]((command, _) => StringValue(command.platformTime.toString))
.onTree
}

View File

@ -1,138 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.store
import java.time.Instant
import com.daml.ledger.api.domain.{PartyDetails, User}
import com.daml.navigator.model._
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.time.TimeProviderWithType
trait ActorStatus
/** Actor reports working at full health */
case object ActorRunning extends ActorStatus
/** Actor is still starting up */
case object ActorStarting extends ActorStatus
/** Actor reports a permanently failed state */
case class ActorFailed(error: Throwable) extends ActorStatus
/** Actor did not respond within a reasonable time */
case object ActorUnresponsive extends ActorStatus
object Store {
/** Reinitialize the platform connection and reset all local state `Unit` */
case object ResetConnection
case object UpdateUsersOrParties
case class UpdatedUsers(details: Seq[User])
case class UpdatedParties(details: List[PartyDetails])
/** Request to subscribe a party to the store (without response to sender). */
case class Subscribe(
displayName: String,
name: ApiTypes.Party,
userRole: Option[String] = None,
useDatabase: Boolean = false,
)
/** Request to create a contract instance for a template and respond with a `scala.util.Try[CommandId]`. */
case class CreateContract(party: PartyState, templateId: TemplateStringId, argument: ApiRecord)
/** Request to exercise a choice on a contract and respond with a `scala.util.Try[CommandId]`. */
case class ExerciseChoice(
party: PartyState,
contractId: ApiTypes.ContractId,
interfaceId: Option[InterfaceStringId],
choiceId: ApiTypes.Choice,
argument: ApiValue,
)
/** Request to respond with a `scala.util.Try[TimeProviderWithType]` with the current store time. */
case object ReportCurrentTime
/** Request to advance time to the specified instant and respond with updated store time as a
* `scala.util.Try[TimeProviderWithType]`.
*/
case class AdvanceTime(to: Instant)
/** An indication that the store was not able to execute a request. */
case class StoreException(message: String) extends Exception(message)
/** Request diagnostic information about the state of the application and respond with a [[ApplicationStateInfo]]. */
case object GetApplicationStateInfo
/** Request a list of all parties that have an active actor on the system matching the provided search string */
final case class GetParties(search: String)
/** Response to a request for parties */
final case class PartyList(parties: List[ApiTypes.Party])
/** Diagnostic information about the state of the application */
sealed trait ApplicationStateInfo {
def platformHost: String
def platformPort: Int
def tls: Boolean
def applicationId: String
}
/** Application is still connecting to the ledger */
final case class ApplicationStateConnecting(
platformHost: String,
platformPort: Int,
tls: Boolean,
applicationId: String,
) extends ApplicationStateInfo
/** Application is still connecting to the ledger */
final case class ApplicationStateConnected(
platformHost: String,
platformPort: Int,
tls: Boolean,
applicationId: String,
ledgerId: String,
ledgerTime: TimeProviderWithType,
// `partyActors`'s keys are passed to the frontend as possible user ids to log in as
partyActors: Map[String, PartyActorResponse],
) extends ApplicationStateInfo
/** Application failed to start up */
final case class ApplicationStateFailed(
platformHost: String,
platformPort: Int,
tls: Boolean,
applicationId: String,
error: Throwable,
) extends ApplicationStateInfo
/** Request diagnostic information about a party and respond with a [[PartyActorInfo]]. */
case object GetPartyActorInfo
sealed trait PartyActorResponse
final case class PartyActorRunning(info: PartyActorInfo) extends PartyActorResponse
final case object PartyActorUnresponsive extends PartyActorResponse
/** Diagnostic information about a party */
sealed trait PartyActorInfo {
def party: ApiTypes.Party = state.name
def state: PartyState
}
/** Actor still starting up */
final case class PartyActorStarting(state: PartyState) extends PartyActorInfo
/** Actor running and consuming the transaction stream */
final case class PartyActorStarted(state: PartyState) extends PartyActorInfo
/** Actor permanently failed */
final case class PartyActorFailed(
state: PartyState,
error: Throwable,
) extends PartyActorInfo
}

View File

@ -1,552 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.store.platform
import java.time.{Duration, Instant}
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
import org.apache.pekko.actor.{Actor, ActorLogging, ActorRef, Props, Scheduler, Stash}
import org.apache.pekko.pattern.{ask, pipe}
import org.apache.pekko.stream.Materializer
import org.apache.pekko.util.Timeout
import com.daml.grpc.GrpcException
import com.daml.grpc.adapter.{PekkoExecutionSequencerPool, ExecutionSequencerFactory}
import com.daml.ledger.api.domain.Feature
import com.daml.ledger.api.refinements.{ApiTypes, IdGenerator}
import com.daml.ledger.api.tls.TlsConfiguration
import com.daml.ledger.api.v1.testing.time_service.TimeServiceGrpc
import com.daml.ledger.client.LedgerClient
import com.daml.ledger.client.configuration.{
CommandClientConfiguration,
LedgerClientChannelConfiguration,
LedgerClientConfiguration,
LedgerIdRequirement,
}
import com.daml.ledger.client.services.testing.time.StaticTime
import com.daml.lf.data.Ref
import com.daml.navigator.ApplicationInfo
import com.daml.navigator.model._
import com.daml.navigator.store.Store._
import com.daml.navigator.time._
import com.daml.navigator.util.RetryHelper
import io.grpc.Channel
import io.grpc.netty.GrpcSslContexts
import io.netty.handler.ssl.SslContext
import org.slf4j.LoggerFactory
import scalaz.syntax.tag._
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration._
import scala.util.{Failure, Random, Success, Try}
object PlatformStore {
def props(
platformHost: String,
platformPort: Int,
tlsConfig: Option[TlsConfiguration],
accessToken: Option[String],
timeProviderType: TimeProviderType,
applicationInfo: ApplicationInfo,
ledgerMaxInbound: Int,
enableUserManagement: Boolean,
): Props =
Props(
classOf[PlatformStore],
platformHost,
platformPort,
tlsConfig,
accessToken,
timeProviderType,
applicationInfo,
ledgerMaxInbound,
enableUserManagement,
)
type PlatformTime = Instant
case class ConnectionResult(
ledgerClient: LedgerClient,
staticTime: Option[StaticTime],
time: TimeProviderWithType,
)
case class Connected(ledgerClient: Try[ConnectionResult])
case class StateConnected(
ledgerClient: LedgerClient,
// A key in `parties` could be: a Party's displayName, a party's name, a User's id, or a name specified in our config
parties: Map[String, PartyState],
staticTime: Option[StaticTime],
time: TimeProviderWithType,
)
case class StateInitial(parties: Map[String, PartyState])
case class StateFailed(error: Throwable)
}
/** Store implementation that accesses the platform API to fetch data and execute commands. */
@SuppressWarnings(Array("org.wartremover.warts.Any"))
class PlatformStore(
platformHost: String,
platformPort: Int,
tlsConfig: Option[TlsConfiguration],
token: Option[String],
timeProviderType: TimeProviderType,
applicationInfo: ApplicationInfo,
ledgerMaxInbound: Int,
enableUserManagement: Boolean,
) extends Actor
with ActorLogging
with Stash {
// ----------------------------------------------------------------------------------------------
// Global immutable state - mutable state is stored in parameters of the receive methods
// ----------------------------------------------------------------------------------------------
private val system = context.system
implicit val s: Scheduler = system.scheduler
implicit val ec: ExecutionContext = system.dispatcher
implicit val materializer: Materializer = Materializer(system)
implicit val esf: ExecutionSequencerFactory =
new PekkoExecutionSequencerPool("esf-" + this.getClass.getSimpleName)(system)
private val applicationId =
Ref.LedgerString.assertFromString(
applicationInfo.id + "#" + new Random(System.currentTimeMillis()).nextLong().toHexString
)
private[this] def userFacingLogger = LoggerFactory.getLogger("user-facing-logs")
import PlatformStore._
// ----------------------------------------------------------------------------------------------
// Lifecycle
// ----------------------------------------------------------------------------------------------
override def preStart(): Unit = {
connect()
}
override def postStop(): Unit = {
esf.close()
}
// ----------------------------------------------------------------------------------------------
// Messages
// ----------------------------------------------------------------------------------------------
override def receive: Receive = connecting(StateInitial(Map.empty[String, PartyState]))
def connecting(state: StateInitial): Receive = {
case Connected(Success(value)) =>
context.become(
connected(StateConnected(value.ledgerClient, state.parties, value.staticTime, value.time))
)
unstashAll()
case Connected(Failure(e)) =>
// Connection failed even after several retries - not sure how to recover from this
val message =
s"Permanently failed to connect to the ledger at $platformHost:$platformPort. " +
"Please fix any issues and restart this application."
userFacingLogger.error(message)
context.become(failed(StateFailed(e)))
case GetApplicationStateInfo =>
sender() ! ApplicationStateConnecting(
platformHost,
platformPort,
tlsConfig.exists(_.enabled),
applicationId,
)
case _ =>
stash()
}
def connected(state: StateConnected): Receive = {
case UpdateUsersOrParties =>
// If user management is enabled for navigator (the default),
// and the ledger supports it, only display users on the login screen.
// Otherwise, we fall back to legacy behavior of logging in as a party.
val hasUserManagement =
if (!enableUserManagement) Future.successful(false)
else
state.ledgerClient.versionClient
.getApiFeatures(state.ledgerClient.ledgerId)
.map { features =>
val hasUserManagement = features.contains(Feature.UserManagement)
if (!hasUserManagement) {
log.warning("User management is enabled but ledger does not expose this feature.")
}
hasUserManagement
}
// TODO https://github.com/digital-asset/daml/issues/12663 participant user management: Emulating no-pagination
def listAllUsers(client: LedgerClient) =
client.userManagementClient
.listUsers(token = None /* set at startup */, pageToken = "", pageSize = 10000)
.map { case (users, _) =>
users.toList
}
hasUserManagement
.flatMap {
case true =>
listAllUsers(state.ledgerClient)
.map(details => UpdatedUsers(details): Any)
// ^^^^^
// make wartremover happy about the lub of UpdatedUsers and UpdatedParties
case false =>
state.ledgerClient.partyManagementClient
.listKnownParties()
.map(details => UpdatedParties(details): Any)
}
.pipeTo(self)
()
case UpdatedUsers(users) =>
// Note: you cannot log in as a user without a primary party
val usersWithPrimaryParties = users.flatMap { user =>
user.primaryParty.map(p => (user.id, p))
}
users.filter(_.primaryParty.isEmpty) match {
case usersWithoutPrimaryParty if !usersWithoutPrimaryParty.isEmpty =>
log.warning(
s"Users without primary party (counted ${usersWithoutPrimaryParty.length})" +
s" cannot be used for login ${usersWithoutPrimaryParty.take(10).map(_.id.toString).mkString("(e.g., ", ", ", ")")}."
)
}
usersWithPrimaryParties.foreach { case (userId, party) =>
self ! Subscribe(userId, ApiTypes.Party(party))
}
case UpdatedParties(details) =>
details.foreach { partyDetails =>
if (partyDetails.isLocal) {
val displayName = partyDetails.displayName.getOrElse(partyDetails.party)
self ! Subscribe(displayName, ApiTypes.Party(partyDetails.party))
} else {
log.debug(s"Ignoring non-local party ${partyDetails.party}")
}
}
case Subscribe(displayName, name, userRole, useDatabase) =>
if (!state.parties.contains(displayName)) {
val partyState =
new PartyState(name, userRole, useDatabase) // do this allocation only once per party
log.info(s"Starting actor for party ${partyState.name} (display name $displayName)")
// start party actor if needed (since users subscribe to their primary party,
// we may subscribe to the same party under different display names, but we should only create one actor per party)
val partyActorName = partyState.actorName
if (context.child(partyActorName).isEmpty)
context.actorOf(
PlatformSubscriber.props(state.ledgerClient, partyState, applicationId, token),
partyActorName,
)
val updatedParties = state.parties + (displayName -> partyState)
context.become(connected(state.copy(parties = updatedParties)))
} else {
log.debug(s"Actor for party $name (display name $displayName) is already running")
}
case CreateContract(party, templateId, value) =>
createContract(state.time.time.getCurrentTime, party, templateId, value, sender())
case ExerciseChoice(party, contractId, interfaceId, choiceId, value) =>
exerciseChoice(
state.time.time.getCurrentTime,
party,
contractId,
interfaceId,
choiceId,
value,
sender(),
)
case GetParties(search) =>
val lowerCaseSearch = search.toLowerCase
val result = state.parties.values.view.collect {
case party if party.name.unwrap.toLowerCase.contains(lowerCaseSearch) => party.name
}
sender() ! PartyList(result.toList)
case ReportCurrentTime =>
sender() ! Success(state.time)
case AdvanceTime(to) =>
advanceTime(state.staticTime, to, sender())
case ResetConnection =>
// Wait for all children to stop, then initiate new connection
implicit val actorTimeout: Timeout = Timeout(60, TimeUnit.SECONDS)
context.children.foreach(child => child ? ResetConnection)
context.become(connecting(StateInitial(state.parties)))
connect()
case GetApplicationStateInfo =>
implicit val actorTimeout: Timeout = Timeout(5, TimeUnit.SECONDS)
// Store the original sender (sender is mutable)
val snd = sender()
// context.child must be invoked from actor thread
val userIdToActorRef = state.parties.view
.mapValues(partyState => context.child(partyState.actorName))
.toList // TODO can we keep this a map and make traverse work?
Future
.traverse(userIdToActorRef) { case (userId, actorRef) =>
// let Future deal with empty option actorRef
Future { actorRef.get }
.flatMap(_ ? GetPartyActorInfo)
.mapTo[PartyActorInfo]
.map(info => PartyActorRunning(info): PartyActorResponse)
.recover { case _ => PartyActorUnresponsive }
.map((userId, _))
}
.map(_.toMap)
.recover { case error =>
log.error(error.getMessage)
Map.empty[String, PartyActorResponse]
}
.foreach { userIdToPartyActorResponse =>
snd ! ApplicationStateConnected(
platformHost,
platformPort,
tlsConfig.exists(_.enabled),
applicationId,
state.ledgerClient.ledgerId.unwrap,
state.time,
userIdToPartyActorResponse,
)
}
}
// Permanently failed state
def failed(state: StateFailed): Receive = {
case GetApplicationStateInfo =>
sender() ! ApplicationStateFailed(
platformHost,
platformPort,
tlsConfig.exists(_.enabled),
applicationId,
state.error,
)
case _ => ()
}
// ----------------------------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------------------------
private def sslContext: Option[SslContext] =
tlsConfig.flatMap { c =>
if (c.enabled)
Some(
GrpcSslContexts
.forClient()
.trustManager(c.trustCollectionFile.orNull)
.keyManager(c.certChainFile.orNull, c.privateKeyFile.orNull)
.build
)
else None
}
private def connect(): Unit = {
val retryMaxAttempts = 10
val retryDelay = 5.seconds
val maxCommandsInFlight = 10
val maxParallelSubmissions = 10
val clientConfig = LedgerClientConfiguration(
applicationId,
LedgerIdRequirement.none,
CommandClientConfiguration(
maxCommandsInFlight,
maxParallelSubmissions,
Duration.ofSeconds(30),
),
token,
)
val channelConfig = LedgerClientChannelConfiguration(sslContext)
val result =
RetryHelper.retry(retryMaxAttempts, retryDelay)(RetryHelper.failFastOnPermissionDenied)(
tryConnect(clientConfig, channelConfig)
)
result onComplete {
case Failure(error) =>
log.error(
"Failed to connect to platform at '{}:{}': {}",
platformHost,
platformPort,
error.getMessage,
)
self ! PlatformStore.Connected(Failure(error))
case Success(connection) =>
log.info("Connected to platform at '{}:{}'", platformHost, platformPort)
self ! PlatformStore.Connected(Success(connection))
}
}
private def tryConnect(
clientConfig: LedgerClientConfiguration,
channelConfig: LedgerClientChannelConfiguration,
): Future[ConnectionResult] = {
if (channelConfig.sslContext.isDefined) {
log.info("Connecting to {}:{}, using TLS", platformHost, platformPort)
} else {
log.info("Connecting to {}:{}, using a plaintext connection", platformHost, platformPort)
}
for {
ledgerClient <- LedgerClient.singleHost(
platformHost,
platformPort,
clientConfig,
channelConfig.copy(maxInboundMessageSize = ledgerMaxInbound),
)
staticTime <- getStaticTime(ledgerClient.channel, ledgerClient.ledgerId.unwrap)
time <- getTimeProvider(staticTime)
} yield ConnectionResult(ledgerClient, staticTime, time)
}
private def getStaticTime(channel: Channel, ledgerId: String): Future[Option[StaticTime]] = {
// Note: StaticTime is a TimeProvider that is automatically updated by push events from the ledger.
Future
.fromTry(Try(TimeServiceGrpc.stub(channel)))
.flatMap(tp => StaticTime.updatedVia(tp, ledgerId, token))
.map(staticTime => {
log.info(s"Time service is available, platform time is ${staticTime.getCurrentTime}")
Some(staticTime)
})
.recover({
// If the time service is not implemented, then the ledger uses UTC time.
case GrpcException.UNIMPLEMENTED() =>
log.info("Time service is not implemented")
None
})
}
private def getTimeProvider(ledgerTime: Option[StaticTime]): Future[TimeProviderWithType] = {
TimeProviderFactory(timeProviderType, ledgerTime)
.fold[Future[TimeProviderWithType]]({
log.error("Unable to initialize the time provider")
Future.failed(StoreException("Unable to initialize the time provider"))
})(t => {
log.debug(s"Time provider initialized: type=${t.`type`}, time=${t.time.getCurrentTime}")
Future.successful(t)
})
}
private def advanceTime(staticTime: Option[StaticTime], to: Instant, sender: ActorRef): Unit = {
staticTime.fold[Unit](
sender ! Failure(StoreException("staticTime not available"))
)(t => {
log.info("Advancing time from {} to {}.", t.getCurrentTime, to)
t.setTime(to)
.map(_ => TimeProviderWithType(t, TimeProviderType.Static))
.recoverWith(apiFailure)
.andThen(returnToSender[TimeProviderWithType](sender))
()
})
}
private def createContract(
platformTime: Instant,
party: PartyState,
templateId: TemplateStringId,
value: ApiRecord,
sender: ActorRef,
): Unit = {
val commandId = commandIdGenerator.generateRandom
val workflowId = workflowIdGenerator.generateRandom
val index = commandIndex.incrementAndGet()
parseTemplateOpaqueIdentifier(templateId).fold({
val msg = s"Create contract command not sent, '$templateId' is not a valid Daml-LF identifier"
log.warning(msg)
sender ! Failure(StoreException(msg))
})(id => {
val command = CreateCommand(commandId, index, workflowId, platformTime, id, value)
submitCommand(party, command, sender)
})
}
private def exerciseChoice(
platformTime: Instant,
party: PartyState,
contractId: ApiTypes.ContractId,
interfaceId: Option[InterfaceStringId],
choice: ApiTypes.Choice,
value: ApiValue,
sender: ActorRef,
): Unit = {
val commandId = commandIdGenerator.generateRandom
val workflowId = workflowIdGenerator.generateRandom
val index = commandIndex.incrementAndGet()
// Note: the ledger API does not need the template ID to submit an exercise command.
// However, Navigator needs it to serialize/deserialize the choice argument, so we look up the template here.
party.ledger
.contract(contractId, party.packageRegistry)
.fold({
val msg = s"Exercise contract command not sent, contract $contractId not found"
log.warning(msg)
sender ! Failure(StoreException(msg))
})(contract => {
val command =
ExerciseCommand(
commandId,
index,
workflowId,
platformTime,
contractId,
contract.template.id,
interfaceId.flatMap(parseInterfaceOpaqueIdentifier),
choice,
value,
)
submitCommand(party, command, sender)
})
}
private def submitCommand(partyState: PartyState, command: Command, sender: ActorRef): Unit = {
// Each party has its own command completion stream.
// Forward the request to the party actor, so that it can be tracked.
context
.child(partyState.actorName)
.foreach(child => child ! PlatformSubscriber.SubmitCommand(command, sender))
}
private val idGeneratorSeed = System.currentTimeMillis()
private val workflowIdGenerator: IdGenerator[ApiTypes.WorkflowIdTag] = new IdGenerator(
idGeneratorSeed
)
private val commandIdGenerator: IdGenerator[ApiTypes.CommandIdTag] = new IdGenerator(
idGeneratorSeed + 1
)
private val commandIndex = new AtomicLong(0)
private def returnToSender[T](sender: ActorRef): PartialFunction[Try[T], Unit] = {
case Success(e) =>
log.debug(s"Sending Success($e) to $sender")
sender ! Success(e)
case Failure(f) =>
log.debug(s"Sending Failure($f) to $sender")
sender ! Failure(f)
}
private def apiFailure[T]: PartialFunction[Throwable, Future[T]] = { case exception: Exception =>
log.error("Unable to perform API operation: {}", exception.getMessage)
Future.failed(StoreException(exception.getMessage))
}
}

View File

@ -1,383 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.store.platform
import org.apache.pekko.NotUsed
import org.apache.pekko.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
import org.apache.pekko.stream._
import org.apache.pekko.stream.scaladsl.{Sink, Source, SourceQueueWithComplete}
import com.daml.ledger.api.v1
import com.daml.ledger.client.LedgerClient
import com.daml.ledger.client.services.commands.CommandSubmission
import com.daml.ledger.client.services.commands.tracker.CompletionResponse
import com.daml.lf.archive.ArchivePayloadParser
import com.daml.lf.data.{Ref => DamlLfRef}
import com.daml.lf.typesig.reader.{Errors, SignatureReader}
import com.daml.navigator.model._
import com.daml.navigator.model.converter.TypeNotFoundError
import com.daml.navigator.store.Store._
import com.daml.util.Ctx
import com.google.rpc.code
import scalaz.Tag
import scalaz.syntax.tag._
import scala.annotation.nowarn
import scala.concurrent.Future
import scala.util.{Failure, Success}
object PlatformSubscriber {
// Actor messages
case object ConnectionReset
case class Started(commandTracker: TrackCommandsSource)
case class SubmitCommand(command: Command, sender: ActorRef)
// Actor state
case class StateRunning(commandTracker: TrackCommandsSource)
type TrackCommandsSource = SourceQueueWithComplete[Ctx[Command, CommandSubmission]]
def props(
ledgerClient: LedgerClient,
party: PartyState,
applicationId: DamlLfRef.LedgerString,
token: Option[String],
) =
Props(classOf[PlatformSubscriber], ledgerClient, party, applicationId, token)
}
/** Actor subscribing to platform event stream of a single DA party. */
@SuppressWarnings(Array("org.wartremover.warts.Any"))
class PlatformSubscriber(
ledgerClient: LedgerClient,
party: PartyState,
applicationId: DamlLfRef.LedgerString,
token: Option[String],
) extends Actor
with ActorLogging
with Stash {
// ----------------------------------------------------------------------------------------------
// Global immutable state - mutable state is stored in parameters of the receive methods
// ----------------------------------------------------------------------------------------------
private val system = context.system
private val killSwitch = KillSwitches.shared("platform-subscriber")
import system.dispatcher
implicit val materializer: Materializer = Materializer(system)
import PlatformSubscriber._
// ----------------------------------------------------------------------------------------------
// Lifecycle
// ----------------------------------------------------------------------------------------------
override def preStart(): Unit = {
log.debug("Starting actor for '{}'", party.name)
val started = for {
_ <- fetchPackages(ledgerClient)
tracker <- startTrackingCommands()
_ <- startStreamingTransactions()
} yield {
Started(tracker)
}
started onComplete {
case Success(value) =>
log.debug("Started actor for '{}'", party.name)
self ! value
case Failure(error) =>
// Failed to start up, giving up
log.error(
"Failed to start actor for party '{}': {}. Please fix any issues and restart this application.",
party.name,
error,
)
context.become(failed(error))
}
}
override def postStop(): Unit = {
log.debug("Stopped actor for '{}'", party.name)
}
// ----------------------------------------------------------------------------------------------
// Messages
// ----------------------------------------------------------------------------------------------
override def receive: Receive = initial
def initial: Receive = {
case Started(tracker) =>
context.become(running(StateRunning(tracker)))
unstashAll()
case GetPartyActorInfo =>
sender() ! PartyActorStarting(party)
case _ =>
stash()
}
def running(state: StateRunning): Receive = {
case ResetConnection =>
log.debug("Resetting connection for '{}'", party.name)
killSwitch.shutdown()
context.stop(self)
sender() ! ConnectionReset
case SubmitCommand(command, commandSender) =>
// Submit command and reply to
submitCommand(ledgerClient, state.commandTracker, party, command, commandSender)
case GetPartyActorInfo =>
sender() ! PartyActorStarted(party)
}
// Permanently failed state
def failed(error: Throwable): Receive = {
case GetApplicationStateInfo =>
sender() ! PartyActorFailed(party, error)
case _ => ()
}
// ----------------------------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------------------------
/** This has two side effects:
* - each converted transaction is added to PartyState.ledger
* - if packages need to be re-fetched, PartyState.packageRegistry is updated
*/
private def processTransaction[Tx](
id: String,
transaction: Tx,
reader: (Tx, converter.LedgerApiV1.Context) => Either[converter.ConversionError, Transaction],
): Future[NotUsed] = {
def go(retryMissingTemplate: Boolean): Future[NotUsed] = {
val ttx =
reader(transaction, converter.LedgerApiV1.Context(party.name, party.packageRegistry))
ttx match {
case Right(tx) =>
party.addLatestTransaction(tx)
Future.successful(NotUsed)
case Left(e: TypeNotFoundError) =>
if (retryMissingTemplate) {
log.info(
"Template '{}' not found while processing transaction '{}', retrying after re-fetching packages.",
e.id,
id,
)
fetchPackages(ledgerClient)
.flatMap(_ => go(false))
} else {
Future.failed(e)
}
case Left(e) =>
Future.failed(e)
}
}
try {
go(true).recoverWith { case e: Throwable =>
log.error(
"Error processing transaction {}: {}. Its effects will not be visible.",
e.getMessage,
id,
)
Future.failed(e)
}
} catch {
case e: Throwable =>
log.error(
"Error processing transaction {}: {}. Its effects will not be visible.",
e.getMessage,
id,
)
Future.failed(e)
}
}
private def startStreamingTransactions(): Future[Unit] = {
val ledgerBegin = v1.ledger_offset.LedgerOffset(
v1.ledger_offset.LedgerOffset.Value
.Boundary(v1.ledger_offset.LedgerOffset.LedgerBoundary.LEDGER_BEGIN)
)
val transactionFilter = v1.transaction_filter.TransactionFilter(
Map(Tag.unwrap(party.name) -> v1.transaction_filter.Filters(None))
)
// Create a source (transactions stream from ledger)
val treeSource: Source[NotUsed, NotUsed] =
ledgerClient.transactionClient
.getTransactionTrees(ledgerBegin, None, transactionFilter, token = token)
.mapAsync(1)(tx =>
processTransaction(tx.transactionId, tx, converter.LedgerApiV1.readTransactionTree)
)
// Run the stream.
// The stream runs independently of the actor. Add a kill switch to terminate the stream when actor stops.
// Note: processTransaction changes the PartyState as a side effect. The stream output can be ignored.
treeSource
.via(killSwitch.flow)
.runWith(Sink.ignore)
// This stream starts immediately
Future.unit
}
@nowarn("msg=method dropNew in object OverflowStrategy is deprecated")
private def startTrackingCommands()
: Future[SourceQueueWithComplete[Ctx[Command, CommandSubmission]]] = {
for {
commandTracker <- ledgerClient.commandClient
.trackCommands[Command](List(Tag.unwrap(party.name)), token)
} yield
// Note: this uses a buffer of 1000 commands awaiting handling by the command client.
// The command client itself can process (config.maxParallelSubmissions) commands in parallel.
// In the highly unlikely case both buffers are full, the command submission will fail immediately.
Source
.queue[Ctx[Command, CommandSubmission]](1000, OverflowStrategy.dropNew)
.via(commandTracker)
.map(result => {
val commandId = result.context.id
result.value match {
case Right(_) =>
// Command completed with success.
// Don't update anything, state will be updated from the transaction stream instead.
// This is because the completion stream does not contain information to correlate command id with transaction id.
log.info("Command '{}' completed successfully.", commandId)
case Left(error) =>
error match {
case notOk: CompletionResponse.NotOkResponse =>
// Command completed with an error
val status = CommandStatusError(
code.Code.fromValue(notOk.grpcStatus.code).toString(),
notOk.grpcStatus.message,
)
party.addCommandStatus(commandId, status)
log.info("Command '{}' completed with status '{}'.", commandId, status)
case CompletionResponse.TimeoutResponse(_) =>
val status = CommandStatusError(code.Code.ABORTED.toString(), "Timeout")
party.addCommandStatus(commandId, status)
log.info("Command '{}' completed with status '{}'.", commandId, status)
case CompletionResponse.NoStatusInResponse(_, _) =>
party.addCommandStatus(commandId, CommandStatusUnknown())
log.error(
"Command tracking failed. Status unknown for command '{}': {}.",
commandId,
error,
)
}
}
})
.to(Sink.ignore)
.run()
}
/*
Note about packages:
- There may be packages uploaded at a later time
- All parties see the same list of packages
- The packages associated to package ids will never change
- The package set should probably only grow, but it's not codified anywhere
Whenever the client sees a template that it does not recognize in the transaction stream,
it can re-fetch the packages from the server to get the metadata it needs to make sense of them.
*/
private def fetchPackages(ledgerClient: LedgerClient): Future[Unit] = {
ledgerClient.packageClient
.listPackages(token)
.flatMap(response => {
Future.traverse(response.packageIds)(id => {
party.packageRegistry.pack(DamlLfRef.PackageId.assertFromString(id)) match {
case Some(_) =>
Future.successful(None)
case None =>
ledgerClient.packageClient.getPackage(id, token).map(Some(_))
}
})
})
.flatMap(responses0 => {
val interfaces = responses0
.collect { case Some(resp) => resp }
.map(decodePackage)
.toList
party.addPackages(interfaces)
log.info(
"Successfully loaded packages {}",
interfaces.map(_.packageId).mkString("[", ", ", "]"),
)
Future.unit
})
.recoverWith(apiFailureF)
}
private def decodePackage(res: v1.package_service.GetPackageResponse) = {
val payload = ArchivePayloadParser.assertFromByteString(res.archivePayload)
val (errors, out) =
SignatureReader.readPackageSignature(DamlLfRef.PackageId.assertFromString(res.hash), payload)
if (!errors.equals(Errors.zeroErrors)) {
log.error("Errors loading package {}: {}", res.hash, errors.toString)
}
out
}
private def submitCommand(
ledgerClient: LedgerClient,
commandTracker: TrackCommandsSource,
party: PartyState,
command: Command,
sender: ActorRef,
): Unit = {
// Convert to ledger API command
converter.LedgerApiV1
.writeCommands(party, command, ledgerClient.ledgerId.unwrap, applicationId)
.fold[Unit](
error => {
// Failed to convert command. Most likely, the argument is incomplete.
sender ! Failure(error)
},
commands => {
import org.apache.pekko.stream.{QueueOfferResult => QOR}
// Store the command
party.addCommand(command)
// Send command to ledger
commandTracker
.offer(Ctx(command, CommandSubmission(commands)))
.andThen {
case Success(QOR.Dropped) =>
party.addCommandStatus(
command.id,
CommandStatusError("INTERNAL", "Command submission failed: buffer full"),
)
case Success(QOR.QueueClosed) =>
party.addCommandStatus(
command.id,
CommandStatusError("INTERNAL", "Command submission failed: queue closed"),
)
case Success(QOR.Failure(e)) =>
party.addCommandStatus(
command.id,
CommandStatusError("INTERNAL", s"Command submission failed: $e"),
)
case Failure(e) =>
party.addCommandStatus(
command.id,
CommandStatusError("INTERNAL", s"Command submission failed: $e"),
)
}
// Immediately return the command ID
sender ! Success(command.id)
},
)
}
private def apiFailureF[T]: PartialFunction[Throwable, Future[T]] = { case exception: Exception =>
log.error("Unable to perform API operation: {}", exception.getMessage)
Future.failed(StoreException(exception.getMessage))
}
}

View File

@ -1,38 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.time
import java.time.Duration
import com.daml.api.util.TimeProvider
import com.daml.ledger.client.services.testing.time.StaticTime
case class TimeProviderWithType(time: TimeProvider, `type`: TimeProviderType)
object TimeProviderFactory {
def apply(
timeProviderType: TimeProviderType,
ledgerTime: Option[StaticTime],
): Option[TimeProviderWithType] =
timeProviderType match {
case TimeProviderType.Auto =>
ledgerTime.fold(
Some(TimeProviderWithType(TimeProvider.UTC, TimeProviderType.WallClock))
)(t => Some(TimeProviderWithType(t, TimeProviderType.Static)))
case TimeProviderType.WallClock =>
Some(TimeProviderWithType(TimeProvider.UTC, TimeProviderType.WallClock))
case TimeProviderType.Static =>
ledgerTime.map(t => TimeProviderWithType(t, TimeProviderType.Static))
case TimeProviderType.Simulated =>
ledgerTime.map(lt => {
val utc: TimeProvider = TimeProvider.UTC
val diff = Duration.between(lt.getCurrentTime, utc.getCurrentTime)
TimeProviderWithType(
TimeProvider.MappedTimeProvider(utc, i => i minus diff),
TimeProviderType.Simulated,
)
})
}
}

View File

@ -1,51 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.time
import scopt.Read
sealed abstract class TimeProviderType extends Product with Serializable {
def name: String
}
object TimeProviderType {
case object Auto extends TimeProviderType {
val name = "auto"
}
case object Static extends TimeProviderType {
val name = "static"
}
case object Simulated extends TimeProviderType {
val name = "simulated"
}
case object WallClock extends TimeProviderType {
val name = "wallclock"
}
def unapply(s: String): Option[TimeProviderType] =
if (s.compareToIgnoreCase(Auto.name) == 0) Some(Auto)
else if (s.compareToIgnoreCase(Static.name) == 0) Some(Static)
else if (s.compareToIgnoreCase(Simulated.name) == 0) Some(Simulated)
else if (s.compareToIgnoreCase(WallClock.name) == 0) Some(WallClock)
else None
def write(tpt: TimeProviderType): String = tpt.name
def acceptedValues: List[String] =
List(Auto, Static, Simulated, WallClock).map(_.name)
implicit val read: Read[TimeProviderType] = new Read[TimeProviderType] {
override def arity: Int = 1
override def reads: String => TimeProviderType = { s =>
unapply(s).getOrElse(
throw new IllegalArgumentException(
s"$s is not a valid time provider. The following values are accepted: ${acceptedValues
.mkString(", ")}."
)
)
}
}
}

View File

@ -1,94 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.util
import java.lang.Math.floor
import org.apache.pekko.actor.Scheduler
import org.apache.pekko.pattern.after
import com.daml.grpc.GrpcException
import com.typesafe.scalalogging.LazyLogging
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal
/** Configuration values for initial service binding retrial
*/
trait IRetryConfig {
/** @return The interval between retries.
*/
def intervalMs: Long
def interval: FiniteDuration = intervalMs.millis
/** @return The total timeout we allow for the operation to succeed.
*/
def timeoutMs: Long
def timeout: FiniteDuration = timeoutMs.millis
}
object RetryHelper extends LazyLogging {
/** Return '''true''' if you want to re-try a statement that caused the specified exception.
*/
type RetryStrategy = PartialFunction[Throwable, Boolean]
/** Always retries if exception is `NonFatal`.
*/
val always: RetryStrategy = { case NonFatal(_) =>
true
}
val failFastOnPermissionDenied: RetryStrategy = {
case GrpcException.PERMISSION_DENIED() => false
case NonFatal(_) => true
}
def retry[T](
retryConfig: Option[(Scheduler, IRetryConfig)]
)(retryStrategy: RetryStrategy)(f: => Future[T])(implicit ec: ExecutionContext): Future[T] = {
retryConfig match {
case None =>
f
case Some(rc) =>
implicit val scheduler: Scheduler = rc._1
retry(Option(rc._2))(retryStrategy)(f)
}
}
def retry[T](retryConfig: Option[IRetryConfig])(
retryStrategy: RetryStrategy
)(f: => Future[T])(implicit ec: ExecutionContext, s: Scheduler): Future[T] = {
retryConfig match {
case None =>
f
case Some(rc) =>
val maxAttempts = floor(rc.timeout / rc.interval).toInt
retry(maxAttempts, rc.interval)(retryStrategy)(f)
}
}
def retry[T](maxAttempts: Int, delay: FiniteDuration)(
retryStrategy: RetryStrategy
)(f: => Future[T])(implicit ec: ExecutionContext, s: Scheduler): Future[T] = {
def shouldRetry(n: Int, e: Throwable): Boolean =
n > 0 && retryStrategy.applyOrElse(e, (_: Throwable) => false)
val remainingAttempts = maxAttempts - 1 // the next line will trigger a future evaluation
f.recoverWith {
case NonFatal(e) if shouldRetry(remainingAttempts, e) =>
logWarning(remainingAttempts, e)
after(delay, s)(retry(remainingAttempts, delay)(retryStrategy)(f))
}
}
private def logWarning(remainingAttempts: Int, e: Throwable): Unit = {
logger.warn(
s"Retrying after failure. Attempts remaining: $remainingAttempts. Error: ${e.getMessage}"
)
}
}

View File

@ -1,11 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -1,236 +0,0 @@
type Choice {
name: String!
parameter: DamlLfType!
consuming: Boolean!
inheritedInterface: String
}
interface Command {
id: ID!
workflowId: ID!
index: Int!
platformTime: Time!
status: CommandStatus!
}
type CommandEdge {
node: Command!
cursor: String!
}
scalar CommandId
type CommandPagination {
beforeCount: Int!
totalCount: Int!
sortings: [Sorting!]
edges: [CommandEdge!]!
}
interface CommandStatus {
completed: Boolean!
}
type CommandStatusWaiting implements CommandStatus
type CommandStatusSuccess implements CommandStatus {
transaction: Transaction!
}
type CommandStatusError implements CommandStatus {
code: String!
details: String!
}
type CommandStatusUnknown implements CommandStatus
type Contract implements Node {
id: ID!
template: Template!
createEvent: CreatedEvent!
archiveEvent: ExercisedEvent
exerciseEvents: [ExercisedEvent!]!
argument: DamlLfValueRecord!
agreementText: String
signatories: [String!]!
observers: [String!]!
key: DamlLfValue
}
type ContractEdge {
node: Contract!
cursor: String!
}
type ContractPagination {
beforeCount: Int!
totalCount: Int!
sortings: [Sorting!]
edges: [ContractEdge!]!
}
type CreateCommand implements Node & Command {
template: Template
templateId: String!
argument: DamlLfValueRecord!
}
type CreatedEvent implements Node & Event {
id: ID!
parent: Event
transaction: Transaction!
witnessParties: [Party!]!
workflowId: String!
contract: Contract!
argument: DamlLfValueRecord!
}
scalar DamlLfDataType
type DamlLfDefDataType implements Node & DamlLfNode {
id: ID!
package: String!
module: String!
name: String!
dataType: DamlLfDataType!
typeVars: [String!]!
dependencies(depth: Int): [DamlLfDefDataType!]!
}
interface DamlLfNode {
id: ID!
package: String!
module: String!
name: String!
}
scalar DamlLfType
scalar DamlLfValue
scalar DamlLfValueRecord
enum Direction {
ASCENDING
DESCENDING
}
interface Event {
id: ID!
parent: Event
transaction: Transaction!
witnessParties: [Party!]!
workflowId: String!
}
type ExerciseCommand implements Node & Command {
contract: Contract
contractId: String!
interfaceId: String
choice: String!
argument: DamlLfValue!
}
type ExercisedEvent implements Node & Event {
id: ID!
parent: Event
transaction: Transaction!
witnessParties: [Party!]!
workflowId: String!
contract: Contract!
choice: String!
argument: DamlLfValue!
actingParties: [Party!]!
consuming: Boolean!
children: [Event!]!
}
input FilterCriterion {
field: String!
value: String!
}
scalar LedgerOffset
type LedgerTime {
id: ID!
time: Time!
type: TimeType!
}
type Mutation {
advanceTime(time: Time!): LedgerTime!
create(templateId: ID!, argument: DamlLfValue): CommandId!
exercise(contractId: ID!, interfaceId: ID, choiceId: ID!, argument: DamlLfValue): CommandId!
}
interface Node {
id: ID!
}
scalar Party
type Query {
parties(search: String): [Party!]!
ledgerTime: LedgerTime!
latestTransaction: Transaction
node(typename: String!, id: ID!): Node
nodes(typename: String!, ids: [ID!]!): [Node!]!
template(topLevelDecl: String!): [Template!]!
commandStatus(id: ID!): CommandStatus
templates(search: String, filter: [FilterCriterion!], count: Int, start: String, sort: [SortCriterion!]): TemplatePagination!
contracts(search: String, filter: [FilterCriterion!], includeArchived: Boolean, count: Int, start: String, sort: [SortCriterion!]): ContractPagination!
commands(search: String, filter: [FilterCriterion!], count: Int, start: String, sort: [SortCriterion!]): CommandPagination!
}
input SortCriterion {
field: String!
direction: Direction!
}
type Sorting {
field: String!
direction: Direction!
}
type Template implements Node & DamlLfNode {
id: ID!
package: String!
module: String!
name: String!
topLevelDecl: String!
parameter: DamlLfType!
parameterDef: DamlLfDefDataType!
choices: [Choice!]!
contracts(search: String, filter: [FilterCriterion!], includeArchived: Boolean, count: Int, start: String, sort: [SortCriterion!]): ContractPagination!
implementedInterfaces: [String!]!
}
type TemplateEdge {
node: Template!
cursor: String!
}
type TemplatePagination {
beforeCount: Int!
totalCount: Int!
sortings: [Sorting!]
edges: [TemplateEdge!]!
}
scalar Time
enum TimeType {
static
wallclock
simulated
}
type Transaction implements Node {
id: ID!
offset: LedgerOffset!
effectiveAt: Time!
commandId: CommandId
events: [Event!]!
}

View File

@ -1,362 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import com.daml.lf.data.{
Decimal => LfDecimal,
FrontStack,
ImmArray,
SortedLookupList,
Ref => DamlLfRef,
}
import com.daml.navigator.model._
import com.daml.lf.{typesig => DamlLfIface}
import com.daml.lf.value.{Value => V}
import com.daml.lf.value.json.ApiValueImplicits._
import scala.language.implicitConversions
case class DamlConstants()
/** Daml related constants usable in tests
*/
case object DamlConstants {
// ------------------------------------------------------------------------------------------------------------------
// Daml-LF: Ids
// ------------------------------------------------------------------------------------------------------------------
val packageId0 = DamlLfRef.PackageId.assertFromString("hash")
def defRef(name: String): DamlLfIdentifier = DamlLfIdentifier(
packageId0,
DamlLfQualifiedName(
DamlLfDottedName.assertFromString("module"),
DamlLfDottedName.assertFromString(name),
),
)
val ref0: DamlLfIdentifier = defRef("T0")
val ref1: DamlLfIdentifier = defRef("T1")
val ref2: DamlLfIdentifier = defRef("T2")
// ------------------------------------------------------------------------------------------------------------------
// Daml-LF: simple types
// ------------------------------------------------------------------------------------------------------------------
val simpleTextT = DamlLfTypePrim(DamlLfPrimType.Text, DamlLfImmArraySeq())
val simpleInt64T = DamlLfTypePrim(DamlLfPrimType.Int64, DamlLfImmArraySeq())
val simpleDecimalT = DamlLfTypeNumeric(LfDecimal.scale)
val simpleUnitT = DamlLfTypePrim(DamlLfPrimType.Unit, DamlLfImmArraySeq())
val simpleDateT = DamlLfTypePrim(DamlLfPrimType.Date, DamlLfImmArraySeq())
val simpleTimestampT = DamlLfTypePrim(DamlLfPrimType.Timestamp, DamlLfImmArraySeq())
val simpleBoolT = DamlLfTypePrim(DamlLfPrimType.Bool, DamlLfImmArraySeq())
val simplePartyT = DamlLfTypePrim(DamlLfPrimType.Party, DamlLfImmArraySeq())
val simpleContractIdT = DamlLfTypePrim(DamlLfPrimType.ContractId, DamlLfImmArraySeq())
def simpleOptionalT(typ: DamlLfIface.Type) =
DamlLfTypePrim(DamlLfPrimType.Optional, DamlLfImmArraySeq(typ))
def simpleListT(typ: DamlLfIface.Type) =
DamlLfTypePrim(DamlLfPrimType.List, DamlLfImmArraySeq(typ))
def simpleTextMapT(typ: DamlLfIface.Type) =
DamlLfTypePrim(DamlLfPrimType.TextMap, DamlLfImmArraySeq(typ))
val simpleTextV = V.ValueText("foo")
val simpleInt64V = V.ValueInt64(100)
val simpleDecimalV = V.ValueNumeric(LfDecimal assertFromString "100")
val simpleUnitV = V.ValueUnit
val simpleDateV = V.ValueDate.fromIso8601("2019-01-28")
val simpleTimestampV = V.ValueTimestamp.fromIso8601("2019-01-28T12:44:33.22Z")
val simpleOptionalV = V.ValueOptional(Some(V.ValueText("foo")))
val simpleTextMapV = V.ValueTextMap(
SortedLookupList(Map("1" -> V.ValueInt64(1), "2" -> V.ValueInt64(2), "3" -> V.ValueInt64(3)))
)
// ------------------------------------------------------------------------------------------------------------------
// Daml-LF: empty record
// ------------------------------------------------------------------------------------------------------------------
val emptyRecordId: DamlLfIdentifier = defRef("EmptyRecord")
val emptyRecordGD = DamlLfRecord(DamlLfImmArraySeq())
val emptyRecordGC = DamlLfDefDataType(DamlLfImmArraySeq(), emptyRecordGD)
val emptyRecordTC = DamlLfTypeCon(DamlLfTypeConName(emptyRecordId), DamlLfImmArraySeq())
val emptyRecordT = emptyRecordTC.instantiate(emptyRecordGC).asInstanceOf[DamlLfRecord]
val emptyRecordV = V.ValueRecord(Some(emptyRecordId), ImmArray.Empty)
// ------------------------------------------------------------------------------------------------------------------
// Daml-LF: simple record (data SimpleRecord a b = {fA: a, fB: b})
// ------------------------------------------------------------------------------------------------------------------
val simpleRecordId: DamlLfIdentifier = defRef("SimpleRecord")
val simpleRecordGD = DamlLfRecord(
DamlLfImmArraySeq(
name("fA") -> DamlLfTypeVar("a"),
name("fB") -> DamlLfTypeVar("b"),
)
)
val simpleRecordGC = DamlLfDefDataType(DamlLfImmArraySeq("a", "b"), simpleRecordGD)
val simpleRecordTC = DamlLfTypeCon(
DamlLfTypeConName(simpleRecordId),
DamlLfImmArraySeq(simpleTextT, simpleInt64T),
)
val simpleRecordT = simpleRecordTC.instantiate(simpleRecordGC).asInstanceOf[DamlLfRecord]
def simpleRecordV =
V.ValueRecord(
Some(simpleRecordId),
ImmArray(
(Some(name("fA")), V.ValueText("foo")),
(Some(name("fB")), V.ValueInt64(100)),
),
)
// ------------------------------------------------------------------------------------------------------------------
// Daml-LF: simple variant (data DamlLfVariant a b = fA a | fB b)
// ------------------------------------------------------------------------------------------------------------------
val simpleVariantId: DamlLfIdentifier = defRef("SimpleVariant")
val simpleVariantGD = DamlLfVariant(
DamlLfImmArraySeq(
name("fA") -> DamlLfTypeVar("a"),
name("fB") -> DamlLfTypeVar("b"),
)
)
val simpleVariantGC = DamlLfDefDataType(DamlLfImmArraySeq("a", "b"), simpleVariantGD)
val simpleVariantTC = DamlLfTypeCon(
DamlLfTypeConName(simpleVariantId),
DamlLfImmArraySeq(simpleTextT, simpleInt64T),
)
val simpleVariantT = simpleVariantTC.instantiate(simpleVariantGC).asInstanceOf[DamlLfVariant]
def simpleVariantV = V.ValueVariant(Some(simpleVariantId), "fA", V.ValueText("foo"))
// ------------------------------------------------------------------------------------------------------------------
// Daml-LF: recursive type (data Tree = Leaf a | Node {left: Tree a, right: Tree a})
// ------------------------------------------------------------------------------------------------------------------
val treeNodeId: DamlLfIdentifier = defRef("TreeNode")
val treeId: DamlLfIdentifier = defRef("Tree")
val treeNodeGD = DamlLfRecord(
DamlLfImmArraySeq(
name("left") -> DamlLfTypeCon(
DamlLfTypeConName(treeId),
DamlLfImmArraySeq(DamlLfTypeVar("a")),
),
name("right") -> DamlLfTypeCon(
DamlLfTypeConName(treeId),
DamlLfImmArraySeq(DamlLfTypeVar("a")),
),
)
)
val treeNodeGC = DamlLfDefDataType(DamlLfImmArraySeq("a"), treeNodeGD)
val treeNodeTC = DamlLfTypeCon(
DamlLfTypeConName(treeNodeId),
DamlLfImmArraySeq(simpleTextT),
)
val treeNodeT = treeNodeTC.instantiate(treeNodeGC).asInstanceOf[DamlLfRecord]
val treeGD = DamlLfVariant(
DamlLfImmArraySeq(
name("Leaf") -> DamlLfTypeVar("a"),
name("Node") -> DamlLfTypeCon(
DamlLfTypeConName(treeNodeId),
DamlLfImmArraySeq(DamlLfTypeVar("a")),
),
)
)
val treeGC = DamlLfDefDataType(DamlLfImmArraySeq("a"), treeGD)
val treeTC = DamlLfTypeCon(
DamlLfTypeConName(treeId),
DamlLfImmArraySeq(simpleTextT),
)
val treeLeftV = V.ValueVariant(
Some(treeId),
"Node",
V.ValueRecord(
Some(treeNodeId),
ImmArray(
Some(name("left")) -> V.ValueVariant(Some(treeId), "Leaf", V.ValueText("LL")),
Some(name("right")) -> V.ValueVariant(Some(treeId), "Leaf", V.ValueText("LR")),
),
),
)
val treeRightV = V.ValueVariant(
Some(treeId),
"Node",
V.ValueRecord(
Some(treeNodeId),
ImmArray(
Some(name("left")) -> V.ValueVariant(Some(treeId), "Leaf", V.ValueText("RL")),
Some(name("right")) -> V.ValueVariant(Some(treeId), "Leaf", V.ValueText("RR")),
),
),
)
val treeV = V.ValueVariant(
Some(treeId),
"Node",
V.ValueRecord(
Some(treeNodeId),
ImmArray(Some(name("left")) -> treeLeftV, Some(name("right")) -> treeRightV),
),
)
val colorGD = DamlLfEnum(DamlLfImmArraySeq(name("Red"), name("Green"), name("Blue")))
val colorGC = DamlLfDefDataType(DamlLfImmArraySeq.empty, colorGD)
val colorId: DamlLfIdentifier = defRef("Color")
val redTC = DamlLfTypeCon(
DamlLfTypeConName(colorId),
DamlLfImmArraySeq.empty,
)
val redV = V.ValueEnum(Some(colorId), "Red")
// ------------------------------------------------------------------------------------------------------------------
// Daml-LF: complex record containing all Daml types
// ------------------------------------------------------------------------------------------------------------------
val complexRecordId: DamlLfIdentifier = defRef("ComplexRecord")
val complexRecordGD = DamlLfRecord(
DamlLfImmArraySeq(
name("fText") -> simpleTextT,
name("fBool") -> simpleBoolT,
name("fDecimal") -> simpleDecimalT,
name("fUnit") -> simpleUnitT,
name("fInt64") -> simpleInt64T,
name("fParty") -> simplePartyT,
name("fContractId") -> simpleContractIdT,
name("fListOfText") -> simpleListT(simpleTextT),
name("fListOfUnit") -> simpleListT(simpleUnitT),
name("fDate") -> simpleDateT,
name("fTimestamp") -> simpleTimestampT,
name("fOptionalText") -> simpleOptionalT(simpleTextT),
name("fOptionalUnit") -> simpleOptionalT(simpleUnitT),
name("fOptOptText") -> simpleOptionalT(simpleOptionalT(simpleTextT)),
name("fMap") -> simpleTextMapT(simpleInt64T),
name("fVariant") -> simpleVariantTC,
name("fRecord") -> simpleRecordTC,
)
)
val complexRecordGC = DamlLfDefDataType(DamlLfImmArraySeq(), complexRecordGD)
val complexRecordTC = DamlLfTypeCon(DamlLfTypeConName(complexRecordId), DamlLfImmArraySeq())
val complexRecordT = complexRecordTC.instantiate(complexRecordGC).asInstanceOf[DamlLfRecord]
val complexRecordV = V.ValueRecord(
Some(complexRecordId),
ImmArray(
("fText", simpleTextV),
("fBool", V.ValueTrue),
("fDecimal", simpleDecimalV),
("fUnit", V.ValueUnit),
("fInt64", simpleInt64V),
("fParty", V.ValueParty(DamlLfRef.Party assertFromString "BANK1")),
("fContractId", V.ValueContractId(V.ContractId.assertFromString("00" + "00" * 32 + "c0"))),
("fListOfText", V.ValueList(FrontStack(V.ValueText("foo"), V.ValueText("bar")))),
("fListOfUnit", V.ValueList(FrontStack(V.ValueUnit, V.ValueUnit))),
("fDate", simpleDateV),
("fTimestamp", simpleTimestampV),
("fOptionalText", V.ValueNone),
("fOptionalUnit", V.ValueOptional(Some(V.ValueUnit))),
("fOptOptText", V.ValueOptional(Some(V.ValueOptional(Some(V.ValueText("foo")))))),
(
"fMap",
V.ValueTextMap(
SortedLookupList(
Map("1" -> V.ValueInt64(1), "2" -> V.ValueInt64(2), "3" -> V.ValueInt64(3))
)
),
),
("fVariant", simpleVariantV),
("fRecord", simpleRecordV),
).map { case (k, v) => (Some(name(k)), v) },
)
// ------------------------------------------------------------------------------------------------------------------
// Daml-LF: package mockup
// ------------------------------------------------------------------------------------------------------------------
val allTypes: Map[DamlLfIdentifier, DamlLfDefDataType] = Map(
emptyRecordId -> emptyRecordGC,
simpleRecordId -> simpleRecordGC,
simpleVariantId -> simpleVariantGC,
complexRecordId -> complexRecordGC,
treeId -> treeGC,
treeNodeId -> treeNodeGC,
colorId -> colorGC,
)
// Note: these templates may not be valid Daml templates
val simpleRecordTemplateId: DamlLfIdentifier = defRef("SimpleRecordTemplate")
private val ChoiceUnit = DamlLfRef.Name.assertFromString("unit")
private val choiceText = DamlLfRef.Name.assertFromString("text")
private val choiceNonconsuming = DamlLfRef.Name.assertFromString("nonconsuming")
private val ChoiceReplace = DamlLfRef.Name.assertFromString("replace")
val simpleRecordTemplate = DamlLfIface.PackageSignature.TypeDecl.Template(
simpleRecordT,
DamlLfIface.DefTemplate(
DamlLfIface.TemplateChoices.Resolved fromDirect Map(
ChoiceUnit -> DamlLfIface.TemplateChoice(simpleUnitT, false, simpleUnitT),
choiceText -> DamlLfIface.TemplateChoice(simpleTextT, false, simpleUnitT),
choiceNonconsuming -> DamlLfIface.TemplateChoice(simpleUnitT, true, simpleUnitT),
ChoiceReplace -> DamlLfIface.TemplateChoice(simpleRecordTC, false, simpleUnitT),
),
None,
Seq.empty,
),
)
val complexRecordTemplate = DamlLfIface.PackageSignature.TypeDecl.Template(
complexRecordT,
DamlLfIface.DefTemplate(
DamlLfIface.TemplateChoices.Resolved fromDirect Map(
ChoiceUnit -> DamlLfIface.TemplateChoice(simpleUnitT, false, simpleUnitT),
choiceText -> DamlLfIface.TemplateChoice(simpleTextT, false, simpleUnitT),
choiceNonconsuming -> DamlLfIface.TemplateChoice(simpleUnitT, true, simpleUnitT),
ChoiceReplace -> DamlLfIface.TemplateChoice(complexRecordTC, false, simpleUnitT),
),
None,
Seq.empty,
),
)
val treeNodeTemplate = DamlLfIface.PackageSignature.TypeDecl.Template(
treeNodeT,
DamlLfIface.DefTemplate(
DamlLfIface.TemplateChoices.Resolved fromDirect Map(
ChoiceUnit -> DamlLfIface.TemplateChoice(simpleUnitT, false, simpleUnitT),
choiceText -> DamlLfIface.TemplateChoice(simpleTextT, false, simpleUnitT),
choiceNonconsuming -> DamlLfIface.TemplateChoice(simpleUnitT, true, simpleUnitT),
ChoiceReplace -> DamlLfIface.TemplateChoice(treeNodeTC, false, simpleUnitT),
),
None,
Seq.empty,
),
)
def complexGenMapT(keyTyp: DamlLfIface.Type, valueTyp: DamlLfIface.Type) =
DamlLfTypePrim(DamlLfPrimType.GenMap, DamlLfImmArraySeq(keyTyp, valueTyp))
val complexGenMapV = V.ValueGenMap(
ImmArray(
treeV -> V.ValueInt64(1),
treeLeftV -> V.ValueInt64(2),
treeRightV -> V.ValueInt64(3),
)
)
val iface = DamlLfIface.PackageSignature(
packageId0,
None,
Map(
emptyRecordId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl.Normal(emptyRecordGC),
simpleRecordId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl.Normal(simpleRecordGC),
simpleVariantId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl
.Normal(simpleVariantGC),
treeId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl.Normal(treeGC),
treeNodeId.qualifiedName -> DamlLfIface.PackageSignature.TypeDecl.Normal(treeNodeGC),
simpleRecordTemplateId.qualifiedName -> simpleRecordTemplate,
complexRecordId.qualifiedName -> complexRecordTemplate,
treeNodeId.qualifiedName -> treeNodeTemplate,
),
Map.empty,
)
private[navigator] implicit def name(s: String): DamlLfRef.Name =
DamlLfRef.Name.assertFromString(s)
@throws[IllegalArgumentException]
private[navigator] def record(fields: (String, ApiValue)*): ApiRecord =
V.ValueRecord(
None,
fields.iterator.map { case (label, value) => Some(name(label)) -> value }.to(ImmArray),
)
}

View File

@ -1,221 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.model.{HttpRequest, StatusCodes, Uri}
import org.apache.pekko.http.scaladsl.server.Route
import org.apache.pekko.http.scaladsl.settings.ServerSettings
import org.apache.pekko.util.ByteString
import com.daml.buildinfo.BuildInfo
import com.daml.integrationtest.CantonFixture
import com.daml.ledger.api.domain
import com.daml.ledger.client.LedgerClient
import com.daml.lf.data.Ref
import com.daml.lf.data.Ref.UserId
import com.daml.navigator.config.{Arguments, Config}
import org.scalatest._
import org.scalatest.freespec.AsyncFreeSpec
import org.scalatest.matchers.should.Matchers
import com.daml.timer.RetryStrategy
import com.google.protobuf.field_mask.FieldMask
import java.util.UUID
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
class IntegrationTest extends AsyncFreeSpec with CantonFixture with Matchers {
self: Suite =>
private def withNavigator[A](
userMgmt: Boolean
)(testFn: Uri => LedgerClient => Future[A]): Future[A] = {
import scala.jdk.FutureConverters
val args = Arguments(
port = 0,
participantPort = ports.head.value,
enableUserManagement = userMgmt,
)
val sys = ActorSystem(s"navigator-${UUID.randomUUID().toString}")
val backend = new UIBackend {
override def customEndpoints: Set[CustomEndpoint[_]] = Set()
override def customRoutes: List[Route] = Nil
override def applicationInfo: ApplicationInfo = ApplicationInfo(
id = s"Test-Navigator-${UUID.randomUUID().toString}",
name = "Test-Navigator",
version = BuildInfo.Version,
)
}
val (graphQL, info, _, getAppState, partyRefresh) = backend.setup(args, Config())(sys)
val bindingF = Http()
.newServerAt("localhost", 0)
.withSettings(ServerSettings(system).withTransparentHeadRequests(true))
.bind(backend.getRoute(system, args, graphQL, info, getAppState))
// TODO https://github.com/digital-asset/daml/issues/12663 participant user management: Emulating no-pagination
def listAllUsers(client: LedgerClient) =
client.userManagementClient.listUsers(pageToken = "", pageSize = 10000).map {
case (users, _) => users.toList
}
// Don't close the LedgerClient on termination, because all it does is close the channel,
// which then causes a subsequent test to fail when creating a LedgerClient using a closed channel
// ("io.grpc.StatusRuntimeException: UNAVAILABLE: Channel shutdown invoked")
sys.registerOnTermination {
partyRefresh.foreach(_.cancel())
Await.ready(bindingF.flatMap(_.terminate(30.seconds)), 30.seconds)
}
import FutureConverters._
for {
binding <- bindingF
client <- defaultLedgerClient()
uri = Uri.from(
scheme = "http",
host = binding.localAddress.getHostName,
port = binding.localAddress.getPort,
)
a <- testFn(uri)(client)
_ <- sys.terminate()
_ <- Await.ready(sys.getWhenTerminated.asScala, 30.seconds)
_ = logger.info(s"Terminated actor system ${sys.name}")
// Reset sandbox enough to avoid users leaking between tests: delete all users except admin
users <- listAllUsers(client)
_ <- Future.traverse(users)(user =>
if (user.id != "participant_admin") client.userManagementClient.deleteUser(user.id)
else Future.unit
)
_ = logger.info("Removed all users from ledger as part of cleanup.")
} yield a
}
private def okSessionBody(expectedBody: String, useRegex: Boolean = false)(implicit
uri: Uri
): Future[Assertion] = {
RetryStrategy.constant(20, 1.second) { case (_, _) =>
for {
resp <- Http().singleRequest(
HttpRequest(uri = uri.withPath(Uri.Path("/api/session/")))
)
respBody <- resp.entity.dataBytes
.runFold(ByteString.empty)((b, a) => b ++ a)
.map(_.utf8String)
_ = resp.status shouldBe StatusCodes.OK
} yield
if (useRegex)
(respBody should fullyMatch regex expectedBody.r)
else
(respBody shouldBe expectedBody)
}
}
private def allocateParty(partyName: String)(implicit client: LedgerClient) = {
client.partyManagementClient
.allocateParty(hint = None, displayName = Some(partyName))
}
private def createUser(userName: String, primaryParty: Ref.Party)(implicit
client: LedgerClient
): Future[domain.User] = {
client.userManagementClient
.createUser(
domain.User(UserId.assertFromString(userName), Some(primaryParty))
)
}
private def updateUser(userName: String, primaryParty: Ref.Party)(implicit
client: LedgerClient
): Future[domain.User] = {
client.userManagementClient
.updateUser(
domain.User(UserId.assertFromString(userName), Some(primaryParty)),
Some(FieldMask(Seq("primary_party"))),
)
}
private def createUser(userName: String)(implicit client: LedgerClient): Future[domain.User] = {
client.userManagementClient
.createUser(
domain.User(UserId.assertFromString(userName), None)
)
}
"Navigator (parties)" - {
"picks up newly allocated parties" in withNavigator(userMgmt = false) {
implicit uri => implicit client =>
for {
_ <- okSessionBody(
"""\{"method":\{"type":"select","users":\["participant0::[a-f0-9]+"\]\},"type":"sign-in"\}""",
true,
)
_ <- allocateParty("display-name")
_ <- okSessionBody(
"""\{"method":\{"type":"select","users":\["participant0::[a-f0-9]+","display-name"\]\},"type":"sign-in"\}""",
true,
)
} yield succeed
}
}
"Navigator (basic user)" - {
"picks up newly created users (1 user)" in withNavigator(userMgmt = true) {
implicit uri => implicit client =>
for {
_ <- okSessionBody("""{"method":{"type":"select","users":[]},"type":"sign-in"}""")
partyDetails <- allocateParty("primary-party")
_ <- createUser("user-name", partyDetails.party)
_ <- okSessionBody(
"""{"method":{"type":"select","users":["user-name"]},"type":"sign-in"}"""
)
} yield succeed
}
}
"Navigator (users)" - {
"picks up newly created users (2 users, 1 primary party)" in withNavigator(userMgmt = true) {
implicit uri => implicit client =>
for {
_ <- okSessionBody("""{"method":{"type":"select","users":[]},"type":"sign-in"}""")
partyDetails <- allocateParty("primary-party")
_ <- createUser("user-name-1", partyDetails.party)
_ <- createUser("user-name-2", partyDetails.party)
_ <- okSessionBody(
"""{"method":{"type":"select","users":["user-name-1","user-name-2"]},"type":"sign-in"}"""
)
} yield succeed
}
"picks up newly created users (2 users, 2 primary parties)" in withNavigator(userMgmt = true) {
implicit uri => implicit client =>
for {
_ <- okSessionBody("""{"method":{"type":"select","users":[]},"type":"sign-in"}""")
partyDetails <- allocateParty("primary-party")
_ <- createUser("user-name-1", partyDetails.party)
partyDetails2 <- allocateParty("primary-party2")
_ <- createUser("user-name-2", partyDetails2.party)
_ <- okSessionBody(
"""{"method":{"type":"select","users":["user-name-1","user-name-2"]},"type":"sign-in"}"""
)
_ <- updateUser("user-name-2", partyDetails.party)
_ <- okSessionBody(
"""{"method":{"type":"select","users":["user-name-1","user-name-2"]},"type":"sign-in"}"""
)
} yield succeed
}
"can only log in if user has a primary party (1 user, 0 primary parties)" in withNavigator(
userMgmt = true
) { implicit uri => implicit client =>
for {
_ <- okSessionBody("""{"method":{"type":"select","users":[]},"type":"sign-in"}""")
_ <- createUser("user-name-1")
_ <- okSessionBody(
"""{"method":{"type":"select","users":[]},"type":"sign-in"}"""
)
} yield succeed
}
}
}

View File

@ -1,197 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import org.apache.pekko.http.scaladsl.model.headers.{Cookie, `Set-Cookie`}
import org.apache.pekko.http.scaladsl.server.Route
import org.apache.pekko.http.scaladsl.testkit.ScalatestRouteTest
import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal
import com.daml.api.util.TimeProvider.UTC
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.SessionJsonProtocol._
import com.daml.navigator.config.Arguments
import com.daml.navigator.model.PartyState
import com.daml.navigator.store.Store.{
ApplicationStateConnected,
ApplicationStateConnecting,
ApplicationStateFailed,
ApplicationStateInfo,
PartyActorRunning,
PartyActorStarted,
}
import com.daml.navigator.time.TimeProviderType.Static
import com.daml.navigator.time.TimeProviderWithType
import com.typesafe.scalalogging.LazyLogging
import org.scalatest.OptionValues
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import scalaz.syntax.tag._
import spray.json._
import scala.concurrent.Future
import scala.util.Success
class ServerTest
extends AnyFlatSpec
with Matchers
with ScalatestRouteTest
with LazyLogging
with OptionValues {
val userId = "userId"
val role = "role"
val party = ApiTypes.Party("party")
val partyState = new PartyState(party, Some(role), useDatabase = false)
val user = User(userId, partyState, Some(role), true)
val userJson = JsObject(
"id" -> JsString(userId),
"role" -> JsString(role),
"party" -> JsString(party.unwrap),
"canAdvanceTime" -> JsBoolean(true),
)
val sessionJson = JsObject(
"type" -> JsString("session"),
"user" -> userJson,
)
case object TestInfoHandler extends InfoHandler {
override def getInfo: Future[JsValue] = Future.successful(JsString("test"))
}
private def route(state: ApplicationStateInfo): Route =
NavigatorBackend.getRoute(
system = ActorSystem("da-ui-backend-test"),
arguments = Arguments.default,
graphQL = DefaultGraphQLHandler(Set.empty, None),
info = TestInfoHandler,
getAppState = () => Future.successful(state),
)
private[this] val connected =
route(
ApplicationStateConnected(
"localhost",
6865,
true,
"n/a",
"0",
TimeProviderWithType(UTC, Static),
Map(userId -> PartyActorRunning(PartyActorStarted(partyState))),
)
)
private[this] val unauthorized =
route(
ApplicationStateFailed(
"localhost",
6865,
true,
"n/a",
io.grpc.Status.PERMISSION_DENIED.asException,
)
)
private[this] val failed =
route(
ApplicationStateFailed(
"localhost",
6865,
true,
"n/a",
io.grpc.Status.INVALID_ARGUMENT.asException,
)
)
private[this] val connecting =
route(
ApplicationStateConnecting(
"localhost",
6865,
true,
"n/a",
)
)
def sessionCookie(): String = {
val cookies = headers.collect { case `Set-Cookie`(x) if x.name == "session-id" => x }
cookies should have size 1
cookies.head.value
}
def withCleanSessions[T](f: => T): T = {
Session.clean()
f
}
"SelectMode GET /api/session/" should "respond SignIn with method SignInSelect with the available users" in withCleanSessions {
Get("/api/session/") ~> connected ~> check {
responseAs[SignIn] shouldEqual SignIn(method = SignInSelect(userIds = Set(userId)))
}
}
it should "respond with the Session when already signed-in" in withCleanSessions {
val sessionId = "session-id-value"
Session.open(sessionId, userId, Some(role), user.party)
Get("/api/session/") ~> Cookie("session-id" -> sessionId) ~> connected ~> check {
Unmarshal(response.entity).to[String].value.map(_.map(_.parseJson)) shouldEqual Some(
Success((sessionJson))
)
}
}
"SelectMode POST /api/session/" should "allow to SignIn with an existing user" in withCleanSessions {
Post("/api/session/", LoginRequest(userId)) ~> connected ~> check {
Unmarshal(response.entity).to[String].value.map(_.map(_.parseJson)) shouldEqual Some(
Success((sessionJson))
)
val sessionId = sessionCookie()
Session.current(sessionId).value shouldEqual Session(user)
}
}
it should "forbid to SignIn with a non existing user" in withCleanSessions {
Post("/api/session/", LoginRequest(userId + " ")) ~> connected ~> check {
responseAs[SignIn] shouldEqual SignIn(
method = SignInSelect(userIds = Set(userId)),
Some(InvalidCredentials),
)
}
}
it should "forbid to SignIn with when unauthorized and report the error" in withCleanSessions {
Post("/api/session/", LoginRequest(userId)) ~> unauthorized ~> check {
responseAs[SignIn] shouldEqual SignIn(
method = SignInSelect(userIds = Set()),
Some(InvalidCredentials),
)
}
}
it should "forbid to SignIn with when it's impossible to connect to the ledger" in withCleanSessions {
Post("/api/session/", LoginRequest(userId)) ~> failed ~> check {
responseAs[SignIn] shouldEqual SignIn(method = SignInSelect(userIds = Set()), Some(Unknown))
}
}
it should "forbid to SignIn with when still connecting to a ledger" in withCleanSessions {
Post("/api/session/", LoginRequest(userId)) ~> connecting ~> check {
responseAs[SignIn] shouldEqual SignIn(
method = SignInSelect(userIds = Set()),
Some(NotConnected),
)
}
}
"SelectMode DELETE /api/session/" should "delete a given Session when signed-in" in withCleanSessions {
val sessionId = "session-id-value-2"
Session.open(sessionId, userId, Some(role), user.party)
Delete("/api/session/") ~> Cookie("session-id", sessionId) ~> connected ~> check {
Session.current(sessionId) shouldBe None
}
}
}

View File

@ -1,46 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator
import com.daml.navigator.model.PartyState
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import SessionJsonProtocol.userWriter
import com.daml.ledger.api.refinements.ApiTypes
import spray.json.{JsBoolean, JsObject, JsString}
class SessionJsonProtocolTest extends AnyFlatSpec with Matchers {
val userClassName = User.getClass.getSimpleName
val party = ApiTypes.Party("party")
behavior of s"JsonCodec[$userClassName]"
it should s"encode $userClassName without role" in {
val user =
User(id = "id", party = new PartyState(party, None, false), canAdvanceTime = true)
val userJson = JsObject(
"id" -> JsString("id"),
"party" -> JsString("party"),
"canAdvanceTime" -> JsBoolean(true),
)
userWriter.write(user) shouldEqual userJson
}
it should s"encode $userClassName with role" in {
val user = User(
id = "id",
party = new PartyState(party, Some("role"), false),
role = Some("role"),
canAdvanceTime = false,
)
val userJson = JsObject(
"id" -> JsString("id"),
"role" -> JsString("role"),
"party" -> JsString("party"),
"canAdvanceTime" -> JsBoolean(false),
)
userWriter.write(user) shouldEqual userJson
}
}

View File

@ -1,52 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.config
import com.daml.ledger.api.refinements.ApiTypes.Party
import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Path, Paths}
import org.scalatest.matchers.should.Matchers
import org.scalatest.freespec.AnyFreeSpec
class RowSpec extends AnyFreeSpec with Matchers {
private val defaultConfigText = "users { Alice { party = alice }, Bob { party = bob } }"
private val defaultConfig = Config(
Map(
"Alice" -> UserConfig(Party("alice"), None, false),
"Bob" -> UserConfig(Party("bob"), None, false),
)
)
private def withConfig[A](f: Path => A): A = {
val path = Files.createTempFile("navigator", ".conf")
try {
Files.write(path, defaultConfigText.getBytes(StandardCharsets.UTF_8))
f(path)
} finally {
Files.delete(path)
}
}
"Config" - {
"load" - {
"loads explicit config" in {
withConfig { conf =>
Config.load(ExplicitConfig(conf), false) shouldBe Right(defaultConfig)
}
}
"fails if explicit config does not exist" in {
Config.load(ExplicitConfig(Paths.get("nonexistentgarbage")), false) shouldBe Left(
ConfigNotFound("File nonexistentgarbage not found")
)
}
"loads default config if none is specified" in {
withConfig { conf =>
Config.load(DefaultConfig(conf), false) shouldBe Right(defaultConfig)
}
}
"loads an empty config if default config does not exist" in {
Config.load(DefaultConfig(Paths.get("nonexistentgarbage")), false) shouldBe Right(Config())
}
}
}
}

View File

@ -1,166 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.data
import java.time.Instant
import com.daml.ledger.api.refinements.ApiTypes
import com.daml.navigator.model.{
ChoiceExercised,
CommandStatusError,
CommandStatusSuccess,
CommandStatusUnknown,
CommandStatusWaiting,
ContractCreated,
CreateCommand,
ExerciseCommand,
PackageRegistry,
Transaction,
}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scala.util.Success
class RowSpec extends AnyWordSpec with Matchers {
import com.daml.navigator.{DamlConstants => C}
private val registry: PackageRegistry = PackageRegistry().withPackages(List(C.iface))
"CommandRow" when {
"converting CreateCommand" should {
val value = CreateCommand(
ApiTypes.CommandId("c01"),
1L,
ApiTypes.WorkflowId("w01"),
Instant.EPOCH,
C.complexRecordId,
C.complexRecordV,
)
"not change the value" in {
CommandRow.fromCommand(value).toCommand(registry) shouldBe Success(value)
}
}
"converting ExerciseCommand" should {
val value = ExerciseCommand(
ApiTypes.CommandId("c01"),
1L,
ApiTypes.WorkflowId("w01"),
Instant.EPOCH,
ApiTypes.ContractId("#0:0"),
C.complexRecordId,
Some(C.complexRecordId),
ApiTypes.Choice("text"),
C.simpleTextV,
)
"not change the value" in {
CommandRow.fromCommand(value).toCommand(registry) shouldBe Success(value)
}
}
}
"CommandStatusRow" when {
"converting CommandStatusWaiting" should {
val id = ApiTypes.CommandId("c01")
val value = CommandStatusWaiting()
"not change the value" in {
CommandStatusRow
.fromCommandStatus(id, value)
.toCommandStatus(_ => Success(None)) shouldBe Success(value)
}
}
"converting CommandStatusError" should {
val id = ApiTypes.CommandId("c01")
val value = CommandStatusError("code", "message")
"not change the value" in {
CommandStatusRow
.fromCommandStatus(id, value)
.toCommandStatus(_ => Success(None)) shouldBe Success(value)
}
}
"converting CommandStatusSuccess" should {
val id = ApiTypes.CommandId("c01")
val tx = Transaction(
ApiTypes.TransactionId("t01"),
Some(ApiTypes.CommandId("c01")),
Instant.EPOCH,
"1",
List.empty,
)
val value = CommandStatusSuccess(tx)
"not change the value" in {
CommandStatusRow
.fromCommandStatus(id, value)
.toCommandStatus(_ => Success(Some(tx))) shouldBe Success(value)
}
}
"converting CommandStatusUnknown" should {
val id = ApiTypes.CommandId("c01")
val value = CommandStatusUnknown()
"not change the value" in {
CommandStatusRow
.fromCommandStatus(id, value)
.toCommandStatus(_ => Success(None)) shouldBe Success(value)
}
}
}
"EventRow" when {
val alice = ApiTypes.Party("Alice")
val bob = ApiTypes.Party("Bob")
val charlie = ApiTypes.Party("Charlie")
"converting ContractCreated" should {
val value = ContractCreated(
ApiTypes.EventId("e01"),
Some(ApiTypes.EventId("e00")),
ApiTypes.TransactionId("t01"),
List(ApiTypes.Party("p01")),
ApiTypes.WorkflowId("w01"),
ApiTypes.ContractId("c01"),
C.complexRecordId,
C.complexRecordV,
Some("agreement"),
List(alice),
List(bob, charlie),
None,
)
"not change the value" in {
EventRow.fromEvent(value).toEvent(registry) shouldBe Success(value)
}
}
"converting ChoiceExercised" should {
val value = ChoiceExercised(
ApiTypes.EventId("e01"),
Some(ApiTypes.EventId("e00")),
ApiTypes.TransactionId("t01"),
List(ApiTypes.Party("p01")),
ApiTypes.WorkflowId("w01"),
ApiTypes.ContractId("c01"),
C.complexRecordId,
ApiTypes.Choice("text"),
C.simpleTextV,
List(ApiTypes.Party("p01")),
true,
)
"not change the value" in {
EventRow.fromEvent(value).toEvent(registry) shouldBe Success(value)
}
}
}
}

View File

@ -1,45 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.dotnot
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class DotNotSpec extends AnyFlatSpec with Matchers {
behavior of "DotNot"
it should "register an action on leaf values" in {
val tree =
root[String, Int, Unit]("foobar")
.onLeaf("foo")
.onAnyValue
.const(1)
tree.onTree.nameMatcherToActions should have size 1
tree.valueMatcherToActions should have size 1
tree.run("", PropertyCursor.fromString("foo"), "", ()) shouldEqual Right(1)
}
it should "register multiple actions on leaf values" in {
val tree =
root[String, Int, Unit]("foobar")
.onLeaf("foo")
.onValue("*")
.const(1)
.onValue("2")
.const(2)
.onAnyValue
.const(3)
val cursor = PropertyCursor.fromString("foo")
tree.onTree.nameMatcherToActions should have size 1
tree.valueMatcherToActions should have size 3
tree.run("", cursor, "*", ()) shouldEqual Right(1)
tree.run("", cursor, "2", ()) shouldEqual Right(2)
tree.run("", cursor, "", ()) shouldEqual Right(3)
val wrongCursor = PropertyCursor.fromString("bar")
tree.run("", wrongCursor, "", ()) shouldBe a[Left[_, _]]
}
}

View File

@ -1,33 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.graphql
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import sangria.parser.QueryParser
import sangria.schema.SchemaChange.DescriptionChange
import sangria.schema.Schema
import scala.io.Source
class GraphQLSchemaSpec extends AnyWordSpec with Matchers {
"The rendered schema" should {
"match the expected schema definition" in {
val idl =
Source.fromInputStream(getClass.getResourceAsStream("/schema.graphql"), "UTF-8").mkString
val schema = Schema.buildFromAst(QueryParser.parse(idl).get)
// Compare schemata but ignore description changes.
val changes = schema
.compare(new GraphQLSchema(Set()).QuerySchema)
.filter(!_.isInstanceOf[DescriptionChange])
if (changes.nonEmpty) {
fail(
s"Schema definition does not match:\n- ${changes.map(_.description).mkString("\n- ")}\n"
)
}
}
}
}

View File

@ -1,51 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.json
import com.daml.navigator.model
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scala.util.{Success, Try}
class ApiCodecCompressedSpec extends AnyWordSpec with Matchers {
import com.daml.navigator.{DamlConstants => C}
/** Serializes the API value to JSON, then parses it back to an API value */
private def serializeAndParse(
value: model.ApiValue,
typ: model.DamlLfType,
): Try[model.ApiValue] = {
import com.daml.lf.value.json.ApiCodecCompressed
import ApiCodecCompressed.JsonImplicits._
import spray.json._
for {
serialized <- Try(value.toJson.prettyPrint)
json <- Try(serialized.parseJson)
parsed <- Try(ApiCodecCompressed.jsValueToApiValue(json, typ, C.allTypes.get _))
} yield parsed
}
"API verbose JSON codec" when {
"serializing and parsing a value" should {
"work for SimpleRecord" in {
serializeAndParse(C.simpleRecordV, C.simpleRecordTC) shouldBe Success(C.simpleRecordV)
}
"work for SimpleVariant" in {
serializeAndParse(C.simpleVariantV, C.simpleVariantTC) shouldBe Success(C.simpleVariantV)
}
"work for ComplexRecord" in {
serializeAndParse(C.complexRecordV, C.complexRecordTC) shouldBe Success(C.complexRecordV)
}
"work for Tree" in {
serializeAndParse(C.treeV, C.treeTC) shouldBe Success(C.treeV)
}
"work for Enum" in {
serializeAndParse(C.redV, C.redTC) shouldBe Success(C.redV)
}
}
}
}

View File

@ -1,75 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.json
import com.daml.navigator.model
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scala.util.{Success, Try}
class ApiCodecVerboseSpec extends AnyWordSpec with Matchers {
import com.daml.navigator.{DamlConstants => C}
/** Serializes the API value to JSON, then parses it back to an API value */
private def serializeAndParse(value: model.ApiValue): Try[model.ApiValue] = {
import com.daml.navigator.json.ApiCodecVerbose.JsonImplicits._
import spray.json._
for {
serialized <- Try(value.toJson.prettyPrint)
json <- Try(serialized.parseJson)
parsed <- Try(json.convertTo[model.ApiValue])
} yield parsed
}
"API verbose JSON codec" when {
"serializing and parsing a value" should {
"work for Text" in {
serializeAndParse(C.simpleTextV) shouldBe Success(C.simpleTextV)
}
"work for Int64" in {
serializeAndParse(C.simpleInt64V) shouldBe Success(C.simpleInt64V)
}
"work for Decimal" in {
serializeAndParse(C.simpleDecimalV) shouldBe Success(C.simpleDecimalV)
}
"work for Unit" in {
serializeAndParse(C.simpleUnitV) shouldBe Success(C.simpleUnitV)
}
"work for Date" in {
serializeAndParse(C.simpleDateV) shouldBe Success(C.simpleDateV)
}
"work for Timestamp" in {
serializeAndParse(C.simpleTimestampV) shouldBe Success(C.simpleTimestampV)
}
"work for Optional" in {
serializeAndParse(C.simpleOptionalV) shouldBe Success(C.simpleOptionalV)
}
"work for EmptyRecord" in {
serializeAndParse(C.emptyRecordV) shouldBe Success(C.emptyRecordV)
}
"work for SimpleRecord" in {
serializeAndParse(C.simpleRecordV) shouldBe Success(C.simpleRecordV)
}
"work for SimpleVariant" in {
serializeAndParse(C.simpleVariantV) shouldBe Success(C.simpleVariantV)
}
"work for ComplexRecord" in {
serializeAndParse(C.complexRecordV) shouldBe Success(C.complexRecordV)
}
"work for Tree" in {
serializeAndParse(C.treeV) shouldBe Success(C.treeV)
}
"work for TextMap" in {
serializeAndParse(C.simpleTextMapV) shouldBe Success(C.simpleTextMapV)
}
"work for GenMap" in {
serializeAndParse(C.complexGenMapV) shouldBe Success(C.complexGenMapV)
}
}
}
}

View File

@ -1,58 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.json
import com.daml.navigator.json.DamlLfCodec.JsonImplicits._
import com.daml.navigator.model
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scala.util.{Success, Try}
class DamlLfCodecSpec extends AnyWordSpec with Matchers {
import com.daml.navigator.{DamlConstants => C}
/** Serializes the API value to JSON, then parses it back to an API value */
private def serializeAndParse[T](value: T)(implicit fmt: spray.json.JsonFormat[T]): Try[T] = {
import spray.json._
for {
serialized <- Try(value.toJson.prettyPrint)
json <- Try(serialized.parseJson)
parsed <- Try(json.convertTo[T])
} yield parsed
}
"Daml-LF JSON codec" when {
"serializing and parsing a Daml-LF object" should {
"work for DamlLFIdentifier" in {
serializeAndParse(C.ref0) shouldBe Success(C.ref0)
}
"work for DamlLfTypePrim(Text)" in {
serializeAndParse[model.DamlLfType](C.simpleTextT) shouldBe Success(C.simpleTextT)
}
"work for DamlLfTypeCon(SimpleRecord)" in {
serializeAndParse[model.DamlLfType](C.simpleRecordTC) shouldBe Success(C.simpleRecordTC)
}
"work for DamlLfTypeCon(Tree)" in {
serializeAndParse[model.DamlLfType](C.treeTC) shouldBe Success(C.treeTC)
}
"work for DamlLfDefDataType(SimpleRecord)" in {
serializeAndParse[model.DamlLfDefDataType](C.simpleRecordGC) shouldBe Success(
C.simpleRecordGC
)
}
"work for DamlLfDefDataType(Tree)" in {
serializeAndParse[model.DamlLfDefDataType](C.treeGC) shouldBe Success(C.treeGC)
}
"work for DamlLfDefDataType(ComplexRecord)" in {
serializeAndParse[model.DamlLfDefDataType](C.complexRecordGC) shouldBe Success(
C.complexRecordGC
)
}
}
}
}

View File

@ -1,75 +0,0 @@
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.navigator.model
import java.time.{Instant, LocalDate}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import com.daml.lf.data.{Time => LfTime}
import com.daml.lf.value.Value.{ValueDate, ValueTimestamp}
import com.daml.lf.value.json.ApiValueImplicits._
class ApiValueSpec extends AnyWordSpec with Matchers {
"Api values" when {
"converting Date from ISO and back" should {
val isoDate = "2019-01-28"
val result = ValueDate.fromIso8601(isoDate).toIso8601
"not change the value" in {
result shouldBe isoDate
}
}
"converting Date to ISO and back" should {
val date = ValueDate(LfTime.Date assertFromDaysSinceEpoch 10000)
val result = ValueDate.fromIso8601(date.toIso8601)
"not change the value" in {
result shouldBe date
}
}
"converting Date from LocalDate and back" should {
val localDate = LocalDate.of(2019, 1, 28)
val result = ValueDate.fromLocalDate(localDate).toLocalDate
"not change the value" in {
result shouldBe localDate
}
}
"converting Timestamp from ISO and back" should {
// Timestamp has microsecond resolution
val isoDateTime = "2019-01-28T12:44:33.123456Z"
val result = ValueTimestamp.fromIso8601(isoDateTime).toIso8601
"not change the value" in {
result shouldBe isoDateTime
}
}
"converting Timestamp to ISO and back" should {
val timestamp = ValueTimestamp(LfTime.Timestamp assertFromLong 123456789123456L)
val result = ValueTimestamp.fromIso8601(timestamp.toIso8601)
"not change the value" in {
result shouldBe timestamp
}
}
"converting Timestamp from Instant and back" should {
// Timestamp has microsecond resolution
val instant = Instant.ofEpochSecond(86400L * 365L * 30L, 123456L * 1000L)
val result = ValueTimestamp.fromInstant(instant).toInstant
"not change the value" in {
result shouldBe instant
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More