diff --git a/app/ResolveRoute.js b/app/ResolveRoute.js
index fd9ec96..d057967 100644
--- a/app/ResolveRoute.js
+++ b/app/ResolveRoute.js
@@ -1,14 +1,18 @@
export const routeRegex = {
UserProfile1: /^\/(@[\w\.\d-]+)\/?$/,
- UserProfile2: /^\/(@[\w\.\d-]+)\/(transfers|assets|create-asset|invites|curation-rewards|author-rewards|donates-from|donates-to|filled-orders|permissions|created|password|witness|settings)\/??(?:&?[^=&]*=[^=&]*)*$/,
+ UserProfile2: /^\/(@[\w\.\d-]+)\/(transfers|assets|create-asset|invites|curation-rewards|author-rewards|donates-from|donates-to|nft-history|nft-tokens|nft-collections|filled-orders|permissions|created|password|witness|settings)\/??(?:&?[^=&]*=[^=&]*)*$/,
UserProfile3: /^\/(@[\w\.\d-]+)\/[\w\.\d-]+/,
+ UserNFTEndPoints: /^\/(@[\w\.\d-]+)\/nft-tokens\/([\w\d.-]+)\/?$/,
UserAssetEndPoints: /^\/(@[\w\.\d-]+)\/assets\/([\w\d.-]+)\/(update|transfer)$/,
- UserEndPoints: /^(transfers|assets|create-asset|invites|curation-rewards|author-rewards|donates-from|donates-to|filled-orders|permissions|created|password|witness|settings)$/,
+ UserEndPoints: /^(transfers|assets|create-asset|invites|curation-rewards|author-rewards|donates-from|donates-to|nft-history|nft-tokens|nft-collections|filled-orders|permissions|created|password|witness|settings)$/,
WorkerSort: /^\/workers\/([\w\d\-]+)\/?($|\?)/,
WorkerSearchByAuthor: /^\/workers\/([\w\d\-]+)\/(\@[\w\d.-]+)\/?($|\?)/,
WorkerRequest: /^\/workers\/([\w\d\-]+)\/(\@[\w\d.-]+)\/([\w\d-]+)\/?($|\?)/,
MarketPair: /^\/market\/([\w\d\.]+)\/([\w\d.]+)\/?($|\?)/,
ConvertPair: /^\/convert\/([\w\d\.]+)\/([\w\d.]+)\/?($|\?)/,
+ NFTCollection: /^\/nft-collections\/([\w\d\.]+)\/?($|\?)/,
+ NFTToken: /^\/nft-tokens\/([\w\d\.]+)\/?($|\?)/,
+ NFTMarket: /^\/nft\/([\w\d\.]+)\/?($|\?)/,
UserJson: /^\/(@[\w\.\d-]+)(\.json)$/,
UserNameJson: /^.*(?=(\.json))/
};
@@ -72,6 +76,10 @@ export default function resolveRoute(path)
if (match) {
return {page: 'Workers', params: match.slice(1)};
}
+ match = path.match(routeRegex.UserNFTEndPoints)
+ if (match) {
+ return {page: 'UserProfile', params: [match[1], 'nft-tokens', match[2]]}
+ }
match = path.match(routeRegex.UserAssetEndPoints);
if (match) {
return {page: 'UserProfile', params: [match[1], 'assets', match[2], match[3]]};
@@ -89,5 +97,20 @@ export default function resolveRoute(path)
if (match) {
return {page: 'ConvertAssetsPage', params: match.slice(1)}
}
+ match = path.match(routeRegex.NFTCollection)
+ if (match) {
+ return {page: 'NFTCollectionPage', params: match.slice(1)}
+ }
+ match = path.match(routeRegex.NFTToken)
+ if (match) {
+ return {page: 'NFTTokenPage', params: match.slice(1)}
+ }
+ if (path === '/nft') {
+ return {page: 'NFTMarketPage'}
+ }
+ match = path.match(routeRegex.NFTMarket)
+ if (match) {
+ return {page: 'NFTMarketPage', params: match.slice(1)}
+ }
return {page: 'NotFound'};
}
diff --git a/app/RootRoute.js b/app/RootRoute.js
index bd2cad2..77d8721 100644
--- a/app/RootRoute.js
+++ b/app/RootRoute.js
@@ -33,6 +33,12 @@ export default {
cb(null, [require('@pages/UserProfile')]);
} else if (route.page === 'ConvertAssetsPage') {
cb(null, [require('@pages/ConvertAssetsPage')]);
+ } else if (route.page === 'NFTCollectionPage') {
+ cb(null, [require('@pages/nft/NFTCollectionPage')]);
+ } else if (route.page === 'NFTTokenPage') {
+ cb(null, [require('@pages/nft/NFTTokenPage')]);
+ } else if (route.page === 'NFTMarketPage') {
+ cb(null, [require('@pages/nft/NFTMarketPage')]);
} else if (route.page === 'Market') {
cb(null, [require('@pages/MarketLoader')]);
} else if (route.page === 'Rating') {
diff --git a/app/assets/images/nft.png b/app/assets/images/nft.png
new file mode 100644
index 0000000..db3321a
Binary files /dev/null and b/app/assets/images/nft.png differ
diff --git a/app/components/all.scss b/app/components/all.scss
index d85eb42..fb72464 100644
--- a/app/components/all.scss
+++ b/app/components/all.scss
@@ -39,6 +39,9 @@
@import "./elements/market/MarketPair";
@import "./elements/market/OrderForm";
@import "./elements/market/TickerPriceStat";
+@import "./elements/nft/NFTSmallIcon";
+@import "./elements/nft/NFTMarketCollections";
+@import "./elements/nft/NFTTokenItem";
@import "./elements/workers/WorkerRequestVoting";
// dialogs
@@ -67,6 +70,12 @@
@import "./modules/Powerdown.scss";
@import "./modules/QuickBuy.scss";
@import "./modules/Modals";
+@import "./modules/nft/NFTCollections";
+@import "./modules/nft/CreateNFTCollection";
+@import "./modules/nft/IssueNFTToken";
+@import "./modules/nft/NFTTokens";
+@import "./modules/nft/NFTTokenSell";
+@import "./modules/nft/NFTTokenTransfer";
// pages
@import "./pages/Exchanges";
@@ -76,3 +85,6 @@
@import "./pages/Rating";
@import "./pages/UserProfile";
@import "./pages/Witnesses";
+@import "./pages/nft/NFTCollectionPage";
+@import "./pages/nft/NFTMarketPage";
+@import "./pages/nft/NFTTokenPage";
diff --git a/app/components/cards/TransferHistoryRow.jsx b/app/components/cards/TransferHistoryRow.jsx
index 302ab55..fdbc8ff 100644
--- a/app/components/cards/TransferHistoryRow.jsx
+++ b/app/components/cards/TransferHistoryRow.jsx
@@ -29,11 +29,38 @@ class TransferHistoryRow extends React.Component {
/* all transfers involve up to 2 accounts, context and 1 other. */
let description_start = "";
let link = null, linkTitle = null, linkExternal = false
+ let description_middle = ''
+ let link2 = null, linkTitle2 = null, linkExternal2 = false
+ let description_middle2 = ''
+ let link3 = null, linkTitle3 = null, linkExternal3 = false
let code_key = "";
let description_end = "";
+ let link4 = null, linkTitle4 = null, linkExternal4 = false
let target_hint = "";
let data_memo = data.memo;
+ const getToken = (token_id) => {
+ const { nft_tokens } = this.props
+ let tokenLink
+ let tokenTitle
+ const token = nft_tokens && nft_tokens.toJS()[data.token_id]
+ if (token) {
+ try {
+ const meta = JSON.parse(token.json_metadata)
+ tokenTitle = meta.title
+ } catch (err) {
+ console.error(err)
+ }
+ }
+ if (!tokenTitle) {
+ tokenTitle = '#' + data.token_id
+ }
+ tokenLink =
+ {tokenTitle}
+
+ return { tokenTitle, tokenLink }
+ }
+
if (/^transfer$|^transfer_to_savings$|^transfer_from_savings$/.test(type)) {
const fromWhere =
type === 'transfer_to_savings' ? tt('transferhistoryrow_jsx.to_savings') :
@@ -312,10 +339,73 @@ class TransferHistoryRow extends React.Component {
} else {
code_key = JSON.stringify({type, ...data}, null, 2);
}
+ } else if (type === 'nft_token') {
+ link = data.creator
+ description_middle = tt('transferhistoryrow_jsx.nft_issued') + tt('transferhistoryrow_jsx.nft_token') + ' '
+ const { tokenTitle, tokenLink } = getToken(data.token_id)
+ link2 = tokenLink
+ if (!link2) {
+ description_middle += tokenTitle
+ }
+ linkExternal2 = true
+ if (data.creator !== data.to) {
+ description_middle2 += tt('transferhistoryrow_jsx.nft_issued_for')
+ link3 = data.to
+ }
+ description_end = ', ' + tt('transferhistoryrow_jsx.nft_issued_cost') + Asset(data.issue_cost).floatString
+ } else if (type === 'nft_transfer') {
+ if (this.props.context === data.from) {
+ if (data.to === 'null') {
+ description_end += tt('transferhistoryrow_jsx.burnt') + ' '
+ description_end += tt('transferhistoryrow_jsx.nft_token')
+ } else {
+ description_start += tt('transferhistoryrow_jsx.you_gifted0') + ' '
+ link = data.to
+ description_end += tt('transferhistoryrow_jsx.you_gifted') + ': '
+ description_end += tt('transferhistoryrow_jsx.nft_token')
+ }
+ } else {
+ link = data.from
+ description_end += ' ' + tt('transferhistoryrow_jsx.gifted') + ' '
+ description_end += tt('transferhistoryrow_jsx.nft_token')
+ }
+ const { tokenTitle, tokenLink } = getToken(data.token_id)
+ link4 = tokenLink
+ linkExternal4 = true
+ description_end += ' ' + (!link4 ? tokenTitle : '')
+ } else if (type === 'nft_sell') {
+ link = data.seller
+ description_middle = tt('transferhistoryrow_jsx.nft_sell')
+ description_middle += tt('transferhistoryrow_jsx.nft_token') + ' '
+ const { tokenTitle, tokenLink } = getToken(data.token_id)
+ link2 = tokenLink
+ if (!link2) {
+ description_middle += tokenTitle
+ }
+ linkExternal2 = true
+ description_middle2 += tt('transferhistoryrow_jsx.for')
+ description_middle2 += Asset(data.price).floatString
+ } else if (type === 'nft_token_sold') {
+ link = data.seller
+ description_middle = tt('transferhistoryrow_jsx.sold')
+ link2 = data.buyer
+ description_middle2 = ' ' + tt('transferhistoryrow_jsx.nft_token') + ' '
+ const { tokenTitle, tokenLink } = getToken(data.token_id)
+ link3 = tokenLink
+ if (!link3) {
+ description_middle2 += tokenTitle
+ }
+ linkExternal3 = true
+ description_end = tt('transferhistoryrow_jsx.for')
+ description_end += Asset(data.price).floatString
+ } else {
+ code_key = JSON.stringify({type, ...data}, null, 2);
}
- else {
- code_key = JSON.stringify({type, ...data}, null, 2);
+ const wrapLink = (href, title, isExternal) => {
+ return (isExternal ?
+ {title || href} :
+ {title || href})
}
return(
@@ -328,10 +418,13 @@ class TransferHistoryRow extends React.Component {
{description_start}
{code_key && {code_key}}
- {link && (linkExternal ?
- {linkTitle || link} :
- {linkTitle || link})}
+ {link && wrapLink(link, linkTitle, linkExternal)}
+ {description_middle}
+ {link2 && wrapLink(link2, linkTitle2, linkExternal2)}
+ {description_middle2}
+ {link3 && wrapLink(link3, linkTitle3, linkExternal3)}
{description_end}
+ {link4 && wrapLink(link4, linkTitle4, linkExternal4)}
{target_hint && [{target_hint}]}
|
@@ -358,6 +451,7 @@ export default connect(
username,
curation_reward,
author_reward,
+ nft_tokens: state.global.get('nft_token_map')
}
},
)(TransferHistoryRow)
diff --git a/app/components/elements/DropdownMenu.jsx b/app/components/elements/DropdownMenu.jsx
index 521bf75..2173600 100644
--- a/app/components/elements/DropdownMenu.jsx
+++ b/app/components/elements/DropdownMenu.jsx
@@ -68,7 +68,7 @@ export default class DropdownMenu extends React.Component {
}
render() {
- const {el, items, selected, children, className, title, href, noArrow} = this.props;
+ const {el, items, selected, children, className, title, href, onClick, noArrow} = this.props;
const hasDropdown = items.length > 0
let entry = children ||
@@ -76,7 +76,7 @@ export default class DropdownMenu extends React.Component {
{hasDropdown && !noArrow && }
- if(hasDropdown) entry = {entry}
+ if(hasDropdown) entry = { onClick(e); this.toggle(e) } : this.toggle}>{entry}
const menu = ;
const cls = 'DropdownMenu' + (this.state.shown ? ' show' : '') + (className ? ` ${className}` : '')
diff --git a/app/components/elements/Expandable.jsx b/app/components/elements/Expandable.jsx
index 9cd2089..222bb1f 100644
--- a/app/components/elements/Expandable.jsx
+++ b/app/components/elements/Expandable.jsx
@@ -13,9 +13,9 @@ class Expandable extends Component {
};
render() {
- const { title, } = this.props;
+ const { title, ...rest } = this.props;
const { opened, } = this.state;
- return (
+ return (
{title}
diff --git a/app/components/elements/Memo.jsx b/app/components/elements/Memo.jsx
index cc2a380..db8d34e 100644
--- a/app/components/elements/Memo.jsx
+++ b/app/components/elements/Memo.jsx
@@ -4,10 +4,12 @@ import { connect, } from 'react-redux';
import tt from 'counterpart';
import { memo, } from 'golos-lib-js';
import { Link, } from 'react-router';
-import links from 'app/utils/Links';
+
+import links from 'app/utils/Links'
import shouldComponentUpdate from 'app/utils/shouldComponentUpdate'
import { validate_account_name, } from 'app/utils/ChainValidation'
-import user from 'app/redux/User';
+import user from 'app/redux/User'
+import { blogsUrl } from 'app/utils/blogsUtils'
class Memo extends React.Component {
static propTypes = {
@@ -36,6 +38,17 @@ class Memo extends React.Component {
const sections = []
let idx = 0
if (!text || (typeof text.split !== 'function')) return
+ const isNftPost = text.startsWith('Post: ')
+ const isNftComment = !isNftPost && text.startsWith('Comment: ')
+ if (isNftPost || isNftComment) {
+ sections.push((isNftPost ? tt('g.for_the_post') : tt('g.for_the_comment')) + ': ')
+ const link = text.split(' ')[1]
+ let linkText = link
+ const truncateText = isNftPost ? 50 : 40
+ if (linkText.length > truncateText) linkText = linkText.substr(0, truncateText) + '...'
+ sections.push( {linkText} )
+ return sections
+ }
for (let section of text.split(' ')) {
if (section.trim().length === 0) continue
const matchUserName = section.match(/(^|\s)(@[a-z][-\.a-z\d]+[a-z\d])/i)
diff --git a/app/components/elements/forms/AmountAssetField.jsx b/app/components/elements/forms/AmountAssetField.jsx
new file mode 100644
index 0000000..4eb5782
--- /dev/null
+++ b/app/components/elements/forms/AmountAssetField.jsx
@@ -0,0 +1,50 @@
+import React from 'react'
+import { Field, } from 'formik'
+import { Asset, AssetEditor, } from 'golos-lib-js/lib/utils'
+
+class AmountAssetField extends React.Component {
+ static defaultProps = {
+ amountField: 'amount',
+ }
+
+ onChange = (e) => {
+ const { amountField, values, setFieldValue, assets } = this.props
+ const value = e.target.value
+ const asset = assets[value]
+ if (asset) {
+ const { supply } = asset
+ const oldValue = values[amountField].asset
+ setFieldValue(amountField, AssetEditor(oldValue.amount,
+ supply.precision, supply.symbol))
+
+ if (!values.author) { // if not edit mode
+ if (asset.allow_override_transfer || value === 'GBG') {
+ if (values.tip_cost)
+ setFieldValue('tip_cost', false)
+ setFieldValue('disable_tip', true)
+ } else {
+ setFieldValue('disable_tip', false)
+ }
+ }
+ }
+ }
+
+ render() {
+ const { name, assets, values, amountField } = this.props
+
+ const options = []
+ for (const [ sym, asset ] of Object.entries(assets)) {
+ options.push( )
+ }
+
+ const { asset } = values[amountField]
+
+ return (
+ {options}
+ )
+ }
+}
+
+export default AmountAssetField
diff --git a/app/components/elements/forms/AmountField.jsx b/app/components/elements/forms/AmountField.jsx
index 8aeda2f..f9b5ef2 100644
--- a/app/components/elements/forms/AmountField.jsx
+++ b/app/components/elements/forms/AmountField.jsx
@@ -3,26 +3,33 @@ import { Field, ErrorMessage, } from 'formik'
import { AssetEditor } from 'golos-lib-js/lib/utils'
class AmountField extends React.Component {
+ static defaultProps = {
+ name: 'amount',
+ }
+
_renderInput = ({ field, form }) => {
- const { value, ...rest } = field
+ // TODO: is it right to pass all props to input
+ const { placeholder, name, ...rest } = this.props
+ const { value, } = field
const { values, setFieldValue } = form
- return this.onChange(e, values, setFieldValue)}
/>
}
onChange = (e, values, setFieldValue) => {
- const newAmount = values.amount.withChange(e.target.value)
+ const { name } = this.props
+ const newAmount = values[name].withChange(e.target.value)
if (newAmount.hasChange && newAmount.asset.amount >= 0) {
- setFieldValue('amount', newAmount)
+ setFieldValue(name, newAmount)
}
}
render() {
- const { placeholder, } = this.props
- return (
+ autoComplete='off' autoCorrect='off' spellCheck='false' {...rest}>
{this._renderInput}
)
}
diff --git a/app/components/elements/nft/NFTMarketCollections.jsx b/app/components/elements/nft/NFTMarketCollections.jsx
new file mode 100644
index 0000000..0da0f8c
--- /dev/null
+++ b/app/components/elements/nft/NFTMarketCollections.jsx
@@ -0,0 +1,47 @@
+import React from 'react'
+import tt from 'counterpart'
+
+import Icon from 'app/components/elements/Icon'
+import NFTSmallIcon from 'app/components/elements/nft/NFTSmallIcon'
+import PagedDropdownMenu from 'app/components/elements/PagedDropdownMenu'
+
+class NFTMarketCollections extends React.Component {
+ render() {
+ let { nft_market_collections, selected } = this.props
+ const nft_colls = nft_market_collections ? nft_market_collections.toJS() : []
+
+ const colls = []
+ colls.push({
+ key: '_all',
+ link: '/nft',
+ label:
+
+ {tt('nft_market_page_jsx.all_collections')}
+ ,
+ value: tt('nft_market_page_jsx.all_collections')
+ })
+ for (const nft_coll of nft_colls) {
+ colls.push({
+ key: nft_coll.name,
+ link: '/nft/' + nft_coll.name,
+ label:
+
+ {nft_coll.name}
+ ,
+ value: nft_coll.name
+ })
+ }
+
+ selected = selected || tt('nft_market_page_jsx.all_collections')
+
+ return item}
+ selected={selected}
+ perPage={20}>
+ {selected}
+
+
+ }
+}
+
+export default NFTMarketCollections
diff --git a/app/components/elements/nft/NFTMarketCollections.scss b/app/components/elements/nft/NFTMarketCollections.scss
new file mode 100644
index 0000000..7a0b01a
--- /dev/null
+++ b/app/components/elements/nft/NFTMarketCollections.scss
@@ -0,0 +1,9 @@
+.NFTMarketCollections {
+ .NFTSmallIcon {
+ margin-top: 0.25rem;
+ margin-right: 0.25rem;
+ margin-bottom: 0.25rem;
+ width: 2rem;
+ height: 2rem;
+ }
+}
diff --git a/app/components/elements/nft/NFTSmallIcon.jsx b/app/components/elements/nft/NFTSmallIcon.jsx
new file mode 100644
index 0000000..c3dba89
--- /dev/null
+++ b/app/components/elements/nft/NFTSmallIcon.jsx
@@ -0,0 +1,12 @@
+import React, { Component, } from 'react'
+
+class NFTSmallIcon extends Component {
+ render() {
+ const { image, ...rest } = this.props
+
+ return
+ }
+}
+
+export default NFTSmallIcon
diff --git a/app/components/elements/nft/NFTSmallIcon.scss b/app/components/elements/nft/NFTSmallIcon.scss
new file mode 100644
index 0000000..0ee5941
--- /dev/null
+++ b/app/components/elements/nft/NFTSmallIcon.scss
@@ -0,0 +1,11 @@
+.NFTSmallIcon {
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: 50% 50%;
+ border-radius: 50%;
+
+ width: 3rem;
+ height: 3rem;
+ display: inline-block;
+ vertical-align: top;
+}
diff --git a/app/components/elements/nft/NFTTokenItem.jsx b/app/components/elements/nft/NFTTokenItem.jsx
new file mode 100644
index 0000000..9033f88
--- /dev/null
+++ b/app/components/elements/nft/NFTTokenItem.jsx
@@ -0,0 +1,248 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import {connect} from 'react-redux'
+import { Link } from 'react-router'
+import tt from 'counterpart'
+import { Asset } from 'golos-lib-js/lib/utils'
+
+import DropdownMenu from 'app/components/elements/DropdownMenu'
+import Icon from 'app/components/elements/Icon'
+import g from 'app/redux/GlobalReducer'
+import user from 'app/redux/User'
+import transaction from 'app/redux/Transaction'
+import { getAssetMeta } from 'app/utils/market/utils'
+
+class NFTTokenItem extends Component {
+ state = {}
+
+ constructor() {
+ super()
+ }
+
+ cancelOrder = async (e, tokenIdx) => {
+ e.preventDefault()
+ const { currentUser } = this.props
+ const order = this.getOrder()
+ await this.props.cancelOrder(order.order_id, currentUser, () => {
+ this.props.refetch()
+ }, (err) => {
+ if (!err || err.toString() === 'Canceled') return
+ console.error(err)
+ alert(err.toString())
+ })
+ }
+
+ buyToken = async (e, tokenIdx) => {
+ e.preventDefault()
+ const { token, currentUser } = this.props
+ const { token_id, json_metadata } = token
+ const order = this.getOrder()
+ let tokenTitle
+ try {
+ tokenTitle = JSON.parse(json_metadata).title.substring(0, 100)
+ } catch (err) {
+ console.error(err)
+ tokenTitle = '#' + token_id
+ }
+ const price = Asset(order.price).floatString
+
+ if (!currentUser) {
+ this.props.login()
+ return
+ }
+ await this.props.buyToken(token_id, order.order_id, tokenTitle, price, currentUser, () => {
+ this.props.refetch()
+ }, (err) => {
+ if (!err || err.toString() === 'Canceled') return
+ console.error(err)
+ alert(err.toString())
+ })
+ }
+
+ burnIt = async (e, tokenIdx) => {
+ e.preventDefault()
+ const { token, currentUser } = this.props
+ const { token_id } = token
+ await this.props.burnToken(token_id, currentUser, () => {
+ this.props.refetch()
+ }, (err) => {
+ if (!err || err.toString() === 'Canceled') return
+ console.error(err)
+ alert(err.toString())
+ })
+ }
+
+ getOrder = () => {
+ const { tokenOrder } = this.props
+ if (tokenOrder) return tokenOrder
+ return this.props.token.order
+ }
+
+ render() {
+ const { token, tokenIdx, currentUser, page, assets } = this.props
+
+ const { json_metadata, image, selling } = token
+
+ let data
+ if (json_metadata) {
+ data = JSON.parse(json_metadata)
+ }
+ data = data || {} // node allows to use '', null, object, or array
+
+ let last_price
+ const price = token.selling && Asset(this.getOrder().price)
+ if (price) {
+ const asset = assets[price.symbol]
+ let imageUrl
+ if (asset) {
+ imageUrl = getAssetMeta(asset).image_url
+ }
+ last_price =
+ {imageUrl && }
+ {price.amountFloat}
+
+ }
+
+ const link = '/nft-tokens/' + token.token_id
+
+ const kebabItems = [
+ { link, target: '_blank', value: tt('g.more_hint') },
+ ]
+
+ const isMy = currentUser && currentUser.get('username') === token.owner
+
+ if (isMy && !selling) {
+ kebabItems.unshift({ link: '#', onClick: e => {
+ this.burnIt(e, tokenIdx)
+ }, value: tt('g.burn') })
+ }
+
+ if (last_price && !selling && isMy) {
+ kebabItems.unshift({ link: '#', onClick: e => {
+ this.props.showTransfer(e, tokenIdx)
+ }, value: tt('g.transfer') })
+ }
+
+ const isCollection = page === 'collection'
+ const isMarket = page === 'market'
+
+ let buttons
+ if (last_price) {
+ buttons =
+ {last_price}
+ {kebabItems.length > 1 ?
+
+ : null}
+ {isMy && !selling && }
+ {isMy && selling && }
+ {!isMy && selling && }
+
+ } else {
+ buttons =
+ {!isMy && }
+ {isMy && }
+ {isMy && }
+ {kebabItems.length > 1 ?
+
+ : null}
+
+ }
+
+ return
+
+
+ {!isMy &&
+ {'@' + token.owner}
+ }
+
+ {data.title}
+
+
+ {token.name}
+
+
+ {buttons}
+
+
+
+ }
+}
+
+export default connect(
+ (state, ownProps) => {
+ const {locationBeforeTransitions: {pathname}} = state.routing;
+ let currentUser = ownProps.currentUser || state.user.getIn(['current'])
+ return { ...ownProps, currentUser }
+ },
+ dispatch => ({
+ burnToken: (
+ token_id, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ from: username,
+ to: 'null',
+ token_id,
+ memo: ''
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_transfer',
+ confirm: tt('g.are_you_sure'),
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ },
+ cancelOrder: (
+ order_id, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ owner: username,
+ order_id,
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_cancel_order',
+ confirm: tt('g.are_you_sure'),
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ },
+ buyToken: (
+ token_id, order_id, tokenTitle, price, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ buyer: username,
+ name: '',
+ token_id,
+ order_id,
+ price: '0.000 GOLOS'
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_buy',
+ confirm: tt('nft_tokens_jsx.buy_confirm') + tokenTitle + tt('nft_tokens_jsx.buy_confirm2') + price + '?',
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ },
+ login: () => {
+ dispatch(user.actions.showLogin({
+ loginDefault: { unclosable: false }
+ }));
+ },
+ })
+)(NFTTokenItem)
diff --git a/app/components/elements/nft/NFTTokenItem.scss b/app/components/elements/nft/NFTTokenItem.scss
new file mode 100644
index 0000000..0782900
--- /dev/null
+++ b/app/components/elements/nft/NFTTokenItem.scss
@@ -0,0 +1,69 @@
+.NFTTokenItem {
+ display: inline-block;
+ border: 1px solid rgba(128,128,128,0.45);
+ border-radius: 5px;
+ margin-right: 1rem;
+ margin-bottom: 1em;
+ padding: 0.5rem;
+ cursor: pointer;
+ position: relative;
+
+ .token-owner {
+ position: absolute;
+ top: 15px;
+ right: 15px;
+ background-color: white;
+ border: 1px solid rgba(128,128,128,0.45);
+ border-radius: 8px;
+ padding-left: 5px;
+ padding-right: 5px;
+ font-size: 90%;
+ }
+
+ &.collection {
+ background-color: rgb(226, 247, 223);
+ }
+
+ .token-image {
+ width: 200px;
+ height: 200px;
+ object-fit: cover;
+ }
+ .price-icon {
+ width: 20px;
+ height: 20px;
+ margin-right: 0.25rem;
+ }
+
+ .token-title {
+ margin-top: 0.25rem;
+ margin-bottom: 0.1rem;
+ }
+ .token-coll {
+ display: inline-block;
+ margin-bottom: 0.35rem;
+ }
+
+ .button {
+ margin: 0px !important;
+ margin-right: 5px !important;
+ }
+ .button:hover {
+ background-color: #016aad !important;
+ }
+ .button.hollow {
+ border: 0px;
+ }
+ .button.hollow:hover {
+ background-color: transparent !important;
+ }
+}
+
+.NFTTokenItem:hover {
+ background-color: rgba(208,208,208,0.45);
+ border: 1px solid rgba(128,128,128,0.45);
+
+ &.collection {
+ background-color: rgb(206, 247, 203);
+ }
+}
diff --git a/app/components/modules/Header.jsx b/app/components/modules/Header.jsx
index 12bfd41..42e8fd4 100644
--- a/app/components/modules/Header.jsx
+++ b/app/components/modules/Header.jsx
@@ -91,23 +91,31 @@ class Header extends React.Component {
const name = acct_meta ? normalizeProfile(acct_meta.toJS()).name : null;
const user_title = name ? `${name} (@${user_name})` : user_name;
page_title = user_title;
- if(route.params[1] === "curation-rewards"){
+ if (route.params[1] === "curation-rewards"){
page_title = tt('header_jsx.curation_rewards_by') + " " + user_title;
- }
- if(route.params[1] === "author-rewards"){
+ } else if (route.params[1] === "author-rewards"){
page_title = tt('header_jsx.author_rewards_by') + " " + user_title;
- }
- if(route.params[1] === "donates-from"){
+ } else if (route.params[1] === "donates-from"){
page_title = tt('header_jsx.donates_from') + " " + user_title;
- }
- if(route.params[1] === "donates-to"){
+ } else if (route.params[1] === "donates-to"){
page_title = tt('header_jsx.donates_to') + " " + user_title;
- }
- if(route.params[1] === "recent-replies"){
+ } else if (route.params[1] === "recent-replies"){
page_title = tt('header_jsx.replies_to') + " " + user_title;
+ } else if (route.params[1] === "nft-tokens"){
+ page_title = tt('header_jsx.nft_tokens') + " " + user_title
+ } else if (route.params[1] === "nft-collections"){
+ page_title = tt('header_jsx.nft_collections') + " " + user_title
+ } else if (route.params[1] === "nft-history"){
+ page_title = tt('g.nft_history') + " " + user_title
}
} else if (route.page === 'ConvertAssetsPage') {
page_title = tt('g.convert_assets')
+ } else if (route.page === `NFTCollectionPage`){
+ page_title = tt('header_jsx.nft_collection')
+ } else if (route.page === `NFTTokenPage`){
+ page_title = tt('header_jsx.nft_token')
+ } else if (route.page === `NFTMarketPage`){
+ page_title = tt('header_jsx.nft_market')
} else {
page_name = ''; //page_title = route.page.replace( /([a-z])([A-Z])/g, '$1 $2' ).toLowerCase();
}
diff --git a/app/components/modules/TopRightMenu.jsx b/app/components/modules/TopRightMenu.jsx
index 4e020b0..e460924 100644
--- a/app/components/modules/TopRightMenu.jsx
+++ b/app/components/modules/TopRightMenu.jsx
@@ -150,7 +150,7 @@ function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops,
if (loggedIn) { // change back to if(username) after bug fix: Clicking on Login does not cause drop-down to close #TEMP!
let user_menu = [
- {link: walletLink, icon: 'new/wallet', value: tt('g.wallet'), addon: },
+ {link: walletLink, icon: 'new/wallet', value: tt('g.wallet'), addon: },
{link: uiaLink, icon: 'editor/coin', value: tt('g.assets')},
{link: ordersLink, icon: 'trade', value: tt('navigation.market2'), addon: },
{link: inviteLink, icon: 'hf/hf19', value: tt('g.invites')},
@@ -196,7 +196,7 @@ function TopRightMenu({account, savings_withdraws, price_per_golos, globalprops,
-
+
}
{navAdditional}
diff --git a/app/components/modules/nft/CreateNFTCollection.jsx b/app/components/modules/nft/CreateNFTCollection.jsx
new file mode 100644
index 0000000..d953d1d
--- /dev/null
+++ b/app/components/modules/nft/CreateNFTCollection.jsx
@@ -0,0 +1,398 @@
+import React, { Component, } from 'react'
+import tt from 'counterpart'
+import { connect, } from 'react-redux'
+import CloseButton from 'react-foundation-components/lib/global/close-button'
+import { Formik, Form, Field, ErrorMessage, } from 'formik'
+
+import Expandable from 'app/components/elements/Expandable'
+import LoadingIndicator from 'app/components/elements/LoadingIndicator'
+import transaction from 'app/redux/Transaction'
+
+const UINT32_MAX = '4294967295'
+
+class CreateNFTCollection extends Component {
+ state = {
+ collection: {
+ name: '',
+ title: '',
+ json_metadata: '{}',
+ max_token_count: UINT32_MAX,
+ infinity: true
+ }
+ }
+
+ validate = (values) => {
+ const errors = {}
+ const { title, name } = values
+ if (!title.length) {
+ errors.title = tt('g.required')
+ }
+ if (name.length < 3) {
+ errors.name = tt('assets_jsx.symbol_too_short')
+ } else {
+ const parts = name.split('.')
+ if (parts[0] == 'GOLOS' || parts[0] == 'GBG' || parts[0] == 'GESTS') {
+ errors.name = tt('assets_jsx.top_symbol_not_your')
+ } else if (parts.length == 2 && parts[1].length < 3) {
+ errors.name = tt('assets_jsx.subsymbol_too_short')
+ }
+ }
+ let meta = values.json_metadata
+ try {
+ meta = JSON.parse(meta)
+ if (!meta || Array.isArray(meta)) throw new Error('JSON is array')
+ } catch (err) {
+ console.error('json_metadata', err)
+ //errors.json_metadata = tt('create_nft_collection_jsx.json_wrong')
+ meta = null
+ }
+ if (meta) {
+ const noFields = []
+ if (!('title' in meta)) noFields.push('title')
+ if (values.image && !('image' in meta)) noFields.push('image')
+ if (values.description && !('description' in meta)) noFields.push('description')
+ if (noFields.length) {
+ errors.json_metadata = tt('create_nft_collection_jsx.json_no_fields')
+ errors.json_metadata += noFields.join(', ') + '. '
+ }
+ }
+ ++this.validationTime
+ return errors
+ }
+
+ setSubmitting = (submitting) => {
+ this.setState({ submitting })
+ }
+
+ _onSubmit = async (values) => {
+ this.setSubmitting(true)
+ this.setState({
+ errorMessage: ''
+ })
+ const { currentUser } = this.props
+ const username = currentUser.get('username')
+ await this.props.createCollection(values.name, values.json_metadata, values.max_token_count, currentUser, () => {
+ this.props.fetchState()
+ this.props.onClose()
+ this.setSubmitting(false)
+ }, (err) => {
+ console.error(err)
+ this.setSubmitting(false)
+ this.setState({
+ errorMessage: err.toString()
+ })
+ })
+ }
+
+ _renderSubmittingIndicator = () => {
+ const { submitting } = this.state
+
+ return submitting ?
+
+ : null
+ }
+
+ onNameChange = (e, values, setFieldValue) => {
+ let newName = ''
+ let hasDot
+ for (let i = 0; i < e.target.value.length; ++i) {
+ const c = e.target.value[i]
+ if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c !== '.') {
+ continue
+ }
+ if (c == '.') {
+ if (i < 3 || hasDot) {
+ continue
+ }
+ hasDot = true
+ }
+ newName += c.toUpperCase()
+ }
+ setFieldValue('name', newName)
+ }
+
+ updateJSONMetadata = (values, setFieldValue, currentVals, force = false) => {
+ let { json_metadata } = values
+ try {
+ json_metadata = JSON.parse(json_metadata)
+ if (json_metadata === null || Array.isArray(json_metadata)) {
+ json_metadata = {}
+ }
+ } catch (err) {
+ console.error('updateJSONMetadata', err)
+ if (!force) {
+ return
+ }
+ json_metadata = {}
+ }
+
+ const title = currentVals.title || values.title
+ json_metadata.title = title || ''
+
+ const description = currentVals.description || values.description
+ if (description) {
+ json_metadata.description = description
+ }
+
+ const image = currentVals.image || values.image
+ if (image) {
+ json_metadata.image = image
+ }
+
+ setFieldValue('json_metadata', JSON.stringify(json_metadata, null, 2))
+ }
+
+ onTitleChange = (e, values, setFieldValue) => {
+ const title = e.target.value
+ setFieldValue('title', title)
+ this.updateJSONMetadata(values, setFieldValue, { title })
+ }
+
+ onDescriptionChange = (e, values, setFieldValue) => {
+ const description = e.target.value
+ setFieldValue('description', description)
+ this.updateJSONMetadata(values, setFieldValue, { description })
+ }
+
+ onImageChange = (e, values, setFieldValue) => {
+ const image = e.target.value
+ setFieldValue('image', image)
+ this.updateJSONMetadata(values, setFieldValue, { image })
+ }
+
+ onImageBlur = (e) => {
+ e.preventDefault()
+ this.setState({
+ showImage: true
+ })
+ }
+
+ onMaxTokenCountChange = (e, setFieldValue) => {
+ let maxTokenCount = ''
+ for (let i = 0; i < e.target.value.length; ++i) {
+ const c = e.target.value[i]
+ if (c < '0' || c > '9') {
+ continue
+ }
+ maxTokenCount += c
+ }
+ if (maxTokenCount === UINT32_MAX) {
+ setFieldValue('infinity', true)
+ }
+ setFieldValue('max_token_count', maxTokenCount)
+ }
+
+ onInfinityChange = (e, values, setFieldValue) => {
+ if (!values.infinity) {
+ setFieldValue('max_token_count', UINT32_MAX)
+ setFieldValue('infinity', !values.infinity)
+ } else {
+ setFieldValue('max_token_count', '')
+ setFieldValue('infinity', !values.infinity)
+ }
+ }
+
+ restoreJson = (e, values, setFieldValue) => {
+ e.preventDefault()
+ this.updateJSONMetadata(values, setFieldValue, {}, true)
+ }
+
+ fixJson = (e, values, setFieldValue) => {
+ e.preventDefault()
+ this.updateJSONMetadata(values, setFieldValue, {})
+ }
+
+ onCancelMouseDown = (e) => {
+ e.preventDefault()
+ this.setState({
+ cancelMouseDown: true
+ })
+ }
+
+ onCancelMouseUp = (e) => {
+ e.preventDefault()
+ if (this.state.cancelMouseDown) {
+ this.props.onClose()
+ this.setState({
+ cancelMouseDown: false
+ })
+ }
+ }
+
+ onMouseUp = (e) => {
+ e.preventDefault()
+ if (this.state.cancelMouseDown) {
+ this.props.onClose()
+ }
+ }
+
+ render() {
+ const { onClose, } = this.props;
+ const { submitting, showImage, errorMessage, hideErrors } = this.state
+
+ return (
+
+
+ {tt('create_nft_collection_jsx.title')}
+
+
+ {({
+ handleSubmit, isValid, values, errors, touched, setFieldValue, handleChange,
+ }) => {
+ return (
+
+ )}}
+ )
+ }
+}
+
+export default connect(
+ // mapStateToProps
+ (state, ownProps) => {
+ const {locationBeforeTransitions: {pathname}} = state.routing;
+ let currentUser = ownProps.currentUser || state.user.getIn(['current'])
+ if (!currentUser) {
+ const currentUserNameFromRoute = pathname.split(`/`)[1].substring(1);
+ currentUser = Map({username: currentUserNameFromRoute});
+ }
+ const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]);
+ return { ...ownProps, currentUser, currentAccount, };
+ },
+
+ dispatch => ({
+ createCollection: (
+ name, json_metadata, max_token_count, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ let json = JSON.parse(json_metadata)
+ json = JSON.stringify(json)
+ const operation = {
+ creator: username,
+ name,
+ json_metadata: json,
+ max_token_count: parseInt(max_token_count)
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_collection',
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ }
+ })
+)(CreateNFTCollection)
diff --git a/app/components/modules/nft/CreateNFTCollection.scss b/app/components/modules/nft/CreateNFTCollection.scss
new file mode 100644
index 0000000..3db9e28
--- /dev/null
+++ b/app/components/modules/nft/CreateNFTCollection.scss
@@ -0,0 +1,34 @@
+.CreateNFTCollection {
+ .column {
+ padding-left: 0rem;
+ padding-right: 0rem;
+ }
+ .padding-left {
+ padding-left: 0.75rem;
+ }
+ .image-preview {
+ max-width: 150px;
+ height: 40px;
+ margin-left: 0.75rem;
+ border: none;
+ }
+ .json_metadata {
+ min-width: 100%;
+ min-height: 120px;
+ font-family: monospace;
+ }
+ .json-error {
+ margin-bottom: 0px;
+ }
+ .Expandable {
+ margin-bottom: 0px;
+ padding-bottom: 0px;
+ width: 100%;
+ .Expander {
+ margin-bottom: 0px;
+ h5 {
+ font-size: 1rem;
+ }
+ }
+ }
+}
diff --git a/app/components/modules/nft/IssueNFTToken.jsx b/app/components/modules/nft/IssueNFTToken.jsx
new file mode 100644
index 0000000..df971b6
--- /dev/null
+++ b/app/components/modules/nft/IssueNFTToken.jsx
@@ -0,0 +1,361 @@
+import React, { Component, } from 'react'
+import tt from 'counterpart'
+import { connect, } from 'react-redux'
+import CloseButton from 'react-foundation-components/lib/global/close-button'
+import { Formik, Form, Field, ErrorMessage, } from 'formik'
+import { Asset, validateAccountName } from 'golos-lib-js/lib/utils'
+
+import Expandable from 'app/components/elements/Expandable'
+import LoadingIndicator from 'app/components/elements/LoadingIndicator'
+import transaction from 'app/redux/Transaction'
+
+class IssueNFTToken extends Component {
+ state = {
+ token: {
+ name: '',
+ to: '',
+ title: '',
+ json_metadata: '{}',
+ }
+ }
+
+ initTo = (currentUser) => {
+ if (!currentUser) return
+ const username = currentUser.get('username')
+ this.setState({
+ token: {
+ ...this.state.token,
+ to: username
+ }
+ })
+ }
+
+ componentDidMount() {
+ this.initTo(this.props.currentUser)
+ }
+
+ componentDidUpdate(prevProps) {
+ const { currentUser } = this.props
+ if (currentUser && !prevProps.currentUser) {
+ this.initTo(currentUser)
+ }
+ }
+
+ validate = (values) => {
+ const errors = {}
+ const { title, to } = values
+ if (!title.length) {
+ errors.title = tt('g.required')
+ }
+ const accNameError = validateAccountName(values.to)
+ if (accNameError.error) {
+ errors.to = tt('account_name.' + accNameError.error)
+ }
+ let meta = values.json_metadata
+ try {
+ meta = JSON.parse(meta)
+ if (Array.isArray(meta)) throw new Error('JSON is array')
+ } catch (err) {
+ console.error('json_metadata', err)
+ errors.json_metadata = tt('create_nft_collection_jsx.json_wrong')
+ meta = null
+ }
+ if (meta) {
+ const noFields = []
+ if (!('title' in meta)) noFields.push('title')
+ if (values.image && !('image' in meta)) noFields.push('image')
+ if (values.description && !('description' in meta)) noFields.push('description')
+ if (noFields.length) {
+ errors.json_metadata = tt('create_nft_collection_jsx.json_no_fields')
+ errors.json_metadata += noFields.join(', ') + '. '
+ }
+ }
+ return errors
+ }
+
+ setSubmitting = (submitting) => {
+ this.setState({ submitting })
+ }
+
+ _onSubmit = async (values) => {
+ this.setSubmitting(true)
+ this.setState({
+ errorMessage: ''
+ })
+ const { currentUser, issueName } = this.props
+ const username = currentUser.get('username')
+ await this.props.issueToken(issueName, values.to, values.json_metadata, currentUser, () => {
+ this.props.fetchState()
+ this.props.onClose()
+ this.setSubmitting(false)
+ }, (err) => {
+ console.error(err)
+ this.setSubmitting(false)
+ this.setState({
+ errorMessage: err.toString()
+ })
+ })
+ }
+
+ _renderSubmittingIndicator = () => {
+ const { submitting } = this.state
+
+ return submitting ?
+
+ : null
+ }
+
+ updateJSONMetadata = (values, setFieldValue, currentVals, force = false) => {
+ let { json_metadata } = values
+ try {
+ json_metadata = JSON.parse(json_metadata)
+ if (Array.isArray(json_metadata)) {
+ json_metadata = {}
+ }
+ } catch (err) {
+ console.error('updateJSONMetadata', err)
+ if (!force) {
+ return
+ }
+ json_metadata = {}
+ }
+
+ const title = currentVals.title || values.title
+ json_metadata.title = title || ''
+
+ const description = currentVals.description || values.description
+ if (description) {
+ json_metadata.description = description
+ }
+
+ const image = currentVals.image || values.image
+ if (image) {
+ json_metadata.image = image
+ }
+
+ setFieldValue('json_metadata', JSON.stringify(json_metadata, null, 2))
+ }
+
+ onTitleChange = (e, values, setFieldValue) => {
+ const title = e.target.value
+ setFieldValue('title', title)
+ this.updateJSONMetadata(values, setFieldValue, { title })
+ }
+
+ onDescriptionChange = (e, values, setFieldValue) => {
+ const description = e.target.value
+ setFieldValue('description', description)
+ this.updateJSONMetadata(values, setFieldValue, { description })
+ }
+
+ onImageChange = (e, values, setFieldValue) => {
+ const image = e.target.value
+ setFieldValue('image', image)
+ this.updateJSONMetadata(values, setFieldValue, { image })
+ }
+
+ onImageBlur = (e) => {
+ e.preventDefault()
+ this.setState({
+ showImage: true
+ })
+ }
+
+ restoreJson = (e, values, setFieldValue) => {
+ e.preventDefault()
+ this.updateJSONMetadata(values, setFieldValue, {}, true)
+ }
+
+ fixJson = (e, values, setFieldValue) => {
+ e.preventDefault()
+ this.updateJSONMetadata(values, setFieldValue, {})
+ }
+
+ onCancelMouseDown = (e) => {
+ e.preventDefault()
+ this.setState({
+ cancelMouseDown: true
+ })
+ }
+
+ onCancelMouseUp = (e) => {
+ e.preventDefault()
+ if (this.state.cancelMouseDown) {
+ this.props.onClose()
+ this.setState({
+ cancelMouseDown: false
+ })
+ }
+ }
+
+ onMouseUp = (e) => {
+ e.preventDefault()
+ if (this.state.cancelMouseDown) {
+ this.props.onClose()
+ }
+ }
+
+ render() {
+ const { issueName, issueNum, cprops, onClose, } = this.props;
+ const { submitting, showImage, errorMessage } = this.state
+
+ return (
+
+
+ {tt('issue_nft_token_jsx.title') + ' (' + issueName + ', #' + issueNum + ')'}
+
+
+ {({
+ handleSubmit, isValid, values, errors, touched, setFieldValue, handleChange,
+ }) => {
+ let cost = null
+ if (cprops) {
+ let issueCost = Asset(cprops.get('nft_issue_cost'))
+ const multiplier = Math.floor(values.json_metadata.length / 1024)
+ issueCost = issueCost.plus(issueCost.mul(multiplier))
+ cost =
+ {tt('issue_nft_token_jsx.issue_cost')}
+ {issueCost.floatString}
+ .
+
+ }
+
+ return (
+
+ )}}
+ )
+ }
+}
+
+export default connect(
+ // mapStateToProps
+ (state, ownProps) => {
+ const {locationBeforeTransitions: {pathname}} = state.routing;
+ let currentUser = ownProps.currentUser || state.user.getIn(['current'])
+ if (!currentUser) {
+ const currentUserNameFromRoute = pathname.split(`/`)[1].substring(1);
+ currentUser = Map({username: currentUserNameFromRoute});
+ }
+ const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')]);
+ const cprops = state.global.get('cprops')
+ return { ...ownProps, currentUser, currentAccount, cprops, }
+ },
+
+ dispatch => ({
+ issueToken: (
+ name, to, json_metadata, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ let json = JSON.parse(json_metadata)
+ json = JSON.stringify(json)
+ const operation = {
+ creator: username,
+ name,
+ to,
+ json_metadata: json,
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_issue',
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ }
+ })
+)(IssueNFTToken)
diff --git a/app/components/modules/nft/IssueNFTToken.scss b/app/components/modules/nft/IssueNFTToken.scss
new file mode 100644
index 0000000..ff33515
--- /dev/null
+++ b/app/components/modules/nft/IssueNFTToken.scss
@@ -0,0 +1,31 @@
+.IssueNFTToken {
+ .column {
+ padding-left: 0rem;
+ padding-right: 0rem;
+ }
+ .image-preview {
+ max-width: 150px;
+ height: 40px;
+ margin-left: 0.75rem;
+ border: none;
+ }
+ .json_metadata {
+ min-width: 100%;
+ min-height: 120px;
+ font-family: monospace;
+ }
+ .json-error {
+ margin-bottom: 0px;
+ }
+ .Expandable {
+ margin-bottom: 0px;
+ padding-bottom: 0px;
+ width: 100%;
+ .Expander {
+ margin-bottom: 0px;
+ h5 {
+ font-size: 1rem;
+ }
+ }
+ }
+}
diff --git a/app/components/modules/nft/NFTCollections.jsx b/app/components/modules/nft/NFTCollections.jsx
new file mode 100644
index 0000000..bdcedbe
--- /dev/null
+++ b/app/components/modules/nft/NFTCollections.jsx
@@ -0,0 +1,210 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import {connect} from 'react-redux';
+import { Link } from 'react-router';
+import tt from 'counterpart';
+import { Asset } from 'golos-lib-js/lib/utils';
+import Reveal from 'react-foundation-components/lib/global/reveal';
+
+import Icon from 'app/components/elements/Icon';
+import LoadingIndicator from 'app/components/elements/LoadingIndicator'
+import NFTSmallIcon from 'app/components/elements/nft/NFTSmallIcon'
+import CreateNFTCollection from 'app/components/modules/nft/CreateNFTCollection'
+import IssueNFTToken from 'app/components/modules/nft/IssueNFTToken'
+import g from 'app/redux/GlobalReducer'
+import user from 'app/redux/User'
+import transaction from 'app/redux/Transaction'
+import { getAssetMeta } from 'app/utils/market/utils'
+
+class NFTCollections extends Component {
+ state = {}
+
+ constructor() {
+ super()
+ }
+
+ showCreate = (e) => {
+ e.preventDefault()
+ this.setState({
+ showCreate: true,
+ })
+ }
+
+ hideCreate = () => {
+ this.setState({
+ showCreate: false,
+ })
+ }
+
+ showIssue = (e) => {
+ e.preventDefault()
+ this.setState({
+ showIssue: true,
+ })
+ }
+
+ hideIssue = () => {
+ this.setState({
+ showIssue: false,
+ })
+ }
+
+ render() {
+ const { account, isMyAccount, nft_collections, nft_assets, fetchState } = this.props
+ const accountName = account.get('name')
+
+ const collections = nft_collections ? nft_collections.toJS() : null
+ const assets = nft_assets ? nft_assets.toJS() : null
+
+ let items
+ if (!collections) {
+ items =
+ } else if (!collections.length) {
+ if (isMyAccount) {
+ items = {tt('nft_collections_jsx.not_yet')}
+ } else {
+ items = {tt('nft_collections_jsx.not_yet2') + accountName + tt('nft_collections_jsx.not_yet3')}
+ }
+ } else {
+ items = []
+ let count = 0
+ for (const collection of collections) {
+ const { name, token_count, json_metadata, image, market_volume, last_buy_price } = collection
+
+ let data
+ if (json_metadata) {
+ data = JSON.parse(json_metadata)
+ }
+ data = data || {} // node allows to use '', null, object, or array
+
+ const issueIt = (e) => {
+ e.preventDefault()
+ this.setState({
+ showIssue: true,
+ issueName: name,
+ issueNum: token_count + 1
+ })
+ }
+
+ const deleteIt = async (e) => {
+ e.preventDefault()
+
+ await this.props.deleteCollection(name, accountName, () => {
+ this.props.fetchState()
+ }, (err) => {
+ console.error(err)
+ })
+ }
+
+ const price = Asset(last_buy_price)
+ const asset = assets[price.symbol]
+ let imageUrl
+ if (asset) {
+ imageUrl = getAssetMeta(asset).image_url
+ }
+
+ items.push(
+
+
+ |
+
+
+ {name}
+
+ |
+
+ {tt('nft_tokens_jsx.token_count', { count: token_count })}
+ {isMyAccount ? : null}
+ |
+
+
+ {tt('rating_jsx.volume') + ' ' + parseFloat(market_volume).toFixed(3)}
+
+
+ {imageUrl && }
+ {price.amountFloat}
+
+ |
+
+ {isMyAccount ? : null}
+ |
+ )
+
+ ++count
+ }
+
+ items =
+ }
+
+ const { showCreate, showIssue, issueName, issueNum } = this.state
+
+ return ( )
+ }
+}
+
+export default connect(
+ (state, ownProps) => {
+ return {...ownProps,
+ nft_collections: state.global.get('nft_collections'),
+ nft_assets: state.global.get('nft_assets')
+ }
+ },
+ dispatch => ({
+ fetchState: () => {
+ const pathname = window.location.pathname
+ dispatch({type: 'FETCH_STATE', payload: {pathname}})
+ },
+ deleteCollection: (
+ name, username, successCallback, errorCallback
+ ) => {
+ const operation = {
+ creator: username,
+ name,
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_collection_delete',
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ }
+ })
+)(NFTCollections)
diff --git a/app/components/modules/nft/NFTCollections.scss b/app/components/modules/nft/NFTCollections.scss
new file mode 100644
index 0000000..0074e6c
--- /dev/null
+++ b/app/components/modules/nft/NFTCollections.scss
@@ -0,0 +1,20 @@
+.NFTCollections {
+ .market-stats {
+ font-size: 90%;
+ }
+ .price-icon {
+ width: 20px;
+ height: 20px;
+ margin-right: 0.25rem;
+ }
+ .price-val {
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ .button {
+ margin-left: 0.5rem;
+ margin-right: 0rem;
+ margin-bottom: 0rem;
+ }
+}
diff --git a/app/components/modules/nft/NFTHistory.jsx b/app/components/modules/nft/NFTHistory.jsx
new file mode 100644
index 0000000..d89483e
--- /dev/null
+++ b/app/components/modules/nft/NFTHistory.jsx
@@ -0,0 +1,85 @@
+/* eslint react/prop-types: 0 */
+import React from 'react';
+import {connect} from 'react-redux'
+import tt from 'counterpart'
+
+import TransferHistoryRow from 'app/components/cards/TransferHistoryRow'
+
+class NFTHistory extends React.Component {
+ state = { historyIndex: 0 }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (!this.props.account.nft_history) return true;
+ if (!nextProps.account.nft_history) return true;
+ return (
+ nextProps.account.nft_history.length !== this.props.account.nft_history.length ||
+ nextState.historyIndex !== this.state.historyIndex);
+ }
+
+ _setHistoryPage(back) {
+ const newIndex = this.state.historyIndex + (back ? 10 : -10);
+ this.setState({historyIndex: Math.max(0, newIndex)});
+ }
+
+ render() {
+ const {state: {historyIndex}} = this
+ const {account, incoming} = this.props;
+
+ const nft_history = account.nft_history || [];
+
+ /// nft log
+ let idx = 0
+ let nftLog = nft_history.map((item, index) => {
+ return ;
+ }).filter(el => !!el);
+ let currentIndex = -1;
+ const nftLength = nftLog.length;
+ const limitedIndex = Math.min(historyIndex, nftLength - 10);
+ nftLog = nftLog.reverse().filter(() => {
+ currentIndex++;
+ return currentIndex >= limitedIndex && currentIndex < limitedIndex + 10;
+ });
+
+ const navButtons = (
+
+ );
+
+ return (
+
+
+ {tt('g.nft_history')}
+ {navButtons}
+
+ {navButtons}
+
+
+ );
+ }
+}
+
+export default connect(
+ // mapStateToProps
+ (state, ownProps) => {
+ return {
+ state,
+ ...ownProps
+ }
+ }
+)(NFTHistory)
diff --git a/app/components/modules/nft/NFTTokenSell.jsx b/app/components/modules/nft/NFTTokenSell.jsx
new file mode 100644
index 0000000..9fd9e6b
--- /dev/null
+++ b/app/components/modules/nft/NFTTokenSell.jsx
@@ -0,0 +1,215 @@
+import React, { Component, } from 'react'
+import tt from 'counterpart'
+import { connect, } from 'react-redux'
+import CloseButton from 'react-foundation-components/lib/global/close-button'
+import { Formik, Form, Field, ErrorMessage, } from 'formik'
+import { Asset, AssetEditor } from 'golos-lib-js/lib/utils'
+
+import AmountField from 'app/components/elements/forms/AmountField'
+import AmountAssetField from 'app/components/elements/forms/AmountAssetField'
+import LoadingIndicator from 'app/components/elements/LoadingIndicator'
+import NFTSmallIcon from 'app/components/elements/nft/NFTSmallIcon'
+import transaction from 'app/redux/Transaction'
+import { generateOrderID } from 'app/utils/market/utils'
+
+class NFTTokenSell extends Component {
+ state = {
+ order: {
+ price: AssetEditor('0.000 GOLOS')
+ }
+ }
+
+ validate = (values) => {
+ const errors = {}
+ const { price } = values
+ if (values.price.asset.eq(0)) {
+ errors.price = tt('nft_token_sell_jsx.fill_price')
+ }
+ return errors
+ }
+
+ setSubmitting = (submitting) => {
+ this.setState({ submitting })
+ }
+
+ getToken = () => {
+ const { nft_tokens, tokenIdx } = this.props
+ if (tokenIdx !== undefined) {
+ return nft_tokens.toJS().data[tokenIdx]
+ }
+ return this.props.token
+ }
+
+ _onSubmit = async (values) => {
+ this.setSubmitting(true)
+ this.setState({
+ errorMessage: ''
+ })
+
+ const { currentUser, onClose, } = this.props
+ const token = this.getToken()
+ const { token_id } = token
+
+ const username = currentUser.get('username')
+
+ await this.props.sellToken(token_id, values.price, currentUser, () => {
+ this.props.onClose()
+ this.setSubmitting(false)
+ this.doNotRender = true
+ this.props.refetch()
+ }, (err) => {
+ console.error(err)
+ this.setSubmitting(false)
+ this.setState({
+ errorMessage: err.toString()
+ })
+ })
+ }
+
+ onCancelMouseDown = (e) => {
+ e.preventDefault()
+ this.setState({
+ cancelMouseDown: true
+ })
+ }
+
+ onCancelMouseUp = (e) => {
+ e.preventDefault()
+ if (this.state.cancelMouseDown) {
+ this.props.onClose()
+ this.setState({
+ cancelMouseDown: false
+ })
+ }
+ }
+
+ onMouseUp = (e) => {
+ e.preventDefault()
+ if (this.state.cancelMouseDown) {
+ this.props.onClose()
+ }
+ }
+
+ _renderSubmittingIndicator = () => {
+ const { submitting } = this.state
+
+ return submitting ?
+
+ : null
+ }
+
+ render() {
+ if (this.doNotRender) {
+ return
+ }
+
+ const { onClose, } = this.props
+
+ const token = this.getToken()
+
+ const { json_metadata, image } = token
+
+ let data
+ if (json_metadata) {
+ data = JSON.parse(json_metadata)
+ }
+ data = data || {} // node allows to use '', null, object, or array
+
+ const { errorMessage, submitting } = this.state
+
+ const assets = {}
+ assets['GOLOS'] = { supply: Asset(0, 3, 'GOLOS') }
+ assets['GBG'] = { supply: Asset(0, 3, 'GBG') }
+ for (const asset of this.props.assets) {
+ asset.supply = asset.supply.symbol ? asset.supply : Asset(asset.supply)
+ assets[asset.supply.symbol] = asset
+ }
+
+ return
+
+ {tt('g.sell')}
+
+
+ {data.title}
+
+
+ {({
+ handleSubmit, isValid, values, errors, touched, setFieldValue, handleChange,
+ }) => {
+ return (
+
+ )}}
+
+ }
+}
+
+export default connect(
+ // mapStateToProps
+ (state, ownProps) => {
+ return { ...ownProps,
+ nft_tokens: state.global.get('nft_tokens'),
+ assets: state.global.get('assets')
+ }
+ },
+
+ dispatch => ({
+ sellToken: (
+ token_id, price, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ seller: username,
+ token_id,
+ buyer: '',
+ order_id: generateOrderID(),
+ price: price.asset.toString()
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_sell',
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ }
+ })
+)(NFTTokenSell)
diff --git a/app/components/modules/nft/NFTTokenSell.scss b/app/components/modules/nft/NFTTokenSell.scss
new file mode 100644
index 0000000..cfe7ea3
--- /dev/null
+++ b/app/components/modules/nft/NFTTokenSell.scss
@@ -0,0 +1,9 @@
+.NFTTokenSell {
+ .column {
+ padding-left: 0rem;
+ padding-right: 0rem;
+ }
+ .error {
+ margin-bottom: 0px;
+ }
+}
diff --git a/app/components/modules/nft/NFTTokenTransfer.jsx b/app/components/modules/nft/NFTTokenTransfer.jsx
new file mode 100644
index 0000000..6a3a150
--- /dev/null
+++ b/app/components/modules/nft/NFTTokenTransfer.jsx
@@ -0,0 +1,200 @@
+import React, { Component, } from 'react'
+import tt from 'counterpart'
+import { connect, } from 'react-redux'
+import CloseButton from 'react-foundation-components/lib/global/close-button'
+import { Formik, Form, Field, ErrorMessage, } from 'formik'
+import { validateAccountName } from 'golos-lib-js/lib/utils'
+
+import LoadingIndicator from 'app/components/elements/LoadingIndicator'
+import NFTSmallIcon from 'app/components/elements/nft/NFTSmallIcon'
+import transaction from 'app/redux/Transaction'
+
+class NFTTokenTransfer extends Component {
+ state = {
+ token: {
+ to: ''
+ }
+ }
+
+ validate = (values) => {
+ const errors = {}
+ const { to } = values
+ const accNameError = validateAccountName(values.to)
+ if (accNameError.error) {
+ errors.to = tt('account_name.' + accNameError.error)
+ }
+ return errors
+ }
+
+ setSubmitting = (submitting) => {
+ this.setState({ submitting })
+ }
+
+ getToken = () => {
+ const { nft_tokens, tokenIdx } = this.props
+ if (tokenIdx !== undefined) {
+ return nft_tokens.toJS().data[tokenIdx]
+ }
+ return this.props.token
+ }
+
+ _onSubmit = async (values) => {
+ this.setSubmitting(true)
+ this.setState({
+ errorMessage: ''
+ })
+
+ const { currentUser, onClose, } = this.props
+ const token = this.getToken()
+ const { token_id } = token
+
+ const username = currentUser.get('username')
+
+ await this.props.transferToken(token_id, values.to, currentUser, () => {
+ this.props.onClose()
+ this.setSubmitting(false)
+ this.doNotRender = true
+ this.props.refetch()
+ }, (err) => {
+ console.error(err)
+ this.setSubmitting(false)
+ this.setState({
+ errorMessage: err.toString()
+ })
+ })
+ }
+
+ onCancelMouseDown = (e) => {
+ e.preventDefault()
+ this.setState({
+ cancelMouseDown: true
+ })
+ }
+
+ onCancelMouseUp = (e) => {
+ e.preventDefault()
+ if (this.state.cancelMouseDown) {
+ this.props.onClose()
+ this.setState({
+ cancelMouseDown: false
+ })
+ }
+ }
+
+ onMouseUp = (e) => {
+ e.preventDefault()
+ if (this.state.cancelMouseDown) {
+ this.props.onClose()
+ }
+ }
+
+ _renderSubmittingIndicator = () => {
+ const { submitting } = this.state
+
+ return submitting ?
+
+ : null
+ }
+
+ render() {
+ if (this.doNotRender) {
+ return
+ }
+
+ const { onClose, } = this.props
+
+ const token = this.getToken()
+
+ const { json_metadata, image } = token
+
+ let data
+ if (json_metadata) {
+ data = JSON.parse(json_metadata)
+ }
+ data = data || {} // node allows to use '', null, object, or array
+
+ const { errorMessage, submitting } = this.state
+
+ return
+
+ {tt('g.transfer')}
+
+
+ {data.title}
+
+
+ {({
+ handleSubmit, isValid, values, errors, touched, setFieldValue, handleChange,
+ }) => {
+ return (
+
+ )}}
+
+ }
+}
+
+export default connect(
+ // mapStateToProps
+ (state, ownProps) => {
+ return { ...ownProps,
+ nft_tokens: state.global.get('nft_tokens'),
+ }
+ },
+
+ dispatch => ({
+ transferToken: (
+ token_id, to, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ from: username,
+ to,
+ token_id,
+ memo: ''
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_transfer',
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ }
+ })
+)(NFTTokenTransfer)
diff --git a/app/components/modules/nft/NFTTokenTransfer.scss b/app/components/modules/nft/NFTTokenTransfer.scss
new file mode 100644
index 0000000..f371653
--- /dev/null
+++ b/app/components/modules/nft/NFTTokenTransfer.scss
@@ -0,0 +1,9 @@
+.NFTTokenTransfer {
+ .column {
+ padding-left: 0rem;
+ padding-right: 0rem;
+ }
+ .error {
+ margin-bottom: 0px;
+ }
+}
diff --git a/app/components/modules/nft/NFTTokens.jsx b/app/components/modules/nft/NFTTokens.jsx
new file mode 100644
index 0000000..0186c7c
--- /dev/null
+++ b/app/components/modules/nft/NFTTokens.jsx
@@ -0,0 +1,195 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import {connect} from 'react-redux'
+import { Link } from 'react-router'
+import tt from 'counterpart'
+import { Map } from 'immutable'
+import Reveal from 'react-foundation-components/lib/global/reveal'
+
+import DropdownMenu from 'app/components/elements/DropdownMenu'
+import Icon from 'app/components/elements/Icon'
+import LoadingIndicator from 'app/components/elements/LoadingIndicator'
+import NFTTokenItem from 'app/components/elements/nft/NFTTokenItem'
+import NFTTokenTransfer from 'app/components/modules/nft/NFTTokenTransfer'
+import NFTTokenSell from 'app/components/modules/nft/NFTTokenSell'
+import g from 'app/redux/GlobalReducer'
+
+class NFTTokens extends Component {
+ state = {}
+
+ constructor() {
+ super()
+ }
+
+ componentDidMount() {
+ if (!this.props.nft_tokens) {
+ this.refetch()
+ }
+ }
+
+ refetch = () => {
+ this.props.fetchNFTTokens(this.props.account, 0, this.sort, this.sortReversed)
+ }
+
+ showTransfer = (e, tokenIdx) => {
+ e.preventDefault()
+ this.setState({
+ showTransfer: true,
+ tokenIdx,
+ })
+ }
+
+ hideTransfer = () => {
+ this.setState({
+ showTransfer: false,
+ })
+ }
+
+ showSell = (e, tokenIdx) => {
+ e.preventDefault()
+ this.setState({
+ showSell: true,
+ tokenIdx,
+ })
+ }
+
+ hideSell = () => {
+ this.setState({
+ showSell: false,
+ })
+ }
+
+ sortOrder = (e, sort, sortReversed) => {
+ e.preventDefault()
+ this.sort = sort
+ this.sortReversed = sortReversed
+ this.refetch()
+ }
+
+ render() {
+ const { currentUser, account, isMyAccount, nft_tokens, nft_assets, } = this.props
+ const accountName = account.get('name')
+
+ const tokens = nft_tokens ? nft_tokens.toJS().data : null
+ const assets = nft_assets ? nft_assets.toJS() : {}
+
+ const next_from = nft_tokens && nft_tokens.get('next_from')
+
+ let items = []
+ if (!tokens) {
+ items =
+ } else if (!tokens.length) {
+ if (isMyAccount) {
+ items = {tt('nft_tokens_jsx.not_yet')}
+ } else {
+ items = {tt('nft_tokens_jsx.not_yet2') + accountName + tt('nft_tokens_jsx.not_yet3')}
+ }
+ } else {
+ for (let i = 0; i < tokens.length; ++i) {
+ const token = tokens[i]
+ items.push( )
+ }
+ }
+
+ const { showTransfer, showSell, tokenIdx } = this.state
+
+ const sortItems = [
+ { link: '#', onClick: e => {
+ this.sortOrder(e, 'by_last_update', false)
+ }, value: tt('nft_tokens_jsx.sort_new') },
+ { link: '#', onClick: e => {
+ this.sortOrder(e, 'by_last_update', true)
+ }, value: tt('nft_tokens_jsx.sort_old') },
+ { link: '#', onClick: e => {
+ this.sortOrder(e, 'by_last_price', false)
+ }, value: tt('nft_tokens_jsx.sort_price') },
+ { link: '#', onClick: e => {
+ this.sortOrder(e, 'by_name', false)
+ }, value: tt('nft_tokens_jsx.sort_name') },
+ ]
+
+ let currentSort
+ if (this.sort === 'by_last_price') {
+ currentSort = tt('nft_tokens_jsx.sort_price')
+ } else if (this.sort === 'by_name') {
+ currentSort = tt('nft_tokens_jsx.sort_name')
+ } else {
+ if (this.sortReversed) {
+ currentSort = tt('nft_tokens_jsx.sort_old')
+ } else {
+ currentSort = tt('nft_tokens_jsx.sort_new')
+ }
+ }
+
+ return (
+
+
+ {tt('g.nft_tokens')}
+
+ {tt('g.buy')}
+
+
+
+
+ {currentSort}
+
+
+
+
+
+
+
+ {items}
+ {next_from ?
+
+ : null}
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default connect(
+ (state, ownProps) => {
+ const {locationBeforeTransitions: {pathname}} = state.routing;
+ let currentUser = ownProps.currentUser || state.user.getIn(['current'])
+ if (!currentUser) {
+ const currentUserNameFromRoute = pathname.split(`/`)[1].substring(1);
+ currentUser = Map({username: currentUserNameFromRoute});
+ }
+ return {...ownProps, currentUser,
+ nft_tokens: state.global.get('nft_tokens'),
+ nft_assets: state.global.get('nft_assets')
+ }
+ },
+ dispatch => ({
+ fetchNFTTokens: (account, start_token_id, sort, sortReversed) => {
+ if (!account) return
+ dispatch(g.actions.fetchNftTokens({ account: account.get('name'), start_token_id, sort, reverse_sort: sortReversed }))
+ },
+ })
+)(NFTTokens)
diff --git a/app/components/modules/nft/NFTTokens.scss b/app/components/modules/nft/NFTTokens.scss
new file mode 100644
index 0000000..6e85ea3
--- /dev/null
+++ b/app/components/modules/nft/NFTTokens.scss
@@ -0,0 +1,2 @@
+.NFTTokens {
+}
diff --git a/app/components/pages/UserProfile.jsx b/app/components/pages/UserProfile.jsx
index 14c99c1..ca9898b 100644
--- a/app/components/pages/UserProfile.jsx
+++ b/app/components/pages/UserProfile.jsx
@@ -14,6 +14,8 @@ import CreateAsset from 'app/components/modules/uia/CreateAsset';
import Assets from 'app/components/modules/uia/Assets';
import UpdateAsset from 'app/components/modules/uia/UpdateAsset';
import TransferAsset from 'app/components/modules/uia/TransferAsset';
+import NFTCollections from 'app/components/modules/nft/NFTCollections'
+import NFTTokens from 'app/components/modules/nft/NFTTokens'
import Invites from 'app/components/elements/Invites';
import PasswordReset from 'app/components/elements/PasswordReset';
import UserWallet from 'app/components/modules/UserWallet';
@@ -24,6 +26,7 @@ import DonatesTo from 'app/components/modules/DonatesTo';
import CurationRewards from 'app/components/modules/CurationRewards';
import AuthorRewards from 'app/components/modules/AuthorRewards';
import FilledOrders from 'app/components/modules/FilledOrders'
+import NFTHistory from 'app/components/modules/nft/NFTHistory'
import LoadingIndicator from 'app/components/elements/LoadingIndicator';
import { authUrl, } from 'app/utils/AuthApiClient'
import { getGameLevel } from 'app/utils/GameUtils'
@@ -213,7 +216,7 @@ export default class UserProfile extends React.Component {
// const global_status = this.props.global.get('status');
- let rewardsClass = '', walletClass = '', permissionsClass = '';
+ let rewardsClass = '', walletClass = '', permissionsClass = '', nftClass = ''
if (!section || section === 'transfers') {
// transfers, check if url has query params
const { location: { query } } = this.props;
@@ -246,8 +249,18 @@ export default class UserProfile extends React.Component {
;
- }
- else if( section === 'curation-rewards' ) {
+ } else if( section === 'nft-collections' ) {
+ nftClass = 'active'
+ tab_content =
+
+
+ } else if( section === 'nft-tokens' ) {
+ nftClass = 'active'
+ tab_content =
+
+
+
+ } else if( section === 'curation-rewards' ) {
rewardsClass = 'active';
tab_content =
}
+ else if( section === 'nft-history' ) {
+ nftClass = 'active'
+ tab_content =
+
+
+
+ }
else if( section === 'donates-from' ) {
rewardsClass = 'active';
tab_content = },
+ {link: `/@${accountname}/nft-collections`, label: tt('g.nft_collections'), value: tt('g.nft_collections')},
+ {link: `/@${accountname}/nft-history`, label: tt('g.nft_history'), value: tt('g.nft_history'), addon: isMyAccount && },
+ ];
+
let permissionsMenu = [
{link: `/@${accountname}/permissions`, label: tt('g.keys'), value: tt('g.keys')},
{link: `/@${accountname}/password`, label: tt('g.reset_password'), value: tt('g.reset_password')}
@@ -378,6 +407,20 @@ export default class UserProfile extends React.Component {
{tt('g.assets')}
+
{isMyAccount ?
{tt('navigation.market2')}
: null}
diff --git a/app/components/pages/nft/NFTCollectionPage.jsx b/app/components/pages/nft/NFTCollectionPage.jsx
new file mode 100644
index 0000000..dfded4d
--- /dev/null
+++ b/app/components/pages/nft/NFTCollectionPage.jsx
@@ -0,0 +1,272 @@
+import React, { Component, } from 'react'
+import tt from 'counterpart'
+import { connect, } from 'react-redux'
+import { Link } from 'react-router'
+import Reveal from 'react-foundation-components/lib/global/reveal'
+
+import IssueNFTToken from 'app/components/modules/nft/IssueNFTToken'
+import LoadingIndicator from 'app/components/elements/LoadingIndicator'
+import NFTTokenItem from 'app/components/elements/nft/NFTTokenItem'
+import NFTTokenTransfer from 'app/components/modules/nft/NFTTokenTransfer'
+import NFTTokenSell from 'app/components/modules/nft/NFTTokenSell'
+import NotFound from 'app/components/pages/NotFound'
+import g from 'app/redux/GlobalReducer'
+import transaction from 'app/redux/Transaction'
+
+class NFTCollectionPage extends Component {
+ state = {
+ }
+
+ componentDidMount() {
+ this.refetch()
+ }
+
+ componentDidUpdate() {
+ if (!this.props.nft_tokens)
+ this.refetch()
+ }
+
+ refetch = () => {
+ this.props.fetchNftCollectionTokens(this.props.routeParams.name,
+ 0, this.sort, this.sortReversed)
+ }
+
+ showIssue = (e) => {
+ e.preventDefault()
+ const { nft_collection, } = this.props
+
+ const coll = nft_collection.toJS()
+
+ const { name, token_count } = coll
+ this.setState({
+ showIssue: true,
+ issueName: name,
+ issueNum: token_count + 1
+ })
+ }
+
+ hideIssue = () => {
+ this.setState({
+ showIssue: false,
+ })
+ }
+
+ deleteIt = async (e) => {
+ e.preventDefault()
+
+ const { currentUser, nft_collection } = this.props
+ const coll = nft_collection.toJS()
+
+ await this.props.deleteCollection(coll.name, currentUser, () => {
+ this.refetch()
+ }, (err) => {
+ console.error(err)
+ })
+ }
+
+ showTransfer = (e, tokenIdx) => {
+ e.preventDefault()
+ this.setState({
+ showTransfer: true,
+ tokenIdx,
+ })
+ }
+
+ hideTransfer = () => {
+ this.setState({
+ showTransfer: false,
+ })
+ }
+
+ showSell = (e, tokenIdx) => {
+ e.preventDefault()
+ this.setState({
+ showSell: true,
+ tokenIdx,
+ })
+ }
+
+ hideSell = () => {
+ this.setState({
+ showSell: false,
+ })
+ }
+
+ onBurnClick = async (e) => {
+ const { nft_token, currentUser, } = this.props
+ const token = nft_token.toJS()
+ const { token_id } = token
+ await this.props.burnToken(token_id, currentUser, () => {
+ this.refetch()
+ }, (err) => {
+ if (!err || err.toString() === 'Canceled') return
+ console.error(err)
+ alert(err.toString())
+ })
+ }
+
+ render() {
+ const { nft_collection, nft_collection_loaded, nft_tokens, nft_assets, currentUser } = this.props
+
+ if (!nft_collection_loaded) {
+ return
+ }
+
+ const coll = nft_collection.toJS()
+
+ if (!coll.name) {
+ return
+ }
+
+ const tokens = nft_tokens ? nft_tokens.toJS().data : null
+ const assets = nft_assets ? nft_assets.toJS() : null
+
+ const next_from = nft_tokens && nft_tokens.get('next_from')
+
+ let items = []
+ if (!tokens) {
+ items =
+ } else if (!tokens.length) {
+ items = {tt('nft_collection_page_jsx.not_yet')}
+ } else {
+ for (let i = 0; i < tokens.length; ++i) {
+ const token = tokens[i]
+ items.push()
+ }
+ }
+
+ const { showIssue, issueName, issueNum, showTransfer, showSell, tokenIdx } = this.state
+
+ const isMy = currentUser && currentUser.get('username') === coll.creator
+
+ const { json_metadata, token_count } = coll
+
+ let data
+ if (json_metadata) {
+ data = JSON.parse(json_metadata)
+ }
+ data = data || {} // node allows to use '', null, object, or array
+
+ return
+
+ {data.title || coll.name}
+
+
+
+
+ {tt('nft_collection_page_jsx.owner_is')}
+
+
+ @{coll.creator}
+ {isMy ?
+
+
+ : null}
+
+
+
+ {items}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+}
+
+module.exports = {
+ path: '/nft-collections(/:name)',
+ component: connect(
+ // mapStateToProps
+ (state, ownProps) => {
+ const currentUser = state.user.getIn(['current'])
+
+ return { ...ownProps,
+ currentUser,
+ nft_collection: state.global.get('nft_collection'),
+ nft_collection_loaded: state.global.get('nft_collection_loaded'),
+ nft_tokens: state.global.get('nft_tokens'),
+ nft_assets: state.global.get('nft_assets')
+ }
+ },
+
+ dispatch => ({
+ fetchNftCollectionTokens: (collectionName, start_token_id, sort, sortReversed) => {
+ dispatch(g.actions.fetchNftCollectionTokens({ collectionName, start_token_id, sort, reverse_sort: sortReversed }))
+ },
+ deleteCollection: (
+ name, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ creator: username,
+ name,
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_collection_delete',
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ },
+ burnToken: (
+ token_id, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ from: username,
+ to: 'null',
+ token_id,
+ memo: ''
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_transfer',
+ confirm: tt('g.are_you_sure'),
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ }
+ })
+ )(NFTCollectionPage)
+}
+
diff --git a/app/components/pages/nft/NFTCollectionPage.scss b/app/components/pages/nft/NFTCollectionPage.scss
new file mode 100644
index 0000000..83d7d55
--- /dev/null
+++ b/app/components/pages/nft/NFTCollectionPage.scss
@@ -0,0 +1,8 @@
+.NFTCollectionPage {
+ padding: 1rem;
+ flex: 1;
+
+ .button {
+ margin-bottom: 0px;
+ }
+}
diff --git a/app/components/pages/nft/NFTMarketPage.jsx b/app/components/pages/nft/NFTMarketPage.jsx
new file mode 100644
index 0000000..fa47824
--- /dev/null
+++ b/app/components/pages/nft/NFTMarketPage.jsx
@@ -0,0 +1,100 @@
+import React from 'react'
+import { connect, } from 'react-redux'
+import tt from 'counterpart'
+
+import LoadingIndicator from 'app/components/elements/LoadingIndicator'
+import NFTTokenItem from 'app/components/elements/nft/NFTTokenItem'
+import g from 'app/redux/GlobalReducer'
+
+class NFTMarketPage extends React.Component {
+ componentDidMount() {
+ setTimeout(() => {
+ this.refetch()
+ }, 500)
+ }
+
+ refetch() {
+ const { currentUser } = this.props
+ const username = currentUser && currentUser.get('username')
+ this.props.fetchNftMarket(username, '', 0, this.sort, this.sortReversed)
+ }
+
+ render() {
+ const { currentAccount, nft_orders, own_nft_orders, nft_assets, routeParams } = this.props
+
+ const { name } = routeParams
+
+ let content
+ const orders = nft_orders ? nft_orders.toJS().data : null
+ const own_orders = own_nft_orders ? own_nft_orders.toJS().data : null
+ const assets = nft_assets ? nft_assets.toJS() : {}
+
+ if (!orders || !own_orders) {
+ content =
+ } else {
+ let items = []
+ if (!orders.length) {
+ items = tt('nft_market_page_jsx.no_orders')
+ } else {
+ for (let i = 0; i < orders.length; ++i) {
+ const order = orders[i]
+ items.push()
+ }
+ }
+
+ let ownItems = []
+ if (!own_orders.length) {
+ ownItems = tt('nft_market_page_jsx.no_own_orders')
+ } else {
+ for (let i = 0; i < own_orders.length; ++i) {
+ const order = own_orders[i]
+ ownItems.push()
+ }
+ }
+
+ content =
+ {items}
+ {tt('nft_market_page_jsx.own_orders')}
+ {ownItems}
+
+ }
+
+ return
+
+ {tt('header_jsx.nft_market')}
+ {content}
+
+
+ }
+}
+
+module.exports = {
+ path: '/nft(/:name)',
+ component: connect(
+ (state, ownProps) => {
+ const currentUser = state.user.getIn(['current'])
+ const currentAccount = currentUser && state.global.getIn(['accounts', currentUser.get('username')])
+
+ const nft_orders = state.global.get('nft_orders')
+ const own_nft_orders = state.global.get('own_nft_orders')
+
+ return {
+ currentUser,
+ currentAccount,
+ nft_orders,
+ own_nft_orders,
+ nft_assets: state.global.get('nft_assets')
+ }
+ },
+ dispatch => ({
+ fetchNftMarket: (account, collectionName, start_order_id, sort, reverse_sort) => {
+ dispatch(g.actions.fetchNftMarket({ account, collectionName, start_order_id, sort, reverse_sort }))
+ },
+ })
+ )(NFTMarketPage),
+}
diff --git a/app/components/pages/nft/NFTMarketPage.scss b/app/components/pages/nft/NFTMarketPage.scss
new file mode 100644
index 0000000..7be7f25
--- /dev/null
+++ b/app/components/pages/nft/NFTMarketPage.scss
@@ -0,0 +1,4 @@
+.NFTMarketPage {
+ padding: 1rem;
+ flex: 1;
+}
diff --git a/app/components/pages/nft/NFTTokenPage.jsx b/app/components/pages/nft/NFTTokenPage.jsx
new file mode 100644
index 0000000..cd986c1
--- /dev/null
+++ b/app/components/pages/nft/NFTTokenPage.jsx
@@ -0,0 +1,431 @@
+import React, { Component, } from 'react'
+import tt from 'counterpart'
+import { connect, } from 'react-redux'
+import { Link } from 'react-router'
+import { api } from 'golos-lib-js'
+import { Asset } from 'golos-lib-js/lib/utils'
+import Reveal from 'react-foundation-components/lib/global/reveal'
+
+import Expandable from 'app/components/elements/Expandable'
+import LoadingIndicator from 'app/components/elements/LoadingIndicator'
+import TimeAgoWrapper from 'app/components/elements/TimeAgoWrapper'
+import NFTTokenTransfer from 'app/components/modules/nft/NFTTokenTransfer'
+import NFTTokenSell from 'app/components/modules/nft/NFTTokenSell'
+import NotFound from 'app/components/pages/NotFound'
+import { msgsHost, msgsLink } from 'app/utils/ExtLinkUtils'
+import { getAssetMeta } from 'app/utils/market/utils'
+import transaction from 'app/redux/Transaction'
+
+class NFTTokenPage extends Component {
+ state = {
+ }
+
+ cancelOrder = async (e) => {
+ e.preventDefault()
+ const { nft_token, currentUser, } = this.props
+ const token = nft_token.toJS()
+ const { order } = token
+ await this.props.cancelOrder(order.order_id, currentUser, () => {
+ this.props.fetchState()
+ }, (err) => {
+ if (!err || err.toString() === 'Canceled') return
+ console.error(err)
+ alert(err.toString())
+ })
+ }
+
+ buyToken = async (e) => {
+ e.preventDefault()
+ const { nft_token, currentUser, } = this.props
+ const token = nft_token.toJS()
+ const { token_id, order, json_metadata } = token
+ let tokenTitle
+ try {
+ tokenTitle = JSON.parse(json_metadata).title.substring(0, 100)
+ } catch (err) {
+ console.error(err)
+ tokenTitle = '#' + token_id
+ }
+ const price = Asset(order.price).floatString
+
+ await this.props.buyToken(token_id, order.order_id, tokenTitle, price, currentUser, () => {
+ this.props.fetchState()
+ }, (err) => {
+ if (!err || err.toString() === 'Canceled') return
+ console.error(err)
+ alert(err.toString())
+ })
+ }
+
+ onBurnClick = async (e) => {
+ const { nft_token, currentUser, } = this.props
+ const token = nft_token.toJS()
+ const { token_id } = token
+ await this.props.burnToken(token_id, currentUser, () => {
+ this.props.fetchState()
+ }, (err) => {
+ if (!err || err.toString() === 'Canceled') return
+ console.error(err)
+ alert(err.toString())
+ })
+ }
+
+ async loadOps() {
+ const { nft_token, } = this.props
+ const token = nft_token.toJS()
+ const { token_id } = token
+ const ops = await api.getNftTokenOps({
+ token_ids: [ token_id ],
+ from: 0,
+ limit: 500
+ })
+ this.setState({
+ ops: ops[token_id]
+ })
+ }
+
+ async componentDidMount() {
+ if (this.props.nft_token) {
+ await this.loadOps()
+ }
+ }
+
+ async componentDidUpdate(prevProps) {
+ if (this.props.nft_token && !prevProps.nft_token) {
+ await this.loadOps()
+ }
+ }
+
+ showTransfer = (e) => {
+ e.preventDefault()
+ this.setState({
+ showTransfer: true,
+ })
+ }
+
+ hideTransfer = () => {
+ this.setState({
+ showTransfer: false,
+ })
+ }
+
+ showSell = (e) => {
+ e.preventDefault()
+ this.setState({
+ showSell: true,
+ })
+ }
+
+ hideSell = () => {
+ this.setState({
+ showSell: false,
+ })
+ }
+
+ _renderOp = (trx, i) => {
+ const [ opType, op ] = trx.op
+
+ const accLink = (acc) => {
+ let title = acc
+ if (title.length > 16) {
+ title = title.substr(0, 13) + '...'
+ }
+ return {title}
+ }
+
+ let content
+ if (opType === 'nft_token') {
+ content =
+ {accLink(op.creator)}
+ {tt('nft_token_page_jsx.issued')}
+ {op.creator !== op.to ?
+ {tt('nft_token_page_jsx.issued_for') + ' '}
+ {accLink(op.to)}
+ : null}
+
+ } else if (opType === 'nft_transfer') {
+ content =
+ {accLink(op.from)}
+ {tt('nft_token_page_jsx.transfered') + ' '}
+ {accLink(op.to)}
+
+ } else if (opType === 'nft_sell') {
+ const price = Asset(op.price)
+ if (op.buyer) {
+ content =
+ {accLink(op.seller)}
+ {tt('nft_token_page_jsx.selled2')}
+ {accLink(op.buyer)}
+ {!price.eq(0) ? (tt('nft_token_page_jsx.selled2m') + price.floatString) : ''}
+
+ } else {
+ content =
+ {accLink(op.seller)}
+ {tt('nft_token_page_jsx.selled')}
+ {price.floatString}
+
+ }
+ } else if (opType === 'nft_buy') {
+ const price = Asset(op.price)
+ if (!op.name) {
+ content =
+ {accLink(op.buyer)}
+ {tt('nft_token_page_jsx.bought')}
+ {!price.eq(0) ? (tt('nft_token_page_jsx.selled2m') + price.floatString) : ''}
+
+ }
+ }
+ return |
+ |
+ {content} |
+
+ }
+
+ _renderOps = (ops) => {
+ if (!ops) {
+ return
+ }
+
+ const rows = []
+ for (let i = 0; i < ops.length; ++i) {
+ const trx = ops[i]
+ rows.push(this._renderOp(trx, i))
+ }
+ return
+ }
+
+ render() {
+ if (!this.props.nft_token_loaded) {
+ return
+ }
+
+ const { nft_token, nft_assets, currentUser } = this.props
+
+ const token = nft_token.toJS()
+
+ if (!token.name) {
+ return
+ }
+
+ if (token.burnt) {
+ return
+
+
{tt('nft_token_page_jsx.burnt')}
+
{tt('nft_token_page_jsx.burnt2')}
+
{tt('nft_token_page_jsx.burnt3')}
+
+
+ }
+
+ const assets = nft_assets.toJS()
+
+ const { json_metadata, image, selling } = token
+
+ let data
+ if (json_metadata) {
+ data = JSON.parse(json_metadata)
+ }
+ data = data || {} // node allows to use '', null, object, or array
+
+ let last_price
+ const price = token.selling && Asset(token.order.price)
+ if (price) {
+ const asset = assets[price.symbol]
+ let imageUrl
+ if (asset) {
+ imageUrl = getAssetMeta(asset).image_url
+ }
+ last_price =
+ {imageUrl && }
+ {(token.selling ? (' ' + tt('nft_tokens_jsx.selling_for')) : '') + price.floatString}
+
+ }
+
+ const dataStr = JSON.stringify(data, null, 2)
+
+ const { ops } = this.state
+
+ let title = data.title || ''
+ if (title.length > 45) {
+ title = title.substr(0, 45) + '...'
+ }
+
+ let name = token.name
+ if (name.length > 38) {
+ name = name.substr(0, 38) + '...'
+ }
+
+ const description = data.description || ''
+
+ const isMy = currentUser && currentUser.get('username') === token.owner
+
+ return
+
+
+
+
+
+ {title}
+
+
+
+
+ {name}
+
+
+
+ {tt('nft_token_page_jsx.owner_is')}
+
+ {'@' + token.owner}
+
+
+
+ {description ?
+ {description}
+
: null}
+
+
+ {dataStr}
+
+
+
+ {this._renderOps(ops)}
+
+ {!description ?
+
: null}
+ {isMy ?
+ {last_price}
+ {selling && }
+
+ {!selling && }
+ {!selling && }
+ {!selling && }
+
:
}
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+}
+
+module.exports = {
+ path: '/nft-tokens(/:id)',
+ component: connect(
+ // mapStateToProps
+ (state, ownProps) => {
+ const currentUser = state.user.getIn(['current'])
+
+ return { ...ownProps,
+ currentUser,
+ nft_token: state.global.get('nft_token'),
+ nft_token_loaded: state.global.get('nft_token_loaded'),
+ nft_assets: state.global.get('nft_assets')
+ }
+ },
+
+ dispatch => ({
+ fetchState: () => {
+ const pathname = window.location.pathname
+ dispatch({type: 'FETCH_STATE', payload: {pathname}})
+ },
+ burnToken: (
+ token_id, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ from: username,
+ to: 'null',
+ token_id,
+ memo: ''
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_transfer',
+ confirm: tt('g.are_you_sure'),
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ },
+ cancelOrder: (
+ order_id, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ owner: username,
+ order_id,
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_cancel_order',
+ confirm: tt('g.are_you_sure'),
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ },
+ buyToken: (
+ token_id, order_id, tokenTitle, price, currentUser, successCallback, errorCallback
+ ) => {
+ const username = currentUser.get('username')
+ const operation = {
+ buyer: username,
+ name: '',
+ token_id,
+ order_id,
+ price: '0.000 GOLOS'
+ }
+
+ dispatch(transaction.actions.broadcastOperation({
+ type: 'nft_buy',
+ confirm: tt('nft_tokens_jsx.buy_confirm') + tokenTitle + tt('nft_tokens_jsx.buy_confirm2') + price + '?',
+ username,
+ operation,
+ successCallback,
+ errorCallback
+ }))
+ }
+ })
+ )(NFTTokenPage)
+}
diff --git a/app/components/pages/nft/NFTTokenPage.scss b/app/components/pages/nft/NFTTokenPage.scss
new file mode 100644
index 0000000..f85c008
--- /dev/null
+++ b/app/components/pages/nft/NFTTokenPage.scss
@@ -0,0 +1,49 @@
+.NFTTokenPage {
+ padding: 1rem;
+ flex: 1;
+ width: 100%;
+
+ .container {
+ display: flex;
+ width: 100%;
+ }
+ .buttons {
+ align-self: flex-end;
+ display: flex;
+ width: 100%;
+ }
+
+ img {
+ max-width: 320px;
+ max-height: 320px;
+ }
+
+ .Expandable {
+ margin-bottom: 0.5rem;
+ padding-bottom: 0px;
+ width: 100%;
+ .Expander {
+ margin-bottom: 0rem;
+ h5 {
+ font-size: 1rem;
+ }
+ }
+ }
+
+ .price-icon {
+ width: 20px;
+ height: 20px;
+ margin-right: 0.25rem;
+ }
+
+ .button {
+ margin: 0px !important;
+ margin-left: 5px !important;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ }
+ .button-center {
+ margin-top: -4px !important;
+ margin-left: 10px !important;
+ }
+}
diff --git a/app/locales/en.json b/app/locales/en.json
index d0d9054..834b797 100644
--- a/app/locales/en.json
+++ b/app/locales/en.json
@@ -58,6 +58,7 @@
"bid": "Bid",
"blog": "Blog",
"browse": "Browse",
+ "burn": "Burn",
"buy": "Buy",
"quick_buy": "Quick Buy",
"buy_VESTING_TOKEN": "Buy %(VESTING_TOKEN)s",
@@ -133,6 +134,10 @@
"reset_password": "Password reset",
"invites": "Invite checks",
"assets": "UIA assets",
+ "nft": "NFT",
+ "nft_tokens": "NFT-tokens",
+ "nft_collections": "NFT-collections",
+ "nft_history": "NFT history",
"help_wallet": "Wallet functions",
"phone": "phone",
"post": "Post",
@@ -312,7 +317,8 @@
"hide_content": "Information was not found or was hidden for violating the rules.",
"donate_for_post": "thanked you for the post",
"you_donate_for_post": "You thanked",
- "for_the_post": "for the post",
+ "for_the_post": "for the post",
+ "for_the_comment": "for the comment",
"night_mode": "Night Mode",
"social_network": "Social network",
"about_project": "Golos Wallet - is a decentralized service based on the GOLOS blockchain"
@@ -511,6 +517,16 @@
"other": "%(count)s posts"
}
},
+ "account_name": {
+ "account_name_should_be_shorter": "Account name should be shorter.",
+ "account_name_should_be_longer": "Account name should be longer.",
+ "account_name_should_not_be_empty": "Account name should not be empty.",
+ "each_account_segment_should_start_with_a_letter": "Each account name segment should start with a letter.",
+ "each_account_segment_should_have_only_letters_digits_or_dashes": "Each account name segment should have only letters, digits or dashes.",
+ "each_account_segment_should_have_only_one_dash_in_a_row": "Each account name segment should have only one dash in a row.",
+ "each_account_segment_should_end_with_a_letter_or_digit": "Each account name segment should end with a letter or digit.",
+ "each_account_segment_should_be_longer": "Each account name segment should be longer."
+ },
"authorrewards_jsx": {
"estimated_author_rewards_last_week": "Estimated author rewards last week",
"author_rewards_history": "Author Rewards History"
@@ -715,6 +731,87 @@
"no_way_error": "If you set at leas one of these fields, you should also set at least 1 withdrawal method or Details field.",
"no_to_error": "If you set withdraw via transfer with memo, please also set, to which account to transfer."
},
+ "nft_collections_jsx": {
+ "not_yet": "You haven't any own NFT-token collections.",
+ "not_yet2": "",
+ "not_yet3": " haven't any own NFT-token collections.",
+ "create": "Create collection",
+ "name_exists": "Collection with such name is already created by another author",
+ "tokens_exist": "Cannot delete collection which has tokens",
+ "volume_hint": "Market volume for all time",
+ "price_hint": "Last buy price"
+ },
+ "create_nft_collection_jsx": {
+ "title": "Create NFT collection",
+ "name": "Token name",
+ "coll_title": "Title",
+ "coll_descr": "Description",
+ "image": "Image URL",
+ "json_metadata": "JSON metadata",
+ "json_wrong": "Wrong JSON. ",
+ "not_required": "(not required)",
+ "token_count": "Max token count",
+ "infinity": "Infinity",
+ "create": "Create",
+ "restore_json": "Restore default",
+ "json_no_fields": "In JSON missing fields: ",
+ "json_fix": "Add missing fields"
+ },
+ "issue_nft_token_jsx": {
+ "title": "Issue NFT",
+ "issue_cost": "The issue cost is",
+ "max_token_count": "Max token count in collection is already reached."
+ },
+ "nft_tokens_jsx": {
+ "not_yet": "You have no NFT-tokens.",
+ "not_yet2": "",
+ "not_yet3": " has no NFT-tokens.",
+ "token_count": {
+ "zero": "0 tokens",
+ "one": "1 token",
+ "other": "%(count)s tokens"
+ },
+ "sort": "Sorting",
+ "sort_new": "News first",
+ "sort_old": "Old first",
+ "sort_price": "By price",
+ "sort_name": "By collection name",
+ "cancel": "Cancel",
+ "cancel_hint": "Cancel selling",
+ "selling_for": "Selling for ",
+ "buy": "Buy",
+ "buy2": "Buy for ",
+ "buy_confirm": "Are you sure you want to buy ",
+ "buy_confirm2": " for ",
+ "your_token": "Your token",
+ "owner": "Owner"
+ },
+ "nft_collection_page_jsx": {
+ "owner_is": "Owner of collection is ",
+ "not_yet": "No tokens yet in this collection."
+ },
+ "nft_token_page_jsx": {
+ "issued": " issued token",
+ "issued_for": " for",
+ "transfered": " sent token for",
+ "selled": " placed selling order for ",
+ "selled2": " selled token to ",
+ "selled2m": " for ",
+ "bought": " bought token ",
+ "owner_is": "Owner is ",
+ "burnt": "This NFT-токен was existing sometime.",
+ "burnt2": "But it was burnt.",
+ "burnt3": "And all that reminds us of him is a small handful of ashes on our server room...",
+ "msg": "Chat with owner"
+ },
+ "nft_token_sell_jsx": {
+ "fill_price": "Fill price please"
+ },
+ "nft_market_page_jsx": {
+ "no_orders": "No NFT tokens are selling on the market now.",
+ "no_own_orders": "You are not selling any NFT token yet.",
+ "own_orders": "My orders"
+ },
"invites_jsx": {
"create_invite": "Create new invite check",
"create_invite_info": "Cheques (invite codes) are a universal tool for transferring of GOLOS tokens to other people outside the blockchain. There are two ways to redeem the code: transfer its balance to your account or register a new account using it.",
@@ -879,7 +976,17 @@
"vote": "vote",
"some_action": "action",
"with_negrep": " with negative reputation",
- "with_unlimit": " out of daily limit"
+ "with_unlimit": " out of daily limit",
+ "gifted": "gifted",
+ "you_gifted0": "Sent a gift for",
+ "you_gifted": "",
+ "nft_token": "NFT-token",
+ "burnt": "Burnt",
+ "nft_issued": " issued ",
+ "nft_issued_for": " for ",
+ "nft_issued_cost": "issue cost is ",
+ "nft_sell": " placed order of selling ",
+ "sold": " sold "
},
"savingswithdrawhistory_jsx": {
"cancel_this_withdraw_request": "Cancel this withdraw request?",
@@ -918,6 +1025,11 @@
"author_rewards_by": "Author rewards by",
"donates_from": "Donates from",
"donates_to": "Donates to",
+ "nft_tokens": "NFT-tokens",
+ "nft_collections": "NFT-collections, created by",
+ "nft_token": "NFT-token",
+ "nft_collection": "NFT-collection",
+ "nft_market": "NFT-market",
"replies_to": "Replies to",
"comments_by": "Comments by"
},
diff --git a/app/locales/ru-RU.json b/app/locales/ru-RU.json
index d4a29b1..cf135f4 100644
--- a/app/locales/ru-RU.json
+++ b/app/locales/ru-RU.json
@@ -12,6 +12,16 @@
"TIP_TOKEN": "TIP-баланс",
"CLAIM_TOKEN": "Накопительный баланс"
},
+ "account_name": {
+ "account_name_should_be_shorter": "Имя аккаунта должно быть короче.",
+ "account_name_should_be_longer": "Имя аккаунта должно быть длиннее.",
+ "account_name_should_not_be_empty": "Имя аккаунта не должно быть пустым.",
+ "each_account_segment_should_start_with_a_letter": "Каждый сегмент имени аккаунта должен начинаться с буквы.",
+ "each_account_segment_should_have_only_letters_digits_or_dashes": "Сегмент имени аккаунта может содержать только буквы, цифры и дефисы.",
+ "each_account_segment_should_have_only_one_dash_in_a_row": "Каждый сегмент имени аккаунта может содержать только один дефис.",
+ "each_account_segment_should_end_with_a_letter_or_digit": "Каждый сегмент имени аккаунта должен заканчиваться буквой или цифрой.",
+ "each_account_segment_should_be_longer": "Сегмент имени аккаунта должен быть длиннее."
+ },
"authorrewards_jsx": {
"estimated_author_rewards_last_week": "Оценочные авторские вознаграждения за неделю",
"author_rewards_history": "История авторских наград"
@@ -164,6 +174,7 @@
"blog": "Блог",
"browse": "Посмотреть",
"basic": "Основные",
+ "burn": "Сжечь",
"buy": "Купить",
"quick_buy": "Быстрая покупка",
"buy_VESTING_TOKEN": "Купить %(VESTING_TOKEN2)s",
@@ -242,6 +253,10 @@
"reset_password": "Сброс пароля",
"invites": "Инвайт-чеки",
"assets": "Активы UIA",
+ "nft": "NFT",
+ "nft_tokens": "Токены NFT",
+ "nft_collections": "Коллекции NFT",
+ "nft_history": "История NFT",
"help_wallet": "Функции кошелька",
"phone": "телефон",
"post": "Пост",
@@ -427,6 +442,7 @@
"donate_for_post": "отблагодарил вас за пост",
"you_donate_for_post": "Вы отблагодарили",
"for_the_post": "за пост",
+ "for_the_comment": "за комментарий",
"night_mode": "Ночной режим",
"social_network": "Социальные сети",
"about_project": "Голос Кошелёк — децентрализованный сервис, работающий на базе блокчейна Golos",
@@ -501,6 +517,11 @@
"author_rewards_by": "Автор награжден",
"donates_from": "Донаты, отправленные",
"donates_to": "Донаты, полученные",
+ "nft_tokens": "NFT-токены",
+ "nft_collections": "NFT-коллекции, созданные",
+ "nft_token": "NFT-токен",
+ "nft_collection": "NFT-коллекция",
+ "nft_market": "Биржа NFT",
"replies_to": "Ответы на",
"comments_by": "Комментарии"
},
@@ -914,7 +935,17 @@
"vote": "голос",
"some_action": "действие",
"with_negrep": " при отрицательной репутации",
- "with_unlimit": " вне суточного лимита"
+ "with_unlimit": " вне суточного лимита",
+ "gifted": "подарил",
+ "you_gifted0": "К",
+ "you_gifted": " отправлен подарок",
+ "nft_token": "NFT-токен",
+ "burnt": "Сожжен",
+ "nft_issued": " выпустил ",
+ "nft_issued_for": " для ",
+ "nft_issued_cost": "за выпуск списано ",
+ "nft_sell": " выставил на продажу ",
+ "sold": " продал "
},
"user_profile": {
"unknown_account": "Неизвестный аккаунт",
@@ -1053,6 +1084,87 @@
"no_way_error": "Если вы заполнили одно из полей, то вы должны указать хотя бы один способ вывода или заполнить Дополнительно.",
"no_to_error": "Если вы указали вывод через перевод с заметкой, то укажите, куда переводить."
},
+ "nft_collections_jsx": {
+ "not_yet": "У вас нет своих собственных коллекций NFT-токенов.",
+ "not_yet2": "У ",
+ "not_yet3": " нет своих собственных коллекций NFT-токенов.",
+ "create": "Создать коллекцию",
+ "name_exists": "Коллекция с таким именем уже создана другим автором",
+ "tokens_exist": "Нельзя удалить коллекцию, в которой уже есть токены",
+ "volume_hint": "Объем торгов за все время",
+ "price_hint": "Последняя цена покупки"
+ },
+ "create_nft_collection_jsx": {
+ "title": "Создать коллекцию NFT",
+ "name": "Имя токена",
+ "coll_title": "Название",
+ "coll_descr": "Описание",
+ "image": "Ссылка на изображение",
+ "json_metadata": "JSON-метаданные",
+ "json_wrong": "Некорректный JSON. ",
+ "not_required": "(не обязательно)",
+ "token_count": "Количество токенов",
+ "infinity": "Бесконечное",
+ "create": "Создать",
+ "restore_json": "Вернуть по умолчанию",
+ "json_no_fields": "В JSON не хватает полей: ",
+ "json_fix": "Добавить недостающее"
+ },
+ "issue_nft_token_jsx": {
+ "title": "Выпустить NFT",
+ "issue_cost": "Будет списана плата за выпуск -",
+ "max_token_count": "Достигнуто максимальное кол-во токенов в коллекции."
+ },
+ "nft_tokens_jsx": {
+ "not_yet": "У вас пока нет NFT-токенов.",
+ "not_yet2": "У ",
+ "not_yet3": " пока нет NFT-токенов.",
+ "token_count": {
+ "zero": "0 токенов",
+ "one": "1 токен",
+ "other": "%(count)s токенов"
+ },
+ "sort": "Сортировка",
+ "sort_new": "Сначала новые",
+ "sort_old": "Сначала старые",
+ "sort_price": "По цене",
+ "sort_name": "По имени коллекции",
+ "cancel": "Отменить",
+ "cancel_hint": "Отменить продажу",
+ "selling_for": "Продается за ",
+ "buy": "Купить",
+ "buy2": "Купить за ",
+ "buy_confirm": "Вы уверены, что хотите купить ",
+ "buy_confirm2": " за ",
+ "your_token": "Ваш токен",
+ "owner": "Владелец"
+ },
+ "nft_collection_page_jsx": {
+ "owner_is": "Владелец коллекции - ",
+ "not_yet": "В этой коллекции еще нет токенов."
+ },
+ "nft_token_page_jsx": {
+ "issued": " выпустил токен",
+ "issued_for": " для",
+ "transfered": " отправил токен",
+ "selled": " выставил на продажу токен за ",
+ "selled2": " продал токен ",
+ "selled2m": " за ",
+ "bought": " купил токен",
+ "owner_is": "Владелец - ",
+ "burnt": "Этот NFT-токен существовал когда-то.",
+ "burnt2": "Но он был сожжен.",
+ "burnt3": "И все, что о нем напоминает, это маленькая горстка пепла в нашей серверной...",
+ "msg": "Написать владельцу"
+ },
+ "nft_token_sell_jsx": {
+ "fill_price": "Укажите цену"
+ },
+ "nft_market_page_jsx": {
+ "no_orders": "На бирже сейчас не продается ни один NFT-токен.",
+ "no_own_orders": "Вы пока не продаете NFT-токенов.",
+ "own_orders": "Мои ордеры"
+ },
"invites_jsx": {
"create_invite": "Создание чека",
"create_invite_info": "Чеки (инвайт-коды) — инструмент для передачи токенов другим людям вне блокчейна. Использовать чек можно двумя способами: перевести его баланс на аккаунт (форма для этого ниже) или зарегистрировать с его помощью новый аккаунт.",
diff --git a/app/redux/FetchDataSaga.js b/app/redux/FetchDataSaga.js
index 6c48c16..13af9ff 100644
--- a/app/redux/FetchDataSaga.js
+++ b/app/redux/FetchDataSaga.js
@@ -1,6 +1,7 @@
import { call, put, select, fork, cancelled, takeLatest, takeEvery } from 'redux-saga/effects';
import cookie from "react-cookie";
import {config, api} from 'golos-lib-js';
+import { Asset } from 'golos-lib-js/lib/utils'
import { getPinnedPosts, getMutedInNew } from 'app/utils/NormalizeProfile';
import { getBlockings, listBlockings } from 'app/redux/BlockingSaga'
@@ -11,6 +12,45 @@ import constants from './constants';
import { reveseTag, getFilterTags } from 'app/utils/tags';
import { CATEGORIES, SELECT_TAGS_KEY, DEBT_TOKEN_SHORT, LIQUID_TICKER } from 'app/client_config';
import { getAllPairs } from 'app/utils/market/utils'
+import { parseNFTImage, NFTImageStub } from 'app/utils/NFTUtils'
+
+function* fillNftCollectionImages(nft_collections) {
+ const noImgColls = {}
+ for (let i = 0; i < nft_collections.length; ++i) {
+ const nco = nft_collections[i]
+
+ nco.image = parseNFTImage(nco.json_metadata, false)
+ if (!nco.image) {
+ noImgColls[nco.name] = i
+ }
+ }
+
+ const noImgKeys = Object.keys(noImgColls)
+
+ const tokens = (yield call([api, api.getNftTokensAsync], {
+ select_collections: noImgKeys,
+ collection_limit: 1,
+ limit: 100,
+ collections: false,
+ orders: false,
+ }))
+
+ for (const token of tokens) {
+ const idx = noImgColls[token.name]
+ const nco = nft_collections[idx]
+ nco.image = parseNFTImage(token.json_metadata)
+ }
+}
+
+function* loadNftAssets(nft_assets, syms) {
+ if (syms.size) {
+ const assets = yield call([api, api.getAssets], '', [...syms])
+ for (const a of assets) {
+ const supply = Asset(a.supply)
+ nft_assets[supply.symbol] = a
+ }
+ }
+}
export function* fetchDataWatches () {
yield fork(watchLocationChange);
@@ -21,6 +61,9 @@ export function* fetchDataWatches () {
yield fork(watchFetchExchangeRates);
yield fork(watchFetchVestingDelegations);
yield fork(watchFetchUiaBalances);
+ yield fork(watchFetchNftTokens)
+ yield fork(watchFetchNftCollectionTokens)
+ yield fork(watchFetchNftMarket)
}
export function* watchGetContent() {
@@ -124,6 +167,37 @@ export function* fetchState(location_change_action) {
state.cprops = yield call([api, api.getChainPropertiesAsync])
break
+ case 'nft-collections':
+ state.nft_collections = (yield call([api, api.getNftCollectionsAsync], {
+ creator: uname,
+ limit: 100,
+ sort: 'by_created'
+ }))
+
+ try {
+ yield fillNftCollectionImages(state.nft_collections)
+
+ const syms = new Set()
+
+ for (const nco of state.nft_collections) {
+ nco.image = nco.image || NFTImageStub()
+
+ const price = Asset(nco.last_buy_price)
+ syms.add(price.symbol)
+ }
+
+ state.nft_assets = {}
+ yield loadNftAssets(state.nft_assets, syms)
+ } catch (err) {
+ console.error(err)
+ }
+
+ state.cprops = yield call([api, api.getChainPropertiesAsync])
+ break
+
+ case 'nft-tokens':
+ break
+
case 'invites':
state.cprops = yield call([api, api.getChainPropertiesAsync])
break
@@ -141,13 +215,52 @@ export function* fetchState(location_change_action) {
case 'witness':
state.witnesses[uname] = yield call([api, api.getWitnessByAccountAsync], uname)
break
+ case 'nft-history':
+ const nftHistory = yield call([api, api.getAccountHistoryAsync], uname, -1, 1000, {select_ops: ['nft_token', 'nft_transfer', 'nft_token_sold']})
+ account.nft_history = []
+
+ state.cprops = yield call([api, api.getChainPropertiesAsync])
+
+ const nft_token_ids = new Set()
+
+ nftHistory.forEach(operation => {
+ switch (operation[1].op[0]) {
+ case 'nft_token':
+ case 'nft_transfer':
+ case 'nft_token_sold':
+ const { token_id } = operation[1].op[1]
+ if (token_id !== 0) nft_token_ids.add(token_id)
+ state.accounts[uname].nft_history.push(operation)
+ break
+
+ default:
+ break
+ }
+ })
+
+ try {
+ if (nft_token_ids.size) {
+ state.nft_token_map = {}
+ const nft_tokens = (yield call([api, api.getNftTokensAsync], {
+ select_token_ids: [...nft_token_ids],
+ state: 'any'
+ }))
+ for (const nft of nft_tokens) {
+ state.nft_token_map[nft.token_id] = nft
+ }
+ }
+ } catch (err) {
+ console.error(err)
+ }
+ break
case 'transfers':
default:
- const history = yield call([api, api.getAccountHistoryAsync], uname, -1, 1000, {select_ops: ['donate', 'transfer', 'author_reward', 'curation_reward', 'transfer_to_tip', 'transfer_from_tip', 'transfer_to_vesting', 'withdraw_vesting', 'asset_issue', 'invite', 'transfer_to_savings', 'transfer_from_savings', 'convert_sbd_debt', 'convert', 'fill_convert_request', 'interest', 'worker_reward', 'account_freeze', 'unwanted_cost', 'unlimit_cost', 'subscription_payment'/*, 'subscription_prepaid_return'*/]})
+ const history = yield call([api, api.getAccountHistoryAsync], uname, -1, 1000, {select_ops: ['donate', 'transfer', 'author_reward', 'curation_reward', 'transfer_to_tip', 'transfer_from_tip', 'transfer_to_vesting', 'withdraw_vesting', 'asset_issue', 'invite', 'transfer_to_savings', 'transfer_from_savings', 'convert_sbd_debt', 'convert', 'fill_convert_request', 'interest', 'worker_reward', 'account_freeze', 'unwanted_cost', 'unlimit_cost', 'subscription_payment'/*, 'subscription_prepaid_return'*/, ]})
account.transfer_history = []
account.other_history = []
- state.cprops = yield call([api, api.getChainPropertiesAsync])
+ state.cprops = yield call([api, api.getChainPropertiesAsync])
+
history.forEach(operation => {
switch (operation[1].op[0]) {
case 'donate':
@@ -183,6 +296,33 @@ export function* fetchState(location_change_action) {
}
}
+ } else if (parts[0] === 'nft-tokens') {
+ if (parts[1]) {
+ state.nft_token = (yield call([api, api.getNftTokensAsync], {
+ select_token_ids: [parts[1]],
+ state: 'any'
+ }))
+ state.nft_token = state.nft_token[0]
+ state.nft_token_loaded = true
+
+ try {
+ const syms = new Set()
+
+ if (state.nft_token) {
+ state.nft_token.image = parseNFTImage(state.nft_token.json_metadata)
+
+ if (state.nft_token.order) {
+ const price = Asset(state.nft_token.order.price)
+ syms.add(price.symbol)
+ }
+ }
+
+ state.nft_assets = {}
+ yield loadNftAssets(state.nft_assets, syms)
+ } catch (err) {
+ console.error(err)
+ }
+ }
} else if (parts[0] === 'witnesses' || parts[0] === '~witnesses') {
state.witnesses = {};
@@ -387,3 +527,178 @@ export function* fetchUiaBalances({ payload: { account } }) {
console.error('fetchUiaBalances', err)
}
}
+
+export function* watchFetchNftTokens() {
+ yield takeLatest('global/FETCH_NFT_TOKENS', fetchNftTokens)
+}
+
+export function* fetchNftTokens({ payload: { account, start_token_id, sort, reverse_sort } }) {
+ try {
+ const limit = 20
+
+ const nft_tokens = yield call([api, api.getNftTokensAsync], {
+ owner: account,
+ collections: false,
+ start_token_id,
+ limit: limit + 1,
+ sort: sort || 'by_last_update',
+ selling_sorting: sort == 'by_last_price' ? 'sort_up_by_price': 'nothing',
+ reverse_sort: !!reverse_sort
+ })
+
+ let next_from
+ if (nft_tokens.length > limit) {
+ next_from = nft_tokens.pop().token_id
+ }
+
+ let nft_assets
+
+ try {
+ const syms = new Set()
+
+ for (const no of nft_tokens) {
+ no.image = parseNFTImage(no.json_metadata)
+
+ if (no.order) {
+ const price = Asset(no.order.price)
+ syms.add(price.symbol)
+ }
+ }
+
+ nft_assets = {}
+ yield loadNftAssets(nft_assets, syms)
+ } catch (err) {
+ console.error(err)
+ }
+
+ yield put(GlobalReducer.actions.receiveNftTokens({nft_tokens, start_token_id, next_from, nft_assets}))
+ } catch (err) {
+ console.error('fetchNftTokens', err)
+ }
+}
+
+export function* watchFetchNftCollectionTokens() {
+ yield takeLatest('global/FETCH_NFT_COLLECTION_TOKENS', fetchNftCollectionTokens)
+}
+
+export function* fetchNftCollectionTokens({ payload: { collectionName, start_token_id, sort, reverse_sort } }) {
+ try {
+ let nft_coll
+
+ if (!start_token_id) {
+ nft_coll = yield call([api, api.getNftCollectionsAsync], {
+ select_names: [collectionName],
+ limit: 1
+ })
+ nft_coll = nft_coll[0]
+ }
+
+ const limit = 20
+
+ const nft_tokens = yield call([api, api.getNftTokensAsync], {
+ select_collections: [collectionName],
+ start_token_id,
+ collections: false,
+ sort: sort || 'by_last_update',
+ reverse_sort: !!reverse_sort,
+ selling_sorting: 'sort_up_by_price',
+ limit: limit + 1
+ })
+
+ let next_from
+ if (nft_tokens.length > limit) {
+ next_from = nft_tokens.pop().token_id
+ }
+
+ let nft_assets
+
+ try {
+ const syms = new Set()
+
+ for (const no of nft_tokens) {
+ no.image = parseNFTImage(no.json_metadata)
+
+ if (no.order) {
+ const price = Asset(no.order.price)
+ syms.add(price.symbol)
+ }
+
+ const price = Asset(no.last_buy_price)
+ syms.add(price.symbol)
+ }
+
+ nft_assets = {}
+ yield loadNftAssets(nft_assets, syms)
+ } catch (err) {
+ console.error(err)
+ }
+
+ yield put(GlobalReducer.actions.receiveNftCollectionTokens({nft_coll, nft_tokens, start_token_id, next_from, nft_assets}))
+ } catch (err) {
+ console.error('fetchNftCollectionTokens', err)
+ }
+}
+
+export function* watchFetchNftMarket() {
+ yield takeLatest('global/FETCH_NFT_MARKET', fetchNftMarket)
+}
+
+export function* fetchNftMarket({ payload: { account, collectionName, start_order_id, sort, reverse_sort } }) {
+ try {
+ const limit = 20
+
+ const nft_orders = yield call([api, api.getNftOrdersAsync], {
+ filter_owners: [account],
+ select_collections: collectionName ? [collectionName] : undefined,
+ start_order_id,
+ type: 'selling',
+ sort: sort || 'by_price',
+ reverse_sort: !!reverse_sort,
+ limit: limit + 1
+ })
+
+ let next_from
+ if (nft_orders.length > limit) {
+ next_from = nft_orders.pop().order_id
+ }
+
+ const own_nft_orders = account ? yield call([api, api.getNftOrdersAsync], {
+ owner: account,
+ select_collections: collectionName ? [collectionName] : undefined,
+ start_order_id,
+ type: 'selling',
+ sort: sort || 'by_price',
+ reverse_sort: !!reverse_sort,
+ limit: limit + 1
+ }) : []
+
+ let nft_assets
+
+ try {
+ const syms = new Set()
+
+ for (const no of nft_orders) {
+ no.token.image = parseNFTImage(no.token.json_metadata)
+
+ const price = Asset(no.price)
+ syms.add(price.symbol)
+ }
+
+ for (const no of own_nft_orders) {
+ no.token.image = parseNFTImage(no.token.json_metadata)
+
+ const price = Asset(no.price)
+ syms.add(price.symbol)
+ }
+
+ nft_assets = {}
+ yield loadNftAssets(nft_assets, syms)
+ } catch (err) {
+ console.error(err)
+ }
+
+ yield put(GlobalReducer.actions.receiveNftMarket({nft_orders, own_nft_orders, start_order_id, next_from, nft_assets}))
+ } catch (err) {
+ console.error('fetchNftMarket', err)
+ }
+}
\ No newline at end of file
diff --git a/app/redux/GlobalReducer.js b/app/redux/GlobalReducer.js
index ce156bd..ae8a45c 100644
--- a/app/redux/GlobalReducer.js
+++ b/app/redux/GlobalReducer.js
@@ -6,6 +6,51 @@ import { fromJSGreedy } from 'app/utils/StateFunctions';
const emptyContentMap = Map(emptyContent);
+const upsertNftAssets = (state, nft_assets, start_token_id) => {
+ if (!start_token_id) {
+ state = state.set('nft_assets', fromJS(nft_assets))
+ } else {
+ state = state.update('nft_assets', data => {
+ data = data.merge(nft_assets)
+ return data
+ })
+ }
+ return state
+}
+
+const upsertPagedItems = (state, key, items, start_item_id, next_from) => {
+ if (!start_item_id) {
+ state = state.set(key, fromJS({
+ data: items,
+ next_from
+ }))
+ } else {
+ state = state.update(key, stateItems => {
+ stateItems = stateItems.update('data', data => {
+ for (const item of items) {
+ data = data.push(fromJS(item))
+ }
+ return data
+ })
+ stateItems = stateItems.set('next_from', next_from)
+ return stateItems
+ })
+ }
+ return state
+}
+
+const upsertNftTokens = (state, nft_tokens, start_token_id, next_from) => {
+ return upsertPagedItems(state, 'nft_tokens', nft_tokens, start_token_id, next_from)
+}
+
+const upsertNftOrders = (state, nft_orders, start_order_id, next_from) => {
+ return upsertPagedItems(state, 'nft_orders', nft_orders, start_order_id, next_from)
+}
+
+const upsertOwnNftOrders = (state, own_nft_orders, start_order_id, next_from) => {
+ return upsertPagedItems(state, 'own_nft_orders', own_nft_orders, start_order_id, next_from)
+}
+
export default createModule({
name: 'global',
initialState: Map({
@@ -52,7 +97,15 @@ export default createModule({
);
}
}
- let res = state.mergeDeep(payload)
+ let res = state
+ if (res.has('nft_collections'))
+ res = res.delete('nft_collections')
+ res = res.mergeDeep(payload)
+ if (!payload.has('nft_tokens')) {
+ if (!window.location.pathname.endsWith('/nft-tokens')) {
+ res = res.delete('nft_tokens')
+ }
+ }
return res
},
},
@@ -163,6 +216,56 @@ export default createModule({
return state.set('assets', fromJS(assets))
},
},
+ {
+ action: 'FETCH_NFT_TOKENS',
+ reducer: state => state,
+ },
+ {
+ action: 'RECEIVE_NFT_TOKENS',
+ reducer: (state, { payload: { nft_tokens, start_token_id, next_from, nft_assets } }) => {
+ let new_state = state
+ new_state = upsertNftTokens(new_state, nft_tokens, start_token_id, next_from)
+ if (nft_assets) {
+ new_state = upsertNftAssets(new_state, nft_assets, start_token_id)
+ }
+ return new_state
+ },
+ },
+ {
+ action: 'FETCH_NFT_COLLECTION_TOKENS',
+ reducer: state => state,
+ },
+ {
+ action: 'RECEIVE_NFT_COLLECTION_TOKENS',
+ reducer: (state, { payload: { nft_coll, nft_tokens, start_token_id, next_from, nft_assets } }) => {
+ let new_state = state
+ if (nft_coll) {
+ new_state = new_state.set('nft_collection', fromJS(nft_coll))
+ new_state = new_state.set('nft_collection_loaded', true)
+ }
+ new_state = upsertNftTokens(new_state, nft_tokens, start_token_id, next_from)
+ if (nft_assets) {
+ new_state = upsertNftAssets(new_state, nft_assets, start_token_id)
+ }
+ return new_state
+ },
+ },
+ {
+ action: 'FETCH_NFT_MARKET',
+ reducer: state => state,
+ },
+ {
+ action: 'RECEIVE_NFT_MARKET',
+ reducer: (state, { payload: { nft_orders, own_nft_orders, start_order_id, next_from, nft_assets } }) => {
+ let new_state = state
+ new_state = upsertNftOrders(new_state, nft_orders, start_order_id, next_from)
+ new_state = upsertOwnNftOrders(new_state, own_nft_orders)
+ if (nft_assets) {
+ new_state = upsertNftAssets(new_state, nft_assets, start_order_id)
+ }
+ return new_state
+ },
+ },
{
action: 'LINK_REPLY',
reducer: (state, { payload: op }) => {
diff --git a/app/redux/Transaction_Error.js b/app/redux/Transaction_Error.js
index 0ea51cd..e05e861 100644
--- a/app/redux/Transaction_Error.js
+++ b/app/redux/Transaction_Error.js
@@ -8,6 +8,8 @@ export default function transactionErrorReducer(
let errorStr = error.toString();
let errorKey = 'Transaction broadcast error.';
+ let handled = false
+
for (const [type] of operations) {
switch (type) {
case 'vote':
@@ -42,6 +44,21 @@ export default function transactionErrorReducer(
errorKey = errorStr = tt('invites_jsx.claim_wrong_secret_fatal');
}
break;
+ case 'nft_collection':
+ if (errorStr.includes('Object already exist')) {
+ errorKey = errorStr = tt('nft_collections_jsx.name_exists')
+ handled = true
+ }
+ break;
+ case 'nft_issue':
+ if (errorStr.includes('Account does not have sufficient funds')) {
+ errorKey = errorStr = tt('transfer_jsx.insufficient_funds')
+ handled = true
+ } else if (errorStr.includes('Cannot issue more tokens')) {
+ errorKey = errorStr = tt('issue_nft_token_jsx.max_token_count')
+ handled = true
+ }
+ break;
case 'withdraw_vesting':
if (
errorStr.includes(
@@ -64,7 +81,6 @@ export default function transactionErrorReducer(
break;
}
- let handled = false
if (errorStr.includes('You are blocked by user')) {
errorKey = errorStr = tt('chain_errors.user_blocked_user')
handled = true
diff --git a/app/utils/NFTUtils.js b/app/utils/NFTUtils.js
new file mode 100644
index 0000000..bd761da
--- /dev/null
+++ b/app/utils/NFTUtils.js
@@ -0,0 +1,13 @@
+
+export function NFTImageStub() {
+ return require('app/assets/images/nft.png')
+}
+
+export function parseNFTImage(json_metadata, useStub = true) {
+ if (json_metadata) {
+ const meta = JSON.parse(json_metadata)
+ if (meta) return meta.image
+ }
+ if (!useStub) return null
+ return NFTImageStub()
+}
diff --git a/package.json b/package.json
index 4aa7f8c..84e59a3 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
"foundation-sites": "^6.4.3",
"fs-extra": "^10.0.1",
"git-rev-sync": "^3.0.2",
- "golos-lib-js": "^0.9.53",
+ "golos-lib-js": "^0.9.60",
"history": "^2.0.0-rc2",
"immutable": "^3.8.2",
"intl": "^1.2.5",
diff --git a/yarn.lock b/yarn.lock
index ddbc36f..3b85059 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2185,14 +2185,15 @@ assert@^1.4.1:
util "0.10.3"
assert@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32"
- integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd"
+ integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==
dependencies:
- es6-object-assign "^1.1.0"
- is-nan "^1.2.1"
- object-is "^1.0.1"
- util "^0.12.0"
+ call-bind "^1.0.2"
+ is-nan "^1.3.2"
+ object-is "^1.1.5"
+ object.assign "^4.1.4"
+ util "^0.12.5"
assertion-error@^1.1.0:
version "1.1.0"
@@ -2970,9 +2971,9 @@ core-js@^2.4.0, core-js@^2.5.0:
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.17.3:
- version "3.30.0"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.0.tgz#64ac6f83bc7a49fd42807327051701d4b1478dea"
- integrity sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg==
+ version "3.33.0"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
+ integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==
core-js@^3.19.1, core-js@^3.6.0, core-js@^3.8.3:
version "3.25.0"
@@ -3605,11 +3606,6 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
-es6-object-assign@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
- integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==
-
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -4204,10 +4200,10 @@ globule@^1.0.0:
lodash "^4.17.21"
minimatch "~3.0.2"
-golos-lib-js@^0.9.53:
- version "0.9.53"
- resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.53.tgz#8469e0cfb5b0183bb0b161279ceb059907e5251e"
- integrity sha512-ipjFLKOXhhI31JuQzYsgZtZbFJQem15PRwU0OG+ocnQtUHB1CsU02GGJV0la3gL+K2KvHZdmKqd8XzGx8jgA4g==
+golos-lib-js@^0.9.60:
+ version "0.9.60"
+ resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.60.tgz#abf7e88499954312c817ebc38532e8e8d0451cbd"
+ integrity sha512-9UlH8OLTZj70godojecYTHIJ/6X+YW80zDVTYEq4qGDjZFlIAyczqu4UHlwZIxOtlZoyFMFtxMonWEXkw3nnPg==
dependencies:
abort-controller "^3.0.0"
assert "^2.0.0"
@@ -4849,7 +4845,7 @@ is-lambda@^1.0.1:
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==
-is-nan@^1.2.1:
+is-nan@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
@@ -6073,7 +6069,7 @@ object-inspect@^1.12.2:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
-object-is@^1.0.1:
+object-is@^1.0.1, object-is@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
@@ -7242,11 +7238,16 @@ selfsigned@^2.1.1:
dependencies:
node-forge "^1"
-"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^5.7.0:
+"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+semver@^5.5.0:
+ version "5.7.2"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
+ integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
+
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@@ -8148,7 +8149,7 @@ util@0.10.3:
safe-buffer "^5.1.2"
which-typed-array "^1.1.2"
-util@^0.12.0:
+util@^0.12.5:
version "0.12.5"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
@@ -8531,9 +8532,9 @@ ws@^5.2.0:
async-limiter "~1.0.0"
ws@^8.2.3:
- version "8.13.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
- integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
+ version "8.14.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f"
+ integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
ws@^8.4.2:
version "8.11.0"