From 3bfd19283724576cf00cc1c5c62117bad9a44d6e Mon Sep 17 00:00:00 2001 From: Matus Zamborsky Date: Thu, 12 Apr 2018 17:42:27 +0200 Subject: [PATCH] add DDO attributes and claims --- package.json | 2 + .../{ontIdClaims.tsx => ontIdAttributes.tsx} | 18 ++-- src/ontIds/ontIdDdoAttributes.tsx | 70 ++++++++++++ src/ontIds/ontIdDdoClaims.tsx | 81 ++++++++++++++ src/ontIds/ontIdDetail.ts | 16 ++- src/ontIds/ontIdDetailView.tsx | 22 +++- src/server/ingestApi.ts | 101 ++++++++++++------ src/shared/ddoApi.ts | 67 ++++++++++++ src/shared/ont/model.ts | 20 +++- yarn.lock | 25 ++++- 10 files changed, 377 insertions(+), 45 deletions(-) rename src/ontIds/{ontIdClaims.tsx => ontIdAttributes.tsx} (81%) create mode 100644 src/ontIds/ontIdDdoAttributes.tsx create mode 100644 src/ontIds/ontIdDdoClaims.tsx create mode 100644 src/shared/ddoApi.ts diff --git a/package.json b/package.json index d8f5b19..a0a1292 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "ont-sdk-ts": "backslash47/ontology-ts-sdk#exported", "query-string": "^6.0.0", "react": "^16.2.0", + "react-copy-to-clipboard": "^5.0.1", "react-dom": "^16.2.0", "react-router-dom": "^4.2.2", "react-scripts-ts": "2.14.0", @@ -49,6 +50,7 @@ "@types/react": "^16.0.40", "@types/react-dom": "^16.0.4", "@types/react-router-dom": "^4.2.5", + "@types/react-copy-to-clipboard": "^4.2.5", "@types/recompose": "^0.24.6", "@types/ws": "^4.0.2", "typescript": "^2.7.2" diff --git a/src/ontIds/ontIdClaims.tsx b/src/ontIds/ontIdAttributes.tsx similarity index 81% rename from src/ontIds/ontIdClaims.tsx rename to src/ontIds/ontIdAttributes.tsx index e99e184..de63452 100644 --- a/src/ontIds/ontIdClaims.tsx +++ b/src/ontIds/ontIdAttributes.tsx @@ -23,10 +23,10 @@ import { Segment, Table, Popup } from 'semantic-ui-react'; import { Claim } from '~/shared/ont/model'; type Props = { - claims: Claim[] + attributes: Claim[] }; -const OntIds: React.SFC = (props) => ( +const OntIdAttributes: React.SFC = (props) => ( @@ -37,19 +37,19 @@ const OntIds: React.SFC = (props) => ( - {props.claims.map(claim => ( - + {props.attributes.map(attr => ( + - + {distanceInWordsToNow(claim.Timestamp)}} + trigger={{distanceInWordsToNow(attr.Timestamp)}} > - {format(claim.Timestamp, 'MMM Do YYYY HH:mm:ss')} + {format(attr.Timestamp, 'MMM Do YYYY HH:mm:ss')} - {claim.Attribute} + {attr.Attribute} ))} @@ -59,4 +59,4 @@ const OntIds: React.SFC = (props) => ( ); -export default OntIds; +export default OntIdAttributes; diff --git a/src/ontIds/ontIdDdoAttributes.tsx b/src/ontIds/ontIdDdoAttributes.tsx new file mode 100644 index 0000000..a87d4f2 --- /dev/null +++ b/src/ontIds/ontIdDdoAttributes.tsx @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2018 Matus Zamborsky + * This file is part of The ONT Detective. + * + * The ONT Detective is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The ONT Detective is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with The ONT Detective. If not, see . + */ + +import * as React from 'react'; +import { Segment, Table, Popup, Button, Icon } from 'semantic-ui-react'; +import * as CopyToClipboard from 'react-copy-to-clipboard'; +import { DdoAttribute } from '~/shared/ont/model'; + +type Props = { + attributes: DdoAttribute[] +}; + +const DdoAttributes: React.SFC = (props) => ( + + +
+ + + Id + Type + Value + + + + {props.attributes.map(attr => ( + + + {attr.Id} + + + {attr.Type} + + + + + + )} + > + {attr.Value} + + + + + ))} + +
+
+
+); + +export default DdoAttributes; diff --git a/src/ontIds/ontIdDdoClaims.tsx b/src/ontIds/ontIdDdoClaims.tsx new file mode 100644 index 0000000..e7b0060 --- /dev/null +++ b/src/ontIds/ontIdDdoClaims.tsx @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 Matus Zamborsky + * This file is part of The ONT Detective. + * + * The ONT Detective is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The ONT Detective is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with The ONT Detective. If not, see . + */ + +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { distanceInWordsToNow, format } from 'date-fns'; +import { Segment, Table, Popup, Icon, Button } from 'semantic-ui-react'; +import * as CopyToClipboard from 'react-copy-to-clipboard'; +import { DdoClaim } from '~/shared/ont/model'; + +type Props = { + claims: DdoClaim[] +}; + +const DdoClaims: React.SFC = (props) => ( + + + + + + Issuer + Created + Content + + + + {props.claims.map(claim => ( + + + {claim.Issuer !== null ? ( + {claim.Issuer} + ) : null} + + + {claim.Timestamp !== null ? ( + {distanceInWordsToNow(claim.Timestamp)}} + > + {format(claim.Timestamp, 'MMM Do YYYY HH:mm:ss')} + + ) : null} + + + {claim.Content !== null ? ( + + + + )} + > + {claim.Content} + + ) : null} + + + ))} + +
+
+
+); + +export default DdoClaims; diff --git a/src/ontIds/ontIdDetail.ts b/src/ontIds/ontIdDetail.ts index 37cd813..b0c2e47 100644 --- a/src/ontIds/ontIdDetail.ts +++ b/src/ontIds/ontIdDetail.ts @@ -21,7 +21,8 @@ import { match } from 'react-router'; import { Location } from 'history'; import { StateSetter } from '~/utils'; import { getOntId } from '~/shared/ontIdApi'; -import { OntId } from '~/shared/ont/model'; +import { getDdo } from '~/shared/ddoApi'; +import { OntId, Ddo } from '~/shared/ont/model'; import View from './ontIdDetailView'; interface PropsOuter { @@ -35,6 +36,7 @@ interface PropsOwn { interface State { ontId: OntId; + ddo?: Ddo; loaded: boolean; } @@ -56,6 +58,18 @@ export default compose( ontId, loaded: true }); + + // load ddo if possible + try { + const ddo = await getDdo(this.props.id); + + this.props.setState({ + ...this.props.state, + ddo + }); + } catch (e) { + console.log('Failed to load DDO', e); + } } }), flattenProp('state') diff --git a/src/ontIds/ontIdDetailView.tsx b/src/ontIds/ontIdDetailView.tsx index 76e88e4..6da0296 100644 --- a/src/ontIds/ontIdDetailView.tsx +++ b/src/ontIds/ontIdDetailView.tsx @@ -21,7 +21,9 @@ import { Link } from 'react-router-dom'; import { Breadcrumb, Segment, Table, Header, Popup, Loader } from 'semantic-ui-react'; import { distanceInWordsToNow, format } from 'date-fns'; import { PropsInner as Props } from './ontIdDetail'; -import OntIdClaims from './ontIdClaims'; +import OntIdAttributes from './ontIdAttributes'; +import OntIdDdoAttributes from './ontIdDdoAttributes'; +import OntIdDdoClaims from './ontIdDdoClaims'; const OntIdView: React.SFC = (props) => ( @@ -74,8 +76,22 @@ const OntIdView: React.SFC = (props) => ( {props.loaded && props.ontId.ClaimsCount > 0 ? ( -
Claims
- +
Attributes
+ +
+ ) : null} + + {props.loaded && props.ddo !== undefined && props.ddo.Attributes.length > 0 ? ( + +
DDO Attributes
+ +
+ ) : null} + + {props.loaded && props.ddo !== undefined && props.ddo.Claims.length > 0 ? ( + +
DDO Claims
+
) : null}
diff --git a/src/server/ingestApi.ts b/src/server/ingestApi.ts index de1b3fb..4e40f02 100644 --- a/src/server/ingestApi.ts +++ b/src/server/ingestApi.ts @@ -19,7 +19,6 @@ import ReconnectingWebSocket from './ReconnectingWebsocket'; import * as Html5WebSocket from 'html5-websocket'; import { find, get } from 'lodash'; -import axios from 'axios'; import { initAccountMappings, initTransferMappings, initOntIdMapping, initTxMappings } from '../shared/elastic/api'; import { Account, TxType, Transaction, BlockWrapper, Event, OntId } from '../shared/ont/model'; import { getAccount, indexAccount } from '../shared/accountsApi'; @@ -27,8 +26,9 @@ import { indexTransaction, getTransactions } from '../shared/transactionsApi'; import { indexTransfer } from '../shared/transfersApi'; import { indexBlock, getLastBlock } from '../shared/blocksApi'; import { indexOntId, getOntId } from '../shared/ontIdApi'; +import { getDdo } from '../shared/ddoApi'; -import { Token, utils, core, CONST } from 'ont-sdk-ts'; +import { Token, utils, core, DDO, CONST, Wallet, scrypt, TransactionBuilder, TxSender, OntidContract, Claim, Metadata, WebSocketClientApi, RestClient } from 'ont-sdk-ts'; import { Assets, OntIdAction, OntIdAttributeOperation, OntIdRegisterOperation } from '../const'; import { sleep } from '../utils'; @@ -139,7 +139,7 @@ async function ingestTransfer(transaction: Transaction, i: number, event: Event) await indexTransfer(transfer); } -async function ingestOntIdChange(transaction: Transaction, i: number, event: Event ): Promise { +async function ingestOntIdChange(transaction: Transaction, i: number, event: Event): Promise { const params: string[] = event.States[0] as string[]; @@ -228,16 +228,6 @@ export async function ingestBlock(block: BlockWrapper): Promise { await indexBlock(block.Header); } -function constructRequest(index: number): string { - const request = { - Action: 'getblockbyheight', - Version: '1.0.0', - Height: index - }; - - return JSON.stringify(request); -} - interface BlockResponse { Action: string; Desc: string; @@ -273,7 +263,7 @@ function fixEventResponse(response: EventResponse) { for (let state of result.States) { const params: string[] = state as string[]; - for(let i = 0; i < params.length; i++) { + for (let i = 0; i < params.length; i++) { params[i] = utils.hexstr2str(params[i]); } } @@ -287,16 +277,63 @@ function fixEventResponse(response: EventResponse) { } async function fetchEvents(txHash: string): Promise { - const func = '/api/v1/smartcode/event/txhash/'; - const url = `http://${CONST.TEST_NODE}:${CONST.HTTP_REST_PORT}${func}${txHash}`; + const client = new RestClient(CONST.TEST_ONT_URL.REST_URL); + const data = await client.getSmartCodeEvent(txHash); - const response = await axios.get(url); - const data = response.data; fixEventResponse(data); - return data.Result; } +export async function createOntId() { + const ontId = 'did:ont:THKWoVP247EHUNt8DFH3sj23TWGHvCYwFm'; + const privateKey = 'ccd14ca73dd2043401cd598b249a282aa202b6a7bcdc1c8108c3befb3774acae'; + + const tx = OntidContract.buildRegisterOntidTx(ontId, privateKey) + const param = TransactionBuilder.buildTxParam(tx); + console.log('sending: ', param); + + const txSender = new TxSender(CONST.TEST_ONT_URL.SOCKET_URL); + txSender.sendTxWithSocket(param, (res, socket) => { + console.log("receiving:", JSON.stringify(res)); + }); +} + +export async function createOntClaim() { + const ontId = 'did:ont:THKWoVP247EHUNt8DFH3sj23TWGHvCYwFm'; + const privateKey = 'ccd14ca73dd2043401cd598b249a282aa202b6a7bcdc1c8108c3befb3774acae'; + + const context = 'claim:standard0001'; + const claimData = { + test: 'backslash' + }; + + let date = (new Date()).toISOString() + if(date.indexOf('.') > -1) { + date = date.substring(0, date.indexOf('.')) + 'Z' + } + + const metadata = new Metadata(); + metadata.CreateTime = date; + metadata.Issuer = ontId; + metadata.Subject = ontId; + + const claim = new Claim(context, claimData, metadata) + claim.sign(privateKey); + + const type = utils.str2hexstr('Json') + const value = utils.str2hexstr(JSON.stringify(claim)); + + const tx = OntidContract.buildAddAttributeTx(utils.str2hexstr('claim:' + claim.Id), value, type, ontId, privateKey); + const param = TransactionBuilder.buildTxParam(tx); + console.log('sending: ', param); + //send the transaction + + const txSender = new TxSender(CONST.TEST_ONT_URL.SOCKET_URL); + txSender.sendTxWithSocket(param, (res, socket) => { + console.log("receiving:", JSON.stringify(res)); + }); +} + // function constructHeartBeat(): string { // const request = { // Action: 'heartbeat', @@ -315,7 +352,7 @@ async function fetchEvents(txHash: string): Promise { // const request = { // Action: 'getsmartcodeeventbyhash', // Version: '1.0.0', -// Hash: '45515b81017c34911e0bf0f4082d04fb8de2a5140311a1fc79867fa732009750' +// Hash: '473fdea5859f719814f05b67116252046369236799500bc4b698ff77994707ce' // }; // return JSON.stringify(request); @@ -323,15 +360,19 @@ async function fetchEvents(txHash: string): Promise { export async function ingestBlocks(): Promise { const socket = CONST.TEST_ONT_URL.SOCKET_URL; - const ws = new ReconnectingWebSocket(socket, undefined, {constructor: Html5WebSocket}); - + const ws = new ReconnectingWebSocket(socket, undefined, { constructor: Html5WebSocket }); + let lastBlock = await getLastBlock(); - let last = lastBlock ? lastBlock.Height : -1; + let last = 186162; // lastBlock ? lastBlock.Height : -1; let working: number | null = null; + //const resp = await fetchEvents('473fdea5859f719814f05b67116252046369236799500bc4b698ff77994707ce'); + //console.log(JSON.stringify(resp)); + const builder = new WebSocketClientApi(); + ws.onopen = function open() { console.log('Websocket connected. Starting from ', last + 1); - ws.send(constructRequest(last + 1)); + ws.send(builder.getBlockJson(last + 1)); }; ws.onclose = function close(event: {}) { @@ -344,24 +385,24 @@ export async function ingestBlocks(): Promise { if (response.Desc == 'SUCCESS') { const block = response.Result; working = block.Header.Height; - + console.log('Ingesting block: ', working); await ingestBlock(block); - + last = working; - ws.send(constructRequest(working + 1)); - + ws.send(builder.getBlockJson(working + 1)); + } else if (response.Desc === 'UNKNOWN BLOCK') { console.log('No new block, waiting 3 seconds.'); await sleep(3000); - ws.send(constructRequest(last + 1)); + ws.send(builder.getBlockJson(last + 1)); } else { console.log('Received error:', response); } }; - ws.onerror = function(event) { + ws.onerror = function (event) { console.log(event); } } diff --git a/src/shared/ddoApi.ts b/src/shared/ddoApi.ts new file mode 100644 index 0000000..dd31ece --- /dev/null +++ b/src/shared/ddoApi.ts @@ -0,0 +1,67 @@ +import { get } from 'lodash'; +import axios, { AxiosResponse } from 'axios'; +import { TransactionBuilder, OntidContract, CONST, DDO as OntDdo } from 'ont-sdk-ts'; +import { DdoAttribute, Ddo, DdoClaim } from './ont/model'; + +interface DdoResponse { + Action: string; + Desc: string; + Error: number; + Result: string | null; +} + +function convertISODate(str: string): number { + return Date.parse(str); +} + +function parse(ddoStr: string): Ddo { + const ontDdo = OntDdo.deserialize(ddoStr); + const Attributes: DdoAttribute[] = []; + const Claims: DdoClaim[] = []; + + ontDdo.attributes.forEach(ontAtr => { + Attributes.push({ + Id: ontAtr.path, + Type: ontAtr.type, + Value: ontAtr.value + }); + + if (ontAtr.type.toLowerCase() === 'json') { + const attrValue = JSON.parse(ontAtr.value); + + if (get(attrValue, 'Metadata') !== undefined) { + + const Issuer: string | null = get(attrValue, 'Metadata.Issuer', null); + const TimestampStr: string | null = get(attrValue, 'Metadata.CreateTime', null); + const ContentObj: object | null = get(attrValue, 'Content', null); + + const Timestamp: number | null = TimestampStr !== null ? convertISODate(TimestampStr) : null; + const Content: string | null = ContentObj !== null ? JSON.stringify(ContentObj) : null; + + Claims.push({ + Id: ontAtr.path, + Issuer, + Timestamp, + Content + }); + } + } + }); + + return { + Attributes, + Claims + }; +} + +export async function getDdo(ontId: string): Promise { + + let tx = OntidContract.buildGetDDOTx(ontId); + let param = TransactionBuilder.buildRestfulParam(tx); + + const url = TransactionBuilder.sendRawTxRestfulUrl(CONST.TEST_ONT_URL.REST_URL, true); + + const res: AxiosResponse = await axios.post(url, param); + + return parse(res.data.Result[0]); +} diff --git a/src/shared/ont/model.ts b/src/shared/ont/model.ts index 7092e6c..853e5b4 100644 --- a/src/shared/ont/model.ts +++ b/src/shared/ont/model.ts @@ -172,6 +172,24 @@ export type OntId = { RegistrationTimestamp: number; LastTimestamp: number; LastTxHash: string; - Claims: Claim[]; + Claims: Claim[]; // it is not really claims but attributes ClaimsCount: number; }; + +export type DdoClaim = { + Id: string; + Issuer: string | null; + Timestamp: number | null; + Content: string | null; +}; + +export type DdoAttribute = { + Id: string; + Type: string; + Value: string; +}; + +export type Ddo = { + Attributes: DdoAttribute[]; + Claims: DdoClaim[]; +}; diff --git a/yarn.lock b/yarn.lock index e1b247c..d504e2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -93,6 +93,12 @@ version "5.1.0" resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-5.1.0.tgz#7f40cdea49ddafa0ea4f3db35fb6c24d3bfd4dcc" +"@types/react-copy-to-clipboard@^4.2.5": + version "4.2.5" + resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.5.tgz#bda288b4256288676019b75ca86f1714bbd206d4" + dependencies: + "@types/react" "*" + "@types/react-dom@^16.0.4": version "16.0.4" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.4.tgz#2e8fd45f5443780ed49bf2cdd9809e6091177a7d" @@ -1888,6 +1894,12 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" +copy-to-clipboard@^3: + version "3.0.8" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9" + dependencies: + toggle-selection "^1.0.3" + core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" @@ -5894,7 +5906,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0: +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0: version "15.6.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" dependencies: @@ -6044,6 +6056,13 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-copy-to-clipboard@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e" + dependencies: + copy-to-clipboard "^3" + prop-types "^15.5.8" + react-dev-utils@4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-4.2.1.tgz#9f2763e7bafa1a1b9c52254d2a479deec280f111" @@ -7266,6 +7285,10 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toggle-selection@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + toposort@^1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec"