aboutsummaryrefslogtreecommitdiffstats
path: root/routes
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2021-08-03 01:52:53 -0700
committerAdam Soltys <[email protected]>2021-08-03 01:52:53 -0700
commit1344458e0cf3322253599524899b3cce565cf1af (patch)
treefd2eb66c71211f25f4e5e2ec209a78747a81e123 /routes
parentbca4fe152c83ab6a615a5e1601ca965187ee95e3 (diff)
parente5e2bcf015b128c71cacf125d6aea4a0dddd30c3 (diff)
downloadcoinos-server-1344458e0cf3322253599524899b3cce565cf1af.tar.gz
coinos-server-1344458e0cf3322253599524899b3cce565cf1af.zip
Merge branch 'dev' into v1.0.0
Diffstat (limited to 'routes')
-rw-r--r--routes/admin.js224
-rw-r--r--routes/bitcoin/send.js55
-rw-r--r--routes/invoices.js4
-rw-r--r--routes/lightning/receive.js31
-rw-r--r--routes/liquid/fee.js9
-rw-r--r--routes/liquid/receive.js8
-rw-r--r--routes/liquid/send.js34
-rw-r--r--routes/payments.js6
-rw-r--r--routes/referrals.js7
-rw-r--r--routes/send.js2
-rw-r--r--routes/users.js25
11 files changed, 157 insertions, 248 deletions
diff --git a/routes/admin.js b/routes/admin.js
index c401c724..c7b7578d 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -35,20 +35,12 @@ router.get(
.select('username', 'email', 'phone', 'verified', knex.raw('LEFT(createdAt,10) as created_at'))
.from('users')
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
+ var timeCondition = parseInput.addTimeSearch(req.query, 'users.createdAt')
+ var userCondition = parseInput.addUserSearch(req.query)
- if (search) {
- users = users
- .where(function() {
- this.where('username', 'like', search)
- this.orWhere('email', 'like', search)
- })
- }
- debug('search ? ' + search)
+ if (timeCondition) users = users.whereRaw(timeCondition)
+ if (userCondition) users = users.whereRaw(userCondition)
+
// Alternative Sequelize query syntax
// await db.User.findAll({
// attributes: ['username', 'email', 'phone', 'verified', 'createdAt']
@@ -101,6 +93,11 @@ router.get(
// ]
// })
+ var timeCondition = parseInput.addTimeSearch(req.query, 'referrals.updatedAt')
+ var userCondition = parseInput.addUserSearch(req.query, 'sponsor')
+
+ if (timeCondition) referrals = referrals.whereRaw(timeCondition)
+ if (userCondition) referrals = referrals.whereRaw(userCondition)
debug('Referrals: ' + JSON.stringify(referrals))
return res.send({referrals: referrals})
@@ -141,20 +138,12 @@ router.get(
.leftJoin('users', 'users.email', 'waiting_list.email')
.where('waiting_list.id', '>', 0)
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
+ var timeCondition = parseInput.addTimeSearch(req.query, 'waiting_list.updated_at')
+ var userCondition = parseInput.addUserSearch(req.query)
+
+ if (timeCondition) queue = queue.whereRaw(timeCondition)
+ if (userCondition) queue = queue.whereRaw(userCondition)
- if (search) {
- queue = queue
- .where(function() {
- this.where('waiting_list.email', 'like', search)
- this.orWhere('users.username', 'like', search)
- })
- }
- debug('search ? ' + search)
const found = await queue
debug('Waiting list: ' + JSON.stringify(found))
@@ -195,19 +184,12 @@ router.get(
.where('balance', '>', 0)
}
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
+ var timeCondition = parseInput.addTimeSearch(req.query, 'accounts.updatedAt')
+ var userCondition = parseInput.addUserSearch(req.query)
+
+ if (timeCondition) accounts = accounts.whereRaw(timeCondition)
+ if (userCondition) accounts = accounts.whereRaw(userCondition)
- if (search) {
- accounts = accounts
- .where(function() {
- this.where('users.email', 'like', search)
- this.orWhere('users.username', 'like', search)
- })
- }
debug('search ? ' + search)
const found = await accounts
@@ -270,21 +252,11 @@ router.get(
debug(types[i] + ' since ' + since)
}
}
+
if (since) transactions = transactions.havingRaw('invoices + payments + deposits + orders > ?', [0])
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
-
- if (search) {
- transactions = transactions
- .where(function() {
- this.where('users.email', 'like', search)
- this.orWhere('users.username', 'like', search)
- })
- }
+ var userCondition = parseInput.addUserSearch(req.query)
+ if (userCondition) transactions = transactions.whereRaw(userCondition)
transactions = transactions
.groupBy('users.id')
@@ -338,27 +310,16 @@ router.get(
.leftJoin('users as u1', 'a1.user_id', 'u1.id')
.leftJoin('users as u2', 'a2.user_id', 'u2.id')
- if (since) {
- orders = orders
- .where('orders.updatedAt', '>=', since)
- }
+ var timeCondition = parseInput.addTimeSearch(req.query, 'orders.updatedAt')
+ if (timeCondition) orders = orders.whereRaw(timeCondition)
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
-
- if (search) {
- orders = orders
- .where( function () {
- this.where('u1.email', 'like', search)
- this.orWhere('u1.username', 'like', search)
- this.orWhere('u2.username', 'like', search)
- this.orWhere('u2.email', 'like', search)
- })
- }
- debug('orders since ' + since)
+ var userCondition1 = parseInput.addUserSearch(req.query, 'u1')
+ var userCondition2 = parseInput.addUserSearch(req.query, 'u2')
+
+ if (userCondition1) orders = orders.where( function () {
+ this.whereRaw(userCondition1)
+ this.orWhere(knex.raw(userCondition2))
+ })
var found = await orders
debug('orders: ' + JSON.stringify(found))
@@ -403,26 +364,11 @@ router.get(
.leftJoin('accounts', 'invoices.account_id', 'accounts.id')
.leftJoin('users', 'accounts.user_id', 'users.id')
- if (since) {
- invoices = invoices
- .where('invoices.updatedAt', '>=', since)
- }
-
- debug('invoices since ' + since)
-
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
+ var timeCondition = parseInput.addTimeSearch(req.query, 'invoices.updatedAt')
+ var userCondition = parseInput.addUserSearch(req.query)
- if (search) {
- invoices = invoices
- .where(function() {
- this.where('users.email', 'like', search)
- this.orWhere('users.username', 'like', search)
- })
- }
+ if (timeCondition) invoices = invoices.whereRaw(timeCondition)
+ if (userCondition) invoices = invoices.whereRaw(userCondition)
var found = await invoices
debug('invoices: ' + JSON.stringify(found))
@@ -468,26 +414,11 @@ router.get(
debug('dtype: ' + deposits.constructor + ' : ' + typeof(deposits))
- if (since) {
- deposits = deposits
- .where('deposits.updatedAt', '>=', since)
- }
-
- debug('deposits since ' + since)
-
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
+ var timeCondition = parseInput.addTimeSearch(req.query, 'deposits.updatedAt')
+ var userCondition = parseInput.addUserSearch(req.query)
- if (search) {
- deposits = deposits
- .where(function() {
- this.where('users.email', 'like', search)
- this.orWhere('users.username', 'like', search)
- })
- }
+ if (timeCondition) deposits = deposits.whereRaw(timeCondition)
+ if (userCondition) deposits = deposits.whereRaw(userCondition)
var found = await deposits
debug('deposits: ' + JSON.stringify(found))
@@ -530,27 +461,11 @@ router.get(
.from('withdrawals')
.leftJoin('users', 'withdrawals.user_id', 'users.id')
- if (since) {
- withdrawals = withdrawals
- .where('withdrawals.updatedAt', '>=', since)
- }
-
- debug('withdrawals since ' + since)
-
-
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
+ var timeCondition = parseInput.addTimeSearch(req.query, 'withdrawals.updatedAt')
+ var userCondition = parseInput.addUserSearch(req.query)
- if (search) {
- withdrawals = withdrawals
- .where(function() {
- this.where('users.email', 'like', search)
- this.orWhere('users.username', 'like', search)
- })
- }
+ if (timeCondition) withdrawals = withdrawals.whereRaw(timeCondition)
+ if (userCondition) withdrawals = withdrawals.whereRaw(userCondition)
var found = await withdrawals
debug('withdrawals: ' + JSON.stringify(found))
@@ -595,27 +510,12 @@ router.get(
.leftJoin('accounts', 'payments.account_id', 'accounts.id')
.leftJoin('users', 'accounts.user_id', 'users.id')
- if (since) {
- payments = payments
- .where('payments.updatedAt', '>=', since)
- }
-
- debug('payments since ' + since)
-
+ var timeCondition = parseInput.addTimeSearch(req.query, 'payments.updatedAt')
+ var userCondition = parseInput.addUserSearch(req.query)
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
+ if (timeCondition) payments = payments.whereRaw(timeCondition)
+ if (userCondition) payments = payments.whereRaw(userCondition)
- if (search) {
- payments = payments
- .where(function() {
- this.where('users.email', 'like', search)
- this.orWhere('users.username', 'like', search)
- })
- }
var found = await payments
debug('payments: ' + JSON.stringify(found))
return res.send({payments: found})
@@ -658,7 +558,9 @@ router.get(
// 'payments.network',
// knex.raw('LEFT(hash,20) as hash'),
// knex.raw('Left(payments.updatedAt,16) as transferred')
- )
+ // knex.raw( parseInput.readableDate('Max(payments.updatedAt)', 'last_changed') )
+ knex.raw( 'DATE_FORMAT(Max(payments.updatedAt), ?) AS last_changed', "%M %d, %Y" )
+ )
.from('payments')
.leftJoin('accounts', 'payments.account_id', 'accounts.id')
.leftJoin('users', 'payments.user_id', 'users.id') // should really point to accounts
@@ -666,30 +568,16 @@ router.get(
// .groupBy('payments.currency')
// .groupBy('payments.network')
- if (since) {
- kyc = kyc
- .where('payments.updatedAt', '>=', since)
- }
-
- debug('payments since ' + since)
+ var timeCondition = parseInput.addTimeSearch(req.query, 'payments.updatedAt')
+ var userCondition = parseInput.addUserSearch(req.query)
- if (starts_with) {
- search = search + '%'
- } else if (contains) {
- search = '%' + search + '%'
- }
+ if (timeCondition) kyc = kyc.whereRaw(timeCondition)
+ if (userCondition) kyc = kyc.whereRaw(userCondition)
- if (search) {
- kyc = kyc
- .where(function() {
- this.where('users.email', 'like', search)
- this.orWhere('users.username', 'like', search)
- })
- }
var found = await kyc.having('max', '>=', threshold)
debug('payments: ' + JSON.stringify(found))
- return res.send({kyc_transactions: found})
+ return res.send({found: found})
})
);
diff --git a/routes/bitcoin/send.js b/routes/bitcoin/send.js
index cab79e06..470889b3 100644
--- a/routes/bitcoin/send.js
+++ b/routes/bitcoin/send.js
@@ -1,4 +1,3 @@
-const bitcoin = require("bitcoinjs-lib");
const btc = config.liquid.btcasset;
module.exports = ah(async (req, res) => {
@@ -32,10 +31,10 @@ module.exports = ah(async (req, res) => {
total = total - change + fee;
let amount = total - fee;
- let params;
+ let account, params;
try {
await db.transaction(async transaction => {
- let account = await db.Account.findOne({
+ account = await db.Account.findOne({
where: {
id: user.account_id
},
@@ -62,38 +61,38 @@ module.exports = ah(async (req, res) => {
await account.decrement({ balance: total }, { transaction });
await account.reload({ transaction });
+ });
- params = {
- amount: -amount,
- fee,
- memo,
- account_id: account.id,
- user_id: user.id,
- rate: app.get("rates")[user.currency],
- currency: user.currency,
- address,
- confirmed: true,
- received: false,
- network: "bitcoin"
- };
+ params = {
+ amount: -amount,
+ fee,
+ memo,
+ account_id: account.id,
+ user_id: user.id,
+ rate: app.get("rates")[user.currency],
+ currency: user.currency,
+ address,
+ confirmed: true,
+ received: false,
+ network: "bitcoin"
+ };
- if (config.bitcoin.walletpass)
- await bc.walletPassphrase(config.bitcoin.walletpass, 300);
+ if (config.bitcoin.walletpass)
+ await bc.walletPassphrase(config.bitcoin.walletpass, 300);
- hex = (await bc.signRawTransactionWithWallet(hex)).hex;
- params.hash = await bc.sendRawTransaction(hex);
+ hex = (await bc.signRawTransactionWithWallet(hex)).hex;
+ params.hash = await bc.sendRawTransaction(hex);
- let payment = await db.Payment.create(params, { transaction });
+ let payment = await db.Payment.create(params);
- payment = payment.get({ plain: true });
- payment.account = account.get({ plain: true });
+ payment = payment.get({ plain: true });
+ payment.account = account.get({ plain: true });
- emit(user.username, "payment", payment);
- res.send(payment);
+ emit(user.username, "payment", payment);
+ res.send(payment);
- payments.push(params.hash);
- l.info("sent bitcoin", user.username, total);
- });
+ payments.push(params.hash);
+ l.info("sent bitcoin", user.username, total);
} catch (e) {
l.error("error sending bitcoin", e.message);
return res.status(500).send(e.message);
diff --git a/routes/invoices.js b/routes/invoices.js
index c188811e..51906b8c 100644
--- a/routes/invoices.js
+++ b/routes/invoices.js
@@ -27,6 +27,10 @@ app.post(
let { liquidAddress, invoice, user, tx } = req.body;
let { blindkey } = invoice;
+ if (invoice.amount < 0) throw new Error("amount out of range");
+ if (invoice.tip > invoice.amount || invoice.tip > 1000000 || invoice.tip < 0)
+ throw new Error("tip amount out of range");
+
if (liquidAddress) {
l.info("conversion request for", liquidAddress, invoice.text);
convert[invoice.text] = { address: liquidAddress, tx };
diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js
index 32c54de6..8d2aa750 100644
--- a/routes/lightning/receive.js
+++ b/routes/lightning/receive.js
@@ -1,20 +1,18 @@
const handlePayment = async msg => {
+ if (!msg.settled) return;
let account, total, user;
- try {
- await db.transaction(async transaction => {
- if (!msg.settled) return;
- const invoice = await db.Invoice.findOne({
- where: {
- text: msg.payment_request
- }
- });
+ const invoice = await db.Invoice.findOne({
+ where: {
+ text: msg.payment_request
+ }
+ });
- if (!invoice)
- return l.warn(
- "received lightning with no invoice",
- msg.payment_request
- );
+ if (!invoice)
+ return l.warn("received lightning with no invoice", msg.payment_request);
+
+ try {
+ await db.transaction(async transaction => {
const { text: hash, currency, memo, rate, tip, user_id } = invoice;
const amount = parseInt(msg.amt_paid_sat) - tip;
@@ -85,7 +83,12 @@ const handlePayment = async msg => {
let c = convert[msg.payment_request];
if (msg.payment_request && c) {
- l.info("lightning detected for conversion request", msg.payment_request, c.address, user.username);
+ l.info(
+ "lightning detected for conversion request",
+ msg.payment_request,
+ c.address,
+ user.username
+ );
user.account = account;
diff --git a/routes/liquid/fee.js b/routes/liquid/fee.js
index 2070873a..fad69f13 100644
--- a/routes/liquid/fee.js
+++ b/routes/liquid/fee.js
@@ -3,11 +3,6 @@ const buildTx = require("../../lib/buildliquidtx");
liquidTx = async ({ address, asset, amount, feeRate, replaceable, user }) => {
let tx, fee;
let node = lq;
- /*
- if (asset === config.liquid.btcasset) {
- node = rare;
- }
- */
if (user.account.pubkey) {
let psbt = await buildTx({
@@ -64,6 +59,8 @@ liquidTx = async ({ address, asset, amount, feeRate, replaceable, user }) => {
}
);
+ l.info("funding tx for fee estimate", tx);
+
tx = await node.fundRawTransaction(tx, params);
let blinded = await node.blindRawTransaction(tx.hex);
@@ -81,7 +78,7 @@ module.exports = ah(async (req, res) => {
let tx = await liquidTx({ ...req.body, user: req.user })
res.send(tx);
} catch (e) {
- l.error("error estimating liquid fee", e.message, e.stack);
+ l.error("error estimating liquid fee", e.message);
return res.status(500).send(e.message);
}
});
diff --git a/routes/liquid/receive.js b/routes/liquid/receive.js
index c377a6b5..259db7c7 100644
--- a/routes/liquid/receive.js
+++ b/routes/liquid/receive.js
@@ -101,7 +101,7 @@ zmqRawTx.on("message", async (topic, message, sequence) => {
user_id: user.id,
network: "liquid"
},
- order: [["id", "DESC"]],
+ order: [["id", "DESC"]]
});
if (!invoice) return;
@@ -286,9 +286,9 @@ setInterval(async () => {
for (let i = 0; i < arr.length; i++) {
const hash = arr[i];
- let account, address, user, total;
+ let account, address, user, total, p;
await db.transaction(async transaction => {
- let p = await db.Payment.findOne({
+ p = await db.Payment.findOne({
where: { hash, confirmed: 0, received: 1 },
include: [
{
@@ -347,7 +347,7 @@ setInterval(async () => {
});
let c = convert[address];
- if (address && c) {
+ if (address && c && p.account.asset === network.assetHash) {
l.info(
"liquid detected for conversion request",
address,
diff --git a/routes/liquid/send.js b/routes/liquid/send.js
index 19b7344f..13f77d26 100644
--- a/routes/liquid/send.js
+++ b/routes/liquid/send.js
@@ -1,5 +1,6 @@
const btc = config.liquid.btcasset;
const lcad = config.liquid.cadasset;
+const { Transaction } = require("liquidjs-lib");
sendLiquid = async ({ asset, amount, user, address, memo, tx, limit }) => {
if (amount > 10000000) throw new Error("Amount too large");
@@ -48,7 +49,8 @@ sendLiquid = async ({ asset, amount, user, address, memo, tx, limit }) => {
const assets = Object.keys(totals);
const payments = [];
- return db.transaction(async transaction => {
+ let main, signed;
+ await db.transaction(async transaction => {
for (let i = 0; i < assets.length; i++) {
let asset = assets[i];
let amount = totals[asset];
@@ -75,12 +77,12 @@ sendLiquid = async ({ asset, amount, user, address, memo, tx, limit }) => {
await faucet.reload({ transaction });
await faucet.save({ transaction });
}
-
- total += fee - covered;
}
+
+ total += fee - covered;
}
- if (limit && total > limit)
+ if (limit && total > limit + fee)
throw new Error("Tx amount exceeds authorized amount");
if (asset !== btc || total) {
@@ -132,16 +134,11 @@ sendLiquid = async ({ asset, amount, user, address, memo, tx, limit }) => {
}
}
- if (config.liquid.walletpass)
- await lq.walletPassphrase(config.liquid.walletpass, 300);
-
- let blinded = await lq.blindRawTransaction(tx.hex);
- let signed = await lq.signRawTransactionWithWallet(blinded);
- let txid = await lq.sendRawTransaction(signed.hex);
-
- l.info("sent liquid tx", txid, address);
+ signed = await lq.signRawTransactionWithWallet(
+ await lq.blindRawTransaction(tx.hex)
+ );
+ let txid = Transaction.fromHex(signed.hex).getId();
- let main;
for (let i = 0; i < assets.length; i++) {
p = payments[i];
if (p) {
@@ -157,15 +154,22 @@ sendLiquid = async ({ asset, amount, user, address, memo, tx, limit }) => {
emit(user.username, "account", main.account);
emit(user.username, "payment", main);
- return main;
});
+
+ if (config.liquid.walletpass)
+ await lq.walletPassphrase(config.liquid.walletpass, 300);
+
+ let txid = await lq.sendRawTransaction(signed.hex);
+ l.info("sent liquid tx", txid, address);
+
+ return main;
};
module.exports = ah(async (req, res) => {
let { user } = req;
try {
- res.send(await sendLiquid({...req.body, user }));
+ res.send(await sendLiquid({ ...req.body, user }));
} catch (e) {
l.error("problem sending liquid", user.username, e.message);
return res.status(500).send(e);
diff --git a/routes/payments.js b/routes/payments.js
index a749e333..e78730d2 100644
--- a/routes/payments.js
+++ b/routes/payments.js
@@ -151,9 +151,9 @@ ah(async () => {
const { hdkeypath } = await lq.getAddressInfo(address);
const parts = hdkeypath.split("/");
app.set("lqAddressIndex", parts[parts.length - 1].slice(0, -1));
- } catch (e) {
- console.error("Problem getting liquid address", e);
- }
+ } catch(e) {
+ l.warn("Problem getting liquid address index", e.message);
+ }
}, 50);
}
diff --git a/routes/referrals.js b/routes/referrals.js
index 3bcce5b2..11cf8590 100644
--- a/routes/referrals.js
+++ b/routes/referrals.js
@@ -244,6 +244,7 @@ router.post(
debug('email: ' + email)
debug('phone: ' + phone)
+ debug('user_id: ' + user_id)
await knex.table('waiting_list')
.insert({
@@ -259,15 +260,17 @@ router.post(
router.get(
"/joinQueue",
ah(async (req, res) => {
- const {email, phone} = req.query;
+ const {email, phone, user_id} = req.query;
debug('email: ' + email)
debug('phone: ' + phone)
+ debug('user: ' + user_id)
await knex('waiting_list')
.insert({
email: email,
- phone: phone
+ phone: phone,
+ user_id: user_id
})
res.send({success: true, message: 'Added ' + email + ' to waiting list ' + phone})
diff --git a/routes/send.js b/routes/send.js
index 579e70b4..3aec9fb6 100644
--- a/routes/send.js
+++ b/routes/send.js
@@ -45,7 +45,7 @@ module.exports = ah(async (req, res, next) => {
};
if (!username) {
- return res.status(500).send("Vouchers temporarily disabled");
+ throw new Error("Vouchers temporarily disabled");
l.info("creating redeemable payment");
params.redeemcode = uuidv4();
params.hash = `${req.get("origin")}/redeem/${params.redeemcode}`;
diff --git a/routes/users.js b/routes/users.js
index 71f4b89a..23f016c7 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -148,7 +148,8 @@ app.post(
return res.status(500).send("Username taken");
} else {
sockets[username] = sockets[user.username];
- if (user.username !== username) l.info("changing username", user.username, username);
+ if (user.username !== username)
+ l.info("changing username", user.username, username);
user.username = username;
token = jwt.sign({ username }, config.jwt);
@@ -323,10 +324,7 @@ app.post(
(user.password &&
!(await bcrypt.compare(req.body.password, user.password)))
) {
- l.warn(
- "invalid username or password attempt",
- req.body.username
- );
+ l.warn("invalid username or password attempt", req.body.username);
return res.status(401).end();
}
@@ -426,11 +424,24 @@ app.get(
: config.liquid.network
];
type = "p2sh";
-
+
// async request to node to bump its internal index but don't use result
- lq.getNewAddress().catch(console.error);
+ lq.getNewAddress().catch(e =>
+ l.warn("Problem bumping liquid address index", e.message)
+ );
i = parseInt(app.get("lqAddressIndex"));
+
+ if (!i) {
+ const { hdkeypath } = await lq.getAddressInfo(
+ await lq.getNewAddress()
+ );
+ const parts = hdkeypath.split("/");
+ i = parts[parts.length - 1].slice(0, -1);
+ }
+
+ if (!i) throw new Error("Problem generating address");
+
hd = fromBase58(config.liquid.masterkey, n).derivePath(`m/0'/0'/${i}'`);
app.set("lqAddressIndex", i + 1);
} else {