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
|
||||
|
||||
Copyright (c) 2021 DeSo Community Developers
|
||||
Copyright (c) 2022 LBRY
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
17
angular.json
|
@ -88,23 +88,6 @@
|
|||
"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": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"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)
|
||||
|
||||
# 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).
|
||||
|
||||
TODO
|
||||
|
|
|
@ -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/tx": "^3.3.2",
|
||||
"@ethereumjs/vm": "^5.5.3",
|
||||
"amplitude-js": "^7.4.3",
|
||||
"assert": "^2.0.0",
|
||||
"autolinker": "^3.14.2",
|
||||
"bs58": "^4.0.1",
|
||||
|
@ -101,39 +100,6 @@
|
|||
"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": {
|
||||
"version": "0.1002.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz",
|
||||
|
@ -5713,17 +5679,6 @@
|
|||
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
|
@ -27902,32 +27852,6 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "0.1002.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz",
|
||||
|
@ -32184,17 +32108,6 @@
|
|||
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
"@ethereumjs/common": "^2.5.0",
|
||||
"@ethereumjs/tx": "^3.3.2",
|
||||
"@ethereumjs/vm": "^5.5.3",
|
||||
"amplitude-js": "^7.4.3",
|
||||
"assert": "^2.0.0",
|
||||
"autolinker": "^3.14.2",
|
||||
"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> {
|
||||
// TODO - we may not need this method
|
||||
// see notes in: (identity) src/app/identity.service.ts
|
||||
const request = this.identityService.jwt({
|
||||
...this.identityService.identityServiceParamsForKey(publicKey),
|
||||
});
|
||||
|
|
|
@ -73,7 +73,7 @@ export class GlobalVarsService {
|
|||
|
||||
testLoginLBRY() : Observable<string[]> {
|
||||
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.
|
||||
subscriber.next(res.addresses)
|
||||
subscriber.complete()
|
||||
|
|
|
@ -50,7 +50,6 @@ export class IdentityService {
|
|||
nonWitnessUtxoHexes?: string,
|
||||
fromAddress?: string,
|
||||
public_key?: string;
|
||||
accessLevelRequest?: number;
|
||||
}
|
||||
): Observable<any> {
|
||||
let url = this.identityServiceURL as string;
|
||||
|
@ -92,10 +91,6 @@ export class IdentityService {
|
|||
httpParams = httpParams.append("public_key", params.public_key);
|
||||
}
|
||||
|
||||
if (params?.accessLevelRequest) {
|
||||
httpParams = httpParams.append("accessLevelRequest", params.accessLevelRequest.toString());
|
||||
}
|
||||
|
||||
const paramsStr = httpParams.toString();
|
||||
if (paramsStr) {
|
||||
url += `?${paramsStr}`;
|
||||
|
|
|
@ -60,7 +60,7 @@ export class SpendLBCComponent implements OnInit {
|
|||
const toAddress = (<HTMLInputElement>document.getElementById("toAddress")).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 => {
|
||||
this.psbtHex = res.psbtHex
|
||||
this.nonWitnessUtxoHexes = res.nonWitnessUtxoHex
|
||||
|
@ -99,14 +99,14 @@ export class SpendLBCComponent implements OnInit {
|
|||
this.globalVars.testSignTransactionLBRY(this.psbtHex, this.nonWitnessUtxoHexes, this.fromAddress).subscribe({
|
||||
next: (signedTransactionHex) => {
|
||||
this.signedTransactionHex = signedTransactionHex
|
||||
this.backendApi.DecodeTransaction("localhost:8090", signedTransactionHex).subscribe({
|
||||
this.backendApi.DecodeTransaction(environment.backendHostname, signedTransactionHex).subscribe({
|
||||
next: res => {
|
||||
this.decodedTransaction = res.decodedTransaction
|
||||
console.log(res.decodedTransaction)
|
||||
},
|
||||
error: err => { this.setError(err) }
|
||||
})
|
||||
this.backendApi.BroadcastTransaction("localhost:8090", signedTransactionHex).subscribe({
|
||||
this.backendApi.BroadcastTransaction(environment.backendHostname, signedTransactionHex).subscribe({
|
||||
next: res => {
|
||||
this.setError(null)
|
||||
this.success = res.txid
|
||||
|
|
|
@ -2,32 +2,7 @@
|
|||
"name": "deso",
|
||||
"short_name": "deso",
|
||||
"description": "The crypto social network",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"icons": [],
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"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 = {
|
||||
production: true,
|
||||
uploadImageHostname: "node.deso.org",
|
||||
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/",
|
||||
},
|
||||
identityURL: "http://localhost:4201",
|
||||
};
|
||||
|
|
|
@ -4,25 +4,6 @@
|
|||
|
||||
export const environment = {
|
||||
production: false,
|
||||
uploadImageHostname: "node.deso.org",
|
||||
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/",
|
||||
},
|
||||
backendHostname: 'localhost:8090',
|
||||
identityURL: "http://localhost:4201",
|
||||
};
|
||||
|
|
|
@ -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="#121212" media="(prefers-color-scheme: dark)">
|
||||
<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="/" />
|
||||
|
||||
<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",
|
||||
"bip32": "^2.0.6",
|
||||
"bip39": "^3.0.3",
|
||||
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
||||
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"ecpair": "^1.0.1",
|
||||
"elliptic": "^6.5.4",
|
||||
|
@ -32,8 +32,6 @@
|
|||
"jsonwebtoken": "^8.5.1",
|
||||
"key-encoder": "^2.0.3",
|
||||
"ngx-bootstrap": "^6.2.0",
|
||||
"ngx-cookie": "^5.0.2",
|
||||
"ngx-intl-tel-input": "^3.1.1",
|
||||
"readable-stream": "^3.6.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "~6.6.0",
|
||||
|
@ -3400,8 +3398,8 @@
|
|||
},
|
||||
"node_modules/bitcoinjs-lib": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
||||
"integrity": "sha512-xluOW5MDgy2GTFAW8Vg7qYAgAlOTTaFWlogg8ejNetNRSZ8tg3go9XkEHdQXFhf1mDxKl55ybvSl0ie1ApiTrA==",
|
||||
"resolved": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||
"integrity": "sha512-AwoS80uq7ADqozrLW3aDjSvLZpQQn7a2Jt6Qt9DOiYTcwjxWarWEt+ntkNskzk63Azs779Wb0suD6KqWbJ/U5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bech32": "^2.0.0",
|
||||
|
@ -7200,15 +7198,6 @@
|
|||
"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": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
||||
|
@ -10228,34 +10217,6 @@
|
|||
"@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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
|
@ -21472,9 +21433,9 @@
|
|||
}
|
||||
},
|
||||
"bitcoinjs-lib": {
|
||||
"version": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
||||
"integrity": "sha512-xluOW5MDgy2GTFAW8Vg7qYAgAlOTTaFWlogg8ejNetNRSZ8tg3go9XkEHdQXFhf1mDxKl55ybvSl0ie1ApiTrA==",
|
||||
"from": "bitcoinjs-lib@lbryio/bitcoinjs-lib#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
||||
"version": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||
"integrity": "sha512-AwoS80uq7ADqozrLW3aDjSvLZpQQn7a2Jt6Qt9DOiYTcwjxWarWEt+ntkNskzk63Azs779Wb0suD6KqWbJ/U5A==",
|
||||
"from": "bitcoinjs-lib@github:lbryio/bitcoinjs-lib#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||
"requires": {
|
||||
"bech32": "^2.0.0",
|
||||
"bip174": "^2.0.1",
|
||||
|
@ -24755,12 +24716,6 @@
|
|||
"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": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
||||
|
@ -27259,22 +27214,6 @@
|
|||
"integrity": "sha512-5WKHo6/ltkenw4UyXZwED8rODCgp2RGbWurzYzZsF/gH1JO5SN7TJ+AL6kXYk6XM42sDA2WhN9Db+ZPNjiyHnA==",
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"base64-js": "^1.5.1",
|
||||
"bip32": "^2.0.6",
|
||||
"bip39": "^3.0.3",
|
||||
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e",
|
||||
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"ecpair": "^1.0.1",
|
||||
"elliptic": "^6.5.4",
|
||||
|
@ -37,8 +37,6 @@
|
|||
"jsonwebtoken": "^8.5.1",
|
||||
"key-encoder": "^2.0.3",
|
||||
"ngx-bootstrap": "^6.2.0",
|
||||
"ngx-cookie": "^5.0.2",
|
||||
"ngx-intl-tel-input": "^3.1.1",
|
||||
"readable-stream": "^3.6.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "~6.6.0",
|
||||
|
|
|
@ -1,60 +1,228 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {CryptoService} from './crypto.service';
|
||||
import {GlobalVarsService} from './global-vars.service';
|
||||
import {AccessLevel, Network, PrivateUserInfo, PublicUserInfo} from '../types/identity';
|
||||
import HDKey from 'hdkey';
|
||||
import {HubService} from './hub.service';
|
||||
import {SigningService} from './signing.service';
|
||||
import {
|
||||
AccessLevel,
|
||||
ActionType,
|
||||
PrivateAccountInfo,
|
||||
PrivateChannelInfo,
|
||||
PublicChannelInfo,
|
||||
} from '../types/identity';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AccountService {
|
||||
private static usersStorageKey = 'users';
|
||||
private static levelsStorageKey = 'levels';
|
||||
|
||||
private static publicKeyRegex = /^[a-zA-Z0-9]{54,55}$/;
|
||||
private static walletStorageKey = 'wallet';
|
||||
private static channelsStorageKey = 'channels';
|
||||
private static accessStorageKey = 'access';
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private globalVars: GlobalVarsService,
|
||||
private signingService: SigningService,
|
||||
) { }
|
||||
|
||||
// Public Getters
|
||||
/*
|
||||
|
||||
getPublicKeys(): any {
|
||||
return Object.keys(this.getPrivateUsers());
|
||||
What we're keeping in local storage. TODO - make these into data structs.
|
||||
|
||||
// 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} {
|
||||
const hostname = this.globalVars.hostname;
|
||||
const privateUsers = this.getPrivateUsers();
|
||||
const publicUsers: {[key: string]: PublicUserInfo} = {};
|
||||
// Access information. For each hostname, what channel is logged in, and what
|
||||
// level of permission for various actions did the user give?
|
||||
localStorage["access"] = {
|
||||
// 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)) {
|
||||
const privateUser = privateUsers[publicKey];
|
||||
const accessLevel = this.getAccessLevel(publicKey, hostname);
|
||||
if (accessLevel === AccessLevel.None) {
|
||||
localStorage["channels"] = {
|
||||
// There can be multiple channels
|
||||
"<channels>": PrivateChannelInfo,
|
||||
}
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const encryptedSeedHex = this.cryptoService.encryptSeedHex(privateUser.seedHex, hostname);
|
||||
const accessLevelHmac = this.cryptoService.accessLevelHmac(accessLevel, privateUser.seedHex);
|
||||
|
||||
publicUsers[publicKey] = {
|
||||
hasExtraText: privateUser.extraText?.length > 0,
|
||||
encryptedSeedHex,
|
||||
network: privateUser.network,
|
||||
accessLevel,
|
||||
accessLevelHmac,
|
||||
};
|
||||
filteredAccounts.push(account);
|
||||
}
|
||||
|
||||
return publicUsers;
|
||||
return filteredAccounts
|
||||
}
|
||||
|
||||
getAccessLevel(publicKey: string, hostname: string): AccessLevel {
|
||||
const levels = JSON.parse(localStorage.getItem(AccountService.levelsStorageKey) || '{}');
|
||||
const hostMapping = levels[hostname] || {};
|
||||
const accessLevel = hostMapping[publicKey];
|
||||
private clearChannels() {
|
||||
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(null));
|
||||
}
|
||||
|
||||
// TODO - This function is async due to the http call, so now I have to
|
||||
// rethink the guarantees about login state being based on the data being in
|
||||
// localStorage.
|
||||
//
|
||||
// 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)) {
|
||||
return accessLevel;
|
||||
|
@ -63,80 +231,63 @@ export class AccountService {
|
|||
}
|
||||
}
|
||||
|
||||
// Public Modifiers
|
||||
|
||||
addUser(keychain: HDKey, mnemonic: string, extraText: string, network: Network): string {
|
||||
const seedHex = this.cryptoService.keychainToSeedHex(keychain);
|
||||
|
||||
return this.addPrivateUser({
|
||||
seedHex,
|
||||
mnemonic,
|
||||
extraText,
|
||||
network,
|
||||
});
|
||||
}
|
||||
|
||||
deleteUser(publicKey: string): void {
|
||||
const privateUsers = this.getPrivateUsersRaw();
|
||||
|
||||
delete privateUsers[publicKey];
|
||||
|
||||
this.setPrivateUsersRaw(privateUsers);
|
||||
}
|
||||
|
||||
setAccessLevel(publicKey: string, hostname: string, accessLevel: AccessLevel): void {
|
||||
const levels = JSON.parse(localStorage.getItem(AccountService.levelsStorageKey) || '{}');
|
||||
|
||||
levels[hostname] ||= {};
|
||||
levels[hostname][publicKey] = accessLevel;
|
||||
|
||||
localStorage.setItem(AccountService.levelsStorageKey, JSON.stringify(levels));
|
||||
}
|
||||
|
||||
// Private Getters and Modifiers
|
||||
|
||||
// TEMP: public for import flow
|
||||
public addPrivateUser(userInfo: PrivateUserInfo): string {
|
||||
const privateUsers = this.getPrivateUsersRaw();
|
||||
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} {
|
||||
const privateUsers = this.getPrivateUsersRaw();
|
||||
const filteredPrivateUsers: {[key: string]: PrivateUserInfo} = {};
|
||||
|
||||
for (const publicKey of Object.keys(privateUsers)) {
|
||||
const privateUser = privateUsers[publicKey];
|
||||
|
||||
// Only include users from the current network
|
||||
if (privateUser.network !== this.globalVars.network) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get rid of some users who have invalid public keys
|
||||
if (!publicKey.match(AccountService.publicKeyRegex)) {
|
||||
this.deleteUser(publicKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
filteredPrivateUsers[publicKey] = privateUser;
|
||||
public setAccessLevel(hostname: string, channelClaimId: string, action: ActionType, level: AccessLevel) {
|
||||
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||
if (!(hostname in access)) {
|
||||
access[hostname] = {levels: {}}
|
||||
}
|
||||
|
||||
return filteredPrivateUsers;
|
||||
if (!(channelClaimId in access[hostname].levels)) {
|
||||
access[hostname].levels[channelClaimId] = {}
|
||||
}
|
||||
access[hostname].levels[channelClaimId][action] = level
|
||||
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
|
||||
}
|
||||
|
||||
private getPrivateUsersRaw(): {[key: string]: PrivateUserInfo} {
|
||||
return JSON.parse(localStorage.getItem(AccountService.usersStorageKey) || '{}');
|
||||
public setAccessCurrentChannel(hostname: string, channelClaimId: string) {
|
||||
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||
if (!(hostname in access)) {
|
||||
access[hostname] = {levels: {}}
|
||||
}
|
||||
access[hostname].currentChannel = channelClaimId
|
||||
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
|
||||
}
|
||||
|
||||
private setPrivateUsersRaw(privateUsers: {[key: string]: PrivateUserInfo}): void {
|
||||
localStorage.setItem(AccountService.usersStorageKey, JSON.stringify(privateUsers));
|
||||
public walletLogout() {
|
||||
this.putWallet(null)
|
||||
this.clearAccess()
|
||||
this.clearChannels()
|
||||
}
|
||||
|
||||
public walletLogin(wallet: object | null) {
|
||||
// no ambiguity from half-completed actions
|
||||
// make sure we're fully logged out
|
||||
this.walletLogout()
|
||||
|
||||
this.putWallet(wallet)
|
||||
this.initAccess()
|
||||
this.updateChannels()
|
||||
}
|
||||
|
||||
// TODO - delete this if I don't end up using it
|
||||
public walletIsLoggedIn(): boolean {
|
||||
return (
|
||||
// All three should be set. It means the last login was complete, and no
|
||||
// logout was started since.
|
||||
this.hasAccess() && this.hasChannels() && this.hasWallet()
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Do we want to save per-hostname-per-channel-per-action access levels
|
||||
// between logins? Counterargument: users probably won't log in and out too
|
||||
// often.
|
||||
private appClearAccess(hostname: string): void {
|
||||
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||
delete access[hostname]
|
||||
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
|
||||
}
|
||||
|
||||
public appLogout(hostname: string): void {
|
||||
this.appClearAccess(hostname)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,38 +1,35 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
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 {EmbedComponent} from './embed/embed.component';
|
||||
import {HomeComponent} from './home/home.component';
|
||||
import {LogoutComponent} from './logout/logout.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 {LoadSeedComponent} from './load-seed/load-seed.component';
|
||||
|
||||
export class RouteNames {
|
||||
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 EMBED = 'embed';
|
||||
public static LOGOUT = 'logout';
|
||||
public static SIGN_UP = 'sign-up';
|
||||
public static LOG_IN = 'log-in';
|
||||
public static LOAD_SEED = 'load-seed';
|
||||
public static LOG_IN_APP = 'log-in-app';
|
||||
public static APPROVE = 'approve';
|
||||
}
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: HomeComponent, 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.EMBED, component: EmbedComponent, pathMatch: 'full' },
|
||||
{ path: RouteNames.LOGOUT, component: LogoutComponent, pathMatch: 'full' },
|
||||
{ path: RouteNames.SIGN_UP, component: SignUpComponent, pathMatch: 'full' },
|
||||
{ path: RouteNames.LOG_IN, component: LogInComponent, pathMatch: 'full' },
|
||||
{ path: RouteNames.LOAD_SEED, component: LoadSeedComponent, pathMatch: 'full' },
|
||||
{ path: RouteNames.LOG_IN_APP, component: LogInAppComponent, pathMatch: 'full' },
|
||||
{ path: RouteNames.APPROVE, component: ApproveComponent, pathMatch: 'full' },
|
||||
];
|
||||
|
||||
|
|
|
@ -22,17 +22,12 @@ export class AppComponent implements OnInit {
|
|||
// load params
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
const accessLevelRequest = params.get('accessLevelRequest');
|
||||
if (accessLevelRequest) {
|
||||
this.globalVars.accessLevelRequest = parseInt(accessLevelRequest, 10);
|
||||
}
|
||||
|
||||
if (params.get('webview')) {
|
||||
this.globalVars.webview = true;
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -49,6 +44,17 @@ export class AppComponent implements OnInit {
|
|||
|
||||
if (this.globalVars.callback) {
|
||||
// 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.finishInit();
|
||||
} 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 {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {IdentityService} from './identity.service';
|
||||
import {CookieModule} from 'ngx-cookie';
|
||||
import { LogoutComponent } from './logout/logout.component';
|
||||
import { BannerComponent } from './banner/banner.component';
|
||||
import { SignUpComponent } from './sign-up/sign-up.component';
|
||||
import {AccountService} from './account.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 { ApproveComponent } from './approve/approve.component';
|
||||
import { LoadSeedComponent } from './load-seed/load-seed.component';
|
||||
import { ErrorCallbackComponent } from './error-callback/error-callback.component';
|
||||
import { NgxIntlTelInputModule } from 'ngx-intl-tel-input';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TestSignComponent } from './test-sign/test-sign.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({
|
||||
declarations: [
|
||||
|
@ -34,13 +31,12 @@ import { TestLbryLogInComponent } from './test-lbry-log-in/test-lbry-log-in.comp
|
|||
LogoutComponent,
|
||||
BannerComponent,
|
||||
SignUpComponent,
|
||||
LogInComponent,
|
||||
LogInAppComponent,
|
||||
ApproveComponent,
|
||||
LoadSeedComponent,
|
||||
ErrorCallbackComponent,
|
||||
TestSignComponent,
|
||||
TestSignTransactionComponent,
|
||||
TestLbryLogInComponent,
|
||||
LogInWalletComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -49,10 +45,8 @@ import { TestLbryLogInComponent } from './test-lbry-log-in/test-lbry-log-in.comp
|
|||
AppRoutingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxIntlTelInputModule,
|
||||
MatFormFieldModule,
|
||||
MatTooltipModule,
|
||||
CookieModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
IdentityService,
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
{{ globalVars.hostname }} wants to {{ transactionDescription }}
|
||||
</p>
|
||||
<br/>
|
||||
<div *ngIf="transactionDeSoSpent">
|
||||
<div *ngIf="transactionSpent">
|
||||
<p class="pb-15px">
|
||||
Total Cost: {{ transactionDeSoSpent }} $DESO
|
||||
Total Cost: {{ transactionSpent }} $DESO
|
||||
</p>
|
||||
</div>
|
||||
<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 {CryptoService} from '../crypto.service';
|
||||
import {IdentityService} from '../identity.service';
|
||||
import {AccountService} from '../account.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 {Observable, of} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
@ -33,7 +32,6 @@ import {
|
|||
TransactionMetadataDAOCoin,
|
||||
TransactionMetadataTransferDAOCoin
|
||||
} from '../../lib/deso/transaction';
|
||||
import bs58check from 'bs58check';
|
||||
|
||||
@Component({
|
||||
selector: 'app-approve',
|
||||
|
@ -46,29 +44,32 @@ export class ApproveComponent implements OnInit {
|
|||
transactionHex: any;
|
||||
username: any;
|
||||
transactionDescription: any;
|
||||
transactionDeSoSpent: string | boolean = false;
|
||||
transactionSpent: string | boolean = false;
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private cryptoService: CryptoService,
|
||||
private identityService: IdentityService,
|
||||
private accountService: AccountService,
|
||||
public globalVars: GlobalVarsService,
|
||||
private signingService: SigningService,
|
||||
// private signingService: SigningService,
|
||||
private backendApi: BackendAPIService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.queryParams.subscribe(params => {
|
||||
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');
|
||||
this.transaction = Transaction.fromBytes(txBytes)[0];
|
||||
this.publicKey = this.base58KeyCheck(this.transaction.publicKey);
|
||||
|
||||
this.generateTransactionDescription();
|
||||
/*
|
||||
TODO this.accountService.getActiveChannelAccessLevel
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -77,22 +78,26 @@ export class ApproveComponent implements OnInit {
|
|||
}
|
||||
|
||||
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);
|
||||
*/
|
||||
|
||||
/*
|
||||
TODO this.accountService.setAccessLevel if people want to keep allowing the action
|
||||
*/
|
||||
}
|
||||
|
||||
finishFlow(signedTransactionHex?: string): void {
|
||||
this.identityService.login({
|
||||
users: this.accountService.getEncryptedUsers(),
|
||||
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||
signedTransactionHex,
|
||||
});
|
||||
}
|
||||
|
||||
seedHex(): string {
|
||||
const encryptedSeedHex = this.accountService.getEncryptedUsers()[this.publicKey].encryptedSeedHex;
|
||||
return this.cryptoService.decryptSeedHex(encryptedSeedHex, this.globalVars.hostname);
|
||||
}
|
||||
|
||||
generateTransactionDescription(): void {
|
||||
let description = 'sign an unknown transaction';
|
||||
let publicKeys: string[] = [];
|
||||
|
@ -252,8 +257,13 @@ export class ApproveComponent implements OnInit {
|
|||
}
|
||||
|
||||
base58KeyCheck(keyBytes: Uint8Array): string {
|
||||
const prefix = CryptoService.PUBLIC_KEY_PREFIXES[this.globalVars.network].deso;
|
||||
return bs58check.encode(Buffer.from([...prefix, ...keyBytes]));
|
||||
// TODO
|
||||
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 {
|
||||
|
@ -273,7 +283,7 @@ export class ApproveComponent implements OnInit {
|
|||
return of(description);
|
||||
}
|
||||
// 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;
|
||||
// If the response has no users, return the description as is.
|
||||
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 {Observable, of} from 'rxjs';
|
||||
import {catchError, map} from 'rxjs/operators';
|
||||
import {environment} from '../environments/environment';
|
||||
import {SigningService} from './signing.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 {
|
||||
Username: string | null = null;
|
||||
Description: string | null = null;
|
||||
ProfilePic?: string;
|
||||
PublicKeyBase58Check?: string;
|
||||
}
|
||||
|
||||
export class User {
|
||||
|
@ -25,98 +18,83 @@ export class User {
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class BackendAPIService {
|
||||
walletSyncEndpoint = `https://${environment.walletSyncHostname}/api/v0`;
|
||||
|
||||
endpoint = `https://${environment.nodeHostname}/api/v0`;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private cryptoService: CryptoService,
|
||||
private signingService: SigningService,
|
||||
private accountService: AccountService,
|
||||
private globalVars: GlobalVarsService,
|
||||
) { }
|
||||
|
||||
post(path: string, body: any): Observable<any> {
|
||||
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(
|
||||
publicKeys: string[], SkipForLeaderboard: boolean = false,
|
||||
publicKeys: string[]
|
||||
): Observable<{ UserList: User[]}> {
|
||||
return this.httpClient.post<any>(
|
||||
`${this.endpoint}/get-users-stateless`,
|
||||
{
|
||||
PublicKeysBase58Check: publicKeys,
|
||||
SkipForLeaderboard,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
GetUserProfiles(
|
||||
GetUsernames(
|
||||
publicKeys: string[]
|
||||
): Observable<{[key: string]: UserProfile}> {
|
||||
const userProfiles: {[key: string]: any} = {};
|
||||
const req = this.GetUsersStateless(publicKeys, true);
|
||||
): Observable<{[key: string]: string}> {
|
||||
const usernames: {[key: string]: any} = {};
|
||||
const req = this.GetUsersStateless(publicKeys);
|
||||
if (publicKeys.length > 0) {
|
||||
return req.pipe(
|
||||
map( res => {
|
||||
for (const user of res.UserList) {
|
||||
userProfiles[user.PublicKeyBase58Check] = {
|
||||
username: user.ProfileEntryResponse?.Username,
|
||||
};
|
||||
usernames[user.PublicKeyBase58Check] = user.ProfileEntryResponse?.Username
|
||||
}
|
||||
return userProfiles;
|
||||
return usernames;
|
||||
})
|
||||
).pipe(
|
||||
catchError(() => {
|
||||
for(const publicKey of publicKeys) {
|
||||
userProfiles[publicKey] = {};
|
||||
usernames[publicKey] = "";
|
||||
}
|
||||
return of(userProfiles);
|
||||
return of(usernames);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return of(userProfiles);
|
||||
return of(usernames);
|
||||
}
|
||||
}
|
||||
|
||||
GetTransactionSpending(
|
||||
transactionHex: string
|
||||
): Observable<number> {
|
||||
const req = this.httpClient.post<any>(
|
||||
`${this.endpoint}/get-transaction-spending`,
|
||||
// TODO - WIP, not using for now.
|
||||
|
||||
// This isn't what the current wallet sync API looks like, but it will
|
||||
// likely change anyway. So this is an approximation with a stub for the time being.
|
||||
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 HDNode from 'hdkey';
|
||||
import * as bip39 from 'bip39';
|
||||
import HDKey from 'hdkey';
|
||||
import {ec as EC} from 'elliptic';
|
||||
import bs58check from 'bs58check';
|
||||
import {CookieService} from 'ngx-cookie';
|
||||
import {createHmac, createCipher, createDecipher, randomBytes} from 'crypto';
|
||||
import {AccessLevel, Network} from '../types/identity';
|
||||
import { GlobalVarsService } from './global-vars.service';
|
||||
import {AccessLevel} from '../types/identity';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CryptoService {
|
||||
|
||||
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;
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
// 32 bytes = 256 bits is plenty of entropy for encryption
|
||||
newEncryptionKey(): string {
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
// TODO we won't need this soon right?
|
||||
seedHexEncryptionStorageKey(hostname: string): string {
|
||||
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 {
|
||||
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
|
||||
// previous key is overwritten, which is useful in logging out users.
|
||||
|
@ -115,20 +34,10 @@ export class CryptoService {
|
|||
const storageKey = this.seedHexEncryptionStorageKey(hostname);
|
||||
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) || '';
|
||||
if (!encryptionKey || reset) {
|
||||
encryptionKey = this.newEncryptionKey();
|
||||
localStorage.setItem(storageKey, encryptionKey);
|
||||
}
|
||||
encryptionKey = localStorage.getItem(storageKey) || '';
|
||||
if (!encryptionKey || reset) {
|
||||
encryptionKey = this.newEncryptionKey();
|
||||
localStorage.setItem(storageKey, encryptionKey);
|
||||
}
|
||||
|
||||
// If the encryption key is unset or malformed we need to stop
|
||||
|
@ -165,40 +74,6 @@ export class CryptoService {
|
|||
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
|
||||
publicKeyToECBuffer(publicKey: string): Buffer {
|
||||
// Sanity check similar to Base58CheckDecodePrefix from core/lib/base58.go
|
||||
|
@ -213,4 +88,16 @@ export class CryptoService {
|
|||
|
||||
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 {AccessLevel, Network} from '../types/identity';
|
||||
import {Network} from '../types/identity';
|
||||
import {environment} from '../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GlobalVarsService {
|
||||
network = Network.mainnet;
|
||||
network : Network = Network.MainNet;
|
||||
hostname = '';
|
||||
accessLevelRequest = AccessLevel.ApproveAll;
|
||||
|
||||
inTab = !!window.opener;
|
||||
webview = false;
|
||||
|
@ -17,7 +16,6 @@ export class GlobalVarsService {
|
|||
callback = '';
|
||||
callbackInvalid = false;
|
||||
|
||||
|
||||
constructor() { }
|
||||
|
||||
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 {Observable, Subject} from 'rxjs';
|
||||
import {v4 as uuid} from 'uuid';
|
||||
import {AccessLevel, PublicUserInfo} from '../types/identity';
|
||||
import {AccessLevel, PublicChannelInfo} from '../types/identity';
|
||||
import {CryptoService} from './crypto.service';
|
||||
import {GlobalVarsService} from './global-vars.service';
|
||||
import {CookieService} from 'ngx-cookie';
|
||||
import {SigningService} from './signing.service';
|
||||
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({
|
||||
providedIn: 'root'
|
||||
|
@ -48,12 +23,30 @@ export class IdentityService {
|
|||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private globalVars: GlobalVarsService,
|
||||
private cookieService: CookieService,
|
||||
private signingService: SigningService,
|
||||
) {
|
||||
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
|
||||
|
||||
initialize(): Observable<any> {
|
||||
|
@ -65,8 +58,7 @@ export class IdentityService {
|
|||
}
|
||||
|
||||
login(payload: {
|
||||
users: {[key: string]: PublicUserInfo},
|
||||
publicKeyAdded?: string,
|
||||
channel: PublicChannelInfo | null,
|
||||
signedUp?: boolean
|
||||
signedTransactionHex?: string,
|
||||
addresses?: string[],
|
||||
|
@ -89,11 +81,14 @@ export class IdentityService {
|
|||
// Incoming Messages
|
||||
|
||||
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
|
||||
// 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
|
||||
// that approvalRequired = true, which the caller can then uses to trigger
|
||||
|
@ -102,24 +97,27 @@ export class IdentityService {
|
|||
return;
|
||||
}
|
||||
|
||||
// If we get to this point, no approval UI was required. This typically
|
||||
// happens if the caller has full signing access or signing access for
|
||||
// non-spending txns such as like, post, update profile, etc. In the
|
||||
// latter case we need a subsequent check to ensure that the txn is not
|
||||
// sending money to any public keys other than the sender himself.
|
||||
if (!this.approveSpending(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seedHex = this.cryptoService.decryptSeedHex(encryptedSeedHex, this.globalVars.hostname);
|
||||
const signedTransactionHex = this.signingService.signTransaction(seedHex, transactionHex);
|
||||
// TODO - this.signingService.signPSBT instead
|
||||
if it's a transaction
|
||||
const signedTransactionHex = this.signingService.signPSBT(transaction details);
|
||||
else // just an action
|
||||
const signedActionHex = this.signingService.signActiov(action details);
|
||||
|
||||
// TODO figure this out...
|
||||
this.respond(id, {
|
||||
signedTransactionHex,
|
||||
signedActionHex,
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
|
@ -136,7 +134,7 @@ export class IdentityService {
|
|||
private async handleInfo(event: MessageEvent): Promise<void> {
|
||||
// check storage access API
|
||||
let hasStorageAccess = true;
|
||||
if (this.cryptoService.mustUseStorageAccess()) {
|
||||
if (this.mustUseStorageAccess()) {
|
||||
hasStorageAccess = await document.hasStorageAccess();
|
||||
}
|
||||
|
||||
|
@ -148,57 +146,49 @@ export class IdentityService {
|
|||
hasLocalStorageAccess = false;
|
||||
}
|
||||
|
||||
// check for cookie access
|
||||
this.cookieService.put('deso-test-access', 'true');
|
||||
const hasCookieAccess = !!this.cookieService.get('deso-test-access');
|
||||
// TODO - Sort out the storage access issue:
|
||||
//
|
||||
// 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
|
||||
this.browserSupported = hasCookieAccess || hasLocalStorageAccess;
|
||||
this.browserSupported = hasLocalStorageAccess && !this.mustUseStorageAccess();
|
||||
|
||||
this.respond(event.data.id, {
|
||||
hasCookieAccess,
|
||||
hasStorageAccess,
|
||||
hasLocalStorageAccess,
|
||||
browserSupported: this.browserSupported,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Access levels
|
||||
|
||||
private getRequiredAccessLevel(transactionHex: string): AccessLevel {
|
||||
const txBytes = new Buffer(transactionHex, 'hex');
|
||||
const transaction = Transaction.fromBytes(txBytes)[0] as Transaction<any>;
|
||||
|
||||
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;
|
||||
}
|
||||
// TODO implement for lbry
|
||||
// private getRequiredAccessLevel(transactionHex: string): AccessLevel {
|
||||
// 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
|
||||
|
||||
private hasAccessLevel(data: any, requiredAccessLevel: AccessLevel): boolean {
|
||||
const { payload: { encryptedSeedHex, accessLevel, accessLevelHmac }} = data;
|
||||
|
@ -210,25 +200,6 @@ export class IdentityService {
|
|||
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 {
|
||||
const hasAccess = this.hasAccessLevel(data, accessLevel);
|
||||
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 { LogInComponent } from './log-in.component';
|
||||
import { LogInAppComponent } from './log-in-app.component';
|
||||
|
||||
describe('LogInComponent', () => {
|
||||
let component: LogInComponent;
|
||||
let fixture: ComponentFixture<LogInComponent>;
|
||||
describe('LogInAppComponent', () => {
|
||||
let component: LogInAppComponent;
|
||||
let fixture: ComponentFixture<LogInAppComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ LogInComponent ]
|
||||
declarations: [ LogInAppComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LogInComponent);
|
||||
fixture = TestBed.createComponent(LogInAppComponent);
|
||||
component = fixture.componentInstance;
|
||||
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 { LoadSeedComponent } from './load-seed.component';
|
||||
import { LogInWalletComponent } from './log-in-wallet.component';
|
||||
|
||||
describe('LoadSeedComponent', () => {
|
||||
let component: LoadSeedComponent;
|
||||
let fixture: ComponentFixture<LoadSeedComponent>;
|
||||
describe('LogInWalletComponent', () => {
|
||||
let component: LogInWalletComponent;
|
||||
let fixture: ComponentFixture<LogInWalletComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ LoadSeedComponent ]
|
||||
declarations: [ LogInWalletComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoadSeedComponent);
|
||||
fixture = TestBed.createComponent(LogInWalletComponent);
|
||||
component = fixture.componentInstance;
|
||||
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 {ActivatedRoute} from '@angular/router';
|
||||
import {CryptoService} from '../crypto.service';
|
||||
import {IdentityService} from '../identity.service';
|
||||
import {AccountService} from '../account.service';
|
||||
import {AccessLevel} from '../../types/identity';
|
||||
import {GlobalVarsService} from '../global-vars.service';
|
||||
|
||||
@Component({
|
||||
|
@ -20,7 +18,6 @@ export class LogoutComponent implements OnInit {
|
|||
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private cryptoService: CryptoService,
|
||||
private identityService: IdentityService,
|
||||
private accountService: AccountService,
|
||||
|
@ -28,9 +25,6 @@ export class LogoutComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.queryParams.subscribe(params => {
|
||||
this.publicKey = params.publicKey || '';
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
|
@ -39,7 +33,7 @@ export class LogoutComponent implements OnInit {
|
|||
|
||||
onSubmit(): void {
|
||||
// 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
|
||||
// the logged out user, will regenerate their encryptedSeedHex. Without this,
|
||||
// someone could have reused the encryptedSeedHex of an already logged out user.
|
||||
|
@ -49,7 +43,7 @@ export class LogoutComponent implements OnInit {
|
|||
|
||||
finishFlow(): void {
|
||||
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 {EntropyService} from '../entropy.service';
|
||||
import {CryptoService} from '../crypto.service';
|
||||
import {AccountService} from '../account.service';
|
||||
import {IdentityService} from '../identity.service';
|
||||
import {GlobalVarsService} from '../global-vars.service';
|
||||
|
@ -19,7 +18,6 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
|||
seedCopied = false;
|
||||
mnemonicCheck = '';
|
||||
extraTextCheck = '';
|
||||
publicKeyAdded = '';
|
||||
|
||||
// Advanced tab
|
||||
showMnemonicError = false;
|
||||
|
@ -32,7 +30,6 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
|||
|
||||
constructor(
|
||||
public entropyService: EntropyService,
|
||||
private cryptoService: CryptoService,
|
||||
private accountService: AccountService,
|
||||
private identityService: IdentityService,
|
||||
public globalVars: GlobalVarsService,
|
||||
|
@ -81,17 +78,29 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
|||
////// STEP TWO BUTTONS ///////
|
||||
|
||||
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 mnemonic = this.mnemonicCheck;
|
||||
const extraText = this.extraTextCheck;
|
||||
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.publicKeyAdded, this.globalVars.hostname, this.globalVars.accessLevelRequest);
|
||||
this.accountService.initAccess()
|
||||
|
||||
this.login();
|
||||
*/
|
||||
}
|
||||
|
||||
stepTwoBack(): void {
|
||||
|
@ -102,8 +111,7 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
|||
|
||||
login(): void {
|
||||
this.identityService.login({
|
||||
users: this.accountService.getEncryptedUsers(),
|
||||
publicKeyAdded: this.publicKeyAdded,
|
||||
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||
signedUp: true,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {HubService} from './hub.service';
|
||||
import KeyEncoder from 'key-encoder';
|
||||
import {GlobalVarsService} from './global-vars.service';
|
||||
import * as jsonwebtoken from 'jsonwebtoken';
|
||||
import {CryptoService} from './crypto.service';
|
||||
import * as sha256 from 'sha256';
|
||||
import { uvarint64ToBuf } from '../lib/bindata/util';
|
||||
import {PrivateAccountInfo, Network} from '../types/identity';
|
||||
|
||||
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.
|
||||
|
@ -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 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
|
||||
|
||||
@Injectable({
|
||||
|
@ -19,41 +20,389 @@ const NETWORK = lbry.networks.mainnet
|
|||
export class SigningService {
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private hubService: HubService,
|
||||
private globalVars: GlobalVarsService,
|
||||
) { }
|
||||
|
||||
// this should be audited and go into a library. hobbled this together from
|
||||
// code in bitcoinjs-lib.
|
||||
private getAddressFromBip32(node: BIP32Interface): string {
|
||||
const hash = lbry.crypto.hash160(node.publicKey)
|
||||
|
||||
const payload = Buffer.allocUnsafe(21);
|
||||
payload.writeUInt8(NETWORK.pubKeyHash, 0);
|
||||
hash.copy(payload, 1);
|
||||
return bs58check.encode(payload);
|
||||
private *generateKeys(
|
||||
node: BIP32Interface,
|
||||
keyPath: lbry.bip32Lbry.KeyPath
|
||||
): IterableIterator<BIP32Interface> {
|
||||
for (let childIndex = 0; ; childIndex++) {
|
||||
yield node.derive(keyPath).derive(childIndex)
|
||||
}
|
||||
}
|
||||
|
||||
getSigningKey(wallet: any, address: string): Buffer | null {
|
||||
const account = wallet.accounts
|
||||
.filter((account: any) => {
|
||||
let node: BIP32Interface = bip32.fromBase58(account.private_key);
|
||||
return address === this.getAddressFromBip32(node)
|
||||
})[0]
|
||||
return bip32.fromBase58(account.private_key).privateKey || null;
|
||||
// TODO - have a minimum batch size actually, like 10, that's bigger than a
|
||||
// gap, to reduce number of requests to the hub. or maybe 5. Get an opinion
|
||||
// on what's a common number of used keys. And channels, that may be a
|
||||
// different answer.
|
||||
// TODO - have a maximum number of keys? Just in case there's a bug so we
|
||||
// don't blow up the user's browser?
|
||||
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)
|
||||
}
|
||||
|
||||
// Yield them, and get back the ones that were found to be used
|
||||
// TODO - if it's easy for the consumer, we can just accept the last one,
|
||||
// or even better, its index within `batchKeys`
|
||||
const usedBatchKeys = yield batchKeys
|
||||
if (usedBatchKeys === undefined) {
|
||||
throw "generateKeyBatches iteration needs an array of used batch keys"
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
getAddresses(wallet: any): string[] {
|
||||
return wallet.accounts
|
||||
// won't venture into deterministic yet
|
||||
.filter((account: any) => account.address_generator.name === 'single-address')
|
||||
.map((account: any) => {
|
||||
let node: BIP32Interface = bip32.fromBase58(account.private_key);
|
||||
return this.getAddressFromBip32(node)
|
||||
/*
|
||||
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 {
|
||||
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 psbt = lbry.Psbt.fromHex(psbtHex)
|
||||
|
||||
|
@ -75,39 +424,29 @@ export class SigningService {
|
|||
}
|
||||
|
||||
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 encodedPrivateKey = keyEncoder.encodePrivate(seedHex, 'raw', 'pem');
|
||||
return jsonwebtoken.sign({ }, encodedPrivateKey, { algorithm: 'ES256', expiresIn: 60 * 10 });
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
/*
|
||||
const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex);
|
||||
|
||||
const actionBytes = new Buffer(actionHex, 'hex');
|
||||
const actionHash = new Buffer(sha256.x2(actionBytes), 'hex');
|
||||
const signature = privateKey.sign(actionHash);
|
||||
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 {IdentityService} from '../identity.service';
|
||||
import {CryptoService} from '../crypto.service';
|
||||
import { GlobalVarsService } from '../global-vars.service';
|
||||
import {GlobalVarsService} from '../global-vars.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-test-sign-transaction',
|
||||
|
@ -14,9 +14,9 @@ export class TestSignTransactionComponent implements OnInit {
|
|||
signedTransactionHex?: string
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private signingService: SigningService,
|
||||
private identityService: IdentityService,
|
||||
private cryptoService: CryptoService,
|
||||
private globalVars: GlobalVarsService,
|
||||
) { }
|
||||
|
||||
|
@ -26,7 +26,7 @@ export class TestSignTransactionComponent implements OnInit {
|
|||
const fromAddress = params.get("fromAddress") || ""
|
||||
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)
|
||||
|
||||
// TODO what if error? etc etc.
|
||||
|
@ -39,7 +39,7 @@ export class TestSignTransactionComponent implements OnInit {
|
|||
|
||||
finishFlow(signedTransactionHex?: string): void {
|
||||
this.identityService.login({
|
||||
users: {}, // TODO sigh
|
||||
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||
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 {IdentityService} from '../identity.service';
|
||||
|
||||
|
@ -18,6 +20,8 @@ export class TestSignComponent implements OnInit {
|
|||
privateKeyString: string = "thhUUVXQtyxonMaezCKwihLw9tZUrGJgWBMxDNfxoWub8dLGA"
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private globalVars: GlobalVarsService,
|
||||
private signingService: SigningService,
|
||||
private identityService: IdentityService,
|
||||
) { }
|
||||
|
@ -34,7 +38,7 @@ export class TestSignComponent implements OnInit {
|
|||
|
||||
finishFlow(signatureHex?: string): void {
|
||||
this.identityService.login({
|
||||
users: {}, // TODO sigh
|
||||
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||
signatureHex,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export const environment = {
|
||||
production: true,
|
||||
hostname: 'identity.deso.org',
|
||||
nodeHostname: 'node.deso.org',
|
||||
hostname: 'localhost:4201',
|
||||
nodeHostname: 'node.deso.org', // TODO deleteme
|
||||
};
|
||||
|
|
|
@ -4,8 +4,20 @@
|
|||
|
||||
export const environment = {
|
||||
production: false,
|
||||
hostname: 'identity.deso.org',
|
||||
nodeHostname: 'node.deso.org',
|
||||
hostname: 'localhost:4201',
|
||||
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 {
|
||||
seedHex: string;
|
||||
mnemonic: string;
|
||||
extraText: string;
|
||||
network: Network;
|
||||
}
|
||||
|
||||
export interface PublicUserInfo {
|
||||
hasExtraText: boolean;
|
||||
encryptedSeedHex: string;
|
||||
network: Network;
|
||||
accessLevel: AccessLevel;
|
||||
accessLevelHmac: string;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
username: string;
|
||||
// TODO: Use our wallet json-schema. Otherwise, bitcoinjs-lib probably already has a networks enum.
|
||||
// TODO: what about encrypted wallets?
|
||||
|
||||
export enum AddressType {
|
||||
DeterministicChain = "deterministic-chain",
|
||||
SingleAddress = "single-address",
|
||||
}
|
||||
|
||||
// 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 {
|
||||
mainnet = 'mainnet',
|
||||
testnet = 'testnet',
|
||||
MainNet = "lbc_mainnet",
|
||||
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 {
|
||||
|
@ -38,3 +83,9 @@ export enum AccessLevel {
|
|||
// Node can sign all transactions without approval
|
||||
Full = 4,
|
||||
}
|
||||
|
||||
export enum ActionType {
|
||||
Action = 0,
|
||||
Transaction = 1,
|
||||
// TODO - probably gets a lot more detailed than this
|
||||
}
|
||||
|
|