@forgesworn/moneyer 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/store.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import { sha256 } from '@noble/hashes/sha2.js';
3
3
  import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
4
+ import { bearerNoteId } from "./spend.js";
4
5
  // What identifies one mutation request, so a retry of it can be answered
5
6
  // with the same reply instead of "already spent". Everything the WALLET
6
7
  // chose goes in: the notes it named (by id, never by secret - the store
@@ -123,6 +124,16 @@ export class NoteStore {
123
124
  if (!nameColumns.includes('next_index')) {
124
125
  this.db.exec('ALTER TABLE zap_names ADD COLUMN next_index INTEGER NOT NULL DEFAULT 0');
125
126
  }
127
+ if (!invoiceColumns.includes('output_named')) {
128
+ this.db.exec('ALTER TABLE mint_invoices ADD COLUMN output_named TEXT');
129
+ }
130
+ // The Q of the bearer note the invoice's payment preimage would open.
131
+ // That preimage reaches the funding source, every hop, and /verify, so
132
+ // no note may ever be credited there. See outputIdInUse.
133
+ if (!invoiceColumns.includes('payment_q')) {
134
+ this.db.exec('ALTER TABLE mint_invoices ADD COLUMN payment_q TEXT');
135
+ }
136
+ this.db.exec('CREATE INDEX IF NOT EXISTS mint_invoices_payment_q ON mint_invoices (payment_q)');
126
137
  this.db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);');
127
138
  // The moment this mint stopped publishing preimages for notes nobody
128
139
  // named. Written once, the first time a build carrying this code opens
@@ -138,6 +149,66 @@ export class NoteStore {
138
149
  this.db
139
150
  .prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('unnamed_verify_cutover', ?)")
140
151
  .run(String(Date.now()));
152
+ this.indexLegacyIds();
153
+ // Lightning Address payments now land on their own purpose of a name's
154
+ // branch, a counter that starts at 0. The indices on file counted the
155
+ // old single ladder, so restart them once rather than open a gap a
156
+ // wallet's scan could stop short of.
157
+ this.tx(() => {
158
+ const moved = this.db
159
+ .prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('address_purpose_counter', ?)")
160
+ .run(String(Date.now()));
161
+ if (moved.changes > 0)
162
+ this.db.exec('UPDATE zap_names SET next_index = 0');
163
+ });
164
+ }
165
+ // Once per database: the Q each id written before notes were keyed by Q
166
+ // would have as a bearer note (see the top of this file). Every note row,
167
+ // every note an invoice was told to credit, and every payment hash that
168
+ // is itself a note's id. Rows written after this run are keyed by Q
169
+ // already, apart from notes still to land from invoices indexed here.
170
+ indexLegacyIds() {
171
+ this.db.exec('CREATE TABLE IF NOT EXISTS legacy_ids (q TEXT PRIMARY KEY, id TEXT NOT NULL)');
172
+ const done = this.db.prepare("SELECT 1 FROM meta WHERE key = 'legacy_ids_indexed'").get();
173
+ if (done)
174
+ return;
175
+ this.tx(() => {
176
+ const ids = this.db
177
+ .prepare(`SELECT id FROM notes
178
+ UNION SELECT output_id FROM mint_invoices WHERE output_id IS NOT NULL
179
+ UNION SELECT payment_hash FROM mint_invoices WHERE output_id IS NULL`)
180
+ .all();
181
+ const insert = this.db.prepare('INSERT OR IGNORE INTO legacy_ids (q, id) VALUES (?, ?)');
182
+ for (const { id } of ids) {
183
+ if (/^[0-9a-f]{64}$/.test(id))
184
+ insert.run(bearerNoteId(id), id);
185
+ }
186
+ const unmapped = this.db.prepare('SELECT payment_hash FROM mint_invoices WHERE payment_q IS NULL').all();
187
+ const setQ = this.db.prepare('UPDATE mint_invoices SET payment_q = ? WHERE payment_hash = ?');
188
+ for (const { payment_hash } of unmapped)
189
+ setQ.run(bearerNoteId(payment_hash), payment_hash);
190
+ this.db.prepare("INSERT INTO meta (key, value) VALUES ('legacy_ids_indexed', ?)").run(String(Date.now()));
191
+ });
192
+ }
193
+ // The old id a note named by `q` is stored under, if it predates Q ids.
194
+ // A read-only store opened on a database no upgraded mint has touched
195
+ // has no index yet, and so no old ids to find.
196
+ legacyIdOf(q) {
197
+ try {
198
+ const row = this.db.prepare('SELECT id FROM legacy_ids WHERE q = ?').get(q);
199
+ return row?.id ?? null;
200
+ }
201
+ catch {
202
+ return null;
203
+ }
204
+ }
205
+ // The note whose output key is `q`, wherever it is stored.
206
+ noteByQ(q) {
207
+ const note = this.noteById(q);
208
+ if (note)
209
+ return note;
210
+ const legacy = this.legacyIdOf(q);
211
+ return legacy ? this.noteById(legacy) : null;
141
212
  }
142
213
  // Invoices quoted at or after this instant get no verify if they named no
143
214
  // output. See the migration above for why it is not simply "now".
@@ -159,11 +230,12 @@ export class NoteStore {
159
230
  throw err;
160
231
  }
161
232
  }
233
+ // By the row's own key. A request names notes by Q: see noteByQ.
162
234
  noteById(id) {
163
235
  const row = this.db
164
- .prepare('SELECT id, amount_msat, state FROM notes WHERE id = ?')
236
+ .prepare('SELECT id, amount_msat, state, created_at FROM notes WHERE id = ?')
165
237
  .get(id);
166
- return row ? { id: row.id, amountMsat: row.amount_msat, state: row.state } : null;
238
+ return row ? { id: row.id, amountMsat: row.amount_msat, state: row.state, createdAt: row.created_at } : null;
167
239
  }
168
240
  insertNote(id, amountMsat) {
169
241
  const now = Date.now();
@@ -187,11 +259,16 @@ export class NoteStore {
187
259
  // Public because the pay callback has to answer this before it asks the
188
260
  // funding source for an invoice: a wallet must never pay for a quote the
189
261
  // mint was always going to refuse.
262
+ //
263
+ // A Q that an old id maps to is in use too, burned or not: crediting a
264
+ // new note there would give one secret two notes.
190
265
  outputIdInUse(id) {
191
266
  const asNote = this.db.prepare('SELECT 1 FROM notes WHERE id = ?').get(id);
192
267
  const asInvoice = this.db.prepare('SELECT 1 FROM mint_invoices WHERE payment_hash = ?').get(id);
193
268
  const asBoundOutput = this.db.prepare('SELECT 1 FROM mint_invoices WHERE output_id = ?').get(id);
194
- return Boolean(asNote || asInvoice || asBoundOutput);
269
+ const asLegacy = this.db.prepare('SELECT 1 FROM legacy_ids WHERE q = ?').get(id);
270
+ const asPreimageNote = this.db.prepare('SELECT 1 FROM mint_invoices WHERE payment_q = ?').get(id);
271
+ return Boolean(asNote || asInvoice || asBoundOutput || asLegacy || asPreimageNote);
195
272
  }
196
273
  assertOutputIdFree(id) {
197
274
  if (this.outputIdInUse(id))
@@ -313,22 +390,23 @@ export class NoteStore {
313
390
  outcome: row.outcome
314
391
  }));
315
392
  }
316
- // `outputId` is the note id the payer's wallet asked for with `h`. Both
317
- // ids are checked in the same transaction that inserts the row, so two
318
- // requests racing for one id cannot both be told yes.
319
- recordMintInvoice(paymentHash, pr, grossMsat, netMsat, outputId = null) {
393
+ // `outputId` is hex(Q) of the note the payer's wallet named, and
394
+ // `outputNamed` the value it named it by. Both ids are checked in the
395
+ // same transaction that inserts the row, so two requests racing for one
396
+ // id cannot both be told yes.
397
+ recordMintInvoice(paymentHash, pr, grossMsat, netMsat, outputId = null, outputNamed = outputId) {
320
398
  this.tx(() => {
321
399
  this.assertOutputIdFree(paymentHash);
322
400
  if (outputId !== null)
323
401
  this.assertOutputIdFree(outputId);
324
402
  this.db
325
- .prepare('INSERT INTO mint_invoices (payment_hash, pr, gross_msat, net_msat, settled, created_at, output_id) VALUES (?, ?, ?, ?, 0, ?, ?)')
326
- .run(paymentHash, pr, grossMsat, netMsat, Date.now(), outputId);
403
+ .prepare('INSERT INTO mint_invoices (payment_hash, pr, gross_msat, net_msat, settled, created_at, output_id, output_named, payment_q) VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?)')
404
+ .run(paymentHash, pr, grossMsat, netMsat, Date.now(), outputId, outputNamed, bearerNoteId(paymentHash));
327
405
  });
328
406
  }
329
407
  mintInvoiceByHash(paymentHash) {
330
408
  const row = this.db
331
- .prepare('SELECT payment_hash, pr, gross_msat, net_msat, settled, output_id, created_at FROM mint_invoices WHERE payment_hash = ?')
409
+ .prepare('SELECT payment_hash, pr, gross_msat, net_msat, settled, output_id, output_named, created_at FROM mint_invoices WHERE payment_hash = ?')
332
410
  .get(paymentHash);
333
411
  if (!row)
334
412
  return null;
@@ -339,6 +417,7 @@ export class NoteStore {
339
417
  netMsat: row.net_msat,
340
418
  settled: row.settled === 1,
341
419
  outputId: row.output_id,
420
+ outputNamed: row.output_named,
342
421
  createdAt: row.created_at
343
422
  };
344
423
  }
@@ -355,7 +434,7 @@ export class NoteStore {
355
434
  // expired invoices - so it is dead weight, and every /p/cb call adds one.
356
435
  unsettledMintInvoices() {
357
436
  const rows = this.db
358
- .prepare('SELECT payment_hash, pr, gross_msat, net_msat, settled, output_id, created_at FROM mint_invoices WHERE settled = 0')
437
+ .prepare('SELECT payment_hash, pr, gross_msat, net_msat, settled, output_id, output_named, created_at FROM mint_invoices WHERE settled = 0')
359
438
  .all();
360
439
  return rows.map(row => ({
361
440
  paymentHash: row.payment_hash,
@@ -364,6 +443,7 @@ export class NoteStore {
364
443
  netMsat: row.net_msat,
365
444
  settled: false,
366
445
  outputId: row.output_id,
446
+ outputNamed: row.output_named,
367
447
  createdAt: row.created_at
368
448
  }));
369
449
  }
package/dist/zap.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
2
- import { decodeCx1, deriveNotePubkey, encodeCp1, hashK1 } from '@lnurlcash/kit';
2
+ import { decodeCx1, encodeCp1, hashK1 } from '@lnurlcash/kit';
3
+ import { NOTE_PURPOSE_LIGHTNING_ADDRESS, bearerNoteIdOfPreimage, deriveNotePubkey } from "./spend.js";
3
4
  import { tryDecodeBolt11 } from 'farrier-kit/bolt11';
4
5
  import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
5
6
  import { SimplePool } from 'nostr-tools/pool';
@@ -83,8 +84,11 @@ const metadataFor = (name, host, mintFeeLine, feeInWords = null, internalTransfe
83
84
  ['text/plain', `Zap ${name}@${host}: arrives as a Lightning bearer note${feeInWords ? ` (${feeInWords})` : ''}`],
84
85
  ['text/identifier', `${name}@${host}`]
85
86
  ];
87
+ // `text/cpub`, never the older `text/xpub` as well: a wallet that reads
88
+ // `text/xpub` derives the index without a purpose, and would transfer to
89
+ // a key the payee never scans. Without it that wallet pays by Lightning.
86
90
  if (internalTransfer)
87
- metadata.push(['text/xpub', internalTransfer]);
91
+ metadata.push(['text/cpub', internalTransfer]);
88
92
  if (mintFeeLine)
89
93
  metadata.push(['text/plain', mintFeeLine]);
90
94
  return JSON.stringify(metadata);
@@ -113,7 +117,7 @@ export const createZapBridge = (deps) => {
113
117
  let index = registered.nextIndex;
114
118
  for (let tries = 0; tries < 1000; tries++, index++) {
115
119
  try {
116
- const noteId = bytesToHex(deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, index));
120
+ const noteId = bytesToHex(deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, NOTE_PURPOSE_LIGHTNING_ADDRESS, index));
117
121
  if (!store.outputIdInUse(noteId)) {
118
122
  internalTransfer = `${registered.cx1}:${index}`;
119
123
  break;
@@ -224,7 +228,7 @@ export const createZapBridge = (deps) => {
224
228
  // the certificate, and the index `i` the holder derives the key at. A
225
229
  // reader that knows only note URLs sees no k1 and passes it by, and the
226
230
  // note waits at the mint for a scan to find it.
227
- const buildKeyWrap = (row, noteId, index) => wrap(row, [...wrapTags(row), ['i', String(index)]], `${origin}/w?p=${encodeCp1(hexToBytes(noteId))}&sig=${deps.certify(noteId, row.netMsat)}&i=${index}`);
231
+ const buildKeyWrap = (row, noteId, index) => wrap(row, [...wrapTags(row), ['i', String(index)]], `${origin}/w?p=${encodeCp1(hexToBytes(noteId))}&c=${deps.certify(noteId, row.netMsat)}&i=${index}`);
228
232
  // NIP-57 receipt, without the preimage tag: it is optional there, and
229
233
  // here it would only invite someone to mistake it for the note.
230
234
  const buildReceipt = (row) => {
@@ -251,7 +255,7 @@ export const createZapBridge = (deps) => {
251
255
  const receipt = buildReceipt(row);
252
256
  const keyAt = (index) => {
253
257
  try {
254
- return bytesToHex(deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, index));
258
+ return bytesToHex(deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, NOTE_PURPOSE_LIGHTNING_ADDRESS, index));
255
259
  }
256
260
  catch {
257
261
  return null;
@@ -268,7 +272,8 @@ export const createZapBridge = (deps) => {
268
272
  continue;
269
273
  }
270
274
  const k1 = bytesToHex(randomBytes(32));
271
- const noteId = hashK1(k1);
275
+ // The bearer note whose preimage is k1, stored under its Q.
276
+ const noteId = bearerNoteIdOfPreimage(k1);
272
277
  const wrap = buildWrap(row, k1);
273
278
  const receipt = buildReceipt(row);
274
279
  if (store.settleZapInvoice(row.paymentHash, noteId, JSON.stringify(wrap), receipt ? JSON.stringify(receipt) : null)) {
package/llms.txt CHANGED
@@ -29,11 +29,16 @@ Client library to build against a mint: @lnurlcash/kit
29
29
  inside the invoice. `h` is an additive alias for the same commitment and
30
30
  never replaces the comment. Because the note is not keyed by the
31
31
  preimage, a funding source whose node chooses its own preimages
32
- (phoenixd, NIP-47 make_invoice) can back a mint. LUD-25 Part 2: the
33
- comment may instead be `cp1<pk>`, and the note is keyed by that key.
32
+ (phoenixd, NIP-47 make_invoice) can back a mint. Every note is a LUD-25
33
+ taproot output key Q: a 64-hex comment is a bearer note's h (Q follows
34
+ from it), and `cp1<Q>` names any note directly.
34
35
  2. Mutations are atomic: all named k1 burn and all outputs mint, or
35
36
  nothing happens. A note named twice in one request is refused, compared
36
- by the note each k1 spends: one Part 2 note has many valid ck1 strings.
37
+ by the Q each k1 spends: one note has many valid spends (preimage, cw1,
38
+ ck1). Every k1 is verified first: a ck1 is bound to this mint's domains
39
+ (clearnet and onion), a cw1 is checked for leaf version, OP_SUCCESSx and
40
+ Unix-time claims, and only bearer hashlock leaves are evaluated in
41
+ process; other leaves need MoneyerDeps.scriptVerifier.
37
42
  3. p1/p2 (older names h/h2, still accepted; both spellings of one output
38
43
  must agree) are WALLET-supplied hashes or cp1 keys; the mint never generates a
39
44
  replacement secret, and refuses output ids that already exist as
@@ -84,10 +89,17 @@ configFromEnv(env?) -> MoneyerConfig MONEYER_* environment variables
84
89
  operator configured - it never builds one.
85
90
  createFakeBackend() -> LightningBackend with .control test hooks
86
91
  createClnBackend({url, rune}) / createLndBackend({url, macaroon})
92
+ decodeSpend(k1) -> Spend | null a preimage, ck1 or cw1, with the Q it opens
93
+ decodeNote(value) -> hex(Q) | null a cp1, or a bearer note's 64-hex h
94
+ verifySpend(spend, {outputKey, domains, now, lockedAt, scriptVerifier?}) -> Promise<SpendVerdict>
95
+ bearerNoteId(hHex) / bearerNoteIdOfPreimage(k1Hex) -> hex(Q)
96
+ keyPathSighash(Q, domain) / scriptPathSighash(Q, domain, leaf, locktime, sequence) -> 32 bytes
97
+ ScriptVerifier = ({outputKey, domain, spend, now, lockedAt}) => Promise<reason | null>
98
+ pass as MoneyerDeps.scriptVerifier to judge leaves moneyer cannot evaluate itself
87
99
  createNoteSigner(privHex) -> {pubkey, sign(noteId, amountMsat)}
88
100
  privHex is a secp256k1 private key, as MONEYER_SIGNING_KEY; zero is rejected
89
101
  fakeBolt11({amountMsat, paymentHashHex, memo?}) -> decodable, unpayable invoice
90
- NoteStore - SQLite store; notes by sha256(k1), never secrets
102
+ NoteStore - SQLite store; notes by hex(Q), never spends; pre-Q rows found via legacy_ids
91
103
  runMelt(job, deps) -> Promise<void> / reconcilePendingMelts(store, backend, inFlight, log?)
92
104
  the melt discipline: burn only on confirmed payment, restore only on confirmed
93
105
  non-payment. The /w/cb handler drives both; outcomes land in the store, not in a
@@ -108,15 +120,15 @@ signStats(stats, privHex) -> string the signature alone, hex r || s || recover
108
120
 
109
121
  /.well-known/lnurlp/{user} LUD-16 payRequest (mints)
110
122
  /.well-known/lnurlw/{user} LUD-25 mint address (experimental)
111
- /p/cb LUD-06 pay callback (comment names the note: hash or cp1)
123
+ /p/cb LUD-06 pay callback (comment names the note: a bearer h or a cp1)
112
124
  /verify/{payment_hash} LUD-21 verify (mint invoices AND melt payments)
113
- /w LUD-03 informational GET by k1 (secret or ck1) or p (hash or cp1, older h); a cp1 note carries its cs1 as sig
114
- /w/cb melt / rotate / split / merge; inputs secrets or ck1, outputs p1/p2 hashes or cp1 (or older h/h2); cp1 outputs certified as cs1
125
+ /w LUD-03 informational GET by k1 (preimage, ck1 or cw1, verified in full) or p (a bearer h or a cp1, older h); every note carries its cs1 over Q as sig
126
+ /w/cb melt / rotate / split / merge; inputs preimages, ck1 or cw1, outputs p1/p2 as a bearer h or cp1 (or older h/h2); every output certified as cs1 over Q; a taken output is `already in use`
115
127
  POST /names claim a name (NIP-98; the signer owns it); body {name, note?, cx1?}.
116
128
  The owner may resend {name, cx1} to set a branch, or cx1: null to clear it
117
129
  /z/cb/{name} zap invoice for a name. With a cx1 the settled note is credited to the
118
- holder's next key, and the kind 2525 gift wrap carries no secret:
119
- /w?p=<cp1>&sig=<amount-bearing-cs1>&i=<index> plus an `i` tag. The index is
130
+ holder's next key on the branch's Lightning Address purpose (2), and the kind 2525 gift wrap carries no secret:
131
+ /w?p=<cp1>&c=<amount-bearing-cs1>&i=<index> plus an `i` tag. The index is
120
132
  taken at settlement, never at the invoice, and occupied keys are skipped
121
- Its payRequest metadata publishes text/xpub = cx1:<next-free-index> for
133
+ Its payRequest metadata publishes text/cpub = cx1:<next-free-purpose-2-index> for
122
134
  same-mint rotate/split/merge transfers; a raced key returns "already in use"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/moneyer",
3
- "version": "0.16.2",
3
+ "version": "0.17.0",
4
4
  "description": "An LNURLcash (LUD-25) mint - strikes Lightning bearer notes. Independent implementation, cln/lnd funding sources, SQLite, zero HTTP framework.",
5
5
  "author": "TheCryptoDonkey",
6
6
  "license": "MIT",
@@ -50,6 +50,7 @@
50
50
  "dist",
51
51
  "web/dist",
52
52
  "scripts/live-bound-mint-check.mjs",
53
+ "scripts/kernel-verifier.py",
53
54
  "LICENSE",
54
55
  "README.md",
55
56
  "CHANGELOG.md",
@@ -87,7 +88,7 @@
87
88
  "@types/node": "^24.0.0",
88
89
  "animejs": "^4.0.0",
89
90
  "happy-dom": "^20.0.0",
90
- "lnurlcash-conformance": "0.13.1",
91
+ "lnurlcash-conformance": "0.15.0",
91
92
  "playwright": "^1.62.1",
92
93
  "typescript": "^5.7.0",
93
94
  "uqr": "^0.1.2",
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """moneyer's tapscript verifier: Bitcoin Core's own script interpreter,
3
+ through lnurlcash-kernel (pip install lnurlcash-kernel), the same verifier
4
+ the reference mint uses.
5
+
6
+ moneyer evaluates a bearer note's hashlock leaf itself and hands every other
7
+ script-path spend here. One JSON request per line on stdin, one reply per
8
+ line on stdout, matched by id:
9
+
10
+ -> {"id": 1, "q": "<hex Q>", "domain": "mint.example", "cw1": "cw1...",
11
+ "now": 1790000000, "locked_at": 1789990000}
12
+ <- {"id": 1, "ok": true}
13
+ <- {"id": 1, "reason": "bitcoin core rejected the spend"}
14
+
15
+ `now` and `locked_at` are moneyer's clock, in Unix seconds: the kernel never
16
+ reads one. A reply carrying "error" is a broken install or a bug, never a
17
+ verdict, and moneyer refuses the spend.
18
+ """
19
+
20
+ import json
21
+ import sys
22
+
23
+ import lnurlcashkernel as kernel
24
+
25
+
26
+ def answer(request: dict) -> dict:
27
+ spend = kernel.decode_spend(request["cw1"])
28
+ if spend is None or spend.key_path:
29
+ return {"reason": "malformed cw1"}
30
+ try:
31
+ kernel.verify_spend(
32
+ output_key=bytes.fromhex(request["q"]),
33
+ domain=request["domain"],
34
+ spend=spend,
35
+ now=int(request["now"]),
36
+ locked_at=int(request["locked_at"]),
37
+ )
38
+ except kernel.SpendRejected as exc:
39
+ return {"reason": exc.reason}
40
+ return {"ok": True}
41
+
42
+
43
+ def main() -> None:
44
+ for line in sys.stdin:
45
+ try:
46
+ request = json.loads(line)
47
+ request_id = request["id"]
48
+ except (ValueError, KeyError, TypeError):
49
+ continue
50
+ try:
51
+ reply = answer(request)
52
+ except Exception as exc: # noqa: BLE001 - reported, never a verdict
53
+ reply = {"error": type(exc).__name__}
54
+ sys.stdout.write(json.dumps({"id": request_id, **reply}) + "\n")
55
+ sys.stdout.flush()
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()