mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
fix: remove postgres/valkey/legacy mode — consensus+ZapDB only
- Removed --mode, --api-db, --api-kv, --db flags - Removed legacy NATS/Consul transport (runNode, ~200 lines) - Removed standalone API server (runAPIOnly, ~200 lines) - Removed Consul helpers, NATS connection, interactive prompts - Dashboard now uses ZapDB (SQLite) instead of postgres - pkg/db: removed postgres backend, removed valkey client - Consensus mode is the only mode. Zero external deps.
This commit is contained in:
+68
-738
@@ -24,11 +24,9 @@ import (
|
||||
|
||||
"github.com/mr-tron/base58"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/hanzoai/base"
|
||||
"github.com/hanzoai/base/core"
|
||||
@@ -36,13 +34,9 @@ import (
|
||||
|
||||
mpcapi "github.com/luxfi/mpc/pkg/api"
|
||||
"github.com/luxfi/mpc/pkg/backup"
|
||||
"github.com/luxfi/mpc/pkg/config"
|
||||
"github.com/luxfi/mpc/pkg/constant"
|
||||
"github.com/luxfi/mpc/pkg/db"
|
||||
"github.com/luxfi/mpc/pkg/event"
|
||||
"github.com/luxfi/mpc/pkg/eventconsumer"
|
||||
"github.com/luxfi/mpc/pkg/identity"
|
||||
"github.com/luxfi/mpc/pkg/infra"
|
||||
"github.com/luxfi/mpc/pkg/keyinfo"
|
||||
"github.com/luxfi/mpc/pkg/kvstore"
|
||||
"github.com/luxfi/mpc/pkg/logger"
|
||||
@@ -67,40 +61,27 @@ func main() {
|
||||
Name: "start",
|
||||
Usage: "Start a Lux MPC node",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Node name",
|
||||
Required: false, // Not required in consensus mode
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mode",
|
||||
Aliases: []string{"m"},
|
||||
Usage: "Transport mode: 'consensus' (ZAP/PoA) or 'legacy' (NATS/Consul, deprecated)",
|
||||
Value: "consensus",
|
||||
},
|
||||
// Consensus mode flags
|
||||
&cli.StringFlag{
|
||||
Name: "node-id",
|
||||
Usage: "Node ID (consensus mode)",
|
||||
Usage: "Node ID",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "listen",
|
||||
Usage: "P2P listen address (consensus mode)",
|
||||
Usage: "P2P listen address",
|
||||
Value: ":9651",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "api",
|
||||
Usage: "API listen address (consensus mode)",
|
||||
Usage: "Internal API listen address",
|
||||
Value: ":9800",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "data",
|
||||
Usage: "Data directory (consensus mode)",
|
||||
Usage: "Data directory",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "keys",
|
||||
Usage: "Keys directory (consensus mode)",
|
||||
Usage: "Keys directory",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "threshold",
|
||||
@@ -117,15 +98,6 @@ func main() {
|
||||
Usage: "Log level (debug, info, warn, error)",
|
||||
Value: "info",
|
||||
},
|
||||
// Dashboard API flags
|
||||
&cli.StringFlag{
|
||||
Name: "api-db",
|
||||
Usage: "PostgreSQL connection URL for dashboard API",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "api-kv",
|
||||
Usage: "Valkey/Redis address for KV cache (e.g. localhost:6379)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "api-listen",
|
||||
Usage: "Dashboard API listen address",
|
||||
@@ -165,18 +137,6 @@ func main() {
|
||||
Sources: cli.EnvVars("MPC_HSM_ATTEST"),
|
||||
Value: false,
|
||||
},
|
||||
// Legacy mode flags
|
||||
&cli.BoolFlag{
|
||||
Name: "decrypt-private-key",
|
||||
Aliases: []string{"d"},
|
||||
Value: false,
|
||||
Usage: "Decrypt node private key (legacy mode)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "prompt-credentials",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Prompt for sensitive parameters (legacy mode)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable debug logging",
|
||||
@@ -184,52 +144,9 @@ func main() {
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
mode := c.String("mode")
|
||||
if mode == "legacy" {
|
||||
logger.Warn("Legacy mode (NATS/Consul) is deprecated and will be removed in a future release. Migrate to --mode=consensus.")
|
||||
return runNode(ctx, c)
|
||||
}
|
||||
return runNodeConsensus(ctx, c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "api",
|
||||
Usage: "Start Dashboard API server only (no MPC transport)",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "db",
|
||||
Usage: "PostgreSQL connection URL",
|
||||
Sources: cli.EnvVars("DATABASE_URL", "MPC_API_DB"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "listen",
|
||||
Usage: "API listen address",
|
||||
Value: ":8081",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "jwt-secret",
|
||||
Usage: "JWT signing secret",
|
||||
Sources: cli.EnvVars("JWT_SECRET", "MPC_JWT_SECRET"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "cluster-url",
|
||||
Usage: "MPC cluster URL for forwarding operations (e.g. http://mpc-node:9800)",
|
||||
Sources: cli.EnvVars("MPC_CLUSTER_URL"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "cluster-api-key",
|
||||
Usage: "API key for authenticating with MPC cluster",
|
||||
Sources: cli.EnvVars("MPC_CLUSTER_API_KEY"),
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable debug logging",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
return runAPIOnly(ctx, c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
Usage: "Display detailed version information",
|
||||
@@ -247,282 +164,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: runNode uses legacy NATS/Consul transport. Use runNodeConsensus (--mode=consensus) instead.
|
||||
func runNode(ctx context.Context, c *cli.Command) error {
|
||||
nodeName := c.String("name")
|
||||
decryptPrivateKey := c.Bool("decrypt-private-key")
|
||||
usePrompts := c.Bool("prompt-credentials")
|
||||
debug := c.Bool("debug")
|
||||
|
||||
viper.SetDefault("backup_enabled", true)
|
||||
config.InitViperConfig()
|
||||
environment := viper.GetString("environment")
|
||||
logger.Init(environment, debug)
|
||||
|
||||
// Create environment-prefixed node ID
|
||||
nodeID := fmt.Sprintf("lux-%s-%s", environment, nodeName)
|
||||
logger.Info("Starting MPC node", "nodeID", nodeID, "environment", environment)
|
||||
|
||||
// Resolve ZapDB password via HSM provider (supports AWS KMS, GCP Cloud KMS, Azure Key Vault, env, file)
|
||||
// This must happen before checkRequiredConfigValues so cloud KMS passwords work.
|
||||
var zapDBPassword string
|
||||
if usePrompts {
|
||||
promptForSensitiveCredentials()
|
||||
zapDBPassword = viper.GetString("zapdb_password")
|
||||
} else {
|
||||
// When using an HSM provider other than env (which reads from config anyway),
|
||||
// skip the zapdb_password config check — the password comes from KMS.
|
||||
hsmProvider := c.String("hsm-provider")
|
||||
skipPasswordCheck := hsmProvider != "" && hsmProvider != "env"
|
||||
checkRequiredConfigValues(skipPasswordCheck)
|
||||
zapDBPassword = resolveZapDBPassword(ctx, c)
|
||||
}
|
||||
|
||||
consulClient := infra.GetConsulClient(environment)
|
||||
keyinfoStore := keyinfo.NewStore(consulClient.KV())
|
||||
peers := LoadPeersFromConsul(consulClient)
|
||||
// Use the environment-prefixed nodeID we created above
|
||||
// nodeID is already set with environment prefix
|
||||
|
||||
zapKV := NewZapKV(nodeName, nodeID, zapDBPassword)
|
||||
defer zapKV.Close()
|
||||
|
||||
// Wrap ZapDB store with KMS-enabled store if configured
|
||||
var kvStore kvstore.KVStore = zapKV
|
||||
kmsEnabledStore, err := mpc.NewKMSEnabledKVStore(zapKV, nodeID)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create KMS-enabled store, using regular ZapDB", "error", err)
|
||||
} else {
|
||||
kvStore = kmsEnabledStore
|
||||
logger.Info("Using KMS-enabled storage for sensitive keys")
|
||||
}
|
||||
|
||||
// Start background backup job
|
||||
backupEnabled := viper.GetBool("backup_enabled")
|
||||
if backupEnabled {
|
||||
backupPeriodSeconds := viper.GetInt("backup_period_seconds")
|
||||
stopBackup := StartPeriodicBackup(ctx, zapKV, backupPeriodSeconds)
|
||||
defer stopBackup()
|
||||
}
|
||||
|
||||
identityStore, err := identity.NewFileStore("identity", nodeName, decryptPrivateKey)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to create identity store", err)
|
||||
}
|
||||
|
||||
natsConn, err := GetNATSConnection(environment)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to connect to NATS", err)
|
||||
}
|
||||
defer natsConn.Close()
|
||||
|
||||
pubsub := messaging.NewNATSPubSub(natsConn)
|
||||
keygenBroker, err := messaging.NewJetStreamBroker(ctx, natsConn, event.KeygenBrokerStream, []string{
|
||||
event.KeygenRequestTopic,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to create keygen jetstream broker", err)
|
||||
}
|
||||
signingBroker, err := messaging.NewJetStreamBroker(ctx, natsConn, event.SigningPublisherStream, []string{
|
||||
event.SigningRequestTopic,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to create signing jetstream broker", err)
|
||||
}
|
||||
|
||||
_ = messaging.NewNatsDirectMessaging(natsConn) // directMessaging available for future use
|
||||
mqManager := messaging.NewNATsMessageQueueManager("mpc", []string{
|
||||
"mpc.mpc_keygen_result.*",
|
||||
event.SigningResultTopic,
|
||||
"mpc.mpc_reshare_result.*",
|
||||
}, natsConn)
|
||||
|
||||
genKeyResultQueue := mqManager.NewMessageQueue("mpc_keygen_result")
|
||||
defer genKeyResultQueue.Close()
|
||||
singingResultQueue := mqManager.NewMessageQueue("mpc_signing_result")
|
||||
defer singingResultQueue.Close()
|
||||
reshareResultQueue := mqManager.NewMessageQueue("mpc_reshare_result")
|
||||
defer reshareResultQueue.Close()
|
||||
|
||||
logger.Info("Node is running", "peerID", nodeID, "name", nodeName)
|
||||
|
||||
peerNodeIDs := GetPeerIDs(peers)
|
||||
peerRegistry := mpc.NewRegistry(nodeID, peerNodeIDs, consulClient.KV())
|
||||
|
||||
mpcNode := mpc.NewNode(
|
||||
nodeID,
|
||||
peerNodeIDs,
|
||||
pubsub,
|
||||
kvStore,
|
||||
keyinfoStore,
|
||||
peerRegistry,
|
||||
identityStore,
|
||||
)
|
||||
|
||||
eventConsumer := eventconsumer.NewEventConsumer(
|
||||
mpcNode,
|
||||
pubsub,
|
||||
genKeyResultQueue,
|
||||
singingResultQueue,
|
||||
reshareResultQueue,
|
||||
identityStore,
|
||||
)
|
||||
eventConsumer.Run()
|
||||
defer eventConsumer.Close()
|
||||
|
||||
timeoutConsumer := eventconsumer.NewTimeOutConsumer(
|
||||
natsConn,
|
||||
singingResultQueue,
|
||||
)
|
||||
|
||||
timeoutConsumer.Run()
|
||||
defer timeoutConsumer.Close()
|
||||
keygenConsumer := eventconsumer.NewKeygenConsumer(natsConn, keygenBroker, pubsub, peerRegistry)
|
||||
signingConsumer := eventconsumer.NewSigningConsumer(natsConn, signingBroker, pubsub, peerRegistry)
|
||||
|
||||
// Make the node ready before starting the signing consumer
|
||||
if err := peerRegistry.Ready(); err != nil {
|
||||
logger.Error("Failed to mark peer registry as ready", err)
|
||||
}
|
||||
logger.Info("[READY] Node is ready", "nodeID", nodeID)
|
||||
appContext, cancel := context.WithCancel(context.Background())
|
||||
// Setup signal handling to cancel context on termination signals.
|
||||
go func() {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
logger.Warn("Shutdown signal received, canceling context...")
|
||||
cancel()
|
||||
|
||||
// Gracefully close consumers
|
||||
if err := keygenConsumer.Close(); err != nil {
|
||||
logger.Error("Failed to close keygen consumer", err)
|
||||
}
|
||||
if err := signingConsumer.Close(); err != nil {
|
||||
logger.Error("Failed to close signing consumer", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errChan := make(chan error, 2)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := keygenConsumer.Run(appContext); err != nil {
|
||||
logger.Error("error running keygen consumer", err)
|
||||
errChan <- fmt.Errorf("keygen consumer error: %w", err)
|
||||
return
|
||||
}
|
||||
logger.Info("Keygen consumer finished successfully")
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := signingConsumer.Run(appContext); err != nil {
|
||||
logger.Error("error running signing consumer", err)
|
||||
errChan <- fmt.Errorf("signing consumer error: %w", err)
|
||||
return
|
||||
}
|
||||
logger.Info("Signing consumer finished successfully")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
logger.Info("All consumers have finished")
|
||||
close(errChan)
|
||||
}()
|
||||
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
logger.Error("Consumer error received", err)
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prompt user for sensitive configuration values
|
||||
func promptForSensitiveCredentials() {
|
||||
fmt.Println("WARNING: Please back up your ZapDB password in a secure location.")
|
||||
fmt.Println("If you lose this password, you will permanently lose access to your data!")
|
||||
|
||||
// Prompt for ZapDB password with confirmation
|
||||
var zapPass []byte
|
||||
var confirmPass []byte
|
||||
var err error
|
||||
|
||||
for {
|
||||
fmt.Print("Enter ZapDB password: ")
|
||||
zapPass, err = term.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to read ZapDB password", err)
|
||||
}
|
||||
fmt.Println() // Add newline after password input
|
||||
|
||||
if len(zapPass) == 0 {
|
||||
fmt.Println("Password cannot be empty. Please try again.")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Print("Confirm ZapDB password: ")
|
||||
confirmPass, err = term.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to read confirmation password", err)
|
||||
}
|
||||
fmt.Println() // Add newline after password input
|
||||
|
||||
if string(zapPass) != string(confirmPass) {
|
||||
fmt.Println("Passwords do not match. Please try again.")
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Show masked password for confirmation
|
||||
maskedPassword := maskString(string(zapPass))
|
||||
fmt.Printf("Password set: %s\n", maskedPassword)
|
||||
|
||||
viper.Set("zapdb_password", string(zapPass))
|
||||
|
||||
// Prompt for initiator public key (using regular input since it's not as sensitive)
|
||||
var initiatorKey string
|
||||
fmt.Print("Enter event initiator public key (hex): ")
|
||||
if _, err := fmt.Scanln(&initiatorKey); err != nil {
|
||||
logger.Fatal("Failed to read initiator key", err)
|
||||
}
|
||||
|
||||
if initiatorKey == "" {
|
||||
logger.Fatal("Initiator public key cannot be empty", nil)
|
||||
}
|
||||
|
||||
// Show masked key for confirmation
|
||||
maskedKey := maskString(initiatorKey)
|
||||
fmt.Printf("Event initiator public key set: %s\n", maskedKey)
|
||||
|
||||
viper.Set("event_initiator_pubkey", initiatorKey)
|
||||
fmt.Println("\n✓ Configuration complete!")
|
||||
}
|
||||
|
||||
// maskString shows the first and last character of a string, replacing the middle with asterisks
|
||||
func maskString(s string) string {
|
||||
if len(s) <= 2 {
|
||||
return s // Too short to mask
|
||||
}
|
||||
|
||||
masked := s[0:1]
|
||||
for i := 0; i < len(s)-2; i++ {
|
||||
masked += "*"
|
||||
}
|
||||
masked += s[len(s)-1:]
|
||||
|
||||
return masked
|
||||
}
|
||||
|
||||
// Check required configuration values are present.
|
||||
// checkRequiredConfigValues verifies required viper config values are present.
|
||||
// skipPasswordCheck should be true when ZapDB password will be resolved via HSM provider.
|
||||
func checkRequiredConfigValues(skipPasswordCheck bool) {
|
||||
// Show warning if we're using file-based config but no password is set
|
||||
@@ -577,49 +219,6 @@ func resolveZapDBPassword(ctx context.Context, c *cli.Command) string {
|
||||
return password
|
||||
}
|
||||
|
||||
func NewConsulClient(addr string) *api.Client {
|
||||
// Create a new Consul client
|
||||
consulConfig := api.DefaultConfig()
|
||||
consulConfig.Address = addr
|
||||
consulClient, err := api.NewClient(consulConfig)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to create consul client", err)
|
||||
}
|
||||
logger.Info("Connected to consul!")
|
||||
return consulClient
|
||||
}
|
||||
|
||||
// Deprecated: LoadPeersFromConsul is used by legacy mode only.
|
||||
func LoadPeersFromConsul(consulClient *api.Client) []config.Peer { // Create a Consul Key-Value store client
|
||||
kv := consulClient.KV()
|
||||
peers, err := config.LoadPeersFromConsul(kv, "mpc_peers/")
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to load peers from Consul", err)
|
||||
}
|
||||
logger.Info("Loaded peers from consul", "peers", peers)
|
||||
|
||||
return peers
|
||||
}
|
||||
|
||||
func GetPeerIDs(peers []config.Peer) []string {
|
||||
var peersIDs []string
|
||||
for _, peer := range peers {
|
||||
peersIDs = append(peersIDs, peer.ID)
|
||||
}
|
||||
return peersIDs
|
||||
}
|
||||
|
||||
// Given node name, loop through peers and find the matching ID
|
||||
func GetIDFromName(name string, peers []config.Peer) string {
|
||||
// Get nodeID from node name
|
||||
nodeID := config.GetNodeID(name, peers)
|
||||
if nodeID == "" {
|
||||
logger.Fatal("Empty Node ID", fmt.Errorf("node ID not found for name %s", name))
|
||||
}
|
||||
|
||||
return nodeID
|
||||
}
|
||||
|
||||
func NewZapKV(nodeName, nodeID, password string) *kvstore.Store {
|
||||
// ZapDB KV store
|
||||
// Use configured db_path or default to current directory + "db"
|
||||
@@ -678,39 +277,7 @@ func StartPeriodicBackup(ctx context.Context, zapKV *kvstore.Store, periodSecond
|
||||
return backupCancel
|
||||
}
|
||||
|
||||
// Deprecated: GetNATSConnection is used by legacy mode only.
|
||||
func GetNATSConnection(environment string) (*nats.Conn, error) {
|
||||
url := viper.GetString("nats.url")
|
||||
opts := []nats.Option{
|
||||
nats.MaxReconnects(-1), // retry forever
|
||||
nats.ReconnectWait(2 * time.Second),
|
||||
nats.DisconnectHandler(func(nc *nats.Conn) {
|
||||
logger.Warn("Disconnected from NATS")
|
||||
}),
|
||||
nats.ReconnectHandler(func(nc *nats.Conn) {
|
||||
logger.Info("Reconnected to NATS", "url", nc.ConnectedUrl())
|
||||
}),
|
||||
nats.ClosedHandler(func(nc *nats.Conn) {
|
||||
logger.Info("NATS connection closed!")
|
||||
}),
|
||||
}
|
||||
|
||||
if environment == constant.EnvProduction {
|
||||
clientCert := filepath.Join(".", "certs", "client-cert.pem")
|
||||
clientKey := filepath.Join(".", "certs", "client-key.pem")
|
||||
caCert := filepath.Join(".", "certs", "rootCA.pem")
|
||||
|
||||
opts = append(opts,
|
||||
nats.ClientCert(clientCert, clientKey),
|
||||
nats.RootCAs(caCert),
|
||||
nats.UserInfo(viper.GetString("nats.username"), viper.GetString("nats.password")),
|
||||
)
|
||||
}
|
||||
|
||||
return nats.Connect(url, opts...)
|
||||
}
|
||||
|
||||
// runNodeConsensus runs the MPC node with consensus-embedded transport (no NATS/Consul)
|
||||
// runNodeConsensus runs the MPC node with consensus-embedded transport
|
||||
func runNodeConsensus(ctx context.Context, c *cli.Command) error {
|
||||
nodeID := c.String("node-id")
|
||||
listenAddr := c.String("listen")
|
||||
@@ -1093,85 +660,73 @@ func runNodeConsensus(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Start Dashboard API server if PostgreSQL URL is provided
|
||||
apiDBURL := c.String("api-db")
|
||||
if apiDBURL == "" {
|
||||
apiDBURL = os.Getenv("MPC_API_DB")
|
||||
// Start Dashboard API server (ZapDB-backed, no external dependencies)
|
||||
apiListenAddr := c.String("api-listen")
|
||||
jwtSecret := c.String("jwt-secret")
|
||||
if jwtSecret == "" {
|
||||
jwtSecret = os.Getenv("MPC_JWT_SECRET")
|
||||
}
|
||||
if apiDBURL != "" {
|
||||
apiListenAddr := c.String("api-listen")
|
||||
jwtSecret := c.String("jwt-secret")
|
||||
if jwtSecret == "" {
|
||||
jwtSecret = os.Getenv("MPC_JWT_SECRET")
|
||||
}
|
||||
if jwtSecret == "" {
|
||||
return fmt.Errorf("--jwt-secret is required (set JWT_SECRET or MPC_JWT_SECRET env var, source from KMS)")
|
||||
}
|
||||
|
||||
apiKVAddr := c.String("api-kv")
|
||||
if apiKVAddr == "" {
|
||||
apiKVAddr = os.Getenv("MPC_API_KV")
|
||||
}
|
||||
database, err := db.New(apiDBURL, apiKVAddr)
|
||||
if jwtSecret != "" {
|
||||
dbPath := filepath.Join(dataDir, "dashboard.db")
|
||||
database, err := db.New(dbPath, "")
|
||||
if err != nil {
|
||||
logger.Error("Failed to connect to dashboard database", err)
|
||||
logger.Error("Failed to open dashboard database", err)
|
||||
} else {
|
||||
defer database.Close()
|
||||
{
|
||||
mpcBackend := &ConsensusMPCBackend{
|
||||
pubSub: pubSub,
|
||||
peerRegistry: peerRegistry,
|
||||
factory: factory,
|
||||
keyInfoStore: factory.KeyInfoStore(),
|
||||
identity: consensusIdentity,
|
||||
nodeID: nodeID,
|
||||
threshold: threshold,
|
||||
}
|
||||
|
||||
apiServer := mpcapi.NewServer(database, mpcBackend, jwtSecret)
|
||||
apiServer.StartScheduler(ctx)
|
||||
|
||||
// Wire HSM signer for intent co-signing
|
||||
signerType := c.String("hsm-signer")
|
||||
if signerType != "" {
|
||||
signer, signerErr := hsm.NewSigner(signerType, nil)
|
||||
if signerErr != nil {
|
||||
logger.Error("Failed to create HSM signer", signerErr, "provider", signerType)
|
||||
} else {
|
||||
apiServer.SetHSM(signer)
|
||||
logger.Info("HSM signer configured for co-signing", "provider", signer.Provider())
|
||||
}
|
||||
}
|
||||
|
||||
// HSM threshold attestation (key share vault storage)
|
||||
if c.Bool("hsm-attest") {
|
||||
logger.Info("HSM threshold attestation enabled",
|
||||
"signer", c.String("hsm-signer"),
|
||||
"attest_key", c.String("hsm-signer-key-id"),
|
||||
"vault_provider", c.String("hsm-provider"),
|
||||
)
|
||||
}
|
||||
|
||||
// Mount chi handler on Base
|
||||
os.Args = []string{"mpcd", "serve", "--http", apiListenAddr}
|
||||
baseApp := base.New()
|
||||
baseApp.OnServe().BindFunc(func(e *core.ServeEvent) error {
|
||||
e.Router.Any("/{path...}", func(re *core.RequestEvent) error {
|
||||
apiServer.Handler().ServeHTTP(re.Response, re.Request)
|
||||
return nil
|
||||
})
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
logger.Info("Dashboard API starting (Base)", "addr", apiListenAddr)
|
||||
go func() {
|
||||
if err := baseApp.Start(); err != nil {
|
||||
logger.Error("Dashboard API server failed", err)
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info("Dashboard API ready", "addr", apiListenAddr)
|
||||
mpcBackend := &ConsensusMPCBackend{
|
||||
pubSub: pubSub,
|
||||
peerRegistry: peerRegistry,
|
||||
factory: factory,
|
||||
keyInfoStore: factory.KeyInfoStore(),
|
||||
identity: consensusIdentity,
|
||||
nodeID: nodeID,
|
||||
threshold: threshold,
|
||||
}
|
||||
|
||||
apiServer := mpcapi.NewServer(database, mpcBackend, jwtSecret)
|
||||
apiServer.StartScheduler(ctx)
|
||||
|
||||
// Wire HSM signer for intent co-signing
|
||||
signerType := c.String("hsm-signer")
|
||||
if signerType != "" {
|
||||
signer, signerErr := hsm.NewSigner(signerType, nil)
|
||||
if signerErr != nil {
|
||||
logger.Error("Failed to create HSM signer", signerErr, "provider", signerType)
|
||||
} else {
|
||||
apiServer.SetHSM(signer)
|
||||
logger.Info("HSM signer configured for co-signing", "provider", signer.Provider())
|
||||
}
|
||||
}
|
||||
|
||||
// HSM threshold attestation (key share vault storage)
|
||||
if c.Bool("hsm-attest") {
|
||||
logger.Info("HSM threshold attestation enabled",
|
||||
"signer", c.String("hsm-signer"),
|
||||
"attest_key", c.String("hsm-signer-key-id"),
|
||||
"vault_provider", c.String("hsm-provider"),
|
||||
)
|
||||
}
|
||||
|
||||
// Mount chi handler on Base
|
||||
os.Args = []string{"mpcd", "serve", "--http", apiListenAddr}
|
||||
baseApp := base.New()
|
||||
baseApp.OnServe().BindFunc(func(e *core.ServeEvent) error {
|
||||
e.Router.Any("/{path...}", func(re *core.RequestEvent) error {
|
||||
apiServer.Handler().ServeHTTP(re.Response, re.Request)
|
||||
return nil
|
||||
})
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
logger.Info("Dashboard API starting (Base+SQLite)", "addr", apiListenAddr)
|
||||
go func() {
|
||||
if err := baseApp.Start(); err != nil {
|
||||
logger.Error("Dashboard API server failed", err)
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info("Dashboard API ready", "addr", apiListenAddr, "db", dbPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1792,228 +1347,3 @@ func eddsaPubKeyToSolAddress(pubKey []byte) string {
|
||||
return base58.Encode(pubKey)
|
||||
}
|
||||
|
||||
// runAPIOnly starts only the Dashboard API server without any MPC transport.
|
||||
// This is used for the cloud dashboard deployment where MPC operations are
|
||||
// forwarded to the MPC nodes separately.
|
||||
func runAPIOnly(ctx context.Context, c *cli.Command) error {
|
||||
debug := c.Bool("debug")
|
||||
logger.Init("api", debug)
|
||||
|
||||
dbURL := c.String("db")
|
||||
if dbURL == "" {
|
||||
return fmt.Errorf("--db (or DATABASE_URL env) is required")
|
||||
}
|
||||
listenAddr := c.String("listen")
|
||||
jwtSecret := c.String("jwt-secret")
|
||||
if jwtSecret == "" {
|
||||
return fmt.Errorf("--jwt-secret (or JWT_SECRET env) is required")
|
||||
}
|
||||
|
||||
logger.Info("Starting Dashboard API server (standalone)", "addr", listenAddr)
|
||||
|
||||
database, err := db.New(dbURL, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
logger.Info("Database connected")
|
||||
|
||||
// Determine MPC backend: if cluster-url is provided, forward to the cluster;
|
||||
// otherwise use a stub that returns descriptive errors.
|
||||
clusterURL := c.String("cluster-url")
|
||||
clusterAPIKey := c.String("cluster-api-key")
|
||||
|
||||
var mpcBackend mpcapi.MPCBackend
|
||||
if clusterURL != "" {
|
||||
mpcBackend = newAPIOnlyMPCBackend(clusterURL, clusterAPIKey)
|
||||
logger.Info("API-only mode: forwarding MPC operations to cluster", "url", clusterURL)
|
||||
} else {
|
||||
mpcBackend = &stubMPCBackend{}
|
||||
logger.Warn("API-only mode: no MPC_CLUSTER_URL set, MPC operations will fail")
|
||||
}
|
||||
|
||||
apiServer := mpcapi.NewServer(database, mpcBackend, jwtSecret)
|
||||
apiServer.StartScheduler(ctx)
|
||||
|
||||
// Wire HSM signer for intent co-signing
|
||||
signerType := os.Getenv("MPC_HSM_SIGNER")
|
||||
if signerType == "" {
|
||||
signerType = "local"
|
||||
}
|
||||
signer, signerErr := hsm.NewSigner(signerType, nil)
|
||||
if signerErr != nil {
|
||||
logger.Error("Failed to create HSM signer", signerErr, "provider", signerType)
|
||||
} else {
|
||||
apiServer.SetHSM(signer)
|
||||
logger.Info("HSM signer configured for co-signing", "provider", signer.Provider())
|
||||
}
|
||||
|
||||
// Mount chi handler on Base
|
||||
os.Args = []string{"mpcd", "serve", "--http", listenAddr}
|
||||
baseApp := base.New()
|
||||
baseApp.OnServe().BindFunc(func(e *core.ServeEvent) error {
|
||||
e.Router.Any("/{path...}", func(re *core.RequestEvent) error {
|
||||
apiServer.Handler().ServeHTTP(re.Response, re.Request)
|
||||
return nil
|
||||
})
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
logger.Info("Dashboard API starting (Base)", "addr", listenAddr)
|
||||
|
||||
// Base owns the process lifecycle (blocks until shutdown)
|
||||
if err := baseApp.Start(); err != nil {
|
||||
return fmt.Errorf("API server failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stubMPCBackend returns errors for MPC operations when no cluster URL is configured.
|
||||
type stubMPCBackend struct{}
|
||||
|
||||
func (s *stubMPCBackend) TriggerKeygen(orgID, walletID string) (*mpcapi.KeygenResult, error) {
|
||||
return nil, fmt.Errorf("MPC operations not available: set MPC_CLUSTER_URL to enable forwarding")
|
||||
}
|
||||
|
||||
func (s *stubMPCBackend) TriggerSign(orgID, walletID string, payload []byte) (*mpcapi.SignResult, error) {
|
||||
return nil, fmt.Errorf("MPC operations not available: set MPC_CLUSTER_URL to enable forwarding")
|
||||
}
|
||||
|
||||
func (s *stubMPCBackend) TriggerReshare(orgID, walletID string, newThreshold int, newParticipants []string) error {
|
||||
return fmt.Errorf("MPC operations not available: set MPC_CLUSTER_URL to enable forwarding")
|
||||
}
|
||||
|
||||
func (s *stubMPCBackend) ExportKeyShare(orgID, walletID string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("MPC operations not available: set MPC_CLUSTER_URL to enable forwarding")
|
||||
}
|
||||
|
||||
func (s *stubMPCBackend) GetClusterStatus() *mpcapi.ClusterStatus {
|
||||
return &mpcapi.ClusterStatus{
|
||||
NodeID: "api-only",
|
||||
Mode: "api-only",
|
||||
Ready: false,
|
||||
Version: Version,
|
||||
}
|
||||
}
|
||||
|
||||
// apiOnlyMPCBackend forwards MPC operations to the actual MPC cluster via HTTP.
|
||||
type apiOnlyMPCBackend struct {
|
||||
clusterURL string
|
||||
apiKey string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func newAPIOnlyMPCBackend(clusterURL, apiKey string) *apiOnlyMPCBackend {
|
||||
// Strip trailing slash from cluster URL
|
||||
clusterURL = strings.TrimRight(clusterURL, "/")
|
||||
return &apiOnlyMPCBackend{
|
||||
clusterURL: clusterURL,
|
||||
apiKey: apiKey,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 120 * time.Second, // MPC keygen can take ~30s; allow generous timeout
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// doRequest sends an HTTP request to the MPC cluster and decodes the JSON response.
|
||||
func (a *apiOnlyMPCBackend) doRequest(method, path string, reqBody interface{}, respBody interface{}) error {
|
||||
var bodyReader *strings.Reader
|
||||
if reqBody != nil {
|
||||
data, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
bodyReader = strings.NewReader(string(data))
|
||||
} else {
|
||||
bodyReader = strings.NewReader("")
|
||||
}
|
||||
|
||||
url := a.clusterURL + path
|
||||
req, err := http.NewRequest(method, url, bodyReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if a.apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+a.apiKey)
|
||||
}
|
||||
|
||||
resp, err := a.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cluster request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
var errBody struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&errBody)
|
||||
if errBody.Error != "" {
|
||||
return fmt.Errorf("cluster returned %d: %s", resp.StatusCode, errBody.Error)
|
||||
}
|
||||
return fmt.Errorf("cluster returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if respBody != nil {
|
||||
if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil {
|
||||
return fmt.Errorf("failed to decode cluster response: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *apiOnlyMPCBackend) TriggerKeygen(orgID, walletID string) (*mpcapi.KeygenResult, error) {
|
||||
reqBody := map[string]string{"org_id": orgID, "wallet_id": walletID}
|
||||
var result mpcapi.KeygenResult
|
||||
// Internal MPC API on port 9800 uses /keygen (not /v1/keygen)
|
||||
if err := a.doRequest("POST", "/keygen", reqBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("keygen forwarding failed: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (a *apiOnlyMPCBackend) TriggerSign(orgID, walletID string, payload []byte) (*mpcapi.SignResult, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
"org_id": orgID,
|
||||
"wallet_id": walletID,
|
||||
"payload": hex.EncodeToString(payload),
|
||||
}
|
||||
var result mpcapi.SignResult
|
||||
if err := a.doRequest("POST", "/v1/sign", reqBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("sign forwarding failed: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (a *apiOnlyMPCBackend) TriggerReshare(orgID, walletID string, newThreshold int, newParticipants []string) error {
|
||||
reqBody := map[string]interface{}{
|
||||
"org_id": orgID,
|
||||
"wallet_id": walletID,
|
||||
"new_threshold": newThreshold,
|
||||
"new_participants": newParticipants,
|
||||
}
|
||||
if err := a.doRequest("POST", "/v1/reshare", reqBody, nil); err != nil {
|
||||
return fmt.Errorf("reshare forwarding failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *apiOnlyMPCBackend) ExportKeyShare(orgID, walletID string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("ExportKeyShare not available in api-only mode; use consensus mode for backup operations")
|
||||
}
|
||||
|
||||
func (a *apiOnlyMPCBackend) GetClusterStatus() *mpcapi.ClusterStatus {
|
||||
var status mpcapi.ClusterStatus
|
||||
// Internal MPC API on port 9800 uses /health (not /v1/status)
|
||||
logger.Warn("Failed to get cluster status", "err", err, "url", a.clusterURL)
|
||||
return &mpcapi.ClusterStatus{
|
||||
NodeID: "api-only",
|
||||
Mode: "api-only-proxy",
|
||||
Ready: false,
|
||||
Version: Version,
|
||||
}
|
||||
}
|
||||
return &status
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ require (
|
||||
github.com/gtank/ristretto255 v0.2.0
|
||||
github.com/hanzoai/base v0.39.1
|
||||
github.com/hanzoai/dbx v1.13.0
|
||||
github.com/hanzoai/kv-go/v9 v9.18.0
|
||||
github.com/hanzoai/orm v0.3.2
|
||||
github.com/hashicorp/consul/api v1.33.7
|
||||
github.com/luxfi/crypto v1.17.45
|
||||
@@ -117,6 +116,7 @@ require (
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/gtank/merlin v0.1.1 // indirect
|
||||
github.com/hanzoai/kv-go/v9 v9.18.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
|
||||
@@ -154,7 +154,7 @@ require (
|
||||
github.com/luxfi/sampler v1.0.0 // indirect
|
||||
github.com/luxfi/utils v1.1.4 // indirect
|
||||
github.com/luxfi/zap v0.2.0 // indirect
|
||||
github.com/luxfi/zapdb v1.0.0 // indirect
|
||||
github.com/luxfi/zapdb/v4 v4.9.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
|
||||
+34
@@ -472,10 +472,14 @@ github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU
|
||||
github.com/luxfi/ai v0.0.0-20251225021023-3f15131f2bd1/go.mod h1:4+FVvnCo1QqM3NaVUte0da4zgH2IblVLmUxKQi0BwE0=
|
||||
github.com/luxfi/api v1.0.1/go.mod h1:N2Ymyqo3SoB6/DmPXRV6W377k2qNqZujSP/B8fbIB9U=
|
||||
github.com/luxfi/atomic v0.0.1/go.mod h1:UzFk1K7VXjJhjm2c4Yiu5HydKbWi36kFtrlDKbm8MG8=
|
||||
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
|
||||
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
|
||||
github.com/luxfi/bft v0.1.0/go.mod h1:IUT167Erk2h8z36197Yrmu/3CBIsZku5WlEG8O2NMp4=
|
||||
github.com/luxfi/cache v1.1.0/go.mod h1:9GvlEEE9rFPaaWxvVpSPwW8ZMo2+8VMNNcuPa4AwzPg=
|
||||
github.com/luxfi/cache v1.2.0/go.mod h1:Esn48WMB3JL/+UOdCEV2Chyjr1MG3SSckXWLhB/P080=
|
||||
github.com/luxfi/codec v1.1.2/go.mod h1:T+DV0Jau5bi+8wMcaoj51zS1rgt80VovQKzaN2CV5Mg=
|
||||
github.com/luxfi/codec v1.1.4 h1:Yl8ZalMNkqo7cD6R9AjczAajkLOmsjyZ9+DASVYHrvg=
|
||||
github.com/luxfi/codec v1.1.4/go.mod h1:oGQ3j6E8c2P0pL0irYtWkrB1hmDUFIE0puXHK4gV5KI=
|
||||
github.com/luxfi/compress v0.0.2/go.mod h1:cfa7L3STdZdsMwJc9faZML1modLNh1n2u72n/DOfuNU=
|
||||
github.com/luxfi/compress v0.0.4/go.mod h1:/eN0eca98igoE6DDsLv/4w7n1gsQKwa+HUTxmWz/iWw=
|
||||
github.com/luxfi/concurrent v0.0.2/go.mod h1:SwEDrQXVAXgBiObXVg0pfmLzU1xsa9ZeBRptkBdyNUU=
|
||||
@@ -488,6 +492,8 @@ github.com/luxfi/consensus v1.22.62/go.mod h1:sEAGM6n/tGXx7Sd5d7F22862rqT8n2y+Z1
|
||||
github.com/luxfi/consensus v1.22.67/go.mod h1:mUffRVhwE3zz8bHwAIXT0xSroqOceszepHl8Tp6GwrI=
|
||||
github.com/luxfi/const v1.4.1/go.mod h1:qWfecjdImPY3kXsy3UAe4ZVpMcsp2/iUlHwUMPVREoI=
|
||||
github.com/luxfi/constants v1.4.2/go.mod h1:xk+0eA7UY1GkHrmI7yqR+9LDe4TUe3p1VfRvs6cJbNM=
|
||||
github.com/luxfi/constants v1.4.7 h1:e/Qs+DQP3pugle3Zncq6fZCxKgqqtbyD/z7Gm4ZjsYg=
|
||||
github.com/luxfi/constants v1.4.7/go.mod h1:hOszZ2NDQ8gMZKncfcZ67PXkb5OIbnwAzXC3oFbQwW0=
|
||||
github.com/luxfi/container v0.0.2/go.mod h1:nnK+TxHmGry0mGnfun4jx0tLqWJgV/GbZXTETMuFJn4=
|
||||
github.com/luxfi/coreth v0.15.71/go.mod h1:Z/OTRrw+q+uSNrCh1Fg0hkMf1fRquzbTQelx7wR0HOM=
|
||||
github.com/luxfi/crypto v1.17.28/go.mod h1:v6eaW5ejuzW2gecChWKQqx4CtVhoM1b+e40ro/6WVPg=
|
||||
@@ -495,39 +501,57 @@ github.com/luxfi/crypto v1.17.36/go.mod h1:UHzltsXPDF/tGcmTiL/DbkClAflr3YmvOaXlw
|
||||
github.com/luxfi/crypto v1.17.37/go.mod h1:vu+pCHdyPQZULzrA+K+lTFYrg8vOtNdu+/5Vko8RzZ0=
|
||||
github.com/luxfi/crypto v1.17.38/go.mod h1:G2t1GQvPsrwnzwyVEj0LQDuX2AWZVI5kEAPyVeicc5o=
|
||||
github.com/luxfi/crypto v1.17.39/go.mod h1:mChLWmW4CLR1wAN6CeJTveCzUv0DTzGQnYgq01x3W0U=
|
||||
github.com/luxfi/crypto v1.17.45 h1:uGK0y4+aLipE/M0YIQ5hcsWv0ZG0E4cPv03a94K/eLE=
|
||||
github.com/luxfi/crypto v1.17.45/go.mod h1:GnAkhQ7HNs3X0Tzx5nOONS3kl0yRmWHbDcRO5ffILsg=
|
||||
github.com/luxfi/czmq/v4 v4.2.2/go.mod h1:WFY9ldUbeU900P8bcIRr7Jim23veOhJL5A5zSpTogdU=
|
||||
github.com/luxfi/database v1.17.35/go.mod h1:dYxaF30KGJQzuxA3VH7+WGU5/dhwRyzLgzSk9F3wje0=
|
||||
github.com/luxfi/database v1.17.38/go.mod h1:q6BJNN+47VtkmkGrswQmC0ikERYAdlmZCYhGTXkF4mM=
|
||||
github.com/luxfi/database v1.17.39/go.mod h1:iHE5evTMxltmYGe3QkewrZv1xgGrDBbqcv0y4NkDJcI=
|
||||
github.com/luxfi/database v1.17.40/go.mod h1:du1q0PemBc0gZcdtHhsARs6mIsZM3a5R/C040DVYLCM=
|
||||
github.com/luxfi/database v1.17.42/go.mod h1:0nArLBm0r42J103oHR2xkAe8+mftuz5jtFgNylU1hgg=
|
||||
github.com/luxfi/database v1.17.44 h1:hfiTls7sqbweW+o4iaZqB8P997paC+vpgWmhN6v5MJ0=
|
||||
github.com/luxfi/database v1.17.44/go.mod h1:6Ey5y3I0WNLHbxIlIdFqUuKfBg+b0fAgTA8FgRgQ8zg=
|
||||
github.com/luxfi/evm v0.8.30/go.mod h1:S0fChMeuWV4XqY4OB6RozqvyHcm+imJCKyqsMY713hY=
|
||||
github.com/luxfi/fhe v1.7.6-0.20260106060801-28e308e4c2f8/go.mod h1:SVO5KV3GKMW5U0XeQSV4x7fryNWHsCjyhhhAoIPCpG8=
|
||||
github.com/luxfi/fhe v1.7.7 h1:RP26i3CxvNy2y/3LKiJPgd9uR5MrlG/MvQnv5rkQFhk=
|
||||
github.com/luxfi/fhe v1.7.7/go.mod h1:FHfGNgKL5ZLcnKVKFVNLDJOBbQyQm5DGpVrMnY8ASJo=
|
||||
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
|
||||
github.com/luxfi/formatting v1.0.0/go.mod h1:lswh6dPQ4E9Q+nZLwispV+Jdthr5R1PkYTBA/LhuSgE=
|
||||
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
|
||||
github.com/luxfi/genesis v1.5.21/go.mod h1:XI1T2oMh/VS/f+LWMWPDQIEhEneCNVHodhn2Qr3rFMU=
|
||||
github.com/luxfi/genesis v1.5.22/go.mod h1:ernOqYxKPEZnwu/QvgJ9y4Lb+IUpfoOtZ3/DubzExv0=
|
||||
github.com/luxfi/geth v1.16.73/go.mod h1:CPzps7Zp3I7Og3KT3m+Qs41WW8z+YPGx1TOJQnHdBHY=
|
||||
github.com/luxfi/geth v1.16.79 h1:MtP8ZUuSVZDjmZDa1kTJCl0PpG8+wGbQft652GO6a3A=
|
||||
github.com/luxfi/geth v1.16.79/go.mod h1:6xNi4sHoh0v1kBf0TOjmFhBg5+4umbSqrwiFZsDanZ0=
|
||||
github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk=
|
||||
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
|
||||
github.com/luxfi/gpu v0.30.0/go.mod h1:7pFsHqra1Vrmy2aGXVEPKeKsPR+Bn+QmBXuByK3fdPA=
|
||||
github.com/luxfi/hid v0.9.3/go.mod h1:XJ/7DZAHf5dggm3zWNbitKuFGB7J96b4iX+8NO3BsnY=
|
||||
github.com/luxfi/hsm v1.1.1 h1:sIJZLSHD844/DdLx70tA2j8Ql1q3oy1zmsuV4TwVNXU=
|
||||
github.com/luxfi/hsm v1.1.1/go.mod h1:+tB7UEipw6h4cGNRFhvQsviM95fnseyMt894bqiRrbU=
|
||||
github.com/luxfi/ids v1.2.7/go.mod h1:svLsj7e6ixJVfRYaQqv9RWjBIW11Vz738+d08BVpeCg=
|
||||
github.com/luxfi/ids v1.2.8/go.mod h1:14vKgM828w7yqnh6z0LhNrObXHVvtyRDyWBEmWBj5i8=
|
||||
github.com/luxfi/keychain v1.0.1/go.mod h1:X7HLk5QRZ1VHJxovAyBL6z7f/nlosoXlveEtvUGhoaw=
|
||||
github.com/luxfi/keychain v1.0.2/go.mod h1:q/4ULgZBlstKkwzOzG/0T6y73BDPgnkrcibbJyTvmbU=
|
||||
github.com/luxfi/keys v1.0.7/go.mod h1:Faz87SfMIzVpE+2uIsYQnw+36rRhQNKydd/3mEEXPbg=
|
||||
github.com/luxfi/lattice/v7 v7.0.1 h1:RsK+YR0+fKdey08t95Is0GdiinXEvyYY/dXDbHDdmJg=
|
||||
github.com/luxfi/lattice/v7 v7.0.1/go.mod h1:eGIdt34H0QsHlvSsB9PAbE0Bxa7BLOsj6HVHqbFS/5I=
|
||||
github.com/luxfi/ledger v1.1.6/go.mod h1:hyNV+4a6nI3yfwhyJcmXICwJeWfnxA1PbMZEOZ1VFnw=
|
||||
github.com/luxfi/log v1.2.1/go.mod h1:0vJzb4Hl9fvgwWDMKcbCD/SYH3bO0iSmgZMcYdfdl1o=
|
||||
github.com/luxfi/log v1.4.0/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
|
||||
github.com/luxfi/lpm v1.0.5/go.mod h1:dS/NS6muB9zlHWbO/VeCMYCe53YwIjXDHg/jif7TejI=
|
||||
github.com/luxfi/math v1.2.2/go.mod h1:C8STnF2H+D6rqBPt248CiWY2TGuJgdtv/+4UqrT15iM=
|
||||
github.com/luxfi/math v1.2.4 h1:iVz7s0gToCNUU/2cLT8gUa8MLzc0YRp4jBmeXBeKdXk=
|
||||
github.com/luxfi/math v1.2.4/go.mod h1:i+Am9YvFsqDE75ZW8MPE62XCXGukXNiN/7mD9SKxxZY=
|
||||
github.com/luxfi/math/safe v0.0.1/go.mod h1:EejrmOJHh03YAD8+Zww8cPcMR1K3Q2I7w1dX4sMloeo=
|
||||
github.com/luxfi/mdns v0.1.0 h1:VB3mQcETc9j5SY1S6lAgFtuGr/rjWuDgPYnxS+OKWMQ=
|
||||
github.com/luxfi/mdns v0.1.0/go.mod h1:/3dheKVjUk2yiS/ocH1IDzeLXOIe+kpVsErIGDOZdiQ=
|
||||
github.com/luxfi/metric v1.4.9/go.mod h1:JlAAc4Jm5apoU2DHEgt57UHD7+8AjMdpP2rQJ/zaACY=
|
||||
github.com/luxfi/metric v1.4.10/go.mod h1:3OIQ+e6mciesm3cik3Q4CSOAvTlBy4JA0ZoKzCB9APo=
|
||||
github.com/luxfi/metric v1.4.11/go.mod h1:EAFmkzd5EX654G48HXKIsyJufOUa4sxmOJKvwSRbjng=
|
||||
github.com/luxfi/metric v1.4.12/go.mod h1:EAFmkzd5EX654G48HXKIsyJufOUa4sxmOJKvwSRbjng=
|
||||
github.com/luxfi/metric v1.5.1 h1:6tVarXMnNR3Xzud8FYUHjtXabTll3HI/OEqG0tgLAcI=
|
||||
github.com/luxfi/metric v1.5.1/go.mod h1:PkD4D4JoGuyKtfUkqPNYkrg9xKrJeNVZFdW/5XvAe/A=
|
||||
github.com/luxfi/mock v0.1.0/go.mod h1:izF+9K0gGzFC9zERn6Po37v46eLdPB+EIsDjL3GLk+U=
|
||||
github.com/luxfi/net v0.0.1/go.mod h1:JTPsJHaUcpF0HruczW14FlUTjPk78xF73XGSMQa86NA=
|
||||
github.com/luxfi/netrunner v1.14.35/go.mod h1:vfeCr7L49R9yoSjZCpZe2+xtoNdPt4fstEtbKt7Jtls=
|
||||
@@ -561,6 +585,8 @@ github.com/luxfi/staking v1.0.0/go.mod h1:E+fm3upfQTOu0fLCr3Uo4xYgAm7r/vEzgcUpNA
|
||||
github.com/luxfi/staking v1.1.0/go.mod h1:xwYacmreoKoy31ELT6QPno6b5M8q7bg4zJrmmt524X4=
|
||||
github.com/luxfi/sys v0.0.0-20260110090042-50187ec5ffd8/go.mod h1:Y0UwjnEJYSbGM7IvJbKYLf8BWNVLfT8Oz00bEiDITHw=
|
||||
github.com/luxfi/threshold v1.5.1/go.mod h1:OsWmH2foqB2K4vVpHTjSHrVM4oveSzqOZb/k+fqNsYY=
|
||||
github.com/luxfi/threshold v1.6.0 h1:pq/n1iYowws+CXe/+slJodjPlJ152hFF94luHp2E82c=
|
||||
github.com/luxfi/threshold v1.6.0/go.mod h1:XWSfWM3n/tVggluh/LH7is4vYL85zmyMHCqfz2qrYVU=
|
||||
github.com/luxfi/timer v1.0.1/go.mod h1:PQ15H3AEUfvWVf0kv5WhIGPWmhr9BxBv/VKLyyekpk4=
|
||||
github.com/luxfi/tls v1.0.0/go.mod h1:QZXWuYFZPfGA9AyRIBXgIuXPrmvWWHtQkJtplZnTlWc=
|
||||
github.com/luxfi/tls v1.0.2/go.mod h1:t6Fr+KYKDZEz1KxciGHzRrj/KsQIx8V3iG/8YiSqpvU=
|
||||
@@ -570,6 +596,8 @@ github.com/luxfi/units v1.0.0/go.mod h1:tma28v4ed1tupdS0kpSeyO+u1wWK/g1NqODPbN1Y
|
||||
github.com/luxfi/upgrade v1.0.0/go.mod h1:DZF7dO4erhUEKf907B2e65k4/vGkCPkUngpvIEUS3eI=
|
||||
github.com/luxfi/utils v1.1.0/go.mod h1:ABqhBdGNig0CaDcnNYldv1byS0BTNi5IKPbJSPF1p98=
|
||||
github.com/luxfi/utils v1.1.1/go.mod h1:DL7634uXEcCu2tCBll0bmX0SVnMJeABbmhm5NkLMu78=
|
||||
github.com/luxfi/utils v1.1.4 h1:8OY0jXCkpp6OotN1Y++6DATJkvtEwzq2k0p56imJlAk=
|
||||
github.com/luxfi/utils v1.1.4/go.mod h1:c3yz1RjzrB+cs5GZm+q1T3/2cCKElO9vxm9yRRtgSEM=
|
||||
github.com/luxfi/utxo v0.0.1/go.mod h1:cwkUv/J6zNohRI7+xrj03wi39bm6CYkek+6XRhQrPTU=
|
||||
github.com/luxfi/utxo v0.2.3/go.mod h1:I23ZJND7kU6NUklD2nY24mU93jQXBPGu1c9Y1hjX9Vk=
|
||||
github.com/luxfi/validators v1.0.0/go.mod h1:QGppjtI/gqprbplWc10J+4OIK8UYWpv+53mfH2aKhy4=
|
||||
@@ -587,7 +615,13 @@ github.com/luxfi/warp v1.18.2/go.mod h1:qu3TZv98repNhIDf04I6pzXePSgHjzP+JoSfWmF4
|
||||
github.com/luxfi/warp v1.18.4/go.mod h1:5kXaZGYBBzOFyzPoDukVynvTTPskElrA8EQ5TVgovqc=
|
||||
github.com/luxfi/warp v1.18.5/go.mod h1:SFyC529HDvbP/TWRAdYQSyJUliMa5JKFRtBrTLEElp4=
|
||||
github.com/luxfi/zap v0.0.0-20260126200933-22a2a4c72dfb/go.mod h1:uvmQO9kVvOfBcqPhY52wx5Z1O6L/4Mv2tBrJDc3gVgM=
|
||||
github.com/luxfi/zap v0.2.0 h1:RzvOkp3EoN5UCkpnqfObHLM1sEHy7YcxXKuermhE/VA=
|
||||
github.com/luxfi/zap v0.2.0/go.mod h1:2hydPSa/XMCMtfW6/DC9M5Bt03N5h75QwCV5Vypsqr4=
|
||||
github.com/luxfi/zapdb v1.0.0 h1:u8TF4g/bWew5mQwiR/Mcu+QIFYTT6BP5Ty8oBaDgalA=
|
||||
github.com/luxfi/zapdb v1.0.0/go.mod h1:tT2WF2iSabQHrAYe9l7LHscaIBfN2YtCbdgOrCyCI2A=
|
||||
github.com/luxfi/zapdb/v4 v4.9.1/go.mod h1:gYJjLcFCxmUATZ6Zg7iZHwAbgBMxGVP1g8UOWomK70Y=
|
||||
github.com/luxfi/zapdb/v4 v4.9.3 h1:KV1DrbZqtZsZvrWyPw5D8WHR0DxzqW2jHjqx6obxgoM=
|
||||
github.com/luxfi/zapdb/v4 v4.9.3/go.mod h1:BrpLaDeYy+smON4ASDPCFKWrQ58CXhy3WNEPGna7vuU=
|
||||
github.com/luxfi/zmq/v4 v4.2.2/go.mod h1:omy8ymZAhx60lx/nPsWT6uOV11uNFVnjtls+Hi5KqaU=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
|
||||
+17
-47
@@ -4,65 +4,38 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
kv "github.com/hanzoai/kv-go/v9"
|
||||
"github.com/hanzoai/orm"
|
||||
ormdb "github.com/hanzoai/orm/db"
|
||||
)
|
||||
|
||||
// Database holds the ORM entity store and optional KV cache.
|
||||
// Database holds the ORM entity store backed by SQLite.
|
||||
type Database struct {
|
||||
ORM orm.DB
|
||||
KV kv.UniversalClient
|
||||
}
|
||||
|
||||
// New creates a Database. Detects backend from DSN:
|
||||
// - "" or "sqlite://..." or file path → SQLite (default, zero-config)
|
||||
// - "postgres://..." → PostgreSQL
|
||||
//
|
||||
// kvURL is optional — pass "" to skip KV cache.
|
||||
func New(dsn, kvURL string) (*Database, error) {
|
||||
var ormInstance orm.DB
|
||||
|
||||
// New creates a Database backed by SQLite.
|
||||
// dsn accepts "" (defaults to mpc.db), "sqlite://path", or a ".db" file path.
|
||||
// kvURL is ignored (retained for call-site compat, will be removed).
|
||||
func New(dsn, _ string) (*Database, error) {
|
||||
path := "mpc.db"
|
||||
switch {
|
||||
case dsn == "" || strings.HasPrefix(dsn, "sqlite://") || strings.HasSuffix(dsn, ".db"):
|
||||
// SQLite — default, zero external deps.
|
||||
path := "mpc.db"
|
||||
if strings.HasPrefix(dsn, "sqlite://") {
|
||||
path = strings.TrimPrefix(dsn, "sqlite://")
|
||||
} else if strings.HasSuffix(dsn, ".db") {
|
||||
path = dsn
|
||||
}
|
||||
sqliteDB, err := ormdb.NewSQLiteDB(&ormdb.SQLiteDBConfig{Path: path})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sqlite: %w", err)
|
||||
}
|
||||
ormInstance = orm.AdaptDB(sqliteDB)
|
||||
|
||||
case strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://"):
|
||||
sqlDB, err := ormdb.NewSQLDB(&ormdb.SQLConfig{
|
||||
DSN: dsn,
|
||||
MaxConns: 20,
|
||||
MinConns: 2,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("postgres: %w", err)
|
||||
}
|
||||
ormInstance = orm.AdaptDB(sqlDB)
|
||||
|
||||
case dsn == "":
|
||||
// default
|
||||
case strings.HasPrefix(dsn, "sqlite://"):
|
||||
path = strings.TrimPrefix(dsn, "sqlite://")
|
||||
case strings.HasSuffix(dsn, ".db"):
|
||||
path = dsn
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported DSN: %q (use sqlite://, postgres://, or .db path)", dsn)
|
||||
return nil, fmt.Errorf("unsupported DSN: %q (use sqlite:// or .db path)", dsn)
|
||||
}
|
||||
|
||||
var kvClient kv.UniversalClient
|
||||
if kvURL != "" {
|
||||
kvClient = kv.NewUniversalClient(&kv.UniversalOptions{
|
||||
Addrs: []string{kvURL},
|
||||
})
|
||||
sqliteDB, err := ormdb.NewSQLiteDB(&ormdb.SQLiteDBConfig{Path: path})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sqlite: %w", err)
|
||||
}
|
||||
|
||||
return &Database{
|
||||
ORM: ormInstance,
|
||||
KV: kvClient,
|
||||
ORM: orm.AdaptDB(sqliteDB),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -71,7 +44,4 @@ func (d *Database) Close() {
|
||||
if d.ORM != nil {
|
||||
d.ORM.Close()
|
||||
}
|
||||
if d.KV != nil {
|
||||
d.KV.Close()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -435,7 +435,7 @@ type DeviceEnrollment struct {
|
||||
func init() { orm.Register[DeviceEnrollment]("device-enrollment") }
|
||||
|
||||
// PendingTrade represents a trade awaiting user biometric approval before the
|
||||
// ATS shard co-signs. The trade is recorded in PostgreSQL; the MPC signing only
|
||||
// ATS shard co-signs. The trade is recorded in the database; the MPC signing only
|
||||
// happens after the user approves via Face ID / Touch ID on their device.
|
||||
//
|
||||
// Status flow: pending_approval → approved → signing → signed → settled
|
||||
|
||||
Reference in New Issue
Block a user