Merge pull request #587 from keerifox/decentralized-aliases

Decentralized aliases
This commit is contained in:
Joohansson (Json) 2023-08-14 13:12:00 +02:00 committed by GitHub
commit 6d8a5aa3c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 313 additions and 13 deletions

View file

@ -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;

View file

@ -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_');

View file

@ -197,6 +197,23 @@
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-form-horizontal">
<div class="uk-margin">
<label class="uk-form-label">{{ 'configure-app.decentralized-aliases' | transloco }} <span uk-icon="icon: info;" uk-tooltip [title]="t('configure-app.decentralized-aliases-require-external-requests')"></span></label>
<div class="uk-form-controls">
<div class="uk-inline uk-width-1-1">
<select class="uk-select" [(ngModel)]="selectedDecentralizedAliasesOption">
<option *ngFor="let decentralizedAliasesOption of decentralizedAliasesOptions" [value]="decentralizedAliasesOption.value">{{ decentralizedAliasesOption.name }}</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="uk-width-1-1">
<div class="uk-form-horizontal">

View file

@ -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,
};

View file

@ -32,24 +32,49 @@
<div class="uk-form-controls">
<div class="form-input-destination uk-inline uk-width-1-1">
<a class="hide-on-small-viewports uk-form-icon uk-form-icon-flip" uk-icon="icon: camera" (click)="openQR('account1','account')" uk-tooltip title="Scan from QR code"></a>
<input (blur)="validateDestination()" (input)="onDestinationAddressInput()" (focus)="searchAddressBook()" [(ngModel)]="toAccountID" [ngClass]="{ 'uk-form-success': toAccountStatus === 2, 'uk-form-danger': toAccountStatus === 0 }" class="uk-input" id="form-horizontal-text2" type="text" placeholder="Address to send to / nano:.." autocomplete="off">
<input (blur)="validateDestination()" (input)="onDestinationAddressInput()" (keydown.enter)="lookupAlias()" (focus)="searchAddressBook()" [(ngModel)]="toAccountID" [ngClass]="{ 'uk-form-success': toAccountStatus === 2, 'uk-form-danger': toAccountStatus === 0 }" class="uk-input" id="form-horizontal-text2" type="text" placeholder="Address to send to / Alias / nano:.." autocomplete="off">
<div *ngIf="(addressBookResults$ | async).length" [hidden]="!showAddressBook" class="nlt-dropdown uk-animation-slide-down-small uk-width-1-1 uk-card uk-card-default uk-card-body uk-position-absolute" style="z-index: 15000">
<div *ngIf="(aliasResults$ | async).length || (addressBookResults$ | async).length" [hidden]="!showAddressBook && !isDestinationAccountAlias" class="nlt-dropdown uk-animation-slide-down-small uk-width-1-1 uk-card uk-card-default uk-card-body uk-position-absolute" style="z-index: 15000">
<ul class="uk-nav uk-nav-default">
<li class="uk-nav-header">Address Book Results</li>
<li class="uk-nav-divider"></li>
<li *ngFor="let book of addressBookResults$ | async">
<a (click)="selectBookEntry(book.account)">{{ book.name }}</a>
</li>
<ng-container *ngIf="(aliasResults$ | async).length">
<li class="uk-nav-header">Alias Lookup</li>
<li class="uk-nav-divider"></li>
<li *ngIf="(aliasLookupInProgress.domain !== '')">
<a>
<span class="spinner uk-margin-small-right" uk-spinner="ratio: 0.5;"></span><span class="text-half-muted">Looking up {{ aliasLookupInProgress.fullText }}...</span>
</a>
</li>
<ng-container *ngIf="(aliasLookupInProgress.domain === '')">
<li *ngFor="let alias of aliasResults$ | async">
<a (click)="lookupAlias()" class="uk-padding-remove-bottom">
<span class="text-half-muted">Press Enter or click here to request alias from</span>
</a>
<a (click)="lookupAlias()">
<span class="uk-text-primary">{{ alias.domain }}</span>
</a>
</li>
</ng-container>
<li class="uk-nav-divider" *ngIf="(addressBookResults$ | async).length"></li>
</ng-container>
<ng-container *ngIf="(addressBookResults$ | async).length">
<li class="uk-nav-header">Address Book Results</li>
<li class="uk-nav-divider"></li>
<li *ngFor="let book of addressBookResults$ | async">
<a (click)="selectBookEntry(book.account)">{{ book.name }}</a>
</li>
</ng-container>
</ul>
</div>
</div>
</div>
<div class="uk-form-controls" *ngIf="addressBookMatch">
<div class="uk-inline uk-width-1-1">
<div class="uk-form-controls" *ngIf="addressBookMatch || addressAliasMatch">
<div class="uk-inline uk-width-auto uk-margin-small-right" *ngIf="addressBookMatch">
<span class="account-label blue uk-margin-small-top">{{ addressBookMatch }}</span>
</div>
<div class="uk-inline uk-width-auto" *ngIf="addressAliasMatch">
<span class="account-label alias uk-margin-small-top">{{ addressAliasMatch }}</span>
</div>
</div>
</div>

View file

@ -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(
`<p class="uk-alert uk-alert-warning"><br><span class="uk-flex"><span uk-icon="icon: warning; ratio: 3;" class="uk-align-center"></span></span>
<span style="font-size: 18px;">
${ this.translocoService.translate('configure-app.decentralized-aliases-require-external-requests') }
</span>`,
{
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<any>(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.`);

View file

@ -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,

View file

@ -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",

View file

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