wrote out architecture rant in readme
@@ -1,5 +1,177 @@
|
||||
# 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:
|
||||
|
||||

|
||||
|
||||
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 |
|
After Width: | Height: | Size: 16 KiB |
@@ -2,4 +2,5 @@
|
||||
{realm, local}.
|
||||
{name, jrx}.
|
||||
{version, "0.1.0"}.
|
||||
{deps, ["local-tweetnacl-1.0.3"]}.
|
||||
{deps, ["local-tweetnacl-1.0.3",
|
||||
"local-awcp-0.2.1"]}.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,246 +1,27 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
jr_content_main();
|
||||
|
||||
|
||||
async function
|
||||
jr_main
|
||||
jr_content_main
|
||||
()
|
||||
{
|
||||
// @ts-ignore browser
|
||||
// browser.runtime.onMessage.addListener(handler);
|
||||
window.addEventListener('message', window_message_handler);
|
||||
browser.runtime.onMessage.addListener(handler);
|
||||
window.addEventListener('message', page_script_message_handler);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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";
|
||||