// Uncredit the user's account for the given payment. // If the deposit isn't credited, do nothing. // Returns true if the resulting balance is negative. // Must be idempotent. func UncreditDepositForPayment(payment *bitcoin.Payment) (balance *Balance) { // SANITY CHECK paymentId := payment.Id // Reload the payment. payment = bitcoin.LoadPaymentByTxId(payment.TxId, payment.Vout) // Ensure that it is orphaned. if payment.Orphaned != bitcoin.PAYMENT_ORPHANED_STATUS_ORPHANED { panic(NewError("Cannot uncredit deposit for a payment that isn't in STATUS_ORPHANED")) } // Are you paranoid enough? if paymentId != payment.Id { panic(NewError("payment.Id didn't match")) } // END SANITY CHECK err := db.DoBeginSerializable(func(tx *db.ModelTx) { // Load the corresponding deposit. deposit := LoadDepositForPayment(tx, payment.Id) // If the deposit isn't credited, do nothing. if deposit.Status != DEPOSIT_STATUS_CREDITED { return } // Uncredit the account. balance = UpdateBalanceByWallet(tx, deposit.UserId, deposit.Wallet, deposit.Coin, -int64(deposit.Amount), false) UpdateDepositSetStatus(tx, deposit.Id, DEPOSIT_STATUS_PENDING) }) if err != nil { panic(err) } return }
// Cancels an existing order. // Returns the most up-to-date version of order. func (market *Market) ProcessOrderCancellation(order *Order) *Order { // reload the order, it might have been touched since. order = LoadOrder(order.Id) switch order.Status { case ORDER_STATUS_COMPLETE: return order case ORDER_STATUS_CANCELED: return order case ORDER_STATUS_PENDING: break default: panic(NewError("Unrecognized order status %v", order.Status)) } // remove it from mempool dropped := market.DropOrderFromMempool(order) if dropped != nil { market.LoadMore(order.Type, order.Id) } err := db.DoBeginSerializable(func(tx *db.ModelTx) { // save the status as canceled order.Status = ORDER_STATUS_CANCELED UpdateOrder(tx, order) // return the reserved funds ReleaseReservedFundsForOrder(tx, order) }) if err != nil { panic(err) } return order }
// plog: The basis PriceLog entry to save // nextTime: The next PriceLog entry will have this time. // * This is how we know whether to finalize parent blogs. func (logger *PriceLogger) addPriceLog(plog *PriceLog, nextTime int64) { if plog.Interval != BasisInterval { panic("addPriceLog() expects smallest interval") } if plog.Time%plog.Interval != 0 { panic("plog.Time % Interval should be zero") } if plog.Market != logger.Market { panic("plog.Market wasn't logger.market") } logger.entries = append(logger.entries, plog) toSave := []*PriceLog{plog} for i, interval := range Intervals { if i == 0 { continue } // If this interval window has closed... //if (plog.Time / interval) < (nextTime / interval) { toSave = append(toSave, logger.computeForInterval(plog.Time, interval)) //} } // TODO: this doesn't have to be serializable err := db.DoBeginSerializable(func(tx *db.ModelTx) { for _, plog := range toSave { SaveOrUpdatePriceLog(tx, plog) } }) if err != nil { panic(err) } }
func DepositMoneyForUser(user *auth.User, coin string, amount uint64) { err := db.DoBeginSerializable(func(tx *db.ModelTx) { account.UpdateBalanceByWallet(tx, user.Id, account.WALLET_MAIN, coin, int64(amount), false) }) if err != nil { panic(err) } }
func MarkPaymentsAsSpent(paymentIds []interface{}, wtxId int64) { err := db.DoBeginSerializable(func(tx *db.ModelTx) { UpdatePaymentsSpent(tx, paymentIds, PAYMENT_SPENT_STATUS_CHECKEDOUT, PAYMENT_SPENT_STATUS_SPENT, wtxId) }) if err != nil { panic(err) } }
func CheckoutPaymentsToSpend(paymentIds []interface{}, wtxId int64) { err := db.DoBeginSerializable(func(tx *db.ModelTx) { UpdatePaymentsSpent(tx, paymentIds, PAYMENT_SPENT_STATUS_AVAILABLE, PAYMENT_SPENT_STATUS_CHECKEDOUT, wtxId) }) if err != nil { panic(err) } }
func ResumeWithdrawals(wthIds []interface{}) { err := db.DoBeginSerializable(func(tx *db.ModelTx) { // update status UpdateWithdrawals(tx, wthIds, WITHDRAWAL_STATUS_STALLED, WITHDRAWAL_STATUS_PENDING, 0) }) if err != nil { panic(err) } }
func CancelWithdrawal(wth *Withdrawal) { err := db.DoBeginSerializable(func(tx *db.ModelTx) { // update status UpdateWithdrawals(tx, []interface{}{wth.Id}, WITHDRAWAL_STATUS_PENDING, WITHDRAWAL_STATUS_CANCELED, 0) // adjust balance UpdateBalanceByWallet(tx, wth.UserId, WALLET_RESERVED_WITHDRAWAL, wth.Coin, -int64(wth.Amount), true) UpdateBalanceByWallet(tx, wth.UserId, WALLET_MAIN, wth.Coin, int64(wth.Amount), false) }) if err != nil { panic(err) } }
func CheckoutWithdrawals(coin string, limit uint) (wths []*Withdrawal) { err := db.DoBeginSerializable(func(tx *db.ModelTx) { wths = LoadWithdrawalsByStatus(tx, coin, WITHDRAWAL_STATUS_PENDING, limit) wthIds := Map(wths, "Id") UpdateWithdrawals(tx, wthIds, WITHDRAWAL_STATUS_PENDING, WITHDRAWAL_STATUS_CHECKEDOUT, 0) }) if err != nil { panic(err) } return }
func injectMoneyForUsers(users []*auth.User, coins []string) { Info("Injecting money for each user") for _, user := range users { for _, coin := range coins { err := db.DoBeginSerializable(func(tx *db.ModelTx) { account.UpdateBalanceByWallet(tx, user.Id, account.WALLET_MAIN, coin, int64(100000000000000), false) }) if err != nil { panic(err) } } } Info("Done injecting money for each user") }
func CompleteWithdrawals(wths []*Withdrawal, wtxId int64) { wthIds := Map(wths, "Id") err := db.DoBeginSerializable(func(tx *db.ModelTx) { // update status UpdateWithdrawals(tx, wthIds, WITHDRAWAL_STATUS_CHECKEDOUT, WITHDRAWAL_STATUS_COMPLETE, wtxId) // adjust balance for _, wth := range wths { UpdateBalanceByWallet(tx, wth.UserId, WALLET_RESERVED_WITHDRAWAL, wth.Coin, -int64(wth.Amount), true) } }) if err != nil { panic(err) } }
// Funds are reserved by moving them to the account.WALLET_RESERVED_ORDER wallet when // the order is saved to the DB. // If there aren't enough funds, the order isn't saved, and an error is returned. // The returned error.Error() is a front-end message. func SaveAndReserveFundsForOrder(order *Order) { err := db.DoBeginSerializable(func(tx *db.ModelTx) { // Save the order, get the id SaveOrder(tx, order) // Reserve the funds if order.Type == ORDER_TYPE_BID { account.UpdateBalanceByWallet(tx, order.UserId, account.WALLET_MAIN, order.BasisCoin, -int64(order.BasisAmount+order.BasisFee), true) account.UpdateBalanceByWallet(tx, order.UserId, account.WALLET_RESERVED_ORDER, order.BasisCoin, int64(order.BasisAmount+order.BasisFee), false) } else { account.UpdateBalanceByWallet(tx, order.UserId, account.WALLET_MAIN, order.Coin, -int64(order.Amount), true) account.UpdateBalanceByWallet(tx, order.UserId, account.WALLET_RESERVED_ORDER, order.Coin, int64(order.Amount), false) } }) if err != nil { panic(err) } }
func AddWithdrawal(userId int64, toAddr string, coin string, amount uint64) (*Withdrawal, error) { wth := &Withdrawal{ UserId: userId, Wallet: WALLET_MAIN, Coin: coin, ToAddress: toAddr, Amount: amount, Status: WITHDRAWAL_STATUS_PENDING, } err := db.DoBeginSerializable(func(tx *db.ModelTx) { // save withdrawal SaveWithdrawal(tx, wth) // adjust balance. UpdateBalanceByWallet(tx, userId, WALLET_MAIN, coin, -int64(amount), true) UpdateBalanceByWallet(tx, userId, WALLET_RESERVED_WITHDRAWAL, coin, int64(amount), false) }) return wth, err }
func AddTransfer(fromUserId int64, fromWallet string, toUserId int64, toWallet string, coin string, amount uint64) error { // Create new transfer item that moves amount. trans := &Transfer{ UserId: fromUserId, Wallet: fromWallet, User2Id: toUserId, Wallet2: toWallet, Coin: coin, Amount: amount, Fee: uint64(0), } err := db.DoBeginSerializable(func(tx *db.ModelTx) { // Adjust balance UpdateBalanceByWallet(tx, trans.UserId, trans.Wallet, trans.Coin, -int64(trans.Amount), true) UpdateBalanceByWallet(tx, trans.User2Id, trans.Wallet2, trans.Coin, int64(trans.Amount), false) // Save transfer SaveTransfer(tx, trans) }) return err }
// Credit the user's account for the given bank deposit. // If the deposit is already credited, do nothing. // Must be idempotent. func CreditDeposit(deposit *Deposit) { // SANITY CHECK if deposit.Id == 0 { panic(NewError("Expected a saved deposit but got a new one")) } // END SANITY CHECK err := db.DoBeginSerializable(func(tx *db.ModelTx) { // Load the corresponding deposit. deposit := LoadDeposit(tx, deposit.Id) // If the deposit isn't pending, do nothing. if deposit.Status != DEPOSIT_STATUS_PENDING { return } // Credit the account. UpdateBalanceByWallet(tx, deposit.UserId, deposit.Wallet, deposit.Coin, int64(deposit.Amount), false) UpdateDepositSetStatus(tx, deposit.Id, DEPOSIT_STATUS_CREDITED) }) if err != nil { panic(err) } }
// Executes a new order, which may or may not be a market order. // The order should have already been saved. // If the order is executable, it gets executed as well. // If the order is not executable, or after execution it is // not complete and no longer executable, it gets added // into the mempool. func (market *Market) ProcessOrderExecution(order *Order) { if order.Id == 0 { panic("Order hasn't been saved yet") } if order.Complete() { panic("New order is already complete.") } // Until order is complete, or there are no more matches... for { match := market.NextMatch(order) if match != nil { if match.Complete() { panic(NewError("Match %v is already complete.", match.Id)) } // Figure out which is bid & ask. var bid, ask *Order if order.Type == ORDER_TYPE_BID { bid, ask = order, match } else { bid, ask = match, order } // Figure out how much to trade. tradeAmount, tradeBasis, bidBasisFee, askBasisFee := order.ComputeTradeAndFees(match) // Info("TradeCoin %v tradeBasis %v", tradeAmount, tradeBasis) // Update filled & sanity check before updating the DB. bid.Filled += tradeAmount bid.BasisFilled += tradeBasis bid.BasisFeeFilled += bidBasisFee ask.Filled += tradeAmount ask.BasisFilled += tradeBasis ask.BasisFeeFilled += askBasisFee // Update order & match accordingly. if order.Complete() { order.Status = ORDER_STATUS_COMPLETE } if match.Complete() { match.Status = ORDER_STATUS_COMPLETE } // Sanity check bid.Validate() ask.Validate() if askBasisFee > tradeBasis { panic("askBasisFee exceeded tradeBasis ?!") } if !ask.Complete() && !bid.Complete() { panic("Neither ask nor bid was fulfilled after trade.") } // Make trade trade := &Trade{ BidUserId: bid.UserId, BidOrderId: bid.Id, BidBasisFee: bidBasisFee, AskUserId: ask.UserId, AskOrderId: ask.Id, AskBasisFee: askBasisFee, Coin: order.Coin, BasisCoin: order.BasisCoin, TradeAmount: tradeAmount, TradeBasis: tradeBasis, Price: match.Price, } // Perform transaction. // -> update order & match filled & status. // -> perform trade of coins between both users. // -> return unfilled reserved coins back to the account.WALLET_MAIN wallet. err := db.DoBeginSerializable(func(tx *db.ModelTx) { UpdateOrder(tx, match) UpdateOrder(tx, order) // Save trade info. SaveTrade(tx, trade) // Release remaining reserved funds if bid.Complete() { ReleaseReservedFundsForOrder(tx, bid) } if ask.Complete() { ReleaseReservedFundsForOrder(tx, ask) } // Trade funds & check parity in reserved wallets _, err := tx.Exec(`SELECT exchange_do_trade(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, bid.Id, bid.UserId, bidBasisFee, ask.Id, ask.UserId, askBasisFee, order.BasisCoin, tradeBasis, order.Coin, tradeAmount, ) if err != nil { panic(err) } }) if err != nil { panic(err) } // Add trade to price log. market.PriceLogger.AddTrade(order.Type, tradeAmount, match.Price, trade.Time) // Remove match from mempool if complete. if match.Complete() { market.DropOrderFromMempool(match) market.LoadMore(match.Type, order.Id) } // Return if we're done with this order. if order.Complete() { return } } else { // There are no more matches for this order, // or the order isn't immediatly executable. // The order was already saved to DB, but // we need to insert it into market.Bids/Asks if in range. market.InsertIfInRange(order) return } } }
// Returns false if no withdrawals are available to process. func ProcessUserWithdrawals(coin string) (bool, error) { // Checkout withdrawals // TODO: Gather multiple small withdrawals. wths := account.CheckoutWithdrawals(coin, 1) if len(wths) == 0 { return false, nil } wthIds := Map(wths, "Id") amounts := map[string]uint64{} amountSum := uint64(0) for _, wth := range wths { if wth.Amount <= 0 { panic(NewError("Invalid send amount %v", wth.Amount)) } amounts[wth.ToAddress] += uint64(wth.Amount) amountSum += uint64(wth.Amount) } // figure out which payments to use. signedTx, payments, minerFees, chgAddress, err := ComputeWithdrawalTransaction(coin, amounts) if err != nil { account.StallWithdrawals(wthIds) return false, err } paymentIds := Map(payments, "Id") // save withdrawal info for bookkeeping. wthTx := SaveWithdrawalTx(&WithdrawalTx{ Coin: coin, Type: WITHDRAWAL_TX_TYPE_WITHDRAWAL, Amount: amountSum, MinerFee: minerFees, ChgAddress: chgAddress, RawTx: signedTx, TxId: bitcoin.ComputeTxId(signedTx), }) // checkout those payments. bitcoin.CheckoutPaymentsToSpend(paymentIds, wthTx.Id) // TODO: the Tx should go out to our partners who sign them for us. // TODO: receive the signed Tx. // deduct change amount from system user's "change" wallet. // this creates a negative balance, which will revert to zero // when the change is received. if chgAddress != "" { changeAmount := amounts[chgAddress] err := db.DoBeginSerializable(func(tx *db.ModelTx) { account.UpdateBalanceByWallet(tx, 0, account.WALLET_CHANGE, coin, -int64(changeAmount), false) }) if err != nil { panic(err) } } // broadcast transaction. rpc.SendRawTransaction(coin, signedTx) // update payments as spent. bitcoin.MarkPaymentsAsSpent(paymentIds, wthTx.Id) // update withdrawals as complete. account.CompleteWithdrawals(wths, wthTx.Id) return true, nil }
// Create a new user. func SaveUser(user *User) (*User, error) { // Create email confirmation code. if user.EmailCode == "" { user.EmailCode = RandId(24) } // Create TOTPKey. if len(user.TOTPKey) == 0 { user.TOTPKey = RandBytes(10) } // Scrypt the password. if user.Password != "" { salt := RandId(12) scryptPass, err := scrypt.Key([]byte(user.Password), []byte(salt), 16384, 8, 1, 32) if err != nil { return nil, err } user.Salt = []byte(salt) user.Scrypt = scryptPass } err := db.DoBeginSerializable(func(tx *db.ModelTx) { // Insert into users table. err := tx.QueryRow( `INSERT INTO auth_user (`+UserModel.FieldsInsert+`) VALUES (`+UserModel.Placeholders+`) RETURNING id`, user, ).Scan(&user.Id) if err != nil { panic(err) } // Set the chain_idx if user.Id > math.MaxInt32 { panic("User autoinc id has exceeded MaxInt32") } user.ChainIdx = int32(user.Id) _, err = tx.Exec( `UPDATE auth_user SET chain_idx = id WHERE id=?`, user.Id, ) if err != nil { panic(err) } // Generate an API key for the user apiKey := &APIKey{Key: RandId(24), UserId: user.Id} SaveAPIKey(tx, apiKey) }) switch db.GetErrorType(err) { case db.ERR_DUPLICATE_ENTRY: return nil, ERR_DUPLICATE_ADDRESS case nil: break default: panic(err) } return user, nil }