mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Generated
+7
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user