2025-05-31 16:26:51 +03:00
import { auth } from '$lib/auth' ;
import { error , json } from '@sveltejs/kit' ;
import { db } from '$lib/server/db' ;
import { user , userPortfolio , coin , transaction } from '$lib/server/db/schema' ;
import { eq , and } from 'drizzle-orm' ;
2025-06-11 18:37:03 +03:00
import { createNotification } from '$lib/server/notification' ;
import { formatValue } from '$lib/utils' ;
2025-06-24 19:37:34 +03:00
import { verifyTurnstile } from '$lib/server/turnstile' ;
import { setTurnstileVerifiedRedis , isTurnstileVerifiedRedis } from '$lib/server/redis' ;
2025-05-31 16:26:51 +03:00
import type { RequestHandler } from './$types' ;
interface TransferRequest {
recipientUsername : string ;
type : 'CASH' | 'COIN' ;
amount : number ;
coinSymbol? : string ;
}
export const POST : RequestHandler = async ( { request } ) = > {
const session = await auth . api . getSession ( {
headers : request.headers
} ) ;
if ( ! session ? . user ) {
throw error ( 401 , 'Not authenticated' ) ;
2025-06-11 18:37:03 +03:00
} try {
2025-06-24 19:37:34 +03:00
const { recipientUsername , type , amount , coinSymbol , turnstileToken } : TransferRequest & { turnstileToken? : string } = await request . json ( ) ;
const alreadyVerified = await isTurnstileVerifiedRedis ( session . user . id ) ;
if ( ! alreadyVerified ) {
if ( ! turnstileToken || ! ( await verifyTurnstile ( turnstileToken , request ) ) ) {
throw error ( 400 , 'Captcha verification failed' ) ;
}
await setTurnstileVerifiedRedis ( session . user . id ) ;
}
2025-05-31 16:26:51 +03:00
if ( ! recipientUsername || ! type || ! amount || typeof amount !== 'number' || ! Number . isFinite ( amount ) || amount <= 0 ) {
throw error ( 400 , 'Invalid transfer parameters' ) ;
}
if ( amount > Number . MAX_SAFE_INTEGER ) {
throw error ( 400 , 'Transfer amount too large' ) ;
}
2025-06-02 12:23:34 +03:00
if ( type === 'CASH' && amount < 10 ) {
throw error ( 400 , 'Cash transfers require a minimum of $10.00' ) ;
}
2025-05-31 16:26:51 +03:00
if ( type === 'COIN' && ! coinSymbol ) {
throw error ( 400 , 'Coin symbol required for coin transfers' ) ;
}
const senderId = Number ( session . user . id ) ;
return await db . transaction ( async ( tx ) = > {
const [ senderData ] = await tx
. select ( {
id : user.id ,
username : user.username ,
baseCurrencyBalance : user.baseCurrencyBalance
} )
. from ( user )
. where ( eq ( user . id , senderId ) )
. for ( 'update' )
. limit ( 1 ) ;
if ( ! senderData ) {
throw error ( 404 , 'Sender not found' ) ;
}
const [ recipientData ] = await tx
. select ( {
id : user.id ,
username : user.username ,
baseCurrencyBalance : user.baseCurrencyBalance
} )
. from ( user )
. where ( eq ( user . username , recipientUsername ) )
. for ( 'update' )
. limit ( 1 ) ;
if ( ! recipientData ) {
throw error ( 404 , 'Recipient not found' ) ;
}
if ( senderData . id === recipientData . id ) {
throw error ( 400 , 'Cannot transfer to yourself' ) ;
}
if ( type === 'CASH' ) {
const senderBalance = Number ( senderData . baseCurrencyBalance ) ;
if ( senderBalance < amount ) {
throw error ( 400 , ` Insufficient funds. You have $ ${ senderBalance . toFixed ( 2 ) } but trying to send $ ${ amount . toFixed ( 2 ) } ` ) ;
}
const recipientBalance = Number ( recipientData . baseCurrencyBalance ) ;
await tx
. update ( user )
. set ( {
baseCurrencyBalance : ( senderBalance - amount ) . toFixed ( 8 ) ,
updatedAt : new Date ( )
} )
. where ( eq ( user . id , senderId ) ) ;
await tx
. update ( user )
. set ( {
baseCurrencyBalance : ( recipientBalance + amount ) . toFixed ( 8 ) ,
updatedAt : new Date ( )
} )
. where ( eq ( user . id , recipientData . id ) ) ;
await tx . insert ( transaction ) . values ( {
userId : senderId ,
coinId : 1 ,
type : 'TRANSFER_OUT' ,
quantity : '0' ,
pricePerCoin : '1' ,
totalBaseCurrencyAmount : amount.toString ( ) ,
timestamp : new Date ( ) ,
senderUserId : senderId ,
recipientUserId : recipientData.id
} ) ;
await tx . insert ( transaction ) . values ( {
userId : recipientData.id ,
coinId : 1 ,
type : 'TRANSFER_IN' ,
quantity : '0' ,
pricePerCoin : '1' ,
totalBaseCurrencyAmount : amount.toString ( ) ,
timestamp : new Date ( ) ,
senderUserId : senderId ,
recipientUserId : recipientData.id
} ) ;
2025-06-11 18:37:03 +03:00
( async ( ) = > {
await createNotification (
recipientData . id . toString ( ) ,
'TRANSFER' ,
'Money received!' ,
` You received ${ formatValue ( amount ) } from @ ${ senderData . username } ` ,
) ;
} ) ( ) ;
2025-05-31 16:26:51 +03:00
return json ( {
success : true ,
type : 'CASH' ,
amount ,
recipient : recipientData.username ,
newBalance : senderBalance - amount
} ) ;
} else {
const normalizedSymbol = coinSymbol ! . toUpperCase ( ) ;
const [ coinData ] = await tx
. select ( { id : coin.id , symbol : coin . symbol , name : coin.name , currentPrice : coin.currentPrice } )
. from ( coin )
. where ( eq ( coin . symbol , normalizedSymbol ) )
. limit ( 1 ) ;
if ( ! coinData ) {
throw error ( 404 , 'Coin not found' ) ;
}
2025-06-02 12:23:34 +03:00
const coinPrice = Number ( coinData . currentPrice ) || 0 ;
const estimatedValue = amount * coinPrice ;
if ( estimatedValue < 10 ) {
throw error ( 400 , ` Coin transfers require a minimum estimated value of $ 10.00. ${ amount . toFixed ( 6 ) } ${ coinData . symbol } is worth approximately $ ${ estimatedValue . toFixed ( 2 ) } ` ) ;
}
2025-05-31 16:26:51 +03:00
const [ senderHolding ] = await tx
. select ( {
quantity : userPortfolio.quantity
} )
. from ( userPortfolio )
. where ( and (
eq ( userPortfolio . userId , senderId ) ,
eq ( userPortfolio . coinId , coinData . id )
) )
. for ( 'update' )
. limit ( 1 ) ;
if ( ! senderHolding || Number ( senderHolding . quantity ) < amount ) {
const availableAmount = senderHolding ? Number ( senderHolding . quantity ) : 0 ;
throw error ( 400 , ` Insufficient ${ coinData . symbol } . You have ${ availableAmount . toFixed ( 6 ) } but trying to send ${ amount . toFixed ( 6 ) } ` ) ;
}
const [ recipientHolding ] = await tx
. select ( { quantity : userPortfolio.quantity } )
. from ( userPortfolio )
. where ( and (
eq ( userPortfolio . userId , recipientData . id ) ,
eq ( userPortfolio . coinId , coinData . id )
) )
. for ( 'update' )
. limit ( 1 ) ;
const totalValue = amount * coinPrice ;
const newSenderQuantity = Number ( senderHolding . quantity ) - amount ;
if ( newSenderQuantity > 0.000001 ) {
await tx
. update ( userPortfolio )
. set ( {
quantity : newSenderQuantity.toString ( ) ,
updatedAt : new Date ( )
} )
. where ( and (
eq ( userPortfolio . userId , senderId ) ,
eq ( userPortfolio . coinId , coinData . id )
) ) ;
} else {
await tx
. delete ( userPortfolio )
. where ( and (
eq ( userPortfolio . userId , senderId ) ,
eq ( userPortfolio . coinId , coinData . id )
) ) ;
}
if ( recipientHolding ) {
const newRecipientQuantity = Number ( recipientHolding . quantity ) + amount ;
await tx
. update ( userPortfolio )
. set ( {
quantity : newRecipientQuantity.toString ( ) ,
updatedAt : new Date ( )
} )
. where ( and (
eq ( userPortfolio . userId , recipientData . id ) ,
eq ( userPortfolio . coinId , coinData . id )
) ) ;
} else {
await tx
. insert ( userPortfolio )
. values ( {
userId : recipientData.id ,
coinId : coinData.id ,
quantity : amount.toString ( )
} ) ;
}
await tx . insert ( transaction ) . values ( {
userId : senderId ,
coinId : coinData.id ,
type : 'TRANSFER_OUT' ,
quantity : amount.toString ( ) ,
pricePerCoin : coinPrice.toString ( ) ,
totalBaseCurrencyAmount : totalValue.toString ( ) ,
timestamp : new Date ( ) ,
senderUserId : senderId ,
recipientUserId : recipientData.id
} ) ;
await tx . insert ( transaction ) . values ( {
userId : recipientData.id ,
coinId : coinData.id ,
type : 'TRANSFER_IN' ,
quantity : amount.toString ( ) ,
pricePerCoin : coinPrice.toString ( ) ,
totalBaseCurrencyAmount : totalValue.toString ( ) ,
timestamp : new Date ( ) ,
senderUserId : senderId ,
recipientUserId : recipientData.id
} ) ;
2025-06-11 18:37:03 +03:00
( async ( ) = > {
await createNotification (
recipientData . id . toString ( ) ,
'TRANSFER' ,
'Coins received!' ,
` You received ${ amount . toFixed ( 6 ) } * ${ coinData . symbol } from @ ${ senderData . username } ` ,
) ;
} ) ( ) ;
2025-05-31 16:26:51 +03:00
return json ( {
success : true ,
type : 'COIN' ,
amount ,
coinSymbol : coinData.symbol ,
coinName : coinData.name ,
recipient : recipientData.username ,
newQuantity : newSenderQuantity
} ) ;
}
} ) ;
} catch ( e ) {
console . error ( 'Transfer error:' , e ) ;
if ( e && typeof e === 'object' && 'status' in e ) {
throw e ;
}
return json ( { error : 'Transfer failed' } , { status : 500 } ) ;
}
} ;