start sidekick docs
This commit is contained in:
@@ -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 <https://aegora.jp/r/115/sign_mess-2.js>.
|
||||||
|
|
||||||
|
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_t>
|
||||||
|
= {ok : true,
|
||||||
|
result : ok_t};
|
||||||
|
|
||||||
|
type Error<err_t>
|
||||||
|
= {ok : false,
|
||||||
|
error : err_t};
|
||||||
|
|
||||||
|
type Safe<ok_t, err_t>
|
||||||
|
= Ok<ok_t>
|
||||||
|
| Error<err_t>;
|
||||||
|
```
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user