Files
chains/bridgevm/rpc.go
T
zeekayandHanzo Dev 3e9fd77e98 rpc: /ext/ -> /v1/ on our own hosts
luxd serves /v1/*; /ext/* 404s. Third-party chains keep their own paths.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 21:15:23 -07:00

967 lines
31 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bridgevm
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/gorilla/rpc/v2"
"github.com/luxfi/ids"
)
// Service provides JSON-RPC endpoints for BridgeVM:
// - LP-333 signer-set management (RegisterValidator, GetSignerSetInfo, …)
// - Permissionless bridge settlement
// (EstimateFee, SubmitRequest, GetStatus, CancelRequest, …)
//
// Any client that can reach /v1/bc/B/rpc has equal authority — the
// daemon at cmd/bridge is one such client. There are no privileged
// methods on this surface; rate-limiting and auth (when desired) are
// applied at the ingress layer.
type Service struct {
vm *VM
}
// NewService returns a new Service instance
func NewService(vm *VM) *Service {
return &Service{vm: vm}
}
// RegisterService registers the BridgeVM RPC handlers
func (vm *VM) RegisterService(server *rpc.Server) error {
return server.RegisterService(&Service{vm: vm}, "bridge")
}
// =============================================================================
// LP-333 JSON-RPC Endpoints
// =============================================================================
// RegisterValidatorArgs are the arguments for bridge_registerValidator
type RegisterValidatorArgs struct {
NodeID string `json:"nodeId"`
BondAmount string `json:"bondAmount,omitempty"` // 100M LUX bond (slashable)
MPCPubKey string `json:"mpcPubKey,omitempty"`
}
// RegisterValidatorReply is the reply for bridge_registerValidator
type RegisterValidatorReply struct {
Success bool `json:"success"`
NodeID string `json:"nodeId"`
Registered bool `json:"registered"`
Waitlisted bool `json:"waitlisted"`
SignerIndex int `json:"signerIndex"`
WaitlistIndex int `json:"waitlistIndex,omitempty"`
TotalSigners int `json:"totalSigners"`
Threshold int `json:"threshold"`
ReshareNeeded bool `json:"reshareNeeded"`
CurrentEpoch uint64 `json:"currentEpoch"`
SetFrozen bool `json:"setFrozen"`
RemainingSlots int `json:"remainingSlots"`
Message string `json:"message"`
}
// RegisterValidator registers a validator as a bridge signer (LP-333 opt-in model)
// First 100 validators are accepted directly without reshare.
// After 100 signers, new validators go to waitlist.
func (s *Service) RegisterValidator(_ *http.Request, args *RegisterValidatorArgs, reply *RegisterValidatorReply) error {
input := &RegisterValidatorInput{
NodeID: args.NodeID,
BondAmount: args.BondAmount,
MPCPubKey: args.MPCPubKey,
}
result, err := s.vm.RegisterValidator(input)
if err != nil {
return err
}
reply.Success = result.Success
reply.NodeID = result.NodeID
reply.Registered = result.Registered
reply.Waitlisted = result.Waitlisted
reply.SignerIndex = result.SignerIndex
reply.WaitlistIndex = result.WaitlistIndex
reply.TotalSigners = result.TotalSigners
reply.Threshold = result.Threshold
reply.ReshareNeeded = result.ReshareNeeded
reply.CurrentEpoch = result.CurrentEpoch
reply.SetFrozen = result.SetFrozen
reply.RemainingSlots = result.RemainingSlots
reply.Message = result.Message
return nil
}
// GetSignerSetInfoArgs are the arguments for bridge_getSignerSetInfo (empty)
type GetSignerSetInfoArgs struct{}
// GetSignerSetInfoReply is the reply for bridge_getSignerSetInfo
type GetSignerSetInfoReply struct {
TotalSigners int `json:"totalSigners"`
Threshold int `json:"threshold"`
MaxSigners int `json:"maxSigners"`
CurrentEpoch uint64 `json:"currentEpoch"`
SetFrozen bool `json:"setFrozen"`
RemainingSlots int `json:"remainingSlots"`
WaitlistSize int `json:"waitlistSize"`
Signers []SignerInfoReply `json:"signers"`
PublicKey string `json:"publicKey,omitempty"`
}
// SignerInfoReply contains signer information for RPC replies
type SignerInfoReply struct {
NodeID string `json:"nodeId"`
PartyID string `json:"partyId"`
BondAmount uint64 `json:"bondAmount"` // 100M LUX bond (slashable)
Active bool `json:"active"`
JoinedAt string `json:"joinedAt"`
SlotIndex int `json:"slotIndex"`
Slashed bool `json:"slashed"`
SlashCount int `json:"slashCount"`
}
// GetSignerSetInfo returns information about the current signer set (LP-333)
func (s *Service) GetSignerSetInfo(_ *http.Request, _ *GetSignerSetInfoArgs, reply *GetSignerSetInfoReply) error {
info := s.vm.GetSignerSetInfo()
reply.TotalSigners = info.TotalSigners
reply.Threshold = info.Threshold
reply.MaxSigners = info.MaxSigners
reply.CurrentEpoch = info.CurrentEpoch
reply.SetFrozen = info.SetFrozen
reply.RemainingSlots = info.RemainingSlots
reply.WaitlistSize = info.WaitlistSize
reply.PublicKey = info.PublicKey
reply.Signers = make([]SignerInfoReply, len(info.Signers))
for i, signer := range info.Signers {
reply.Signers[i] = SignerInfoReply{
NodeID: signer.NodeID.String(),
PartyID: string(signer.PartyID),
BondAmount: signer.BondAmount,
Active: signer.Active,
JoinedAt: signer.JoinedAt.Format("2006-01-02T15:04:05Z"),
SlotIndex: signer.SlotIndex,
Slashed: signer.Slashed,
SlashCount: signer.SlashCount,
}
}
return nil
}
// ReplaceSignerArgs are the arguments for bridge_replaceSigner
type ReplaceSignerArgs struct {
NodeID string `json:"nodeId"` // Signer to remove
ReplacementNodeID string `json:"replacementNodeId"` // Explicit replacement (optional, uses waitlist if empty)
}
// ReplaceSignerReply is the reply for bridge_replaceSigner
type ReplaceSignerReply struct {
Success bool `json:"success"`
RemovedNodeID string `json:"removedNodeId,omitempty"`
ReplacementNodeID string `json:"replacementNodeId,omitempty"`
ReshareSession string `json:"reshareSession,omitempty"`
NewEpoch uint64 `json:"newEpoch"`
ActiveSigners int `json:"activeSigners"`
Threshold int `json:"threshold"`
Message string `json:"message"`
}
// ReplaceSigner removes a failed signer and triggers reshare (LP-333)
// This is the ONLY operation that triggers a reshare.
func (s *Service) ReplaceSigner(_ *http.Request, args *ReplaceSignerArgs, reply *ReplaceSignerReply) error {
nodeID, err := ids.NodeIDFromString(args.NodeID)
if err != nil {
return err
}
var replacementNodeID *ids.NodeID
if args.ReplacementNodeID != "" {
rid, err := ids.NodeIDFromString(args.ReplacementNodeID)
if err != nil {
return err
}
replacementNodeID = &rid
}
result, err := s.vm.RemoveSigner(nodeID, replacementNodeID)
if err != nil {
return err
}
reply.Success = result.Success
reply.RemovedNodeID = result.RemovedNodeID
reply.ReplacementNodeID = result.ReplacementNodeID
reply.ReshareSession = result.ReshareSession
reply.NewEpoch = result.NewEpoch
reply.ActiveSigners = result.ActiveSigners
reply.Threshold = result.Threshold
reply.Message = result.Message
return nil
}
// HasSignerArgs are the arguments for bridge_hasSigner
type HasSignerArgs struct {
NodeID string `json:"nodeId"`
}
// HasSignerReply is the reply for bridge_hasSigner
type HasSignerReply struct {
IsSigner bool `json:"isSigner"`
}
// HasSigner checks if a node is in the active signer set
func (s *Service) HasSigner(_ *http.Request, args *HasSignerArgs, reply *HasSignerReply) error {
nodeID, err := ids.NodeIDFromString(args.NodeID)
if err != nil {
return err
}
reply.IsSigner = s.vm.HasSigner(nodeID)
return nil
}
// GetWaitlistArgs are the arguments for bridge_getWaitlist (empty)
type GetWaitlistArgs struct{}
// GetWaitlistReply is the reply for bridge_getWaitlist
type GetWaitlistReply struct {
WaitlistSize int `json:"waitlistSize"`
NodeIDs []string `json:"nodeIds"`
}
// GetWaitlist returns the current waitlist of validators waiting for signer slots
func (s *Service) GetWaitlist(_ *http.Request, _ *GetWaitlistArgs, reply *GetWaitlistReply) error {
s.vm.mu.RLock()
defer s.vm.mu.RUnlock()
reply.WaitlistSize = len(s.vm.signerSet.Waitlist)
reply.NodeIDs = make([]string, len(s.vm.signerSet.Waitlist))
for i, nodeID := range s.vm.signerSet.Waitlist {
reply.NodeIDs[i] = nodeID.String()
}
return nil
}
// GetCurrentEpochArgs are the arguments for bridge_getCurrentEpoch (empty)
type GetCurrentEpochArgs struct{}
// GetCurrentEpochReply is the reply for bridge_getCurrentEpoch
type GetCurrentEpochReply struct {
Epoch uint64 `json:"epoch"`
TotalSigners int `json:"totalSigners"`
Threshold int `json:"threshold"`
SetFrozen bool `json:"setFrozen"`
}
// GetCurrentEpoch returns the current epoch (incremented only on reshare)
func (s *Service) GetCurrentEpoch(_ *http.Request, _ *GetCurrentEpochArgs, reply *GetCurrentEpochReply) error {
s.vm.mu.RLock()
defer s.vm.mu.RUnlock()
reply.Epoch = s.vm.signerSet.CurrentEpoch
reply.TotalSigners = len(s.vm.signerSet.Signers)
reply.Threshold = s.vm.signerSet.ThresholdT
reply.SetFrozen = s.vm.signerSet.SetFrozen
return nil
}
// SlashSignerArgs are the arguments for bridge_slashSigner
type SlashSignerArgs struct {
NodeID string `json:"nodeId"`
Reason string `json:"reason"`
SlashPercent int `json:"slashPercent"` // Percentage of bond to slash (1-100)
Evidence string `json:"evidence"` // Hex-encoded proof of misbehavior
}
// SlashSignerReply is the reply for bridge_slashSigner
type SlashSignerReply struct {
Success bool `json:"success"`
NodeID string `json:"nodeId"`
SlashedAmount uint64 `json:"slashedAmount"`
RemainingBond uint64 `json:"remainingBond"`
TotalSlashCount int `json:"totalSlashCount"`
RemovedFromSet bool `json:"removedFromSet"`
Message string `json:"message"`
}
// SlashSigner slashes a misbehaving bridge signer's bond
// The bond is NOT stake - it's a slashable deposit that can be partially or fully seized
func (s *Service) SlashSigner(_ *http.Request, args *SlashSignerArgs, reply *SlashSignerReply) error {
nodeID, err := ids.NodeIDFromString(args.NodeID)
if err != nil {
return err
}
input := &SlashSignerInput{
NodeID: nodeID,
Reason: args.Reason,
SlashPercent: args.SlashPercent,
Evidence: []byte(args.Evidence),
}
result, err := s.vm.SlashSigner(input)
if err != nil {
return err
}
reply.Success = result.Success
reply.NodeID = result.NodeID
reply.SlashedAmount = result.SlashedAmount
reply.RemainingBond = result.RemainingBond
reply.TotalSlashCount = result.TotalSlashCount
reply.RemovedFromSet = result.RemovedFromSet
reply.Message = result.Message
return nil
}
// =============================================================================
// Permissionless settlement RPC
// =============================================================================
// EstimateFeeArgs are the bridge_estimateFee request body.
type EstimateFeeArgs struct {
SourceChain string `json:"sourceChain"`
DestChain string `json:"destChain"`
SourceAsset string `json:"sourceAsset"`
DestAsset string `json:"destAsset"`
Amount string `json:"amount"`
Refuel bool `json:"refuel,omitempty"`
}
// EstimateFeeReply is the bridge_estimateFee response.
type EstimateFeeReply struct {
FeeAmount string `json:"feeAmount"`
NetAmount string `json:"netAmount"`
EstimatedTime int `json:"estimatedTime"`
}
// EstimateFee answers bridge_estimateFee. Authoritative settlement
// math runs in the VM (see quote.go) so the result is what the
// chain will pay.
func (s *Service) EstimateFee(_ *http.Request, args *EstimateFeeArgs, reply *EstimateFeeReply) error {
if s.vm == nil || s.vm.quoteEngine == nil {
return errors.New("bridgevm: quote engine not configured")
}
amt, err := strconv.ParseFloat(strings.TrimSpace(args.Amount), 64)
if err != nil || amt <= 0 {
return fmt.Errorf("bridgevm: amount must be a positive number (got %q)", args.Amount)
}
res, err := s.vm.quoteEngine.Quote(QuoteInput{
Amount: amt,
SourceNetwork: args.SourceChain,
SourceAsset: args.SourceAsset,
DestinationNetwork: args.DestChain,
DestinationAsset: args.DestAsset,
Refuel: args.Refuel,
})
if err != nil {
return err
}
reply.FeeAmount = formatAmount(res.ServiceFee)
reply.NetAmount = formatAmount(res.ReceiveAmount)
reply.EstimatedTime = res.EstimatedTime
return nil
}
// SubmitRequestArgs is the bridge_submitRequest request body.
type SubmitRequestArgs struct {
SourceChain string `json:"sourceChain"`
DestChain string `json:"destChain"`
SourceAsset string `json:"sourceAsset"`
DestAsset string `json:"destAsset"`
Amount string `json:"amount"`
Recipient string `json:"recipient"`
Sender string `json:"sender"`
Refuel bool `json:"refuel,omitempty"`
}
// SubmitRequestReply is the bridge_submitRequest response.
type SubmitRequestReply BridgeRequestRecord
// SubmitRequest creates a new bridge request server-side and snapshots
// the quote into the record so the daemon's signing pipeline pays out
// what the chain committed to.
func (s *Service) SubmitRequest(_ *http.Request, args *SubmitRequestArgs, reply *SubmitRequestReply) error {
if s.vm == nil || s.vm.swapStore == nil {
return errors.New("bridgevm: swap store not configured")
}
if args.SourceChain == "" || args.DestChain == "" || args.Recipient == "" {
return errors.New("bridgevm: missing required field (sourceChain, destChain, recipient)")
}
amt, err := strconv.ParseFloat(strings.TrimSpace(args.Amount), 64)
if err != nil || amt <= 0 {
return fmt.Errorf("bridgevm: amount must be a positive number (got %q)", args.Amount)
}
// Snapshot the quote — chain commits to these economics at create
// time so post-create price flapping does not change the payout.
res, err := s.vm.quoteEngine.Quote(QuoteInput{
Amount: amt,
SourceNetwork: args.SourceChain,
SourceAsset: args.SourceAsset,
DestinationNetwork: args.DestChain,
DestinationAsset: args.DestAsset,
Refuel: args.Refuel,
})
if err != nil {
return err
}
rec := &BridgeRequestRecord{
SourceChain: args.SourceChain,
DestChain: args.DestChain,
SourceAsset: args.SourceAsset,
DestAsset: args.DestAsset,
Amount: strings.TrimSpace(args.Amount),
Recipient: args.Recipient,
Sender: args.Sender,
Status: StatusPending,
FeeAmount: formatAmount(res.ServiceFee),
NetAmount: formatAmount(res.ReceiveAmount),
}
if err := s.vm.swapStore.Put(rec); err != nil {
return err
}
*reply = SubmitRequestReply(*rec)
return nil
}
// GetStatusArgs is the bridge_getStatus request body.
type GetStatusArgs struct {
RequestID string `json:"requestId"`
}
// GetStatusReply is the bridge_getStatus response.
type GetStatusReply BridgeRequestRecord
// GetStatus answers bridge_getStatus from the authoritative swap store.
func (s *Service) GetStatus(_ *http.Request, args *GetStatusArgs, reply *GetStatusReply) error {
if s.vm == nil || s.vm.swapStore == nil {
return errors.New("bridgevm: swap store not configured")
}
rec, err := s.vm.swapStore.Get(args.RequestID)
if err != nil {
return err
}
*reply = GetStatusReply(*rec)
return nil
}
// CancelRequestArgs is the bridge_cancelRequest request body.
type CancelRequestArgs struct {
RequestID string `json:"requestId"`
}
// CancelRequestReply is the bridge_cancelRequest response.
type CancelRequestReply struct {
Success bool `json:"success"`
}
// CancelRequest answers bridge_cancelRequest. Idempotent — cancelling
// an already-terminal swap is a no-op success so retries are safe.
func (s *Service) CancelRequest(_ *http.Request, args *CancelRequestArgs, reply *CancelRequestReply) error {
if s.vm == nil || s.vm.swapStore == nil {
return errors.New("bridgevm: swap store not configured")
}
rec, err := s.vm.swapStore.Get(args.RequestID)
if err != nil {
return err
}
if rec.Status == StatusCompleted ||
rec.Status == StatusFailed ||
rec.Status == StatusCancelled {
reply.Success = true
return nil
}
if _, err := s.vm.swapStore.Patch(args.RequestID, func(r *BridgeRequestRecord) {
r.Status = StatusCancelled
}); err != nil {
return err
}
reply.Success = true
return nil
}
// HealthArgs are empty (no params).
type HealthArgs struct{}
// HealthReply is the bridge_health response.
type HealthReply struct {
Status string `json:"status"`
MPCReady bool `json:"mpcReady"`
}
// Health answers bridge_health. Liveness probe used by daemons + load
// balancers before routing traffic at this node.
func (s *Service) Health(_ *http.Request, _ *HealthArgs, reply *HealthReply) error {
reply.Status = "healthy"
if s.vm != nil {
reply.MPCReady = len(s.vm.mpcGroupPublicKey()) > 0
}
return nil
}
// GetMPCPublicKeyArgs are empty.
type GetMPCPublicKeyArgs struct{}
// GetMPCPublicKeyReply is the bridge_getMPCPublicKey response.
type GetMPCPublicKeyReply struct {
PublicKey string `json:"publicKey"`
}
// GetMPCPublicKey answers bridge_getMPCPublicKey with the active
// threshold-signing group public key.
func (s *Service) GetMPCPublicKey(_ *http.Request, _ *GetMPCPublicKeyArgs, reply *GetMPCPublicKeyReply) error {
if s.vm == nil {
return errors.New("bridgevm: VM not initialized")
}
key := s.vm.mpcGroupPublicKey()
if len(key) == 0 {
return errors.New("bridgevm: group public key not yet established")
}
reply.PublicKey = hexEncode(key)
return nil
}
// =============================================================================
// Discovery: bridge_getInfo / bridge_getSupportedChains / bridge_getChainConfig
// =============================================================================
//
// These four methods close the gap between the daemon's bchain client
// (internal/bchain.Client) and the VM-side authoritative surface. Before
// them, the daemon called bridge_estimateFee/submitRequest/getStatus/
// cancelRequest/health/getMPCPublicKey directly against this RPC but
// fell back to its own assumed values for info/supported-chains/chain-
// config/signature lookups — a centralization vector if the daemon's
// cached view drifted from chain consensus.
//
// All four are read-only: they answer from VM state (config + registry
// + swapStore) and never mutate anything. Cancellable via the caller's
// HTTP timeout — these handlers never block on consensus.
// GetBridgeInfoArgs are empty.
type GetBridgeInfoArgs struct{}
// GetBridgeInfoReply is bridge_getInfo's response. Mirrors the wire shape
// the daemon's bchain.BridgeInfo decodes.
type GetBridgeInfoReply struct {
Version string `json:"version"`
NodeID string `json:"nodeId"`
ChainID string `json:"chainId"`
MPCReady bool `json:"mpcReady"`
MPCPublicKey string `json:"mpcPublicKey"`
Threshold int `json:"threshold"`
TotalParties int `json:"totalParties"`
SupportedChains []string `json:"supportedChains"`
TotalBridged string `json:"totalBridged"`
TotalFees string `json:"totalFees"`
}
// GetBridgeInfo answers bridge_getInfo. Read-only snapshot of node-level
// bridge state for dashboards + daemon discovery. Authoritative — no
// daemon-cached fallback. When MPC has not generated a group key the
// MPCPublicKey field is empty (not an error) so callers can render
// "MPC pending" without dispatching on an error code.
func (s *Service) GetBridgeInfo(_ *http.Request, _ *GetBridgeInfoArgs, reply *GetBridgeInfoReply) error {
if s.vm == nil {
return errors.New("bridgevm: VM not initialized")
}
reply.Version = Version.String()
if s.vm.rt != nil {
reply.NodeID = s.vm.rt.NodeID.String()
}
reply.ChainID = "B"
if key := s.vm.mpcGroupPublicKey(); len(key) > 0 {
reply.MPCReady = true
reply.MPCPublicKey = hexEncode(key)
}
reply.Threshold = s.vm.config.MPCThreshold
reply.TotalParties = s.vm.config.MPCTotalParties
reply.SupportedChains = append([]string(nil), s.vm.config.SupportedChains...)
// Aggregate totals from the registry. Stringified so the daemon's
// bigint-safe decode (parseAmount) consumes them directly.
var totalBridged uint64
if s.vm.bridgeRegistry != nil {
s.vm.bridgeRegistry.mu.RLock()
for _, v := range s.vm.bridgeRegistry.Validators {
totalBridged += v.TotalBridged
}
s.vm.bridgeRegistry.mu.RUnlock()
}
reply.TotalBridged = strconv.FormatUint(totalBridged, 10)
// TotalFees is not tracked in registry yet; expose 0 explicitly so
// the daemon's JSON decode doesn't surface an "empty result" error.
reply.TotalFees = "0"
return nil
}
// GetSupportedChainsArgs are empty.
type GetSupportedChainsArgs struct{}
// GetSupportedChainsReply is the bridge_getSupportedChains response —
// the list of ChainConfig snapshots the VM is configured to bridge.
type GetSupportedChainsReply struct {
Chains []ChainConfigReply `json:"chains"`
}
// ChainConfigReply is one chain's bridge config (wire-stable; matches
// bchain.ChainConfig).
type ChainConfigReply struct {
ChainID string `json:"chainId"`
ChainName string `json:"chainName"`
RPCEndpoint string `json:"rpcEndpoint"`
BridgeContract string `json:"bridgeContract"`
TokenContracts map[string]string `json:"tokenContracts"`
NativeCurrency string `json:"nativeCurrency"`
BlockTime int `json:"blockTime"`
Confirmations int `json:"confirmations"`
Enabled bool `json:"enabled"`
}
// GetSupportedChains answers bridge_getSupportedChains. The VM's
// SupportedChains config drives the result; per-chain RPC endpoints /
// token contracts are not yet stored chain-side, so we surface the
// minimal stable shape (ChainID + Enabled) and leave the optional
// fields empty. The daemon already treats those as informational.
func (s *Service) GetSupportedChains(_ *http.Request, _ *GetSupportedChainsArgs, reply *GetSupportedChainsReply) error {
if s.vm == nil {
return errors.New("bridgevm: VM not initialized")
}
out := make([]ChainConfigReply, 0, len(s.vm.config.SupportedChains))
for _, id := range s.vm.config.SupportedChains {
out = append(out, ChainConfigReply{
ChainID: id,
ChainName: id,
Confirmations: int(s.vm.config.MinConfirmations),
Enabled: true,
})
}
reply.Chains = out
return nil
}
// GetChainConfigArgs is the bridge_getChainConfig request body.
type GetChainConfigArgs struct {
ChainID string `json:"chainId"`
}
// GetChainConfigReply is the bridge_getChainConfig response.
type GetChainConfigReply ChainConfigReply
// GetChainConfig answers bridge_getChainConfig for one chain. Returns
// an explicit error when the chain isn't in the SupportedChains set so
// callers can distinguish "unknown chain" from "RPC mis-routed".
func (s *Service) GetChainConfig(_ *http.Request, args *GetChainConfigArgs, reply *GetChainConfigReply) error {
if s.vm == nil {
return errors.New("bridgevm: VM not initialized")
}
want := strings.TrimSpace(args.ChainID)
if want == "" {
return errors.New("bridgevm: chainId required")
}
for _, id := range s.vm.config.SupportedChains {
if strings.EqualFold(id, want) {
*reply = GetChainConfigReply{
ChainID: id,
ChainName: id,
Confirmations: int(s.vm.config.MinConfirmations),
Enabled: true,
}
return nil
}
}
return fmt.Errorf("bridgevm: chain %q not in supportedChains", want)
}
// GetSignatureArgs is the bridge_getSignature request body.
type GetSignatureArgs struct {
RequestID string `json:"requestId"`
}
// GetSignatureReply is the bridge_getSignature response.
type GetSignatureReply struct {
Signature string `json:"signature"`
SessionID string `json:"sessionId,omitempty"`
}
// GetSignature answers bridge_getSignature. The signature lives on the
// BridgeRequestRecord (populated by the MPC signing pipeline once the
// threshold quorum aggregates shares). Returns an error when the swap
// exists but the signature is not yet available so callers can poll
// without conflating "no such swap" with "still signing".
func (s *Service) GetSignature(_ *http.Request, args *GetSignatureArgs, reply *GetSignatureReply) error {
if s.vm == nil || s.vm.swapStore == nil {
return errors.New("bridgevm: swap store not configured")
}
rec, err := s.vm.swapStore.Get(strings.TrimSpace(args.RequestID))
if err != nil {
return err
}
if rec.Signature == "" {
return fmt.Errorf("bridgevm: signature not yet available for %s (status=%s)", rec.RequestID, rec.Status)
}
reply.Signature = rec.Signature
reply.SessionID = rec.RequestID
return nil
}
// hexEncode formats a byte slice as lowercase hex (no 0x prefix), the
// canonical JSON-RPC encoding for raw MPC bytes.
func hexEncode(b []byte) string {
const hexChars = "0123456789abcdef"
out := make([]byte, len(b)*2)
for i, v := range b {
out[i*2] = hexChars[v>>4]
out[i*2+1] = hexChars[v&0x0f]
}
return string(out)
}
// =============================================================================
// HTTP Handler Integration
// =============================================================================
// CreateRPCHandlers creates HTTP handlers for JSON-RPC endpoints
func (vm *VM) CreateRPCHandlers() (map[string]http.Handler, error) {
service := NewService(vm)
// Create a simple HTTP handler that wraps the service methods
handlers := map[string]http.Handler{
"/rpc": &jsonRPCHandler{service: service},
}
return handlers, nil
}
// jsonRPCHandler handles JSON-RPC requests
type jsonRPCHandler struct {
service *Service
}
// jsonRPCRequest represents a JSON-RPC request
type jsonRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
ID interface{} `json:"id"`
}
// jsonRPCResponse represents a JSON-RPC response
type jsonRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result interface{} `json:"result,omitempty"`
Error *jsonRPCError `json:"error,omitempty"`
ID interface{} `json:"id"`
}
// jsonRPCError represents a JSON-RPC error
type jsonRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func (h *jsonRPCHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req jsonRPCRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.writeError(w, nil, -32700, "parse error", err)
return
}
var result interface{}
var err error
switch req.Method {
case "bridge_registerValidator", "bridge.registerValidator":
var args RegisterValidatorArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply RegisterValidatorReply
err = h.service.RegisterValidator(r, &args, &reply)
result = reply
case "bridge_getSignerSetInfo", "bridge.getSignerSetInfo":
var reply GetSignerSetInfoReply
err = h.service.GetSignerSetInfo(r, &GetSignerSetInfoArgs{}, &reply)
result = reply
case "bridge_replaceSigner", "bridge.replaceSigner":
var args ReplaceSignerArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply ReplaceSignerReply
err = h.service.ReplaceSigner(r, &args, &reply)
result = reply
case "bridge_hasSigner", "bridge.hasSigner":
var args HasSignerArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply HasSignerReply
err = h.service.HasSigner(r, &args, &reply)
result = reply
case "bridge_getWaitlist", "bridge.getWaitlist":
var reply GetWaitlistReply
err = h.service.GetWaitlist(r, &GetWaitlistArgs{}, &reply)
result = reply
case "bridge_getCurrentEpoch", "bridge.getCurrentEpoch":
var reply GetCurrentEpochReply
err = h.service.GetCurrentEpoch(r, &GetCurrentEpochArgs{}, &reply)
result = reply
case "bridge_slashSigner", "bridge.slashSigner":
var args SlashSignerArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply SlashSignerReply
err = h.service.SlashSigner(r, &args, &reply)
result = reply
case "bridge_estimateFee", "bridge.estimateFee":
var args EstimateFeeArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply EstimateFeeReply
err = h.service.EstimateFee(r, &args, &reply)
result = reply
case "bridge_submitRequest", "bridge.submitRequest":
var args SubmitRequestArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply SubmitRequestReply
err = h.service.SubmitRequest(r, &args, &reply)
result = reply
case "bridge_getStatus", "bridge.getStatus":
var args GetStatusArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply GetStatusReply
err = h.service.GetStatus(r, &args, &reply)
result = reply
case "bridge_cancelRequest", "bridge.cancelRequest":
var args CancelRequestArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply CancelRequestReply
err = h.service.CancelRequest(r, &args, &reply)
result = reply
case "bridge_health", "bridge.health":
var reply HealthReply
err = h.service.Health(r, &HealthArgs{}, &reply)
result = reply
case "bridge_getMPCPublicKey", "bridge.getMPCPublicKey":
var reply GetMPCPublicKeyReply
err = h.service.GetMPCPublicKey(r, &GetMPCPublicKeyArgs{}, &reply)
result = reply
case "bridge_getInfo", "bridge.getInfo":
var reply GetBridgeInfoReply
err = h.service.GetBridgeInfo(r, &GetBridgeInfoArgs{}, &reply)
result = reply
case "bridge_getSupportedChains", "bridge.getSupportedChains":
var reply GetSupportedChainsReply
err = h.service.GetSupportedChains(r, &GetSupportedChainsArgs{}, &reply)
result = reply
case "bridge_getChainConfig", "bridge.getChainConfig":
var args GetChainConfigArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply GetChainConfigReply
err = h.service.GetChainConfig(r, &args, &reply)
result = reply
case "bridge_getSignature", "bridge.getSignature":
var args GetSignatureArgs
if err := json.Unmarshal(req.Params, &args); err != nil {
h.writeError(w, req.ID, -32602, "invalid params", err)
return
}
var reply GetSignatureReply
err = h.service.GetSignature(r, &args, &reply)
result = reply
default:
h.writeError(w, req.ID, -32601, "method not found", nil)
return
}
if err != nil {
// Surface the actual error in the message so callers can
// dispatch on it (e.g. "swap not found" vs "price unknown")
// rather than parsing the opaque `data` envelope.
h.writeError(w, req.ID, -32000, err.Error(), nil)
return
}
h.writeResult(w, req.ID, result)
}
func (h *jsonRPCHandler) writeResult(w http.ResponseWriter, id interface{}, result interface{}) {
resp := jsonRPCResponse{
JSONRPC: "2.0",
Result: result,
ID: id,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func (h *jsonRPCHandler) writeError(w http.ResponseWriter, id interface{}, code int, message string, data interface{}) {
resp := jsonRPCResponse{
JSONRPC: "2.0",
Error: &jsonRPCError{
Code: code,
Message: message,
Data: data,
},
ID: id,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}