aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2023-12-20 14:39:05 -0800
committerAdam Soltys <[email protected]>2023-12-20 14:39:08 -0800
commit44b5d463d8e32d9dd400ebb834ed2a877d93a639 (patch)
tree3e52a95fba04dc96dd1d1ab0e79e2d8f03564781
parentb67ca07bac3a0cb78f9b3c19a783c3253f06c29f (diff)
downloadcoinos-server-44b5d463d8e32d9dd400ebb834ed2a877d93a639.tar.gz
coinos-server-44b5d463d8e32d9dd400ebb834ed2a877d93a639.zip
calculate fee from inputs and outputs
-rw-r--r--index.js4
-rw-r--r--lib/payments.js16
-rw-r--r--routes/payments.js218
-rw-r--r--routes/users.js4
4 files changed, 68 insertions, 174 deletions
diff --git a/index.js b/index.js
index a6881cc5..55d05100 100644
--- a/index.js
+++ b/index.js
@@ -58,7 +58,6 @@ app.post("/parse", payments.parse);
app.get("/fund/:name", payments.fund);
app.get("/fund/:name/withdraw", payments.withdraw);
app.post("/take", auth, payments.take);
-app.post("/buy", auth, payments.buy);
app.post("/print", auth, payments.print);
app.post("/send/:lnaddress/:amount", auth, payments.lnaddress);
@@ -68,13 +67,11 @@ app.get("/lnurlp/:username", lnurl.lnurlp);
app.get("/lnurl/:id", lnurl.lnurl);
app.post("/freeze", payments.freeze);
-app.post("/fix", auth, payments.fix);
app.post("/bitcoin", payments.bitcoin);
app.post("/bitcoin/fee", auth, payments.fee);
app.post("/bitcoin/send", auth, payments.send);
app.get("/users", auth, users.list);
-app.get("/fix", auth, users.fix);
app.get("/me", auth, users.me);
app.get("/users/:key", users.get);
app.post("/register", users.create);
@@ -102,7 +99,6 @@ app.get("/requests", auth, requests.list);
app.post("/requests", auth, requests.create);
app.post("/requests/delete", auth, requests.destroy);
-app.post("/proxy", payments.proxy);
app.post("/shopify/:id", shopify);
let host = process.env.HOST || "0.0.0.0";
diff --git a/lib/payments.js b/lib/payments.js
index 20429864..d257ed77 100644
--- a/lib/payments.js
+++ b/lib/payments.js
@@ -25,7 +25,7 @@ export const types = {
classic: "classic",
};
-export let debit = async (
+export let debit = async ({
hash,
amount,
fee = 0,
@@ -33,12 +33,11 @@ export let debit = async (
user,
type = types.internal,
id = v4(),
-) => {
+ rate = store.rates[currency]
+}) => {
let ref;
let { id: uid, currency, username } = user;
- let rate = store.rates[currency];
-
let iid = await g(`invoice:${hash}`);
if (iid && iid.hash) iid = iid.hash;
let invoice = await g(`invoice:${iid}`);
@@ -259,7 +258,14 @@ export let payLightning = async (user, payreq, amount, maxfee, memo) => {
if (pays.find((p) => p.status === "pending"))
fail("Payment is already underway");
- p = await debit(payreq, total, maxfee, memo, user, types.lightning);
+ p = await debit({
+ hash: payreq,
+ amount: total,
+ fee: maxfee,
+ memo,
+ user,
+ type: types.lightning,
+ });
let check = async () => {
try {
diff --git a/routes/payments.js b/routes/payments.js
index 2b332221..328e4d80 100644
--- a/routes/payments.js
+++ b/routes/payments.js
@@ -55,7 +55,13 @@ export default {
if (username && username.endsWith("@classic")) {
let { username: source } = user;
username = username.replace("@classic", "");
- p = await debit(hash, amount, 0, username, user, types.classic);
+ p = await debit({
+ hash,
+ amount,
+ memo: username,
+ user,
+ type: types.classic,
+ });
await got
.post(`${config.classic}/admin/credit`, {
@@ -69,10 +75,10 @@ export default {
if (!p) {
if (hash) {
- p = await debit(hash, amount, 0, memo, user);
+ p = await debit({ hash, amount, memo, user });
await credit(hash, amount, memo, user.id);
} else if (fund) {
- p = await debit(hash, amount, 0, fund, user, types.fund);
+ p = await debit({ hash, amount, memo: fund, user, type: types.fund });
await db.incrBy(`fund:${fund}`, amount);
await db.lPush(`fund:${fund}:payments`, p.id);
l("funded fund", fund);
@@ -251,45 +257,56 @@ export default {
}
},
- async fee({ body: { amount, address, feeRate, subtract }, user }, res) {
+ async fee({ body: { amount, address, feeRate: fee_rate }, user }, res) {
try {
+ amount = parseInt(amount);
+
+ let replaceable = true;
let ourfee = Math.round(amount * config.fee);
let credit = await g(`credit:bitcoin:${user.id}`);
let covered = Math.min(credit, ourfee) || 0;
ourfee -= covered;
- if (subtract) amount -= ourfee;
-
let outs = [{ [address]: btc(amount) }];
let raw = await bc.createRawTransaction([], outs, 0, replaceable);
let tx = await bc.fundRawTransaction(raw, {
- replaceable: true,
+ fee_rate,
+ replaceable,
+ subtractFeeFromOutputs: [],
});
let fee = sats(tx.fee);
+ let dust = 547;
+
+ let balance = await g(`balance:${user.id}`);
+
+ if (amount + fee + ourfee > balance) {
+ if (amount <= fee + ourfee + dust)
+ return bail(res, "insufficient funds");
+
+ outs = [{ [address]: btc(amount - ourfee) }];
+ raw = await bc.createRawTransaction([], outs, 0, replaceable);
- if (amount + fee + ourfee > (await g(`balance:${user.id}`))) {
tx = await bc.fundRawTransaction(raw, {
- replaceable: true,
+ fee_rate,
+ replaceable,
subtractFeeFromOutputs: [0],
});
+
+ fee = sats(tx.fee);
}
- res.send({ feeRate, ourfee, fee, tx });
+ res.send({ feeRate: fee_rate, ourfee, fee, hex: tx.hex });
} catch (e) {
warn("problem estimating fee", e.message, user.username, amount, address);
bail(res, "problem estimating fee");
}
},
- async send(req, res) {
+ async send({ body: { address, memo, hex, rate, pin }, user }, res) {
try {
- await requirePin(req);
-
- let { user } = req;
- let { address, memo, tx, subtract } = req.body;
- let { hex, fee } = tx;
+ await requirePin({ body: { pin }, user });
if (config.bitcoin.walletpass)
await bc.walletPassphrase(config.bitcoin.walletpass, 300);
@@ -299,31 +316,42 @@ export default {
let r = await bc.testMempoolAccept([hex]);
if (!r[0].allowed) fail("transaction rejected");
- fee = sats(fee);
- if (fee < 0) fail("fee cannot be negative");
-
- tx = await bc.decodeRawTransaction(hex);
+ let tx = await bc.decodeRawTransaction(hex);
let { txid } = tx;
let total = 0;
let change = 0;
+ let totalIn = 0;
+ for await (let { txid, vout } of tx.vin) {
+ let hex = await bc.getRawTransaction(txid);
+ let tx = await bc.decodeRawTransaction(hex);
+ totalIn += sats(tx.vout[vout].value);
+ }
+
for (let {
scriptPubKey: { address },
value,
} of tx.vout) {
total += sats(value);
- let iid = await g(`invoice:${address}`);
- if ((await bc.getAddressInfo(address)).ismine && !iid)
- change += sats(value);
- }
+ if (await g(`invoice:${address}`))
+ fail("Cannot send to internal address");
- total = total - change + fee;
- let amount = total - fee;
+ if ((await bc.getAddressInfo(address)).ismine) change += sats(value);
+ }
- if (change && !amount) fail("Cannot send to unregistered coinos address");
+ let fee = totalIn - total;
+ let amount = total - change;
- await debit(txid, amount, fee, null, user, types.bitcoin, txid);
+ await debit({
+ hash: txid,
+ amount,
+ fee,
+ rate,
+ user,
+ type: types.bitcoin,
+ id: txid,
+ });
await bc.sendRawTransaction(hex);
res.send({ txid });
@@ -333,138 +361,6 @@ export default {
}
},
- async buy({ body: { amount, number, year, month, cvc }, user }, res) {
- if (!user.eligible) fail("not eligible");
-
- let stripe = "https://api.stripe.com/v1";
- let { stripe: username } = config;
-
- let form = {
- "card[number]": number,
- "card[exp_month]": month,
- "card[exp_year]": year,
- "card[cvc]": cvc,
- };
-
- let { id: source } = await got
- .post(`${stripe}/tokens`, {
- form,
- username,
- })
- .json();
-
- let currency = "CAD";
- form = {
- amount,
- currency,
- source,
- description: "starter coupon",
- };
-
- let r = await got
- .post(`${stripe}/charges`, {
- form,
- username,
- })
- .json();
-
- let { status } = r;
- if (status === "succeeded") {
- let id = v4();
- let hash = r.id;
- let memo = "stripe";
- await s(`invoice:${id}`, {
- id,
- hash: id,
- uid: user.id,
- received: 0,
- });
- amount = sats(amount / store.rates[currency]);
- let uid = await g("user:coinos");
- let coinos = await g(`user:${uid}`);
- let p = await debit(hash, amount, 0, memo, coinos);
- await credit(hash, amount, memo, coinos.id);
- } else fail("Card payment failed");
-
- res.send({ status });
- },
-
- async proxy({ body: { method, params } }, res) {
- try {
- let whitelist = [
- "getblock",
- "getblockhash",
- "estimatesmartfee",
- "echo",
- "getblockchaininfo",
- "getnetworkinfo",
- ];
-
- if (!whitelist.includes(method)) fail("unsupported method");
-
- if (method === "estimatesmartfee" || method === "getblockhash")
- params[0] = parseInt(params[0]);
-
- if (method === "getblock") params[1] = parseInt(params[1]);
-
- let result = await bc[method](...params);
-
- if (result.feerate) result.feerate = result.feerate.toFixed(8);
-
- res.send(result);
- } catch (e) {
- bail(res, e.message);
- }
- },
-
- async fix({ body: { uid, username }, user }, res) {
- try {
- if (!user.admin) fail("unauthorized");
-
- for await (let k of db.scanIterator({ MATCH: "balance:*" })) {
- let bal = await g(k);
- if (bal < 0) s(k, 0);
- }
-
- for await (let k of db.scanIterator({ MATCH: "*:lastlen" })) {
- await db.del(k);
- }
-
- for await (let k of db.scanIterator({ MATCH: "user:*" })) {
- let u = await g(k);
- let id = k.split(":")[1];
-
- if (typeof u === "string") {
- id = u;
- u = await g(`user:${u}`);
- }
-
- if (!(u && u.username)) continue;
-
- let touched;
-
- console.log(u.username, u.profile, u.banner);
-
- if (u.profile) {
- u.profile = `${u.id}-profile`;
- touched = true;
- }
-
- if (u.banner) {
- u.banner = `${u.id}-banner`;
- touched = true;
- }
-
- if (touched) await s(`user:${u.id}`, u);
- }
-
- res.send("ok");
- } catch (e) {
- console.log(e);
- bail(res, e.message);
- }
- },
-
async freeze({ body: { secret } }, res) {
try {
if (secret !== config.adminpass) fail("unauthorized");
@@ -521,7 +417,7 @@ export default {
let p = await payLightning(user, pr, amount, maxfee, memo);
if (!p) {
- p = await debit(pr, amount, 0, memo, user);
+ p = await debit({ hash: pr, amount, memo, user });
await credit(pr, amount, memo, user.id);
}
diff --git a/routes/users.js b/routes/users.js
index 432908e9..a0796d63 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -399,10 +399,6 @@ export default {
res.send({});
},
- async fix(req, res) {
- res.send(req.headers);
- },
-
async reset({ body: { code, username, password }, user: u }, res) {
let admin = u && u.admin;
let id, user;