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.

../_images/gsg_architecture.svg

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 name Follow.
  • 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 the Party that is allowed to execute the choice. In this case, it is the username party associated with the User 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 the following 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 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

Now we 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}
        httpBaseUrl={httpBaseUrl}
      >
        <MainScreen onLogout={() => 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${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.