commit e594812857a80d3f09d028cc86ce3dfc3af85f48 Author: Nulo Date: Tue Nov 9 18:47:36 2021 -0300 Inicio diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7687d79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +nfts/ +.env +nft.nulo.in diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..df967fc --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,17 @@ +pipeline: + build: + image: docker.io/golang:1.17-alpine + commands: + - go build + publish: + image: plugins/docker + registry: registry.nulo.in + repo: registry.nulo.in/Nulo/nft.nulo.in + tags: + - latest + username: sutty + secrets: + - docker_password + when: + branch: kopimi + event: push diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..0dda009 --- /dev/null +++ b/Containerfile @@ -0,0 +1,5 @@ +FROM docker.io/alpine:3.14 +COPY nft.nulo.in index.tmpl nft.tmpl /app/ +WORKDIR /app +EXPOSE 5050/tcp +ENTRYPOINT ["/app/nft.nulo.in"] diff --git a/etherscan.go b/etherscan.go new file mode 100644 index 0000000..b312da7 --- /dev/null +++ b/etherscan.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "errors" + "math" + "net/http" + "os" + "strconv" +) + +type Transaction struct { + Value float64 +} + +func GetTransaction(id string) (Transaction, error) { + transaction := Transaction{} + key, exists := os.LookupEnv("ETHERSCAN_KEY") + if !exists { + return transaction, errors.New("No tengo ETHERSCAN_KEY, conseguir en https://etherscan.io/apis") + } + res, err := http.DefaultClient.Get("https://api.etherscan.io/api?module=proxy" + + "&action=eth_getTransactionByHash" + + "&txhash=" + id + + "&apikey=" + key) + if err != nil { + return transaction, err + } + result := struct { + Result struct { + Value string `json:"value"` + } `json:"result,omitempty"` + Error struct { + Message string `json:"message"` + } `json:"error,omitempty"` + }{} + err = json.NewDecoder(res.Body).Decode(&result) + if err != nil { + return transaction, err + } + if len(result.Error.Message) > 0 { + return transaction, errors.New(result.Error.Message) + } + parsed, err := strconv.ParseUint(result.Result.Value[2:], 16, 64) + if err != nil { + return transaction, err + } + transaction.Value = float64(parsed) / math.Pow10(18) + return transaction, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0c56756 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module nft.nulo.in + +go 1.17 diff --git a/index.tmpl b/index.tmpl new file mode 100644 index 0000000..2b02cb1 --- /dev/null +++ b/index.tmpl @@ -0,0 +1,13 @@ + + + + +NFTmashin +
+

NFTmashin

+

Beta: las copias pueden ser eliminadas en el futuro.

+

NFTs descargados: {{ .NFTNum }}

+

Valor total: USD ${{ printf "%.2f" .TotalUSDValue }}

+ ¡Copiar uno! +

Inspirado en Kopimashin de brokep.

+
diff --git a/main.go b/main.go new file mode 100644 index 0000000..4eafe2b --- /dev/null +++ b/main.go @@ -0,0 +1,212 @@ +package main + +import ( + "encoding/json" + "errors" + "html/template" + "io" + "io/fs" + "log" + "net/http" + "os" + "path" + "path/filepath" + "strings" +) + +type Status struct { + NFTNum int + TotalUSDValue float64 +} + +const NFTS_DIR = "./nfts" + +func getDownloadedOpenSeaAsset(id string) (downloadedOpenSeaAsset, error) { + asset := downloadedOpenSeaAsset{} + file, err := os.Open(filepath.Join(NFTS_DIR, id+".json")) + if err != nil { + return asset, err + } + err = json.NewDecoder(file).Decode(&asset) + if err != nil { + return asset, err + } + return asset, nil +} + +func getStatus() (Status, error) { + status := Status{} + entries, err := os.ReadDir(NFTS_DIR) + if err != nil { + return status, err + } + + for _, val := range entries { + info, err := val.Info() + if err != nil { + return status, err + } + if filepath.Ext(info.Name()) != ".json" { + continue + } + + asset, err := getDownloadedOpenSeaAsset(strings.Split(info.Name(), ".")[0]) + if err != nil { + return status, err + } + + status.NFTNum += 1 + status.TotalUSDValue += asset.GetUSDPrice() + } + return status, nil +} + +func getOpenSeaId(id string) string { + return "opensea:asset:" + id +} + +func getRandomOpenSeaAsset() (Asset, error) { + assets, err := GetAssets() + if err != nil { + log.Println("Error hablandole a OpenSea:", err) + return Asset{}, nil + } + for _, a := range assets { + if len(a.ImageUrl) == 0 { + continue + } + id := getOpenSeaId(a.TokenId) + file, err := os.Open(filepath.Join(NFTS_DIR, id)) + defer file.Close() + if errors.Is(err, fs.ErrNotExist) { + return a, nil + } + } + // TODO: paginar y cachear paginación + return Asset{}, errors.New("No conseguí ningún asset") +} + +type downloadedOpenSeaAsset struct { + Asset `json:"Asset"` + Transaction `json:"Transaction"` +} + +func (d downloadedOpenSeaAsset) GetUSDPrice() float64 { + return d.Transaction.Value * d.Asset.LastSale.TokenUSDPrice +} + +func downloadOpenSeaAsset(asset Asset, transaction Transaction) (string, error) { + id := getOpenSeaId(asset.TokenId) + jsonFile, err := os.Create(filepath.Join(NFTS_DIR, id+".json")) + if err != nil { + return "", err + } + err = json.NewEncoder(jsonFile).Encode(downloadedOpenSeaAsset{ + Asset: asset, + Transaction: transaction, + }) + if err != nil { + return "", err + } + + imageRes, err := http.DefaultClient.Get(asset.ImageUrl) + if err != nil { + return "", err + } + file, err := os.Create(filepath.Join(NFTS_DIR, id)) + if err != nil { + return "", err + } + _, err = io.Copy(file, imageRes.Body) + if err != nil { + return "", err + } + + return id, nil +} + +func must(err error) { + if err != nil { + log.Panicln(err) + } +} + +func main() { + indexTmpl, err := template.ParseFiles("./index.tmpl") + must(err) + nftsTmpl, err := template.ParseFiles("./nft.tmpl") + must(err) + + fs := http.FileServer(http.Dir(NFTS_DIR)) + http.Handle("/static/nfts/", http.StripPrefix("/static/nfts/", fs)) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + status, err := getStatus() + if err != nil { + log.Println("Error en Status:", err) + internalError(w, "Error consiguiendo el contador de NFTs.") + return + } + w.Header().Add("Content-Type", "text/html") + err = indexTmpl.Execute(w, status) + if err != nil { + log.Panicln("No pude escribir index", err) + } + }) + http.HandleFunc("/copiar", func(w http.ResponseWriter, r *http.Request) { + asset, err := getRandomOpenSeaAsset() + if err != nil { + log.Println("Error hablandole a OpenSea:", err) + internalError(w, "Error hablandole a OpenSea.") + return + } + log.Println(asset) + + transaction, err := GetTransaction(asset.LastSale.TransactionHash) + if err != nil { + log.Println("Error consiguiendo transacción de Etherscan", err) + internalError(w, "Error hablandole a Etherscan.") + return + } + log.Println(transaction) + + id, err := downloadOpenSeaAsset(asset, transaction) + if err != nil { + log.Println("Error descargando OpenSea asset", err) + internalError(w, "Error descargando NFT.") + return + } + + w.Header().Add("Location", "/nfts/"+id) + w.WriteHeader(http.StatusTemporaryRedirect) + w.Write([]byte("Redirijiendo al NFT...")) + }) + http.HandleFunc("/nfts/", func(w http.ResponseWriter, r *http.Request) { + _, id := path.Split(r.URL.Path) + nft, err := getDownloadedOpenSeaAsset(id) + if err != nil { + log.Panicln("No pude conseguir NFT", err) + } + w.Header().Add("Content-Type", "text/html") + err = nftsTmpl.Execute(w, struct { + Id string + NFT downloadedOpenSeaAsset + }{ + Id: id, + NFT: nft, + }) + if err != nil { + log.Panicln("No pude escribir nft", err) + } + }) + log.Println("Hola") + http.ListenAndServe(":5050", nil) +} + +func internalError(w http.ResponseWriter, err_str string) { + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write([]byte(err_str)) + if err != nil { + log.Panicln("No pude escribir el error xD") + } +} diff --git a/nft.tmpl b/nft.tmpl new file mode 100644 index 0000000..02ebcc6 --- /dev/null +++ b/nft.tmpl @@ -0,0 +1,13 @@ + + + + +{{ .NFT.Asset.Name }} + +

{{ .NFT.Asset.Name }}

+

Vendido por USD ${{ printf "%.2f" .NFT.GetUSDPrice }}

+Abrir en OpenSea +
+ +
+Volver diff --git a/opensea.go b/opensea.go new file mode 100644 index 0000000..0a7439f --- /dev/null +++ b/opensea.go @@ -0,0 +1,74 @@ +package main + +import ( + "encoding/json" + "net/http" + "strconv" +) + +// https://docs.opensea.io/reference/asset-object +type Asset struct { + TokenId string + Permalink string + ImageUrl string + Name string + LastSale AssetLastSale +} + +type AssetLastSale struct { + TokenUSDPrice float64 + TransactionHash string +} + +// https://docs.opensea.io/reference/getting-assets +type Assets []Asset + +func GetAssets() (Assets, error) { + parsed := struct { + Assets []struct { + TokenId string `json:"token_id"` + Permalink string `json:"permalink"` + ImageUrl string `json:"image_url"` + Name string `json:"name"` + LastSale struct { + PaymentToken struct { + USDPrice string `json:"usd_price"` + } `json:"payment_token"` + Transaction struct { + Hash string `json:"transaction_hash"` + } `json:"transaction"` + } `json:"last_sale"` + } `json:"assets"` + }{} + assets := Assets{} + res, err := http.DefaultClient.Get("https://api.opensea.io/api/v1/assets" + + "?order_direction=desc" + + "&offset=0" + + "&limit=50" + + "&order_by=sale_date") + if err != nil { + return assets, err + } + err = json.NewDecoder(res.Body).Decode(&parsed) + if err != nil { + return assets, err + } + for _, a := range parsed.Assets { + tokenUSDPrice, err := strconv.ParseFloat(a.LastSale.PaymentToken.USDPrice, 64) + if err != nil { + return assets, err + } + assets = append(assets, Asset{ + TokenId: a.TokenId, + Permalink: a.Permalink, + ImageUrl: a.ImageUrl, + Name: a.Name, + LastSale: AssetLastSale{ + TokenUSDPrice: tokenUSDPrice, + TransactionHash: a.LastSale.Transaction.Hash, + }, + }) + + } + return assets, nil +}