diff --git a/docs/configs/pdf/index.rst b/docs/configs/pdf/index.rst index b79b304515..36ed423c2f 100644 --- a/docs/configs/pdf/index.rst +++ b/docs/configs/pdf/index.rst @@ -12,7 +12,7 @@ Getting started :maxdepth: 2 Installing the SDK - getting-started/quickstart + Quickstart Guide Writing DAML ------------ @@ -97,6 +97,7 @@ Experimental features daml-script/index tools/visual daml2ts/index + getting-started/index Support and updates ------------------- diff --git a/docs/source/getting-started/app-architecture.rst b/docs/source/getting-started/app-architecture.rst new file mode 100644 index 0000000000..4756ea0b5c --- /dev/null +++ b/docs/source/getting-started/app-architecture.rst @@ -0,0 +1,105 @@ +.. Copyright (c) 2020 The DAML Authors. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +App Architecture +**************** + +In this section we'll look at the different components of our social network app. +The goal is to familiarise you enough to feel comfortable extending the code with a new feature in the next section. + +There are two main components in the code - the DAML model and the React/TypeScript frontend - with generated TypeScript code to bridge the two. +Let's start by looking at the DAML model, as this sets the core logic of the application. + +The DAML Model +============== + +Using VSCode (or a code editor of your choice), navigate to the ``daml`` subdirectory. +There is a single DAML file called ``User.daml`` with the model for users of the app. + +The core data is at the start of the ``User`` contract template. + +.. literalinclude:: code/daml/User.daml + :language: daml + :start-after: -- MAIN_TEMPLATE_BEGIN + :end-before: -- MAIN_TEMPLATE_END + +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 their current list of friends. +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 authorized to create new versions of the contract or archive the contract. +In this case only the user has those rights. +The observers are the parties who are able to view the contract on the ledger. +In this case all friends of a user are able to see the user contract. + +Let's say 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 Alice is a friend in Bob's user contract. +For this to be true, Bob must have previously added Alice as a friend (i.e. updated his user contract), as he is the sole signatory on his user contract. +If not, Bob will be invisible to Alice. + +We can see some concepts here that are central to DAML, namely *privacy* and *authorization*. +Privacy is about who can *see* what, and authorization is about who can *do* what. +In DAML we must answer these questions upfront, as they fundamentally change the design of an application. + +The last thing we'll point out about the DAML model for now is the operation to add friends, called a *choice* in DAML. + +.. literalinclude:: code/daml/User.daml + :language: daml + :start-after: -- ADDFRIEND_BEGIN + :end-before: -- ADDFRIEND_END + +DAML contracts are *immutable* (can not be changed in place), so they must be updated by archiving the current instance and creating a new one. +That is what the ``AddFriend`` choice does: after checking some preconditions, it creates a new user contract with the new friend added to the list. +The ``choice`` syntax automatically includes the archival of the current instance. + +.. TODO Update depending on consuming/nonconsuming choice. + +Next we'll see how our DAML code 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 in development through its type system. + +In order to build an application on top of DAML, we need a way to refer to the DAML template and choices in TypeScript. +We do this using a DAML to TypeScript code generation tool in the DAML SDK. + +To run code generation, we first need to compile the DAML model to an archive format (with a ``.dar`` extension). +Then the command ``daml codegen ts`` takes this file as argument to produce a number of TypeScript files in the specified location. + + daml build + daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts/src + +We now have TypeScript types and companion objects in the ``daml-ts`` workspace which we can use from our UI code. +We'll see that next. + +The UI +====== + +Our UI is written using `React `_ and +React helps us write modular UI components using a functional style - a component is rerendered whenever one of its inputs changes - combined with a judicious use of global state. + +We can see the latter in the way we handle ledger state throughout the application code. +For this we use a state management feature in React called `Hooks `_. +You can see the capabilities of the DAML React hooks in ``create-daml-app/ui/src/daml-react-hooks/hooks.ts``. +For example, we can query the ledger for all visible contracts (relative to a particular user), create contracts and exercise choices on contracts. + +.. TODO Update location to view DAML react hooks API + +Let's see some examples of DAML React hooks. + +.. literalinclude:: code/ui-before/MainView.tsx + :start-after: -- HOOKS_BEGIN + :end-before: -- HOOKS_END + +This is the start of the component which provides data from the current state of the ledger to the main screen of our app. +The three declarations within ``MainView`` all use DAML hooks to get information from the ledger. +For instance, ``allUsers`` uses a catch-all query to get the ``User`` contracts on the ledger. +However, the query respects the privacy guarantees of a DAML ledger: the contracts returned are only those visible to the currently logged in party. +This explains why you cannot see *all* users in the network on the main screen, only those who have added you as a friend (making you an observer of their ``User`` contract). + +.. TODO You also see friends of friends; either explain or prevent this. diff --git a/docs/source/getting-started/code/daml/User.daml b/docs/source/getting-started/code/daml/User.daml new file mode 100644 index 0000000000..b244f3658b --- /dev/null +++ b/docs/source/getting-started/code/daml/User.daml @@ -0,0 +1,45 @@ +-- Copyright (c) 2020 The DAML Authors. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +daml 1.2 +module User where + +-- MAIN_TEMPLATE_BEGIN +template User with + username: Party + friends: [Party] + where + signatory username + observer friends +-- MAIN_TEMPLATE_END + + key username: Party + maintainer key + +-- ADDFRIEND_BEGIN + choice AddFriend: ContractId User with + friend: Party + controller username + do + assertMsg "You cannot add yourself as a friend" (friend /= username) + assertMsg "You cannot add a friend twice" (friend `notElem` friends) + create this with friends = friend :: friends +-- ADDFRIEND_END + +-- SENDMESSAGE_BEGIN + nonconsuming choice SendMessage: ContractId Message with + sender: Party + content: Text + controller sender + do + create Message with sender, receiver = username, content +-- SENDMESSAGE_END + +-- MESSAGE_BEGIN +template Message with + sender: Party + receiver: Party + content: Text + where + signatory sender, receiver +-- MESSAGE_END diff --git a/docs/source/getting-started/code/ui-after/Feed.tsx b/docs/source/getting-started/code/ui-after/Feed.tsx new file mode 100644 index 0000000000..e2532d70d8 --- /dev/null +++ b/docs/source/getting-started/code/ui-after/Feed.tsx @@ -0,0 +1,27 @@ +// Copyright (c) 2020 The DAML Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react' +import { List, ListItem } from 'semantic-ui-react'; +import { Message } from '@daml2ts/create-daml-app/lib/create-daml-app-0.1.0/User'; + +type Props = { + messages: Message[]; +} + +/** + * React component to show a feed of messages for a particular user. + */ +const Feed: React.FC = ({messages}) => { + const showMessage = (message: Message): string => { + return (message.sender + ": " + message.content); + } + + return ( + + {messages.map((message) => {showMessage(message)})} + + ); +} + +export default Feed; diff --git a/docs/source/getting-started/code/ui-after/MainView.tsx b/docs/source/getting-started/code/ui-after/MainView.tsx new file mode 100644 index 0000000000..389b29e61b --- /dev/null +++ b/docs/source/getting-started/code/ui-after/MainView.tsx @@ -0,0 +1,138 @@ +// Copyright (c) 2020 The DAML Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { Container, Grid, Header, Icon, Segment, Divider } from 'semantic-ui-react'; +import { Party } from '@daml/types'; +import { useParty, useReload, useExerciseByKey, useFetchByKey, useQuery } from '@daml/react'; +import UserList from './UserList'; +import PartyListEdit from './PartyListEdit'; +// -- IMPORTS_BEGIN +import { User, Message } from '@daml2ts/create-daml-app/lib/create-daml-app-0.1.0/User'; +import MessageEdit from './MessageEdit'; +import Feed from './Feed'; +// -- IMPORTS_END + +const MainView: React.FC = () => { + const username = useParty(); + const myUserResult = useFetchByKey(User, () => username, [username]); + const myUser = myUserResult.contract?.payload; + const allUsersResult = useQuery(User); + const allUsers = allUsersResult.contracts.map((user) => user.payload); + const reload = useReload(); + + const [exerciseAddFriend] = useExerciseByKey(User.AddFriend); + const [exerciseRemoveFriend] = useExerciseByKey(User.RemoveFriend); + +// -- HOOKS_BEGIN + const messagesResult = useQuery(Message, () => ({receiver: username}), []); + const messages = messagesResult.contracts.map((message) => message.payload); + + const [exerciseSendMessage] = useExerciseByKey(User.SendMessage); +// -- HOOKS_END + + + const addFriend = async (friend: Party): Promise => { + try { + await exerciseAddFriend(username, {friend}); + return true; + } catch (error) { + alert("Unknown error:\n" + JSON.stringify(error)); + return false; + } + } + + const removeFriend = async (friend: Party): Promise => { + try { + await exerciseRemoveFriend(username, {friend}); + } catch (error) { + alert("Unknown error:\n" + JSON.stringify(error)); + } + } + +// -- SENDMESSAGE_BEGIN + const sendMessage = async (content: string, receiver: string): Promise => { + try { + await exerciseSendMessage(receiver, {sender: username, content}); + return true; + } catch (error) { + alert("Error while sending message:\n" + JSON.stringify(error)); + return false; + } + } +// -- SENDMESSAGE_END + + React.useEffect(() => { + const interval = setInterval(reload, 5000); + return () => clearInterval(interval); + }, [reload]); + + return ( + + + + +
+ {myUser ? `Welcome, ${myUser.username}!` : 'Loading...'} +
+ + +
+ + + {myUser?.username ?? 'Loading...'} + Me and my friends + +
+ + +
+ +
+ + + The Network + + Others and their friends + +
+ + user1.username.localeCompare(user2.username))} + onAddFriend={addFriend} + /> +
+// -- MESSAGES_SEGMENT_BEGIN + +
+ + + Messages + Send a message to a friend + +
+ + + +
+// -- MESSAGES_SEGMENT_END +
+
+
+
+ ); +} + +export default MainView; diff --git a/docs/source/getting-started/code/ui-after/MessageEdit.tsx b/docs/source/getting-started/code/ui-after/MessageEdit.tsx new file mode 100644 index 0000000000..a2073aa7bd --- /dev/null +++ b/docs/source/getting-started/code/ui-after/MessageEdit.tsx @@ -0,0 +1,60 @@ +// Copyright (c) 2020 The DAML Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react' +import { Form, Input, Button } from 'semantic-ui-react'; +import { Text } from '@daml/types'; + +type Props = { + sendMessage: (content: Text, receiver: string) => Promise; +} + +/** + * React component to edit a message to send to a friend. + */ +const MessageEdit: React.FC = ({sendMessage}) => { + const [content, setContent] = React.useState(''); + const [receiver, setReceiver] = React.useState(''); + const [isSubmitting, setIsSubmitting] = React.useState(false); + + const submitMessage = async (event?: React.FormEvent) => { + if (event) { + event.preventDefault(); + } + setIsSubmitting(true); + const success = await sendMessage(content, receiver); + setIsSubmitting(false); + if (success) { + setContent(''); + setReceiver(''); + } + } + + return ( +
+ setReceiver(event.currentTarget.value)} + /> +
+ setContent(event.currentTarget.value)} + /> +
+ +
+ ); +}; + +export default MessageEdit; diff --git a/docs/source/getting-started/code/ui-before/MainView.tsx b/docs/source/getting-started/code/ui-before/MainView.tsx new file mode 100644 index 0000000000..8771d3e72f --- /dev/null +++ b/docs/source/getting-started/code/ui-before/MainView.tsx @@ -0,0 +1,100 @@ +// Copyright (c) 2020 The DAML Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { Container, Grid, Header, Icon, Segment, Divider } from 'semantic-ui-react'; +import { Party } from '@daml/types'; +import { useParty, useReload, useExerciseByKey, useFetchByKey, useQuery } from '@daml/react'; +import UserList from './UserList'; +import PartyListEdit from './PartyListEdit'; +import { User } from '@daml2ts/create-daml-app/lib/create-daml-app-0.1.0/User'; + +const MainView: React.FC = () => { +// -- HOOKS_BEGIN + const username = useParty(); + const myUserResult = useFetchByKey(User, () => username, [username]); + const myUser = myUserResult.contract?.payload; + const allUsersResult = useQuery(User); + const allUsers = allUsersResult.contracts.map((user) => user.payload); +// -- HOOKS_END + const reload = useReload(); + + const [exerciseAddFriend] = useExerciseByKey(User.AddFriend); + const [exerciseRemoveFriend] = useExerciseByKey(User.RemoveFriend); + + const addFriend = async (friend: Party): Promise => { + try { + await exerciseAddFriend(username, {friend}); + return true; + } catch (error) { + alert("Unknown error:\n" + JSON.stringify(error)); + return false; + } + } + + const removeFriend = async (friend: Party): Promise => { + try { + await exerciseRemoveFriend(username, {friend}); + } catch (error) { + alert("Unknown error:\n" + JSON.stringify(error)); + } + } + + React.useEffect(() => { + const interval = setInterval(reload, 5000); + return () => clearInterval(interval); + }, [reload]); + + return ( + + + + +
+ {myUser ? `Welcome, ${myUser.username}!` : 'Loading...'} +
+ + +
+ + + {myUser?.username ?? 'Loading...'} + Me and my friends + +
+ + +
+ +
+ + + The Network + + Others and their friends + +
+ + user1.username.localeCompare(user2.username))} + onAddFriend={addFriend} + /> +
+
+
+
+
+ ); +} + +export default MainView; diff --git a/docs/source/getting-started/first-feature.rst b/docs/source/getting-started/first-feature.rst new file mode 100644 index 0000000000..b8801ddd21 --- /dev/null +++ b/docs/source/getting-started/first-feature.rst @@ -0,0 +1,143 @@ +.. Copyright (c) 2020 The DAML Authors. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +Your First Feature: Messaging Friends +************************************* + +Let's dive into implementing a feature for our social network app. +From that we'll get a better idea of how to build DAML applications using our template. + +Right now our app allows us to add and remove friends, but we can't communicate with them! +Let's fix that by adding a private messaging feature. +We will allow a user to send messages to a number of friends at once, and see all the messages that have been sent to them. +Of course we must make sure that no one can see messages that were not sent to them. +We will see that DAML lets us implement this in a direct and intuitive way. + +There are two parts to building the messaging feature: the DAML code and the UI. +Let's start with adding to the DAML code, on which we will base our UI changes. + +DAML Changes +============ + +The DAML code defines the *workflow* of the application. +This means: what interactions between users (or *parties*) are permitted by the system? +In the context of our feature, the question is: when is a user allowed to message another user? + +The approach we'll take is: a user Bob can message another user Alice if Alice has added Bob as a friend. +Remember that friendships are single-directional in our app. +So Alice adding Bob as a friend means that she gives permission (or *authority*) for Bob to send her a message. + +In DAML this workflow is represented as a new choice on the ``User`` contract. + +.. literalinclude:: code/daml/User.daml + :language: daml + :start-after: -- SENDMESSAGE_BEGIN + :end-before: -- SENDMESSAGE_END + +Let's break this down. +The choice is ``nonconsuming`` because sending a message should not affect the existence of the ``User`` contract. +By convention, the choice returns the ``ContractId`` of the resulting ``Message`` contract (which we'll show next). +Next, the parameters to the choice are the sender (the party wishing to talk to the signatory of this ``User`` contract) and the message text. +The ``controller`` clause suggests that it is the ``sender`` who can exercise the choice. +Finally, the body of the choice simply creates the new ``Message`` with the sender, receiver and content. + +Note that there is no explicit check in the choice that the ``sender`` is a friend of the user. +This is because the ``User`` contract is only ever visible to friends (the observers of the contract). + +Now let's see the ``Message`` contract template. +This is very simple - data and no choices - as well as the ``signatory`` declaration. + +.. literalinclude:: code/daml/User.daml + :language: daml + :start-after: -- MESSAGE_BEGIN + :end-before: -- MESSAGE_END + +Note that we have two signatories on the ``Message`` contract: both the sender and receiver. +This enforces the fact that the contract creation (and archival) must be authorized by both parties. + +Now we've specified the workflow of sending messages, let's integrate the functionality into our app. + +TypeScript Code Generation +========================== + +Remember that we interface with our DAML code from the UI components using the generated TypeScript. +Since we have changed our DAML code, we also need to rerun the TypeScript code generator. +Let's do this now by running:: + + daml build + daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts/src + +As the TypeScript code is generated into the separate ``daml-ts`` workspace on which the UI depends, we need to rebuild the workspaces from the root directory using:: + + yarn workspaces run build + +We should now have the updated TypeScript code with equivalents of the ``Message`` template and ``SendMessage`` choice. + +Now let's implement our messaging feature in the UI! + +Messaging UI +============ + +Our messaging feature has two parts: a form with inputs for selecting friends and composing the message text, and a "feed" of messages that have been sent to you. +Both parts will be implemented as React components that render on the main screen. + +Feed Component +-------------- + +The feed component is fairly straight-forward: it queries all ``Message`` contracts and displays their contents as a list. +Here is the code for the entire component. + +.. literalinclude:: code/ui-after/Feed.tsx + +The key point here is that for any particular user, the ``Message`` query yields exactly the messages that have been either written by or sent to that user. +This is due to how we modelled the signatories and observers in the ``Message`` template, and means we do not risk a privacy breach coming from the application code. + +Message Edit Component +---------------------- + +In addition to the feed component, we need a component for composing messages and sending them using the appropriate choice on the ``User`` contract. + +.. literalinclude:: code/ui-after/MessageEdit.tsx + +In this component we use React hooks for the message content and receiver. +You can see these used in the ``submitMessage`` function, called when the "Send" button is clicked. +The ``isSubmitting`` state is used to ensure that message requests are processed one at a time. +The result of each send is a new ``Message`` contract created, after which the form is cleared. + +View Component +-------------------- + +The ``MainView`` component is the workhorse of this application which queries the ledger for data and houses the different subcomponents (e.g. friends, the network and our messaging components above). +To support the messaging components, we will need DAML React hooks for querying ``Message`` contracts and exercising the ``SendMessage`` choice on our ``User`` contract. + +First import the generated Typescript code for the ``Message`` contract template, as well as our two new components. + +.. literalinclude:: code/ui-after/MainView.tsx + :start-after: -- IMPORTS_BEGIN + :end-before: -- IMPORTS_END + +Then we declare the hooks themselves at the start of the component. + +.. literalinclude:: code/ui-after/MainView.tsx + :start-after: -- HOOKS_BEGIN + :end-before: -- HOOKS_END + +The ``messagesResult`` tracks the state of ``Message`` contracts on the ledger, where we specify no restrictions on the query. +We extract the actual message data in ``messages``. +The ``exerciseSendMessage`` hook gives us a function to exercise the appropriate choice on our ``User``. +We wrap this in another ``sendMessage`` function which splits an input string into a list of parties and then exercises the choice, reporting to the user in the case of an error. + +.. literalinclude:: code/ui-after/MainView.tsx + :start-after: -- SENDMESSAGE_BEGIN + :end-before: -- SENDMESSAGE_END + +Finally we can integrate our new messaging components into the main screen view. +In another segment we add the panel including our two new components: the ``MessageEdit`` and the ``Feed``. + +.. literalinclude:: code/ui-after/MainView.tsx + :start-after: -- MESSAGES_SEGMENT_BEGIN + :end-before: -- MESSAGES_SEGMENT_END + +You have now finished implementing your first end-to-end DAML feature! +Let's give the new functionality a spin. +We follow the instructions in "Running the app" to start up the new app. diff --git a/docs/source/getting-started/index.rst b/docs/source/getting-started/index.rst new file mode 100644 index 0000000000..4d90369f4a --- /dev/null +++ b/docs/source/getting-started/index.rst @@ -0,0 +1,96 @@ +.. Copyright (c) 2020 The DAML Authors. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +.. _new-quickstart: + +Full Stack DAML +############### + +**Disclaimer:** This guide is being actively developed. +Expect major changes to the tutorial text and minor changes to the template application. + +The goal of this tutorial is to get you up and running with full-stack, DAML-driven app development. We will provide you with the template for a miniature social networking app, get you writing your first end-to-end feature and finally deploy your new app to a persistent ledger. + +By the end of the tutorial, you should have an idea of what DAML contracts and ledgers are, how the UI interacts with them, and how you might solve a potential use case with a DAML solution. We do not aim to give a comprehensive guide to all DAML concepts and tools; for that, see the later sections of the documentation. With that, let's get started! + +.. TODO: reference specific sections of docs instead of saying "later sections". + +.. toctree:: + :hidden: + + app-architecture + first-feature + +Prerequisites +************* + +If you haven't already, see :doc:`installation` for the DAML SDK and VSCode development environment. + +You will also need some common software tools to build and interact with the template project. + +- `Git `_ version control system +- `Yarn `_ package manager for Javascript +- A terminal application for command line interaction + + +Running the app +*************** + +We'll start by getting the app up and running, and then explain the different components which we will later extend. + +First off, open a terminal, clone the template project and move to the project folder:: + + git clone https://github.com/digital-asset/create-daml-app.git + cd create-daml-app + +In order to connect the DAML model to the UI code, we need to run a code generation step (more on this later):: + + daml build + daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts/src + +Now, use Yarn to install the project dependencies and build the app:: + + yarn install + yarn workspaces run build + +You should see ``Compiled successfully.`` in the output if everything is working as expected. + +.. TODO: Give instructions for possible failures. + +We can now run the app in two steps. +You'll need two terminal windows running for this. + +In one terminal, at the root of the ``create-daml-app`` directory, run the script:: + + ./daml-start.sh + +This compiles the DAML component of the project and starts a *Sandbox* ledger for the app. +The ledger in this case is stored in the Sandbox application memory; it is not persistent but is useful for testing and development. +We'll leave the Sandbox running to serve requests from the UI, which result in changes to the in-memory ledger. + +In a second terminal, navigate to the ``create-daml-app/ui`` folder and run:: + + yarn start + +This starts the UI application connected to the already running Sandbox. +The command should automatically open a window in your default browser at http://localhost:3000. +If it doesn't, just open that link in any web browser. +You may be asked whether to allow the app to receive network connections, which you should allow. + +At this point you should see the login page for the social network. + +.. TODO: Screenshot + +Enter a user name of your choice and click the calculator icon next to the password field to generate a password token. +(We do not have proper authentication in this app for simplicity.) +Once you click "Sign up", you can see a screen with panels for your friends and the entire social network. +Initially these are both empty as you don't have any friends yet! +Go ahead and add some using the form (and remove them using the cross icons if you change your mind). + +Now let's grow the network. Log out and sign up using the name of one of your friends. +Let's say your name is Alice and your friend's name is Bob. +Bob should now see Alice in the network (since she added him as a friend) and he is able to add her back. +Note that in this app, friendships can be added in one direction at a time, similar to how "followers" work in Twitter. + +Add a few more friends as Bob, and play around a bit more by logging in as different users and adding/removing friends from the network. +This should give you a idea of the app's functionality. diff --git a/docs/source/index.rst b/docs/source/index.rst index e28b703b65..3bef7ddcab 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,7 +18,7 @@ DAML SDK documentation :caption: Getting started Installing the SDK - getting-started/quickstart + Quickstart Guide .. toctree:: :titlesonly: @@ -102,6 +102,7 @@ DAML SDK documentation DAML Script tools/visual daml2ts/index + getting-started/index .. toctree:: :titlesonly: