bool PathPaymentOpFrame::doApply(medida::MetricsRegistry& metrics, LedgerDelta& delta, LedgerManager& ledgerManager) { Database& db = ledgerManager.getDatabase(); innerResult().code(PATH_PAYMENT_SUCCESS); // tracks the last amount that was traded int64_t curBReceived = mPathPayment.destAmount; Asset curB = mPathPayment.destAsset; // update balances, walks backwards // build the full path to the destination, starting with sendAsset std::vector<Asset> fullPath; fullPath.emplace_back(mPathPayment.sendAsset); fullPath.insert(fullPath.end(), mPathPayment.path.begin(), mPathPayment.path.end()); bool bypassIssuerCheck = false; // if the payment doesn't involve intermediate accounts // and the destination is the issuer we don't bother // checking if the destination account even exist // so that it's always possible to send credits back to its issuer bypassIssuerCheck = (curB.type() != ASSET_TYPE_NATIVE) && (fullPath.size() == 1) && (mPathPayment.sendAsset == mPathPayment.destAsset) && (getIssuer(curB) == mPathPayment.destination); AccountFrame::pointer destination; if (!bypassIssuerCheck) { destination = AccountFrame::loadAccount(mPathPayment.destination, db); if (!destination) { metrics.NewMeter({"op-path-payment", "failure", "no-destination"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_NO_DESTINATION); return false; } } // update last balance in the chain if (curB.type() == ASSET_TYPE_NATIVE) { destination->getAccount().balance += curBReceived; destination->storeChange(delta, db); } else { TrustFrame::pointer destLine; if (bypassIssuerCheck) { destLine = TrustFrame::loadTrustLine(mPathPayment.destination, curB, db); } else { auto tlI = TrustFrame::loadTrustLineIssuer(mPathPayment.destination, curB, db); if (!tlI.second) { metrics.NewMeter({"op-path-payment", "failure", "no-issuer"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_NO_ISSUER); innerResult().noIssuer() = curB; return false; } destLine = tlI.first; } if (!destLine) { metrics.NewMeter({"op-path-payment", "failure", "no-trust"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_NO_TRUST); return false; } if (!destLine->isAuthorized()) { metrics.NewMeter({"op-path-payment", "failure", "not-authorized"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_NOT_AUTHORIZED); return false; } if (!destLine->addBalance(curBReceived)) { metrics.NewMeter({"op-path-payment", "failure", "line-full"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_LINE_FULL); return false; } destLine->storeChange(delta, db); } innerResult().success().last = SimplePaymentResult(mPathPayment.destination, curB, curBReceived); // now, walk the path backwards for (int i = (int)fullPath.size() - 1; i >= 0; i--) { int64_t curASent, actualCurBReceived; Asset const& curA = fullPath[i]; if (curA == curB) { continue; } if (curA.type() != ASSET_TYPE_NATIVE) { if (!AccountFrame::loadAccount(getIssuer(curA), db)) { metrics.NewMeter({"op-path-payment", "failure", "no-issuer"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_NO_ISSUER); innerResult().noIssuer() = curA; return false; } } OfferExchange oe(delta, ledgerManager); // curA -> curB OfferExchange::ConvertResult r = oe.convertWithOffers( curA, INT64_MAX, curASent, curB, curBReceived, actualCurBReceived, [this, &metrics](OfferFrame const& o) { if (o.getSellerID() == getSourceID()) { // we are crossing our own offer, potentially invalidating // mSourceAccount (balance or numSubEntries) metrics.NewMeter({"op-path-payment", "failure", "offer-cross-self"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_OFFER_CROSS_SELF); return OfferExchange::eStop; } return OfferExchange::eKeep; }); switch (r) { case OfferExchange::eFilterStop: return false; case OfferExchange::eOK: if (curBReceived == actualCurBReceived) { break; } // fall through case OfferExchange::ePartial: metrics.NewMeter({"op-path-payment", "failure", "too-few-offers"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_TOO_FEW_OFFERS); return false; } assert(curBReceived == actualCurBReceived); curBReceived = curASent; // next round, we need to send enough curB = curA; // add offers that got taken on the way // insert in front to match the path's order auto& offers = innerResult().success().offers; offers.insert(offers.begin(), oe.getOfferTrail().begin(), oe.getOfferTrail().end()); } // last step: we've reached the first account in the chain, update its // balance int64_t curBSent; curBSent = curBReceived; if (curBSent > mPathPayment.sendMax) { // make sure not over the max metrics.NewMeter({"op-path-payment", "failure", "over-send-max"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_OVER_SENDMAX); return false; } if (curB.type() == ASSET_TYPE_NATIVE) { int64_t minBalance = mSourceAccount->getMinimumBalance(ledgerManager); if ((mSourceAccount->getAccount().balance - curBSent) < minBalance) { // they don't have enough to send metrics.NewMeter({"op-path-payment", "failure", "underfunded"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_UNDERFUNDED); return false; } mSourceAccount->getAccount().balance -= curBSent; mSourceAccount->storeChange(delta, db); } else { TrustFrame::pointer sourceLineFrame; if (bypassIssuerCheck) { sourceLineFrame = TrustFrame::loadTrustLine(getSourceID(), curB, db); } else { auto tlI = TrustFrame::loadTrustLineIssuer(getSourceID(), curB, db); if (!tlI.second) { metrics.NewMeter({"op-path-payment", "failure", "no-issuer"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_NO_ISSUER); innerResult().noIssuer() = curB; return false; } sourceLineFrame = tlI.first; } if (!sourceLineFrame) { metrics.NewMeter({"op-path-payment", "failure", "src-no-trust"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_SRC_NO_TRUST); return false; } if (!sourceLineFrame->isAuthorized()) { metrics.NewMeter( {"op-path-payment", "failure", "src-not-authorized"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_SRC_NOT_AUTHORIZED); return false; } if (!sourceLineFrame->addBalance(-curBSent)) { metrics.NewMeter({"op-path-payment", "failure", "underfunded"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_UNDERFUNDED); return false; } sourceLineFrame->storeChange(delta, db); } metrics.NewMeter({"op-path-payment", "success", "apply"}, "operation") .Mark(); return true; }
// you are selling sheep for wheat // need to check the counter offers selling wheat for sheep // see if this is modifying an old offer // see if this offer crosses any existing offers bool ManageOfferOpFrame::doApply(medida::MetricsRegistry& metrics, LedgerDelta& delta, LedgerManager& ledgerManager) { Database& db = ledgerManager.getDatabase(); if (!checkOfferValid(metrics, db)) { return false; } Asset const& sheep = mManageOffer.selling; Asset const& wheat = mManageOffer.buying; bool creatingNewOffer = false; uint64_t offerID = mManageOffer.offerID; if (offerID) { // modifying an old offer mSellSheepOffer = OfferFrame::loadOffer(getSourceID(), offerID, db); if (mSellSheepOffer) { // make sure the currencies are the same if (!compareAsset(mManageOffer.selling, mSellSheepOffer->getOffer().selling) || !compareAsset(mManageOffer.buying, mSellSheepOffer->getOffer().buying)) { metrics.NewMeter({"op-manage-offer", "invalid", "mismatch"}, "operation").Mark(); innerResult().code(MANAGE_OFFER_MISMATCH); return false; } mPassive = mSellSheepOffer->getFlags() & PASSIVE_FLAG; } else { metrics.NewMeter({"op-manage-offer", "invalid", "not-found"}, "operation").Mark(); innerResult().code(MANAGE_OFFER_NOT_FOUND); return false; } } else { // creating a new Offer creatingNewOffer = true; mSellSheepOffer = OfferFrame::from(getSourceID(), mManageOffer); if (mPassive) mSellSheepOffer->mEntry.data.offer().flags = PASSIVE_FLAG; } int64_t maxSheepSend = mManageOffer.amount; int64_t maxAmountOfSheepCanSell; if (sheep.type() == ASSET_TYPE_NATIVE) { maxAmountOfSheepCanSell = mSourceAccount->getBalanceAboveReserve(ledgerManager); } else { maxAmountOfSheepCanSell = mSheepLineA->getBalance(); } // the maximum is defined by how much wheat it can receive int64_t maxWheatCanSell; if (wheat.type() == ASSET_TYPE_NATIVE) { maxWheatCanSell = INT64_MAX; } else { maxWheatCanSell = mWheatLineA->getMaxAmountReceive(); if (maxWheatCanSell == 0) { metrics.NewMeter({"op-manage-offer", "invalid", "line-full"}, "operation").Mark(); innerResult().code(MANAGE_OFFER_LINE_FULL); return false; } } { int64_t maxSheepBasedOnWheat; if (!bigDivide(maxSheepBasedOnWheat, maxWheatCanSell, mManageOffer.price.d, mManageOffer.price.n)) { maxSheepBasedOnWheat = INT64_MAX; } if (maxAmountOfSheepCanSell > maxSheepBasedOnWheat) { maxAmountOfSheepCanSell = maxSheepBasedOnWheat; } } // amount of sheep for sale is the lesser of amount we can sell and amount // put in the offer if (maxAmountOfSheepCanSell < maxSheepSend) { maxSheepSend = maxAmountOfSheepCanSell; } Price sheepPrice = mManageOffer.price; innerResult().code(MANAGE_OFFER_SUCCESS); { soci::transaction sqlTx(db.getSession()); LedgerDelta tempDelta(delta); int64_t sheepSent, wheatReceived; OfferExchange oe(tempDelta, ledgerManager); Price maxWheatPrice(sheepPrice.d, sheepPrice.n); OfferExchange::ConvertResult r = oe.convertWithOffers( sheep, maxSheepSend, sheepSent, wheat, maxWheatCanSell, wheatReceived, [this, maxWheatPrice](OfferFrame const& o) { if ((mPassive && (o.getPrice() >= maxWheatPrice)) || (o.getPrice() > maxWheatPrice)) { return OfferExchange::eStop; } if (o.getSellerID() == getSourceID()) { // we are crossing our own offer innerResult().code(MANAGE_OFFER_CROSS_SELF); return OfferExchange::eStop; } return OfferExchange::eKeep; }); bool offerIsValid = false; switch (r) { case OfferExchange::eOK: case OfferExchange::ePartial: offerIsValid = true; break; case OfferExchange::eFilterStop: if (innerResult().code() != MANAGE_OFFER_SUCCESS) { return false; } offerIsValid = true; break; } // updates the result with the offers that got taken on the way for (auto const& oatom : oe.getOfferTrail()) { innerResult().success().offersClaimed.push_back(oatom); } if (wheatReceived > 0) { if (wheat.type() == ASSET_TYPE_NATIVE) { mSourceAccount->getAccount().balance += wheatReceived; mSourceAccount->storeChange(delta, db); } else { TrustFrame::pointer wheatLineSigningAccount; wheatLineSigningAccount = TrustFrame::loadTrustLine(getSourceID(), wheat, db); if (!wheatLineSigningAccount) { throw std::runtime_error("invalid database state: must " "have matching trust line"); } if (!wheatLineSigningAccount->addBalance(wheatReceived)) { // this would indicate a bug in OfferExchange throw std::runtime_error("offer claimed over limit"); } wheatLineSigningAccount->storeChange(delta, db); } if (sheep.type() == ASSET_TYPE_NATIVE) { mSourceAccount->getAccount().balance -= sheepSent; mSourceAccount->storeChange(delta, db); } else { if (!mSheepLineA->addBalance(-sheepSent)) { // this would indicate a bug in OfferExchange throw std::runtime_error("offer sold more than balance"); } mSheepLineA->storeChange(delta, db); } } // recomputes the amount of sheep for sale mSellSheepOffer->getOffer().amount = maxSheepSend - sheepSent; if (offerIsValid && mSellSheepOffer->getOffer().amount > 0) { // we still have sheep to sell so leave an offer if (creatingNewOffer) { // make sure we don't allow us to add offers when we don't have // the minbalance if (!mSourceAccount->addNumEntries(1, ledgerManager)) { metrics.NewMeter( {"op-manage-offer", "invalid", "low reserve"}, "operation").Mark(); innerResult().code(MANAGE_OFFER_LOW_RESERVE); return false; } mSellSheepOffer->mEntry.data.offer().offerID = tempDelta.getHeaderFrame().generateID(); innerResult().success().offer.effect(MANAGE_OFFER_CREATED); mSellSheepOffer->storeAdd(tempDelta, db); mSourceAccount->storeChange(tempDelta, db); } else { innerResult().success().offer.effect(MANAGE_OFFER_UPDATED); mSellSheepOffer->storeChange(tempDelta, db); } innerResult().success().offer.offer() = mSellSheepOffer->getOffer(); } else { innerResult().success().offer.effect(MANAGE_OFFER_DELETED); if (!creatingNewOffer) { mSellSheepOffer->storeDelete(tempDelta, db); mSourceAccount->addNumEntries(-1, ledgerManager); mSourceAccount->storeChange(tempDelta, db); } } sqlTx.commit(); tempDelta.commit(); } metrics.NewMeter({"op-create-offer", "success", "apply"}, "operation") .Mark(); return true; }
OfferExchange::CrossOfferResult OfferExchange::crossOffer(OfferFrame& sellingWheatOffer, int64_t maxWheatReceived, int64_t& numWheatReceived, int64_t maxSheepSend, int64_t& numSheepSend) { Asset& sheep = sellingWheatOffer.getOffer().buying; Asset& wheat = sellingWheatOffer.getOffer().selling; AccountID& accountBID = sellingWheatOffer.getOffer().sellerID; Database& db = mLedgerManager.getDatabase(); AccountFrame::pointer accountB; accountB = AccountFrame::loadAccount(accountBID, db); if (!accountB) { throw std::runtime_error( "invalid database state: offer must have matching account"); } TrustFrame::pointer wheatLineAccountB; if (wheat.type() != ASSET_TYPE_NATIVE) { wheatLineAccountB = TrustFrame::loadTrustLine(accountBID, wheat, db); if (!wheatLineAccountB) { throw std::runtime_error( "invalid database state: offer must have matching trust line"); } } TrustFrame::pointer sheepLineAccountB; if (sheep.type() == ASSET_TYPE_NATIVE) { numWheatReceived = INT64_MAX; } else { sheepLineAccountB = TrustFrame::loadTrustLine(accountBID, sheep, db); if (!sheepLineAccountB) { throw std::runtime_error( "invalid database state: offer must have matching trust line"); } // compute numWheatReceived based on what the account can receive int64_t sellerMaxSheep = sheepLineAccountB->getMaxAmountReceive(); if (!bigDivide(numWheatReceived, sellerMaxSheep, sellingWheatOffer.getOffer().price.d, sellingWheatOffer.getOffer().price.n)) { numWheatReceived = INT64_MAX; } } // adjust numWheatReceived with what the seller has { int64_t wheatCanSell; if (wheat.type() == ASSET_TYPE_NATIVE) { // can only send above the minimum balance wheatCanSell = accountB->getBalanceAboveReserve(mLedgerManager); } else { if (wheatLineAccountB->isAuthorized()) { wheatCanSell = wheatLineAccountB->getBalance(); } else { wheatCanSell = 0; } } if (numWheatReceived > wheatCanSell) { numWheatReceived = wheatCanSell; } } // you can receive the lesser of the amount of wheat offered or // the amount the guy has if (numWheatReceived >= sellingWheatOffer.getOffer().amount) { numWheatReceived = sellingWheatOffer.getOffer().amount; } else { // update the offer based on the balance (to determine if it should be // deleted or not) // note that we don't need to write into the db at this point as the // actual update // is done further down sellingWheatOffer.getOffer().amount = numWheatReceived; } bool reducedOffer = false; if (numWheatReceived > maxWheatReceived) { numWheatReceived = maxWheatReceived; reducedOffer = true; } // this guy can get X wheat to you. How many sheep does that get him? if (!bigDivide(numSheepSend, numWheatReceived, sellingWheatOffer.getOffer().price.n, sellingWheatOffer.getOffer().price.d)) { numSheepSend = INT64_MAX; } if (numSheepSend > maxSheepSend) { // reduce the number even more if there is a limit on Sheep numSheepSend = maxSheepSend; reducedOffer = true; } // bias towards seller (this cannot overflow at this point) numWheatReceived = bigDivide(numSheepSend, sellingWheatOffer.getOffer().price.d, sellingWheatOffer.getOffer().price.n); bool offerTaken = false; if (numWheatReceived == 0 || numSheepSend == 0) { if (reducedOffer) { return eOfferCantConvert; } else { // force delete the offer as it represents a bogus offer numWheatReceived = 0; numSheepSend = 0; offerTaken = true; } } offerTaken = offerTaken || sellingWheatOffer.getOffer().amount <= numWheatReceived; if (offerTaken) { // entire offer is taken sellingWheatOffer.storeDelete(mDelta, db); accountB->addNumEntries(-1, mLedgerManager); accountB->storeChange(mDelta, db); } else { sellingWheatOffer.getOffer().amount -= numWheatReceived; sellingWheatOffer.storeChange(mDelta, db); } // Adjust balances if (sheep.type() == ASSET_TYPE_NATIVE) { accountB->getAccount().balance += numSheepSend; accountB->storeChange(mDelta, db); } else { if (!sheepLineAccountB->addBalance(numSheepSend)) { return eOfferCantConvert; } sheepLineAccountB->storeChange(mDelta, db); } if (wheat.type() == ASSET_TYPE_NATIVE) { accountB->getAccount().balance -= numWheatReceived; accountB->storeChange(mDelta, db); } else { if (!wheatLineAccountB->addBalance(-numWheatReceived)) { return eOfferCantConvert; } wheatLineAccountB->storeChange(mDelta, db); } mOfferTrail.push_back( ClaimOfferAtom(accountB->getID(), sellingWheatOffer.getOfferID(), wheat, numWheatReceived, sheep, numSheepSend)); return offerTaken ? eOfferTaken : eOfferPartial; }
bool PathPaymentOpFrame::doApply(medida::MetricsRegistry& metrics, LedgerDelta& delta, LedgerManager& ledgerManager) { AccountFrame::pointer destination; Database& db = ledgerManager.getDatabase(); destination = AccountFrame::loadAccount(mPathPayment.destination, db); if (!destination) { metrics.NewMeter({"op-path-payment", "failure", "no-destination"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_NO_DESTINATION); return false; } innerResult().code(PATH_PAYMENT_SUCCESS); // tracks the last amount that was traded int64_t curBReceived = mPathPayment.destAmount; Asset curB = mPathPayment.destAsset; // update balances, walks backwards // build the full path to the destination, starting with sendAsset std::vector<Asset> fullPath; fullPath.emplace_back(mPathPayment.sendAsset); fullPath.insert(fullPath.end(), mPathPayment.path.begin(), mPathPayment.path.end()); // update last balance in the chain { if (curB.type() == ASSET_TYPE_NATIVE) { destination->getAccount().balance += curBReceived; destination->storeChange(delta, db); } else { TrustFrame::pointer destLine; destLine = TrustFrame::loadTrustLine(destination->getID(), curB, db); if (!destLine) { metrics.NewMeter({"op-path-payment", "failure", "no-trust"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_NO_TRUST); return false; } if (!destLine->isAuthorized()) { metrics.NewMeter( {"op-path-payment", "failure", "not-authorized"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_NOT_AUTHORIZED); return false; } if (!destLine->addBalance(curBReceived)) { metrics.NewMeter({"op-path-payment", "failure", "line-full"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_LINE_FULL); return false; } destLine->storeChange(delta, db); } innerResult().success().last = SimplePaymentResult(destination->getID(), curB, curBReceived); } // now, walk the path backwards for (int i = (int)fullPath.size() - 1; i >= 0; i--) { int64_t curASent, actualCurBReceived; Asset const& curA = fullPath[i]; if (curA == curB) { continue; } OfferExchange oe(delta, ledgerManager); // curA -> curB OfferExchange::ConvertResult r = oe.convertWithOffers(curA, INT64_MAX, curASent, curB, curBReceived, actualCurBReceived, nullptr); switch (r) { case OfferExchange::eFilterStop: assert(false); // no filter -> should not happen break; case OfferExchange::eOK: if (curBReceived == actualCurBReceived) { break; } // fall through case OfferExchange::ePartial: metrics.NewMeter({"op-path-payment", "failure", "too-few-offers"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_TOO_FEW_OFFERS); return false; } assert(curBReceived == actualCurBReceived); curBReceived = curASent; // next round, we need to send enough curB = curA; // add offers that got taken on the way // insert in front to match the path's order auto& offers = innerResult().success().offers; offers.insert(offers.begin(), oe.getOfferTrail().begin(), oe.getOfferTrail().end()); } // last step: we've reached the first account in the chain, update its // balance int64_t curBSent; curBSent = curBReceived; if (curBSent > mPathPayment.sendMax) { // make sure not over the max metrics.NewMeter({"op-path-payment", "failure", "over-send-max"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_OVER_SENDMAX); return false; } if (curB.type() == ASSET_TYPE_NATIVE) { int64_t minBalance = mSourceAccount->getMinimumBalance(ledgerManager); if ((mSourceAccount->getAccount().balance - curBSent) < minBalance) { // they don't have enough to send metrics.NewMeter({"op-path-payment", "failure", "underfunded"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_UNDERFUNDED); return false; } mSourceAccount->getAccount().balance -= curBSent; mSourceAccount->storeChange(delta, db); } else { AccountFrame::pointer issuer; if(curB.type()==ASSET_TYPE_CREDIT_ALPHANUM4) issuer = AccountFrame::loadAccount(curB.alphaNum4().issuer, db); else if(curB.type()==ASSET_TYPE_CREDIT_ALPHANUM12) issuer = AccountFrame::loadAccount(curB.alphaNum12().issuer, db); if (!issuer) { metrics.NewMeter({"op-path-payment", "failure", "no-issuer"}, "operation").Mark(); throw std::runtime_error("sendCredit Issuer not found"); } TrustFrame::pointer sourceLineFrame; sourceLineFrame = TrustFrame::loadTrustLine(getSourceID(), curB, db); if (!sourceLineFrame) { metrics.NewMeter({"op-path-payment", "failure", "src-no-trust"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_SRC_NO_TRUST); return false; } if (!sourceLineFrame->isAuthorized()) { metrics.NewMeter( {"op-path-payment", "failure", "src-not-authorized"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_SRC_NOT_AUTHORIZED); return false; } if (!sourceLineFrame->addBalance(-curBSent)) { metrics.NewMeter({"op-path-payment", "failure", "underfunded"}, "operation").Mark(); innerResult().code(PATH_PAYMENT_UNDERFUNDED); return false; } sourceLineFrame->storeChange(delta, db); } metrics.NewMeter({"op-path-payment", "success", "apply"}, "operation") .Mark(); return true; }