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