@unicitylabs/sphere-sdk 0.7.1-dev.3 → 0.7.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/README.md +63 -77
- package/dist/core/index.cjs +541 -37
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +117 -1
- package/dist/core/index.d.ts +117 -1
- package/dist/core/index.js +540 -37
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +669 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +258 -1
- package/dist/index.d.ts +258 -1
- package/dist/index.js +651 -37
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -12347,6 +12347,132 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
12347
12347
|
};
|
|
12348
12348
|
}
|
|
12349
12349
|
}
|
|
12350
|
+
/**
|
|
12351
|
+
* Mint a fungible token directly to this wallet (genesis mint).
|
|
12352
|
+
*
|
|
12353
|
+
* Useful for test setups that need to seed a wallet with specific token
|
|
12354
|
+
* balances WITHOUT depending on the testnet faucet HTTP service. The
|
|
12355
|
+
* resulting token has the canonical CoinId bytes (passed in `coinIdHex`)
|
|
12356
|
+
* — when those bytes match a registered symbol in the TokenRegistry,
|
|
12357
|
+
* the token shows up under the symbol's name (e.g. "UCT"). There is no
|
|
12358
|
+
* cryptographic restriction on which key may issue a given CoinId; the
|
|
12359
|
+
* aggregator records the mint regardless of issuer identity.
|
|
12360
|
+
*
|
|
12361
|
+
* The flow:
|
|
12362
|
+
* 1. Generate a random TokenId.
|
|
12363
|
+
* 2. Build TokenCoinData with [(coinId, amount)].
|
|
12364
|
+
* 3. Build MintTransactionData with recipient = self (UnmaskedPredicate
|
|
12365
|
+
* from this wallet's signing service).
|
|
12366
|
+
* 4. Submit MintCommitment to the aggregator.
|
|
12367
|
+
* 5. Wait for the inclusion proof.
|
|
12368
|
+
* 6. Construct an SDK Token via Token.mint().
|
|
12369
|
+
* 7. Convert to wallet Token format and call addToken().
|
|
12370
|
+
*
|
|
12371
|
+
* @param coinIdHex - 64-char lowercase hex CoinId. Must match the bytes
|
|
12372
|
+
* used by the registered symbol if you want the wallet to recognize
|
|
12373
|
+
* the token as that symbol (e.g. UCT's coinId from the public registry).
|
|
12374
|
+
* @param amount - Amount in smallest units (multiply by 10^decimals
|
|
12375
|
+
* when converting from human values).
|
|
12376
|
+
* @returns Result with the resulting wallet Token and its on-chain id.
|
|
12377
|
+
*/
|
|
12378
|
+
async mintFungibleToken(coinIdHex, amount) {
|
|
12379
|
+
this.ensureInitialized();
|
|
12380
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
12381
|
+
if (!stClient) {
|
|
12382
|
+
return { success: false, error: "State transition client not available" };
|
|
12383
|
+
}
|
|
12384
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
12385
|
+
if (!trustBase) {
|
|
12386
|
+
return { success: false, error: "Trust base not available" };
|
|
12387
|
+
}
|
|
12388
|
+
try {
|
|
12389
|
+
const signingService = await this.createSigningService();
|
|
12390
|
+
const { TokenId: TokenId5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
12391
|
+
const { TokenCoinData: TokenCoinData3 } = await import("@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData");
|
|
12392
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
12393
|
+
const tokenTypeBytes = fromHex4("f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509");
|
|
12394
|
+
const tokenType = new TokenType3(tokenTypeBytes);
|
|
12395
|
+
const tokenIdBytes = new Uint8Array(32);
|
|
12396
|
+
crypto.getRandomValues(tokenIdBytes);
|
|
12397
|
+
const tokenId = new TokenId5(tokenIdBytes);
|
|
12398
|
+
const coinIdBytes = fromHex4(coinIdHex);
|
|
12399
|
+
const coinId = new CoinId4(coinIdBytes);
|
|
12400
|
+
const coinData = TokenCoinData3.create([[coinId, amount]]);
|
|
12401
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
12402
|
+
tokenType,
|
|
12403
|
+
signingService.algorithm,
|
|
12404
|
+
signingService.publicKey,
|
|
12405
|
+
HashAlgorithm5.SHA256
|
|
12406
|
+
);
|
|
12407
|
+
const ownerAddress = await addressRef.toAddress();
|
|
12408
|
+
const salt = new Uint8Array(32);
|
|
12409
|
+
crypto.getRandomValues(salt);
|
|
12410
|
+
const mintData = await MintTransactionData3.create(
|
|
12411
|
+
tokenId,
|
|
12412
|
+
tokenType,
|
|
12413
|
+
null,
|
|
12414
|
+
// tokenData: no metadata
|
|
12415
|
+
coinData,
|
|
12416
|
+
// fungible coin data
|
|
12417
|
+
ownerAddress,
|
|
12418
|
+
// recipient = self
|
|
12419
|
+
salt,
|
|
12420
|
+
null,
|
|
12421
|
+
// recipientDataHash
|
|
12422
|
+
null
|
|
12423
|
+
// reason: null (genesis, no burn predecessor)
|
|
12424
|
+
);
|
|
12425
|
+
const commitment = await MintCommitment3.create(mintData);
|
|
12426
|
+
const MAX_RETRIES = 3;
|
|
12427
|
+
let lastStatus;
|
|
12428
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
12429
|
+
const response = await stClient.submitMintCommitment(commitment);
|
|
12430
|
+
lastStatus = response.status;
|
|
12431
|
+
if (response.status === "SUCCESS" || response.status === "REQUEST_ID_EXISTS") break;
|
|
12432
|
+
if (attempt === MAX_RETRIES) {
|
|
12433
|
+
return { success: false, error: `Mint submit failed after ${MAX_RETRIES} attempts: ${response.status}` };
|
|
12434
|
+
}
|
|
12435
|
+
await new Promise((r) => setTimeout(r, 1e3 * attempt));
|
|
12436
|
+
}
|
|
12437
|
+
if (lastStatus !== "SUCCESS" && lastStatus !== "REQUEST_ID_EXISTS") {
|
|
12438
|
+
return { success: false, error: `Mint submit failed: ${lastStatus}` };
|
|
12439
|
+
}
|
|
12440
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
12441
|
+
const genesisTransaction = commitment.toTransaction(inclusionProof);
|
|
12442
|
+
const predicate = await UnmaskedPredicate5.create(
|
|
12443
|
+
tokenId,
|
|
12444
|
+
tokenType,
|
|
12445
|
+
signingService,
|
|
12446
|
+
HashAlgorithm5.SHA256,
|
|
12447
|
+
salt
|
|
12448
|
+
);
|
|
12449
|
+
const tokenState = new TokenState5(predicate, null);
|
|
12450
|
+
const sdkToken = await SdkToken2.mint(trustBase, tokenState, genesisTransaction);
|
|
12451
|
+
const tokenIdHex = tokenId.toJSON();
|
|
12452
|
+
const symbol = this.getCoinSymbol(coinIdHex);
|
|
12453
|
+
const name = this.getCoinName(coinIdHex);
|
|
12454
|
+
const decimals = this.getCoinDecimals(coinIdHex);
|
|
12455
|
+
const iconUrl = this.getCoinIconUrl(coinIdHex);
|
|
12456
|
+
const uiToken = {
|
|
12457
|
+
id: tokenIdHex,
|
|
12458
|
+
coinId: coinIdHex,
|
|
12459
|
+
symbol,
|
|
12460
|
+
name,
|
|
12461
|
+
decimals,
|
|
12462
|
+
...iconUrl !== void 0 ? { iconUrl } : {},
|
|
12463
|
+
amount: amount.toString(),
|
|
12464
|
+
status: "confirmed",
|
|
12465
|
+
createdAt: Date.now(),
|
|
12466
|
+
updatedAt: Date.now(),
|
|
12467
|
+
sdkData: JSON.stringify(sdkToken.toJSON())
|
|
12468
|
+
};
|
|
12469
|
+
await this.addToken(uiToken);
|
|
12470
|
+
return { success: true, token: uiToken, tokenId: tokenIdHex };
|
|
12471
|
+
} catch (err) {
|
|
12472
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12473
|
+
return { success: false, error: `Local mint failed: ${msg}` };
|
|
12474
|
+
}
|
|
12475
|
+
}
|
|
12350
12476
|
/**
|
|
12351
12477
|
* Check if a nametag is available for minting
|
|
12352
12478
|
* @param nametag - The nametag to check (e.g., "alice" or "@alice")
|
|
@@ -18557,6 +18683,24 @@ var AccountingModule = class _AccountingModule {
|
|
|
18557
18683
|
dirtyLedgerEntries = /* @__PURE__ */ new Set();
|
|
18558
18684
|
/** Count of unknown (not in invoiceTermsCache) invoice IDs in the ledger. */
|
|
18559
18685
|
unknownLedgerCount = 0;
|
|
18686
|
+
/**
|
|
18687
|
+
* Per-unknown-invoice first-seen timestamp for TTL eviction.
|
|
18688
|
+
*
|
|
18689
|
+
* W1 (steelman round-4): without TTL, an attacker who can deliver 500
|
|
18690
|
+
* inbound transfers with synthesized memo invoiceIds permanently exhausts
|
|
18691
|
+
* the unknown-ledger cap, after which legitimate orphan transfers (out-of-
|
|
18692
|
+
* order delivery for real swaps) are silently dropped at the cap-check.
|
|
18693
|
+
*
|
|
18694
|
+
* Round-5 perf: gated by `unknownLedgerNextSweepMs` to amortize the
|
|
18695
|
+
* sweep cost. The naive every-call sweep is O(N) where N=cap=500;
|
|
18696
|
+
* combined with the per-token cleanup loop inside the sweep it became
|
|
18697
|
+
* O(N×M) on every transfer under flood. Now we sweep at most every
|
|
18698
|
+
* `UNKNOWN_LEDGER_SWEEP_INTERVAL_MS` (60s) UNLESS the cap is currently
|
|
18699
|
+
* full, in which case we sweep on each call (the only path that can
|
|
18700
|
+
* actually drop a legitimate orphan).
|
|
18701
|
+
*/
|
|
18702
|
+
unknownLedgerFirstSeen = /* @__PURE__ */ new Map();
|
|
18703
|
+
unknownLedgerNextSweepMs = 0;
|
|
18560
18704
|
/** W17: Tracks whether tokenScanState has been mutated since last flush. */
|
|
18561
18705
|
tokenScanDirty = false;
|
|
18562
18706
|
/** W2 fix: Serialization guard for _flushDirtyLedgerEntries. */
|
|
@@ -19547,6 +19691,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
19547
19691
|
}
|
|
19548
19692
|
if (this.invoiceLedger.has(tokenId) && !this.invoiceTermsCache.has(tokenId)) {
|
|
19549
19693
|
this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
|
|
19694
|
+
this.unknownLedgerFirstSeen.delete(tokenId);
|
|
19550
19695
|
}
|
|
19551
19696
|
this.invoiceTermsCache.set(tokenId, terms);
|
|
19552
19697
|
this._addToHashIndex(tokenId);
|
|
@@ -19815,6 +19960,29 @@ var AccountingModule = class _AccountingModule {
|
|
|
19815
19960
|
closed: this.closedInvoices.has(invoiceId)
|
|
19816
19961
|
};
|
|
19817
19962
|
}
|
|
19963
|
+
/**
|
|
19964
|
+
* Return the set of token IDs that are currently linked to the given
|
|
19965
|
+
* invoice. Populated by both the on-chain `_processTokenTransactions`
|
|
19966
|
+
* path (tokens with `inv:` references) and the transport-memo orphan
|
|
19967
|
+
* buffering path in `_handleIncomingTransfer`.
|
|
19968
|
+
*
|
|
19969
|
+
* Used by callers that want to scope per-invoice operations (e.g.
|
|
19970
|
+
* SwapModule.verifyPayout's L3 validation) to only the tokens that
|
|
19971
|
+
* cover this invoice — avoiding false negatives when the wallet
|
|
19972
|
+
* contains unrelated tokens of the same currency in unconfirmed or
|
|
19973
|
+
* spent state.
|
|
19974
|
+
*
|
|
19975
|
+
* Returns an empty set if no tokens are currently linked.
|
|
19976
|
+
*/
|
|
19977
|
+
getTokenIdsForInvoice(invoiceId) {
|
|
19978
|
+
const result = /* @__PURE__ */ new Set();
|
|
19979
|
+
for (const [tokenId, invoiceIds] of this.tokenInvoiceMap) {
|
|
19980
|
+
if (invoiceIds.has(invoiceId)) {
|
|
19981
|
+
result.add(tokenId);
|
|
19982
|
+
}
|
|
19983
|
+
}
|
|
19984
|
+
return result;
|
|
19985
|
+
}
|
|
19818
19986
|
/**
|
|
19819
19987
|
* Explicitly close an invoice. Only target parties may close (§8.3).
|
|
19820
19988
|
*
|
|
@@ -20134,6 +20302,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
20134
20302
|
ledger.set(entryKey, forwardRef);
|
|
20135
20303
|
this.dirtyLedgerEntries.add(invoiceId);
|
|
20136
20304
|
this.balanceCache.delete(invoiceId);
|
|
20305
|
+
await this._persistProvisionalAndVerify(invoiceId, "payInvoice");
|
|
20137
20306
|
}
|
|
20138
20307
|
return result;
|
|
20139
20308
|
} finally {
|
|
@@ -20301,6 +20470,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
20301
20470
|
this.dirtyLedgerEntries.add(invoiceId);
|
|
20302
20471
|
}
|
|
20303
20472
|
this.balanceCache.delete(invoiceId);
|
|
20473
|
+
await this._persistProvisionalAndVerify(invoiceId, "returnInvoicePayment");
|
|
20304
20474
|
}
|
|
20305
20475
|
return result;
|
|
20306
20476
|
} finally {
|
|
@@ -21380,9 +21550,30 @@ var AccountingModule = class _AccountingModule {
|
|
|
21380
21550
|
continue;
|
|
21381
21551
|
}
|
|
21382
21552
|
innerMap.set(entryKey, ref);
|
|
21383
|
-
|
|
21553
|
+
const HEX_64 = /^[a-f0-9]{64}$/i;
|
|
21554
|
+
if (entryKey.startsWith("mt:")) {
|
|
21555
|
+
const firstColon = entryKey.indexOf(":");
|
|
21556
|
+
const secondColon = entryKey.indexOf(":", firstColon + 1);
|
|
21557
|
+
if (secondColon > firstColon + 1) {
|
|
21558
|
+
const tokenIdFromKey2 = entryKey.slice(firstColon + 1, secondColon);
|
|
21559
|
+
if (HEX_64.test(tokenIdFromKey2)) {
|
|
21560
|
+
this._addToTokenInvoiceMap(tokenIdFromKey2, invoiceId);
|
|
21561
|
+
}
|
|
21562
|
+
}
|
|
21563
|
+
} else if (entryKey.startsWith("synthetic:")) {
|
|
21564
|
+
const afterPrefix = entryKey.slice("synthetic:".length);
|
|
21565
|
+
const tokenIdEnd = afterPrefix.indexOf(":");
|
|
21566
|
+
if (tokenIdEnd > 0) {
|
|
21567
|
+
const tokenId = afterPrefix.slice(0, tokenIdEnd);
|
|
21568
|
+
if (HEX_64.test(tokenId) && ref.transferId !== tokenId) {
|
|
21569
|
+
this._addToTokenInvoiceMap(tokenId, invoiceId);
|
|
21570
|
+
}
|
|
21571
|
+
}
|
|
21572
|
+
} else if (!ref.transferId.startsWith("provisional:") && ref.transferId.includes(":")) {
|
|
21384
21573
|
const tokenIdFromRef = ref.transferId.slice(0, ref.transferId.indexOf(":"));
|
|
21385
|
-
|
|
21574
|
+
if (HEX_64.test(tokenIdFromRef)) {
|
|
21575
|
+
this._addToTokenInvoiceMap(tokenIdFromRef, invoiceId);
|
|
21576
|
+
}
|
|
21386
21577
|
}
|
|
21387
21578
|
}
|
|
21388
21579
|
} catch (err) {
|
|
@@ -21583,7 +21774,14 @@ var AccountingModule = class _AccountingModule {
|
|
|
21583
21774
|
}
|
|
21584
21775
|
}
|
|
21585
21776
|
for (const [existingKey, existingRef] of ledger) {
|
|
21586
|
-
if (existingKey.startsWith("synthetic:") && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21777
|
+
if ((existingKey.startsWith("synthetic:") || existingKey.startsWith("synthetic-tx:")) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21778
|
+
keysToDelete.push(existingKey);
|
|
21779
|
+
break;
|
|
21780
|
+
}
|
|
21781
|
+
}
|
|
21782
|
+
const mtPrefix = `mt:${tokenId}:`;
|
|
21783
|
+
for (const [existingKey, existingRef] of ledger) {
|
|
21784
|
+
if (existingKey.startsWith(mtPrefix) && existingRef.coinId === coinId && existingRef.paymentDirection === paymentDirection) {
|
|
21587
21785
|
keysToDelete.push(existingKey);
|
|
21588
21786
|
break;
|
|
21589
21787
|
}
|
|
@@ -21700,6 +21898,49 @@ var AccountingModule = class _AccountingModule {
|
|
|
21700
21898
|
});
|
|
21701
21899
|
}
|
|
21702
21900
|
}
|
|
21901
|
+
/**
|
|
21902
|
+
* Synchronously persist any pending provisional ledger entry for `invoiceId`
|
|
21903
|
+
* before returning to the caller. Used by `payInvoice` and
|
|
21904
|
+
* `returnInvoicePayment` to make the in-memory provisional entry durable
|
|
21905
|
+
* inside the same per-invoice gate that wrote it, closing the
|
|
21906
|
+
* crash-mid-conclude race that produces over-coverage on receivers.
|
|
21907
|
+
*
|
|
21908
|
+
* Implementation:
|
|
21909
|
+
* 1. Schedule a flush via the existing `_flushPromise` chain (so
|
|
21910
|
+
* concurrent `_handleTokenChange` callers waiting on the chain
|
|
21911
|
+
* observe ours as part of the sequence).
|
|
21912
|
+
* 2. Await OUR flush directly — NOT `_drainFlushPromise()`, which would
|
|
21913
|
+
* spin while concurrent token changes keep extending the chain and
|
|
21914
|
+
* hold the per-invoice gate for an unbounded number of additional
|
|
21915
|
+
* flushes. We only need OUR provisional entry durable.
|
|
21916
|
+
* 3. `_flushDirtyLedgerEntries` swallows per-invoice `storage.set`
|
|
21917
|
+
* rejections internally (sets a local `step1Failed` flag), leaving
|
|
21918
|
+
* the dirty entry on the set without re-throwing. So we post-check
|
|
21919
|
+
* `dirtyLedgerEntries.has(invoiceId)` and throw a `STORAGE_ERROR`
|
|
21920
|
+
* `SphereError` if our entry is still dirty — propagating to the
|
|
21921
|
+
* caller so they learn about the durability failure rather than
|
|
21922
|
+
* receiving a silent "success" return that lies on disk.
|
|
21923
|
+
*
|
|
21924
|
+
* @param invoiceId The invoice whose provisional entry must be durable.
|
|
21925
|
+
* @param callContext Used in the error message so the caller is named
|
|
21926
|
+
* ('payInvoice' / 'returnInvoicePayment') without
|
|
21927
|
+
* forcing a stack-trace inspection.
|
|
21928
|
+
*/
|
|
21929
|
+
async _persistProvisionalAndVerify(invoiceId, callContext) {
|
|
21930
|
+
const flushTrigger = (this._flushPromise ?? Promise.resolve()).then(() => this._flushDirtyLedgerEntries());
|
|
21931
|
+
const tracked = flushTrigger.catch(() => {
|
|
21932
|
+
}).finally(() => {
|
|
21933
|
+
if (this._flushPromise === tracked) this._flushPromise = null;
|
|
21934
|
+
});
|
|
21935
|
+
this._flushPromise = tracked;
|
|
21936
|
+
await flushTrigger;
|
|
21937
|
+
if (this.dirtyLedgerEntries.has(invoiceId)) {
|
|
21938
|
+
throw new SphereError(
|
|
21939
|
+
`${callContext}: provisional ledger entry for invoice ${invoiceId} failed to persist \u2014 caller should retry`,
|
|
21940
|
+
"STORAGE_ERROR"
|
|
21941
|
+
);
|
|
21942
|
+
}
|
|
21943
|
+
}
|
|
21703
21944
|
// ===========================================================================
|
|
21704
21945
|
// Internal: Event handlers
|
|
21705
21946
|
// ===========================================================================
|
|
@@ -21760,13 +22001,96 @@ var AccountingModule = class _AccountingModule {
|
|
|
21760
22001
|
}
|
|
21761
22002
|
}
|
|
21762
22003
|
if (!this.invoiceTermsCache.has(invoiceId)) {
|
|
21763
|
-
|
|
21764
|
-
|
|
21765
|
-
invoiceId
|
|
21766
|
-
|
|
21767
|
-
|
|
21768
|
-
|
|
21769
|
-
|
|
22004
|
+
let gracefullyGraduated = false;
|
|
22005
|
+
await this.withInvoiceGate(invoiceId, async () => {
|
|
22006
|
+
if (this.invoiceTermsCache.has(invoiceId)) {
|
|
22007
|
+
gracefullyGraduated = true;
|
|
22008
|
+
return;
|
|
22009
|
+
}
|
|
22010
|
+
const syntheticRef = this._buildSyntheticTransferRef(
|
|
22011
|
+
transfer,
|
|
22012
|
+
invoiceId,
|
|
22013
|
+
paymentDirection,
|
|
22014
|
+
confirmed
|
|
22015
|
+
);
|
|
22016
|
+
deps.emitEvent("invoice:unknown_reference", { invoiceId, transfer: syntheticRef });
|
|
22017
|
+
const MAX_UNKNOWN_INVOICE_IDS = 500;
|
|
22018
|
+
const UNKNOWN_LEDGER_TTL_MS = 30 * 60 * 1e3;
|
|
22019
|
+
const MAX_ORPHAN_ENTRIES_PER_INVOICE = 50;
|
|
22020
|
+
const UNKNOWN_LEDGER_SWEEP_INTERVAL_MS = 6e4;
|
|
22021
|
+
const nowMs = Date.now();
|
|
22022
|
+
const capFull = this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS;
|
|
22023
|
+
const sweepDue = nowMs >= this.unknownLedgerNextSweepMs;
|
|
22024
|
+
if (this.unknownLedgerFirstSeen.size > 0 && (capFull || sweepDue)) {
|
|
22025
|
+
this.unknownLedgerNextSweepMs = nowMs + UNKNOWN_LEDGER_SWEEP_INTERVAL_MS;
|
|
22026
|
+
const expiredIds = [];
|
|
22027
|
+
for (const [unkId, firstSeen] of this.unknownLedgerFirstSeen) {
|
|
22028
|
+
if (nowMs - firstSeen > UNKNOWN_LEDGER_TTL_MS) {
|
|
22029
|
+
expiredIds.push(unkId);
|
|
22030
|
+
}
|
|
22031
|
+
}
|
|
22032
|
+
for (const expiredId of expiredIds) {
|
|
22033
|
+
if (!this.invoiceTermsCache.has(expiredId) && this.invoiceLedger.has(expiredId)) {
|
|
22034
|
+
this.invoiceLedger.delete(expiredId);
|
|
22035
|
+
this.unknownLedgerCount = Math.max(0, this.unknownLedgerCount - 1);
|
|
22036
|
+
for (const [tokenId, invoiceSet] of this.tokenInvoiceMap) {
|
|
22037
|
+
if (invoiceSet.has(expiredId)) {
|
|
22038
|
+
invoiceSet.delete(expiredId);
|
|
22039
|
+
if (invoiceSet.size === 0) this.tokenInvoiceMap.delete(tokenId);
|
|
22040
|
+
}
|
|
22041
|
+
}
|
|
22042
|
+
}
|
|
22043
|
+
this.unknownLedgerFirstSeen.delete(expiredId);
|
|
22044
|
+
}
|
|
22045
|
+
}
|
|
22046
|
+
if (!this.invoiceLedger.has(invoiceId)) {
|
|
22047
|
+
if (this.unknownLedgerCount >= MAX_UNKNOWN_INVOICE_IDS) {
|
|
22048
|
+
return;
|
|
22049
|
+
}
|
|
22050
|
+
this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
|
|
22051
|
+
this.unknownLedgerCount++;
|
|
22052
|
+
this.unknownLedgerFirstSeen.set(invoiceId, nowMs);
|
|
22053
|
+
}
|
|
22054
|
+
const orphanLedger = this.invoiceLedger.get(invoiceId);
|
|
22055
|
+
let mtEntryCount = 0;
|
|
22056
|
+
for (const k of orphanLedger.keys()) {
|
|
22057
|
+
if (k.startsWith("mt:")) mtEntryCount++;
|
|
22058
|
+
}
|
|
22059
|
+
if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
|
|
22060
|
+
return;
|
|
22061
|
+
}
|
|
22062
|
+
for (const token of transfer.tokens) {
|
|
22063
|
+
if (!token.id) continue;
|
|
22064
|
+
let onChainAttributed = false;
|
|
22065
|
+
const tokenKeyPrefix = `${token.id}:`;
|
|
22066
|
+
for (const existingKey of orphanLedger.keys()) {
|
|
22067
|
+
if (existingKey.startsWith(tokenKeyPrefix) && !existingKey.startsWith("mt:")) {
|
|
22068
|
+
onChainAttributed = true;
|
|
22069
|
+
break;
|
|
22070
|
+
}
|
|
22071
|
+
}
|
|
22072
|
+
if (!onChainAttributed) {
|
|
22073
|
+
if (mtEntryCount >= MAX_ORPHAN_ENTRIES_PER_INVOICE) {
|
|
22074
|
+
break;
|
|
22075
|
+
}
|
|
22076
|
+
const orphanKey = `mt:${token.id}:${transfer.id}`;
|
|
22077
|
+
if (!orphanLedger.has(orphanKey)) {
|
|
22078
|
+
orphanLedger.set(orphanKey, syntheticRef);
|
|
22079
|
+
mtEntryCount++;
|
|
22080
|
+
}
|
|
22081
|
+
}
|
|
22082
|
+
if (!this.tokenInvoiceMap.has(token.id)) {
|
|
22083
|
+
this.tokenInvoiceMap.set(token.id, /* @__PURE__ */ new Set());
|
|
22084
|
+
}
|
|
22085
|
+
this.tokenInvoiceMap.get(token.id).add(invoiceId);
|
|
22086
|
+
}
|
|
22087
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22088
|
+
this.balanceCache.delete(invoiceId);
|
|
22089
|
+
await this._flushDirtyLedgerEntries();
|
|
22090
|
+
});
|
|
22091
|
+
if (gracefullyGraduated) {
|
|
22092
|
+
await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
|
|
22093
|
+
}
|
|
21770
22094
|
return;
|
|
21771
22095
|
}
|
|
21772
22096
|
await this._processInvoiceTransferEvent(transfer, invoiceId, paymentDirection, confirmed);
|
|
@@ -22202,7 +22526,8 @@ var AccountingModule = class _AccountingModule {
|
|
|
22202
22526
|
}
|
|
22203
22527
|
const existingLedger = this.invoiceLedger.get(invoiceId);
|
|
22204
22528
|
const firstTokenId = transfer.tokens.find((t) => t.id)?.id;
|
|
22205
|
-
const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22529
|
+
const syntheticKey = firstTokenId ? `synthetic:${firstTokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22530
|
+
let mutated = false;
|
|
22206
22531
|
if (!existingLedger.has(syntheticKey)) {
|
|
22207
22532
|
let hasRealEntry = false;
|
|
22208
22533
|
for (const tok of transfer.tokens) {
|
|
@@ -22217,7 +22542,24 @@ var AccountingModule = class _AccountingModule {
|
|
|
22217
22542
|
}
|
|
22218
22543
|
if (!hasRealEntry) {
|
|
22219
22544
|
existingLedger.set(syntheticKey, { ...syntheticRef });
|
|
22545
|
+
mutated = true;
|
|
22546
|
+
}
|
|
22547
|
+
}
|
|
22548
|
+
for (const tok of transfer.tokens) {
|
|
22549
|
+
if (!tok.id) continue;
|
|
22550
|
+
if (!this.tokenInvoiceMap.has(tok.id)) {
|
|
22551
|
+
this.tokenInvoiceMap.set(tok.id, /* @__PURE__ */ new Set());
|
|
22552
|
+
mutated = true;
|
|
22220
22553
|
}
|
|
22554
|
+
const beforeSize = this.tokenInvoiceMap.get(tok.id).size;
|
|
22555
|
+
this.tokenInvoiceMap.get(tok.id).add(invoiceId);
|
|
22556
|
+
if (this.tokenInvoiceMap.get(tok.id).size !== beforeSize) {
|
|
22557
|
+
mutated = true;
|
|
22558
|
+
}
|
|
22559
|
+
}
|
|
22560
|
+
if (mutated) {
|
|
22561
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22562
|
+
this.balanceCache.delete(invoiceId);
|
|
22221
22563
|
}
|
|
22222
22564
|
deps.emitEvent("invoice:payment", {
|
|
22223
22565
|
invoiceId,
|
|
@@ -22368,7 +22710,8 @@ var AccountingModule = class _AccountingModule {
|
|
|
22368
22710
|
this.invoiceLedger.set(invoiceId, /* @__PURE__ */ new Map());
|
|
22369
22711
|
}
|
|
22370
22712
|
const hLedger = this.invoiceLedger.get(invoiceId);
|
|
22371
|
-
const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22713
|
+
const hKey = entry.tokenId ? `synthetic:${entry.tokenId}::${syntheticRef.coinId}` : `synthetic-tx:${syntheticRef.transferId}::${syntheticRef.coinId}`;
|
|
22714
|
+
let hMutated = false;
|
|
22372
22715
|
if (!hLedger.has(hKey)) {
|
|
22373
22716
|
let hasRealEntry = false;
|
|
22374
22717
|
if (entry.tokenId) {
|
|
@@ -22381,8 +22724,24 @@ var AccountingModule = class _AccountingModule {
|
|
|
22381
22724
|
}
|
|
22382
22725
|
if (!hasRealEntry) {
|
|
22383
22726
|
hLedger.set(hKey, { ...syntheticRef });
|
|
22727
|
+
hMutated = true;
|
|
22728
|
+
}
|
|
22729
|
+
}
|
|
22730
|
+
if (entry.tokenId) {
|
|
22731
|
+
if (!this.tokenInvoiceMap.has(entry.tokenId)) {
|
|
22732
|
+
this.tokenInvoiceMap.set(entry.tokenId, /* @__PURE__ */ new Set());
|
|
22733
|
+
hMutated = true;
|
|
22734
|
+
}
|
|
22735
|
+
const beforeSize = this.tokenInvoiceMap.get(entry.tokenId).size;
|
|
22736
|
+
this.tokenInvoiceMap.get(entry.tokenId).add(invoiceId);
|
|
22737
|
+
if (this.tokenInvoiceMap.get(entry.tokenId).size !== beforeSize) {
|
|
22738
|
+
hMutated = true;
|
|
22384
22739
|
}
|
|
22385
22740
|
}
|
|
22741
|
+
if (hMutated) {
|
|
22742
|
+
this.dirtyLedgerEntries.add(invoiceId);
|
|
22743
|
+
this.balanceCache.delete(invoiceId);
|
|
22744
|
+
}
|
|
22386
22745
|
deps.emitEvent("invoice:payment", {
|
|
22387
22746
|
invoiceId,
|
|
22388
22747
|
transfer: syntheticRef,
|
|
@@ -24928,17 +25287,63 @@ var SwapModule = class {
|
|
|
24928
25287
|
for (const addr of allAddresses) {
|
|
24929
25288
|
myDirectAddresses.add(addr.directAddress);
|
|
24930
25289
|
}
|
|
24931
|
-
|
|
24932
|
-
|
|
24933
|
-
|
|
24934
|
-
|
|
24935
|
-
|
|
25290
|
+
const matchesPartyA = myDirectAddresses.has(swap.manifest.party_a_address);
|
|
25291
|
+
const matchesPartyB = myDirectAddresses.has(swap.manifest.party_b_address);
|
|
25292
|
+
if (matchesPartyA && matchesPartyB) {
|
|
25293
|
+
throw new SphereError(
|
|
25294
|
+
"Ambiguous party identity: local wallet matches both party_a_address and party_b_address",
|
|
25295
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25296
|
+
);
|
|
25297
|
+
}
|
|
25298
|
+
let myExpectedCurrency;
|
|
25299
|
+
if (matchesPartyA) {
|
|
25300
|
+
myExpectedCurrency = swap.manifest.party_a_currency_to_change;
|
|
25301
|
+
} else if (matchesPartyB) {
|
|
25302
|
+
myExpectedCurrency = swap.manifest.party_b_currency_to_change;
|
|
24936
25303
|
} else {
|
|
24937
25304
|
throw new SphereError(
|
|
24938
25305
|
"Local wallet address does not match either party in the swap manifest",
|
|
24939
25306
|
"SWAP_DEPOSIT_FAILED"
|
|
24940
25307
|
);
|
|
24941
25308
|
}
|
|
25309
|
+
if (!myExpectedCurrency || myExpectedCurrency === "") {
|
|
25310
|
+
throw new SphereError(
|
|
25311
|
+
"Manifest currency_to_change is empty for this party",
|
|
25312
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25313
|
+
);
|
|
25314
|
+
}
|
|
25315
|
+
const invoiceRefForAssetLookup = deps.accounting.getInvoice(swap.depositInvoiceId);
|
|
25316
|
+
if (!invoiceRefForAssetLookup) {
|
|
25317
|
+
throw new SphereError(
|
|
25318
|
+
"Deposit invoice not yet imported into accounting module",
|
|
25319
|
+
"SWAP_WRONG_STATE"
|
|
25320
|
+
);
|
|
25321
|
+
}
|
|
25322
|
+
const depositTarget = invoiceRefForAssetLookup.terms.targets[0];
|
|
25323
|
+
if (!depositTarget) {
|
|
25324
|
+
throw new SphereError(
|
|
25325
|
+
"Deposit invoice has no targets",
|
|
25326
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25327
|
+
);
|
|
25328
|
+
}
|
|
25329
|
+
const assetIndex = depositTarget.assets.findIndex(
|
|
25330
|
+
(a) => a.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)
|
|
25331
|
+
);
|
|
25332
|
+
if (assetIndex < 0) {
|
|
25333
|
+
throw new SphereError(
|
|
25334
|
+
`No asset matching expected currency ${myExpectedCurrency} found in deposit invoice`,
|
|
25335
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25336
|
+
);
|
|
25337
|
+
}
|
|
25338
|
+
for (let i = assetIndex + 1; i < depositTarget.assets.length; i += 1) {
|
|
25339
|
+
const a = depositTarget.assets[i];
|
|
25340
|
+
if (a?.coin !== void 0 && coinIdsMatch(a.coin[0], myExpectedCurrency)) {
|
|
25341
|
+
throw new SphereError(
|
|
25342
|
+
`Ambiguous asset match in deposit invoice: slots ${assetIndex} and ${i} both match currency ${myExpectedCurrency}`,
|
|
25343
|
+
"SWAP_DEPOSIT_FAILED"
|
|
25344
|
+
);
|
|
25345
|
+
}
|
|
25346
|
+
}
|
|
24942
25347
|
return this.withSwapGate(swapId, async () => {
|
|
24943
25348
|
if (swap.progress !== "announced") {
|
|
24944
25349
|
throw new SphereError(
|
|
@@ -25044,7 +25449,6 @@ var SwapModule = class {
|
|
|
25044
25449
|
swap.updatedAt = Date.now();
|
|
25045
25450
|
this.clearLocalTimer(swap.swapId);
|
|
25046
25451
|
this.terminalSwapIds.add(swap.swapId);
|
|
25047
|
-
const entryIdx = this._storedTerminalEntries.length;
|
|
25048
25452
|
this._storedTerminalEntries.push({
|
|
25049
25453
|
swapId: swap.swapId,
|
|
25050
25454
|
progress: "failed",
|
|
@@ -25059,7 +25463,13 @@ var SwapModule = class {
|
|
|
25059
25463
|
swap.error = prevError;
|
|
25060
25464
|
swap.updatedAt = prevUpdatedAt;
|
|
25061
25465
|
this.terminalSwapIds.delete(swap.swapId);
|
|
25062
|
-
this._storedTerminalEntries.
|
|
25466
|
+
for (let i = this._storedTerminalEntries.length - 1; i >= 0; i--) {
|
|
25467
|
+
const entry = this._storedTerminalEntries[i];
|
|
25468
|
+
if (entry.swapId === swap.swapId && entry.progress === "failed") {
|
|
25469
|
+
this._storedTerminalEntries.splice(i, 1);
|
|
25470
|
+
break;
|
|
25471
|
+
}
|
|
25472
|
+
}
|
|
25063
25473
|
logger.warn(LOG_TAG3, `failPayout: persistSwap failed for ${swapId}; fraud detection will retry on next load:`, persistErr);
|
|
25064
25474
|
throw persistErr;
|
|
25065
25475
|
}
|
|
@@ -25103,9 +25513,25 @@ var SwapModule = class {
|
|
|
25103
25513
|
if (!targetStatus.coinAssets[0].isCovered) {
|
|
25104
25514
|
return returnFalse();
|
|
25105
25515
|
}
|
|
25106
|
-
|
|
25516
|
+
let netCoveredAmount;
|
|
25517
|
+
let expectedAmountBigInt;
|
|
25518
|
+
try {
|
|
25519
|
+
netCoveredAmount = BigInt(targetStatus.coinAssets[0].netCoveredAmount);
|
|
25520
|
+
expectedAmountBigInt = BigInt(expectedAmount);
|
|
25521
|
+
} catch (parseErr) {
|
|
25522
|
+
return failPayout(
|
|
25523
|
+
`MALFORMED_AMOUNT: failed to parse coverage amounts (netCoveredAmount=${targetStatus.coinAssets[0].netCoveredAmount}, expectedAmount=${expectedAmount}): ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`
|
|
25524
|
+
);
|
|
25525
|
+
}
|
|
25526
|
+
if (netCoveredAmount < expectedAmountBigInt) {
|
|
25107
25527
|
return returnFalse();
|
|
25108
25528
|
}
|
|
25529
|
+
if (netCoveredAmount > expectedAmountBigInt) {
|
|
25530
|
+
const surplus = netCoveredAmount - expectedAmountBigInt;
|
|
25531
|
+
return failPayout(
|
|
25532
|
+
`OVER_COVERAGE: net=${netCoveredAmount.toString()}, expected=${expectedAmount}, surplus=${surplus.toString()} \u2014 surplus refund expected via auto-return; settlement halted`
|
|
25533
|
+
);
|
|
25534
|
+
}
|
|
25109
25535
|
const escrowAddr = swap.deal.escrowAddress ?? this.config.defaultEscrowAddress;
|
|
25110
25536
|
if (escrowAddr) {
|
|
25111
25537
|
const escrowPeer = await deps.resolve(escrowAddr);
|
|
@@ -25115,8 +25541,28 @@ var SwapModule = class {
|
|
|
25115
25541
|
}
|
|
25116
25542
|
const validationResult = await deps.payments.validate();
|
|
25117
25543
|
if (validationResult.invalid.length > 0) {
|
|
25118
|
-
|
|
25119
|
-
|
|
25544
|
+
const payoutTokenIds = deps.accounting.getTokenIdsForInvoice?.(swap.payoutInvoiceId) ?? /* @__PURE__ */ new Set();
|
|
25545
|
+
if (payoutTokenIds.size === 0) {
|
|
25546
|
+
logger.warn(
|
|
25547
|
+
LOG_TAG3,
|
|
25548
|
+
`verifyPayout for ${swapId.slice(0, 12)}: ${validationResult.invalid.length} invalid token(s) but tokenInvoiceMap is empty for this payout invoice \u2014 failing closed until reverse index rebuilds`
|
|
25549
|
+
);
|
|
25550
|
+
return returnFalse();
|
|
25551
|
+
}
|
|
25552
|
+
const relevantInvalid = validationResult.invalid.filter(
|
|
25553
|
+
(t) => payoutTokenIds.has(t.id)
|
|
25554
|
+
);
|
|
25555
|
+
if (relevantInvalid.length > 0) {
|
|
25556
|
+
logger.warn(
|
|
25557
|
+
LOG_TAG3,
|
|
25558
|
+
`verifyPayout for ${swapId.slice(0, 12)}: L3 validation found ${relevantInvalid.length} invalid token(s) covering this payout invoice \u2014 retry after wallet sync`
|
|
25559
|
+
);
|
|
25560
|
+
return returnFalse();
|
|
25561
|
+
}
|
|
25562
|
+
logger.debug(
|
|
25563
|
+
LOG_TAG3,
|
|
25564
|
+
`verifyPayout for ${swapId.slice(0, 12)}: ${validationResult.invalid.length} unrelated invalid token(s) ignored (not linked to this payout invoice)`
|
|
25565
|
+
);
|
|
25120
25566
|
}
|
|
25121
25567
|
if (swap.progress === "completed") {
|
|
25122
25568
|
swap.payoutVerified = true;
|
|
@@ -25295,8 +25741,22 @@ var SwapModule = class {
|
|
|
25295
25741
|
* @param dm - The incoming direct message.
|
|
25296
25742
|
*/
|
|
25297
25743
|
handleIncomingDM(dm) {
|
|
25744
|
+
if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
|
|
25745
|
+
logger.warn(
|
|
25746
|
+
LOG_TAG3,
|
|
25747
|
+
`diag_swap_dm_arrived sender=${dm.senderPubkey.slice(0, 16)} length=${dm.content.length}`
|
|
25748
|
+
);
|
|
25749
|
+
}
|
|
25298
25750
|
const parsed = parseSwapDM(dm.content);
|
|
25299
|
-
if (!parsed)
|
|
25751
|
+
if (!parsed) {
|
|
25752
|
+
if (dm.content.startsWith("{") && dm.content.includes('"invoice_delivery"')) {
|
|
25753
|
+
logger.warn(
|
|
25754
|
+
LOG_TAG3,
|
|
25755
|
+
`diag_swap_dm_parse_rejected sender=${dm.senderPubkey.slice(0, 16)} prefix=${dm.content.slice(0, 80)}`
|
|
25756
|
+
);
|
|
25757
|
+
}
|
|
25758
|
+
return;
|
|
25759
|
+
}
|
|
25300
25760
|
void (async () => {
|
|
25301
25761
|
try {
|
|
25302
25762
|
switch (parsed.kind) {
|
|
@@ -25662,16 +26122,43 @@ var SwapModule = class {
|
|
|
25662
26122
|
// invoice_delivery (§12.4.2 + §12.4.3)
|
|
25663
26123
|
// ---------------------------------------------------------------
|
|
25664
26124
|
case "invoice_delivery": {
|
|
25665
|
-
|
|
26125
|
+
logger.warn(
|
|
26126
|
+
LOG_TAG3,
|
|
26127
|
+
`diag_invoice_delivery_received swap_id=${swapId?.slice(0, 16)} sender=${dm.senderPubkey.slice(0, 16)} invoice_type=${msg.invoice_type} invoice_id=${msg.invoice_id?.slice(0, 16)}`
|
|
26128
|
+
);
|
|
26129
|
+
if (!swapId) {
|
|
26130
|
+
logger.warn(LOG_TAG3, "diag_invoice_delivery_dropped reason=no_swap_id");
|
|
26131
|
+
return;
|
|
26132
|
+
}
|
|
25666
26133
|
const swap = this.swaps.get(swapId);
|
|
25667
|
-
if (!swap)
|
|
25668
|
-
|
|
26134
|
+
if (!swap) {
|
|
26135
|
+
logger.warn(
|
|
26136
|
+
LOG_TAG3,
|
|
26137
|
+
`diag_invoice_delivery_dropped reason=swap_not_in_map swap_id=${swapId.slice(0, 16)} known_swap_ids_count=${this.swaps.size}`
|
|
26138
|
+
);
|
|
26139
|
+
return;
|
|
26140
|
+
}
|
|
26141
|
+
if (!this.isFromExpectedEscrow(dm.senderPubkey, swap)) {
|
|
26142
|
+
logger.warn(
|
|
26143
|
+
LOG_TAG3,
|
|
26144
|
+
`diag_invoice_delivery_dropped reason=not_expected_escrow swap_id=${swapId.slice(0, 16)} sender=${dm.senderPubkey.slice(0, 16)} expected_escrow_pubkey=${swap.escrowPubkey?.slice(0, 16)} expected_escrow_addr=${swap.escrowDirectAddress?.slice(0, 24)}`
|
|
26145
|
+
);
|
|
26146
|
+
return;
|
|
26147
|
+
}
|
|
25669
26148
|
const deps = this.deps;
|
|
25670
26149
|
if (msg.invoice_type === "deposit") {
|
|
26150
|
+
logger.warn(
|
|
26151
|
+
LOG_TAG3,
|
|
26152
|
+
`diag_invoice_delivery_proceeding_to_import swap_id=${swapId.slice(0, 16)} progress=${swap.progress} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)}`
|
|
26153
|
+
);
|
|
25671
26154
|
await this.withSwapGate(swapId, async () => {
|
|
25672
26155
|
if (isTerminalProgress(swap.progress)) return;
|
|
25673
26156
|
try {
|
|
25674
26157
|
await deps.accounting.importInvoice(msg.invoice_token);
|
|
26158
|
+
logger.warn(
|
|
26159
|
+
LOG_TAG3,
|
|
26160
|
+
`diag_invoice_imported swap_id=${swapId.slice(0, 16)} invoice_id=${(msg.invoice_id ?? "").slice(0, 16)} type=deposit`
|
|
26161
|
+
);
|
|
25675
26162
|
} catch (err) {
|
|
25676
26163
|
if (err instanceof SphereError && err.code === "INVOICE_ALREADY_EXISTS") {
|
|
25677
26164
|
logger.debug(LOG_TAG3, `Deposit invoice for swap ${swapId} already imported \u2014 relay re-delivery, continuing`);
|
|
@@ -26310,6 +26797,65 @@ init_constants();
|
|
|
26310
26797
|
init_errors();
|
|
26311
26798
|
init_logger();
|
|
26312
26799
|
import CryptoJS6 from "crypto-js";
|
|
26800
|
+
var DEFAULT_ITERATIONS = 1e5;
|
|
26801
|
+
var KEY_SIZE = 256;
|
|
26802
|
+
var SALT_SIZE = 16;
|
|
26803
|
+
var IV_SIZE = 16;
|
|
26804
|
+
function deriveKey(password, salt, iterations) {
|
|
26805
|
+
return CryptoJS6.PBKDF2(password, salt, {
|
|
26806
|
+
keySize: KEY_SIZE / 32,
|
|
26807
|
+
// WordArray uses 32-bit words
|
|
26808
|
+
iterations,
|
|
26809
|
+
hasher: CryptoJS6.algo.SHA256
|
|
26810
|
+
});
|
|
26811
|
+
}
|
|
26812
|
+
function encrypt2(plaintext, password, options = {}) {
|
|
26813
|
+
const iterations = options.iterations ?? DEFAULT_ITERATIONS;
|
|
26814
|
+
const data = typeof plaintext === "string" ? plaintext : JSON.stringify(plaintext);
|
|
26815
|
+
const salt = CryptoJS6.lib.WordArray.random(SALT_SIZE);
|
|
26816
|
+
const iv = CryptoJS6.lib.WordArray.random(IV_SIZE);
|
|
26817
|
+
const key = deriveKey(password, salt, iterations);
|
|
26818
|
+
const encrypted = CryptoJS6.AES.encrypt(data, key, {
|
|
26819
|
+
iv,
|
|
26820
|
+
mode: CryptoJS6.mode.CBC,
|
|
26821
|
+
padding: CryptoJS6.pad.Pkcs7
|
|
26822
|
+
});
|
|
26823
|
+
return {
|
|
26824
|
+
ciphertext: encrypted.ciphertext.toString(CryptoJS6.enc.Base64),
|
|
26825
|
+
iv: iv.toString(CryptoJS6.enc.Hex),
|
|
26826
|
+
salt: salt.toString(CryptoJS6.enc.Hex),
|
|
26827
|
+
algorithm: "aes-256-cbc",
|
|
26828
|
+
kdf: "pbkdf2",
|
|
26829
|
+
iterations
|
|
26830
|
+
};
|
|
26831
|
+
}
|
|
26832
|
+
function decrypt2(encryptedData, password) {
|
|
26833
|
+
const salt = CryptoJS6.enc.Hex.parse(encryptedData.salt);
|
|
26834
|
+
const iv = CryptoJS6.enc.Hex.parse(encryptedData.iv);
|
|
26835
|
+
const key = deriveKey(password, salt, encryptedData.iterations);
|
|
26836
|
+
const ciphertext = CryptoJS6.enc.Base64.parse(encryptedData.ciphertext);
|
|
26837
|
+
const cipherParams = CryptoJS6.lib.CipherParams.create({
|
|
26838
|
+
ciphertext
|
|
26839
|
+
});
|
|
26840
|
+
const decrypted = CryptoJS6.AES.decrypt(cipherParams, key, {
|
|
26841
|
+
iv,
|
|
26842
|
+
mode: CryptoJS6.mode.CBC,
|
|
26843
|
+
padding: CryptoJS6.pad.Pkcs7
|
|
26844
|
+
});
|
|
26845
|
+
const result = decrypted.toString(CryptoJS6.enc.Utf8);
|
|
26846
|
+
if (!result) {
|
|
26847
|
+
throw new SphereError("Decryption failed: invalid password or corrupted data", "DECRYPTION_ERROR");
|
|
26848
|
+
}
|
|
26849
|
+
return result;
|
|
26850
|
+
}
|
|
26851
|
+
function decryptJson(encryptedData, password) {
|
|
26852
|
+
const decrypted = decrypt2(encryptedData, password);
|
|
26853
|
+
try {
|
|
26854
|
+
return JSON.parse(decrypted);
|
|
26855
|
+
} catch {
|
|
26856
|
+
throw new SphereError("Decryption failed: invalid JSON data", "DECRYPTION_ERROR");
|
|
26857
|
+
}
|
|
26858
|
+
}
|
|
26313
26859
|
function encryptSimple(plaintext, password) {
|
|
26314
26860
|
return CryptoJS6.AES.encrypt(plaintext, password).toString();
|
|
26315
26861
|
}
|
|
@@ -26336,6 +26882,12 @@ function decryptWithSalt(ciphertext, password, salt) {
|
|
|
26336
26882
|
return null;
|
|
26337
26883
|
}
|
|
26338
26884
|
}
|
|
26885
|
+
function encryptMnemonic(mnemonic, password) {
|
|
26886
|
+
return encryptSimple(mnemonic, password);
|
|
26887
|
+
}
|
|
26888
|
+
function decryptMnemonic(encryptedMnemonic, password) {
|
|
26889
|
+
return decryptSimple(encryptedMnemonic, password);
|
|
26890
|
+
}
|
|
26339
26891
|
|
|
26340
26892
|
// core/scan.ts
|
|
26341
26893
|
init_logger();
|
|
@@ -27079,27 +27631,42 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
27079
27631
|
|
|
27080
27632
|
// core/Sphere.ts
|
|
27081
27633
|
import { SigningService as SigningService2 } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
|
|
27634
|
+
import { normalizeNametag as normalizeNametag2, isPhoneNumber } from "@unicitylabs/nostr-js-sdk";
|
|
27635
|
+
|
|
27636
|
+
// core/address-derivation.ts
|
|
27082
27637
|
import { TokenType as TokenType5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
27083
27638
|
import { HashAlgorithm as HashAlgorithm7 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
27084
27639
|
import { UnmaskedPredicateReference as UnmaskedPredicateReference3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
|
|
27085
|
-
|
|
27640
|
+
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
27641
|
+
var COMPRESSED_PUBKEY_RE = /^(02|03)[0-9a-fA-F]{64}$/;
|
|
27642
|
+
async function computeDirectAddressFromChainPubkey(chainPubkey) {
|
|
27643
|
+
if (typeof chainPubkey !== "string" || !COMPRESSED_PUBKEY_RE.test(chainPubkey)) {
|
|
27644
|
+
throw new Error(
|
|
27645
|
+
`computeDirectAddressFromChainPubkey: chainPubkey must be 66-char hex with 02/03 prefix, got "${String(chainPubkey).slice(0, 12)}..."`
|
|
27646
|
+
);
|
|
27647
|
+
}
|
|
27648
|
+
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
27649
|
+
const tokenType = new TokenType5(tokenTypeBytes);
|
|
27650
|
+
const publicKeyBytes = Buffer.from(chainPubkey, "hex");
|
|
27651
|
+
const predicateRef = await UnmaskedPredicateReference3.create(
|
|
27652
|
+
tokenType,
|
|
27653
|
+
"secp256k1",
|
|
27654
|
+
publicKeyBytes,
|
|
27655
|
+
HashAlgorithm7.SHA256
|
|
27656
|
+
);
|
|
27657
|
+
return (await predicateRef.toAddress()).toString();
|
|
27658
|
+
}
|
|
27659
|
+
|
|
27660
|
+
// core/Sphere.ts
|
|
27086
27661
|
function isValidNametag2(nametag) {
|
|
27087
27662
|
if (isPhoneNumber(nametag)) return true;
|
|
27088
27663
|
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
27089
27664
|
}
|
|
27090
|
-
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
27091
27665
|
async function deriveL3PredicateAddress(privateKey) {
|
|
27092
27666
|
const secret = Buffer.from(privateKey, "hex");
|
|
27093
27667
|
const signingService = await SigningService2.createFromSecret(secret);
|
|
27094
|
-
const
|
|
27095
|
-
|
|
27096
|
-
const predicateRef = UnmaskedPredicateReference3.create(
|
|
27097
|
-
tokenType,
|
|
27098
|
-
signingService.algorithm,
|
|
27099
|
-
signingService.publicKey,
|
|
27100
|
-
HashAlgorithm7.SHA256
|
|
27101
|
-
);
|
|
27102
|
-
return (await (await predicateRef).toAddress()).toString();
|
|
27668
|
+
const pubkeyHex = Buffer.from(signingService.publicKey).toString("hex");
|
|
27669
|
+
return computeDirectAddressFromChainPubkey(pubkeyHex);
|
|
27103
27670
|
}
|
|
27104
27671
|
var Sphere = class _Sphere {
|
|
27105
27672
|
// Singleton
|
|
@@ -31118,7 +31685,37 @@ function createPriceProvider(config) {
|
|
|
31118
31685
|
throw new SphereError(`Unsupported price platform: ${String(config.platform)}`, "INVALID_CONFIG");
|
|
31119
31686
|
}
|
|
31120
31687
|
}
|
|
31688
|
+
|
|
31689
|
+
// core/auth.ts
|
|
31690
|
+
var AuthVerificationError = class extends Error {
|
|
31691
|
+
code;
|
|
31692
|
+
constructor(message, code) {
|
|
31693
|
+
super(message);
|
|
31694
|
+
this.name = "AuthVerificationError";
|
|
31695
|
+
this.code = code;
|
|
31696
|
+
}
|
|
31697
|
+
};
|
|
31698
|
+
async function verifySphereAuth(input) {
|
|
31699
|
+
const { challenge, signature, chainPubkey } = input;
|
|
31700
|
+
let directAddress;
|
|
31701
|
+
try {
|
|
31702
|
+
directAddress = await computeDirectAddressFromChainPubkey(chainPubkey);
|
|
31703
|
+
} catch (err) {
|
|
31704
|
+
throw new AuthVerificationError(
|
|
31705
|
+
`chainPubkey is malformed: ${err.message}`,
|
|
31706
|
+
"PUBKEY_MALFORMED"
|
|
31707
|
+
);
|
|
31708
|
+
}
|
|
31709
|
+
if (!verifySignedMessage(challenge, signature, chainPubkey)) {
|
|
31710
|
+
throw new AuthVerificationError(
|
|
31711
|
+
"Signature does not verify against chainPubkey",
|
|
31712
|
+
"SIGNATURE_INVALID"
|
|
31713
|
+
);
|
|
31714
|
+
}
|
|
31715
|
+
return { chainPubkey, directAddress };
|
|
31716
|
+
}
|
|
31121
31717
|
export {
|
|
31718
|
+
AuthVerificationError,
|
|
31122
31719
|
COIN_TYPES,
|
|
31123
31720
|
CoinGeckoPriceProvider,
|
|
31124
31721
|
CommunicationsModule,
|
|
@@ -31168,6 +31765,8 @@ export {
|
|
|
31168
31765
|
buildTxfStorageData,
|
|
31169
31766
|
bytesToHex3 as bytesToHex,
|
|
31170
31767
|
checkNetworkHealth,
|
|
31768
|
+
coinIdsMatch,
|
|
31769
|
+
computeDirectAddressFromChainPubkey,
|
|
31171
31770
|
computeSwapId,
|
|
31172
31771
|
countCommittedTransactions,
|
|
31173
31772
|
createAddress,
|
|
@@ -31186,22 +31785,34 @@ export {
|
|
|
31186
31785
|
createSwapModule,
|
|
31187
31786
|
createTokenValidator,
|
|
31188
31787
|
decodeBech32,
|
|
31788
|
+
decrypt2 as decrypt,
|
|
31189
31789
|
decryptCMasterKey,
|
|
31790
|
+
decryptJson,
|
|
31791
|
+
decryptMnemonic,
|
|
31190
31792
|
decryptNametag2 as decryptNametag,
|
|
31191
31793
|
decryptPrivateKey,
|
|
31794
|
+
decryptSimple,
|
|
31192
31795
|
decryptTextFormatKey,
|
|
31796
|
+
decryptWallet,
|
|
31797
|
+
decryptWithSalt,
|
|
31193
31798
|
deriveAddressInfo,
|
|
31194
31799
|
deriveChildKey,
|
|
31195
31800
|
deriveKeyAtPath,
|
|
31196
31801
|
doubleSha256,
|
|
31197
31802
|
encodeBech32,
|
|
31803
|
+
encrypt2 as encrypt,
|
|
31804
|
+
encryptMnemonic,
|
|
31198
31805
|
encryptNametag,
|
|
31806
|
+
encryptSimple,
|
|
31807
|
+
encryptWallet,
|
|
31199
31808
|
extractFromText,
|
|
31200
31809
|
findPattern,
|
|
31201
31810
|
forkedKeyFromTokenIdAndState,
|
|
31202
31811
|
formatAmount,
|
|
31812
|
+
generateAddressFromMasterKey,
|
|
31203
31813
|
generateMasterKey,
|
|
31204
31814
|
generateMnemonic2 as generateMnemonic,
|
|
31815
|
+
generatePrivateKey,
|
|
31205
31816
|
getAddressHrp,
|
|
31206
31817
|
getAddressId,
|
|
31207
31818
|
getAddressStorageKey,
|
|
@@ -31224,6 +31835,7 @@ export {
|
|
|
31224
31835
|
hashNametag,
|
|
31225
31836
|
hashSignMessage,
|
|
31226
31837
|
hexToBytes2 as hexToBytes,
|
|
31838
|
+
hexToWIF,
|
|
31227
31839
|
identityFromMnemonicSync,
|
|
31228
31840
|
initSphere,
|
|
31229
31841
|
isArchivedKey,
|
|
@@ -31253,6 +31865,7 @@ export {
|
|
|
31253
31865
|
logger,
|
|
31254
31866
|
mnemonicToSeedSync2 as mnemonicToSeedSync,
|
|
31255
31867
|
normalizeAddress,
|
|
31868
|
+
normalizeCoinId,
|
|
31256
31869
|
normalizeNametag3 as normalizeNametag,
|
|
31257
31870
|
normalizeSdkTokenToStorage,
|
|
31258
31871
|
objectToTxf,
|
|
@@ -31283,6 +31896,7 @@ export {
|
|
|
31283
31896
|
verifyManifestIntegrity,
|
|
31284
31897
|
verifyNametagBinding,
|
|
31285
31898
|
verifySignedMessage,
|
|
31899
|
+
verifySphereAuth,
|
|
31286
31900
|
verifySwapSignature
|
|
31287
31901
|
};
|
|
31288
31902
|
/*! Bundled license information:
|