@breeztech/breez-sdk-spark 0.27.2 → 0.28.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/README.md +9 -8
- package/bundler/breez_sdk_spark_wasm.d.ts +141 -16
- package/bundler/breez_sdk_spark_wasm.js +1 -1
- package/bundler/breez_sdk_spark_wasm_bg.js +190 -40
- package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +23 -11
- package/bundler/index.js +6 -13
- package/bundler/storage/index.js +89 -17
- package/deno/breez_sdk_spark_wasm.d.ts +141 -16
- package/deno/breez_sdk_spark_wasm.js +190 -40
- package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +23 -11
- package/nodejs/breez_sdk_spark_wasm.d.ts +141 -16
- package/nodejs/breez_sdk_spark_wasm.js +194 -40
- package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +23 -11
- package/nodejs/index.d.mts +3 -0
- package/nodejs/index.js +30 -120
- package/nodejs/index.mjs +4 -0
- package/nodejs/mysql-session-store/index.cjs +24 -7
- package/nodejs/mysql-session-store/migrations.cjs +6 -0
- package/nodejs/mysql-storage/index.cjs +36 -19
- package/nodejs/mysql-storage/migrations.cjs +24 -9
- package/nodejs/mysql-token-store/index.cjs +28 -18
- package/nodejs/postgres-session-store/index.cjs +25 -7
- package/nodejs/postgres-session-store/migrations.cjs +12 -9
- package/nodejs/postgres-storage/index.cjs +36 -19
- package/nodejs/postgres-storage/migrations.cjs +24 -9
- package/nodejs/postgres-token-store/index.cjs +32 -18
- package/nodejs/postgres-token-store/migrations.cjs +5 -9
- package/nodejs/postgres-tree-store/migrations.cjs +5 -9
- package/nodejs/storage/index.cjs +35 -14
- package/nodejs/storage/migrations.cjs +25 -15
- package/nodejs/tree-store/index.cjs +1 -1
- package/nodejs/tree-store/migrations.cjs +6 -13
- package/package.json +27 -6
- package/ssr/index.js +24 -0
- package/web/breez_sdk_spark_wasm.d.ts +164 -27
- package/web/breez_sdk_spark_wasm.js +190 -40
- package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +23 -11
- package/web/index.js +6 -13
- package/web/passkey-prf-provider/index.js +1 -0
- package/web/storage/index.js +89 -17
- package/breez-sdk-spark.tgz +0 -0
- package/itest/.gitignore +0 -2
- package/itest/database-drivers.test.js +0 -45
- package/itest/helpers/assert.test.js +0 -70
- package/itest/helpers/faucet.js +0 -68
- package/itest/helpers/lnurl-fixture.js +0 -229
- package/itest/helpers/scenario.js +0 -444
- package/itest/package-lock.json +0 -519
- package/itest/package.json +0 -18
- package/itest/scenarios.test.js +0 -47
- package/itest/smoke.test.js +0 -184
package/web/storage/index.js
CHANGED
|
@@ -27,7 +27,7 @@ class MigrationManager {
|
|
|
27
27
|
const transaction = event.target.transaction;
|
|
28
28
|
|
|
29
29
|
this._log(
|
|
30
|
-
"
|
|
30
|
+
"INFO",
|
|
31
31
|
`Upgrading IndexedDB from version ${oldVersion} to ${newVersion}`
|
|
32
32
|
);
|
|
33
33
|
|
|
@@ -35,14 +35,14 @@ class MigrationManager {
|
|
|
35
35
|
for (let i = oldVersion; i < newVersion; i++) {
|
|
36
36
|
const migration = this.migrations[i];
|
|
37
37
|
if (migration) {
|
|
38
|
-
this._log("
|
|
38
|
+
this._log("DEBUG", `Running migration ${i + 1}: ${migration.name}`);
|
|
39
39
|
migration.upgrade(db, transaction);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
this._log("
|
|
42
|
+
this._log("INFO", `Database migration completed successfully`);
|
|
43
43
|
} catch (error) {
|
|
44
44
|
this._log(
|
|
45
|
-
"
|
|
45
|
+
"ERROR",
|
|
46
46
|
`Migration failed at version ${oldVersion}: ${error.message}`
|
|
47
47
|
);
|
|
48
48
|
throw new this.StorageError(
|
|
@@ -53,14 +53,7 @@ class MigrationManager {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
_log(level, message) {
|
|
56
|
-
|
|
57
|
-
this.logger.log({
|
|
58
|
-
line: message,
|
|
59
|
-
level: level,
|
|
60
|
-
});
|
|
61
|
-
} else if (level === "error") {
|
|
62
|
-
console.error(`[MigrationManager] ${message}`);
|
|
63
|
-
}
|
|
56
|
+
this.logger?.log({ line: message, level });
|
|
64
57
|
}
|
|
65
58
|
|
|
66
59
|
/**
|
|
@@ -149,7 +142,7 @@ class MigrationManager {
|
|
|
149
142
|
}
|
|
150
143
|
});
|
|
151
144
|
|
|
152
|
-
|
|
145
|
+
this._log("INFO", `Migrated ${updated} payment records to BigInt format`);
|
|
153
146
|
};
|
|
154
147
|
},
|
|
155
148
|
},
|
|
@@ -653,6 +646,77 @@ class MigrationManager {
|
|
|
653
646
|
}
|
|
654
647
|
},
|
|
655
648
|
},
|
|
649
|
+
{
|
|
650
|
+
// A withdrawal stored before the SDK kept its destination address and
|
|
651
|
+
// output index loses its details, which the SDK cannot rebuild here.
|
|
652
|
+
// With the sync offset at 0, the SDK re-fetches every transfer on its
|
|
653
|
+
// next sync and rewrites past withdrawals with both values.
|
|
654
|
+
name: "Clear legacy withdrawal details and reset sync offset",
|
|
655
|
+
upgrade: (db, transaction) => {
|
|
656
|
+
if (db.objectStoreNames.contains("payments")) {
|
|
657
|
+
const paymentStore = transaction.objectStore("payments");
|
|
658
|
+
// The cursor opens from a callback, so it reads the payments after
|
|
659
|
+
// the earlier steps of this upgrade have written theirs.
|
|
660
|
+
paymentStore.count().onsuccess = () => {
|
|
661
|
+
const cursorRequest = paymentStore.openCursor();
|
|
662
|
+
cursorRequest.onsuccess = (event) => {
|
|
663
|
+
const cursor = event.target.result;
|
|
664
|
+
if (!cursor) return;
|
|
665
|
+
const payment = cursor.value;
|
|
666
|
+
let details = null;
|
|
667
|
+
if (payment.details && typeof payment.details === "string") {
|
|
668
|
+
try {
|
|
669
|
+
details = JSON.parse(payment.details);
|
|
670
|
+
} catch (e) {
|
|
671
|
+
details = null;
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
details = payment.details;
|
|
675
|
+
}
|
|
676
|
+
if (
|
|
677
|
+
details &&
|
|
678
|
+
details.type === "withdraw" &&
|
|
679
|
+
(details.vout == null || details.destinationAddress == null)
|
|
680
|
+
) {
|
|
681
|
+
payment.details = null;
|
|
682
|
+
cursor.update(payment);
|
|
683
|
+
}
|
|
684
|
+
cursor.continue();
|
|
685
|
+
};
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
if (!db.objectStoreNames.contains("settings")) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const settingsStore = transaction.objectStore("settings");
|
|
692
|
+
const getRequest = settingsStore.get("sync_offset");
|
|
693
|
+
getRequest.onsuccess = () => {
|
|
694
|
+
const syncCache = getRequest.result;
|
|
695
|
+
if (!syncCache || !syncCache.value) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
const syncInfo = JSON.parse(syncCache.value);
|
|
700
|
+
syncInfo.offset = 0;
|
|
701
|
+
settingsStore.put({
|
|
702
|
+
key: "sync_offset",
|
|
703
|
+
value: toJson(syncInfo),
|
|
704
|
+
});
|
|
705
|
+
} catch (e) {
|
|
706
|
+
// If parsing fails, just continue
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
name: "Force lnurl_receive_metadata re-sync for the lightning address",
|
|
713
|
+
upgrade: (db, transaction) => {
|
|
714
|
+
if (db.objectStoreNames.contains("settings")) {
|
|
715
|
+
const settings = transaction.objectStore("settings");
|
|
716
|
+
settings.delete("lnurl_metadata_updated_after");
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
},
|
|
656
720
|
];
|
|
657
721
|
}
|
|
658
722
|
}
|
|
@@ -681,7 +745,7 @@ class IndexedDBStorage {
|
|
|
681
745
|
// so existing databases depend on indices never shifting. Never insert,
|
|
682
746
|
// reorder, or delete a migration — only append. dbVersion MUST equal the
|
|
683
747
|
// number of migrations (enforced by the guard in initialize()).
|
|
684
|
-
this.dbVersion =
|
|
748
|
+
this.dbVersion = 25; // Current schema version (= migration count)
|
|
685
749
|
}
|
|
686
750
|
|
|
687
751
|
/**
|
|
@@ -1071,13 +1135,15 @@ class IndexedDBStorage {
|
|
|
1071
1135
|
shouldPersist = true;
|
|
1072
1136
|
shouldEmit = true;
|
|
1073
1137
|
} else if (stored === next) {
|
|
1074
|
-
|
|
1138
|
+
this._log(
|
|
1139
|
+
"DEBUG",
|
|
1075
1140
|
`Skipping redundant payment event: id=${payment.id} status=${next}`
|
|
1076
1141
|
);
|
|
1077
1142
|
shouldPersist = true;
|
|
1078
1143
|
shouldEmit = false;
|
|
1079
1144
|
} else if (this._isFinalPaymentStatus(stored)) {
|
|
1080
|
-
|
|
1145
|
+
this._log(
|
|
1146
|
+
"WARN",
|
|
1081
1147
|
`Skipping payment update (would replace terminal status): id=${payment.id} stored=${stored} new=${next}`
|
|
1082
1148
|
);
|
|
1083
1149
|
shouldPersist = false;
|
|
@@ -1293,7 +1359,7 @@ class IndexedDBStorage {
|
|
|
1293
1359
|
/**
|
|
1294
1360
|
* Gets payments that have any of the specified parent payment IDs.
|
|
1295
1361
|
* @param {string[]} parentPaymentIds - Array of parent payment IDs
|
|
1296
|
-
* @returns {Promise<Object>} Map of parentPaymentId -> array of
|
|
1362
|
+
* @returns {Promise<Object>} Map of parentPaymentId -> array of Payment objects
|
|
1297
1363
|
*/
|
|
1298
1364
|
async getPaymentsByParentIds(parentPaymentIds) {
|
|
1299
1365
|
if (!this.db) {
|
|
@@ -2007,6 +2073,7 @@ class IndexedDBStorage {
|
|
|
2007
2073
|
nostrZapRequest: item.nostrZapRequest || null,
|
|
2008
2074
|
nostrZapReceipt: item.nostrZapReceipt || null,
|
|
2009
2075
|
senderComment: item.senderComment || null,
|
|
2076
|
+
lightningAddress: item.lightningAddress || null,
|
|
2010
2077
|
});
|
|
2011
2078
|
|
|
2012
2079
|
request.onsuccess = () => {
|
|
@@ -2725,6 +2792,10 @@ class IndexedDBStorage {
|
|
|
2725
2792
|
};
|
|
2726
2793
|
}
|
|
2727
2794
|
|
|
2795
|
+
_log(level, line) {
|
|
2796
|
+
this.logger?.log({ line, level });
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2728
2799
|
_normalizePaymentStatus(status) {
|
|
2729
2800
|
return typeof status === "string" ? status.toLowerCase() : status;
|
|
2730
2801
|
}
|
|
@@ -3040,6 +3111,7 @@ class IndexedDBStorage {
|
|
|
3040
3111
|
nostrZapRequest: lnurlReceiveMetadata.nostrZapRequest || null,
|
|
3041
3112
|
nostrZapReceipt: lnurlReceiveMetadata.nostrZapReceipt || null,
|
|
3042
3113
|
senderComment: lnurlReceiveMetadata.senderComment || null,
|
|
3114
|
+
lightningAddress: lnurlReceiveMetadata.lightningAddress || null,
|
|
3043
3115
|
};
|
|
3044
3116
|
}
|
|
3045
3117
|
resolve(payment);
|
package/breez-sdk-spark.tgz
DELETED
|
Binary file
|
package/itest/.gitignore
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// This project installs neither `pg` nor `mysql2`. The package loads without
|
|
4
|
-
// a warning about them, and tells an app that selects PostgreSQL or MySQL
|
|
5
|
-
// storage which driver to install. No network.
|
|
6
|
-
|
|
7
|
-
const assert = require('node:assert/strict')
|
|
8
|
-
const test = require('node:test')
|
|
9
|
-
|
|
10
|
-
// The package prints its warnings on the first require in a process.
|
|
11
|
-
const warnings = []
|
|
12
|
-
const consoleWarn = console.warn
|
|
13
|
-
console.warn = (...args) => warnings.push(args.join(' '))
|
|
14
|
-
const {
|
|
15
|
-
SdkBuilder,
|
|
16
|
-
defaultConfig,
|
|
17
|
-
defaultMysqlStorageConfig,
|
|
18
|
-
defaultPostgresStorageConfig,
|
|
19
|
-
mysqlStorage,
|
|
20
|
-
postgresStorage,
|
|
21
|
-
} = require('@breeztech/breez-sdk-spark/nodejs')
|
|
22
|
-
console.warn = consoleWarn
|
|
23
|
-
|
|
24
|
-
function buildWith(storage) {
|
|
25
|
-
const mnemonic = require('bip39').generateMnemonic()
|
|
26
|
-
const builder = SdkBuilder.new(defaultConfig('regtest'), { type: 'mnemonic', mnemonic })
|
|
27
|
-
return builder.withStorageBackend(storage).build()
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
test('loading the package prints no database driver warning', () => {
|
|
31
|
-
assert.deepEqual(
|
|
32
|
-
warnings.filter((warning) => /PostgreSQL|MySQL/.test(warning)),
|
|
33
|
-
[]
|
|
34
|
-
)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('selecting PostgreSQL storage names the missing driver', async () => {
|
|
38
|
-
const config = defaultPostgresStorageConfig('postgres://user:password@localhost:5432/spark')
|
|
39
|
-
await assert.rejects(buildWith(postgresStorage(config)), /pg not found\. Please install it/)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('selecting MySQL storage names the missing driver', async () => {
|
|
43
|
-
const config = defaultMysqlStorageConfig('mysql://user:password@localhost:3306/spark')
|
|
44
|
-
await assert.rejects(buildWith(mysqlStorage(config)), /mysql2 not found\. Please install it/)
|
|
45
|
-
})
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// Pure tests for the tolerant JSON evaluator; no network. Mirrors the unit
|
|
4
|
-
// tests in crates/breez-sdk/cli/tests/harness/assert.rs.
|
|
5
|
-
|
|
6
|
-
const assert = require('node:assert/strict')
|
|
7
|
-
const test = require('node:test')
|
|
8
|
-
|
|
9
|
-
const { checkMatcher, extractJsonDocs, interpolate, lookupPath } = require('./scenario')
|
|
10
|
-
|
|
11
|
-
test('extracts pretty and inline docs and skips noise', () => {
|
|
12
|
-
const chunk = 'Breez SDK: noise\n{\n "a": 1\n}\nError: nope\n{"b": 2}\nEvent: {"c": 3}\n'
|
|
13
|
-
assert.deepEqual(extractJsonDocs(chunk), [{ a: 1 }, { b: 2 }])
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
test('path matches across casings', () => {
|
|
17
|
-
const rust = { payment_request: { amount_msat: 5 } }
|
|
18
|
-
const wasm = { paymentRequest: { amountMsat: 5 } }
|
|
19
|
-
for (const doc of [rust, wasm]) {
|
|
20
|
-
assert.equal(lookupPath(doc, 'payment_request.amount_msat'), 5)
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('path bridges enum tags', () => {
|
|
25
|
-
const rust = { Bolt11Invoice: { amount_msat: 7 } }
|
|
26
|
-
const wasm = { type: 'bolt11Invoice', amountMsat: 7 }
|
|
27
|
-
for (const doc of [rust, wasm]) {
|
|
28
|
-
assert.equal(lookupPath(doc, 'bolt11_invoice.amount_msat'), 7)
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('empty path addresses the document', () => {
|
|
33
|
-
assert.deepEqual(lookupPath({}, ''), {})
|
|
34
|
-
checkMatcher({}, lookupPath({}, ''))
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('path indexes arrays', () => {
|
|
38
|
-
const doc = { payments: [{ id: 'x' }, { id: 'y' }] }
|
|
39
|
-
assert.equal(lookupPath(doc, 'payments.1.id'), 'y')
|
|
40
|
-
assert.equal(lookupPath(doc, 'payments.2.id'), undefined)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('equality tolerates case and bigint strings', () => {
|
|
44
|
-
checkMatcher('completed', 'Completed')
|
|
45
|
-
checkMatcher('1000', 1000)
|
|
46
|
-
checkMatcher(1000, '1000')
|
|
47
|
-
assert.throws(() => checkMatcher('completed', 'failed'))
|
|
48
|
-
assert.throws(() => checkMatcher('completed', undefined))
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('gte accepts numbers and numeric strings', () => {
|
|
52
|
-
checkMatcher({ gte: 10 }, 11)
|
|
53
|
-
checkMatcher({ gte: 10 }, '10')
|
|
54
|
-
assert.throws(() => checkMatcher({ gte: 10 }, 9))
|
|
55
|
-
assert.throws(() => checkMatcher({ gte: 10 }, 'abc'))
|
|
56
|
-
assert.throws(() => checkMatcher({ gte: 10 }, undefined))
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('exists checks presence and null', () => {
|
|
60
|
-
checkMatcher({ exists: true }, 'x')
|
|
61
|
-
checkMatcher({ exists: false }, undefined)
|
|
62
|
-
checkMatcher({ exists: false }, null)
|
|
63
|
-
assert.throws(() => checkMatcher({ exists: true }, null))
|
|
64
|
-
assert.throws(() => checkMatcher({ exists: true }, undefined))
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('interpolation substitutes known vars and rejects unknown', () => {
|
|
68
|
-
assert.equal(interpolate('pay -r ${addr} -a 5', { addr: 'spark1x' }), 'pay -r spark1x -a 5')
|
|
69
|
-
assert.throws(() => interpolate('${missing}', {}))
|
|
70
|
-
})
|
package/itest/helpers/faucet.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// Minimal client for the Lightspark regtest faucet, mirroring the Rust
|
|
4
|
-
// harness (crates/breez-sdk/cli/tests/harness/faucet.rs).
|
|
5
|
-
|
|
6
|
-
const DEFAULT_URL = 'https://api.lightspark.com/graphql/spark/rc'
|
|
7
|
-
const MAX_RETRIES = 3
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Fund a regtest bitcoin address, returning the funding txid. Retries with
|
|
11
|
-
* exponential backoff. Reads FAUCET_URL, FAUCET_USERNAME, and
|
|
12
|
-
* FAUCET_PASSWORD from the environment.
|
|
13
|
-
*
|
|
14
|
-
* @param {string} address
|
|
15
|
-
* @param {number} amountSats
|
|
16
|
-
* @returns {Promise<string>}
|
|
17
|
-
*/
|
|
18
|
-
async function fundAddress(address, amountSats) {
|
|
19
|
-
let lastError
|
|
20
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
21
|
-
if (attempt > 0) {
|
|
22
|
-
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000))
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
return await tryFundAddress(address, amountSats)
|
|
26
|
-
} catch (e) {
|
|
27
|
-
lastError = e
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
throw lastError
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function tryFundAddress(address, amountSats) {
|
|
34
|
-
const url = process.env.FAUCET_URL || DEFAULT_URL
|
|
35
|
-
const headers = { 'Content-Type': 'application/json' }
|
|
36
|
-
const { FAUCET_USERNAME, FAUCET_PASSWORD } = process.env
|
|
37
|
-
if (FAUCET_USERNAME && FAUCET_PASSWORD) {
|
|
38
|
-
const credentials = Buffer.from(`${FAUCET_USERNAME}:${FAUCET_PASSWORD}`).toString('base64')
|
|
39
|
-
headers.Authorization = `Basic ${credentials}`
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const response = await fetch(url, {
|
|
43
|
-
method: 'POST',
|
|
44
|
-
headers,
|
|
45
|
-
body: JSON.stringify({
|
|
46
|
-
operationName: 'RequestRegtestFunds',
|
|
47
|
-
variables: { amount_sats: amountSats, address },
|
|
48
|
-
query:
|
|
49
|
-
'mutation RequestRegtestFunds($address: String!, $amount_sats: Long!) { ' +
|
|
50
|
-
'request_regtest_funds(input: {address: $address, amount_sats: $amount_sats}) ' +
|
|
51
|
-
'{ transaction_hash}}'
|
|
52
|
-
})
|
|
53
|
-
})
|
|
54
|
-
const body = await response.text()
|
|
55
|
-
if (!response.ok) {
|
|
56
|
-
throw new Error(`faucet request failed with status ${response.status}: ${body}`)
|
|
57
|
-
}
|
|
58
|
-
const parsed = JSON.parse(body)
|
|
59
|
-
if (parsed.errors && parsed.errors.length > 0) {
|
|
60
|
-
throw new Error(`faucet returned errors: ${parsed.errors.map((e) => e.message).join(', ')}`)
|
|
61
|
-
}
|
|
62
|
-
if (!parsed.data) {
|
|
63
|
-
throw new Error(`faucet response has no data: ${body}`)
|
|
64
|
-
}
|
|
65
|
-
return parsed.data.request_regtest_funds.transaction_hash
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
module.exports = { fundAddress }
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// LNURL server fixture driven through the docker CLI, mirroring the Rust
|
|
4
|
-
// harness (crates/breez-sdk/cli/tests/harness/lnurl.rs). Shares the
|
|
5
|
-
// breez-lnurl-built image tag: whichever runner builds it first wins. To
|
|
6
|
-
// force a rebuild: `docker image rm breez-lnurl-built:latest`.
|
|
7
|
-
|
|
8
|
-
const net = require('net')
|
|
9
|
-
const path = require('path')
|
|
10
|
-
const { execFile, execFileSync } = require('child_process')
|
|
11
|
-
const { promisify } = require('util')
|
|
12
|
-
|
|
13
|
-
const execFileAsync = promisify(execFile)
|
|
14
|
-
|
|
15
|
-
const IMAGE = 'breez-lnurl-built:latest'
|
|
16
|
-
const POSTGRES_IMAGE = 'postgres:16-alpine'
|
|
17
|
-
const HTTP_PORT = 8080
|
|
18
|
-
const POSTGRES_PORT = 5432
|
|
19
|
-
const START_TIMEOUT_MS = 120_000
|
|
20
|
-
const WORKSPACE_ROOT = path.join(__dirname, '../../../..')
|
|
21
|
-
|
|
22
|
-
/** @returns {boolean} whether the docker daemon is reachable */
|
|
23
|
-
function dockerAvailable() {
|
|
24
|
-
try {
|
|
25
|
-
execFileSync('docker', ['version'], { stdio: 'ignore' })
|
|
26
|
-
return true
|
|
27
|
-
} catch {
|
|
28
|
-
return false
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function docker(args, opts = {}) {
|
|
33
|
-
const { stdout } = await execFileAsync('docker', args, { maxBuffer: 64 * 1024 * 1024, ...opts })
|
|
34
|
-
return stdout
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function buildImageIfMissing() {
|
|
38
|
-
try {
|
|
39
|
-
await docker(['image', 'inspect', IMAGE])
|
|
40
|
-
return
|
|
41
|
-
} catch {
|
|
42
|
-
// Image missing: build it.
|
|
43
|
-
}
|
|
44
|
-
console.error(`building ${IMAGE} (first run only, this can take several minutes)`)
|
|
45
|
-
await docker([
|
|
46
|
-
'build',
|
|
47
|
-
'-f',
|
|
48
|
-
path.join(WORKSPACE_ROOT, 'crates/breez-sdk/lnurl/Dockerfile'),
|
|
49
|
-
'-t',
|
|
50
|
-
IMAGE,
|
|
51
|
-
'--build-arg',
|
|
52
|
-
'CARGO_FEATURES=dev',
|
|
53
|
-
WORKSPACE_ROOT
|
|
54
|
-
])
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** @returns {Promise<number>} host port the container port was published to */
|
|
58
|
-
async function publishedPort(containerId, containerPort) {
|
|
59
|
-
const portOutput = await docker(['port', containerId, `${containerPort}/tcp`])
|
|
60
|
-
return Number(portOutput.split('\n')[0].trim().split(':').pop())
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Resolves once something answers HTTP on `port`. Any status answers the
|
|
65
|
-
* question, so this only checks that a response came back. HTTP/1.0 so the
|
|
66
|
-
* server closes the connection when it is done.
|
|
67
|
-
*/
|
|
68
|
-
function httpResponds(port) {
|
|
69
|
-
return new Promise((resolve, reject) => {
|
|
70
|
-
const socket = net.connect(port, '127.0.0.1')
|
|
71
|
-
let response = ''
|
|
72
|
-
socket.setTimeout(5000)
|
|
73
|
-
socket.on('connect', () => socket.write('GET /health HTTP/1.0\r\nHost: localhost\r\n\r\n'))
|
|
74
|
-
socket.on('data', (chunk) => {
|
|
75
|
-
response += chunk
|
|
76
|
-
})
|
|
77
|
-
socket.on('end', () =>
|
|
78
|
-
response.startsWith('HTTP/') ? resolve() : reject(new Error('no HTTP response'))
|
|
79
|
-
)
|
|
80
|
-
socket.on('timeout', () => socket.destroy(new Error('timed out')))
|
|
81
|
-
socket.on('error', reject)
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
class LnurlFixture {
|
|
86
|
-
/** @param {string} containerId @param {string} httpUrl @param {string} postgresContainerId */
|
|
87
|
-
constructor(containerId, httpUrl, postgresContainerId) {
|
|
88
|
-
this.containerId = containerId
|
|
89
|
-
this.httpUrl = httpUrl
|
|
90
|
-
// Backing database. Postgres is the server's only supported backend.
|
|
91
|
-
this.postgresContainerId = postgresContainerId
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
static async start() {
|
|
95
|
-
await buildImageIfMissing()
|
|
96
|
-
const fixture = new LnurlFixture('', '', '')
|
|
97
|
-
try {
|
|
98
|
-
const postgresOutput = await docker([
|
|
99
|
-
'run',
|
|
100
|
-
'-d',
|
|
101
|
-
// All interfaces rather than 127.0.0.1: the lnurl server reaches this
|
|
102
|
-
// from inside its own container through the host gateway, which on
|
|
103
|
-
// Linux is the bridge address and not loopback.
|
|
104
|
-
'-p',
|
|
105
|
-
'0.0.0.0:0:5432',
|
|
106
|
-
'-e', 'POSTGRES_PASSWORD=postgres',
|
|
107
|
-
POSTGRES_IMAGE
|
|
108
|
-
])
|
|
109
|
-
fixture.postgresContainerId = postgresOutput.trim()
|
|
110
|
-
await fixture.waitForPostgres()
|
|
111
|
-
const dbPort = await publishedPort(fixture.postgresContainerId, POSTGRES_PORT)
|
|
112
|
-
|
|
113
|
-
const runOutput = await docker([
|
|
114
|
-
'run',
|
|
115
|
-
'-d',
|
|
116
|
-
'-p',
|
|
117
|
-
'127.0.0.1:0:8080',
|
|
118
|
-
'--add-host',
|
|
119
|
-
'host.docker.internal:host-gateway',
|
|
120
|
-
'-e', 'BREEZ_LNURL_NETWORK=regtest',
|
|
121
|
-
'-e', 'BREEZ_LNURL_AUTO_MIGRATE=true',
|
|
122
|
-
'-e', `BREEZ_LNURL_DB_URL=postgres://postgres:postgres@host.docker.internal:${dbPort}/postgres`,
|
|
123
|
-
'-e', 'BREEZ_LNURL_LOG_LEVEL=lnurl=trace,info',
|
|
124
|
-
'-e', 'BREEZ_LNURL_DOMAINS=',
|
|
125
|
-
'-e', 'BREEZ_LNURL_SCHEME=http',
|
|
126
|
-
'-e', 'BREEZ_LNURL_NSEC=nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsmhltgl',
|
|
127
|
-
'-e', 'BREEZ_LNURL_MIN_SENDABLE=1000',
|
|
128
|
-
'-e', 'BREEZ_LNURL_MAX_SENDABLE=1000000000',
|
|
129
|
-
'-e', 'BREEZ_LNURL_DEV_DONT_USE_LNURL_INCLUDE_SPARK_ADDRESS=false',
|
|
130
|
-
IMAGE
|
|
131
|
-
])
|
|
132
|
-
fixture.containerId = runOutput.trim()
|
|
133
|
-
|
|
134
|
-
const port = await publishedPort(fixture.containerId, HTTP_PORT)
|
|
135
|
-
await fixture.waitForHttp(port)
|
|
136
|
-
fixture.httpUrl = `http://127.0.0.1:${port}`
|
|
137
|
-
return fixture
|
|
138
|
-
} catch (e) {
|
|
139
|
-
fixture.stop()
|
|
140
|
-
throw e
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Readiness cannot be taken from the logs: the server's startup line is
|
|
146
|
-
* printed before it migrates the database and binds, and it logs nothing
|
|
147
|
-
* afterwards. Nor is reaching the port enough, since docker's proxy accepts
|
|
148
|
-
* connections there from the moment the container starts and closes them
|
|
149
|
-
* again while nothing is listening inside.
|
|
150
|
-
*/
|
|
151
|
-
async waitForHttp(port) {
|
|
152
|
-
const deadline = Date.now() + START_TIMEOUT_MS
|
|
153
|
-
for (;;) {
|
|
154
|
-
try {
|
|
155
|
-
await httpResponds(port)
|
|
156
|
-
return
|
|
157
|
-
} catch {
|
|
158
|
-
// Not serving yet.
|
|
159
|
-
}
|
|
160
|
-
await this.ensureRunning(this.containerId, 'lnurl server')
|
|
161
|
-
if (Date.now() >= deadline) {
|
|
162
|
-
const logs = await this.logs(this.containerId)
|
|
163
|
-
throw new Error(`lnurl server did not serve HTTP within ${START_TIMEOUT_MS}ms:\n${logs}`)
|
|
164
|
-
}
|
|
165
|
-
await new Promise((resolve) => setTimeout(resolve, 250))
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async waitForPostgres() {
|
|
170
|
-
const deadline = Date.now() + START_TIMEOUT_MS
|
|
171
|
-
for (;;) {
|
|
172
|
-
try {
|
|
173
|
-
// Probe TCP explicitly: while initialising, the image's entrypoint
|
|
174
|
-
// runs a temporary socket-only server that the default `pg_isready`
|
|
175
|
-
// check would accept as ready.
|
|
176
|
-
await docker([
|
|
177
|
-
'exec', this.postgresContainerId,
|
|
178
|
-
'pg_isready', '-h', '127.0.0.1', '-U', 'postgres'
|
|
179
|
-
])
|
|
180
|
-
return
|
|
181
|
-
} catch {
|
|
182
|
-
// Not accepting connections yet.
|
|
183
|
-
}
|
|
184
|
-
await this.ensureRunning(this.postgresContainerId, 'postgres')
|
|
185
|
-
if (Date.now() >= deadline) {
|
|
186
|
-
throw new Error(`postgres did not accept connections within ${START_TIMEOUT_MS}ms`)
|
|
187
|
-
}
|
|
188
|
-
await new Promise((resolve) => setTimeout(resolve, 250))
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Fail with the container's logs once it has exited. A container that dies
|
|
194
|
-
* on startup otherwise surfaces only as a readiness timeout, or as an
|
|
195
|
-
* unrelated docker error, neither of which names the real cause.
|
|
196
|
-
*/
|
|
197
|
-
async ensureRunning(containerId, name) {
|
|
198
|
-
const state = await docker(['inspect', '-f', '{{.State.Running}}', containerId])
|
|
199
|
-
if (state.trim() === 'true') {
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
throw new Error(`${name} container exited before it was ready:\n${await this.logs(containerId)}`)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async logs(containerId) {
|
|
206
|
-
try {
|
|
207
|
-
// docker logs writes to both stdout and stderr; check both.
|
|
208
|
-
const { stdout, stderr } = await execFileAsync('docker', ['logs', containerId])
|
|
209
|
-
return stdout + stderr
|
|
210
|
-
} catch (e) {
|
|
211
|
-
return `failed to read container logs: ${e.message}`
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
stop() {
|
|
216
|
-
for (const containerId of [this.containerId, this.postgresContainerId]) {
|
|
217
|
-
if (!containerId) {
|
|
218
|
-
continue
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
execFileSync('docker', ['rm', '-f', containerId], { stdio: 'ignore' })
|
|
222
|
-
} catch {
|
|
223
|
-
// Container may already be gone.
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
module.exports = { LnurlFixture, dockerAvailable }
|