feat: 1% bridge fee on exits from Lux/Zoo, fix build errors

- 1% service fee applied when source is Lux or Zoo (exits + inter-chain)
- 0% fee when bridging IN from external chains (BTC, ETH, SOL, etc.)
- Fee deducted from payout amount, collected to BRIDGE_FEE_COLLECTOR address
- Quote and rate APIs reflect directional fee
- Fix: add @noble/ed25519 dependency for MPC signing
- Fix: string|null type compatibility in checkDepositAction
This commit is contained in:
2026-03-05 23:53:57 -08:00
parent 04e8ce82e7
commit b86d48a050
5 changed files with 103 additions and 57 deletions
+2 -1
View File
@@ -56,9 +56,10 @@
"typescript": "^5.5.4"
},
"dependencies": {
"@prisma/client": "^6.3.1",
"@luxfi/core": "^10.0.4",
"@luxfi/utila": "^3.0.0",
"@noble/ed25519": "^2.3.0",
"@prisma/client": "^6.3.1",
"@solana/addresses": "2.0.0",
"axios": "^1.7.4",
"consul": "^2.0.1",
+33 -16
View File
@@ -1,33 +1,50 @@
import { getTokenPrice } from "@/domain/tokens"
// 0% fee bridging IN from external chains
// 1% fee on every transfer FROM Lux/Zoo (to each other, or out to external)
const BRIDGE_FEE_RATE = 0.01
const LUX_ZOO_NETWORKS = ['LUX_MAINNET', 'LUX_TESTNET', 'ZOO_MAINNET', 'ZOO_TESTNET']
function isExitFromLux(fromNetwork: string, _toNetwork: string): boolean {
return LUX_ZOO_NETWORKS.includes(fromNetwork)
}
export const getQuote = async (
fromNetwork: string,
fromAsset: string,
toNetwork: string,
toAsset: string,
amount: number,
refuel: number,
useDepositAddress: string
fromNetwork: string,
fromAsset: string,
toNetwork: string,
toAsset: string,
amount: number,
refuel: number,
useDepositAddress: string
) => {
const [sourcePrice, destinationPrice] = await Promise.all([
getTokenPrice(fromAsset),
getTokenPrice(fromAsset),
getTokenPrice(toAsset)
])
const rawReceiveAmount = (amount * sourcePrice) / destinationPrice
const feeRate = isExitFromLux(fromNetwork, toNetwork) ? BRIDGE_FEE_RATE : 0
const serviceFee = rawReceiveAmount * feeRate
const receiveAmount = rawReceiveAmount - serviceFee
const serviceFeeUsd = serviceFee * destinationPrice
return ({
quote: {
receive_amount: (amount * sourcePrice) / destinationPrice,
min_receive_amount: 0.975,
blockchain_fee: 0.145766, // we need to estimate blockchain fee.
service_fee: 0.01, // our bridge service fee is 1%. or We should make no fee for our bridge currently.
avg_completion_time: "00:00:47.4595530", // it should be about 20 min for btc, and 3 ~ 4 mins for evm chains.
receive_amount: receiveAmount,
min_receive_amount: receiveAmount * (1 - 0.025),
blockchain_fee: 0,
service_fee: feeRate,
avg_completion_time: "00:03:00",
refuel_in_source: null,
slippage: 0.025,
total_fee: 0.245766,
total_fee_in_usd: 0.245766
total_fee: serviceFee,
total_fee_in_usd: serviceFeeUsd
},
refuel: null,
reward: {}
})
}
}
export { isExitFromLux, BRIDGE_FEE_RATE }
+21 -14
View File
@@ -1,31 +1,38 @@
import { getTokenPrice } from "@/domain/tokens"
import { isExitFromLux, BRIDGE_FEE_RATE } from "./quote"
export const getRate = async (
fromNetwork: string,
fromAsset: string,
toNetwork: string,
toAsset: string,
amount: number,
version: 'mainnet' | 'testnet',
fromNetwork: string,
fromAsset: string,
toNetwork: string,
toAsset: string,
amount: number,
version: 'mainnet' | 'testnet',
) => {
const [sourcePrice, destinationPrice] = await Promise.all([
getTokenPrice(fromAsset),
getTokenPrice(fromAsset),
getTokenPrice(toAsset)
])
const rawReceiveAmount = amount * sourcePrice / destinationPrice
const feeRate = isExitFromLux(fromNetwork, toNetwork) ? BRIDGE_FEE_RATE : 0
const feeAmount = rawReceiveAmount * feeRate
const receiveAmount = rawReceiveAmount - feeAmount
const feeUsd = feeAmount * destinationPrice
return {
wallet_fee_in_usd: 1,
wallet_fee: 0.01,
wallet_receive_amount: amount,
manual_fee_in_usd: 0,
manual_fee: 0,
manual_receive_amount: amount * sourcePrice / destinationPrice,
wallet_fee_in_usd: feeUsd,
wallet_fee: feeRate,
wallet_receive_amount: receiveAmount,
manual_fee_in_usd: feeUsd,
manual_fee: feeRate,
manual_receive_amount: receiveAmount,
avg_completion_time: {
total_minutes: 2,
total_seconds: 0,
total_hours: 0,
},
fee_usd_price: 10,
fee_usd_price: feeUsd,
}
}
+40 -14
View File
@@ -7,6 +7,7 @@ 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"
export interface SwapData {
@@ -110,22 +111,26 @@ export async function handleSwapCreation(data: SwapData) {
}
})
// estimate swap rate
// estimate swap rate — 1% fee only on exits from Lux/Zoo
const [sourcePrice, destinationPrice] = await Promise.all([getTokenPrice(source_asset), getTokenPrice(destination_asset)])
const receive_amount = Number(amount) * sourcePrice / destinationPrice
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: 0,
min_receive_amount: isNaN(receive_amount) ? 0 : receive_amount * (1 - 0.025),
blockchain_fee: 0,
service_fee: 0,
service_fee: feeRate,
avg_completion_time: "00:03:00",
slippage: 0.025,
total_fee: 0,
total_fee_in_usd: 0
total_fee: isNaN(service_fee_amount) ? 0 : service_fee_amount,
total_fee_in_usd: isNaN(service_fee_usd) ? 0 : service_fee_usd
}
})
@@ -384,7 +389,7 @@ export async function checkDepositAction({
asset: string,
wallet: string,
requestedAmount: number,
networkInternalName?: string,
networkInternalName?: string | null,
}) {
try {
// Extract address from wallet###address format
@@ -603,7 +608,8 @@ export async function handlerUtilaPayoutAction(swapId: string) {
destination_network: {
include: { currencies: false }
},
destination_asset: true
destination_asset: true,
quotes: true
}
})
if (!swap) {
@@ -627,9 +633,15 @@ export async function handlerUtilaPayoutAction(swapId: string) {
})
if (confirmed) {
// todo: mint L or Z tokens here
// Replace with your private key (store it securely, not hardcoded in production)
const privateKey = process.env.LUX_SIGNER
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
const rpcs: Record<string, string> = {
@@ -642,7 +654,9 @@ export async function handlerUtilaPayoutAction(swapId: string) {
console.log(">> payout details", {
rpc,
token: swap.destination_asset.contract_address,
amount: swap.requested_amount
payoutAmount,
feeAmount,
feeCollector
})
if (!rpc) {
throw new Error(`No RPC found for chain ${swap?.destination_network?.internal_name}`)
@@ -651,24 +665,36 @@ export async function handlerUtilaPayoutAction(swapId: string) {
const feeData = await provider.getFeeData()
// signer
const wallet = new Wallet(privateKey!, provider)
// mint payout token
try {
console.log(`>> Minting PayoutToken for this Swap [${swap.id}]`)
const contract = new Contract(swap.destination_asset.contract_address as string, ERC20B_ABI, wallet)
const decimals = swap?.destination_asset.decimals ?? 18
const txMint = await contract.bridgeMint(swap.destination_address, parseUnits(swap.requested_amount.toFixed(decimals), decimals))
// Mint net amount to user (after 1% fee)
const txMint = await contract.bridgeMint(swap.destination_address, parseUnits(payoutAmount.toFixed(decimals), decimals))
await txMint.wait()
console.log(`>> Minted L/Z tokens ${txMint.hash}`)
// Mint fee to fee collector
if (feeAmount > 0 && feeCollector !== '0x0000000000000000000000000000000000000000') {
try {
const txFee = await contract.bridgeMint(feeCollector, parseUnits(feeAmount.toFixed(decimals), decimals))
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: wallet.address,
to: swap.destination_address,
amount: swap.requested_amount,
amount: payoutAmount,
transaction_hash: txMint.hash,
confirmations: 2,
max_confirmations: 2,
+7 -12
View File
@@ -40,6 +40,9 @@ catalogs:
specifier: 5.7.2
version: 5.7.2
overrides:
keccak: 3.0.4
importers:
.:
@@ -549,6 +552,9 @@ importers:
'@luxfi/utila':
specifier: ^3.0.0
version: 3.0.0(@babel/[email protected])(@babel/[email protected])(@swc/[email protected](@swc/[email protected]))(@types/[email protected])(@types/[email protected])([email protected])([email protected](@swc/[email protected](@swc/[email protected])))([email protected](@swc/[email protected](@swc/[email protected]))(@types/[email protected])([email protected]))([email protected])([email protected])([email protected])
'@noble/ed25519':
specifier: ^2.3.0
version: 2.3.0
'@prisma/client':
specifier: ^6.3.1
version: 6.3.1([email protected]([email protected]))([email protected])
@@ -9919,10 +9925,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
[email protected]:
resolution: {integrity: sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==}
engines: {node: '>=4.0.0'}
[email protected]:
resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==}
engines: {node: '>=10.0.0'}
@@ -25766,7 +25768,7 @@ snapshots:
bn.js: 4.12.1
create-hash: 1.2.0
ethjs-util: 0.1.6
keccak: 1.4.0
keccak: 3.0.4
rlp: 2.2.7
safe-buffer: 5.2.1
secp256k1: 3.8.1
@@ -27714,13 +27716,6 @@ snapshots:
jwa: 1.4.1
safe-buffer: 5.2.1
[email protected]:
dependencies:
bindings: 1.5.0
inherits: 2.0.4
nan: 2.22.0
safe-buffer: 5.2.1
[email protected]:
dependencies:
node-addon-api: 2.0.2