Skip to content

Commit f83d7ce

Browse files
authored
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
1 parent ef5021d commit f83d7ce

23 files changed

+1219
-64
lines changed

src/app/app.module.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -157,16 +157,16 @@ import { SanitizeVideoUrlPipe } from "../lib/pipes/sanitize-video-url-pipe";
157157
import { AdminNodeFeesComponent } from "./admin/admin-node-fees/admin-node-fees.component";
158158
import { AdminNodeAddFeesComponent } from "./admin/admin-node-fees/admin-node-add-fee/admin-node-add-fees.component";
159159
import { FreeDesoMessageComponent } from "./free-deso-message/free-deso-message.component";
160+
import { SupplyMonitoringStatsPageComponent } from "./supply-monitoring-stats-page/supply-monitoring-stats-page.component";
161+
import { SupplyMonitoringStatsComponent } from "./supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component";
162+
import { TransferNftModalComponent } from "./transfer-nft-modal/transfer-nft-modal.component";
163+
import { TransferNftAcceptModalComponent } from "./transfer-nft-accept-modal/transfer-nft-accept-modal.component";
164+
import { NftBurnModalComponent } from "./nft-burn-modal/nft-burn-modal.component";
165+
import { NftSelectSerialNumberComponent } from "./nft-select-serial-number/nft-select-serial-number.component";
160166

161167
// Modular Themes for DeSo by Carsen Klock @carsenk
162168
import { ThemeModule } from "./theme/theme.module";
163169
import { Theme } from "./theme/symbols";
164-
import {
165-
SupplyMonitoringStatsPageComponent
166-
} from "./supply-monitoring-stats-page/supply-monitoring-stats-page.component";
167-
import {
168-
SupplyMonitoringStatsComponent
169-
} from "./supply-monitoring-stats-page/supply-monitoring-stats/supply-monitoring-stats.component";
170170
const lightTheme: Theme = { key: "light", name: "Light Theme" };
171171
const darkTheme: Theme = { key: "dark", name: "Dark Theme" };
172172
const icydarkTheme: Theme = { key: "icydark", name: "Icy Dark Theme" };
@@ -311,6 +311,10 @@ const greenishTheme: Theme = { key: "greenish", name: "Green Theme" };
311311
SupplyMonitoringStatsPageComponent,
312312
SupplyMonitoringStatsComponent,
313313
FreeDesoMessageComponent,
314+
TransferNftAcceptModalComponent,
315+
TransferNftModalComponent,
316+
NftBurnModalComponent,
317+
NftSelectSerialNumberComponent,
314318
],
315319
imports: [
316320
BrowserModule,

src/app/backend-api.service.ts

+77-2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ export class BackendRoutes {
8484
static RoutePathGetNextNFTShowcase = "/api/v0/get-next-nft-showcase";
8585
static RoutePathGetNFTCollectionSummary = "/api/v0/get-nft-collection-summary";
8686
static RoutePathGetNFTEntriesForPostHash = "/api/v0/get-nft-entries-for-nft-post";
87+
static RoutePathTransferNFT = "/api/v0/transfer-nft";
88+
static RoutePathAcceptNFTTransfer = "/api/v0/accept-nft-transfer";
89+
static RoutePathBurnNFT = "/api/v0/burn-nft";
8790

8891
// ETH
8992
static RoutePathSubmitETHTx = "/api/v0/submit-eth-tx";
@@ -154,7 +157,7 @@ export class BackendRoutes {
154157
// Supply Monitoring endpoints
155158
static RoutePathGetTotalSupply = "/api/v0/total-supply";
156159
static RoutePathGetRichList = "/api/v0/rich-list";
157-
static RoutePathGetCountKeysWithDESO = "/api/v0/count-keys-with-deso"
160+
static RoutePathGetCountKeysWithDESO = "/api/v0/count-keys-with-deso";
158161
}
159162

160163
export class Transaction {
@@ -334,6 +337,7 @@ export class NFTEntryResponse {
334337
PostEntryResponse: PostEntryResponse | undefined;
335338
SerialNumber: number;
336339
IsForSale: boolean;
340+
IsPending?: boolean;
337341
MinBidAmountNanos: number;
338342
LastAcceptedBidAmountNanos: number;
339343

@@ -977,6 +981,75 @@ export class BackendApiService {
977981
return this.signAndSubmitTransaction(endpoint, request, UpdaterPublicKeyBase58Check);
978982
}
979983

984+
TransferNFT(
985+
endpoint: string,
986+
SenderPublicKeyBase58Check: string,
987+
ReceiverPublicKeyBase58Check: string,
988+
NFTPostHashHex: string,
989+
SerialNumber: number,
990+
UnencryptedUnlockableText: string,
991+
MinFeeRateNanosPerKB: number
992+
): Observable<any> {
993+
let request = UnencryptedUnlockableText
994+
? this.identityService.encrypt({
995+
...this.identityService.identityServiceParamsForKey(SenderPublicKeyBase58Check),
996+
recipientPublicKey: ReceiverPublicKeyBase58Check,
997+
message: UnencryptedUnlockableText,
998+
})
999+
: of({ encryptedMessage: "" });
1000+
request = request.pipe(
1001+
switchMap((encrypted) => {
1002+
const EncryptedUnlockableText = encrypted.encryptedMessage;
1003+
return this.post(endpoint, BackendRoutes.RoutePathTransferNFT, {
1004+
SenderPublicKeyBase58Check,
1005+
ReceiverPublicKeyBase58Check,
1006+
NFTPostHashHex,
1007+
SerialNumber,
1008+
EncryptedUnlockableText,
1009+
MinFeeRateNanosPerKB,
1010+
}).pipe(
1011+
map((request) => {
1012+
return { ...request };
1013+
})
1014+
);
1015+
})
1016+
);
1017+
1018+
return this.signAndSubmitTransaction(endpoint, request, SenderPublicKeyBase58Check);
1019+
}
1020+
1021+
AcceptNFTTransfer(
1022+
endpoint: string,
1023+
UpdaterPublicKeyBase58Check: string,
1024+
NFTPostHashHex: string,
1025+
SerialNumber: number,
1026+
MinFeeRateNanosPerKB: number
1027+
): Observable<any> {
1028+
const request = this.post(endpoint, BackendRoutes.RoutePathAcceptNFTTransfer, {
1029+
UpdaterPublicKeyBase58Check,
1030+
NFTPostHashHex,
1031+
SerialNumber,
1032+
MinFeeRateNanosPerKB,
1033+
});
1034+
return this.signAndSubmitTransaction(endpoint, request, UpdaterPublicKeyBase58Check);
1035+
}
1036+
1037+
BurnNFT(
1038+
endpoint: string,
1039+
UpdaterPublicKeyBase58Check: string,
1040+
NFTPostHashHex: string,
1041+
SerialNumber: number,
1042+
MinFeeRateNanosPerKB: number
1043+
): Observable<any> {
1044+
const request = this.post(endpoint, BackendRoutes.RoutePathBurnNFT, {
1045+
UpdaterPublicKeyBase58Check,
1046+
NFTPostHashHex,
1047+
SerialNumber,
1048+
MinFeeRateNanosPerKB,
1049+
});
1050+
return this.signAndSubmitTransaction(endpoint, request, UpdaterPublicKeyBase58Check);
1051+
}
1052+
9801053
DecryptUnlockableTexts(
9811054
ReaderPublicKeyBase58Check: string,
9821055
UnlockableNFTEntryResponses: NFTEntryResponse[]
@@ -1016,12 +1089,14 @@ export class BackendApiService {
10161089
endpoint: string,
10171090
UserPublicKeyBase58Check: string,
10181091
ReaderPublicKeyBase58Check: string,
1019-
IsForSale: boolean | null = null
1092+
IsForSale: boolean | null = null,
1093+
IsPending: boolean | null = null
10201094
): Observable<any> {
10211095
return this.post(endpoint, BackendRoutes.RoutePathGetNFTsForUser, {
10221096
UserPublicKeyBase58Check,
10231097
ReaderPublicKeyBase58Check,
10241098
IsForSale,
1099+
IsPending,
10251100
});
10261101
}
10271102

src/app/close-nft-auction-modal/close-nft-auction-modal.component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export class CloseNftAuctionModalComponent {
5252
.subscribe(
5353
(res) => {
5454
// Hide this modal and open the next one.
55-
this.bsModalRef.hide();
5655
this.modalService.setDismissReason("auction cancelled");
56+
this.bsModalRef.hide();
5757
},
5858
(err) => {
5959
console.error(err);

src/app/create-nft-auction-modal/create-nft-auction-modal.component.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component, Input } from "@angular/core";
2-
import { BsModalRef } from "ngx-bootstrap/modal";
2+
import { BsModalRef, BsModalService } from "ngx-bootstrap/modal";
33
import { GlobalVarsService } from "../global-vars.service";
44
import { BackendApiService, NFTEntryResponse, PostEntryResponse } from "../backend-api.service";
55
import { concatMap, last, map } from "rxjs/operators";
@@ -25,6 +25,7 @@ export class CreateNftAuctionModalComponent {
2525
private backendApi: BackendApiService,
2626
public globalVars: GlobalVarsService,
2727
public bsModalRef: BsModalRef,
28+
private modalService: BsModalService,
2829
private router: Router
2930
) {}
3031

@@ -70,6 +71,7 @@ export class CreateNftAuctionModalComponent {
7071
.subscribe(
7172
(res) => {
7273
this.router.navigate(["/" + this.globalVars.RouteNames.NFT + "/" + this.post.PostHashHex]);
74+
this.modalService.setDismissReason("auction created");
7375
this.bsModalRef.hide();
7476
},
7577
(err) => {
@@ -84,6 +86,7 @@ export class CreateNftAuctionModalComponent {
8486
return this.nftEntryResponses.filter(
8587
(nftEntryResponse) =>
8688
!nftEntryResponse.IsForSale &&
89+
!nftEntryResponse.IsPending &&
8790
nftEntryResponse.OwnerPublicKeyBase58Check === this.globalVars.loggedInUser?.PublicKeyBase58Check
8891
);
8992
}

src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.html

+33
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,32 @@
4646
</ng-template>
4747
</div>
4848
</div>
49+
<div
50+
*ngIf="
51+
!showProfileAsReserved && !isLoading && !nftResponse?.length && activeTab === CreatorProfileNftsComponent.TRANSFERABLE
52+
"
53+
class="p-15px"
54+
>
55+
<div class="background-color-grey p-35px br-12px d-flex flex-row align-items-center" style="text-align: center">
56+
<span *ngIf="profileBelongsToLoggedInUser(); else elseMissingPostBlock">No transferable NFTs right now.</span>
57+
<ng-template #elseMissingPostBlock>
58+
<span>@{{ profile.Username }} has no transferable NFTs yet.</span>
59+
</ng-template>
60+
</div>
61+
</div>
62+
<div
63+
*ngIf="
64+
!showProfileAsReserved && !isLoading && !nftResponse?.length && activeTab === CreatorProfileNftsComponent.MY_PENDING_TRANSFERS
65+
"
66+
class="p-15px"
67+
>
68+
<div class="background-color-grey p-35px br-12px d-flex flex-row align-items-center" style="text-align: center">
69+
<span *ngIf="profileBelongsToLoggedInUser(); else elseMissingPostBlock">No pending NFTs right now.</span>
70+
<ng-template #elseMissingPostBlock>
71+
<span>@{{ profile.Username }} has no pending NFTs yet.</span>
72+
</ng-template>
73+
</div>
74+
</div>
4975
<div
5076
*ngIf="
5177
!showProfileAsReserved && !isLoading && !myBids?.length && activeTab === CreatorProfileNftsComponent.MY_BIDS
@@ -70,6 +96,12 @@
7096
<div *ngIf="activeTab === CreatorProfileNftsComponent.FOR_SALE && nftResponse?.length">
7197
NFTs that @{{ profile.Username }} is currently selling
7298
</div>
99+
<div *ngIf="activeTab === CreatorProfileNftsComponent.TRANSFERABLE && nftResponse?.length">
100+
NFTs that @{{ profile.Username }} can transfer
101+
</div>
102+
<div *ngIf="activeTab === CreatorProfileNftsComponent.MY_PENDING_TRANSFERS && nftResponse?.length">
103+
Transferred NFTs pending acceptance by @{{ profile.Username }}
104+
</div>
73105
</div>
74106
<div *ngIf="!globalVars.hasUserBlockedCreator(profile.PublicKeyBase58Check)">
75107
<div
@@ -96,6 +128,7 @@
96128
[cardStyle]="true"
97129
[profilePublicKeyBase58Check]="profile.PublicKeyBase58Check"
98130
[isForSaleOnly]="activeTab === CreatorProfileNftsComponent.FOR_SALE"
131+
[acceptNFT]="activeTab === CreatorProfileNftsComponent.MY_PENDING_TRANSFERS"
99132
(userBlocked)="userBlocked()"
100133
></feed-post>
101134
<div *ngIf="activeTab === CreatorProfileNftsComponent.MY_BIDS && nftEntry.PostEntryResponse">

src/app/creator-profile-page/creator-profile-nfts/creator-profile-nfts.component.ts

+46-11
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,25 @@ export class CreatorProfileNftsComponent implements OnInit {
3939
static FOR_SALE = "For Sale";
4040
static MY_BIDS = "My Bids";
4141
static MY_GALLERY = "Gallery";
42+
static TRANSFERABLE = "Transferable";
43+
static MY_PENDING_TRANSFERS = "Pending Transfers";
4244
tabs = [CreatorProfileNftsComponent.FOR_SALE, CreatorProfileNftsComponent.MY_GALLERY];
4345
activeTab: string;
4446

4547
nftTabMap = {
4648
my_bids: CreatorProfileNftsComponent.MY_BIDS,
4749
for_sale: CreatorProfileNftsComponent.FOR_SALE,
4850
my_gallery: CreatorProfileNftsComponent.MY_GALLERY,
51+
transferable: CreatorProfileNftsComponent.TRANSFERABLE,
52+
my_pending_transfers: CreatorProfileNftsComponent.MY_PENDING_TRANSFERS,
4953
};
5054

5155
nftTabInverseMap = {
5256
[CreatorProfileNftsComponent.FOR_SALE]: "for_sale",
5357
[CreatorProfileNftsComponent.MY_BIDS]: "my_bids",
5458
[CreatorProfileNftsComponent.MY_GALLERY]: "my_gallery",
59+
[CreatorProfileNftsComponent.TRANSFERABLE]: "transferable",
60+
[CreatorProfileNftsComponent.MY_PENDING_TRANSFERS]: "my_pending_transfers",
5561
};
5662

5763
CreatorProfileNftsComponent = CreatorProfileNftsComponent;
@@ -68,13 +74,19 @@ export class CreatorProfileNftsComponent implements OnInit {
6874
) {}
6975

7076
ngOnInit(): void {
71-
if (this.globalVars.loggedInUser?.PublicKeyBase58Check === this.profile.PublicKeyBase58Check) {
72-
this.tabs.push(CreatorProfileNftsComponent.MY_BIDS);
77+
if (this.profileBelongsToLoggedInUser()) {
78+
this.tabs.push(
79+
CreatorProfileNftsComponent.MY_BIDS,
80+
CreatorProfileNftsComponent.MY_PENDING_TRANSFERS,
81+
CreatorProfileNftsComponent.TRANSFERABLE
82+
);
7383
}
7484
this.route.queryParams.subscribe((queryParams) => {
7585
if (queryParams.nftTab && queryParams.nftTab in this.nftTabMap) {
7686
if (
77-
queryParams.nftTab === this.nftTabInverseMap[CreatorProfileNftsComponent.MY_BIDS] &&
87+
(queryParams.nftTab === this.nftTabInverseMap[CreatorProfileNftsComponent.MY_BIDS] ||
88+
queryParams.nftTab === this.nftTabInverseMap[CreatorProfileNftsComponent.TRANSFERABLE] ||
89+
queryParams.nftTab === this.nftTabInverseMap[CreatorProfileNftsComponent.MY_PENDING_TRANSFERS]) &&
7890
this.globalVars.loggedInUser?.PublicKeyBase58Check !== this.profile.PublicKeyBase58Check
7991
) {
8092
this.updateNFTTabParam(CreatorProfileNftsComponent.MY_GALLERY);
@@ -121,13 +133,14 @@ export class CreatorProfileNftsComponent implements OnInit {
121133
);
122134
}
123135

124-
getNFTs(isForSale: boolean | null = null): Subscription {
136+
getNFTs(isForSale: boolean | null = null, isPending: boolean | null = null): Subscription {
125137
return this.backendApi
126138
.GetNFTsForUser(
127139
this.globalVars.localNode,
128140
this.profile.PublicKeyBase58Check,
129141
this.globalVars.loggedInUser?.PublicKeyBase58Check,
130-
isForSale
142+
isForSale,
143+
isPending
131144
)
132145
.subscribe(
133146
(res: {
@@ -136,13 +149,16 @@ export class CreatorProfileNftsComponent implements OnInit {
136149
this.nftResponse = [];
137150
for (const k in res.NFTsMap) {
138151
const responseElement = res.NFTsMap[k];
152+
// Exclude NFTs created by profile from Gallery and don't show pending NFTs in galley.
139153
if (
140-
(this.activeTab === CreatorProfileNftsComponent.MY_GALLERY &&
141-
responseElement.PostEntryResponse.PosterPublicKeyBase58Check !== this.profile.PublicKeyBase58Check) ||
142-
this.activeTab === CreatorProfileNftsComponent.FOR_SALE
154+
this.activeTab === CreatorProfileNftsComponent.MY_GALLERY &&
155+
(responseElement.PostEntryResponse.PosterPublicKeyBase58Check === this.profile.PublicKeyBase58Check ||
156+
responseElement.NFTEntryResponses.filter((nftEntryResponse) => !nftEntryResponse.IsPending).length ===
157+
0)
143158
) {
144-
this.nftResponse.push(responseElement);
159+
continue;
145160
}
161+
this.nftResponse.push(responseElement);
146162
}
147163
this.lastPage = Math.floor(this.nftResponse.length / CreatorProfileNftsComponent.PAGE_SIZE);
148164
return this.nftResponse;
@@ -218,7 +234,7 @@ export class CreatorProfileNftsComponent implements OnInit {
218234
this.resetDatasource(event);
219235
});
220236
} else {
221-
return this.getNFTs(this.getIsForSaleValue()).add(() => {
237+
return this.getNFTs(this.getIsForSaleValue(), this.getIsPendingValue()).add(() => {
222238
this.resetDatasource(event);
223239
});
224240
}
@@ -290,6 +306,25 @@ export class CreatorProfileNftsComponent implements OnInit {
290306
}
291307

292308
getIsForSaleValue(): boolean | null {
293-
return this.activeTab === CreatorProfileNftsComponent.MY_GALLERY ? null : true;
309+
if (this.activeTab === CreatorProfileNftsComponent.FOR_SALE) {
310+
return true;
311+
} else if (this.activeTab === CreatorProfileNftsComponent.TRANSFERABLE) {
312+
return false;
313+
} else {
314+
return null;
315+
}
316+
}
317+
318+
getIsPendingValue(): boolean | null {
319+
if (this.activeTab === CreatorProfileNftsComponent.MY_PENDING_TRANSFERS) {
320+
return true;
321+
} else if (
322+
this.activeTab === CreatorProfileNftsComponent.MY_GALLERY ||
323+
this.activeTab === CreatorProfileNftsComponent.TRANSFERABLE
324+
) {
325+
return false;
326+
} else {
327+
return null;
328+
}
294329
}
295330
}

0 commit comments

Comments
 (0)