Files
bridge/app/server/vitest.config.ts
zeekay a7030ec59b feat(app/server): real Utila cosigner implementation + 13 tests (issue #386)
Replaces the runUtila STUB with a real evm_personal_sign attestation
flow against @luxfi/utila Connect-RPC. After this commit the layered
cosigner feature is feature-complete for BOTH providers — no stubs
remain in app/server/src/domain/cosigners.ts.

Flow:
  1. Build per-tenant gRPC client via createGrpcClient + serviceAccount-
     AuthStrategy with { email: intent.org_id, privateKey: () => secretPem }
     — mirrors the env-singleton client in domain/utila.ts but isolates
     each tenant's auth context so multiple tenants cosign in parallel
     without singleton contention.

  2. initiateTransaction({
       parent: vaults/<intent.vault_id>,
       details: { details: { case: 'evmPersonalSign', value: {
         fromAddress: intent.client_id, messageHex: opts.txHash
       } } },
       priority: NORMAL, note: 'lux-bridge cosign swap=<id>'
     })

     vault_id is REQUIRED — utila scopes initiateTransaction by parent
     vault. Missing vault_id returns 'failed' before any RPC.

  3. Poll getTransaction(name) every 1.5s, partition Utila's
     TransactionState_Enum (proto path:
     pkg/utila/src/lib/gen/utila/api/v1/transactions_pb.ts) into:

       approved   SIGNED=4, AWAITING_PUBLISH=5, PUBLISHED=6,
                  MINED=7, CONFIRMED=13
       rejected   DECLINED=9, CANCELED=11, DROPPED=12, EXPIRED=15
       failed     FAILED=8, REPLACED=10

     If state is in 'approved' but evmMessage.signature isn't populated
     yet (Utila publishes the sig in a later state transition), keep
     polling — don't bail. Intermediates (AWAITING_APPROVAL=2,
     AWAITING_POLICY_CHECK=14, AWAITING_SIGNATURE=3) also keep polling.

  4. 60s timeout ceiling (UTILA_COSIGNER_TIMEOUT_MS env override).
     Dynamic read on each call so config picks up without a restart.

  5. Return CosignResult with signature converted from Uint8Array proto
     bytes to 0x-prefixed hex; externalId = Utila transaction.name.

Edge cases covered by 13 tests (mocked @luxfi/utila):
  - happy: SIGNED + signature → approved
  - happy: CONFIRMED (later state, sig already attested) → approved
  - happy: SIGNED-without-sig keeps polling, picks up next tick
  - rejected: DECLINED, CANCELED, EXPIRED, DROPPED — all map to rejected
  - failed: FAILED proto state
  - failed: initiateTransaction throws (auth: bad jwt)
  - failed: initiateTransaction returns no transaction.name
  - failed: timeout (AWAITING_* indefinitely)
  - failed: missing vault_id → fail BEFORE any RPC call
  - failed: env-var fallback unset → clear KMS-TODO error
  - wire-shape assertion: initiateTransaction is called with the right
    parent + evmPersonalSign + fromAddress + messageHex + note

Test infra:
  - vitest.config.ts gains a resolve.alias mapping @luxfi/utila to its
    workspace src/grpc-client.ts. The published 3.0.0 rollup-built
    dist/ is what npm consumers see, but the workspace package's
    local rollup config has a long-standing outDir-vs-file-path issue
    that prevents in-place builds. Vitest resolves the TS source
    directly and runtime production is unaffected.

Tests: 91 → 104 total (58 SDK + 46 server).
  Server breakdown: 16 validator + 10 Fireblocks + 7 persistence + 13 Utila.

After this commit there are no STUBs in the cosigner module. The
remaining work on #386 is the KMS round-trip in fetchCosignerSecret
(env-var fallback works for dev; production needs KMS path read).

User correction: 'utila was already 100% complete... please make sure
it's done' — addressed by replacing the runUtila STUB with the real
implementation that reuses the @luxfi/utila Connect-RPC machinery
already in the workspace.
2026-05-22 17:34:34 -07:00

28 lines
1.0 KiB
TypeScript

import { defineConfig } from "vitest/config"
import { fileURLToPath } from "node:url"
import path from "node:path"
// Server-side unit tests. Mirrors the `@/*` path alias used by the
// runtime (see tsconfig.json) so tests import the same way as src code.
const here = path.dirname(fileURLToPath(import.meta.url))
export default defineConfig({
test: {
environment: "node",
include: ["src/**/__tests__/*.test.ts"],
},
resolve: {
alias: {
"@": path.resolve(here, "src"),
// @luxfi/utila ships from a published rollup-built dist/ to npm,
// but the workspace dist/ is not built in-place (the rollup
// config has a long-standing outDir-vs-file-path issue). For
// tests, point at the TypeScript source directly — vitest can
// handle TS imports natively. Production builds resolve through
// the package.json main field once the published artifact is
// installed from npm.
"@luxfi/utila": path.resolve(here, "../../pkg/utila/src/grpc-client.ts"),
},
},
})