diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts index 34ba3b998..e7ac03e82 100644 --- a/src/app/backend-api.service.ts +++ b/src/app/backend-api.service.ts @@ -30,7 +30,9 @@ export class BackendRoutes { static RoutePathSubmitPost = '/api/v0/submit-post'; static RoutePathUploadImage = '/api/v0/upload-image'; static RoutePathSubmitTransaction = '/api/v0/submit-transaction'; + static RoutePathSubmitAtomicTransaction = '/api/v0/submit-atomic-transaction'; static RoutePathUpdateProfile = '/api/v0/update-profile'; + static RoutePathSubsidizedUpdateProfile = '/api/v0/subsidized-update-profile'; static RoutePathGetPostsStateless = '/api/v0/get-posts-stateless'; static RoutePathGetHotFeed = '/api/v0/get-hot-feed'; static RoutePathGetProfiles = '/api/v0/get-profiles'; @@ -747,6 +749,58 @@ export class BackendApiService { this.identityService.identityServicePublicKeyAdded = publicKeyAdded; } + signAndSubmitSubsidizedUpdateProfileTransaction( + endpoint: string, + request: Observable, + PublicKeyBase58Check: string + ): Observable { + let incompleteAtomicTransactionHex: string = ''; + return request + .pipe( + // Collect the signature for the unsigned update profile transaction. + switchMap((res) => { + // Capture the incomplete atomic transaction. + incompleteAtomicTransactionHex = res.IncompleteAtomicTransactionHex; + + // Continue processing without the incomplete atomic transaction. + // Hit the identity service to sign the update profile transaction. + return this.identityService + .sign({ + transactionHex: res.UpdateProfileTransactionHex, + ...this.identityService.identityServiceParamsForKey( + PublicKeyBase58Check + ), + }) + .pipe( + switchMap((signed) => { + return this.identityService + .launch('/approve', { + tx: res.UpdateProfileTransactionHex, + }) + .pipe( + map((approved) => { + this.setIdentityServiceUsers(approved.users); + return { ...res, ...approved }; + }) + ); + }) + ); + }) + ) + .pipe( + // Construct the atomic transaction and submit. + // Submit the incomplete atomic transaction along with the new transaction. + switchMap((res) => + this.SubmitAtomicTransaction( + endpoint, + incompleteAtomicTransactionHex, + [res.signedTransactionHex] + ).pipe(map((broadcasted) => ({ ...res, ...broadcasted }))) + ) + ) + .pipe(catchError(this._handleError)); + } + signAndSubmitTransaction( endpoint: string, request: Observable, @@ -765,16 +819,16 @@ export class BackendApiService { .pipe( switchMap((signed) => { //if (signed.approvalRequired) { - return this.identityService - .launch('/approve', { - tx: res.TransactionHex, + return this.identityService + .launch('/approve', { + tx: res.TransactionHex, + }) + .pipe( + map((approved) => { + this.setIdentityServiceUsers(approved.users); + return { ...res, ...approved }; }) - .pipe( - map((approved) => { - this.setIdentityServiceUsers(approved.users); - return { ...res, ...approved }; - }) - ); + ); //} else { // return of({ ...res, ...signed }); //} @@ -1004,6 +1058,17 @@ export class BackendApiService { }); } + SubmitAtomicTransaction( + endpoint: string, + IncompleteAtomicTransactionHex: string, + SignedInnerTransactionsHex: string[] + ): Observable { + return this.post(endpoint, BackendRoutes.RoutePathSubmitAtomicTransaction, { + IncompleteAtomicTransactionHex, + SignedInnerTransactionsHex, + }); + } + SendMessage( endpoint: string, SenderPublicKeyBase58Check: string, @@ -1967,6 +2032,51 @@ export class BackendApiService { }); } + SubsidizedUpdateProfile( + endpoint: string, + // Specific fields + UpdaterPublicKeyBase58Check: string, + // Optional: Only needed when updater public key != profile public key + ProfilePublicKeyBase58Check: string, + NewUsername: string, + NewDescription: string, + NewProfilePic: string, + NewCreatorBasisPoints: number, + NewStakeMultipleBasisPoints: number, + IsHidden: boolean, + // End specific fields + MinFeeRateNanosPerKB: number + ): Observable { + NewCreatorBasisPoints = Math.floor(NewCreatorBasisPoints); + NewStakeMultipleBasisPoints = Math.floor(NewStakeMultipleBasisPoints); + + const request = this.post( + endpoint, + BackendRoutes.RoutePathSubsidizedUpdateProfile, + { + UpdaterPublicKeyBase58Check, + ProfilePublicKeyBase58Check, + NewUsername, + NewDescription, + NewProfilePic, + NewCreatorBasisPoints, + NewStakeMultipleBasisPoints, + IsHidden, + MinFeeRateNanosPerKB, + } + ); + + // NOTE: Unlike the traditional UpdateProfile endpoint, there's + // no need to wait for a subsidization transaction to be broadcast + // across the network as the subsidization transaction is bundled + // together in the returned atomic transaction. + return this.signAndSubmitSubsidizedUpdateProfileTransaction( + endpoint, + request, + UpdaterPublicKeyBase58Check + ); + } + UpdateProfile( endpoint: string, // Specific fields @@ -2950,7 +3060,7 @@ export class BackendApiService { Username: string, IsBlacklistUpdate: boolean, - AddUserToList: boolean, + AddUserToList: boolean ): Observable { return this.jwtPost( endpoint, @@ -3895,12 +4005,24 @@ export class BackendApiService { return this.get(endpoint, BackendRoutes.RoutePathGetCountKeysWithDESO); } - GetLockupYieldCurvePoints(endpoint: string, publicKey: string): Observable { - return this.get(endpoint, BackendRoutes.RoutePathLockupYieldCurvePoints + "/" + publicKey); + GetLockupYieldCurvePoints( + endpoint: string, + publicKey: string + ): Observable { + return this.get( + endpoint, + BackendRoutes.RoutePathLockupYieldCurvePoints + '/' + publicKey + ); } - GetLockedBalanceEntries(endpoint: string, publicKey: string): Observable { - return this.get(endpoint, BackendRoutes.RoutePathLockedBalanceEntries + "/" + publicKey); + GetLockedBalanceEntries( + endpoint: string, + publicKey: string + ): Observable { + return this.get( + endpoint, + BackendRoutes.RoutePathLockedBalanceEntries + '/' + publicKey + ); } CoinLockup( @@ -3922,14 +4044,14 @@ export class BackendApiService { VestingEndTimestampNanoSecs, LockupAmountBaseUnits, ExtraData, - MinFeeRateNanosPerKB, - }); + MinFeeRateNanosPerKB, + }); return this.signAndSubmitTransaction( endpoint, request, - TransactorPublicKeyBase58Check, - ) - }; + TransactorPublicKeyBase58Check + ); + } UpdateCoinLockupParams( endpoint: string, @@ -3942,22 +4064,26 @@ export class BackendApiService { ExtraData: { [k: string]: string }, MinFeeRateNanosPerKB: number ): Observable { - const request = this.post(endpoint, BackendRoutes.RoutePathUpdateCoinLockupParams, { - TransactorPublicKeyBase58Check, - LockupYieldDurationNanoSecs, - LockupYieldAPYBasisPoints, - RemoveYieldCurvePoint, - NewLockupTransferRestrictions, - LockupTransferRestrictionStatus, - ExtraData, - MinFeeRateNanosPerKB, - }); + const request = this.post( + endpoint, + BackendRoutes.RoutePathUpdateCoinLockupParams, + { + TransactorPublicKeyBase58Check, + LockupYieldDurationNanoSecs, + LockupYieldAPYBasisPoints, + RemoveYieldCurvePoint, + NewLockupTransferRestrictions, + LockupTransferRestrictionStatus, + ExtraData, + MinFeeRateNanosPerKB, + } + ); return this.signAndSubmitTransaction( endpoint, request, - TransactorPublicKeyBase58Check, - ) - }; + TransactorPublicKeyBase58Check + ); + } CoinLockupTransfer( endpoint: string, @@ -3969,21 +4095,25 @@ export class BackendApiService { ExtraData: { [k: string]: string }, MinFeeRateNanosPerKB: number ): Observable { - const request = this.post(endpoint, BackendRoutes.RoutePathCoinLockupTransfer, { - TransactorPublicKeyBase58Check, - ProfilePublicKeyBase58Check, - RecipientPublicKeyBase58Check, - UnlockTimestampNanoSecs, - LockedCoinsToTransferBaseUnits, - ExtraData, - MinFeeRateNanosPerKB, - }); + const request = this.post( + endpoint, + BackendRoutes.RoutePathCoinLockupTransfer, + { + TransactorPublicKeyBase58Check, + ProfilePublicKeyBase58Check, + RecipientPublicKeyBase58Check, + UnlockTimestampNanoSecs, + LockedCoinsToTransferBaseUnits, + ExtraData, + MinFeeRateNanosPerKB, + } + ); return this.signAndSubmitTransaction( endpoint, request, - TransactorPublicKeyBase58Check, - ) - }; + TransactorPublicKeyBase58Check + ); + } CoinUnlock( endpoint: string, @@ -4001,9 +4131,9 @@ export class BackendApiService { return this.signAndSubmitTransaction( endpoint, request, - TransactorPublicKeyBase58Check, - ) - }; + TransactorPublicKeyBase58Check + ); + } // Error parsing stringifyError(err): string { diff --git a/src/app/update-profile-page/update-profile/update-profile.component.html b/src/app/update-profile-page/update-profile/update-profile.component.html index f2ea5f567..04056b101 100644 --- a/src/app/update-profile-page/update-profile/update-profile.component.html +++ b/src/app/update-profile-page/update-profile/update-profile.component.html @@ -199,7 +199,7 @@
Everyone needs a profile. Let's update yours!
diff --git a/src/app/update-profile-page/update-profile/update-profile.component.ts b/src/app/update-profile-page/update-profile/update-profile.component.ts index 1ce1a41e5..f099e6179 100644 --- a/src/app/update-profile-page/update-profile/update-profile.component.ts +++ b/src/app/update-profile-page/update-profile/update-profile.component.ts @@ -203,6 +203,24 @@ export class UpdateProfileComponent implements OnInit, OnChanges { ); } + _callBackendSubsidizedUpdateProfile() { + return this.backendApi.SubsidizedUpdateProfile( + environment.verificationEndpointHostname, + this.globalVars.loggedInUser + .PublicKeyBase58Check /*UpdaterPublicKeyBase58Check*/, + '' /*ProfilePublicKeyBase58Check*/, + // Start params + this.profileUpdates.usernameUpdate /*NewUsername*/, + this.profileUpdates.descriptionUpdate /*NewDescription*/, + this.profileUpdates.profilePicUpdate /*NewProfilePic*/, + this.founderRewardInput * 100 /*NewCreatorBasisPoints*/, + 1.25 * 100 * 100 /*NewStakeMultipleBasisPoints*/, + false /*IsHidden*/, + // End params + this.globalVars.feeRateDeSoPerKB * 1e9 /*MinFeeRateNanosPerKB*/ + ); + } + _updateProfile() { // Trim the username input in case the user added a space at the end. Some mobile // browsers may do this. @@ -264,6 +282,67 @@ export class UpdateProfileComponent implements OnInit, OnChanges { ); } + _subsidizedUpdateProfile() { + // Trim the username input in case the user added a space at the end. Some mobile + // browsers may do this. + this.usernameInput = this.usernameInput.trim(); + + const hasErrors = this._setProfileErrors(); + if (hasErrors) { + this.globalVars.logEvent( + 'profile : update : has-errors', + this.profileUpdateErrors + ); + return; + } + + this.updateProfileBeingCalled = true; + this._setProfileUpdates(); + this._callBackendSubsidizedUpdateProfile().subscribe( + (res) => { + this.globalVars.profileUpdateTimestamp = Date.now(); + this.globalVars.logEvent('profile : update'); + // This updates things like the username that shows up in the dropdown. + this.globalVars.updateEverything( + res.TxnHashHex, + this._updateProfileSuccess, + this._updateProfileFailure, + this + ); + }, + (err) => { + const parsedError = this.backendApi.parseProfileError(err); + const lowBalance = parsedError.indexOf('insufficient'); + this.globalVars.logEvent('profile : update : error', { + parsedError, + lowBalance, + }); + this.updateProfileBeingCalled = false; + SwalHelper.fire({ + target: this.globalVars.getTargetComponentSelector(), + icon: 'error', + title: `An Error Occurred`, + html: parsedError, + showConfirmButton: true, + focusConfirm: true, + customClass: { + confirmButton: 'btn btn-light', + cancelButton: 'btn btn-light no', + }, + confirmButtonText: lowBalance ? 'Buy $DESO' : null, + cancelButtonText: lowBalance ? 'Later' : null, + showCancelButton: !!lowBalance, + }).then((res) => { + if (lowBalance && res.isConfirmed) { + this.router.navigate([RouteNames.BUY_DESO], { + queryParamsHandling: 'merge', + }); + } + }); + } + ); + } + _updateProfileSuccess(comp: UpdateProfileComponent) { comp.globalVars.celebrate(); comp.updateProfileBeingCalled = false;