Compare commits

...

44 commits

Author SHA1 Message Date
Daniel Krol
fd74cf1f5a WIP - Channels, spending Keys (sort of) from hub
This doesn't compile. Lots of TODO comments and such. This should be undone and force pushed before merging into master.
2022-05-24 15:39:02 -04:00
Daniel Krol
8ad085c24c Get rid of a use of a constant that I don't need.
Need to delete a use of it from one other place and I can delete it altogether.

Some comments as well on related stuff.
2022-05-23 18:02:42 -04:00
Daniel Krol
f841c897dc Comment and cleanup TODO 2022-05-23 17:10:18 -04:00
Daniel Krol
1c8fc79f08 Comment about code cleanup later 2022-05-23 16:56:13 -04:00
Daniel Krol
1dcb1d36a1 Better localStorage format description 2022-05-23 16:25:30 -04:00
Daniel Krol
30c17f3bdb Upgrade bitcoinjs-lib (lbry fork) package 2022-05-23 14:55:30 -04:00
Daniel Krol
e7c61254a5 Choose a random hub 2022-05-10 17:36:30 -04:00
Daniel Krol
5acc79d212 Clean up app login page. Remove unused global var. 2022-05-10 16:06:18 -04:00
Daniel Krol
a913d45060 Missing imports and environment var for hub 2022-05-10 15:24:54 -04:00
Daniel Krol
14e9f4d925 Forgot one for accounts->channel update 2022-05-10 15:23:08 -04:00
Daniel Krol
6bbd324388 Uninstall ngx-cookie and ngx-intl-tel-input
Unused anymore
2022-05-10 15:13:54 -04:00
Daniel Krol
d0b197c5b0 Begin to get Hub service ready for http connections 2022-05-10 14:59:39 -04:00
Daniel Krol
1c7854b9ec Switch to sending Channel info to apps
Instead of account info. Stubbed out the hub work. Haven't made the example app accommodate it. Plenty left to do, but this lays down the order of things.
2022-05-10 12:57:11 -04:00
Daniel Krol
d0583e540a getPublicAccounts -> getChannels
Login won't give LBRY accounts, it'll give channels
2022-05-05 19:49:29 -04:00
Daniel Krol
439ce26e0f Renamed components for clarity
test-lbry-log-in -> log-in-wallet
log-in -> log-in-app
2022-05-05 16:55:00 -04:00
Daniel Krol
65d675041a WIP for the new "log in" page 2022-05-05 16:08:59 -04:00
Daniel Krol
4e891a5763 Comment and throw in weird place in code 2022-05-05 15:11:41 -04:00
Daniel Krol
c69894806c Update but also disable sign-up component.
We're not ready yet and certainly not using DeSo's.
2022-05-05 13:53:00 -04:00
Daniel Krol
340e6ccaa3 Rename a method and make it private. Comment it out because we don't need it yet. 2022-05-05 13:49:22 -04:00
Daniel Krol
b18af383be Add LBRY to LICENSE notice 2022-05-05 13:45:55 -04:00
Daniel Krol
aee6b977d7 TODO - example READMEs 2022-05-05 13:39:25 -04:00
Daniel Krol
205411adda A bit of half-backed wallet sync stuff, but commented out. 2022-05-05 13:30:07 -04:00
Daniel Krol
dadeb066c8 Some comments 2022-05-04 19:44:16 -04:00
Daniel Krol
970c0e68e7 Remove some stuff we wont need in signing and identity service 2022-05-04 19:13:44 -04:00
Daniel Krol
a761ba1a06 Remove jwtPost from backend-api in identity service 2022-05-04 18:14:46 -04:00
Daniel Krol
95d783ffc1 Remove protractor (testing) related stuff we're most likely not going to use 2022-05-04 17:34:54 -04:00
Daniel Krol
a3023dc40c Very WIP changes from DeSo structs to LBRY structs 2022-05-04 15:58:21 -04:00
Daniel Krol
6b6efe6ce0 Disable DeSo's approve component. TODOs to put in our own txn signing stuff 2022-05-04 15:23:14 -04:00
Daniel Krol
5c8d42b2b1 Get rid of PUBLIC_KEY_PREFIXES
I think we have all we need in bitcoinjs-lib
2022-05-04 15:09:00 -04:00
Daniel Krol
ee4db066a0 Remove load-seed component
We won't be "loading a seed". We'll be doing a wallet service login in the long run, pasting a wallet in the interim.
2022-05-04 14:04:10 -04:00
Daniel Krol
eecf21aec4 Get rid of some functions we won't need soon (this won't compile)
This and the next few commits won't be clean; I made a bunch of changes and I need to start committing in chunks.
2022-05-04 12:05:41 -04:00
Daniel Krol
2e84e0ff40 Remove parameter we don't care about. 2022-04-09 19:18:27 -04:00
Daniel Krol
a75c7b22a1 Get rid of a few more fields 2022-04-09 19:06:29 -04:00
Daniel Krol
51e089f9a9 Don't need UserProfile struct anymore. We'll make a new one if this separation matters again. 2022-04-09 19:04:23 -04:00
Daniel Krol
44f65ab93e Space 2022-04-09 18:00:03 -04:00
Daniel Krol
b89ff006ce Remove DeSo's GetTransactionSpending. LBRY will get this info from parsing psbts. 2022-04-09 17:38:19 -04:00
Daniel Krol
8441a49232 Don't need stringifyError 2022-04-09 16:48:46 -04:00
Daniel Krol
8b28bd35ff Delete a bunhc of environment vars we don't care about. 2022-04-09 16:07:30 -04:00
Daniel Krol
a9b3a683c4 uninstall amplitude-js 2022-04-09 16:04:23 -04:00
Daniel Krol
0c76472093 Delete extra LICENSE. It's the same entity and year as the other one anyway anyway. 2022-04-09 15:41:06 -04:00
Daniel Krol
14d36075fc Delete a bunch of angular/deso stuff we won't need post-angular 2022-04-09 15:39:29 -04:00
Daniel Krol
7cd0eee93b Remove reference to bitclout and a couple other things 2022-04-09 15:14:15 -04:00
Daniel Krol
b7fc4f9e8e Some hostnames in environment 2022-04-09 14:51:29 -04:00
Daniel Krol
af4bbc9949 package-lock.json reference reformat 2022-04-08 20:35:46 -04:00
81 changed files with 1285 additions and 1751 deletions

View file

@ -1,2 +0,0 @@
# For now, tag everyone who wants to participate
* @deso-protocol/reviewers

View file

@ -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;"

View file

@ -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"]

View file

@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2021 DeSo Community Developers Copyright (c) 2021 DeSo Community Developers
Copyright (c) 2022 LBRY
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -88,23 +88,6 @@
"browserTarget": "identity:build" "browserTarget": "identity:build"
} }
}, },
"test": {
"builder": "@angular-bulders/custom-webpack:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
},
"lint": { "lint": {
"builder": "@angular-devkit/build-angular:tslint", "builder": "@angular-devkit/build-angular:tslint",
"options": { "options": {

View file

@ -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
}
}));
}
};

View file

@ -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));
});
});

View file

@ -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();
}
}

View file

@ -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"
]
}
}

View file

@ -0,0 +1 @@
TODO: explain config.go

View file

@ -1,2 +0,0 @@
# For now, tag everyone who wants to participate
* @deso-protocol/reviewers

View file

@ -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';"

View file

@ -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"]

View file

@ -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.

View file

@ -1,34 +1 @@
![DeSo Logo](src/assets/deso/camelcase_logo.svg) TODO
# About DeSo
DeSo is a blockchain built from the ground up to support a fully-featured
social network. Its architecture is similar to Bitcoin, only it supports complex
social network data like profiles, posts, follows, creator coin transactions, and
more.
[Read about the vision](https://docs.deso.org/#the-ultimate-vision)
# About This Repo
Documentation for this repo lives on docs.deso.org. Specifically, the following
docs should give you everything you need to get started:
* [DeSo Code Walkthrough](https://docs.deso.org/code/walkthrough)
* [Setting Up Your Dev Environment](https://docs.deso.org/code/dev-setup)
* [Making Your First Changes](https://docs.deso.org/code/making-your-first-changes)
# Start Coding
The quickest way to contribute changes to the BitClout Frontend is the following these steps:
1. Open frontend repo in Gitpod
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/deso-protocol/frontend)
You can use any repo / branch URL and just prepend `https://gitpod.io/#` to it.
2. If needed, login to your github account
3. Set the correct `lastLocalNodeV2` to `"https://api.tijn.club"` in your browser Local Storage for the gitpod preview URL
4. Create a new branch to start working
To commit / submit a pull reqest from gitpod, you will need to give gitpod additional permissions to your github account: `public_repo, read:org, read:user, repo, user:email, workflow` which you can do on the [GitPod Integrations page](https://gitpod.io/integrations).

View file

@ -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": {}
}
]
}

View file

@ -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 } }));
}
};

View file

@ -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)
);
});
});

View file

@ -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>;
}
}

View file

@ -1,13 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

View file

@ -20,7 +20,6 @@
"@ethereumjs/common": "^2.5.0", "@ethereumjs/common": "^2.5.0",
"@ethereumjs/tx": "^3.3.2", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.3", "@ethereumjs/vm": "^5.5.3",
"amplitude-js": "^7.4.3",
"assert": "^2.0.0", "assert": "^2.0.0",
"autolinker": "^3.14.2", "autolinker": "^3.14.2",
"bs58": "^4.0.1", "bs58": "^4.0.1",
@ -101,39 +100,6 @@
"webpack-cli": "^3.3.10" "webpack-cli": "^3.3.10"
} }
}, },
"node_modules/@amplitude/types": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.5.0.tgz",
"integrity": "sha512-XspuOsUzUcxwAptHeGiIn4giuLWs285xTJa7h8kAEEynxtEI3/krWCoDYZSB9PekaPXB6phxiO/tMd9t5V9LgQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/@amplitude/ua-parser-js": {
"version": "0.7.24",
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.24.tgz",
"integrity": "sha512-VbQuJymJ20WEw0HtI2np7EdC3NJGUWi8+Xdbc7uk8WfMIF308T0howpzkQ3JFMN7ejnrcSM/OyNGveeE3TP3TA==",
"engines": {
"node": "*"
}
},
"node_modules/@amplitude/utils": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.5.0.tgz",
"integrity": "sha512-1DrDJkb4dVX+FiBXhGpO2Dn2cRKdP+gtrVR8vZcE8wz/V2XxUI3DDx7uQbIS6WbQf6swv6Uo2eMHYtrwebostw==",
"dependencies": {
"@amplitude/types": "^1.5.0",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@amplitude/utils/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@angular-devkit/architect": { "node_modules/@angular-devkit/architect": {
"version": "0.1002.1", "version": "0.1002.1",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz",
@ -5713,17 +5679,6 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
"dev": true "dev": true
}, },
"node_modules/amplitude-js": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.4.3.tgz",
"integrity": "sha512-ea42XGJ/4MwqtJ/OZX5uZf2VsNePONUNTJoEaaTxjCwFGk5sR5bOh9W60puMuL1HQ3YSy6lFBdrcY4wzaCl7PA==",
"dependencies": {
"@amplitude/ua-parser-js": "0.7.24",
"@amplitude/utils": "^1.0.5",
"blueimp-md5": "^2.10.0",
"query-string": "5"
}
},
"node_modules/ansi-colors": { "node_modules/ansi-colors": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
@ -6691,11 +6646,6 @@
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
}, },
"node_modules/blueimp-md5": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.18.0.tgz",
"integrity": "sha512-vE52okJvzsVWhcgUHOv+69OG3Mdg151xyn41aVQN/5W5S+S43qZhxECtYLAEHMSFWX6Mv5IZrzj3T5+JqXfj5Q=="
},
"node_modules/bn.js": { "node_modules/bn.js": {
"version": "4.11.9", "version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
@ -27902,32 +27852,6 @@
} }
}, },
"dependencies": { "dependencies": {
"@amplitude/types": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.5.0.tgz",
"integrity": "sha512-XspuOsUzUcxwAptHeGiIn4giuLWs285xTJa7h8kAEEynxtEI3/krWCoDYZSB9PekaPXB6phxiO/tMd9t5V9LgQ=="
},
"@amplitude/ua-parser-js": {
"version": "0.7.24",
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.24.tgz",
"integrity": "sha512-VbQuJymJ20WEw0HtI2np7EdC3NJGUWi8+Xdbc7uk8WfMIF308T0howpzkQ3JFMN7ejnrcSM/OyNGveeE3TP3TA=="
},
"@amplitude/utils": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.5.0.tgz",
"integrity": "sha512-1DrDJkb4dVX+FiBXhGpO2Dn2cRKdP+gtrVR8vZcE8wz/V2XxUI3DDx7uQbIS6WbQf6swv6Uo2eMHYtrwebostw==",
"requires": {
"@amplitude/types": "^1.5.0",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@angular-devkit/architect": { "@angular-devkit/architect": {
"version": "0.1002.1", "version": "0.1002.1",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1002.1.tgz",
@ -32184,17 +32108,6 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
"dev": true "dev": true
}, },
"amplitude-js": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.4.3.tgz",
"integrity": "sha512-ea42XGJ/4MwqtJ/OZX5uZf2VsNePONUNTJoEaaTxjCwFGk5sR5bOh9W60puMuL1HQ3YSy6lFBdrcY4wzaCl7PA==",
"requires": {
"@amplitude/ua-parser-js": "0.7.24",
"@amplitude/utils": "^1.0.5",
"blueimp-md5": "^2.10.0",
"query-string": "5"
}
},
"ansi-colors": { "ansi-colors": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
@ -32928,11 +32841,6 @@
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
}, },
"blueimp-md5": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.18.0.tgz",
"integrity": "sha512-vE52okJvzsVWhcgUHOv+69OG3Mdg151xyn41aVQN/5W5S+S43qZhxECtYLAEHMSFWX6Mv5IZrzj3T5+JqXfj5Q=="
},
"bn.js": { "bn.js": {
"version": "4.11.9", "version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",

View file

@ -29,7 +29,6 @@
"@ethereumjs/common": "^2.5.0", "@ethereumjs/common": "^2.5.0",
"@ethereumjs/tx": "^3.3.2", "@ethereumjs/tx": "^3.3.2",
"@ethereumjs/vm": "^5.5.3", "@ethereumjs/vm": "^5.5.3",
"amplitude-js": "^7.4.3",
"assert": "^2.0.0", "assert": "^2.0.0",
"autolinker": "^3.14.2", "autolinker": "^3.14.2",
"bs58": "^4.0.1", "bs58": "^4.0.1",

View file

@ -1,5 +0,0 @@
#!/bin/sh
echo "Loading Caddy config from file: ${CADDY_FILE}"
caddy run --config ${CADDY_FILE}

View file

@ -174,6 +174,8 @@ export class BackendApiService {
} }
jwtPost(endpoint: string, path: string, publicKey: string, body: any): Observable<any> { jwtPost(endpoint: string, path: string, publicKey: string, body: any): Observable<any> {
// TODO - we may not need this method
// see notes in: (identity) src/app/identity.service.ts
const request = this.identityService.jwt({ const request = this.identityService.jwt({
...this.identityService.identityServiceParamsForKey(publicKey), ...this.identityService.identityServiceParamsForKey(publicKey),
}); });

View file

@ -73,7 +73,7 @@ export class GlobalVarsService {
testLoginLBRY() : Observable<string[]> { testLoginLBRY() : Observable<string[]> {
return new Observable(subscriber => { return new Observable(subscriber => {
this.identityService.launch("/test-lbry-log-in", {}).subscribe((res) => { this.identityService.launch("/log-in-wallet", {}).subscribe((res) => {
// TODO - maybe we want public key instead of address? we should, as DeSo did, have a list of users with everything we need from them. // TODO - maybe we want public key instead of address? we should, as DeSo did, have a list of users with everything we need from them.
subscriber.next(res.addresses) subscriber.next(res.addresses)
subscriber.complete() subscriber.complete()

View file

@ -50,7 +50,6 @@ export class IdentityService {
nonWitnessUtxoHexes?: string, nonWitnessUtxoHexes?: string,
fromAddress?: string, fromAddress?: string,
public_key?: string; public_key?: string;
accessLevelRequest?: number;
} }
): Observable<any> { ): Observable<any> {
let url = this.identityServiceURL as string; let url = this.identityServiceURL as string;
@ -92,10 +91,6 @@ export class IdentityService {
httpParams = httpParams.append("public_key", params.public_key); httpParams = httpParams.append("public_key", params.public_key);
} }
if (params?.accessLevelRequest) {
httpParams = httpParams.append("accessLevelRequest", params.accessLevelRequest.toString());
}
const paramsStr = httpParams.toString(); const paramsStr = httpParams.toString();
if (paramsStr) { if (paramsStr) {
url += `?${paramsStr}`; url += `?${paramsStr}`;

View file

@ -60,7 +60,7 @@ export class SpendLBCComponent implements OnInit {
const toAddress = (<HTMLInputElement>document.getElementById("toAddress")).value; const toAddress = (<HTMLInputElement>document.getElementById("toAddress")).value;
const desiredAmount = Number((<HTMLInputElement>document.getElementById("desiredAmount")).value); const desiredAmount = Number((<HTMLInputElement>document.getElementById("desiredAmount")).value);
this.backendApi.GetPsbt("localhost:8090", this.fromAddress, toAddress, desiredAmount).subscribe({ this.backendApi.GetPsbt(environment.backendHostname, this.fromAddress, toAddress, desiredAmount).subscribe({
next: res => { next: res => {
this.psbtHex = res.psbtHex this.psbtHex = res.psbtHex
this.nonWitnessUtxoHexes = res.nonWitnessUtxoHex this.nonWitnessUtxoHexes = res.nonWitnessUtxoHex
@ -99,14 +99,14 @@ export class SpendLBCComponent implements OnInit {
this.globalVars.testSignTransactionLBRY(this.psbtHex, this.nonWitnessUtxoHexes, this.fromAddress).subscribe({ this.globalVars.testSignTransactionLBRY(this.psbtHex, this.nonWitnessUtxoHexes, this.fromAddress).subscribe({
next: (signedTransactionHex) => { next: (signedTransactionHex) => {
this.signedTransactionHex = signedTransactionHex this.signedTransactionHex = signedTransactionHex
this.backendApi.DecodeTransaction("localhost:8090", signedTransactionHex).subscribe({ this.backendApi.DecodeTransaction(environment.backendHostname, signedTransactionHex).subscribe({
next: res => { next: res => {
this.decodedTransaction = res.decodedTransaction this.decodedTransaction = res.decodedTransaction
console.log(res.decodedTransaction) console.log(res.decodedTransaction)
}, },
error: err => { this.setError(err) } error: err => { this.setError(err) }
}) })
this.backendApi.BroadcastTransaction("localhost:8090", signedTransactionHex).subscribe({ this.backendApi.BroadcastTransaction(environment.backendHostname, signedTransactionHex).subscribe({
next: res => { next: res => {
this.setError(null) this.setError(null)
this.success = res.txid this.success = res.txid

View file

@ -2,32 +2,7 @@
"name": "deso", "name": "deso",
"short_name": "deso", "short_name": "deso",
"description": "The crypto social network", "description": "The crypto social network",
"icons": [ "icons": [],
{
"src": "/assets/bitclout/logo-192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "maskable"
},
{
"src": "/assets/bitclout/logo-512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
},
{
"src": "/assets/bitclout/logo-192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "any"
},
{
"src": "/assets/bitclout/logo-512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "any"
}
],
"start_url": "/", "start_url": "/",
"scope": "/", "scope": "/",
"display": "standalone", "display": "standalone",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View file

@ -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/",
},
};

View file

@ -1,24 +1,4 @@
export const environment = { export const environment = {
production: true, production: true,
uploadImageHostname: "node.deso.org", identityURL: "http://localhost:4201",
verificationEndpointHostname: "https://node.deso.org",
uploadVideoHostname: "node.deso.org",
identityURL: "https://identity.deso.org",
supportEmail: "",
dd: {
apiKey: "DCEB26AC8BF47F1D7B4D87440EDCA6",
jsPath: "https://bitclout.com/tags.js",
ajaxListenerPath: "bitclout.com/api",
endpoint: "https://bitclout.com/js/",
},
amplitude: {
key: "",
domain: "",
},
node: {
id: 1,
name: "DeSo",
url: "https://node.deso.org",
logoAssetDir: "/assets/deso/",
},
}; };

View file

@ -4,25 +4,6 @@
export const environment = { export const environment = {
production: false, production: false,
uploadImageHostname: "node.deso.org", backendHostname: 'localhost:8090',
verificationEndpointHostname: "https://node.deso.org", identityURL: "http://localhost:4201",
uploadVideoHostname: "node.deso.org",
identityURL: "https://identity.deso.org",
supportEmail: "",
dd: {
apiKey: "DCEB26AC8BF47F1D7B4D87440EDCA6",
jsPath: "https://bitclout.com/tags.js",
ajaxListenerPath: "bitclout.com/api",
endpoint: "https://bitclout.com/js/",
},
amplitude: {
key: "",
domain: "",
},
node: {
id: 1,
name: "DeSo",
url: "https://node.deso.org",
logoAssetDir: "/assets/deso/",
},
}; };

View file

@ -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>

View file

@ -8,16 +8,6 @@
<meta name="theme-color" content="#eeeeee" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#eeeeee" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://node.deso.org/assets/deso/camelcase_logo_og.jpg" />
<meta property="og:title" content="Welcome to DeSo" />
<meta property="og:description" content="DeSo is a platform owned by its users. Bitcoin is decentralizing money, DeSo is decentralizing social media." />
<meta property="og:site_name" content="DeSo" />
<meta property='og:image' content="https://node.deso.org/assets/deso/camelcase_logo_og.jpg" />
<meta property="og:image:secure_url" content="https://node.deso.org/assets/deso/camelcase_logo_og.jpg" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:alt" content="DeSo Logo on White Background" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://node.deso.org" />
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=0.9" /> <meta name="viewport" content="width=device-width, initial-scale=0.9" />

View file

@ -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
});
};

View file

@ -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
View file

@ -23,7 +23,7 @@
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"bip32": "^2.0.6", "bip32": "^2.0.6",
"bip39": "^3.0.3", "bip39": "^3.0.3",
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e", "bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"ecpair": "^1.0.1", "ecpair": "^1.0.1",
"elliptic": "^6.5.4", "elliptic": "^6.5.4",
@ -32,8 +32,6 @@
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"key-encoder": "^2.0.3", "key-encoder": "^2.0.3",
"ngx-bootstrap": "^6.2.0", "ngx-bootstrap": "^6.2.0",
"ngx-cookie": "^5.0.2",
"ngx-intl-tel-input": "^3.1.1",
"readable-stream": "^3.6.0", "readable-stream": "^3.6.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "~6.6.0", "rxjs": "~6.6.0",
@ -3400,8 +3398,8 @@
}, },
"node_modules/bitcoinjs-lib": { "node_modules/bitcoinjs-lib": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e", "resolved": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
"integrity": "sha512-xluOW5MDgy2GTFAW8Vg7qYAgAlOTTaFWlogg8ejNetNRSZ8tg3go9XkEHdQXFhf1mDxKl55ybvSl0ie1ApiTrA==", "integrity": "sha512-AwoS80uq7ADqozrLW3aDjSvLZpQQn7a2Jt6Qt9DOiYTcwjxWarWEt+ntkNskzk63Azs779Wb0suD6KqWbJ/U5A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bech32": "^2.0.0", "bech32": "^2.0.0",
@ -7200,15 +7198,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/google-libphonenumber": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.25.tgz",
"integrity": "sha512-M/b5mij5o2aGnbe+Id9O3847jBtP0baW61foFkevxBxbuV4LH9AcujjYLd2UVkUPKVdMpKyBZxfeNwdxqobQFQ==",
"peer": true,
"engines": {
"node": ">=0.10"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.6", "version": "4.2.6",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
@ -10228,34 +10217,6 @@
"@angular/core": ">=7.0.0" "@angular/core": ">=7.0.0"
} }
}, },
"node_modules/ngx-cookie": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/ngx-cookie/-/ngx-cookie-5.0.2.tgz",
"integrity": "sha512-auivWhAhC5bW1HssvtQild1TREHWb1JtcKO0e+VGe9T7LHrfi5w2qcP8C58ly64PT+brZHQBvT1Azb7a6goHZA==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": ">9.0.0",
"@angular/core": ">9.0.0"
}
},
"node_modules/ngx-intl-tel-input": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/ngx-intl-tel-input/-/ngx-intl-tel-input-3.1.1.tgz",
"integrity": "sha512-jk/6dBBDo9Z9QNBzFvJKQs159mZtKfC+/hF3MDoMpya8xzOz7Tbwh5qNsHaIFtzDx82Fv1Ji9+0VPWIv1P0fIQ==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": "8.x - 11.x",
"@angular/core": "8.x - 11.x",
"@angular/forms": "8.x - 11.x",
"google-libphonenumber": "^3.2.3",
"intl-tel-input": "^17.0.3",
"ngx-bootstrap": "^6.0.0"
}
},
"node_modules/nice-try": { "node_modules/nice-try": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -21472,9 +21433,9 @@
} }
}, },
"bitcoinjs-lib": { "bitcoinjs-lib": {
"version": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e", "version": "git+ssh://git@github.com/lbryio/bitcoinjs-lib.git#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
"integrity": "sha512-xluOW5MDgy2GTFAW8Vg7qYAgAlOTTaFWlogg8ejNetNRSZ8tg3go9XkEHdQXFhf1mDxKl55ybvSl0ie1ApiTrA==", "integrity": "sha512-AwoS80uq7ADqozrLW3aDjSvLZpQQn7a2Jt6Qt9DOiYTcwjxWarWEt+ntkNskzk63Azs779Wb0suD6KqWbJ/U5A==",
"from": "bitcoinjs-lib@lbryio/bitcoinjs-lib#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e", "from": "bitcoinjs-lib@github:lbryio/bitcoinjs-lib#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
"requires": { "requires": {
"bech32": "^2.0.0", "bech32": "^2.0.0",
"bip174": "^2.0.1", "bip174": "^2.0.1",
@ -24755,12 +24716,6 @@
"slash": "^3.0.0" "slash": "^3.0.0"
} }
}, },
"google-libphonenumber": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.25.tgz",
"integrity": "sha512-M/b5mij5o2aGnbe+Id9O3847jBtP0baW61foFkevxBxbuV4LH9AcujjYLd2UVkUPKVdMpKyBZxfeNwdxqobQFQ==",
"peer": true
},
"graceful-fs": { "graceful-fs": {
"version": "4.2.6", "version": "4.2.6",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
@ -27259,22 +27214,6 @@
"integrity": "sha512-5WKHo6/ltkenw4UyXZwED8rODCgp2RGbWurzYzZsF/gH1JO5SN7TJ+AL6kXYk6XM42sDA2WhN9Db+ZPNjiyHnA==", "integrity": "sha512-5WKHo6/ltkenw4UyXZwED8rODCgp2RGbWurzYzZsF/gH1JO5SN7TJ+AL6kXYk6XM42sDA2WhN9Db+ZPNjiyHnA==",
"requires": {} "requires": {}
}, },
"ngx-cookie": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/ngx-cookie/-/ngx-cookie-5.0.2.tgz",
"integrity": "sha512-auivWhAhC5bW1HssvtQild1TREHWb1JtcKO0e+VGe9T7LHrfi5w2qcP8C58ly64PT+brZHQBvT1Azb7a6goHZA==",
"requires": {
"tslib": "^2.0.0"
}
},
"ngx-intl-tel-input": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/ngx-intl-tel-input/-/ngx-intl-tel-input-3.1.1.tgz",
"integrity": "sha512-jk/6dBBDo9Z9QNBzFvJKQs159mZtKfC+/hF3MDoMpya8xzOz7Tbwh5qNsHaIFtzDx82Fv1Ji9+0VPWIv1P0fIQ==",
"requires": {
"tslib": "^2.0.0"
}
},
"nice-try": { "nice-try": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",

View file

@ -28,7 +28,7 @@
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"bip32": "^2.0.6", "bip32": "^2.0.6",
"bip39": "^3.0.3", "bip39": "^3.0.3",
"bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#a1c5104f4a892a775b2d97b9fc88a1ba5b17403e", "bitcoinjs-lib": "github:lbryio/bitcoinjs-lib#ae14ef1355d2d8bb3e9aca98a01201d279e085bf",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"ecpair": "^1.0.1", "ecpair": "^1.0.1",
"elliptic": "^6.5.4", "elliptic": "^6.5.4",
@ -37,8 +37,6 @@
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"key-encoder": "^2.0.3", "key-encoder": "^2.0.3",
"ngx-bootstrap": "^6.2.0", "ngx-bootstrap": "^6.2.0",
"ngx-cookie": "^5.0.2",
"ngx-intl-tel-input": "^3.1.1",
"readable-stream": "^3.6.0", "readable-stream": "^3.6.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "~6.6.0", "rxjs": "~6.6.0",

View file

@ -1,60 +1,228 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {CryptoService} from './crypto.service';
import {GlobalVarsService} from './global-vars.service'; import {GlobalVarsService} from './global-vars.service';
import {AccessLevel, Network, PrivateUserInfo, PublicUserInfo} from '../types/identity'; import {HubService} from './hub.service';
import HDKey from 'hdkey'; import {SigningService} from './signing.service';
import {
AccessLevel,
ActionType,
PrivateAccountInfo,
PrivateChannelInfo,
PublicChannelInfo,
} from '../types/identity';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AccountService { export class AccountService {
private static usersStorageKey = 'users'; private static walletStorageKey = 'wallet';
private static levelsStorageKey = 'levels'; private static channelsStorageKey = 'channels';
private static accessStorageKey = 'access';
private static publicKeyRegex = /^[a-zA-Z0-9]{54,55}$/;
constructor( constructor(
private cryptoService: CryptoService,
private globalVars: GlobalVarsService, private globalVars: GlobalVarsService,
private signingService: SigningService,
) { } ) { }
// Public Getters /*
getPublicKeys(): any { What we're keeping in local storage. TODO - make these into data structs.
return Object.keys(this.getPrivateUsers());
// The wallet, taken from wallet Sync. Perhaps even with local changes to be
// pushed back to sync.
localStorage["wallet"] = {
// We may later want to add signature, sync metadata, etc. Or maybe we want
// the strict string representation that came out of sync.
walletStoreVersion: 0,
wallet: "...",
} }
getEncryptedUsers(): {[key: string]: PublicUserInfo} { // Access information. For each hostname, what channel is logged in, and what
const hostname = this.globalVars.hostname; // level of permission for various actions did the user give?
const privateUsers = this.getPrivateUsers(); localStorage["access"] = {
const publicUsers: {[key: string]: PublicUserInfo} = {}; // There can be multiple hostnames
"<hostnames>": { // TODO - type Hostname
"currentChannel": "<channel-claim-id>", // TODO - type ChannelClaimID
"levels": {
// There can be multiple channels
"<channel-claim-ids>": {
// There can be multiple action types
"<action-types>": "<level>",
}
}
}
}
for (const publicKey of Object.keys(privateUsers)) { localStorage["channels"] = {
const privateUser = privateUsers[publicKey]; // There can be multiple channels
const accessLevel = this.getAccessLevel(publicKey, hostname); "<channels>": PrivateChannelInfo,
if (accessLevel === AccessLevel.None) { }
*/
private hasWallet(): boolean {
return !!localStorage.getItem(AccountService.walletStorageKey);
}
// TODO define a wallet type, and/or use a type defined by json-schema
public getWallet(): {accounts: [PrivateAccountInfo]} | null {
const walletStoreStr = localStorage.getItem(AccountService.walletStorageKey);
const walletStore = JSON.parse(walletStoreStr || 'null')
if (walletStore !== null) {
if (walletStore.walletStoreVersion === 0) {
return walletStore.wallet
}
}
return null
}
private putWallet(wallet: object | null) {
const walletStore = {
// We may later want to add signature, sync metadata, etc. Or maybe we
// want the strict string representation that came out of sync. This is
// not the version of the wallet's internal structure; that has its own
// version key
walletStoreVersion: 0,
wallet
}
localStorage.setItem(AccountService.walletStorageKey, JSON.stringify(walletStore));
}
// TODO - PrivateAccountInfo should just contain a bip32 node and an
// "address" (account ID) that it generates when reading from storage. It
// wouldn't write these values back to storage, they're a function of the
// data already there. It would clean up the code by removing the need for
// bip32FromAccount, getAddress, and getAddressFromBip32.
public getAccounts(): PrivateAccountInfo[] {
const wallet = this.getWallet()
if (wallet === null) {
return []
}
const filteredAccounts: PrivateAccountInfo[] = [];
for (const account of wallet.accounts) {
// Only include accounts from the current network
if (account.ledger !== this.globalVars.network) {
continue; continue;
} }
const encryptedSeedHex = this.cryptoService.encryptSeedHex(privateUser.seedHex, hostname); filteredAccounts.push(account);
const accessLevelHmac = this.cryptoService.accessLevelHmac(accessLevel, privateUser.seedHex); }
return filteredAccounts
publicUsers[publicKey] = {
hasExtraText: privateUser.extraText?.length > 0,
encryptedSeedHex,
network: privateUser.network,
accessLevel,
accessLevelHmac,
};
} }
return publicUsers; private clearChannels() {
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(null));
} }
getAccessLevel(publicKey: string, hostname: string): AccessLevel { // TODO - This function is async due to the http call, so now I have to
const levels = JSON.parse(localStorage.getItem(AccountService.levelsStorageKey) || '{}'); // rethink the guarantees about login state being based on the data being in
const hostMapping = levels[hostname] || {}; // localStorage.
const accessLevel = hostMapping[publicKey]; //
// TODO error handling?
public updateChannels(): Observable<null> {
// Where we accumulate the channels for all accounts through all of the
// recursions
let channels: {[key: string]: {[key: string]: PrivateChannelInfo}} = {};
// Return this so the caller can do something pending this completing
// (perhaps keep the login state orderly)
// TODO - there's got to be a better way.
return new Observable(subscriber => {
accumulateChannelsForAccounts(accounts: PrivateAccountInfo[]) {
if(!accounts.length){
// We got the channels for all accounts. Give it to the subscriber so we can add it to local storage.
subscriber.next(channels)
subscriber.complete()
return
}
this.signingService.getChannelsForAccount(accounts[0])
.pipe(
map(acountChannels => {
const accountId = this.signingService.getAddress(account)
// `acountChannels` is an array. We want the same data in an object,
// keyed by the pubKey field.
channelsByPubkey = Object.fromEntries(
acountChannels.map(channel => [channel.pubKeyId, channel])
)
channels[accountId] = channelsByPubkey
// Call again, omitting the account we just handled.
accumulateChannelsForAccount(accounts.slice(1))
})
)
}
// Kick it off with all accounts.
accumulateChannelsForAccounts(this.getAccounts())
}).pipe(
map(channels => {
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(channels));
return null
})
).subscribe() // We want to actually kick off these actions if this function is called (see pipe vs subscribe)
}
public hasChannels() {
return !!localStorage.getItem(AccountService.channelsStorageKey);
}
public getChannelsPrivate(): {[key: string]: PrivateChannelInfo} {
return JSON.parse(localStorage.getItem(AccountService.channelsStorageKey) || '{}');
}
// returns {accountId: [PublicChannelInfo]}
public getChannelsPublic(): {[key: string]: PublicChannelInfo} {
const privateChannels: {[key: string]: PrivateChannelInfo} = this.getChannelsPrivate()
const publicChannels: {[key: string]: PublicChannelInfo} = {}
for(const accountId of Object.keys(privateChannels)) {
publicChannels[accountId] = {
claimId: privateChannels[accountId].claimId,
name: privateChannels[accountId].name,
normalizedName: privateChannels[accountId].normalizedName,
pubKeyId: privateChannels[accountId].pubKeyId,
}
}
return publicChannels
}
private clearAccess() {
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(null));
}
private initAccess() {
// no currentChannel or access level for any action on any hostname
localStorage.setItem(AccountService.accessStorageKey, '{}');
}
public hasAccess() {
return !!localStorage.getItem(AccountService.accessStorageKey);
}
public getActiveChannel(hostname: string): PublicChannelInfo | null {
// TODO - and actually, this maybe only needs to happen on startup. could save in a local variable.
const channels = this.getChannelsPublic()
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
if (access[hostname]) {
const activeChannelClaimId = access[hostname].currentChannel
return channels[activeChannelClaimId]
}
return null
}
public getActiveChannelAccessLevel(hostname: string, action: ActionType): AccessLevel {
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
if (!access[hostname]) {
return AccessLevel.None
}
const activeChannelClaimId = access[hostname].currentChannel
if (!access[hostname].levels[activeChannelClaimId]) {
return AccessLevel.None
}
const accessLevel = access[hostname].levels[activeChannelClaimId][action]
if (Object.values(AccessLevel).includes(accessLevel)) { if (Object.values(AccessLevel).includes(accessLevel)) {
return accessLevel; return accessLevel;
@ -63,80 +231,63 @@ export class AccountService {
} }
} }
// Public Modifiers public setAccessLevel(hostname: string, channelClaimId: string, action: ActionType, level: AccessLevel) {
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
addUser(keychain: HDKey, mnemonic: string, extraText: string, network: Network): string { if (!(hostname in access)) {
const seedHex = this.cryptoService.keychainToSeedHex(keychain); access[hostname] = {levels: {}}
}
return this.addPrivateUser({ if (!(channelClaimId in access[hostname].levels)) {
seedHex, access[hostname].levels[channelClaimId] = {}
mnemonic, }
extraText, access[hostname].levels[channelClaimId][action] = level
network, localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
});
} }
deleteUser(publicKey: string): void { public setAccessCurrentChannel(hostname: string, channelClaimId: string) {
const privateUsers = this.getPrivateUsersRaw(); const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
if (!(hostname in access)) {
delete privateUsers[publicKey]; access[hostname] = {levels: {}}
}
this.setPrivateUsersRaw(privateUsers); access[hostname].currentChannel = channelClaimId
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
} }
setAccessLevel(publicKey: string, hostname: string, accessLevel: AccessLevel): void { public walletLogout() {
const levels = JSON.parse(localStorage.getItem(AccountService.levelsStorageKey) || '{}'); this.putWallet(null)
this.clearAccess()
levels[hostname] ||= {}; this.clearChannels()
levels[hostname][publicKey] = accessLevel;
localStorage.setItem(AccountService.levelsStorageKey, JSON.stringify(levels));
} }
// Private Getters and Modifiers public walletLogin(wallet: object | null) {
// no ambiguity from half-completed actions
// make sure we're fully logged out
this.walletLogout()
// TEMP: public for import flow this.putWallet(wallet)
public addPrivateUser(userInfo: PrivateUserInfo): string { this.initAccess()
const privateUsers = this.getPrivateUsersRaw(); this.updateChannels()
const privateKey = this.cryptoService.seedHexToPrivateKey(userInfo.seedHex);
const publicKey = this.cryptoService.privateKeyToDeSoPublicKey(privateKey, userInfo.network);
privateUsers[publicKey] = userInfo;
this.setPrivateUsersRaw(privateUsers);
return publicKey;
} }
private getPrivateUsers(): {[key: string]: PrivateUserInfo} { // TODO - delete this if I don't end up using it
const privateUsers = this.getPrivateUsersRaw(); public walletIsLoggedIn(): boolean {
const filteredPrivateUsers: {[key: string]: PrivateUserInfo} = {}; return (
// All three should be set. It means the last login was complete, and no
for (const publicKey of Object.keys(privateUsers)) { // logout was started since.
const privateUser = privateUsers[publicKey]; this.hasAccess() && this.hasChannels() && this.hasWallet()
)
// Only include users from the current network
if (privateUser.network !== this.globalVars.network) {
continue;
} }
// Get rid of some users who have invalid public keys // TODO: Do we want to save per-hostname-per-channel-per-action access levels
if (!publicKey.match(AccountService.publicKeyRegex)) { // between logins? Counterargument: users probably won't log in and out too
this.deleteUser(publicKey); // often.
continue; private appClearAccess(hostname: string): void {
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
delete access[hostname]
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
} }
filteredPrivateUsers[publicKey] = privateUser; public appLogout(hostname: string): void {
this.appClearAccess(hostname)
} }
return filteredPrivateUsers;
}
private getPrivateUsersRaw(): {[key: string]: PrivateUserInfo} {
return JSON.parse(localStorage.getItem(AccountService.usersStorageKey) || '{}');
}
private setPrivateUsersRaw(privateUsers: {[key: string]: PrivateUserInfo}): void {
localStorage.setItem(AccountService.usersStorageKey, JSON.stringify(privateUsers));
}
} }

View file

@ -1,38 +1,35 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import {TestSignComponent} from './test-sign/test-sign.component'; import {TestSignComponent} from './test-sign/test-sign.component';
import {TestLbryLogInComponent} from './test-lbry-log-in/test-lbry-log-in.component'; import {LogInWalletComponent} from './log-in-wallet/log-in-wallet.component';
import {TestSignTransactionComponent} from './test-sign-transaction/test-sign-transaction.component'; import {TestSignTransactionComponent} from './test-sign-transaction/test-sign-transaction.component';
import {EmbedComponent} from './embed/embed.component'; import {EmbedComponent} from './embed/embed.component';
import {HomeComponent} from './home/home.component'; import {HomeComponent} from './home/home.component';
import {LogoutComponent} from './logout/logout.component'; import {LogoutComponent} from './logout/logout.component';
import {SignUpComponent} from './sign-up/sign-up.component'; import {SignUpComponent} from './sign-up/sign-up.component';
import {LogInComponent} from './log-in/log-in.component'; import {LogInAppComponent} from './log-in-app/log-in-app.component';
import {ApproveComponent} from './approve/approve.component'; import {ApproveComponent} from './approve/approve.component';
import {LoadSeedComponent} from './load-seed/load-seed.component';
export class RouteNames { export class RouteNames {
public static TEST_SIGN = 'test-sign'; public static TEST_SIGN = 'test-sign';
public static TEST_LBRY_LOG_IN = 'test-lbry-log-in'; public static LOG_IN_WALLET = 'log-in-wallet';
public static TEST_SIGN_TRANSACTION = 'test-sign-transaction'; public static TEST_SIGN_TRANSACTION = 'test-sign-transaction';
public static EMBED = 'embed'; public static EMBED = 'embed';
public static LOGOUT = 'logout'; public static LOGOUT = 'logout';
public static SIGN_UP = 'sign-up'; public static SIGN_UP = 'sign-up';
public static LOG_IN = 'log-in'; public static LOG_IN_APP = 'log-in-app';
public static LOAD_SEED = 'load-seed';
public static APPROVE = 'approve'; public static APPROVE = 'approve';
} }
const routes: Routes = [ const routes: Routes = [
{ path: '', component: HomeComponent, pathMatch: 'full' }, { path: '', component: HomeComponent, pathMatch: 'full' },
{ path: RouteNames.TEST_SIGN, component: TestSignComponent, pathMatch: 'full' }, { path: RouteNames.TEST_SIGN, component: TestSignComponent, pathMatch: 'full' },
{ path: RouteNames.TEST_LBRY_LOG_IN, component: TestLbryLogInComponent, pathMatch: 'full' }, { path: RouteNames.LOG_IN_WALLET, component: LogInWalletComponent, pathMatch: 'full' },
{ path: RouteNames.TEST_SIGN_TRANSACTION, component: TestSignTransactionComponent, pathMatch: 'full' }, { path: RouteNames.TEST_SIGN_TRANSACTION, component: TestSignTransactionComponent, pathMatch: 'full' },
{ path: RouteNames.EMBED, component: EmbedComponent, pathMatch: 'full' }, { path: RouteNames.EMBED, component: EmbedComponent, pathMatch: 'full' },
{ path: RouteNames.LOGOUT, component: LogoutComponent, pathMatch: 'full' }, { path: RouteNames.LOGOUT, component: LogoutComponent, pathMatch: 'full' },
{ path: RouteNames.SIGN_UP, component: SignUpComponent, pathMatch: 'full' }, { path: RouteNames.SIGN_UP, component: SignUpComponent, pathMatch: 'full' },
{ path: RouteNames.LOG_IN, component: LogInComponent, pathMatch: 'full' }, { path: RouteNames.LOG_IN_APP, component: LogInAppComponent, pathMatch: 'full' },
{ path: RouteNames.LOAD_SEED, component: LoadSeedComponent, pathMatch: 'full' },
{ path: RouteNames.APPROVE, component: ApproveComponent, pathMatch: 'full' }, { path: RouteNames.APPROVE, component: ApproveComponent, pathMatch: 'full' },
]; ];

View file

@ -22,17 +22,12 @@ export class AppComponent implements OnInit {
// load params // load params
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const accessLevelRequest = params.get('accessLevelRequest');
if (accessLevelRequest) {
this.globalVars.accessLevelRequest = parseInt(accessLevelRequest, 10);
}
if (params.get('webview')) { if (params.get('webview')) {
this.globalVars.webview = true; this.globalVars.webview = true;
} }
if (params.get('testnet')) { if (params.get('testnet')) {
this.globalVars.network = Network.testnet; this.globalVars.network = Network.TestNet;
} }
// Callback should only be used in mobile applications, where payload is passed through URL parameters. // Callback should only be used in mobile applications, where payload is passed through URL parameters.
@ -49,6 +44,17 @@ export class AppComponent implements OnInit {
if (this.globalVars.callback) { if (this.globalVars.callback) {
// If callback is set, we won't be sending the initialize message. // If callback is set, we won't be sending the initialize message.
// TODO - Why is it being set to 'localhost'? Seems arbitrary. Seems
// like we need this set to the correct value?
//
// It could be a ui security problem. we say "`this.globalVars.hostname`
// wants to do `transaction`". If it's set to "localhost" they might get
// the wrong idea. Or maybe I have no idea what this actually means.
// Or maybe localhost is actually safe since it's unlikely enough that
// somebody would be trying to pwn themselves from localhost.
throw "figure this out or delete this code branch"
this.globalVars.hostname = 'localhost'; this.globalVars.hostname = 'localhost';
this.finishInit(); this.finishInit();
} else if (this.globalVars.webview || this.globalVars.inTab || this.globalVars.inFrame()) { } else if (this.globalVars.webview || this.globalVars.inTab || this.globalVars.inFrame()) {

View file

@ -9,22 +9,19 @@ import { EmbedComponent } from './embed/embed.component';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {IdentityService} from './identity.service'; import {IdentityService} from './identity.service';
import {CookieModule} from 'ngx-cookie';
import { LogoutComponent } from './logout/logout.component'; import { LogoutComponent } from './logout/logout.component';
import { BannerComponent } from './banner/banner.component'; import { BannerComponent } from './banner/banner.component';
import { SignUpComponent } from './sign-up/sign-up.component'; import { SignUpComponent } from './sign-up/sign-up.component';
import {AccountService} from './account.service'; import {AccountService} from './account.service';
import {EntropyService} from './entropy.service'; import {EntropyService} from './entropy.service';
import { LogInComponent } from './log-in/log-in.component'; import { LogInAppComponent } from './log-in-app/log-in-app.component';
import {HttpClientModule} from '@angular/common/http'; import {HttpClientModule} from '@angular/common/http';
import { ApproveComponent } from './approve/approve.component'; import { ApproveComponent } from './approve/approve.component';
import { LoadSeedComponent } from './load-seed/load-seed.component';
import { ErrorCallbackComponent } from './error-callback/error-callback.component'; import { ErrorCallbackComponent } from './error-callback/error-callback.component';
import { NgxIntlTelInputModule } from 'ngx-intl-tel-input';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { TestSignComponent } from './test-sign/test-sign.component'; import { TestSignComponent } from './test-sign/test-sign.component';
import { TestSignTransactionComponent } from './test-sign-transaction/test-sign-transaction.component'; import { TestSignTransactionComponent } from './test-sign-transaction/test-sign-transaction.component';
import { TestLbryLogInComponent } from './test-lbry-log-in/test-lbry-log-in.component' import { LogInWalletComponent } from './log-in-wallet/log-in-wallet.component'
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -34,13 +31,12 @@ import { TestLbryLogInComponent } from './test-lbry-log-in/test-lbry-log-in.comp
LogoutComponent, LogoutComponent,
BannerComponent, BannerComponent,
SignUpComponent, SignUpComponent,
LogInComponent, LogInAppComponent,
ApproveComponent, ApproveComponent,
LoadSeedComponent,
ErrorCallbackComponent, ErrorCallbackComponent,
TestSignComponent, TestSignComponent,
TestSignTransactionComponent, TestSignTransactionComponent,
TestLbryLogInComponent, LogInWalletComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -49,10 +45,8 @@ import { TestLbryLogInComponent } from './test-lbry-log-in/test-lbry-log-in.comp
AppRoutingModule, AppRoutingModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxIntlTelInputModule,
MatFormFieldModule, MatFormFieldModule,
MatTooltipModule, MatTooltipModule,
CookieModule.forRoot()
], ],
providers: [ providers: [
IdentityService, IdentityService,

View file

@ -9,9 +9,9 @@
{{ globalVars.hostname }} wants to {{ transactionDescription }} {{ globalVars.hostname }} wants to {{ transactionDescription }}
</p> </p>
<br/> <br/>
<div *ngIf="transactionDeSoSpent"> <div *ngIf="transactionSpent">
<p class="pb-15px"> <p class="pb-15px">
Total Cost: {{ transactionDeSoSpent }} $DESO Total Cost: {{ transactionSpent }} $DESO
</p> </p>
</div> </div>
<div class="d-flex align-items-end justify-content-between"> <div class="d-flex align-items-end justify-content-between">

View file

@ -1,10 +1,9 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {CryptoService} from '../crypto.service';
import {IdentityService} from '../identity.service'; import {IdentityService} from '../identity.service';
import {AccountService} from '../account.service'; import {AccountService} from '../account.service';
import {GlobalVarsService} from '../global-vars.service'; import {GlobalVarsService} from '../global-vars.service';
import {SigningService} from '../signing.service'; // import {SigningService} from '../signing.service';
import {BackendAPIService, User} from '../backend-api.service'; import {BackendAPIService, User} from '../backend-api.service';
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
@ -33,7 +32,6 @@ import {
TransactionMetadataDAOCoin, TransactionMetadataDAOCoin,
TransactionMetadataTransferDAOCoin TransactionMetadataTransferDAOCoin
} from '../../lib/deso/transaction'; } from '../../lib/deso/transaction';
import bs58check from 'bs58check';
@Component({ @Component({
selector: 'app-approve', selector: 'app-approve',
@ -46,29 +44,32 @@ export class ApproveComponent implements OnInit {
transactionHex: any; transactionHex: any;
username: any; username: any;
transactionDescription: any; transactionDescription: any;
transactionDeSoSpent: string | boolean = false; transactionSpent: string | boolean = false;
constructor( constructor(
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private cryptoService: CryptoService,
private identityService: IdentityService, private identityService: IdentityService,
private accountService: AccountService, private accountService: AccountService,
public globalVars: GlobalVarsService, public globalVars: GlobalVarsService,
private signingService: SigningService, // private signingService: SigningService,
private backendApi: BackendAPIService, private backendApi: BackendAPIService,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.activatedRoute.queryParams.subscribe(params => { this.activatedRoute.queryParams.subscribe(params => {
this.transactionHex = params.tx; this.transactionHex = params.tx;
this.backendApi.GetTransactionSpending(this.transactionHex).subscribe( res => {
this.transactionDeSoSpent = res ? this.nanosToUnitString(res) : false; // TODO - for LBRY
}); this.transactionSpent = false;
const txBytes = new Buffer(this.transactionHex, 'hex'); const txBytes = new Buffer(this.transactionHex, 'hex');
this.transaction = Transaction.fromBytes(txBytes)[0]; this.transaction = Transaction.fromBytes(txBytes)[0];
this.publicKey = this.base58KeyCheck(this.transaction.publicKey); this.publicKey = this.base58KeyCheck(this.transaction.publicKey);
this.generateTransactionDescription(); this.generateTransactionDescription();
/*
TODO this.accountService.getActiveChannelAccessLevel
*/
}); });
} }
@ -77,22 +78,26 @@ export class ApproveComponent implements OnInit {
} }
onSubmit(): void { onSubmit(): void {
const signedTransactionHex = this.signingService.signTransaction(this.seedHex(), this.transactionHex); // TODO
throw "replace all of this transaction parsing and checking with bitcoinjs-lib."
/*
const seedHex = ""
const signedTransactionHex = this.signingService.signTransaction(seedHex, this.transactionHex);
this.finishFlow(signedTransactionHex); this.finishFlow(signedTransactionHex);
*/
/*
TODO this.accountService.setAccessLevel if people want to keep allowing the action
*/
} }
finishFlow(signedTransactionHex?: string): void { finishFlow(signedTransactionHex?: string): void {
this.identityService.login({ this.identityService.login({
users: this.accountService.getEncryptedUsers(), channel: this.accountService.getActiveChannel(this.globalVars.hostname),
signedTransactionHex, signedTransactionHex,
}); });
} }
seedHex(): string {
const encryptedSeedHex = this.accountService.getEncryptedUsers()[this.publicKey].encryptedSeedHex;
return this.cryptoService.decryptSeedHex(encryptedSeedHex, this.globalVars.hostname);
}
generateTransactionDescription(): void { generateTransactionDescription(): void {
let description = 'sign an unknown transaction'; let description = 'sign an unknown transaction';
let publicKeys: string[] = []; let publicKeys: string[] = [];
@ -252,8 +257,13 @@ export class ApproveComponent implements OnInit {
} }
base58KeyCheck(keyBytes: Uint8Array): string { base58KeyCheck(keyBytes: Uint8Array): string {
const prefix = CryptoService.PUBLIC_KEY_PREFIXES[this.globalVars.network].deso; // TODO
return bs58check.encode(Buffer.from([...prefix, ...keyBytes])); throw "replace all of this transaction parsing and checking with bitcoinjs-lib."
return ""
// TODO Don't use this.globalVars.network here, use the network specified
// in the relevant account.ledger (assuming we even really need network)
// const prefix = CryptoService.PUBLIC_KEY_PREFIXES[this.globalVars.network as Network].deso;
// return bs58check.encode(Buffer.from([...prefix, ...keyBytes]));
} }
hexNanosToUnitString(nanos: Buffer): string { hexNanosToUnitString(nanos: Buffer): string {
@ -273,7 +283,7 @@ export class ApproveComponent implements OnInit {
return of(description); return of(description);
} }
// Otherwise, we hit get-users-stateless to fetch profiles. // Otherwise, we hit get-users-stateless to fetch profiles.
return this.backendApi.GetUsersStateless(publicKeys, true).pipe((map(res => { return this.backendApi.GetUsersStateless(publicKeys).pipe((map(res => {
const userList = res.UserList; const userList = res.UserList;
// If the response has no users, return the description as is. // If the response has no users, return the description as is.
if (userList.length === 0) { if (userList.length === 0) {

View file

@ -3,17 +3,10 @@ import {HttpClient} from '@angular/common/http';
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {catchError, map} from 'rxjs/operators'; import {catchError, map} from 'rxjs/operators';
import {environment} from '../environments/environment'; import {environment} from '../environments/environment';
import {SigningService} from './signing.service';
import {AccountService} from './account.service'; import {AccountService} from './account.service';
import {CryptoService} from './crypto.service';
import {GlobalVarsService} from './global-vars.service';
import {UserProfile} from '../types/identity';
export class ProfileEntryResponse { export class ProfileEntryResponse {
Username: string | null = null; Username: string | null = null;
Description: string | null = null;
ProfilePic?: string;
PublicKeyBase58Check?: string;
} }
export class User { export class User {
@ -25,98 +18,83 @@ export class User {
providedIn: 'root' providedIn: 'root'
}) })
export class BackendAPIService { export class BackendAPIService {
walletSyncEndpoint = `https://${environment.walletSyncHostname}/api/v0`;
endpoint = `https://${environment.nodeHostname}/api/v0`; endpoint = `https://${environment.nodeHostname}/api/v0`;
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private cryptoService: CryptoService,
private signingService: SigningService,
private accountService: AccountService, private accountService: AccountService,
private globalVars: GlobalVarsService,
) { } ) { }
post(path: string, body: any): Observable<any> { post(path: string, body: any): Observable<any> {
return this.httpClient.post<any>(`${this.endpoint}/${path}`, body); return this.httpClient.post<any>(`${this.endpoint}/${path}`, body);
} }
jwtPost(path: string, publicKey: string, body: any): Observable<any> {
const publicUserInfo = this.accountService.getEncryptedUsers()[publicKey];
if (!publicUserInfo) {
return of(null);
}
const seedHex = this.cryptoService.decryptSeedHex(publicUserInfo.encryptedSeedHex, this.globalVars.hostname);
const jwt = this.signingService.signJWT(seedHex);
return this.post(path, {...body, ...{JWT: jwt}});
}
// Error parsing
stringifyError(err: any): string {
return err?.error?.error || JSON.stringify(err);
}
// When SkipForLeaderboard is true, this endpoint only returns ProfileEntryResponse, IsGraylisted, IsBlacklisted,
// IsAdmin, and IsSuperAdmin for each user.
// When SkipForLeaderboard is false, we also fetch the user's balance, profiles this user follows, hodlings, and
// UserMetadata. Oftentimes, this information is not needed and excluding it significantly improves performance.
GetUsersStateless( GetUsersStateless(
publicKeys: string[], SkipForLeaderboard: boolean = false, publicKeys: string[]
): Observable<{ UserList: User[]}> { ): Observable<{ UserList: User[]}> {
return this.httpClient.post<any>( return this.httpClient.post<any>(
`${this.endpoint}/get-users-stateless`, `${this.endpoint}/get-users-stateless`,
{ {
PublicKeysBase58Check: publicKeys, PublicKeysBase58Check: publicKeys,
SkipForLeaderboard,
}, },
); );
} }
GetUserProfiles( GetUsernames(
publicKeys: string[] publicKeys: string[]
): Observable<{[key: string]: UserProfile}> { ): Observable<{[key: string]: string}> {
const userProfiles: {[key: string]: any} = {}; const usernames: {[key: string]: any} = {};
const req = this.GetUsersStateless(publicKeys, true); const req = this.GetUsersStateless(publicKeys);
if (publicKeys.length > 0) { if (publicKeys.length > 0) {
return req.pipe( return req.pipe(
map( res => { map( res => {
for (const user of res.UserList) { for (const user of res.UserList) {
userProfiles[user.PublicKeyBase58Check] = { usernames[user.PublicKeyBase58Check] = user.ProfileEntryResponse?.Username
username: user.ProfileEntryResponse?.Username,
};
} }
return userProfiles; return usernames;
}) })
).pipe( ).pipe(
catchError(() => { catchError(() => {
for(const publicKey of publicKeys) { for(const publicKey of publicKeys) {
userProfiles[publicKey] = {}; usernames[publicKey] = "";
} }
return of(userProfiles); return of(usernames);
}) })
); );
} else { } else {
return of(userProfiles); return of(usernames);
} }
} }
GetTransactionSpending( // TODO - WIP, not using for now.
transactionHex: string
): Observable<number> { // This isn't what the current wallet sync API looks like, but it will
const req = this.httpClient.post<any>( // likely change anyway. So this is an approximation with a stub for the time being.
`${this.endpoint}/get-transaction-spending`, WalletSyncLogin(
username: string,
password: string,
): Observable<{bodyJson: string, signature: string} | null> {
// A stub for now
const wallet : object | null = this.accountService.getWallet();
if (wallet === null) {
return of(null)
}
return of({
bodyJson: JSON.stringify(wallet, null, 2),
signature: "",
})
// Later we'll do something like this...
return this.httpClient.post<any>(
`${this.walletSyncEndpoint}/log-in`,
{ {
TransactionHex: transactionHex, username: username,
password: password,
}, },
); );
return req.pipe(
map( res => {
return res.TotalSpendingNanos as number;
})
).pipe(
catchError(() => {
return of(0);
})
);
} }
} }

View file

@ -1,113 +1,32 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import HDNode from 'hdkey';
import * as bip39 from 'bip39';
import HDKey from 'hdkey';
import {ec as EC} from 'elliptic'; import {ec as EC} from 'elliptic';
import bs58check from 'bs58check'; import bs58check from 'bs58check';
import {CookieService} from 'ngx-cookie';
import {createHmac, createCipher, createDecipher, randomBytes} from 'crypto'; import {createHmac, createCipher, createDecipher, randomBytes} from 'crypto';
import {AccessLevel, Network} from '../types/identity'; import {AccessLevel} from '../types/identity';
import { GlobalVarsService } from './global-vars.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class CryptoService { export class CryptoService {
constructor( constructor() {}
private cookieService: CookieService,
private globalVars: GlobalVarsService
) {}
// TODO - LBRY?
static PUBLIC_KEY_PREFIXES = {
mainnet: {
deso: [0xcd, 0x14, 0x0],
},
testnet: {
deso: [0x11, 0xc2, 0x0],
}
};
// Safari only lets us store things in cookies
mustUseStorageAccess(): boolean {
// Webviews have full control over storage access
if (this.globalVars.webview) {
return false;
}
const supportsStorageAccess = typeof document.hasStorageAccess === 'function';
const isChrome = navigator.userAgent.indexOf('Chrome') > -1;
const isSafari = !isChrome && navigator.userAgent.indexOf('Safari') > -1;
// Firefox and Edge support the storage access API but do not enforce it.
// For now, only use cookies if we support storage access and use Safari.
const mustUseStorageAccess = supportsStorageAccess && isSafari;
return mustUseStorageAccess;
}
// 32 bytes = 256 bits is plenty of entropy for encryption // 32 bytes = 256 bits is plenty of entropy for encryption
newEncryptionKey(): string { newEncryptionKey(): string {
return randomBytes(32).toString('hex'); return randomBytes(32).toString('hex');
} }
// TODO we won't need this soon right?
seedHexEncryptionStorageKey(hostname: string): string { seedHexEncryptionStorageKey(hostname: string): string {
return `seed-hex-key-${hostname}`; return `seed-hex-key-${hostname}`;
} }
// Alternate plan is to use this same system, but instead of encrypting the
// seed and sending it back and forth, we do the wallet. It may be superior
// because at most times the decrypted wallet is not accessible anywhere
// without sending a message between the app and identity service. But we'd
// have to trust that we'll never accidentally send the wallet to the app
// unencrypted.
walletStorageKey(hostname: string): string {
return `wallet-key-${hostname}`;
}
hasWallet(hostname: string): boolean {
const storageKey = this.walletStorageKey(hostname);
if (this.mustUseStorageAccess()) {
return !!this.cookieService.get(storageKey);
} else {
return !!localStorage.getItem(storageKey);
}
}
hasSeedHexEncryptionKey(hostname: string): boolean { hasSeedHexEncryptionKey(hostname: string): boolean {
const storageKey = this.seedHexEncryptionStorageKey(hostname); const storageKey = this.seedHexEncryptionStorageKey(hostname);
if (this.mustUseStorageAccess()) {
return !!this.cookieService.get(storageKey);
} else {
return !!localStorage.getItem(storageKey); return !!localStorage.getItem(storageKey);
} }
}
getWallet(hostname: string): object | null {
const storageKey = this.walletStorageKey(hostname);
let walletStr
if (this.mustUseStorageAccess()) {
walletStr = this.cookieService.get(storageKey);
} else {
walletStr = localStorage.getItem(storageKey);
}
return JSON.parse(walletStr || 'null')
}
putWallet(hostname: string, wallet: object | null) {
const storageKey = this.walletStorageKey(hostname);
if (this.mustUseStorageAccess()) {
this.cookieService.put(storageKey, JSON.stringify(wallet), {
expires: new Date('2100/01/01 00:00:00'),
});
} else {
localStorage.setItem(storageKey, JSON.stringify(wallet));
}
}
// Place a seed encryption key in storage. If reset is set to true, the // Place a seed encryption key in storage. If reset is set to true, the
// previous key is overwritten, which is useful in logging out users. // previous key is overwritten, which is useful in logging out users.
@ -115,21 +34,11 @@ export class CryptoService {
const storageKey = this.seedHexEncryptionStorageKey(hostname); const storageKey = this.seedHexEncryptionStorageKey(hostname);
let encryptionKey; let encryptionKey;
if (this.mustUseStorageAccess()) {
encryptionKey = this.cookieService.get(storageKey);
if (!encryptionKey || reset) {
encryptionKey = this.newEncryptionKey();
this.cookieService.put(storageKey, encryptionKey, {
expires: new Date('2100/01/01 00:00:00'),
});
}
} else {
encryptionKey = localStorage.getItem(storageKey) || ''; encryptionKey = localStorage.getItem(storageKey) || '';
if (!encryptionKey || reset) { if (!encryptionKey || reset) {
encryptionKey = this.newEncryptionKey(); encryptionKey = this.newEncryptionKey();
localStorage.setItem(storageKey, encryptionKey); localStorage.setItem(storageKey, encryptionKey);
} }
}
// If the encryption key is unset or malformed we need to stop // If the encryption key is unset or malformed we need to stop
// everything to avoid returning unencrypted information. // everything to avoid returning unencrypted information.
@ -165,40 +74,6 @@ export class CryptoService {
return hmac === this.accessLevelHmac(accessLevel, seedHex); return hmac === this.accessLevelHmac(accessLevel, seedHex);
} }
encryptedSeedHexToPrivateKey(encryptedSeedHex: string, domain: string): EC.KeyPair {
const seedHex = this.decryptSeedHex(encryptedSeedHex, domain);
return this.seedHexToPrivateKey(seedHex);
}
mnemonicToKeychain(mnemonic: string, extraText?: string, nonStandard?: boolean): HDNode {
const seed = bip39.mnemonicToSeedSync(mnemonic, extraText);
// @ts-ignore
return HDKey.fromMasterSeed(seed).derive('m/44\'/0\'/0\'/0/0', nonStandard);
}
keychainToSeedHex(keychain: HDNode): string {
return keychain.privateKey.toString('hex');
}
seedHexToPrivateKey(seedHex: string): EC.KeyPair {
const ec = new EC('secp256k1');
return ec.keyFromPrivate(seedHex);
}
privateKeyToDeSoPublicKey(privateKey: EC.KeyPair, network: Network): string {
const prefix = CryptoService.PUBLIC_KEY_PREFIXES[network].deso;
const key = privateKey.getPublic().encode('array', true);
const prefixAndKey = Uint8Array.from([...prefix, ...key]);
return bs58check.encode(prefixAndKey);
}
publicKeyToDeSoPublicKey(publicKey: EC.KeyPair, network: Network): string {
const prefix = CryptoService.PUBLIC_KEY_PREFIXES[network].deso;
const key = publicKey.getPublic().encode('array', true);
return bs58check.encode(Buffer.from([...prefix, ...key]));
}
// Decode public key base58check to Buffer of secp256k1 public key // Decode public key base58check to Buffer of secp256k1 public key
publicKeyToECBuffer(publicKey: string): Buffer { publicKeyToECBuffer(publicKey: string): Buffer {
// Sanity check similar to Base58CheckDecodePrefix from core/lib/base58.go // Sanity check similar to Base58CheckDecodePrefix from core/lib/base58.go
@ -213,4 +88,16 @@ export class CryptoService {
return new Buffer(publicKeyEC.getPublic('array')); return new Buffer(publicKeyEC.getPublic('array'));
} }
// TODO check that the signature for the walletStr is valid
checkSig(walletStr: string, walletSignature: string): boolean {
throw "implement me"
return true
}
// TODO find errors in the wallet. missing fields, etc. json-schema
validateWallet(wallet: object): string | null {
throw "implement me"
return null
}
} }

View file

@ -1,14 +1,13 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {AccessLevel, Network} from '../types/identity'; import {Network} from '../types/identity';
import {environment} from '../environments/environment'; import {environment} from '../environments/environment';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class GlobalVarsService { export class GlobalVarsService {
network = Network.mainnet; network : Network = Network.MainNet;
hostname = ''; hostname = '';
accessLevelRequest = AccessLevel.ApproveAll;
inTab = !!window.opener; inTab = !!window.opener;
webview = false; webview = false;
@ -17,7 +16,6 @@ export class GlobalVarsService {
callback = ''; callback = '';
callbackInvalid = false; callbackInvalid = false;
constructor() { } constructor() { }
inFrame(): boolean { inFrame(): boolean {

View 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
View 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(),
}]
}
}

View file

@ -1,36 +1,11 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Observable, Subject} from 'rxjs'; import {Observable, Subject} from 'rxjs';
import {v4 as uuid} from 'uuid'; import {v4 as uuid} from 'uuid';
import {AccessLevel, PublicUserInfo} from '../types/identity'; import {AccessLevel, PublicChannelInfo} from '../types/identity';
import {CryptoService} from './crypto.service'; import {CryptoService} from './crypto.service';
import {GlobalVarsService} from './global-vars.service'; import {GlobalVarsService} from './global-vars.service';
import {CookieService} from 'ngx-cookie';
import {SigningService} from './signing.service'; import {SigningService} from './signing.service';
import {HttpParams} from '@angular/common/http'; import {HttpParams} from '@angular/common/http';
import {
Transaction,
TransactionMetadataBasicTransfer,
TransactionMetadataBitcoinExchange,
TransactionMetadataCreatorCoin,
TransactionMetadataCreatorCoinTransfer,
TransactionMetadataFollow,
TransactionMetadataLike,
TransactionMetadataPrivateMessage,
TransactionMetadataSubmitPost,
TransactionMetadataSwapIdentity,
TransactionMetadataUpdateBitcoinUSDExchangeRate,
TransactionMetadataUpdateGlobalParams,
TransactionMetadataUpdateProfile,
TransactionMetadataNFTTransfer,
TransactionMetadataAcceptNFTTransfer,
TransactionMetadataBurnNFT,
TransactionMetadataNFTBid,
TransactionMetadataAcceptNFTBid,
TransactionMetadataUpdateNFT,
TransactionMetadataCreateNFT,
TransactionMetadataDAOCoin,
TransactionMetadataTransferDAOCoin
} from '../lib/deso/transaction';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -48,12 +23,30 @@ export class IdentityService {
constructor( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
private globalVars: GlobalVarsService, private globalVars: GlobalVarsService,
private cookieService: CookieService,
private signingService: SigningService, private signingService: SigningService,
) { ) {
window.addEventListener('message', (event) => this.handleMessage(event)); window.addEventListener('message', (event) => this.handleMessage(event));
} }
// Safari only lets us store things in cookies
mustUseStorageAccess(): boolean {
// Webviews have full control over storage access
// TODO why do we trust the app to send this properly
if (this.globalVars.webview) {
return false;
}
const supportsStorageAccess = typeof document.hasStorageAccess === 'function';
const isChrome = navigator.userAgent.indexOf('Chrome') > -1;
const isSafari = !isChrome && navigator.userAgent.indexOf('Safari') > -1;
// Firefox and Edge support the storage access API but do not enforce it.
// For now, only use cookies if we support storage access and use Safari.
const mustUseStorageAccess = supportsStorageAccess && isSafari;
return mustUseStorageAccess;
}
// Outgoing Messages // Outgoing Messages
initialize(): Observable<any> { initialize(): Observable<any> {
@ -65,8 +58,7 @@ export class IdentityService {
} }
login(payload: { login(payload: {
users: {[key: string]: PublicUserInfo}, channel: PublicChannelInfo | null,
publicKeyAdded?: string,
signedUp?: boolean signedUp?: boolean
signedTransactionHex?: string, signedTransactionHex?: string,
addresses?: string[], addresses?: string[],
@ -89,11 +81,14 @@ export class IdentityService {
// Incoming Messages // Incoming Messages
private handleSign(data: any): void { private handleSign(data: any): void {
const { id, payload: { encryptedSeedHex, transactionHex } } = data; // TODO
throw "implement for lbry"
/*
const transaction or action details = data;
// This will tell us whether we need full signing access or just ApproveLarge // This will tell us whether we need full signing access or just ApproveLarge
// level of access. // level of access.
const requiredAccessLevel = this.getRequiredAccessLevel(transactionHex); const requiredAccessLevel = this.getRequiredAccessLevel(transaction or action details);
// In the case that approve() fails, it responds with a message indicating // In the case that approve() fails, it responds with a message indicating
// that approvalRequired = true, which the caller can then uses to trigger // that approvalRequired = true, which the caller can then uses to trigger
@ -102,24 +97,27 @@ export class IdentityService {
return; return;
} }
// If we get to this point, no approval UI was required. This typically // TODO - this.signingService.signPSBT instead
// happens if the caller has full signing access or signing access for if it's a transaction
// non-spending txns such as like, post, update profile, etc. In the const signedTransactionHex = this.signingService.signPSBT(transaction details);
// latter case we need a subsequent check to ensure that the txn is not else // just an action
// sending money to any public keys other than the sender himself. const signedActionHex = this.signingService.signActiov(action details);
if (!this.approveSpending(data)) {
return;
}
const seedHex = this.cryptoService.decryptSeedHex(encryptedSeedHex, this.globalVars.hostname);
const signedTransactionHex = this.signingService.signTransaction(seedHex, transactionHex);
// TODO figure this out...
this.respond(id, { this.respond(id, {
signedTransactionHex, signedTransactionHex,
signedActionHex,
}); });
*/
} }
private handleJwt(data: any): void { private handleJwt(data: any): void {
// Give a permission token that expires in 10 minutes. DeSo apps use it for
// things like image uploading. Creation of this token is subject to same
// access level requirements as actions and transactions.
// TODO - make this work with LBRY. Or, nix it if we know we don't need it.
// Perhaps this will actually *be* our "actions"?
if (!this.approve(data, AccessLevel.ApproveAll)) { if (!this.approve(data, AccessLevel.ApproveAll)) {
return; return;
} }
@ -136,7 +134,7 @@ export class IdentityService {
private async handleInfo(event: MessageEvent): Promise<void> { private async handleInfo(event: MessageEvent): Promise<void> {
// check storage access API // check storage access API
let hasStorageAccess = true; let hasStorageAccess = true;
if (this.cryptoService.mustUseStorageAccess()) { if (this.mustUseStorageAccess()) {
hasStorageAccess = await document.hasStorageAccess(); hasStorageAccess = await document.hasStorageAccess();
} }
@ -148,57 +146,49 @@ export class IdentityService {
hasLocalStorageAccess = false; hasLocalStorageAccess = false;
} }
// check for cookie access // TODO - Sort out the storage access issue:
this.cookieService.put('deso-test-access', 'true'); //
const hasCookieAccess = !!this.cookieService.get('deso-test-access'); // There was a part of the code that was defaulting to cookies if
// this.mustUseStorageAccess() was true (only applies to Safari). I don't
// want to do that because I'm afraid the data we'll be storing won't fit.
// So, I'm going to consider browsers with this.mustUseStorageAccess() as
// unsupported until we can get back and figure out why DeSo chose to go
// with cookies for those cases.
//
// It looks like the issue is that Safari only supports saving in cookies
// (per the comment above this.mustUseStorageAccess(), which I moved from
// DeSo's crypto.service). If that's the case, I don't understand how it
// was able to save users and levels in the identity service. Someone who
// understands should look into that.
//
// PERHAPS it's that DeSo only strictly need certain things to be in
// localStorage, and everything else (like the encryption key) could fit
// in the cookie. Maybe we could store the wallet sync password in the
// cookie and reconstruct the wallet and channels on start. The problem
// would be resetting the access levels each time. Maybe that will just
// be the cost of using Safari.
//
// See:
// https://github.com/deso-protocol/identity/blob/0543c40cb4e7e39cc9098554f99c27649e3d1d03/src/app/crypto.service.ts#L36
// https://github.com/deso-protocol/identity/blob/0543c40cb4e7e39cc9098554f99c27649e3d1d03/src/app/identity.service.ts#L261
// https://github.com/deso-protocol/identity/pull/50/
// store if browser is supported or not // store if browser is supported or not
this.browserSupported = hasCookieAccess || hasLocalStorageAccess; this.browserSupported = hasLocalStorageAccess && !this.mustUseStorageAccess();
this.respond(event.data.id, { this.respond(event.data.id, {
hasCookieAccess,
hasStorageAccess, hasStorageAccess,
hasLocalStorageAccess, hasLocalStorageAccess,
browserSupported: this.browserSupported, browserSupported: this.browserSupported,
}); });
} }
// Access levels // Access levels
private getRequiredAccessLevel(transactionHex: string): AccessLevel { // TODO implement for lbry
const txBytes = new Buffer(transactionHex, 'hex'); // private getRequiredAccessLevel(transactionHex: string): AccessLevel {
const transaction = Transaction.fromBytes(txBytes)[0] as Transaction<any>; // switch case on the type of transaction or action, and return the required access level
// unless required access level is going to be case by case for each user
switch (transaction.metadata.constructor) {
case TransactionMetadataBasicTransfer:
case TransactionMetadataBitcoinExchange:
case TransactionMetadataUpdateBitcoinUSDExchangeRate:
case TransactionMetadataCreatorCoin:
case TransactionMetadataCreatorCoinTransfer:
case TransactionMetadataSwapIdentity:
case TransactionMetadataUpdateGlobalParams:
case TransactionMetadataUpdateProfile:
case TransactionMetadataCreateNFT:
case TransactionMetadataUpdateNFT:
case TransactionMetadataAcceptNFTBid:
case TransactionMetadataNFTBid:
case TransactionMetadataNFTTransfer:
case TransactionMetadataAcceptNFTTransfer:
case TransactionMetadataBurnNFT:
case TransactionMetadataDAOCoin:
case TransactionMetadataTransferDAOCoin:
return AccessLevel.Full;
case TransactionMetadataFollow:
case TransactionMetadataPrivateMessage:
case TransactionMetadataSubmitPost:
case TransactionMetadataLike:
return AccessLevel.ApproveLarge;
}
return AccessLevel.Full;
}
private hasAccessLevel(data: any, requiredAccessLevel: AccessLevel): boolean { private hasAccessLevel(data: any, requiredAccessLevel: AccessLevel): boolean {
const { payload: { encryptedSeedHex, accessLevel, accessLevelHmac }} = data; const { payload: { encryptedSeedHex, accessLevel, accessLevelHmac }} = data;
@ -210,25 +200,6 @@ export class IdentityService {
return this.cryptoService.validAccessLevelHmac(accessLevel, seedHex, accessLevelHmac); return this.cryptoService.validAccessLevelHmac(accessLevel, seedHex, accessLevelHmac);
} }
// This method checks if transaction in the payload has correct outputs for requested AccessLevel.
private approveSpending(data: any): boolean {
const { payload: { accessLevel, transactionHex }} = data;
// If the requested access level is ApproveLarge, we want to confirm that transaction doesn't
// attempt sending $DESO to a non-owner public key. If it does, we respond with approvalRequired.
if (accessLevel === AccessLevel.ApproveLarge) {
const txBytes = new Buffer(transactionHex, 'hex');
const transaction = Transaction.fromBytes(txBytes)[0] as Transaction<any>;
for (const output of transaction.outputs) {
if (output.publicKey.toString('hex') !== transaction.publicKey.toString('hex')) {
this.respond(data.id, {approvalRequired: true});
return false;
}
}
}
return true;
}
private approve(data: any, accessLevel: AccessLevel): boolean { private approve(data: any, accessLevel: AccessLevel): boolean {
const hasAccess = this.hasAccessLevel(data, accessLevel); const hasAccess = this.hasAccessLevel(data, accessLevel);
const hasEncryptionKey = this.cryptoService.hasSeedHexEncryptionKey(this.globalVars.hostname); const hasEncryptionKey = this.cryptoService.hasSeedHexEncryptionKey(this.globalVars.hostname);

View file

@ -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>

View file

@ -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'});
}
}

View 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>
&nbsp;
&nbsp;
<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>

View file

@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LogInComponent } from './log-in.component'; import { LogInAppComponent } from './log-in-app.component';
describe('LogInComponent', () => { describe('LogInAppComponent', () => {
let component: LogInComponent; let component: LogInAppComponent;
let fixture: ComponentFixture<LogInComponent>; let fixture: ComponentFixture<LogInAppComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ LogInComponent ] declarations: [ LogInAppComponent ]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LogInComponent); fixture = TestBed.createComponent(LogInAppComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View 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
});
}
}

View 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>

View file

@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoadSeedComponent } from './load-seed.component'; import { LogInWalletComponent } from './log-in-wallet.component';
describe('LoadSeedComponent', () => { describe('LogInWalletComponent', () => {
let component: LoadSeedComponent; let component: LogInWalletComponent;
let fixture: ComponentFixture<LoadSeedComponent>; let fixture: ComponentFixture<LogInWalletComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ LoadSeedComponent ] declarations: [ LogInWalletComponent ]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LoadSeedComponent); fixture = TestBed.createComponent(LogInWalletComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View 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'});
}
}

View file

@ -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 }}&hellip;</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>

View file

@ -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
});
}
}

View file

@ -1,9 +1,7 @@
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {CryptoService} from '../crypto.service'; import {CryptoService} from '../crypto.service';
import {IdentityService} from '../identity.service'; import {IdentityService} from '../identity.service';
import {AccountService} from '../account.service'; import {AccountService} from '../account.service';
import {AccessLevel} from '../../types/identity';
import {GlobalVarsService} from '../global-vars.service'; import {GlobalVarsService} from '../global-vars.service';
@Component({ @Component({
@ -20,7 +18,6 @@ export class LogoutComponent implements OnInit {
constructor( constructor(
private activatedRoute: ActivatedRoute,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private identityService: IdentityService, private identityService: IdentityService,
private accountService: AccountService, private accountService: AccountService,
@ -28,9 +25,6 @@ export class LogoutComponent implements OnInit {
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.activatedRoute.queryParams.subscribe(params => {
this.publicKey = params.publicKey || '';
});
} }
onCancel(): void { onCancel(): void {
@ -39,7 +33,7 @@ export class LogoutComponent implements OnInit {
onSubmit(): void { onSubmit(): void {
// We set the accessLevel for the logged out user to None. // We set the accessLevel for the logged out user to None.
this.accountService.setAccessLevel(this.publicKey, this.globalVars.hostname, AccessLevel.None); this.accountService.appLogout(this.globalVars.hostname);
// We reset the seed encryption key so that all existing accounts, except // We reset the seed encryption key so that all existing accounts, except
// the logged out user, will regenerate their encryptedSeedHex. Without this, // the logged out user, will regenerate their encryptedSeedHex. Without this,
// someone could have reused the encryptedSeedHex of an already logged out user. // someone could have reused the encryptedSeedHex of an already logged out user.
@ -49,7 +43,7 @@ export class LogoutComponent implements OnInit {
finishFlow(): void { finishFlow(): void {
this.identityService.login({ this.identityService.login({
users: this.accountService.getEncryptedUsers(), channel: this.accountService.getActiveChannel(this.globalVars.hostname),
}); });
} }

View file

@ -1,6 +1,5 @@
import {Component, OnDestroy, OnInit} from '@angular/core'; import {Component, OnDestroy, OnInit} from '@angular/core';
import {EntropyService} from '../entropy.service'; import {EntropyService} from '../entropy.service';
import {CryptoService} from '../crypto.service';
import {AccountService} from '../account.service'; import {AccountService} from '../account.service';
import {IdentityService} from '../identity.service'; import {IdentityService} from '../identity.service';
import {GlobalVarsService} from '../global-vars.service'; import {GlobalVarsService} from '../global-vars.service';
@ -19,7 +18,6 @@ export class SignUpComponent implements OnInit, OnDestroy {
seedCopied = false; seedCopied = false;
mnemonicCheck = ''; mnemonicCheck = '';
extraTextCheck = ''; extraTextCheck = '';
publicKeyAdded = '';
// Advanced tab // Advanced tab
showMnemonicError = false; showMnemonicError = false;
@ -32,7 +30,6 @@ export class SignUpComponent implements OnInit, OnDestroy {
constructor( constructor(
public entropyService: EntropyService, public entropyService: EntropyService,
private cryptoService: CryptoService,
private accountService: AccountService, private accountService: AccountService,
private identityService: IdentityService, private identityService: IdentityService,
public globalVars: GlobalVarsService, public globalVars: GlobalVarsService,
@ -81,17 +78,29 @@ export class SignUpComponent implements OnInit, OnDestroy {
////// STEP TWO BUTTONS /////// ////// STEP TWO BUTTONS ///////
stepTwoNext(): void { stepTwoNext(): void {
// TODO - signup for LBRY will be very different from DeSo. We'll need to
// make a wallet, not just "users". And other things happen.
throw 'signup not implemented'
/*
// this is a mix of some of what DeSo left over, and some new LBRY.id
// specific things that I figure we won't want to forget. This is just
// a guide for the future when we tackle signup.
// TODO Don't use this.globalVars.network here, use the network specified
// in the relevant account.ledger (assuming we even really need network)
const network = this.globalVars.network; const network = this.globalVars.network;
const mnemonic = this.mnemonicCheck; const mnemonic = this.mnemonicCheck;
const extraText = this.extraTextCheck; const extraText = this.extraTextCheck;
const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, extraText); const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, extraText);
this.publicKeyAdded = this.accountService.addUser(keychain, mnemonic, extraText, network); const accountNameAdded = this.accountService.addUser(keychain, mnemonic, extraText, network);
this.accountService.setAccessLevel( this.accountService.initAccess()
this.publicKeyAdded, this.globalVars.hostname, this.globalVars.accessLevelRequest);
this.login(); this.login();
*/
} }
stepTwoBack(): void { stepTwoBack(): void {
@ -102,8 +111,7 @@ export class SignUpComponent implements OnInit, OnDestroy {
login(): void { login(): void {
this.identityService.login({ this.identityService.login({
users: this.accountService.getEncryptedUsers(), channel: this.accountService.getActiveChannel(this.globalVars.hostname),
publicKeyAdded: this.publicKeyAdded,
signedUp: true, signedUp: true,
}); });
} }

View file

@ -1,9 +1,9 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {HubService} from './hub.service';
import KeyEncoder from 'key-encoder'; import KeyEncoder from 'key-encoder';
import {GlobalVarsService} from './global-vars.service';
import * as jsonwebtoken from 'jsonwebtoken'; import * as jsonwebtoken from 'jsonwebtoken';
import {CryptoService} from './crypto.service'; import {PrivateAccountInfo, Network} from '../types/identity';
import * as sha256 from 'sha256';
import { uvarint64ToBuf } from '../lib/bindata/util';
import * as bip32 from 'bip32'; import * as bip32 from 'bip32';
import { BIP32Interface } from 'bip32'; // TODO: Installed 2.0.6 instead of latest version, only because of weird typescript compilation stuff. Should probably get the latest. import { BIP32Interface } from 'bip32'; // TODO: Installed 2.0.6 instead of latest version, only because of weird typescript compilation stuff. Should probably get the latest.
@ -11,6 +11,7 @@ import * as bs58check from 'bs58check'
import * as lbry from 'bitcoinjs-lib' // TODO - package recommends browserify, which I did not do here. This works, but maybe there's good reason to browserify? import * as lbry from 'bitcoinjs-lib' // TODO - package recommends browserify, which I did not do here. This works, but maybe there's good reason to browserify?
import * as ecpair from 'ecpair'; // TODO - required acorn-class-fields for this version of Angular's webpack to accept it. see extend-acorn.js. import * as ecpair from 'ecpair'; // TODO - required acorn-class-fields for this version of Angular's webpack to accept it. see extend-acorn.js.
// TODO deleteme once I remove the last use of this
const NETWORK = lbry.networks.mainnet const NETWORK = lbry.networks.mainnet
@Injectable({ @Injectable({
@ -19,41 +20,389 @@ const NETWORK = lbry.networks.mainnet
export class SigningService { export class SigningService {
constructor( constructor(
private cryptoService: CryptoService, private hubService: HubService,
private globalVars: GlobalVarsService,
) { } ) { }
// this should be audited and go into a library. hobbled this together from private *generateKeys(
// code in bitcoinjs-lib. node: BIP32Interface,
private getAddressFromBip32(node: BIP32Interface): string { keyPath: lbry.bip32Lbry.KeyPath
const hash = lbry.crypto.hash160(node.publicKey) ): IterableIterator<BIP32Interface> {
for (let childIndex = 0; ; childIndex++) {
const payload = Buffer.allocUnsafe(21); yield node.derive(keyPath).derive(childIndex)
payload.writeUInt8(NETWORK.pubKeyHash, 0); }
hash.copy(payload, 1);
return bs58check.encode(payload);
} }
getSigningKey(wallet: any, address: string): Buffer | null { // TODO - have a minimum batch size actually, like 10, that's bigger than a
const account = wallet.accounts // gap, to reduce number of requests to the hub. or maybe 5. Get an opinion
.filter((account: any) => { // on what's a common number of used keys. And channels, that may be a
let node: BIP32Interface = bip32.fromBase58(account.private_key); // different answer.
return address === this.getAddressFromBip32(node) // TODO - have a maximum number of keys? Just in case there's a bug so we
})[0] // don't blow up the user's browser?
return bip32.fromBase58(account.private_key).privateKey || null; private *generateKeyBatches(
node: BIP32Interface,
keyPath: lbry.bip32Lbry.KeyPath,
gap: number,
): IterableIterator<BIP32Interface[]> {
const generatedKeys = this.generateKeys(node, keyPath)
let batchSize = gap
while(true) {
// TODO - confirm gap off-by-one error
// Get the next `batchSize` keys
let batchKeys: BIP32Interface[] = []
for (let index = 0; index < batchSize; index++) {
batchKeys.push(generatedKeys.next().value)
} }
getAddresses(wallet: any): string[] { // Yield them, and get back the ones that were found to be used
return wallet.accounts // TODO - if it's easy for the consumer, we can just accept the last one,
// won't venture into deterministic yet // or even better, its index within `batchKeys`
.filter((account: any) => account.address_generator.name === 'single-address') const usedBatchKeys = yield batchKeys
.map((account: any) => { if (usedBatchKeys === undefined) {
let node: BIP32Interface = bip32.fromBase58(account.private_key); throw "generateKeyBatches iteration needs an array of used batch keys"
return this.getAddressFromBip32(node) }
// If none are used, we passed the gap and found nothing
if (usedBatchKeys.length === 0) return
// Easier for finding
const batchPubKeys = batchKeys.map(key => key.public_key)
const usedBatchPubKeys = usedBatchKeys.map(key => key.public_key)
// What is the last index of batchPubKeys (and thus batchKeys) that is in
// usedBatchPubKeys?
const lastUsedIndex = Math.max.apply(
Math, usedBatchPubKeys.map(pubKey => batchPubKeys.indexOf(pubKey))
)
// Indicates a bug here, or `usedBatchKeys` failed to be a subset of
// `batchKeys`.
if (lastUsedIndex === -1) throw "error determining next batch"
// TODO think about if this is right. Maybe needs test cases.
// Now that we know what the last used key is in our batch, we want to
// have enough keys after it to cover the next gap.
// Any keys in our current batch after the last used key is the beginning
// of the next gap.
const gapInThisBatch = batchSize - (lastUsedIndex + 1)
// The rest of the next gap will be the at the beginning of the next
// batch. We'll make our batch big enough to contain it, in case it's
// full size.
batchSize = gap - gapInThisBatch;
}
}
/*
Example claim. Just so I know the format while I'm working on this. Delete
after channel retrieval work is done.
{
"address": "bbvk6TMEzujW8r3xhP6e1FhCuSW3FYP9SJ",
"amount": "0.01",
"claim_id": "7d39c627771c529e65656f4ca86d13686acc0442",
"claim_op": "create",
"confirmations": 193,
"has_signing_key": true,
"height": 1157989,
"is_internal_transfer": false,
"is_my_input": true,
"is_my_output": true,
"is_spent": false,
"meta": {},
"name": "@lolstupidtest2",
"normalized_name": "@lolstupidtest2",
"nout": 0,
"permanent_url": "lbry://@lolstupidtest2#7d39c627771c529e65656f4ca86d13686acc0442",
"timestamp": 1652278858,
"txid": "a62878d9c558cd17f1f946df03aa8584dd002238004a02d742261b5c560dc43f",
"type": "claim",
"value": {
"public_key": "023bfe202119244b448f8974ee3152a3d859ac169a420b8ef1dda423fa015b2a4f",
"public_key_id": "bUfKnUamA7T3S2JGjV6Rmb8o3Wtn79WSTA"
},
"value_type": "channel"
}
*/
// TODO error handling?
// TODO - deprecated channel key (`certificates` field)
private getChannelsForAccount(account: PrivateAccountInfo): Observable<PrivateChannelInfo[]> {
// TODO Can single addresses register channels?
// Grin says yes.
// If so, handle single key cases. For generated key cases, assert that
// it === "deterministic-chain" (there shouldn't be anything else), or make
// a note to validate the wallet via jsonschema when reading it initially.
// TODO - there's got to be a better way.
return new Observable(subscriber => {
const keyBatches = this.generateKeyBatches(
this.bip32FromAccount(account),
lbry.bip32Lbry.KeyPath.CHANNEL,
1, // TODO what gap do I actually use for channels?
)
// Where we accumulate all of the channels we find through all of the
// recursions
let foundChannels: PrivateChannelInfo[] = []
accumulateChannels(possibleKeys: BIP32Interface[], done: boolean) {
if (done) {
// The address generator has indicated that all used channel keys
// have been found. Give the subscriber all of the channels we just
// accumulated across recursions.
subscriber.next(foundChannels)
subscriber.complete()
return
}
possibleKeysByKeyId = Object.fromEntries(
possibleKeys.map(key => [this.getAddressFromBip32(key, account.network), key])
)
this.hubService.findChannels(
possibleKeys.map(key => this.getAddressFromBip32(key, account.network))
).pipe(
map(res => {
// TODO - Should expect HUB to return revoked channels as well.
// Theose need to be added to `usedKeys` since they count toward
// gaps.
const newChannels: PrivateChannelInfo[] = res.map(hubChannel => ({
claimId: hubChannel.claim_id,
name: hubChannel.name,
normalizedName: hubChannel.normalizedName,
pubKeyId: hubChannel.value.public_key_id,
signingKey: possibleKeysByKeyId[hubChannel.value.public_key_id],
// TODO - more fields?
}))
foundChannels = foundChannels.concat(newChannels)
const usedKeys = newChannels.map(channel => possibleKeysByKeyId[channel.pubKeyId])
// Give the address generator `usedKeys` so it knows how many to grab
// next, to cover the gap we're looking for
({value: possibleKeys, done} = keyBatches.next(usedKeys));
accumulateChannels(possibleKeys, done)
})
)
}
(const {value: possibleKeys, done} = keyBatches.next());
accumulateChannels(possibleKeys, done)
}) })
} }
/*
The functions below in the commented out block are an earlier layer of WIP
than the rest of it. At that point, we thought we needed to get all of the
used addresses and use them to get the channel claims. (Whereas the newer
plan would be to use special hub endpoints to get the channel info more
directly.)
That said, we will eventually probably have use for some of this stuff.
Primarily, we'll need to find spending keys. But who knows, maybe we'll
even change plans back again wrt Channel querying. I was afraid of deleting
it until we had something working.
It didn't get completed (or so I recall?) before we switched gears so it's
commented out. (Not that the newer plan work is completed either!)
Even if we use some or all of this, we should probably reimplement it using
`generateKeyBatches` (from the newer plan's code), because it's just
cleaner.
*/
/*
// Add all of the keys I can find from a private key
// `channelAddresses` should be all channel addresses, from both active and
// revoked claims. This helps us avoid issues with key generation gaps.
private findChannelKeysForAccount(
node: BIP32Interface,
claimChannelAddresses: string[],
deprecatedChannelKeys: {string: string}
): {[key: string]: BIP32Interface}
{
let numRemainingKeys = claimChannelAddresses.length
const channelKeys: {[key: string]: BIP32Interface} = {}
for (const channelAddress in deprecatedChannelKeys) {
if (channelAddress in claimChannelAddresses) {
// TODO - Implement getting deprecated channel private keys from pem
// format to bip32, somehow.
// channelKeys[channelAddress] = this.fromPem(deprecatedChannelKeys[channelAddress])
numRemainingKeys--;
}
}
const generatedChannelKeys = this.generateKeys(node, lbry.bip32Lbry.KeyPath.CHANNEL)
let numRemainingSkippedKeys = 10
while(numRemainingSkippedKeys>0) {
const channelKey = generatedChannelKeys.next().value
const channelAddress = this.getAddressFromBip32(channelKey, channelKey.network)
if (channelAddress in claimChannelAddresses) {
channelKeys[channelAddress] = channelKey
numRemainingKeys--;
} else {
// TODO - should this ever happen? How many should there be? Should this use the normal "gap system"?
numRemainingSkippedKeys--;
}
}
return channelKeys;
}
// TODO - Respect address use maximum, here, or wherever is relevant.
private getUsedAddresses(account: PrivateAccountInfo): {[key in lbry.bip32Lbry.KeyPath]: string[]} {
if (account.address_generator.name == "single-address") {
// TODO is this right? Can single addresses register channels?
const accountId = this.getAddress(account)
// TODO - actually not quite a "used" address either. May not have been
// used, but we should search for channels with it. So what is the right
// name?
return {RECEIVE: [accountId]} // and not have CHANGE
}
// If it is not this, something is amiss.
// TODO - actually just put this in the wallet json schema
if(account.address_generator.name !== "deterministic-chain") {
throw "Expected deterministic-chain at this point"
}
const result = {}
for (let keyPath of [lbry.bip32Lbry.KeyPath.CHANGE, lbry.bip32Lbry.KeyPath.RECEIVE]) {
const keyBatches = this.generateKeyBatches(
this.bip32FromAccount(account),
keyPath,
account.address_generator.change.gap,
)
let usedAddresses: string[] = []
let possibleKeys;
let done = false;
({value: possibleKeys, done} = keyBatches.next());
while (!done) {
const nextUsedAddresses = this.hubService.doIHaveHistory(
possibleKeys.map(key => this.getAddressFromBip32(key, key.network))
)
usedAddresses = usedAddresses.concat(nextUsedAddresses)
keyBatches.next(nextUsedAddresses)
}
result[keyPath] = usedAddresses
}
return result
}
// Query the hub for all our used addresses. Note which account they came
// from.
// return: {usedAddress: accountId}
private getAccountIDsByUsedAddress(accounts: PrivateAccountInfo[]): {[key: string]: string} {
// accountIDsByUsedAddress: {usedAddress: accountId}
const accountIDsByUsedAddress: {[key: string]: string} = {}
for (let account of accounts) {
// TODO - this.getUsedAddresses returns the addresses by keypath now
for (let usedAddress of this.getUsedAddresses(account)) {
accountIDsByUsedAddress[usedAddress] = this.getAddress(account)
}
}
}
// Query the hub for all our channel claims, based on used keys. Organize The
// channels by accountId.
// returns channelsByAccountId: {accountId: channelClaims[]}
public getChannelsByAccountId(accounts: PrivateAccountInfo[]): {[key: string]: any}{
const accountIDsByUsedAddress = this.getAccountIDsByUsedAddress(accounts)
const channelsByAccountId = {}
for (let claim of this.hubService.getClaims(Object.keys(accountIDsByUsedAddress))) {
if (claim.value_type != 'channel') {
continue
}
const accountId = accountIDsByUsedAddress[claim.address]
if(!accountId) {
// Don't trust what comes from the hub
// TODO - trust fewer things?
throw "bad accountId"
}
if (!channelsByAccountId[claim.address]) {
channelsByAccountId[claim.address] = []
}
channelsByAccountId[accountId].push(claim)
}
return channelsByAccountId
}
// Figure out channel private keys for the channel addresses, for each
// account.
// returns {accountId: {channelAddress: channelPrivateKey}}
public getChannelKeysByAccountId(accounts, channelsByAccountId): {[key: string]: {[key: string]: string}} {
const channelKeysForAccount: {[key: string]: {[key: string]: string}} = {}
for (const account of accounts) {
const accountId = this.getAddress(account)
channelKeysForAccount[accountId] = this.findChannelKeysForAccount(
this.bip32FromAccount(account),
channelsByAccountId[accountId].map(claim => claim.value.public_key_id),
account.certificates,
)
}
return channelKeysForAccount
}
*/
private bip32FromAccount(account: PrivateAccountInfo): BIP32Interface {
const network = {
'lbc_mainnet': lbry.networks.mainnet,
'lbc_testnet': lbry.networks.testnet,
'lbc_regtest': lbry.networks.regtest,
}[account.ledger]
return bip32.fromBase58(account.private_key, network);
}
// TODO - make sure this comes up with the right account ID!
public getAddress(accountKey: string, accountNetwork: Network): string {
const network = {
'lbc_mainnet': lbry.networks.mainnet,
'lbc_testnet': lbry.networks.testnet,
'lbc_regtest': lbry.networks.regtest,
}[accountNetwork]
let node: BIP32Interface = bip32.fromBase58(accountKey, network);
return this.getAddressFromBip32(node, network)
}
private getAddressFromBip32(node: BIP32Interface, network: lbry.networks.Network): string {
// taken from a test in bitcoinjs-lib
return lbry.payments.p2pkh({ pubkey: node.publicKey, network }).address!;
}
// TODO - Note to future self - I think this may be a function we only needed
// for the demo (like getSpendingAddresses). Or maybe it just needs to be
// updated for real use.
getSigningKey(wallet: any, address: string): Buffer | null {
const account = wallet.accounts
.filter((account: any) => {
return address === this.getAddress(account)
})[0]
return bip32.fromBase58(account.private_key, network).privateKey || null;
}
// Does this belong in identity.service next to getChannels? or does
// getChannels belong here next to this?
// TODO - This is outdated, but make sure there's not something I need to
// extract from here before deleting it.
/*
private getSpendingAddresses(wallet: any): string[] {
return wallet.accounts
// won't venture into deterministic yet
.filter((account: any) => account.address_generator.name === 'single-address')
.map((account: any) => this.getAddress(account))
)
}
*/
signPSBT(psbtHex: string, nonWitnessUtxoHexes: string[], signingKey: Buffer): string { signPSBT(psbtHex: string, nonWitnessUtxoHexes: string[], signingKey: Buffer): string {
const keyPair = ecpair.ECPair.fromPrivateKey(signingKey, { network: NETWORK }) // TODO Don't use this.globalVars.network here, use the network specified
// in the relevant account.ledger (assuming we even really need network)
const keyPair = ecpair.ECPair.fromPrivateKey(signingKey, { network: this.globalVars.network })
const nonWitnessUtxos = nonWitnessUtxoHexes.map(h => Buffer.from(h, 'hex')) const nonWitnessUtxos = nonWitnessUtxoHexes.map(h => Buffer.from(h, 'hex'))
const psbt = lbry.Psbt.fromHex(psbtHex) const psbt = lbry.Psbt.fromHex(psbtHex)
@ -75,39 +424,29 @@ export class SigningService {
} }
signJWT(seedHex: string): string { signJWT(seedHex: string): string {
return ""
// TODO - use bitcoinjs-lib and do an actual signature with the actual key in the wallet, send the identifier of the wallet over, etc etc etc.
// Assuming we want to keep this
const keyEncoder = new KeyEncoder('secp256k1'); const keyEncoder = new KeyEncoder('secp256k1');
const encodedPrivateKey = keyEncoder.encodePrivate(seedHex, 'raw', 'pem'); const encodedPrivateKey = keyEncoder.encodePrivate(seedHex, 'raw', 'pem');
return jsonwebtoken.sign({ }, encodedPrivateKey, { algorithm: 'ES256', expiresIn: 60 * 10 }); return jsonwebtoken.sign({ }, encodedPrivateKey, { algorithm: 'ES256', expiresIn: 60 * 10 });
} }
signAction(seedHex: string, actionHex: string): string { signAction(seedHex: string, actionHex: string): string {
return ""
// TODO - use bitcoinjs-lib and do an actual signature with the actual key in the wallet, send the identifier of the wallet over, etc etc etc. // TODO - use bitcoinjs-lib and do an actual signature with the actual key in the wallet, send the identifier of the wallet over, etc etc etc.
/*
const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex); const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex);
const actionBytes = new Buffer(actionHex, 'hex'); const actionBytes = new Buffer(actionHex, 'hex');
const actionHash = new Buffer(sha256.x2(actionBytes), 'hex'); const actionHash = new Buffer(sha256.x2(actionBytes), 'hex');
const signature = privateKey.sign(actionHash); const signature = privateKey.sign(actionHash);
return new Buffer(signature.toDER()).toString('hex'); return new Buffer(signature.toDER()).toString('hex');
} */
signTransaction(seedHex: string, transactionHex: string): string {
const privateKey = this.cryptoService.seedHexToPrivateKey(seedHex);
const transactionBytes = new Buffer(transactionHex, 'hex');
const transactionHash = new Buffer(sha256.x2(transactionBytes), 'hex');
const signature = privateKey.sign(transactionHash);
const signatureBytes = new Buffer(signature.toDER());
const signatureLength = uvarint64ToBuf(signatureBytes.length);
const signedTransactionBytes = Buffer.concat([
// This slice is bad. We need to remove the existing signature length field prior to appending the new one.
// Once we have frontend transaction construction we won't need to do this.
transactionBytes.slice(0, -1),
signatureLength,
signatureBytes,
]);
return signedTransactionBytes.toString('hex');
} }
} }

View file

@ -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>

View file

@ -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();
});
});

View file

@ -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,
});
}
}

View file

@ -1,7 +1,7 @@
import {AccountService} from '../account.service';
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {SigningService} from '../signing.service'; import {SigningService} from '../signing.service';
import {IdentityService} from '../identity.service'; import {IdentityService} from '../identity.service';
import {CryptoService} from '../crypto.service';
import {GlobalVarsService} from '../global-vars.service'; import {GlobalVarsService} from '../global-vars.service';
@Component({ @Component({
@ -14,9 +14,9 @@ export class TestSignTransactionComponent implements OnInit {
signedTransactionHex?: string signedTransactionHex?: string
constructor( constructor(
private accountService: AccountService,
private signingService: SigningService, private signingService: SigningService,
private identityService: IdentityService, private identityService: IdentityService,
private cryptoService: CryptoService,
private globalVars: GlobalVarsService, private globalVars: GlobalVarsService,
) { } ) { }
@ -26,7 +26,7 @@ export class TestSignTransactionComponent implements OnInit {
const fromAddress = params.get("fromAddress") || "" const fromAddress = params.get("fromAddress") || ""
const nonWitnessUtxoHexes = params.get("nonWitnessUtxoHexes") || null const nonWitnessUtxoHexes = params.get("nonWitnessUtxoHexes") || null
const wallet : object | null = this.cryptoService.getWallet(this.globalVars.hostname); const wallet : object | null = this.accountService.getWallet();
const signingKey = this.signingService.getSigningKey(wallet, fromAddress) const signingKey = this.signingService.getSigningKey(wallet, fromAddress)
// TODO what if error? etc etc. // TODO what if error? etc etc.
@ -39,7 +39,7 @@ export class TestSignTransactionComponent implements OnInit {
finishFlow(signedTransactionHex?: string): void { finishFlow(signedTransactionHex?: string): void {
this.identityService.login({ this.identityService.login({
users: {}, // TODO sigh channel: this.accountService.getActiveChannel(this.globalVars.hostname),
signedTransactionHex, signedTransactionHex,
}); });
} }

View file

@ -1,4 +1,6 @@
import {AccountService} from '../account.service';
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {GlobalVarsService} from '../global-vars.service';
import {SigningService} from '../signing.service'; import {SigningService} from '../signing.service';
import {IdentityService} from '../identity.service'; import {IdentityService} from '../identity.service';
@ -18,6 +20,8 @@ export class TestSignComponent implements OnInit {
privateKeyString: string = "thhUUVXQtyxonMaezCKwihLw9tZUrGJgWBMxDNfxoWub8dLGA" privateKeyString: string = "thhUUVXQtyxonMaezCKwihLw9tZUrGJgWBMxDNfxoWub8dLGA"
constructor( constructor(
private accountService: AccountService,
private globalVars: GlobalVarsService,
private signingService: SigningService, private signingService: SigningService,
private identityService: IdentityService, private identityService: IdentityService,
) { } ) { }
@ -34,7 +38,7 @@ export class TestSignComponent implements OnInit {
finishFlow(signatureHex?: string): void { finishFlow(signatureHex?: string): void {
this.identityService.login({ this.identityService.login({
users: {}, // TODO sigh channel: this.accountService.getActiveChannel(this.globalVars.hostname),
signatureHex, signatureHex,
}); });
} }

View file

@ -1,5 +1,5 @@
export const environment = { export const environment = {
production: true, production: true,
hostname: 'identity.deso.org', hostname: 'localhost:4201',
nodeHostname: 'node.deso.org', nodeHostname: 'node.deso.org', // TODO deleteme
}; };

View file

@ -4,8 +4,20 @@
export const environment = { export const environment = {
production: false, production: false,
hostname: 'identity.deso.org', hostname: 'localhost:4201',
nodeHostname: 'node.deso.org', nodeHostname: 'node.deso.org', // TODO deleteme
walletSyncHostname: 'localhost:8091',
hubHostnames: [
'spv11.lbry.com:50001',
'spv12.lbry.com:50001',
'spv13.lbry.com:50001',
'spv14.lbry.com:50001',
'spv15.lbry.com:50001',
'spv16.lbry.com:50001',
'spv17.lbry.com:50001',
'spv18.lbry.com:50001',
'spv19.lbry.com:50001',
]
}; };
/* /*

View file

@ -1,25 +1,70 @@
export interface PrivateUserInfo { // TODO: Use our wallet json-schema. Otherwise, bitcoinjs-lib probably already has a networks enum.
seedHex: string; // TODO: what about encrypted wallets?
mnemonic: string;
extraText: string; export enum AddressType {
network: Network; DeterministicChain = "deterministic-chain",
} SingleAddress = "single-address",
export interface PublicUserInfo {
hasExtraText: boolean;
encryptedSeedHex: string;
network: Network;
accessLevel: AccessLevel;
accessLevelHmac: string;
}
export interface UserProfile {
username: string;
} }
// The `ledger` field of accounts in a wallet (or PrivateAccountInfo).
// TODO - Rename this to "Ledger" to distinguish from `lbry.networks`,
// particularly since the ledger values are different from the
// `lbry.networks` field names.
export enum Network { export enum Network {
mainnet = 'mainnet', MainNet = "lbc_mainnet",
testnet = 'testnet', TestNet = "lbc_testnet",
RegTest = "lbc_regtest",
}
// Only safe for web wallet, not sent to the app
export interface PrivateAccountInfo {
address_generator: {
change?: {
gap: number,
maximum_uses_per_address: number,
},
name: AddressType,
receiving?: {
gap: number,
maximum_uses_per_address: number,
}
},
certificates: {[key: string]: string},
encrypted: boolean,
ledger: Network,
modified_on: number,
name: string,
private_key: string,
public_key: string,
seed: string,
}
export interface PrivateChannelInfo {
// TODO - add more useful stuff
claimId: string;
name: string;
normalizedName: string;
pubKeyId: string;
signingKey: string;
}
// can be sent to the app
export interface PublicChannelInfo {
// TODO - add more useful stuff
claimId: string;
name: string;
normalizedName: string;
pubKeyId: string;
// Don't care about sending the hmac-verifiable accessLevel to the app for it
// to send back, as DeSo did. I don't get it, it's overly complicated. We can
// just check the permissions based on what's in localStorage.
//
// Though, maybe this was for the sake of Safari where localStorage doesn't
// work? We'll see I guess.
//
// accessLevel: AccessLevel;
// accessLevelHmac: string;
} }
export enum AccessLevel { export enum AccessLevel {
@ -38,3 +83,9 @@ export enum AccessLevel {
// Node can sign all transactions without approval // Node can sign all transactions without approval
Full = 4, Full = 4,
} }
export enum ActionType {
Action = 0,
Transaction = 1,
// TODO - probably gets a lot more detailed than this
}