@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.js
CHANGED
|
@@ -2272,6 +2272,7 @@ import { MintCommitment } from "@unicitylabs/state-transition-sdk/lib/transactio
|
|
|
2272
2272
|
import { HashAlgorithm as HashAlgorithm2 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
2273
2273
|
import { UnmaskedPredicate as UnmaskedPredicate2 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
2274
2274
|
import { waitInclusionProof as waitInclusionProof2 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
2275
|
+
import { normalizeNametag } from "@unicitylabs/nostr-js-sdk";
|
|
2275
2276
|
var UNICITY_TOKEN_TYPE_HEX = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
2276
2277
|
var NametagMinter = class {
|
|
2277
2278
|
client;
|
|
@@ -2296,7 +2297,8 @@ var NametagMinter = class {
|
|
|
2296
2297
|
*/
|
|
2297
2298
|
async isNametagAvailable(nametag) {
|
|
2298
2299
|
try {
|
|
2299
|
-
const
|
|
2300
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2301
|
+
const cleanNametag = normalizeNametag(stripped);
|
|
2300
2302
|
const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
|
|
2301
2303
|
const isMinted = await this.client.isMinted(this.trustBase, nametagTokenId);
|
|
2302
2304
|
return !isMinted;
|
|
@@ -2313,7 +2315,8 @@ var NametagMinter = class {
|
|
|
2313
2315
|
* @returns MintNametagResult with token if successful
|
|
2314
2316
|
*/
|
|
2315
2317
|
async mintNametag(nametag, ownerAddress) {
|
|
2316
|
-
const
|
|
2318
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2319
|
+
const cleanNametag = normalizeNametag(stripped);
|
|
2317
2320
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2318
2321
|
try {
|
|
2319
2322
|
const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
|
|
@@ -2467,7 +2470,9 @@ var STORAGE_KEYS_ADDRESS = {
|
|
|
2467
2470
|
/** Messages for this address */
|
|
2468
2471
|
MESSAGES: "messages",
|
|
2469
2472
|
/** Transaction history for this address */
|
|
2470
|
-
TRANSACTION_HISTORY: "transaction_history"
|
|
2473
|
+
TRANSACTION_HISTORY: "transaction_history",
|
|
2474
|
+
/** Pending V5 finalization tokens (unconfirmed instant split tokens) */
|
|
2475
|
+
PENDING_V5_TOKENS: "pending_v5_tokens"
|
|
2471
2476
|
};
|
|
2472
2477
|
var STORAGE_KEYS = {
|
|
2473
2478
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -2882,6 +2887,18 @@ function parseTxfStorageData(data) {
|
|
|
2882
2887
|
result.validationErrors.push(`Forked token ${parsed.tokenId}: invalid structure`);
|
|
2883
2888
|
}
|
|
2884
2889
|
}
|
|
2890
|
+
} else if (key.startsWith("token-")) {
|
|
2891
|
+
try {
|
|
2892
|
+
const entry = storageData[key];
|
|
2893
|
+
const txfToken = entry?.token;
|
|
2894
|
+
if (txfToken?.genesis?.data?.tokenId) {
|
|
2895
|
+
const tokenId = txfToken.genesis.data.tokenId;
|
|
2896
|
+
const token = txfToToken(tokenId, txfToken);
|
|
2897
|
+
result.tokens.push(token);
|
|
2898
|
+
}
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
result.validationErrors.push(`Token ${key}: ${err}`);
|
|
2901
|
+
}
|
|
2885
2902
|
}
|
|
2886
2903
|
}
|
|
2887
2904
|
return result;
|
|
@@ -3457,8 +3474,9 @@ var InstantSplitExecutor = class {
|
|
|
3457
3474
|
const criticalPathDuration = performance.now() - startTime;
|
|
3458
3475
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3459
3476
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3477
|
+
let backgroundPromise;
|
|
3460
3478
|
if (!options?.skipBackground) {
|
|
3461
|
-
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3479
|
+
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3462
3480
|
signingService: this.signingService,
|
|
3463
3481
|
tokenType: tokenToSplit.type,
|
|
3464
3482
|
coinId,
|
|
@@ -3474,7 +3492,8 @@ var InstantSplitExecutor = class {
|
|
|
3474
3492
|
nostrEventId,
|
|
3475
3493
|
splitGroupId,
|
|
3476
3494
|
criticalPathDurationMs: criticalPathDuration,
|
|
3477
|
-
backgroundStarted: !options?.skipBackground
|
|
3495
|
+
backgroundStarted: !options?.skipBackground,
|
|
3496
|
+
backgroundPromise
|
|
3478
3497
|
};
|
|
3479
3498
|
} catch (error) {
|
|
3480
3499
|
const duration = performance.now() - startTime;
|
|
@@ -3536,7 +3555,7 @@ var InstantSplitExecutor = class {
|
|
|
3536
3555
|
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3537
3556
|
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3538
3557
|
]);
|
|
3539
|
-
submissions.then(async (results) => {
|
|
3558
|
+
return submissions.then(async (results) => {
|
|
3540
3559
|
const submitDuration = performance.now() - startTime;
|
|
3541
3560
|
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3542
3561
|
context.onProgress?.({
|
|
@@ -4001,6 +4020,11 @@ import { AddressScheme } from "@unicitylabs/state-transition-sdk/lib/address/Add
|
|
|
4001
4020
|
import { UnmaskedPredicate as UnmaskedPredicate5 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
4002
4021
|
import { TokenState as TokenState5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
4003
4022
|
import { HashAlgorithm as HashAlgorithm5 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
4023
|
+
import { TokenType as TokenType3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
4024
|
+
import { MintCommitment as MintCommitment3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment";
|
|
4025
|
+
import { MintTransactionData as MintTransactionData3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData";
|
|
4026
|
+
import { waitInclusionProof as waitInclusionProof5 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
4027
|
+
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof";
|
|
4004
4028
|
function enrichWithRegistry(info) {
|
|
4005
4029
|
const registry = TokenRegistry.getInstance();
|
|
4006
4030
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -4198,6 +4222,13 @@ function extractTokenStateKey(token) {
|
|
|
4198
4222
|
if (!tokenId || !stateHash) return null;
|
|
4199
4223
|
return createTokenStateKey(tokenId, stateHash);
|
|
4200
4224
|
}
|
|
4225
|
+
function fromHex4(hex) {
|
|
4226
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
4227
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
4228
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
4229
|
+
}
|
|
4230
|
+
return bytes;
|
|
4231
|
+
}
|
|
4201
4232
|
function hasSameGenesisTokenId(t1, t2) {
|
|
4202
4233
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
4203
4234
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
@@ -4287,6 +4318,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4287
4318
|
// Token State
|
|
4288
4319
|
tokens = /* @__PURE__ */ new Map();
|
|
4289
4320
|
pendingTransfers = /* @__PURE__ */ new Map();
|
|
4321
|
+
pendingBackgroundTasks = [];
|
|
4290
4322
|
// Repository State (tombstones, archives, forked, history)
|
|
4291
4323
|
tombstones = [];
|
|
4292
4324
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
@@ -4311,6 +4343,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4311
4343
|
// Poll every 2s
|
|
4312
4344
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4313
4345
|
// Max 30 attempts (~60s)
|
|
4346
|
+
// Storage event subscriptions (push-based sync)
|
|
4347
|
+
storageEventUnsubscribers = [];
|
|
4348
|
+
syncDebounceTimer = null;
|
|
4349
|
+
static SYNC_DEBOUNCE_MS = 500;
|
|
4350
|
+
/** Sync coalescing: concurrent sync() calls share the same operation */
|
|
4351
|
+
_syncInProgress = null;
|
|
4314
4352
|
constructor(config) {
|
|
4315
4353
|
this.moduleConfig = {
|
|
4316
4354
|
autoSync: config?.autoSync ?? true,
|
|
@@ -4321,7 +4359,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4321
4359
|
};
|
|
4322
4360
|
this.l1 = config?.l1 === null ? null : new L1PaymentsModule(config?.l1);
|
|
4323
4361
|
}
|
|
4324
|
-
/**
|
|
4362
|
+
/**
|
|
4363
|
+
* Get the current module configuration (excluding L1 config).
|
|
4364
|
+
*
|
|
4365
|
+
* @returns Resolved configuration with all defaults applied.
|
|
4366
|
+
*/
|
|
4325
4367
|
getConfig() {
|
|
4326
4368
|
return this.moduleConfig;
|
|
4327
4369
|
}
|
|
@@ -4362,9 +4404,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4362
4404
|
transport: deps.transport
|
|
4363
4405
|
});
|
|
4364
4406
|
}
|
|
4365
|
-
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4366
|
-
this.handleIncomingTransfer(transfer)
|
|
4367
|
-
|
|
4407
|
+
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4408
|
+
(transfer) => this.handleIncomingTransfer(transfer)
|
|
4409
|
+
);
|
|
4368
4410
|
if (deps.transport.onPaymentRequest) {
|
|
4369
4411
|
this.unsubscribePaymentRequests = deps.transport.onPaymentRequest((request) => {
|
|
4370
4412
|
this.handleIncomingPaymentRequest(request);
|
|
@@ -4375,9 +4417,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4375
4417
|
this.handlePaymentRequestResponse(response);
|
|
4376
4418
|
});
|
|
4377
4419
|
}
|
|
4420
|
+
this.subscribeToStorageEvents();
|
|
4378
4421
|
}
|
|
4379
4422
|
/**
|
|
4380
|
-
* Load
|
|
4423
|
+
* Load all token data from storage providers and restore wallet state.
|
|
4424
|
+
*
|
|
4425
|
+
* Loads tokens, nametag data, transaction history, and pending transfers
|
|
4426
|
+
* from configured storage providers. Restores pending V5 tokens and
|
|
4427
|
+
* triggers a fire-and-forget {@link resolveUnconfirmed} call.
|
|
4381
4428
|
*/
|
|
4382
4429
|
async load() {
|
|
4383
4430
|
this.ensureInitialized();
|
|
@@ -4394,6 +4441,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4394
4441
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4395
4442
|
}
|
|
4396
4443
|
}
|
|
4444
|
+
await this.loadPendingV5Tokens();
|
|
4397
4445
|
await this.loadTokensFromFileStorage();
|
|
4398
4446
|
await this.loadNametagFromFileStorage();
|
|
4399
4447
|
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
@@ -4411,9 +4459,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4411
4459
|
this.pendingTransfers.set(transfer.id, transfer);
|
|
4412
4460
|
}
|
|
4413
4461
|
}
|
|
4462
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4463
|
+
});
|
|
4414
4464
|
}
|
|
4415
4465
|
/**
|
|
4416
|
-
* Cleanup
|
|
4466
|
+
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
4467
|
+
*
|
|
4468
|
+
* Should be called when the wallet is being shut down or the module is
|
|
4469
|
+
* no longer needed. Also destroys the L1 sub-module if present.
|
|
4417
4470
|
*/
|
|
4418
4471
|
destroy() {
|
|
4419
4472
|
this.unsubscribeTransfers?.();
|
|
@@ -4431,6 +4484,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4431
4484
|
resolver.reject(new Error("Module destroyed"));
|
|
4432
4485
|
}
|
|
4433
4486
|
this.pendingResponseResolvers.clear();
|
|
4487
|
+
this.unsubscribeStorageEvents();
|
|
4434
4488
|
if (this.l1) {
|
|
4435
4489
|
this.l1.destroy();
|
|
4436
4490
|
}
|
|
@@ -4447,7 +4501,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4447
4501
|
const result = {
|
|
4448
4502
|
id: crypto.randomUUID(),
|
|
4449
4503
|
status: "pending",
|
|
4450
|
-
tokens: []
|
|
4504
|
+
tokens: [],
|
|
4505
|
+
tokenTransfers: []
|
|
4451
4506
|
};
|
|
4452
4507
|
try {
|
|
4453
4508
|
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
@@ -4484,69 +4539,147 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4484
4539
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4485
4540
|
result.status = "submitted";
|
|
4486
4541
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4542
|
+
const transferMode = request.transferMode ?? "instant";
|
|
4487
4543
|
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4544
|
+
if (transferMode === "conservative") {
|
|
4545
|
+
this.log("Executing conservative split...");
|
|
4546
|
+
const splitExecutor = new TokenSplitExecutor({
|
|
4547
|
+
stateTransitionClient: stClient,
|
|
4548
|
+
trustBase,
|
|
4549
|
+
signingService
|
|
4550
|
+
});
|
|
4551
|
+
const splitResult = await splitExecutor.executeSplit(
|
|
4552
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4553
|
+
splitPlan.splitAmount,
|
|
4554
|
+
splitPlan.remainderAmount,
|
|
4555
|
+
splitPlan.coinId,
|
|
4556
|
+
recipientAddress
|
|
4557
|
+
);
|
|
4558
|
+
const changeTokenData = splitResult.tokenForSender.toJSON();
|
|
4559
|
+
const changeUiToken = {
|
|
4560
|
+
id: crypto.randomUUID(),
|
|
4561
|
+
coinId: request.coinId,
|
|
4562
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4563
|
+
name: this.getCoinName(request.coinId),
|
|
4564
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4565
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4566
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4567
|
+
status: "confirmed",
|
|
4568
|
+
createdAt: Date.now(),
|
|
4569
|
+
updatedAt: Date.now(),
|
|
4570
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4571
|
+
};
|
|
4572
|
+
await this.addToken(changeUiToken, true);
|
|
4573
|
+
this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
|
|
4574
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4575
|
+
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
4576
|
+
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
4577
|
+
memo: request.memo
|
|
4578
|
+
});
|
|
4579
|
+
const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
|
|
4580
|
+
const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
|
|
4581
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
|
|
4582
|
+
result.tokenTransfers.push({
|
|
4583
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4584
|
+
method: "split",
|
|
4585
|
+
requestIdHex: splitRequestIdHex
|
|
4586
|
+
});
|
|
4587
|
+
this.log(`Conservative split transfer completed`);
|
|
4588
|
+
} else {
|
|
4589
|
+
this.log("Executing instant split...");
|
|
4590
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4591
|
+
const executor = new InstantSplitExecutor({
|
|
4592
|
+
stateTransitionClient: stClient,
|
|
4593
|
+
trustBase,
|
|
4594
|
+
signingService,
|
|
4595
|
+
devMode
|
|
4596
|
+
});
|
|
4597
|
+
const instantResult = await executor.executeSplitInstant(
|
|
4598
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4599
|
+
splitPlan.splitAmount,
|
|
4600
|
+
splitPlan.remainderAmount,
|
|
4601
|
+
splitPlan.coinId,
|
|
4602
|
+
recipientAddress,
|
|
4603
|
+
this.deps.transport,
|
|
4604
|
+
recipientPubkey,
|
|
4605
|
+
{
|
|
4606
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4607
|
+
const changeTokenData = changeToken.toJSON();
|
|
4608
|
+
const uiToken = {
|
|
4609
|
+
id: crypto.randomUUID(),
|
|
4610
|
+
coinId: request.coinId,
|
|
4611
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4612
|
+
name: this.getCoinName(request.coinId),
|
|
4613
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4614
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4615
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4616
|
+
status: "confirmed",
|
|
4617
|
+
createdAt: Date.now(),
|
|
4618
|
+
updatedAt: Date.now(),
|
|
4619
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4620
|
+
};
|
|
4621
|
+
await this.addToken(uiToken, true);
|
|
4622
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4623
|
+
},
|
|
4624
|
+
onStorageSync: async () => {
|
|
4625
|
+
await this.save();
|
|
4626
|
+
return true;
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
);
|
|
4630
|
+
if (!instantResult.success) {
|
|
4631
|
+
throw new Error(instantResult.error || "Instant split failed");
|
|
4632
|
+
}
|
|
4633
|
+
if (instantResult.backgroundPromise) {
|
|
4634
|
+
this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
|
|
4635
|
+
}
|
|
4636
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
4637
|
+
result.tokenTransfers.push({
|
|
4638
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4639
|
+
method: "split",
|
|
4640
|
+
splitGroupId: instantResult.splitGroupId,
|
|
4641
|
+
nostrEventId: instantResult.nostrEventId
|
|
4642
|
+
});
|
|
4643
|
+
this.log(`Instant split transfer completed`);
|
|
4644
|
+
}
|
|
4527
4645
|
}
|
|
4528
4646
|
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4529
4647
|
const token = tokenWithAmount.uiToken;
|
|
4530
4648
|
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4649
|
+
if (transferMode === "conservative") {
|
|
4650
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4651
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4652
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4653
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4654
|
+
}
|
|
4655
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4656
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4657
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4658
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4659
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4660
|
+
memo: request.memo
|
|
4661
|
+
});
|
|
4662
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4663
|
+
} else {
|
|
4664
|
+
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4665
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4666
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4667
|
+
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4668
|
+
memo: request.memo
|
|
4669
|
+
});
|
|
4670
|
+
console.log(`[Payments] NOSTR-FIRST: Direct token sent successfully`);
|
|
4671
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4672
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4673
|
+
);
|
|
4537
4674
|
}
|
|
4538
|
-
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
4539
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4540
4675
|
const requestIdBytes = commitment.requestId;
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
memo: request.memo
|
|
4676
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4677
|
+
result.tokenTransfers.push({
|
|
4678
|
+
sourceTokenId: token.id,
|
|
4679
|
+
method: "direct",
|
|
4680
|
+
requestIdHex
|
|
4547
4681
|
});
|
|
4548
|
-
|
|
4549
|
-
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
4682
|
+
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4550
4683
|
await this.removeToken(token.id, recipientNametag, true);
|
|
4551
4684
|
}
|
|
4552
4685
|
result.status = "delivered";
|
|
@@ -4559,7 +4692,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4559
4692
|
coinId: request.coinId,
|
|
4560
4693
|
symbol: this.getCoinSymbol(request.coinId),
|
|
4561
4694
|
timestamp: Date.now(),
|
|
4562
|
-
recipientNametag
|
|
4695
|
+
recipientNametag,
|
|
4696
|
+
transferId: result.id
|
|
4563
4697
|
});
|
|
4564
4698
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4565
4699
|
return result;
|
|
@@ -4695,6 +4829,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4695
4829
|
}
|
|
4696
4830
|
);
|
|
4697
4831
|
if (result.success) {
|
|
4832
|
+
if (result.backgroundPromise) {
|
|
4833
|
+
this.pendingBackgroundTasks.push(result.backgroundPromise);
|
|
4834
|
+
}
|
|
4698
4835
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4699
4836
|
await this.removeToken(tokenToSplit.id, recipientNametag, true);
|
|
4700
4837
|
await this.addToHistory({
|
|
@@ -4736,6 +4873,63 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4736
4873
|
*/
|
|
4737
4874
|
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4738
4875
|
this.ensureInitialized();
|
|
4876
|
+
if (!isInstantSplitBundleV5(bundle)) {
|
|
4877
|
+
return this.processInstantSplitBundleSync(bundle, senderPubkey);
|
|
4878
|
+
}
|
|
4879
|
+
try {
|
|
4880
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
4881
|
+
if (this.tokens.has(deterministicId)) {
|
|
4882
|
+
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
4883
|
+
return { success: true, durationMs: 0 };
|
|
4884
|
+
}
|
|
4885
|
+
const registry = TokenRegistry.getInstance();
|
|
4886
|
+
const pendingData = {
|
|
4887
|
+
type: "v5_bundle",
|
|
4888
|
+
stage: "RECEIVED",
|
|
4889
|
+
bundleJson: JSON.stringify(bundle),
|
|
4890
|
+
senderPubkey,
|
|
4891
|
+
savedAt: Date.now(),
|
|
4892
|
+
attemptCount: 0
|
|
4893
|
+
};
|
|
4894
|
+
const uiToken = {
|
|
4895
|
+
id: deterministicId,
|
|
4896
|
+
coinId: bundle.coinId,
|
|
4897
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4898
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4899
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4900
|
+
amount: bundle.amount,
|
|
4901
|
+
status: "submitted",
|
|
4902
|
+
// UNCONFIRMED
|
|
4903
|
+
createdAt: Date.now(),
|
|
4904
|
+
updatedAt: Date.now(),
|
|
4905
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4906
|
+
};
|
|
4907
|
+
await this.addToken(uiToken, false);
|
|
4908
|
+
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
4909
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
4910
|
+
id: bundle.splitGroupId,
|
|
4911
|
+
senderPubkey,
|
|
4912
|
+
tokens: [uiToken],
|
|
4913
|
+
receivedAt: Date.now()
|
|
4914
|
+
});
|
|
4915
|
+
await this.save();
|
|
4916
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4917
|
+
});
|
|
4918
|
+
return { success: true, durationMs: 0 };
|
|
4919
|
+
} catch (error) {
|
|
4920
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4921
|
+
return {
|
|
4922
|
+
success: false,
|
|
4923
|
+
error: errorMessage,
|
|
4924
|
+
durationMs: 0
|
|
4925
|
+
};
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
/**
|
|
4929
|
+
* Synchronous V4 bundle processing (dev mode only).
|
|
4930
|
+
* Kept for backward compatibility with V4 bundles.
|
|
4931
|
+
*/
|
|
4932
|
+
async processInstantSplitBundleSync(bundle, senderPubkey) {
|
|
4739
4933
|
try {
|
|
4740
4934
|
const signingService = await this.createSigningService();
|
|
4741
4935
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
@@ -4821,7 +5015,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4821
5015
|
}
|
|
4822
5016
|
}
|
|
4823
5017
|
/**
|
|
4824
|
-
*
|
|
5018
|
+
* Type-guard: check whether a payload is a valid {@link InstantSplitBundle} (V4 or V5).
|
|
5019
|
+
*
|
|
5020
|
+
* @param payload - The object to test.
|
|
5021
|
+
* @returns `true` if the payload matches the InstantSplitBundle shape.
|
|
4825
5022
|
*/
|
|
4826
5023
|
isInstantSplitBundle(payload) {
|
|
4827
5024
|
return isInstantSplitBundle(payload);
|
|
@@ -4902,39 +5099,57 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4902
5099
|
return [...this.paymentRequests];
|
|
4903
5100
|
}
|
|
4904
5101
|
/**
|
|
4905
|
-
* Get
|
|
5102
|
+
* Get the count of payment requests with status `'pending'`.
|
|
5103
|
+
*
|
|
5104
|
+
* @returns Number of pending incoming payment requests.
|
|
4906
5105
|
*/
|
|
4907
5106
|
getPendingPaymentRequestsCount() {
|
|
4908
5107
|
return this.paymentRequests.filter((r) => r.status === "pending").length;
|
|
4909
5108
|
}
|
|
4910
5109
|
/**
|
|
4911
|
-
* Accept a payment request
|
|
5110
|
+
* Accept a payment request and notify the requester.
|
|
5111
|
+
*
|
|
5112
|
+
* Marks the request as `'accepted'` and sends a response via transport.
|
|
5113
|
+
* The caller should subsequently call {@link send} to fulfill the payment.
|
|
5114
|
+
*
|
|
5115
|
+
* @param requestId - ID of the incoming payment request to accept.
|
|
4912
5116
|
*/
|
|
4913
5117
|
async acceptPaymentRequest(requestId2) {
|
|
4914
5118
|
this.updatePaymentRequestStatus(requestId2, "accepted");
|
|
4915
5119
|
await this.sendPaymentRequestResponse(requestId2, "accepted");
|
|
4916
5120
|
}
|
|
4917
5121
|
/**
|
|
4918
|
-
* Reject a payment request
|
|
5122
|
+
* Reject a payment request and notify the requester.
|
|
5123
|
+
*
|
|
5124
|
+
* @param requestId - ID of the incoming payment request to reject.
|
|
4919
5125
|
*/
|
|
4920
5126
|
async rejectPaymentRequest(requestId2) {
|
|
4921
5127
|
this.updatePaymentRequestStatus(requestId2, "rejected");
|
|
4922
5128
|
await this.sendPaymentRequestResponse(requestId2, "rejected");
|
|
4923
5129
|
}
|
|
4924
5130
|
/**
|
|
4925
|
-
* Mark a payment request as paid (
|
|
5131
|
+
* Mark a payment request as paid (local status update only).
|
|
5132
|
+
*
|
|
5133
|
+
* Typically called after a successful {@link send} to record that the
|
|
5134
|
+
* request has been fulfilled.
|
|
5135
|
+
*
|
|
5136
|
+
* @param requestId - ID of the incoming payment request to mark as paid.
|
|
4926
5137
|
*/
|
|
4927
5138
|
markPaymentRequestPaid(requestId2) {
|
|
4928
5139
|
this.updatePaymentRequestStatus(requestId2, "paid");
|
|
4929
5140
|
}
|
|
4930
5141
|
/**
|
|
4931
|
-
*
|
|
5142
|
+
* Remove all non-pending incoming payment requests from memory.
|
|
5143
|
+
*
|
|
5144
|
+
* Keeps only requests with status `'pending'`.
|
|
4932
5145
|
*/
|
|
4933
5146
|
clearProcessedPaymentRequests() {
|
|
4934
5147
|
this.paymentRequests = this.paymentRequests.filter((r) => r.status === "pending");
|
|
4935
5148
|
}
|
|
4936
5149
|
/**
|
|
4937
|
-
* Remove a specific payment request
|
|
5150
|
+
* Remove a specific incoming payment request by ID.
|
|
5151
|
+
*
|
|
5152
|
+
* @param requestId - ID of the payment request to remove.
|
|
4938
5153
|
*/
|
|
4939
5154
|
removePaymentRequest(requestId2) {
|
|
4940
5155
|
this.paymentRequests = this.paymentRequests.filter((r) => r.id !== requestId2);
|
|
@@ -5059,7 +5274,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5059
5274
|
});
|
|
5060
5275
|
}
|
|
5061
5276
|
/**
|
|
5062
|
-
* Cancel
|
|
5277
|
+
* Cancel an active {@link waitForPaymentResponse} call.
|
|
5278
|
+
*
|
|
5279
|
+
* The pending promise is rejected with a `'Cancelled'` error.
|
|
5280
|
+
*
|
|
5281
|
+
* @param requestId - The outgoing request ID whose wait should be cancelled.
|
|
5063
5282
|
*/
|
|
5064
5283
|
cancelWaitForPaymentResponse(requestId2) {
|
|
5065
5284
|
const resolver = this.pendingResponseResolvers.get(requestId2);
|
|
@@ -5070,14 +5289,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5070
5289
|
}
|
|
5071
5290
|
}
|
|
5072
5291
|
/**
|
|
5073
|
-
* Remove an outgoing payment request
|
|
5292
|
+
* Remove an outgoing payment request and cancel any pending wait.
|
|
5293
|
+
*
|
|
5294
|
+
* @param requestId - ID of the outgoing request to remove.
|
|
5074
5295
|
*/
|
|
5075
5296
|
removeOutgoingPaymentRequest(requestId2) {
|
|
5076
5297
|
this.outgoingPaymentRequests.delete(requestId2);
|
|
5077
5298
|
this.cancelWaitForPaymentResponse(requestId2);
|
|
5078
5299
|
}
|
|
5079
5300
|
/**
|
|
5080
|
-
*
|
|
5301
|
+
* Remove all outgoing payment requests that are `'paid'`, `'rejected'`, or `'expired'`.
|
|
5081
5302
|
*/
|
|
5082
5303
|
clearCompletedOutgoingPaymentRequests() {
|
|
5083
5304
|
for (const [id, request] of this.outgoingPaymentRequests) {
|
|
@@ -5149,6 +5370,71 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5149
5370
|
}
|
|
5150
5371
|
}
|
|
5151
5372
|
// ===========================================================================
|
|
5373
|
+
// Public API - Receive
|
|
5374
|
+
// ===========================================================================
|
|
5375
|
+
/**
|
|
5376
|
+
* Fetch and process pending incoming transfers from the transport layer.
|
|
5377
|
+
*
|
|
5378
|
+
* Performs a one-shot query to fetch all pending events, processes them
|
|
5379
|
+
* through the existing pipeline, and resolves after all stored events
|
|
5380
|
+
* are handled. Useful for batch/CLI apps that need explicit receive.
|
|
5381
|
+
*
|
|
5382
|
+
* When `finalize` is true, polls resolveUnconfirmed() + load() until all
|
|
5383
|
+
* tokens are confirmed or the timeout expires. Otherwise calls
|
|
5384
|
+
* resolveUnconfirmed() once to submit pending commitments.
|
|
5385
|
+
*
|
|
5386
|
+
* @param options - Optional receive options including finalization control
|
|
5387
|
+
* @param callback - Optional callback invoked for each newly received transfer
|
|
5388
|
+
* @returns ReceiveResult with transfers and finalization metadata
|
|
5389
|
+
*/
|
|
5390
|
+
async receive(options, callback) {
|
|
5391
|
+
this.ensureInitialized();
|
|
5392
|
+
if (!this.deps.transport.fetchPendingEvents) {
|
|
5393
|
+
throw new Error("Transport provider does not support fetchPendingEvents");
|
|
5394
|
+
}
|
|
5395
|
+
const opts = options ?? {};
|
|
5396
|
+
const tokensBefore = new Set(this.tokens.keys());
|
|
5397
|
+
await this.deps.transport.fetchPendingEvents();
|
|
5398
|
+
await this.load();
|
|
5399
|
+
const received = [];
|
|
5400
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5401
|
+
if (!tokensBefore.has(tokenId)) {
|
|
5402
|
+
const transfer = {
|
|
5403
|
+
id: tokenId,
|
|
5404
|
+
senderPubkey: "",
|
|
5405
|
+
tokens: [token],
|
|
5406
|
+
receivedAt: Date.now()
|
|
5407
|
+
};
|
|
5408
|
+
received.push(transfer);
|
|
5409
|
+
if (callback) callback(transfer);
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5412
|
+
const result = { transfers: received };
|
|
5413
|
+
if (opts.finalize) {
|
|
5414
|
+
const timeout = opts.timeout ?? 6e4;
|
|
5415
|
+
const pollInterval = opts.pollInterval ?? 2e3;
|
|
5416
|
+
const startTime = Date.now();
|
|
5417
|
+
while (Date.now() - startTime < timeout) {
|
|
5418
|
+
const resolution = await this.resolveUnconfirmed();
|
|
5419
|
+
result.finalization = resolution;
|
|
5420
|
+
if (opts.onProgress) opts.onProgress(resolution);
|
|
5421
|
+
const stillUnconfirmed = Array.from(this.tokens.values()).some(
|
|
5422
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5423
|
+
);
|
|
5424
|
+
if (!stillUnconfirmed) break;
|
|
5425
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
5426
|
+
await this.load();
|
|
5427
|
+
}
|
|
5428
|
+
result.finalizationDurationMs = Date.now() - startTime;
|
|
5429
|
+
result.timedOut = Array.from(this.tokens.values()).some(
|
|
5430
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5431
|
+
);
|
|
5432
|
+
} else {
|
|
5433
|
+
result.finalization = await this.resolveUnconfirmed();
|
|
5434
|
+
}
|
|
5435
|
+
return result;
|
|
5436
|
+
}
|
|
5437
|
+
// ===========================================================================
|
|
5152
5438
|
// Public API - Balance & Tokens
|
|
5153
5439
|
// ===========================================================================
|
|
5154
5440
|
/**
|
|
@@ -5158,10 +5444,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5158
5444
|
this.priceProvider = provider;
|
|
5159
5445
|
}
|
|
5160
5446
|
/**
|
|
5161
|
-
*
|
|
5162
|
-
*
|
|
5447
|
+
* Wait for all pending background operations (e.g., instant split change token creation).
|
|
5448
|
+
* Call this before process exit to ensure all tokens are saved.
|
|
5163
5449
|
*/
|
|
5164
|
-
async
|
|
5450
|
+
async waitForPendingOperations() {
|
|
5451
|
+
if (this.pendingBackgroundTasks.length > 0) {
|
|
5452
|
+
await Promise.allSettled(this.pendingBackgroundTasks);
|
|
5453
|
+
this.pendingBackgroundTasks = [];
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
/**
|
|
5457
|
+
* Get total portfolio value in USD.
|
|
5458
|
+
* Returns null if PriceProvider is not configured.
|
|
5459
|
+
*/
|
|
5460
|
+
async getFiatBalance() {
|
|
5165
5461
|
const assets = await this.getAssets();
|
|
5166
5462
|
if (!this.priceProvider) {
|
|
5167
5463
|
return null;
|
|
@@ -5177,19 +5473,95 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5177
5473
|
return hasAnyPrice ? total : null;
|
|
5178
5474
|
}
|
|
5179
5475
|
/**
|
|
5180
|
-
* Get
|
|
5181
|
-
*
|
|
5476
|
+
* Get token balances grouped by coin type.
|
|
5477
|
+
*
|
|
5478
|
+
* Returns an array of {@link Asset} objects, one per coin type held.
|
|
5479
|
+
* Each entry includes confirmed and unconfirmed breakdowns. Tokens with
|
|
5480
|
+
* status `'spent'`, `'invalid'`, or `'transferring'` are excluded.
|
|
5481
|
+
*
|
|
5482
|
+
* This is synchronous — no price data is included. Use {@link getAssets}
|
|
5483
|
+
* for the async version with fiat pricing.
|
|
5484
|
+
*
|
|
5485
|
+
* @param coinId - Optional coin ID to filter by (e.g. hex string). When omitted, all coin types are returned.
|
|
5486
|
+
* @returns Array of balance summaries (synchronous — no await needed).
|
|
5487
|
+
*/
|
|
5488
|
+
getBalance(coinId) {
|
|
5489
|
+
return this.aggregateTokens(coinId);
|
|
5490
|
+
}
|
|
5491
|
+
/**
|
|
5492
|
+
* Get aggregated assets (tokens grouped by coinId) with price data.
|
|
5493
|
+
* Includes both confirmed and unconfirmed tokens with breakdown.
|
|
5182
5494
|
*/
|
|
5183
5495
|
async getAssets(coinId) {
|
|
5496
|
+
const rawAssets = this.aggregateTokens(coinId);
|
|
5497
|
+
if (!this.priceProvider || rawAssets.length === 0) {
|
|
5498
|
+
return rawAssets;
|
|
5499
|
+
}
|
|
5500
|
+
try {
|
|
5501
|
+
const registry = TokenRegistry.getInstance();
|
|
5502
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5503
|
+
for (const asset of rawAssets) {
|
|
5504
|
+
const def = registry.getDefinition(asset.coinId);
|
|
5505
|
+
if (def?.name) {
|
|
5506
|
+
const existing = nameToCoins.get(def.name);
|
|
5507
|
+
if (existing) {
|
|
5508
|
+
existing.push(asset.coinId);
|
|
5509
|
+
} else {
|
|
5510
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
if (nameToCoins.size > 0) {
|
|
5515
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
5516
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5517
|
+
return rawAssets.map((raw) => {
|
|
5518
|
+
const def = registry.getDefinition(raw.coinId);
|
|
5519
|
+
const price = def?.name ? prices.get(def.name) : void 0;
|
|
5520
|
+
let fiatValueUsd = null;
|
|
5521
|
+
let fiatValueEur = null;
|
|
5522
|
+
if (price) {
|
|
5523
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5524
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5525
|
+
if (price.priceEur != null) {
|
|
5526
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
return {
|
|
5530
|
+
...raw,
|
|
5531
|
+
priceUsd: price?.priceUsd ?? null,
|
|
5532
|
+
priceEur: price?.priceEur ?? null,
|
|
5533
|
+
change24h: price?.change24h ?? null,
|
|
5534
|
+
fiatValueUsd,
|
|
5535
|
+
fiatValueEur
|
|
5536
|
+
};
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
5539
|
+
} catch (error) {
|
|
5540
|
+
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5541
|
+
}
|
|
5542
|
+
return rawAssets;
|
|
5543
|
+
}
|
|
5544
|
+
/**
|
|
5545
|
+
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5546
|
+
* Excludes tokens with status 'spent', 'invalid', or 'transferring'.
|
|
5547
|
+
*/
|
|
5548
|
+
aggregateTokens(coinId) {
|
|
5184
5549
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5185
5550
|
for (const token of this.tokens.values()) {
|
|
5186
|
-
if (token.status
|
|
5551
|
+
if (token.status === "spent" || token.status === "invalid" || token.status === "transferring") continue;
|
|
5187
5552
|
if (coinId && token.coinId !== coinId) continue;
|
|
5188
5553
|
const key = token.coinId;
|
|
5554
|
+
const amount = BigInt(token.amount);
|
|
5555
|
+
const isConfirmed = token.status === "confirmed";
|
|
5189
5556
|
const existing = assetsMap.get(key);
|
|
5190
5557
|
if (existing) {
|
|
5191
|
-
|
|
5192
|
-
|
|
5558
|
+
if (isConfirmed) {
|
|
5559
|
+
existing.confirmedAmount += amount;
|
|
5560
|
+
existing.confirmedTokenCount++;
|
|
5561
|
+
} else {
|
|
5562
|
+
existing.unconfirmedAmount += amount;
|
|
5563
|
+
existing.unconfirmedTokenCount++;
|
|
5564
|
+
}
|
|
5193
5565
|
} else {
|
|
5194
5566
|
assetsMap.set(key, {
|
|
5195
5567
|
coinId: token.coinId,
|
|
@@ -5197,78 +5569,42 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5197
5569
|
name: token.name,
|
|
5198
5570
|
decimals: token.decimals,
|
|
5199
5571
|
iconUrl: token.iconUrl,
|
|
5200
|
-
|
|
5201
|
-
|
|
5572
|
+
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5573
|
+
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5574
|
+
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5575
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
5202
5576
|
});
|
|
5203
5577
|
}
|
|
5204
5578
|
}
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
if (this.priceProvider && rawAssets.length > 0) {
|
|
5208
|
-
try {
|
|
5209
|
-
const registry = TokenRegistry.getInstance();
|
|
5210
|
-
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5211
|
-
for (const asset of rawAssets) {
|
|
5212
|
-
const def = registry.getDefinition(asset.coinId);
|
|
5213
|
-
if (def?.name) {
|
|
5214
|
-
const existing = nameToCoins.get(def.name);
|
|
5215
|
-
if (existing) {
|
|
5216
|
-
existing.push(asset.coinId);
|
|
5217
|
-
} else {
|
|
5218
|
-
nameToCoins.set(def.name, [asset.coinId]);
|
|
5219
|
-
}
|
|
5220
|
-
}
|
|
5221
|
-
}
|
|
5222
|
-
if (nameToCoins.size > 0) {
|
|
5223
|
-
const tokenNames = Array.from(nameToCoins.keys());
|
|
5224
|
-
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5225
|
-
priceMap = /* @__PURE__ */ new Map();
|
|
5226
|
-
for (const [name, coinIds] of nameToCoins) {
|
|
5227
|
-
const price = prices.get(name);
|
|
5228
|
-
if (price) {
|
|
5229
|
-
for (const cid of coinIds) {
|
|
5230
|
-
priceMap.set(cid, {
|
|
5231
|
-
priceUsd: price.priceUsd,
|
|
5232
|
-
priceEur: price.priceEur,
|
|
5233
|
-
change24h: price.change24h
|
|
5234
|
-
});
|
|
5235
|
-
}
|
|
5236
|
-
}
|
|
5237
|
-
}
|
|
5238
|
-
}
|
|
5239
|
-
} catch (error) {
|
|
5240
|
-
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5241
|
-
}
|
|
5242
|
-
}
|
|
5243
|
-
return rawAssets.map((raw) => {
|
|
5244
|
-
const price = priceMap?.get(raw.coinId);
|
|
5245
|
-
let fiatValueUsd = null;
|
|
5246
|
-
let fiatValueEur = null;
|
|
5247
|
-
if (price) {
|
|
5248
|
-
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5249
|
-
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5250
|
-
if (price.priceEur != null) {
|
|
5251
|
-
fiatValueEur = humanAmount * price.priceEur;
|
|
5252
|
-
}
|
|
5253
|
-
}
|
|
5579
|
+
return Array.from(assetsMap.values()).map((raw) => {
|
|
5580
|
+
const totalAmount = (raw.confirmedAmount + raw.unconfirmedAmount).toString();
|
|
5254
5581
|
return {
|
|
5255
5582
|
coinId: raw.coinId,
|
|
5256
5583
|
symbol: raw.symbol,
|
|
5257
5584
|
name: raw.name,
|
|
5258
5585
|
decimals: raw.decimals,
|
|
5259
5586
|
iconUrl: raw.iconUrl,
|
|
5260
|
-
totalAmount
|
|
5261
|
-
tokenCount: raw.
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5587
|
+
totalAmount,
|
|
5588
|
+
tokenCount: raw.confirmedTokenCount + raw.unconfirmedTokenCount,
|
|
5589
|
+
confirmedAmount: raw.confirmedAmount.toString(),
|
|
5590
|
+
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5591
|
+
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5592
|
+
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
5593
|
+
priceUsd: null,
|
|
5594
|
+
priceEur: null,
|
|
5595
|
+
change24h: null,
|
|
5596
|
+
fiatValueUsd: null,
|
|
5597
|
+
fiatValueEur: null
|
|
5267
5598
|
};
|
|
5268
5599
|
});
|
|
5269
5600
|
}
|
|
5270
5601
|
/**
|
|
5271
|
-
* Get all tokens
|
|
5602
|
+
* Get all tokens, optionally filtered by coin type and/or status.
|
|
5603
|
+
*
|
|
5604
|
+
* @param filter - Optional filter criteria.
|
|
5605
|
+
* @param filter.coinId - Return only tokens of this coin type.
|
|
5606
|
+
* @param filter.status - Return only tokens with this status (e.g. `'submitted'` for unconfirmed).
|
|
5607
|
+
* @returns Array of matching {@link Token} objects (synchronous).
|
|
5272
5608
|
*/
|
|
5273
5609
|
getTokens(filter) {
|
|
5274
5610
|
let tokens = Array.from(this.tokens.values());
|
|
@@ -5281,19 +5617,327 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5281
5617
|
return tokens;
|
|
5282
5618
|
}
|
|
5283
5619
|
/**
|
|
5284
|
-
* Get single token
|
|
5620
|
+
* Get a single token by its local ID.
|
|
5621
|
+
*
|
|
5622
|
+
* @param id - The local UUID assigned when the token was added.
|
|
5623
|
+
* @returns The token, or `undefined` if not found.
|
|
5285
5624
|
*/
|
|
5286
5625
|
getToken(id) {
|
|
5287
5626
|
return this.tokens.get(id);
|
|
5288
5627
|
}
|
|
5289
5628
|
// ===========================================================================
|
|
5629
|
+
// Public API - Unconfirmed Token Resolution
|
|
5630
|
+
// ===========================================================================
|
|
5631
|
+
/**
|
|
5632
|
+
* Attempt to resolve unconfirmed (status `'submitted'`) tokens by acquiring
|
|
5633
|
+
* their missing aggregator proofs.
|
|
5634
|
+
*
|
|
5635
|
+
* Each unconfirmed V5 token progresses through stages:
|
|
5636
|
+
* `RECEIVED` → `MINT_SUBMITTED` → `MINT_PROVEN` → `TRANSFER_SUBMITTED` → `FINALIZED`
|
|
5637
|
+
*
|
|
5638
|
+
* Uses 500 ms quick-timeouts per proof check so the call returns quickly even
|
|
5639
|
+
* when proofs are not yet available. Tokens that exceed 50 failed attempts are
|
|
5640
|
+
* marked `'invalid'`.
|
|
5641
|
+
*
|
|
5642
|
+
* Automatically called (fire-and-forget) by {@link load}.
|
|
5643
|
+
*
|
|
5644
|
+
* @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
|
|
5645
|
+
*/
|
|
5646
|
+
async resolveUnconfirmed() {
|
|
5647
|
+
this.ensureInitialized();
|
|
5648
|
+
const result = {
|
|
5649
|
+
resolved: 0,
|
|
5650
|
+
stillPending: 0,
|
|
5651
|
+
failed: 0,
|
|
5652
|
+
details: []
|
|
5653
|
+
};
|
|
5654
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5655
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5656
|
+
if (!stClient || !trustBase) return result;
|
|
5657
|
+
const signingService = await this.createSigningService();
|
|
5658
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5659
|
+
if (token.status !== "submitted") continue;
|
|
5660
|
+
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5661
|
+
if (!pending2) {
|
|
5662
|
+
result.stillPending++;
|
|
5663
|
+
continue;
|
|
5664
|
+
}
|
|
5665
|
+
if (pending2.type === "v5_bundle") {
|
|
5666
|
+
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
5667
|
+
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5668
|
+
if (progress === "resolved") result.resolved++;
|
|
5669
|
+
else if (progress === "failed") result.failed++;
|
|
5670
|
+
else result.stillPending++;
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
if (result.resolved > 0 || result.failed > 0) {
|
|
5674
|
+
await this.save();
|
|
5675
|
+
}
|
|
5676
|
+
return result;
|
|
5677
|
+
}
|
|
5678
|
+
// ===========================================================================
|
|
5679
|
+
// Private - V5 Lazy Resolution Helpers
|
|
5680
|
+
// ===========================================================================
|
|
5681
|
+
/**
|
|
5682
|
+
* Process a single V5 token through its finalization stages with quick-timeout proof checks.
|
|
5683
|
+
*/
|
|
5684
|
+
async resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService) {
|
|
5685
|
+
const bundle = JSON.parse(pending2.bundleJson);
|
|
5686
|
+
pending2.attemptCount++;
|
|
5687
|
+
pending2.lastAttemptAt = Date.now();
|
|
5688
|
+
try {
|
|
5689
|
+
if (pending2.stage === "RECEIVED") {
|
|
5690
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5691
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5692
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5693
|
+
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
5694
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5695
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5696
|
+
}
|
|
5697
|
+
pending2.stage = "MINT_SUBMITTED";
|
|
5698
|
+
this.updatePendingFinalization(token, pending2);
|
|
5699
|
+
}
|
|
5700
|
+
if (pending2.stage === "MINT_SUBMITTED") {
|
|
5701
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5702
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5703
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5704
|
+
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5705
|
+
if (!proof) {
|
|
5706
|
+
this.updatePendingFinalization(token, pending2);
|
|
5707
|
+
return "pending";
|
|
5708
|
+
}
|
|
5709
|
+
pending2.mintProofJson = JSON.stringify(proof);
|
|
5710
|
+
pending2.stage = "MINT_PROVEN";
|
|
5711
|
+
this.updatePendingFinalization(token, pending2);
|
|
5712
|
+
}
|
|
5713
|
+
if (pending2.stage === "MINT_PROVEN") {
|
|
5714
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5715
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5716
|
+
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
5717
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5718
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5719
|
+
}
|
|
5720
|
+
pending2.stage = "TRANSFER_SUBMITTED";
|
|
5721
|
+
this.updatePendingFinalization(token, pending2);
|
|
5722
|
+
}
|
|
5723
|
+
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
5724
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5725
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5726
|
+
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5727
|
+
if (!proof) {
|
|
5728
|
+
this.updatePendingFinalization(token, pending2);
|
|
5729
|
+
return "pending";
|
|
5730
|
+
}
|
|
5731
|
+
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5732
|
+
const confirmedToken = {
|
|
5733
|
+
id: token.id,
|
|
5734
|
+
coinId: token.coinId,
|
|
5735
|
+
symbol: token.symbol,
|
|
5736
|
+
name: token.name,
|
|
5737
|
+
decimals: token.decimals,
|
|
5738
|
+
iconUrl: token.iconUrl,
|
|
5739
|
+
amount: token.amount,
|
|
5740
|
+
status: "confirmed",
|
|
5741
|
+
createdAt: token.createdAt,
|
|
5742
|
+
updatedAt: Date.now(),
|
|
5743
|
+
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5744
|
+
};
|
|
5745
|
+
this.tokens.set(tokenId, confirmedToken);
|
|
5746
|
+
await this.saveTokenToFileStorage(confirmedToken);
|
|
5747
|
+
await this.addToHistory({
|
|
5748
|
+
type: "RECEIVED",
|
|
5749
|
+
amount: confirmedToken.amount,
|
|
5750
|
+
coinId: confirmedToken.coinId,
|
|
5751
|
+
symbol: confirmedToken.symbol || "UNK",
|
|
5752
|
+
timestamp: Date.now(),
|
|
5753
|
+
senderPubkey: pending2.senderPubkey
|
|
5754
|
+
});
|
|
5755
|
+
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5756
|
+
return "resolved";
|
|
5757
|
+
}
|
|
5758
|
+
return "pending";
|
|
5759
|
+
} catch (error) {
|
|
5760
|
+
console.error(`[Payments] resolveV5Token failed for ${tokenId.slice(0, 8)}:`, error);
|
|
5761
|
+
if (pending2.attemptCount > 50) {
|
|
5762
|
+
token.status = "invalid";
|
|
5763
|
+
token.updatedAt = Date.now();
|
|
5764
|
+
this.tokens.set(tokenId, token);
|
|
5765
|
+
return "failed";
|
|
5766
|
+
}
|
|
5767
|
+
this.updatePendingFinalization(token, pending2);
|
|
5768
|
+
return "pending";
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
/**
|
|
5772
|
+
* Non-blocking proof check with 500ms timeout.
|
|
5773
|
+
*/
|
|
5774
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5775
|
+
async quickProofCheck(stClient, trustBase, commitment, timeoutMs = 500) {
|
|
5776
|
+
try {
|
|
5777
|
+
const proof = await Promise.race([
|
|
5778
|
+
waitInclusionProof5(trustBase, stClient, commitment),
|
|
5779
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
|
5780
|
+
]);
|
|
5781
|
+
return proof;
|
|
5782
|
+
} catch {
|
|
5783
|
+
return null;
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
/**
|
|
5787
|
+
* Perform V5 bundle finalization from stored bundle data and proofs.
|
|
5788
|
+
* Extracted from InstantSplitProcessor.processV5Bundle() steps 4-10.
|
|
5789
|
+
*/
|
|
5790
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5791
|
+
async finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase) {
|
|
5792
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5793
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5794
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5795
|
+
const mintProofJson = JSON.parse(pending2.mintProofJson);
|
|
5796
|
+
const mintProof = InclusionProof.fromJSON(mintProofJson);
|
|
5797
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
5798
|
+
const tokenType = new TokenType3(fromHex4(bundle.tokenTypeHex));
|
|
5799
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
5800
|
+
const tokenJson = {
|
|
5801
|
+
version: "2.0",
|
|
5802
|
+
state: senderMintedStateJson,
|
|
5803
|
+
genesis: mintTransaction.toJSON(),
|
|
5804
|
+
transactions: [],
|
|
5805
|
+
nametags: []
|
|
5806
|
+
};
|
|
5807
|
+
const mintedToken = await SdkToken2.fromJSON(tokenJson);
|
|
5808
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5809
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5810
|
+
const transferProof = await waitInclusionProof5(trustBase, stClient, transferCommitment);
|
|
5811
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
5812
|
+
const transferSalt = fromHex4(bundle.transferSaltHex);
|
|
5813
|
+
const recipientPredicate = await UnmaskedPredicate5.create(
|
|
5814
|
+
mintData.tokenId,
|
|
5815
|
+
tokenType,
|
|
5816
|
+
signingService,
|
|
5817
|
+
HashAlgorithm5.SHA256,
|
|
5818
|
+
transferSalt
|
|
5819
|
+
);
|
|
5820
|
+
const recipientState = new TokenState5(recipientPredicate, null);
|
|
5821
|
+
let nametagTokens = [];
|
|
5822
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
5823
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
5824
|
+
if (bundle.nametagTokenJson) {
|
|
5825
|
+
try {
|
|
5826
|
+
const nametagToken = await SdkToken2.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
5827
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5828
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5829
|
+
if (proxy.address === recipientAddressStr) {
|
|
5830
|
+
nametagTokens = [nametagToken];
|
|
5831
|
+
}
|
|
5832
|
+
} catch {
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5835
|
+
if (nametagTokens.length === 0 && this.nametag?.token) {
|
|
5836
|
+
try {
|
|
5837
|
+
const nametagToken = await SdkToken2.fromJSON(this.nametag.token);
|
|
5838
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5839
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5840
|
+
if (proxy.address === recipientAddressStr) {
|
|
5841
|
+
nametagTokens = [nametagToken];
|
|
5842
|
+
}
|
|
5843
|
+
} catch {
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
}
|
|
5847
|
+
return stClient.finalizeTransaction(trustBase, mintedToken, recipientState, transferTransaction, nametagTokens);
|
|
5848
|
+
}
|
|
5849
|
+
/**
|
|
5850
|
+
* Parse pending finalization metadata from token's sdkData.
|
|
5851
|
+
*/
|
|
5852
|
+
parsePendingFinalization(sdkData) {
|
|
5853
|
+
if (!sdkData) return null;
|
|
5854
|
+
try {
|
|
5855
|
+
const data = JSON.parse(sdkData);
|
|
5856
|
+
if (data._pendingFinalization && data._pendingFinalization.type === "v5_bundle") {
|
|
5857
|
+
return data._pendingFinalization;
|
|
5858
|
+
}
|
|
5859
|
+
return null;
|
|
5860
|
+
} catch {
|
|
5861
|
+
return null;
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
/**
|
|
5865
|
+
* Update pending finalization metadata in token's sdkData.
|
|
5866
|
+
* Creates a new token object since sdkData is readonly.
|
|
5867
|
+
*/
|
|
5868
|
+
updatePendingFinalization(token, pending2) {
|
|
5869
|
+
const updated = {
|
|
5870
|
+
id: token.id,
|
|
5871
|
+
coinId: token.coinId,
|
|
5872
|
+
symbol: token.symbol,
|
|
5873
|
+
name: token.name,
|
|
5874
|
+
decimals: token.decimals,
|
|
5875
|
+
iconUrl: token.iconUrl,
|
|
5876
|
+
amount: token.amount,
|
|
5877
|
+
status: token.status,
|
|
5878
|
+
createdAt: token.createdAt,
|
|
5879
|
+
updatedAt: Date.now(),
|
|
5880
|
+
sdkData: JSON.stringify({ _pendingFinalization: pending2 })
|
|
5881
|
+
};
|
|
5882
|
+
this.tokens.set(token.id, updated);
|
|
5883
|
+
}
|
|
5884
|
+
/**
|
|
5885
|
+
* Save pending V5 tokens to key-value storage.
|
|
5886
|
+
* These tokens can't be serialized to TXF format (no genesis/state),
|
|
5887
|
+
* so we persist them separately and restore on load().
|
|
5888
|
+
*/
|
|
5889
|
+
async savePendingV5Tokens() {
|
|
5890
|
+
const pendingTokens = [];
|
|
5891
|
+
for (const token of this.tokens.values()) {
|
|
5892
|
+
if (this.parsePendingFinalization(token.sdkData)) {
|
|
5893
|
+
pendingTokens.push(token);
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
if (pendingTokens.length > 0) {
|
|
5897
|
+
await this.deps.storage.set(
|
|
5898
|
+
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
5899
|
+
JSON.stringify(pendingTokens)
|
|
5900
|
+
);
|
|
5901
|
+
} else {
|
|
5902
|
+
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
/**
|
|
5906
|
+
* Load pending V5 tokens from key-value storage and merge into tokens map.
|
|
5907
|
+
* Called during load() to restore tokens that TXF format can't represent.
|
|
5908
|
+
*/
|
|
5909
|
+
async loadPendingV5Tokens() {
|
|
5910
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
5911
|
+
if (!data) return;
|
|
5912
|
+
try {
|
|
5913
|
+
const pendingTokens = JSON.parse(data);
|
|
5914
|
+
for (const token of pendingTokens) {
|
|
5915
|
+
if (!this.tokens.has(token.id)) {
|
|
5916
|
+
this.tokens.set(token.id, token);
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
if (pendingTokens.length > 0) {
|
|
5920
|
+
this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
|
|
5921
|
+
}
|
|
5922
|
+
} catch {
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
// ===========================================================================
|
|
5290
5926
|
// Public API - Token Operations
|
|
5291
5927
|
// ===========================================================================
|
|
5292
5928
|
/**
|
|
5293
|
-
* Add a token
|
|
5294
|
-
*
|
|
5295
|
-
*
|
|
5296
|
-
*
|
|
5929
|
+
* Add a token to the wallet.
|
|
5930
|
+
*
|
|
5931
|
+
* Tokens are uniquely identified by a `(tokenId, stateHash)` composite key.
|
|
5932
|
+
* Duplicate detection:
|
|
5933
|
+
* - **Tombstoned** — rejected if the exact `(tokenId, stateHash)` pair has a tombstone.
|
|
5934
|
+
* - **Exact duplicate** — rejected if a token with the same composite key already exists.
|
|
5935
|
+
* - **State replacement** — if the same `tokenId` exists with a *different* `stateHash`,
|
|
5936
|
+
* the old state is archived and replaced with the incoming one.
|
|
5937
|
+
*
|
|
5938
|
+
* @param token - The token to add.
|
|
5939
|
+
* @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
|
|
5940
|
+
* @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
|
|
5297
5941
|
*/
|
|
5298
5942
|
async addToken(token, skipHistory = false) {
|
|
5299
5943
|
this.ensureInitialized();
|
|
@@ -5351,7 +5995,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5351
5995
|
});
|
|
5352
5996
|
}
|
|
5353
5997
|
await this.save();
|
|
5354
|
-
|
|
5998
|
+
if (!this.parsePendingFinalization(token.sdkData)) {
|
|
5999
|
+
await this.saveTokenToFileStorage(token);
|
|
6000
|
+
}
|
|
5355
6001
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
5356
6002
|
return true;
|
|
5357
6003
|
}
|
|
@@ -5408,6 +6054,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5408
6054
|
const data = fileData;
|
|
5409
6055
|
const tokenJson = data.token;
|
|
5410
6056
|
if (!tokenJson) continue;
|
|
6057
|
+
if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
|
|
6058
|
+
continue;
|
|
6059
|
+
}
|
|
5411
6060
|
let sdkTokenId;
|
|
5412
6061
|
if (typeof tokenJson === "object" && tokenJson !== null) {
|
|
5413
6062
|
const tokenObj = tokenJson;
|
|
@@ -5459,7 +6108,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5459
6108
|
this.log(`Loaded ${this.tokens.size} tokens from file storage`);
|
|
5460
6109
|
}
|
|
5461
6110
|
/**
|
|
5462
|
-
* Update an existing token
|
|
6111
|
+
* Update an existing token or add it if not found.
|
|
6112
|
+
*
|
|
6113
|
+
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
6114
|
+
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
6115
|
+
*
|
|
6116
|
+
* @param token - The token with updated data. Must include a valid `id`.
|
|
5463
6117
|
*/
|
|
5464
6118
|
async updateToken(token) {
|
|
5465
6119
|
this.ensureInitialized();
|
|
@@ -5483,7 +6137,15 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5483
6137
|
this.log(`Updated token ${token.id}`);
|
|
5484
6138
|
}
|
|
5485
6139
|
/**
|
|
5486
|
-
* Remove a token
|
|
6140
|
+
* Remove a token from the wallet.
|
|
6141
|
+
*
|
|
6142
|
+
* The token is archived first, then a tombstone `(tokenId, stateHash)` is
|
|
6143
|
+
* created to prevent re-addition via Nostr re-delivery. A `SENT` history
|
|
6144
|
+
* entry is created unless `skipHistory` is `true`.
|
|
6145
|
+
*
|
|
6146
|
+
* @param tokenId - Local UUID of the token to remove.
|
|
6147
|
+
* @param recipientNametag - Optional nametag of the transfer recipient (for history).
|
|
6148
|
+
* @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
|
|
5487
6149
|
*/
|
|
5488
6150
|
async removeToken(tokenId, recipientNametag, skipHistory = false) {
|
|
5489
6151
|
this.ensureInitialized();
|
|
@@ -5545,13 +6207,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5545
6207
|
// Public API - Tombstones
|
|
5546
6208
|
// ===========================================================================
|
|
5547
6209
|
/**
|
|
5548
|
-
* Get all
|
|
6210
|
+
* Get all tombstone entries.
|
|
6211
|
+
*
|
|
6212
|
+
* Each tombstone is keyed by `(tokenId, stateHash)` and prevents a spent
|
|
6213
|
+
* token state from being re-added (e.g. via Nostr re-delivery).
|
|
6214
|
+
*
|
|
6215
|
+
* @returns A shallow copy of the tombstone array.
|
|
5549
6216
|
*/
|
|
5550
6217
|
getTombstones() {
|
|
5551
6218
|
return [...this.tombstones];
|
|
5552
6219
|
}
|
|
5553
6220
|
/**
|
|
5554
|
-
* Check
|
|
6221
|
+
* Check whether a specific `(tokenId, stateHash)` combination is tombstoned.
|
|
6222
|
+
*
|
|
6223
|
+
* @param tokenId - The genesis token ID.
|
|
6224
|
+
* @param stateHash - The state hash of the token version to check.
|
|
6225
|
+
* @returns `true` if the exact combination has been tombstoned.
|
|
5555
6226
|
*/
|
|
5556
6227
|
isStateTombstoned(tokenId, stateHash) {
|
|
5557
6228
|
return this.tombstones.some(
|
|
@@ -5559,8 +6230,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5559
6230
|
);
|
|
5560
6231
|
}
|
|
5561
6232
|
/**
|
|
5562
|
-
* Merge remote
|
|
5563
|
-
*
|
|
6233
|
+
* Merge tombstones received from a remote sync source.
|
|
6234
|
+
*
|
|
6235
|
+
* Any local token whose `(tokenId, stateHash)` matches a remote tombstone is
|
|
6236
|
+
* removed. The remote tombstones are then added to the local set (union merge).
|
|
6237
|
+
*
|
|
6238
|
+
* @param remoteTombstones - Tombstone entries from the remote source.
|
|
6239
|
+
* @returns Number of local tokens that were removed.
|
|
5564
6240
|
*/
|
|
5565
6241
|
async mergeTombstones(remoteTombstones) {
|
|
5566
6242
|
this.ensureInitialized();
|
|
@@ -5596,7 +6272,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5596
6272
|
return removedCount;
|
|
5597
6273
|
}
|
|
5598
6274
|
/**
|
|
5599
|
-
*
|
|
6275
|
+
* Remove tombstones older than `maxAge` and cap the list at 100 entries.
|
|
6276
|
+
*
|
|
6277
|
+
* @param maxAge - Maximum age in milliseconds (default: 30 days).
|
|
5600
6278
|
*/
|
|
5601
6279
|
async pruneTombstones(maxAge) {
|
|
5602
6280
|
const originalCount = this.tombstones.length;
|
|
@@ -5610,20 +6288,38 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5610
6288
|
// Public API - Archives
|
|
5611
6289
|
// ===========================================================================
|
|
5612
6290
|
/**
|
|
5613
|
-
* Get archived tokens
|
|
6291
|
+
* Get all archived (spent/superseded) tokens in TXF format.
|
|
6292
|
+
*
|
|
6293
|
+
* Archived tokens are kept for recovery and sync purposes. The map key is
|
|
6294
|
+
* the genesis token ID.
|
|
6295
|
+
*
|
|
6296
|
+
* @returns A shallow copy of the archived token map.
|
|
5614
6297
|
*/
|
|
5615
6298
|
getArchivedTokens() {
|
|
5616
6299
|
return new Map(this.archivedTokens);
|
|
5617
6300
|
}
|
|
5618
6301
|
/**
|
|
5619
|
-
* Get best archived version of a token
|
|
6302
|
+
* Get the best (most committed transactions) archived version of a token.
|
|
6303
|
+
*
|
|
6304
|
+
* Searches both archived and forked token maps and returns the version with
|
|
6305
|
+
* the highest number of committed transactions.
|
|
6306
|
+
*
|
|
6307
|
+
* @param tokenId - The genesis token ID to look up.
|
|
6308
|
+
* @returns The best TXF token version, or `null` if not found.
|
|
5620
6309
|
*/
|
|
5621
6310
|
getBestArchivedVersion(tokenId) {
|
|
5622
6311
|
return findBestTokenVersion(tokenId, this.archivedTokens, this.forkedTokens);
|
|
5623
6312
|
}
|
|
5624
6313
|
/**
|
|
5625
|
-
* Merge remote
|
|
5626
|
-
*
|
|
6314
|
+
* Merge archived tokens from a remote sync source.
|
|
6315
|
+
*
|
|
6316
|
+
* For each remote token:
|
|
6317
|
+
* - If missing locally, it is added.
|
|
6318
|
+
* - If the remote version is an incremental update of the local, it replaces it.
|
|
6319
|
+
* - If the histories diverge (fork), the remote version is stored via {@link storeForkedToken}.
|
|
6320
|
+
*
|
|
6321
|
+
* @param remoteArchived - Map of genesis token ID → TXF token from remote.
|
|
6322
|
+
* @returns Number of tokens that were updated or added locally.
|
|
5627
6323
|
*/
|
|
5628
6324
|
async mergeArchivedTokens(remoteArchived) {
|
|
5629
6325
|
let mergedCount = 0;
|
|
@@ -5646,7 +6342,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5646
6342
|
return mergedCount;
|
|
5647
6343
|
}
|
|
5648
6344
|
/**
|
|
5649
|
-
* Prune archived tokens
|
|
6345
|
+
* Prune archived tokens to keep at most `maxCount` entries.
|
|
6346
|
+
*
|
|
6347
|
+
* Oldest entries (by insertion order) are removed first.
|
|
6348
|
+
*
|
|
6349
|
+
* @param maxCount - Maximum number of archived tokens to retain (default: 100).
|
|
5650
6350
|
*/
|
|
5651
6351
|
async pruneArchivedTokens(maxCount = 100) {
|
|
5652
6352
|
if (this.archivedTokens.size <= maxCount) return;
|
|
@@ -5659,13 +6359,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5659
6359
|
// Public API - Forked Tokens
|
|
5660
6360
|
// ===========================================================================
|
|
5661
6361
|
/**
|
|
5662
|
-
* Get forked
|
|
6362
|
+
* Get all forked token versions.
|
|
6363
|
+
*
|
|
6364
|
+
* Forked tokens represent alternative histories detected during sync.
|
|
6365
|
+
* The map key is `{tokenId}_{stateHash}`.
|
|
6366
|
+
*
|
|
6367
|
+
* @returns A shallow copy of the forked tokens map.
|
|
5663
6368
|
*/
|
|
5664
6369
|
getForkedTokens() {
|
|
5665
6370
|
return new Map(this.forkedTokens);
|
|
5666
6371
|
}
|
|
5667
6372
|
/**
|
|
5668
|
-
* Store a forked token
|
|
6373
|
+
* Store a forked token version (alternative history).
|
|
6374
|
+
*
|
|
6375
|
+
* No-op if the exact `(tokenId, stateHash)` key already exists.
|
|
6376
|
+
*
|
|
6377
|
+
* @param tokenId - Genesis token ID.
|
|
6378
|
+
* @param stateHash - State hash of this forked version.
|
|
6379
|
+
* @param txfToken - The TXF token data to store.
|
|
5669
6380
|
*/
|
|
5670
6381
|
async storeForkedToken(tokenId, stateHash, txfToken) {
|
|
5671
6382
|
const key = `${tokenId}_${stateHash}`;
|
|
@@ -5675,8 +6386,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5675
6386
|
await this.save();
|
|
5676
6387
|
}
|
|
5677
6388
|
/**
|
|
5678
|
-
* Merge remote
|
|
5679
|
-
*
|
|
6389
|
+
* Merge forked tokens from a remote sync source. Only new keys are added.
|
|
6390
|
+
*
|
|
6391
|
+
* @param remoteForked - Map of `{tokenId}_{stateHash}` → TXF token from remote.
|
|
6392
|
+
* @returns Number of new forked tokens added.
|
|
5680
6393
|
*/
|
|
5681
6394
|
async mergeForkedTokens(remoteForked) {
|
|
5682
6395
|
let addedCount = 0;
|
|
@@ -5692,7 +6405,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5692
6405
|
return addedCount;
|
|
5693
6406
|
}
|
|
5694
6407
|
/**
|
|
5695
|
-
* Prune forked tokens
|
|
6408
|
+
* Prune forked tokens to keep at most `maxCount` entries.
|
|
6409
|
+
*
|
|
6410
|
+
* @param maxCount - Maximum number of forked tokens to retain (default: 50).
|
|
5696
6411
|
*/
|
|
5697
6412
|
async pruneForkedTokens(maxCount = 50) {
|
|
5698
6413
|
if (this.forkedTokens.size <= maxCount) return;
|
|
@@ -5705,13 +6420,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5705
6420
|
// Public API - Transaction History
|
|
5706
6421
|
// ===========================================================================
|
|
5707
6422
|
/**
|
|
5708
|
-
* Get transaction history
|
|
6423
|
+
* Get the transaction history sorted newest-first.
|
|
6424
|
+
*
|
|
6425
|
+
* @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
|
|
5709
6426
|
*/
|
|
5710
6427
|
getHistory() {
|
|
5711
6428
|
return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
|
|
5712
6429
|
}
|
|
5713
6430
|
/**
|
|
5714
|
-
*
|
|
6431
|
+
* Append an entry to the transaction history.
|
|
6432
|
+
*
|
|
6433
|
+
* A unique `id` is auto-generated. The entry is immediately persisted to storage.
|
|
6434
|
+
*
|
|
6435
|
+
* @param entry - History entry fields (without `id`).
|
|
5715
6436
|
*/
|
|
5716
6437
|
async addToHistory(entry) {
|
|
5717
6438
|
this.ensureInitialized();
|
|
@@ -5729,7 +6450,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5729
6450
|
// Public API - Nametag
|
|
5730
6451
|
// ===========================================================================
|
|
5731
6452
|
/**
|
|
5732
|
-
* Set nametag for current identity
|
|
6453
|
+
* Set the nametag data for the current identity.
|
|
6454
|
+
*
|
|
6455
|
+
* Persists to both key-value storage and file storage (lottery compatibility).
|
|
6456
|
+
*
|
|
6457
|
+
* @param nametag - The nametag data including minted token JSON.
|
|
5733
6458
|
*/
|
|
5734
6459
|
async setNametag(nametag) {
|
|
5735
6460
|
this.ensureInitialized();
|
|
@@ -5739,19 +6464,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5739
6464
|
this.log(`Nametag set: ${nametag.name}`);
|
|
5740
6465
|
}
|
|
5741
6466
|
/**
|
|
5742
|
-
* Get nametag
|
|
6467
|
+
* Get the current nametag data.
|
|
6468
|
+
*
|
|
6469
|
+
* @returns The nametag data, or `null` if no nametag is set.
|
|
5743
6470
|
*/
|
|
5744
6471
|
getNametag() {
|
|
5745
6472
|
return this.nametag;
|
|
5746
6473
|
}
|
|
5747
6474
|
/**
|
|
5748
|
-
* Check
|
|
6475
|
+
* Check whether a nametag is currently set.
|
|
6476
|
+
*
|
|
6477
|
+
* @returns `true` if nametag data is present.
|
|
5749
6478
|
*/
|
|
5750
6479
|
hasNametag() {
|
|
5751
6480
|
return this.nametag !== null;
|
|
5752
6481
|
}
|
|
5753
6482
|
/**
|
|
5754
|
-
*
|
|
6483
|
+
* Remove the current nametag data from memory and storage.
|
|
5755
6484
|
*/
|
|
5756
6485
|
async clearNametag() {
|
|
5757
6486
|
this.ensureInitialized();
|
|
@@ -5845,9 +6574,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5845
6574
|
try {
|
|
5846
6575
|
const signingService = await this.createSigningService();
|
|
5847
6576
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5848
|
-
const { TokenType:
|
|
6577
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5849
6578
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5850
|
-
const tokenType = new
|
|
6579
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5851
6580
|
const addressRef = await UnmaskedPredicateReference4.create(
|
|
5852
6581
|
tokenType,
|
|
5853
6582
|
signingService.algorithm,
|
|
@@ -5908,11 +6637,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5908
6637
|
// Public API - Sync & Validate
|
|
5909
6638
|
// ===========================================================================
|
|
5910
6639
|
/**
|
|
5911
|
-
* Sync with all token storage providers (IPFS,
|
|
5912
|
-
*
|
|
6640
|
+
* Sync local token state with all configured token storage providers (IPFS, file, etc.).
|
|
6641
|
+
*
|
|
6642
|
+
* For each provider, the local data is packaged into TXF storage format, sent
|
|
6643
|
+
* to the provider's `sync()` method, and the merged result is applied locally.
|
|
6644
|
+
* Emits `sync:started`, `sync:completed`, and `sync:error` events.
|
|
6645
|
+
*
|
|
6646
|
+
* @returns Summary with counts of tokens added and removed during sync.
|
|
5913
6647
|
*/
|
|
5914
6648
|
async sync() {
|
|
5915
6649
|
this.ensureInitialized();
|
|
6650
|
+
if (this._syncInProgress) {
|
|
6651
|
+
return this._syncInProgress;
|
|
6652
|
+
}
|
|
6653
|
+
this._syncInProgress = this._doSync();
|
|
6654
|
+
try {
|
|
6655
|
+
return await this._syncInProgress;
|
|
6656
|
+
} finally {
|
|
6657
|
+
this._syncInProgress = null;
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
6660
|
+
async _doSync() {
|
|
5916
6661
|
this.deps.emitEvent("sync:started", { source: "payments" });
|
|
5917
6662
|
try {
|
|
5918
6663
|
const providers = this.getTokenStorageProviders();
|
|
@@ -5950,6 +6695,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5950
6695
|
});
|
|
5951
6696
|
}
|
|
5952
6697
|
}
|
|
6698
|
+
if (totalAdded > 0 || totalRemoved > 0) {
|
|
6699
|
+
await this.save();
|
|
6700
|
+
}
|
|
5953
6701
|
this.deps.emitEvent("sync:completed", {
|
|
5954
6702
|
source: "payments",
|
|
5955
6703
|
count: this.tokens.size
|
|
@@ -5963,6 +6711,66 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5963
6711
|
throw error;
|
|
5964
6712
|
}
|
|
5965
6713
|
}
|
|
6714
|
+
// ===========================================================================
|
|
6715
|
+
// Storage Event Subscription (Push-Based Sync)
|
|
6716
|
+
// ===========================================================================
|
|
6717
|
+
/**
|
|
6718
|
+
* Subscribe to 'storage:remote-updated' events from all token storage providers.
|
|
6719
|
+
* When a provider emits this event, a debounced sync is triggered.
|
|
6720
|
+
*/
|
|
6721
|
+
subscribeToStorageEvents() {
|
|
6722
|
+
this.unsubscribeStorageEvents();
|
|
6723
|
+
const providers = this.getTokenStorageProviders();
|
|
6724
|
+
for (const [providerId, provider] of providers) {
|
|
6725
|
+
if (provider.onEvent) {
|
|
6726
|
+
const unsub = provider.onEvent((event) => {
|
|
6727
|
+
if (event.type === "storage:remote-updated") {
|
|
6728
|
+
this.log("Remote update detected from provider", providerId, event.data);
|
|
6729
|
+
this.debouncedSyncFromRemoteUpdate(providerId, event.data);
|
|
6730
|
+
}
|
|
6731
|
+
});
|
|
6732
|
+
this.storageEventUnsubscribers.push(unsub);
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
}
|
|
6736
|
+
/**
|
|
6737
|
+
* Unsubscribe from all storage provider events and clear debounce timer.
|
|
6738
|
+
*/
|
|
6739
|
+
unsubscribeStorageEvents() {
|
|
6740
|
+
for (const unsub of this.storageEventUnsubscribers) {
|
|
6741
|
+
unsub();
|
|
6742
|
+
}
|
|
6743
|
+
this.storageEventUnsubscribers = [];
|
|
6744
|
+
if (this.syncDebounceTimer) {
|
|
6745
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6746
|
+
this.syncDebounceTimer = null;
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
/**
|
|
6750
|
+
* Debounced sync triggered by a storage:remote-updated event.
|
|
6751
|
+
* Waits 500ms to batch rapid updates, then performs sync.
|
|
6752
|
+
*/
|
|
6753
|
+
debouncedSyncFromRemoteUpdate(providerId, eventData) {
|
|
6754
|
+
if (this.syncDebounceTimer) {
|
|
6755
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6756
|
+
}
|
|
6757
|
+
this.syncDebounceTimer = setTimeout(() => {
|
|
6758
|
+
this.syncDebounceTimer = null;
|
|
6759
|
+
this.sync().then((result) => {
|
|
6760
|
+
const data = eventData;
|
|
6761
|
+
this.deps?.emitEvent("sync:remote-update", {
|
|
6762
|
+
providerId,
|
|
6763
|
+
name: data?.name ?? "",
|
|
6764
|
+
sequence: data?.sequence ?? 0,
|
|
6765
|
+
cid: data?.cid ?? "",
|
|
6766
|
+
added: result.added,
|
|
6767
|
+
removed: result.removed
|
|
6768
|
+
});
|
|
6769
|
+
}).catch((err) => {
|
|
6770
|
+
this.log("Auto-sync from remote update failed:", err);
|
|
6771
|
+
});
|
|
6772
|
+
}, _PaymentsModule.SYNC_DEBOUNCE_MS);
|
|
6773
|
+
}
|
|
5966
6774
|
/**
|
|
5967
6775
|
* Get all active token storage providers
|
|
5968
6776
|
*/
|
|
@@ -5978,15 +6786,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5978
6786
|
return /* @__PURE__ */ new Map();
|
|
5979
6787
|
}
|
|
5980
6788
|
/**
|
|
5981
|
-
*
|
|
6789
|
+
* Replace the set of token storage providers at runtime.
|
|
6790
|
+
*
|
|
6791
|
+
* Use when providers are added or removed dynamically (e.g. IPFS node started).
|
|
6792
|
+
*
|
|
6793
|
+
* @param providers - New map of provider ID → TokenStorageProvider.
|
|
5982
6794
|
*/
|
|
5983
6795
|
updateTokenStorageProviders(providers) {
|
|
5984
6796
|
if (this.deps) {
|
|
5985
6797
|
this.deps.tokenStorageProviders = providers;
|
|
6798
|
+
this.subscribeToStorageEvents();
|
|
5986
6799
|
}
|
|
5987
6800
|
}
|
|
5988
6801
|
/**
|
|
5989
|
-
* Validate tokens
|
|
6802
|
+
* Validate all tokens against the aggregator (oracle provider).
|
|
6803
|
+
*
|
|
6804
|
+
* Tokens that fail validation or are detected as spent are marked `'invalid'`.
|
|
6805
|
+
*
|
|
6806
|
+
* @returns Object with arrays of valid and invalid tokens.
|
|
5990
6807
|
*/
|
|
5991
6808
|
async validate() {
|
|
5992
6809
|
this.ensureInitialized();
|
|
@@ -6007,7 +6824,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6007
6824
|
return { valid, invalid };
|
|
6008
6825
|
}
|
|
6009
6826
|
/**
|
|
6010
|
-
* Get pending transfers
|
|
6827
|
+
* Get all in-progress (pending) outgoing transfers.
|
|
6828
|
+
*
|
|
6829
|
+
* @returns Array of {@link TransferResult} objects for transfers that have not yet completed.
|
|
6011
6830
|
*/
|
|
6012
6831
|
getPendingTransfers() {
|
|
6013
6832
|
return Array.from(this.pendingTransfers.values());
|
|
@@ -6071,9 +6890,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6071
6890
|
*/
|
|
6072
6891
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
6073
6892
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
6074
|
-
const { TokenType:
|
|
6893
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
6075
6894
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
6076
|
-
const tokenType = new
|
|
6895
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
6077
6896
|
const pubkeyBytes = new Uint8Array(
|
|
6078
6897
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
6079
6898
|
);
|
|
@@ -6285,7 +7104,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6285
7104
|
this.deps.emitEvent("transfer:confirmed", {
|
|
6286
7105
|
id: crypto.randomUUID(),
|
|
6287
7106
|
status: "completed",
|
|
6288
|
-
tokens: [finalizedToken]
|
|
7107
|
+
tokens: [finalizedToken],
|
|
7108
|
+
tokenTransfers: []
|
|
6289
7109
|
});
|
|
6290
7110
|
await this.addToHistory({
|
|
6291
7111
|
type: "RECEIVED",
|
|
@@ -6308,14 +7128,26 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6308
7128
|
async handleIncomingTransfer(transfer) {
|
|
6309
7129
|
try {
|
|
6310
7130
|
const payload = transfer.payload;
|
|
7131
|
+
let instantBundle = null;
|
|
6311
7132
|
if (isInstantSplitBundle(payload)) {
|
|
7133
|
+
instantBundle = payload;
|
|
7134
|
+
} else if (payload.token) {
|
|
7135
|
+
try {
|
|
7136
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
7137
|
+
if (isInstantSplitBundle(inner)) {
|
|
7138
|
+
instantBundle = inner;
|
|
7139
|
+
}
|
|
7140
|
+
} catch {
|
|
7141
|
+
}
|
|
7142
|
+
}
|
|
7143
|
+
if (instantBundle) {
|
|
6312
7144
|
this.log("Processing INSTANT_SPLIT bundle...");
|
|
6313
7145
|
try {
|
|
6314
7146
|
if (!this.nametag) {
|
|
6315
7147
|
await this.loadNametagFromFileStorage();
|
|
6316
7148
|
}
|
|
6317
7149
|
const result = await this.processInstantSplitBundle(
|
|
6318
|
-
|
|
7150
|
+
instantBundle,
|
|
6319
7151
|
transfer.senderTransportPubkey
|
|
6320
7152
|
);
|
|
6321
7153
|
if (result.success) {
|
|
@@ -6328,6 +7160,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6328
7160
|
}
|
|
6329
7161
|
return;
|
|
6330
7162
|
}
|
|
7163
|
+
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
7164
|
+
this.log("Processing NOSTR-FIRST commitment-only transfer...");
|
|
7165
|
+
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
7166
|
+
return;
|
|
7167
|
+
}
|
|
6331
7168
|
let tokenData;
|
|
6332
7169
|
let finalizedSdkToken = null;
|
|
6333
7170
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -6483,6 +7320,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6483
7320
|
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
6484
7321
|
}
|
|
6485
7322
|
}
|
|
7323
|
+
await this.savePendingV5Tokens();
|
|
6486
7324
|
}
|
|
6487
7325
|
async saveToOutbox(transfer, recipient) {
|
|
6488
7326
|
const outbox = await this.loadOutbox();
|
|
@@ -6500,8 +7338,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6500
7338
|
}
|
|
6501
7339
|
async createStorageData() {
|
|
6502
7340
|
return await buildTxfStorageData(
|
|
6503
|
-
|
|
6504
|
-
// Empty - active tokens stored as token-xxx files
|
|
7341
|
+
Array.from(this.tokens.values()),
|
|
6505
7342
|
{
|
|
6506
7343
|
version: 1,
|
|
6507
7344
|
address: this.deps.identity.l1Address,
|
|
@@ -6686,7 +7523,7 @@ function createPaymentsModule(config) {
|
|
|
6686
7523
|
// modules/payments/TokenRecoveryService.ts
|
|
6687
7524
|
import { TokenId as TokenId4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenId";
|
|
6688
7525
|
import { TokenState as TokenState6 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
6689
|
-
import { TokenType as
|
|
7526
|
+
import { TokenType as TokenType4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
6690
7527
|
import { CoinId as CoinId5 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
|
|
6691
7528
|
import { HashAlgorithm as HashAlgorithm6 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
6692
7529
|
import { UnmaskedPredicate as UnmaskedPredicate6 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
@@ -7750,15 +8587,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
7750
8587
|
|
|
7751
8588
|
// core/Sphere.ts
|
|
7752
8589
|
import { SigningService as SigningService2 } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
|
|
7753
|
-
import { TokenType as
|
|
8590
|
+
import { TokenType as TokenType5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
7754
8591
|
import { HashAlgorithm as HashAlgorithm7 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
7755
8592
|
import { UnmaskedPredicateReference as UnmaskedPredicateReference3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
|
|
8593
|
+
import { normalizeNametag as normalizeNametag2, isPhoneNumber } from "@unicitylabs/nostr-js-sdk";
|
|
8594
|
+
function isValidNametag(nametag) {
|
|
8595
|
+
if (isPhoneNumber(nametag)) return true;
|
|
8596
|
+
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
8597
|
+
}
|
|
7756
8598
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
7757
8599
|
async function deriveL3PredicateAddress(privateKey) {
|
|
7758
8600
|
const secret = Buffer.from(privateKey, "hex");
|
|
7759
8601
|
const signingService = await SigningService2.createFromSecret(secret);
|
|
7760
8602
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
7761
|
-
const tokenType = new
|
|
8603
|
+
const tokenType = new TokenType5(tokenTypeBytes);
|
|
7762
8604
|
const predicateRef = UnmaskedPredicateReference3.create(
|
|
7763
8605
|
tokenType,
|
|
7764
8606
|
signingService.algorithm,
|
|
@@ -8034,6 +8876,14 @@ var Sphere = class _Sphere {
|
|
|
8034
8876
|
console.log("[Sphere.import] Registering nametag...");
|
|
8035
8877
|
await sphere.registerNametag(options.nametag);
|
|
8036
8878
|
}
|
|
8879
|
+
if (sphere._tokenStorageProviders.size > 0) {
|
|
8880
|
+
try {
|
|
8881
|
+
const syncResult = await sphere._payments.sync();
|
|
8882
|
+
console.log(`[Sphere.import] Auto-sync: +${syncResult.added} -${syncResult.removed}`);
|
|
8883
|
+
} catch (err) {
|
|
8884
|
+
console.warn("[Sphere.import] Auto-sync failed (non-fatal):", err);
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8037
8887
|
console.log("[Sphere.import] Import complete");
|
|
8038
8888
|
return sphere;
|
|
8039
8889
|
}
|
|
@@ -8904,9 +9754,9 @@ var Sphere = class _Sphere {
|
|
|
8904
9754
|
if (index < 0) {
|
|
8905
9755
|
throw new Error("Address index must be non-negative");
|
|
8906
9756
|
}
|
|
8907
|
-
const newNametag = options?.nametag
|
|
8908
|
-
if (newNametag && !
|
|
8909
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9757
|
+
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
9758
|
+
if (newNametag && !isValidNametag(newNametag)) {
|
|
9759
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
8910
9760
|
}
|
|
8911
9761
|
const addressInfo = this.deriveAddress(index, false);
|
|
8912
9762
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
@@ -9290,9 +10140,9 @@ var Sphere = class _Sphere {
|
|
|
9290
10140
|
*/
|
|
9291
10141
|
async registerNametag(nametag) {
|
|
9292
10142
|
this.ensureReady();
|
|
9293
|
-
const cleanNametag =
|
|
9294
|
-
if (!
|
|
9295
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
10143
|
+
const cleanNametag = this.cleanNametag(nametag);
|
|
10144
|
+
if (!isValidNametag(cleanNametag)) {
|
|
10145
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
9296
10146
|
}
|
|
9297
10147
|
if (this._identity?.nametag) {
|
|
9298
10148
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
@@ -9601,13 +10451,11 @@ var Sphere = class _Sphere {
|
|
|
9601
10451
|
}
|
|
9602
10452
|
}
|
|
9603
10453
|
/**
|
|
9604
|
-
*
|
|
10454
|
+
* Strip @ prefix and normalize a nametag (lowercase, phone E.164, strip @unicity suffix).
|
|
9605
10455
|
*/
|
|
9606
|
-
|
|
9607
|
-
const
|
|
9608
|
-
|
|
9609
|
-
);
|
|
9610
|
-
return pattern.test(nametag);
|
|
10456
|
+
cleanNametag(raw) {
|
|
10457
|
+
const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
10458
|
+
return normalizeNametag2(stripped);
|
|
9611
10459
|
}
|
|
9612
10460
|
// ===========================================================================
|
|
9613
10461
|
// Public Methods - Lifecycle
|
|
@@ -10307,6 +11155,14 @@ function createTokenValidator(options) {
|
|
|
10307
11155
|
return new TokenValidator(options);
|
|
10308
11156
|
}
|
|
10309
11157
|
|
|
11158
|
+
// index.ts
|
|
11159
|
+
import {
|
|
11160
|
+
normalizeNametag as normalizeNametag3,
|
|
11161
|
+
isPhoneNumber as isPhoneNumber2,
|
|
11162
|
+
hashNametag,
|
|
11163
|
+
areSameNametag
|
|
11164
|
+
} from "@unicitylabs/nostr-js-sdk";
|
|
11165
|
+
|
|
10310
11166
|
// price/CoinGeckoPriceProvider.ts
|
|
10311
11167
|
var CoinGeckoPriceProvider = class {
|
|
10312
11168
|
platform = "coingecko";
|
|
@@ -10442,6 +11298,7 @@ export {
|
|
|
10442
11298
|
TokenRegistry,
|
|
10443
11299
|
TokenValidator,
|
|
10444
11300
|
archivedKeyFromTokenId,
|
|
11301
|
+
areSameNametag,
|
|
10445
11302
|
base58Decode,
|
|
10446
11303
|
base58Encode2 as base58Encode,
|
|
10447
11304
|
buildTxfStorageData,
|
|
@@ -10489,6 +11346,7 @@ export {
|
|
|
10489
11346
|
hasUncommittedTransactions,
|
|
10490
11347
|
hasValidTxfData,
|
|
10491
11348
|
hash160,
|
|
11349
|
+
hashNametag,
|
|
10492
11350
|
hexToBytes,
|
|
10493
11351
|
identityFromMnemonicSync,
|
|
10494
11352
|
initSphere,
|
|
@@ -10500,10 +11358,12 @@ export {
|
|
|
10500
11358
|
isKnownToken,
|
|
10501
11359
|
isPaymentSessionTerminal,
|
|
10502
11360
|
isPaymentSessionTimedOut,
|
|
11361
|
+
isPhoneNumber2 as isPhoneNumber,
|
|
10503
11362
|
isSQLiteDatabase,
|
|
10504
11363
|
isTextWalletEncrypted,
|
|
10505
11364
|
isTokenKey,
|
|
10506
11365
|
isValidBech32,
|
|
11366
|
+
isValidNametag,
|
|
10507
11367
|
isValidPrivateKey,
|
|
10508
11368
|
isValidTokenId,
|
|
10509
11369
|
isWalletDatEncrypted,
|
|
@@ -10511,6 +11371,7 @@ export {
|
|
|
10511
11371
|
keyFromTokenId,
|
|
10512
11372
|
loadSphere,
|
|
10513
11373
|
mnemonicToSeedSync2 as mnemonicToSeedSync,
|
|
11374
|
+
normalizeNametag3 as normalizeNametag,
|
|
10514
11375
|
normalizeSdkTokenToStorage,
|
|
10515
11376
|
objectToTxf,
|
|
10516
11377
|
parseAndDecryptWalletDat,
|