From 0c89b8700852f5dc6891e0fae0305f0dad9ab88f Mon Sep 17 00:00:00 2001 From: Woft257 Date: Wed, 24 Dec 2025 21:46:05 +0700 Subject: [PATCH 01/13] feat(cardano_poc): implement minimal Cardano preprod proof of concept - Create wallet using MPCIUM and derive EDDSA public key - Generate Cardano enterprise address from public key - Implement polling mechanism to wait for funding UTxO via Blockfrost - Build and sign transaction body in CBOR format - Submit signed transaction to Blockfrost and handle responses - Add necessary helper functions for address encoding, UTxO fetching, and transaction building --- examples/cardano_poc/main.go | 435 +++++++++++++++++++++++++++++++++++ go.mod | 28 ++- go.sum | 103 +++++++-- 3 files changed, 531 insertions(+), 35 deletions(-) create mode 100644 examples/cardano_poc/main.go diff --git a/examples/cardano_poc/main.go b/examples/cardano_poc/main.go new file mode 100644 index 0000000..b32cb4f --- /dev/null +++ b/examples/cardano_poc/main.go @@ -0,0 +1,435 @@ +package main + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "slices" + "strings" + "time" + + "golang.org/x/crypto/blake2b" + + "github.com/cosmos/btcutil/bech32" + "github.com/fxamacker/cbor/v2" + "github.com/fystack/mpcium/pkg/client" + "github.com/fystack/mpcium/pkg/config" + "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/logger" + "github.com/fystack/mpcium/pkg/types" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/spf13/viper" +) + +// Minimal Cardano preprod PoC: +// - CreateWallet (MPCIUM) -> EDDSA pubkey +// - Derive enterprise address +// - Wait for funding UTxO (Blockfrost) +// - Build tx body CBOR, ask MPCIUM to sign tx-body hash +// - Build signed tx CBOR and submit (Blockfrost) + +func main() { + const environment = "development" + config.InitViperConfig("") + logger.Init(environment, true) + + bfProjectID := os.Getenv("BLOCKFROST_PROJECT_ID") + if bfProjectID == "" { + logger.Fatal("BLOCKFROST_PROJECT_ID env var is required", nil) + } + + algorithm := viper.GetString("event_initiator_algorithm") + if algorithm == "" { + algorithm = string(types.EventInitiatorKeyTypeEd25519) + } + if !slices.Contains([]string{string(types.EventInitiatorKeyTypeEd25519), string(types.EventInitiatorKeyTypeP256)}, algorithm) { + logger.Fatal(fmt.Sprintf("invalid algorithm: %s", algorithm), nil) + } + + natsURL := viper.GetString("nats.url") + natsConn, err := nats.Connect(natsURL) + if err != nil { + logger.Fatal("Failed to connect to NATS", err) + } + defer natsConn.Drain() + defer natsConn.Close() + + localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{KeyPath: "./event_initiator.key"}) + if err != nil { + logger.Fatal("Failed to create local signer", err) + } + + mpcClient := client.NewMPCClient(client.Options{NatsConn: natsConn, Signer: localSigner}) + + walletID := uuid.New().String() + created := make(chan event.KeygenResultEvent, 1) + err = mpcClient.OnWalletCreationResult(func(evt event.KeygenResultEvent) { + if evt.WalletID == walletID { + created <- evt + } + }) + if err != nil { + logger.Fatal("OnWalletCreationResult subscribe failed", err) + } + + if err := mpcClient.CreateWallet(walletID); err != nil { + logger.Fatal("CreateWallet failed", err) + } + logger.Info("CreateWallet sent", "walletID", walletID) + + var keygen event.KeygenResultEvent + select { + case keygen = <-created: + if keygen.ResultType == event.ResultTypeError { + logger.Fatal("Keygen failed: "+keygen.ErrorReason, nil) + } + logger.Info("Wallet created", "walletID", keygen.WalletID, "eddsa_pubkey_hex", hex.EncodeToString(keygen.EDDSAPubKey)) + case <-time.After(60 * time.Second): + logger.Fatal("Timeout waiting for wallet creation", nil) + } + + // 2) Normalize EDDSA pubkey (MPCIUM may return 33-byte compressed; Cardano needs 32-byte raw) + rawPub, err := normalizeEd25519PubKey(keygen.EDDSAPubKey) + if err != nil { + logger.Fatal("normalize pubkey failed", err) + } + logger.Info("EDDSA pubkey length", "len", len(keygen.EDDSAPubKey), "raw_len", len(rawPub)) + + // 3) Derive Cardano preprod enterprise address from pubkey + ourAddr, err := deriveEnterpriseAddressPreprod(rawPub) + if err != nil { + logger.Fatal("derive address failed", err) + } + logger.Info("Derived deposit address", "address", ourAddr) + + // 3) Wait for funds then fetch UTxOs (Blockfrost) + // Many explorers show an address even before it has appeared on-chain, + // but Blockfrost returns 404 until the first tx/UTxO exists for that address. + // So we poll with a generous timeout. + const depositWaitTimeout = 15 * time.Minute + ctx, cancel := context.WithTimeout(context.Background(), depositWaitTimeout) + defer cancel() + utxo, err := waitForFirstUtxo(ctx, bfProjectID, ourAddr) + if err != nil { + logger.Fatal("waiting utxo failed", err) + } + logger.Info("Found UTxO", "tx_hash", utxo.TxHash, "tx_index", utxo.TxIndex, "lovelace", utxo.Lovelace) + + // 4) Build a minimal tx body (1 input -> 1 output to destination + change back) + destAddr := "addr_test1qqe4rw4jujaezsrux4f4u58tyxl7ffr0aj7t8fnxukc702utg7lxfcdeet9d0kk73jlmuwytv6aj5t96mazuh7lpv8kq2ekx5u" + sendLovelace := uint64(1_000_000) // 1 ADA + feeLovelace := uint64(200_000) // rough fee for PoC + change := utxo.Lovelace - sendLovelace - feeLovelace + if utxo.Lovelace <= sendLovelace+feeLovelace { + logger.Fatal("not enough funds in UTxO", nil) + } + + txBodyCbor, err := buildTxBodyCBOR(utxo.TxHash, utxo.TxIndex, destAddr, sendLovelace, ourAddr, change, feeLovelace) + if err != nil { + logger.Fatal("buildTxBodyCBOR failed", err) + } + + // 5) Hash tx body (Cardano rule) and sign with MPCIUM + h := blake2b.Sum256(txBodyCbor) + txHash := h[:] + logger.Info("Prepared tx hash", "tx_hash_hex", hex.EncodeToString(txHash)) + + signResultCh := make(chan event.SigningResultEvent, 1) + err = mpcClient.OnSignResult(func(evt event.SigningResultEvent) { + if evt.WalletID == walletID { + signResultCh <- evt + } + }) + if err != nil { + logger.Fatal("OnSignResult subscribe failed", err) + } + + txID := uuid.New().String() + signMsg := &types.SignTxMessage{ + KeyType: types.KeyTypeEd25519, + WalletID: walletID, + NetworkInternalCode: "cardano-testnet", + TxID: txID, + Tx: txHash, // IMPORTANT: we ask MPCIUM to sign txHash bytes + } + if err := mpcClient.SignTransaction(signMsg); err != nil { + logger.Fatal("SignTransaction failed", err) + } + logger.Info("SignTransaction sent", "txID", txID) + + select { + case res := <-signResultCh: + if res.ResultType == event.ResultTypeError { + logger.Fatal("Signing failed: "+res.ErrorReason, nil) + } + logger.Info("Signature received", "sig_hex", hex.EncodeToString(res.Signature)) + + // 6) Build witness + full tx and submit via Blockfrost + signedTxCbor, err := buildSignedTxCBOR(txBodyCbor, rawPub, res.Signature) + if err != nil { + logger.Fatal("buildSignedTxCBOR failed", err) + } + + submitURL := "https://cardano-preprod.blockfrost.io/api/v0/tx/submit" + respBody, status, err := blockfrostPOSTCBOR(context.Background(), bfProjectID, submitURL, signedTxCbor) + if err != nil { + logger.Fatal("submit failed", err) + } + if status < 200 || status >= 300 { + logger.Fatal(fmt.Sprintf("submit HTTP %d: %s", status, prettyJSON(respBody)), nil) + } + var submittedHash string + _ = json.Unmarshal(respBody, &submittedHash) // response is a JSON string + if submittedHash == "" { + submittedHash = strings.TrimSpace(string(respBody)) + } + logger.Info("Submitted tx", "tx_hash", submittedHash) + case <-time.After(60 * time.Second): + logger.Fatal("Timeout waiting for signing result", nil) + } + +} + +// --- Cardano helpers (preprod) --- + +func normalizeEd25519PubKey(pk []byte) ([]byte, error) { + // Cardano Ed25519 vkey is 32 bytes. + if len(pk) == 32 { + return pk, nil + } + // MPCIUM EDDSA pubkey is sometimes encoded as 33 bytes with 0x02/0x03 prefix. + if len(pk) == 33 && (pk[0] == 0x02 || pk[0] == 0x03) { + return pk[1:], nil + } + prefix := byte(0) + if len(pk) > 0 { + prefix = pk[0] + } + return nil, fmt.Errorf("unsupported eddsa pubkey format: len=%d prefix=0x%02x", len(pk), prefix) +} + +type bfUtxo struct { + TxHash string `json:"tx_hash"` + TxIndex int `json:"tx_index"` + Amount []struct { + Unit string `json:"unit"` + Quantity string `json:"quantity"` + } `json:"amount"` +} + +type simpleUtxo struct { + TxHash string + TxIndex uint32 + Lovelace uint64 +} + +func waitForFirstUtxo(ctx context.Context, projectID, addr string) (*simpleUtxo, error) { + url := "https://cardano-preprod.blockfrost.io/api/v0/addresses/" + addr + "/utxos" + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + b, status, err := blockfrostGET(ctx, projectID, url) + if err != nil { + return nil, err + } + if status == 404 { + logger.Info("UTxO not found yet (404), waiting...", "address", addr) + continue + } + if status < 200 || status >= 300 { + return nil, fmt.Errorf("utxo HTTP %d: %s", status, prettyJSON(b)) + } + var utxos []bfUtxo + if err := json.Unmarshal(b, &utxos); err != nil { + return nil, err + } + if len(utxos) == 0 { + logger.Info("No UTxO yet, waiting...", "address", addr) + continue + } + lovelace, err := findLovelace(utxos[0].Amount) + if err != nil { + return nil, err + } + return &simpleUtxo{TxHash: utxos[0].TxHash, TxIndex: uint32(utxos[0].TxIndex), Lovelace: lovelace}, nil + } + } +} + +func findLovelace(amts []struct { + Unit string `json:"unit"` + Quantity string `json:"quantity"` +}) (uint64, error) { + for _, a := range amts { + if a.Unit == "lovelace" { + var v uint64 + _, err := fmt.Sscanf(a.Quantity, "%d", &v) + return v, err + } + } + return 0, errors.New("no lovelace in utxo") +} + +func deriveEnterpriseAddressPreprod(pubKey []byte) (string, error) { + // Cardano enterprise address (CIP-19) for testnet/preprod. + // address = header || payment_keyhash + // header: 0b0110 (enterprise) << 4 | network_id + // preprod network_id = 0 (same as testnet) + if len(pubKey) != 32 { + return "", fmt.Errorf("expected 32-byte ed25519 pubkey, got %d", len(pubKey)) + } + + // payment key hash = blake2b-224(pubkey) + // (matches cardano-serialization-lib: utxoPubKey.to_raw_key().hash()) + h, err := blake2b.New(28, nil) + if err != nil { + return "", err + } + _, _ = h.Write(pubKey) + payKeyHash := h.Sum(nil) // 28 bytes + + // preprod/testnet network id = 0 + header := byte(0x60) // enterprise (0x6) with network id 0 + addrBytes := append([]byte{header}, payKeyHash...) + + data5, err := bech32.ConvertBits(addrBytes, 8, 5, true) + if err != nil { + return "", err + } + encoded, err := bech32.Encode("addr_test", data5) + if err != nil { + return "", err + } + + // sanity check: decode back the bytes we just encoded + _, decoded5, derr := bech32.DecodeNoLimit(encoded) + if derr != nil { + return "", fmt.Errorf("bech32 self-check decode failed: %w", derr) + } + decoded8, derr := bech32.ConvertBits(decoded5, 5, 8, false) + if derr != nil { + return "", fmt.Errorf("bech32 self-check convertbits failed: %w", derr) + } + if !bytes.Equal(decoded8, addrBytes) { + return "", fmt.Errorf("bech32 self-check mismatch: want %x got %x", addrBytes, decoded8) + } + + return encoded, nil +} + +// TxBody CBOR is a CBOR map keyed by integers per Cardano spec. +// For PoC we keep it minimal: inputs, outputs, fee. +func buildTxBodyCBOR(txHashHex string, txIndex uint32, toAddr string, toLovelace uint64, changeAddr string, changeLovelace uint64, feeLovelace uint64) ([]byte, error) { + // Inputs: [ [ txHash(bytes32), index(uint) ] ] + txHash, err := hex.DecodeString(txHashHex) + if err != nil { + return nil, err + } + if len(txHash) != 32 { + return nil, fmt.Errorf("tx hash must be 32 bytes, got %d", len(txHash)) + } + + toBytes, err := decodeCardanoAddressBytes(toAddr) + if err != nil { + return nil, err + } + chgBytes, err := decodeCardanoAddressBytes(changeAddr) + if err != nil { + return nil, err + } + + // Cardano TxOut value is a Coin (uint) for ADA-only, or a multi-asset structure. + // For ADA-only outputs, encode value as uint (NOT a map). + out1 := []any{toBytes, toLovelace} + out2 := []any{chgBytes, changeLovelace} + + body := map[any]any{ + 0: []any{[]any{txHash, txIndex}}, // inputs + 1: []any{out1, out2}, // outputs + 2: feeLovelace, // fee + } + return cbor.Marshal(body) +} +func decodeCardanoAddressBytes(addrStr string) ([]byte, error) { + _, data5, err := bech32.DecodeNoLimit(addrStr) + if err != nil { + return nil, fmt.Errorf("failed to decode bech32 address '%s': %w", addrStr, err) + } + // The data part returned by Decode is 5-bit grouped. Convert to 8-bit bytes. + data8, err := bech32.ConvertBits(data5, 5, 8, false) + if err != nil { + return nil, fmt.Errorf("failed to convert address bits for '%s': %w", addrStr, err) + } + return data8, nil +} + +func buildSignedTxCBOR(txBodyCbor []byte, pubKey []byte, sig []byte) ([]byte, error) { + if len(sig) == 0 { + return nil, errors.New("empty signature") + } + // Transaction: [ tx_body, witness_set, is_valid(true), auxiliary_data(nil) ] + // Witness set for vkey: { 0: [ [ vkey, sig ] ] } + witnessSet := map[any]any{0: []any{[]any{pubKey, sig}}} + + var body any + if err := cbor.Unmarshal(txBodyCbor, &body); err != nil { + return nil, err + } + tx := []any{body, witnessSet, true, nil} + return cbor.Marshal(tx) +} + +// --- Blockfrost helpers --- + +func blockfrostGET(ctx context.Context, projectID, url string) ([]byte, int, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, 0, err + } + req.Header.Set("project_id", projectID) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, 0, err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + return b, resp.StatusCode, err +} + +func blockfrostPOSTCBOR(ctx context.Context, projectID, url string, cbor []byte) ([]byte, int, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(cbor)) + if err != nil { + return nil, 0, err + } + req.Header.Set("project_id", projectID) + req.Header.Set("Content-Type", "application/cbor") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, 0, err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + return b, resp.StatusCode, err +} + +// Debug helper +func prettyJSON(b []byte) string { + var v any + if err := json.Unmarshal(b, &v); err != nil { + return string(b) + } + out, _ := json.MarshalIndent(v, "", " ") + return string(out) +} diff --git a/go.mod b/go.mod index cf8dc21..a821b34 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,12 @@ require ( github.com/btcsuite/btcd v0.24.2 github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcutil v1.0.2 + github.com/cosmos/btcutil v1.0.5 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 - github.com/dgraph-io/badger/v4 v4.7.0 + github.com/dgraph-io/badger/v4 v4.2.0 + github.com/fxamacker/cbor/v2 v2.6.0 github.com/google/uuid v1.6.0 - github.com/hashicorp/consul/api v1.32.1 + github.com/hashicorp/consul/api v1.31.0 github.com/mitchellh/mapstructure v1.5.0 github.com/nats-io/nats.go v1.31.0 github.com/rs/zerolog v1.31.0 @@ -23,7 +25,7 @@ require ( github.com/spf13/viper v1.18.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.3.2 - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.26.0 golang.org/x/term v0.31.0 ) @@ -45,14 +47,18 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.0.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect @@ -86,20 +92,20 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.39.0 // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 697a3ea..a25e7cd 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -79,12 +80,17 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -97,14 +103,20 @@ github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgW github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= -github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= -github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= -github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -116,22 +128,26 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/fystack/tss-lib/v2 v2.0.3 h1:A0HGL5GDPpKbNW+0ZXgv1Ri3+ks88AvxTS7OK40gnUY= github.com/fystack/tss-lib/v2 v2.0.3/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -140,26 +156,35 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= -github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= +github.com/hashicorp/consul/api v1.31.0 h1:32BUNLembeSRek0G/ZAM6WNfdEwYdYo8oQ4+JoqGkNQ= +github.com/hashicorp/consul/api v1.31.0/go.mod h1:2ZGIiXM3A610NmDULmCHd/aqBJj8CkMfOhswhOafxRg= github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -223,6 +248,7 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -307,6 +333,7 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= @@ -355,6 +382,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -365,18 +393,14 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o= github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= @@ -402,10 +426,14 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -414,8 +442,11 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -425,14 +456,16 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -443,6 +476,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -475,6 +509,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -499,7 +534,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -514,13 +552,27 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= @@ -540,10 +592,13 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= From 1f2b4bf0f29b3bd14e8b3298ab5b167f1f28928c Mon Sep 17 00:00:00 2001 From: Woft257 Date: Wed, 24 Dec 2025 23:32:26 +0700 Subject: [PATCH 02/13] feat(cardano_poc): add create_wallet and sign_tx examples with shared helpers --- examples/cardano_poc/create_wallet.go | 102 ++++++ examples/cardano_poc/lib.go | 191 +++++++++++ examples/cardano_poc/main.go | 435 -------------------------- examples/cardano_poc/sign_tx.go | 217 +++++++++++++ 4 files changed, 510 insertions(+), 435 deletions(-) create mode 100644 examples/cardano_poc/create_wallet.go create mode 100644 examples/cardano_poc/lib.go delete mode 100644 examples/cardano_poc/main.go create mode 100644 examples/cardano_poc/sign_tx.go diff --git a/examples/cardano_poc/create_wallet.go b/examples/cardano_poc/create_wallet.go new file mode 100644 index 0000000..7f0eb13 --- /dev/null +++ b/examples/cardano_poc/create_wallet.go @@ -0,0 +1,102 @@ +//go:build create_wallet + +package main + +import ( + "encoding/hex" + "errors" + "fmt" + "os" + "slices" + "time" + + "github.com/fystack/mpcium/pkg/client" + "github.com/fystack/mpcium/pkg/config" + "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/logger" + "github.com/fystack/mpcium/pkg/types" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/spf13/viper" +) + +// Usage: +// go run ./examples/cardano_poc/create_wallet.go +// Output: +// Prints wallet_id and deposit address (enterprise) to stdout. +// +// Note: this command does NOT wait for funding. + +func main() { + const environment = "development" + config.InitViperConfig("") + logger.Init(environment, true) + + algorithm := viper.GetString("event_initiator_algorithm") + if algorithm == "" { + algorithm = string(types.EventInitiatorKeyTypeEd25519) + } + if !slices.Contains([]string{string(types.EventInitiatorKeyTypeEd25519), string(types.EventInitiatorKeyTypeP256)}, algorithm) { + logger.Fatal(fmt.Sprintf("invalid algorithm: %s", algorithm), nil) + } + + natsURL := viper.GetString("nats.url") + natsConn, err := nats.Connect(natsURL) + if err != nil { + logger.Fatal("Failed to connect to NATS", err) + } + defer natsConn.Drain() + defer natsConn.Close() + + localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{KeyPath: "./event_initiator.key"}) + if err != nil { + logger.Fatal("Failed to create local signer", err) + } + + mpcClient := client.NewMPCClient(client.Options{NatsConn: natsConn, Signer: localSigner}) + + walletID := uuid.New().String() + created := make(chan event.KeygenResultEvent, 1) + err = mpcClient.OnWalletCreationResult(func(evt event.KeygenResultEvent) { + if evt.WalletID == walletID { + created <- evt + } + }) + if err != nil { + logger.Fatal("OnWalletCreationResult subscribe failed", err) + } + + if err := mpcClient.CreateWallet(walletID); err != nil { + logger.Fatal("CreateWallet failed", err) + } + logger.Info("CreateWallet sent", "walletID", walletID) + + select { + case keygen := <-created: + if keygen.ResultType == event.ResultTypeError { + logger.Fatal("Keygen failed: "+keygen.ErrorReason, nil) + } + + rawPub, err := normalizeEd25519PubKey(keygen.EDDSAPubKey) + if err != nil { + logger.Fatal("normalize pubkey failed", err) + } + ourAddr, err := deriveEnterpriseAddressPreprod(rawPub) + if err != nil { + logger.Fatal("derive address failed", err) + } + + // Save to a local file next to this PoC. + // This keeps it simple and avoids forcing a "reuse" mode. + rec := fmt.Sprintf("{\n \"wallet_id\": \"%s\",\n \"eddsa_pubkey_hex\": \"%s\"\n}\n", walletID, hex.EncodeToString(keygen.EDDSAPubKey)) + if werr := os.WriteFile("examples/cardano_poc/cardano_poc_wallet.json", []byte(rec), 0o644); werr != nil { + logger.Fatal("failed to write wallet file", werr) + } + + fmt.Println("wallet_id:", walletID) + fmt.Println("eddsa_pubkey_hex:", hex.EncodeToString(keygen.EDDSAPubKey)) + fmt.Println("deposit_address:", ourAddr) + case <-time.After(60 * time.Second): + logger.Fatal("Timeout waiting for wallet creation", errors.New("timeout")) + } +} diff --git a/examples/cardano_poc/lib.go b/examples/cardano_poc/lib.go new file mode 100644 index 0000000..df7bd42 --- /dev/null +++ b/examples/cardano_poc/lib.go @@ -0,0 +1,191 @@ +package main + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/cosmos/btcutil/bech32" + "github.com/fxamacker/cbor/v2" + "golang.org/x/crypto/blake2b" +) + +// --- shared helpers (used by create_wallet.go and sign_tx.go) --- + +type bfUtxo struct { + TxHash string `json:"tx_hash"` + TxIndex int `json:"tx_index"` + Amount []struct { + Unit string `json:"unit"` + Quantity string `json:"quantity"` + } `json:"amount"` +} + +type simpleUtxo struct { + TxHash string + TxIndex uint32 + Lovelace uint64 +} + +func normalizeEd25519PubKey(pk []byte) ([]byte, error) { + if len(pk) == 32 { + return pk, nil + } + if len(pk) == 33 && (pk[0] == 0x02 || pk[0] == 0x03) { + return pk[1:], nil + } + prefix := byte(0) + if len(pk) > 0 { + prefix = pk[0] + } + return nil, fmt.Errorf("unsupported eddsa pubkey format: len=%d prefix=0x%02x", len(pk), prefix) +} + +func deriveEnterpriseAddressPreprod(pubKey []byte) (string, error) { + if len(pubKey) != 32 { + return "", fmt.Errorf("expected 32-byte ed25519 pubkey, got %d", len(pubKey)) + } + h, err := blake2b.New(28, nil) + if err != nil { + return "", err + } + _, _ = h.Write(pubKey) + payKeyHash := h.Sum(nil) + + header := byte(0x60) // enterprise, network id 0 (preprod/testnet) + addrBytes := append([]byte{header}, payKeyHash...) + + data5, err := bech32.ConvertBits(addrBytes, 8, 5, true) + if err != nil { + return "", err + } + encoded, err := bech32.Encode("addr_test", data5) + if err != nil { + return "", err + } + // sanity + _, decoded5, derr := bech32.DecodeNoLimit(encoded) + if derr != nil { + return "", fmt.Errorf("bech32 self-check decode failed: %w", derr) + } + decoded8, derr := bech32.ConvertBits(decoded5, 5, 8, false) + if derr != nil { + return "", fmt.Errorf("bech32 self-check convertbits failed: %w", derr) + } + if !bytes.Equal(decoded8, addrBytes) { + return "", fmt.Errorf("bech32 self-check mismatch") + } + return encoded, nil +} + +func decodeCardanoAddressBytes(addrStr string) ([]byte, error) { + _, data5, err := bech32.DecodeNoLimit(addrStr) + if err != nil { + return nil, fmt.Errorf("failed to decode bech32 address '%s': %w", addrStr, err) + } + data8, err := bech32.ConvertBits(data5, 5, 8, false) + if err != nil { + return nil, fmt.Errorf("failed to convert address bits for '%s': %w", addrStr, err) + } + return data8, nil +} + +func buildTxBodyCBOR(txHashHex string, txIndex uint32, toAddr string, toLovelace uint64, changeAddr string, changeLovelace uint64, feeLovelace uint64) ([]byte, error) { + txHash, err := hex.DecodeString(txHashHex) + if err != nil { + return nil, err + } + if len(txHash) != 32 { + return nil, fmt.Errorf("tx hash must be 32 bytes, got %d", len(txHash)) + } + toBytes, err := decodeCardanoAddressBytes(toAddr) + if err != nil { + return nil, err + } + chgBytes, err := decodeCardanoAddressBytes(changeAddr) + if err != nil { + return nil, err + } + out1 := []any{toBytes, toLovelace} + out2 := []any{chgBytes, changeLovelace} + body := map[any]any{0: []any{[]any{txHash, txIndex}}, 1: []any{out1, out2}, 2: feeLovelace} + return cbor.Marshal(body) +} + +func buildSignedTxCBOR(txBodyCbor []byte, pubKey []byte, sig []byte) ([]byte, error) { + if len(sig) == 0 { + return nil, errors.New("empty signature") + } + witnessSet := map[any]any{0: []any{[]any{pubKey, sig}}} + var body any + if err := cbor.Unmarshal(txBodyCbor, &body); err != nil { + return nil, err + } + tx := []any{body, witnessSet, true, nil} + return cbor.Marshal(tx) +} + +func findLovelace(amts []struct { + Unit string `json:"unit"` + Quantity string `json:"quantity"` +}) (uint64, error) { + for _, a := range amts { + if a.Unit == "lovelace" { + var v uint64 + _, err := fmt.Sscanf(a.Quantity, "%d", &v) + return v, err + } + } + return 0, errors.New("no lovelace in utxo") +} + +func blockfrostGET(ctx context.Context, projectID, url string) ([]byte, int, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, 0, err + } + req.Header.Set("project_id", projectID) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, 0, err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + return b, resp.StatusCode, err +} + +func blockfrostPOSTCBOR(ctx context.Context, projectID, url string, cborBytes []byte) ([]byte, int, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(cborBytes)) + if err != nil { + return nil, 0, err + } + req.Header.Set("project_id", projectID) + req.Header.Set("Content-Type", "application/cbor") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, 0, err + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + return b, resp.StatusCode, err +} + +func prettyJSON(b []byte) string { + var v any + if err := json.Unmarshal(b, &v); err != nil { + return string(b) + } + out, _ := json.MarshalIndent(v, "", " ") + return string(out) +} + +func trimJSONQuotes(s string) string { + s = strings.TrimSpace(s) + return strings.Trim(s, "\"\n\r\t ") +} diff --git a/examples/cardano_poc/main.go b/examples/cardano_poc/main.go deleted file mode 100644 index b32cb4f..0000000 --- a/examples/cardano_poc/main.go +++ /dev/null @@ -1,435 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "slices" - "strings" - "time" - - "golang.org/x/crypto/blake2b" - - "github.com/cosmos/btcutil/bech32" - "github.com/fxamacker/cbor/v2" - "github.com/fystack/mpcium/pkg/client" - "github.com/fystack/mpcium/pkg/config" - "github.com/fystack/mpcium/pkg/event" - "github.com/fystack/mpcium/pkg/logger" - "github.com/fystack/mpcium/pkg/types" - "github.com/google/uuid" - "github.com/nats-io/nats.go" - "github.com/spf13/viper" -) - -// Minimal Cardano preprod PoC: -// - CreateWallet (MPCIUM) -> EDDSA pubkey -// - Derive enterprise address -// - Wait for funding UTxO (Blockfrost) -// - Build tx body CBOR, ask MPCIUM to sign tx-body hash -// - Build signed tx CBOR and submit (Blockfrost) - -func main() { - const environment = "development" - config.InitViperConfig("") - logger.Init(environment, true) - - bfProjectID := os.Getenv("BLOCKFROST_PROJECT_ID") - if bfProjectID == "" { - logger.Fatal("BLOCKFROST_PROJECT_ID env var is required", nil) - } - - algorithm := viper.GetString("event_initiator_algorithm") - if algorithm == "" { - algorithm = string(types.EventInitiatorKeyTypeEd25519) - } - if !slices.Contains([]string{string(types.EventInitiatorKeyTypeEd25519), string(types.EventInitiatorKeyTypeP256)}, algorithm) { - logger.Fatal(fmt.Sprintf("invalid algorithm: %s", algorithm), nil) - } - - natsURL := viper.GetString("nats.url") - natsConn, err := nats.Connect(natsURL) - if err != nil { - logger.Fatal("Failed to connect to NATS", err) - } - defer natsConn.Drain() - defer natsConn.Close() - - localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{KeyPath: "./event_initiator.key"}) - if err != nil { - logger.Fatal("Failed to create local signer", err) - } - - mpcClient := client.NewMPCClient(client.Options{NatsConn: natsConn, Signer: localSigner}) - - walletID := uuid.New().String() - created := make(chan event.KeygenResultEvent, 1) - err = mpcClient.OnWalletCreationResult(func(evt event.KeygenResultEvent) { - if evt.WalletID == walletID { - created <- evt - } - }) - if err != nil { - logger.Fatal("OnWalletCreationResult subscribe failed", err) - } - - if err := mpcClient.CreateWallet(walletID); err != nil { - logger.Fatal("CreateWallet failed", err) - } - logger.Info("CreateWallet sent", "walletID", walletID) - - var keygen event.KeygenResultEvent - select { - case keygen = <-created: - if keygen.ResultType == event.ResultTypeError { - logger.Fatal("Keygen failed: "+keygen.ErrorReason, nil) - } - logger.Info("Wallet created", "walletID", keygen.WalletID, "eddsa_pubkey_hex", hex.EncodeToString(keygen.EDDSAPubKey)) - case <-time.After(60 * time.Second): - logger.Fatal("Timeout waiting for wallet creation", nil) - } - - // 2) Normalize EDDSA pubkey (MPCIUM may return 33-byte compressed; Cardano needs 32-byte raw) - rawPub, err := normalizeEd25519PubKey(keygen.EDDSAPubKey) - if err != nil { - logger.Fatal("normalize pubkey failed", err) - } - logger.Info("EDDSA pubkey length", "len", len(keygen.EDDSAPubKey), "raw_len", len(rawPub)) - - // 3) Derive Cardano preprod enterprise address from pubkey - ourAddr, err := deriveEnterpriseAddressPreprod(rawPub) - if err != nil { - logger.Fatal("derive address failed", err) - } - logger.Info("Derived deposit address", "address", ourAddr) - - // 3) Wait for funds then fetch UTxOs (Blockfrost) - // Many explorers show an address even before it has appeared on-chain, - // but Blockfrost returns 404 until the first tx/UTxO exists for that address. - // So we poll with a generous timeout. - const depositWaitTimeout = 15 * time.Minute - ctx, cancel := context.WithTimeout(context.Background(), depositWaitTimeout) - defer cancel() - utxo, err := waitForFirstUtxo(ctx, bfProjectID, ourAddr) - if err != nil { - logger.Fatal("waiting utxo failed", err) - } - logger.Info("Found UTxO", "tx_hash", utxo.TxHash, "tx_index", utxo.TxIndex, "lovelace", utxo.Lovelace) - - // 4) Build a minimal tx body (1 input -> 1 output to destination + change back) - destAddr := "addr_test1qqe4rw4jujaezsrux4f4u58tyxl7ffr0aj7t8fnxukc702utg7lxfcdeet9d0kk73jlmuwytv6aj5t96mazuh7lpv8kq2ekx5u" - sendLovelace := uint64(1_000_000) // 1 ADA - feeLovelace := uint64(200_000) // rough fee for PoC - change := utxo.Lovelace - sendLovelace - feeLovelace - if utxo.Lovelace <= sendLovelace+feeLovelace { - logger.Fatal("not enough funds in UTxO", nil) - } - - txBodyCbor, err := buildTxBodyCBOR(utxo.TxHash, utxo.TxIndex, destAddr, sendLovelace, ourAddr, change, feeLovelace) - if err != nil { - logger.Fatal("buildTxBodyCBOR failed", err) - } - - // 5) Hash tx body (Cardano rule) and sign with MPCIUM - h := blake2b.Sum256(txBodyCbor) - txHash := h[:] - logger.Info("Prepared tx hash", "tx_hash_hex", hex.EncodeToString(txHash)) - - signResultCh := make(chan event.SigningResultEvent, 1) - err = mpcClient.OnSignResult(func(evt event.SigningResultEvent) { - if evt.WalletID == walletID { - signResultCh <- evt - } - }) - if err != nil { - logger.Fatal("OnSignResult subscribe failed", err) - } - - txID := uuid.New().String() - signMsg := &types.SignTxMessage{ - KeyType: types.KeyTypeEd25519, - WalletID: walletID, - NetworkInternalCode: "cardano-testnet", - TxID: txID, - Tx: txHash, // IMPORTANT: we ask MPCIUM to sign txHash bytes - } - if err := mpcClient.SignTransaction(signMsg); err != nil { - logger.Fatal("SignTransaction failed", err) - } - logger.Info("SignTransaction sent", "txID", txID) - - select { - case res := <-signResultCh: - if res.ResultType == event.ResultTypeError { - logger.Fatal("Signing failed: "+res.ErrorReason, nil) - } - logger.Info("Signature received", "sig_hex", hex.EncodeToString(res.Signature)) - - // 6) Build witness + full tx and submit via Blockfrost - signedTxCbor, err := buildSignedTxCBOR(txBodyCbor, rawPub, res.Signature) - if err != nil { - logger.Fatal("buildSignedTxCBOR failed", err) - } - - submitURL := "https://cardano-preprod.blockfrost.io/api/v0/tx/submit" - respBody, status, err := blockfrostPOSTCBOR(context.Background(), bfProjectID, submitURL, signedTxCbor) - if err != nil { - logger.Fatal("submit failed", err) - } - if status < 200 || status >= 300 { - logger.Fatal(fmt.Sprintf("submit HTTP %d: %s", status, prettyJSON(respBody)), nil) - } - var submittedHash string - _ = json.Unmarshal(respBody, &submittedHash) // response is a JSON string - if submittedHash == "" { - submittedHash = strings.TrimSpace(string(respBody)) - } - logger.Info("Submitted tx", "tx_hash", submittedHash) - case <-time.After(60 * time.Second): - logger.Fatal("Timeout waiting for signing result", nil) - } - -} - -// --- Cardano helpers (preprod) --- - -func normalizeEd25519PubKey(pk []byte) ([]byte, error) { - // Cardano Ed25519 vkey is 32 bytes. - if len(pk) == 32 { - return pk, nil - } - // MPCIUM EDDSA pubkey is sometimes encoded as 33 bytes with 0x02/0x03 prefix. - if len(pk) == 33 && (pk[0] == 0x02 || pk[0] == 0x03) { - return pk[1:], nil - } - prefix := byte(0) - if len(pk) > 0 { - prefix = pk[0] - } - return nil, fmt.Errorf("unsupported eddsa pubkey format: len=%d prefix=0x%02x", len(pk), prefix) -} - -type bfUtxo struct { - TxHash string `json:"tx_hash"` - TxIndex int `json:"tx_index"` - Amount []struct { - Unit string `json:"unit"` - Quantity string `json:"quantity"` - } `json:"amount"` -} - -type simpleUtxo struct { - TxHash string - TxIndex uint32 - Lovelace uint64 -} - -func waitForFirstUtxo(ctx context.Context, projectID, addr string) (*simpleUtxo, error) { - url := "https://cardano-preprod.blockfrost.io/api/v0/addresses/" + addr + "/utxos" - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-ticker.C: - b, status, err := blockfrostGET(ctx, projectID, url) - if err != nil { - return nil, err - } - if status == 404 { - logger.Info("UTxO not found yet (404), waiting...", "address", addr) - continue - } - if status < 200 || status >= 300 { - return nil, fmt.Errorf("utxo HTTP %d: %s", status, prettyJSON(b)) - } - var utxos []bfUtxo - if err := json.Unmarshal(b, &utxos); err != nil { - return nil, err - } - if len(utxos) == 0 { - logger.Info("No UTxO yet, waiting...", "address", addr) - continue - } - lovelace, err := findLovelace(utxos[0].Amount) - if err != nil { - return nil, err - } - return &simpleUtxo{TxHash: utxos[0].TxHash, TxIndex: uint32(utxos[0].TxIndex), Lovelace: lovelace}, nil - } - } -} - -func findLovelace(amts []struct { - Unit string `json:"unit"` - Quantity string `json:"quantity"` -}) (uint64, error) { - for _, a := range amts { - if a.Unit == "lovelace" { - var v uint64 - _, err := fmt.Sscanf(a.Quantity, "%d", &v) - return v, err - } - } - return 0, errors.New("no lovelace in utxo") -} - -func deriveEnterpriseAddressPreprod(pubKey []byte) (string, error) { - // Cardano enterprise address (CIP-19) for testnet/preprod. - // address = header || payment_keyhash - // header: 0b0110 (enterprise) << 4 | network_id - // preprod network_id = 0 (same as testnet) - if len(pubKey) != 32 { - return "", fmt.Errorf("expected 32-byte ed25519 pubkey, got %d", len(pubKey)) - } - - // payment key hash = blake2b-224(pubkey) - // (matches cardano-serialization-lib: utxoPubKey.to_raw_key().hash()) - h, err := blake2b.New(28, nil) - if err != nil { - return "", err - } - _, _ = h.Write(pubKey) - payKeyHash := h.Sum(nil) // 28 bytes - - // preprod/testnet network id = 0 - header := byte(0x60) // enterprise (0x6) with network id 0 - addrBytes := append([]byte{header}, payKeyHash...) - - data5, err := bech32.ConvertBits(addrBytes, 8, 5, true) - if err != nil { - return "", err - } - encoded, err := bech32.Encode("addr_test", data5) - if err != nil { - return "", err - } - - // sanity check: decode back the bytes we just encoded - _, decoded5, derr := bech32.DecodeNoLimit(encoded) - if derr != nil { - return "", fmt.Errorf("bech32 self-check decode failed: %w", derr) - } - decoded8, derr := bech32.ConvertBits(decoded5, 5, 8, false) - if derr != nil { - return "", fmt.Errorf("bech32 self-check convertbits failed: %w", derr) - } - if !bytes.Equal(decoded8, addrBytes) { - return "", fmt.Errorf("bech32 self-check mismatch: want %x got %x", addrBytes, decoded8) - } - - return encoded, nil -} - -// TxBody CBOR is a CBOR map keyed by integers per Cardano spec. -// For PoC we keep it minimal: inputs, outputs, fee. -func buildTxBodyCBOR(txHashHex string, txIndex uint32, toAddr string, toLovelace uint64, changeAddr string, changeLovelace uint64, feeLovelace uint64) ([]byte, error) { - // Inputs: [ [ txHash(bytes32), index(uint) ] ] - txHash, err := hex.DecodeString(txHashHex) - if err != nil { - return nil, err - } - if len(txHash) != 32 { - return nil, fmt.Errorf("tx hash must be 32 bytes, got %d", len(txHash)) - } - - toBytes, err := decodeCardanoAddressBytes(toAddr) - if err != nil { - return nil, err - } - chgBytes, err := decodeCardanoAddressBytes(changeAddr) - if err != nil { - return nil, err - } - - // Cardano TxOut value is a Coin (uint) for ADA-only, or a multi-asset structure. - // For ADA-only outputs, encode value as uint (NOT a map). - out1 := []any{toBytes, toLovelace} - out2 := []any{chgBytes, changeLovelace} - - body := map[any]any{ - 0: []any{[]any{txHash, txIndex}}, // inputs - 1: []any{out1, out2}, // outputs - 2: feeLovelace, // fee - } - return cbor.Marshal(body) -} -func decodeCardanoAddressBytes(addrStr string) ([]byte, error) { - _, data5, err := bech32.DecodeNoLimit(addrStr) - if err != nil { - return nil, fmt.Errorf("failed to decode bech32 address '%s': %w", addrStr, err) - } - // The data part returned by Decode is 5-bit grouped. Convert to 8-bit bytes. - data8, err := bech32.ConvertBits(data5, 5, 8, false) - if err != nil { - return nil, fmt.Errorf("failed to convert address bits for '%s': %w", addrStr, err) - } - return data8, nil -} - -func buildSignedTxCBOR(txBodyCbor []byte, pubKey []byte, sig []byte) ([]byte, error) { - if len(sig) == 0 { - return nil, errors.New("empty signature") - } - // Transaction: [ tx_body, witness_set, is_valid(true), auxiliary_data(nil) ] - // Witness set for vkey: { 0: [ [ vkey, sig ] ] } - witnessSet := map[any]any{0: []any{[]any{pubKey, sig}}} - - var body any - if err := cbor.Unmarshal(txBodyCbor, &body); err != nil { - return nil, err - } - tx := []any{body, witnessSet, true, nil} - return cbor.Marshal(tx) -} - -// --- Blockfrost helpers --- - -func blockfrostGET(ctx context.Context, projectID, url string) ([]byte, int, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, 0, err - } - req.Header.Set("project_id", projectID) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, 0, err - } - defer resp.Body.Close() - b, err := io.ReadAll(resp.Body) - return b, resp.StatusCode, err -} - -func blockfrostPOSTCBOR(ctx context.Context, projectID, url string, cbor []byte) ([]byte, int, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(cbor)) - if err != nil { - return nil, 0, err - } - req.Header.Set("project_id", projectID) - req.Header.Set("Content-Type", "application/cbor") - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, 0, err - } - defer resp.Body.Close() - b, err := io.ReadAll(resp.Body) - return b, resp.StatusCode, err -} - -// Debug helper -func prettyJSON(b []byte) string { - var v any - if err := json.Unmarshal(b, &v); err != nil { - return string(b) - } - out, _ := json.MarshalIndent(v, "", " ") - return string(out) -} diff --git a/examples/cardano_poc/sign_tx.go b/examples/cardano_poc/sign_tx.go new file mode 100644 index 0000000..5c91d6e --- /dev/null +++ b/examples/cardano_poc/sign_tx.go @@ -0,0 +1,217 @@ +//go:build sign_tx + +package main + +import ( + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "slices" + "strings" + "time" + + "golang.org/x/crypto/blake2b" + + "github.com/fystack/mpcium/pkg/client" + "github.com/fystack/mpcium/pkg/config" + "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/logger" + "github.com/fystack/mpcium/pkg/types" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/spf13/viper" +) + +// Usage: +// $env:BLOCKFROST_PROJECT_ID="..." +// go run ./examples/cardano_poc/sign_tx.go --wallet-id --to --amount-ada 1 +// +// Notes: +// - This file does NOT wait for UTxO. It expects you to fund the deposit address first. +// - It reads pubkey from examples/cardano_poc/cardano_poc_wallet.json. + +type walletFile struct { + WalletID string `json:"wallet_id"` + EDDSAPubKeyHex string `json:"eddsa_pubkey_hex"` +} + +func main() { + logger.Init("development", true) + config.InitViperConfig("") + + bfProjectID := os.Getenv("BLOCKFROST_PROJECT_ID") + if bfProjectID == "" { + logger.Fatal("BLOCKFROST_PROJECT_ID env var is required", nil) + } + + walletID := "" + toAddr := "" + amountAda := uint64(1) + for i := 1; i < len(os.Args); i++ { + switch os.Args[i] { + case "--wallet-id": + i++ + walletID = os.Args[i] + case "--to": + i++ + toAddr = os.Args[i] + case "--amount-ada": + i++ + _, _ = fmt.Sscanf(os.Args[i], "%d", &amountAda) + } + } + if walletID == "" || toAddr == "" { + logger.Fatal("usage: sign_tx.go --wallet-id --to --amount-ada 1", nil) + } + + b, err := os.ReadFile("examples/cardano_poc/cardano_poc_wallet.json") + if err != nil { + logger.Fatal("failed to read wallet file", err) + } + var wf walletFile + if err := json.Unmarshal(b, &wf); err != nil { + logger.Fatal("invalid wallet file json", err) + } + if wf.WalletID != walletID { + logger.Fatal(fmt.Sprintf("wallet mismatch: file has %s", wf.WalletID), nil) + } + pubKeyBytes, err := hex.DecodeString(wf.EDDSAPubKeyHex) + if err != nil { + logger.Fatal("invalid pubkey hex", err) + } + + rawPub, err := normalizeEd25519PubKey(pubKeyBytes) + if err != nil { + logger.Fatal("normalize pubkey failed", err) + } + ourAddr, err := deriveEnterpriseAddressPreprod(rawPub) + if err != nil { + logger.Fatal("derive address failed", err) + } + logger.Info("Deposit address (must be funded before signing)", "address", ourAddr) + + // Fetch first UTxO once (NO WAIT/RETRY) + utxo, err := fetchFirstUtxoOnce(context.Background(), bfProjectID, ourAddr) + if err != nil { + logger.Fatal("fetch utxo failed", err) + } + logger.Info("Using UTxO", "tx_hash", utxo.TxHash, "tx_index", utxo.TxIndex, "lovelace", utxo.Lovelace) + + sendLovelace := amountAda * 1_000_000 + feeLovelace := uint64(200_000) + if utxo.Lovelace <= sendLovelace+feeLovelace { + logger.Fatal("not enough funds", nil) + } + change := utxo.Lovelace - sendLovelace - feeLovelace + + txBodyCbor, err := buildTxBodyCBOR(utxo.TxHash, utxo.TxIndex, toAddr, sendLovelace, ourAddr, change, feeLovelace) + if err != nil { + logger.Fatal("buildTxBodyCBOR failed", err) + } + h := blake2b.Sum256(txBodyCbor) + txHash := h[:] + logger.Info("Prepared tx hash", "tx_hash_hex", hex.EncodeToString(txHash)) + + algorithm := viper.GetString("event_initiator_algorithm") + if algorithm == "" { + algorithm = string(types.EventInitiatorKeyTypeEd25519) + } + if !slices.Contains([]string{string(types.EventInitiatorKeyTypeEd25519), string(types.EventInitiatorKeyTypeP256)}, algorithm) { + logger.Fatal(fmt.Sprintf("invalid algorithm: %s", algorithm), nil) + } + + natsURL := viper.GetString("nats.url") + natsConn, err := nats.Connect(natsURL) + if err != nil { + logger.Fatal("Failed to connect to NATS", err) + } + defer natsConn.Drain() + defer natsConn.Close() + + localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{KeyPath: "./event_initiator.key"}) + if err != nil { + logger.Fatal("Failed to create local signer", err) + } + mpcClient := client.NewMPCClient(client.Options{NatsConn: natsConn, Signer: localSigner}) + + signResultCh := make(chan event.SigningResultEvent, 1) + err = mpcClient.OnSignResult(func(evt event.SigningResultEvent) { + if evt.WalletID == walletID { + signResultCh <- evt + } + }) + if err != nil { + logger.Fatal("OnSignResult subscribe failed", err) + } + + txID := uuid.New().String() + if err := mpcClient.SignTransaction(&types.SignTxMessage{ + KeyType: types.KeyTypeEd25519, + WalletID: walletID, + NetworkInternalCode: "cardano-testnet", + TxID: txID, + Tx: txHash, + }); err != nil { + logger.Fatal("SignTransaction failed", err) + } + logger.Info("SignTransaction sent", "txID", txID) + + select { + case res := <-signResultCh: + if res.ResultType == event.ResultTypeError { + logger.Fatal("Signing failed: "+res.ErrorReason, nil) + } + + signedTxCbor, err := buildSignedTxCBOR(txBodyCbor, rawPub, res.Signature) + if err != nil { + logger.Fatal("buildSignedTxCBOR failed", err) + } + + submitURL := "https://cardano-preprod.blockfrost.io/api/v0/tx/submit" + respBody, status, err := blockfrostPOSTCBOR(context.Background(), bfProjectID, submitURL, signedTxCbor) + if err != nil { + logger.Fatal("submit failed", err) + } + if status < 200 || status >= 300 { + logger.Fatal(fmt.Sprintf("submit HTTP %d: %s", status, prettyJSON(respBody)), nil) + } + var submittedHash string + _ = json.Unmarshal(respBody, &submittedHash) + if submittedHash == "" { + submittedHash = strings.TrimSpace(string(respBody)) + } + fmt.Println(submittedHash) + logger.Info("Submitted tx", "tx_hash", submittedHash) + case <-time.After(60 * time.Second): + logger.Fatal("Timeout waiting for signing result", errors.New("timeout")) + } +} + +func fetchFirstUtxoOnce(ctx context.Context, projectID, addr string) (*simpleUtxo, error) { + url := "https://cardano-preprod.blockfrost.io/api/v0/addresses/" + addr + "/utxos" + b, status, err := blockfrostGET(ctx, projectID, url) + if err != nil { + return nil, err + } + if status == 404 { + return nil, errors.New("address not found on-chain yet (404) - fund it first") + } + if status < 200 || status >= 300 { + return nil, fmt.Errorf("utxo HTTP %d: %s", status, prettyJSON(b)) + } + var utxos []bfUtxo + if err := json.Unmarshal(b, &utxos); err != nil { + return nil, err + } + if len(utxos) == 0 { + return nil, errors.New("no UTxO at address - fund it first") + } + lovelace, err := findLovelace(utxos[0].Amount) + if err != nil { + return nil, err + } + return &simpleUtxo{TxHash: utxos[0].TxHash, TxIndex: uint32(utxos[0].TxIndex), Lovelace: lovelace}, nil +} From 2d8d54394c58ad422bba81a92030a679f7b54f68 Mon Sep 17 00:00:00 2001 From: Woft257 Date: Thu, 25 Dec 2025 00:05:55 +0700 Subject: [PATCH 03/13] feat(cardano_poc): enhance wallet management and transaction signing logic --- examples/cardano_poc/create_wallet.go | 36 ++++++++++++++++-- examples/cardano_poc/lib.go | 8 +++- examples/cardano_poc/sign_tx.go | 55 +++++++++++++++++++-------- 3 files changed, 78 insertions(+), 21 deletions(-) diff --git a/examples/cardano_poc/create_wallet.go b/examples/cardano_poc/create_wallet.go index 7f0eb13..c38208e 100644 --- a/examples/cardano_poc/create_wallet.go +++ b/examples/cardano_poc/create_wallet.go @@ -4,6 +4,7 @@ package main import ( "encoding/hex" + "encoding/json" "errors" "fmt" "os" @@ -86,10 +87,37 @@ func main() { logger.Fatal("derive address failed", err) } - // Save to a local file next to this PoC. - // This keeps it simple and avoids forcing a "reuse" mode. - rec := fmt.Sprintf("{\n \"wallet_id\": \"%s\",\n \"eddsa_pubkey_hex\": \"%s\"\n}\n", walletID, hex.EncodeToString(keygen.EDDSAPubKey)) - if werr := os.WriteFile("examples/cardano_poc/cardano_poc_wallet.json", []byte(rec), 0o644); werr != nil { + // Read existing wallets, add the new one, and write back. + const walletFilePath = "examples/cardano_poc/cardano_poc_wallet.json" + type walletRecord struct { + WalletID string `json:"wallet_id"` + EDDSAPubKeyHex string `json:"eddsa_pubkey_hex"` + DepositAddress string `json:"deposit_address"` + } + + wallets := make(map[string]walletRecord) + b, err := os.ReadFile(walletFilePath) + if err == nil { // if file exists and is readable + if jerr := json.Unmarshal(b, &wallets); jerr != nil { + logger.Fatal("failed to unmarshal existing wallet file", jerr) + } + } else if !os.IsNotExist(err) { // if error is something other than "not found" + logger.Fatal("failed to read wallet file", err) + } + + // Add new wallet + wallets[walletID] = walletRecord{ + WalletID: walletID, + EDDSAPubKeyHex: hex.EncodeToString(keygen.EDDSAPubKey), + DepositAddress: ourAddr, + } + + // Write back to file + recBytes, err := json.MarshalIndent(wallets, "", " ") + if err != nil { + logger.Fatal("failed to marshal wallet file json", err) + } + if werr := os.WriteFile(walletFilePath, recBytes, 0o644); werr != nil { logger.Fatal("failed to write wallet file", werr) } diff --git a/examples/cardano_poc/lib.go b/examples/cardano_poc/lib.go index df7bd42..06872b5 100644 --- a/examples/cardano_poc/lib.go +++ b/examples/cardano_poc/lib.go @@ -113,8 +113,12 @@ func buildTxBodyCBOR(txHashHex string, txIndex uint32, toAddr string, toLovelace return nil, err } out1 := []any{toBytes, toLovelace} - out2 := []any{chgBytes, changeLovelace} - body := map[any]any{0: []any{[]any{txHash, txIndex}}, 1: []any{out1, out2}, 2: feeLovelace} + outs := []any{out1} + if changeLovelace > 0 { + out2 := []any{chgBytes, changeLovelace} + outs = append(outs, out2) + } + body := map[any]any{0: []any{[]any{txHash, txIndex}}, 1: outs, 2: feeLovelace} return cbor.Marshal(body) } diff --git a/examples/cardano_poc/sign_tx.go b/examples/cardano_poc/sign_tx.go index 5c91d6e..d13bdb7 100644 --- a/examples/cardano_poc/sign_tx.go +++ b/examples/cardano_poc/sign_tx.go @@ -33,9 +33,10 @@ import ( // - This file does NOT wait for UTxO. It expects you to fund the deposit address first. // - It reads pubkey from examples/cardano_poc/cardano_poc_wallet.json. -type walletFile struct { +type walletRecord struct { WalletID string `json:"wallet_id"` EDDSAPubKeyHex string `json:"eddsa_pubkey_hex"` + DepositAddress string `json:"deposit_address"` } func main() { @@ -49,7 +50,7 @@ func main() { walletID := "" toAddr := "" - amountAda := uint64(1) + amountAda := float64(1.0) for i := 1; i < len(os.Args); i++ { switch os.Args[i] { case "--wallet-id": @@ -60,24 +61,29 @@ func main() { toAddr = os.Args[i] case "--amount-ada": i++ - _, _ = fmt.Sscanf(os.Args[i], "%d", &amountAda) + _, _ = fmt.Sscanf(os.Args[i], "%f", &amountAda) } } if walletID == "" || toAddr == "" { logger.Fatal("usage: sign_tx.go --wallet-id --to --amount-ada 1", nil) } - b, err := os.ReadFile("examples/cardano_poc/cardano_poc_wallet.json") + const walletFilePath = "examples/cardano_poc/cardano_poc_wallet.json" + b, err := os.ReadFile(walletFilePath) if err != nil { logger.Fatal("failed to read wallet file", err) } - var wf walletFile - if err := json.Unmarshal(b, &wf); err != nil { - logger.Fatal("invalid wallet file json", err) + + wallets := make(map[string]walletRecord) + if err := json.Unmarshal(b, &wallets); err != nil { + logger.Fatal("failed to unmarshal wallets file", err) } - if wf.WalletID != walletID { - logger.Fatal(fmt.Sprintf("wallet mismatch: file has %s", wf.WalletID), nil) + + wf, ok := wallets[walletID] + if !ok { + logger.Fatal(fmt.Sprintf("wallet with ID %s not found in %s", walletID, walletFilePath), nil) } + pubKeyBytes, err := hex.DecodeString(wf.EDDSAPubKeyHex) if err != nil { logger.Fatal("invalid pubkey hex", err) @@ -87,11 +93,8 @@ func main() { if err != nil { logger.Fatal("normalize pubkey failed", err) } - ourAddr, err := deriveEnterpriseAddressPreprod(rawPub) - if err != nil { - logger.Fatal("derive address failed", err) - } - logger.Info("Deposit address (must be funded before signing)", "address", ourAddr) + ourAddr := wf.DepositAddress // Use address from file + logger.Info("Using wallet", "wallet_id", wf.WalletID, "address", ourAddr) // Fetch first UTxO once (NO WAIT/RETRY) utxo, err := fetchFirstUtxoOnce(context.Background(), bfProjectID, ourAddr) @@ -100,12 +103,34 @@ func main() { } logger.Info("Using UTxO", "tx_hash", utxo.TxHash, "tx_index", utxo.TxIndex, "lovelace", utxo.Lovelace) - sendLovelace := amountAda * 1_000_000 + // NOTE: Cardano requires each output to be >= min-UTxO (varies by era/protocol). + // If the computed change would be below a safe threshold, we add it to fee and omit the change output. + // This avoids "BabbageOutputTooSmallUTxO". + const minChangeLovelace = uint64(1_000_000) // 1 ADA safe-ish for ADA-only outputs + + sendLovelace := uint64(amountAda * 1_000_000) feeLovelace := uint64(200_000) if utxo.Lovelace <= sendLovelace+feeLovelace { logger.Fatal("not enough funds", nil) } change := utxo.Lovelace - sendLovelace - feeLovelace + if change > 0 && change < minChangeLovelace { + // This is the "smart wallet" logic. + // Instead of burning small change into fees, we adjust the send amount + // to ensure the change output is valid. + newSendLovelace := utxo.Lovelace - minChangeLovelace - feeLovelace + if newSendLovelace <= 0 { + logger.Fatal("Not enough funds to create a valid change output after sending", nil) + } + logger.Info( + "Send amount adjusted to ensure valid change output", + "original_send", sendLovelace, + "new_send", newSendLovelace, + "change_set_to", minChangeLovelace, + ) + sendLovelace = newSendLovelace + change = minChangeLovelace // Recalculate for consistency, should be minChangeLovelace + } txBodyCbor, err := buildTxBodyCBOR(utxo.TxHash, utxo.TxIndex, toAddr, sendLovelace, ourAddr, change, feeLovelace) if err != nil { From eed8c6ac46d982607d743c7ae3105699d79e4b8b Mon Sep 17 00:00:00 2001 From: Woft257 Date: Thu, 25 Dec 2025 00:16:55 +0700 Subject: [PATCH 04/13] feat(cardano_poc): add protocol parameters fetching for transaction fee calculation --- examples/cardano_poc/lib.go | 21 +++++++++++++++++++++ examples/cardano_poc/sign_tx.go | 10 +++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/examples/cardano_poc/lib.go b/examples/cardano_poc/lib.go index 06872b5..f531895 100644 --- a/examples/cardano_poc/lib.go +++ b/examples/cardano_poc/lib.go @@ -193,3 +193,24 @@ func trimJSONQuotes(s string) string { s = strings.TrimSpace(s) return strings.Trim(s, "\"\n\r\t ") } + +type protocolParams struct { + MinFeeA int `json:"min_fee_a"` + MinFeeB int `json:"min_fee_b"` +} + +func fetchProtocolParams(ctx context.Context, projectID string) (*protocolParams, error) { + url := "https://cardano-preprod.blockfrost.io/api/v0/epochs/latest/parameters" + b, status, err := blockfrostGET(ctx, projectID, url) + if err != nil { + return nil, err + } + if status < 200 || status >= 300 { + return nil, fmt.Errorf("protocol params HTTP %d: %s", status, prettyJSON(b)) + } + var params protocolParams + if err := json.Unmarshal(b, ¶ms); err != nil { + return nil, err + } + return ¶ms, nil +} \ No newline at end of file diff --git a/examples/cardano_poc/sign_tx.go b/examples/cardano_poc/sign_tx.go index d13bdb7..9aac72a 100644 --- a/examples/cardano_poc/sign_tx.go +++ b/examples/cardano_poc/sign_tx.go @@ -47,6 +47,12 @@ func main() { if bfProjectID == "" { logger.Fatal("BLOCKFROST_PROJECT_ID env var is required", nil) } +// Fetch latest protocol params for fee calculation + params, err := fetchProtocolParams(context.Background(), bfProjectID) + if err != nil { + logger.Fatal("failed to fetch protocol params", err) + } + logger.Info("Fetched protocol params", "min_fee_a", params.MinFeeA, "min_fee_b", params.MinFeeB) walletID := "" toAddr := "" @@ -109,7 +115,9 @@ func main() { const minChangeLovelace = uint64(1_000_000) // 1 ADA safe-ish for ADA-only outputs sendLovelace := uint64(amountAda * 1_000_000) - feeLovelace := uint64(200_000) + const estimatedTxSizeBytes = 512 // A safe-ish estimate for a simple tx (1 input, 2 outputs) + feeLovelace := uint64(params.MinFeeA*estimatedTxSizeBytes + params.MinFeeB) + logger.Info("Calculated fee", "fee_lovelace", feeLovelace, "estimated_tx_size_bytes", estimatedTxSizeBytes) if utxo.Lovelace <= sendLovelace+feeLovelace { logger.Fatal("not enough funds", nil) } From ce5574d2c1a15d3d123e65fd51d930d819e42f5e Mon Sep 17 00:00:00 2001 From: Woft257 Date: Thu, 25 Dec 2025 00:36:03 +0700 Subject: [PATCH 05/13] feat(cardano_poc): add configuration file for Blockfrost API and update wallet scripts to use it --- examples/cardano_poc/config.yaml.template | 10 ++++++++++ examples/cardano_poc/create_wallet.go | 2 +- examples/cardano_poc/sign_tx.go | 8 ++++---- 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 examples/cardano_poc/config.yaml.template diff --git a/examples/cardano_poc/config.yaml.template b/examples/cardano_poc/config.yaml.template new file mode 100644 index 0000000..12c6d25 --- /dev/null +++ b/examples/cardano_poc/config.yaml.template @@ -0,0 +1,10 @@ +# Blockfrost API configuration for the Cardano PoC +blockfrost_project_id: "preprod..." # Replace with your actual preprod project ID + +# NATS Jetstream configuration +nats: + url: "nats://localhost:4222" + +# Event initiator key algorithm (ed25519 or p256) +event_initiator_algorithm: "ed25519" + diff --git a/examples/cardano_poc/create_wallet.go b/examples/cardano_poc/create_wallet.go index c38208e..3cbea46 100644 --- a/examples/cardano_poc/create_wallet.go +++ b/examples/cardano_poc/create_wallet.go @@ -30,7 +30,7 @@ import ( func main() { const environment = "development" - config.InitViperConfig("") + config.InitViperConfig("examples/cardano_poc/config.yaml") logger.Init(environment, true) algorithm := viper.GetString("event_initiator_algorithm") diff --git a/examples/cardano_poc/sign_tx.go b/examples/cardano_poc/sign_tx.go index 9aac72a..0c83396 100644 --- a/examples/cardano_poc/sign_tx.go +++ b/examples/cardano_poc/sign_tx.go @@ -41,11 +41,11 @@ type walletRecord struct { func main() { logger.Init("development", true) - config.InitViperConfig("") + config.InitViperConfig("examples/cardano_poc/config.yaml") - bfProjectID := os.Getenv("BLOCKFROST_PROJECT_ID") - if bfProjectID == "" { - logger.Fatal("BLOCKFROST_PROJECT_ID env var is required", nil) + bfProjectID := viper.GetString("blockfrost_project_id") + if bfProjectID == "" || bfProjectID == "preprod..." { + logger.Fatal("blockfrost_project_id is not set in examples/cardano_poc/config.yaml", nil) } // Fetch latest protocol params for fee calculation params, err := fetchProtocolParams(context.Background(), bfProjectID) From bbb7ad074bbab3c0eb59b4df5d611897a22612ed Mon Sep 17 00:00:00 2001 From: Woft257 Date: Thu, 25 Dec 2025 00:54:33 +0700 Subject: [PATCH 06/13] chore(deps): update hashicorp/consul and golang.org/x dependencies to latest versions --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index a821b34..d40a8c1 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/dgraph-io/badger/v4 v4.2.0 github.com/fxamacker/cbor/v2 v2.6.0 github.com/google/uuid v1.6.0 - github.com/hashicorp/consul/api v1.31.0 + github.com/hashicorp/consul/api v1.32.1 github.com/mitchellh/mapstructure v1.5.0 github.com/nats-io/nats.go v1.31.0 github.com/rs/zerolog v1.31.0 @@ -25,7 +25,7 @@ require ( github.com/spf13/viper v1.18.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.3.2 - golang.org/x/crypto v0.26.0 + golang.org/x/crypto v0.37.0 golang.org/x/term v0.31.0 ) @@ -99,7 +99,7 @@ require ( go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.39.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect diff --git a/go.sum b/go.sum index a25e7cd..669b4e2 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/consul/api v1.31.0 h1:32BUNLembeSRek0G/ZAM6WNfdEwYdYo8oQ4+JoqGkNQ= -github.com/hashicorp/consul/api v1.31.0/go.mod h1:2ZGIiXM3A610NmDULmCHd/aqBJj8CkMfOhswhOafxRg= +github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= +github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -426,8 +426,8 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= @@ -463,8 +463,8 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From a6ae13fe02c16ffb4101b84ce47322342489488e Mon Sep 17 00:00:00 2001 From: Woft257 Date: Thu, 25 Dec 2025 01:05:53 +0700 Subject: [PATCH 07/13] docs: update usage instructions for create_wallet and sign_tx examples to include build tags --- examples/cardano_poc/create_wallet.go | 2 +- examples/cardano_poc/sign_tx.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cardano_poc/create_wallet.go b/examples/cardano_poc/create_wallet.go index 3cbea46..a227bdc 100644 --- a/examples/cardano_poc/create_wallet.go +++ b/examples/cardano_poc/create_wallet.go @@ -22,7 +22,7 @@ import ( ) // Usage: -// go run ./examples/cardano_poc/create_wallet.go +// go run -tags=create_wallet ./examples/cardano_poc // Output: // Prints wallet_id and deposit address (enterprise) to stdout. // diff --git a/examples/cardano_poc/sign_tx.go b/examples/cardano_poc/sign_tx.go index 0c83396..51d4522 100644 --- a/examples/cardano_poc/sign_tx.go +++ b/examples/cardano_poc/sign_tx.go @@ -27,7 +27,7 @@ import ( // Usage: // $env:BLOCKFROST_PROJECT_ID="..." -// go run ./examples/cardano_poc/sign_tx.go --wallet-id --to --amount-ada 1 +// go run -tags=sign_tx ./examples/cardano_poc/sign_tx.go --wallet-id --to --amount-ada // // Notes: // - This file does NOT wait for UTxO. It expects you to fund the deposit address first. From cb60aa7c71e1eb207bb274c610016ece1b97ee23 Mon Sep 17 00:00:00 2001 From: Woft257 Date: Fri, 26 Dec 2025 21:46:36 +0700 Subject: [PATCH 08/13] feat(cardano_poc): enhance Blockfrost API configuration and transaction signing logic --- examples/cardano_poc/config.yaml.template | 14 +- examples/cardano_poc/lib.go | 151 +++++++++-- examples/cardano_poc/sign_tx.go | 316 ++++++++++++++-------- 3 files changed, 346 insertions(+), 135 deletions(-) diff --git a/examples/cardano_poc/config.yaml.template b/examples/cardano_poc/config.yaml.template index 12c6d25..266fb74 100644 --- a/examples/cardano_poc/config.yaml.template +++ b/examples/cardano_poc/config.yaml.template @@ -1,5 +1,17 @@ # Blockfrost API configuration for the Cardano PoC -blockfrost_project_id: "preprod..." # Replace with your actual preprod project ID +blockfrost_project_id: "preprod....." # Replace with your actual preprod project ID +# Network / endpoints +# Supported: preprod (default), preview +network: "preprod" + +# Override Blockfrost base URL if needed +# blockfrost_base_url: "https://cardano-preprod.blockfrost.io/api/v0" + +# TTL in seconds (slot length is 1s on these testnets) +ttl_seconds: 3600 + +# PoC safety threshold for change output (ADA-only). +min_change_lovelace: 1000000 # NATS Jetstream configuration nats: diff --git a/examples/cardano_poc/lib.go b/examples/cardano_poc/lib.go index f531895..440cb5d 100644 --- a/examples/cardano_poc/lib.go +++ b/examples/cardano_poc/lib.go @@ -10,12 +10,48 @@ import ( "io" "net/http" "strings" + "time" "github.com/cosmos/btcutil/bech32" "github.com/fxamacker/cbor/v2" + "github.com/spf13/viper" "golang.org/x/crypto/blake2b" ) +type bfConfig struct { + ProjectID string + BaseURL string // e.g. https://cardano-preprod.blockfrost.io/api/v0 + Network string // preprod|preview + TTLSeconds uint64 + MinChangeLov uint64 +} + +func loadBFConfig() bfConfig { + cfg := bfConfig{} + cfg.ProjectID = viper.GetString("blockfrost_project_id") + cfg.Network = viper.GetString("network") + if cfg.Network == "" { + cfg.Network = "preprod" + } + cfg.BaseURL = viper.GetString("blockfrost_base_url") + if cfg.BaseURL == "" { + switch strings.ToLower(cfg.Network) { + case "preview": + cfg.BaseURL = "https://cardano-preview.blockfrost.io/api/v0" + default: + cfg.BaseURL = "https://cardano-preprod.blockfrost.io/api/v0" + } + } + cfg.TTLSeconds = uint64(viper.GetInt("ttl_seconds")) + if cfg.TTLSeconds == 0 { + cfg.TTLSeconds = 3600 + } + cfg.MinChangeLov = uint64(viper.GetInt("min_change_lovelace")) + if cfg.MinChangeLov == 0 { + cfg.MinChangeLov = 1_000_000 + } + return cfg +} // --- shared helpers (used by create_wallet.go and sign_tx.go) --- type bfUtxo struct { @@ -28,9 +64,10 @@ type bfUtxo struct { } type simpleUtxo struct { - TxHash string - TxIndex uint32 - Lovelace uint64 + TxHash string + TxIndex uint32 + Lovelace uint64 + LovelaceOnly bool // PoC flag: true if UTxO contains ONLY lovelace } func normalizeEd25519PubKey(pk []byte) ([]byte, error) { @@ -96,14 +133,12 @@ func decodeCardanoAddressBytes(addrStr string) ([]byte, error) { return data8, nil } -func buildTxBodyCBOR(txHashHex string, txIndex uint32, toAddr string, toLovelace uint64, changeAddr string, changeLovelace uint64, feeLovelace uint64) ([]byte, error) { - txHash, err := hex.DecodeString(txHashHex) - if err != nil { - return nil, err - } - if len(txHash) != 32 { - return nil, fmt.Errorf("tx hash must be 32 bytes, got %d", len(txHash)) - } +type txInput struct { + TxHashHex string + TxIndex uint32 +} + +func buildTxBodyCBOR(inputs []txInput, toAddr string, toLovelace uint64, changeAddr string, changeLovelace uint64, feeLovelace uint64, ttlSlot uint64) ([]byte, error) { toBytes, err := decodeCardanoAddressBytes(toAddr) if err != nil { return nil, err @@ -112,15 +147,73 @@ func buildTxBodyCBOR(txHashHex string, txIndex uint32, toAddr string, toLovelace if err != nil { return nil, err } + + cborInputs := make([]any, 0, len(inputs)) + for _, in := range inputs { + txHash, err := hex.DecodeString(in.TxHashHex) + if err != nil { + return nil, err + } + if len(txHash) != 32 { + return nil, fmt.Errorf("tx hash must be 32 bytes, got %d", len(txHash)) + } + cborInputs = append(cborInputs, []any{txHash, in.TxIndex}) + } + out1 := []any{toBytes, toLovelace} outs := []any{out1} if changeLovelace > 0 { out2 := []any{chgBytes, changeLovelace} outs = append(outs, out2) } - body := map[any]any{0: []any{[]any{txHash, txIndex}}, 1: outs, 2: feeLovelace} + body := map[any]any{0: cborInputs, 1: outs, 2: feeLovelace} + if ttlSlot > 0 { + // 3 = ttl/invalid_hereafter + body[uint64(3)] = ttlSlot + } return cbor.Marshal(body) } +func blockfrostGETWithRetry(ctx context.Context, projectID, url string) ([]byte, int, error) { + // Simple PoC retry for 429/5xx + var lastErr error + for i := 0; i < 4; i++ { + b, status, err := blockfrostGET(ctx, projectID, url) + if err == nil && status >= 200 && status < 300 { + return b, status, nil + } + if err != nil { + lastErr = err + } else { + lastErr = fmt.Errorf("HTTP %d: %s", status, prettyJSON(b)) + // no retry for 4xx except 429 + if status >= 400 && status < 500 && status != 429 { + return b, status, lastErr + } + } + time.Sleep(time.Duration(250*(i+1)) * time.Millisecond) + } + return nil, 0, lastErr +} + +func blockfrostPOSTCBORWithRetry(ctx context.Context, projectID, url string, cborBody []byte) ([]byte, int, error) { + var lastErr error + for i := 0; i < 3; i++ { + b, status, err := blockfrostPOSTCBOR(ctx, projectID, url, cborBody) + if err == nil && status >= 200 && status < 300 { + return b, status, nil + } + if err != nil { + lastErr = err + } else { + lastErr = fmt.Errorf("HTTP %d: %s", status, prettyJSON(b)) + if status >= 400 && status < 500 && status != 429 { + return b, status, lastErr + } + } + time.Sleep(time.Duration(300*(i+1)) * time.Millisecond) + } + return nil, 0, lastErr +} func buildSignedTxCBOR(txBodyCbor []byte, pubKey []byte, sig []byte) ([]byte, error) { if len(sig) == 0 { @@ -195,22 +288,46 @@ func trimJSONQuotes(s string) string { } type protocolParams struct { - MinFeeA int `json:"min_fee_a"` - MinFeeB int `json:"min_fee_b"` + MinFeeA int `json:"min_fee_a"` + MinFeeB int `json:"min_fee_b"` + Epoch int `json:"epoch"` + Slot int `json:"slot"` } -func fetchProtocolParams(ctx context.Context, projectID string) (*protocolParams, error) { - url := "https://cardano-preprod.blockfrost.io/api/v0/epochs/latest/parameters" - b, status, err := blockfrostGET(ctx, projectID, url) +func fetchProtocolParams(ctx context.Context, cfg bfConfig) (*protocolParams, error) { + url := cfg.BaseURL + "/epochs/latest/parameters" + b, status, err := blockfrostGET(ctx, cfg.ProjectID, url) if err != nil { return nil, err } if status < 200 || status >= 300 { return nil, fmt.Errorf("protocol params HTTP %d: %s", status, prettyJSON(b)) } + var params protocolParams if err := json.Unmarshal(b, ¶ms); err != nil { return nil, err } return ¶ms, nil +} +// fetchCurrentSlot returns the latest known slot number from Blockfrost. +func fetchCurrentSlot(ctx context.Context, cfg bfConfig) (uint64, error) { + url := cfg.BaseURL + "/blocks/latest" + b, status, err := blockfrostGET(ctx, cfg.ProjectID, url) + if err != nil { + return 0, err + } + if status < 200 || status >= 300 { + return 0, fmt.Errorf("blocks/latest HTTP %d: %s", status, prettyJSON(b)) + } + var resp struct { + Slot uint64 `json:"slot"` + } + if err := json.Unmarshal(b, &resp); err != nil { + return 0, err + } + if resp.Slot == 0 { + return 0, errors.New("blockfrost returned slot=0") + } + return resp.Slot, nil } \ No newline at end of file diff --git a/examples/cardano_poc/sign_tx.go b/examples/cardano_poc/sign_tx.go index 51d4522..23118b5 100644 --- a/examples/cardano_poc/sign_tx.go +++ b/examples/cardano_poc/sign_tx.go @@ -9,7 +9,7 @@ import ( "errors" "fmt" "os" - "slices" + "sort" "strings" "time" @@ -26,12 +26,11 @@ import ( ) // Usage: -// $env:BLOCKFROST_PROJECT_ID="..." -// go run -tags=sign_tx ./examples/cardano_poc/sign_tx.go --wallet-id --to --amount-ada +// go run -tags=sign_tx ./examples/cardano_poc --wallet-id --to --amount-ada // // Notes: // - This file does NOT wait for UTxO. It expects you to fund the deposit address first. -// - It reads pubkey from examples/cardano_poc/cardano_poc_wallet.json. +// - It reads pubkey/address from examples/cardano_poc/cardano_poc_wallet.json. type walletRecord struct { WalletID string `json:"wallet_id"` @@ -39,24 +38,81 @@ type walletRecord struct { DepositAddress string `json:"deposit_address"` } + + func main() { logger.Init("development", true) config.InitViperConfig("examples/cardano_poc/config.yaml") - bfProjectID := viper.GetString("blockfrost_project_id") - if bfProjectID == "" || bfProjectID == "preprod..." { + bfCfg := loadBFConfig() + if bfCfg.ProjectID == "" { logger.Fatal("blockfrost_project_id is not set in examples/cardano_poc/config.yaml", nil) } -// Fetch latest protocol params for fee calculation - params, err := fetchProtocolParams(context.Background(), bfProjectID) + + params, err := fetchProtocolParams(context.Background(), bfCfg) if err != nil { logger.Fatal("failed to fetch protocol params", err) } - logger.Info("Fetched protocol params", "min_fee_a", params.MinFeeA, "min_fee_b", params.MinFeeB) + logger.Info("Fetched protocol params", "min_fee_a", params.MinFeeA, "min_fee_b", params.MinFeeB, "network", bfCfg.Network, "base_url", bfCfg.BaseURL) + + walletID, toAddr, amountAda := parseArgsOrFatal() + wf, rawPub, ourAddr := loadWalletOrFatal(walletID) + logger.Info("Using wallet", "wallet_id", wf.WalletID, "address", ourAddr) + + minChangeLovelace := bfCfg.MinChangeLov + sendLovelace := uint64(amountAda * 1_000_000) + // Fail fast: ADA-only outputs must be >= min-UTxO. We use min_change_lovelace as PoC guard. + if sendLovelace < minChangeLovelace { + logger.Fatal(fmt.Sprintf("amount too small: send_lovelace=%d < min_change_lovelace=%d (increase --amount-ada)", sendLovelace, minChangeLovelace), nil) + } + + algorithm := viper.GetString("event_initiator_algorithm") + if algorithm == "" { + algorithm = string(types.EventInitiatorKeyTypeEd25519) + } + + natsURL := viper.GetString("nats.url") + natsConn, err := nats.Connect(natsURL) + if err != nil { + logger.Fatal("Failed to connect to NATS", err) + } + defer natsConn.Drain() + defer natsConn.Close() + + localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{KeyPath: "./event_initiator.key"}) + if err != nil { + logger.Fatal("Failed to create local signer", err) + } + mpcClient := client.NewMPCClient(client.Options{NatsConn: natsConn, Signer: localSigner}) + + signResultCh := make(chan event.SigningResultEvent, 1) + if err := mpcClient.OnSignResult(func(evt event.SigningResultEvent) { + if evt.WalletID == walletID { + signResultCh <- evt + } + }); err != nil { + logger.Fatal("OnSignResult subscribe failed", err) + } - walletID := "" - toAddr := "" - amountAda := float64(1.0) + submitURL := bfCfg.BaseURL + "/tx/submit" + maxAttempts := 2 + for attempt := 1; attempt <= maxAttempts; attempt++ { + submittedHash, retryableErr, err := buildSignSubmitOnce(context.Background(), bfCfg, params, mpcClient, signResultCh, submitURL, walletID, rawPub, ourAddr, toAddr, sendLovelace, minChangeLovelace) + if err == nil { + fmt.Println(submittedHash) + fmt.Println("explorer:", "https://preprod.cardanoscan.io/transaction/"+submittedHash) + return + } + logger.Info("Submit attempt failed", "attempt", attempt, "retryable", retryableErr, "error", err.Error()) + if !retryableErr || attempt == maxAttempts { + logger.Fatal("submit failed", err) + } + logger.Info("Rebuilding tx after failure", "next_attempt", attempt+1) + } +} + +func parseArgsOrFatal() (walletID string, toAddr string, amountAda float64) { + amountAda = 1.0 for i := 1; i < len(os.Args); i++ { switch os.Args[i] { case "--wallet-id": @@ -73,113 +129,132 @@ func main() { if walletID == "" || toAddr == "" { logger.Fatal("usage: sign_tx.go --wallet-id --to --amount-ada 1", nil) } + return +} +func loadWalletOrFatal(walletID string) (walletRecord, []byte, string) { const walletFilePath = "examples/cardano_poc/cardano_poc_wallet.json" b, err := os.ReadFile(walletFilePath) if err != nil { logger.Fatal("failed to read wallet file", err) } - wallets := make(map[string]walletRecord) if err := json.Unmarshal(b, &wallets); err != nil { logger.Fatal("failed to unmarshal wallets file", err) } - wf, ok := wallets[walletID] if !ok { logger.Fatal(fmt.Sprintf("wallet with ID %s not found in %s", walletID, walletFilePath), nil) } - pubKeyBytes, err := hex.DecodeString(wf.EDDSAPubKeyHex) if err != nil { logger.Fatal("invalid pubkey hex", err) } - rawPub, err := normalizeEd25519PubKey(pubKeyBytes) if err != nil { logger.Fatal("normalize pubkey failed", err) } - ourAddr := wf.DepositAddress // Use address from file - logger.Info("Using wallet", "wallet_id", wf.WalletID, "address", ourAddr) + return wf, rawPub, wf.DepositAddress +} - // Fetch first UTxO once (NO WAIT/RETRY) - utxo, err := fetchFirstUtxoOnce(context.Background(), bfProjectID, ourAddr) +func buildSignSubmitOnce( + ctx context.Context, + bfCfg bfConfig, + params *protocolParams, + mpcClient client.MPCClient, + signResultCh <-chan event.SigningResultEvent, + submitURL string, + walletID string, + rawPub []byte, + ourAddr string, + toAddr string, + sendLovelace uint64, + minChangeLovelace uint64, +) (submittedHash string, retryable bool, err error) { + // 1) Fetch UTxOs + utxos, err := fetchAllUtxosOnce(ctx, bfCfg, ourAddr) if err != nil { - logger.Fatal("fetch utxo failed", err) + return "", true, err } - logger.Info("Using UTxO", "tx_hash", utxo.TxHash, "tx_index", utxo.TxIndex, "lovelace", utxo.Lovelace) - - // NOTE: Cardano requires each output to be >= min-UTxO (varies by era/protocol). - // If the computed change would be below a safe threshold, we add it to fee and omit the change output. - // This avoids "BabbageOutputTooSmallUTxO". - const minChangeLovelace = uint64(1_000_000) // 1 ADA safe-ish for ADA-only outputs - - sendLovelace := uint64(amountAda * 1_000_000) - const estimatedTxSizeBytes = 512 // A safe-ish estimate for a simple tx (1 input, 2 outputs) - feeLovelace := uint64(params.MinFeeA*estimatedTxSizeBytes + params.MinFeeB) - logger.Info("Calculated fee", "fee_lovelace", feeLovelace, "estimated_tx_size_bytes", estimatedTxSizeBytes) - if utxo.Lovelace <= sendLovelace+feeLovelace { - logger.Fatal("not enough funds", nil) - } - change := utxo.Lovelace - sendLovelace - feeLovelace - if change > 0 && change < minChangeLovelace { - // This is the "smart wallet" logic. - // Instead of burning small change into fees, we adjust the send amount - // to ensure the change output is valid. - newSendLovelace := utxo.Lovelace - minChangeLovelace - feeLovelace - if newSendLovelace <= 0 { - logger.Fatal("Not enough funds to create a valid change output after sending", nil) + filtered := make([]simpleUtxo, 0, len(utxos)) + for _, u := range utxos { + if u.LovelaceOnly { + filtered = append(filtered, u) } - logger.Info( - "Send amount adjusted to ensure valid change output", - "original_send", sendLovelace, - "new_send", newSendLovelace, - "change_set_to", minChangeLovelace, - ) - sendLovelace = newSendLovelace - change = minChangeLovelace // Recalculate for consistency, should be minChangeLovelace } + if len(filtered) == 0 { + return "", false, errors.New("no ADA-only UTxO at address") + } + sort.Slice(filtered, func(i, j int) bool { return filtered[i].Lovelace > filtered[j].Lovelace }) - txBodyCbor, err := buildTxBodyCBOR(utxo.TxHash, utxo.TxIndex, toAddr, sendLovelace, ourAddr, change, feeLovelace) + // 2) TTL + currentSlot, err := fetchCurrentSlot(ctx, bfCfg) if err != nil { - logger.Fatal("buildTxBodyCBOR failed", err) + return "", true, err } - h := blake2b.Sum256(txBodyCbor) - txHash := h[:] - logger.Info("Prepared tx hash", "tx_hash_hex", hex.EncodeToString(txHash)) + ttlSlot := currentSlot + bfCfg.TTLSeconds + logger.Info("Using TTL", "current_slot", currentSlot, "ttl_slot", ttlSlot) - algorithm := viper.GetString("event_initiator_algorithm") - if algorithm == "" { - algorithm = string(types.EventInitiatorKeyTypeEd25519) + // 3) Coin selection (simple) + const feeUpperBound uint64 = 600_000 + target := sendLovelace + feeUpperBound + minChangeLovelace + var total uint64 + inputs := make([]txInput, 0, len(filtered)) + for _, u := range filtered { + total += u.Lovelace + inputs = append(inputs, txInput{TxHashHex: u.TxHash, TxIndex: u.TxIndex}) + if total >= target { + break + } } - if !slices.Contains([]string{string(types.EventInitiatorKeyTypeEd25519), string(types.EventInitiatorKeyTypeP256)}, algorithm) { - logger.Fatal(fmt.Sprintf("invalid algorithm: %s", algorithm), nil) + logger.Info("Selected UTxOs", "selected", len(inputs), "total_lovelace", total, "target_lovelace", target) + if total < sendLovelace+minChangeLovelace { + return "", false, errors.New("not enough funds") } - natsURL := viper.GetString("nats.url") - natsConn, err := nats.Connect(natsURL) - if err != nil { - logger.Fatal("Failed to connect to NATS", err) + // 4) Fee converge with dummy witness + dummySig := make([]byte, 64) + var feeLovelace uint64 + var change uint64 + var txBodyCbor []byte + for iter := 0; iter < 3; iter++ { + if total <= sendLovelace+feeLovelace { + return "", false, errors.New("not enough funds after fee") + } + change = total - sendLovelace - feeLovelace + if change > 0 && change < minChangeLovelace { + // omit change output by folding into fee + feeLovelace += change + change = 0 + } + body, err := buildTxBodyCBOR(inputs, toAddr, sendLovelace, ourAddr, change, feeLovelace, ttlSlot) + if err != nil { + return "", false, err + } + dummySigned, err := buildSignedTxCBOR(body, rawPub, dummySig) + if err != nil { + return "", false, err + } + size := len(dummySigned) + newFee := uint64(params.MinFeeA*size + params.MinFeeB) + logger.Info("Calculated fee", "iter", iter, "fee_lovelace", newFee, "signed_tx_size_bytes", size) + if newFee == feeLovelace { + txBodyCbor = body + break + } + feeLovelace = newFee + txBodyCbor = body } - defer natsConn.Drain() - defer natsConn.Close() - - localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{KeyPath: "./event_initiator.key"}) - if err != nil { - logger.Fatal("Failed to create local signer", err) + if txBodyCbor == nil { + return "", false, errors.New("failed to build tx body") } - mpcClient := client.NewMPCClient(client.Options{NatsConn: natsConn, Signer: localSigner}) - signResultCh := make(chan event.SigningResultEvent, 1) - err = mpcClient.OnSignResult(func(evt event.SigningResultEvent) { - if evt.WalletID == walletID { - signResultCh <- evt - } - }) - if err != nil { - logger.Fatal("OnSignResult subscribe failed", err) - } + // 5) Tx hash + h := blake2b.Sum256(txBodyCbor) + txHash := h[:] + logger.Info("Prepared tx hash", "tx_hash_hex", hex.EncodeToString(txHash)) + // 6) Sign txID := uuid.New().String() if err := mpcClient.SignTransaction(&types.SignTxMessage{ KeyType: types.KeyTypeEd25519, @@ -188,44 +263,45 @@ func main() { TxID: txID, Tx: txHash, }); err != nil { - logger.Fatal("SignTransaction failed", err) + return "", true, err } logger.Info("SignTransaction sent", "txID", txID) + var res event.SigningResultEvent select { - case res := <-signResultCh: + case res = <-signResultCh: if res.ResultType == event.ResultTypeError { - logger.Fatal("Signing failed: "+res.ErrorReason, nil) - } - - signedTxCbor, err := buildSignedTxCBOR(txBodyCbor, rawPub, res.Signature) - if err != nil { - logger.Fatal("buildSignedTxCBOR failed", err) - } - - submitURL := "https://cardano-preprod.blockfrost.io/api/v0/tx/submit" - respBody, status, err := blockfrostPOSTCBOR(context.Background(), bfProjectID, submitURL, signedTxCbor) - if err != nil { - logger.Fatal("submit failed", err) - } - if status < 200 || status >= 300 { - logger.Fatal(fmt.Sprintf("submit HTTP %d: %s", status, prettyJSON(respBody)), nil) + return "", false, errors.New("signing failed: "+res.ErrorReason) } - var submittedHash string - _ = json.Unmarshal(respBody, &submittedHash) - if submittedHash == "" { - submittedHash = strings.TrimSpace(string(respBody)) - } - fmt.Println(submittedHash) - logger.Info("Submitted tx", "tx_hash", submittedHash) case <-time.After(60 * time.Second): - logger.Fatal("Timeout waiting for signing result", errors.New("timeout")) + return "", true, errors.New("timeout waiting for signing result") } + + // 7) Submit + signedTxCbor, err := buildSignedTxCBOR(txBodyCbor, rawPub, res.Signature) + if err != nil { + return "", false, err + } + respBody, status, err := blockfrostPOSTCBORWithRetry(ctx, bfCfg.ProjectID, submitURL, signedTxCbor) + if err != nil { + return "", true, err + } + if status < 200 || status >= 300 { + msg := prettyJSON(respBody) + retryable = strings.Contains(msg, "BadInputsUTxO") || strings.Contains(msg, "InvalidWitnessesUTXOW") || strings.Contains(msg, "ValueNotConservedUTxO") || strings.Contains(msg, "OutsideValidityIntervalUTxO") + return "", retryable, fmt.Errorf("submit HTTP %d: %s", status, msg) + } + _ = json.Unmarshal(respBody, &submittedHash) + if submittedHash == "" { + submittedHash = strings.TrimSpace(string(respBody)) + } + return submittedHash, false, nil } -func fetchFirstUtxoOnce(ctx context.Context, projectID, addr string) (*simpleUtxo, error) { - url := "https://cardano-preprod.blockfrost.io/api/v0/addresses/" + addr + "/utxos" - b, status, err := blockfrostGET(ctx, projectID, url) + +func fetchAllUtxosOnce(ctx context.Context, cfg bfConfig, addr string) ([]simpleUtxo, error) { + url := cfg.BaseURL + "/addresses/" + addr + "/utxos" + b, status, err := blockfrostGETWithRetry(ctx, cfg.ProjectID, url) if err != nil { return nil, err } @@ -235,16 +311,22 @@ func fetchFirstUtxoOnce(ctx context.Context, projectID, addr string) (*simpleUtx if status < 200 || status >= 300 { return nil, fmt.Errorf("utxo HTTP %d: %s", status, prettyJSON(b)) } - var utxos []bfUtxo - if err := json.Unmarshal(b, &utxos); err != nil { + var raw []bfUtxo + if err := json.Unmarshal(b, &raw); err != nil { return nil, err } - if len(utxos) == 0 { + if len(raw) == 0 { return nil, errors.New("no UTxO at address - fund it first") } - lovelace, err := findLovelace(utxos[0].Amount) - if err != nil { - return nil, err + + out := make([]simpleUtxo, 0, len(raw)) + for _, u := range raw { + lovelace, err := findLovelace(u.Amount) + if err != nil { + return nil, err + } + lovelaceOnly := len(u.Amount) == 1 + out = append(out, simpleUtxo{TxHash: u.TxHash, TxIndex: uint32(u.TxIndex), Lovelace: lovelace, LovelaceOnly: lovelaceOnly}) } - return &simpleUtxo{TxHash: utxos[0].TxHash, TxIndex: uint32(utxos[0].TxIndex), Lovelace: lovelace}, nil -} + return out, nil +} \ No newline at end of file From 1f0dce68eedaca1e21894b82ddd3779de56bf861 Mon Sep 17 00:00:00 2001 From: Woft257 Date: Fri, 2 Jan 2026 20:44:27 +0700 Subject: [PATCH 09/13] feat(cardano_poc): implement multi-asset transaction handling and fallback logic for ADA-only UTxOs --- examples/cardano_poc/lib.go | 340 +++++++++++++++++++- examples/cardano_poc/sign_tx.go | 172 ++++++++++- examples/cardano_poc/sign_tx_token.go | 428 ++++++++++++++++++++++++++ 3 files changed, 932 insertions(+), 8 deletions(-) create mode 100644 examples/cardano_poc/sign_tx_token.go diff --git a/examples/cardano_poc/lib.go b/examples/cardano_poc/lib.go index 440cb5d..9297530 100644 --- a/examples/cardano_poc/lib.go +++ b/examples/cardano_poc/lib.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "time" @@ -64,10 +65,11 @@ type bfUtxo struct { } type simpleUtxo struct { - TxHash string - TxIndex uint32 - Lovelace uint64 + TxHash string + TxIndex uint32 + Lovelace uint64 LovelaceOnly bool // PoC flag: true if UTxO contains ONLY lovelace + Assets map[string]uint64 // unit => quantity; unit is "lovelace" or policy+asset hex } func normalizeEd25519PubKey(pk []byte) ([]byte, error) { @@ -242,6 +244,63 @@ func findLovelace(amts []struct { return 0, errors.New("no lovelace in utxo") } +func assetUnitFromPolicyAndName(policyHex, nameHex string) (string, error) { + policyHex = strings.TrimPrefix(policyHex, "0x") + nameHex = strings.TrimPrefix(nameHex, "0x") + if len(policyHex) != 56 { + return "", fmt.Errorf("policy_id_hex must be 56 hex chars, got %d", len(policyHex)) + } + if _, err := hex.DecodeString(policyHex); err != nil { + return "", fmt.Errorf("invalid policy_id_hex: %w", err) + } + if nameHex != "" { + if _, err := hex.DecodeString(nameHex); err != nil { + return "", fmt.Errorf("invalid asset_name_hex: %w", err) + } + } + return policyHex + nameHex, nil +} + +func sumAssetsMaps(dst map[string]uint64, src map[string]uint64) { + for unit, q := range src { + dst[unit] += q + } +} + +func subAssetsMaps(dst map[string]uint64, src map[string]uint64) error { + for unit, q := range src { + cur := dst[unit] + if cur < q { + return fmt.Errorf("insufficient asset %s: have %d need %d", unit, cur, q) + } + if cur == q { + delete(dst, unit) + } else { + dst[unit] = cur - q + } + } + return nil +} + +func assetsMapToCardanoAssets(m map[string]uint64) ([]cardanoAsset, error) { + out := make([]cardanoAsset, 0) + for unit, q := range m { + if unit == "lovelace" { + continue + } + if q == 0 { + continue + } + if len(unit) < 56 { + return nil, fmt.Errorf("invalid asset unit (too short): %s", unit) + } + policy := unit[:56] + name := unit[56:] + out = append(out, cardanoAsset{PolicyIDHex: policy, AssetNameHex: name, Quantity: q}) + } + return out, nil +} + func blockfrostGET(ctx context.Context, projectID, url string) ([]byte, int, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -288,10 +347,11 @@ func trimJSONQuotes(s string) string { } type protocolParams struct { - MinFeeA int `json:"min_fee_a"` - MinFeeB int `json:"min_fee_b"` - Epoch int `json:"epoch"` - Slot int `json:"slot"` + MinFeeA int `json:"min_fee_a"` + MinFeeB int `json:"min_fee_b"` + CoinsPerUTXOWord string `json:"coins_per_utxo_word"` // Sửa từ int sang string + Epoch int `json:"epoch"` + Slot int `json:"slot"` } func fetchProtocolParams(ctx context.Context, cfg bfConfig) (*protocolParams, error) { @@ -330,4 +390,270 @@ func fetchCurrentSlot(ctx context.Context, cfg bfConfig) (uint64, error) { return 0, errors.New("blockfrost returned slot=0") } return resp.Slot, nil +} +// --- Cardano multi-asset (token) helpers for PoC --- + +// cardanoAmount represents an amount in a tx output. +// - For ADA-only: uint64 lovelace +// - For multi-asset: [lovelace, {policyIdBytes: {assetNameBytes: quantity}}] +// CBOR map keys must be raw bytes (policy id 28 bytes, asset name 0..32 bytes). +// This is a simplified model sufficient for common native assets. + +type cardanoAsset struct { + PolicyIDHex string // 56 hex chars => 28 bytes + AssetNameHex string // hex-encoded asset name bytes (can be empty) + Quantity uint64 +} + +func buildTxBodyCBORMultiAsset( + inputs []txInput, + toAddr string, + toLovelace uint64, + toAssets []cardanoAsset, + changeAddr string, + changeLovelace uint64, + changeAssets []cardanoAsset, + feeLovelace uint64, + ttlSlot uint64, +) ([]byte, error) { + toBytes, err := decodeCardanoAddressBytes(toAddr) + if err != nil { + return nil, err + } + chgBytes, err := decodeCardanoAddressBytes(changeAddr) + if err != nil { + return nil, err + } + + cborInputs := make([]any, 0, len(inputs)) + for _, in := range inputs { + txHash, err := hex.DecodeString(in.TxHashHex) + if err != nil { + return nil, err + } + if len(txHash) != 32 { + return nil, fmt.Errorf("tx hash must be 32 bytes, got %d", len(txHash)) + } + cborInputs = append(cborInputs, []any{txHash, in.TxIndex}) + } + + mkAmount := func(lovelace uint64, assets []cardanoAsset) (any, error) { + if len(assets) == 0 { + return lovelace, nil + } + + // IMPORTANT: + // - Go map keys cannot be slices ([]byte) + // - Cardano multi-asset CBOR requires keys to be byte strings (policy_id bytes, asset_name bytes) + // We use hex strings as intermediate keys, then build a CBOR-ready structure + // using cbor.ByteString as map keys (hashable), avoiding "unhashable []uint8" panics. + policies := make(map[string]map[string]uint64) + + for _, a := range assets { + if a.Quantity == 0 { + continue + } + policyHex := strings.TrimPrefix(a.PolicyIDHex, "0x") + assetHex := strings.TrimPrefix(a.AssetNameHex, "0x") + + if len(policyHex) != 56 { + return nil, fmt.Errorf("policy_id_hex must be 56 hex chars (28 bytes), got %d", len(policyHex)) + } + if _, err := hex.DecodeString(policyHex); err != nil { + return nil, fmt.Errorf("invalid policy_id_hex: %w", err) + } + if assetHex != "" { + if _, err := hex.DecodeString(assetHex); err != nil { + return nil, fmt.Errorf("invalid asset_name_hex: %w", err) + } + } + + inner, ok := policies[policyHex] + if !ok { + inner = make(map[string]uint64) + policies[policyHex] = inner + } + inner[assetHex] += a.Quantity + } + + cborPolicies := make(map[any]any) + for policyHex, assetsByName := range policies { + pidBytes, _ := hex.DecodeString(policyHex) + cborInner := make(map[any]any) + for assetHex, qty := range assetsByName { + nameBytes := []byte{} + if assetHex != "" { + nameBytes, _ = hex.DecodeString(assetHex) + } + cborInner[cbor.ByteString(nameBytes)] = qty + } + cborPolicies[cbor.ByteString(pidBytes)] = cborInner + } + + return []any{lovelace, cborPolicies}, nil + } + + toAmt, err := mkAmount(toLovelace, toAssets) + if err != nil { + return nil, err + } + out1 := []any{toBytes, toAmt} + outs := []any{out1} + if changeLovelace > 0 || len(changeAssets) > 0 { + chgAmt, err := mkAmount(changeLovelace, changeAssets) + if err != nil { + return nil, err + } + out2 := []any{chgBytes, chgAmt} + outs = append(outs, out2) + } + + body := map[any]any{0: cborInputs, 1: outs, 2: feeLovelace} + if ttlSlot > 0 { + body[uint64(3)] = ttlSlot + } + return cbor.Marshal(body) +} + +func buildOutputAmount(lovelace uint64, assets []cardanoAsset) (any, error) { + if len(assets) == 0 { + return lovelace, nil + } + + policies := make(map[string]map[string]uint64) + for _, a := range assets { + if a.Quantity == 0 { + continue + } + policyHex := strings.TrimPrefix(a.PolicyIDHex, "0x") + assetHex := strings.TrimPrefix(a.AssetNameHex, "0x") + inner, ok := policies[policyHex] + if !ok { + inner = make(map[string]uint64) + policies[policyHex] = inner + } + inner[assetHex] += a.Quantity + } + + cborPolicies := make(map[any]any) + for policyHex, assetsByName := range policies { + pidBytes, _ := hex.DecodeString(policyHex) + cborInner := make(map[any]any) + for assetHex, qty := range assetsByName { + nameBytes := []byte{} + if assetHex != "" { + nameBytes, _ = hex.DecodeString(assetHex) + } + cborInner[cbor.ByteString(nameBytes)] = qty + } + cborPolicies[cbor.ByteString(pidBytes)] = cborInner + } + + return []any{lovelace, cborPolicies}, nil +} + +func estimateMinAdaForOutput(params *protocolParams, addrBech32 string, lovelace uint64, assets []cardanoAsset) (uint64, error) { + const ( + fallback uint64 = 2_000_000 + minAdaWithToken uint64 = 1_200_000 // Tối thiểu 1.2 ADA khi có token + ) + + if len(assets) > 0 { + // Nếu có token, trả về ít nhất 1.2 ADA + return minAdaWithToken, nil + } + + // Phần còn lại giữ nguyên cho trường hợp không có token + coinsPerUTXOWord := 0 + if params != nil { + coinsPerUTXOWord, _ = strconv.Atoi(strings.TrimSpace(params.CoinsPerUTXOWord)) + } + if coinsPerUTXOWord == 0 { + return fallback, nil + } + + addrBytes, err := decodeCardanoAddressBytes(addrBech32) + if err != nil { + return fallback, nil + } + + output := []any{addrBytes, lovelace} + ser, err := cbor.Marshal(output) + if err != nil { + return fallback, nil + } + + minAda := (160 + uint64(len(ser))) * uint64(coinsPerUTXOWord) / 8 + if minAda == 0 { + return fallback, nil + } + return minAda, nil +} + +func parseCardanoAssetArg(s string) (cardanoAsset, error) { + var out cardanoAsset + s = strings.TrimSpace(s) + if s == "" { + return out, errors.New("empty asset spec") + } + + parts := strings.Split(s, ":") + if len(parts) != 2 { + return out, fmt.Errorf("invalid asset spec (missing ':'): %s", s) + } + + idPart := strings.TrimSpace(parts[0]) + qtyPart := strings.TrimSpace(parts[1]) + + var qty uint64 + if _, err := fmt.Sscanf(qtyPart, "%d", &qty); err != nil { + return out, fmt.Errorf("invalid quantity: %w", err) + } + + policy := idPart + var assetName string + + if strings.Contains(idPart, ".") { + sp := strings.SplitN(idPart, ".", 2) + policy = sp[0] + assetName = sp[1] + + // Convert ASCII to hex if needed + if assetName != "" && !isHex(assetName) { + assetName = hex.EncodeToString([]byte(assetName)) + } + } + + out.PolicyIDHex = policy + out.AssetNameHex = assetName + out.Quantity = qty + + return out, nil +} + +// Add this helper function in lib.go +func isHex(s string) bool { + for _, c := range s { + if !((c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F')) { + return false + } + } + return len(s) > 0 +} + +func parseAmountToAssetsMap(amount []struct { + Unit string `json:"unit"` + Quantity string `json:"quantity"` +}) (map[string]uint64, error) { + assets := make(map[string]uint64) + for _, a := range amount { + qty, err := strconv.ParseUint(a.Quantity, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid quantity for asset %s: %v", a.Unit, err) + } + assets[a.Unit] = qty + } + return assets, nil } \ No newline at end of file diff --git a/examples/cardano_poc/sign_tx.go b/examples/cardano_poc/sign_tx.go index 23118b5..4b5a604 100644 --- a/examples/cardano_poc/sign_tx.go +++ b/examples/cardano_poc/sign_tx.go @@ -98,6 +98,10 @@ func main() { maxAttempts := 2 for attempt := 1; attempt <= maxAttempts; attempt++ { submittedHash, retryableErr, err := buildSignSubmitOnce(context.Background(), bfCfg, params, mpcClient, signResultCh, submitURL, walletID, rawPub, ourAddr, toAddr, sendLovelace, minChangeLovelace) + if err != nil && strings.Contains(err.Error(), "no ADA-only UTxO") { + // Fallback: wallet may only have multi-asset UTxOs; preserve tokens in change. + submittedHash, retryableErr, err = buildSignSubmitMultiAssetAdaFallback(context.Background(), bfCfg, params, mpcClient, signResultCh, submitURL, walletID, rawPub, ourAddr, toAddr, sendLovelace, minChangeLovelace) + } if err == nil { fmt.Println(submittedHash) fmt.Println("explorer:", "https://preprod.cardanoscan.io/transaction/"+submittedHash) @@ -176,6 +180,11 @@ func buildSignSubmitOnce( if err != nil { return "", true, err } + if len(utxos) == 0 { + return "", false, errors.New("no UTxO at address") + } + // For ADA-only send, we must NOT spend UTxOs that also carry tokens. + // If we do, we'd need to reproduce those tokens in outputs; otherwise ValueNotConservedUTxO. filtered := make([]simpleUtxo, 0, len(utxos)) for _, u := range utxos { if u.LovelaceOnly { @@ -297,6 +306,163 @@ func buildSignSubmitOnce( } return submittedHash, false, nil } +// buildSignSubmitMultiAssetAdaFallback builds an ADA-send tx but allows selecting multi-asset UTxOs. +// Any non-lovelace assets present in the selected inputs are returned back to ourAddr as change, +// so ValueNotConservedUTxO is satisfied. +func buildSignSubmitMultiAssetAdaFallback( + ctx context.Context, + bfCfg bfConfig, + params *protocolParams, + mpcClient client.MPCClient, + signResultCh <-chan event.SigningResultEvent, + submitURL string, + walletID string, + rawPub []byte, + ourAddr string, + toAddr string, + sendLovelace uint64, + minChangeLovelace uint64, +) (submittedHash string, retryable bool, err error) { + utxos, err := fetchAllUtxosOnce(ctx, bfCfg, ourAddr) + if err != nil { + return "", true, err + } + if len(utxos) == 0 { + return "", false, errors.New("no UTxO at address") + } + + currentSlot, err := fetchCurrentSlot(ctx, bfCfg) + if err != nil { + return "", true, err + } + ttlSlot := currentSlot + bfCfg.TTLSeconds + logger.Info("Using TTL", "current_slot", currentSlot, "ttl_slot", ttlSlot) + + // coin selection over total lovelace (can include token-carrying utxos) + const feeUpperBound uint64 = 900_000 + target := sendLovelace + feeUpperBound + minChangeLovelace + sort.Slice(utxos, func(i, j int) bool { return utxos[i].Lovelace > utxos[j].Lovelace }) + + inputs := make([]txInput, 0) + totalInputAssets := make(map[string]uint64) + for _, u := range utxos { + inputs = append(inputs, txInput{TxHashHex: u.TxHash, TxIndex: u.TxIndex}) + sumAssetsMaps(totalInputAssets, u.Assets) + if totalInputAssets["lovelace"] >= target { + break + } + } + logger.Info("Selected UTxOs (fallback)", "selected", len(inputs), "total_lovelace", totalInputAssets["lovelace"], "target_lovelace", target) + if totalInputAssets["lovelace"] < sendLovelace+minChangeLovelace { + return "", false, errors.New("not enough funds") + } + + // outputs: receiver gets only lovelace; all other assets go back to change + outputAssets := map[string]uint64{"lovelace": sendLovelace} + + dummySig := make([]byte, 64) + var feeLovelace uint64 + var txBodyCbor []byte + + for iter := 0; iter < 3; iter++ { + changeAssets := make(map[string]uint64) + sumAssetsMaps(changeAssets, totalInputAssets) + + if err := subAssetsMaps(changeAssets, outputAssets); err != nil { + return "", false, fmt.Errorf("value conservation error (outputs): %w", err) + } + if changeAssets["lovelace"] < feeLovelace { + return "", false, errors.New("not enough lovelace for fee") + } + changeAssets["lovelace"] -= feeLovelace + + // dust change handling + if changeAssets["lovelace"] > 0 && changeAssets["lovelace"] < minChangeLovelace { + feeLovelace += changeAssets["lovelace"] + changeAssets["lovelace"] = 0 + } + if changeAssets["lovelace"] == 0 { + delete(changeAssets, "lovelace") + } + + changeAssetsSlice, err := assetsMapToCardanoAssets(changeAssets) + if err != nil { + return "", false, fmt.Errorf("failed to convert change map to slice: %w", err) + } + + body, err := buildTxBodyCBORMultiAsset( + inputs, + toAddr, sendLovelace, nil, + ourAddr, changeAssets["lovelace"], changeAssetsSlice, + feeLovelace, + ttlSlot, + ) + if err != nil { + return "", false, err + } + dummySigned, err := buildSignedTxCBOR(body, rawPub, dummySig) + if err != nil { + return "", false, err + } + size := len(dummySigned) + newFee := uint64(params.MinFeeA*size + params.MinFeeB) + logger.Info("Calculated fee", "iter", iter, "fee_lovelace", newFee, "signed_tx_size_bytes", size) + if newFee == feeLovelace { + txBodyCbor = body + break + } + feeLovelace = newFee + txBodyCbor = body + } + if txBodyCbor == nil { + return "", false, errors.New("failed to build tx body") + } + + h := blake2b.Sum256(txBodyCbor) + txHash := h[:] + logger.Info("Prepared tx hash", "tx_hash_hex", hex.EncodeToString(txHash)) + + txID := uuid.New().String() + if err := mpcClient.SignTransaction(&types.SignTxMessage{ + KeyType: types.KeyTypeEd25519, + WalletID: walletID, + NetworkInternalCode: "cardano-testnet", + TxID: txID, + Tx: txHash, + }); err != nil { + return "", true, err + } + logger.Info("SignTransaction sent", "txID", txID) + + var res event.SigningResultEvent + select { + case res = <-signResultCh: + if res.ResultType == event.ResultTypeError { + return "", false, errors.New("signing failed: " + res.ErrorReason) + } + case <-time.After(60 * time.Second): + return "", true, errors.New("timeout waiting for signing result") + } + + signedTxCbor, err := buildSignedTxCBOR(txBodyCbor, rawPub, res.Signature) + if err != nil { + return "", false, err + } + respBody, status, err := blockfrostPOSTCBORWithRetry(ctx, bfCfg.ProjectID, submitURL, signedTxCbor) + if err != nil { + return "", true, err + } + if status < 200 || status >= 300 { + msg := prettyJSON(respBody) + retryable = strings.Contains(msg, "BadInputsUTxO") || strings.Contains(msg, "InvalidWitnessesUTXOW") || strings.Contains(msg, "ValueNotConservedUTxO") || strings.Contains(msg, "OutsideValidityIntervalUTxO") + return "", retryable, fmt.Errorf("submit HTTP %d: %s", status, msg) + } + _ = json.Unmarshal(respBody, &submittedHash) + if submittedHash == "" { + submittedHash = strings.TrimSpace(string(respBody)) + } + return submittedHash, false, nil +} func fetchAllUtxosOnce(ctx context.Context, cfg bfConfig, addr string) ([]simpleUtxo, error) { @@ -325,8 +491,12 @@ func fetchAllUtxosOnce(ctx context.Context, cfg bfConfig, addr string) ([]simple if err != nil { return nil, err } + assets, err := parseAmountToAssetsMap(u.Amount) + if err != nil { + return nil, err + } lovelaceOnly := len(u.Amount) == 1 - out = append(out, simpleUtxo{TxHash: u.TxHash, TxIndex: uint32(u.TxIndex), Lovelace: lovelace, LovelaceOnly: lovelaceOnly}) + out = append(out, simpleUtxo{TxHash: u.TxHash, TxIndex: uint32(u.TxIndex), Lovelace: lovelace, LovelaceOnly: lovelaceOnly, Assets: assets}) } return out, nil } \ No newline at end of file diff --git a/examples/cardano_poc/sign_tx_token.go b/examples/cardano_poc/sign_tx_token.go new file mode 100644 index 0000000..8486c4f --- /dev/null +++ b/examples/cardano_poc/sign_tx_token.go @@ -0,0 +1,428 @@ +//go:build sign_tx_token + +package main + +import ( + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "sort" + "strings" + "time" + + "golang.org/x/crypto/blake2b" + + "github.com/fystack/mpcium/pkg/client" + "github.com/fystack/mpcium/pkg/config" + "github.com/fystack/mpcium/pkg/event" + "github.com/fystack/mpcium/pkg/logger" + "github.com/fystack/mpcium/pkg/types" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + "github.com/spf13/viper" +) + +// Usage: +// go run -tags=sign_tx_token ./examples/cardano_poc --wallet-id --to --token .: [--ada 1.5] +// +// Notes (PoC limitations): +// - Only supports sending ONE token (native asset) in this tx. +// - Only supports simple outputs (no datum/script, no stake addr). +// - Change address is always our own deposit address. + +type tokenArgs struct { + WalletID string + ToAddr string + Ada float64 + Asset cardanoAsset +} + +type walletRecord struct { + WalletID string `json:"wallet_id"` + EDDSAPubKeyHex string `json:"eddsa_pubkey_hex"` + DepositAddress string `json:"deposit_address"` +} + +func loadWalletOrFatal(walletID string) (walletRecord, []byte, string) { + const walletFilePath = "examples/cardano_poc/cardano_poc_wallet.json" + b, err := os.ReadFile(walletFilePath) + if err != nil { + logger.Fatal("failed to read wallet file", err) + } + wallets := make(map[string]walletRecord) + if err := json.Unmarshal(b, &wallets); err != nil { + logger.Fatal("failed to unmarshal wallets file", err) + } + wf, ok := wallets[walletID] + if !ok { + logger.Fatal(fmt.Sprintf("wallet with ID %s not found in %s", walletID, walletFilePath), nil) + } + pubKeyBytes, err := hex.DecodeString(wf.EDDSAPubKeyHex) + if err != nil { + logger.Fatal("invalid pubkey hex", err) + } + rawPub, err := normalizeEd25519PubKey(pubKeyBytes) + if err != nil { + logger.Fatal("normalize pubkey failed", err) + } + return wf, rawPub, wf.DepositAddress +} + +func fetchAllUtxosOnce(ctx context.Context, cfg bfConfig, addr string) ([]simpleUtxo, error) { + url := cfg.BaseURL + "/addresses/" + addr + "/utxos" + b, status, err := blockfrostGETWithRetry(ctx, cfg.ProjectID, url) + if err != nil { + return nil, err + } + if status == 404 { + return nil, errors.New("address not found on-chain yet (404) - fund it first") + } + if status < 200 || status >= 300 { + return nil, fmt.Errorf("utxo HTTP %d: %s", status, prettyJSON(b)) + } + var raw []bfUtxo + if err := json.Unmarshal(b, &raw); err != nil { + return nil, err + } + if len(raw) == 0 { + return nil, errors.New("no UTxO at address - fund it first") + } + + out := make([]simpleUtxo, 0, len(raw)) + for _, u := range raw { + lovelace, err := findLovelace(u.Amount) + if err != nil { + return nil, err + } + assets, err := parseAmountToAssetsMap(u.Amount) + if err != nil { + return nil, err + } + lovelaceOnly := len(u.Amount) == 1 + out = append(out, simpleUtxo{TxHash: u.TxHash, TxIndex: uint32(u.TxIndex), Lovelace: lovelace, LovelaceOnly: lovelaceOnly, Assets: assets}) + } + return out, nil +} + + +func main() { + logger.Init("development", true) + config.InitViperConfig("examples/cardano_poc/config.yaml") + + bfCfg := loadBFConfig() + if bfCfg.ProjectID == "" { + logger.Fatal("blockfrost_project_id is not set in examples/cardano_poc/config.yaml", nil) + } + + params, err := fetchProtocolParams(context.Background(), bfCfg) + if err != nil { + logger.Fatal("failed to fetch protocol params", err) + } + logger.Info("Fetched protocol params", "min_fee_a", params.MinFeeA, "min_fee_b", params.MinFeeB, "network", bfCfg.Network, "base_url", bfCfg.BaseURL) + + args := parseTokenArgsOrFatal() + _, rawPub, ourAddr := loadWalletOrFatal(args.WalletID) + logger.Info("Using wallet", "wallet_id", args.WalletID, "address", ourAddr) + + minChangeLovelace := bfCfg.MinChangeLov + + // sendAdaLov starts from user input (could be 0) and may be auto-bumped later. + sendAdaLov := uint64(args.Ada * 1_000_000) + + algorithm := viper.GetString("event_initiator_algorithm") + if algorithm == "" { + algorithm = string(types.EventInitiatorKeyTypeEd25519) + } + + natsURL := viper.GetString("nats.url") + natsConn, err := nats.Connect(natsURL) + if err != nil { + logger.Fatal("Failed to connect to NATS", err) + } + defer natsConn.Drain() + defer natsConn.Close() + + localSigner, err := client.NewLocalSigner(types.EventInitiatorKeyType(algorithm), client.LocalSignerOptions{KeyPath: "./event_initiator.key"}) + if err != nil { + logger.Fatal("Failed to create local signer", err) + } + mpcClient := client.NewMPCClient(client.Options{NatsConn: natsConn, Signer: localSigner}) + + signResultCh := make(chan event.SigningResultEvent, 1) + if err := mpcClient.OnSignResult(func(evt event.SigningResultEvent) { + if evt.WalletID == args.WalletID { + signResultCh <- evt + } + }); err != nil { + logger.Fatal("OnSignResult subscribe failed", err) + } + + submitURL := bfCfg.BaseURL + "/tx/submit" + maxAttempts := 2 + for attempt := 1; attempt <= maxAttempts; attempt++ { + submittedHash, retryableErr, err := buildSignSubmitTokenOnce( + context.Background(), bfCfg, params, + mpcClient, signResultCh, + submitURL, + args.WalletID, rawPub, + ourAddr, args.ToAddr, + sendAdaLov, args.Asset, + minChangeLovelace, + ) + if err == nil { + fmt.Println(submittedHash) + fmt.Println("explorer:", "https://preprod.cardanoscan.io/transaction/"+submittedHash) + return + } + logger.Info("Submit attempt failed", "attempt", attempt, "retryable", retryableErr, "error", err.Error()) + if !retryableErr || attempt == maxAttempts { + logger.Fatal("submit failed", err) + } + logger.Info("Rebuilding tx after failure", "next_attempt", attempt+1) + } +} + +func parseTokenArgsOrFatal() tokenArgs { + var out tokenArgs + out.Ada = 0 // Default to 0, will be auto-calculated + + var tokenSpec string + for i := 1; i < len(os.Args); i++ { + switch os.Args[i] { + case "--wallet-id": + i++ + out.WalletID = os.Args[i] + case "--to": + i++ + out.ToAddr = os.Args[i] + case "--token": + i++ + tokenSpec = os.Args[i] + case "--ada": + i++ + _, _ = fmt.Sscanf(os.Args[i], "%f", &out.Ada) + } + } + if out.WalletID == "" || out.ToAddr == "" || tokenSpec == "" { + logger.Fatal("usage: sign_tx_token --wallet-id --to --token [--ada 1.0]", nil) + } + asset, err := parseCardanoAssetArg(tokenSpec) + if err != nil { + logger.Fatal("invalid --token", err) + } + out.Asset = asset + return out +} + +func buildSignSubmitTokenOnce( + ctx context.Context, + bfCfg bfConfig, + params *protocolParams, + mpcClient client.MPCClient, + signResultCh <-chan event.SigningResultEvent, + submitURL string, + walletID string, + rawPub []byte, + ourAddr string, + toAddr string, + toAdaLovelace uint64, + token cardanoAsset, + minChangeLovelace uint64, +) (submittedHash string, retryable bool, err error) { + // 1) Fetch UTxOs + utxos, err := fetchAllUtxosOnce(ctx, bfCfg, ourAddr) + if err != nil { + return "", true, err + } + if len(utxos) == 0 { + return "", false, errors.New("no UTxO at address") + } + + // 2) TTL + currentSlot, err := fetchCurrentSlot(ctx, bfCfg) + if err != nil { + return "", true, err + } + ttlSlot := currentSlot + bfCfg.TTLSeconds + logger.Info("Using TTL", "current_slot", currentSlot, "ttl_slot", ttlSlot) + + // 3) Coin selection (real multi-asset) + const feeUpperBound uint64 = 900_000 + tokenUnit, err := assetUnitFromPolicyAndName(token.PolicyIDHex, token.AssetNameHex) + if err != nil { + return "", false, fmt.Errorf("invalid token spec: %w", err) + } + + // Find UTxOs with the target token, and other UTxOs for ADA + tokenUtxos := make([]simpleUtxo, 0) + adaOnlyUtxos := make([]simpleUtxo, 0) + for _, u := range utxos { + if u.Assets[tokenUnit] > 0 { + tokenUtxos = append(tokenUtxos, u) + } else if u.LovelaceOnly { + adaOnlyUtxos = append(adaOnlyUtxos, u) + } + } + sort.Slice(tokenUtxos, func(i, j int) bool { return tokenUtxos[i].Assets[tokenUnit] > tokenUtxos[j].Assets[tokenUnit] }) + sort.Slice(adaOnlyUtxos, func(i, j int) bool { return adaOnlyUtxos[i].Lovelace > adaOnlyUtxos[j].Lovelace }) + + // Select inputs + inputs := make([]txInput, 0) + totalInputAssets := make(map[string]uint64) + for _, u := range tokenUtxos { + if totalInputAssets[tokenUnit] >= token.Quantity { + break // we have enough of the target token + } + inputs = append(inputs, txInput{TxHashHex: u.TxHash, TxIndex: u.TxIndex}) + sumAssetsMaps(totalInputAssets, u.Assets) + } + if totalInputAssets[tokenUnit] < token.Quantity { + return "", false, fmt.Errorf("insufficient token balance for %s: have %d, need %d", tokenUnit, totalInputAssets[tokenUnit], token.Quantity) + } + + // Add ADA-only UTxOs if needed for fee + ADA output + neededLovelace := toAdaLovelace + feeUpperBound + minChangeLovelace + if totalInputAssets["lovelace"] < neededLovelace { + for _, u := range adaOnlyUtxos { + inputs = append(inputs, txInput{TxHashHex: u.TxHash, TxIndex: u.TxIndex}) + sumAssetsMaps(totalInputAssets, u.Assets) + if totalInputAssets["lovelace"] >= neededLovelace { + break + } + } + } + logger.Info("Selected UTxOs", "count", len(inputs), "total_input_lovelace", totalInputAssets["lovelace"], "total_input_token", totalInputAssets[tokenUnit]) + +// 4) Ensure receiver output has enough ADA (min-UTxO) + // NOTE: --ada is optional; we treat it as a *minimum* (can be 0). + minToAda, err := estimateMinAdaForOutput(params, toAddr, 0, []cardanoAsset{token}) + if err != nil || minToAda == 0 { + // Fallback to minChangeLovelace if calculation fails + minToAda = minChangeLovelace + } + if toAdaLovelace < minToAda { + logger.Info("Auto-bumping receiver ADA to satisfy min-UTxO", "min_required", minToAda, "requested", toAdaLovelace) + toAdaLovelace = minToAda + } + + // 5) Fee & Change Calculation + dummySig := make([]byte, 64) + var feeLovelace uint64 + var txBodyCbor []byte + + outputAssets := map[string]uint64{ + "lovelace": toAdaLovelace, + tokenUnit: token.Quantity, + } + + for iter := 0; iter < 3; iter++ { + // Calculate change based on current fee estimate + changeAssets := make(map[string]uint64) + sumAssetsMaps(changeAssets, totalInputAssets) + + // Subtract outputs + if err := subAssetsMaps(changeAssets, outputAssets); err != nil { + return "", false, fmt.Errorf("value conservation error (outputs): %w", err) + } + + // Subtract fee + if changeAssets["lovelace"] < feeLovelace { + return "", false, errors.New("not enough lovelace for fee") + } + changeAssets["lovelace"] -= feeLovelace + + // Handle dust change + changeLovelace := changeAssets["lovelace"] + if changeLovelace > 0 && changeLovelace < minChangeLovelace { + feeLovelace += changeLovelace + changeAssets["lovelace"] = 0 + } + if changeAssets["lovelace"] == 0 { + delete(changeAssets, "lovelace") + } + + changeAssetsSlice, err := assetsMapToCardanoAssets(changeAssets) + if err != nil { + return "", false, fmt.Errorf("failed to convert change map to slice: %w", err) + } + + body, err := buildTxBodyCBORMultiAsset( + inputs, + toAddr, toAdaLovelace, []cardanoAsset{token}, + ourAddr, changeAssets["lovelace"], changeAssetsSlice, + feeLovelace, + ttlSlot, + ) + if err != nil { + return "", false, err + } + dummySigned, err := buildSignedTxCBOR(body, rawPub, dummySig) + if err != nil { + return "", false, err + } + size := len(dummySigned) + newFee := uint64(params.MinFeeA*size + params.MinFeeB) + logger.Info("Calculated fee", "iter", iter, "fee_lovelace", newFee, "signed_tx_size_bytes", size) + if newFee == feeLovelace { + txBodyCbor = body + break + } + feeLovelace = newFee + txBodyCbor = body + } + if txBodyCbor == nil { + return "", false, errors.New("failed to build tx body") + } + + // 5) Tx hash + h := blake2b.Sum256(txBodyCbor) + txHash := h[:] + logger.Info("Prepared tx hash", "tx_hash_hex", hex.EncodeToString(txHash)) + + // 6) Sign + txID := uuid.New().String() + if err := mpcClient.SignTransaction(&types.SignTxMessage{ + KeyType: types.KeyTypeEd25519, + WalletID: walletID, + NetworkInternalCode: "cardano-testnet", + TxID: txID, + Tx: txHash, + }); err != nil { + return "", true, err + } + logger.Info("SignTransaction sent", "txID", txID) + + var res event.SigningResultEvent + select { + case res = <-signResultCh: + if res.ResultType == event.ResultTypeError { + return "", false, errors.New("signing failed: "+res.ErrorReason) + } + case <-time.After(60 * time.Second): + return "", true, errors.New("timeout waiting for signing result") + } + + // 7) Submit + signedTxCbor, err := buildSignedTxCBOR(txBodyCbor, rawPub, res.Signature) + if err != nil { + return "", false, err + } + respBody, status, err := blockfrostPOSTCBORWithRetry(ctx, bfCfg.ProjectID, submitURL, signedTxCbor) + if err != nil { + return "", true, err + } + if status < 200 || status >= 300 { + msg := prettyJSON(respBody) + retryable = strings.Contains(msg, "BadInputsUTxO") || strings.Contains(msg, "InvalidWitnessesUTXOW") || strings.Contains(msg, "ValueNotConservedUTxO") || strings.Contains(msg, "OutsideValidityIntervalUTxO") + return "", retryable, fmt.Errorf("submit HTTP %d: %s", status, msg) + } + _ = json.Unmarshal(respBody, &submittedHash) + if submittedHash == "" { + submittedHash = strings.TrimSpace(string(respBody)) + } + return submittedHash, false, nil +} + From 999daaec9d6151c58eab8bf38f79a3d1548fba5d Mon Sep 17 00:00:00 2001 From: Woft257 Date: Fri, 2 Jan 2026 20:57:28 +0700 Subject: [PATCH 10/13] refactor(cardano_poc): improve comments for clarity and update usage instructions in sign_tx --- examples/cardano_poc/lib.go | 115 +++++++++++--------------------- examples/cardano_poc/sign_tx.go | 12 +++- 2 files changed, 49 insertions(+), 78 deletions(-) diff --git a/examples/cardano_poc/lib.go b/examples/cardano_poc/lib.go index 9297530..d3d356a 100644 --- a/examples/cardano_poc/lib.go +++ b/examples/cardano_poc/lib.go @@ -53,7 +53,7 @@ func loadBFConfig() bfConfig { } return cfg } -// --- shared helpers (used by create_wallet.go and sign_tx.go) --- +// --- Shared helpers (used by the Cardano PoC binaries) --- type bfUtxo struct { TxHash string `json:"tx_hash"` @@ -347,11 +347,11 @@ func trimJSONQuotes(s string) string { } type protocolParams struct { - MinFeeA int `json:"min_fee_a"` - MinFeeB int `json:"min_fee_b"` - CoinsPerUTXOWord string `json:"coins_per_utxo_word"` // Sửa từ int sang string - Epoch int `json:"epoch"` - Slot int `json:"slot"` + MinFeeA int `json:"min_fee_a"` + MinFeeB int `json:"min_fee_b"` + CoinsPerUTXOWord string `json:"coins_per_utxo_word"` // Blockfrost returns this as a string + Epoch int `json:"epoch"` + Slot int `json:"slot"` } func fetchProtocolParams(ctx context.Context, cfg bfConfig) (*protocolParams, error) { @@ -391,7 +391,7 @@ func fetchCurrentSlot(ctx context.Context, cfg bfConfig) (uint64, error) { } return resp.Slot, nil } -// --- Cardano multi-asset (token) helpers for PoC --- +// --- Cardano multi-asset (token) helpers for the PoC --- // cardanoAmount represents an amount in a tx output. // - For ADA-only: uint64 lovelace @@ -515,79 +515,44 @@ func buildTxBodyCBORMultiAsset( return cbor.Marshal(body) } -func buildOutputAmount(lovelace uint64, assets []cardanoAsset) (any, error) { - if len(assets) == 0 { - return lovelace, nil - } - - policies := make(map[string]map[string]uint64) - for _, a := range assets { - if a.Quantity == 0 { - continue - } - policyHex := strings.TrimPrefix(a.PolicyIDHex, "0x") - assetHex := strings.TrimPrefix(a.AssetNameHex, "0x") - inner, ok := policies[policyHex] - if !ok { - inner = make(map[string]uint64) - policies[policyHex] = inner - } - inner[assetHex] += a.Quantity - } - - cborPolicies := make(map[any]any) - for policyHex, assetsByName := range policies { - pidBytes, _ := hex.DecodeString(policyHex) - cborInner := make(map[any]any) - for assetHex, qty := range assetsByName { - nameBytes := []byte{} - if assetHex != "" { - nameBytes, _ = hex.DecodeString(assetHex) - } - cborInner[cbor.ByteString(nameBytes)] = qty - } - cborPolicies[cbor.ByteString(pidBytes)] = cborInner - } - return []any{lovelace, cborPolicies}, nil -} func estimateMinAdaForOutput(params *protocolParams, addrBech32 string, lovelace uint64, assets []cardanoAsset) (uint64, error) { - const ( - fallback uint64 = 2_000_000 - minAdaWithToken uint64 = 1_200_000 // Tối thiểu 1.2 ADA khi có token - ) - - if len(assets) > 0 { - // Nếu có token, trả về ít nhất 1.2 ADA - return minAdaWithToken, nil - } + const ( + fallback uint64 = 2_000_000 + minAdaWithToken uint64 = 1_200_000 // Minimum 1.2 ADA is a safe bet for outputs with native tokens. + ) - // Phần còn lại giữ nguyên cho trường hợp không có token - coinsPerUTXOWord := 0 - if params != nil { - coinsPerUTXOWord, _ = strconv.Atoi(strings.TrimSpace(params.CoinsPerUTXOWord)) - } - if coinsPerUTXOWord == 0 { - return fallback, nil - } + if len(assets) > 0 { + // If the output includes any native tokens, enforce the minimum ADA requirement for such outputs. + return minAdaWithToken, nil + } - addrBytes, err := decodeCardanoAddressBytes(addrBech32) - if err != nil { - return fallback, nil - } + // The rest of this function is for ADA-only outputs, which have a lower min-ADA requirement. + coinsPerUTXOWord := 0 + if params != nil { + coinsPerUTXOWord, _ = strconv.Atoi(strings.TrimSpace(params.CoinsPerUTXOWord)) + } + if coinsPerUTXOWord == 0 { + return fallback, nil + } - output := []any{addrBytes, lovelace} - ser, err := cbor.Marshal(output) - if err != nil { - return fallback, nil - } + addrBytes, err := decodeCardanoAddressBytes(addrBech32) + if err != nil { + return fallback, nil + } - minAda := (160 + uint64(len(ser))) * uint64(coinsPerUTXOWord) / 8 - if minAda == 0 { - return fallback, nil - } - return minAda, nil + output := []any{addrBytes, lovelace} + ser, err := cbor.Marshal(output) + if err != nil { + return fallback, nil + } + + minAda := (160 + uint64(len(ser))) * uint64(coinsPerUTXOWord) / 8 + if minAda == 0 { + return fallback, nil + } + return minAda, nil } func parseCardanoAssetArg(s string) (cardanoAsset, error) { @@ -618,7 +583,7 @@ func parseCardanoAssetArg(s string) (cardanoAsset, error) { policy = sp[0] assetName = sp[1] - // Convert ASCII to hex if needed + // Convert ASCII asset names to hex (Blockfrost uses hex-encoded asset names). if assetName != "" && !isHex(assetName) { assetName = hex.EncodeToString([]byte(assetName)) } @@ -631,7 +596,7 @@ func parseCardanoAssetArg(s string) (cardanoAsset, error) { return out, nil } -// Add this helper function in lib.go +// isHex reports whether s contains only hexadecimal characters. func isHex(s string) bool { for _, c := range s { if !((c >= '0' && c <= '9') || diff --git a/examples/cardano_poc/sign_tx.go b/examples/cardano_poc/sign_tx.go index 4b5a604..237f2d6 100644 --- a/examples/cardano_poc/sign_tx.go +++ b/examples/cardano_poc/sign_tx.go @@ -29,8 +29,9 @@ import ( // go run -tags=sign_tx ./examples/cardano_poc --wallet-id --to --amount-ada // // Notes: -// - This file does NOT wait for UTxO. It expects you to fund the deposit address first. +// - This binary does NOT wait for UTxO confirmation. Fund the deposit address first. // - It reads pubkey/address from examples/cardano_poc/cardano_poc_wallet.json. +// - For sending native tokens, use sign_tx_token. type walletRecord struct { WalletID string `json:"wallet_id"` @@ -99,7 +100,9 @@ func main() { for attempt := 1; attempt <= maxAttempts; attempt++ { submittedHash, retryableErr, err := buildSignSubmitOnce(context.Background(), bfCfg, params, mpcClient, signResultCh, submitURL, walletID, rawPub, ourAddr, toAddr, sendLovelace, minChangeLovelace) if err != nil && strings.Contains(err.Error(), "no ADA-only UTxO") { - // Fallback: wallet may only have multi-asset UTxOs; preserve tokens in change. + // Fallback: the wallet may only have multi-asset UTxOs. + // Build a multi-asset tx body that sends only ADA to the recipient, + // while returning all non-ADA assets back to our change output. submittedHash, retryableErr, err = buildSignSubmitMultiAssetAdaFallback(context.Background(), bfCfg, params, mpcClient, signResultCh, submitURL, walletID, rawPub, ourAddr, toAddr, sendLovelace, minChangeLovelace) } if err == nil { @@ -131,7 +134,10 @@ func parseArgsOrFatal() (walletID string, toAddr string, amountAda float64) { } } if walletID == "" || toAddr == "" { - logger.Fatal("usage: sign_tx.go --wallet-id --to --amount-ada 1", nil) + logger.Fatal(`Invalid arguments. Usage: + --wallet-id Wallet ID from cardano_poc_wallet.json + --to Recipient address + --amount-ada ADA amount to send`, nil) } return } From a5a7678220771bc844f3cadfaf0f895b0996e491 Mon Sep 17 00:00:00 2001 From: Woft257 Date: Fri, 2 Jan 2026 21:03:34 +0700 Subject: [PATCH 11/13] Refactor code structure for improved readability and maintainability --- go.mod | 34 ++++++++--------- go.sum | 113 +++++++++++++++++---------------------------------------- 2 files changed, 49 insertions(+), 98 deletions(-) diff --git a/go.mod b/go.mod index d40a8c1..44f80c3 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,12 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.18.8 github.com/aws/aws-sdk-go-v2/service/kms v1.45.0 github.com/bnb-chain/tss-lib/v2 v2.0.2 - github.com/btcsuite/btcd v0.24.2 - github.com/btcsuite/btcd/btcec/v2 v2.3.2 + github.com/btcsuite/btcd v0.25.0 + github.com/btcsuite/btcd/btcec/v2 v2.3.5 github.com/btcsuite/btcutil v1.0.2 github.com/cosmos/btcutil v1.0.5 github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 - github.com/dgraph-io/badger/v4 v4.2.0 + github.com/dgraph-io/badger/v4 v4.9.0 github.com/fxamacker/cbor/v2 v2.6.0 github.com/google/uuid v1.6.0 github.com/hashicorp/consul/api v1.32.1 @@ -25,8 +25,8 @@ require ( github.com/spf13/viper v1.18.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.3.2 - golang.org/x/crypto v0.37.0 - golang.org/x/term v0.31.0 + golang.org/x/crypto v0.41.0 + golang.org/x/term v0.34.0 ) require ( @@ -47,18 +47,14 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect + github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.0.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect @@ -93,17 +89,19 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.24.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + google.golang.org/protobuf v1.36.7 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 669b4e2..c12a3d0 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -57,12 +56,13 @@ github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43/go.mod github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= -github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= -github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd v0.25.0 h1:JPbjwvHGpSywBRuorFFqTjaVP4y6Qw69XJ1nQ6MyWJM= +github.com/btcsuite/btcd v0.25.0/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= +github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= @@ -80,14 +80,11 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= @@ -103,20 +100,14 @@ github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3/go.mod h1:AKpV6+wZ2MfPRJnTbQ6NPgW github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= -github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgraph-io/badger/v4 v4.9.0 h1:tpqWb0NewSrCYqTvywbcXOhQdWcqephkVkbBmaaqHzc= +github.com/dgraph-io/badger/v4 v4.9.0/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= +github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= +github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -136,18 +127,16 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -156,31 +145,22 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= @@ -248,7 +228,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -333,7 +312,6 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= @@ -382,7 +360,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -399,8 +376,14 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= @@ -426,14 +409,10 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -442,11 +421,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -456,16 +432,14 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -476,7 +450,6 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -509,20 +482,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -531,13 +503,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -552,30 +521,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -599,6 +554,4 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= From 299f96b4730d0373af0064083b964cfeed32d45e Mon Sep 17 00:00:00 2001 From: Woft257 Date: Fri, 2 Jan 2026 21:19:03 +0700 Subject: [PATCH 12/13] docs: update usage instructions in sign_tx_token --- examples/cardano_poc/sign_tx_token.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/cardano_poc/sign_tx_token.go b/examples/cardano_poc/sign_tx_token.go index 8486c4f..d45b82c 100644 --- a/examples/cardano_poc/sign_tx_token.go +++ b/examples/cardano_poc/sign_tx_token.go @@ -26,12 +26,7 @@ import ( ) // Usage: -// go run -tags=sign_tx_token ./examples/cardano_poc --wallet-id --to --token .: [--ada 1.5] -// -// Notes (PoC limitations): -// - Only supports sending ONE token (native asset) in this tx. -// - Only supports simple outputs (no datum/script, no stake addr). -// - Change address is always our own deposit address. +// go run -tags=sign_tx_token ./examples/cardano_poc --wallet-id --to --token .: type tokenArgs struct { WalletID string From 3d79fb041cf720a47ab05bc8ac2cb478a3f425b9 Mon Sep 17 00:00:00 2001 From: Woft257 Date: Fri, 2 Jan 2026 23:13:58 +0700 Subject: [PATCH 13/13] feat(cardano_poc): enhance token handling by updating quantity parsing and improving ADA calculation for multi-asset transactions --- examples/cardano_poc/lib.go | 5 +-- examples/cardano_poc/sign_tx_token.go | 44 ++++++++++++++++----------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/examples/cardano_poc/lib.go b/examples/cardano_poc/lib.go index d3d356a..839f590 100644 --- a/examples/cardano_poc/lib.go +++ b/examples/cardano_poc/lib.go @@ -570,10 +570,11 @@ func parseCardanoAssetArg(s string) (cardanoAsset, error) { idPart := strings.TrimSpace(parts[0]) qtyPart := strings.TrimSpace(parts[1]) - var qty uint64 - if _, err := fmt.Sscanf(qtyPart, "%d", &qty); err != nil { + var qtyF float64 + if _, err := fmt.Sscanf(qtyPart, "%f", &qtyF); err != nil { return out, fmt.Errorf("invalid quantity: %w", err) } + qty := uint64(qtyF * 1_000_000) policy := idPart var assetName string diff --git a/examples/cardano_poc/sign_tx_token.go b/examples/cardano_poc/sign_tx_token.go index d45b82c..dd14c12 100644 --- a/examples/cardano_poc/sign_tx_token.go +++ b/examples/cardano_poc/sign_tx_token.go @@ -244,27 +244,43 @@ func buildSignSubmitTokenOnce( ttlSlot := currentSlot + bfCfg.TTLSeconds logger.Info("Using TTL", "current_slot", currentSlot, "ttl_slot", ttlSlot) - // 3) Coin selection (real multi-asset) + // 3) Coin selection & min-ADA calculation (multi-asset) const feeUpperBound uint64 = 900_000 tokenUnit, err := assetUnitFromPolicyAndName(token.PolicyIDHex, token.AssetNameHex) if err != nil { return "", false, fmt.Errorf("invalid token spec: %w", err) } + // 4) Ensure receiver output has enough ADA (min-UTxO) BEFORE selecting ADA-only inputs + // NOTE: --ada is optional; we treat it as a *minimum* (can be 0). + minToAda, err := estimateMinAdaForOutput(params, toAddr, 0, []cardanoAsset{token}) + if err != nil || minToAda == 0 { + // Fallback to minChangeLovelace if calculation fails + minToAda = minChangeLovelace + } + if toAdaLovelace < minToAda { + logger.Info("Auto-bumping receiver ADA to satisfy min-UTxO", "min_required", minToAda, "requested", toAdaLovelace) + toAdaLovelace = minToAda + } + // Find UTxOs with the target token, and other UTxOs for ADA tokenUtxos := make([]simpleUtxo, 0) adaOnlyUtxos := make([]simpleUtxo, 0) for _, u := range utxos { if u.Assets[tokenUnit] > 0 { tokenUtxos = append(tokenUtxos, u) - } else if u.LovelaceOnly { + } + // Any UTxO that does NOT contain the target token can be used to top up lovelace for fees/min-ADA, + // not only "lovelace-only" ones. This fixes the case where the wallet has ADA but it's sitting in + // UTxOs that also contain other tokens. + if u.Assets[tokenUnit] == 0 { adaOnlyUtxos = append(adaOnlyUtxos, u) } } sort.Slice(tokenUtxos, func(i, j int) bool { return tokenUtxos[i].Assets[tokenUnit] > tokenUtxos[j].Assets[tokenUnit] }) sort.Slice(adaOnlyUtxos, func(i, j int) bool { return adaOnlyUtxos[i].Lovelace > adaOnlyUtxos[j].Lovelace }) - // Select inputs + // Select token inputs first inputs := make([]txInput, 0) totalInputAssets := make(map[string]uint64) for _, u := range tokenUtxos { @@ -278,7 +294,7 @@ func buildSignSubmitTokenOnce( return "", false, fmt.Errorf("insufficient token balance for %s: have %d, need %d", tokenUnit, totalInputAssets[tokenUnit], token.Quantity) } - // Add ADA-only UTxOs if needed for fee + ADA output + // Add ADA-only UTxOs if needed for fee + receiver min-ADA + change min-ADA neededLovelace := toAdaLovelace + feeUpperBound + minChangeLovelace if totalInputAssets["lovelace"] < neededLovelace { for _, u := range adaOnlyUtxos { @@ -289,19 +305,13 @@ func buildSignSubmitTokenOnce( } } } - logger.Info("Selected UTxOs", "count", len(inputs), "total_input_lovelace", totalInputAssets["lovelace"], "total_input_token", totalInputAssets[tokenUnit]) - -// 4) Ensure receiver output has enough ADA (min-UTxO) - // NOTE: --ada is optional; we treat it as a *minimum* (can be 0). - minToAda, err := estimateMinAdaForOutput(params, toAddr, 0, []cardanoAsset{token}) - if err != nil || minToAda == 0 { - // Fallback to minChangeLovelace if calculation fails - minToAda = minChangeLovelace - } - if toAdaLovelace < minToAda { - logger.Info("Auto-bumping receiver ADA to satisfy min-UTxO", "min_required", minToAda, "requested", toAdaLovelace) - toAdaLovelace = minToAda - } + logger.Info( + "Selected UTxOs", + "count", len(inputs), + "total_input_lovelace", totalInputAssets["lovelace"], + "total_input_token", totalInputAssets[tokenUnit], + "needed_lovelace_upper_bound", neededLovelace, + ) // 5) Fee & Change Calculation dummySig := make([]byte, 64)