Inicio
Some checks failed
continuous-integration/woodpecker the build failed

This commit is contained in:
Cat /dev/Nulo 2021-11-09 18:47:36 -03:00
commit e594812857
9 changed files with 390 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
nfts/
.env
nft.nulo.in

17
.woodpecker.yml Normal file
View file

@ -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

5
Containerfile Normal file
View file

@ -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"]

50
etherscan.go Normal file
View file

@ -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
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module nft.nulo.in
go 1.17

13
index.tmpl Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<meta charset=utf8>
<meta name=viewport content='width=device-width, initial-scale=1.0'>
<link rel=stylesheet href=https://nulo.in/drip.css>
<title>NFTmashin</title>
<section style=text-align:center>
<h1>NFTmashin</h1>
<p><em>Beta: las copias pueden ser eliminadas en el futuro.</em></p>
<p>NFTs descargados: {{ .NFTNum }}</p>
<p>Valor total: <span style=color:green>USD ${{ printf "%.2f" .TotalUSDValue }}</span></p>
<a href=/copiar>¡Copiar uno!</a>
<p>Inspirado en <a href="https://konsthack.se/portfolio/kh000-kopimashin/" rel="noreferrer noopener">Kopimashin</a> de brokep.</p>
</section>

212
main.go Normal file
View file

@ -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")
}
}

13
nft.tmpl Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<meta charset=utf8>
<meta name=viewport content='width=device-width, initial-scale=1.0'>
<link rel=stylesheet href=https://nulo.in/drip.css>
<title>{{ .NFT.Asset.Name }}</title>
<h1>{{ .NFT.Asset.Name }}</h1>
<p>Vendido por <span style=color:green>USD ${{ printf "%.2f" .NFT.GetUSDPrice }}</span></p>
<a href="{{ .NFT.Asset.Permalink }}" rel="noreferrer noopener">Abrir en OpenSea</a>
<figure>
<img src="/static/nfts/{{ .Id }}">
</figure>
<a href=/>Volver</a>

74
opensea.go Normal file
View file

@ -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
}