@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.js
CHANGED
|
@@ -2118,6 +2118,7 @@ import { MintCommitment } from "@unicitylabs/state-transition-sdk/lib/transactio
|
|
|
2118
2118
|
import { HashAlgorithm as HashAlgorithm2 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
2119
2119
|
import { UnmaskedPredicate as UnmaskedPredicate2 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
2120
2120
|
import { waitInclusionProof as waitInclusionProof2 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
2121
|
+
import { normalizeNametag } from "@unicitylabs/nostr-js-sdk";
|
|
2121
2122
|
var UNICITY_TOKEN_TYPE_HEX = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
2122
2123
|
var NametagMinter = class {
|
|
2123
2124
|
client;
|
|
@@ -2142,7 +2143,8 @@ var NametagMinter = class {
|
|
|
2142
2143
|
*/
|
|
2143
2144
|
async isNametagAvailable(nametag) {
|
|
2144
2145
|
try {
|
|
2145
|
-
const
|
|
2146
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2147
|
+
const cleanNametag = normalizeNametag(stripped);
|
|
2146
2148
|
const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
|
|
2147
2149
|
const isMinted = await this.client.isMinted(this.trustBase, nametagTokenId);
|
|
2148
2150
|
return !isMinted;
|
|
@@ -2159,7 +2161,8 @@ var NametagMinter = class {
|
|
|
2159
2161
|
* @returns MintNametagResult with token if successful
|
|
2160
2162
|
*/
|
|
2161
2163
|
async mintNametag(nametag, ownerAddress) {
|
|
2162
|
-
const
|
|
2164
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2165
|
+
const cleanNametag = normalizeNametag(stripped);
|
|
2163
2166
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2164
2167
|
try {
|
|
2165
2168
|
const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
|
|
@@ -2312,7 +2315,9 @@ var STORAGE_KEYS_ADDRESS = {
|
|
|
2312
2315
|
/** Messages for this address */
|
|
2313
2316
|
MESSAGES: "messages",
|
|
2314
2317
|
/** Transaction history for this address */
|
|
2315
|
-
TRANSACTION_HISTORY: "transaction_history"
|
|
2318
|
+
TRANSACTION_HISTORY: "transaction_history",
|
|
2319
|
+
/** Pending V5 finalization tokens (unconfirmed instant split tokens) */
|
|
2320
|
+
PENDING_V5_TOKENS: "pending_v5_tokens"
|
|
2316
2321
|
};
|
|
2317
2322
|
var STORAGE_KEYS = {
|
|
2318
2323
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -2331,16 +2336,6 @@ function getAddressId(directAddress) {
|
|
|
2331
2336
|
}
|
|
2332
2337
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
2333
2338
|
var DEFAULT_DERIVATION_PATH2 = `${DEFAULT_BASE_PATH}/0/0`;
|
|
2334
|
-
var LIMITS = {
|
|
2335
|
-
/** Min nametag length */
|
|
2336
|
-
NAMETAG_MIN_LENGTH: 3,
|
|
2337
|
-
/** Max nametag length */
|
|
2338
|
-
NAMETAG_MAX_LENGTH: 20,
|
|
2339
|
-
/** Max memo length */
|
|
2340
|
-
MEMO_MAX_LENGTH: 500,
|
|
2341
|
-
/** Max message length */
|
|
2342
|
-
MESSAGE_MAX_LENGTH: 1e4
|
|
2343
|
-
};
|
|
2344
2339
|
|
|
2345
2340
|
// types/txf.ts
|
|
2346
2341
|
var ARCHIVED_PREFIX = "archived-";
|
|
@@ -2633,6 +2628,18 @@ function parseTxfStorageData(data) {
|
|
|
2633
2628
|
result.validationErrors.push(`Forked token ${parsed.tokenId}: invalid structure`);
|
|
2634
2629
|
}
|
|
2635
2630
|
}
|
|
2631
|
+
} else if (key.startsWith("token-")) {
|
|
2632
|
+
try {
|
|
2633
|
+
const entry = storageData[key];
|
|
2634
|
+
const txfToken = entry?.token;
|
|
2635
|
+
if (txfToken?.genesis?.data?.tokenId) {
|
|
2636
|
+
const tokenId = txfToken.genesis.data.tokenId;
|
|
2637
|
+
const token = txfToToken(tokenId, txfToken);
|
|
2638
|
+
result.tokens.push(token);
|
|
2639
|
+
}
|
|
2640
|
+
} catch (err) {
|
|
2641
|
+
result.validationErrors.push(`Token ${key}: ${err}`);
|
|
2642
|
+
}
|
|
2636
2643
|
}
|
|
2637
2644
|
}
|
|
2638
2645
|
return result;
|
|
@@ -3133,8 +3140,9 @@ var InstantSplitExecutor = class {
|
|
|
3133
3140
|
const criticalPathDuration = performance.now() - startTime;
|
|
3134
3141
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3135
3142
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3143
|
+
let backgroundPromise;
|
|
3136
3144
|
if (!options?.skipBackground) {
|
|
3137
|
-
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3145
|
+
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3138
3146
|
signingService: this.signingService,
|
|
3139
3147
|
tokenType: tokenToSplit.type,
|
|
3140
3148
|
coinId,
|
|
@@ -3150,7 +3158,8 @@ var InstantSplitExecutor = class {
|
|
|
3150
3158
|
nostrEventId,
|
|
3151
3159
|
splitGroupId,
|
|
3152
3160
|
criticalPathDurationMs: criticalPathDuration,
|
|
3153
|
-
backgroundStarted: !options?.skipBackground
|
|
3161
|
+
backgroundStarted: !options?.skipBackground,
|
|
3162
|
+
backgroundPromise
|
|
3154
3163
|
};
|
|
3155
3164
|
} catch (error) {
|
|
3156
3165
|
const duration = performance.now() - startTime;
|
|
@@ -3212,7 +3221,7 @@ var InstantSplitExecutor = class {
|
|
|
3212
3221
|
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3213
3222
|
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3214
3223
|
]);
|
|
3215
|
-
submissions.then(async (results) => {
|
|
3224
|
+
return submissions.then(async (results) => {
|
|
3216
3225
|
const submitDuration = performance.now() - startTime;
|
|
3217
3226
|
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3218
3227
|
context.onProgress?.({
|
|
@@ -3677,6 +3686,11 @@ import { AddressScheme } from "@unicitylabs/state-transition-sdk/lib/address/Add
|
|
|
3677
3686
|
import { UnmaskedPredicate as UnmaskedPredicate5 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
3678
3687
|
import { TokenState as TokenState5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
3679
3688
|
import { HashAlgorithm as HashAlgorithm5 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
3689
|
+
import { TokenType as TokenType3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
3690
|
+
import { MintCommitment as MintCommitment3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment";
|
|
3691
|
+
import { MintTransactionData as MintTransactionData3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData";
|
|
3692
|
+
import { waitInclusionProof as waitInclusionProof5 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
3693
|
+
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof";
|
|
3680
3694
|
function enrichWithRegistry(info) {
|
|
3681
3695
|
const registry = TokenRegistry.getInstance();
|
|
3682
3696
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -3874,6 +3888,13 @@ function extractTokenStateKey(token) {
|
|
|
3874
3888
|
if (!tokenId || !stateHash) return null;
|
|
3875
3889
|
return createTokenStateKey(tokenId, stateHash);
|
|
3876
3890
|
}
|
|
3891
|
+
function fromHex4(hex) {
|
|
3892
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3893
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3894
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
3895
|
+
}
|
|
3896
|
+
return bytes;
|
|
3897
|
+
}
|
|
3877
3898
|
function hasSameGenesisTokenId(t1, t2) {
|
|
3878
3899
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
3879
3900
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
@@ -3963,6 +3984,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
3963
3984
|
// Token State
|
|
3964
3985
|
tokens = /* @__PURE__ */ new Map();
|
|
3965
3986
|
pendingTransfers = /* @__PURE__ */ new Map();
|
|
3987
|
+
pendingBackgroundTasks = [];
|
|
3966
3988
|
// Repository State (tombstones, archives, forked, history)
|
|
3967
3989
|
tombstones = [];
|
|
3968
3990
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
@@ -3987,6 +4009,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
3987
4009
|
// Poll every 2s
|
|
3988
4010
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
3989
4011
|
// Max 30 attempts (~60s)
|
|
4012
|
+
// Storage event subscriptions (push-based sync)
|
|
4013
|
+
storageEventUnsubscribers = [];
|
|
4014
|
+
syncDebounceTimer = null;
|
|
4015
|
+
static SYNC_DEBOUNCE_MS = 500;
|
|
4016
|
+
/** Sync coalescing: concurrent sync() calls share the same operation */
|
|
4017
|
+
_syncInProgress = null;
|
|
3990
4018
|
constructor(config) {
|
|
3991
4019
|
this.moduleConfig = {
|
|
3992
4020
|
autoSync: config?.autoSync ?? true,
|
|
@@ -3997,7 +4025,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
3997
4025
|
};
|
|
3998
4026
|
this.l1 = config?.l1 === null ? null : new L1PaymentsModule(config?.l1);
|
|
3999
4027
|
}
|
|
4000
|
-
/**
|
|
4028
|
+
/**
|
|
4029
|
+
* Get the current module configuration (excluding L1 config).
|
|
4030
|
+
*
|
|
4031
|
+
* @returns Resolved configuration with all defaults applied.
|
|
4032
|
+
*/
|
|
4001
4033
|
getConfig() {
|
|
4002
4034
|
return this.moduleConfig;
|
|
4003
4035
|
}
|
|
@@ -4038,9 +4070,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4038
4070
|
transport: deps.transport
|
|
4039
4071
|
});
|
|
4040
4072
|
}
|
|
4041
|
-
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4042
|
-
this.handleIncomingTransfer(transfer)
|
|
4043
|
-
|
|
4073
|
+
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4074
|
+
(transfer) => this.handleIncomingTransfer(transfer)
|
|
4075
|
+
);
|
|
4044
4076
|
if (deps.transport.onPaymentRequest) {
|
|
4045
4077
|
this.unsubscribePaymentRequests = deps.transport.onPaymentRequest((request) => {
|
|
4046
4078
|
this.handleIncomingPaymentRequest(request);
|
|
@@ -4051,9 +4083,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4051
4083
|
this.handlePaymentRequestResponse(response);
|
|
4052
4084
|
});
|
|
4053
4085
|
}
|
|
4086
|
+
this.subscribeToStorageEvents();
|
|
4054
4087
|
}
|
|
4055
4088
|
/**
|
|
4056
|
-
* Load
|
|
4089
|
+
* Load all token data from storage providers and restore wallet state.
|
|
4090
|
+
*
|
|
4091
|
+
* Loads tokens, nametag data, transaction history, and pending transfers
|
|
4092
|
+
* from configured storage providers. Restores pending V5 tokens and
|
|
4093
|
+
* triggers a fire-and-forget {@link resolveUnconfirmed} call.
|
|
4057
4094
|
*/
|
|
4058
4095
|
async load() {
|
|
4059
4096
|
this.ensureInitialized();
|
|
@@ -4070,6 +4107,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4070
4107
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4071
4108
|
}
|
|
4072
4109
|
}
|
|
4110
|
+
await this.loadPendingV5Tokens();
|
|
4073
4111
|
await this.loadTokensFromFileStorage();
|
|
4074
4112
|
await this.loadNametagFromFileStorage();
|
|
4075
4113
|
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
@@ -4087,9 +4125,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4087
4125
|
this.pendingTransfers.set(transfer.id, transfer);
|
|
4088
4126
|
}
|
|
4089
4127
|
}
|
|
4128
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4129
|
+
});
|
|
4090
4130
|
}
|
|
4091
4131
|
/**
|
|
4092
|
-
* Cleanup
|
|
4132
|
+
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
4133
|
+
*
|
|
4134
|
+
* Should be called when the wallet is being shut down or the module is
|
|
4135
|
+
* no longer needed. Also destroys the L1 sub-module if present.
|
|
4093
4136
|
*/
|
|
4094
4137
|
destroy() {
|
|
4095
4138
|
this.unsubscribeTransfers?.();
|
|
@@ -4107,6 +4150,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4107
4150
|
resolver.reject(new Error("Module destroyed"));
|
|
4108
4151
|
}
|
|
4109
4152
|
this.pendingResponseResolvers.clear();
|
|
4153
|
+
this.unsubscribeStorageEvents();
|
|
4110
4154
|
if (this.l1) {
|
|
4111
4155
|
this.l1.destroy();
|
|
4112
4156
|
}
|
|
@@ -4123,7 +4167,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4123
4167
|
const result = {
|
|
4124
4168
|
id: crypto.randomUUID(),
|
|
4125
4169
|
status: "pending",
|
|
4126
|
-
tokens: []
|
|
4170
|
+
tokens: [],
|
|
4171
|
+
tokenTransfers: []
|
|
4127
4172
|
};
|
|
4128
4173
|
try {
|
|
4129
4174
|
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
@@ -4160,69 +4205,147 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4160
4205
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4161
4206
|
result.status = "submitted";
|
|
4162
4207
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4208
|
+
const transferMode = request.transferMode ?? "instant";
|
|
4163
4209
|
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4210
|
+
if (transferMode === "conservative") {
|
|
4211
|
+
this.log("Executing conservative split...");
|
|
4212
|
+
const splitExecutor = new TokenSplitExecutor({
|
|
4213
|
+
stateTransitionClient: stClient,
|
|
4214
|
+
trustBase,
|
|
4215
|
+
signingService
|
|
4216
|
+
});
|
|
4217
|
+
const splitResult = await splitExecutor.executeSplit(
|
|
4218
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4219
|
+
splitPlan.splitAmount,
|
|
4220
|
+
splitPlan.remainderAmount,
|
|
4221
|
+
splitPlan.coinId,
|
|
4222
|
+
recipientAddress
|
|
4223
|
+
);
|
|
4224
|
+
const changeTokenData = splitResult.tokenForSender.toJSON();
|
|
4225
|
+
const changeUiToken = {
|
|
4226
|
+
id: crypto.randomUUID(),
|
|
4227
|
+
coinId: request.coinId,
|
|
4228
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4229
|
+
name: this.getCoinName(request.coinId),
|
|
4230
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4231
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4232
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4233
|
+
status: "confirmed",
|
|
4234
|
+
createdAt: Date.now(),
|
|
4235
|
+
updatedAt: Date.now(),
|
|
4236
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4237
|
+
};
|
|
4238
|
+
await this.addToken(changeUiToken, true);
|
|
4239
|
+
this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
|
|
4240
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4241
|
+
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
4242
|
+
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
4243
|
+
memo: request.memo
|
|
4244
|
+
});
|
|
4245
|
+
const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
|
|
4246
|
+
const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
|
|
4247
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
|
|
4248
|
+
result.tokenTransfers.push({
|
|
4249
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4250
|
+
method: "split",
|
|
4251
|
+
requestIdHex: splitRequestIdHex
|
|
4252
|
+
});
|
|
4253
|
+
this.log(`Conservative split transfer completed`);
|
|
4254
|
+
} else {
|
|
4255
|
+
this.log("Executing instant split...");
|
|
4256
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4257
|
+
const executor = new InstantSplitExecutor({
|
|
4258
|
+
stateTransitionClient: stClient,
|
|
4259
|
+
trustBase,
|
|
4260
|
+
signingService,
|
|
4261
|
+
devMode
|
|
4262
|
+
});
|
|
4263
|
+
const instantResult = await executor.executeSplitInstant(
|
|
4264
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4265
|
+
splitPlan.splitAmount,
|
|
4266
|
+
splitPlan.remainderAmount,
|
|
4267
|
+
splitPlan.coinId,
|
|
4268
|
+
recipientAddress,
|
|
4269
|
+
this.deps.transport,
|
|
4270
|
+
recipientPubkey,
|
|
4271
|
+
{
|
|
4272
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4273
|
+
const changeTokenData = changeToken.toJSON();
|
|
4274
|
+
const uiToken = {
|
|
4275
|
+
id: crypto.randomUUID(),
|
|
4276
|
+
coinId: request.coinId,
|
|
4277
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4278
|
+
name: this.getCoinName(request.coinId),
|
|
4279
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4280
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4281
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4282
|
+
status: "confirmed",
|
|
4283
|
+
createdAt: Date.now(),
|
|
4284
|
+
updatedAt: Date.now(),
|
|
4285
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4286
|
+
};
|
|
4287
|
+
await this.addToken(uiToken, true);
|
|
4288
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4289
|
+
},
|
|
4290
|
+
onStorageSync: async () => {
|
|
4291
|
+
await this.save();
|
|
4292
|
+
return true;
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
);
|
|
4296
|
+
if (!instantResult.success) {
|
|
4297
|
+
throw new Error(instantResult.error || "Instant split failed");
|
|
4298
|
+
}
|
|
4299
|
+
if (instantResult.backgroundPromise) {
|
|
4300
|
+
this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
|
|
4301
|
+
}
|
|
4302
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
4303
|
+
result.tokenTransfers.push({
|
|
4304
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4305
|
+
method: "split",
|
|
4306
|
+
splitGroupId: instantResult.splitGroupId,
|
|
4307
|
+
nostrEventId: instantResult.nostrEventId
|
|
4308
|
+
});
|
|
4309
|
+
this.log(`Instant split transfer completed`);
|
|
4310
|
+
}
|
|
4203
4311
|
}
|
|
4204
4312
|
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4205
4313
|
const token = tokenWithAmount.uiToken;
|
|
4206
4314
|
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4315
|
+
if (transferMode === "conservative") {
|
|
4316
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4317
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4318
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4319
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4320
|
+
}
|
|
4321
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4322
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4323
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4324
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4325
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4326
|
+
memo: request.memo
|
|
4327
|
+
});
|
|
4328
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4329
|
+
} else {
|
|
4330
|
+
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4331
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4332
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4333
|
+
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4334
|
+
memo: request.memo
|
|
4335
|
+
});
|
|
4336
|
+
console.log(`[Payments] NOSTR-FIRST: Direct token sent successfully`);
|
|
4337
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4338
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4339
|
+
);
|
|
4213
4340
|
}
|
|
4214
|
-
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
4215
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4216
4341
|
const requestIdBytes = commitment.requestId;
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
memo: request.memo
|
|
4342
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4343
|
+
result.tokenTransfers.push({
|
|
4344
|
+
sourceTokenId: token.id,
|
|
4345
|
+
method: "direct",
|
|
4346
|
+
requestIdHex
|
|
4223
4347
|
});
|
|
4224
|
-
|
|
4225
|
-
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
4348
|
+
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4226
4349
|
await this.removeToken(token.id, recipientNametag, true);
|
|
4227
4350
|
}
|
|
4228
4351
|
result.status = "delivered";
|
|
@@ -4235,7 +4358,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4235
4358
|
coinId: request.coinId,
|
|
4236
4359
|
symbol: this.getCoinSymbol(request.coinId),
|
|
4237
4360
|
timestamp: Date.now(),
|
|
4238
|
-
recipientNametag
|
|
4361
|
+
recipientNametag,
|
|
4362
|
+
transferId: result.id
|
|
4239
4363
|
});
|
|
4240
4364
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4241
4365
|
return result;
|
|
@@ -4371,6 +4495,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4371
4495
|
}
|
|
4372
4496
|
);
|
|
4373
4497
|
if (result.success) {
|
|
4498
|
+
if (result.backgroundPromise) {
|
|
4499
|
+
this.pendingBackgroundTasks.push(result.backgroundPromise);
|
|
4500
|
+
}
|
|
4374
4501
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4375
4502
|
await this.removeToken(tokenToSplit.id, recipientNametag, true);
|
|
4376
4503
|
await this.addToHistory({
|
|
@@ -4412,6 +4539,63 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4412
4539
|
*/
|
|
4413
4540
|
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4414
4541
|
this.ensureInitialized();
|
|
4542
|
+
if (!isInstantSplitBundleV5(bundle)) {
|
|
4543
|
+
return this.processInstantSplitBundleSync(bundle, senderPubkey);
|
|
4544
|
+
}
|
|
4545
|
+
try {
|
|
4546
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
4547
|
+
if (this.tokens.has(deterministicId)) {
|
|
4548
|
+
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
4549
|
+
return { success: true, durationMs: 0 };
|
|
4550
|
+
}
|
|
4551
|
+
const registry = TokenRegistry.getInstance();
|
|
4552
|
+
const pendingData = {
|
|
4553
|
+
type: "v5_bundle",
|
|
4554
|
+
stage: "RECEIVED",
|
|
4555
|
+
bundleJson: JSON.stringify(bundle),
|
|
4556
|
+
senderPubkey,
|
|
4557
|
+
savedAt: Date.now(),
|
|
4558
|
+
attemptCount: 0
|
|
4559
|
+
};
|
|
4560
|
+
const uiToken = {
|
|
4561
|
+
id: deterministicId,
|
|
4562
|
+
coinId: bundle.coinId,
|
|
4563
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4564
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4565
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4566
|
+
amount: bundle.amount,
|
|
4567
|
+
status: "submitted",
|
|
4568
|
+
// UNCONFIRMED
|
|
4569
|
+
createdAt: Date.now(),
|
|
4570
|
+
updatedAt: Date.now(),
|
|
4571
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4572
|
+
};
|
|
4573
|
+
await this.addToken(uiToken, false);
|
|
4574
|
+
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
4575
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
4576
|
+
id: bundle.splitGroupId,
|
|
4577
|
+
senderPubkey,
|
|
4578
|
+
tokens: [uiToken],
|
|
4579
|
+
receivedAt: Date.now()
|
|
4580
|
+
});
|
|
4581
|
+
await this.save();
|
|
4582
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4583
|
+
});
|
|
4584
|
+
return { success: true, durationMs: 0 };
|
|
4585
|
+
} catch (error) {
|
|
4586
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4587
|
+
return {
|
|
4588
|
+
success: false,
|
|
4589
|
+
error: errorMessage,
|
|
4590
|
+
durationMs: 0
|
|
4591
|
+
};
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
/**
|
|
4595
|
+
* Synchronous V4 bundle processing (dev mode only).
|
|
4596
|
+
* Kept for backward compatibility with V4 bundles.
|
|
4597
|
+
*/
|
|
4598
|
+
async processInstantSplitBundleSync(bundle, senderPubkey) {
|
|
4415
4599
|
try {
|
|
4416
4600
|
const signingService = await this.createSigningService();
|
|
4417
4601
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
@@ -4497,7 +4681,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4497
4681
|
}
|
|
4498
4682
|
}
|
|
4499
4683
|
/**
|
|
4500
|
-
*
|
|
4684
|
+
* Type-guard: check whether a payload is a valid {@link InstantSplitBundle} (V4 or V5).
|
|
4685
|
+
*
|
|
4686
|
+
* @param payload - The object to test.
|
|
4687
|
+
* @returns `true` if the payload matches the InstantSplitBundle shape.
|
|
4501
4688
|
*/
|
|
4502
4689
|
isInstantSplitBundle(payload) {
|
|
4503
4690
|
return isInstantSplitBundle(payload);
|
|
@@ -4578,39 +4765,57 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4578
4765
|
return [...this.paymentRequests];
|
|
4579
4766
|
}
|
|
4580
4767
|
/**
|
|
4581
|
-
* Get
|
|
4768
|
+
* Get the count of payment requests with status `'pending'`.
|
|
4769
|
+
*
|
|
4770
|
+
* @returns Number of pending incoming payment requests.
|
|
4582
4771
|
*/
|
|
4583
4772
|
getPendingPaymentRequestsCount() {
|
|
4584
4773
|
return this.paymentRequests.filter((r) => r.status === "pending").length;
|
|
4585
4774
|
}
|
|
4586
4775
|
/**
|
|
4587
|
-
* Accept a payment request
|
|
4776
|
+
* Accept a payment request and notify the requester.
|
|
4777
|
+
*
|
|
4778
|
+
* Marks the request as `'accepted'` and sends a response via transport.
|
|
4779
|
+
* The caller should subsequently call {@link send} to fulfill the payment.
|
|
4780
|
+
*
|
|
4781
|
+
* @param requestId - ID of the incoming payment request to accept.
|
|
4588
4782
|
*/
|
|
4589
4783
|
async acceptPaymentRequest(requestId2) {
|
|
4590
4784
|
this.updatePaymentRequestStatus(requestId2, "accepted");
|
|
4591
4785
|
await this.sendPaymentRequestResponse(requestId2, "accepted");
|
|
4592
4786
|
}
|
|
4593
4787
|
/**
|
|
4594
|
-
* Reject a payment request
|
|
4788
|
+
* Reject a payment request and notify the requester.
|
|
4789
|
+
*
|
|
4790
|
+
* @param requestId - ID of the incoming payment request to reject.
|
|
4595
4791
|
*/
|
|
4596
4792
|
async rejectPaymentRequest(requestId2) {
|
|
4597
4793
|
this.updatePaymentRequestStatus(requestId2, "rejected");
|
|
4598
4794
|
await this.sendPaymentRequestResponse(requestId2, "rejected");
|
|
4599
4795
|
}
|
|
4600
4796
|
/**
|
|
4601
|
-
* Mark a payment request as paid (
|
|
4797
|
+
* Mark a payment request as paid (local status update only).
|
|
4798
|
+
*
|
|
4799
|
+
* Typically called after a successful {@link send} to record that the
|
|
4800
|
+
* request has been fulfilled.
|
|
4801
|
+
*
|
|
4802
|
+
* @param requestId - ID of the incoming payment request to mark as paid.
|
|
4602
4803
|
*/
|
|
4603
4804
|
markPaymentRequestPaid(requestId2) {
|
|
4604
4805
|
this.updatePaymentRequestStatus(requestId2, "paid");
|
|
4605
4806
|
}
|
|
4606
4807
|
/**
|
|
4607
|
-
*
|
|
4808
|
+
* Remove all non-pending incoming payment requests from memory.
|
|
4809
|
+
*
|
|
4810
|
+
* Keeps only requests with status `'pending'`.
|
|
4608
4811
|
*/
|
|
4609
4812
|
clearProcessedPaymentRequests() {
|
|
4610
4813
|
this.paymentRequests = this.paymentRequests.filter((r) => r.status === "pending");
|
|
4611
4814
|
}
|
|
4612
4815
|
/**
|
|
4613
|
-
* Remove a specific payment request
|
|
4816
|
+
* Remove a specific incoming payment request by ID.
|
|
4817
|
+
*
|
|
4818
|
+
* @param requestId - ID of the payment request to remove.
|
|
4614
4819
|
*/
|
|
4615
4820
|
removePaymentRequest(requestId2) {
|
|
4616
4821
|
this.paymentRequests = this.paymentRequests.filter((r) => r.id !== requestId2);
|
|
@@ -4735,7 +4940,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4735
4940
|
});
|
|
4736
4941
|
}
|
|
4737
4942
|
/**
|
|
4738
|
-
* Cancel
|
|
4943
|
+
* Cancel an active {@link waitForPaymentResponse} call.
|
|
4944
|
+
*
|
|
4945
|
+
* The pending promise is rejected with a `'Cancelled'` error.
|
|
4946
|
+
*
|
|
4947
|
+
* @param requestId - The outgoing request ID whose wait should be cancelled.
|
|
4739
4948
|
*/
|
|
4740
4949
|
cancelWaitForPaymentResponse(requestId2) {
|
|
4741
4950
|
const resolver = this.pendingResponseResolvers.get(requestId2);
|
|
@@ -4746,14 +4955,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4746
4955
|
}
|
|
4747
4956
|
}
|
|
4748
4957
|
/**
|
|
4749
|
-
* Remove an outgoing payment request
|
|
4958
|
+
* Remove an outgoing payment request and cancel any pending wait.
|
|
4959
|
+
*
|
|
4960
|
+
* @param requestId - ID of the outgoing request to remove.
|
|
4750
4961
|
*/
|
|
4751
4962
|
removeOutgoingPaymentRequest(requestId2) {
|
|
4752
4963
|
this.outgoingPaymentRequests.delete(requestId2);
|
|
4753
4964
|
this.cancelWaitForPaymentResponse(requestId2);
|
|
4754
4965
|
}
|
|
4755
4966
|
/**
|
|
4756
|
-
*
|
|
4967
|
+
* Remove all outgoing payment requests that are `'paid'`, `'rejected'`, or `'expired'`.
|
|
4757
4968
|
*/
|
|
4758
4969
|
clearCompletedOutgoingPaymentRequests() {
|
|
4759
4970
|
for (const [id, request] of this.outgoingPaymentRequests) {
|
|
@@ -4825,6 +5036,71 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4825
5036
|
}
|
|
4826
5037
|
}
|
|
4827
5038
|
// ===========================================================================
|
|
5039
|
+
// Public API - Receive
|
|
5040
|
+
// ===========================================================================
|
|
5041
|
+
/**
|
|
5042
|
+
* Fetch and process pending incoming transfers from the transport layer.
|
|
5043
|
+
*
|
|
5044
|
+
* Performs a one-shot query to fetch all pending events, processes them
|
|
5045
|
+
* through the existing pipeline, and resolves after all stored events
|
|
5046
|
+
* are handled. Useful for batch/CLI apps that need explicit receive.
|
|
5047
|
+
*
|
|
5048
|
+
* When `finalize` is true, polls resolveUnconfirmed() + load() until all
|
|
5049
|
+
* tokens are confirmed or the timeout expires. Otherwise calls
|
|
5050
|
+
* resolveUnconfirmed() once to submit pending commitments.
|
|
5051
|
+
*
|
|
5052
|
+
* @param options - Optional receive options including finalization control
|
|
5053
|
+
* @param callback - Optional callback invoked for each newly received transfer
|
|
5054
|
+
* @returns ReceiveResult with transfers and finalization metadata
|
|
5055
|
+
*/
|
|
5056
|
+
async receive(options, callback) {
|
|
5057
|
+
this.ensureInitialized();
|
|
5058
|
+
if (!this.deps.transport.fetchPendingEvents) {
|
|
5059
|
+
throw new Error("Transport provider does not support fetchPendingEvents");
|
|
5060
|
+
}
|
|
5061
|
+
const opts = options ?? {};
|
|
5062
|
+
const tokensBefore = new Set(this.tokens.keys());
|
|
5063
|
+
await this.deps.transport.fetchPendingEvents();
|
|
5064
|
+
await this.load();
|
|
5065
|
+
const received = [];
|
|
5066
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5067
|
+
if (!tokensBefore.has(tokenId)) {
|
|
5068
|
+
const transfer = {
|
|
5069
|
+
id: tokenId,
|
|
5070
|
+
senderPubkey: "",
|
|
5071
|
+
tokens: [token],
|
|
5072
|
+
receivedAt: Date.now()
|
|
5073
|
+
};
|
|
5074
|
+
received.push(transfer);
|
|
5075
|
+
if (callback) callback(transfer);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
const result = { transfers: received };
|
|
5079
|
+
if (opts.finalize) {
|
|
5080
|
+
const timeout = opts.timeout ?? 6e4;
|
|
5081
|
+
const pollInterval = opts.pollInterval ?? 2e3;
|
|
5082
|
+
const startTime = Date.now();
|
|
5083
|
+
while (Date.now() - startTime < timeout) {
|
|
5084
|
+
const resolution = await this.resolveUnconfirmed();
|
|
5085
|
+
result.finalization = resolution;
|
|
5086
|
+
if (opts.onProgress) opts.onProgress(resolution);
|
|
5087
|
+
const stillUnconfirmed = Array.from(this.tokens.values()).some(
|
|
5088
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5089
|
+
);
|
|
5090
|
+
if (!stillUnconfirmed) break;
|
|
5091
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
5092
|
+
await this.load();
|
|
5093
|
+
}
|
|
5094
|
+
result.finalizationDurationMs = Date.now() - startTime;
|
|
5095
|
+
result.timedOut = Array.from(this.tokens.values()).some(
|
|
5096
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5097
|
+
);
|
|
5098
|
+
} else {
|
|
5099
|
+
result.finalization = await this.resolveUnconfirmed();
|
|
5100
|
+
}
|
|
5101
|
+
return result;
|
|
5102
|
+
}
|
|
5103
|
+
// ===========================================================================
|
|
4828
5104
|
// Public API - Balance & Tokens
|
|
4829
5105
|
// ===========================================================================
|
|
4830
5106
|
/**
|
|
@@ -4834,10 +5110,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4834
5110
|
this.priceProvider = provider;
|
|
4835
5111
|
}
|
|
4836
5112
|
/**
|
|
4837
|
-
*
|
|
4838
|
-
*
|
|
5113
|
+
* Wait for all pending background operations (e.g., instant split change token creation).
|
|
5114
|
+
* Call this before process exit to ensure all tokens are saved.
|
|
4839
5115
|
*/
|
|
4840
|
-
async
|
|
5116
|
+
async waitForPendingOperations() {
|
|
5117
|
+
if (this.pendingBackgroundTasks.length > 0) {
|
|
5118
|
+
await Promise.allSettled(this.pendingBackgroundTasks);
|
|
5119
|
+
this.pendingBackgroundTasks = [];
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
/**
|
|
5123
|
+
* Get total portfolio value in USD.
|
|
5124
|
+
* Returns null if PriceProvider is not configured.
|
|
5125
|
+
*/
|
|
5126
|
+
async getFiatBalance() {
|
|
4841
5127
|
const assets = await this.getAssets();
|
|
4842
5128
|
if (!this.priceProvider) {
|
|
4843
5129
|
return null;
|
|
@@ -4853,19 +5139,95 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4853
5139
|
return hasAnyPrice ? total : null;
|
|
4854
5140
|
}
|
|
4855
5141
|
/**
|
|
4856
|
-
* Get
|
|
4857
|
-
*
|
|
5142
|
+
* Get token balances grouped by coin type.
|
|
5143
|
+
*
|
|
5144
|
+
* Returns an array of {@link Asset} objects, one per coin type held.
|
|
5145
|
+
* Each entry includes confirmed and unconfirmed breakdowns. Tokens with
|
|
5146
|
+
* status `'spent'`, `'invalid'`, or `'transferring'` are excluded.
|
|
5147
|
+
*
|
|
5148
|
+
* This is synchronous — no price data is included. Use {@link getAssets}
|
|
5149
|
+
* for the async version with fiat pricing.
|
|
5150
|
+
*
|
|
5151
|
+
* @param coinId - Optional coin ID to filter by (e.g. hex string). When omitted, all coin types are returned.
|
|
5152
|
+
* @returns Array of balance summaries (synchronous — no await needed).
|
|
5153
|
+
*/
|
|
5154
|
+
getBalance(coinId) {
|
|
5155
|
+
return this.aggregateTokens(coinId);
|
|
5156
|
+
}
|
|
5157
|
+
/**
|
|
5158
|
+
* Get aggregated assets (tokens grouped by coinId) with price data.
|
|
5159
|
+
* Includes both confirmed and unconfirmed tokens with breakdown.
|
|
4858
5160
|
*/
|
|
4859
5161
|
async getAssets(coinId) {
|
|
5162
|
+
const rawAssets = this.aggregateTokens(coinId);
|
|
5163
|
+
if (!this.priceProvider || rawAssets.length === 0) {
|
|
5164
|
+
return rawAssets;
|
|
5165
|
+
}
|
|
5166
|
+
try {
|
|
5167
|
+
const registry = TokenRegistry.getInstance();
|
|
5168
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5169
|
+
for (const asset of rawAssets) {
|
|
5170
|
+
const def = registry.getDefinition(asset.coinId);
|
|
5171
|
+
if (def?.name) {
|
|
5172
|
+
const existing = nameToCoins.get(def.name);
|
|
5173
|
+
if (existing) {
|
|
5174
|
+
existing.push(asset.coinId);
|
|
5175
|
+
} else {
|
|
5176
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
if (nameToCoins.size > 0) {
|
|
5181
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
5182
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5183
|
+
return rawAssets.map((raw) => {
|
|
5184
|
+
const def = registry.getDefinition(raw.coinId);
|
|
5185
|
+
const price = def?.name ? prices.get(def.name) : void 0;
|
|
5186
|
+
let fiatValueUsd = null;
|
|
5187
|
+
let fiatValueEur = null;
|
|
5188
|
+
if (price) {
|
|
5189
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5190
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5191
|
+
if (price.priceEur != null) {
|
|
5192
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
return {
|
|
5196
|
+
...raw,
|
|
5197
|
+
priceUsd: price?.priceUsd ?? null,
|
|
5198
|
+
priceEur: price?.priceEur ?? null,
|
|
5199
|
+
change24h: price?.change24h ?? null,
|
|
5200
|
+
fiatValueUsd,
|
|
5201
|
+
fiatValueEur
|
|
5202
|
+
};
|
|
5203
|
+
});
|
|
5204
|
+
}
|
|
5205
|
+
} catch (error) {
|
|
5206
|
+
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5207
|
+
}
|
|
5208
|
+
return rawAssets;
|
|
5209
|
+
}
|
|
5210
|
+
/**
|
|
5211
|
+
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5212
|
+
* Excludes tokens with status 'spent', 'invalid', or 'transferring'.
|
|
5213
|
+
*/
|
|
5214
|
+
aggregateTokens(coinId) {
|
|
4860
5215
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
4861
5216
|
for (const token of this.tokens.values()) {
|
|
4862
|
-
if (token.status
|
|
5217
|
+
if (token.status === "spent" || token.status === "invalid" || token.status === "transferring") continue;
|
|
4863
5218
|
if (coinId && token.coinId !== coinId) continue;
|
|
4864
5219
|
const key = token.coinId;
|
|
5220
|
+
const amount = BigInt(token.amount);
|
|
5221
|
+
const isConfirmed = token.status === "confirmed";
|
|
4865
5222
|
const existing = assetsMap.get(key);
|
|
4866
5223
|
if (existing) {
|
|
4867
|
-
|
|
4868
|
-
|
|
5224
|
+
if (isConfirmed) {
|
|
5225
|
+
existing.confirmedAmount += amount;
|
|
5226
|
+
existing.confirmedTokenCount++;
|
|
5227
|
+
} else {
|
|
5228
|
+
existing.unconfirmedAmount += amount;
|
|
5229
|
+
existing.unconfirmedTokenCount++;
|
|
5230
|
+
}
|
|
4869
5231
|
} else {
|
|
4870
5232
|
assetsMap.set(key, {
|
|
4871
5233
|
coinId: token.coinId,
|
|
@@ -4873,78 +5235,42 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4873
5235
|
name: token.name,
|
|
4874
5236
|
decimals: token.decimals,
|
|
4875
5237
|
iconUrl: token.iconUrl,
|
|
4876
|
-
|
|
4877
|
-
|
|
5238
|
+
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5239
|
+
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5240
|
+
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5241
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
4878
5242
|
});
|
|
4879
5243
|
}
|
|
4880
5244
|
}
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
if (this.priceProvider && rawAssets.length > 0) {
|
|
4884
|
-
try {
|
|
4885
|
-
const registry = TokenRegistry.getInstance();
|
|
4886
|
-
const nameToCoins = /* @__PURE__ */ new Map();
|
|
4887
|
-
for (const asset of rawAssets) {
|
|
4888
|
-
const def = registry.getDefinition(asset.coinId);
|
|
4889
|
-
if (def?.name) {
|
|
4890
|
-
const existing = nameToCoins.get(def.name);
|
|
4891
|
-
if (existing) {
|
|
4892
|
-
existing.push(asset.coinId);
|
|
4893
|
-
} else {
|
|
4894
|
-
nameToCoins.set(def.name, [asset.coinId]);
|
|
4895
|
-
}
|
|
4896
|
-
}
|
|
4897
|
-
}
|
|
4898
|
-
if (nameToCoins.size > 0) {
|
|
4899
|
-
const tokenNames = Array.from(nameToCoins.keys());
|
|
4900
|
-
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
4901
|
-
priceMap = /* @__PURE__ */ new Map();
|
|
4902
|
-
for (const [name, coinIds] of nameToCoins) {
|
|
4903
|
-
const price = prices.get(name);
|
|
4904
|
-
if (price) {
|
|
4905
|
-
for (const cid of coinIds) {
|
|
4906
|
-
priceMap.set(cid, {
|
|
4907
|
-
priceUsd: price.priceUsd,
|
|
4908
|
-
priceEur: price.priceEur,
|
|
4909
|
-
change24h: price.change24h
|
|
4910
|
-
});
|
|
4911
|
-
}
|
|
4912
|
-
}
|
|
4913
|
-
}
|
|
4914
|
-
}
|
|
4915
|
-
} catch (error) {
|
|
4916
|
-
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
4917
|
-
}
|
|
4918
|
-
}
|
|
4919
|
-
return rawAssets.map((raw) => {
|
|
4920
|
-
const price = priceMap?.get(raw.coinId);
|
|
4921
|
-
let fiatValueUsd = null;
|
|
4922
|
-
let fiatValueEur = null;
|
|
4923
|
-
if (price) {
|
|
4924
|
-
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
4925
|
-
fiatValueUsd = humanAmount * price.priceUsd;
|
|
4926
|
-
if (price.priceEur != null) {
|
|
4927
|
-
fiatValueEur = humanAmount * price.priceEur;
|
|
4928
|
-
}
|
|
4929
|
-
}
|
|
5245
|
+
return Array.from(assetsMap.values()).map((raw) => {
|
|
5246
|
+
const totalAmount = (raw.confirmedAmount + raw.unconfirmedAmount).toString();
|
|
4930
5247
|
return {
|
|
4931
5248
|
coinId: raw.coinId,
|
|
4932
5249
|
symbol: raw.symbol,
|
|
4933
5250
|
name: raw.name,
|
|
4934
5251
|
decimals: raw.decimals,
|
|
4935
5252
|
iconUrl: raw.iconUrl,
|
|
4936
|
-
totalAmount
|
|
4937
|
-
tokenCount: raw.
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
5253
|
+
totalAmount,
|
|
5254
|
+
tokenCount: raw.confirmedTokenCount + raw.unconfirmedTokenCount,
|
|
5255
|
+
confirmedAmount: raw.confirmedAmount.toString(),
|
|
5256
|
+
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5257
|
+
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5258
|
+
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
5259
|
+
priceUsd: null,
|
|
5260
|
+
priceEur: null,
|
|
5261
|
+
change24h: null,
|
|
5262
|
+
fiatValueUsd: null,
|
|
5263
|
+
fiatValueEur: null
|
|
4943
5264
|
};
|
|
4944
5265
|
});
|
|
4945
5266
|
}
|
|
4946
5267
|
/**
|
|
4947
|
-
* Get all tokens
|
|
5268
|
+
* Get all tokens, optionally filtered by coin type and/or status.
|
|
5269
|
+
*
|
|
5270
|
+
* @param filter - Optional filter criteria.
|
|
5271
|
+
* @param filter.coinId - Return only tokens of this coin type.
|
|
5272
|
+
* @param filter.status - Return only tokens with this status (e.g. `'submitted'` for unconfirmed).
|
|
5273
|
+
* @returns Array of matching {@link Token} objects (synchronous).
|
|
4948
5274
|
*/
|
|
4949
5275
|
getTokens(filter) {
|
|
4950
5276
|
let tokens = Array.from(this.tokens.values());
|
|
@@ -4957,19 +5283,327 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4957
5283
|
return tokens;
|
|
4958
5284
|
}
|
|
4959
5285
|
/**
|
|
4960
|
-
* Get single token
|
|
5286
|
+
* Get a single token by its local ID.
|
|
5287
|
+
*
|
|
5288
|
+
* @param id - The local UUID assigned when the token was added.
|
|
5289
|
+
* @returns The token, or `undefined` if not found.
|
|
4961
5290
|
*/
|
|
4962
5291
|
getToken(id) {
|
|
4963
5292
|
return this.tokens.get(id);
|
|
4964
5293
|
}
|
|
4965
5294
|
// ===========================================================================
|
|
5295
|
+
// Public API - Unconfirmed Token Resolution
|
|
5296
|
+
// ===========================================================================
|
|
5297
|
+
/**
|
|
5298
|
+
* Attempt to resolve unconfirmed (status `'submitted'`) tokens by acquiring
|
|
5299
|
+
* their missing aggregator proofs.
|
|
5300
|
+
*
|
|
5301
|
+
* Each unconfirmed V5 token progresses through stages:
|
|
5302
|
+
* `RECEIVED` → `MINT_SUBMITTED` → `MINT_PROVEN` → `TRANSFER_SUBMITTED` → `FINALIZED`
|
|
5303
|
+
*
|
|
5304
|
+
* Uses 500 ms quick-timeouts per proof check so the call returns quickly even
|
|
5305
|
+
* when proofs are not yet available. Tokens that exceed 50 failed attempts are
|
|
5306
|
+
* marked `'invalid'`.
|
|
5307
|
+
*
|
|
5308
|
+
* Automatically called (fire-and-forget) by {@link load}.
|
|
5309
|
+
*
|
|
5310
|
+
* @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
|
|
5311
|
+
*/
|
|
5312
|
+
async resolveUnconfirmed() {
|
|
5313
|
+
this.ensureInitialized();
|
|
5314
|
+
const result = {
|
|
5315
|
+
resolved: 0,
|
|
5316
|
+
stillPending: 0,
|
|
5317
|
+
failed: 0,
|
|
5318
|
+
details: []
|
|
5319
|
+
};
|
|
5320
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5321
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5322
|
+
if (!stClient || !trustBase) return result;
|
|
5323
|
+
const signingService = await this.createSigningService();
|
|
5324
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5325
|
+
if (token.status !== "submitted") continue;
|
|
5326
|
+
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5327
|
+
if (!pending2) {
|
|
5328
|
+
result.stillPending++;
|
|
5329
|
+
continue;
|
|
5330
|
+
}
|
|
5331
|
+
if (pending2.type === "v5_bundle") {
|
|
5332
|
+
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
5333
|
+
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5334
|
+
if (progress === "resolved") result.resolved++;
|
|
5335
|
+
else if (progress === "failed") result.failed++;
|
|
5336
|
+
else result.stillPending++;
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
if (result.resolved > 0 || result.failed > 0) {
|
|
5340
|
+
await this.save();
|
|
5341
|
+
}
|
|
5342
|
+
return result;
|
|
5343
|
+
}
|
|
5344
|
+
// ===========================================================================
|
|
5345
|
+
// Private - V5 Lazy Resolution Helpers
|
|
5346
|
+
// ===========================================================================
|
|
5347
|
+
/**
|
|
5348
|
+
* Process a single V5 token through its finalization stages with quick-timeout proof checks.
|
|
5349
|
+
*/
|
|
5350
|
+
async resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService) {
|
|
5351
|
+
const bundle = JSON.parse(pending2.bundleJson);
|
|
5352
|
+
pending2.attemptCount++;
|
|
5353
|
+
pending2.lastAttemptAt = Date.now();
|
|
5354
|
+
try {
|
|
5355
|
+
if (pending2.stage === "RECEIVED") {
|
|
5356
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5357
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5358
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5359
|
+
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
5360
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5361
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5362
|
+
}
|
|
5363
|
+
pending2.stage = "MINT_SUBMITTED";
|
|
5364
|
+
this.updatePendingFinalization(token, pending2);
|
|
5365
|
+
}
|
|
5366
|
+
if (pending2.stage === "MINT_SUBMITTED") {
|
|
5367
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5368
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5369
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5370
|
+
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5371
|
+
if (!proof) {
|
|
5372
|
+
this.updatePendingFinalization(token, pending2);
|
|
5373
|
+
return "pending";
|
|
5374
|
+
}
|
|
5375
|
+
pending2.mintProofJson = JSON.stringify(proof);
|
|
5376
|
+
pending2.stage = "MINT_PROVEN";
|
|
5377
|
+
this.updatePendingFinalization(token, pending2);
|
|
5378
|
+
}
|
|
5379
|
+
if (pending2.stage === "MINT_PROVEN") {
|
|
5380
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5381
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5382
|
+
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
5383
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5384
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5385
|
+
}
|
|
5386
|
+
pending2.stage = "TRANSFER_SUBMITTED";
|
|
5387
|
+
this.updatePendingFinalization(token, pending2);
|
|
5388
|
+
}
|
|
5389
|
+
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
5390
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5391
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5392
|
+
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5393
|
+
if (!proof) {
|
|
5394
|
+
this.updatePendingFinalization(token, pending2);
|
|
5395
|
+
return "pending";
|
|
5396
|
+
}
|
|
5397
|
+
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5398
|
+
const confirmedToken = {
|
|
5399
|
+
id: token.id,
|
|
5400
|
+
coinId: token.coinId,
|
|
5401
|
+
symbol: token.symbol,
|
|
5402
|
+
name: token.name,
|
|
5403
|
+
decimals: token.decimals,
|
|
5404
|
+
iconUrl: token.iconUrl,
|
|
5405
|
+
amount: token.amount,
|
|
5406
|
+
status: "confirmed",
|
|
5407
|
+
createdAt: token.createdAt,
|
|
5408
|
+
updatedAt: Date.now(),
|
|
5409
|
+
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5410
|
+
};
|
|
5411
|
+
this.tokens.set(tokenId, confirmedToken);
|
|
5412
|
+
await this.saveTokenToFileStorage(confirmedToken);
|
|
5413
|
+
await this.addToHistory({
|
|
5414
|
+
type: "RECEIVED",
|
|
5415
|
+
amount: confirmedToken.amount,
|
|
5416
|
+
coinId: confirmedToken.coinId,
|
|
5417
|
+
symbol: confirmedToken.symbol || "UNK",
|
|
5418
|
+
timestamp: Date.now(),
|
|
5419
|
+
senderPubkey: pending2.senderPubkey
|
|
5420
|
+
});
|
|
5421
|
+
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5422
|
+
return "resolved";
|
|
5423
|
+
}
|
|
5424
|
+
return "pending";
|
|
5425
|
+
} catch (error) {
|
|
5426
|
+
console.error(`[Payments] resolveV5Token failed for ${tokenId.slice(0, 8)}:`, error);
|
|
5427
|
+
if (pending2.attemptCount > 50) {
|
|
5428
|
+
token.status = "invalid";
|
|
5429
|
+
token.updatedAt = Date.now();
|
|
5430
|
+
this.tokens.set(tokenId, token);
|
|
5431
|
+
return "failed";
|
|
5432
|
+
}
|
|
5433
|
+
this.updatePendingFinalization(token, pending2);
|
|
5434
|
+
return "pending";
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
5437
|
+
/**
|
|
5438
|
+
* Non-blocking proof check with 500ms timeout.
|
|
5439
|
+
*/
|
|
5440
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5441
|
+
async quickProofCheck(stClient, trustBase, commitment, timeoutMs = 500) {
|
|
5442
|
+
try {
|
|
5443
|
+
const proof = await Promise.race([
|
|
5444
|
+
waitInclusionProof5(trustBase, stClient, commitment),
|
|
5445
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
|
5446
|
+
]);
|
|
5447
|
+
return proof;
|
|
5448
|
+
} catch {
|
|
5449
|
+
return null;
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
/**
|
|
5453
|
+
* Perform V5 bundle finalization from stored bundle data and proofs.
|
|
5454
|
+
* Extracted from InstantSplitProcessor.processV5Bundle() steps 4-10.
|
|
5455
|
+
*/
|
|
5456
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5457
|
+
async finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase) {
|
|
5458
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5459
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5460
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5461
|
+
const mintProofJson = JSON.parse(pending2.mintProofJson);
|
|
5462
|
+
const mintProof = InclusionProof.fromJSON(mintProofJson);
|
|
5463
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
5464
|
+
const tokenType = new TokenType3(fromHex4(bundle.tokenTypeHex));
|
|
5465
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
5466
|
+
const tokenJson = {
|
|
5467
|
+
version: "2.0",
|
|
5468
|
+
state: senderMintedStateJson,
|
|
5469
|
+
genesis: mintTransaction.toJSON(),
|
|
5470
|
+
transactions: [],
|
|
5471
|
+
nametags: []
|
|
5472
|
+
};
|
|
5473
|
+
const mintedToken = await SdkToken2.fromJSON(tokenJson);
|
|
5474
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5475
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5476
|
+
const transferProof = await waitInclusionProof5(trustBase, stClient, transferCommitment);
|
|
5477
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
5478
|
+
const transferSalt = fromHex4(bundle.transferSaltHex);
|
|
5479
|
+
const recipientPredicate = await UnmaskedPredicate5.create(
|
|
5480
|
+
mintData.tokenId,
|
|
5481
|
+
tokenType,
|
|
5482
|
+
signingService,
|
|
5483
|
+
HashAlgorithm5.SHA256,
|
|
5484
|
+
transferSalt
|
|
5485
|
+
);
|
|
5486
|
+
const recipientState = new TokenState5(recipientPredicate, null);
|
|
5487
|
+
let nametagTokens = [];
|
|
5488
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
5489
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
5490
|
+
if (bundle.nametagTokenJson) {
|
|
5491
|
+
try {
|
|
5492
|
+
const nametagToken = await SdkToken2.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
5493
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5494
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5495
|
+
if (proxy.address === recipientAddressStr) {
|
|
5496
|
+
nametagTokens = [nametagToken];
|
|
5497
|
+
}
|
|
5498
|
+
} catch {
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5501
|
+
if (nametagTokens.length === 0 && this.nametag?.token) {
|
|
5502
|
+
try {
|
|
5503
|
+
const nametagToken = await SdkToken2.fromJSON(this.nametag.token);
|
|
5504
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5505
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5506
|
+
if (proxy.address === recipientAddressStr) {
|
|
5507
|
+
nametagTokens = [nametagToken];
|
|
5508
|
+
}
|
|
5509
|
+
} catch {
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
return stClient.finalizeTransaction(trustBase, mintedToken, recipientState, transferTransaction, nametagTokens);
|
|
5514
|
+
}
|
|
5515
|
+
/**
|
|
5516
|
+
* Parse pending finalization metadata from token's sdkData.
|
|
5517
|
+
*/
|
|
5518
|
+
parsePendingFinalization(sdkData) {
|
|
5519
|
+
if (!sdkData) return null;
|
|
5520
|
+
try {
|
|
5521
|
+
const data = JSON.parse(sdkData);
|
|
5522
|
+
if (data._pendingFinalization && data._pendingFinalization.type === "v5_bundle") {
|
|
5523
|
+
return data._pendingFinalization;
|
|
5524
|
+
}
|
|
5525
|
+
return null;
|
|
5526
|
+
} catch {
|
|
5527
|
+
return null;
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
/**
|
|
5531
|
+
* Update pending finalization metadata in token's sdkData.
|
|
5532
|
+
* Creates a new token object since sdkData is readonly.
|
|
5533
|
+
*/
|
|
5534
|
+
updatePendingFinalization(token, pending2) {
|
|
5535
|
+
const updated = {
|
|
5536
|
+
id: token.id,
|
|
5537
|
+
coinId: token.coinId,
|
|
5538
|
+
symbol: token.symbol,
|
|
5539
|
+
name: token.name,
|
|
5540
|
+
decimals: token.decimals,
|
|
5541
|
+
iconUrl: token.iconUrl,
|
|
5542
|
+
amount: token.amount,
|
|
5543
|
+
status: token.status,
|
|
5544
|
+
createdAt: token.createdAt,
|
|
5545
|
+
updatedAt: Date.now(),
|
|
5546
|
+
sdkData: JSON.stringify({ _pendingFinalization: pending2 })
|
|
5547
|
+
};
|
|
5548
|
+
this.tokens.set(token.id, updated);
|
|
5549
|
+
}
|
|
5550
|
+
/**
|
|
5551
|
+
* Save pending V5 tokens to key-value storage.
|
|
5552
|
+
* These tokens can't be serialized to TXF format (no genesis/state),
|
|
5553
|
+
* so we persist them separately and restore on load().
|
|
5554
|
+
*/
|
|
5555
|
+
async savePendingV5Tokens() {
|
|
5556
|
+
const pendingTokens = [];
|
|
5557
|
+
for (const token of this.tokens.values()) {
|
|
5558
|
+
if (this.parsePendingFinalization(token.sdkData)) {
|
|
5559
|
+
pendingTokens.push(token);
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
if (pendingTokens.length > 0) {
|
|
5563
|
+
await this.deps.storage.set(
|
|
5564
|
+
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
5565
|
+
JSON.stringify(pendingTokens)
|
|
5566
|
+
);
|
|
5567
|
+
} else {
|
|
5568
|
+
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
/**
|
|
5572
|
+
* Load pending V5 tokens from key-value storage and merge into tokens map.
|
|
5573
|
+
* Called during load() to restore tokens that TXF format can't represent.
|
|
5574
|
+
*/
|
|
5575
|
+
async loadPendingV5Tokens() {
|
|
5576
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
5577
|
+
if (!data) return;
|
|
5578
|
+
try {
|
|
5579
|
+
const pendingTokens = JSON.parse(data);
|
|
5580
|
+
for (const token of pendingTokens) {
|
|
5581
|
+
if (!this.tokens.has(token.id)) {
|
|
5582
|
+
this.tokens.set(token.id, token);
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
if (pendingTokens.length > 0) {
|
|
5586
|
+
this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
|
|
5587
|
+
}
|
|
5588
|
+
} catch {
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
// ===========================================================================
|
|
4966
5592
|
// Public API - Token Operations
|
|
4967
5593
|
// ===========================================================================
|
|
4968
5594
|
/**
|
|
4969
|
-
* Add a token
|
|
4970
|
-
*
|
|
4971
|
-
*
|
|
4972
|
-
*
|
|
5595
|
+
* Add a token to the wallet.
|
|
5596
|
+
*
|
|
5597
|
+
* Tokens are uniquely identified by a `(tokenId, stateHash)` composite key.
|
|
5598
|
+
* Duplicate detection:
|
|
5599
|
+
* - **Tombstoned** — rejected if the exact `(tokenId, stateHash)` pair has a tombstone.
|
|
5600
|
+
* - **Exact duplicate** — rejected if a token with the same composite key already exists.
|
|
5601
|
+
* - **State replacement** — if the same `tokenId` exists with a *different* `stateHash`,
|
|
5602
|
+
* the old state is archived and replaced with the incoming one.
|
|
5603
|
+
*
|
|
5604
|
+
* @param token - The token to add.
|
|
5605
|
+
* @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
|
|
5606
|
+
* @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
|
|
4973
5607
|
*/
|
|
4974
5608
|
async addToken(token, skipHistory = false) {
|
|
4975
5609
|
this.ensureInitialized();
|
|
@@ -5027,7 +5661,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5027
5661
|
});
|
|
5028
5662
|
}
|
|
5029
5663
|
await this.save();
|
|
5030
|
-
|
|
5664
|
+
if (!this.parsePendingFinalization(token.sdkData)) {
|
|
5665
|
+
await this.saveTokenToFileStorage(token);
|
|
5666
|
+
}
|
|
5031
5667
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
5032
5668
|
return true;
|
|
5033
5669
|
}
|
|
@@ -5084,6 +5720,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5084
5720
|
const data = fileData;
|
|
5085
5721
|
const tokenJson = data.token;
|
|
5086
5722
|
if (!tokenJson) continue;
|
|
5723
|
+
if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
|
|
5724
|
+
continue;
|
|
5725
|
+
}
|
|
5087
5726
|
let sdkTokenId;
|
|
5088
5727
|
if (typeof tokenJson === "object" && tokenJson !== null) {
|
|
5089
5728
|
const tokenObj = tokenJson;
|
|
@@ -5135,7 +5774,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5135
5774
|
this.log(`Loaded ${this.tokens.size} tokens from file storage`);
|
|
5136
5775
|
}
|
|
5137
5776
|
/**
|
|
5138
|
-
* Update an existing token
|
|
5777
|
+
* Update an existing token or add it if not found.
|
|
5778
|
+
*
|
|
5779
|
+
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
5780
|
+
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
5781
|
+
*
|
|
5782
|
+
* @param token - The token with updated data. Must include a valid `id`.
|
|
5139
5783
|
*/
|
|
5140
5784
|
async updateToken(token) {
|
|
5141
5785
|
this.ensureInitialized();
|
|
@@ -5159,7 +5803,15 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5159
5803
|
this.log(`Updated token ${token.id}`);
|
|
5160
5804
|
}
|
|
5161
5805
|
/**
|
|
5162
|
-
* Remove a token
|
|
5806
|
+
* Remove a token from the wallet.
|
|
5807
|
+
*
|
|
5808
|
+
* The token is archived first, then a tombstone `(tokenId, stateHash)` is
|
|
5809
|
+
* created to prevent re-addition via Nostr re-delivery. A `SENT` history
|
|
5810
|
+
* entry is created unless `skipHistory` is `true`.
|
|
5811
|
+
*
|
|
5812
|
+
* @param tokenId - Local UUID of the token to remove.
|
|
5813
|
+
* @param recipientNametag - Optional nametag of the transfer recipient (for history).
|
|
5814
|
+
* @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
|
|
5163
5815
|
*/
|
|
5164
5816
|
async removeToken(tokenId, recipientNametag, skipHistory = false) {
|
|
5165
5817
|
this.ensureInitialized();
|
|
@@ -5221,13 +5873,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5221
5873
|
// Public API - Tombstones
|
|
5222
5874
|
// ===========================================================================
|
|
5223
5875
|
/**
|
|
5224
|
-
* Get all
|
|
5876
|
+
* Get all tombstone entries.
|
|
5877
|
+
*
|
|
5878
|
+
* Each tombstone is keyed by `(tokenId, stateHash)` and prevents a spent
|
|
5879
|
+
* token state from being re-added (e.g. via Nostr re-delivery).
|
|
5880
|
+
*
|
|
5881
|
+
* @returns A shallow copy of the tombstone array.
|
|
5225
5882
|
*/
|
|
5226
5883
|
getTombstones() {
|
|
5227
5884
|
return [...this.tombstones];
|
|
5228
5885
|
}
|
|
5229
5886
|
/**
|
|
5230
|
-
* Check
|
|
5887
|
+
* Check whether a specific `(tokenId, stateHash)` combination is tombstoned.
|
|
5888
|
+
*
|
|
5889
|
+
* @param tokenId - The genesis token ID.
|
|
5890
|
+
* @param stateHash - The state hash of the token version to check.
|
|
5891
|
+
* @returns `true` if the exact combination has been tombstoned.
|
|
5231
5892
|
*/
|
|
5232
5893
|
isStateTombstoned(tokenId, stateHash) {
|
|
5233
5894
|
return this.tombstones.some(
|
|
@@ -5235,8 +5896,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5235
5896
|
);
|
|
5236
5897
|
}
|
|
5237
5898
|
/**
|
|
5238
|
-
* Merge remote
|
|
5239
|
-
*
|
|
5899
|
+
* Merge tombstones received from a remote sync source.
|
|
5900
|
+
*
|
|
5901
|
+
* Any local token whose `(tokenId, stateHash)` matches a remote tombstone is
|
|
5902
|
+
* removed. The remote tombstones are then added to the local set (union merge).
|
|
5903
|
+
*
|
|
5904
|
+
* @param remoteTombstones - Tombstone entries from the remote source.
|
|
5905
|
+
* @returns Number of local tokens that were removed.
|
|
5240
5906
|
*/
|
|
5241
5907
|
async mergeTombstones(remoteTombstones) {
|
|
5242
5908
|
this.ensureInitialized();
|
|
@@ -5272,7 +5938,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5272
5938
|
return removedCount;
|
|
5273
5939
|
}
|
|
5274
5940
|
/**
|
|
5275
|
-
*
|
|
5941
|
+
* Remove tombstones older than `maxAge` and cap the list at 100 entries.
|
|
5942
|
+
*
|
|
5943
|
+
* @param maxAge - Maximum age in milliseconds (default: 30 days).
|
|
5276
5944
|
*/
|
|
5277
5945
|
async pruneTombstones(maxAge) {
|
|
5278
5946
|
const originalCount = this.tombstones.length;
|
|
@@ -5286,20 +5954,38 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5286
5954
|
// Public API - Archives
|
|
5287
5955
|
// ===========================================================================
|
|
5288
5956
|
/**
|
|
5289
|
-
* Get archived tokens
|
|
5957
|
+
* Get all archived (spent/superseded) tokens in TXF format.
|
|
5958
|
+
*
|
|
5959
|
+
* Archived tokens are kept for recovery and sync purposes. The map key is
|
|
5960
|
+
* the genesis token ID.
|
|
5961
|
+
*
|
|
5962
|
+
* @returns A shallow copy of the archived token map.
|
|
5290
5963
|
*/
|
|
5291
5964
|
getArchivedTokens() {
|
|
5292
5965
|
return new Map(this.archivedTokens);
|
|
5293
5966
|
}
|
|
5294
5967
|
/**
|
|
5295
|
-
* Get best archived version of a token
|
|
5968
|
+
* Get the best (most committed transactions) archived version of a token.
|
|
5969
|
+
*
|
|
5970
|
+
* Searches both archived and forked token maps and returns the version with
|
|
5971
|
+
* the highest number of committed transactions.
|
|
5972
|
+
*
|
|
5973
|
+
* @param tokenId - The genesis token ID to look up.
|
|
5974
|
+
* @returns The best TXF token version, or `null` if not found.
|
|
5296
5975
|
*/
|
|
5297
5976
|
getBestArchivedVersion(tokenId) {
|
|
5298
5977
|
return findBestTokenVersion(tokenId, this.archivedTokens, this.forkedTokens);
|
|
5299
5978
|
}
|
|
5300
5979
|
/**
|
|
5301
|
-
* Merge remote
|
|
5302
|
-
*
|
|
5980
|
+
* Merge archived tokens from a remote sync source.
|
|
5981
|
+
*
|
|
5982
|
+
* For each remote token:
|
|
5983
|
+
* - If missing locally, it is added.
|
|
5984
|
+
* - If the remote version is an incremental update of the local, it replaces it.
|
|
5985
|
+
* - If the histories diverge (fork), the remote version is stored via {@link storeForkedToken}.
|
|
5986
|
+
*
|
|
5987
|
+
* @param remoteArchived - Map of genesis token ID → TXF token from remote.
|
|
5988
|
+
* @returns Number of tokens that were updated or added locally.
|
|
5303
5989
|
*/
|
|
5304
5990
|
async mergeArchivedTokens(remoteArchived) {
|
|
5305
5991
|
let mergedCount = 0;
|
|
@@ -5322,7 +6008,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5322
6008
|
return mergedCount;
|
|
5323
6009
|
}
|
|
5324
6010
|
/**
|
|
5325
|
-
* Prune archived tokens
|
|
6011
|
+
* Prune archived tokens to keep at most `maxCount` entries.
|
|
6012
|
+
*
|
|
6013
|
+
* Oldest entries (by insertion order) are removed first.
|
|
6014
|
+
*
|
|
6015
|
+
* @param maxCount - Maximum number of archived tokens to retain (default: 100).
|
|
5326
6016
|
*/
|
|
5327
6017
|
async pruneArchivedTokens(maxCount = 100) {
|
|
5328
6018
|
if (this.archivedTokens.size <= maxCount) return;
|
|
@@ -5335,13 +6025,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5335
6025
|
// Public API - Forked Tokens
|
|
5336
6026
|
// ===========================================================================
|
|
5337
6027
|
/**
|
|
5338
|
-
* Get forked
|
|
6028
|
+
* Get all forked token versions.
|
|
6029
|
+
*
|
|
6030
|
+
* Forked tokens represent alternative histories detected during sync.
|
|
6031
|
+
* The map key is `{tokenId}_{stateHash}`.
|
|
6032
|
+
*
|
|
6033
|
+
* @returns A shallow copy of the forked tokens map.
|
|
5339
6034
|
*/
|
|
5340
6035
|
getForkedTokens() {
|
|
5341
6036
|
return new Map(this.forkedTokens);
|
|
5342
6037
|
}
|
|
5343
6038
|
/**
|
|
5344
|
-
* Store a forked token
|
|
6039
|
+
* Store a forked token version (alternative history).
|
|
6040
|
+
*
|
|
6041
|
+
* No-op if the exact `(tokenId, stateHash)` key already exists.
|
|
6042
|
+
*
|
|
6043
|
+
* @param tokenId - Genesis token ID.
|
|
6044
|
+
* @param stateHash - State hash of this forked version.
|
|
6045
|
+
* @param txfToken - The TXF token data to store.
|
|
5345
6046
|
*/
|
|
5346
6047
|
async storeForkedToken(tokenId, stateHash, txfToken) {
|
|
5347
6048
|
const key = `${tokenId}_${stateHash}`;
|
|
@@ -5351,8 +6052,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5351
6052
|
await this.save();
|
|
5352
6053
|
}
|
|
5353
6054
|
/**
|
|
5354
|
-
* Merge remote
|
|
5355
|
-
*
|
|
6055
|
+
* Merge forked tokens from a remote sync source. Only new keys are added.
|
|
6056
|
+
*
|
|
6057
|
+
* @param remoteForked - Map of `{tokenId}_{stateHash}` → TXF token from remote.
|
|
6058
|
+
* @returns Number of new forked tokens added.
|
|
5356
6059
|
*/
|
|
5357
6060
|
async mergeForkedTokens(remoteForked) {
|
|
5358
6061
|
let addedCount = 0;
|
|
@@ -5368,7 +6071,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5368
6071
|
return addedCount;
|
|
5369
6072
|
}
|
|
5370
6073
|
/**
|
|
5371
|
-
* Prune forked tokens
|
|
6074
|
+
* Prune forked tokens to keep at most `maxCount` entries.
|
|
6075
|
+
*
|
|
6076
|
+
* @param maxCount - Maximum number of forked tokens to retain (default: 50).
|
|
5372
6077
|
*/
|
|
5373
6078
|
async pruneForkedTokens(maxCount = 50) {
|
|
5374
6079
|
if (this.forkedTokens.size <= maxCount) return;
|
|
@@ -5381,13 +6086,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5381
6086
|
// Public API - Transaction History
|
|
5382
6087
|
// ===========================================================================
|
|
5383
6088
|
/**
|
|
5384
|
-
* Get transaction history
|
|
6089
|
+
* Get the transaction history sorted newest-first.
|
|
6090
|
+
*
|
|
6091
|
+
* @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
|
|
5385
6092
|
*/
|
|
5386
6093
|
getHistory() {
|
|
5387
6094
|
return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
|
|
5388
6095
|
}
|
|
5389
6096
|
/**
|
|
5390
|
-
*
|
|
6097
|
+
* Append an entry to the transaction history.
|
|
6098
|
+
*
|
|
6099
|
+
* A unique `id` is auto-generated. The entry is immediately persisted to storage.
|
|
6100
|
+
*
|
|
6101
|
+
* @param entry - History entry fields (without `id`).
|
|
5391
6102
|
*/
|
|
5392
6103
|
async addToHistory(entry) {
|
|
5393
6104
|
this.ensureInitialized();
|
|
@@ -5405,7 +6116,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5405
6116
|
// Public API - Nametag
|
|
5406
6117
|
// ===========================================================================
|
|
5407
6118
|
/**
|
|
5408
|
-
* Set nametag for current identity
|
|
6119
|
+
* Set the nametag data for the current identity.
|
|
6120
|
+
*
|
|
6121
|
+
* Persists to both key-value storage and file storage (lottery compatibility).
|
|
6122
|
+
*
|
|
6123
|
+
* @param nametag - The nametag data including minted token JSON.
|
|
5409
6124
|
*/
|
|
5410
6125
|
async setNametag(nametag) {
|
|
5411
6126
|
this.ensureInitialized();
|
|
@@ -5415,19 +6130,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5415
6130
|
this.log(`Nametag set: ${nametag.name}`);
|
|
5416
6131
|
}
|
|
5417
6132
|
/**
|
|
5418
|
-
* Get nametag
|
|
6133
|
+
* Get the current nametag data.
|
|
6134
|
+
*
|
|
6135
|
+
* @returns The nametag data, or `null` if no nametag is set.
|
|
5419
6136
|
*/
|
|
5420
6137
|
getNametag() {
|
|
5421
6138
|
return this.nametag;
|
|
5422
6139
|
}
|
|
5423
6140
|
/**
|
|
5424
|
-
* Check
|
|
6141
|
+
* Check whether a nametag is currently set.
|
|
6142
|
+
*
|
|
6143
|
+
* @returns `true` if nametag data is present.
|
|
5425
6144
|
*/
|
|
5426
6145
|
hasNametag() {
|
|
5427
6146
|
return this.nametag !== null;
|
|
5428
6147
|
}
|
|
5429
6148
|
/**
|
|
5430
|
-
*
|
|
6149
|
+
* Remove the current nametag data from memory and storage.
|
|
5431
6150
|
*/
|
|
5432
6151
|
async clearNametag() {
|
|
5433
6152
|
this.ensureInitialized();
|
|
@@ -5521,9 +6240,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5521
6240
|
try {
|
|
5522
6241
|
const signingService = await this.createSigningService();
|
|
5523
6242
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5524
|
-
const { TokenType:
|
|
6243
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5525
6244
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5526
|
-
const tokenType = new
|
|
6245
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5527
6246
|
const addressRef = await UnmaskedPredicateReference4.create(
|
|
5528
6247
|
tokenType,
|
|
5529
6248
|
signingService.algorithm,
|
|
@@ -5584,11 +6303,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5584
6303
|
// Public API - Sync & Validate
|
|
5585
6304
|
// ===========================================================================
|
|
5586
6305
|
/**
|
|
5587
|
-
* Sync with all token storage providers (IPFS,
|
|
5588
|
-
*
|
|
6306
|
+
* Sync local token state with all configured token storage providers (IPFS, file, etc.).
|
|
6307
|
+
*
|
|
6308
|
+
* For each provider, the local data is packaged into TXF storage format, sent
|
|
6309
|
+
* to the provider's `sync()` method, and the merged result is applied locally.
|
|
6310
|
+
* Emits `sync:started`, `sync:completed`, and `sync:error` events.
|
|
6311
|
+
*
|
|
6312
|
+
* @returns Summary with counts of tokens added and removed during sync.
|
|
5589
6313
|
*/
|
|
5590
6314
|
async sync() {
|
|
5591
6315
|
this.ensureInitialized();
|
|
6316
|
+
if (this._syncInProgress) {
|
|
6317
|
+
return this._syncInProgress;
|
|
6318
|
+
}
|
|
6319
|
+
this._syncInProgress = this._doSync();
|
|
6320
|
+
try {
|
|
6321
|
+
return await this._syncInProgress;
|
|
6322
|
+
} finally {
|
|
6323
|
+
this._syncInProgress = null;
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
async _doSync() {
|
|
5592
6327
|
this.deps.emitEvent("sync:started", { source: "payments" });
|
|
5593
6328
|
try {
|
|
5594
6329
|
const providers = this.getTokenStorageProviders();
|
|
@@ -5626,6 +6361,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5626
6361
|
});
|
|
5627
6362
|
}
|
|
5628
6363
|
}
|
|
6364
|
+
if (totalAdded > 0 || totalRemoved > 0) {
|
|
6365
|
+
await this.save();
|
|
6366
|
+
}
|
|
5629
6367
|
this.deps.emitEvent("sync:completed", {
|
|
5630
6368
|
source: "payments",
|
|
5631
6369
|
count: this.tokens.size
|
|
@@ -5639,6 +6377,66 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5639
6377
|
throw error;
|
|
5640
6378
|
}
|
|
5641
6379
|
}
|
|
6380
|
+
// ===========================================================================
|
|
6381
|
+
// Storage Event Subscription (Push-Based Sync)
|
|
6382
|
+
// ===========================================================================
|
|
6383
|
+
/**
|
|
6384
|
+
* Subscribe to 'storage:remote-updated' events from all token storage providers.
|
|
6385
|
+
* When a provider emits this event, a debounced sync is triggered.
|
|
6386
|
+
*/
|
|
6387
|
+
subscribeToStorageEvents() {
|
|
6388
|
+
this.unsubscribeStorageEvents();
|
|
6389
|
+
const providers = this.getTokenStorageProviders();
|
|
6390
|
+
for (const [providerId, provider] of providers) {
|
|
6391
|
+
if (provider.onEvent) {
|
|
6392
|
+
const unsub = provider.onEvent((event) => {
|
|
6393
|
+
if (event.type === "storage:remote-updated") {
|
|
6394
|
+
this.log("Remote update detected from provider", providerId, event.data);
|
|
6395
|
+
this.debouncedSyncFromRemoteUpdate(providerId, event.data);
|
|
6396
|
+
}
|
|
6397
|
+
});
|
|
6398
|
+
this.storageEventUnsubscribers.push(unsub);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
/**
|
|
6403
|
+
* Unsubscribe from all storage provider events and clear debounce timer.
|
|
6404
|
+
*/
|
|
6405
|
+
unsubscribeStorageEvents() {
|
|
6406
|
+
for (const unsub of this.storageEventUnsubscribers) {
|
|
6407
|
+
unsub();
|
|
6408
|
+
}
|
|
6409
|
+
this.storageEventUnsubscribers = [];
|
|
6410
|
+
if (this.syncDebounceTimer) {
|
|
6411
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6412
|
+
this.syncDebounceTimer = null;
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
/**
|
|
6416
|
+
* Debounced sync triggered by a storage:remote-updated event.
|
|
6417
|
+
* Waits 500ms to batch rapid updates, then performs sync.
|
|
6418
|
+
*/
|
|
6419
|
+
debouncedSyncFromRemoteUpdate(providerId, eventData) {
|
|
6420
|
+
if (this.syncDebounceTimer) {
|
|
6421
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6422
|
+
}
|
|
6423
|
+
this.syncDebounceTimer = setTimeout(() => {
|
|
6424
|
+
this.syncDebounceTimer = null;
|
|
6425
|
+
this.sync().then((result) => {
|
|
6426
|
+
const data = eventData;
|
|
6427
|
+
this.deps?.emitEvent("sync:remote-update", {
|
|
6428
|
+
providerId,
|
|
6429
|
+
name: data?.name ?? "",
|
|
6430
|
+
sequence: data?.sequence ?? 0,
|
|
6431
|
+
cid: data?.cid ?? "",
|
|
6432
|
+
added: result.added,
|
|
6433
|
+
removed: result.removed
|
|
6434
|
+
});
|
|
6435
|
+
}).catch((err) => {
|
|
6436
|
+
this.log("Auto-sync from remote update failed:", err);
|
|
6437
|
+
});
|
|
6438
|
+
}, _PaymentsModule.SYNC_DEBOUNCE_MS);
|
|
6439
|
+
}
|
|
5642
6440
|
/**
|
|
5643
6441
|
* Get all active token storage providers
|
|
5644
6442
|
*/
|
|
@@ -5654,15 +6452,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5654
6452
|
return /* @__PURE__ */ new Map();
|
|
5655
6453
|
}
|
|
5656
6454
|
/**
|
|
5657
|
-
*
|
|
6455
|
+
* Replace the set of token storage providers at runtime.
|
|
6456
|
+
*
|
|
6457
|
+
* Use when providers are added or removed dynamically (e.g. IPFS node started).
|
|
6458
|
+
*
|
|
6459
|
+
* @param providers - New map of provider ID → TokenStorageProvider.
|
|
5658
6460
|
*/
|
|
5659
6461
|
updateTokenStorageProviders(providers) {
|
|
5660
6462
|
if (this.deps) {
|
|
5661
6463
|
this.deps.tokenStorageProviders = providers;
|
|
6464
|
+
this.subscribeToStorageEvents();
|
|
5662
6465
|
}
|
|
5663
6466
|
}
|
|
5664
6467
|
/**
|
|
5665
|
-
* Validate tokens
|
|
6468
|
+
* Validate all tokens against the aggregator (oracle provider).
|
|
6469
|
+
*
|
|
6470
|
+
* Tokens that fail validation or are detected as spent are marked `'invalid'`.
|
|
6471
|
+
*
|
|
6472
|
+
* @returns Object with arrays of valid and invalid tokens.
|
|
5666
6473
|
*/
|
|
5667
6474
|
async validate() {
|
|
5668
6475
|
this.ensureInitialized();
|
|
@@ -5683,7 +6490,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5683
6490
|
return { valid, invalid };
|
|
5684
6491
|
}
|
|
5685
6492
|
/**
|
|
5686
|
-
* Get pending transfers
|
|
6493
|
+
* Get all in-progress (pending) outgoing transfers.
|
|
6494
|
+
*
|
|
6495
|
+
* @returns Array of {@link TransferResult} objects for transfers that have not yet completed.
|
|
5687
6496
|
*/
|
|
5688
6497
|
getPendingTransfers() {
|
|
5689
6498
|
return Array.from(this.pendingTransfers.values());
|
|
@@ -5747,9 +6556,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5747
6556
|
*/
|
|
5748
6557
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
5749
6558
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5750
|
-
const { TokenType:
|
|
6559
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5751
6560
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5752
|
-
const tokenType = new
|
|
6561
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5753
6562
|
const pubkeyBytes = new Uint8Array(
|
|
5754
6563
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
5755
6564
|
);
|
|
@@ -5961,7 +6770,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5961
6770
|
this.deps.emitEvent("transfer:confirmed", {
|
|
5962
6771
|
id: crypto.randomUUID(),
|
|
5963
6772
|
status: "completed",
|
|
5964
|
-
tokens: [finalizedToken]
|
|
6773
|
+
tokens: [finalizedToken],
|
|
6774
|
+
tokenTransfers: []
|
|
5965
6775
|
});
|
|
5966
6776
|
await this.addToHistory({
|
|
5967
6777
|
type: "RECEIVED",
|
|
@@ -5984,14 +6794,26 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5984
6794
|
async handleIncomingTransfer(transfer) {
|
|
5985
6795
|
try {
|
|
5986
6796
|
const payload = transfer.payload;
|
|
6797
|
+
let instantBundle = null;
|
|
5987
6798
|
if (isInstantSplitBundle(payload)) {
|
|
6799
|
+
instantBundle = payload;
|
|
6800
|
+
} else if (payload.token) {
|
|
6801
|
+
try {
|
|
6802
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
6803
|
+
if (isInstantSplitBundle(inner)) {
|
|
6804
|
+
instantBundle = inner;
|
|
6805
|
+
}
|
|
6806
|
+
} catch {
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
if (instantBundle) {
|
|
5988
6810
|
this.log("Processing INSTANT_SPLIT bundle...");
|
|
5989
6811
|
try {
|
|
5990
6812
|
if (!this.nametag) {
|
|
5991
6813
|
await this.loadNametagFromFileStorage();
|
|
5992
6814
|
}
|
|
5993
6815
|
const result = await this.processInstantSplitBundle(
|
|
5994
|
-
|
|
6816
|
+
instantBundle,
|
|
5995
6817
|
transfer.senderTransportPubkey
|
|
5996
6818
|
);
|
|
5997
6819
|
if (result.success) {
|
|
@@ -6004,6 +6826,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6004
6826
|
}
|
|
6005
6827
|
return;
|
|
6006
6828
|
}
|
|
6829
|
+
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
6830
|
+
this.log("Processing NOSTR-FIRST commitment-only transfer...");
|
|
6831
|
+
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
6007
6834
|
let tokenData;
|
|
6008
6835
|
let finalizedSdkToken = null;
|
|
6009
6836
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -6159,6 +6986,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6159
6986
|
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
6160
6987
|
}
|
|
6161
6988
|
}
|
|
6989
|
+
await this.savePendingV5Tokens();
|
|
6162
6990
|
}
|
|
6163
6991
|
async saveToOutbox(transfer, recipient) {
|
|
6164
6992
|
const outbox = await this.loadOutbox();
|
|
@@ -6176,8 +7004,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6176
7004
|
}
|
|
6177
7005
|
async createStorageData() {
|
|
6178
7006
|
return await buildTxfStorageData(
|
|
6179
|
-
|
|
6180
|
-
// Empty - active tokens stored as token-xxx files
|
|
7007
|
+
Array.from(this.tokens.values()),
|
|
6181
7008
|
{
|
|
6182
7009
|
version: 1,
|
|
6183
7010
|
address: this.deps.identity.l1Address,
|
|
@@ -6362,7 +7189,7 @@ function createPaymentsModule(config) {
|
|
|
6362
7189
|
// modules/payments/TokenRecoveryService.ts
|
|
6363
7190
|
import { TokenId as TokenId4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenId";
|
|
6364
7191
|
import { TokenState as TokenState6 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
6365
|
-
import { TokenType as
|
|
7192
|
+
import { TokenType as TokenType4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
6366
7193
|
import { CoinId as CoinId5 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
|
|
6367
7194
|
import { HashAlgorithm as HashAlgorithm6 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
6368
7195
|
import { UnmaskedPredicate as UnmaskedPredicate6 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
@@ -7511,15 +8338,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
7511
8338
|
|
|
7512
8339
|
// core/Sphere.ts
|
|
7513
8340
|
import { SigningService as SigningService2 } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
|
|
7514
|
-
import { TokenType as
|
|
8341
|
+
import { TokenType as TokenType5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
7515
8342
|
import { HashAlgorithm as HashAlgorithm7 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
7516
8343
|
import { UnmaskedPredicateReference as UnmaskedPredicateReference3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
|
|
8344
|
+
import { normalizeNametag as normalizeNametag2, isPhoneNumber } from "@unicitylabs/nostr-js-sdk";
|
|
8345
|
+
function isValidNametag(nametag) {
|
|
8346
|
+
if (isPhoneNumber(nametag)) return true;
|
|
8347
|
+
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
8348
|
+
}
|
|
7517
8349
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
7518
8350
|
async function deriveL3PredicateAddress(privateKey) {
|
|
7519
8351
|
const secret = Buffer.from(privateKey, "hex");
|
|
7520
8352
|
const signingService = await SigningService2.createFromSecret(secret);
|
|
7521
8353
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
7522
|
-
const tokenType = new
|
|
8354
|
+
const tokenType = new TokenType5(tokenTypeBytes);
|
|
7523
8355
|
const predicateRef = UnmaskedPredicateReference3.create(
|
|
7524
8356
|
tokenType,
|
|
7525
8357
|
signingService.algorithm,
|
|
@@ -7795,6 +8627,14 @@ var Sphere = class _Sphere {
|
|
|
7795
8627
|
console.log("[Sphere.import] Registering nametag...");
|
|
7796
8628
|
await sphere.registerNametag(options.nametag);
|
|
7797
8629
|
}
|
|
8630
|
+
if (sphere._tokenStorageProviders.size > 0) {
|
|
8631
|
+
try {
|
|
8632
|
+
const syncResult = await sphere._payments.sync();
|
|
8633
|
+
console.log(`[Sphere.import] Auto-sync: +${syncResult.added} -${syncResult.removed}`);
|
|
8634
|
+
} catch (err) {
|
|
8635
|
+
console.warn("[Sphere.import] Auto-sync failed (non-fatal):", err);
|
|
8636
|
+
}
|
|
8637
|
+
}
|
|
7798
8638
|
console.log("[Sphere.import] Import complete");
|
|
7799
8639
|
return sphere;
|
|
7800
8640
|
}
|
|
@@ -8665,9 +9505,9 @@ var Sphere = class _Sphere {
|
|
|
8665
9505
|
if (index < 0) {
|
|
8666
9506
|
throw new Error("Address index must be non-negative");
|
|
8667
9507
|
}
|
|
8668
|
-
const newNametag = options?.nametag
|
|
8669
|
-
if (newNametag && !
|
|
8670
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9508
|
+
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
9509
|
+
if (newNametag && !isValidNametag(newNametag)) {
|
|
9510
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
8671
9511
|
}
|
|
8672
9512
|
const addressInfo = this.deriveAddress(index, false);
|
|
8673
9513
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
@@ -9051,9 +9891,9 @@ var Sphere = class _Sphere {
|
|
|
9051
9891
|
*/
|
|
9052
9892
|
async registerNametag(nametag) {
|
|
9053
9893
|
this.ensureReady();
|
|
9054
|
-
const cleanNametag =
|
|
9055
|
-
if (!
|
|
9056
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9894
|
+
const cleanNametag = this.cleanNametag(nametag);
|
|
9895
|
+
if (!isValidNametag(cleanNametag)) {
|
|
9896
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
9057
9897
|
}
|
|
9058
9898
|
if (this._identity?.nametag) {
|
|
9059
9899
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
@@ -9362,13 +10202,11 @@ var Sphere = class _Sphere {
|
|
|
9362
10202
|
}
|
|
9363
10203
|
}
|
|
9364
10204
|
/**
|
|
9365
|
-
*
|
|
10205
|
+
* Strip @ prefix and normalize a nametag (lowercase, phone E.164, strip @unicity suffix).
|
|
9366
10206
|
*/
|
|
9367
|
-
|
|
9368
|
-
const
|
|
9369
|
-
|
|
9370
|
-
);
|
|
9371
|
-
return pattern.test(nametag);
|
|
10207
|
+
cleanNametag(raw) {
|
|
10208
|
+
const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
10209
|
+
return normalizeNametag2(stripped);
|
|
9372
10210
|
}
|
|
9373
10211
|
// ===========================================================================
|
|
9374
10212
|
// Public Methods - Lifecycle
|
|
@@ -9728,6 +10566,7 @@ export {
|
|
|
9728
10566
|
initSphere,
|
|
9729
10567
|
isEncryptedData,
|
|
9730
10568
|
isValidBech32,
|
|
10569
|
+
isValidNametag,
|
|
9731
10570
|
isValidPrivateKey,
|
|
9732
10571
|
loadSphere,
|
|
9733
10572
|
mnemonicToEntropy2 as mnemonicToEntropy,
|