├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── assets ├── pandoc │ ├── header-includes.tex │ ├── include-before.tex │ └── pandoc-defaults.yaml └── templates │ └── contract.dhall.tmpl ├── cmd └── themis-contract │ ├── compile.go │ ├── execute.go │ ├── list_signatories.go │ ├── main.go │ ├── new.go │ ├── profile.go │ ├── review.go │ ├── root.go │ ├── sign.go │ ├── signature.go │ ├── update.go │ ├── upstream.go │ └── version.go ├── config ├── Contract.dhall ├── FileRef.dhall ├── Signatory.dhall ├── Template.dhall ├── TemplateFormat.dhall └── package.dhall ├── docs ├── 01-setup.md ├── 02-first-contract.md ├── 03-workflows.md ├── 04-formatting.md ├── cross-references.png ├── enumerated-lists.png └── numbered-sections.png ├── examples └── service-agreement │ ├── contract.dhall │ ├── contract.md │ ├── contract.pdf │ └── params.dhall ├── go.mod ├── go.sum └── pkg └── themis-contract ├── cache.go ├── cache_test.go ├── context.go ├── contract.go ├── diff.go ├── exports_test.go ├── file_ref.go ├── file_ref_test.go ├── git.go ├── git_test.go ├── profile.go ├── signatory.go ├── signature.go ├── static_resources.go ├── statik ├── README.md └── statik.go ├── template.go ├── utils.go ├── utils_test.go └── web.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.14+ 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: "^1.14" 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | go get github.com/rakyll/statik 29 | 30 | - name: Build 31 | run: make 32 | 33 | - name: Test 34 | run: make test 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: [ "v*" ] 6 | 7 | jobs: 8 | 9 | build-linux: 10 | name: Build for Linux 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Set up Go 1.14+ 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: "^1.14" 18 | id: go 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v2 22 | 23 | - name: Get dependencies 24 | run: | 25 | go get -v -t -d ./... 26 | go get github.com/rakyll/statik 27 | 28 | - name: Build 29 | run: make 30 | 31 | - name: Test 32 | run: make test 33 | 34 | - name: Upload Linux build artifact 35 | uses: actions/upload-artifact@v2 36 | with: 37 | name: bin-linux 38 | path: build/themis-contract 39 | 40 | build-macos: 41 | name: Build for MacOS 42 | runs-on: macos-latest 43 | steps: 44 | 45 | - name: Set up Go 1.14+ 46 | uses: actions/setup-go@v2 47 | with: 48 | go-version: "^1.14" 49 | id: go 50 | 51 | - name: Check out code into the Go module directory 52 | uses: actions/checkout@v2 53 | 54 | - name: Get dependencies 55 | run: | 56 | go get -v -t -d ./... 57 | go get github.com/rakyll/statik 58 | 59 | - name: Build 60 | run: make 61 | 62 | - name: Test 63 | run: make test 64 | 65 | - name: Upload MacOS build artifact 66 | uses: actions/upload-artifact@v2 67 | with: 68 | name: bin-macos 69 | path: build/themis-contract 70 | 71 | release: 72 | name: Create release 73 | runs-on: ubuntu-latest 74 | needs: [ "build-linux", "build-macos" ] 75 | steps: 76 | 77 | - name: Download Linux Artifact 78 | uses: actions/download-artifact@v2 79 | with: 80 | name: bin-linux 81 | path: linux 82 | 83 | - name: Download MacOS Artifact 84 | uses: actions/download-artifact@v2 85 | with: 86 | name: bin-macos 87 | path: macos 88 | 89 | - name: List artifacts 90 | run: | 91 | ls -lah 92 | ls -lah linux 93 | ls -lah macos 94 | 95 | - name: Create release 96 | id: create_release 97 | uses: actions/create-release@v1 98 | env: 99 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 100 | with: 101 | tag_name: ${{ github.ref }} 102 | release_name: Release ${{ github.ref }} 103 | draft: false 104 | prerelease: false 105 | 106 | - name: Upload Linux binary for release 107 | uses: actions/upload-release-asset@v1 108 | env: 109 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 110 | with: 111 | upload_url: ${{ steps.create_release.outputs.upload_url }} 112 | asset_path: ./linux/themis-contract 113 | asset_name: themis-contract-linux 114 | asset_content_type: application/octet-stream 115 | 116 | - name: Upload MacOS binary for release 117 | uses: actions/upload-release-asset@v1 118 | env: 119 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 120 | with: 121 | upload_url: ${{ steps.create_release.outputs.upload_url }} 122 | asset_path: ./macos/themis-contract 123 | asset_name: themis-contract-macos 124 | asset_content_type: application/octet-stream 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | build/ 15 | 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | * Fix issue where Git repositories weren't being created correctly when 6 | deriving a new contract into an empty (non-Git repo) folder 7 | ([\#116](https://github.com/informalsystems/themis-contract/issues/116)) 8 | 9 | ## v0.2.4 10 | 11 | * Update Dhall contract template to use the package configuration on `master` 12 | ([\#72](https://github.com/informalsystems/themis-contract/pull/72)) 13 | 14 | ## v0.2.3 15 | 16 | * Fix some minor spelling mistakes in the CLI help 17 | ([\#71](https://github.com/informalsystems/themis-contract/pull/71)). 18 | 19 | ## v0.2.2 20 | 21 | * Fix a critical bug that prevents signing of a contract 22 | ([\#70](https://github.com/informalsystems/themis-contract/pull/70)). 23 | 24 | ## v0.2.1 25 | 26 | * Fix a critical compilation bug that prevented signature image details from 27 | being provided during compilation 28 | ([\#69](https://github.com/informalsystems/themis-contract/pull/69)) 29 | 30 | ## v0.2 31 | 32 | * Completely rebuilt in Go. 33 | * Totally different CLI (see [README](README.md) for details). 34 | 35 | ## Unreleased (`prototype/v1` branch) 36 | 37 | * Remove `--style` flag from `compile` subcommand 38 | ([#21](https://github.com/informalsystems/themis-contract/pull/21)) 39 | * Add `--defaults` flag to `compile` command, allowing pass through of a pandoc 40 | defaults file. ([#21](https://github.com/informalsystems/themis-contract/pull/21)) 41 | * Add `init` subcommand for initializing user environment 42 | ([#21](https://github.com/informalsystems/themis-contract/pull/21)) 43 | * Add `Dockerfile` 44 | ([\#51](https://github.com/informalsystems/themis-contract/pull/51)). 45 | 46 | ## v0.1.6 47 | 48 | * Rename `neat-contract` to `themis-contract` 49 | 50 | ## v0.1.5 51 | 52 | * [\#13](https://github.com/informalsystems/themis-contract/pull/13) - Report 53 | reason for TOML parsing errors. 54 | 55 | ## v0.1.4 56 | 57 | * Switch default PDF engine to **tectonic** 58 | 59 | ## v0.1.3 60 | 61 | * Make cryptographic signing with Keybase the default option for the `sign` 62 | command 63 | 64 | ## v0.1.2 65 | 66 | * Add prerequisite fonts to `fonts` folder 67 | * Add installation script to `scripts/install.sh` 68 | * Add `gen-sigimages` command to allow for generation of signature images from 69 | detached cryptographic signature files 70 | 71 | ## v0.1.1 72 | 73 | * Allow users to specify font spec for signature when signing using Keybase 74 | * Add support for Keybase-based signing and verification 75 | 76 | ## v0.1.0 77 | First prototype release with basic functionality: 78 | 79 | * Contract creation from templates 80 | * Parameter substitution 81 | * Right now only TOML-based contracts are supported 82 | * PDF generation 83 | * Uses `pandoc` and `tectonic` 84 | * Templating support: 85 | * Support for Mustache templates 86 | * Support for Handlebars templates 87 | * Templates can be in pretty much any format that `pandoc` supports 88 | * LaTeX templates are not passed through `pandoc`, they are compiled directly 89 | with `tectonic` 90 | * Support for using templates located locally on the file system (absolute or 91 | relative paths) 92 | * Support for fetching remote templates 93 | * Via HTTPS 94 | * Via Git 95 | * Signing of contracts 96 | * Can sign with a simple image-based signature 97 | * Can sign cryptographically using Keybase signatures 98 | * Saving counterparties and signatories for easy insertion into contracts 99 | * Saving identities for quick use when signing 100 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 4 | **Table of Contents** 5 | 6 | - [Contributing](#contributing) 7 | - [Contributing to the source](#contributing-to-the-source) 8 | - [Signing the DCO](#signing-the-dco) 9 | - [Adding the signoff retroactively](#adding-the-signoff-retroactively) 10 | 11 | 12 | 13 | We are glad you're interested in contributing to `themis-contract`! 14 | Contributions are most welcome. 15 | 16 | There are two principles ways to contribute at this point: 17 | 18 | - [File an issue][] with a bug report or feature request. 19 | - Contribute to the source code (including documentation). 20 | 21 | ## Contributing to the source 22 | 23 | ### Signing the DCO 24 | 25 | We require that all commits be signed, certifying the [Developer Certificate 26 | of Origin (DCO)][DCO]. By signing your commits you confirm that you wrote 27 | your contribution, or have otherwise secured the right to contribute the 28 | contribution as an open-source addition to our project. 29 | 30 | Here is the full text, also hosted at https://developercertificate.org/ : 31 | 32 | > Developer Certificate of Origin 33 | > Version 1.1 34 | > 35 | > Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 36 | > 1 Letterman Drive 37 | > Suite D4700 38 | > San Francisco, CA, 94129 39 | > 40 | > Everyone is permitted to copy and distribute verbatim copies of this 41 | > license document, but changing it is not allowed. 42 | > 43 | > 44 | > Developer's Certificate of Origin 1.1 45 | > 46 | > By making a contribution to this project, I certify that: 47 | > 48 | > (a) The contribution was created in whole or in part by me and I 49 | > have the right to submit it under the open source license 50 | > indicated in the file; or 51 | > 52 | > (b) The contribution is based upon previous work that, to the best 53 | > of my knowledge, is covered under an appropriate open source 54 | > license and I have the right under that license to submit that 55 | > work with modifications, whether created in whole or in part 56 | > by me, under the same open source license (unless I am 57 | > permitted to submit under a different license), as indicated 58 | > in the file; or 59 | > 60 | > (c) The contribution was provided directly to me by some other 61 | > person who certified (a), (b) or (c) and I have not modified 62 | > it. 63 | > 64 | > (d) I understand and agree that this project and the contribution 65 | > are public and that a record of the contribution (including all 66 | > personal information I submit with it, including my sign-off) is 67 | > maintained indefinitely and may be redistributed consistent with 68 | > this project or the open source license(s) involved. 69 | 70 | You can sign your commits automatically by using the `--signoff` (or `-s`) flag 71 | when you commit: 72 | 73 | ```sh 74 | git commit -s -m 'Some commit message' 75 | ``` 76 | 77 | Or manually, by including a line with your name and email in each commit 78 | message, following this pattern: 79 | 80 | ``` 81 | Some commit message 82 | 83 | Signed-off-by: Some Name 84 | ``` 85 | 86 | #### Adding the signoff retroactively 87 | 88 | If you forget to signoff on one ore more commits (or if you didn't read the 89 | contributing guide before opening a PR ;) ) you can easily fix your commits by 90 | following [this 91 | guide from source{d}](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md#how-to-add-sign-offs-retroactively). 92 | 93 | [File an issue]: https://github.com/informalsystems/themis-contract/issues/new 94 | [DCO]: https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGES=$(shell go list ./...) 2 | OUTPUT=build/themis-contract 3 | VERSION=$(shell git describe --tags) 4 | TIMESTAMP=$(shell date -u +%Y%m%d.%H%M%S) 5 | BUILD_FLAGS=-mod=readonly -ldflags="-X main.version=$(VERSION)-$(TIMESTAMP)" 6 | .DEFAULT_GOAL := build 7 | 8 | THEMIS_INSTALL_DIR ?= /usr/local/bin/ 9 | 10 | all: build test 11 | 12 | deps: 13 | go get github.com/rakyll/statik 14 | 15 | build: 16 | cd pkg/themis-contract/ && statik -f -src=../../assets/ 17 | go build $(BUILD_FLAGS) -o $(OUTPUT) cmd/themis-contract/* 18 | 19 | test: 20 | go test $(PACKAGES) 21 | 22 | install: build 23 | cp $(OUTPUT) $(THEMIS_INSTALL_DIR) 24 | 25 | .PHONY: all build test install deps 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | >***NOTE***: _This repository is no longer supported or updated by Informal Systems. If you wish to continue to develop this code yourself, we recommend you fork it._ 2 | 3 | # Themis Contract 🖋 4 | 5 | ## Overview 6 | 7 | Themis Contract is a command line-based tool to help with legal contracting. 8 | It currently aims to: 9 | 10 | 1. Make contracting modular (in a decentralized way). 11 | 2. Make contracting more programmatic (and eventually executable) than is 12 | traditionally the case. 13 | 3. Make managing contracts more like managing software (so we can leverage 14 | the value that software development processes offer, like version control, 15 | continuous integration, etc.). 16 | 17 | **Disclaimer** 18 | 19 | Themis Contract is considered **alpha quality** at present. No semantic 20 | versioning will be used just yet. Breaking changes can be released at any time. 21 | For the original NodeJS-based prototype of Themis Contract, please see the 22 | `prototype/v1` branch. 23 | 24 | ## 🙋🏾‍♀️ Get Involved 25 | 26 | - Say hi in one of our **chat rooms** :speech_balloon: 27 | - [Register][join-zulip] on Zulip, our chat platform 28 | - [`#introductions`][zulip-intros] is a great place to start 29 | - [`#disco-tools`][zulip-disco] is for discussing tooling for distributed co-op organizations (including `themis-contract`) 30 | 31 | [join-zulip]: https://informal-systems.zulipchat.com/register/ 32 | [zulip-intros]: https://informal-systems.zulipchat.com/#narrow/stream/268313-Introductions 33 | [zulip-disco]: https://informal-systems.zulipchat.com/#narrow/stream/268309-disco-tools 34 | 35 | ## Installation 36 | 37 | ### Packaged 38 | 39 | #### MacOS 40 | 41 | Use [homebrew](https://brew.sh/) to Install from the [informal.systems 42 | tap](https://github.com/informalsystems/homebrew-pkgs): 43 | 44 | ```bash 45 | brew install informalsystems/pkgs/themis-contract 46 | ``` 47 | 48 | ### Manually 49 | 50 | #### Requirements 51 | 52 | In order to install Themis Contract locally, you will need: 53 | 54 | - Go 1.14+ (and supporting tooling, like `make`) 55 | - [pandoc] 56 | - [pandoc-crossref][] 57 | - Any LaTeX distribution that includes `pdflatex` (such as [MacTeX] for macOS) 58 | - [dhall-to-json] 59 | - Git 60 | 61 | #### Pre-built binaries 62 | 63 | Once you have the requirements installed locally, you can simply download the 64 | latest [release] binary for your platform (right now we only build for Linux and 65 | MacOS) and put it somewhere in your path (e.g. 66 | `/usr/local/bin/themis-contract`). 67 | 68 | #### From Source 69 | 70 | To install from source: 71 | 72 | ```bash 73 | git clone https://github.com/informalsystems/themis-contract.git 74 | cd themis-contract 75 | # once-off 76 | make deps 77 | # Optionally setting THEMIS_INSTALL_DIR to your desired location 78 | # (default is to /usr/local/bin/) 79 | THEMIS_INSTALL_DIR=~/.local/bin make install 80 | ``` 81 | 82 | ## Usage 83 | 84 | See the following tutorials for details as to how to set up and use Themis 85 | Contract to get the most out of it: 86 | 87 | - [Setting up Themis Contract](docs/01-setup.md) 88 | - [Your first contract](docs/02-first-contract.md) 89 | - [Contracting workflows](docs/03-workflows.md) 90 | - [Formatting guide](docs/04-formatting.md) 91 | 92 | More tutorials will be coming soon! 93 | 94 | ## Uninstalling 95 | 96 | Since Themis Contract is just a single standalone binary, uninstalling just 97 | involves deleting that binary: 98 | 99 | ```bash 100 | rm /usr/local/bin/themis-contract 101 | 102 | # Optional: to delete all Themis Contract-related data 103 | rm -rf ~/.themis/contract 104 | ``` 105 | 106 | ## Contributing 107 | 108 | See [CONTRIBUTING.md](./CONTRIBUTING.md). 109 | 110 | ## License 111 | 112 | Copyright 2020 Informal Systems Inc. 113 | 114 | Licensed under the Apache License, Version 2.0 (the "License"); 115 | you may not use this file except in compliance with the License. 116 | You may obtain a copy of the License at 117 | 118 | http://www.apache.org/licenses/LICENSE-2.0 119 | 120 | Unless required by applicable law or agreed to in writing, software 121 | distributed under the License is distributed on an "AS IS" BASIS, 122 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 123 | See the License for the specific language governing permissions and 124 | limitations under the License. 125 | 126 | [dhall]: https://dhall-lang.org/ 127 | [pandoc]: https://pandoc.org/ 128 | [pandoc-crossref]: https://github.com/lierdakil/pandoc-crossref#installation 129 | [mactex]: https://www.tug.org/mactex/ 130 | [dhall-to-json]: https://github.com/dhall-lang/dhall-haskell/tree/master/dhall-json 131 | [release]: https://github.com/informalsystems/themis-contract/releases 132 | -------------------------------------------------------------------------------- /assets/pandoc/header-includes.tex: -------------------------------------------------------------------------------- 1 | %% Don't add visible rule lines to tables 2 | %% helpful for styling since we use tables for signature boxes 3 | \def\toprule{} 4 | \def\bottomrule{} 5 | \def\midrule{} -------------------------------------------------------------------------------- /assets/pandoc/include-before.tex: -------------------------------------------------------------------------------- 1 | %% Reduces the amount of space between the title headings and the 2 | %% start of the document content 3 | \thispagestyle{empty} 4 | \vspace*{-3em} -------------------------------------------------------------------------------- /assets/pandoc/pandoc-defaults.yaml: -------------------------------------------------------------------------------- 1 | from: markdown+fenced_divs+bracketed_spans 2 | 3 | # Use if h1 headings should not be numbered sections 4 | # shift-heading-levels-by: -1 5 | 6 | standalone: true 7 | self-contained: true 8 | 9 | # Enables explicit sections divs based on relative heading positions 10 | section-divs: true 11 | 12 | # Number sections 13 | number-sections: true 14 | 15 | # Custom latex styles to add to the template 16 | # each item in the list should be a file path 17 | include-in-header: ["header-includes.tex"] 18 | include-before-body: ["include-before.tex"] 19 | 20 | # Transformations over the pandoc AST 21 | filters: 22 | # See https://github.com/lierdakil/pandoc-crossref 23 | - pandoc-crossref 24 | 25 | metadata: 26 | # References to `#sec:foo` tags are rendered as "Section 1" 27 | secPrefix: ["Section", "Sections"] 28 | -------------------------------------------------------------------------------- /assets/templates/contract.dhall.tmpl: -------------------------------------------------------------------------------- 1 | {- 2 | Do not modify this file - it is automatically generated and managed by 3 | Themis Contract. Any changes may be automatically overwritten. 4 | -} 5 | 6 | let ThemisContract = https://raw.githubusercontent.com/informalsystems/themis-contract/master/config/package.dhall 7 | sha256:016b3829eaee279f2ce7a740a974f1ac75758893c42d220865a487c35ff9a890 8 | 9 | let contract : ThemisContract.Contract = 10 | { params = 11 | { location = "{{.ParamsFile.Location}}" 12 | , hash = "{{.ParamsFile.Hash}}" 13 | } 14 | , upstream = {{if .Upstream}}Some 15 | { location = "{{.Upstream.Location}}" 16 | , hash = "{{.Upstream.Hash}}" 17 | }{{else}}None ThemisContract.FileRef{{end}} 18 | , template = 19 | { format = ThemisContract.TemplateFormat.{{.Template.Format}} 20 | , file = 21 | { location = "{{.Template.File.Location}}" 22 | , hash = "{{.Template.File.Hash}}" 23 | } 24 | } 25 | } 26 | 27 | in contract -------------------------------------------------------------------------------- /cmd/themis-contract/compile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 7 | "github.com/rs/zerolog/log" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | flagOutput string 13 | ) 14 | 15 | func compileCmd() *cobra.Command { 16 | cmd := &cobra.Command{ 17 | Use: "compile [contract]", 18 | Short: "Compile a contract's sources to produce a PDF", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | contractPath := defaultContractPath 21 | if len(args) > 0 { 22 | contractPath = args[0] 23 | } 24 | c, err := contract.Load(contractPath, ctx) 25 | if err != nil { 26 | log.Error().Msgf("Failed to load contract: %s", err) 27 | os.Exit(1) 28 | } 29 | err = c.Compile(flagOutput, ctx) 30 | if err != nil { 31 | log.Error().Msgf("Failed to compile contract: %s", err) 32 | os.Exit(1) 33 | } 34 | log.Info().Msg("Successfully compiled contract") 35 | }, 36 | } 37 | cmd.PersistentFlags().StringVarP(&flagOutput, "output", "o", "contract.pdf", "where to write the output contract") 38 | return cmd 39 | } 40 | -------------------------------------------------------------------------------- /cmd/themis-contract/execute.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 7 | "github.com/rs/zerolog/log" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func executeCmd() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "execute [contract]", 14 | Short: "Shortcut to sign and compile a contract", 15 | Long: `Provides a simple shortcut to sign and compile a contract. If the output 16 | contract is stored in the same Git repository as the contract itself, Themis 17 | Contract will automatically try to commit and push the act of signing and the 18 | newly compiled contract.`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | contractPath := defaultContractPath 21 | if len(args) > 0 { 22 | contractPath = args[0] 23 | } 24 | c, err := contract.Load(contractPath, ctx) 25 | if err != nil { 26 | log.Error().Msgf("Failed to load contract: %s", err) 27 | os.Exit(1) 28 | } 29 | err = c.Execute(flagSigId, flagOutput, ctx) 30 | if err != nil { 31 | log.Error().Msgf("Failed to compile contract: %s", err) 32 | os.Exit(1) 33 | } 34 | log.Info().Msg("Successfully executed contract") 35 | }, 36 | } 37 | cmd.PersistentFlags().StringVar(&flagSigId, "as", "", "the ID of the signatory on behalf of whom you want to sign") 38 | cmd.PersistentFlags().StringVarP(&flagOutput, "output", "o", "contract.pdf", "where to write the output contract") 39 | return cmd 40 | } 41 | -------------------------------------------------------------------------------- /cmd/themis-contract/list_signatories.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 7 | "github.com/rs/zerolog/log" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func listSignatoriesCmd() *cobra.Command { 12 | return &cobra.Command{ 13 | Use: "list-signatories [contract]", 14 | Short: "List all signatories' details for a given contract", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | contractPath := defaultContractPath 17 | if len(args) > 0 { 18 | contractPath = args[0] 19 | } 20 | c, err := contract.Load(contractPath, ctx) 21 | if err != nil { 22 | log.Error().Err(err).Msg("Failed to load contract") 23 | os.Exit(1) 24 | } 25 | sigs := c.Signatories() 26 | if len(sigs) == 0 { 27 | log.Error().Msg("No signatories in contract (there should be at least one)") 28 | // this should not happen 29 | os.Exit(1) 30 | } 31 | log.Info().Msg("Signatories:") 32 | for _, sig := range sigs { 33 | log.Info().Msgf("- %s (id: %s, e-mail: %s)", sig.Name, sig.Id, sig.Email) 34 | } 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/themis-contract/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | cmd, err := rootCmd() 10 | if err != nil { 11 | fmt.Printf("Failed to initialize CLI: %e\n", err) 12 | os.Exit(1) 13 | } 14 | if err := cmd.Execute(); err != nil { 15 | fmt.Printf("Failed to execute CLI: %s\n", err) 16 | os.Exit(1) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cmd/themis-contract/new.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 7 | "github.com/rs/zerolog/log" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var flagGitRemote string 12 | 13 | func newCmd() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "new [upstream] [output]", 16 | Short: "Create a new contract", 17 | Long: "Create a new contract, using the specified upstream contract effectively as a template", 18 | Args: cobra.MinimumNArgs(1), 19 | Run: func(cmd *cobra.Command, args []string) { 20 | contractPath := defaultContractPath 21 | if len(args) > 1 { 22 | contractPath = args[1] 23 | } 24 | if _, err := contract.New(contractPath, args[0], flagGitRemote, ctx); err != nil { 25 | log.Error().Err(err).Msg("Failed to create new contract") 26 | os.Exit(1) 27 | } 28 | log.Info().Msg("Successfully created new contract") 29 | }, 30 | } 31 | cmd.PersistentFlags().StringVar(&flagGitRemote, "git-remote", "", "assuming you're creating a new repo for your contract, the URL of the Git remote") 32 | return cmd 33 | } 34 | -------------------------------------------------------------------------------- /cmd/themis-contract/profile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 9 | "github.com/rs/zerolog/log" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | flagProfileSigID string 15 | flagProfileContractsRepo string 16 | flagProfileID string 17 | ) 18 | 19 | func profileCmd() *cobra.Command { 20 | cmd := &cobra.Command{ 21 | Use: "profile", 22 | Aliases: []string{"profiles"}, 23 | Short: "Profile management", 24 | } 25 | cmd.AddCommand( 26 | profileListCmd(), 27 | profileUseCmd(), 28 | profileAddCmd(), 29 | profileRemoveCmd(), 30 | profileRenameCmd(), 31 | profileSetCmd(), 32 | profileContractsCmd(), 33 | ) 34 | return cmd 35 | } 36 | 37 | func profileListCmd() *cobra.Command { 38 | return &cobra.Command{ 39 | Use: "list", 40 | Aliases: []string{"ls"}, 41 | Short: "List existing profiles", 42 | Run: func(cmd *cobra.Command, args []string) { 43 | profiles := ctx.Profiles() 44 | if len(profiles) == 0 { 45 | log.Info().Msgf("No profiles configured yet. Use \"themis-contract profile add\" to add one.") 46 | return 47 | } 48 | activeProfile := ctx.ActiveProfile() 49 | if activeProfile == nil { 50 | log.Info().Msg("No active profile currently. Use \"themis-contract use\" to set one.") 51 | } else { 52 | log.Info().Msgf("Currently active profile: %s", activeProfile.ID()) 53 | } 54 | log.Info().Msgf("%d profile(s) available:", len(profiles)) 55 | for _, profile := range profiles { 56 | act := " " 57 | if activeProfile != nil && activeProfile.ID() == profile.ID() { 58 | act = "> " 59 | } 60 | log.Info().Msgf("%s%s", act, profile.Display()) 61 | } 62 | }, 63 | } 64 | } 65 | 66 | func profileUseCmd() *cobra.Command { 67 | return &cobra.Command{ 68 | Use: "use [id]", 69 | Short: "Switch to using a specific profile", 70 | Long: "Switch to using the profile with the specified ID", 71 | Args: cobra.ExactArgs(1), 72 | Run: func(cmd *cobra.Command, args []string) { 73 | profile, err := ctx.UseProfile(args[0]) 74 | if err != nil { 75 | log.Error().Msgf("Failed to switch to profile \"%s\": %s", args[0], err) 76 | os.Exit(1) 77 | } 78 | log.Info().Msgf("Switched to profile: %s", profile.Display()) 79 | }, 80 | } 81 | } 82 | 83 | func profileAddCmd() *cobra.Command { 84 | cmd := &cobra.Command{ 85 | Use: "add [name]", 86 | Short: "Add a new profile", 87 | Long: "Add a new profile with the given name", 88 | Run: func(cmd *cobra.Command, args []string) { 89 | profile, err := ctx.AddProfile(args[0], flagProfileSigID, flagProfileContractsRepo) 90 | if err != nil { 91 | log.Error().Msgf("Failed to add new profile: %s", err) 92 | os.Exit(1) 93 | } 94 | log.Info().Msgf("Added profile: %s", profile.Display()) 95 | // if we only have one new profile now, try to make it the default 96 | if len(ctx.Profiles()) == 1 { 97 | log.Info().Msgf("Automatically choosing profile \"%s\" as currently active, since it's the only profile", profile.ID()) 98 | if _, err := ctx.UseProfile(profile.ID()); err != nil { 99 | log.Error().Msgf("Failed to select profile \"%s\" as active: %s", profile.ID(), err) 100 | } 101 | } 102 | }, 103 | } 104 | cmd.PersistentFlags().StringVar(&flagProfileSigID, "sig-id", "", "optionally specify a signature ID to use") 105 | cmd.PersistentFlags().StringVar(&flagProfileContractsRepo, "contracts-repo", "", "optionally specify a repository for all contracts for this profile") 106 | return cmd 107 | } 108 | 109 | func profileRemoveCmd() *cobra.Command { 110 | return &cobra.Command{ 111 | Use: "remove [id]", 112 | Aliases: []string{"rm", "del"}, 113 | Short: "Remove a profile", 114 | Long: "Remove the profile with the given ID", 115 | Args: cobra.ExactArgs(1), 116 | Run: func(cmd *cobra.Command, args []string) { 117 | if err := ctx.RemoveProfile(args[0]); err != nil { 118 | log.Error().Msgf("Failed to remove profile \"%s\": %s", args[0], err) 119 | os.Exit(1) 120 | } 121 | log.Info().Msgf("Successfully removed profile with ID \"%s\"", args[0]) 122 | }, 123 | } 124 | } 125 | 126 | func profileRenameCmd() *cobra.Command { 127 | return &cobra.Command{ 128 | Use: "rename [src-id] [dest-name]", 129 | Aliases: []string{"mv", "ren"}, 130 | Short: "Rename a profile", 131 | Long: `Rename the profile with the given ID to have the specified name (the new ID 132 | will automatically be derived from the name)`, 133 | Args: cobra.ExactArgs(2), 134 | Run: func(cmd *cobra.Command, args []string) { 135 | if err := ctx.RenameProfile(args[0], args[1]); err != nil { 136 | log.Error().Msgf("Failed to rename profile \"%s\": %s", args[0], err) 137 | os.Exit(1) 138 | } 139 | log.Info().Msgf("Successfully renamed profile with ID \"%s\" to \"%s\"", args[0], args[1]) 140 | }, 141 | } 142 | } 143 | 144 | func profileSetCmd() *cobra.Command { 145 | cmd := &cobra.Command{ 146 | Use: "set [param] [value]", 147 | Short: "Set a profile parameter value", 148 | Long: fmt.Sprintf(` 149 | Set a specific profile parameter to the given value. If no profile ID is 150 | supplied, the currently active profile's parameter will be set. 151 | 152 | Valid profile parameter names include: %s`, strings.Join(contract.ValidProfileParamNames(), ", ")), 153 | Args: cobra.ExactArgs(2), 154 | Run: func(cmd *cobra.Command, args []string) { 155 | profile := ctx.ActiveProfile() 156 | if len(flagProfileID) > 0 { 157 | var err error 158 | profile, err = ctx.GetProfileByID(flagProfileID) 159 | if err != nil { 160 | log.Error().Msgf("Failed to load profile \"%s\": %s", flagProfileID, err) 161 | os.Exit(1) 162 | } 163 | } 164 | if err := ctx.SetProfileParam(profile, args[0], args[1]); err != nil { 165 | log.Error().Msgf("Failed to set parameter \"%s\" for profile \"%s\": %s", args[0], profile.ID(), err) 166 | os.Exit(1) 167 | } 168 | if err := profile.Save(); err != nil { 169 | log.Error().Msgf("Failed to save profile \"%s\": %s", profile.ID(), err) 170 | os.Exit(1) 171 | } 172 | log.Info().Msgf("Successfully updated profile \"%s\" (ID: \"%s\")", profile.Name, profile.ID()) 173 | }, 174 | } 175 | cmd.PersistentFlags().StringVar(&flagProfileID, "id", "", "the profile ID whose parameter is to be set") 176 | return cmd 177 | } 178 | 179 | func profileContractsCmd() *cobra.Command { 180 | cmd := &cobra.Command{ 181 | Use: "contracts", 182 | Short: "Manage profile-specific contracts", 183 | } 184 | cmd.AddCommand( 185 | profileContractsListCmd(), 186 | profileContractsSyncCmd(), 187 | ) 188 | return cmd 189 | } 190 | 191 | func profileContractsListCmd() *cobra.Command { 192 | return &cobra.Command{ 193 | Use: "list [profile-id]", 194 | Aliases: []string{"ls"}, 195 | Short: "List cached contracts for a profile", 196 | Long: `List cached contracts for a profile (if no profile ID is supplied, the cached 197 | contracts for the currently active profile will be listed)`, 198 | Run: func(cmd *cobra.Command, args []string) { 199 | var err error 200 | profile := ctx.ActiveProfile() 201 | if len(args) > 0 { 202 | profile, err = ctx.GetProfileByID(args[0]) 203 | if err != nil { 204 | log.Error().Msgf("Failed to get profile with ID \"%s\": %s", args[0], err) 205 | os.Exit(1) 206 | } 207 | } 208 | listProfileContracts(profile) 209 | }, 210 | } 211 | } 212 | 213 | func profileContractsSyncCmd() *cobra.Command { 214 | return &cobra.Command{ 215 | Use: "sync [profile-id]", 216 | Short: "Sync cached contracts for a profile", 217 | Long: `Sync cached contracts for a profile (if no profile ID is supplied, the cached 218 | contracts for the currently active profile will be used)`, 219 | Run: func(cmd *cobra.Command, args []string) { 220 | var err error 221 | profile := ctx.ActiveProfile() 222 | if len(args) > 0 { 223 | profile, err = ctx.GetProfileByID(args[0]) 224 | if err != nil { 225 | log.Error().Msgf("Failed to get profile with ID \"%s\": %s", args[0], err) 226 | os.Exit(1) 227 | } 228 | } 229 | if len(profile.ContractsRepo) == 0 { 230 | log.Error().Msgf("No contracts repository configured for profile \"%s\" - please set one first", profile.ID()) 231 | os.Exit(1) 232 | } 233 | log.Info().Msgf("Synchronizing contracts repo \"%s\" for profile \"%s\"...", profile.ContractsRepo, profile.ID()) 234 | if err := profile.SyncContractsRepo(ctx); err != nil { 235 | log.Error().Msgf("Failed to sync contracts repo for profile \"%s\": %s", profile.ID(), err) 236 | os.Exit(1) 237 | } 238 | listProfileContracts(profile) 239 | }, 240 | } 241 | } 242 | 243 | func listProfileContracts(profile *contract.Profile) { 244 | if len(profile.Contracts) == 0 { 245 | log.Info().Msgf("No contracts for profile \"%s\"", profile.ID()) 246 | os.Exit(0) 247 | } 248 | log.Info().Msgf("Contracts for profile \"%s\":", profile.ID()) 249 | for _, c := range profile.Contracts { 250 | log.Info().Msgf("- %s: %s", c.ID, c.URL()) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /cmd/themis-contract/review.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //func reviewCmd() *cobra.Command { 4 | // cmd := &cobra.Command{ 5 | // Use: "review [contract-url]", 6 | // Args: cobra.ExactArgs(1), 7 | // Short: "Fetch a pre-existing contract for local review", 8 | // Long: `The review command allows you to fetch a contract from an existing Git 9 | //repository for local review. It clones the remote repository locally. If you 10 | //have already cloned a repository using the "review" command, rather use the 11 | //"update" command for future updates.`, 12 | // Run: func(cmd *cobra.Command, args []string) { 13 | // c, err := contract.Review(args[0]) 14 | // if err != nil { 15 | // log.Error().Msgf("Failed to acquire remote contract for review: %s", err) 16 | // os.Exit(1) 17 | // } 18 | // log.Info().Msgf("Successfully acquired remote contract here: %s", c) 19 | // }, 20 | // } 21 | // return cmd 22 | //} 23 | -------------------------------------------------------------------------------- /cmd/themis-contract/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "path" 7 | 8 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/log" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | const defaultContractPath = "contract.dhall" 15 | 16 | var ( 17 | flagVerbose bool 18 | flagHome string 19 | flagNoAutoCommit bool 20 | flagNoAutoPush bool 21 | 22 | ctx *contract.Context 23 | ) 24 | 25 | func defaultThemisContractHome() (string, error) { 26 | usr, err := user.Current() 27 | if err != nil { 28 | return "", err 29 | } 30 | return path.Join(usr.HomeDir, ".themis", "contract"), nil 31 | } 32 | 33 | func rootCmd() (*cobra.Command, error) { 34 | home, err := defaultThemisContractHome() 35 | if err != nil { 36 | return nil, err 37 | } 38 | cmd := &cobra.Command{ 39 | Use: "themis-contract", 40 | Short: "Themis Contract is a tool to help with parameterized legal contracting", 41 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 42 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}) 43 | level := zerolog.InfoLevel 44 | if flagVerbose { 45 | level = zerolog.DebugLevel 46 | } 47 | zerolog.SetGlobalLevel(level) 48 | log.Debug().Msg("Increasing output verbosity to debug level") 49 | 50 | ctx, err = contract.InitContext(flagHome, !flagNoAutoCommit, !flagNoAutoPush) 51 | if err != nil { 52 | log.Error().Msgf("Failed to initialize context: %s", err) 53 | os.Exit(1) 54 | } 55 | }, 56 | } 57 | cmd.PersistentFlags().BoolVar(&flagNoAutoCommit, "no-auto-commit", false, "do not attempt to automatically commit changes to contracts to their parent Git repository") 58 | cmd.PersistentFlags().BoolVar(&flagNoAutoPush, "no-auto-push", false, "do not attempt to automatically push changes to contracts to their remote Git repository") 59 | cmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "increase output logging verbosity") 60 | cmd.PersistentFlags().StringVar(&flagHome, "home", home, "path to the root of your Themis Contract configuration directory") 61 | cmd.AddCommand( 62 | newCmd(), 63 | compileCmd(), 64 | listSignatoriesCmd(), 65 | signCmd(), 66 | updateCmd(), 67 | profileCmd(), 68 | signatureCmd(), 69 | executeCmd(), 70 | upstreamCmd(), 71 | //reviewCmd(), 72 | versionCmd(), 73 | ) 74 | return cmd, nil 75 | } 76 | -------------------------------------------------------------------------------- /cmd/themis-contract/sign.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 7 | "github.com/rs/zerolog/log" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var flagSigId string 12 | 13 | func signCmd() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "sign [contract]", 16 | Short: "Sign a contract", 17 | Long: "Sign a contract, optionally specifying the signatory as whom you want to sign", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | contractPath := defaultContractPath 20 | if len(args) > 0 { 21 | contractPath = args[0] 22 | } 23 | c, err := contract.Load(contractPath, ctx) 24 | if err != nil { 25 | log.Error().Msgf("Failed to load contract: %s", err) 26 | os.Exit(1) 27 | } 28 | err = c.Sign(flagSigId, ctx) 29 | if err != nil { 30 | log.Error().Msgf("Failed to sign contract: %s", err) 31 | os.Exit(1) 32 | } 33 | log.Info().Msg("Successfully signed contract") 34 | }, 35 | } 36 | cmd.PersistentFlags().StringVar(&flagSigId, "as", "", "the ID of the signatory on behalf of whom you want to sign") 37 | return cmd 38 | } 39 | -------------------------------------------------------------------------------- /cmd/themis-contract/signature.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 9 | "github.com/rs/zerolog/log" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func signatureCmd() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "signature", 16 | Aliases: []string{"signatures", "sig", "sigs"}, 17 | Short: "Signature management", 18 | } 19 | cmd.AddCommand( 20 | signatureListCmd(), 21 | signatureAddCmd(), 22 | signatureRemoveCmd(), 23 | signatureRenameCmd(), 24 | signatureSetCmd(), 25 | ) 26 | return cmd 27 | } 28 | 29 | func signatureListCmd() *cobra.Command { 30 | return &cobra.Command{ 31 | Use: "list", 32 | Aliases: []string{"ls"}, 33 | Short: "List existing signatures", 34 | Run: func(cmd *cobra.Command, args []string) { 35 | sigs, err := ctx.Signatures() 36 | if err != nil { 37 | log.Error().Msgf("Failed to load signatures: %s", err) 38 | os.Exit(1) 39 | } 40 | if len(sigs) == 0 { 41 | log.Info().Msgf("No signatures configured yet. Use \"themis-contract signature add\" to add one.") 42 | return 43 | } 44 | log.Info().Msgf("%d signatures(s) available:", len(sigs)) 45 | for _, sig := range sigs { 46 | log.Info().Msgf("- %s", sig.Display()) 47 | } 48 | }, 49 | } 50 | } 51 | 52 | func signatureAddCmd() *cobra.Command { 53 | return &cobra.Command{ 54 | Use: "add [name] [email] [image]", 55 | Short: "Add a new signature", 56 | Long: `Add a new signature with the given name and e-mail address, and copy the 57 | specified image to use when signing contracts`, 58 | Args: cobra.ExactArgs(3), 59 | Run: func(cmd *cobra.Command, args []string) { 60 | sig, err := ctx.AddSignature(args[0], args[1], args[2]) 61 | if err != nil { 62 | log.Error().Msgf("Failed to add new signature: %s", err) 63 | os.Exit(1) 64 | } 65 | log.Info().Msgf("Added signature: %s", sig.Display()) 66 | }, 67 | } 68 | } 69 | 70 | func signatureRemoveCmd() *cobra.Command { 71 | return &cobra.Command{ 72 | Use: "remove [id]", 73 | Aliases: []string{"rm", "del"}, 74 | Short: "Remove a signature", 75 | Long: "Remove the signature with the given ID", 76 | Args: cobra.ExactArgs(1), 77 | Run: func(cmd *cobra.Command, args []string) { 78 | if err := ctx.RemoveSignature(args[0]); err != nil { 79 | log.Error().Msgf("Failed to remove signature \"%s\": %s", args[0], err) 80 | os.Exit(1) 81 | } 82 | log.Info().Msgf("Successfully removed signature with ID \"%s\"", args[0]) 83 | }, 84 | } 85 | } 86 | 87 | func signatureRenameCmd() *cobra.Command { 88 | return &cobra.Command{ 89 | Use: "rename [src-id] [dest-name]", 90 | Aliases: []string{"mv", "ren"}, 91 | Short: "Rename a signature", 92 | Long: `Rename the signature with the given ID to have the specified name (the new ID 93 | will automatically be derived from the name)`, 94 | Args: cobra.ExactArgs(2), 95 | Run: func(cmd *cobra.Command, args []string) { 96 | if err := ctx.RenameSignature(args[0], args[1]); err != nil { 97 | log.Error().Msgf("Failed to rename signature \"%s\": %s", args[0], err) 98 | os.Exit(1) 99 | } 100 | log.Info().Msgf("Successfully renamed signature with ID \"%s\" to \"%s\"", args[0], args[1]) 101 | }, 102 | } 103 | } 104 | 105 | func signatureSetCmd() *cobra.Command { 106 | return &cobra.Command{ 107 | Use: "set [id] [param] [value]", 108 | Short: "Set a signature parameter value", 109 | Long: fmt.Sprintf(`Set a specific signature parameter to the given value 110 | 111 | Valid signature parameter names include: %s`, strings.Join(contract.ValidSignatureParamNames(), ", ")), 112 | Args: cobra.ExactArgs(3), 113 | Run: func(cmd *cobra.Command, args []string) { 114 | sig, err := ctx.GetSignatureByID(args[0]) 115 | if err != nil { 116 | log.Error().Msgf("Failed to load signature \"%s\": %s", args[0], err) 117 | os.Exit(1) 118 | } 119 | if err := ctx.SetSignatureParam(sig, args[1], args[2]); err != nil { 120 | log.Error().Msgf("Failed to set parameter \"%s\" for signature \"%s\": %s", args[1], args[0], err) 121 | os.Exit(1) 122 | } 123 | if err := sig.Save(); err != nil { 124 | log.Error().Msgf("Failed to save signature \"%s\": %s", args[0], err) 125 | os.Exit(1) 126 | } 127 | log.Info().Msgf("Successfully updated signature \"%s\"", sig.Name) 128 | }, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /cmd/themis-contract/update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 7 | "github.com/rs/zerolog/log" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func updateCmd() *cobra.Command { 12 | return &cobra.Command{ 13 | Use: "update [contract]", 14 | Short: "Update a contract's parameters/template files' hashes", 15 | Long: "Automatically refreshes the hashes of the parameters and/or template files", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | contractPath := defaultContractPath 18 | if len(args) > 0 { 19 | contractPath = args[0] 20 | } 21 | if err := contract.Update(contractPath, ctx); err != nil { 22 | log.Error().Err(err).Msg("Failed to load contract") 23 | os.Exit(1) 24 | } 25 | log.Info().Msg("Successfully updated contract") 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cmd/themis-contract/upstream.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | contract "github.com/informalsystems/themis-contract/pkg/themis-contract" 7 | "github.com/rs/zerolog/log" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var flagDiffProg string 12 | 13 | func upstreamCmd() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "upstream", 16 | Short: "Comparing a contract to its upstream", 17 | } 18 | cmd.AddCommand( 19 | upstreamDiffCmd(), 20 | ) 21 | return cmd 22 | } 23 | 24 | func upstreamDiffCmd() *cobra.Command { 25 | cmd := &cobra.Command{ 26 | Use: "diff [contract]", 27 | Short: "Show a diff of a contract compared to its upstream", 28 | Long: `Show a diff of the various components of a contract compared to its upstream. 29 | This will show differences between both the parameters of the upstream and the 30 | template used in the upstream contract.`, 31 | Run: func(cmd *cobra.Command, args []string) { 32 | contractPath := defaultContractPath 33 | if len(args) > 0 { 34 | contractPath = args[0] 35 | } 36 | c, err := contract.Load(contractPath, ctx) 37 | if err != nil { 38 | log.Error().Msgf("Failed to load contract: %s", err) 39 | os.Exit(1) 40 | } 41 | diff, err := c.UpstreamDiff(flagDiffProg, ctx) 42 | if err != nil { 43 | log.Error().Msgf("Upstream diff failed: %s", err) 44 | os.Exit(1) 45 | } 46 | if len(diff.ParamsDiff) == 0 { 47 | log.Info().Msgf("Parameters files are identical") 48 | } else { 49 | log.Info().Msgf("Comparing our parameters (left) to upstream (right):\n%s\n", diff.ParamsDiff) 50 | } 51 | if len(diff.TemplateDiff) == 0 { 52 | log.Info().Msgf("Template files are identical") 53 | } else { 54 | log.Info().Msgf("Comparing our template (left) to upstream (right):\n%s\n", diff.TemplateDiff) 55 | } 56 | }, 57 | } 58 | cmd.PersistentFlags().StringVar(&flagDiffProg, "diff-prog", "diff", "the program to use to perform the diff (try colordiff too)") 59 | return cmd 60 | } 61 | -------------------------------------------------------------------------------- /cmd/themis-contract/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var version string 10 | 11 | func versionCmd() *cobra.Command { 12 | return &cobra.Command{ 13 | Use: "version", 14 | Short: "Show the current version of Themis Contract", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | fmt.Printf("Themis Contract %s\n", version) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/Contract.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | A contract, conceptually, has an optional reference to the upstream contract 3 | from which it was derived, as well as the template for its content. 4 | -} 5 | 6 | let Template = ./Template.dhall 7 | let FileRef = ./FileRef.dhall 8 | 9 | let Contract : Type = 10 | { params : FileRef 11 | , template : Template 12 | , upstream : Optional FileRef 13 | } 14 | 15 | in Contract 16 | -------------------------------------------------------------------------------- /config/FileRef.dhall: -------------------------------------------------------------------------------- 1 | let FileRef : Type = 2 | { location : Text 3 | , hash : Text 4 | } 5 | 6 | in FileRef 7 | -------------------------------------------------------------------------------- /config/Signatory.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | A signatory, in contracting terms, is typically a natural person acting 3 | on behalf of either themselves or an organization. 4 | -} 5 | 6 | let Signatory : Type = 7 | { id : Text -- An identifier for this signatory unique in the contract (must be camelCase or snake_case). 8 | , name : Text -- A human-readable name for this signatory. 9 | , email : Text -- An e-mail address for this signatory. 10 | } 11 | 12 | in Signatory 13 | -------------------------------------------------------------------------------- /config/Template.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | A contract content template is a text file that contains the actual wording 3 | of most of the contract, but is parameterized according to the configured 4 | templating language. 5 | -} 6 | 7 | let TemplateFormat = ./TemplateFormat.dhall 8 | let FileRef = ./FileRef.dhall 9 | 10 | let Template : Type = 11 | { format : TemplateFormat 12 | , file : FileRef 13 | } 14 | 15 | in Template 16 | -------------------------------------------------------------------------------- /config/TemplateFormat.dhall: -------------------------------------------------------------------------------- 1 | let TemplateFormat : Type = 2 | < Mustache | Dhall > 3 | 4 | in TemplateFormat 5 | -------------------------------------------------------------------------------- /config/package.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Common configuration-related definitions used in Themis Contract-related 3 | contracts. 4 | -} 5 | 6 | { Contract = ./Contract.dhall 7 | , Signatory = ./Signatory.dhall 8 | , Template = ./Template.dhall 9 | , TemplateFormat = ./TemplateFormat.dhall 10 | , FileRef = ./FileRef.dhall 11 | } 12 | -------------------------------------------------------------------------------- /docs/01-setup.md: -------------------------------------------------------------------------------- 1 | # Setting up Themis Contract 2 | 3 | This brief tutorial assumes you have already successfully installed Themis 4 | Contract and its dependencies. 5 | 6 | Themis Contract assumes that you may want to sign contracts on behalf of one 7 | or more counterparties. For example, you may want to sign contracts in your 8 | personal capacity, but you also may want to sign contracts on behalf of your 9 | company. 10 | 11 | To allow for this, Themis Contract allows you to create **signatures**, where 12 | each signature can be attached to one or more **profiles**. 13 | 14 | Running through an example of setting up your first signature and profile will 15 | help to illustrate the distinction and potential usefulness of these different 16 | concepts. 17 | 18 | ## Step 1: Create a signature 19 | 20 | To set up your first signature, make sure you have an image handy that contains 21 | the signature you want to apply to contracts that you sign. To do this, you 22 | could use an image editing program or your could scan or take a photo of your 23 | handwritten signature. 24 | 25 | *Note: in future, we aim to introduce rendered signatures based on a 26 | user-selected font, or even cryptographic signatures contained in barcodes. 27 | These features are not yet supported.* 28 | 29 | Let's say this image is located at `~/Documents/signature.png`. And you name is 30 | *Marwah Sparrow*. 31 | 32 | ```bash 33 | # Add a signature with the id "msparrow" associated with e-mail address 34 | # "marwah@email.com", using your custom, handwritten signature. 35 | themis-contract signatures add \ 36 | msparrow \ 37 | marwah@email.com \ 38 | ~/Documents/signature.png 39 | 40 | # Now list all available signatures to see the newly added one 41 | themis-contract signatures list 42 | 43 | # Shorthand version of the above command 44 | themis-contract sigs ls 45 | ``` 46 | 47 | This data is stored in the Themis Contracts home folder, which, by default, 48 | is located at `~/.themis/contract/`. It's stored in plain text files, and your 49 | signature image is copied across, so you could potentially commit Themis 50 | Contract's data to a Git repository for backup purposes. 51 | 52 | For the above signature specifically, look in 53 | `~/.themis/contract/signatures/msparrow` for its contents. 54 | 55 | ## Step 2: Create a profile 56 | 57 | Profiles allow you to group certain information to facilitate quicker signing 58 | of contracts. At any given point in time when using Themis Contract, a profile 59 | must be active. Profiles additionally allow you to group the following 60 | information: 61 | 62 | * Which signature must be used when signing with that profile active? 63 | * Does this profile have a contract templates Git repository associated with it? 64 | If so, where is this located? 65 | * Formatting customizations to apply when compiling the contract using this 66 | specific profile. 67 | 68 | ```bash 69 | # Add a profile called "Personal" (we won't add a contract templates repo yet - 70 | # we'll get to that in another tutorial) 71 | themis-contract profile add \ 72 | Personal \ 73 | --sig-id msparrow 74 | 75 | # List all profiles 76 | themis-contract profile ls 77 | 78 | # Activate your "Personal" profile for subsequent Themis Contract commands 79 | themis-contract profile use personal 80 | ``` 81 | 82 | Again, take a look at `~/.themis/contract/profiles/personal` for the metadata 83 | that Themis Contract stores regarding your new profile. 84 | 85 | ## Next Steps 86 | 87 | Once you've created a signature and a profile, you're ready to get started with 88 | [creating your first contract](02-first-contract.md). 89 | -------------------------------------------------------------------------------- /docs/02-first-contract.md: -------------------------------------------------------------------------------- 1 | # Your First Contract 2 | 3 | Once you've completed the [setup](01-setup.md) tutorial and have a signature 4 | and profile ready to use, you can now get started on crafting your first 5 | contract. 6 | 7 | You probably already have your contract text available in some format. For 8 | simplicity's sake, we'll assume for now that your contract text is written in 9 | Markdown (or that you've used [pandoc] to convert your contract text to 10 | Markdown). 11 | 12 | [Example files](../examples/service-agreement/) are included in the repository 13 | for this basic tutorial. You can also consult our [formatting 14 | guide](./04-formatting.md) for useful tips on markdown syntax for representing 15 | contracts. 16 | 17 | ## Step 1: Create a local Git repo for your contract 18 | 19 | Themis Contract assumes that all the files relating to a contract are stored 20 | within the same folder. Themis Contract also wraps interaction with Git to 21 | automatically push/pull changes to your contract. 22 | 23 | ```bash 24 | cd /path/to/working/dir 25 | mkdir my-first-contract 26 | cd my-first-contract 27 | 28 | # We ensure the contract's repository's initialized as a Git repository 29 | git init 30 | git remote add origin git@github.com/you/my-first-contract.git 31 | ``` 32 | 33 | ## Step 2: Get your template ready 34 | 35 | ```bash 36 | # Copy your template text to the repo folder 37 | cp /path/to/contract/template.md . 38 | ``` 39 | 40 | Now you need to parameterize your template. Themis Contract uses 41 | [Mustache][mustache] as its template language due to its simplicity (and if 42 | you need any complexity in terms of calculations or inclusion of external 43 | data, you can rather define this in your contract parameters - we'll get to this 44 | later in this tutorial). 45 | 46 | Let's use the following as an example `template.md` file, which has already 47 | been parameterized: 48 | 49 | ```markdown 50 | # Service Agreement 51 | 52 | This service agreement is agreed upon by the following parties: 53 | 54 | * {{client.name}}, located at {{client.address}} 55 | * {{supplier.name}}, located at {{supplier.address}} 56 | 57 | ## Terms of Service 58 | 59 | 1. Some 60 | 2. Terms 61 | 3. Will 62 | 4. Go 63 | 5. Here 64 | 65 | ## Hourly Rate 66 | 67 | The hourly rate will be {{supplier.currency}} {{supplier.hourlyRate}}. 68 | 69 | ## Signatures 70 | 71 | Signed, 72 | 73 | {{#signatories}} 74 | ![Signature]({{signature}}){width=200px} \ 75 | {{name}} 76 | 77 | Signed on: {{signed_date}} 78 | 79 | {{/signatories}} 80 | ``` 81 | 82 | You'll see here that a number of Mustache variables (in `{{curlybraces}}`) have 83 | been set up in this template. When compiling your contract to a PDF, Themis 84 | Contract will attempt to inject values for these variables into your template 85 | prior to rendering the PDF. 86 | 87 | 88 | Note the addition of the `width` [link attribute][link-attributes] giving us 89 | fine grained control over the size of the signature image. 90 | 91 | Where do these values come from? They come from your *parameters file(s)*. 92 | 93 | [link-attributes]: https://pandoc.org/MANUAL.html#extension-link_attributes 94 | 95 | ## Step 3: Set up your parameters 96 | 97 | We generally recommend that you set up your contract's parameters using 98 | [Dhall][dhall], but Themis Contract also supports JSON, YAML and TOML-based 99 | parameters files. 100 | 101 | Here's an example parameters file, in Dhall, for the contract template used in 102 | step 2: 103 | 104 | ```dhall 105 | {- 106 | A dummy service agreement contract for services to be rendered by the 107 | supplier to the client. 108 | -} 109 | 110 | let Signatory : Type = 111 | { id : Text 112 | , name : Text 113 | , email : Text 114 | } 115 | 116 | let Counterparty : Type = 117 | { id : Text 118 | , name : Text 119 | , address : Text 120 | , currency : Optional Text 121 | , hourlyRate : Optional Natural 122 | } 123 | 124 | let client : Counterparty = 125 | { id = "client" 126 | , name = "Client Org" 127 | , address = "(Client address goes here)" 128 | , currency = None Text 129 | , hourlyRate = None Natural 130 | } 131 | 132 | let supplier : Counterparty = 133 | { id = "supplier" 134 | , name = "Bronwyn Savvy (Freelancer)" 135 | , address = "(Bronwyn Savvy's address goes here)" 136 | , currency = Some "EUR" 137 | , hourlyRate = Some 50 138 | } 139 | 140 | {- 141 | We need to expose a signatory list of this format so that Themis Contract 142 | can understand who needs to sign the contract. 143 | -} 144 | let signatories : List Signatory = 145 | [ { id = "manderson" 146 | , name = "Michael Anderson" 147 | , email = "manderson@somewhere.com" 148 | } 149 | , { id = "bsavvy" 150 | , name = "Bronwyn Savvy" 151 | , email = "bronwyn@savvy.com" 152 | } 153 | ] 154 | 155 | {- 156 | All of the parameters we want to expose during template rendering. 157 | -} 158 | in { client = client 159 | , supplier = supplier 160 | , signatories = signatories 161 | } 162 | ``` 163 | 164 | There's only one mandatory field that you need to expose for Themis Contract 165 | to be able to compile your contract, and that's the `signatories` field. This 166 | field needs to be an **array**, where each item, at minimum, needs the following 167 | fields: 168 | 169 | * `id` - A unique identifier to associate with this signatory. This should be 170 | lowercase, ideally using `snake_case`. 171 | * `email` - An e-mail address to associate with this signatory. 172 | * `name` - A human-readable name for this signatory. Usually the signatory's 173 | full names. 174 | 175 | You can define additional fields here, and they will be passed through to your 176 | template when rendering the template. 177 | 178 | ## Step 4: Configure your contract 179 | 180 | In Themis Contract terminology, a "contract", technically, is a single 181 | Dhall file that exposes 3 parameters: 182 | 183 | * `params` - Details of your parameters file. This field must additionally 184 | contain sub-fields: 185 | * `location` - Where to find your parameters file. 186 | * `hash` - The SHA256 hash of the parameters file (an integrity check). 187 | * `template` - Details of your template file. This field must additionally 188 | contain sub-fields: 189 | * `format` - By default we use Mustache as our templating language. 190 | * `file` - Details of the template file itself: 191 | * `location` - Where to find the template file. 192 | * `hash` - The SHA256 hash of your template file (another integrity check). 193 | * `upstream` - If your contract was derived from another contract, this field 194 | should contains details of the upstream contract. This will allow you to see 195 | the differences between your contract template/parameters and those of the 196 | upstream contract. In our case here we won't have an upstream contract. 197 | 198 | Here is an example contract that ties together our template from step 2 and our 199 | parameters file from step 3, which you can save as `contract.dhall`: 200 | 201 | ```dhall 202 | {- 203 | Do not modify this file - it is automatically generated and managed by 204 | Themis Contract. Any changes may be automatically overwritten. 205 | -} 206 | 207 | let ThemisContract = https://raw.githubusercontent.com/informalsystems/themis-contract/master/config/package.dhall 208 | sha256:016b3829eaee279f2ce7a740a974f1ac75758893c42d220865a487c35ff9a890 209 | 210 | let contract : ThemisContract.Contract = 211 | { params = 212 | { location = "params.dhall" 213 | , hash = "4cbd373af2669e5c5fc5ffc7ecd02abc16aa8fc0855f1de712a7940bb2245aee" 214 | } 215 | , upstream = None ThemisContract.FileRef 216 | , template = 217 | { format = ThemisContract.TemplateFormat.Mustache 218 | , file = 219 | { location = "contract.md" 220 | , hash = "6212e73deb62a698f2cf6178ab48cdd5a5615504253d5c0d06fa058ca369d1d0" 221 | } 222 | } 223 | } 224 | 225 | in contract 226 | ``` 227 | 228 | ## Step 5: Modify and update your contract 229 | 230 | You should be able to tweak your parameters and template to your needs now. 231 | Once you're done, you won't be able to compile your contract yet because the 232 | SHA256 hashes in your `contract.dhall` file probably won't match up with the 233 | hashes of your parameters/template file(s). 234 | 235 | To update your `contract.dhall` file, Themis Contract provides a simple command: 236 | 237 | ```bash 238 | # Assumes your contract is located in a `contract.dhall` file in the current 239 | # working directory. This will also add and commit all your files to your Git 240 | # repository and automatically push the changes. 241 | themis-contract update 242 | 243 | # To avoid committing/pushing the changes: 244 | themis-contract update --no-auto-commit 245 | 246 | # To commit the changes without pushing yet: 247 | themis-contract update --no-auto-push 248 | ``` 249 | 250 | ## Step 6: Compile your contract 251 | 252 | Compiling a PDF from your contract should be pretty easy now: 253 | 254 | ```bash 255 | # Automatically looks for a `contract.dhall` file in the current working 256 | # directory, and produces a PDF file called `contract.pdf`. 257 | themis-contract compile 258 | ``` 259 | 260 | Note that this command doesn't automatically commit and update your Git 261 | repository. 262 | 263 | ## Step 7: Sign your contract 264 | 265 | Finally, one of the most important things you can do with a contract is sign 266 | it. You already set up your signature and profile in the 267 | [setup tutorial](01-setup.md), so now you just need to sign your contract on 268 | behalf of a specific signatory. 269 | 270 | By default, Themis Contract tries to guess which signatory you're signing as 271 | based on the e-mail address of your currently active profile. If you want to 272 | specify the signatory on behalf of whom you're signing, you can do so using the 273 | `--as` flag (see `themis-contract sign -h` for details). 274 | 275 | First, let's just list the existing signatories on the contract: 276 | 277 | ```bash 278 | # Lists all signatories in the `contract.dhall` file in the current working 279 | # directory 280 | themis-contract list-signatories 281 | ``` 282 | 283 | If you haven't modified the `params.dhall` file from step 3 above, you should 284 | see a listing of our two predefined signatories "Michael Anderson" 285 | (id `manderson`) and "Bronwyn Savvy" (id `bsavvy`). 286 | 287 | ```bash 288 | # Try to autodetect which signatory to sign as based on your currently active 289 | # profile's e-mail address 290 | themis-contract sign 291 | 292 | # Or you can manually specify the signatory ID 293 | themis-contract sign --as bsavvy 294 | ``` 295 | 296 | This will automatically sign the contract, commit the signature and act of 297 | signing to your Git repository, and push the changes to your remote Git repo. 298 | 299 | ## Next Steps 300 | 301 | In the [next tutorial](03-workflows.md), we'll see how to supercharge your 302 | contracting workflows by reducing the number of steps you need to execute 303 | contracts. We'll also cover some of the benefits of why using Git repositories 304 | is a great solution to contract negotiation. 305 | 306 | [pandoc]: https://pandoc.org/ 307 | [mustache]: https://mustache.github.io/ 308 | [dhall]: https://dhall-lang.org/ 309 | -------------------------------------------------------------------------------- /docs/03-workflows.md: -------------------------------------------------------------------------------- 1 | # Contracting Workflows 2 | 3 | Themis Contract aims to speed up the process of contracting while simultaneously 4 | allowing for better-tracked contract negotiation (better than having to track 5 | endless e-mail trails). 6 | 7 | By leveraging the power of GitHub/GitLab and Git, we automatically get the 8 | following for free: 9 | 10 | * Discussions on desired changes (to parameters and/or contract text) 11 | * Permissioned merging of changes (when using pull/merge requests) 12 | * Version control history (with optional cryptographic integrity checks if 13 | Git commits have to be signed when submitted) 14 | 15 | Let's take a look at some simple workflow examples that could help to make your 16 | contracting experience easier. 17 | 18 | ## Example 1: Negotiating contract text with lawyers 19 | 20 | If your lawyers are open to negotiating contracts in Markdown via GitHub/GitLab 21 | pull/merge requests, then this is the workflow for you. Usually in this case 22 | you'd be deliberating on the *text of the contract itself*, and not so much the 23 | parameters. 24 | 25 | 1. Create a Git repository 26 | 2. Push your initial contract, template and parameters files. 27 | 3. Open up a pull/merge request with the desired changes to the template text. 28 | 4. Deliberate changes through the GitHub/GitLab UI. 29 | 5. Once all parties agree that the contract text is acceptable, someone needs 30 | to update the `contract.dhall` file (`themis-contract update`), commit it 31 | to the pull/merge request, and merge it all. 32 | 33 | Now you have all deliberations and conversations tracked in one place, and a 34 | full commit history of who modified which part of the contract when (and 35 | hopefully why they made those modifications in the commit messages). 36 | 37 | ## Example 2: Negotiating with counterparties 38 | 39 | Generally when you're negotiating with counterparties, you'd hopefully have a 40 | contract template that was already approved by your lawyers (as per example 1 41 | above). Most of the time here you'd only really be negotiating on parameters 42 | (your `params.dhall` file), but sometimes counterparties want minor 43 | modifications to the template text itself. 44 | 45 | In this case, you'd need to: 46 | 47 | 1. Have your upstream contract (pre-negotiated with your lawyers) at the ready. 48 | This should hopefully be stored in a Git repository. 49 | 2. Set up your new contract either inside a new Git repository or inside an 50 | existing one (but ideally not alongside your upstream): 51 | 52 | ```bash 53 | mkdir new-contract 54 | 55 | # Uses the upstream contract in your `contract-templates` repository, while 56 | # setting the new contract's remote origin to your `new-contract` repository 57 | themis-contract new \ 58 | git://git@github.com/you/contract-templates.git/service-agreement/contract.dhall \ 59 | --git-remote git://git@github.com/you/new-contract.git 60 | 61 | # Optionally leave out the --git-remote if you're creating a contract in an 62 | # existing Git repository or if you'd prefer to configure the remote yourself 63 | themis-contract new \ 64 | git://git@github.com/you/contract-templates.git/service-agreement/contract.dhall 65 | ``` 66 | 67 | 3. Open up a pull/merge request to negotiate changes to the `params.dhall` and 68 | `template.md` files. 69 | 4. Once everyone's happy with the contract, sign it (`themis-contract sign`) 70 | and push the signatures to the pull/merge request. 71 | 5. Merge all signatures in and you've got a signed contract. 72 | 73 | If you've changed something in the `template.md` file and would like to see 74 | how different the new contract's text is from the upstream's, Themis Contract 75 | provides a shortcut for you: 76 | 77 | ```bash 78 | # Show a diff between our `template.md` and the upstream's `template.md` files 79 | themis-contract upstream diff 80 | ``` 81 | 82 | ## Next Steps 83 | 84 | More tutorials will be coming soon! 85 | -------------------------------------------------------------------------------- /docs/04-formatting.md: -------------------------------------------------------------------------------- 1 | # Formatting guide 2 | 3 | We are seeking a sweet spot between plain text readability and rich semantic 4 | encoding. As our practice proceeds, we have begun to establish some useful 5 | idioms and best practices for representing common legal constructs in markdown. 6 | We have opted for the less common [pandoc markdown][pandoc-markdown] variant, 7 | because of its support for expressive annotations that can be layered on top of 8 | markdown's clear but minimal syntax and the ease with which the syntax can be 9 | augmented via filters. 10 | 11 | ## The YAML Header 12 | 13 | ### Titles and subtitles 14 | 15 | The title and subtitle of a document should be specified in the [YAML metadata 16 | header][yaml-header]. E.g., for a contract titled "My First Contract: An 17 | Example" written in the file `my-first-contract.md`, the file should begin 18 | 19 | ```markdown 20 | --- 21 | title: My First Contract 22 | subtitle: An Example 23 | --- 24 | ``` 25 | 26 | Either field may include multiple lines of text formatted using markdown, if 27 | the value of the YAML field uses the literal style format specifier `|`. E.g., 28 | if we want our title to span multiple lines, and include an emphasized span: 29 | 30 | ```markdown 31 | --- 32 | title: | 33 | | My **Multiline** Title 34 | | with an Emphasized Spans 35 | --- 36 | ``` 37 | 38 | ### Metadata 39 | 40 | The YAML header can include any additional arbitrary metadata. We sometimes use 41 | it to encode the location of a file from which the plain text contract has been 42 | derived, or the URL of a form that the contract represents. 43 | 44 | It can also be used to pass instructions to pandoc to tweak the way that the 45 | document is processed when exporting to pdf (or other formats). See the pandoc 46 | documentation on the [YAML metadata block][yaml-header] for details. 47 | 48 | ## Sections and clauses 49 | 50 | ### Sections are delimited by headers 51 | 52 | A section of a contract is specified by a header: 53 | 54 | ```markdown 55 | # Section 1 56 | 57 | The content of section 1. 58 | ``` 59 | 60 | Sections can be nested 61 | 62 | 63 | ```markdown 64 | # Section 1 65 | 66 | ## Subsection 1.1 67 | ``` 68 | 69 | ### Section numbering 70 | 71 | By default, sections will be numbered using a dot separated, nested, enumeration 72 | on export. E.g., the [subsection example](#subsection-example) renders as 73 | 74 | ![Example showing sections prefixed with section and subsection number](./numbered-sections.png) 75 | 76 | #### Disable numbering for a section 77 | 78 | To disable numbering for a section, specify that the section is `unnumbered` via 79 | a class ascription 80 | 81 | ```markdown 82 | # An unnumbered section {.unnumbered} 83 | ``` 84 | 85 | Or the shorthand `{-}` 86 | 87 | ```markdown 88 | # Another unnumbered section {-} 89 | ``` 90 | 91 | ### Clauses are just sections 92 | 93 | We represent clauses and subclauses via header-delimited sections with 94 | descriptive titles. E.g., a clause specifying dividends on shares might 95 | look like this: 96 | 97 | ```markdown 98 | ### Dividends 99 | 100 | Subject to any unanimous shareholder agreement with respect to the Corporation 101 | then in force, the holders of Shares are entitled to receive dividends if, as 102 | and when declared by the Board on the Shares. 103 | ``` 104 | 105 | ## Cross references 106 | 107 | We use [`pandoc-crossref`][pandoc-crossref] to generate links to cross 108 | references. 109 | 110 | `pandoc-corssref` is able to generate cross references to tables, figures, code 111 | blocks and more, but we generally only need it to generate cross references to 112 | sections/clauses. 113 | 114 | To label a section as a possible reference, give the header an id with the 115 | syntax `sec: