@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/core/index.cjs
CHANGED
|
@@ -520,6 +520,7 @@ __export(core_exports, {
|
|
|
520
520
|
initSphere: () => initSphere,
|
|
521
521
|
isEncryptedData: () => isEncryptedData,
|
|
522
522
|
isValidBech32: () => isValidBech32,
|
|
523
|
+
isValidNametag: () => isValidNametag,
|
|
523
524
|
isValidPrivateKey: () => isValidPrivateKey,
|
|
524
525
|
loadSphere: () => loadSphere,
|
|
525
526
|
mnemonicToEntropy: () => mnemonicToEntropy2,
|
|
@@ -2210,6 +2211,7 @@ var import_MintCommitment = require("@unicitylabs/state-transition-sdk/lib/trans
|
|
|
2210
2211
|
var import_HashAlgorithm2 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
2211
2212
|
var import_UnmaskedPredicate2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
2212
2213
|
var import_InclusionProofUtils2 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
2214
|
+
var import_nostr_js_sdk = require("@unicitylabs/nostr-js-sdk");
|
|
2213
2215
|
var UNICITY_TOKEN_TYPE_HEX = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
2214
2216
|
var NametagMinter = class {
|
|
2215
2217
|
client;
|
|
@@ -2234,7 +2236,8 @@ var NametagMinter = class {
|
|
|
2234
2236
|
*/
|
|
2235
2237
|
async isNametagAvailable(nametag) {
|
|
2236
2238
|
try {
|
|
2237
|
-
const
|
|
2239
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2240
|
+
const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
|
|
2238
2241
|
const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
|
|
2239
2242
|
const isMinted = await this.client.isMinted(this.trustBase, nametagTokenId);
|
|
2240
2243
|
return !isMinted;
|
|
@@ -2251,7 +2254,8 @@ var NametagMinter = class {
|
|
|
2251
2254
|
* @returns MintNametagResult with token if successful
|
|
2252
2255
|
*/
|
|
2253
2256
|
async mintNametag(nametag, ownerAddress) {
|
|
2254
|
-
const
|
|
2257
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2258
|
+
const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
|
|
2255
2259
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2256
2260
|
try {
|
|
2257
2261
|
const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
|
|
@@ -2404,7 +2408,9 @@ var STORAGE_KEYS_ADDRESS = {
|
|
|
2404
2408
|
/** Messages for this address */
|
|
2405
2409
|
MESSAGES: "messages",
|
|
2406
2410
|
/** Transaction history for this address */
|
|
2407
|
-
TRANSACTION_HISTORY: "transaction_history"
|
|
2411
|
+
TRANSACTION_HISTORY: "transaction_history",
|
|
2412
|
+
/** Pending V5 finalization tokens (unconfirmed instant split tokens) */
|
|
2413
|
+
PENDING_V5_TOKENS: "pending_v5_tokens"
|
|
2408
2414
|
};
|
|
2409
2415
|
var STORAGE_KEYS = {
|
|
2410
2416
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -2423,16 +2429,6 @@ function getAddressId(directAddress) {
|
|
|
2423
2429
|
}
|
|
2424
2430
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
2425
2431
|
var DEFAULT_DERIVATION_PATH2 = `${DEFAULT_BASE_PATH}/0/0`;
|
|
2426
|
-
var LIMITS = {
|
|
2427
|
-
/** Min nametag length */
|
|
2428
|
-
NAMETAG_MIN_LENGTH: 3,
|
|
2429
|
-
/** Max nametag length */
|
|
2430
|
-
NAMETAG_MAX_LENGTH: 20,
|
|
2431
|
-
/** Max memo length */
|
|
2432
|
-
MEMO_MAX_LENGTH: 500,
|
|
2433
|
-
/** Max message length */
|
|
2434
|
-
MESSAGE_MAX_LENGTH: 1e4
|
|
2435
|
-
};
|
|
2436
2432
|
|
|
2437
2433
|
// types/txf.ts
|
|
2438
2434
|
var ARCHIVED_PREFIX = "archived-";
|
|
@@ -2725,6 +2721,18 @@ function parseTxfStorageData(data) {
|
|
|
2725
2721
|
result.validationErrors.push(`Forked token ${parsed.tokenId}: invalid structure`);
|
|
2726
2722
|
}
|
|
2727
2723
|
}
|
|
2724
|
+
} else if (key.startsWith("token-")) {
|
|
2725
|
+
try {
|
|
2726
|
+
const entry = storageData[key];
|
|
2727
|
+
const txfToken = entry?.token;
|
|
2728
|
+
if (txfToken?.genesis?.data?.tokenId) {
|
|
2729
|
+
const tokenId = txfToken.genesis.data.tokenId;
|
|
2730
|
+
const token = txfToToken(tokenId, txfToken);
|
|
2731
|
+
result.tokens.push(token);
|
|
2732
|
+
}
|
|
2733
|
+
} catch (err) {
|
|
2734
|
+
result.validationErrors.push(`Token ${key}: ${err}`);
|
|
2735
|
+
}
|
|
2728
2736
|
}
|
|
2729
2737
|
}
|
|
2730
2738
|
return result;
|
|
@@ -3225,8 +3233,9 @@ var InstantSplitExecutor = class {
|
|
|
3225
3233
|
const criticalPathDuration = performance.now() - startTime;
|
|
3226
3234
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3227
3235
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3236
|
+
let backgroundPromise;
|
|
3228
3237
|
if (!options?.skipBackground) {
|
|
3229
|
-
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3238
|
+
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3230
3239
|
signingService: this.signingService,
|
|
3231
3240
|
tokenType: tokenToSplit.type,
|
|
3232
3241
|
coinId,
|
|
@@ -3242,7 +3251,8 @@ var InstantSplitExecutor = class {
|
|
|
3242
3251
|
nostrEventId,
|
|
3243
3252
|
splitGroupId,
|
|
3244
3253
|
criticalPathDurationMs: criticalPathDuration,
|
|
3245
|
-
backgroundStarted: !options?.skipBackground
|
|
3254
|
+
backgroundStarted: !options?.skipBackground,
|
|
3255
|
+
backgroundPromise
|
|
3246
3256
|
};
|
|
3247
3257
|
} catch (error) {
|
|
3248
3258
|
const duration = performance.now() - startTime;
|
|
@@ -3304,7 +3314,7 @@ var InstantSplitExecutor = class {
|
|
|
3304
3314
|
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3305
3315
|
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3306
3316
|
]);
|
|
3307
|
-
submissions.then(async (results) => {
|
|
3317
|
+
return submissions.then(async (results) => {
|
|
3308
3318
|
const submitDuration = performance.now() - startTime;
|
|
3309
3319
|
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3310
3320
|
context.onProgress?.({
|
|
@@ -3769,6 +3779,11 @@ var import_AddressScheme = require("@unicitylabs/state-transition-sdk/lib/addres
|
|
|
3769
3779
|
var import_UnmaskedPredicate5 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
3770
3780
|
var import_TokenState5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
3771
3781
|
var import_HashAlgorithm5 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
3782
|
+
var import_TokenType3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
3783
|
+
var import_MintCommitment3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment");
|
|
3784
|
+
var import_MintTransactionData3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
|
|
3785
|
+
var import_InclusionProofUtils5 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
3786
|
+
var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof");
|
|
3772
3787
|
function enrichWithRegistry(info) {
|
|
3773
3788
|
const registry = TokenRegistry.getInstance();
|
|
3774
3789
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -3966,6 +3981,13 @@ function extractTokenStateKey(token) {
|
|
|
3966
3981
|
if (!tokenId || !stateHash) return null;
|
|
3967
3982
|
return createTokenStateKey(tokenId, stateHash);
|
|
3968
3983
|
}
|
|
3984
|
+
function fromHex4(hex) {
|
|
3985
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3986
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3987
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
3988
|
+
}
|
|
3989
|
+
return bytes;
|
|
3990
|
+
}
|
|
3969
3991
|
function hasSameGenesisTokenId(t1, t2) {
|
|
3970
3992
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
3971
3993
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
@@ -4055,6 +4077,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4055
4077
|
// Token State
|
|
4056
4078
|
tokens = /* @__PURE__ */ new Map();
|
|
4057
4079
|
pendingTransfers = /* @__PURE__ */ new Map();
|
|
4080
|
+
pendingBackgroundTasks = [];
|
|
4058
4081
|
// Repository State (tombstones, archives, forked, history)
|
|
4059
4082
|
tombstones = [];
|
|
4060
4083
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
@@ -4079,6 +4102,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4079
4102
|
// Poll every 2s
|
|
4080
4103
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4081
4104
|
// Max 30 attempts (~60s)
|
|
4105
|
+
// Storage event subscriptions (push-based sync)
|
|
4106
|
+
storageEventUnsubscribers = [];
|
|
4107
|
+
syncDebounceTimer = null;
|
|
4108
|
+
static SYNC_DEBOUNCE_MS = 500;
|
|
4109
|
+
/** Sync coalescing: concurrent sync() calls share the same operation */
|
|
4110
|
+
_syncInProgress = null;
|
|
4082
4111
|
constructor(config) {
|
|
4083
4112
|
this.moduleConfig = {
|
|
4084
4113
|
autoSync: config?.autoSync ?? true,
|
|
@@ -4089,7 +4118,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4089
4118
|
};
|
|
4090
4119
|
this.l1 = config?.l1 === null ? null : new L1PaymentsModule(config?.l1);
|
|
4091
4120
|
}
|
|
4092
|
-
/**
|
|
4121
|
+
/**
|
|
4122
|
+
* Get the current module configuration (excluding L1 config).
|
|
4123
|
+
*
|
|
4124
|
+
* @returns Resolved configuration with all defaults applied.
|
|
4125
|
+
*/
|
|
4093
4126
|
getConfig() {
|
|
4094
4127
|
return this.moduleConfig;
|
|
4095
4128
|
}
|
|
@@ -4130,9 +4163,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4130
4163
|
transport: deps.transport
|
|
4131
4164
|
});
|
|
4132
4165
|
}
|
|
4133
|
-
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4134
|
-
this.handleIncomingTransfer(transfer)
|
|
4135
|
-
|
|
4166
|
+
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4167
|
+
(transfer) => this.handleIncomingTransfer(transfer)
|
|
4168
|
+
);
|
|
4136
4169
|
if (deps.transport.onPaymentRequest) {
|
|
4137
4170
|
this.unsubscribePaymentRequests = deps.transport.onPaymentRequest((request) => {
|
|
4138
4171
|
this.handleIncomingPaymentRequest(request);
|
|
@@ -4143,9 +4176,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4143
4176
|
this.handlePaymentRequestResponse(response);
|
|
4144
4177
|
});
|
|
4145
4178
|
}
|
|
4179
|
+
this.subscribeToStorageEvents();
|
|
4146
4180
|
}
|
|
4147
4181
|
/**
|
|
4148
|
-
* Load
|
|
4182
|
+
* Load all token data from storage providers and restore wallet state.
|
|
4183
|
+
*
|
|
4184
|
+
* Loads tokens, nametag data, transaction history, and pending transfers
|
|
4185
|
+
* from configured storage providers. Restores pending V5 tokens and
|
|
4186
|
+
* triggers a fire-and-forget {@link resolveUnconfirmed} call.
|
|
4149
4187
|
*/
|
|
4150
4188
|
async load() {
|
|
4151
4189
|
this.ensureInitialized();
|
|
@@ -4162,6 +4200,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4162
4200
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4163
4201
|
}
|
|
4164
4202
|
}
|
|
4203
|
+
await this.loadPendingV5Tokens();
|
|
4165
4204
|
await this.loadTokensFromFileStorage();
|
|
4166
4205
|
await this.loadNametagFromFileStorage();
|
|
4167
4206
|
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
@@ -4179,9 +4218,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4179
4218
|
this.pendingTransfers.set(transfer.id, transfer);
|
|
4180
4219
|
}
|
|
4181
4220
|
}
|
|
4221
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4222
|
+
});
|
|
4182
4223
|
}
|
|
4183
4224
|
/**
|
|
4184
|
-
* Cleanup
|
|
4225
|
+
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
4226
|
+
*
|
|
4227
|
+
* Should be called when the wallet is being shut down or the module is
|
|
4228
|
+
* no longer needed. Also destroys the L1 sub-module if present.
|
|
4185
4229
|
*/
|
|
4186
4230
|
destroy() {
|
|
4187
4231
|
this.unsubscribeTransfers?.();
|
|
@@ -4199,6 +4243,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4199
4243
|
resolver.reject(new Error("Module destroyed"));
|
|
4200
4244
|
}
|
|
4201
4245
|
this.pendingResponseResolvers.clear();
|
|
4246
|
+
this.unsubscribeStorageEvents();
|
|
4202
4247
|
if (this.l1) {
|
|
4203
4248
|
this.l1.destroy();
|
|
4204
4249
|
}
|
|
@@ -4215,7 +4260,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4215
4260
|
const result = {
|
|
4216
4261
|
id: crypto.randomUUID(),
|
|
4217
4262
|
status: "pending",
|
|
4218
|
-
tokens: []
|
|
4263
|
+
tokens: [],
|
|
4264
|
+
tokenTransfers: []
|
|
4219
4265
|
};
|
|
4220
4266
|
try {
|
|
4221
4267
|
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
@@ -4252,69 +4298,147 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4252
4298
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4253
4299
|
result.status = "submitted";
|
|
4254
4300
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4301
|
+
const transferMode = request.transferMode ?? "instant";
|
|
4255
4302
|
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4303
|
+
if (transferMode === "conservative") {
|
|
4304
|
+
this.log("Executing conservative split...");
|
|
4305
|
+
const splitExecutor = new TokenSplitExecutor({
|
|
4306
|
+
stateTransitionClient: stClient,
|
|
4307
|
+
trustBase,
|
|
4308
|
+
signingService
|
|
4309
|
+
});
|
|
4310
|
+
const splitResult = await splitExecutor.executeSplit(
|
|
4311
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4312
|
+
splitPlan.splitAmount,
|
|
4313
|
+
splitPlan.remainderAmount,
|
|
4314
|
+
splitPlan.coinId,
|
|
4315
|
+
recipientAddress
|
|
4316
|
+
);
|
|
4317
|
+
const changeTokenData = splitResult.tokenForSender.toJSON();
|
|
4318
|
+
const changeUiToken = {
|
|
4319
|
+
id: crypto.randomUUID(),
|
|
4320
|
+
coinId: request.coinId,
|
|
4321
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4322
|
+
name: this.getCoinName(request.coinId),
|
|
4323
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4324
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4325
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4326
|
+
status: "confirmed",
|
|
4327
|
+
createdAt: Date.now(),
|
|
4328
|
+
updatedAt: Date.now(),
|
|
4329
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4330
|
+
};
|
|
4331
|
+
await this.addToken(changeUiToken, true);
|
|
4332
|
+
this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
|
|
4333
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4334
|
+
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
4335
|
+
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
4336
|
+
memo: request.memo
|
|
4337
|
+
});
|
|
4338
|
+
const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
|
|
4339
|
+
const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
|
|
4340
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
|
|
4341
|
+
result.tokenTransfers.push({
|
|
4342
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4343
|
+
method: "split",
|
|
4344
|
+
requestIdHex: splitRequestIdHex
|
|
4345
|
+
});
|
|
4346
|
+
this.log(`Conservative split transfer completed`);
|
|
4347
|
+
} else {
|
|
4348
|
+
this.log("Executing instant split...");
|
|
4349
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4350
|
+
const executor = new InstantSplitExecutor({
|
|
4351
|
+
stateTransitionClient: stClient,
|
|
4352
|
+
trustBase,
|
|
4353
|
+
signingService,
|
|
4354
|
+
devMode
|
|
4355
|
+
});
|
|
4356
|
+
const instantResult = await executor.executeSplitInstant(
|
|
4357
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4358
|
+
splitPlan.splitAmount,
|
|
4359
|
+
splitPlan.remainderAmount,
|
|
4360
|
+
splitPlan.coinId,
|
|
4361
|
+
recipientAddress,
|
|
4362
|
+
this.deps.transport,
|
|
4363
|
+
recipientPubkey,
|
|
4364
|
+
{
|
|
4365
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4366
|
+
const changeTokenData = changeToken.toJSON();
|
|
4367
|
+
const uiToken = {
|
|
4368
|
+
id: crypto.randomUUID(),
|
|
4369
|
+
coinId: request.coinId,
|
|
4370
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4371
|
+
name: this.getCoinName(request.coinId),
|
|
4372
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4373
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4374
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4375
|
+
status: "confirmed",
|
|
4376
|
+
createdAt: Date.now(),
|
|
4377
|
+
updatedAt: Date.now(),
|
|
4378
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4379
|
+
};
|
|
4380
|
+
await this.addToken(uiToken, true);
|
|
4381
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4382
|
+
},
|
|
4383
|
+
onStorageSync: async () => {
|
|
4384
|
+
await this.save();
|
|
4385
|
+
return true;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
);
|
|
4389
|
+
if (!instantResult.success) {
|
|
4390
|
+
throw new Error(instantResult.error || "Instant split failed");
|
|
4391
|
+
}
|
|
4392
|
+
if (instantResult.backgroundPromise) {
|
|
4393
|
+
this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
|
|
4394
|
+
}
|
|
4395
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
4396
|
+
result.tokenTransfers.push({
|
|
4397
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4398
|
+
method: "split",
|
|
4399
|
+
splitGroupId: instantResult.splitGroupId,
|
|
4400
|
+
nostrEventId: instantResult.nostrEventId
|
|
4401
|
+
});
|
|
4402
|
+
this.log(`Instant split transfer completed`);
|
|
4403
|
+
}
|
|
4295
4404
|
}
|
|
4296
4405
|
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4297
4406
|
const token = tokenWithAmount.uiToken;
|
|
4298
4407
|
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4408
|
+
if (transferMode === "conservative") {
|
|
4409
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4410
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4411
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4412
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4413
|
+
}
|
|
4414
|
+
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
4415
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4416
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4417
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4418
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4419
|
+
memo: request.memo
|
|
4420
|
+
});
|
|
4421
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4422
|
+
} else {
|
|
4423
|
+
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4424
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4425
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4426
|
+
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4427
|
+
memo: request.memo
|
|
4428
|
+
});
|
|
4429
|
+
console.log(`[Payments] NOSTR-FIRST: Direct token sent successfully`);
|
|
4430
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4431
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4432
|
+
);
|
|
4305
4433
|
}
|
|
4306
|
-
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
4307
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4308
4434
|
const requestIdBytes = commitment.requestId;
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
memo: request.memo
|
|
4435
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4436
|
+
result.tokenTransfers.push({
|
|
4437
|
+
sourceTokenId: token.id,
|
|
4438
|
+
method: "direct",
|
|
4439
|
+
requestIdHex
|
|
4315
4440
|
});
|
|
4316
|
-
|
|
4317
|
-
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
4441
|
+
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4318
4442
|
await this.removeToken(token.id, recipientNametag, true);
|
|
4319
4443
|
}
|
|
4320
4444
|
result.status = "delivered";
|
|
@@ -4327,7 +4451,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4327
4451
|
coinId: request.coinId,
|
|
4328
4452
|
symbol: this.getCoinSymbol(request.coinId),
|
|
4329
4453
|
timestamp: Date.now(),
|
|
4330
|
-
recipientNametag
|
|
4454
|
+
recipientNametag,
|
|
4455
|
+
transferId: result.id
|
|
4331
4456
|
});
|
|
4332
4457
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4333
4458
|
return result;
|
|
@@ -4463,6 +4588,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4463
4588
|
}
|
|
4464
4589
|
);
|
|
4465
4590
|
if (result.success) {
|
|
4591
|
+
if (result.backgroundPromise) {
|
|
4592
|
+
this.pendingBackgroundTasks.push(result.backgroundPromise);
|
|
4593
|
+
}
|
|
4466
4594
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4467
4595
|
await this.removeToken(tokenToSplit.id, recipientNametag, true);
|
|
4468
4596
|
await this.addToHistory({
|
|
@@ -4504,6 +4632,63 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4504
4632
|
*/
|
|
4505
4633
|
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4506
4634
|
this.ensureInitialized();
|
|
4635
|
+
if (!isInstantSplitBundleV5(bundle)) {
|
|
4636
|
+
return this.processInstantSplitBundleSync(bundle, senderPubkey);
|
|
4637
|
+
}
|
|
4638
|
+
try {
|
|
4639
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
4640
|
+
if (this.tokens.has(deterministicId)) {
|
|
4641
|
+
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
4642
|
+
return { success: true, durationMs: 0 };
|
|
4643
|
+
}
|
|
4644
|
+
const registry = TokenRegistry.getInstance();
|
|
4645
|
+
const pendingData = {
|
|
4646
|
+
type: "v5_bundle",
|
|
4647
|
+
stage: "RECEIVED",
|
|
4648
|
+
bundleJson: JSON.stringify(bundle),
|
|
4649
|
+
senderPubkey,
|
|
4650
|
+
savedAt: Date.now(),
|
|
4651
|
+
attemptCount: 0
|
|
4652
|
+
};
|
|
4653
|
+
const uiToken = {
|
|
4654
|
+
id: deterministicId,
|
|
4655
|
+
coinId: bundle.coinId,
|
|
4656
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4657
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4658
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4659
|
+
amount: bundle.amount,
|
|
4660
|
+
status: "submitted",
|
|
4661
|
+
// UNCONFIRMED
|
|
4662
|
+
createdAt: Date.now(),
|
|
4663
|
+
updatedAt: Date.now(),
|
|
4664
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4665
|
+
};
|
|
4666
|
+
await this.addToken(uiToken, false);
|
|
4667
|
+
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
4668
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
4669
|
+
id: bundle.splitGroupId,
|
|
4670
|
+
senderPubkey,
|
|
4671
|
+
tokens: [uiToken],
|
|
4672
|
+
receivedAt: Date.now()
|
|
4673
|
+
});
|
|
4674
|
+
await this.save();
|
|
4675
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4676
|
+
});
|
|
4677
|
+
return { success: true, durationMs: 0 };
|
|
4678
|
+
} catch (error) {
|
|
4679
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4680
|
+
return {
|
|
4681
|
+
success: false,
|
|
4682
|
+
error: errorMessage,
|
|
4683
|
+
durationMs: 0
|
|
4684
|
+
};
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
/**
|
|
4688
|
+
* Synchronous V4 bundle processing (dev mode only).
|
|
4689
|
+
* Kept for backward compatibility with V4 bundles.
|
|
4690
|
+
*/
|
|
4691
|
+
async processInstantSplitBundleSync(bundle, senderPubkey) {
|
|
4507
4692
|
try {
|
|
4508
4693
|
const signingService = await this.createSigningService();
|
|
4509
4694
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
@@ -4589,7 +4774,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4589
4774
|
}
|
|
4590
4775
|
}
|
|
4591
4776
|
/**
|
|
4592
|
-
*
|
|
4777
|
+
* Type-guard: check whether a payload is a valid {@link InstantSplitBundle} (V4 or V5).
|
|
4778
|
+
*
|
|
4779
|
+
* @param payload - The object to test.
|
|
4780
|
+
* @returns `true` if the payload matches the InstantSplitBundle shape.
|
|
4593
4781
|
*/
|
|
4594
4782
|
isInstantSplitBundle(payload) {
|
|
4595
4783
|
return isInstantSplitBundle(payload);
|
|
@@ -4670,39 +4858,57 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4670
4858
|
return [...this.paymentRequests];
|
|
4671
4859
|
}
|
|
4672
4860
|
/**
|
|
4673
|
-
* Get
|
|
4861
|
+
* Get the count of payment requests with status `'pending'`.
|
|
4862
|
+
*
|
|
4863
|
+
* @returns Number of pending incoming payment requests.
|
|
4674
4864
|
*/
|
|
4675
4865
|
getPendingPaymentRequestsCount() {
|
|
4676
4866
|
return this.paymentRequests.filter((r) => r.status === "pending").length;
|
|
4677
4867
|
}
|
|
4678
4868
|
/**
|
|
4679
|
-
* Accept a payment request
|
|
4869
|
+
* Accept a payment request and notify the requester.
|
|
4870
|
+
*
|
|
4871
|
+
* Marks the request as `'accepted'` and sends a response via transport.
|
|
4872
|
+
* The caller should subsequently call {@link send} to fulfill the payment.
|
|
4873
|
+
*
|
|
4874
|
+
* @param requestId - ID of the incoming payment request to accept.
|
|
4680
4875
|
*/
|
|
4681
4876
|
async acceptPaymentRequest(requestId2) {
|
|
4682
4877
|
this.updatePaymentRequestStatus(requestId2, "accepted");
|
|
4683
4878
|
await this.sendPaymentRequestResponse(requestId2, "accepted");
|
|
4684
4879
|
}
|
|
4685
4880
|
/**
|
|
4686
|
-
* Reject a payment request
|
|
4881
|
+
* Reject a payment request and notify the requester.
|
|
4882
|
+
*
|
|
4883
|
+
* @param requestId - ID of the incoming payment request to reject.
|
|
4687
4884
|
*/
|
|
4688
4885
|
async rejectPaymentRequest(requestId2) {
|
|
4689
4886
|
this.updatePaymentRequestStatus(requestId2, "rejected");
|
|
4690
4887
|
await this.sendPaymentRequestResponse(requestId2, "rejected");
|
|
4691
4888
|
}
|
|
4692
4889
|
/**
|
|
4693
|
-
* Mark a payment request as paid (
|
|
4890
|
+
* Mark a payment request as paid (local status update only).
|
|
4891
|
+
*
|
|
4892
|
+
* Typically called after a successful {@link send} to record that the
|
|
4893
|
+
* request has been fulfilled.
|
|
4894
|
+
*
|
|
4895
|
+
* @param requestId - ID of the incoming payment request to mark as paid.
|
|
4694
4896
|
*/
|
|
4695
4897
|
markPaymentRequestPaid(requestId2) {
|
|
4696
4898
|
this.updatePaymentRequestStatus(requestId2, "paid");
|
|
4697
4899
|
}
|
|
4698
4900
|
/**
|
|
4699
|
-
*
|
|
4901
|
+
* Remove all non-pending incoming payment requests from memory.
|
|
4902
|
+
*
|
|
4903
|
+
* Keeps only requests with status `'pending'`.
|
|
4700
4904
|
*/
|
|
4701
4905
|
clearProcessedPaymentRequests() {
|
|
4702
4906
|
this.paymentRequests = this.paymentRequests.filter((r) => r.status === "pending");
|
|
4703
4907
|
}
|
|
4704
4908
|
/**
|
|
4705
|
-
* Remove a specific payment request
|
|
4909
|
+
* Remove a specific incoming payment request by ID.
|
|
4910
|
+
*
|
|
4911
|
+
* @param requestId - ID of the payment request to remove.
|
|
4706
4912
|
*/
|
|
4707
4913
|
removePaymentRequest(requestId2) {
|
|
4708
4914
|
this.paymentRequests = this.paymentRequests.filter((r) => r.id !== requestId2);
|
|
@@ -4827,7 +5033,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4827
5033
|
});
|
|
4828
5034
|
}
|
|
4829
5035
|
/**
|
|
4830
|
-
* Cancel
|
|
5036
|
+
* Cancel an active {@link waitForPaymentResponse} call.
|
|
5037
|
+
*
|
|
5038
|
+
* The pending promise is rejected with a `'Cancelled'` error.
|
|
5039
|
+
*
|
|
5040
|
+
* @param requestId - The outgoing request ID whose wait should be cancelled.
|
|
4831
5041
|
*/
|
|
4832
5042
|
cancelWaitForPaymentResponse(requestId2) {
|
|
4833
5043
|
const resolver = this.pendingResponseResolvers.get(requestId2);
|
|
@@ -4838,14 +5048,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4838
5048
|
}
|
|
4839
5049
|
}
|
|
4840
5050
|
/**
|
|
4841
|
-
* Remove an outgoing payment request
|
|
5051
|
+
* Remove an outgoing payment request and cancel any pending wait.
|
|
5052
|
+
*
|
|
5053
|
+
* @param requestId - ID of the outgoing request to remove.
|
|
4842
5054
|
*/
|
|
4843
5055
|
removeOutgoingPaymentRequest(requestId2) {
|
|
4844
5056
|
this.outgoingPaymentRequests.delete(requestId2);
|
|
4845
5057
|
this.cancelWaitForPaymentResponse(requestId2);
|
|
4846
5058
|
}
|
|
4847
5059
|
/**
|
|
4848
|
-
*
|
|
5060
|
+
* Remove all outgoing payment requests that are `'paid'`, `'rejected'`, or `'expired'`.
|
|
4849
5061
|
*/
|
|
4850
5062
|
clearCompletedOutgoingPaymentRequests() {
|
|
4851
5063
|
for (const [id, request] of this.outgoingPaymentRequests) {
|
|
@@ -4917,6 +5129,71 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4917
5129
|
}
|
|
4918
5130
|
}
|
|
4919
5131
|
// ===========================================================================
|
|
5132
|
+
// Public API - Receive
|
|
5133
|
+
// ===========================================================================
|
|
5134
|
+
/**
|
|
5135
|
+
* Fetch and process pending incoming transfers from the transport layer.
|
|
5136
|
+
*
|
|
5137
|
+
* Performs a one-shot query to fetch all pending events, processes them
|
|
5138
|
+
* through the existing pipeline, and resolves after all stored events
|
|
5139
|
+
* are handled. Useful for batch/CLI apps that need explicit receive.
|
|
5140
|
+
*
|
|
5141
|
+
* When `finalize` is true, polls resolveUnconfirmed() + load() until all
|
|
5142
|
+
* tokens are confirmed or the timeout expires. Otherwise calls
|
|
5143
|
+
* resolveUnconfirmed() once to submit pending commitments.
|
|
5144
|
+
*
|
|
5145
|
+
* @param options - Optional receive options including finalization control
|
|
5146
|
+
* @param callback - Optional callback invoked for each newly received transfer
|
|
5147
|
+
* @returns ReceiveResult with transfers and finalization metadata
|
|
5148
|
+
*/
|
|
5149
|
+
async receive(options, callback) {
|
|
5150
|
+
this.ensureInitialized();
|
|
5151
|
+
if (!this.deps.transport.fetchPendingEvents) {
|
|
5152
|
+
throw new Error("Transport provider does not support fetchPendingEvents");
|
|
5153
|
+
}
|
|
5154
|
+
const opts = options ?? {};
|
|
5155
|
+
const tokensBefore = new Set(this.tokens.keys());
|
|
5156
|
+
await this.deps.transport.fetchPendingEvents();
|
|
5157
|
+
await this.load();
|
|
5158
|
+
const received = [];
|
|
5159
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5160
|
+
if (!tokensBefore.has(tokenId)) {
|
|
5161
|
+
const transfer = {
|
|
5162
|
+
id: tokenId,
|
|
5163
|
+
senderPubkey: "",
|
|
5164
|
+
tokens: [token],
|
|
5165
|
+
receivedAt: Date.now()
|
|
5166
|
+
};
|
|
5167
|
+
received.push(transfer);
|
|
5168
|
+
if (callback) callback(transfer);
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
const result = { transfers: received };
|
|
5172
|
+
if (opts.finalize) {
|
|
5173
|
+
const timeout = opts.timeout ?? 6e4;
|
|
5174
|
+
const pollInterval = opts.pollInterval ?? 2e3;
|
|
5175
|
+
const startTime = Date.now();
|
|
5176
|
+
while (Date.now() - startTime < timeout) {
|
|
5177
|
+
const resolution = await this.resolveUnconfirmed();
|
|
5178
|
+
result.finalization = resolution;
|
|
5179
|
+
if (opts.onProgress) opts.onProgress(resolution);
|
|
5180
|
+
const stillUnconfirmed = Array.from(this.tokens.values()).some(
|
|
5181
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5182
|
+
);
|
|
5183
|
+
if (!stillUnconfirmed) break;
|
|
5184
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
5185
|
+
await this.load();
|
|
5186
|
+
}
|
|
5187
|
+
result.finalizationDurationMs = Date.now() - startTime;
|
|
5188
|
+
result.timedOut = Array.from(this.tokens.values()).some(
|
|
5189
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5190
|
+
);
|
|
5191
|
+
} else {
|
|
5192
|
+
result.finalization = await this.resolveUnconfirmed();
|
|
5193
|
+
}
|
|
5194
|
+
return result;
|
|
5195
|
+
}
|
|
5196
|
+
// ===========================================================================
|
|
4920
5197
|
// Public API - Balance & Tokens
|
|
4921
5198
|
// ===========================================================================
|
|
4922
5199
|
/**
|
|
@@ -4926,10 +5203,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4926
5203
|
this.priceProvider = provider;
|
|
4927
5204
|
}
|
|
4928
5205
|
/**
|
|
4929
|
-
*
|
|
4930
|
-
*
|
|
5206
|
+
* Wait for all pending background operations (e.g., instant split change token creation).
|
|
5207
|
+
* Call this before process exit to ensure all tokens are saved.
|
|
4931
5208
|
*/
|
|
4932
|
-
async
|
|
5209
|
+
async waitForPendingOperations() {
|
|
5210
|
+
if (this.pendingBackgroundTasks.length > 0) {
|
|
5211
|
+
await Promise.allSettled(this.pendingBackgroundTasks);
|
|
5212
|
+
this.pendingBackgroundTasks = [];
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
/**
|
|
5216
|
+
* Get total portfolio value in USD.
|
|
5217
|
+
* Returns null if PriceProvider is not configured.
|
|
5218
|
+
*/
|
|
5219
|
+
async getFiatBalance() {
|
|
4933
5220
|
const assets = await this.getAssets();
|
|
4934
5221
|
if (!this.priceProvider) {
|
|
4935
5222
|
return null;
|
|
@@ -4945,19 +5232,95 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4945
5232
|
return hasAnyPrice ? total : null;
|
|
4946
5233
|
}
|
|
4947
5234
|
/**
|
|
4948
|
-
* Get
|
|
4949
|
-
*
|
|
5235
|
+
* Get token balances grouped by coin type.
|
|
5236
|
+
*
|
|
5237
|
+
* Returns an array of {@link Asset} objects, one per coin type held.
|
|
5238
|
+
* Each entry includes confirmed and unconfirmed breakdowns. Tokens with
|
|
5239
|
+
* status `'spent'`, `'invalid'`, or `'transferring'` are excluded.
|
|
5240
|
+
*
|
|
5241
|
+
* This is synchronous — no price data is included. Use {@link getAssets}
|
|
5242
|
+
* for the async version with fiat pricing.
|
|
5243
|
+
*
|
|
5244
|
+
* @param coinId - Optional coin ID to filter by (e.g. hex string). When omitted, all coin types are returned.
|
|
5245
|
+
* @returns Array of balance summaries (synchronous — no await needed).
|
|
5246
|
+
*/
|
|
5247
|
+
getBalance(coinId) {
|
|
5248
|
+
return this.aggregateTokens(coinId);
|
|
5249
|
+
}
|
|
5250
|
+
/**
|
|
5251
|
+
* Get aggregated assets (tokens grouped by coinId) with price data.
|
|
5252
|
+
* Includes both confirmed and unconfirmed tokens with breakdown.
|
|
4950
5253
|
*/
|
|
4951
5254
|
async getAssets(coinId) {
|
|
5255
|
+
const rawAssets = this.aggregateTokens(coinId);
|
|
5256
|
+
if (!this.priceProvider || rawAssets.length === 0) {
|
|
5257
|
+
return rawAssets;
|
|
5258
|
+
}
|
|
5259
|
+
try {
|
|
5260
|
+
const registry = TokenRegistry.getInstance();
|
|
5261
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5262
|
+
for (const asset of rawAssets) {
|
|
5263
|
+
const def = registry.getDefinition(asset.coinId);
|
|
5264
|
+
if (def?.name) {
|
|
5265
|
+
const existing = nameToCoins.get(def.name);
|
|
5266
|
+
if (existing) {
|
|
5267
|
+
existing.push(asset.coinId);
|
|
5268
|
+
} else {
|
|
5269
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
if (nameToCoins.size > 0) {
|
|
5274
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
5275
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5276
|
+
return rawAssets.map((raw) => {
|
|
5277
|
+
const def = registry.getDefinition(raw.coinId);
|
|
5278
|
+
const price = def?.name ? prices.get(def.name) : void 0;
|
|
5279
|
+
let fiatValueUsd = null;
|
|
5280
|
+
let fiatValueEur = null;
|
|
5281
|
+
if (price) {
|
|
5282
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5283
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5284
|
+
if (price.priceEur != null) {
|
|
5285
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
return {
|
|
5289
|
+
...raw,
|
|
5290
|
+
priceUsd: price?.priceUsd ?? null,
|
|
5291
|
+
priceEur: price?.priceEur ?? null,
|
|
5292
|
+
change24h: price?.change24h ?? null,
|
|
5293
|
+
fiatValueUsd,
|
|
5294
|
+
fiatValueEur
|
|
5295
|
+
};
|
|
5296
|
+
});
|
|
5297
|
+
}
|
|
5298
|
+
} catch (error) {
|
|
5299
|
+
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5300
|
+
}
|
|
5301
|
+
return rawAssets;
|
|
5302
|
+
}
|
|
5303
|
+
/**
|
|
5304
|
+
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5305
|
+
* Excludes tokens with status 'spent', 'invalid', or 'transferring'.
|
|
5306
|
+
*/
|
|
5307
|
+
aggregateTokens(coinId) {
|
|
4952
5308
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
4953
5309
|
for (const token of this.tokens.values()) {
|
|
4954
|
-
if (token.status
|
|
5310
|
+
if (token.status === "spent" || token.status === "invalid" || token.status === "transferring") continue;
|
|
4955
5311
|
if (coinId && token.coinId !== coinId) continue;
|
|
4956
5312
|
const key = token.coinId;
|
|
5313
|
+
const amount = BigInt(token.amount);
|
|
5314
|
+
const isConfirmed = token.status === "confirmed";
|
|
4957
5315
|
const existing = assetsMap.get(key);
|
|
4958
5316
|
if (existing) {
|
|
4959
|
-
|
|
4960
|
-
|
|
5317
|
+
if (isConfirmed) {
|
|
5318
|
+
existing.confirmedAmount += amount;
|
|
5319
|
+
existing.confirmedTokenCount++;
|
|
5320
|
+
} else {
|
|
5321
|
+
existing.unconfirmedAmount += amount;
|
|
5322
|
+
existing.unconfirmedTokenCount++;
|
|
5323
|
+
}
|
|
4961
5324
|
} else {
|
|
4962
5325
|
assetsMap.set(key, {
|
|
4963
5326
|
coinId: token.coinId,
|
|
@@ -4965,78 +5328,42 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4965
5328
|
name: token.name,
|
|
4966
5329
|
decimals: token.decimals,
|
|
4967
5330
|
iconUrl: token.iconUrl,
|
|
4968
|
-
|
|
4969
|
-
|
|
5331
|
+
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5332
|
+
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5333
|
+
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5334
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
4970
5335
|
});
|
|
4971
5336
|
}
|
|
4972
5337
|
}
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
if (this.priceProvider && rawAssets.length > 0) {
|
|
4976
|
-
try {
|
|
4977
|
-
const registry = TokenRegistry.getInstance();
|
|
4978
|
-
const nameToCoins = /* @__PURE__ */ new Map();
|
|
4979
|
-
for (const asset of rawAssets) {
|
|
4980
|
-
const def = registry.getDefinition(asset.coinId);
|
|
4981
|
-
if (def?.name) {
|
|
4982
|
-
const existing = nameToCoins.get(def.name);
|
|
4983
|
-
if (existing) {
|
|
4984
|
-
existing.push(asset.coinId);
|
|
4985
|
-
} else {
|
|
4986
|
-
nameToCoins.set(def.name, [asset.coinId]);
|
|
4987
|
-
}
|
|
4988
|
-
}
|
|
4989
|
-
}
|
|
4990
|
-
if (nameToCoins.size > 0) {
|
|
4991
|
-
const tokenNames = Array.from(nameToCoins.keys());
|
|
4992
|
-
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
4993
|
-
priceMap = /* @__PURE__ */ new Map();
|
|
4994
|
-
for (const [name, coinIds] of nameToCoins) {
|
|
4995
|
-
const price = prices.get(name);
|
|
4996
|
-
if (price) {
|
|
4997
|
-
for (const cid of coinIds) {
|
|
4998
|
-
priceMap.set(cid, {
|
|
4999
|
-
priceUsd: price.priceUsd,
|
|
5000
|
-
priceEur: price.priceEur,
|
|
5001
|
-
change24h: price.change24h
|
|
5002
|
-
});
|
|
5003
|
-
}
|
|
5004
|
-
}
|
|
5005
|
-
}
|
|
5006
|
-
}
|
|
5007
|
-
} catch (error) {
|
|
5008
|
-
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5009
|
-
}
|
|
5010
|
-
}
|
|
5011
|
-
return rawAssets.map((raw) => {
|
|
5012
|
-
const price = priceMap?.get(raw.coinId);
|
|
5013
|
-
let fiatValueUsd = null;
|
|
5014
|
-
let fiatValueEur = null;
|
|
5015
|
-
if (price) {
|
|
5016
|
-
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5017
|
-
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5018
|
-
if (price.priceEur != null) {
|
|
5019
|
-
fiatValueEur = humanAmount * price.priceEur;
|
|
5020
|
-
}
|
|
5021
|
-
}
|
|
5338
|
+
return Array.from(assetsMap.values()).map((raw) => {
|
|
5339
|
+
const totalAmount = (raw.confirmedAmount + raw.unconfirmedAmount).toString();
|
|
5022
5340
|
return {
|
|
5023
5341
|
coinId: raw.coinId,
|
|
5024
5342
|
symbol: raw.symbol,
|
|
5025
5343
|
name: raw.name,
|
|
5026
5344
|
decimals: raw.decimals,
|
|
5027
5345
|
iconUrl: raw.iconUrl,
|
|
5028
|
-
totalAmount
|
|
5029
|
-
tokenCount: raw.
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5346
|
+
totalAmount,
|
|
5347
|
+
tokenCount: raw.confirmedTokenCount + raw.unconfirmedTokenCount,
|
|
5348
|
+
confirmedAmount: raw.confirmedAmount.toString(),
|
|
5349
|
+
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5350
|
+
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5351
|
+
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
5352
|
+
priceUsd: null,
|
|
5353
|
+
priceEur: null,
|
|
5354
|
+
change24h: null,
|
|
5355
|
+
fiatValueUsd: null,
|
|
5356
|
+
fiatValueEur: null
|
|
5035
5357
|
};
|
|
5036
5358
|
});
|
|
5037
5359
|
}
|
|
5038
5360
|
/**
|
|
5039
|
-
* Get all tokens
|
|
5361
|
+
* Get all tokens, optionally filtered by coin type and/or status.
|
|
5362
|
+
*
|
|
5363
|
+
* @param filter - Optional filter criteria.
|
|
5364
|
+
* @param filter.coinId - Return only tokens of this coin type.
|
|
5365
|
+
* @param filter.status - Return only tokens with this status (e.g. `'submitted'` for unconfirmed).
|
|
5366
|
+
* @returns Array of matching {@link Token} objects (synchronous).
|
|
5040
5367
|
*/
|
|
5041
5368
|
getTokens(filter) {
|
|
5042
5369
|
let tokens = Array.from(this.tokens.values());
|
|
@@ -5049,19 +5376,327 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5049
5376
|
return tokens;
|
|
5050
5377
|
}
|
|
5051
5378
|
/**
|
|
5052
|
-
* Get single token
|
|
5379
|
+
* Get a single token by its local ID.
|
|
5380
|
+
*
|
|
5381
|
+
* @param id - The local UUID assigned when the token was added.
|
|
5382
|
+
* @returns The token, or `undefined` if not found.
|
|
5053
5383
|
*/
|
|
5054
5384
|
getToken(id) {
|
|
5055
5385
|
return this.tokens.get(id);
|
|
5056
5386
|
}
|
|
5057
5387
|
// ===========================================================================
|
|
5388
|
+
// Public API - Unconfirmed Token Resolution
|
|
5389
|
+
// ===========================================================================
|
|
5390
|
+
/**
|
|
5391
|
+
* Attempt to resolve unconfirmed (status `'submitted'`) tokens by acquiring
|
|
5392
|
+
* their missing aggregator proofs.
|
|
5393
|
+
*
|
|
5394
|
+
* Each unconfirmed V5 token progresses through stages:
|
|
5395
|
+
* `RECEIVED` → `MINT_SUBMITTED` → `MINT_PROVEN` → `TRANSFER_SUBMITTED` → `FINALIZED`
|
|
5396
|
+
*
|
|
5397
|
+
* Uses 500 ms quick-timeouts per proof check so the call returns quickly even
|
|
5398
|
+
* when proofs are not yet available. Tokens that exceed 50 failed attempts are
|
|
5399
|
+
* marked `'invalid'`.
|
|
5400
|
+
*
|
|
5401
|
+
* Automatically called (fire-and-forget) by {@link load}.
|
|
5402
|
+
*
|
|
5403
|
+
* @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
|
|
5404
|
+
*/
|
|
5405
|
+
async resolveUnconfirmed() {
|
|
5406
|
+
this.ensureInitialized();
|
|
5407
|
+
const result = {
|
|
5408
|
+
resolved: 0,
|
|
5409
|
+
stillPending: 0,
|
|
5410
|
+
failed: 0,
|
|
5411
|
+
details: []
|
|
5412
|
+
};
|
|
5413
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5414
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5415
|
+
if (!stClient || !trustBase) return result;
|
|
5416
|
+
const signingService = await this.createSigningService();
|
|
5417
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5418
|
+
if (token.status !== "submitted") continue;
|
|
5419
|
+
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5420
|
+
if (!pending2) {
|
|
5421
|
+
result.stillPending++;
|
|
5422
|
+
continue;
|
|
5423
|
+
}
|
|
5424
|
+
if (pending2.type === "v5_bundle") {
|
|
5425
|
+
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
5426
|
+
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5427
|
+
if (progress === "resolved") result.resolved++;
|
|
5428
|
+
else if (progress === "failed") result.failed++;
|
|
5429
|
+
else result.stillPending++;
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
if (result.resolved > 0 || result.failed > 0) {
|
|
5433
|
+
await this.save();
|
|
5434
|
+
}
|
|
5435
|
+
return result;
|
|
5436
|
+
}
|
|
5437
|
+
// ===========================================================================
|
|
5438
|
+
// Private - V5 Lazy Resolution Helpers
|
|
5439
|
+
// ===========================================================================
|
|
5440
|
+
/**
|
|
5441
|
+
* Process a single V5 token through its finalization stages with quick-timeout proof checks.
|
|
5442
|
+
*/
|
|
5443
|
+
async resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService) {
|
|
5444
|
+
const bundle = JSON.parse(pending2.bundleJson);
|
|
5445
|
+
pending2.attemptCount++;
|
|
5446
|
+
pending2.lastAttemptAt = Date.now();
|
|
5447
|
+
try {
|
|
5448
|
+
if (pending2.stage === "RECEIVED") {
|
|
5449
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5450
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5451
|
+
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5452
|
+
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
5453
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5454
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5455
|
+
}
|
|
5456
|
+
pending2.stage = "MINT_SUBMITTED";
|
|
5457
|
+
this.updatePendingFinalization(token, pending2);
|
|
5458
|
+
}
|
|
5459
|
+
if (pending2.stage === "MINT_SUBMITTED") {
|
|
5460
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5461
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5462
|
+
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5463
|
+
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5464
|
+
if (!proof) {
|
|
5465
|
+
this.updatePendingFinalization(token, pending2);
|
|
5466
|
+
return "pending";
|
|
5467
|
+
}
|
|
5468
|
+
pending2.mintProofJson = JSON.stringify(proof);
|
|
5469
|
+
pending2.stage = "MINT_PROVEN";
|
|
5470
|
+
this.updatePendingFinalization(token, pending2);
|
|
5471
|
+
}
|
|
5472
|
+
if (pending2.stage === "MINT_PROVEN") {
|
|
5473
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5474
|
+
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5475
|
+
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
5476
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5477
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5478
|
+
}
|
|
5479
|
+
pending2.stage = "TRANSFER_SUBMITTED";
|
|
5480
|
+
this.updatePendingFinalization(token, pending2);
|
|
5481
|
+
}
|
|
5482
|
+
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
5483
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5484
|
+
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5485
|
+
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5486
|
+
if (!proof) {
|
|
5487
|
+
this.updatePendingFinalization(token, pending2);
|
|
5488
|
+
return "pending";
|
|
5489
|
+
}
|
|
5490
|
+
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5491
|
+
const confirmedToken = {
|
|
5492
|
+
id: token.id,
|
|
5493
|
+
coinId: token.coinId,
|
|
5494
|
+
symbol: token.symbol,
|
|
5495
|
+
name: token.name,
|
|
5496
|
+
decimals: token.decimals,
|
|
5497
|
+
iconUrl: token.iconUrl,
|
|
5498
|
+
amount: token.amount,
|
|
5499
|
+
status: "confirmed",
|
|
5500
|
+
createdAt: token.createdAt,
|
|
5501
|
+
updatedAt: Date.now(),
|
|
5502
|
+
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5503
|
+
};
|
|
5504
|
+
this.tokens.set(tokenId, confirmedToken);
|
|
5505
|
+
await this.saveTokenToFileStorage(confirmedToken);
|
|
5506
|
+
await this.addToHistory({
|
|
5507
|
+
type: "RECEIVED",
|
|
5508
|
+
amount: confirmedToken.amount,
|
|
5509
|
+
coinId: confirmedToken.coinId,
|
|
5510
|
+
symbol: confirmedToken.symbol || "UNK",
|
|
5511
|
+
timestamp: Date.now(),
|
|
5512
|
+
senderPubkey: pending2.senderPubkey
|
|
5513
|
+
});
|
|
5514
|
+
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5515
|
+
return "resolved";
|
|
5516
|
+
}
|
|
5517
|
+
return "pending";
|
|
5518
|
+
} catch (error) {
|
|
5519
|
+
console.error(`[Payments] resolveV5Token failed for ${tokenId.slice(0, 8)}:`, error);
|
|
5520
|
+
if (pending2.attemptCount > 50) {
|
|
5521
|
+
token.status = "invalid";
|
|
5522
|
+
token.updatedAt = Date.now();
|
|
5523
|
+
this.tokens.set(tokenId, token);
|
|
5524
|
+
return "failed";
|
|
5525
|
+
}
|
|
5526
|
+
this.updatePendingFinalization(token, pending2);
|
|
5527
|
+
return "pending";
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
/**
|
|
5531
|
+
* Non-blocking proof check with 500ms timeout.
|
|
5532
|
+
*/
|
|
5533
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5534
|
+
async quickProofCheck(stClient, trustBase, commitment, timeoutMs = 500) {
|
|
5535
|
+
try {
|
|
5536
|
+
const proof = await Promise.race([
|
|
5537
|
+
(0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment),
|
|
5538
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
|
5539
|
+
]);
|
|
5540
|
+
return proof;
|
|
5541
|
+
} catch {
|
|
5542
|
+
return null;
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
/**
|
|
5546
|
+
* Perform V5 bundle finalization from stored bundle data and proofs.
|
|
5547
|
+
* Extracted from InstantSplitProcessor.processV5Bundle() steps 4-10.
|
|
5548
|
+
*/
|
|
5549
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5550
|
+
async finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase) {
|
|
5551
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5552
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5553
|
+
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5554
|
+
const mintProofJson = JSON.parse(pending2.mintProofJson);
|
|
5555
|
+
const mintProof = import_InclusionProof.InclusionProof.fromJSON(mintProofJson);
|
|
5556
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
5557
|
+
const tokenType = new import_TokenType3.TokenType(fromHex4(bundle.tokenTypeHex));
|
|
5558
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
5559
|
+
const tokenJson = {
|
|
5560
|
+
version: "2.0",
|
|
5561
|
+
state: senderMintedStateJson,
|
|
5562
|
+
genesis: mintTransaction.toJSON(),
|
|
5563
|
+
transactions: [],
|
|
5564
|
+
nametags: []
|
|
5565
|
+
};
|
|
5566
|
+
const mintedToken = await import_Token6.Token.fromJSON(tokenJson);
|
|
5567
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5568
|
+
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5569
|
+
const transferProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, transferCommitment);
|
|
5570
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
5571
|
+
const transferSalt = fromHex4(bundle.transferSaltHex);
|
|
5572
|
+
const recipientPredicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
|
|
5573
|
+
mintData.tokenId,
|
|
5574
|
+
tokenType,
|
|
5575
|
+
signingService,
|
|
5576
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256,
|
|
5577
|
+
transferSalt
|
|
5578
|
+
);
|
|
5579
|
+
const recipientState = new import_TokenState5.TokenState(recipientPredicate, null);
|
|
5580
|
+
let nametagTokens = [];
|
|
5581
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
5582
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
5583
|
+
if (bundle.nametagTokenJson) {
|
|
5584
|
+
try {
|
|
5585
|
+
const nametagToken = await import_Token6.Token.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
5586
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5587
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5588
|
+
if (proxy.address === recipientAddressStr) {
|
|
5589
|
+
nametagTokens = [nametagToken];
|
|
5590
|
+
}
|
|
5591
|
+
} catch {
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
if (nametagTokens.length === 0 && this.nametag?.token) {
|
|
5595
|
+
try {
|
|
5596
|
+
const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
|
|
5597
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5598
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5599
|
+
if (proxy.address === recipientAddressStr) {
|
|
5600
|
+
nametagTokens = [nametagToken];
|
|
5601
|
+
}
|
|
5602
|
+
} catch {
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
return stClient.finalizeTransaction(trustBase, mintedToken, recipientState, transferTransaction, nametagTokens);
|
|
5607
|
+
}
|
|
5608
|
+
/**
|
|
5609
|
+
* Parse pending finalization metadata from token's sdkData.
|
|
5610
|
+
*/
|
|
5611
|
+
parsePendingFinalization(sdkData) {
|
|
5612
|
+
if (!sdkData) return null;
|
|
5613
|
+
try {
|
|
5614
|
+
const data = JSON.parse(sdkData);
|
|
5615
|
+
if (data._pendingFinalization && data._pendingFinalization.type === "v5_bundle") {
|
|
5616
|
+
return data._pendingFinalization;
|
|
5617
|
+
}
|
|
5618
|
+
return null;
|
|
5619
|
+
} catch {
|
|
5620
|
+
return null;
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
/**
|
|
5624
|
+
* Update pending finalization metadata in token's sdkData.
|
|
5625
|
+
* Creates a new token object since sdkData is readonly.
|
|
5626
|
+
*/
|
|
5627
|
+
updatePendingFinalization(token, pending2) {
|
|
5628
|
+
const updated = {
|
|
5629
|
+
id: token.id,
|
|
5630
|
+
coinId: token.coinId,
|
|
5631
|
+
symbol: token.symbol,
|
|
5632
|
+
name: token.name,
|
|
5633
|
+
decimals: token.decimals,
|
|
5634
|
+
iconUrl: token.iconUrl,
|
|
5635
|
+
amount: token.amount,
|
|
5636
|
+
status: token.status,
|
|
5637
|
+
createdAt: token.createdAt,
|
|
5638
|
+
updatedAt: Date.now(),
|
|
5639
|
+
sdkData: JSON.stringify({ _pendingFinalization: pending2 })
|
|
5640
|
+
};
|
|
5641
|
+
this.tokens.set(token.id, updated);
|
|
5642
|
+
}
|
|
5643
|
+
/**
|
|
5644
|
+
* Save pending V5 tokens to key-value storage.
|
|
5645
|
+
* These tokens can't be serialized to TXF format (no genesis/state),
|
|
5646
|
+
* so we persist them separately and restore on load().
|
|
5647
|
+
*/
|
|
5648
|
+
async savePendingV5Tokens() {
|
|
5649
|
+
const pendingTokens = [];
|
|
5650
|
+
for (const token of this.tokens.values()) {
|
|
5651
|
+
if (this.parsePendingFinalization(token.sdkData)) {
|
|
5652
|
+
pendingTokens.push(token);
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
if (pendingTokens.length > 0) {
|
|
5656
|
+
await this.deps.storage.set(
|
|
5657
|
+
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
5658
|
+
JSON.stringify(pendingTokens)
|
|
5659
|
+
);
|
|
5660
|
+
} else {
|
|
5661
|
+
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
/**
|
|
5665
|
+
* Load pending V5 tokens from key-value storage and merge into tokens map.
|
|
5666
|
+
* Called during load() to restore tokens that TXF format can't represent.
|
|
5667
|
+
*/
|
|
5668
|
+
async loadPendingV5Tokens() {
|
|
5669
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
5670
|
+
if (!data) return;
|
|
5671
|
+
try {
|
|
5672
|
+
const pendingTokens = JSON.parse(data);
|
|
5673
|
+
for (const token of pendingTokens) {
|
|
5674
|
+
if (!this.tokens.has(token.id)) {
|
|
5675
|
+
this.tokens.set(token.id, token);
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
if (pendingTokens.length > 0) {
|
|
5679
|
+
this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
|
|
5680
|
+
}
|
|
5681
|
+
} catch {
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
// ===========================================================================
|
|
5058
5685
|
// Public API - Token Operations
|
|
5059
5686
|
// ===========================================================================
|
|
5060
5687
|
/**
|
|
5061
|
-
* Add a token
|
|
5062
|
-
*
|
|
5063
|
-
*
|
|
5064
|
-
*
|
|
5688
|
+
* Add a token to the wallet.
|
|
5689
|
+
*
|
|
5690
|
+
* Tokens are uniquely identified by a `(tokenId, stateHash)` composite key.
|
|
5691
|
+
* Duplicate detection:
|
|
5692
|
+
* - **Tombstoned** — rejected if the exact `(tokenId, stateHash)` pair has a tombstone.
|
|
5693
|
+
* - **Exact duplicate** — rejected if a token with the same composite key already exists.
|
|
5694
|
+
* - **State replacement** — if the same `tokenId` exists with a *different* `stateHash`,
|
|
5695
|
+
* the old state is archived and replaced with the incoming one.
|
|
5696
|
+
*
|
|
5697
|
+
* @param token - The token to add.
|
|
5698
|
+
* @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
|
|
5699
|
+
* @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
|
|
5065
5700
|
*/
|
|
5066
5701
|
async addToken(token, skipHistory = false) {
|
|
5067
5702
|
this.ensureInitialized();
|
|
@@ -5119,7 +5754,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5119
5754
|
});
|
|
5120
5755
|
}
|
|
5121
5756
|
await this.save();
|
|
5122
|
-
|
|
5757
|
+
if (!this.parsePendingFinalization(token.sdkData)) {
|
|
5758
|
+
await this.saveTokenToFileStorage(token);
|
|
5759
|
+
}
|
|
5123
5760
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
5124
5761
|
return true;
|
|
5125
5762
|
}
|
|
@@ -5176,6 +5813,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5176
5813
|
const data = fileData;
|
|
5177
5814
|
const tokenJson = data.token;
|
|
5178
5815
|
if (!tokenJson) continue;
|
|
5816
|
+
if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
|
|
5817
|
+
continue;
|
|
5818
|
+
}
|
|
5179
5819
|
let sdkTokenId;
|
|
5180
5820
|
if (typeof tokenJson === "object" && tokenJson !== null) {
|
|
5181
5821
|
const tokenObj = tokenJson;
|
|
@@ -5227,7 +5867,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5227
5867
|
this.log(`Loaded ${this.tokens.size} tokens from file storage`);
|
|
5228
5868
|
}
|
|
5229
5869
|
/**
|
|
5230
|
-
* Update an existing token
|
|
5870
|
+
* Update an existing token or add it if not found.
|
|
5871
|
+
*
|
|
5872
|
+
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
5873
|
+
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
5874
|
+
*
|
|
5875
|
+
* @param token - The token with updated data. Must include a valid `id`.
|
|
5231
5876
|
*/
|
|
5232
5877
|
async updateToken(token) {
|
|
5233
5878
|
this.ensureInitialized();
|
|
@@ -5251,7 +5896,15 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5251
5896
|
this.log(`Updated token ${token.id}`);
|
|
5252
5897
|
}
|
|
5253
5898
|
/**
|
|
5254
|
-
* Remove a token
|
|
5899
|
+
* Remove a token from the wallet.
|
|
5900
|
+
*
|
|
5901
|
+
* The token is archived first, then a tombstone `(tokenId, stateHash)` is
|
|
5902
|
+
* created to prevent re-addition via Nostr re-delivery. A `SENT` history
|
|
5903
|
+
* entry is created unless `skipHistory` is `true`.
|
|
5904
|
+
*
|
|
5905
|
+
* @param tokenId - Local UUID of the token to remove.
|
|
5906
|
+
* @param recipientNametag - Optional nametag of the transfer recipient (for history).
|
|
5907
|
+
* @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
|
|
5255
5908
|
*/
|
|
5256
5909
|
async removeToken(tokenId, recipientNametag, skipHistory = false) {
|
|
5257
5910
|
this.ensureInitialized();
|
|
@@ -5313,13 +5966,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5313
5966
|
// Public API - Tombstones
|
|
5314
5967
|
// ===========================================================================
|
|
5315
5968
|
/**
|
|
5316
|
-
* Get all
|
|
5969
|
+
* Get all tombstone entries.
|
|
5970
|
+
*
|
|
5971
|
+
* Each tombstone is keyed by `(tokenId, stateHash)` and prevents a spent
|
|
5972
|
+
* token state from being re-added (e.g. via Nostr re-delivery).
|
|
5973
|
+
*
|
|
5974
|
+
* @returns A shallow copy of the tombstone array.
|
|
5317
5975
|
*/
|
|
5318
5976
|
getTombstones() {
|
|
5319
5977
|
return [...this.tombstones];
|
|
5320
5978
|
}
|
|
5321
5979
|
/**
|
|
5322
|
-
* Check
|
|
5980
|
+
* Check whether a specific `(tokenId, stateHash)` combination is tombstoned.
|
|
5981
|
+
*
|
|
5982
|
+
* @param tokenId - The genesis token ID.
|
|
5983
|
+
* @param stateHash - The state hash of the token version to check.
|
|
5984
|
+
* @returns `true` if the exact combination has been tombstoned.
|
|
5323
5985
|
*/
|
|
5324
5986
|
isStateTombstoned(tokenId, stateHash) {
|
|
5325
5987
|
return this.tombstones.some(
|
|
@@ -5327,8 +5989,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5327
5989
|
);
|
|
5328
5990
|
}
|
|
5329
5991
|
/**
|
|
5330
|
-
* Merge remote
|
|
5331
|
-
*
|
|
5992
|
+
* Merge tombstones received from a remote sync source.
|
|
5993
|
+
*
|
|
5994
|
+
* Any local token whose `(tokenId, stateHash)` matches a remote tombstone is
|
|
5995
|
+
* removed. The remote tombstones are then added to the local set (union merge).
|
|
5996
|
+
*
|
|
5997
|
+
* @param remoteTombstones - Tombstone entries from the remote source.
|
|
5998
|
+
* @returns Number of local tokens that were removed.
|
|
5332
5999
|
*/
|
|
5333
6000
|
async mergeTombstones(remoteTombstones) {
|
|
5334
6001
|
this.ensureInitialized();
|
|
@@ -5364,7 +6031,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5364
6031
|
return removedCount;
|
|
5365
6032
|
}
|
|
5366
6033
|
/**
|
|
5367
|
-
*
|
|
6034
|
+
* Remove tombstones older than `maxAge` and cap the list at 100 entries.
|
|
6035
|
+
*
|
|
6036
|
+
* @param maxAge - Maximum age in milliseconds (default: 30 days).
|
|
5368
6037
|
*/
|
|
5369
6038
|
async pruneTombstones(maxAge) {
|
|
5370
6039
|
const originalCount = this.tombstones.length;
|
|
@@ -5378,20 +6047,38 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5378
6047
|
// Public API - Archives
|
|
5379
6048
|
// ===========================================================================
|
|
5380
6049
|
/**
|
|
5381
|
-
* Get archived tokens
|
|
6050
|
+
* Get all archived (spent/superseded) tokens in TXF format.
|
|
6051
|
+
*
|
|
6052
|
+
* Archived tokens are kept for recovery and sync purposes. The map key is
|
|
6053
|
+
* the genesis token ID.
|
|
6054
|
+
*
|
|
6055
|
+
* @returns A shallow copy of the archived token map.
|
|
5382
6056
|
*/
|
|
5383
6057
|
getArchivedTokens() {
|
|
5384
6058
|
return new Map(this.archivedTokens);
|
|
5385
6059
|
}
|
|
5386
6060
|
/**
|
|
5387
|
-
* Get best archived version of a token
|
|
6061
|
+
* Get the best (most committed transactions) archived version of a token.
|
|
6062
|
+
*
|
|
6063
|
+
* Searches both archived and forked token maps and returns the version with
|
|
6064
|
+
* the highest number of committed transactions.
|
|
6065
|
+
*
|
|
6066
|
+
* @param tokenId - The genesis token ID to look up.
|
|
6067
|
+
* @returns The best TXF token version, or `null` if not found.
|
|
5388
6068
|
*/
|
|
5389
6069
|
getBestArchivedVersion(tokenId) {
|
|
5390
6070
|
return findBestTokenVersion(tokenId, this.archivedTokens, this.forkedTokens);
|
|
5391
6071
|
}
|
|
5392
6072
|
/**
|
|
5393
|
-
* Merge remote
|
|
5394
|
-
*
|
|
6073
|
+
* Merge archived tokens from a remote sync source.
|
|
6074
|
+
*
|
|
6075
|
+
* For each remote token:
|
|
6076
|
+
* - If missing locally, it is added.
|
|
6077
|
+
* - If the remote version is an incremental update of the local, it replaces it.
|
|
6078
|
+
* - If the histories diverge (fork), the remote version is stored via {@link storeForkedToken}.
|
|
6079
|
+
*
|
|
6080
|
+
* @param remoteArchived - Map of genesis token ID → TXF token from remote.
|
|
6081
|
+
* @returns Number of tokens that were updated or added locally.
|
|
5395
6082
|
*/
|
|
5396
6083
|
async mergeArchivedTokens(remoteArchived) {
|
|
5397
6084
|
let mergedCount = 0;
|
|
@@ -5414,7 +6101,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5414
6101
|
return mergedCount;
|
|
5415
6102
|
}
|
|
5416
6103
|
/**
|
|
5417
|
-
* Prune archived tokens
|
|
6104
|
+
* Prune archived tokens to keep at most `maxCount` entries.
|
|
6105
|
+
*
|
|
6106
|
+
* Oldest entries (by insertion order) are removed first.
|
|
6107
|
+
*
|
|
6108
|
+
* @param maxCount - Maximum number of archived tokens to retain (default: 100).
|
|
5418
6109
|
*/
|
|
5419
6110
|
async pruneArchivedTokens(maxCount = 100) {
|
|
5420
6111
|
if (this.archivedTokens.size <= maxCount) return;
|
|
@@ -5427,13 +6118,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5427
6118
|
// Public API - Forked Tokens
|
|
5428
6119
|
// ===========================================================================
|
|
5429
6120
|
/**
|
|
5430
|
-
* Get forked
|
|
6121
|
+
* Get all forked token versions.
|
|
6122
|
+
*
|
|
6123
|
+
* Forked tokens represent alternative histories detected during sync.
|
|
6124
|
+
* The map key is `{tokenId}_{stateHash}`.
|
|
6125
|
+
*
|
|
6126
|
+
* @returns A shallow copy of the forked tokens map.
|
|
5431
6127
|
*/
|
|
5432
6128
|
getForkedTokens() {
|
|
5433
6129
|
return new Map(this.forkedTokens);
|
|
5434
6130
|
}
|
|
5435
6131
|
/**
|
|
5436
|
-
* Store a forked token
|
|
6132
|
+
* Store a forked token version (alternative history).
|
|
6133
|
+
*
|
|
6134
|
+
* No-op if the exact `(tokenId, stateHash)` key already exists.
|
|
6135
|
+
*
|
|
6136
|
+
* @param tokenId - Genesis token ID.
|
|
6137
|
+
* @param stateHash - State hash of this forked version.
|
|
6138
|
+
* @param txfToken - The TXF token data to store.
|
|
5437
6139
|
*/
|
|
5438
6140
|
async storeForkedToken(tokenId, stateHash, txfToken) {
|
|
5439
6141
|
const key = `${tokenId}_${stateHash}`;
|
|
@@ -5443,8 +6145,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5443
6145
|
await this.save();
|
|
5444
6146
|
}
|
|
5445
6147
|
/**
|
|
5446
|
-
* Merge remote
|
|
5447
|
-
*
|
|
6148
|
+
* Merge forked tokens from a remote sync source. Only new keys are added.
|
|
6149
|
+
*
|
|
6150
|
+
* @param remoteForked - Map of `{tokenId}_{stateHash}` → TXF token from remote.
|
|
6151
|
+
* @returns Number of new forked tokens added.
|
|
5448
6152
|
*/
|
|
5449
6153
|
async mergeForkedTokens(remoteForked) {
|
|
5450
6154
|
let addedCount = 0;
|
|
@@ -5460,7 +6164,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5460
6164
|
return addedCount;
|
|
5461
6165
|
}
|
|
5462
6166
|
/**
|
|
5463
|
-
* Prune forked tokens
|
|
6167
|
+
* Prune forked tokens to keep at most `maxCount` entries.
|
|
6168
|
+
*
|
|
6169
|
+
* @param maxCount - Maximum number of forked tokens to retain (default: 50).
|
|
5464
6170
|
*/
|
|
5465
6171
|
async pruneForkedTokens(maxCount = 50) {
|
|
5466
6172
|
if (this.forkedTokens.size <= maxCount) return;
|
|
@@ -5473,13 +6179,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5473
6179
|
// Public API - Transaction History
|
|
5474
6180
|
// ===========================================================================
|
|
5475
6181
|
/**
|
|
5476
|
-
* Get transaction history
|
|
6182
|
+
* Get the transaction history sorted newest-first.
|
|
6183
|
+
*
|
|
6184
|
+
* @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
|
|
5477
6185
|
*/
|
|
5478
6186
|
getHistory() {
|
|
5479
6187
|
return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
|
|
5480
6188
|
}
|
|
5481
6189
|
/**
|
|
5482
|
-
*
|
|
6190
|
+
* Append an entry to the transaction history.
|
|
6191
|
+
*
|
|
6192
|
+
* A unique `id` is auto-generated. The entry is immediately persisted to storage.
|
|
6193
|
+
*
|
|
6194
|
+
* @param entry - History entry fields (without `id`).
|
|
5483
6195
|
*/
|
|
5484
6196
|
async addToHistory(entry) {
|
|
5485
6197
|
this.ensureInitialized();
|
|
@@ -5497,7 +6209,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5497
6209
|
// Public API - Nametag
|
|
5498
6210
|
// ===========================================================================
|
|
5499
6211
|
/**
|
|
5500
|
-
* Set nametag for current identity
|
|
6212
|
+
* Set the nametag data for the current identity.
|
|
6213
|
+
*
|
|
6214
|
+
* Persists to both key-value storage and file storage (lottery compatibility).
|
|
6215
|
+
*
|
|
6216
|
+
* @param nametag - The nametag data including minted token JSON.
|
|
5501
6217
|
*/
|
|
5502
6218
|
async setNametag(nametag) {
|
|
5503
6219
|
this.ensureInitialized();
|
|
@@ -5507,19 +6223,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5507
6223
|
this.log(`Nametag set: ${nametag.name}`);
|
|
5508
6224
|
}
|
|
5509
6225
|
/**
|
|
5510
|
-
* Get nametag
|
|
6226
|
+
* Get the current nametag data.
|
|
6227
|
+
*
|
|
6228
|
+
* @returns The nametag data, or `null` if no nametag is set.
|
|
5511
6229
|
*/
|
|
5512
6230
|
getNametag() {
|
|
5513
6231
|
return this.nametag;
|
|
5514
6232
|
}
|
|
5515
6233
|
/**
|
|
5516
|
-
* Check
|
|
6234
|
+
* Check whether a nametag is currently set.
|
|
6235
|
+
*
|
|
6236
|
+
* @returns `true` if nametag data is present.
|
|
5517
6237
|
*/
|
|
5518
6238
|
hasNametag() {
|
|
5519
6239
|
return this.nametag !== null;
|
|
5520
6240
|
}
|
|
5521
6241
|
/**
|
|
5522
|
-
*
|
|
6242
|
+
* Remove the current nametag data from memory and storage.
|
|
5523
6243
|
*/
|
|
5524
6244
|
async clearNametag() {
|
|
5525
6245
|
this.ensureInitialized();
|
|
@@ -5613,9 +6333,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5613
6333
|
try {
|
|
5614
6334
|
const signingService = await this.createSigningService();
|
|
5615
6335
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5616
|
-
const { TokenType:
|
|
6336
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5617
6337
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5618
|
-
const tokenType = new
|
|
6338
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5619
6339
|
const addressRef = await UnmaskedPredicateReference4.create(
|
|
5620
6340
|
tokenType,
|
|
5621
6341
|
signingService.algorithm,
|
|
@@ -5676,11 +6396,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5676
6396
|
// Public API - Sync & Validate
|
|
5677
6397
|
// ===========================================================================
|
|
5678
6398
|
/**
|
|
5679
|
-
* Sync with all token storage providers (IPFS,
|
|
5680
|
-
*
|
|
6399
|
+
* Sync local token state with all configured token storage providers (IPFS, file, etc.).
|
|
6400
|
+
*
|
|
6401
|
+
* For each provider, the local data is packaged into TXF storage format, sent
|
|
6402
|
+
* to the provider's `sync()` method, and the merged result is applied locally.
|
|
6403
|
+
* Emits `sync:started`, `sync:completed`, and `sync:error` events.
|
|
6404
|
+
*
|
|
6405
|
+
* @returns Summary with counts of tokens added and removed during sync.
|
|
5681
6406
|
*/
|
|
5682
6407
|
async sync() {
|
|
5683
6408
|
this.ensureInitialized();
|
|
6409
|
+
if (this._syncInProgress) {
|
|
6410
|
+
return this._syncInProgress;
|
|
6411
|
+
}
|
|
6412
|
+
this._syncInProgress = this._doSync();
|
|
6413
|
+
try {
|
|
6414
|
+
return await this._syncInProgress;
|
|
6415
|
+
} finally {
|
|
6416
|
+
this._syncInProgress = null;
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
async _doSync() {
|
|
5684
6420
|
this.deps.emitEvent("sync:started", { source: "payments" });
|
|
5685
6421
|
try {
|
|
5686
6422
|
const providers = this.getTokenStorageProviders();
|
|
@@ -5718,6 +6454,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5718
6454
|
});
|
|
5719
6455
|
}
|
|
5720
6456
|
}
|
|
6457
|
+
if (totalAdded > 0 || totalRemoved > 0) {
|
|
6458
|
+
await this.save();
|
|
6459
|
+
}
|
|
5721
6460
|
this.deps.emitEvent("sync:completed", {
|
|
5722
6461
|
source: "payments",
|
|
5723
6462
|
count: this.tokens.size
|
|
@@ -5731,6 +6470,66 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5731
6470
|
throw error;
|
|
5732
6471
|
}
|
|
5733
6472
|
}
|
|
6473
|
+
// ===========================================================================
|
|
6474
|
+
// Storage Event Subscription (Push-Based Sync)
|
|
6475
|
+
// ===========================================================================
|
|
6476
|
+
/**
|
|
6477
|
+
* Subscribe to 'storage:remote-updated' events from all token storage providers.
|
|
6478
|
+
* When a provider emits this event, a debounced sync is triggered.
|
|
6479
|
+
*/
|
|
6480
|
+
subscribeToStorageEvents() {
|
|
6481
|
+
this.unsubscribeStorageEvents();
|
|
6482
|
+
const providers = this.getTokenStorageProviders();
|
|
6483
|
+
for (const [providerId, provider] of providers) {
|
|
6484
|
+
if (provider.onEvent) {
|
|
6485
|
+
const unsub = provider.onEvent((event) => {
|
|
6486
|
+
if (event.type === "storage:remote-updated") {
|
|
6487
|
+
this.log("Remote update detected from provider", providerId, event.data);
|
|
6488
|
+
this.debouncedSyncFromRemoteUpdate(providerId, event.data);
|
|
6489
|
+
}
|
|
6490
|
+
});
|
|
6491
|
+
this.storageEventUnsubscribers.push(unsub);
|
|
6492
|
+
}
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
/**
|
|
6496
|
+
* Unsubscribe from all storage provider events and clear debounce timer.
|
|
6497
|
+
*/
|
|
6498
|
+
unsubscribeStorageEvents() {
|
|
6499
|
+
for (const unsub of this.storageEventUnsubscribers) {
|
|
6500
|
+
unsub();
|
|
6501
|
+
}
|
|
6502
|
+
this.storageEventUnsubscribers = [];
|
|
6503
|
+
if (this.syncDebounceTimer) {
|
|
6504
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6505
|
+
this.syncDebounceTimer = null;
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
/**
|
|
6509
|
+
* Debounced sync triggered by a storage:remote-updated event.
|
|
6510
|
+
* Waits 500ms to batch rapid updates, then performs sync.
|
|
6511
|
+
*/
|
|
6512
|
+
debouncedSyncFromRemoteUpdate(providerId, eventData) {
|
|
6513
|
+
if (this.syncDebounceTimer) {
|
|
6514
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6515
|
+
}
|
|
6516
|
+
this.syncDebounceTimer = setTimeout(() => {
|
|
6517
|
+
this.syncDebounceTimer = null;
|
|
6518
|
+
this.sync().then((result) => {
|
|
6519
|
+
const data = eventData;
|
|
6520
|
+
this.deps?.emitEvent("sync:remote-update", {
|
|
6521
|
+
providerId,
|
|
6522
|
+
name: data?.name ?? "",
|
|
6523
|
+
sequence: data?.sequence ?? 0,
|
|
6524
|
+
cid: data?.cid ?? "",
|
|
6525
|
+
added: result.added,
|
|
6526
|
+
removed: result.removed
|
|
6527
|
+
});
|
|
6528
|
+
}).catch((err) => {
|
|
6529
|
+
this.log("Auto-sync from remote update failed:", err);
|
|
6530
|
+
});
|
|
6531
|
+
}, _PaymentsModule.SYNC_DEBOUNCE_MS);
|
|
6532
|
+
}
|
|
5734
6533
|
/**
|
|
5735
6534
|
* Get all active token storage providers
|
|
5736
6535
|
*/
|
|
@@ -5746,15 +6545,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5746
6545
|
return /* @__PURE__ */ new Map();
|
|
5747
6546
|
}
|
|
5748
6547
|
/**
|
|
5749
|
-
*
|
|
6548
|
+
* Replace the set of token storage providers at runtime.
|
|
6549
|
+
*
|
|
6550
|
+
* Use when providers are added or removed dynamically (e.g. IPFS node started).
|
|
6551
|
+
*
|
|
6552
|
+
* @param providers - New map of provider ID → TokenStorageProvider.
|
|
5750
6553
|
*/
|
|
5751
6554
|
updateTokenStorageProviders(providers) {
|
|
5752
6555
|
if (this.deps) {
|
|
5753
6556
|
this.deps.tokenStorageProviders = providers;
|
|
6557
|
+
this.subscribeToStorageEvents();
|
|
5754
6558
|
}
|
|
5755
6559
|
}
|
|
5756
6560
|
/**
|
|
5757
|
-
* Validate tokens
|
|
6561
|
+
* Validate all tokens against the aggregator (oracle provider).
|
|
6562
|
+
*
|
|
6563
|
+
* Tokens that fail validation or are detected as spent are marked `'invalid'`.
|
|
6564
|
+
*
|
|
6565
|
+
* @returns Object with arrays of valid and invalid tokens.
|
|
5758
6566
|
*/
|
|
5759
6567
|
async validate() {
|
|
5760
6568
|
this.ensureInitialized();
|
|
@@ -5775,7 +6583,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5775
6583
|
return { valid, invalid };
|
|
5776
6584
|
}
|
|
5777
6585
|
/**
|
|
5778
|
-
* Get pending transfers
|
|
6586
|
+
* Get all in-progress (pending) outgoing transfers.
|
|
6587
|
+
*
|
|
6588
|
+
* @returns Array of {@link TransferResult} objects for transfers that have not yet completed.
|
|
5779
6589
|
*/
|
|
5780
6590
|
getPendingTransfers() {
|
|
5781
6591
|
return Array.from(this.pendingTransfers.values());
|
|
@@ -5839,9 +6649,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5839
6649
|
*/
|
|
5840
6650
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
5841
6651
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5842
|
-
const { TokenType:
|
|
6652
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5843
6653
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5844
|
-
const tokenType = new
|
|
6654
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5845
6655
|
const pubkeyBytes = new Uint8Array(
|
|
5846
6656
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
5847
6657
|
);
|
|
@@ -6053,7 +6863,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6053
6863
|
this.deps.emitEvent("transfer:confirmed", {
|
|
6054
6864
|
id: crypto.randomUUID(),
|
|
6055
6865
|
status: "completed",
|
|
6056
|
-
tokens: [finalizedToken]
|
|
6866
|
+
tokens: [finalizedToken],
|
|
6867
|
+
tokenTransfers: []
|
|
6057
6868
|
});
|
|
6058
6869
|
await this.addToHistory({
|
|
6059
6870
|
type: "RECEIVED",
|
|
@@ -6076,14 +6887,26 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6076
6887
|
async handleIncomingTransfer(transfer) {
|
|
6077
6888
|
try {
|
|
6078
6889
|
const payload = transfer.payload;
|
|
6890
|
+
let instantBundle = null;
|
|
6079
6891
|
if (isInstantSplitBundle(payload)) {
|
|
6892
|
+
instantBundle = payload;
|
|
6893
|
+
} else if (payload.token) {
|
|
6894
|
+
try {
|
|
6895
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
6896
|
+
if (isInstantSplitBundle(inner)) {
|
|
6897
|
+
instantBundle = inner;
|
|
6898
|
+
}
|
|
6899
|
+
} catch {
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
if (instantBundle) {
|
|
6080
6903
|
this.log("Processing INSTANT_SPLIT bundle...");
|
|
6081
6904
|
try {
|
|
6082
6905
|
if (!this.nametag) {
|
|
6083
6906
|
await this.loadNametagFromFileStorage();
|
|
6084
6907
|
}
|
|
6085
6908
|
const result = await this.processInstantSplitBundle(
|
|
6086
|
-
|
|
6909
|
+
instantBundle,
|
|
6087
6910
|
transfer.senderTransportPubkey
|
|
6088
6911
|
);
|
|
6089
6912
|
if (result.success) {
|
|
@@ -6096,6 +6919,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6096
6919
|
}
|
|
6097
6920
|
return;
|
|
6098
6921
|
}
|
|
6922
|
+
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
6923
|
+
this.log("Processing NOSTR-FIRST commitment-only transfer...");
|
|
6924
|
+
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
6925
|
+
return;
|
|
6926
|
+
}
|
|
6099
6927
|
let tokenData;
|
|
6100
6928
|
let finalizedSdkToken = null;
|
|
6101
6929
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -6251,6 +7079,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6251
7079
|
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
6252
7080
|
}
|
|
6253
7081
|
}
|
|
7082
|
+
await this.savePendingV5Tokens();
|
|
6254
7083
|
}
|
|
6255
7084
|
async saveToOutbox(transfer, recipient) {
|
|
6256
7085
|
const outbox = await this.loadOutbox();
|
|
@@ -6268,8 +7097,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6268
7097
|
}
|
|
6269
7098
|
async createStorageData() {
|
|
6270
7099
|
return await buildTxfStorageData(
|
|
6271
|
-
|
|
6272
|
-
// Empty - active tokens stored as token-xxx files
|
|
7100
|
+
Array.from(this.tokens.values()),
|
|
6273
7101
|
{
|
|
6274
7102
|
version: 1,
|
|
6275
7103
|
address: this.deps.identity.l1Address,
|
|
@@ -6454,7 +7282,7 @@ function createPaymentsModule(config) {
|
|
|
6454
7282
|
// modules/payments/TokenRecoveryService.ts
|
|
6455
7283
|
var import_TokenId4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
6456
7284
|
var import_TokenState6 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
6457
|
-
var
|
|
7285
|
+
var import_TokenType4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
6458
7286
|
var import_CoinId5 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
|
|
6459
7287
|
var import_HashAlgorithm6 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
6460
7288
|
var import_UnmaskedPredicate6 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
@@ -7603,15 +8431,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
7603
8431
|
|
|
7604
8432
|
// core/Sphere.ts
|
|
7605
8433
|
var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
|
|
7606
|
-
var
|
|
8434
|
+
var import_TokenType5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
7607
8435
|
var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
7608
8436
|
var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
8437
|
+
var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
|
|
8438
|
+
function isValidNametag(nametag) {
|
|
8439
|
+
if ((0, import_nostr_js_sdk2.isPhoneNumber)(nametag)) return true;
|
|
8440
|
+
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
8441
|
+
}
|
|
7609
8442
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
7610
8443
|
async function deriveL3PredicateAddress(privateKey) {
|
|
7611
8444
|
const secret = Buffer.from(privateKey, "hex");
|
|
7612
8445
|
const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
|
|
7613
8446
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
7614
|
-
const tokenType = new
|
|
8447
|
+
const tokenType = new import_TokenType5.TokenType(tokenTypeBytes);
|
|
7615
8448
|
const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
|
|
7616
8449
|
tokenType,
|
|
7617
8450
|
signingService.algorithm,
|
|
@@ -7887,6 +8720,14 @@ var Sphere = class _Sphere {
|
|
|
7887
8720
|
console.log("[Sphere.import] Registering nametag...");
|
|
7888
8721
|
await sphere.registerNametag(options.nametag);
|
|
7889
8722
|
}
|
|
8723
|
+
if (sphere._tokenStorageProviders.size > 0) {
|
|
8724
|
+
try {
|
|
8725
|
+
const syncResult = await sphere._payments.sync();
|
|
8726
|
+
console.log(`[Sphere.import] Auto-sync: +${syncResult.added} -${syncResult.removed}`);
|
|
8727
|
+
} catch (err) {
|
|
8728
|
+
console.warn("[Sphere.import] Auto-sync failed (non-fatal):", err);
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
7890
8731
|
console.log("[Sphere.import] Import complete");
|
|
7891
8732
|
return sphere;
|
|
7892
8733
|
}
|
|
@@ -8757,9 +9598,9 @@ var Sphere = class _Sphere {
|
|
|
8757
9598
|
if (index < 0) {
|
|
8758
9599
|
throw new Error("Address index must be non-negative");
|
|
8759
9600
|
}
|
|
8760
|
-
const newNametag = options?.nametag
|
|
8761
|
-
if (newNametag && !
|
|
8762
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9601
|
+
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
9602
|
+
if (newNametag && !isValidNametag(newNametag)) {
|
|
9603
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
8763
9604
|
}
|
|
8764
9605
|
const addressInfo = this.deriveAddress(index, false);
|
|
8765
9606
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
@@ -9143,9 +9984,9 @@ var Sphere = class _Sphere {
|
|
|
9143
9984
|
*/
|
|
9144
9985
|
async registerNametag(nametag) {
|
|
9145
9986
|
this.ensureReady();
|
|
9146
|
-
const cleanNametag =
|
|
9147
|
-
if (!
|
|
9148
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9987
|
+
const cleanNametag = this.cleanNametag(nametag);
|
|
9988
|
+
if (!isValidNametag(cleanNametag)) {
|
|
9989
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
9149
9990
|
}
|
|
9150
9991
|
if (this._identity?.nametag) {
|
|
9151
9992
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
@@ -9454,13 +10295,11 @@ var Sphere = class _Sphere {
|
|
|
9454
10295
|
}
|
|
9455
10296
|
}
|
|
9456
10297
|
/**
|
|
9457
|
-
*
|
|
10298
|
+
* Strip @ prefix and normalize a nametag (lowercase, phone E.164, strip @unicity suffix).
|
|
9458
10299
|
*/
|
|
9459
|
-
|
|
9460
|
-
const
|
|
9461
|
-
|
|
9462
|
-
);
|
|
9463
|
-
return pattern.test(nametag);
|
|
10300
|
+
cleanNametag(raw) {
|
|
10301
|
+
const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
10302
|
+
return (0, import_nostr_js_sdk2.normalizeNametag)(stripped);
|
|
9464
10303
|
}
|
|
9465
10304
|
// ===========================================================================
|
|
9466
10305
|
// Public Methods - Lifecycle
|
|
@@ -9821,6 +10660,7 @@ init_bech32();
|
|
|
9821
10660
|
initSphere,
|
|
9822
10661
|
isEncryptedData,
|
|
9823
10662
|
isValidBech32,
|
|
10663
|
+
isValidNametag,
|
|
9824
10664
|
isValidPrivateKey,
|
|
9825
10665
|
loadSphere,
|
|
9826
10666
|
mnemonicToEntropy,
|