├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .flowconfig
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── pr-checks.yml
│ └── pr-rebase.yml
├── .gitignore
├── .husky
└── pre-commit
├── .mocharc.json
├── .prettierrc.json
├── .travis.yml
├── .yarnrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
├── build.gradle
├── format-java.sh
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── cpp
│ ├── CMakeLists.txt
│ ├── edge-core-jni.c
│ └── scrypt
│ │ ├── crypto_scrypt.c
│ │ ├── crypto_scrypt.h
│ │ ├── readme.md
│ │ ├── sha256.c
│ │ ├── sha256.h
│ │ └── sysendian.h
│ └── java
│ └── app
│ └── edge
│ └── reactnative
│ └── core
│ ├── Disklet.java
│ ├── EdgeCorePackage.java
│ ├── EdgeCoreWebView.java
│ ├── EdgeCoreWebViewManager.java
│ ├── EdgeNative.java
│ ├── PendingCall.java
│ └── StreamStringReader.java
├── docs
└── key-formats.md
├── edge-core-js.podspec
├── flow-typed
└── react-native-libs.js
├── ios
├── Disklet.swift
├── EdgeCoreWebView.swift
├── EdgeCoreWebViewManager.m
├── EdgeCoreWebViewManager.swift
├── EdgeNative.swift
├── PendingCall.swift
├── edge-core-js-Bridging-Header.h
└── edge-core-js.xcodeproj
│ └── project.pbxproj
├── package.json
├── rollup.config.js
├── scripts
└── make-types.ts
├── src
├── browser.ts
├── client-side.ts
├── core
│ ├── account
│ │ ├── account-api.ts
│ │ ├── account-cleaners.ts
│ │ ├── account-files.ts
│ │ ├── account-init.ts
│ │ ├── account-pixie.ts
│ │ ├── account-reducer.ts
│ │ ├── account-types.ts
│ │ ├── custom-tokens.ts
│ │ ├── data-store-api.ts
│ │ ├── lobby-api.ts
│ │ ├── memory-wallet.ts
│ │ └── plugin-api.ts
│ ├── actions.ts
│ ├── context
│ │ ├── client-file.ts
│ │ ├── context-api.ts
│ │ ├── context-pixie.ts
│ │ ├── info-cache-file.ts
│ │ └── internal-api.ts
│ ├── core.ts
│ ├── currency
│ │ ├── change-server-connection.ts
│ │ ├── change-server-protocol.ts
│ │ ├── currency-pixie.ts
│ │ ├── currency-reducer.ts
│ │ ├── currency-selectors.ts
│ │ └── wallet
│ │ │ ├── currency-wallet-api.ts
│ │ │ ├── currency-wallet-callbacks.ts
│ │ │ ├── currency-wallet-cleaners.ts
│ │ │ ├── currency-wallet-export.ts
│ │ │ ├── currency-wallet-files.ts
│ │ │ ├── currency-wallet-pixie.ts
│ │ │ ├── currency-wallet-reducer.ts
│ │ │ ├── enabled-tokens.ts
│ │ │ ├── max-spend.ts
│ │ │ ├── metadata.ts
│ │ │ └── upgrade-memos.ts
│ ├── fake
│ │ ├── fake-db.ts
│ │ ├── fake-io.ts
│ │ ├── fake-responses.ts
│ │ ├── fake-server.ts
│ │ └── fake-world.ts
│ ├── log
│ │ └── log.ts
│ ├── login
│ │ ├── airbitz-stashes.ts
│ │ ├── create.ts
│ │ ├── edge.ts
│ │ ├── keys.ts
│ │ ├── lobby.ts
│ │ ├── login-delete.ts
│ │ ├── login-fetch.ts
│ │ ├── login-messages.ts
│ │ ├── login-reducer.ts
│ │ ├── login-secret.ts
│ │ ├── login-selectors.ts
│ │ ├── login-stash.ts
│ │ ├── login-types.ts
│ │ ├── login-username.ts
│ │ ├── login.ts
│ │ ├── otp.ts
│ │ ├── password.ts
│ │ ├── pin2.ts
│ │ ├── recovery2.ts
│ │ ├── splitting.ts
│ │ ├── storage-keys.ts
│ │ └── vouchers.ts
│ ├── plugins
│ │ ├── plugins-actions.ts
│ │ ├── plugins-reducer.ts
│ │ └── plugins-selectors.ts
│ ├── root-pixie.ts
│ ├── root-reducer.ts
│ ├── root.ts
│ ├── scrypt
│ │ ├── scrypt-pixie.ts
│ │ └── scrypt-selectors.ts
│ ├── storage
│ │ ├── encrypt-disklet.ts
│ │ ├── repo.ts
│ │ ├── storage-actions.ts
│ │ ├── storage-api.ts
│ │ ├── storage-reducer.ts
│ │ └── storage-selectors.ts
│ └── swap
│ │ └── swap-api.ts
├── globals.d.ts
├── index.ts
├── io
│ ├── browser
│ │ ├── browser-io.ts
│ │ └── fetch-cors-proxy.ts
│ ├── hidden-properties.ts
│ ├── node
│ │ └── node-io.ts
│ └── react-native
│ │ ├── native-bridge.ts
│ │ ├── polyfills.ts
│ │ ├── react-native-types.ts
│ │ ├── react-native-webview.tsx
│ │ ├── react-native-worker.ts
│ │ └── yaob-callbacks.ts
├── libs.d.ts
├── react-native.tsx
├── types
│ ├── error.ts
│ ├── exports.ts
│ ├── fake-types.ts
│ ├── server-cleaners.ts
│ ├── server-types.ts
│ ├── type-helpers.ts
│ └── types.ts
└── util
│ ├── asMap.ts
│ ├── asyncWaterfall.ts
│ ├── compare.ts
│ ├── crypto
│ ├── crypto.ts
│ ├── hashes.ts
│ ├── hotp.ts
│ ├── scrypt.ts
│ └── verify.ts
│ ├── encoding.ts
│ ├── file-helpers.ts
│ ├── json-rpc.ts
│ ├── match-json.ts
│ ├── periodic-task.ts
│ ├── promise.ts
│ ├── shuffle.ts
│ ├── snooze.ts
│ ├── updateQueue.ts
│ ├── util.ts
│ └── validateServer.ts
├── test
├── core
│ ├── account
│ │ ├── account.test.ts
│ │ └── data-store.test.ts
│ ├── context
│ │ └── context.test.ts
│ ├── currency
│ │ ├── confirmations.test.ts
│ │ ├── currency.test.ts
│ │ └── wallet
│ │ │ └── currency-wallet.test.ts
│ ├── login
│ │ ├── airbitz.test.ts
│ │ ├── edge.test.ts
│ │ ├── keys.test.ts
│ │ ├── lobby.test.ts
│ │ ├── login.test.ts
│ │ ├── otp.test.ts
│ │ └── splitting.test.ts
│ ├── plugins
│ │ └── plugins.test.ts
│ ├── scrypt
│ │ └── snrp.test.ts
│ ├── storage
│ │ └── repo.test.ts
│ └── swap.test.ts
├── expect-rejection.ts
├── fake
│ ├── fake-broken-engine.ts
│ ├── fake-currency-plugin.ts
│ ├── fake-plugins.ts
│ ├── fake-swap-plugin.ts
│ ├── fake-transactions.ts
│ └── fake-user.ts
├── setup.test.ts
└── util
│ ├── asMap.test.ts
│ ├── compare.test.ts
│ ├── crypto
│ ├── crypto.test.ts
│ ├── hashes.test.ts
│ ├── hotp.test.ts
│ └── scrypt.test.ts
│ ├── encoding.test.ts
│ ├── promise.test.ts
│ └── validateServer.test.ts
├── tsconfig.json
├── types.d.ts
├── webpack.config.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /android/src/main/assets/
2 | /lib/
3 | /types.js
4 | /types.js.flow
5 | /types.mjs
6 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "standard-kit/prettier",
4 | "standard-kit/prettier/node",
5 | "standard-kit/prettier/jsx",
6 | "standard-kit/prettier/flow",
7 | "standard-kit/prettier/typescript"
8 | ],
9 | "parserOptions": {
10 | "project": "tsconfig.json"
11 | },
12 | "plugins": [
13 | "simple-import-sort"
14 | ],
15 | "rules": {
16 | "@typescript-eslint/default-param-last": "off",
17 | "@typescript-eslint/no-invalid-void-type": "off",
18 | "@typescript-eslint/promise-function-async": "off",
19 | "simple-import-sort/imports": "error"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 | .*/node_modules/resolve/.*
3 | .*/node_modules/webpack-cli/.*
4 |
5 | [include]
6 |
7 | [libs]
8 |
9 | [lints]
10 | sketchy-null=error
11 |
12 | [options]
13 | esproposal.optional_chaining=enable
14 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### CHANGELOG
2 |
3 | Does this branch warrant an entry to the CHANGELOG?
4 |
5 | - [ ] Yes
6 | - [ ] No
7 |
8 | ### Dependencies
9 |
10 | none
11 |
12 | ### Description
13 |
14 | none
15 |
--------------------------------------------------------------------------------
/.github/workflows/pr-checks.yml:
--------------------------------------------------------------------------------
1 | name: PR Checks
2 | on: [pull_request]
3 | jobs:
4 | block-wip-pr:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2.0.0
8 | - name: Block WIP PR
9 | uses: samholmes/block-wip-pr-action@v1.2.0
10 |
--------------------------------------------------------------------------------
/.github/workflows/pr-rebase.yml:
--------------------------------------------------------------------------------
1 | name: PR Rebase
2 | on:
3 | issue_comment:
4 | types: [created]
5 | jobs:
6 | rebase:
7 | name: Rebase
8 | if: >-
9 | github.event.issue.pull_request != '' &&
10 | (
11 | contains(github.event.comment.body, '/autosquash') ||
12 | contains(github.event.comment.body, '/fixup') ||
13 | contains(github.event.comment.body, '/rebase')
14 | )
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout the latest code
18 | uses: actions/checkout@v3
19 | with:
20 | token: ${{ secrets.GITHUB_TOKEN }}
21 | fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
22 | - name: Automatic Rebase
23 | uses: EdgeApp/rebase@changelog-resolver
24 | with:
25 | autosquash: ${{ true }}
26 | changelogResolver: ${{ true }}
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build output:
2 | /android/*.jar
3 | /android/src/main/assets/
4 | /lib/
5 | /types.js
6 | /types.js.flow
7 | /types.mjs
8 |
9 | # Package managers:
10 | node_modules/
11 | npm-debug.log
12 | package-lock.json
13 | yarn-error.log
14 | stats.json
15 |
16 | # Editors:
17 | .DS_Store
18 | .idea/
19 | .vscode/
20 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run precommit
5 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": "sucrase/register",
3 | "spec": "test/**/*.test.ts"
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "semi": false,
4 | "singleQuote": true,
5 | "trailingComma": "none"
6 | }
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 14
4 | script:
5 | - 'yarn verify'
6 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | --ignore-scripts true
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Airbitz Inc
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms are permitted provided that
5 | the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 | 2. Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 | 3. Redistribution or use of modified source code requires the express written
13 | permission of Airbitz Inc.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
26 | The views and conclusions contained in the software and documentation are those
27 | of the authors and should not be interpreted as representing official policies,
28 | either expressed or implied, of the Airbitz Project.
29 |
30 |
31 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 |
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:3.6.0'
9 | }
10 | }
11 |
12 | apply plugin: 'com.android.library'
13 |
14 | def safeExtGet(prop, fallback) {
15 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
16 | }
17 |
18 | def DEFAULT_COMPILE_SDK_VERSION = 28
19 | def DEFAULT_BUILD_TOOLS_VERSION = '28.0.2'
20 | def DEFAULT_MIN_SDK_VERSION = 19
21 | def DEFAULT_TARGET_SDK_VERSION = 27
22 | def DEFAULT_WEBKIT_VERSION = '1.4.0'
23 |
24 | android {
25 | namespace "app.edge.reactnative.core"
26 | compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
27 | buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
28 |
29 | defaultConfig {
30 | minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION)
31 | targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
32 | versionCode 1
33 | versionName '1.0'
34 | }
35 | lintOptions {
36 | abortOnError false
37 | }
38 | externalNativeBuild {
39 | cmake {
40 | path "src/main/cpp/CMakeLists.txt"
41 | }
42 | }
43 | }
44 |
45 | repositories {
46 | }
47 |
48 | def webkit_version = safeExtGet('webkitVersion', DEFAULT_WEBKIT_VERSION)
49 |
50 | dependencies {
51 | implementation "androidx.webkit:webkit:$webkit_version"
52 | implementation 'com.facebook.react:react-native:+'
53 | }
54 |
--------------------------------------------------------------------------------
/android/format-java.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | here=$(dirname $0)
4 |
5 | formatJava() {
6 | tool="google-java-format-1.7-all-deps.jar"
7 | url="https://github.com/google/google-java-format/releases/download/google-java-format-1.7/$tool"
8 | jar="$here/$tool"
9 |
10 | # If the tool is missing, grab it from GitHub:
11 | if [ ! -e "$jar" ]; then
12 | curl -L -o "$jar" "$url"
13 | fi
14 |
15 | echo $(find "$here/src" -name "*.java")
16 | java -jar "$jar" --replace $(find "$here/src" -name "*.java")
17 | }
18 |
19 | formatJava
20 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/android/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.4.1)
2 | project("edge-core-js")
3 |
4 | add_compile_options(-fvisibility=hidden -w)
5 |
6 | include_directories("scrypt/")
7 |
8 | add_library(
9 | edge-core-jni
10 | SHARED
11 | edge-core-jni.c
12 | scrypt/crypto_scrypt.c
13 | scrypt/sha256.c
14 | )
15 |
--------------------------------------------------------------------------------
/android/src/main/cpp/edge-core-jni.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "scrypt/crypto_scrypt.h"
4 |
5 | JNIEXPORT jbyteArray JNICALL
6 | Java_app_edge_reactnative_core_EdgeNative_scrypt(
7 | JNIEnv *env,
8 | jobject self,
9 | jbyteArray data,
10 | jbyteArray salt,
11 | jint n,
12 | jint r,
13 | jint p,
14 | jint dklen
15 | ) {
16 | jsize dataLength = (*env)->GetArrayLength(env, data);
17 | jsize saltLength = (*env)->GetArrayLength(env, salt);
18 | jbyte *pData = alloca(dataLength * sizeof(jbyte));
19 | jbyte *pSalt = alloca(saltLength * sizeof(jbyte));
20 | jbyte *pOut = alloca(dklen * sizeof(jbyte));
21 | jbyteArray out = (*env)->NewByteArray(env, dklen);
22 | if (!out) return NULL;
23 |
24 | (*env)->GetByteArrayRegion(env, data, 0, dataLength, pData);
25 | (*env)->GetByteArrayRegion(env, salt, 0, saltLength, pSalt);
26 |
27 | if (crypto_scrypt(
28 | pData, dataLength,
29 | pSalt, saltLength,
30 | n, r, p,
31 | pOut, dklen
32 | )) return NULL;
33 |
34 | (*env)->SetByteArrayRegion(env, out, 0, dklen, pOut);
35 | return out;
36 | }
37 |
--------------------------------------------------------------------------------
/android/src/main/cpp/scrypt/crypto_scrypt.h:
--------------------------------------------------------------------------------
1 | /*-
2 | * Copyright 2009 Colin Percival
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions
7 | * are met:
8 | * 1. Redistributions of source code must retain the above copyright
9 | * notice, this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright
11 | * notice, this list of conditions and the following disclaimer in the
12 | * documentation and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24 | * SUCH DAMAGE.
25 | *
26 | * This file was originally written by Colin Percival as part of the Tarsnap
27 | * online backup system.
28 | */
29 | #ifndef _CRYPTO_SCRYPT_H_
30 | #define _CRYPTO_SCRYPT_H_
31 |
32 | #include
33 |
34 | /**
35 | * crypto_scrypt(passwd, passwdlen, salt, saltlen, N, r, p, buf, buflen):
36 | * Compute scrypt(passwd[0 .. passwdlen - 1], salt[0 .. saltlen - 1], N, r,
37 | * p, buflen) and write the result into buf. The parameters r, p, and buflen
38 | * must satisfy r * p < 2^30 and buflen <= (2^32 - 1) * 32. The parameter N
39 | * must be a power of 2 greater than 1.
40 | *
41 | * Return 0 on success; or -1 on error.
42 | */
43 | int crypto_scrypt(const uint8_t *, size_t, const uint8_t *, size_t, uint64_t,
44 | uint32_t, uint32_t, uint8_t *, size_t);
45 |
46 | #endif /* !_CRYPTO_SCRYPT_H_ */
47 |
--------------------------------------------------------------------------------
/android/src/main/cpp/scrypt/readme.md:
--------------------------------------------------------------------------------
1 | # scrypt
2 |
3 | This source code has been extracted from the
4 | [scrypt command-line utility](https://www.tarsnap.com/scrypt.html)
5 | and turned into a standalone library.
6 |
7 | The original locations in the scrypt-1.1.6.tgz file are:
8 |
9 | - lib/crypto/crypto_scrypt-ref.c
10 | - lib/crypto/crypto_scrypt.h
11 | - lib/crypto/sha256.c
12 | - lib/crypto/sha256.h
13 | - lib/util/sysendian.h
14 |
--------------------------------------------------------------------------------
/android/src/main/cpp/scrypt/sha256.h:
--------------------------------------------------------------------------------
1 | /*-
2 | * Copyright 2005,2007,2009 Colin Percival
3 | * All rights reserved.
4 | *
5 | * Redistribution and use in source and binary forms, with or without
6 | * modification, are permitted provided that the following conditions
7 | * are met:
8 | * 1. Redistributions of source code must retain the above copyright
9 | * notice, this list of conditions and the following disclaimer.
10 | * 2. Redistributions in binary form must reproduce the above copyright
11 | * notice, this list of conditions and the following disclaimer in the
12 | * documentation and/or other materials provided with the distribution.
13 | *
14 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20 | * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23 | * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24 | * SUCH DAMAGE.
25 | *
26 | * $FreeBSD: src/lib/libmd/sha256.h,v 1.2 2006/01/17 15:35:56 phk Exp $
27 | */
28 |
29 | #ifndef _SHA256_H_
30 | #define _SHA256_H_
31 |
32 | #include
33 |
34 | #include
35 |
36 | typedef struct SHA256Context {
37 | uint32_t state[8];
38 | uint32_t count[2];
39 | unsigned char buf[64];
40 | } SHA256_CTX;
41 |
42 | typedef struct HMAC_SHA256Context {
43 | SHA256_CTX ictx;
44 | SHA256_CTX octx;
45 | } HMAC_SHA256_CTX;
46 |
47 | void SHA256_Init(SHA256_CTX *);
48 | void SHA256_Update(SHA256_CTX *, const void *, size_t);
49 | void SHA256_Final(unsigned char [32], SHA256_CTX *);
50 | void HMAC_SHA256_Init(HMAC_SHA256_CTX *, const void *, size_t);
51 | void HMAC_SHA256_Update(HMAC_SHA256_CTX *, const void *, size_t);
52 | void HMAC_SHA256_Final(unsigned char [32], HMAC_SHA256_CTX *);
53 |
54 | /**
55 | * PBKDF2_SHA256(passwd, passwdlen, salt, saltlen, c, buf, dkLen):
56 | * Compute PBKDF2(passwd, salt, c, dkLen) using HMAC-SHA256 as the PRF, and
57 | * write the output to buf. The value dkLen must be at most 32 * (2^32 - 1).
58 | */
59 | void PBKDF2_SHA256(const uint8_t *, size_t, const uint8_t *, size_t,
60 | uint64_t, uint8_t *, size_t);
61 |
62 | #endif /* !_SHA256_H_ */
63 |
--------------------------------------------------------------------------------
/android/src/main/java/app/edge/reactnative/core/Disklet.java:
--------------------------------------------------------------------------------
1 | package app.edge.reactnative.core;
2 |
3 | import android.util.AtomicFile;
4 | import java.io.File;
5 | import java.io.FileOutputStream;
6 | import java.io.IOException;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.HashMap;
9 | import java.util.Map;
10 |
11 | public class Disklet {
12 | private final File mBase;
13 |
14 | public Disklet(File base) {
15 | this.mBase = base;
16 | }
17 |
18 | public void delete(String path) {
19 | File file = new File(mBase, path);
20 | deepDelete(file);
21 | }
22 |
23 | public byte[] getData(String path) throws IOException {
24 | AtomicFile file = new AtomicFile(new File(mBase, path));
25 | return file.readFully();
26 | }
27 |
28 | public String getText(String path) throws IOException {
29 | AtomicFile file = new AtomicFile(new File(mBase, path));
30 | byte[] data = file.readFully();
31 | return new String(data, StandardCharsets.UTF_8);
32 | }
33 |
34 | public Map list(String path) {
35 | File file = new File(mBase, path);
36 | try {
37 | HashMap out = new HashMap();
38 | if (file.exists()) {
39 | if (file.isDirectory()) {
40 | String prefix = "".equals(path) ? path : path + "/";
41 | File[] files = file.listFiles();
42 | for (File child : files) {
43 | out.put(prefix + child.getName(), child.isDirectory() ? "folder" : "file");
44 | }
45 | } else {
46 | out.put(path, "file");
47 | }
48 | }
49 | return out;
50 | } catch (Throwable e) {
51 | return new HashMap();
52 | }
53 | }
54 |
55 | public void setData(String path, byte[] data) throws IOException {
56 | File file = new File(mBase, path);
57 | writeFile(file, data);
58 | }
59 |
60 | public void setText(String path, String text) throws IOException {
61 | File file = new File(mBase, path);
62 | byte[] data = text.getBytes(StandardCharsets.UTF_8);
63 | writeFile(file, data);
64 | }
65 |
66 | // helpers -----------------------------------------------------------
67 |
68 | private void deepDelete(File file) {
69 | if (file.isDirectory()) {
70 | for (File child : file.listFiles()) deepDelete(child);
71 | }
72 | new AtomicFile(file).delete();
73 | }
74 |
75 | private void writeFile(File file, byte[] data) throws IOException {
76 | File parent = file.getParentFile();
77 | if (!parent.exists()) parent.mkdirs();
78 |
79 | AtomicFile atomicFile = new AtomicFile(file);
80 | FileOutputStream stream = atomicFile.startWrite();
81 | try {
82 | stream.write(data);
83 | atomicFile.finishWrite(stream);
84 | } catch (IOException e) {
85 | atomicFile.failWrite(stream);
86 | throw e;
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/android/src/main/java/app/edge/reactnative/core/EdgeCorePackage.java:
--------------------------------------------------------------------------------
1 | package app.edge.reactnative.core;
2 |
3 | import androidx.annotation.NonNull;
4 | import com.facebook.react.ReactPackage;
5 | import com.facebook.react.bridge.NativeModule;
6 | import com.facebook.react.bridge.ReactApplicationContext;
7 | import com.facebook.react.uimanager.ViewManager;
8 | import java.util.Collections;
9 | import java.util.List;
10 |
11 | public class EdgeCorePackage implements ReactPackage {
12 | @NonNull
13 | @Override
14 | public List createNativeModules(@NonNull ReactApplicationContext reactContext) {
15 | return Collections.emptyList();
16 | }
17 |
18 | @NonNull
19 | @Override
20 | public List createViewManagers(@NonNull ReactApplicationContext reactContext) {
21 | return Collections.singletonList(new EdgeCoreWebViewManager(reactContext));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/android/src/main/java/app/edge/reactnative/core/EdgeCoreWebViewManager.java:
--------------------------------------------------------------------------------
1 | package app.edge.reactnative.core;
2 |
3 | import android.webkit.WebView;
4 | import androidx.annotation.NonNull;
5 | import com.facebook.react.bridge.ReactApplicationContext;
6 | import com.facebook.react.bridge.ReadableArray;
7 | import com.facebook.react.common.MapBuilder;
8 | import com.facebook.react.uimanager.SimpleViewManager;
9 | import com.facebook.react.uimanager.ThemedReactContext;
10 | import com.facebook.react.uimanager.annotations.ReactProp;
11 | import java.util.HashMap;
12 | import java.util.Map;
13 |
14 | public class EdgeCoreWebViewManager extends SimpleViewManager {
15 | public EdgeCoreWebViewManager(ReactApplicationContext context) {}
16 |
17 | @NonNull
18 | @Override
19 | public EdgeCoreWebView createViewInstance(@NonNull ThemedReactContext themedContext) {
20 | return new EdgeCoreWebView(themedContext);
21 | }
22 |
23 | @Override
24 | public Map getExportedCustomDirectEventTypeConstants() {
25 | final Map constants = new HashMap<>();
26 | constants.put("onMessage", MapBuilder.of("registrationName", "onMessage"));
27 | constants.put("onScriptError", MapBuilder.of("registrationName", "onScriptError"));
28 | return constants;
29 | }
30 |
31 | @NonNull
32 | @Override
33 | public String getName() {
34 | return "EdgeCoreWebView";
35 | }
36 |
37 | @Override
38 | public void receiveCommand(@NonNull EdgeCoreWebView view, String command, ReadableArray args) {
39 | if ("runJs".equals(command)) {
40 | view.runJs(args.getString(0));
41 | }
42 | }
43 |
44 | @ReactProp(name = "allowDebugging")
45 | public void setAllowDebugging(@NonNull EdgeCoreWebView view, boolean allow) {
46 | WebView.setWebContentsDebuggingEnabled(allow);
47 | }
48 |
49 | @ReactProp(name = "source")
50 | public void setSource(@NonNull EdgeCoreWebView view, String source) {
51 | view.setSource(source);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/android/src/main/java/app/edge/reactnative/core/PendingCall.java:
--------------------------------------------------------------------------------
1 | package app.edge.reactnative.core;
2 |
3 | public interface PendingCall {
4 | public void resolve(Object value);
5 |
6 | public void reject(String message);
7 | }
8 |
--------------------------------------------------------------------------------
/android/src/main/java/app/edge/reactnative/core/StreamStringReader.java:
--------------------------------------------------------------------------------
1 | package app.edge.reactnative.core;
2 |
3 | import android.util.Base64;
4 | import androidx.annotation.NonNull;
5 | import java.io.ByteArrayOutputStream;
6 | import java.io.IOException;
7 | import java.io.InputStream;
8 | import java.nio.ByteBuffer;
9 | import java.nio.charset.CharacterCodingException;
10 | import java.nio.charset.CodingErrorAction;
11 | import java.nio.charset.StandardCharsets;
12 |
13 | /** Consumes an input stream and converts it to text. */
14 | class StreamStringReader extends ByteArrayOutputStream {
15 | public void read(@NonNull InputStream in, int sizeHint) throws IOException {
16 | int size;
17 | byte[] data = new byte[sizeHint > 0 ? sizeHint : 4096];
18 | while ((size = in.read(data)) > 0) {
19 | write(data, 0, size);
20 | }
21 | }
22 |
23 | public @NonNull String getUtf8() throws CharacterCodingException {
24 | return StandardCharsets.UTF_8
25 | .newDecoder()
26 | .onMalformedInput(CodingErrorAction.REPORT)
27 | .onUnmappableCharacter(CodingErrorAction.REPORT)
28 | .decode(ByteBuffer.wrap(buf, 0, count))
29 | .toString();
30 | }
31 |
32 | public @NonNull String getBase64() {
33 | return Base64.encodeToString(buf, 0, count, Base64.NO_WRAP);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/edge-core-js.podspec:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4 |
5 | Pod::Spec.new do |s|
6 | s.name = package['name']
7 | s.version = package['version']
8 | s.summary = package['description']
9 | s.homepage = package['homepage']
10 | s.license = package['license']
11 | s.authors = package['author']
12 |
13 | s.platform = :ios, "9.0"
14 | s.requires_arc = true
15 | s.source = {
16 | :git => "https://github.com/EdgeApp/edge-core-js.git",
17 | :tag => "v#{s.version}"
18 | }
19 | s.source_files =
20 | "android/src/main/cpp/scrypt/crypto_scrypt.c",
21 | "android/src/main/cpp/scrypt/crypto_scrypt.h",
22 | "android/src/main/cpp/scrypt/sha256.c",
23 | "android/src/main/cpp/scrypt/sha256.h",
24 | "android/src/main/cpp/scrypt/sysendian.h",
25 | "ios/Disklet.swift",
26 | "ios/edge-core-js-Bridging-Header.h",
27 | "ios/EdgeCoreWebView.swift",
28 | "ios/EdgeCoreWebViewManager.m",
29 | "ios/EdgeCoreWebViewManager.swift",
30 | "ios/EdgeNative.swift",
31 | "ios/PendingCall.swift"
32 |
33 | s.resource_bundles = {
34 | "edge-core-js" => "android/src/main/assets/edge-core-js/edge-core.js"
35 | }
36 |
37 | s.dependency "React-Core"
38 | end
39 |
--------------------------------------------------------------------------------
/flow-typed/react-native-libs.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | declare module 'react-native' {
4 | declare module.exports: any
5 | }
6 |
--------------------------------------------------------------------------------
/ios/Disklet.swift:
--------------------------------------------------------------------------------
1 | class Disklet {
2 | let baseUrl: URL
3 |
4 | init() {
5 | let paths = NSSearchPathForDirectoriesInDomains(
6 | .documentDirectory,
7 | .userDomainMask,
8 | true
9 | )
10 | baseUrl = URL.init(fileURLWithPath: paths[0])
11 | }
12 |
13 | func delete(path: String) throws {
14 | let url = URL.init(fileURLWithPath: path, relativeTo: baseUrl)
15 | do {
16 | try FileManager().removeItem(at: url)
17 | } catch CocoaError.fileNoSuchFile {}
18 | }
19 |
20 | func getData(path: String) throws -> Data {
21 | let url = URL.init(fileURLWithPath: path, relativeTo: baseUrl)
22 | return try Data.init(contentsOf: url)
23 | }
24 |
25 | func getText(path: String) throws -> String {
26 | let url = URL.init(fileURLWithPath: path, relativeTo: baseUrl)
27 | return try String.init(contentsOf: url)
28 | }
29 |
30 | func list(path: String) throws -> [String: String] {
31 | let url = URL.init(fileURLWithPath: path, relativeTo: baseUrl)
32 | let fs = FileManager()
33 |
34 | let isDirectory = try? url.resourceValues(
35 | forKeys: [.isDirectoryKey]
36 | ).isDirectory
37 | if isDirectory == nil { return [:] }
38 | if !isDirectory! { return [path: "file"] }
39 |
40 | let prefix = path == "" ? "" : path + "/"
41 | var out: [String: String] = [:]
42 | let urls = try fs.contentsOfDirectory(
43 | at: url,
44 | includingPropertiesForKeys: [.isDirectoryKey],
45 | options: [.skipsSubdirectoryDescendants]
46 | )
47 | for item in urls {
48 | if let isDirectory = try? item.resourceValues(
49 | forKeys: [.isDirectoryKey]
50 | ).isDirectory {
51 | out[prefix + item.lastPathComponent] = isDirectory ? "folder" : "file"
52 | }
53 | }
54 | return out
55 | }
56 |
57 | func setData(path: String, data: Data) throws {
58 | let url: URL = URL.init(fileURLWithPath: path, relativeTo: baseUrl)
59 |
60 | try FileManager().createDirectory(
61 | at: url.deletingLastPathComponent(),
62 | withIntermediateDirectories: true
63 | )
64 | try data.write(to: url, options: [.atomic])
65 | }
66 |
67 | func setText(path: String, text: String) throws {
68 | let url: URL = URL.init(fileURLWithPath: path, relativeTo: baseUrl)
69 | try FileManager().createDirectory(
70 | at: url.deletingLastPathComponent(),
71 | withIntermediateDirectories: true
72 | )
73 | try text.write(to: url, atomically: true, encoding: .utf8)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/ios/EdgeCoreWebViewManager.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface RCT_EXTERN_MODULE(EdgeCoreWebViewManager, RCTViewManager)
4 | RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
5 | RCT_EXPORT_VIEW_PROPERTY(onScriptError, RCTDirectEventBlock)
6 | RCT_EXPORT_VIEW_PROPERTY(allowDebugging, BOOL)
7 | RCT_EXPORT_VIEW_PROPERTY(source, NSString)
8 | RCT_EXTERN_METHOD(runJs:(nonnull NSNumber *)reactTag js:(nonnull NSString *)js)
9 | @end
10 |
--------------------------------------------------------------------------------
/ios/EdgeCoreWebViewManager.swift:
--------------------------------------------------------------------------------
1 | @objc(EdgeCoreWebViewManager) class EdgeCoreWebViewManager: RCTViewManager {
2 | override static func requiresMainQueueSetup() -> Bool { return false }
3 | override func view() -> UIView! { return EdgeCoreWebView() }
4 |
5 | @objc func runJs(_ reactTag: NSNumber, js: String) {
6 | bridge.uiManager.addUIBlock({ (uiManager, viewRegistry) in
7 | let view = uiManager?.view(forReactTag: reactTag)
8 | if let webView = view as? EdgeCoreWebView {
9 | webView.runJs(js: js)
10 | }
11 | })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ios/PendingCall.swift:
--------------------------------------------------------------------------------
1 | struct PendingCall {
2 | var resolve: (_ value: Any?) -> Void
3 | var reject: (_ message: String) -> Void
4 | }
5 |
--------------------------------------------------------------------------------
/ios/edge-core-js-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #include "crypto_scrypt.h"
5 |
--------------------------------------------------------------------------------
/ios/edge-core-js.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // Placeholder so react-native autolinking can find us.
2 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from '@rollup/plugin-babel'
2 | import resolve from '@rollup/plugin-node-resolve'
3 | import flowEntry from 'rollup-plugin-flow-entry'
4 | import mjs from 'rollup-plugin-mjs-entry'
5 |
6 | import packageJson from './package.json'
7 |
8 | const extensions = ['.ts']
9 |
10 | const babelOpts = {
11 | babelHelpers: 'bundled',
12 | babelrc: false,
13 | extensions,
14 | plugins: ['babel-plugin-transform-fake-error-class'],
15 | presets: ['@babel/preset-typescript', '@babel/preset-react']
16 | }
17 |
18 | const external = ['crypto', ...Object.keys(packageJson.dependencies)]
19 |
20 | const resolveOpts = { extensions }
21 |
22 | // Produces the Node entry point and standalone type definition files.
23 | export default [
24 | {
25 | external,
26 | input: './src/index.ts',
27 | output: { file: packageJson.main, format: 'cjs' },
28 | plugins: [
29 | resolve(resolveOpts),
30 | babel(babelOpts),
31 | flowEntry({ types: './lib/flow/exports.js' }),
32 | mjs()
33 | ]
34 | },
35 | {
36 | external,
37 | input: './src/types/types.ts',
38 | output: { file: './types.js', format: 'cjs' },
39 | plugins: [
40 | resolve(resolveOpts),
41 | babel(babelOpts),
42 | flowEntry({ types: './lib/flow/types.js' }),
43 | mjs()
44 | ]
45 | }
46 | ]
47 |
--------------------------------------------------------------------------------
/scripts/make-types.ts:
--------------------------------------------------------------------------------
1 | // Run as `node -r sucrase/register scripts/make-types.ts`
2 |
3 | import { makeNodeDisklet } from 'disklet'
4 | import prettier from 'prettier'
5 |
6 | async function tsToFlow(code: string): Promise {
7 | // First, use Prettier to add semicolons everywhere:
8 | const formatted = await prettier.format(code, {
9 | parser: 'typescript',
10 | semi: true
11 | })
12 |
13 | const fixed = formatted
14 | // Language differences:
15 | .replace(/\breadonly /g, '+')
16 | .replace(/\bexport declare const\b/g, 'declare export var')
17 | .replace(/\bexport declare\b/g, 'declare export')
18 | .replace(/\binterface (\w+) {/g, 'type $1 = {')
19 | .replace(/\binterface (\w+)<([^>]+)> {/g, 'type $1<$2> = {')
20 | .replace(
21 | /\binterface (\w+) extends (\w+) {/g,
22 | 'type $1 = {\n ...$Exact<$2>;'
23 | )
24 | .replace(/\bunknown\b/g, 'mixed')
25 | .replace(/\| undefined\b/g, '| void')
26 | .replace(/: undefined\b/g, ': void')
27 |
28 | // Builtin types:
29 | .replace(/\b(\w+): ComponentType/g, '$Rest<$1, { ... }>')
31 | .replace(/\bAsyncIterableIterator\b/g, 'AsyncGenerator')
32 |
33 | return '// @flow\n\n' + fixed
34 | }
35 |
36 | async function main(): Promise {
37 | const disklet = makeNodeDisklet('.')
38 | const listing = await disklet.list('src/types')
39 | const paths = Object.keys(listing).filter(name => listing[name] === 'file')
40 |
41 | // Transpile Flow types to Typescript:
42 | for (const path of paths) {
43 | const source = await disklet.getText(path)
44 |
45 | const flowPath = path
46 | .replace('src/types/', 'lib/flow/')
47 | .replace('.ts', '.js')
48 | const flowSource = await tsToFlow(source)
49 |
50 | await disklet.setText(flowPath, flowSource)
51 | }
52 | }
53 |
54 | main().catch(error => {
55 | console.error(error)
56 | process.exit(1)
57 | })
58 |
--------------------------------------------------------------------------------
/src/browser.ts:
--------------------------------------------------------------------------------
1 | import { makeContext, makeFakeWorld } from './core/core'
2 | import { defaultOnLog } from './core/log/log'
3 | import { makeBrowserIo } from './io/browser/browser-io'
4 | import {
5 | EdgeContext,
6 | EdgeContextOptions,
7 | EdgeFakeUser,
8 | EdgeFakeWorld,
9 | EdgeFakeWorldOptions
10 | } from './types/types'
11 |
12 | export { makeBrowserIo }
13 | export {
14 | addEdgeCorePlugins,
15 | closeEdge,
16 | lockEdgeCorePlugins,
17 | makeFakeIo
18 | } from './core/core'
19 | export * from './types/types'
20 |
21 | export function makeEdgeContext(
22 | opts: EdgeContextOptions
23 | ): Promise {
24 | const { crashReporter, onLog = defaultOnLog } = opts
25 | return makeContext(
26 | { io: makeBrowserIo(), nativeIo: {} },
27 | { crashReporter, onLog },
28 | opts
29 | )
30 | }
31 |
32 | export function makeFakeEdgeWorld(
33 | users: EdgeFakeUser[] = [],
34 | opts: EdgeFakeWorldOptions = {}
35 | ): Promise {
36 | const { crashReporter, onLog = defaultOnLog } = opts
37 | return Promise.resolve(
38 | makeFakeWorld(
39 | { io: makeBrowserIo(), nativeIo: {} },
40 | { crashReporter, onLog },
41 | users
42 | )
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/src/client-side.ts:
--------------------------------------------------------------------------------
1 | import { close, shareData } from 'yaob'
2 |
3 | import {
4 | EdgePasswordRules,
5 | EdgeStreamTransactionOptions,
6 | EdgeTransaction,
7 | EdgeWalletInfoFull
8 | } from './types/types'
9 |
10 | export interface InternalWalletStream {
11 | next: () => Promise<{
12 | done: boolean
13 | value: EdgeTransaction[]
14 | }>
15 | }
16 |
17 | export interface InternalWalletMethods {
18 | $internalStreamTransactions: (
19 | opts: EdgeStreamTransactionOptions
20 | ) => Promise
21 | }
22 |
23 | /**
24 | * Client-side EdgeAccount methods.
25 | */
26 | export class AccountSync {
27 | readonly allKeys!: EdgeWalletInfoFull[]
28 |
29 | getFirstWalletInfo(type: string): EdgeWalletInfoFull | undefined {
30 | return this.allKeys.find(info => info.type === type)
31 | }
32 |
33 | getWalletInfo(id: string): EdgeWalletInfoFull | undefined {
34 | return this.allKeys.find(info => info.id === id)
35 | }
36 |
37 | listWalletIds(): string[] {
38 | return this.allKeys.map(info => info.id)
39 | }
40 | }
41 | shareData(AccountSync.prototype, 'AccountSync')
42 |
43 | /**
44 | * Verifies that a password meets our suggested rules.
45 | */
46 | export function checkPasswordRules(password: string): EdgePasswordRules {
47 | const tooShort = password.length < 10
48 | const noNumber = !/[0-9]/.test(password)
49 | const noLowerCase = !/[a-z]/.test(password)
50 | const noUpperCase = !/[A-Z]/.test(password)
51 |
52 | // Quick & dirty password strength estimation:
53 | const charset =
54 | (/[0-9]/.test(password) ? 10 : 0) +
55 | (/[A-Z]/.test(password) ? 26 : 0) +
56 | (/[a-z]/.test(password) ? 26 : 0) +
57 | (/[^0-9A-Za-z]/.test(password) ? 30 : 0)
58 | const secondsToCrack = Math.pow(charset, password.length) / 1e6
59 |
60 | return {
61 | secondsToCrack,
62 | tooShort,
63 | noNumber,
64 | noLowerCase,
65 | noUpperCase,
66 | passed:
67 | password.length >= 16 ||
68 | !(tooShort || noNumber || noUpperCase || noLowerCase)
69 | }
70 | }
71 | shareData({ checkPasswordRules })
72 |
73 | /**
74 | * Normalizes a username, and checks for invalid characters.
75 | * TODO: Support a wider character range via Unicode normalization.
76 | */
77 | export function fixUsername(username: string): string {
78 | const out = username
79 | .toLowerCase()
80 | .replace(/[ \f\r\n\t\v]+/g, ' ')
81 | .replace(/ $/, '')
82 | .replace(/^ /, '')
83 |
84 | for (let i = 0; i < out.length; ++i) {
85 | const c = out.charCodeAt(i)
86 | if (c < 0x20 || c > 0x7e) {
87 | throw new Error('Bad characters in username')
88 | }
89 | }
90 | return out
91 | }
92 | shareData({ fixUsername })
93 |
94 | /**
95 | * Synchronously constructs a transaction stream.
96 | * This method creates a secret internal stream,
97 | * which differs slightly from the AsyncIterableIterator protocol
98 | * because of YAOB limitations.
99 | * It then wraps the internal stream object with the correct API.
100 | */
101 | export function streamTransactions(
102 | this: InternalWalletMethods,
103 | opts: EdgeStreamTransactionOptions
104 | ): AsyncIterableIterator {
105 | let stream: InternalWalletStream | undefined
106 | let streamClosed = false
107 |
108 | const out: AsyncIterableIterator = {
109 | next: async () => {
110 | if (stream == null) stream = await this.$internalStreamTransactions(opts)
111 | if (!streamClosed) {
112 | const out = await stream.next()
113 | if (!out.done) return out
114 | close(stream)
115 | streamClosed = true
116 | }
117 | return { done: true, value: undefined }
118 | },
119 |
120 | /**
121 | * Closes the iterator early if the client doesn't want all the results.
122 | * This is necessary to prevent memory leaks over the bridge.
123 | */
124 | return: async () => {
125 | if (stream != null && !streamClosed) {
126 | close(stream)
127 | streamClosed = true
128 | }
129 | return { done: true, value: undefined }
130 | },
131 |
132 | [Symbol.asyncIterator]: () => out
133 | }
134 | return out
135 | }
136 | shareData({ streamTransactions }, 'CurrencyWalletSync')
137 |
--------------------------------------------------------------------------------
/src/core/account/account-cleaners.ts:
--------------------------------------------------------------------------------
1 | import {
2 | asArray,
3 | asBoolean,
4 | asNumber,
5 | asObject,
6 | asOptional,
7 | asString
8 | } from 'cleaners'
9 |
10 | import { asBase16 } from '../../types/server-cleaners'
11 | import { EdgeDenomination, EdgeToken } from '../../types/types'
12 | import { asJsonObject } from '../../util/file-helpers'
13 | import { SwapSettings } from './account-types'
14 |
15 | // ---------------------------------------------------------------------
16 | // building-block types
17 | // ---------------------------------------------------------------------
18 |
19 | const asEdgeDenomination = asObject({
20 | multiplier: asString,
21 | name: asString,
22 | symbol: asOptional(asString)
23 | })
24 |
25 | const asEdgeToken = asObject({
26 | currencyCode: asString,
27 | denominations: asArray(asEdgeDenomination),
28 | displayName: asString,
29 | networkLocation: asOptional(asJsonObject)
30 | })
31 |
32 | const asSwapSettings = asObject({
33 | enabled: asOptional(asBoolean, true)
34 | }).withRest
35 |
36 | // ---------------------------------------------------------------------
37 | // file types
38 | // ---------------------------------------------------------------------
39 |
40 | /**
41 | * An Airbitz Bitcoin wallet, which includes the private key & state.
42 | */
43 | export const asLegacyWalletFile = asObject({
44 | SortIndex: asOptional(asNumber, 0),
45 | Archived: asOptional(asBoolean, false),
46 | BitcoinSeed: asBase16,
47 | MK: asBase16,
48 | SyncKey: asBase16
49 | }).withRest
50 |
51 | /**
52 | * An Edge wallet state file. The keys are stored in the login server.
53 | */
54 | export const asWalletStateFile = asObject({
55 | id: asString,
56 | archived: asOptional(asBoolean),
57 | deleted: asOptional(asBoolean),
58 | hidden: asOptional(asBoolean),
59 | migratedFromWalletId: asOptional(asString),
60 | sortIndex: asOptional(asNumber)
61 | })
62 |
63 | /**
64 | * Stores settings for currency and swap plugins.
65 | */
66 | export const asPluginSettingsFile = asObject({
67 | // Currency plugins:
68 | userSettings: asOptional(asObject(asJsonObject), () => ({})),
69 |
70 | // Swap plugins:
71 | swapSettings: asOptional(asObject(asSwapSettings), () => ({}))
72 | }).withRest
73 |
74 | /**
75 | * The settings file managed by the GUI.
76 | */
77 | export const asGuiSettingsFile = asObject({
78 | customTokens: asArray(
79 | asObject({
80 | contractAddress: asString,
81 | currencyCode: asString,
82 | currencyName: asString,
83 | denomination: asString,
84 | denominations: asArray(asEdgeDenomination),
85 | isVisible: asOptional(asBoolean, true),
86 | multiplier: asString,
87 | walletType: asOptional(asString, 'wallet:ethereum')
88 | })
89 | )
90 | })
91 |
92 | export const asCustomTokensFile = asObject({
93 | customTokens: asObject(asObject(asEdgeToken))
94 | })
95 |
--------------------------------------------------------------------------------
/src/core/account/account-types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Swap plugin settings.
3 | */
4 | export interface SwapSettings {
5 | enabled: boolean
6 | }
7 |
--------------------------------------------------------------------------------
/src/core/account/data-store-api.ts:
--------------------------------------------------------------------------------
1 | import { asObject, asString } from 'cleaners'
2 | import { justFiles, justFolders } from 'disklet'
3 | import { bridgifyObject } from 'yaob'
4 |
5 | import { EdgeDataStore } from '../../types/types'
6 | import { makeJsonFile } from '../../util/file-helpers'
7 | import { ApiInput } from '../root-pixie'
8 | import {
9 | getStorageWalletDisklet,
10 | hashStorageWalletFilename
11 | } from '../storage/storage-selectors'
12 |
13 | /**
14 | * Each data store folder has a "Name.json" file with this format.
15 | */
16 | const storeIdFile = makeJsonFile(
17 | asObject({
18 | name: asString
19 | })
20 | )
21 |
22 | /**
23 | * The items saved in a data store have this format.
24 | */
25 | const storeItemFile = makeJsonFile(
26 | asObject({
27 | key: asString,
28 | data: asString
29 | })
30 | )
31 |
32 | export function makeDataStoreApi(
33 | ai: ApiInput,
34 | accountId: string
35 | ): EdgeDataStore {
36 | const { accountWalletInfo } = ai.props.state.accounts[accountId]
37 | const disklet = getStorageWalletDisklet(ai.props.state, accountWalletInfo.id)
38 |
39 | // Path manipulation:
40 | const hashName = (data: string): string =>
41 | hashStorageWalletFilename(ai.props.state, accountWalletInfo.id, data)
42 | const getStorePath = (storeId: string): string =>
43 | `Plugins/${hashName(storeId)}`
44 | const getItemPath = (storeId: string, itemId: string): string =>
45 | `${getStorePath(storeId)}/${hashName(itemId)}.json`
46 |
47 | const out: EdgeDataStore = {
48 | async deleteItem(storeId: string, itemId: string): Promise {
49 | await disklet.delete(getItemPath(storeId, itemId))
50 | },
51 |
52 | async deleteStore(storeId: string): Promise {
53 | await disklet.delete(getStorePath(storeId))
54 | },
55 |
56 | async listItemIds(storeId: string): Promise {
57 | const itemIds: string[] = []
58 | const paths = justFiles(await disklet.list(getStorePath(storeId)))
59 | await Promise.all(
60 | paths.map(async path => {
61 | const clean = await storeItemFile.load(disklet, path)
62 | if (clean != null) itemIds.push(clean.key)
63 | })
64 | )
65 | return itemIds
66 | },
67 |
68 | async listStoreIds(): Promise {
69 | const storeIds: string[] = []
70 | const paths = justFolders(await disklet.list('Plugins'))
71 | await Promise.all(
72 | paths.map(async path => {
73 | const clean = await storeIdFile.load(disklet, `${path}/Name.json`)
74 | if (clean != null) storeIds.push(clean.name)
75 | })
76 | )
77 | return storeIds
78 | },
79 |
80 | async getItem(storeId: string, itemId: string): Promise {
81 | const clean = await storeItemFile.load(
82 | disklet,
83 | getItemPath(storeId, itemId)
84 | )
85 | if (clean == null) throw new Error(`No item named "${itemId}"`)
86 | return clean.data
87 | },
88 |
89 | async setItem(
90 | storeId: string,
91 | itemId: string,
92 | value: string
93 | ): Promise {
94 | // Set up the plugin folder, if needed:
95 | const namePath = `${getStorePath(storeId)}/Name.json`
96 | const clean = await storeIdFile.load(disklet, namePath)
97 | if (clean == null) {
98 | await storeIdFile.save(disklet, namePath, { name: storeId })
99 | } else if (clean.name !== storeId) {
100 | throw new Error(`Warning: folder name doesn't match for ${storeId}`)
101 | }
102 |
103 | // Set up the actual item:
104 | await storeItemFile.save(disklet, getItemPath(storeId, itemId), {
105 | key: itemId,
106 | data: value
107 | })
108 | }
109 | }
110 | bridgifyObject(out)
111 |
112 | return out
113 | }
114 |
--------------------------------------------------------------------------------
/src/core/context/client-file.ts:
--------------------------------------------------------------------------------
1 | import { asBoolean, asMaybe, asObject } from 'cleaners'
2 |
3 | import { asBase64 } from '../../types/server-cleaners'
4 | import { makeJsonFile } from '../../util/file-helpers'
5 |
6 | export const CLIENT_FILE_NAME = 'client.json'
7 |
8 | export interface ClientInfo {
9 | clientId: Uint8Array
10 | /**
11 | * This is a boolean flag that puts the device into duress mode.
12 | */
13 | duressEnabled: boolean
14 | }
15 |
16 | export const clientFile = makeJsonFile(
17 | asObject({
18 | clientId: asBase64,
19 | duressEnabled: asMaybe(asBoolean, false)
20 | })
21 | )
22 |
--------------------------------------------------------------------------------
/src/core/context/context-pixie.ts:
--------------------------------------------------------------------------------
1 | import {
2 | combinePixies,
3 | filterPixie,
4 | stopUpdates,
5 | TamePixie
6 | } from 'redux-pixies'
7 | import { close, update } from 'yaob'
8 |
9 | import { EdgeContext, EdgeLogSettings, EdgeUserInfo } from '../../types/types'
10 | import { makePeriodicTask } from '../../util/periodic-task'
11 | import { shuffle } from '../../util/shuffle'
12 | import { ApiInput, RootProps } from '../root-pixie'
13 | import { makeContextApi } from './context-api'
14 | import {
15 | asInfoCacheFile,
16 | INFO_CACHE_FILE_NAME,
17 | infoCacheFile
18 | } from './info-cache-file'
19 |
20 | export interface ContextOutput {
21 | api: EdgeContext
22 | }
23 |
24 | export const context: TamePixie = combinePixies({
25 | api(ai: ApiInput) {
26 | return {
27 | destroy() {
28 | close(ai.props.output.context.api)
29 | },
30 | update() {
31 | ai.onOutput(makeContextApi(ai))
32 | return stopUpdates
33 | }
34 | }
35 | },
36 |
37 | watcher(ai: ApiInput) {
38 | let lastLocalUsers: EdgeUserInfo[] | undefined
39 | let lastPaused: boolean | undefined
40 | let lastLogSettings: EdgeLogSettings | undefined
41 |
42 | return () => {
43 | if (
44 | lastLocalUsers !== ai.props.state.login.localUsers ||
45 | lastPaused !== ai.props.state.paused ||
46 | lastLogSettings !== ai.props.state.logSettings
47 | ) {
48 | lastLocalUsers = ai.props.state.login.localUsers
49 | lastPaused = ai.props.state.paused
50 | lastLogSettings = ai.props.state.logSettings
51 | if (ai.props.output.context.api != null) {
52 | update(ai.props.output.context.api)
53 | }
54 | }
55 | }
56 | },
57 |
58 | infoFetcher: filterPixie(
59 | (input: ApiInput) => {
60 | async function doInfoSync(): Promise {
61 | const { dispatch, io } = input.props
62 |
63 | const [infoServerUri] = shuffle(input.props.state.infoServers)
64 | const response = await fetch(`${infoServerUri}/v1/coreRollup`, {
65 | headers: { accept: 'application/json' }
66 | })
67 | if (!response.ok) return
68 | const json = await response.json()
69 |
70 | const infoCache = asInfoCacheFile(json)
71 | dispatch({
72 | type: 'INFO_CACHE_FETCHED',
73 | payload: infoCache
74 | })
75 | await infoCacheFile.save(io.disklet, INFO_CACHE_FILE_NAME, infoCache)
76 | }
77 |
78 | const infoTask = makePeriodicTask(doInfoSync, 10 * 60 * 1000, {
79 | onError(error) {
80 | input.props.onError(error)
81 | }
82 | })
83 |
84 | return {
85 | update() {
86 | if (!infoTask.started) infoTask.start()
87 | },
88 | destroy() {
89 | infoTask.stop()
90 | }
91 | }
92 | },
93 | props => (props.state.paused ? undefined : props)
94 | )
95 | })
96 |
--------------------------------------------------------------------------------
/src/core/context/info-cache-file.ts:
--------------------------------------------------------------------------------
1 | import { asArray, asObject, asString } from 'cleaners'
2 |
3 | import { EdgePluginMap, JsonObject } from '../../browser'
4 | import { asJsonObject, makeJsonFile } from '../../util/file-helpers'
5 |
6 | export interface InfoCacheFile {
7 | corePlugins?: EdgePluginMap
8 | syncServers?: string[]
9 | }
10 |
11 | export const INFO_CACHE_FILE_NAME = 'infoCache.json'
12 |
13 | export const asInfoCacheFile = asObject({
14 | corePlugins: asObject(asJsonObject),
15 | syncServers: asArray(asString)
16 | })
17 |
18 | export const infoCacheFile = makeJsonFile(asInfoCacheFile)
19 |
--------------------------------------------------------------------------------
/src/core/context/internal-api.ts:
--------------------------------------------------------------------------------
1 | import { Disklet } from 'disklet'
2 | import { Bridgeable, bridgifyObject, close, emit, update } from 'yaob'
3 | import { Unsubscribe } from 'yavent'
4 |
5 | import { EdgeLobbyRequest, LoginRequestBody } from '../../types/server-types'
6 | import { EdgeContext } from '../../types/types'
7 | import {
8 | fetchLobbyRequest,
9 | LobbyInstance,
10 | makeLobby,
11 | sendLobbyReply
12 | } from '../login/lobby'
13 | import { loginFetch } from '../login/login-fetch'
14 | import { hashUsername } from '../login/login-selectors'
15 | import { ApiInput } from '../root-pixie'
16 | import { makeRepoPaths, syncRepo, SyncResult } from '../storage/repo'
17 |
18 | /**
19 | * The requesting side of an Edge login lobby.
20 | * The `replies` property will update as replies come in.
21 | */
22 | class EdgeLobby extends Bridgeable<
23 | {
24 | replies: unknown[]
25 | lobbyId: string
26 | },
27 | { error: Error }
28 | > {
29 | _lobby: LobbyInstance
30 | _cleanups: Unsubscribe[]
31 |
32 | constructor(lobby: LobbyInstance) {
33 | super()
34 | this._lobby = lobby
35 |
36 | this._cleanups = [
37 | lobby.close,
38 | lobby.on('reply', reply => update(this, 'replies')),
39 | lobby.on('error', error => emit(this, 'error', error))
40 | ]
41 | }
42 |
43 | get lobbyId(): string {
44 | return this._lobby.lobbyId
45 | }
46 |
47 | get replies(): unknown[] {
48 | return this._lobby.replies
49 | }
50 |
51 | close(): void {
52 | this._cleanups.forEach(f => f())
53 | close(this)
54 | }
55 | }
56 |
57 | /**
58 | * A secret internal API which has some goodies for the CLI
59 | * and for unit testing.
60 | */
61 | export class EdgeInternalStuff extends Bridgeable<{}> {
62 | _ai: ApiInput
63 |
64 | constructor(ai: ApiInput) {
65 | super()
66 | this._ai = ai
67 | }
68 |
69 | authRequest(
70 | method: string,
71 | path: string,
72 | body?: LoginRequestBody
73 | ): Promise {
74 | return loginFetch(this._ai, method, path, body)
75 | }
76 |
77 | hashUsername(username: string): Promise {
78 | return hashUsername(this._ai, username)
79 | }
80 |
81 | async makeLobby(
82 | lobbyRequest: Partial,
83 | period: number = 1000
84 | ): Promise {
85 | const lobby = await makeLobby(this._ai, lobbyRequest, period)
86 | return new EdgeLobby(lobby)
87 | }
88 |
89 | fetchLobbyRequest(lobbyId: string): Promise {
90 | return fetchLobbyRequest(this._ai, lobbyId)
91 | }
92 |
93 | async sendLobbyReply(
94 | lobbyId: string,
95 | lobbyRequest: EdgeLobbyRequest,
96 | replyData: unknown
97 | ): Promise {
98 | await sendLobbyReply(this._ai, lobbyId, lobbyRequest, replyData)
99 | }
100 |
101 | async syncRepo(syncKey: Uint8Array): Promise {
102 | const { io, syncClient } = this._ai.props
103 | const paths = makeRepoPaths(io, { dataKey: new Uint8Array(0), syncKey })
104 | return await syncRepo(syncClient, paths, {
105 | lastSync: 0,
106 | lastHash: undefined
107 | })
108 | }
109 |
110 | async getRepoDisklet(
111 | syncKey: Uint8Array,
112 | dataKey: Uint8Array
113 | ): Promise {
114 | const { io } = this._ai.props
115 | const paths = makeRepoPaths(io, { dataKey, syncKey })
116 | bridgifyObject(paths.disklet)
117 | return paths.disklet
118 | }
119 | }
120 |
121 | /**
122 | * Our public Flow types don't include the internal stuff,
123 | * so this function hacks around Flow to retrieve it.
124 | */
125 | export function getInternalStuff(context: EdgeContext): EdgeInternalStuff {
126 | const flowHack: any = context
127 | return flowHack.$internalStuff
128 | }
129 |
--------------------------------------------------------------------------------
/src/core/core.ts:
--------------------------------------------------------------------------------
1 | export { makeFakeIo } from './fake/fake-io'
2 | export { makeFakeWorld } from './fake/fake-world'
3 | export {
4 | addEdgeCorePlugins,
5 | lockEdgeCorePlugins
6 | } from './plugins/plugins-actions'
7 | export { closeEdge, makeContext } from './root'
8 |
--------------------------------------------------------------------------------
/src/core/currency/change-server-connection.ts:
--------------------------------------------------------------------------------
1 | import {
2 | changeProtocol,
3 | SubscribeParams,
4 | SubscribeResult
5 | } from './change-server-protocol'
6 |
7 | interface ChangeServerCallbacks {
8 | handleChange: (address: SubscribeParams) => void
9 | handleConnect: () => void
10 | handleDisconnect: () => void
11 | handleSubLost: (params: SubscribeParams) => void
12 | }
13 |
14 | export interface ChangeServerConnection {
15 | subscribe: (params: SubscribeParams[]) => Promise
16 | unsubscribe: (params: SubscribeParams[]) => Promise
17 | close: () => void
18 | connected: boolean
19 | }
20 |
21 | /**
22 | * Bundles a change-server Websocket and codec pair.
23 | */
24 | export function connectChangeServer(
25 | url: string,
26 | callbacks: ChangeServerCallbacks
27 | ): ChangeServerConnection {
28 | let ws: WebSocket
29 | function makeWs(): void {
30 | ws = new WebSocket(url)
31 | ws.binaryType = 'arraybuffer'
32 |
33 | ws.addEventListener('message', ev => {
34 | codec.handleMessage(ev.data)
35 | })
36 |
37 | ws.addEventListener('close', () => {
38 | out.connected = false
39 | codec.handleClose()
40 | callbacks.handleDisconnect()
41 | })
42 |
43 | ws.addEventListener('error', errEvent => {
44 | console.error('changeServer websocket error:', errEvent)
45 | ws.close()
46 | // Reconnect after 5 seconds:
47 | setTimeout(() => {
48 | makeWs()
49 | }, 5000)
50 | })
51 |
52 | ws.addEventListener('open', () => {
53 | out.connected = true
54 | callbacks.handleConnect()
55 | })
56 | }
57 | makeWs()
58 |
59 | const codec = changeProtocol.makeClientCodec({
60 | // We failed to send a message, so shut down the socket:
61 | handleError(err) {
62 | console.error('changeServer error:', err)
63 | ws.close()
64 | },
65 |
66 | async handleSend(text) {
67 | ws.send(text)
68 | },
69 |
70 | localMethods: {
71 | update(params) {
72 | callbacks.handleChange(params)
73 | },
74 | subLost(params) {
75 | callbacks.handleSubLost(params)
76 | }
77 | }
78 | })
79 |
80 | const out: ChangeServerConnection = {
81 | async subscribe(params) {
82 | return await codec.remoteMethods.subscribe(params)
83 | },
84 |
85 | async unsubscribe(params) {
86 | await codec.remoteMethods.unsubscribe(params)
87 | },
88 |
89 | close() {
90 | ws.close()
91 | },
92 |
93 | connected: false
94 | }
95 | return out
96 | }
97 |
--------------------------------------------------------------------------------
/src/core/currency/change-server-protocol.ts:
--------------------------------------------------------------------------------
1 | import { asArray, asOptional, asString, asTuple, asValue } from 'cleaners'
2 |
3 | import { makeRpcProtocol } from '../../util/json-rpc'
4 |
5 | /**
6 | * A chain and address identifier, like `['bitcoin', '19z88q...']`
7 | */
8 | export type SubscribeParams = [
9 | pluginId: string,
10 | address: string,
11 |
12 | /**
13 | * Block height or similar.
14 | * Might be missing the first time we scan an address.
15 | */
16 | checkpoint?: string
17 | ]
18 |
19 | const asSubscribeParams = asTuple(
20 | asString, // pluginId
21 | asString, // address
22 | asOptional(asString) // checkpoint
23 | )
24 |
25 | export type SubscribeResult = ReturnType
26 | const asSubscribeResult = asValue(
27 | /** Subscribe failed; not supported */
28 | -1,
29 | /** Subscribe failed; some thing went wrong */
30 | 0,
31 | /** Subscribe succeeded, no changes */
32 | 1,
33 | /** Subscribed succeeded, changes present */
34 | 2
35 | )
36 |
37 | export const changeProtocol = makeRpcProtocol({
38 | serverMethods: {
39 | subscribe: {
40 | asParams: asArray(asSubscribeParams),
41 | asResult: asArray(asSubscribeResult)
42 | },
43 |
44 | unsubscribe: {
45 | asParams: asArray(asSubscribeParams),
46 | asResult: asValue(undefined)
47 | }
48 | },
49 |
50 | clientMethods: {
51 | update: {
52 | asParams: asSubscribeParams
53 | },
54 | subLost: {
55 | asParams: asSubscribeParams
56 | }
57 | }
58 | })
59 |
--------------------------------------------------------------------------------
/src/core/currency/currency-reducer.ts:
--------------------------------------------------------------------------------
1 | import { buildReducer, mapReducer } from 'redux-keto'
2 |
3 | import { RootAction } from '../actions'
4 | import { RootState } from '../root-reducer'
5 | import {
6 | currencyWalletReducer,
7 | CurrencyWalletState
8 | } from './wallet/currency-wallet-reducer'
9 |
10 | export interface CurrencyState {
11 | readonly currencyWalletIds: string[]
12 | readonly wallets: { [walletId: string]: CurrencyWalletState }
13 | }
14 |
15 | export const currency = buildReducer({
16 | currencyWalletIds(state, action, next): string[] {
17 | // Optimize the common case:
18 | if (next.accountIds.length === 1) {
19 | const id = next.accountIds[0]
20 | return next.accounts[id].activeWalletIds
21 | }
22 |
23 | const out: string[] = []
24 | for (const accountId of next.accountIds) {
25 | out.push(...next.accounts[accountId].activeWalletIds)
26 | }
27 | return out
28 | },
29 |
30 | wallets: mapReducer(
31 | currencyWalletReducer,
32 | (props: RootState) => props.currency.currencyWalletIds
33 | )
34 | })
35 |
--------------------------------------------------------------------------------
/src/core/currency/currency-selectors.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EdgeCurrencyInfo,
3 | EdgeCurrencyWallet,
4 | EdgeTokenMap
5 | } from '../../types/types'
6 | import { ApiInput, RootProps } from '../root-pixie'
7 |
8 | export function getCurrencyMultiplier(
9 | currencyInfo: EdgeCurrencyInfo,
10 | allTokens: EdgeTokenMap,
11 | currencyCode: string
12 | ): string {
13 | for (const denomination of currencyInfo.denominations) {
14 | if (denomination.name === currencyCode) {
15 | return denomination.multiplier
16 | }
17 | }
18 |
19 | for (const tokenId of Object.keys(allTokens)) {
20 | const token = allTokens[tokenId]
21 | for (const denomination of token.denominations) {
22 | if (denomination.name === currencyCode) {
23 | return denomination.multiplier
24 | }
25 | }
26 | }
27 |
28 | return '1'
29 | }
30 |
31 | export function waitForCurrencyWallet(
32 | ai: ApiInput,
33 | walletId: string
34 | ): Promise {
35 | const out: Promise = ai.waitFor(
36 | (props: RootProps): EdgeCurrencyWallet | undefined => {
37 | // If the wallet id doesn't even exist, bail out:
38 | if (props.state.currency.wallets[walletId] == null) {
39 | throw new Error(`Wallet id ${walletId} does not exist in this account`)
40 | }
41 |
42 | // Return the error if one exists:
43 | const { engineFailure } = props.state.currency.wallets[walletId]
44 | if (engineFailure != null) throw engineFailure
45 |
46 | // Return the API if that exists:
47 | if (props.output.currency.wallets[walletId] != null) {
48 | return props.output.currency.wallets[walletId].walletApi
49 | }
50 | }
51 | )
52 | return out
53 | }
54 |
--------------------------------------------------------------------------------
/src/core/currency/wallet/currency-wallet-export.ts:
--------------------------------------------------------------------------------
1 | import { EdgeTransaction } from '../../../types/types'
2 | import { ApiInput } from '../../root-pixie'
3 |
4 | export function dateFilter(
5 | tx: EdgeTransaction,
6 | afterDate: Date = new Date(0),
7 | beforeDate: Date = new Date()
8 | ): boolean {
9 | return (
10 | tx.date * 1000 >= afterDate.valueOf() &&
11 | tx.date * 1000 < beforeDate.valueOf()
12 | )
13 | }
14 |
15 | export function searchStringFilter(
16 | ai: ApiInput,
17 | tx: EdgeTransaction,
18 | searchString: string | undefined
19 | ): boolean {
20 | const currencyState = ai.props.state.currency
21 |
22 | if (searchString == null || searchString === '') return true
23 |
24 | // Sanitize search string
25 | let cleanString = searchString.toLowerCase().replace('.', '').replace(',', '')
26 | // Remove leading zeroes
27 | for (let i = 0; i < cleanString.length; i++) {
28 | if (cleanString[i] !== '0') {
29 | cleanString = cleanString.substring(i)
30 | break
31 | }
32 | }
33 |
34 | function checkNullTypeAndIndex(value: string | number): boolean {
35 | if (
36 | value == null ||
37 | (typeof value !== 'string' && typeof value !== 'number')
38 | )
39 | return false
40 | if (
41 | !value
42 | .toString()
43 | .toLowerCase()
44 | .replace('.', '')
45 | .replace(',', '')
46 | .includes(cleanString)
47 | )
48 | return false
49 | return true
50 | }
51 |
52 | if (checkNullTypeAndIndex(tx.nativeAmount)) return true
53 | if (tx.metadata != null) {
54 | const {
55 | category = '',
56 | name = '',
57 | notes = '',
58 | exchangeAmount = {}
59 | } = tx.metadata
60 | const txCurrencyWalletState =
61 | tx.walletId != null ? currencyState.wallets[tx.walletId] : undefined
62 | if (
63 | checkNullTypeAndIndex(category) ||
64 | checkNullTypeAndIndex(name) ||
65 | checkNullTypeAndIndex(notes) ||
66 | (txCurrencyWalletState != null &&
67 | checkNullTypeAndIndex(exchangeAmount[txCurrencyWalletState.fiat]))
68 | )
69 | return true
70 | }
71 | if (tx.swapData != null) {
72 | const { displayName = '', pluginId = '' } = tx.swapData.plugin
73 | if (checkNullTypeAndIndex(displayName) || checkNullTypeAndIndex(pluginId))
74 | return true
75 | }
76 | const action = tx.savedAction ?? tx.chainAction
77 |
78 | if (action != null) {
79 | if (action.actionType === 'swap') {
80 | const { pluginId: destPluginId } = action.toAsset
81 | const { pluginId: sourcePluginId } = action.fromAsset
82 | const { displayName, supportEmail } = action.swapInfo
83 | if (
84 | checkNullTypeAndIndex(sourcePluginId) ||
85 | checkNullTypeAndIndex(destPluginId) ||
86 | checkNullTypeAndIndex(displayName) ||
87 | checkNullTypeAndIndex(supportEmail)
88 | )
89 | return true
90 | }
91 | }
92 | if (tx.spendTargets != null) {
93 | for (const target of tx.spendTargets) {
94 | const { publicAddress = '', memo = '' } = target
95 | if (checkNullTypeAndIndex(publicAddress) || checkNullTypeAndIndex(memo))
96 | return true
97 | }
98 | }
99 | if (tx.ourReceiveAddresses.length > 0) {
100 | for (const address of tx.ourReceiveAddresses) {
101 | if (checkNullTypeAndIndex(address)) return true
102 | }
103 | }
104 | if (checkNullTypeAndIndex(tx.txid)) return true
105 | return false
106 | }
107 |
--------------------------------------------------------------------------------
/src/core/currency/wallet/enabled-tokens.ts:
--------------------------------------------------------------------------------
1 | import { EdgeCurrencyInfo, EdgeTokenMap } from '../../../types/types'
2 |
3 | function flipTokenMap(tokens: EdgeTokenMap): {
4 | [currencyCode: string]: string
5 | } {
6 | const out: { [currencyCode: string]: string } = {}
7 | for (const tokenId of Object.keys(tokens)) {
8 | const token = tokens[tokenId]
9 | out[token.currencyCode] = tokenId
10 | }
11 | return out
12 | }
13 |
14 | export function currencyCodesToTokenIds(
15 | builtinTokens: EdgeTokenMap = {},
16 | customTokens: EdgeTokenMap = {},
17 | currencyInfo: EdgeCurrencyInfo,
18 | currencyCodes: string[]
19 | ): string[] {
20 | const builtinIds = flipTokenMap(builtinTokens)
21 | const customIds = flipTokenMap(customTokens)
22 |
23 | const out: string[] = []
24 | for (const currencyCode of currencyCodes) {
25 | const tokenId = customIds[currencyCode] ?? builtinIds[currencyCode]
26 | if (tokenId != null) out.push(tokenId)
27 | }
28 | return out
29 | }
30 |
31 | export function tokenIdsToCurrencyCodes(
32 | builtinTokens: EdgeTokenMap = {},
33 | customTokens: EdgeTokenMap = {},
34 | currencyInfo: EdgeCurrencyInfo,
35 | tokenIds: string[]
36 | ): string[] {
37 | const out: string[] = []
38 | for (const tokenId of tokenIds) {
39 | const token = customTokens[tokenId] ?? builtinTokens[tokenId]
40 | if (token != null) out.push(token.currencyCode)
41 | }
42 | return out
43 | }
44 |
45 | /**
46 | * Returns the unique items of an array,
47 | * optionally removing the items in `omit`.
48 | */
49 | export function uniqueStrings(array: string[], omit: string[] = []): string[] {
50 | const table = new Set(omit)
51 |
52 | const out: string[] = []
53 | for (const item of array) {
54 | if (table.has(item)) continue
55 | table.add(item)
56 | out.push(item)
57 | }
58 | return out
59 | }
60 |
--------------------------------------------------------------------------------
/src/core/currency/wallet/max-spend.ts:
--------------------------------------------------------------------------------
1 | import { add, div, lte, sub } from 'biggystring'
2 |
3 | import {
4 | EdgeCurrencyEngine,
5 | EdgeCurrencyPlugin,
6 | EdgeSpendInfo,
7 | EdgeTokenMap,
8 | EdgeWalletInfo
9 | } from '../../../browser'
10 | import { upgradeCurrencyCode } from '../../../types/type-helpers'
11 | import { upgradeMemos } from './upgrade-memos'
12 |
13 | export const getMaxSpendableInner = async (
14 | spendInfo: EdgeSpendInfo,
15 | plugin: EdgeCurrencyPlugin,
16 | engine: EdgeCurrencyEngine,
17 | allTokens: EdgeTokenMap,
18 | walletInfo: EdgeWalletInfo
19 | ): Promise => {
20 | spendInfo = upgradeMemos(spendInfo, plugin.currencyInfo)
21 | // Figure out which asset this is:
22 | const upgradedCurrency = upgradeCurrencyCode({
23 | allTokens,
24 | currencyInfo: plugin.currencyInfo,
25 | tokenId: spendInfo.tokenId
26 | })
27 |
28 | const unsafeMakeSpend = plugin.currencyInfo.unsafeMakeSpend ?? false
29 |
30 | if (typeof engine.getMaxSpendable === 'function') {
31 | // Only provide wallet info if currency requires it:
32 | const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined
33 |
34 | return await engine.getMaxSpendable(
35 | { ...spendInfo, ...upgradedCurrency },
36 | { privateKeys }
37 | )
38 | }
39 |
40 | const { networkFeeOption, customNetworkFee } = spendInfo
41 | const balance = engine.getBalance(upgradedCurrency)
42 |
43 | // Copy all the spend targets, setting the amounts to 0
44 | // but keeping all other information so we can get accurate fees:
45 | const spendTargets = spendInfo.spendTargets.map(spendTarget => {
46 | return { ...spendTarget, nativeAmount: '0' }
47 | })
48 |
49 | // The range of possible values includes `min`, but not `max`.
50 | function getMax(min: string, max: string): Promise {
51 | const diff = sub(max, min)
52 | if (lte(diff, '1')) {
53 | return Promise.resolve(min)
54 | }
55 | const mid = add(min, div(diff, '2'))
56 |
57 | // Try the average:
58 | spendTargets[0].nativeAmount = mid
59 |
60 | // Only provide wallet info if currency requires it:
61 | const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined
62 |
63 | return engine
64 | .makeSpend(
65 | {
66 | ...upgradedCurrency,
67 | spendTargets,
68 | networkFeeOption,
69 | customNetworkFee
70 | },
71 | { privateKeys }
72 | )
73 | .then(() => getMax(mid, max))
74 | .catch(() => getMax(min, mid))
75 | }
76 |
77 | return await getMax('0', add(balance, '1'))
78 | }
79 |
--------------------------------------------------------------------------------
/src/core/currency/wallet/metadata.ts:
--------------------------------------------------------------------------------
1 | import { asNumber, asObject, asOptional, asString, Cleaner } from 'cleaners'
2 |
3 | import { EdgeMetadata, EdgeMetadataChange } from '../../../types/types'
4 |
5 | export const asEdgeMetadata: Cleaner = raw => {
6 | const clean = asDiskMetadata(raw)
7 | const { exchangeAmount = {} } = clean
8 |
9 | // Delete corrupt amounts that exceed the Javascript number range:
10 | for (const fiat of Object.keys(exchangeAmount)) {
11 | if (String(exchangeAmount[fiat]).includes('e')) {
12 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
13 | delete exchangeAmount[fiat]
14 | }
15 | }
16 |
17 | return clean
18 | }
19 |
20 | export function mergeMetadata(
21 | under: EdgeMetadata,
22 | over: EdgeMetadata | EdgeMetadataChange
23 | ): EdgeMetadata {
24 | const out: EdgeMetadata = { exchangeAmount: {} }
25 | const { exchangeAmount = {} } = out
26 |
27 | // Merge the fiat amounts:
28 | const underAmounts = under.exchangeAmount ?? {}
29 | const overAmounts = over.exchangeAmount ?? {}
30 | for (const fiat of Object.keys(underAmounts)) {
31 | if (overAmounts[fiat] !== null) exchangeAmount[fiat] = underAmounts[fiat]
32 | }
33 | for (const fiat of Object.keys(overAmounts)) {
34 | const amount = overAmounts[fiat]
35 | if (amount != null) exchangeAmount[fiat] = amount
36 | }
37 |
38 | // Merge simple fields:
39 | if (over.bizId !== null) out.bizId = over.bizId ?? under.bizId
40 | if (over.category !== null) out.category = over.category ?? under.category
41 | if (over.name !== null) out.name = over.name ?? under.name
42 | if (over.notes !== null) out.notes = over.notes ?? under.notes
43 |
44 | return out
45 | }
46 |
47 | const asDiskMetadata = asObject({
48 | bizId: asOptional(asNumber),
49 | category: asOptional(asString),
50 | exchangeAmount: asOptional(asObject(asNumber)),
51 | name: asOptional(asString),
52 | notes: asOptional(asString)
53 | })
54 |
--------------------------------------------------------------------------------
/src/core/currency/wallet/upgrade-memos.ts:
--------------------------------------------------------------------------------
1 | import { EdgeCurrencyInfo, EdgeMemo, EdgeSpendInfo } from '../../../types/types'
2 |
3 | /**
4 | * Upgrades the memo fields inside an EdgeSpendTarget,
5 | * so any combination of legacy or modern apps or plugins will work.
6 | */
7 | export function upgradeMemos(
8 | spendInfo: EdgeSpendInfo,
9 | currencyInfo: EdgeCurrencyInfo
10 | ): EdgeSpendInfo {
11 | const legacyMemos: EdgeMemo[] = []
12 |
13 | // If this chain supports legacy memos, grab those:
14 | const { memoType } = currencyInfo
15 | if (memoType === 'hex' || memoType === 'number' || memoType === 'text') {
16 | for (const target of spendInfo.spendTargets) {
17 | if (target.memo != null) {
18 | legacyMemos.push({
19 | type: memoType,
20 | value: target.memo
21 | })
22 | } else if (target.uniqueIdentifier != null) {
23 | legacyMemos.push({
24 | type: memoType,
25 | value: target.uniqueIdentifier
26 | })
27 | } else if (typeof target.otherParams?.uniqueIdentifier === 'string') {
28 | legacyMemos.push({
29 | type: memoType,
30 | value: target.otherParams.uniqueIdentifier
31 | })
32 | }
33 | }
34 | }
35 |
36 | // We need to support 0x prefixes for backwards compatibility:
37 | for (const memo of legacyMemos) {
38 | if (memo.type === 'hex') memo.value = memo.value.replace(/^0x/i, '')
39 | }
40 |
41 | // Make a modern, legacy-free spend target:
42 | const out: EdgeSpendInfo = {
43 | ...spendInfo,
44 |
45 | // Delete any legacy memo fields:
46 | spendTargets: spendInfo.spendTargets.map(target => ({
47 | ...target,
48 | memo: undefined,
49 | uniqueIdentifier: undefined
50 | })),
51 |
52 | // Only use the legacy memos if new ones are missing:
53 | memos: spendInfo.memos ?? legacyMemos
54 | }
55 |
56 | // If we have exactly one memo, copy it to the legacy fields
57 | // to support out-dated currency plugins:
58 | if (out.memos?.length === 1 && out.spendTargets.length >= 1) {
59 | const [memo] = out.memos
60 | if (memo.type === currencyInfo.memoType) {
61 | const [target] = out.spendTargets
62 | target.memo = memo.value
63 | target.uniqueIdentifier = memo.value
64 | target.otherParams = {
65 | ...target.otherParams,
66 | uniqueIdenfitifer: memo.value
67 | }
68 | }
69 | }
70 |
71 | return out
72 | }
73 |
--------------------------------------------------------------------------------
/src/core/fake/fake-io.ts:
--------------------------------------------------------------------------------
1 | import { makeMemoryDisklet } from 'disklet'
2 |
3 | import {
4 | EdgeFetchFunction,
5 | EdgeIo,
6 | EdgeRandomFunction
7 | } from '../../types/types'
8 | import { scrypt } from '../../util/crypto/scrypt'
9 |
10 | /**
11 | * Generates deterministic "random" data for unit-testing.
12 | */
13 | function makeFakeRandom(): EdgeRandomFunction {
14 | let seed = 0
15 |
16 | return (bytes: number) => {
17 | const out = new Uint8Array(bytes)
18 |
19 | for (let i = 0; i < bytes; ++i) {
20 | // Simplest numbers that give a full-period generator with
21 | // a good mix of high & low values within the first few bytes:
22 | seed = (5 * seed + 3) & 0xff
23 | out[i] = seed
24 | }
25 |
26 | return out
27 | }
28 | }
29 |
30 | const fakeFetch: EdgeFetchFunction = () => {
31 | return Promise.reject(new Error('Fake network error'))
32 | }
33 |
34 | /**
35 | * Creates a simulated io context object.
36 | */
37 | export function makeFakeIo(): EdgeIo {
38 | const out: EdgeIo = {
39 | // Crypto:
40 | random: makeFakeRandom(),
41 | scrypt,
42 |
43 | // Local io:
44 | disklet: makeMemoryDisklet(),
45 |
46 | // Networking:
47 | fetch: fakeFetch,
48 | fetchCors: fakeFetch
49 | }
50 | return out
51 | }
52 |
--------------------------------------------------------------------------------
/src/core/log/log.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EdgeCrashReporter,
3 | EdgeLog,
4 | EdgeLogEvent,
5 | EdgeLogMethod,
6 | EdgeLogSettings,
7 | EdgeOnLog
8 | } from '../../types/types'
9 | import { addHiddenProperties } from '../../util/util'
10 |
11 | export interface LogBackend {
12 | crashReporter?: EdgeCrashReporter
13 | onLog: EdgeOnLog
14 | }
15 |
16 | function makeLogMethod(
17 | onLog: EdgeOnLog,
18 | type: EdgeLogEvent['type'],
19 | source: string
20 | ): EdgeLogMethod {
21 | return function log() {
22 | let message = ''
23 | for (let i = 0; i < arguments.length; ++i) {
24 | const arg = arguments[i]
25 | if (i > 0) message += ' '
26 | if (typeof arg === 'string') message += arg
27 | else if (arg instanceof Error) message += String(arg)
28 | else message += JSON.stringify(arg, null, 2)
29 | }
30 |
31 | onLog({ message, source, time: new Date(), type })
32 | }
33 | }
34 |
35 | export function defaultOnLog(event: EdgeLogEvent): void {
36 | const prettyDate = event.time
37 | .toISOString()
38 | .replace(/.*(\d\d-\d\d)T(\d\d:\d\d:\d\d).*/, '$1 $2')
39 | console.info(`${prettyDate} ${event.source}: ${event.message}`)
40 | }
41 |
42 | export function filterLogs(
43 | backend: LogBackend,
44 | getSettings: () => EdgeLogSettings
45 | ): LogBackend {
46 | function onLog(event: EdgeLogEvent): void {
47 | const { sources, defaultLogLevel } = getSettings()
48 |
49 | const logLevel =
50 | sources[event.source] != null ? sources[event.source] : defaultLogLevel
51 |
52 | switch (event.type) {
53 | case 'info':
54 | if (logLevel === 'info') backend.onLog(event)
55 | break
56 | case 'warn':
57 | if (logLevel === 'info' || logLevel === 'warn') backend.onLog(event)
58 | break
59 | case 'error':
60 | if (logLevel !== 'silent') backend.onLog(event)
61 | break
62 | }
63 | }
64 | return { ...backend, onLog }
65 | }
66 |
67 | export function makeLog(backend: LogBackend, source: string): EdgeLog {
68 | const { onLog, crashReporter } = backend
69 |
70 | return addHiddenProperties(makeLogMethod(onLog, 'info', source), {
71 | breadcrumb(message: string, metadata: object) {
72 | const time = new Date()
73 | if (crashReporter != null) {
74 | crashReporter.logBreadcrumb({ message, metadata, source, time })
75 | } else {
76 | message = `${message} ${JSON.stringify(metadata, null, 2)}`
77 | onLog({ message, source, time, type: 'warn' })
78 | }
79 | },
80 | crash(error: unknown, metadata: object) {
81 | const time = new Date()
82 | if (crashReporter != null) {
83 | crashReporter.logCrash({ error, metadata, source, time })
84 | } else {
85 | const message = `${String(error)} ${JSON.stringify(metadata, null, 2)}`
86 | onLog({ message, source, time, type: 'error' })
87 | }
88 | },
89 | warn: makeLogMethod(onLog, 'warn', source),
90 | error: makeLogMethod(onLog, 'error', source)
91 | })
92 | }
93 |
--------------------------------------------------------------------------------
/src/core/login/airbitz-stashes.ts:
--------------------------------------------------------------------------------
1 | import { asCodec, asObject, asOptional, asString, Cleaner } from 'cleaners'
2 | import { justFolders, navigateDisklet } from 'disklet'
3 |
4 | import { fixUsername } from '../../client-side'
5 | import { asBase32, asEdgeBox, asEdgeSnrp } from '../../types/server-cleaners'
6 | import { EdgeIo } from '../../types/types'
7 | import { base58, utf8 } from '../../util/encoding'
8 | import { makeJsonFile } from '../../util/file-helpers'
9 | import { userIdSnrp } from '../scrypt/scrypt-selectors'
10 | import { LoginStash } from './login-stash'
11 |
12 | /**
13 | * Reads legacy Airbitz login stashes from disk.
14 | */
15 | export async function loadAirbitzStashes(
16 | io: EdgeIo,
17 | avoidUsernames: Set
18 | ): Promise {
19 | const out: LoginStash[] = []
20 |
21 | const paths = await io.disklet.list('Accounts').then(justFolders)
22 | for (const path of paths) {
23 | const folder = navigateDisklet(io.disklet, path)
24 | const [
25 | carePackage,
26 | loginPackage,
27 | otp,
28 | pin2Key,
29 | recovery2Key,
30 | usernameJson
31 | ] = await Promise.all([
32 | await carePackageFile.load(folder, 'CarePackage.json'),
33 | await loginPackageFile.load(folder, 'LoginPackage.json'),
34 | await otpFile.load(folder, 'OtpKey.json'),
35 | await pin2KeyFile.load(folder, 'Pin2Key.json'),
36 | await recovery2KeyFile.load(folder, 'Recovery2Key.json'),
37 | await usernameFile.load(folder, 'UserName.json')
38 | ])
39 |
40 | if (usernameJson == null) continue
41 | const username = fixUsername(usernameJson.userName)
42 | if (avoidUsernames.has(username)) continue
43 | const userId = await io.scrypt(
44 | utf8.parse(username),
45 | userIdSnrp.salt_hex,
46 | userIdSnrp.n,
47 | userIdSnrp.r,
48 | userIdSnrp.p,
49 | 32
50 | )
51 |
52 | // Assemble a modern stash object:
53 | const stash: LoginStash = {
54 | appId: '',
55 | loginId: userId,
56 | pendingVouchers: [],
57 | username
58 | }
59 | if (carePackage != null && loginPackage != null) {
60 | stash.passwordKeySnrp = carePackage.SNRP2
61 | stash.passwordBox = loginPackage.EMK_LP2
62 | stash.syncKeyBox = loginPackage.ESyncKey
63 | stash.passwordAuthBox = loginPackage.ELP1
64 | }
65 | if (otp != null) {
66 | stash.otpKey = otp.TOTP
67 | }
68 | if (pin2Key != null) {
69 | stash.pin2Key = pin2Key.pin2Key
70 | }
71 | if (recovery2Key != null) {
72 | stash.recovery2Key = recovery2Key.recovery2Key
73 | }
74 |
75 | out.push(stash)
76 | }
77 |
78 | return out
79 | }
80 |
81 | /**
82 | * A string of base58-encoded binary data.
83 | */
84 | const asBase58: Cleaner = asCodec(
85 | raw => base58.parse(asString(raw)),
86 | clean => base58.stringify(clean)
87 | )
88 |
89 | const carePackageFile = makeJsonFile(
90 | asObject({
91 | SNRP2: asEdgeSnrp, // passwordKeySnrp
92 | SNRP3: asOptional(asEdgeSnrp), // recoveryKeySnrp
93 | SNRP4: asOptional(asEdgeSnrp), // questionKeySnrp
94 | ERQ: asOptional(asEdgeBox) // questionBox
95 | })
96 | )
97 |
98 | const loginPackageFile = makeJsonFile(
99 | asObject({
100 | EMK_LP2: asEdgeBox, // passwordBox
101 | EMK_LRA3: asOptional(asEdgeBox), // recoveryBox
102 |
103 | ESyncKey: asEdgeBox, // syncKeyBox
104 | ELP1: asEdgeBox // passwordAuthBox
105 | })
106 | )
107 |
108 | const otpFile = makeJsonFile(asObject({ TOTP: asBase32 }))
109 | const pin2KeyFile = makeJsonFile(asObject({ pin2Key: asBase58 }))
110 | const recovery2KeyFile = makeJsonFile(asObject({ recovery2Key: asBase58 }))
111 | const usernameFile = makeJsonFile(asObject({ userName: asString }))
112 |
--------------------------------------------------------------------------------
/src/core/login/login-delete.ts:
--------------------------------------------------------------------------------
1 | import { ApiInput } from '../root-pixie'
2 | import { makeAuthJson } from './login'
3 | import { loginFetch } from './login-fetch'
4 | import { getStashById } from './login-selectors'
5 | import { LoginTree } from './login-types'
6 |
7 | /**
8 | * Deletes a login from the server.
9 | */
10 | export async function deleteLogin(
11 | ai: ApiInput,
12 | login: LoginTree
13 | ): Promise {
14 | const { stashTree } = getStashById(ai, login.loginId)
15 | await loginFetch(
16 | ai,
17 | 'POST',
18 | '/v2/login/delete',
19 | makeAuthJson(stashTree, login)
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/core/login/login-messages.ts:
--------------------------------------------------------------------------------
1 | import { base64 } from 'rfc4648'
2 |
3 | import { asMessagesPayload } from '../../types/server-cleaners'
4 | import { EdgeLoginMessage } from '../../types/types'
5 | import { ApiInput } from '../root-pixie'
6 | import { loginFetch } from './login-fetch'
7 |
8 | /**
9 | * Fetches any login-related messages for all the users on this device.
10 | */
11 | export async function fetchLoginMessages(
12 | ai: ApiInput
13 | ): Promise {
14 | const { stashes } = ai.props.state.login
15 |
16 | const loginMap: { [loginId: string]: string } = {} // loginId -> username
17 | const loginIds: Uint8Array[] = []
18 | for (const stash of stashes) {
19 | const { loginId, username } = stash
20 | if (username == null) continue
21 | loginMap[base64.stringify(loginId)] = username
22 | loginIds.push(loginId)
23 | }
24 |
25 | const request = {
26 | loginIds
27 | }
28 | const reply = await loginFetch(ai, 'POST', '/v2/messages', request)
29 | const out: EdgeLoginMessage[] = []
30 | for (const message of asMessagesPayload(reply)) {
31 | const { loginId, ...rest } = message
32 | const id = base64.stringify(loginId)
33 | const username = loginMap[id]
34 | if (username == null) continue
35 | out.push({ ...rest, loginId: id, username })
36 | }
37 | return out
38 | }
39 |
--------------------------------------------------------------------------------
/src/core/login/login-secret.ts:
--------------------------------------------------------------------------------
1 | import { wasChangeSecretPayload } from '../../types/server-cleaners'
2 | import { encrypt } from '../../util/crypto/crypto'
3 | import { ApiInput } from '../root-pixie'
4 | import { LoginKit, LoginTree } from './login-types'
5 |
6 | export function makeSecretKit(
7 | ai: ApiInput,
8 | login: Pick
9 | ): LoginKit {
10 | const { io } = ai.props
11 | const { loginId, loginKey } = login
12 |
13 | const loginAuth = io.random(32)
14 | const loginAuthBox = encrypt(io, loginAuth, loginKey)
15 |
16 | return {
17 | loginId,
18 | server: wasChangeSecretPayload({
19 | loginAuth,
20 | loginAuthBox
21 | }),
22 | serverPath: '/v2/login/secret',
23 | stash: {
24 | loginAuthBox
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/core/login/login-selectors.ts:
--------------------------------------------------------------------------------
1 | import { base64 } from 'rfc4648'
2 |
3 | import { verifyData } from '../../util/crypto/verify'
4 | import { ApiInput } from '../root-pixie'
5 | import { scrypt, userIdSnrp } from '../scrypt/scrypt-selectors'
6 | import { searchTree } from './login'
7 | import { LoginStash } from './login-stash'
8 | import { StashLeaf } from './login-types'
9 |
10 | export function getEmptyStash(username?: string): LoginStash {
11 | return {
12 | username,
13 | appId: '',
14 | loginId: new Uint8Array(0),
15 | pendingVouchers: []
16 | }
17 | }
18 |
19 | /**
20 | * Finds the login stash for the given username.
21 | */
22 | export function getStashByUsername(
23 | ai: ApiInput,
24 | username: string
25 | ): LoginStash | undefined {
26 | const { stashes } = ai.props.state.login
27 | for (const stash of stashes) {
28 | if (stash.username === username) return stash
29 | }
30 | }
31 |
32 | export function getStashById(ai: ApiInput, loginId: Uint8Array): StashLeaf {
33 | const { stashes } = ai.props.state.login
34 | for (const stashTree of stashes) {
35 | const stash = searchTree(stashTree, stash =>
36 | verifyData(stash.loginId, loginId)
37 | )
38 | if (stash != null) return { stashTree, stash }
39 | }
40 | throw new Error(`Cannot find stash '${base64.stringify(loginId)}'`)
41 | }
42 |
43 | export function getChildStash(
44 | stashTree: LoginStash,
45 | loginId: Uint8Array
46 | ): LoginStash {
47 | const stash = searchTree(stashTree, stash =>
48 | verifyData(stash.loginId, loginId)
49 | )
50 | if (stash != null) return stash
51 | throw new Error(`Cannot find child stash '${base64.stringify(loginId)}'`)
52 | }
53 |
54 | // Hashed username cache:
55 | const userIdCache: { [username: string]: Promise } = {}
56 |
57 | /**
58 | * Hashes a username into a userId.
59 | */
60 | export function hashUsername(
61 | ai: ApiInput,
62 | username: string
63 | ): Promise {
64 | if (userIdCache[username] == null) {
65 | userIdCache[username] = scrypt(ai, username, userIdSnrp)
66 | }
67 | return userIdCache[username]
68 | }
69 |
--------------------------------------------------------------------------------
/src/core/login/login-types.ts:
--------------------------------------------------------------------------------
1 | import { asObject, asString, uncleaner } from 'cleaners'
2 |
3 | import {
4 | EdgePendingVoucher,
5 | EdgeWalletInfo,
6 | EdgeWalletInfoFull
7 | } from '../../types/types'
8 | import { asJsonObject } from '../../util/file-helpers'
9 | import { LoginStash } from './login-stash'
10 |
11 | /**
12 | * A key that decrypts a login stash.
13 | */
14 | export interface SessionKey {
15 | /** The login that this key belongs to. This may be a child login. */
16 | loginId: Uint8Array
17 |
18 | /** The decryption key. */
19 | loginKey: Uint8Array
20 | }
21 |
22 | /**
23 | * The login data decrypted into memory.
24 | * @deprecated Use `LoginStash` instead and decrypt it at the point of use.
25 | * This is an ongoing refactor to remove this type.
26 | */
27 | export interface LoginTree {
28 | isRoot: boolean
29 |
30 | // Identity:
31 | appId: string
32 | created?: Date
33 | lastLogin: Date
34 | loginId: Uint8Array
35 | loginKey: Uint8Array
36 |
37 | // 2-factor:
38 | otpKey?: Uint8Array
39 | otpResetDate?: Date
40 | otpTimeout?: number
41 | pendingVouchers: EdgePendingVoucher[]
42 |
43 | // Login methods:
44 | loginAuth?: Uint8Array
45 | passwordAuth?: Uint8Array
46 | pin?: string
47 | pin2Key?: Uint8Array
48 | recovery2Key?: Uint8Array
49 |
50 | // Username:
51 | userId?: Uint8Array
52 | username?: string
53 |
54 | // Resources:
55 | children: LoginTree[]
56 | }
57 |
58 | export type LoginType =
59 | | 'edgeLogin'
60 | | 'keyLogin'
61 | | 'newAccount'
62 | | 'passwordLogin'
63 | | 'pinLogin'
64 | | 'recoveryLogin'
65 |
66 | export interface LoginKit {
67 | /** The change will affect the node with this ID. */
68 | loginId: Uint8Array
69 |
70 | /**
71 | * The login-server payload that achieves the change.
72 | * Not all routes take a payload, such as the DELETE routes.
73 | */
74 | server: object | undefined
75 |
76 | /**
77 | * The login-server HTTP method that makes the change.
78 | * Defaults to "POST" if not present.
79 | */
80 | serverMethod?: string
81 | serverPath: string
82 |
83 | /**
84 | * A diff to apply to the stash tree, starting at the `loginId` node.
85 | * TODO: Update the login server to return a diff on every endpoint,
86 | * so we can get rid of this.
87 | */
88 | stash: Partial
89 | }
90 |
91 | /**
92 | * A stash for a specific child account,
93 | * along with its containing tree.
94 | */
95 | export interface StashLeaf {
96 | stash: LoginStash
97 | stashTree: LoginStash
98 | }
99 |
100 | export interface WalletInfoFullMap {
101 | [walletId: string]: EdgeWalletInfoFull
102 | }
103 |
104 | export const asEdgeWalletInfo = asObject({
105 | id: asString,
106 | keys: asJsonObject,
107 | type: asString
108 | })
109 |
110 | export const wasEdgeWalletInfo = uncleaner(asEdgeWalletInfo)
111 |
--------------------------------------------------------------------------------
/src/core/login/login-username.ts:
--------------------------------------------------------------------------------
1 | import { ChangeUsernameOptions } from '../../browser'
2 | import { wasChangeUsernamePayload } from '../../types/server-cleaners'
3 | import { encrypt } from '../../util/crypto/crypto'
4 | import { utf8 } from '../../util/encoding'
5 | import { ApiInput } from '../root-pixie'
6 | import { applyKits } from './login'
7 | import { hashUsername } from './login-selectors'
8 | import { LoginKit, LoginTree } from './login-types'
9 | import { makePasswordKit } from './password'
10 | import { makeChangePin2IdKit } from './pin2'
11 | import { makeChangeRecovery2IdKit } from './recovery2'
12 |
13 | export async function changeUsername(
14 | ai: ApiInput,
15 | accountId: string,
16 | opts: ChangeUsernameOptions
17 | ): Promise {
18 | const { loginTree } = ai.props.state.accounts[accountId]
19 |
20 | function makeKits(login: LoginTree): Array> {
21 | const out = [makeChangeUsernameKit(ai, login, opts)]
22 | for (const child of login.children) {
23 | out.push(...makeKits(child))
24 | }
25 | return out
26 | }
27 |
28 | const kits = await Promise.all(makeKits(loginTree))
29 | await applyKits(ai, loginTree, kits)
30 | }
31 |
32 | /**
33 | * Figures out which changes are needed to change a username,
34 | * and combines the necessary kits.
35 | */
36 | export async function makeChangeUsernameKit(
37 | ai: ApiInput,
38 | login: LoginTree,
39 | opts: ChangeUsernameOptions
40 | ): Promise {
41 | const { password, username } = opts
42 | const { isRoot, loginId, passwordAuth } = login
43 |
44 | // Validate our input:
45 | if (passwordAuth != null && password == null) {
46 | throw new Error('A password is required to change the username')
47 | }
48 |
49 | const pin2Kit = makeChangePin2IdKit(login, username)
50 | const recovery2Kit = makeChangeRecovery2IdKit(login, username)
51 | let passwordKit: LoginKit | undefined
52 | let usernameKit: LoginKit | undefined
53 |
54 | if (password != null) {
55 | passwordKit = await makePasswordKit(ai, login, username, password)
56 | }
57 | if (isRoot) {
58 | usernameKit = await makeUsernameKit(ai, login, username)
59 | }
60 |
61 | // Stop if we have no changes:
62 | if (
63 | passwordKit == null &&
64 | pin2Kit == null &&
65 | recovery2Kit == null &&
66 | usernameKit == null
67 | ) {
68 | return
69 | }
70 |
71 | return {
72 | loginId,
73 | server: {
74 | ...passwordKit?.server,
75 | ...pin2Kit?.server,
76 | ...recovery2Kit?.server,
77 | ...usernameKit?.server
78 | },
79 | serverPath: '/v2/login/username',
80 | stash: {
81 | ...passwordKit?.stash,
82 | ...pin2Kit?.stash,
83 | ...recovery2Kit?.stash,
84 | ...usernameKit?.stash
85 | }
86 | }
87 | }
88 |
89 | /**
90 | * Creates the values needed to set up a username.
91 | * This is only useful for root logins.
92 | */
93 | export async function makeUsernameKit(
94 | ai: ApiInput,
95 | login: LoginTree,
96 | username: string
97 | ): Promise {
98 | const { io } = ai.props
99 | const { loginId, loginKey } = login
100 |
101 | const userId = await hashUsername(ai, username)
102 |
103 | return {
104 | loginId,
105 | server: wasChangeUsernamePayload({
106 | userId,
107 | userTextBox: encrypt(io, utf8.parse(username), loginKey)
108 | }),
109 | serverPath: '',
110 | stash: { userId, username }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/core/login/storage-keys.ts:
--------------------------------------------------------------------------------
1 | import { asObject, uncleaner } from 'cleaners'
2 |
3 | import { asBase64 } from '../../browser'
4 | import { ApiInput } from '../root-pixie'
5 |
6 | export const asEdgeStorageKeys = asObject({
7 | dataKey: asBase64,
8 | syncKey: asBase64
9 | })
10 | export const wasEdgeStorageKeys = uncleaner(asEdgeStorageKeys)
11 | export type EdgeStorageKeys = ReturnType
12 |
13 | /**
14 | * Makes keys for accessing an encrypted Git repo.
15 | */
16 | export function createStorageKeys(ai: ApiInput): EdgeStorageKeys {
17 | const { io } = ai.props
18 | return {
19 | dataKey: io.random(32),
20 | syncKey: io.random(20)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/core/login/vouchers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | asLoginPayload,
3 | wasChangeVouchersPayload
4 | } from '../../types/server-cleaners'
5 | import { ChangeVouchersPayload } from '../../types/server-types'
6 | import { ApiInput } from '../root-pixie'
7 | import { applyLoginPayload, makeAuthJson } from './login'
8 | import { loginFetch } from './login-fetch'
9 | import { getStashById } from './login-selectors'
10 | import { saveStash } from './login-stash'
11 | import { LoginTree } from './login-types'
12 |
13 | /**
14 | * Approves or rejects vouchers on the server.
15 | */
16 | export async function changeVoucherStatus(
17 | ai: ApiInput,
18 | login: LoginTree,
19 | vouchers: ChangeVouchersPayload
20 | ): Promise {
21 | const { stashTree } = getStashById(ai, login.loginId)
22 | const reply = await loginFetch(ai, 'POST', '/v2/login/vouchers', {
23 | ...makeAuthJson(stashTree, login),
24 | data: wasChangeVouchersPayload(vouchers)
25 | })
26 | const newStashTree = applyLoginPayload(
27 | stashTree,
28 | login.loginKey,
29 | asLoginPayload(reply)
30 | )
31 | return await saveStash(ai, newStashTree)
32 | }
33 |
--------------------------------------------------------------------------------
/src/core/plugins/plugins-actions.ts:
--------------------------------------------------------------------------------
1 | import { navigateDisklet } from 'disklet'
2 | import { Dispatch } from 'redux'
3 |
4 | import {
5 | EdgeCorePlugin,
6 | EdgeCorePluginOptions,
7 | EdgeCorePlugins,
8 | EdgeCorePluginsInit,
9 | EdgeIo,
10 | EdgeNativeIo,
11 | EdgePluginMap
12 | } from '../../types/types'
13 | import { RootAction } from '../actions'
14 | import { InfoCacheFile } from '../context/info-cache-file'
15 | import { LogBackend, makeLog } from '../log/log'
16 |
17 | export interface PluginIos {
18 | io: EdgeIo
19 | nativeIo: EdgeNativeIo
20 | }
21 |
22 | type PluginsAddedWatcher = (plugins: EdgeCorePlugins) => void
23 | type PluginsLockedWatcher = () => void
24 |
25 | const allPlugins: EdgeCorePlugins = {}
26 | let allPluginsLocked: boolean = false
27 | const onPluginsAdded: PluginsAddedWatcher[] = []
28 | const onPluginsLocked: PluginsLockedWatcher[] = []
29 |
30 | /**
31 | * Adds plugins to the core.
32 | */
33 | export function addEdgeCorePlugins(plugins: EdgeCorePlugins): void {
34 | if (allPluginsLocked) {
35 | throw new Error('The Edge core plugin list has already been locked')
36 | }
37 |
38 | // Save the new plugins:
39 | for (const pluginId of Object.keys(plugins)) {
40 | allPlugins[pluginId] = plugins[pluginId]
41 | }
42 |
43 | // Update already-booted contexts:
44 | for (const f of onPluginsAdded) f(plugins)
45 | }
46 |
47 | /**
48 | * Finalizes the core plugin list, so no further plugins are expected.
49 | */
50 | export function lockEdgeCorePlugins(): void {
51 | allPluginsLocked = true
52 | for (const f of onPluginsLocked) f()
53 | }
54 |
55 | /**
56 | * Subscribes a context object to the core plugin list.
57 | */
58 | export function watchPlugins(
59 | ios: PluginIos,
60 | infoCache: InfoCacheFile,
61 | logBackend: LogBackend,
62 | pluginsInit: EdgeCorePluginsInit,
63 | dispatch: Dispatch
64 | ): () => void {
65 | const { io, nativeIo } = ios
66 | const legacyIo = { ...io, console }
67 |
68 | function pluginsAdded(plugins: EdgeCorePlugins): void {
69 | const out: EdgePluginMap = {}
70 |
71 | for (const pluginId of Object.keys(plugins)) {
72 | const plugin = plugins[pluginId]
73 | const log = makeLog(logBackend, pluginId)
74 | const initOptions = pluginsInit[pluginId]
75 | if (initOptions === false || initOptions == null) continue
76 |
77 | // Figure out what kind of object this is:
78 | try {
79 | if (typeof plugin === 'function') {
80 | const opts: EdgeCorePluginOptions = {
81 | infoPayload: infoCache.corePlugins?.[pluginId] ?? {},
82 | initOptions: typeof initOptions === 'object' ? initOptions : {},
83 | io: legacyIo,
84 | log,
85 | nativeIo,
86 | pluginDisklet: navigateDisklet(io.disklet, 'plugins/' + pluginId)
87 | }
88 | out[pluginId] = plugin(opts)
89 | } else if (typeof plugin === 'object' && plugin != null) {
90 | out[pluginId] = plugin
91 | } else {
92 | throw new TypeError(
93 | `Plugins must be functions or objects, got ${typeof plugin}`
94 | )
95 | }
96 | } catch (error: unknown) {
97 | // Show the error but keep going:
98 | log.error(error)
99 | }
100 | }
101 |
102 | dispatch({ type: 'CORE_PLUGINS_ADDED', payload: out })
103 | }
104 |
105 | function pluginsLocked(): void {
106 | dispatch({ type: 'CORE_PLUGINS_LOCKED', payload: pluginsInit })
107 | }
108 |
109 | // Add any plugins currently available:
110 | pluginsAdded(allPlugins)
111 | if (allPluginsLocked) pluginsLocked()
112 |
113 | // Save the callbacks:
114 | onPluginsAdded.push(pluginsAdded)
115 | onPluginsLocked.push(pluginsLocked)
116 |
117 | return () => {
118 | onPluginsAdded.filter(f => f !== pluginsAdded)
119 | onPluginsLocked.filter(f => f !== pluginsLocked)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/core/plugins/plugins-reducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EdgeCorePluginsInit,
3 | EdgeCurrencyPlugin,
4 | EdgeCurrencyTools,
5 | EdgePluginMap,
6 | EdgeSwapPlugin
7 | } from '../../types/types'
8 | import { RootAction } from '../actions'
9 |
10 | export interface PluginsState {
11 | readonly init: EdgeCorePluginsInit
12 | readonly locked: boolean
13 |
14 | readonly currency: EdgePluginMap
15 | readonly swap: EdgePluginMap
16 |
17 | readonly currencyTools: EdgePluginMap>
18 | }
19 |
20 | const initialState: PluginsState = {
21 | init: {},
22 | locked: false,
23 | currency: {},
24 | swap: {},
25 | currencyTools: {}
26 | }
27 |
28 | export const plugins = (
29 | state: PluginsState = initialState,
30 | action: RootAction
31 | ): PluginsState => {
32 | switch (action.type) {
33 | case 'CORE_PLUGINS_ADDED': {
34 | const out = {
35 | ...state,
36 | currency: { ...state.currency },
37 | swap: { ...state.swap }
38 | }
39 | for (const pluginId of Object.keys(action.payload)) {
40 | const plugin = action.payload[pluginId]
41 |
42 | // Don't stop loading the bundle if plugin(s) fail to load. Some plugins may rely on advanced features
43 | // that aren't locally available so we can skip loading those but should continue and load what we can.
44 | if (plugin == null) {
45 | out.init = { ...out.init, [pluginId]: false }
46 | continue
47 | }
48 |
49 | if ('currencyInfo' in plugin) {
50 | // Update the currencyInfo display names, if necessary
51 | if (plugin.currencyInfo.chainDisplayName == null) {
52 | plugin.currencyInfo.chainDisplayName =
53 | plugin.currencyInfo.displayName
54 | }
55 | if (plugin.currencyInfo.assetDisplayName == null) {
56 | plugin.currencyInfo.assetDisplayName =
57 | plugin.currencyInfo.displayName
58 | }
59 |
60 | out.currency[pluginId] = plugin
61 | }
62 | if ('swapInfo' in plugin) out.swap[pluginId] = plugin
63 | }
64 | return out
65 | }
66 | case 'CORE_PLUGINS_LOCKED':
67 | return { ...state, locked: true }
68 | case 'CURRENCY_TOOLS_LOADED': {
69 | const currencyTools = { ...state.currencyTools }
70 | currencyTools[action.payload.pluginId] = action.payload.tools
71 | return { ...state, currencyTools }
72 | }
73 | case 'INIT':
74 | return { ...state, init: action.payload.pluginsInit }
75 | }
76 | return state
77 | }
78 |
--------------------------------------------------------------------------------
/src/core/plugins/plugins-selectors.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EdgeCurrencyPlugin,
3 | EdgeCurrencyTools,
4 | EdgePluginMap
5 | } from '../../types/types'
6 | import { ApiInput, RootProps } from '../root-pixie'
7 |
8 | /**
9 | * Finds the currency plugin that can handle a particular wallet type,
10 | * or throws an error if there is none.
11 | */
12 | export function findCurrencyPluginId(
13 | plugins: EdgePluginMap,
14 | walletType: string
15 | ): string {
16 | const pluginId = maybeFindCurrencyPluginId(plugins, walletType)
17 | if (pluginId == null) {
18 | throw new Error(
19 | `Cannot find a currency plugin for wallet type ${walletType}`
20 | )
21 | }
22 | return pluginId
23 | }
24 |
25 | /**
26 | * Finds the currency plugin that can handle a particular wallet type,
27 | * or `undefined` if there is none.
28 | */
29 | export function maybeFindCurrencyPluginId(
30 | plugins: EdgePluginMap,
31 | walletType: string
32 | ): string | undefined {
33 | return Object.keys(plugins).find(
34 | pluginId => plugins[pluginId].currencyInfo.walletType === walletType
35 | )
36 | }
37 |
38 | /**
39 | * Finds the currency tools for a particular wallet type,
40 | * loading them if needed.
41 | */
42 | export function getCurrencyTools(
43 | ai: ApiInput,
44 | pluginId: string
45 | ): Promise {
46 | const { dispatch, state } = ai.props
47 |
48 | // Already loaded / loading:
49 | const tools = state.plugins.currencyTools[pluginId]
50 | if (tools != null) return tools
51 |
52 | // Never touched, so start the load:
53 | const plugin = state.plugins.currency[pluginId]
54 | const promise = plugin.makeCurrencyTools()
55 | dispatch({
56 | type: 'CURRENCY_TOOLS_LOADED',
57 | payload: { pluginId, tools: promise }
58 | })
59 | return promise
60 | }
61 |
62 | /**
63 | * Waits for the plugins to load,
64 | * then validates that all plugins are present.
65 | */
66 | export async function waitForPlugins(ai: ApiInput): Promise {
67 | await ai.waitFor((props: RootProps): true | undefined => {
68 | const { init, locked } = props.state.plugins
69 | if (!locked) return
70 |
71 | const { currency, swap } = props.state.plugins
72 | const missingPlugins: string[] = []
73 | for (const pluginId of Object.keys(init)) {
74 | const shouldLoad = init[pluginId] !== false && init[pluginId] != null
75 | if (shouldLoad && currency[pluginId] == null && swap[pluginId] == null) {
76 | missingPlugins.push(pluginId)
77 | }
78 | }
79 | if (missingPlugins.length > 0) {
80 | throw new Error(
81 | 'The following plugins are missing or failed to load: ' +
82 | missingPlugins.join(', ')
83 | )
84 | }
85 |
86 | return true
87 | })
88 | }
89 |
--------------------------------------------------------------------------------
/src/core/root-pixie.ts:
--------------------------------------------------------------------------------
1 | import { SyncClient } from 'edge-sync-client'
2 | import { combinePixies, PixieInput, ReduxProps, TamePixie } from 'redux-pixies'
3 |
4 | import { EdgeIo, EdgeLog } from '../types/types'
5 | import { AccountOutput, accounts } from './account/account-pixie'
6 | import { Dispatch } from './actions'
7 | import { context, ContextOutput } from './context/context-pixie'
8 | import { currency, CurrencyOutput } from './currency/currency-pixie'
9 | import { LogBackend } from './log/log'
10 | import { RootState } from './root-reducer'
11 | import { scrypt, ScryptOutput } from './scrypt/scrypt-pixie'
12 |
13 | // The top-level pixie output structure:
14 | export interface RootOutput {
15 | readonly accounts: { [accountId: string]: AccountOutput }
16 | readonly context: ContextOutput
17 | readonly currency: CurrencyOutput
18 | readonly scrypt: ScryptOutput
19 | }
20 |
21 | // Props passed to the root pixie:
22 | export interface RootProps extends ReduxProps {
23 | readonly close: () => void
24 | readonly io: EdgeIo
25 | readonly log: EdgeLog
26 | readonly logBackend: LogBackend
27 | readonly onError: (error: unknown) => void
28 | readonly output: RootOutput
29 | readonly syncClient: SyncClient
30 | }
31 |
32 | export type ApiInput = PixieInput
33 |
34 | /**
35 | * Downstream pixies take props that extend from `RootProps`,
36 | * so this casts those back down if necessary.
37 | */
38 | export const toApiInput = (input: PixieInput): ApiInput => input
39 |
40 | export const rootPixie: TamePixie = combinePixies({
41 | accounts,
42 | context,
43 | currency,
44 | scrypt
45 | })
46 |
--------------------------------------------------------------------------------
/src/core/scrypt/scrypt-selectors.ts:
--------------------------------------------------------------------------------
1 | import { EdgeSnrp } from '../../types/server-types'
2 | import { utf8 } from '../../util/encoding'
3 | import { ApiInput } from '../root-pixie'
4 |
5 | /**
6 | * Computes an SNRP value.
7 | */
8 | export function makeSnrp(
9 | ai: ApiInput,
10 | targetMs: number = 2000
11 | ): Promise {
12 | return ai.props.output.scrypt.makeSnrp(targetMs)
13 | }
14 |
15 | /**
16 | * Performs an scrypt derivation.
17 | */
18 | export async function scrypt(
19 | ai: ApiInput,
20 | data: Uint8Array | string,
21 | snrp: EdgeSnrp
22 | ): Promise {
23 | if (typeof data === 'string') data = utf8.parse(data)
24 |
25 | const value = await ai.props.output.scrypt.timeScrypt(data, snrp)
26 | return value.hash
27 | }
28 |
29 | export const userIdSnrp: EdgeSnrp = {
30 | salt_hex: Uint8Array.from([
31 | 0xb5, 0x86, 0x5f, 0xfb, 0x9f, 0xa7, 0xb3, 0xbf, 0xe4, 0xb2, 0x38, 0x4d,
32 | 0x47, 0xce, 0x83, 0x1e, 0xe2, 0x2a, 0x4a, 0x9d, 0x5c, 0x34, 0xc7, 0xef,
33 | 0x7d, 0x21, 0x46, 0x7c, 0xc7, 0x58, 0xf8, 0x1b
34 | ]),
35 | n: 16384,
36 | r: 1,
37 | p: 1
38 | }
39 |
--------------------------------------------------------------------------------
/src/core/storage/encrypt-disklet.ts:
--------------------------------------------------------------------------------
1 | import { Disklet, DiskletListing } from 'disklet'
2 | import { bridgifyObject } from 'yaob'
3 |
4 | import { asEdgeBox, wasEdgeBox } from '../../types/server-cleaners'
5 | import { EdgeIo } from '../../types/types'
6 | import { decrypt, decryptText, encrypt } from '../../util/crypto/crypto'
7 | import { utf8 } from '../../util/encoding'
8 |
9 | export function encryptDisklet(
10 | io: EdgeIo,
11 | dataKey: Uint8Array,
12 | disklet: Disklet
13 | ): Disklet {
14 | const out = {
15 | delete(path: string): Promise {
16 | return disklet.delete(path)
17 | },
18 |
19 | async getData(path: string): Promise {
20 | const text = await disklet.getText(path)
21 | const box = asEdgeBox(JSON.parse(text))
22 | return decrypt(box, dataKey)
23 | },
24 |
25 | async getText(path: string): Promise {
26 | const text = await disklet.getText(path)
27 | const box = asEdgeBox(JSON.parse(text))
28 | return decryptText(box, dataKey)
29 | },
30 |
31 | list(path?: string): Promise {
32 | return disklet.list(path)
33 | },
34 |
35 | setData(path: string, data: ArrayLike): Promise {
36 | return disklet.setText(
37 | path,
38 | JSON.stringify(wasEdgeBox(encrypt(io, Uint8Array.from(data), dataKey)))
39 | )
40 | },
41 |
42 | setText(path: string, text: string): Promise {
43 | return this.setData(path, utf8.parse(text))
44 | }
45 | }
46 | bridgifyObject(out)
47 | return out
48 | }
49 |
--------------------------------------------------------------------------------
/src/core/storage/storage-actions.ts:
--------------------------------------------------------------------------------
1 | import { bridgifyObject } from 'yaob'
2 |
3 | import { EdgeWalletInfo } from '../../types/types'
4 | import { asEdgeStorageKeys } from '../login/storage-keys'
5 | import { ApiInput } from '../root-pixie'
6 | import {
7 | loadRepoStatus,
8 | makeLocalDisklet,
9 | makeRepoPaths,
10 | syncRepo
11 | } from './repo'
12 | import { StorageWalletStatus } from './storage-reducer'
13 |
14 | export const SYNC_INTERVAL = 30 * 1000
15 |
16 | export async function addStorageWallet(
17 | ai: ApiInput,
18 | walletInfo: EdgeWalletInfo
19 | ): Promise {
20 | const { dispatch, io, onError } = ai.props
21 |
22 | const storageKeys = asEdgeStorageKeys(walletInfo.keys)
23 | const paths = makeRepoPaths(io, storageKeys)
24 | const localDisklet = makeLocalDisklet(io, walletInfo.id)
25 | bridgifyObject(localDisklet)
26 |
27 | const status: StorageWalletStatus = await loadRepoStatus(paths)
28 | dispatch({
29 | type: 'STORAGE_WALLET_ADDED',
30 | payload: {
31 | id: walletInfo.id,
32 | initialState: {
33 | localDisklet,
34 | paths,
35 | status,
36 | lastChanges: []
37 | }
38 | }
39 | })
40 |
41 | // If we have already done a sync, let this one run in the background:
42 | const syncPromise = syncStorageWallet(ai, walletInfo.id)
43 | if (status.lastSync > 0) {
44 | syncPromise.catch(error => {
45 | const { syncKey } = walletInfo.keys
46 | const { lastHash } = status
47 | ai.props.log.error(
48 | `Could not sync ${String(syncKey)} with last hash ${String(
49 | lastHash
50 | )}: ${String(error)}`
51 | )
52 | onError(error)
53 | })
54 | } else await syncPromise
55 | }
56 |
57 | export function syncStorageWallet(
58 | ai: ApiInput,
59 | walletId: string
60 | ): Promise {
61 | const { dispatch, syncClient, state } = ai.props
62 | const { paths, status } = state.storageWallets[walletId]
63 |
64 | return syncRepo(syncClient, paths, { ...status }).then(
65 | ({ changes, status }) => {
66 | dispatch({
67 | type: 'STORAGE_WALLET_SYNCED',
68 | payload: { id: walletId, changes: Object.keys(changes), status }
69 | })
70 | return Object.keys(changes)
71 | }
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/src/core/storage/storage-api.ts:
--------------------------------------------------------------------------------
1 | import { Disklet } from 'disklet'
2 |
3 | import { EdgeWalletInfo } from '../../types/types'
4 | import { ApiInput } from '../root-pixie'
5 | import { syncStorageWallet } from './storage-actions'
6 | import {
7 | getStorageWalletDisklet,
8 | getStorageWalletLocalDisklet
9 | } from './storage-selectors'
10 |
11 | export interface EdgeStorageWallet {
12 | readonly id: string
13 | readonly keys: object
14 | readonly type: string
15 | readonly disklet: Disklet
16 | readonly localDisklet: Disklet
17 | sync: () => Promise
18 | }
19 |
20 | export function makeStorageWalletApi(
21 | ai: ApiInput,
22 | walletInfo: EdgeWalletInfo
23 | ): EdgeStorageWallet {
24 | const { id, type, keys } = walletInfo
25 |
26 | return {
27 | // Broken-out key info:
28 | id,
29 | type,
30 | keys,
31 |
32 | // Folders:
33 | get disklet(): Disklet {
34 | return getStorageWalletDisklet(ai.props.state, id)
35 | },
36 |
37 | get localDisklet(): Disklet {
38 | return getStorageWalletLocalDisklet(ai.props.state, id)
39 | },
40 |
41 | async sync(): Promise {
42 | await syncStorageWallet(ai, id)
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/core/storage/storage-reducer.ts:
--------------------------------------------------------------------------------
1 | import { Disklet } from 'disklet'
2 | import { combineReducers } from 'redux'
3 |
4 | import { RootAction } from '../actions'
5 |
6 | export interface StorageWalletPaths {
7 | dataKey: Uint8Array
8 | syncKey: Uint8Array
9 |
10 | baseDisklet: Disklet
11 | changesDisklet: Disklet
12 | dataDisklet: Disklet
13 | disklet: Disklet
14 | }
15 |
16 | export interface StorageWalletStatus {
17 | lastHash: string | undefined
18 | lastSync: number
19 | }
20 |
21 | export interface StorageWalletState {
22 | lastChanges: string[]
23 | localDisklet: Disklet
24 | paths: StorageWalletPaths
25 | status: StorageWalletStatus
26 | }
27 |
28 | export interface StorageWalletsState {
29 | [id: string]: StorageWalletState
30 | }
31 |
32 | /**
33 | * Individual repo reducer.
34 | */
35 | const storageWalletReducer = combineReducers({
36 | lastChanges(state = [], action): string[] {
37 | if (action.type === 'STORAGE_WALLET_SYNCED') {
38 | const { changes } = action.payload
39 | return changes.length > 0 ? changes : state
40 | }
41 | return state
42 | },
43 |
44 | localDisklet(state: any = null): Disklet {
45 | return state
46 | },
47 |
48 | paths(state: any = null): StorageWalletPaths {
49 | return state
50 | },
51 |
52 | status(
53 | state = { lastSync: 0, lastHash: undefined },
54 | action
55 | ): StorageWalletStatus {
56 | return action.type === 'STORAGE_WALLET_SYNCED'
57 | ? action.payload.status
58 | : state
59 | }
60 | })
61 |
62 | /**
63 | * Repo list reducer.
64 | */
65 | export const storageWallets = function storageWalletsReducer(
66 | state: StorageWalletsState = {},
67 | action: RootAction
68 | ): StorageWalletsState {
69 | switch (action.type) {
70 | case 'STORAGE_WALLET_ADDED': {
71 | const { id, initialState } = action.payload
72 | const out: StorageWalletsState = { ...state }
73 | out[id] = storageWalletReducer(initialState, { type: 'UPDATE_NEXT' })
74 | return out
75 | }
76 |
77 | case 'STORAGE_WALLET_SYNCED': {
78 | const { id } = action.payload
79 | if (state[id] != null) {
80 | const out: StorageWalletsState = { ...state }
81 | out[id] = storageWalletReducer(state[id], action)
82 | return out
83 | }
84 | return state
85 | }
86 | }
87 | return state
88 | }
89 |
--------------------------------------------------------------------------------
/src/core/storage/storage-selectors.ts:
--------------------------------------------------------------------------------
1 | import { Disklet } from 'disklet'
2 |
3 | import { EdgeIo } from '../../types/types'
4 | import { hmacSha256 } from '../../util/crypto/hashes'
5 | import { base58, utf8 } from '../../util/encoding'
6 | import { RootState } from '../root-reducer'
7 | import { encryptDisklet } from './encrypt-disklet'
8 |
9 | export function getStorageWalletLastChanges(
10 | state: RootState,
11 | walletId: string
12 | ): string[] {
13 | return state.storageWallets[walletId].lastChanges
14 | }
15 |
16 | export function getStorageWalletDisklet(
17 | state: RootState,
18 | walletId: string
19 | ): Disklet {
20 | return state.storageWallets[walletId].paths.disklet
21 | }
22 |
23 | export function getStorageWalletLocalDisklet(
24 | state: RootState,
25 | walletId: string
26 | ): Disklet {
27 | return state.storageWallets[walletId].localDisklet
28 | }
29 |
30 | export function makeStorageWalletLocalEncryptedDisklet(
31 | state: RootState,
32 | walletId: string,
33 | io: EdgeIo
34 | ): Disklet {
35 | return encryptDisklet(
36 | io,
37 | state.storageWallets[walletId].paths.dataKey,
38 | state.storageWallets[walletId].localDisklet
39 | )
40 | }
41 |
42 | export function hashStorageWalletFilename(
43 | state: RootState,
44 | walletId: string,
45 | data: string
46 | ): string {
47 | const dataKey = state.storageWallets[walletId].paths.dataKey
48 | return base58.stringify(hmacSha256(utf8.parse(data), dataKey))
49 | }
50 |
--------------------------------------------------------------------------------
/src/globals.d.ts:
--------------------------------------------------------------------------------
1 | import { StoreEnhancer } from 'redux'
2 | import { Bridge } from 'yaob'
3 |
4 | import { NativeBridge } from './io/react-native/native-bridge'
5 | import { EdgeCorePlugins } from './types/types'
6 |
7 | interface EnhancerOptions {
8 | name?: string
9 | }
10 |
11 | declare global {
12 | interface Window {
13 | __REDUX_DEVTOOLS_EXTENSION__?: (config?: EnhancerOptions) => StoreEnhancer
14 |
15 | /**
16 | * Plugins call this to register themselves with the core.
17 | * We call `lockEdgeCorePlugins` ourselves on React Native.
18 | */
19 | addEdgeCorePlugins?: (plugins: EdgeCorePlugins) => void
20 |
21 | /**
22 | * Native code calls this bridge to pass back results from IO methods.
23 | */
24 | nativeBridge: NativeBridge
25 |
26 | /**
27 | * Native code calls this bridge to pass in messages from React Native.
28 | */
29 | reactBridge: Bridge
30 |
31 | /**
32 | * Our Java code injects this into the Android WebView,
33 | * allowing JavaScript to talk to native code.
34 | */
35 | edgeCore: {
36 | /**
37 | * Sends a message to the React Native component.
38 | */
39 | postMessage: (message: unknown) => void
40 |
41 | /**
42 | * Calls a native IO method.
43 | */
44 | call: (id: number, name: string, args: string) => void
45 | }
46 |
47 | /**
48 | * Our Swift code installs this message handler into the iOS WebView,
49 | * allowing JavaScript to talk to native code.
50 | */
51 | webkit: {
52 | messageHandlers: {
53 | edgeCore: {
54 | postMessage: (args: [number, string, unknown[]]) => void
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { makeLocalBridge } from 'yaob'
2 |
3 | import { makeContext, makeFakeWorld } from './core/core'
4 | import { defaultOnLog } from './core/log/log'
5 | import { hideProperties } from './io/hidden-properties'
6 | import { makeNodeIo } from './io/node/node-io'
7 | import {
8 | EdgeContext,
9 | EdgeContextOptions,
10 | EdgeFakeUser,
11 | EdgeFakeWorld,
12 | EdgeFakeWorldOptions
13 | } from './types/types'
14 |
15 | export { makeNodeIo }
16 | export {
17 | addEdgeCorePlugins,
18 | closeEdge,
19 | lockEdgeCorePlugins,
20 | makeFakeIo
21 | } from './core/core'
22 | export * from './types/types'
23 |
24 | export function makeEdgeContext(
25 | opts: EdgeContextOptions
26 | ): Promise {
27 | const { crashReporter, onLog = defaultOnLog, path = './edge' } = opts
28 | return makeContext(
29 | { io: makeNodeIo(path), nativeIo: {} },
30 | { crashReporter, onLog },
31 | opts
32 | )
33 | }
34 |
35 | export function makeFakeEdgeWorld(
36 | users: EdgeFakeUser[] = [],
37 | opts: EdgeFakeWorldOptions = {}
38 | ): Promise {
39 | const { crashReporter, onLog = defaultOnLog } = opts
40 | return Promise.resolve(
41 | makeLocalBridge(
42 | makeFakeWorld(
43 | { io: makeNodeIo('.'), nativeIo: {} },
44 | { crashReporter, onLog },
45 | users
46 | ),
47 | {
48 | cloneMessage: message => JSON.parse(JSON.stringify(message)),
49 | hideProperties
50 | }
51 | )
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/src/io/browser/browser-io.ts:
--------------------------------------------------------------------------------
1 | import { makeLocalStorageDisklet } from 'disklet'
2 |
3 | import { EdgeFetchOptions, EdgeFetchResponse, EdgeIo } from '../../types/types'
4 | import { scrypt } from '../../util/crypto/scrypt'
5 | import { fetchCorsProxy } from './fetch-cors-proxy'
6 |
7 | // Only try CORS proxy/bridge techniques up to 5 times
8 | const MAX_CORS_FAILURE_COUNT = 5
9 |
10 | // A map of domains that failed CORS and succeeded via the CORS proxy server
11 | const hostnameProxyWhitelist = new Set()
12 |
13 | // A map of domains that failed all CORS techniques and should not re-attempt CORS techniques
14 | const hostnameCorsProxyBlacklist = new Map()
15 |
16 | /**
17 | * Extracts the io functions we need from the browser.
18 | */
19 | export function makeBrowserIo(): EdgeIo {
20 | if (typeof window === 'undefined') {
21 | throw new Error('No `window` object')
22 | }
23 | if (window.crypto == null || window.crypto.getRandomValues == null) {
24 | throw new Error('No secure random number generator in this browser')
25 | }
26 |
27 | const io: EdgeIo = {
28 | // Crypto:
29 | random: size => {
30 | const out = new Uint8Array(size)
31 | window.crypto.getRandomValues(out)
32 | return out
33 | },
34 | scrypt,
35 |
36 | // Local io:
37 | disklet: makeLocalStorageDisklet(window.localStorage, {
38 | prefix: 'airbitz'
39 | }),
40 |
41 | // Networking:
42 | async fetch(
43 | uri: string,
44 | opts?: EdgeFetchOptions
45 | ): Promise {
46 | const { corsBypass = 'auto' } = opts ?? {}
47 |
48 | if (corsBypass === 'always') {
49 | return await fetchCorsProxy(uri, opts)
50 | }
51 | if (corsBypass === 'never') {
52 | return await window.fetch(uri, opts)
53 | }
54 |
55 | const { hostname } = new URL(uri)
56 | const corsFailureCount = hostnameCorsProxyBlacklist.get(hostname) ?? 0
57 |
58 | let doFetch = true
59 | const doFetchCors = true
60 |
61 | if (
62 | corsFailureCount < MAX_CORS_FAILURE_COUNT &&
63 | hostnameProxyWhitelist.has(hostname)
64 | ) {
65 | // Proactively use fetchCorsProxy for any hostnames added to whitelist:
66 | doFetch = false
67 | }
68 |
69 | let errorToThrow
70 | if (doFetch) {
71 | try {
72 | // Attempt regular fetch:
73 | return await window.fetch(uri, opts)
74 | } catch (error: unknown) {
75 | // If we exhaust attempts to use CORS-safe fetch, then throw the error:
76 | if (corsFailureCount >= MAX_CORS_FAILURE_COUNT) {
77 | throw error
78 | }
79 | errorToThrow = error
80 | }
81 | }
82 |
83 | if (doFetchCors) {
84 | try {
85 | const response = await fetchCorsProxy(uri, opts)
86 | hostnameProxyWhitelist.add(hostname)
87 | return response
88 | } catch (error: unknown) {
89 | if (errorToThrow == null) errorToThrow = error
90 | }
91 | }
92 |
93 | // We failed all CORS techniques, so track attempts
94 | hostnameCorsProxyBlacklist.set(hostname, corsFailureCount + 1)
95 |
96 | // Throw the error from the first fetch instead of the one from
97 | // proxy server.
98 | throw errorToThrow
99 | },
100 |
101 | async fetchCors(
102 | uri: string,
103 | opts?: EdgeFetchOptions
104 | ): Promise {
105 | return await io.fetch(uri, opts)
106 | }
107 | }
108 |
109 | return io
110 | }
111 |
--------------------------------------------------------------------------------
/src/io/browser/fetch-cors-proxy.ts:
--------------------------------------------------------------------------------
1 | import { EdgeFetchOptions } from '../../types/types'
2 | import { asyncWaterfall } from '../../util/asyncWaterfall'
3 | import { shuffle } from '../../util/shuffle'
4 |
5 | // Hard-coded CORS proxy server
6 | const PROXY_SERVER_URLS = [
7 | 'https://cors1.edge.app',
8 | 'https://cors2.edge.app',
9 | 'https://cors3.edge.app',
10 | 'https://cors4.edge.app'
11 | ]
12 |
13 | export const fetchCorsProxy = async (
14 | uri: string,
15 | opts?: EdgeFetchOptions
16 | ): Promise => {
17 | const shuffledUrls = shuffle([...PROXY_SERVER_URLS])
18 | const tasks = shuffledUrls.map(
19 | proxyServerUrl => async () =>
20 | await window.fetch(proxyServerUrl, {
21 | ...opts,
22 | headers: {
23 | ...opts?.headers,
24 | 'x-proxy-url': uri
25 | }
26 | })
27 | )
28 | return await asyncWaterfall(tasks)
29 | }
30 |
--------------------------------------------------------------------------------
/src/io/hidden-properties.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Properties we want YAOB to hide from `console.log` or `JSON.stringify`.
3 | */
4 | export const hideProperties: string[] = [
5 | 'allKeys',
6 | 'displayPrivateSeed',
7 | 'displayPublicSeed',
8 | 'keys',
9 | 'otpKey',
10 | 'loginKey',
11 | 'publicWalletInfo',
12 | 'recoveryKey'
13 | ]
14 |
--------------------------------------------------------------------------------
/src/io/node/node-io.ts:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto'
2 | import { makeNodeDisklet } from 'disklet'
3 | import fetch from 'node-fetch'
4 |
5 | import { EdgeIo } from '../../types/types'
6 | import { scrypt } from '../../util/crypto/scrypt'
7 |
8 | /**
9 | * Creates the io resources needed to run the Edge core on node.js.
10 | *
11 | * @param {string} path Location where data should be written to disk.
12 | */
13 | export function makeNodeIo(path: string): EdgeIo {
14 | return {
15 | // Crypto:
16 | random(bytes: number) {
17 | return Uint8Array.from(crypto.randomBytes(bytes))
18 | },
19 | scrypt,
20 |
21 | // Local io:
22 | disklet: makeNodeDisklet(path),
23 |
24 | // Networking:
25 | fetch,
26 | fetchCors: fetch
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/io/react-native/native-bridge.ts:
--------------------------------------------------------------------------------
1 | interface NativeMethods {
2 | diskletDelete: (path: string) => Promise
3 | diskletGetData: (path: string) => Promise // base64
4 | diskletGetText: (path: string) => Promise
5 | diskletList: (path: string) => Promise<{ [path: string]: 'file' | 'folder' }>
6 | diskletSetData: (path: string, data64: string) => Promise
7 | diskletSetText: (path: string, text: string) => Promise
8 |
9 | fetch: (
10 | url: string,
11 | method: string,
12 | headers: { [name: string]: string },
13 | body?: string,
14 | bodyIsBase64?: boolean
15 | ) => Promise<{
16 | status: number
17 | headers: { [name: string]: string }
18 | body: string
19 | bodyIsBase64: boolean
20 | }>
21 |
22 | randomBytes: (size: number) => Promise // base64
23 |
24 | scrypt: (
25 | data64: string,
26 | salt64: string,
27 | n: number,
28 | r: number,
29 | p: number,
30 | dklen: number
31 | ) => Promise // base64
32 | }
33 |
34 | export interface NativeBridge {
35 | call: (
36 | name: Name,
37 | ...args: Parameters
38 | ) => ReturnType
39 |
40 | // The native code uses this method to pass return values.
41 | resolve: (id: number, value: unknown) => void
42 |
43 | // The native code uses this method if a call fails.
44 | reject: (id: number, message: string) => void
45 | }
46 |
47 | export function makeNativeBridge(
48 | doCall: (id: number, name: string, args: unknown[]) => void
49 | ): NativeBridge {
50 | const list = makePendingList()
51 | return {
52 | call(name, ...args) {
53 | const promise = new Promise((resolve, reject) => {
54 | doCall(list.add({ resolve, reject }), name, args)
55 | })
56 | // TypeScript can't check our Java / Swift return values:
57 | return promise as any
58 | },
59 | resolve(id, value) {
60 | list.grab(id).resolve(value)
61 | },
62 | reject(id, message) {
63 | list.grab(id).reject(new Error(message))
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * A pending call into native code.
70 | */
71 | interface PendingCall {
72 | resolve: (value: unknown) => void
73 | reject: (error: Error) => void
74 | }
75 |
76 | /**
77 | * Maintains a list of pending native calls.
78 | */
79 | interface PendingList {
80 | add: (call: PendingCall) => number
81 | grab: (id: number) => PendingCall
82 | }
83 |
84 | function makePendingList(): PendingList {
85 | const dummyCall: PendingCall = { resolve() {}, reject() {} }
86 | let lastId: number = 0
87 |
88 | if (typeof Map !== 'undefined') {
89 | // Better map-based version:
90 | const map = new Map()
91 | return {
92 | add(call) {
93 | const id = ++lastId
94 | map.set(id, call)
95 | return id
96 | },
97 | grab(id) {
98 | const call = map.get(id)
99 | if (call == null) return dummyCall
100 | map.delete(id)
101 | return call
102 | }
103 | }
104 | }
105 |
106 | // Slower object-based version:
107 | const map: { [id: string]: PendingCall } = {}
108 | return {
109 | add(call) {
110 | const id = ++lastId
111 | map[String(id)] = call
112 | return id
113 | },
114 | grab(id) {
115 | const call = map[String(id)]
116 | if (call == null) return dummyCall
117 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
118 | delete map[String(id)]
119 | return call
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/io/react-native/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Object.assign
3 | */
4 | function assign(out: any): any {
5 | if (out == null) {
6 | throw new TypeError('Cannot convert undefined or null to object')
7 | }
8 | out = Object(out)
9 |
10 | for (let i = 1; i < arguments.length; ++i) {
11 | const from = arguments[i]
12 | if (from == null) continue
13 | for (const key of Object.keys(from)) out[key] = from[key]
14 | }
15 | return out
16 | }
17 |
18 | /**
19 | * Array.fill
20 | */
21 | function fill(this: any[], value: any, start?: number, end?: number): any[] {
22 | const length = this.length
23 | function clamp(endpoint: number): number {
24 | return endpoint < 0
25 | ? Math.max(length + endpoint, 0)
26 | : Math.min(endpoint, length)
27 | }
28 | const first = start != null ? clamp(start) : 0
29 | const last = end != null ? clamp(end) : length
30 |
31 | for (let i = first; i < last; ++i) {
32 | this[i] = value
33 | }
34 | return this
35 | }
36 |
37 | /**
38 | * Array.find
39 | */
40 | function find(
41 | this: any[],
42 | test: (value: any, i: number, array: any[]) => boolean,
43 | testThis?: any
44 | ): any {
45 | for (let i = 0; i < this.length; ++i) {
46 | const value = this[i]
47 | if (test.call(testThis, value, i, this)) {
48 | return value
49 | }
50 | }
51 | }
52 |
53 | /**
54 | * Array.includes
55 | */
56 | function includes(this: any[], target: any): boolean {
57 | return Array.prototype.indexOf.call(this, target) >= 0
58 | }
59 |
60 | /**
61 | * Adds a non-enumerable method to an object.
62 | */
63 | function safeAdd(object: any, name: string, value: any): void {
64 | if (typeof object[name] !== 'function') {
65 | Object.defineProperty(object, name, {
66 | configurable: true,
67 | writable: true,
68 | value
69 | })
70 | }
71 | }
72 |
73 | // Perform the polyfill:
74 | safeAdd(Object, 'assign', assign)
75 | safeAdd(Array.prototype, 'fill', fill)
76 | safeAdd(Array.prototype, 'find', find)
77 | safeAdd(Array.prototype, 'includes', includes)
78 | safeAdd(Uint8Array.prototype, 'fill', Array.prototype.fill)
79 |
--------------------------------------------------------------------------------
/src/io/react-native/react-native-types.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { LogBackend } from '../../core/log/log'
4 | import {
5 | EdgeContext,
6 | EdgeContextOptions,
7 | EdgeFakeUser,
8 | EdgeFakeWorld,
9 | EdgeNativeIo
10 | } from '../../types/types'
11 |
12 | export interface WorkerApi {
13 | makeEdgeContext: (
14 | nativeIo: EdgeNativeIo,
15 | logBackend: LogBackend,
16 | pluginUris: string[],
17 | opts: EdgeContextOptions
18 | ) => Promise
19 |
20 | makeFakeEdgeWorld: (
21 | nativeIo: EdgeNativeIo,
22 | logBackend: LogBackend,
23 | pluginUris: string[],
24 | users?: EdgeFakeUser[]
25 | ) => Promise
26 | }
27 |
28 | export interface EdgeCoreMessageEvent {
29 | nativeEvent: { message: string }
30 | }
31 |
32 | export interface EdgeCoreScriptError {
33 | nativeEvent: { source: string }
34 | }
35 |
36 | export interface EdgeCoreWebViewProps {
37 | allowDebugging?: boolean
38 | source: string | null
39 | style?: any
40 | onMessage?: (event: EdgeCoreMessageEvent) => void
41 | onScriptError?: (event: EdgeCoreScriptError) => void
42 | }
43 | export type EdgeCoreWebView = React.ComponentClass
44 | export type EdgeCoreWebViewRef = React.Component
45 |
46 | // Throttle YAOB updates
47 | export const YAOB_THROTTLE_MS = 50
48 |
--------------------------------------------------------------------------------
/src/io/react-native/react-native-webview.tsx:
--------------------------------------------------------------------------------
1 | import '../../client-side'
2 |
3 | import * as React from 'react'
4 | import { requireNativeComponent } from 'react-native'
5 |
6 | import { EdgeCoreWebView, WorkerApi } from './react-native-types'
7 | import { makeYaobCallbacks, YaobCallbacks } from './yaob-callbacks'
8 |
9 | interface Props {
10 | allowDebugging?: boolean
11 | debug?: boolean
12 | onError: (error: unknown) => void
13 | onLoad: (root: WorkerApi) => Promise
14 | }
15 |
16 | /**
17 | * Launches the Edge core worker in a WebView and returns its API.
18 | */
19 | export class EdgeCoreBridge extends React.Component {
20 | callbacks: YaobCallbacks
21 |
22 | constructor(props: Props) {
23 | super(props)
24 | const { onError, onLoad } = props
25 |
26 | // Set up the YAOB bridge:
27 | this.callbacks = makeYaobCallbacks((root: WorkerApi) => {
28 | onLoad(root).catch(onError)
29 | })
30 | }
31 |
32 | render(): JSX.Element {
33 | const { allowDebugging = false, debug = false, onError } = this.props
34 |
35 | return (
36 | {
43 | if (onError != null) {
44 | onError(new Error(`Cannot load "${event.nativeEvent.source}"`))
45 | }
46 | }}
47 | />
48 | )
49 | }
50 | }
51 |
52 | const NativeWebView: EdgeCoreWebView = requireNativeComponent('EdgeCoreWebView')
53 |
--------------------------------------------------------------------------------
/src/io/react-native/yaob-callbacks.ts:
--------------------------------------------------------------------------------
1 | import '../../client-side'
2 |
3 | import { findNodeHandle, UIManager } from 'react-native'
4 | import { Bridge, onMethod } from 'yaob'
5 |
6 | import { hideProperties } from '../hidden-properties'
7 | import {
8 | EdgeCoreMessageEvent,
9 | EdgeCoreWebViewRef,
10 | YAOB_THROTTLE_MS
11 | } from './react-native-types'
12 |
13 | export interface YaobCallbacks {
14 | handleMessage: (event: EdgeCoreMessageEvent) => void
15 | setRef: (element: EdgeCoreWebViewRef | null) => void
16 | }
17 |
18 | /**
19 | * Sets up a YAOB bridge for use with a React Native WebView.
20 | * The returned callbacks should be passed to the `onMessage` and `ref`
21 | * properties of the WebView. Handles WebView reloads and related
22 | * race conditions.
23 | * @param {*} onRoot Called when the inner HTML sends a root object.
24 | * May be called multiple times if the inner HTML reloads.
25 | * @param {*} debug Provide a message prefix to enable debugging.
26 | */
27 | export function makeYaobCallbacks(
28 | onRoot: (root: Root) => void,
29 | debug?: string
30 | ): YaobCallbacks {
31 | let bridge: Bridge | undefined
32 | let gatedRoot: Root | undefined
33 | let webview: EdgeCoreWebViewRef | null = null
34 |
35 | // Gate the root object on the WebView being ready:
36 | function tryReleasingRoot(): void {
37 | if (gatedRoot != null && webview != null) {
38 | onRoot(gatedRoot)
39 | gatedRoot = undefined
40 | }
41 | }
42 |
43 | // Feed incoming messages into the YAOB bridge (if any):
44 | function handleMessage(event: EdgeCoreMessageEvent): void {
45 | const message = JSON.parse(event.nativeEvent.message)
46 | if (debug != null) console.info(`${debug} →`, message)
47 |
48 | // This is a terrible hack. We are using our inside knowledge
49 | // of YAOB's message format to determine when the client has restarted.
50 | if (
51 | bridge != null &&
52 | message.events != null &&
53 | message.events.find((event: any) => event.localId === 0) != null
54 | ) {
55 | bridge.close(new Error('edge-core: The WebView has been unmounted.'))
56 | bridge = undefined
57 | }
58 |
59 | // If we have no bridge, start one:
60 | if (bridge == null) {
61 | let firstMessage = true
62 | bridge = new Bridge({
63 | hideProperties,
64 | sendMessage: message => {
65 | if (debug != null) console.info(`${debug} ←`, message)
66 | if (webview == null) return
67 |
68 | const messageJs = JSON.stringify(message)
69 | .replace(/\u2028/g, '\\u2028')
70 | .replace(/\u2029/g, '\\u2029')
71 | const js = `if (window.reactBridge != null) {${
72 | firstMessage
73 | ? 'window.reactBridge.inSync = true;'
74 | : 'window.reactBridge.inSync && '
75 | } window.reactBridge.handleMessage(${messageJs})}`
76 | firstMessage = false
77 |
78 | UIManager.dispatchViewManagerCommand(
79 | findNodeHandle(webview),
80 | 'runJs',
81 | [js]
82 | )
83 | },
84 | throttleMs: YAOB_THROTTLE_MS
85 | })
86 |
87 | // Use our inside knowledge of YAOB to directly
88 | // subscribe to the root object appearing:
89 | // @ts-expect-error
90 | onMethod.call(bridge._state, 'root', root => {
91 | gatedRoot = root
92 | tryReleasingRoot()
93 | })
94 | }
95 |
96 | // Finally, pass the message to the bridge:
97 | bridge.handleMessage(message)
98 | }
99 |
100 | // Listen for the WebView component to mount:
101 | function setRef(element: EdgeCoreWebViewRef | null): void {
102 | webview = element
103 | tryReleasingRoot()
104 | }
105 |
106 | return { handleMessage, setRef }
107 | }
108 |
--------------------------------------------------------------------------------
/src/libs.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'aes-js'
2 |
3 | declare module 'currency-codes'
4 | declare module 'ethereumjs-tx'
5 | declare module 'ethereumjs-util'
6 | declare module 'hmac-drbg'
7 | declare module 'node-fetch'
8 |
9 | declare module 'react-native' {
10 | // We can't install the React Native type definitions in node_modules,
11 | // since they conflict with the DOM ones, which we also need.
12 | //
13 | // Instead, we provide our own local definitions for the few React Native
14 | // things we use.
15 |
16 | function findNodeHandle(component: React.Component): number
17 | function requireNativeComponent(name: string): React.ComponentClass
18 |
19 | interface UIManager {
20 | dispatchViewManagerCommand: (
21 | handle: number,
22 | method: string,
23 | args: any[]
24 | ) => void
25 | }
26 | const UIManager: UIManager
27 | }
28 |
29 | declare module 'scrypt-js' {
30 | export default function scrypt(
31 | data: ArrayLike,
32 | salt: ArrayLike,
33 | n: number,
34 | r: number,
35 | p: number,
36 | outLength: number,
37 | onProgress: (
38 | error: Error | undefined,
39 | progress: number,
40 | key: ArrayLike | undefined
41 | ) => void
42 | ): void
43 | }
44 |
--------------------------------------------------------------------------------
/src/types/exports.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | EdgeContext,
3 | EdgeContextOptions,
4 | EdgeCorePlugins,
5 | EdgeCorePluginsInit,
6 | EdgeCrashReporter,
7 | EdgeFakeUser,
8 | EdgeFakeWorld,
9 | EdgeFakeWorldOptions,
10 | EdgeIo,
11 | EdgeLoginMessage,
12 | EdgeLogSettings,
13 | EdgeNativeIo,
14 | EdgeOnLog
15 | } from './types'
16 |
17 | export * from './types'
18 |
19 | export declare function addEdgeCorePlugins(plugins: EdgeCorePlugins): void
20 | export declare function lockEdgeCorePlugins(): void
21 | export declare function closeEdge(): void
22 | export declare function makeFakeIo(): EdgeIo
23 |
24 | // System-specific io exports:
25 | export declare function makeBrowserIo(): EdgeIo
26 | export declare function makeNodeIo(path: string): EdgeIo
27 |
28 | /**
29 | * Initializes the Edge core library,
30 | * automatically selecting the appropriate platform.
31 | */
32 | export declare function makeEdgeContext(
33 | opts: EdgeContextOptions
34 | ): Promise
35 |
36 | export declare function makeFakeEdgeWorld(
37 | users?: EdgeFakeUser[],
38 | opts?: EdgeFakeWorldOptions
39 | ): Promise
40 |
41 | // ---------------------------------------------------------------------
42 | // react-native
43 | // ---------------------------------------------------------------------
44 |
45 | interface CommonProps {
46 | // Allows the Chrome debugger to attach to the Android WebView.
47 | // This is mainly useful for debugging plugins,
48 | // since the `debug` prop also activates Chrome debugging.
49 | allowDebugging?: boolean
50 |
51 | // Enable core debugging.
52 | // You must call `yarn start` in the edge-core-js project for this to work:
53 | debug?: boolean
54 |
55 | // React Native modules to pass over the bridge to the plugins:
56 | nativeIo?: EdgeNativeIo
57 |
58 | // Extra JavaScript files to load into the core as plugins.
59 | // Relative URL's resolve to the app's default asset location:
60 | pluginUris?: string[]
61 |
62 | // Called if something goes wrong when starting the core:
63 | // This will change to `(error: unknown) => void`
64 | onError?: (error: any) => unknown
65 | }
66 |
67 | export interface EdgeContextProps extends CommonProps {
68 | /**
69 | * Called once the core finishes loading.
70 | * The return type will change to `=> void`
71 | */
72 | onLoad: (context: EdgeContext) => unknown
73 |
74 | // EdgeFakeWorldOptions:
75 | crashReporter?: EdgeCrashReporter
76 | onLog?: EdgeOnLog
77 |
78 | // EdgeContextOptions:
79 | airbitzSupport?: boolean
80 | apiKey?: string
81 | apiSecret?: Uint8Array
82 | appId?: string
83 | changeServer?: string | string[]
84 | infoServer?: string | string[]
85 | loginServer?: string | string[] // Do not include `/api` in the path
86 | syncServer?: string | string[]
87 | deviceDescription?: string
88 | hideKeys?: boolean
89 | logSettings?: Partial
90 | plugins?: EdgeCorePluginsInit
91 | skipBlockHeight?: boolean
92 |
93 | /** @deprecated Use `loginServer` instead */
94 | authServer?: string
95 | }
96 |
97 | export interface EdgeFakeWorldProps extends CommonProps {
98 | /**
99 | * Called once the core finishes loading.
100 | * The return type will change to `=> void`
101 | */
102 | onLoad: (world: EdgeFakeWorld) => unknown
103 | users?: EdgeFakeUser[]
104 |
105 | // EdgeFakeWorldOptions:
106 | crashReporter?: EdgeCrashReporter
107 | onLog?: EdgeOnLog
108 | }
109 |
110 | /**
111 | * We don't want this library to depend on `@types/react`,
112 | * since that isn't relevant for our Node or browser builds.
113 | */
114 | type ComponentType = (props: Props) => {
115 | type: any
116 | props: any
117 | key: string | null
118 | }
119 |
120 | /**
121 | * React Native component for creating an EdgeContext.
122 | */
123 | export declare const MakeEdgeContext: ComponentType
124 |
125 | /**
126 | * React Native component for creating an EdgeFakeWorld for testing.
127 | */
128 | export declare const MakeFakeEdgeWorld: ComponentType
129 |
130 | /**
131 | * React Native function for getting login alerts without a context:
132 | */
133 | export declare function fetchLoginMessages(apiKey: string): EdgeLoginMessage[]
134 |
--------------------------------------------------------------------------------
/src/types/type-helpers.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | EdgeCurrencyInfo,
3 | EdgeSwapQuote,
4 | EdgeTokenId,
5 | EdgeTokenMap,
6 | EdgeTransaction
7 | } from './types'
8 |
9 | /**
10 | * Translates a currency code to a tokenId,
11 | * and then back again for bi-directional backwards compatibility.
12 | */
13 | export function upgradeCurrencyCode(opts: {
14 | allTokens: EdgeTokenMap
15 | currencyInfo: EdgeCurrencyInfo
16 | currencyCode?: string
17 | tokenId?: EdgeTokenId
18 | }): { currencyCode: string; tokenId: EdgeTokenId } {
19 | const { currencyInfo, allTokens } = opts
20 |
21 | // Find the tokenId:
22 | let tokenId = opts.tokenId
23 | if (
24 | tokenId === undefined &&
25 | opts.currencyCode != null &&
26 | opts.currencyCode !== currencyInfo.currencyCode
27 | ) {
28 | tokenId = Object.keys(allTokens).find(
29 | tokenId => allTokens[tokenId].currencyCode === opts.currencyCode
30 | )
31 | }
32 |
33 | // Get the currency code:
34 | const { currencyCode } = tokenId == null ? currencyInfo : allTokens[tokenId]
35 |
36 | return { currencyCode, tokenId: tokenId ?? null }
37 | }
38 |
39 | export function upgradeSwapQuote(quote: EdgeSwapQuote): EdgeSwapQuote {
40 | if (quote.networkFee != null && quote.networkFee.tokenId == null) {
41 | quote.networkFee.tokenId = quote.request.fromTokenId
42 | }
43 | return quote
44 | }
45 |
46 | export const upgradeTxNetworkFees = (tx: EdgeTransaction): void => {
47 | if (tx.networkFees == null || tx.networkFees.length === 0) {
48 | tx.networkFees = [
49 | {
50 | tokenId: tx.tokenId,
51 | nativeAmount: tx.networkFee
52 | }
53 | ]
54 | if (tx.parentNetworkFee != null) {
55 | tx.networkFees.push({ tokenId: null, nativeAmount: tx.parentNetworkFee })
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/util/asMap.ts:
--------------------------------------------------------------------------------
1 | import { asCodec, asObject, Cleaner } from 'cleaners'
2 |
3 | import { EdgeTokenId } from '../browser'
4 |
5 | /**
6 | * Reads a JSON-style object into a JavaScript `Map` object with string keys.
7 | */
8 | export function asMap(cleaner: Cleaner): Cleaner