From 0b1b00622476239c335d632d1cc6152201d57d22 Mon Sep 17 00:00:00 2001 From: Jon Pollock Date: Wed, 10 Apr 2024 15:23:49 -0700 Subject: [PATCH 1/5] Initial frontend implementation for subsidized update profile. --- src/app/backend-api.service.ts | 225 ++++++++++++++---- .../update-profile.component.html | 2 +- .../update-profile.component.ts | 76 ++++++ 3 files changed, 255 insertions(+), 48 deletions(-) diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts index 34ba3b998..15a4521b7 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 incompleteAtomicTransaction: any | null = null; + return request + .pipe( + // Collect the signature for the unsigned update profile transaction. + switchMap((res) => { + // Capture the incomplete atomic transaction. + incompleteAtomicTransaction = res.IncompleteAtomicTransaction; + + // 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.TransactionHex, + }) + .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, + incompleteAtomicTransaction, + [res.signedTransactionsHex] + ).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, + IncompleteAtomicTransaction: any, + SignedInnerTransactionsHex: string[] + ): Observable { + return this.post(endpoint, BackendRoutes.RoutePathSubmitAtomicTransaction, { + IncompleteAtomicTransaction, + SignedInnerTransactionsHex, + }); + } + SendMessage( endpoint: string, SenderPublicKeyBase58Check: string, @@ -1967,6 +2032,52 @@ 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 +3061,7 @@ export class BackendApiService { Username: string, IsBlacklistUpdate: boolean, - AddUserToList: boolean, + AddUserToList: boolean ): Observable { return this.jwtPost( endpoint, @@ -3895,12 +4006,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 +4045,14 @@ export class BackendApiService { VestingEndTimestampNanoSecs, LockupAmountBaseUnits, ExtraData, - MinFeeRateNanosPerKB, - }); + MinFeeRateNanosPerKB, + }); return this.signAndSubmitTransaction( endpoint, request, - TransactorPublicKeyBase58Check, - ) - }; + TransactorPublicKeyBase58Check + ); + } UpdateCoinLockupParams( endpoint: string, @@ -3942,22 +4065,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 +4096,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 +4132,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..1aa498464 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,64 @@ 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'); + + // Log the resulting transaction. + debugger; + console.log(res); + }, + (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; From 88b9dcfa526e05772ccd0103d8f1bf2bc06c2811 Mon Sep 17 00:00:00 2001 From: Jon Pollock Date: Wed, 10 Apr 2024 20:10:47 -0700 Subject: [PATCH 2/5] Remove extra debugger. --- .../update-profile/update-profile.component.ts | 4 ---- 1 file changed, 4 deletions(-) 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 1aa498464..daf2c0e4c 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 @@ -302,10 +302,6 @@ export class UpdateProfileComponent implements OnInit, OnChanges { (res) => { this.globalVars.profileUpdateTimestamp = Date.now(); this.globalVars.logEvent('profile : update'); - - // Log the resulting transaction. - debugger; - console.log(res); }, (err) => { const parsedError = this.backendApi.parseProfileError(err); From 6ad7cea35e0e57a66f7dd8b21b91fdbbf45a1030 Mon Sep 17 00:00:00 2001 From: Jon Pollock Date: Wed, 10 Apr 2024 20:38:45 -0700 Subject: [PATCH 3/5] Fix isses w/ identity. --- src/app/backend-api.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts index 15a4521b7..6ea25be66 100644 --- a/src/app/backend-api.service.ts +++ b/src/app/backend-api.service.ts @@ -775,7 +775,7 @@ export class BackendApiService { switchMap((signed) => { return this.identityService .launch('/approve', { - tx: res.TransactionHex, + tx: res.UpdateProfileTransactionHex, }) .pipe( map((approved) => { @@ -794,7 +794,7 @@ export class BackendApiService { this.SubmitAtomicTransaction( endpoint, incompleteAtomicTransaction, - [res.signedTransactionsHex] + [res.signedTransactionHex] ).pipe(map((broadcasted) => ({ ...res, ...broadcasted }))) ) ) @@ -1063,6 +1063,7 @@ export class BackendApiService { IncompleteAtomicTransaction: any, SignedInnerTransactionsHex: string[] ): Observable { + debugger; return this.post(endpoint, BackendRoutes.RoutePathSubmitAtomicTransaction, { IncompleteAtomicTransaction, SignedInnerTransactionsHex, @@ -2070,7 +2071,6 @@ export class BackendApiService { // 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, From 1cc3754b3041596b16fc3fde3b151d76df4effe8 Mon Sep 17 00:00:00 2001 From: Jon Pollock Date: Fri, 12 Apr 2024 16:29:54 -0700 Subject: [PATCH 4/5] Support hex strings for subsidized update profiles. --- src/app/backend-api.service.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts index 6ea25be66..e7ac03e82 100644 --- a/src/app/backend-api.service.ts +++ b/src/app/backend-api.service.ts @@ -754,13 +754,13 @@ export class BackendApiService { request: Observable, PublicKeyBase58Check: string ): Observable { - let incompleteAtomicTransaction: any | null = null; + let incompleteAtomicTransactionHex: string = ''; return request .pipe( // Collect the signature for the unsigned update profile transaction. switchMap((res) => { // Capture the incomplete atomic transaction. - incompleteAtomicTransaction = res.IncompleteAtomicTransaction; + incompleteAtomicTransactionHex = res.IncompleteAtomicTransactionHex; // Continue processing without the incomplete atomic transaction. // Hit the identity service to sign the update profile transaction. @@ -793,7 +793,7 @@ export class BackendApiService { switchMap((res) => this.SubmitAtomicTransaction( endpoint, - incompleteAtomicTransaction, + incompleteAtomicTransactionHex, [res.signedTransactionHex] ).pipe(map((broadcasted) => ({ ...res, ...broadcasted }))) ) @@ -1060,12 +1060,11 @@ export class BackendApiService { SubmitAtomicTransaction( endpoint: string, - IncompleteAtomicTransaction: any, + IncompleteAtomicTransactionHex: string, SignedInnerTransactionsHex: string[] ): Observable { - debugger; return this.post(endpoint, BackendRoutes.RoutePathSubmitAtomicTransaction, { - IncompleteAtomicTransaction, + IncompleteAtomicTransactionHex, SignedInnerTransactionsHex, }); } From 909f0af47b026bbb8379d809d277be051f466292 Mon Sep 17 00:00:00 2001 From: Jon Pollock Date: Fri, 12 Apr 2024 16:36:27 -0700 Subject: [PATCH 5/5] Add updateEverything to subsidized update profiles. --- .../update-profile/update-profile.component.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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 daf2c0e4c..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 @@ -302,6 +302,13 @@ export class UpdateProfileComponent implements OnInit, OnChanges { (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);