Browse Source

another big wallet cleanup

miguelfreitas
Miguel Freitas 11 years ago
parent
commit
78ba2a5609
  1. 1
      src/bitcoinrpc.h
  2. 2
      src/init.cpp
  3. 2
      src/rpcdump.cpp
  4. 3
      src/rpcwallet.cpp
  5. 515
      src/wallet.cpp
  6. 50
      src/wallet.h
  7. 2
      src/walletdb.cpp

1
src/bitcoinrpc.h

@ -11,7 +11,6 @@
#include <map> #include <map>
class CBlockIndex; class CBlockIndex;
class CReserveKey;
#include "json/json_spirit_reader_template.h" #include "json/json_spirit_reader_template.h"
#include "json/json_spirit_writer_template.h" #include "json/json_spirit_writer_template.h"

2
src/init.cpp

@ -840,12 +840,14 @@ bool AppInit2(boost::thread_group& threadGroup)
// Create new keyUser and set as default key // Create new keyUser and set as default key
RandAddSeedPerfmon(); RandAddSeedPerfmon();
/* [MF] FIXME: require user on initialization?
CPubKey newDefaultKey; CPubKey newDefaultKey;
if (pwalletMain->GetKeyFromPool(newDefaultKey, false)) { if (pwalletMain->GetKeyFromPool(newDefaultKey, false)) {
pwalletMain->SetDefaultKey(newDefaultKey); pwalletMain->SetDefaultKey(newDefaultKey);
if (!pwalletMain->SetAddressBookName(pwalletMain->vchDefaultKey.GetID(), "")) if (!pwalletMain->SetAddressBookName(pwalletMain->vchDefaultKey.GetID(), ""))
strErrors << _("Cannot write default address") << "\n"; strErrors << _("Cannot write default address") << "\n";
} }
*/
pwalletMain->SetBestChain(CBlockLocator(pindexBest)); pwalletMain->SetBestChain(CBlockLocator(pindexBest));
} }

2
src/rpcdump.cpp

@ -228,9 +228,7 @@ Value dumpwallet(const Array& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file"); throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
std::map<CKeyID, int64> mapKeyBirth; std::map<CKeyID, int64> mapKeyBirth;
std::set<CKeyID> setKeyPool;
pwalletMain->GetKeyBirthTimes(mapKeyBirth); pwalletMain->GetKeyBirthTimes(mapKeyBirth);
pwalletMain->GetAllReserveKeys(setKeyPool);
// sort time/key pairs // sort time/key pairs
std::vector<std::pair<int64, CKeyID> > vKeyBirth; std::vector<std::pair<int64, CKeyID> > vKeyBirth;

3
src/rpcwallet.cpp

@ -80,9 +80,6 @@ Value getinfo(const Array& params, bool fHelp)
obj.push_back(Pair("proxy", (proxy.first.IsValid() ? proxy.first.ToStringIPPort() : string()))); obj.push_back(Pair("proxy", (proxy.first.IsValid() ? proxy.first.ToStringIPPort() : string())));
obj.push_back(Pair("difficulty", (double)GetDifficulty())); obj.push_back(Pair("difficulty", (double)GetDifficulty()));
obj.push_back(Pair("testnet", TestNet())); obj.push_back(Pair("testnet", TestNet()));
obj.push_back(Pair("keypoololdest", (boost::int64_t)pwalletMain->GetOldestKeyPoolTime()));
obj.push_back(Pair("keypoolsize", pwalletMain->GetKeyPoolSize()));
obj.push_back(Pair("paytxfee", ValueFromAmount(nTransactionFee)));
if (pwalletMain->IsCrypted()) if (pwalletMain->IsCrypted())
obj.push_back(Pair("unlocked_until", (boost::int64_t)nWalletUnlockTime)); obj.push_back(Pair("unlocked_until", (boost::int64_t)nWalletUnlockTime));
obj.push_back(Pair("errors", GetWarnings("statusbar"))); obj.push_back(Pair("errors", GetWarnings("statusbar")));

515
src/wallet.cpp

@ -49,6 +49,9 @@ CPubKey CWallet::GenerateNewKey(std::string username)
if (!AddKeyPubKey(secret, pubkey)) if (!AddKeyPubKey(secret, pubkey))
throw std::runtime_error("CWallet::GenerateNewKey() : AddKey failed"); throw std::runtime_error("CWallet::GenerateNewKey() : AddKey failed");
if (!vchDefaultKey.IsValid())
vchDefaultKey = pubkey;
return pubkey; return pubkey;
} }
@ -683,253 +686,6 @@ void CWallet::ResendWalletTransactions()
// Actions // Actions
// //
bool CWallet::CreateTransaction(const vector<pair<CScript, int64> >& vecSend,
CWalletTx& wtxNew, CReserveKey& reservekey, int64& nFeeRet, std::string& strFailReason)
{
int64 nValue = 0;
BOOST_FOREACH (const PAIRTYPE(CScript, int64)& s, vecSend)
{
if (nValue < 0)
{
strFailReason = _("Transaction amounts must be positive");
return false;
}
nValue += s.second;
}
if (vecSend.empty() || nValue < 0)
{
strFailReason = _("Transaction amounts must be positive");
return false;
}
wtxNew.BindWallet(this);
{
LOCK2(cs_main, cs_wallet);
{
nFeeRet = nTransactionFee;
/*
loop
{
wtxNew.vin.clear();
wtxNew.vout.clear();
wtxNew.fFromMe = true;
int64 nTotalValue = nValue + nFeeRet;
double dPriority = 0;
// vouts to the payees
BOOST_FOREACH (const PAIRTYPE(CScript, int64)& s, vecSend)
{
CTxOut txout(s.second, s.first);
if (txout.IsDust(CTransaction::nMinRelayTxFee))
{
strFailReason = _("Transaction amount too small");
return false;
}
wtxNew.vout.push_back(txout);
}
// Choose coins to use
set<pair<const CWalletTx*,unsigned int> > setCoins;
int64 nValueIn = 0;
if (!SelectCoins(nTotalValue, setCoins, nValueIn))
{
strFailReason = _("Insufficient funds");
return false;
}
BOOST_FOREACH(PAIRTYPE(const CWalletTx*, unsigned int) pcoin, setCoins)
{
int64 nCredit = pcoin.first->vout[pcoin.second].nValue;
//The priority after the next block (depth+1) is used instead of the current,
//reflecting an assumption the user would accept a bit more delay for
//a chance at a free transaction.
dPriority += (double)nCredit * (pcoin.first->GetDepthInMainChain()+1);
}
int64 nChange = nValueIn - nValue - nFeeRet;
// if sub-cent change is required, the fee must be raised to at least nMinTxFee
// or until nChange becomes zero
// NOTE: this depends on the exact behaviour of GetMinFee
if (nFeeRet < CTransaction::nMinTxFee && nChange > 0 && nChange < CENT)
{
int64 nMoveToFee = min(nChange, CTransaction::nMinTxFee - nFeeRet);
nChange -= nMoveToFee;
nFeeRet += nMoveToFee;
}
if (nChange > 0)
{
// Note: We use a new key here to keep it from being obvious which side is the change.
// The drawback is that by not reusing a previous key, the change may be lost if a
// backup is restored, if the backup doesn't have the new private key for the change.
// If we reused the old key, it would be possible to add code to look for and
// rediscover unknown transactions that were written with keys of ours to recover
// post-backup change.
// Reserve a new key pair from key pool
CPubKey vchPubKey;
assert(reservekey.GetReservedKey(vchPubKey)); // should never fail, as we just unlocked
// Fill a vout to ourself
// TODO: pass in scriptChange instead of reservekey so
// change transaction isn't always pay-to-bitcoin-address
CScript scriptChange;
scriptChange.SetDestination(vchPubKey.GetID());
CTxOut newTxOut(nChange, scriptChange);
// Never create dust outputs; if we would, just
// add the dust to the fee.
if (newTxOut.IsDust(CTransaction::nMinRelayTxFee))
{
nFeeRet += nChange;
reservekey.ReturnKey();
}
else
{
// Insert change txn at random position:
vector<CTxOut>::iterator position = wtxNew.vout.begin()+GetRandInt(wtxNew.vout.size()+1);
wtxNew.vout.insert(position, newTxOut);
}
}
else
reservekey.ReturnKey();
// Fill vin
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
wtxNew.vin.push_back(CTxIn(coin.first->GetHash(),coin.second));
// Sign
int nIn = 0;
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
if (!SignSignature(*this, *coin.first, wtxNew, nIn++))
{
strFailReason = _("Signing transaction failed");
return false;
}
// Limit size
unsigned int nBytes = ::GetSerializeSize(*(CTransaction*)&wtxNew, SER_NETWORK, PROTOCOL_VERSION);
if (nBytes >= MAX_STANDARD_TX_SIZE)
{
strFailReason = _("Transaction too large");
return false;
}
dPriority /= nBytes;
// Check that enough fee is included
int64 nPayFee = nTransactionFee * (1 + (int64)nBytes / 1000);
bool fAllowFree = AllowFree(dPriority);
int64 nMinFee = GetMinFee(wtxNew, fAllowFree, GMF_SEND);
if (nFeeRet < max(nPayFee, nMinFee))
{
nFeeRet = max(nPayFee, nMinFee);
continue;
}
// Fill vtxPrev by copying from previous transactions vtxPrev
wtxNew.AddSupportingTransactions();
wtxNew.fTimeReceivedIsTxTime = true;
break;
}
*/
}
}
return true;
}
bool CWallet::CreateTransaction(CScript scriptPubKey, int64 nValue,
CWalletTx& wtxNew, CReserveKey& reservekey, int64& nFeeRet, std::string& strFailReason)
{
vector< pair<CScript, int64> > vecSend;
vecSend.push_back(make_pair(scriptPubKey, nValue));
return CreateTransaction(vecSend, wtxNew, reservekey, nFeeRet, strFailReason);
}
// Call after CreateTransaction unless you want to abort
bool CWallet::CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey)
{
{
LOCK2(cs_main, cs_wallet);
printf("CommitTransaction:\n%s", wtxNew.ToString().c_str());
{
// This is only to keep the database open to defeat the auto-flush for the
// duration of this scope. This is the only place where this optimization
// maybe makes sense; please don't do it anywhere else.
CWalletDB* pwalletdb = fFileBacked ? new CWalletDB(strWalletFile,"r") : NULL;
// Take key pair from key pool so it won't be used again
reservekey.KeepKey();
// Add tx to wallet, because if it has change it's also ours,
// otherwise just for transaction history.
AddToWallet(wtxNew);
// Mark old coins as spent
set<CWalletTx*> setCoins;
/*
BOOST_FOREACH(const CTxIn& txin, wtxNew.vin)
{
CWalletTx &coin = mapWallet[txin.prevout.hash];
coin.BindWallet(this);
coin.MarkSpent(txin.prevout.n);
coin.WriteToDisk();
NotifyTransactionChanged(this, coin.GetHash(), CT_UPDATED);
}
*/
if (fFileBacked)
delete pwalletdb;
}
// Track how many getdata requests our transaction gets
mapRequestCount[wtxNew.GetHash()] = 0;
// Broadcast
if (!wtxNew.AcceptToMemoryPool(false))
{
// This must not fail. The transaction has already been signed and recorded.
printf("CommitTransaction() : Error: Transaction not valid");
return false;
}
wtxNew.RelayWalletTransaction();
}
return true;
}
string CWallet::SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, bool fAskFee)
{
CReserveKey reservekey(this);
int64 nFeeRequired;
if (IsLocked())
{
string strError = _("Error: Wallet locked, unable to create transaction!");
printf("SendMoney() : %s", strError.c_str());
return strError;
}
string strError;
if (!CreateTransaction(scriptPubKey, nValue, wtxNew, reservekey, nFeeRequired, strError))
{
printf("SendMoney() : %s\n", strError.c_str());
return strError;
}
if (fAskFee && !uiInterface.ThreadSafeAskFee(nFeeRequired))
return "ABORTED";
if (!CommitTransaction(wtxNew, reservekey))
return _("Error: The transaction was rejected! This might happen if some of the coins in your wallet were already spent, such as if you used a copy of wallet.dat and coins were spent in the copy but not marked as spent here.");
return "";
}
DBErrors CWallet::LoadWallet(bool& fFirstRunRet) DBErrors CWallet::LoadWallet(bool& fFirstRunRet)
{ {
if (!fFileBacked) if (!fFileBacked)
@ -955,24 +711,6 @@ DBErrors CWallet::LoadWallet(bool& fFirstRunRet)
} }
bool CWallet::SetAddressBookName(const CTxDestination& address, const string& strName)
{
std::map<CTxDestination, std::string>::iterator mi = mapAddressBook.find(address);
mapAddressBook[address] = strName;
NotifyAddressBookChanged(this, address, strName, ::IsMine(*this, address), (mi == mapAddressBook.end()) ? CT_NEW : CT_UPDATED);
if (!fFileBacked)
return false;
return CWalletDB(strWalletFile).WriteName(CBitcoinAddress(address).ToString(), strName);
}
bool CWallet::DelAddressBookName(const CTxDestination& address)
{
mapAddressBook.erase(address);
NotifyAddressBookChanged(this, address, "", ::IsMine(*this, address), CT_DELETED);
if (!fFileBacked)
return false;
return CWalletDB(strWalletFile).EraseName(CBitcoinAddress(address).ToString());
}
bool CWallet::SetDefaultKey(const CPubKey &vchPubKey) bool CWallet::SetDefaultKey(const CPubKey &vchPubKey)
@ -995,253 +733,6 @@ bool GetWalletFile(CWallet* pwallet, string &strWalletFileOut)
} }
void CWallet::ReserveKeyFromKeyPool(int64& nIndex, CKeyPool& keypool)
{
nIndex = -1;
keypool.vchPubKey = CPubKey();
{
LOCK(cs_wallet);
// Get the oldest key
if(setKeyPool.empty())
return;
CWalletDB walletdb(strWalletFile);
nIndex = *(setKeyPool.begin());
setKeyPool.erase(setKeyPool.begin());
if (!walletdb.ReadPool(nIndex, keypool))
throw runtime_error("ReserveKeyFromKeyPool() : read failed");
if (!HaveKey(keypool.vchPubKey.GetID()))
throw runtime_error("ReserveKeyFromKeyPool() : unknown key in key pool");
assert(keypool.vchPubKey.IsValid());
printf("keypool reserve %"PRI64d"\n", nIndex);
}
}
int64 CWallet::AddReserveKey(const CKeyPool& keypool)
{
{
LOCK2(cs_main, cs_wallet);
CWalletDB walletdb(strWalletFile);
int64 nIndex = 1 + *(--setKeyPool.end());
if (!walletdb.WritePool(nIndex, keypool))
throw runtime_error("AddReserveKey() : writing added key failed");
setKeyPool.insert(nIndex);
return nIndex;
}
return -1;
}
void CWallet::KeepKey(int64 nIndex)
{
// Remove from key pool
if (fFileBacked)
{
CWalletDB walletdb(strWalletFile);
walletdb.ErasePool(nIndex);
}
printf("keypool keep %"PRI64d"\n", nIndex);
}
void CWallet::ReturnKey(int64 nIndex)
{
// Return to key pool
{
LOCK(cs_wallet);
setKeyPool.insert(nIndex);
}
printf("keypool return %"PRI64d"\n", nIndex);
}
bool CWallet::GetKeyFromPool(CPubKey& result, bool fAllowReuse)
{
/* [MF] pool is going to die.
int64 nIndex = 0;
CKeyPool keypool;
{
LOCK(cs_wallet);
ReserveKeyFromKeyPool(nIndex, keypool);
if (nIndex == -1)
{
if (fAllowReuse && vchDefaultKey.IsValid())
{
result = vchDefaultKey;
return true;
}
if (IsLocked()) return false;
result = GenerateNewKey();
return true;
}
KeepKey(nIndex);
result = keypool.vchPubKey;
}
return true;
*/
return false;
}
int64 CWallet::GetOldestKeyPoolTime()
{
int64 nIndex = 0;
CKeyPool keypool;
ReserveKeyFromKeyPool(nIndex, keypool);
if (nIndex == -1)
return GetTime();
ReturnKey(nIndex);
return keypool.nTime;
}
set< set<CTxDestination> > CWallet::GetAddressGroupings()
{
set< set<CTxDestination> > groupings;
set<CTxDestination> grouping;
BOOST_FOREACH(PAIRTYPE(uint256, CWalletTx) walletEntry, mapWallet)
{
CWalletTx *pcoin = &walletEntry.second;
/*
if (pcoin->vin.size() > 0)
{
bool any_mine = false;
// group all input addresses with each other
BOOST_FOREACH(CTxIn txin, pcoin->vin)
{
CTxDestination address;
if(!IsMine(txin))
continue;
if(!ExtractDestination(mapWallet[txin.prevout.hash].vout[txin.prevout.n].scriptPubKey, address))
continue;
grouping.insert(address);
any_mine = true;
}
// group change with input addresses
if (any_mine)
{
BOOST_FOREACH(CTxOut txout, pcoin->vout)
if (IsChange(txout))
{
CTxDestination txoutAddr;
if(!ExtractDestination(txout.scriptPubKey, txoutAddr))
continue;
grouping.insert(txoutAddr);
}
}
if (grouping.size() > 0)
{
groupings.insert(grouping);
grouping.clear();
}
}
// group lone addrs by themselves
for (unsigned int i = 0; i < pcoin->vout.size(); i++)
if (IsMine(pcoin->vout[i]))
{
CTxDestination address;
if(!ExtractDestination(pcoin->vout[i].scriptPubKey, address))
continue;
grouping.insert(address);
groupings.insert(grouping);
grouping.clear();
}
*/
}
set< set<CTxDestination>* > uniqueGroupings; // a set of pointers to groups of addresses
map< CTxDestination, set<CTxDestination>* > setmap; // map addresses to the unique group containing it
BOOST_FOREACH(set<CTxDestination> grouping, groupings)
{
// make a set of all the groups hit by this new group
set< set<CTxDestination>* > hits;
map< CTxDestination, set<CTxDestination>* >::iterator it;
BOOST_FOREACH(CTxDestination address, grouping)
if ((it = setmap.find(address)) != setmap.end())
hits.insert((*it).second);
// merge all hit groups into a new single group and delete old groups
set<CTxDestination>* merged = new set<CTxDestination>(grouping);
BOOST_FOREACH(set<CTxDestination>* hit, hits)
{
merged->insert(hit->begin(), hit->end());
uniqueGroupings.erase(hit);
delete hit;
}
uniqueGroupings.insert(merged);
// update setmap
BOOST_FOREACH(CTxDestination element, *merged)
setmap[element] = merged;
}
set< set<CTxDestination> > ret;
BOOST_FOREACH(set<CTxDestination>* uniqueGrouping, uniqueGroupings)
{
ret.insert(*uniqueGrouping);
delete uniqueGrouping;
}
return ret;
}
bool CReserveKey::GetReservedKey(CPubKey& pubkey)
{
if (nIndex == -1)
{
CKeyPool keypool;
pwallet->ReserveKeyFromKeyPool(nIndex, keypool);
if (nIndex != -1)
vchPubKey = keypool.vchPubKey;
else {
if (pwallet->vchDefaultKey.IsValid()) {
printf("CReserveKey::GetReservedKey(): Warning: Using default key instead of a new key, top up your keypool!");
vchPubKey = pwallet->vchDefaultKey;
} else
return false;
}
}
assert(vchPubKey.IsValid());
pubkey = vchPubKey;
return true;
}
void CReserveKey::KeepKey()
{
if (nIndex != -1)
pwallet->KeepKey(nIndex);
nIndex = -1;
vchPubKey = CPubKey();
}
void CReserveKey::ReturnKey()
{
if (nIndex != -1)
pwallet->ReturnKey(nIndex);
nIndex = -1;
vchPubKey = CPubKey();
}
void CWallet::GetAllReserveKeys(set<CKeyID>& setAddress) const
{
setAddress.clear();
CWalletDB walletdb(strWalletFile);
LOCK2(cs_main, cs_wallet);
BOOST_FOREACH(const int64& id, setKeyPool)
{
CKeyPool keypool;
if (!walletdb.ReadPool(id, keypool))
throw runtime_error("GetAllReserveKeyHashes() : read failed");
assert(keypool.vchPubKey.IsValid());
CKeyID keyID = keypool.vchPubKey.GetID();
if (!HaveKey(keyID))
throw runtime_error("GetAllReserveKeyHashes() : unknown key in key pool");
setAddress.insert(keyID);
}
}
void CWallet::UpdatedTransaction(const uint256 &hashTx) void CWallet::UpdatedTransaction(const uint256 &hashTx)
{ {

50
src/wallet.h

@ -20,7 +20,6 @@
#include "util.h" #include "util.h"
class CWalletTx; class CWalletTx;
class CReserveKey;
class COutput; class COutput;
class CWalletDB; class CWalletDB;
@ -160,22 +159,6 @@ public:
int ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate = false); int ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate = false);
void ReacceptWalletTransactions(); void ReacceptWalletTransactions();
void ResendWalletTransactions(); void ResendWalletTransactions();
bool CreateTransaction(const std::vector<std::pair<CScript, int64> >& vecSend,
CWalletTx& wtxNew, CReserveKey& reservekey, int64& nFeeRet, std::string& strFailReason);
bool CreateTransaction(CScript scriptPubKey, int64 nValue,
CWalletTx& wtxNew, CReserveKey& reservekey, int64& nFeeRet, std::string& strFailReason);
bool CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey);
std::string SendMoney(CScript scriptPubKey, int64 nValue, CWalletTx& wtxNew, bool fAskFee=false);
int64 AddReserveKey(const CKeyPool& keypool);
void ReserveKeyFromKeyPool(int64& nIndex, CKeyPool& keypool);
void KeepKey(int64 nIndex);
void ReturnKey(int64 nIndex);
bool GetKeyFromPool(CPubKey &key, bool fAllowReuse=true);
int64 GetOldestKeyPoolTime();
void GetAllReserveKeys(std::set<CKeyID>& setAddress) const;
std::set< std::set<CTxDestination> > GetAddressGroupings();
bool IsMine(const CTransaction& tx) const bool IsMine(const CTransaction& tx) const
{ {
@ -189,10 +172,6 @@ public:
DBErrors LoadWallet(bool& fFirstRunRet); DBErrors LoadWallet(bool& fFirstRunRet);
bool SetAddressBookName(const CTxDestination& address, const std::string& strName);
bool DelAddressBookName(const CTxDestination& address);
void UpdatedTransaction(const uint256 &hashTx); void UpdatedTransaction(const uint256 &hashTx);
void PrintWallet(const CBlock& block); void PrintWallet(const CBlock& block);
@ -207,11 +186,6 @@ public:
} }
} }
int GetKeyPoolSize()
{
return setKeyPool.size();
}
bool GetTransaction(const uint256 &hashTx, CWalletTx& wtx); bool GetTransaction(const uint256 &hashTx, CWalletTx& wtx);
bool SetDefaultKey(const CPubKey &vchPubKey); bool SetDefaultKey(const CPubKey &vchPubKey);
@ -236,30 +210,6 @@ public:
boost::signals2::signal<void (CWallet *wallet, const uint256 &hashTx, ChangeType status)> NotifyTransactionChanged; boost::signals2::signal<void (CWallet *wallet, const uint256 &hashTx, ChangeType status)> NotifyTransactionChanged;
}; };
/** A key allocated from the key pool. */
class CReserveKey
{
protected:
CWallet* pwallet;
int64 nIndex;
CPubKey vchPubKey;
public:
CReserveKey(CWallet* pwalletIn)
{
nIndex = -1;
pwallet = pwalletIn;
}
~CReserveKey()
{
ReturnKey();
}
void ReturnKey();
bool GetReservedKey(CPubKey &pubkey);
void KeepKey();
};
typedef std::map<std::string, std::string> mapValue_t; typedef std::map<std::string, std::string> mapValue_t;

2
src/walletdb.cpp

@ -11,8 +11,6 @@ using namespace std;
using namespace boost; using namespace boost;
static uint64 nAccountingEntryNumber = 0;
// //
// CWalletDB // CWalletDB
// //

Loading…
Cancel
Save