From e5b282105cc5caa283e42d1093f436977138e8f6 Mon Sep 17 00:00:00 2001 From: piratekev <93568025+piratekev@users.noreply.github.com> Date: Wed, 18 May 2022 14:55:49 -0700 Subject: [PATCH] Pull in v2.2.1 (#12) * 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 * nft notification enhancements (#551) * save current progress on notifications * Update NFT notifications and fix some small bugs * make global vars private again * remove console logging * Ln/update sell nft modal (#554) * show additional royalties in sell nft modal * remove service fee which is no longer user * Messages V3 (#543) * Messages V3 * V3 support * Messages V3 final * Add comments & fix version * Fix circular dependancies * Revert "Messages V3 (#543)" (#555) This reverts commit 9d98ea748dc02b022870e54cc49dc5dbdc1fb09e. * if get user metadata throws an error, swallow it and return null (#556) * [stable] Release 2.0.2 * Ln/dao coins (#548) * save current progress on DAO coin UI * add support for transferring DAO coins in the UI, add inputs for other fields for DAO coin transaction * add burn support, add DAO coin tab to profile, use number abbreviation to keep DAO coin numbers manageable, move utility func for parsing hex balances to global vars * add some frontend validation in transfer modal * fix up DAO modals - add balances and validations, hit isHodling endpoint if transfer restriction status is DAO Members only * fix alignment on DAO coin page * disable mint and burn if the amount is less than or equal to 0 * add notifications for DAO coin txns * add sweet alerts before DAO actions, only show profile owner if transfer restriction status is profile owner only and logged in user is not profile owner * address TGS feedback * Fix errors when user does not have DAO coin yet * Messages v3 (#557) * Messages V3 * V3 support * Messages V3 final * Add comments & fix version * Fix circular dependancies * Add senderGroupKeyName to TransferNFT * update Query ETH RPC to remove JWT requirement (#558) * Add support for Pearl node (#561) * Posts/Users pages are blank when user not logged in (#560) * fix: metadata fails when user not logged in this additional checks makes sure verification / node.deso.org metadata is only requested if loggedInUserPublicKey is ok. Currently it requests metadata from `https://node.deso.org/api/v0/get-user-metadata/undefined` which returns status 404 error and prevents post/user pages from showing the content. * align with diamondapp use same approach as DiamondApp * [stable] Release 2.0.4 (#562) * display media content in replies (#564) * allow media when creating comments (#565) * Add support for twitter images.. (#568) * Add support for twitter images.. this will show images hosted on twitter (they have link like pbs.twing.com ex: https://pbs.twimg.com/media/FO9LbSjaQAEFE26.jpg) before ![image](https://user-images.githubusercontent.com/55331140/160470352-bde7f317-fcc7-4680-9180-f9487a7892bc.png) after ![image](https://user-images.githubusercontent.com/55331140/160470432-5168c8a3-ad78-44e9-a147-3afc1fd11803.png) will be helpful for those projects which are building bridge between twitter and deso. * Update Caddyfile Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * added Mousai embedding support to frontend (#563) * Added support for embedding Mousai streaming link in frontend * Fixed prettier/prettier issue * Added tests for Mousai embedding functionality * improved regex matching for mousai links * added the mousai's url in Caddyfile * Removed the test-cases that prevent tests from running properly Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * Replaced `jasmin.arrayContaining` call with a more logically sounding alternative (based on the available variables) Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * Updated `regex` based on the suggestion provided by @lazynina Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * Updated `regex` based on the suggestion provided by @lazynina Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * Added proper height to Mousai link * Update src/lib/services/embed-url-parser-service/embed-url-parser-service.ts Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * Update src/lib/services/embed-url-parser-service/embed-url-parser-service.ts Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> Co-authored-by: Lazy Nina <81658138+lazynina@users.noreply.github.com> * [stable] Release 2.1.0 (#569) * update reporting links (#571) * update reporting links * add /content to path * Remove jumio messaging and referrals (#573) * [stable] Release 2.2.0 * [stable] Release 2.2.1 Bumping release to fix a CI issue * pull in v2.2.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 Co-authored-by: Piotr Nojszewski <29924594+AeonSw4n@users.noreply.github.com> Co-authored-by: Tijno (@tijn on Deso) <69529928+tijno@users.noreply.github.com> Co-authored-by: ItsAditya.eth <55331140+AdityaChaudhary0005@users.noreply.github.com> Co-authored-by: Farsad Fakhim --- Caddyfile | 2 + package-lock.json | 23 +- src/app/app-routing.module.ts | 10 +- src/app/app.component.ts | 10 +- src/app/app.module.ts | 8 + src/app/backend-api.service.ts | 172 +++++- .../buy-deso-eth/buy-deso-eth.component.ts | 2 +- .../create-nft-auction-modal.component.html | 2 +- .../creator-profile-details.component.html | 24 +- .../creator-profile-details.component.ts | 2 + .../creator-profile-hodlers.component.html | 26 +- .../creator-profile-hodlers.component.ts | 4 +- .../creator-profile-top-card.component.ts | 2 +- .../burn-dao-coin-modal.component.html | 35 ++ .../burn-dao-coin-modal.component.ts | 62 +++ .../dao-coins-page.component.html | 3 + .../dao-coins-page.component.scss | 0 .../dao-coins-page.component.spec.ts | 24 + .../dao-coins-page.component.ts | 11 + src/app/dao-coins/dao-coins.component.html | 263 +++++++++ src/app/dao-coins/dao-coins.component.ts | 521 ++++++++++++++++++ .../transfer-dao-coin-modal.component.html | 47 ++ .../transfer-dao-coin-modal.component.ts | 127 +++++ .../feed-create-post.component.html | 35 +- .../feed-create-post.component.ts | 9 +- .../feed-post-dropdown.component.ts | 2 +- .../feed/feed-post/feed-post.component.html | 6 +- src/app/feed/feed.component.html | 7 - src/app/global-vars.service.ts | 37 +- src/app/identity.service.ts | 1 + src/app/left-bar/left-bar.component.html | 13 +- .../mint-nft-modal.component.ts | 3 +- .../notifications-list.component.ts | 170 ++++-- .../place-bid-modal.component.html | 6 +- .../place-bid-modal.component.ts | 7 + .../right-bar-creators.component.html | 3 - .../sell-nft-modal.component.html | 22 +- .../sell-nft-modal.component.ts | 29 +- src/app/wallet/wallet.component.html | 1 - src/app/wallet/wallet.component.ts | 9 +- src/environments/environment.ts | 2 +- .../embed-url-parser-service.spec.ts | 39 ++ .../embed-url-parser-service.ts | 37 ++ 43 files changed, 1632 insertions(+), 186 deletions(-) create mode 100644 src/app/dao-coins/burn-dao-coin-modal/burn-dao-coin-modal.component.html create mode 100644 src/app/dao-coins/burn-dao-coin-modal/burn-dao-coin-modal.component.ts create mode 100644 src/app/dao-coins/dao-coins-page/dao-coins-page.component.html create mode 100644 src/app/dao-coins/dao-coins-page/dao-coins-page.component.scss create mode 100644 src/app/dao-coins/dao-coins-page/dao-coins-page.component.spec.ts create mode 100644 src/app/dao-coins/dao-coins-page/dao-coins-page.component.ts create mode 100644 src/app/dao-coins/dao-coins.component.html create mode 100644 src/app/dao-coins/dao-coins.component.ts create mode 100644 src/app/dao-coins/transfer-dao-coin-modal/transfer-dao-coin-modal.component.html create mode 100644 src/app/dao-coins/transfer-dao-coin-modal/transfer-dao-coin-modal.component.ts diff --git a/Caddyfile b/Caddyfile index 0db2f2e96..e3053a03b 100644 --- a/Caddyfile +++ b/Caddyfile @@ -64,6 +64,7 @@ header @html Content-Security-Policy " arweave.net *.arweave.net *.pearl.app + *.twimg.com cloudflare-ipfs.com; font-src 'self' https://fonts.googleapis.com @@ -87,6 +88,7 @@ header @html Content-Security-Policy " https://w.soundcloud.com https://player.twitch.tv https://clips.twitch.tv + https://mousai.stream pay.testwyre.com pay.sendwyre.com https://iframe.videodelivery.net; diff --git a/package-lock.json b/package-lock.json index adec7e1ab..88a7485d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1548,6 +1548,7 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-11.2.11.tgz", "integrity": "sha512-INDyO6Vh4WjsWkYAeZN39B4wTs+VqoAcTGdVBA39uij6wdu00ufr7pPRHtjAoNgrOWjEd/SCgDaUMcFEZ2+lcg==", "dependencies": { + "parse5": "^5.0.0", "tslib": "^2.0.0" }, "optionalDependencies": { @@ -7179,6 +7180,7 @@ "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", + "fsevents": "~2.1.2", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -14735,6 +14737,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dependencies": { + "graceful-fs": "^4.1.6" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -15186,7 +15191,14 @@ "dev": true, "dependencies": { "copy-anything": "^2.0.1", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^2.5.2", "parse-node-version": "^1.0.1", + "source-map": "~0.6.0", "tslib": "^1.10.0" }, "bin": { @@ -16415,6 +16427,7 @@ "integrity": "sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ==", "dev": true, "dependencies": { + "encoding": "^0.1.12", "minipass": "^3.1.0", "minipass-sized": "^1.0.3", "minizlib": "^2.0.0" @@ -16937,6 +16950,7 @@ "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", + "fsevents": "~2.3.1", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -22998,6 +23012,9 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.4.tgz", "integrity": "sha512-B0LcJhjiwKkTl79aGVF/u5KdzsH8IylVfV56Ut6c9ouWLJcUK17T83aZBetNYSnZtXf2OHD4+2PbmRW+Fp5ulg==", "dev": true, + "dependencies": { + "fsevents": "~2.3.1" + }, "bin": { "rollup": "dist/bin/rollup" }, @@ -26315,8 +26332,10 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", "dependencies": { + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.0" }, "optionalDependencies": { "chokidar": "^3.4.1", @@ -26376,6 +26395,7 @@ "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", + "fsevents": "^1.2.7", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0", @@ -27163,6 +27183,7 @@ "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", + "fsevents": "^1.2.7", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 2eb06aaa2..666c7f96a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -27,7 +27,6 @@ import { DiamondPostsPageComponent } from "./diamond-posts-page/diamond-posts-pa import { TrendsPageComponent } from "./trends-page/trends-page.component"; import { NftPostPageComponent } from "./nft-post-page/nft-post-page.component"; import { VerifyEmailComponent } from "./verify-email/verify-email.component"; -import { ReferralsComponent } from "./referrals/referrals.component"; import { CreateProfileTutorialPageComponent } from "./tutorial/create-profile-tutorial-page/create-profile-tutorial-page.component"; import { BuyCreatorCoinsTutorialPageComponent } from "./tutorial/buy-creator-coins-tutorial-page/buy-creator-coins-tutorial-page.component"; import { BuyCreatorCoinsConfirmTutorialComponent } from "./tutorial/buy-creator-coins-tutorial-page/buy-creator-coins-confirm-tutorial/buy-creator-coins-confirm-tutorial.component"; @@ -36,9 +35,8 @@ 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"; +import { SupplyMonitoringStatsPageComponent } from "./supply-monitoring-stats-page/supply-monitoring-stats-page.component"; +import { DaoCoinsPageComponent } from "./dao-coins/dao-coins-page/dao-coins-page.component"; class RouteNames { // Not sure if we should have a smarter schema for this, e.g. what happens if we have @@ -77,7 +75,6 @@ class RouteNames { public static LANDING = "/"; public static DIAMONDS = "diamonds"; public static TRENDS = "trends"; - public static REFERRALS = "referrals"; public static NFT = "nft"; public static VERIFY_EMAIL = "verify-email"; @@ -85,6 +82,7 @@ class RouteNames { public static CREATE_PROFILE = "create-profile"; public static INVEST = "invest"; public static SUPPLY_STATS = "supply-stats"; + public static DAO = "dao"; } const routes: Routes = [ @@ -96,7 +94,6 @@ const routes: Routes = [ { path: RouteNames.BUY_DESO, component: BuyDeSoPageComponent, pathMatch: "full" }, { path: RouteNames.PICK_A_COIN, component: PickACoinPageComponent, pathMatch: "full" }, { path: RouteNames.INBOX_PREFIX, component: MessagesPageComponent, pathMatch: "full" }, - { path: RouteNames.REFERRALS, component: ReferralsComponent, pathMatch: "full" }, { path: RouteNames.SIGN_UP, component: SignUpComponent, pathMatch: "full" }, { path: RouteNames.WALLET, component: WalletPageComponent, pathMatch: "full" }, { path: RouteNames.UPDATE_PROFILE, component: UpdateProfilePageComponent, pathMatch: "full" }, @@ -108,6 +105,7 @@ const routes: Routes = [ { path: RouteNames.POSTS + "/:postHashHex", component: PostThreadPageComponent, pathMatch: "full" }, { path: RouteNames.NFT + "/:postHashHex", component: NftPostPageComponent, pathMatch: "full" }, { path: RouteNames.SEND_DESO, component: TransferDeSoPageComponent, pathMatch: "full" }, + { path: RouteNames.DAO, component: DaoCoinsPageComponent, pathMatch: "full" }, { path: RouteNames.TOS, component: TosPageComponent, pathMatch: "full" }, { path: "tos", component: TosPageComponent, pathMatch: "full" }, { path: RouteNames.ADMIN, component: AdminPageComponent, pathMatch: "full" }, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3e602f1f1..c32a08bd4 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -7,6 +7,7 @@ import * as _ from "lodash"; import { environment } from "../environments/environment"; import { ThemeService } from "./theme/theme.service"; import { of, Subscription, zip } from "rxjs"; +import { catchError } from "rxjs/operators"; @Component({ selector: "app-root", @@ -113,8 +114,13 @@ export class AppComponent implements OnInit { return zip( this.backendApi.GetUsersStateless(this.globalVars.localNode, [loggedInUserPublicKey], false), - environment.verificationEndpointHostname - ? this.backendApi.GetUserMetadata(environment.verificationEndpointHostname, loggedInUserPublicKey) + environment.verificationEndpointHostname && !_.isNil(loggedInUserPublicKey) + ? this.backendApi.GetUserMetadata(environment.verificationEndpointHostname, loggedInUserPublicKey).pipe( + catchError((err) => { + console.error(err); + return of(null); + }) + ) : of(null) ).subscribe( ([res, userMetadata]) => { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 31a3c9883..7d47978dc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -163,6 +163,10 @@ import { TransferNftModalComponent } from "./transfer-nft-modal/transfer-nft-mod 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"; +import { DaoCoinsComponent } from "./dao-coins/dao-coins.component"; +import { DaoCoinsPageComponent } from "./dao-coins/dao-coins-page/dao-coins-page.component"; +import { TransferDAOCoinModalComponent } from "./dao-coins/transfer-dao-coin-modal/transfer-dao-coin-modal.component"; +import { BurnDaoCoinModalComponent } from "./dao-coins/burn-dao-coin-modal/burn-dao-coin-modal.component"; // Modular Themes for DeSo by Carsen Klock @carsenk import { ThemeModule } from "./theme/theme.module"; @@ -315,6 +319,10 @@ const greenishTheme: Theme = { key: "greenish", name: "Green Theme" }; TransferNftModalComponent, NftBurnModalComponent, NftSelectSerialNumberComponent, + DaoCoinsComponent, + DaoCoinsPageComponent, + TransferDAOCoinModalComponent, + BurnDaoCoinModalComponent, ], imports: [ BrowserModule, diff --git a/src/app/backend-api.service.ts b/src/app/backend-api.service.ts index 0aa0ad243..5fbdac146 100644 --- a/src/app/backend-api.service.ts +++ b/src/app/backend-api.service.ts @@ -8,6 +8,7 @@ import { map, switchMap, catchError, filter, take, concatMap } from "rxjs/operat import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { IdentityService } from "./identity.service"; import { environment } from "src/environments/environment"; +import { Hex } from "web3-utils/types"; export class BackendRoutes { static ExchangeRateRoute = "/api/v0/get-exchange-rate"; @@ -27,8 +28,10 @@ export class BackendRoutes { static RoutePathGetPostsForPublicKey = "/api/v0/get-posts-for-public-key"; static RoutePathGetDiamondedPosts = "/api/v0/get-diamonded-posts"; static RoutePathGetHodlersForPublicKey = "/api/v0/get-hodlers-for-public-key"; + static RoutePathIsHodlingPublicKey = "/api/v0/is-hodling-public-key"; static RoutePathSendMessageStateless = "/api/v0/send-message-stateless"; static RoutePathGetMessagesStateless = "/api/v0/get-messages-stateless"; + static RoutePathCheckPartyMessagingKeys = "/api/v0/check-party-messaging-keys"; static RoutePathMarkContactMessagesRead = "/api/v0/mark-contact-messages-read"; static RoutePathMarkAllMessagesRead = "/api/v0/mark-all-messages-read"; static RoutePathGetFollowsStateless = "/api/v0/get-follows-stateless"; @@ -90,6 +93,10 @@ export class BackendRoutes { static RoutePathAcceptNFTTransfer = "/api/v0/accept-nft-transfer"; static RoutePathBurnNFT = "/api/v0/burn-nft"; + // DAO routes + static RoutePathDAOCoin = "/api/v0/dao-coin"; + static RoutePathTransferDAOCoin = "/api/v0/transfer-dao-coin"; + // ETH static RoutePathSubmitETHTx = "/api/v0/submit-eth-tx"; static RoutePathQueryETHRPC = "/api/v0/query-eth-rpc"; @@ -177,6 +184,13 @@ export class Transaction { signatureBytesHex: string; } +export type DAOCoinEntryResponse = { + CoinsInCirculationNanos: Hex; + MintingDisabled: boolean; + NumberOfHolders: number; + TransferRestrictionStatus: TransferRestrictionStatusString; +}; + export class ProfileEntryResponse { Username: string; Description: string; @@ -187,6 +201,7 @@ export class ProfileEntryResponse { CoinsInCirculationNanos: number; CreatorBasisPoints: number; }; + DAOCoinEntry?: DAOCoinEntryResponse; CoinPriceDeSoNanos?: number; StakeMultipleBasisPoints?: number; PublicKeyBase58Check?: string; @@ -328,6 +343,8 @@ export class BalanceEntryResponse { HasPurchased: boolean; // How much this HODLer owns of a particular creator coin. BalanceNanos: number; + // Use this balance for DAO Coin balances + BalanceNanosUint256: Hex; // The net effect of transactions in the mempool on a given BalanceEntry's BalanceNanos. // This is used by the frontend to convey info about mining. NetBalanceInMempool: number; @@ -442,6 +459,20 @@ export type CountryLevelSignUpBonusResponse = { CountryCodeDetails: CountryCodeDetails; }; +export enum DAOCoinOperationTypeString { + MINT = "mint", + BURN = "burn", + UPDATE_TRANSFER_RESTRICTION_STATUS = "update_transfer_restriction_status", + DISABLE_MINTING = "disable_minting", +} + +export enum TransferRestrictionStatusString { + UNRESTRICTED = "unrestricted", + PROFILE_OWNER_ONLY = "profile_owner_only", + DAO_MEMBERS_ONLY = "dao_members_only", + PERMANENTLY_UNRESTRICTED = "permanently_unrestricted", +} + @Injectable({ providedIn: "root", }) @@ -471,6 +502,9 @@ export class BackendApiService { // Store the last identity service URL in localStorage LastIdentityServiceKey = "lastIdentityServiceURLV2"; + // Messaging V3 default key name. + DefaultKey = "default-key"; + // TODO: Wipe all this data when transition is complete LegacyUserListKey = "userList"; LegacySeedListKey = "seedList"; @@ -740,29 +774,51 @@ export class BackendApiService { MessageText: string, MinFeeRateNanosPerKB: number ): Observable { - //First encrypt message in identity - //Then pipe ciphertext to RoutePathSendMessageStateless - let req = this.identityService - .encrypt({ - ...this.identityService.identityServiceParamsForKey(SenderPublicKeyBase58Check), - recipientPublicKey: RecipientPublicKeyBase58Check, - message: MessageText, - }) - .pipe( - switchMap((encrypted) => { - const EncryptedMessageText = encrypted.encryptedMessage; - return this.post(endpoint, BackendRoutes.RoutePathSendMessageStateless, { - SenderPublicKeyBase58Check, - RecipientPublicKeyBase58Check, - EncryptedMessageText, - MinFeeRateNanosPerKB, - }).pipe( - map((request) => { - return { ...request }; + // First check if either sender or recipient has registered the "default-key" messaging group key. + // In V3 messages, we expect users to migrate to the V3 messages, which means they'll have the default + // key registered on-chain. We want to automatically send messages to this default key is it's registered. + // To check the messaging key we call the RoutePathCheckPartyMessaging keys backend API route. + let req = this.post(endpoint, BackendRoutes.RoutePathCheckPartyMessagingKeys, { + SenderPublicKeyBase58Check, + SenderMessagingKeyName: this.DefaultKey, + RecipientPublicKeyBase58Check, + RecipientMessagingKeyName: this.DefaultKey, + }).pipe( + switchMap((partyMessagingKeys) => { + // Once we determine the messaging keys of the parties, we will then encrypt a message based on the keys. + return this.identityService + .encrypt({ + ...this.identityService.identityServiceParamsForKey(SenderPublicKeyBase58Check), + recipientPublicKey: partyMessagingKeys.RecipientMessagingPublicKeyBase58Check, + senderGroupKeyName: partyMessagingKeys.SenderMessagingKeyName, + message: MessageText, + }) + .pipe( + switchMap((encrypted) => { + // Now we will use the ciphertext encrypted to user's messaging keys as part of the metadata of the + // sendMessage transaction. + const EncryptedMessageText = encrypted.encryptedMessage; + // Determine whether to use V3 messaging group key names for sender or recipient. + const senderV3 = partyMessagingKeys.IsSenderMessagingKey; + const SenderMessagingGroupKeyName = senderV3 ? partyMessagingKeys.SenderMessagingKeyName : ""; + const recipientV3 = partyMessagingKeys.IsRecipientMessagingKey; + const RecipientMessagingGroupKeyName = recipientV3 ? partyMessagingKeys.RecipientMessagingKeyName : ""; + return this.post(endpoint, BackendRoutes.RoutePathSendMessageStateless, { + SenderPublicKeyBase58Check, + RecipientPublicKeyBase58Check, + EncryptedMessageText, + SenderMessagingGroupKeyName, + RecipientMessagingGroupKeyName, + MinFeeRateNanosPerKB, + }).pipe( + map((request) => { + return { ...request }; + }) + ); }) ); - }) - ); + }) + ); return this.signAndSubmitTransaction(endpoint, req, SenderPublicKeyBase58Check); } @@ -975,6 +1031,7 @@ export class BackendApiService { ? this.identityService.encrypt({ ...this.identityService.identityServiceParamsForKey(UpdaterPublicKeyBase58Check), recipientPublicKey: BidderPublicKeyBase58Check, + senderGroupKeyName: "", message: UnencryptedUnlockableText, }) : of({ encryptedMessage: "" }); @@ -1012,6 +1069,7 @@ export class BackendApiService { ? this.identityService.encrypt({ ...this.identityService.identityServiceParamsForKey(SenderPublicKeyBase58Check), recipientPublicKey: ReceiverPublicKeyBase58Check, + senderGroupKeyName: "", message: UnencryptedUnlockableText, }) : of({ encryptedMessage: "" }); @@ -1340,8 +1398,9 @@ export class BackendApiService { LastPublicKeyBase58Check: string, NumToFetch: number, FetchHodlings: boolean = false, - FetchAll: boolean = false - ): Observable { + FetchAll: boolean = false, + IsDAOCoin: boolean = false + ): Observable<{ Hodlers: BalanceEntryResponse[]; LastPublicKeyBase58Check: string }> { return this.post(endpoint, BackendRoutes.RoutePathGetHodlersForPublicKey, { PublicKeyBase58Check, Username, @@ -1349,8 +1408,23 @@ export class BackendApiService { NumToFetch, FetchHodlings, FetchAll, + IsDAOCoin, + }); + } + + IsHodlingPublicKey( + endpoint: string, + PublicKeyBase58Check: string, + IsHodlingPublicKeyBase58Check: string, + IsDAOCoin: boolean + ): Observable<{ IsHodling: boolean; BalanceEntry: BalanceEntryResponse }> { + return this.post(endpoint, BackendRoutes.RoutePathIsHodlingPublicKey, { + PublicKeyBase58Check, + IsHodlingPublicKeyBase58Check, + IsDAOCoin, }); } + UpdateProfile( endpoint: string, // Specific fields @@ -1465,7 +1539,12 @@ export class BackendApiService { EncryptedHex: message.EncryptedText, PublicKey: message.IsSender ? message.RecipientPublicKeyBase58Check : message.SenderPublicKeyBase58Check, IsSender: message.IsSender, - Legacy: !message.V2, + Legacy: !message.V2 && (!message.Version || message.Version < 2), + Version: message.Version, + SenderMessagingPublicKey: message.SenderMessagingPublicKey, + SenderMessagingGroupKeyName: message.SenderMessagingGroupKeyName, + RecipientMessagingPublicKey: message.RecipientMessagingPublicKey, + RecipientMessagingGroupKeyName: message.RecipientMessagingGroupKeyName, })) ); return { ...res, encryptedMessages }; @@ -1690,6 +1769,46 @@ export class BackendApiService { return request; } + DAOCoin( + endpoint: string, + UpdaterPublicKeyBase58Check: string, + ProfilePublicKeyBase58CheckOrUsername: string, + OperationType: DAOCoinOperationTypeString, + TransferRestrictionStatus: TransferRestrictionStatusString | undefined, + CoinsToMintNanos: Hex | undefined, + CoinsToBurnNanos: Hex | undefined, + MinFeeRateNanosPerKB: number + ): Observable { + const request = this.post(endpoint, BackendRoutes.RoutePathDAOCoin, { + UpdaterPublicKeyBase58Check, + ProfilePublicKeyBase58CheckOrUsername, + OperationType, + CoinsToMintNanos, + CoinsToBurnNanos, + TransferRestrictionStatus, + MinFeeRateNanosPerKB, + }); + return this.signAndSubmitTransaction(endpoint, request, UpdaterPublicKeyBase58Check); + } + + TransferDAOCoin( + endpoint: string, + SenderPublicKeyBase58Check: string, + ProfilePublicKeyBase58CheckOrUsername: string, + ReceiverPublicKeyBase58CheckOrUsername: string, + DAOCoinToTransferNanos: Hex, + MinFeeRateNanosPerKB: number + ): Observable { + const request = this.post(endpoint, BackendRoutes.RoutePathTransferDAOCoin, { + SenderPublicKeyBase58Check, + ProfilePublicKeyBase58CheckOrUsername, + ReceiverPublicKeyBase58CheckOrUsername, + DAOCoinToTransferNanos, + MinFeeRateNanosPerKB, + }); + return this.signAndSubmitTransaction(endpoint, request, SenderPublicKeyBase58Check); + } + BlockPublicKey( endpoint: string, PublicKeyBase58Check: string, @@ -1820,11 +1939,10 @@ export class BackendApiService { }); } - QueryETHRPC(endpoint: string, Method: string, Params: string[], PublicKeyBase58Check: string): Observable { - return this.jwtPost(endpoint, BackendRoutes.RoutePathQueryETHRPC, PublicKeyBase58Check, { + QueryETHRPC(endpoint: string, Method: string, Params: string[]): Observable { + return this.post(endpoint, BackendRoutes.RoutePathQueryETHRPC, { Method, Params, - PublicKeyBase58Check, }); } 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 f47651749..6cb220142 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 @@ -537,7 +537,7 @@ export class BuyDeSoEthComponent implements OnInit { queryETHRPC(method: string, params: any[]): Promise { return this.backendApi - .QueryETHRPC(this.globalVars.localNode, method, params, this.globalVars.loggedInUser?.PublicKeyBase58Check) + .QueryETHRPC(this.globalVars.localNode, method, params) .toPromise() .then( (res) => { 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 f9aa94ed3..8830f4a9c 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 @@ -5,7 +5,7 @@
- Minimum price (Optional) + Minimum Bid Amount (Optional)
@@ -83,7 +83,7 @@
- Holders of ${{ profile.Username }} coin + Holders of ${{ profile.Username }}'s Creator Coin
@@ -109,6 +109,26 @@
+ +
+
+
+ Holders of ${{ profile.Username }}'s DAO Coin +
+
+ +
+
+
+
Username or PubKey
+
Coins Held
+
+ +
+
+
+
+
diff --git a/src/app/creator-profile-page/creator-profile-details/creator-profile-details.component.ts b/src/app/creator-profile-page/creator-profile-details/creator-profile-details.component.ts index 1e9065b0f..e620d1d9a 100644 --- a/src/app/creator-profile-page/creator-profile-details/creator-profile-details.component.ts +++ b/src/app/creator-profile-page/creator-profile-details/creator-profile-details.component.ts @@ -21,6 +21,7 @@ export class CreatorProfileDetailsComponent implements OnInit { // Leaving this one in so old links will direct to the Coin Purchasers tab. "creator-coin": "Creator Coin", "coin-purchasers": "Creator Coin", + dao: "DAO Coin", diamonds: "Diamonds", nfts: "NFTs", }; @@ -29,6 +30,7 @@ export class CreatorProfileDetailsComponent implements OnInit { "Creator Coin": "creator-coin", Diamonds: "diamonds", NFTs: "nfts", + "DAO Coin": "dao", }; appData: GlobalVarsService; userName: string; diff --git a/src/app/creator-profile-page/creator-profile-hodlers/creator-profile-hodlers.component.html b/src/app/creator-profile-page/creator-profile-hodlers/creator-profile-hodlers.component.html index ecc3b19d3..9056de1e0 100644 --- a/src/app/creator-profile-page/creator-profile-hodlers/creator-profile-hodlers.component.html +++ b/src/app/creator-profile-page/creator-profile-hodlers/creator-profile-hodlers.component.html @@ -1,7 +1,12 @@
- No one owns ${{ profile.Username }} coin yet.  - Be the first! + No one owns ${{ profile.Username }}'s {{ isDAOCoin ? "DAO" : "Creator" }} Coin yet.  + + Be the first! +
@@ -23,6 +28,7 @@ matTooltipClass="global__mat-tooltip global__mat-tooltip-font-size" [matTooltip]="getTooltipForRow(row)" #tooltip="matTooltip" + *ngIf="!isDAOCoin" >
@@ -80,6 +86,7 @@ matTooltipClass="global__mat-tooltip global__mat-tooltip-font-size" [matTooltip]="getTooltipForRow(row)" #tooltip="matTooltip" + *ngIf="!isDAOCoin" >
@@ -99,20 +106,23 @@
-
- {{ (row.BalanceNanos / 1e9).toFixed(4) }} +
+ {{ isDAOCoin ? globalVars.hexNanosToUnitString(row.BalanceNanosUint256) : (row.BalanceNanos / 1e9).toFixed(4) }}
-
+
≈ {{ globalVars.creatorCoinNanosToUSDNaive(row.BalanceNanos, profile.CoinPriceDeSoNanos, true) }}
Total
-
- {{ (profile.CoinEntry.CoinsInCirculationNanos / 1e9).toFixed(4) }} +
+ {{ isDAOCoin ? + globalVars.hexNanosToUnitString(profile.DAOCoinEntry.CoinsInCirculationNanos) : + (profile.CoinEntry.CoinsInCirculationNanos / 1e9).toFixed(4) + }}
-
+
≈ {{ globalVars.creatorCoinNanosToUSDNaive( diff --git a/src/app/creator-profile-page/creator-profile-hodlers/creator-profile-hodlers.component.ts b/src/app/creator-profile-page/creator-profile-hodlers/creator-profile-hodlers.component.ts index dc7ab3353..18246a184 100644 --- a/src/app/creator-profile-page/creator-profile-hodlers/creator-profile-hodlers.component.ts +++ b/src/app/creator-profile-page/creator-profile-hodlers/creator-profile-hodlers.component.ts @@ -17,6 +17,7 @@ export class CreatorProfileHodlersComponent { constructor(private globalVars: GlobalVarsService, private backendApi: BackendApiService) {} @Input() profile: ProfileEntryResponse; + @Input() isDAOCoin: boolean = false; showTotal = false; lastPage = null; @@ -40,7 +41,8 @@ export class CreatorProfileHodlersComponent { lastPublicKeyBase58Check, CreatorProfileHodlersComponent.PAGE_SIZE, false, - false + false, + this.isDAOCoin, ) .toPromise() .then((res) => { diff --git a/src/app/creator-profile-page/creator-profile-top-card/creator-profile-top-card.component.ts b/src/app/creator-profile-page/creator-profile-top-card/creator-profile-top-card.component.ts index 463168666..b6ccb81f4 100644 --- a/src/app/creator-profile-page/creator-profile-top-card/creator-profile-top-card.component.ts +++ b/src/app/creator-profile-page/creator-profile-top-card/creator-profile-top-card.component.ts @@ -66,7 +66,7 @@ export class CreatorProfileTopCardComponent implements OnInit, OnDestroy { reportUser(): void { this.globalVars.logEvent("post : report-user"); window.open( - `https://report.bitclout.com/account?ReporterPublicKey=${this.globalVars.loggedInUser?.PublicKeyBase58Check}&ReportedAccountPublicKey=${this.profile.PublicKeyBase58Check}` + `https://desoreporting.aidaform.com/account?ReporterPublicKey=${this.globalVars.loggedInUser?.PublicKeyBase58Check}&ReportedAccountPublicKey=${this.profile.PublicKeyBase58Check}` ); } diff --git a/src/app/dao-coins/burn-dao-coin-modal/burn-dao-coin-modal.component.html b/src/app/dao-coins/burn-dao-coin-modal/burn-dao-coin-modal.component.html new file mode 100644 index 000000000..b95b67203 --- /dev/null +++ b/src/app/dao-coins/burn-dao-coin-modal/burn-dao-coin-modal.component.html @@ -0,0 +1,35 @@ +
+
+ Burn {{ balanceEntryResponse.ProfileEntryResponse?.Username }} DAO Coins +
+
+ Your Balance: {{ globalVars.hexNanosToUnitString(balanceEntryResponse.BalanceNanosUint256) }} {{ balanceEntryResponse.ProfileEntryResponse?.Username }} DAO Coins +
+
+
+ Amount To Burn +
+ +
+ + +
+ {{ validationError }} +
+
+
+ {{ backendErrors }} +
+
diff --git a/src/app/dao-coins/burn-dao-coin-modal/burn-dao-coin-modal.component.ts b/src/app/dao-coins/burn-dao-coin-modal/burn-dao-coin-modal.component.ts new file mode 100644 index 000000000..b9a29639b --- /dev/null +++ b/src/app/dao-coins/burn-dao-coin-modal/burn-dao-coin-modal.component.ts @@ -0,0 +1,62 @@ +import { Component, Input } from "@angular/core"; +import { BsModalRef, BsModalService } from "ngx-bootstrap/modal"; +import { GlobalVarsService } from "../../global-vars.service"; +import { BackendApiService, BalanceEntryResponse, DAOCoinOperationTypeString } from "../../backend-api.service"; +import { toBN } from "web3-utils"; + +@Component({ + selector: "burn-dao-coin-modal", + templateUrl: "./burn-dao-coin-modal.component.html", +}) +export class BurnDaoCoinModalComponent { + @Input() balanceEntryResponse: BalanceEntryResponse; + + amountToBurn: number = 0; + burningDAOCoin: boolean = false; + validationErrors: string[] = []; + backendErrors: string = ""; + constructor( + public bsModalRef: BsModalRef, + public modalService: BsModalService, + public globalVars: GlobalVarsService, + private backendApi: BackendApiService + ) {} + + burnDAOCoin(): void { + this.burningDAOCoin = true; + this.backendErrors = ""; + this.backendApi + .DAOCoin( + this.globalVars.localNode, + this.globalVars.loggedInUser?.PublicKeyBase58Check, + this.balanceEntryResponse.CreatorPublicKeyBase58Check, + DAOCoinOperationTypeString.BURN, + undefined, + undefined, + this.globalVars.toHexNanos(this.amountToBurn), + this.globalVars.defaultFeeRateNanosPerKB + ) + .subscribe( + (res) => { + this.modalService.setDismissReason(`dao coins burned|${this.globalVars.toHexNanos(this.amountToBurn)}`); + this.bsModalRef.hide(); + }, + (err) => { + this.backendErrors = err.error.error; + console.error(err); + } + ) + .add(() => (this.burningDAOCoin = false)); + } + + updateValidationErrors(): void { + let err: string[] = []; + if (this.amountToBurn <= 0) { + err.push("Must transfer a non-zero amount\n"); + } + if (this.globalVars.unitToBNNanos(this.amountToBurn || 0).gt(toBN(this.balanceEntryResponse.BalanceNanosUint256))) { + err.push("Amount to burn exceeds balance\n"); + } + this.validationErrors = err; + } +} diff --git a/src/app/dao-coins/dao-coins-page/dao-coins-page.component.html b/src/app/dao-coins/dao-coins-page/dao-coins-page.component.html new file mode 100644 index 000000000..4a41fca29 --- /dev/null +++ b/src/app/dao-coins/dao-coins-page/dao-coins-page.component.html @@ -0,0 +1,3 @@ + + + diff --git a/src/app/dao-coins/dao-coins-page/dao-coins-page.component.scss b/src/app/dao-coins/dao-coins-page/dao-coins-page.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/dao-coins/dao-coins-page/dao-coins-page.component.spec.ts b/src/app/dao-coins/dao-coins-page/dao-coins-page.component.spec.ts new file mode 100644 index 000000000..8a6e94c61 --- /dev/null +++ b/src/app/dao-coins/dao-coins-page/dao-coins-page.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; + +import { DaoCoinsPageComponent } from "./wallet-page.component"; + +describe("WalletPageComponent", () => { + let component: DaoCoinsPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DaoCoinsPageComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DaoCoinsPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dao-coins/dao-coins-page/dao-coins-page.component.ts b/src/app/dao-coins/dao-coins-page/dao-coins-page.component.ts new file mode 100644 index 000000000..90e286ee1 --- /dev/null +++ b/src/app/dao-coins/dao-coins-page/dao-coins-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; +import { GlobalVarsService } from "../../global-vars.service"; + +@Component({ + selector: "dao-coins-page", + templateUrl: "./dao-coins-page.component.html", + styleUrls: ["./dao-coins-page.component.scss"], +}) +export class DaoCoinsPageComponent { + constructor(public globalVars: GlobalVarsService) {} +} diff --git a/src/app/dao-coins/dao-coins.component.html b/src/app/dao-coins/dao-coins.component.html new file mode 100644 index 000000000..721bab230 --- /dev/null +++ b/src/app/dao-coins/dao-coins.component.html @@ -0,0 +1,263 @@ + +
+ +
+
DAO Coins
+
+
+ +
+ + + + +
+
My DAO
+
+
+ Coins In Circulation:  + {{ globalVars.hexNanosToUnitString(myDAOCoin?.CoinsInCirculationNanos || 0) }} +
+
+ Transfer Restriction Status:  + {{ getDisplayTransferRestrictionStatus(myDAOCoin?.TransferRestrictionStatus) }} +
+
+ Minting Disabled:  + {{ myDAOCoin?.MintingDisabled || false }} +
+
+
+ + + +
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + +
+
DAO Coins
+
+ + + + +
+ + +
+
+
+ {{ emptyHodlerListMessage() }} +
+ +
+ +
+
+ +
+ +
+
+ + + +
diff --git a/src/app/dao-coins/dao-coins.component.ts b/src/app/dao-coins/dao-coins.component.ts new file mode 100644 index 000000000..3edece213 --- /dev/null +++ b/src/app/dao-coins/dao-coins.component.ts @@ -0,0 +1,521 @@ +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { GlobalVarsService } from "../global-vars.service"; +import { AppRoutingModule } from "../app-routing.module"; +import { + BackendApiService, + BalanceEntryResponse, + DAOCoinEntryResponse, + DAOCoinOperationTypeString, + TransferRestrictionStatusString, +} from "../backend-api.service"; +import { Title } from "@angular/platform-browser"; +import { ActivatedRoute, Router } from "@angular/router"; +import { InfiniteScroller } from "../infinite-scroller"; +import { IAdapter, IDatasource } from "ngx-ui-scroll"; +import { Observable, Subscription, throwError, zip } from "rxjs"; +import { environment } from "src/environments/environment"; +import { toBN } from "web3-utils"; +import { catchError, map } from "rxjs/operators"; +import { BsModalService } from "ngx-bootstrap/modal"; +import { TransferDAOCoinModalComponent } from "./transfer-dao-coin-modal/transfer-dao-coin-modal.component"; +import { BurnDaoCoinModalComponent } from "./burn-dao-coin-modal/burn-dao-coin-modal.component"; +import { SwalHelper } from "../../lib/helpers/swal-helper"; + +@Component({ + selector: "dao-coins", + templateUrl: "./dao-coins.component.html", +}) +export class DaoCoinsComponent implements OnInit, OnDestroy { + static PAGE_SIZE = 20; + static BUFFER_SIZE = 10; + static WINDOW_VIEWPORT = true; + static PADDING = 0.5; + + @Input() inTutorial: boolean; + + globalVars: GlobalVarsService; + AppRoutingModule = AppRoutingModule; + hasUnminedCreatorCoins: boolean; + + sortedCoinsFromHighToLow: number = 0; + sortedUsernameFromHighToLow: number = 0; + hideMyDAOTab: boolean = false; + showDAOCoinHoldings: boolean = false; + + myDAOCoin: DAOCoinEntryResponse; + myDAOCapTable: BalanceEntryResponse[] = []; + daoCoinHoldings: BalanceEntryResponse[] = []; + + loadingMyDAOCapTable: boolean = false; + loadingMyDAOCoinHoldings: boolean = false; + loadingNewSelection: boolean = false; + + static myDAOTab: string = "My DAO"; + static daoCoinsTab: string = "DAO Holdings"; + tabs = [DaoCoinsComponent.myDAOTab, DaoCoinsComponent.daoCoinsTab]; + activeTab: string = DaoCoinsComponent.myDAOTab; + balanceEntryToHihlight: BalanceEntryResponse; + + TransferRestrictionStatusString = TransferRestrictionStatusString; + transferRestrictionStatus: TransferRestrictionStatusString; + coinsToMint: number = 0; + coinsToBurn: number = 0; + mintingDAOCoin: boolean = false; + disablingMinting: boolean = false; + burningDAOCoin: boolean = false; + updatingTransferRestrictionStatus: boolean = false; + + transferRestrictionStatusOptions = [ + TransferRestrictionStatusString.UNRESTRICTED, + TransferRestrictionStatusString.PROFILE_OWNER_ONLY, + TransferRestrictionStatusString.DAO_MEMBERS_ONLY, + TransferRestrictionStatusString.PERMANENTLY_UNRESTRICTED, + ]; + + constructor( + private appData: GlobalVarsService, + private titleService: Title, + private router: Router, + private route: ActivatedRoute, + public backendApi: BackendApiService, + private modalService: BsModalService + ) { + this.globalVars = appData; + } + + subscriptions = new Subscription(); + + ngOnInit() { + // Don't look up my DAO if I don't have a profile + if (this.globalVars.loggedInUser?.ProfileEntryResponse) { + this.myDAOCoin = this.globalVars.loggedInUser.ProfileEntryResponse.DAOCoinEntry; + this.transferRestrictionStatus = + this.myDAOCoin?.TransferRestrictionStatus || TransferRestrictionStatusString.UNRESTRICTED; + this.loadMyDAOCapTable().subscribe((res) => {}); + } else { + this.hideMyDAOTab = true; + this.showDAOCoinHoldings = true; + this.activeTab = DaoCoinsComponent.daoCoinsTab; + this.tabs = [DaoCoinsComponent.daoCoinsTab]; + } + this.loadMyDAOCoinHoldings().subscribe((res) => {}); + this.titleService.setTitle(`DAO Coins - ${environment.node.name}`); + } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } + + loadMyDAOCapTable(): Observable { + this.loadingMyDAOCapTable = true; + return this.backendApi + .GetHodlersForPublicKey( + this.globalVars.localNode, + this.globalVars.loggedInUser?.PublicKeyBase58Check, + "", + "", + 0, + false, + true, + true + ) + .pipe( + map((res) => { + this.myDAOCapTable = res.Hodlers || []; + this.loadingMyDAOCapTable = false; + return res.Hodlers; + }), + catchError((err) => { + console.error(err); + this.loadingMyDAOCapTable = false; + return throwError(err); + }) + ); + } + + loadMyDAOCoinHoldings(): Observable { + this.loadingMyDAOCoinHoldings = true; + return this.backendApi + .GetHodlersForPublicKey( + this.globalVars.localNode, + this.globalVars.loggedInUser?.PublicKeyBase58Check, + "", + "", + 0, + true, + true, + true + ) + .pipe( + map((res) => { + this.daoCoinHoldings = res.Hodlers || []; + this.loadingMyDAOCoinHoldings = false; + this.loadingMyDAOCoinHoldings = false; + return res.Hodlers; + }), + catchError((err) => { + console.error(err); + this.loadingMyDAOCoinHoldings = false; + return throwError(err); + }) + ); + } + + // Thanks to @brabenetz for the solution on forward padding with the ngx-ui-scroll component. + // https://github.com/dhilt/ngx-ui-scroll/issues/111#issuecomment-697269318 + correctDataPaddingForwardElementHeight(viewportElement: HTMLElement): void { + const dataPaddingForwardElement: HTMLElement = viewportElement.querySelector(`[data-padding-forward]`); + if (dataPaddingForwardElement) { + dataPaddingForwardElement.setAttribute("style", "height: 0px;"); + } + } + + // sort by Coins held + sortHodlingsCoins(hodlings: BalanceEntryResponse[], descending: boolean): void { + this.sortedUsernameFromHighToLow = 0; + this.sortedCoinsFromHighToLow = descending ? -1 : 1; + hodlings.sort((a: BalanceEntryResponse, b: BalanceEntryResponse) => { + return this.sortedCoinsFromHighToLow * (a.BalanceNanos - b.BalanceNanos); + }); + } + + // sort by username + sortHodlingsUsername(hodlings: BalanceEntryResponse[], descending: boolean): void { + this.sortedUsernameFromHighToLow = descending ? -1 : 1; + this.sortedCoinsFromHighToLow = 0; + hodlings.sort((a: BalanceEntryResponse, b: BalanceEntryResponse) => { + return ( + this.sortedUsernameFromHighToLow * + b.ProfileEntryResponse.Username.localeCompare(a.ProfileEntryResponse.Username) + ); + }); + } + + sortWallet(column: string) { + let descending: boolean; + switch (column) { + case "username": + // code block + descending = this.sortedUsernameFromHighToLow !== -1; + this.sortHodlingsUsername(this.myDAOCapTable, descending); + this.sortHodlingsUsername(this.daoCoinHoldings, descending); + break; + case "coins": + descending = this.sortedCoinsFromHighToLow !== -1; + this.sortHodlingsCoins(this.myDAOCapTable, descending); + this.sortHodlingsCoins(this.daoCoinHoldings, descending); + break; + default: + // do nothing + } + this.scrollerReset(); + } + + mintDAOCoin(): void { + if (this.myDAOCoin.MintingDisabled || this.mintingDAOCoin || this.coinsToMint <= 0) { + return; + } + SwalHelper.fire({ + target: this.globalVars.getTargetComponentSelector(), + title: "Mint DAO Coins", + html: `Click confirm to mint ${this.coinsToMint} ${this.globalVars.loggedInUser?.ProfileEntryResponse?.Username} DAO coins`, + showCancelButton: true, + customClass: { + confirmButton: "btn btn-light", + cancelButton: "btn btn-light no", + }, + reverseButtons: true, + }).then((res: any) => { + if (res.isConfirmed) { + this.loadingNewSelection = true; + this.mintingDAOCoin = true; + this.doDAOCoinTxn(this.globalVars.loggedInUser?.PublicKeyBase58Check, DAOCoinOperationTypeString.MINT) + .subscribe( + (res) => { + this.myDAOCoin.CoinsInCirculationNanos = toBN(this.myDAOCoin.CoinsInCirculationNanos) + .add(toBN(this.globalVars.toHexNanos(this.coinsToMint))) + .toString("hex"); + zip(this.loadMyDAOCapTable(), this.loadMyDAOCoinHoldings()).subscribe(() => { + this.loadingNewSelection = false; + this._handleTabClick(this.activeTab); + }); + this.coinsToMint = 0; + }, + (err) => { + this.globalVars._alertError(err.error.error); + console.error(err); + } + ) + .add(() => { + this.mintingDAOCoin = false; + this.loadingNewSelection = false; + }); + } + }); + } + + disableMinting(): void { + if (this.myDAOCoin.MintingDisabled || this.disablingMinting) { + return; + } + SwalHelper.fire({ + target: this.globalVars.getTargetComponentSelector(), + title: "Disable Minting", + html: `Click confirm to disable minting for ${this.globalVars.loggedInUser?.ProfileEntryResponse?.Username} DAO coins. Please note, this is irreversible.`, + showCancelButton: true, + customClass: { + confirmButton: "btn btn-light", + cancelButton: "btn btn-light no", + }, + reverseButtons: true, + }).then((res: any) => { + if (res.isConfirmed) { + this.disablingMinting = true; + this.doDAOCoinTxn( + this.globalVars.loggedInUser?.PublicKeyBase58Check, + DAOCoinOperationTypeString.DISABLE_MINTING + ) + .subscribe( + (res) => { + this.myDAOCoin.MintingDisabled = true; + }, + (err) => { + this.globalVars._alertError(err.error.error); + console.error(err); + } + ) + .add(() => (this.disablingMinting = false)); + } + }); + } + + updateTransferRestrictionStatus(): void { + if ( + this.myDAOCoin.TransferRestrictionStatus === TransferRestrictionStatusString.PERMANENTLY_UNRESTRICTED || + this.updatingTransferRestrictionStatus || + this.transferRestrictionStatus === this.myDAOCoin.TransferRestrictionStatus + ) { + return; + } + SwalHelper.fire({ + target: this.globalVars.getTargetComponentSelector(), + title: "Update Transfer Restriction Status", + html: `Click confirm to update the transfer restriction status to ${this.getDisplayTransferRestrictionStatus( + this.transferRestrictionStatus + )} for ${this.globalVars.loggedInUser?.ProfileEntryResponse?.Username} DAO coins`, + showCancelButton: true, + customClass: { + confirmButton: "btn btn-light", + cancelButton: "btn btn-light no", + }, + reverseButtons: true, + }).then((res: any) => { + if (res.isConfirmed) { + this.updatingTransferRestrictionStatus = true; + this.doDAOCoinTxn( + this.globalVars.loggedInUser?.PublicKeyBase58Check, + DAOCoinOperationTypeString.UPDATE_TRANSFER_RESTRICTION_STATUS + ) + .subscribe( + (res) => { + this.myDAOCoin.TransferRestrictionStatus = this.transferRestrictionStatus; + }, + (err) => { + this.globalVars._alertError(err.error.error); + console.error(err); + } + ) + .add(() => (this.updatingTransferRestrictionStatus = false)); + } + }); + } + + burnDAOCoin(profilePublicKeyBase58Check: string): void { + if (this.burningDAOCoin || this.coinsToBurn <= 0) { + return; + } + SwalHelper.fire({ + target: this.globalVars.getTargetComponentSelector(), + title: "Burn DAO Coins", + html: `Click confirm to burn ${this.coinsToBurn} ${this.globalVars.loggedInUser?.ProfileEntryResponse?.Username} DAO coins`, + showCancelButton: true, + customClass: { + confirmButton: "btn btn-light", + cancelButton: "btn btn-light no", + }, + reverseButtons: true, + }).then((res: any) => { + if (res.isConfirmed) { + this.burningDAOCoin = true; + this.loadingNewSelection = true; + this.doDAOCoinTxn(this.globalVars.loggedInUser?.PublicKeyBase58Check, DAOCoinOperationTypeString.BURN) + .subscribe( + (res) => { + if (profilePublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check) { + this.myDAOCoin.CoinsInCirculationNanos = toBN(this.myDAOCoin.CoinsInCirculationNanos) + .add(toBN(this.globalVars.toHexNanos(this.coinsToBurn))) + .toString("hex"); + } + this.coinsToBurn = 0; + }, + (err) => { + this.globalVars._alertError(err.error.error); + console.error(err); + } + ) + .add(() => { + this.burningDAOCoin = false; + this.loadingNewSelection = false; + }); + } + }); + } + + doDAOCoinTxn(profilePublicKeyBase58Check: string, operationType: DAOCoinOperationTypeString): Observable { + if ( + profilePublicKeyBase58Check !== this.globalVars.loggedInUser?.PublicKeyBase58Check && + operationType !== DAOCoinOperationTypeString.BURN + ) { + return throwError("invalid dao coin operation - must be owner to perform " + operationType); + } + return this.backendApi.DAOCoin( + this.globalVars.localNode, + this.globalVars.loggedInUser?.PublicKeyBase58Check, + profilePublicKeyBase58Check, + operationType, + operationType === DAOCoinOperationTypeString.UPDATE_TRANSFER_RESTRICTION_STATUS + ? this.transferRestrictionStatus + : undefined, + operationType === DAOCoinOperationTypeString.MINT ? this.globalVars.toHexNanos(this.coinsToMint) : undefined, + operationType === DAOCoinOperationTypeString.BURN ? this.globalVars.toHexNanos(this.coinsToBurn) : undefined, + this.globalVars.defaultFeeRateNanosPerKB + ); + } + + unminedDeSoToolTip() { + return ( + "Mining in progress. Feel free to transact in the meantime.\n\n" + + "Mined balance:\n" + + this.globalVars.nanosToDeSo(this.globalVars.loggedInUser.BalanceNanos, 9) + + " DeSo.\n\n" + + "Unmined balance:\n" + + this.globalVars.nanosToDeSo(this.globalVars.loggedInUser.UnminedBalanceNanos, 9) + + " DeSo." + ); + } + + unminedCreatorCoinToolTip(creator: any) { + return ( + "Mining in progress. Feel free to transact in the meantime.\n\n" + + "Net unmined transactions:\n" + + this.globalVars.nanosToDeSo(creator.NetBalanceInMempool, 9) + + " DeSo.\n\n" + + "Balance w/unmined transactions:\n" + + this.globalVars.nanosToDeSo(creator.BalanceNanos, 9) + + " DeSo.\n\n" + ); + } + + usernameTruncationLength(): number { + return this.globalVars.isMobile() ? 14 : 20; + } + + emptyHodlerListMessage(): string { + return this.showDAOCoinHoldings ? "You don't hold any DAO coins" : "Your DAO doesn't have any coins yet."; + } + + _handleTabClick(tab: string) { + this.showDAOCoinHoldings = tab === DaoCoinsComponent.daoCoinsTab; + this.lastPage = Math.floor( + (this.showDAOCoinHoldings ? this.daoCoinHoldings : this.myDAOCapTable).length / DaoCoinsComponent.PAGE_SIZE + ); + this.activeTab = tab; + this.scrollerReset(); + } + + scrollerReset() { + this.infiniteScroller.reset(); + this.datasource.adapter.reset().then(() => this.datasource.adapter.check()); + } + + lastPage = null; + infiniteScroller: InfiniteScroller = new InfiniteScroller( + DaoCoinsComponent.PAGE_SIZE, + this.getPage.bind(this), + DaoCoinsComponent.WINDOW_VIEWPORT, + DaoCoinsComponent.BUFFER_SIZE, + DaoCoinsComponent.PADDING + ); + datasource: IDatasource> = this.infiniteScroller.getDatasource(); + + getPage(page: number) { + if (this.lastPage != null && page > this.lastPage) { + return []; + } + + const startIdx = page * DaoCoinsComponent.PAGE_SIZE; + const endIdx = (page + 1) * DaoCoinsComponent.PAGE_SIZE; + + return new Promise((resolve, reject) => { + resolve( + this.showDAOCoinHoldings + ? this.daoCoinHoldings.slice(startIdx, Math.min(endIdx, this.daoCoinHoldings.length)) + : this.myDAOCapTable.slice(startIdx, Math.min(endIdx, this.myDAOCapTable.length)) + ); + }); + } + + getDisplayTransferRestrictionStatus(transferRestrictionStatus: TransferRestrictionStatusString): string { + // If we're not provided a value, we assume it's unrestricted. + transferRestrictionStatus = transferRestrictionStatus || TransferRestrictionStatusString.UNRESTRICTED; + return transferRestrictionStatus + .split("_") + .map((status) => status.charAt(0).toUpperCase() + status.slice(1)) + .join(" ") + .replace("Dao", "DAO"); + } + + openTransferDAOCoinModal(creator: BalanceEntryResponse): void { + const modalDetails = this.modalService.show(TransferDAOCoinModalComponent, { + class: "modal-dialog-centered", + initialState: { balanceEntryResponse: creator }, + }); + const onHideEvent = modalDetails.onHide; + onHideEvent.subscribe((response) => { + if (response === "dao coins transferred") { + this.loadingNewSelection = true; + zip(this.loadMyDAOCoinHoldings(), this.loadMyDAOCapTable()).subscribe((res) => { + this.loadingNewSelection = false; + this._handleTabClick(this.activeTab); + }); + } + }); + } + + openBurnDAOCoinModal(creator: BalanceEntryResponse): void { + const modalDetails = this.modalService.show(BurnDaoCoinModalComponent, { + class: "modal-dialog-centered", + initialState: { balanceEntryResponse: creator }, + }); + const onHideEvent = modalDetails.onHide; + onHideEvent.subscribe((response) => { + if (response.startsWith("dao coins burned")) { + this.loadingNewSelection = true; + zip(this.loadMyDAOCoinHoldings(), this.loadMyDAOCapTable()).subscribe((res) => { + // If we burned our own coin in the modal, update the coins in circulation. + if (creator.CreatorPublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check) { + const splitResponse = response.split("|"); + if (splitResponse.length === 2) { + const burnAmountHex = splitResponse[1]; + this.myDAOCoin.CoinsInCirculationNanos = toBN(this.myDAOCoin.CoinsInCirculationNanos) + .sub(toBN(burnAmountHex)) + .toString("hex"); + } + } + this.loadingNewSelection = false; + this._handleTabClick(this.activeTab); + }); + } + }); + } +} diff --git a/src/app/dao-coins/transfer-dao-coin-modal/transfer-dao-coin-modal.component.html b/src/app/dao-coins/transfer-dao-coin-modal/transfer-dao-coin-modal.component.html new file mode 100644 index 000000000..e063a59cf --- /dev/null +++ b/src/app/dao-coins/transfer-dao-coin-modal/transfer-dao-coin-modal.component.html @@ -0,0 +1,47 @@ +
+
+ Transfer {{ balanceEntryResponse.ProfileEntryResponse?.Username }} DAO Coins +
+ + +
+ Your Balance: {{ globalVars.hexNanosToUnitString(balanceEntryResponse.BalanceNanosUint256) }} {{ balanceEntryResponse.ProfileEntryResponse?.Username }} DAO Coins +
+
+
+ Amount To Transfer +
+ +
+ + +
+ {{ validationError }} +
+
+
+ {{ backendErrors }} +
+
diff --git a/src/app/dao-coins/transfer-dao-coin-modal/transfer-dao-coin-modal.component.ts b/src/app/dao-coins/transfer-dao-coin-modal/transfer-dao-coin-modal.component.ts new file mode 100644 index 000000000..ca559c7c3 --- /dev/null +++ b/src/app/dao-coins/transfer-dao-coin-modal/transfer-dao-coin-modal.component.ts @@ -0,0 +1,127 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { BsModalRef, BsModalService } from "ngx-bootstrap/modal"; +import { GlobalVarsService } from "../../global-vars.service"; +import { + BackendApiService, + BalanceEntryResponse, + ProfileEntryResponse, + TransferRestrictionStatusString, +} from "../../backend-api.service"; +import { toBN } from "web3-utils"; + +@Component({ + selector: "transfer-dao-coin-modal", + templateUrl: "./transfer-dao-coin-modal.component.html", +}) +export class TransferDAOCoinModalComponent implements OnInit { + @Input() balanceEntryResponse: BalanceEntryResponse; + + amountToTransfer: number = 0; + receiver: ProfileEntryResponse; + receiverIsDAOMember: boolean = false; + transferringDAOCoin: boolean = false; + backendErrors: string = ""; + validationErrors: string[] = []; + hideCreatorSearch: boolean = false; + constructor( + public bsModalRef: BsModalRef, + public modalService: BsModalService, + public globalVars: GlobalVarsService, + private backendApi: BackendApiService + ) {} + + ngOnInit(): void { + // If this DAO coin can only be transferred to the profile owner and we're not the profile owner, set the receiver + // to the profile owner and don't let them search. + if ( + this.balanceEntryResponse?.ProfileEntryResponse?.DAOCoinEntry?.TransferRestrictionStatus === + TransferRestrictionStatusString.PROFILE_OWNER_ONLY && + this.balanceEntryResponse?.CreatorPublicKeyBase58Check !== this.globalVars.loggedInUser?.PublicKeyBase58Check + ) { + this.hideCreatorSearch = true; + this.receiver = this.balanceEntryResponse?.ProfileEntryResponse; + } + } + + _handleCreatorSelectedInSearch(creator): void { + this.receiver = creator; + if ( + this.balanceEntryResponse.ProfileEntryResponse.DAOCoinEntry.TransferRestrictionStatus === + TransferRestrictionStatusString.DAO_MEMBERS_ONLY + ) { + this.backendApi + .IsHodlingPublicKey( + this.globalVars.localNode, + this.receiver.PublicKeyBase58Check, + this.balanceEntryResponse.CreatorPublicKeyBase58Check, + true + ) + .subscribe((res) => { + this.receiverIsDAOMember = res.IsHodling; + }) + .add(() => this.updateValidationErrors()); + } + this.updateValidationErrors(); + } + + transferDAOCoin(): void { + this.transferringDAOCoin = true; + this.backendErrors = ""; + this.backendApi + .TransferDAOCoin( + this.globalVars.localNode, + this.globalVars.loggedInUser?.PublicKeyBase58Check, + this.balanceEntryResponse.CreatorPublicKeyBase58Check, + this.receiver.PublicKeyBase58Check, + this.globalVars.toHexNanos(this.amountToTransfer), + this.globalVars.defaultFeeRateNanosPerKB + ) + .subscribe( + (res) => { + this.modalService.setDismissReason("dao coins transferred"); + this.bsModalRef.hide(); + }, + (err) => { + this.backendErrors = err.error.error; + console.error(err); + } + ) + .add(() => (this.transferringDAOCoin = false)); + } + + updateValidationErrors(): void { + let err: string[] = []; + if (this.receiver?.PublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check) { + err.push("Cannot transfer to yourself\n"); + } + if (this.receiver && this.amountToTransfer <= 0) { + err.push("Must transfer a non-zero amount\n"); + } + if ( + this.globalVars.unitToBNNanos(this.amountToTransfer || 0).gt(toBN(this.balanceEntryResponse.BalanceNanosUint256)) + ) { + err.push("Amount to transfer exceeds balance\n"); + } + if ( + this.receiver && + this.balanceEntryResponse.ProfileEntryResponse.DAOCoinEntry.TransferRestrictionStatus === + TransferRestrictionStatusString.PROFILE_OWNER_ONLY && + this.balanceEntryResponse.ProfileEntryResponse.PublicKeyBase58Check !== + this.globalVars.loggedInUser?.PublicKeyBase58Check && + this.balanceEntryResponse.ProfileEntryResponse.PublicKeyBase58Check !== this.receiver?.PublicKeyBase58Check + ) { + err.push("This DAO coin can only be transferred to or from the profile owner\n"); + } + if ( + this.receiver && + this.balanceEntryResponse.ProfileEntryResponse.DAOCoinEntry.TransferRestrictionStatus === + TransferRestrictionStatusString.DAO_MEMBERS_ONLY && + !this.receiverIsDAOMember && + this.balanceEntryResponse.ProfileEntryResponse.PublicKeyBase58Check !== + this.globalVars.loggedInUser?.PublicKeyBase58Check + ) { + err.push("This DAO coin can only be transferred to existing DAO members\n"); + } + this.validationErrors = err; + } +} diff --git a/src/app/feed/feed-create-post/feed-create-post.component.html b/src/app/feed/feed-create-post/feed-create-post.component.html index 1290d8822..158d52ad5 100644 --- a/src/app/feed/feed-create-post/feed-create-post.component.html +++ b/src/app/feed/feed-create-post/feed-create-post.component.html @@ -37,18 +37,18 @@ #autosize="cdkTextareaAutosize" > -
+
-
+
Video Processing In Progress