@breeztech/breez-sdk-spark 0.20.0-dev1 → 0.21.1
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/breez-sdk-spark.tgz +0 -0
- package/bundler/breez_sdk_spark_wasm.d.ts +69 -11
- package/bundler/breez_sdk_spark_wasm_bg.js +44 -38
- package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +3 -0
- package/deno/breez_sdk_spark_wasm.d.ts +69 -11
- package/deno/breez_sdk_spark_wasm.js +44 -38
- package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +3 -0
- package/nodejs/breez_sdk_spark_wasm.d.ts +69 -11
- package/nodejs/breez_sdk_spark_wasm.js +44 -38
- package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +3 -0
- package/nodejs/mysql-token-store/index.cjs +302 -205
- package/nodejs/mysql-tree-store/index.cjs +43 -24
- package/nodejs/postgres-token-store/index.cjs +284 -205
- package/nodejs/postgres-tree-store/index.cjs +33 -17
- package/package.json +1 -1
- package/web/breez_sdk_spark_wasm.d.ts +72 -11
- package/web/breez_sdk_spark_wasm.js +44 -38
- package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +3 -0
- package/web/passkey-prf-provider/index.d.ts +9 -2
- package/web/passkey-prf-provider/index.js +49 -11
|
@@ -66,6 +66,60 @@ function buildPlaceholders(n) {
|
|
|
66
66
|
return new Array(n).fill("?").join(", ");
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function buildPairPlaceholders(n) {
|
|
70
|
+
return new Array(n).fill("(?, ?)").join(", ");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Groups outputs by the token each one names, keeping first-seen token order.
|
|
75
|
+
* @param {Array<Object>} outputs
|
|
76
|
+
* @returns {Array<[string, Array<Object>]>}
|
|
77
|
+
*/
|
|
78
|
+
function _groupOutputsByToken(outputs) {
|
|
79
|
+
const grouped = new Map();
|
|
80
|
+
for (const output of outputs ?? []) {
|
|
81
|
+
const identifier = output.output.tokenIdentifier;
|
|
82
|
+
if (!grouped.has(identifier)) {
|
|
83
|
+
grouped.set(identifier, []);
|
|
84
|
+
}
|
|
85
|
+
grouped.get(identifier).push(output);
|
|
86
|
+
}
|
|
87
|
+
return Array.from(grouped.entries());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The metadata entry describing `identifier`, or null when absent.
|
|
92
|
+
*/
|
|
93
|
+
function _metadataFor(metadata, identifier) {
|
|
94
|
+
return (metadata ?? []).find((m) => m.identifier === identifier) ?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The distinct token identifiers the outputs belong to, in first-seen order. */
|
|
98
|
+
function _tokenIdentifiersOf(outputs) {
|
|
99
|
+
return Array.from(new Set(outputs.map((o) => o.output.tokenIdentifier)));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Rejects reservation targets that can never be satisfied.
|
|
104
|
+
* @param {Array<[string, {type: string, value: string|number}]>} targets
|
|
105
|
+
*/
|
|
106
|
+
function _validateTargets(targets) {
|
|
107
|
+
if (!targets || targets.length === 0) {
|
|
108
|
+
throw new TokenStoreError("No reservation targets provided");
|
|
109
|
+
}
|
|
110
|
+
for (const [, target] of targets) {
|
|
111
|
+
if (
|
|
112
|
+
target.type === "minTotalValue" &&
|
|
113
|
+
(!target.value || target.value === "0")
|
|
114
|
+
) {
|
|
115
|
+
throw new TokenStoreError("Amount to reserve must be greater than zero");
|
|
116
|
+
}
|
|
117
|
+
if (target.type === "maxOutputCount" && !target.value) {
|
|
118
|
+
throw new TokenStoreError("Count to reserve must be greater than zero");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
69
123
|
class MysqlTokenStore {
|
|
70
124
|
/**
|
|
71
125
|
* @param {import('mysql2/promise').Pool} pool
|
|
@@ -190,6 +244,13 @@ class MysqlTokenStore {
|
|
|
190
244
|
|
|
191
245
|
// ===== TokenOutputStore Methods =====
|
|
192
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Set the full set of token outputs, reconciling reservations.
|
|
249
|
+
* @param {{metadata: Array<Object>, outputs: Array<Object>}} tokenOutputs - A flat
|
|
250
|
+
* output list that may span several tokens, plus one metadata entry per token
|
|
251
|
+
* the outputs name.
|
|
252
|
+
* @param {number} refreshStartedAtMs - Milliseconds since epoch when the refresh started
|
|
253
|
+
*/
|
|
193
254
|
async setTokensOutputs(tokenOutputs, refreshStartedAtMs) {
|
|
194
255
|
try {
|
|
195
256
|
const refreshTimestamp = new Date(refreshStartedAtMs);
|
|
@@ -238,12 +299,9 @@ class MysqlTokenStore {
|
|
|
238
299
|
[this.identity, refreshTimestamp]
|
|
239
300
|
);
|
|
240
301
|
|
|
241
|
-
const incomingOutpoints = new Set(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
incomingOutpoints.add(`${o.prevTxHash}:${o.prevTxVout}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
302
|
+
const incomingOutpoints = new Set(
|
|
303
|
+
tokenOutputs.outputs.map((o) => `${o.prevTxHash}:${o.prevTxVout}`)
|
|
304
|
+
);
|
|
247
305
|
|
|
248
306
|
const [reservedRows] = await conn.query(
|
|
249
307
|
`SELECT r.id, o.prev_tx_hash, o.prev_tx_vout
|
|
@@ -339,19 +397,25 @@ class MysqlTokenStore {
|
|
|
339
397
|
[this.identity, this.identity]
|
|
340
398
|
);
|
|
341
399
|
|
|
342
|
-
for (const
|
|
343
|
-
|
|
400
|
+
for (const [tokenIdentifier, outputs] of _groupOutputsByToken(
|
|
401
|
+
tokenOutputs.outputs
|
|
402
|
+
)) {
|
|
403
|
+
const metadata = _metadataFor(tokenOutputs.metadata, tokenIdentifier);
|
|
404
|
+
if (!metadata) {
|
|
405
|
+
this._log(
|
|
406
|
+
"warn",
|
|
407
|
+
`Skipping outputs of token ${tokenIdentifier}: no metadata provided`
|
|
408
|
+
);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
await this._upsertMetadata(conn, metadata);
|
|
344
412
|
|
|
345
|
-
for (const output of
|
|
413
|
+
for (const output of outputs) {
|
|
346
414
|
const outpoint = `${output.prevTxHash}:${output.prevTxVout}`;
|
|
347
415
|
if (reservedOutpoints.has(outpoint) || spentOutpoints.has(outpoint)) {
|
|
348
416
|
continue;
|
|
349
417
|
}
|
|
350
|
-
await this._insertSingleOutput(
|
|
351
|
-
conn,
|
|
352
|
-
to.metadata.identifier,
|
|
353
|
-
output
|
|
354
|
-
);
|
|
418
|
+
await this._insertSingleOutput(conn, tokenIdentifier, output);
|
|
355
419
|
}
|
|
356
420
|
}
|
|
357
421
|
});
|
|
@@ -547,7 +611,9 @@ class MysqlTokenStore {
|
|
|
547
611
|
/**
|
|
548
612
|
* Atomically remove spent outputs and insert new outputs.
|
|
549
613
|
* @param {Array<[string, number]>} outputsToRemove - Array of [prevTxHash, prevTxVout] tuples
|
|
550
|
-
* @param {Object
|
|
614
|
+
* @param {{metadata: Array<Object>, outputs: Array<Object>}} outputsToAdd - A flat
|
|
615
|
+
* output list that may span several tokens, plus one metadata entry per token
|
|
616
|
+
* the outputs name.
|
|
551
617
|
* @returns {Promise<void>}
|
|
552
618
|
*/
|
|
553
619
|
async updateTokenOutputs(outputsToRemove, outputsToAdd) {
|
|
@@ -571,30 +637,34 @@ class MysqlTokenStore {
|
|
|
571
637
|
}
|
|
572
638
|
}
|
|
573
639
|
|
|
574
|
-
// 2. Insert new outputs.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
for (const o of outputsToAdd.outputs) {
|
|
584
|
-
params.push(o.prevTxHash, o.prevTxVout);
|
|
585
|
-
}
|
|
586
|
-
await conn.query(
|
|
587
|
-
`DELETE FROM brz_token_spent_outputs WHERE user_id = ? AND (prev_tx_hash, prev_tx_vout) IN (${pairPlaceholders})`,
|
|
588
|
-
params
|
|
640
|
+
// 2. Insert new outputs, grouped by the token each one names.
|
|
641
|
+
for (const [tokenIdentifier, outputs] of _groupOutputsByToken(
|
|
642
|
+
outputsToAdd?.outputs
|
|
643
|
+
)) {
|
|
644
|
+
const metadata = _metadataFor(outputsToAdd.metadata, tokenIdentifier);
|
|
645
|
+
if (!metadata) {
|
|
646
|
+
this._log(
|
|
647
|
+
"warn",
|
|
648
|
+
`Skipping outputs of token ${tokenIdentifier}: no metadata provided`
|
|
589
649
|
);
|
|
650
|
+
continue;
|
|
590
651
|
}
|
|
652
|
+
await this._upsertMetadata(conn, metadata);
|
|
591
653
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
654
|
+
// Clear spent status for outputs being (re-)added.
|
|
655
|
+
const params = [this.identity];
|
|
656
|
+
for (const o of outputs) {
|
|
657
|
+
params.push(o.prevTxHash, o.prevTxVout);
|
|
658
|
+
}
|
|
659
|
+
await conn.query(
|
|
660
|
+
`DELETE FROM brz_token_spent_outputs WHERE user_id = ? AND (prev_tx_hash, prev_tx_vout) IN (${buildPairPlaceholders(
|
|
661
|
+
outputs.length
|
|
662
|
+
)})`,
|
|
663
|
+
params
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
for (const output of outputs) {
|
|
667
|
+
await this._insertSingleOutput(conn, tokenIdentifier, output);
|
|
598
668
|
}
|
|
599
669
|
}
|
|
600
670
|
});
|
|
@@ -607,98 +677,42 @@ class MysqlTokenStore {
|
|
|
607
677
|
}
|
|
608
678
|
}
|
|
609
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Reserve token outputs for a payment or swap.
|
|
682
|
+
*
|
|
683
|
+
* Selection and reservation of every target share one transaction and the
|
|
684
|
+
* write lock it holds, so a multi-token reservation is all-or-nothing: no
|
|
685
|
+
* concurrent writer can take an output between two targets being selected.
|
|
686
|
+
* @param {Array<[string, {type: string, value: string|number}]>} targets - One
|
|
687
|
+
* [tokenIdentifier, target] pair per token to reserve for, target being
|
|
688
|
+
* MinTotalValue or MaxOutputCount
|
|
689
|
+
* @param {string} purpose - "Payment" or "Swap"
|
|
690
|
+
* @param {Array|null} preferredOutputs
|
|
691
|
+
* @param {string|null} selectionStrategy - "SmallestFirst" or "LargestFirst"
|
|
692
|
+
* @returns {Promise<{id: string, tokenOutputs: {metadata: Array<Object>, outputs: Array}}>}
|
|
693
|
+
*/
|
|
610
694
|
async reserveTokenOutputs(
|
|
611
|
-
|
|
612
|
-
target,
|
|
695
|
+
targets,
|
|
613
696
|
purpose,
|
|
614
697
|
preferredOutputs,
|
|
615
698
|
selectionStrategy
|
|
616
699
|
) {
|
|
617
700
|
try {
|
|
618
|
-
|
|
619
|
-
if (
|
|
620
|
-
target.type === "minTotalValue" &&
|
|
621
|
-
(!target.value || target.value === "0")
|
|
622
|
-
) {
|
|
623
|
-
throw new TokenStoreError(
|
|
624
|
-
"Amount to reserve must be greater than zero"
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
if (
|
|
628
|
-
target.type === "maxOutputCount" &&
|
|
629
|
-
(!target.value || target.value === 0)
|
|
630
|
-
) {
|
|
631
|
-
throw new TokenStoreError(
|
|
632
|
-
"Count to reserve must be greater than zero"
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const [metadataRows] = await conn.query(
|
|
637
|
-
"SELECT * FROM brz_token_metadata WHERE user_id = ? AND identifier = ?",
|
|
638
|
-
[this.identity, tokenIdentifier]
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
if (metadataRows.length === 0) {
|
|
642
|
-
throw new TokenStoreError(
|
|
643
|
-
`Token outputs not found for identifier: ${tokenIdentifier}`
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const metadata = this._metadataFromRow(metadataRows[0]);
|
|
648
|
-
|
|
649
|
-
const [outputRows] = await conn.query(
|
|
650
|
-
`SELECT o.owner_public_key, o.revocation_commitment,
|
|
651
|
-
o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
|
|
652
|
-
o.token_public_key, o.token_amount, o.token_identifier,
|
|
653
|
-
o.prev_tx_hash, o.prev_tx_vout
|
|
654
|
-
FROM brz_token_outputs o
|
|
655
|
-
WHERE o.user_id = ? AND o.token_identifier = ? AND o.reservation_id IS NULL`,
|
|
656
|
-
[this.identity, tokenIdentifier]
|
|
657
|
-
);
|
|
658
|
-
|
|
659
|
-
let outputs = outputRows.map((row) => this._outputFromRow(row));
|
|
660
|
-
|
|
661
|
-
if (preferredOutputs) {
|
|
662
|
-
const preferredOutpoints = new Set(
|
|
663
|
-
preferredOutputs.map((p) => `${p.prevTxHash}:${p.prevTxVout}`)
|
|
664
|
-
);
|
|
665
|
-
outputs = outputs.filter((o) =>
|
|
666
|
-
preferredOutpoints.has(`${o.prevTxHash}:${o.prevTxVout}`)
|
|
667
|
-
);
|
|
668
|
-
}
|
|
701
|
+
_validateTargets(targets);
|
|
669
702
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
703
|
+
return await this._withWriteTransaction(async (conn) => {
|
|
704
|
+
const selected = await this._selectForTargets(
|
|
705
|
+
conn,
|
|
706
|
+
targets,
|
|
707
|
+
preferredOutputs,
|
|
673
708
|
selectionStrategy
|
|
674
709
|
);
|
|
675
710
|
|
|
676
711
|
const reservationId = this._generateId();
|
|
712
|
+
await this._insertReservation(conn, reservationId, purpose);
|
|
713
|
+
await this._assignReservation(conn, reservationId, selected.outputs);
|
|
677
714
|
|
|
678
|
-
|
|
679
|
-
"INSERT INTO brz_token_reservations (user_id, id, purpose, created_at) VALUES (?, ?, ?, UTC_TIMESTAMP(6))",
|
|
680
|
-
[this.identity, reservationId, purpose]
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
if (selectedOutputs.length > 0) {
|
|
684
|
-
const pairPlaceholders = selectedOutputs
|
|
685
|
-
.map(() => "(?, ?)")
|
|
686
|
-
.join(", ");
|
|
687
|
-
const params = [reservationId, this.identity];
|
|
688
|
-
for (const o of selectedOutputs) {
|
|
689
|
-
params.push(o.prevTxHash, o.prevTxVout);
|
|
690
|
-
}
|
|
691
|
-
await conn.query(
|
|
692
|
-
`UPDATE brz_token_outputs SET reservation_id = ? WHERE user_id = ?
|
|
693
|
-
AND (prev_tx_hash, prev_tx_vout) IN (${pairPlaceholders})`,
|
|
694
|
-
params
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return {
|
|
699
|
-
id: reservationId,
|
|
700
|
-
tokenOutputs: { metadata, outputs: selectedOutputs },
|
|
701
|
-
};
|
|
715
|
+
return { id: reservationId, tokenOutputs: selected };
|
|
702
716
|
});
|
|
703
717
|
} catch (error) {
|
|
704
718
|
if (error instanceof TokenStoreError) throw error;
|
|
@@ -709,7 +723,7 @@ class MysqlTokenStore {
|
|
|
709
723
|
}
|
|
710
724
|
}
|
|
711
725
|
|
|
712
|
-
_selectOutputs(outputs, target, selectionStrategy) {
|
|
726
|
+
_selectOutputs(tokenIdentifier, outputs, target, selectionStrategy) {
|
|
713
727
|
if (target.type === "minTotalValue") {
|
|
714
728
|
const amount = BigInt(target.value);
|
|
715
729
|
const totalAvailable = outputs.reduce(
|
|
@@ -717,7 +731,9 @@ class MysqlTokenStore {
|
|
|
717
731
|
0n
|
|
718
732
|
);
|
|
719
733
|
if (totalAvailable < amount) {
|
|
720
|
-
throw new TokenStoreError(
|
|
734
|
+
throw new TokenStoreError(
|
|
735
|
+
`InsufficientFunds: ${tokenIdentifier}`
|
|
736
|
+
);
|
|
721
737
|
}
|
|
722
738
|
|
|
723
739
|
const exactMatch = outputs.find(
|
|
@@ -747,7 +763,9 @@ class MysqlTokenStore {
|
|
|
747
763
|
remaining -= BigInt(output.output.tokenAmount);
|
|
748
764
|
}
|
|
749
765
|
if (remaining > 0n) {
|
|
750
|
-
throw new TokenStoreError(
|
|
766
|
+
throw new TokenStoreError(
|
|
767
|
+
`InsufficientFunds: ${tokenIdentifier}`
|
|
768
|
+
);
|
|
751
769
|
}
|
|
752
770
|
return selected;
|
|
753
771
|
}
|
|
@@ -771,64 +789,24 @@ class MysqlTokenStore {
|
|
|
771
789
|
throw new TokenStoreError(`Unknown target type: ${target.type}`);
|
|
772
790
|
}
|
|
773
791
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
792
|
+
/**
|
|
793
|
+
* Select outputs covering every target, without reserving them.
|
|
794
|
+
* @param {Array<[string, {type: string, value: string|number}]>} targets - One
|
|
795
|
+
* [tokenIdentifier, target] pair per token to select for
|
|
796
|
+
* @param {Array|null} preferredOutputs
|
|
797
|
+
* @param {string|null} selectionStrategy - "SmallestFirst" or "LargestFirst"
|
|
798
|
+
* @returns {Promise<{metadata: Array<Object>, outputs: Array}>}
|
|
799
|
+
*/
|
|
800
|
+
async selectTokenOutputs(targets, preferredOutputs, selectionStrategy) {
|
|
780
801
|
try {
|
|
781
|
-
|
|
782
|
-
target.type === "minTotalValue" &&
|
|
783
|
-
(!target.value || target.value === "0")
|
|
784
|
-
) {
|
|
785
|
-
throw new TokenStoreError("Amount to reserve must be greater than zero");
|
|
786
|
-
}
|
|
787
|
-
if (
|
|
788
|
-
target.type === "maxOutputCount" &&
|
|
789
|
-
(!target.value || target.value === 0)
|
|
790
|
-
) {
|
|
791
|
-
throw new TokenStoreError("Count to reserve must be greater than zero");
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const [metadataRows] = await this.pool.query(
|
|
795
|
-
"SELECT * FROM brz_token_metadata WHERE user_id = ? AND identifier = ?",
|
|
796
|
-
[this.identity, tokenIdentifier]
|
|
797
|
-
);
|
|
798
|
-
if (metadataRows.length === 0) {
|
|
799
|
-
throw new TokenStoreError(
|
|
800
|
-
`Token outputs not found for identifier: ${tokenIdentifier}`
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
const metadata = this._metadataFromRow(metadataRows[0]);
|
|
804
|
-
|
|
805
|
-
const [outputRows] = await this.pool.query(
|
|
806
|
-
`SELECT o.owner_public_key, o.revocation_commitment,
|
|
807
|
-
o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
|
|
808
|
-
o.token_public_key, o.token_amount, o.token_identifier,
|
|
809
|
-
o.prev_tx_hash, o.prev_tx_vout
|
|
810
|
-
FROM brz_token_outputs o
|
|
811
|
-
WHERE o.user_id = ? AND o.token_identifier = ? AND o.reservation_id IS NULL`,
|
|
812
|
-
[this.identity, tokenIdentifier]
|
|
813
|
-
);
|
|
814
|
-
|
|
815
|
-
let outputs = outputRows.map((row) => this._outputFromRow(row));
|
|
816
|
-
|
|
817
|
-
if (preferredOutputs) {
|
|
818
|
-
const preferredOutpoints = new Set(
|
|
819
|
-
preferredOutputs.map((p) => `${p.prevTxHash}:${p.prevTxVout}`)
|
|
820
|
-
);
|
|
821
|
-
outputs = outputs.filter((o) =>
|
|
822
|
-
preferredOutpoints.has(`${o.prevTxHash}:${o.prevTxVout}`)
|
|
823
|
-
);
|
|
824
|
-
}
|
|
802
|
+
_validateTargets(targets);
|
|
825
803
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
804
|
+
return await this._selectForTargets(
|
|
805
|
+
this.pool,
|
|
806
|
+
targets,
|
|
807
|
+
preferredOutputs,
|
|
829
808
|
selectionStrategy
|
|
830
809
|
);
|
|
831
|
-
return { metadata, outputs: selectedOutputs };
|
|
832
810
|
} catch (error) {
|
|
833
811
|
if (error instanceof TokenStoreError) throw error;
|
|
834
812
|
throw new TokenStoreError(
|
|
@@ -838,36 +816,33 @@ class MysqlTokenStore {
|
|
|
838
816
|
}
|
|
839
817
|
}
|
|
840
818
|
|
|
841
|
-
|
|
819
|
+
/**
|
|
820
|
+
* Reserve the outputs at the given outpoints, which may belong to several tokens.
|
|
821
|
+
* @param {Array<{prevTxHash: string, prevTxVout: number}>} outpoints
|
|
822
|
+
* @param {string} purpose - "Payment" or "Swap"
|
|
823
|
+
* @returns {Promise<{id: string, tokenOutputs: {metadata: Array<Object>, outputs: Array}}>}
|
|
824
|
+
*/
|
|
825
|
+
async reserveTokenOutputsByOutpoints(outpoints, purpose) {
|
|
842
826
|
try {
|
|
843
827
|
if (!outpoints || outpoints.length === 0) {
|
|
844
828
|
throw new TokenStoreError("No outpoints provided");
|
|
845
829
|
}
|
|
846
830
|
return await this._withWriteTransaction(async (conn) => {
|
|
847
|
-
const
|
|
848
|
-
"SELECT * FROM brz_token_metadata WHERE user_id = ? AND identifier = ?",
|
|
849
|
-
[this.identity, tokenIdentifier]
|
|
850
|
-
);
|
|
851
|
-
if (metadataRows.length === 0) {
|
|
852
|
-
throw new TokenStoreError(
|
|
853
|
-
`Token outputs not found for identifier: ${tokenIdentifier}`
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
const metadata = this._metadataFromRow(metadataRows[0]);
|
|
857
|
-
|
|
858
|
-
const pairPlaceholders = outpoints.map(() => "(?, ?)").join(", ");
|
|
859
|
-
const selectParams = [this.identity, tokenIdentifier];
|
|
831
|
+
const selectParams = [this.identity];
|
|
860
832
|
for (const o of outpoints) {
|
|
861
833
|
selectParams.push(o.prevTxHash, o.prevTxVout);
|
|
862
834
|
}
|
|
835
|
+
// The outpoints are not scoped to a token: they may belong to several.
|
|
863
836
|
const [outputRows] = await conn.query(
|
|
864
837
|
`SELECT o.owner_public_key, o.revocation_commitment,
|
|
865
838
|
o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
|
|
866
839
|
o.token_public_key, o.token_amount, o.token_identifier,
|
|
867
840
|
o.prev_tx_hash, o.prev_tx_vout
|
|
868
841
|
FROM brz_token_outputs o
|
|
869
|
-
WHERE o.user_id = ? AND o.
|
|
870
|
-
AND (o.prev_tx_hash, o.prev_tx_vout) IN (${
|
|
842
|
+
WHERE o.user_id = ? AND o.reservation_id IS NULL
|
|
843
|
+
AND (o.prev_tx_hash, o.prev_tx_vout) IN (${buildPairPlaceholders(
|
|
844
|
+
outpoints.length
|
|
845
|
+
)})`,
|
|
871
846
|
selectParams
|
|
872
847
|
);
|
|
873
848
|
|
|
@@ -882,24 +857,14 @@ class MysqlTokenStore {
|
|
|
882
857
|
throw new TokenStoreError("InsufficientFunds");
|
|
883
858
|
}
|
|
884
859
|
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
[this.identity, reservationId, purpose]
|
|
860
|
+
const metadata = await this._fetchMetadata(
|
|
861
|
+
conn,
|
|
862
|
+
_tokenIdentifiersOf(selectedOutputs)
|
|
889
863
|
);
|
|
890
864
|
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const updateParams = [reservationId, this.identity];
|
|
895
|
-
for (const o of selectedOutputs) {
|
|
896
|
-
updateParams.push(o.prevTxHash, o.prevTxVout);
|
|
897
|
-
}
|
|
898
|
-
await conn.query(
|
|
899
|
-
`UPDATE brz_token_outputs SET reservation_id = ? WHERE user_id = ?
|
|
900
|
-
AND (prev_tx_hash, prev_tx_vout) IN (${updatePlaceholders})`,
|
|
901
|
-
updateParams
|
|
902
|
-
);
|
|
865
|
+
const reservationId = this._generateId();
|
|
866
|
+
await this._insertReservation(conn, reservationId, purpose);
|
|
867
|
+
await this._assignReservation(conn, reservationId, selectedOutputs);
|
|
903
868
|
|
|
904
869
|
return {
|
|
905
870
|
id: reservationId,
|
|
@@ -1029,6 +994,138 @@ class MysqlTokenStore {
|
|
|
1029
994
|
|
|
1030
995
|
// ===== Private Helpers =====
|
|
1031
996
|
|
|
997
|
+
_log(level, message) {
|
|
998
|
+
if (this.logger && typeof this.logger.log === "function") {
|
|
999
|
+
this.logger.log({ line: message, level });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Select available outputs covering every target, without reserving them.
|
|
1005
|
+
*
|
|
1006
|
+
* The result carries metadata for every requested token. A target whose token
|
|
1007
|
+
* is unknown to this tenant is an error, and a target that cannot be covered
|
|
1008
|
+
* by the available outputs yields `InsufficientFunds`.
|
|
1009
|
+
* @param {import('mysql2/promise').PoolConnection|import('mysql2/promise').Pool} conn
|
|
1010
|
+
* @returns {Promise<{metadata: Array<Object>, outputs: Array}>}
|
|
1011
|
+
*/
|
|
1012
|
+
async _selectForTargets(conn, targets, preferredOutputs, selectionStrategy) {
|
|
1013
|
+
const tokenIdentifiers = Array.from(new Set(targets.map(([id]) => id)));
|
|
1014
|
+
|
|
1015
|
+
const metadata = await this._fetchMetadata(conn, tokenIdentifiers);
|
|
1016
|
+
for (const identifier of tokenIdentifiers) {
|
|
1017
|
+
if (!metadata.some((m) => m.identifier === identifier)) {
|
|
1018
|
+
throw new TokenStoreError(
|
|
1019
|
+
`Token outputs not found for identifier: ${identifier}`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const [outputRows] = await conn.query(
|
|
1025
|
+
`SELECT o.owner_public_key, o.revocation_commitment,
|
|
1026
|
+
o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
|
|
1027
|
+
o.token_public_key, o.token_amount, o.token_identifier,
|
|
1028
|
+
o.prev_tx_hash, o.prev_tx_vout
|
|
1029
|
+
FROM brz_token_outputs o
|
|
1030
|
+
WHERE o.user_id = ?
|
|
1031
|
+
AND o.token_identifier IN (${buildPlaceholders(
|
|
1032
|
+
tokenIdentifiers.length
|
|
1033
|
+
)})
|
|
1034
|
+
AND o.reservation_id IS NULL`,
|
|
1035
|
+
[this.identity, ...tokenIdentifiers]
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
let available = outputRows.map((row) => this._outputFromRow(row));
|
|
1039
|
+
|
|
1040
|
+
if (preferredOutputs) {
|
|
1041
|
+
const preferredOutpoints = new Set(
|
|
1042
|
+
preferredOutputs.map((p) => `${p.prevTxHash}:${p.prevTxVout}`)
|
|
1043
|
+
);
|
|
1044
|
+
available = available.filter((o) =>
|
|
1045
|
+
preferredOutpoints.has(`${o.prevTxHash}:${o.prevTxVout}`)
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const availablePerToken = new Map();
|
|
1050
|
+
for (const output of available) {
|
|
1051
|
+
const identifier = output.output.tokenIdentifier;
|
|
1052
|
+
if (!availablePerToken.has(identifier)) {
|
|
1053
|
+
availablePerToken.set(identifier, []);
|
|
1054
|
+
}
|
|
1055
|
+
availablePerToken.get(identifier).push(output);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const outputs = [];
|
|
1059
|
+
for (const [tokenIdentifier, target] of targets) {
|
|
1060
|
+
const candidates = availablePerToken.get(tokenIdentifier) ?? [];
|
|
1061
|
+
const selected = this._selectOutputs(
|
|
1062
|
+
tokenIdentifier,
|
|
1063
|
+
candidates,
|
|
1064
|
+
target,
|
|
1065
|
+
selectionStrategy
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
// Outputs picked for one target are withheld from the next, so repeated
|
|
1069
|
+
// entries for the same token do not select the same output twice.
|
|
1070
|
+
const taken = new Set(
|
|
1071
|
+
selected.map((o) => `${o.prevTxHash}:${o.prevTxVout}`)
|
|
1072
|
+
);
|
|
1073
|
+
availablePerToken.set(
|
|
1074
|
+
tokenIdentifier,
|
|
1075
|
+
candidates.filter((o) => !taken.has(`${o.prevTxHash}:${o.prevTxVout}`))
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
outputs.push(...selected);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
return { metadata, outputs };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Load this tenant's metadata for the given token identifiers. Unknown
|
|
1086
|
+
* identifiers are simply absent from the result.
|
|
1087
|
+
*/
|
|
1088
|
+
async _fetchMetadata(conn, tokenIdentifiers) {
|
|
1089
|
+
if (tokenIdentifiers.length === 0) {
|
|
1090
|
+
return [];
|
|
1091
|
+
}
|
|
1092
|
+
const [rows] = await conn.query(
|
|
1093
|
+
`SELECT * FROM brz_token_metadata
|
|
1094
|
+
WHERE user_id = ? AND identifier IN (${buildPlaceholders(
|
|
1095
|
+
tokenIdentifiers.length
|
|
1096
|
+
)})`,
|
|
1097
|
+
[this.identity, ...tokenIdentifiers]
|
|
1098
|
+
);
|
|
1099
|
+
return rows.map((row) => this._metadataFromRow(row));
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async _insertReservation(conn, reservationId, purpose) {
|
|
1103
|
+
await conn.query(
|
|
1104
|
+
"INSERT INTO brz_token_reservations (user_id, id, purpose, created_at) VALUES (?, ?, ?, UTC_TIMESTAMP(6))",
|
|
1105
|
+
[this.identity, reservationId, purpose]
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Point the given outputs at the reservation, addressing them by outpoint.
|
|
1111
|
+
*/
|
|
1112
|
+
async _assignReservation(conn, reservationId, outputs) {
|
|
1113
|
+
if (outputs.length === 0) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const params = [reservationId, this.identity];
|
|
1117
|
+
for (const o of outputs) {
|
|
1118
|
+
params.push(o.prevTxHash, o.prevTxVout);
|
|
1119
|
+
}
|
|
1120
|
+
await conn.query(
|
|
1121
|
+
`UPDATE brz_token_outputs SET reservation_id = ? WHERE user_id = ?
|
|
1122
|
+
AND (prev_tx_hash, prev_tx_vout) IN (${buildPairPlaceholders(
|
|
1123
|
+
outputs.length
|
|
1124
|
+
)})`,
|
|
1125
|
+
params
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1032
1129
|
_generateId() {
|
|
1033
1130
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1034
1131
|
return crypto.randomUUID();
|