From 40d96e61d9f8fe753274cdfe485a9d73b13a39ec Mon Sep 17 00:00:00 2001 From: piratekev <93568025+piratekev@users.noreply.github.com> Date: Wed, 19 Jan 2022 11:29:17 -0800 Subject: [PATCH] V2.0.1upgrade (#8) * add notification transformation for NFT Transfers, fix console error when there are no fees in TransactionFeeMap (#526) * Fix calculation of ETH exchange rate including fee (#530) * add supply-stats route to show total supply and rich list (#531) * Fix preview and home screen icons for DeSo (#532) * Add support for managing sign-up bonus configurations (#529) * save current progress on updating admin panel * add support for modifying the sign up bonus config for a single country, update to use default jumio USD cents instead of DeSo nanos * refresh country bonuses after updating default jumio USD cents * add tooltip disclaimer about free DESO amount * update copy * simplify loops in GetMessages that handles encryption/decryption (#533) * simplify loops in GetMessages that handles encryption/decryption * message -> Message * Fix admin jumio checkboxes (#534) * use country sign up bonus config inferred from IP address when computing referral amount to display for sign up bonus (#535) * use flatMap to flatten array of message before decryption (#536) * Upload referral csv directly instead of parsing on the frontend (#537) * top diamonded list fix (#480) * use altumbase for daily gainers leaderboard (#538) * use altumbase for daily gainers leaderboard * fix import styling * [stable] Release 1.2.9 * add disclaimer on referrals page about amounts varying by locality of ID AND add support for setting default kickback amount (#539) * add disclaimer on referrals page about amounts varying by locality of ID * add support for updating the default kickback amount for referrers * add referral code, jumio starter DESO txn Hash, and referrer DeSo Txn hash to User Admin Data (#540) * make referral link relative to window origin (#541) * Ln/count keys with deso (#542) * save current progress * update some styling on the supply monitoring page * update supply stats page to show count of keys holding DESO * Update src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.html * fix admin panel jumio kickback usd cents (#544) * add support for NFT transfers, burns, and acceptance of transfers (#545) * add support for NFT transfers, burns, and acceptance of transfers * fix styling * change font color to gray for pending ownership * add fas class to fix issue with icons not appearing in select serial number component (#547) * Buy Now NFTs and NFT Splits (#546) * [stable] Release 2.0.0 (#550) * [stable] Release 2.0.1 Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> Co-authored-by: NikolaiL Co-authored-by: maebeam Co-authored-by: diamondhands0 <81935176+diamondhands0@users.noreply.github.com> Co-authored-by: diamondhands --- Caddyfile | 3 +- ...-edit-country-sign-up-bonus.component.html | 60 ++++ ...io-edit-country-sign-up-bonus.component.ts | 98 ++++++ .../admin-jumio/admin-jumio.component.html | 172 +++++++--- .../admin-jumio/admin-jumio.component.ts | 129 +++++++- src/app/admin/admin.component.html | 12 + src/app/app-routing.module.ts | 9 + src/app/app.component.ts | 9 +- src/app/app.module.ts | 16 + src/app/backend-api.service.ts | 246 +++++++++++++-- .../buy-deso-eth/buy-deso-eth.component.ts | 2 +- .../close-nft-auction-modal.component.ts | 4 +- .../create-nft-auction-modal.component.html | 55 ++++ .../create-nft-auction-modal.component.ts | 34 +- .../creator-profile-nfts.component.html | 33 ++ .../creator-profile-nfts.component.ts | 57 +++- .../feed-post-dropdown.component.html | 21 +- .../feed-post-dropdown.component.ts | 69 +++- .../feed/feed-post/feed-post.component.html | 15 + src/app/feed/feed-post/feed-post.component.ts | 52 ++- .../free-deso-message.component.html | 12 + .../free-deso-message.component.ts | 12 + src/app/global-vars.service.ts | 27 +- .../jumio-status/jumio-status.component.html | 4 +- .../landing-page/landing-page.component.html | 6 +- .../mint-nft-modal.component.html | 296 ++++++++++++++++-- .../mint-nft-modal.component.ts | 187 ++++++++++- .../nft-burn-modal.component.html | 103 ++++++ .../nft-burn-modal.component.ts | 151 +++++++++ .../nft-post/nft-post.component.html | 6 +- .../nft-post/nft-post.component.ts | 4 + .../nft-select-serial-number.component.html | 100 ++++++ .../nft-select-serial-number.component.ts | 68 ++++ .../notifications-list.component.ts | 194 ++++++++++-- .../place-bid-modal.component.html | 48 +-- .../place-bid-modal.component.ts | 32 +- .../referral-program-mgr.component.ts | 18 +- src/app/referrals/referrals.component.html | 8 +- ...ight-bar-creators-leaderboard.component.ts | 4 - .../right-bar-creators.component.ts | 5 +- ...upply-monitoring-stats-page.component.html | 3 + ...upply-monitoring-stats-page.component.scss | 0 ...ly-monitoring-stats-page.component.spec.ts | 24 ++ .../supply-monitoring-stats-page.component.ts | 10 + .../supply-monitoring-stats.component.html | 30 ++ .../supply-monitoring-stats.component.scss | 0 .../supply-monitoring-stats.component.spec.ts | 24 ++ .../supply-monitoring-stats.component.ts | 89 ++++++ src/app/theme/themes/cake.scss | 3 +- src/app/theme/themes/dark.scss | 5 +- src/app/theme/themes/greenish.scss | 5 +- src/app/theme/themes/icydark.scss | 3 +- src/app/theme/themes/legends.scss | 3 +- src/app/theme/themes/light.scss | 3 +- .../trade-creator/trade-creator.component.ts | 3 +- .../transfer-nft-accept-modal.component.html | 103 ++++++ .../transfer-nft-accept-modal.component.ts | 119 +++++++ .../transfer-nft-modal.component.html | 108 +++++++ .../transfer-nft-modal.component.ts | 188 +++++++++++ .../trends-page/trends/trends.component.ts | 12 +- src/index.bitclout.html | 8 +- src/index.html | 18 +- .../altumbase-service.ts} | 90 +++--- src/styles.scss | 29 ++ 64 files changed, 2923 insertions(+), 338 deletions(-) create mode 100644 src/app/admin/admin-jumio/admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component.html create mode 100644 src/app/admin/admin-jumio/admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component.ts create mode 100644 src/app/free-deso-message/free-deso-message.component.html create mode 100644 src/app/free-deso-message/free-deso-message.component.ts create mode 100644 src/app/nft-burn-modal/nft-burn-modal.component.html create mode 100644 src/app/nft-burn-modal/nft-burn-modal.component.ts create mode 100644 src/app/nft-select-serial-number/nft-select-serial-number.component.html create mode 100644 src/app/nft-select-serial-number/nft-select-serial-number.component.ts create mode 100644 src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.html create mode 100644 src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.scss create mode 100644 src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.spec.ts create mode 100644 src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.ts create mode 100644 src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.html create mode 100644 src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.scss create mode 100644 src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.spec.ts create mode 100644 src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.ts create mode 100644 src/app/transfer-nft-accept-modal/transfer-nft-accept-modal.component.html create mode 100644 src/app/transfer-nft-accept-modal/transfer-nft-accept-modal.component.ts create mode 100644 src/app/transfer-nft-modal/transfer-nft-modal.component.html create mode 100644 src/app/transfer-nft-modal/transfer-nft-modal.component.ts rename src/lib/services/{pulse/pulse-service.ts => altumbase/altumbase-service.ts} (52%) diff --git a/Caddyfile b/Caddyfile index 0e94859e2..c89c3f0b8 100644 --- a/Caddyfile +++ b/Caddyfile @@ -32,11 +32,10 @@ header @html Content-Security-Policy " node.deso.org amp.deso.org bithunt.deso.org - pulse.deso.org bitclout.com:* api.bitclout.com bithunt.bitclout.com - pulse.bitclout.com + https://altumbase.com localhost:* explorer.bitclout.com https://api.blockchain.com/ticker diff --git a/src/app/admin/admin-jumio/admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component.html b/src/app/admin/admin-jumio/admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component.html new file mode 100644 index 000000000..be4324916 --- /dev/null +++ b/src/app/admin/admin-jumio/admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component.html @@ -0,0 +1,60 @@ +
+
+
Update Sign-Up Bonus for {{ getCountryName() }}
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+
+ diff --git a/src/app/admin/admin-jumio/admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component.ts b/src/app/admin/admin-jumio/admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component.ts new file mode 100644 index 000000000..482ee1612 --- /dev/null +++ b/src/app/admin/admin-jumio/admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component.ts @@ -0,0 +1,98 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { + BackendApiService, + CountryCodeDetails, + CountryLevelSignUpBonus, + CountryLevelSignUpBonusResponse, +} from "../../../backend-api.service"; +import { GlobalVarsService } from "../../../global-vars.service"; +import { BsModalRef } from "ngx-bootstrap/modal"; +import { BsModalService } from "ngx-bootstrap/modal"; +import { Router } from "@angular/router"; + +@Component({ + selector: "admin-jumio-edit-country-sign-up-bonus", + templateUrl: "./admin-jumio-edit-country-sign-up-bonus.component.html", +}) +export class AdminJumioEditCountrySignUpBonusComponent implements OnInit { + @Input() countryLevelSignUpBonusResponse: CountryLevelSignUpBonusResponse; + + newAllowCustomReferralAmount: boolean; + newAllowCustomKickbackAmount: boolean; + newReferralAmountOverrideUSD: number; + newKickbackAmountOverrideUSD: number; + updatingCountryLevelBonus: boolean = false; + + constructor( + private globalVars: GlobalVarsService, + private backendApi: BackendApiService, + private modalService: BsModalService, + private router: Router, + public bsModalRef: BsModalRef + ) {} + + ngOnInit(): void { + this.newAllowCustomKickbackAmount = this.getAllowCustomKickbackAmount(); + this.newAllowCustomReferralAmount = this.getAllowCustomReferralAmount(); + this.newReferralAmountOverrideUSD = this.getReferralAmountOverrideUSDCents() / 100; + this.newKickbackAmountOverrideUSD = this.getKickbackAmountOverrideUSDCents() / 100; + } + + getCountryCodeDetails(): CountryCodeDetails { + return this.countryLevelSignUpBonusResponse.CountryCodeDetails; + } + + getCountryLevelSignUpBonus(): CountryLevelSignUpBonus { + return this.countryLevelSignUpBonusResponse.CountryLevelSignUpBonus; + } + + getCountryName(): string { + return this.getCountryCodeDetails().Name; + } + + getCountryAlpha3(): string { + return this.getCountryCodeDetails().Alpha3; + } + + getAllowCustomReferralAmount(): boolean { + return this.getCountryLevelSignUpBonus().AllowCustomReferralAmount; + } + + getAllowCustomKickbackAmount(): boolean { + return this.getCountryLevelSignUpBonus().AllowCustomKickbackAmount; + } + + getReferralAmountOverrideUSDCents(): number { + return this.getCountryLevelSignUpBonus().ReferralAmountOverrideUSDCents; + } + + getKickbackAmountOverrideUSDCents(): number { + return this.getCountryLevelSignUpBonus().KickbackAmountOverrideUSDCents; + } + + updateSignUpBonus() { + this.updatingCountryLevelBonus = true; + this.backendApi + .AdminUpdateJumioCountrySignUpBonus( + this.globalVars.localNode, + this.globalVars.loggedInUser?.PublicKeyBase58Check, + this.getCountryAlpha3(), + { + AllowCustomKickbackAmount: this.newAllowCustomKickbackAmount, + AllowCustomReferralAmount: this.newAllowCustomReferralAmount, + ReferralAmountOverrideUSDCents: Math.trunc(this.newReferralAmountOverrideUSD * 100), + KickbackAmountOverrideUSDCents: Math.trunc(this.newKickbackAmountOverrideUSD * 100), + } + ) + .subscribe( + () => { + this.modalService.setDismissReason("sign-up-bonus-updated"); + this.bsModalRef.hide(); + }, + (err) => { + console.error(err); + } + ) + .add(() => (this.updatingCountryLevelBonus = false)); + } +} diff --git a/src/app/admin/admin-jumio/admin-jumio.component.html b/src/app/admin/admin-jumio/admin-jumio.component.html index da5a2c890..4bdd1ea1b 100644 --- a/src/app/admin/admin-jumio/admin-jumio.component.html +++ b/src/app/admin/admin-jumio/admin-jumio.component.html @@ -1,54 +1,132 @@
-
- Set Jumio Starter $DESO amount (in nanos): -
- - - + +
+
+ Set Jumio Starter Amount in $USD: +
+ + + +
-
-
- Reset Jumio for Public Key or Username: -
- - - +
+ Set Jumio Kickback Amount in $USD: +
+ + + +
+
+
+ Reset Jumio for Public Key or Username: +
+ + + +
+
+
+ Execute Jumio Callback for Public Key or Username: +
+
+ + +
+ + +
-
- Execute Jumio Callback for Public Key or Username: -
- - - +
+
+
Country
+
Referral Amount
+
Allow Referral Override
+
Kickback Amount
+
Allow Kickback Override
+
+
+ + {{ signUpBonus.key }} +
+
+ {{ globalVars.formatUSD(signUpBonus.value.CountryLevelSignUpBonus.ReferralAmountOverrideUSDCents / 100, 2) }} +
+
+ +
+
+ {{ globalVars.formatUSD(signUpBonus.value.CountryLevelSignUpBonus.KickbackAmountOverrideUSDCents / 100, 2) }} +
+
+ +
+
diff --git a/src/app/admin/admin-jumio/admin-jumio.component.ts b/src/app/admin/admin-jumio/admin-jumio.component.ts index 5ea14857f..f30e04b41 100644 --- a/src/app/admin/admin-jumio/admin-jumio.component.ts +++ b/src/app/admin/admin-jumio/admin-jumio.component.ts @@ -1,8 +1,12 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { GlobalVarsService } from "../../global-vars.service"; -import { BackendApiService } from "../../backend-api.service"; +import { BackendApiService, CountryLevelSignUpBonus, CountryLevelSignUpBonusResponse } from "../../backend-api.service"; import { SwalHelper } from "../../../lib/helpers/swal-helper"; +import { AdminJumioEditCountrySignUpBonusComponent } from "./admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component"; +import { BsModalService } from "ngx-bootstrap/modal"; +import { Subscription } from "rxjs"; +import { ToastrService } from "ngx-toastr"; @Component({ selector: "admin-jumio", @@ -14,17 +18,50 @@ export class AdminJumioComponent { usernameToExecuteJumioCallback = ""; resettingJumio = false; executingJumioCallback = false; + jumioCallbackCountrySelected: string = ""; - jumioDeSoNanos: number = 0; - updatingJumioDeSoNanos = false; + jumioUSD: number = 0; + jumioKickbackUSD: number = 0; + updatingJumioUSDCents = false; + updatingJumioKickbackUSDCents = false; + + countryLevelSignUpBonuses: { [k: string]: CountryLevelSignUpBonusResponse } = {}; + defaultSignUpBonus: CountryLevelSignUpBonus; + + static GENERAL = "General"; + static COUNTRY_BONUSES = "Country Bonuses"; + tabs = [AdminJumioComponent.GENERAL, AdminJumioComponent.COUNTRY_BONUSES]; + activeTab: string = AdminJumioComponent.GENERAL; + AdminJumioComponent = AdminJumioComponent; constructor( private globalVars: GlobalVarsService, private router: Router, private route: ActivatedRoute, - private backendApi: BackendApiService + private backendApi: BackendApiService, + private modalService: BsModalService, + private toastr: ToastrService ) { - this.jumioDeSoNanos = globalVars.jumioDeSoNanos; + this.jumioUSD = globalVars.jumioUSDCents / 100; + this.jumioKickbackUSD = globalVars.jumioKickbackUSDCents / 100; + this.refreshCountryBonuses(); + } + + refreshCountryBonuses(): Subscription { + return this.backendApi + .AdminGetAllCountryLevelSignUpBonuses( + this.globalVars.localNode, + this.globalVars.loggedInUser?.PublicKeyBase58Check + ) + .subscribe( + (res) => { + this.countryLevelSignUpBonuses = res.SignUpBonusMetadata; + this.defaultSignUpBonus = res.DefaultSignUpBonusMetadata; + }, + (err) => { + console.error(err); + } + ); } _resetJumio(): void { @@ -68,7 +105,8 @@ export class AdminJumioComponent { this.globalVars.localNode, this.globalVars.loggedInUser.PublicKeyBase58Check, pubKey, - username + username, + this.jumioCallbackCountrySelected ) .subscribe( (res) => { @@ -81,12 +119,13 @@ export class AdminJumioComponent { .add(() => (this.executingJumioCallback = false)); } - updateJumioDeSoNanos(): void { + updateJumioUSDCents(): void { SwalHelper.fire({ target: this.globalVars.getTargetComponentSelector(), title: "Are you ready?", - html: `You are about to update the amount of $DESO sent for verifying with Jumio to ${this.globalVars.nanosToDeSo( - this.jumioDeSoNanos + html: `You are about to update the default sign-up amount sent for verifying with Jumio to ${this.globalVars.formatUSD( + this.jumioUSD, + 2 )}.`, showConfirmButton: true, showCancelButton: true, @@ -99,22 +138,84 @@ export class AdminJumioComponent { cancelButtonText: "Cancel", }).then((res) => { if (res.isConfirmed) { - this.updatingJumioDeSoNanos = true; + this.updatingJumioUSDCents = true; + this.backendApi + .AdminUpdateJumioUSDCents( + this.globalVars.localNode, + this.globalVars.loggedInUser.PublicKeyBase58Check, + this.jumioUSD * 100 + ) + .subscribe( + (res) => { + this.globalVars.jumioUSDCents = res.USDCents; + this.refreshCountryBonuses(); + }, + (err) => { + console.error(err); + } + ) + .add(() => (this.updatingJumioUSDCents = false)); + } + }); + } + + updateJumioKickbackUSDCents(): void { + SwalHelper.fire({ + target: this.globalVars.getTargetComponentSelector(), + title: "Are you ready?", + html: `You are about to update the default kickback amount to ${this.globalVars.formatUSD( + this.jumioKickbackUSD, + 2 + )}. This is the default amount referrers will get when they refer someone who verifies with Jumio.`, + showConfirmButton: true, + showCancelButton: true, + reverseButtons: true, + customClass: { + confirmButton: "btn btn-light", + cancelButton: "btn btn-light no", + }, + confirmButtonText: "Ok", + cancelButtonText: "Cancel", + }).then((res) => { + if (res.isConfirmed) { + this.updatingJumioKickbackUSDCents = true; this.backendApi - .AdminUpdateJumioDeSo( + .AdminUpdateJumioKickbackUSDCents( this.globalVars.localNode, this.globalVars.loggedInUser.PublicKeyBase58Check, - this.jumioDeSoNanos + this.jumioKickbackUSD * 100 ) .subscribe( (res) => { - this.globalVars.jumioDeSoNanos = res.DeSoNanos; + this.globalVars.jumioKickbackUSDCents = res.USDCents; + this.refreshCountryBonuses(); }, (err) => { console.error(err); } ) - .add(() => (this.updatingJumioDeSoNanos = false)); + .add(() => (this.updatingJumioKickbackUSDCents = false)); + } + }); + } + + _handleTabClick(tab: string): void { + this.activeTab = tab; + } + + editCountry(country: string, event): void { + event.stopPropagation(); + const editBonusModal = this.modalService.show(AdminJumioEditCountrySignUpBonusComponent, { + class: "modal-dialog-centered modal-lg", + initialState: { countryLevelSignUpBonusResponse: this.countryLevelSignUpBonuses[country] }, + }); + editBonusModal.onHide.subscribe((res) => { + if (res === "sign-up-bonus-updated") { + this.refreshCountryBonuses(); + this.toastr.info(`Sign-Up Bonus updated for ${country}`, null, { + positionClass: "toast-top-center", + timeOut: 3000, + }); } }); } diff --git a/src/app/admin/admin.component.html b/src/app/admin/admin.component.html index f30c574f6..f7028c034 100644 --- a/src/app/admin/admin.component.html +++ b/src/app/admin/admin.component.html @@ -455,6 +455,18 @@ Email: {{ getUserAdminDataResponse.Email }}
+
+ Referral Code: + {{ getUserAdminDataResponse.ReferralHashBase58Check }} +
+
+ Jumio Starter Payment Txn ID: + {{ getUserAdminDataResponse.JumioStarterDeSoTxnHashBase58Check }} +
+
+ Referrer Payment Txn ID: + {{ getUserAdminDataResponse.ReferrerDeSoTxnHashBase58Check }} +
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index b8e21025e..2eb06aaa2 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -36,6 +36,9 @@ import { WalletTutorialPageComponent } from "./tutorial/wallet-tutorial-page/wal import { SellCreatorCoinsTutorialComponent } from "./tutorial/sell-creator-coins-tutorial-page/sell-creator-coins-tutorial/sell-creator-coins-tutorial.component"; import { DiamondTutorialPageComponent } from "./tutorial/diamond-tutorial-page/diamond-tutorial-page.component"; import { CreatePostTutorialPageComponent } from "./tutorial/create-post-tutorial-page/create-post-tutorial-page.component"; +import { + SupplyMonitoringStatsPageComponent +} from "./supply-monitoring-stats-page/supply-monitoring-stats-page.component"; class RouteNames { // Not sure if we should have a smarter schema for this, e.g. what happens if we have @@ -81,6 +84,7 @@ class RouteNames { public static TUTORIAL = "tutorial"; public static CREATE_PROFILE = "create-profile"; public static INVEST = "invest"; + public static SUPPLY_STATS = "supply-stats"; } const routes: Routes = [ @@ -162,6 +166,11 @@ const routes: Routes = [ component: CreatePostTutorialPageComponent, pathMatch: "full", }, + { + path: RouteNames.SUPPLY_STATS, + component: SupplyMonitoringStatsPageComponent, + pathMatch: "full", + }, // This NotFound route must be the last one as it catches all paths that were not matched above. { path: "**", component: NotFoundPageComponent, pathMatch: "full" }, ]; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 9292abd45..3e602f1f1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -196,6 +196,8 @@ export class AppComponent implements OnInit { this.globalVars.showBuyWithETH = res.BuyWithETH; this.globalVars.showJumio = res.HasJumioIntegration; this.globalVars.jumioDeSoNanos = res.JumioDeSoNanos; + this.globalVars.jumioUSDCents = res.JumioUSDCents; + this.globalVars.jumioKickbackUSDCents = res.JumioKickbackUSDCents; this.globalVars.isTestnet = res.IsTestnet; this.identityService.isTestnet = res.IsTestnet; this.globalVars.showPhoneNumberVerification = res.HasTwilioAPIKey && res.HasStarterDeSoSeed; @@ -208,16 +210,16 @@ export class AppComponent implements OnInit { // Calculate max fee for display in frontend // Sort so highest fee is at the top - var simpleFeeMap: { txnType: string; fees: number }[] = Object.keys(res.TransactionFeeMap) + const simpleFeeMap: { txnType: string; fees: number }[] = Object.keys(res.TransactionFeeMap) .map((k) => { if (res.TransactionFeeMap[k] !== null) { // only return for non empty transactions // sum in case there are multiple fee earners for the txn type - var sumOfFees = res.TransactionFeeMap[k] + const sumOfFees = res.TransactionFeeMap[k] .map((f) => f.AmountNanos) .reduce((partial_sum, a) => partial_sum + a, 0); // Capitalize and use spaces in Txn type - var txnType = (" " + k) + const txnType = (" " + k) .replace(/_/g, " ") .toLowerCase() .replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => " " + chr.toUpperCase()) @@ -225,6 +227,7 @@ export class AppComponent implements OnInit { return { txnType: txnType, fees: sumOfFees }; } }) + .filter((fee) => fee) .sort((a, b) => b.fees - a.fees); //Get the max of all fees diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b8f38be7e..31a3c9883 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -136,6 +136,7 @@ import { NftDropMgrComponent } from "./nft-drop-mgr/nft-drop-mgr.component"; import { NftShowcaseComponent } from "./nft-showcase/nft-showcase.component"; import { VerifyEmailComponent } from "./verify-email/verify-email.component"; import { AdminJumioComponent } from "./admin/admin-jumio/admin-jumio.component"; +import { AdminJumioEditCountrySignUpBonusComponent } from "./admin/admin-jumio/admin-jumio-edit-country-sign-up-bonus/admin-jumio-edit-country-sign-up-bonus.component"; import { JumioStatusComponent } from "./jumio-status/jumio-status.component"; import { AdminTutorialComponent } from "./admin/admin-tutorial/admin-tutorial.component"; import { CreateProfileTutorialPageComponent } from "./tutorial/create-profile-tutorial-page/create-profile-tutorial-page.component"; @@ -155,6 +156,13 @@ import { BuyDeSoEthComponent } from "./buy-deso-page/buy-deso-eth/buy-deso-eth.c import { SanitizeVideoUrlPipe } from "../lib/pipes/sanitize-video-url-pipe"; import { AdminNodeFeesComponent } from "./admin/admin-node-fees/admin-node-fees.component"; import { AdminNodeAddFeesComponent } from "./admin/admin-node-fees/admin-node-add-fee/admin-node-add-fees.component"; +import { FreeDesoMessageComponent } from "./free-deso-message/free-deso-message.component"; +import { SupplyMonitoringStatsPageComponent } from "./supply-monitoring-stats-page/supply-monitoring-stats-page.component"; +import { SupplyMonitoringStatsComponent } from "./supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component"; +import { TransferNftModalComponent } from "./transfer-nft-modal/transfer-nft-modal.component"; +import { TransferNftAcceptModalComponent } from "./transfer-nft-accept-modal/transfer-nft-accept-modal.component"; +import { NftBurnModalComponent } from "./nft-burn-modal/nft-burn-modal.component"; +import { NftSelectSerialNumberComponent } from "./nft-select-serial-number/nft-select-serial-number.component"; // Modular Themes for DeSo by Carsen Klock @carsenk import { ThemeModule } from "./theme/theme.module"; @@ -281,6 +289,7 @@ const greenishTheme: Theme = { key: "greenish", name: "Green Theme" }; NftShowcaseComponent, VerifyEmailComponent, AdminJumioComponent, + AdminJumioEditCountrySignUpBonusComponent, JumioStatusComponent, ReferralProgramMgrComponent, ReferralsComponent, @@ -299,6 +308,13 @@ const greenishTheme: Theme = { key: "greenish", name: "Green Theme" }; SanitizeVideoUrlPipe, AdminNodeFeesComponent, AdminNodeAddFeesComponent, + SupplyMonitoringStatsPageComponent, + SupplyMonitoringStatsComponent, + FreeDesoMessageComponent, + TransferNftAcceptModalComponent, + TransferNftModalComponent, + NftBurnModalComponent, + NftSelectSerialNumberComponent, ], imports: [ BrowserModule, diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts index 2a630450e..0aa0ad243 100644 --- a/src/app/backend-api.service.ts +++ b/src/app/backend-api.service.ts @@ -55,6 +55,8 @@ export class BackendRoutes { static RoutePathGetQuoteRepostsForPost = "/api/v0/get-quote-reposts-for-post"; static RoutePathGetJumioStatusForPublicKey = "/api/v0/get-jumio-status-for-public-key"; static RoutePathGetUserMetadata = "/api/v0/get-user-metadata"; + static RoutePathGetUsernameForPublicKey = "/api/v0/get-user-name-for-public-key"; + static RoutePathGetPublicKeyForUsername = "/api/v0/get-public-key-for-user-name"; // Verify static RoutePathVerifyEmail = "/api/v0/verify-email"; @@ -84,6 +86,9 @@ export class BackendRoutes { static RoutePathGetNextNFTShowcase = "/api/v0/get-next-nft-showcase"; static RoutePathGetNFTCollectionSummary = "/api/v0/get-nft-collection-summary"; static RoutePathGetNFTEntriesForPostHash = "/api/v0/get-nft-entries-for-nft-post"; + static RoutePathTransferNFT = "/api/v0/transfer-nft"; + static RoutePathAcceptNFTTransfer = "/api/v0/accept-nft-transfer"; + static RoutePathBurnNFT = "/api/v0/burn-nft"; // ETH static RoutePathSubmitETHTx = "/api/v0/submit-eth-tx"; @@ -122,6 +127,10 @@ export class BackendRoutes { static RoutePathAdminResetTutorialStatus = "/api/v0/admin/reset-tutorial-status"; static RoutePathAdminGetTutorialCreators = "/api/v0/admin/get-tutorial-creators"; static RoutePathAdminJumioCallback = "/api/v0/admin/jumio-callback"; + static RoutePathAdminGetAllCountryLevelSignUpBonuses = "/api/v0/admin/get-all-country-level-sign-up-bonuses"; + static RoutePathAdminUpdateJumioCountrySignUpBonus = "/api/v0/admin/update-jumio-country-sign-up-bonus"; + static RoutePathAdminUpdateJumioUSDCents = "/api/v0/admin/update-jumio-usd-cents"; + static RoutePathAdminUpdateJumioKickbackUSDCents = "/api/v0/admin/update-jumio-kickback-usd-cents"; // Referral program admin routes. static RoutePathAdminCreateReferralHash = "/api/v0/admin/create-referral-hash"; @@ -146,6 +155,11 @@ export class BackendRoutes { static RoutePathAdminGetTransactionFeeMap = "/api/v0/admin/get-transaction-fee-map"; static RoutePathAdminAddExemptPublicKey = "/api/v0/admin/add-exempt-public-key"; static RoutePathAdminGetExemptPublicKeys = "/api/v0/admin/get-exempt-public-keys"; + + // Supply Monitoring endpoints + static RoutePathGetTotalSupply = "/api/v0/total-supply"; + static RoutePathGetRichList = "/api/v0/rich-list"; + static RoutePathGetCountKeysWithDESO = "/api/v0/count-keys-with-deso"; } export class Transaction { @@ -274,6 +288,8 @@ export class PostEntryResponse { IsNFT: boolean; NFTRoyaltyToCoinBasisPoints: number; NFTRoyaltyToCreatorBasisPoints: number; + AdditionalDESORoyaltiesMap: { [k: string]: number }; + AdditionalCoinRoyaltiesMap: { [k: string]: number }; } export class DiamondsPost { @@ -325,8 +341,11 @@ export class NFTEntryResponse { PostEntryResponse: PostEntryResponse | undefined; SerialNumber: number; IsForSale: boolean; + IsPending?: boolean; MinBidAmountNanos: number; LastAcceptedBidAmountNanos: number; + IsBuyNow: boolean; + BuyNowPriceNanos: number; HighestBidAmountNanos: number; LowestBidAmountNanos: number; @@ -397,6 +416,32 @@ type GetUsersStatelessResponse = { ParamUpdaters: { [k: string]: boolean }; }; +export type RichListEntryResponse = { + PublicKeyBase58Check: string; + BalanceNanos: number; + BalanceDESO: number; + Percentage: number; + Value: number; +}; + +export type CountryLevelSignUpBonus = { + AllowCustomReferralAmount: boolean; + ReferralAmountOverrideUSDCents: number; + AllowCustomKickbackAmount: boolean; + KickbackAmountOverrideUSDCents: number; +}; + +export type CountryCodeDetails = { + Name: string; + CountryCode: string; + Alpha3: string; +}; + +export type CountryLevelSignUpBonusResponse = { + CountryLevelSignUpBonus: CountryLevelSignUpBonus; + CountryCodeDetails: CountryCodeDetails; +}; + @Injectable({ providedIn: "root", }) @@ -848,6 +893,10 @@ export class BackendApiService { HasUnlockable: boolean, IsForSale: boolean, MinBidAmountNanos: number, + IsBuyNow: boolean, + BuyNowPriceNanos: number, + AdditionalDESORoyaltiesMap: { [k: string]: number }, + AdditionalCoinRoyaltiesMap: { [k: string]: number }, MinFeeRateNanosPerKB: number ): Observable { const request = this.post(endpoint, BackendRoutes.RoutePathCreateNft, { @@ -859,6 +908,10 @@ export class BackendApiService { HasUnlockable, IsForSale, MinBidAmountNanos, + IsBuyNow, + BuyNowPriceNanos, + AdditionalDESORoyaltiesMap, + AdditionalCoinRoyaltiesMap, MinFeeRateNanosPerKB, }); @@ -872,6 +925,8 @@ export class BackendApiService { SerialNumber: number, IsForSale: boolean, MinBidAmountNanos: number, + IsBuyNow: boolean, + BuyNowPriceNanos: number, MinFeeRateNanosPerKB: number ): Observable { const request = this.post(endpoint, BackendRoutes.RoutePathUpdateNFT, { @@ -880,6 +935,8 @@ export class BackendApiService { SerialNumber, IsForSale, MinBidAmountNanos, + IsBuyNow, + BuyNowPriceNanos, MinFeeRateNanosPerKB, }); @@ -942,6 +999,75 @@ export class BackendApiService { return this.signAndSubmitTransaction(endpoint, request, UpdaterPublicKeyBase58Check); } + TransferNFT( + endpoint: string, + SenderPublicKeyBase58Check: string, + ReceiverPublicKeyBase58Check: string, + NFTPostHashHex: string, + SerialNumber: number, + UnencryptedUnlockableText: string, + MinFeeRateNanosPerKB: number + ): Observable { + let request = UnencryptedUnlockableText + ? this.identityService.encrypt({ + ...this.identityService.identityServiceParamsForKey(SenderPublicKeyBase58Check), + recipientPublicKey: ReceiverPublicKeyBase58Check, + message: UnencryptedUnlockableText, + }) + : of({ encryptedMessage: "" }); + request = request.pipe( + switchMap((encrypted) => { + const EncryptedUnlockableText = encrypted.encryptedMessage; + return this.post(endpoint, BackendRoutes.RoutePathTransferNFT, { + SenderPublicKeyBase58Check, + ReceiverPublicKeyBase58Check, + NFTPostHashHex, + SerialNumber, + EncryptedUnlockableText, + MinFeeRateNanosPerKB, + }).pipe( + map((request) => { + return { ...request }; + }) + ); + }) + ); + + return this.signAndSubmitTransaction(endpoint, request, SenderPublicKeyBase58Check); + } + + AcceptNFTTransfer( + endpoint: string, + UpdaterPublicKeyBase58Check: string, + NFTPostHashHex: string, + SerialNumber: number, + MinFeeRateNanosPerKB: number + ): Observable { + const request = this.post(endpoint, BackendRoutes.RoutePathAcceptNFTTransfer, { + UpdaterPublicKeyBase58Check, + NFTPostHashHex, + SerialNumber, + MinFeeRateNanosPerKB, + }); + return this.signAndSubmitTransaction(endpoint, request, UpdaterPublicKeyBase58Check); + } + + BurnNFT( + endpoint: string, + UpdaterPublicKeyBase58Check: string, + NFTPostHashHex: string, + SerialNumber: number, + MinFeeRateNanosPerKB: number + ): Observable { + const request = this.post(endpoint, BackendRoutes.RoutePathBurnNFT, { + UpdaterPublicKeyBase58Check, + NFTPostHashHex, + SerialNumber, + MinFeeRateNanosPerKB, + }); + return this.signAndSubmitTransaction(endpoint, request, UpdaterPublicKeyBase58Check); + } + DecryptUnlockableTexts( ReaderPublicKeyBase58Check: string, UnlockableNFTEntryResponses: NFTEntryResponse[] @@ -981,12 +1107,14 @@ export class BackendApiService { endpoint: string, UserPublicKeyBase58Check: string, ReaderPublicKeyBase58Check: string, - IsForSale: boolean | null = null + IsForSale: boolean | null = null, + IsPending: boolean | null = null ): Observable { return this.post(endpoint, BackendRoutes.RoutePathGetNFTsForUser, { UserPublicKeyBase58Check, ReaderPublicKeyBase58Check, IsForSale, + IsPending, }); } @@ -1332,18 +1460,14 @@ export class BackendApiService { map((res) => { // This array contains encrypted messages with public keys // Public keys of the other party involved in the correspondence - const encryptedMessages = []; - for (const threads of res.OrderedContactsWithMessages) { - for (const message of threads.Messages) { - const payload = { - EncryptedHex: message.EncryptedText, - PublicKey: message.IsSender ? message.RecipientPublicKeyBase58Check : message.SenderPublicKeyBase58Check, - IsSender: message.IsSender, - Legacy: !message.V2, - }; - encryptedMessages.push(payload); - } - } + const encryptedMessages = res.OrderedContactsWithMessages.flatMap((thread) => + thread.Messages.flatMap((message) => ({ + EncryptedHex: message.EncryptedText, + PublicKey: message.IsSender ? message.RecipientPublicKeyBase58Check : message.SenderPublicKeyBase58Check, + IsSender: message.IsSender, + Legacy: !message.V2, + })) + ); return { ...res, encryptedMessages }; }) ); @@ -1358,12 +1482,11 @@ export class BackendApiService { }) .pipe( map((decrypted) => { - for (const threads of res.OrderedContactsWithMessages) { - for (const message of threads.Messages) { - message.DecryptedText = decrypted.decryptedHexes[message.EncryptedText]; - } - } - + res.OrderedContactsWithMessages.forEach((threads) => + threads.Messages.forEach( + (message) => (message.DecryptedText = decrypted.decryptedHexes[message.EncryptedText]) + ) + ); return { ...res, ...decrypted }; }) ); @@ -1668,6 +1791,14 @@ export class BackendApiService { return this.get(endpoint, BackendRoutes.RoutePathGetUserMetadata + "/" + PublicKeyBase58Check); } + GetUsernameForPublicKey(endpoint: string, PublicKeyBase58Check: string): Observable { + return this.get(endpoint, BackendRoutes.RoutePathGetUsernameForPublicKey + "/" + PublicKeyBase58Check); + } + + GetPublicKeyForUsername(endpoint: string, Username: string): Observable { + return this.get(endpoint, BackendRoutes.RoutePathGetPublicKeyForUsername + "/" + Username); + } + GetJumioStatusForPublicKey(endpoint: string, PublicKeyBase58Check: string): Observable { return this.jwtPost(endpoint, BackendRoutes.RoutePathGetJumioStatusForPublicKey, PublicKeyBase58Check, { PublicKeyBase58Check, @@ -1989,16 +2120,57 @@ export class BackendApiService { }); } + AdminUpdateJumioUSDCents(endpoint: string, AdminPublicKey: string, USDCents: number): Observable { + return this.jwtPost(endpoint, BackendRoutes.RoutePathAdminUpdateJumioUSDCents, AdminPublicKey, { + AdminPublicKey, + USDCents, + }); + } + + AdminUpdateJumioKickbackUSDCents(endpoint: string, AdminPublicKey: string, USDCents: number): Observable { + return this.jwtPost(endpoint, BackendRoutes.RoutePathAdminUpdateJumioKickbackUSDCents, AdminPublicKey, { + AdminPublicKey, + USDCents, + }); + } + AdminJumioCallback( endpoint: string, AdminPublicKey: string, PublicKeyBase58Check: string, - Username: string + Username: string, + CountryAlpha3: string = "" ): Observable { return this.jwtPost(endpoint, BackendRoutes.RoutePathAdminJumioCallback, AdminPublicKey, { PublicKeyBase58Check, Username, AdminPublicKey, + CountryAlpha3, + }); + } + + AdminGetAllCountryLevelSignUpBonuses( + endpoint: string, + AdminPublicKey: string + ): Observable<{ + SignUpBonusMetadata: { [k: string]: CountryLevelSignUpBonusResponse }; + DefaultSignUpBonusMetadata: CountryLevelSignUpBonus; + }> { + return this.jwtPost(endpoint, BackendRoutes.RoutePathAdminGetAllCountryLevelSignUpBonuses, AdminPublicKey, { + AdminPublicKey, + }); + } + + AdminUpdateJumioCountrySignUpBonus( + endpoint: string, + AdminPublicKey: string, + CountryCode: string, + CountryLevelSignUpBonus: CountryLevelSignUpBonus + ): Observable { + return this.jwtPost(endpoint, BackendRoutes.RoutePathAdminUpdateJumioCountrySignUpBonus, AdminPublicKey, { + AdminPublicKey, + CountryCode, + CountryLevelSignUpBonus, }); } @@ -2063,11 +2235,20 @@ export class BackendApiService { }); } - AdminUploadReferralCSV(endpoint: string, AdminPublicKey: string, CSVRows: Array>): Observable { - return this.jwtPost(endpoint, BackendRoutes.RoutePathAdminUploadReferralCSV, AdminPublicKey, { - AdminPublicKey, - CSVRows, + AdminUploadReferralCSV(endpoint: string, AdminPublicKey: string, file: File): Observable { + const request = this.identityService.jwt({ + ...this.identityService.identityServiceParamsForKey(AdminPublicKey), }); + return request.pipe( + switchMap((signed) => { + const formData = new FormData(); + formData.append("file", file); + formData.append("UserPublicKeyBase58Check", AdminPublicKey); + formData.append("JWT", signed.jwt); + + return this.post(endpoint, BackendRoutes.RoutePathAdminUploadReferralCSV, formData); + }) + ); } GetReferralInfoForUser(endpoint: string, PublicKeyBase58Check: string): Observable { @@ -2076,7 +2257,10 @@ export class BackendApiService { }); } - GetReferralInfoForReferralHash(endpoint: string, ReferralHash: string): Observable { + GetReferralInfoForReferralHash( + endpoint: string, + ReferralHash: string + ): Observable<{ ReferralInfoResponse: any; CountrySignUpBonus: CountryLevelSignUpBonus }> { return this.post(endpoint, BackendRoutes.RoutePathGetReferralInfoForReferralHash, { ReferralHash, }); @@ -2229,6 +2413,18 @@ export class BackendApiService { return this.get(endpoint, `${BackendRoutes.RoutePathGetVideoStatus}/${videoId}`); } + GetTotalSupply(endpoint: string): Observable { + return this.get(endpoint, BackendRoutes.RoutePathGetTotalSupply); + } + + GetRichList(endpoint: string): Observable { + return this.get(endpoint, BackendRoutes.RoutePathGetRichList); + } + + GetCountOfKeysWithDESO(endpoint: string): Observable { + return this.get(endpoint, BackendRoutes.RoutePathGetCountKeysWithDESO); + } + // Error parsing stringifyError(err): string { if (err && err.error && err.error.error) { diff --git a/src/app/buy-deso-page/buy-deso-eth/buy-deso-eth.component.ts b/src/app/buy-deso-page/buy-deso-eth/buy-deso-eth.component.ts index b412c4d2d..f47651749 100644 --- a/src/app/buy-deso-page/buy-deso-eth/buy-deso-eth.component.ts +++ b/src/app/buy-deso-page/buy-deso-eth/buy-deso-eth.component.ts @@ -472,7 +472,7 @@ export class BuyDeSoEthComponent implements OnInit { } getExchangeRateAfterFee(): BN { - return new BN(this.globalVars.nanosPerETHExchangeRate).mul(new BN(this.nodeFee())); + return new BN(this.globalVars.nanosPerETHExchangeRate / this.nodeFee()); } getWeiPerNanoExchangeRate(): BN { diff --git a/src/app/close-nft-auction-modal/close-nft-auction-modal.component.ts b/src/app/close-nft-auction-modal/close-nft-auction-modal.component.ts index 7aed6b9cd..b7606ac13 100644 --- a/src/app/close-nft-auction-modal/close-nft-auction-modal.component.ts +++ b/src/app/close-nft-auction-modal/close-nft-auction-modal.component.ts @@ -38,6 +38,8 @@ export class CloseNftAuctionModalComponent { nftEntry.SerialNumber, false, nftEntry.MinBidAmountNanos, + false, + 0, this.globalVars.defaultFeeRateNanosPerKB ) .pipe( @@ -52,8 +54,8 @@ export class CloseNftAuctionModalComponent { .subscribe( (res) => { // Hide this modal and open the next one. - this.bsModalRef.hide(); this.modalService.setDismissReason("auction cancelled"); + this.bsModalRef.hide(); }, (err) => { console.error(err); diff --git a/src/app/create-nft-auction-modal/create-nft-auction-modal.component.html b/src/app/create-nft-auction-modal/create-nft-auction-modal.component.html index 1e8cf4402..f9aa94ed3 100644 --- a/src/app/create-nft-auction-modal/create-nft-auction-modal.component.html +++ b/src/app/create-nft-auction-modal/create-nft-auction-modal.component.html @@ -38,6 +38,61 @@ [disabled]="creatingAuction"/>
+ +
+
Enable "Buy Now"(Optional)
+
+ + +
+
+
+ Buy Now Price +
+
+
+
+  USD  +
+ +
+
+
+ DESO +
+ +
+
+
+ Buy Now price must be greater than min bid amount +
+
+ diff --git a/src/app/create-nft-auction-modal/create-nft-auction-modal.component.ts b/src/app/create-nft-auction-modal/create-nft-auction-modal.component.ts index d8276f2ce..ce4104a82 100644 --- a/src/app/create-nft-auction-modal/create-nft-auction-modal.component.ts +++ b/src/app/create-nft-auction-modal/create-nft-auction-modal.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from "@angular/core"; -import { BsModalRef } from "ngx-bootstrap/modal"; +import { BsModalRef, BsModalService } from "ngx-bootstrap/modal"; import { GlobalVarsService } from "../global-vars.service"; import { BackendApiService, NFTEntryResponse, PostEntryResponse } from "../backend-api.service"; import { concatMap, last, map } from "rxjs/operators"; @@ -15,16 +15,20 @@ export class CreateNftAuctionModalComponent { @Input() post: PostEntryResponse; @Input() nftEntryResponses: NFTEntryResponse[]; loading = false; - minBidAmountUSD: string; - minBidAmountDESO: number; + minBidAmountUSD: string = "0"; + minBidAmountDESO: number = 0; selectedSerialNumbers: boolean[] = []; selectAll: boolean = false; creatingAuction: boolean = false; + isBuyNow: boolean = false; + buyNowPriceUSD: string = "0"; + buyNowPriceDESO: number = 0; constructor( private backendApi: BackendApiService, public globalVars: GlobalVarsService, public bsModalRef: BsModalRef, + private modalService: BsModalService, private router: Router ) {} @@ -36,6 +40,21 @@ export class CreateNftAuctionModalComponent { this.minBidAmountDESO = Math.trunc(this.globalVars.usdToNanosNumber(usdAmount)) / 1e9; } + updateBuyNowPriceUSD(desoAmount): void { + this.buyNowPriceUSD = this.globalVars.nanosToUSDNumber(desoAmount * 1e9).toFixed(2); + } + + updateBuyNowPriceDESO(usdAmount): void { + this.buyNowPriceDESO = Math.trunc(this.globalVars.usdToNanosNumber(usdAmount)) / 1e9; + } + + updateBuyNowStatus(isBuyNow: boolean): void { + if (!isBuyNow) { + this.buyNowPriceDESO = 0; + this.buyNowPriceUSD = "0"; + } + } + auctionTotal: number; auctionCounter: number = 0; createAuction() { @@ -53,6 +72,8 @@ export class CreateNftAuctionModalComponent { val, true, Math.trunc(this.minBidAmountDESO * 1e9), + this.isBuyNow, + Math.trunc(this.buyNowPriceDESO * 1e9), this.globalVars.defaultFeeRateNanosPerKB ) .pipe( @@ -70,6 +91,7 @@ export class CreateNftAuctionModalComponent { .subscribe( (res) => { this.router.navigate(["/" + this.globalVars.RouteNames.NFT + "/" + this.post.PostHashHex]); + this.modalService.setDismissReason("auction created"); this.bsModalRef.hide(); }, (err) => { @@ -84,6 +106,7 @@ export class CreateNftAuctionModalComponent { return this.nftEntryResponses.filter( (nftEntryResponse) => !nftEntryResponse.IsForSale && + !nftEntryResponse.IsPending && nftEntryResponse.OwnerPublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check ); } @@ -95,7 +118,10 @@ export class CreateNftAuctionModalComponent { } createAuctionDisabled(): boolean { - return !this.selectedSerialNumbers.filter((isSelected) => isSelected)?.length; + return ( + !this.selectedSerialNumbers.filter((isSelected) => isSelected)?.length || + (this.isBuyNow && this.buyNowPriceDESO < this.minBidAmountDESO) + ); } selectSerialNumber(idx: number): void { diff --git a/src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.html b/src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.html index 86a64226c..2fa9d9cf6 100644 --- a/src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.html +++ b/src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.html @@ -46,6 +46,32 @@ +
+
+ No transferable NFTs right now. + + @{{ profile.Username }} has no transferable NFTs yet. + +
+
+
+
+ No pending NFTs right now. + + @{{ profile.Username }} has no pending NFTs yet. + +
+
NFTs that @{{ profile.Username }} is currently selling
+
+ NFTs that @{{ profile.Username }} can transfer +
+
+ Transferred NFTs pending acceptance by @{{ profile.Username }} +
diff --git a/src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.ts b/src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.ts index 403777c2c..ed43b4a71 100644 --- a/src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.ts +++ b/src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.ts @@ -39,6 +39,8 @@ export class CreatorProfileNftsComponent implements OnInit { static FOR_SALE = "For Sale"; static MY_BIDS = "My Bids"; static MY_GALLERY = "Gallery"; + static TRANSFERABLE = "Transferable"; + static MY_PENDING_TRANSFERS = "Pending Transfers"; tabs = [CreatorProfileNftsComponent.FOR_SALE, CreatorProfileNftsComponent.MY_GALLERY]; activeTab: string; @@ -46,12 +48,16 @@ export class CreatorProfileNftsComponent implements OnInit { my_bids: CreatorProfileNftsComponent.MY_BIDS, for_sale: CreatorProfileNftsComponent.FOR_SALE, my_gallery: CreatorProfileNftsComponent.MY_GALLERY, + transferable: CreatorProfileNftsComponent.TRANSFERABLE, + my_pending_transfers: CreatorProfileNftsComponent.MY_PENDING_TRANSFERS, }; nftTabInverseMap = { [CreatorProfileNftsComponent.FOR_SALE]: "for_sale", [CreatorProfileNftsComponent.MY_BIDS]: "my_bids", [CreatorProfileNftsComponent.MY_GALLERY]: "my_gallery", + [CreatorProfileNftsComponent.TRANSFERABLE]: "transferable", + [CreatorProfileNftsComponent.MY_PENDING_TRANSFERS]: "my_pending_transfers", }; CreatorProfileNftsComponent = CreatorProfileNftsComponent; @@ -68,13 +74,19 @@ export class CreatorProfileNftsComponent implements OnInit { ) {} ngOnInit(): void { - if (this.globalVars.loggedInUser?.PublicKeyBase58Check === this.profile.PublicKeyBase58Check) { - this.tabs.push(CreatorProfileNftsComponent.MY_BIDS); + if (this.profileBelongsToLoggedInUser()) { + this.tabs.push( + CreatorProfileNftsComponent.MY_BIDS, + CreatorProfileNftsComponent.MY_PENDING_TRANSFERS, + CreatorProfileNftsComponent.TRANSFERABLE + ); } this.route.queryParams.subscribe((queryParams) => { if (queryParams.nftTab && queryParams.nftTab in this.nftTabMap) { if ( - queryParams.nftTab === this.nftTabInverseMap[CreatorProfileNftsComponent.MY_BIDS] && + (queryParams.nftTab === this.nftTabInverseMap[CreatorProfileNftsComponent.MY_BIDS] || + queryParams.nftTab === this.nftTabInverseMap[CreatorProfileNftsComponent.TRANSFERABLE] || + queryParams.nftTab === this.nftTabInverseMap[CreatorProfileNftsComponent.MY_PENDING_TRANSFERS]) && this.globalVars.loggedInUser?.PublicKeyBase58Check !== this.profile.PublicKeyBase58Check ) { this.updateNFTTabParam(CreatorProfileNftsComponent.MY_GALLERY); @@ -121,13 +133,14 @@ export class CreatorProfileNftsComponent implements OnInit { ); } - getNFTs(isForSale: boolean | null = null): Subscription { + getNFTs(isForSale: boolean | null = null, isPending: boolean | null = null): Subscription { return this.backendApi .GetNFTsForUser( this.globalVars.localNode, this.profile.PublicKeyBase58Check, this.globalVars.loggedInUser?.PublicKeyBase58Check, - isForSale + isForSale, + isPending ) .subscribe( (res: { @@ -136,13 +149,16 @@ export class CreatorProfileNftsComponent implements OnInit { this.nftResponse = []; for (const k in res.NFTsMap) { const responseElement = res.NFTsMap[k]; + // Exclude NFTs created by profile from Gallery and don't show pending NFTs in galley. if ( - (this.activeTab === CreatorProfileNftsComponent.MY_GALLERY && - responseElement.PostEntryResponse.PosterPublicKeyBase58Check !== this.profile.PublicKeyBase58Check) || - this.activeTab === CreatorProfileNftsComponent.FOR_SALE + this.activeTab === CreatorProfileNftsComponent.MY_GALLERY && + (responseElement.PostEntryResponse.PosterPublicKeyBase58Check === this.profile.PublicKeyBase58Check || + responseElement.NFTEntryResponses.filter((nftEntryResponse) => !nftEntryResponse.IsPending).length === + 0) ) { - this.nftResponse.push(responseElement); + continue; } + this.nftResponse.push(responseElement); } this.lastPage = Math.floor(this.nftResponse.length / CreatorProfileNftsComponent.PAGE_SIZE); return this.nftResponse; @@ -218,7 +234,7 @@ export class CreatorProfileNftsComponent implements OnInit { this.resetDatasource(event); }); } else { - return this.getNFTs(this.getIsForSaleValue()).add(() => { + return this.getNFTs(this.getIsForSaleValue(), this.getIsPendingValue()).add(() => { this.resetDatasource(event); }); } @@ -290,6 +306,25 @@ export class CreatorProfileNftsComponent implements OnInit { } getIsForSaleValue(): boolean | null { - return this.activeTab === CreatorProfileNftsComponent.MY_GALLERY ? null : true; + if (this.activeTab === CreatorProfileNftsComponent.FOR_SALE) { + return true; + } else if (this.activeTab === CreatorProfileNftsComponent.TRANSFERABLE) { + return false; + } else { + return null; + } + } + + getIsPendingValue(): boolean | null { + if (this.activeTab === CreatorProfileNftsComponent.MY_PENDING_TRANSFERS) { + return true; + } else if ( + this.activeTab === CreatorProfileNftsComponent.MY_GALLERY || + this.activeTab === CreatorProfileNftsComponent.TRANSFERABLE + ) { + return false; + } else { + return null; + } } } diff --git a/src/app/feed/feed-post-dropdown/feed-post-dropdown.component.html b/src/app/feed/feed-post-dropdown/feed-post-dropdown.component.html index c05ab82e4..f0347584b 100644 --- a/src/app/feed/feed-post-dropdown/feed-post-dropdown.component.html +++ b/src/app/feed/feed-post-dropdown/feed-post-dropdown.component.html @@ -12,7 +12,7 @@ (click)="copyPostLinkToClipboard($event)" > - Link to Post + Link to {{ post.IsNFT ? "NFT" : "Post" }} - Share Post + Share {{ post.IsNFT ? "NFT" : "Post" }} + Put On Sale + + + Transfer NFT + + + + Burn NFT + + !nftEntryResponse.IsPending && + !nftEntryResponse.IsForSale && + nftEntryResponse.OwnerPublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check + )?.length + ); + } + + showBurnNFT(): boolean { + return ( + this.post.IsNFT && + !!this.nftEntryResponses?.filter( + (nftEntryResponse) => + !nftEntryResponse.IsForSale && + nftEntryResponse.OwnerPublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check + )?.length + ); + } + hidePost() { this.postHidden.emit(); } @@ -228,9 +255,47 @@ export class FeedPostDropdownComponent { } openCreateNFTAuctionModal(event): void { - this.modalService.show(CreateNftAuctionModalComponent, { + const modalDetails = this.modalService.show(CreateNftAuctionModalComponent, { class: "modal-dialog-centered", initialState: { post: this.post, nftEntryResponses: this.nftEntryResponses }, }); + const onHideEvent = modalDetails.onHide; + onHideEvent.subscribe((response) => { + if (response === "auction created") { + this.refreshNFTEntries.emit(); + } + }); + } + + openTransferNFTModal(event): void { + const modalDetails = this.modalService.show(TransferNftModalComponent, { + class: "modal-dialog-centered modal-lg", + initialState: { post: this.post, postHashHex: this.post.PostHashHex }, + }); + const onHideEvent = modalDetails.onHide; + onHideEvent.subscribe((response) => { + if (response === "nft transferred") { + this.refreshNFTEntries.emit(); + } + }); + } + + openBurnNFTModal(event): void { + const burnNFTEntryResponses = filter(this.nftEntryResponses, (nftEntryResponse: NFTEntryResponse) => { + return ( + !nftEntryResponse.IsForSale && + nftEntryResponse.OwnerPublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check + ); + }); + const modalDetails = this.modalService.show(NftBurnModalComponent, { + class: "modal-dialog-centered modal-lg", + initialState: { post: this.post, postHashHex: this.post.PostHashHex, burnNFTEntryResponses }, + }); + const onHideEvent = modalDetails.onHide; + onHideEvent.subscribe((response) => { + if (response === "nft burned") { + this.refreshNFTEntries.emit(); + } + }); } } diff --git a/src/app/feed/feed-post/feed-post.component.html b/src/app/feed/feed-post/feed-post.component.html index d9eb98979..80e4df000 100755 --- a/src/app/feed/feed-post/feed-post.component.html +++ b/src/app/feed/feed-post/feed-post.component.html @@ -105,6 +105,7 @@ (postHidden)="hidePost()" (userBlocked)="blockUser()" (toggleGlobalFeed)="_addPostToGlobalFeed()" + (refreshNFTEntries)="refreshNFTEntriesHandler()" >
@@ -225,6 +226,7 @@ (userBlocked)="blockUser()" (toggleGlobalFeed)="_addPostToGlobalFeed($event)" (togglePostPin)="_pinPostToGlobalFeed($event)" + (refreshNFTEntries)="refreshNFTEntriesHandler()" >
@@ -530,6 +532,9 @@
Min bid: {{ globalVars.nanosToDeSo(nftMinBidAmountNanos) }} $DESO
+
+ Buy Now Price: {{ globalVars.nanosToDeSo(nftBuyNowPriceNanos) }} $DESO +
+
diff --git a/src/app/feed/feed-post/feed-post.component.ts b/src/app/feed/feed-post/feed-post.component.ts index 932594a4e..bdab4d68e 100755 --- a/src/app/feed/feed-post/feed-post.component.ts +++ b/src/app/feed/feed-post/feed-post.component.ts @@ -16,6 +16,7 @@ import { PlaceBidModalComponent } from "../../place-bid-modal/place-bid-modal.co import { EmbedUrlParserService } from "../../../lib/services/embed-url-parser-service/embed-url-parser-service"; import { SharedDialogs } from "../../../lib/shared-dialogs"; import { environment } from "src/environments/environment"; +import { TransferNftAcceptModalComponent } from "../../transfer-nft-accept-modal/transfer-nft-accept-modal.component"; @Component({ selector: "feed-post", @@ -96,8 +97,11 @@ export class FeedPostComponent implements OnInit { @Input() nftCollectionHighBid = 0; @Input() nftCollectionLowBid = 0; @Input() isForSaleOnly: boolean = false; + + // Only populated when there is exactly one copy nftLastAcceptedBidAmountNanos: number; nftMinBidAmountNanos: number; + nftBuyNowPriceNanos: number; @Input() showNFTDetails = false; @Input() showExpandedNFTDetails = false; @@ -111,6 +115,9 @@ export class FeedPostComponent implements OnInit { @Input() inTutorial: boolean = false; + // If this is a pending NFT post that still needs to be accepted by the user + @Input() acceptNFT: boolean = false; + // emits the PostEntryResponse @Output() postDeleted = new EventEmitter(); @@ -123,6 +130,9 @@ export class FeedPostComponent implements OnInit { // emits diamondSent event @Output() diamondSent = new EventEmitter(); + // Emit tells parent component to refresh NFT data + @Output() refreshNFTEntries = new EventEmitter(); + AppRoutingModule = AppRoutingModule; addingPostToGlobalFeed = false; repost: any; @@ -198,14 +208,23 @@ export class FeedPostComponent implements OnInit { this.highBid = _.maxBy(this.availableSerialNumbers, "HighestBidAmountNanos")?.HighestBidAmountNanos || 0; this.lowBid = _.minBy(this.availableSerialNumbers, "HighestBidAmountNanos")?.HighestBidAmountNanos || 0; if (this.nftEntryResponses.length === 1) { - this.nftLastAcceptedBidAmountNanos = this.nftEntryResponses[0].LastAcceptedBidAmountNanos; - if (this.nftEntryResponses[0].MinBidAmountNanos > 0) { - this.nftMinBidAmountNanos = this.nftEntryResponses[0].MinBidAmountNanos; + const nftEntryResponse = this.nftEntryResponses[0]; + this.nftLastAcceptedBidAmountNanos = nftEntryResponse.LastAcceptedBidAmountNanos; + if (nftEntryResponse.MinBidAmountNanos > 0) { + this.nftMinBidAmountNanos = nftEntryResponse.MinBidAmountNanos; + } + if (nftEntryResponse.BuyNowPriceNanos > 0 && nftEntryResponse.IsBuyNow) { + this.nftBuyNowPriceNanos = nftEntryResponse.BuyNowPriceNanos; } } }); } + refreshNFTEntriesHandler() { + this.getNFTEntries(); + this.refreshNFTEntries.emit(); + } + ngOnInit() { if (!this.post.RepostCount) { this.post.RepostCount = 0; @@ -547,6 +566,30 @@ export class FeedPostComponent implements OnInit { return imgURL; } + acceptTransfer(event) { + event.stopPropagation(); + const transferNFTEntryResponses = _.filter(this.nftEntryResponses, (nftEntryResponse: NFTEntryResponse) => { + return ( + nftEntryResponse.OwnerPublicKeyBase58Check === this.globalVars.loggedInUser.PublicKeyBase58Check && + nftEntryResponse.IsPending + ); + }); + + const modalDetails = this.modalService.show(TransferNftAcceptModalComponent, { + class: "modal-dialog-centered modal-lg", + initialState: { + post: this.postContent, + transferNFTEntryResponses, + }, + }); + const onHideEvent = modalDetails.onHide; + onHideEvent.subscribe((response) => { + if (response === "transfer accepted") { + this.getNFTEntries(); + } + }); + } + openPlaceBidModal(event: any) { if (!this.globalVars.loggedInUser?.ProfileEntryResponse) { SharedDialogs.showCreateProfileToPerformActionDialog(this.router, "place a bid"); @@ -562,6 +605,9 @@ export class FeedPostComponent implements OnInit { if (response === "bid placed") { this.getNFTEntries(); this.nftBidPlaced.emit(); + } else if (response === "nft purchased") { + this.getNFTEntries(); + this.refreshNFTEntries.emit(); } }); } diff --git a/src/app/free-deso-message/free-deso-message.component.html b/src/app/free-deso-message/free-deso-message.component.html new file mode 100644 index 000000000..57d84c02e --- /dev/null +++ b/src/app/free-deso-message/free-deso-message.component.html @@ -0,0 +1,12 @@ + + {{ globalVars.getFreeDESOMessage() }} + + + + diff --git a/src/app/free-deso-message/free-deso-message.component.ts b/src/app/free-deso-message/free-deso-message.component.ts new file mode 100644 index 000000000..9c5aa8163 --- /dev/null +++ b/src/app/free-deso-message/free-deso-message.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from "@angular/core"; +import { GlobalVarsService } from "../global-vars.service"; + +@Component({ + selector: "free-deso-message", + templateUrl: "./free-deso-message.component.html", +}) +export class FreeDesoMessageComponent { + @Input() hideMessage: boolean = false; + + constructor(public globalVars: GlobalVarsService) {} +} diff --git a/src/app/global-vars.service.ts b/src/app/global-vars.service.ts index 2bdf3d87f..813fec524 100755 --- a/src/app/global-vars.service.ts +++ b/src/app/global-vars.service.ts @@ -20,7 +20,7 @@ import { AmplitudeClient } from "amplitude-js"; import { DomSanitizer } from "@angular/platform-browser"; import { IdentityService } from "./identity.service"; import { BithuntService, CommunityProject } from "../lib/services/bithunt/bithunt-service"; -import { LeaderboardResponse, PulseService } from "../lib/services/pulse/pulse-service"; +import { LeaderboardResponse, AltumbaseService } from "../lib/services/altumbase/altumbase-service"; import { RightBarCreatorsLeaderboardComponent } from "./right-bar-creators/right-bar-creators-leaderboard/right-bar-creators-leaderboard.component"; import { HttpClient } from "@angular/common/http"; import { FeedComponent } from "./feed/feed.component"; @@ -213,6 +213,8 @@ export class GlobalVarsService { profileUpdateTimestamp: number; jumioDeSoNanos = 0; + jumioUSDCents = 0; + jumioKickbackUSDCents = 0; referralUSDCents: number = 0; @@ -222,7 +224,7 @@ export class GlobalVarsService { buyETHAddress: string = ""; - nodes: { [id: number]: DeSoNode } + nodes: { [id: number]: DeSoNode }; SetupMessages() { // If there's no loggedInUser, we set the notification count to zero @@ -401,9 +403,7 @@ export class GlobalVarsService { } getLinkForReferralHash(referralHash: string) { - // FIXME: Generalize this once there are referral programs running - // on other nodes. - return `https://diamondapp.com?r=${referralHash}`; + return `${window.location.origin}?r=${referralHash}`; } hasUserBlockedCreator(publicKeyBase58Check): boolean { @@ -937,13 +937,13 @@ export class GlobalVarsService { } updateLeaderboard(forceRefresh: boolean = false): void { - const pulseService = new PulseService(this.httpClient, this.backendApi, this); - + const altumbaseService = new AltumbaseService(this.httpClient, this.backendApi, this); if (this.topGainerLeaderboard.length === 0 || forceRefresh) { - pulseService.getDeSoLockedLeaderboard().subscribe((res) => (this.topGainerLeaderboard = res)); + altumbaseService.getDeSoLockedLeaderboard().subscribe((res) => (this.topGainerLeaderboard = res)); } + if (this.topDiamondedLeaderboard.length === 0 || forceRefresh) { - pulseService.getDiamondsReceivedLeaderboard().subscribe((res) => (this.topDiamondedLeaderboard = res)); + altumbaseService.getDiamondsReceivedLeaderboard().subscribe((res) => (this.topDiamondedLeaderboard = res)); } if (this.topCommunityProjectsLeaderboard.length === 0 || forceRefresh) { @@ -1174,7 +1174,7 @@ export class GlobalVarsService { getFreeDESOMessage(): string { return this.referralUSDCents ? this.formatUSD(this.referralUSDCents / 100, 0) - : this.nanosToUSD(this.jumioDeSoNanos, 0); + : this.formatUSD(this.jumioUSDCents / 100, 0); } getReferralUSDCents(): void { @@ -1184,11 +1184,16 @@ export class GlobalVarsService { .GetReferralInfoForReferralHash(environment.verificationEndpointHostname, referralHash) .subscribe((res) => { const referralInfo = res.ReferralInfoResponse.Info; - if ( + const countrySignUpBonus = res.CountrySignUpBonus; + if (!countrySignUpBonus.AllowCustomReferralAmount) { + this.referralUSDCents = countrySignUpBonus.ReferralAmountOverrideUSDCents; + } else if ( res.ReferralInfoResponse.IsActive && (referralInfo.TotalReferrals < referralInfo.MaxReferrals || referralInfo.MaxReferrals == 0) ) { this.referralUSDCents = referralInfo.RefereeAmountUSDCents; + } else { + this.referralUSDCents = countrySignUpBonus.ReferralAmountOverrideUSDCents; } }); } diff --git a/src/app/jumio-status/jumio-status.component.html b/src/app/jumio-status/jumio-status.component.html index b22a9031a..80f03bcef 100644 --- a/src/app/jumio-status/jumio-status.component.html +++ b/src/app/jumio-status/jumio-status.component.html @@ -6,7 +6,7 @@ ⌛
- Your free {{ globalVars.getFreeDESOMessage() }} will arrive in about a minute. + Your free will arrive in about a minute.
⌛ @@ -19,7 +19,7 @@ 💰
- Click here to get your free {{ globalVars.getFreeDESOMessage() }}. + Click here to get your free .
💰 diff --git a/src/app/landing-page/landing-page.component.html b/src/app/landing-page/landing-page.component.html index 6f5688268..69a92d620 100644 --- a/src/app/landing-page/landing-page.component.html +++ b/src/app/landing-page/landing-page.component.html @@ -23,7 +23,7 @@
@@ -33,7 +33,7 @@
@@ -76,7 +76,7 @@

Sign Up

- Get {{ globalVars.getFreeDESOMessage() }} Free + Get Free
diff --git a/src/app/mint-nft-modal/mint-nft-modal.component.html b/src/app/mint-nft-modal/mint-nft-modal.component.html index 8653e023f..5ab4f4352 100644 --- a/src/app/mint-nft-modal/mint-nft-modal.component.html +++ b/src/app/mint-nft-modal/mint-nft-modal.component.html @@ -58,8 +58,13 @@
Put it on sale
- - + +
@@ -78,8 +83,11 @@ [ngStyle]="{'max-width': !globalVars.isMobile() ? '250px' : 'none'}" aria-describedby="usd-label" class="form-control fs-15px text-right d-inline-block" - type="number" min="0" - placeholder="0"/> + type="number" + min="0" + placeholder="0" + [disabled]="!putOnSale" + />
@@ -91,14 +99,76 @@ (ngModelChange)="updateMinBidAmountUSD($event)" aria-describedby="deso-label" class="form-control fs-15px text-right d-inline-block" - type="number" min="0" - placeholder="0"/> + type="number" + min="0" + placeholder="0" + [disabled]="!putOnSale" + />
The minimum bid must be greater than or equal to zero.
+
+
+ Enable "Buy Now" +
+
+ + +
+
+
+
+ Buy Now Price +
+
+
+
+  USD  +
+ +
+
+
+ DESO +
+ +
+
+
+
+ The Buy Now Price must be greater than or equal to the min bid amount. +
@@ -108,31 +178,197 @@
-
- % Creator Royalty +
+ Creator Royalty +
+
+ +
+  %  +
- +
+
+ Creator royalty must be between 0% and 100%
+
+ Coin-holder Royalty +
+
+ +
+  %  +
+
+
+
+ Coin royalty must be between 0% and 100% +
+
- % Coin-holder Royalty + Additional DESO Royalties + +
+ +
+ +
+ + + +
+
+ +
+  %  +
+
+
+
+ Each royalty must be between 0% and 100% +
+
+ Cannot specify an additional royalty to the post creator. Please specify above in the Creator Royalty section. +
+
+
+ Each additional DESO royalties must go to a unique user. +
+
+ +
+
+
+
+ Additional Coin Royalties + +
+ +
+ +
+ + + +
+ +
+ +
+  %  +
+
+
+
+ Each royalty must be between 0% and 100% +
+
+ Cannot specify an additional royalty to the post creator's coin. Please specify above in the Coin-Holder Royalty section. +
+
+ Additional Coin Royalties may only be specified for public key's that already have a profile. +
+
+
+ Each additional coin royalties must go to a unique creator. +
+
+
-
- The sum of creator and coin-holder royalties must be less than 100. + The sum of creator and coin-holder and additional royalties must be less than 100.
On every sale, including resale, a customizable percentage goes to you, the creator, and to your @@ -150,8 +386,13 @@
Enable Unlockable Content
- - + +
@@ -168,7 +409,8 @@
+
+ + + + +
+
Choose the serial number you wish to burn.
+
+ +
+
+
+ There are no serial numbers available for you to bid on. +
+ + diff --git a/src/app/nft-burn-modal/nft-burn-modal.component.ts b/src/app/nft-burn-modal/nft-burn-modal.component.ts new file mode 100644 index 000000000..c0184d3c6 --- /dev/null +++ b/src/app/nft-burn-modal/nft-burn-modal.component.ts @@ -0,0 +1,151 @@ +import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; +import { GlobalVarsService } from "../global-vars.service"; +import { BackendApiService, NFTEntryResponse, PostEntryResponse } from "../backend-api.service"; +import { Router } from "@angular/router"; +import { isNumber } from "lodash"; +import { ToastrService } from "ngx-toastr"; +import {BsModalRef, BsModalService} from "ngx-bootstrap/modal"; +import { Location } from "@angular/common"; +import { SwalHelper } from "../../lib/helpers/swal-helper"; + +@Component({ + selector: "nft-burn-modal", + templateUrl: "./nft-burn-modal.component.html", +}) +export class NftBurnModalComponent implements OnInit { + static PAGE_SIZE = 50; + static BUFFER_SIZE = 10; + static WINDOW_VIEWPORT = false; + static PADDING = 0.5; + + @Input() postHashHex: string; + @Input() post: PostEntryResponse; + @Input() burnNFTEntryResponses: NFTEntryResponse[]; + @Output() closeModal = new EventEmitter(); + @Output() changeTitle = new EventEmitter(); + + bidAmountDeSo: number; + bidAmountUSD: number; + selectedSerialNumber: NFTEntryResponse = null; + availableCount: number; + availableSerialNumbers: NFTEntryResponse[]; + filteredSerialNumbers: NFTEntryResponse[]; + highBid: number = null; + lowBid: number = null; + loading = true; + isSelectingSerialNumber = true; + saveSelectionDisabled = false; + showSelectedSerialNumbers = false; + burningNft: boolean = false; + errors: string[] = []; + minBidCurrency: string = "USD"; + minBidInput: number = 0; + transferringUser: string; + + constructor( + public globalVars: GlobalVarsService, + private backendApi: BackendApiService, + private modalService: BsModalService, + private router: Router, + private toastr: ToastrService, + private location: Location, + public bsModalRef: BsModalRef + ) {} + + ngOnInit(): void { + this.backendApi + .GetNFTCollectionSummary( + this.globalVars.localNode, + this.globalVars.loggedInUser.PublicKeyBase58Check, + this.post.PostHashHex + ) + .subscribe((res) => { + this.availableSerialNumbers = Object.values(res.SerialNumberToNFTEntryResponse); + this.availableCount = res.NFTCollectionResponse.PostEntryResponse.NumNFTCopiesForSale; + this.filteredSerialNumbers = this.burnNFTEntryResponses; + }) + .add(() => (this.loading = false)); + } + + burnNft() { + this.saveSelectionDisabled = true; + this.burningNft = true; + SwalHelper.fire({ + target: this.globalVars.getTargetComponentSelector(), + title: "Burn NFT", + html: `You are about to burn this NFT - this cannot be undone. Are you sure?`, + showConfirmButton: true, + showCancelButton: true, + reverseButtons: true, + customClass: { + confirmButton: "btn btn-light", + cancelButton: "btn btn-light no", + }, + confirmButtonText: "Ok", + cancelButtonText: "Cancel", + }).then((res) => { + if (res.isConfirmed) { + this.backendApi + .BurnNFT( + this.globalVars.localNode, + this.globalVars.loggedInUser.PublicKeyBase58Check, + this.post.PostHashHex, + this.selectedSerialNumber.SerialNumber, + this.globalVars.defaultFeeRateNanosPerKB + ) + .subscribe( + (res) => { + this.modalService.setDismissReason("nft burned"); + this.bsModalRef.hide(); + this.toastr.show("Your nft was burned", null, { + toastClass: "info-toast", + positionClass: "toast-bottom-center", + }); + }, + (err) => { + console.error(err); + } + ) + .add(() => { + this.burningNft = false; + this.saveSelectionDisabled = false; + }); + } else { + this.burningNft = false; + this.saveSelectionDisabled = false; + } + }); + } + + saveSelection(): void { + if (!this.saveSelectionDisabled) { + this.isSelectingSerialNumber = false; + this.showSelectedSerialNumbers = true; + this.changeTitle.emit("Confirm Transfer"); + this.highBid = this.selectedSerialNumber.HighestBidAmountNanos; + this.lowBid = this.selectedSerialNumber.LowestBidAmountNanos; + } + } + + goBackToSerialSelection(): void { + this.isSelectingSerialNumber = true; + this.showSelectedSerialNumbers = false; + this.changeTitle.emit("Choose an edition"); + this.highBid = null; + this.lowBid = null; + this.selectedSerialNumber = null; + } + + selectSerialNumber(serialNumber: NFTEntryResponse) { + this.selectedSerialNumber = serialNumber; + this.saveSelection(); + } + + bidAmountUSDFormatted() { + return isNumber(this.bidAmountUSD) ? `~${this.globalVars.formatUSD(this.bidAmountUSD, 0)}` : ""; + } + + bidAmountDeSoFormatted() { + return isNumber(this.bidAmountDeSo) ? `~${this.bidAmountDeSo.toFixed(2)} $DESO` : ""; + } +} diff --git a/src/app/nft-post-page/nft-post/nft-post.component.html b/src/app/nft-post-page/nft-post/nft-post.component.html index e6780c6fe..1f6add424 100644 --- a/src/app/nft-post-page/nft-post/nft-post.component.html +++ b/src/app/nft-post-page/nft-post/nft-post.component.html @@ -11,8 +11,8 @@
- -
+ +
@@ -215,6 +216,7 @@
+
Pending
{{ globalVars.nanosToUSD(owner.ProfileEntryResponse.CoinPriceDeSoNanos, 2) }}
diff --git a/src/app/nft-post-page/nft-post/nft-post.component.ts b/src/app/nft-post-page/nft-post/nft-post.component.ts index 8710dadfa..54a79068e 100644 --- a/src/app/nft-post-page/nft-post/nft-post.component.ts +++ b/src/app/nft-post-page/nft-post/nft-post.component.ts @@ -102,6 +102,7 @@ export class NftPostComponent { } refreshPosts() { + this.loading = true; // Fetch the post entry this.getPost().subscribe( (res) => { @@ -160,6 +161,9 @@ export class NftPostComponent { if (!this.nftBidData.BidEntryResponses) { this.nftBidData.BidEntryResponses = []; } + if (!this.nftBidData.NFTEntryResponses) { + this.nftBidData.NFTEntryResponses = []; + } this.availableSerialNumbers = this.nftBidData.NFTEntryResponses.filter( (nftEntryResponse) => nftEntryResponse.IsForSale ); diff --git a/src/app/nft-select-serial-number/nft-select-serial-number.component.html b/src/app/nft-select-serial-number/nft-select-serial-number.component.html new file mode 100644 index 000000000..64bbd8227 --- /dev/null +++ b/src/app/nft-select-serial-number/nft-select-serial-number.component.html @@ -0,0 +1,100 @@ +
+
+ Number + +
+
+ Buy Now + +
+ +
+ Highest Bid + +
+
+ Min Bid Amount + +
+
+
+
+
+ + #{{ nft.SerialNumber }} +
+
+ +
+
+
+
{{ globalVars.nanosToDeSo(nft.HighestBidAmountNanos) }} DESO
+
( ~{{globalVars.nanosToUSD(nft.HighestBidAmountNanos, 2) }})
+
+
+
+
+
{{ globalVars.nanosToDeSo(nft.MinBidAmountNanos) }} DESO
+
(~{{ globalVars.nanosToUSD(nft.MinBidAmountNanos, 2) }})
+
+
+
+
diff --git a/src/app/nft-select-serial-number/nft-select-serial-number.component.ts b/src/app/nft-select-serial-number/nft-select-serial-number.component.ts new file mode 100644 index 000000000..edc049ed2 --- /dev/null +++ b/src/app/nft-select-serial-number/nft-select-serial-number.component.ts @@ -0,0 +1,68 @@ +import { Component, Input, Output, EventEmitter, OnInit } from "@angular/core"; +import { GlobalVarsService } from "../global-vars.service"; +import { BackendApiService, NFTEntryResponse } from "../backend-api.service"; +import * as _ from "lodash"; + +@Component({ + selector: "nft-select-serial-number", + templateUrl: "./nft-select-serial-number.component.html", +}) +export class NftSelectSerialNumberComponent implements OnInit { + static PAGE_SIZE = 50; + static BUFFER_SIZE = 10; + static WINDOW_VIEWPORT = false; + static PADDING = 0.5; + + @Input() serialNumbers: NFTEntryResponse[]; + @Input() showBuyNow: boolean = false; + @Input() postHashHex: string; + @Output() serialNumberSelected = new EventEmitter(); + @Output() nftPurchased = new EventEmitter(); + + SN_FIELD = "SerialNumber"; + HIGH_BID_FIELD = "HighestBidAmountNanos"; + MIN_BID_FIELD = "MinBidAmountNanos"; + BUY_NOW_FIELD = "BuyNowPriceNanos"; + + selectedSerialNumber: NFTEntryResponse = null; + sortedSerialNumbers: NFTEntryResponse[]; + sortByField = this.SN_FIELD; + sortByOrder: "desc" | "asc" = "desc"; + + constructor(public globalVars: GlobalVarsService, private backendApi: BackendApiService) {} + + ngOnInit() { + this.updateBidSort(this.SN_FIELD); + } + + selectSerialNumber(idx: number) { + this.selectedSerialNumber = this.serialNumbers.find((sn) => sn.SerialNumber === idx); + this.serialNumberSelected.emit(this.selectedSerialNumber); + } + + updateBidSort(sortField: string) { + if (this.sortByField === sortField) { + this.sortByOrder = this.sortByOrder === "asc" ? "desc" : "asc"; + } else { + this.sortByOrder = "asc"; + } + this.sortByField = sortField; + this.sortedSerialNumbers = _.orderBy(this.serialNumbers, [this.sortByField], [this.sortByOrder]); + } + + buyNow(event, nft: NFTEntryResponse): void { + event.stopPropagation(); + this.backendApi + .CreateNFTBid( + this.globalVars.localNode, + this.globalVars.loggedInUser?.PublicKeyBase58Check, + this.postHashHex, + nft.SerialNumber, + nft.BuyNowPriceNanos, + this.globalVars.defaultFeeRateNanosPerKB + ) + .subscribe((res) => { + this.nftPurchased.emit(); + }); + } +} diff --git a/src/app/notifications-page/notifications-list/notifications-list.component.ts b/src/app/notifications-page/notifications-list/notifications-list.component.ts index 7db4a3018..b6b63662b 100644 --- a/src/app/notifications-page/notifications-list/notifications-list.component.ts +++ b/src/app/notifications-page/notifications-list/notifications-list.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from "@angular/core"; import { GlobalVarsService } from "../../global-vars.service"; -import { BackendApiService, PostEntryResponse } from "../../backend-api.service"; +import { BackendApiService, NFTEntryResponse, PostEntryResponse } from "../../backend-api.service"; import { Datasource, IAdapter, IDatasource } from "ngx-ui-scroll"; import * as _ from "lodash"; import { AppRoutingModule } from "../../app-routing.module"; @@ -114,14 +114,32 @@ export class NotificationsListComponent { // We map everything to an easy-to-use object so the template // doesn't have to do any hard work - const result = { + const result: { + actor: string; + category: string | null; + icon: string | null; + iconClass: string | null; + action: string | null; + actionDetails: string | null; + post: PostEntryResponse; + parentPost: PostEntryResponse; + link: string; + bidInfo: any | null; + comment: string | null; + nftEntryResponses: NFTEntryResponse[]; + } = { actor, // who created the notification icon: null, + category: null, // category used for filtering + iconClass: null, action: null, // the action they took + actionDetails: null, // Summarized details of the action for compact mode post: null, // the post involved parentPost: null, // the parent post involved link: AppRoutingModule.profilePath(actor.Username), bidInfo: null, + comment: null, // the text of the comment + nftEntryResponses: null, // NFT Entry Responses, for transfers }; if (txnMeta.TxnType === "BASIC_TRANSFER") { @@ -277,28 +295,66 @@ export class NotificationsListComponent { result.link = AppRoutingModule.postPath(postHash); return result; - } else if (txnMeta.TxnType == "NFT_BID") { + } else if (txnMeta.TxnType === "NFT_BID") { const nftBidMeta = txnMeta.NFTBidTxindexMetadata; if (!nftBidMeta) { return null; } const postHash = nftBidMeta.NFTPostHashHex; - const actorName = actor.Username !== "anonymous" ? actor.Username : txnMeta.TransactorPublicKeyBase58Check; - result.post = this.postMap[postHash]; - result.action = nftBidMeta.BidAmountNanos - ? `${actorName} bid ${this.globalVars.nanosToDeSo( - nftBidMeta.BidAmountNanos, - 2 - )} DESO (~${this.globalVars.nanosToUSD(nftBidMeta.BidAmountNanos, 2)}) for serial number ${ - nftBidMeta.SerialNumber - }` - : `${actorName} cancelled their bid on serial number ${nftBidMeta.SerialNumber}`; - result.icon = nftBidMeta.BidAmountNanos ? "fas fa-dollar-sign fc-blue" : "fas fa-dollar-sign fc-red"; result.bidInfo = { SerialNumber: nftBidMeta.SerialNumber, BidAmountNanos: nftBidMeta.BidAmountNanos }; - return result; - } else if (txnMeta.TxnType == "ACCEPT_NFT_BID") { + result.post = this.postMap[postHash]; + if ( + nftBidMeta.IsBuyNowBid && + this.globalVars.loggedInUser?.PublicKeyBase58Check === nftBidMeta.OwnerPublicKeyBase58check + ) { + result.action = `${actorName} bought serial number ${nftBidMeta.SerialNumber} for ${this.globalVars.nanosToDeSo( + nftBidMeta.BidAmountNanos, + 2 + )} DESO (~${this.globalVars.nanosToUSD(nftBidMeta.BidAmountNanos, 2)})`; + result.icon = "fas fa-cash-register fc-green"; + return result; + } else if (this.globalVars.loggedInUser?.PublicKeyBase58Check === nftBidMeta.OwnerPublicKeyBase58check) { + result.action = nftBidMeta.BidAmountNanos + ? `${actorName} bid ${this.globalVars.nanosToDeSo( + nftBidMeta.BidAmountNanos, + 2 + )} DESO (~${this.globalVars.nanosToUSD(nftBidMeta.BidAmountNanos, 2)}) for serial number ${ + nftBidMeta.SerialNumber + }` + : `${actorName} cancelled their bid on serial number ${nftBidMeta.SerialNumber}`; + result.icon = nftBidMeta.BidAmountNanos ? "fas fa-dollar-sign fc-blue" : "fas fa-dollar-sign fc-red"; + return result; + } else { + const additionalCoinRoyaltiesMap: { [k: string]: number } = nftBidMeta.AdditionalCoinRoyaltiesMap || {}; + const additionalDESORoyaltiesMap: { [k: string]: number } = nftBidMeta.AdditionalDESORoyaltiesMap || {}; + if ( + this.globalVars.loggedInUser?.PublicKeyBase58Check in additionalCoinRoyaltiesMap || + this.globalVars.loggedInUser?.PublicKeyBase58Check in additionalDESORoyaltiesMap + ) { + const additionalCoinRoyalty = additionalCoinRoyaltiesMap[this.globalVars.loggedInUser?.PublicKeyBase58Check]; + const coinRoyaltyStr = additionalCoinRoyalty + ? `a royalty of ${this.globalVars.nanosToDeSo(additionalCoinRoyalty)} (~${this.globalVars.nanosToUSD( + additionalCoinRoyalty + )}) DESO to your creator coin` + : ""; + const additionalDESORoyalty = additionalDESORoyaltiesMap[this.globalVars.loggedInUser?.PublicKeyBase58Check]; + const desoRoyaltyStr = additionalDESORoyalty + ? `a royalty of ${this.globalVars.nanosToDeSo(additionalDESORoyalty)} (~${this.globalVars.nanosToUSD( + additionalDESORoyalty + )}) DESO to your wallet` + : ""; + result.action = `${actor.Username} bought an NFT that generated ${desoRoyaltyStr}${ + desoRoyaltyStr && coinRoyaltyStr && " and " + }${coinRoyaltyStr}`; + result.icon = "fas fa-hand-holding-usd fc-green"; + return result; + } else { + return null; + } + } + } else if (txnMeta.TxnType === "ACCEPT_NFT_BID") { const acceptNFTBidMeta = txnMeta.AcceptNFTBidTxindexMetadata; if (!acceptNFTBidMeta) { return null; @@ -307,13 +363,107 @@ export class NotificationsListComponent { const postHash = acceptNFTBidMeta.NFTPostHashHex; result.post = this.postMap[postHash]; - result.action = `${actor.Username} accepted your bid of ${this.globalVars.nanosToDeSo( - acceptNFTBidMeta.BidAmountNanos, - 2 - )} for serial number ${acceptNFTBidMeta.SerialNumber}`; - result.icon = "fas fa-trophy"; - result.bidInfo = { SerialNumber: acceptNFTBidMeta.SerialNumber, BidAmountNanos: acceptNFTBidMeta.BidAmountNanos }; + const additionalCoinRoyaltiesMap: { [k: string]: number } = acceptNFTBidMeta.AdditionalCoinRoyaltiesMap || {}; + const additionalDESORoyaltiesMap: { [k: string]: number } = acceptNFTBidMeta.AdditionalDESORoyaltiesMap || {}; + if ( + this.globalVars.loggedInUser?.PublicKeyBase58Check in additionalCoinRoyaltiesMap || + this.globalVars.loggedInUser?.PublicKeyBase58Check in additionalDESORoyaltiesMap + ) { + const additionalCoinRoyalty = additionalCoinRoyaltiesMap[this.globalVars.loggedInUser?.PublicKeyBase58Check]; + const coinRoyaltyStr = additionalCoinRoyalty + ? `a royalty of ${this.globalVars.nanosToDeSo(additionalCoinRoyalty)} (~${this.globalVars.nanosToUSD( + additionalCoinRoyalty + )}) to your creator coin` + : ""; + const additionalDESORoyalty = additionalDESORoyaltiesMap[this.globalVars.loggedInUser?.PublicKeyBase58Check]; + const desoRoyaltyStr = additionalDESORoyalty + ? `a royalty of ${this.globalVars.nanosToDeSo(additionalDESORoyalty)} (~${this.globalVars.nanosToUSD( + additionalDESORoyalty + )}) to your wallet` + : ""; + result.action = `${actor.Username} accepted a bid on an NFT that generated ${desoRoyaltyStr}${ + desoRoyaltyStr && coinRoyaltyStr && " and " + }${coinRoyaltyStr}`; + result.icon = "fas fa-hand-holding-usd fc-green"; + return result; + } else { + result.action = `${actor.Username} accepted your bid of ${this.globalVars.nanosToDeSo( + acceptNFTBidMeta.BidAmountNanos, + 2 + )} for serial number ${acceptNFTBidMeta.SerialNumber}`; + result.icon = "fas fa-trophy fc-gold"; + result.bidInfo = { + SerialNumber: acceptNFTBidMeta.SerialNumber, + BidAmountNanos: acceptNFTBidMeta.BidAmountNanos, + }; + return result; + } + } else if (txnMeta.TxnType == "NFT_TRANSFER") { + const nftTransferMeta = txnMeta.NFTTransferTxindexMetadata; + if (!nftTransferMeta) { + return null; + } + + const postHash = nftTransferMeta.NFTPostHashHex; + + const actorName = actor.Username !== "anonymous" ? actor.Username : txnMeta.TransactorPublicKeyBase58Check; + result.post = this.postMap[postHash]; + result.action = `${actorName} transferred you an NFT`; + result.icon = "fas fa-paper-plane fc-blue"; return result; + } else if (txnMeta.TxnType === "CREATE_NFT") { + const createNFTMeta = txnMeta.CreateNFTTxindexMetadata; + if (!createNFTMeta) { + return null; + } + createNFTMeta.AdditionalDESORoyaltiesMap = createNFTMeta.AdditionalDESORoyaltiesMap || {}; + createNFTMeta.AdditionalCoinRoyaltiesMap = createNFTMeta.AdditionalCoinRoyaltiesMap || {}; + const additionalCoinRoyalty = + createNFTMeta.AdditionalCoinRoyaltiesMap[this.globalVars.loggedInUser?.PublicKeyBase58Check]; + const coinRoyaltyStr = additionalCoinRoyalty + ? `a royalty of ${additionalCoinRoyalty / 100}% to your creator coin` + : ""; + const additionalDESORoyalty = + createNFTMeta.AdditionalDESORoyaltiesMap[this.globalVars.loggedInUser?.PublicKeyBase58Check]; + const desoRoyaltyStr = additionalDESORoyalty ? `a royalty of ${additionalDESORoyalty / 100}% to your wallet` : ""; + if (!coinRoyaltyStr && !desoRoyaltyStr) { + return null; + } + result.action = `${actorName} minted an NFT and gave ${desoRoyaltyStr}${ + coinRoyaltyStr && desoRoyaltyStr && " and " + }${coinRoyaltyStr}`; + result.icon = "fas fa-percentage fc-green"; + result.post = this.postMap[createNFTMeta.NFTPostHashHex]; + return result; + } else if (txnMeta.TxnType === "UPDATE_NFT") { + const updateNFTMeta = txnMeta.UpdateNFTTxindexMetadata; + if (!updateNFTMeta || !updateNFTMeta.IsForSale) { + return null; + } + result.post = this.postMap[updateNFTMeta.NFTPostHashHex]; + result.icon = "fas fa-tags fc-green"; + if (result.post.PosterPublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check) { + result.action = `${actorName} put your NFT on sale`; + return result; + } else { + const additionalDESORoyaltiesMap = result.post.AdditionalDESORoyaltiesMap || {}; + const additionalCoinRoyaltiesMap = result.post.AdditionalCoinRoyaltiesMap || {}; + const additionalCoinRoyalty = additionalCoinRoyaltiesMap[this.globalVars.loggedInUser?.PublicKeyBase58Check]; + const coinRoyaltyStr = additionalCoinRoyalty + ? `a royalty of ${additionalCoinRoyalty / 100}% to your creator coin` + : ""; + const additionalDESORoyalty = additionalDESORoyaltiesMap[this.globalVars.loggedInUser?.PublicKeyBase58Check]; + const desoRoyaltyStr = additionalDESORoyalty + ? `a royalty of ${additionalDESORoyalty / 100}% to your wallet` + : ""; + if (!coinRoyaltyStr && !desoRoyaltyStr) { + return null; + } + result.action = `${actorName} put an NFT on sale - you receive ${desoRoyaltyStr}${ + desoRoyaltyStr && coinRoyaltyStr && " and " + }${coinRoyaltyStr} on the sale`; + return result; + } } // If we don't recognize the transaction type we return null diff --git a/src/app/place-bid-modal/place-bid-modal.component.html b/src/app/place-bid-modal/place-bid-modal.component.html index 6de75421b..8a6695b0f 100644 --- a/src/app/place-bid-modal/place-bid-modal.component.html +++ b/src/app/place-bid-modal/place-bid-modal.component.html @@ -27,6 +27,11 @@  |  Coin-holder Royalty {{ post.NFTRoyaltyToCoinBasisPoints / 100 }}%
+
+ Additional Creator Royalties {{ sumAdditionalRoyaltiesBasisPoints(post.AdditionalDESORoyaltiesMap) / 100 }}% +  |  + Additional Coin-Holder Royalties {{ sumAdditionalRoyaltiesBasisPoints(post.AdditionalCoinRoyaltiesMap) / 100 }}% +
-
-
- Serial Number -
-
- Highest Bid -
-
- Min Bid Amount -
-
-
-
-
- #{{nft.SerialNumber}} -
-
-
-
{{globalVars.nanosToDeSo(nft.HighestBidAmountNanos)}} DESO
-
(~{{globalVars.nanosToUSD(nft.HighestBidAmountNanos, 2)}})
-
-
-
-
-
{{ globalVars.nanosToDeSo(nft.MinBidAmountNanos) }} DESO
-
(~{{globalVars.nanosToUSD(nft.MinBidAmountNanos, 2)}})
-
-
-
-
+
diff --git a/src/app/place-bid-modal/place-bid-modal.component.ts b/src/app/place-bid-modal/place-bid-modal.component.ts index 4a32fdc22..ab10e6ee0 100644 --- a/src/app/place-bid-modal/place-bid-modal.component.ts +++ b/src/app/place-bid-modal/place-bid-modal.component.ts @@ -76,21 +76,25 @@ export class PlaceBidModalComponent implements OnInit { setErrors(): void { const bidAmountExceedsBalance = this.bidAmountDESO * 1e9 > this.globalVars.loggedInUser.BalanceNanos; - this.errors = !this.bidAmountDESO && this.selectedSerialNumber.MinBidAmountNanos === 0 ? "You must bid more than 0 DESO.\n\n" : ""; + this.errors = + !this.bidAmountDESO && this.selectedSerialNumber.MinBidAmountNanos === 0 + ? "You must bid more than 0 DESO.\n\n" + : ""; this.errors += !this.selectedSerialNumber ? "You must select an edition to bid.\n\n" : ""; - this.errors += bidAmountExceedsBalance - ? `You do not have ${this.bidAmountDESO} DESO to fulfill this bid.\n\n` - : ""; + this.errors += bidAmountExceedsBalance ? `You do not have ${this.bidAmountDESO} DESO to fulfill this bid.\n\n` : ""; this.errors += this.selectedSerialNumber?.MinBidAmountNanos > this.bidAmountDESO * 1e9 - ? `Your bid of ${ - this.bidAmountDESO - } does not meet the minimum bid requirement of ${this.globalVars.nanosToDeSo( + ? `Your bid of ${this.bidAmountDESO} does not meet the minimum bid requirement of ${this.globalVars.nanosToDeSo( this.selectedSerialNumber.MinBidAmountNanos )} DESO (${this.globalVars.nanosToUSD(this.selectedSerialNumber.MinBidAmountNanos, 2)})\n\n` : ""; } + nftPurchasedHandler() { + this.modalService.setDismissReason("nft purchased"); + this.bsModalRef.hide(); + } + placeBid() { this.setErrors(); if (this.errors) { @@ -109,12 +113,8 @@ export class PlaceBidModalComponent implements OnInit { ) .subscribe( (res) => { - // Hide this modal and open the next one. - this.bsModalRef.hide(); - this.modalService.show(BidPlacedModalComponent, { - class: "modal-dialog-centered modal-sm", - }); this.modalService.setDismissReason("bid placed"); + this.bsModalRef.hide(); }, (err) => { console.error(err); @@ -142,11 +142,15 @@ export class PlaceBidModalComponent implements OnInit { } } - selectSerialNumber(idx: number) { - this.selectedSerialNumber = this.availableSerialNumbers.find((sn) => sn.SerialNumber === idx); + selectSerialNumber(serialNumber: NFTEntryResponse) { + this.selectedSerialNumber = serialNumber; this.saveSelection(); } + sumAdditionalRoyaltiesBasisPoints(royalties: { [k: string]: number }): number { + return Object.values(royalties).reduce((prevVal, val) => prevVal + val); + } + deselectSerialNumber() { if (this.placingBids) { return; diff --git a/src/app/referral-program-mgr/referral-program-mgr.component.ts b/src/app/referral-program-mgr/referral-program-mgr.component.ts index 59092a8e8..ef06fb9b4 100644 --- a/src/app/referral-program-mgr/referral-program-mgr.component.ts +++ b/src/app/referral-program-mgr/referral-program-mgr.component.ts @@ -200,24 +200,12 @@ export class ReferralProgramMgrComponent implements OnInit { return; } - // Process the file. The CSV has a simple, expected input format so we can parse it manually. - fileToUpload.text().then((text) => { - let rowStrings = text.split("\n"); - let rows = []; - for (let ii = 0; ii < rowStrings.length; ii++) { - if (rowStrings[ii].length == 0) { - break; - } - let row = rowStrings[ii].split(","); - rows.push(row); - } - this._uploadCSVRows(rows); - }); + this._uploadCSV(fileToUpload); } - _uploadCSVRows(csvRows: Array>) { + _uploadCSV(file: File) { this.backendApi - .AdminUploadReferralCSV(this.globalVars.localNode, this.globalVars.loggedInUser.PublicKeyBase58Check, csvRows) + .AdminUploadReferralCSV(this.globalVars.localNode, this.globalVars.loggedInUser.PublicKeyBase58Check, file) .subscribe( (res) => { this.globalVars._alertSuccess( diff --git a/src/app/referrals/referrals.component.html b/src/app/referrals/referrals.component.html index 9b3542ebe..808324ff6 100644 --- a/src/app/referrals/referrals.component.html +++ b/src/app/referrals/referrals.component.html @@ -34,7 +34,8 @@
This link is no longer active.
- You get ${{ referral.Info.ReferrerAmountUSDCents / 100 }} USD per referral + You get ${{ referral.Info.ReferrerAmountUSDCents / 100 }} USD per referral +
Total: @@ -43,7 +44,8 @@
- Your referrals get ${{ referral.Info.RefereeAmountUSDCents / 100 }} USD + Your referrals get ${{ referral.Info.RefereeAmountUSDCents / 100 }} USD +
Total: @@ -53,7 +55,7 @@
Referrals remaining: - {{ referral.Info.MaxReferrals > 0 ? + {{ referral.Info.MaxReferrals > 0 ? referral.Info.MaxReferrals - referral.Info.TotalReferrals : 'Unlimited' }} diff --git a/src/app/right-bar-creators/right-bar-creators-leaderboard/right-bar-creators-leaderboard.component.ts b/src/app/right-bar-creators/right-bar-creators-leaderboard/right-bar-creators-leaderboard.component.ts index 9cb0a1b5f..4e866f16c 100644 --- a/src/app/right-bar-creators/right-bar-creators-leaderboard/right-bar-creators-leaderboard.component.ts +++ b/src/app/right-bar-creators/right-bar-creators-leaderboard/right-bar-creators-leaderboard.component.ts @@ -1,11 +1,7 @@ import { Component, Input, OnInit } from "@angular/core"; import { GlobalVarsService } from "../../global-vars.service"; import { ActivatedRoute, Router } from "@angular/router"; -import { BackendApiService } from "../../backend-api.service"; import { RightBarCreatorsComponent } from "../right-bar-creators.component"; -import { HttpClient } from "@angular/common/http"; -import { PulseService } from "../../../lib/services/pulse/pulse-service"; -import { BithuntService } from "../../../lib/services/bithunt/bithunt-service"; @Component({ selector: "right-bar-creators-leaderboard", diff --git a/src/app/right-bar-creators/right-bar-creators.component.ts b/src/app/right-bar-creators/right-bar-creators.component.ts index 702dfd93d..ce6fb4bb1 100644 --- a/src/app/right-bar-creators/right-bar-creators.component.ts +++ b/src/app/right-bar-creators/right-bar-creators.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnInit } from "@angular/core"; import { GlobalVarsService } from "../global-vars.service"; import { BackendApiService } from "../backend-api.service"; import { Router } from "@angular/router"; +import { environment } from "../../environments/environment"; export class RightBarTabOption { name: string; @@ -32,12 +33,12 @@ export class RightBarCreatorsComponent implements OnInit { static GAINERS: RightBarTabOption = { name: "Top Daily Gainers", width: 175, - poweredBy: { name: "Bitclout Pulse", link: "https://desopulse.com" }, + poweredBy: { name: "Altumbase", link: `https://altumbase.com/tools?${environment.node.name}` }, }; static DIAMONDS: RightBarTabOption = { name: "Top Daily Diamonded Creators", width: 275, - poweredBy: { name: "Bitclout Pulse", link: "https://desopulse.com" }, + poweredBy: { name: "Altumbase", link: `https://altumbase.com/tools?${environment.node.name}` }, }; static COMMUNITY: RightBarTabOption = { name: "Top Community Projects", diff --git a/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.html b/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.html new file mode 100644 index 000000000..1238e1972 --- /dev/null +++ b/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.html @@ -0,0 +1,3 @@ + + + diff --git a/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.scss b/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.spec.ts b/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.spec.ts new file mode 100644 index 000000000..8068498ba --- /dev/null +++ b/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; + +import { SupplyMonitoringStatsPageComponent } from "./supply-monitoring-stats-page.component"; + +describe("SupplyMonitoringStatsPageComponent", () => { + let component: SupplyMonitoringStatsPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [SupplyMonitoringStatsPageComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SupplyMonitoringStatsPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.ts b/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.ts new file mode 100644 index 000000000..e26f4aab0 --- /dev/null +++ b/src/app/supply-monitoring-stats-page/supply-monitoring-stats-page.component.ts @@ -0,0 +1,10 @@ +import { Component, OnInit } from "@angular/core"; + +@Component({ + selector: "supply-monitoring-stats-page", + templateUrl: "./supply-monitoring-stats-page.component.html", + styleUrls: ["./supply-monitoring-stats-page.component.scss"], +}) +export class SupplyMonitoringStatsPageComponent { + constructor() {} +} diff --git a/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.html b/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.html new file mode 100644 index 000000000..6576cbe6b --- /dev/null +++ b/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.html @@ -0,0 +1,30 @@ +
+ +
Total Supply: {{ loadingTotalSupply ? "...loading" : totalSupplyDESO }}
+
Count of Public Keys Holding DESO: {{ loadingCountKeysWithDESO ? "...loading" : countKeysWithDESO }}
+
+
+
Rank
+
Public Key
+
Balance (DESO)
+
Percentage
+
USD Value
+
+
+
#{{ ii + 1}}
+
+
{{ entry.PublicKeyBase58Check }}
+
+
{{ globalVars.nanosToDeSo(entry.BalanceNanos) }}
+
{{ (entry.Percentage * 100).toFixed(2) }} %
+
{{ globalVars.nanosToUSD(entry.BalanceNanos, 2) }}
+
+
+
+
+
+ This node does not support monitoring of supply statistics. +
+
diff --git a/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.scss b/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.spec.ts b/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.spec.ts new file mode 100644 index 000000000..4ecb3dd68 --- /dev/null +++ b/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; + +import { SupplyMonitoringStatsComponent } from "./supply-monitoring-stats.component"; + +describe("SupplyMonitoringStatsComponent", () => { + let component: SupplyMonitoringStatsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [SupplyMonitoringStatsComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SupplyMonitoringStatsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.ts b/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.ts new file mode 100644 index 000000000..ec13b45f7 --- /dev/null +++ b/src/app/supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component.ts @@ -0,0 +1,89 @@ +import { Component } from "@angular/core"; +import { BackendApiService, RichListEntryResponse } from "../../backend-api.service"; +import { GlobalVarsService } from "../../global-vars.service"; +import { Datasource, IAdapter, IDatasource } from "ngx-ui-scroll"; + +@Component({ + selector: "supply-monitoring-stats", + templateUrl: "./supply-monitoring-stats.component.html", + styleUrls: ["./supply-monitoring-stats.component.scss"], +}) +export class SupplyMonitoringStatsComponent { + static BUFFER_SIZE = 10; + static PAGE_SIZE = 10; + static WINDOW_VIEWPORT = true; + totalSupplyDESO: number; + loadingTotalSupply: boolean = true; + failedLoadingTotalSupply: boolean = false; + richList: RichListEntryResponse[]; + loadingRichList: boolean = true; + failedLoadingRichList: boolean = false; + countKeysWithDESO: number; + loadingCountKeysWithDESO: boolean = false; + failedLoadingCountKeysWithDESO: boolean = false; + noSupplyMonitoring: boolean = false; + datasource: IDatasource> = this.getDatasource(); + constructor(public globalVars: GlobalVarsService, private backendApi: BackendApiService) { + this.backendApi + .GetRichList(this.globalVars.localNode) + .subscribe( + (res) => { + this.richList = res || []; + }, + (err) => { + this.failedLoadingRichList = true; + } + ) + .add(() => (this.loadingRichList = false)); + this.backendApi + .GetTotalSupply(this.globalVars.localNode) + .subscribe( + (res) => { + this.totalSupplyDESO = res; + }, + (err) => { + this.failedLoadingTotalSupply = true; + } + ) + .add(() => (this.loadingTotalSupply = false)); + + this.backendApi + .GetCountOfKeysWithDESO(this.globalVars.localNode) + .subscribe( + (res) => { + this.countKeysWithDESO = res; + }, + (err) => { + this.failedLoadingCountKeysWithDESO = true; + } + ) + .add(() => (this.loadingCountKeysWithDESO = false)); + } + + getDatasource(): IDatasource> { + return new Datasource({ + get: (index, count, success) => { + const startIdx = Math.max(index, 0); + const endIdx = index + count - 1; + if (startIdx > endIdx) { + success([]); + return; + } + if (endIdx + 1 > this.richList.length) { + success(this.richList.slice(startIdx, this.richList.length) as any[]); + return; + } + success(this.richList.slice(startIdx, endIdx + 1) as any[]); + return; + }, + settings: { + startIndex: 0, + minIndex: 0, + bufferSize: 5, + padding: 0.5, + windowViewport: true, + infinite: true, + }, + }); + } +} diff --git a/src/app/theme/themes/cake.scss b/src/app/theme/themes/cake.scss index 5baf1443d..e3ab5e080 100644 --- a/src/app/theme/themes/cake.scss +++ b/src/app/theme/themes/cake.scss @@ -1,5 +1,5 @@ // Cake Theme -// By: @brixboston100 +// By: @brixboston100 .cake[app-theme] { --background: #f8edeb; --text: #47205c; @@ -19,6 +19,7 @@ --cblue: #457b9d; --cred: #ff3298; --cgreen: #69ab4f; + --cgold: #FFD700; --button: #009aff; --loading: #d2a59d; } diff --git a/src/app/theme/themes/dark.scss b/src/app/theme/themes/dark.scss index 826cf6409..a1fe0ebee 100644 --- a/src/app/theme/themes/dark.scss +++ b/src/app/theme/themes/dark.scss @@ -1,5 +1,5 @@ // Dark Theme -// By: +// By: .dark[app-theme] { --background: #121212; --text: #EFF3F8; @@ -19,6 +19,7 @@ --cblue: #0058F7; --cred: #fe3537; --cgreen: #19B028; + --cgold: #FFD700; --button: #0058F7; --loading: #999; -} \ No newline at end of file +} diff --git a/src/app/theme/themes/greenish.scss b/src/app/theme/themes/greenish.scss index 2b7b3d26d..4a0af94b9 100644 --- a/src/app/theme/themes/greenish.scss +++ b/src/app/theme/themes/greenish.scss @@ -19,6 +19,7 @@ --cblue: #238eff; --cred: #e0245e; --cgreen: #17BF63; + --cgold: #FFD700; --button: #03A8BB; - --loading: #ffffff; -} \ No newline at end of file + --loading: #ffffff; +} diff --git a/src/app/theme/themes/icydark.scss b/src/app/theme/themes/icydark.scss index 18387184c..1b1a60eeb 100644 --- a/src/app/theme/themes/icydark.scss +++ b/src/app/theme/themes/icydark.scss @@ -19,6 +19,7 @@ --cblue: #238eff; --cred: #e0245e; --cgreen: #17BF63; +--cgold: #FFD700; --button: #0058F7; --loading: #999; -} \ No newline at end of file +} diff --git a/src/app/theme/themes/legends.scss b/src/app/theme/themes/legends.scss index 0abe91012..508d1f71b 100644 --- a/src/app/theme/themes/legends.scss +++ b/src/app/theme/themes/legends.scss @@ -19,6 +19,7 @@ --cblue: #238eff; --cred: #e0245e; --cgreen: #17BF63; + --cgold: #FFD700; --button: #D3B882; --loading: #999; - } \ No newline at end of file + } diff --git a/src/app/theme/themes/light.scss b/src/app/theme/themes/light.scss index 06ce5d0d3..13055ef1b 100644 --- a/src/app/theme/themes/light.scss +++ b/src/app/theme/themes/light.scss @@ -19,6 +19,7 @@ --cblue: #0058F7; --cred: #fe3537; --cgreen: #19B028; + --cgold: #FFD700; --button: #0058F7; --loading: #777777; -} \ No newline at end of file +} diff --git a/src/app/trade-creator-page/trade-creator/trade-creator.component.ts b/src/app/trade-creator-page/trade-creator/trade-creator.component.ts index c5f169c46..6f9af6f4f 100644 --- a/src/app/trade-creator-page/trade-creator/trade-creator.component.ts +++ b/src/app/trade-creator-page/trade-creator/trade-creator.component.ts @@ -204,7 +204,8 @@ export class TradeCreatorComponent implements OnInit { setUpBuyTutorial(): void { let balance = this.appData.loggedInUser?.BalanceNanos; - const jumioDeSoNanos = this.appData.jumioDeSoNanos > 0 ? this.appData.jumioDeSoNanos : 1e8; + const jumioDeSoNanos = + this.appData.jumioUSDCents > 0 ? this.appData.usdToNanosNumber(this.appData.jumioUSDCents / 100) : 1e8; balance = balance > jumioDeSoNanos ? jumioDeSoNanos : balance; const percentToBuy = this.creatorProfile.PublicKeyBase58Check === this.globalVars.loggedInUser.PublicKeyBase58Check ? 0.1 : 0.5; diff --git a/src/app/transfer-nft-accept-modal/transfer-nft-accept-modal.component.html b/src/app/transfer-nft-accept-modal/transfer-nft-accept-modal.component.html new file mode 100644 index 000000000..14a8dbaca --- /dev/null +++ b/src/app/transfer-nft-accept-modal/transfer-nft-accept-modal.component.html @@ -0,0 +1,103 @@ +
+ + +
+
+
+ You are about to accept an NFT transferred by @{{ this.transferringUser }}. +
+ +
+
+ Number + #{{ selectedSerialNumber?.SerialNumber }} +
+
+ Highest Bid + {{ globalVars.nanosToDeSo(highBid) }} DESO (~{{globalVars.nanosToUSD(highBid, 2)}}) +
+
+ Min Bid Amount + {{ globalVars.nanosToDeSo(selectedSerialNumber?.MinBidAmountNanos) }} DESO (~{{globalVars.nanosToUSD(selectedSerialNumber?.MinBidAmountNanos, 2)}}) +
+
+ +
+
+ Number +
+ #{{ selectedSerialNumber?.SerialNumber }} + Change +
+
+
+
+ Highest Bid + + {{ globalVars.nanosToDeSo(highBid) }} DESO (~{{ globalVars.nanosToUSD(highBid, 2) }}) + +
+
+ Min Bid Amount + + {{ globalVars.nanosToDeSo(selectedSerialNumber?.MinBidAmountNanos) }} DESO (~{{ + globalVars.nanosToUSD(selectedSerialNumber?.MinBidAmountNanos, 2) + }}) + +
+
+
+ + +
+
+
+ + + +
+ + {{ error }} +
+
+ +
+ +
+ + +
+
An NFT can have multiple editions, each with its own unique serial number.
+
+ +
+
+
+ There are no serial numbers available for you to accept. +
+
+
diff --git a/src/app/transfer-nft-accept-modal/transfer-nft-accept-modal.component.ts b/src/app/transfer-nft-accept-modal/transfer-nft-accept-modal.component.ts new file mode 100644 index 000000000..13799d7f0 --- /dev/null +++ b/src/app/transfer-nft-accept-modal/transfer-nft-accept-modal.component.ts @@ -0,0 +1,119 @@ +import { Component, Input, Output, EventEmitter } from "@angular/core"; +import { GlobalVarsService } from "../global-vars.service"; +import { BackendApiService, NFTEntryResponse, PostEntryResponse } from "../backend-api.service"; +import { Router } from "@angular/router"; +import { isNumber } from "lodash"; +import { ToastrService } from "ngx-toastr"; +import { BsModalRef, BsModalService } from "ngx-bootstrap/modal"; +import { Location } from "@angular/common"; + +@Component({ + selector: "transfer-nft-accept-modal", + templateUrl: "./transfer-nft-accept-modal.component.html", +}) +export class TransferNftAcceptModalComponent { + static PAGE_SIZE = 50; + static BUFFER_SIZE = 10; + static WINDOW_VIEWPORT = false; + static PADDING = 0.5; + + @Input() postHashHex: string; + @Input() post: PostEntryResponse; + @Input() transferNFTEntryResponses: NFTEntryResponse[]; + @Output() closeModal = new EventEmitter(); + @Output() changeTitle = new EventEmitter(); + + bidAmountDeSo: number; + bidAmountUSD: number; + selectedSerialNumber: NFTEntryResponse = null; + highBid: number = null; + lowBid: number = null; + loading = false; + isSelectingSerialNumber = true; + saveSelectionDisabled = false; + showSelectedSerialNumbers = false; + acceptingTransfer: boolean = false; + errors: string[] = []; + minBidCurrency: string = "USD"; + minBidInput: number = 0; + transferringUser: string; + + constructor( + public globalVars: GlobalVarsService, + private backendApi: BackendApiService, + private modalService: BsModalService, + private router: Router, + private toastr: ToastrService, + private location: Location, + public bsModalRef: BsModalRef + ) {} + + acceptTransfer() { + this.saveSelectionDisabled = true; + this.acceptingTransfer = true; + this.backendApi + .AcceptNFTTransfer( + this.globalVars.localNode, + this.globalVars.loggedInUser.PublicKeyBase58Check, + this.post.PostHashHex, + this.selectedSerialNumber.SerialNumber, + this.globalVars.defaultFeeRateNanosPerKB + ) + .subscribe( + (res) => { + this.modalService.setDismissReason("transfer accepted"); + this.bsModalRef.hide(); + this.toastr.show("Your transfer was completed", null, { + toastClass: "info-toast", + positionClass: "toast-bottom-center", + }); + }, + (err) => { + console.error(err); + } + ) + .add(() => { + this.acceptingTransfer = false; + this.saveSelectionDisabled = false; + }); + } + + saveSelection(): void { + if (!this.saveSelectionDisabled) { + this.isSelectingSerialNumber = false; + this.showSelectedSerialNumbers = true; + this.changeTitle.emit("Confirm Transfer"); + this.highBid = this.selectedSerialNumber.HighestBidAmountNanos; + this.lowBid = this.selectedSerialNumber.LowestBidAmountNanos; + } + } + + goBackToSerialSelection(): void { + this.isSelectingSerialNumber = true; + this.showSelectedSerialNumbers = false; + this.changeTitle.emit("Choose an edition"); + this.highBid = null; + this.lowBid = null; + this.selectedSerialNumber = null; + } + + selectSerialNumber(serialNumber: NFTEntryResponse) { + this.selectedSerialNumber = serialNumber; + this.backendApi + .GetSingleProfile(this.globalVars.localNode, this.selectedSerialNumber.LastOwnerPublicKeyBase58Check, "") + .subscribe((res) => { + if (res && !res.IsBlacklisted) { + this.transferringUser = res.Profile?.Username; + } + }); + this.saveSelection(); + } + + bidAmountUSDFormatted() { + return isNumber(this.bidAmountUSD) ? `~${this.globalVars.formatUSD(this.bidAmountUSD, 0)}` : ""; + } + + bidAmountDeSoFormatted() { + return isNumber(this.bidAmountDeSo) ? `~${this.bidAmountDeSo.toFixed(2)} $DESO` : ""; + } +} diff --git a/src/app/transfer-nft-modal/transfer-nft-modal.component.html b/src/app/transfer-nft-modal/transfer-nft-modal.component.html new file mode 100644 index 000000000..53dfa3fa6 --- /dev/null +++ b/src/app/transfer-nft-modal/transfer-nft-modal.component.html @@ -0,0 +1,108 @@ +
+ + +
+
+
+ You are about to transfer the NFT shown below. +
+ +
+ Number #{{ selectedSerialNumber?.SerialNumber }} + Change +
+ +
+
+ Number #{{ selectedSerialNumber?.SerialNumber }} +
+ Change +
+
+
+ +
+ Search for a public key or username to whom you want to transfer this NFT +
+ + + +
+
+ This NFT includes unlockable content. Enter it below. +
+ + +
+ +
+ +
+ + +
+
+
+ + + +
+ + +
+
These are the serial numbers that are transferable
+
+ +
+
+
+ There are no serial numbers available for you to transfer. +
+
+
diff --git a/src/app/transfer-nft-modal/transfer-nft-modal.component.ts b/src/app/transfer-nft-modal/transfer-nft-modal.component.ts new file mode 100644 index 000000000..cfe394ee5 --- /dev/null +++ b/src/app/transfer-nft-modal/transfer-nft-modal.component.ts @@ -0,0 +1,188 @@ +import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; +import { GlobalVarsService } from "../global-vars.service"; +import { BackendApiService, NFTEntryResponse, PostEntryResponse, ProfileEntryResponse } from "../backend-api.service"; +import { orderBy } from "lodash"; +import { Router } from "@angular/router"; +import { ToastrService } from "ngx-toastr"; +import { BsModalRef, BsModalService } from "ngx-bootstrap/modal"; +import { Location } from "@angular/common"; +import { SwalHelper } from "../../lib/helpers/swal-helper"; + +@Component({ + selector: "transfer-nft-modal", + templateUrl: "./transfer-nft-modal.component.html", +}) +export class TransferNftModalComponent implements OnInit { + static PAGE_SIZE = 50; + static BUFFER_SIZE = 10; + static WINDOW_VIEWPORT = false; + static PADDING = 0.5; + + @Input() postHashHex: string; + @Input() post: PostEntryResponse; + @Output() closeModal = new EventEmitter(); + @Output() changeTitle = new EventEmitter(); + + selectedSerialNumber: NFTEntryResponse = null; + loading = true; + transferringNFT: boolean = false; + isSelectingSerialNumber = true; + saveSelectionDisabled = false; + showSelectedSerialNumbers = false; + transferableSerialNumbers: NFTEntryResponse[]; + SN_FIELD = "SerialNumber"; + LAST_PRICE_FIELD = "LastAcceptedBidAmountNanos"; + sortByField = this.SN_FIELD; + sortByOrder: "desc" | "asc" = "asc"; + selectedCreator: ProfileEntryResponse; + unlockableText: string = ""; + + constructor( + public globalVars: GlobalVarsService, + private backendApi: BackendApiService, + private modalService: BsModalService, + private router: Router, + private toastr: ToastrService, + private location: Location, + public bsModalRef: BsModalRef + ) {} + + ngOnInit(): void { + this.backendApi + .GetNFTEntriesForNFTPost( + this.globalVars.localNode, + this.globalVars.loggedInUser.PublicKeyBase58Check, + this.post.PostHashHex + ) + .subscribe((res) => { + this.transferableSerialNumbers = orderBy( + (res.NFTEntryResponses as NFTEntryResponse[]).filter( + (nftEntryResponse) => + nftEntryResponse.OwnerPublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check && + !nftEntryResponse.IsPending && + !nftEntryResponse.IsForSale + ), + [this.sortByField], + [this.sortByOrder] + ); + }) + .add(() => (this.loading = false)); + } + + transferNFT() { + this.saveSelectionDisabled = true; + this.transferringNFT = true; + SwalHelper.fire({ + target: this.globalVars.getTargetComponentSelector(), + title: "Transfer NFT", + html: `You are about to transfer this NFT to ${ + this.selectedCreator?.Username || this.selectedCreator?.PublicKeyBase58Check + }`, + showConfirmButton: true, + showCancelButton: true, + reverseButtons: true, + customClass: { + confirmButton: "btn btn-light", + cancelButton: "btn btn-light no", + }, + confirmButtonText: "Ok", + cancelButtonText: "Cancel", + }).then((res) => { + if (res.isConfirmed) { + this.backendApi + .TransferNFT( + this.globalVars.localNode, + this.globalVars.loggedInUser?.PublicKeyBase58Check, + this.selectedCreator?.PublicKeyBase58Check, + this.post.PostHashHex, + this.selectedSerialNumber?.SerialNumber, + this.unlockableText, + this.globalVars.defaultFeeRateNanosPerKB + ) + .subscribe( + (res) => { + this.modalService.setDismissReason("nft transferred"); + this.bsModalRef.hide(); + this.showToast(); + }, + (err) => { + console.error(err); + this.globalVars._alertError(this.backendApi.parseMessageError(err)); + } + ) + .add(() => { + this.transferringNFT = false; + this.saveSelectionDisabled = false; + }); + } else { + this.transferringNFT = false; + this.saveSelectionDisabled = false; + } + }); + } + + showToast(): void { + const link = `/${this.globalVars.RouteNames.NFT}/${this.post.PostHashHex}`; + this.toastr.show(`NFT TransferredView`, null, { + toastClass: "info-toast", + enableHtml: true, + positionClass: "toast-bottom-center", + }); + } + + saveSelection(): void { + if (!this.saveSelectionDisabled) { + this.isSelectingSerialNumber = false; + this.showSelectedSerialNumbers = true; + this.changeTitle.emit("Transfer NFT"); + } + } + + goBackToSerialSelection(): void { + this.isSelectingSerialNumber = true; + this.showSelectedSerialNumbers = false; + this.changeTitle.emit("Choose an edition"); + this.selectedSerialNumber = null; + } + + selectSerialNumber(serialNumber: NFTEntryResponse) { + this.selectedSerialNumber = serialNumber; + this.saveSelection(); + } + + deselectSerialNumber() { + if (this.transferringNFT) { + return; + } + this.selectedSerialNumber = null; + this.showSelectedSerialNumbers = false; + } + + lastPage = null; + + getPage(page: number) { + if (this.lastPage != null && page > this.lastPage) { + return []; + } + const startIdx = page * TransferNftModalComponent.PAGE_SIZE; + const endIdx = (page + 1) * TransferNftModalComponent.PAGE_SIZE; + + return new Promise((resolve, reject) => { + resolve(this.transferableSerialNumbers.slice(startIdx, Math.min(endIdx, this.transferableSerialNumbers.length))); + }); + } + + updateSort(sortField: string) { + if (this.sortByField === sortField) { + this.sortByOrder = this.sortByOrder === "asc" ? "desc" : "asc"; + } else { + this.sortByOrder = "asc"; + } + this.sortByField = sortField; + this.transferableSerialNumbers = orderBy(this.transferableSerialNumbers, [this.sortByField], [this.sortByOrder]); + } + + _selectCreator(selectedCreator: ProfileEntryResponse) { + this.selectedCreator = selectedCreator; + } +} diff --git a/src/app/trends-page/trends/trends.component.ts b/src/app/trends-page/trends/trends.component.ts index 82f557e5c..f8356966d 100644 --- a/src/app/trends-page/trends/trends.component.ts +++ b/src/app/trends-page/trends/trends.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { GlobalVarsService } from "../../global-vars.service"; import { BackendApiService } from "../../backend-api.service"; import { HttpClient } from "@angular/common/http"; -import { PulseService } from "../../../lib/services/pulse/pulse-service"; +import { AltumbaseService } from "../../../lib/services/altumbase/altumbase-service"; import { BithuntService } from "../../../lib/services/bithunt/bithunt-service"; import { RightBarCreatorsComponent, RightBarTabOption } from "../../right-bar-creators/right-bar-creators.component"; import { IAdapter, IDatasource } from "ngx-ui-scroll"; @@ -35,7 +35,7 @@ export class TrendsComponent implements OnInit { loadingNextPage = false; bithuntService: BithuntService; - pulseService: PulseService; + altumbaseService: AltumbaseService; constructor( public globalVars: GlobalVarsService, @@ -51,7 +51,7 @@ export class TrendsComponent implements OnInit { this.lastPageByTab[tab] = null; }); this.bithuntService = new BithuntService(this.httpClient, this.backendApi, this.globalVars); - this.pulseService = new PulseService(this.httpClient, this.backendApi, this.globalVars); + this.altumbaseService = new AltumbaseService(this.httpClient, this.backendApi, this.globalVars); this.selectTab(); } @@ -71,7 +71,7 @@ export class TrendsComponent implements OnInit { getPage(page: number) { if (this.activeTab === RightBarCreatorsComponent.GAINERS.name) { this.loadingNextPage = page !== 0; - return this.pulseService + return this.altumbaseService .getDeSoLockedPage(page + 1, TrendsComponent.PAGE_SIZE, true) .toPromise() .then( @@ -90,8 +90,8 @@ export class TrendsComponent implements OnInit { } if (this.activeTab === RightBarCreatorsComponent.DIAMONDS.name) { this.loadingNextPage = page !== 0; - return this.pulseService - .getDiamondsReceivedPage(page + 1, TrendsComponent.PAGE_SIZE, true) + return this.altumbaseService + .getDiamondsReceivedPage(page + 1, TrendsComponent.PAGE_SIZE, false) .toPromise() .then( (res) => { diff --git a/src/index.bitclout.html b/src/index.bitclout.html index 1ece469ac..75d40d1c2 100644 --- a/src/index.bitclout.html +++ b/src/index.bitclout.html @@ -32,10 +32,10 @@ - - - - + + + + diff --git a/src/index.html b/src/index.html index 2f430895f..49f28511f 100644 --- a/src/index.html +++ b/src/index.html @@ -8,16 +8,16 @@ - + - - + + - + - + @@ -32,10 +32,10 @@ - - - - + + + + diff --git a/src/lib/services/pulse/pulse-service.ts b/src/lib/services/altumbase/altumbase-service.ts similarity index 52% rename from src/lib/services/pulse/pulse-service.ts rename to src/lib/services/altumbase/altumbase-service.ts index 44aca7d21..84bb367b6 100644 --- a/src/lib/services/pulse/pulse-service.ts +++ b/src/lib/services/altumbase/altumbase-service.ts @@ -6,59 +6,59 @@ import { GlobalVarsService } from "../../../app/global-vars.service"; import { map, switchMap } from "rxjs/operators"; import * as _ from "lodash"; -class PulseLeaderboardResult { +class AltumbaseLeaderboardResult { public_key: string; - diamonds?: number; - net_change_24h_bitclout_nanos?: number; + diamonds_received_24h?: number; + diamonds_received_value_24h?: number; } -class PulseLeaderboardResponse { - results: PulseLeaderboardResult[]; +class AltumbaseLeaderboardResponse { + data: AltumbaseLeaderboardResult[]; pagination: { current_page: number; - total_pages: number; + last_page: number; }; } -const DeSoLocked = "bitclout_locked_24h"; +const DeSoLocked = "deso_locked_24h"; const Diamonds = "diamonds_received_24h"; -export enum PulseLeaderboardType { - DeSoLocked = "bitclout_locked_24h", +export enum AltumbaseLeaderboardType { + DeSoLocked = "deso_locked_24h", Diamonds = "diamonds_received_24h", } export class LeaderboardResponse { Profile: ProfileEntryResponse; - DeSoLockedGained: number; DiamondsReceived: number; + DiamondsReceivedValue: number; User: User; } export const LeaderboardToDataAttribute = { - [PulseLeaderboardType.DeSoLocked]: "net_change_24h_bitclout_nanos", - [PulseLeaderboardType.Diamonds]: "diamonds", + [AltumbaseLeaderboardType.DeSoLocked]: "deso_locked_24h", + [AltumbaseLeaderboardType.Diamonds]: "diamonds_received_24h", }; @Injectable({ providedIn: "root", }) -export class PulseService { - static pulseApiURL = "https://pulse.bitclout.com/api/bitclout/leaderboard"; - static pulseRef = "ref=bcl"; - static pulsePageSize = 20; +export class AltumbaseService { + static altumbaseApiURL = "https://altumbase.com/api"; + static altumbaseRef = "ref=bcl"; + static altumbasePageSize = 20; constructor( private httpClient: HttpClient, private backendApi: BackendApiService, private globalVars: GlobalVarsService ) {} - constructPulseURL( + constructAltumbaseURL( leaderboardType: string, - pageIndex: number = 0, - pageSize: number = PulseService.pulsePageSize + pageIndex: number = 1, + pageSize: number = AltumbaseService.altumbasePageSize ): string { - return `${PulseService.pulseApiURL}/${leaderboardType}?${PulseService.pulseRef}&page_size=${pageSize}&page_index=${pageIndex}`; + return `${AltumbaseService.altumbaseApiURL}/${leaderboardType}?${AltumbaseService.altumbaseRef}&page_size=${pageSize}&page=${pageIndex}`; } getDiamondsReceivedLeaderboard(): Observable { @@ -67,40 +67,43 @@ export class PulseService { getDiamondsReceivedPage( pageNumber: number, - pageSize: number = PulseService.pulsePageSize, + pageSize: number = AltumbaseService.altumbasePageSize, skipFilters = false ): Observable { - return this.httpClient.get(this.constructPulseURL(PulseLeaderboardType.Diamonds, pageNumber, pageSize)).pipe( - switchMap((res: PulseLeaderboardResponse) => { - return this.getProfilesForPulseLeaderboard(res, PulseLeaderboardType.Diamonds, skipFilters); - }) - ); + return this.httpClient + .get(this.constructAltumbaseURL(AltumbaseLeaderboardType.Diamonds, pageNumber, pageSize)) + .pipe( + switchMap((res: AltumbaseLeaderboardResponse) => { + return this.getProfilesForAltumbaseLeaderboard(res, AltumbaseLeaderboardType.Diamonds, skipFilters); + }) + ); } - getDeSoLockedLeaderboard(): Observable { + getDeSoLockedLeaderboard(): Observable { return this.getDeSoLockedPage(0); } getDeSoLockedPage( pageNumber: number, - pageSize: number = PulseService.pulsePageSize, + pageSize: number = AltumbaseService.altumbasePageSize, skipFilters = false ): Observable { return this.httpClient - .get(this.constructPulseURL(PulseLeaderboardType.DeSoLocked, pageNumber, pageSize)) + .get(this.constructAltumbaseURL(AltumbaseLeaderboardType.DeSoLocked, pageNumber, pageSize)) .pipe( - switchMap((res: PulseLeaderboardResponse) => - this.getProfilesForPulseLeaderboard(res, PulseLeaderboardType.DeSoLocked, skipFilters) - ) + switchMap((res: AltumbaseLeaderboardResponse) => { + return this.getProfilesForAltumbaseLeaderboard(res, AltumbaseLeaderboardType.DeSoLocked, skipFilters); + }) ); } - getProfilesForPulseLeaderboard( - res: PulseLeaderboardResponse, - leaderboardType: PulseLeaderboardType, + getProfilesForAltumbaseLeaderboard( + res: AltumbaseLeaderboardResponse, + leaderboardType: AltumbaseLeaderboardType, skipFilters: boolean = false - ): Observable { - const results = res.results; + ): Observable { + const results = res.data; + if (results.length === 0) { return of([]); } @@ -121,17 +124,20 @@ export class PulseService { res.UserList = res.UserList.slice(0, 10); } } - return res.UserList.map((user: User, index: number) => { return { User: user, Profile: user.ProfileEntryResponse, - DeSoLockedGained: - leaderboardType === PulseLeaderboardType.DeSoLocked + DiamondsReceived: + leaderboardType === AltumbaseLeaderboardType.Diamonds ? results[index][LeaderboardToDataAttribute[leaderboardType]] : null, - DiamondsReceived: - leaderboardType === PulseLeaderboardType.Diamonds + DiamondsReceivedValue: + leaderboardType === AltumbaseLeaderboardType.Diamonds + ? results[index]["diamonds_received_value_24h"] + : null, + DeSoLockedGained: + leaderboardType === AltumbaseLeaderboardType.DeSoLocked ? results[index][LeaderboardToDataAttribute[leaderboardType]] : null, }; diff --git a/src/styles.scss b/src/styles.scss index ff8c9b653..509f073f0 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -219,6 +219,7 @@ $green-font-color: #19B028; .fc-blue { color: var(--cblue); } .fc-red { color: var(--cred); } .fc-green { color: var(--cgreen); } +.fc-gold { color: var(--cgold); } .font-weight-500 { font-weight: 500 } @@ -2831,3 +2832,31 @@ input { .referral-link-info__blue-border { border-left: 5px solid $buy-button-blue !important; } + +.checkbox-circle { + i { + display: none; + position: relative; + top: -5px; + } + padding: 3px; + width: 22px; + height: 22px; + border: 1px solid var(--border); + border-radius: 100%; + background: none; + transition: background-color .5s; + &.radio { + transition: background-color 0s; + } + &.checked { + background: var(--highlight); + i { + display: inline; + color: var(--tile-bg); + } + &.radio { + transition: background-color .5s; + } + } +}