App Architecture¶
In this section we’ll look at the different components of our social network app. The goal is to familiarize you enough to feel comfortable extending the code with a new feature in the next section. There are two main components:
- the Daml model and
- the React/TypeScript frontend.
We generate TypeScript code to bridge the two.
Overall, the social networking app is following the recommended architecture of a fullstack Daml application. Below you can see a simplified version of the architecture represented in the app.
Let’s start by looking at the Daml model, which defines the core logic of the application. Have the Daml cheat-sheet open in a separate tab for a quick overview of the most common Daml concepts.
The Daml Model¶
In your terminal, navigate to the root create-daml-app
directory and run:
daml studio
This should open the Visual Studio Code editor at the root of the project.
(You may get a new tab pop up with release notes for the latest version of Daml Connect - just close this.)
Using the file Explorer on the left sidebar, navigate to the daml
folder and double-click on the User.daml
file.
The Daml code defines the data and workflow of the application.
Both are described in the User
contract template.
Let’s look at the data portion first.
template User with
username: Party
following: [Party]
where
signatory username
observer following
There are two important aspects here:
1. The data definition (a schema in database terms), describing the data stored with each user contract.
In this case it is an identifier for the user and the list of users they are following.
Both fields use the built-in Party
type which lets us use them in the following clauses.
2. The signatories and observers of the contract. The signatories are the parties whose authorization is required to create or archive contracts, in this case the user herself. The observers are the parties who are able to view the contract on the ledger. In this case all users that a particular user is following are able to see the user contract.
Let’s see what the signatory
and observer
clauses mean in our app more concretely.
A user Alice can see another user Bob in the network only when Bob is following Alice (only if Alice is in the following
list in his user contract).
For this to be true, Bob must have previously started to follow Alice, as he is the sole signatory on his user contract.
If not, Bob will be invisible to Alice.
Here we see two concepts that are central to Daml: authorization and privacy. Authorization is about who can do what, and privacy is about who can see what. In Daml we must answer these questions upfront, as they fundamentally change the design of an application.
The last part of the Daml model is the operation to follow users, called a choice in Daml.
nonconsuming choice Follow: ContractId User with
userToFollow: Party
controller username
do
assertMsg "You cannot follow yourself" (userToFollow /= username)
assertMsg "You cannot follow the same user twice" (notElem userToFollow following)
archive self
create this with following = userToFollow :: following
Daml contracts are immutable (can not be changed in place), so the only way to “update” one is to archive it and create a new instance.
That is what the Follow
choice does: after checking some preconditions, it archives the current user contract and creates a new one with the new user to follow added to the list. Here is a quick explanation of the code:
- The choice starts with the
nonconsuming choice
keyword followed by the choice nameFollow
.- The return type of a choice is defined next. In this case it is
ContractId User
.- After that we declare choice parameters with the
with
keyword. Here this is the user we want to start following.- The keyword
controller
defines theParty
that is allowed to execute the choice. In this case, it is theusername
party associated with theUser
contract.- The
do
keyword marks the start of the choice body where its functionality will be written.- After passing some checks, the current contract is archived with
archive self
.- A new
User
contract with the new user we have started following is created (the new user is added to thefollowing
list).
This information should be enough for understanding how choices work in this guide. More detailed information on choices can be found in our docs.
Let’s move on to how our Daml model is reflected and used on the UI side.
TypeScript Code Generation¶
The user interface for our app is written in TypeScript. TypeScript is a variant of JavaScript that provides more support during development through its type system.
In order to build an application on top of Daml, we need a way to refer to our Daml templates and choices in TypeScript. We do this by using a Daml to TypeScript code generation tool in the SDK.
To run code generation, we first need to compile the Daml model to an archive format (a .dar
file).
The daml codegen js
command then takes this file as argument to produce a number of TypeScript packages in the output folder.
daml build
daml codegen js .daml/dist/create-daml-app-0.1.0.dar -o daml.js
We now have a TypeScript interface (types and companion objects) to our Daml model, which we’ll use in our UI code next.
The UI¶
On top of TypeScript, we use the UI framework React. React helps us write modular UI components using a functional style - a component is rerendered whenever one of its inputs changes - with careful use of global state.
Let’s see an example of a React component.
All components are in the ui/src/components
folder.
You can navigate there within Visual Studio Code using the file explorer on the left sidebar.
We’ll first look at App.tsx
, which is the entry point to our application.
const App: React.FC = () => {
const [credentials, setCredentials] = React.useState<Credentials | undefined>();
return credentials
? <DamlLedger
token={credentials.token}
party={credentials.party}
>
<MainScreen onLogout={() => {
if (authConfig.provider === 'daml-hub') {
damlHubLogout();
}
setCredentials(undefined);
}} />
</DamlLedger>
: <LoginScreen onLogin={setCredentials} />
}
An important tool in the design of our components is a React feature called Hooks. Hooks allow you to share and update state across components, avoiding having to thread it through manually. We take advantage of hooks in particular to share ledger state across components. We use custom Daml React hooks to query the ledger for contracts, create new contracts, and exercise choices. This is the library you will be using the most when interacting with the ledger [1] .
The useState
hook (not specific to Daml) here keeps track of the user’s credentials.
If they are not set, we render the LoginScreen
with a callback to setCredentials
.
If they are set, then we render the MainScreen
of the app.
This is wrapped in the DamlLedger
component, a React context with a handle to the ledger.
Let’s move on to more advanced uses of our Daml React library.
The MainScreen
is a simple frame around the MainView
component, which houses the main functionality of our app.
It uses Daml React hooks to query and update ledger state.
const MainView: React.FC = () => {
const username = useParty();
const myUserResult = useStreamFetchByKeys(User.User, () => [username], [username]);
const myUser = myUserResult.contracts[0]?.payload;
const allUsers = useStreamQueries(User.User).contracts;
The useParty
hook simply returns the current user as stored in the DamlLedger
context.
A more interesting example is the allUsers
line.
This uses the useStreamQueries
hook to get all User
contracts on the ledger.
(User.User
here is an object generated by daml codegen js
- it stores metadata of the User
template defined in User.daml
.)
Note however that this query preserves privacy: only users that follow the current user have their contracts revealed.
This behaviour is due to the observers on the User
contract being exactly in the list of users that the current user is following.
A final point on this is the streaming aspect of the query. This means that results are updated as they come in - there is no need for periodic or manual reloading to see updates.
Another example, showing how to update ledger state, is how we exercise the Follow
choice of the User
template.
const ledger = useLedger();
const follow = async (userToFollow: Party): Promise<boolean> => {
try {
await ledger.exerciseByKey(User.User.Follow, username, {userToFollow});
return true;
} catch (error) {
alert(`Unknown error:\n${JSON.stringify(error)}`);
return false;
}
}
The useLedger
hook returns an object with methods for exercising choices.
The core of the follow
function here is the call to ledger.exerciseByKey
.
The key in this case is the username of the current user, used to look up the corresponding User
contract.
The wrapper function follow
is then passed to the subcomponents of MainView
.
For example, follow
is passed to the UserList
component as an argument (a prop in React terms).
This gets triggered when you click the icon next to a user’s name in the Network panel.
<UserList
users={followers}
onFollow={follow}
/>
This should give you a taste of how the UI works alongside a Daml ledger. You’ll see this more as you develop your first feature for our social network.
Footnotes
[1] | FYI Behind the scenes the Daml React hooks library uses the Daml Ledger TypeScript library to communicate with a ledger implementation via the HTTP JSON API. |