From e594812857a80d3f09d028cc86ce3dfc3af85f48 Mon Sep 17 00:00:00 2001 From: Nulo Date: Tue, 9 Nov 2021 18:47:36 -0300 Subject: [PATCH] Inicio --- .gitignore | 3 + .woodpecker.yml | 17 ++++ Containerfile | 5 ++ etherscan.go | 50 ++++++++++++ go.mod | 3 + index.tmpl | 13 +++ main.go | 212 ++++++++++++++++++++++++++++++++++++++++++++++++ nft.tmpl | 13 +++ opensea.go | 74 +++++++++++++++++ 9 files changed, 390 insertions(+) create mode 100644 .gitignore create mode 100644 .woodpecker.yml create mode 100644 Containerfile create mode 100644 etherscan.go create mode 100644 go.mod create mode 100644 index.tmpl create mode 100644 main.go create mode 100644 nft.tmpl create mode 100644 opensea.go 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 +}