From e470413f77da40293a2a099e28c19016a83ad2ca Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 2 May 2023 16:45:20 -0600 Subject: [PATCH] start sidekick docs --- docs/sk-awcp/README.md | 274 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 docs/sk-awcp/README.md diff --git a/docs/sk-awcp/README.md b/docs/sk-awcp/README.md new file mode 100644 index 0000000..c407dd6 --- /dev/null +++ b/docs/sk-awcp/README.md @@ -0,0 +1,274 @@ +# Sidekick, AWCP, and Jex + +## Contents + +## Background and Motivation + +Sidekick is a very simple TypeScript/JavaScript library for talking to an +Aeternity browser wallet extension (e.g. Superhero or Jack Russell) from a page +script. + +AWCP (Æpp-Wællet Communication Protocol) is the messaging protocol between an +extension and a wallet. Sidekick implements this from the perspective of a page +script (the "Æpp"). + +Jex is my homemade alternative to NPM. It is currently super confusing and +unusable, but I maintain it is significantly better than NPM. + +The motivation was [Aegora.jp](https://aegora.jp/), which is a basic e-commerce +site where items are priced in Aeternity cryptocurrency. + +We (Craig and Peter) started working on Aegora around January of 2022, and +launched on December 8, 2022. + +Our first task was quite simple: discover if the user has a wallet installed. +If so, make the page background green; if not, make it red. + +This seemingly simple task was absurdly difficult with the Aeternity JavaScript +SDK, at least in its state as of early 2022. The SDK has been improved +significantly since that time, but it is still nowhere near production quality. +To this day, I would have no idea how to do that task. + +Here is how to do it with Sidekick: + +```ts +// from https://github.com/aeternity/Vanillae/blob/892c9cd9da5dcbf50224a26b8da9ab2fd38435ab/sidekick/examples/src/connection.ts#L34-L66https://github.com/aeternity/Vanillae/blob/892c9cd9da5dcbf50224a26b8da9ab2fd38435ab/sidekick/examples/src/connection.ts#L34-L66 +let maybe_wallet = await sk.detect(sk.TIMEOUT_DEF_DETECT_MS, // timeout + "failed to detect wallet", // error if timeout + logger); // logger +``` + +You have to pass in a logger to each call. You probably want `sk.cl()` which +is the console logger. Sidekick exports a few other loggers, as well as the +`sk.Logger` interface if you want to write a custom one. For instance, maybe +you want to log the page script/wallet conversation on your own server, so if +something goes wrong, you hear about it. You can write your own logger which +`POST`s the logs back to your own server, and just pass it in as an argument. + +Anyway, back in Februaryish of 2022, I ended up going to work trying to figure +out how the SDK was communicating with Superhero. It turns out there's an +event bus available to both the document and extension context, and posting +messages in said event bus is how the two communicate. + +Sidekick started at this time as just a scratch project to figure out what the +SDK was actually doing. I (Peter) wrote it originally as a fuzzer that +replayed the SDK's half of the conversation, with no intention of ever using it +in production. + +At some point, it dawned on us that I had written a 2000 line TypeScript +library that did exactly what we needed for Aegora purposes, that we understood +perfectly, was easy to use, and was entirely self-contained (i.e. no external +dependencies, could be wrapped up in a tarball and dropped on the server). And +maybe we should just use that. So we did. + +By the way, the tooling to do "just wrap it up in a tarball and drop it on a +server" is Jex. + +## Annotated example: Aegora log-in code + +Background: we don't use passwords to do login at Aegora (we don't store *any* +private information about our users). Instead, we have your wallet +cryptographically sign a random message, and then verify the signature. + +Here is the page script for the Aegora log in page, annotated. +You can follow along reading . + +The first thing we do is import sidekick + +```js +import * as sk from "./sidekick-0.2.0/dist/sidekick.js"; +``` + +The idea here is you can just unpack the tarball on your server and just use +it. Everything is versioned so you can take advantage of caching. + +Here is the super complicated `main` function: + +```js +function main() +{ + let logger = sk.cl(); + detect(logger); +} +``` + +Let's look at the `detect` function. + +```js +async function detect(logger) +{ + // try to detect the wallet + let detective = await sk.detect(sk.TIMEOUT_DEF_DETECT_MS, "no waellet", logger); + // if the wallet was detected, then proceed to the next step + if (detective.ok) + { + connect(logger); + } + // otherwise show an error message + else + { + console.log(detective); + let failure = document.getElementById('failure'); + searching.style.display = "none"; + searching.style.visibility = "hidden"; + failure.style.display = "block"; + failure.style.visibility = "visible"; + } +} +``` + +That's basically the entire pattern. There's a sequence of steps to talk to the wallet: + +1. `detect` +2. `connect` +3. `address` +4. You've now established a conversation with the wallet, and you now + do one of two things: + + 1. Ask the wallet to sign a transaction: `tx_sign_noprop`. + 2. Ask the wallet to sign a message: `msg_sign`. + + This is what we're going to do here shortly + +Every "porcelain" call in sidekick returns this `Safe` type: + +```ts +type Ok + = {ok : true, + result : ok_t}; + +type Error + = {ok : false, + error : err_t}; + +type Safe + = Ok + | Error; +``` + +This allows you to use an `if` statement to ask the simple question "did it +work or not"? The pattern here is: + +1. Perform a step +2. Use an `if` statement to tell if it worked or not. (The user rejecting a request from the page counts as "not working"). +3. If it worked, proceed to the next step +4. If it didn't, show an error message + +You might want to re-read the `detect` function, and then let's continue with `connect`: + + +```js +async function connect(logger) +{ + let maybe_wallet = await sk.connect('ske-connect-1', // arbitrary string|number. for sorting out "this message from the wallet is the response to this prior message from the page script" + {name: 'Aegora', version: 1}, // message we're sending to the wallet. name is arbitrary, version must be 1 + 2000, // timeout (in milliseconds); this call is instantaneous in practice + "failed to connect to wallet", // error on timeout + logger); + let searching = document.getElementById('searching'); + // if it worked, proceed to read_key + if (maybe_wallet.ok) + { + searching.style.display = "none"; + searching.style.visibility = "hidden"; + let found = document.getElementById('sign_dis_chit'); + found.style.display = "block"; + found.style.visibility = "visible"; + read_key(logger); + } + // otherwise show an error message + else + { + console.log(maybe_wallet); + let failure = document.getElementById('failure'); + searching.style.display = "none"; + searching.style.visibility = "hidden"; + failure.style.display = "block"; + failure.style.visibility = "visible"; + } +} +``` + +Get the picture? Alright, let's look at `read_key`: + +```js +async function read_key(logger) +{ + // get the user's public key + let wallet_info = await sk.address('ske-address-1', // coordination id + {type: 'subscribe', value: 'connected'}, // message you're actually sending to the wallet, must be exactly this + 300000, // timeout (this pops up a confirm dialog for the user, so you need to give the user time to read it) + "failed to address to wallet", // error on timeout + logger); + // if it worked, dig out the key and proceed + if (wallet_info.ok) + { + // this call returns this ridiculous data structure where the user's + // public keys are the keys in a hashmap that point to empty objects + // + // it's literally this: + // + // {ok : true, + // result : {subscription : ["connected"], + // address : {current : {"ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE": {}}, + // connected : {"ak_21HW2BeR8KQnzB76b9RSeNAXFf8SEvquLG3ichyLaXhdxUpXe9" : {}, + // "ak_Bd9rA8pDWucwfriVp6Zgb68csxanCzWDqstyoBKBbzUnNhpKQ" : {}, + // "ak_TuwioiZCt3Ajx9dgVS9qdnS9VW1t4GMWFML5zBPgzouZUGUDA" : {}, + // "ak_ywR1N7GDpj7djeEEEnHSTmYbQmxvWCvFgfsLpxdFpK1ptkZMU" : {}}}}} + // + // so you need this beautiful line of code to actually dig out the key: + let pk = Object.keys(wallet_info.result.address.current)[0]; + // proceed + sign_mess(pk, logger); + } + // otherwise, error + else + { + console.log(wallet_info); + let failure = document.getElementById('failure'); + failure.style.display = "block"; + failure.style.visibility = "visible"; + } +} +``` + +Alright, let's look at actually signing the message: + +```js +async function sign_mess(pk, logger) +{ + let public_key = document.getElementById('public_key'); + public_key.value = pk; + // this field has the message we're going to have the user sign + let blob = document.getElementById('unsigned').value; + // ask the wallet to sign it + let signature = await sk.msg_sign('ske-msg_sign-1', + pk, + blob, + sk.TIMEOUT_DEF_MSG_SIGN_MS, + 'message signing took too long', + logger); + // if it worked, populate the form fields and make the form submittable + if (signature.ok) + { + let signed_data = signature.result.signature; + let signed = document.getElementById('signed'); + signed.value = signature.result.signature; + // oh that's right + // forgot about this + // for some reason, javascript indexes timezones by negative minutes + // everything in this language is like this + let ts = document.getElementById('ts'); + ts.value = new Date().getTimezoneOffset() * -60; + let submit_button = document.getElementById('submit_button'); + submit_button.disabled = false; + } + // otherwise, show the error in the signature field + else + { + console.log(signature); + let signed = document.getElementById('signed'); + signed.value = signature; + } +} +```