Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions cmd/vpn-cli/datum_ref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package main

import (
"errors"
"fmt"

"github.com/blinklabs-io/gouroboros/cbor"
)

type plan struct {
Duration int
Price int
}

func decodeRefDatumFlexible(datumCBOR []byte) ([]plan, []string, error) {
// Decode CBOR into any first
var v any
_, err := cbor.Decode(datumCBOR, &v)
if err != nil {
return nil, nil, fmt.Errorf("cbor decode: %w", err)
}

// Remove CBOR wrappers (Tag/Constructors)
v = unwrapAll(v)

// We expect either:
// [ ....,...] OR Constructor( [ plans, regions, ... ] )
seq, ok := toSlice(v)
if !ok {
return nil, nil, errors.New("refdatum: top-level is not a list/constructor")
}

// conisdered first list of (int,int) as plans and the first "list of string" as regions.
var rawPlans any
var rawRegions any
for _, e := range seq {
eu := unwrapAll(e)
if rawPlans == nil {
if ps, ok := isListOfIntPairs(eu); ok {
rawPlans = ps
continue
}
}
if rawRegions == nil {
if rs, ok := isListOfStrings(eu); ok {
rawRegions = rs
continue
}
}
}
if rawPlans == nil {
return nil, nil, fmt.Errorf("refdatum: plans not found")
}
pairs := rawPlans.([][]int)
outPlans := make([]plan, 0, len(pairs))
for _, p := range pairs {
outPlans = append(outPlans, plan{Duration: p[0], Price: p[1]})
}

if rawRegions == nil {
return nil, nil, fmt.Errorf("refdatum: regions not found")
}
outRegions := rawRegions.([]string)

return outPlans, outRegions, nil
}

// Removes cbor.Tag and cbor.Constructor.
func unwrapAll(x any) any {
for {
switch t := x.(type) {
case cbor.Tag:
x = t.Content
continue
case *cbor.Constructor:
fields := t.Fields()
if len(fields) == 1 {
x = fields[0]
continue
}
x = fields
continue
case cbor.Constructor:
if len(t.Fields()) == 1 {
x = t.Fields()[0]
continue
}
x = t.Fields()
continue
default:
return x
}
}
}

// toSlice returns v as []any
func toSlice(v any) ([]any, bool) {
s, ok := v.([]any)
return s, ok
}

// Read a list as [[int,int], ...] even if each pair is []any.
func isListOfIntPairs(v any) ([][]int, bool) {
items, ok := toSlice(v)
if !ok || len(items) == 0 {
return nil, false
}
out := make([][]int, 0, len(items))
for _, it := range items {
// unwrap constructor to CBOR fields
it = unwrapAll(it)
pair, ok := toSlice(it)
if !ok || len(pair) != 2 {
return nil, false
}
a, okA := toInt(unwrapAll(pair[0]))
b, okB := toInt(unwrapAll(pair[1]))
if !okA || !okB {
return nil, false
}
out = append(out, []int{a, b})
}
return out, true
}

// Can be either strings or []byte (UTF-8) per element.
func isListOfStrings(v any) ([]string, bool) {
items, ok := toSlice(v)
if !ok {
return nil, false
}
out := make([]string, 0, len(items))
for _, it := range items {
it = unwrapAll(it)
switch s := it.(type) {
case string:
out = append(out, s)
case []byte:
out = append(out, string(s))
default:
return nil, false
}
}
return out, true
}

// convert any CBOR number into an int.
func toInt(v any) (int, bool) {
switch n := v.(type) {
case int:
return n, true
case int64:
return int(n), true
case uint64:
return int(n), true
case uint:
return int(n), true
case float64:
if n == float64(int(n)) {
return int(n), true
}
return 0, false
default:
return 0, false
}
}
118 changes: 118 additions & 0 deletions cmd/vpn-cli/kupo_ref.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"context"
"encoding/hex"
"errors"
"fmt"
"strings"

"github.com/SundaeSwap-finance/kugo"
"github.com/blinklabs-io/vpn-indexer/internal/config"
"github.com/blinklabs-io/vpn-indexer/internal/database"
)

func getKupoClient() (*kugo.Client, error) {
cfg := config.GetConfig()
if cfg.TxBuilder.KupoUrl == "" {
return nil, errors.New("no kupo url provided")
}
k := kugo.New(kugo.WithEndpoint(cfg.TxBuilder.KupoUrl))
if k == nil {
return nil, fmt.Errorf("failed kupo client: %s", cfg.TxBuilder.KupoUrl)
}
return k, nil
}

func loadReferenceFromKugoClient(ctx context.Context) (database.Reference, error) {
cfg := config.GetConfig()
if strings.TrimSpace(cfg.TxBuilder.KupoUrl) == "" {
return database.Reference{}, errors.New("kupo url not configured")
}
k, err := getKupoClient()
if err != nil {
return database.Reference{}, err
}

if tok := strings.TrimSpace(cfg.Indexer.ReferenceToken); tok != "" {
if ref, err := refByKugoMatches(ctx, k, tok); err == nil {
return ref, nil
}
}
if addr := strings.TrimSpace(cfg.Indexer.ScriptAddress); addr != "" {
return refByKugoMatches(ctx, k, addr)
}

return database.Reference{}, errors.New("neither reference token nor script address configured")
}

func refByKugoMatches(ctx context.Context, k *kugo.Client, pattern string) (database.Reference, error) {
// Query matches by pattern (address).
matches, err := k.Matches(ctx, kugo.Pattern(pattern))
if err != nil {
return database.Reference{}, fmt.Errorf("kupo matches: %w", err)
}
if len(matches) == 0 {
return database.Reference{}, fmt.Errorf("no matches for pattern %q", pattern)
}
match := matches[0]

// Decode transaction_id (bytes to hex)
txid, err := hex.DecodeString(parseHex(match.TransactionID))
if err != nil {
return database.Reference{}, fmt.Errorf("txid decode: %w", err)
}

// Acquire datum CBOR using kugo only
var datumCBOR []byte
switch {
case strings.TrimSpace(getDatumHash(match)) != "":
hexStr, derr := k.Datum(ctx, parseHex(getDatumHash(match)))
if derr != nil {
return database.Reference{}, fmt.Errorf("fetch datum by hash: %w", derr)
}
datumCBOR, err = hex.DecodeString(parseHex(hexStr))
if err != nil {
return database.Reference{}, fmt.Errorf("datum decode: %w", err)
}
default:
return database.Reference{}, errors.New("reference utxo has no datum (no bytes/hash)")
}

// Decode reference plans and regions from datumCBOR
plans, regions, err := decodeRefDatumFlexible(datumCBOR)
if err != nil {
return database.Reference{}, fmt.Errorf("decode ref datum: %w", err)
}

// Map to database.Reference expected by BuildSignupTx
refPrices := make([]database.ReferencePrice, 0, len(plans))
for _, p := range plans {
refPrices = append(refPrices, database.ReferencePrice{
Duration: p.Duration,
Price: p.Price,
})
}
refRegions := make([]database.ReferenceRegion, 0, len(regions))
for _, r := range regions {
refRegions = append(refRegions, database.ReferenceRegion{Name: r})
}

return database.Reference{
TxId: txid,
OutputIdx: match.OutputIndex,
Prices: refPrices,
Regions: refRegions,
}, nil
}

func getDatumHash(m kugo.Match) string {
return m.DatumHash
}

func parseHex(s string) string {
s = strings.TrimSpace(s)
s = strings.TrimPrefix(s, "0x")
s = strings.TrimPrefix(s, "0X")
return s
}
Loading
Loading