mirror of
https://github.com/luxfi/cosigner.git
synced 2026-10-30 16:59:04 +00:00
Decomplect Fireblocks / Utila / Anchorage co-signing out of luxfi/bridge into one orthogonal, stdlib-only leaf. Wire-shape validation keeps secret material off the wire; env + KMS secret stores fetch the secret half by public id; the real Fireblocks REST + RS256-JWT family signs, the Utila and Anchorage families fail loud until their vendor clients land; the dispatcher runs families in parallel behind an AllApproved gate. Errors name the cosigner KIND and never echo the resolved URI, tenant id, or secret path.
101 lines
4.6 KiB
Go
101 lines
4.6 KiB
Go
package cosigner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// anchorage.go — Anchorage Digital cosigner family (regulated qualified
|
|
// custodian).
|
|
//
|
|
// STATUS: BLOCKED ON VENDOR SPEC. RunAnchorage returns a typed
|
|
// *NotImplementedError (Status == StatusFailed, errors.Is(Err,
|
|
// ErrNotImplemented) == true). It is NOT a silent stub — every Anchorage
|
|
// intent fails LOUDLY and names the precise missing artifact.
|
|
//
|
|
// What's missing: the Anchorage Digital API spec / SDK. Unlike Fireblocks
|
|
// (real REST + JWT) and Utila (a TypeScript Connect-RPC client exists),
|
|
// there is NO Anchorage artifact anywhere under ~/work/lux — no `.proto`,
|
|
// no Go/TS SDK, no OpenAPI, no API client. Anchorage's production API
|
|
// authenticates each request with a public API key (Api-Access-Key
|
|
// header) plus a request signature over a canonical payload, and scopes
|
|
// operations to a custody vault; but the exact endpoint shapes,
|
|
// signature canonicalization, and operation/approval state machine are
|
|
// not derivable from anything local. Implementing against a guessed wire
|
|
// format would be the "stub dressed up as real" this module forbids, so
|
|
// we stop at the honest interface boundary.
|
|
//
|
|
// The wire type (AnchorageIntent), the KindAnchorage discriminator, and
|
|
// the parseAnchorage validator all live in cosigner.go alongside the
|
|
// other kinds — Anchorage is wired through ValidateIntents and the
|
|
// dispatcher exactly like Utila and Fireblocks. Only the real network
|
|
// flow (RunAnchorage's body) is blocked.
|
|
//
|
|
// When the spec lands, fill in RunAnchorage:
|
|
//
|
|
// 1. Build a per-tenant client from intent.APIKeyID (public) + the
|
|
// secret signing key the SecretStore returned (FetchAnchorage),
|
|
// against intent.APIHost (or Anchorage's production host).
|
|
// 2. Create a message-sign / transfer operation scoped to
|
|
// vaults/<vault_id> that attests to opts.TxHash.
|
|
// 3. Poll the operation to a terminal state and map it to:
|
|
// - StatusApproved (Signature = attestation, ExternalID = op id)
|
|
// - StatusRejected (policy / approval-workflow denial)
|
|
// - StatusFailed (transport / config error / timeout)
|
|
|
|
// anchorageMissingArtifact is the precise blocker named in every
|
|
// Anchorage not-implemented Result. One constant ⇒ identical, greppable
|
|
// message everywhere.
|
|
const anchorageMissingArtifact = "Anchorage Digital API spec / SDK " +
|
|
"(no .proto, SDK, OpenAPI, or API client found under ~/work/lux)"
|
|
|
|
// AnchorageFamily implements FamilyDispatcher with — once the Anchorage
|
|
// API spec lands — the real qualified-custody co-sign flow. The other
|
|
// families delegate to their configured delegates (default
|
|
// StubFamilyDispatcher) so this family can be wired standalone.
|
|
type AnchorageFamily struct {
|
|
// UtilaDelegate routes RunUtila calls when this family is wired
|
|
// standalone. Zero ⇒ StubFamilyDispatcher.
|
|
UtilaDelegate FamilyDispatcher
|
|
|
|
// FireblocksDelegate routes RunFireblocks calls when this family is
|
|
// wired standalone. Zero ⇒ StubFamilyDispatcher.
|
|
FireblocksDelegate FamilyDispatcher
|
|
|
|
// HTTPClient / PollInterval / Timeout / Now / Nonce land with the
|
|
// real client, mirroring FireblocksRESTFamily's pluggable surface.
|
|
// Timeout is declared now so the construction site is stable across
|
|
// the future port.
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// RunAnchorage is the integration point for the real Anchorage client.
|
|
// Today it fails LOUDLY with a typed *NotImplementedError naming the
|
|
// missing artifact + the call context (swap id, api key id, vault) for
|
|
// traceability. The "all listed must approve" policy then refunds the
|
|
// swap — never a silent drop.
|
|
func (a AnchorageFamily) RunAnchorage(_ context.Context, intent *AnchorageIntent, _ string, opts DispatchOptions) Result {
|
|
detail := fmt.Sprintf("swap=%s api_key_id=%s vault=%s",
|
|
opts.SwapID, intent.APIKeyID, intent.VaultID)
|
|
return notImplemented(Intent{Kind: KindAnchorage, Anchorage: intent}, anchorageMissingArtifact, detail)
|
|
}
|
|
|
|
// RunUtila delegates to the configured UtilaDelegate (default stub).
|
|
func (a AnchorageFamily) RunUtila(ctx context.Context, intent *UtilaIntent, secret string, opts DispatchOptions) Result {
|
|
delegate := a.UtilaDelegate
|
|
if delegate == nil {
|
|
delegate = StubFamilyDispatcher{}
|
|
}
|
|
return delegate.RunUtila(ctx, intent, secret, opts)
|
|
}
|
|
|
|
// RunFireblocks delegates to the configured FireblocksDelegate (default stub).
|
|
func (a AnchorageFamily) RunFireblocks(ctx context.Context, intent *FireblocksIntent, secret string, opts DispatchOptions) Result {
|
|
delegate := a.FireblocksDelegate
|
|
if delegate == nil {
|
|
delegate = StubFamilyDispatcher{}
|
|
}
|
|
return delegate.RunFireblocks(ctx, intent, secret, opts)
|
|
}
|