Merge branch 'pharpend/develop' into 'master'
bring master up to my develop branch so jr on master builds See merge request ioecs/Vanillae!11
@@ -3,3 +3,5 @@
|
||||
erl_crash.dump
|
||||
*.beam
|
||||
__pycache__
|
||||
jex_include
|
||||
jex_mindist
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
@@ -1 +1,3 @@
|
||||
dist
|
||||
src/jex_include
|
||||
docs
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
This is an HTML page like any other. A popup script is simply a script
|
||||
that runs in the context of the popup page. It has the same basic array of
|
||||
permissions as the background script.
|
||||
|
||||
|
||||
```
|
||||
PAGE SCRIPT <-> CONTENT SCRIPT <-> BACKGROUND SCRIPT <-> POPUP SCRIPT
|
||||
Aegora.jp <-> src/content.ts <-> src/background.ts <-> src/popup.ts
|
||||
```
|
||||
|
||||
In JavaScript, the standard way of communicating between mutually opaque
|
||||
contexts A and B (for instance, between a page script and a wallet) is to write
|
||||
a class in each context that manages state, which listens for events, and then
|
||||
does things in response to those events. `ClassA` can dispatch events which are
|
||||
visible to the event listener in `ClassB` and vice versa. This is the natural
|
||||
result of trying to shoehorn concurrency into an object-oriented language.
|
||||
|
||||
Sidekick and JR internally work by mimicking Erlang's interprocess
|
||||
communication (IPC) idiom.
|
||||
|
||||
Erlang is a concurrency-first language, and thus has a much saner and more
|
||||
well-thought-out approach to this problem. There is a build-in function called
|
||||
`send(PID, Message)`. The message can be any Erlang term. In the receiving
|
||||
process, there is primitive syntax for receiving messages:
|
||||
|
||||
```erl
|
||||
receive
|
||||
Pattern1 ->
|
||||
do_thing_1();
|
||||
Pattern2 ->
|
||||
do_thing_2();
|
||||
_ ->
|
||||
handle_arbitrary_message()
|
||||
after NMilliseconds ->
|
||||
do_timeout_thing()
|
||||
end.
|
||||
```
|
||||
|
||||
Each process has a mailbox. When a process enters a `receive`, it sits there
|
||||
and scans its mailbox to find a message that matches one of the patterns. If
|
||||
it finds a message that matches `Pattern1`, it executes the function
|
||||
`do_thing_1()`, and so on. The `after` part is optional, and it exists as a
|
||||
backstop so the process doesn't get stuck in an infinite `receive` loop.
|
||||
|
||||
Here is an example of two processes just exchanging ping and pong:
|
||||
|
||||
```erl
|
||||
#!/usr/bin/env escript
|
||||
|
||||
-mode(compile).
|
||||
|
||||
%% this is the code that is run by the pinger process
|
||||
pinger() ->
|
||||
%% the pinger just sits and waits until a pong message appears in the
|
||||
%% mailbox and sends back ping
|
||||
receive
|
||||
%% wait to receive a pong
|
||||
%% only accept this pattern
|
||||
{SenderPID, pong} ->
|
||||
%% self() gets the current process pid
|
||||
%% print out the pong to the console
|
||||
ok = io:format("~p received pong from ~p~n", [self(), SenderPID]),
|
||||
%% wait for 1 second
|
||||
ok = timer:sleep(1000),
|
||||
%% send a ping back
|
||||
SenderPID ! {self(), ping},
|
||||
%% start over
|
||||
pinger()
|
||||
end.
|
||||
|
||||
|
||||
%% this is the code that is run by the ponger process
|
||||
%% identical to the pinger except the words "ping" and "pong" have been swapped
|
||||
ponger() ->
|
||||
%% the ponger just sits and waits until a ping message appears in the
|
||||
%% mailbox and sends back pong
|
||||
receive
|
||||
%% wait to receive a ping
|
||||
%% only accept this pattern
|
||||
{SenderPID, ping} ->
|
||||
%% self gets the current process's pid
|
||||
%% print out the ping to the console
|
||||
ok = io:format("~p received ping from ~p~n", [self(), SenderPID]),
|
||||
%% wait for 1 second
|
||||
ok = timer:sleep(1000),
|
||||
%% send a pong back
|
||||
SenderPID ! {self(), pong},
|
||||
%% start over
|
||||
ponger()
|
||||
end.
|
||||
|
||||
|
||||
%% this is the main process
|
||||
main([]) ->
|
||||
%% start up pingers and pongers
|
||||
%% these just sit and wait until they are told to do something
|
||||
PingerPID = spawn(fun pinger/0),
|
||||
PongerPID = spawn(fun ponger/0),
|
||||
%% ping the ponger, but lie about who is doing it
|
||||
PongerPID ! {PingerPID, ping},
|
||||
%% this is a hack so that main doesn't exit and therefore the demo works
|
||||
receive
|
||||
_ ->
|
||||
ok = io:format("bye~n")
|
||||
end.
|
||||
```
|
||||
|
||||
```
|
||||
[~] % escript pingpong.erl
|
||||
<0.81.0> received ping from <0.80.0>
|
||||
<0.80.0> received pong from <0.81.0>
|
||||
<0.81.0> received ping from <0.80.0>
|
||||
<0.80.0> received pong from <0.81.0>
|
||||
<0.81.0> received ping from <0.80.0>
|
||||
<0.80.0> received pong from <0.81.0>
|
||||
<0.81.0> received ping from <0.80.0>
|
||||
<0.80.0> received pong from <0.81.0>
|
||||
^C
|
||||
```
|
||||
|
||||
JR basically thinks of each of the 3 different components of an extension as
|
||||
Erlang processes, and has some band-aid code to kind of sort of mimick Erlang's
|
||||
IPC idiom. Frankly, Erlang's IPC idiom doesn't quite fit into TS/JS, however
|
||||
even the shoehorned band-aid Erlang idiom is significantly simpler and more
|
||||
pleasant to work with than the OOPy event idiom.
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -2,4 +2,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"]}.
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
{"manifest_version" : 2,
|
||||
{"manifest_version" : 3,
|
||||
"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"},
|
||||
"icons" : {"48" : "art/jrx-icon-48.png",
|
||||
"96" : "art/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"]}
|
||||
"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" : ["*://*/*"]}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'); };
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
/**
|
||||
* 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",
|
||||
let detect_msg = {type : "to_aepp",
|
||||
data : {jsonrpc : "2.0",
|
||||
method : "connection.announcePresence",
|
||||
params : {id : "jr",
|
||||
name : "JR",
|
||||
networkId : "ae_uat",
|
||||
origin : "foobar",
|
||||
type : "extension"};
|
||||
}
|
||||
|
||||
|
||||
|
||||
origin : browser.runtime.getURL('/'),
|
||||
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()
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
{"compilerOptions" : {"target" : "es2022",
|
||||
"module" : "es2022",
|
||||
"strict" : true,
|
||||
"esModuleInterop" : true,
|
||||
"skipLibCheck" : true,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{type, library}.
|
||||
{realm, local}.
|
||||
{name, awcp}.
|
||||
{version, "0.2.2"}.
|
||||
{version, "0.2.3"}.
|
||||
{deps, []}.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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+
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{type, external}.
|
||||
{realm, local}.
|
||||
{name, blakejs}.
|
||||
{version, "1.2.1"}.
|
||||
{deps, []}.
|
||||
@@ -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
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{type, library}.
|
||||
{realm, local}.
|
||||
{name, parasite}.
|
||||
{version, "0.2.2"}.
|
||||
{version, "0.2.3"}.
|
||||
{deps, []}.
|
||||
|
||||
@@ -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)
|
||||
|
||||
[
|
||||
](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+
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# `vdk_aecrypt`
|
||||
|
||||
Aeternity-specific cryptography helper functions
|
||||
|
||||
## building
|
||||
|
||||
requires [jex](../../utils/jex/)
|
||||
|
||||
```
|
||||
jex install
|
||||
```
|
||||
@@ -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"]}.
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
*
|
||||
@@ -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}
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"]}.
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# `vdk_base58`
|
||||
|
||||
Base58 encode/decode routines. See <https://zxq9.com/archives/2688>
|
||||
|
||||
## building
|
||||
|
||||
requires [jex](../../utils/jex/)
|
||||
|
||||
```
|
||||
jex install
|
||||
```
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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)};
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# `vdk_base64`
|
||||
|
||||
Base64 encode/decode routines. See <https://zxq9.com/archives/2688>
|
||||
|
||||
## building
|
||||
|
||||
Requires [jex](../../utils/jex)
|
||||
|
||||
```
|
||||
jex install
|
||||
```
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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)};
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# `vdk_binary`
|
||||
|
||||
Basic binary utility functions
|
||||
|
||||
## building
|
||||
|
||||
Requires [jex](../../utils/jex/)
|
||||
|
||||
```
|
||||
jex install
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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)};
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
{type, library}.
|
||||
{realm, local}.
|
||||
{name, vdk_colors}.
|
||||
{version, "0.1.0"}.
|
||||
{deps, []}.
|
||||
@@ -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};
|
||||
@@ -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}
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# `vdk_control`
|
||||
|
||||
Missing control flow routines (`sleep`, `bulrtot`, `eicifebulrtot`) from JS
|
||||
standard library
|
||||
@@ -0,0 +1,5 @@
|
||||
{type, library}.
|
||||
{realm, local}.
|
||||
{name, vdk_control}.
|
||||
{version, "0.1.0"}.
|
||||
{deps, ["local-vdk_safe-0.1.0"]}.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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};
|
||||
}
|
||||
|
||||