From cbb117b504ba3f4cfc63e112c342176a2b76e218 Mon Sep 17 00:00:00 2001 From: pharpend Date: Wed, 5 Oct 2022 08:30:49 -0600 Subject: [PATCH 01/10] Pharpend/develop (#1) * move stuff in here * reorganizing because zx needs to feel special * add base58/base64 explainer draft --- .gitignore | 4 + README.md | 53 + {erlang => bindings/erlang}/.gitignore | 0 {erlang => bindings/erlang}/Emakefile | 0 {erlang => bindings/erlang}/LICENSE | 0 {erlang => bindings/erlang}/ebin/vanillae.app | 0 {erlang => bindings/erlang}/src/vanillae.erl | 0 .../erlang}/src/vanillae_fetcher.erl | 0 .../erlang}/src/vanillae_man.erl | 0 {erlang => bindings/erlang}/zomp.meta | 0 docs/baseN/README.md | 245 ++++ libs/awcp/.gitignore | 18 + libs/awcp/LICENSE | 16 + libs/awcp/Makefile | 15 + libs/awcp/README.md | 490 ++++++++ libs/awcp/jex.eterms | 5 + libs/awcp/src/awcp.ts | 724 +++++++++++ libs/awcp/tsconfig.json | 16 + libs/parasite/.gitignore | 6 + libs/parasite/LICENSE | 16 + libs/parasite/Makefile | 14 + libs/parasite/README.md | 0 libs/parasite/jex.eterms | 5 + libs/parasite/src/ae_compiler.ts | 96 ++ libs/parasite/src/ae_node.ts | 272 ++++ libs/parasite/src/net.ts | 85 ++ libs/parasite/tsconfig.json | 16 + sidekick/.gitignore | 19 + sidekick/LICENSE | 16 + sidekick/Makefile | 15 + sidekick/README.md | 136 ++ sidekick/examples/.gitignore | 6 + sidekick/examples/LICENSE | 16 + sidekick/examples/Makefile | 10 + sidekick/examples/README.md | 90 ++ sidekick/examples/connection.html | 88 ++ sidekick/examples/favicon.ico | Bin 0 -> 4166 bytes sidekick/examples/hello.html | 32 + sidekick/examples/index.html | 25 + sidekick/examples/jex.eterms | 7 + sidekick/examples/scratch/logging.html | 32 + sidekick/examples/scratch/logging.ts | 6 + sidekick/examples/scratch/logging_http.html | 28 + sidekick/examples/scratch/logging_http.ts | 23 + sidekick/examples/src/connection.ts | 219 ++++ sidekick/examples/src/hello.ts | 3 + sidekick/examples/tsconfig.json | 16 + sidekick/icons/sidekick_icon_128.png | Bin 0 -> 2883 bytes sidekick/icons/sidekick_icon_128.xcf | Bin 0 -> 5815 bytes sidekick/jex.eterms | 5 + sidekick/scratch/STYLE_GUIDE.md | 187 +++ sidekick/scratch/TODO.md | 250 ++++ sidekick/scratch/async_tests.html | 9 + sidekick/scratch/async_tests.js | 26 + .../examples_old/contract-examples/LICENSE | 674 ++++++++++ .../contract-examples/base_contract.aes | 96 ++ .../contract-examples/crypto_hamster.aes | 76 ++ .../examples_old/contract-examples/floyd.aes | 11 + .../contract-examples/sales_contract.aes | 200 +++ .../examples_html/do_a_transfer.html | 54 + .../examples_old/examples_html/hello.html | 27 + .../examples_old/examples_html/ide.html | 218 ++++ .../examples_old/examples_html/maerket.html | 24 + .../examples_src_ts/do_a_transfer.ts | 160 +++ .../examples_old/examples_src_ts/ide.ts | 222 ++++ .../examples_src_ts/parasite/ae_compiler.ts | 96 ++ .../examples_src_ts/parasite/ae_node.ts | 272 ++++ .../examples_src_ts/parasite/net.ts | 85 ++ sidekick/scratch/examples_old/favicon.ico | Bin 0 -> 1150 bytes sidekick/scratch/examples_old/index.html | 23 + sidekick/scratch/examples_old/style.css | 34 + sidekick/scratch/examples_old/tsconfig.json | 13 + sidekick/scratch/examples_old/why.html | 8 + sidekick/scratch/logging.ts | 219 ++++ sidekick/scratch/old-makefile | 50 + sidekick/scratch/old-readme.md | 490 ++++++++ sidekick/scratch/scratch.txt | 244 ++++ sidekick/src/sidekick.ts | 1102 +++++++++++++++++ sidekick/tsconfig.json | 16 + utils/jex/.gitignore | 15 + utils/jex/Emakefile | 1 + utils/jex/LICENSE | 16 + utils/jex/ebin/jex.app | 7 + utils/jex/src/jex.erl | 356 ++++++ utils/jex/zomp.meta | 18 + utils/vlogd | 1 + 86 files changed, 8188 insertions(+) create mode 100644 .gitignore rename {erlang => bindings/erlang}/.gitignore (100%) rename {erlang => bindings/erlang}/Emakefile (100%) rename {erlang => bindings/erlang}/LICENSE (100%) rename {erlang => bindings/erlang}/ebin/vanillae.app (100%) rename {erlang => bindings/erlang}/src/vanillae.erl (100%) rename {erlang => bindings/erlang}/src/vanillae_fetcher.erl (100%) rename {erlang => bindings/erlang}/src/vanillae_man.erl (100%) rename {erlang => bindings/erlang}/zomp.meta (100%) create mode 100644 docs/baseN/README.md create mode 100644 libs/awcp/.gitignore create mode 100644 libs/awcp/LICENSE create mode 100644 libs/awcp/Makefile create mode 100644 libs/awcp/README.md create mode 100644 libs/awcp/jex.eterms create mode 100644 libs/awcp/src/awcp.ts create mode 100644 libs/awcp/tsconfig.json create mode 100644 libs/parasite/.gitignore create mode 100644 libs/parasite/LICENSE create mode 100644 libs/parasite/Makefile create mode 100644 libs/parasite/README.md create mode 100644 libs/parasite/jex.eterms create mode 100644 libs/parasite/src/ae_compiler.ts create mode 100644 libs/parasite/src/ae_node.ts create mode 100644 libs/parasite/src/net.ts create mode 100644 libs/parasite/tsconfig.json create mode 100644 sidekick/.gitignore create mode 100644 sidekick/LICENSE create mode 100644 sidekick/Makefile create mode 100644 sidekick/README.md create mode 100644 sidekick/examples/.gitignore create mode 100644 sidekick/examples/LICENSE create mode 100644 sidekick/examples/Makefile create mode 100644 sidekick/examples/README.md create mode 100644 sidekick/examples/connection.html create mode 100644 sidekick/examples/favicon.ico create mode 100644 sidekick/examples/hello.html create mode 100644 sidekick/examples/index.html create mode 100644 sidekick/examples/jex.eterms create mode 100644 sidekick/examples/scratch/logging.html create mode 100644 sidekick/examples/scratch/logging.ts create mode 100644 sidekick/examples/scratch/logging_http.html create mode 100644 sidekick/examples/scratch/logging_http.ts create mode 100644 sidekick/examples/src/connection.ts create mode 100644 sidekick/examples/src/hello.ts create mode 100644 sidekick/examples/tsconfig.json create mode 100644 sidekick/icons/sidekick_icon_128.png create mode 100644 sidekick/icons/sidekick_icon_128.xcf create mode 100644 sidekick/jex.eterms create mode 100644 sidekick/scratch/STYLE_GUIDE.md create mode 100644 sidekick/scratch/TODO.md create mode 100644 sidekick/scratch/async_tests.html create mode 100644 sidekick/scratch/async_tests.js create mode 100644 sidekick/scratch/examples_old/contract-examples/LICENSE create mode 100644 sidekick/scratch/examples_old/contract-examples/base_contract.aes create mode 100644 sidekick/scratch/examples_old/contract-examples/crypto_hamster.aes create mode 100644 sidekick/scratch/examples_old/contract-examples/floyd.aes create mode 100644 sidekick/scratch/examples_old/contract-examples/sales_contract.aes create mode 100644 sidekick/scratch/examples_old/examples_html/do_a_transfer.html create mode 100644 sidekick/scratch/examples_old/examples_html/hello.html create mode 100644 sidekick/scratch/examples_old/examples_html/ide.html create mode 100644 sidekick/scratch/examples_old/examples_html/maerket.html create mode 100644 sidekick/scratch/examples_old/examples_src_ts/do_a_transfer.ts create mode 100644 sidekick/scratch/examples_old/examples_src_ts/ide.ts create mode 100644 sidekick/scratch/examples_old/examples_src_ts/parasite/ae_compiler.ts create mode 100644 sidekick/scratch/examples_old/examples_src_ts/parasite/ae_node.ts create mode 100644 sidekick/scratch/examples_old/examples_src_ts/parasite/net.ts create mode 100644 sidekick/scratch/examples_old/favicon.ico create mode 100644 sidekick/scratch/examples_old/index.html create mode 100644 sidekick/scratch/examples_old/style.css create mode 100644 sidekick/scratch/examples_old/tsconfig.json create mode 100644 sidekick/scratch/examples_old/why.html create mode 100644 sidekick/scratch/logging.ts create mode 100644 sidekick/scratch/old-makefile create mode 100644 sidekick/scratch/old-readme.md create mode 100644 sidekick/scratch/scratch.txt create mode 100644 sidekick/src/sidekick.ts create mode 100644 sidekick/tsconfig.json create mode 100644 utils/jex/.gitignore create mode 100644 utils/jex/Emakefile create mode 100644 utils/jex/LICENSE create mode 100644 utils/jex/ebin/jex.app create mode 100644 utils/jex/src/jex.erl create mode 100644 utils/jex/zomp.meta create mode 160000 utils/vlogd diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a37767b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.swp +*.swo +erl_crash.dump +*.beam diff --git a/README.md b/README.md index 83b30a8..093f28e 100644 --- a/README.md +++ b/README.md @@ -1 +1,54 @@ # Vanillae + +Vanillae is an Aeternity tool suite meant for small business use cases. Focus +is placed on simplicity, code quality, and good documentation. + +## About + +### Applications + +- Jaeck Russell (Erlang) + + A minimal browser wallet extension. + +- jex (Erlang) + + A TypeScript/JavaScript package manager that avoids a large class of + NPM-related security issues. + +- vlogd (Erlang) + + **NOT FOR PRODUCTION USAGE** + + Simple HTTP server that just logs requests it gets. + + +### Erlang Libraries + +- vanillae + + +### TypeScript Libraries + +- awcp + + Aepp/waellet communication protocol + + This is just type definitions, what data the wallet expects from the Aepp, + and what data the aepp expects from the waellet. + +- libjr + + This is the complement to sidekick. It essentially is sidekick from the + perspective of the waellet. + +- sidekick + + This a TypeScript/JavaScript library for talking to Aeternity browser wallets + (e.g. Superhero, Jaeck Russell) from the perspective of an "aepp" (i.e. your + website's JavaScript). + + +## Documentation + +- base64 versus base58 explainer diff --git a/erlang/.gitignore b/bindings/erlang/.gitignore similarity index 100% rename from erlang/.gitignore rename to bindings/erlang/.gitignore diff --git a/erlang/Emakefile b/bindings/erlang/Emakefile similarity index 100% rename from erlang/Emakefile rename to bindings/erlang/Emakefile diff --git a/erlang/LICENSE b/bindings/erlang/LICENSE similarity index 100% rename from erlang/LICENSE rename to bindings/erlang/LICENSE diff --git a/erlang/ebin/vanillae.app b/bindings/erlang/ebin/vanillae.app similarity index 100% rename from erlang/ebin/vanillae.app rename to bindings/erlang/ebin/vanillae.app diff --git a/erlang/src/vanillae.erl b/bindings/erlang/src/vanillae.erl similarity index 100% rename from erlang/src/vanillae.erl rename to bindings/erlang/src/vanillae.erl diff --git a/erlang/src/vanillae_fetcher.erl b/bindings/erlang/src/vanillae_fetcher.erl similarity index 100% rename from erlang/src/vanillae_fetcher.erl rename to bindings/erlang/src/vanillae_fetcher.erl diff --git a/erlang/src/vanillae_man.erl b/bindings/erlang/src/vanillae_man.erl similarity index 100% rename from erlang/src/vanillae_man.erl rename to bindings/erlang/src/vanillae_man.erl diff --git a/erlang/zomp.meta b/bindings/erlang/zomp.meta similarity index 100% rename from erlang/zomp.meta rename to bindings/erlang/zomp.meta diff --git a/docs/baseN/README.md b/docs/baseN/README.md new file mode 100644 index 0000000..4d723c2 --- /dev/null +++ b/docs/baseN/README.md @@ -0,0 +1,245 @@ +# Base58/Base64 Number Encoding Schema in Detail + +## tldr + +```erlang +b58_enc(Bits) -> + NBits = bit_size(Bits), + <> = Bits, + b58_enc(BitNum, []). + +b58_enc(0, Acc) -> + lists:map(fun b58_int2char/1, Acc); +b58_enc(BitNum, Acc) -> + Q = BitNum div 58, + R = BitNum rem 58, + b58_enc(Q, [R | Acc]). + + + +b58_dec(Str) -> + Ns = lists:map(fun b58_char2int/1, Str), + b58_dec(Ns, 0). + +b58_dec([N | Ns], Acc) -> + NewAcc = (Acc*58) + N, + b58_dec(Ns, NewAcc); +b58_dec([], FinalAccN) -> + MinNBits = trunc(math:log2(FinalAccN) + 1), + NBytes = ceil(MinNBits / 8), + NBits = NBytes * 8, + <>. + + + +b64_enc(<>) -> + CA = b64_int2char(A), + CB = b64_int2char(B), + CC = b64_int2char(C), + CD = b64_int2char(D), + [CA, CB, CC, CD | b64_enc(Rest)], +b64_enc(<>) -> + CA = b64_int2char(A), + CB = b64_int2char(B), + CC = b64_int2char(C bsl 2), + [CA, CB, CC, $=]; +b64_enc(<>) -> + CA = b64_int2char(A), + CB = b64_int2char(B bsl 4), + [CA, CB, $=, $=]; +b64_enc(<<>>) -> + []. + + + +b64_dec(Base64_String) -> + b64_dec(Base64_String, <<>>). + +b64_dec([W, X, $=, $=], Acc) -> + NW = b64_char2int(W), + NX = b64_char2int(X), + <> = <>, + <>; +b64_dec([W, X, Y, $=], Acc) -> + NW = b64_char2int(W), + NX = b64_char2int(X), + NY = b64_char2int(Y), + <> = <>, + <>; +b64_dec([], Acc) -> + Acc; +b64_dec([W, X, Y, Z | Rest], Acc) -> + NW = b64_char2int(W), + NX = b64_char2int(X), + NY = b64_char2int(Y), + NZ = b64_char2int(Z), + NewAcc = <>, + b64_dec(Rest, NewAcc). +``` + +## Introduction + +This document explains the Base58 and Base64 notations, and the algorithms +for working with them. I wrote this document because I had a fair bit of +difficulty working this out for myself, even with a strong math background. +I couldn't find any resource on the internet explaining all of this simply. + +Code examples are given in Erlang and TypeScript. These are the two most +common languages used within the Aeternity project, and both happen to be +languges that make these tasks easy. This document assumes you are familiar +with either/both languages. Even if that's not true, Erlang is a very simple +and clean language, so the code should be pretty self-explanatory if you read +it. + +Base64 is kind of annoying but it's pretty straightforward to code. My +initial assumption was that Base58 was in some way "the same" algorithm but +with `n = 58` instead of `n = 64`. When I went to look up the spec, I found +this ([source](https://digitalbazaar.github.io/base58-spec/)) + +> ### 3. The Base58 Encoding Algorithm +> +> To encode an array of bytes to a Base58 encoded value, run the following +> algorithm. All mathematical operations MUST be performed using integer +> arithmetic. Start by initializing a `zero_counter` to zero (`0x0`), an +> `encoding_flag` to zero (`0x0`), a `b58_bytes` array, a `b58_encoding` +> array, and a `carry` value to zero (`0x0`). For each byte in the array of +> bytes and while `carry` does not equal zero (`0x0`) after the first +> iteration: +> +> 1. If `encoding_flag` is not set, and if the byte is a zero (`0x0`), +> increment the value of `zero_counter`. If the value is not zero `(0x0)`, +> set `encoding_flag` to true `(0x1)`. +> 2. If `encoding_flag` is set, multiply the current byte value by 256 and add +> it to `carry`. +> 3. Set the corresponding byte value in `b58_bytes` to the value of `carry` +> modulus 58. +> 4. Set `carry` to the value of `carry` divided by 58. +> +> Once the `b58_bytes` array has been constructed, generate the final +> `b58_encoding` using the following algorithm. Set the first `zero_counter` +> bytes in `b58_encoding` to `1`. Then, for every byte in `b58_array`, map the +> byte value using the Base58 alphabet in the previous section to its +> corresponding character in `b58_encoding`. Return `b58_encoding` as the +> Base58 representation of the input array of bytes. + +I personally have no idea what that does. I found a YouTube video that +explained the Base58 algorithm in a way that made a lot more sense. +([source](https://youtu.be/GedV3S9X89c)). The video gave a clear enough +explanation of the Base58 algorithm that I could _figure out_ what is going on +and why it makes sense. I was able to relate what I was seeing in the video to +background context I happen to have from mathematics. But the video didn't +provide that context. + +I want this document to explain what both algorithms do, why they make sense, +how they are different, and why they _have_ to be different. All with code +examples and sufficient mathematical context. + +Let's get started. + +Any data stored in a computer is represented as an integer. For the purposes of +this discussion, we're going to assume all integers are non-negative (greater +than or equal to 0). The discussion below can easily be modified to accomodate +negative integers. This would add a small amount of annoying complexity in +exchange for no gain in conceptual clarity. Nothing we are doing requires +dealing with negative numbers. + +The problem we are interested in is _how do we represent really big integers in +plain text?_. + +The first point I want you to take away is that **these are two totally +different solutions**. Do not be fooled by the name. It is **NOT** the +case that these are two instances of the same "Base N" +algorithm, just one is `N = 64` and one is `N = 58`. **These are two totally +different approaches to solving the same problem.** + +More precisely, the underlying mathematics behind the two notations is very +similar, but the algorithms for producing them are very different. More detail +later. + +Like I said, any given piece of data is---from the perspective of your +computer---just a very big integer. The difference between the two algorithms +is, roughly: + +1. The Base64 algorithm thinks of that integer as a "stream of digits" +2. The Base58 algorithm thinks of that integer as a "pure integer," kind of + the way math thinks of an integer: the integer _itself_ is a different + thing than the way the integer is _represented_. + +Base64 encoding/decoding involves a straightforward translation back and forth +from the machine representation of integers, without thinking too much (or at +all) about the math involved. + +Base58 encoding/decoding requires thinking about the integer from a more mathy +point of view. That weird arcane algorithm above is what happens when you try +to phrase the mathematics in terms of the machine representation of +really big integers. + +We're going to focus on the mathy point of view and then circle back to the +weird arcane algorithm later on. + +There is actually a good reason we don't use decimal notation for really big +integers: it's extremely wasteful. + +I'll explain the following in more detail in a later section. Roll with me. To +any piece of data there is associated a quantity called **information**. The +_unit_ of information is the _bit_, in the same sense that the unit of length +is the meter. + +1. There are 256 distinct bytes. A single byte (machine digit) + contains exactly 8 bits $8 = \log_2 256$ of information. + +2. There are 10 distinct decimal ("Base10") symbols. A single decimal digit + contains approximately 3.32 bits (`3.32 ~ log2(10)`) of information. + +3. There are 64 distinct Base64 symbols. A single Base64 digit contains + exactly $6$ bits (`6 = log2(64)`) of information. + +4. There are 58 distinct Base58 symbols. A single Base58 digit contains + approximately 5.86 bits (`5.86 ~ log2(58)`) of information. + +What this means is, in base64 notation, each symbol consumes 6 bits of +information, roughly twice the rate of decimal notation (~3.32 bits per +symbol). What this means in practice is that a number written in Base64 +notation is about half as long as a number written in decimal notation. + +For instance, the number `K = 90 682 877 680 429` + +1. requires 14 digits (count them!) in decimal notation + + $$ + \frac{(\log_2 K) \text{ bits}} + {(\log_2 10) \text{ bits per symbol}} + \approx + \frac{46.37 \text{ bits}} + { 3.37 \text{ bits per symbol}} + \approx 13.96 \text{ symbols} + $$ + +2. requires 8 digits in Base64 notation (`UnnAthst`) + + $$ + \frac{(\log_2 K) \text{ bits}} + {(\log_2 64) \text{ bits per symbol}} + \approx + \frac{46.37 \text{ bits}} + { 6 \text{ bits per symbol}} + \approx 7.73 \text{ symbols} + $$ + +3. requires 8 digits in Base58 notation (`i55xNZNt`) + + $$ + \frac{(\log_2 K) \text{ bits}} + {(\log_2 58) \text{ bits per symbol}} + \approx + \frac{46.37 \text{ bits}} + { 5.86 \text{ bits per symbol}} + \approx 7.91 \text{ symbols} + $$ + +As you can see, the difference in space complexity between Base58 and Base64 is +pretty small, but the difference between Base10 is pretty large. Base58 has +the same alphabet (set of symbols) as Base64, minus a handful that can cause +readability or manual input issues. For instance, the Base64 alphabet contains +both the symbol `0` (numeral zero) and `O` (uppercase letter `o`). The Base58 +alphabet contains neither. diff --git a/libs/awcp/.gitignore b/libs/awcp/.gitignore new file mode 100644 index 0000000..f7f9d8a --- /dev/null +++ b/libs/awcp/.gitignore @@ -0,0 +1,18 @@ +*.swp +*.swo +*.beam +dist_js +sidekick_dist +docs +examples/examples_dist_js +www_tree +prose +*.hi +*.o +sidekick_mindist +sidekick_fulldist +dist +jx_mindist +*jx_include* +jex_mindist +erl_crash.dump diff --git a/libs/awcp/LICENSE b/libs/awcp/LICENSE new file mode 100644 index 0000000..2e52b8d --- /dev/null +++ b/libs/awcp/LICENSE @@ -0,0 +1,16 @@ +ISC License + +Copyright (c) 2022 Peter Harpending + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + diff --git a/libs/awcp/Makefile b/libs/awcp/Makefile new file mode 100644 index 0000000..131d276 --- /dev/null +++ b/libs/awcp/Makefile @@ -0,0 +1,15 @@ +all: prepare build + +deploy: prepare build mindist push + +prepare: + jx pull -f + +build: + jx build -f + +mindist: + jx mindist -f + +push: + jx push -f diff --git a/libs/awcp/README.md b/libs/awcp/README.md new file mode 100644 index 0000000..5257acb --- /dev/null +++ b/libs/awcp/README.md @@ -0,0 +1,490 @@ +# Sidekick + +Sidekick is a simple JavaScript library for talking to an Aeternity +browser wallet extension such as Superhero from the document context +of a webpage. + +| **Thing** | **URL** | +| --- | --- | +| Bug Tracker | https://gitlab.com/DoctorAjayKumar/sidekick/-/issues | +| Documentation | http://orangepill.healthcare/projects/sidekick/current/docs | +| Examples | https://gitlab.com/DoctorAjayKumar/sidekick/-/tree/master/examples | +| Git repository | https://gitlab.com/DoctorAjayKumar/sidekick | +| Homepage | http://orangepill.healthcare/projects/sidekick | +| License | ./LICENSE.txt | +| Maintainer | Dr. Ajay Kumar PHD | +| Releases | http://orangepill.healthcare/projects/sidekick/releases | + + +> One of the most important things for designing a computer, which I +> think most designers don't do, is you study the problem you want to +> solve. And then use what you learn from studying the problem you +> want to solve to put in the mechanisms needed to solve it in the +> computer you're building. No more, no less. + +— Gerald Jay Sussman + +### What sidekick is NOT + +Sidekick is **not** ideal if you operate in the Node/NPM ecosystem. If you are +working in the Node/NPM ecosystem, you probably want the [Aeternity JavaScript +SDK](https://github.com/aeternity/aepp-sdk-js/). Every single +Aeternity-related thing you could ever possibly want to do is possible to +accomplish with the SDK. + +All that Sidekick knows how to do is talk to a browser wallet extension. In an +application, there is a great deal of necessary functionality (e.g. forming +transactions for the wallet to sign) which sidekick assumes your server-side +code has already handled. + +In all software there is a tradeoff between simplicity and number of features. +Sidekick is very simple: 2300 lines of TypeScript, including comments, with no +dependencies. Therefore Sidekick intentionally only has a very limited set of +features. + + +# How to use this library + +To obtain this library, download and unpack a release tarball + + wget http://zxq9.com/projects/vanillae/sidekick/releases/sidekick_dist_X-Y-Z.tar.gz + tar xzvf sidekick_dist_X-Y-Z.tar.gz + +You *should* be able to get all of the functionality you need just +from the exposed functions in the `sidekick` module. (If this is not +the case, please report this as a bug.) + +You probably want to read the documentation of: + + sidekick.js : top-level function calls + awcp/awcp.js : explains the return types of top-level sidekick + functions, explains what sidekick actually does + for you, and explains how all the various + modules fit together + skylight.js : main data structure in sidekick + helpers.js : what it sounds like + +There are examples explained in this document. Their source can be +found in full in the GitLab repository, link above. The examples +exhibit how to interact with the standard release tarball using +TypeScript. + + +## General flow of Sidekick usage + +1. Import the sidekick.js file: + + ```typescript + import * as sk from '/path/to/sidekick_dist_X-Y-Z/dist_js/sidekick.js' + ``` + +2. Create a `Skylight`. This is the main data structure of Sidekick. + + There are two ways to do this + + 1. `start_dwim() : Promise` + + This is probably the one you want. It starts up the Skylight + and does the handshake with the wallet (will pop up the + little window for your user). The effect of this is to + populate all the variables like the user's public key. + + Note that this is an async function. It doesn't return until + the handshake is complete. + + ```typescript + // Needs to be wrapped in an async function in order to use + // await keyword + async function main() : Promise { + let my_skylight = await sk.start_dwim(); + ... + } + + main(); + ``` + + 2. `start : Skylight` + + This is the vanilla option. All it does is create the + Skylight object. + + Beware that this does start up a message queue that passively + listens for messages from the wallet. + + +3. Pass the skylight in to various functions along with additional + calldata generated by your server-side backend code. + + +## Example 1: Hello World + +The effect of this example is to print "hello world" to the JS +console (Ctrl+Shift+C in most browsers). This example is included for +debugging/mental-quicksand-escape purposes. + +```html + + + + + Hello world: Sidekick + + +

Sidekick Example: Hello World

+ +

Check the console

+ + + +``` + +The important lines are + +```html + +``` + +This shows + +- you need to use `type="module"` in order for imports to work +- how to do an import +- how to call functions from an imported module + + +## Example 2: get the user to sign a transaction + +Anything you will want the user to do (e.g. sign smart contracts) is +contained in "get the user to sign a transaction". This example +illustrates the basic things you need to do in order to accomplish +that. + +Sidekick does not provide functionality for forming the transaction +string you want the user to sign. Your backend should be doing that. + +The example here uses a shim library called `parasite`, which you can find in +the GitLab repository in the `examples` directory. Parasite is just a rewrite +layer in front of the `testnet.aeternity.io` JSON interface. It is not suitable +for usage in production. + +This is adapted from +[`examples/examples_src_ts/do_a_transfer.ts`][dat_ts]. The +difference is that `do_a_transfer.ts` is written against +[`examples/examples_html/do_a_transfer.html`][dat_html]. It contains +a lot of unnecessary detail, like doing things in response to buttons +being clicked, and filling in DOM textboxes with the results of things. + +The full example has logic that resembles a register machine, rather +than a functional program. This example is closer to a functional +program. + +[dat_html]: https://gitlab.com/DoctorAjayKumar/sidekick/-/blob/master/examples/examples_html/do_a_transfer.html +[dat_ts]: https://gitlab.com/DoctorAjayKumar/sidekick/-/blob/master/examples/examples_src_ts/do_a_transfer.ts + + +```typescript +import * as ae_node from './parasite/ae_node.js'; +import * as sk from '../sidekick_dist/dist_js/sidekick.js'; + +async function +main() + : Promise +{ + //--------------------------------------------------------------- + // STEP 1: MAKE A SKYLIGHT + //--------------------------------------------------------------- + let skl : sk.Skylight = + await sk.start_dwim(sk.TIMEOUT_DEF_DETECT, + sk.TIMEOUT_DEF_CONNECT, + sk.TIMEOUT_DEF_ADDRESS); + + + //--------------------------------------------------------------- + // STEP 2: GET THE USER'S ADDRESS + //--------------------------------------------------------------- + let user_addr : string = + await sk.address(skl, sk.TIMEOUT_DEF_ADDRESS); + + // faster/non-async: + // + // let user_addr = skl.waellet_address + // + // the non-commented code + // + // - will crash if the wallet isn't connected + // - will query the wallet for the address if the wallet *is* + // connected but the skl.waellet_address field *is not* + // populated + // - will crash if that times out after the second argument + // number of milliseconds + // + // if you know with 100% certainty that the address field will be + // populated (which we do here, because we used connect_dwim), + // then it's safe to just grab the field from the skylight + // + // The problem is that if the address field isn't populated, the + // commented code defaults to `undefined`, rather than crashing + // with an error message and callstack trace + + + //--------------------------------------------------------------- + // STEP 3: FORM THE TRANSACTION + // + // This will be different for your application, because you're + // not using parasite (right?) + // + // You need to figure out how to make something like `tx_obj` on + // your own. + // + // `tx_obj` is just `{tx: "some_base58_garbage"}`. + //--------------------------------------------------------------- + + let target_addr : string = + 'ak_dvNHMgVvdSgDchLsmcUpuFTbMBGfG3E5V9KZnNjLYPyEhcqnL'; + + // amount is in aettos + let amount : number = 1; + let endpoint : string = ae_node.URL_TESTNET; + let spendtx = {'recipient_id' : target_addr, + 'amount' : amount, + 'fee' : ae_node.MIN_FEE, + 'sender_id' : user_addr, + 'payload' : ""}; + let tx_obj = await ae_node.PostSpend(endpoint, spendtx); + + + //--------------------------------------------------------------- + // STEP 4: HAVE THE USER SIGN THE TRANSACTION + // + // There are two options: + // + // 1. `tx_sign_no_propagate` simply has the wallet sign the + // transaction, and return the signed transaction back to you + // + // 2. `tx_sign_yes_propagate` has the wallet sign the transaction + // and also propagates it into the network. It returns a more + // elaborate data structure, which is documented in somewhere + // the AWCP module documentation. + //--------------------------------------------------------------- + + let result = + await sk.tx_sign_no_propagate(skl, + tx_obj, + sk.NETWORK_ID_TESTNET, + sk.TIMEOUT_DEF_SIGN); + + console.log(result); + +} + +main(); +``` + + +# Known pitfalls + +- See notes in sidekick module about potential state crossups if your + document logic is multi-threaded. + +- Has only been tested against Firefox-on-Linux + +- Have not worked out good user idioms for handling errors + + + + + +# How the release is structured and why + +The idea of the release is that it contains exactly what is needed in +order to drop Sidekick into your project and start using it. No more, +no less. + +A release has the following structure: + +``` +sidekick_dist_X-Y-Z/ + dist_js/............tsc-generated human-readable JS tree + foo.js..............the actual code that is run + foo.d.ts............included so that your TypeScript code can + typecheck against sidekick + foo.js.map..........debug symbols that map foo.js to + locations in the TypeScript source + src_ts/.............included so that the browser's debugger works + foo.ts + README.txt + LICENSE.txt +``` + +We use semantic versioning: `X.Y.Z` + +- A change in `X` means an API-breaking change +- A change in `Y` means an non-breaking API change +- A change in `Z` means no change to the API + +There is no documentation included in the release. There is +autogenerated API documentation linked above, which is generated from +this file and the sources that are included in the release. + + +## What each file does + +``` +src_ts/.....................TypeScript source for sidekick library + awcp/.......................Aepp-Waellet Communication Protocol + awcp.ts.....................Protocol definition + msgq.ts.....................Block-on-raseev implementation + msgr.ts.....................Protocol implementation + helpers.ts..................what it sounds like + sidekick.ts.................top-level module + skylight.ts.................primary data structure +``` + + +## Where is the NPM package or the webpack bundle? + +There isn't one. + + +### Why? + +Sidekick is a library that deals with cryptocurrency. The security +model is based on transparency and trust. A user must be able to +inspect code that is running on his hardware handling his money. + +We don't support NPM because of the security issues that NPM +introduces. Briefly, the code can change at any time under the +developer's nose without the developer knowing. The [leftpad +debacle][lpad] and the [RIAEvangelist debacle][ria] are good examples +of the types of vulnerabilities that package managers like NPM +enable. + +[lpad]: https://archive.ph/Qsh7j +[ria]: https://archive.ph/OF5I9 + +We don't use something like webpack because that introduces an opaque +rewrite using an untrusted tool. Webpack could plausibly alter the +runtime behavior of the program, and we would have no way to detect +that. Even if we trusted webpack, how do we obtain webpack? NPM. +So. + +It is debatable whether or not we should trust the TypeScript +compiler (TSC). On the whole, TSC probably makes Sidekick more +secure, simply by virtue of increasing overall code quality and +eliminating the largest categories of potential bugs. Moreover, the +output that TSC produces is human-readable, and has a very +straightforward mapping to the original source code. A human can +easily read the TSC-generated JavaScript tree, even without the aid +of the source map, and have a high degree of faith that the code is +trustworthy and that TSC is behaving as promised. + + + +# How to obtain the source tree + +The source tree is included in your release. You can clone the +git repository with + +``` +git clone https://gitlab.com/DoctorAjayKumar/sidekick.git +``` + + + +# Repo file tree (non-exhaustive) + + +``` +examples/...................Examples + contract-examples/..........Example Sophia smart contracts + examples_html/..............Low-budget example interfaces + do_a_transfer.html......Send money to an arbitrary address + hello.html..............Hello world + ide.html................Play around with smart contracts + examples_src_ts/............TypeScript source for each example + parasite/...................JavaScript shim that does what + your backend code ordinarily + would do + ae_compiler.ts..............Talk to a remote Sophia + compiler JSON HTTP interface + ae_node.ts..................Talk to a node (for forming + transactions, querying chain, + etc) + net.ts......................Network helper functions + do_a_transfer.ts........Send money to an arbitrary address + ide.ts..................Play around with smart contracts + tsconfig.json...............Examples-specific tsc configuration +src_ts/.....................TypeScript source for sidekick library + awcp/.......................Aepp-Waellet Communication Protocol + awcp.ts.....................Protocol definition + msgq.ts.....................Block-on-raseev implementation + msgr.ts.....................Protocol implementation + helpers.ts..................what it sounds like + sidekick.ts.................top-level module + skylight.ts.................primary data structure +LICENSE.txt.................MIT License +Makefile....................Makefile + make........................Equivalent to `make build` + make build..................Run tsc + make clean..................rm -r dist_js sidekick_dist docs \ + examples/examples_dist_js + make dist...................Build a release tarball directory + make jsdoc..................Build the HTML documentation + make build_examples.........cd examples && tsc + make serve_examples.........cd examples && python3 -m \ + http.server 8001 +README.txt..................This file +STYLE_GUIDE.md..............Explains why the code looks so weird +TODO.md.....................what it sounds like +tsconfig.json...............Configuration file for tsc +``` + + + +# How to build a release + +You will need the TypeScript compiler installed. See prereqs section. + + make dist + + +## Prereqs for building source files + +If you want to edit and rebuild the source files, you need TypeScript +installed, and you should update npm. + + npm install -g typescript + npm install -g npm + +## Protip: avoid using `sudo npm install -g` + +If you want to avoid using sudo + +``` +mkdir ~/.npm-packages +npm config set prefix "${HOME}/.npm-packages" +``` + +Edit `~/.bashrc` or `~/.zshrc` with + +``` +NPM_PACKAGES="${HOME}/.npm-packages" +export PATH=$NPM_PACKAGES/bin:$PATH +``` + +Source: https://github.com/sindresorhus/guides/blob/main/npm-global-without-sudo.md + + +# How to build and view documentation + +``` +make build_docs +make serve_docs +``` + + diff --git a/libs/awcp/jex.eterms b/libs/awcp/jex.eterms new file mode 100644 index 0000000..ddebe0b --- /dev/null +++ b/libs/awcp/jex.eterms @@ -0,0 +1,5 @@ +{type, library}. +{realm, local}. +{name, awcp}. +{version, "0.1.0"}. +{deps, []}. diff --git a/libs/awcp/src/awcp.ts b/libs/awcp/src/awcp.ts new file mode 100644 index 0000000..c1163a1 --- /dev/null +++ b/libs/awcp/src/awcp.ts @@ -0,0 +1,724 @@ +/** + * # AWCP: aepp-waellet communication protocol + * + * Suppose you are the aepp and you want to communicate with a waellet. What + * you do is pick an `EventTarget` (typically `window`), and listen to its + * `MessageEvent`s, via something like + * + * ```typescript + * window.addEventListener('message', my_listener); + * ``` + * + * You then communicate with the wallet by sending messages back over the + * `EventTarget` + * + * This module defines the shape of the messages that are sent. There are several + * layers to the onion, each corresponding to natural branch points in the + * protocol. + * + * 1. The `MessageEvent` layer. This is what is actually sent as an event. + * This is an opaque object that is built into every runtime's standard + * library: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent + * + * The `MessageEvent` has a field called `data`, which corresponds to the + * next layer. + * + * 2. The `EventData` layer. This is what goes in `message_event.data`. The + * structure that goes in here is one of + * + * 1. `EventData_W2A`: waellet-to-aepp + * 2. `EventData_A2W`: aepp-to-waellet + * + * This layer corresponds to the "am I supposed to pay attention to this + * event?" branch point. + * + * Those data structures mentioned above have two fields. + * + * 1. `type` is a string which is either `"to_aepp"` or `"to_waellet"` + * 2. `data` contains the next layer + * + * ```typescript + * type EventData_W2A + * + * = {type : "to_aepp", + * data : t}; + * ``` + * + * ``` typescript + * type EventData_A2W + * + * = {type : "to_waellet", + * data : t}; + * ``` + * + * + * 3. We're at `message_event.data.data`. The idiom here is "JSON RPC", which + * is sort of a poor man's HTTP. + * + * In general, the wallet is the server and the aepp is the client. + * + * If you are the aepp, usually you are handling a response to a request + * you sent to the waellet. For instance, you formed a transaction and + * sent it to the waellet to sign, and the waellet is sending you back + * either the signed transaction or an error (e.g. user rejected the + * transaction). + * + * The exception to this pattern is the wallet notifying you that it + * exists, which is the only time the waellet sends a request (a "cast", or + * a "notification") to the aepp. __In no event does the aepp send a + * response to the waellet.__ + * + * I am not 100% sure what RPC stands for, but it will be helpful to think + * about it as "remote procedure call". More below. This layer roughly + * corresponds to the "given that I am supposed to pay attention to this + * event, what am I supposed to do with this information?" + * + * All requests have a `method` field (a string) and a `params` field (an + * object). There are two types of requests: + * + * 1. "casts" (the RPC standard calls these "notifications"). These do not + * need a response. This is only used for the waellet announcing it + * exists. + * + * 2. "calls". These have an `id` field, and get a response. These are + * used when the aepp is requesting the waellet to do something. The + * response will have the same `id` field and the same `method` field. + * + * 4. So far nothing we've talked about is specific to Aeternity, Vanillae, + * JR, or sidekick. This fourth layer is the actual semantics of the + * messaging protocol between the aepp and the waellet. + * + * By analogy, the first two layers are developing something like TCP. The + * third layer is developing HTTP. And this layer is the actual routing + * table of your website, which carries with it the expected semantics of + * how the website is supposed to behave. + * + * This module DOES __NOT__ exhaustively define all of the communication + * protocol that occurs in the SDK, only the subset that I have encountered + * in practice. + * + * The first layer is defined by the runtime, not here. So we're starting with + * layer 2. + * + * # Notes on JSON RPC 2.0 + * + * I have subtly changed the RPC protocol to + * + * 1. improve it in such a way that it is easier to use in code + * 2. more accurately represent how it is used in practice + * + * The only difference here is that the `params` field of requests is + * non-optional, and must be an object (the RPC standard allows arrays). + * + * ## Requests + * + * I have adapted the verbiage here, borrowing from Erlang, to make a + * distinction between + * + * 1. casts (RPC calls these "notifications"): these + * + * 1. do __NOT__ have an `id` field + * 2. __AND__ do __NOT__ require a response. + * + * 2. calls: these + * + * 1. __DO__ have an `id` field + * 2. __AND__ __DO__ require a response. + * + * The `RpcCall` and `RpcResp` data structures each have an `id_n` type + * parameter. + * + * The purpose of this is to notate (and possibly enforce) at the type level + * the constraint that, given a call with say `id = 7`, the response must also + * have `id = 7`. + * + * # Links + * + * - `MessageEvent`s: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent + * - JSON RPC 2.0 https://www.jsonrpc.org/specification + * + * @module + */ + +// TODO: TS code style guide +// TODO: annotate everything with examples +// TODONE: enumerate RPC errors +// TODO: move Safe in here +// TODO: constants for method names + + +//============================================================================= +// IMPORTS +//============================================================================= + +//import type { +// ERROR_TYPE_RpcInvalidTransactionError, +// ERROR_TYPE_RpcBroadcastError, +// ERROR_TYPE_RpcRejectedByUserError, +// ERROR_TYPE_RpcUnsupportedProtocolError, +// ERROR_TYPE_RpcConnectionDenyError, +// ERROR_TYPE_RpcNotAuthorizeError, +// ERROR_TYPE_RpcPermissionDenyError, +// ERROR_TYPE_RpcInternalError, +// ERROR_TYPE_RpcMethodNotFoundError, +//} from './errcode.js'; + + + + +//============================================================================= +// EXPORTS +//============================================================================= + + +export { + // error code constants + ERROR_CODE_RpcInvalidTransactionError, + ERROR_CODE_RpcBroadcastError, + ERROR_CODE_RpcRejectedByUserError, + ERROR_CODE_RpcUnsupportedProtocolError, + ERROR_CODE_RpcConnectionDenyError, + ERROR_CODE_RpcNotAuthorizeError, + ERROR_CODE_RpcPermissionDenyError, + ERROR_CODE_RpcInternalError, + ERROR_CODE_RpcMethodNotFoundError, + // Layer 2: events + EventData_W2A, + EventData_A2W, + // Layer 3: RPC + RpcError, + RpcCast, + RpcCall, + RpcResp_error, + RpcResp_ok, + RpcResp, + RpcResp_Any, + // Layer 4: specific semantics + // connection.announcePresence + Params_W2A_connection_announcePresence, + RpcCast_W2A_connection_announcePresence, + EventData_W2A_connection_announcePresence, + // connection.open + Params_A2W_connection_open, + Result_W2A_connection_open, + RpcCall_A2W_connection_open, + RpcResp_W2A_connection_open, + EventData_A2W_connection_open, + EventData_W2A_connection_open, + // address.subscribe + Params_A2W_address_subscribe, + Result_W2A_address_subscribe, + RpcCall_A2W_address_subscribe, + RpcResp_W2A_address_subscribe, + EventData_A2W_address_subscribe, + EventData_W2A_address_subscribe, + //// transaction.sign (propagate) + //Params_A2W_tx_sign_yesprop, + //Result_W2A_tx_sign_yesprop, + //RpcCall_A2W_tx_sign_yesprop, + //RpcResp_W2A_tx_sign_yesprop, + //EventData_A2W_tx_sign_yesprop, + //EventData_W2A_tx_sign_yesprop, + // transaction.sign (do not propagate) + Params_A2W_tx_sign_noprop, + Result_W2A_tx_sign_noprop, + RpcCall_A2W_tx_sign_noprop, + RpcResp_W2A_tx_sign_noprop, + EventData_A2W_tx_sign_noprop, + EventData_W2A_tx_sign_noprop +}; + + + +//============================================================================= +// LAYER 2: WHO IS THIS MESSAGE FOR +// +// The first layer is defined by the runtime, not here. So we're starting with +// layer 2. +//============================================================================= + +/** + * This is the data that is sent from the wallet to the aepp through the Event + * bus + */ +type EventData_W2A + + = {type : "to_aepp", + data : t}; + + +/** + * This is the data that is sent from the aepp to the wallet through the Event + * bus + */ +type EventData_A2W + + = {type : "to_waellet", + data : t}; + + + + +//============================================================================= +// LAYER 3: JSON RPC 2.0 +// +// It should be noted in general that the id field exists as a type parameter +// so that we can use the type system to denote ID matches in callbacks. +// +// Remember, in TypeScript's type system, values are valid types. +// +// So `(_arg0: Foo<3, string>) => Bar<3, number>` is a valid type +//============================================================================= + +/** + * `const ERROR_CODE_RpcInvalidTransactionError = 2;` + * + * See https://github.com/aeternity/aepp-sdk-js/blob/1065da9a46b8dbfe60a2c3e5646e7422ee7e495e/src/aepp-wallet-communication/schema.ts#L92 + */ +const ERROR_CODE_RpcInvalidTransactionError = 2; + +/** + * `const ERROR_CODE_RpcBroadcastError = 3;` + * + * See + * https://github.com/aeternity/aepp-sdk-js/blob/1065da9a46b8dbfe60a2c3e5646e7422ee7e495e/src/aepp-wallet-communication/schema.ts#L108 + */ +const ERROR_CODE_RpcBroadcastError = 3; + +/** + * `const ERROR_CODE_RpcRejectedByUserError = 4;` + * + * See https://github.com/aeternity/aepp-sdk-js/blob/1065da9a46b8dbfe60a2c3e5646e7422ee7e495e/src/aepp-wallet-communication/schema.ts#L122 + */ +const ERROR_CODE_RpcRejectedByUserError = 4; + + +/** + * `const ERROR_CODE_RpcUnsupportedProtocolError = 5;` + * + * See https://github.com/aeternity/aepp-sdk-js/blob/1065da9a46b8dbfe60a2c3e5646e7422ee7e495e/src/aepp-wallet-communication/schema.ts#L140 + */ +const ERROR_CODE_RpcUnsupportedProtocolError = 5; + + +/** + * This error occurs when the user rejects your attempt to connect. (I think) + * + * The error name here is ungrammatical but following the lead of the SDK. + * + * `const ERROR_CODE_RpcConnectionDenyError = 9;` + * + * See https://github.com/aeternity/aepp-sdk-js/blob/1065da9a46b8dbfe60a2c3e5646e7422ee7e495e/src/aepp-wallet-communication/schema.ts#L155 + */ +const ERROR_CODE_RpcConnectionDenyError = 9; + +/** + * This error occurs when you are not connected to the wallet. + * + * The error name here is ungrammatical but following the lead of the SDK. + * + * `const ERROR_CODE_RpcNotAuthorizeError = 10;` + * + * See https://github.com/aeternity/aepp-sdk-js/blob/1065da9a46b8dbfe60a2c3e5646e7422ee7e495e/src/aepp-wallet-communication/schema.ts#L171 + */ +const ERROR_CODE_RpcNotAuthorizeError = 10; + + +/** + * This error occurs when you are not `address.subscribe`d to the wallet (I think?) + * + * The error name here is ungrammatical but following the lead of the SDK. + * + * `const ERROR_CODE_RpcPermissionDenyError = 11;` + * + * See https://github.com/aeternity/aepp-sdk-js/blob/1065da9a46b8dbfe60a2c3e5646e7422ee7e495e/src/aepp-wallet-communication/schema.ts#L186 + */ +const ERROR_CODE_RpcPermissionDenyError = 11; + +/** + * This is the general "something went wrong, i dunno" negative error. + * + * See https://github.com/aeternity/aepp-sdk-js/blob/1065da9a46b8dbfe60a2c3e5646e7422ee7e495e/src/aepp-wallet-communication/schema.ts#L196-L209 + */ +const ERROR_CODE_RpcInternalError = 12; + + +/** + * This is presumably the equivalent of the HTTP 404 error + * + * See https://github.com/aeternity/aepp-sdk-js/blob/1065da9a46b8dbfe60a2c3e5646e7422ee7e495e/src/aepp-wallet-communication/schema.ts#L211-L224 + */ +const ERROR_CODE_RpcMethodNotFoundError = -32601; + + +/** + * Error data inside the `error` field of a RpcResp_Err + */ +type RpcError + = {code : number, + message : string, + data? : any}; + + + + +//----------------------------------------------------------------------------- +// Requests +//----------------------------------------------------------------------------- + +/** + * This type is used for "notifications", i.e. messages sent from the client to + * the server that do not need a response. An example is the wallet announcing + * it exists. + */ +type RpcCast + + = {jsonrpc : "2.0", + method : method_s, + params : params_t}; + + + +/** + * This type is used for requests from the client to the server that require a + * response. + */ +type RpcCall + + = {jsonrpc : "2.0", + id : number | string, + method : method_s, + params : params_t}; + + + + +//----------------------------------------------------------------------------- +// Responses +//----------------------------------------------------------------------------- + +/** + * This is the shape of unsuccessful responses + */ +type RpcResp_error + + = {jsonrpc : "2.0", + id : number | string, + method : method_s, + error : RpcError}; + + + +/** + * This is the shape of successful responses + */ +type RpcResp_ok + + = {jsonrpc : "2.0", + id : number | string, + method : method_s, + result : result_t}; + + + +/** + * This is the shape of generic responses + */ +type RpcResp + + = RpcResp_ok + | RpcResp_error; + + + +/** + * Most generic possible response + */ +type RpcResp_Any = RpcResp; + + + + +//============================================================================= +// LAYER 4: VANILLAE-SPECIFIC MESSAGE PROTOCOL +// +// https://github.com/aeternity/aepp-sdk-js/blob/a435e9df5c94004bcd16326b26c38a9c0b284279/src/aepp-wallet-communication/schema.ts#L32-L42 +// +// Only the request/responses that I have actually encountered in practice are +// enumerated here. There are many more things that the SDK code appears to +// use which I did not encounter in the wild. +//============================================================================= + +// TODO: need to do more experimentation with superhero to see what sorts of +// messages it sends back to the other requests + +//---------------------------------------------------------------------------- +// connection.announcePresence +//---------------------------------------------------------------------------- + +/** + * Waellet-to-aepp parameters of "connection.announcePresence" cast + * + * (layer 4) + */ +type Params_W2A_connection_announcePresence + = {id : string, + name : string, + origin : string, + type : "window" | "extension"}; + + + +/** + * Shape of the waellet-to-aepp "connection.announcePresence" RPC cast + * + * (layer 3) + */ +type RpcCast_W2A_connection_announcePresence + = RpcCast<"connection.announcePresence", + Params_W2A_connection_announcePresence>; + + +/** + * The actual waellet-to-aepp event passed when the wallet announces it exists + * + * (layer 2) + * + * @example + * + * ```json + * { + * "type": "to_aepp", + * "data": { + * "jsonrpc": "2.0", + * "method": "connection.announcePresence", + * "params": { + * "id": "{aee9e933-52b6-410a-8c3f-99c6be596b4e}", + * "name": "Superhero", + * "networkId": "ae_mainnet", + * "origin": "moz-extension://ee425d81-d5b2-44b6-9406-4da31b019e7c", + * "type": "extension" + * } + * } + * } + * ``` + */ +type EventData_W2A_connection_announcePresence + = EventData_W2A + + + +//---------------------------------------------------------------------------- +// connection.open +//---------------------------------------------------------------------------- + +/** + * Parameters of aepp-to-waellet "connection.open" call + * + * (layer 4) + */ +type Params_A2W_connection_open + = {name : string, + version : 1, + networkId? : string}; + + +/** + * Result type of "connection.open" call + * + * Same as `Params_W2A_connection_open` empirically + * + * (layer 4) + */ +type Result_W2A_connection_open + = Params_W2A_connection_announcePresence; + + + +/** + * Shape of aepp-to-waellet "connection.open" RPC call + * + * (layer 3) + */ +type RpcCall_A2W_connection_open + = RpcCall<"connection.open", + Params_A2W_connection_open>; + + + +/** + * Shape of waellet-to-aepp "connection.open" RPC response + * + * (layer 3) + */ +type RpcResp_W2A_connection_open + = RpcResp<"connection.open", + Result_W2A_connection_open>; + + + +/** + * The actual aepp-to-waellet "connection.open" event data passed over the message bus + * + * (layer 2) + */ +type EventData_A2W_connection_open + = EventData_A2W; + + + +/** + * The actual waellet-to-aepp "connection.open" event data passed over the message bus + * + * (layer 2) + */ +type EventData_W2A_connection_open + = EventData_W2A; + + + + +//---------------------------------------------------------------------------- +// address.subscribe +//---------------------------------------------------------------------------- + +/** + * Parameter type of aepp-to-waellet "address.subscribe" call + * + * (layer 4) + */ +type Params_A2W_address_subscribe + = {type : "subscribe", + value : "connected"}; + + + +/** + * Result type of waellet-to-aepp "address.subscribe response + * + * (layer 4) + * + * @example + * ```typescript + * {subscription : ["connected"], + * address : {current : {"ak_2Wsa8iAmAm917evwDEZjouvPUXKx2nUv5Uz8e8oNXTDfDXnMRN": {}}, + * connected : {}}} + * ``` + */ +type Result_W2A_address_subscribe + = {subscription : ["connected"], + address : {current : object, + connected : object}}; + + + +/** + * Shape of aepp-to-waellet "address.subscribe" RPC call + * + * (layer 3) + */ +type RpcCall_A2W_address_subscribe + = RpcCall<"address.subscribe", + Params_A2W_address_subscribe>; + + + +/** + * Result of waellet-to-aepp "address.subscribe" response + * + * (layer 3) + */ +type RpcResp_W2A_address_subscribe + = RpcResp<"address.subscribe", + Result_W2A_address_subscribe>; + + + +/** + * Actual aepp-to-waellet "address.subscribe" event data sent over the message bus + * + * (layer 2) + */ +type EventData_A2W_address_subscribe + = EventData_A2W; + + + +/** + * Actual waellet-to-aepp "address.subscribe" event data sent over the message bus + * + * (layer 2) + */ +type EventData_W2A_address_subscribe + = EventData_W2A; + + + +//---------------------------------------------------------------------------- +// transaction.sign (do not propagate) +//---------------------------------------------------------------------------- + +/** + * Parameters for "transaction.sign" (do not propagate) + * + * (layer 4) + */ +type Params_A2W_tx_sign_noprop + = {tx : string, + returnSigned : true, + networkId : string} + + + +/** + * Success result type for "transaction.sign" (do not propagate) + * + * (layer 4) + */ +type Result_W2A_tx_sign_noprop + = {signedTransaction : string}; + + +/** + * Request type for "transaction.sign" (do not propagate) + * + * (layer 3) + */ +type RpcCall_A2W_tx_sign_noprop + = RpcCall<"transaction.sign", + Params_A2W_tx_sign_noprop>; + + + +/** + * Response type for "transaction.sign" (do not propagate) + * + * (layer 3) + */ +type RpcResp_W2A_tx_sign_noprop + = RpcResp<"transaction.sign", + Result_W2A_tx_sign_noprop>; + + + +/** + * Event data for aepp-to-waellet "transaction.sign" (do not propagate) message + * + * (layer 2) + */ +type EventData_A2W_tx_sign_noprop + = EventData_A2W; + + + +/** + * Event data for aepp-to-waellet "transaction.sign" (do not propagate) response + * + * (layer 2) + */ +type EventData_W2A_tx_sign_noprop + = EventData_W2A; diff --git a/libs/awcp/tsconfig.json b/libs/awcp/tsconfig.json new file mode 100644 index 0000000..c870354 --- /dev/null +++ b/libs/awcp/tsconfig.json @@ -0,0 +1,16 @@ +{"compilerOptions" : {"target" : "es2022", + "strict" : true, + "esModuleInterop" : true, + "skipLibCheck" : true, + "forceConsistentCasingInFileNames" : true, + "noImplicitAny" : true, + "strictNullChecks" : true, + "strictPropertyInitialization" : true, + "sourceMap" : true, + "outDir" : "dist", + "declaration" : true}, + "$schema" : "https://json.schemastore.org/tsconfig", + "display" : "Recommended", + "include" : ["src/**/*"], + "exclude" : ["src/jx_include"], + "composite" : true} diff --git a/libs/parasite/.gitignore b/libs/parasite/.gitignore new file mode 100644 index 0000000..6d324d9 --- /dev/null +++ b/libs/parasite/.gitignore @@ -0,0 +1,6 @@ +*.swp +*.swo +jex_mindist +dist +src/jex_include +erl_crash.dump diff --git a/libs/parasite/LICENSE b/libs/parasite/LICENSE new file mode 100644 index 0000000..2e52b8d --- /dev/null +++ b/libs/parasite/LICENSE @@ -0,0 +1,16 @@ +ISC License + +Copyright (c) 2022 Peter Harpending + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + diff --git a/libs/parasite/Makefile b/libs/parasite/Makefile new file mode 100644 index 0000000..33e1aec --- /dev/null +++ b/libs/parasite/Makefile @@ -0,0 +1,14 @@ +all: pull build +deploy: pull build mindist push + +pull: + jx pull -f + +build: + jx build -f + +mindist: + jx mindist -f + +push: + jx push -f diff --git a/libs/parasite/README.md b/libs/parasite/README.md new file mode 100644 index 0000000..e69de29 diff --git a/libs/parasite/jex.eterms b/libs/parasite/jex.eterms new file mode 100644 index 0000000..1584815 --- /dev/null +++ b/libs/parasite/jex.eterms @@ -0,0 +1,5 @@ +{type, library}. +{realm, local}. +{name, parasite}. +{version, "0.1.0"}. +{deps, []}. diff --git a/libs/parasite/src/ae_compiler.ts b/libs/parasite/src/ae_compiler.ts new file mode 100644 index 0000000..decf930 --- /dev/null +++ b/libs/parasite/src/ae_compiler.ts @@ -0,0 +1,96 @@ +/******************************************************************* +** Compiler API +** +** The manner in which this module is laid out differs meaningfully +** from the `ae_node` module. +** +** - ae_node basically exposes a subset of the node interface as +** functions. It only black-boxes away the networking aspects. +** +** - part of the reason for this is that the data structures that get +** sent to the node tend to be pretty involved (lots of fields) +** +** - in general, the node interface is significantly more complicated +** than the compiler interface +** +** - by contrast, the data structures that get sent to the compiler +** tend to be pretty simple and only have a small number of fields +** +** - the manner in which this manifests is that ae_node functions +** tend to have a weird esoteric data structure as the input, +** because otherwise there would be too many parameters. +** +** here, there are few parameters, and they are passed directly +** +** - in general, the compiler has fewer things it can do, and the +** things it does tend to depend on less information. so this +** interface is significantly simpler than the node interface +*******************************************************************/ + +import * as net from './net.js'; + +const URL_COMPILER = 'https://compiler.aepps.com'; +// endpoints +const EPT_CompileContract = URL_COMPILER + '/compile'; +const EPT_EncodeCalldata = URL_COMPILER + '/encode-calldata'; + + +//------------------------------------------------------------------- +// HELPERS +//------------------------------------------------------------------- + +// make a CompileOpts data structure +function +compile_options(filename: string) + : object +{ + return {"backend" : "fate", + "file_system" : {}, + "src_file" : filename}; +} + + + +//------------------------------------------------------------------- +// API CALLS +//------------------------------------------------------------------- + +// send back compile response +async function +CompileContract(code : string, + filename : string) + : Promise +{ + let send_obj = + {"code" : code, + "options" : compile_options(filename)}; + + let response = await net.post_json_response(EPT_CompileContract, send_obj); + return response; +} + + + +async function +EncodeCalldata(code : string, + filename : string, + function_name : string, + function_args : Array) + : Promise +{ + let send_obj = + {"source" : code, + "options" : compile_options(filename), + "function" : function_name, + "arguments" : function_args}; + + let response = await net.post_json_response(EPT_EncodeCalldata, send_obj); + return response; +} + + + +export { + CompileContract, + EncodeCalldata +} diff --git a/libs/parasite/src/ae_node.ts b/libs/parasite/src/ae_node.ts new file mode 100644 index 0000000..932d540 --- /dev/null +++ b/libs/parasite/src/ae_node.ts @@ -0,0 +1,272 @@ +/******************************************************************** +** Node API: functions for talking to an Aeternity node +** +** In the future, everything that this module does will be replaced +** by the backend. +** +** Functions should be sorted in alphabetical order. +** +** Names are what they are in the documentation +** +** Useful links: +** +** - HTML API docs : https://api-docs.aeternity.io/ +** - YAML API docs : https://github.com/aeternity/aeternity/blob/master/apps/aehttp/priv/swagger.yaml +** +********************************************************************/ + + +import * as net from './net.js' +import * as ae_compiler from './ae_compiler.js' + +//------------------------------------------------------------------- +// CONSTANTS +//------------------------------------------------------------------- + +export const MIN_FEE = 16660000000000; +export const MIN_CONTRACT_FEE = 79080000000000; +export const MIN_GAS_PRICE = 1000000000; +export const URL_MAINNET = "https://mainnet.aeternity.io/v2"; +export const URL_TESTNET = "https://testnet.aeternity.io/v2"; + + +//------------------------------------------------------------------- +// CANONICAL TYPES ("MODELS") FROM THE DOCUMENTATION +// +// All of these names are as given in the documentation except `Error` +// (which is a reserve term in JS), renamed to `ErrorReason` +//------------------------------------------------------------------- + +// Error +// +// Docs: https://api-docs.aeternity.io/#/definitions/Error +// Docs: https://github.com/aeternity/aeternity/blob/v6.4.0/apps/aehttp/priv/swagger.yaml#L3168-L3172 +type ErrorReason = {reason: string}; + +type Tx = {tx: string}; + + + +//------------------------------------------------------------------- +// FUNCTIONS +// +// The names here follow their names in the documentation +//------------------------------------------------------------------- + + +//------------------------------------------------------------------- +// GetAccountNextNonce: /accounts/{pubkey}/next-nonce +// +// Docs: https://api-docs.aeternity.io/#/account/GetAccountNextNonce +// +// > Get an account's next nonce; This is computed according to +// > whatever is the current account nonce and what transactions are +// > currently present in the transaction pool +//------------------------------------------------------------------- + +type GetAccountNextNonce_params = {pubkey : string, + strategy? : "max" | "continuity"}; + +type GetAccountNextNonce_ret = {next_nonce: string}; + + +async function +GetAccountNextNonce(endpoint_url : string, + params : GetAccountNextNonce_params) + : Promise< GetAccountNextNonce_ret + | ErrorReason> +{ + let pubkey = params.pubkey; + let url = `${endpoint_url}/accounts/${pubkey}/next-nonce`; + + // if the "strategy" field is present, add it as a ?strategy=x + // option + // + // note if the field is absent from `params`, then + // `params.strategy` will be `undefined`, which in js whacko + // world is "falsy" + let strategy = params.strategy; + if (strategy) + { + let addon = `?strategy=${strategy}`; + url += addon; + } + + // irrespective of the response code, this is what we return + // so branching is gay + let ret = await net.get_json(url); + return ret; +} + + + +//------------------------------------------------------------------- +// PostContractCreate +// +// Docs: https://api-docs.aeternity.io/#/contract/PostContractCreate +//------------------------------------------------------------------- + +// > Get a contract_create transaction object + + +type ContractCreateTx = {owner_id : string, + nonce? : number, + code : string, + vm_version : number, + abi_version : number, + deposit : number, + amount : number, + gas : number, + gas_price : number, + fee : number, + ttl? : number, + call_data : string}; + + + +async function +PostContractCreate(endpoint_url : string, + body_obj : ContractCreateTx) + : Promise +{ + // console.log('body_obj', body_obj); + let url = `${endpoint_url}/debug/contracts/create`; + let ret = await net.post_json_response(url, body_obj); + return ret; +} + + + +async function +create_contract(whoami : string, + code : string, + filename : string, + init_args : Array) + : Promise +{ + let code_resp = await ae_compiler.CompileContract(code, filename); + let code_json = await code_resp.json(); + let bytecode = code_json.bytecode; + + let calldata_resp = await ae_compiler.EncodeCalldata(code, filename, "init", init_args); + // assert(calldata_resp.ok); + let calldata_json = await calldata_resp.json(); + let calldata = calldata_json.calldata; + + let cctx: ContractCreateTx = + {owner_id : whoami, + code : bytecode, + vm_version : 7, + abi_version : 3, + deposit : 0, + amount : 0, + gas : 25000, + gas_price : 1*MIN_GAS_PRICE, + fee : 1*MIN_CONTRACT_FEE, + call_data : calldata}; + + let ret = await PostContractCreate(URL_TESTNET, cctx); + + return ret; +} + + + +//------------------------------------------------------------------- +// PostSpend: /debug/transactions/spend +// +// Docs: +// - Input type : https://api-docs.aeternity.io/#/definitions/SpendTx +// - Return type : https://api-docs.aeternity.io/#/definitions/Tx +// - Function : https://api-docs.aeternity.io/#/transaction/PostSpend +// +// > Get a spend transaction object +//------------------------------------------------------------------- + +//------------------------------------------------------------------- +// SpendTx +// +// Docs: https://api-docs.aeternity.io/#/definitions/SpendTx +// Docs: https://github.com/aeternity/aeternity/blob/v6.4.0/apps/aehttp/priv/swagger.yaml#L2187-L2209 +//------------------------------------------------------------------- +type SpendTx = + {recipient_id : string, + amount : number, + fee : number, + ttl? : number, + sender_id : string, + nonce? : number, + payload : string}; + + + +async function +PostSpend(endpoint_url : string, + body_obj : SpendTx) + : Promise +{ + let url = `${endpoint_url}/debug/transactions/spend`; + let ret = await net.post_json(url, body_obj); + return ret; +} + + + +//------------------------------------------------------------------- +// PostTransaction +// +// Docs: https://api-docs.aeternity.io/#/contract/PostTransaction +// +// > Post a new transaction +//------------------------------------------------------------------- + +async function +PostTransaction(endpoint_url : string, + body_obj : Tx) + : Promise +{ + // console.log('body_obj', body_obj); + let url = `${endpoint_url}/transactions`; + let ret = await net.post_json_response(url, body_obj); + return ret; +} + + + +//------------------------------------------------------------------- +// GetTransactionInfoByHash +// +// Docs: https://api-docs.aeternity.io/#/contract/GetTransactionInfoByHash +//------------------------------------------------------------------- + +async function +GetTransactionInfoByHash(endpoint_url : string, + hash : string) + : Promise +{ + // console.log('body_obj', body_obj); + let url = `${endpoint_url}/transactions/${hash}/info`; + let ret = await net.get_json_response(url); + return ret; +} + + +//------------------------------------------------------------------- +// EXPORTS +//------------------------------------------------------------------- + +// type exports +export type { + ErrorReason, + Tx, + SpendTx, + ContractCreateTx +}; + +export { + GetAccountNextNonce, + PostContractCreate, + create_contract, + PostSpend, + PostTransaction +}; diff --git a/libs/parasite/src/net.ts b/libs/parasite/src/net.ts new file mode 100644 index 0000000..8416dbf --- /dev/null +++ b/libs/parasite/src/net.ts @@ -0,0 +1,85 @@ +//------------------------------------------------------------------- +// Common networking functions +// +// Mozilla fetch docs : https://developer.mozilla.org/en-US/docs/Web/API/fetch +//------------------------------------------------------------------- + + + +/* pf = pretty format +*/ +function pf(x : any) : string +{ + return JSON.stringify(x, undefined, 4); +} + + + +//------------------------------------------------------------------- +// FUNCTIONS +//------------------------------------------------------------------- + +// GET request with return type of JSON +async function +get_json(url: string) + : Promise +{ + let response = await fetch(url); + let ret = await response.json(); + return ret; +} + + + +// GET request with return type of JSON +async function +get_json_response(url: string) + : Promise +{ + let response = await fetch(url); + //let ret = await response.json(); + return response; +} + + + +// POST request with content type of json and return type of JSON +async function +post_json(url : string, + body_obj : any) + : Promise +{ + let body_str = pf(body_obj); + let req_opts = {method : 'POST', + body : body_str, + headers : {"Content-Type": "application/json"}}; + let response = await fetch(url, req_opts); + let ret = await response.json(); + return ret; +} + + +async function +post_json_response(url : string, + body_obj : any) + : Promise +{ + let body_str = pf(body_obj); + let req_opts = {method : 'POST', + body : body_str, + headers : {"Content-Type": "application/json"}}; + let response = await fetch(url, req_opts); + return response; +} + + + +//------------------------------------------------------------------- +// EXPORTS +//------------------------------------------------------------------- +export { + get_json, + get_json_response, + post_json, + post_json_response +} diff --git a/libs/parasite/tsconfig.json b/libs/parasite/tsconfig.json new file mode 100644 index 0000000..c870354 --- /dev/null +++ b/libs/parasite/tsconfig.json @@ -0,0 +1,16 @@ +{"compilerOptions" : {"target" : "es2022", + "strict" : true, + "esModuleInterop" : true, + "skipLibCheck" : true, + "forceConsistentCasingInFileNames" : true, + "noImplicitAny" : true, + "strictNullChecks" : true, + "strictPropertyInitialization" : true, + "sourceMap" : true, + "outDir" : "dist", + "declaration" : true}, + "$schema" : "https://json.schemastore.org/tsconfig", + "display" : "Recommended", + "include" : ["src/**/*"], + "exclude" : ["src/jx_include"], + "composite" : true} diff --git a/sidekick/.gitignore b/sidekick/.gitignore new file mode 100644 index 0000000..6b22493 --- /dev/null +++ b/sidekick/.gitignore @@ -0,0 +1,19 @@ +*.swp +*.swo +*.beam +dist_js +sidekick_dist +docs +examples/examples_dist_js +www_tree +prose +*.hi +*.o +sidekick_mindist +sidekick_fulldist +dist +jx_mindist +*jx_include* +jex_mindist +src/jex_include +erl_crash.dump diff --git a/sidekick/LICENSE b/sidekick/LICENSE new file mode 100644 index 0000000..2e52b8d --- /dev/null +++ b/sidekick/LICENSE @@ -0,0 +1,16 @@ +ISC License + +Copyright (c) 2022 Peter Harpending + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + diff --git a/sidekick/Makefile b/sidekick/Makefile new file mode 100644 index 0000000..131d276 --- /dev/null +++ b/sidekick/Makefile @@ -0,0 +1,15 @@ +all: prepare build + +deploy: prepare build mindist push + +prepare: + jx pull -f + +build: + jx build -f + +mindist: + jx mindist -f + +push: + jx push -f diff --git a/sidekick/README.md b/sidekick/README.md new file mode 100644 index 0000000..be83c94 --- /dev/null +++ b/sidekick/README.md @@ -0,0 +1,136 @@ +# Sidekick + +Sidekick is a simple JavaScript library for talking to an Aeternity +browser wallet extension such as Jaeck Russell or Superhero from the document +context of a webpage. + +> One of the most important things for designing a computer, which I +> think most designers don't do, is you study the problem you want to +> solve. And then use what you learn from studying the problem you +> want to solve to put in the mechanisms needed to solve it in the +> computer you're building. No more, no less. + +— Gerald Jay Sussman + + + +# Build Prereqs + +Assuming Ubuntu 18.04. Adapt these instructions for your own system. + +You need + +- `npm` to build TypeScript +- `tsc` to compile +- [`jx`][jx] to orchestrate the build properly +- Python 3.6 or later to run `jx` + +[jx]: https://gitlab.com/pharpend/jx/-/tree/master/#jx-secure-typescript-package-manager + +Steps + + sudo snap refresh + sudo snap install node --channel 18/stable + npm install -g typescript + wget https://gitlab.com/pharpend/jx/-/raw/master/jx -O ~/.local/bin/jx + chmod u+x ~/.local/bin/jx + + + +## Protip: avoid using `sudo npm install -g` + +If you want to avoid using sudo + +``` +mkdir ~/.npm-packages +npm config set prefix "${HOME}/.npm-packages" +``` + +Edit `~/.bashrc` or `~/.zshrc` with + +``` +NPM_PACKAGES="${HOME}/.npm-packages" +export PATH=$NPM_PACKAGES/bin:$PATH +``` + + + +# Build + + make + + +# Examples + +There is a repository of examples at https://gitlab.com/pharpend/sidekick_examples.git + +# What sidekick is NOT + +Sidekick is **not** ideal if you operate in the Node/NPM ecosystem. If you are +working in the Node/NPM ecosystem, you probably want the [Aeternity JavaScript +SDK](https://github.com/aeternity/aepp-sdk-js/). Every single +Aeternity-related thing you could ever possibly want to do is possible to +accomplish with the SDK. + +All that Sidekick knows how to do is talk to a browser wallet extension. In an +application, there is a great deal of necessary functionality (e.g. forming +transactions for the wallet to sign) which sidekick assumes your server-side +code has already handled. + +In all software there is a tradeoff between simplicity and number of features. +Sidekick is very simple: 2300 lines of TypeScript, including comments, with no +dependencies. Therefore Sidekick intentionally only has a very limited set of +features. + + + +# Where is the NPM package or the webpack bundle? + +There isn't one. + +## Why? + +Sidekick is a library that deals with cryptocurrency. The security +model is based on transparency and trust. A user must be able to +inspect code that is running on his hardware handling his money. + +We don't support NPM because of the security issues that NPM +introduces. Briefly, the code can change at any time under the +developer's nose without the developer knowing. The [leftpad +debacle][lpad] and the [RIAEvangelist debacle][ria] are good examples +of the types of vulnerabilities that package managers like NPM +enable. + +[lpad]: https://archive.ph/Qsh7j +[ria]: https://archive.ph/OF5I9 + +We don't use something like webpack because that introduces an opaque +rewrite using an untrusted tool. Webpack could plausibly alter the +runtime behavior of the program, and we would have no way to detect +that. Even if we trusted webpack, how do we obtain webpack? NPM. +So. + +It is debatable whether or not we should trust the TypeScript +compiler (TSC). On the whole, TSC probably makes Sidekick more +secure, simply by virtue of increasing overall code quality and +eliminating the largest categories of potential bugs. Moreover, the +output that TSC produces is human-readable, and has a very +straightforward mapping to the original source code. A human can +easily read the TSC-generated JavaScript tree, even without the aid +of the source map, and have a high degree of faith that the code is +trustworthy and that TSC is behaving as promised. + + + + +Source: https://github.com/sindresorhus/guides/blob/main/npm-global-without-sudo.md + + +# How to build and view documentation + +``` +make build_docs +make serve_docs +``` + + diff --git a/sidekick/examples/.gitignore b/sidekick/examples/.gitignore new file mode 100644 index 0000000..682a3d7 --- /dev/null +++ b/sidekick/examples/.gitignore @@ -0,0 +1,6 @@ +*.swp +*.swo +src/jx_include +dist/ +erl_crash.dump +src/jex_include diff --git a/sidekick/examples/LICENSE b/sidekick/examples/LICENSE new file mode 100644 index 0000000..2e52b8d --- /dev/null +++ b/sidekick/examples/LICENSE @@ -0,0 +1,16 @@ +ISC License + +Copyright (c) 2022 Peter Harpending + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + diff --git a/sidekick/examples/Makefile b/sidekick/examples/Makefile new file mode 100644 index 0000000..e074f31 --- /dev/null +++ b/sidekick/examples/Makefile @@ -0,0 +1,10 @@ +all: prepare build + +prepare: + jx pull -f + +build: + jx build -f + +serve: + python3 -m http.server 8001 diff --git a/sidekick/examples/README.md b/sidekick/examples/README.md new file mode 100644 index 0000000..91c5c99 --- /dev/null +++ b/sidekick/examples/README.md @@ -0,0 +1,90 @@ +# sidekick examples + +# TODO + +- form a spend transaction +- transfer money on testnet +- retrieve transaction info + +# Build Prereqs + +Assuming Ubuntu 18.04. Adapt these instructions for your own system. + +You need + +- `npm` to build TypeScript +- `tsc` to compile +- [`jx`][jx] to orchestrate the build properly +- [sidekick][sk] as a dependency +- Python 3.6 or later to run `jx` + +[jx]: https://gitlab.com/pharpend/jx/-/tree/master/#jx-secure-typescript-package-manager + +Steps + + # install node and tsc + sudo snap refresh + sudo snap install node --channel 18/stable + npm install -g typescript + # install dependencies + mkdir vanillae && cd vanillae + git clone https://gitlab.com/pharpend/jx + git clone https://gitlab.com/pharpend/sidekick + chmod u+x ~/.local/bin/jx + git clone https + + + +## Protip: avoid using `sudo npm install -g` + +If you want to avoid using sudo + +``` +mkdir ~/.npm-packages +npm config set prefix "${HOME}/.npm-packages" +``` + +Edit `~/.bashrc` or `~/.zshrc` with + +``` +NPM_PACKAGES="${HOME}/.npm-packages" +export PATH=$NPM_PACKAGES/bin:$PATH +``` + +## How to build + +You need + +- [TypeScript compiler `tsc`](https://www.typescriptlang.org/download) +- [HTTP logging server vlogd](https://gitlab.com/pharpend/vlogd) +- [Build tool `jx`](https://gitlab.com/pharpend/jx) +- Build [sidekick](https://gitlab.com/pharpend/sidekick) separately +- Python 3.6 or later +- `make` + +#### Build + +You need to build [sidekick](https://gitlab.com/pharpend/sidekick) separately + +``` +make +``` + +#### View + +Many of the examples assume there is an HTTP server at `localhost:8841` +which responds positively to POST requests at `/` + +[vlogd](https://gitlab.com/pharpend/vlogd) is such a server, but you can roll +your own if you want. + +``` +make serve +firefox localhost:8001 +``` + + + +# Build + + make diff --git a/sidekick/examples/connection.html b/sidekick/examples/connection.html new file mode 100644 index 0000000..be48b7e --- /dev/null +++ b/sidekick/examples/connection.html @@ -0,0 +1,88 @@ + + + + + + Sidekick Example: Connecting to Wallet + + + + +
  • + Examples +
    • + Connecting to wallet +
    +
+ +

Sidekick Example: Connecting to wallet

+ +

Page variables

+ +
    +
  1. Wallet Address:
  2. +
  3. SpendTx Base58:
  4. +
+ + + +

1. Detect wallet

+ +

not detected

+

+
+

2. Connect to wallet

+ +

not connected

+

+
+

3. Get the wallet address

+ +

not addressed

+

+
+

4. Sign a transaction

+ +

4.1 Transaction info (information needed from you)

+ +
    +
  1. Source address (step 3):
  2. +
  3. Target address:
  4. +
  5. Amount (aettos):
  6. +
  7. Network: (FIXME: radio buttons)
  8. +
+ + +

4.2 Form transaction (using parasite)

+ +

This uses a shim JavaScript library called parasite to form the transaction +data for the wallet to sign. In a commercial application, this should be done +by your backend

+ + + +
SpendTx JS object
+

+
+
SpendTx Base58
+

+
+

4.3 Sign the transaction (sidekick)

+ +
    +
  1. +
+ + + +
Result:
+

+
+

4.4 Propagate the transaction (parasite)

+ +

4.5 Verify the transaction was propagated (parasite)

+ + + + + diff --git a/sidekick/examples/favicon.ico b/sidekick/examples/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7cbc7a53e136cca1768feff963a17211af88de3a GIT binary patch literal 4166 zcmeH|F>=&E5Jh`;%eCQd1fdAY-~d$VD82@nbQEr49fAWWBy`|_av6*Ff6t8MB_M)G zL#;HgyQlk4w_HRV_&1J6zdv6i-bKVmej`7Owrjhuy#sp(_73bF*gLRyVDG@T1Ixc( z&)<#@n<~F3f7tL3$=L*0-MIxkn@^1&&1-_m9L-;NsD?TH{S9|Fd|8&C5j-=0Ab?#y zl_7U;{A4Bzr?e0_NA&GH0w?q8A0vhqkDpWUOB@&O$a(yW_+G998@K6IS0CBFI%s)$ z4^J?O<$C2;8F{(nPx&j)zTV1{gFu4KtCnlvgqiNg<3UOU$bY7$fdr%ADm^;+_`* zazum=FalCgwG$^Z_I7RnIhQ=QJdL~b=f#Tv;*$}mz`_sp8_9f6odik%tlM?kSP=T| zzXz#{P+lx@jw#$Io>CBI7Ca{#i)Qkd`K4y@)u5E|Lt#XsM4kY literal 0 HcmV?d00001 diff --git a/sidekick/examples/hello.html b/sidekick/examples/hello.html new file mode 100644 index 0000000..2e9b396 --- /dev/null +++ b/sidekick/examples/hello.html @@ -0,0 +1,32 @@ + + + + + Sidekick Example: Hello World + + + + + + + +

Sidekick Example: Hello World

+ +

Check the console

+ +

+ This example just demonstrates how to import sidekick and call functions + from the module. Useful for debugging purposes, or getting out of mental + quicksand. +

+ + + + + + diff --git a/sidekick/examples/index.html b/sidekick/examples/index.html new file mode 100644 index 0000000..2ee4d77 --- /dev/null +++ b/sidekick/examples/index.html @@ -0,0 +1,25 @@ + + + + + Sidekick Examples: Home + + + + +
  • + Examples +
+ +

Sidekick Examples

+
    +
  1. Hello World
  2. + +
  3. Connecting to Wallet
  4. +
+ + diff --git a/sidekick/examples/jex.eterms b/sidekick/examples/jex.eterms new file mode 100644 index 0000000..aca9cd7 --- /dev/null +++ b/sidekick/examples/jex.eterms @@ -0,0 +1,7 @@ +{type, site}. +{realm, local}. +{name, sk_examples}. +{version, "0.1.0"}. +{deps, ["local-awcp-0.1.0", + "local-parasite-0.1.0", + "local-sidekick-0.1.0"]}. diff --git a/sidekick/examples/scratch/logging.html b/sidekick/examples/scratch/logging.html new file mode 100644 index 0000000..70ee0f2 --- /dev/null +++ b/sidekick/examples/scratch/logging.html @@ -0,0 +1,32 @@ + + + + + Sidekick Example: console logger + + + + + + +

Sidekick Example: console logger

+ +

check the console

+ +

+ This example illustrates a debugging feature of sidekick. Sidekick + and the wallet each communicate with the other by sending + MessageEvents to window. The code in this example + adds a listener to the window which just + console.logs each MessageEvent. +

+ + + + + diff --git a/sidekick/examples/scratch/logging.ts b/sidekick/examples/scratch/logging.ts new file mode 100644 index 0000000..f3fa492 --- /dev/null +++ b/sidekick/examples/scratch/logging.ts @@ -0,0 +1,6 @@ +import * as sk from './jx_include/ppr-sidekick-0.1.0/dist/sidekick.js' + +let logger = new sk.ConsoleLogger(); +logger.listen(window); + +window.postMessage("I have questions regarding my vehicle's extended warranty."); diff --git a/sidekick/examples/scratch/logging_http.html b/sidekick/examples/scratch/logging_http.html new file mode 100644 index 0000000..9598672 --- /dev/null +++ b/sidekick/examples/scratch/logging_http.html @@ -0,0 +1,28 @@ + + + + + + Sidekick Example: HTTP logger + + + + + + +

Sidekick Example: HTTP Logger

+ +

+ You need to have an HTTP server listening on port 8841. You + can use vlogd. +

+ + + + + diff --git a/sidekick/examples/scratch/logging_http.ts b/sidekick/examples/scratch/logging_http.ts new file mode 100644 index 0000000..b16de9c --- /dev/null +++ b/sidekick/examples/scratch/logging_http.ts @@ -0,0 +1,23 @@ +import * as sk from './jx_include/ppr-sidekick-0.1.0/dist/sidekick.js' + +//var body_count = 1; +// +//function +//message_event_to_request +// (evt : {data: object}) +//{ +// let url = 'http://localhost:8841/'; +// let body = JSON.stringify(evt.data, undefined, 4); +// let result = new Request(url, {method: 'POST', body: body}); +// return result; +//} + +let cl = new sk.ConsoleLogger(); +cl.listen(window); + +let hl = new sk.HttpLogger('http://localhost:8841/'); +hl.listen(window); + +let logger = new sk.SeqLogger([cl, hl]); + +window.postMessage("I have questions regarding my vehicle's extended warranty."); diff --git a/sidekick/examples/src/connection.ts b/sidekick/examples/src/connection.ts new file mode 100644 index 0000000..145e339 --- /dev/null +++ b/sidekick/examples/src/connection.ts @@ -0,0 +1,219 @@ +import * as ae_node from './jex_include/local-parasite-0.1.0/dist/ae_node.js'; +import * as awcp from './jex_include/local-awcp-0.1.0/dist/awcp.js'; +import * as sk from './jex_include/local-sidekick-0.1.0/dist/sidekick.js'; + +var pv_address: string; +var pv_spendtx_base58: string; + +function +set_pv_address + (new_address: string) + : void +{ + pv_address = new_address; + for (let elt of document.getElementsByClassName('pv-address')) + { + elt.innerHTML = new_address; + } +} + +function +set_pv_spendtx_base58 + (new_address: string) + : void +{ + pv_spendtx_base58 = new_address; + for (let elt of document.getElementsByClassName('pv-spendtx-base58')) + { + elt.innerHTML = new_address; + } +} + + + +async function +detect + (logger: sk.Logger) + : Promise +{ + let h4 = document.getElementById("detected")!; + let pre = document.getElementById("detect-info")!; + + h4.innerHTML = 'detecting...'; + h4.style.color = 'GoldenRod'; + + // try to detect the wallet + // will fail on timeout error + // console logger + let maybe_wallet_info =//: sk.Safe = + await sk.detect(sk.TIMEOUT_DEF_DETECT_MS, "failed to detect wallet", logger); + + console.log(maybe_wallet_info); + + // ok means wallet was detected + if (maybe_wallet_info.ok) + { + h4.innerHTML = "detected"; + h4.style.color = "green"; + pre.innerHTML = JSON.stringify(maybe_wallet_info.result, undefined, 4); + } + else + { + h4.innerHTML = "error"; + h4.style.color = "crimson"; + pre.innerHTML = JSON.stringify(maybe_wallet_info.error, undefined, 4); + } +} + +async function +connect + (logger: sk.Logger) + : Promise +{ + // this is not working + // I am creating and posting an identical message to the working example + // it must be a mismatch in the extraneous properties of the MessageEvent that is causing the problem + let h4 = document.getElementById("connected")!; + let pre = document.getElementById("connect-info")!; + + h4.innerHTML = 'connecting...'; + h4.style.color = 'GoldenRod'; + + // try to connect to the wallet + // will fail on timeout error + // console logger + let maybe_wallet_info = await sk.connect( + 'ske-connect-1', + {name: 'sidekick examples', + version: 1}, + sk.TIMEOUT_DEF_CONNECT_MS, + "failed to connect to wallet", + logger + ); + + console.log(maybe_wallet_info); + + // ok means wallet was connected + if (maybe_wallet_info.ok) + { + h4.innerHTML = "connected"; + h4.style.color = "green"; + pre.innerHTML = JSON.stringify(maybe_wallet_info.result, undefined, 4); + } + else + { + h4.innerHTML = "error"; + h4.style.color = "crimson"; + pre.innerHTML = JSON.stringify(maybe_wallet_info.error, undefined, 4); + } +} + +async function +address + (logger: sk.Logger) + : Promise +{ + // this is not working + // I am creating and posting an identical message to the working example + // it must be a mismatch in the extraneous properties of the MessageEvent that is causing the problem + let h4 = document.getElementById("addressed")!; + let pre = document.getElementById("address-info")!; + + h4.innerHTML = 'addressing...'; + h4.style.color = 'GoldenRod'; + + // try to address to the wallet + // will fail on timeout error + // console logger + let maybe_wallet_info = await sk.address( + 'ske-address-1', + {type: 'subscribe', + value: 'connected'}, + sk.TIMEOUT_DEF_ADDRESS_MS, + "failed to address to wallet", + logger + ); + + console.log(maybe_wallet_info); + + // ok means wallet was addressed + if (maybe_wallet_info.ok) + { + h4.innerHTML = "addressed"; + h4.style.color = "green"; + pre.innerHTML = JSON.stringify(maybe_wallet_info.result, undefined, 4); + // update global variable + let the_address = Object.keys(maybe_wallet_info.result.address.current)[0]; + set_pv_address(the_address); + } + else + { + h4.innerHTML = "error"; + h4.style.color = "crimson"; + pre.innerHTML = JSON.stringify(maybe_wallet_info.error, undefined, 4); + } +} + +async function +form_tx + (logger: sk.Logger) + : Promise +{ + // @ts-ignore + let recipient_id = document.getElementById('tx-info-target')!.value; + // @ts-ignore + let amount = parseInt(document.getElementById('tx-info-amount')!.value); + let fee = ae_node.MIN_FEE; + let sender_id = pv_address; + let payload = 'hainana'; + let spendtx = {recipient_id : recipient_id, + amount : amount, + fee : fee, + sender_id : sender_id, + payload : payload}; + document.getElementById('spendtx-json')!.innerHTML = + JSON.stringify(spendtx, undefined, 4); + let tx = await ae_node.PostSpend(ae_node.URL_TESTNET, spendtx); + // @ts-ignore + set_pv_spendtx_base58(tx.tx); + //document.getElementById('spendtx-base58')!.innerHTML = + // JSON.stringify(tx, undefined, 4); + // +} + +async function +sign_tx + (logger: sk.Logger) + : Promise +{ + let sign_params = { + tx: pv_spendtx_base58, + // returnsigned: false tells it to propagate the transaction + // returnsigned: true just signs it and returns the signed tx back + returnSigned: true as true, + networkId: "ae_uat" + }; + let signedTx = await sk.tx_sign_noprop('sk-tx-sign-1', sign_params, sk.TIMEOUT_DEF_TX_SIGN_NOPROP_MS, 'sign transaction timed out', logger); + document.getElementById("sign-spendtx-result")!.innerHTML = JSON.stringify(signedTx, undefined, 4); +} + +function +main + () +{ + // want to create a logger down here + let logger = sk.cl(); + // custom logger + window.addEventListener('message', function(evt) { if("to_waellet" === evt.data.type) {console.log(evt)} }); + // the !s are typescript jizz to turn off the warning that the return value + // of getElementById might be null + document.getElementById('detect')!.onclick = function() { detect(logger); } ; + document.getElementById('connect')!.onclick = function() { connect(logger); }; + document.getElementById('address')!.onclick = function() { address(logger); }; + document.getElementById('mk-spendtx')!.onclick = function() { form_tx(logger); }; + document.getElementById('sign-spendtx')!.onclick = function() { sign_tx(logger); }; + + //set_pv_address("ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE"); +} + +main(); diff --git a/sidekick/examples/src/hello.ts b/sidekick/examples/src/hello.ts new file mode 100644 index 0000000..d833f41 --- /dev/null +++ b/sidekick/examples/src/hello.ts @@ -0,0 +1,3 @@ +import * as sk from './jex_include/local-sidekick-0.1.0/dist/sidekick.js'; + +sk.hello(); diff --git a/sidekick/examples/tsconfig.json b/sidekick/examples/tsconfig.json new file mode 100644 index 0000000..f240cdc --- /dev/null +++ b/sidekick/examples/tsconfig.json @@ -0,0 +1,16 @@ +{"compilerOptions" : {"target" : "es2022", + "strict" : true, + "esModuleInterop" : true, + "skipLibCheck" : true, + "forceConsistentCasingInFileNames" : true, + "noImplicitAny" : true, + "strictNullChecks" : true, + "strictPropertyInitialization" : true, + "sourceMap" : true, + "outDir" : "dist", + "declaration" : true}, + "$schema" : "https://json.schemastore.org/tsconfig", + "display" : "Recommended", + "include" : ["src/**/*"], + "exclude" : ["src/jex_include"], + "composite" : true} diff --git a/sidekick/icons/sidekick_icon_128.png b/sidekick/icons/sidekick_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..fe60ff26f8efc4592548771f93ec87b76b73f812 GIT binary patch literal 2883 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 literal 0 HcmV?d00001 diff --git a/sidekick/icons/sidekick_icon_128.xcf b/sidekick/icons/sidekick_icon_128.xcf new file mode 100644 index 0000000000000000000000000000000000000000..a1a9aeba26dd48b6461a9e0d6ee50987b887ac36 GIT binary patch literal 5815 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 l7T8e3rLHF