mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
- ws.go: EventBus pubsub + /v1/ws upgrade handler (stdlib, no deps) - zap_server.go: StartZAP(port) with opcodes 0x0060-0x0064 keygen, sign, status, wallets, intents over ZAP binary protocol - Events field on Server for cross-transport event broadcasting - Each consensus node serves full API (not separate mpc-api sidecar)
227 lines
5.9 KiB
Go
227 lines
5.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/hanzoai/orm"
|
|
|
|
"github.com/luxfi/mpc/pkg/db"
|
|
"github.com/luxfi/mpc/pkg/logger"
|
|
"github.com/luxfi/zap"
|
|
)
|
|
|
|
// ZAP opcodes for MPC API operations (0x0060-0x006F range).
|
|
const (
|
|
OpMPCKeygen uint16 = 0x0060
|
|
OpMPCSign uint16 = 0x0061
|
|
OpMPCStatus uint16 = 0x0062
|
|
OpMPCWallets uint16 = 0x0063
|
|
OpMPCIntent uint16 = 0x0064
|
|
)
|
|
|
|
// ZAP object field offsets (after the 8-byte req/resp correlation header).
|
|
const (
|
|
fieldOrgID = 8
|
|
fieldWalletID = 9
|
|
fieldPayload = 10
|
|
fieldIntentID = 11
|
|
|
|
fieldStatus = 8
|
|
fieldData = 9
|
|
fieldError = 10
|
|
)
|
|
|
|
// StartZAP creates a ZAP node on a separate port and registers MPC handlers.
|
|
func (s *Server) StartZAP(port int) error {
|
|
node := zap.NewNode(zap.NodeConfig{
|
|
NodeID: fmt.Sprintf("mpc-api-%d", port),
|
|
ServiceType: "_mpc._tcp",
|
|
Port: port,
|
|
NoDiscovery: true,
|
|
})
|
|
|
|
node.Handle(OpMPCKeygen, s.zapKeygen)
|
|
node.Handle(OpMPCSign, s.zapSign)
|
|
node.Handle(OpMPCStatus, s.zapStatus)
|
|
node.Handle(OpMPCWallets, s.zapWallets)
|
|
node.Handle(OpMPCIntent, s.zapIntent)
|
|
|
|
if err := node.Start(); err != nil {
|
|
return fmt.Errorf("zap start: %w", err)
|
|
}
|
|
logger.Info("ZAP server started", "port", port)
|
|
|
|
// Forward events to ZAP peers
|
|
go s.zapEventForwarder(node)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) zapKeygen(_ context.Context, _ string, msg *zap.Message) (*zap.Message, error) {
|
|
root := msg.Root()
|
|
orgID := root.Text(fieldOrgID)
|
|
walletID := root.Text(fieldWalletID)
|
|
if orgID == "" || walletID == "" {
|
|
return zapError("org_id and wallet_id required")
|
|
}
|
|
|
|
result, err := s.mpc.TriggerKeygen(orgID, walletID)
|
|
if err != nil {
|
|
return zapError(err.Error())
|
|
}
|
|
|
|
data, _ := json.Marshal(result)
|
|
s.Events.Publish(Event{Type: "wallet.created", OrgID: orgID, WalletID: walletID, Data: data})
|
|
return zapOK(data)
|
|
}
|
|
|
|
func (s *Server) zapSign(_ context.Context, _ string, msg *zap.Message) (*zap.Message, error) {
|
|
root := msg.Root()
|
|
orgID := root.Text(fieldOrgID)
|
|
walletID := root.Text(fieldWalletID)
|
|
payload := root.Bytes(fieldPayload)
|
|
if orgID == "" || walletID == "" || len(payload) == 0 {
|
|
return zapError("org_id, wallet_id, and payload required")
|
|
}
|
|
|
|
result, err := s.mpc.TriggerSign(orgID, walletID, payload)
|
|
if err != nil {
|
|
return zapError(err.Error())
|
|
}
|
|
|
|
data, _ := json.Marshal(result)
|
|
s.Events.Publish(Event{Type: "sign.complete", OrgID: orgID, WalletID: walletID, Data: data})
|
|
return zapOK(data)
|
|
}
|
|
|
|
func (s *Server) zapStatus(_ context.Context, _ string, _ *zap.Message) (*zap.Message, error) {
|
|
data, _ := json.Marshal(s.mpc.GetClusterStatus())
|
|
return zapOK(data)
|
|
}
|
|
|
|
func (s *Server) zapWallets(_ context.Context, _ string, msg *zap.Message) (*zap.Message, error) {
|
|
orgID := msg.Root().Text(fieldOrgID)
|
|
if orgID == "" {
|
|
return zapError("org_id required")
|
|
}
|
|
|
|
wallets, err := orm.TypedQuery[db.Wallet](s.db.ORM).
|
|
Filter("orgId=", orgID).
|
|
Order("-createdAt").
|
|
GetAll(context.Background())
|
|
if err != nil {
|
|
return zapError(err.Error())
|
|
}
|
|
if wallets == nil {
|
|
wallets = []*db.Wallet{}
|
|
}
|
|
|
|
data, _ := json.Marshal(wallets)
|
|
return zapOK(data)
|
|
}
|
|
|
|
func (s *Server) zapIntent(_ context.Context, _ string, msg *zap.Message) (*zap.Message, error) {
|
|
root := msg.Root()
|
|
orgID := root.Text(fieldOrgID)
|
|
intentID := root.Text(fieldIntentID)
|
|
if orgID == "" {
|
|
return zapError("org_id required")
|
|
}
|
|
|
|
// Query existing intent
|
|
if intentID != "" {
|
|
intent, err := orm.Get[db.Intent](s.db.ORM, intentID)
|
|
if err != nil || intent.OrgID != orgID {
|
|
return zapError("intent not found")
|
|
}
|
|
data, _ := json.Marshal(intent)
|
|
return zapOK(data)
|
|
}
|
|
|
|
// Create from payload
|
|
payload := root.Bytes(fieldPayload)
|
|
if len(payload) == 0 {
|
|
return zapError("intent_id or payload required")
|
|
}
|
|
|
|
var req struct {
|
|
WalletID string `json:"wallet_id"`
|
|
IntentType string `json:"intent_type"`
|
|
Chain string `json:"chain"`
|
|
ToAddress string `json:"to_address"`
|
|
Amount string `json:"amount"`
|
|
Token string `json:"token,omitempty"`
|
|
}
|
|
if err := json.Unmarshal(payload, &req); err != nil {
|
|
return zapError("invalid intent payload")
|
|
}
|
|
if req.WalletID == "" || req.IntentType == "" || req.Chain == "" || req.Amount == "" {
|
|
return zapError("wallet_id, intent_type, chain, amount required")
|
|
}
|
|
|
|
intent := orm.New[db.Intent](s.db.ORM)
|
|
intent.OrgID = orgID
|
|
intent.WalletID = req.WalletID
|
|
intent.IntentType = req.IntentType
|
|
intent.Chain = req.Chain
|
|
intent.ToAddress = nilIfEmpty(req.ToAddress)
|
|
intent.Amount = req.Amount
|
|
intent.Token = nilIfEmpty(req.Token)
|
|
intent.Status = "pending_sign"
|
|
if err := intent.Create(); err != nil {
|
|
return zapError(err.Error())
|
|
}
|
|
|
|
data, _ := json.Marshal(intent)
|
|
s.Events.Publish(Event{Type: "intent.created", OrgID: orgID, WalletID: req.WalletID, Data: data})
|
|
return zapOK(data)
|
|
}
|
|
|
|
// zapEventForwarder broadcasts EventBus events to ZAP peers.
|
|
func (s *Server) zapEventForwarder(node *zap.Node) {
|
|
ch, unsub := s.Events.Subscribe("")
|
|
defer unsub()
|
|
for evt := range ch {
|
|
data, err := json.Marshal(evt)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
errs := node.Broadcast(context.Background(), zapMsg(data))
|
|
for peer, berr := range errs {
|
|
logger.Warn("zap broadcast failed", "peer", peer, "err", berr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// zapError builds a ZAP response with error status.
|
|
func zapError(msg string) (*zap.Message, error) {
|
|
b := zap.NewBuilder(256)
|
|
ob := b.StartObject(16)
|
|
ob.SetUint8(fieldStatus, 1)
|
|
ob.SetText(fieldError, msg)
|
|
ob.FinishAsRoot()
|
|
return zap.Parse(b.Finish())
|
|
}
|
|
|
|
// zapOK builds a ZAP response with success status and JSON data.
|
|
func zapOK(data []byte) (*zap.Message, error) {
|
|
b := zap.NewBuilder(len(data) + 64)
|
|
ob := b.StartObject(16)
|
|
ob.SetUint8(fieldStatus, 0)
|
|
ob.SetBytes(fieldData, data)
|
|
ob.FinishAsRoot()
|
|
return zap.Parse(b.Finish())
|
|
}
|
|
|
|
// zapMsg builds a one-way ZAP message with data payload.
|
|
func zapMsg(data []byte) *zap.Message {
|
|
b := zap.NewBuilder(len(data) + 64)
|
|
ob := b.StartObject(16)
|
|
ob.SetBytes(fieldData, data)
|
|
ob.FinishAsRoot()
|
|
m, _ := zap.Parse(b.Finish())
|
|
return m
|
|
}
|