uchaindb/UClient

View on GitHub
src/ClientApp/app/services/chain-db.service.ts

Summary

Maintainability
F
5 days
Test Coverage
import { Injectable, Injector } from '@angular/core';
import { Observable } from "rxjs/Observable";
import 'rxjs/add/observable/of';
import { ConfigurationService } from "./configuration.service";
import { EndpointFactory } from "./endpoint-factory.service";
import { Http, Headers, Response, RequestOptions } from "@angular/http";
import { Router } from "@angular/router";
import { Pager } from "../models/pager.model";
import { ChainDb, HistoryEntry, QueryTableResponse, RowDef, ColumnDef, Tx, QueryCellResponse, DataAction, StatusRpcResponse, ListTablesRpcResponse, QueryDataRpcResponse, QueryChainRpcResponse, QueryCellRpcResponse, CreateTxRpcResponse, ListTableSchema, ColumnData, SchemaAction, SchemaColumnDefinition, LockTarget, ChainDbRpcMethod } from '../models/chain-db.model';
import { LocalStoreManager } from './local-store-manager.service';
import { CryptographyService } from './cryptography.service';
import { NotificationService } from './notification.service';
import { Signature, PrivateKey, Address } from '../models/cryptography.model';
import { B58 } from './b58';

@Injectable()
export class ChainDbService extends EndpointFactory {
    private readonly _baseUrl: string = "";
    get baseUrl() { return this.configurations.baseUrl + this._baseUrl; }

    public static readonly DBKEY_CHAIN_DB_DATA = "chain_db";

    constructor(
        http: Http,
        configurations: ConfigurationService,
        injector: Injector,
        private localStoreManager: LocalStoreManager,
        private cryptoService: CryptographyService,
        private notificationService: NotificationService,
    ) {
        super(http, configurations, injector);
    }

    getDbList(pager?: Pager): Observable<Array<ChainDb>> {
        if (!this.localStoreManager.exists(ChainDbService.DBKEY_CHAIN_DB_DATA)) {
            this.localStoreManager.savePermanentData([], ChainDbService.DBKEY_CHAIN_DB_DATA);
        }
        var dblist = this.localStoreManager.getData(ChainDbService.DBKEY_CHAIN_DB_DATA);
        return Observable.of(dblist);
    }

    setDbEditMode(dbid: string, edit = true): void {
        var dblist = this.localStoreManager.getData(ChainDbService.DBKEY_CHAIN_DB_DATA) as Array<ChainDb>;
        var idx = dblist.findIndex(_ => _.id == dbid);
        if (idx == -1) {
            console.warn("setDbEditMode failed due to cannot find dbid");
            return;
        }

        dblist[idx].editmode = edit;
        this.localStoreManager.savePermanentData(dblist, ChainDbService.DBKEY_CHAIN_DB_DATA);
    }

    getRecommendDbList(pager?: Pager): Observable<Array<ChainDb>> {
        var dblist: Array<ChainDb> = [
            {
                id: "1",
                name: "备份灯火计划捐赠数据",
                description: "由。。。运营,保存有从xxx开始的数据,为实时数据,接受大众监督。由。。。运营,保存有从xxx开始的数据,为实时数据,接受大众监督。由。。。运营,保存有从xxx开始的数据,为实时数据,接受大众监督。",
                address: "http://localhost:7847/api/rpc",
                image: "https://placeimg.com/200/200/any",
                staffpick: 1,
            },
            {
                id: "2",
                name: "XXXX捐赠数据",
                description: "由。。。运营,保存有从xxx开始的数据,为实时数据,接受大众监督。由。。。运营,保存有从xxx开始的数据,为实时数据,接受大众监督。由。。。运营,保存有从xxx开始的数据,为实时数据,接受大众监督。",
                address: "http://localhost:7848/api/rpc",
                image: "https://placeimg.com/100/100/any",
            },
        ];
        let endpointUrl = `${this.baseUrl}/database.json`;

        return this.http.get(endpointUrl)
            .map((response: Response) => response.json())
            .catch(error => Observable.of(dblist));
    }

    addChainDb(db: ChainDb): Observable<boolean> {
        if (!db) throw 'db not exist calling addChainDb';
        var dblist = this.localStoreManager.getData(ChainDbService.DBKEY_CHAIN_DB_DATA) as Array<ChainDb>;
        if (dblist.findIndex(_ => _.id == db.id) > -1) return Observable.of(false);
        dblist.push(db);
        this.localStoreManager.savePermanentData(dblist, ChainDbService.DBKEY_CHAIN_DB_DATA);

        return Observable.of(true);
    }

    removeChainDb(db: ChainDb): void {
        if (!db) throw 'db not exist calling removeChainDb';
        var dblist = this.localStoreManager.getData(ChainDbService.DBKEY_CHAIN_DB_DATA) as Array<ChainDb>;
        var idx = dblist
            .findIndex(_ => _.id == db.id);
        dblist.splice(idx, 1);
        this.localStoreManager.savePermanentData(dblist, ChainDbService.DBKEY_CHAIN_DB_DATA);
    }

    getChainDbStatus(db: ChainDb): Observable<StatusRpcResponse> {
        return this.rpcCall(db.address, "Status", []);
    }

    getChainDb(dbid: string): Observable<ChainDb> {
        var dblist = this.localStoreManager.getData(ChainDbService.DBKEY_CHAIN_DB_DATA) as Array<ChainDb>;
        var cdb = dblist && dblist.find(_ => _.id == dbid);
        if (cdb) return Observable.of(cdb);
        // add db from recommend list if not exist
        return this.getRecommendDbList()
            .switchMap(cdblist => {
                var ncdb = cdblist.find(_ => _.id == dbid);
                if (ncdb) {
                    return this.addChainDb(ncdb)
                        .map(result => result ? ncdb : null);
                }

                return Observable.of(null);
            });
    }

    getChainDbTableNames(db: ChainDb): Observable<ListTablesRpcResponse> {
        return this.rpcCall(db.address, "ListTables", []);
    }

    getChainDbTable(db: ChainDb, tableName: string, start: number = 0, size: number = 100): Observable<QueryTableResponse> {
        return this.rpcCall(db.address, "QueryData", [tableName, start, size, "", ""]).
            map((_: QueryDataRpcResponse) => {
                let dataHist = _.DataHistories || [];
                let pkname = _.PrimaryKeyName;

                let columns: Array<ColumnDef> = [];
                let headers = _.Headers;
                let pkidx = headers.findIndex(_ => _ == pkname);
                let colCount = headers.length;
                for (let i = 0; i < colCount; i++) {
                    //let hist = allHist[colHist[i]];
                    columns.push({
                        name: headers[i],
                        //tran: hist && hist.TxHash,
                        //history: hist && hist.HistoryLength,
                    });
                }

                let rows: Array<RowDef> = [];
                let data = _.Data;
                let rowCount = data.length / colCount;
                for (let i = 0; i < rowCount; i++) {
                    let row: RowDef = [];
                    for (let j = 0; j < colCount; j++) {
                        let hist = dataHist[i * colCount + j];
                        let pkval = data[i * colCount + pkidx];
                        row.push({
                            name: headers[j],
                            pkval: pkval,
                            data: data[i * colCount + j],
                            history: hist,
                        });
                    }
                    rows.push(row);
                }

                return {
                    data: {
                        rows: rows,
                        columns: columns,
                        pkname: pkname,
                        dbid: db.id,
                        tableName: tableName,
                    },
                    cursorId: _.CursorId,
                }
            });
    }

    getQueryChain(db: ChainDb, mixId: string): Observable<QueryChainRpcResponse> {
        if (!db) throw 'db not exist calling getQueryChain';
        return this.rpcCall(db.address, "QueryChain", [mixId]);
    }

    getQueryCell(db: ChainDb, tableName: string, primaryKeyValue: string, columnName: string, columns: string[]): Observable<QueryCellResponse> {
        if (!db) throw 'db not exist calling getQueryCell';
        columns = columns || [];
        return this.rpcCall(db.address, "QueryCell", [tableName, primaryKeyValue, columnName, ...columns])
            .map((_: QueryCellRpcResponse) => {
                let row: RowDef = [];
                let headers = _.Headers;
                let data = _.Row;
                let datahist = _.RowHistories;
                if (!data || !datahist) throw "unexpected data";
                let pkname = _.PrimaryKeyName;
                let pkidx = headers.findIndex(_ => _ == pkname);
                let pkval = data[pkidx];
                let colCount = headers.length;
                for (let i = 0; i < colCount; i++) {
                    let hist = datahist[i];
                    row.push({
                        name: headers[i],
                        pkval: pkval,
                        data: data[i],
                        tran: hist && hist.TxHash,
                        history: hist && hist.HistoryLength,
                    });
                }
                let rows = [row];
                let columns = row.map(r => ({ name: r.name, tran: null, history: null }));

                let txs = (_.Txs || [])
                    .map(_ => new Tx({ Hash: _ }));
                return {
                    data: {
                        rows: rows,
                        columns: columns,
                        pkname: pkname,
                        dbid: db.id,
                        tableName: tableName,
                    },
                    txs: txs,
                }
            });
    }

    createDataTx(db: ChainDb, privateKey: PrivateKey, unlockPrivateKey: PrivateKey, actions: Array<DataAction>): Observable<CreateTxRpcResponse> {
        let pubKey = this.cryptoService.getPublicKey(privateKey);
        let initiator = pubKey.toAddress();

        return this.getChainDbStatus(db)
            .flatMap(result => {
                let witness = result.Tail.Hash;
                let unlockScripts = this.generateUnlockScriptsForDataTx(unlockPrivateKey, initiator, witness, actions);
                let hashContent = this.getDataTxHashContent(initiator, witness, actions, unlockScripts);
                let sig = this.signTx(privateKey, hashContent);
                let as = actions.map(_ => JSON.stringify(_));
                return this.rpcCall(db.address, "CreateDataTx", [initiator.toB58String(), sig, witness, unlockScripts, ...as]);
            });
    }

    createSchemaTx(db: ChainDb, privateKey: PrivateKey, unlockPrivateKey: PrivateKey, actions: Array<SchemaAction>): Observable<CreateTxRpcResponse> {
        let pubKey = this.cryptoService.getPublicKey(privateKey);
        let initiator = pubKey.toAddress();

        return this.getChainDbStatus(db)
            .flatMap(result => {
                let witness = result.Tail.Hash;
                let unlockScripts = this.generateUnlockScriptsForSchemaTx(unlockPrivateKey, initiator, witness, actions);
                let hashContent = this.getSchemaTxHashContent(initiator, witness, actions, unlockScripts);
                let sig = this.signTx(privateKey, hashContent);
                let as = actions.map(_ => JSON.stringify(_));
                return this.rpcCall(db.address, "CreateSchemaTx", [initiator.toB58String(), sig, witness, unlockScripts, ...as]);
            });
    }

    createLockTx(db: ChainDb, privateKey: PrivateKey, unlockPrivateKey: PrivateKey, lockScripts: string, targets: Array<LockTarget>): Observable<CreateTxRpcResponse> {
        let pubKey = this.cryptoService.getPublicKey(privateKey);
        let initiator = pubKey.toAddress();

        return this.getChainDbStatus(db)
            .flatMap(result => {
                let witness = result.Tail.Hash;
                let unlockScripts = this.generateUnlockScriptsForLockTx(unlockPrivateKey, initiator, witness, lockScripts, targets);
                let hashContent = this.getLockTxHashContent(initiator, witness, lockScripts, targets, unlockScripts);
                let sig = this.signTx(privateKey, hashContent);
                let as = targets.map(_ => JSON.stringify(_));
                return this.rpcCall(db.address, "CreateLockTx", [initiator.toB58String(), sig, witness, unlockScripts, lockScripts, ...as]);
            });
    }

    private generateUnlockScriptsForDataTx(privateKey: PrivateKey, initiator: Address, witness: string, actions: Array<DataAction>): string {
        if (!privateKey) return null;
        let hashContent = this.getDataTxHashContent(initiator, witness, actions);
        return this.generateUnlockScriptsForTx(privateKey, hashContent);
    }

    private generateUnlockScriptsForSchemaTx(privateKey: PrivateKey, initiator: Address, witness: string, actions: Array<SchemaAction>): string {
        if (!privateKey) return null;
        let hashContent = this.getSchemaTxHashContent(initiator, witness, actions);
        return this.generateUnlockScriptsForTx(privateKey, hashContent);
    }

    private generateUnlockScriptsForLockTx(privateKey: PrivateKey, initiator: Address, witness: string, lockScripts: string, targets: Array<LockTarget>): string {
        if (!privateKey) return null;
        let hashContent = this.getLockTxHashContent(initiator, witness, lockScripts, targets);
        return this.generateUnlockScriptsForTx(privateKey, hashContent);
    }

    private generateUnlockScriptsForTx(privateKey: PrivateKey, hashContent: string): string {
        let tosign = this.cryptoService.hash(hashContent);
        let signature = this.cryptoService.sign(tosign, privateKey);
        return this.getSignatureB58(signature);
    }

    private signTx(privateKey: PrivateKey, hashContent: string): string {
        let signature = this.cryptoService.sign(hashContent, privateKey);
        return this.getSignatureB58(signature);
    }

    private getSignatureB58(signature: Signature): string {
        let sigarr = new Uint8Array(64);
        sigarr.set(signature.r);
        sigarr.set(signature.s, 32);
        let sig = B58.toB58(sigarr);
        return sig;
    }

    private getDataTxHashContent(initiator: Address, witness: string, actions: Array<DataAction>, unlockScripts: string = null): string {
        let mapColumns = (columns: Array<ColumnData>): Array<string> =>
            columns.map(_ => `${_.Name}:${_.Data}`);
        let acts = actions
            .map(_ => {
                switch (_.Type) {
                    case "InsertDataAction":
                        return `[${_.SchemaName}]Insert:${mapColumns(_.Columns).join(",")}`;
                    case "UpdateDataAction":
                        return `[${_.SchemaName}]Update[${_.PrimaryKeyValue}]:${mapColumns(_.Columns).join(",")}`;
                    case "DeleteDataAction":
                        return `[${_.SchemaName}]Delete[${_.PrimaryKeyValue}]`;
                    default:
                }
            });
        let unlockContent = unlockScripts ? unlockScripts + "|" : "";
        return `${unlockContent}${initiator.toB58String()}|${witness}|${acts.join(",")}`
    }

    private getSchemaTxHashContent(initiator: Address, witness: string, actions: Array<SchemaAction>, unlockScripts: string = null): string {
        let mapColumns = (columns: Array<SchemaColumnDefinition>): Array<string> =>
            !columns ? []
                : columns.map(_ => `${(_.PrimaryKey ? '[P]' : '')}${_.Name}:${_.Type}`);
        let acts = actions
            .map(_ => {
                switch (_.Type) {
                    case "CreateSchemaAction":
                        return `[${_.Name}]CreateColumns:${mapColumns(_.Columns).join(",")}`;
                    case "ModifySchemaAction":
                        return `[${_.Name}]DropColumns:${(_.DropColumns || []).join(",")};AddOrModifyColumns:${mapColumns(_.AddOrModifyColumns).join(",")}`;
                    case "DropSchemaAction":
                        return `[${_.Name}]DropSchema`;
                    default:
                }
            });
        let unlockContent = unlockScripts ? unlockScripts + "|" : "";
        return `${unlockContent}${initiator.toB58String()}|${witness}|${acts.join(",")}`
    }

    private getLockTxHashContent(initiator: Address, witness: string, lockScripts: string, targets: Array<LockTarget>, unlockScripts: string = null): string {
        let mapColumns = (columns: Array<LockTarget>): Array<string> =>
            columns.map(_ => `[${_.TargetType}][${_.PublicPermission}]${(!_.TableName ? '' : _.TableName)}:${(!_.PrimaryKey ? '' : _.PrimaryKey)}:${(!_.ColumnName ? '' : _.ColumnName)}`);
        let unlockContent = unlockScripts ? unlockScripts + "|" : "";
        return `${unlockContent}${initiator.toB58String()}|${witness}|${lockScripts ? lockScripts : ''}|${mapColumns(targets).join(",")}`
    }

    readonly errorCodes = {
        '-32700': 'JSON-RPC server reported a parse error in JSON request',
        '-32600': 'JSON-RPC server reported an invalid request',
        '-32601': 'Method not found',
        '-32602': 'Invalid parameters',
        '-32603': 'Internal error'
    };

    rpcCall(url: string, method: ChainDbRpcMethod, params: Array<string | number>): Observable<any> {
        let jsonParams = {
            jsonrpc: '2.0',
            id: 1,//(new Date).getTime(),
            method: method,
            params: params
        };
        let requestString = JSON.stringify(jsonParams);
        return this.http.post(url, requestString)
            .map((response: Response) => {
                console.debug(response);
                let decodedResponse = response.json();
                if (decodedResponse.error) {
                    let errorMessage = this.errorCodes[decodedResponse.error.code];
                    errorMessage += " " + decodedResponse.error.message;
                    throw new Error(errorMessage);
                }
                return decodedResponse.result;
            })
            .catch(error => this.handleError(error, () => this.rpcCall(url, method, params)));
    }

}