/
nft-burn.go
251 lines (227 loc) · 9.23 KB
/
nft-burn.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"github.com/deso-smart/deso-backend/v2/routes"
"github.com/deso-smart/deso-backend/v2/scripts/tools/toolslib"
"github.com/deso-smart/deso-core/v2/lib"
"github.com/pkg/errors"
"github.com/tyler-smith/go-bip39"
"io/ioutil"
"net/http"
"net/url"
"sort"
"time"
)
// nft-burn.go is a helper script to burn off all remaining NFTs.
// A burnable NFT is defined as one that has never been sold and is still for sale.
func main() {
flagParamDeSoNodeURL := flag.String("deso-node",
"", "A DeSo node to target for sourcing data and submitting transactions.")
flagBurnerMnemonic := flag.String("burner-mnemonic",
"", "The mnemonic associated with the burner's public/private key pair.")
flagParamNFTHash := flag.String("nft-post-hash",
"", "A NFT hash to target. This NFT will have all associated unsold copies burnt.\n"+
"The hash should be passed as a hex string.")
flagDelayMilliseconds := flag.Int("delay_milliseconds", 1000,
"The delay in milliseconds between each burn.")
flagMaxNFTsBurned := flag.Int("max_nfts_burned", -1,
"The maximum number of NFTs to be burned.")
flag.Parse()
// Process flags.
nftPostHash := *flagParamNFTHash
burnDelayMilliseconds := *flagDelayMilliseconds
maxNFTsBurned := *flagMaxNFTsBurned
controlledBurn := false
if maxNFTsBurned > 0 {
controlledBurn = true
}
fmt.Printf("Targeted NFT Post Hash: %s\n", nftPostHash)
// Construct necessary endpoints.
desoNodeURL, err := url.Parse(*flagParamDeSoNodeURL)
if err != nil {
panic(errors.Wrap(err, "main(): Invalid DeSo node specified. Please specify a valid node using --deso-node flag\n"))
}
if len(desoNodeURL.String()) == 0 {
panic(fmt.Errorf("main(): Please specify a valid node using --deso-node flag\n"))
}
fmt.Printf("DeSo Node: %s\n", desoNodeURL.String())
getNFTEntriesEndpoint := desoNodeURL.String() + routes.RoutePathGetNFTEntriesForPostHash
getSinglePostEndpoint := desoNodeURL.String() + routes.RoutePathGetSinglePost
// Setup Network Parameters.
params := &lib.DeSoMainnetParams
fmt.Printf("Network type set: %s\n", params.NetworkType.String())
// Fetch post information on the requested NFT.
var getSinglePostResponse routes.GetSinglePostResponse
{
// Create request payload.
payload := &routes.GetSinglePostRequest{
PostHashHex: nftPostHash,
}
postBody, err := json.Marshal(payload)
if err != nil {
panic(errors.Wrap(err, "main(): Could not complete request"))
}
postBuffer := bytes.NewBuffer(postBody)
// Execute request.
resp, err := http.Post(getSinglePostEndpoint, "application/json", postBuffer)
if err != nil {
panic(errors.Wrap(err, "main(): failed request"))
}
if resp.StatusCode != 200 {
bodyBytes, _ := ioutil.ReadAll(resp.Body)
panic(errors.Errorf("main(): Received non 200 response code: "+
"Status Code: %v Body: %v", resp.StatusCode, string(bodyBytes)))
}
// Process Response.
err = json.NewDecoder(resp.Body).Decode(&getSinglePostResponse)
if err != nil {
panic(errors.Wrap(err, "main(): Failed to decode response\n"))
}
err = resp.Body.Close()
if err != nil {
panic(errors.Wrap(err, "main(): Failed to decode body\n"))
}
}
if getSinglePostResponse.PostFound == nil {
panic(errors.Errorf("main(): Could not find post for the specified NFT post hash."))
}
fmt.Printf("main(): Post found contains: \n\tPoster Public Key: %s\n\tPost Body: \"%s\"\n\n",
getSinglePostResponse.PostFound.PosterPublicKeyBase58Check, getSinglePostResponse.PostFound.Body)
if !getSinglePostResponse.PostFound.IsNFT {
panic(errors.Errorf("main(): Post found is not a NFT."))
}
// Generate the burner's keys from provided mnemonic.
if len(*flagBurnerMnemonic) == 0 {
panic(errors.Errorf("main(): Please specify a valid mnemonic using --burner-mnemonic flag\n"))
}
seedBytes, err := bip39.NewSeedWithErrorChecking(*flagBurnerMnemonic, "")
if err != nil {
panic(errors.Wrap(err, "main(): Could not generate key pair from mnemonic"))
}
burnerPubKey, burnerPrivKey, _, err := lib.ComputeKeysFromSeed(seedBytes, 0, params)
if lib.PkToString(burnerPubKey.SerializeCompressed(), params) != getSinglePostResponse.PostFound.PosterPublicKeyBase58Check {
panic(errors.Errorf("main(): Burner mnemonic generated mismatched key pair. Mnemonic public key: %s\n",
lib.PkToString(burnerPubKey.SerializeCompressed(), params)))
}
// Fetch NFT entry information on the requested NFT.
fmt.Printf("Requesting entry information on NFT...\n")
var getNFTEntriesResponse routes.GetNFTEntriesForPostHashResponse
{
// Create request payload.
payload := &routes.GetNFTEntriesForPostHashRequest{
PostHashHex: nftPostHash,
ReaderPublicKeyBase58Check: "",
}
postBody, err := json.Marshal(payload)
if err != nil {
panic(errors.Wrap(err, "main(): Could not complete request"))
}
postBuffer := bytes.NewBuffer(postBody)
// Execute request.
resp, err := http.Post(getNFTEntriesEndpoint, "application/json", postBuffer)
if err != nil {
panic(errors.Wrap(err, "main(): failed request"))
}
if resp.StatusCode != 200 {
bodyBytes, _ := ioutil.ReadAll(resp.Body)
panic(errors.Errorf("main(): Received non 200 response code: "+
"Status Code: %v Body: %v", resp.StatusCode, string(bodyBytes)))
}
// Process Response.
err = json.NewDecoder(resp.Body).Decode(&getNFTEntriesResponse)
if err != nil {
panic(errors.Wrap(err, "main(): Failed to decode response\n"))
}
err = resp.Body.Close()
if err != nil {
panic(errors.Wrap(err, "main(): Failed to decode body\n"))
}
}
sort.Slice(getNFTEntriesResponse.NFTEntryResponses, func(ii, jj int) bool {
return getNFTEntriesResponse.NFTEntryResponses[ii].SerialNumber <
getNFTEntriesResponse.NFTEntryResponses[jj].SerialNumber
})
// Process NFT Entries, collecting burnable NFT copies.
var burnableNFTEntryResponses []*routes.NFTEntryResponse
var burnableNFTSerialNumbers []uint64
uniqueSerialNumberMap := make(map[uint64]struct{})
for _, nftEntryResponse := range getNFTEntriesResponse.NFTEntryResponses {
if _, serialNumberSeen := uniqueSerialNumberMap[nftEntryResponse.SerialNumber]; serialNumberSeen {
panic(errors.Errorf("main(): Found duplicate serial numbers in NFT entries response\n"))
}
if nftEntryResponse.OwnerPublicKeyBase58Check == getSinglePostResponse.PostFound.PosterPublicKeyBase58Check {
burnableNFTEntryResponses = append(burnableNFTEntryResponses, nftEntryResponse)
burnableNFTSerialNumbers = append(burnableNFTSerialNumbers, nftEntryResponse.SerialNumber)
uniqueSerialNumberMap[nftEntryResponse.SerialNumber] = struct{}{}
}
}
fmt.Printf("Number of burnable NFTs found: %d\n", len(burnableNFTEntryResponses))
fmt.Printf("Serial Numbers of Burnable NFTs: %v\n", burnableNFTSerialNumbers)
// Prompt user to confirm this is the correct NFT to burn.
var userConfirmation string
fmt.Print("Proceed with burn (Y/n)? ")
_, err = fmt.Scan(&userConfirmation)
if err != nil || userConfirmation != "Y" {
fmt.Printf("Exiting without burning.\n")
return
}
// Mark all burnable NFTs as not for sale.
for ii := 0; ii < len(burnableNFTEntryResponses); ii++ {
// Check if the user specified a controlled burn.
if controlledBurn && ii >= maxNFTsBurned {
break
}
burnableNFTRespone := burnableNFTEntryResponses[ii]
if !burnableNFTRespone.IsForSale {
continue
}
// Make sure the NFT is no longer for sale.
fmt.Printf("Closing Sale For Serial Number #%d (#%d of #%d)\n",
int(burnableNFTRespone.SerialNumber), ii+1, len(burnableNFTEntryResponses))
err := toolslib.UpdateNFT(burnerPubKey, burnerPrivKey, nftPostHash, int(burnableNFTRespone.SerialNumber),
false, int(burnableNFTRespone.MinBidAmountNanos), burnableNFTRespone.IsBuyNow,
burnableNFTRespone.BuyNowPriceNanos, params, desoNodeURL.String())
if err != nil {
fmt.Printf("main(): Ran into an error when trying to close sale for NFT: %s\n", err.Error())
var userConfirmation string
fmt.Print("Cancel burn (Y/n)? Continuing will retry transaction: ")
_, err = fmt.Scan(&userConfirmation)
if err != nil || userConfirmation == "Y" {
fmt.Printf("Exiting without burning remaining NFTs.\n")
return
}
ii--
}
// Sleep to prevent being blacklisted from the node.
time.Sleep(time.Duration(burnDelayMilliseconds) * time.Millisecond)
}
// Burn all the burnable NFT copies.
for ii := 0; ii < len(burnableNFTEntryResponses); ii++ {
// Check if the user specified a controlled burn.
if controlledBurn && ii >= maxNFTsBurned {
break
}
burnableNFTRespone := burnableNFTEntryResponses[ii]
// Burn the NFT.
fmt.Printf("Burning Serial Number #%d (#%d of #%d)\n",
int(burnableNFTRespone.SerialNumber), ii+1, len(burnableNFTEntryResponses))
err = toolslib.BurnNFT(burnerPubKey, burnerPrivKey, nftPostHash, int(burnableNFTRespone.SerialNumber),
params, desoNodeURL.String())
if err != nil {
fmt.Printf("main(): Ran into an error when trying to burn NFT: %s\n", err.Error())
var userConfirmation string
fmt.Print("Cancel burn (Y/n)? Continuing will retry transaction: ")
_, err = fmt.Scan(&userConfirmation)
if err != nil || userConfirmation == "Y" {
fmt.Printf("Exiting without burning remaining NFTs.\n")
return
}
ii--
}
// Sleep to prevent being blacklisted from the node.
time.Sleep(time.Duration(burnDelayMilliseconds) * time.Millisecond)
}
}