This commit is contained in:
commit
e594812857
9 changed files with 390 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
nfts/
|
||||
.env
|
||||
nft.nulo.in
|
17
.woodpecker.yml
Normal file
17
.woodpecker.yml
Normal 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
5
Containerfile
Normal 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
50
etherscan.go
Normal 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
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
|||
module nft.nulo.in
|
||||
|
||||
go 1.17
|
13
index.tmpl
Normal file
13
index.tmpl
Normal 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
212
main.go
Normal 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
13
nft.tmpl
Normal 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
74
opensea.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue