e.stopPropagation()}>
{children}
diff --git a/app/components/NameClaimModal/index.js b/app/components/NameClaimModal/index.js
new file mode 100644
index 000000000..e8466fded
--- /dev/null
+++ b/app/components/NameClaimModal/index.js
@@ -0,0 +1,1063 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import classNames from "classnames";
+import BackArrowIcon from "../../assets/images/arrow-back-blue.svg";
+import "./name-claim-modal.scss";
+import Modal from "../Modal";
+import {ensureDot} from "../../utils/nameHelpers";
+import {displayBalance} from "../../utils/balances";
+import {clientStub as wClientStub} from "../../background/wallet/client";
+import {constants, dnssec, wire, Ownership, util} from 'bns';
+import blake2b from "bcrypto/lib/blake2b";
+import fs from "fs";
+const { dialog } = require("electron").remote;
+import {clientStub as nClientStub} from "../../background/node/client";
+import Alert from "../Alert";
+import CopyButton from "../CopyButton";
+import {HeaderItem, HeaderRow, Table, TableItem, TableRow} from "../Table";
+import {MiniModal} from "../Modal/MiniModal";
+import { shell } from 'electron';
+
+const {Proof} = Ownership;
+const wallet = wClientStub(() => require('electron').ipcRenderer);
+const node = nClientStub(() => require('electron').ipcRenderer);
+const {
+ keyFlags,
+ classes,
+ types
+} = constants;
+
+const {
+ Record,
+ TXTRecord
+} = wire;
+
+const STEP = {
+ CHECK_NAME: 'CHECK_NAME',
+ CHOOSE_OPTIONS: 'CHOOSE_OPTIONS',
+ CLAIM: 'CLAIM',
+};
+
+const CLAIM_TYPE = {
+ TXT: 'TXT',
+ DNSSEC: 'DNSSEC',
+ RRSIG: 'RRSIG',
+ BASE64: 'BASE64',
+};
+
+@connect(
+ state => ({
+
+ }),
+)
+export default class NameClaimModal extends Component {
+ static propTypes = {
+ onClose: PropTypes.func.isRequired,
+ name: PropTypes.string,
+ };
+
+ state = {
+ stepType: STEP.CHECK_NAME,
+ claimType: null,
+ name: this.props.name,
+ url: '',
+ error: '',
+ claim: {reward: '', fee: '', txt: ''},
+ proof: null,
+ keys: [],
+ txt: '',
+ success: null,
+ };
+
+ async componentWillMount() {
+ if (this.props.name) {
+ this.getClaim(this.props.name, null);
+ }
+ }
+
+ changeClaimType = (claimType) => {
+ this.setState({ claimType });
+ };
+
+ handleInputValueChange = async (e) => {
+ let { value } = e.target;
+ value = value.toLowerCase();
+ this.setState({
+ url: value
+ })
+ };
+
+ searchReserved = async (rawURL) => {
+ const url = ensureDot(rawURL);
+ const labels = url.split('.');
+ const name = labels[0];
+
+ return this.getClaim(name, url);
+ };
+
+ getClaim = async (name, url) => {
+ let claim;
+ try {
+ claim = await wallet.createClaim(name);
+ } catch(e) {
+ this.setState({
+ name: '',
+ error: e.message,
+ claim: {reward: '', fee: '', txt: ''},
+ proof: null,
+ keys: [],
+ txt: '',
+ url: '',
+ });
+ return;
+ }
+
+ if (url && claim.target !== url) {
+ this.setState({
+ name: '',
+ error: `"${name}" is reserved, but belongs to "${claim.target}", not "${url}"`,
+ claim: {reward: '', fee: '', txt: ''},
+ proof: null,
+ keys: [],
+ txt: '',
+ url: '',
+ });
+ return;
+ }
+
+ const reward = displayBalance(claim.value, true);
+ let fee = displayBalance(claim.fee, true);
+ while (reward.length > fee.length)
+ fee = ' ' + fee;
+ const txt = claim.txt;
+
+ this.setState({
+ name,
+ url: claim.target,
+ claim: {reward, fee, txt},
+ error: '',
+ });
+
+ const txtRecord = this.getTXT();
+ this.setState({
+ txt: txtRecord.toString(),
+ });
+
+ // Check DNSSEC for this name
+ let json;
+ try {
+ json = await node.getDNSSECProof(claim.target);
+ } catch(e) {
+ // If we catch an error here, the user should be alerted.
+ // They may need to set up DNSSEC for their own name,
+ // or their parent zone does.
+ this.setState({
+ error: `"${name}" can not be claimed at this time: ${e.message}`,
+ proof: null,
+ keys: [],
+ txt: '',
+ });
+ return;
+ }
+
+ this.setState({
+ proof: Proof.fromJSON(json)
+ });
+
+ this.getKeys();
+ };
+
+ submitClaim = async () => {
+ let json;
+ try {
+ json = await wallet.sendClaim(this.state.name);
+ } catch(e) {
+ this.setState({
+ error: `Error sending claim: ${e.message}`,
+ });
+ return;
+ }
+
+ const proof = Proof.fromJSON(json);
+ const data = proof.encode();
+ const hash = blake2b.digest(data).toString('hex');
+
+ this.setState({success: hash});
+ };
+
+ getTXT() {
+ const rr = new Record();
+ const rd = new TXTRecord();
+
+ rr.name = this.state.url;
+ rr.type = types.TXT;
+ rr.class = classes.IN;
+ rr.ttl = 3600;
+ rr.data = rd;
+
+ rd.txt.push(this.state.claim.txt);
+
+ return rr;
+ }
+
+ getKeys() {
+ let keys = [];
+ if (!this.state.proof)
+ return keys;
+
+ const proof = this.state.proof;
+ const zone = proof.zones[proof.zones.length - 1];
+
+ for (const key of zone.keys) {
+ if (key.type !== types.DNSKEY)
+ continue;
+
+ const kd = key.data;
+
+ if (!(kd.flags & keyFlags.ZONE))
+ continue;
+
+ if (kd.flags & keyFlags.SEP)
+ continue;
+
+ if (kd.flags & keyFlags.REVOKE)
+ continue;
+
+ key.filename = dnssec.filename(
+ this.state.url,
+ key.data.algorithm,
+ key.data.keyTag()
+ ) + '.private';
+ keys.push(key);
+ }
+
+ this.setState({
+ keys
+ });
+ }
+
+ localSign = async () => {
+ if (!this.state.keys.length || !this.state.proof)
+ return;
+
+ const filenames = [];
+ for (let key of this.state.keys) {
+ filenames.push(key.filename);
+ }
+ const list = filenames.join(', ');
+
+
+ const filePaths = await dialog.showOpenDialog({
+ title: `Open DNSSEC private key file: ${list}`,
+ message: `Open DNSSEC private key file: ${list}`,
+ properties: ["openFile"],
+ filters: {
+ name: "BIND Private Key",
+ extensions: ["private"]
+ }
+ });
+
+ if (!filePaths.filePaths.length)
+ return;
+
+ const filePath = filePaths.filePaths[0];
+
+ let fileName = filePath.split('/');
+
+ fileName = fileName[fileName.length - 1];
+
+ let found = false;
+ for (const acceptable of filenames) {
+ if (fileName === acceptable) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ this.setState({
+ error: `Can not sign with key ${fileName}`,
+ });
+ return;
+ }
+
+ const keyFile = fs.readFileSync(filePath, 'utf-8');
+ const [alg, privateKey] = dnssec.decodePrivate(keyFile);
+
+ const rr = this.getTXT();
+ const lifespan = 365 * 24 * 60 * 60;
+
+ const sig = dnssec.sign(this.state.keys[0], privateKey, [rr], lifespan);
+
+ const proof = this.state.proof;
+ const zone = proof.zones[proof.zones.length - 1];
+ zone.claim.push(rr);
+ zone.claim.push(sig);
+
+ let json;
+ try {
+ json = await node.sendRawClaim(proof.toBase64());
+ } catch (e) {
+ this.setState({
+ error: `Error sending claim: ${e.message}`,
+ });
+ return;
+ }
+
+ this.setState({success: json});
+ }
+
+ insertRecords = async (pasteRRs) => {
+ if (!this.state.proof) {
+ return;
+ }
+
+ const chunk = pasteRRs;
+ let RRs;
+
+ this.setState({error: ''});
+
+ try {
+ RRs = wire.fromZone(chunk);
+ } catch (e) {
+ this.setState({
+ error: `Error processing records: ${e.message}`,
+ });
+ return;
+ }
+
+ if (RRs.length !== 2) {
+ this.setState({
+ error: 'Exactly two records expected (TXT & RRSIG)',
+ });
+ return;
+ }
+
+ const rr = RRs[0];
+ const sig = RRs[1];
+
+ if (rr.type !== types.TXT || sig.type !== types.RRSIG) {
+ this.setState({
+ error: 'Only single TXT and single RRSIG allowed.',
+ });
+ return;
+ }
+
+ const proof = this.state.proof;
+ const zone = proof.zones[proof.zones.length - 1];
+
+ // clear other TXT records
+ zone.claim.length = 0;
+
+ zone.claim.push(rr);
+ zone.claim.push(sig);
+
+ let json;
+ try {
+ this.verifyProof(proof);
+ json = await node.sendRawClaim(proof.toBase64());
+ } catch (e) {
+ this.setState({
+ error: `Error sending claim: ${e.message}`,
+ });
+ return;
+ }
+
+ this.setState({success: json});
+ };
+
+ sendRawClaim = async (blob) => {
+ this.setState({error: ''});
+
+ // Verify first to be nice
+ let proof;
+
+ try {
+ if (!blob) {
+ throw new Error('Not a valid base64 string.');
+ }
+
+ const data = Buffer.from(blob, 'base64');
+ proof = Proof.decode(data);
+ } catch (e) {
+ this.setState({
+ error: `Error decoding base64: ${e.message}`,
+ });
+ return;
+ }
+
+ let json;
+ try {
+ this.verifyProof(proof);
+ json = await node.sendRawClaim(blob);
+ } catch (e) {
+ this.setState({
+ error: `Error sending claim: ${e.message}`,
+ });
+ return;
+ }
+
+ this.setState({success: json});
+ };
+
+ verifyProof(proof) {
+ const o = new Ownership();
+
+ if (!o.isSane(proof))
+ throw new Error('Malformed DNSSEC proof.');
+
+ if (!o.verifySignatures(proof))
+ throw new Error('Invalid DNSSEC proof signatures.');
+
+ if (!o.verifyTimes(proof, util.now()))
+ throw new Error('Proof contains expired signature.');
+
+ return true;
+ }
+
+ render() {
+ const {
+ onClose,
+ } = this.props;
+
+ return (
+
+ { this.renderContent() }
+
+ );
+ }
+
+ renderContent() {
+ const {
+ stepType,
+ url,
+ name,
+ claim,
+ error,
+ keys,
+ } = this.state;
+
+ const {
+ onClose,
+ } = this.props;
+
+ if (this.state.success) {
+ return (
+
+
+
+
+
+
+
+ You have submitted your name claim successfully!
+
+
+ It may take up to 15 minutes to be confirmed in the coinbase transaction of a block.
+ Your claim ID is not like a transaction ID and will not be found by most block explorers.
+ It may still be useful in debugging from the logs.
+
+
+ Your claim ID: {this.state.success}
+
+
+
+
+
+
+ Finish
+
+
+
+ )
+ }
+
+ switch (stepType) {
+ case STEP.CLAIM:
+ return (
+
this.setState({ stepType: STEP.CHOOSE_OPTIONS, error: '' })}
+ submitClaim={this.submitClaim}
+ localSign={this.localSign}
+ insertRecords={this.insertRecords}
+ sendRawClaim={this.sendRawClaim}
+ />
+ );
+ case STEP.CHOOSE_OPTIONS:
+ return (
+ this.setState({ stepType: STEP.CLAIM })}
+ onBack={() => this.setState({ stepType: STEP.CHECK_NAME })}
+ />
+ );
+ case STEP.CHECK_NAME:
+ default:
+ return (
+ this.setState({ stepType: STEP.CHOOSE_OPTIONS })}
+ />
+ );
+ }
+ }
+}
+
+class ClaimOption extends Component {
+ static propTypes = {
+ onClose: PropTypes.func.isRequired,
+ onBack: PropTypes.func.isRequired,
+ submitClaim: PropTypes.func.isRequired,
+ localSign: PropTypes.func.isRequired,
+ insertRecords: PropTypes.func.isRequired,
+ sendRawClaim: PropTypes.func.isRequired,
+ claimType: PropTypes.string,
+ url: PropTypes.string,
+ reward: PropTypes.string,
+ fee: PropTypes.string,
+ txt: PropTypes.string,
+ keys: PropTypes.object,
+ error: PropTypes.string,
+ };
+
+ state = {
+ pasteRRs: '',
+ blob: '',
+ };
+
+ render() {
+ const {
+ onClose,
+ onBack,
+ url,
+ name,
+ reward,
+ fee,
+ error,
+ } = this.props;
+
+ return (
+
+
+
+
Reserved Name Claim
+
✕
+
+
+
+ { this.renderContent() }
+
+ { !!error &&
{error} }
+
+ { this.renderCTA() }
+
+
+ );
+ }
+
+ renderCTA () {
+ const {
+ submitClaim,
+ localSign,
+ insertRecords,
+ sendRawClaim,
+ } = this.props;
+
+ const {
+ pasteRRs,
+ blob,
+ } = this.state;
+
+ switch (this.props.claimType) {
+ case CLAIM_TYPE.TXT:
+ return (
+
+ Check DNS
+
+ );
+ case CLAIM_TYPE.DNSSEC:
+ return (
+
+ Sign with local key
+
+ );
+ case CLAIM_TYPE.RRSIG:
+ return (
+ insertRecords(pasteRRs)}
+ disabled={!pasteRRs}
+ >
+ Insert records into claim
+
+ );
+ case CLAIM_TYPE.BASE64:
+ return (
+ sendRawClaim(blob)}
+ disabled={!blob}
+ >
+ Send raw claim
+
+ );
+ default:
+ return null;
+ }
+ }
+
+ renderContent() {
+ switch (this.props.claimType) {
+ case CLAIM_TYPE.TXT:
+ return this.renderTXT();
+ case CLAIM_TYPE.DNSSEC:
+ return this.renderDNSSEC();
+ case CLAIM_TYPE.RRSIG:
+ return this.renderRRSIG();
+ case CLAIM_TYPE.BASE64:
+ return this.renderBASE64();
+ default:
+ return null;
+ }
+ }
+
+ renderBASE64() {
+ const {
+ txt,
+ url,
+ } = this.props;
+
+ const fakeproof = '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';
+
+ return (
+
+
+
+
+
+
+ text string
+
+
+
+
+
+ {txt}
+
+
+
+
+
+ );
+ }
+
+ renderRRSIG() {
+ const {
+ txt,
+ url,
+ } = this.props;
+
+ const faketxt = (!!url && !!txt)
+ ? `${url} 300 IN TXT "${txt}"`
+ : '';
+ const fakesig = (!!url && !!txt)
+ ? `${url} 300 IN RRSIG TXT 13 2 300 20210603180907 20210601160907 34505 ${url} f6x5CBP1ySenfPodSGSPNPCdzLzhlXK8shtpfzcEmCs09amCSqCIwniq eEIR1EYCuijP4OCKFyEnEhfEk+l81A==`
+ : '';
+ const fakeRRs = faketxt + '\n\n' + fakesig;
+
+ return (
+
+
+
+
+ Type
+ Host/Name
+ Value
+
+
+ TXT
+ {url}
+
+ {txt}
+
+
+
+
+
+ );
+ }
+
+ renderTXT() {
+ const {
+ txt,
+ url,
+ } = this.props;
+
+ return (
+
+
+ Create a TXT record with the below string in your zone using your domain name service provider, then click "Check DNS".
+
+
+
+ Type
+ Host/Name
+ Value
+
+
+ TXT
+ {url}
+
+ {txt}
+
+
+
+
+
+ );
+ }
+
+ renderDNSSEC() {
+ const rows = [];
+ for (const key of this.props.keys) {
+ const filename = key.filename;
+ const json = key.getJSON();
+
+ rows.push(
+
+ {json.data.keyTag}
+ {json.data.algName}
+ {filename}
+
+ );
+ }
+
+ return (
+
+
+ Select your DNSSEC zone-signing private key to automatically sign and broadcast the claim proof.
+
+
+
+ Key tag
+ Key algorithm
+ Filename
+
+ {rows}
+
+
+ );
+ }
+}
+
+class ChooseOptions extends Component {
+ static propTypes = {
+ onClose: PropTypes.func.isRequired,
+ onBack: PropTypes.func.isRequired,
+ onNext: PropTypes.func.isRequired,
+ changeClaimType: PropTypes.func.isRequired,
+ url: PropTypes.string,
+ reward: PropTypes.string,
+ fee: PropTypes.string,
+ error: PropTypes.string,
+ };
+
+ render() {
+ const {
+ onClose,
+ onBack,
+ onNext,
+ url,
+ name,
+ reward,
+ fee,
+ error,
+ } = this.props;
+
+ return (
+
+
+
+
Reserved Name Claim
+
✕
+
+
+
+
+
+ How is your domain's DNSSEC managed?
+
+
+ { this.renderOption('Online DNS service', CLAIM_TYPE.TXT)}
+ { this.renderOption('Local DNSSEC key', CLAIM_TYPE.DNSSEC)}
+ { this.renderOption('Offline DNSSEC key', CLAIM_TYPE.RRSIG)}
+ { this.renderOption('HSM / PKCS#11', CLAIM_TYPE.BASE64)}
+
+
+
+ { this.renderDescription() }
+
+
+ Next
+
+
+
+ )
+ }
+
+ renderDescription() {
+ switch (this.props.claimType) {
+ case CLAIM_TYPE.TXT:
+ return (
+
+
Online DNS service: add TXT record to legacy DNS
+ Create a TXT record in your zone using your domain name service provider. Your domain and its top-level domain must have strong DNSSEC enabled.
+
+ );
+ case CLAIM_TYPE.RRSIG:
+ return (
+
+ );
+ case CLAIM_TYPE.DNSSEC:
+ return (
+
+
Local DNSSEC key: sign using private key on this computer
+ If your DNSSEC zone-signing private key is available on this machine you can select it to automatically sign and broadcast the claim proof.
+
+ );
+ case CLAIM_TYPE.BASE64:
+ return (
+
+ );
+ default:
+ return null;
+ }
+
+ }
+
+ renderOption(label, type) {
+ return (
+ this.props.changeClaimType(type)}
+ >
+ {label}
+
+ )
+ }
+}
+
+class CheckName extends Component {
+ static propTypes = {
+ onClose: PropTypes.func.isRequired,
+ onNext: PropTypes.func.isRequired,
+ onUrlChange: PropTypes.func.isRequired,
+ url: PropTypes.string,
+ reward: PropTypes.string,
+ fee: PropTypes.string,
+ error: PropTypes.string,
+ searchReserved: PropTypes.func.isRequired,
+ };
+
+ state = {
+ url: '',
+ };
+
+ handleChange = e => {
+ this.setState({
+ url: e.target.value,
+ });
+ };
+
+ render() {
+ const {
+ onClose,
+ searchReserved,
+ url,
+ name,
+ reward,
+ fee,
+ error,
+ onNext,
+ } = this.props;
+
+ return (
+
+
+
Reserved Name Claim
+
✕
+
+
+
+
Enter your reserved domain
+
searchReserved(this.state.url)}
+ onKeyDown={e => e.key === 'Enter' && searchReserved(this.state.url)}
+ placeholder="icann.org"
+ />
+
+ {
+ !!error && (
+
+ {error}
+
+ )
+ }
+
+
+
+
+ Next
+
+
+
+ );
+ }
+}
+
+class ReservedNameInfoCard extends Component {
+ static propTypes = {
+ name: PropTypes.string,
+ url: PropTypes.string,
+ reward: PropTypes.string,
+ fee: PropTypes.string,
+ };
+
+ render() {
+ const {name, url, reward, fee} = this.props;
+ return (
+
+
+
+
+
Reward
+
{reward || '-'}
+
+
+
+ )
+ }
+}
diff --git a/app/components/NameClaimModal/name-claim-modal.scss b/app/components/NameClaimModal/name-claim-modal.scss
new file mode 100644
index 000000000..e89b0797f
--- /dev/null
+++ b/app/components/NameClaimModal/name-claim-modal.scss
@@ -0,0 +1,288 @@
+@import "../../variables";
+
+.name-claim {
+ max-width: 38rem;
+ position: relative;
+ padding: 1.5rem 0;
+ box-sizing: border-box;
+ cursor: default;
+
+ a {
+ color: $azure-blue;
+ &:hover {
+ cursor: pointer;
+ }
+ }
+
+ &__container {
+ @extend %col-nowrap;
+
+ .alert {
+ margin: 0 1.875rem 1rem;
+ }
+ }
+
+ &__content {
+ @extend %col-nowrap;
+ padding: 0 1.875rem 1rem;
+ align-items: center;
+
+ &__success {
+ width: 100%;
+ @extend %col-nowrap;
+ margin: 1rem 0;
+ align-items: center;
+
+ &__icon {
+ background-image: url('../../assets/images/check-circle-blue.svg');
+ height: 2.5rem;
+ width: 2.5rem;
+ background-position: center;
+ background-size: cover;
+ margin: 1rem 0;
+ }
+
+ &__title {
+ font-size: 1.125rem;
+ line-height: 1.3125;
+ font-weight: 600;
+ color: $azure-blue;
+ margin-bottom: 1rem;
+ }
+
+ &__content {
+ width: 100%;
+
+ hr {
+ margin: 2rem 0;
+ }
+ }
+
+ &__claim-id {
+ font-size: .8125rem;
+ text-align: center;
+
+ span {
+ margin-top: .25rem;
+ }
+ }
+ }
+ }
+
+ &__back-btn {
+ position: absolute;
+ top: 1.25rem;
+ left: 2rem;
+ font-weight: 700;
+ cursor: pointer;
+ height: 1.5rem;
+ width: 1.5rem;
+ background-position: center;
+ background-size: contain;
+ }
+
+ &__close-btn {
+ position: absolute;
+ top: 1.25rem;
+ right: 2rem;
+ font-weight: 700;
+ cursor: pointer;
+ font-size: 1.2rem;
+ }
+
+ &__header {
+ @extend %col-nowrap;
+ margin-bottom: 1rem;
+ }
+
+ &__title {
+ font-weight: 600;
+ font-size: 1.25rem;
+ text-align: center;
+ }
+
+ &__name-input-group {
+ width: 20rem;
+ margin: 1rem 0 2rem;
+ }
+
+ &__name-input-group {
+ @extend %col-nowrap;
+
+ &__label {
+ color: rgba($black, 0.4);
+ font-weight: 600;
+ font-size: 0.8rem;
+ margin-bottom: 0.75rem;
+ }
+
+ &__input {
+ @extend %box-input;
+ @extend %row-nowrap;
+ box-shadow: 0 0 0 1.25px rgba($black, .1);
+ background-color: $white;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 0.5rem 0.75rem;
+ outline: none;
+ flex: 1 1 auto;
+ font-size: 0.85rem;
+ border: none;
+ }
+ }
+
+ &__choose-options {
+ margin: 1rem;
+ width: 100%;
+
+ &__title {
+ font-weight: 600;
+ text-align: center;
+ }
+
+ &__options {
+ display: grid;
+ grid-auto-rows: auto;
+ grid-template-columns: 1fr 1fr;
+ margin-top: 1rem;
+ grid-gap: .5rem;
+ }
+
+ &__option {
+ background-color: rgba($black,.05);
+ color: rgba($black,.25);
+ padding: .75rem 1.5rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: .8125rem;
+ transition: 200ms ease-in-out;
+
+ &:hover {
+ background-color: rgba($black,.15);
+ color: rgba($black,.8);
+ }
+
+ &:active {
+ background-color: rgba($black,.15);
+ color: rgba($black,.8);
+ }
+
+ &--active {
+ background-color: rgba($black,.15);
+ color: rgba($black,.8);
+ font-weight: 600;
+ }
+ }
+ }
+
+ &__option-description {
+ margin: 0 auto 1rem;
+ font-size: .75rem;
+ text-align: center;
+ width: 27rem;
+
+ > div {
+ margin-bottom: .25rem;
+ font-size: .8125rem;
+ }
+ }
+
+ &__claim-content {
+ width: 100%;
+
+ &__title {
+ font-size: .875rem;
+ margin: 1rem auto;
+ text-align: center;
+ }
+
+ &__txt-table {
+ .table__row__item:nth-of-type(1),
+ .table__header__item:nth-of-type(1) {
+ width: 2rem;
+ flex: 0 0 auto;
+ }
+
+ .table__row__item:nth-of-type(2),
+ .table__header__item:nth-of-type(2) {
+ width: 10rem;
+ flex: 0 0 auto;
+ }
+
+ .table__row__item:nth-of-type(3),
+ .table__header__item:nth-of-type(3),
+ .table__row__item:nth-of-type(4),
+ .table__header__item:nth-of-type(4){
+ white-space: initial;
+ word-break: break-all;
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ padding: .5rem;
+
+ > button {
+ flex: 0 0 auto;
+ margin: 1rem;
+ }
+ }
+ }
+
+ &__textarea {
+ margin: 1rem 0;
+ height: 9rem;
+ width: 100%;
+ box-sizing: border-box;
+ border: 1px solid $grey;;
+ padding: .5rem;
+ font-family: "Courier New", monospace;
+ outline: none;
+ resize: none;
+
+ &:disabled {
+ }
+ }
+ }
+
+ &__footer {
+ @extend %row-nowrap;
+ justify-content: center;
+ padding: 0.5rem 0.75rem;
+ }
+
+ &__cta {
+ height: 38px;
+ border-radius: 8px;
+ background-color: $azure-blue;
+ color: white;
+ font-family: 'Roboto', sans-serif;
+ font-weight: 600;
+ line-height: 16px;
+ text-align: center;
+ @extend %btn-primary;
+ }
+}
+
+.reserved-name-info {
+ display: grid;
+ grid-auto-rows: auto;
+ grid-template-columns: 1fr 1fr;
+ background-color: rgba($azure-blue, .1);
+ padding: .5rem 1rem;
+ width: calc(100% - 2rem);
+ border-radius: 4px;
+
+ &__group {
+ @extend %col-nowrap;
+ padding: .5rem 1rem;
+
+ &__label {
+ color: rgba($black, 0.4);
+ font-size: 0.8rem;
+ margin-bottom: 0.25rem;
+ }
+
+ &__value {
+ font-weight: 600;
+ }
+ }
+}
diff --git a/app/components/Sidebar/index.js b/app/components/Sidebar/index.js
index 557a88bed..078b55b61 100644
--- a/app/components/Sidebar/index.js
+++ b/app/components/Sidebar/index.js
@@ -147,7 +147,7 @@ class Sidebar extends Component {
to="/get_coins"
activeClassName="sidebar__action--selected"
>
- Add Funds
+ Claim Airdrop or Name
+
{this.props.children}
)
@@ -45,9 +65,28 @@ export class TableRow extends Component {
}
export class TableItem extends Component {
+ static propTypes = {
+ shrink: PropTypes.number,
+ grow: PropTypes.number,
+ width: PropTypes.string,
+ };
+
render() {
+ const {
+ shrink,
+ grow,
+ width
+ } = this.props;
+
return (
-
+
{this.props.children}
)
diff --git a/app/components/Transactions/Transaction/index.js b/app/components/Transactions/Transaction/index.js
index 83644860d..597062696 100644
--- a/app/components/Transactions/Transaction/index.js
+++ b/app/components/Transactions/Transaction/index.js
@@ -13,6 +13,7 @@ import { shell } from 'electron';
const RECEIVE = 'RECEIVE';
const SEND = 'SEND';
+const CLAIM = 'CLAIM';
const OPEN = 'OPEN';
const BID = 'BID';
const REVEAL = 'REVEAL';
@@ -64,6 +65,7 @@ class Transaction extends Component {
&& !tx.pending,
'transaction__number--neutral':
(tx.type === UPDATE
+ || tx.type === CLAIM
|| tx.type === RENEW
|| tx.type === OPEN
|| tx.type === TRANSFER
@@ -100,6 +102,9 @@ class Transaction extends Component {
} else if (tx.type === RECEIVE) {
description = 'Received Funds';
content = ellipsify(tx.meta.from, 12);
+ } else if (tx.type === CLAIM) {
+ description = 'Claimed Reserved Name';
+ content = this.formatDomain(tx.meta.domain);
} else if (tx.type === OPEN) {
description = 'Opened Auction';
content = this.formatDomain(tx.meta.domain);
@@ -157,7 +162,7 @@ class Transaction extends Component {
{' '}
{
tx.type === RECEIVE || tx.type === COINBASE || tx.type === REDEEM || tx.type === REVEAL || tx.type === REGISTER ? '+'
- : tx.type === UPDATE || tx.type === RENEW || tx.type === OPEN || tx.type === FINALIZE ? ''
+ : tx.type === UPDATE || tx.type === RENEW || tx.type === OPEN || tx.type === FINALIZE || tx.type === CLAIM ? ''
: '-'
}
{ (tx.type === FINALIZE && tx.value > 0) ? '+': '' }
diff --git a/app/ducks/walletActions.js b/app/ducks/walletActions.js
index 3e213ce56..a891e8333 100644
--- a/app/ducks/walletActions.js
+++ b/app/ducks/walletActions.js
@@ -418,7 +418,12 @@ async function parseInputsOutputs(net, tx) {
? output.value
: tx.inputs[i].value - output.value;
} else {
- covValue += output.value;
+ // Special case for reserved name claims, we are receiving
+ // coins but they are locked. The spendable balance will
+ // be incremented by the reward value when we REGISTER.
+ // (The name's value is burned at 0 but we get the reward as change)
+ if (covenant.action !== 'CLAIM')
+ covValue += output.value;
}
if (covenant.action == 'FINALIZE') {
@@ -502,6 +507,8 @@ async function parseInputsOutputs(net, tx) {
async function parseCovenant(net, covenant) {
switch (covenant.action) {
+ case 'CLAIM':
+ return {type: 'CLAIM', meta: {domain: await nameByHash(net, covenant)}};
case 'OPEN':
return {type: 'OPEN', meta: {domain: await nameByHash(net, covenant)}};
case 'BID':
diff --git a/app/pages/App/app.scss b/app/pages/App/app.scss
index 082b0d54d..8e82b4dcf 100644
--- a/app/pages/App/app.scss
+++ b/app/pages/App/app.scss
@@ -46,7 +46,7 @@
&__sidebar-wrapper {
flex: 0 0 auto;
- width: 220px;
+ width: 230px;
z-index: 1;
}
diff --git a/app/pages/Auction/BidActionPanel/Reserved.js b/app/pages/Auction/BidActionPanel/Reserved.js
index 4dd82a75d..cb1002170 100644
--- a/app/pages/Auction/BidActionPanel/Reserved.js
+++ b/app/pages/Auction/BidActionPanel/Reserved.js
@@ -19,10 +19,10 @@ export default class Reserved extends Component {
return (
- Reserved for the top 100,000 Alexa websites
+ Reserved for ICANN TLDs and the top 100,000 Alexa websites
- In the top 100,000 as of 6/1/18
+ In the reserved name list as of 2/1/20
);
diff --git a/app/pages/Auction/domains.scss b/app/pages/Auction/domains.scss
index 77d28528f..8db523a2a 100644
--- a/app/pages/Auction/domains.scss
+++ b/app/pages/Auction/domains.scss
@@ -90,6 +90,14 @@
&__description {
font-size: .65rem;
+
+ button {
+ @extend %btn-primary;
+ padding: 0.5rem;
+ width: 100%;
+ border-radius: 0.5rem;
+ margin-top: 1rem;
+ }
}
}
}
diff --git a/app/pages/Auction/index.js b/app/pages/Auction/index.js
index 34aec869a..32b3ae034 100644
--- a/app/pages/Auction/index.js
+++ b/app/pages/Auction/index.js
@@ -28,6 +28,7 @@ import BidHistory from './BidHistory';
import Records from '../../components/Records';
import './domains.scss';
import { clientStub as aClientStub } from '../../background/analytics/client';
+import NameClaimModal from "../../components/NameClaimModal";
const Sentry = require('@sentry/electron');
@@ -76,6 +77,10 @@ export default class Auction extends Component {
explorer: PropTypes.object.isRequired,
};
+ state = {
+ isShowingClaimModal: false,
+ };
+
async componentWillMount() {
try {
this.setState({isLoading: true});
@@ -104,6 +109,12 @@ export default class Auction extends Component {
render() {
return (
+ { this.state.isShowingClaimModal && (
+
this.setState({ isShowingClaimModal: false })}
+ />
+ )}
{this.renderContent()}
{this.renderAuctionRight()}
@@ -273,9 +284,16 @@ export default class Auction extends Component {
const {domain} = this.props;
let status = '';
let description = '';
+
if (isReserved(domain)) {
status = 'Reserved';
- // this.setState({ showCollapsibles: false })
+ description = (
+ this.setState({ isShowingClaimModal: true })}
+ >
+ Claim this name
+
+ );
} else if (isOpening(domain)) {
status = 'Opening';
description = 'Bidding Soon';
diff --git a/app/pages/GetCoins/index.js b/app/pages/GetCoins/index.js
index fa6ede789..dae453b54 100644
--- a/app/pages/GetCoins/index.js
+++ b/app/pages/GetCoins/index.js
@@ -1,8 +1,10 @@
import React, { Component } from 'react';
+import { withRouter } from 'react-router';
import { shell } from 'electron';
import ProofModal from '../../components/ProofModal/index';
import './get-coins.scss';
import { clientStub as aClientStub } from '../../background/analytics/client';
+import NameClaimModal from "../../components/NameClaimModal";
const analytics = aClientStub(() => require('electron').ipcRenderer);
@@ -17,24 +19,68 @@ const Step = ({number, title, paragraph}) => (
);
-export default class GetCoins extends Component {
+class GetCoins extends Component {
state = {
isShowingGitHubModal: false,
isShowingPGPModal: false,
isShowingFaucetModal: false,
+ isShowingNameClaimModal: false,
};
componentDidMount() {
analytics.screenView('Get Coins');
}
- closeModal = () => this.setState({isShowingFaucetModal: false, isShowingGitHubModal: false, isShowingPGPModal: false});
- openGitHubModal = () => this.setState({isShowingFaucetModal: false, isShowingGitHubModal: true, isShowingPGPModal: false});
- openPGPModal = () => this.setState({isShowingFaucetModal: false, isShowingGitHubModal: false, isShowingPGPModal: true});
- openFaucetModal = () => this.setState({isShowingFaucetModal: true, isShowingGitHubModal: false, isShowingPGPModal: false});
+ closeModal = () => this.setState({
+ isShowingFaucetModal: false,
+ isShowingGitHubModal: false,
+ isShowingPGPModal: false,
+ isShowingNameClaimModal: false,
+ });
+
+ openGitHubModal = () => this.setState({
+ isShowingFaucetModal: false,
+ isShowingGitHubModal: true,
+ isShowingPGPModal: false,
+ isShowingNameClaimModal: false,
+ });
+
+ openPGPModal = () => this.setState({
+ isShowingFaucetModal: false,
+ isShowingGitHubModal: false,
+ isShowingPGPModal: true,
+ isShowingNameClaimModal: false,
+ });
+
+ openFaucetModal = () => this.setState({
+ isShowingFaucetModal: true,
+ isShowingGitHubModal: false,
+ isShowingPGPModal: false,
+ isShowingNameClaimModal: false,
+ });
+
+ openNameClaimModal = () => this.setState({
+ isShowingFaucetModal: false,
+ isShowingGitHubModal: false,
+ isShowingPGPModal: false,
+ isShowingNameClaimModal: true,
+ });
renderModal() {
- const {isShowingGitHubModal, isShowingPGPModal, isShowingFaucetModal} = this.state;
+ const {
+ isShowingGitHubModal,
+ isShowingPGPModal,
+ isShowingFaucetModal,
+ isShowingNameClaimModal,
+ } = this.state;
+
+ if (isShowingNameClaimModal) {
+ return (
+
+ );
+ }
if (isShowingGitHubModal) {
return (
@@ -132,7 +178,6 @@ export default class GetCoins extends Component {
-
Reserved for Top Developers
GitHub Developers
+ 4,662.598321 HNS
@@ -153,6 +198,15 @@ export default class GetCoins extends Component {
Seed phrase
Redeem
+
+
Reserved Name Claims
+
+ 503 HNS and up
+
ICANN root zone TLDs
+
Alexa top 100,000
+
+ Claim
+
+
{this.renderModal()}
@@ -160,3 +214,5 @@ export default class GetCoins extends Component {
);
}
}
+
+export default withRouter(GetCoins);
diff --git a/app/utils/nameHelpers.js b/app/utils/nameHelpers.js
index b10c4e7d7..5292a41d5 100644
--- a/app/utils/nameHelpers.js
+++ b/app/utils/nameHelpers.js
@@ -42,12 +42,22 @@ export const isAvailable = name => {
export const isReserved = name => {
const {start} = name || {};
+ const {info} = name || {};
+
+ // Maybe already claimed
+ if (isClosed(name))
+ return false;
// Not available if start is undefined
if (!start) {
return false;
}
+ // Reserved names become un-reserved after they are expired or revoked.
+ if (info) {
+ return !!start.reserved && !info.expired;
+ }
+
return !!start.reserved;
};
@@ -106,3 +116,10 @@ export const formatName = name => {
return `${name}/`;
}
+
+export const ensureDot = (string) => {
+ if (string[string.length - 1] !== '.')
+ return string + '.';
+
+ return string;
+};