Files
bridge/app/server/src/domain/swaps.ts
T
zeekay dd406080d9 feat(app/server): CosignerStep Prisma model + state-machine gate (issue #386)
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.
2026-05-22 17:24:24 -07:00

1235 lines
35 KiB
TypeScript

import { Contract, formatEther, JsonRpcProvider, parseEther, parseUnits, Wallet } from "ethers"
import { TransactionType, swapStatusByIndex, SwapStatus, utilaTransactionStatusByIndex } from "@luxfi/core"
import { UTILA_NETWORKS } from "@/domain/constants"
import ERC20B_ABI from "@/domain/constants/ERC20B.json"
import logger from "@/logger"
import { prisma } from "@/prisma-instance"
import { isValidAddress } from "@/util"
import { getTokenPrice } from "./tokens"
import { isExitFromLux, BRIDGE_FEE_RATE } from "./quote"
import { createMPCWalletForDeposit, checkNativeDeposit, archiveMPCWallet, NETWORK_ASSET_MAP } from "./mpc-wallet"
import { isMPCSigningEnabled, mpcBridgeMint, mpcSendNative } from "./mpc-signer"
import type { CosignerIntent } from "./cosigners"
import {
dispatchCosignersForSwap,
persistCosignerIntents,
} from "./cosigners"
export interface SwapData {
amount: number
source_network: any
source_exchange?: string
source_asset: string
source_address: string
destination_network: string
destination_exchange?: string
destination_asset: string
destination_address: string
refuel: boolean
use_deposit_address: boolean
deposit_address?: string
use_teleporter: boolean
/**
* Optional layered cosigner intents (issue #386). The SDK forwards
* public identifiers only; secret material is fetched from KMS in
* `domain/cosigners.ts::fetchCosignerSecret`. When non-empty, the swap
* state machine MUST gate `broadcasting` on every listed cosigner
* returning `approved` in addition to the native MPC sign.
*
* Persistence: not yet wired to Prisma. The intent currently flows
* into the in-process logger via `handleSwapCreation`, and the
* `dispatchCosigners` call site (after native sign completes) is
* where the new dev needs to add `await dispatchCosigners({ swapId,
* nativeSignature, txHash, cosigners })` once a CosignerStep model
* (or Swap.metadata JSON column) lands.
*/
cosigners?: CosignerIntent[]
[property: string]: any
}
/* TODO
export type UpdateSwapData = {
confirmations: number
max_confirmations: number
amount: string | number
form: string
to: string
id: string
add_output_tx: string
add_input_tx: string
// [property: string]: any;
}
*/
/**
* Create swap according to users' input
* @param data SwapData
* @returns swap result
*/
export async function handleSwapCreation(data: SwapData) {
const { amount, source_network, source_exchange, source_asset, source_address, destination_network, destination_exchange, destination_asset, destination_address, refuel, use_deposit_address, use_teleporter, cosigners } = data
// Layered cosigners — log + thread the intent. TODO(#386): persist on
// the swap record (Prisma `Swap.metadata` JSON column or a related
// `CosignerStep` model) so the post-native-sign step can read it back.
// The validator in routes/swaps.ts has already rejected secret-like
// fields, so anything that reaches here is safe to log.
if (cosigners && cosigners.length > 0) {
logger.info(
`[swaps] layered cosigners requested (${cosigners.length}): ` +
cosigners.map((c) => c.kind).join(", "),
)
}
try {
// source network
const sourceNetwork = await prisma.network.findUnique({
where: {
internal_name: source_network
}
})
// source currency
const sourceCurrency = await prisma.currency.findFirst({
where: {
network_id: sourceNetwork?.id,
asset: source_asset
}
})
// destination network
const destinationNetwork = await prisma.network.findUnique({
where: {
internal_name: destination_network
}
})
// destination currency
const destinationCurrency = await prisma.currency.findFirst({
where: {
network_id: destinationNetwork?.id,
asset: destination_asset
}
})
let deposit_address = ""
if (use_deposit_address) {
// Native MPC wallet — replaces Utila
const wallet = await createMPCWalletForDeposit(source_network)
deposit_address = `${wallet.name}###${wallet.addresses[0].address}`
}
const swap = await prisma.swap.create({
data: {
requested_amount: amount,
source_network_id: Number(sourceNetwork?.id),
source_asset_id: Number(sourceCurrency?.id),
source_exchange,
source_address,
destination_network_id: Number(destinationNetwork?.id),
destination_asset_id: Number(destinationCurrency?.id),
destination_exchange,
destination_address,
refuel,
use_deposit_address,
deposit_address,
use_teleporter,
status: use_teleporter ? SwapStatus.UserTransferPending : SwapStatus.UserDepositPending,
quotes: {}
}
})
// Persist layered cosigner intents (issue #386). One CosignerStep row
// per intent at status="pending". The post-native-sign hook
// (handlerUpdateMpcSignAction) reads them back via
// dispatchCosignersForSwap and updates each row with its terminal
// status before allowing the swap to advance to broadcasting.
if (cosigners && cosigners.length > 0) {
await persistCosignerIntents(swap.id, cosigners)
}
// native currency for fee pay
const nativeCurrency = await prisma.currency.findFirst({
where: {
network_id: sourceNetwork?.id,
is_native: true
}
})
// estimate swap rate — 1% fee only on exits from Lux/Zoo
const [sourcePrice, destinationPrice] = await Promise.all([getTokenPrice(source_asset), getTokenPrice(destination_asset)])
const raw_receive_amount = Number(amount) * sourcePrice / destinationPrice
const feeRate = isExitFromLux(source_network, destination_network) ? BRIDGE_FEE_RATE : 0
const service_fee_amount = raw_receive_amount * feeRate
const receive_amount = raw_receive_amount - service_fee_amount
const service_fee_usd = service_fee_amount * destinationPrice
// save quote
const quote = await prisma.quote.create({
data: {
swap_id: swap.id,
receive_amount: isNaN(receive_amount) ? 0 : receive_amount,
min_receive_amount: isNaN(receive_amount) ? 0 : receive_amount * (1 - 0.025),
blockchain_fee: 0,
service_fee: feeRate,
avg_completion_time: "00:03:00",
slippage: 0.025,
total_fee: isNaN(service_fee_amount) ? 0 : service_fee_amount,
total_fee_in_usd: isNaN(service_fee_usd) ? 0 : service_fee_usd
}
})
const result = {
swap_id: swap.id,
swap: {
...swap,
source_network,
source_asset,
destination_network,
destination_asset,
deposit_address
},
quote: { ...quote },
refuel: null,
reward: null
}
return result
} catch (error: any) {
//catchPrismaKnowError(error)
throw new Error(`Error creating swap and related entities: ${String(error)}`)
}
}
export async function handleUpdateSwapTransactionByID(swapId: string, tokenId: number, networkId: number, type: TransactionType) {
// Transaction
await prisma.transaction.create({
data: {
status: "completed",
type: type,
from: "",
to: "",
transaction_hash: "",
confirmations: 2,
max_confirmations: 2,
amount: 2,
swap: {
connect: {
id: swapId
}
},
currency: {
connect: {
id: tokenId
}
},
network: {
connect: {
id: networkId
}
}
}
})
}
export async function handlerGetSwap(id: string) {
try {
const swap = await prisma.swap.findUnique({
where: { id },
include: {
source_network: true,
source_asset: true,
destination_network: true,
destination_asset: true,
deposit_actions: true,
quotes: true,
transactions: {
include: {
currency: true,
network: {
include: { currencies: false }
}
}
}
}
})
return {
...swap,
source_network: swap?.source_network?.internal_name,
source_asset: swap?.source_asset?.asset,
destination_network: swap?.destination_network?.internal_name,
destination_asset: swap?.destination_asset?.asset
}
} catch (error: any) {
//catchPrismaKnowError(error)
throw new Error(`Error getting swap: ${error?.message}`)
}
}
export async function handlerUpdateUserTransferAction(id: string, txHash: string, amount: number, from: string, to: string) {
try {
let swap = await prisma.swap.findUnique({
where: { id }
})
const transaction = await prisma.transaction.create({
data: {
type: TransactionType.Input,
status: "user_transfer",
from: from,
to: to,
amount: amount,
transaction_hash: txHash,
confirmations: 2,
max_confirmations: 2,
swap: {
connect: {
id: id
}
},
network: {
connect: {
id: swap?.source_network_id
}
},
currency: {
connect: {
id: swap?.source_asset_id
}
}
}
})
await prisma.swap.update({
where: { id },
data: {
status: SwapStatus.TeleportProcessPending,
transactions: {
connect: {
id: transaction.id // Connect the new transaction to the swap
}
}
}
})
swap = await prisma.swap.findUnique({
where: { id },
include: {
source_network: true,
source_asset: true,
destination_network: true,
destination_asset: true,
deposit_actions: true,
quotes: true,
transactions: {
include: {
currency: true,
network: {
include: { currencies: false }
}
}
}
}
})
return {
...swap
}
} catch (error: any) {
console.log(error)
//catchPrismaKnowError(error)
throw new Error(`Error getting swap: ${error?.message}`)
}
}
/**
*
* @param id ]
* @param txHash
* @param amount
* @param from
* @param to
* @returns
*/
export async function handlerUpdatePayoutAction(id: string, txHash: string, amount: number, from: string, to: string) {
try {
let swap = await prisma.swap.findUnique({
where: { id }
})
const transaction = await prisma.transaction.create({
data: {
status: "payout",
type: TransactionType.Output,
from: from,
to: to,
amount: amount,
transaction_hash: txHash,
confirmations: 2,
max_confirmations: 2,
swap: {
connect: {
id: id
}
},
network: {
connect: {
id: swap?.destination_network_id
}
},
currency: {
connect: {
id: swap?.destination_asset_id
}
}
}
})
await prisma.swap.update({
where: { id },
data: {
status: SwapStatus.PayoutSuccess,
transactions: {
connect: {
id: transaction.id // Connect the new transaction to the swap
}
}
}
})
swap = await prisma.swap.findUnique({
where: { id },
include: {
source_network: true,
source_asset: true,
destination_network: true,
destination_asset: true,
deposit_actions: true,
quotes: true,
transactions: {
include: {
currency: true,
network: {
include: { currencies: false }
}
}
}
}
})
return {
...swap
}
} catch (error: any) {
console.log(error)
//catchPrismaKnowError(error)
throw new Error(`Error getting swap: ${error?.message}`)
}
}
/**
*
* @param param0
* @returns
*/
export async function checkDepositAction({
asset,
wallet,
requestedAmount,
networkInternalName,
}: {
asset: string,
wallet: string,
requestedAmount: number,
networkInternalName?: string | null,
}) {
try {
// Extract address from wallet###address format
const address = wallet.includes('###') ? wallet.split('###')[1] : wallet
// If we have the network name, use native deposit check
if (networkInternalName) {
return await checkNativeDeposit({
networkInternalName,
address,
asset,
requiredAmount: requestedAmount,
})
}
// Fallback: try all known networks for this asset
for (const [network, assets] of Object.entries(NETWORK_ASSET_MAP)) {
if (assets[asset]) {
const result = await checkNativeDeposit({
networkInternalName: network,
address,
asset,
requiredAmount: requestedAmount,
})
if (result) return true
}
}
return false
} catch (err: any) {
console.error(">> Error while checking Deposit Action", err?.message)
return false
}
}
/**
*
* @param state
* @param hash
* @param amount
* @param asset
* @param sourceAddress
* @param destinationAddress
* @returns
*/
export async function handlerDepositAction(
state: number,
hash: string,
amount: number,
asset: string,
sourceAddress: string | undefined,
destinationAddress: string,
created: Date,
vault: string,
type: string
) {
console.log({
asset,
sourceAddress,
destinationAddress,
amount,
hash,
status: utilaTransactionStatusByIndex[state],
created,
type
})
const swap = await prisma.swap.findFirst({
where: {
deposit_address: {
endsWith: destinationAddress
}
},
include: {
deposit_actions: true,
source_network: true,
source_asset: true
}
})
if (!swap) {
throw new Error("There is No swap for this transaction")
}
// check if action already exists
const depositActions = swap.deposit_actions
const _depositAction = depositActions.find((d: any) => d.transaction_hash === hash)
// Check network and asset match using native mapping
const networkName = swap.source_network.internal_name as string
const assetMap = NETWORK_ASSET_MAP[networkName] || UTILA_NETWORKS[networkName]?.assets || {}
const _wallet = swap.deposit_address?.split('###')?.[0] as string
const _asset = assetMap[swap.source_asset.asset as string] || swap.source_asset.asset
if (!_asset) {
throw new Error(`Unrecognized asset ${swap.source_asset.asset} on ${networkName}`)
}
// if deposit action exists, update it else create new one
if (_depositAction) {
await prisma.depositAction.updateMany({
where: {
transaction_hash: hash
},
data: {
status: utilaTransactionStatusByIndex[state],
confirmations: state === 13 ? 10 : 0,
max_confirmations: 10,
created_date: created
}
})
console.log(`>> Successfully Updated Deposit Action for Swap [${swap.id}]`)
} else {
await prisma.depositAction.create({
data: {
status: utilaTransactionStatusByIndex[state],
from: sourceAddress ?? 'unknown sender...',
to: destinationAddress,
amount: amount,
transaction_hash: hash,
confirmations: state === 13 ? 10 : 0,
max_confirmations: 10,
created_date: created,
swap: {
connect: {
id: swap.id
}
},
network: {
connect: {
id: swap?.source_network_id
}
},
currency: {
connect: {
id: swap?.destination_network_id
}
}
}
})
}
// if confirmed, check whether deposit is
if (state === 13) { // if confirmed, check if deposit completed
const confirmed = await checkDepositAction({
asset: _asset,
wallet: _wallet,
requestedAmount: Number(swap.requested_amount),
networkInternalName: swap.source_network.internal_name,
})
if (confirmed) {
await prisma.swap.update({
where: { id: swap.id },
data: {
status: SwapStatus.BridgeTransferPending
}
})
console.log(`>> Deposit Completed for swap [${swap.id}]`)
}
}
console.log(`>> Successfully Created Deposit Action for Swap [${swap.id}]`)
}
/**
*
* @param swapId
*/
export async function handlerCheckDeposit(
swapId: string
) {
const swap = await prisma.swap.findUnique({
where: {
id: swapId
},
include: {
deposit_actions: true,
source_network: true,
source_asset: true
}
})
if (!swap) {
throw new Error("There is No swap for this transaction")
}
const networkName = swap.source_network.internal_name as string
const assetMap = NETWORK_ASSET_MAP[networkName] || UTILA_NETWORKS[networkName]?.assets || {}
const _wallet = swap.deposit_address?.split('###')?.[0] as string
const _asset = assetMap[swap.source_asset.asset as string] || swap.source_asset.asset
const unconfirmedActions = swap.deposit_actions.filter((d: any) => d.status !== 'CONFIRMED')
if (unconfirmedActions.length > 0) {
throw new Error(`Some transactions still not confirmed yet`)
}
// if confirmed, check whether deposit is
const confirmed = await checkDepositAction({
asset: _asset,
wallet: _wallet,
requestedAmount: Number(swap.requested_amount)
})
if (confirmed) {
await prisma.swap.update({
where: { id: swap.id },
data: {
status: SwapStatus.BridgeTransferPending
}
})
console.log(`>> Deposit Completed for swap [${swap.id}]`)
} else {
throw new Error(`Deposit is not confirmed yet for this swap`)
}
}
/**
* handler after deposit is checked
*/
export async function handlerUtilaPayoutAction(swapId: string) {
const swap = await prisma.swap.findUnique({
where: {
id: swapId
},
include: {
source_network: true,
source_asset: true,
destination_network: {
include: { currencies: false }
},
destination_asset: true,
quotes: true
}
})
if (!swap) {
throw new Error("No swap found for this id")
}
// Native MPC wallet and asset
const networkName = swap.source_network.internal_name as string
const assetMap = NETWORK_ASSET_MAP[networkName] || UTILA_NETWORKS[networkName]?.assets || {}
const _wallet = swap?.deposit_address?.split('###')?.[0] as string
const _asset = assetMap[swap.source_asset.asset as string] || swap.source_asset.asset
// if already minted, reject
const allowedStatuses = [SwapStatus.BridgeTransferPending, SwapStatus.TeleportProcessPending]
if (!allowedStatuses.includes(swap.status as SwapStatus)) {
throw new Error("Deposit is not completed or Already minted payout token for this swap")
}
// Teleporter swaps skip deposit verification — the user's transfer tx is already recorded
const isTeleporterSwap = swap.status === SwapStatus.TeleportProcessPending
let confirmed = isTeleporterSwap
if (!isTeleporterSwap) {
// check if deposit is completed (deposit-address flow only)
confirmed = await checkDepositAction({
asset: _asset,
wallet: _wallet,
requestedAmount: Number(swap.requested_amount)
})
}
if (confirmed) {
const feeCollector = process.env.BRIDGE_FEE_COLLECTOR || '0x0000000000000000000000000000000000000000'
// 1% fee only on exits from Lux/Zoo; entries are free
const isExitSwap = isExitFromLux(swap.source_network.internal_name || '', swap.destination_network?.internal_name || '')
const payoutAmount = isExitSwap
? (swap.quotes?.receive_amount ?? (swap.requested_amount * (1 - BRIDGE_FEE_RATE)))
: swap.requested_amount
const feeAmount = isExitSwap ? (swap.requested_amount - payoutAmount) : 0
// rpc urls — Zoo is an L2 subnet on Lux, NOT the Z-chain
const rpcs: Record<string, string> = {
'LUX_MAINNET': 'https://api.lux.network/ext/bc/C/rpc',
'LUX_TESTNET': 'https://api.lux-test.network/ext/bc/C/rpc',
'LUX_DEVNET': 'https://api.lux-dev.network/ext/bc/C/rpc',
'ZOO_MAINNET': 'https://api.lux.network/ext/bc/zoo/rpc',
'ZOO_TESTNET': 'https://api.lux-test.network/ext/bc/zoo/rpc',
'ZOO_DEVNET': 'https://api.lux-dev.network/ext/bc/zoo/rpc'
}
const rpc = rpcs[swap?.destination_network?.internal_name as string]
console.log(">> payout details", {
rpc,
token: swap.destination_asset.contract_address,
payoutAmount,
feeAmount,
feeCollector,
mpcEnabled: isMPCSigningEnabled()
})
if (!rpc) {
throw new Error(`No RPC found for chain ${swap?.destination_network?.internal_name}`)
}
const provider = new JsonRpcProvider(rpc)
const decimals = swap?.destination_asset.decimals ?? 18
const tokenAddress = swap.destination_asset.contract_address as string
const mintAmount = parseUnits(payoutAmount.toFixed(decimals), decimals)
const feeAmountWei = feeAmount > 0 ? parseUnits(feeAmount.toFixed(decimals), decimals) : 0n
const useMPC = isMPCSigningEnabled()
let signerAddress: string
let mintTxHash: string
try {
console.log(`>> Minting PayoutToken for this Swap [${swap.id}] (MPC: ${useMPC})`)
if (useMPC) {
// MPC-signed payout
mintTxHash = await mpcBridgeMint(provider, tokenAddress, swap.destination_address, mintAmount, ERC20B_ABI)
signerAddress = process.env.MPC_WALLET_ID || 'mpc'
console.log(`>> Minted L/Z tokens via MPC ${mintTxHash}`)
// Mint fee to fee collector via MPC
if (feeAmountWei > 0n && feeCollector !== '0x0000000000000000000000000000000000000000') {
try {
const feeTxHash = await mpcBridgeMint(provider, tokenAddress, feeCollector, feeAmountWei, ERC20B_ABI)
console.log(`>> Collected ${feeAmount} fee to ${feeCollector} via MPC (${feeTxHash})`)
} catch (feeErr) {
console.log(">> Fee collection failed (payout still succeeded):", feeErr)
}
}
} else {
// Legacy private key signer
const privateKey = process.env.LUX_SIGNER
const wallet = new Wallet(privateKey!, provider)
signerAddress = wallet.address
const contract = new Contract(tokenAddress, ERC20B_ABI, wallet)
const txMint = await contract.bridgeMint(swap.destination_address, mintAmount)
await txMint.wait()
mintTxHash = txMint.hash
console.log(`>> Minted L/Z tokens ${mintTxHash}`)
// Mint fee to fee collector
if (feeAmountWei > 0n && feeCollector !== '0x0000000000000000000000000000000000000000') {
try {
const txFee = await contract.bridgeMint(feeCollector, feeAmountWei)
await txFee.wait()
console.log(`>> Collected ${feeAmount} fee to ${feeCollector} (${txFee.hash})`)
} catch (feeErr) {
console.log(">> Fee collection failed (payout still succeeded):", feeErr)
}
}
}
await prisma.transaction.create({
data: {
status: "payout",
type: TransactionType.Output,
from: signerAddress,
to: swap.destination_address,
amount: payoutAmount,
transaction_hash: mintTxHash,
confirmations: 2,
max_confirmations: 2,
swap: {
connect: {
id: swap.id
}
},
network: {
connect: {
id: swap?.destination_network_id
}
},
currency: {
connect: {
id: swap?.destination_asset_id
}
}
}
})
await prisma.swap.update({
where: { id: swap.id },
data: {
status: SwapStatus.PayoutSuccess
}
})
} catch (err) {
console.log(">> Error while minting Z/L tokens", err)
throw new Error("Error while minting Z/L tokens")
}
// Refuel: send 1 native token if balance < 1
try {
const _balance = await provider.getBalance(swap.destination_address)
console.log(">> Lux or Zoo balance:", Number(formatEther(_balance)));
if (Number(formatEther(_balance)) < 1) {
console.log(">> Sending 1 L/Z token");
if (useMPC) {
const refuelHash = await mpcSendNative(provider, swap.destination_address, parseEther('1'))
console.log(`>> 1 Zoo or Lux sent via MPC to ${swap.destination_address} (${refuelHash})`)
} else {
const privateKey = process.env.LUX_SIGNER
const wallet = new Wallet(privateKey!, provider)
const feeData = await provider.getFeeData()
const _txSendLux = await wallet.sendTransaction({
to: swap.destination_address,
value: parseEther('1'),
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
gasLimit: 3000000,
})
await _txSendLux.wait()
console.log(`>> 1 Zoo or Lux is sent to ${swap.destination_address}`)
}
}
} catch (err) {
console.log(">> Error while sending 1 Z/L tokens", err)
}
return {
status: 'success',
msg: 'Z/L tokens have been minted successfully'
}
} else {
// if deposit is not completed, set status as UserTransferPending
await prisma.swap.update({
where: { id: swap.id },
data: {
status: SwapStatus.UserTransferPending
}
})
return {
status: 'ex',
msg: 'deposit not completed yet'
}
}
}
/**
* make swap expire if there is no deposit for 72h
* @param id
* @returns
*/
export async function handlerSwapExpire(id: string) {
try {
const swap = await prisma.swap.findUnique({
where: { id }
})
if (!swap) throw { message: "No swap found for swapId" }
const wallet = swap.deposit_address?.split("#")?.[0]
archiveMPCWallet(wallet as string)
await prisma.swap.update({
where: { id },
data: {
status: SwapStatus.Expired
}
})
} catch (error: any) {
console.log(error)
//catchPrismaKnowError(error)
throw new Error(`Error Getting Swap: ${error?.message}`)
}
}
/**
*
* @param id
* @param txHash
* @param amount
* @param from
* @param to
* @returns
*/
export async function handlerUpdateMpcSignAction(id: string, txHash: string, amount: number, from: string, to: string) {
try {
let swap = await prisma.swap.findUnique({
where: { id }
})
const transaction = await prisma.transaction.create({
data: {
status: "mpc_sign",
type: TransactionType.Input,
from: from,
to: to,
amount: amount,
transaction_hash: txHash,
confirmations: 2,
max_confirmations: 2,
swap: {
connect: {
id: id
}
},
network: {
connect: {
id: swap?.source_network_id
}
},
currency: {
connect: {
id: swap?.source_asset_id
}
}
}
})
// Layered cosigner gate (issue #386). After native MPC has produced
// a signature, every configured external cosigner (Utila /
// Fireblocks) must also approve before we advance the swap. If any
// listed cosigner rejects or fails, the swap is marked Failed with
// a user-visible fail_reason and we return early — the user-payout
// step is NEVER reached without 2-of-2 (native + every listed
// cosigner). For native-only swaps (cosigner_steps row count = 0),
// dispatchCosignersForSwap returns "no_steps" instantly and the
// existing code path runs unchanged.
const cosignVerdict = await dispatchCosignersForSwap(id, /*nativeSignature*/ txHash, txHash)
if (cosignVerdict.aggregate === "any_rejected" || cosignVerdict.aggregate === "any_failed") {
const failReason = `Layered cosigner ${cosignVerdict.aggregate === "any_rejected" ? "rejection" : "failure"}: ${cosignVerdict.failingReasons.join(" | ")}`
logger.warn(`[swaps] swap=${id} ${failReason}`)
await prisma.swap.update({
where: { id },
data: {
status: SwapStatus.Failed,
fail_reason: failReason,
transactions: {
connect: { id: transaction.id },
},
},
})
return prisma.swap.findUnique({
where: { id },
include: {
source_network: true,
source_asset: true,
destination_network: true,
destination_asset: true,
deposit_actions: true,
quotes: true,
transactions: { include: { currency: true, network: true } },
cosigner_steps: true,
},
})
}
await prisma.swap.update({
where: { id },
data: {
status: "user_payout_pending",
transactions: {
connect: {
id: transaction.id // Connect the new transaction to the swap
}
}
}
})
swap = await prisma.swap.findUnique({
where: { id },
include: {
source_network: true,
source_asset: true,
destination_network: true,
destination_asset: true,
deposit_actions: true,
quotes: true,
transactions: {
include: {
currency: true,
network: {
include: { currencies: false }
}
}
}
}
})
return {
...swap
}
} catch (error: any) {
console.log(error)
//catchPrismaKnowError(error)
throw new Error(`Error getting swap: ${error?.message}`)
}
}
/**
*
* @param address
* @param isDeleted
* @param isMainnet
* @returns
*/
export async function handlerGetSwaps(isDeleted?: boolean, isMainnet?: boolean, isTeleport?: boolean, page?: number, pageSize: number = 20) {
try {
if (page) {
const swaps = await prisma.swap.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: {
created_date: "desc"
},
where: {
use_teleporter: isTeleport,
source_network: {
is_testnet: !isMainnet, // Add the condition for source_network's istestnet field
}
},
include: {
source_network: true,
source_asset: true,
destination_network: true,
destination_asset: true,
deposit_actions: true,
quotes: true,
transactions: {
include: {
currency: true,
network: {
include: { currencies: false }
}
}
}
}
})
return swaps.map((s: any) => ({
...s,
source_network: s?.source_network?.internal_name,
source_asset: s?.source_asset?.asset,
destination_network: s?.destination_network?.internal_name,
destination_asset: s?.destination_asset?.asset
}))
} else {
const swaps = await prisma.swap.findMany({
orderBy: {
created_date: "desc"
},
where: {
use_teleporter: isTeleport,
source_network: {
is_testnet: !isMainnet, // Add the condition for source_network's istestnet field
}
},
include: {
source_network: true,
source_asset: true,
destination_network: true,
destination_asset: true,
deposit_actions: true,
quotes: true,
transactions: {
include: {
currency: true,
network: {
include: { currencies: false }
}
}
}
}
})
return swaps.map((s: any) => ({
...s,
source_network: s?.source_network?.internal_name,
source_asset: s?.source_asset?.asset,
destination_network: s?.destination_network?.internal_name,
destination_asset: s?.destination_asset?.asset
}))
}
} catch (error: any) {
//catchPrismaKnowError(error)
throw new Error(`Error getting swap: ${error?.message}`)
}
}
/**
*
* @param address
* @returns
*/
export async function handlerGetSwapsByAddress(address: string) {
try {
const swaps = await prisma.swap.findMany({
orderBy: {
created_date: "desc"
},
where: {
source_address: address
},
include: {
source_network: true,
source_asset: true,
destination_network: true,
destination_asset: true,
deposit_actions: true,
quotes: true,
transactions: {
include: {
currency: true,
network: {
include: { currencies: false }
}
}
}
}
})
return swaps.map((s: any) => ({
...s,
source_network: s?.source_network?.internal_name,
source_asset: s?.source_asset?.asset,
destination_network: s?.destination_network?.internal_name,
destination_asset: s?.destination_asset?.asset
}))
} catch (error: any) {
//catchPrismaKnowError(error)
throw new Error(`Error getting swap: ${error?.message}`)
}
}
/**
*
* @param address
* @returns
*/
export async function handlerGetHasBySwaps(address: string) {
try {
const isadd = isValidAddress(address)
if (isadd) {
return await handlerGetSwapsByAddress(address)
} else {
console.time()
const transaction = await prisma.transaction.findFirstOrThrow({
where: { transaction_hash: address },
select: {
swap: {
include: {
transactions: {
include: {
currency: true,
network: {
include: {
currencies: true
}
}
}
}
}
}
}
})
return [{ ...transaction.swap }]
}
} catch (error: any) {
//catchPrismaKnowError(error)
throw new Error(`Error getting swap: ${error?.message}`)
}
}
export async function handlerGetExplorer(status: string[]) {
console.time()
const statuses = status.map((numberStr: string) => swapStatusByIndex[Number(numberStr)])
console.log("🚀 ~ handlerGetExplorer ~ statuses:", statuses)
try {
const swaps = await prisma.swap.findMany({
orderBy: {
created_date: "desc"
},
where: {
status: {
in: statuses
}
},
include: {
source_network: true,
source_asset: true,
destination_network: true,
destination_asset: true,
deposit_actions: true,
quotes: true,
transactions: {
include: {
currency: true,
network: {
include: { currencies: true }
}
}
}
}
})
console.timeEnd()
return swaps.map((s: any) => ({
...s,
source_network: s?.source_network?.internal_name,
source_asset: s?.source_asset?.asset,
destination_network: s?.destination_network?.internal_name,
destination_asset: s?.destination_asset?.asset
}))
} catch (error: any) {
//catchPrismaKnowError(error)
throw new Error(`Error getting swap: ${error?.message}`)
}
}
export async function handlerDelSwap(swapData: { id: string }) {
try {
await prisma.swap.update({
where: { id: swapData.id },
data: { ...swapData }
})
return "success"
} catch (error: any) {
//catchPrismaKnowError(error)
throw new Error(`Error deleting swaps: ${error?.message}`)
}
}
// function catchPrismaKnowError(error: Error) {
// if (error instanceof Prisma.PrismaClientKnownRequestError) {
// throw new Error(`Error getting Prisma code: ${error.name} msg:${error?.message}`)
// }
// }