mirror of
https://github.com/luxfi/wallet.git
synced 2026-10-30 15:13:19 +00:00
Add apps/backend — the wallet's custody server. The load-bearing Front-E gap
(repo was FE-only) is now closed: a wallet account model backed by MPC custody
where the private key is split t-of-n across the MPC nodes (lux →
mpc.lux.network) and NEVER assembled — this service stores only public keys +
addresses.
- internal/iam: lux.id OIDC bearer verification via JWKS (discovery + RS256/
ES256, alg-confusion-safe — HMAC/none refused). Org from the 'owner' claim.
golang-jwt/v5 (matches the lux ecosystem), stdlib JWKS fetcher (no extra dep).
- internal/custody: the Custodian port (a value the wallet owns) + HTTP adapter
to lux/mpc's verified /keygen + /sign wire. Mirrors mpc client.Threshold's
security contract (idempotency required, org-scoped). PQ-ready: mldsa65 is a
first-class scheme (the PQ primitives stay in pkgs/wallet/.../pq + consensus —
not duplicated). No in-process key fallback — unconfigured MPC is an error.
- internal/store: wallet↔org↔addresses (no key field). Org-scoped reads — a
cross-org get is ErrNotFound (no existence disclosure).
- internal/api: /v1/* (no /api/ prefix). IAM-gated, org-scoped. POST/GET
/v1/wallets, GET /v1/wallets/{id}, POST /v1/wallets/{id}/sign, GET /v1/health.
- cmd/wallet-backend: env config (white-label per tenant: IAM_ISSUER/AUDIENCE,
MPC_ENDPOINT — shared hanzo-mpc or BYOMPC), JSON logs, graceful shutdown.
Deploy (native CI + operator, NO GHA): build-only .platform.yml → ghcr.io/
luxfi/wallet-backend on lux-build pools; lux operator rolls
apps/backend/k8s/wallet-backend.yaml (lux.cloud/v1 Service + KMSSecret for
MPC_SERVICE_TOKEN), ingress wallet-api.lux.network.
17 tests green (go test ./...): IAM (valid/no-org/wrong-aud/expired/alg-
confusion), custody (no-key-leak, idempotency-required, PQ-accepted, no-
fallback), API (auth-gate, org-isolation cross-org→404, create→sign), + a full
E2E through the real adapter→mock-MPC→real-server. Binary boots, /v1/health ok,
/v1/wallets 401 unauth. LLM.md captures the App(Wallet) spec for the unified
operator's App Kind.
76 lines
3.1 KiB
Go
76 lines
3.1 KiB
Go
// Package custody is the wallet backend's custody port — the ONE interface the
|
|
// API depends on to create wallets and sign transactions. Custody is MPC: the
|
|
// private key is split t-of-n across the MPC nodes and NEVER exists in one
|
|
// place, so this backend stores public keys + addresses only — no plaintext
|
|
// key, ever.
|
|
//
|
|
// The port mirrors the security contract of github.com/luxfi/mpc/client's
|
|
// Threshold interface (idempotency, org-scoping) but the wallet owns its own
|
|
// minimal interface (decomplect: depend on a value we define, not on MPC's
|
|
// whole module). The HTTP adapter (mpc.go) speaks the real lux/mpc wire.
|
|
package custody
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
// Scheme is a signature scheme. PQ schemes (mldsa65) are first-class — the
|
|
// custody layer is PQ-ready; the PQ primitives themselves live in the wallet's
|
|
// pkgs/wallet/src/features/wallet/pq (Go-verified ML-DSA), not duplicated here.
|
|
type Scheme string
|
|
|
|
const (
|
|
SchemeSecp256k1 Scheme = "secp256k1" // EVM / BTC
|
|
SchemeEd25519 Scheme = "ed25519" // Solana
|
|
SchemeMLDSA65 Scheme = "mldsa65" // post-quantum (FIPS-204)
|
|
)
|
|
|
|
// Wallet is a custody wallet: an MPC-held key referenced by WalletID, with its
|
|
// derived public addresses. There is no field for a private key — by design.
|
|
type Wallet struct {
|
|
WalletID string `json:"walletId"`
|
|
ECDSAPubKey string `json:"ecdsaPubKey,omitempty"`
|
|
EDDSAPubKey string `json:"eddsaPubKey,omitempty"`
|
|
Addresses map[string]string `json:"addresses"` // chain → address (evm, btc, sol)
|
|
}
|
|
|
|
// SignRequest is a custody signing request. PayloadHash is the 32-byte tx hash
|
|
// to sign; the backend never sees or reconstructs a private key. Idempotency is
|
|
// required — a replayed key with a different payload MUST be refused (anti-
|
|
// oracle), matching the MPC client contract.
|
|
type SignRequest struct {
|
|
WalletID string
|
|
Scheme Scheme
|
|
ChainID int
|
|
PayloadHash string // 32-byte hex
|
|
IdempotencyKey string
|
|
}
|
|
|
|
// SignResult carries the signature bytes (hex). The signature is produced by
|
|
// the MPC threshold protocol; no key reconstruction occurs.
|
|
type SignResult struct {
|
|
Signature string `json:"signature"`
|
|
SessionID string `json:"sessionId,omitempty"`
|
|
}
|
|
|
|
var (
|
|
ErrIdempotencyKeyRequired = errors.New("custody: idempotency key required")
|
|
ErrUnsupportedScheme = errors.New("custody: unsupported scheme")
|
|
ErrBackendUnavailable = errors.New("custody: backend unavailable")
|
|
ErrKeygenFailed = errors.New("custody: keygen failed")
|
|
)
|
|
|
|
// Custodian is the MPC custody backend. org scopes every call to one tenant —
|
|
// the API derives it from the verified lux.id identity, never from the client.
|
|
type Custodian interface {
|
|
// CreateWallet runs an MPC distributed keygen for the org and returns the
|
|
// resulting Wallet (public keys + addresses). The private key is generated
|
|
// in shares across the MPC nodes and is never assembled.
|
|
CreateWallet(ctx context.Context, org string) (*Wallet, error)
|
|
|
|
// Sign requests an MPC threshold signature over req.PayloadHash for the
|
|
// org's wallet. No private key is reconstructed.
|
|
Sign(ctx context.Context, org string, req SignRequest) (*SignResult, error)
|
|
}
|