Merge branch 'pharpend/develop' into 'master'

bring master up to my develop branch so jr on master builds

See merge request ioecs/Vanillae!11
This commit is contained in:
2024-03-19 00:49:19 +00:00
338 changed files with 26998 additions and 79134 deletions
+2
View File
@@ -3,3 +3,5 @@
erl_crash.dump
*.beam
__pycache__
jex_include
jex_mindist
+111 -40
View File
@@ -1,59 +1,130 @@
# Vanillae
Vanillae is an Aeternity tool suite meant for small business use cases. Focus
is placed on ease-of-use, simplicity, code quality, and good documentation.
## How to use this project
You can read the [grant
proposal](https://forum.aeternity.com/t/active-application-vanillae/10638) for
more information about what this project is.
Vanillae is (intentionally) a very minimal toolset that only covers the most
common Aeternity use cases. We also do not have any tooling for the Node/NPM
ecosystem. If you have a more complicated use case or live in the Node/NPM
ecosystem, you want the [Aeternity JavaScript
SDK](https://github.com/aeternity/aepp-sdk-js). The SDK is a Swiss Army Knife.
Vanillae is just a knife.
## Downloads
Vanillae grew out of [Aegora.jp](https://aegora.jp). We made a simple
## Quick Links
- [Vanillae Grant Proposal][grant-proposal]
- [Vanillae Files][vanillae-files], miscellaneous explainers mostly for
things that should take 1 hour to learn but in practice take 11 hours
because information has to be synthesized from multiple sources
- Video content
- [Vanillae Rumble Channel](https://rumble.com/c/c-3509606)
- [Vanillae Odysee Channel](https://odysee.com/@VanillaeProject:0)
- [Vanillae YouTube Channel](https://www.youtube.com/@vanillaeproject)
- Miscellany:
- [Aegora.jp][aegora], original impetus for product
- [Aeternity JS SDK][aesdk], which you want for more complicated use cases
## About this project
Aeternity is a high-performance open-source decentralized payment network.
Vanillae is a tool suite for using Aeternity. There is both developer-facing
tooling and user-facing tooling. There is also miscellaneous documentation, and
explainers etc.
The two developers are Craig Everett (@zxq9) and Peter Harpending (@pharpend).
Vanillae grew out of [Aegora.jp][aegora]. We made a simple
e-commerce store using Aeternity as the payment system. We encountered a lot
of rough edges in Aeternity's tooling, and so developed our own, and
open-sourced it. That became this project
of rough edges in Aeternity's tooling, so developed our own, and
open-sourced it. The Aeternity Foundation chose to pay us to grow out our
toolset, and that became this project. You can read the [grant
proposal][grant-proposal] for more information.
Our tooling is grouped according to the project that spawned it
Often during this process we ran into some thing that really should take 1 hour
to learn about, but in practice takes 11 hours because you have to synthesize
information from 100 different sources. In many of those cases, we went to the
trouble to write it all out in one place (that's the [Vanillae
Files][vanillae-files]), and in some cases there are videos on our
YT/Rumble/Odysee channels.
- [Sidekick](./sidekick/)
The goal is to write an explainer doc and have one or more videos for each
instance of information synthesis paralysis.
We needed the ability to talk to Superhero (the standard Aeternity browser
wallet extension) from our page script. Sidekick is a simple library to do
just that.
Generally, we write tools that we want to use. So
- [Sidekick Examples](./sidekick_examples/) illustrate how to use sidekick
- Functionality tends to be pretty minimal.
- The only usage cases covered are those we have actually encountered.
- When we encounter a tradeoff between simplicity and flexibility, we
generally have chosen simplicity.
- Our primary deployment target is the [zx/zomp ecosystem][zx-zomp], not the
Node/NPM ecosystem
- [AWCP](./libs/awcp/): the aepp/waellet communication protocol
For instance, this means that our JS/TS code is meant to run inside a
browser, and we don't give any thought to running it as server-side JS,
because that's not something we would ever do ourselves.
This is the definition of the messaging protocol between the page
script and the wallet. Sidekick implements one half of this protocol.
We're happy to support the Node/NPM ecosystem on a case-by-case basis as
long as it doesn't force us to reduce the quality of our product.
- [jex](./utils/jex/)
If you have a more complicated use case or live in the Node/NPM ecosystem, you
want the [Aeternity JavaScript SDK][aesdk] The SDK is a Swiss Army Knife.
Vanillae is just a set of knives, each of which was designed for a specific
cutting task.
We encountered packaging as an obvious problem to solve, and NPM is
simply a terrible way to solve it. Jex is a TypeScript/JavaScript
package manager that avoids most of the NPM-related security issues.
- [Vanillae Erlang Bindings](./bindings/erlang)
## Directory Structure
Our server backend is written in Erlang. We needed to talk to the
blockchain from our server backend. Vanillae.erl is an Erlang application
that does that.
```
art/ Project icons, etc
bindings/ FLAGSHIP: APIs to talk to the AE blockchain from app backend
erlang/ [ERL] Erlang Vanillae Bindings
docs/ Explainer docs
baseN/ Base64 versus Base58 (they are fundamentally different ideas!)
ecc/ Elliptic Curve Cryptography
jex/ Jex: our TS/JS packaging tool we use instead of NPM
kek/ The Keccak/SHA-3 Algorithm explained
npm-misc/ How to install NPM on Linux such that you never have to use `sudo npm`
rlp/ Ethereum's Recursive-Length-Prefix codec explained
seed-phrases/ How seed phrase recovery works
sk-awcp/ How to use Sidekick and AWCP
jrx/ [TS] FLAGSHIP: the Jack Russell browser wallet extension
libs/ Miscellaneous (non-flagship) libraries
awcp/ [TS] Aepp-Waellet Communication Protocol: types for messages sent between page scripts and browser extension wallets
parasite/ [TS] Function library for talking to public aeternity nodes (do not use please)
tweetnacl/ [TS] Jex-packaged clone of public domain TweetNaCL library
vdk_aeser/ [TS] Serializer/deserializer library for AE data structures
vdk_base58/ [TS] Base 58 encode/decode library
vdk_base64/ [TS] Base 64 encode/decode library
vdk_binary/ [TS] Miscellaneous binary functions that should exist in the stdlib but don't
vdk_colors/ [TS] Assigns a color to arbitrary byte array (for color-coding keys in JR)
vdk_faert/ [TS] Fast AEternity Recovery Text: a replacement for the Bitcoin seed phrase standard
vdk_names/ [TS] Assigns a human readable name to a byte array (for naming keys in JR)
vdk_rlp/ [TS] Ethereum RLP standard TypeScript library
vdk_safe/ [TS] Equivalent of Haskell's Either type
vdk_tests/ [TS] Test/example suite for VDK
vdk_tests_cases/ [TS] Randomly generated test cases for some VDK packages (e.g. checking our RLP agrees with Ethereum's)
vrlp/ [ERL] Ethereum RLP library in Erlang
sidekick/ [TS] FLAGSHIP: TS library to talk to browser wallet extension (e.g. Superhero, hence name) from page script
sidekick_examples/ [TS] Example/test suite for sidekick
site/ Website for the vanillae project (soon to be deprecated/deleted)
utils/ Miscellaneous utilities
jex/ [ERL] Simple TS/JS packaging utility, our alternative to NPM
vlogd/ [ERL] Simple HTTP server that just logs input data, for demoing sidekick's HTTP logging functionality
vw/ [ERL] Vanillae Wallet: scratchpad for working out complicated wallet stuff (e.g. seed phrase recovery) in saner environment
```
It is designed in such a way that you could write say a Vanillae Go
library with an identical API but totally different internals. Ultimately
the idea here is to create a language-agnostic API for talking to the
Aeternity blockchain from the
## Flagship products
- [Jaeck Russell](./jrx/): this is a (work-in-progress) simpler wallet than
Superhero
### Vanillae Bindings
- [Vanillae Files](./docs/): any time where we encountered some weird thing
that is like 1 hour of information but takes 15 hours to understand because
it's poorly documented, we tried to document it here.
### Jack Russell
### Sidekick
## Miscellaneous products of note
### Vanillae Files
### Jex
[aegora]: https://aegora.jp
[aesdk]: https://github.com/aeternity/aepp-sdk-js
[grant-proposal]: https://forum.aeternity.com/t/active-application-vanillae/10638
[vanillae-files]: ./docs/
[zx-zomp]: http://zxq9.com/projects/zomp/
-13
View File
@@ -1,13 +0,0 @@
TODONE
- vdk_base58
- vdk_faert
- vdk_rlp
- vdk_tests
- vdk_tests_cases
TODO
- sidekick package
- jex run
- jex docs
- website
- entropy counting doc
+2
View File
@@ -1 +1,3 @@
dist
src/jex_include
docs
+181 -1
View File
@@ -1,5 +1,185 @@
# 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.
## Building
Requires [jex](../utils/jex).
```
jex dwim-
```
## 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

+5 -1
View File
@@ -2,4 +2,8 @@
{realm, local}.
{name, jrx}.
{version, "0.1.0"}.
{deps, ["local-tweetnacl-1.0.3"]}.
{deps, ["local-tweetnacl-1.0.3",
"local-awcp-0.2.3",
"local-vdk_aecrypt-0.1.2",
"local-vdk_aeser-0.1.1",
"local-vdk_binary-0.1.0"]}.
+14 -12
View File
@@ -1,12 +1,14 @@
{"manifest_version" : 2,
"name" : "Jack Russell",
"version" : "0.1.0",
"description" : "Browser wallet extension for Aeternity",
"icons" : {"48" : "icons/jrx-icon-48.png",
"96" : "icons/jrx-icon-96.png"},
"content_scripts" : [{"matches" : ["*://*/*"],
"js" : ["dist/content.js"]}],
"browser_action" : {"default_icon" : "icons/jrx-icon-32.png",
"default_title" : "Jaeck Russell",
"default_popup" : "popup.html"},
"permissions" : ["storage"]}
{"manifest_version" : 3,
"name" : "Jack Russell",
"version" : "0.1.0",
"description" : "Browser wallet extension for Aeternity",
"icons" : {"48" : "art/jrx-icon-48.png",
"96" : "art/jrx-icon-96.png"},
"content_scripts" : [{"matches" : ["*://*/*"],
"js" : ["dist/content.js"]}],
"action" : {"default_icon" : "art/jrx-icon-32.png",
"default_title" : "Jæck Russell",
"default_popup" : "pages/popup.html"},
"background" : {"page" : "pages/background.html"},
"permissions" : ["storage", "scripting"],
"host_permissions" : ["*://*/*"]}
-1178
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -1,3 +1,10 @@
TODO
- manifest v3
- mansplaining better
- factor out "show a popup window"
- show correct pubkey on popup
- seed oil recovery
MODEL
popup.html/popup.js
@@ -29,3 +36,9 @@ background script
- raseev requests
- send responses back to page
-
----
vdk general lib
sleep
bulrtot
+90
View File
@@ -0,0 +1,90 @@
<!DOCTYPE html>
<!-- This exists to load NaCl into context for the background script -->
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<!--
Trying to figure out why TweetNaCl isn't loading
ok so the problem is not an invalid hash, it's something specific
to nacl
to have an inline script, its hash must be specified in
manifest.json
example (integrity key is not necessary, it is there purely for
comment purposes)
beginning of line
V
| <script integrity="sha256-8T3dvQxHPNQvgxzCemw3KGUCM5RhknpCQF6pCcTdFTw=">console.log('bg-test-inline.js line 1!');
|</script>
newline is there for hash purposes
this works if and only if
'sha256-8T3dvQxHPNQvgxzCemw3KGUCM5RhknpCQF6pCcTdFTw=' is included
in the manifest.json
example (top level key):
"content_security_policy" : "script-src 'self' 'sha256-8T3dvQxHPNQvgxzCemw3KGUCM5RhknpCQF6pCcTdFTw='; object-src 'self'",
Absent that explicit allow, I get this error from Firefox:
Content Security Policy: The page’s settings blocked the loading of a resource at inline (“script-src”).
Refs:
1. https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_security_policy
2. https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_Security_Policy#inline_javascript
If I include the same script as a file,
<script src="bg-test-inline.js"></script>
this works irrespective of if I specify it as allowable in the manifest
nacl is just refusing to load
Loading failed for the <script> with source “moz-extension://eafd6a74-b75b-4312-9a34-8087b68395e7/dist/jex_include/local-nacl-1.0.3/nacl.js”.
I initially thought it was a hash problem, but it is not
TweetNaCL doesn't work as an ES6 module, it works by destructively
updating the window namespace. This I think was necessary before
ES6, and that behavior is retained either because of laziness or
because of backward compatibility reasons.
hmm
so this just plainly doesn't work and i cannot figure out why
<script src="dist/jex_include/local-nacl-1.0.3/nacl.js"></script>
oh my god
<script src="dist/jex_include/local-tweetnacl-1.0.3/dist/nacl.js"></script>
i got the url wrong
i hate myself
ok well glad we got that sorted out at least
And i discovered a future potential pitfall
-->
<script src="../dist/jex_include/local-tweetnacl-1.0.3/dist/nacl.js"></script>
<!-- same but for blakejs
<script src="./dist/jex_include/local-blakejs-1.2.1/dist/index.js"></script>
-->
<!--
this is a non-issue
-->
<script type="module" src="../dist/background.js"></script>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<!-- HTML page for a "generic dialog" -->
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<h1 id="title-h1"></h1>
<pre id="miscinfo"></pre>
<br>
<button id="good">Yes</button>
<br>
<button id="bad">No</button>
<script type="module" src="./gb.js"></script>
<body>
</html>
+19
View File
@@ -0,0 +1,19 @@
// see b_lib.ts, function gb for the thing that talks to this
main();
async function main() {
// connect to the runtime
let port = browser.runtime.connect();
// wait for a message
port.onMessage.addListener(
function({title, miscinfo}) {
console.log('title', title);
console.log('miscinfo', miscinfo);
document.getElementById('title-h1').innerHTML = title;
document.getElementById('miscinfo').innerHTML = miscinfo;
document.getElementById('good').onclick = function() { port.postMessage('good'); };
document.getElementById('bad').onclick = function() { port.postMessage('bad'); };
}
);
}
+74
View File
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style type="text/css">
body {
background: #d6efff;
width: 400px;
font-family: monospace, sans-serif;
}
</style>
</head>
<body>
<table width="100%" style="background: #dad1ff; border: 2px solid #6c5d96; border-radius: 5px;">
<tr>
<td><img src="../art/jrx-icon-48.png" style="width: 32px; height: 32px;"></td>
<td style="font-size: 32px; text-align: center; font-weight: bold">
JÆCK RUSSELL
</td>
</tr>
</table>
<div style="margin-top: 5px; width: 95%; background: #d9ffd6; border: 1px solid #6c5d96; border-radius: 5px;">
<table >
<tr>
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
<td style="justify-content: center; font-size: 28px; text-align: center; font-weight: bold">
KEYPAIRS
</td>
</tr>
</table>
<p id="no-keypairs">You have no keypairs yet. Either generate a new one or
recover one you already have.</p>
<ul id="keypairs"></ul>
<!-- recover from faert phrase -->
<form>
<button id="generate-btn">Generate New Keypair</button>
</form>
<!-- recover from faert phrase -->
<form>
<label for="faert-input">FÆRT Phrase:</label>
<input id="faert-input"></input>
<button id="faert-btn">Recover Keypair from FÆRT Phrase</button>
</form>
<!-- recover from seed phrase
<form>
<label for="seed-input">Seed Phrase:</label>
<input id="seed-input"></input>
<button id="seed-btn">Recover Keypair from Seed Phrase</button>
</form>
-->
</div>
<pre id="scratch"></pre>
<!--
<div style="margin-top: 5px; width: 95%; background: #d9ffd6; border: 1px solid #6c5d96; border-radius: 5px;">
<table>
<tr>
<td><img src="../art/keypair.svg" style="width: 32px; height: 32px;"></td>
<td style="justify-content: center; font-size: 28px; text-align: center; font-weight: bold">
KEYPAIRS
</td>
</tr>
</table>
</div>
-->
<script type="module" src="./dist/popup.js"></script>
</body>
</html>
-51
View File
@@ -1,51 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!--
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="./ext/bootstrap-5.3.0-alpha3-dist/css/bootstrap.min.css">
-->
<style type="text/css">
body {
background: #d6efff;
width: 400px;
font-family: monospace, sans-serif;
}
</style>
</head>
<body>
<table width="100%" style="background: #dad1ff; border: 2px solid #6c5d96; border-radius: 5px;">
<tr>
<td><img src="./icons/jrx-icon-48.png" style="width: 32px; height: 32px;"></td>
<td style="font-size: 32px; text-align: center; font-weight: bold">
JÆCK RUSSELL
</td>
</tr>
</table>
<div style="margin-top: 5px; width: 95%; background: #d9ffd6; border: 1px solid #6c5d96; border-radius: 5px;">
<table >
<tr>
<td><img src="./ext/icons/keypair.svg" style="width: 32px; height: 32px;"></td>
<td style="justify-content: center; font-size: 28px; text-align: center; font-weight: bold">
KEYPAIRS
</td>
</tr>
</table>
<ul id="keypairs">
</ul>
</div>
<pre id="scratch">
</pre>
<!-- including nacl here -->
<script src="dist/jex_include/local-nacl-1.0.3/nacl.js"></script>
<script type="module" src="dist/popup.js"></script>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
function
the_function_that_runs_in_the_context_of_gb_dot_html
(gb_title : string,
gb_miscinfo : string,
gb_timeout_ms : number)
: string
{
// FIXME: not alerting user about bobs
alert('bobs!');
//// the last evaluated statement of this function is the "return value"
//// as far as scripting.executeScript is concerned
//console.log('gb_title', gb_title);
//console.log('gb_miscinfo', gb_miscinfo);
//console.log('gb_timeout_ms', gb_timeout_ms);
//document.getElementById('title-h1')!.innerHTML = gb_title;
return 'good';
}
console.log('badness');
// @ts-ignore i'm just assuming typescript is going to be stupid here i don't even know
let dialog_result: Array<{result: gb} | {error: any}> =
await browser.scripting.executeScript(
// @ts-ignore i'm just assuming typescript is going to be stupid here i don't even know
{func : the_function_that_runs_in_the_context_of_gb_dot_html,
args : [title, miscinfo, timeout_ms],
target : {tabId : tabid},
// script just hangs forever without this line
// in no case does it alert user about bobs
// alright
// need a break
// when we get back, let's try a file
// otherwise we're going to have to try a totally different idiom
// maybe go back to some retarded message passing nonsense
injectImmediately: true
}
);
console.log('dialog_result', dialog_result[0]);
// @ts-ignore
if (dialog_result[0].error) {
// @ts-ignore
throw dialog_result.error;
}
else {
// @ts-ignore
return dialog_result[0].result;
}
+98
View File
@@ -0,0 +1,98 @@
/**
* Page script for "confirm sign message" popup window
*/
main();
async function
main
()
: Promise<void>
{
console.log('msg_confirm main');
let result : '' | 'good' | 'bad'
= '';
document.getElementById('good')!.onclick
= function() {
console.log('click good');
result = 'good';
};
document.getElementById('bad')!.onclick
= function() {
console.log('click bad');
result = 'bad';
};
type bg_msg
= {tx_str : string,
tx_data : object};
async function listener
(msg : bg_msg,
_sender : any,
_sendResponse : any)
: Promise<'good' | 'bad'>
{
console.log('listener triggered', msg);
console.log('tx_str:', msg.tx_str);
console.log('tx_data:', msg.tx_data);
document.getElementById('tx-base64')!.innerHTML = msg.tx_str;
document.getElementById('tx-decomposed')!.innerHTML = JSON.stringify(msg.tx_data, undefined, 4);
// every 5 ms check
// timeout of 10 minutes = 10*60 secs * 20 iterations =
// number of iters in a full second
let ITERS = 1;
let SEC = 200*ITERS;
let MIN = 60*SEC;
//let n_max = 10*MIN;
let n = 1;
let n_max = 30*MIN;
// result starts as neutral
// if user clicks good, result will be updated to 'good'
// if user clicks bad, result will be updated to 'bad'
// if user doesn't click in 30 seconds, default to 'bad'
// this has the effect of sitting and waiting until the user clicks a button
// with a timeout as a backstop
while
(result === '') {
// if haven't timed out yet
if (n <= n_max) {
await sleep(5);
n = n + 1;
}
// if timed out
else {
result = 'bad';
// this break is implied but you're not smart enough to figure
// that out yourself
break;
}
}
// send result back
return result;
}
// add listener
browser.runtime.onMessage.addListener(listener);
}
/**
* Hack from stack overflow somewhere to sleep for the given number of ms
*/
async function
sleep
(ms: number)
: Promise<void>
{
return new Promise(resolve => setTimeout(resolve, ms));
}
+92
View File
@@ -0,0 +1,92 @@
/**
* JR Controller
*
* Note that this is a *thread*. We put our business and storage logic within
* this thread. The reason we do that is we don't want to deal with the
* problem of conflicting concurrent writes..
*
* @module
*/
export type {
jr_data,
named_keypair,
keypair
}
export {
// top-level getters/setters
};
/**
* The data structure we store
*/
type jr_data
= {version : 0,
named_keypairs : Array<named_keypair>};
/**
* This is the default {@link jr_data}.
*
* @private
*/
let jr_data_mzero = {version : 0 as 0, // yay ts type inference can't infer 0 is of type 0
named_keypairs : []};
/**
* Every keypair has to have a name
*/
type named_keypair
= {name : string}
& keypair;
/**
* From NaCl
*/
type keypair
= {secretKey : Uint8Array,
publicKey : Uint8Array};
/**
* Get the state from the system
*/
async function
fetch_migrate
()
: Promise<jr_data>
{
// try to get data
let browser_data = await browser.storage.local.get('jr');
// test if it's there
if (!!(browser_data.jr))
return browser_data.jr;
// if the data is not there
else
return jr_data_mzero;
}
/**
* Put state
*
* @private
*/
async function
put
(state : jr_data)
: Promise<void>
{
await browser.storage.local.set({jr: state});
}
+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);
}
+966
View File
@@ -0,0 +1,966 @@
/**
* JR Controller "Library".
*
* Conceptually this is the thread corresponding to the application controller.
* The background script calls `b_main()` and does nothing else. The popup
* script calls the various `p_...` functions, which black-box away the IPC
* nonsense. Really, conceptually, the popup script is making a call to the
* background script, and the code idiom merely reflects that conceptual
* reality.
*
* In a perfect world, the content script would also act like this. However,
*
* 1. The content script is already a finished product. All it needs to do is
* relay messages back and forth between this thread and the page script,
* and it does that just fine.
* 2. The content script isn't an ES6 module, and so "imports" are a giant pain
* in the ass. So rewriting it to fit this "call the background script"
* idiom is just not worth it. It already works. The meaningful IPC is
* between the page script and a generic wallet, and that is already defined
* in AWCP.
*
* # Naming/Notation Conventions
*
* ```
* - b_... -> background script API
* - bi_... -> background internal
* - bis_... -> background internal storage
* - p_... -> popup script API
* - !... -> bookmark for regex searches to quickly find a section
* bang = bookmark
* ```
*
*
* Re storage: the browser's storage layer for extensions basically stores
* the data in JSON, so we need a separate type layer to make sure the
* conversion between the program's internal state and what the browser
* stores doesn't get fornicated up. Despite the fact that JSON is magical
* fairy dust that seamlessly encodes/decodes losslessly to and from any type,
* I still am much more comfortable writing it out myself.
*
*
* FIXME: check bytes or sth for keypair storage to guard against json fornicateery
* FIXME: versioned storage
* FIXME: move some of the AWCP data making functions to the AWCP lib
* FIXME: the address_subscribe data scraping nonsense needs to be factored out
* FIXME: quiet mode (undetectable by default)
* FIXME: ask user if he wants to connect/address/etc
* FIXME: ask user if he wants to sign messages
*
* @module
*/
import * as awcp from './jex_include/local-awcp-0.2.3/dist/awcp.js';
import * as vdk_aecrypt from './jex_include/local-vdk_aecrypt-0.1.2/dist/vdk_aecrypt.js';
import * as vdk_aeser from './jex_include/local-vdk_aeser-0.1.1/dist/vdk_aeser.js';
import * as vdk_binary from './jex_include/local-vdk_binary-0.1.0/dist/vdk_binary.js';
export type {
// global types
// popup script call/return data
};
export {
// popup script api
// background script api
b_main
};
// INTERNAL CONSTANTS ALL IN ONE PLACE
let GB_ITER_MS = 5; // [MS / ITER] 5 ms every 1 iteration
let GB_ITERS = 1;
let GB_SEC = 200*GB_ITERS; // [ITER]; 1iter / 5ms = 200iter / sec
let GB_MIN = 60*GB_SEC; // [ITER]
let MSG_SIGN_TIMEOUT = 30*GB_MIN;
let TX_SIGN_TIMEOUT = 30*GB_MIN;
//=============================================================================
//=============================================================================
// API: GLOBAL TYPES
//=============================================================================
//=============================================================================
//=============================================================================
//=============================================================================
// API: POPUP TYPES
//=============================================================================
//=============================================================================
//=============================================================================
//=============================================================================
// API: POPUP SCRIPT
//=============================================================================
//=============================================================================
//=============================================================================
//=============================================================================
// API: BACKGROUND SCRIPT
//=============================================================================
//=============================================================================
/**
* main function for background thread
*/
async function
b_main
()
: Promise<void>
{
console.log('b_main');
// handle messages from content or popup scripts
browser.runtime.onMessage.addListener(bi_runtime_msg_handler);
}
//=============================================================================
//=============================================================================
// INTERNALS: TYPES
//=============================================================================
//=============================================================================
// !bi_types !bi_state !bi-types !bi-state
/**
* @example
* ```ts
* // EventData_A2w
* {type : "to_waellet",
* // RpcCall
* data : {jsonrpc : "2.0",
* id : "ske-connect-1",
* method : "connection.open",
* // Params_A2W_connection_open
* params : {name : "sidekick examples",
* version : 1}}}
* ```
*
* @internal
*/
type bi_a2w_calldata
= awcp.EventData_A2W<awcp.RpcCall<string, any>>;
// method params
/**
* Return data for queries from a page script
*
* @internal
*/
type bi_w2a_return_data
= awcp.EventData_W2A<awcp.RpcResp<string, any>>;
/**
* Type copied from NaCL
*
* @internal
*/
type bi_nacl_keypair
= {secretKey : Uint8Array,
publicKey : Uint8Array};
/**
* JR state used internally
*
* @internal
*/
type bi_state
= {keypairs: Array<bi_nacl_keypair>};
/**
* JR storage state
*
* Storage is in JSON so we have to have a different type for storage and for
* what's actually used during ordinary data processing.
*
* @internal
*/
type bis_state
= {jr_state: {keypairs: Array<bis_nacl_keypair>}};
/**
* How keypairs are stored
*
* @internal
*/
type bis_nacl_keypair
= {secretKey : Array<number>,
publicKey : Array<number>};
//=============================================================================
//=============================================================================
// INTERNALS: FUNCTIONS
//=============================================================================
//=============================================================================
/**
* Handle messages from either content or popup script
*
* Note to self: `sendResponse` is fake. Doesn't work. Instead just return the
* response
*
* @internal
*/
async function
bi_runtime_msg_handler
(msg : {frum: 'content' | 'popup',
data: any},
sender : any,
sendResponse : any)
: Promise<bi_w2a_return_data | bi_popup_return_data>
{
//console.log('msg', msg);
//console.log('sender', sender);
//console.log('sendResponse', sendResponse);
switch (msg.frum) {
// messages from the content script (i.e. from page scripts)
case 'content':
return await bi_msg_handler_content(msg.data as bi_a2w_calldata, sender, sendResponse);
// messages from the popup window
case 'popup':
return await bi_msg_handler_popup(msg.data as bi_popup_calldata, sender, sendResponse);
}
}
//-----------------------------------------------------------------------------
// INTERNALS: CONTENT SCRIPT MESSAGE HANDLING
//-----------------------------------------------------------------------------
/**
* Handle messages from the content script
*
* Note to self: `sendResponse` is fake. Doesn't work. Instead just return the
* response.
*
* Literally the way it works is that
*
* - IF this is a non-async function (i.e. does NOT return a `Promise`), THEN
* `sendResponse` is fake and the runtime sends back the return value of the
* function. This is our case.
* - ELSE `sendResponse` works. Presumably this is for backward compabitility.
*
* @internal
*/
async function
bi_msg_handler_content
(msg : bi_a2w_calldata,
sender : any,
sendResponse : any)
: Promise<bi_w2a_return_data>
{
console.log('bi_msg_handler_content', {msg: msg});
//console.log('msg', msg);
//console.log('sender', sender);
//console.log('sendResponse', sendResponse);
let msg_method : string = msg.data.method;
let msg_id : string | number = msg.data.id;
// get the state
let i_state : bi_state = await bi_get_state();
// function to encode the ok message
// depends on parameters above so makes sense to have it be a lambda
function w2a_ok(result_for_content_script: any) {
return bi_mk_w2a_msg_ok(msg_method,
msg_id,
result_for_content_script);
}
// function to encode the error message
// depends on parameters above so makes sense to have it be a lambda
function w2a_err(code: number, message: string) {
return bi_mk_w2a_msg_err(msg_method,
msg_id,
code,
message);
}
console.log('jr bg content message handler method:', msg.data.method);
let public_key : Uint8Array = i_state.keypairs[0].publicKey;
let secret_key : Uint8Array = i_state.keypairs[0].secretKey;
switch (msg.data.method) {
// right now just give connect info back
case "connection.open":
return w2a_ok({id : "jr",
name : "JR",
networkId : "ae_uat",
origin : browser.runtime.getURL('/'),
type : "extension"});
// right now just give address back
case "address.subscribe":
console.log('jr bg content message handler address.subscribe');
// convert it to a string
let address_ak_str : string = await vdk_aeser.pubkey2ak_str(public_key);
console.log('public key:', address_ak_str);
return w2a_ok(bi_address_reply(address_ak_str, []));
// right now just sign message
case "message.sign":
console.log('jr bg content message handler message sign');
let msg_str : string = msg.data.params.message;
// TODONE: need to break here on whether the msg signature was good
// (signed) or bad (not)
let resultt = await msg_sign(msg_str, secret_key);
if (resultt.result === 'good') {
return w2a_ok({signature: resultt.signature});
}
else {
return w2a_err(awcp.ERROR_CODE_RpcRejectedByUserError, "User either rejected message signature request or did not confirm within 30 minutes of popup window shown.");
}
// right now just sign tx
case "transaction.sign":
console.log('jr bg content message handler transaction sign');
let tx_str : string = msg.data.params.tx;
console.log('transaction: ', tx_str);
let result = await tx_sign(tx_str, secret_key)
if
('good' === result.result) {
return w2a_ok({signedTransaction: result.signedTransaction});
}
else {
return w2a_err(awcp.ERROR_CODE_RpcRejectedByUserError, "your not pretty enough");
}
//if (result.result === 'good') {
// return w2a_ok({signedTransaction: result.signedTransaction});
//}
//else {
// return w2a_err(awcp.ERROR_CODE_RpcRejectedByUserError, "you will never be pretty enough");
//}
// default is NYI
default:
return w2a_err(awcp.ERROR_CODE_RpcMethodNotFoundError, 'not yet implemented');
}
}
type gb = 'good' | 'bad';
/**
* Fornicating js block scope rules
*
* NOOOOOOOO YOU CAN'T DECLARE TWO DIFFERENT VARIABLES WITH THE SAME NAME IN
* TWO DIFFERENT CASES EVEN THOUGH THEY'RE MUTUALLY EXCLUSIVE
*
* wojak.jpg
*
* Good result = user said yes
* Bad result = user said no
*
* @internal
*/
async function
msg_sign
(msg_str : string,
secret_key : Uint8Array)
: Promise< {result : 'good',
signature : string}
| {result : 'bad'}
>
{
let result : gb = await gb('Do you want to sign this message?', msg_str, MSG_SIGN_TIMEOUT);
// if the user wants us to sign the transaction, return the signed transaction
// confirm
if
(result === 'good') {
// use nacl detached signatures
// https://github.com/aeternity/aepp-sdk-js/blob/5df22dd297abebc0607710793a7234e6761570d4/src/utils/crypto.ts#L141-L143
// https://github.com/aeternity/aepp-sdk-js/blob/5df22dd297abebc0607710793a7234e6761570d4/src/utils/crypto.ts#L160-L167
//console.error('hi 1');
let hashed_salted_msg : Uint8Array = vdk_aecrypt.hash_and_salt_msg(msg_str);
//console.error('hi 2');
// @ts-ignore yes nacl is stupid
let signature : Uint8Array = nacl.sign.detached(hashed_salted_msg, secret_key);
let signature_str : string = vdk_binary.bytes_to_hex_str(signature);
return {result : 'good',
signature : signature_str};
}
// otherwise don't
else {
return {result : 'bad'};
}
}
/**
* Fornicating js block scope rules
*
* NOOOOOOOO YOU CAN'T DECLARE TWO DIFFERENT VARIABLES WITH THE SAME NAME IN
* TWO DIFFERENT CASES EVEN THOUGH THEY'RE MUTUALLY EXCLUSIVE
*
* wojak.jpg
*
* @internal
*/
async function
tx_sign
(tx_str : string,
secret_key : Uint8Array)
: Promise< {result : 'good',
signedTransaction : string}
| {result : 'bad'}
>
{
let mansplained_tx : string = await vdk_aeser.mansplain_str(tx_str);
let result : gb = await gb('Do you want to sign this transaction?',
mansplained_tx,
TX_SIGN_TIMEOUT);
// if we're supposed to sign the tx, then sign it
if
('good' === result) {
let tx_bytes : Uint8Array = (await vdk_aeser.unbaseNcheck(tx_str)).bytes;
// thank you ulf
// https://github.com/aeternity/protocol/tree/fd179822fc70241e79cbef7636625cf344a08109/consensus#transaction-signature
// we sign <<NetworkId, SerializedObject>>
// SerializedObject can either be the object or the hash of the object
// let's stick with hash for now
// FIXME: get network id from application state
let network_id : Uint8Array = vdk_binary.encode_utf8('ae_uat');
// let tx_hash_bytes : Uint8Array = hash(tx_bytes);
let sign_data : Uint8Array = vdk_binary.bytes_concat(network_id, tx_bytes);
// @ts-ignore yes nacl is stupid
let signature : Uint8Array = nacl.sign.detached(sign_data, secret_key);
let signed_tx_bytes : Uint8Array = vdk_aeser.signed_tx([signature], tx_bytes);
let signed_tx_str : string = await vdk_aeser.baseNcheck('tx', signed_tx_bytes);
// debugging
let mansplained_stx : object = await vdk_aeser.mansplain(signed_tx_str);
console.log('mansplained signed tx:', mansplained_stx);
return {result : 'good',
signedTransaction : signed_tx_str};
}
// if user rejected the tx
else {
return {result : 'bad'};
}
}
/**
* This pops up a window for the luser and asks if the thing is good or bad.
*
* Returns good or bad
*
* Timeout is in "iterations"; 1 iteration is 5 milliseconds. Use `GB_SEC` and
* `GB_MIN` to get those units.
*
* # Why this is stupid
*
* Ok super important thing: there's a very stupid idiom in here for talking to
* the good/bad popup window. This is not because I am stupid. This is because
* web browsers are stupid.
*
* We cannot just spawn a window and then start talking to it. The problem is
* that this isn't Erlang where the process has a mailbox and we can count on
* the messages being delivered there and then being there when the process
* initiates a `raseev`.
*
* Instead the good/bad popup-window process has to spawn *and then listen for
* messages*, and if we send a message before it's listening, tough, it just
* doesn't get there.
*
* First instinct was of course to just wait for however many milliseconds for
* the popup window to spawn before sending it messages. But 200ms was not
* enough time for this approach to work on my beast of a machine. Given that
* our users are going to be running JR on normal machines, this approach is
* simply infeasible.
*
* Instead after much denial and error, I found the approach where the good/bad page script
* initiates a connection works consistently and is idiomatically sane, at
* least in relative terms. In this case, we are using the Port abstraction
* rather than the "one-off" connection thing that Mozilla says is low class.
* It is better in this case so that we don't have to deal with process-level
* global state in this function.
*
* If we wanted to interleave that nonsense into the global raseev loop at the
* top of this file, we would have to basically have some registry of pages
* we're trying to talk to, and then have some message queue that pairs each
* message from a script with whomever is trying to listen to it. That's
* doable but I would prefer not to create global state if it's unnecessary.
*
* @internal
*/
async function
gb
(title : string,
miscinfo : string,
timeout_iters : number)
: Promise<gb>
{
// TODO: the thread model here is a bit dubious... it's not 100% clear that
// we're always going to be talking to the correct window. Do some
// experiments.
// we're going to make an unresolved result first
// write our code to handle messages from the popup window
// which updates this variable
// once this variable is updated with resolved: true
// then the entire function returns
let the_result : {resolved : false}
| {resolved : true,
result : gb}
= {resolved : false};
// this is a closure that talks to the popup window from this context
// it updates the_result when it gets something back from the user
let port_talker_toer_lambda =
function(port : browser.runtime.Port) {
// @ts-ignore bad type info in the typedefs
port.postMessage({title : title,
miscinfo : miscinfo});
// oh my god
// nested lambdas
// i think we've reached peak js
let result_handler =
function(result_from_gb_popup_window : gb) {
console.error('RESULT FROM POPUP WINDOW!!!!', result_from_gb_popup_window);
// the message will either be good or bad
the_result = {resolved : true,
result : result_from_gb_popup_window};
};
// @ts-ignore types from github are wrong
port.onMessage.addListener(result_handler);
}
browser.runtime.onConnect.addListener(port_talker_toer_lambda);
// does the user want to sign the message
let confirm_window = await browser.windows.create({url : '../pages/gb.html',
type : 'popup'});
// @ts-ignore shut the fuck up
let tabid : number = confirm_window.tabs[0].id;
let this_iter_i1 : number = 1;
let max_iters : number = timeout_iters;
// poor man's for loop because I just don't care
// sleep for 5 milliseconds until resolved
// this loop is guaranteed to terminate
// and at its termination, the_result will have resolved:true
while (!(the_result.resolved)) {
// if we've timed out, time out
if (this_iter_i1 > max_iters) {
// the result is resolved and it's bad
the_result = {resolved: true, result: 'bad'};
}
// in this case, we haven't timed out yet
else {
// so we simply increase the iteration
this_iter_i1 = this_iter_i1 + 1;
// sleep
await sleep(GB_ITER_MS);
}
}
// at this point, the_result is resolved:true
// we know that the_result.result exists
// typescript does not know that
// close the tab
browser.tabs.remove(tabid);
// return the result
// @ts-ignore i know this exists because I am jesus
return the_result.result;
}
/**
* Make the dumb address_subscribe thing
*
* @internal
*/
function
bi_address_reply
(current_pubkey_str : string,
other_pubkey_strs : Array<string>)
: awcp.Result_W2A_address_subscribe
{
// From the AWCP docs:
//
// @example
// This is if the user has many keypairs. The currently selected one is under
// `current`. Craig, I agree this is stupid, but that's how it works.
//
// {
// "subscription": [
// "connected"
// ],
// "address": {
// "current": {
// "ak_25C3xaAGQddyKAnaLLMjAhX24xMktH2NNZxY3fMaZQLMGED2Nf": {}
// },
// "connected": {
// "ak_BMtPGuqDhWLnMVL4t6VFfS32y2hd8TSYwiYa2Z3VdmGzgNtJP": {},
// "ak_25BqQuiVCasiqTkXHEffq7XCsuYEtgjNeZFeVFbuRtJkfC9NyX": {},
// "ak_4p6gGoCcwQzLXd88KhdjRWYgd4MfTsaCeD8f99pzZhJ6vzYYV": {}
// }
// }
// }
// so we make the "object" in the "current" field
// this is so dumb but
let current_obj = {};
// @ts-ignore shut up tsc i know this is stupid it's not my fault
current_obj[current_pubkey_str] = {};
// make the object in the "connected" field
let connected_obj = {};
for (let this_pubkey_str of other_pubkey_strs) {
// @ts-ignore shut up tsc i know this is stupid it's not my fault
connected_obj[this_pubkey_str] = {};
}
return {subscription : ['connected'],
address : {current : current_obj,
connected : connected_obj}};
}
/**
* SUCCESS CASE: Wrap up bs for w2a message
*
* This really should go into the AWCP library but I'm lazy
*
* @internal
*/
function
bi_mk_w2a_msg_ok
<result_t extends any>
(method : string,
id : string | number,
result : result_t)
: awcp.EventData_W2A<awcp.RpcResp_ok<string, result_t>>
{
return {type : "to_aepp",
data : {jsonrpc : "2.0",
method : method,
id : id,
result : result}};
}
/**
* ERROR CASE: Wrap up bs for w2a message
*
* This really should go into the AWCP library but I'm lazy
*
* @internal
*/
function
bi_mk_w2a_msg_err
(method : string,
id : string | number,
code : number,
message : string)
: awcp.EventData_W2A<awcp.RpcResp_error<string>>
{
return {type : "to_aepp",
data : {jsonrpc : "2.0",
method : method,
id : id,
error : {code : code,
message : message}}};
}
//----------------------------------------------------------------------------
// INTERNALS: POPUP WINDOW MESSAGE HANDLING
//----------------------------------------------------------------------------
type bi_popup_calldata
= 'init';
type bi_popup_return_data
= 'die';
/**
* Handle messages from the popup
*
* @internal
*/
async function
bi_msg_handler_popup
(msg : bi_popup_calldata,
sender : any,
sendResponse : any)
: Promise<bi_popup_return_data>
{
console.log('jr bg_msg_handler_popup msg', msg);
return 'die';
}
//=============================================================================
//=============================================================================
// INTERNALS: STORAGE API
//=============================================================================
//=============================================================================
/**
* Fetch the state from local storage
*
* If there is no state make an initial state and store it
*
* @internal
*/
async function
bi_get_state
()
: Promise<bi_state>
{
console.log('jr bg bi_get_state');
// so
// - if extension has NEVER committed state (i.e. this is the first
// invocation), there will be NO key "jr_state"
// - if we have committed state, that key will exist
//
// right now our game is branching on whether or not there is existing
// state in the browser's storage
//
// - if there is state, we simply fetch it from storage, convert it to a
// type suitable for use in code (e.g. keypairs are byte arrays rather
// than dumb JSON number arrays)
// - if there is no state, we create a default initial state, commit it,
// and hand it back to the calling code
// this may or may not have the keyword "jr_state"
let gotten_state : ({} | bis_state) = await browser.storage.local.get();
console.log('jr bg gotten_state', gotten_state);
// problem: browser storage is JSON basically
// so we need to convert our state to and from json
// if there is such a state, get it
// @ts-ignore ts doesn't understand querying if a key exists
if (!!(gotten_state.jr_state)) {
console.log('jr state exists');
// TS doesn't know we've proven the key exists and so therefore this is
// of type bis_state
return bi_s2i(gotten_state as bis_state);
}
// otherwise return default state
else {
console.log('jr no state... generating');
// set the state
let default_i_state : bi_state = bi_state_default();
await bi_set_state(default_i_state);
// return it
return default_i_state;
}
}
/**
* Set the state. Blocks until state is set, exception is thrown if there is an
* exception.
*
* @internal
*/
async function
bi_set_state
(i_state : bi_state)
: Promise<void>
{
console.log('jr bg bi_set_state internal state:', i_state);
// convert to storage state
let s_state : bis_state = bi_i2s(i_state);
console.log('jr bg bi_set_state storage state:', s_state);
// store
await browser.storage.local.set(s_state);
}
//-----------------------------------------------------------------------------
// INTERNALS: INTERNAL->STORAGE TYPE COERCION
//-----------------------------------------------------------------------------
/**
* Internal state to storage state converter
*
* @internal
*/
function
bi_i2s
(internal_state : bi_state)
: bis_state
{
// convert keypairs
let i_keypairs : Array<bi_nacl_keypair> = internal_state.keypairs;
let s_keypairs : Array<bis_nacl_keypair> = i_keypairs.map(bi_i2s_keypair);
return {jr_state : {keypairs: s_keypairs}};
}
/**
* JSONify a keypair
*
* basically turn each array into numbers
*
* @internal
*/
function
bi_i2s_keypair
(i_keypair : bi_nacl_keypair)
: bis_nacl_keypair
{
let i_secretKey : Uint8Array = i_keypair.secretKey;
let i_publicKey : Uint8Array = i_keypair.publicKey;
let s_secretKey : Array<number> = bi_i2s_bytes2nums(i_secretKey);
let s_publicKey : Array<number> = bi_i2s_bytes2nums(i_publicKey);
return {secretKey : s_secretKey,
publicKey : s_publicKey};
}
/**
* Convert a byte array to an array of numbers
*
* @internal
*/
function
bi_i2s_bytes2nums
(bytes: Uint8Array)
: Array<number>
{
let arr : Array<number> = [];
for (let this_byte of bytes) {
arr.push(this_byte);
}
return arr;
}
/**
* Default state for JR
*
* Note
* 1. This returns the *internal* state that lives in RAM, **NOT** what is
* stored in browser storage
* 2. This function is **NOT** deterministic. It randomly generates a keypair.
*
* @internal
*/
function
bi_state_default
()
: bi_state
{
console.log('jr_state_default');
// if no key, generate one
// @ts-ignore ts doesn't like that nacl is dumb. i don't like it either
let init_keypair : bi_nacl_keypair = nacl.sign.keyPair() as bi_nacl_keypair;
let default_state : bi_state = { keypairs: [init_keypair] };
console.log('jr_state_default default_state', default_state);
return default_state;
}
//-----------------------------------------------------------------------------
// INTERNALS: STORAGE->INTERNAL TYPE COERCION
//-----------------------------------------------------------------------------
/**
* Storage -> internal state converter
*
* @internal
*/
function
bi_s2i
(s_state : bis_state)
: bi_state
{
// for now bis_state is just keypairs
let s_keypairs : Array<bis_nacl_keypair> = s_state.jr_state.keypairs;
// convert numbers to byte array
function nums2bytes(nums: Array<number>): Uint8Array {
return new Uint8Array(nums);
}
// convert storage keypair to normal keypair
function keypair_s2i(s_keypair: bis_nacl_keypair): bi_nacl_keypair {
let s_secretKey : Array<number> = s_keypair.secretKey;
let s_publicKey : Array<number> = s_keypair.publicKey;
let i_secretKey : Uint8Array = nums2bytes(s_secretKey);
let i_publicKey : Uint8Array = nums2bytes(s_publicKey);
return {secretKey : i_secretKey,
publicKey : i_publicKey};
}
// convert each keypair
let i_keypairs : Array<bi_nacl_keypair> = s_keypairs.map(keypair_s2i);
return {keypairs: i_keypairs};
}
/**
* Hack from stack overflow somewhere to sleep for the given number of ms
*/
async function
sleep
(ms: number)
: Promise<void>
{
return new Promise(resolve => setTimeout(resolve, ms));
}
+8 -87
View File
@@ -1,92 +1,13 @@
/**
* JR Controller
* This is the "background script", which just calls b_lib:main/0
*
* Note that this is a *thread*. We put our business and storage logic within
* this thread. The reason we do that is we don't want to deal with the
* problem of conflicting concurrent writes..
*
* @module
* b_lib is the background "library". I want the popup and content script to be
* able to "call" the background script (really there's a rewrite layer there
* so the entire IPC messaging protocol is in one file). I don't want to fire the
* missiles whenever say the popup script loads the "background library", so
* the background "script" is a separate file.
*/
import {b_main} from './b_lib.js';
export type {
jr_data,
named_keypair,
keypair
}
export {
// top-level getters/setters
};
/**
* The data structure we store
*/
type jr_data
= {version : 0,
named_keypairs : Array<named_keypair>};
/**
* This is the default {@link jr_data}.
*
* @private
*/
let jr_data_mzero = {version : 0 as 0, // yay ts type inference can't infer 0 is of type 0
named_keypairs : []};
/**
* Every keypair has to have a name
*/
type named_keypair
= {name : string}
& keypair;
/**
* From NaCl
*/
type keypair
= {secretKey : Uint8Array,
publicKey : Uint8Array};
/**
* Get the state from the system
*/
async function
fetch_migrate
()
: Promise<jr_data>
{
// try to get data
let browser_data = await browser.storage.local.get('jr');
// test if it's there
if (!!(browser_data.jr))
return browser_data.jr;
// if the data is not there
else
return jr_data_mzero;
}
/**
* Put state
*
* @private
*/
async function
put
(state : jr_data)
: Promise<void>
{
await browser.storage.local.set({jr: state});
}
b_main();
+70 -225
View File
@@ -1,67 +1,49 @@
/**
* JR Content Script
*
* Notes:
*
* 1. This is not a module, so calling async code is super annoying and
* everything has to be in script order.
* 2. All this does is relay messages between here and the background script
* (i.e. application controller).
*
* This is more or less a finished script. Doesn't need the "api call" idiom
* that the popup <-> controller IPC needs.
*
* @module
*/
jr_main();
let detect_msg = {type : "to_aepp",
data : {jsonrpc : "2.0",
method : "connection.announcePresence",
params : {id : "jr",
name : "JR",
networkId : "ae_uat",
origin : browser.runtime.getURL('/'),
type : "extension"}}};
/**
* 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
* Spam the "connection.announcePresence" (awcp terminology: "detect") message
*/
async function
mk_detectable
(msg: object)
c_spam_detect
()
{
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);
while (true) {
window.postMessage(detect_msg);
await c_sleep(3000);
}
}
/**
* sleep for the given number of ms
* Hack from stack overflow somewhere to sleep for the given number of ms
*/
async function
sleep
c_sleep
(ms: number)
: Promise<void>
{
@@ -69,193 +51,56 @@ sleep
}
/**
* 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
* Relays messages between page scripts and the background script
*/
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
c_a2w_handler
(msg: {data: {type? : "to_aepp" | "to_waellet"}})
: Promise<void>
{
//console.error('JR: a2w_handler', {msg: msg});
function onSuccessCase(response_from_bg: any) {
console.log('jr: c_a2w_handler success ', response_from_bg);
window.postMessage(response_from_bg);
}
function onErrorCase(error: any) {
console.error('jr: c_a2w_handler: error from controller', error);
}
// branch
// all messages are visible to us
// if the message is for the wallet
// send it to the wallet
// otherwise ignore it
if ("to_waellet" === msg.data.type) {
console.log('jr: c_a2w_handler relaying message', msg.data);
browser.runtime
.sendMessage({frum: 'content',
data: msg.data})
.then(onSuccessCase,
onErrorCase);
}
// otherwise ignore
}
/**
* Main function
*/
async function
c_main
()
{
// @ts-ignore browser
// browser.runtime.onMessage.addListener(handler);
window.addEventListener('message', window_message_handler);
console.log('c_main');
// relay messages back and forth between page script and controller
window.addEventListener('message', c_a2w_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);
// spam the detect message
c_spam_detect();
}
c_main()
+75 -2
View File
@@ -2,15 +2,88 @@
* This is the script for the popup window
*/
main();
p_main();
/**
* runs whenever the popup opens
*
* also whenever a button is clicked it appears the page re-renders which
* re-runs this
*
* suggests the following structure:
*
* 1. Contact the background script and say "I'm a popup".
* 2. Get all relevant data.
* 3. Render
*
* On click:
*
* 1. Send relevant information back to background script
* 2. Epstein
*/
async function
main
p_main
()
: Promise<void>
{
//let scratch = document.getElementById('scratch')!;
//scratch.innerText += 'line 17 hello\n';
console.log('popup main');
//function onSuccessCase(response_from_bg: any) {
// console.log('jr popup response from background script success', response_from_bg);
//}
//function onErrorCase(error_from_bg: any) {
// console.log('jr popup response from background script error', error_from_bg);
//}
//browser.runtime
// .sendMessage({frum: 'popup',
// data: 'init'})
// .then(onSuccessCase,
// onErrorCase);
//// add hooks for each button
//document.getElementById('generate-btn')!.addEventListener('click', onclick_generate);
//document.getElementById('faert-btn')!.addEventListener('click', onclick_faert);
////document.getElementById('seed-btn')!.addEventListener('click', onclick_seed);
}
///**
// * Runs when the user clicks the "generate" button
// */
//async function
//onclick_generate
// ()
// : Promise<void>
//{
// console.log('onclick_generate');
//}
//
//
//
///**
// * Runs when the user clicks the "recover from faert phrase" button
// */
//async function
//onclick_faert
// ()
// : Promise<void>
//{
// console.log('onclick_faert');
//}
//
//
//
/////**
//// * Runs when the user clicks the "recover from seed phrase" button
//// */
////async function
////onclick_seed
//// ()
////{
//// console.log('onclick_seed');
////}
+1
View File
@@ -1,4 +1,5 @@
{"compilerOptions" : {"target" : "es2022",
"module" : "es2022",
"strict" : true,
"esModuleInterop" : true,
"skipLibCheck" : true,
+1
View File
@@ -1,6 +1,7 @@
ISC License
Copyright (c) 2022 Peter Harpending
Copyright (c) 2022-2023 Aeternity Foundation
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
+25 -2
View File
@@ -1,3 +1,26 @@
# awcp (package documentation)
# awcp: Aepp-Waellet Communication Protocol.
Please click the word "Documentation" in the sidebar
Mostly an internal package.
Page scripts (aepps) and wallet browser extensions (waellets) need to talk to
each other. This package contains
1. type definitions of the messages
2. a list of error codes
[src/awcp.ts](./src/awcp.ts) contains a detailed explanation of how the
protocol is structured.
[Sidekick](../../sidekick/) exhibits how to use this from the perspective of
the page script (aepp), and [Jack Russell](../../jrx) exhibits how to use this
from the perspective of the waellet.
## Building
Requires [jex]
```
jex install
```
[jex]: ../../utils/jex/
+1 -1
View File
@@ -1,5 +1,5 @@
{type, library}.
{realm, local}.
{name, awcp}.
{version, "0.2.2"}.
{version, "0.2.3"}.
{deps, []}.
+6 -5
View File
@@ -15,11 +15,12 @@
* this.
*
* Keep in mind that these messages are not secret. Any browser extension or
* foreign page script can intercept these messages. Imagine as an (imperfect)
* analogy that you work in an office. Everyone's mail is dumped on the floor
* in the middle of the office, and you are responsible for picking out which
* letters are addressed to you. Anyone else can pick up letters addressed to
* you, and read them.
* foreign page script can intercept these messages, and inject messages of
* their own. Imagine as an (imperfect) analogy that you work in an office.
* Everyone's mail is dumped on the floor in the middle of the office. You are
* responsible for picking out which letters are addressed to you. Anyone else
* can pick up letters addressed to you, and read them. Anyone else can also
* place their own letters in the pile.
*
* 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
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2022 Blakejs contributors
Copyright (c) 2023 Aeternity Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17
View File
@@ -0,0 +1,17 @@
# blakejs
This is the [blakejs](https://www.npmjs.com/package/blakejs) library adapted
for [the jex package manager](../../utils/jex).
I (Peter) only included the subset of blake which is actually used, the blake2b
module. The package is modified to "just work" with the ES6 module system.
For internal use in Jack Russell
## Building
Requires [jex](../../utils/jex)
```
jex dwim+
```
+366
View File
@@ -0,0 +1,366 @@
// Blake2B in pure Javascript
// Adapted from the reference implementation in RFC7693
// Ported to Javascript by DC - https://github.com/dcposch
// const util = require('./util')
import * as util from './util.js';
// 64-bit unsigned addition
// Sets v[a,a+1] += v[b,b+1]
// v should be a Uint32Array
function ADD64AA (v, a, b) {
const o0 = v[a] + v[b]
let o1 = v[a + 1] + v[b + 1]
if (o0 >= 0x100000000) {
o1++
}
v[a] = o0
v[a + 1] = o1
}
// 64-bit unsigned addition
// Sets v[a,a+1] += b
// b0 is the low 32 bits of b, b1 represents the high 32 bits
function ADD64AC (v, a, b0, b1) {
let o0 = v[a] + b0
if (b0 < 0) {
o0 += 0x100000000
}
let o1 = v[a + 1] + b1
if (o0 >= 0x100000000) {
o1++
}
v[a] = o0
v[a + 1] = o1
}
// Little-endian byte access
function B2B_GET32 (arr, i) {
return arr[i] ^ (arr[i + 1] << 8) ^ (arr[i + 2] << 16) ^ (arr[i + 3] << 24)
}
// G Mixing function
// The ROTRs are inlined for speed
function B2B_G (a, b, c, d, ix, iy) {
const x0 = m[ix]
const x1 = m[ix + 1]
const y0 = m[iy]
const y1 = m[iy + 1]
ADD64AA(v, a, b) // v[a,a+1] += v[b,b+1] ... in JS we must store a uint64 as two uint32s
ADD64AC(v, a, x0, x1) // v[a, a+1] += x ... x0 is the low 32 bits of x, x1 is the high 32 bits
// v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated to the right by 32 bits
let xor0 = v[d] ^ v[a]
let xor1 = v[d + 1] ^ v[a + 1]
v[d] = xor1
v[d + 1] = xor0
ADD64AA(v, c, d)
// v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 24 bits
xor0 = v[b] ^ v[c]
xor1 = v[b + 1] ^ v[c + 1]
v[b] = (xor0 >>> 24) ^ (xor1 << 8)
v[b + 1] = (xor1 >>> 24) ^ (xor0 << 8)
ADD64AA(v, a, b)
ADD64AC(v, a, y0, y1)
// v[d,d+1] = (v[d,d+1] xor v[a,a+1]) rotated right by 16 bits
xor0 = v[d] ^ v[a]
xor1 = v[d + 1] ^ v[a + 1]
v[d] = (xor0 >>> 16) ^ (xor1 << 16)
v[d + 1] = (xor1 >>> 16) ^ (xor0 << 16)
ADD64AA(v, c, d)
// v[b,b+1] = (v[b,b+1] xor v[c,c+1]) rotated right by 63 bits
xor0 = v[b] ^ v[c]
xor1 = v[b + 1] ^ v[c + 1]
v[b] = (xor1 >>> 31) ^ (xor0 << 1)
v[b + 1] = (xor0 >>> 31) ^ (xor1 << 1)
}
// Initialization Vector
const BLAKE2B_IV32 = new Uint32Array([
0xf3bcc908, 0x6a09e667, 0x84caa73b, 0xbb67ae85, 0xfe94f82b, 0x3c6ef372,
0x5f1d36f1, 0xa54ff53a, 0xade682d1, 0x510e527f, 0x2b3e6c1f, 0x9b05688c,
0xfb41bd6b, 0x1f83d9ab, 0x137e2179, 0x5be0cd19
])
const SIGMA8 = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13,
6, 1, 12, 0, 2, 11, 7, 5, 3, 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1,
9, 4, 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, 9, 0, 5, 7, 2, 4,
10, 15, 14, 1, 11, 12, 6, 8, 3, 13, 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5,
15, 14, 1, 9, 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11, 13, 11, 7,
14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, 6, 15, 14, 9, 11, 3, 0, 8, 12, 2,
13, 7, 1, 4, 10, 5, 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0, 0,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, 6,
1, 12, 0, 2, 11, 7, 5, 3
]
// These are offsets into a uint64 buffer.
// Multiply them all by 2 to make them offsets into a uint32 buffer,
// because this is Javascript and we don't have uint64s
const SIGMA82 = new Uint8Array(
SIGMA8.map(function (x) {
return x * 2
})
)
// Compression function. 'last' flag indicates last block.
// Note we're representing 16 uint64s as 32 uint32s
const v = new Uint32Array(32)
const m = new Uint32Array(32)
function blake2bCompress (ctx, last) {
let i = 0
// init work variables
for (i = 0; i < 16; i++) {
v[i] = ctx.h[i]
v[i + 16] = BLAKE2B_IV32[i]
}
// low 64 bits of offset
v[24] = v[24] ^ ctx.t
v[25] = v[25] ^ (ctx.t / 0x100000000)
// high 64 bits not supported, offset may not be higher than 2**53-1
// last block flag set ?
if (last) {
v[28] = ~v[28]
v[29] = ~v[29]
}
// get little-endian words
for (i = 0; i < 32; i++) {
m[i] = B2B_GET32(ctx.b, 4 * i)
}
// twelve rounds of mixing
// uncomment the DebugPrint calls to log the computation
// and match the RFC sample documentation
// util.debugPrint(' m[16]', m, 64)
for (i = 0; i < 12; i++) {
// util.debugPrint(' (i=' + (i < 10 ? ' ' : '') + i + ') v[16]', v, 64)
B2B_G(0, 8, 16, 24, SIGMA82[i * 16 + 0], SIGMA82[i * 16 + 1])
B2B_G(2, 10, 18, 26, SIGMA82[i * 16 + 2], SIGMA82[i * 16 + 3])
B2B_G(4, 12, 20, 28, SIGMA82[i * 16 + 4], SIGMA82[i * 16 + 5])
B2B_G(6, 14, 22, 30, SIGMA82[i * 16 + 6], SIGMA82[i * 16 + 7])
B2B_G(0, 10, 20, 30, SIGMA82[i * 16 + 8], SIGMA82[i * 16 + 9])
B2B_G(2, 12, 22, 24, SIGMA82[i * 16 + 10], SIGMA82[i * 16 + 11])
B2B_G(4, 14, 16, 26, SIGMA82[i * 16 + 12], SIGMA82[i * 16 + 13])
B2B_G(6, 8, 18, 28, SIGMA82[i * 16 + 14], SIGMA82[i * 16 + 15])
}
// util.debugPrint(' (i=12) v[16]', v, 64)
for (i = 0; i < 16; i++) {
ctx.h[i] = ctx.h[i] ^ v[i] ^ v[i + 16]
}
// util.debugPrint('h[8]', ctx.h, 64)
}
// reusable parameterBlock
const parameterBlock = new Uint8Array([
0,
0,
0,
0, // 0: outlen, keylen, fanout, depth
0,
0,
0,
0, // 4: leaf length, sequential mode
0,
0,
0,
0, // 8: node offset
0,
0,
0,
0, // 12: node offset
0,
0,
0,
0, // 16: node depth, inner length, rfu
0,
0,
0,
0, // 20: rfu
0,
0,
0,
0, // 24: rfu
0,
0,
0,
0, // 28: rfu
0,
0,
0,
0, // 32: salt
0,
0,
0,
0, // 36: salt
0,
0,
0,
0, // 40: salt
0,
0,
0,
0, // 44: salt
0,
0,
0,
0, // 48: personal
0,
0,
0,
0, // 52: personal
0,
0,
0,
0, // 56: personal
0,
0,
0,
0 // 60: personal
])
// Creates a BLAKE2b hashing context
// Requires an output length between 1 and 64 bytes
// Takes an optional Uint8Array key
// Takes an optinal Uint8Array salt
// Takes an optinal Uint8Array personal
function blake2bInit (outlen, key, salt, personal) {
if (outlen === 0 || outlen > 64) {
throw new Error('Illegal output length, expected 0 < length <= 64')
}
if (key && key.length > 64) {
throw new Error('Illegal key, expected Uint8Array with 0 < length <= 64')
}
if (salt && salt.length !== 16) {
throw new Error('Illegal salt, expected Uint8Array with length is 16')
}
if (personal && personal.length !== 16) {
throw new Error('Illegal personal, expected Uint8Array with length is 16')
}
// state, 'param block'
const ctx = {
b: new Uint8Array(128),
h: new Uint32Array(16),
t: 0, // input count
c: 0, // pointer within buffer
outlen: outlen // output length in bytes
}
// initialize parameterBlock before usage
parameterBlock.fill(0)
parameterBlock[0] = outlen
if (key) parameterBlock[1] = key.length
parameterBlock[2] = 1 // fanout
parameterBlock[3] = 1 // depth
if (salt) parameterBlock.set(salt, 32)
if (personal) parameterBlock.set(personal, 48)
// initialize hash state
for (let i = 0; i < 16; i++) {
ctx.h[i] = BLAKE2B_IV32[i] ^ B2B_GET32(parameterBlock, i * 4)
}
// key the hash, if applicable
if (key) {
blake2bUpdate(ctx, key)
// at the end
ctx.c = 128
}
return ctx
}
// Updates a BLAKE2b streaming hash
// Requires hash context and Uint8Array (byte array)
function blake2bUpdate (ctx, input) {
for (let i = 0; i < input.length; i++) {
if (ctx.c === 128) {
// buffer full ?
ctx.t += ctx.c // add counters
blake2bCompress(ctx, false) // compress (not last)
ctx.c = 0 // counter to zero
}
ctx.b[ctx.c++] = input[i]
}
}
// Completes a BLAKE2b streaming hash
// Returns a Uint8Array containing the message digest
function blake2bFinal (ctx) {
ctx.t += ctx.c // mark last block offset
while (ctx.c < 128) {
// fill up with zeros
ctx.b[ctx.c++] = 0
}
blake2bCompress(ctx, true) // final block flag = 1
// little endian convert and store
const out = new Uint8Array(ctx.outlen)
for (let i = 0; i < ctx.outlen; i++) {
out[i] = ctx.h[i >> 2] >> (8 * (i & 3))
}
return out
}
// Computes the BLAKE2B hash of a string or byte array, and returns a Uint8Array
//
// Returns a n-byte Uint8Array
//
// Parameters:
// - input - the input bytes, as a string, Buffer or Uint8Array
// - key - optional key Uint8Array, up to 64 bytes
// - outlen - optional output length in bytes, default 64
// - salt - optional salt bytes, string, Buffer or Uint8Array
// - personal - optional personal bytes, string, Buffer or Uint8Array
function blake2b (input, key, outlen, salt, personal) {
// preprocess inputs
outlen = outlen || 64
input = util.normalizeInput(input)
if (salt) {
salt = util.normalizeInput(salt)
}
if (personal) {
personal = util.normalizeInput(personal)
}
// do the math
const ctx = blake2bInit(outlen, key, salt, personal)
blake2bUpdate(ctx, input)
return blake2bFinal(ctx)
}
// Computes the BLAKE2B hash of a string or byte array
//
// Returns an n-byte hash in hex, all lowercase
//
// Parameters:
// - input - the input bytes, as a string, Buffer, or Uint8Array
// - key - optional key Uint8Array, up to 64 bytes
// - outlen - optional output length in bytes, default 64
// - salt - optional salt bytes, string, Buffer or Uint8Array
// - personal - optional personal bytes, string, Buffer or Uint8Array
function blake2bHex (input, key, outlen, salt, personal) {
const output = blake2b(input, key, outlen, salt, personal)
return util.toHex(output)
}
export {
blake2b,
blake2bHex,
blake2bInit,
blake2bUpdate,
blake2bFinal
}
+84
View File
@@ -0,0 +1,84 @@
const ERROR_MSG_INPUT = 'Input must be an string, Buffer or Uint8Array'
// For convenience, let people hash a string, not just a Uint8Array
function normalizeInput (input) {
let ret
if (input instanceof Uint8Array) {
ret = input
} else if (typeof input === 'string') {
const encoder = new TextEncoder()
ret = encoder.encode(input)
} else {
throw new Error(ERROR_MSG_INPUT)
}
return ret
}
// Converts a Uint8Array to a hexadecimal string
// For example, toHex([255, 0, 255]) returns "ff00ff"
function toHex (bytes) {
return Array.prototype.map
.call(bytes, function (n) {
return (n < 16 ? '0' : '') + n.toString(16)
})
.join('')
}
// Converts any value in [0...2^32-1] to an 8-character hex string
function uint32ToHex (val) {
return (0x100000000 + val).toString(16).substring(1)
}
// For debugging: prints out hash state in the same format as the RFC
// sample computation exactly, so that you can diff
function debugPrint (label, arr, size) {
let msg = '\n' + label + ' = '
for (let i = 0; i < arr.length; i += 2) {
if (size === 32) {
msg += uint32ToHex(arr[i]).toUpperCase()
msg += ' '
msg += uint32ToHex(arr[i + 1]).toUpperCase()
} else if (size === 64) {
msg += uint32ToHex(arr[i + 1]).toUpperCase()
msg += uint32ToHex(arr[i]).toUpperCase()
} else throw new Error('Invalid size ' + size)
if (i % 6 === 4) {
msg += '\n' + new Array(label.length + 4).join(' ')
} else if (i < arr.length - 2) {
msg += ' '
}
}
console.log(msg)
}
// For performance testing: generates N bytes of input, hashes M times
// Measures and prints MB/second hash performance each time
function testSpeed (hashFn, N, M) {
let startMs = new Date().getTime()
const input = new Uint8Array(N)
for (let i = 0; i < N; i++) {
input[i] = i % 256
}
const genMs = new Date().getTime()
console.log('Generated random input in ' + (genMs - startMs) + 'ms')
startMs = genMs
for (let i = 0; i < M; i++) {
const hashHex = hashFn(input)
const hashMs = new Date().getTime()
const ms = hashMs - startMs
startMs = hashMs
console.log('Hashed in ' + ms + 'ms: ' + hashHex.substring(0, 20) + '...')
console.log(
Math.round((N / (1 << 20) / (ms / 1000)) * 100) / 100 + ' MB PER SECOND'
)
}
}
export {
normalizeInput,
toHex,
debugPrint,
testSpeed
}
+5
View File
@@ -0,0 +1,5 @@
{type, external}.
{realm, local}.
{name, blakejs}.
{version, "1.2.1"}.
{deps, []}.
+1
View File
@@ -1,6 +1,7 @@
ISC License
Copyright (c) 2022 Peter Harpending
Copyright (c) 2022-2023 Aeternity Foundation
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
+11
View File
@@ -0,0 +1,11 @@
# parasite lib: talk to aeternity json node from ts
do not use this library
## building
requires [jex](../../utils/jex/)
```
jex install
```
+1 -1
View File
@@ -1,5 +1,5 @@
{type, library}.
{realm, local}.
{name, parasite}.
{version, "0.2.2"}.
{version, "0.2.3"}.
{deps, []}.
+8 -477
View File
@@ -1,482 +1,13 @@
TweetNaCl.js
============
# tweetnacl
Port of [TweetNaCl](http://tweetnacl.cr.yp.to) / [NaCl](http://nacl.cr.yp.to/)
to JavaScript for modern browsers and Node.js. Public domain.
This is a jex port of [TweetNaCl](https://github.com/dchest/tweetnacl-js)
[![Build Status](https://travis-ci.org/dchest/tweetnacl-js.svg?branch=master)
](https://travis-ci.org/dchest/tweetnacl-js)
Public Domain
Demo: <https://dchest.github.io/tweetnacl-js/>
## building
Documentation
=============
requires [jex](../../utils/jex/)
* [Overview](#overview)
* [Audits](#audits)
* [Installation](#installation)
* [Examples](#examples)
* [Usage](#usage)
* [Public-key authenticated encryption (box)](#public-key-authenticated-encryption-box)
* [Secret-key authenticated encryption (secretbox)](#secret-key-authenticated-encryption-secretbox)
* [Scalar multiplication](#scalar-multiplication)
* [Signatures](#signatures)
* [Hashing](#hashing)
* [Random bytes generation](#random-bytes-generation)
* [Constant-time comparison](#constant-time-comparison)
* [System requirements](#system-requirements)
* [Development and testing](#development-and-testing)
* [Benchmarks](#benchmarks)
* [Contributors](#contributors)
* [Who uses it](#who-uses-it)
Overview
--------
The primary goal of this project is to produce a translation of TweetNaCl to
JavaScript which is as close as possible to the original C implementation, plus
a thin layer of idiomatic high-level API on top of it.
There are two versions, you can use either of them:
* `nacl.js` is the port of TweetNaCl with minimum differences from the
original + high-level API.
* `nacl-fast.js` is like `nacl.js`, but with some functions replaced with
faster versions. (Used by default when importing NPM package.)
Audits
------
TweetNaCl.js has been audited by [Cure53](https://cure53.de/) in January-February
2017 (audit was sponsored by [Deletype](https://deletype.com)):
> The overall outcome of this audit signals a particularly positive assessment
> for TweetNaCl-js, as the testing team was unable to find any security
> problems in the library.
[Read full audit report](https://cure53.de/tweetnacl.pdf)
While the audit didn't find any bugs, there has been [1 bug](https://github.com/dchest/tweetnacl-js/issues/187) discovered and fixed after the audit.
Installation
------------
You can install TweetNaCl.js via a package manager:
[Yarn](https://yarnpkg.com/):
$ yarn add tweetnacl
[NPM](https://www.npmjs.org/):
$ npm install tweetnacl
or [download source code](https://github.com/dchest/tweetnacl-js/releases).
Examples
--------
You can find usage examples in our [wiki](https://github.com/dchest/tweetnacl-js/wiki/Examples).
Usage
-----
All API functions accept and return bytes as `Uint8Array`s. If you need to
encode or decode strings, use functions from
<https://github.com/dchest/tweetnacl-util-js> or one of the more robust codec
packages.
In Node.js v4 and later `Buffer` objects are backed by `Uint8Array`s, so you
can freely pass them to TweetNaCl.js functions as arguments. The returned
objects are still `Uint8Array`s, so if you need `Buffer`s, you'll have to
convert them manually; make sure to convert using copying: `Buffer.from(array)`
(or `new Buffer(array)` in Node.js v4 or earlier), instead of sharing:
`Buffer.from(array.buffer)` (or `new Buffer(array.buffer)` Node 4 or earlier),
because some functions return subarrays of their buffers.
### Public-key authenticated encryption (box)
Implements *x25519-xsalsa20-poly1305*.
#### nacl.box.keyPair()
Generates a new random key pair for box and returns it as an object with
`publicKey` and `secretKey` members:
{
publicKey: ..., // Uint8Array with 32-byte public key
secretKey: ... // Uint8Array with 32-byte secret key
}
#### nacl.box.keyPair.fromSecretKey(secretKey)
Returns a key pair for box with public key corresponding to the given secret
key.
#### nacl.box(message, nonce, theirPublicKey, mySecretKey)
Encrypts and authenticates message using peer's public key, our secret key, and
the given nonce, which must be unique for each distinct message for a key pair.
Returns an encrypted and authenticated message, which is
`nacl.box.overheadLength` longer than the original message.
#### nacl.box.open(box, nonce, theirPublicKey, mySecretKey)
Authenticates and decrypts the given box with peer's public key, our secret
key, and the given nonce.
Returns the original message, or `null` if authentication fails.
#### nacl.box.before(theirPublicKey, mySecretKey)
Returns a precomputed shared key which can be used in `nacl.box.after` and
`nacl.box.open.after`.
#### nacl.box.after(message, nonce, sharedKey)
Same as `nacl.box`, but uses a shared key precomputed with `nacl.box.before`.
#### nacl.box.open.after(box, nonce, sharedKey)
Same as `nacl.box.open`, but uses a shared key precomputed with `nacl.box.before`.
#### Constants
##### nacl.box.publicKeyLength = 32
Length of public key in bytes.
##### nacl.box.secretKeyLength = 32
Length of secret key in bytes.
##### nacl.box.sharedKeyLength = 32
Length of precomputed shared key in bytes.
##### nacl.box.nonceLength = 24
Length of nonce in bytes.
##### nacl.box.overheadLength = 16
Length of overhead added to box compared to original message.
### Secret-key authenticated encryption (secretbox)
Implements *xsalsa20-poly1305*.
#### nacl.secretbox(message, nonce, key)
Encrypts and authenticates message using the key and the nonce. The nonce must
be unique for each distinct message for this key.
Returns an encrypted and authenticated message, which is
`nacl.secretbox.overheadLength` longer than the original message.
#### nacl.secretbox.open(box, nonce, key)
Authenticates and decrypts the given secret box using the key and the nonce.
Returns the original message, or `null` if authentication fails.
#### Constants
##### nacl.secretbox.keyLength = 32
Length of key in bytes.
##### nacl.secretbox.nonceLength = 24
Length of nonce in bytes.
##### nacl.secretbox.overheadLength = 16
Length of overhead added to secret box compared to original message.
### Scalar multiplication
Implements *x25519*.
#### nacl.scalarMult(n, p)
Multiplies an integer `n` by a group element `p` and returns the resulting
group element.
#### nacl.scalarMult.base(n)
Multiplies an integer `n` by a standard group element and returns the resulting
group element.
#### Constants
##### nacl.scalarMult.scalarLength = 32
Length of scalar in bytes.
##### nacl.scalarMult.groupElementLength = 32
Length of group element in bytes.
### Signatures
Implements [ed25519](http://ed25519.cr.yp.to).
#### nacl.sign.keyPair()
Generates new random key pair for signing and returns it as an object with
`publicKey` and `secretKey` members:
{
publicKey: ..., // Uint8Array with 32-byte public key
secretKey: ... // Uint8Array with 64-byte secret key
}
#### nacl.sign.keyPair.fromSecretKey(secretKey)
Returns a signing key pair with public key corresponding to the given
64-byte secret key. The secret key must have been generated by
`nacl.sign.keyPair` or `nacl.sign.keyPair.fromSeed`.
#### nacl.sign.keyPair.fromSeed(seed)
Returns a new signing key pair generated deterministically from a 32-byte seed.
The seed must contain enough entropy to be secure. This method is not
recommended for general use: instead, use `nacl.sign.keyPair` to generate a new
key pair from a random seed.
#### nacl.sign(message, secretKey)
Signs the message using the secret key and returns a signed message.
#### nacl.sign.open(signedMessage, publicKey)
Verifies the signed message and returns the message without signature.
Returns `null` if verification failed.
#### nacl.sign.detached(message, secretKey)
Signs the message using the secret key and returns a signature.
#### nacl.sign.detached.verify(message, signature, publicKey)
Verifies the signature for the message and returns `true` if verification
succeeded or `false` if it failed.
#### Constants
##### nacl.sign.publicKeyLength = 32
Length of signing public key in bytes.
##### nacl.sign.secretKeyLength = 64
Length of signing secret key in bytes.
##### nacl.sign.seedLength = 32
Length of seed for `nacl.sign.keyPair.fromSeed` in bytes.
##### nacl.sign.signatureLength = 64
Length of signature in bytes.
### Hashing
Implements *SHA-512*.
#### nacl.hash(message)
Returns SHA-512 hash of the message.
#### Constants
##### nacl.hash.hashLength = 64
Length of hash in bytes.
### Random bytes generation
#### nacl.randomBytes(length)
Returns a `Uint8Array` of the given length containing random bytes of
cryptographic quality.
**Implementation note**
TweetNaCl.js uses the following methods to generate random bytes,
depending on the platform it runs on:
* `window.crypto.getRandomValues` (WebCrypto standard)
* `window.msCrypto.getRandomValues` (Internet Explorer 11)
* `crypto.randomBytes` (Node.js)
If the platform doesn't provide a suitable PRNG, the following functions,
which require random numbers, will throw exception:
* `nacl.randomBytes`
* `nacl.box.keyPair`
* `nacl.sign.keyPair`
Other functions are deterministic and will continue working.
If a platform you are targeting doesn't implement secure random number
generator, but you somehow have a cryptographically-strong source of entropy
(not `Math.random`!), and you know what you are doing, you can plug it into
TweetNaCl.js like this:
nacl.setPRNG(function(x, n) {
// ... copy n random bytes into x ...
});
Note that `nacl.setPRNG` *completely replaces* internal random byte generator
with the one provided.
### Constant-time comparison
#### nacl.verify(x, y)
Compares `x` and `y` in constant time and returns `true` if their lengths are
non-zero and equal, and their contents are equal.
Returns `false` if either of the arguments has zero length, or arguments have
different lengths, or their contents differ.
System requirements
-------------------
TweetNaCl.js supports modern browsers that have a cryptographically secure
pseudorandom number generator and typed arrays, including the latest versions
of:
* Chrome
* Firefox
* Safari (Mac, iOS)
* Internet Explorer 11
Other systems:
* Node.js
Development and testing
------------------------
Install NPM modules needed for development:
$ npm install
To build minified versions:
$ npm run build
Tests use minified version, so make sure to rebuild it every time you change
`nacl.js` or `nacl-fast.js`.
### Testing
To run tests in Node.js:
$ npm run test-node
By default all tests described here work on `nacl.min.js`. To test other
versions, set environment variable `NACL_SRC` to the file name you want to test.
For example, the following command will test fast minified version:
$ NACL_SRC=nacl-fast.min.js npm run test-node
To run full suite of tests in Node.js, including comparing outputs of
JavaScript port to outputs of the original C version:
$ npm run test-node-all
To prepare tests for browsers:
$ npm run build-test-browser
and then open `test/browser/test.html` (or `test/browser/test-fast.html`) to
run them.
To run tests in both Node and Electron:
$ npm test
### Benchmarking
To run benchmarks in Node.js:
$ npm run bench
$ NACL_SRC=nacl-fast.min.js npm run bench
To run benchmarks in a browser, open `test/benchmark/bench.html` (or
`test/benchmark/bench-fast.html`).
Benchmarks
----------
For reference, here are benchmarks from MacBook Pro (Retina, 13-inch, Mid 2014)
laptop with 2.6 GHz Intel Core i5 CPU (Intel) in Chrome 53/OS X, Xiaomi Redmi
Note 3 smartphone with 1.8 GHz Qualcomm Snapdragon 650 64-bit CPU (ARM) in
Chrome 52/Android, and MacBook Air 2020 with Apple M1 SOC (M1) in Chromium 102/macOS.
| | nacl.js Intel | nacl-fast.js Intel | nacl.js ARM | nacl-fast.js ARM | nacl-fast.js M1 |
| ------------- |:-------------:|:-------------------:|:-------------:|:-----------------:|:-----------------:|
| salsa20 | 1.3 MB/s | 128 MB/s | 0.4 MB/s | 43 MB/s | 268 MB/s |
| poly1305 | 13 MB/s | 171 MB/s | 4 MB/s | 52 MB/s | 248 MB/s |
| hash | 4 MB/s | 34 MB/s | 0.9 MB/s | 12 MB/s | 76 MB/s |
| secretbox 1K | 1113 op/s | 57583 op/s | 334 op/s | 14227 op/s | 54546 op/s |
| box 1K | 145 op/s | 718 op/s | 37 op/s | 368 op/s | 1836 op/s |
| scalarMult | 171 op/s | 733 op/s | 56 op/s | 380 op/s | 1882 op/s |
| sign | 77 op/s | 200 op/s | 20 op/s | 61 op/s | 592 op/s |
| sign.open | 39 op/s | 102 op/s | 11 op/s | 31 op/s | 300 op/s |
(You can run benchmarks on your devices by clicking on the links at the bottom
of the [home page](https://tweetnacl.js.org)).
In short, with *nacl-fast.js* and 1024-byte messages you can expect to encrypt and
authenticate more than 57000 messages per second on a typical laptop or more than
14000 messages per second on a $170 smartphone, sign about 500 and verify 300
messages per second on a laptop or 60 and 30 messages per second on a smartphone,
per CPU core (with Web Workers you can do these operations in parallel),
which is good enough for most applications.
Contributors
------------
See AUTHORS.md file.
Third-party libraries based on TweetNaCl.js
-------------------------------------------
* [chloride](https://github.com/dominictarr/chloride) - unified API for various NaCl modules
* [forward-secrecy](https://github.com/alax/forward-secrecy) — Axolotl ratchet implementation
* [nacl-stream](https://github.com/dchest/nacl-stream-js) - streaming encryption
* [ristretto255-js](https://github.com/calibra/ristretto255-js) — implementation of the [ristretto255 group](https://ristretto.group/)
* [tweetnacl-auth-js](https://github.com/dchest/tweetnacl-auth-js) — implementation of [`crypto_auth`](http://nacl.cr.yp.to/auth.html)
* [tweetnacl-js-sealed-box](https://github.com/TogaTech/tweetnacl-js-sealed-box) — fork that adds [`sealed boxes`](https://download.libsodium.org/doc/public-key_cryptography/sealed_boxes.html)
* [ed2curve](https://github.com/dchest/ed2curve-js) — convert Ed25519 signing key pair to X25519 boxes key pair
Who uses it
-----------
Some notable users of TweetNaCl.js are listed on the [associated wiki page](https://github.com/dchest/tweetnacl-js/wiki/Who-uses-TweetNaCl.js).
```
jex dwim+
```
+16
View File
@@ -0,0 +1,16 @@
ISC License
Copyright (c) 2023 Aeternity Foundation
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.
+11
View File
@@ -0,0 +1,11 @@
# `vdk_aecrypt`
Aeternity-specific cryptography helper functions
## building
requires [jex](../../utils/jex/)
```
jex install
```
+6
View File
@@ -0,0 +1,6 @@
{type, library}.
{realm, local}.
{name, vdk_aecrypt}.
{version, "0.1.2"}.
{deps, ["local-blakejs-1.2.1",
"local-vdk_binary-0.1.0"]}.
+260
View File
@@ -0,0 +1,260 @@
/**
* Black boxes away all the AE-specific cryptography routines that call other
* libs
*
* Refs:
* 1. https://github.com/aeternity/aepp-sdk-js/blob/5df22dd297abebc0607710793a7234e6761570d4/src/utils/crypto.ts#L141-L
* 2. https://github.com/aeternity/aepp-sdk-js/blob/5df22dd297abebc0607710793a7234e6761570d4/src/utils/crypto.ts#L160-L
* 3. https://github.com/aeternity/Vanillae/blob/f054744bbceb957afa8fbcf31d93055dd277396b/sidekick/src/sidekick.ts#L926-L1015
*
* @module
*/
// @ts-ignore FIXME yes blake2b sucks
import * as blake2b from './jex_include/local-blakejs-1.2.1/dist/blake2b.js';
import * as vdk_binary from './jex_include/local-vdk_binary-0.1.0/dist/vdk_binary.js';
export {
hash_and_salt_msg,
hash,
salt_msg,
btc_varuint_encode
}
/**
* Quoth https://github.com/aeternity/Vanillae/blob/f054744bbceb957afa8fbcf31d93055dd277396b/sidekick/src/sidekick.ts#L926-L1015
*
* > In order to exclude the possibility of someone using this functionality to
* > trick the user into signing a transaction, the wallet salts and hashes the
* > message, and *then* signs the salted/hashed message.
* >
* > Therefore, naively attempting to verify the signature will not work. You
* > must apply the same preprocessing steps as the wallet, **THEN** check the
* > signature against the salted/hashed message.
* >
* > ```erlang
* > -spec hashed_salted_msg(Message) -> HashedSaltedMessage
* > when Message :: binary(),
* > HashedSaltedMessage :: binary().
* > % @doc Salt the message then hash with blake2b. See:
* > % 1. https://github.com/aeternity/aepp-sdk-js/blob/370f1e30064ad0239ba59931908d9aba0a2e86b6/src/utils/crypto.ts#L83-L85
* > % 2. https://github.com/aeternity/eblake2/blob/60a079f00d72d1bfcc25de8e6996d28f912db3fd/src/eblake2.erl#L23-L25
* >
* > hashed_salted_msg(Msg) ->
* > {ok, HSMsg} = eblake2:blake2b(32, salted_msg(Msg)),
* > HSMsg.
* >
* >
* >
* > -spec salted_msg(Message) -> SaltedMessage
* > when Message :: binary(),
* > SaltedMessage :: binary().
* > % @doc Salt the message the way Superhero does before signing.
* > %
* > % See: https://github.com/aeternity/aepp-sdk-js/blob/370f1e30064ad0239ba59931908d9aba0a2e86b6/src/utils/crypto.ts#L171-L175
* >
* > salted_msg(Msg) when is_binary(Msg) ->
* > P = <<"aeternity Signed Message:\n">>,
* > {ok, SP} = btc_varuint_encode(byte_size(P)),
* > {ok, SMsg} = btc_varuint_encode(byte_size(Msg)),
* > <<SP/binary,
* > P/binary,
* > SMsg/binary,
* > Msg/binary>>.
* >
* >
* >
* > -spec btc_varuint_encode(Integer) -> Result
* > when Integer :: integer(),
* > Result :: {ok, Encoded :: binary()}
* > | {error, Reason :: term()}.
* > % @doc Bitcoin varuint encode
* > %
* > % See: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
* >
* > btc_varuint_encode(N) when N < 0 ->
* > {error, {negative_N, N}};
* > btc_varuint_encode(N) when N < 16#FD ->
* > {ok, <<N>>};
* > btc_varuint_encode(N) when N =< 16#FFFF ->
* > NBytes = eu(N, 2),
* > {ok, <<16#FD, NBytes/binary>>};
* > btc_varuint_encode(N) when N =< 16#FFFF_FFFF ->
* > NBytes = eu(N, 4),
* > {ok, <<16#FE, NBytes/binary>>};
* > btc_varuint_encode(N) when N < (2 bsl 64) ->
* > NBytes = eu(N, 8),
* > {ok, <<16#FF, NBytes/binary>>}.
* >
* > % eu = encode unsigned (little endian with a given byte width)
* > % means add zero bytes to the end as needed
* > eu(N, Size) ->
* > Bytes = binary:encode_unsigned(N, little),
* > NExtraZeros = Size - byte_size(Bytes),
* > ExtraZeros = << <<0>> || _ <- lists:seq(1, NExtraZeros) >>,
* > <<Bytes/binary, ExtraZeros/binary>>.
* > ```
*/
function
hash_and_salt_msg
(message_str : string)
: Uint8Array
{
let message_bytes : Uint8Array = vdk_binary.encode_utf8(message_str);
let salted_bytes : Uint8Array = salt_msg(message_bytes);
return hash(salted_bytes);
}
/**
* Blake2 hash of data
*/
function
hash
(data_bytes : Uint8Array)
: Uint8Array
{
// console.error('hash!');
return blake2b.blake2b(data_bytes, // bytes to hash
undefined, // key (optional)
32); // resulting byte length
}
/**
* salt the message
*
* ```erlang
* -spec salted_msg(Message) -> SaltedMessage
* when Message :: binary(),
* SaltedMessage :: binary().
* % @doc Salt the message the way Superhero does before signing.
* %
* % See: https://github.com/aeternity/aepp-sdk-js/blob/370f1e30064ad0239ba59931908d9aba0a2e86b6/src/utils/crypto.ts#L171-L175
*
* salted_msg(Msg) when is_binary(Msg) ->
* P = <<"aeternity Signed Message:\n">>,
* {ok, SP} = btc_varuint_encode(byte_size(P)),
* {ok, SMsg} = btc_varuint_encode(byte_size(Msg)),
* <<SP/binary,
* P/binary,
* SMsg/binary,
* Msg/binary>>.
* ```
*/
function
salt_msg
(msg_bytes : Uint8Array)
: Uint8Array
{
let prefix_str : string = 'aeternity Signed Message:\n';
let prefix_bytes : Uint8Array = vdk_binary.encode_utf8(prefix_str);
let prefix_size_n : number = prefix_bytes.byteLength;
let prefix_size_bytes : Uint8Array = btc_varuint_encode(prefix_size_n);
let msg_size_n : number = msg_bytes.byteLength;
let msg_size_bytes : Uint8Array = btc_varuint_encode(msg_size_n);
return vdk_binary.bytes_concat_arr([prefix_size_bytes,
prefix_bytes,
msg_size_bytes,
msg_bytes]);
}
/**
* btc varuint encoding
*
* function is the following Erlang code translated into TS
*
* ```erlang
* -spec btc_varuint_encode(Integer) -> Result
* when Integer :: integer(),
* Result :: {ok, Encoded :: binary()}
* | {error, Reason :: term()}.
* % @doc Bitcoin varuint encode
* %
* % See: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
*
* btc_varuint_encode(N) when N < 0 ->
* {error, {negative_N, N}};
* btc_varuint_encode(N) when N < 16#FD ->
* {ok, <<N>>};
* btc_varuint_encode(N) when N =< 16#FFFF ->
* NBytes = eu(N, 2),
* {ok, <<16#FD, NBytes/binary>>};
* btc_varuint_encode(N) when N =< 16#FFFF_FFFF ->
* NBytes = eu(N, 4),
* {ok, <<16#FE, NBytes/binary>>};
* btc_varuint_encode(N) when N < (2 bsl 64) ->
* NBytes = eu(N, 8),
* {ok, <<16#FF, NBytes/binary>>}.
* ```
*/
function
btc_varuint_encode
(n : number)
: Uint8Array
{
if
(n < 0) {
throw new Error('n < 0');
}
else if
(n < 0xFD) {
return new Uint8Array([n]);
}
else if
(n < 0xFFFF) {
let prefix : Uint8Array = new Uint8Array([0xFD]);
let n_bytes : Uint8Array = eu(n, 2);
return vdk_binary.bytes_concat(prefix, n_bytes);
}
else if
(n < 0xFFFF_FFFF) {
let prefix : Uint8Array = new Uint8Array([0xFE]);
let n_bytes : Uint8Array = eu(n, 4);
return vdk_binary.bytes_concat(prefix, n_bytes);
}
else {
let prefix : Uint8Array = new Uint8Array([0xFF]);
let n_bytes : Uint8Array = eu(n, 8);
return vdk_binary.bytes_concat(prefix, n_bytes);
}
}
/**
* unsigned integer little endian encoding with a given byte width
*
* @internal
*/
function
eu
(n : number,
byte_width : number)
: Uint8Array
{
// endianness is byte-level not bit-level
// 3> binary:encode_unsigned(258, big).
// <<1,2>>
// 4> binary:encode_unsigned(258, little).
// <<2,1>>
// first encode n as little endian
let n_bytes : Uint8Array = vdk_binary.bigint_to_bytes_little(BigInt(n));
// figure out how much padding we need
let num_extra_zeros : number = byte_width - n_bytes.byteLength;
let extra_zeros : Uint8Array = vdk_binary.bytes_zeros(num_extra_zeros);
// in little endian, padding 0s go on the right
return vdk_binary.bytes_concat(n_bytes, extra_zeros);
}
+10
View File
@@ -0,0 +1,10 @@
dist
jex_mindist
jex_include
*.beam
*.swp
*.swo
test/testgen/b58_cases_2.eterms
test/testgen/b58_cases_3.eterms
__pycache__
docs/
+16
View File
@@ -0,0 +1,16 @@
ISC License
Copyright (c) 2023 Aeternity Foundation
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.
+11
View File
@@ -0,0 +1,11 @@
# `vdk_aertext`: AE Recovery Text
Seed phrases bad. AE Recovery Text good
## building
requires [jex](../../utils/jex/)
```
jex install
```
@@ -1,5 +1,5 @@
{type, library}.
{realm, local}.
{name, vdk_faert}.
{name, vdk_aertext}.
{version, "0.1.0"}.
{deps, ["local-vdk_safe-0.1.0"]}.
@@ -1,5 +1,5 @@
/**
* FÆRT: Fast Æternity Recovery Text
* ÆRText: Æternity Recovery Text
*
* Reference: https://gitlab.com/zxq9/passgas/-/blob/83607fedb08be5dfd03210e331f9c76125bb3467/fullofbeans
*
+16
View File
@@ -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}
+10
View File
@@ -0,0 +1,10 @@
dist
jex_mindist
jex_include
*.beam
*.swp
*.swo
test/testgen/b58_cases_2.eterms
test/testgen/b58_cases_3.eterms
__pycache__
docs/
+16
View File
@@ -0,0 +1,16 @@
ISC License
Copyright (c) 2023 Aeternity Foundation
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.
+12
View File
@@ -0,0 +1,12 @@
# `vdk_aeser`
Serialization/deserialization routines for AE data. For instance, read a
`tx_...` string and figure out what the sender, recipient, payload, etc are
## building
requires [jex](../../utils/jex/)
```
jex install
```
+8
View File
@@ -0,0 +1,8 @@
{type, library}.
{realm, local}.
{name, vdk_aeser}.
{version, "0.1.1"}.
{deps, ["local-vdk_base58-0.1.0",
"local-vdk_base64-0.1.0",
"local-vdk_rlp-0.1.0",
"local-vdk_binary-0.1.0"]}.
+346
View File
@@ -0,0 +1,346 @@
/**
* Miscellaneous serialization/deserialization functions
*
* Refs:
* 1. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
* 2. https://github.com/aeternity/protocol/blob/master/serializations.md
*
* @module
*/
import * as vdk_base58 from './jex_include/local-vdk_base58-0.1.0/dist/vdk_base58.js';
import * as vdk_base64 from './jex_include/local-vdk_base64-0.1.0/dist/vdk_base64.js';
import * as vdk_binary from './jex_include/local-vdk_binary-0.1.0/dist/vdk_binary.js';
import * as vdk_rlp from './jex_include/local-vdk_rlp-0.1.0/dist/vdk_rlp.js';
export {
pubkey2ak_str,
add_check_bytes,
shasha4,
unbaseNcheck,
baseNcheck,
signed_tx,
mansplain,
mansplain_str
}
export type {
baseNchecked
}
/**
* Take a public key as 32 bytes and encode it in AE API ak_... notation
*
* Refs:
* 1. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
*/
async function
pubkey2ak_str
(pubkey_bytes : Uint8Array)
: Promise<string>
{
let checked_pubkey_bytes : Uint8Array = await add_check_bytes(pubkey_bytes);
let b58_str : string = vdk_base58.encode(checked_pubkey_bytes);
return ("ak_" + b58_str);
}
/**
* Take the double-sha of a byte array and return the first 4 bytes
*
* TypeScript equivalent of
*
* ```erlang
* add_check_bytes(Bin) when is_binary(Bin) ->
* <<Check:4/binary, _/binary>> = sha256(sha256(Bin)),
* <<Bin/binary, Check/binary>>.
* ```
*
* Refs:
* 1. https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
* 2. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
* 3. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/slice
*
* is async because everything is retarded.
*/
async function
add_check_bytes
(data_bytes : Uint8Array)
: Promise<Uint8Array>
{
let check_bytes : Uint8Array = await shasha4(data_bytes);
return vdk_binary.bytes_concat(data_bytes, check_bytes);
}
/**
* Take the double-sha of a byte array and return the first 4 bytes
*
* TypeScript equivalent of
*
* ```erlang
* shasha4(Bin) when is_binary(Bin) ->
* <<Check:4/binary, _/binary>> = sha256(sha256(Bin)),
* <<Check/binary>>.
* ```
*
* Refs:
* 1. https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
* 2. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
* 3. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/slice
*
* is async because everything is retarded.
*/
async function
shasha4
(pubkey_bytes : Uint8Array)
: Promise<Uint8Array>
{
let shad_bytes : ArrayBuffer = await crypto.subtle.digest("SHA-256", pubkey_bytes);
let shashad_bytes : ArrayBuffer = await crypto.subtle.digest("SHA-256", shad_bytes);
let shashad_bytes_4 : ArrayBuffer = shashad_bytes.slice(0, 4);
return new Uint8Array(shashad_bytes_4);
}
/**
* Result of decoding a baseNCheck encoded string
*/
type baseNchecked
= {bytes : Uint8Array,
check_passed : boolean}
/**
* take a baseNcheck (N = 58, 64) encoded string and get out the resulting bytes
*
* returns boolean
*/
async function
unbaseNcheck
(tx_str : string)
: Promise<baseNchecked>
{
// tx_... ak_... etc
let prefix : string = tx_str.slice(0, 2);
let baseNstr : string = tx_str.slice(3);
// branch for base58 or base64 based on prefix
// table:
// https://github.com/aeternity/protocol/blob/fd179822fc70241e79cbef7636625cf344a08109/node/api/api_encoding.md
switch (prefix) {
case 'ak': return await unbase58check(baseNstr); // Account pubkey
case 'ba': return await unbase64check(baseNstr); // Byte array
case 'bf': return await unbase58check(baseNstr); // Block Proof of Fraud hash
case 'bs': return await unbase58check(baseNstr); // Block State hash
case 'bx': return await unbase58check(baseNstr); // Block transaction hash
case 'cb': return await unbase64check(baseNstr); // Contract byte array
case 'ch': return await unbase58check(baseNstr); // Channel
case 'cm': return await unbase58check(baseNstr); // Commitment
case 'ct': return await unbase58check(baseNstr); // Contract pubkey
case 'kh': return await unbase58check(baseNstr); // Key block hash
case 'mh': return await unbase58check(baseNstr); // Micro block hash
case 'nm': return await unbase58check(baseNstr); // Name
case 'ok': return await unbase58check(baseNstr); // Oracle pubkey
case 'oq': return await unbase58check(baseNstr); // Oracle query id
case 'or': return await unbase64check(baseNstr); // Oracle response
case 'ov': return await unbase64check(baseNstr); // Oracle query
case 'pi': return await unbase64check(baseNstr); // Proof of Inclusion
case 'pp': return await unbase58check(baseNstr); // Peer pubkey
case 'sg': return await unbase58check(baseNstr); // Signature
case 'ss': return await unbase64check(baseNstr); // State trees
case 'cs': return await unbase64check(baseNstr); // Contract calls state tree
case 'ck': return await unbase64check(baseNstr); // Contract state key
case 'cv': return await unbase64check(baseNstr); // Contract state value
case 'st': return await unbase64check(baseNstr); // State
case 'th': return await unbase58check(baseNstr); // Transaction hash
case 'tx': return await unbase64check(baseNstr); // Transaction
default : throw new Error('unknown prefix: ' + prefix);
}
}
async function
unbase64check
(baseNstr : string)
: Promise<baseNchecked>
{
let bytes : Uint8Array = vdk_base64.decode(baseNstr);
// ah ok here is the bug
let bytes_len : number = bytes.byteLength;
let data_bytes_len : number = bytes_len - 4;
let data_bytes : Uint8Array = bytes.slice(0, data_bytes_len);
let check_bytes : Uint8Array = bytes.slice(data_bytes_len);
let correct_check_bytes : Uint8Array = await shasha4(data_bytes);
let check_passed : boolean = vdk_binary.bytes_eq(correct_check_bytes, check_bytes);
return {bytes : data_bytes,
check_passed : check_passed};
}
async function
unbase58check
(baseNstr : string)
: Promise<baseNchecked>
{
let bytes : Uint8Array = vdk_base58.decode(baseNstr);
let bytes_len : number = bytes.byteLength;
let data_bytes_len : number = bytes_len - 4;
let data_bytes : Uint8Array = bytes.slice(0, data_bytes_len);
let check_bytes : Uint8Array = bytes.slice(data_bytes_len);
let correct_check_bytes : Uint8Array = await shasha4(data_bytes);
let check_passed : boolean = vdk_binary.bytes_eq(correct_check_bytes, check_bytes);
return {bytes : data_bytes,
check_passed : check_passed};
}
/**
* take bytes and encode using baseNcheck (N = 58, 64) according to the prefix
*/
async function
baseNcheck
(prefix : string,
data_bytes : Uint8Array)
: Promise<string>
{
// tx_... ak_... etc
// branch for base58 or base64 based on prefix
// table:
// https://github.com/aeternity/protocol/blob/fd179822fc70241e79cbef7636625cf344a08109/node/api/api_encoding.md
switch (prefix) {
case 'ak': return await base58check(prefix, data_bytes); // Account pubkey
case 'ba': return await base64check(prefix, data_bytes); // Byte array
case 'bf': return await base58check(prefix, data_bytes); // Block Proof of Fraud hash
case 'bs': return await base58check(prefix, data_bytes); // Block State hash
case 'bx': return await base58check(prefix, data_bytes); // Block transaction hash
case 'cb': return await base64check(prefix, data_bytes); // Contract byte array
case 'ch': return await base58check(prefix, data_bytes); // Channel
case 'cm': return await base58check(prefix, data_bytes); // Commitment
case 'ct': return await base58check(prefix, data_bytes); // Contract pubkey
case 'kh': return await base58check(prefix, data_bytes); // Key block hash
case 'mh': return await base58check(prefix, data_bytes); // Micro block hash
case 'nm': return await base58check(prefix, data_bytes); // Name
case 'ok': return await base58check(prefix, data_bytes); // Oracle pubkey
case 'oq': return await base58check(prefix, data_bytes); // Oracle query id
case 'or': return await base64check(prefix, data_bytes); // Oracle response
case 'ov': return await base64check(prefix, data_bytes); // Oracle query
case 'pi': return await base64check(prefix, data_bytes); // Proof of Inclusion
case 'pp': return await base58check(prefix, data_bytes); // Peer pubkey
case 'sg': return await base58check(prefix, data_bytes); // Signature
case 'ss': return await base64check(prefix, data_bytes); // State trees
case 'cs': return await base64check(prefix, data_bytes); // Contract calls state tree
case 'ck': return await base64check(prefix, data_bytes); // Contract state key
case 'cv': return await base64check(prefix, data_bytes); // Contract state value
case 'st': return await base64check(prefix, data_bytes); // State
case 'th': return await base58check(prefix, data_bytes); // Transaction hash
case 'tx': return await base64check(prefix, data_bytes); // Transaction
default : throw new Error('unknown prefix: ' + prefix);
}
}
async function
base64check
(prefix : string,
data_bytes : Uint8Array)
: Promise<string>
{
let full_bytes : Uint8Array = await add_check_bytes(data_bytes);
let full_str : string = vdk_base64.encode(full_bytes);
return prefix + '_' + full_str;
}
async function
base58check
(prefix : string,
data_bytes : Uint8Array)
: Promise<string>
{
let full_bytes : Uint8Array = await add_check_bytes(data_bytes);
let full_str : string = vdk_base58.encode(full_bytes);
return prefix + '_' + full_str;
}
/**
* RLP-encode signed tx (signatures and tx are both the BINARY representations)
*
* See https://github.com/aeternity/protocol/blob/fd179822fc70241e79cbef7636625cf344a08109/serializations.md#signed-transaction
*/
function
signed_tx
(signatures : Array<Uint8Array>,
tx : Uint8Array)
: Uint8Array
{
// tag for signed tx
let tag_bytes = vdk_rlp.encode_uint(11);
// not sure what version number should be but guessing 1
let vsn_bytes = vdk_rlp.encode_uint(1);
// result is [tag, vsn, signatures, tx]
return vdk_rlp.encode([tag_bytes, vsn_bytes, signatures, tx]);
}
/**
* Take apart some API-encoded data and return a string that explains it to the
* user.
*
* Mostly for the "confirm transaction" popup
*/
async function
mansplain_str
(api_str : string)
: Promise<string>
{
let mansplained_obj : object = await mansplain(api_str)
return JSON.stringify(mansplained_obj, undefined, 4);
}
/**
* Take apart some API-encoded data and show its component parts
*
* For debugging purposes
*/
async function
mansplain
(api_str : string)
: Promise<object>
{
let prefix : string
let x = await unbaseNcheck(api_str);
let check_passed : boolean = x.check_passed;
let data_bytes : Uint8Array = x.bytes;
let mansplained_data : object = mansplain_bytes(data_bytes);
return {check_passed : check_passed,
data : mansplained_data};
}
/**
* Take RLP encoded bytes and mansplain them
*/
function
mansplain_bytes
(data_bytes : Uint8Array)
: object
{
return vdk_rlp.decode(data_bytes);
}
+16
View File
@@ -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}
+16
View File
@@ -0,0 +1,16 @@
ISC License
Copyright (c) 2023 Aeternity Foundation
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.
+11
View File
@@ -0,0 +1,11 @@
# `vdk_base58`
Base58 encode/decode routines. See <https://zxq9.com/archives/2688>
## building
requires [jex](../../utils/jex/)
```
jex install
```
-403
View File
@@ -1,403 +0,0 @@
/**
* Node API constructor/deconstructor
*
* This is similar to serialization/deserialization, but not quite the same
* thing. It converts back and forth between different forms of
* "api-serialized" data.
*
* References:
* 1. https://github.com/aeternity/protocol/blob/master/serializations.md
* 2. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
*
* ## General type rules
*
* ```
* ERLANG TYPE | JS TYPE
* -------------------------------
* integer | bigint
* list | Array
* binary | Uint8Array
* ```
*
* # Example
*
* We start with the string `tx_+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==`.
*
* We can tell by the `tx_` prefix that this represents transaction data of
* some sort. But the rest of the data is totally opaque. The task of this
* module is to "humanize" that `tx_...` string and show what data is contained
* in the rest of it.
*
* The remainder of the string is a base64-encoded bytestring
*
* ```erlang
* 3> io:format("~tw~n", [base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>)]).
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,118,68,68,244,48,85,218,93,85,120,164,154,55,90,250,24,220,161,1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,89,203,191,216,108,88,186,7,3,234,139,205,10,184,167,108,10,134,15,38,245,97,200,0,0,9,135,104,97,105,110,97,110,97,3,182,66,215>>
* ```
*
* That bytestring contains data encoded using Ethereum's RLP codec. Luckily, I
* wrote an RLP decoder. RLP has two types of data: binaries, and
* arbitrary-depth (possibly empty) lists of binaries.
*
* ```erlang
* -type decoded_data() :: binary() | [decoded_data()].
*
* -spec decode(RLP) -> {Data, Rest}
* when RLP :: binary(),
* Data :: decoded_data(),
* Rest :: binary().
* ```
*
* ```erlang
* 2> rlp:decode(base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>)).
* {[<<"\f">>,
* <<1>>,
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,...>>,
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,...>>,
* <<"\n">>,
* <<15,38,245,97,200,0>>,
* <<0>>,
* <<"\t">>,<<"hainana">>],
* <<3,182,66,215>>}
* ```
*
* As expected, we get back the return tuple `{Data, Rest}`. `Rest` is the double-sha256 of the beginning
*
* ```erlang
* 3> X = base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>).
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,
* 127,198,64,232,35,118,68,68,244,48,85,218,93,...>>
* 4> SizeX = byte_size(X).
* 94
* 6> <<RLPEncodedData:(SizeX - 4)/binary, Hash/binary>> = X.
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,
* 127,198,64,232,35,118,68,68,244,48,85,218,93,...>>
* 10> <<Check:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, RLPEncodedData)).
* <<3,182,66,215,195,99,112,99,25,7,84,31,151,188,149,81,
* 189,184,82,207,164,68,128,43,11,174,236,59,77,...>>
* 11> Hash.
* <<3,182,66,215>>
* 12> Check.
* <<3,182,66,215>>
* ```
*
* What we're really interested in is `Data`
*
* ```erlang
* 14> {Data, _} = rlp:decode(X).
* {[<<"\f">>,
* <<1>>,
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,...>>,
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,...>>,
* <<"\n">>,
* <<15,38,245,97,200,0>>,
* <<0>>,
* <<"\t">>,<<"hainana">>],
* <<3,182,66,215>>}
* ```
*
* `Data` is a list. The first field `<<"\f">>` is meant to be an integer which
* tells us what type of data this is.
*
* ```erlang
* 16> $\f.
* 12
* ```
*
* If we look at our table
* (https://github.com/aeternity/protocol/blob/master/serializations.md#table-of-object-tags),
* we see that a value of `12` is a spend transaction.
*
* The second field `<<1>>` tells us the "version" of the field orderings,
* which we can ignore for now.
*
* The remaining fields are the fields of a spend transaction (https://github.com/aeternity/protocol/blob/master/serializations.md#spend-transaction)
*
* ```erlang
* [ <sender> :: id() % <<1,201,99,126,...> "=" "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE"
* , <recipient> :: id() % <<1,123,102,230,...> "=" "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv"
* , <amount> :: int() % <<"\n">> "=" 10
* , <fee> :: int() % <<15,38,245,97,200,0>> "=" 16_660_000_000_000
* , <ttl> :: int() % <<0>> "=" 0
* , <nonce> :: int() % <<"\t">> "=" 9
* , <payload> :: binary() % <<"hainana">> "=" "hainana"
* ]
* ```
*
* Our task here is to be able to pull apart the "tx_..." string into its fields.
*
* Converting the binaries to integers is pretty trivial. The only mildly
* annoying thing is the `id` type.
*
* `id`s have two fields: a single-byte prefix which says which type of ID it
* is. In this case, both `id`s have a prefix of `1`, which means they are both
* normal accounts (hence the `ak_` prefix on the "api-encoded" id). The other
* options are oracles (prefix `4`/`ok_`), contracts (prefix `5`/`ct_`), or
* names (prefix `2`/`nm_`)
*
* To "api-encode" the name, we first pick the appropriate prefix based on the
* first byte (in this case `1 -> "ak_"). The remaining 32 bytes are then
* double-SHA'd to get the 4-byte check suffix
*
* ```erlang
* 30> SenderBytes = lists:nth(3, Data).
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,120,164,154,55,...>>
* 31> <<1, SenderAddrBytes/binary>> = SenderBytes.
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,120,164,154,55,...>>
* 32> DoubleSha = fun(Bytes) -> <<Foo:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, Bytes)), Foo end.
* #Fun<erl_eval.44.97283095>
* 33> "ak_" ++ b58:enc(<<SenderAddrBytes/binary, (DoubleSha(SenderAddrBytes))/binary>>).
* "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE"
* 34> RecipBytes = lists:nth(4, Data).
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,7,3,234,139,205,...>>
* 35> <<1, RecipAddrBytes/binary>> = RecipBytes.
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,7,3,234,139,205,...>>
* 36> "ak_" ++ b58:enc(<<RecipAddrBytes/binary, (DoubleSha(RecipAddrBytes))/binary>>).
* "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv"
* ```
*
* ```js
* > anth.deconstruct("tx_+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==")
* {tag : 'SpendTx',
* version : 1n,
* fields : {sender : "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE",
* recipient : "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv",
* amount : 10n,
* fee : 16660000000000n,
* ttl : 0n,
* nonce : 9n,
* payload : Uint8Array([104, 97, 105, 110, 97, 110, 97])}}
* ```
*
* @module
*/
export {
// types
tx_str,
deconstructed_tx,
// functions
deconstruct_tx
};
import * as b64 from './b64.js'
import * as bin from './bin.js'
import * as rlp from './rlp.js'
/**
* Alias type for a `tx_...` string
*/
type tx_str = string;
/**
* Alias type for a `sg_...` string
*/
type sg_str = string;
/**
* types of decoded tx we currently support
* @internal
*/
type tx_type_str
= 'SignedTx'
| 'SpendTx'
| 'ContractCreateTx'
| 'ContractCallTx';
/**
* Results of deconstruct_tx
*/
type deconstructed_tx
= {type : 'SignedTx',
version : bigint,
fields : fields_SignedTx}
| {type : 'SpendTx',
version : bigint,
fields : fields_SpendTx}
| {type : 'ContractCreateTx',
version : bigint,
fields : fields_ContractCreateTx}
| {type : 'ContractCallTx'
version : bigint,
fields : fields_ContractCallTx};
/**
* Convenient type alias
*
* @internal
*/
type rlpdata = rlp.decoded_data;
/**
* Fields types
*/
type fields
= fields_SignedTx
| fields_SpendTx
| fields_ContractCreateTx
| fields_ContractCallTx;
type fields_SignedTx
= {signatures : Array<sg_str>,
transaction : tx_str};
type fields_SpendTx
= {sender : string,
recipient : string,
amount : bigint,
fee : bigint,
ttl : bigint,
nonce : bigint,
payload : Uint8Array};
/**
* Deconstruct a Tx
*/
function
deconstruct_tx
(tx_str: tx_str)
: deconstructed_tx
{
let b64_str : string = tx_str.slice(3); // tx_[...] -> [...]
let tx_rlp_encoded : Uint8Array = b64.decode(b64_str); // [...] -> bytes
let tx_data : Array<rlpdata> = shasha_rlp_decode_list(tx_rlp_encoded); // decode data and check the double-sha thing
let tx_type : bigint = bin.bytes_to_bigint(tx_data[0]); // get a bigint
let tts : tx_type_str = tx_type_str(tx_type);
let tx_version : bigint = bin.bytes_to_bigint(tx_data[1]);
let tx_fields : fields = deconstruct_fields(tts, tx_version, tx_data.slice(2));
return {type : tts,
version : tx_version,
fields : tx_fields};
}
/**
* Data that's "api-encoded" goes through the following stages:
*
* 1. data structure -> rlp decode data (arbitrary-depth [possibly 0] list of bytestrings)
* 2. rlp decode data -> bytestring
* 3. bytestring -> <<Bytestring/binary, Hash:4/binary>>
* 4. HashedBytestring -> base64/base58 string encoding
* 5. Add string prefix
*
* This function undoes step 3 and step 2, returns back the rlp decode data
*
* FIXME: Does not check double-sha (yet); need to figure out way to handle hash failures
* FIXME: No good way to handle failure cases
*
* @internal
*/
function
shasha_rlp_decode_list
(hashed_bs : Uint8Array)
: Array<rlpdata>
{
let len = hashed_bs.length;
let bytes = hashed_bs.slice(0, len - 4);
let {decoded_data} = rlp.decode(bytes);
return (decoded_data as Array<rlpdata>);
}
/**
* Convert an object tag that's a type of transaction to the type string
*
* See: https://github.com/aeternity/protocol/blob/master/serializations.md#table-of-object-tags
*
* @internal
*/
function
tx_type_str
(tx_type_int : bigint)
: tx_type_str
{
switch (tx_type_int)
{
case 11n: return 'SignedTx';
case 12n: return 'SpendTx';
case 42n: return 'ContractCreateTx';
case 43n: return 'ContractCallTx';
default: throw new Error('invalid transaction type: ' + tx_type_int);
}
}
/**
* Given an array of data decoded from RLP, convert it to the fields, as
* appropriate as given by the tx type string and the version
*/
function
deconstruct_fields
(tx_type_str : tx_type_str,
tx_version : bigint,
tx_rawfields : Array<rlpdata>)
: fields
{
switch (tx_type_str)
{
// case 'SignedTx' : return deconstruct_fields_SignedTx(tx_rawfields);
case 'SpendTx' : return deconstruct_fields_SpendTx(tx_rawfields);
// case 'ContractCreateTx' : return deconstruct_fields_ContractCreateTx(tx_rawfields);
// case 'ContractCallTx' : return deconstruct_fields_ContractCallTx(tx_rawfields);
default : throw new Error('invalid tx type str: ' + tx_type_str);
}
}
// TODO: do all this in Erlang
function
deconstruct_fields_SpendTx
(fields: Array<rlpdata>)
: fields_SpendTx
{
let sender_bytes = fields[0];
let recip_bytes = fields[1];
let amount_bytes = fields[2];
let fee_bytes = fields[3];
let ttl_bytes = fields[4];
let nonce_bytes = fields[5];
let payload_bytes = fields[6];
return {sender : encode_id(sender_bytes),
recipient : encode_id(sender_bytes),
amount : bin.bytes_to_bigint(amount_bytes),
fee : bin.bytes_to_bigint(fee_bytes),
ttl : bin.bytes_to_bigint(ttl_bytes),
nonce : bin.bytes_to_bigint(nonce_bytes),
payload : bin.bytes_to_bigint(payload_bytes)};
}
/**
* Convert a binary account/name/etc binary id into the appropriate type of string
*
* @internal
*/
function
encode_id
(id: Uint8Array)
: string
{
throw new Error('nyi');
}
/*
FIXME:
1. work out all this in Erlang to clear conceptual goo
2. think about how i want type safety etc to work
3. think about a language to assert that the data has the correct shape to it
4. get some examples working in Erlang
5. convert erlang code back to ts
*/
-112
View File
@@ -1,112 +0,0 @@
/**
* Concatenate two bitstrings
*/
function
bits_concat
(bits1 : bits,
bits2 : bits)
: bits
{
let result_bit_length : number = bits1.bit_length + bits2.bit_length;
let bytes1 : Uint8Array = bits1.bytes;
let bytes2 : Uint8Array = bits2.bytes;
// using zeros here because of our xor trick in a minute
let result_bits : bits = bits_zeros(result_bit_length);
let result_bytes : Uint8Array = result_bits.bytes;
// alright so
// we can start by copying the first bytes into result bytes
for (let bytes1_idx0 = 0;
bytes1_idx0 < bytes1.length;
bytes1_idx0++)
{
result_bytes[bytes1_idx0] = bytes1[bytes1_idx0];
}
// next
// we need to calculate the left-shift offset
// this will be 8 - (bytes1.bit_length % 8)
let num_trailing_zeros_in_first_array : number = 8 - (bits1.bit_length % 8);
// so
// bytes1: ABCD_EF00
// bytes2: GH12_3000
// result: ABCD_EFGH 1230_0000
// ah ok, so we need to for each byte in the second array
// take the first however many bits, xor it with the existing byte
// then take the last however many bits and place them into the next byte
// this is super confusing but
// ABCD_EF00
// GH12_3456
// operation:
// ABCD_EF00
// xor 0000_00GH
// = ABCD_EFGH 1234_5600
//
// then on the next iteration
// 1234_5600
// abcd_efgh
// ->
// 1234_56ab cdef_gh00
//
// ah so there's a pattern
// however many trailing 0s there are in the first array
// say there's 2
// we take the first 2 bits of the upcoming byte
// xor that against the current byte
// take the last 6 bits of the upcoming byte
// set the next byte to that
//
// have to think about edge behavior
// this is ripe for off-by-1 errors
// but i think the general idea is right
//
// so we start the iteration
// on the last byte of the first array
let last_byte_of_first_array_idx0 : number = bytes1.length - 1;
// and we end
// on the second-to-last-byte of the result array
let second_to_last_byte_of_result_array_idx0 : number = result_bytes.length - 2;
// the reason we do that is because we're doing this is because we are
// going along, xoring against the current byte and then setting the next
// byte
//
// ok so
for (let this_result_byte_idx0 = last_byte_of_first_array_idx0;
this_result_byte_idx0 <= second_to_last_byte_of_result_array_idx0;
this_result_byte_idx0++)
{
let this_result_byte : number = result_bytes[this_result_byte_idx0];
// ok here we need to fish out the relevant byte of the second array
// gaaah
// so this will be 0 at the start of the loop
let relevant_byte_of_second_array_idx0 : number = this_result_byte_idx0 - last_byte_of_first_array_idx0;
let relevant_byte_of_second_array : number = bytes2[relevant_byte_of_second_array_idx0];
// ok so let's fish out the leading digits
// the number of leading digits is the number of trailing 0s in the first array
let num_leading_digits : number = num_trailing_zeros_in_first_array;
let num_trailing_digits : number = 8 - num_leading_digits;
// suppose there are 2 leading digits and 6 trailing digits
// ABCD_EFGH
// leading digits are
// ABCD_EFGH >> 6 = 0000_00AB
// trailing digits are
// (ABCD_EFGH << 2) % 255 = CDEF_GH00
let leading_digits : number = relevant_byte_of_second_array >> num_trailing_digits;
let trailing_digits : number = (relevant_byte_of_second_array << num_leading_digits) % 255;
// xor the current byte against the leading digits
let new_this_result_byte : number = this_result_byte ^ leading_digits;
result_bytes[this_result_byte_idx0] = new_this_result_byte;
// set the next byte to the trailing digits
result_bytes[this_result_byte_idx0 + 1] = trailing_digits;
}
// i think we're done
return {bit_length : result_bit_length,
bytes : result_bytes};
}
-159
View File
@@ -1,159 +0,0 @@
const OTAG_SIGNED_TX = 11n;
const OTAG_SPEND_TX = 12n;
const OTAG_CONTRACT_CREATE_TX = 42n;
const OTAG_CONTRACT_CALL_TX = 43n;
type otag = 11n | 12n | 42n | 43n;
const IDTAG_ACCOUNT = 1n;
const IDTAG_NAME = 2n;
const IDTAG_CONTRACT = 5n;
type idtag = 1n | 2n | 5n;
type id =
{tag : idtag,
hash : Uint8Array};
type SignedTx =
{signatures : Array<Uint8Array>,
transaction : Uint8Array};
type SpendTx =
{sender : id,
recipient : id,
amount : bigint,
fee : bigint,
ttl : bigint,
nonce : bigint,
payload : Uint8Array};
type ContractCreateTx =
{owner : id,
nonce : bigint,
code : Uint8Array,
ct_version : bigint,
fee : bigint,
ttl : bigint,
deposit : bigint,
amount : bigint,
gas : bigint,
gas_price : bigint,
call_data : Uint8Array};
type ContractCallTx =
{caller : id,
nonce : bigint,
contract : id,
abi_version : bigint,
fee : bigint,
ttl : bigint,
amount : bigint,
gas : bigint,
gas_price : bigint,
call_data : Uint8Array};
type tx = SignedTx | SpendTx | ContractCreateTx | ContractCallTx;
type decoded_tx =
{tag : otag,
version : Uint8Array,
tx : tx};
/**
* Decode a `tx_Base64` string
*/
function
decode_tx(tx_str : string): decoded_tx {
let base64_stuff : string = tx_str.slice(3); // tx_[...] -> [...]
let stuff : Uint8Array = b64.decode(base64_stuff); // <<Bin/binary, DoubleSha:4>>
let rlp_stuff : Uint8Array = stuff.slice(0, stuff.length - 4); // <<Bin/binary>>
let decoded_datas : Array<rlp.decoded_data> = rlp.decode(rlp_stuff).decoded_data as Array<rlp.decoded_data>; // decoded_data : list(rlp.decoded_data() :: binary() | list(decoded_data()))
// tag, vsn
let tag_bytes : Uint8Array = decoded_datas[0] as Uint8Array; // [tag, vsn, fields] -> tag
let tag : bigint = bytes_to_bigint(tag_bytes); // <<Tag:(byte_size(TagBytes))>> = TagBytes
let vsn : Uint8Array = decoded_datas[1] as Uint8Array;
// tx fields
let tx_fields : Array<rlp.decoded_data> = decoded_datas.slice(2);
let tx : tx = decode_fields(tag, tx_fields);
return {tag: tag as otag, version: vsn, tx: tx};
}
/**
* Decode a transaction given the raw fields
*
* @internal
*/
function
decode_fields(tag: bigint, fields: Array<rlp.decoded_data>): tx {
switch (tag) {
case 11n: return decode_fields_SignedTx(fields);
case 12n: return decode_fields_SpendTx(fields);
case 42n: return decode_fields_ContractCreateTx(fields);
case 43n: return decode_fields_ContractCallTx(fields);
default : throw new Error("invalid object tag: " + tag);
}
console.log('fields: ', fields);
throw new Error("nyi");
}
/**
* Decode a SignedTx
*
* @internal
*/
function
decode_fields_SignedTx(fields: Array<rlp.decoded_data>): SignedTx {
throw new Error('nyi');
}
/**
* Decode a SpendTx
*
* @internal
*/
function
decode_fields_SpendTx(fields: Array<rlp.decoded_data>): SpendTx {
// [<sender> :: id(),
// <recipient> :: id(),
// <amount> :: int(),
// <fee> :: int(),
// <ttl> :: int(),
// <nonce> :: int(),
// <payload> :: binary()]
let sender : id = decode_id(fields[0] as Uint8Array);
let recipient : id = decode_id(fields[1] as Uint8Array);
let amount : bigint = bytes_to_bigint(fields[2] as Uint8Array);
let fee : bigint = bytes_to_bigint(fields[3] as Uint8Array);
let ttl : bigint = bytes_to_bigint(fields[4] as Uint8Array);
let nonce : bigint = bytes_to_bigint(fields[5] as Uint8Array);
let payload : Uint8Array = fields[6] as Uint8Array;
return {sender : sender,
recipient : recipient,
amount : amount,
fee : fee,
ttl : ttl,
nonce : nonce,
payload : payload};
}
function
decode_fields_ContractCreateTx(fields: Array<rlp.decoded_data>): ContractCreateTx {
throw new Error('nyi');
}
function
decode_fields_ContractCallTx(fields: Array<rlp.decoded_data>): ContractCallTx {
throw new Error('nyi');
}
function
decode_id(id: Uint8Array): id {
let idtag : idtag = BigInt(id[0]) as idtag;
return {tag: idtag, hash: id.slice(1)};
}
-23
View File
@@ -1,23 +0,0 @@
/**
* Vanillae Seed Phrase Library
*
* Refs:
* 1. BIP 39: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
*
* @module
*/
/**
* Get a given number of seed bits.
*
* `how_many` must be a multiple of 33.
*
* Ref: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
*/
function
seed_and_check_bits
(how_many : number)
: Uint8Array
{
}
+16
View File
@@ -0,0 +1,16 @@
ISC License
Copyright (c) 2023 Aeternity Foundation
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.
+11
View File
@@ -0,0 +1,11 @@
# `vdk_base64`
Base64 encode/decode routines. See <https://zxq9.com/archives/2688>
## building
Requires [jex](../../utils/jex)
```
jex install
```
-403
View File
@@ -1,403 +0,0 @@
/**
* Node API constructor/deconstructor
*
* This is similar to serialization/deserialization, but not quite the same
* thing. It converts back and forth between different forms of
* "api-serialized" data.
*
* References:
* 1. https://github.com/aeternity/protocol/blob/master/serializations.md
* 2. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
*
* ## General type rules
*
* ```
* ERLANG TYPE | JS TYPE
* -------------------------------
* integer | bigint
* list | Array
* binary | Uint8Array
* ```
*
* # Example
*
* We start with the string `tx_+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==`.
*
* We can tell by the `tx_` prefix that this represents transaction data of
* some sort. But the rest of the data is totally opaque. The task of this
* module is to "humanize" that `tx_...` string and show what data is contained
* in the rest of it.
*
* The remainder of the string is a base64-encoded bytestring
*
* ```erlang
* 3> io:format("~tw~n", [base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>)]).
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,118,68,68,244,48,85,218,93,85,120,164,154,55,90,250,24,220,161,1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,89,203,191,216,108,88,186,7,3,234,139,205,10,184,167,108,10,134,15,38,245,97,200,0,0,9,135,104,97,105,110,97,110,97,3,182,66,215>>
* ```
*
* That bytestring contains data encoded using Ethereum's RLP codec. Luckily, I
* wrote an RLP decoder. RLP has two types of data: binaries, and
* arbitrary-depth (possibly empty) lists of binaries.
*
* ```erlang
* -type decoded_data() :: binary() | [decoded_data()].
*
* -spec decode(RLP) -> {Data, Rest}
* when RLP :: binary(),
* Data :: decoded_data(),
* Rest :: binary().
* ```
*
* ```erlang
* 2> rlp:decode(base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>)).
* {[<<"\f">>,
* <<1>>,
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,...>>,
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,...>>,
* <<"\n">>,
* <<15,38,245,97,200,0>>,
* <<0>>,
* <<"\t">>,<<"hainana">>],
* <<3,182,66,215>>}
* ```
*
* As expected, we get back the return tuple `{Data, Rest}`. `Rest` is the double-sha256 of the beginning
*
* ```erlang
* 3> X = base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>).
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,
* 127,198,64,232,35,118,68,68,244,48,85,218,93,...>>
* 4> SizeX = byte_size(X).
* 94
* 6> <<RLPEncodedData:(SizeX - 4)/binary, Hash/binary>> = X.
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,
* 127,198,64,232,35,118,68,68,244,48,85,218,93,...>>
* 10> <<Check:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, RLPEncodedData)).
* <<3,182,66,215,195,99,112,99,25,7,84,31,151,188,149,81,
* 189,184,82,207,164,68,128,43,11,174,236,59,77,...>>
* 11> Hash.
* <<3,182,66,215>>
* 12> Check.
* <<3,182,66,215>>
* ```
*
* What we're really interested in is `Data`
*
* ```erlang
* 14> {Data, _} = rlp:decode(X).
* {[<<"\f">>,
* <<1>>,
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,...>>,
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,...>>,
* <<"\n">>,
* <<15,38,245,97,200,0>>,
* <<0>>,
* <<"\t">>,<<"hainana">>],
* <<3,182,66,215>>}
* ```
*
* `Data` is a list. The first field `<<"\f">>` is meant to be an integer which
* tells us what type of data this is.
*
* ```erlang
* 16> $\f.
* 12
* ```
*
* If we look at our table
* (https://github.com/aeternity/protocol/blob/master/serializations.md#table-of-object-tags),
* we see that a value of `12` is a spend transaction.
*
* The second field `<<1>>` tells us the "version" of the field orderings,
* which we can ignore for now.
*
* The remaining fields are the fields of a spend transaction (https://github.com/aeternity/protocol/blob/master/serializations.md#spend-transaction)
*
* ```erlang
* [ <sender> :: id() % <<1,201,99,126,...> "=" "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE"
* , <recipient> :: id() % <<1,123,102,230,...> "=" "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv"
* , <amount> :: int() % <<"\n">> "=" 10
* , <fee> :: int() % <<15,38,245,97,200,0>> "=" 16_660_000_000_000
* , <ttl> :: int() % <<0>> "=" 0
* , <nonce> :: int() % <<"\t">> "=" 9
* , <payload> :: binary() % <<"hainana">> "=" "hainana"
* ]
* ```
*
* Our task here is to be able to pull apart the "tx_..." string into its fields.
*
* Converting the binaries to integers is pretty trivial. The only mildly
* annoying thing is the `id` type.
*
* `id`s have two fields: a single-byte prefix which says which type of ID it
* is. In this case, both `id`s have a prefix of `1`, which means they are both
* normal accounts (hence the `ak_` prefix on the "api-encoded" id). The other
* options are oracles (prefix `4`/`ok_`), contracts (prefix `5`/`ct_`), or
* names (prefix `2`/`nm_`)
*
* To "api-encode" the name, we first pick the appropriate prefix based on the
* first byte (in this case `1 -> "ak_"). The remaining 32 bytes are then
* double-SHA'd to get the 4-byte check suffix
*
* ```erlang
* 30> SenderBytes = lists:nth(3, Data).
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,120,164,154,55,...>>
* 31> <<1, SenderAddrBytes/binary>> = SenderBytes.
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,120,164,154,55,...>>
* 32> DoubleSha = fun(Bytes) -> <<Foo:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, Bytes)), Foo end.
* #Fun<erl_eval.44.97283095>
* 33> "ak_" ++ b58:enc(<<SenderAddrBytes/binary, (DoubleSha(SenderAddrBytes))/binary>>).
* "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE"
* 34> RecipBytes = lists:nth(4, Data).
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,7,3,234,139,205,...>>
* 35> <<1, RecipAddrBytes/binary>> = RecipBytes.
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,7,3,234,139,205,...>>
* 36> "ak_" ++ b58:enc(<<RecipAddrBytes/binary, (DoubleSha(RecipAddrBytes))/binary>>).
* "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv"
* ```
*
* ```js
* > anth.deconstruct("tx_+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==")
* {tag : 'SpendTx',
* version : 1n,
* fields : {sender : "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE",
* recipient : "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv",
* amount : 10n,
* fee : 16660000000000n,
* ttl : 0n,
* nonce : 9n,
* payload : Uint8Array([104, 97, 105, 110, 97, 110, 97])}}
* ```
*
* @module
*/
export {
// types
tx_str,
deconstructed_tx,
// functions
deconstruct_tx
};
import * as b64 from './b64.js'
import * as bin from './bin.js'
import * as rlp from './rlp.js'
/**
* Alias type for a `tx_...` string
*/
type tx_str = string;
/**
* Alias type for a `sg_...` string
*/
type sg_str = string;
/**
* types of decoded tx we currently support
* @internal
*/
type tx_type_str
= 'SignedTx'
| 'SpendTx'
| 'ContractCreateTx'
| 'ContractCallTx';
/**
* Results of deconstruct_tx
*/
type deconstructed_tx
= {type : 'SignedTx',
version : bigint,
fields : fields_SignedTx}
| {type : 'SpendTx',
version : bigint,
fields : fields_SpendTx}
| {type : 'ContractCreateTx',
version : bigint,
fields : fields_ContractCreateTx}
| {type : 'ContractCallTx'
version : bigint,
fields : fields_ContractCallTx};
/**
* Convenient type alias
*
* @internal
*/
type rlpdata = rlp.decoded_data;
/**
* Fields types
*/
type fields
= fields_SignedTx
| fields_SpendTx
| fields_ContractCreateTx
| fields_ContractCallTx;
type fields_SignedTx
= {signatures : Array<sg_str>,
transaction : tx_str};
type fields_SpendTx
= {sender : string,
recipient : string,
amount : bigint,
fee : bigint,
ttl : bigint,
nonce : bigint,
payload : Uint8Array};
/**
* Deconstruct a Tx
*/
function
deconstruct_tx
(tx_str: tx_str)
: deconstructed_tx
{
let b64_str : string = tx_str.slice(3); // tx_[...] -> [...]
let tx_rlp_encoded : Uint8Array = b64.decode(b64_str); // [...] -> bytes
let tx_data : Array<rlpdata> = shasha_rlp_decode_list(tx_rlp_encoded); // decode data and check the double-sha thing
let tx_type : bigint = bin.bytes_to_bigint(tx_data[0]); // get a bigint
let tts : tx_type_str = tx_type_str(tx_type);
let tx_version : bigint = bin.bytes_to_bigint(tx_data[1]);
let tx_fields : fields = deconstruct_fields(tts, tx_version, tx_data.slice(2));
return {type : tts,
version : tx_version,
fields : tx_fields};
}
/**
* Data that's "api-encoded" goes through the following stages:
*
* 1. data structure -> rlp decode data (arbitrary-depth [possibly 0] list of bytestrings)
* 2. rlp decode data -> bytestring
* 3. bytestring -> <<Bytestring/binary, Hash:4/binary>>
* 4. HashedBytestring -> base64/base58 string encoding
* 5. Add string prefix
*
* This function undoes step 3 and step 2, returns back the rlp decode data
*
* FIXME: Does not check double-sha (yet); need to figure out way to handle hash failures
* FIXME: No good way to handle failure cases
*
* @internal
*/
function
shasha_rlp_decode_list
(hashed_bs : Uint8Array)
: Array<rlpdata>
{
let len = hashed_bs.length;
let bytes = hashed_bs.slice(0, len - 4);
let {decoded_data} = rlp.decode(bytes);
return (decoded_data as Array<rlpdata>);
}
/**
* Convert an object tag that's a type of transaction to the type string
*
* See: https://github.com/aeternity/protocol/blob/master/serializations.md#table-of-object-tags
*
* @internal
*/
function
tx_type_str
(tx_type_int : bigint)
: tx_type_str
{
switch (tx_type_int)
{
case 11n: return 'SignedTx';
case 12n: return 'SpendTx';
case 42n: return 'ContractCreateTx';
case 43n: return 'ContractCallTx';
default: throw new Error('invalid transaction type: ' + tx_type_int);
}
}
/**
* Given an array of data decoded from RLP, convert it to the fields, as
* appropriate as given by the tx type string and the version
*/
function
deconstruct_fields
(tx_type_str : tx_type_str,
tx_version : bigint,
tx_rawfields : Array<rlpdata>)
: fields
{
switch (tx_type_str)
{
// case 'SignedTx' : return deconstruct_fields_SignedTx(tx_rawfields);
case 'SpendTx' : return deconstruct_fields_SpendTx(tx_rawfields);
// case 'ContractCreateTx' : return deconstruct_fields_ContractCreateTx(tx_rawfields);
// case 'ContractCallTx' : return deconstruct_fields_ContractCallTx(tx_rawfields);
default : throw new Error('invalid tx type str: ' + tx_type_str);
}
}
// TODO: do all this in Erlang
function
deconstruct_fields_SpendTx
(fields: Array<rlpdata>)
: fields_SpendTx
{
let sender_bytes = fields[0];
let recip_bytes = fields[1];
let amount_bytes = fields[2];
let fee_bytes = fields[3];
let ttl_bytes = fields[4];
let nonce_bytes = fields[5];
let payload_bytes = fields[6];
return {sender : encode_id(sender_bytes),
recipient : encode_id(sender_bytes),
amount : bin.bytes_to_bigint(amount_bytes),
fee : bin.bytes_to_bigint(fee_bytes),
ttl : bin.bytes_to_bigint(ttl_bytes),
nonce : bin.bytes_to_bigint(nonce_bytes),
payload : bin.bytes_to_bigint(payload_bytes)};
}
/**
* Convert a binary account/name/etc binary id into the appropriate type of string
*
* @internal
*/
function
encode_id
(id: Uint8Array)
: string
{
throw new Error('nyi');
}
/*
FIXME:
1. work out all this in Erlang to clear conceptual goo
2. think about how i want type safety etc to work
3. think about a language to assert that the data has the correct shape to it
4. get some examples working in Erlang
5. convert erlang code back to ts
*/
-112
View File
@@ -1,112 +0,0 @@
/**
* Concatenate two bitstrings
*/
function
bits_concat
(bits1 : bits,
bits2 : bits)
: bits
{
let result_bit_length : number = bits1.bit_length + bits2.bit_length;
let bytes1 : Uint8Array = bits1.bytes;
let bytes2 : Uint8Array = bits2.bytes;
// using zeros here because of our xor trick in a minute
let result_bits : bits = bits_zeros(result_bit_length);
let result_bytes : Uint8Array = result_bits.bytes;
// alright so
// we can start by copying the first bytes into result bytes
for (let bytes1_idx0 = 0;
bytes1_idx0 < bytes1.length;
bytes1_idx0++)
{
result_bytes[bytes1_idx0] = bytes1[bytes1_idx0];
}
// next
// we need to calculate the left-shift offset
// this will be 8 - (bytes1.bit_length % 8)
let num_trailing_zeros_in_first_array : number = 8 - (bits1.bit_length % 8);
// so
// bytes1: ABCD_EF00
// bytes2: GH12_3000
// result: ABCD_EFGH 1230_0000
// ah ok, so we need to for each byte in the second array
// take the first however many bits, xor it with the existing byte
// then take the last however many bits and place them into the next byte
// this is super confusing but
// ABCD_EF00
// GH12_3456
// operation:
// ABCD_EF00
// xor 0000_00GH
// = ABCD_EFGH 1234_5600
//
// then on the next iteration
// 1234_5600
// abcd_efgh
// ->
// 1234_56ab cdef_gh00
//
// ah so there's a pattern
// however many trailing 0s there are in the first array
// say there's 2
// we take the first 2 bits of the upcoming byte
// xor that against the current byte
// take the last 6 bits of the upcoming byte
// set the next byte to that
//
// have to think about edge behavior
// this is ripe for off-by-1 errors
// but i think the general idea is right
//
// so we start the iteration
// on the last byte of the first array
let last_byte_of_first_array_idx0 : number = bytes1.length - 1;
// and we end
// on the second-to-last-byte of the result array
let second_to_last_byte_of_result_array_idx0 : number = result_bytes.length - 2;
// the reason we do that is because we're doing this is because we are
// going along, xoring against the current byte and then setting the next
// byte
//
// ok so
for (let this_result_byte_idx0 = last_byte_of_first_array_idx0;
this_result_byte_idx0 <= second_to_last_byte_of_result_array_idx0;
this_result_byte_idx0++)
{
let this_result_byte : number = result_bytes[this_result_byte_idx0];
// ok here we need to fish out the relevant byte of the second array
// gaaah
// so this will be 0 at the start of the loop
let relevant_byte_of_second_array_idx0 : number = this_result_byte_idx0 - last_byte_of_first_array_idx0;
let relevant_byte_of_second_array : number = bytes2[relevant_byte_of_second_array_idx0];
// ok so let's fish out the leading digits
// the number of leading digits is the number of trailing 0s in the first array
let num_leading_digits : number = num_trailing_zeros_in_first_array;
let num_trailing_digits : number = 8 - num_leading_digits;
// suppose there are 2 leading digits and 6 trailing digits
// ABCD_EFGH
// leading digits are
// ABCD_EFGH >> 6 = 0000_00AB
// trailing digits are
// (ABCD_EFGH << 2) % 255 = CDEF_GH00
let leading_digits : number = relevant_byte_of_second_array >> num_trailing_digits;
let trailing_digits : number = (relevant_byte_of_second_array << num_leading_digits) % 255;
// xor the current byte against the leading digits
let new_this_result_byte : number = this_result_byte ^ leading_digits;
result_bytes[this_result_byte_idx0] = new_this_result_byte;
// set the next byte to the trailing digits
result_bytes[this_result_byte_idx0 + 1] = trailing_digits;
}
// i think we're done
return {bit_length : result_bit_length,
bytes : result_bytes};
}
-159
View File
@@ -1,159 +0,0 @@
const OTAG_SIGNED_TX = 11n;
const OTAG_SPEND_TX = 12n;
const OTAG_CONTRACT_CREATE_TX = 42n;
const OTAG_CONTRACT_CALL_TX = 43n;
type otag = 11n | 12n | 42n | 43n;
const IDTAG_ACCOUNT = 1n;
const IDTAG_NAME = 2n;
const IDTAG_CONTRACT = 5n;
type idtag = 1n | 2n | 5n;
type id =
{tag : idtag,
hash : Uint8Array};
type SignedTx =
{signatures : Array<Uint8Array>,
transaction : Uint8Array};
type SpendTx =
{sender : id,
recipient : id,
amount : bigint,
fee : bigint,
ttl : bigint,
nonce : bigint,
payload : Uint8Array};
type ContractCreateTx =
{owner : id,
nonce : bigint,
code : Uint8Array,
ct_version : bigint,
fee : bigint,
ttl : bigint,
deposit : bigint,
amount : bigint,
gas : bigint,
gas_price : bigint,
call_data : Uint8Array};
type ContractCallTx =
{caller : id,
nonce : bigint,
contract : id,
abi_version : bigint,
fee : bigint,
ttl : bigint,
amount : bigint,
gas : bigint,
gas_price : bigint,
call_data : Uint8Array};
type tx = SignedTx | SpendTx | ContractCreateTx | ContractCallTx;
type decoded_tx =
{tag : otag,
version : Uint8Array,
tx : tx};
/**
* Decode a `tx_Base64` string
*/
function
decode_tx(tx_str : string): decoded_tx {
let base64_stuff : string = tx_str.slice(3); // tx_[...] -> [...]
let stuff : Uint8Array = b64.decode(base64_stuff); // <<Bin/binary, DoubleSha:4>>
let rlp_stuff : Uint8Array = stuff.slice(0, stuff.length - 4); // <<Bin/binary>>
let decoded_datas : Array<rlp.decoded_data> = rlp.decode(rlp_stuff).decoded_data as Array<rlp.decoded_data>; // decoded_data : list(rlp.decoded_data() :: binary() | list(decoded_data()))
// tag, vsn
let tag_bytes : Uint8Array = decoded_datas[0] as Uint8Array; // [tag, vsn, fields] -> tag
let tag : bigint = bytes_to_bigint(tag_bytes); // <<Tag:(byte_size(TagBytes))>> = TagBytes
let vsn : Uint8Array = decoded_datas[1] as Uint8Array;
// tx fields
let tx_fields : Array<rlp.decoded_data> = decoded_datas.slice(2);
let tx : tx = decode_fields(tag, tx_fields);
return {tag: tag as otag, version: vsn, tx: tx};
}
/**
* Decode a transaction given the raw fields
*
* @internal
*/
function
decode_fields(tag: bigint, fields: Array<rlp.decoded_data>): tx {
switch (tag) {
case 11n: return decode_fields_SignedTx(fields);
case 12n: return decode_fields_SpendTx(fields);
case 42n: return decode_fields_ContractCreateTx(fields);
case 43n: return decode_fields_ContractCallTx(fields);
default : throw new Error("invalid object tag: " + tag);
}
console.log('fields: ', fields);
throw new Error("nyi");
}
/**
* Decode a SignedTx
*
* @internal
*/
function
decode_fields_SignedTx(fields: Array<rlp.decoded_data>): SignedTx {
throw new Error('nyi');
}
/**
* Decode a SpendTx
*
* @internal
*/
function
decode_fields_SpendTx(fields: Array<rlp.decoded_data>): SpendTx {
// [<sender> :: id(),
// <recipient> :: id(),
// <amount> :: int(),
// <fee> :: int(),
// <ttl> :: int(),
// <nonce> :: int(),
// <payload> :: binary()]
let sender : id = decode_id(fields[0] as Uint8Array);
let recipient : id = decode_id(fields[1] as Uint8Array);
let amount : bigint = bytes_to_bigint(fields[2] as Uint8Array);
let fee : bigint = bytes_to_bigint(fields[3] as Uint8Array);
let ttl : bigint = bytes_to_bigint(fields[4] as Uint8Array);
let nonce : bigint = bytes_to_bigint(fields[5] as Uint8Array);
let payload : Uint8Array = fields[6] as Uint8Array;
return {sender : sender,
recipient : recipient,
amount : amount,
fee : fee,
ttl : ttl,
nonce : nonce,
payload : payload};
}
function
decode_fields_ContractCreateTx(fields: Array<rlp.decoded_data>): ContractCreateTx {
throw new Error('nyi');
}
function
decode_fields_ContractCallTx(fields: Array<rlp.decoded_data>): ContractCallTx {
throw new Error('nyi');
}
function
decode_id(id: Uint8Array): id {
let idtag : idtag = BigInt(id[0]) as idtag;
return {tag: idtag, hash: id.slice(1)};
}
-23
View File
@@ -1,23 +0,0 @@
/**
* Vanillae Seed Phrase Library
*
* Refs:
* 1. BIP 39: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
*
* @module
*/
/**
* Get a given number of seed bits.
*
* `how_many` must be a multiple of 33.
*
* Ref: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
*/
function
seed_and_check_bits
(how_many : number)
: Uint8Array
{
}
+16
View File
@@ -0,0 +1,16 @@
ISC License
Copyright (c) 2023 Aeternity Foundation
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.
+11
View File
@@ -0,0 +1,11 @@
# `vdk_binary`
Basic binary utility functions
## building
Requires [jex](../../utils/jex/)
```
jex install
```
-24
View File
@@ -1,24 +0,0 @@
done:
- base64/base58 encoding/decoding
- rlp
today:
- pull apart data
tomorrow:
- basic easy serialization/deserialization
- serialize/deserialize examples
- (maybe) pull in awcp/sidekick
- sidekick: sign/propagate and sign/noprop
later:
- general node querying with parasite (think about autogeneration)
- simple aetto/ae/etc unit converter
- ghetto sophia ide
- general "send money to someone" page
GUI:
- simple testnet explorer
- contract ide
-403
View File
@@ -1,403 +0,0 @@
/**
* Node API constructor/deconstructor
*
* This is similar to serialization/deserialization, but not quite the same
* thing. It converts back and forth between different forms of
* "api-serialized" data.
*
* References:
* 1. https://github.com/aeternity/protocol/blob/master/serializations.md
* 2. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
*
* ## General type rules
*
* ```
* ERLANG TYPE | JS TYPE
* -------------------------------
* integer | bigint
* list | Array
* binary | Uint8Array
* ```
*
* # Example
*
* We start with the string `tx_+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==`.
*
* We can tell by the `tx_` prefix that this represents transaction data of
* some sort. But the rest of the data is totally opaque. The task of this
* module is to "humanize" that `tx_...` string and show what data is contained
* in the rest of it.
*
* The remainder of the string is a base64-encoded bytestring
*
* ```erlang
* 3> io:format("~tw~n", [base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>)]).
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,118,68,68,244,48,85,218,93,85,120,164,154,55,90,250,24,220,161,1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,89,203,191,216,108,88,186,7,3,234,139,205,10,184,167,108,10,134,15,38,245,97,200,0,0,9,135,104,97,105,110,97,110,97,3,182,66,215>>
* ```
*
* That bytestring contains data encoded using Ethereum's RLP codec. Luckily, I
* wrote an RLP decoder. RLP has two types of data: binaries, and
* arbitrary-depth (possibly empty) lists of binaries.
*
* ```erlang
* -type decoded_data() :: binary() | [decoded_data()].
*
* -spec decode(RLP) -> {Data, Rest}
* when RLP :: binary(),
* Data :: decoded_data(),
* Rest :: binary().
* ```
*
* ```erlang
* 2> rlp:decode(base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>)).
* {[<<"\f">>,
* <<1>>,
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,...>>,
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,...>>,
* <<"\n">>,
* <<15,38,245,97,200,0>>,
* <<0>>,
* <<"\t">>,<<"hainana">>],
* <<3,182,66,215>>}
* ```
*
* As expected, we get back the return tuple `{Data, Rest}`. `Rest` is the double-sha256 of the beginning
*
* ```erlang
* 3> X = base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>).
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,
* 127,198,64,232,35,118,68,68,244,48,85,218,93,...>>
* 4> SizeX = byte_size(X).
* 94
* 6> <<RLPEncodedData:(SizeX - 4)/binary, Hash/binary>> = X.
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,
* 127,198,64,232,35,118,68,68,244,48,85,218,93,...>>
* 10> <<Check:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, RLPEncodedData)).
* <<3,182,66,215,195,99,112,99,25,7,84,31,151,188,149,81,
* 189,184,82,207,164,68,128,43,11,174,236,59,77,...>>
* 11> Hash.
* <<3,182,66,215>>
* 12> Check.
* <<3,182,66,215>>
* ```
*
* What we're really interested in is `Data`
*
* ```erlang
* 14> {Data, _} = rlp:decode(X).
* {[<<"\f">>,
* <<1>>,
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,...>>,
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,...>>,
* <<"\n">>,
* <<15,38,245,97,200,0>>,
* <<0>>,
* <<"\t">>,<<"hainana">>],
* <<3,182,66,215>>}
* ```
*
* `Data` is a list. The first field `<<"\f">>` is meant to be an integer which
* tells us what type of data this is.
*
* ```erlang
* 16> $\f.
* 12
* ```
*
* If we look at our table
* (https://github.com/aeternity/protocol/blob/master/serializations.md#table-of-object-tags),
* we see that a value of `12` is a spend transaction.
*
* The second field `<<1>>` tells us the "version" of the field orderings,
* which we can ignore for now.
*
* The remaining fields are the fields of a spend transaction (https://github.com/aeternity/protocol/blob/master/serializations.md#spend-transaction)
*
* ```erlang
* [ <sender> :: id() % <<1,201,99,126,...> "=" "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE"
* , <recipient> :: id() % <<1,123,102,230,...> "=" "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv"
* , <amount> :: int() % <<"\n">> "=" 10
* , <fee> :: int() % <<15,38,245,97,200,0>> "=" 16_660_000_000_000
* , <ttl> :: int() % <<0>> "=" 0
* , <nonce> :: int() % <<"\t">> "=" 9
* , <payload> :: binary() % <<"hainana">> "=" "hainana"
* ]
* ```
*
* Our task here is to be able to pull apart the "tx_..." string into its fields.
*
* Converting the binaries to integers is pretty trivial. The only mildly
* annoying thing is the `id` type.
*
* `id`s have two fields: a single-byte prefix which says which type of ID it
* is. In this case, both `id`s have a prefix of `1`, which means they are both
* normal accounts (hence the `ak_` prefix on the "api-encoded" id). The other
* options are oracles (prefix `4`/`ok_`), contracts (prefix `5`/`ct_`), or
* names (prefix `2`/`nm_`)
*
* To "api-encode" the name, we first pick the appropriate prefix based on the
* first byte (in this case `1 -> "ak_"). The remaining 32 bytes are then
* double-SHA'd to get the 4-byte check suffix
*
* ```erlang
* 30> SenderBytes = lists:nth(3, Data).
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,120,164,154,55,...>>
* 31> <<1, SenderAddrBytes/binary>> = SenderBytes.
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,120,164,154,55,...>>
* 32> DoubleSha = fun(Bytes) -> <<Foo:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, Bytes)), Foo end.
* #Fun<erl_eval.44.97283095>
* 33> "ak_" ++ b58:enc(<<SenderAddrBytes/binary, (DoubleSha(SenderAddrBytes))/binary>>).
* "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE"
* 34> RecipBytes = lists:nth(4, Data).
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,7,3,234,139,205,...>>
* 35> <<1, RecipAddrBytes/binary>> = RecipBytes.
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,7,3,234,139,205,...>>
* 36> "ak_" ++ b58:enc(<<RecipAddrBytes/binary, (DoubleSha(RecipAddrBytes))/binary>>).
* "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv"
* ```
*
* ```js
* > anth.deconstruct("tx_+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==")
* {tag : 'SpendTx',
* version : 1n,
* fields : {sender : "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE",
* recipient : "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv",
* amount : 10n,
* fee : 16660000000000n,
* ttl : 0n,
* nonce : 9n,
* payload : Uint8Array([104, 97, 105, 110, 97, 110, 97])}}
* ```
*
* @module
*/
export {
// types
tx_str,
deconstructed_tx,
// functions
deconstruct_tx
};
import * as b64 from './b64.js'
import * as bin from './bin.js'
import * as rlp from './rlp.js'
/**
* Alias type for a `tx_...` string
*/
type tx_str = string;
/**
* Alias type for a `sg_...` string
*/
type sg_str = string;
/**
* types of decoded tx we currently support
* @internal
*/
type tx_type_str
= 'SignedTx'
| 'SpendTx'
| 'ContractCreateTx'
| 'ContractCallTx';
/**
* Results of deconstruct_tx
*/
type deconstructed_tx
= {type : 'SignedTx',
version : bigint,
fields : fields_SignedTx}
| {type : 'SpendTx',
version : bigint,
fields : fields_SpendTx}
| {type : 'ContractCreateTx',
version : bigint,
fields : fields_ContractCreateTx}
| {type : 'ContractCallTx'
version : bigint,
fields : fields_ContractCallTx};
/**
* Convenient type alias
*
* @internal
*/
type rlpdata = rlp.decoded_data;
/**
* Fields types
*/
type fields
= fields_SignedTx
| fields_SpendTx
| fields_ContractCreateTx
| fields_ContractCallTx;
type fields_SignedTx
= {signatures : Array<sg_str>,
transaction : tx_str};
type fields_SpendTx
= {sender : string,
recipient : string,
amount : bigint,
fee : bigint,
ttl : bigint,
nonce : bigint,
payload : Uint8Array};
/**
* Deconstruct a Tx
*/
function
deconstruct_tx
(tx_str: tx_str)
: deconstructed_tx
{
let b64_str : string = tx_str.slice(3); // tx_[...] -> [...]
let tx_rlp_encoded : Uint8Array = b64.decode(b64_str); // [...] -> bytes
let tx_data : Array<rlpdata> = shasha_rlp_decode_list(tx_rlp_encoded); // decode data and check the double-sha thing
let tx_type : bigint = bin.bytes_to_bigint(tx_data[0]); // get a bigint
let tts : tx_type_str = tx_type_str(tx_type);
let tx_version : bigint = bin.bytes_to_bigint(tx_data[1]);
let tx_fields : fields = deconstruct_fields(tts, tx_version, tx_data.slice(2));
return {type : tts,
version : tx_version,
fields : tx_fields};
}
/**
* Data that's "api-encoded" goes through the following stages:
*
* 1. data structure -> rlp decode data (arbitrary-depth [possibly 0] list of bytestrings)
* 2. rlp decode data -> bytestring
* 3. bytestring -> <<Bytestring/binary, Hash:4/binary>>
* 4. HashedBytestring -> base64/base58 string encoding
* 5. Add string prefix
*
* This function undoes step 3 and step 2, returns back the rlp decode data
*
* FIXME: Does not check double-sha (yet); need to figure out way to handle hash failures
* FIXME: No good way to handle failure cases
*
* @internal
*/
function
shasha_rlp_decode_list
(hashed_bs : Uint8Array)
: Array<rlpdata>
{
let len = hashed_bs.length;
let bytes = hashed_bs.slice(0, len - 4);
let {decoded_data} = rlp.decode(bytes);
return (decoded_data as Array<rlpdata>);
}
/**
* Convert an object tag that's a type of transaction to the type string
*
* See: https://github.com/aeternity/protocol/blob/master/serializations.md#table-of-object-tags
*
* @internal
*/
function
tx_type_str
(tx_type_int : bigint)
: tx_type_str
{
switch (tx_type_int)
{
case 11n: return 'SignedTx';
case 12n: return 'SpendTx';
case 42n: return 'ContractCreateTx';
case 43n: return 'ContractCallTx';
default: throw new Error('invalid transaction type: ' + tx_type_int);
}
}
/**
* Given an array of data decoded from RLP, convert it to the fields, as
* appropriate as given by the tx type string and the version
*/
function
deconstruct_fields
(tx_type_str : tx_type_str,
tx_version : bigint,
tx_rawfields : Array<rlpdata>)
: fields
{
switch (tx_type_str)
{
// case 'SignedTx' : return deconstruct_fields_SignedTx(tx_rawfields);
case 'SpendTx' : return deconstruct_fields_SpendTx(tx_rawfields);
// case 'ContractCreateTx' : return deconstruct_fields_ContractCreateTx(tx_rawfields);
// case 'ContractCallTx' : return deconstruct_fields_ContractCallTx(tx_rawfields);
default : throw new Error('invalid tx type str: ' + tx_type_str);
}
}
// TODO: do all this in Erlang
function
deconstruct_fields_SpendTx
(fields: Array<rlpdata>)
: fields_SpendTx
{
let sender_bytes = fields[0];
let recip_bytes = fields[1];
let amount_bytes = fields[2];
let fee_bytes = fields[3];
let ttl_bytes = fields[4];
let nonce_bytes = fields[5];
let payload_bytes = fields[6];
return {sender : encode_id(sender_bytes),
recipient : encode_id(sender_bytes),
amount : bin.bytes_to_bigint(amount_bytes),
fee : bin.bytes_to_bigint(fee_bytes),
ttl : bin.bytes_to_bigint(ttl_bytes),
nonce : bin.bytes_to_bigint(nonce_bytes),
payload : bin.bytes_to_bigint(payload_bytes)};
}
/**
* Convert a binary account/name/etc binary id into the appropriate type of string
*
* @internal
*/
function
encode_id
(id: Uint8Array)
: string
{
throw new Error('nyi');
}
/*
FIXME:
1. work out all this in Erlang to clear conceptual goo
2. think about how i want type safety etc to work
3. think about a language to assert that the data has the correct shape to it
4. get some examples working in Erlang
5. convert erlang code back to ts
*/
-112
View File
@@ -1,112 +0,0 @@
/**
* Concatenate two bitstrings
*/
function
bits_concat
(bits1 : bits,
bits2 : bits)
: bits
{
let result_bit_length : number = bits1.bit_length + bits2.bit_length;
let bytes1 : Uint8Array = bits1.bytes;
let bytes2 : Uint8Array = bits2.bytes;
// using zeros here because of our xor trick in a minute
let result_bits : bits = bits_zeros(result_bit_length);
let result_bytes : Uint8Array = result_bits.bytes;
// alright so
// we can start by copying the first bytes into result bytes
for (let bytes1_idx0 = 0;
bytes1_idx0 < bytes1.length;
bytes1_idx0++)
{
result_bytes[bytes1_idx0] = bytes1[bytes1_idx0];
}
// next
// we need to calculate the left-shift offset
// this will be 8 - (bytes1.bit_length % 8)
let num_trailing_zeros_in_first_array : number = 8 - (bits1.bit_length % 8);
// so
// bytes1: ABCD_EF00
// bytes2: GH12_3000
// result: ABCD_EFGH 1230_0000
// ah ok, so we need to for each byte in the second array
// take the first however many bits, xor it with the existing byte
// then take the last however many bits and place them into the next byte
// this is super confusing but
// ABCD_EF00
// GH12_3456
// operation:
// ABCD_EF00
// xor 0000_00GH
// = ABCD_EFGH 1234_5600
//
// then on the next iteration
// 1234_5600
// abcd_efgh
// ->
// 1234_56ab cdef_gh00
//
// ah so there's a pattern
// however many trailing 0s there are in the first array
// say there's 2
// we take the first 2 bits of the upcoming byte
// xor that against the current byte
// take the last 6 bits of the upcoming byte
// set the next byte to that
//
// have to think about edge behavior
// this is ripe for off-by-1 errors
// but i think the general idea is right
//
// so we start the iteration
// on the last byte of the first array
let last_byte_of_first_array_idx0 : number = bytes1.length - 1;
// and we end
// on the second-to-last-byte of the result array
let second_to_last_byte_of_result_array_idx0 : number = result_bytes.length - 2;
// the reason we do that is because we're doing this is because we are
// going along, xoring against the current byte and then setting the next
// byte
//
// ok so
for (let this_result_byte_idx0 = last_byte_of_first_array_idx0;
this_result_byte_idx0 <= second_to_last_byte_of_result_array_idx0;
this_result_byte_idx0++)
{
let this_result_byte : number = result_bytes[this_result_byte_idx0];
// ok here we need to fish out the relevant byte of the second array
// gaaah
// so this will be 0 at the start of the loop
let relevant_byte_of_second_array_idx0 : number = this_result_byte_idx0 - last_byte_of_first_array_idx0;
let relevant_byte_of_second_array : number = bytes2[relevant_byte_of_second_array_idx0];
// ok so let's fish out the leading digits
// the number of leading digits is the number of trailing 0s in the first array
let num_leading_digits : number = num_trailing_zeros_in_first_array;
let num_trailing_digits : number = 8 - num_leading_digits;
// suppose there are 2 leading digits and 6 trailing digits
// ABCD_EFGH
// leading digits are
// ABCD_EFGH >> 6 = 0000_00AB
// trailing digits are
// (ABCD_EFGH << 2) % 255 = CDEF_GH00
let leading_digits : number = relevant_byte_of_second_array >> num_trailing_digits;
let trailing_digits : number = (relevant_byte_of_second_array << num_leading_digits) % 255;
// xor the current byte against the leading digits
let new_this_result_byte : number = this_result_byte ^ leading_digits;
result_bytes[this_result_byte_idx0] = new_this_result_byte;
// set the next byte to the trailing digits
result_bytes[this_result_byte_idx0 + 1] = trailing_digits;
}
// i think we're done
return {bit_length : result_bit_length,
bytes : result_bytes};
}
-159
View File
@@ -1,159 +0,0 @@
const OTAG_SIGNED_TX = 11n;
const OTAG_SPEND_TX = 12n;
const OTAG_CONTRACT_CREATE_TX = 42n;
const OTAG_CONTRACT_CALL_TX = 43n;
type otag = 11n | 12n | 42n | 43n;
const IDTAG_ACCOUNT = 1n;
const IDTAG_NAME = 2n;
const IDTAG_CONTRACT = 5n;
type idtag = 1n | 2n | 5n;
type id =
{tag : idtag,
hash : Uint8Array};
type SignedTx =
{signatures : Array<Uint8Array>,
transaction : Uint8Array};
type SpendTx =
{sender : id,
recipient : id,
amount : bigint,
fee : bigint,
ttl : bigint,
nonce : bigint,
payload : Uint8Array};
type ContractCreateTx =
{owner : id,
nonce : bigint,
code : Uint8Array,
ct_version : bigint,
fee : bigint,
ttl : bigint,
deposit : bigint,
amount : bigint,
gas : bigint,
gas_price : bigint,
call_data : Uint8Array};
type ContractCallTx =
{caller : id,
nonce : bigint,
contract : id,
abi_version : bigint,
fee : bigint,
ttl : bigint,
amount : bigint,
gas : bigint,
gas_price : bigint,
call_data : Uint8Array};
type tx = SignedTx | SpendTx | ContractCreateTx | ContractCallTx;
type decoded_tx =
{tag : otag,
version : Uint8Array,
tx : tx};
/**
* Decode a `tx_Base64` string
*/
function
decode_tx(tx_str : string): decoded_tx {
let base64_stuff : string = tx_str.slice(3); // tx_[...] -> [...]
let stuff : Uint8Array = b64.decode(base64_stuff); // <<Bin/binary, DoubleSha:4>>
let rlp_stuff : Uint8Array = stuff.slice(0, stuff.length - 4); // <<Bin/binary>>
let decoded_datas : Array<rlp.decoded_data> = rlp.decode(rlp_stuff).decoded_data as Array<rlp.decoded_data>; // decoded_data : list(rlp.decoded_data() :: binary() | list(decoded_data()))
// tag, vsn
let tag_bytes : Uint8Array = decoded_datas[0] as Uint8Array; // [tag, vsn, fields] -> tag
let tag : bigint = bytes_to_bigint(tag_bytes); // <<Tag:(byte_size(TagBytes))>> = TagBytes
let vsn : Uint8Array = decoded_datas[1] as Uint8Array;
// tx fields
let tx_fields : Array<rlp.decoded_data> = decoded_datas.slice(2);
let tx : tx = decode_fields(tag, tx_fields);
return {tag: tag as otag, version: vsn, tx: tx};
}
/**
* Decode a transaction given the raw fields
*
* @internal
*/
function
decode_fields(tag: bigint, fields: Array<rlp.decoded_data>): tx {
switch (tag) {
case 11n: return decode_fields_SignedTx(fields);
case 12n: return decode_fields_SpendTx(fields);
case 42n: return decode_fields_ContractCreateTx(fields);
case 43n: return decode_fields_ContractCallTx(fields);
default : throw new Error("invalid object tag: " + tag);
}
console.log('fields: ', fields);
throw new Error("nyi");
}
/**
* Decode a SignedTx
*
* @internal
*/
function
decode_fields_SignedTx(fields: Array<rlp.decoded_data>): SignedTx {
throw new Error('nyi');
}
/**
* Decode a SpendTx
*
* @internal
*/
function
decode_fields_SpendTx(fields: Array<rlp.decoded_data>): SpendTx {
// [<sender> :: id(),
// <recipient> :: id(),
// <amount> :: int(),
// <fee> :: int(),
// <ttl> :: int(),
// <nonce> :: int(),
// <payload> :: binary()]
let sender : id = decode_id(fields[0] as Uint8Array);
let recipient : id = decode_id(fields[1] as Uint8Array);
let amount : bigint = bytes_to_bigint(fields[2] as Uint8Array);
let fee : bigint = bytes_to_bigint(fields[3] as Uint8Array);
let ttl : bigint = bytes_to_bigint(fields[4] as Uint8Array);
let nonce : bigint = bytes_to_bigint(fields[5] as Uint8Array);
let payload : Uint8Array = fields[6] as Uint8Array;
return {sender : sender,
recipient : recipient,
amount : amount,
fee : fee,
ttl : ttl,
nonce : nonce,
payload : payload};
}
function
decode_fields_ContractCreateTx(fields: Array<rlp.decoded_data>): ContractCreateTx {
throw new Error('nyi');
}
function
decode_fields_ContractCallTx(fields: Array<rlp.decoded_data>): ContractCallTx {
throw new Error('nyi');
}
function
decode_id(id: Uint8Array): id {
let idtag : idtag = BigInt(id[0]) as idtag;
return {tag: idtag, hash: id.slice(1)};
}
-23
View File
@@ -1,23 +0,0 @@
/**
* Vanillae Seed Phrase Library
*
* Refs:
* 1. BIP 39: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
*
* @module
*/
/**
* Get a given number of seed bits.
*
* `how_many` must be a multiple of 33.
*
* Ref: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
*/
function
seed_and_check_bits
(how_many : number)
: Uint8Array
{
}
+134 -1
View File
@@ -8,9 +8,15 @@ export {
// basic bytes functions
bytes_eq,
bytes_concat,
bytes_concat_arr,
strong_rand_bytes,
bytes_to_bigint,
bytes_to_bigint_little,
bigint_to_bytes,
bigint_to_bytes_little,
encode_utf8,
bytes_zeros,
bytes_to_hex_str,
// basic bits functions
bits_null,
bits_zeros,
@@ -54,7 +60,7 @@ bytes_eq
/**
* Concatenate two arrays
* Concatenate two byte arrays
*/
function
bytes_concat
@@ -91,6 +97,22 @@ bytes_concat
/**
* Concatenate an arbitrary number of byte arrays
*
* Calling with the empty array as an argument returns an empty byte array
*/
function
bytes_concat_arr
(arrays : Array<Uint8Array>)
: Uint8Array
{
// reduce is what js calls foldl
return arrays.reduce(bytes_concat, new Uint8Array([]));
}
/**
* Cryptographically random bytes
*/
@@ -110,6 +132,8 @@ strong_rand_bytes
* Convert a byte array to a bigint
*
* Equivalent to `binary:decode_unsigned/1` from Erlang
*
* Big endian byte order
*/
function
bytes_to_bigint
@@ -129,11 +153,32 @@ bytes_to_bigint
/**
* Convert a byte array to a bigint
*
* Equivalent to `binary:decode_unsigned/1` from Erlang
*
* Little endian byte order
*/
function
bytes_to_bigint_little
(bytes: Uint8Array)
: bigint
{
// reverse bytes and then call bytes_to_bigint
bytes.reverse();
return bytes_to_bigint(bytes);
}
/**
* Convert a bigint to a byte array
*
* Equivalent to `binary:encode_unsigned/1` from Erlang
*
* Big endian byte order
*
* Requires input to be positive
*/
function
@@ -151,12 +196,100 @@ bigint_to_bytes
q /= 256n;
arr_reverse.push(r);
}
// i wrote the big endian implementation first
// so leaving as is
// but if we wanted to be more l33t we would refactor this as calling
// `bigint_to_bytes_little` and reversing the result
arr_reverse.reverse();
return new Uint8Array(arr_reverse);
}
/**
* Same as `bigint_to_bytes` but result array is reversed (i.e. little endian
* byte order)
*/
function
bigint_to_bytes_little
(q: bigint)
: Uint8Array
{
// copied from big endian implementation
if (q < 0n) {
throw new Error('q < 0n: ' + q);
}
let arr_reverse = [];
while (q > 0n) {
let r = Number(q % 256n);
q /= 256n;
arr_reverse.push(r);
}
// ah: BIG endian notation reverses the array
// we will just not do that
// arr_reverse.reverse();
return new Uint8Array(arr_reverse);
}
/**
* Encode a string in utf8
*/
function
encode_utf8
(str : string)
: Uint8Array
{
return (new TextEncoder()).encode(str);
}
/**
* Given number of zero bytes
*/
function
bytes_zeros
(byte_length: number)
: Uint8Array
{
let result : Uint8Array = new Uint8Array(byte_length);
for (let i0=0; i0<byte_length; i0++) {
result[i0] = 0;
}
return result;
}
/**
* Encode a byte string in hexadecimal
*/
function
bytes_to_hex_str
(bytes: Uint8Array)
: string
{
// make sure each byte is a 2 digit string
function byte_to_hex2(n: number): string {
if (n < 16)
return '0' + n.toString(16);
else
return n.toString(16);
}
let result: string = '';
for (let n of bytes) {
result += byte_to_hex2(n);
}
return result;
}
/**
* Oh no, bitstrings in a language that only has bytestrings
*
+10
View File
@@ -0,0 +1,10 @@
dist
jex_mindist
jex_include
*.beam
*.swp
*.swo
test/testgen/b58_cases_2.eterms
test/testgen/b58_cases_3.eterms
__pycache__
docs/
+16
View File
@@ -0,0 +1,16 @@
ISC License
Copyright (c) 2023 Aeternity Foundation
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.
+4
View File
@@ -0,0 +1,4 @@
# vdk_names
This is a library for taking a bytestring and giving it a human name. There are
4096 names. The use case is giving public keys human names in JR.
+5
View File
@@ -0,0 +1,5 @@
{type, library}.
{realm, local}.
{name, vdk_colors}.
{version, "0.1.0"}.
{deps, []}.
+38
View File
@@ -0,0 +1,38 @@
/**
* Miscellaneous helper functions for RGB/HSV color arithmetic
*
* Reference: https://en.wikipedia.org/wiki/HSL_and_HSV
*
* @module
*/
export {
};
export type {
rgb
}
/**
* standard red-green-blue color representation
*
* attributes `r`, `g` and `b` each are integers ranging within `0..255` inclusive
*/
type rgb
= {r : number,
g : number,
b : number};
/**
* hue-saturation-value color representation
*
- * `h` is an inte
*/
type hsv
= {h : number,
s : number,
v : number};
+16
View File
@@ -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}
+10
View File
@@ -0,0 +1,10 @@
dist
jex_mindist
jex_include
*.beam
*.swp
*.swo
test/testgen/b58_cases_2.eterms
test/testgen/b58_cases_3.eterms
__pycache__
docs/
+16
View File
@@ -0,0 +1,16 @@
ISC License
Copyright (c) 2023 Aeternity Foundation
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.
+4
View File
@@ -0,0 +1,4 @@
# `vdk_control`
Missing control flow routines (`sleep`, `bulrtot`, `eicifebulrtot`) from JS
standard library
+5
View File
@@ -0,0 +1,5 @@
{type, library}.
{realm, local}.
{name, vdk_control}.
{version, "0.1.0"}.
{deps, ["local-vdk_safe-0.1.0"]}.
+61
View File
@@ -0,0 +1,61 @@
/**
* Missing routines
*
* @module
*/
export {
sleep,
bulrtot
}
import * as vdk_safe from './jex_include/local-vdk_safe-0.1.0/dist/vdk_safe.js';
/**
* Hack from stack overflow somewhere to sleep for the given number of ms
*/
async function
sleep
(ms: number)
: Promise<void>
{
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Block until lambda returns true or timeout
*
* `timeout_ms` should be divisible by `interval_ms`
*
* @internal
*/
async function
bulrtot
<ok_t>
(fun : (() => boolean),
result_fun : (() => ok_t),
timeout_ms : number,
timeout_msg : string,
interval_ms : number)
: Promise<vdk_safe.Safe<ok_t, string>>
{
let max_iters : number = Math.floor(timeout_ms / interval_ms);
for (let i = 1;
i <= max_iters;
i++)
{
if (fun()) {
let result = vdk_safe.ok(result_fun());
return result;
}
else
await sleep(interval_ms);
}
let result = vdk_safe.error(timeout_msg);
return result;
}
+16
View File
@@ -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}
-24
View File
@@ -1,24 +0,0 @@
done:
- base64/base58 encoding/decoding
- rlp
today:
- pull apart data
tomorrow:
- basic easy serialization/deserialization
- serialize/deserialize examples
- (maybe) pull in awcp/sidekick
- sidekick: sign/propagate and sign/noprop
later:
- general node querying with parasite (think about autogeneration)
- simple aetto/ae/etc unit converter
- ghetto sophia ide
- general "send money to someone" page
GUI:
- simple testnet explorer
- contract ide
-403
View File
@@ -1,403 +0,0 @@
/**
* Node API constructor/deconstructor
*
* This is similar to serialization/deserialization, but not quite the same
* thing. It converts back and forth between different forms of
* "api-serialized" data.
*
* References:
* 1. https://github.com/aeternity/protocol/blob/master/serializations.md
* 2. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
*
* ## General type rules
*
* ```
* ERLANG TYPE | JS TYPE
* -------------------------------
* integer | bigint
* list | Array
* binary | Uint8Array
* ```
*
* # Example
*
* We start with the string `tx_+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==`.
*
* We can tell by the `tx_` prefix that this represents transaction data of
* some sort. But the rest of the data is totally opaque. The task of this
* module is to "humanize" that `tx_...` string and show what data is contained
* in the rest of it.
*
* The remainder of the string is a base64-encoded bytestring
*
* ```erlang
* 3> io:format("~tw~n", [base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>)]).
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,118,68,68,244,48,85,218,93,85,120,164,154,55,90,250,24,220,161,1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,89,203,191,216,108,88,186,7,3,234,139,205,10,184,167,108,10,134,15,38,245,97,200,0,0,9,135,104,97,105,110,97,110,97,3,182,66,215>>
* ```
*
* That bytestring contains data encoded using Ethereum's RLP codec. Luckily, I
* wrote an RLP decoder. RLP has two types of data: binaries, and
* arbitrary-depth (possibly empty) lists of binaries.
*
* ```erlang
* -type decoded_data() :: binary() | [decoded_data()].
*
* -spec decode(RLP) -> {Data, Rest}
* when RLP :: binary(),
* Data :: decoded_data(),
* Rest :: binary().
* ```
*
* ```erlang
* 2> rlp:decode(base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>)).
* {[<<"\f">>,
* <<1>>,
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,...>>,
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,...>>,
* <<"\n">>,
* <<15,38,245,97,200,0>>,
* <<0>>,
* <<"\t">>,<<"hainana">>],
* <<3,182,66,215>>}
* ```
*
* As expected, we get back the return tuple `{Data, Rest}`. `Rest` is the double-sha256 of the beginning
*
* ```erlang
* 3> X = base64:decode(<<"+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==">>).
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,
* 127,198,64,232,35,118,68,68,244,48,85,218,93,...>>
* 4> SizeX = byte_size(X).
* 94
* 6> <<RLPEncodedData:(SizeX - 4)/binary, Hash/binary>> = X.
* <<248,88,12,1,161,1,201,99,126,70,0,231,171,59,194,230,
* 127,198,64,232,35,118,68,68,244,48,85,218,93,...>>
* 10> <<Check:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, RLPEncodedData)).
* <<3,182,66,215,195,99,112,99,25,7,84,31,151,188,149,81,
* 189,184,82,207,164,68,128,43,11,174,236,59,77,...>>
* 11> Hash.
* <<3,182,66,215>>
* 12> Check.
* <<3,182,66,215>>
* ```
*
* What we're really interested in is `Data`
*
* ```erlang
* 14> {Data, _} = rlp:decode(X).
* {[<<"\f">>,
* <<1>>,
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,...>>,
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,...>>,
* <<"\n">>,
* <<15,38,245,97,200,0>>,
* <<0>>,
* <<"\t">>,<<"hainana">>],
* <<3,182,66,215>>}
* ```
*
* `Data` is a list. The first field `<<"\f">>` is meant to be an integer which
* tells us what type of data this is.
*
* ```erlang
* 16> $\f.
* 12
* ```
*
* If we look at our table
* (https://github.com/aeternity/protocol/blob/master/serializations.md#table-of-object-tags),
* we see that a value of `12` is a spend transaction.
*
* The second field `<<1>>` tells us the "version" of the field orderings,
* which we can ignore for now.
*
* The remaining fields are the fields of a spend transaction (https://github.com/aeternity/protocol/blob/master/serializations.md#spend-transaction)
*
* ```erlang
* [ <sender> :: id() % <<1,201,99,126,...> "=" "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE"
* , <recipient> :: id() % <<1,123,102,230,...> "=" "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv"
* , <amount> :: int() % <<"\n">> "=" 10
* , <fee> :: int() % <<15,38,245,97,200,0>> "=" 16_660_000_000_000
* , <ttl> :: int() % <<0>> "=" 0
* , <nonce> :: int() % <<"\t">> "=" 9
* , <payload> :: binary() % <<"hainana">> "=" "hainana"
* ]
* ```
*
* Our task here is to be able to pull apart the "tx_..." string into its fields.
*
* Converting the binaries to integers is pretty trivial. The only mildly
* annoying thing is the `id` type.
*
* `id`s have two fields: a single-byte prefix which says which type of ID it
* is. In this case, both `id`s have a prefix of `1`, which means they are both
* normal accounts (hence the `ak_` prefix on the "api-encoded" id). The other
* options are oracles (prefix `4`/`ok_`), contracts (prefix `5`/`ct_`), or
* names (prefix `2`/`nm_`)
*
* To "api-encode" the name, we first pick the appropriate prefix based on the
* first byte (in this case `1 -> "ak_"). The remaining 32 bytes are then
* double-SHA'd to get the 4-byte check suffix
*
* ```erlang
* 30> SenderBytes = lists:nth(3, Data).
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,120,164,154,55,...>>
* 31> <<1, SenderAddrBytes/binary>> = SenderBytes.
* <<1,201,99,126,70,0,231,171,59,194,230,127,198,64,232,35,
* 118,68,68,244,48,85,218,93,85,120,164,154,55,...>>
* 32> DoubleSha = fun(Bytes) -> <<Foo:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, Bytes)), Foo end.
* #Fun<erl_eval.44.97283095>
* 33> "ak_" ++ b58:enc(<<SenderAddrBytes/binary, (DoubleSha(SenderAddrBytes))/binary>>).
* "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE"
* 34> RecipBytes = lists:nth(4, Data).
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,7,3,234,139,205,...>>
* 35> <<1, RecipAddrBytes/binary>> = RecipBytes.
* <<1,123,102,230,195,5,7,8,54,228,233,121,32,82,61,195,234,
* 89,203,191,216,108,88,186,7,3,234,139,205,...>>
* 36> "ak_" ++ b58:enc(<<RecipAddrBytes/binary, (DoubleSha(RecipAddrBytes))/binary>>).
* "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv"
* ```
*
* ```js
* > anth.deconstruct("tx_+FgMAaEByWN+RgDnqzvC5n/GQOgjdkRE9DBV2l1VeKSaN1r6GNyhAXtm5sMFBwg25Ol5IFI9w+pZy7/YbFi6BwPqi80KuKdsCoYPJvVhyAAACYdoYWluYW5hA7ZC1w==")
* {tag : 'SpendTx',
* version : 1n,
* fields : {sender : "ak_2XhCkjzTwcq1coXSSzHJoMZkUzTwnjH88zmPGkkowUsFNTo9UE",
* recipient : "ak_wM8yFU8eSETXU7VSN48HMDmevGoCMiuveQZgkPuRn1nTiRqyv",
* amount : 10n,
* fee : 16660000000000n,
* ttl : 0n,
* nonce : 9n,
* payload : Uint8Array([104, 97, 105, 110, 97, 110, 97])}}
* ```
*
* @module
*/
export {
// types
tx_str,
deconstructed_tx,
// functions
deconstruct_tx
};
import * as b64 from './b64.js'
import * as bin from './bin.js'
import * as rlp from './rlp.js'
/**
* Alias type for a `tx_...` string
*/
type tx_str = string;
/**
* Alias type for a `sg_...` string
*/
type sg_str = string;
/**
* types of decoded tx we currently support
* @internal
*/
type tx_type_str
= 'SignedTx'
| 'SpendTx'
| 'ContractCreateTx'
| 'ContractCallTx';
/**
* Results of deconstruct_tx
*/
type deconstructed_tx
= {type : 'SignedTx',
version : bigint,
fields : fields_SignedTx}
| {type : 'SpendTx',
version : bigint,
fields : fields_SpendTx}
| {type : 'ContractCreateTx',
version : bigint,
fields : fields_ContractCreateTx}
| {type : 'ContractCallTx'
version : bigint,
fields : fields_ContractCallTx};
/**
* Convenient type alias
*
* @internal
*/
type rlpdata = rlp.decoded_data;
/**
* Fields types
*/
type fields
= fields_SignedTx
| fields_SpendTx
| fields_ContractCreateTx
| fields_ContractCallTx;
type fields_SignedTx
= {signatures : Array<sg_str>,
transaction : tx_str};
type fields_SpendTx
= {sender : string,
recipient : string,
amount : bigint,
fee : bigint,
ttl : bigint,
nonce : bigint,
payload : Uint8Array};
/**
* Deconstruct a Tx
*/
function
deconstruct_tx
(tx_str: tx_str)
: deconstructed_tx
{
let b64_str : string = tx_str.slice(3); // tx_[...] -> [...]
let tx_rlp_encoded : Uint8Array = b64.decode(b64_str); // [...] -> bytes
let tx_data : Array<rlpdata> = shasha_rlp_decode_list(tx_rlp_encoded); // decode data and check the double-sha thing
let tx_type : bigint = bin.bytes_to_bigint(tx_data[0]); // get a bigint
let tts : tx_type_str = tx_type_str(tx_type);
let tx_version : bigint = bin.bytes_to_bigint(tx_data[1]);
let tx_fields : fields = deconstruct_fields(tts, tx_version, tx_data.slice(2));
return {type : tts,
version : tx_version,
fields : tx_fields};
}
/**
* Data that's "api-encoded" goes through the following stages:
*
* 1. data structure -> rlp decode data (arbitrary-depth [possibly 0] list of bytestrings)
* 2. rlp decode data -> bytestring
* 3. bytestring -> <<Bytestring/binary, Hash:4/binary>>
* 4. HashedBytestring -> base64/base58 string encoding
* 5. Add string prefix
*
* This function undoes step 3 and step 2, returns back the rlp decode data
*
* FIXME: Does not check double-sha (yet); need to figure out way to handle hash failures
* FIXME: No good way to handle failure cases
*
* @internal
*/
function
shasha_rlp_decode_list
(hashed_bs : Uint8Array)
: Array<rlpdata>
{
let len = hashed_bs.length;
let bytes = hashed_bs.slice(0, len - 4);
let {decoded_data} = rlp.decode(bytes);
return (decoded_data as Array<rlpdata>);
}
/**
* Convert an object tag that's a type of transaction to the type string
*
* See: https://github.com/aeternity/protocol/blob/master/serializations.md#table-of-object-tags
*
* @internal
*/
function
tx_type_str
(tx_type_int : bigint)
: tx_type_str
{
switch (tx_type_int)
{
case 11n: return 'SignedTx';
case 12n: return 'SpendTx';
case 42n: return 'ContractCreateTx';
case 43n: return 'ContractCallTx';
default: throw new Error('invalid transaction type: ' + tx_type_int);
}
}
/**
* Given an array of data decoded from RLP, convert it to the fields, as
* appropriate as given by the tx type string and the version
*/
function
deconstruct_fields
(tx_type_str : tx_type_str,
tx_version : bigint,
tx_rawfields : Array<rlpdata>)
: fields
{
switch (tx_type_str)
{
// case 'SignedTx' : return deconstruct_fields_SignedTx(tx_rawfields);
case 'SpendTx' : return deconstruct_fields_SpendTx(tx_rawfields);
// case 'ContractCreateTx' : return deconstruct_fields_ContractCreateTx(tx_rawfields);
// case 'ContractCallTx' : return deconstruct_fields_ContractCallTx(tx_rawfields);
default : throw new Error('invalid tx type str: ' + tx_type_str);
}
}
// TODO: do all this in Erlang
function
deconstruct_fields_SpendTx
(fields: Array<rlpdata>)
: fields_SpendTx
{
let sender_bytes = fields[0];
let recip_bytes = fields[1];
let amount_bytes = fields[2];
let fee_bytes = fields[3];
let ttl_bytes = fields[4];
let nonce_bytes = fields[5];
let payload_bytes = fields[6];
return {sender : encode_id(sender_bytes),
recipient : encode_id(sender_bytes),
amount : bin.bytes_to_bigint(amount_bytes),
fee : bin.bytes_to_bigint(fee_bytes),
ttl : bin.bytes_to_bigint(ttl_bytes),
nonce : bin.bytes_to_bigint(nonce_bytes),
payload : bin.bytes_to_bigint(payload_bytes)};
}
/**
* Convert a binary account/name/etc binary id into the appropriate type of string
*
* @internal
*/
function
encode_id
(id: Uint8Array)
: string
{
throw new Error('nyi');
}
/*
FIXME:
1. work out all this in Erlang to clear conceptual goo
2. think about how i want type safety etc to work
3. think about a language to assert that the data has the correct shape to it
4. get some examples working in Erlang
5. convert erlang code back to ts
*/
-112
View File
@@ -1,112 +0,0 @@
/**
* Concatenate two bitstrings
*/
function
bits_concat
(bits1 : bits,
bits2 : bits)
: bits
{
let result_bit_length : number = bits1.bit_length + bits2.bit_length;
let bytes1 : Uint8Array = bits1.bytes;
let bytes2 : Uint8Array = bits2.bytes;
// using zeros here because of our xor trick in a minute
let result_bits : bits = bits_zeros(result_bit_length);
let result_bytes : Uint8Array = result_bits.bytes;
// alright so
// we can start by copying the first bytes into result bytes
for (let bytes1_idx0 = 0;
bytes1_idx0 < bytes1.length;
bytes1_idx0++)
{
result_bytes[bytes1_idx0] = bytes1[bytes1_idx0];
}
// next
// we need to calculate the left-shift offset
// this will be 8 - (bytes1.bit_length % 8)
let num_trailing_zeros_in_first_array : number = 8 - (bits1.bit_length % 8);
// so
// bytes1: ABCD_EF00
// bytes2: GH12_3000
// result: ABCD_EFGH 1230_0000
// ah ok, so we need to for each byte in the second array
// take the first however many bits, xor it with the existing byte
// then take the last however many bits and place them into the next byte
// this is super confusing but
// ABCD_EF00
// GH12_3456
// operation:
// ABCD_EF00
// xor 0000_00GH
// = ABCD_EFGH 1234_5600
//
// then on the next iteration
// 1234_5600
// abcd_efgh
// ->
// 1234_56ab cdef_gh00
//
// ah so there's a pattern
// however many trailing 0s there are in the first array
// say there's 2
// we take the first 2 bits of the upcoming byte
// xor that against the current byte
// take the last 6 bits of the upcoming byte
// set the next byte to that
//
// have to think about edge behavior
// this is ripe for off-by-1 errors
// but i think the general idea is right
//
// so we start the iteration
// on the last byte of the first array
let last_byte_of_first_array_idx0 : number = bytes1.length - 1;
// and we end
// on the second-to-last-byte of the result array
let second_to_last_byte_of_result_array_idx0 : number = result_bytes.length - 2;
// the reason we do that is because we're doing this is because we are
// going along, xoring against the current byte and then setting the next
// byte
//
// ok so
for (let this_result_byte_idx0 = last_byte_of_first_array_idx0;
this_result_byte_idx0 <= second_to_last_byte_of_result_array_idx0;
this_result_byte_idx0++)
{
let this_result_byte : number = result_bytes[this_result_byte_idx0];
// ok here we need to fish out the relevant byte of the second array
// gaaah
// so this will be 0 at the start of the loop
let relevant_byte_of_second_array_idx0 : number = this_result_byte_idx0 - last_byte_of_first_array_idx0;
let relevant_byte_of_second_array : number = bytes2[relevant_byte_of_second_array_idx0];
// ok so let's fish out the leading digits
// the number of leading digits is the number of trailing 0s in the first array
let num_leading_digits : number = num_trailing_zeros_in_first_array;
let num_trailing_digits : number = 8 - num_leading_digits;
// suppose there are 2 leading digits and 6 trailing digits
// ABCD_EFGH
// leading digits are
// ABCD_EFGH >> 6 = 0000_00AB
// trailing digits are
// (ABCD_EFGH << 2) % 255 = CDEF_GH00
let leading_digits : number = relevant_byte_of_second_array >> num_trailing_digits;
let trailing_digits : number = (relevant_byte_of_second_array << num_leading_digits) % 255;
// xor the current byte against the leading digits
let new_this_result_byte : number = this_result_byte ^ leading_digits;
result_bytes[this_result_byte_idx0] = new_this_result_byte;
// set the next byte to the trailing digits
result_bytes[this_result_byte_idx0 + 1] = trailing_digits;
}
// i think we're done
return {bit_length : result_bit_length,
bytes : result_bytes};
}

Some files were not shown because too many files have changed in this diff Show More