@breeztech/breez-sdk-spark 0.20.0-dev1 → 0.21.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/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
|
@@ -57,6 +57,56 @@ function _identityLockKey(prefix, identity) {
|
|
|
57
57
|
return hash.digest().readBigInt64BE(0);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Groups outputs by the token each one names, keeping first-seen token order.
|
|
62
|
+
* @param {Array<Object>} outputs
|
|
63
|
+
* @returns {Array<[string, Array<Object>]>}
|
|
64
|
+
*/
|
|
65
|
+
function _groupOutputsByToken(outputs) {
|
|
66
|
+
const grouped = new Map();
|
|
67
|
+
for (const output of outputs ?? []) {
|
|
68
|
+
const identifier = output.output.tokenIdentifier;
|
|
69
|
+
if (!grouped.has(identifier)) {
|
|
70
|
+
grouped.set(identifier, []);
|
|
71
|
+
}
|
|
72
|
+
grouped.get(identifier).push(output);
|
|
73
|
+
}
|
|
74
|
+
return Array.from(grouped.entries());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The metadata entry describing `identifier`, or null when absent.
|
|
79
|
+
*/
|
|
80
|
+
function _metadataFor(metadata, identifier) {
|
|
81
|
+
return (metadata ?? []).find((m) => m.identifier === identifier) ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The distinct token identifiers the outputs belong to, in first-seen order. */
|
|
85
|
+
function _tokenIdentifiersOf(outputs) {
|
|
86
|
+
return Array.from(new Set(outputs.map((o) => o.output.tokenIdentifier)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Rejects reservation targets that can never be satisfied.
|
|
91
|
+
* @param {Array<[string, {type: string, value: string|number}]>} targets
|
|
92
|
+
*/
|
|
93
|
+
function _validateTargets(targets) {
|
|
94
|
+
if (!targets || targets.length === 0) {
|
|
95
|
+
throw new TokenStoreError("No reservation targets provided");
|
|
96
|
+
}
|
|
97
|
+
for (const [, target] of targets) {
|
|
98
|
+
if (
|
|
99
|
+
target.type === "minTotalValue" &&
|
|
100
|
+
(!target.value || target.value === "0")
|
|
101
|
+
) {
|
|
102
|
+
throw new TokenStoreError("Amount to reserve must be greater than zero");
|
|
103
|
+
}
|
|
104
|
+
if (target.type === "maxOutputCount" && !target.value) {
|
|
105
|
+
throw new TokenStoreError("Count to reserve must be greater than zero");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
60
110
|
class PostgresTokenStore {
|
|
61
111
|
/**
|
|
62
112
|
* @param {import('pg').Pool} pool
|
|
@@ -160,7 +210,9 @@ class PostgresTokenStore {
|
|
|
160
210
|
|
|
161
211
|
/**
|
|
162
212
|
* Set the full set of token outputs, reconciling reservations.
|
|
163
|
-
* @param {
|
|
213
|
+
* @param {{metadata: Array<Object>, outputs: Array<Object>}} tokenOutputs - A flat
|
|
214
|
+
* output list that may span several tokens, plus one metadata entry per token
|
|
215
|
+
* the outputs name.
|
|
164
216
|
* @param {number} refreshStartedAtMs - Milliseconds since epoch when the refresh started
|
|
165
217
|
*/
|
|
166
218
|
async setTokensOutputs(tokenOutputs, refreshStartedAtMs) {
|
|
@@ -217,12 +269,9 @@ class PostgresTokenStore {
|
|
|
217
269
|
);
|
|
218
270
|
|
|
219
271
|
// Build a set of all incoming outpoints for reconciliation
|
|
220
|
-
const incomingOutpoints = new Set(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
incomingOutpoints.add(`${o.prevTxHash}:${o.prevTxVout}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
272
|
+
const incomingOutpoints = new Set(
|
|
273
|
+
tokenOutputs.outputs.map((o) => `${o.prevTxHash}:${o.prevTxVout}`)
|
|
274
|
+
);
|
|
226
275
|
|
|
227
276
|
// Reconcile reservations: find reserved outputs that no longer exist
|
|
228
277
|
const reservedRows = await client.query(
|
|
@@ -326,19 +375,25 @@ class PostgresTokenStore {
|
|
|
326
375
|
);
|
|
327
376
|
|
|
328
377
|
// Insert new metadata and outputs, excluding spent and reserved
|
|
329
|
-
for (const
|
|
330
|
-
|
|
378
|
+
for (const [tokenIdentifier, outputs] of _groupOutputsByToken(
|
|
379
|
+
tokenOutputs.outputs
|
|
380
|
+
)) {
|
|
381
|
+
const metadata = _metadataFor(tokenOutputs.metadata, tokenIdentifier);
|
|
382
|
+
if (!metadata) {
|
|
383
|
+
this._log(
|
|
384
|
+
"warn",
|
|
385
|
+
`Skipping outputs of token ${tokenIdentifier}: no metadata provided`
|
|
386
|
+
);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
await this._upsertMetadata(client, metadata);
|
|
331
390
|
|
|
332
|
-
for (const output of
|
|
391
|
+
for (const output of outputs) {
|
|
333
392
|
const outpoint = `${output.prevTxHash}:${output.prevTxVout}`;
|
|
334
393
|
if (reservedOutpoints.has(outpoint) || spentOutpoints.has(outpoint)) {
|
|
335
394
|
continue;
|
|
336
395
|
}
|
|
337
|
-
await this._insertSingleOutput(
|
|
338
|
-
client,
|
|
339
|
-
to.metadata.identifier,
|
|
340
|
-
output
|
|
341
|
-
);
|
|
396
|
+
await this._insertSingleOutput(client, tokenIdentifier, output);
|
|
342
397
|
}
|
|
343
398
|
}
|
|
344
399
|
});
|
|
@@ -542,14 +597,12 @@ class PostgresTokenStore {
|
|
|
542
597
|
}
|
|
543
598
|
}
|
|
544
599
|
|
|
545
|
-
/**
|
|
546
|
-
* Insert token outputs (upsert metadata, insert outputs with ON CONFLICT DO NOTHING).
|
|
547
|
-
* @param {{metadata: Object, outputs: Array}} tokenOutputs
|
|
548
|
-
*/
|
|
549
600
|
/**
|
|
550
601
|
* Atomically remove spent outputs and insert new outputs.
|
|
551
602
|
* @param {Array<[string, number]>} outputsToRemove - Array of [prevTxHash, prevTxVout] tuples
|
|
552
|
-
* @param {Object
|
|
603
|
+
* @param {{metadata: Array<Object>, outputs: Array<Object>}} outputsToAdd - A flat
|
|
604
|
+
* output list that may span several tokens, plus one metadata entry per token
|
|
605
|
+
* the outputs name.
|
|
553
606
|
* @returns {Promise<void>}
|
|
554
607
|
*/
|
|
555
608
|
async updateTokenOutputs(outputsToRemove, outputsToAdd) {
|
|
@@ -573,29 +626,34 @@ class PostgresTokenStore {
|
|
|
573
626
|
}
|
|
574
627
|
}
|
|
575
628
|
|
|
576
|
-
// 2. Insert new outputs.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
`
|
|
585
|
-
WHERE user_id = $1
|
|
586
|
-
AND (prev_tx_hash, prev_tx_vout) IN (
|
|
587
|
-
SELECT * FROM UNNEST($2::text[], $3::int[])
|
|
588
|
-
)`,
|
|
589
|
-
[this.identity, txHashes, vouts]
|
|
629
|
+
// 2. Insert new outputs, grouped by the token each one names.
|
|
630
|
+
for (const [tokenIdentifier, outputs] of _groupOutputsByToken(
|
|
631
|
+
outputsToAdd?.outputs
|
|
632
|
+
)) {
|
|
633
|
+
const metadata = _metadataFor(outputsToAdd.metadata, tokenIdentifier);
|
|
634
|
+
if (!metadata) {
|
|
635
|
+
this._log(
|
|
636
|
+
"warn",
|
|
637
|
+
`Skipping outputs of token ${tokenIdentifier}: no metadata provided`
|
|
590
638
|
);
|
|
639
|
+
continue;
|
|
591
640
|
}
|
|
641
|
+
await this._upsertMetadata(client, metadata);
|
|
592
642
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
643
|
+
// Clear spent status for outputs being (re-)added.
|
|
644
|
+
const txHashes = outputs.map((o) => o.prevTxHash);
|
|
645
|
+
const vouts = outputs.map((o) => o.prevTxVout);
|
|
646
|
+
await client.query(
|
|
647
|
+
`DELETE FROM brz_token_spent_outputs
|
|
648
|
+
WHERE user_id = $1
|
|
649
|
+
AND (prev_tx_hash, prev_tx_vout) IN (
|
|
650
|
+
SELECT * FROM UNNEST($2::text[], $3::int[])
|
|
651
|
+
)`,
|
|
652
|
+
[this.identity, txHashes, vouts]
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
for (const output of outputs) {
|
|
656
|
+
await this._insertSingleOutput(client, tokenIdentifier, output);
|
|
599
657
|
}
|
|
600
658
|
}
|
|
601
659
|
});
|
|
@@ -610,108 +668,44 @@ class PostgresTokenStore {
|
|
|
610
668
|
|
|
611
669
|
/**
|
|
612
670
|
* Reserve token outputs for a payment or swap.
|
|
613
|
-
*
|
|
614
|
-
*
|
|
671
|
+
*
|
|
672
|
+
* Selection and reservation of every target share one transaction and the
|
|
673
|
+
* write lock it holds, so a multi-token reservation is all-or-nothing: no
|
|
674
|
+
* concurrent writer can take an output between two targets being selected.
|
|
675
|
+
* @param {Array<[string, {type: string, value: string|number}]>} targets - One
|
|
676
|
+
* [tokenIdentifier, target] pair per token to reserve for, target being
|
|
677
|
+
* MinTotalValue or MaxOutputCount
|
|
615
678
|
* @param {string} purpose - "Payment" or "Swap"
|
|
616
679
|
* @param {Array|null} preferredOutputs
|
|
617
680
|
* @param {string|null} selectionStrategy - "SmallestFirst" or "LargestFirst"
|
|
618
|
-
* @returns {Promise<{id: string, tokenOutputs: {metadata: Object
|
|
681
|
+
* @returns {Promise<{id: string, tokenOutputs: {metadata: Array<Object>, outputs: Array}}>}
|
|
619
682
|
*/
|
|
620
683
|
async reserveTokenOutputs(
|
|
621
|
-
|
|
622
|
-
target,
|
|
684
|
+
targets,
|
|
623
685
|
purpose,
|
|
624
686
|
preferredOutputs,
|
|
625
687
|
selectionStrategy
|
|
626
688
|
) {
|
|
627
689
|
try {
|
|
628
|
-
|
|
629
|
-
// Validate target
|
|
630
|
-
if (target.type === "minTotalValue" && (!target.value || target.value === "0")) {
|
|
631
|
-
throw new TokenStoreError(
|
|
632
|
-
"Amount to reserve must be greater than zero"
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
if (target.type === "maxOutputCount" && (!target.value || target.value === 0)) {
|
|
636
|
-
throw new TokenStoreError(
|
|
637
|
-
"Count to reserve must be greater than zero"
|
|
638
|
-
);
|
|
639
|
-
}
|
|
690
|
+
_validateTargets(targets);
|
|
640
691
|
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (metadataResult.rows.length === 0) {
|
|
648
|
-
throw new TokenStoreError(
|
|
649
|
-
`Token outputs not found for identifier: ${tokenIdentifier}`
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const metadata = this._metadataFromRow(metadataResult.rows[0]);
|
|
654
|
-
|
|
655
|
-
// Get available (non-reserved) outputs
|
|
656
|
-
const outputRows = await client.query(
|
|
657
|
-
`SELECT o.owner_public_key, o.revocation_commitment,
|
|
658
|
-
o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
|
|
659
|
-
o.token_public_key, o.token_amount, o.token_identifier,
|
|
660
|
-
o.prev_tx_hash, o.prev_tx_vout
|
|
661
|
-
FROM brz_token_outputs o
|
|
662
|
-
WHERE o.user_id = $1
|
|
663
|
-
AND o.token_identifier = $2
|
|
664
|
-
AND o.reservation_id IS NULL`,
|
|
665
|
-
[this.identity, tokenIdentifier]
|
|
666
|
-
);
|
|
667
|
-
|
|
668
|
-
let outputs = outputRows.rows.map((row) => this._outputFromRow(row));
|
|
669
|
-
|
|
670
|
-
// Filter by preferred if provided
|
|
671
|
-
if (preferredOutputs) {
|
|
672
|
-
const preferredOutpoints = new Set(
|
|
673
|
-
preferredOutputs.map((p) => `${p.prevTxHash}:${p.prevTxVout}`)
|
|
674
|
-
);
|
|
675
|
-
outputs = outputs.filter((o) =>
|
|
676
|
-
preferredOutpoints.has(`${o.prevTxHash}:${o.prevTxVout}`)
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const selectedOutputs = this._selectOutputs(
|
|
681
|
-
outputs,
|
|
682
|
-
target,
|
|
692
|
+
return await this._withWriteTransaction(async (client) => {
|
|
693
|
+
const selected = await this._selectForTargets(
|
|
694
|
+
client,
|
|
695
|
+
targets,
|
|
696
|
+
preferredOutputs,
|
|
683
697
|
selectionStrategy
|
|
684
698
|
);
|
|
685
699
|
|
|
686
|
-
// Create reservation
|
|
687
700
|
const reservationId = this._generateId();
|
|
688
|
-
|
|
689
|
-
await
|
|
690
|
-
|
|
691
|
-
|
|
701
|
+
await this._insertReservation(client, reservationId, purpose);
|
|
702
|
+
await this._assignReservation(
|
|
703
|
+
client,
|
|
704
|
+
reservationId,
|
|
705
|
+
selected.outputs
|
|
692
706
|
);
|
|
693
707
|
|
|
694
|
-
|
|
695
|
-
if (selectedOutputs.length > 0) {
|
|
696
|
-
const selectedTxHashes = selectedOutputs.map((o) => o.prevTxHash);
|
|
697
|
-
const selectedVouts = selectedOutputs.map((o) => o.prevTxVout);
|
|
698
|
-
await client.query(
|
|
699
|
-
`UPDATE brz_token_outputs SET reservation_id = $1
|
|
700
|
-
WHERE user_id = $4
|
|
701
|
-
AND (prev_tx_hash, prev_tx_vout) IN (
|
|
702
|
-
SELECT * FROM UNNEST($2::text[], $3::int[])
|
|
703
|
-
)`,
|
|
704
|
-
[reservationId, selectedTxHashes, selectedVouts, this.identity]
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
return {
|
|
709
|
-
id: reservationId,
|
|
710
|
-
tokenOutputs: {
|
|
711
|
-
metadata,
|
|
712
|
-
outputs: selectedOutputs,
|
|
713
|
-
},
|
|
714
|
-
};
|
|
708
|
+
return { id: reservationId, tokenOutputs: selected };
|
|
715
709
|
});
|
|
716
710
|
} catch (error) {
|
|
717
711
|
if (error instanceof TokenStoreError) throw error;
|
|
@@ -722,7 +716,7 @@ class PostgresTokenStore {
|
|
|
722
716
|
}
|
|
723
717
|
}
|
|
724
718
|
|
|
725
|
-
_selectOutputs(outputs, target, selectionStrategy) {
|
|
719
|
+
_selectOutputs(tokenIdentifier, outputs, target, selectionStrategy) {
|
|
726
720
|
if (target.type === "minTotalValue") {
|
|
727
721
|
const amount = BigInt(target.value);
|
|
728
722
|
const totalAvailable = outputs.reduce(
|
|
@@ -730,7 +724,9 @@ class PostgresTokenStore {
|
|
|
730
724
|
0n
|
|
731
725
|
);
|
|
732
726
|
if (totalAvailable < amount) {
|
|
733
|
-
throw new TokenStoreError(
|
|
727
|
+
throw new TokenStoreError(
|
|
728
|
+
`InsufficientFunds: ${tokenIdentifier}`
|
|
729
|
+
);
|
|
734
730
|
}
|
|
735
731
|
|
|
736
732
|
const exactMatch = outputs.find(
|
|
@@ -760,7 +756,9 @@ class PostgresTokenStore {
|
|
|
760
756
|
remaining -= BigInt(output.output.tokenAmount);
|
|
761
757
|
}
|
|
762
758
|
if (remaining > 0n) {
|
|
763
|
-
throw new TokenStoreError(
|
|
759
|
+
throw new TokenStoreError(
|
|
760
|
+
`InsufficientFunds: ${tokenIdentifier}`
|
|
761
|
+
);
|
|
764
762
|
}
|
|
765
763
|
return selected;
|
|
766
764
|
}
|
|
@@ -784,60 +782,24 @@ class PostgresTokenStore {
|
|
|
784
782
|
throw new TokenStoreError(`Unknown target type: ${target.type}`);
|
|
785
783
|
}
|
|
786
784
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
785
|
+
/**
|
|
786
|
+
* Select outputs covering every target, without reserving them.
|
|
787
|
+
* @param {Array<[string, {type: string, value: string|number}]>} targets - One
|
|
788
|
+
* [tokenIdentifier, target] pair per token to select for
|
|
789
|
+
* @param {Array|null} preferredOutputs
|
|
790
|
+
* @param {string|null} selectionStrategy - "SmallestFirst" or "LargestFirst"
|
|
791
|
+
* @returns {Promise<{metadata: Array<Object>, outputs: Array}>}
|
|
792
|
+
*/
|
|
793
|
+
async selectTokenOutputs(targets, preferredOutputs, selectionStrategy) {
|
|
793
794
|
try {
|
|
794
|
-
|
|
795
|
-
throw new TokenStoreError("Amount to reserve must be greater than zero");
|
|
796
|
-
}
|
|
797
|
-
if (target.type === "maxOutputCount" && (!target.value || target.value === 0)) {
|
|
798
|
-
throw new TokenStoreError("Count to reserve must be greater than zero");
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const metadataResult = await this.pool.query(
|
|
802
|
-
"SELECT * FROM brz_token_metadata WHERE user_id = $1 AND identifier = $2",
|
|
803
|
-
[this.identity, tokenIdentifier]
|
|
804
|
-
);
|
|
805
|
-
if (metadataResult.rows.length === 0) {
|
|
806
|
-
throw new TokenStoreError(
|
|
807
|
-
`Token outputs not found for identifier: ${tokenIdentifier}`
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
const metadata = this._metadataFromRow(metadataResult.rows[0]);
|
|
795
|
+
_validateTargets(targets);
|
|
811
796
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
o.prev_tx_hash, o.prev_tx_vout
|
|
817
|
-
FROM brz_token_outputs o
|
|
818
|
-
WHERE o.user_id = $1
|
|
819
|
-
AND o.token_identifier = $2
|
|
820
|
-
AND o.reservation_id IS NULL`,
|
|
821
|
-
[this.identity, tokenIdentifier]
|
|
822
|
-
);
|
|
823
|
-
|
|
824
|
-
let outputs = outputRows.rows.map((row) => this._outputFromRow(row));
|
|
825
|
-
|
|
826
|
-
if (preferredOutputs) {
|
|
827
|
-
const preferredOutpoints = new Set(
|
|
828
|
-
preferredOutputs.map((p) => `${p.prevTxHash}:${p.prevTxVout}`)
|
|
829
|
-
);
|
|
830
|
-
outputs = outputs.filter((o) =>
|
|
831
|
-
preferredOutpoints.has(`${o.prevTxHash}:${o.prevTxVout}`)
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const selectedOutputs = this._selectOutputs(
|
|
836
|
-
outputs,
|
|
837
|
-
target,
|
|
797
|
+
return await this._selectForTargets(
|
|
798
|
+
this.pool,
|
|
799
|
+
targets,
|
|
800
|
+
preferredOutputs,
|
|
838
801
|
selectionStrategy
|
|
839
802
|
);
|
|
840
|
-
return { metadata, outputs: selectedOutputs };
|
|
841
803
|
} catch (error) {
|
|
842
804
|
if (error instanceof TokenStoreError) throw error;
|
|
843
805
|
throw new TokenStoreError(
|
|
@@ -847,25 +809,22 @@ class PostgresTokenStore {
|
|
|
847
809
|
}
|
|
848
810
|
}
|
|
849
811
|
|
|
850
|
-
|
|
812
|
+
/**
|
|
813
|
+
* Reserve the outputs at the given outpoints, which may belong to several tokens.
|
|
814
|
+
* @param {Array<{prevTxHash: string, prevTxVout: number}>} outpoints
|
|
815
|
+
* @param {string} purpose - "Payment" or "Swap"
|
|
816
|
+
* @returns {Promise<{id: string, tokenOutputs: {metadata: Array<Object>, outputs: Array}}>}
|
|
817
|
+
*/
|
|
818
|
+
async reserveTokenOutputsByOutpoints(outpoints, purpose) {
|
|
851
819
|
try {
|
|
852
820
|
if (!outpoints || outpoints.length === 0) {
|
|
853
821
|
throw new TokenStoreError("No outpoints provided");
|
|
854
822
|
}
|
|
855
823
|
return await this._withWriteTransaction(async (client) => {
|
|
856
|
-
const metadataResult = await client.query(
|
|
857
|
-
"SELECT * FROM brz_token_metadata WHERE user_id = $1 AND identifier = $2",
|
|
858
|
-
[this.identity, tokenIdentifier]
|
|
859
|
-
);
|
|
860
|
-
if (metadataResult.rows.length === 0) {
|
|
861
|
-
throw new TokenStoreError(
|
|
862
|
-
`Token outputs not found for identifier: ${tokenIdentifier}`
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
const metadata = this._metadataFromRow(metadataResult.rows[0]);
|
|
866
|
-
|
|
867
824
|
const txHashes = outpoints.map((o) => o.prevTxHash);
|
|
868
825
|
const vouts = outpoints.map((o) => o.prevTxVout);
|
|
826
|
+
|
|
827
|
+
// The outpoints are not scoped to a token: they may belong to several.
|
|
869
828
|
const outputRows = await client.query(
|
|
870
829
|
`SELECT o.owner_public_key, o.revocation_commitment,
|
|
871
830
|
o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
|
|
@@ -873,12 +832,11 @@ class PostgresTokenStore {
|
|
|
873
832
|
o.prev_tx_hash, o.prev_tx_vout
|
|
874
833
|
FROM brz_token_outputs o
|
|
875
834
|
WHERE o.user_id = $1
|
|
876
|
-
AND o.token_identifier = $2
|
|
877
835
|
AND o.reservation_id IS NULL
|
|
878
836
|
AND (o.prev_tx_hash, o.prev_tx_vout) IN (
|
|
879
|
-
SELECT * FROM UNNEST($
|
|
837
|
+
SELECT * FROM UNNEST($2::text[], $3::int[])
|
|
880
838
|
)`,
|
|
881
|
-
[this.identity,
|
|
839
|
+
[this.identity, txHashes, vouts]
|
|
882
840
|
);
|
|
883
841
|
|
|
884
842
|
const selectedOutputs = outputRows.rows.map((row) =>
|
|
@@ -892,20 +850,15 @@ class PostgresTokenStore {
|
|
|
892
850
|
throw new TokenStoreError("InsufficientFunds");
|
|
893
851
|
}
|
|
894
852
|
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
[this.identity, reservationId, purpose]
|
|
899
|
-
);
|
|
900
|
-
await client.query(
|
|
901
|
-
`UPDATE brz_token_outputs SET reservation_id = $1
|
|
902
|
-
WHERE user_id = $4
|
|
903
|
-
AND (prev_tx_hash, prev_tx_vout) IN (
|
|
904
|
-
SELECT * FROM UNNEST($2::text[], $3::int[])
|
|
905
|
-
)`,
|
|
906
|
-
[reservationId, txHashes, vouts, this.identity]
|
|
853
|
+
const metadata = await this._fetchMetadata(
|
|
854
|
+
client,
|
|
855
|
+
_tokenIdentifiersOf(selectedOutputs)
|
|
907
856
|
);
|
|
908
857
|
|
|
858
|
+
const reservationId = this._generateId();
|
|
859
|
+
await this._insertReservation(client, reservationId, purpose);
|
|
860
|
+
await this._assignReservation(client, reservationId, selectedOutputs);
|
|
861
|
+
|
|
909
862
|
return {
|
|
910
863
|
id: reservationId,
|
|
911
864
|
tokenOutputs: {
|
|
@@ -1054,6 +1007,132 @@ class PostgresTokenStore {
|
|
|
1054
1007
|
|
|
1055
1008
|
// ===== Private Helpers =====
|
|
1056
1009
|
|
|
1010
|
+
_log(level, message) {
|
|
1011
|
+
if (this.logger && typeof this.logger.log === "function") {
|
|
1012
|
+
this.logger.log({ line: message, level });
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Select available outputs covering every target, without reserving them.
|
|
1018
|
+
*
|
|
1019
|
+
* The result carries metadata for every requested token. A target whose token
|
|
1020
|
+
* is unknown to this tenant is an error, and a target that cannot be covered
|
|
1021
|
+
* by the available outputs yields `InsufficientFunds`.
|
|
1022
|
+
* @param {import('pg').PoolClient|import('pg').Pool} client
|
|
1023
|
+
* @returns {Promise<{metadata: Array<Object>, outputs: Array}>}
|
|
1024
|
+
*/
|
|
1025
|
+
async _selectForTargets(client, targets, preferredOutputs, selectionStrategy) {
|
|
1026
|
+
const tokenIdentifiers = Array.from(new Set(targets.map(([id]) => id)));
|
|
1027
|
+
|
|
1028
|
+
const metadata = await this._fetchMetadata(client, tokenIdentifiers);
|
|
1029
|
+
for (const identifier of tokenIdentifiers) {
|
|
1030
|
+
if (!metadata.some((m) => m.identifier === identifier)) {
|
|
1031
|
+
throw new TokenStoreError(
|
|
1032
|
+
`Token outputs not found for identifier: ${identifier}`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const outputRows = await client.query(
|
|
1038
|
+
`SELECT o.owner_public_key, o.revocation_commitment,
|
|
1039
|
+
o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
|
|
1040
|
+
o.token_public_key, o.token_amount, o.token_identifier,
|
|
1041
|
+
o.prev_tx_hash, o.prev_tx_vout
|
|
1042
|
+
FROM brz_token_outputs o
|
|
1043
|
+
WHERE o.user_id = $1
|
|
1044
|
+
AND o.token_identifier = ANY($2)
|
|
1045
|
+
AND o.reservation_id IS NULL`,
|
|
1046
|
+
[this.identity, tokenIdentifiers]
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
let available = outputRows.rows.map((row) => this._outputFromRow(row));
|
|
1050
|
+
|
|
1051
|
+
if (preferredOutputs) {
|
|
1052
|
+
const preferredOutpoints = new Set(
|
|
1053
|
+
preferredOutputs.map((p) => `${p.prevTxHash}:${p.prevTxVout}`)
|
|
1054
|
+
);
|
|
1055
|
+
available = available.filter((o) =>
|
|
1056
|
+
preferredOutpoints.has(`${o.prevTxHash}:${o.prevTxVout}`)
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const availablePerToken = new Map();
|
|
1061
|
+
for (const output of available) {
|
|
1062
|
+
const identifier = output.output.tokenIdentifier;
|
|
1063
|
+
if (!availablePerToken.has(identifier)) {
|
|
1064
|
+
availablePerToken.set(identifier, []);
|
|
1065
|
+
}
|
|
1066
|
+
availablePerToken.get(identifier).push(output);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const outputs = [];
|
|
1070
|
+
for (const [tokenIdentifier, target] of targets) {
|
|
1071
|
+
const candidates = availablePerToken.get(tokenIdentifier) ?? [];
|
|
1072
|
+
const selected = this._selectOutputs(
|
|
1073
|
+
tokenIdentifier,
|
|
1074
|
+
candidates,
|
|
1075
|
+
target,
|
|
1076
|
+
selectionStrategy
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
// Outputs picked for one target are withheld from the next, so repeated
|
|
1080
|
+
// entries for the same token do not select the same output twice.
|
|
1081
|
+
const taken = new Set(
|
|
1082
|
+
selected.map((o) => `${o.prevTxHash}:${o.prevTxVout}`)
|
|
1083
|
+
);
|
|
1084
|
+
availablePerToken.set(
|
|
1085
|
+
tokenIdentifier,
|
|
1086
|
+
candidates.filter((o) => !taken.has(`${o.prevTxHash}:${o.prevTxVout}`))
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
outputs.push(...selected);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return { metadata, outputs };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Load this tenant's metadata for the given token identifiers. Unknown
|
|
1097
|
+
* identifiers are simply absent from the result.
|
|
1098
|
+
*/
|
|
1099
|
+
async _fetchMetadata(client, tokenIdentifiers) {
|
|
1100
|
+
if (tokenIdentifiers.length === 0) {
|
|
1101
|
+
return [];
|
|
1102
|
+
}
|
|
1103
|
+
const result = await client.query(
|
|
1104
|
+
"SELECT * FROM brz_token_metadata WHERE user_id = $1 AND identifier = ANY($2)",
|
|
1105
|
+
[this.identity, tokenIdentifiers]
|
|
1106
|
+
);
|
|
1107
|
+
return result.rows.map((row) => this._metadataFromRow(row));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async _insertReservation(client, reservationId, purpose) {
|
|
1111
|
+
await client.query(
|
|
1112
|
+
"INSERT INTO brz_token_reservations (user_id, id, purpose) VALUES ($1, $2, $3)",
|
|
1113
|
+
[this.identity, reservationId, purpose]
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Point the given outputs at the reservation, addressing them by outpoint.
|
|
1119
|
+
*/
|
|
1120
|
+
async _assignReservation(client, reservationId, outputs) {
|
|
1121
|
+
if (outputs.length === 0) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const txHashes = outputs.map((o) => o.prevTxHash);
|
|
1125
|
+
const vouts = outputs.map((o) => o.prevTxVout);
|
|
1126
|
+
await client.query(
|
|
1127
|
+
`UPDATE brz_token_outputs SET reservation_id = $1
|
|
1128
|
+
WHERE user_id = $4
|
|
1129
|
+
AND (prev_tx_hash, prev_tx_vout) IN (
|
|
1130
|
+
SELECT * FROM UNNEST($2::text[], $3::int[])
|
|
1131
|
+
)`,
|
|
1132
|
+
[reservationId, txHashes, vouts, this.identity]
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1057
1136
|
/**
|
|
1058
1137
|
* Generate a unique reservation ID (UUIDv4).
|
|
1059
1138
|
*/
|