Compare commits
44 commits
master
...
wip-channe
Author | SHA1 | Date | |
---|---|---|---|
|
fd74cf1f5a | ||
|
8ad085c24c | ||
|
f841c897dc | ||
|
1c8fc79f08 | ||
|
1dcb1d36a1 | ||
|
30c17f3bdb | ||
|
e7c61254a5 | ||
|
5acc79d212 | ||
|
a913d45060 | ||
|
14e9f4d925 | ||
|
6bbd324388 | ||
|
d0b197c5b0 | ||
|
1c7854b9ec | ||
|
d0583e540a | ||
|
439ce26e0f | ||
|
65d675041a | ||
|
4e891a5763 | ||
|
c69894806c | ||
|
340e6ccaa3 | ||
|
b18af383be | ||
|
aee6b977d7 | ||
|
205411adda | ||
|
dadeb066c8 | ||
|
970c0e68e7 | ||
|
a761ba1a06 | ||
|
95d783ffc1 | ||
|
a3023dc40c | ||
|
6b6efe6ce0 | ||
|
5c8d42b2b1 | ||
|
ee4db066a0 | ||
|
eecf21aec4 | ||
|
2e84e0ff40 | ||
|
a75c7b22a1 | ||
|
51e089f9a9 | ||
|
44f65ab93e | ||
|
b89ff006ce | ||
|
8441a49232 | ||
|
8b28bd35ff | ||
|
a9b3a683c4 | ||
|
0c76472093 | ||
|
14d36075fc | ||
|
7cd0eee93b | ||
|
b7fc4f9e8e | ||
|
af4bbc9949 |
|
@ -1,2 +0,0 @@
|
||||||
# For now, tag everyone who wants to participate
|
|
||||||
* @deso-protocol/reviewers
|
|
46
Caddyfile
|
@ -1,46 +0,0 @@
|
||||||
# Global caddy config options must be first
|
|
||||||
{
|
|
||||||
admin off
|
|
||||||
auto_https off
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bind to port 82
|
|
||||||
:82
|
|
||||||
|
|
||||||
# Serve static files
|
|
||||||
file_server
|
|
||||||
|
|
||||||
# Fallback to index.html for everything but assets
|
|
||||||
@html {
|
|
||||||
not path *.js *.css *.png *.svg *.ttf *.woff2
|
|
||||||
|
|
||||||
file index.html
|
|
||||||
}
|
|
||||||
|
|
||||||
handle_errors {
|
|
||||||
header Cache-Control no-store
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite @html {http.matchers.file.relative}
|
|
||||||
|
|
||||||
# Don't cache index.html and set CSP
|
|
||||||
header @html Cache-Control no-store
|
|
||||||
header @html Content-Security-Policy "
|
|
||||||
default-src 'self';
|
|
||||||
connect-src
|
|
||||||
{$DOMAIN:https://node.deso.org}/api/v0/get-users-stateless
|
|
||||||
{$DOMAIN:https://node.deso.org}/api/v0/get-app-state
|
|
||||||
{$DOMAIN:https://node.deso.org}/api/v0/get-referral-info-for-referral-hash
|
|
||||||
{$DOMAIN:https://node.deso.org}/api/v0/get-user-derived-keys
|
|
||||||
{$DOMAIN:https://node.deso.org}/api/v0/get-transaction-spending
|
|
||||||
{$DOMAIN:https://node.deso.org}/api/v0/send-phone-number-verification-text
|
|
||||||
{$DOMAIN:https://node.deso.org}/api/v0/submit-phone-number-verification-code
|
|
||||||
img-src 'self'
|
|
||||||
{$DOMAIN:https://node.deso.org}/api/v0/get-single-profile-picture/;
|
|
||||||
style-src 'self' 'unsafe-inline'
|
|
||||||
https://fonts.googleapis.com
|
|
||||||
https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css;
|
|
||||||
font-src 'self'
|
|
||||||
https://fonts.googleapis.com
|
|
||||||
https://fonts.gstatic.com
|
|
||||||
https://ka-f.fontawesome.com;"
|
|
43
Dockerfile
|
@ -1,43 +0,0 @@
|
||||||
FROM node:14.15.5-alpine3.13 AS identity
|
|
||||||
|
|
||||||
WORKDIR /identity
|
|
||||||
|
|
||||||
RUN apk add git
|
|
||||||
|
|
||||||
COPY ./package.json .
|
|
||||||
COPY ./package-lock.json .
|
|
||||||
COPY ./.npmrc .
|
|
||||||
|
|
||||||
# use yarn to upgrade npm
|
|
||||||
RUN yarn global add npm@7
|
|
||||||
|
|
||||||
# install frontend dependencies before copying the frontend code
|
|
||||||
# into the container so we get docker cache benefits
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# don't allow any dependencies with vulnerabilities
|
|
||||||
#RUN npx audit-ci --low
|
|
||||||
|
|
||||||
# running ngcc before build_prod lets us utilize the docker
|
|
||||||
# cache and significantly speeds up builds without requiring us
|
|
||||||
# to import/export the node_modules folder from the container
|
|
||||||
RUN npm run ngcc
|
|
||||||
|
|
||||||
COPY ./angular.json .
|
|
||||||
COPY ./tsconfig.json .
|
|
||||||
COPY ./tsconfig.app.json .
|
|
||||||
COPY ./webpack.config.js .
|
|
||||||
COPY ./tslint.json .
|
|
||||||
COPY ./src ./src
|
|
||||||
|
|
||||||
RUN npm run build_prod
|
|
||||||
|
|
||||||
# build minified version of frontend, served using caddy
|
|
||||||
FROM caddy:2.3.0-alpine
|
|
||||||
|
|
||||||
WORKDIR /identity
|
|
||||||
|
|
||||||
COPY ./Caddyfile .
|
|
||||||
COPY --from=identity /identity/dist/identity .
|
|
||||||
|
|
||||||
ENTRYPOINT ["caddy", "run"]
|
|
1
LICENSE
|
@ -1,6 +1,7 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 DeSo Community Developers
|
Copyright (c) 2021 DeSo Community Developers
|
||||||
|
Copyright (c) 2022 LBRY
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
17
angular.json
|
@ -88,23 +88,6 @@
|
||||||
"browserTarget": "identity:build"
|
"browserTarget": "identity:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
|
||||||
"builder": "@angular-bulders/custom-webpack:karma",
|
|
||||||
"options": {
|
|
||||||
"main": "src/test.ts",
|
|
||||||
"polyfills": "src/polyfills.ts",
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
|
||||||
"karmaConfig": "karma.conf.js",
|
|
||||||
"assets": [
|
|
||||||
"src/favicon.ico",
|
|
||||||
"src/assets"
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"src/styles.scss"
|
|
||||||
],
|
|
||||||
"scripts": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
"lint": {
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
"options": {
|
"options": {
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
// Protractor configuration file, see link for more information
|
|
||||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
|
||||||
|
|
||||||
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type { import("protractor").Config }
|
|
||||||
*/
|
|
||||||
exports.config = {
|
|
||||||
allScriptsTimeout: 11000,
|
|
||||||
specs: [
|
|
||||||
'./src/**/*.e2e-spec.ts'
|
|
||||||
],
|
|
||||||
capabilities: {
|
|
||||||
browserName: 'chrome'
|
|
||||||
},
|
|
||||||
directConnect: true,
|
|
||||||
SELENIUM_PROMISE_MANAGER: false,
|
|
||||||
baseUrl: 'http://localhost:4200/',
|
|
||||||
framework: 'jasmine',
|
|
||||||
jasmineNodeOpts: {
|
|
||||||
showColors: true,
|
|
||||||
defaultTimeoutInterval: 30000,
|
|
||||||
print: function() {}
|
|
||||||
},
|
|
||||||
onPrepare() {
|
|
||||||
require('ts-node').register({
|
|
||||||
project: require('path').join(__dirname, './tsconfig.json')
|
|
||||||
});
|
|
||||||
jasmine.getEnv().addReporter(new SpecReporter({
|
|
||||||
spec: {
|
|
||||||
displayStacktrace: StacktraceOption.PRETTY
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { browser, logging } from 'protractor';
|
|
||||||
import { AppPage } from './app.po';
|
|
||||||
|
|
||||||
describe('workspace-project App', () => {
|
|
||||||
let page: AppPage;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
page = new AppPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display welcome message', async () => {
|
|
||||||
await page.navigateTo();
|
|
||||||
expect(await page.getTitleText()).toEqual('identity app is running!');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
// Assert that there are no errors emitted from the browser
|
|
||||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
|
||||||
expect(logs).not.toContain(jasmine.objectContaining({
|
|
||||||
level: logging.Level.SEVERE,
|
|
||||||
} as logging.Entry));
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { browser, by, element } from 'protractor';
|
|
||||||
|
|
||||||
export class AppPage {
|
|
||||||
async navigateTo(): Promise<unknown> {
|
|
||||||
return browser.get(browser.baseUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTitleText(): Promise<string> {
|
|
||||||
return element(by.css('app-root .content span')).getText();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
|
||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../out-tsc/e2e",
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es2018",
|
|
||||||
"types": [
|
|
||||||
"jasmine",
|
|
||||||
"node"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
1
example/backend/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
TODO: explain config.go
|
|
@ -1,2 +0,0 @@
|
||||||
# For now, tag everyone who wants to participate
|
|
||||||
* @deso-protocol/reviewers
|
|
|
@ -1,92 +0,0 @@
|
||||||
# Global caddy config options must be first
|
|
||||||
{
|
|
||||||
admin off
|
|
||||||
auto_https off
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bind to port 80
|
|
||||||
:80
|
|
||||||
|
|
||||||
# Serve static files
|
|
||||||
file_server
|
|
||||||
|
|
||||||
# Fallback to index.html for everything but assets
|
|
||||||
@html {
|
|
||||||
not path *.js *.css *.png *.jpg *.svg *.pdf *.eot *.ttf *.woff *.woff2 *.webmanifest
|
|
||||||
|
|
||||||
file index.html
|
|
||||||
}
|
|
||||||
|
|
||||||
handle_errors {
|
|
||||||
header Cache-Control no-store
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite @html {http.matchers.file.relative}
|
|
||||||
|
|
||||||
# Don't cache index.html and set CSP
|
|
||||||
header @html Cache-Control no-store
|
|
||||||
header @html Content-Security-Policy "
|
|
||||||
default-src 'self';
|
|
||||||
connect-src 'self'
|
|
||||||
node.deso.org
|
|
||||||
amp.deso.org
|
|
||||||
bithunt.deso.org
|
|
||||||
bitclout.com:*
|
|
||||||
api.bitclout.com
|
|
||||||
bithunt.bitclout.com
|
|
||||||
https://altumbase.com
|
|
||||||
localhost:*
|
|
||||||
explorer.bitclout.com
|
|
||||||
https://api.blockchain.com/ticker
|
|
||||||
https://api.blockchain.com/mempool/fees
|
|
||||||
https://ka-f.fontawesome.com/
|
|
||||||
bitcoinfees.earn.com
|
|
||||||
api.blockcypher.com
|
|
||||||
amp.bitclout.com
|
|
||||||
api.testwyre.com
|
|
||||||
api.sendwyre.com
|
|
||||||
https://videodelivery.net
|
|
||||||
https://upload.videodelivery.net;
|
|
||||||
script-src 'self'
|
|
||||||
https://kit.fontawesome.com/070ca4195b.js
|
|
||||||
https://ka-f.fontawesome.com/;
|
|
||||||
style-src 'self'
|
|
||||||
'unsafe-inline'
|
|
||||||
https://fonts.googleapis.com
|
|
||||||
https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css;
|
|
||||||
img-src 'self'
|
|
||||||
data:
|
|
||||||
i.imgur.com
|
|
||||||
images.deso.org
|
|
||||||
images.bitclout.com
|
|
||||||
quickchart.io
|
|
||||||
arweave.net
|
|
||||||
*.arweave.net
|
|
||||||
*.pearl.app
|
|
||||||
cloudflare-ipfs.com;
|
|
||||||
font-src 'self'
|
|
||||||
https://fonts.googleapis.com
|
|
||||||
https://fonts.gstatic.com
|
|
||||||
https://ka-f.fontawesome.com;
|
|
||||||
frame-src 'self'
|
|
||||||
localhost:*
|
|
||||||
identity.deso.org
|
|
||||||
identity.deso.blue
|
|
||||||
identity.deso.green
|
|
||||||
identity.bitclout.com
|
|
||||||
identity.bitclout.blue
|
|
||||||
identity.bitclout.green
|
|
||||||
https://geo.captcha-delivery.com
|
|
||||||
https://www.youtube.com
|
|
||||||
https://youtube.com
|
|
||||||
https://player.vimeo.com
|
|
||||||
https://www.tiktok.com
|
|
||||||
https://giphy.com
|
|
||||||
https://open.spotify.com
|
|
||||||
https://w.soundcloud.com
|
|
||||||
https://player.twitch.tv
|
|
||||||
https://clips.twitch.tv
|
|
||||||
pay.testwyre.com
|
|
||||||
pay.sendwyre.com
|
|
||||||
https://iframe.videodelivery.net;
|
|
||||||
frame-ancestors 'self';"
|
|
|
@ -1,60 +0,0 @@
|
||||||
FROM node:14.15.5-alpine3.13 AS frontend
|
|
||||||
|
|
||||||
WORKDIR /frontend
|
|
||||||
|
|
||||||
# install git
|
|
||||||
RUN apk add git
|
|
||||||
|
|
||||||
# use yarn to upgrade npm
|
|
||||||
RUN yarn global add npm@7
|
|
||||||
|
|
||||||
COPY ./package.json .
|
|
||||||
COPY ./package-lock.json .
|
|
||||||
COPY ./.npmrc .
|
|
||||||
|
|
||||||
# install frontend dependencies before copying the frontend code
|
|
||||||
# into the container so we get docker cache benefits
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# don't allow any dependencies with vulnerabilities
|
|
||||||
#RUN npx audit-ci --low
|
|
||||||
|
|
||||||
# running ngcc before build_prod lets us utilize the docker
|
|
||||||
# cache and significantly speeds up builds without requiring us
|
|
||||||
# to import/export the node_modules folder from the container
|
|
||||||
RUN npm run ngcc
|
|
||||||
|
|
||||||
COPY ./angular.json .
|
|
||||||
COPY ./tsconfig.json .
|
|
||||||
COPY ./src ./src
|
|
||||||
|
|
||||||
# use --build-arg index=index.custom.html to specify a custom index.html file
|
|
||||||
ARG index=index.html
|
|
||||||
|
|
||||||
# overwrite default index file with custom file
|
|
||||||
COPY ./src/$index ./src/index.html
|
|
||||||
|
|
||||||
# use --build-arg environment=custom to specify a custom environment
|
|
||||||
ARG environment=prod
|
|
||||||
|
|
||||||
# overwrite default environment file with custom file
|
|
||||||
COPY ./src/environments/environment.$environment.ts ./src/environments/environment.prod.ts
|
|
||||||
|
|
||||||
RUN npm run build_prod
|
|
||||||
|
|
||||||
# build minified version of frontend, served using caddy
|
|
||||||
FROM caddy:2.3.0-alpine
|
|
||||||
|
|
||||||
WORKDIR /frontend
|
|
||||||
|
|
||||||
COPY ./Caddyfile .
|
|
||||||
COPY --from=frontend /frontend/dist .
|
|
||||||
|
|
||||||
# We use a run.sh script so that we can pass environment variables
|
|
||||||
# to it.
|
|
||||||
COPY ./run.sh .
|
|
||||||
|
|
||||||
# Default options overrideable by docker-compose
|
|
||||||
ENV CADDY_FILE "/frontend/Caddyfile"
|
|
||||||
|
|
||||||
ENTRYPOINT ["/frontend/run.sh"]
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 DeSo Community Developers
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,34 +1 @@
|
||||||
![DeSo Logo](src/assets/deso/camelcase_logo.svg)
|
TODO
|
||||||
|
|
||||||
# About DeSo
|
|
||||||
DeSo is a blockchain built from the ground up to support a fully-featured
|
|
||||||
social network. Its architecture is similar to Bitcoin, only it supports complex
|
|
||||||
social network data like profiles, posts, follows, creator coin transactions, and
|
|
||||||
more.
|
|
||||||
|
|
||||||
[Read about the vision](https://docs.deso.org/#the-ultimate-vision)
|
|
||||||
|
|
||||||
# About This Repo
|
|
||||||
Documentation for this repo lives on docs.deso.org. Specifically, the following
|
|
||||||
docs should give you everything you need to get started:
|
|
||||||
* [DeSo Code Walkthrough](https://docs.deso.org/code/walkthrough)
|
|
||||||
* [Setting Up Your Dev Environment](https://docs.deso.org/code/dev-setup)
|
|
||||||
* [Making Your First Changes](https://docs.deso.org/code/making-your-first-changes)
|
|
||||||
|
|
||||||
# Start Coding
|
|
||||||
The quickest way to contribute changes to the BitClout Frontend is the following these steps:
|
|
||||||
|
|
||||||
1. Open frontend repo in Gitpod
|
|
||||||
|
|
||||||
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/deso-protocol/frontend)
|
|
||||||
|
|
||||||
You can use any repo / branch URL and just prepend `https://gitpod.io/#` to it.
|
|
||||||
|
|
||||||
2. If needed, login to your github account
|
|
||||||
|
|
||||||
3. Set the correct `lastLocalNodeV2` to `"https://api.tijn.club"` in your browser Local Storage for the gitpod preview URL
|
|
||||||
|
|
||||||
4. Create a new branch to start working
|
|
||||||
|
|
||||||
To commit / submit a pull reqest from gitpod, you will need to give gitpod additional permissions to your github account: `public_repo, read:org, read:user, repo, user:email, workflow` which you can do on the [GitPod Integrations page](https://gitpod.io/integrations).
|
|
||||||
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../.eslintrc.json",
|
|
||||||
"ignorePatterns": [
|
|
||||||
"!**/*"
|
|
||||||
],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"*.ts"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"project": [
|
|
||||||
"e2e//tsconfig.app.json",
|
|
||||||
"e2e//tsconfig.spec.json",
|
|
||||||
"e2e//e2e/tsconfig.json"
|
|
||||||
],
|
|
||||||
"createDefaultProgram": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"@angular-eslint/directive-selector": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"type": "attribute",
|
|
||||||
"prefix": "",
|
|
||||||
"style": "camelCase"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@angular-eslint/component-selector": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"type": "element",
|
|
||||||
"prefix": "",
|
|
||||||
"style": "kebab-case"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"*.html"
|
|
||||||
],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
// Protractor configuration file, see link for more information
|
|
||||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
|
||||||
|
|
||||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
|
||||||
|
|
||||||
exports.config = {
|
|
||||||
allScriptsTimeout: 11000,
|
|
||||||
specs: [
|
|
||||||
'./src/**/*.e2e-spec.ts'
|
|
||||||
],
|
|
||||||
capabilities: {
|
|
||||||
'browserName': 'chrome'
|
|
||||||
},
|
|
||||||
directConnect: true,
|
|
||||||
baseUrl: 'http://localhost:4200/',
|
|
||||||
framework: 'jasmine',
|
|
||||||
jasmineNodeOpts: {
|
|
||||||
showColors: true,
|
|
||||||
defaultTimeoutInterval: 30000,
|
|
||||||
print: function() {}
|
|
||||||
},
|
|
||||||
onPrepare() {
|
|
||||||
require('ts-node').register({
|
|
||||||
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
|
||||||
});
|
|
||||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { AppPage } from "./app.po";
|
|
||||||
import { browser, logging } from "protractor";
|
|
||||||
|
|
||||||
describe("workspace-project App", () => {
|
|
||||||
let page: AppPage;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
page = new AppPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should display welcome message", () => {
|
|
||||||
page.navigateTo();
|
|
||||||
expect(page.getTitleText()).toEqual("Welcome to electron-angular-app!");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
// Assert that there are no errors emitted from the browser
|
|
||||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
|
||||||
expect(logs).not.toContain(
|
|
||||||
jasmine.objectContaining({
|
|
||||||
level: logging.Level.SEVERE,
|
|
||||||
} as logging.Entry)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { browser, by, element } from "protractor";
|
|
||||||
|
|
||||||
export class AppPage {
|
|
||||||
navigateTo() {
|
|
||||||
return browser.get(browser.baseUrl) as Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTitleText() {
|
|
||||||
return element(by.css("app-root h1")).getText() as Promise<string>;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../out-tsc/app",
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"types": [
|
|
||||||
"jasmine",
|
|
||||||
"jasminewd2",
|
|
||||||
"node"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
92
example/frontend/package-lock.json
generated
|
@ -20,7 +20,6 @@
|
||||||
"@ethereumjs/common": "^2.5.0",
|
"@ethereumjs/common": "^2.5.0",
|
||||||
"@ethereumjs/tx": "^3.3.2",
|
"@ethereumjs/tx": "^3.3.2",
|
||||||
"@ethereumjs/vm": "^5.5.3",
|
"@ethereumjs/vm": "^5.5.3",
|
||||||
"amplitude-js": "^7.4.3",
|
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
"autolinker": "^3.14.2",
|
"autolinker": "^3.14.2",
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
|
@ -101,39 +100,6 @@
|
||||||
"webpack-cli": "^3.3.10"
|
"webpack-cli": "^3.3.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@amplitude/types": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-XspuOsUzUcxwAptHeGiIn4giuLWs285xTJa7h8kAEEynxtEI3/krWCoDYZSB9PekaPXB6phxiO/tMd9t5V9LgQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@amplitude/ua-parser-js": {
|
|
||||||
"version": "0.7.24",
|
|
||||||
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.24.tgz",
|
|
||||||
"integrity": "sha512-VbQuJymJ20WEw0HtI2np7EdC3NJGUWi8+Xdbc7uk8WfMIF308T0howpzkQ3JFMN7ejnrcSM/OyNGveeE3TP3TA==",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@amplitude/utils": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-1DrDJkb4dVX+FiBXhGpO2Dn2cRKdP+gtrVR8vZcE8wz/V2XxUI3DDx7uQbIS6WbQf6swv6Uo2eMHYtrwebostw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@amplitude/types": "^1.5.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@amplitude/utils/node_modules/tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
|
||||||
},
|
|
||||||
"node_modules/@angular-devkit/architect": {
|
"node_modules/@angular-devkit/architect": {
|
||||||
"version": "0.1002.1",
|
"version": "0.1002.1",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz",
|
||||||
|
@ -5713,17 +5679,6 @@
|
||||||
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/amplitude-js": {
|
|
||||||
"version": "7.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.4.3.tgz",
|
|
||||||
"integrity": "sha512-ea42XGJ/4MwqtJ/OZX5uZf2VsNePONUNTJoEaaTxjCwFGk5sR5bOh9W60puMuL1HQ3YSy6lFBdrcY4wzaCl7PA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@amplitude/ua-parser-js": "0.7.24",
|
|
||||||
"@amplitude/utils": "^1.0.5",
|
|
||||||
"blueimp-md5": "^2.10.0",
|
|
||||||
"query-string": "5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ansi-colors": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
||||||
|
@ -6691,11 +6646,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||||
},
|
},
|
||||||
"node_modules/blueimp-md5": {
|
|
||||||
"version": "2.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.18.0.tgz",
|
|
||||||
"integrity": "sha512-vE52okJvzsVWhcgUHOv+69OG3Mdg151xyn41aVQN/5W5S+S43qZhxECtYLAEHMSFWX6Mv5IZrzj3T5+JqXfj5Q=="
|
|
||||||
},
|
|
||||||
"node_modules/bn.js": {
|
"node_modules/bn.js": {
|
||||||
"version": "4.11.9",
|
"version": "4.11.9",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||||
|
@ -27902,32 +27852,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amplitude/types": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-XspuOsUzUcxwAptHeGiIn4giuLWs285xTJa7h8kAEEynxtEI3/krWCoDYZSB9PekaPXB6phxiO/tMd9t5V9LgQ=="
|
|
||||||
},
|
|
||||||
"@amplitude/ua-parser-js": {
|
|
||||||
"version": "0.7.24",
|
|
||||||
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.24.tgz",
|
|
||||||
"integrity": "sha512-VbQuJymJ20WEw0HtI2np7EdC3NJGUWi8+Xdbc7uk8WfMIF308T0howpzkQ3JFMN7ejnrcSM/OyNGveeE3TP3TA=="
|
|
||||||
},
|
|
||||||
"@amplitude/utils": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-1DrDJkb4dVX+FiBXhGpO2Dn2cRKdP+gtrVR8vZcE8wz/V2XxUI3DDx7uQbIS6WbQf6swv6Uo2eMHYtrwebostw==",
|
|
||||||
"requires": {
|
|
||||||
"@amplitude/types": "^1.5.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@angular-devkit/architect": {
|
"@angular-devkit/architect": {
|
||||||
"version": "0.1002.1",
|
"version": "0.1002.1",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz",
|
||||||
|
@ -32184,17 +32108,6 @@
|
||||||
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"amplitude-js": {
|
|
||||||
"version": "7.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.4.3.tgz",
|
|
||||||
"integrity": "sha512-ea42XGJ/4MwqtJ/OZX5uZf2VsNePONUNTJoEaaTxjCwFGk5sR5bOh9W60puMuL1HQ3YSy6lFBdrcY4wzaCl7PA==",
|
|
||||||
"requires": {
|
|
||||||
"@amplitude/ua-parser-js": "0.7.24",
|
|
||||||
"@amplitude/utils": "^1.0.5",
|
|
||||||
"blueimp-md5": "^2.10.0",
|
|
||||||
"query-string": "5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ansi-colors": {
|
"ansi-colors": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
||||||
|
@ -32928,11 +32841,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||||
},
|
},
|
||||||
"blueimp-md5": {
|
|
||||||
"version": "2.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.18.0.tgz",
|
|
||||||
"integrity": "sha512-vE52okJvzsVWhcgUHOv+69OG3Mdg151xyn41aVQN/5W5S+S43qZhxECtYLAEHMSFWX6Mv5IZrzj3T5+JqXfj5Q=="
|
|
||||||
},
|
|
||||||
"bn.js": {
|
"bn.js": {
|
||||||
"version": "4.11.9",
|
"version": "4.11.9",
|
||||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
"@ethereumjs/common": "^2.5.0",
|
"@ethereumjs/common": "^2.5.0",
|
||||||
"@ethereumjs/tx": "^3.3.2",
|
"@ethereumjs/tx": "^3.3.2",
|
||||||
"@ethereumjs/vm": "^5.5.3",
|
"@ethereumjs/vm": "^5.5.3",
|
||||||
"amplitude-js": "^7.4.3",
|
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
"autolinker": "^3.14.2",
|
"autolinker": "^3.14.2",
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
echo "Loading Caddy config from file: ${CADDY_FILE}"
|
|
||||||
|
|
||||||
caddy run --config ${CADDY_FILE}
|
|
|
@ -174,6 +174,8 @@ export class BackendApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtPost(endpoint: string, path: string, publicKey: string, body: any): Observable<any> {
|
jwtPost(endpoint: string, path: string, publicKey: string, body: any): Observable<any> {
|
||||||
|
// TODO - we may not need this method
|
||||||
|
// see notes in: (identity) src/app/identity.service.ts
|
||||||
const request = this.identityService.jwt({
|
const request = this.identityService.jwt({
|
||||||
...this.identityService.identityServiceParamsForKey(publicKey),
|
...this.identityService.identityServiceParamsForKey(publicKey),
|
||||||
});
|
});
|
||||||
|
|
|
@ -73,7 +73,7 @@ export class GlobalVarsService {
|
||||||
|
|
||||||
testLoginLBRY() : Observable<string[]> {
|
testLoginLBRY() : Observable<string[]> {
|
||||||
return new Observable(subscriber => {
|
return new Observable(subscriber => {
|
||||||
this.identityService.launch("/test-lbry-log-in", {}).subscribe((res) => {
|
this.identityService.launch("/log-in-wallet", {}).subscribe((res) => {
|
||||||
// TODO - maybe we want public key instead of address? we should, as DeSo did, have a list of users with everything we need from them.
|
// TODO - maybe we want public key instead of address? we should, as DeSo did, have a list of users with everything we need from them.
|
||||||
subscriber.next(res.addresses)
|
subscriber.next(res.addresses)
|
||||||
subscriber.complete()
|
subscriber.complete()
|
||||||
|
|
|
@ -50,7 +50,6 @@ export class IdentityService {
|
||||||
nonWitnessUtxoHexes?: string,
|
nonWitnessUtxoHexes?: string,
|
||||||
fromAddress?: string,
|
fromAddress?: string,
|
||||||
public_key?: string;
|
public_key?: string;
|
||||||
accessLevelRequest?: number;
|
|
||||||
}
|
}
|
||||||
): Observable<any> {
|
): Observable<any> {
|
||||||
let url = this.identityServiceURL as string;
|
let url = this.identityServiceURL as string;
|
||||||
|
@ -92,10 +91,6 @@ export class IdentityService {
|
||||||
httpParams = httpParams.append("public_key", params.public_key);
|
httpParams = httpParams.append("public_key", params.public_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params?.accessLevelRequest) {
|
|
||||||
httpParams = httpParams.append("accessLevelRequest", params.accessLevelRequest.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
const paramsStr = httpParams.toString();
|
const paramsStr = httpParams.toString();
|
||||||
if (paramsStr) {
|
if (paramsStr) {
|
||||||
url += `?${paramsStr}`;
|
url += `?${paramsStr}`;
|
||||||
|
|
|
@ -60,7 +60,7 @@ export class SpendLBCComponent implements OnInit {
|
||||||
const toAddress = (<HTMLInputElement>document.getElementById("toAddress")).value;
|
const toAddress = (<HTMLInputElement>document.getElementById("toAddress")).value;
|
||||||
const desiredAmount = Number((<HTMLInputElement>document.getElementById("desiredAmount")).value);
|
const desiredAmount = Number((<HTMLInputElement>document.getElementById("desiredAmount")).value);
|
||||||
|
|
||||||
this.backendApi.GetPsbt("localhost:8090", this.fromAddress, toAddress, desiredAmount).subscribe({
|
this.backendApi.GetPsbt(environment.backendHostname, this.fromAddress, toAddress, desiredAmount).subscribe({
|
||||||
next: res => {
|
next: res => {
|
||||||
this.psbtHex = res.psbtHex
|
this.psbtHex = res.psbtHex
|
||||||
this.nonWitnessUtxoHexes = res.nonWitnessUtxoHex
|
this.nonWitnessUtxoHexes = res.nonWitnessUtxoHex
|
||||||
|
@ -99,14 +99,14 @@ export class SpendLBCComponent implements OnInit {
|
||||||
this.globalVars.testSignTransactionLBRY(this.psbtHex, this.nonWitnessUtxoHexes, this.fromAddress).subscribe({
|
this.globalVars.testSignTransactionLBRY(this.psbtHex, this.nonWitnessUtxoHexes, this.fromAddress).subscribe({
|
||||||
next: (signedTransactionHex) => {
|
next: (signedTransactionHex) => {
|
||||||
this.signedTransactionHex = signedTransactionHex
|
this.signedTransactionHex = signedTransactionHex
|
||||||
this.backendApi.DecodeTransaction("localhost:8090", signedTransactionHex).subscribe({
|
this.backendApi.DecodeTransaction(environment.backendHostname, signedTransactionHex).subscribe({
|
||||||
next: res => {
|
next: res => {
|
||||||
this.decodedTransaction = res.decodedTransaction
|
this.decodedTransaction = res.decodedTransaction
|
||||||
console.log(res.decodedTransaction)
|
console.log(res.decodedTransaction)
|
||||||
},
|
},
|
||||||
error: err => { this.setError(err) }
|
error: err => { this.setError(err) }
|
||||||
})
|
})
|
||||||
this.backendApi.BroadcastTransaction("localhost:8090", signedTransactionHex).subscribe({
|
this.backendApi.BroadcastTransaction(environment.backendHostname, signedTransactionHex).subscribe({
|
||||||
next: res => {
|
next: res => {
|
||||||
this.setError(null)
|
this.setError(null)
|
||||||
this.success = res.txid
|
this.success = res.txid
|
||||||
|
|
|
@ -2,32 +2,7 @@
|
||||||
"name": "deso",
|
"name": "deso",
|
||||||
"short_name": "deso",
|
"short_name": "deso",
|
||||||
"description": "The crypto social network",
|
"description": "The crypto social network",
|
||||||
"icons": [
|
"icons": [],
|
||||||
{
|
|
||||||
"src": "/assets/bitclout/logo-192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/assets/bitclout/logo-512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"purpose": "maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/assets/bitclout/logo-192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"purpose": "any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/assets/bitclout/logo-512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"purpose": "any"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
|
|
Before Width: | Height: | Size: 4.2 KiB |
|
@ -1,7 +0,0 @@
|
||||||
<svg width="138" height="28" viewBox="0 0 138 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M29.7001 21.7632H35.7321C40.1781 21.7632 42.9601 20.5932 42.9601 16.9012C42.9601 13.6252 40.7501 12.4812 37.9421 12.4292V11.8052C39.8401 11.6752 41.6081 10.5832 41.6081 7.93118C41.6081 4.31718 39.0341 3.43318 34.5621 3.43318H29.7001V21.7632ZM31.8321 11.3112V5.25318H34.6401C37.6301 5.25318 39.4761 5.43518 39.4761 8.26918C39.4761 10.8692 37.6301 11.3112 34.6401 11.3112H31.8321ZM31.8321 19.9432V13.1312H35.8101C38.9301 13.1312 40.8281 13.8592 40.8281 16.5372C40.8281 19.4232 38.9301 19.9432 35.8101 19.9432H31.8321ZM49.2637 6.78718C50.3037 6.78718 51.1357 6.00718 51.1357 4.96718C51.1357 3.97918 50.3037 3.17318 49.2637 3.17318C48.1977 3.17318 47.3917 3.97918 47.3917 4.96718C47.3917 6.00718 48.1977 6.78718 49.2637 6.78718ZM44.9477 21.7632H54.2297V19.9432H50.5897V10.1412C50.5897 9.15318 50.1217 8.47718 48.8997 8.58118L45.9097 8.84118V10.6612L48.5877 10.4272V19.9432H44.9477V21.7632ZM59.29 20.2292V10.6612H63.32V8.84118H59.29V4.86318L57.184 5.51318V8.84118H54.324V10.6612H57.184V20.2552C57.184 21.4512 57.73 22.1012 58.952 22.0232L63.84 21.7632V19.9432L59.29 20.2292ZM66.612 12.5852C66.612 17.9412 69.81 22.0232 75.192 22.0232C79.872 22.0232 82.94 19.2152 83.486 14.6652H81.354C80.938 18.1752 78.78 20.2032 75.192 20.2032C70.928 20.2032 68.744 17.0312 68.744 12.5852C68.744 8.16518 70.928 4.99318 75.192 4.99318C78.78 4.99318 80.938 6.99518 81.354 10.5052H83.486C82.94 5.98118 79.872 3.17318 75.192 3.17318C69.81 3.17318 66.612 7.25518 66.612 12.5852ZM84.7033 21.7632H94.0633V19.9432H90.4493V4.18718C90.4493 2.96518 89.8773 2.28918 88.6813 2.39318L85.6653 2.65318V4.47318L88.3433 4.23918V19.9432H84.7033V21.7632ZM95.5258 15.2892C95.5258 19.0592 98.2298 22.0232 102.364 22.0232C106.472 22.0232 109.15 19.0592 109.15 15.2892C109.15 11.5452 106.472 8.58118 102.364 8.58118C98.2298 8.58118 95.5258 11.5452 95.5258 15.2892ZM97.6578 15.2892C97.6578 12.6112 99.2958 10.4012 102.364 10.4012C105.406 10.4012 107.044 12.6112 107.044 15.2892C107.044 17.9932 105.406 20.2032 102.364 20.2032C99.2958 20.2032 97.6578 17.9932 97.6578 15.2892ZM117.266 20.2032C115.03 20.2032 113.99 19.1632 113.99 16.4592V8.84118H111.858V16.7452C111.858 20.3332 113.756 22.0232 116.694 22.0232C119.502 22.0232 121.14 20.3852 121.66 18.0452H122.258L121.92 19.5272V21.7632H124.026V8.84118H121.92V14.5872C121.92 18.0452 119.996 20.2032 117.266 20.2032ZM131.247 20.2292V10.6612H135.277V8.84118H131.247V4.86318L129.141 5.51318V8.84118H126.281V10.6612H129.141V20.2552C129.141 21.4512 129.687 22.1012 130.909 22.0232L135.797 21.7632V19.9432L131.247 20.2292Z" fill="#222222"/>
|
|
||||||
<path d="M9.66243 25.3511L17.6755 15.2309C17.8052 15.067 17.6885 14.8257 17.4795 14.8257H0.907031C0.694218 14.8257 0.57872 15.0746 0.716149 15.2371L9.27554 25.3574C9.37753 25.478 9.56439 25.475 9.66243 25.3511Z" fill="white" stroke="black"/>
|
|
||||||
<path d="M17.5745 14.3587L9.44746 10.4872C9.30597 10.4197 9.14105 10.4226 9.00196 10.4948L0.999407 14.6513C0.650668 14.8324 0.637574 15.3265 0.976231 15.5259L8.96405 20.2278C9.1233 20.3216 9.32127 20.3198 9.47885 20.2233L17.6206 15.2365C17.9589 15.0293 17.9327 14.5294 17.5745 14.3587Z" fill="white" stroke="black"/>
|
|
||||||
<path d="M9.66243 15.971L17.6755 5.85075C17.8052 5.68686 17.6885 5.44556 17.4795 5.44556H0.907031C0.694218 5.44556 0.57872 5.69451 0.716149 5.857L9.27554 15.9773C9.37753 16.0979 9.56439 16.0948 9.66243 15.971Z" fill="white" stroke="black"/>
|
|
||||||
<path d="M17.5745 4.97862L9.44746 1.10703C9.30597 1.03962 9.14105 1.04246 9.00196 1.1147L0.999407 5.27113C0.650668 5.45226 0.637574 5.9464 0.976231 6.14574L8.96405 10.8477C9.1233 10.9414 9.32127 10.9397 9.47885 10.8432L17.6206 5.85639C17.9589 5.64915 17.9327 5.14926 17.5745 4.97862Z" fill="white" stroke="black"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 7 KiB |
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="704pt" height="801pt" viewBox="0 0 704 801" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g id="#000000ff">
|
|
||||||
<path fill="#000000" opacity="1.00" d=" M 273.03 42.04 C 299.57 28.75 325.97 15.16 352.57 2.00 C 442.37 47.92 532.24 93.71 622.15 139.42 C 616.20 148.00 609.48 156.04 603.23 164.41 C 554.73 227.60 506.20 290.76 457.70 353.95 C 512.62 381.98 567.58 409.91 622.49 437.96 C 532.95 554.49 443.40 671.01 353.83 787.52 C 262.59 671.00 171.37 554.47 80.15 437.94 C 135.98 409.75 191.82 381.56 247.65 353.34 C 191.89 282.04 136.17 210.71 80.38 139.43 C 144.53 106.85 208.84 74.55 273.03 42.04 M 144.87 138.36 C 213.96 175.07 283.07 211.77 352.16 248.48 C 421.07 211.91 489.97 175.33 558.87 138.77 C 489.97 103.74 421.08 68.68 352.18 33.66 C 283.06 68.53 213.96 103.43 144.87 138.36 M 136.71 165.58 C 209.17 258.00 281.50 350.52 353.84 443.03 C 424.45 350.88 495.27 258.89 565.97 166.80 C 494.73 204.53 423.57 242.40 352.34 280.12 C 280.47 241.93 208.62 203.69 136.71 165.58 M 144.84 436.71 C 213.93 473.40 283.15 509.84 352.19 546.63 C 421.24 510.23 490.11 473.47 559.09 436.92 C 519.48 416.83 479.88 396.72 440.30 376.58 C 411.47 414.11 382.66 451.64 353.84 489.15 C 324.30 451.45 294.80 413.71 265.26 376.00 C 225.11 396.21 185.00 416.50 144.84 436.71 M 136.50 464.12 C 208.83 556.64 281.25 649.10 353.61 741.60 C 424.45 649.48 495.20 557.28 566.02 465.13 C 494.70 502.96 423.44 540.90 352.12 578.71 C 280.26 540.48 208.38 502.30 136.50 464.12 Z" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 892 B |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 9.6 KiB |
|
@ -1,24 +0,0 @@
|
||||||
export const environment = {
|
|
||||||
production: true,
|
|
||||||
uploadImageHostname: "node.deso.org",
|
|
||||||
verificationEndpointHostname: "https://node.deso.org",
|
|
||||||
uploadVideoHostname: "node.deso.org",
|
|
||||||
identityURL: "https://identity.deso.org",
|
|
||||||
supportEmail: "node.admin@bitclout.com",
|
|
||||||
dd: {
|
|
||||||
apiKey: "DCEB26AC8BF47F1D7B4D87440EDCA6",
|
|
||||||
jsPath: "https://bitclout.com/tags.js",
|
|
||||||
ajaxListenerPath: "bitclout.com/api",
|
|
||||||
endpoint: "https://bitclout.com/js/",
|
|
||||||
},
|
|
||||||
amplitude: {
|
|
||||||
key: "23345b239094949bc7f3402cebe9e5d2",
|
|
||||||
domain: "amp.bitclout.com",
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
id: 2,
|
|
||||||
name: "BitClout",
|
|
||||||
url: "https://bitclout.com",
|
|
||||||
logoAssetDir: "/assets/bitclout/",
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,24 +1,4 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
uploadImageHostname: "node.deso.org",
|
identityURL: "http://localhost:4201",
|
||||||
verificationEndpointHostname: "https://node.deso.org",
|
|
||||||
uploadVideoHostname: "node.deso.org",
|
|
||||||
identityURL: "https://identity.deso.org",
|
|
||||||
supportEmail: "",
|
|
||||||
dd: {
|
|
||||||
apiKey: "DCEB26AC8BF47F1D7B4D87440EDCA6",
|
|
||||||
jsPath: "https://bitclout.com/tags.js",
|
|
||||||
ajaxListenerPath: "bitclout.com/api",
|
|
||||||
endpoint: "https://bitclout.com/js/",
|
|
||||||
},
|
|
||||||
amplitude: {
|
|
||||||
key: "",
|
|
||||||
domain: "",
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
id: 1,
|
|
||||||
name: "DeSo",
|
|
||||||
url: "https://node.deso.org",
|
|
||||||
logoAssetDir: "/assets/deso/",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,25 +4,6 @@
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
uploadImageHostname: "node.deso.org",
|
backendHostname: 'localhost:8090',
|
||||||
verificationEndpointHostname: "https://node.deso.org",
|
identityURL: "http://localhost:4201",
|
||||||
uploadVideoHostname: "node.deso.org",
|
|
||||||
identityURL: "https://identity.deso.org",
|
|
||||||
supportEmail: "",
|
|
||||||
dd: {
|
|
||||||
apiKey: "DCEB26AC8BF47F1D7B4D87440EDCA6",
|
|
||||||
jsPath: "https://bitclout.com/tags.js",
|
|
||||||
ajaxListenerPath: "bitclout.com/api",
|
|
||||||
endpoint: "https://bitclout.com/js/",
|
|
||||||
},
|
|
||||||
amplitude: {
|
|
||||||
key: "",
|
|
||||||
domain: "",
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
id: 1,
|
|
||||||
name: "DeSo",
|
|
||||||
url: "https://node.deso.org",
|
|
||||||
logoAssetDir: "/assets/deso/",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="referrer" content="no-referrer" />
|
|
||||||
<title>Welcome to BitClout</title>
|
|
||||||
<meta name="description" content="BitClout is a platform owned by its users. Bitcoin is decentralizing money, BitClout is decentralizing social media." />
|
|
||||||
<meta name="theme-color" content="#eeeeee" media="(prefers-color-scheme: light)">
|
|
||||||
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:image" content="https://bitclout.com/assets/bitclout/camelcase_logo_og.jpg" />
|
|
||||||
<meta property="og:title" content="Welcome to BitClout" />
|
|
||||||
<meta property="og:description" content="BitClout is a platform owned by its users. Bitcoin is decentralizing money, BitClout is decentralizing social media." />
|
|
||||||
<meta property="og:site_name" content="BitClout" />
|
|
||||||
<meta property='og:image' content="https://bitclout.com/assets/bitclout/camelcase_logo_og.jpg" />
|
|
||||||
<meta property="og:image:secure_url" content="https://bitclout.com/assets/bitclout/camelcase_logo_og.jpg" />
|
|
||||||
<meta property="og:image:type" content="image/jpeg" />
|
|
||||||
<meta property="og:image:alt" content="bitclout.com Logo on White Background" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:url" content="https://bitclout.com" />
|
|
||||||
<base href="/" />
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.9" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="/assets/bitclout/favicon.png" />
|
|
||||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet" />
|
|
||||||
<link href="/vendor/roboto-mono.css" rel="stylesheet" />
|
|
||||||
<link href="/vendor/roboto.css" rel="stylesheet" />
|
|
||||||
<link href="/vendor/fontello.css" rel="stylesheet" />
|
|
||||||
<link href="/vendor/bs-datepicker.css" rel="stylesheet" />
|
|
||||||
|
|
||||||
<script src="https://kit.fontawesome.com/070ca4195b.js" crossorigin="anonymous"></script>
|
|
||||||
<!-- Add to iOS home screen -->
|
|
||||||
<link rel="manifest" href="/assets/app.webmanifest" />
|
|
||||||
<script async src="/vendor/pwacompat.min.js" crossorigin="anonymous"></script>
|
|
||||||
<link rel="icon" type="image/png" href="/assets/bitclout/logo-512.png" sizes="512x512" />
|
|
||||||
<link rel="icon" type="image/png" href="/assets/bitclout/logo-192.png" sizes="192x192" />
|
|
||||||
<link rel="apple-touch-icon" sizes="192x192" href="/assets/bitclout/logo-192.png" type="image/png" />
|
|
||||||
<link rel="apple-touch-icon" sizes="512x512" href="/assets/bitclout/logo-512.png" type="image/png" />
|
|
||||||
</head>
|
|
||||||
<body app-theme>
|
|
||||||
<app-root></app-root>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -8,16 +8,6 @@
|
||||||
<meta name="theme-color" content="#eeeeee" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#eeeeee" media="(prefers-color-scheme: light)">
|
||||||
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:image" content="https://node.deso.org/assets/deso/camelcase_logo_og.jpg" />
|
|
||||||
<meta property="og:title" content="Welcome to DeSo" />
|
|
||||||
<meta property="og:description" content="DeSo is a platform owned by its users. Bitcoin is decentralizing money, DeSo is decentralizing social media." />
|
|
||||||
<meta property="og:site_name" content="DeSo" />
|
|
||||||
<meta property='og:image' content="https://node.deso.org/assets/deso/camelcase_logo_og.jpg" />
|
|
||||||
<meta property="og:image:secure_url" content="https://node.deso.org/assets/deso/camelcase_logo_og.jpg" />
|
|
||||||
<meta property="og:image:type" content="image/jpeg" />
|
|
||||||
<meta property="og:image:alt" content="DeSo Logo on White Background" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:url" content="https://node.deso.org" />
|
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.9" />
|
<meta name="viewport" content="width=device-width, initial-scale=0.9" />
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
// Karma configuration file, see link for more information
|
|
||||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
basePath: '',
|
|
||||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
|
||||||
plugins: [
|
|
||||||
require('karma-jasmine'),
|
|
||||||
require('karma-chrome-launcher'),
|
|
||||||
require('karma-jasmine-html-reporter'),
|
|
||||||
require('karma-coverage-istanbul-reporter'),
|
|
||||||
require('@angular-devkit/build-angular/plugins/karma')
|
|
||||||
],
|
|
||||||
client: {
|
|
||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
|
||||||
},
|
|
||||||
coverageIstanbulReporter: {
|
|
||||||
dir: require('path').join(__dirname, '../coverage/electron-angular-app'),
|
|
||||||
reports: ['html', 'lcovonly', 'text-summary'],
|
|
||||||
fixWebpackSourcePaths: true
|
|
||||||
},
|
|
||||||
reporters: ['progress', 'kjhtml'],
|
|
||||||
port: 9876,
|
|
||||||
colors: true,
|
|
||||||
logLevel: config.LOG_INFO,
|
|
||||||
autoWatch: true,
|
|
||||||
browsers: ['Chrome'],
|
|
||||||
singleRun: false,
|
|
||||||
restartOnFileChange: true
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,44 +0,0 @@
|
||||||
// Karma configuration file, see link for more information
|
|
||||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
basePath: '',
|
|
||||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
|
||||||
plugins: [
|
|
||||||
require('karma-jasmine'),
|
|
||||||
require('karma-chrome-launcher'),
|
|
||||||
require('karma-jasmine-html-reporter'),
|
|
||||||
require('karma-coverage'),
|
|
||||||
require('@angular-devkit/build-angular/plugins/karma')
|
|
||||||
],
|
|
||||||
client: {
|
|
||||||
jasmine: {
|
|
||||||
// you can add configuration options for Jasmine here
|
|
||||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
|
||||||
// for example, you can disable the random execution with `random: false`
|
|
||||||
// or set a specific seed with `seed: 4321`
|
|
||||||
},
|
|
||||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
|
||||||
},
|
|
||||||
jasmineHtmlReporter: {
|
|
||||||
suppressAll: true // removes the duplicated traces
|
|
||||||
},
|
|
||||||
coverageReporter: {
|
|
||||||
dir: require('path').join(__dirname, './coverage/identity'),
|
|
||||||
subdir: '.',
|
|
||||||
reporters: [
|
|
||||||
{ type: 'html' },
|
|
||||||
{ type: 'text-summary' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
reporters: ['progress', 'kjhtml'],
|
|
||||||
port: 9876,
|
|
||||||
colors: true,
|
|
||||||
logLevel: config.LOG_INFO,
|
|
||||||
autoWatch: true,
|
|
||||||
browsers: ['Chrome'],
|
|
||||||
singleRun: false,
|
|
||||||
restartOnFileChange: true
|
|
||||||
});
|
|
||||||
};
|
|
73
package-lock.json
generated
|
@ -23,7 +23,7 @@
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"bip32": "^2.0.6",
|
"bip32": "^2.0.6",
|
||||||
"bip39": "^3.0.3",
|
"bip39": "^3.0.3",
|
||||||
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"ecpair": "^1.0.1",
|
"ecpair": "^1.0.1",
|
||||||
"elliptic": "^6.5.4",
|
"elliptic": "^6.5.4",
|
||||||
|
@ -32,8 +32,6 @@
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"key-encoder": "^2.0.3",
|
"key-encoder": "^2.0.3",
|
||||||
"ngx-bootstrap": "^6.2.0",
|
"ngx-bootstrap": "^6.2.0",
|
||||||
"ngx-cookie": "^5.0.2",
|
|
||||||
"ngx-intl-tel-input": "^3.1.1",
|
|
||||||
"readable-stream": "^3.6.0",
|
"readable-stream": "^3.6.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "~6.6.0",
|
"rxjs": "~6.6.0",
|
||||||
|
@ -3400,8 +3398,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/bitcoinjs-lib": {
|
"node_modules/bitcoinjs-lib": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
"resolved": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||||
"integrity": "sha512-xluOW5MDgy2GTFAW8Vg7qYAgAlOTTaFWlogg8ejNetNRSZ8tg3go9XkEHdQXFhf1mDxKl55ybvSl0ie1ApiTrA==",
|
"integrity": "sha512-AwoS80uq7ADqozrLW3aDjSvLZpQQn7a2Jt6Qt9DOiYTcwjxWarWEt+ntkNskzk63Azs779Wb0suD6KqWbJ/U5A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
|
@ -7200,15 +7198,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/google-libphonenumber": {
|
|
||||||
"version": "3.2.25",
|
|
||||||
"resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.25.tgz",
|
|
||||||
"integrity": "sha512-M/b5mij5o2aGnbe+Id9O3847jBtP0baW61foFkevxBxbuV4LH9AcujjYLd2UVkUPKVdMpKyBZxfeNwdxqobQFQ==",
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.6",
|
"version": "4.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
||||||
|
@ -10228,34 +10217,6 @@
|
||||||
"@angular/core": ">=7.0.0"
|
"@angular/core": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ngx-cookie": {
|
|
||||||
"version": "5.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ngx-cookie/-/ngx-cookie-5.0.2.tgz",
|
|
||||||
"integrity": "sha512-auivWhAhC5bW1HssvtQild1TREHWb1JtcKO0e+VGe9T7LHrfi5w2qcP8C58ly64PT+brZHQBvT1Azb7a6goHZA==",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@angular/common": ">9.0.0",
|
|
||||||
"@angular/core": ">9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ngx-intl-tel-input": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ngx-intl-tel-input/-/ngx-intl-tel-input-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-jk/6dBBDo9Z9QNBzFvJKQs159mZtKfC+/hF3MDoMpya8xzOz7Tbwh5qNsHaIFtzDx82Fv1Ji9+0VPWIv1P0fIQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@angular/common": "8.x - 11.x",
|
|
||||||
"@angular/core": "8.x - 11.x",
|
|
||||||
"@angular/forms": "8.x - 11.x",
|
|
||||||
"google-libphonenumber": "^3.2.3",
|
|
||||||
"intl-tel-input": "^17.0.3",
|
|
||||||
"ngx-bootstrap": "^6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nice-try": {
|
"node_modules/nice-try": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||||
|
@ -21472,9 +21433,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bitcoinjs-lib": {
|
"bitcoinjs-lib": {
|
||||||
"version": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
"version": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||||
"integrity": "sha512-xluOW5MDgy2GTFAW8Vg7qYAgAlOTTaFWlogg8ejNetNRSZ8tg3go9XkEHdQXFhf1mDxKl55ybvSl0ie1ApiTrA==",
|
"integrity": "sha512-AwoS80uq7ADqozrLW3aDjSvLZpQQn7a2Jt6Qt9DOiYTcwjxWarWEt+ntkNskzk63Azs779Wb0suD6KqWbJ/U5A==",
|
||||||
"from": "bitcoinjs-lib@lbryio/bitcoinjs-lib#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
"from": "bitcoinjs-lib@github:lbryio/bitcoinjs-lib#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||||
"requires": {
|
"requires": {
|
||||||
"bech32": "^2.0.0",
|
"bech32": "^2.0.0",
|
||||||
"bip174": "^2.0.1",
|
"bip174": "^2.0.1",
|
||||||
|
@ -24755,12 +24716,6 @@
|
||||||
"slash": "^3.0.0"
|
"slash": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"google-libphonenumber": {
|
|
||||||
"version": "3.2.25",
|
|
||||||
"resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.25.tgz",
|
|
||||||
"integrity": "sha512-M/b5mij5o2aGnbe+Id9O3847jBtP0baW61foFkevxBxbuV4LH9AcujjYLd2UVkUPKVdMpKyBZxfeNwdxqobQFQ==",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"graceful-fs": {
|
"graceful-fs": {
|
||||||
"version": "4.2.6",
|
"version": "4.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
||||||
|
@ -27259,22 +27214,6 @@
|
||||||
"integrity": "sha512-5WKHo6/ltkenw4UyXZwED8rODCgp2RGbWurzYzZsF/gH1JO5SN7TJ+AL6kXYk6XM42sDA2WhN9Db+ZPNjiyHnA==",
|
"integrity": "sha512-5WKHo6/ltkenw4UyXZwED8rODCgp2RGbWurzYzZsF/gH1JO5SN7TJ+AL6kXYk6XM42sDA2WhN9Db+ZPNjiyHnA==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"ngx-cookie": {
|
|
||||||
"version": "5.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ngx-cookie/-/ngx-cookie-5.0.2.tgz",
|
|
||||||
"integrity": "sha512-auivWhAhC5bW1HssvtQild1TREHWb1JtcKO0e+VGe9T7LHrfi5w2qcP8C58ly64PT+brZHQBvT1Azb7a6goHZA==",
|
|
||||||
"requires": {
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ngx-intl-tel-input": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ngx-intl-tel-input/-/ngx-intl-tel-input-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-jk/6dBBDo9Z9QNBzFvJKQs159mZtKfC+/hF3MDoMpya8xzOz7Tbwh5qNsHaIFtzDx82Fv1Ji9+0VPWIv1P0fIQ==",
|
|
||||||
"requires": {
|
|
||||||
"tslib": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nice-try": {
|
"nice-try": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"bip32": "^2.0.6",
|
"bip32": "^2.0.6",
|
||||||
"bip39": "^3.0.3",
|
"bip39": "^3.0.3",
|
||||||
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"ecpair": "^1.0.1",
|
"ecpair": "^1.0.1",
|
||||||
"elliptic": "^6.5.4",
|
"elliptic": "^6.5.4",
|
||||||
|
@ -37,8 +37,6 @@
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"key-encoder": "^2.0.3",
|
"key-encoder": "^2.0.3",
|
||||||
"ngx-bootstrap": "^6.2.0",
|
"ngx-bootstrap": "^6.2.0",
|
||||||
"ngx-cookie": "^5.0.2",
|
|
||||||
"ngx-intl-tel-input": "^3.1.1",
|
|
||||||
"readable-stream": "^3.6.0",
|
"readable-stream": "^3.6.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "~6.6.0",
|
"rxjs": "~6.6.0",
|
||||||
|
|
|
@ -1,60 +1,228 @@
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {CryptoService} from './crypto.service';
|
|
||||||
import {GlobalVarsService} from './global-vars.service';
|
import {GlobalVarsService} from './global-vars.service';
|
||||||
import {AccessLevel, Network, PrivateUserInfo, PublicUserInfo} from '../types/identity';
|
import {HubService} from './hub.service';
|
||||||
import HDKey from 'hdkey';
|
import {SigningService} from './signing.service';
|
||||||
|
import {
|
||||||
|
AccessLevel,
|
||||||
|
ActionType,
|
||||||
|
PrivateAccountInfo,
|
||||||
|
PrivateChannelInfo,
|
||||||
|
PublicChannelInfo,
|
||||||
|
} from '../types/identity';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AccountService {
|
export class AccountService {
|
||||||
private static usersStorageKey = 'users';
|
private static walletStorageKey = 'wallet';
|
||||||
private static levelsStorageKey = 'levels';
|
private static channelsStorageKey = 'channels';
|
||||||
|
private static accessStorageKey = 'access';
|
||||||
private static publicKeyRegex = /^[a-zA-Z0-9]{54,55}$/;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private globalVars: GlobalVarsService,
|
private globalVars: GlobalVarsService,
|
||||||
|
private signingService: SigningService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
// Public Getters
|
/*
|
||||||
|
|
||||||
getPublicKeys(): any {
|
What we're keeping in local storage. TODO - make these into data structs.
|
||||||
return Object.keys(this.getPrivateUsers());
|
|
||||||
|
// The wallet, taken from wallet Sync. Perhaps even with local changes to be
|
||||||
|
// pushed back to sync.
|
||||||
|
localStorage["wallet"] = {
|
||||||
|
// We may later want to add signature, sync metadata, etc. Or maybe we want
|
||||||
|
// the strict string representation that came out of sync.
|
||||||
|
walletStoreVersion: 0,
|
||||||
|
|
||||||
|
wallet: "...",
|
||||||
}
|
}
|
||||||
|
|
||||||
getEncryptedUsers(): {[key: string]: PublicUserInfo} {
|
// Access information. For each hostname, what channel is logged in, and what
|
||||||
const hostname = this.globalVars.hostname;
|
// level of permission for various actions did the user give?
|
||||||
const privateUsers = this.getPrivateUsers();
|
localStorage["access"] = {
|
||||||
const publicUsers: {[key: string]: PublicUserInfo} = {};
|
// There can be multiple hostnames
|
||||||
|
"<hostnames>": { // TODO - type Hostname
|
||||||
|
"currentChannel": "<channel-claim-id>", // TODO - type ChannelClaimID
|
||||||
|
"levels": {
|
||||||
|
// There can be multiple channels
|
||||||
|
"<channel-claim-ids>": {
|
||||||
|
// There can be multiple action types
|
||||||
|
"<action-types>": "<level>",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const publicKey of Object.keys(privateUsers)) {
|
localStorage["channels"] = {
|
||||||
const privateUser = privateUsers[publicKey];
|
// There can be multiple channels
|
||||||
const accessLevel = this.getAccessLevel(publicKey, hostname);
|
"<channels>": PrivateChannelInfo,
|
||||||
if (accessLevel === AccessLevel.None) {
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
private hasWallet(): boolean {
|
||||||
|
return !!localStorage.getItem(AccountService.walletStorageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO define a wallet type, and/or use a type defined by json-schema
|
||||||
|
public getWallet(): {accounts: [PrivateAccountInfo]} | null {
|
||||||
|
const walletStoreStr = localStorage.getItem(AccountService.walletStorageKey);
|
||||||
|
const walletStore = JSON.parse(walletStoreStr || 'null')
|
||||||
|
if (walletStore !== null) {
|
||||||
|
if (walletStore.walletStoreVersion === 0) {
|
||||||
|
return walletStore.wallet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private putWallet(wallet: object | null) {
|
||||||
|
const walletStore = {
|
||||||
|
// We may later want to add signature, sync metadata, etc. Or maybe we
|
||||||
|
// want the strict string representation that came out of sync. This is
|
||||||
|
// not the version of the wallet's internal structure; that has its own
|
||||||
|
// version key
|
||||||
|
walletStoreVersion: 0,
|
||||||
|
|
||||||
|
wallet
|
||||||
|
}
|
||||||
|
localStorage.setItem(AccountService.walletStorageKey, JSON.stringify(walletStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - PrivateAccountInfo should just contain a bip32 node and an
|
||||||
|
// "address" (account ID) that it generates when reading from storage. It
|
||||||
|
// wouldn't write these values back to storage, they're a function of the
|
||||||
|
// data already there. It would clean up the code by removing the need for
|
||||||
|
// bip32FromAccount, getAddress, and getAddressFromBip32.
|
||||||
|
public getAccounts(): PrivateAccountInfo[] {
|
||||||
|
const wallet = this.getWallet()
|
||||||
|
if (wallet === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const filteredAccounts: PrivateAccountInfo[] = [];
|
||||||
|
|
||||||
|
for (const account of wallet.accounts) {
|
||||||
|
// Only include accounts from the current network
|
||||||
|
if (account.ledger !== this.globalVars.network) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedSeedHex = this.cryptoService.encryptSeedHex(privateUser.seedHex, hostname);
|
filteredAccounts.push(account);
|
||||||
const accessLevelHmac = this.cryptoService.accessLevelHmac(accessLevel, privateUser.seedHex);
|
}
|
||||||
|
return filteredAccounts
|
||||||
publicUsers[publicKey] = {
|
|
||||||
hasExtraText: privateUser.extraText?.length > 0,
|
|
||||||
encryptedSeedHex,
|
|
||||||
network: privateUser.network,
|
|
||||||
accessLevel,
|
|
||||||
accessLevelHmac,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return publicUsers;
|
private clearChannels() {
|
||||||
|
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccessLevel(publicKey: string, hostname: string): AccessLevel {
|
// TODO - This function is async due to the http call, so now I have to
|
||||||
const levels = JSON.parse(localStorage.getItem(AccountService.levelsStorageKey) || '{}');
|
// rethink the guarantees about login state being based on the data being in
|
||||||
const hostMapping = levels[hostname] || {};
|
// localStorage.
|
||||||
const accessLevel = hostMapping[publicKey];
|
//
|
||||||
|
// TODO error handling?
|
||||||
|
public updateChannels(): Observable<null> {
|
||||||
|
// Where we accumulate the channels for all accounts through all of the
|
||||||
|
// recursions
|
||||||
|
let channels: {[key: string]: {[key: string]: PrivateChannelInfo}} = {};
|
||||||
|
|
||||||
|
// Return this so the caller can do something pending this completing
|
||||||
|
// (perhaps keep the login state orderly)
|
||||||
|
// TODO - there's got to be a better way.
|
||||||
|
return new Observable(subscriber => {
|
||||||
|
accumulateChannelsForAccounts(accounts: PrivateAccountInfo[]) {
|
||||||
|
if(!accounts.length){
|
||||||
|
// We got the channels for all accounts. Give it to the subscriber so we can add it to local storage.
|
||||||
|
subscriber.next(channels)
|
||||||
|
subscriber.complete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.signingService.getChannelsForAccount(accounts[0])
|
||||||
|
.pipe(
|
||||||
|
map(acountChannels => {
|
||||||
|
const accountId = this.signingService.getAddress(account)
|
||||||
|
// `acountChannels` is an array. We want the same data in an object,
|
||||||
|
// keyed by the pubKey field.
|
||||||
|
channelsByPubkey = Object.fromEntries(
|
||||||
|
acountChannels.map(channel => [channel.pubKeyId, channel])
|
||||||
|
)
|
||||||
|
channels[accountId] = channelsByPubkey
|
||||||
|
|
||||||
|
// Call again, omitting the account we just handled.
|
||||||
|
accumulateChannelsForAccount(accounts.slice(1))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick it off with all accounts.
|
||||||
|
accumulateChannelsForAccounts(this.getAccounts())
|
||||||
|
}).pipe(
|
||||||
|
map(channels => {
|
||||||
|
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(channels));
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
).subscribe() // We want to actually kick off these actions if this function is called (see pipe vs subscribe)
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasChannels() {
|
||||||
|
return !!localStorage.getItem(AccountService.channelsStorageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getChannelsPrivate(): {[key: string]: PrivateChannelInfo} {
|
||||||
|
return JSON.parse(localStorage.getItem(AccountService.channelsStorageKey) || '{}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns {accountId: [PublicChannelInfo]}
|
||||||
|
public getChannelsPublic(): {[key: string]: PublicChannelInfo} {
|
||||||
|
const privateChannels: {[key: string]: PrivateChannelInfo} = this.getChannelsPrivate()
|
||||||
|
const publicChannels: {[key: string]: PublicChannelInfo} = {}
|
||||||
|
for(const accountId of Object.keys(privateChannels)) {
|
||||||
|
publicChannels[accountId] = {
|
||||||
|
claimId: privateChannels[accountId].claimId,
|
||||||
|
name: privateChannels[accountId].name,
|
||||||
|
normalizedName: privateChannels[accountId].normalizedName,
|
||||||
|
pubKeyId: privateChannels[accountId].pubKeyId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return publicChannels
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearAccess() {
|
||||||
|
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private initAccess() {
|
||||||
|
// no currentChannel or access level for any action on any hostname
|
||||||
|
localStorage.setItem(AccountService.accessStorageKey, '{}');
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasAccess() {
|
||||||
|
return !!localStorage.getItem(AccountService.accessStorageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveChannel(hostname: string): PublicChannelInfo | null {
|
||||||
|
// TODO - and actually, this maybe only needs to happen on startup. could save in a local variable.
|
||||||
|
const channels = this.getChannelsPublic()
|
||||||
|
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||||
|
|
||||||
|
if (access[hostname]) {
|
||||||
|
const activeChannelClaimId = access[hostname].currentChannel
|
||||||
|
return channels[activeChannelClaimId]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveChannelAccessLevel(hostname: string, action: ActionType): AccessLevel {
|
||||||
|
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||||
|
|
||||||
|
if (!access[hostname]) {
|
||||||
|
return AccessLevel.None
|
||||||
|
}
|
||||||
|
const activeChannelClaimId = access[hostname].currentChannel
|
||||||
|
if (!access[hostname].levels[activeChannelClaimId]) {
|
||||||
|
return AccessLevel.None
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessLevel = access[hostname].levels[activeChannelClaimId][action]
|
||||||
|
|
||||||
if (Object.values(AccessLevel).includes(accessLevel)) {
|
if (Object.values(AccessLevel).includes(accessLevel)) {
|
||||||
return accessLevel;
|
return accessLevel;
|
||||||
|
@ -63,80 +231,63 @@ export class AccountService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public Modifiers
|
public setAccessLevel(hostname: string, channelClaimId: string, action: ActionType, level: AccessLevel) {
|
||||||
|
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||||
addUser(keychain: HDKey, mnemonic: string, extraText: string, network: Network): string {
|
if (!(hostname in access)) {
|
||||||
const seedHex = this.cryptoService.keychainToSeedHex(keychain);
|
access[hostname] = {levels: {}}
|
||||||
|
}
|
||||||
return this.addPrivateUser({
|
if (!(channelClaimId in access[hostname].levels)) {
|
||||||
seedHex,
|
access[hostname].levels[channelClaimId] = {}
|
||||||
mnemonic,
|
}
|
||||||
extraText,
|
access[hostname].levels[channelClaimId][action] = level
|
||||||
network,
|
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteUser(publicKey: string): void {
|
public setAccessCurrentChannel(hostname: string, channelClaimId: string) {
|
||||||
const privateUsers = this.getPrivateUsersRaw();
|
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||||
|
if (!(hostname in access)) {
|
||||||
delete privateUsers[publicKey];
|
access[hostname] = {levels: {}}
|
||||||
|
}
|
||||||
this.setPrivateUsersRaw(privateUsers);
|
access[hostname].currentChannel = channelClaimId
|
||||||
|
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
|
||||||
}
|
}
|
||||||
|
|
||||||
setAccessLevel(publicKey: string, hostname: string, accessLevel: AccessLevel): void {
|
public walletLogout() {
|
||||||
const levels = JSON.parse(localStorage.getItem(AccountService.levelsStorageKey) || '{}');
|
this.putWallet(null)
|
||||||
|
this.clearAccess()
|
||||||
levels[hostname] ||= {};
|
this.clearChannels()
|
||||||
levels[hostname][publicKey] = accessLevel;
|
|
||||||
|
|
||||||
localStorage.setItem(AccountService.levelsStorageKey, JSON.stringify(levels));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private Getters and Modifiers
|
public walletLogin(wallet: object | null) {
|
||||||
|
// no ambiguity from half-completed actions
|
||||||
|
// make sure we're fully logged out
|
||||||
|
this.walletLogout()
|
||||||
|
|
||||||
// TEMP: public for import flow
|
this.putWallet(wallet)
|
||||||
public addPrivateUser(userInfo: PrivateUserInfo): string {
|
this.initAccess()
|
||||||
const privateUsers = this.getPrivateUsersRaw();
|
this.updateChannels()
|
||||||
const privateKey = this.cryptoService.seedHexToPrivateKey(userInfo.seedHex);
|
|
||||||
const publicKey = this.cryptoService.privateKeyToDeSoPublicKey(privateKey, userInfo.network);
|
|
||||||
|
|
||||||
privateUsers[publicKey] = userInfo;
|
|
||||||
|
|
||||||
this.setPrivateUsersRaw(privateUsers);
|
|
||||||
|
|
||||||
return publicKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPrivateUsers(): {[key: string]: PrivateUserInfo} {
|
// TODO - delete this if I don't end up using it
|
||||||
const privateUsers = this.getPrivateUsersRaw();
|
public walletIsLoggedIn(): boolean {
|
||||||
const filteredPrivateUsers: {[key: string]: PrivateUserInfo} = {};
|
return (
|
||||||
|
// All three should be set. It means the last login was complete, and no
|
||||||
for (const publicKey of Object.keys(privateUsers)) {
|
// logout was started since.
|
||||||
const privateUser = privateUsers[publicKey];
|
this.hasAccess() && this.hasChannels() && this.hasWallet()
|
||||||
|
)
|
||||||
// Only include users from the current network
|
|
||||||
if (privateUser.network !== this.globalVars.network) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rid of some users who have invalid public keys
|
// TODO: Do we want to save per-hostname-per-channel-per-action access levels
|
||||||
if (!publicKey.match(AccountService.publicKeyRegex)) {
|
// between logins? Counterargument: users probably won't log in and out too
|
||||||
this.deleteUser(publicKey);
|
// often.
|
||||||
continue;
|
private appClearAccess(hostname: string): void {
|
||||||
|
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||||
|
delete access[hostname]
|
||||||
|
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredPrivateUsers[publicKey] = privateUser;
|
public appLogout(hostname: string): void {
|
||||||
|
this.appClearAccess(hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredPrivateUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPrivateUsersRaw(): {[key: string]: PrivateUserInfo} {
|
|
||||||
return JSON.parse(localStorage.getItem(AccountService.usersStorageKey) || '{}');
|
|
||||||
}
|
|
||||||
|
|
||||||
private setPrivateUsersRaw(privateUsers: {[key: string]: PrivateUserInfo}): void {
|
|
||||||
localStorage.setItem(AccountService.usersStorageKey, JSON.stringify(privateUsers));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,35 @@
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import {TestSignComponent} from './test-sign/test-sign.component';
|
import {TestSignComponent} from './test-sign/test-sign.component';
|
||||||
import {TestLbryLogInComponent} from './test-lbry-log-in/test-lbry-log-in.component';
|
import {LogInWalletComponent} from './log-in-wallet/log-in-wallet.component';
|
||||||
import {TestSignTransactionComponent} from './test-sign-transaction/test-sign-transaction.component';
|
import {TestSignTransactionComponent} from './test-sign-transaction/test-sign-transaction.component';
|
||||||
import {EmbedComponent} from './embed/embed.component';
|
import {EmbedComponent} from './embed/embed.component';
|
||||||
import {HomeComponent} from './home/home.component';
|
import {HomeComponent} from './home/home.component';
|
||||||
import {LogoutComponent} from './logout/logout.component';
|
import {LogoutComponent} from './logout/logout.component';
|
||||||
import {SignUpComponent} from './sign-up/sign-up.component';
|
import {SignUpComponent} from './sign-up/sign-up.component';
|
||||||
import {LogInComponent} from './log-in/log-in.component';
|
import {LogInAppComponent} from './log-in-app/log-in-app.component';
|
||||||
import {ApproveComponent} from './approve/approve.component';
|
import {ApproveComponent} from './approve/approve.component';
|
||||||
import {LoadSeedComponent} from './load-seed/load-seed.component';
|
|
||||||
|
|
||||||
export class RouteNames {
|
export class RouteNames {
|
||||||
public static TEST_SIGN = 'test-sign';
|
public static TEST_SIGN = 'test-sign';
|
||||||
public static TEST_LBRY_LOG_IN = 'test-lbry-log-in';
|
public static LOG_IN_WALLET = 'log-in-wallet';
|
||||||
public static TEST_SIGN_TRANSACTION = 'test-sign-transaction';
|
public static TEST_SIGN_TRANSACTION = 'test-sign-transaction';
|
||||||
public static EMBED = 'embed';
|
public static EMBED = 'embed';
|
||||||
public static LOGOUT = 'logout';
|
public static LOGOUT = 'logout';
|
||||||
public static SIGN_UP = 'sign-up';
|
public static SIGN_UP = 'sign-up';
|
||||||
public static LOG_IN = 'log-in';
|
public static LOG_IN_APP = 'log-in-app';
|
||||||
public static LOAD_SEED = 'load-seed';
|
|
||||||
public static APPROVE = 'approve';
|
public static APPROVE = 'approve';
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', component: HomeComponent, pathMatch: 'full' },
|
{ path: '', component: HomeComponent, pathMatch: 'full' },
|
||||||
{ path: RouteNames.TEST_SIGN, component: TestSignComponent, pathMatch: 'full' },
|
{ path: RouteNames.TEST_SIGN, component: TestSignComponent, pathMatch: 'full' },
|
||||||
{ path: RouteNames.TEST_LBRY_LOG_IN, component: TestLbryLogInComponent, pathMatch: 'full' },
|
{ path: RouteNames.LOG_IN_WALLET, component: LogInWalletComponent, pathMatch: 'full' },
|
||||||
{ path: RouteNames.TEST_SIGN_TRANSACTION, component: TestSignTransactionComponent, pathMatch: 'full' },
|
{ path: RouteNames.TEST_SIGN_TRANSACTION, component: TestSignTransactionComponent, pathMatch: 'full' },
|
||||||
{ path: RouteNames.EMBED, component: EmbedComponent, pathMatch: 'full' },
|
{ path: RouteNames.EMBED, component: EmbedComponent, pathMatch: 'full' },
|
||||||
{ path: RouteNames.LOGOUT, component: LogoutComponent, pathMatch: 'full' },
|
{ path: RouteNames.LOGOUT, component: LogoutComponent, pathMatch: 'full' },
|
||||||
{ path: RouteNames.SIGN_UP, component: SignUpComponent, pathMatch: 'full' },
|
{ path: RouteNames.SIGN_UP, component: SignUpComponent, pathMatch: 'full' },
|
||||||
{ path: RouteNames.LOG_IN, component: LogInComponent, pathMatch: 'full' },
|
{ path: RouteNames.LOG_IN_APP, component: LogInAppComponent, pathMatch: 'full' },
|
||||||
{ path: RouteNames.LOAD_SEED, component: LoadSeedComponent, pathMatch: 'full' },
|
|
||||||
{ path: RouteNames.APPROVE, component: ApproveComponent, pathMatch: 'full' },
|
{ path: RouteNames.APPROVE, component: ApproveComponent, pathMatch: 'full' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -22,17 +22,12 @@ export class AppComponent implements OnInit {
|
||||||
// load params
|
// load params
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
const accessLevelRequest = params.get('accessLevelRequest');
|
|
||||||
if (accessLevelRequest) {
|
|
||||||
this.globalVars.accessLevelRequest = parseInt(accessLevelRequest, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.get('webview')) {
|
if (params.get('webview')) {
|
||||||
this.globalVars.webview = true;
|
this.globalVars.webview = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.get('testnet')) {
|
if (params.get('testnet')) {
|
||||||
this.globalVars.network = Network.testnet;
|
this.globalVars.network = Network.TestNet;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback should only be used in mobile applications, where payload is passed through URL parameters.
|
// Callback should only be used in mobile applications, where payload is passed through URL parameters.
|
||||||
|
@ -49,6 +44,17 @@ export class AppComponent implements OnInit {
|
||||||
|
|
||||||
if (this.globalVars.callback) {
|
if (this.globalVars.callback) {
|
||||||
// If callback is set, we won't be sending the initialize message.
|
// If callback is set, we won't be sending the initialize message.
|
||||||
|
|
||||||
|
// TODO - Why is it being set to 'localhost'? Seems arbitrary. Seems
|
||||||
|
// like we need this set to the correct value?
|
||||||
|
//
|
||||||
|
// It could be a ui security problem. we say "`this.globalVars.hostname`
|
||||||
|
// wants to do `transaction`". If it's set to "localhost" they might get
|
||||||
|
// the wrong idea. Or maybe I have no idea what this actually means.
|
||||||
|
// Or maybe localhost is actually safe since it's unlikely enough that
|
||||||
|
// somebody would be trying to pwn themselves from localhost.
|
||||||
|
throw "figure this out or delete this code branch"
|
||||||
|
|
||||||
this.globalVars.hostname = 'localhost';
|
this.globalVars.hostname = 'localhost';
|
||||||
this.finishInit();
|
this.finishInit();
|
||||||
} else if (this.globalVars.webview || this.globalVars.inTab || this.globalVars.inFrame()) {
|
} else if (this.globalVars.webview || this.globalVars.inTab || this.globalVars.inFrame()) {
|
||||||
|
|
|
@ -9,22 +9,19 @@ import { EmbedComponent } from './embed/embed.component';
|
||||||
import { HomeComponent } from './home/home.component';
|
import { HomeComponent } from './home/home.component';
|
||||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
import {IdentityService} from './identity.service';
|
import {IdentityService} from './identity.service';
|
||||||
import {CookieModule} from 'ngx-cookie';
|
|
||||||
import { LogoutComponent } from './logout/logout.component';
|
import { LogoutComponent } from './logout/logout.component';
|
||||||
import { BannerComponent } from './banner/banner.component';
|
import { BannerComponent } from './banner/banner.component';
|
||||||
import { SignUpComponent } from './sign-up/sign-up.component';
|
import { SignUpComponent } from './sign-up/sign-up.component';
|
||||||
import {AccountService} from './account.service';
|
import {AccountService} from './account.service';
|
||||||
import {EntropyService} from './entropy.service';
|
import {EntropyService} from './entropy.service';
|
||||||
import { LogInComponent } from './log-in/log-in.component';
|
import { LogInAppComponent } from './log-in-app/log-in-app.component';
|
||||||
import {HttpClientModule} from '@angular/common/http';
|
import {HttpClientModule} from '@angular/common/http';
|
||||||
import { ApproveComponent } from './approve/approve.component';
|
import { ApproveComponent } from './approve/approve.component';
|
||||||
import { LoadSeedComponent } from './load-seed/load-seed.component';
|
|
||||||
import { ErrorCallbackComponent } from './error-callback/error-callback.component';
|
import { ErrorCallbackComponent } from './error-callback/error-callback.component';
|
||||||
import { NgxIntlTelInputModule } from 'ngx-intl-tel-input';
|
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { TestSignComponent } from './test-sign/test-sign.component';
|
import { TestSignComponent } from './test-sign/test-sign.component';
|
||||||
import { TestSignTransactionComponent } from './test-sign-transaction/test-sign-transaction.component';
|
import { TestSignTransactionComponent } from './test-sign-transaction/test-sign-transaction.component';
|
||||||
import { TestLbryLogInComponent } from './test-lbry-log-in/test-lbry-log-in.component'
|
import { LogInWalletComponent } from './log-in-wallet/log-in-wallet.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -34,13 +31,12 @@ import { TestLbryLogInComponent } from './test-lbry-log-in/test-lbry-log-in.comp
|
||||||
LogoutComponent,
|
LogoutComponent,
|
||||||
BannerComponent,
|
BannerComponent,
|
||||||
SignUpComponent,
|
SignUpComponent,
|
||||||
LogInComponent,
|
LogInAppComponent,
|
||||||
ApproveComponent,
|
ApproveComponent,
|
||||||
LoadSeedComponent,
|
|
||||||
ErrorCallbackComponent,
|
ErrorCallbackComponent,
|
||||||
TestSignComponent,
|
TestSignComponent,
|
||||||
TestSignTransactionComponent,
|
TestSignTransactionComponent,
|
||||||
TestLbryLogInComponent,
|
LogInWalletComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -49,10 +45,8 @@ import { TestLbryLogInComponent } from './test-lbry-log-in/test-lbry-log-in.comp
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxIntlTelInputModule,
|
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
CookieModule.forRoot()
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
IdentityService,
|
IdentityService,
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
{{ globalVars.hostname }} wants to {{ transactionDescription }}
|
{{ globalVars.hostname }} wants to {{ transactionDescription }}
|
||||||
</p>
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
<div *ngIf="transactionDeSoSpent">
|
<div *ngIf="transactionSpent">
|
||||||
<p class="pb-15px">
|
<p class="pb-15px">
|
||||||
Total Cost: {{ transactionDeSoSpent }} $DESO
|
Total Cost: {{ transactionSpent }} $DESO
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-end justify-content-between">
|
<div class="d-flex align-items-end justify-content-between">
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {CryptoService} from '../crypto.service';
|
|
||||||
import {IdentityService} from '../identity.service';
|
import {IdentityService} from '../identity.service';
|
||||||
import {AccountService} from '../account.service';
|
import {AccountService} from '../account.service';
|
||||||
import {GlobalVarsService} from '../global-vars.service';
|
import {GlobalVarsService} from '../global-vars.service';
|
||||||
import {SigningService} from '../signing.service';
|
// import {SigningService} from '../signing.service';
|
||||||
import {BackendAPIService, User} from '../backend-api.service';
|
import {BackendAPIService, User} from '../backend-api.service';
|
||||||
import {Observable, of} from 'rxjs';
|
import {Observable, of} from 'rxjs';
|
||||||
import {map} from 'rxjs/operators';
|
import {map} from 'rxjs/operators';
|
||||||
|
@ -33,7 +32,6 @@ import {
|
||||||
TransactionMetadataDAOCoin,
|
TransactionMetadataDAOCoin,
|
||||||
TransactionMetadataTransferDAOCoin
|
TransactionMetadataTransferDAOCoin
|
||||||
} from '../../lib/deso/transaction';
|
} from '../../lib/deso/transaction';
|
||||||
import bs58check from 'bs58check';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-approve',
|
selector: 'app-approve',
|
||||||
|
@ -46,29 +44,32 @@ export class ApproveComponent implements OnInit {
|
||||||
transactionHex: any;
|
transactionHex: any;
|
||||||
username: any;
|
username: any;
|
||||||
transactionDescription: any;
|
transactionDescription: any;
|
||||||
transactionDeSoSpent: string | boolean = false;
|
transactionSpent: string | boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private identityService: IdentityService,
|
private identityService: IdentityService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
public globalVars: GlobalVarsService,
|
public globalVars: GlobalVarsService,
|
||||||
private signingService: SigningService,
|
// private signingService: SigningService,
|
||||||
private backendApi: BackendAPIService,
|
private backendApi: BackendAPIService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.activatedRoute.queryParams.subscribe(params => {
|
this.activatedRoute.queryParams.subscribe(params => {
|
||||||
this.transactionHex = params.tx;
|
this.transactionHex = params.tx;
|
||||||
this.backendApi.GetTransactionSpending(this.transactionHex).subscribe( res => {
|
|
||||||
this.transactionDeSoSpent = res ? this.nanosToUnitString(res) : false;
|
// TODO - for LBRY
|
||||||
});
|
this.transactionSpent = false;
|
||||||
|
|
||||||
const txBytes = new Buffer(this.transactionHex, 'hex');
|
const txBytes = new Buffer(this.transactionHex, 'hex');
|
||||||
this.transaction = Transaction.fromBytes(txBytes)[0];
|
this.transaction = Transaction.fromBytes(txBytes)[0];
|
||||||
this.publicKey = this.base58KeyCheck(this.transaction.publicKey);
|
this.publicKey = this.base58KeyCheck(this.transaction.publicKey);
|
||||||
|
|
||||||
this.generateTransactionDescription();
|
this.generateTransactionDescription();
|
||||||
|
/*
|
||||||
|
TODO this.accountService.getActiveChannelAccessLevel
|
||||||
|
*/
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,22 +78,26 @@ export class ApproveComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
const signedTransactionHex = this.signingService.signTransaction(this.seedHex(), this.transactionHex);
|
// TODO
|
||||||
|
throw "replace all of this transaction parsing and checking with bitcoinjs-lib."
|
||||||
|
/*
|
||||||
|
const seedHex = ""
|
||||||
|
const signedTransactionHex = this.signingService.signTransaction(seedHex, this.transactionHex);
|
||||||
this.finishFlow(signedTransactionHex);
|
this.finishFlow(signedTransactionHex);
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO this.accountService.setAccessLevel if people want to keep allowing the action
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
finishFlow(signedTransactionHex?: string): void {
|
finishFlow(signedTransactionHex?: string): void {
|
||||||
this.identityService.login({
|
this.identityService.login({
|
||||||
users: this.accountService.getEncryptedUsers(),
|
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||||
signedTransactionHex,
|
signedTransactionHex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
seedHex(): string {
|
|
||||||
const encryptedSeedHex = this.accountService.getEncryptedUsers()[this.publicKey].encryptedSeedHex;
|
|
||||||
return this.cryptoService.decryptSeedHex(encryptedSeedHex, this.globalVars.hostname);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateTransactionDescription(): void {
|
generateTransactionDescription(): void {
|
||||||
let description = 'sign an unknown transaction';
|
let description = 'sign an unknown transaction';
|
||||||
let publicKeys: string[] = [];
|
let publicKeys: string[] = [];
|
||||||
|
@ -252,8 +257,13 @@ export class ApproveComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
base58KeyCheck(keyBytes: Uint8Array): string {
|
base58KeyCheck(keyBytes: Uint8Array): string {
|
||||||
const prefix = CryptoService.PUBLIC_KEY_PREFIXES[this.globalVars.network].deso;
|
// TODO
|
||||||
return bs58check.encode(Buffer.from([...prefix, ...keyBytes]));
|
throw "replace all of this transaction parsing and checking with bitcoinjs-lib."
|
||||||
|
return ""
|
||||||
|
// TODO Don't use this.globalVars.network here, use the network specified
|
||||||
|
// in the relevant account.ledger (assuming we even really need network)
|
||||||
|
// const prefix = CryptoService.PUBLIC_KEY_PREFIXES[this.globalVars.network as Network].deso;
|
||||||
|
// return bs58check.encode(Buffer.from([...prefix, ...keyBytes]));
|
||||||
}
|
}
|
||||||
|
|
||||||
hexNanosToUnitString(nanos: Buffer): string {
|
hexNanosToUnitString(nanos: Buffer): string {
|
||||||
|
@ -273,7 +283,7 @@ export class ApproveComponent implements OnInit {
|
||||||
return of(description);
|
return of(description);
|
||||||
}
|
}
|
||||||
// Otherwise, we hit get-users-stateless to fetch profiles.
|
// Otherwise, we hit get-users-stateless to fetch profiles.
|
||||||
return this.backendApi.GetUsersStateless(publicKeys, true).pipe((map(res => {
|
return this.backendApi.GetUsersStateless(publicKeys).pipe((map(res => {
|
||||||
const userList = res.UserList;
|
const userList = res.UserList;
|
||||||
// If the response has no users, return the description as is.
|
// If the response has no users, return the description as is.
|
||||||
if (userList.length === 0) {
|
if (userList.length === 0) {
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
import { Injectable } from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient} from '@angular/common/http';
|
||||||
import {Observable, of} from 'rxjs';
|
import {Observable, of} from 'rxjs';
|
||||||
import {catchError, map} from 'rxjs/operators';
|
import {catchError, map} from 'rxjs/operators';
|
||||||
import {environment} from '../environments/environment';
|
import {environment} from '../environments/environment';
|
||||||
import {SigningService} from './signing.service';
|
|
||||||
import {AccountService} from './account.service';
|
import {AccountService} from './account.service';
|
||||||
import {CryptoService} from './crypto.service';
|
|
||||||
import {GlobalVarsService} from './global-vars.service';
|
|
||||||
import {UserProfile} from '../types/identity';
|
|
||||||
|
|
||||||
export class ProfileEntryResponse {
|
export class ProfileEntryResponse {
|
||||||
Username: string | null = null;
|
Username: string | null = null;
|
||||||
Description: string | null = null;
|
|
||||||
ProfilePic?: string;
|
|
||||||
PublicKeyBase58Check?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
|
@ -25,98 +18,83 @@ export class User {
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class BackendAPIService {
|
export class BackendAPIService {
|
||||||
|
walletSyncEndpoint = `https://${environment.walletSyncHostname}/api/v0`;
|
||||||
|
|
||||||
endpoint = `https://${environment.nodeHostname}/api/v0`;
|
endpoint = `https://${environment.nodeHostname}/api/v0`;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private httpClient: HttpClient,
|
private httpClient: HttpClient,
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private signingService: SigningService,
|
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private globalVars: GlobalVarsService,
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
post(path: string, body: any): Observable<any> {
|
post(path: string, body: any): Observable<any> {
|
||||||
return this.httpClient.post<any>(`${this.endpoint}/${path}`, body);
|
return this.httpClient.post<any>(`${this.endpoint}/${path}`, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtPost(path: string, publicKey: string, body: any): Observable<any> {
|
|
||||||
const publicUserInfo = this.accountService.getEncryptedUsers()[publicKey];
|
|
||||||
if (!publicUserInfo) {
|
|
||||||
return of(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const seedHex = this.cryptoService.decryptSeedHex(publicUserInfo.encryptedSeedHex, this.globalVars.hostname);
|
|
||||||
const jwt = this.signingService.signJWT(seedHex);
|
|
||||||
return this.post(path, {...body, ...{JWT: jwt}});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error parsing
|
|
||||||
stringifyError(err: any): string {
|
|
||||||
return err?.error?.error || JSON.stringify(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// When SkipForLeaderboard is true, this endpoint only returns ProfileEntryResponse, IsGraylisted, IsBlacklisted,
|
|
||||||
// IsAdmin, and IsSuperAdmin for each user.
|
|
||||||
// When SkipForLeaderboard is false, we also fetch the user's balance, profiles this user follows, hodlings, and
|
|
||||||
// UserMetadata. Oftentimes, this information is not needed and excluding it significantly improves performance.
|
|
||||||
GetUsersStateless(
|
GetUsersStateless(
|
||||||
publicKeys: string[], SkipForLeaderboard: boolean = false,
|
publicKeys: string[]
|
||||||
): Observable<{ UserList: User[]}> {
|
): Observable<{ UserList: User[]}> {
|
||||||
return this.httpClient.post<any>(
|
return this.httpClient.post<any>(
|
||||||
`${this.endpoint}/get-users-stateless`,
|
`${this.endpoint}/get-users-stateless`,
|
||||||
{
|
{
|
||||||
PublicKeysBase58Check: publicKeys,
|
PublicKeysBase58Check: publicKeys,
|
||||||
SkipForLeaderboard,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
GetUserProfiles(
|
GetUsernames(
|
||||||
publicKeys: string[]
|
publicKeys: string[]
|
||||||
): Observable<{[key: string]: UserProfile}> {
|
): Observable<{[key: string]: string}> {
|
||||||
const userProfiles: {[key: string]: any} = {};
|
const usernames: {[key: string]: any} = {};
|
||||||
const req = this.GetUsersStateless(publicKeys, true);
|
const req = this.GetUsersStateless(publicKeys);
|
||||||
if (publicKeys.length > 0) {
|
if (publicKeys.length > 0) {
|
||||||
return req.pipe(
|
return req.pipe(
|
||||||
map( res => {
|
map( res => {
|
||||||
for (const user of res.UserList) {
|
for (const user of res.UserList) {
|
||||||
userProfiles[user.PublicKeyBase58Check] = {
|
usernames[user.PublicKeyBase58Check] = user.ProfileEntryResponse?.Username
|
||||||
username: user.ProfileEntryResponse?.Username,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return userProfiles;
|
return usernames;
|
||||||
})
|
})
|
||||||
).pipe(
|
).pipe(
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
for(const publicKey of publicKeys) {
|
for(const publicKey of publicKeys) {
|
||||||
userProfiles[publicKey] = {};
|
usernames[publicKey] = "";
|
||||||
}
|
}
|
||||||
return of(userProfiles);
|
return of(usernames);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return of(userProfiles);
|
return of(usernames);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GetTransactionSpending(
|
// TODO - WIP, not using for now.
|
||||||
transactionHex: string
|
|
||||||
): Observable<number> {
|
// This isn't what the current wallet sync API looks like, but it will
|
||||||
const req = this.httpClient.post<any>(
|
// likely change anyway. So this is an approximation with a stub for the time being.
|
||||||
`${this.endpoint}/get-transaction-spending`,
|
WalletSyncLogin(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Observable<{bodyJson: string, signature: string} | null> {
|
||||||
|
|
||||||
|
// A stub for now
|
||||||
|
const wallet : object | null = this.accountService.getWallet();
|
||||||
|
if (wallet === null) {
|
||||||
|
return of(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return of({
|
||||||
|
bodyJson: JSON.stringify(wallet, null, 2),
|
||||||
|
signature: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Later we'll do something like this...
|
||||||
|
return this.httpClient.post<any>(
|
||||||
|
`${this.walletSyncEndpoint}/log-in`,
|
||||||
{
|
{
|
||||||
TransactionHex: transactionHex,
|
username: username,
|
||||||
|
password: password,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return req.pipe(
|
|
||||||
map( res => {
|
|
||||||
return res.TotalSpendingNanos as number;
|
|
||||||
})
|
|
||||||
).pipe(
|
|
||||||
catchError(() => {
|
|
||||||
return of(0);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,113 +1,32 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import HDNode from 'hdkey';
|
|
||||||
import * as bip39 from 'bip39';
|
|
||||||
import HDKey from 'hdkey';
|
|
||||||
import {ec as EC} from 'elliptic';
|
import {ec as EC} from 'elliptic';
|
||||||
import bs58check from 'bs58check';
|
import bs58check from 'bs58check';
|
||||||
import {CookieService} from 'ngx-cookie';
|
|
||||||
import {createHmac, createCipher, createDecipher, randomBytes} from 'crypto';
|
import {createHmac, createCipher, createDecipher, randomBytes} from 'crypto';
|
||||||
import {AccessLevel, Network} from '../types/identity';
|
import {AccessLevel} from '../types/identity';
|
||||||
import { GlobalVarsService } from './global-vars.service';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class CryptoService {
|
export class CryptoService {
|
||||||
|
|
||||||
constructor(
|
constructor() {}
|
||||||
private cookieService: CookieService,
|
|
||||||
private globalVars: GlobalVarsService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
// TODO - LBRY?
|
|
||||||
static PUBLIC_KEY_PREFIXES = {
|
|
||||||
mainnet: {
|
|
||||||
deso: [0xcd, 0x14, 0x0],
|
|
||||||
},
|
|
||||||
testnet: {
|
|
||||||
deso: [0x11, 0xc2, 0x0],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Safari only lets us store things in cookies
|
|
||||||
mustUseStorageAccess(): boolean {
|
|
||||||
// Webviews have full control over storage access
|
|
||||||
if (this.globalVars.webview) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportsStorageAccess = typeof document.hasStorageAccess === 'function';
|
|
||||||
const isChrome = navigator.userAgent.indexOf('Chrome') > -1;
|
|
||||||
const isSafari = !isChrome && navigator.userAgent.indexOf('Safari') > -1;
|
|
||||||
|
|
||||||
// Firefox and Edge support the storage access API but do not enforce it.
|
|
||||||
// For now, only use cookies if we support storage access and use Safari.
|
|
||||||
const mustUseStorageAccess = supportsStorageAccess && isSafari;
|
|
||||||
|
|
||||||
return mustUseStorageAccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 32 bytes = 256 bits is plenty of entropy for encryption
|
// 32 bytes = 256 bits is plenty of entropy for encryption
|
||||||
newEncryptionKey(): string {
|
newEncryptionKey(): string {
|
||||||
return randomBytes(32).toString('hex');
|
return randomBytes(32).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO we won't need this soon right?
|
||||||
seedHexEncryptionStorageKey(hostname: string): string {
|
seedHexEncryptionStorageKey(hostname: string): string {
|
||||||
return `seed-hex-key-${hostname}`;
|
return `seed-hex-key-${hostname}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alternate plan is to use this same system, but instead of encrypting the
|
|
||||||
// seed and sending it back and forth, we do the wallet. It may be superior
|
|
||||||
// because at most times the decrypted wallet is not accessible anywhere
|
|
||||||
// without sending a message between the app and identity service. But we'd
|
|
||||||
// have to trust that we'll never accidentally send the wallet to the app
|
|
||||||
// unencrypted.
|
|
||||||
walletStorageKey(hostname: string): string {
|
|
||||||
return `wallet-key-${hostname}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasWallet(hostname: string): boolean {
|
|
||||||
const storageKey = this.walletStorageKey(hostname);
|
|
||||||
|
|
||||||
if (this.mustUseStorageAccess()) {
|
|
||||||
return !!this.cookieService.get(storageKey);
|
|
||||||
} else {
|
|
||||||
return !!localStorage.getItem(storageKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hasSeedHexEncryptionKey(hostname: string): boolean {
|
hasSeedHexEncryptionKey(hostname: string): boolean {
|
||||||
const storageKey = this.seedHexEncryptionStorageKey(hostname);
|
const storageKey = this.seedHexEncryptionStorageKey(hostname);
|
||||||
|
|
||||||
if (this.mustUseStorageAccess()) {
|
|
||||||
return !!this.cookieService.get(storageKey);
|
|
||||||
} else {
|
|
||||||
return !!localStorage.getItem(storageKey);
|
return !!localStorage.getItem(storageKey);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
getWallet(hostname: string): object | null {
|
|
||||||
const storageKey = this.walletStorageKey(hostname);
|
|
||||||
let walletStr
|
|
||||||
if (this.mustUseStorageAccess()) {
|
|
||||||
walletStr = this.cookieService.get(storageKey);
|
|
||||||
} else {
|
|
||||||
walletStr = localStorage.getItem(storageKey);
|
|
||||||
}
|
|
||||||
return JSON.parse(walletStr || 'null')
|
|
||||||
}
|
|
||||||
|
|
||||||
putWallet(hostname: string, wallet: object | null) {
|
|
||||||
const storageKey = this.walletStorageKey(hostname);
|
|
||||||
|
|
||||||
if (this.mustUseStorageAccess()) {
|
|
||||||
this.cookieService.put(storageKey, JSON.stringify(wallet), {
|
|
||||||
expires: new Date('2100/01/01 00:00:00'),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(wallet));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Place a seed encryption key in storage. If reset is set to true, the
|
// Place a seed encryption key in storage. If reset is set to true, the
|
||||||
// previous key is overwritten, which is useful in logging out users.
|
// previous key is overwritten, which is useful in logging out users.
|
||||||
|
@ -115,21 +34,11 @@ export class CryptoService {
|
||||||
const storageKey = this.seedHexEncryptionStorageKey(hostname);
|
const storageKey = this.seedHexEncryptionStorageKey(hostname);
|
||||||
let encryptionKey;
|
let encryptionKey;
|
||||||
|
|
||||||
if (this.mustUseStorageAccess()) {
|
|
||||||
encryptionKey = this.cookieService.get(storageKey);
|
|
||||||
if (!encryptionKey || reset) {
|
|
||||||
encryptionKey = this.newEncryptionKey();
|
|
||||||
this.cookieService.put(storageKey, encryptionKey, {
|
|
||||||
expires: new Date('2100/01/01 00:00:00'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
encryptionKey = localStorage.getItem(storageKey) || '';
|
encryptionKey = localStorage.getItem(storageKey) || '';
|
||||||
if (!encryptionKey || reset) {
|
if (!encryptionKey || reset) {
|
||||||
encryptionKey = this.newEncryptionKey();
|
encryptionKey = this.newEncryptionKey();
|
||||||
localStorage.setItem(storageKey, encryptionKey);
|
localStorage.setItem(storageKey, encryptionKey);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If the encryption key is unset or malformed we need to stop
|
// If the encryption key is unset or malformed we need to stop
|
||||||
// everything to avoid returning unencrypted information.
|
// everything to avoid returning unencrypted information.
|
||||||
|
@ -165,40 +74,6 @@ export class CryptoService {
|
||||||
return hmac === this.accessLevelHmac(accessLevel, seedHex);
|
return hmac === this.accessLevelHmac(accessLevel, seedHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptedSeedHexToPrivateKey(encryptedSeedHex: string, domain: string): EC.KeyPair {
|
|
||||||
const seedHex = this.decryptSeedHex(encryptedSeedHex, domain);
|
|
||||||
return this.seedHexToPrivateKey(seedHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
mnemonicToKeychain(mnemonic: string, extraText?: string, nonStandard?: boolean): HDNode {
|
|
||||||
const seed = bip39.mnemonicToSeedSync(mnemonic, extraText);
|
|
||||||
// @ts-ignore
|
|
||||||
return HDKey.fromMasterSeed(seed).derive('m/44\'/0\'/0\'/0/0', nonStandard);
|
|
||||||
}
|
|
||||||
|
|
||||||
keychainToSeedHex(keychain: HDNode): string {
|
|
||||||
return keychain.privateKey.toString('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
seedHexToPrivateKey(seedHex: string): EC.KeyPair {
|
|
||||||
const ec = new EC('secp256k1');
|
|
||||||
return ec.keyFromPrivate(seedHex);
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKeyToDeSoPublicKey(privateKey: EC.KeyPair, network: Network): string {
|
|
||||||
const prefix = CryptoService.PUBLIC_KEY_PREFIXES[network].deso;
|
|
||||||
const key = privateKey.getPublic().encode('array', true);
|
|
||||||
const prefixAndKey = Uint8Array.from([...prefix, ...key]);
|
|
||||||
|
|
||||||
return bs58check.encode(prefixAndKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
publicKeyToDeSoPublicKey(publicKey: EC.KeyPair, network: Network): string {
|
|
||||||
const prefix = CryptoService.PUBLIC_KEY_PREFIXES[network].deso;
|
|
||||||
const key = publicKey.getPublic().encode('array', true);
|
|
||||||
return bs58check.encode(Buffer.from([...prefix, ...key]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode public key base58check to Buffer of secp256k1 public key
|
// Decode public key base58check to Buffer of secp256k1 public key
|
||||||
publicKeyToECBuffer(publicKey: string): Buffer {
|
publicKeyToECBuffer(publicKey: string): Buffer {
|
||||||
// Sanity check similar to Base58CheckDecodePrefix from core/lib/base58.go
|
// Sanity check similar to Base58CheckDecodePrefix from core/lib/base58.go
|
||||||
|
@ -213,4 +88,16 @@ export class CryptoService {
|
||||||
|
|
||||||
return new Buffer(publicKeyEC.getPublic('array'));
|
return new Buffer(publicKeyEC.getPublic('array'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO check that the signature for the walletStr is valid
|
||||||
|
checkSig(walletStr: string, walletSignature: string): boolean {
|
||||||
|
throw "implement me"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO find errors in the wallet. missing fields, etc. json-schema
|
||||||
|
validateWallet(wallet: object): string | null {
|
||||||
|
throw "implement me"
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import {AccessLevel, Network} from '../types/identity';
|
import {Network} from '../types/identity';
|
||||||
import {environment} from '../environments/environment';
|
import {environment} from '../environments/environment';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class GlobalVarsService {
|
export class GlobalVarsService {
|
||||||
network = Network.mainnet;
|
network : Network = Network.MainNet;
|
||||||
hostname = '';
|
hostname = '';
|
||||||
accessLevelRequest = AccessLevel.ApproveAll;
|
|
||||||
|
|
||||||
inTab = !!window.opener;
|
inTab = !!window.opener;
|
||||||
webview = false;
|
webview = false;
|
||||||
|
@ -17,7 +16,6 @@ export class GlobalVarsService {
|
||||||
callback = '';
|
callback = '';
|
||||||
callbackInvalid = false;
|
callbackInvalid = false;
|
||||||
|
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
inFrame(): boolean {
|
inFrame(): boolean {
|
||||||
|
|
16
src/app/hub.service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HubService } from './account.service';
|
||||||
|
|
||||||
|
describe('HubService', () => {
|
||||||
|
let service: HubService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(HubService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
44
src/app/hub.service.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {HttpClient} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {environment} from '../environments/environment';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class HubService {
|
||||||
|
hubEndpoint = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private httpClient: HttpClient,
|
||||||
|
) {
|
||||||
|
const hubHostname = environment.hubHostnames[
|
||||||
|
Math.floor(Math.random() * environment.hubHostnames.length)
|
||||||
|
];
|
||||||
|
this.hubEndpoint = `https://${hubHostname}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
post(path: string, body: any): Observable<any> {
|
||||||
|
return this.httpClient.post<any>(`${this.hubEndpoint}/${path}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just for stubbing until we get the real thing
|
||||||
|
private rndStr() {
|
||||||
|
return (Math.random() + 1).toString(16).substring(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obviously just a stub for the actual API
|
||||||
|
public getChannels(xPubs: string[]): any {
|
||||||
|
|
||||||
|
return [{
|
||||||
|
claimId: this.rndStr(),
|
||||||
|
handle: '@test-' + this.rndStr(),
|
||||||
|
pubKeyAddress: this.rndStr(),
|
||||||
|
}, {
|
||||||
|
claimId: this.rndStr(),
|
||||||
|
handle: '@test-' + this.rndStr(),
|
||||||
|
pubKeyAddress: this.rndStr(),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,36 +1,11 @@
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {Observable, Subject} from 'rxjs';
|
import {Observable, Subject} from 'rxjs';
|
||||||
import {v4 as uuid} from 'uuid';
|
import {v4 as uuid} from 'uuid';
|
||||||
import {AccessLevel, PublicUserInfo} from '../types/identity';
|
import {AccessLevel, PublicChannelInfo} from '../types/identity';
|
||||||
import {CryptoService} from './crypto.service';
|
import {CryptoService} from './crypto.service';
|
||||||
import {GlobalVarsService} from './global-vars.service';
|
import {GlobalVarsService} from './global-vars.service';
|
||||||
import {CookieService} from 'ngx-cookie';
|
|
||||||
import {SigningService} from './signing.service';
|
import {SigningService} from './signing.service';
|
||||||
import {HttpParams} from '@angular/common/http';
|
import {HttpParams} from '@angular/common/http';
|
||||||
import {
|
|
||||||
Transaction,
|
|
||||||
TransactionMetadataBasicTransfer,
|
|
||||||
TransactionMetadataBitcoinExchange,
|
|
||||||
TransactionMetadataCreatorCoin,
|
|
||||||
TransactionMetadataCreatorCoinTransfer,
|
|
||||||
TransactionMetadataFollow,
|
|
||||||
TransactionMetadataLike,
|
|
||||||
TransactionMetadataPrivateMessage,
|
|
||||||
TransactionMetadataSubmitPost,
|
|
||||||
TransactionMetadataSwapIdentity,
|
|
||||||
TransactionMetadataUpdateBitcoinUSDExchangeRate,
|
|
||||||
TransactionMetadataUpdateGlobalParams,
|
|
||||||
TransactionMetadataUpdateProfile,
|
|
||||||
TransactionMetadataNFTTransfer,
|
|
||||||
TransactionMetadataAcceptNFTTransfer,
|
|
||||||
TransactionMetadataBurnNFT,
|
|
||||||
TransactionMetadataNFTBid,
|
|
||||||
TransactionMetadataAcceptNFTBid,
|
|
||||||
TransactionMetadataUpdateNFT,
|
|
||||||
TransactionMetadataCreateNFT,
|
|
||||||
TransactionMetadataDAOCoin,
|
|
||||||
TransactionMetadataTransferDAOCoin
|
|
||||||
} from '../lib/deso/transaction';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -48,12 +23,30 @@ export class IdentityService {
|
||||||
constructor(
|
constructor(
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private globalVars: GlobalVarsService,
|
private globalVars: GlobalVarsService,
|
||||||
private cookieService: CookieService,
|
|
||||||
private signingService: SigningService,
|
private signingService: SigningService,
|
||||||
) {
|
) {
|
||||||
window.addEventListener('message', (event) => this.handleMessage(event));
|
window.addEventListener('message', (event) => this.handleMessage(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safari only lets us store things in cookies
|
||||||
|
mustUseStorageAccess(): boolean {
|
||||||
|
// Webviews have full control over storage access
|
||||||
|
// TODO why do we trust the app to send this properly
|
||||||
|
if (this.globalVars.webview) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportsStorageAccess = typeof document.hasStorageAccess === 'function';
|
||||||
|
const isChrome = navigator.userAgent.indexOf('Chrome') > -1;
|
||||||
|
const isSafari = !isChrome && navigator.userAgent.indexOf('Safari') > -1;
|
||||||
|
|
||||||
|
// Firefox and Edge support the storage access API but do not enforce it.
|
||||||
|
// For now, only use cookies if we support storage access and use Safari.
|
||||||
|
const mustUseStorageAccess = supportsStorageAccess && isSafari;
|
||||||
|
|
||||||
|
return mustUseStorageAccess;
|
||||||
|
}
|
||||||
|
|
||||||
// Outgoing Messages
|
// Outgoing Messages
|
||||||
|
|
||||||
initialize(): Observable<any> {
|
initialize(): Observable<any> {
|
||||||
|
@ -65,8 +58,7 @@ export class IdentityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
login(payload: {
|
login(payload: {
|
||||||
users: {[key: string]: PublicUserInfo},
|
channel: PublicChannelInfo | null,
|
||||||
publicKeyAdded?: string,
|
|
||||||
signedUp?: boolean
|
signedUp?: boolean
|
||||||
signedTransactionHex?: string,
|
signedTransactionHex?: string,
|
||||||
addresses?: string[],
|
addresses?: string[],
|
||||||
|
@ -89,11 +81,14 @@ export class IdentityService {
|
||||||
// Incoming Messages
|
// Incoming Messages
|
||||||
|
|
||||||
private handleSign(data: any): void {
|
private handleSign(data: any): void {
|
||||||
const { id, payload: { encryptedSeedHex, transactionHex } } = data;
|
// TODO
|
||||||
|
throw "implement for lbry"
|
||||||
|
/*
|
||||||
|
const transaction or action details = data;
|
||||||
|
|
||||||
// This will tell us whether we need full signing access or just ApproveLarge
|
// This will tell us whether we need full signing access or just ApproveLarge
|
||||||
// level of access.
|
// level of access.
|
||||||
const requiredAccessLevel = this.getRequiredAccessLevel(transactionHex);
|
const requiredAccessLevel = this.getRequiredAccessLevel(transaction or action details);
|
||||||
|
|
||||||
// In the case that approve() fails, it responds with a message indicating
|
// In the case that approve() fails, it responds with a message indicating
|
||||||
// that approvalRequired = true, which the caller can then uses to trigger
|
// that approvalRequired = true, which the caller can then uses to trigger
|
||||||
|
@ -102,24 +97,27 @@ export class IdentityService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get to this point, no approval UI was required. This typically
|
// TODO - this.signingService.signPSBT instead
|
||||||
// happens if the caller has full signing access or signing access for
|
if it's a transaction
|
||||||
// non-spending txns such as like, post, update profile, etc. In the
|
const signedTransactionHex = this.signingService.signPSBT(transaction details);
|
||||||
// latter case we need a subsequent check to ensure that the txn is not
|
else // just an action
|
||||||
// sending money to any public keys other than the sender himself.
|
const signedActionHex = this.signingService.signActiov(action details);
|
||||||
if (!this.approveSpending(data)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const seedHex = this.cryptoService.decryptSeedHex(encryptedSeedHex, this.globalVars.hostname);
|
|
||||||
const signedTransactionHex = this.signingService.signTransaction(seedHex, transactionHex);
|
|
||||||
|
|
||||||
|
// TODO figure this out...
|
||||||
this.respond(id, {
|
this.respond(id, {
|
||||||
signedTransactionHex,
|
signedTransactionHex,
|
||||||
|
signedActionHex,
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleJwt(data: any): void {
|
private handleJwt(data: any): void {
|
||||||
|
// Give a permission token that expires in 10 minutes. DeSo apps use it for
|
||||||
|
// things like image uploading. Creation of this token is subject to same
|
||||||
|
// access level requirements as actions and transactions.
|
||||||
|
// TODO - make this work with LBRY. Or, nix it if we know we don't need it.
|
||||||
|
// Perhaps this will actually *be* our "actions"?
|
||||||
|
|
||||||
if (!this.approve(data, AccessLevel.ApproveAll)) {
|
if (!this.approve(data, AccessLevel.ApproveAll)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -136,7 +134,7 @@ export class IdentityService {
|
||||||
private async handleInfo(event: MessageEvent): Promise<void> {
|
private async handleInfo(event: MessageEvent): Promise<void> {
|
||||||
// check storage access API
|
// check storage access API
|
||||||
let hasStorageAccess = true;
|
let hasStorageAccess = true;
|
||||||
if (this.cryptoService.mustUseStorageAccess()) {
|
if (this.mustUseStorageAccess()) {
|
||||||
hasStorageAccess = await document.hasStorageAccess();
|
hasStorageAccess = await document.hasStorageAccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,57 +146,49 @@ export class IdentityService {
|
||||||
hasLocalStorageAccess = false;
|
hasLocalStorageAccess = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for cookie access
|
// TODO - Sort out the storage access issue:
|
||||||
this.cookieService.put('deso-test-access', 'true');
|
//
|
||||||
const hasCookieAccess = !!this.cookieService.get('deso-test-access');
|
// There was a part of the code that was defaulting to cookies if
|
||||||
|
// this.mustUseStorageAccess() was true (only applies to Safari). I don't
|
||||||
|
// want to do that because I'm afraid the data we'll be storing won't fit.
|
||||||
|
// So, I'm going to consider browsers with this.mustUseStorageAccess() as
|
||||||
|
// unsupported until we can get back and figure out why DeSo chose to go
|
||||||
|
// with cookies for those cases.
|
||||||
|
//
|
||||||
|
// It looks like the issue is that Safari only supports saving in cookies
|
||||||
|
// (per the comment above this.mustUseStorageAccess(), which I moved from
|
||||||
|
// DeSo's crypto.service). If that's the case, I don't understand how it
|
||||||
|
// was able to save users and levels in the identity service. Someone who
|
||||||
|
// understands should look into that.
|
||||||
|
//
|
||||||
|
// PERHAPS it's that DeSo only strictly need certain things to be in
|
||||||
|
// localStorage, and everything else (like the encryption key) could fit
|
||||||
|
// in the cookie. Maybe we could store the wallet sync password in the
|
||||||
|
// cookie and reconstruct the wallet and channels on start. The problem
|
||||||
|
// would be resetting the access levels each time. Maybe that will just
|
||||||
|
// be the cost of using Safari.
|
||||||
|
//
|
||||||
|
// See:
|
||||||
|
// https://github.com/deso-protocol/identity/blob/0543c40cb4e7e39cc9098554f99c27649e3d1d03/src/app/crypto.service.ts#L36
|
||||||
|
// https://github.com/deso-protocol/identity/blob/0543c40cb4e7e39cc9098554f99c27649e3d1d03/src/app/identity.service.ts#L261
|
||||||
|
// https://github.com/deso-protocol/identity/pull/50/
|
||||||
|
|
||||||
// store if browser is supported or not
|
// store if browser is supported or not
|
||||||
this.browserSupported = hasCookieAccess || hasLocalStorageAccess;
|
this.browserSupported = hasLocalStorageAccess && !this.mustUseStorageAccess();
|
||||||
|
|
||||||
this.respond(event.data.id, {
|
this.respond(event.data.id, {
|
||||||
hasCookieAccess,
|
|
||||||
hasStorageAccess,
|
hasStorageAccess,
|
||||||
hasLocalStorageAccess,
|
hasLocalStorageAccess,
|
||||||
browserSupported: this.browserSupported,
|
browserSupported: this.browserSupported,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Access levels
|
// Access levels
|
||||||
|
|
||||||
private getRequiredAccessLevel(transactionHex: string): AccessLevel {
|
// TODO implement for lbry
|
||||||
const txBytes = new Buffer(transactionHex, 'hex');
|
// private getRequiredAccessLevel(transactionHex: string): AccessLevel {
|
||||||
const transaction = Transaction.fromBytes(txBytes)[0] as Transaction<any>;
|
// switch case on the type of transaction or action, and return the required access level
|
||||||
|
// unless required access level is going to be case by case for each user
|
||||||
switch (transaction.metadata.constructor) {
|
|
||||||
case TransactionMetadataBasicTransfer:
|
|
||||||
case TransactionMetadataBitcoinExchange:
|
|
||||||
case TransactionMetadataUpdateBitcoinUSDExchangeRate:
|
|
||||||
case TransactionMetadataCreatorCoin:
|
|
||||||
case TransactionMetadataCreatorCoinTransfer:
|
|
||||||
case TransactionMetadataSwapIdentity:
|
|
||||||
case TransactionMetadataUpdateGlobalParams:
|
|
||||||
case TransactionMetadataUpdateProfile:
|
|
||||||
case TransactionMetadataCreateNFT:
|
|
||||||
case TransactionMetadataUpdateNFT:
|
|
||||||
case TransactionMetadataAcceptNFTBid:
|
|
||||||
case TransactionMetadataNFTBid:
|
|
||||||
case TransactionMetadataNFTTransfer:
|
|
||||||
case TransactionMetadataAcceptNFTTransfer:
|
|
||||||
case TransactionMetadataBurnNFT:
|
|
||||||
case TransactionMetadataDAOCoin:
|
|
||||||
case TransactionMetadataTransferDAOCoin:
|
|
||||||
return AccessLevel.Full;
|
|
||||||
|
|
||||||
case TransactionMetadataFollow:
|
|
||||||
case TransactionMetadataPrivateMessage:
|
|
||||||
case TransactionMetadataSubmitPost:
|
|
||||||
case TransactionMetadataLike:
|
|
||||||
return AccessLevel.ApproveLarge;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AccessLevel.Full;
|
|
||||||
}
|
|
||||||
|
|
||||||
private hasAccessLevel(data: any, requiredAccessLevel: AccessLevel): boolean {
|
private hasAccessLevel(data: any, requiredAccessLevel: AccessLevel): boolean {
|
||||||
const { payload: { encryptedSeedHex, accessLevel, accessLevelHmac }} = data;
|
const { payload: { encryptedSeedHex, accessLevel, accessLevelHmac }} = data;
|
||||||
|
@ -210,25 +200,6 @@ export class IdentityService {
|
||||||
return this.cryptoService.validAccessLevelHmac(accessLevel, seedHex, accessLevelHmac);
|
return this.cryptoService.validAccessLevelHmac(accessLevel, seedHex, accessLevelHmac);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method checks if transaction in the payload has correct outputs for requested AccessLevel.
|
|
||||||
private approveSpending(data: any): boolean {
|
|
||||||
const { payload: { accessLevel, transactionHex }} = data;
|
|
||||||
|
|
||||||
// If the requested access level is ApproveLarge, we want to confirm that transaction doesn't
|
|
||||||
// attempt sending $DESO to a non-owner public key. If it does, we respond with approvalRequired.
|
|
||||||
if (accessLevel === AccessLevel.ApproveLarge) {
|
|
||||||
const txBytes = new Buffer(transactionHex, 'hex');
|
|
||||||
const transaction = Transaction.fromBytes(txBytes)[0] as Transaction<any>;
|
|
||||||
for (const output of transaction.outputs) {
|
|
||||||
if (output.publicKey.toString('hex') !== transaction.publicKey.toString('hex')) {
|
|
||||||
this.respond(data.id, {approvalRequired: true});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private approve(data: any, accessLevel: AccessLevel): boolean {
|
private approve(data: any, accessLevel: AccessLevel): boolean {
|
||||||
const hasAccess = this.hasAccessLevel(data, accessLevel);
|
const hasAccess = this.hasAccessLevel(data, accessLevel);
|
||||||
const hasEncryptionKey = this.cryptoService.hasSeedHexEncryptionKey(this.globalVars.hostname);
|
const hasEncryptionKey = this.cryptoService.hasSeedHexEncryptionKey(this.globalVars.hostname);
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
<app-banner></app-banner>
|
|
||||||
|
|
||||||
<div class="page-container" *ngIf="globalVars.inTab || globalVars.webview || globalVars.callback">
|
|
||||||
<div class="title-text">
|
|
||||||
Log in to {{ globalVars.hostname }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="main-text mb-20px">
|
|
||||||
Enter your DeSo seed phrase to load your account
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div *ngIf="loadSeedError" class="alert alert-danger mt-15px">
|
|
||||||
{{ loadSeedError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-input-container">
|
|
||||||
<textarea [(ngModel)]="mnemonic"
|
|
||||||
class="text-input"
|
|
||||||
rows="4"
|
|
||||||
placeholder="Enter your DeSo seed phrase"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-text mb-20px">
|
|
||||||
If you have a passphrase, enter it below.
|
|
||||||
</div>
|
|
||||||
<div class="text-input-container">
|
|
||||||
<textarea [(ngModel)]="extraText"
|
|
||||||
class="text-input"
|
|
||||||
rows="4"
|
|
||||||
placeholder="Enter your passphrase"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button (click)="clickLoadAccount()"
|
|
||||||
class="button button-primary button-large">
|
|
||||||
Load Account
|
|
||||||
</button>
|
|
||||||
</div>
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import {AccountService} from '../account.service';
|
|
||||||
import {CryptoService} from '../crypto.service';
|
|
||||||
import {EntropyService} from '../entropy.service';
|
|
||||||
import {GlobalVarsService} from '../global-vars.service';
|
|
||||||
import {Router} from '@angular/router';
|
|
||||||
import {RouteNames} from '../app-routing.module';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-load-seed',
|
|
||||||
templateUrl: './load-seed.component.html',
|
|
||||||
styleUrls: ['./load-seed.component.scss']
|
|
||||||
})
|
|
||||||
export class LoadSeedComponent implements OnInit {
|
|
||||||
// Loading an account
|
|
||||||
loadSeedError = '';
|
|
||||||
mnemonic = '';
|
|
||||||
extraText = '';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private accountService: AccountService,
|
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private entropyService: EntropyService,
|
|
||||||
public globalVars: GlobalVarsService,
|
|
||||||
private router: Router,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
clickLoadAccount(): void {
|
|
||||||
// Store mnemonic and extraText locally because we clear them below and otherwise
|
|
||||||
// they don't get saved in local storage reliably
|
|
||||||
const mnemonic = this.mnemonic;
|
|
||||||
const extraText = this.extraText;
|
|
||||||
const network = this.globalVars.network;
|
|
||||||
|
|
||||||
if (!this.entropyService.isValidMnemonic(mnemonic)) {
|
|
||||||
this.loadSeedError = 'Invalid mnemonic';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, extraText);
|
|
||||||
|
|
||||||
this.accountService.addUser(keychain, mnemonic, extraText, network);
|
|
||||||
|
|
||||||
// Clear the form
|
|
||||||
this.mnemonic = '';
|
|
||||||
this.extraText = '';
|
|
||||||
|
|
||||||
this.router.navigate(['/', RouteNames.LOG_IN], {queryParamsHandling: 'merge'});
|
|
||||||
}
|
|
||||||
}
|
|
49
src/app/log-in-app/log-in-app.component.html
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<app-banner></app-banner>
|
||||||
|
|
||||||
|
<div class="container home-container text-center" *ngIf="globalVars.inTab || globalVars.webview">
|
||||||
|
<div class="mb-20px">
|
||||||
|
<span class="title-text">Choose a channel to share with {{ globalVars.hostname }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column" *ngIf="hasChannels">
|
||||||
|
<ul class="list-group mt-7px mb-30px saved-seeds-list">
|
||||||
|
<span class="saved-seeds-header d-flex align-items-center"><span>Select an account</span></span>
|
||||||
|
<div class="saved-seeds-scroll">
|
||||||
|
<li *ngFor="let item of allChannels | keyvalue" class="list-group-item list-group-item-action cursor-pointer saved-seed" (click)="selectAccount(item.key)">
|
||||||
|
<div class="w-100">
|
||||||
|
<div *ngIf="item.value" class="d-flex align-items-center">
|
||||||
|
<b>{{ item.value.name }}</b>
|
||||||
|
|
||||||
|
|
||||||
|
<i>(#{{ item.value.claimId }})</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button button-large button-secondary mb-40px" [routerLink]="['/log-in-wallet']">
|
||||||
|
<span class="font-weight-normal">Go back to Wallet Log In</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center flex-column">
|
||||||
|
<!--
|
||||||
|
Hmm... Maybe sign-up remains a TODO.
|
||||||
|
-->
|
||||||
|
<button class="button button-large button-secondary mb-40px" [routerLink]="['/sign-up']">
|
||||||
|
<span class="font-weight-normal">Create a new Wallet</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="fs-18px mt-30px">
|
||||||
|
Logging in grants <b>{{ globalVars.hostname }}</b> access to your public
|
||||||
|
channel information. As you use the site, you will perform actions (such
|
||||||
|
as leaving a comment or spending LBC) that will bring you back here to
|
||||||
|
get your permission. At that point, you will have the option to give the
|
||||||
|
app permission to perform the same sort of action again without bringing
|
||||||
|
up another popup.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,20 +1,20 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { LogInComponent } from './log-in.component';
|
import { LogInAppComponent } from './log-in-app.component';
|
||||||
|
|
||||||
describe('LogInComponent', () => {
|
describe('LogInAppComponent', () => {
|
||||||
let component: LogInComponent;
|
let component: LogInAppComponent;
|
||||||
let fixture: ComponentFixture<LogInComponent>;
|
let fixture: ComponentFixture<LogInAppComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [ LogInComponent ]
|
declarations: [ LogInAppComponent ]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LogInComponent);
|
fixture = TestBed.createComponent(LogInAppComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
41
src/app/log-in-app/log-in-app.component.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {AccountService} from '../account.service';
|
||||||
|
import {IdentityService} from '../identity.service';
|
||||||
|
import {GlobalVarsService} from '../global-vars.service';
|
||||||
|
import {PublicChannelInfo} from '../../types/identity';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-log-in-app',
|
||||||
|
templateUrl: './log-in-app.component.html',
|
||||||
|
styleUrls: ['./log-in-app.component.scss']
|
||||||
|
})
|
||||||
|
export class LogInAppComponent implements OnInit {
|
||||||
|
allChannels: {[key: string]: PublicChannelInfo} = {};
|
||||||
|
hasChannels: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
|
private identityService: IdentityService,
|
||||||
|
public globalVars: GlobalVarsService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.allChannels = this.accountService.getChannelsPublic()
|
||||||
|
this.hasChannels = Object.keys(this.allChannels).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAccount(channelClaimId: string): void {
|
||||||
|
this.accountService.setAccessCurrentChannel(this.globalVars.hostname, channelClaimId)
|
||||||
|
// At this point, DeSo had globalVars.accessLevelRequest, where the app
|
||||||
|
// would specify which access level it would be operating with, and the
|
||||||
|
// user would grant permission on login. We could do something similar: The
|
||||||
|
// app could specify which sorts of actions it will be likely asking
|
||||||
|
// permission for. The user could specify on login "don't bother asking my
|
||||||
|
// permission for these actions" so they never get a popup for it.
|
||||||
|
|
||||||
|
this.identityService.login({
|
||||||
|
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||||
|
signedUp: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
51
src/app/log-in-wallet/log-in-wallet.component.html
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<app-banner></app-banner>
|
||||||
|
|
||||||
|
<div class="page-container" *ngIf="globalVars.inTab || globalVars.webview || globalVars.callback">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<div class="title-text">
|
||||||
|
Log into the LBRY wallet sync service
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="main-text mb-20px">
|
||||||
|
Log in to {{ globalVars.environment.walletSyncHostname }}:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div *ngIf="loginError" class="alert alert-danger mt-15px">
|
||||||
|
{{ loginError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-input-container">
|
||||||
|
<textarea [(ngModel)]="loginUsername"
|
||||||
|
class="text-input"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Username"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-input-container">
|
||||||
|
<textarea [(ngModel)]="loginPassword"
|
||||||
|
class="text-input"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Password"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button (click)="clickLogin()"
|
||||||
|
class="button button-primary button-large">
|
||||||
|
Log In
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="title-text">
|
||||||
|
Paste your wallet directly
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<textarea [value]="walletDumpInitial" id="wallet-dump" rows="20" cols="150"></textarea>
|
||||||
|
<button (click)="loginWithWalletDump()"
|
||||||
|
class="button button-primary button-large">
|
||||||
|
Log In
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
|
@ -1,20 +1,20 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { LoadSeedComponent } from './load-seed.component';
|
import { LogInWalletComponent } from './log-in-wallet.component';
|
||||||
|
|
||||||
describe('LoadSeedComponent', () => {
|
describe('LogInWalletComponent', () => {
|
||||||
let component: LoadSeedComponent;
|
let component: LogInWalletComponent;
|
||||||
let fixture: ComponentFixture<LoadSeedComponent>;
|
let fixture: ComponentFixture<LogInWalletComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [ LoadSeedComponent ]
|
declarations: [ LogInWalletComponent ]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LoadSeedComponent);
|
fixture = TestBed.createComponent(LogInWalletComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
107
src/app/log-in-wallet/log-in-wallet.component.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import {BackendAPIService} from '../backend-api.service';
|
||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {AccountService} from '../account.service';
|
||||||
|
import {CryptoService} from '../crypto.service';
|
||||||
|
import {GlobalVarsService} from '../global-vars.service';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
import {RouteNames} from '../app-routing.module';
|
||||||
|
import {of} from 'rxjs';
|
||||||
|
|
||||||
|
// This component handles two ways of logging in:
|
||||||
|
// * Wallet Sync (currently commented out)
|
||||||
|
// * Paste Wallet (temporary measure for initial version)
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-log-in-wallet',
|
||||||
|
templateUrl: './log-in-wallet.component.html',
|
||||||
|
styleUrls: ['./log-in-wallet.component.scss']
|
||||||
|
})
|
||||||
|
export class LogInWalletComponent implements OnInit {
|
||||||
|
walletDumpInitial = this.getWalletDumpInitial();
|
||||||
|
|
||||||
|
loginError = '';
|
||||||
|
loginUsername = '';
|
||||||
|
loginPassword = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private backendApi: BackendAPIService,
|
||||||
|
private cryptoService: CryptoService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
public globalVars: GlobalVarsService,
|
||||||
|
private router: Router,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {}
|
||||||
|
|
||||||
|
// Wallet Sync (WIP, unused atm)
|
||||||
|
|
||||||
|
clickLogin() {
|
||||||
|
// Store username and password locally because we clear them below and otherwise
|
||||||
|
// they don't get saved in local storage reliably
|
||||||
|
const loginUsername = this.loginUsername;
|
||||||
|
const loginPassword = this.loginPassword;
|
||||||
|
|
||||||
|
if (loginUsername.length === 0) {
|
||||||
|
this.loginError = 'Enter a username';
|
||||||
|
return of();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginPassword.length === 0) {
|
||||||
|
this.loginError = 'Enter a password';
|
||||||
|
return of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returning the observable only because I'm in the habit of returning
|
||||||
|
// promises. But maybe it's not needed here.
|
||||||
|
return this.backendApi.WalletSyncLogin(loginUsername, loginPassword).subscribe(
|
||||||
|
(res => {
|
||||||
|
if (res === null) {
|
||||||
|
this.loginError = "Login Error. Try again?"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const walletStr = res.bodyJson
|
||||||
|
const walletSignature = res.signature
|
||||||
|
|
||||||
|
if (!this.cryptoService.checkSig(walletStr, walletSignature)) {
|
||||||
|
this.loginError = 'Wallet signature failed!';
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const wallet = JSON.parse(res.bodyJson)
|
||||||
|
const walletError: string | null = this.cryptoService.validateWallet(wallet);
|
||||||
|
|
||||||
|
if (walletError !== null) {
|
||||||
|
this.loginError = "Wallet error: " + walletError;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.accountService.walletLogin(wallet);
|
||||||
|
|
||||||
|
// Clear the form
|
||||||
|
this.loginUsername = '';
|
||||||
|
this.loginPassword = '';
|
||||||
|
|
||||||
|
this.router.navigate(['/', RouteNames.LOG_IN_APP], {queryParamsHandling: 'merge'});
|
||||||
|
}),
|
||||||
|
(() => {
|
||||||
|
this.loginError = "Login Error. Try again?"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paste Wallet (temporary measure for initial version)
|
||||||
|
|
||||||
|
getWalletDumpInitial() {
|
||||||
|
const wallet : object | null = this.accountService.getWallet();
|
||||||
|
return JSON.stringify(wallet, null, 2) || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
loginWithWalletDump(): void {
|
||||||
|
const walletStr = (<HTMLInputElement>document.getElementById("wallet-dump")).value;
|
||||||
|
const wallet = JSON.parse(walletStr)
|
||||||
|
this.accountService.walletLogin(wallet);
|
||||||
|
|
||||||
|
this.router.navigate(['/', RouteNames.LOG_IN_APP], {queryParamsHandling: 'merge'});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,58 +0,0 @@
|
||||||
<app-banner></app-banner>
|
|
||||||
|
|
||||||
<div class="container home-container text-center" *ngIf="globalVars.inTab || globalVars.webview">
|
|
||||||
<div class="mb-20px">
|
|
||||||
<span class="title-text">Log in to {{ globalVars.hostname }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex flex-column" *ngIf="hasUsers">
|
|
||||||
<ul class="list-group mt-7px mb-30px saved-seeds-list">
|
|
||||||
<span class="saved-seeds-header d-flex align-items-center"><span>Select an account</span></span>
|
|
||||||
<div class="saved-seeds-scroll">
|
|
||||||
<li *ngFor="let item of allUsers | keyvalue" class="list-group-item list-group-item-action cursor-pointer saved-seed" (click)="selectAccount(item.key)">
|
|
||||||
<div class="w-100">
|
|
||||||
<div *ngIf="!item.value.username" class="text-truncate">{{ item.key }}…</div>
|
|
||||||
<div *ngIf="item.value.username" class="d-flex align-items-center">
|
|
||||||
<div class="text-truncate">{{ item.value.username }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center flex-column">
|
|
||||||
<button class="button button-large button-secondary mb-40px" [routerLink]="['/sign-up']">
|
|
||||||
<span class="font-weight-normal">Sign up with DeSo seed</span>
|
|
||||||
</button>
|
|
||||||
<a class="link" [routerLink]="['/load-seed']"><u>Log in with DeSo seed</u></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="fs-18px mt-30px">
|
|
||||||
Logging in grants <b>{{ globalVars.hostname }}</b> access to:
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="list-group list-group-flush mt-15px">
|
|
||||||
<li class="list-group-item">
|
|
||||||
<span *ngIf="globalVars.accessLevelRequest >= 2">✅</span>
|
|
||||||
<span *ngIf="globalVars.accessLevelRequest < 2">❌</span>
|
|
||||||
My basic information
|
|
||||||
<div class="fs-14px text-muted"><b>{{ globalVars.hostname }}</b> can access my public key and any other public information</div>
|
|
||||||
</li>
|
|
||||||
<li class="list-group-item">
|
|
||||||
<span *ngIf="globalVars.accessLevelRequest >= 3">✅</span>
|
|
||||||
<span *ngIf="globalVars.accessLevelRequest < 3">❌</span>
|
|
||||||
Post, message, like, and follow on my behalf
|
|
||||||
<div class="fs-14px text-muted" *ngIf="globalVars.accessLevelRequest < 3"><b>{{ globalVars.hostname }}</b> will require approval to post, message, like, and follow</div>
|
|
||||||
<div class="fs-14px text-muted" *ngIf="globalVars.accessLevelRequest >= 3"><b>{{ globalVars.hostname }}</b> may post, message, like, and follow without requiring approval</div>
|
|
||||||
</li>
|
|
||||||
<li class="list-group-item">
|
|
||||||
<span *ngIf="globalVars.accessLevelRequest === 4">✅</span>
|
|
||||||
<span *ngIf="globalVars.accessLevelRequest < 4">❌</span>
|
|
||||||
Buy, sell, and send coins on my behalf
|
|
||||||
<div class="fs-14px text-muted" *ngIf="globalVars.accessLevelRequest < 4"><b>{{ globalVars.hostname }}</b> will require approval to buy, sell, or send coins</div>
|
|
||||||
<div class="fs-14px text-muted" *ngIf="globalVars.accessLevelRequest === 4"><b>{{ globalVars.hostname }}</b> may buy, sell, and send coins without requiring approval</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,42 +0,0 @@
|
||||||
import {Component, OnInit} from '@angular/core';
|
|
||||||
import {AccountService} from '../account.service';
|
|
||||||
import {IdentityService} from '../identity.service';
|
|
||||||
import {GlobalVarsService} from '../global-vars.service';
|
|
||||||
import {BackendAPIService} from '../backend-api.service';
|
|
||||||
import {UserProfile} from '../../types/identity';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-log-in',
|
|
||||||
templateUrl: './log-in.component.html',
|
|
||||||
styleUrls: ['./log-in.component.scss']
|
|
||||||
})
|
|
||||||
export class LogInComponent implements OnInit {
|
|
||||||
allUsers: {[key: string]: UserProfile} = {};
|
|
||||||
hasUsers = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private accountService: AccountService,
|
|
||||||
private identityService: IdentityService,
|
|
||||||
public globalVars: GlobalVarsService,
|
|
||||||
private backendApi: BackendAPIService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
// Load profile pictures and usernames
|
|
||||||
const publicKeys = this.accountService.getPublicKeys();
|
|
||||||
this.hasUsers = publicKeys.length > 0;
|
|
||||||
this.backendApi.GetUserProfiles(publicKeys)
|
|
||||||
.subscribe(profiles => {
|
|
||||||
this.allUsers = profiles;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAccount(publicKey: string): void {
|
|
||||||
this.accountService.setAccessLevel(publicKey, this.globalVars.hostname, this.globalVars.accessLevelRequest);
|
|
||||||
this.identityService.login({
|
|
||||||
users: this.accountService.getEncryptedUsers(),
|
|
||||||
publicKeyAdded: publicKey,
|
|
||||||
signedUp: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,7 @@
|
||||||
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
|
||||||
import {CryptoService} from '../crypto.service';
|
import {CryptoService} from '../crypto.service';
|
||||||
import {IdentityService} from '../identity.service';
|
import {IdentityService} from '../identity.service';
|
||||||
import {AccountService} from '../account.service';
|
import {AccountService} from '../account.service';
|
||||||
import {AccessLevel} from '../../types/identity';
|
|
||||||
import {GlobalVarsService} from '../global-vars.service';
|
import {GlobalVarsService} from '../global-vars.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -20,7 +18,6 @@ export class LogoutComponent implements OnInit {
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private activatedRoute: ActivatedRoute,
|
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private identityService: IdentityService,
|
private identityService: IdentityService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
@ -28,9 +25,6 @@ export class LogoutComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.activatedRoute.queryParams.subscribe(params => {
|
|
||||||
this.publicKey = params.publicKey || '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel(): void {
|
onCancel(): void {
|
||||||
|
@ -39,7 +33,7 @@ export class LogoutComponent implements OnInit {
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
// We set the accessLevel for the logged out user to None.
|
// We set the accessLevel for the logged out user to None.
|
||||||
this.accountService.setAccessLevel(this.publicKey, this.globalVars.hostname, AccessLevel.None);
|
this.accountService.appLogout(this.globalVars.hostname);
|
||||||
// We reset the seed encryption key so that all existing accounts, except
|
// We reset the seed encryption key so that all existing accounts, except
|
||||||
// the logged out user, will regenerate their encryptedSeedHex. Without this,
|
// the logged out user, will regenerate their encryptedSeedHex. Without this,
|
||||||
// someone could have reused the encryptedSeedHex of an already logged out user.
|
// someone could have reused the encryptedSeedHex of an already logged out user.
|
||||||
|
@ -49,7 +43,7 @@ export class LogoutComponent implements OnInit {
|
||||||
|
|
||||||
finishFlow(): void {
|
finishFlow(): void {
|
||||||
this.identityService.login({
|
this.identityService.login({
|
||||||
users: this.accountService.getEncryptedUsers(),
|
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {EntropyService} from '../entropy.service';
|
import {EntropyService} from '../entropy.service';
|
||||||
import {CryptoService} from '../crypto.service';
|
|
||||||
import {AccountService} from '../account.service';
|
import {AccountService} from '../account.service';
|
||||||
import {IdentityService} from '../identity.service';
|
import {IdentityService} from '../identity.service';
|
||||||
import {GlobalVarsService} from '../global-vars.service';
|
import {GlobalVarsService} from '../global-vars.service';
|
||||||
|
@ -19,7 +18,6 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
||||||
seedCopied = false;
|
seedCopied = false;
|
||||||
mnemonicCheck = '';
|
mnemonicCheck = '';
|
||||||
extraTextCheck = '';
|
extraTextCheck = '';
|
||||||
publicKeyAdded = '';
|
|
||||||
|
|
||||||
// Advanced tab
|
// Advanced tab
|
||||||
showMnemonicError = false;
|
showMnemonicError = false;
|
||||||
|
@ -32,7 +30,6 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public entropyService: EntropyService,
|
public entropyService: EntropyService,
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private identityService: IdentityService,
|
private identityService: IdentityService,
|
||||||
public globalVars: GlobalVarsService,
|
public globalVars: GlobalVarsService,
|
||||||
|
@ -81,17 +78,29 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
||||||
////// STEP TWO BUTTONS ///////
|
////// STEP TWO BUTTONS ///////
|
||||||
|
|
||||||
stepTwoNext(): void {
|
stepTwoNext(): void {
|
||||||
|
// TODO - signup for LBRY will be very different from DeSo. We'll need to
|
||||||
|
// make a wallet, not just "users". And other things happen.
|
||||||
|
throw 'signup not implemented'
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
// this is a mix of some of what DeSo left over, and some new LBRY.id
|
||||||
|
// specific things that I figure we won't want to forget. This is just
|
||||||
|
// a guide for the future when we tackle signup.
|
||||||
|
|
||||||
|
// TODO Don't use this.globalVars.network here, use the network specified
|
||||||
|
// in the relevant account.ledger (assuming we even really need network)
|
||||||
const network = this.globalVars.network;
|
const network = this.globalVars.network;
|
||||||
const mnemonic = this.mnemonicCheck;
|
const mnemonic = this.mnemonicCheck;
|
||||||
const extraText = this.extraTextCheck;
|
const extraText = this.extraTextCheck;
|
||||||
const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, extraText);
|
const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, extraText);
|
||||||
|
|
||||||
this.publicKeyAdded = this.accountService.addUser(keychain, mnemonic, extraText, network);
|
const accountNameAdded = this.accountService.addUser(keychain, mnemonic, extraText, network);
|
||||||
|
|
||||||
this.accountService.setAccessLevel(
|
this.accountService.initAccess()
|
||||||
this.publicKeyAdded, this.globalVars.hostname, this.globalVars.accessLevelRequest);
|
|
||||||
|
|
||||||
this.login();
|
this.login();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
stepTwoBack(): void {
|
stepTwoBack(): void {
|
||||||
|
@ -102,8 +111,7 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
login(): void {
|
login(): void {
|
||||||
this.identityService.login({
|
this.identityService.login({
|
||||||
users: this.accountService.getEncryptedUsers(),
|
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||||
publicKeyAdded: this.publicKeyAdded,
|
|
||||||
signedUp: true,
|
signedUp: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import {HubService} from './hub.service';
|
||||||
import KeyEncoder from 'key-encoder';
|
import KeyEncoder from 'key-encoder';
|
||||||
|
import {GlobalVarsService} from './global-vars.service';
|
||||||
import * as jsonwebtoken from 'jsonwebtoken';
|
import * as jsonwebtoken from 'jsonwebtoken';
|
||||||
import {CryptoService} from './crypto.service';
|
import {PrivateAccountInfo, Network} from '../types/identity';
|
||||||
import * as sha256 from 'sha256';
|
|
||||||
import { uvarint64ToBuf } from '../lib/bindata/util';
|
|
||||||
|
|
||||||
import * as bip32 from 'bip32';
|
import * as bip32 from 'bip32';
|
||||||
import { BIP32Interface } from 'bip32'; // TODO: Installed 2.0.6 instead of latest version, only because of weird typescript compilation stuff. Should probably get the latest.
|
import { BIP32Interface } from 'bip32'; // TODO: Installed 2.0.6 instead of latest version, only because of weird typescript compilation stuff. Should probably get the latest.
|
||||||
|
@ -11,6 +11,7 @@ import * as bs58check from 'bs58check'
|
||||||
import * as lbry from 'bitcoinjs-lib' // TODO - package recommends browserify, which I did not do here. This works, but maybe there's good reason to browserify?
|
import * as lbry from 'bitcoinjs-lib' // TODO - package recommends browserify, which I did not do here. This works, but maybe there's good reason to browserify?
|
||||||
import * as ecpair from 'ecpair'; // TODO - required acorn-class-fields for this version of Angular's webpack to accept it. see extend-acorn.js.
|
import * as ecpair from 'ecpair'; // TODO - required acorn-class-fields for this version of Angular's webpack to accept it. see extend-acorn.js.
|
||||||
|
|
||||||
|
// TODO deleteme once I remove the last use of this
|
||||||
const NETWORK = lbry.networks.mainnet
|
const NETWORK = lbry.networks.mainnet
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
@ -19,41 +20,389 @@ const NETWORK = lbry.networks.mainnet
|
||||||
export class SigningService {
|
export class SigningService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cryptoService: CryptoService,
|
private hubService: HubService,
|
||||||
|
private globalVars: GlobalVarsService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
// this should be audited and go into a library. hobbled this together from
|
private *generateKeys(
|
||||||
// code in bitcoinjs-lib.
|
node: BIP32Interface,
|
||||||
private getAddressFromBip32(node: BIP32Interface): string {
|
keyPath: lbry.bip32Lbry.KeyPath
|
||||||
const hash = lbry.crypto.hash160(node.publicKey)
|
): IterableIterator<BIP32Interface> {
|
||||||
|
for (let childIndex = 0; ; childIndex++) {
|
||||||
const payload = Buffer.allocUnsafe(21);
|
yield node.derive(keyPath).derive(childIndex)
|
||||||
payload.writeUInt8(NETWORK.pubKeyHash, 0);
|
}
|
||||||
hash.copy(payload, 1);
|
|
||||||
return bs58check.encode(payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSigningKey(wallet: any, address: string): Buffer | null {
|
// TODO - have a minimum batch size actually, like 10, that's bigger than a
|
||||||
const account = wallet.accounts
|
// gap, to reduce number of requests to the hub. or maybe 5. Get an opinion
|
||||||
.filter((account: any) => {
|
// on what's a common number of used keys. And channels, that may be a
|
||||||
let node: BIP32Interface = bip32.fromBase58(account.private_key);
|
// different answer.
|
||||||
return address === this.getAddressFromBip32(node)
|
// TODO - have a maximum number of keys? Just in case there's a bug so we
|
||||||
})[0]
|
// don't blow up the user's browser?
|
||||||
return bip32.fromBase58(account.private_key).privateKey || null;
|
private *generateKeyBatches(
|
||||||
|
node: BIP32Interface,
|
||||||
|
keyPath: lbry.bip32Lbry.KeyPath,
|
||||||
|
gap: number,
|
||||||
|
): IterableIterator<BIP32Interface[]> {
|
||||||
|
const generatedKeys = this.generateKeys(node, keyPath)
|
||||||
|
let batchSize = gap
|
||||||
|
while(true) {
|
||||||
|
// TODO - confirm gap off-by-one error
|
||||||
|
// Get the next `batchSize` keys
|
||||||
|
let batchKeys: BIP32Interface[] = []
|
||||||
|
for (let index = 0; index < batchSize; index++) {
|
||||||
|
batchKeys.push(generatedKeys.next().value)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddresses(wallet: any): string[] {
|
// Yield them, and get back the ones that were found to be used
|
||||||
return wallet.accounts
|
// TODO - if it's easy for the consumer, we can just accept the last one,
|
||||||
// won't venture into deterministic yet
|
// or even better, its index within `batchKeys`
|
||||||
.filter((account: any) => account.address_generator.name === 'single-address')
|
const usedBatchKeys = yield batchKeys
|
||||||
.map((account: any) => {
|
if (usedBatchKeys === undefined) {
|
||||||
let node: BIP32Interface = bip32.fromBase58(account.private_key);
|
throw "generateKeyBatches iteration needs an array of used batch keys"
|
||||||
return this.getAddressFromBip32(node)
|
}
|
||||||
|
|
||||||
|
// If none are used, we passed the gap and found nothing
|
||||||
|
if (usedBatchKeys.length === 0) return
|
||||||
|
|
||||||
|
// Easier for finding
|
||||||
|
const batchPubKeys = batchKeys.map(key => key.public_key)
|
||||||
|
const usedBatchPubKeys = usedBatchKeys.map(key => key.public_key)
|
||||||
|
|
||||||
|
// What is the last index of batchPubKeys (and thus batchKeys) that is in
|
||||||
|
// usedBatchPubKeys?
|
||||||
|
const lastUsedIndex = Math.max.apply(
|
||||||
|
Math, usedBatchPubKeys.map(pubKey => batchPubKeys.indexOf(pubKey))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Indicates a bug here, or `usedBatchKeys` failed to be a subset of
|
||||||
|
// `batchKeys`.
|
||||||
|
if (lastUsedIndex === -1) throw "error determining next batch"
|
||||||
|
|
||||||
|
// TODO think about if this is right. Maybe needs test cases.
|
||||||
|
|
||||||
|
// Now that we know what the last used key is in our batch, we want to
|
||||||
|
// have enough keys after it to cover the next gap.
|
||||||
|
|
||||||
|
// Any keys in our current batch after the last used key is the beginning
|
||||||
|
// of the next gap.
|
||||||
|
const gapInThisBatch = batchSize - (lastUsedIndex + 1)
|
||||||
|
|
||||||
|
// The rest of the next gap will be the at the beginning of the next
|
||||||
|
// batch. We'll make our batch big enough to contain it, in case it's
|
||||||
|
// full size.
|
||||||
|
batchSize = gap - gapInThisBatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Example claim. Just so I know the format while I'm working on this. Delete
|
||||||
|
after channel retrieval work is done.
|
||||||
|
|
||||||
|
{
|
||||||
|
"address": "bbvk6TMEzujW8r3xhP6e1FhCuSW3FYP9SJ",
|
||||||
|
"amount": "0.01",
|
||||||
|
"claim_id": "7d39c627771c529e65656f4ca86d13686acc0442",
|
||||||
|
"claim_op": "create",
|
||||||
|
"confirmations": 193,
|
||||||
|
"has_signing_key": true,
|
||||||
|
"height": 1157989,
|
||||||
|
"is_internal_transfer": false,
|
||||||
|
"is_my_input": true,
|
||||||
|
"is_my_output": true,
|
||||||
|
"is_spent": false,
|
||||||
|
"meta": {},
|
||||||
|
"name": "@lolstupidtest2",
|
||||||
|
"normalized_name": "@lolstupidtest2",
|
||||||
|
"nout": 0,
|
||||||
|
"permanent_url": "lbry://@lolstupidtest2#7d39c627771c529e65656f4ca86d13686acc0442",
|
||||||
|
"timestamp": 1652278858,
|
||||||
|
"txid": "a62878d9c558cd17f1f946df03aa8584dd002238004a02d742261b5c560dc43f",
|
||||||
|
"type": "claim",
|
||||||
|
"value": {
|
||||||
|
"public_key": "023bfe202119244b448f8974ee3152a3d859ac169a420b8ef1dda423fa015b2a4f",
|
||||||
|
"public_key_id": "bUfKnUamA7T3S2JGjV6Rmb8o3Wtn79WSTA"
|
||||||
|
},
|
||||||
|
"value_type": "channel"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO error handling?
|
||||||
|
// TODO - deprecated channel key (`certificates` field)
|
||||||
|
private getChannelsForAccount(account: PrivateAccountInfo): Observable<PrivateChannelInfo[]> {
|
||||||
|
// TODO Can single addresses register channels?
|
||||||
|
// Grin says yes.
|
||||||
|
// If so, handle single key cases. For generated key cases, assert that
|
||||||
|
// it === "deterministic-chain" (there shouldn't be anything else), or make
|
||||||
|
// a note to validate the wallet via jsonschema when reading it initially.
|
||||||
|
|
||||||
|
// TODO - there's got to be a better way.
|
||||||
|
return new Observable(subscriber => {
|
||||||
|
const keyBatches = this.generateKeyBatches(
|
||||||
|
this.bip32FromAccount(account),
|
||||||
|
lbry.bip32Lbry.KeyPath.CHANNEL,
|
||||||
|
1, // TODO what gap do I actually use for channels?
|
||||||
|
)
|
||||||
|
|
||||||
|
// Where we accumulate all of the channels we find through all of the
|
||||||
|
// recursions
|
||||||
|
let foundChannels: PrivateChannelInfo[] = []
|
||||||
|
|
||||||
|
accumulateChannels(possibleKeys: BIP32Interface[], done: boolean) {
|
||||||
|
if (done) {
|
||||||
|
// The address generator has indicated that all used channel keys
|
||||||
|
// have been found. Give the subscriber all of the channels we just
|
||||||
|
// accumulated across recursions.
|
||||||
|
subscriber.next(foundChannels)
|
||||||
|
subscriber.complete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
possibleKeysByKeyId = Object.fromEntries(
|
||||||
|
possibleKeys.map(key => [this.getAddressFromBip32(key, account.network), key])
|
||||||
|
)
|
||||||
|
this.hubService.findChannels(
|
||||||
|
possibleKeys.map(key => this.getAddressFromBip32(key, account.network))
|
||||||
|
).pipe(
|
||||||
|
map(res => {
|
||||||
|
// TODO - Should expect HUB to return revoked channels as well.
|
||||||
|
// Theose need to be added to `usedKeys` since they count toward
|
||||||
|
// gaps.
|
||||||
|
const newChannels: PrivateChannelInfo[] = res.map(hubChannel => ({
|
||||||
|
claimId: hubChannel.claim_id,
|
||||||
|
name: hubChannel.name,
|
||||||
|
normalizedName: hubChannel.normalizedName,
|
||||||
|
pubKeyId: hubChannel.value.public_key_id,
|
||||||
|
signingKey: possibleKeysByKeyId[hubChannel.value.public_key_id],
|
||||||
|
// TODO - more fields?
|
||||||
|
}))
|
||||||
|
|
||||||
|
foundChannels = foundChannels.concat(newChannels)
|
||||||
|
|
||||||
|
const usedKeys = newChannels.map(channel => possibleKeysByKeyId[channel.pubKeyId])
|
||||||
|
|
||||||
|
// Give the address generator `usedKeys` so it knows how many to grab
|
||||||
|
// next, to cover the gap we're looking for
|
||||||
|
({value: possibleKeys, done} = keyBatches.next(usedKeys));
|
||||||
|
|
||||||
|
accumulateChannels(possibleKeys, done)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
(const {value: possibleKeys, done} = keyBatches.next());
|
||||||
|
accumulateChannels(possibleKeys, done)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The functions below in the commented out block are an earlier layer of WIP
|
||||||
|
than the rest of it. At that point, we thought we needed to get all of the
|
||||||
|
used addresses and use them to get the channel claims. (Whereas the newer
|
||||||
|
plan would be to use special hub endpoints to get the channel info more
|
||||||
|
directly.)
|
||||||
|
|
||||||
|
That said, we will eventually probably have use for some of this stuff.
|
||||||
|
Primarily, we'll need to find spending keys. But who knows, maybe we'll
|
||||||
|
even change plans back again wrt Channel querying. I was afraid of deleting
|
||||||
|
it until we had something working.
|
||||||
|
|
||||||
|
It didn't get completed (or so I recall?) before we switched gears so it's
|
||||||
|
commented out. (Not that the newer plan work is completed either!)
|
||||||
|
|
||||||
|
Even if we use some or all of this, we should probably reimplement it using
|
||||||
|
`generateKeyBatches` (from the newer plan's code), because it's just
|
||||||
|
cleaner.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Add all of the keys I can find from a private key
|
||||||
|
// `channelAddresses` should be all channel addresses, from both active and
|
||||||
|
// revoked claims. This helps us avoid issues with key generation gaps.
|
||||||
|
private findChannelKeysForAccount(
|
||||||
|
node: BIP32Interface,
|
||||||
|
claimChannelAddresses: string[],
|
||||||
|
deprecatedChannelKeys: {string: string}
|
||||||
|
): {[key: string]: BIP32Interface}
|
||||||
|
{
|
||||||
|
let numRemainingKeys = claimChannelAddresses.length
|
||||||
|
const channelKeys: {[key: string]: BIP32Interface} = {}
|
||||||
|
for (const channelAddress in deprecatedChannelKeys) {
|
||||||
|
if (channelAddress in claimChannelAddresses) {
|
||||||
|
// TODO - Implement getting deprecated channel private keys from pem
|
||||||
|
// format to bip32, somehow.
|
||||||
|
// channelKeys[channelAddress] = this.fromPem(deprecatedChannelKeys[channelAddress])
|
||||||
|
numRemainingKeys--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const generatedChannelKeys = this.generateKeys(node, lbry.bip32Lbry.KeyPath.CHANNEL)
|
||||||
|
let numRemainingSkippedKeys = 10
|
||||||
|
while(numRemainingSkippedKeys>0) {
|
||||||
|
const channelKey = generatedChannelKeys.next().value
|
||||||
|
const channelAddress = this.getAddressFromBip32(channelKey, channelKey.network)
|
||||||
|
if (channelAddress in claimChannelAddresses) {
|
||||||
|
channelKeys[channelAddress] = channelKey
|
||||||
|
numRemainingKeys--;
|
||||||
|
} else {
|
||||||
|
// TODO - should this ever happen? How many should there be? Should this use the normal "gap system"?
|
||||||
|
numRemainingSkippedKeys--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return channelKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - Respect address use maximum, here, or wherever is relevant.
|
||||||
|
private getUsedAddresses(account: PrivateAccountInfo): {[key in lbry.bip32Lbry.KeyPath]: string[]} {
|
||||||
|
if (account.address_generator.name == "single-address") {
|
||||||
|
// TODO is this right? Can single addresses register channels?
|
||||||
|
const accountId = this.getAddress(account)
|
||||||
|
// TODO - actually not quite a "used" address either. May not have been
|
||||||
|
// used, but we should search for channels with it. So what is the right
|
||||||
|
// name?
|
||||||
|
return {RECEIVE: [accountId]} // and not have CHANGE
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is not this, something is amiss.
|
||||||
|
// TODO - actually just put this in the wallet json schema
|
||||||
|
if(account.address_generator.name !== "deterministic-chain") {
|
||||||
|
throw "Expected deterministic-chain at this point"
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {}
|
||||||
|
for (let keyPath of [lbry.bip32Lbry.KeyPath.CHANGE, lbry.bip32Lbry.KeyPath.RECEIVE]) {
|
||||||
|
const keyBatches = this.generateKeyBatches(
|
||||||
|
this.bip32FromAccount(account),
|
||||||
|
keyPath,
|
||||||
|
account.address_generator.change.gap,
|
||||||
|
)
|
||||||
|
|
||||||
|
let usedAddresses: string[] = []
|
||||||
|
let possibleKeys;
|
||||||
|
let done = false;
|
||||||
|
({value: possibleKeys, done} = keyBatches.next());
|
||||||
|
while (!done) {
|
||||||
|
const nextUsedAddresses = this.hubService.doIHaveHistory(
|
||||||
|
possibleKeys.map(key => this.getAddressFromBip32(key, key.network))
|
||||||
|
)
|
||||||
|
usedAddresses = usedAddresses.concat(nextUsedAddresses)
|
||||||
|
keyBatches.next(nextUsedAddresses)
|
||||||
|
}
|
||||||
|
result[keyPath] = usedAddresses
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query the hub for all our used addresses. Note which account they came
|
||||||
|
// from.
|
||||||
|
// return: {usedAddress: accountId}
|
||||||
|
private getAccountIDsByUsedAddress(accounts: PrivateAccountInfo[]): {[key: string]: string} {
|
||||||
|
// accountIDsByUsedAddress: {usedAddress: accountId}
|
||||||
|
const accountIDsByUsedAddress: {[key: string]: string} = {}
|
||||||
|
|
||||||
|
for (let account of accounts) {
|
||||||
|
// TODO - this.getUsedAddresses returns the addresses by keypath now
|
||||||
|
for (let usedAddress of this.getUsedAddresses(account)) {
|
||||||
|
accountIDsByUsedAddress[usedAddress] = this.getAddress(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query the hub for all our channel claims, based on used keys. Organize The
|
||||||
|
// channels by accountId.
|
||||||
|
// returns channelsByAccountId: {accountId: channelClaims[]}
|
||||||
|
public getChannelsByAccountId(accounts: PrivateAccountInfo[]): {[key: string]: any}{
|
||||||
|
const accountIDsByUsedAddress = this.getAccountIDsByUsedAddress(accounts)
|
||||||
|
const channelsByAccountId = {}
|
||||||
|
|
||||||
|
for (let claim of this.hubService.getClaims(Object.keys(accountIDsByUsedAddress))) {
|
||||||
|
if (claim.value_type != 'channel') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const accountId = accountIDsByUsedAddress[claim.address]
|
||||||
|
if(!accountId) {
|
||||||
|
// Don't trust what comes from the hub
|
||||||
|
// TODO - trust fewer things?
|
||||||
|
throw "bad accountId"
|
||||||
|
}
|
||||||
|
if (!channelsByAccountId[claim.address]) {
|
||||||
|
channelsByAccountId[claim.address] = []
|
||||||
|
}
|
||||||
|
channelsByAccountId[accountId].push(claim)
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelsByAccountId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out channel private keys for the channel addresses, for each
|
||||||
|
// account.
|
||||||
|
// returns {accountId: {channelAddress: channelPrivateKey}}
|
||||||
|
public getChannelKeysByAccountId(accounts, channelsByAccountId): {[key: string]: {[key: string]: string}} {
|
||||||
|
const channelKeysForAccount: {[key: string]: {[key: string]: string}} = {}
|
||||||
|
for (const account of accounts) {
|
||||||
|
const accountId = this.getAddress(account)
|
||||||
|
channelKeysForAccount[accountId] = this.findChannelKeysForAccount(
|
||||||
|
this.bip32FromAccount(account),
|
||||||
|
channelsByAccountId[accountId].map(claim => claim.value.public_key_id),
|
||||||
|
account.certificates,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return channelKeysForAccount
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
private bip32FromAccount(account: PrivateAccountInfo): BIP32Interface {
|
||||||
|
const network = {
|
||||||
|
'lbc_mainnet': lbry.networks.mainnet,
|
||||||
|
'lbc_testnet': lbry.networks.testnet,
|
||||||
|
'lbc_regtest': lbry.networks.regtest,
|
||||||
|
}[account.ledger]
|
||||||
|
return bip32.fromBase58(account.private_key, network);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - make sure this comes up with the right account ID!
|
||||||
|
public getAddress(accountKey: string, accountNetwork: Network): string {
|
||||||
|
const network = {
|
||||||
|
'lbc_mainnet': lbry.networks.mainnet,
|
||||||
|
'lbc_testnet': lbry.networks.testnet,
|
||||||
|
'lbc_regtest': lbry.networks.regtest,
|
||||||
|
}[accountNetwork]
|
||||||
|
let node: BIP32Interface = bip32.fromBase58(accountKey, network);
|
||||||
|
return this.getAddressFromBip32(node, network)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAddressFromBip32(node: BIP32Interface, network: lbry.networks.Network): string {
|
||||||
|
// taken from a test in bitcoinjs-lib
|
||||||
|
return lbry.payments.p2pkh({ pubkey: node.publicKey, network }).address!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - Note to future self - I think this may be a function we only needed
|
||||||
|
// for the demo (like getSpendingAddresses). Or maybe it just needs to be
|
||||||
|
// updated for real use.
|
||||||
|
getSigningKey(wallet: any, address: string): Buffer | null {
|
||||||
|
const account = wallet.accounts
|
||||||
|
.filter((account: any) => {
|
||||||
|
return address === this.getAddress(account)
|
||||||
|
})[0]
|
||||||
|
return bip32.fromBase58(account.private_key, network).privateKey || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does this belong in identity.service next to getChannels? or does
|
||||||
|
// getChannels belong here next to this?
|
||||||
|
// TODO - This is outdated, but make sure there's not something I need to
|
||||||
|
// extract from here before deleting it.
|
||||||
|
/*
|
||||||
|
private getSpendingAddresses(wallet: any): string[] {
|
||||||
|
return wallet.accounts
|
||||||
|
// won't venture into deterministic yet
|
||||||
|
.filter((account: any) => account.address_generator.name === 'single-address')
|
||||||
|
.map((account: any) => this.getAddress(account))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
signPSBT(psbtHex: string, nonWitnessUtxoHexes: string[], signingKey: Buffer): string {
|
signPSBT(psbtHex: string, nonWitnessUtxoHexes: string[], signingKey: Buffer): string {
|
||||||
const keyPair = ecpair.ECPair.fromPrivateKey(signingKey, { network: NETWORK })
|
// TODO Don't use this.globalVars.network here, use the network specified
|
||||||
|
// in the relevant account.ledger (assuming we even really need network)
|
||||||
|
const keyPair = ecpair.ECPair.fromPrivateKey(signingKey, { network: this.globalVars.network })
|
||||||
const nonWitnessUtxos = nonWitnessUtxoHexes.map(h => Buffer.from(h, 'hex'))
|
const nonWitnessUtxos = nonWitnessUtxoHexes.map(h => Buffer.from(h, 'hex'))
|
||||||
const psbt = lbry.Psbt.fromHex(psbtHex)
|
const psbt = lbry.Psbt.fromHex(psbtHex)
|
||||||
|
|
||||||
|
@ -75,39 +424,29 @@ export class SigningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
signJWT(seedHex: string): string {
|
signJWT(seedHex: string): string {
|
||||||
|
return ""
|
||||||
|
|
||||||
|
// TODO - use bitcoinjs-lib and do an actual signature with the actual key in the wallet, send the identifier of the wallet over, etc etc etc.
|
||||||
|
// Assuming we want to keep this
|
||||||
|
|
||||||
const keyEncoder = new KeyEncoder('secp256k1');
|
const keyEncoder = new KeyEncoder('secp256k1');
|
||||||
const encodedPrivateKey = keyEncoder.encodePrivate(seedHex, 'raw', 'pem');
|
const encodedPrivateKey = keyEncoder.encodePrivate(seedHex, 'raw', 'pem');
|
||||||
return jsonwebtoken.sign({ }, encodedPrivateKey, { algorithm: 'ES256', expiresIn: 60 * 10 });
|
return jsonwebtoken.sign({ }, encodedPrivateKey, { algorithm: 'ES256', expiresIn: 60 * 10 });
|
||||||
}
|
}
|
||||||
|
|
||||||
signAction(seedHex: string, actionHex: string): string {
|
signAction(seedHex: string, actionHex: string): string {
|
||||||
|
return ""
|
||||||
|
|
||||||
// TODO - use bitcoinjs-lib and do an actual signature with the actual key in the wallet, send the identifier of the wallet over, etc etc etc.
|
// TODO - use bitcoinjs-lib and do an actual signature with the actual key in the wallet, send the identifier of the wallet over, etc etc etc.
|
||||||
|
|
||||||
|
/*
|
||||||
const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex);
|
const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex);
|
||||||
|
|
||||||
const actionBytes = new Buffer(actionHex, 'hex');
|
const actionBytes = new Buffer(actionHex, 'hex');
|
||||||
const actionHash = new Buffer(sha256.x2(actionBytes), 'hex');
|
const actionHash = new Buffer(sha256.x2(actionBytes), 'hex');
|
||||||
const signature = privateKey.sign(actionHash);
|
const signature = privateKey.sign(actionHash);
|
||||||
return new Buffer(signature.toDER()).toString('hex');
|
return new Buffer(signature.toDER()).toString('hex');
|
||||||
}
|
*/
|
||||||
|
|
||||||
signTransaction(seedHex: string, transactionHex: string): string {
|
|
||||||
const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex);
|
|
||||||
|
|
||||||
const transactionBytes = new Buffer(transactionHex, 'hex');
|
|
||||||
const transactionHash = new Buffer(sha256.x2(transactionBytes), 'hex');
|
|
||||||
const signature = privateKey.sign(transactionHash);
|
|
||||||
const signatureBytes = new Buffer(signature.toDER());
|
|
||||||
const signatureLength = uvarint64ToBuf(signatureBytes.length);
|
|
||||||
|
|
||||||
const signedTransactionBytes = Buffer.concat([
|
|
||||||
// This slice is bad. We need to remove the existing signature length field prior to appending the new one.
|
|
||||||
// Once we have frontend transaction construction we won't need to do this.
|
|
||||||
transactionBytes.slice(0, -1),
|
|
||||||
signatureLength,
|
|
||||||
signatureBytes,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return signedTransactionBytes.toString('hex');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<p>
|
|
||||||
<b>wallet me</b>: <textarea id="wallet" rows="20" cols="150"></textarea><button type="button" (click)="saveWallet()">Go.</button>
|
|
||||||
</p>
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { TestLbryLogInComponent } from './test-lbry-log-in.component';
|
|
||||||
|
|
||||||
describe('TestLbryLogInComponent', () => {
|
|
||||||
let component: TestLbryLogInComponent;
|
|
||||||
let fixture: ComponentFixture<TestLbryLogInComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [ TestLbryLogInComponent ]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(TestLbryLogInComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { CryptoService } from '../crypto.service';
|
|
||||||
import { SigningService } from '../signing.service';
|
|
||||||
import { IdentityService } from '../identity.service';
|
|
||||||
import { GlobalVarsService } from '../global-vars.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-test-lbry-log-in',
|
|
||||||
templateUrl: './test-lbry-log-in.component.html',
|
|
||||||
styleUrls: ['./test-lbry-log-in.component.scss']
|
|
||||||
})
|
|
||||||
export class TestLbryLogInComponent implements OnInit {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private signingService: SigningService,
|
|
||||||
private identityService: IdentityService,
|
|
||||||
private globalVars: GlobalVarsService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
const wallet : object | null = this.cryptoService.getWallet(this.globalVars.hostname);
|
|
||||||
(<HTMLInputElement>document.getElementById("wallet")).value = JSON.stringify(wallet, null, 2) || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
saveWallet(): void {
|
|
||||||
const walletStr = (<HTMLInputElement>document.getElementById("wallet")).value;
|
|
||||||
const wallet = JSON.parse(walletStr)
|
|
||||||
this.cryptoService.putWallet(this.globalVars.hostname, wallet);
|
|
||||||
|
|
||||||
const addresses = this.signingService.getAddresses(wallet)
|
|
||||||
this.finishFlow(addresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
finishFlow(addresses: string[]): void {
|
|
||||||
this.identityService.login({
|
|
||||||
users: {}, // TODO sigh
|
|
||||||
addresses,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import {AccountService} from '../account.service';
|
||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {SigningService} from '../signing.service';
|
import {SigningService} from '../signing.service';
|
||||||
import {IdentityService} from '../identity.service';
|
import {IdentityService} from '../identity.service';
|
||||||
import {CryptoService} from '../crypto.service';
|
import {GlobalVarsService} from '../global-vars.service';
|
||||||
import { GlobalVarsService } from '../global-vars.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-test-sign-transaction',
|
selector: 'app-test-sign-transaction',
|
||||||
|
@ -14,9 +14,9 @@ export class TestSignTransactionComponent implements OnInit {
|
||||||
signedTransactionHex?: string
|
signedTransactionHex?: string
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
private signingService: SigningService,
|
private signingService: SigningService,
|
||||||
private identityService: IdentityService,
|
private identityService: IdentityService,
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private globalVars: GlobalVarsService,
|
private globalVars: GlobalVarsService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export class TestSignTransactionComponent implements OnInit {
|
||||||
const fromAddress = params.get("fromAddress") || ""
|
const fromAddress = params.get("fromAddress") || ""
|
||||||
const nonWitnessUtxoHexes = params.get("nonWitnessUtxoHexes") || null
|
const nonWitnessUtxoHexes = params.get("nonWitnessUtxoHexes") || null
|
||||||
|
|
||||||
const wallet : object | null = this.cryptoService.getWallet(this.globalVars.hostname);
|
const wallet : object | null = this.accountService.getWallet();
|
||||||
const signingKey = this.signingService.getSigningKey(wallet, fromAddress)
|
const signingKey = this.signingService.getSigningKey(wallet, fromAddress)
|
||||||
|
|
||||||
// TODO what if error? etc etc.
|
// TODO what if error? etc etc.
|
||||||
|
@ -39,7 +39,7 @@ export class TestSignTransactionComponent implements OnInit {
|
||||||
|
|
||||||
finishFlow(signedTransactionHex?: string): void {
|
finishFlow(signedTransactionHex?: string): void {
|
||||||
this.identityService.login({
|
this.identityService.login({
|
||||||
users: {}, // TODO sigh
|
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||||
signedTransactionHex,
|
signedTransactionHex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import {AccountService} from '../account.service';
|
||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {GlobalVarsService} from '../global-vars.service';
|
||||||
import {SigningService} from '../signing.service';
|
import {SigningService} from '../signing.service';
|
||||||
import {IdentityService} from '../identity.service';
|
import {IdentityService} from '../identity.service';
|
||||||
|
|
||||||
|
@ -18,6 +20,8 @@ export class TestSignComponent implements OnInit {
|
||||||
privateKeyString: string = "thhUUVXQtyxonMaezCKwihLw9tZUrGJgWBMxDNfxoWub8dLGA"
|
privateKeyString: string = "thhUUVXQtyxonMaezCKwihLw9tZUrGJgWBMxDNfxoWub8dLGA"
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
|
private globalVars: GlobalVarsService,
|
||||||
private signingService: SigningService,
|
private signingService: SigningService,
|
||||||
private identityService: IdentityService,
|
private identityService: IdentityService,
|
||||||
) { }
|
) { }
|
||||||
|
@ -34,7 +38,7 @@ export class TestSignComponent implements OnInit {
|
||||||
|
|
||||||
finishFlow(signatureHex?: string): void {
|
finishFlow(signatureHex?: string): void {
|
||||||
this.identityService.login({
|
this.identityService.login({
|
||||||
users: {}, // TODO sigh
|
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||||
signatureHex,
|
signatureHex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
hostname: 'identity.deso.org',
|
hostname: 'localhost:4201',
|
||||||
nodeHostname: 'node.deso.org',
|
nodeHostname: 'node.deso.org', // TODO deleteme
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,8 +4,20 @@
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
hostname: 'identity.deso.org',
|
hostname: 'localhost:4201',
|
||||||
nodeHostname: 'node.deso.org',
|
nodeHostname: 'node.deso.org', // TODO deleteme
|
||||||
|
walletSyncHostname: 'localhost:8091',
|
||||||
|
hubHostnames: [
|
||||||
|
'spv11.lbry.com:50001',
|
||||||
|
'spv12.lbry.com:50001',
|
||||||
|
'spv13.lbry.com:50001',
|
||||||
|
'spv14.lbry.com:50001',
|
||||||
|
'spv15.lbry.com:50001',
|
||||||
|
'spv16.lbry.com:50001',
|
||||||
|
'spv17.lbry.com:50001',
|
||||||
|
'spv18.lbry.com:50001',
|
||||||
|
'spv19.lbry.com:50001',
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -1,25 +1,70 @@
|
||||||
export interface PrivateUserInfo {
|
// TODO: Use our wallet json-schema. Otherwise, bitcoinjs-lib probably already has a networks enum.
|
||||||
seedHex: string;
|
// TODO: what about encrypted wallets?
|
||||||
mnemonic: string;
|
|
||||||
extraText: string;
|
export enum AddressType {
|
||||||
network: Network;
|
DeterministicChain = "deterministic-chain",
|
||||||
}
|
SingleAddress = "single-address",
|
||||||
|
|
||||||
export interface PublicUserInfo {
|
|
||||||
hasExtraText: boolean;
|
|
||||||
encryptedSeedHex: string;
|
|
||||||
network: Network;
|
|
||||||
accessLevel: AccessLevel;
|
|
||||||
accessLevelHmac: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserProfile {
|
|
||||||
username: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The `ledger` field of accounts in a wallet (or PrivateAccountInfo).
|
||||||
|
// TODO - Rename this to "Ledger" to distinguish from `lbry.networks`,
|
||||||
|
// particularly since the ledger values are different from the
|
||||||
|
// `lbry.networks` field names.
|
||||||
export enum Network {
|
export enum Network {
|
||||||
mainnet = 'mainnet',
|
MainNet = "lbc_mainnet",
|
||||||
testnet = 'testnet',
|
TestNet = "lbc_testnet",
|
||||||
|
RegTest = "lbc_regtest",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only safe for web wallet, not sent to the app
|
||||||
|
export interface PrivateAccountInfo {
|
||||||
|
address_generator: {
|
||||||
|
change?: {
|
||||||
|
gap: number,
|
||||||
|
maximum_uses_per_address: number,
|
||||||
|
},
|
||||||
|
name: AddressType,
|
||||||
|
receiving?: {
|
||||||
|
gap: number,
|
||||||
|
maximum_uses_per_address: number,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
certificates: {[key: string]: string},
|
||||||
|
encrypted: boolean,
|
||||||
|
ledger: Network,
|
||||||
|
modified_on: number,
|
||||||
|
name: string,
|
||||||
|
private_key: string,
|
||||||
|
public_key: string,
|
||||||
|
seed: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivateChannelInfo {
|
||||||
|
// TODO - add more useful stuff
|
||||||
|
claimId: string;
|
||||||
|
name: string;
|
||||||
|
normalizedName: string;
|
||||||
|
pubKeyId: string;
|
||||||
|
signingKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// can be sent to the app
|
||||||
|
export interface PublicChannelInfo {
|
||||||
|
// TODO - add more useful stuff
|
||||||
|
claimId: string;
|
||||||
|
name: string;
|
||||||
|
normalizedName: string;
|
||||||
|
pubKeyId: string;
|
||||||
|
|
||||||
|
// Don't care about sending the hmac-verifiable accessLevel to the app for it
|
||||||
|
// to send back, as DeSo did. I don't get it, it's overly complicated. We can
|
||||||
|
// just check the permissions based on what's in localStorage.
|
||||||
|
//
|
||||||
|
// Though, maybe this was for the sake of Safari where localStorage doesn't
|
||||||
|
// work? We'll see I guess.
|
||||||
|
//
|
||||||
|
// accessLevel: AccessLevel;
|
||||||
|
// accessLevelHmac: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AccessLevel {
|
export enum AccessLevel {
|
||||||
|
@ -38,3 +83,9 @@ export enum AccessLevel {
|
||||||
// Node can sign all transactions without approval
|
// Node can sign all transactions without approval
|
||||||
Full = 4,
|
Full = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ActionType {
|
||||||
|
Action = 0,
|
||||||
|
Transaction = 1,
|
||||||
|
// TODO - probably gets a lot more detailed than this
|
||||||
|
}
|
||||||
|
|