aboutsummaryrefslogtreecommitdiffstats
path: root/tools/history-export-test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-22 09:23:47 -0700
committerAdam Soltys <[email protected]>2026-10-22 09:23:47 -0700
commit178842f26f9fce19e9148cbb9f0e9f1aed9131f7 (patch)
tree60fc5a576345b5b1c31de8fff4d64a4f71a14863 /tools/history-export-test.js
parentf3a3d88c7d5a86ae1fa06cf23932631eaa050151 (diff)
downloadcoinosv3-178842f26f9fce19e9148cbb9f0e9f1aed9131f7.tar.gz
coinosv3-178842f26f9fce19e9148cbb9f0e9f1aed9131f7.zip
Videos on a desktop: moving the mouse over a playing clip brings the seek bar up, and it fades about 2 s after the mouse goes still (it stays while the pointer rests on the bar)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'tools/history-export-test.js')
0 files changed, 0 insertions, 0 deletions
me the previous revision' href='/coinos-server.git/blame/routes/lnurl.js?id=3c46dc86b753cdfd71da7b8bb86710032668b5eb'>^
cd33441e ^

08253d0d ^
cd33441e ^
d6af2106 ^


55d47631 ^

488bc726 ^

96dff083 ^




2bcd5e9c ^





488bc726 ^

2bcd5e9c ^





















488bc726 ^
2bcd5e9c ^


79c9febb ^
2bcd5e9c ^



f15a6679 ^
2bcd5e9c ^







3980fec0 ^


488bc726 ^

3980fec0 ^
2bcd5e9c ^


88da47b2 ^
2bcd5e9c ^











488bc726 ^
2bcd5e9c ^
8b06e5a7 ^
2bcd5e9c ^









488bc726 ^
9306393f ^

2bcd5e9c ^










321ce847 ^

cd33441e ^






321ce847 ^
cd33441e ^





321ce847 ^
321ce847 ^



















2bcd5e9c ^




488bc726 ^

f15a6679 ^

2bcd5e9c ^




2bcd5e9c ^



488bc726 ^
2bcd5e9c ^
4b28676d ^
2bcd5e9c ^





d6af2106 ^
2bcd5e9c ^









681e2e6f ^
488bc726 ^
2bcd5e9c ^

92e913ba ^

2bcd5e9c ^



98eceb8d ^
2bcd5e9c ^
92e913ba ^




















2bcd5e9c ^

2bcd5e9c ^














d6af2106 ^
2bcd5e9c ^



3dd0d83f ^
2bcd5e9c ^



544cb801 ^

cced852e ^


544cb801 ^



2bcd5e9c ^
















488bc726 ^
2bcd5e9c ^

3dd0d83f ^
2bcd5e9c ^







d6af2106 ^
544cb801 ^


1ffed1ea ^



















9306393f ^


cd33441e ^


c405440c ^
cd33441e ^



98eceb8d ^
843a7455 ^
98eceb8d ^
cd33441e ^

98eceb8d ^
cd33441e ^


2bcd5e9c ^

cd33441e ^

2bcd5e9c ^
cd33441e ^
cd33441e ^
096ea59d ^
cd33441e ^

9306393f ^
cd33441e ^



52ca1e9f ^
cd33441e ^




9f29f46b ^
cd33441e ^


















9306393f ^
cd33441e ^

9306393f ^
cd33441e ^
c602ce9e ^

cd33441e ^





9306393f ^
cd33441e ^


55d47631 ^
9306393f ^
cd33441e ^








9306393f ^
55d47631 ^














55d47631 ^






51ed4347 ^





55d47631 ^
















cd33441e ^
d6af2106 ^
cd33441e ^








55d47631 ^
4f45df1e ^
cd33441e ^


9306393f ^
cd33441e ^























9306393f ^
cd33441e ^
9306393f ^
55d47631 ^
cd33441e ^
9306393f ^
cd33441e ^

9306393f ^
cd33441e ^






9306393f ^
9306393f ^
cd33441e ^



9306393f ^


488bc726 ^
9306393f ^
17b85b21 ^

d6af2106 ^

488bc726 ^
d6af2106 ^


9f29f46b ^
9306393f ^
d6af2106 ^
488bc726 ^
d6af2106 ^

9f29f46b ^
17b85b21 ^
a28cdca2 ^

488bc726 ^

9f29f46b ^
d6af2106 ^
17b85b21 ^



9f29f46b ^


071a97a7 ^
488bc726 ^
9f29f46b ^

9306393f ^
d6af2106 ^



9306393f ^
d6af2106 ^
071a97a7 ^
d6af2106 ^

9f29f46b ^
d6af2106 ^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566


                                    
                                   
                                          
                                 
 

                                             
                                              
                                               


                                               

                                                                                            

                                                                              




                     





                                          

                              





















                                                                          
                                            


                                            
 



                                                             
     







                                         


                                               

                       
         


                     
                        











                                      
                              
                 
                                                    









                                                
                        

       










                                               

                                






                                        
 





                                                                      
 



















                                                                             




                                      

                

       




                                                                    



                                                                     
                                                                             
         
 





                                                           
     









                                
              
                              

                 

                                                                                



                                             
                                      
 




















                                                              

                                                   














                                                               
 



                                                         
     



        

                                


                                                             



        
















                                                               
                          

           
 







                                                    
 


                                                                     



















                                                        


                                               


                                                            
 



                                                             
                                            
 
                      

                                                  
                      


                                            

             

                                                  
           
         
       
 

                                  
 



                                               
               




                               
 


















                                                                             
 

                                
 
                                       

       





                                                            
 


                                                                
                                                           
 








                                                     
 














                                                            






                                                                                                      





                                                                       
















                                                                                           
             
 








                                                      
                                     
                               


                             
 























                                                                               
               
                     
 
                                                                                            
                                                  
 

                                                     
 






                                                             
       
 



                                                  


   
                                              
       

                                                       

                                         
                                               


             
                                                     
                                       
                                    
                           

         
                 
                                                        

                             

                    
           
 



                                           


                                 
                                     
                       

           
                                           



                                                  
                                          
                        
                                                                         

               
                                                  

   
const axios = require("axios");
const lnurl = require("lnurl");
const jwt = require("jsonwebtoken");
const qs = require("query-string");
const persist = require("../lib/persist");
const bolt11 = require("bolt11");

logins = persist("data/logins.json");
recipients = persist("data/recipients.json");
lnurlPayments = persist("data/payments.json");
withdrawals = persist("data/withdrawals.json");

lnurlServer = lnurl.createServer(config.lnurl);

let { computeConversionFee, conversionFeeReceiver } = require('lightning/conversionFee.js');

var optionalAuth = function(req, res, next) {
  passport.authenticate("jwt", { session: false }, function(err, user, info) {
    req.user = user;
    next();
  })(req, res, next);
};

app.get(
  "/url",
  ah(async (req, res, next) => {
    try {
      const code = await db.Code.findOne({
        where: {
          code: req.query.code
        }
      });

      if (code) {
        res.send(code.text);
      } else {
        throw new Error("code not found");
      }
    } catch (e) {
      l.error("couldn't find url", e.message);
    }
  })
);

app.get(
  "/withdraw",
  auth,
  ah(async (req, res, next) => {
    const { min, max } = req.query;
    try {
      const result = await lnurlServer.generateNewUrl("withdrawRequest", {
        minWithdrawable: min * 1000,
        maxWithdrawable: max * 1000,
        defaultDescription: "coinos voucher"
      });

      withdrawals[result.secret] = req.user;

      res.send(result);
    } catch (e) {
      l.error("problem generating withdrawl url", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.post(
  "/code",
  ah(async (req, res, next) => {
    try {
      const { encoded } = req.body.lnurl;
      const code = await db.Code.findOrCreate({
        where: {
          code: `lnurl:${encoded.substr(-32)}`,
          text: encoded
        }
      });

      res.send(code);
    } catch (e) {
      l.error(e.errors);
      res.status(500).send(e.message);
    }
  })
);

app.post(
  "/withdraw",
  auth,
  ah(async (req, res, next) => {
    const { user } = req;
    const {
      amount: value,
      params: { callback, k1 }
    } = req.body;
    const invoice = await lnp.addInvoice({ value });
    const { payment_request: pr } = invoice;
    const url = `${callback}?k1=${k1}&pr=${pr}`;

    await db.Invoice.create({
      user_id: user.id,
      text: pr,
      rate: app.get("rates")[user.currency],
      currency: user.currency,
      amount: value,
      tip: 0,
      network: "bitcoin"
    });

    try {
      result = (await axios.get(url)).data;
      res.send(result);
    } catch (e) {
      l.error("failed to withdraw", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.get(
  "/lnurlp/:username",
  ah(async (req, res, next) => {
    try {
      const { username } = req.params;
      let user = await db.User.findOne({
        where: {
          username
        }
      });

      if (!user) throw new Error("user not found");

      let { amount, minSendable, maxSendable } = req.query;
      minSendable = minSendable || 1000;
      maxSendable = maxSendable || 1000000000;
      if (parseInt(amount)) minSendable = maxSendable = amount * 1000;

      let result = await lnurlServer.generateNewUrl("payRequest", {
        minSendable,
        maxSendable,
        metadata: JSON.stringify([["text/plain", `paying ${user.username}`]])
      });

      recipients[result.secret] = user;
      l.info("recipient", user.username, result.secret);

      result = await axios.get(result.url);

      res.send(result.data);
    } catch (e) {
      l.error("problem generating payment url", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.get(
  "/pay/:username",
  ah(async (req, res, next) => {
    const { username } = req.params;
    let user = await db.User.findOne({
      where: {
        username
      }
    });

    let { amount, minSendable, maxSendable } = req.query;
    minSendable = minSendable || 1000;
    maxSendable = maxSendable || 1000000000;
    if (parseInt(amount)) minSendable = maxSendable = amount * 1000;

    try {
      const result = await lnurlServer.generateNewUrl("payRequest", {
        minSendable,
        maxSendable,
        metadata: JSON.stringify([["text/plain", `paying ${user.username}`]])
      });

      recipients[result.secret] = user;
      l.info("recipient", user.username, result.secret);
      res.send(result);
    } catch (e) {
      l.error("problem generating payment url", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.post(
  "/pay",
  auth,
  ah(async (req, res, next) => {
    const { user } = req;
    const {
      amount,
      comment,
      params: { callback, k1 }
    } = req.body;

    let url = `${callback}${callback.includes("?") ? "&" : "?"}amount=${amount *
      1000}${comment ? "&comment=" + comment : ""}`;

    try {
      const parts = callback.split("/");
      const secret = parts[parts.length - 1];
      lnurlPayments[secret] = user.id;

      if (recipients[secret]) {
        url = `${req.protocol}://${req.get("host")}/api/send`;
        const { data } = await axios.post(
          url,
          {
            amount,
            memo: comment,
            username: recipients[secret].username
          },
          {
            headers: {
              Authorization: req.get("Authorization")
            }
          }
        );

        res.send(data);
      } else {
        const { data } = await axios.get(url);
        res.send(await send(amount, "", data.pr, user));
      }
    } catch (e) {
      l.error("failed to send payment", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.get(
  "/login",
  optionalAuth,
  ah(async (req, res, next) => {
    try {
      const result = await lnurlServer.generateNewUrl("login");

      if (req.user) {
        logins[result.secret] = req.user.username;
      }

      res.send(result);
    } catch (e) {
      l.error("problem generating login url", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.get(
  "/encode",
  ah(async (req, res, next) => {
    let { domain, name } = req.query;
    let url = `https://${domain}/.well-known/lnurlp/${name}`;
    res.send(lnurl.encode(url));
  })
);

app.get(
  "/decode",
  ah(async (req, res, next) => {
    const { text } = req.query;

    try {
      const url = lnurl.decode(text);
      let spl = url.split("?");
      if (spl.length > 1 && qs.parse(spl[1]).tag === "login") {
        return res.send({
          tag: "login",
          k1: qs.parse(spl[1]).k1,
          callback: url,
          domain: url
            .split("://")[1]
            .split("/")[0]
            .split("@")
            .slice(-1)[0]
            .split(":")[0]
        });
      }

      const { data: params } = await axios.get(url);
      res.send(params);
    } catch (e) {
      l.error("problem decoding lnurl", e.message);
      res.status(500).send(e.message);
    }
  })
);

lnurlServer.on("payRequest:action:processed", async function(event) {
  const { secret, params, result } = event;
  const { id, invoice } = result;
  const recipient = recipients[secret];
  let payreq = bolt11.decode(invoice);

  try {
    let i = await db.Invoice.create({
      user_id: recipient.id,
      text: invoice,
      rate: app.get("rates")[recipient.currency],
      currency: recipient.currency,
      amount: payreq.satoshis,
      tip: 0,
      network: "lightning"
    });

    l.info("invoice created", i.text, i.amount);
  } catch (e) {
    l.error("problem finding lnurl invoice", e.message);
  }
});

lnurlServer.bindToHook(
  "middleware:signedLnurl:afterCheckSignature",
  async (req, res, next) => {
    try {
      let user;
      const { amount: msats, key, tag, pr, k1 } = req.query;

      if (msats) {
        amount = Math.round(msats / 1000);
        const parts = req.originalUrl.split("/");
        const secret = parts[parts.length - 1].split("?")[0];
        let user_id = lnurlPayments[secret];

        if (user_id) {
          let account = await db.Account.findOne({
            where: {
              user_id,
              asset: config.liquid.btcasset,
              pubkey: null
            }
          });

          if (account.balance < amount) {
            throw new Error("Insufficient funds");
          }
        }
      }

      if (tag === "login") {
        let username = logins[k1];

        if (!username) {
          const keyObj = await db.Key.findOne({
            where: { hex: key },
            include: [
              {
                model: db.User,
                as: "user"
              }
            ]
          });

          if (keyObj) ({ user } = keyObj);

          if (!user) {
            let ip =
              req.headers["x-forwarded-for"] || req.connection.remoteAddress;

            let username = `satoshi-${key.substr(0, 12)}`;
            let existing = await db.User.findOne({ where: { username } });
            if (existing) user = existing;
            else {
              user = await register(
                {
                  username,
                  password: key
                },
                ip
              );
            }
          }

          ({ username } = user);
        }

        logins[key] = { k1, username };
      }

      if (pr) {
        user = withdrawals[k1];
        if (!user) {
          if (next) next(new Error("withdrawal not found"));
          return;
        }

        try {
          let decoded = await lnp.decodePayReq({ pay_req: pr });
          let amount = decoded.num_satoshis;
          let conversionFee = computeConversionFee(amount);

          await db.transaction(async transaction => {
            let account = await db.Account.findOne({
              where: {
                user_id: user.id,
                asset: config.liquid.btcasset
              },
              lock: transaction.LOCK.UPDATE,
              transaction
            });

            // account that receives conversion fees
            let receiverAccount = await db.Account.findOne({
              where: {
                "$user.username$": conversionFeeReceiver
              },
              include: [
                {
                  model: db.User,
                  as: "user"
                },
              ],
              lock: transaction.LOCK.UPDATE,
              transaction
            });

            let conversionFeeDeduction = Math.min(account.lightning_credits, conversionFee);
            if (conversionFeeDeduction) {
              await account.decrement({ lightning_credits: conversionFeeDeduction }, { transaction });
              await account.reload({ transaction });
              conversionFee -= conversionFeeDeduction;
            }

            if (account.balance < amount) {
              throw new Error("Insufficient funds");
            } else if (account.balance < amount + conversionFee) {
              throw new Error("Insufficient funds for conversion fee");
            }

            let fee_payment_id = null;
            if (conversionFee) {
              await receiverAccount.increment({ balance: conversionFee }, { transaction });
              await receiverAccount.reload({ transaction });
              let fee_payment = await db.Payment.create({
                amount: conversionFee,
                fee: 0,
                memo: "Bitcoin conversion fee",
                account_id: receiverAccount.id,
                user_id: receiverAccount.user_id,
                rate: app.get("rates")[receiverAccount.user.currency],
                currency: receiverAccount.user.currency,
                confirmed: true,
                received: true,
                network: "COINOS"
              }, { transaction });
              fee_payment_id = fee_payment.id;
            }

            let payment = await db.Payment.create(
              {
                amount: -amount,
                account_id: account.id,
                user_id: user.id,
                hash: pr,
                rate: app.get("rates")[user.currency],
                currency: user.currency,
                confirmed: true,
                network: "lightning",
                fee_payment_id,
              },
              { transaction }
            );

            setTimeout(async () => {
              try {
                let { payments } = await lnp.listPayments({
                  include_incomplete: false,
                  max_payments: 5,
                  reversed: true
                });

                let p = payments.find(p => p.payment_request === pr);
                if (p) {
                  l.info("found payment", pr);
                  payment.fee = p.fee;
                  await account.decrement({ balance: p.fee }, { transaction });
                  await account.reload({ transaction });
                  await payment.save();
                  payment = payment.get({ plain: true });
                  payment.account = account.get({ plain: true });
                  emit(user.username, "account", account);
                  emit(user.username, "payment", payment);
                } else {
                  l.warn("payment not found, fee not set", pr);
                }
              } catch (e) {
                l.error("problem trying to get ln withdrawal payment", e);
              }
            }, 1000);

            await account.decrement({ balance: (amount + conversionFee) }, { transaction });
            await account.reload({ transaction });

            let p = payment.get({ plain: true });
            p.account = account.get({ plain: true });

            emit(user.username, "account", p.account);
            emit(user.username, "payment", p);
          });
        } catch (e) {
          l.error("failed to process withdrawal", e.message);
          return next(e);
        }
      }

      if (next) next(req, res);
    } catch (e) {
      l.error("unhandled lnurl error", e.message);
    }
  }
);

lnurlServer.bindToHook("login", async key => {
  try {
    if (!key) throw new Error("login key not defined");

    const exists = await db.Key.findOne({
      where: { hex: key },
      include: [{ model: db.User, as: "user" }]
    });

    let user;
    if (logins[key] && logins[key] !== "undefined") {
      const { username } = logins[key];
      user = await db.User.findOne({
        where: { username }
      });

      if (user) {
        const [k, created] = await db.Key.findOrCreate({
          where: {
            user_id: user.id,
            hex: key
          }
        });

        if (created) {
          l.info("added key", username, k);
          emit(username, "key", k);
        }
      } else {
        l.info("user not found");
        user = await register({
          username: key.substr(0, 8),
          password: key
        });
      }
    } else if (exists) ({ user } = exists);

    if (user && user.username) {
      const payload = { username: user.username };
      const token = jwt.sign(payload, config.jwt);
      const ws = sessions[logins[key].k1];
      if (ws && ws.send)
        ws.send(JSON.stringify({ type: "token", data: { token, key } }));
    }
  } catch (e) {
    l.error("problem with login hook", e.message);
  }
});