-
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 {