From c615a409acfdfb6bf1f4bc11651845979dea2e79 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 18 Sep 2020 22:54:12 -0700 Subject: fee faucets --- routes/assets.js | 525 +++++++++++++++++++++++++++----------------------- routes/liquid/send.js | 124 +++++++----- 2 files changed, 356 insertions(+), 293 deletions(-) diff --git a/routes/assets.js b/routes/assets.js index c476b3f5..34856f04 100644 --- a/routes/assets.js +++ b/routes/assets.js @@ -8,7 +8,7 @@ let fetchAssets; "https://assets.blockstream.info/" ); - app.set('assets', assets); + app.set("assets", assets); } catch (e) { l.error("error fetching assets", e.message); res.status(500).send("error fetching assets"); @@ -17,287 +17,279 @@ let fetchAssets; setTimeout(fetchAssets, 7200000); })(); -app.get("/assets", ah(async (req, res) => { - if (app.get('assets')) { - const assets = app.get('assets'); - const accounts = await db.Account.findAll({ - group: ["asset"], - }); - - Object.keys(assets).map((a) => (assets[a].registered = true)); - - accounts.map((a) => { - if (!assets[a.asset]) assets[a.asset] = a; - }); - - res.send(assets); - } - else app.status(500).send('Problem fetching blockstream asset registry data'); -})); - -app.post("/assets", auth, ah(async (req, res) => { - try { - const sha256 = crypto.createHash("sha256"); - const { id: user_id } = req.user; - const token_address = await lq.getNewAddress("", "legacy"); - const asset_address = await lq.getNewAddress("", "legacy"); - const blind = false; - const info = await lq.getAddressInfo(asset_address); - const { pubkey: issuer_pubkey } = info; - - const { - domain, - name, - asset_amount, - token_amount, - precision, - ticker, - } = req.body; - const version = 0; - - const contract = { - entity: { domain }, - issuer_pubkey, - name, - precision, - ticker, - version, - }; +app.get( + "/assets", + ah(async (req, res) => { + if (app.get("assets")) { + const assets = app.get("assets"); + const accounts = await db.Account.findAll({ + group: ["asset"] + }); - sha256.update(JSON.stringify(contract)); - const hash = sha256.digest("hex"); - const contract_hash = hash - .match(/[a-f0-9]{2}/g) - .reverse() - .join(""); - const rawtx = await lq.createRawTransaction([], { data: "00" }); - const funded = await lq.fundRawTransaction(rawtx, { feeRate: 0.000003 }); - const params = { - asset_amount, - asset_address, - blind, - contract_hash, - }; + Object.keys(assets).map(a => (assets[a].registered = true)); - params.asset_amount = asset_amount / (SATS / 10 ** precision); + accounts.map(a => { + if (!assets[a.asset]) assets[a.asset] = a; + }); - l.info("asset_amount", params.asset_amount); - if (token_amount) { - params.token_amount = token_amount; - params.token_address = token_address; - params.token_amount = parseInt(params.token_amount); - } + res.send(assets); + } else + app.status(500).send("Problem fetching blockstream asset registry data"); + }) +); - let ria; +app.post( + "/assets", + auth, + ah(async (req, res) => { try { - ria = await lq.rawIssueAsset(funded.hex, [params]); - } catch (e) { - l.info(asset_amount, token_amount, params); - throw new Error(e.message); - } + const sha256 = crypto.createHash("sha256"); + const { id: user_id } = req.user; + const token_address = await lq.getNewAddress("", "legacy"); + const asset_address = await lq.getNewAddress("", "legacy"); + const blind = false; + const info = await lq.getAddressInfo(asset_address); + const { pubkey: issuer_pubkey } = info; + + const { + domain, + name, + asset_amount, + token_amount, + precision, + ticker + } = req.body; + const version = 0; + + const contract = { + entity: { domain }, + issuer_pubkey, + name, + precision, + ticker, + version + }; - const { asset, hex, token } = ria[0]; - const brt = await lq.blindRawTransaction(hex, true, [], false); - const srt = await lq.signRawTransactionWithWallet(brt); - const allowed = (await lq.testMempoolAccept([srt.hex]))[0].allowed; - if (allowed) { - const txid = await lq.sendRawTransaction(srt.hex); + sha256.update(JSON.stringify(contract)); + const hash = sha256.digest("hex"); + const contract_hash = hash + .match(/[a-f0-9]{2}/g) + .reverse() + .join(""); + const rawtx = await lq.createRawTransaction([], { data: "00" }); + const funded = await lq.fundRawTransaction(rawtx, { feeRate: 0.000003 }); + const params = { + asset_amount, + asset_address, + blind, + contract_hash + }; - await db.transaction(async (transaction) => { - let account = await db.Account.findOne({ - where: { - user_id, - asset: config.liquid.btcasset, - pubkey: null, - }, - include: { - model: db.User, - as: "user", - }, - lock: transaction.LOCK.UPDATE, - transaction, - }); + params.asset_amount = asset_amount / (SATS / 10 ** precision); - let { user } = account; + l.info("asset_amount", params.asset_amount); + if (token_amount) { + params.token_amount = token_amount; + params.token_address = token_address; + params.token_amount = parseInt(params.token_amount); + } - if (Math.round(funded.fee * SATS) > account.balance) { - l.error( - "amount exceeds balance", - asset_amount, - funded.fee, - account.balance - ); - throw new Error(`Insufficient funds to pay fee of ${funded.fee} BTC`); - } + let ria; + try { + ria = await lq.rawIssueAsset(funded.hex, [params]); + } catch (e) { + l.info(asset_amount, token_amount, params); + throw new Error(e.message); + } - account.balance -= Math.round(funded.fee * SATS); - await account.save({ transaction }); - emit(user.username, "account", account); + const { asset, hex, token } = ria[0]; + const brt = await lq.blindRawTransaction(hex, true, [], false); + const srt = await lq.signRawTransactionWithWallet(brt); + const allowed = (await lq.testMempoolAccept([srt.hex]))[0].allowed; + if (allowed) { + const txid = await lq.sendRawTransaction(srt.hex); - account = await db.Account.create( - { - asset, - contract, - domain, - user_id, - ticker, - precision, - name, - balance: 0, - network: 'liquid', - pending: params.asset_amount * SATS, - }, - { transaction } - ); + await db.transaction(async transaction => { + let account = await db.Account.findOne({ + where: { + user_id, + asset: config.liquid.btcasset, + pubkey: null + }, + include: { + model: db.User, + as: "user" + }, + lock: transaction.LOCK.UPDATE, + transaction + }); - emit(user.username, "account", account); + let { user } = account; + + if (Math.round(funded.fee * SATS) > account.balance) { + l.error( + "amount exceeds balance", + asset_amount, + funded.fee, + account.balance + ); + throw new Error( + `Insufficient funds to pay fee of ${funded.fee} BTC` + ); + } - const asset_payment = await db.Payment.create( - { - account_id: account.id, - user_id, - hash: txid, - amount: params.asset_amount * SATS, - received: true, - confirmed: false, - address: asset_address, - network: "liquid", - }, - { transaction } - ); - emit(user.username, "payment", asset_payment); - - issuances[txid] = { - user_id, - asset, - asset_amount: params.asset_amount, - asset_payment_id: asset_payment.id, - }; + account.balance -= Math.round(funded.fee * SATS); + await account.save({ transaction }); + emit(user.username, "account", account); - if (token_amount) { account = await db.Account.create( { - asset: token, - user_id, + asset, + contract, domain, - ticker: `${ticker}REISSUANCETOKEN`, - precision: 8, - name: `${name} Reissuance Token`, + user_id, + ticker, + precision, + name, balance: 0, - pending: token_amount * SATS, + network: "liquid", + pending: params.asset_amount * SATS }, { transaction } ); + emit(user.username, "account", account); - const token_payment = await db.Payment.create( + const asset_payment = await db.Payment.create( { account_id: account.id, user_id, hash: txid, - amount: token_amount * SATS, + amount: params.asset_amount * SATS, received: true, confirmed: false, - address: token_address, - network: "liquid", + address: asset_address, + network: "liquid" }, { transaction } ); - emit(user.username, "payment", token_payment); - - issuances[txid].token = token; - issuances[txid].token_amount = token_amount; - issuances[txid].token_payment_id = token_payment.id; - } - }); + emit(user.username, "payment", asset_payment); - res.send(issuances[txid]); - } - } catch (e) { - l.error("asset issuance failed", e.message); - res.status(500).send(e.message); - } -})); + issuances[txid] = { + user_id, + asset, + asset_amount: params.asset_amount, + asset_payment_id: asset_payment.id + }; + + if (token_amount) { + account = await db.Account.create( + { + asset: token, + user_id, + domain, + ticker: `${ticker}REISSUANCETOKEN`, + precision: 8, + name: `${name} Reissuance Token`, + balance: 0, + pending: token_amount * SATS + }, + { transaction } + ); + emit(user.username, "account", account); + + const token_payment = await db.Payment.create( + { + account_id: account.id, + user_id, + hash: txid, + amount: token_amount * SATS, + received: true, + confirmed: false, + address: token_address, + network: "liquid" + }, + { transaction } + ); + emit(user.username, "payment", token_payment); -app.post("/assets/register", auth, ah(async (req, res) => { - const { asset } = req.body; - const account = await db.Account.findOne({ - where: { - user_id: req.user.id, - asset, - }, - }); + issuances[txid].token = token; + issuances[txid].token_amount = token_amount; + issuances[txid].token_payment_id = token_payment.id; + } + }); - try { - const { data: result } = await axios.post( - "https://assets.blockstream.info/", - { - asset_id: asset, - contract: account.contract, + res.send(issuances[txid]); } - ); - l.info("register asset result", req.user.username, result); - res.send(result); - } catch (e) { - l.error("asset registration failed", e.message); - res.status(500).send(e.message); - } -})); - -app.post("/loadFaucet", auth, ah(async (req, res) => { - const { asset, amount } = req.body; - - try { - await db.transaction(async transaction => { - let { account } = user; - - if (account.balance < amount) { - throw new Error("Insufficient funds"); + } catch (e) { + l.error("asset issuance failed", e.message); + res.status(500).send(e.message); + } + }) +); + +app.post( + "/assets/register", + auth, + ah(async (req, res) => { + const { asset } = req.body; + const account = await db.Account.findOne({ + where: { + user_id: req.user.id, + asset } + }); - let fee = 0; - - account.balance -= amount; - await account.save({ transaction }); - - let params = { - amount: -amount, - account_id: account.id, - memo, - user_id: user.id, - rate: app.get("rates")[user.currency], - currency: user.currency, - confirmed: true, - hash: "Internal Transfer", - network: "COINOS" - }; + try { + const { data: result } = await axios.post( + "https://assets.blockstream.info/", + { + asset_id: asset, + contract: account.contract + } + ); + l.info("register asset result", req.user.username, result); + res.send(result); + } catch (e) { + l.error("asset registration failed", e.message); + res.status(500).send(e.message); + } + }) +); - let payment = await db.Payment.create(params, { transaction }); +app.post( + "/loadFaucet", + auth, + ah(async (req, res) => { + const { user } = req; + const { asset, amount } = req.body; - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); + try { + await db.transaction(async transaction => { + let { account } = user; + + if (account.asset !== config.liquid.btcasset) + throw new Error( + "Faucet has to be funded with bitcoin. Try sending from another wallet." + ); + if (account.balance < amount) throw new Error("Insufficient funds"); - l.info("sent internal", user.username, -payment.amount); + let fee = 0; - emit(user.username, "payment", payment); - emit(user.username, "account", account); - emit(user.username, "user", user); + account.balance -= amount; + await account.save({ transaction }); let a2; let acc = { user_id: null, - asset, + asset }; - if (recipient.account.asset === asset && !recipient.account.pubkey) - ({ account: a2 } = recipient); - else { - a2 = await db.Account.findOne({ - where: acc, - lock: transaction.LOCK.UPDATE, - transaction - }); - } + a2 = await db.Account.findOne({ + where: acc, + lock: transaction.LOCK.UPDATE, + transaction + }); if (a2) { a2.balance += amount; @@ -308,7 +300,7 @@ app.post("/loadFaucet", auth, ah(async (req, res) => { let ticker = asset.substr(0, 3).toUpperCase(); let precision = 8; - const assets = app.get('assets'); + const assets = app.get("assets"); if (assets[asset]) { ({ domain, ticker, precision, name } = assets[asset]); @@ -329,20 +321,61 @@ app.post("/loadFaucet", auth, ah(async (req, res) => { acc = { ...acc, ...{ domain, ticker, precision, name } }; acc.balance = amount; acc.pending = 0; - acc.network = 'liquid'; + acc.network = "liquid"; a2 = await db.Account.create(acc, { transaction }); } + let params = { + amount: -amount, + account_id: account.id, + user_id: user.id, + rate: app.get("rates")[user.currency], + currency: user.currency, + confirmed: true, + hash: `Loaded Faucet - ${a2.ticker}`, + network: "COINOS" + }; + + let payment = await db.Payment.create(params, { transaction }); + + payment = payment.get({ plain: true }); + payment.account = account.get({ plain: true }); + + l.info("sent internal", user.username, -payment.amount); + + emit(user.username, "payment", payment); + emit(user.username, "account", account); + emit(user.username, "user", user); + l.info("loaded faucet", asset, amount); res.end(); + }); + } catch (e) { + l.error("problem loading faucet", user.username, user.balance, e.message, e.stack); + return res.status(500).send(e.message); + throw(e); + } + }) +); + +app.get( + "/faucet", + auth, + ah(async (req, res) => { + let { asset } = req.query; + let faucet = await db.Account.findOne({ + where: { + asset, + user_id: null, + } }); - } catch (e) { - l.error( - "problem loading faucet", - user.username, - user.balance, - e.message, - ); - return res.status(500).send(e.message); - } -})); + + if (!faucet) faucet = { + asset, + balance: 0, + ticker: 'Unknown', + }; + + res.send(faucet); + }) +); diff --git a/routes/liquid/send.js b/routes/liquid/send.js index a38f84e2..0562c750 100644 --- a/routes/liquid/send.js +++ b/routes/liquid/send.js @@ -1,8 +1,10 @@ +const btc = config.liquid.btcasset; + module.exports = ah(async (req, res) => { let { user } = req; let { address, memo, tx } = req.body; - const isChange = async (address) => + const isChange = async address => (await lq.getAddressInfo(address)).ismine && !Object.keys(addresses).includes(address); @@ -16,7 +18,7 @@ module.exports = ah(async (req, res) => { let { asset, value, - scriptPubKey: { type, addresses }, + scriptPubKey: { type, addresses } } = vout[i]; if (type === "fee") fee = toSats(value); @@ -36,53 +38,79 @@ module.exports = ah(async (req, res) => { const payments = []; try { - await db.transaction(async (transaction) => { + await db.transaction(async transaction => { for (let i = 0; i < assets.length; i++) { let asset = assets[i]; let amount = totals[asset]; if (change[asset]) amount -= change[asset]; let total = amount; - if (asset === config.liquid.btcasset) total += fee; - l.info("creating liquid payment", user.username, asset, total, fee); + if (asset === btc) { + let covered = 0; + let nonbtc = assets.filter(a => a !== btc); + if (nonbtc.length === 1) { + let faucet = await db.Account.findOne({ + where: { + asset: nonbtc[0], + user_id: null + } + }); + + if (faucet) { + covered = faucet.balance; + if (covered > fee) covered = fee; + faucet.balance -= covered; + await faucet.save({ transaction }); + } + + total += fee - covered; + } + } - let account = await db.Account.findOne({ - where: { - user_id: user.id, - asset, - }, - lock: transaction.LOCK.UPDATE, - transaction, - }); - - if (total > account.balance) { - l.warn("amount exceeds balance", { - total, - fee, - balance: account.balance, + if (asset !== btc || total) { + l.info("creating liquid payment", user.username, asset, total, fee); + + let account = await db.Account.findOne({ + where: { + user_id: user.id, + asset, + pubkey: null + }, + lock: transaction.LOCK.UPDATE, + transaction }); - throw new Error("Insufficient funds"); - } - account.balance -= total; - await account.save({ transaction }); - - let payment = { - amount: -amount, - account_id: account.id, - fee, - memo, - user_id: user.id, - rate: app.get("rates")[user.currency], - currency: user.currency, - address, - confirmed: true, - received: false, - network: "liquid", - }; - - payment.account = account; - payments.push(payment); + if (total > account.balance) { + l.warn("amount exceeds balance", { + total, + fee, + balance: account.balance + }); + throw new Error( + `Insufficient funds, need ${total} ${account.ticker}, have ${account.balance}` + ); + } + + account.balance -= total; + await account.save({ transaction }); + + let payment = { + amount: -amount, + account_id: account.id, + fee, + memo, + user_id: user.id, + rate: app.get("rates")[user.currency], + currency: user.currency, + address, + confirmed: true, + received: false, + network: "liquid" + }; + + payment.account = account; + payments.push(payment); + } } }); } catch (e) { @@ -91,7 +119,7 @@ module.exports = ah(async (req, res) => { } try { - await db.transaction(async (transaction) => { + await db.transaction(async transaction => { if (config.liquid.walletpass) await lq.walletPassphrase(config.liquid.walletpass, 300); @@ -102,12 +130,14 @@ module.exports = ah(async (req, res) => { let main; for (let i = 0; i < assets.length; i++) { p = payments[i]; - let { account } = p; - p.hash = txid; - p = await db.Payment.create(p, { transaction }); - if (account.ticker !== "BTC" || !main) { - main = p.get({ plain: true }); - main.account = account.get({ plain: true }); + if (p) { + let { account } = p; + p.hash = txid; + p = await db.Payment.create(p, { transaction }); + if (account.ticker !== "BTC" || !main) { + main = p.get({ plain: true }); + main.account = account.get({ plain: true }); + } } } -- cgit v1.2.3