cln-ledger

CLN ledger accounting
git clone git://jb55.com/cln-ledger
Log | Files | Refs

index.js (8281B)


      1 var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
      2     function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
      3     return new (P || (P = Promise))(function (resolve, reject) {
      4         function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
      5         function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
      6         function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
      7         step((generator = generator.apply(thisArg, _arguments || [])).next());
      8     });
      9 };
     10 var __asyncValues = (this && this.__asyncValues) || function (o) {
     11     if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
     12     var m = o[Symbol.asyncIterator], i;
     13     return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
     14     function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
     15     function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
     16 };
     17 import fs from 'fs';
     18 import readline from 'readline';
     19 import assert from 'assert';
     20 function description_string(desc) {
     21     switch (desc.type) {
     22         case "generic":
     23             return desc.value;
     24         case "lnurl":
     25             return desc.description;
     26         case "zap":
     27             return "Zap";
     28     }
     29 }
     30 function determine_zap_type(zap) {
     31     const etag = get_tag(zap.tags, "e");
     32     const ptag = get_tag(zap.tags, "p");
     33     if (!ptag)
     34         return null;
     35     if (!etag) {
     36         let profile_zap = { pubkey: ptag, type: "profile" };
     37         return profile_zap;
     38     }
     39     let zap_ev = { evid: etag, pubkey: ptag, type: "event" };
     40     return zap_ev;
     41 }
     42 function get_tag(tags, key) {
     43     let v = tags.find(tag => tag[0] === key);
     44     return v && v[1];
     45 }
     46 function get_lnurl_description(json) {
     47     const description = get_tag(json, "text/plain");
     48     const address = get_tag(json, "text/identifier");
     49     return { description, address, type: "lnurl" };
     50 }
     51 function is_json(str) {
     52     return str[0] === '{' || (str[0] === '[' && str[1] === '[');
     53 }
     54 function create_generic_desc(value) {
     55     return { type: "generic", value };
     56 }
     57 function determine_description(str, tag) {
     58     if (tag === "routed") {
     59         let desc = { type: "routed" };
     60         return desc;
     61     }
     62     try {
     63         let json;
     64         if (is_json(str) && (json = JSON.parse(str))) {
     65             if (json.kind === 9734) {
     66                 let zaptype = determine_zap_type(json);
     67                 let zap_desc = { zap_type: zaptype, zap: json, type: "zap" };
     68                 return zap_desc;
     69             }
     70             else if (json.length > 0) {
     71                 return get_lnurl_description(json);
     72             }
     73             return create_generic_desc(str);
     74         }
     75         return create_generic_desc(str);
     76     }
     77     catch (e) {
     78         return create_generic_desc(str);
     79     }
     80 }
     81 function msat(val) {
     82     return `${val} msat`;
     83 }
     84 function classify_account(str, debit) {
     85     if (str.includes("Damus Merch"))
     86         return "merch:tshirt";
     87     if (str.includes("Damus Hat"))
     88         return "merch:hat";
     89     if (str.includes("@tipjar"))
     90         return "lnurl:jb55@sendsats.lol:tipjar";
     91     if (debit === 1971000)
     92         return "zap:1971";
     93     else if (debit === 420000)
     94         return "zap:420";
     95     return "unknown";
     96 }
     97 function determine_postings(credit, debit, desc) {
     98     let postings = [];
     99     const is_credit = credit > 0;
    100     const acct = is_credit ? "income" : "expenses";
    101     const amount = is_credit ? credit : debit;
    102     switch (desc.type) {
    103         case "routed":
    104             postings.push({ account: `${acct}:routed`, amount: msat(is_credit ? -amount : amount) });
    105             postings.push({ account: `assets:cln`, amount: msat(is_credit ? amount : -amount) });
    106             break;
    107         case "generic":
    108             // todo: categorize
    109             const subacct = classify_account(desc.value, debit);
    110             postings.push({ account: `${acct}:${subacct}`, amount: msat(is_credit ? -amount : amount) });
    111             postings.push({ account: `assets:cln`, amount: msat(is_credit ? amount : -amount) });
    112             break;
    113         case "lnurl":
    114             assert(debit === 0);
    115             postings.push({ account: `income:lnurl:${desc.address}`, amount: msat(-credit) });
    116             postings.push({ account: `assets:cln`, amount: msat(credit) });
    117             break;
    118         case "zap":
    119             assert(debit === 0);
    120             switch (desc.zap_type.type) {
    121                 case 'profile':
    122                     postings.push({ account: `income:zap:profile:${desc.zap_type.pubkey}`, amount: msat(-credit) });
    123                     break;
    124                 case 'event':
    125                     // todo: attribute this to profile as well?
    126                     let evtype = desc.zap_type;
    127                     postings.push({ account: `income:zap:event:${evtype.evid}`, amount: msat(-credit) });
    128                     break;
    129             }
    130             postings.push({ account: `assets:cln`, amount: msat(credit) });
    131             break;
    132     }
    133     return postings;
    134 }
    135 function process(filePath) {
    136     var _a, e_1, _b, _c;
    137     return __awaiter(this, void 0, void 0, function* () {
    138         const fileStream = fs.createReadStream(filePath);
    139         const rl = readline.createInterface({
    140             input: fileStream,
    141             crlfDelay: Infinity
    142         });
    143         console.log("P 2022-07-22 msat 0.0000004 CAD");
    144         console.log("P 2022-07-22 msat 0.001 sat");
    145         console.log("P 2022-07-22 msat 0.00001 bit");
    146         console.log("P 2022-07-22 msat 0.00000000001 btc");
    147         try {
    148             for (var _d = true, rl_1 = __asyncValues(rl), rl_1_1; rl_1_1 = yield rl_1.next(), _a = rl_1_1.done, !_a;) {
    149                 _c = rl_1_1.value;
    150                 _d = false;
    151                 try {
    152                     const line = _c;
    153                     let parts = line.split('\t');
    154                     if (parts.length !== 6) {
    155                         console.error(`Invalid line: ${line} ${parts.length} !== 5`);
    156                         continue;
    157                     }
    158                     const credit = Number(parts[2]);
    159                     const debit = Number(parts[3]);
    160                     if (credit === 0 && debit === 0)
    161                         continue;
    162                     const tag = parts[1];
    163                     if (!(tag === "invoice" || tag === "routed"))
    164                         continue;
    165                     const timestamp = Number(parts[4]);
    166                     const date = new Date(timestamp * 1000);
    167                     const description = determine_description(parts[5], tag);
    168                     const postings = determine_postings(credit, debit, description);
    169                     // type,.tag,.credit_msat,.debit_msat,.timestamp,.description
    170                     let transaction = { date, description, postings };
    171                     console.log(transactionToLedger(transaction));
    172                 }
    173                 finally {
    174                     _d = true;
    175                 }
    176             }
    177         }
    178         catch (e_1_1) { e_1 = { error: e_1_1 }; }
    179         finally {
    180             try {
    181                 if (!_d && !_a && (_b = rl_1.return)) yield _b.call(rl_1);
    182             }
    183             finally { if (e_1) throw e_1.error; }
    184         }
    185     });
    186 }
    187 function format_date(date) {
    188     const year = date.getFullYear();
    189     const month = (date.getMonth() + 1).toString().padStart(2, '0');
    190     const day = date.getDate().toString().padStart(2, '0');
    191     return `${year}-${month}-${day}`;
    192 }
    193 function transactionToLedger(transaction) {
    194     const tx = `${format_date(transaction.date)}	${description_string(transaction.description)}\n`;
    195     return tx + transaction.postings.map(p => `	${p.account}	${p.amount}`).join("\n") + "\n";
    196 }
    197 function main() {
    198     return __awaiter(this, void 0, void 0, function* () {
    199         yield process('events.txt');
    200     });
    201 }
    202 main().catch(console.error);
    203 //# sourceMappingURL=index.js.map