@unicitylabs/sphere-sdk 0.2.3 → 0.2.5
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 +61 -20
- package/dist/core/index.cjs +1053 -213
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +406 -216
- package/dist/core/index.d.ts +406 -216
- package/dist/core/index.js +1052 -213
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +1988 -2
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +1988 -2
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +1874 -512
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +1874 -512
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +1988 -3
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +63 -3
- package/dist/impl/nodejs/index.d.ts +63 -3
- package/dist/impl/nodejs/index.js +1988 -3
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +1064 -203
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +422 -62
- package/dist/index.d.ts +422 -62
- package/dist/index.js +1064 -203
- package/dist/index.js.map +1 -1
- package/package.json +25 -5
package/dist/index.cjs
CHANGED
|
@@ -498,6 +498,7 @@ __export(index_exports, {
|
|
|
498
498
|
TokenRegistry: () => TokenRegistry,
|
|
499
499
|
TokenValidator: () => TokenValidator,
|
|
500
500
|
archivedKeyFromTokenId: () => archivedKeyFromTokenId,
|
|
501
|
+
areSameNametag: () => import_nostr_js_sdk3.areSameNametag,
|
|
501
502
|
base58Decode: () => base58Decode,
|
|
502
503
|
base58Encode: () => base58Encode2,
|
|
503
504
|
buildTxfStorageData: () => buildTxfStorageData,
|
|
@@ -545,6 +546,7 @@ __export(index_exports, {
|
|
|
545
546
|
hasUncommittedTransactions: () => hasUncommittedTransactions,
|
|
546
547
|
hasValidTxfData: () => hasValidTxfData,
|
|
547
548
|
hash160: () => hash160,
|
|
549
|
+
hashNametag: () => import_nostr_js_sdk3.hashNametag,
|
|
548
550
|
hexToBytes: () => hexToBytes,
|
|
549
551
|
identityFromMnemonicSync: () => identityFromMnemonicSync,
|
|
550
552
|
initSphere: () => initSphere,
|
|
@@ -556,10 +558,12 @@ __export(index_exports, {
|
|
|
556
558
|
isKnownToken: () => isKnownToken,
|
|
557
559
|
isPaymentSessionTerminal: () => isPaymentSessionTerminal,
|
|
558
560
|
isPaymentSessionTimedOut: () => isPaymentSessionTimedOut,
|
|
561
|
+
isPhoneNumber: () => import_nostr_js_sdk3.isPhoneNumber,
|
|
559
562
|
isSQLiteDatabase: () => isSQLiteDatabase,
|
|
560
563
|
isTextWalletEncrypted: () => isTextWalletEncrypted,
|
|
561
564
|
isTokenKey: () => isTokenKey,
|
|
562
565
|
isValidBech32: () => isValidBech32,
|
|
566
|
+
isValidNametag: () => isValidNametag,
|
|
563
567
|
isValidPrivateKey: () => isValidPrivateKey,
|
|
564
568
|
isValidTokenId: () => isValidTokenId,
|
|
565
569
|
isWalletDatEncrypted: () => isWalletDatEncrypted,
|
|
@@ -567,6 +571,7 @@ __export(index_exports, {
|
|
|
567
571
|
keyFromTokenId: () => keyFromTokenId,
|
|
568
572
|
loadSphere: () => loadSphere,
|
|
569
573
|
mnemonicToSeedSync: () => mnemonicToSeedSync2,
|
|
574
|
+
normalizeNametag: () => import_nostr_js_sdk3.normalizeNametag,
|
|
570
575
|
normalizeSdkTokenToStorage: () => normalizeSdkTokenToStorage,
|
|
571
576
|
objectToTxf: () => objectToTxf,
|
|
572
577
|
parseAndDecryptWalletDat: () => parseAndDecryptWalletDat,
|
|
@@ -2414,6 +2419,7 @@ var import_MintCommitment = require("@unicitylabs/state-transition-sdk/lib/trans
|
|
|
2414
2419
|
var import_HashAlgorithm2 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
2415
2420
|
var import_UnmaskedPredicate2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
2416
2421
|
var import_InclusionProofUtils2 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
2422
|
+
var import_nostr_js_sdk = require("@unicitylabs/nostr-js-sdk");
|
|
2417
2423
|
var UNICITY_TOKEN_TYPE_HEX = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
2418
2424
|
var NametagMinter = class {
|
|
2419
2425
|
client;
|
|
@@ -2438,7 +2444,8 @@ var NametagMinter = class {
|
|
|
2438
2444
|
*/
|
|
2439
2445
|
async isNametagAvailable(nametag) {
|
|
2440
2446
|
try {
|
|
2441
|
-
const
|
|
2447
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2448
|
+
const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
|
|
2442
2449
|
const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
|
|
2443
2450
|
const isMinted = await this.client.isMinted(this.trustBase, nametagTokenId);
|
|
2444
2451
|
return !isMinted;
|
|
@@ -2455,7 +2462,8 @@ var NametagMinter = class {
|
|
|
2455
2462
|
* @returns MintNametagResult with token if successful
|
|
2456
2463
|
*/
|
|
2457
2464
|
async mintNametag(nametag, ownerAddress) {
|
|
2458
|
-
const
|
|
2465
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2466
|
+
const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
|
|
2459
2467
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2460
2468
|
try {
|
|
2461
2469
|
const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
|
|
@@ -2609,7 +2617,9 @@ var STORAGE_KEYS_ADDRESS = {
|
|
|
2609
2617
|
/** Messages for this address */
|
|
2610
2618
|
MESSAGES: "messages",
|
|
2611
2619
|
/** Transaction history for this address */
|
|
2612
|
-
TRANSACTION_HISTORY: "transaction_history"
|
|
2620
|
+
TRANSACTION_HISTORY: "transaction_history",
|
|
2621
|
+
/** Pending V5 finalization tokens (unconfirmed instant split tokens) */
|
|
2622
|
+
PENDING_V5_TOKENS: "pending_v5_tokens"
|
|
2613
2623
|
};
|
|
2614
2624
|
var STORAGE_KEYS = {
|
|
2615
2625
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -3024,6 +3034,18 @@ function parseTxfStorageData(data) {
|
|
|
3024
3034
|
result.validationErrors.push(`Forked token ${parsed.tokenId}: invalid structure`);
|
|
3025
3035
|
}
|
|
3026
3036
|
}
|
|
3037
|
+
} else if (key.startsWith("token-")) {
|
|
3038
|
+
try {
|
|
3039
|
+
const entry = storageData[key];
|
|
3040
|
+
const txfToken = entry?.token;
|
|
3041
|
+
if (txfToken?.genesis?.data?.tokenId) {
|
|
3042
|
+
const tokenId = txfToken.genesis.data.tokenId;
|
|
3043
|
+
const token = txfToToken(tokenId, txfToken);
|
|
3044
|
+
result.tokens.push(token);
|
|
3045
|
+
}
|
|
3046
|
+
} catch (err) {
|
|
3047
|
+
result.validationErrors.push(`Token ${key}: ${err}`);
|
|
3048
|
+
}
|
|
3027
3049
|
}
|
|
3028
3050
|
}
|
|
3029
3051
|
return result;
|
|
@@ -3599,8 +3621,9 @@ var InstantSplitExecutor = class {
|
|
|
3599
3621
|
const criticalPathDuration = performance.now() - startTime;
|
|
3600
3622
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3601
3623
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3624
|
+
let backgroundPromise;
|
|
3602
3625
|
if (!options?.skipBackground) {
|
|
3603
|
-
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3626
|
+
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3604
3627
|
signingService: this.signingService,
|
|
3605
3628
|
tokenType: tokenToSplit.type,
|
|
3606
3629
|
coinId,
|
|
@@ -3616,7 +3639,8 @@ var InstantSplitExecutor = class {
|
|
|
3616
3639
|
nostrEventId,
|
|
3617
3640
|
splitGroupId,
|
|
3618
3641
|
criticalPathDurationMs: criticalPathDuration,
|
|
3619
|
-
backgroundStarted: !options?.skipBackground
|
|
3642
|
+
backgroundStarted: !options?.skipBackground,
|
|
3643
|
+
backgroundPromise
|
|
3620
3644
|
};
|
|
3621
3645
|
} catch (error) {
|
|
3622
3646
|
const duration = performance.now() - startTime;
|
|
@@ -3678,7 +3702,7 @@ var InstantSplitExecutor = class {
|
|
|
3678
3702
|
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3679
3703
|
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3680
3704
|
]);
|
|
3681
|
-
submissions.then(async (results) => {
|
|
3705
|
+
return submissions.then(async (results) => {
|
|
3682
3706
|
const submitDuration = performance.now() - startTime;
|
|
3683
3707
|
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3684
3708
|
context.onProgress?.({
|
|
@@ -4143,6 +4167,11 @@ var import_AddressScheme = require("@unicitylabs/state-transition-sdk/lib/addres
|
|
|
4143
4167
|
var import_UnmaskedPredicate5 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
4144
4168
|
var import_TokenState5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
4145
4169
|
var import_HashAlgorithm5 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
4170
|
+
var import_TokenType3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
4171
|
+
var import_MintCommitment3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment");
|
|
4172
|
+
var import_MintTransactionData3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
|
|
4173
|
+
var import_InclusionProofUtils5 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
4174
|
+
var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof");
|
|
4146
4175
|
function enrichWithRegistry(info) {
|
|
4147
4176
|
const registry = TokenRegistry.getInstance();
|
|
4148
4177
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -4340,6 +4369,13 @@ function extractTokenStateKey(token) {
|
|
|
4340
4369
|
if (!tokenId || !stateHash) return null;
|
|
4341
4370
|
return createTokenStateKey(tokenId, stateHash);
|
|
4342
4371
|
}
|
|
4372
|
+
function fromHex4(hex) {
|
|
4373
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
4374
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
4375
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
4376
|
+
}
|
|
4377
|
+
return bytes;
|
|
4378
|
+
}
|
|
4343
4379
|
function hasSameGenesisTokenId(t1, t2) {
|
|
4344
4380
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
4345
4381
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
@@ -4429,6 +4465,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4429
4465
|
// Token State
|
|
4430
4466
|
tokens = /* @__PURE__ */ new Map();
|
|
4431
4467
|
pendingTransfers = /* @__PURE__ */ new Map();
|
|
4468
|
+
pendingBackgroundTasks = [];
|
|
4432
4469
|
// Repository State (tombstones, archives, forked, history)
|
|
4433
4470
|
tombstones = [];
|
|
4434
4471
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
@@ -4453,6 +4490,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4453
4490
|
// Poll every 2s
|
|
4454
4491
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4455
4492
|
// Max 30 attempts (~60s)
|
|
4493
|
+
// Storage event subscriptions (push-based sync)
|
|
4494
|
+
storageEventUnsubscribers = [];
|
|
4495
|
+
syncDebounceTimer = null;
|
|
4496
|
+
static SYNC_DEBOUNCE_MS = 500;
|
|
4497
|
+
/** Sync coalescing: concurrent sync() calls share the same operation */
|
|
4498
|
+
_syncInProgress = null;
|
|
4456
4499
|
constructor(config) {
|
|
4457
4500
|
this.moduleConfig = {
|
|
4458
4501
|
autoSync: config?.autoSync ?? true,
|
|
@@ -4463,7 +4506,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4463
4506
|
};
|
|
4464
4507
|
this.l1 = config?.l1 === null ? null : new L1PaymentsModule(config?.l1);
|
|
4465
4508
|
}
|
|
4466
|
-
/**
|
|
4509
|
+
/**
|
|
4510
|
+
* Get the current module configuration (excluding L1 config).
|
|
4511
|
+
*
|
|
4512
|
+
* @returns Resolved configuration with all defaults applied.
|
|
4513
|
+
*/
|
|
4467
4514
|
getConfig() {
|
|
4468
4515
|
return this.moduleConfig;
|
|
4469
4516
|
}
|
|
@@ -4504,9 +4551,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4504
4551
|
transport: deps.transport
|
|
4505
4552
|
});
|
|
4506
4553
|
}
|
|
4507
|
-
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4508
|
-
this.handleIncomingTransfer(transfer)
|
|
4509
|
-
|
|
4554
|
+
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4555
|
+
(transfer) => this.handleIncomingTransfer(transfer)
|
|
4556
|
+
);
|
|
4510
4557
|
if (deps.transport.onPaymentRequest) {
|
|
4511
4558
|
this.unsubscribePaymentRequests = deps.transport.onPaymentRequest((request) => {
|
|
4512
4559
|
this.handleIncomingPaymentRequest(request);
|
|
@@ -4517,9 +4564,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4517
4564
|
this.handlePaymentRequestResponse(response);
|
|
4518
4565
|
});
|
|
4519
4566
|
}
|
|
4567
|
+
this.subscribeToStorageEvents();
|
|
4520
4568
|
}
|
|
4521
4569
|
/**
|
|
4522
|
-
* Load
|
|
4570
|
+
* Load all token data from storage providers and restore wallet state.
|
|
4571
|
+
*
|
|
4572
|
+
* Loads tokens, nametag data, transaction history, and pending transfers
|
|
4573
|
+
* from configured storage providers. Restores pending V5 tokens and
|
|
4574
|
+
* triggers a fire-and-forget {@link resolveUnconfirmed} call.
|
|
4523
4575
|
*/
|
|
4524
4576
|
async load() {
|
|
4525
4577
|
this.ensureInitialized();
|
|
@@ -4536,6 +4588,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4536
4588
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4537
4589
|
}
|
|
4538
4590
|
}
|
|
4591
|
+
await this.loadPendingV5Tokens();
|
|
4539
4592
|
await this.loadTokensFromFileStorage();
|
|
4540
4593
|
await this.loadNametagFromFileStorage();
|
|
4541
4594
|
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
@@ -4553,9 +4606,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4553
4606
|
this.pendingTransfers.set(transfer.id, transfer);
|
|
4554
4607
|
}
|
|
4555
4608
|
}
|
|
4609
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4610
|
+
});
|
|
4556
4611
|
}
|
|
4557
4612
|
/**
|
|
4558
|
-
* Cleanup
|
|
4613
|
+
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
4614
|
+
*
|
|
4615
|
+
* Should be called when the wallet is being shut down or the module is
|
|
4616
|
+
* no longer needed. Also destroys the L1 sub-module if present.
|
|
4559
4617
|
*/
|
|
4560
4618
|
destroy() {
|
|
4561
4619
|
this.unsubscribeTransfers?.();
|
|
@@ -4573,6 +4631,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4573
4631
|
resolver.reject(new Error("Module destroyed"));
|
|
4574
4632
|
}
|
|
4575
4633
|
this.pendingResponseResolvers.clear();
|
|
4634
|
+
this.unsubscribeStorageEvents();
|
|
4576
4635
|
if (this.l1) {
|
|
4577
4636
|
this.l1.destroy();
|
|
4578
4637
|
}
|
|
@@ -4589,7 +4648,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4589
4648
|
const result = {
|
|
4590
4649
|
id: crypto.randomUUID(),
|
|
4591
4650
|
status: "pending",
|
|
4592
|
-
tokens: []
|
|
4651
|
+
tokens: [],
|
|
4652
|
+
tokenTransfers: []
|
|
4593
4653
|
};
|
|
4594
4654
|
try {
|
|
4595
4655
|
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
@@ -4626,69 +4686,147 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4626
4686
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4627
4687
|
result.status = "submitted";
|
|
4628
4688
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4689
|
+
const transferMode = request.transferMode ?? "instant";
|
|
4629
4690
|
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4691
|
+
if (transferMode === "conservative") {
|
|
4692
|
+
this.log("Executing conservative split...");
|
|
4693
|
+
const splitExecutor = new TokenSplitExecutor({
|
|
4694
|
+
stateTransitionClient: stClient,
|
|
4695
|
+
trustBase,
|
|
4696
|
+
signingService
|
|
4697
|
+
});
|
|
4698
|
+
const splitResult = await splitExecutor.executeSplit(
|
|
4699
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4700
|
+
splitPlan.splitAmount,
|
|
4701
|
+
splitPlan.remainderAmount,
|
|
4702
|
+
splitPlan.coinId,
|
|
4703
|
+
recipientAddress
|
|
4704
|
+
);
|
|
4705
|
+
const changeTokenData = splitResult.tokenForSender.toJSON();
|
|
4706
|
+
const changeUiToken = {
|
|
4707
|
+
id: crypto.randomUUID(),
|
|
4708
|
+
coinId: request.coinId,
|
|
4709
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4710
|
+
name: this.getCoinName(request.coinId),
|
|
4711
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4712
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4713
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4714
|
+
status: "confirmed",
|
|
4715
|
+
createdAt: Date.now(),
|
|
4716
|
+
updatedAt: Date.now(),
|
|
4717
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4718
|
+
};
|
|
4719
|
+
await this.addToken(changeUiToken, true);
|
|
4720
|
+
this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
|
|
4721
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4722
|
+
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
4723
|
+
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
4724
|
+
memo: request.memo
|
|
4725
|
+
});
|
|
4726
|
+
const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
|
|
4727
|
+
const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
|
|
4728
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
|
|
4729
|
+
result.tokenTransfers.push({
|
|
4730
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4731
|
+
method: "split",
|
|
4732
|
+
requestIdHex: splitRequestIdHex
|
|
4733
|
+
});
|
|
4734
|
+
this.log(`Conservative split transfer completed`);
|
|
4735
|
+
} else {
|
|
4736
|
+
this.log("Executing instant split...");
|
|
4737
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4738
|
+
const executor = new InstantSplitExecutor({
|
|
4739
|
+
stateTransitionClient: stClient,
|
|
4740
|
+
trustBase,
|
|
4741
|
+
signingService,
|
|
4742
|
+
devMode
|
|
4743
|
+
});
|
|
4744
|
+
const instantResult = await executor.executeSplitInstant(
|
|
4745
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4746
|
+
splitPlan.splitAmount,
|
|
4747
|
+
splitPlan.remainderAmount,
|
|
4748
|
+
splitPlan.coinId,
|
|
4749
|
+
recipientAddress,
|
|
4750
|
+
this.deps.transport,
|
|
4751
|
+
recipientPubkey,
|
|
4752
|
+
{
|
|
4753
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4754
|
+
const changeTokenData = changeToken.toJSON();
|
|
4755
|
+
const uiToken = {
|
|
4756
|
+
id: crypto.randomUUID(),
|
|
4757
|
+
coinId: request.coinId,
|
|
4758
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4759
|
+
name: this.getCoinName(request.coinId),
|
|
4760
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4761
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4762
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4763
|
+
status: "confirmed",
|
|
4764
|
+
createdAt: Date.now(),
|
|
4765
|
+
updatedAt: Date.now(),
|
|
4766
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4767
|
+
};
|
|
4768
|
+
await this.addToken(uiToken, true);
|
|
4769
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4770
|
+
},
|
|
4771
|
+
onStorageSync: async () => {
|
|
4772
|
+
await this.save();
|
|
4773
|
+
return true;
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
);
|
|
4777
|
+
if (!instantResult.success) {
|
|
4778
|
+
throw new Error(instantResult.error || "Instant split failed");
|
|
4779
|
+
}
|
|
4780
|
+
if (instantResult.backgroundPromise) {
|
|
4781
|
+
this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
|
|
4782
|
+
}
|
|
4783
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
4784
|
+
result.tokenTransfers.push({
|
|
4785
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4786
|
+
method: "split",
|
|
4787
|
+
splitGroupId: instantResult.splitGroupId,
|
|
4788
|
+
nostrEventId: instantResult.nostrEventId
|
|
4789
|
+
});
|
|
4790
|
+
this.log(`Instant split transfer completed`);
|
|
4791
|
+
}
|
|
4669
4792
|
}
|
|
4670
4793
|
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4671
4794
|
const token = tokenWithAmount.uiToken;
|
|
4672
4795
|
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4796
|
+
if (transferMode === "conservative") {
|
|
4797
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4798
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4799
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4800
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4801
|
+
}
|
|
4802
|
+
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
4803
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4804
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4805
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4806
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4807
|
+
memo: request.memo
|
|
4808
|
+
});
|
|
4809
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4810
|
+
} else {
|
|
4811
|
+
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4812
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4813
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4814
|
+
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4815
|
+
memo: request.memo
|
|
4816
|
+
});
|
|
4817
|
+
console.log(`[Payments] NOSTR-FIRST: Direct token sent successfully`);
|
|
4818
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4819
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4820
|
+
);
|
|
4679
4821
|
}
|
|
4680
|
-
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
4681
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4682
4822
|
const requestIdBytes = commitment.requestId;
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
memo: request.memo
|
|
4823
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4824
|
+
result.tokenTransfers.push({
|
|
4825
|
+
sourceTokenId: token.id,
|
|
4826
|
+
method: "direct",
|
|
4827
|
+
requestIdHex
|
|
4689
4828
|
});
|
|
4690
|
-
|
|
4691
|
-
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
4829
|
+
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4692
4830
|
await this.removeToken(token.id, recipientNametag, true);
|
|
4693
4831
|
}
|
|
4694
4832
|
result.status = "delivered";
|
|
@@ -4701,7 +4839,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4701
4839
|
coinId: request.coinId,
|
|
4702
4840
|
symbol: this.getCoinSymbol(request.coinId),
|
|
4703
4841
|
timestamp: Date.now(),
|
|
4704
|
-
recipientNametag
|
|
4842
|
+
recipientNametag,
|
|
4843
|
+
transferId: result.id
|
|
4705
4844
|
});
|
|
4706
4845
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4707
4846
|
return result;
|
|
@@ -4837,6 +4976,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4837
4976
|
}
|
|
4838
4977
|
);
|
|
4839
4978
|
if (result.success) {
|
|
4979
|
+
if (result.backgroundPromise) {
|
|
4980
|
+
this.pendingBackgroundTasks.push(result.backgroundPromise);
|
|
4981
|
+
}
|
|
4840
4982
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4841
4983
|
await this.removeToken(tokenToSplit.id, recipientNametag, true);
|
|
4842
4984
|
await this.addToHistory({
|
|
@@ -4878,6 +5020,63 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4878
5020
|
*/
|
|
4879
5021
|
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4880
5022
|
this.ensureInitialized();
|
|
5023
|
+
if (!isInstantSplitBundleV5(bundle)) {
|
|
5024
|
+
return this.processInstantSplitBundleSync(bundle, senderPubkey);
|
|
5025
|
+
}
|
|
5026
|
+
try {
|
|
5027
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
5028
|
+
if (this.tokens.has(deterministicId)) {
|
|
5029
|
+
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
5030
|
+
return { success: true, durationMs: 0 };
|
|
5031
|
+
}
|
|
5032
|
+
const registry = TokenRegistry.getInstance();
|
|
5033
|
+
const pendingData = {
|
|
5034
|
+
type: "v5_bundle",
|
|
5035
|
+
stage: "RECEIVED",
|
|
5036
|
+
bundleJson: JSON.stringify(bundle),
|
|
5037
|
+
senderPubkey,
|
|
5038
|
+
savedAt: Date.now(),
|
|
5039
|
+
attemptCount: 0
|
|
5040
|
+
};
|
|
5041
|
+
const uiToken = {
|
|
5042
|
+
id: deterministicId,
|
|
5043
|
+
coinId: bundle.coinId,
|
|
5044
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5045
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5046
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5047
|
+
amount: bundle.amount,
|
|
5048
|
+
status: "submitted",
|
|
5049
|
+
// UNCONFIRMED
|
|
5050
|
+
createdAt: Date.now(),
|
|
5051
|
+
updatedAt: Date.now(),
|
|
5052
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5053
|
+
};
|
|
5054
|
+
await this.addToken(uiToken, false);
|
|
5055
|
+
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
5056
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
5057
|
+
id: bundle.splitGroupId,
|
|
5058
|
+
senderPubkey,
|
|
5059
|
+
tokens: [uiToken],
|
|
5060
|
+
receivedAt: Date.now()
|
|
5061
|
+
});
|
|
5062
|
+
await this.save();
|
|
5063
|
+
this.resolveUnconfirmed().catch(() => {
|
|
5064
|
+
});
|
|
5065
|
+
return { success: true, durationMs: 0 };
|
|
5066
|
+
} catch (error) {
|
|
5067
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5068
|
+
return {
|
|
5069
|
+
success: false,
|
|
5070
|
+
error: errorMessage,
|
|
5071
|
+
durationMs: 0
|
|
5072
|
+
};
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
/**
|
|
5076
|
+
* Synchronous V4 bundle processing (dev mode only).
|
|
5077
|
+
* Kept for backward compatibility with V4 bundles.
|
|
5078
|
+
*/
|
|
5079
|
+
async processInstantSplitBundleSync(bundle, senderPubkey) {
|
|
4881
5080
|
try {
|
|
4882
5081
|
const signingService = await this.createSigningService();
|
|
4883
5082
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
@@ -4963,7 +5162,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4963
5162
|
}
|
|
4964
5163
|
}
|
|
4965
5164
|
/**
|
|
4966
|
-
*
|
|
5165
|
+
* Type-guard: check whether a payload is a valid {@link InstantSplitBundle} (V4 or V5).
|
|
5166
|
+
*
|
|
5167
|
+
* @param payload - The object to test.
|
|
5168
|
+
* @returns `true` if the payload matches the InstantSplitBundle shape.
|
|
4967
5169
|
*/
|
|
4968
5170
|
isInstantSplitBundle(payload) {
|
|
4969
5171
|
return isInstantSplitBundle(payload);
|
|
@@ -5044,39 +5246,57 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5044
5246
|
return [...this.paymentRequests];
|
|
5045
5247
|
}
|
|
5046
5248
|
/**
|
|
5047
|
-
* Get
|
|
5249
|
+
* Get the count of payment requests with status `'pending'`.
|
|
5250
|
+
*
|
|
5251
|
+
* @returns Number of pending incoming payment requests.
|
|
5048
5252
|
*/
|
|
5049
5253
|
getPendingPaymentRequestsCount() {
|
|
5050
5254
|
return this.paymentRequests.filter((r) => r.status === "pending").length;
|
|
5051
5255
|
}
|
|
5052
5256
|
/**
|
|
5053
|
-
* Accept a payment request
|
|
5257
|
+
* Accept a payment request and notify the requester.
|
|
5258
|
+
*
|
|
5259
|
+
* Marks the request as `'accepted'` and sends a response via transport.
|
|
5260
|
+
* The caller should subsequently call {@link send} to fulfill the payment.
|
|
5261
|
+
*
|
|
5262
|
+
* @param requestId - ID of the incoming payment request to accept.
|
|
5054
5263
|
*/
|
|
5055
5264
|
async acceptPaymentRequest(requestId2) {
|
|
5056
5265
|
this.updatePaymentRequestStatus(requestId2, "accepted");
|
|
5057
5266
|
await this.sendPaymentRequestResponse(requestId2, "accepted");
|
|
5058
5267
|
}
|
|
5059
5268
|
/**
|
|
5060
|
-
* Reject a payment request
|
|
5269
|
+
* Reject a payment request and notify the requester.
|
|
5270
|
+
*
|
|
5271
|
+
* @param requestId - ID of the incoming payment request to reject.
|
|
5061
5272
|
*/
|
|
5062
5273
|
async rejectPaymentRequest(requestId2) {
|
|
5063
5274
|
this.updatePaymentRequestStatus(requestId2, "rejected");
|
|
5064
5275
|
await this.sendPaymentRequestResponse(requestId2, "rejected");
|
|
5065
5276
|
}
|
|
5066
5277
|
/**
|
|
5067
|
-
* Mark a payment request as paid (
|
|
5278
|
+
* Mark a payment request as paid (local status update only).
|
|
5279
|
+
*
|
|
5280
|
+
* Typically called after a successful {@link send} to record that the
|
|
5281
|
+
* request has been fulfilled.
|
|
5282
|
+
*
|
|
5283
|
+
* @param requestId - ID of the incoming payment request to mark as paid.
|
|
5068
5284
|
*/
|
|
5069
5285
|
markPaymentRequestPaid(requestId2) {
|
|
5070
5286
|
this.updatePaymentRequestStatus(requestId2, "paid");
|
|
5071
5287
|
}
|
|
5072
5288
|
/**
|
|
5073
|
-
*
|
|
5289
|
+
* Remove all non-pending incoming payment requests from memory.
|
|
5290
|
+
*
|
|
5291
|
+
* Keeps only requests with status `'pending'`.
|
|
5074
5292
|
*/
|
|
5075
5293
|
clearProcessedPaymentRequests() {
|
|
5076
5294
|
this.paymentRequests = this.paymentRequests.filter((r) => r.status === "pending");
|
|
5077
5295
|
}
|
|
5078
5296
|
/**
|
|
5079
|
-
* Remove a specific payment request
|
|
5297
|
+
* Remove a specific incoming payment request by ID.
|
|
5298
|
+
*
|
|
5299
|
+
* @param requestId - ID of the payment request to remove.
|
|
5080
5300
|
*/
|
|
5081
5301
|
removePaymentRequest(requestId2) {
|
|
5082
5302
|
this.paymentRequests = this.paymentRequests.filter((r) => r.id !== requestId2);
|
|
@@ -5201,7 +5421,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5201
5421
|
});
|
|
5202
5422
|
}
|
|
5203
5423
|
/**
|
|
5204
|
-
* Cancel
|
|
5424
|
+
* Cancel an active {@link waitForPaymentResponse} call.
|
|
5425
|
+
*
|
|
5426
|
+
* The pending promise is rejected with a `'Cancelled'` error.
|
|
5427
|
+
*
|
|
5428
|
+
* @param requestId - The outgoing request ID whose wait should be cancelled.
|
|
5205
5429
|
*/
|
|
5206
5430
|
cancelWaitForPaymentResponse(requestId2) {
|
|
5207
5431
|
const resolver = this.pendingResponseResolvers.get(requestId2);
|
|
@@ -5212,14 +5436,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5212
5436
|
}
|
|
5213
5437
|
}
|
|
5214
5438
|
/**
|
|
5215
|
-
* Remove an outgoing payment request
|
|
5439
|
+
* Remove an outgoing payment request and cancel any pending wait.
|
|
5440
|
+
*
|
|
5441
|
+
* @param requestId - ID of the outgoing request to remove.
|
|
5216
5442
|
*/
|
|
5217
5443
|
removeOutgoingPaymentRequest(requestId2) {
|
|
5218
5444
|
this.outgoingPaymentRequests.delete(requestId2);
|
|
5219
5445
|
this.cancelWaitForPaymentResponse(requestId2);
|
|
5220
5446
|
}
|
|
5221
5447
|
/**
|
|
5222
|
-
*
|
|
5448
|
+
* Remove all outgoing payment requests that are `'paid'`, `'rejected'`, or `'expired'`.
|
|
5223
5449
|
*/
|
|
5224
5450
|
clearCompletedOutgoingPaymentRequests() {
|
|
5225
5451
|
for (const [id, request] of this.outgoingPaymentRequests) {
|
|
@@ -5291,6 +5517,71 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5291
5517
|
}
|
|
5292
5518
|
}
|
|
5293
5519
|
// ===========================================================================
|
|
5520
|
+
// Public API - Receive
|
|
5521
|
+
// ===========================================================================
|
|
5522
|
+
/**
|
|
5523
|
+
* Fetch and process pending incoming transfers from the transport layer.
|
|
5524
|
+
*
|
|
5525
|
+
* Performs a one-shot query to fetch all pending events, processes them
|
|
5526
|
+
* through the existing pipeline, and resolves after all stored events
|
|
5527
|
+
* are handled. Useful for batch/CLI apps that need explicit receive.
|
|
5528
|
+
*
|
|
5529
|
+
* When `finalize` is true, polls resolveUnconfirmed() + load() until all
|
|
5530
|
+
* tokens are confirmed or the timeout expires. Otherwise calls
|
|
5531
|
+
* resolveUnconfirmed() once to submit pending commitments.
|
|
5532
|
+
*
|
|
5533
|
+
* @param options - Optional receive options including finalization control
|
|
5534
|
+
* @param callback - Optional callback invoked for each newly received transfer
|
|
5535
|
+
* @returns ReceiveResult with transfers and finalization metadata
|
|
5536
|
+
*/
|
|
5537
|
+
async receive(options, callback) {
|
|
5538
|
+
this.ensureInitialized();
|
|
5539
|
+
if (!this.deps.transport.fetchPendingEvents) {
|
|
5540
|
+
throw new Error("Transport provider does not support fetchPendingEvents");
|
|
5541
|
+
}
|
|
5542
|
+
const opts = options ?? {};
|
|
5543
|
+
const tokensBefore = new Set(this.tokens.keys());
|
|
5544
|
+
await this.deps.transport.fetchPendingEvents();
|
|
5545
|
+
await this.load();
|
|
5546
|
+
const received = [];
|
|
5547
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5548
|
+
if (!tokensBefore.has(tokenId)) {
|
|
5549
|
+
const transfer = {
|
|
5550
|
+
id: tokenId,
|
|
5551
|
+
senderPubkey: "",
|
|
5552
|
+
tokens: [token],
|
|
5553
|
+
receivedAt: Date.now()
|
|
5554
|
+
};
|
|
5555
|
+
received.push(transfer);
|
|
5556
|
+
if (callback) callback(transfer);
|
|
5557
|
+
}
|
|
5558
|
+
}
|
|
5559
|
+
const result = { transfers: received };
|
|
5560
|
+
if (opts.finalize) {
|
|
5561
|
+
const timeout = opts.timeout ?? 6e4;
|
|
5562
|
+
const pollInterval = opts.pollInterval ?? 2e3;
|
|
5563
|
+
const startTime = Date.now();
|
|
5564
|
+
while (Date.now() - startTime < timeout) {
|
|
5565
|
+
const resolution = await this.resolveUnconfirmed();
|
|
5566
|
+
result.finalization = resolution;
|
|
5567
|
+
if (opts.onProgress) opts.onProgress(resolution);
|
|
5568
|
+
const stillUnconfirmed = Array.from(this.tokens.values()).some(
|
|
5569
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5570
|
+
);
|
|
5571
|
+
if (!stillUnconfirmed) break;
|
|
5572
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
5573
|
+
await this.load();
|
|
5574
|
+
}
|
|
5575
|
+
result.finalizationDurationMs = Date.now() - startTime;
|
|
5576
|
+
result.timedOut = Array.from(this.tokens.values()).some(
|
|
5577
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5578
|
+
);
|
|
5579
|
+
} else {
|
|
5580
|
+
result.finalization = await this.resolveUnconfirmed();
|
|
5581
|
+
}
|
|
5582
|
+
return result;
|
|
5583
|
+
}
|
|
5584
|
+
// ===========================================================================
|
|
5294
5585
|
// Public API - Balance & Tokens
|
|
5295
5586
|
// ===========================================================================
|
|
5296
5587
|
/**
|
|
@@ -5300,10 +5591,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5300
5591
|
this.priceProvider = provider;
|
|
5301
5592
|
}
|
|
5302
5593
|
/**
|
|
5303
|
-
*
|
|
5304
|
-
*
|
|
5594
|
+
* Wait for all pending background operations (e.g., instant split change token creation).
|
|
5595
|
+
* Call this before process exit to ensure all tokens are saved.
|
|
5305
5596
|
*/
|
|
5306
|
-
async
|
|
5597
|
+
async waitForPendingOperations() {
|
|
5598
|
+
if (this.pendingBackgroundTasks.length > 0) {
|
|
5599
|
+
await Promise.allSettled(this.pendingBackgroundTasks);
|
|
5600
|
+
this.pendingBackgroundTasks = [];
|
|
5601
|
+
}
|
|
5602
|
+
}
|
|
5603
|
+
/**
|
|
5604
|
+
* Get total portfolio value in USD.
|
|
5605
|
+
* Returns null if PriceProvider is not configured.
|
|
5606
|
+
*/
|
|
5607
|
+
async getFiatBalance() {
|
|
5307
5608
|
const assets = await this.getAssets();
|
|
5308
5609
|
if (!this.priceProvider) {
|
|
5309
5610
|
return null;
|
|
@@ -5319,19 +5620,95 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5319
5620
|
return hasAnyPrice ? total : null;
|
|
5320
5621
|
}
|
|
5321
5622
|
/**
|
|
5322
|
-
* Get
|
|
5323
|
-
*
|
|
5623
|
+
* Get token balances grouped by coin type.
|
|
5624
|
+
*
|
|
5625
|
+
* Returns an array of {@link Asset} objects, one per coin type held.
|
|
5626
|
+
* Each entry includes confirmed and unconfirmed breakdowns. Tokens with
|
|
5627
|
+
* status `'spent'`, `'invalid'`, or `'transferring'` are excluded.
|
|
5628
|
+
*
|
|
5629
|
+
* This is synchronous — no price data is included. Use {@link getAssets}
|
|
5630
|
+
* for the async version with fiat pricing.
|
|
5631
|
+
*
|
|
5632
|
+
* @param coinId - Optional coin ID to filter by (e.g. hex string). When omitted, all coin types are returned.
|
|
5633
|
+
* @returns Array of balance summaries (synchronous — no await needed).
|
|
5634
|
+
*/
|
|
5635
|
+
getBalance(coinId) {
|
|
5636
|
+
return this.aggregateTokens(coinId);
|
|
5637
|
+
}
|
|
5638
|
+
/**
|
|
5639
|
+
* Get aggregated assets (tokens grouped by coinId) with price data.
|
|
5640
|
+
* Includes both confirmed and unconfirmed tokens with breakdown.
|
|
5324
5641
|
*/
|
|
5325
5642
|
async getAssets(coinId) {
|
|
5643
|
+
const rawAssets = this.aggregateTokens(coinId);
|
|
5644
|
+
if (!this.priceProvider || rawAssets.length === 0) {
|
|
5645
|
+
return rawAssets;
|
|
5646
|
+
}
|
|
5647
|
+
try {
|
|
5648
|
+
const registry = TokenRegistry.getInstance();
|
|
5649
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5650
|
+
for (const asset of rawAssets) {
|
|
5651
|
+
const def = registry.getDefinition(asset.coinId);
|
|
5652
|
+
if (def?.name) {
|
|
5653
|
+
const existing = nameToCoins.get(def.name);
|
|
5654
|
+
if (existing) {
|
|
5655
|
+
existing.push(asset.coinId);
|
|
5656
|
+
} else {
|
|
5657
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
if (nameToCoins.size > 0) {
|
|
5662
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
5663
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5664
|
+
return rawAssets.map((raw) => {
|
|
5665
|
+
const def = registry.getDefinition(raw.coinId);
|
|
5666
|
+
const price = def?.name ? prices.get(def.name) : void 0;
|
|
5667
|
+
let fiatValueUsd = null;
|
|
5668
|
+
let fiatValueEur = null;
|
|
5669
|
+
if (price) {
|
|
5670
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5671
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5672
|
+
if (price.priceEur != null) {
|
|
5673
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
return {
|
|
5677
|
+
...raw,
|
|
5678
|
+
priceUsd: price?.priceUsd ?? null,
|
|
5679
|
+
priceEur: price?.priceEur ?? null,
|
|
5680
|
+
change24h: price?.change24h ?? null,
|
|
5681
|
+
fiatValueUsd,
|
|
5682
|
+
fiatValueEur
|
|
5683
|
+
};
|
|
5684
|
+
});
|
|
5685
|
+
}
|
|
5686
|
+
} catch (error) {
|
|
5687
|
+
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5688
|
+
}
|
|
5689
|
+
return rawAssets;
|
|
5690
|
+
}
|
|
5691
|
+
/**
|
|
5692
|
+
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5693
|
+
* Excludes tokens with status 'spent', 'invalid', or 'transferring'.
|
|
5694
|
+
*/
|
|
5695
|
+
aggregateTokens(coinId) {
|
|
5326
5696
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5327
5697
|
for (const token of this.tokens.values()) {
|
|
5328
|
-
if (token.status
|
|
5698
|
+
if (token.status === "spent" || token.status === "invalid" || token.status === "transferring") continue;
|
|
5329
5699
|
if (coinId && token.coinId !== coinId) continue;
|
|
5330
5700
|
const key = token.coinId;
|
|
5701
|
+
const amount = BigInt(token.amount);
|
|
5702
|
+
const isConfirmed = token.status === "confirmed";
|
|
5331
5703
|
const existing = assetsMap.get(key);
|
|
5332
5704
|
if (existing) {
|
|
5333
|
-
|
|
5334
|
-
|
|
5705
|
+
if (isConfirmed) {
|
|
5706
|
+
existing.confirmedAmount += amount;
|
|
5707
|
+
existing.confirmedTokenCount++;
|
|
5708
|
+
} else {
|
|
5709
|
+
existing.unconfirmedAmount += amount;
|
|
5710
|
+
existing.unconfirmedTokenCount++;
|
|
5711
|
+
}
|
|
5335
5712
|
} else {
|
|
5336
5713
|
assetsMap.set(key, {
|
|
5337
5714
|
coinId: token.coinId,
|
|
@@ -5339,78 +5716,42 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5339
5716
|
name: token.name,
|
|
5340
5717
|
decimals: token.decimals,
|
|
5341
5718
|
iconUrl: token.iconUrl,
|
|
5342
|
-
|
|
5343
|
-
|
|
5719
|
+
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5720
|
+
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5721
|
+
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5722
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
5344
5723
|
});
|
|
5345
5724
|
}
|
|
5346
5725
|
}
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
if (this.priceProvider && rawAssets.length > 0) {
|
|
5350
|
-
try {
|
|
5351
|
-
const registry = TokenRegistry.getInstance();
|
|
5352
|
-
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5353
|
-
for (const asset of rawAssets) {
|
|
5354
|
-
const def = registry.getDefinition(asset.coinId);
|
|
5355
|
-
if (def?.name) {
|
|
5356
|
-
const existing = nameToCoins.get(def.name);
|
|
5357
|
-
if (existing) {
|
|
5358
|
-
existing.push(asset.coinId);
|
|
5359
|
-
} else {
|
|
5360
|
-
nameToCoins.set(def.name, [asset.coinId]);
|
|
5361
|
-
}
|
|
5362
|
-
}
|
|
5363
|
-
}
|
|
5364
|
-
if (nameToCoins.size > 0) {
|
|
5365
|
-
const tokenNames = Array.from(nameToCoins.keys());
|
|
5366
|
-
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5367
|
-
priceMap = /* @__PURE__ */ new Map();
|
|
5368
|
-
for (const [name, coinIds] of nameToCoins) {
|
|
5369
|
-
const price = prices.get(name);
|
|
5370
|
-
if (price) {
|
|
5371
|
-
for (const cid of coinIds) {
|
|
5372
|
-
priceMap.set(cid, {
|
|
5373
|
-
priceUsd: price.priceUsd,
|
|
5374
|
-
priceEur: price.priceEur,
|
|
5375
|
-
change24h: price.change24h
|
|
5376
|
-
});
|
|
5377
|
-
}
|
|
5378
|
-
}
|
|
5379
|
-
}
|
|
5380
|
-
}
|
|
5381
|
-
} catch (error) {
|
|
5382
|
-
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5383
|
-
}
|
|
5384
|
-
}
|
|
5385
|
-
return rawAssets.map((raw) => {
|
|
5386
|
-
const price = priceMap?.get(raw.coinId);
|
|
5387
|
-
let fiatValueUsd = null;
|
|
5388
|
-
let fiatValueEur = null;
|
|
5389
|
-
if (price) {
|
|
5390
|
-
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5391
|
-
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5392
|
-
if (price.priceEur != null) {
|
|
5393
|
-
fiatValueEur = humanAmount * price.priceEur;
|
|
5394
|
-
}
|
|
5395
|
-
}
|
|
5726
|
+
return Array.from(assetsMap.values()).map((raw) => {
|
|
5727
|
+
const totalAmount = (raw.confirmedAmount + raw.unconfirmedAmount).toString();
|
|
5396
5728
|
return {
|
|
5397
5729
|
coinId: raw.coinId,
|
|
5398
5730
|
symbol: raw.symbol,
|
|
5399
5731
|
name: raw.name,
|
|
5400
5732
|
decimals: raw.decimals,
|
|
5401
5733
|
iconUrl: raw.iconUrl,
|
|
5402
|
-
totalAmount
|
|
5403
|
-
tokenCount: raw.
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5734
|
+
totalAmount,
|
|
5735
|
+
tokenCount: raw.confirmedTokenCount + raw.unconfirmedTokenCount,
|
|
5736
|
+
confirmedAmount: raw.confirmedAmount.toString(),
|
|
5737
|
+
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5738
|
+
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5739
|
+
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
5740
|
+
priceUsd: null,
|
|
5741
|
+
priceEur: null,
|
|
5742
|
+
change24h: null,
|
|
5743
|
+
fiatValueUsd: null,
|
|
5744
|
+
fiatValueEur: null
|
|
5409
5745
|
};
|
|
5410
5746
|
});
|
|
5411
5747
|
}
|
|
5412
5748
|
/**
|
|
5413
|
-
* Get all tokens
|
|
5749
|
+
* Get all tokens, optionally filtered by coin type and/or status.
|
|
5750
|
+
*
|
|
5751
|
+
* @param filter - Optional filter criteria.
|
|
5752
|
+
* @param filter.coinId - Return only tokens of this coin type.
|
|
5753
|
+
* @param filter.status - Return only tokens with this status (e.g. `'submitted'` for unconfirmed).
|
|
5754
|
+
* @returns Array of matching {@link Token} objects (synchronous).
|
|
5414
5755
|
*/
|
|
5415
5756
|
getTokens(filter) {
|
|
5416
5757
|
let tokens = Array.from(this.tokens.values());
|
|
@@ -5423,19 +5764,327 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5423
5764
|
return tokens;
|
|
5424
5765
|
}
|
|
5425
5766
|
/**
|
|
5426
|
-
* Get single token
|
|
5767
|
+
* Get a single token by its local ID.
|
|
5768
|
+
*
|
|
5769
|
+
* @param id - The local UUID assigned when the token was added.
|
|
5770
|
+
* @returns The token, or `undefined` if not found.
|
|
5427
5771
|
*/
|
|
5428
5772
|
getToken(id) {
|
|
5429
5773
|
return this.tokens.get(id);
|
|
5430
5774
|
}
|
|
5431
5775
|
// ===========================================================================
|
|
5776
|
+
// Public API - Unconfirmed Token Resolution
|
|
5777
|
+
// ===========================================================================
|
|
5778
|
+
/**
|
|
5779
|
+
* Attempt to resolve unconfirmed (status `'submitted'`) tokens by acquiring
|
|
5780
|
+
* their missing aggregator proofs.
|
|
5781
|
+
*
|
|
5782
|
+
* Each unconfirmed V5 token progresses through stages:
|
|
5783
|
+
* `RECEIVED` → `MINT_SUBMITTED` → `MINT_PROVEN` → `TRANSFER_SUBMITTED` → `FINALIZED`
|
|
5784
|
+
*
|
|
5785
|
+
* Uses 500 ms quick-timeouts per proof check so the call returns quickly even
|
|
5786
|
+
* when proofs are not yet available. Tokens that exceed 50 failed attempts are
|
|
5787
|
+
* marked `'invalid'`.
|
|
5788
|
+
*
|
|
5789
|
+
* Automatically called (fire-and-forget) by {@link load}.
|
|
5790
|
+
*
|
|
5791
|
+
* @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
|
|
5792
|
+
*/
|
|
5793
|
+
async resolveUnconfirmed() {
|
|
5794
|
+
this.ensureInitialized();
|
|
5795
|
+
const result = {
|
|
5796
|
+
resolved: 0,
|
|
5797
|
+
stillPending: 0,
|
|
5798
|
+
failed: 0,
|
|
5799
|
+
details: []
|
|
5800
|
+
};
|
|
5801
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5802
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5803
|
+
if (!stClient || !trustBase) return result;
|
|
5804
|
+
const signingService = await this.createSigningService();
|
|
5805
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5806
|
+
if (token.status !== "submitted") continue;
|
|
5807
|
+
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5808
|
+
if (!pending2) {
|
|
5809
|
+
result.stillPending++;
|
|
5810
|
+
continue;
|
|
5811
|
+
}
|
|
5812
|
+
if (pending2.type === "v5_bundle") {
|
|
5813
|
+
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
5814
|
+
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5815
|
+
if (progress === "resolved") result.resolved++;
|
|
5816
|
+
else if (progress === "failed") result.failed++;
|
|
5817
|
+
else result.stillPending++;
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
if (result.resolved > 0 || result.failed > 0) {
|
|
5821
|
+
await this.save();
|
|
5822
|
+
}
|
|
5823
|
+
return result;
|
|
5824
|
+
}
|
|
5825
|
+
// ===========================================================================
|
|
5826
|
+
// Private - V5 Lazy Resolution Helpers
|
|
5827
|
+
// ===========================================================================
|
|
5828
|
+
/**
|
|
5829
|
+
* Process a single V5 token through its finalization stages with quick-timeout proof checks.
|
|
5830
|
+
*/
|
|
5831
|
+
async resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService) {
|
|
5832
|
+
const bundle = JSON.parse(pending2.bundleJson);
|
|
5833
|
+
pending2.attemptCount++;
|
|
5834
|
+
pending2.lastAttemptAt = Date.now();
|
|
5835
|
+
try {
|
|
5836
|
+
if (pending2.stage === "RECEIVED") {
|
|
5837
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5838
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5839
|
+
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5840
|
+
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
5841
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5842
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5843
|
+
}
|
|
5844
|
+
pending2.stage = "MINT_SUBMITTED";
|
|
5845
|
+
this.updatePendingFinalization(token, pending2);
|
|
5846
|
+
}
|
|
5847
|
+
if (pending2.stage === "MINT_SUBMITTED") {
|
|
5848
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5849
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5850
|
+
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5851
|
+
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5852
|
+
if (!proof) {
|
|
5853
|
+
this.updatePendingFinalization(token, pending2);
|
|
5854
|
+
return "pending";
|
|
5855
|
+
}
|
|
5856
|
+
pending2.mintProofJson = JSON.stringify(proof);
|
|
5857
|
+
pending2.stage = "MINT_PROVEN";
|
|
5858
|
+
this.updatePendingFinalization(token, pending2);
|
|
5859
|
+
}
|
|
5860
|
+
if (pending2.stage === "MINT_PROVEN") {
|
|
5861
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5862
|
+
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5863
|
+
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
5864
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5865
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5866
|
+
}
|
|
5867
|
+
pending2.stage = "TRANSFER_SUBMITTED";
|
|
5868
|
+
this.updatePendingFinalization(token, pending2);
|
|
5869
|
+
}
|
|
5870
|
+
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
5871
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5872
|
+
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5873
|
+
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5874
|
+
if (!proof) {
|
|
5875
|
+
this.updatePendingFinalization(token, pending2);
|
|
5876
|
+
return "pending";
|
|
5877
|
+
}
|
|
5878
|
+
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5879
|
+
const confirmedToken = {
|
|
5880
|
+
id: token.id,
|
|
5881
|
+
coinId: token.coinId,
|
|
5882
|
+
symbol: token.symbol,
|
|
5883
|
+
name: token.name,
|
|
5884
|
+
decimals: token.decimals,
|
|
5885
|
+
iconUrl: token.iconUrl,
|
|
5886
|
+
amount: token.amount,
|
|
5887
|
+
status: "confirmed",
|
|
5888
|
+
createdAt: token.createdAt,
|
|
5889
|
+
updatedAt: Date.now(),
|
|
5890
|
+
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5891
|
+
};
|
|
5892
|
+
this.tokens.set(tokenId, confirmedToken);
|
|
5893
|
+
await this.saveTokenToFileStorage(confirmedToken);
|
|
5894
|
+
await this.addToHistory({
|
|
5895
|
+
type: "RECEIVED",
|
|
5896
|
+
amount: confirmedToken.amount,
|
|
5897
|
+
coinId: confirmedToken.coinId,
|
|
5898
|
+
symbol: confirmedToken.symbol || "UNK",
|
|
5899
|
+
timestamp: Date.now(),
|
|
5900
|
+
senderPubkey: pending2.senderPubkey
|
|
5901
|
+
});
|
|
5902
|
+
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5903
|
+
return "resolved";
|
|
5904
|
+
}
|
|
5905
|
+
return "pending";
|
|
5906
|
+
} catch (error) {
|
|
5907
|
+
console.error(`[Payments] resolveV5Token failed for ${tokenId.slice(0, 8)}:`, error);
|
|
5908
|
+
if (pending2.attemptCount > 50) {
|
|
5909
|
+
token.status = "invalid";
|
|
5910
|
+
token.updatedAt = Date.now();
|
|
5911
|
+
this.tokens.set(tokenId, token);
|
|
5912
|
+
return "failed";
|
|
5913
|
+
}
|
|
5914
|
+
this.updatePendingFinalization(token, pending2);
|
|
5915
|
+
return "pending";
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
/**
|
|
5919
|
+
* Non-blocking proof check with 500ms timeout.
|
|
5920
|
+
*/
|
|
5921
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5922
|
+
async quickProofCheck(stClient, trustBase, commitment, timeoutMs = 500) {
|
|
5923
|
+
try {
|
|
5924
|
+
const proof = await Promise.race([
|
|
5925
|
+
(0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment),
|
|
5926
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
|
5927
|
+
]);
|
|
5928
|
+
return proof;
|
|
5929
|
+
} catch {
|
|
5930
|
+
return null;
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
/**
|
|
5934
|
+
* Perform V5 bundle finalization from stored bundle data and proofs.
|
|
5935
|
+
* Extracted from InstantSplitProcessor.processV5Bundle() steps 4-10.
|
|
5936
|
+
*/
|
|
5937
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5938
|
+
async finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase) {
|
|
5939
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5940
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5941
|
+
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5942
|
+
const mintProofJson = JSON.parse(pending2.mintProofJson);
|
|
5943
|
+
const mintProof = import_InclusionProof.InclusionProof.fromJSON(mintProofJson);
|
|
5944
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
5945
|
+
const tokenType = new import_TokenType3.TokenType(fromHex4(bundle.tokenTypeHex));
|
|
5946
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
5947
|
+
const tokenJson = {
|
|
5948
|
+
version: "2.0",
|
|
5949
|
+
state: senderMintedStateJson,
|
|
5950
|
+
genesis: mintTransaction.toJSON(),
|
|
5951
|
+
transactions: [],
|
|
5952
|
+
nametags: []
|
|
5953
|
+
};
|
|
5954
|
+
const mintedToken = await import_Token6.Token.fromJSON(tokenJson);
|
|
5955
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5956
|
+
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5957
|
+
const transferProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, transferCommitment);
|
|
5958
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
5959
|
+
const transferSalt = fromHex4(bundle.transferSaltHex);
|
|
5960
|
+
const recipientPredicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
|
|
5961
|
+
mintData.tokenId,
|
|
5962
|
+
tokenType,
|
|
5963
|
+
signingService,
|
|
5964
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256,
|
|
5965
|
+
transferSalt
|
|
5966
|
+
);
|
|
5967
|
+
const recipientState = new import_TokenState5.TokenState(recipientPredicate, null);
|
|
5968
|
+
let nametagTokens = [];
|
|
5969
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
5970
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
5971
|
+
if (bundle.nametagTokenJson) {
|
|
5972
|
+
try {
|
|
5973
|
+
const nametagToken = await import_Token6.Token.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
5974
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5975
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5976
|
+
if (proxy.address === recipientAddressStr) {
|
|
5977
|
+
nametagTokens = [nametagToken];
|
|
5978
|
+
}
|
|
5979
|
+
} catch {
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
if (nametagTokens.length === 0 && this.nametag?.token) {
|
|
5983
|
+
try {
|
|
5984
|
+
const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
|
|
5985
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5986
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5987
|
+
if (proxy.address === recipientAddressStr) {
|
|
5988
|
+
nametagTokens = [nametagToken];
|
|
5989
|
+
}
|
|
5990
|
+
} catch {
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
return stClient.finalizeTransaction(trustBase, mintedToken, recipientState, transferTransaction, nametagTokens);
|
|
5995
|
+
}
|
|
5996
|
+
/**
|
|
5997
|
+
* Parse pending finalization metadata from token's sdkData.
|
|
5998
|
+
*/
|
|
5999
|
+
parsePendingFinalization(sdkData) {
|
|
6000
|
+
if (!sdkData) return null;
|
|
6001
|
+
try {
|
|
6002
|
+
const data = JSON.parse(sdkData);
|
|
6003
|
+
if (data._pendingFinalization && data._pendingFinalization.type === "v5_bundle") {
|
|
6004
|
+
return data._pendingFinalization;
|
|
6005
|
+
}
|
|
6006
|
+
return null;
|
|
6007
|
+
} catch {
|
|
6008
|
+
return null;
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
/**
|
|
6012
|
+
* Update pending finalization metadata in token's sdkData.
|
|
6013
|
+
* Creates a new token object since sdkData is readonly.
|
|
6014
|
+
*/
|
|
6015
|
+
updatePendingFinalization(token, pending2) {
|
|
6016
|
+
const updated = {
|
|
6017
|
+
id: token.id,
|
|
6018
|
+
coinId: token.coinId,
|
|
6019
|
+
symbol: token.symbol,
|
|
6020
|
+
name: token.name,
|
|
6021
|
+
decimals: token.decimals,
|
|
6022
|
+
iconUrl: token.iconUrl,
|
|
6023
|
+
amount: token.amount,
|
|
6024
|
+
status: token.status,
|
|
6025
|
+
createdAt: token.createdAt,
|
|
6026
|
+
updatedAt: Date.now(),
|
|
6027
|
+
sdkData: JSON.stringify({ _pendingFinalization: pending2 })
|
|
6028
|
+
};
|
|
6029
|
+
this.tokens.set(token.id, updated);
|
|
6030
|
+
}
|
|
6031
|
+
/**
|
|
6032
|
+
* Save pending V5 tokens to key-value storage.
|
|
6033
|
+
* These tokens can't be serialized to TXF format (no genesis/state),
|
|
6034
|
+
* so we persist them separately and restore on load().
|
|
6035
|
+
*/
|
|
6036
|
+
async savePendingV5Tokens() {
|
|
6037
|
+
const pendingTokens = [];
|
|
6038
|
+
for (const token of this.tokens.values()) {
|
|
6039
|
+
if (this.parsePendingFinalization(token.sdkData)) {
|
|
6040
|
+
pendingTokens.push(token);
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
if (pendingTokens.length > 0) {
|
|
6044
|
+
await this.deps.storage.set(
|
|
6045
|
+
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
6046
|
+
JSON.stringify(pendingTokens)
|
|
6047
|
+
);
|
|
6048
|
+
} else {
|
|
6049
|
+
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
6050
|
+
}
|
|
6051
|
+
}
|
|
6052
|
+
/**
|
|
6053
|
+
* Load pending V5 tokens from key-value storage and merge into tokens map.
|
|
6054
|
+
* Called during load() to restore tokens that TXF format can't represent.
|
|
6055
|
+
*/
|
|
6056
|
+
async loadPendingV5Tokens() {
|
|
6057
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
6058
|
+
if (!data) return;
|
|
6059
|
+
try {
|
|
6060
|
+
const pendingTokens = JSON.parse(data);
|
|
6061
|
+
for (const token of pendingTokens) {
|
|
6062
|
+
if (!this.tokens.has(token.id)) {
|
|
6063
|
+
this.tokens.set(token.id, token);
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
if (pendingTokens.length > 0) {
|
|
6067
|
+
this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
|
|
6068
|
+
}
|
|
6069
|
+
} catch {
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
// ===========================================================================
|
|
5432
6073
|
// Public API - Token Operations
|
|
5433
6074
|
// ===========================================================================
|
|
5434
6075
|
/**
|
|
5435
|
-
* Add a token
|
|
5436
|
-
*
|
|
5437
|
-
*
|
|
5438
|
-
*
|
|
6076
|
+
* Add a token to the wallet.
|
|
6077
|
+
*
|
|
6078
|
+
* Tokens are uniquely identified by a `(tokenId, stateHash)` composite key.
|
|
6079
|
+
* Duplicate detection:
|
|
6080
|
+
* - **Tombstoned** — rejected if the exact `(tokenId, stateHash)` pair has a tombstone.
|
|
6081
|
+
* - **Exact duplicate** — rejected if a token with the same composite key already exists.
|
|
6082
|
+
* - **State replacement** — if the same `tokenId` exists with a *different* `stateHash`,
|
|
6083
|
+
* the old state is archived and replaced with the incoming one.
|
|
6084
|
+
*
|
|
6085
|
+
* @param token - The token to add.
|
|
6086
|
+
* @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
|
|
6087
|
+
* @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
|
|
5439
6088
|
*/
|
|
5440
6089
|
async addToken(token, skipHistory = false) {
|
|
5441
6090
|
this.ensureInitialized();
|
|
@@ -5493,7 +6142,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5493
6142
|
});
|
|
5494
6143
|
}
|
|
5495
6144
|
await this.save();
|
|
5496
|
-
|
|
6145
|
+
if (!this.parsePendingFinalization(token.sdkData)) {
|
|
6146
|
+
await this.saveTokenToFileStorage(token);
|
|
6147
|
+
}
|
|
5497
6148
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
5498
6149
|
return true;
|
|
5499
6150
|
}
|
|
@@ -5550,6 +6201,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5550
6201
|
const data = fileData;
|
|
5551
6202
|
const tokenJson = data.token;
|
|
5552
6203
|
if (!tokenJson) continue;
|
|
6204
|
+
if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
|
|
6205
|
+
continue;
|
|
6206
|
+
}
|
|
5553
6207
|
let sdkTokenId;
|
|
5554
6208
|
if (typeof tokenJson === "object" && tokenJson !== null) {
|
|
5555
6209
|
const tokenObj = tokenJson;
|
|
@@ -5601,7 +6255,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5601
6255
|
this.log(`Loaded ${this.tokens.size} tokens from file storage`);
|
|
5602
6256
|
}
|
|
5603
6257
|
/**
|
|
5604
|
-
* Update an existing token
|
|
6258
|
+
* Update an existing token or add it if not found.
|
|
6259
|
+
*
|
|
6260
|
+
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
6261
|
+
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
6262
|
+
*
|
|
6263
|
+
* @param token - The token with updated data. Must include a valid `id`.
|
|
5605
6264
|
*/
|
|
5606
6265
|
async updateToken(token) {
|
|
5607
6266
|
this.ensureInitialized();
|
|
@@ -5625,7 +6284,15 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5625
6284
|
this.log(`Updated token ${token.id}`);
|
|
5626
6285
|
}
|
|
5627
6286
|
/**
|
|
5628
|
-
* Remove a token
|
|
6287
|
+
* Remove a token from the wallet.
|
|
6288
|
+
*
|
|
6289
|
+
* The token is archived first, then a tombstone `(tokenId, stateHash)` is
|
|
6290
|
+
* created to prevent re-addition via Nostr re-delivery. A `SENT` history
|
|
6291
|
+
* entry is created unless `skipHistory` is `true`.
|
|
6292
|
+
*
|
|
6293
|
+
* @param tokenId - Local UUID of the token to remove.
|
|
6294
|
+
* @param recipientNametag - Optional nametag of the transfer recipient (for history).
|
|
6295
|
+
* @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
|
|
5629
6296
|
*/
|
|
5630
6297
|
async removeToken(tokenId, recipientNametag, skipHistory = false) {
|
|
5631
6298
|
this.ensureInitialized();
|
|
@@ -5687,13 +6354,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5687
6354
|
// Public API - Tombstones
|
|
5688
6355
|
// ===========================================================================
|
|
5689
6356
|
/**
|
|
5690
|
-
* Get all
|
|
6357
|
+
* Get all tombstone entries.
|
|
6358
|
+
*
|
|
6359
|
+
* Each tombstone is keyed by `(tokenId, stateHash)` and prevents a spent
|
|
6360
|
+
* token state from being re-added (e.g. via Nostr re-delivery).
|
|
6361
|
+
*
|
|
6362
|
+
* @returns A shallow copy of the tombstone array.
|
|
5691
6363
|
*/
|
|
5692
6364
|
getTombstones() {
|
|
5693
6365
|
return [...this.tombstones];
|
|
5694
6366
|
}
|
|
5695
6367
|
/**
|
|
5696
|
-
* Check
|
|
6368
|
+
* Check whether a specific `(tokenId, stateHash)` combination is tombstoned.
|
|
6369
|
+
*
|
|
6370
|
+
* @param tokenId - The genesis token ID.
|
|
6371
|
+
* @param stateHash - The state hash of the token version to check.
|
|
6372
|
+
* @returns `true` if the exact combination has been tombstoned.
|
|
5697
6373
|
*/
|
|
5698
6374
|
isStateTombstoned(tokenId, stateHash) {
|
|
5699
6375
|
return this.tombstones.some(
|
|
@@ -5701,8 +6377,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5701
6377
|
);
|
|
5702
6378
|
}
|
|
5703
6379
|
/**
|
|
5704
|
-
* Merge remote
|
|
5705
|
-
*
|
|
6380
|
+
* Merge tombstones received from a remote sync source.
|
|
6381
|
+
*
|
|
6382
|
+
* Any local token whose `(tokenId, stateHash)` matches a remote tombstone is
|
|
6383
|
+
* removed. The remote tombstones are then added to the local set (union merge).
|
|
6384
|
+
*
|
|
6385
|
+
* @param remoteTombstones - Tombstone entries from the remote source.
|
|
6386
|
+
* @returns Number of local tokens that were removed.
|
|
5706
6387
|
*/
|
|
5707
6388
|
async mergeTombstones(remoteTombstones) {
|
|
5708
6389
|
this.ensureInitialized();
|
|
@@ -5738,7 +6419,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5738
6419
|
return removedCount;
|
|
5739
6420
|
}
|
|
5740
6421
|
/**
|
|
5741
|
-
*
|
|
6422
|
+
* Remove tombstones older than `maxAge` and cap the list at 100 entries.
|
|
6423
|
+
*
|
|
6424
|
+
* @param maxAge - Maximum age in milliseconds (default: 30 days).
|
|
5742
6425
|
*/
|
|
5743
6426
|
async pruneTombstones(maxAge) {
|
|
5744
6427
|
const originalCount = this.tombstones.length;
|
|
@@ -5752,20 +6435,38 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5752
6435
|
// Public API - Archives
|
|
5753
6436
|
// ===========================================================================
|
|
5754
6437
|
/**
|
|
5755
|
-
* Get archived tokens
|
|
6438
|
+
* Get all archived (spent/superseded) tokens in TXF format.
|
|
6439
|
+
*
|
|
6440
|
+
* Archived tokens are kept for recovery and sync purposes. The map key is
|
|
6441
|
+
* the genesis token ID.
|
|
6442
|
+
*
|
|
6443
|
+
* @returns A shallow copy of the archived token map.
|
|
5756
6444
|
*/
|
|
5757
6445
|
getArchivedTokens() {
|
|
5758
6446
|
return new Map(this.archivedTokens);
|
|
5759
6447
|
}
|
|
5760
6448
|
/**
|
|
5761
|
-
* Get best archived version of a token
|
|
6449
|
+
* Get the best (most committed transactions) archived version of a token.
|
|
6450
|
+
*
|
|
6451
|
+
* Searches both archived and forked token maps and returns the version with
|
|
6452
|
+
* the highest number of committed transactions.
|
|
6453
|
+
*
|
|
6454
|
+
* @param tokenId - The genesis token ID to look up.
|
|
6455
|
+
* @returns The best TXF token version, or `null` if not found.
|
|
5762
6456
|
*/
|
|
5763
6457
|
getBestArchivedVersion(tokenId) {
|
|
5764
6458
|
return findBestTokenVersion(tokenId, this.archivedTokens, this.forkedTokens);
|
|
5765
6459
|
}
|
|
5766
6460
|
/**
|
|
5767
|
-
* Merge remote
|
|
5768
|
-
*
|
|
6461
|
+
* Merge archived tokens from a remote sync source.
|
|
6462
|
+
*
|
|
6463
|
+
* For each remote token:
|
|
6464
|
+
* - If missing locally, it is added.
|
|
6465
|
+
* - If the remote version is an incremental update of the local, it replaces it.
|
|
6466
|
+
* - If the histories diverge (fork), the remote version is stored via {@link storeForkedToken}.
|
|
6467
|
+
*
|
|
6468
|
+
* @param remoteArchived - Map of genesis token ID → TXF token from remote.
|
|
6469
|
+
* @returns Number of tokens that were updated or added locally.
|
|
5769
6470
|
*/
|
|
5770
6471
|
async mergeArchivedTokens(remoteArchived) {
|
|
5771
6472
|
let mergedCount = 0;
|
|
@@ -5788,7 +6489,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5788
6489
|
return mergedCount;
|
|
5789
6490
|
}
|
|
5790
6491
|
/**
|
|
5791
|
-
* Prune archived tokens
|
|
6492
|
+
* Prune archived tokens to keep at most `maxCount` entries.
|
|
6493
|
+
*
|
|
6494
|
+
* Oldest entries (by insertion order) are removed first.
|
|
6495
|
+
*
|
|
6496
|
+
* @param maxCount - Maximum number of archived tokens to retain (default: 100).
|
|
5792
6497
|
*/
|
|
5793
6498
|
async pruneArchivedTokens(maxCount = 100) {
|
|
5794
6499
|
if (this.archivedTokens.size <= maxCount) return;
|
|
@@ -5801,13 +6506,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5801
6506
|
// Public API - Forked Tokens
|
|
5802
6507
|
// ===========================================================================
|
|
5803
6508
|
/**
|
|
5804
|
-
* Get forked
|
|
6509
|
+
* Get all forked token versions.
|
|
6510
|
+
*
|
|
6511
|
+
* Forked tokens represent alternative histories detected during sync.
|
|
6512
|
+
* The map key is `{tokenId}_{stateHash}`.
|
|
6513
|
+
*
|
|
6514
|
+
* @returns A shallow copy of the forked tokens map.
|
|
5805
6515
|
*/
|
|
5806
6516
|
getForkedTokens() {
|
|
5807
6517
|
return new Map(this.forkedTokens);
|
|
5808
6518
|
}
|
|
5809
6519
|
/**
|
|
5810
|
-
* Store a forked token
|
|
6520
|
+
* Store a forked token version (alternative history).
|
|
6521
|
+
*
|
|
6522
|
+
* No-op if the exact `(tokenId, stateHash)` key already exists.
|
|
6523
|
+
*
|
|
6524
|
+
* @param tokenId - Genesis token ID.
|
|
6525
|
+
* @param stateHash - State hash of this forked version.
|
|
6526
|
+
* @param txfToken - The TXF token data to store.
|
|
5811
6527
|
*/
|
|
5812
6528
|
async storeForkedToken(tokenId, stateHash, txfToken) {
|
|
5813
6529
|
const key = `${tokenId}_${stateHash}`;
|
|
@@ -5817,8 +6533,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5817
6533
|
await this.save();
|
|
5818
6534
|
}
|
|
5819
6535
|
/**
|
|
5820
|
-
* Merge remote
|
|
5821
|
-
*
|
|
6536
|
+
* Merge forked tokens from a remote sync source. Only new keys are added.
|
|
6537
|
+
*
|
|
6538
|
+
* @param remoteForked - Map of `{tokenId}_{stateHash}` → TXF token from remote.
|
|
6539
|
+
* @returns Number of new forked tokens added.
|
|
5822
6540
|
*/
|
|
5823
6541
|
async mergeForkedTokens(remoteForked) {
|
|
5824
6542
|
let addedCount = 0;
|
|
@@ -5834,7 +6552,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5834
6552
|
return addedCount;
|
|
5835
6553
|
}
|
|
5836
6554
|
/**
|
|
5837
|
-
* Prune forked tokens
|
|
6555
|
+
* Prune forked tokens to keep at most `maxCount` entries.
|
|
6556
|
+
*
|
|
6557
|
+
* @param maxCount - Maximum number of forked tokens to retain (default: 50).
|
|
5838
6558
|
*/
|
|
5839
6559
|
async pruneForkedTokens(maxCount = 50) {
|
|
5840
6560
|
if (this.forkedTokens.size <= maxCount) return;
|
|
@@ -5847,13 +6567,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5847
6567
|
// Public API - Transaction History
|
|
5848
6568
|
// ===========================================================================
|
|
5849
6569
|
/**
|
|
5850
|
-
* Get transaction history
|
|
6570
|
+
* Get the transaction history sorted newest-first.
|
|
6571
|
+
*
|
|
6572
|
+
* @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
|
|
5851
6573
|
*/
|
|
5852
6574
|
getHistory() {
|
|
5853
6575
|
return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
|
|
5854
6576
|
}
|
|
5855
6577
|
/**
|
|
5856
|
-
*
|
|
6578
|
+
* Append an entry to the transaction history.
|
|
6579
|
+
*
|
|
6580
|
+
* A unique `id` is auto-generated. The entry is immediately persisted to storage.
|
|
6581
|
+
*
|
|
6582
|
+
* @param entry - History entry fields (without `id`).
|
|
5857
6583
|
*/
|
|
5858
6584
|
async addToHistory(entry) {
|
|
5859
6585
|
this.ensureInitialized();
|
|
@@ -5871,7 +6597,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5871
6597
|
// Public API - Nametag
|
|
5872
6598
|
// ===========================================================================
|
|
5873
6599
|
/**
|
|
5874
|
-
* Set nametag for current identity
|
|
6600
|
+
* Set the nametag data for the current identity.
|
|
6601
|
+
*
|
|
6602
|
+
* Persists to both key-value storage and file storage (lottery compatibility).
|
|
6603
|
+
*
|
|
6604
|
+
* @param nametag - The nametag data including minted token JSON.
|
|
5875
6605
|
*/
|
|
5876
6606
|
async setNametag(nametag) {
|
|
5877
6607
|
this.ensureInitialized();
|
|
@@ -5881,19 +6611,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5881
6611
|
this.log(`Nametag set: ${nametag.name}`);
|
|
5882
6612
|
}
|
|
5883
6613
|
/**
|
|
5884
|
-
* Get nametag
|
|
6614
|
+
* Get the current nametag data.
|
|
6615
|
+
*
|
|
6616
|
+
* @returns The nametag data, or `null` if no nametag is set.
|
|
5885
6617
|
*/
|
|
5886
6618
|
getNametag() {
|
|
5887
6619
|
return this.nametag;
|
|
5888
6620
|
}
|
|
5889
6621
|
/**
|
|
5890
|
-
* Check
|
|
6622
|
+
* Check whether a nametag is currently set.
|
|
6623
|
+
*
|
|
6624
|
+
* @returns `true` if nametag data is present.
|
|
5891
6625
|
*/
|
|
5892
6626
|
hasNametag() {
|
|
5893
6627
|
return this.nametag !== null;
|
|
5894
6628
|
}
|
|
5895
6629
|
/**
|
|
5896
|
-
*
|
|
6630
|
+
* Remove the current nametag data from memory and storage.
|
|
5897
6631
|
*/
|
|
5898
6632
|
async clearNametag() {
|
|
5899
6633
|
this.ensureInitialized();
|
|
@@ -5987,9 +6721,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5987
6721
|
try {
|
|
5988
6722
|
const signingService = await this.createSigningService();
|
|
5989
6723
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5990
|
-
const { TokenType:
|
|
6724
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5991
6725
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5992
|
-
const tokenType = new
|
|
6726
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5993
6727
|
const addressRef = await UnmaskedPredicateReference4.create(
|
|
5994
6728
|
tokenType,
|
|
5995
6729
|
signingService.algorithm,
|
|
@@ -6050,11 +6784,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6050
6784
|
// Public API - Sync & Validate
|
|
6051
6785
|
// ===========================================================================
|
|
6052
6786
|
/**
|
|
6053
|
-
* Sync with all token storage providers (IPFS,
|
|
6054
|
-
*
|
|
6787
|
+
* Sync local token state with all configured token storage providers (IPFS, file, etc.).
|
|
6788
|
+
*
|
|
6789
|
+
* For each provider, the local data is packaged into TXF storage format, sent
|
|
6790
|
+
* to the provider's `sync()` method, and the merged result is applied locally.
|
|
6791
|
+
* Emits `sync:started`, `sync:completed`, and `sync:error` events.
|
|
6792
|
+
*
|
|
6793
|
+
* @returns Summary with counts of tokens added and removed during sync.
|
|
6055
6794
|
*/
|
|
6056
6795
|
async sync() {
|
|
6057
6796
|
this.ensureInitialized();
|
|
6797
|
+
if (this._syncInProgress) {
|
|
6798
|
+
return this._syncInProgress;
|
|
6799
|
+
}
|
|
6800
|
+
this._syncInProgress = this._doSync();
|
|
6801
|
+
try {
|
|
6802
|
+
return await this._syncInProgress;
|
|
6803
|
+
} finally {
|
|
6804
|
+
this._syncInProgress = null;
|
|
6805
|
+
}
|
|
6806
|
+
}
|
|
6807
|
+
async _doSync() {
|
|
6058
6808
|
this.deps.emitEvent("sync:started", { source: "payments" });
|
|
6059
6809
|
try {
|
|
6060
6810
|
const providers = this.getTokenStorageProviders();
|
|
@@ -6092,6 +6842,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6092
6842
|
});
|
|
6093
6843
|
}
|
|
6094
6844
|
}
|
|
6845
|
+
if (totalAdded > 0 || totalRemoved > 0) {
|
|
6846
|
+
await this.save();
|
|
6847
|
+
}
|
|
6095
6848
|
this.deps.emitEvent("sync:completed", {
|
|
6096
6849
|
source: "payments",
|
|
6097
6850
|
count: this.tokens.size
|
|
@@ -6105,6 +6858,66 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6105
6858
|
throw error;
|
|
6106
6859
|
}
|
|
6107
6860
|
}
|
|
6861
|
+
// ===========================================================================
|
|
6862
|
+
// Storage Event Subscription (Push-Based Sync)
|
|
6863
|
+
// ===========================================================================
|
|
6864
|
+
/**
|
|
6865
|
+
* Subscribe to 'storage:remote-updated' events from all token storage providers.
|
|
6866
|
+
* When a provider emits this event, a debounced sync is triggered.
|
|
6867
|
+
*/
|
|
6868
|
+
subscribeToStorageEvents() {
|
|
6869
|
+
this.unsubscribeStorageEvents();
|
|
6870
|
+
const providers = this.getTokenStorageProviders();
|
|
6871
|
+
for (const [providerId, provider] of providers) {
|
|
6872
|
+
if (provider.onEvent) {
|
|
6873
|
+
const unsub = provider.onEvent((event) => {
|
|
6874
|
+
if (event.type === "storage:remote-updated") {
|
|
6875
|
+
this.log("Remote update detected from provider", providerId, event.data);
|
|
6876
|
+
this.debouncedSyncFromRemoteUpdate(providerId, event.data);
|
|
6877
|
+
}
|
|
6878
|
+
});
|
|
6879
|
+
this.storageEventUnsubscribers.push(unsub);
|
|
6880
|
+
}
|
|
6881
|
+
}
|
|
6882
|
+
}
|
|
6883
|
+
/**
|
|
6884
|
+
* Unsubscribe from all storage provider events and clear debounce timer.
|
|
6885
|
+
*/
|
|
6886
|
+
unsubscribeStorageEvents() {
|
|
6887
|
+
for (const unsub of this.storageEventUnsubscribers) {
|
|
6888
|
+
unsub();
|
|
6889
|
+
}
|
|
6890
|
+
this.storageEventUnsubscribers = [];
|
|
6891
|
+
if (this.syncDebounceTimer) {
|
|
6892
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6893
|
+
this.syncDebounceTimer = null;
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
6896
|
+
/**
|
|
6897
|
+
* Debounced sync triggered by a storage:remote-updated event.
|
|
6898
|
+
* Waits 500ms to batch rapid updates, then performs sync.
|
|
6899
|
+
*/
|
|
6900
|
+
debouncedSyncFromRemoteUpdate(providerId, eventData) {
|
|
6901
|
+
if (this.syncDebounceTimer) {
|
|
6902
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6903
|
+
}
|
|
6904
|
+
this.syncDebounceTimer = setTimeout(() => {
|
|
6905
|
+
this.syncDebounceTimer = null;
|
|
6906
|
+
this.sync().then((result) => {
|
|
6907
|
+
const data = eventData;
|
|
6908
|
+
this.deps?.emitEvent("sync:remote-update", {
|
|
6909
|
+
providerId,
|
|
6910
|
+
name: data?.name ?? "",
|
|
6911
|
+
sequence: data?.sequence ?? 0,
|
|
6912
|
+
cid: data?.cid ?? "",
|
|
6913
|
+
added: result.added,
|
|
6914
|
+
removed: result.removed
|
|
6915
|
+
});
|
|
6916
|
+
}).catch((err) => {
|
|
6917
|
+
this.log("Auto-sync from remote update failed:", err);
|
|
6918
|
+
});
|
|
6919
|
+
}, _PaymentsModule.SYNC_DEBOUNCE_MS);
|
|
6920
|
+
}
|
|
6108
6921
|
/**
|
|
6109
6922
|
* Get all active token storage providers
|
|
6110
6923
|
*/
|
|
@@ -6120,15 +6933,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6120
6933
|
return /* @__PURE__ */ new Map();
|
|
6121
6934
|
}
|
|
6122
6935
|
/**
|
|
6123
|
-
*
|
|
6936
|
+
* Replace the set of token storage providers at runtime.
|
|
6937
|
+
*
|
|
6938
|
+
* Use when providers are added or removed dynamically (e.g. IPFS node started).
|
|
6939
|
+
*
|
|
6940
|
+
* @param providers - New map of provider ID → TokenStorageProvider.
|
|
6124
6941
|
*/
|
|
6125
6942
|
updateTokenStorageProviders(providers) {
|
|
6126
6943
|
if (this.deps) {
|
|
6127
6944
|
this.deps.tokenStorageProviders = providers;
|
|
6945
|
+
this.subscribeToStorageEvents();
|
|
6128
6946
|
}
|
|
6129
6947
|
}
|
|
6130
6948
|
/**
|
|
6131
|
-
* Validate tokens
|
|
6949
|
+
* Validate all tokens against the aggregator (oracle provider).
|
|
6950
|
+
*
|
|
6951
|
+
* Tokens that fail validation or are detected as spent are marked `'invalid'`.
|
|
6952
|
+
*
|
|
6953
|
+
* @returns Object with arrays of valid and invalid tokens.
|
|
6132
6954
|
*/
|
|
6133
6955
|
async validate() {
|
|
6134
6956
|
this.ensureInitialized();
|
|
@@ -6149,7 +6971,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6149
6971
|
return { valid, invalid };
|
|
6150
6972
|
}
|
|
6151
6973
|
/**
|
|
6152
|
-
* Get pending transfers
|
|
6974
|
+
* Get all in-progress (pending) outgoing transfers.
|
|
6975
|
+
*
|
|
6976
|
+
* @returns Array of {@link TransferResult} objects for transfers that have not yet completed.
|
|
6153
6977
|
*/
|
|
6154
6978
|
getPendingTransfers() {
|
|
6155
6979
|
return Array.from(this.pendingTransfers.values());
|
|
@@ -6213,9 +7037,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6213
7037
|
*/
|
|
6214
7038
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
6215
7039
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
6216
|
-
const { TokenType:
|
|
7040
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
6217
7041
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
6218
|
-
const tokenType = new
|
|
7042
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
6219
7043
|
const pubkeyBytes = new Uint8Array(
|
|
6220
7044
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
6221
7045
|
);
|
|
@@ -6427,7 +7251,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6427
7251
|
this.deps.emitEvent("transfer:confirmed", {
|
|
6428
7252
|
id: crypto.randomUUID(),
|
|
6429
7253
|
status: "completed",
|
|
6430
|
-
tokens: [finalizedToken]
|
|
7254
|
+
tokens: [finalizedToken],
|
|
7255
|
+
tokenTransfers: []
|
|
6431
7256
|
});
|
|
6432
7257
|
await this.addToHistory({
|
|
6433
7258
|
type: "RECEIVED",
|
|
@@ -6450,14 +7275,26 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6450
7275
|
async handleIncomingTransfer(transfer) {
|
|
6451
7276
|
try {
|
|
6452
7277
|
const payload = transfer.payload;
|
|
7278
|
+
let instantBundle = null;
|
|
6453
7279
|
if (isInstantSplitBundle(payload)) {
|
|
7280
|
+
instantBundle = payload;
|
|
7281
|
+
} else if (payload.token) {
|
|
7282
|
+
try {
|
|
7283
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
7284
|
+
if (isInstantSplitBundle(inner)) {
|
|
7285
|
+
instantBundle = inner;
|
|
7286
|
+
}
|
|
7287
|
+
} catch {
|
|
7288
|
+
}
|
|
7289
|
+
}
|
|
7290
|
+
if (instantBundle) {
|
|
6454
7291
|
this.log("Processing INSTANT_SPLIT bundle...");
|
|
6455
7292
|
try {
|
|
6456
7293
|
if (!this.nametag) {
|
|
6457
7294
|
await this.loadNametagFromFileStorage();
|
|
6458
7295
|
}
|
|
6459
7296
|
const result = await this.processInstantSplitBundle(
|
|
6460
|
-
|
|
7297
|
+
instantBundle,
|
|
6461
7298
|
transfer.senderTransportPubkey
|
|
6462
7299
|
);
|
|
6463
7300
|
if (result.success) {
|
|
@@ -6470,6 +7307,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6470
7307
|
}
|
|
6471
7308
|
return;
|
|
6472
7309
|
}
|
|
7310
|
+
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
7311
|
+
this.log("Processing NOSTR-FIRST commitment-only transfer...");
|
|
7312
|
+
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
7313
|
+
return;
|
|
7314
|
+
}
|
|
6473
7315
|
let tokenData;
|
|
6474
7316
|
let finalizedSdkToken = null;
|
|
6475
7317
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -6625,6 +7467,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6625
7467
|
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
6626
7468
|
}
|
|
6627
7469
|
}
|
|
7470
|
+
await this.savePendingV5Tokens();
|
|
6628
7471
|
}
|
|
6629
7472
|
async saveToOutbox(transfer, recipient) {
|
|
6630
7473
|
const outbox = await this.loadOutbox();
|
|
@@ -6642,8 +7485,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6642
7485
|
}
|
|
6643
7486
|
async createStorageData() {
|
|
6644
7487
|
return await buildTxfStorageData(
|
|
6645
|
-
|
|
6646
|
-
// Empty - active tokens stored as token-xxx files
|
|
7488
|
+
Array.from(this.tokens.values()),
|
|
6647
7489
|
{
|
|
6648
7490
|
version: 1,
|
|
6649
7491
|
address: this.deps.identity.l1Address,
|
|
@@ -6828,7 +7670,7 @@ function createPaymentsModule(config) {
|
|
|
6828
7670
|
// modules/payments/TokenRecoveryService.ts
|
|
6829
7671
|
var import_TokenId4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
6830
7672
|
var import_TokenState6 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
6831
|
-
var
|
|
7673
|
+
var import_TokenType4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
6832
7674
|
var import_CoinId5 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
|
|
6833
7675
|
var import_HashAlgorithm6 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
6834
7676
|
var import_UnmaskedPredicate6 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
@@ -7892,15 +8734,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
7892
8734
|
|
|
7893
8735
|
// core/Sphere.ts
|
|
7894
8736
|
var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
|
|
7895
|
-
var
|
|
8737
|
+
var import_TokenType5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
7896
8738
|
var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
7897
8739
|
var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
8740
|
+
var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
|
|
8741
|
+
function isValidNametag(nametag) {
|
|
8742
|
+
if ((0, import_nostr_js_sdk2.isPhoneNumber)(nametag)) return true;
|
|
8743
|
+
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
8744
|
+
}
|
|
7898
8745
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
7899
8746
|
async function deriveL3PredicateAddress(privateKey) {
|
|
7900
8747
|
const secret = Buffer.from(privateKey, "hex");
|
|
7901
8748
|
const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
|
|
7902
8749
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
7903
|
-
const tokenType = new
|
|
8750
|
+
const tokenType = new import_TokenType5.TokenType(tokenTypeBytes);
|
|
7904
8751
|
const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
|
|
7905
8752
|
tokenType,
|
|
7906
8753
|
signingService.algorithm,
|
|
@@ -8176,6 +9023,14 @@ var Sphere = class _Sphere {
|
|
|
8176
9023
|
console.log("[Sphere.import] Registering nametag...");
|
|
8177
9024
|
await sphere.registerNametag(options.nametag);
|
|
8178
9025
|
}
|
|
9026
|
+
if (sphere._tokenStorageProviders.size > 0) {
|
|
9027
|
+
try {
|
|
9028
|
+
const syncResult = await sphere._payments.sync();
|
|
9029
|
+
console.log(`[Sphere.import] Auto-sync: +${syncResult.added} -${syncResult.removed}`);
|
|
9030
|
+
} catch (err) {
|
|
9031
|
+
console.warn("[Sphere.import] Auto-sync failed (non-fatal):", err);
|
|
9032
|
+
}
|
|
9033
|
+
}
|
|
8179
9034
|
console.log("[Sphere.import] Import complete");
|
|
8180
9035
|
return sphere;
|
|
8181
9036
|
}
|
|
@@ -9046,9 +9901,9 @@ var Sphere = class _Sphere {
|
|
|
9046
9901
|
if (index < 0) {
|
|
9047
9902
|
throw new Error("Address index must be non-negative");
|
|
9048
9903
|
}
|
|
9049
|
-
const newNametag = options?.nametag
|
|
9050
|
-
if (newNametag && !
|
|
9051
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9904
|
+
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
9905
|
+
if (newNametag && !isValidNametag(newNametag)) {
|
|
9906
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
9052
9907
|
}
|
|
9053
9908
|
const addressInfo = this.deriveAddress(index, false);
|
|
9054
9909
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
@@ -9432,9 +10287,9 @@ var Sphere = class _Sphere {
|
|
|
9432
10287
|
*/
|
|
9433
10288
|
async registerNametag(nametag) {
|
|
9434
10289
|
this.ensureReady();
|
|
9435
|
-
const cleanNametag =
|
|
9436
|
-
if (!
|
|
9437
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
10290
|
+
const cleanNametag = this.cleanNametag(nametag);
|
|
10291
|
+
if (!isValidNametag(cleanNametag)) {
|
|
10292
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
9438
10293
|
}
|
|
9439
10294
|
if (this._identity?.nametag) {
|
|
9440
10295
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
@@ -9743,13 +10598,11 @@ var Sphere = class _Sphere {
|
|
|
9743
10598
|
}
|
|
9744
10599
|
}
|
|
9745
10600
|
/**
|
|
9746
|
-
*
|
|
10601
|
+
* Strip @ prefix and normalize a nametag (lowercase, phone E.164, strip @unicity suffix).
|
|
9747
10602
|
*/
|
|
9748
|
-
|
|
9749
|
-
const
|
|
9750
|
-
|
|
9751
|
-
);
|
|
9752
|
-
return pattern.test(nametag);
|
|
10603
|
+
cleanNametag(raw) {
|
|
10604
|
+
const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
10605
|
+
return (0, import_nostr_js_sdk2.normalizeNametag)(stripped);
|
|
9753
10606
|
}
|
|
9754
10607
|
// ===========================================================================
|
|
9755
10608
|
// Public Methods - Lifecycle
|
|
@@ -10449,6 +11302,9 @@ function createTokenValidator(options) {
|
|
|
10449
11302
|
return new TokenValidator(options);
|
|
10450
11303
|
}
|
|
10451
11304
|
|
|
11305
|
+
// index.ts
|
|
11306
|
+
var import_nostr_js_sdk3 = require("@unicitylabs/nostr-js-sdk");
|
|
11307
|
+
|
|
10452
11308
|
// price/CoinGeckoPriceProvider.ts
|
|
10453
11309
|
var CoinGeckoPriceProvider = class {
|
|
10454
11310
|
platform = "coingecko";
|
|
@@ -10585,6 +11441,7 @@ function createPriceProvider(config) {
|
|
|
10585
11441
|
TokenRegistry,
|
|
10586
11442
|
TokenValidator,
|
|
10587
11443
|
archivedKeyFromTokenId,
|
|
11444
|
+
areSameNametag,
|
|
10588
11445
|
base58Decode,
|
|
10589
11446
|
base58Encode,
|
|
10590
11447
|
buildTxfStorageData,
|
|
@@ -10632,6 +11489,7 @@ function createPriceProvider(config) {
|
|
|
10632
11489
|
hasUncommittedTransactions,
|
|
10633
11490
|
hasValidTxfData,
|
|
10634
11491
|
hash160,
|
|
11492
|
+
hashNametag,
|
|
10635
11493
|
hexToBytes,
|
|
10636
11494
|
identityFromMnemonicSync,
|
|
10637
11495
|
initSphere,
|
|
@@ -10643,10 +11501,12 @@ function createPriceProvider(config) {
|
|
|
10643
11501
|
isKnownToken,
|
|
10644
11502
|
isPaymentSessionTerminal,
|
|
10645
11503
|
isPaymentSessionTimedOut,
|
|
11504
|
+
isPhoneNumber,
|
|
10646
11505
|
isSQLiteDatabase,
|
|
10647
11506
|
isTextWalletEncrypted,
|
|
10648
11507
|
isTokenKey,
|
|
10649
11508
|
isValidBech32,
|
|
11509
|
+
isValidNametag,
|
|
10650
11510
|
isValidPrivateKey,
|
|
10651
11511
|
isValidTokenId,
|
|
10652
11512
|
isWalletDatEncrypted,
|
|
@@ -10654,6 +11514,7 @@ function createPriceProvider(config) {
|
|
|
10654
11514
|
keyFromTokenId,
|
|
10655
11515
|
loadSphere,
|
|
10656
11516
|
mnemonicToSeedSync,
|
|
11517
|
+
normalizeNametag,
|
|
10657
11518
|
normalizeSdkTokenToStorage,
|
|
10658
11519
|
objectToTxf,
|
|
10659
11520
|
parseAndDecryptWalletDat,
|