diff --git a/src/app/components/message-reply/message-reply.component.html b/src/app/components/message-reply/message-reply.component.html new file mode 100644 index 00000000000..4e4410bb8df --- /dev/null +++ b/src/app/components/message-reply/message-reply.component.html @@ -0,0 +1,30 @@ + +
+
+ + {{'Replying to ' | translate }} {{ messageReplyInfo.addressTo }} +
+
+ {{messageReplyInfo.messageOnChain}} +
+
+ +
+ + {{'Private message' | translate}} + + + + + Fee: ~0.00055 XPI + + + {{(this.message).length}} / 206 bytes + + +
+ + + {{ 'Send' | translate }} + +
diff --git a/src/app/components/message-reply/message-reply.component.scss b/src/app/components/message-reply/message-reply.component.scss new file mode 100644 index 00000000000..f7830ca9f72 --- /dev/null +++ b/src/app/components/message-reply/message-reply.component.scss @@ -0,0 +1,105 @@ +message-reply-component { + .scanner-icon { + z-index: 99; + font-size: 28px; + color: var(--ion-color-primary); + } + .check { + font-size: 22px; + padding-top: 1rem; + &.success { + color: var(--ion-color-success); + } + &.fail { + color: var(--ion-color-danger); + } + } + + label-tip { + margin-top: 1.8rem; + } + + .slide-title { + h3 { + margin: 0; + font-weight: 400; + font-size: 24px; + line-height: 32px; + color: #001e2e; + margin-bottom: 2rem; + } + } + form { + margin: 0 8px 24px 8px; + ion-item { + margin: 2rem 0; + border: 2px solid rgba(0, 30, 46, 0.38); + --inner-padding-start: 1rem; + --inner-padding-end: 1rem; + --inner-padding-end: 0; + --padding-start: 0; + --padding-end: 0; + --color: #001e2e; + --background: #fafafb; + border-radius: 4px; + ion-input { + font-weight: 400; + font-size: 16px !important; + line-height: 24px; + letter-spacing: 0.5px; + } + ion-icon { + padding: 0; + } + } + .form-field { + margin-bottom: 3rem !important; + padding: 0 !important; + } + .mat-form-field-subscript-wrapper { + margin-top: 2rem !important; + padding: 0 !important; + } + + .mobile-view { + .mat-form-field-infix { + max-width: 85%; + } + } + } + .reply-to-container { + background: linear-gradient(0deg, rgba(0, 101, 141, 0.14), rgba(0, 101, 141, 0.14)), #FAFAFB; + border: 1px solid rgba(126, 208, 255, 0.08); + border-radius: 8px; + padding: 8px; + margin: 24px; + .reply-to-title { + color: #1C3745; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.1px; + } + .reply-to-message { + color: #1C3745; + letter-spacing: 0.25px; + font-size: 14px; + line-height: 20px; + margin-top: 8px; + } + } + + .button-send-onchain { + width: fit-content !important; + margin: 5px 24px 25px auto !important; + } + + .hint-reply-message { + display: flex; + justify-content: space-between; + padding: 0 1.5rem !important; + margin-top: 4px; + .max-message-txt { + color: var(--color-danger); + } + } +} diff --git a/src/app/components/message-reply/message-reply.component.spec.ts b/src/app/components/message-reply/message-reply.component.spec.ts new file mode 100644 index 00000000000..67db8bed308 --- /dev/null +++ b/src/app/components/message-reply/message-reply.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { MessageReplyComponent } from './message-reply.component'; + +describe('MessageReplyComponent', () => { + let component: MessageReplyComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ MessageReplyComponent ], + imports: [IonicModule.forRoot()] + }).compileComponents(); + + fixture = TestBed.createComponent(MessageReplyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/message-reply/message-reply.component.ts b/src/app/components/message-reply/message-reply.component.ts new file mode 100644 index 00000000000..8a97070a3af --- /dev/null +++ b/src/app/components/message-reply/message-reply.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { Location } from '@angular/common'; +import { Router } from '@angular/router'; +import { Logger } from 'src/app/providers/logger/logger'; +import { ActionSheetParent } from '../action-sheet/action-sheet-parent'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { AppProvider } from 'src/app/providers/app/app'; + +@Component({ + selector: 'message-reply-component', + templateUrl: './message-reply.component.html', + styleUrls: ['./message-reply.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MessageReplyComponent extends ActionSheetParent{ + message = ''; + Buffer = Buffer; + messageReplyInfo: any; + public currentTheme: string; + public messageReplySend: FormGroup; + messageOnChainValue; + validMessage: boolean = false; + constructor( + private formBuilder: FormBuilder, + private appProvider: AppProvider, + ) { + super(); + this.currentTheme = this.appProvider.themeProvider.currentAppTheme; + this.messageReplySend = this.formBuilder.group({ + messageOnChain: [ + '', + [] + ] + }); + } + + ngOnInit() { + this.messageReplyInfo = this.params; + } + + public changeMessage() { + if (this.message.trim().length > 0) { + this.validMessage = true; + } else { + this.validMessage = false; + } + } + + send(){ + const message = this.messageReplySend.value.messageOnChain; + this.dismiss(message); + } +} diff --git a/src/app/components/recipient/recipient.component.html b/src/app/components/recipient/recipient.component.html index 178188153e0..f9afd262e4b 100644 --- a/src/app/components/recipient/recipient.component.html +++ b/src/app/components/recipient/recipient.component.html @@ -6,7 +6,7 @@
-

{{'Transfer to' | translate}}

+

*{{'Transfer to' | translate}}

@@ -36,6 +36,23 @@ {{unit}}

{{recipient.altAmountStr}} {{alternativeUnit}}

+ +
+

{{'Private message' | translate}}

+
+ + + +

+ + Fee: ~0.00055 XPI + + + {{(this.message).length}} / 206 bytes + +

+
+
{{'Send total selected amount' | translate}} diff --git a/src/app/components/recipient/recipient.component.scss b/src/app/components/recipient/recipient.component.scss index 93037ce2180..65e0320f7ad 100644 --- a/src/app/components/recipient/recipient.component.scss +++ b/src/app/components/recipient/recipient.component.scss @@ -7,6 +7,10 @@ recipient-component { appearance: none; margin: 0; } + + .sign-require { + color: var(--color-danger); + } .check { margin: 0 !important; @@ -90,6 +94,12 @@ recipient-component { font-size: 16px; } + ion-textarea{ + --placeholder-color: rgba(0, 30, 46, 0.38); + --color: #001E2E; + font-size: 16px; + } + .scan-icon { width: 20px; height: 20px; @@ -122,6 +132,14 @@ recipient-component { margin: 0; } } + + .amount-header-onchain-message { + padding-top: 1rem !important; + } + + .max-message-txt { + color: var(--color-danger); + } .max-text { cursor: pointer; @@ -135,6 +153,11 @@ recipient-component { margin-top: 5px; margin-left: 18px; margin-bottom: 0; + &.unit-alt-text-onchain-message { + margin: 5px 18px; + display: flex; + justify-content: space-between; + } } } } diff --git a/src/app/components/recipient/recipient.component.ts b/src/app/components/recipient/recipient.component.ts index c32d79d242e..e9cfa45765c 100644 --- a/src/app/components/recipient/recipient.component.ts +++ b/src/app/components/recipient/recipient.component.ts @@ -52,6 +52,7 @@ import { Keyboard } from '@capacitor/keyboard'; export class RecipientComponent implements OnInit { public search: string = ''; public amount: string = ''; + public amountResult: number = 0; navParamsData: any; public isCordova: boolean; public expression; @@ -78,6 +79,7 @@ export class RecipientComponent implements OnInit { public searchValue: string; validAddress = false; validAmount = false; + validMessage = false; isSelectedTotalAmout: boolean = false; remaining: number; isShowReceiveLotus: boolean; @@ -86,6 +88,8 @@ export class RecipientComponent implements OnInit { formatRemaining: string; messagesReceiveLotus: boolean = false; + message=''; + Buffer = Buffer; @Input() recipient: RecipientModel; @@ -113,8 +117,12 @@ export class RecipientComponent implements OnInit { @Input() isDonation?: boolean; - @Output() deleteEvent? = new EventEmitter(); - @Output() sendMaxEvent? = new EventEmitter(); + @Input() + isShowMessage?: boolean; + + @Output() deleteEvent?= new EventEmitter(); + @Output() sendMaxEvent?= new EventEmitter(); + @Output() sendOfficialInfo? = new EventEmitter(); private validDataTypeMap: string[] = [ 'BitcoinAddress', @@ -234,6 +242,17 @@ export class RecipientComponent implements OnInit { this.updateUnitUI(!!isToken); } + public changeMessage() { + if (this.message.trim().length > 0) { + this.validMessage = true; + this.recipient.message = this.message; + this.checkRecipientValid(); + } else { + this.validMessage = false; + this.checkRecipientValid(); + } + } + private updateAddressHandler: any = data => { if (data.recipientId === this.recipient.id) { this.searchValue = data.value; @@ -506,6 +525,7 @@ export class RecipientComponent implements OnInit { this.recipient.amount = parseInt(amount, 10); } this.validAmount = result > 0; + this.amountResult = result; this.checkRecipientValid(); if (isSendMax) { this.sendMaxEvent.emit(true); @@ -749,8 +769,9 @@ export class RecipientComponent implements OnInit { checkRecipientValid() { if (!this.isDonation) { - this.recipient.isValid = this.validAddress && this.validAmount; + this.recipient.isValid = this.validAddress && (this.validAmount || this.validMessage); } else { + if (this.isShowReceiveLotus) { this.recipient.isValid = this.validAddress && this.validAmount; } else { diff --git a/src/app/components/recipient/recipient.model.ts b/src/app/components/recipient/recipient.model.ts index 61c897dc0c2..aeea4eff0c7 100644 --- a/src/app/components/recipient/recipient.model.ts +++ b/src/app/components/recipient/recipient.model.ts @@ -3,6 +3,7 @@ export class RecipientModel { public amount: number; public amountToShow: string; public altAmountStr: string; + public message?: string; public isOfficialInfo: boolean; public isValid?: boolean; public id?: number = Date.now(); diff --git a/src/app/constants.ts b/src/app/constants.ts index 45948987a91..513d9e62036 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -1,3 +1,36 @@ export const CARD_IAB_CONFIG = 'directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,hidden=yes,clearcache=yes,hidespinner=yes,disallowoverscroll=yes,zoom=no,transitionstyle=crossdissolve'; export const DUST_AMOUNT = 546; +export const currency = { + name: 'Lotus', + ticker: 'XPI', + legacyPrefix: 'bitcoincash', + prefixes: ['lotus'], + coingeckoId: 'bitcoin-cash-abc-2', + defaultFee: 1.01, + dustSats: 550, + etokenSats: 546, + cashDecimals: 6, + blockExplorerUrl: 'https://explorer.givelotus.org', + tokenExplorerUrl: 'https://explorer.be.cash', + blockExplorerUrlTestnet: 'https://texplorer.bitcoinabc.org', + tokenName: 'eToken', + tokenTicker: 'eToken', + tokenPrefixes: ['etoken'], + tokenIconsUrl: '', // https://tokens.bitcoin.com/32 for BCH SLP + txHistoryCount: 10, + hydrateUtxoBatchSize: 20, + defaultSettings: { fiatCurrency: 'usd' }, + opReturn: { + opReturnPrefixHex: '6a', + opReturnAppPrefixLengthHex: '04', + opPushDataOne: '4c', + appPrefixesHex: { + eToken: '534c5000', + lotusChat: '02020202', + lotusChatEncrypted: '03030303' + }, + encryptedMsgByteLimit: 206, + unencryptedMsgByteLimit: 215 + } +}; \ No newline at end of file diff --git a/src/app/pages/pages.ts b/src/app/pages/pages.ts index f9d0b143ffc..20480a6f7c4 100644 --- a/src/app/pages/pages.ts +++ b/src/app/pages/pages.ts @@ -111,8 +111,10 @@ import { TokenInforPage } from './token-info/token-info'; import { AccountsPage } from './accounts/accounts'; import { SearchContactPage } from './search/search-contact/search-contact.component'; import { SelectFlowPage } from './onboarding/select-flow/select-flow'; +import { MessageReplyComponent } from '../components/message-reply/message-reply.component'; export const PAGES = [ + MessageReplyComponent, AddPage, AddWalletPage, AmountPage, diff --git a/src/app/pages/send/confirm/confirm.html b/src/app/pages/send/confirm/confirm.html index 2c26f6f90f7..a88267827e0 100644 --- a/src/app/pages/send/confirm/confirm.html +++ b/src/app/pages/send/confirm/confirm.html @@ -70,7 +70,7 @@ · - + {{tx.txp[wallet.id].feeRatePerStr}} of total amount @@ -200,6 +200,21 @@
+ + +
+ Message +
+
+ + +
+ {{tx.messageOnChainToShow}} +
+
+
+ +
diff --git a/src/app/pages/send/confirm/confirm.scss b/src/app/pages/send/confirm/confirm.scss index a6b4fa86c16..111c40249b1 100644 --- a/src/app/pages/send/confirm/confirm.scss +++ b/src/app/pages/send/confirm/confirm.scss @@ -72,6 +72,15 @@ page-confirm { } } + .warning-ico { + margin: unset; + padding: unset; + } + + .label-message-onchain { + align-self: flex-start; + } + .padded-input { padding-top: 10px; padding-bottom: 15px; @@ -376,6 +385,9 @@ page-confirm { .address-note-custom { display: flex; letter-spacing: 0.4px; + .onchain-message-txt { + color: #001E2E !important; + } } .input-contact-custom { diff --git a/src/app/pages/send/confirm/confirm.ts b/src/app/pages/send/confirm/confirm.ts index 7bc1a56cfaf..8348e23ef86 100644 --- a/src/app/pages/send/confirm/confirm.ts +++ b/src/app/pages/send/confirm/confirm.ts @@ -33,14 +33,18 @@ import { TransactionProposal, WalletProvider } from '../../../providers/wallet/wallet'; -import { IonRouterOutlet, ModalController, NavController, NavParams } from '@ionic/angular'; +import { + IonRouterOutlet, + ModalController, + NavController, + NavParams +} from '@ionic/angular'; import { EventManagerService } from 'src/app/providers/event-manager.service'; import { Router } from '@angular/router'; import { ChooseFeeLevelModal } from '../../choose-fee-level/choose-fee-level'; -import { FinishModalPage } from '../../finish/finish'; -import { PreviousRouteService } from 'src/app/providers/previous-route/previous-route'; import { LoadingProvider } from 'src/app/providers/loading/loading'; -import { EventsService } from 'src/app/providers/events.service'; +import { OnchainMessageProvider } from 'src/app/providers'; +import { DUST_AMOUNT } from 'src/app/constants'; @Component({ selector: 'page-confirm', templateUrl: 'confirm.html', @@ -165,14 +169,21 @@ export class ConfirmPage { private location: Location, private routerOutlet: IonRouterOutlet, private loadingProvider: LoadingProvider, - private eventsService: EventsService + private onchainMessageProvider: OnchainMessageProvider ) { if (this.router.getCurrentNavigation()) { - this.navParamsData = this.router.getCurrentNavigation().extras.state ? this.router.getCurrentNavigation().extras.state : {}; + this.navParamsData = this.router.getCurrentNavigation().extras.state + ? this.router.getCurrentNavigation().extras.state + : {}; } else { this.navParamsData = history ? history.state : {}; } - if (_.isEmpty(this.navParamsData) && this.navParams && !_.isEmpty(this.navParams.data)) this.navParamsData = this.navParams.data; + if ( + _.isEmpty(this.navParamsData) && + this.navParams && + !_.isEmpty(this.navParams.data) + ) + this.navParamsData = this.navParams.data; this.wallet = this.profileProvider.getWallet(this.navParamsData.walletId); this.isDonation = this.navParamsData.isDonation; @@ -232,7 +243,7 @@ export class ConfirmPage { this.routerOutlet.swipeGesture = true; } - loadInit() { + async loadInit() { this.logger.info('Loaded: ConfirmPage'); this.routerOutlet.swipeGesture = false; this.isOpenSelector = false; @@ -248,12 +259,10 @@ export class ConfirmPage { amount = this.navParamsData.amount ? this.navParamsData.amount : this.navParamsData.totalInputsAmount; - } - else if (this.navParamsData.isSentXecToEtoken) { + } else if (this.navParamsData.isSentXecToEtoken) { networkName = this.navParamsData.network; amount = this.navParamsData.amount; - } - else { + } else { amount = this.navParamsData.amount; try { networkName = this.addressProvider.getCoinAndNetwork( @@ -282,7 +291,30 @@ export class ConfirmPage { return; } } - + let messageEncrypted = null; + if ( + !!( + this.navParamsData && + this.navParamsData.messageOnChain && + this.navParamsData.messageOnChain.trim().length > 0 + ) + ) { + const result = await this.walletProvider.getMnemonicAndPassword( + this.wallet + ); + try{ + messageEncrypted = + await this.onchainMessageProvider.processEncryptMessageOnchain( + this.navParamsData.messageOnChain, + this.wallet, + result.mnemonic, + this.navParamsData.toAddress + ); + } catch(e){ + this.showErrorInfoSheet(e); + this.location.back(); + } + } this.tx = { toAddress: this.navParamsData.toAddress, description: this.navParamsData.description, @@ -291,7 +323,8 @@ export class ConfirmPage { invoiceID: this.navParamsData.invoiceID, // xrp payProUrl: this.navParamsData.payProUrl, spendUnconfirmed: this.config.wallet.spendUnconfirmed, - + messageOnChain: !!messageEncrypted ? Array.from(messageEncrypted) : null, + messageOnChainToShow: this.navParamsData.messageOnChain, // Vanity tx info (not in the real tx) recipientType: this.navParamsData.recipientType, name: this.navParamsData.name, @@ -318,15 +351,15 @@ export class ConfirmPage { ? 0 : this.tx.coin == 'eth' || this.currencyProvider.isERCToken(this.tx.coin) - ? Number(amount) - : parseInt(amount, 10); + ? Number(amount) + : parseInt(amount, 10); this.tx.origToAddress = this.tx.toAddress; if (this.navParamsData.requiredFeeRate) { this.usingMerchantFee = true; - this.tx.feeRate = this.requiredFeeRate = +this.navParamsData - .requiredFeeRate; + this.tx.feeRate = this.requiredFeeRate = + +this.navParamsData.requiredFeeRate; } else if (this.isSpeedUpTx) { this.usingCustomFee = true; this.tx.feeLevel = @@ -510,7 +543,8 @@ export class ConfirmPage { this.wallet.credentials.multisigEthInfo && this.wallet.credentials.multisigEthInfo.multisigContractAddress ) { - this.tx.multisigContractAddress = this.wallet.credentials.multisigEthInfo.multisigContractAddress; + this.tx.multisigContractAddress = + this.wallet.credentials.multisigEthInfo.multisigContractAddress; } this.setButtonText( @@ -571,7 +605,7 @@ export class ConfirmPage { return ( this.wallet.cachedStatus && this.wallet.cachedStatus.balance.totalAmount >= - this.tx.amount + this.tx.feeRate && + this.tx.amount + this.tx.feeRate && !this.tx.spendUnconfirmed ); } @@ -853,21 +887,20 @@ export class ConfirmPage { } showHighFeeSheet() { - const minerFeeWarning = this.actionSheetProvider.createMinerFeeWarningComponent(); + const minerFeeWarning = + this.actionSheetProvider.createMinerFeeWarningComponent(); minerFeeWarning.present({ maxHeight: '100%', minHeight: '100%' }); } showTotalAmountSheet() { - const totalAmountFeeInfoSheet = this.actionSheetProvider.createInfoSheet( - 'total-amount' - ); + const totalAmountFeeInfoSheet = + this.actionSheetProvider.createInfoSheet('total-amount'); totalAmountFeeInfoSheet.present(); } showSubtotalAmountSheet() { - const subtotalAmountFeeInfoSheet = this.actionSheetProvider.createInfoSheet( - 'subtotal-amount' - ); + const subtotalAmountFeeInfoSheet = + this.actionSheetProvider.createInfoSheet('subtotal-amount'); subtotalAmountFeeInfoSheet.present(); } @@ -915,8 +948,8 @@ export class ConfirmPage { if (!this.minAllowedGasLimit) this.minAllowedGasLimit = this.tx.txp[wallet.id].gasLimit; } - - if (txp.feeTooHigh) { + txp.messageOnChain = tx.messageOnChain; + if (txp.feeTooHigh && ((!this.navParamsData.messageOnChain && !txp.messageOnChain) || (txp.messageOnChain && txp.amount !== DUST_AMOUNT))) { this.showHighFeeSheet(); } @@ -937,9 +970,9 @@ export class ConfirmPage { this.tx = tx; this.logger.debug( 'Confirm. TX Fully Updated for wallet:' + - wallet.id + - ' Txp:' + - txp.id + wallet.id + + ' Txp:' + + txp.id ); this.getTotalAmountDetails(tx, wallet); @@ -1162,7 +1195,7 @@ export class ConfirmPage { } txp.message = tx.description; - + txp.messageOnChain = tx.messageOnChain; if (tx.paypro) { txp.payProUrl = tx.payProUrl; tx.paypro.host = new URL(tx.payProUrl).host; @@ -1273,8 +1306,7 @@ export class ConfirmPage { .Transactions.get({ chain: 'ETHMULTISIG' }) .instantiateEncodeData({ addresses: this.navParamsData.multisigAddresses, - requiredConfirmations: this.navParamsData - .requiredConfirmations, + requiredConfirmations: this.navParamsData.requiredConfirmations, multisigGnosisContractAddress: tx.multisigContractAddress, dailyLimit: 0 }); @@ -1331,10 +1363,11 @@ export class ConfirmPage { sender: txp.from, txId: txp.txid }; - multisigContractInstantiationInfo = await this.walletProvider.getMultisigContractInstantiationInfo( - this.wallet, - opts - ); + multisigContractInstantiationInfo = + await this.walletProvider.getMultisigContractInstantiationInfo( + this.wallet, + opts + ); if (multisigContractInstantiationInfo.length > 0) { const multisigContract = multisigContractInstantiationInfo.filter( multisigContract => { @@ -1403,13 +1436,14 @@ export class ConfirmPage { } private showInsufficientFundsInfoSheet(): void { - const insufficientFundsInfoSheet = this.actionSheetProvider.createInfoSheet( - 'insufficient-funds' - ); + const insufficientFundsInfoSheet = + this.actionSheetProvider.createInfoSheet('insufficient-funds'); insufficientFundsInfoSheet.present(); insufficientFundsInfoSheet.onDidDismiss(option => { if (option || typeof option === 'undefined') { - this.fromWalletDetails ? this.location.back() : this.router.navigate(['']); + this.fromWalletDetails + ? this.location.back() + : this.router.navigate(['']); } else { this.tx.sendMax = true; this.setWallet(this.wallet); @@ -1453,7 +1487,9 @@ export class ConfirmPage { return; } if (exit) { - this.fromWalletDetails ? this.location.back() : this.router.navigate(['']); + this.fromWalletDetails + ? this.location.back() + : this.router.navigate(['']); } }); } @@ -1484,11 +1520,11 @@ export class ConfirmPage { if (error.toString().includes('500 - "{}"')) { msg = this.tx.paypro ? this.translate.instant( - 'There is a temporary problem with the merchant requesting the payment. Please try later' - ) + 'There is a temporary problem with the merchant requesting the payment. Please try later' + ) : this.translate.instant( - 'Error 500 - There is a temporary problem, please try again later.' - ); + 'Error 500 - There is a temporary problem, please try again later.' + ); } const infoSheetTitle = title ? title : this.translate.instant('Error'); @@ -1500,7 +1536,7 @@ export class ConfirmPage { if (exit) { this.fromWalletDetails ? // PopTo AmountPage case - this.location.back() + this.location.back() : this.router.navigate([''], { replaceUrl: true }); } } @@ -1516,7 +1552,7 @@ export class ConfirmPage { else this.setWallet(option); } - public approve(tx, wallet): Promise { + public approve(tx, wallet): any { if (!tx || (!wallet && !this.coinbaseAccount)) return undefined; if (this.nameContact && this.nameContact.trim().length > 0) { this.ab @@ -1840,9 +1876,8 @@ export class ConfirmPage { title: this.walletSelectorTitle, coinbaseData }; - const walletSelector = this.actionSheetProvider.createWalletSelector( - params - ); + const walletSelector = + this.actionSheetProvider.createWalletSelector(params); walletSelector.present(); walletSelector.onDidDismiss(option => { this.onSelectWalletEvent(option); @@ -1882,9 +1917,8 @@ export class ConfirmPage { } private showCustomFeeWarningSheet(data) { - const warningSheet = this.actionSheetProvider.createInfoSheet( - 'custom-fee-warning' - ); + const warningSheet = + this.actionSheetProvider.createInfoSheet('custom-fee-warning'); warningSheet.present(); warningSheet.onDidDismiss(option => { option ? this.chooseFeeLevel() : this.onFeeModalDismiss(data); @@ -1907,9 +1941,8 @@ export class ConfirmPage { public setGasLimit(): void { this.editGasLimit = !this.editGasLimit; - this.tx.gasLimit = this.tx.txp[ - this.wallet.id - ].gasLimit = this.customGasLimit; + this.tx.gasLimit = this.tx.txp[this.wallet.id].gasLimit = + this.customGasLimit; const data = { newFeeLevel: 'custom', diff --git a/src/app/pages/send/send.html b/src/app/pages/send/send.html index e9a8a0b1cfc..5e1f3e8d89b 100644 --- a/src/app/pages/send/send.html +++ b/src/app/pages/send/send.html @@ -75,7 +75,7 @@ + [isShowDelete]="isShowDelete" [token]="token" (sendMaxEvent)="sendMax($event)" [isShowMessage]="isShowMessage && wallet.coin === 'xpi'" (sendOfficialInfo)="handleOfficialInfo($event)">
diff --git a/src/app/pages/send/send.ts b/src/app/pages/send/send.ts index 662fbd1c6c5..fc215d7e44a 100644 --- a/src/app/pages/send/send.ts +++ b/src/app/pages/send/send.ts @@ -32,6 +32,7 @@ import { PageDto, PageModel } from 'src/app/providers/lixi-lotus/lixi-lotus'; import { EventsService } from 'src/app/providers/events.service'; import { Location } from '@angular/common'; +import { DUST_AMOUNT } from 'src/app/constants'; @Component({ @@ -71,6 +72,7 @@ export class SendPage { walletId: string; isShowSendMax: boolean = true; isShowDelete: boolean = false; + isShowMessage: boolean = false; toAddress: string = ''; formatRemaining: string; recipientNotInit: RecipientModel; @@ -149,6 +151,9 @@ export class SendPage { this.onResumeSubscription = this.plt.resume.subscribe(() => { this.setDataFromClipboard(); }); + if(this.wallet.coin === 'xpi' && this.wallet.cachedStatus.wallet.singleAddress){ + this.isShowMessage = true; + } } async handleScrolling(event) { @@ -319,6 +324,9 @@ export class SendPage { })) this.isShowSendMax = this.listRecipient.length === 1; this.isShowDelete = this.listRecipient.length > 1; + if(this.wallet.coin === 'xpi' && this.wallet.cachedStatus.wallet.singleAddress){ + this.isShowMessage = this.listRecipient.length === 1; + } this.content.scrollToBottom(1000); } @@ -326,6 +334,7 @@ export class SendPage { this.listRecipient = this.listRecipient.filter(s => s.id !== id); this.isShowSendMax = this.listRecipient.length === 1; this.isShowDelete = this.listRecipient.length > 1; + this.isShowMessage = this.listRecipient.length === 1; } private goToConfirmToken(isSendMax?: boolean) { @@ -370,6 +379,10 @@ export class SendPage { if (this.isDonation) return this.goToConfirmDonation(); if (this.listRecipient.length === 1) { const recipient = this.listRecipient[0]; + if(!recipient.amount || recipient.amount === 0){ + recipient.amount = DUST_AMOUNT; + } + this.router.navigate(['/confirm'], { state: { walletId: this.wallet.credentials.walletId, @@ -383,7 +396,8 @@ export class SendPage { name: recipient.name, fromWalletDetails: true, isSentXecToEtoken: recipient.isSentXecToEtoken, - isSendFromHome: this.isSendFromHome + isSendFromHome: this.isSendFromHome, + messageOnChain: recipient.message } }); } else { diff --git a/src/app/pages/tx-details/tx-details.html b/src/app/pages/tx-details/tx-details.html index a673a30322f..c86cad45856 100644 --- a/src/app/pages/tx-details/tx-details.html +++ b/src/app/pages/tx-details/tx-details.html @@ -199,6 +199,25 @@
+ + + {{'Message' | translate}} + + +

+ {{messageOnchain}}

+ + + + {{'Reply' | translate}} + + +
+
+ +
+ {{'Memo' | translate}} diff --git a/src/app/pages/tx-details/tx-details.scss b/src/app/pages/tx-details/tx-details.scss index 58949e4b49b..86a37cc97bf 100644 --- a/src/app/pages/tx-details/tx-details.scss +++ b/src/app/pages/tx-details/tx-details.scss @@ -282,6 +282,21 @@ page-tx-details { align-items: center; } + .message-custom { + ion-label { + align-self: flex-start !important; + } + + ion-note { + display: flex; + flex-direction: column; + align-items: flex-end; + } + .message-onchain { + margin: 0; + } + } + .wallet, .multi-recip-title { span { @@ -481,4 +496,9 @@ page-tx-details { font-weight: 600; height: 2.5rem; } + + .btn-reply { + --padding-start: 0; + --padding-end: 0; + } } diff --git a/src/app/pages/tx-details/tx-details.ts b/src/app/pages/tx-details/tx-details.ts index c802d596aad..1f2095a9c79 100644 --- a/src/app/pages/tx-details/tx-details.ts +++ b/src/app/pages/tx-details/tx-details.ts @@ -20,17 +20,21 @@ import { EventManagerService } from 'src/app/providers/event-manager.service'; import { ModalController, NavController, NavParams } from '@ionic/angular'; import { Location } from '@angular/common'; import { PersistenceProvider } from 'src/app/providers/persistence/persistence'; -import { AddressBookProvider, AppProvider, TokenProvider } from 'src/app/providers'; +import { + AddressBookProvider, + AppProvider, + TokenProvider +} from 'src/app/providers'; import { Router } from '@angular/router'; import { DecimalFormatBalance } from 'src/app/providers/decimal-format.ts/decimal-format'; import { Token } from 'src/app/models/tokens/tokens.model'; export interface TokenData { - amountToken: string, - tokenId: string, - symbolToken: string, - name: string, - addressToShow: string + amountToken: string; + tokenId: string; + symbolToken: string; + name: string; + addressToShow: string; } @Component({ @@ -39,9 +43,9 @@ export interface TokenData { styleUrls: ['tx-details.scss'], encapsulation: ViewEncapsulation.None }) - export class TxDetailsModal { private txId: string; + public messageOnchain: string = ''; private config; private blockexplorerUrl: string; private blockexplorerUrlTestnet: string; @@ -62,7 +66,6 @@ export class TxDetailsModal { public isNegative: boolean; public currentTheme; public fiatRateStrToken; - public addressbook = []; constructor( @@ -88,14 +91,17 @@ export class TxDetailsModal { private appProvider: AppProvider, private router: Router, private tokenProvider: TokenProvider, - private addressbookProvider: AddressBookProvider, - ) { } - + private addressbookProvider: AddressBookProvider + ) {} + ngOnInit() { this.events.subscribe('bwsEvent', this.bwsEventHandler); this.config = this.configProvider.get(); this.currentTheme = this.appProvider.themeProvider.currentAppTheme; this.txId = this.navParams.data.txid; + if (this.navParams.data && this.navParams.data.messageOnchain) { + this.messageOnchain = this.navParams.data.messageOnchain; + } this.title = this.translate.instant('Transaction'); this.wallet = this.profileProvider.getWallet(this.navParams.data.walletId); this.tokenData = this.navParams.data.tokenData; @@ -186,6 +192,10 @@ export class TxDetailsModal { }); } + public showReplyMessageModal() { + this.viewCtrl.dismiss(true); + } + private initActionList(): void { this.actionList = []; if ( @@ -238,7 +248,7 @@ export class TxDetailsModal { leading: true } ); - + async updateInputAddress(txId: string) { let inputAddresses = []; try { @@ -254,14 +264,17 @@ export class TxDetailsModal { try { const walletId = this.navParams.data.walletId; const history = await this.walletProvider.getSavedTxs(walletId); - if (!history) return ; + if (!history) return; const historyByTxId = _.find(history, item => item.txid == txid); if (historyByTxId) { historyByTxId.inputAddresses = inputAddresses; const historyToSave = JSON.stringify(history); - return await this.persistenceProvider.setTxHistory(walletId, historyToSave); + return await this.persistenceProvider.setTxHistory( + walletId, + historyToSave + ); } - } catch (error) { } + } catch (error) {} } private updateTx(opts?): void { @@ -272,7 +285,6 @@ export class TxDetailsModal { .then(async tx => { this.retryGetTx = 0; if (!opts.hideLoading) this.onGoingProcess.clear(); - this.btx = this.txFormatProvider.processTx(this.wallet.coin, tx); this.btx.network = this.wallet.credentials.network; this.btx.coin = this.wallet.coin; @@ -307,27 +319,26 @@ export class TxDetailsModal { } if (this.btx.action != 'invalid') { - if (this.btx.isGenesis) { this.title = this.translate.instant('Genesis'); } else { - if (this.btx.action == 'sent'){ + if (this.btx.action == 'sent') { this.title = this.translate.instant('Sent'); this.isNegative = true; } - if (this.btx.action == 'received'){ + if (this.btx.action == 'received') { this.title = this.translate.instant('Received'); this.isNegative = false; } - if (this.btx.action == 'moved'){ + if (this.btx.action == 'moved') { this.title = this.translate.instant('Sent to self'); this.isNegative = false; } - if (this.btx.action == 'immature'){ + if (this.btx.action == 'immature') { this.title = this.translate.instant('Immature'); this.isNegative = false; } - if (this.btx.action == 'mined'){ + if (this.btx.action == 'mined') { this.title = this.translate.instant('Mined'); this.isNegative = false; } @@ -338,7 +349,7 @@ export class TxDetailsModal { this.initActionList(); this.updateFiatRate(); - if(this.token){ + if (this.token) { this.getFiatRateStrToken(); } if (this.currencyProvider.isUtxoCoin(this.wallet.coin)) { @@ -385,19 +396,26 @@ export class TxDetailsModal { tokenInfo: { symbol: this.btx?.symbolToken } - } - const alternativeBalanceToken = this.tokenProvider.getAlternativeBalanceToken(token, this.wallet); - let rate = this.rateProvider.getRate(this.wallet.cachedStatus.alternativeIsoCode, token.tokenInfo.symbol); + }; + const alternativeBalanceToken = + this.tokenProvider.getAlternativeBalanceToken(token, this.wallet); + let rate = this.rateProvider.getRate( + this.wallet.cachedStatus.alternativeIsoCode, + token.tokenInfo.symbol + ); if (!rate) { rate = 0; } - this.fiatRateStrToken = - DecimalFormatBalance(alternativeBalanceToken) + - ' ' + - this.wallet.cachedStatus.alternativeIsoCode + - ' @ ' + DecimalFormatBalance(rate) + ' ' + - this.wallet.cachedStatus.alternativeIsoCode + ' per ' + - token.tokenInfo.symbol.toUpperCase(); + this.fiatRateStrToken = + DecimalFormatBalance(alternativeBalanceToken) + + ' ' + + this.wallet.cachedStatus.alternativeIsoCode + + ' @ ' + + DecimalFormatBalance(rate) + + ' ' + + this.wallet.cachedStatus.alternativeIsoCode + + ' per ' + + token.tokenInfo.symbol.toUpperCase(); } public async saveMemoInfo(): Promise { @@ -462,6 +480,10 @@ export class TxDetailsModal { } } + public handleClick(btx) { + this.viewCtrl.dismiss(true); + } + public openExternalLink(url: string): void { const optIn = true; const title = null; @@ -494,7 +516,8 @@ export class TxDetailsModal { this.getFiatStr(fiat) + ' ' + settings.alternativeIsoCode + - ' @ ' + this.getAlternativeIsoCode(fiat) + + ' @ ' + + this.getAlternativeIsoCode(fiat) + ` ${settings.alternativeIsoCode} per ` + this.wallet.coin.toUpperCase(); } else { @@ -504,13 +527,21 @@ export class TxDetailsModal { } getAlternativeIsoCode(fiat) { - return this.btx.coin == 'xpi' || this.btx.coin == 'xec' ? parseFloat(fiat.rate).toFixed(6).toString() : this.filter.formatFiatAmount(fiat.rate); + return this.btx.coin == 'xpi' || this.btx.coin == 'xec' + ? parseFloat(fiat.rate).toFixed(6).toString() + : this.filter.formatFiatAmount(fiat.rate); } getFiatStr(fiat) { return this.btx.coin == 'xpi' || this.btx.coin == 'xec' - ? parseFloat((fiat.rate * this.btx.amountValueStr.replace(',', '')).toFixed(4)).toString() - : this.filter.formatFiatAmount(parseFloat((fiat.rate * this.btx.amountValueStr.replace(',', '')).toFixed(2))); + ? parseFloat( + (fiat.rate * this.btx.amountValueStr.replace(',', '')).toFixed(4) + ).toString() + : this.filter.formatFiatAmount( + parseFloat( + (fiat.rate * this.btx.amountValueStr.replace(',', '')).toFixed(2) + ) + ); } close() { @@ -522,7 +553,14 @@ export class TxDetailsModal { this.router.navigate(['/send-page'], { state: { walletId: this.wallet.id, - toAddress: btx.address || (btx.addressTo && btx.addressTo !== 'false') ? btx.addressTo : false || (this.tokenData && this.tokenData.addressToShow !== 'false' ? this.tokenData.addressToShow : btx.inputAddresses[0]) || btx.inputAddresses[0], + toAddress: + btx.address || (btx.addressTo && btx.addressTo !== 'false') + ? btx.addressTo + : false || + (this.tokenData && this.tokenData.addressToShow !== 'false' + ? this.tokenData.addressToShow + : btx.inputAddresses[0]) || + btx.inputAddresses[0], token: this.token } }); diff --git a/src/app/pages/wallet-details/wallet-details.html b/src/app/pages/wallet-details/wallet-details.html index 2c53c4315fe..31da40c0d1c 100644 --- a/src/app/pages/wallet-details/wallet-details.html +++ b/src/app/pages/wallet-details/wallet-details.html @@ -233,10 +233,10 @@
- -
+
{{'To: + *ngIf="(!tx.note || (tx.note && !tx.note.body)) && (!addressbook || !tx.outputs[0] || !getContactName(tx.outputs[0].address)) && (!tx.customData || !tx.customData.toWalletName) && tx.addressTo !== 'false'">{{'To: {address}' | translate: {address: tx.addressTo.slice(-8)} }} + {{'To: + {address}' | translate: {address: tx.outputs[0].address.slice(-8)} }} {{ 'To: {walletName}' | translate: {walletName: tx.customData.toWalletName} | translate}} @@ -292,9 +295,11 @@ {{tx.message}} + + {{tx.messageOnchain}}
- +

{{'Pending'| translate}}

@@ -307,6 +312,7 @@

Sent to self

{{'Burned'| translate}}

+

Genesis

Mined

Immature

@@ -315,6 +321,10 @@ amTimeAgo}}

{{tx.time * 1000 | amDateFormat:'MM/DD/YYYY'}}

+ + + {{'Reply' | translate}} +
diff --git a/src/app/pages/wallet-details/wallet-details.scss b/src/app/pages/wallet-details/wallet-details.scss index df1e90233c2..90b978cbc59 100644 --- a/src/app/pages/wallet-details/wallet-details.scss +++ b/src/app/pages/wallet-details/wallet-details.scss @@ -253,6 +253,26 @@ page-wallet-details { .item-icon { margin-right: 15px; } + ion-note { + &.note-onchain-message { + height: 100%; + .tx-info { + height: 100%; + justify-content: center; + } + .status-onchain-white { + background: transparent; + } + } + &.onchain-message-received { + .tx-info { + justify-content: space-between; + p { + margin: 0; + } + } + } + } } img { @@ -269,6 +289,8 @@ page-wallet-details { margin-right: 2rem; .memo { font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; color: rgba(0, 30, 46, 0.6); display: -webkit-box; -webkit-line-clamp: 2; @@ -307,6 +329,11 @@ page-wallet-details { letter-spacing: 0.4px; } + .tx-action-custom { + font-size: 14px; + letter-spacing: 0.4px; + } + .proposal-container { --inner-padding-end: 0; --inner-padding-top: 0; @@ -540,6 +567,11 @@ page-wallet-details { .date { margin: 0; font-size: 12px; + &.btn-reply { + --color: var(--color-light-on-background-theme); + --padding-start: 0; + --padding-end: 0; + } } } diff --git a/src/app/pages/wallet-details/wallet-details.ts b/src/app/pages/wallet-details/wallet-details.ts index e11582be5d5..dc210491238 100644 --- a/src/app/pages/wallet-details/wallet-details.ts +++ b/src/app/pages/wallet-details/wallet-details.ts @@ -33,11 +33,17 @@ import { WalletProvider } from '../../providers/wallet/wallet'; import { TxDetailsModal } from '../../pages/tx-details/tx-details'; import { SearchTxModalPage } from './search-tx-modal/search-tx-modal'; import { WalletBalanceModal } from './wallet-balance/wallet-balance'; -import { LoadingController, ModalController, Platform, ToastController } from '@ionic/angular'; +import { + LoadingController, + ModalController, + Platform, + ToastController +} from '@ionic/angular'; import { Router } from '@angular/router'; import { Location } from '@angular/common'; import { NgxQrcodeErrorCorrectionLevels } from '@techiediaries/ngx-qrcode'; import { EventsService } from 'src/app/providers/events.service'; +import { DUST_AMOUNT } from 'src/app/constants'; const HISTORY_SHOW_LIMIT = 10; const MIN_UPDATE_TIME = 2000; const TIMEOUT_FOR_REFRESHER = 1000; @@ -115,7 +121,6 @@ export class WalletDetailsPage { private actionSheetProvider: ActionSheetProvider, private platform: Platform, private profileProvider: ProfileProvider, - private viewCtrl: ModalController, public platformProvider: PlatformProvider, private socialSharing: SocialSharing, private bwcErrorProvider: BwcErrorProvider, @@ -331,18 +336,24 @@ export class WalletDetailsPage { } private handleTxAddressEcash() { - this.history.forEach((tx) => { + this.history.forEach(tx => { if (tx.action == 'received' && !tx?.tokenId) { const addressToken = tx.inputAddresses[0] || null; if (addressToken) { - const { prefix, type, hash } = this.addressProvider.decodeAddress(addressToken); - const eCashAddess = this.addressProvider.encodeAddress('ecash', type, hash, addressToken); + const { prefix, type, hash } = + this.addressProvider.decodeAddress(addressToken); + const eCashAddess = this.addressProvider.encodeAddress( + 'ecash', + type, + hash, + addressToken + ); tx.inputAddresses[0] = eCashAddess; } } - }) + }); } - + private async showHistory(loading?: boolean) { if (!this.wallet.completeHistory) { return; @@ -351,12 +362,115 @@ export class WalletDetailsPage { 0, (this.currentPage + 1) * HISTORY_SHOW_LIMIT ); + if ( + this.wallet.coin === 'xpi' && + this.wallet.cachedStatus.wallet.singleAddress + ) { + if (this.history && this.history.length > 0) { + for (let index = 0; index < this.history.length; index++) { + const tx = this.history[index]; + this.history[index].outputs = tx.outputs.filter( + output => output.address !== 'false' + ); + } + } + } if (this.wallet.coin == 'xec') this.handleTxAddressEcash(); this.zone.run(() => { this.groupedHistory = this.groupHistory(this.history); }); if (loading) this.currentPage++; } + handleClick(tx) { + this.showReplyMessageModal(tx); + } + + getAddressFrom(tx): any { + if ( + !this.addressbook || + !tx.inputAddresses[0] || + !this.getContactName(tx.inputAddresses[0]) + ) { + return { + name: tx.inputAddresses[0].slice(-8), + address: tx.inputAddresses[0], + type: '' + }; + } else + return { + name: this.getContactName(tx.inputAddresses[0]), + address: tx.inputAddresses[0], + type: 'contact' + }; + } + + getAddressTo(tx): any { + if ( + (!tx.note || (tx.note && !tx.note.body)) && + (!this.addressbook || + !tx.outputs[0] || + !this.getContactName(tx.outputs[0].address)) && + (!tx.customData || !tx.customData.toWalletName) + ) { + return { + name: tx.addressTo.slice(-8), + address: tx.addressTo, + type: '' + }; + } else if ( + (!tx.note || (tx.note && !tx.note.body)) && + (!this.addressbook || + !tx.outputs[0] || + !this.getContactName(tx.outputs[0].address)) && + tx.customData && + tx.customData.toWalletName + ) { + return { + name: tx.customData.toWalletName, + address: tx.outputs[0].address, + type: 'wallet' + }; + } else + return { + name: this.getContactName(tx.outputs[0].address), + address: tx.outputs[0].address, + type: 'contact' + }; + } + + showReplyMessageModal(tx) { + const txInfo = this.getAddressFrom(tx); + const addContactModal = + this.actionSheetProvider.createMessageReplyComponent({ + addressTo: this.getAddressFrom(tx).name, + messageOnChain: tx.messageOnchain + }); + addContactModal.present({ maxHeight: '48%%', minHeight: '48%%' }); + addContactModal.onDidDismiss(rs => { + if (rs) { + this.sendReplyMessage(rs, txInfo); + } + }); + } + + sendReplyMessage(rs, txInfo) { + this.router.navigate(['/confirm'], { + state: { + walletId: this.wallet.credentials.walletId, + recipientType: txInfo.type, + amount: DUST_AMOUNT, + currency: this.wallet.coin, + coin: this.wallet.coin, + network: this.wallet.network, + useSendMax: false, + toAddress: txInfo.address, + name: txInfo.type === 'contact' ? txInfo.name : null, + fromWalletDetails: true, + isSentXecToEtoken: false, + messageOnChain: rs + } + }); + } updateAddressToShowToken(tx) { const outputAddr = tx.outputs[0].address; @@ -579,7 +693,12 @@ export class WalletDetailsPage { } }; - public itemTapped(tx) { + public itemTapped(tx, itemTapped) { + if (itemTapped.target.innerText === 'Reply') { + itemTapped.preventDefault(); + itemTapped.stopPropagation(); + return; + } if (tx.hasUnconfirmedInputs) { const infoSheet = this.actionSheetProvider.createInfoSheet('unconfirmed-inputs'); @@ -630,18 +749,20 @@ export class WalletDetailsPage { }); } - public goToTxDetails(tx) { - const txDetailModal = this.modalCtrl - .create({ - component: TxDetailsModal, - componentProps: { - walletId: this.wallet.credentials.walletId, - txid: tx.txid - } - }) - .then(res => { - res.present(); - }); + public async goToTxDetails(tx) { + const txDetailModal = await this.modalCtrl.create({ + component: TxDetailsModal, + componentProps: { + walletId: this.wallet.credentials.walletId, + txid: tx.txid, + messageOnchain: tx.messageOnchain + } + }); + txDetailModal.present(); + const { data } = await txDetailModal.onWillDismiss(); + if (data) { + this.showReplyMessageModal(tx); + } } public openBackupModal(): void { @@ -787,7 +908,7 @@ export class WalletDetailsPage { } public close() { - this.viewCtrl.dismiss(); + this.modalCtrl.dismiss(); } public goToReceivePage() { diff --git a/src/app/providers/action-sheet/action-sheet.ts b/src/app/providers/action-sheet/action-sheet.ts index 8b2e6f5b622..5460647908f 100644 --- a/src/app/providers/action-sheet/action-sheet.ts +++ b/src/app/providers/action-sheet/action-sheet.ts @@ -6,6 +6,7 @@ import { FooterMenuComponent } from 'src/app/components/footer-menu/footer-menu' import { IncomingDataMenuComponent } from 'src/app/components/incoming-data-menu/incoming-data-menu'; import { InfoSheetComponent } from 'src/app/components/info-sheet/info-sheet'; import { MemoComponent } from 'src/app/components/memo-component/memo-component'; +import { MessageReplyComponent } from 'src/app/components/message-reply/message-reply.component'; import { MinerFeeWarningComponent } from 'src/app/components/miner-fee-warning/miner-fee-warning'; import { MultisignInfoComponent } from 'src/app/components/multisign-info/multisign-info.component'; import { NeedsBackupComponent } from 'src/app/components/needs-backup/needs-backup'; @@ -224,6 +225,15 @@ export class ActionSheetProvider { .instance; } + public createMessageReplyComponent(params?): MessageReplyComponent { + return this.setupSheet( + MessageReplyComponent, + null, + params + ) + .instance; + } + public createMinerFeeWarningComponent(): MinerFeeWarningComponent { return this.setupSheet(MinerFeeWarningComponent) .instance; diff --git a/src/app/providers/index.ts b/src/app/providers/index.ts index f15edb985b5..8101a3f226e 100644 --- a/src/app/providers/index.ts +++ b/src/app/providers/index.ts @@ -75,4 +75,5 @@ export { CustomErrorHandler } from './custom-error-handler.service'; export { RedirectGuard } from './redirect.service'; export { PreviousRouteService } from './previous-route/previous-route'; export { TokenProvider } from './token-sevice/token-sevice'; +export { OnchainMessageProvider } from './onchain-message/onchain-message'; export { LoadingProvider } from './loading/loading'; \ No newline at end of file diff --git a/src/app/providers/onchain-message/onchain-message.ts b/src/app/providers/onchain-message/onchain-message.ts new file mode 100644 index 00000000000..00b9c9d6e8f --- /dev/null +++ b/src/app/providers/onchain-message/onchain-message.ts @@ -0,0 +1,319 @@ +import { PrivateKey, PublicKey } from '@abcpros/bitcore-lib-xpi'; +import BCHJS from '@abcpros/xpi-js'; +import { Injectable } from '@angular/core'; +import { currency } from 'src/app/constants'; +import { Logger } from '../logger/logger'; +import * as forge from 'node-forge'; + +import { BitcoreLib } from '@abcpros/crypto-wallet-core'; +import { ChronikClient, TxHistoryPage } from 'chronik-client'; + +@Injectable({ + providedIn: 'root' +}) +export class OnchainMessageProvider { + bchjs; + Bitcore = BitcoreLib; + PrivateKey = this.Bitcore.PrivateKey; + PublicKey = this.Bitcore.PublicKey; + crypto2 = this.Bitcore.crypto; + + constructor(private logger: Logger) { + this.logger.debug('TokenProvider initialized'); + this.bchjs = new BCHJS({ restURL: '' }); + } + + ngOnInit() {} + + async getPrivateWifKey(wallet, mnemonic) { + const rootSeedBuffer = await this.bchjs.Mnemonic.toSeed(mnemonic); + let masterHDNode; + masterHDNode = this.bchjs.HDNode.fromSeed(rootSeedBuffer); + const rootPath = wallet.getRootPath() + ? wallet.getRootPath() + : "m/44'/1899'/0'"; + const node = this.bchjs.HDNode.derivePath(masterHDNode, rootPath); + const change = this.bchjs.HDNode.derivePath(node, '0/0'); + return this.bchjs.HDNode.toWIF(change); + } + + parseOpReturn(hexStr) { + if ( + !hexStr || + typeof hexStr !== 'string' || + hexStr.substring(0, 2) !== currency.opReturn.opReturnPrefixHex + ) { + return false; + } + + hexStr = hexStr.slice(2); // remove the first byte i.e. 6a + + /* + * @Return: resultArray is structured as follows: + * resultArray[0] is the transaction type i.e. eToken prefix, cashtab prefix, external message itself if unrecognized prefix + * resultArray[1] is the actual cashtab message or the 2nd part of an external message + * resultArray[2 - n] are the additional messages for future protcols + */ + let resultArray = []; + let message = ''; + let hexStrLength = hexStr.length; + + for (let i = 0; hexStrLength !== 0; i++) { + // part 1: check the preceding byte value for the subsequent message + let byteValue = hexStr.substring(0, 2); + let msgByteSize = 0; + if (byteValue === currency.opReturn.opPushDataOne) { + // if this byte is 4c then the next byte is the message byte size - retrieve the message byte size only + msgByteSize = parseInt(hexStr.substring(2, 4), 16); // hex base 16 to decimal base 10 + hexStr = hexStr.slice(4); // strip the 4c + message byte size info + } else { + // take the byte as the message byte size + msgByteSize = parseInt(hexStr.substring(0, 2), 16); // hex base 16 to decimal base 10 + hexStr = hexStr.slice(2); // strip the message byte size info + } + + // part 2: parse the subsequent message based on bytesize + const msgCharLength = 2 * msgByteSize; + message = hexStr.substring(0, msgCharLength); + if (i === 0 && message === currency.opReturn.appPrefixesHex.eToken) { + // add the extracted eToken prefix to array then exit loop + resultArray[i] = currency.opReturn.appPrefixesHex.eToken; + break; + } + else { + // this is either an external message or a subsequent cashtab message loop to extract the message + resultArray[i] = message; + } + + // strip out the parsed message + hexStr = hexStr.slice(msgCharLength); + hexStrLength = hexStr.length; + } + return resultArray; + } + + async getRecipientPublicKey( + XPI, + chronik: ChronikClient, + recipientAddress: string + ): Promise { + let recipientAddressHash160: string; + try { + recipientAddressHash160 = XPI.Address.toHash160(recipientAddress); + } catch (err) { + console.log( + `Error determining XPI.Address.toHash160(${recipientAddress} in getRecipientPublicKey())`, + err + ); + } + + let chronikTxHistoryAtAddress: TxHistoryPage; + try { + // Get 20 txs. If no outgoing txs in those 20 txs, just don't send the tx + chronikTxHistoryAtAddress = await chronik + .script('p2pkh', recipientAddressHash160) + .history(/*page=*/ 0, /*page_size=*/ 20); + } catch (err) { + console.log( + `Error getting await chronik.script('p2pkh', ${recipientAddressHash160}).history();`, + err + ); + throw new Error('Error fetching tx history to parse for public key'); + } + + let recipientPubKeyChronik; + + // Iterate over tx history to find an outgoing tx + for (let i = 0; i < chronikTxHistoryAtAddress.txs.length; i += 1) { + const { inputs } = chronikTxHistoryAtAddress.txs[i]; + for (let j = 0; j < inputs.length; j += 1) { + const thisInput = inputs[j]; + const thisInputSendingHash160 = thisInput.outputScript; + if (thisInputSendingHash160.includes(recipientAddressHash160)) { + // Then this is an outgoing tx, you can get the public key from this tx + // Get the public key + try { + recipientPubKeyChronik = + chronikTxHistoryAtAddress.txs[i].inputs[j].inputScript.slice(-66); + } catch (err) { + throw new Error( + 'Cannot send an encrypted message to a wallet with no outgoing transactions' + ); + } + return recipientPubKeyChronik; + } + } + } + // You get here if you find no outgoing txs in the chronik tx history + throw new Error( + 'Cannot send an encrypted message to a wallet with no outgoing transactions in the last 20 txs' + ); + } + + async processDecryptMessageOnchain( + outputScript, + wallet, + mnemonic, + addressRecepient + ): Promise { + const privateKeyWIF = await this.getPrivateWifKey(wallet, mnemonic); + let chronikClient = null; + if (wallet.coin === 'xpi') { + chronikClient = new ChronikClient('https://chronik.be.cash/xpi'); + } else { + chronikClient = new ChronikClient('https://chronik.be.cash/xec'); + } + const pubKeyHex = await this.getRecipientPublicKey( + this.bchjs, + chronikClient, + addressRecepient + ); + const messageDecrypted = this.decryptMessageOnchain( + outputScript, + privateKeyWIF, + pubKeyHex + ); + return messageDecrypted; + } + + async processEncryptMessageOnchain( + plainText, + wallet, + mnemonic, + addressRecepient + ) { + const privateKeyWIF = await this.getPrivateWifKey(wallet, mnemonic); + let chronikClient = null; + if (wallet.coin === 'xpi') { + chronikClient = new ChronikClient('https://chronik.be.cash/xpi'); + } else { + chronikClient = new ChronikClient('https://chronik.be.cash/xec'); + } + let pubKeyHex = await this.getRecipientPublicKey( + this.bchjs, + chronikClient, + addressRecepient + ); + if (!pubKeyHex) { + pubKeyHex = ''; + } + const encryptedMessage = this.encryptMessageOnchain( + privateKeyWIF, + pubKeyHex, + plainText + ); + return encryptedMessage; + } + + encryptMessageOnchain = ( + privateKeyWIF: string, + recipientPubKeyHex: string, + plainTextMsg: string + ): Uint8Array => { + let encryptedMsg; + try { + const sharedKey = this.createSharedKey(privateKeyWIF, recipientPubKeyHex); + encryptedMsg = this.encrypt(sharedKey, Buffer.from(plainTextMsg)); + } catch (error) { + console.log('ENCRYPTION ERROR', error); + throw error; + } + + return encryptedMsg; + }; + + decryptMessageOnchain(opReturnOutput, privateKeyWIF, publicKeyHex) { + let attachedMsg = null; + const opReturn = this.parseOpReturn(opReturnOutput); + switch (opReturn[0]) { + // unencrypted LotusChat + case currency.opReturn.appPrefixesHex.lotusChat: + attachedMsg = Buffer.from(opReturn[1], 'hex'); + break; + case currency.opReturn.appPrefixesHex.lotusChatEncrypted: + // attachedMsg = 'Not yet implemented chat encrypted'; + const sharedKey = this.createSharedKey(privateKeyWIF, publicKeyHex); + const decryptedMessage = this.decrypt( + sharedKey, + Uint8Array.from(Buffer.from(opReturn[1], 'hex')) + ); + attachedMsg = Buffer.from(decryptedMessage).toString('utf8'); + break; + default: + break; + } + return attachedMsg ? attachedMsg.toString() : null; + } + + decrypt = (sharedKey: Buffer, cipherText: Uint8Array) => { + // Split shared key + const iv = forge.util.createBuffer(sharedKey.slice(0, 16)); + const key = forge.util.createBuffer(sharedKey.slice(16)); + + // Encrypt entries + const cipher = forge.cipher.createDecipher('AES-CBC', key); + cipher.start({ iv }); + const rawBuffer = forge.util.createBuffer(cipherText); + cipher.update(rawBuffer); + cipher.finish(); + const plainText = Uint8Array.from( + Buffer.from(cipher.output.toHex(), 'hex') + ); + return plainText; + }; + + encrypt = (sharedKey: Buffer, plainText: Uint8Array) => { + // Split shared key + const iv = forge.util.createBuffer(sharedKey.slice(0, 16)); + const key = forge.util.createBuffer(sharedKey.slice(16)); + + // Encrypt entries + const cipher = forge.cipher.createCipher('AES-CBC', key); + cipher.start({ iv }); + const rawBuffer = forge.util.createBuffer(plainText); + cipher.update(rawBuffer); + cipher.finish(); + const cipherText = Uint8Array.from( + Buffer.from(cipher.output.toHex(), 'hex') + ); + + return cipherText; + }; + + createSharedKey = (privateKeyWIF: string, publicKeyHex: string): Buffer => { + const publicKeyObj = PublicKey.fromBuffer(Buffer.from(publicKeyHex, 'hex')); + const privateKeyObj = PrivateKey.fromWIF(privateKeyWIF); + + const mergedKey = this.constructMergedKey(privateKeyObj, publicKeyObj); + // const rawMergedKey = mergedKey.toBuffer(); // this function throws assertion error sometimes + const rawMergedKey = this.publicKeyToBuffer(mergedKey); + const sharedKey = this.crypto2.Hash.sha256(rawMergedKey); + return sharedKey; + }; + + constructMergedKey = (privateKey, publicKey) => { + return PublicKey.fromPoint(publicKey.point.mul(privateKey.toBigNumber())); + }; + + publicKeyToBuffer = pubKey => { + const { x, y, compressed } = pubKey.toObject(); + let xBuf = Buffer.from(x, 'hex'); + let yBuf = Buffer.from(y, 'hex'); + let prefix; + let buf; + if (!compressed) { + prefix = Buffer.from([0x04]); + buf = Buffer.concat([prefix, xBuf, yBuf]); + } else { + let odd = yBuf[yBuf.length - 1] % 2; + if (odd) { + prefix = Buffer.from([0x03]); + } else { + prefix = Buffer.from([0x02]); + } + buf = Buffer.concat([prefix, xBuf]); + } + + return buf; + }; +} diff --git a/src/app/providers/persistence/persistence.ts b/src/app/providers/persistence/persistence.ts index 1c9781cf020..ad801971280 100644 --- a/src/app/providers/persistence/persistence.ts +++ b/src/app/providers/persistence/persistence.ts @@ -71,6 +71,7 @@ const Keys = { PROFILE: 'profile', PROFILE_OLD: 'profileOld', REMOTE_PREF_STORED: 'remotePrefStored', + MIGRATE_MESSAGE_ONCHAIN: 'start', TX_CONFIRM_NOTIF: txid => 'txConfirmNotif-' + txid, TX_HISTORY: walletId => 'txsHistory-' + walletId, ORDER_WALLET: walletId => 'order-' + walletId, @@ -600,6 +601,14 @@ export class PersistenceProvider { return this.storage.get(Keys.TX_CONFIRM_NOTIF(txid)); } + setMigrateMessageOnchainProcess(val) { + return this.storage.set(Keys.MIGRATE_MESSAGE_ONCHAIN, val); + } + + getMigrateMessageOnchainProcess() { + return this.storage.get(Keys.MIGRATE_MESSAGE_ONCHAIN); + } + removeTxConfirmNotification(txid: string) { return this.storage.remove(Keys.TX_CONFIRM_NOTIF(txid)); } diff --git a/src/app/providers/wallet/wallet.ts b/src/app/providers/wallet/wallet.ts index 3ac0cff8b0e..ccc3ba6026f 100644 --- a/src/app/providers/wallet/wallet.ts +++ b/src/app/providers/wallet/wallet.ts @@ -18,6 +18,7 @@ import { LanguageProvider } from '../language/language'; import { Logger } from '../logger/logger'; import { LogsProvider } from '../logs/logs'; import { OnGoingProcessProvider } from '../on-going-process/on-going-process'; +import { OnchainMessageProvider } from '../onchain-message/onchain-message'; import { PersistenceProvider } from '../persistence/persistence'; import { PlatformProvider } from '../platform/platform'; import { PopupProvider } from '../popup/popup'; @@ -69,11 +70,12 @@ export interface TransactionProposal { data?: string; gasLimit?: number; }>; - isDonation?: boolean + isDonation?: boolean; receiveLotusAddress?: string; inputs: any; fee: any; message: string; + messageOnChain: string; customData?: { service?: string; giftCardName?: string; @@ -139,7 +141,8 @@ export class WalletProvider { private keyProvider: KeyProvider, private platformProvider: PlatformProvider, private logsProvider: LogsProvider, - private appProvider: AppProvider + private appProvider: AppProvider, + private onchainMessageService: OnchainMessageProvider ) { this.logger.debug('WalletProvider initialized'); this.isPopupOpen = false; @@ -404,10 +407,10 @@ export class WalletProvider { ) { this.logger.debug( 'Retrying update... ' + - walletId + - ' Try:' + - tries + - ' until:', + walletId + + ' Try:' + + tries + + ' until:', opts.until ); return setTimeout(() => { @@ -446,7 +449,7 @@ export class WalletProvider { if (WalletProvider.statusUpdateOnProgress[wallet.id] && !opts.until) { this.logger.info( '!! Status update already on progress for: ' + - wallet.credentials.walletName + wallet.credentials.walletName ); return reject('INPROGRESS'); } @@ -527,22 +530,26 @@ export class WalletProvider { this.calcTotalAmount(wallet, isoCode, lastDayRatesArray) ); if (wallet.tokens) { - totalAlternativeBalanceToken += _.sumBy(wallet.tokens, 'alternativeBalance') + totalAlternativeBalanceToken += _.sumBy( + wallet.tokens, + 'alternativeBalance' + ); } }); - const totalBalanceAlternative = (_.sumBy( - _.compact(totalAmountArray), - b => b.walletTotalBalanceAlternative - ) + totalAlternativeBalanceToken).toFixed(2); - - - const totalBalanceAlternativeLastDay = (_.sumBy( - _.compact(totalAmountArray), - b => b.walletTotalBalanceAlternativeLastDay - ) + totalAlternativeBalanceToken).toFixed(2); - + const totalBalanceAlternative = ( + _.sumBy( + _.compact(totalAmountArray), + b => b.walletTotalBalanceAlternative + ) + totalAlternativeBalanceToken + ).toFixed(2); + const totalBalanceAlternativeLastDay = ( + _.sumBy( + _.compact(totalAmountArray), + b => b.walletTotalBalanceAlternativeLastDay + ) + totalAlternativeBalanceToken + ).toFixed(2); const difference = parseFloat(totalBalanceAlternative.replace(/,/g, '')) - @@ -550,7 +557,7 @@ export class WalletProvider { const totalBalanceChange = (difference * 100) / - (parseFloat(totalBalanceAlternative.replace(/,/g, ''))); + parseFloat(totalBalanceAlternative.replace(/,/g, '')); return { totalBalanceAlternativeIsoCode: isoCode, @@ -576,13 +583,24 @@ export class WalletProvider { }); } - public getAddressView(coin: Coin, network: string, address: string, isEtoken?: boolean): string { + public getAddressView( + coin: Coin, + network: string, + address: string, + isEtoken?: boolean + ): string { if (coin != 'bch' && coin != 'xec') return address; let protoAddr = this.getProtoAddress(coin, network, address); if (isEtoken && coin == 'xec') { try { - const { prefix, type, hash } = this.addressProvider.decodeAddress(protoAddr); - const etokenAddress = this.addressProvider.encodeAddress('etoken', type, hash, protoAddr); + const { prefix, type, hash } = + this.addressProvider.decodeAddress(protoAddr); + const etokenAddress = this.addressProvider.encodeAddress( + 'etoken', + type, + hash, + protoAddr + ); if (etokenAddress) protoAddr = etokenAddress; } catch (error) { protoAddr = 'false'; @@ -785,7 +803,7 @@ export class WalletProvider { const LIMIT = 100; let requestLimit = FIRST_LIMIT; const walletId = wallet.credentials.walletId; - WalletProvider.progressFn[walletId] = progressFn || (() => { }); + WalletProvider.progressFn[walletId] = progressFn || (() => {}); let foundLimitTx: any = []; const fixTxsUnit = (txs): void => { @@ -867,11 +885,11 @@ export class WalletProvider { skip = skip + requestLimit; this.logger.debug( 'Syncing TXs for:' + - walletId + - '. Got:' + - newTxs.length + - ' Skip:' + - skip, + walletId + + '. Got:' + + newTxs.length + + ' Skip:' + + skip, ' EndingTxid:', endingTxid, ' Continue:', @@ -893,7 +911,7 @@ export class WalletProvider { if (!shouldContinue) { this.logger.debug( 'Finished Sync: New / soft confirmed Txs: ' + - newTxs.length + newTxs.length ); return resolve(newTxs); } @@ -974,7 +992,7 @@ export class WalletProvider { } updateNotes() - .then(() => { + .then(async () => { // if (!_.isEmpty(foundLimitTx)) { this.logger.debug( @@ -983,7 +1001,50 @@ export class WalletProvider { return resolve(newHistory); } // - + // encrypt message right here + if(wallet.coin === 'xpi' && wallet.cachedStatus.wallet.singleAddress){ + const result = + await this.getMnemonicAndPassword(wallet); + for (let index = 0; index < newHistory.length; index++) { + const tx = newHistory[index] as any; + if ( + tx.outputScript && + tx.outputScript.length > 0 && + !(tx.messageOnchain && tx.messageOnchain.length > 0) + ) { + const outputFound = _.find( + tx.outputs, + o => + o.outputScript && + o.outputScript.length > 0 && + o.outputScript.includes('030303') + ); + let addressRecepient = ''; + if (tx.action === 'received') { + addressRecepient = tx.inputAddresses[0]; + } else { + addressRecepient = _.find( + tx.outputs, + o => !o.outputScript + ).address; + } + if (outputFound) { + tx.messageOnchain = + await this.onchainMessageService.processDecryptMessageOnchain( + outputFound.outputScript, + wallet, + result.mnemonic, + addressRecepient + ); + } + } + // newHistory[index].outputs = tx.outputs.filter( + // output => output.address !== 'false' + // ); + } + } + + const historyToSave = JSON.stringify(newHistory); _.each(txs, tx => { tx.recent = true; @@ -998,9 +1059,9 @@ export class WalletProvider { .then(() => { this.logger.debug( 'History sync & saved for ' + - wallet.id + - ' Txs: ' + - newHistory.length + wallet.id + + ' Txs: ' + + newHistory.length ); return resolve(undefined); @@ -1047,7 +1108,8 @@ export class WalletProvider { return input.mintHeight < 0; }); } - const isTxCoinbase = tx.coinbase || tx.action == 'immature' || tx.action == 'mined'; + const isTxCoinbase = + tx.coinbase || tx.action == 'immature' || tx.action == 'mined'; if (isTxCoinbase && tx.confirmations >= this.SAFE_CONFIRMATIONS_MINED) { tx.safeConfirmed = this.SAFE_CONFIRMATIONS_MINED + '+'; } else if (!isTxCoinbase && tx.confirmations >= this.SAFE_CONFIRMATIONS) { @@ -1062,6 +1124,18 @@ export class WalletProvider { delete tx.note.encryptedBody; } + const outputFound = _.find( + tx.outputs, + o => + o.outputScript && + o.outputScript.length > 0 && + o.outputScript.includes('030303') + ); + + if (outputFound) { + tx.outputScript = outputFound.outputScript; + } + if (!txHistoryUnique[tx.txid]) { ret.push(tx); txHistoryUnique[tx.txid] = true; @@ -1140,7 +1214,8 @@ export class WalletProvider { const outputSize = 34; nbInputs = nbInputs ? nbInputs : 1; // Assume 1 input const outputReturn = !isAllFund ? 16 : 0; - const size = overhead + inputSize * nbInputs + outputSize * nbOutputs + outputReturn; + const size = + overhead + inputSize * nbInputs + outputSize * nbOutputs + outputReturn; return parseInt((size * (1 + safetyMargin)).toFixed(0), 10); } @@ -1322,7 +1397,9 @@ export class WalletProvider { password ); } catch (err) { - const title = this.translate.instant('Your account is in a corrupt state. Please contact support and share the logs provided.'); + const title = this.translate.instant( + 'Your account is in a corrupt state. Please contact support and share the logs provided.' + ); let message; try { message = err instanceof Error ? err.toString() : JSON.stringify(err); @@ -1734,8 +1811,8 @@ export class WalletProvider { err && err.message ? err.message : this.translate.instant( - 'The payment was created but could not be completed. Please try again from home screen' - ); + 'The payment was created but could not be completed. Please try again from home screen' + ); this.logger.error('Sign error: ' + msg); this.events.publish('Local/TxAction', { walletId: wallet.id, @@ -1822,16 +1899,16 @@ export class WalletProvider { return resolve( info.type + - '|' + - info.data + - '|' + - wallet.credentials.network.toLowerCase() + - '|' + - derivationPath + - '|' + - mnemonicHasPassphrase + - '|' + - wallet.coin + '|' + + info.data + + '|' + + wallet.credentials.network.toLowerCase() + + '|' + + derivationPath + + '|' + + mnemonicHasPassphrase + + '|' + + wallet.coin ); }); } @@ -1967,10 +2044,14 @@ export class WalletProvider { } formatAmout(amount: number, coin: string) { - if (_.isEmpty(coin)) coin = 'xpi' // lotus - const precision = _.get(this.currencyProvider.getPrecision(coin as Coin), 'unitToSatoshi', 0); + if (_.isEmpty(coin)) coin = 'xpi'; // lotus + const precision = _.get( + this.currencyProvider.getPrecision(coin as Coin), + 'unitToSatoshi', + 0 + ); if (precision == 0) return 0; - return (amount / precision) + return amount / precision; } getDonationInfo() { @@ -1980,14 +2061,23 @@ export class WalletProvider { if (errLivenet) { return reject(this.translate.instant('Could not get dynamic fee')); } - donation.donationSupportCoins = _.map(donation.donationToAddresses, item => { - return { - coin: item.coin, - network: item.network || 'livenet' + donation.donationSupportCoins = _.map( + donation.donationToAddresses, + item => { + return { + coin: item.coin, + network: item.network || 'livenet' + }; } - }); - donation.remaining = this.formatAmout(donation.remaining, donation.donationCoin); - donation.receiveAmountLotus = this.formatAmout(donation.receiveAmountLotus, donation.donationCoin); + ); + donation.remaining = this.formatAmout( + donation.remaining, + donation.donationCoin + ); + donation.receiveAmountLotus = this.formatAmout( + donation.receiveAmountLotus, + donation.donationCoin + ); return resolve(donation); }); }); diff --git a/src/assets/i18n/en.po b/src/assets/i18n/en.po index 8195ad01f41..1adc0b6c7ca 100755 --- a/src/assets/i18n/en.po +++ b/src/assets/i18n/en.po @@ -24,6 +24,12 @@ msgstr "Password unmatched!" msgid "Important" msgstr "Important" +msgid "Private message" +msgstr "Private message" + +msgid "Type your private message" +msgstr "Type your private message" + msgid "Account Addresses" msgstr "Account Addresses" @@ -5372,6 +5378,12 @@ msgstr "The account you are using does not match the network and/or the currency msgid "{{newWalletsCount}} wallets found" msgstr "{{newWalletsCount}} wallets found" +msgid "Replying to " +msgstr "Replying to " + +msgid "Reply" +msgstr "Reply" + msgid "" "{{params.countryCode === 'US' ? 'U.S.' : params.countryCode}} Phone Required" msgstr "" diff --git a/src/assets/i18n/vi.po b/src/assets/i18n/vi.po index 874dd3344fd..f7c709f9796 100644 --- a/src/assets/i18n/vi.po +++ b/src/assets/i18n/vi.po @@ -24,6 +24,12 @@ msgstr "Mật khẩu không khớp!" msgid "Important" msgstr "Quan Trọng" +msgid "Private message" +msgstr "Tin nhắn riêng tư" + +msgid "Type your private message" +msgstr "Nhập tin nhắn riêng tư của bạn" + msgid "{appName} cannot recovery your fund for you if you lose your recovery key. The loss of it will lead to permanent asset loss." msgstr "{appName} không thể khôi phục số dư nếu bạn mất cụm từ khóa (12 từ). Đồng nghĩa với việc tài sản của bạn sẽ bị mất vĩnh viễn." @@ -2767,6 +2773,12 @@ msgstr "Không phải bây giờ" msgid "Not set" msgstr "Không được thiết lập" +msgid "Replying to " +msgstr "Trả lời cho " + +msgid "Reply" +msgstr "Trả lời" + msgid "Not valid bitcoin address" msgstr "Địa chỉ bitcoin không hợp lệ" diff --git a/src/assets/img/ico-reply-dark.svg b/src/assets/img/ico-reply-dark.svg new file mode 100644 index 00000000000..cba36e842b3 --- /dev/null +++ b/src/assets/img/ico-reply-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/img/ico-reply-light.svg b/src/assets/img/ico-reply-light.svg new file mode 100644 index 00000000000..8bc472da296 --- /dev/null +++ b/src/assets/img/ico-reply-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/index.html b/src/index.html index 57faf6e32bb..d04787da541 100644 --- a/src/index.html +++ b/src/index.html @@ -16,7 +16,7 @@ - + diff --git a/src/theme/dark.scss b/src/theme/dark.scss index 3f95fbc0ff1..c48ffb5dcdf 100644 --- a/src/theme/dark.scss +++ b/src/theme/dark.scss @@ -1800,6 +1800,9 @@ .note-container { color: rgba(255, 184, 93, 1) !important; } + .onchain-message-txt { + color: #fff !important; + } } .send-to-content { .item-name { @@ -2440,7 +2443,7 @@ --highlight-color-focused: rgba(224, 228, 230, 0.6); --highlight-color-valid: rgba(224, 228, 230, 0.6); --highlight-color-invalid: rgba(224, 228, 230, 0.6); - ion-input { + ion-input, ion-textarea { --placeholder-color: rgba(237, 239, 240, 0.38); --color: #edeff0; } @@ -2456,6 +2459,12 @@ .item-title { color: rgba(237, 239, 240, 0.6); } + .sign-require { + color: #FFB4A9 !important; + } + .max-message-txt { + color: #FFB4A9 !important; + } } create-new-wallet { @@ -3777,4 +3786,20 @@ background: var(--bg-color-dark-theme-8) !important; } } + + message-reply-component { + .reply-to-container { + background: linear-gradient(0deg, rgba(126, 208, 255, 0.14), rgba(126, 208, 255, 0.14)), #001E2E; + border: 1px solid rgba(0, 101, 141, 0.08); + .reply-to-title { + color: #EDEFF0; + } + .reply-to-message { + color: #E0E4E6; + } + } + .max-message-txt { + color: #FFB4A9 !important; + } + } }