mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
Closes the persistence + state-machine gating gap from v1.1.12. After
this commit, layered cosigners are 2-of-2 enforced end-to-end on the
backend.
Prisma:
+ model CosignerStep — one row per intent. Public IDs only:
kind 'utila' | 'fireblocks'
public_id org_id (utila) or api_key (fireblocks)
api_host?, vault_id?
status 'pending' (default) | 'approved' | 'rejected' | 'failed'
signature?, reason?, external_id?
+ Swap.cosigner_steps relation (1:N)
Indexed on swap_id + status for the dispatch query.
Persistence path:
handleSwapCreation(...)
└─ persistCosignerIntents(swapId, cosigners) — Prisma ,
one create() per intent, all status='pending'. No-ops when no
intents listed (backwards compat — zero cost for native-MPC-only
tenants).
Dispatch path (issue #386 trust-boundary enforcement):
handlerUpdateMpcSignAction(swapId, txHash, ...)
└─ dispatchCosignersForSwap(swapId, signature, txHash)
├─ findMany({ swap_id, status: 'pending' })
├─ rowToIntent(...) — invert persistence
├─ dispatchCosigners(...) — runs Fireblocks (real) / Utila (stub)
├─ update each step with terminal state
└─ aggregate: 'all_approved' | 'any_rejected' | 'any_failed' | 'no_steps'
If aggregate is rejected/failed:
- Swap.status → SwapStatus.Failed
- Swap.fail_reason → 'Layered cosigner rejection: <kind>: <reason>'
- Never advances to UserPayoutPending; user payout step is unreachable
without 2-of-2 (native + every listed cosigner).
- cosigner_steps included in the response payload for traceability.
If aggregate is no_steps (native-only swap):
Existing code path runs unchanged — proves out the additive, opt-in
design from the SDK side.
Tests: 7 new persistence + dispatch tests (vi.hoisted Prisma mock).
- SDK: 58/58 unchanged
- Server: 26 → 33 (16 validator + 10 Fireblocks + 7 persistence)
What's still STUB after this commit:
- runUtila — waiting on the first Utila tenant to model the
InitiateTransaction subtype against their TAP policy
- fetchCosignerSecret KMS round-trip — env-var fallback works for
local dev; production needs the KMS path read
The Prisma schema change requires `prisma migrate dev` (in dev) +
`prisma migrate deploy` (in CI) to apply the CosignerStep table to
existing databases. The migration file itself is left for the new
dev to generate against the target Postgres instance — autogenerated
migrations need a connected DB and shouldn't be committed blind.
209 lines
7.4 KiB
Plaintext
209 lines
7.4 KiB
Plaintext
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
// provider = "sqlite"
|
|
url = env("POSTGRES_URL") // uses connection pooling
|
|
directUrl = env("POSTGRES_URL") // uses a direct connection
|
|
}
|
|
|
|
// pnpx prisma generate
|
|
// npx prisma db push
|
|
// npx prisma studio
|
|
// prisma migrate dev
|
|
|
|
model Network {
|
|
id Int @id @default(autoincrement())
|
|
display_name String?
|
|
internal_name String? @unique
|
|
native_currency String?
|
|
is_testnet Boolean?
|
|
is_featured Boolean?
|
|
logo String?
|
|
chain_id String?
|
|
type String?
|
|
average_completion_time String?
|
|
transaction_explorer_template String?
|
|
account_explorer_template String?
|
|
listing_date DateTime?
|
|
source_network Swap[] @relation("sourceNetwork")
|
|
destination_network Swap[] @relation("destinationNetwork")
|
|
currencies Currency[]
|
|
deposit_actions DepositAction[]
|
|
transaction Transaction[]
|
|
nodes RpcNode[]
|
|
}
|
|
|
|
model Currency {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
asset String
|
|
logo String?
|
|
contract_address String?
|
|
decimals Int?
|
|
price_in_usd Float?
|
|
precision Int?
|
|
is_native Boolean?
|
|
listing_date DateTime @default(now())
|
|
deposit_action_currency DepositAction[] @relation("DepositActionCurrency")
|
|
transaction Transaction[]
|
|
network_id Int
|
|
network Network @relation(fields: [network_id], references: [id])
|
|
source_currency Swap[] @relation("sourceAsset")
|
|
destination_currency Swap[] @relation("destinationAsset")
|
|
|
|
@@unique([network_id, asset]) // Composite unique constraint
|
|
@@index([network_id])
|
|
@@index([asset])
|
|
}
|
|
|
|
model DepositAction {
|
|
id Int @id @default(autoincrement())
|
|
type String? @default("transfer")
|
|
status String
|
|
from String
|
|
to String
|
|
amount Float
|
|
transaction_hash String
|
|
max_confirmations Int
|
|
confirmations Int
|
|
created_date DateTime @default(now())
|
|
timestamp DateTime @default(now())
|
|
|
|
network_id Int
|
|
network Network @relation(fields: [network_id], references: [id])
|
|
currency_id Int
|
|
currency Currency @relation("DepositActionCurrency", fields: [currency_id], references: [id])
|
|
swap_id String
|
|
swap Swap @relation(fields: [swap_id], references: [id])
|
|
|
|
@@index([swap_id])
|
|
@@index([transaction_hash])
|
|
@@index([currency_id])
|
|
@@index([network_id])
|
|
}
|
|
|
|
model Swap {
|
|
id String @id @default(uuid())
|
|
created_date DateTime @default(now())
|
|
source_network_id Int
|
|
source_network Network @relation("sourceNetwork", fields: [source_network_id], references: [id])
|
|
source_exchange String?
|
|
source_asset_id Int
|
|
source_asset Currency @relation("sourceAsset", fields: [source_asset_id], references: [id])
|
|
source_address String
|
|
destination_network_id Int
|
|
destination_network Network @relation("destinationNetwork", fields: [destination_network_id], references: [id])
|
|
destination_exchange String?
|
|
destination_asset_id Int
|
|
destination_asset Currency @relation("destinationAsset", fields: [destination_asset_id], references: [id])
|
|
destination_address String
|
|
refuel Boolean
|
|
use_deposit_address Boolean
|
|
use_teleporter Boolean? @default(false)
|
|
requested_amount Float
|
|
status String
|
|
fail_reason String?
|
|
metadata_sequence_number Int?
|
|
|
|
deposit_address String?
|
|
|
|
quotes Quote?
|
|
deposit_actions DepositAction[]
|
|
transactions Transaction[]
|
|
cosigner_steps CosignerStep[]
|
|
|
|
@@index([id])
|
|
}
|
|
|
|
/// Per-swap cosigner record — layered Utila / Fireblocks cosign on top
|
|
/// of the native MPC threshold network (issue #386). One row per
|
|
/// intent listed in the original POST /api/swaps cosigners[] array.
|
|
/// Lifecycle:
|
|
/// pending — created at swap-create time alongside the swap row
|
|
/// approved — external custodian signed; `signature` populated
|
|
/// rejected — external denial; `reason` carries subStatus / detail
|
|
/// failed — transport / config error or terminal Fireblocks FAILED;
|
|
/// also surfaced as fail_reason on the parent Swap
|
|
///
|
|
/// Public IDs only (`public_id` = org_id for utila, api_key for
|
|
/// fireblocks). NEVER stores secret material — secrets stay in KMS,
|
|
/// keyed off `public_id` at dispatch time.
|
|
model CosignerStep {
|
|
id Int @id @default(autoincrement())
|
|
created_date DateTime @default(now())
|
|
updated_date DateTime @updatedAt
|
|
|
|
swap_id String
|
|
swap Swap @relation(fields: [swap_id], references: [id])
|
|
|
|
// Discriminator + public identifiers — match the wire shape exactly.
|
|
kind String // "utila" | "fireblocks"
|
|
public_id String // org_id (utila) or api_key (fireblocks)
|
|
api_host String?
|
|
vault_id String? // utila vault_id OR fireblocks vault_account_id
|
|
|
|
// Step state.
|
|
status String @default("pending") // "pending" | "approved" | "rejected" | "failed"
|
|
signature String?
|
|
reason String?
|
|
external_id String? // utila tx name OR fireblocks tx id
|
|
|
|
@@index([swap_id])
|
|
@@index([status])
|
|
}
|
|
|
|
model Transaction {
|
|
id Int @id @default(autoincrement())
|
|
type String? @default("transfer")
|
|
status String
|
|
from String
|
|
to String
|
|
amount Float
|
|
transaction_hash String
|
|
max_confirmations Int
|
|
confirmations Int
|
|
created_date DateTime @default(now())
|
|
timestamp DateTime @default(now())
|
|
|
|
network_id Int
|
|
network Network @relation(fields: [network_id], references: [id])
|
|
currency_id Int
|
|
currency Currency @relation(fields: [currency_id], references: [id])
|
|
swap_id String
|
|
swap Swap @relation(fields: [swap_id], references: [id])
|
|
|
|
@@index([swap_id])
|
|
@@index([transaction_hash])
|
|
@@index([currency_id])
|
|
@@index([network_id])
|
|
}
|
|
|
|
model Quote {
|
|
id Int @id @default(autoincrement())
|
|
swap_id String @unique @map("swap_id")
|
|
receive_amount Float
|
|
min_receive_amount Float
|
|
blockchain_fee Float
|
|
service_fee Float
|
|
avg_completion_time String
|
|
slippage Float
|
|
total_fee Float
|
|
total_fee_in_usd Float
|
|
swap Swap @relation(fields: [swap_id], references: [id])
|
|
}
|
|
|
|
model RpcNode {
|
|
id Int @id @default(autoincrement())
|
|
url String
|
|
network_id Int
|
|
network Network @relation(fields: [network_id], references: [id])
|
|
}
|