From 4684f474ded33e049638313eaffcfd44586a20c1 Mon Sep 17 00:00:00 2001 From: 1aerostorm Date: Sat, 28 Oct 2023 03:41:23 +0000 Subject: [PATCH] HF 29 operations, NFT transfer URL --- package.json | 2 +- public/icons/dropdown-arrow.svg | 1 + public/images/nft.png | Bin 0 -> 17674 bytes src/App.scss | 7 +- src/elements/DropdownMenu.jsx | 73 +++++++ src/elements/DropdownMenu.scss | 53 +++++ src/elements/PagedDropdownMenu.jsx | 152 ++++++++++++++ src/elements/PagedDropdownMenu.scss | 16 ++ src/elements/VerticalMenu.jsx | 36 ++++ src/elements/VerticalMenu.scss | 74 +++++++ src/elements/nft/NFTSmallIcon.jsx | 12 ++ src/elements/nft/NFTSmallIcon.scss | 11 + src/elements/nft/NFTTokens.jsx | 90 +++++++++ src/elements/nft/NFTTokens.scss | 17 ++ src/locales/en.json | 16 +- src/locales/ru-RU.json | 16 +- src/pages/index.jsx | 10 +- src/pages/sign/transfer_nft.jsx | 300 ++++++++++++++++++++++++++++ src/pages/sign/transfer_nft.scss | 14 ++ src/server/oauth.js | 9 + src/utils/DomUtils.js | 5 + src/utils/LinkEx.jsx | 20 ++ src/utils/oauthPermissions.js | 64 ++++++ yarn.lock | 128 ++++++++++-- 24 files changed, 1100 insertions(+), 26 deletions(-) create mode 100644 public/icons/dropdown-arrow.svg create mode 100644 public/images/nft.png create mode 100644 src/elements/DropdownMenu.jsx create mode 100644 src/elements/DropdownMenu.scss create mode 100644 src/elements/PagedDropdownMenu.jsx create mode 100644 src/elements/PagedDropdownMenu.scss create mode 100644 src/elements/VerticalMenu.jsx create mode 100644 src/elements/VerticalMenu.scss create mode 100644 src/elements/nft/NFTSmallIcon.jsx create mode 100644 src/elements/nft/NFTSmallIcon.scss create mode 100644 src/elements/nft/NFTTokens.jsx create mode 100644 src/elements/nft/NFTTokens.scss create mode 100644 src/pages/sign/transfer_nft.jsx create mode 100644 src/pages/sign/transfer_nft.scss create mode 100644 src/utils/DomUtils.js create mode 100644 src/utils/LinkEx.jsx diff --git a/package.json b/package.json index fd1002f..3931009 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "formik": "^2.2.9", "git-rev-sync": "^3.0.1", "gmail-send": "^1.8.10", - "golos-lib-js": "^0.9.54", + "golos-lib-js": "^0.9.60", "iron-session": "6.0.4", "jspdf": "^2.3.0", "lodash": "^4.17.11", diff --git a/public/icons/dropdown-arrow.svg b/public/icons/dropdown-arrow.svg new file mode 100644 index 0000000..8828aac --- /dev/null +++ b/public/icons/dropdown-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/nft.png b/public/images/nft.png new file mode 100644 index 0000000000000000000000000000000000000000..db3321a57d2f6b2427cf5219df4ec4606da20a4b GIT binary patch literal 17674 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 bbf{>(El2stGjZ@eCJ!& literal 0 HcmV?d00001 diff --git a/src/App.scss b/src/App.scss index 74dd489..410e3af 100644 --- a/src/App.scss +++ b/src/App.scss @@ -6,9 +6,14 @@ @include foundation-everything(true); @import "./elements/AccountMenu"; +@import "./elements/DropdownMenu"; @import "./elements/GeneratedPasswordInput"; @import "./elements/Expandable"; @import "./elements/LoadingIndicator"; +@import "./elements/nft/NFTSmallIcon"; +@import "./elements/nft/NFTTokens"; +@import "./elements/PagedDropdownMenu"; +@import "./elements/VerticalMenu"; @import "./modules/Header"; @import "./modules/LoginForm"; @@ -21,7 +26,7 @@ @import "./pages/login"; @import "./pages/register"; @import "./pages/sign/transfer"; -@import "./pages/sign/transfer"; +@import "./pages/sign/transfer_nft"; @import "./pages/oauth/[client]/[perms]"; @import "./foundation-overrides"; diff --git a/src/elements/DropdownMenu.jsx b/src/elements/DropdownMenu.jsx new file mode 100644 index 0000000..5b247d4 --- /dev/null +++ b/src/elements/DropdownMenu.jsx @@ -0,0 +1,73 @@ +import React from 'react' + +import VerticalMenu from '@/elements/VerticalMenu' +import { findParent } from '@/utils/DomUtils' + +export default class DropdownMenu extends React.Component { + constructor(props) { + super(props); + this.state = { + shown: false, + selected: props.selected + }; + } + + componentWillUnmount() { + document.removeEventListener('click', this.hide); + } + + toggle = (e) => { + const {shown} = this.state + if(shown) this.hide(e) + else this.show(e) + } + + show = (e) => { + e.preventDefault(); + this.setState({shown: true}); + setTimeout(() => { + document.addEventListener('click', this.hide) + }, 1) + }; + + hide = (e) => { + // Do not hide the dropdown if there was a click within it. + const inside_dropdown = !!findParent(e.target, 'VerticalMenu'); + if (inside_dropdown) return; + + e.preventDefault(); + this.setState({shown: false}); + document.removeEventListener('click', this.hide); + }; + + navigate = (e) => { + const a = e.target.nodeName.toLowerCase() === 'a' ? e.target : e.target.parentNode; + this.setState({show: false}); + if (a.host !== window.location.host) return; + e.preventDefault(); + window.location.href = a.pathname + a.search + }; + + getSelectedLabel = (items, selected) => { + const selectedEntry = items.find(i => i.value === selected) + const selectedLabel = selectedEntry && selectedEntry.label ? selectedEntry.label : selected + return selectedLabel + } + + render() { + const {el, items, selected, children, className, title, href, onClick, noArrow, hideSelected} = this.props; + const hasDropdown = items.length > 0 + + let entry = children || + {this.getSelectedLabel(items, selected)} + {hasDropdown && !noArrow && } + + + if(hasDropdown) entry = { onClick(e); this.toggle(e) } : this.toggle}>{entry} + + const menu = ; + const cls = 'DropdownMenu' + (this.state.shown ? ' show' : '') + (className ? ` ${className}` : '') + return React.createElement(el, {className: cls}, [entry, menu]); + } +} + diff --git a/src/elements/DropdownMenu.scss b/src/elements/DropdownMenu.scss new file mode 100644 index 0000000..429ef04 --- /dev/null +++ b/src/elements/DropdownMenu.scss @@ -0,0 +1,53 @@ +.DropdownMenu { + position: relative; + display: inline-block; + + .Icon.dropdown-arrow { + top: 2px; + margin-right: 0; + } + + > .VerticalMenu { + visibility: hidden; + // min-width: 145px; + min-width: 232px; + z-index: 1000; + + background-color: white; + + display: block; + border: 1px solid $medium-gray; + border-radius: $global-radius; + opacity: 0; + position: absolute; + top: 100%; + + // width: auto; + transform: translateY(10%); + transition: all 0.3s ease 0s, visibility 0s linear 0.3s; + box-shadow: 1px 1px 5px 0px rgba(50, 50, 50, 0.75); + } + + &.show > .VerticalMenu { + visibility: visible; + opacity: 1; + transform: translateX(0%); + transition-delay: 0s; + } + + .DropdownMenu.move-left { + .VerticalMenu { + left: -50%; + } + } + + &.above > .VerticalMenu { + bottom: 100%; + top: auto; + } + + &.top-most > .VerticalMenu { + position: fixed; + top: auto; + } +} diff --git a/src/elements/PagedDropdownMenu.jsx b/src/elements/PagedDropdownMenu.jsx new file mode 100644 index 0000000..fe931b3 --- /dev/null +++ b/src/elements/PagedDropdownMenu.jsx @@ -0,0 +1,152 @@ +import React from 'react' +import cloneDeep from 'lodash/cloneDeep' +import isEqual from 'lodash/isEqual' +import tt from 'counterpart' + +import DropdownMenu from '@/elements/DropdownMenu' +import LoadingIndicator from '@/elements/LoadingIndicator' + +const hideLastItem = true; + +export default class PagedDropdownMenu extends React.Component { + static defaultProps = { + page: 1 + } + + constructor(props) { + super(props); + this.state = { + items: [], + page: props.page, + loading: false, + }; + } + + componentDidMount() { + const { items, page, } = this.props + this.initItems(this.sliceItems(items, page)) + this.setState({ page }) + } + + componentDidUpdate(prevProps) { + const { items, page, } = this.props + if (items && (!prevProps.items || !isEqual(items, prevProps.items))) { + const sliced = this.sliceItems(items, 1) + this.initItems(sliced) + this.setState({ page: 1 }) + } else if (page && prevProps.page !== page) { + this.setState({ page }) + } + } + + sliceItems = (items, page) => { + const { onLoadMore, perPage } = this.props + if (onLoadMore) { + return items + } + const startIdx = perPage * (page - 1) + const endIdx = startIdx + perPage + 1 + const sliced = items.slice(startIdx, endIdx) + return sliced + } + + initItems = (items) => { + if (!items || !items.length) + return; + this.setState({ + items: cloneDeep(items), + }); + }; + + loadMore = async (newPage) => { + const { items, page, } = this.state; + const { onLoadMore, } = this.props; + if (!onLoadMore) { + setTimeout(async () => { + this.setState({ + page: newPage + }, () => { + this.initItems(this.sliceItems(this.props.items, newPage)) + }) + }, 10); + return + } + setTimeout(async () => { + this.setState({ + page: newPage, + loading: true, + }); + if (onLoadMore) { + const res = await onLoadMore({ page, newPage, items, }); + this.setState({ + loading: false, + }); + this.initItems(res); + } + }, 10); + }; + + nextPage = () => { + const { page, } = this.state; + this.loadMore(page + 1); + }; + + prevPage = () => { + if (this.state.page === 1) return; + const { page, } = this.state; + this.loadMore(page - 1); + }; + + _renderPaginator = () => { + const { perPage, } = this.props; + const { items, page, } = this.state; + const hasMore = items.length > perPage; + if (page === 1 && !hasMore) { + return null; + } + const hasPrev = page > 1 + return { + value: + + {hasPrev ? '< ' + tt('g.back') : ''} + + {hasMore ? tt('g.more_list') + ' >' : ''} + , + }; + }; + + render() { + const { el, selected, children, className, title, href, noArrow, perPage, renderItem, hideSelected, } = this.props + const { items, loading, } = this.state; + + let itemsWithPaginator = []; + if (!loading) { + for (let i = 0; i < items.length; ++i) { + const rendered = renderItem(items[i]) + itemsWithPaginator.push(rendered) + } + if (items.length > perPage && hideLastItem) { + itemsWithPaginator.pop(); + } + const paginator = this._renderPaginator(); + if (paginator) { + itemsWithPaginator.push(paginator); + } + } else { + itemsWithPaginator = [{value: + + }]; + } + + return () + } +}; \ No newline at end of file diff --git a/src/elements/PagedDropdownMenu.scss b/src/elements/PagedDropdownMenu.scss new file mode 100644 index 0000000..a6c88ee --- /dev/null +++ b/src/elements/PagedDropdownMenu.scss @@ -0,0 +1,16 @@ +.PagedDropdownMenu__paginator { + display: inline-block !important; + text-align: center; + color: #0078C4 !important; + font-weight: normal !important; + font-size: 100% !important; + user-select: none; + cursor: pointer; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + width: 50%; + + &.disabled { + cursor: auto; + } +} diff --git a/src/elements/VerticalMenu.jsx b/src/elements/VerticalMenu.jsx new file mode 100644 index 0000000..1cc3923 --- /dev/null +++ b/src/elements/VerticalMenu.jsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import LinkEx from '@/utils/LinkEx' + +export default class VerticalMenu extends React.Component { + closeMenu = (e) => { + // If this was not a left click, or if CTRL or CMD were held, do not close the menu. + if(e.button !== 0 || e.ctrlKey || e.metaKey) return; + + // Simulate clicking of document body which will close any open menus + document.body.click(); + } + + render() { + const {items, title, description, className, hideValue} = this.props; + return
    + {title &&
  • {title}
  • } + {description &&
  • {description}
  • } + {items.map((i, k) => { + if(i.value === hideValue) return null + const target = i.target + return
  • + {i.link ? + {i.icon}{i.label ? i.label : i.value} + {i.data && {i.data}} +   {i.addon} + : + + {i.icon}{i.label ? i.label : i.value} + + } +
  • + })} +
; + } +} diff --git a/src/elements/VerticalMenu.scss b/src/elements/VerticalMenu.scss new file mode 100644 index 0000000..e8cbc65 --- /dev/null +++ b/src/elements/VerticalMenu.scss @@ -0,0 +1,74 @@ +.VerticalMenu { + + width: 200px; + + .Icon { + padding-left: 0.1rem; + margin-right: 1.3rem; + top: 0; + fill: $dark-gray; + } + + > li > a { + display: flex; + align-items: center !important; + line-height: 1rem; + position: relative; + padding: 0.7rem 1rem; + + font-size: 14px; + letter-spacing: 0.4px; + } + + > li > a:hover { + background-color: #f0f0f0; + } + + > li.title { + padding: 0.56rem; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.4px; + line-height: 16px; + text-align: center; + border-bottom: 1px solid $light-gray; + } + + > li.description { + padding: 0.1rem; + font-size: 87.5%; + text-align: center; + border-bottom: 1px solid $light-gray; + } + + &_nav-profile { + width: 262px; + + & > li > a { + padding: 0 1rem 0 3.8rem; + line-height: 50px; + + .Icon { + } + + &:hover { + .Icon { + fill: #3F46AD; + } + } + } + + } + + &_nav-additional { + width: 260px; + margin: 10px 0; + + & > li > a { + padding: 0 20px; + line-height: 50px; + } + } + +} + diff --git a/src/elements/nft/NFTSmallIcon.jsx b/src/elements/nft/NFTSmallIcon.jsx new file mode 100644 index 0000000..c3dba89 --- /dev/null +++ b/src/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/src/elements/nft/NFTSmallIcon.scss b/src/elements/nft/NFTSmallIcon.scss new file mode 100644 index 0000000..0ee5941 --- /dev/null +++ b/src/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/src/elements/nft/NFTTokens.jsx b/src/elements/nft/NFTTokens.jsx new file mode 100644 index 0000000..1fa9c1c --- /dev/null +++ b/src/elements/nft/NFTTokens.jsx @@ -0,0 +1,90 @@ +import React from 'react' +import tt from 'counterpart' + +import NFTSmallIcon from '@/elements/nft/NFTSmallIcon' +import PagedDropdownMenu from '@/elements/PagedDropdownMenu' +import DropdownMenu from '@/elements/DropdownMenu' + +export function NFTImageStub() { + return '/images/nft.png' +} + +export function parseNFTImage(json_metadata, useStub = true) { + if (json_metadata) { + const meta = JSON.parse(json_metadata) + if (meta && meta.image) return meta.image + } + if (!useStub) return null + return NFTImageStub() +} + +class NFTTokens extends React.Component { + render() { + let { tokens, selected } = this.props + + let selectedItem + + const items = [] + let i = 0 + for (const token of tokens) { + items.push({ + key: i, + link: '#', + value: token.token_id, + }) + + if (selected.toString() === token.token_id.toString()) { + const image = parseNFTImage(token.json_metadata) + + let data = {} + try { + data = JSON.parse(token.json_metadata) + } catch (err) {} + + selectedItem = + +   + + {data.title || '#' + token.token_id} + + + } + + ++i + } + + return { + const token = tokens[item.key] + + const image = parseNFTImage(token.json_metadata) + + let data = {} + try { + data = JSON.parse(token.json_metadata) + } catch (err) {} + + return { + ...item, + label: + +   + {data.title || '#' + token.token_id} + , + addon: {token.name}, + onClick: (e) => { + if (this.props.onItemClick) + this.props.onItemClick(e, token) + } + } + }} + selected={selected} + perPage={10} + hideSelected={tokens.length > 1}> + {selectedItem} + + + } +} + +export default NFTTokens diff --git a/src/elements/nft/NFTTokens.scss b/src/elements/nft/NFTTokens.scss new file mode 100644 index 0000000..6ee4480 --- /dev/null +++ b/src/elements/nft/NFTTokens.scss @@ -0,0 +1,17 @@ +.NFTTokens { + .NFTSmallIcon { + margin-top: 0.25rem; + margin-right: 0.25rem; + margin-bottom: 0.25rem; + width: 2rem; + height: 2rem; + } + + .VerticalMenu { + a { + padding: 5px !important; + padding-left: 8px !important; + color: black !important; + } + } +} diff --git a/src/locales/en.json b/src/locales/en.json index 20572ff..04df70c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -172,6 +172,7 @@ "transfer": "Transfer tokens", "donate": "Donate to user", "delegate_vs": "Delegate vesting shares", + "transfer_nft": "Transfer NFT-token", "unfreeze": "Unfreeze account", "apps_title": "Apps", "apps_empty": "You are not authorized in any app.", @@ -190,6 +191,11 @@ "memo_is_public": "This memo is public.", "submit": "Transfer" }, + "oauth_transfer_nft": { + "no_tokens": "You have not yet any NFT-tokens.", + "token_not_exist": "No such token in your wallet. You can transfer any of these tokens:", + "token_is_not_your": "This token is not your. You can transfer any of these tokens:" + }, "oauth_donate": { "submit": "Donate", "balance": "TIP-balance: " @@ -251,7 +257,15 @@ "worker_request_vote": "Voting for worker requests", "proposal_create": "Creating transaction proposals", "proposal_delete": "Removing transaction proposals", - "proposal_update": "Voting in transaction proposals" + "proposal_update": "Voting in transaction proposals", + "proposal_update_active": "Voting in transaction proposals with active authorities", + "paid_subscription_create": "Creating, deleting, updating paid subscriptions", + "paid_subscription_transfer": "Buying paid subscriptions", + "paid_subscription_cancel": "Canceling of paid subscriptions", + "nft_collection": "Creating and deleting NFT-collections", + "nft_issue": "Issueing of NFT-tokens", + "nft_transfer": "Transferring of NFT-tokens", + "nft_orders": "Selling, buying NFT-tokens" }, "recovery": { "change_title": "Change recovery account", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index 2b771f2..b6b56eb 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -172,6 +172,7 @@ "transfer": "Перевести токены", "donate": "Отблагодарить пользователя", "delegate_vs": "Делегировать Силу Голоса", + "transfer_nft": "Передать NFT-токен", "unfreeze": "Активировать аккаунт", "apps_title": "Приложения", "apps_empty": "Вы пока не авторизованы ни в одном приложении.", @@ -190,6 +191,11 @@ "memo_is_public": "Эта заметка является публичной.", "submit": "Перевести" }, + "oauth_transfer_nft": { + "no_tokens": "У вас пока еще нет NFT-токенов.", + "token_not_exist": "Выбранного токена не существует (или он не принадлежит вам). Вы можете передать любой из следующих токенов:", + "token_is_not_your": "Выбранный токен вам не принадлежит. Вы можете передать любой из следующих токенов:" + }, "oauth_donate": { "submit": "Передать", "balance": "TIP-баланс: " @@ -251,7 +257,15 @@ "worker_request_vote": "Голосование за заявки на работу (воркеры)", "proposal_create": "Создание пропозалов", "proposal_delete": "Удаление пропозалов", - "proposal_update": "Голосование в пропозалах" + "proposal_update": "Голосование в пропозалах", + "proposal_update_active": "Голосование в пропозалах с активным ключом", + "paid_subscription_create": "Создание, удаление и редактирование платных подписок", + "paid_subscription_transfer": "Покупка платных подписок", + "paid_subscription_cancel": "Отмена покупки платных подписок", + "nft_collection": "Создание и удаление коллекций NFT-токенов", + "nft_issue": "Выпуск NFT-токенов", + "nft_transfer": "Перевод NFT-токенов", + "nft_orders": "Покупка, продажа NFT-токенов" }, "recovery": { "change_title": "Задать аккаунт для восстановления", diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 0ae1fcf..7615d55 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -54,13 +54,11 @@ class Index extends React.Component { const { service_account, sign_endpoint, } = oauthCfg; let actions = []; for (let action of [ - 'transfer', 'donate', 'delegate_vs']) { + 'transfer', 'donate', 'delegate_vs', 'transfer_nft']) { actions.push( - - - + ); } let clientList = []; diff --git a/src/pages/sign/transfer_nft.jsx b/src/pages/sign/transfer_nft.jsx new file mode 100644 index 0000000..c322582 --- /dev/null +++ b/src/pages/sign/transfer_nft.jsx @@ -0,0 +1,300 @@ +import React from 'react'; +import tt from 'counterpart'; +import golos from 'golos-lib-js'; +import { Asset, } from 'golos-lib-js/lib/utils'; +import { Formik, Field, ErrorMessage, } from 'formik'; +import Head from 'next/head'; + +import LoadingIndicator from '@/elements/LoadingIndicator' +import LoginForm from '@/modules/LoginForm'; +import NFTTokens from '@/elements/nft/NFTTokens' +import Header from '@/modules/Header' +import { getOAuthCfg, getChainData, } from '@/server/oauth'; +import { getOAuthSession, } from '@/server/oauthSession'; +import { withSecureHeadersSSR, } from '@/server/security'; +import { callApi, } from '@/utils/OAuthClient'; +import validate_account_name from '@/utils/validate_account_name'; + +const uncompose = (query, initial) => { + initial.to = query.to || initial.to + if (query.token_id) initial.token_id = query.token_id + initial.memo = query.memo || initial.memo +}; + +export const getServerSideProps = withSecureHeadersSSR(async ({ req, res, resolvedUrl, query, }) => { + const action = resolvedUrl.split('?')[0].split('/')[2]; + let chainData = null; + const holder = await getOAuthSession(req, res); + if (!holder.oauthEnabled) { + return await holder.clearAndRedirect(); + } + const session = holder.session(); + let initial = null; + if (session.account) { + chainData = await getChainData(session.account, action); + if (chainData.frozen) { + return await holder.freeze(session.account) + } + initial = { + from: session.account, + to: '', + memo: '', + }; + uncompose(query, initial); + } + return { + props: { + action, + oauthCfg: getOAuthCfg(), + session, + chainData, + initial, + }, + }; +}) + +class TransferNFT extends React.Component { + static propTypes = { + }; + + state = { + }; + + componentDidMount() { + const { initial, chainData } = this.props + if (initial) { + if (initial.token_id) { + const { nft_tokens } = chainData + let exists = false + let not_your = false + let first + for (const token of nft_tokens) { + first = first || token.token_id + if (token.token_id.toString() === initial.token_id.toString()) { + exists = true + if (token.owner !== initial.from) { + not_your = true + } + break + } + } + this.setState({ + selected: exists ? initial.token_id : first, + query_token_error: !exists ? tt('oauth_transfer_nft.token_not_exist') : not_your ? tt('oauth_transfer_nft.token_is_not_your') : null + }) + } else { + const { nft_tokens } = chainData + if (nft_tokens.length) { + this.setState({ + selected: nft_tokens[0].token_id + }) + } + } + } + } + + normalizeChainData = () => { + const { chainData, } = this.props; + if (!chainData || !golos.isNativeLibLoaded()) { + return; + } + return chainData + } + + onToChange = (e, handle) => { + let value = e.target.value.trim().toLowerCase(); + e.target.value = value; + return handle(e); + }; + + validate = (values) => { + const errors = {}; + if (!values.to) { + errors.to = tt('g.required'); + } else { + const err = validate_account_name(values.to); + if (err) errors.to = err; + } + + return errors; + }; + + _onSubmit = (values, { setSubmitting, }) => { + this.setState({ + done: false, + }) + + const { action, session, oauthCfg, } = this.props; + const { sign_endpoint, } = oauthCfg; + const { from, to, } = values; + const { selected } = this.state + + let memo = values.memo || ''; + + golos.config.set('websocket', sign_endpoint); + golos.config.set('credentials', 'include'); + const callback = (err, res) => { + setSubmitting(false); + if (err) { + alert(err); + return; + } + window.close(); + this.setState({ + done: true, + }) + }; + golos.broadcast.nftTransfer('', parseInt(selected), from, to, memo, callback); + }; + + compose = (values) => { + if (!$GLS_IsBrowser) { + return; + } + + let url = window.location.href.split('?')[0]; + + const chainData = this.normalizeChainData() + const { to, memo, } = values; + + if (!golos.isNativeLibLoaded() || !chainData) + return '...'; + + url += '?'; + url += 'to=' + (to || ''); + if (this.state.selected) { + url += '&token_id=' + this.state.selected + } + url += '&memo=' + (memo || ''); + + return ( + {tt('oauth_main_jsx.link')} + {url} + ); + }; + + render() { + const { state, } = this; + const { done, selected, query_token_error } = state; + const chainData = this.normalizeChainData() + + const { action, oauthCfg, session, initial, } = this.props; + const { account, } = session; + + if (account === null) { + return (
+ + + {tt('oauth_main_jsx.' + action)} | {tt('oauth_main_jsx.title')} + +
+ +
); + } + + if (!chainData) + return null + + const { nft_tokens } = chainData + + let form = null; + if (initial && chainData) form = ( + {({ + handleSubmit, isSubmitting, isValid, dirty, errors, touched, values, handleChange, setFieldValue, + }) => ( +
+
+ @ + +
+ +
+ @ + this.onToChange(e, handleChange)} + /> +
+ + + {query_token_error &&
{query_token_error}
} + + {nft_tokens.length ? + { + e.preventDefault() + this.setState({ + selected: token.token_id, + query_token_error: null + }) + }} + /> : +
{tt('oauth_transfer_nft.no_tokens')}
} + + + + {isSubmitting && } + + {done ? + {tt('g.done')} + : null} +
+ {this.compose(values)} +
+ + )} +
); + + return (
+ + + {tt('oauth_main_jsx.' + action)} | {tt('oauth_main_jsx.title')} + +
+
+
+

{tt('oauth_main_jsx.' + action)}

+ {(!account || !form) && } + {form} +
+
+
); + } +}; + +export default TransferNFT diff --git a/src/pages/sign/transfer_nft.scss b/src/pages/sign/transfer_nft.scss new file mode 100644 index 0000000..baec762 --- /dev/null +++ b/src/pages/sign/transfer_nft.scss @@ -0,0 +1,14 @@ +.TransferNFT { + .error { + margin-bottom: 1rem; + } + .button { + margin-bottom: 0rem; + } + .done { + margin-left: 0.5rem; + } + .page-link { + margin-top: 1rem; + } +} diff --git a/src/server/oauth.js b/src/server/oauth.js index 498ecab..6bf9635 100644 --- a/src/server/oauth.js +++ b/src/server/oauth.js @@ -159,6 +159,15 @@ async function getChainData(account, action = 'transfer') { if (data.balances['GBG']) data.balances['GBG'] = acc.sbd_balance; + if (action === 'transfer_nft') { + data.nft_tokens = await golos.api.getNftTokensAsync({ + owner: account, + state: 'not_selling_only', + limit: 100, + }) + return data; + } + let uia = await golos.api.getAccountsBalancesAsync([account]); if (!uia[0]) { return data; diff --git a/src/utils/DomUtils.js b/src/utils/DomUtils.js new file mode 100644 index 0000000..36021a8 --- /dev/null +++ b/src/utils/DomUtils.js @@ -0,0 +1,5 @@ +export function findParent(el, class_name) { + if (el.className && el.className.indexOf && el.className.indexOf(class_name) !== -1) return el; + if (el.parentNode) return findParent(el.parentNode, class_name); + return null; +} diff --git a/src/utils/LinkEx.jsx b/src/utils/LinkEx.jsx new file mode 100644 index 0000000..62fd5f7 --- /dev/null +++ b/src/utils/LinkEx.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import Link from 'next/link' + +const isExternal = (url) => { + return /^https?:\/\//.test(url) +} + +class LinkEx extends React.Component { + render() { + const { props } = this + const { href, children } = props + if (isExternal(href) || href === '#') { + const rel = props.rel || 'noopener noreferrer' + return {children} + } + return {children} + } +} + +export default LinkEx diff --git a/src/utils/oauthPermissions.js b/src/utils/oauthPermissions.js index b96c2de..7ee8ec1 100644 --- a/src/utils/oauthPermissions.js +++ b/src/utils/oauthPermissions.js @@ -413,6 +413,70 @@ let permissions = { }, internal: true, }, + paid_subscription_create: { + ops: [ + 'paid_subscription_create', + 'paid_subscription_update', + 'paid_subscription_delete', + ], + cond: (op) => { + return [POSTING, op.author]; + }, + }, + paid_subscription_transfer: { + ops: [ + 'paid_subscription_transfer', + ], + cond: (op) => { + if (op.from_tip) { + return [POSTING, op.from] + } + return [ACTIVE, op.from] + }, + }, + paid_subscription_cancel: { + ops: [ + 'paid_subscription_cancel', + ], + cond: (op) => { + return [POSTING, op.subscriber] + }, + }, + nft_collection: { + ops: [ + 'nft_collection', + 'nft_collection_delete', + ], + cond: (op) => { + return [ACTIVE, op.creator] + }, + }, + nft_issue: { + ops: [ + 'nft_issue', + ], + cond: (op) => { + return [ACTIVE, op.creator] + }, + }, + nft_transfer: { + ops: [ + 'nft_transfer', + ], + cond: (op) => { + return [ACTIVE, op.from] + }, + }, + nft_orders: { + ops: [ + 'nft_sell', + 'nft_buy', + 'nft_cancel_order', + ], + cond: (op) => { + return [ACTIVE, op.seller || op.owner || op.buyer] + }, + }, }; module.exports = { diff --git a/yarn.lock b/yarn.lock index ca30db4..03de80c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1072,7 +1072,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assert@2.0.0, assert@^2.0.0: +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== @@ -1082,6 +1082,17 @@ assert@2.0.0, assert@^2.0.0: object-is "^1.0.1" util "^0.12.0" +assert@^2.0.0: + 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: + call-bind "^1.0.2" + is-nan "^1.3.2" + object-is "^1.1.5" + object.assign "^4.1.4" + util "^0.12.5" + ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" @@ -1706,9 +1717,9 @@ core-js@^2.4.0: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.17.3: - version "3.32.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.0.tgz#7643d353d899747ab1f8b03d2803b0312a0fb3b6" - integrity sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww== + version "3.33.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.1.tgz#ef3766cfa382482d0a2c2bc5cb52c6d88805da52" + integrity sha512-qVSq3s+d4+GsqN0teRCJtM6tdEEXyWxjzbhVrCHmBS5ZTM0FS2MOS0D13dUXAWDUN6a+lHI/N1hF9Ytz6iLl9Q== core-js@^3.6.0: version "3.16.4" @@ -2005,6 +2016,15 @@ deepmerge@^2.1.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== +define-data-property@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -2012,6 +2032,15 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +define-properties@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -2743,6 +2772,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -2783,6 +2817,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-orientation@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/get-orientation/-/get-orientation-1.1.2.tgz#20507928951814f8a91ded0a0e67b29dfab98947" @@ -2924,10 +2968,10 @@ gmail-send@^1.8.10: lodash "^4.17.15" nodemailer "^6.3.0" -golos-lib-js@^0.9.54: - version "0.9.54" - resolved "https://registry.yarnpkg.com/golos-lib-js/-/golos-lib-js-0.9.54.tgz#87399d038c948f5d447e457df768c5d3fb26c8b2" - integrity sha512-YyvWp3QL5jH+1PbYl23lCIIVz0znNWytCZPAfzk4SKbGv3PRYXRB/Jp8ZZNjmJsXMby5+ZB5imRLFZo+ggfXXg== +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" @@ -2951,6 +2995,13 @@ golos-lib-js@^0.9.54: stream-browserify "^3.0.0" ws "^8.2.3" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@4.1.15: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" @@ -3001,11 +3052,28 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== + dependencies: + get-intrinsic "^1.2.2" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -3042,6 +3110,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -3369,7 +3444,7 @@ is-installed-globally@~0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-nan@^1.2.1: +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== @@ -4038,9 +4113,9 @@ node-fetch@2.6.1: integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== node-fetch@^2.6.12: - version "2.6.12" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba" - integrity sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g== + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" @@ -4172,7 +4247,7 @@ object-inspect@^1.11.0, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== -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== @@ -4195,6 +4270,16 @@ object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" @@ -5903,6 +5988,17 @@ util@0.12.4, util@^0.12.0: safe-buffer "^5.1.2" which-typed-array "^1.1.2" +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== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + utils-merge@1.x.x: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -6098,9 +6194,9 @@ wrappy@1: integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 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== xtend@^4.0.2: version "4.0.2"