diff --git a/src/app/components/account-details/account-details.component.ts b/src/app/components/account-details/account-details.component.ts index 211f46e..359c9ed 100644 --- a/src/app/components/account-details/account-details.component.ts +++ b/src/app/components/account-details/account-details.component.ts @@ -744,14 +744,18 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { const regexp = new RegExp('^(Account|' + this.translocoService.translate('general.account') + ') #\\d+$', 'g'); if ( regexp.test(this.addressBookModel) === true ) { - return this.notifications.sendError(`This name is reserved for wallet accounts without a label`); + return this.notifications.sendError(this.translocoService.translate('address-book.this-name-is-reserved-for-wallet-accounts-without-a-label')); + } + + if ( this.addressBookModel.startsWith('@') === true ) { + return this.notifications.sendError(this.translocoService.translate('address-book.this-name-is-reserved-for-decentralized-aliases')); } // Make sure no other entries are using that name const accountIdWithSameName = this.addressBook.getAccountIdByName(this.addressBookModel); if ( (accountIdWithSameName !== null) && (accountIdWithSameName !== this.accountID) ) { - return this.notifications.sendError(`This name is already in use! Please use a unique name`); + return this.notifications.sendError(this.translocoService.translate('address-book.this-name-is-already-in-use-please-use-a-unique-name')); } try { @@ -759,11 +763,11 @@ export class AccountDetailsComponent implements OnInit, OnDestroy { const currentTransactionTracking = this.addressBook.getTransactionTrackingById(this.accountID); await this.addressBook.saveAddress(this.accountID, this.addressBookModel, currentBalanceTracking, currentTransactionTracking); } catch (err) { - this.notifications.sendError(`Unable to save entry: ${err.message}`); + this.notifications.sendError(this.translocoService.translate('address-book.unable-to-save-entry', { message: err.message })); return; } - this.notifications.sendSuccess(`Address book entry saved successfully!`); + this.notifications.sendSuccess(this.translocoService.translate('address-book.address-book-entry-saved-successfully')); this.addressBookEntry = this.addressBookModel; this.showEditAddressBook = false; diff --git a/src/app/components/address-book/address-book.component.ts b/src/app/components/address-book/address-book.component.ts index 2962598..0c6ae61 100644 --- a/src/app/components/address-book/address-book.component.ts +++ b/src/app/components/address-book/address-book.component.ts @@ -281,6 +281,10 @@ export class AddressBookComponent implements OnInit, AfterViewInit, OnDestroy { return this.notificationService.sendError(this.translocoService.translate('address-book.this-name-is-reserved-for-wallet-accounts-without-a-label')); } + if ( this.newAddressName.startsWith('@') === true ) { + return this.notificationService.sendError(this.translocoService.translate('address-book.this-name-is-reserved-for-decentralized-aliases')); + } + // Remove spaces and convert to nano prefix this.newAddressAccount = this.newAddressAccount.replace(/ /g, '').replace('xrb_', 'nano_'); diff --git a/src/app/components/configure-app/configure-app.component.html b/src/app/components/configure-app/configure-app.component.html index b061f84..d9feaea 100644 --- a/src/app/components/configure-app/configure-app.component.html +++ b/src/app/components/configure-app/configure-app.component.html @@ -197,6 +197,23 @@ +
+
+ +
+ +
+ +
+ +
+
+
+
+
+
diff --git a/src/app/components/configure-app/configure-app.component.ts b/src/app/components/configure-app/configure-app.component.ts index 49fb9d8..007fa43 100644 --- a/src/app/components/configure-app/configure-app.component.ts +++ b/src/app/components/configure-app/configure-app.component.ts @@ -148,6 +148,12 @@ export class ConfigureAppComponent implements OnInit { ]; selectedPendingOption = this.pendingOptions[0].value; + decentralizedAliasesOptions = [ + { name: this.translocoService.translate('configure-app.decentralized-aliases-options.disabled'), value: 'disabled' }, + { name: this.translocoService.translate('configure-app.decentralized-aliases-options.enabled'), value: 'enabled' }, + ]; + selectedDecentralizedAliasesOption = this.decentralizedAliasesOptions[0].value; + // prefixOptions = [ // { name: 'xrb_', value: 'xrb' }, // { name: 'nano_', value: 'nano' }, @@ -282,6 +288,9 @@ export class ConfigureAppComponent implements OnInit { const matchingPendingOption = this.pendingOptions.find(d => d.value === settings.pendingOption); this.selectedPendingOption = matchingPendingOption ? matchingPendingOption.value : this.pendingOptions[0].value; + const matchingDecentralizedAliasesOption = this.decentralizedAliasesOptions.find(d => d.value === settings.decentralizedAliasesOption); + this.selectedDecentralizedAliasesOption = matchingDecentralizedAliasesOption ? matchingDecentralizedAliasesOption.value : this.decentralizedAliasesOptions[0].value; + this.serverOptions = this.appSettings.serverOptions; this.selectedServer = settings.serverName; this.serverAPI = settings.serverAPI; @@ -371,6 +380,8 @@ export class ConfigureAppComponent implements OnInit { minReceive = this.minimumReceive; } + const decentralizedAliasesOption = this.selectedDecentralizedAliasesOption; + // reload pending if threshold changes or if receive priority changes from manual to auto let reloadPending = this.appSettings.settings.minimumReceive !== this.minimumReceive || (pendingOption !== 'manual' && pendingOption !== this.appSettings.settings.pendingOption); @@ -432,6 +443,7 @@ export class ConfigureAppComponent implements OnInit { multiplierSource: Number(this.selectedMultiplierOption), customWorkServer: this.customWorkServer, pendingOption: pendingOption, + decentralizedAliasesOption: decentralizedAliasesOption, minimumReceive: minReceive, defaultRepresentative: this.defaultRepresentative || null, }; diff --git a/src/app/components/send/send.component.html b/src/app/components/send/send.component.html index e60f6b0..5c7e7e1 100644 --- a/src/app/components/send/send.component.html +++ b/src/app/components/send/send.component.html @@ -32,24 +32,49 @@
- + -
+
-
-
+
+
+
+ +
diff --git a/src/app/components/send/send.component.ts b/src/app/components/send/send.component.ts index c3ed326..7ad3f1c 100644 --- a/src/app/components/send/send.component.ts +++ b/src/app/components/send/send.component.ts @@ -14,6 +14,7 @@ import {NanoBlockService} from '../../services/nano-block.service'; import { QrModalService } from '../../services/qr-modal.service'; import { environment } from 'environments/environment'; import { TranslocoService } from '@ngneat/transloco'; +import { HttpClient } from '@angular/common/http'; import * as nanocurrency from 'nanocurrency'; const nacl = window['nacl']; @@ -30,9 +31,29 @@ export class SendComponent implements OnInit { sendDestinationType = 'external-address'; accounts = this.walletService.wallet.accounts; + + ALIAS_LOOKUP_DEFAULT_STATE = { + fullText: '', + name: '', + domain: '', + } + + aliasLookup = { + ...this.ALIAS_LOOKUP_DEFAULT_STATE, + } + aliasLookupInProgress = { + ...this.ALIAS_LOOKUP_DEFAULT_STATE, + } + aliasLookupLatestSuccessful = { + ...this.ALIAS_LOOKUP_DEFAULT_STATE, + address: '', + } + aliasResults$ = new BehaviorSubject([]); addressBookResults$ = new BehaviorSubject([]); + isDestinationAccountAlias = false; showAddressBook = false; addressBookMatch = ''; + addressAliasMatch = ''; amounts = [ { name: 'XNO', shortName: 'XNO', value: 'mnano' }, @@ -70,6 +91,7 @@ export class SendComponent implements OnInit { public settings: AppSettingsService, private util: UtilService, private qrModalService: QrModalService, + private http: HttpClient, private translocoService: TranslocoService) { } async ngOnInit() { @@ -134,6 +156,7 @@ export class SendComponent implements OnInit { if (params && params.to) { this.toAccountID = params.to; + this.offerLookupIfDestinationIsAlias(); this.validateDestination(); this.sendDestinationType = 'external-address'; } @@ -194,6 +217,10 @@ export class SendComponent implements OnInit { } onDestinationAddressInput() { + this.addressAliasMatch = ''; + this.addressBookMatch = ''; + + this.offerLookupIfDestinationIsAlias(); this.searchAddressBook(); const destinationAddress = this.toAccountID || ''; @@ -243,9 +270,188 @@ export class SendComponent implements OnInit { this.addressBookResults$.next(matches); } + offerLookupIfDestinationIsAlias() { + const destinationAddress = this.toAccountID || ''; + + const mayBeAnAlias = ( + ( destinationAddress.startsWith('@') === true ) + && ( destinationAddress.includes('.') === true ) + && ( destinationAddress.endsWith('.') === false ) + && ( destinationAddress.includes('/') === false ) + && ( destinationAddress.includes('?') === false ) + ); + + if (mayBeAnAlias === false) { + this.isDestinationAccountAlias = false; + this.aliasLookup = { + ...this.ALIAS_LOOKUP_DEFAULT_STATE, + }; + this.aliasResults$.next([]); + return + } + + this.isDestinationAccountAlias = true; + + let aliasWithoutFirstSymbol = destinationAddress.slice(1).toLowerCase(); + + if (aliasWithoutFirstSymbol.startsWith('_@') === true ) { + aliasWithoutFirstSymbol = aliasWithoutFirstSymbol.slice(2); + } + + const aliasSplitResults = aliasWithoutFirstSymbol.split('@'); + + let aliasName = '' + let aliasDomain = '' + + if (aliasSplitResults.length === 2) { + aliasName = aliasSplitResults[0] + aliasDomain = aliasSplitResults[1] + } else { + aliasDomain = aliasSplitResults[0] + } + + this.aliasLookup = { + fullText: `@${aliasWithoutFirstSymbol}`, + name: aliasName, + domain: aliasDomain, + }; + + this.aliasResults$.next([{ ...this.aliasLookup }]); + + this.toAccountStatus = 1; // Neutral state + } + + async lookupAlias() { + if (this.aliasLookup.domain === '') { + return; + } + + if (this.settings.settings.decentralizedAliasesOption === 'disabled') { + const UIkit = window['UIkit']; + try { + await UIkit.modal.confirm( + `


+ + ${ this.translocoService.translate('configure-app.decentralized-aliases-require-external-requests') } + `, + { + labels: { + cancel: this.translocoService.translate('general.cancel'), + ok: this.translocoService.translate('configure-app.allow-external-requests'), + } + } + ); + + this.settings.setAppSetting('decentralizedAliasesOption', 'enabled'); + } catch (err) { + // pressed cancel, or a different error + return; + } + } + + this.toAccountStatus = 1; // Neutral state + + const aliasLookup = { ...this.aliasLookup }; + + const aliasFullText = aliasLookup.fullText; + const aliasDomain = aliasLookup.domain; + + const aliasName = ( + (aliasLookup.name !== '') + ? aliasLookup.name + : '_' + ); + + const lookupUrl = + `https://${ aliasDomain }/.well-known/nano-currency.json?names=${ aliasName }`; + + this.aliasLookupInProgress = { + ...aliasLookup, + }; + + await this.http.get(lookupUrl).toPromise() + .then(res => { + const isOutdatedRequest = ( + this.aliasLookupInProgress.fullText + !== aliasFullText + ); + + if (isOutdatedRequest === true) { + return; + } + + this.aliasLookupInProgress = { + ...this.ALIAS_LOOKUP_DEFAULT_STATE, + }; + + try { + const aliasesInJsonCount = ( + ( Array.isArray(res.names) === true ) + ? res.names.length + : 0 + ); + + if (aliasesInJsonCount === 0) { + this.toAccountStatus = 0; // Error state + this.notificationService.sendWarning(`Alias @${aliasName} not found on ${aliasDomain}`); + return; + } + + const matchingAccount = + res.names.find( + (account) => + (account.name === aliasName) + ); + + if (matchingAccount == null) { + this.toAccountStatus = 0; // Error state + this.notificationService.sendWarning(`Alias @${aliasName} not found on ${aliasDomain}`); + return; + } + + if (!this.util.account.isValidAccount(matchingAccount.address)) { + this.toAccountStatus = 0; // Error state + this.notificationService.sendWarning(`Alias ${aliasFullText} does not have a valid address`); + return; + } + + this.toAccountID = matchingAccount.address; + + this.aliasLookupLatestSuccessful = { + ...aliasLookup, + address: this.toAccountID, + }; + + this.onDestinationAddressInput(); + this.validateDestination(); + + return; + } catch(err) { + this.toAccountStatus = 0; // Error state + this.notificationService.sendWarning(`Unknown error has occurred while trying to lookup ${aliasFullText}`); + return; + } + }) + .catch(err => { + this.aliasLookupInProgress = { + ...this.ALIAS_LOOKUP_DEFAULT_STATE, + }; + this.toAccountStatus = 0; // Error state + + if (err.status === 404) { + this.notificationService.sendWarning(`No aliases found on ${aliasDomain}`); + } else { + this.notificationService.sendWarning(`Could not reach domain ${aliasDomain}`); + } + + return; + }); + } + selectBookEntry(account) { this.showAddressBook = false; this.toAccountID = account; + this.isDestinationAccountAlias = false; this.searchAddressBook(); this.validateDestination(); } @@ -261,6 +467,21 @@ export class SendComponent implements OnInit { // Remove spaces from the account id this.toAccountID = this.toAccountID.replace(/ /g, ''); + this.addressAliasMatch = ( + ( + (this.aliasLookupLatestSuccessful.address !== '') + && (this.aliasLookupLatestSuccessful.address === this.toAccountID) + ) + ? this.aliasLookupLatestSuccessful.fullText + : null + ); + + if (this.isDestinationAccountAlias === true) { + this.addressBookMatch = null; + this.toAccountStatus = 1; // Neutral state + return; + } + this.addressBookMatch = ( this.addressBookService.getAccountName(this.toAccountID) || this.getAccountLabel(this.toAccountID, null) @@ -426,6 +647,7 @@ export class SendComponent implements OnInit { this.fromAddressBook = ''; this.toAddressBook = ''; this.addressBookMatch = ''; + this.addressAliasMatch = ''; } else { if (!this.walletService.isLedgerWallet()) { this.notificationService.sendError(`There was an error sending your transaction, please try again.`); diff --git a/src/app/services/app-settings.service.ts b/src/app/services/app-settings.service.ts index fe80ffb..b21b78b 100644 --- a/src/app/services/app-settings.service.ts +++ b/src/app/services/app-settings.service.ts @@ -20,6 +20,7 @@ interface AppSettings { multiplierSource: number; customWorkServer: string; pendingOption: string; + decentralizedAliasesOption: string; serverName: string; serverAPI: string | null; serverWS: string | null; @@ -48,6 +49,7 @@ export class AppSettingsService { multiplierSource: 1, customWorkServer: '', pendingOption: 'amount', + decentralizedAliasesOption: 'disabled', serverName: 'random', serverAPI: null, serverWS: null, @@ -233,6 +235,7 @@ export class AppSettingsService { multiplierSource: 1, customWorkServer: '', pendingOption: 'amount', + decentralizedAliasesOption: 'disabled', serverName: 'random', serverAPI: null, serverWS: null, diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index f776630..de718e2 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -84,6 +84,7 @@ "the-balance-will-be-displayed-in-the-address-book": "The balance will be displayed in the address book.", "this-name-is-already-in-use-please-use-a-unique-name": "This name is already in use! Please use a unique name", "this-name-is-reserved-for-wallet-accounts-without-a-label": "This name is reserved for wallet accounts without a label", + "this-name-is-reserved-for-decentralized-aliases": "Names starting with @ are reserved for decentralized aliases", "track-balance": "Track Balance", "track-transactions": "Track Transactions", "unable-to-delete-entry": "Unable to delete entry: {{ message }}", @@ -133,6 +134,7 @@ }, "configure-app": { "advanced-options": "Advanced Options", + "allow-external-requests": "Allow External Requests", "api-server": "API Server", "app-display-settings-successfully-updated": "App display settings successfully updated!", "app-wallet-settings-successfully-updated": "App wallet settings successfully updated!", @@ -158,6 +160,12 @@ }, "custom-api-server-has-an-invalid-address": "Custom API Server has an invalid address.", "custom-update-server-has-an-invalid-address": "Custom Update Server has an invalid address.", + "decentralized-aliases": "Decentralized Aliases", + "decentralized-aliases-options": { + "disabled": "Disabled", + "enabled": "Enabled - Allow External Requests to Alias Domains" + }, + "decentralized-aliases-require-external-requests": "Decentralized aliases require external requests to alias domains. When you try to lookup the address of @madeline@example.com, the domain example.com may record your IP address.", "default-representative": "Default Representative", "default-representative-is-not-a-valid-account": "Default representative is not a valid account", "delete-all-data": "Delete ALL Data", diff --git a/src/less/nault-theme.less b/src/less/nault-theme.less index 646eefe..b4a0679 100644 --- a/src/less/nault-theme.less +++ b/src/less/nault-theme.less @@ -347,6 +347,11 @@ input[type=number] { background: @rep-label-color; color: #FFF; } + + &.alias { + text-transform: none; + font-family: 'Roboto Mono', monospace; + } } .rep-donation-icon {