wrote out architecture rant in readme

This commit is contained in:
2023-07-12 17:17:03 -06:00
parent 373d43dd8c
commit 984f562579
16 changed files with 460 additions and 1411 deletions
+173 -1
View File
@@ -1,5 +1,177 @@
# Jack Russell Extension # Jack Russell Extension
## File Tree This is a browser extension wallet for the Aeternity payment network. It is
meant to work in the latest versions of Firefox and Chromium.
## How it works
You may want to read about how [AWCP](../libs/awcp/src/awcp.ts) and
[Sidekick](../sidekick/src/sidekick.ts) work first. Briefly,
- AWCP (Aepp-Waellet Communication Protocol) defines the message protocol
between a page script (the "aepp") and a wallet (the "waellet"). A page
script is something that you include with `<script src="./foo/bar.js">`
- Sidekick is a library (for page scripts) of TS/JS function calls that
blackboxes away all of the stupid implementation detail nonsense about how to
actually send and recieve the messages defined by AWCP. For instance, there
is a function call `tx_sign_noprop`, where you hand that function a
transaction, and the return value is either the signed transaction or an
error (for instance, the user rejected the signature request).
JR works in a very similar way to Sidekick, but it has more things that need to
talk to other things layers. Browser extensions have 3 basic components:
1. Content scripts. These are almost the same thing as page scripts.
In general, content scripts are typically used to modify websites the
end-user is looking at. For instance, if you were making RedBorder, an
extension that adds a 15 pixel wide red border around every image, the
content script is the part of RedBorder that has the permission to query
the DOM for `<img>` tags, and modify each such tag to have the property
`style="border: 15px solid red;"`.
For the purposes of JR, the content script is simply the messaging layer
between the page script and the rest of the extension. All that matters in
our case is
- the content script can post messages that are visible to page scripts
- vice versa
- the content script has permission to send messages to other parts of our
extension (and to other extensions and to desktop applictions and it can
eat your children but that doesn't matter)
- vice versa
2. The background script.
This is the central controller of the application.
3. Popup scripts.
This is what a "popup" is in the context of browser extensions:
![what the word "popup" means in the browser extensions](./art/popup.png)
This is an HTML page like any other. A popup script is simply a script
that runs in the context of the popup page. It has the same basic array of
permissions as the background script.
```
PAGE SCRIPT <-> CONTENT SCRIPT <-> BACKGROUND SCRIPT <-> POPUP SCRIPT
Aegora.jp <-> src/content.ts <-> src/background.ts <-> src/popup.ts
```
In JavaScript, the standard way of communicating between mutually opaque
contexts A and B (for instance, between a page script and a wallet) is to write
a class in each context that manages state, which listens for events, and then
does things in response to those events. `ClassA` can dispatch events which are
visible to the event listener in `ClassB` and vice versa. This is the natural
result of trying to shoehorn concurrency into an object-oriented language.
Sidekick and JR internally work by mimicking Erlang's interprocess
communication (IPC) idiom.
Erlang is a concurrency-first language, and thus has a much saner and more
well-thought-out approach to this problem. There is a build-in function called
`send(PID, Message)`. The message can be any Erlang term. In the receiving
process, there is primitive syntax for receiving messages:
```erl
receive
Pattern1 ->
do_thing_1();
Pattern2 ->
do_thing_2();
_ ->
handle_arbitrary_message()
after NMilliseconds ->
do_timeout_thing()
end.
```
Each process has a mailbox. When a process enters a `receive`, it sits there
and scans its mailbox to find a message that matches one of the patterns. If
it finds a message that matches `Pattern1`, it executes the function
`do_thing_1()`, and so on. The `after` part is optional, and it exists as a
backstop so the process doesn't get stuck in an infinite `receive` loop.
Here is an example of two processes just exchanging ping and pong:
```erl
#!/usr/bin/env escript
-mode(compile).
%% this is the code that is run by the pinger process
pinger() ->
%% the pinger just sits and waits until a pong message appears in the
%% mailbox and sends back ping
receive
%% wait to receive a pong
%% only accept this pattern
{SenderPID, pong} ->
%% self() gets the current process pid
%% print out the pong to the console
ok = io:format("~p received pong from ~p~n", [self(), SenderPID]),
%% wait for 1 second
ok = timer:sleep(1000),
%% send a ping back
SenderPID ! {self(), ping},
%% start over
pinger()
end.
%% this is the code that is run by the ponger process
%% identical to the pinger except the words "ping" and "pong" have been swapped
ponger() ->
%% the ponger just sits and waits until a ping message appears in the
%% mailbox and sends back pong
receive
%% wait to receive a ping
%% only accept this pattern
{SenderPID, ping} ->
%% self gets the current process's pid
%% print out the ping to the console
ok = io:format("~p received ping from ~p~n", [self(), SenderPID]),
%% wait for 1 second
ok = timer:sleep(1000),
%% send a pong back
SenderPID ! {self(), pong},
%% start over
ponger()
end.
%% this is the main process
main([]) ->
%% start up pingers and pongers
%% these just sit and wait until they are told to do something
PingerPID = spawn(fun pinger/0),
PongerPID = spawn(fun ponger/0),
%% ping the ponger, but lie about who is doing it
PongerPID ! {PingerPID, ping},
%% this is a hack so that main doesn't exit and therefore the demo works
receive
_ ->
ok = io:format("bye~n")
end.
```
```
[~] % escript pingpong.erl
<0.81.0> received ping from <0.80.0>
<0.80.0> received pong from <0.81.0>
<0.81.0> received ping from <0.80.0>
<0.80.0> received pong from <0.81.0>
<0.81.0> received ping from <0.80.0>
<0.80.0> received pong from <0.81.0>
<0.81.0> received ping from <0.80.0>
<0.80.0> received pong from <0.81.0>
^C
```
JR basically thinks of each of the 3 different components of an extension as
Erlang processes, and has some band-aid code to kind of sort of mimick Erlang's
IPC idiom. Frankly, Erlang's IPC idiom doesn't quite fit into TS/JS, however
even the shoehorned band-aid Erlang idiom is significantly simpler and more
pleasant to work with than the OOPy event idiom.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+2 -1
View File
@@ -2,4 +2,5 @@
{realm, local}. {realm, local}.
{name, jrx}. {name, jrx}.
{version, "0.1.0"}. {version, "0.1.0"}.
{deps, ["local-tweetnacl-1.0.3"]}. {deps, ["local-tweetnacl-1.0.3",
"local-awcp-0.2.1"]}.
-1178
View File
File diff suppressed because it is too large Load Diff
+261
View File
@@ -0,0 +1,261 @@
/**
* JR Content Script
*
* @module
*/
jr_main();
/**
* This is the "meat" of what is spammed to the page script when the user makes
* the wallet detectable.
*
* If you go read the AWCP documentation, there's a bunch of layers. This is I
* think the innermost layer.
*/
function detect_msg() {
return {id : "jr",
name : "JR",
networkId : "ae_uat",
origin : "foobar",
type : "extension"};
}
/**
* Post {@link detect_awcp_msg} into the window message queue
*/
function
post_detect_msg
(msg: object)
{
window.postMessage(msg, '*');
}
/**
* For 2 minutes, make the wallet detectable to a page script
*/
async function
mk_detectable
(msg: object)
{
while (true)
// 3 seconds times 40 is 2 minutes
// for (let i=1; i<=40; i++)
{
console.error('pee');
post_detect_msg(msg);
await sleep(3000);
}
}
/**
* sleep for the given number of ms
*/
async function
sleep
(ms: number)
: Promise<void>
{
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* This handles messages from *other parts of the extension*
*/
function
handler
(msg: any)
{
console.error('message: ', msg);
switch(msg)
{
case 'mk-detectable':
mk_detectable();
return;
default:
console.error('your mom is dead');
}
}
/**
* This handles messages from page scripts
*/
function
window_message_handler
(msg: {data: any})
{
console.error('the science is coming', msg);
// example science:
// {
// "type": "to_waellet",
// "data": {
// "jsonrpc": "2.0",
// "id": "ske-connect-1",
// "method": "connection.open",
// "params": {
// "name": "sidekick examples",
// "version": 1
// }
// }
// }
let the_science = msg.data;
console.error('THE SCIENCE: ', the_science);
console.error('is the science good or bad?');
if (the_science.type === 'to_waellet')
{
console.error('the science is good');
the_science_is_good(the_science.data);
}
else
console.error('the science is bad');
}
/**
* window_message_handler handles all messages sent into the event bus,
* including messages that we sent (yay js). window_message_handler branches on
* whether the message is for us or not (is the science good or bad?). If the
* message is for us (the science is good), then this function is triggered.
*
* The bottom line is this is the function that *actually* handles messages
* from the window
*/
function
the_science_is_good
(awcp_rcp_data: {id : string | number,
method : string,
params : object})
{
// example science data:
// // layer 3: json rpc
// {jsonrpc : "2.0",
// id : "ske-connect-1",
// method : "connection.open",
// // layer 4: AWCP-specific semantics
// params : {name : "sidekick examples",
// version : 1}}}
// can assume it is a call
let msg_ident = awcp_rcp_data.id;
let msg_method = awcp_rcp_data.method;
// branch here on the "method" field
switch (msg_method)
{
case "connection.open":
post_connect_msg(msg_ident);
break;
case "address.subscribe":
bg_address_subscribe(msg_ident);
break;
case "transaction.sign":
bg_tx_sign(msg_ident, awcp_rcp_data.params);
break;
case "message.sign":
bg_msg_sign(msg_ident, awcp_rcp_data.params);
break;
default:
console.error('the science is worse than i initially thought');
console.error("we're going to have to put the science to sleep");
}
}
/**
* Called in response to "connection.open" requests from page scripts
*
* FIXME: this should query the user to see if he wants to connect
*/
function
post_connect_msg
(msg_ident : string | number)
{
// : EventData_W2A_connection_open
// http://localhost:6969/local-awcp-0.2.1/types/EventData_W2A_connection_open.html
let connect_response =
{type: "to_aepp",
data: {jsonrpc: "2.0",
id: msg_ident,
method: "connection.open",
result: detect_msg()}};
window.postMessage(connect_response, '*');
}
/**
* Called in response to "address.subscribe" requests from page scripts
*
* this is to get the wallet's address
*/
function
bg_address_subscribe
(msg_ident: string | number)
{
}
/**
* Called in response to "transaction.sign" requests from page scripts
*
* user wants us to sign a transaction
*/
function
bg_tx_sign
(msg_ident : string | number,
params : object)
{
}
/**
* Called in response to "message.sign" requests from page scripts
*
* user wants us to sign a message
*/
function
bg_msg_sign
(msg_ident : string | number,
params : object)
{
}
async function
jr_main
()
{
// @ts-ignore browser
// browser.runtime.onMessage.addListener(handler);
window.addEventListener('message', window_message_handler);
/**
* This is the message that we spam the page script with when the user clicks
* the "make wallet detectable" button.
*
* Contains {@link detect_msg} as a field
*/
let detect_awcp_msg = {type : "to_aepp",
data : {jsonrpc : "2.0",
method : "connection.announcePresence",
params : detect_msg()}};
// mk detectable
mk_detectable(detect_awcp_msg);
}
+11 -230
View File
@@ -1,246 +1,27 @@
/** /**
* JR Content Script * JR Content Script
* *
* Notes:
*
* 1. This is not loaded as a module, which means it has limited permissions.
* In particular, asynchronous calls are 1000x more annoying.
* 2. All this does is relay messages between here and the background script
* (i.e. application controller).
*
* @module * @module
*/ */
jr_main(); jr_content_main();
/**
* This is the "meat" of what is spammed to the page script when the user makes
* the wallet detectable.
*
* If you go read the AWCP documentation, there's a bunch of layers. This is I
* think the innermost layer.
*/
function detect_msg() {
return {id : "jr",
name : "JR",
networkId : "ae_uat",
origin : "foobar",
type : "extension"};
}
/**
* Post {@link detect_awcp_msg} into the window message queue
*/
function
post_detect_msg
(msg: object)
{
window.postMessage(msg, '*');
}
/**
* For 2 minutes, make the wallet detectable to a page script
*/
async function
mk_detectable
(msg: object)
{
while (true)
// 3 seconds times 40 is 2 minutes
// for (let i=1; i<=40; i++)
{
console.error('pee');
post_detect_msg(msg);
await sleep(3000);
}
}
/**
* sleep for the given number of ms
*/
async function
sleep
(ms: number)
: Promise<void>
{
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* This handles messages from *other parts of the extension*
*/
function
handler
(msg: any)
{
console.error('message: ', msg);
switch(msg)
{
case 'mk-detectable':
mk_detectable();
return;
default:
console.error('your mom is dead');
}
}
/**
* This handles messages from page scripts
*/
function
window_message_handler
(msg: {data: any})
{
console.error('the science is coming', msg);
// example science:
// {
// "type": "to_waellet",
// "data": {
// "jsonrpc": "2.0",
// "id": "ske-connect-1",
// "method": "connection.open",
// "params": {
// "name": "sidekick examples",
// "version": 1
// }
// }
// }
let the_science = msg.data;
console.error('THE SCIENCE: ', the_science);
console.error('is the science good or bad?');
if (the_science.type === 'to_waellet')
{
console.error('the science is good');
the_science_is_good(the_science.data);
}
else
console.error('the science is bad');
}
/**
* window_message_handler handles all messages sent into the event bus,
* including messages that we sent (yay js). window_message_handler branches on
* whether the message is for us or not (is the science good or bad?). If the
* message is for us (the science is good), then this function is triggered.
*
* The bottom line is this is the function that *actually* handles messages
* from the window
*/
function
the_science_is_good
(awcp_rcp_data: {id : string | number,
method : string,
params : object})
{
// example science data:
// // layer 3: json rpc
// {jsonrpc : "2.0",
// id : "ske-connect-1",
// method : "connection.open",
// // layer 4: AWCP-specific semantics
// params : {name : "sidekick examples",
// version : 1}}}
// can assume it is a call
let msg_ident = awcp_rcp_data.id;
let msg_method = awcp_rcp_data.method;
// branch here on the "method" field
switch (msg_method)
{
case "connection.open":
post_connect_msg(msg_ident);
break;
case "address.subscribe":
bg_address_subscribe(msg_ident);
break;
case "transaction.sign":
bg_tx_sign(msg_ident, awcp_rcp_data.params);
break;
case "message.sign":
bg_msg_sign(msg_ident, awcp_rcp_data.params);
break;
default:
console.error('the science is worse than i initially thought');
console.error("we're going to have to put the science to sleep");
}
}
/**
* Called in response to "connection.open" requests from page scripts
*
* FIXME: this should query the user to see if he wants to connect
*/
function
post_connect_msg
(msg_ident : string | number)
{
// : EventData_W2A_connection_open
// http://localhost:6969/local-awcp-0.2.1/types/EventData_W2A_connection_open.html
let connect_response =
{type: "to_aepp",
data: {jsonrpc: "2.0",
id: msg_ident,
method: "connection.open",
result: detect_msg()}};
window.postMessage(connect_response, '*');
}
/**
* Called in response to "address.subscribe" requests from page scripts
*
* this is to get the wallet's address
*/
function
bg_address_subscribe
(msg_ident: string | number)
{
}
/**
* Called in response to "transaction.sign" requests from page scripts
*
* user wants us to sign a transaction
*/
function
bg_tx_sign
(msg_ident : string | number,
params : object)
{
}
/**
* Called in response to "message.sign" requests from page scripts
*
* user wants us to sign a message
*/
function
bg_msg_sign
(msg_ident : string | number,
params : object)
{
}
async function async function
jr_main jr_content_main
() ()
{ {
// @ts-ignore browser // @ts-ignore browser
// browser.runtime.onMessage.addListener(handler); browser.runtime.onMessage.addListener(handler);
window.addEventListener('message', window_message_handler); window.addEventListener('message', page_script_message_handler);
+12
View File
@@ -0,0 +1,12 @@
/**
* Definitions of messages that are passed between process A and process B in
* JR. Example: how the content script talks to the background script.
*
* @module
*/
/**
* this is the message sent from the content script to the background script
* when a page script requests
*/
type c2b_addr_subscribe = "address_subscribe";