def accept_proposed_rate(request, shared_data): # this is node_to_confirm's account acct = Account.objects.get(shared_data=shared_data, owner__pk=shared_data.node_to_confirm_id) pmt = None if acct.getBalance( ) and shared_data.interest_rate: # otherwise no update necessary pmt = Payment(payer=acct.owner, payer_email=acct.owner.getPrimaryEmail(), recipient=acct.partner, recipient_email=acct.partner.getPrimaryEmail(), currency=shared_data.currency, amount=0.0, status='PE', description='Interest rate change: %s%% to %s%%.' % \ (shared_data.displayRate(), shared_data.displayProposedRate())) pmt.save() # set pmt time #pmt = Payment.objects.get(pk=pmt.id) # reload because django doesn't update date that is auto-set on save interest = computeInterest(acct.getBalance(), shared_data.interest_rate, shared_data.last_update, pmt.date) path = PaymentPath(payment=pmt, amount=pmt.amount) path.save() link = PaymentLink(path=path, payer_account=acct, position=1, amount=pmt.amount, interest=-interest, interest_rate=shared_data.interest_rate) link.save() shared_data.balance += interest * acct.balance_multiplier if pmt: shared_data.last_update = pmt.date else: shared_data.last_update = datetime.datetime.now() shared_data.interest_rate = shared_data.proposed_rate shared_data.proposed_rate = None shared_data.node_to_confirm_id = None shared_data.save() if pmt: # could do this initially now that this is wrapped in a transaction pmt.status = 'OK' pmt.save() t = template_loader.get_template('emailAcceptRateProposal.txt') c = RequestContext(request, {'acct': acct.get_partner_acct()}) sendEmail("Interest rate proposal accepted", t.render(c), acct.partner.getPrimaryEmail())
def testAccountIntegrity(): for shared in SharedAccountData.objects.all(): #print "\n*** %s ***" % shared acctCurrency = shared.currency fwdAcct = Account.objects.get(shared_data=shared, balance_multiplier=1) bwdAcct = Account.objects.get(shared_data=shared, balance_multiplier=-1) if fwdAcct.getBalanceNoInterest() < -fwdAcct.iou_limit - ERROR: print 'Account %d over limit: Balance %.12f, Limit %.12f.' % \ (fwdAcct.id, fwdAcct.getBalanceNoInterest(), -fwdAcct.iou_limit) if bwdAcct.getBalanceNoInterest() < -bwdAcct.iou_limit - ERROR: print 'Account %d over limit: Balance %.12f, Limit %.12f.' % \ (bwdAcct.id, bwdAcct.getBalanceNoInterest(), -bwdAcct.iou_limit) tally = 0.0 last_date = None for link in PaymentLink.objects.filter(payer_account__id__in=(fwdAcct.id, bwdAcct.id) ).extra(select={'date': 'pmt.date'}, tables=('ripple_payment as pmt', 'ripple_paymentpath as path'), where=('path.payment_id=pmt.id', 'path.id=ripple_paymentlink.path_id') ).order_by('date', 'id'): path = link.path payment = path.payment acct = link.payer_account #print "%d: %.12f (%.12f/%.3f) on %s = %.12f" % \ # (link.id, link.amount, link.interest, link.interest_rate, payment.date, tally) interest = 0.0 if last_date: # skip first transaction on account because balance/interest are zero pathAmount = path.amount if acctCurrency.value: pathAmount *= payment.currency.value / acctCurrency.value interest = computeInterest(tally * acct.balance_multiplier, link.interest_rate, last_date, payment.date) if abs(interest + link.interest) > .5e-12: # should be exact, not sure why sometimes get errors here print "Link %d: incorrect interest.\n\tBalance: %.14f.\n\tCorrect value: %.20f\n\tRecorded %.12f" % \ (link.id, tally, interest, -link.interest) if abs(pathAmount - link.amount) > 1e-12: print "Link %d: incorrect amount. Correct value: %.20f, recorded %.12f" % \ (link.id, pathAmount, link.amount) else: if link.interest != 0.0: print "Link %d: first transaction interest not zero. (%.12f)" % (link.id, link.interest) last_date = payment.date tally -= (link.amount + link.interest) * acct.balance_multiplier if abs(tally - shared.balance) > ERROR: # sometimes this is not quite exact either, maybe due to summing rouding errors? print 'Shared account %d doesn\'t balance with transaction total. Balance: %.12f, Transactions: %.12f.' % \ (shared.id, shared.balance, tally)
def accept_proposed_rate(request, shared_data): # this is node_to_confirm's account acct = Account.objects.get(shared_data=shared_data, owner__pk=shared_data.node_to_confirm_id) pmt = None if acct.getBalance() and shared_data.interest_rate: # otherwise no update necessary pmt = Payment(payer=acct.owner, payer_email=acct.owner.getPrimaryEmail(), recipient=acct.partner, recipient_email=acct.partner.getPrimaryEmail(), currency=shared_data.currency, amount=0.0, status='PE', description='Interest rate change: %s%% to %s%%.' % \ (shared_data.displayRate(), shared_data.displayProposedRate())) pmt.save() # set pmt time #pmt = Payment.objects.get(pk=pmt.id) # reload because django doesn't update date that is auto-set on save interest = computeInterest(acct.getBalance(), shared_data.interest_rate, shared_data.last_update, pmt.date) path = PaymentPath(payment=pmt, amount=pmt.amount) path.save() link = PaymentLink(path=path, payer_account=acct, position=1, amount=pmt.amount, interest=-interest, interest_rate=shared_data.interest_rate) link.save() shared_data.balance += interest * acct.balance_multiplier if pmt: shared_data.last_update = pmt.date else: shared_data.last_update = datetime.datetime.now() shared_data.interest_rate = shared_data.proposed_rate shared_data.proposed_rate = None shared_data.node_to_confirm_id = None shared_data.save() if pmt: # could do this initially now that this is wrapped in a transaction pmt.status = 'OK' pmt.save() t = template_loader.get_template('emailAcceptRateProposal.txt') c = RequestContext(request, {'acct': acct.get_partner_acct()}) sendEmail("Interest rate proposal accepted", t.render(c), acct.partner.getPrimaryEmail())
def payAlongPath(path, amount, cursor, pmt): # use only transaction-safe DB values - means re-get necessary data pathCredit = amount MULTIPLIER, IOU_LIMIT, BALANCE, INTEREST_RATE, LAST_UPDATE, CURR_VALUE = range( 6) # indexes on querySql row querySql = "SELECT acct.balance_multiplier, acct.iou_limit, shared.balance, shared.interest_rate, shared.last_update, curr.value \ FROM ripple_account as acct JOIN ripple_sharedaccountdata as shared ON shared.id = acct.shared_data_id \ JOIN ripple_currencyunit as curr ON curr.id = shared.currency_id WHERE acct.id = %d" valueSql = "SELECT value FROM ripple_currencyunit WHERE id = %d" # first, get value of pmt currency cursor.execute(valueSql, (pmt.currency_id, )) valueRow = cursor.fetchone() pmt.currencyValue = valueRow[0] # value # next, get acct info for each acct in path and use to determine path credit for acct in path[1:len(path):2]: # accts are at odd indices in path # get acct info cursor.execute(querySql, (acct.id, )) queryRow = cursor.fetchone() # calculate avail credit (in pmt units) acct.actual_balance = queryRow[BALANCE] * queryRow[MULTIPLIER] acct.last_update = queryRow[LAST_UPDATE] # *** causing errors when only one simultaneous payment happening # this is because of django timezone setting -- unset it? (works on windows) if acct.last_update > pmt.date: # can't properly compute interest if someone else already has since the time this payment was supposed to occur at print acct.last_update, pmt.date raise PaymentError( code=TX_COLLISION, message= "Another transaction updated this account since the time we wanted to pay at.", retry=True) acct.interest = computeInterest(acct.actual_balance, queryRow[INTEREST_RATE], acct.last_update, pmt.date) acct.eff_balance = acct.actual_balance + acct.interest acct.availCredit = queryRow[IOU_LIMIT] + acct.eff_balance if pmt.currencyValue: # otherwise, no conversions # get acct currency conversion value acct.currencyValue = queryRow[CURR_VALUE] # calculate available credit in pmt units acct.availCredit *= acct.currencyValue / pmt.currencyValue # convert to pmt units acct.availCredit = float( "%.12f" % acct.availCredit ) # round path's amount to 12 decimal places so all paths add exactly to the total payment amount in db if acct.availCredit < pathCredit: pathCredit = acct.availCredit # check that pathCredit doesn't convert back too high, otherwise updating balance may fail # **** occasionally causing minute amounts of payment to not go through on predicted number of paths # **** instead, allow for minute amounts over limit ##if pmt.currencyValue: # only check if we're doing conversions ## roundBack = pathCredit * pmt.currencyValue / acct.currencyValue ## roundBack = float("%.12f" % roundBack) ## if roundBack > pathCredit: ## pathCredit -= 0.000000000001 # twelve digits down, subtract one if pathCredit <= 0: return 0 # no credit, no payment along this path # insert Path into database sql = "INSERT INTO ripple_paymentpath (payment_id, amount) VALUES (%d, %.12f);" sql += "SELECT currval('ripple_paymentpath_id_seq');" # at the same time get the ID of the path we just inserted cursor.execute(sql, (pmt.id, pathCredit)) # find ID of path we just inserted pathId = cursor.fetchone()[0] # make payments linkInsSql = "INSERT INTO ripple_paymentlink (path_id, payer_account_id, position, amount, interest, interest_rate) " linkInsSql += "VALUES (%d, %d, %d, %.12f, %.12f, %.10f);" ##linkInsSql += "SELECT currval('ripple_paymentlink_id_seq');" position = 1 # start link position sequence at 1 balUpdSql = "UPDATE ripple_sharedaccountdata SET balance = balance - %.14f, last_update = '%s' " balUpdSql += "WHERE ripple_sharedaccountdata.id = %d " #balUpdSql += "AND last_update = '%s' " # make sure last_update stays consistent, otherwise interest won't work. balUpdSql += "AND last_update - '%s' <= INTERVAL '0:0:0.000001' " # alternative version because psycopg sometimes loses a microsecond! balUpdSql += "AND balance * %d >= %.14f" # % balance_multiplier, minimum balance needed for payment to succeed # makes sure there's enough, even if another transaction has fudged around! # above WHERE conditions allow us to use Postgresql in Read Committed (default) mode - see http://www.postgresql.org/files/developer/transactions.pdf # *** could do further real-time safety checking on acct.iou_limit too, to be really sure :) *** for acct in path[1:len(path):2]: # insert PaymentLink linkAmount = pathCredit if pmt.currencyValue: linkAmount *= pmt.currencyValue / acct.currencyValue # convert path amount to acct units cursor.execute( linkInsSql, (pathId, acct.id, position, linkAmount, -acct.interest, acct.sharedData.interest_rate )) # negative interest because this is an outgoing amount if not cursor.rowcount == 1: raise PaymentError( code=LINK_INSERT_FAILED, message="Error inserting path link to database.", retry=True) # shouldn't happen ##print 'Link %d:' % cursor.fetchone()[0] ### ##print '\tAmount: %.14f' % linkAmount ##print '\tBalance: %.14f -> %.14f' % (acct.actual_balance, acct.actual_balance - (linkAmount - acct.interest)) ##print '\tInterest: %.14f' % -acct.interest position = position + 1 # update SharedAccountData balance # **** 10e-12 = allow for a minute amount over limit due to rounding error in conversion balChange = linkAmount - acct.interest cursor.execute( balUpdSql, (balChange * acct.balance_multiplier, pmt.date, acct.shared_data_id, acct.last_update, acct.balance_multiplier, balChange - acct.iou_limit - 10e-12)) if not cursor.rowcount == 1: shared = SharedAccountData.objects.get( pk=acct.shared_data_id ) # find out actual balance, last_update in db raise PaymentError(code=BAL_UPDATE_FAILED, # happens if sql conditions fail, great for concurrent transactions! message="Error updating account balance in database. SQL: " + \ balUpdSql % (balChange * acct.balance_multiplier, str(pmt.date), acct.shared_data_id, acct.last_update, \ acct.balance_multiplier, balChange - acct.iou_limit - 1e-12) + \ "\nActual balance: %.14f (expected: %.14f)\nIOU limit: %.2f\nLast update: %s (expected %s)" % \ (shared.balance, acct.actual_balance * acct.balance_multiplier, acct.iou_limit, shared.last_update, acct.last_update), retry=True) return pathCredit
def payAlongPath(path, amount, cursor, pmt): # use only transaction-safe DB values - means re-get necessary data pathCredit = amount MULTIPLIER, IOU_LIMIT, BALANCE, INTEREST_RATE, LAST_UPDATE, CURR_VALUE = range(6) # indexes on querySql row querySql = "SELECT acct.balance_multiplier, acct.iou_limit, shared.balance, shared.interest_rate, shared.last_update, curr.value \ FROM ripple_account as acct JOIN ripple_sharedaccountdata as shared ON shared.id = acct.shared_data_id \ JOIN ripple_currencyunit as curr ON curr.id = shared.currency_id WHERE acct.id = %d" valueSql = "SELECT value FROM ripple_currencyunit WHERE id = %d" # first, get value of pmt currency cursor.execute(valueSql, (pmt.currency_id,)) valueRow = cursor.fetchone() pmt.currencyValue = valueRow[0] # value # next, get acct info for each acct in path and use to determine path credit for acct in path[1:len(path):2]: # accts are at odd indices in path # get acct info cursor.execute(querySql, (acct.id,)) queryRow = cursor.fetchone() # calculate avail credit (in pmt units) acct.actual_balance = queryRow[BALANCE] * queryRow[MULTIPLIER] acct.last_update = queryRow[LAST_UPDATE] # *** causing errors when only one simultaneous payment happening # this is because of django timezone setting -- unset it? (works on windows) if acct.last_update > pmt.date: # can't properly compute interest if someone else already has since the time this payment was supposed to occur at print acct.last_update, pmt.date raise PaymentError(code=TX_COLLISION, message="Another transaction updated this account since the time we wanted to pay at.", retry=True) acct.interest = computeInterest(acct.actual_balance, queryRow[INTEREST_RATE], acct.last_update, pmt.date) acct.eff_balance = acct.actual_balance + acct.interest acct.availCredit = queryRow[IOU_LIMIT] + acct.eff_balance if pmt.currencyValue: # otherwise, no conversions # get acct currency conversion value acct.currencyValue = queryRow[CURR_VALUE] # calculate available credit in pmt units acct.availCredit *= acct.currencyValue / pmt.currencyValue # convert to pmt units acct.availCredit = float("%.12f" % acct.availCredit) # round path's amount to 12 decimal places so all paths add exactly to the total payment amount in db if acct.availCredit < pathCredit: pathCredit = acct.availCredit # check that pathCredit doesn't convert back too high, otherwise updating balance may fail # **** occasionally causing minute amounts of payment to not go through on predicted number of paths # **** instead, allow for minute amounts over limit ##if pmt.currencyValue: # only check if we're doing conversions ## roundBack = pathCredit * pmt.currencyValue / acct.currencyValue ## roundBack = float("%.12f" % roundBack) ## if roundBack > pathCredit: ## pathCredit -= 0.000000000001 # twelve digits down, subtract one if pathCredit <= 0: return 0 # no credit, no payment along this path # insert Path into database sql = "INSERT INTO ripple_paymentpath (payment_id, amount) VALUES (%d, %.12f);" sql += "SELECT currval('ripple_paymentpath_id_seq');" # at the same time get the ID of the path we just inserted cursor.execute(sql, (pmt.id, pathCredit)) # find ID of path we just inserted pathId = cursor.fetchone()[0] # make payments linkInsSql = "INSERT INTO ripple_paymentlink (path_id, payer_account_id, position, amount, interest, interest_rate) " linkInsSql += "VALUES (%d, %d, %d, %.12f, %.12f, %.10f);" ##linkInsSql += "SELECT currval('ripple_paymentlink_id_seq');" position = 1 # start link position sequence at 1 balUpdSql = "UPDATE ripple_sharedaccountdata SET balance = balance - %.14f, last_update = '%s' " balUpdSql += "WHERE ripple_sharedaccountdata.id = %d " #balUpdSql += "AND last_update = '%s' " # make sure last_update stays consistent, otherwise interest won't work. balUpdSql += "AND last_update - '%s' <= INTERVAL '0:0:0.000001' " # alternative version because psycopg sometimes loses a microsecond! balUpdSql += "AND balance * %d >= %.14f" # % balance_multiplier, minimum balance needed for payment to succeed # makes sure there's enough, even if another transaction has fudged around! # above WHERE conditions allow us to use Postgresql in Read Committed (default) mode - see http://www.postgresql.org/files/developer/transactions.pdf # *** could do further real-time safety checking on acct.iou_limit too, to be really sure :) *** for acct in path[1:len(path):2]: # insert PaymentLink linkAmount = pathCredit if pmt.currencyValue: linkAmount *= pmt.currencyValue / acct.currencyValue # convert path amount to acct units cursor.execute(linkInsSql, (pathId, acct.id, position, linkAmount, -acct.interest, acct.sharedData.interest_rate)) # negative interest because this is an outgoing amount if not cursor.rowcount == 1: raise PaymentError(code=LINK_INSERT_FAILED, message="Error inserting path link to database.", retry=True) # shouldn't happen ##print 'Link %d:' % cursor.fetchone()[0] ### ##print '\tAmount: %.14f' % linkAmount ##print '\tBalance: %.14f -> %.14f' % (acct.actual_balance, acct.actual_balance - (linkAmount - acct.interest)) ##print '\tInterest: %.14f' % -acct.interest position = position + 1 # update SharedAccountData balance # **** 10e-12 = allow for a minute amount over limit due to rounding error in conversion balChange = linkAmount - acct.interest cursor.execute(balUpdSql, (balChange * acct.balance_multiplier, pmt.date, acct.shared_data_id, acct.last_update, acct.balance_multiplier, balChange - acct.iou_limit - 10e-12)) if not cursor.rowcount == 1: shared = SharedAccountData.objects.get(pk=acct.shared_data_id) # find out actual balance, last_update in db raise PaymentError(code=BAL_UPDATE_FAILED, # happens if sql conditions fail, great for concurrent transactions! message="Error updating account balance in database. SQL: " + \ balUpdSql % (balChange * acct.balance_multiplier, str(pmt.date), acct.shared_data_id, acct.last_update, \ acct.balance_multiplier, balChange - acct.iou_limit - 1e-12) + \ "\nActual balance: %.14f (expected: %.14f)\nIOU limit: %.2f\nLast update: %s (expected %s)" % \ (shared.balance, acct.actual_balance * acct.balance_multiplier, acct.iou_limit, shared.last_update, acct.last_update), retry=True) return pathCredit