mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
luxd serves chain routes under /v1/chain, and the node, cli and sdk releases already ship it. The deployed gateways still answer the old prefix, so remote reads break until they are redeployed; the estate moves together rather than sitting half converted. Lines that record an observation - a probe, a status code, a past release - keep the prefix they were written against, as do URLs on hosts that are not ours. Avalanche endpoints move to /ext/bc, which is the path that host serves.
661 lines
23 KiB
Go
661 lines
23 KiB
Go
// Package bchain is a JSON-RPC client for the Lux BridgeVM (b-chain).
|
|
//
|
|
// This is the Go port of `pkg/bridge/src/app/lib/bridge-rpc.ts`. Talks to
|
|
// the BridgeVM JSON-RPC interface (`${nodeUrl}/v1/chain/B/rpc`) and, when
|
|
// configured, the ThresholdVM interface (`${nodeUrl}/v1/chain/T/rpc`) for
|
|
// MPC signature retrieval.
|
|
//
|
|
// Trust model matches the TS client: the JSON-RPC node is untrusted
|
|
// transport. Authoritative settlement decisions (min_receive_amount,
|
|
// signature validity) come from the MPC threshold quorum, not from
|
|
// anything in this package. Permissionless: any consumer can point at
|
|
// any BridgeVM node; there is no central authority on the wire protocol.
|
|
//
|
|
// Concurrency: a *Client is safe for use by multiple goroutines.
|
|
// Cancellation: every method takes a context.Context for caller-driven
|
|
// cancellation. A per-request timeout (Client.Timeout) bounds the call
|
|
// independently so a hung node can't block forever.
|
|
package bchain
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// =============================================================================
|
|
// JSON-RPC envelope
|
|
// =============================================================================
|
|
|
|
// jsonrpcRequest is the wire shape for outgoing JSON-RPC calls.
|
|
type jsonrpcRequest struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID string `json:"id"`
|
|
Method string `json:"method"`
|
|
Params any `json:"params,omitempty"`
|
|
}
|
|
|
|
// jsonrpcResponse is the wire shape for incoming JSON-RPC responses.
|
|
// Result is left as RawMessage so each typed method decodes it directly.
|
|
type jsonrpcResponse struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID string `json:"id"`
|
|
Result json.RawMessage `json:"result,omitempty"`
|
|
Error *jsonrpcError `json:"error,omitempty"`
|
|
}
|
|
|
|
type jsonrpcError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data json.RawMessage `json:"data,omitempty"`
|
|
}
|
|
|
|
// RPCError is the typed error returned by every Client method when the
|
|
// remote node reports a JSON-RPC error or the transport fails. Inspect
|
|
// .Code to dispatch on JSON-RPC error codes (-32601 method-not-found,
|
|
// -32602 invalid-params, etc.) and .HTTPStatus for transport-layer
|
|
// failures (0 means no HTTP error was involved).
|
|
type RPCError struct {
|
|
Method string
|
|
Code int
|
|
HTTPStatus int
|
|
Message string
|
|
Data json.RawMessage
|
|
}
|
|
|
|
func (e *RPCError) Error() string {
|
|
if e.HTTPStatus != 0 {
|
|
return fmt.Sprintf("bchain: %s HTTP %d: %s", e.Method, e.HTTPStatus, e.Message)
|
|
}
|
|
return fmt.Sprintf("bchain: %s rpc %d: %s", e.Method, e.Code, e.Message)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Bridge types — mirror the TS bridge-rpc.ts schema verbatim
|
|
// =============================================================================
|
|
|
|
// BridgeRequestStatus is the lifecycle state of a bridge request.
|
|
type BridgeRequestStatus string
|
|
|
|
const (
|
|
StatusPending BridgeRequestStatus = "pending"
|
|
StatusDeposited BridgeRequestStatus = "deposited"
|
|
StatusSigning BridgeRequestStatus = "signing"
|
|
StatusSigned BridgeRequestStatus = "signed"
|
|
StatusReleasing BridgeRequestStatus = "releasing"
|
|
StatusCompleted BridgeRequestStatus = "completed"
|
|
StatusFailed BridgeRequestStatus = "failed"
|
|
StatusCancelled BridgeRequestStatus = "cancelled"
|
|
)
|
|
|
|
// BridgeRequest is one bridge intent + its observable lifecycle state.
|
|
type BridgeRequest struct {
|
|
RequestID string `json:"requestId"`
|
|
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"`
|
|
Status BridgeRequestStatus `json:"status"`
|
|
CreatedAt int64 `json:"createdAt"`
|
|
SourceTxHash string `json:"sourceTxHash,omitempty"`
|
|
DestTxHash string `json:"destTxHash,omitempty"`
|
|
Signature string `json:"signature,omitempty"`
|
|
FeeAmount string `json:"feeAmount,omitempty"`
|
|
NetAmount string `json:"netAmount,omitempty"`
|
|
}
|
|
|
|
// ChainConfig is the per-chain config the BridgeVM reports.
|
|
type ChainConfig 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"`
|
|
}
|
|
|
|
// BridgeInfo is node-level info: mpc readiness, threshold, totals.
|
|
type BridgeInfo 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"`
|
|
}
|
|
|
|
// FeeEstimate is the bridge_estimateFee result.
|
|
type FeeEstimate struct {
|
|
FeeAmount string `json:"feeAmount"`
|
|
NetAmount string `json:"netAmount"`
|
|
EstimatedTime int `json:"estimatedTime"`
|
|
}
|
|
|
|
// Health is the bridge_health result.
|
|
type Health struct {
|
|
Status string `json:"status"`
|
|
MPCReady bool `json:"mpcReady"`
|
|
}
|
|
|
|
// MPCPublicKey is the bridge_getMPCPublicKey result.
|
|
type MPCPublicKey struct {
|
|
PublicKey string `json:"publicKey"`
|
|
}
|
|
|
|
// BridgeSignature is the bridge_getSignature result.
|
|
type BridgeSignature struct {
|
|
Signature string `json:"signature"`
|
|
SessionID string `json:"sessionId"`
|
|
}
|
|
|
|
// =============================================================================
|
|
// LP-333: signer set + epoch coordination
|
|
// =============================================================================
|
|
//
|
|
// LP-333 makes BridgeVM (b-chain) authoritative on which MPC nodes are in
|
|
// the active signer set, the threshold, and epoch transitions. The
|
|
// bridge (this binary) is a CONSUMER of this state — it does not vote
|
|
// on rotations; it observes them and points new keygen/signing at the
|
|
// current epoch's quorum. mpcd remains authoritative on key share
|
|
// material itself; b-chain is the membership + epoch ledger.
|
|
//
|
|
// The methods land on the same JSON-RPC endpoint as bridge_getInfo (the
|
|
// BridgeVM RPC at `${nodeUrl}/v1/chain/B/rpc`), not on ThresholdVM. They
|
|
// are read-only — the bridge never proposes a rotation; node operators
|
|
// do that out-of-band via BridgeVM governance.
|
|
|
|
// SignerMember is one node in the active signer set.
|
|
type SignerMember struct {
|
|
// NodeID is the BridgeVM node identifier (e.g. "node-0",
|
|
// "NodeID-7Xhw..."). Operators correlate this with their MPC
|
|
// cluster identity.
|
|
NodeID string `json:"nodeId"`
|
|
|
|
// PublicKey is the node's announced MPC verifying-key share
|
|
// (hex-encoded). The composition rule (Schnorr sum, BLS aggregate)
|
|
// is implied by the threshold protocol on the cluster — out of
|
|
// scope for the bridge.
|
|
PublicKey string `json:"publicKey"`
|
|
|
|
// Address is the chain-format address corresponding to the
|
|
// PublicKey on whichever family the operator cares to display.
|
|
// May be empty when the BridgeVM has not surfaced it.
|
|
Address string `json:"address,omitempty"`
|
|
}
|
|
|
|
// SignerSetInfo is the bridge_getSignerSetInfo result — a snapshot of
|
|
// the active MPC signer set at the current epoch. Bridge consumers
|
|
// scrape this to surface "is the quorum the one I expect?" on dashboards.
|
|
type SignerSetInfo struct {
|
|
// Members is the active signer-set roster. Order is meaningful:
|
|
// it matches the MPC protocol's index assignment, so logs that
|
|
// reference "party 0", "party 1" etc. line up with this slice.
|
|
Members []SignerMember `json:"members"`
|
|
|
|
// Threshold is the minimum number of members that must cooperate
|
|
// to produce a valid signature (the "t" in t-of-n). At least 1,
|
|
// at most len(Members).
|
|
Threshold int `json:"threshold"`
|
|
|
|
// Total is the cardinality of the signer set (len(Members)).
|
|
// Reported separately so a caller can detect a partially-populated
|
|
// Members slice without recounting.
|
|
Total int `json:"total"`
|
|
|
|
// Epoch is the current LP-333 epoch number. Increments monotonically
|
|
// on every rotation. The bridge keys metrics + logs on this so an
|
|
// operator can correlate a sudden refund-rate spike with an epoch
|
|
// transition.
|
|
Epoch uint64 `json:"epoch"`
|
|
|
|
// SignerSetHash is a fingerprint of the (sorted) Members + Threshold,
|
|
// useful for fast diff-detection on the consumer side without
|
|
// re-comparing the full roster. Hex-encoded.
|
|
SignerSetHash string `json:"signerSetHash,omitempty"`
|
|
|
|
// LastRotationAt is the unix-seconds timestamp of the most recent
|
|
// epoch transition. Zero when never rotated (epoch 0 / genesis).
|
|
LastRotationAt int64 `json:"lastRotationAt,omitempty"`
|
|
}
|
|
|
|
// CurrentEpoch is the bridge_getCurrentEpoch result — the smaller
|
|
// payload meant for high-frequency polling (think 5-second cadence).
|
|
// SignerSetInfo is the snapshot for dashboards; CurrentEpoch is for
|
|
// "did anything change?" checks.
|
|
type CurrentEpoch struct {
|
|
// Epoch is the current epoch number; same value as
|
|
// SignerSetInfo.Epoch.
|
|
Epoch uint64 `json:"epoch"`
|
|
|
|
// SignerSetHash matches SignerSetInfo.SignerSetHash. Operators
|
|
// alert on this changing without an expected operator action.
|
|
SignerSetHash string `json:"signerSetHash,omitempty"`
|
|
|
|
// StartedAt is the unix-seconds timestamp at which the current
|
|
// epoch began (or 0 for genesis).
|
|
StartedAt int64 `json:"startedAt,omitempty"`
|
|
}
|
|
|
|
// CancelResult is the bridge_cancelRequest result.
|
|
type CancelResult struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
// =============================================================================
|
|
// Method param shapes
|
|
// =============================================================================
|
|
|
|
// EstimateFeeParams is the bridge_estimateFee request body.
|
|
type EstimateFeeParams 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"`
|
|
}
|
|
|
|
// SubmitRequestParams is the bridge_submitRequest request body.
|
|
type SubmitRequestParams 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"`
|
|
}
|
|
|
|
// =============================================================================
|
|
// Client
|
|
// =============================================================================
|
|
|
|
// Default request timeout when Client.Timeout is zero.
|
|
const DefaultTimeout = 10 * time.Second
|
|
|
|
// Client is a JSON-RPC client for the BridgeVM (b-chain) and, optionally,
|
|
// the ThresholdVM (t-chain). Safe for concurrent use.
|
|
type Client struct {
|
|
// BridgeRPCURL is the BridgeVM JSON-RPC endpoint
|
|
// (e.g. https://node.lux.network/v1/chain/B/rpc). Required.
|
|
BridgeRPCURL string
|
|
|
|
// ThresholdRPCURL is the ThresholdVM JSON-RPC endpoint. Optional;
|
|
// when empty, calls to thresholdCall return an error so the caller
|
|
// gets an obvious "not configured" signal instead of a 404.
|
|
ThresholdRPCURL string
|
|
|
|
// Timeout bounds each individual RPC call. Zero means DefaultTimeout.
|
|
// The caller's context.Context cancellation is honored in addition.
|
|
Timeout time.Duration
|
|
|
|
// HTTPClient is the underlying http.Client. Zero means http.DefaultClient.
|
|
HTTPClient *http.Client
|
|
|
|
// idSeq is a monotonic per-Client counter used for the JSON-RPC `id`
|
|
// field. Combined with the random prefix it gives unique ids for
|
|
// concurrent calls. Atomic so multiple goroutines can call methods
|
|
// at once without external locking.
|
|
idSeq atomic.Uint64
|
|
|
|
// idPrefix is the random per-Client suffix appended to ids. Set
|
|
// lazily on first call (via nextID) so a zero-value Client works.
|
|
idPrefix atomic.Pointer[string]
|
|
}
|
|
|
|
// New constructs a Client with sane defaults. NodeURL is a convenience
|
|
// that derives both BridgeRPCURL and ThresholdRPCURL from a single
|
|
// Lux-node base; for finer control set the fields on a zero-value Client
|
|
// directly.
|
|
//
|
|
// c := bchain.New("https://api.lux-test.network", 0)
|
|
// info, err := c.GetBridgeInfo(ctx)
|
|
func New(nodeURL string, timeout time.Duration) *Client {
|
|
if nodeURL == "" {
|
|
nodeURL = "http://127.0.0.1:9650"
|
|
}
|
|
return &Client{
|
|
BridgeRPCURL: nodeURL + "/v1/chain/B/rpc",
|
|
ThresholdRPCURL: nodeURL + "/v1/chain/T/rpc",
|
|
Timeout: timeout,
|
|
}
|
|
}
|
|
|
|
// nextID returns the next JSON-RPC id for this client. Format:
|
|
// `<random6>-<monotonic>`. Random portion is generated once per client
|
|
// instance on first call.
|
|
func (c *Client) nextID() string {
|
|
pfx := c.idPrefix.Load()
|
|
if pfx == nil {
|
|
var buf [3]byte
|
|
if _, err := rand.Read(buf[:]); err == nil {
|
|
s := hex.EncodeToString(buf[:])
|
|
c.idPrefix.CompareAndSwap(nil, &s)
|
|
pfx = c.idPrefix.Load()
|
|
} else {
|
|
// Fallback: zeros. Collisions don't matter — id is for
|
|
// node-side log correlation, not authentication.
|
|
s := "000000"
|
|
c.idPrefix.CompareAndSwap(nil, &s)
|
|
pfx = c.idPrefix.Load()
|
|
}
|
|
}
|
|
n := c.idSeq.Add(1)
|
|
return fmt.Sprintf("%s-%d", *pfx, n)
|
|
}
|
|
|
|
func (c *Client) httpClient() *http.Client {
|
|
if c.HTTPClient != nil {
|
|
return c.HTTPClient
|
|
}
|
|
return http.DefaultClient
|
|
}
|
|
|
|
func (c *Client) timeout() time.Duration {
|
|
if c.Timeout > 0 {
|
|
return c.Timeout
|
|
}
|
|
return DefaultTimeout
|
|
}
|
|
|
|
// rpc is the single RPC transport used by every method. The caller's
|
|
// context cancels the request; an internal timeout (c.timeout()) bounds
|
|
// the call independently so a hung node can't block indefinitely even
|
|
// when the caller passed context.Background().
|
|
func (c *Client) rpc(ctx context.Context, url, method string, params any, out any) error {
|
|
if url == "" {
|
|
return &RPCError{
|
|
Method: method,
|
|
Code: -32601,
|
|
Message: "rpc url not configured (set BridgeRPCURL / ThresholdRPCURL)",
|
|
}
|
|
}
|
|
|
|
body, err := json.Marshal(jsonrpcRequest{
|
|
JSONRPC: "2.0",
|
|
ID: c.nextID(),
|
|
Method: method,
|
|
Params: params,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("bchain: marshal %s params: %w", method, err)
|
|
}
|
|
|
|
callCtx, cancel := context.WithTimeout(ctx, c.timeout())
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(callCtx, http.MethodPost, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("bchain: build request for %s: %w", method, err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := c.httpClient().Do(req)
|
|
if err != nil {
|
|
// Distinguish context cancellation from other transport errors
|
|
// so callers can react (context.Canceled vs RPCError).
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
return err
|
|
}
|
|
return &RPCError{
|
|
Method: method,
|
|
Code: -32000,
|
|
Message: err.Error(),
|
|
}
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return &RPCError{
|
|
Method: method,
|
|
HTTPStatus: resp.StatusCode,
|
|
Message: fmt.Sprintf("read response: %v", err),
|
|
}
|
|
}
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
// Try to surface a JSON-RPC error nested in a non-2xx response.
|
|
var maybeRpc jsonrpcResponse
|
|
if json.Unmarshal(respBody, &maybeRpc) == nil && maybeRpc.Error != nil {
|
|
return &RPCError{
|
|
Method: method,
|
|
HTTPStatus: resp.StatusCode,
|
|
Code: maybeRpc.Error.Code,
|
|
Message: maybeRpc.Error.Message,
|
|
Data: maybeRpc.Error.Data,
|
|
}
|
|
}
|
|
return &RPCError{
|
|
Method: method,
|
|
HTTPStatus: resp.StatusCode,
|
|
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, truncate(respBody, 256)),
|
|
}
|
|
}
|
|
|
|
var parsed jsonrpcResponse
|
|
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
|
return &RPCError{
|
|
Method: method,
|
|
HTTPStatus: resp.StatusCode,
|
|
Message: fmt.Sprintf("decode response: %v (body=%s)", err, truncate(respBody, 200)),
|
|
}
|
|
}
|
|
if parsed.Error != nil {
|
|
return &RPCError{
|
|
Method: method,
|
|
Code: parsed.Error.Code,
|
|
Message: parsed.Error.Message,
|
|
Data: parsed.Error.Data,
|
|
}
|
|
}
|
|
if len(parsed.Result) == 0 || string(parsed.Result) == "null" {
|
|
return &RPCError{
|
|
Method: method,
|
|
Code: -32603,
|
|
Message: "empty result",
|
|
}
|
|
}
|
|
if out != nil {
|
|
if err := json.Unmarshal(parsed.Result, out); err != nil {
|
|
return &RPCError{
|
|
Method: method,
|
|
Code: -32603,
|
|
Message: fmt.Sprintf("decode result: %v", err),
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// truncate clips a byte slice for inclusion in error messages.
|
|
func truncate(b []byte, n int) string {
|
|
if len(b) <= n {
|
|
return string(b)
|
|
}
|
|
return string(b[:n]) + "…"
|
|
}
|
|
|
|
// bridgeCall invokes a method against the BridgeVM endpoint.
|
|
func (c *Client) bridgeCall(ctx context.Context, method string, params, out any) error {
|
|
return c.rpc(ctx, c.BridgeRPCURL, method, params, out)
|
|
}
|
|
|
|
// thresholdCall invokes a method against the ThresholdVM endpoint.
|
|
// Returns an error tagged with method == "" when no t-chain URL is set,
|
|
// so callers don't accidentally fire off to a misconfigured Client.
|
|
func (c *Client) thresholdCall(ctx context.Context, method string, params, out any) error {
|
|
if c.ThresholdRPCURL == "" {
|
|
return &RPCError{
|
|
Method: method,
|
|
Code: -32601,
|
|
Message: "ThresholdRPCURL not configured",
|
|
}
|
|
}
|
|
return c.rpc(ctx, c.ThresholdRPCURL, method, params, out)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Bridge: quote / submit / status / cancel
|
|
// =============================================================================
|
|
|
|
// EstimateFee returns the fee + receive-amount estimate for a bridge intent.
|
|
func (c *Client) EstimateFee(ctx context.Context, params EstimateFeeParams) (*FeeEstimate, error) {
|
|
var out FeeEstimate
|
|
if err := c.bridgeCall(ctx, "bridge_estimateFee", params, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// SubmitBridgeRequest creates a new bridge intent server-side. The
|
|
// returned record carries the RequestID the caller polls via GetBridgeStatus.
|
|
func (c *Client) SubmitBridgeRequest(ctx context.Context, params SubmitRequestParams) (*BridgeRequest, error) {
|
|
var out BridgeRequest
|
|
if err := c.bridgeCall(ctx, "bridge_submitRequest", params, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// GetBridgeStatus reads the current state of a bridge request.
|
|
func (c *Client) GetBridgeStatus(ctx context.Context, requestID string) (*BridgeRequest, error) {
|
|
var out BridgeRequest
|
|
if err := c.bridgeCall(ctx, "bridge_getStatus", map[string]string{"requestId": requestID}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// CancelRequest cancels a pending bridge request.
|
|
func (c *Client) CancelRequest(ctx context.Context, requestID string) (*CancelResult, error) {
|
|
var out CancelResult
|
|
if err := c.bridgeCall(ctx, "bridge_cancelRequest", map[string]string{"requestId": requestID}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Bridge: discovery
|
|
// =============================================================================
|
|
|
|
// GetBridgeInfo reads node-level info (mpc readiness, threshold, totals).
|
|
func (c *Client) GetBridgeInfo(ctx context.Context) (*BridgeInfo, error) {
|
|
var out BridgeInfo
|
|
if err := c.bridgeCall(ctx, "bridge_getInfo", nil, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// GetSupportedChains lists the chains the BridgeVM supports.
|
|
func (c *Client) GetSupportedChains(ctx context.Context) ([]ChainConfig, error) {
|
|
var out []ChainConfig
|
|
if err := c.bridgeCall(ctx, "bridge_getSupportedChains", nil, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// GetChainConfig fetches the per-chain config for the given chain id.
|
|
func (c *Client) GetChainConfig(ctx context.Context, chainID string) (*ChainConfig, error) {
|
|
var out ChainConfig
|
|
if err := c.bridgeCall(ctx, "bridge_getChainConfig", map[string]string{"chainId": chainID}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// Health is the BridgeVM liveness probe. Used by the cmd/bridge proxy
|
|
// to decide whether RPC is reachable before routing a request.
|
|
func (c *Client) Health(ctx context.Context) (*Health, error) {
|
|
var out Health
|
|
if err := c.bridgeCall(ctx, "bridge_health", nil, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Bridge: MPC (shared signing party)
|
|
// =============================================================================
|
|
|
|
// GetMPCPublicKey returns the active MPC public key.
|
|
func (c *Client) GetMPCPublicKey(ctx context.Context) (*MPCPublicKey, error) {
|
|
var out MPCPublicKey
|
|
if err := c.bridgeCall(ctx, "bridge_getMPCPublicKey", nil, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// GetBridgeSignature retrieves the MPC signature for a completed
|
|
// bridge request.
|
|
func (c *Client) GetBridgeSignature(ctx context.Context, requestID string) (*BridgeSignature, error) {
|
|
var out BridgeSignature
|
|
if err := c.bridgeCall(ctx, "bridge_getSignature", map[string]string{"requestId": requestID}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// LP-333: signer set + epoch coordination
|
|
// =============================================================================
|
|
|
|
// GetSignerSetInfo reads the active LP-333 signer set: roster, threshold,
|
|
// epoch, and a content hash for fast diff-detection. Operators use this
|
|
// to confirm the bridge is pointed at the expected MPC cluster identity
|
|
// after a rotation; on-call alerts on SignerSetHash drift between scrapes.
|
|
//
|
|
// Returns an *RPCError with Code -32601 when the upstream BridgeVM
|
|
// doesn't implement LP-333 yet — callers should treat that as "b-chain
|
|
// is not authoritative on signer set here; fall back to assumed state."
|
|
func (c *Client) GetSignerSetInfo(ctx context.Context) (*SignerSetInfo, error) {
|
|
var out SignerSetInfo
|
|
if err := c.bridgeCall(ctx, "bridge_getSignerSetInfo", nil, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
// Defensive: if the upstream returned Members without Total, populate
|
|
// it from len(Members) so consumers don't have to recount.
|
|
if out.Total == 0 && len(out.Members) > 0 {
|
|
out.Total = len(out.Members)
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// GetCurrentEpoch reads the current LP-333 epoch number + signer-set
|
|
// hash. Cheap to poll — meant to drive a "did anything change?" gauge
|
|
// without re-fetching the full roster.
|
|
//
|
|
// Returns an *RPCError with Code -32601 when the upstream BridgeVM
|
|
// doesn't implement LP-333 yet. Same fallback contract as GetSignerSetInfo.
|
|
func (c *Client) GetCurrentEpoch(ctx context.Context) (*CurrentEpoch, error) {
|
|
var out CurrentEpoch
|
|
if err := c.bridgeCall(ctx, "bridge_getCurrentEpoch", nil, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|