@unicitylabs/sphere-sdk 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -2
- package/dist/core/index.cjs +2044 -369
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +612 -58
- package/dist/core/index.d.ts +612 -58
- package/dist/core/index.js +2043 -368
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +307 -13
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +307 -13
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +4 -2
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +4 -2
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +326 -15
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +227 -17
- package/dist/impl/nodejs/index.d.ts +227 -17
- package/dist/impl/nodejs/index.js +326 -15
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +2299 -396
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1027 -7605
- package/dist/index.d.ts +1027 -7605
- package/dist/index.js +2286 -393
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.d.cts +7 -0
- package/dist/l1/index.d.ts +7 -0
- package/dist/l1/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
COIN_TYPES: () => COIN_TYPES,
|
|
34
|
+
CoinGeckoPriceProvider: () => CoinGeckoPriceProvider,
|
|
34
35
|
CommunicationsModule: () => CommunicationsModule,
|
|
35
36
|
DEFAULT_AGGREGATOR_TIMEOUT: () => DEFAULT_AGGREGATOR_TIMEOUT,
|
|
36
37
|
DEFAULT_AGGREGATOR_URL: () => DEFAULT_AGGREGATOR_URL,
|
|
@@ -66,8 +67,12 @@ __export(index_exports, {
|
|
|
66
67
|
createCommunicationsModule: () => createCommunicationsModule,
|
|
67
68
|
createKeyPair: () => createKeyPair,
|
|
68
69
|
createL1PaymentsModule: () => createL1PaymentsModule,
|
|
70
|
+
createPaymentSession: () => createPaymentSession,
|
|
71
|
+
createPaymentSessionError: () => createPaymentSessionError,
|
|
69
72
|
createPaymentsModule: () => createPaymentsModule,
|
|
73
|
+
createPriceProvider: () => createPriceProvider,
|
|
70
74
|
createSphere: () => createSphere,
|
|
75
|
+
createSplitPaymentSession: () => createSplitPaymentSession,
|
|
71
76
|
createTokenValidator: () => createTokenValidator,
|
|
72
77
|
decodeBech32: () => decodeBech32,
|
|
73
78
|
decryptCMasterKey: () => decryptCMasterKey,
|
|
@@ -105,7 +110,12 @@ __export(index_exports, {
|
|
|
105
110
|
initSphere: () => initSphere,
|
|
106
111
|
isArchivedKey: () => isArchivedKey,
|
|
107
112
|
isForkedKey: () => isForkedKey,
|
|
113
|
+
isInstantSplitBundle: () => isInstantSplitBundle,
|
|
114
|
+
isInstantSplitBundleV4: () => isInstantSplitBundleV4,
|
|
115
|
+
isInstantSplitBundleV5: () => isInstantSplitBundleV5,
|
|
108
116
|
isKnownToken: () => isKnownToken,
|
|
117
|
+
isPaymentSessionTerminal: () => isPaymentSessionTerminal,
|
|
118
|
+
isPaymentSessionTimedOut: () => isPaymentSessionTimedOut,
|
|
109
119
|
isSQLiteDatabase: () => isSQLiteDatabase,
|
|
110
120
|
isTextWalletEncrypted: () => isTextWalletEncrypted,
|
|
111
121
|
isTokenKey: () => isTokenKey,
|
|
@@ -1671,7 +1681,6 @@ var L1PaymentsModule = class {
|
|
|
1671
1681
|
_initialized = false;
|
|
1672
1682
|
_config;
|
|
1673
1683
|
_identity;
|
|
1674
|
-
_chainCode;
|
|
1675
1684
|
_addresses = [];
|
|
1676
1685
|
_wallet;
|
|
1677
1686
|
_transport;
|
|
@@ -1685,7 +1694,6 @@ var L1PaymentsModule = class {
|
|
|
1685
1694
|
}
|
|
1686
1695
|
async initialize(deps) {
|
|
1687
1696
|
this._identity = deps.identity;
|
|
1688
|
-
this._chainCode = deps.chainCode;
|
|
1689
1697
|
this._addresses = deps.addresses ?? [];
|
|
1690
1698
|
this._transport = deps.transport;
|
|
1691
1699
|
this._wallet = {
|
|
@@ -1721,7 +1729,6 @@ var L1PaymentsModule = class {
|
|
|
1721
1729
|
}
|
|
1722
1730
|
this._initialized = false;
|
|
1723
1731
|
this._identity = void 0;
|
|
1724
|
-
this._chainCode = void 0;
|
|
1725
1732
|
this._addresses = [];
|
|
1726
1733
|
this._wallet = void 0;
|
|
1727
1734
|
}
|
|
@@ -1756,10 +1763,10 @@ var L1PaymentsModule = class {
|
|
|
1756
1763
|
* Resolve nametag to L1 address using transport provider
|
|
1757
1764
|
*/
|
|
1758
1765
|
async resolveNametagToL1Address(nametag) {
|
|
1759
|
-
if (!this._transport?.
|
|
1760
|
-
throw new Error("Transport provider does not support
|
|
1766
|
+
if (!this._transport?.resolve) {
|
|
1767
|
+
throw new Error("Transport provider does not support resolution");
|
|
1761
1768
|
}
|
|
1762
|
-
const info = await this._transport.
|
|
1769
|
+
const info = await this._transport.resolve(nametag);
|
|
1763
1770
|
if (!info) {
|
|
1764
1771
|
throw new Error(`Nametag not found: ${nametag}`);
|
|
1765
1772
|
}
|
|
@@ -2036,25 +2043,11 @@ var TokenSplitCalculator = class {
|
|
|
2036
2043
|
* 3. If no exact match, determine which token to split
|
|
2037
2044
|
*/
|
|
2038
2045
|
async calculateOptimalSplit(availableTokens, targetAmount, targetCoinIdHex) {
|
|
2039
|
-
console.log(
|
|
2040
|
-
`[SplitCalculator] Calculating split for ${targetAmount} of ${targetCoinIdHex}`
|
|
2041
|
-
);
|
|
2042
|
-
console.log(`[SplitCalculator] Available tokens: ${availableTokens.length}`);
|
|
2043
2046
|
const candidates = [];
|
|
2044
2047
|
for (const t of availableTokens) {
|
|
2045
|
-
|
|
2046
|
-
if (t.
|
|
2047
|
-
|
|
2048
|
-
continue;
|
|
2049
|
-
}
|
|
2050
|
-
if (t.status !== "confirmed") {
|
|
2051
|
-
console.log(`[SplitCalculator] Skipping token ${t.id}: status is ${t.status}`);
|
|
2052
|
-
continue;
|
|
2053
|
-
}
|
|
2054
|
-
if (!t.sdkData) {
|
|
2055
|
-
console.log(`[SplitCalculator] Skipping token ${t.id}: no sdkData`);
|
|
2056
|
-
continue;
|
|
2057
|
-
}
|
|
2048
|
+
if (t.coinId !== targetCoinIdHex) continue;
|
|
2049
|
+
if (t.status !== "confirmed") continue;
|
|
2050
|
+
if (!t.sdkData) continue;
|
|
2058
2051
|
try {
|
|
2059
2052
|
const parsed = JSON.parse(t.sdkData);
|
|
2060
2053
|
const sdkToken = await import_Token.Token.fromJSON(parsed);
|
|
@@ -2082,14 +2075,12 @@ var TokenSplitCalculator = class {
|
|
|
2082
2075
|
}
|
|
2083
2076
|
const exactMatch = candidates.find((t) => t.amount === targetAmount);
|
|
2084
2077
|
if (exactMatch) {
|
|
2085
|
-
console.log("[SplitCalculator] Found exact match token");
|
|
2086
2078
|
return this.createDirectPlan([exactMatch], targetAmount, targetCoinIdHex);
|
|
2087
2079
|
}
|
|
2088
2080
|
const maxCombinationSize = Math.min(5, candidates.length);
|
|
2089
2081
|
for (let size = 2; size <= maxCombinationSize; size++) {
|
|
2090
2082
|
const combo = this.findCombinationOfSize(candidates, targetAmount, size);
|
|
2091
2083
|
if (combo) {
|
|
2092
|
-
console.log(`[SplitCalculator] Found exact combination of ${size} tokens`);
|
|
2093
2084
|
return this.createDirectPlan(combo, targetAmount, targetCoinIdHex);
|
|
2094
2085
|
}
|
|
2095
2086
|
}
|
|
@@ -2106,9 +2097,6 @@ var TokenSplitCalculator = class {
|
|
|
2106
2097
|
} else {
|
|
2107
2098
|
const neededFromThisToken = targetAmount - currentSum;
|
|
2108
2099
|
const remainderForSender = candidate.amount - neededFromThisToken;
|
|
2109
|
-
console.log(
|
|
2110
|
-
`[SplitCalculator] Split required. Sending: ${neededFromThisToken}, Remainder: ${remainderForSender}`
|
|
2111
|
-
);
|
|
2112
2100
|
return {
|
|
2113
2101
|
tokensToTransferDirectly: toTransfer,
|
|
2114
2102
|
tokenToSplit: candidate,
|
|
@@ -2127,16 +2115,10 @@ var TokenSplitCalculator = class {
|
|
|
2127
2115
|
*/
|
|
2128
2116
|
getTokenBalance(sdkToken, coinIdHex) {
|
|
2129
2117
|
try {
|
|
2130
|
-
if (!sdkToken.coins)
|
|
2131
|
-
console.log("[SplitCalculator] Token has no coins");
|
|
2132
|
-
return 0n;
|
|
2133
|
-
}
|
|
2118
|
+
if (!sdkToken.coins) return 0n;
|
|
2134
2119
|
const coinId = import_CoinId.CoinId.fromJSON(coinIdHex);
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
return balance ?? 0n;
|
|
2138
|
-
} catch (e) {
|
|
2139
|
-
console.error("[SplitCalculator] Error getting token balance:", e);
|
|
2120
|
+
return sdkToken.coins.get(coinId) ?? 0n;
|
|
2121
|
+
} catch {
|
|
2140
2122
|
return 0n;
|
|
2141
2123
|
}
|
|
2142
2124
|
}
|
|
@@ -2365,20 +2347,18 @@ var NametagMinter = class {
|
|
|
2365
2347
|
const cleanNametag = nametag.replace("@", "").trim();
|
|
2366
2348
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2367
2349
|
try {
|
|
2368
|
-
const isAvailable = await this.isNametagAvailable(cleanNametag);
|
|
2369
|
-
if (!isAvailable) {
|
|
2370
|
-
return {
|
|
2371
|
-
success: false,
|
|
2372
|
-
error: `Nametag "${cleanNametag}" is already taken`
|
|
2373
|
-
};
|
|
2374
|
-
}
|
|
2375
2350
|
const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
|
|
2376
2351
|
const nametagTokenType = new import_TokenType.TokenType(
|
|
2377
2352
|
Buffer.from(UNICITY_TOKEN_TYPE_HEX, "hex")
|
|
2378
2353
|
);
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
2354
|
+
const nametagBytes = new TextEncoder().encode(cleanNametag);
|
|
2355
|
+
const pubKey = this.signingService.publicKey;
|
|
2356
|
+
const saltInput = new Uint8Array(pubKey.length + nametagBytes.length);
|
|
2357
|
+
saltInput.set(pubKey, 0);
|
|
2358
|
+
saltInput.set(nametagBytes, pubKey.length);
|
|
2359
|
+
const saltBuffer = await crypto.subtle.digest("SHA-256", saltInput);
|
|
2360
|
+
const salt = new Uint8Array(saltBuffer);
|
|
2361
|
+
this.log("Generated deterministic salt");
|
|
2382
2362
|
const mintData = await import_MintTransactionData.MintTransactionData.createFromNametag(
|
|
2383
2363
|
cleanNametag,
|
|
2384
2364
|
nametagTokenType,
|
|
@@ -2501,8 +2481,10 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
2501
2481
|
WALLET_EXISTS: "wallet_exists",
|
|
2502
2482
|
/** Current active address index */
|
|
2503
2483
|
CURRENT_ADDRESS_INDEX: "current_address_index",
|
|
2504
|
-
/**
|
|
2505
|
-
ADDRESS_NAMETAGS: "address_nametags"
|
|
2484
|
+
/** Nametag cache per address (separate from tracked addresses registry) */
|
|
2485
|
+
ADDRESS_NAMETAGS: "address_nametags",
|
|
2486
|
+
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
2487
|
+
TRACKED_ADDRESSES: "tracked_addresses"
|
|
2506
2488
|
};
|
|
2507
2489
|
var STORAGE_KEYS_ADDRESS = {
|
|
2508
2490
|
/** Pending transfers for this address */
|
|
@@ -2951,11 +2933,16 @@ function getCurrentStateHash(txf) {
|
|
|
2951
2933
|
if (lastTx?.newStateHash) {
|
|
2952
2934
|
return lastTx.newStateHash;
|
|
2953
2935
|
}
|
|
2954
|
-
|
|
2936
|
+
if (lastTx?.inclusionProof?.authenticator?.stateHash) {
|
|
2937
|
+
return lastTx.inclusionProof.authenticator.stateHash;
|
|
2938
|
+
}
|
|
2955
2939
|
}
|
|
2956
2940
|
if (txf._integrity?.currentStateHash) {
|
|
2957
2941
|
return txf._integrity.currentStateHash;
|
|
2958
2942
|
}
|
|
2943
|
+
if (txf.genesis?.inclusionProof?.authenticator?.stateHash) {
|
|
2944
|
+
return txf.genesis.inclusionProof.authenticator.stateHash;
|
|
2945
|
+
}
|
|
2959
2946
|
return void 0;
|
|
2960
2947
|
}
|
|
2961
2948
|
function hasValidTxfData(token) {
|
|
@@ -3316,16 +3303,733 @@ function getCoinIdByName(name) {
|
|
|
3316
3303
|
return TokenRegistry.getInstance().getCoinIdByName(name);
|
|
3317
3304
|
}
|
|
3318
3305
|
|
|
3319
|
-
// modules/payments/
|
|
3306
|
+
// modules/payments/InstantSplitExecutor.ts
|
|
3320
3307
|
var import_Token4 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
3308
|
+
var import_TokenId3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
3309
|
+
var import_TokenState3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
3321
3310
|
var import_CoinId3 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
|
|
3311
|
+
var import_TokenCoinData2 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData");
|
|
3312
|
+
var import_TokenSplitBuilder2 = require("@unicitylabs/state-transition-sdk/lib/transaction/split/TokenSplitBuilder");
|
|
3313
|
+
var import_HashAlgorithm3 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
3314
|
+
var import_UnmaskedPredicate3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
3315
|
+
var import_UnmaskedPredicateReference2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
3322
3316
|
var import_TransferCommitment2 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
|
|
3317
|
+
var import_InclusionProofUtils3 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
3318
|
+
async function sha2563(input) {
|
|
3319
|
+
const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
3320
|
+
const buffer = new ArrayBuffer(data.length);
|
|
3321
|
+
new Uint8Array(buffer).set(data);
|
|
3322
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
|
|
3323
|
+
return new Uint8Array(hashBuffer);
|
|
3324
|
+
}
|
|
3325
|
+
function toHex2(bytes) {
|
|
3326
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3327
|
+
}
|
|
3328
|
+
function fromHex2(hex) {
|
|
3329
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3330
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3331
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
3332
|
+
}
|
|
3333
|
+
return bytes;
|
|
3334
|
+
}
|
|
3335
|
+
var InstantSplitExecutor = class {
|
|
3336
|
+
client;
|
|
3337
|
+
trustBase;
|
|
3338
|
+
signingService;
|
|
3339
|
+
devMode;
|
|
3340
|
+
constructor(config) {
|
|
3341
|
+
this.client = config.stateTransitionClient;
|
|
3342
|
+
this.trustBase = config.trustBase;
|
|
3343
|
+
this.signingService = config.signingService;
|
|
3344
|
+
this.devMode = config.devMode ?? false;
|
|
3345
|
+
}
|
|
3346
|
+
/**
|
|
3347
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
3348
|
+
*
|
|
3349
|
+
* Critical path (~2.3s):
|
|
3350
|
+
* 1. Create and submit burn commitment
|
|
3351
|
+
* 2. Wait for burn proof
|
|
3352
|
+
* 3. Create mint commitments with SplitMintReason
|
|
3353
|
+
* 4. Create transfer commitment (no mint proof needed)
|
|
3354
|
+
* 5. Send bundle via transport
|
|
3355
|
+
*
|
|
3356
|
+
* @param tokenToSplit - The SDK token to split
|
|
3357
|
+
* @param splitAmount - Amount to send to recipient
|
|
3358
|
+
* @param remainderAmount - Amount to keep as change
|
|
3359
|
+
* @param coinIdHex - Coin ID in hex format
|
|
3360
|
+
* @param recipientAddress - Recipient's address (PROXY or DIRECT)
|
|
3361
|
+
* @param transport - Transport provider for sending the bundle
|
|
3362
|
+
* @param recipientPubkey - Recipient's transport public key
|
|
3363
|
+
* @param options - Optional configuration
|
|
3364
|
+
* @returns InstantSplitResult with success status and timing info
|
|
3365
|
+
*/
|
|
3366
|
+
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3367
|
+
const startTime = performance.now();
|
|
3368
|
+
const splitGroupId = crypto.randomUUID();
|
|
3369
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3370
|
+
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3371
|
+
try {
|
|
3372
|
+
const coinId = new import_CoinId3.CoinId(fromHex2(coinIdHex));
|
|
3373
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
3374
|
+
const recipientTokenId = new import_TokenId3.TokenId(await sha2563(seedString));
|
|
3375
|
+
const senderTokenId = new import_TokenId3.TokenId(await sha2563(seedString + "_sender"));
|
|
3376
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
3377
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3378
|
+
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3379
|
+
tokenToSplit.type,
|
|
3380
|
+
this.signingService.algorithm,
|
|
3381
|
+
this.signingService.publicKey,
|
|
3382
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3383
|
+
);
|
|
3384
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
3385
|
+
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3386
|
+
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3387
|
+
builder.createToken(
|
|
3388
|
+
recipientTokenId,
|
|
3389
|
+
tokenToSplit.type,
|
|
3390
|
+
new Uint8Array(0),
|
|
3391
|
+
coinDataA,
|
|
3392
|
+
senderAddress,
|
|
3393
|
+
// Mint to sender first, then transfer
|
|
3394
|
+
recipientSalt,
|
|
3395
|
+
null
|
|
3396
|
+
);
|
|
3397
|
+
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3398
|
+
builder.createToken(
|
|
3399
|
+
senderTokenId,
|
|
3400
|
+
tokenToSplit.type,
|
|
3401
|
+
new Uint8Array(0),
|
|
3402
|
+
coinDataB,
|
|
3403
|
+
senderAddress,
|
|
3404
|
+
senderSalt,
|
|
3405
|
+
null
|
|
3406
|
+
);
|
|
3407
|
+
const split = await builder.build(tokenToSplit);
|
|
3408
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3409
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3410
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3411
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3412
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3413
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3414
|
+
}
|
|
3415
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3416
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3417
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3418
|
+
const burnDuration = performance.now() - startTime;
|
|
3419
|
+
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3420
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3421
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3422
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3423
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3424
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3425
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3426
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3427
|
+
);
|
|
3428
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3429
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3430
|
+
);
|
|
3431
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3432
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3433
|
+
}
|
|
3434
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3435
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3436
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3437
|
+
recipientMintCommitment.transactionData,
|
|
3438
|
+
recipientAddress,
|
|
3439
|
+
transferSalt,
|
|
3440
|
+
this.signingService
|
|
3441
|
+
);
|
|
3442
|
+
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3443
|
+
recipientTokenId,
|
|
3444
|
+
tokenToSplit.type,
|
|
3445
|
+
this.signingService,
|
|
3446
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3447
|
+
recipientSalt
|
|
3448
|
+
);
|
|
3449
|
+
const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
|
|
3450
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3451
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3452
|
+
let nametagTokenJson;
|
|
3453
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3454
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3455
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3456
|
+
}
|
|
3457
|
+
const bundle = {
|
|
3458
|
+
version: "5.0",
|
|
3459
|
+
type: "INSTANT_SPLIT",
|
|
3460
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3461
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3462
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3463
|
+
amount: splitAmount.toString(),
|
|
3464
|
+
coinId: coinIdHex,
|
|
3465
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3466
|
+
splitGroupId,
|
|
3467
|
+
senderPubkey,
|
|
3468
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3469
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3470
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3471
|
+
finalRecipientStateJson: "",
|
|
3472
|
+
// Recipient creates their own
|
|
3473
|
+
recipientAddressJson: recipientAddressStr,
|
|
3474
|
+
nametagTokenJson
|
|
3475
|
+
};
|
|
3476
|
+
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3477
|
+
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3478
|
+
token: JSON.stringify(bundle),
|
|
3479
|
+
proof: null,
|
|
3480
|
+
// Proof is included in the bundle
|
|
3481
|
+
memo: "INSTANT_SPLIT_V5",
|
|
3482
|
+
sender: {
|
|
3483
|
+
transportPubkey: senderPubkey
|
|
3484
|
+
}
|
|
3485
|
+
});
|
|
3486
|
+
const criticalPathDuration = performance.now() - startTime;
|
|
3487
|
+
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3488
|
+
options?.onNostrDelivered?.(nostrEventId);
|
|
3489
|
+
if (!options?.skipBackground) {
|
|
3490
|
+
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3491
|
+
signingService: this.signingService,
|
|
3492
|
+
tokenType: tokenToSplit.type,
|
|
3493
|
+
coinId,
|
|
3494
|
+
senderTokenId,
|
|
3495
|
+
senderSalt,
|
|
3496
|
+
onProgress: options?.onBackgroundProgress,
|
|
3497
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3498
|
+
onStorageSync: options?.onStorageSync
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
return {
|
|
3502
|
+
success: true,
|
|
3503
|
+
nostrEventId,
|
|
3504
|
+
splitGroupId,
|
|
3505
|
+
criticalPathDurationMs: criticalPathDuration,
|
|
3506
|
+
backgroundStarted: !options?.skipBackground
|
|
3507
|
+
};
|
|
3508
|
+
} catch (error) {
|
|
3509
|
+
const duration = performance.now() - startTime;
|
|
3510
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3511
|
+
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3512
|
+
return {
|
|
3513
|
+
success: false,
|
|
3514
|
+
splitGroupId,
|
|
3515
|
+
criticalPathDurationMs: duration,
|
|
3516
|
+
error: errorMessage,
|
|
3517
|
+
backgroundStarted: false
|
|
3518
|
+
};
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
/**
|
|
3522
|
+
* Create a TransferCommitment from MintTransactionData WITHOUT waiting for mint proof.
|
|
3523
|
+
*
|
|
3524
|
+
* Key insight: TransferCommitment.create() only needs token.state and token.nametagTokens.
|
|
3525
|
+
* It does NOT need the genesis transaction or mint proof.
|
|
3526
|
+
*/
|
|
3527
|
+
async createTransferCommitmentFromMintData(mintData, recipientAddress, transferSalt, signingService, nametagTokens) {
|
|
3528
|
+
const predicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3529
|
+
mintData.tokenId,
|
|
3530
|
+
mintData.tokenType,
|
|
3531
|
+
signingService,
|
|
3532
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3533
|
+
mintData.salt
|
|
3534
|
+
);
|
|
3535
|
+
const state = new import_TokenState3.TokenState(predicate, null);
|
|
3536
|
+
const minimalToken = {
|
|
3537
|
+
state,
|
|
3538
|
+
nametagTokens: nametagTokens || [],
|
|
3539
|
+
id: mintData.tokenId,
|
|
3540
|
+
type: mintData.tokenType
|
|
3541
|
+
};
|
|
3542
|
+
const transferCommitment = await import_TransferCommitment2.TransferCommitment.create(
|
|
3543
|
+
minimalToken,
|
|
3544
|
+
recipientAddress,
|
|
3545
|
+
transferSalt,
|
|
3546
|
+
null,
|
|
3547
|
+
// recipientData
|
|
3548
|
+
null,
|
|
3549
|
+
// recipientDataHash
|
|
3550
|
+
signingService
|
|
3551
|
+
);
|
|
3552
|
+
return transferCommitment;
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* V5 background submission.
|
|
3556
|
+
*
|
|
3557
|
+
* Submits mint commitments to aggregator in PARALLEL after transport delivery.
|
|
3558
|
+
* Then waits for sender's mint proof, reconstructs change token, and saves it.
|
|
3559
|
+
*/
|
|
3560
|
+
submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, context) {
|
|
3561
|
+
console.log("[InstantSplit] Background: Starting parallel mint submission...");
|
|
3562
|
+
const startTime = performance.now();
|
|
3563
|
+
const submissions = Promise.all([
|
|
3564
|
+
this.client.submitMintCommitment(senderMintCommitment).then((res) => ({ type: "senderMint", status: res.status })).catch((err) => ({ type: "senderMint", status: "ERROR", error: err })),
|
|
3565
|
+
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3566
|
+
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3567
|
+
]);
|
|
3568
|
+
submissions.then(async (results) => {
|
|
3569
|
+
const submitDuration = performance.now() - startTime;
|
|
3570
|
+
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3571
|
+
context.onProgress?.({
|
|
3572
|
+
stage: "MINTS_SUBMITTED",
|
|
3573
|
+
message: `All commitments submitted in ${submitDuration.toFixed(0)}ms`
|
|
3574
|
+
});
|
|
3575
|
+
const senderMintResult = results.find((r) => r.type === "senderMint");
|
|
3576
|
+
if (senderMintResult?.status !== "SUCCESS" && senderMintResult?.status !== "REQUEST_ID_EXISTS") {
|
|
3577
|
+
console.error("[InstantSplit] Background: Sender mint failed - cannot save change token");
|
|
3578
|
+
context.onProgress?.({
|
|
3579
|
+
stage: "FAILED",
|
|
3580
|
+
message: "Sender mint submission failed",
|
|
3581
|
+
error: String(senderMintResult?.error)
|
|
3582
|
+
});
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
console.log("[InstantSplit] Background: Waiting for sender mint proof...");
|
|
3586
|
+
const proofStartTime = performance.now();
|
|
3587
|
+
try {
|
|
3588
|
+
const senderMintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(senderMintCommitment) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, senderMintCommitment);
|
|
3589
|
+
const proofDuration = performance.now() - proofStartTime;
|
|
3590
|
+
console.log(`[InstantSplit] Background: Sender mint proof received in ${proofDuration.toFixed(0)}ms`);
|
|
3591
|
+
context.onProgress?.({
|
|
3592
|
+
stage: "MINTS_PROVEN",
|
|
3593
|
+
message: `Mint proof received in ${proofDuration.toFixed(0)}ms`
|
|
3594
|
+
});
|
|
3595
|
+
const mintTransaction = senderMintCommitment.toTransaction(senderMintProof);
|
|
3596
|
+
const predicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3597
|
+
context.senderTokenId,
|
|
3598
|
+
context.tokenType,
|
|
3599
|
+
context.signingService,
|
|
3600
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3601
|
+
context.senderSalt
|
|
3602
|
+
);
|
|
3603
|
+
const state = new import_TokenState3.TokenState(predicate, null);
|
|
3604
|
+
const changeToken = await import_Token4.Token.mint(this.trustBase, state, mintTransaction);
|
|
3605
|
+
if (!this.devMode) {
|
|
3606
|
+
const verification = await changeToken.verify(this.trustBase);
|
|
3607
|
+
if (!verification.isSuccessful) {
|
|
3608
|
+
throw new Error(`Change token verification failed`);
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
console.log("[InstantSplit] Background: Change token created");
|
|
3612
|
+
context.onProgress?.({
|
|
3613
|
+
stage: "CHANGE_TOKEN_SAVED",
|
|
3614
|
+
message: "Change token created and verified"
|
|
3615
|
+
});
|
|
3616
|
+
if (context.onChangeTokenCreated) {
|
|
3617
|
+
await context.onChangeTokenCreated(changeToken);
|
|
3618
|
+
console.log("[InstantSplit] Background: Change token saved");
|
|
3619
|
+
}
|
|
3620
|
+
if (context.onStorageSync) {
|
|
3621
|
+
try {
|
|
3622
|
+
const syncSuccess = await context.onStorageSync();
|
|
3623
|
+
console.log(`[InstantSplit] Background: Storage sync ${syncSuccess ? "completed" : "deferred"}`);
|
|
3624
|
+
context.onProgress?.({
|
|
3625
|
+
stage: "STORAGE_SYNCED",
|
|
3626
|
+
message: syncSuccess ? "Storage synchronized" : "Sync deferred"
|
|
3627
|
+
});
|
|
3628
|
+
} catch (syncError) {
|
|
3629
|
+
console.warn("[InstantSplit] Background: Storage sync error:", syncError);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
const totalDuration = performance.now() - startTime;
|
|
3633
|
+
console.log(`[InstantSplit] Background: Complete in ${totalDuration.toFixed(0)}ms`);
|
|
3634
|
+
context.onProgress?.({
|
|
3635
|
+
stage: "COMPLETED",
|
|
3636
|
+
message: `Background processing complete in ${totalDuration.toFixed(0)}ms`
|
|
3637
|
+
});
|
|
3638
|
+
} catch (proofError) {
|
|
3639
|
+
console.error("[InstantSplit] Background: Failed to get sender mint proof:", proofError);
|
|
3640
|
+
context.onProgress?.({
|
|
3641
|
+
stage: "FAILED",
|
|
3642
|
+
message: "Failed to get mint proof",
|
|
3643
|
+
error: String(proofError)
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
}).catch((err) => {
|
|
3647
|
+
console.error("[InstantSplit] Background: Submission batch failed:", err);
|
|
3648
|
+
context.onProgress?.({
|
|
3649
|
+
stage: "FAILED",
|
|
3650
|
+
message: "Background submission failed",
|
|
3651
|
+
error: String(err)
|
|
3652
|
+
});
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Dev mode bypass for waitInclusionProof.
|
|
3657
|
+
* In dev mode, we create a mock proof for testing.
|
|
3658
|
+
*/
|
|
3659
|
+
async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
|
|
3660
|
+
if (this.devMode) {
|
|
3661
|
+
try {
|
|
3662
|
+
return await Promise.race([
|
|
3663
|
+
(0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, commitment),
|
|
3664
|
+
new Promise(
|
|
3665
|
+
(_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
|
|
3666
|
+
)
|
|
3667
|
+
]);
|
|
3668
|
+
} catch {
|
|
3669
|
+
console.log("[InstantSplit] Dev mode: Using mock proof");
|
|
3670
|
+
return {
|
|
3671
|
+
toJSON: () => ({ mock: true })
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
return (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, commitment);
|
|
3676
|
+
}
|
|
3677
|
+
};
|
|
3678
|
+
|
|
3679
|
+
// modules/payments/InstantSplitProcessor.ts
|
|
3680
|
+
var import_Token5 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
3681
|
+
var import_TokenState4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
3682
|
+
var import_TokenType2 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
3683
|
+
var import_HashAlgorithm4 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
3684
|
+
var import_UnmaskedPredicate4 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
3685
|
+
var import_TransferCommitment3 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
|
|
3323
3686
|
var import_TransferTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction");
|
|
3687
|
+
var import_MintCommitment2 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment");
|
|
3688
|
+
var import_MintTransactionData2 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
|
|
3689
|
+
var import_InclusionProofUtils4 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
3690
|
+
|
|
3691
|
+
// types/instant-split.ts
|
|
3692
|
+
function isInstantSplitBundle(obj) {
|
|
3693
|
+
if (typeof obj !== "object" || obj === null) {
|
|
3694
|
+
return false;
|
|
3695
|
+
}
|
|
3696
|
+
const bundle = obj;
|
|
3697
|
+
if (bundle.type !== "INSTANT_SPLIT") return false;
|
|
3698
|
+
if (typeof bundle.recipientMintData !== "string") return false;
|
|
3699
|
+
if (typeof bundle.transferCommitment !== "string") return false;
|
|
3700
|
+
if (typeof bundle.amount !== "string") return false;
|
|
3701
|
+
if (typeof bundle.coinId !== "string") return false;
|
|
3702
|
+
if (typeof bundle.splitGroupId !== "string") return false;
|
|
3703
|
+
if (typeof bundle.senderPubkey !== "string") return false;
|
|
3704
|
+
if (typeof bundle.recipientSaltHex !== "string") return false;
|
|
3705
|
+
if (typeof bundle.transferSaltHex !== "string") return false;
|
|
3706
|
+
if (bundle.version === "4.0") {
|
|
3707
|
+
return typeof bundle.burnCommitment === "string";
|
|
3708
|
+
} else if (bundle.version === "5.0") {
|
|
3709
|
+
return typeof bundle.burnTransaction === "string" && typeof bundle.mintedTokenStateJson === "string" && typeof bundle.finalRecipientStateJson === "string" && typeof bundle.recipientAddressJson === "string";
|
|
3710
|
+
}
|
|
3711
|
+
return false;
|
|
3712
|
+
}
|
|
3713
|
+
function isInstantSplitBundleV4(obj) {
|
|
3714
|
+
return isInstantSplitBundle(obj) && obj.version === "4.0";
|
|
3715
|
+
}
|
|
3716
|
+
function isInstantSplitBundleV5(obj) {
|
|
3717
|
+
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
// modules/payments/InstantSplitProcessor.ts
|
|
3721
|
+
function fromHex3(hex) {
|
|
3722
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3723
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3724
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
3725
|
+
}
|
|
3726
|
+
return bytes;
|
|
3727
|
+
}
|
|
3728
|
+
var InstantSplitProcessor = class {
|
|
3729
|
+
client;
|
|
3730
|
+
trustBase;
|
|
3731
|
+
devMode;
|
|
3732
|
+
constructor(config) {
|
|
3733
|
+
this.client = config.stateTransitionClient;
|
|
3734
|
+
this.trustBase = config.trustBase;
|
|
3735
|
+
this.devMode = config.devMode ?? false;
|
|
3736
|
+
}
|
|
3737
|
+
/**
|
|
3738
|
+
* Process a received INSTANT_SPLIT bundle.
|
|
3739
|
+
*
|
|
3740
|
+
* @param bundle - The received bundle (V4 or V5)
|
|
3741
|
+
* @param signingService - Recipient's signing service
|
|
3742
|
+
* @param senderPubkey - Sender's public key (for verification)
|
|
3743
|
+
* @param options - Processing options
|
|
3744
|
+
* @returns Processing result with finalized token if successful
|
|
3745
|
+
*/
|
|
3746
|
+
async processReceivedBundle(bundle, signingService, senderPubkey, options) {
|
|
3747
|
+
if (isInstantSplitBundleV5(bundle)) {
|
|
3748
|
+
return this.processV5Bundle(bundle, signingService, senderPubkey, options);
|
|
3749
|
+
} else if (isInstantSplitBundleV4(bundle)) {
|
|
3750
|
+
return this.processV4Bundle(bundle, signingService, senderPubkey, options);
|
|
3751
|
+
}
|
|
3752
|
+
return {
|
|
3753
|
+
success: false,
|
|
3754
|
+
error: `Unknown bundle version: ${bundle.version}`,
|
|
3755
|
+
durationMs: 0
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Process a V5 bundle (production mode).
|
|
3760
|
+
*
|
|
3761
|
+
* V5 Flow:
|
|
3762
|
+
* 1. Burn transaction already has proof (just validate)
|
|
3763
|
+
* 2. Submit mint commitment -> wait for proof
|
|
3764
|
+
* 3. Reconstruct minted token (use sender's state from bundle)
|
|
3765
|
+
* 4. Submit transfer commitment -> wait for proof
|
|
3766
|
+
* 5. Create recipient's final state and finalize token
|
|
3767
|
+
*/
|
|
3768
|
+
async processV5Bundle(bundle, signingService, senderPubkey, options) {
|
|
3769
|
+
console.log("[InstantSplitProcessor] Processing V5 bundle...");
|
|
3770
|
+
const startTime = performance.now();
|
|
3771
|
+
try {
|
|
3772
|
+
if (bundle.senderPubkey !== senderPubkey) {
|
|
3773
|
+
console.warn("[InstantSplitProcessor] Sender pubkey mismatch (non-fatal)");
|
|
3774
|
+
}
|
|
3775
|
+
const burnTxJson = JSON.parse(bundle.burnTransaction);
|
|
3776
|
+
const burnTransaction = await import_TransferTransaction.TransferTransaction.fromJSON(burnTxJson);
|
|
3777
|
+
console.log("[InstantSplitProcessor] Burn transaction validated");
|
|
3778
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
3779
|
+
const mintData = await import_MintTransactionData2.MintTransactionData.fromJSON(mintDataJson);
|
|
3780
|
+
const mintCommitment = await import_MintCommitment2.MintCommitment.create(mintData);
|
|
3781
|
+
console.log("[InstantSplitProcessor] Mint commitment recreated");
|
|
3782
|
+
const mintResponse = await this.client.submitMintCommitment(mintCommitment);
|
|
3783
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3784
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
3785
|
+
}
|
|
3786
|
+
console.log(`[InstantSplitProcessor] Mint submitted: ${mintResponse.status}`);
|
|
3787
|
+
const mintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(mintCommitment, options?.proofTimeoutMs) : await (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, mintCommitment);
|
|
3788
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
3789
|
+
console.log("[InstantSplitProcessor] Mint proof received");
|
|
3790
|
+
const tokenType = new import_TokenType2.TokenType(fromHex3(bundle.tokenTypeHex));
|
|
3791
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
3792
|
+
const tokenJson = {
|
|
3793
|
+
version: "2.0",
|
|
3794
|
+
state: senderMintedStateJson,
|
|
3795
|
+
genesis: mintTransaction.toJSON(),
|
|
3796
|
+
transactions: [],
|
|
3797
|
+
nametags: []
|
|
3798
|
+
};
|
|
3799
|
+
const mintedToken = await import_Token5.Token.fromJSON(tokenJson);
|
|
3800
|
+
console.log("[InstantSplitProcessor] Minted token reconstructed from sender state");
|
|
3801
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
3802
|
+
const transferCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
3803
|
+
const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
|
|
3804
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3805
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
3806
|
+
}
|
|
3807
|
+
console.log(`[InstantSplitProcessor] Transfer submitted: ${transferResponse.status}`);
|
|
3808
|
+
const transferProof = this.devMode ? await this.waitInclusionProofWithDevBypass(transferCommitment, options?.proofTimeoutMs) : await (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, transferCommitment);
|
|
3809
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
3810
|
+
console.log("[InstantSplitProcessor] Transfer proof received");
|
|
3811
|
+
const transferSalt = fromHex3(bundle.transferSaltHex);
|
|
3812
|
+
const finalRecipientPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
|
|
3813
|
+
mintData.tokenId,
|
|
3814
|
+
tokenType,
|
|
3815
|
+
signingService,
|
|
3816
|
+
import_HashAlgorithm4.HashAlgorithm.SHA256,
|
|
3817
|
+
transferSalt
|
|
3818
|
+
);
|
|
3819
|
+
const finalRecipientState = new import_TokenState4.TokenState(finalRecipientPredicate, null);
|
|
3820
|
+
console.log("[InstantSplitProcessor] Final recipient state created");
|
|
3821
|
+
let nametagTokens = [];
|
|
3822
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
3823
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
3824
|
+
console.log("[InstantSplitProcessor] PROXY address detected, finding nametag token...");
|
|
3825
|
+
if (bundle.nametagTokenJson) {
|
|
3826
|
+
try {
|
|
3827
|
+
const nametagToken = await import_Token5.Token.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
3828
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
3829
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
3830
|
+
if (proxy.address !== recipientAddressStr) {
|
|
3831
|
+
console.warn("[InstantSplitProcessor] Nametag PROXY address mismatch, ignoring bundle token");
|
|
3832
|
+
} else {
|
|
3833
|
+
nametagTokens = [nametagToken];
|
|
3834
|
+
console.log("[InstantSplitProcessor] Using nametag token from bundle (address validated)");
|
|
3835
|
+
}
|
|
3836
|
+
} catch (err) {
|
|
3837
|
+
console.warn("[InstantSplitProcessor] Failed to parse nametag token from bundle:", err);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
if (nametagTokens.length === 0 && options?.findNametagToken) {
|
|
3841
|
+
const token = await options.findNametagToken(recipientAddressStr);
|
|
3842
|
+
if (token) {
|
|
3843
|
+
nametagTokens = [token];
|
|
3844
|
+
console.log("[InstantSplitProcessor] Found nametag token via callback");
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
if (nametagTokens.length === 0 && !this.devMode) {
|
|
3848
|
+
throw new Error(
|
|
3849
|
+
`PROXY address transfer requires nametag token for verification. Address: ${recipientAddressStr}`
|
|
3850
|
+
);
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
let finalToken;
|
|
3854
|
+
if (this.devMode) {
|
|
3855
|
+
console.log("[InstantSplitProcessor] Dev mode: finalizing without verification");
|
|
3856
|
+
const tokenJson2 = mintedToken.toJSON();
|
|
3857
|
+
tokenJson2.state = finalRecipientState.toJSON();
|
|
3858
|
+
tokenJson2.transactions = [transferTransaction.toJSON()];
|
|
3859
|
+
finalToken = await import_Token5.Token.fromJSON(tokenJson2);
|
|
3860
|
+
} else {
|
|
3861
|
+
finalToken = await this.client.finalizeTransaction(
|
|
3862
|
+
this.trustBase,
|
|
3863
|
+
mintedToken,
|
|
3864
|
+
finalRecipientState,
|
|
3865
|
+
transferTransaction,
|
|
3866
|
+
nametagTokens
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
console.log("[InstantSplitProcessor] Token finalized");
|
|
3870
|
+
if (!this.devMode) {
|
|
3871
|
+
const verification = await finalToken.verify(this.trustBase);
|
|
3872
|
+
if (!verification.isSuccessful) {
|
|
3873
|
+
throw new Error(`Token verification failed`);
|
|
3874
|
+
}
|
|
3875
|
+
console.log("[InstantSplitProcessor] Token verified");
|
|
3876
|
+
}
|
|
3877
|
+
const duration = performance.now() - startTime;
|
|
3878
|
+
console.log(`[InstantSplitProcessor] V5 bundle processed in ${duration.toFixed(0)}ms`);
|
|
3879
|
+
return {
|
|
3880
|
+
success: true,
|
|
3881
|
+
token: finalToken,
|
|
3882
|
+
durationMs: duration
|
|
3883
|
+
};
|
|
3884
|
+
} catch (error) {
|
|
3885
|
+
const duration = performance.now() - startTime;
|
|
3886
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3887
|
+
console.error(`[InstantSplitProcessor] V5 processing failed:`, error);
|
|
3888
|
+
return {
|
|
3889
|
+
success: false,
|
|
3890
|
+
error: errorMessage,
|
|
3891
|
+
durationMs: duration
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
/**
|
|
3896
|
+
* Process a V4 bundle (dev mode only).
|
|
3897
|
+
*
|
|
3898
|
+
* V4 Flow:
|
|
3899
|
+
* 1. Submit burn commitment -> wait for proof
|
|
3900
|
+
* 2. Submit mint commitment -> wait for proof
|
|
3901
|
+
* 3. Reconstruct minted token
|
|
3902
|
+
* 4. Submit transfer commitment -> wait for proof
|
|
3903
|
+
* 5. Finalize token
|
|
3904
|
+
*/
|
|
3905
|
+
async processV4Bundle(bundle, signingService, _senderPubkey, options) {
|
|
3906
|
+
if (!this.devMode) {
|
|
3907
|
+
return {
|
|
3908
|
+
success: false,
|
|
3909
|
+
error: "INSTANT_SPLIT V4 is only supported in dev mode",
|
|
3910
|
+
durationMs: 0
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3913
|
+
console.log("[InstantSplitProcessor] Processing V4 bundle (dev mode)...");
|
|
3914
|
+
const startTime = performance.now();
|
|
3915
|
+
try {
|
|
3916
|
+
const burnCommitmentJson = JSON.parse(bundle.burnCommitment);
|
|
3917
|
+
const burnCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(burnCommitmentJson);
|
|
3918
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3919
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3920
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3921
|
+
}
|
|
3922
|
+
await this.waitInclusionProofWithDevBypass(burnCommitment, options?.proofTimeoutMs);
|
|
3923
|
+
console.log("[InstantSplitProcessor] V4: Burn proof received");
|
|
3924
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
3925
|
+
const mintData = await import_MintTransactionData2.MintTransactionData.fromJSON(mintDataJson);
|
|
3926
|
+
const mintCommitment = await import_MintCommitment2.MintCommitment.create(mintData);
|
|
3927
|
+
const mintResponse = await this.client.submitMintCommitment(mintCommitment);
|
|
3928
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3929
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
3930
|
+
}
|
|
3931
|
+
const mintProof = await this.waitInclusionProofWithDevBypass(
|
|
3932
|
+
mintCommitment,
|
|
3933
|
+
options?.proofTimeoutMs
|
|
3934
|
+
);
|
|
3935
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
3936
|
+
console.log("[InstantSplitProcessor] V4: Mint proof received");
|
|
3937
|
+
const tokenType = new import_TokenType2.TokenType(fromHex3(bundle.tokenTypeHex));
|
|
3938
|
+
const recipientSalt = fromHex3(bundle.recipientSaltHex);
|
|
3939
|
+
const recipientPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
|
|
3940
|
+
mintData.tokenId,
|
|
3941
|
+
tokenType,
|
|
3942
|
+
signingService,
|
|
3943
|
+
import_HashAlgorithm4.HashAlgorithm.SHA256,
|
|
3944
|
+
recipientSalt
|
|
3945
|
+
);
|
|
3946
|
+
const recipientState = new import_TokenState4.TokenState(recipientPredicate, null);
|
|
3947
|
+
const tokenJson = {
|
|
3948
|
+
version: "2.0",
|
|
3949
|
+
state: recipientState.toJSON(),
|
|
3950
|
+
genesis: mintTransaction.toJSON(),
|
|
3951
|
+
transactions: [],
|
|
3952
|
+
nametags: []
|
|
3953
|
+
};
|
|
3954
|
+
const mintedToken = await import_Token5.Token.fromJSON(tokenJson);
|
|
3955
|
+
console.log("[InstantSplitProcessor] V4: Minted token reconstructed");
|
|
3956
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
3957
|
+
const transferCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
3958
|
+
const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
|
|
3959
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3960
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
3961
|
+
}
|
|
3962
|
+
const transferProof = await this.waitInclusionProofWithDevBypass(
|
|
3963
|
+
transferCommitment,
|
|
3964
|
+
options?.proofTimeoutMs
|
|
3965
|
+
);
|
|
3966
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
3967
|
+
console.log("[InstantSplitProcessor] V4: Transfer proof received");
|
|
3968
|
+
const transferSalt = fromHex3(bundle.transferSaltHex);
|
|
3969
|
+
const finalPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
|
|
3970
|
+
mintData.tokenId,
|
|
3971
|
+
tokenType,
|
|
3972
|
+
signingService,
|
|
3973
|
+
import_HashAlgorithm4.HashAlgorithm.SHA256,
|
|
3974
|
+
transferSalt
|
|
3975
|
+
);
|
|
3976
|
+
const finalState = new import_TokenState4.TokenState(finalPredicate, null);
|
|
3977
|
+
const finalTokenJson = mintedToken.toJSON();
|
|
3978
|
+
finalTokenJson.state = finalState.toJSON();
|
|
3979
|
+
finalTokenJson.transactions = [transferTransaction.toJSON()];
|
|
3980
|
+
const finalToken = await import_Token5.Token.fromJSON(finalTokenJson);
|
|
3981
|
+
console.log("[InstantSplitProcessor] V4: Token finalized");
|
|
3982
|
+
const duration = performance.now() - startTime;
|
|
3983
|
+
console.log(`[InstantSplitProcessor] V4 bundle processed in ${duration.toFixed(0)}ms`);
|
|
3984
|
+
return {
|
|
3985
|
+
success: true,
|
|
3986
|
+
token: finalToken,
|
|
3987
|
+
durationMs: duration
|
|
3988
|
+
};
|
|
3989
|
+
} catch (error) {
|
|
3990
|
+
const duration = performance.now() - startTime;
|
|
3991
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3992
|
+
console.error(`[InstantSplitProcessor] V4 processing failed:`, error);
|
|
3993
|
+
return {
|
|
3994
|
+
success: false,
|
|
3995
|
+
error: errorMessage,
|
|
3996
|
+
durationMs: duration
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Dev mode bypass for waitInclusionProof.
|
|
4002
|
+
*/
|
|
4003
|
+
async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
|
|
4004
|
+
if (this.devMode) {
|
|
4005
|
+
try {
|
|
4006
|
+
return await Promise.race([
|
|
4007
|
+
(0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, commitment),
|
|
4008
|
+
new Promise(
|
|
4009
|
+
(_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
|
|
4010
|
+
)
|
|
4011
|
+
]);
|
|
4012
|
+
} catch {
|
|
4013
|
+
console.log("[InstantSplitProcessor] Dev mode: Using mock proof");
|
|
4014
|
+
return {
|
|
4015
|
+
toJSON: () => ({ mock: true })
|
|
4016
|
+
};
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
return (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, commitment);
|
|
4020
|
+
}
|
|
4021
|
+
};
|
|
4022
|
+
|
|
4023
|
+
// modules/payments/PaymentsModule.ts
|
|
4024
|
+
var import_Token6 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
4025
|
+
var import_CoinId4 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
|
|
4026
|
+
var import_TransferCommitment4 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
|
|
4027
|
+
var import_TransferTransaction2 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction");
|
|
3324
4028
|
var import_SigningService = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
|
|
3325
4029
|
var import_AddressScheme = require("@unicitylabs/state-transition-sdk/lib/address/AddressScheme");
|
|
3326
|
-
var
|
|
3327
|
-
var
|
|
3328
|
-
var
|
|
4030
|
+
var import_UnmaskedPredicate5 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
4031
|
+
var import_TokenState5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
4032
|
+
var import_HashAlgorithm5 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
3329
4033
|
function enrichWithRegistry(info) {
|
|
3330
4034
|
const registry = TokenRegistry.getInstance();
|
|
3331
4035
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -3351,7 +4055,7 @@ async function parseTokenInfo(tokenData) {
|
|
|
3351
4055
|
try {
|
|
3352
4056
|
const data = typeof tokenData === "string" ? JSON.parse(tokenData) : tokenData;
|
|
3353
4057
|
try {
|
|
3354
|
-
const sdkToken = await
|
|
4058
|
+
const sdkToken = await import_Token6.Token.fromJSON(data);
|
|
3355
4059
|
if (sdkToken.id) {
|
|
3356
4060
|
defaultInfo.tokenId = sdkToken.id.toString();
|
|
3357
4061
|
}
|
|
@@ -3364,7 +4068,7 @@ async function parseTokenInfo(tokenData) {
|
|
|
3364
4068
|
if (Array.isArray(firstCoin) && firstCoin.length === 2) {
|
|
3365
4069
|
[coinIdObj, amount] = firstCoin;
|
|
3366
4070
|
}
|
|
3367
|
-
if (coinIdObj instanceof
|
|
4071
|
+
if (coinIdObj instanceof import_CoinId4.CoinId) {
|
|
3368
4072
|
const coinIdHex = coinIdObj.toJSON();
|
|
3369
4073
|
return enrichWithRegistry({
|
|
3370
4074
|
coinId: coinIdHex,
|
|
@@ -3497,21 +4201,48 @@ function extractStateHashFromSdkData(sdkData) {
|
|
|
3497
4201
|
if (!sdkData) return "";
|
|
3498
4202
|
try {
|
|
3499
4203
|
const txf = JSON.parse(sdkData);
|
|
3500
|
-
|
|
4204
|
+
const stateHash = getCurrentStateHash(txf);
|
|
4205
|
+
if (!stateHash) {
|
|
4206
|
+
if (txf.state?.hash) {
|
|
4207
|
+
return txf.state.hash;
|
|
4208
|
+
}
|
|
4209
|
+
if (txf.stateHash) {
|
|
4210
|
+
return txf.stateHash;
|
|
4211
|
+
}
|
|
4212
|
+
if (txf.currentStateHash) {
|
|
4213
|
+
return txf.currentStateHash;
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
return stateHash || "";
|
|
3501
4217
|
} catch {
|
|
3502
4218
|
return "";
|
|
3503
4219
|
}
|
|
3504
4220
|
}
|
|
3505
|
-
function
|
|
3506
|
-
|
|
4221
|
+
function createTokenStateKey(tokenId, stateHash) {
|
|
4222
|
+
return `${tokenId}_${stateHash}`;
|
|
4223
|
+
}
|
|
4224
|
+
function extractTokenStateKey(token) {
|
|
4225
|
+
const tokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
4226
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
4227
|
+
if (!tokenId || !stateHash) return null;
|
|
4228
|
+
return createTokenStateKey(tokenId, stateHash);
|
|
4229
|
+
}
|
|
4230
|
+
function hasSameGenesisTokenId(t1, t2) {
|
|
3507
4231
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
3508
4232
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
3509
4233
|
return !!(id1 && id2 && id1 === id2);
|
|
3510
4234
|
}
|
|
4235
|
+
function isSameTokenState(t1, t2) {
|
|
4236
|
+
const key1 = extractTokenStateKey(t1);
|
|
4237
|
+
const key2 = extractTokenStateKey(t2);
|
|
4238
|
+
return !!(key1 && key2 && key1 === key2);
|
|
4239
|
+
}
|
|
3511
4240
|
function createTombstoneFromToken(token) {
|
|
3512
4241
|
const tokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
3513
|
-
if (!tokenId) return null;
|
|
3514
4242
|
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
4243
|
+
if (!tokenId || !stateHash) {
|
|
4244
|
+
return null;
|
|
4245
|
+
}
|
|
3515
4246
|
return {
|
|
3516
4247
|
tokenId,
|
|
3517
4248
|
stateHash,
|
|
@@ -3577,7 +4308,7 @@ function findBestTokenVersion(tokenId, archivedTokens, forkedTokens) {
|
|
|
3577
4308
|
candidates.sort((a, b) => countCommittedTxns(b) - countCommittedTxns(a));
|
|
3578
4309
|
return candidates[0];
|
|
3579
4310
|
}
|
|
3580
|
-
var PaymentsModule = class {
|
|
4311
|
+
var PaymentsModule = class _PaymentsModule {
|
|
3581
4312
|
moduleConfig;
|
|
3582
4313
|
deps = null;
|
|
3583
4314
|
/** L1 (ALPHA blockchain) payments sub-module (null if disabled) */
|
|
@@ -3602,6 +4333,13 @@ var PaymentsModule = class {
|
|
|
3602
4333
|
unsubscribeTransfers = null;
|
|
3603
4334
|
unsubscribePaymentRequests = null;
|
|
3604
4335
|
unsubscribePaymentRequestResponses = null;
|
|
4336
|
+
// NOSTR-FIRST proof polling (background proof verification)
|
|
4337
|
+
proofPollingJobs = /* @__PURE__ */ new Map();
|
|
4338
|
+
proofPollingInterval = null;
|
|
4339
|
+
static PROOF_POLLING_INTERVAL_MS = 2e3;
|
|
4340
|
+
// Poll every 2s
|
|
4341
|
+
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4342
|
+
// Max 30 attempts (~60s)
|
|
3605
4343
|
constructor(config) {
|
|
3606
4344
|
this.moduleConfig = {
|
|
3607
4345
|
autoSync: config?.autoSync ?? true,
|
|
@@ -3617,6 +4355,8 @@ var PaymentsModule = class {
|
|
|
3617
4355
|
getConfig() {
|
|
3618
4356
|
return this.moduleConfig;
|
|
3619
4357
|
}
|
|
4358
|
+
/** Price provider (optional) */
|
|
4359
|
+
priceProvider = null;
|
|
3620
4360
|
log(...args) {
|
|
3621
4361
|
if (this.moduleConfig.debug) {
|
|
3622
4362
|
console.log("[PaymentsModule]", ...args);
|
|
@@ -3629,7 +4369,21 @@ var PaymentsModule = class {
|
|
|
3629
4369
|
* Initialize module with dependencies
|
|
3630
4370
|
*/
|
|
3631
4371
|
initialize(deps) {
|
|
4372
|
+
this.unsubscribeTransfers?.();
|
|
4373
|
+
this.unsubscribeTransfers = null;
|
|
4374
|
+
this.unsubscribePaymentRequests?.();
|
|
4375
|
+
this.unsubscribePaymentRequests = null;
|
|
4376
|
+
this.unsubscribePaymentRequestResponses?.();
|
|
4377
|
+
this.unsubscribePaymentRequestResponses = null;
|
|
4378
|
+
this.tokens.clear();
|
|
4379
|
+
this.pendingTransfers.clear();
|
|
4380
|
+
this.tombstones = [];
|
|
4381
|
+
this.archivedTokens.clear();
|
|
4382
|
+
this.forkedTokens.clear();
|
|
4383
|
+
this.transactionHistory = [];
|
|
4384
|
+
this.nametag = null;
|
|
3632
4385
|
this.deps = deps;
|
|
4386
|
+
this.priceProvider = deps.price ?? null;
|
|
3633
4387
|
if (this.l1) {
|
|
3634
4388
|
this.l1.initialize({
|
|
3635
4389
|
identity: deps.identity,
|
|
@@ -3700,6 +4454,8 @@ var PaymentsModule = class {
|
|
|
3700
4454
|
this.unsubscribePaymentRequestResponses = null;
|
|
3701
4455
|
this.paymentRequestHandlers.clear();
|
|
3702
4456
|
this.paymentRequestResponseHandlers.clear();
|
|
4457
|
+
this.stopProofPolling();
|
|
4458
|
+
this.proofPollingJobs.clear();
|
|
3703
4459
|
for (const [, resolver] of this.pendingResponseResolvers) {
|
|
3704
4460
|
clearTimeout(resolver.timeout);
|
|
3705
4461
|
resolver.reject(new Error("Module destroyed"));
|
|
@@ -3724,8 +4480,9 @@ var PaymentsModule = class {
|
|
|
3724
4480
|
tokens: []
|
|
3725
4481
|
};
|
|
3726
4482
|
try {
|
|
3727
|
-
const
|
|
3728
|
-
const
|
|
4483
|
+
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
4484
|
+
const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
|
|
4485
|
+
const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
|
|
3729
4486
|
const signingService = await this.createSigningService();
|
|
3730
4487
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
3731
4488
|
if (!stClient) {
|
|
@@ -3745,7 +4502,6 @@ var PaymentsModule = class {
|
|
|
3745
4502
|
if (!splitPlan) {
|
|
3746
4503
|
throw new Error("Insufficient balance");
|
|
3747
4504
|
}
|
|
3748
|
-
this.log(`Split plan: requiresSplit=${splitPlan.requiresSplit}, directTokens=${splitPlan.tokensToTransferDirectly.length}`);
|
|
3749
4505
|
const tokensToSend = splitPlan.tokensToTransferDirectly.map((t) => t.uiToken);
|
|
3750
4506
|
if (splitPlan.tokenToSplit) {
|
|
3751
4507
|
tokensToSend.push(splitPlan.tokenToSplit.uiToken);
|
|
@@ -3788,11 +4544,13 @@ var PaymentsModule = class {
|
|
|
3788
4544
|
};
|
|
3789
4545
|
await this.addToken(changeToken, true);
|
|
3790
4546
|
this.log(`Change token saved: ${changeToken.id}, amount: ${changeToken.amount}`);
|
|
4547
|
+
console.log(`[Payments] Sending split token to ${recipientPubkey.slice(0, 8)}... via Nostr`);
|
|
3791
4548
|
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
3792
4549
|
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
3793
4550
|
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
3794
4551
|
memo: request.memo
|
|
3795
4552
|
});
|
|
4553
|
+
console.log(`[Payments] Split token sent successfully`);
|
|
3796
4554
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
3797
4555
|
result.txHash = "split-" + Date.now().toString(16);
|
|
3798
4556
|
this.log(`Split transfer completed`);
|
|
@@ -3811,11 +4569,13 @@ var PaymentsModule = class {
|
|
|
3811
4569
|
const transferTx = commitment.toTransaction(inclusionProof);
|
|
3812
4570
|
const requestIdBytes = commitment.requestId;
|
|
3813
4571
|
result.txHash = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4572
|
+
console.log(`[Payments] Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}... via Nostr`);
|
|
3814
4573
|
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
3815
4574
|
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
3816
4575
|
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
3817
4576
|
memo: request.memo
|
|
3818
4577
|
});
|
|
4578
|
+
console.log(`[Payments] Direct token sent successfully`);
|
|
3819
4579
|
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
3820
4580
|
await this.removeToken(token.id, recipientNametag);
|
|
3821
4581
|
}
|
|
@@ -3869,28 +4629,257 @@ var PaymentsModule = class {
|
|
|
3869
4629
|
return TokenRegistry.getInstance().getIconUrl(coinId) ?? void 0;
|
|
3870
4630
|
}
|
|
3871
4631
|
// ===========================================================================
|
|
3872
|
-
// Public API -
|
|
4632
|
+
// Public API - Instant Split (V5 Optimized)
|
|
3873
4633
|
// ===========================================================================
|
|
3874
4634
|
/**
|
|
3875
|
-
* Send
|
|
3876
|
-
*
|
|
3877
|
-
*
|
|
3878
|
-
*
|
|
4635
|
+
* Send tokens using INSTANT_SPLIT V5 optimized flow.
|
|
4636
|
+
*
|
|
4637
|
+
* This achieves ~2.3s critical path latency instead of ~42s by:
|
|
4638
|
+
* 1. Waiting only for burn proof (required)
|
|
4639
|
+
* 2. Creating transfer commitment from mint data (no mint proof needed)
|
|
4640
|
+
* 3. Sending bundle via Nostr immediately
|
|
4641
|
+
* 4. Processing mints in background
|
|
4642
|
+
*
|
|
4643
|
+
* @param request - Transfer request with recipient, amount, and coinId
|
|
4644
|
+
* @param options - Optional instant split configuration
|
|
4645
|
+
* @returns InstantSplitResult with timing info
|
|
3879
4646
|
*/
|
|
3880
|
-
async
|
|
4647
|
+
async sendInstant(request, options) {
|
|
3881
4648
|
this.ensureInitialized();
|
|
3882
|
-
|
|
3883
|
-
return {
|
|
3884
|
-
success: false,
|
|
3885
|
-
error: "Transport provider does not support payment requests"
|
|
3886
|
-
};
|
|
3887
|
-
}
|
|
4649
|
+
const startTime = performance.now();
|
|
3888
4650
|
try {
|
|
3889
|
-
const
|
|
3890
|
-
const
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
4651
|
+
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
4652
|
+
const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
|
|
4653
|
+
const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
|
|
4654
|
+
const signingService = await this.createSigningService();
|
|
4655
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
4656
|
+
if (!stClient) {
|
|
4657
|
+
throw new Error("State transition client not available");
|
|
4658
|
+
}
|
|
4659
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
4660
|
+
if (!trustBase) {
|
|
4661
|
+
throw new Error("Trust base not available");
|
|
4662
|
+
}
|
|
4663
|
+
const calculator = new TokenSplitCalculator();
|
|
4664
|
+
const availableTokens = Array.from(this.tokens.values());
|
|
4665
|
+
const splitPlan = await calculator.calculateOptimalSplit(
|
|
4666
|
+
availableTokens,
|
|
4667
|
+
BigInt(request.amount),
|
|
4668
|
+
request.coinId
|
|
4669
|
+
);
|
|
4670
|
+
if (!splitPlan) {
|
|
4671
|
+
throw new Error("Insufficient balance");
|
|
4672
|
+
}
|
|
4673
|
+
if (!splitPlan.requiresSplit || !splitPlan.tokenToSplit) {
|
|
4674
|
+
this.log("No split required, falling back to standard send()");
|
|
4675
|
+
const result2 = await this.send(request);
|
|
4676
|
+
return {
|
|
4677
|
+
success: result2.status === "completed",
|
|
4678
|
+
criticalPathDurationMs: performance.now() - startTime,
|
|
4679
|
+
error: result2.error
|
|
4680
|
+
};
|
|
4681
|
+
}
|
|
4682
|
+
this.log(`InstantSplit: amount=${splitPlan.splitAmount}, remainder=${splitPlan.remainderAmount}`);
|
|
4683
|
+
const tokenToSplit = splitPlan.tokenToSplit.uiToken;
|
|
4684
|
+
tokenToSplit.status = "transferring";
|
|
4685
|
+
this.tokens.set(tokenToSplit.id, tokenToSplit);
|
|
4686
|
+
const devMode = options?.devMode ?? this.deps.oracle.isDevMode?.() ?? false;
|
|
4687
|
+
const executor = new InstantSplitExecutor({
|
|
4688
|
+
stateTransitionClient: stClient,
|
|
4689
|
+
trustBase,
|
|
4690
|
+
signingService,
|
|
4691
|
+
devMode
|
|
4692
|
+
});
|
|
4693
|
+
const result = await executor.executeSplitInstant(
|
|
4694
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4695
|
+
splitPlan.splitAmount,
|
|
4696
|
+
splitPlan.remainderAmount,
|
|
4697
|
+
splitPlan.coinId,
|
|
4698
|
+
recipientAddress,
|
|
4699
|
+
this.deps.transport,
|
|
4700
|
+
recipientPubkey,
|
|
4701
|
+
{
|
|
4702
|
+
...options,
|
|
4703
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4704
|
+
const changeTokenData = changeToken.toJSON();
|
|
4705
|
+
const uiToken = {
|
|
4706
|
+
id: crypto.randomUUID(),
|
|
4707
|
+
coinId: request.coinId,
|
|
4708
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4709
|
+
name: this.getCoinName(request.coinId),
|
|
4710
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4711
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4712
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4713
|
+
status: "confirmed",
|
|
4714
|
+
createdAt: Date.now(),
|
|
4715
|
+
updatedAt: Date.now(),
|
|
4716
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4717
|
+
};
|
|
4718
|
+
await this.addToken(uiToken, true);
|
|
4719
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4720
|
+
},
|
|
4721
|
+
onStorageSync: async () => {
|
|
4722
|
+
await this.save();
|
|
4723
|
+
return true;
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
);
|
|
4727
|
+
if (result.success) {
|
|
4728
|
+
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4729
|
+
await this.removeToken(tokenToSplit.id, recipientNametag);
|
|
4730
|
+
await this.addToHistory({
|
|
4731
|
+
type: "SENT",
|
|
4732
|
+
amount: request.amount,
|
|
4733
|
+
coinId: request.coinId,
|
|
4734
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4735
|
+
timestamp: Date.now(),
|
|
4736
|
+
recipientNametag
|
|
4737
|
+
});
|
|
4738
|
+
await this.save();
|
|
4739
|
+
} else {
|
|
4740
|
+
tokenToSplit.status = "confirmed";
|
|
4741
|
+
this.tokens.set(tokenToSplit.id, tokenToSplit);
|
|
4742
|
+
}
|
|
4743
|
+
return result;
|
|
4744
|
+
} catch (error) {
|
|
4745
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4746
|
+
return {
|
|
4747
|
+
success: false,
|
|
4748
|
+
criticalPathDurationMs: performance.now() - startTime,
|
|
4749
|
+
error: errorMessage
|
|
4750
|
+
};
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
/**
|
|
4754
|
+
* Process a received INSTANT_SPLIT bundle.
|
|
4755
|
+
*
|
|
4756
|
+
* This should be called when receiving an instant split bundle via transport.
|
|
4757
|
+
* It handles the recipient-side processing:
|
|
4758
|
+
* 1. Validate burn transaction
|
|
4759
|
+
* 2. Submit and wait for mint proof
|
|
4760
|
+
* 3. Submit and wait for transfer proof
|
|
4761
|
+
* 4. Finalize and save the token
|
|
4762
|
+
*
|
|
4763
|
+
* @param bundle - The received InstantSplitBundle (V4 or V5)
|
|
4764
|
+
* @param senderPubkey - Sender's public key for verification
|
|
4765
|
+
* @returns Processing result with finalized token
|
|
4766
|
+
*/
|
|
4767
|
+
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4768
|
+
this.ensureInitialized();
|
|
4769
|
+
try {
|
|
4770
|
+
const signingService = await this.createSigningService();
|
|
4771
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
4772
|
+
if (!stClient) {
|
|
4773
|
+
throw new Error("State transition client not available");
|
|
4774
|
+
}
|
|
4775
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
4776
|
+
if (!trustBase) {
|
|
4777
|
+
throw new Error("Trust base not available");
|
|
4778
|
+
}
|
|
4779
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4780
|
+
const processor = new InstantSplitProcessor({
|
|
4781
|
+
stateTransitionClient: stClient,
|
|
4782
|
+
trustBase,
|
|
4783
|
+
devMode
|
|
4784
|
+
});
|
|
4785
|
+
const result = await processor.processReceivedBundle(
|
|
4786
|
+
bundle,
|
|
4787
|
+
signingService,
|
|
4788
|
+
senderPubkey,
|
|
4789
|
+
{
|
|
4790
|
+
findNametagToken: async (proxyAddress) => {
|
|
4791
|
+
if (this.nametag?.token) {
|
|
4792
|
+
try {
|
|
4793
|
+
const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
|
|
4794
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
4795
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
4796
|
+
if (proxy.address === proxyAddress) {
|
|
4797
|
+
return nametagToken;
|
|
4798
|
+
}
|
|
4799
|
+
this.log(`Nametag PROXY address mismatch: ${proxy.address} !== ${proxyAddress}`);
|
|
4800
|
+
return null;
|
|
4801
|
+
} catch (err) {
|
|
4802
|
+
this.log("Failed to parse nametag token:", err);
|
|
4803
|
+
return null;
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
return null;
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
);
|
|
4810
|
+
if (result.success && result.token) {
|
|
4811
|
+
const tokenData = result.token.toJSON();
|
|
4812
|
+
const info = await parseTokenInfo(tokenData);
|
|
4813
|
+
const uiToken = {
|
|
4814
|
+
id: crypto.randomUUID(),
|
|
4815
|
+
coinId: info.coinId,
|
|
4816
|
+
symbol: info.symbol,
|
|
4817
|
+
name: info.name,
|
|
4818
|
+
decimals: info.decimals,
|
|
4819
|
+
iconUrl: info.iconUrl,
|
|
4820
|
+
amount: bundle.amount,
|
|
4821
|
+
status: "confirmed",
|
|
4822
|
+
createdAt: Date.now(),
|
|
4823
|
+
updatedAt: Date.now(),
|
|
4824
|
+
sdkData: JSON.stringify(tokenData)
|
|
4825
|
+
};
|
|
4826
|
+
await this.addToken(uiToken);
|
|
4827
|
+
await this.addToHistory({
|
|
4828
|
+
type: "RECEIVED",
|
|
4829
|
+
amount: bundle.amount,
|
|
4830
|
+
coinId: info.coinId,
|
|
4831
|
+
symbol: info.symbol,
|
|
4832
|
+
timestamp: Date.now(),
|
|
4833
|
+
senderPubkey
|
|
4834
|
+
});
|
|
4835
|
+
await this.save();
|
|
4836
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
4837
|
+
id: bundle.splitGroupId,
|
|
4838
|
+
senderPubkey,
|
|
4839
|
+
tokens: [uiToken],
|
|
4840
|
+
receivedAt: Date.now()
|
|
4841
|
+
});
|
|
4842
|
+
}
|
|
4843
|
+
return result;
|
|
4844
|
+
} catch (error) {
|
|
4845
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4846
|
+
return {
|
|
4847
|
+
success: false,
|
|
4848
|
+
error: errorMessage,
|
|
4849
|
+
durationMs: 0
|
|
4850
|
+
};
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
/**
|
|
4854
|
+
* Check if a payload is an instant split bundle
|
|
4855
|
+
*/
|
|
4856
|
+
isInstantSplitBundle(payload) {
|
|
4857
|
+
return isInstantSplitBundle(payload);
|
|
4858
|
+
}
|
|
4859
|
+
// ===========================================================================
|
|
4860
|
+
// Public API - Payment Requests
|
|
4861
|
+
// ===========================================================================
|
|
4862
|
+
/**
|
|
4863
|
+
* Send a payment request to someone
|
|
4864
|
+
* @param recipientPubkeyOrNametag - Recipient's pubkey or @nametag
|
|
4865
|
+
* @param request - Payment request details
|
|
4866
|
+
* @returns Result with event ID
|
|
4867
|
+
*/
|
|
4868
|
+
async sendPaymentRequest(recipientPubkeyOrNametag, request) {
|
|
4869
|
+
this.ensureInitialized();
|
|
4870
|
+
if (!this.deps.transport.sendPaymentRequest) {
|
|
4871
|
+
return {
|
|
4872
|
+
success: false,
|
|
4873
|
+
error: "Transport provider does not support payment requests"
|
|
4874
|
+
};
|
|
4875
|
+
}
|
|
4876
|
+
try {
|
|
4877
|
+
const peerInfo = await this.deps.transport.resolve?.(recipientPubkeyOrNametag) ?? null;
|
|
4878
|
+
const recipientPubkey = this.resolveTransportPubkey(recipientPubkeyOrNametag, peerInfo);
|
|
4879
|
+
const payload = {
|
|
4880
|
+
amount: request.amount,
|
|
4881
|
+
coinId: request.coinId,
|
|
4882
|
+
message: request.message,
|
|
3894
4883
|
recipientNametag: request.recipientNametag,
|
|
3895
4884
|
metadata: request.metadata
|
|
3896
4885
|
};
|
|
@@ -4190,47 +5179,46 @@ var PaymentsModule = class {
|
|
|
4190
5179
|
// Public API - Balance & Tokens
|
|
4191
5180
|
// ===========================================================================
|
|
4192
5181
|
/**
|
|
4193
|
-
*
|
|
5182
|
+
* Set or update price provider
|
|
4194
5183
|
*/
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
});
|
|
5184
|
+
setPriceProvider(provider) {
|
|
5185
|
+
this.priceProvider = provider;
|
|
5186
|
+
}
|
|
5187
|
+
/**
|
|
5188
|
+
* Get total portfolio value in USD
|
|
5189
|
+
* Returns null if PriceProvider is not configured
|
|
5190
|
+
*/
|
|
5191
|
+
async getBalance() {
|
|
5192
|
+
const assets = await this.getAssets();
|
|
5193
|
+
if (!this.priceProvider) {
|
|
5194
|
+
return null;
|
|
5195
|
+
}
|
|
5196
|
+
let total = 0;
|
|
5197
|
+
let hasAnyPrice = false;
|
|
5198
|
+
for (const asset of assets) {
|
|
5199
|
+
if (asset.fiatValueUsd != null) {
|
|
5200
|
+
total += asset.fiatValueUsd;
|
|
5201
|
+
hasAnyPrice = true;
|
|
4214
5202
|
}
|
|
4215
5203
|
}
|
|
4216
|
-
return
|
|
5204
|
+
return hasAnyPrice ? total : null;
|
|
4217
5205
|
}
|
|
4218
5206
|
/**
|
|
4219
|
-
* Get aggregated assets (tokens grouped by coinId)
|
|
5207
|
+
* Get aggregated assets (tokens grouped by coinId) with price data
|
|
4220
5208
|
* Only includes confirmed tokens
|
|
4221
5209
|
*/
|
|
4222
|
-
getAssets(coinId) {
|
|
4223
|
-
const
|
|
5210
|
+
async getAssets(coinId) {
|
|
5211
|
+
const assetsMap = /* @__PURE__ */ new Map();
|
|
4224
5212
|
for (const token of this.tokens.values()) {
|
|
4225
5213
|
if (token.status !== "confirmed") continue;
|
|
4226
5214
|
if (coinId && token.coinId !== coinId) continue;
|
|
4227
5215
|
const key = token.coinId;
|
|
4228
|
-
const existing =
|
|
5216
|
+
const existing = assetsMap.get(key);
|
|
4229
5217
|
if (existing) {
|
|
4230
5218
|
existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
|
|
4231
5219
|
existing.tokenCount++;
|
|
4232
5220
|
} else {
|
|
4233
|
-
|
|
5221
|
+
assetsMap.set(key, {
|
|
4234
5222
|
coinId: token.coinId,
|
|
4235
5223
|
symbol: token.symbol,
|
|
4236
5224
|
name: token.name,
|
|
@@ -4241,7 +5229,66 @@ var PaymentsModule = class {
|
|
|
4241
5229
|
});
|
|
4242
5230
|
}
|
|
4243
5231
|
}
|
|
4244
|
-
|
|
5232
|
+
const rawAssets = Array.from(assetsMap.values());
|
|
5233
|
+
let priceMap = null;
|
|
5234
|
+
if (this.priceProvider && rawAssets.length > 0) {
|
|
5235
|
+
const registry = TokenRegistry.getInstance();
|
|
5236
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5237
|
+
for (const asset of rawAssets) {
|
|
5238
|
+
const def = registry.getDefinition(asset.coinId);
|
|
5239
|
+
if (def?.name) {
|
|
5240
|
+
const existing = nameToCoins.get(def.name);
|
|
5241
|
+
if (existing) {
|
|
5242
|
+
existing.push(asset.coinId);
|
|
5243
|
+
} else {
|
|
5244
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
if (nameToCoins.size > 0) {
|
|
5249
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
5250
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5251
|
+
priceMap = /* @__PURE__ */ new Map();
|
|
5252
|
+
for (const [name, coinIds] of nameToCoins) {
|
|
5253
|
+
const price = prices.get(name);
|
|
5254
|
+
if (price) {
|
|
5255
|
+
for (const cid of coinIds) {
|
|
5256
|
+
priceMap.set(cid, {
|
|
5257
|
+
priceUsd: price.priceUsd,
|
|
5258
|
+
priceEur: price.priceEur,
|
|
5259
|
+
change24h: price.change24h
|
|
5260
|
+
});
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
return rawAssets.map((raw) => {
|
|
5267
|
+
const price = priceMap?.get(raw.coinId);
|
|
5268
|
+
let fiatValueUsd = null;
|
|
5269
|
+
let fiatValueEur = null;
|
|
5270
|
+
if (price) {
|
|
5271
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5272
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5273
|
+
if (price.priceEur != null) {
|
|
5274
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
return {
|
|
5278
|
+
coinId: raw.coinId,
|
|
5279
|
+
symbol: raw.symbol,
|
|
5280
|
+
name: raw.name,
|
|
5281
|
+
decimals: raw.decimals,
|
|
5282
|
+
iconUrl: raw.iconUrl,
|
|
5283
|
+
totalAmount: raw.totalAmount,
|
|
5284
|
+
tokenCount: raw.tokenCount,
|
|
5285
|
+
priceUsd: price?.priceUsd ?? null,
|
|
5286
|
+
priceEur: price?.priceEur ?? null,
|
|
5287
|
+
change24h: price?.change24h ?? null,
|
|
5288
|
+
fiatValueUsd,
|
|
5289
|
+
fiatValueEur
|
|
5290
|
+
};
|
|
5291
|
+
});
|
|
4245
5292
|
}
|
|
4246
5293
|
/**
|
|
4247
5294
|
* Get all tokens
|
|
@@ -4267,14 +5314,52 @@ var PaymentsModule = class {
|
|
|
4267
5314
|
// ===========================================================================
|
|
4268
5315
|
/**
|
|
4269
5316
|
* Add a token
|
|
4270
|
-
*
|
|
5317
|
+
* Tokens are uniquely identified by (tokenId, stateHash) composite key.
|
|
5318
|
+
* Multiple historic states of the same token can coexist.
|
|
5319
|
+
* @returns false if exact duplicate (same tokenId AND same stateHash)
|
|
4271
5320
|
*/
|
|
4272
5321
|
async addToken(token, skipHistory = false) {
|
|
4273
5322
|
this.ensureInitialized();
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
5323
|
+
const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5324
|
+
const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
5325
|
+
const incomingStateKey = incomingTokenId && incomingStateHash ? createTokenStateKey(incomingTokenId, incomingStateHash) : null;
|
|
5326
|
+
if (incomingTokenId && incomingStateHash && this.isStateTombstoned(incomingTokenId, incomingStateHash)) {
|
|
5327
|
+
this.log(`Rejecting tombstoned token: ${incomingTokenId.slice(0, 8)}..._${incomingStateHash.slice(0, 8)}...`);
|
|
5328
|
+
return false;
|
|
5329
|
+
}
|
|
5330
|
+
if (incomingStateKey) {
|
|
5331
|
+
for (const [existingId, existing] of this.tokens) {
|
|
5332
|
+
if (isSameTokenState(existing, token)) {
|
|
5333
|
+
this.log(`Duplicate token state ignored: ${incomingTokenId?.slice(0, 8)}..._${incomingStateHash?.slice(0, 8)}...`);
|
|
5334
|
+
return false;
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
for (const [existingId, existing] of this.tokens) {
|
|
5339
|
+
if (hasSameGenesisTokenId(existing, token)) {
|
|
5340
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
5341
|
+
if (incomingStateHash && existingStateHash && incomingStateHash === existingStateHash) {
|
|
5342
|
+
continue;
|
|
5343
|
+
}
|
|
5344
|
+
if (existing.status === "spent" || existing.status === "invalid") {
|
|
5345
|
+
this.log(`Replacing spent/invalid token ${incomingTokenId?.slice(0, 8)}...`);
|
|
5346
|
+
this.tokens.delete(existingId);
|
|
5347
|
+
break;
|
|
5348
|
+
}
|
|
5349
|
+
if (incomingStateHash && existingStateHash && incomingStateHash !== existingStateHash) {
|
|
5350
|
+
this.log(`Token ${incomingTokenId?.slice(0, 8)}... state updated: ${existingStateHash.slice(0, 8)}... -> ${incomingStateHash.slice(0, 8)}...`);
|
|
5351
|
+
await this.archiveToken(existing);
|
|
5352
|
+
this.tokens.delete(existingId);
|
|
5353
|
+
break;
|
|
5354
|
+
}
|
|
5355
|
+
if (!incomingStateHash || !existingStateHash) {
|
|
5356
|
+
if (existingId !== token.id) {
|
|
5357
|
+
this.log(`Token ${incomingTokenId?.slice(0, 8)}... .id changed, replacing`);
|
|
5358
|
+
await this.archiveToken(existing);
|
|
5359
|
+
this.tokens.delete(existingId);
|
|
5360
|
+
break;
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
4278
5363
|
}
|
|
4279
5364
|
}
|
|
4280
5365
|
this.tokens.set(token.id, token);
|
|
@@ -4429,8 +5514,10 @@ var PaymentsModule = class {
|
|
|
4429
5514
|
);
|
|
4430
5515
|
if (!alreadyTombstoned) {
|
|
4431
5516
|
this.tombstones.push(tombstone);
|
|
4432
|
-
this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}...`);
|
|
5517
|
+
this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}..._${tombstone.stateHash.slice(0, 8)}...`);
|
|
4433
5518
|
}
|
|
5519
|
+
} else {
|
|
5520
|
+
this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
|
|
4434
5521
|
}
|
|
4435
5522
|
this.tokens.delete(tokenId);
|
|
4436
5523
|
if (!skipHistory && token.coinId && token.amount) {
|
|
@@ -4748,15 +5835,15 @@ var PaymentsModule = class {
|
|
|
4748
5835
|
}
|
|
4749
5836
|
try {
|
|
4750
5837
|
const signingService = await this.createSigningService();
|
|
4751
|
-
const { UnmaskedPredicateReference:
|
|
4752
|
-
const { TokenType:
|
|
5838
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5839
|
+
const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
4753
5840
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
4754
|
-
const tokenType = new
|
|
4755
|
-
const addressRef = await
|
|
5841
|
+
const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5842
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
4756
5843
|
tokenType,
|
|
4757
5844
|
signingService.algorithm,
|
|
4758
5845
|
signingService.publicKey,
|
|
4759
|
-
|
|
5846
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256
|
|
4760
5847
|
);
|
|
4761
5848
|
const ownerAddress = await addressRef.toAddress();
|
|
4762
5849
|
const minter = new NametagMinter({
|
|
@@ -4923,40 +6010,22 @@ var PaymentsModule = class {
|
|
|
4923
6010
|
* Detect if a string is an L3 address (not a nametag)
|
|
4924
6011
|
* Returns true for: hex pubkeys (64+ chars), PROXY:, DIRECT: prefixed addresses
|
|
4925
6012
|
*/
|
|
4926
|
-
isL3Address(value) {
|
|
4927
|
-
if (value.startsWith("PROXY:") || value.startsWith("DIRECT:")) {
|
|
4928
|
-
return true;
|
|
4929
|
-
}
|
|
4930
|
-
if (value.length >= 64 && /^[0-9a-fA-F]+$/.test(value)) {
|
|
4931
|
-
return true;
|
|
4932
|
-
}
|
|
4933
|
-
return false;
|
|
4934
|
-
}
|
|
4935
6013
|
/**
|
|
4936
|
-
* Resolve recipient to
|
|
4937
|
-
*
|
|
6014
|
+
* Resolve recipient to transport pubkey for messaging.
|
|
6015
|
+
* Uses pre-resolved PeerInfo if available, otherwise resolves via transport.
|
|
4938
6016
|
*/
|
|
4939
|
-
|
|
4940
|
-
if (
|
|
4941
|
-
|
|
4942
|
-
const pubkey = await this.deps.transport.resolveNametag?.(nametag);
|
|
4943
|
-
if (!pubkey) {
|
|
4944
|
-
throw new Error(`Nametag not found: ${nametag}`);
|
|
4945
|
-
}
|
|
4946
|
-
return pubkey;
|
|
6017
|
+
resolveTransportPubkey(recipient, peerInfo) {
|
|
6018
|
+
if (peerInfo?.transportPubkey) {
|
|
6019
|
+
return peerInfo.transportPubkey;
|
|
4947
6020
|
}
|
|
4948
|
-
if (
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
if (this.deps?.transport.resolveNametag) {
|
|
4952
|
-
const pubkey = await this.deps.transport.resolveNametag(recipient);
|
|
4953
|
-
if (pubkey) {
|
|
4954
|
-
this.log(`Resolved "${recipient}" as nametag to pubkey`);
|
|
4955
|
-
return pubkey;
|
|
6021
|
+
if (recipient.length >= 64 && /^[0-9a-fA-F]+$/.test(recipient)) {
|
|
6022
|
+
if (recipient.length === 66 && (recipient.startsWith("02") || recipient.startsWith("03"))) {
|
|
6023
|
+
return recipient.slice(2);
|
|
4956
6024
|
}
|
|
6025
|
+
return recipient;
|
|
4957
6026
|
}
|
|
4958
6027
|
throw new Error(
|
|
4959
|
-
`
|
|
6028
|
+
`Cannot resolve transport pubkey for "${recipient}". No binding event found. The recipient must publish their identity first.`
|
|
4960
6029
|
);
|
|
4961
6030
|
}
|
|
4962
6031
|
/**
|
|
@@ -4964,9 +6033,9 @@ var PaymentsModule = class {
|
|
|
4964
6033
|
*/
|
|
4965
6034
|
async createSdkCommitment(token, recipientAddress, signingService) {
|
|
4966
6035
|
const tokenData = token.sdkData ? typeof token.sdkData === "string" ? JSON.parse(token.sdkData) : token.sdkData : token;
|
|
4967
|
-
const sdkToken = await
|
|
6036
|
+
const sdkToken = await import_Token6.Token.fromJSON(tokenData);
|
|
4968
6037
|
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
4969
|
-
const commitment = await
|
|
6038
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.create(
|
|
4970
6039
|
sdkToken,
|
|
4971
6040
|
recipientAddress,
|
|
4972
6041
|
salt,
|
|
@@ -4992,75 +6061,264 @@ var PaymentsModule = class {
|
|
|
4992
6061
|
* Create DirectAddress from a public key using UnmaskedPredicateReference
|
|
4993
6062
|
*/
|
|
4994
6063
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
4995
|
-
const { UnmaskedPredicateReference:
|
|
4996
|
-
const { TokenType:
|
|
6064
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
6065
|
+
const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
4997
6066
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
4998
|
-
const tokenType = new
|
|
6067
|
+
const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
4999
6068
|
const pubkeyBytes = new Uint8Array(
|
|
5000
6069
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
5001
6070
|
);
|
|
5002
|
-
const addressRef = await
|
|
6071
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
5003
6072
|
tokenType,
|
|
5004
6073
|
"secp256k1",
|
|
5005
6074
|
pubkeyBytes,
|
|
5006
|
-
|
|
6075
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256
|
|
5007
6076
|
);
|
|
5008
6077
|
return addressRef.toAddress();
|
|
5009
6078
|
}
|
|
5010
6079
|
/**
|
|
5011
|
-
* Resolve
|
|
5012
|
-
*
|
|
6080
|
+
* Resolve recipient to IAddress for L3 transfers.
|
|
6081
|
+
* Uses pre-resolved PeerInfo when available to avoid redundant network queries.
|
|
5013
6082
|
*/
|
|
5014
|
-
async
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
6083
|
+
async resolveRecipientAddress(recipient, addressMode = "auto", peerInfo) {
|
|
6084
|
+
const { AddressFactory } = await import("@unicitylabs/state-transition-sdk/lib/address/AddressFactory");
|
|
6085
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
6086
|
+
if (recipient.startsWith("PROXY:") || recipient.startsWith("DIRECT:")) {
|
|
6087
|
+
return AddressFactory.createAddress(recipient);
|
|
5018
6088
|
}
|
|
5019
|
-
|
|
6089
|
+
if (recipient.length === 66 && /^[0-9a-fA-F]+$/.test(recipient)) {
|
|
6090
|
+
this.log(`Creating DirectAddress from 33-byte compressed pubkey`);
|
|
6091
|
+
return this.createDirectAddressFromPubkey(recipient);
|
|
6092
|
+
}
|
|
6093
|
+
const info = peerInfo ?? await this.deps?.transport.resolve?.(recipient) ?? null;
|
|
5020
6094
|
if (!info) {
|
|
5021
|
-
|
|
5022
|
-
|
|
6095
|
+
throw new Error(
|
|
6096
|
+
`Recipient "${recipient}" not found. Use @nametag, a valid PROXY:/DIRECT: address, or a 33-byte hex pubkey.`
|
|
6097
|
+
);
|
|
5023
6098
|
}
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
6099
|
+
const nametag = recipient.startsWith("@") ? recipient.slice(1) : info.nametag || recipient;
|
|
6100
|
+
if (addressMode === "proxy") {
|
|
6101
|
+
console.log(`[Payments] Using PROXY address for "${nametag}" (forced)`);
|
|
6102
|
+
return ProxyAddress.fromNameTag(nametag);
|
|
6103
|
+
}
|
|
6104
|
+
if (addressMode === "direct") {
|
|
6105
|
+
if (!info.directAddress) {
|
|
6106
|
+
throw new Error(`"${nametag}" has no DirectAddress stored. It may be a legacy registration.`);
|
|
6107
|
+
}
|
|
6108
|
+
console.log(`[Payments] Using DirectAddress for "${nametag}" (forced): ${info.directAddress.slice(0, 30)}...`);
|
|
6109
|
+
return AddressFactory.createAddress(info.directAddress);
|
|
6110
|
+
}
|
|
6111
|
+
if (info.directAddress) {
|
|
6112
|
+
this.log(`Using DirectAddress for "${nametag}": ${info.directAddress.slice(0, 30)}...`);
|
|
6113
|
+
return AddressFactory.createAddress(info.directAddress);
|
|
5027
6114
|
}
|
|
5028
|
-
|
|
6115
|
+
this.log(`Using PROXY address for legacy nametag "${nametag}"`);
|
|
6116
|
+
return ProxyAddress.fromNameTag(nametag);
|
|
5029
6117
|
}
|
|
5030
6118
|
/**
|
|
5031
|
-
*
|
|
5032
|
-
*
|
|
6119
|
+
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
6120
|
+
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
6121
|
+
* We create the token as 'submitted', submit commitment (idempotent), and poll for proof.
|
|
5033
6122
|
*/
|
|
5034
|
-
async
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
const
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
return this.createDirectAddressFromPubkey(publicKey2);
|
|
6123
|
+
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
6124
|
+
try {
|
|
6125
|
+
const sourceTokenInput = typeof payload.sourceToken === "string" ? JSON.parse(payload.sourceToken) : payload.sourceToken;
|
|
6126
|
+
const commitmentInput = typeof payload.commitmentData === "string" ? JSON.parse(payload.commitmentData) : payload.commitmentData;
|
|
6127
|
+
if (!sourceTokenInput || !commitmentInput) {
|
|
6128
|
+
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
6129
|
+
return;
|
|
5042
6130
|
}
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
6131
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
6132
|
+
const token = {
|
|
6133
|
+
id: tokenInfo.tokenId ?? crypto.randomUUID(),
|
|
6134
|
+
coinId: tokenInfo.coinId,
|
|
6135
|
+
symbol: tokenInfo.symbol,
|
|
6136
|
+
name: tokenInfo.name,
|
|
6137
|
+
decimals: tokenInfo.decimals,
|
|
6138
|
+
iconUrl: tokenInfo.iconUrl,
|
|
6139
|
+
amount: tokenInfo.amount,
|
|
6140
|
+
status: "submitted",
|
|
6141
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
6142
|
+
createdAt: Date.now(),
|
|
6143
|
+
updatedAt: Date.now(),
|
|
6144
|
+
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
6145
|
+
};
|
|
6146
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6147
|
+
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
6148
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
6149
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
6150
|
+
return;
|
|
6151
|
+
}
|
|
6152
|
+
this.tokens.set(token.id, token);
|
|
6153
|
+
await this.save();
|
|
6154
|
+
this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
|
|
6155
|
+
const incomingTransfer = {
|
|
6156
|
+
id: transfer.id,
|
|
6157
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
6158
|
+
tokens: [token],
|
|
6159
|
+
memo: payload.memo,
|
|
6160
|
+
receivedAt: transfer.timestamp
|
|
6161
|
+
};
|
|
6162
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
6163
|
+
try {
|
|
6164
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
6165
|
+
const requestIdBytes = commitment.requestId;
|
|
6166
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
6167
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6168
|
+
if (stClient) {
|
|
6169
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
6170
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
6171
|
+
}
|
|
6172
|
+
this.addProofPollingJob({
|
|
6173
|
+
tokenId: token.id,
|
|
6174
|
+
requestIdHex,
|
|
6175
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
6176
|
+
startedAt: Date.now(),
|
|
6177
|
+
attemptCount: 0,
|
|
6178
|
+
lastAttemptAt: 0,
|
|
6179
|
+
onProofReceived: async (tokenId) => {
|
|
6180
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
|
|
6181
|
+
}
|
|
6182
|
+
});
|
|
6183
|
+
} catch (err) {
|
|
6184
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
6185
|
+
}
|
|
6186
|
+
} catch (error) {
|
|
6187
|
+
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
5051
6188
|
}
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
6189
|
+
}
|
|
6190
|
+
/**
|
|
6191
|
+
* Shared finalization logic for received transfers.
|
|
6192
|
+
* Handles both PROXY (with nametag token + address validation) and DIRECT schemes.
|
|
6193
|
+
*/
|
|
6194
|
+
async finalizeTransferToken(sourceToken, transferTx, stClient, trustBase) {
|
|
6195
|
+
const recipientAddress = transferTx.data.recipient;
|
|
6196
|
+
const addressScheme = recipientAddress.scheme;
|
|
6197
|
+
const signingService = await this.createSigningService();
|
|
6198
|
+
const transferSalt = transferTx.data.salt;
|
|
6199
|
+
const recipientPredicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
|
|
6200
|
+
sourceToken.id,
|
|
6201
|
+
sourceToken.type,
|
|
6202
|
+
signingService,
|
|
6203
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256,
|
|
6204
|
+
transferSalt
|
|
6205
|
+
);
|
|
6206
|
+
const recipientState = new import_TokenState5.TokenState(recipientPredicate, null);
|
|
6207
|
+
let nametagTokens = [];
|
|
6208
|
+
if (addressScheme === import_AddressScheme.AddressScheme.PROXY) {
|
|
6209
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
6210
|
+
if (!this.nametag?.token) {
|
|
6211
|
+
throw new Error("Cannot finalize PROXY transfer - no nametag token");
|
|
6212
|
+
}
|
|
6213
|
+
const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
|
|
6214
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
6215
|
+
if (proxy.address !== recipientAddress.address) {
|
|
6216
|
+
throw new Error(
|
|
6217
|
+
`PROXY address mismatch: nametag resolves to ${proxy.address} but transfer targets ${recipientAddress.address}`
|
|
6218
|
+
);
|
|
6219
|
+
}
|
|
6220
|
+
nametagTokens = [nametagToken];
|
|
5056
6221
|
}
|
|
5057
|
-
|
|
5058
|
-
|
|
6222
|
+
return stClient.finalizeTransaction(
|
|
6223
|
+
trustBase,
|
|
6224
|
+
sourceToken,
|
|
6225
|
+
recipientState,
|
|
6226
|
+
transferTx,
|
|
6227
|
+
nametagTokens
|
|
5059
6228
|
);
|
|
5060
6229
|
}
|
|
6230
|
+
/**
|
|
6231
|
+
* Finalize a received token after proof is available
|
|
6232
|
+
*/
|
|
6233
|
+
async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
|
|
6234
|
+
try {
|
|
6235
|
+
const token = this.tokens.get(tokenId);
|
|
6236
|
+
if (!token) {
|
|
6237
|
+
this.log(`Token ${tokenId} not found for finalization`);
|
|
6238
|
+
return;
|
|
6239
|
+
}
|
|
6240
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
6241
|
+
if (!this.deps.oracle.waitForProofSdk) {
|
|
6242
|
+
this.log("Cannot finalize - no waitForProofSdk");
|
|
6243
|
+
token.status = "confirmed";
|
|
6244
|
+
token.updatedAt = Date.now();
|
|
6245
|
+
await this.save();
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
6249
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
6250
|
+
const sourceToken = await import_Token6.Token.fromJSON(sourceTokenInput);
|
|
6251
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6252
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
6253
|
+
if (!stClient || !trustBase) {
|
|
6254
|
+
this.log("Cannot finalize - missing state transition client or trust base");
|
|
6255
|
+
token.status = "confirmed";
|
|
6256
|
+
token.updatedAt = Date.now();
|
|
6257
|
+
await this.save();
|
|
6258
|
+
return;
|
|
6259
|
+
}
|
|
6260
|
+
const finalizedSdkToken = await this.finalizeTransferToken(
|
|
6261
|
+
sourceToken,
|
|
6262
|
+
transferTx,
|
|
6263
|
+
stClient,
|
|
6264
|
+
trustBase
|
|
6265
|
+
);
|
|
6266
|
+
const finalizedToken = {
|
|
6267
|
+
...token,
|
|
6268
|
+
status: "confirmed",
|
|
6269
|
+
updatedAt: Date.now(),
|
|
6270
|
+
sdkData: JSON.stringify(finalizedSdkToken.toJSON())
|
|
6271
|
+
};
|
|
6272
|
+
this.tokens.set(tokenId, finalizedToken);
|
|
6273
|
+
await this.save();
|
|
6274
|
+
await this.saveTokenToFileStorage(finalizedToken);
|
|
6275
|
+
this.log(`NOSTR-FIRST: Token ${tokenId.slice(0, 8)}... finalized and confirmed`);
|
|
6276
|
+
this.deps.emitEvent("transfer:confirmed", {
|
|
6277
|
+
id: crypto.randomUUID(),
|
|
6278
|
+
status: "completed",
|
|
6279
|
+
tokens: [finalizedToken]
|
|
6280
|
+
});
|
|
6281
|
+
await this.addToHistory({
|
|
6282
|
+
type: "RECEIVED",
|
|
6283
|
+
amount: finalizedToken.amount,
|
|
6284
|
+
coinId: finalizedToken.coinId,
|
|
6285
|
+
symbol: finalizedToken.symbol,
|
|
6286
|
+
timestamp: Date.now(),
|
|
6287
|
+
senderPubkey
|
|
6288
|
+
});
|
|
6289
|
+
} catch (error) {
|
|
6290
|
+
console.error("[Payments] Failed to finalize received token:", error);
|
|
6291
|
+
const token = this.tokens.get(tokenId);
|
|
6292
|
+
if (token && token.status === "submitted") {
|
|
6293
|
+
token.status = "confirmed";
|
|
6294
|
+
token.updatedAt = Date.now();
|
|
6295
|
+
await this.save();
|
|
6296
|
+
}
|
|
6297
|
+
}
|
|
6298
|
+
}
|
|
5061
6299
|
async handleIncomingTransfer(transfer) {
|
|
5062
6300
|
try {
|
|
5063
6301
|
const payload = transfer.payload;
|
|
6302
|
+
if (isInstantSplitBundle(payload)) {
|
|
6303
|
+
this.log("Processing INSTANT_SPLIT bundle...");
|
|
6304
|
+
try {
|
|
6305
|
+
if (!this.nametag) {
|
|
6306
|
+
await this.loadNametagFromFileStorage();
|
|
6307
|
+
}
|
|
6308
|
+
const result = await this.processInstantSplitBundle(
|
|
6309
|
+
payload,
|
|
6310
|
+
transfer.senderTransportPubkey
|
|
6311
|
+
);
|
|
6312
|
+
if (result.success) {
|
|
6313
|
+
this.log("INSTANT_SPLIT processed successfully");
|
|
6314
|
+
} else {
|
|
6315
|
+
console.warn("[Payments] INSTANT_SPLIT processing failed:", result.error);
|
|
6316
|
+
}
|
|
6317
|
+
} catch (err) {
|
|
6318
|
+
console.error("[Payments] INSTANT_SPLIT processing error:", err);
|
|
6319
|
+
}
|
|
6320
|
+
return;
|
|
6321
|
+
}
|
|
5064
6322
|
let tokenData;
|
|
5065
6323
|
let finalizedSdkToken = null;
|
|
5066
6324
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -5071,82 +6329,71 @@ var PaymentsModule = class {
|
|
|
5071
6329
|
console.warn("[Payments] Invalid Sphere wallet transfer format");
|
|
5072
6330
|
return;
|
|
5073
6331
|
}
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
);
|
|
5095
|
-
const recipientState = new import_TokenState3.TokenState(recipientPredicate, null);
|
|
5096
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5097
|
-
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5098
|
-
if (!stClient || !trustBase) {
|
|
5099
|
-
console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
|
|
5100
|
-
return;
|
|
5101
|
-
}
|
|
5102
|
-
finalizedSdkToken = await stClient.finalizeTransaction(
|
|
5103
|
-
trustBase,
|
|
5104
|
-
sourceToken,
|
|
5105
|
-
recipientState,
|
|
5106
|
-
transferTx,
|
|
5107
|
-
[nametagToken]
|
|
5108
|
-
);
|
|
5109
|
-
tokenData = finalizedSdkToken.toJSON();
|
|
5110
|
-
this.log("Token finalized successfully");
|
|
5111
|
-
} catch (finalizeError) {
|
|
5112
|
-
console.error("[Payments] Finalization failed:", finalizeError);
|
|
6332
|
+
let sourceToken;
|
|
6333
|
+
let transferTx;
|
|
6334
|
+
try {
|
|
6335
|
+
sourceToken = await import_Token6.Token.fromJSON(sourceTokenInput);
|
|
6336
|
+
} catch (err) {
|
|
6337
|
+
console.error("[Payments] Failed to parse sourceToken:", err);
|
|
6338
|
+
return;
|
|
6339
|
+
}
|
|
6340
|
+
try {
|
|
6341
|
+
const hasInclusionProof = transferTxInput.inclusionProof !== void 0;
|
|
6342
|
+
const hasData = transferTxInput.data !== void 0;
|
|
6343
|
+
const hasTransactionData = transferTxInput.transactionData !== void 0;
|
|
6344
|
+
const hasAuthenticator = transferTxInput.authenticator !== void 0;
|
|
6345
|
+
if (hasData && hasInclusionProof) {
|
|
6346
|
+
transferTx = await import_TransferTransaction2.TransferTransaction.fromJSON(transferTxInput);
|
|
6347
|
+
} else if (hasTransactionData && hasAuthenticator) {
|
|
6348
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferTxInput);
|
|
6349
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6350
|
+
if (!stClient) {
|
|
6351
|
+
console.error("[Payments] Cannot process commitment - no state transition client");
|
|
5113
6352
|
return;
|
|
5114
6353
|
}
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
recipientState,
|
|
5139
|
-
transferTx,
|
|
5140
|
-
[]
|
|
5141
|
-
// No nametag tokens needed for DIRECT
|
|
5142
|
-
);
|
|
5143
|
-
tokenData = finalizedSdkToken.toJSON();
|
|
5144
|
-
this.log("DIRECT transfer finalized successfully");
|
|
6354
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
6355
|
+
if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
|
|
6356
|
+
console.error("[Payments] Transfer commitment submission failed:", response.status);
|
|
6357
|
+
return;
|
|
6358
|
+
}
|
|
6359
|
+
if (!this.deps.oracle.waitForProofSdk) {
|
|
6360
|
+
console.error("[Payments] Cannot wait for proof - missing oracle method");
|
|
6361
|
+
return;
|
|
6362
|
+
}
|
|
6363
|
+
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
6364
|
+
transferTx = commitment.toTransaction(inclusionProof);
|
|
6365
|
+
} else {
|
|
6366
|
+
try {
|
|
6367
|
+
transferTx = await import_TransferTransaction2.TransferTransaction.fromJSON(transferTxInput);
|
|
6368
|
+
} catch {
|
|
6369
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferTxInput);
|
|
6370
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6371
|
+
if (!stClient || !this.deps.oracle.waitForProofSdk) {
|
|
6372
|
+
throw new Error("Cannot submit commitment - missing oracle methods");
|
|
6373
|
+
}
|
|
6374
|
+
await stClient.submitTransferCommitment(commitment);
|
|
6375
|
+
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
6376
|
+
transferTx = commitment.toTransaction(inclusionProof);
|
|
5145
6377
|
}
|
|
5146
|
-
} catch (finalizeError) {
|
|
5147
|
-
this.log("DIRECT finalization failed, using source token:", finalizeError);
|
|
5148
|
-
tokenData = sourceTokenInput;
|
|
5149
6378
|
}
|
|
6379
|
+
} catch (err) {
|
|
6380
|
+
console.error("[Payments] Failed to parse transferTx:", err);
|
|
6381
|
+
return;
|
|
6382
|
+
}
|
|
6383
|
+
try {
|
|
6384
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6385
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
6386
|
+
if (!stClient || !trustBase) {
|
|
6387
|
+
console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
|
|
6388
|
+
return;
|
|
6389
|
+
}
|
|
6390
|
+
finalizedSdkToken = await this.finalizeTransferToken(sourceToken, transferTx, stClient, trustBase);
|
|
6391
|
+
tokenData = finalizedSdkToken.toJSON();
|
|
6392
|
+
const addressScheme = transferTx.data.recipient.scheme;
|
|
6393
|
+
this.log(`${addressScheme === import_AddressScheme.AddressScheme.PROXY ? "PROXY" : "DIRECT"} finalization successful`);
|
|
6394
|
+
} catch (finalizeError) {
|
|
6395
|
+
console.error(`[Payments] Finalization FAILED - token rejected:`, finalizeError);
|
|
6396
|
+
return;
|
|
5150
6397
|
}
|
|
5151
6398
|
} else if (payload.token) {
|
|
5152
6399
|
tokenData = payload.token;
|
|
@@ -5173,12 +6420,6 @@ var PaymentsModule = class {
|
|
|
5173
6420
|
updatedAt: Date.now(),
|
|
5174
6421
|
sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
|
|
5175
6422
|
};
|
|
5176
|
-
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5177
|
-
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
5178
|
-
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
5179
|
-
this.log(`Rejected tombstoned token ${sdkTokenId.slice(0, 8)}...`);
|
|
5180
|
-
return;
|
|
5181
|
-
}
|
|
5182
6423
|
await this.addToken(token);
|
|
5183
6424
|
const incomingTransfer = {
|
|
5184
6425
|
id: transfer.id,
|
|
@@ -5266,14 +6507,159 @@ var PaymentsModule = class {
|
|
|
5266
6507
|
}
|
|
5267
6508
|
loadFromStorageData(data) {
|
|
5268
6509
|
const parsed = parseTxfStorageData(data);
|
|
6510
|
+
this.tombstones = parsed.tombstones;
|
|
5269
6511
|
this.tokens.clear();
|
|
5270
6512
|
for (const token of parsed.tokens) {
|
|
6513
|
+
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6514
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
6515
|
+
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
6516
|
+
this.log(`Skipping tombstoned token ${sdkTokenId.slice(0, 8)}... during load (exact state match)`);
|
|
6517
|
+
continue;
|
|
6518
|
+
}
|
|
5271
6519
|
this.tokens.set(token.id, token);
|
|
5272
6520
|
}
|
|
5273
|
-
this.tombstones = parsed.tombstones;
|
|
5274
6521
|
this.archivedTokens = parsed.archivedTokens;
|
|
5275
6522
|
this.forkedTokens = parsed.forkedTokens;
|
|
5276
|
-
|
|
6523
|
+
if (parsed.nametag !== null) {
|
|
6524
|
+
this.nametag = parsed.nametag;
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
// ===========================================================================
|
|
6528
|
+
// Private: NOSTR-FIRST Proof Polling
|
|
6529
|
+
// ===========================================================================
|
|
6530
|
+
/**
|
|
6531
|
+
* Submit commitment to aggregator and start background proof polling
|
|
6532
|
+
* (NOSTR-FIRST pattern: fire-and-forget submission)
|
|
6533
|
+
*/
|
|
6534
|
+
async submitAndPollForProof(tokenId, commitment, requestIdHex, onProofReceived) {
|
|
6535
|
+
try {
|
|
6536
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6537
|
+
if (!stClient) {
|
|
6538
|
+
this.log("Cannot submit commitment - no state transition client");
|
|
6539
|
+
return;
|
|
6540
|
+
}
|
|
6541
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
6542
|
+
if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
|
|
6543
|
+
this.log(`Transfer commitment submission failed: ${response.status}`);
|
|
6544
|
+
const token = this.tokens.get(tokenId);
|
|
6545
|
+
if (token) {
|
|
6546
|
+
token.status = "invalid";
|
|
6547
|
+
token.updatedAt = Date.now();
|
|
6548
|
+
this.tokens.set(tokenId, token);
|
|
6549
|
+
await this.save();
|
|
6550
|
+
}
|
|
6551
|
+
return;
|
|
6552
|
+
}
|
|
6553
|
+
this.addProofPollingJob({
|
|
6554
|
+
tokenId,
|
|
6555
|
+
requestIdHex,
|
|
6556
|
+
commitmentJson: JSON.stringify(commitment.toJSON()),
|
|
6557
|
+
startedAt: Date.now(),
|
|
6558
|
+
attemptCount: 0,
|
|
6559
|
+
lastAttemptAt: 0,
|
|
6560
|
+
onProofReceived
|
|
6561
|
+
});
|
|
6562
|
+
} catch (error) {
|
|
6563
|
+
this.log("submitAndPollForProof error:", error);
|
|
6564
|
+
}
|
|
6565
|
+
}
|
|
6566
|
+
/**
|
|
6567
|
+
* Add a proof polling job to the queue
|
|
6568
|
+
*/
|
|
6569
|
+
addProofPollingJob(job) {
|
|
6570
|
+
this.proofPollingJobs.set(job.tokenId, job);
|
|
6571
|
+
this.log(`Added proof polling job for token ${job.tokenId.slice(0, 8)}...`);
|
|
6572
|
+
this.startProofPolling();
|
|
6573
|
+
}
|
|
6574
|
+
/**
|
|
6575
|
+
* Start the proof polling interval if not already running
|
|
6576
|
+
*/
|
|
6577
|
+
startProofPolling() {
|
|
6578
|
+
if (this.proofPollingInterval) return;
|
|
6579
|
+
if (this.proofPollingJobs.size === 0) return;
|
|
6580
|
+
this.log("Starting proof polling...");
|
|
6581
|
+
this.proofPollingInterval = setInterval(
|
|
6582
|
+
() => this.processProofPollingQueue(),
|
|
6583
|
+
_PaymentsModule.PROOF_POLLING_INTERVAL_MS
|
|
6584
|
+
);
|
|
6585
|
+
}
|
|
6586
|
+
/**
|
|
6587
|
+
* Stop the proof polling interval
|
|
6588
|
+
*/
|
|
6589
|
+
stopProofPolling() {
|
|
6590
|
+
if (this.proofPollingInterval) {
|
|
6591
|
+
clearInterval(this.proofPollingInterval);
|
|
6592
|
+
this.proofPollingInterval = null;
|
|
6593
|
+
this.log("Stopped proof polling");
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6596
|
+
/**
|
|
6597
|
+
* Process all pending proof polling jobs
|
|
6598
|
+
*/
|
|
6599
|
+
async processProofPollingQueue() {
|
|
6600
|
+
if (this.proofPollingJobs.size === 0) {
|
|
6601
|
+
this.stopProofPolling();
|
|
6602
|
+
return;
|
|
6603
|
+
}
|
|
6604
|
+
const completedJobs = [];
|
|
6605
|
+
for (const [tokenId, job] of this.proofPollingJobs) {
|
|
6606
|
+
try {
|
|
6607
|
+
job.attemptCount++;
|
|
6608
|
+
job.lastAttemptAt = Date.now();
|
|
6609
|
+
if (job.attemptCount >= _PaymentsModule.PROOF_POLLING_MAX_ATTEMPTS) {
|
|
6610
|
+
this.log(`Proof polling timeout for token ${tokenId.slice(0, 8)}...`);
|
|
6611
|
+
const token2 = this.tokens.get(tokenId);
|
|
6612
|
+
if (token2 && token2.status === "submitted") {
|
|
6613
|
+
token2.status = "invalid";
|
|
6614
|
+
token2.updatedAt = Date.now();
|
|
6615
|
+
this.tokens.set(tokenId, token2);
|
|
6616
|
+
}
|
|
6617
|
+
completedJobs.push(tokenId);
|
|
6618
|
+
continue;
|
|
6619
|
+
}
|
|
6620
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(JSON.parse(job.commitmentJson));
|
|
6621
|
+
let inclusionProof = null;
|
|
6622
|
+
try {
|
|
6623
|
+
const abortController = new AbortController();
|
|
6624
|
+
const timeoutId = setTimeout(() => abortController.abort(), 500);
|
|
6625
|
+
if (this.deps.oracle.waitForProofSdk) {
|
|
6626
|
+
inclusionProof = await Promise.race([
|
|
6627
|
+
this.deps.oracle.waitForProofSdk(commitment, abortController.signal),
|
|
6628
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 500))
|
|
6629
|
+
]);
|
|
6630
|
+
} else {
|
|
6631
|
+
const proof = await this.deps.oracle.getProof(job.requestIdHex);
|
|
6632
|
+
if (proof) {
|
|
6633
|
+
inclusionProof = proof;
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
clearTimeout(timeoutId);
|
|
6637
|
+
} catch (err) {
|
|
6638
|
+
continue;
|
|
6639
|
+
}
|
|
6640
|
+
if (!inclusionProof) {
|
|
6641
|
+
continue;
|
|
6642
|
+
}
|
|
6643
|
+
const token = this.tokens.get(tokenId);
|
|
6644
|
+
if (token) {
|
|
6645
|
+
token.status = "spent";
|
|
6646
|
+
token.updatedAt = Date.now();
|
|
6647
|
+
this.tokens.set(tokenId, token);
|
|
6648
|
+
await this.save();
|
|
6649
|
+
this.log(`Proof received for token ${tokenId.slice(0, 8)}..., status: spent`);
|
|
6650
|
+
}
|
|
6651
|
+
job.onProofReceived?.(tokenId);
|
|
6652
|
+
completedJobs.push(tokenId);
|
|
6653
|
+
} catch (error) {
|
|
6654
|
+
this.log(`Proof polling attempt ${job.attemptCount} for ${tokenId.slice(0, 8)}...: ${error}`);
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
for (const tokenId of completedJobs) {
|
|
6658
|
+
this.proofPollingJobs.delete(tokenId);
|
|
6659
|
+
}
|
|
6660
|
+
if (this.proofPollingJobs.size === 0) {
|
|
6661
|
+
this.stopProofPolling();
|
|
6662
|
+
}
|
|
5277
6663
|
}
|
|
5278
6664
|
// ===========================================================================
|
|
5279
6665
|
// Private: Helpers
|
|
@@ -5288,6 +6674,14 @@ function createPaymentsModule(config) {
|
|
|
5288
6674
|
return new PaymentsModule(config);
|
|
5289
6675
|
}
|
|
5290
6676
|
|
|
6677
|
+
// modules/payments/TokenRecoveryService.ts
|
|
6678
|
+
var import_TokenId4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
6679
|
+
var import_TokenState6 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
6680
|
+
var import_TokenType3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
6681
|
+
var import_CoinId5 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
|
|
6682
|
+
var import_HashAlgorithm6 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
6683
|
+
var import_UnmaskedPredicate6 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
6684
|
+
|
|
5291
6685
|
// modules/communications/CommunicationsModule.ts
|
|
5292
6686
|
var CommunicationsModule = class {
|
|
5293
6687
|
config;
|
|
@@ -6256,20 +7650,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
6256
7650
|
|
|
6257
7651
|
// core/Sphere.ts
|
|
6258
7652
|
var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
|
|
6259
|
-
var
|
|
6260
|
-
var
|
|
6261
|
-
var
|
|
7653
|
+
var import_TokenType4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
7654
|
+
var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
7655
|
+
var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
6262
7656
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
6263
7657
|
async function deriveL3PredicateAddress(privateKey) {
|
|
6264
7658
|
const secret = Buffer.from(privateKey, "hex");
|
|
6265
7659
|
const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
|
|
6266
7660
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
6267
|
-
const tokenType = new
|
|
6268
|
-
const predicateRef =
|
|
7661
|
+
const tokenType = new import_TokenType4.TokenType(tokenTypeBytes);
|
|
7662
|
+
const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
|
|
6269
7663
|
tokenType,
|
|
6270
7664
|
signingService.algorithm,
|
|
6271
7665
|
signingService.publicKey,
|
|
6272
|
-
|
|
7666
|
+
import_HashAlgorithm7.HashAlgorithm.SHA256
|
|
6273
7667
|
);
|
|
6274
7668
|
return (await (await predicateRef).toAddress()).toString();
|
|
6275
7669
|
}
|
|
@@ -6285,7 +7679,11 @@ var Sphere = class _Sphere {
|
|
|
6285
7679
|
_derivationMode = "bip32";
|
|
6286
7680
|
_basePath = DEFAULT_BASE_PATH;
|
|
6287
7681
|
_currentAddressIndex = 0;
|
|
6288
|
-
/**
|
|
7682
|
+
/** Registry of all tracked (activated) addresses, keyed by HD index */
|
|
7683
|
+
_trackedAddresses = /* @__PURE__ */ new Map();
|
|
7684
|
+
/** Reverse lookup: addressId -> HD index */
|
|
7685
|
+
_addressIdToIndex = /* @__PURE__ */ new Map();
|
|
7686
|
+
/** Nametag cache: addressId -> (nametagIndex -> nametag). Separate from tracked addresses. */
|
|
6289
7687
|
_addressNametags = /* @__PURE__ */ new Map();
|
|
6290
7688
|
/** Cached PROXY address (computed once when nametag is set) */
|
|
6291
7689
|
_cachedProxyAddress = void 0;
|
|
@@ -6294,6 +7692,7 @@ var Sphere = class _Sphere {
|
|
|
6294
7692
|
_tokenStorageProviders = /* @__PURE__ */ new Map();
|
|
6295
7693
|
_transport;
|
|
6296
7694
|
_oracle;
|
|
7695
|
+
_priceProvider;
|
|
6297
7696
|
// Modules
|
|
6298
7697
|
_payments;
|
|
6299
7698
|
_communications;
|
|
@@ -6302,10 +7701,11 @@ var Sphere = class _Sphere {
|
|
|
6302
7701
|
// ===========================================================================
|
|
6303
7702
|
// Constructor (private)
|
|
6304
7703
|
// ===========================================================================
|
|
6305
|
-
constructor(storage, transport, oracle, tokenStorage, l1Config) {
|
|
7704
|
+
constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider) {
|
|
6306
7705
|
this._storage = storage;
|
|
6307
7706
|
this._transport = transport;
|
|
6308
7707
|
this._oracle = oracle;
|
|
7708
|
+
this._priceProvider = priceProvider ?? null;
|
|
6309
7709
|
if (tokenStorage) {
|
|
6310
7710
|
this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
|
|
6311
7711
|
}
|
|
@@ -6365,7 +7765,8 @@ var Sphere = class _Sphere {
|
|
|
6365
7765
|
transport: options.transport,
|
|
6366
7766
|
oracle: options.oracle,
|
|
6367
7767
|
tokenStorage: options.tokenStorage,
|
|
6368
|
-
l1: options.l1
|
|
7768
|
+
l1: options.l1,
|
|
7769
|
+
price: options.price
|
|
6369
7770
|
});
|
|
6370
7771
|
return { sphere: sphere2, created: false };
|
|
6371
7772
|
}
|
|
@@ -6389,7 +7790,8 @@ var Sphere = class _Sphere {
|
|
|
6389
7790
|
tokenStorage: options.tokenStorage,
|
|
6390
7791
|
derivationPath: options.derivationPath,
|
|
6391
7792
|
nametag: options.nametag,
|
|
6392
|
-
l1: options.l1
|
|
7793
|
+
l1: options.l1,
|
|
7794
|
+
price: options.price
|
|
6393
7795
|
});
|
|
6394
7796
|
return { sphere, created: true, generatedMnemonic };
|
|
6395
7797
|
}
|
|
@@ -6408,7 +7810,8 @@ var Sphere = class _Sphere {
|
|
|
6408
7810
|
options.transport,
|
|
6409
7811
|
options.oracle,
|
|
6410
7812
|
options.tokenStorage,
|
|
6411
|
-
options.l1
|
|
7813
|
+
options.l1,
|
|
7814
|
+
options.price
|
|
6412
7815
|
);
|
|
6413
7816
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
|
|
6414
7817
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
@@ -6417,10 +7820,12 @@ var Sphere = class _Sphere {
|
|
|
6417
7820
|
await sphere.finalizeWalletCreation();
|
|
6418
7821
|
sphere._initialized = true;
|
|
6419
7822
|
_Sphere.instance = sphere;
|
|
7823
|
+
await sphere.ensureAddressTracked(0);
|
|
6420
7824
|
if (options.nametag) {
|
|
6421
7825
|
await sphere.registerNametag(options.nametag);
|
|
6422
7826
|
} else {
|
|
6423
|
-
await sphere.
|
|
7827
|
+
await sphere.syncIdentityWithTransport();
|
|
7828
|
+
await sphere.recoverNametagFromTransport();
|
|
6424
7829
|
}
|
|
6425
7830
|
return sphere;
|
|
6426
7831
|
}
|
|
@@ -6436,14 +7841,28 @@ var Sphere = class _Sphere {
|
|
|
6436
7841
|
options.transport,
|
|
6437
7842
|
options.oracle,
|
|
6438
7843
|
options.tokenStorage,
|
|
6439
|
-
options.l1
|
|
7844
|
+
options.l1,
|
|
7845
|
+
options.price
|
|
6440
7846
|
);
|
|
6441
7847
|
await sphere.loadIdentityFromStorage();
|
|
6442
7848
|
await sphere.initializeProviders();
|
|
6443
7849
|
await sphere.initializeModules();
|
|
6444
|
-
await sphere.
|
|
7850
|
+
await sphere.syncIdentityWithTransport();
|
|
6445
7851
|
sphere._initialized = true;
|
|
6446
7852
|
_Sphere.instance = sphere;
|
|
7853
|
+
if (sphere._identity?.nametag && !sphere._payments.hasNametag()) {
|
|
7854
|
+
console.log(`[Sphere] Nametag @${sphere._identity.nametag} has no token, attempting to mint...`);
|
|
7855
|
+
try {
|
|
7856
|
+
const result = await sphere.mintNametag(sphere._identity.nametag);
|
|
7857
|
+
if (result.success) {
|
|
7858
|
+
console.log(`[Sphere] Nametag token minted successfully on load`);
|
|
7859
|
+
} else {
|
|
7860
|
+
console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
|
|
7861
|
+
}
|
|
7862
|
+
} catch (err) {
|
|
7863
|
+
console.warn(`[Sphere] Nametag token mint failed:`, err);
|
|
7864
|
+
}
|
|
7865
|
+
}
|
|
6447
7866
|
return sphere;
|
|
6448
7867
|
}
|
|
6449
7868
|
/**
|
|
@@ -6459,7 +7878,8 @@ var Sphere = class _Sphere {
|
|
|
6459
7878
|
options.transport,
|
|
6460
7879
|
options.oracle,
|
|
6461
7880
|
options.tokenStorage,
|
|
6462
|
-
options.l1
|
|
7881
|
+
options.l1,
|
|
7882
|
+
options.price
|
|
6463
7883
|
);
|
|
6464
7884
|
if (options.mnemonic) {
|
|
6465
7885
|
if (!_Sphere.validateMnemonic(options.mnemonic)) {
|
|
@@ -6484,11 +7904,12 @@ var Sphere = class _Sphere {
|
|
|
6484
7904
|
await sphere.initializeProviders();
|
|
6485
7905
|
await sphere.initializeModules();
|
|
6486
7906
|
if (!options.nametag) {
|
|
6487
|
-
await sphere.
|
|
7907
|
+
await sphere.recoverNametagFromTransport();
|
|
6488
7908
|
}
|
|
6489
7909
|
await sphere.finalizeWalletCreation();
|
|
6490
7910
|
sphere._initialized = true;
|
|
6491
7911
|
_Sphere.instance = sphere;
|
|
7912
|
+
await sphere.ensureAddressTracked(0);
|
|
6492
7913
|
if (options.nametag) {
|
|
6493
7914
|
await sphere.registerNametag(options.nametag);
|
|
6494
7915
|
}
|
|
@@ -6524,6 +7945,7 @@ var Sphere = class _Sphere {
|
|
|
6524
7945
|
await storage.remove(STORAGE_KEYS_GLOBAL.DERIVATION_MODE);
|
|
6525
7946
|
await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_SOURCE);
|
|
6526
7947
|
await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_EXISTS);
|
|
7948
|
+
await storage.remove(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
|
|
6527
7949
|
await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
6528
7950
|
await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
6529
7951
|
await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
|
|
@@ -6648,6 +8070,13 @@ var Sphere = class _Sphere {
|
|
|
6648
8070
|
hasTokenStorageProvider(providerId) {
|
|
6649
8071
|
return this._tokenStorageProviders.has(providerId);
|
|
6650
8072
|
}
|
|
8073
|
+
/**
|
|
8074
|
+
* Set or update the price provider after initialization
|
|
8075
|
+
*/
|
|
8076
|
+
setPriceProvider(provider) {
|
|
8077
|
+
this._priceProvider = provider;
|
|
8078
|
+
this._payments.setPriceProvider(provider);
|
|
8079
|
+
}
|
|
6651
8080
|
getTransport() {
|
|
6652
8081
|
return this._transport;
|
|
6653
8082
|
}
|
|
@@ -7135,10 +8564,9 @@ var Sphere = class _Sphere {
|
|
|
7135
8564
|
* @returns Primary nametag (index 0) or undefined if not registered
|
|
7136
8565
|
*/
|
|
7137
8566
|
getNametagForAddress(addressId) {
|
|
7138
|
-
const id = addressId ?? this.
|
|
8567
|
+
const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
|
|
7139
8568
|
if (!id) return void 0;
|
|
7140
|
-
|
|
7141
|
-
return nametagsMap?.get(0);
|
|
8569
|
+
return this._addressNametags.get(id)?.get(0);
|
|
7142
8570
|
}
|
|
7143
8571
|
/**
|
|
7144
8572
|
* Get all nametags for a specific address
|
|
@@ -7147,29 +8575,89 @@ var Sphere = class _Sphere {
|
|
|
7147
8575
|
* @returns Map of nametagIndex to nametag, or undefined if no nametags
|
|
7148
8576
|
*/
|
|
7149
8577
|
getNametagsForAddress(addressId) {
|
|
7150
|
-
const id = addressId ?? this.
|
|
8578
|
+
const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
|
|
7151
8579
|
if (!id) return void 0;
|
|
7152
|
-
const
|
|
7153
|
-
return
|
|
8580
|
+
const nametags = this._addressNametags.get(id);
|
|
8581
|
+
return nametags && nametags.size > 0 ? new Map(nametags) : void 0;
|
|
7154
8582
|
}
|
|
7155
8583
|
/**
|
|
7156
8584
|
* Get all registered address nametags
|
|
7157
|
-
*
|
|
8585
|
+
* @deprecated Use getActiveAddresses() or getAllTrackedAddresses() instead
|
|
7158
8586
|
* @returns Map of addressId to (nametagIndex -> nametag)
|
|
7159
8587
|
*/
|
|
7160
8588
|
getAllAddressNametags() {
|
|
7161
8589
|
const result = /* @__PURE__ */ new Map();
|
|
7162
|
-
this._addressNametags.
|
|
7163
|
-
|
|
7164
|
-
|
|
8590
|
+
for (const [addressId, nametags] of this._addressNametags.entries()) {
|
|
8591
|
+
if (nametags.size > 0) {
|
|
8592
|
+
result.set(addressId, new Map(nametags));
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
7165
8595
|
return result;
|
|
7166
8596
|
}
|
|
7167
8597
|
/**
|
|
7168
|
-
* Get
|
|
8598
|
+
* Get all active (non-hidden) tracked addresses.
|
|
8599
|
+
* Returns addresses that have been activated through create, switchToAddress,
|
|
8600
|
+
* registerNametag, or nametag recovery.
|
|
8601
|
+
*
|
|
8602
|
+
* @returns Array of TrackedAddress entries sorted by index, excluding hidden ones
|
|
8603
|
+
*/
|
|
8604
|
+
getActiveAddresses() {
|
|
8605
|
+
this.ensureReady();
|
|
8606
|
+
const result = [];
|
|
8607
|
+
for (const entry of this._trackedAddresses.values()) {
|
|
8608
|
+
if (!entry.hidden) {
|
|
8609
|
+
const nametag = this._addressNametags.get(entry.addressId)?.get(0);
|
|
8610
|
+
result.push({ ...entry, nametag });
|
|
8611
|
+
}
|
|
8612
|
+
}
|
|
8613
|
+
return result.sort((a, b) => a.index - b.index);
|
|
8614
|
+
}
|
|
8615
|
+
/**
|
|
8616
|
+
* Get all tracked addresses, including hidden ones.
|
|
8617
|
+
*
|
|
8618
|
+
* @returns Array of all TrackedAddress entries sorted by index
|
|
8619
|
+
*/
|
|
8620
|
+
getAllTrackedAddresses() {
|
|
8621
|
+
this.ensureReady();
|
|
8622
|
+
const result = [];
|
|
8623
|
+
for (const entry of this._trackedAddresses.values()) {
|
|
8624
|
+
const nametag = this._addressNametags.get(entry.addressId)?.get(0);
|
|
8625
|
+
result.push({ ...entry, nametag });
|
|
8626
|
+
}
|
|
8627
|
+
return result.sort((a, b) => a.index - b.index);
|
|
8628
|
+
}
|
|
8629
|
+
/**
|
|
8630
|
+
* Get tracked address info by index.
|
|
8631
|
+
*
|
|
8632
|
+
* @param index - Address index
|
|
8633
|
+
* @returns TrackedAddress or undefined if not tracked
|
|
8634
|
+
*/
|
|
8635
|
+
getTrackedAddress(index) {
|
|
8636
|
+
this.ensureReady();
|
|
8637
|
+
const entry = this._trackedAddresses.get(index);
|
|
8638
|
+
if (!entry) return void 0;
|
|
8639
|
+
const nametag = this._addressNametags.get(entry.addressId)?.get(0);
|
|
8640
|
+
return { ...entry, nametag };
|
|
8641
|
+
}
|
|
8642
|
+
/**
|
|
8643
|
+
* Set visibility of a tracked address.
|
|
8644
|
+
* Hidden addresses are not returned by getActiveAddresses() but remain tracked.
|
|
8645
|
+
*
|
|
8646
|
+
* @param index - Address index to hide/unhide
|
|
8647
|
+
* @param hidden - true to hide, false to show
|
|
8648
|
+
* @throws Error if address index is not tracked
|
|
7169
8649
|
*/
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
8650
|
+
async setAddressHidden(index, hidden) {
|
|
8651
|
+
this.ensureReady();
|
|
8652
|
+
const entry = this._trackedAddresses.get(index);
|
|
8653
|
+
if (!entry) {
|
|
8654
|
+
throw new Error(`Address at index ${index} is not tracked. Switch to it first.`);
|
|
8655
|
+
}
|
|
8656
|
+
if (entry.hidden === hidden) return;
|
|
8657
|
+
entry.hidden = hidden;
|
|
8658
|
+
await this.persistTrackedAddresses();
|
|
8659
|
+
const eventType = hidden ? "address:hidden" : "address:unhidden";
|
|
8660
|
+
this.emitEvent(eventType, { index, addressId: entry.addressId });
|
|
7173
8661
|
}
|
|
7174
8662
|
/**
|
|
7175
8663
|
* Switch to a different address by index
|
|
@@ -7190,7 +8678,7 @@ var Sphere = class _Sphere {
|
|
|
7190
8678
|
* await sphere.switchToAddress(0);
|
|
7191
8679
|
* ```
|
|
7192
8680
|
*/
|
|
7193
|
-
async switchToAddress(index) {
|
|
8681
|
+
async switchToAddress(index, options) {
|
|
7194
8682
|
this.ensureReady();
|
|
7195
8683
|
if (!this._masterKey) {
|
|
7196
8684
|
throw new Error("HD derivation requires master key with chain code. Cannot switch addresses.");
|
|
@@ -7198,12 +8686,28 @@ var Sphere = class _Sphere {
|
|
|
7198
8686
|
if (index < 0) {
|
|
7199
8687
|
throw new Error("Address index must be non-negative");
|
|
7200
8688
|
}
|
|
8689
|
+
const newNametag = options?.nametag?.startsWith("@") ? options.nametag.slice(1) : options?.nametag;
|
|
8690
|
+
if (newNametag && !this.validateNametag(newNametag)) {
|
|
8691
|
+
throw new Error("Invalid nametag format. Use alphanumeric characters, 3-20 chars.");
|
|
8692
|
+
}
|
|
7201
8693
|
const addressInfo = this.deriveAddress(index, false);
|
|
7202
8694
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
7203
8695
|
const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
|
|
8696
|
+
await this.ensureAddressTracked(index);
|
|
7204
8697
|
const addressId = getAddressId(predicateAddress);
|
|
7205
|
-
|
|
7206
|
-
|
|
8698
|
+
if (newNametag) {
|
|
8699
|
+
const existing = await this._transport.resolveNametag?.(newNametag);
|
|
8700
|
+
if (existing) {
|
|
8701
|
+
throw new Error(`Nametag @${newNametag} is already taken`);
|
|
8702
|
+
}
|
|
8703
|
+
let nametags = this._addressNametags.get(addressId);
|
|
8704
|
+
if (!nametags) {
|
|
8705
|
+
nametags = /* @__PURE__ */ new Map();
|
|
8706
|
+
this._addressNametags.set(addressId, nametags);
|
|
8707
|
+
}
|
|
8708
|
+
nametags.set(0, newNametag);
|
|
8709
|
+
}
|
|
8710
|
+
const nametag = this._addressNametags.get(addressId)?.get(0);
|
|
7207
8711
|
this._identity = {
|
|
7208
8712
|
privateKey: addressInfo.privateKey,
|
|
7209
8713
|
chainPubkey: addressInfo.publicKey,
|
|
@@ -7216,11 +8720,47 @@ var Sphere = class _Sphere {
|
|
|
7216
8720
|
await this._updateCachedProxyAddress();
|
|
7217
8721
|
await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
|
|
7218
8722
|
this._storage.setIdentity(this._identity);
|
|
7219
|
-
this._transport.setIdentity(this._identity);
|
|
8723
|
+
await this._transport.setIdentity(this._identity);
|
|
7220
8724
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
7221
8725
|
provider.setIdentity(this._identity);
|
|
8726
|
+
await provider.initialize();
|
|
7222
8727
|
}
|
|
7223
8728
|
await this.reinitializeModulesForNewAddress();
|
|
8729
|
+
if (this._identity.nametag) {
|
|
8730
|
+
await this.syncIdentityWithTransport();
|
|
8731
|
+
}
|
|
8732
|
+
if (newNametag) {
|
|
8733
|
+
await this.persistAddressNametags();
|
|
8734
|
+
if (!this._payments.hasNametag()) {
|
|
8735
|
+
console.log(`[Sphere] Minting nametag token for @${newNametag}...`);
|
|
8736
|
+
try {
|
|
8737
|
+
const result = await this.mintNametag(newNametag);
|
|
8738
|
+
if (result.success) {
|
|
8739
|
+
console.log(`[Sphere] Nametag token minted successfully`);
|
|
8740
|
+
} else {
|
|
8741
|
+
console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
|
|
8742
|
+
}
|
|
8743
|
+
} catch (err) {
|
|
8744
|
+
console.warn(`[Sphere] Nametag token mint failed:`, err);
|
|
8745
|
+
}
|
|
8746
|
+
}
|
|
8747
|
+
this.emitEvent("nametag:registered", {
|
|
8748
|
+
nametag: newNametag,
|
|
8749
|
+
addressIndex: index
|
|
8750
|
+
});
|
|
8751
|
+
} else if (this._identity.nametag && !this._payments.hasNametag()) {
|
|
8752
|
+
console.log(`[Sphere] Nametag @${this._identity.nametag} has no token after switch, minting...`);
|
|
8753
|
+
try {
|
|
8754
|
+
const result = await this.mintNametag(this._identity.nametag);
|
|
8755
|
+
if (result.success) {
|
|
8756
|
+
console.log(`[Sphere] Nametag token minted successfully after switch`);
|
|
8757
|
+
} else {
|
|
8758
|
+
console.warn(`[Sphere] Could not mint nametag token after switch: ${result.error}`);
|
|
8759
|
+
}
|
|
8760
|
+
} catch (err) {
|
|
8761
|
+
console.warn(`[Sphere] Nametag token mint failed after switch:`, err);
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
7224
8764
|
this.emitEvent("identity:changed", {
|
|
7225
8765
|
l1Address: this._identity.l1Address,
|
|
7226
8766
|
directAddress: this._identity.directAddress,
|
|
@@ -7242,7 +8782,8 @@ var Sphere = class _Sphere {
|
|
|
7242
8782
|
transport: this._transport,
|
|
7243
8783
|
oracle: this._oracle,
|
|
7244
8784
|
emitEvent,
|
|
7245
|
-
chainCode: this._masterKey?.chainCode
|
|
8785
|
+
chainCode: this._masterKey?.chainCode,
|
|
8786
|
+
price: this._priceProvider ?? void 0
|
|
7246
8787
|
});
|
|
7247
8788
|
this._communications.initialize({
|
|
7248
8789
|
identity: this._identity,
|
|
@@ -7275,6 +8816,14 @@ var Sphere = class _Sphere {
|
|
|
7275
8816
|
*/
|
|
7276
8817
|
deriveAddress(index, isChange = false) {
|
|
7277
8818
|
this.ensureReady();
|
|
8819
|
+
return this._deriveAddressInternal(index, isChange);
|
|
8820
|
+
}
|
|
8821
|
+
/**
|
|
8822
|
+
* Internal address derivation without ensureReady() check.
|
|
8823
|
+
* Used during initialization (loadTrackedAddresses, ensureAddressTracked)
|
|
8824
|
+
* when _initialized is still false.
|
|
8825
|
+
*/
|
|
8826
|
+
_deriveAddressInternal(index, isChange = false) {
|
|
7278
8827
|
if (!this._masterKey) {
|
|
7279
8828
|
throw new Error("HD derivation requires master key with chain code");
|
|
7280
8829
|
}
|
|
@@ -7413,6 +8962,22 @@ var Sphere = class _Sphere {
|
|
|
7413
8962
|
getProxyAddress() {
|
|
7414
8963
|
return this._cachedProxyAddress;
|
|
7415
8964
|
}
|
|
8965
|
+
/**
|
|
8966
|
+
* Resolve any identifier to full peer information.
|
|
8967
|
+
* Accepts @nametag, bare nametag, DIRECT://, PROXY://, L1 address, or transport pubkey.
|
|
8968
|
+
*
|
|
8969
|
+
* @example
|
|
8970
|
+
* ```ts
|
|
8971
|
+
* const peer = await sphere.resolve('@alice');
|
|
8972
|
+
* const peer = await sphere.resolve('DIRECT://...');
|
|
8973
|
+
* const peer = await sphere.resolve('alpha1...');
|
|
8974
|
+
* const peer = await sphere.resolve('ab12cd...'); // 64-char hex transport pubkey
|
|
8975
|
+
* ```
|
|
8976
|
+
*/
|
|
8977
|
+
async resolve(identifier) {
|
|
8978
|
+
this.ensureReady();
|
|
8979
|
+
return this._transport.resolve?.(identifier) ?? null;
|
|
8980
|
+
}
|
|
7416
8981
|
/** Compute and cache the PROXY address from the current nametag */
|
|
7417
8982
|
async _updateCachedProxyAddress() {
|
|
7418
8983
|
const nametag = this._identity?.nametag;
|
|
@@ -7451,11 +9016,12 @@ var Sphere = class _Sphere {
|
|
|
7451
9016
|
if (this._identity?.nametag) {
|
|
7452
9017
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
7453
9018
|
}
|
|
7454
|
-
if (this._transport.
|
|
7455
|
-
const success = await this._transport.
|
|
7456
|
-
cleanNametag,
|
|
9019
|
+
if (this._transport.publishIdentityBinding) {
|
|
9020
|
+
const success = await this._transport.publishIdentityBinding(
|
|
7457
9021
|
this._identity.chainPubkey,
|
|
7458
|
-
this._identity.
|
|
9022
|
+
this._identity.l1Address,
|
|
9023
|
+
this._identity.directAddress || "",
|
|
9024
|
+
cleanNametag
|
|
7459
9025
|
);
|
|
7460
9026
|
if (!success) {
|
|
7461
9027
|
throw new Error("Failed to register nametag. It may already be taken.");
|
|
@@ -7463,14 +9029,14 @@ var Sphere = class _Sphere {
|
|
|
7463
9029
|
}
|
|
7464
9030
|
this._identity.nametag = cleanNametag;
|
|
7465
9031
|
await this._updateCachedProxyAddress();
|
|
7466
|
-
const
|
|
7467
|
-
if (
|
|
7468
|
-
let
|
|
7469
|
-
if (!
|
|
7470
|
-
|
|
7471
|
-
this._addressNametags.set(
|
|
9032
|
+
const currentAddressId = this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
|
|
9033
|
+
if (currentAddressId) {
|
|
9034
|
+
let nametags = this._addressNametags.get(currentAddressId);
|
|
9035
|
+
if (!nametags) {
|
|
9036
|
+
nametags = /* @__PURE__ */ new Map();
|
|
9037
|
+
this._addressNametags.set(currentAddressId, nametags);
|
|
7472
9038
|
}
|
|
7473
|
-
|
|
9039
|
+
nametags.set(0, cleanNametag);
|
|
7474
9040
|
}
|
|
7475
9041
|
await this.persistAddressNametags();
|
|
7476
9042
|
if (!this._payments.hasNametag()) {
|
|
@@ -7489,19 +9055,19 @@ var Sphere = class _Sphere {
|
|
|
7489
9055
|
console.log(`[Sphere] Nametag registered for address ${this._currentAddressIndex}:`, cleanNametag);
|
|
7490
9056
|
}
|
|
7491
9057
|
/**
|
|
7492
|
-
* Persist
|
|
7493
|
-
* Format: { "DIRECT://abc...xyz": { "0": "alice", "1": "alice2" }, ... }
|
|
9058
|
+
* Persist tracked addresses to storage (only minimal fields via StorageProvider)
|
|
7494
9059
|
*/
|
|
7495
|
-
async
|
|
7496
|
-
const
|
|
7497
|
-
this.
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
9060
|
+
async persistTrackedAddresses() {
|
|
9061
|
+
const entries = [];
|
|
9062
|
+
for (const entry of this._trackedAddresses.values()) {
|
|
9063
|
+
entries.push({
|
|
9064
|
+
index: entry.index,
|
|
9065
|
+
hidden: entry.hidden,
|
|
9066
|
+
createdAt: entry.createdAt,
|
|
9067
|
+
updatedAt: entry.updatedAt
|
|
7501
9068
|
});
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
|
|
9069
|
+
}
|
|
9070
|
+
await this._storage.saveTrackedAddresses(entries);
|
|
7505
9071
|
}
|
|
7506
9072
|
/**
|
|
7507
9073
|
* Mint a nametag token on-chain (like Sphere wallet and lottery)
|
|
@@ -7535,63 +9101,184 @@ var Sphere = class _Sphere {
|
|
|
7535
9101
|
return this._payments.isNametagAvailable(nametag);
|
|
7536
9102
|
}
|
|
7537
9103
|
/**
|
|
7538
|
-
* Load
|
|
7539
|
-
*
|
|
7540
|
-
* And legacy format: { "0": "alice" } (migrates to new format on save)
|
|
9104
|
+
* Load tracked addresses from storage.
|
|
9105
|
+
* Falls back to migrating from old ADDRESS_NAMETAGS format.
|
|
7541
9106
|
*/
|
|
7542
|
-
async
|
|
9107
|
+
async loadTrackedAddresses() {
|
|
9108
|
+
this._trackedAddresses.clear();
|
|
9109
|
+
this._addressIdToIndex.clear();
|
|
7543
9110
|
try {
|
|
7544
|
-
const
|
|
7545
|
-
if (
|
|
7546
|
-
const
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
}
|
|
9111
|
+
const entries = await this._storage.loadTrackedAddresses();
|
|
9112
|
+
if (entries.length > 0) {
|
|
9113
|
+
for (const stored of entries) {
|
|
9114
|
+
const addrInfo = this._deriveAddressInternal(stored.index, false);
|
|
9115
|
+
const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
|
|
9116
|
+
const addressId = getAddressId(directAddress);
|
|
9117
|
+
const entry = {
|
|
9118
|
+
...stored,
|
|
9119
|
+
addressId,
|
|
9120
|
+
l1Address: addrInfo.address,
|
|
9121
|
+
directAddress,
|
|
9122
|
+
chainPubkey: addrInfo.publicKey
|
|
9123
|
+
};
|
|
9124
|
+
this._trackedAddresses.set(entry.index, entry);
|
|
9125
|
+
this._addressIdToIndex.set(addressId, entry.index);
|
|
7557
9126
|
}
|
|
9127
|
+
return;
|
|
9128
|
+
}
|
|
9129
|
+
const oldData = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
9130
|
+
if (oldData) {
|
|
9131
|
+
const parsed = JSON.parse(oldData);
|
|
9132
|
+
await this.migrateFromOldNametagFormat(parsed);
|
|
9133
|
+
await this.persistTrackedAddresses();
|
|
7558
9134
|
}
|
|
7559
9135
|
} catch {
|
|
7560
9136
|
}
|
|
7561
9137
|
}
|
|
7562
9138
|
/**
|
|
7563
|
-
*
|
|
7564
|
-
*
|
|
9139
|
+
* Migrate from old ADDRESS_NAMETAGS format to tracked addresses.
|
|
9140
|
+
* Scans HD indices 0..19 to match addressIds from the old format.
|
|
9141
|
+
* Populates both _trackedAddresses and _addressNametags.
|
|
7565
9142
|
*/
|
|
7566
|
-
async
|
|
7567
|
-
const
|
|
7568
|
-
|
|
7569
|
-
|
|
9143
|
+
async migrateFromOldNametagFormat(parsed) {
|
|
9144
|
+
const addressIdToNametags = /* @__PURE__ */ new Map();
|
|
9145
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
9146
|
+
if (typeof value === "object" && value !== null) {
|
|
9147
|
+
addressIdToNametags.set(key, value);
|
|
9148
|
+
}
|
|
9149
|
+
}
|
|
9150
|
+
if (addressIdToNametags.size === 0 || !this._masterKey) return;
|
|
9151
|
+
const SCAN_LIMIT = 20;
|
|
9152
|
+
for (let i = 0; i < SCAN_LIMIT && addressIdToNametags.size > 0; i++) {
|
|
9153
|
+
try {
|
|
9154
|
+
const addrInfo = this._deriveAddressInternal(i, false);
|
|
9155
|
+
const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
|
|
9156
|
+
const addressId = getAddressId(directAddress);
|
|
9157
|
+
if (addressIdToNametags.has(addressId)) {
|
|
9158
|
+
const nametagsObj = addressIdToNametags.get(addressId);
|
|
9159
|
+
const nametagMap = /* @__PURE__ */ new Map();
|
|
9160
|
+
for (const [idx, tag] of Object.entries(nametagsObj)) {
|
|
9161
|
+
nametagMap.set(parseInt(idx, 10), tag);
|
|
9162
|
+
}
|
|
9163
|
+
if (nametagMap.size > 0) {
|
|
9164
|
+
this._addressNametags.set(addressId, nametagMap);
|
|
9165
|
+
}
|
|
9166
|
+
const now = Date.now();
|
|
9167
|
+
const entry = {
|
|
9168
|
+
index: i,
|
|
9169
|
+
addressId,
|
|
9170
|
+
l1Address: addrInfo.address,
|
|
9171
|
+
directAddress,
|
|
9172
|
+
chainPubkey: addrInfo.publicKey,
|
|
9173
|
+
nametag: nametagMap.get(0),
|
|
9174
|
+
hidden: false,
|
|
9175
|
+
createdAt: now,
|
|
9176
|
+
updatedAt: now
|
|
9177
|
+
};
|
|
9178
|
+
this._trackedAddresses.set(i, entry);
|
|
9179
|
+
this._addressIdToIndex.set(addressId, i);
|
|
9180
|
+
addressIdToNametags.delete(addressId);
|
|
9181
|
+
}
|
|
9182
|
+
} catch {
|
|
9183
|
+
}
|
|
9184
|
+
}
|
|
9185
|
+
await this.persistAddressNametags();
|
|
9186
|
+
}
|
|
9187
|
+
/**
|
|
9188
|
+
* Ensure an address is tracked in the registry.
|
|
9189
|
+
* If not yet tracked, derives full info and creates the entry.
|
|
9190
|
+
*/
|
|
9191
|
+
async ensureAddressTracked(index) {
|
|
9192
|
+
const existing = this._trackedAddresses.get(index);
|
|
9193
|
+
if (existing) return existing;
|
|
9194
|
+
const addrInfo = this._deriveAddressInternal(index, false);
|
|
9195
|
+
const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
|
|
9196
|
+
const addressId = getAddressId(directAddress);
|
|
9197
|
+
const now = Date.now();
|
|
9198
|
+
const nametag = this._addressNametags.get(addressId)?.get(0);
|
|
9199
|
+
const entry = {
|
|
9200
|
+
index,
|
|
9201
|
+
addressId,
|
|
9202
|
+
l1Address: addrInfo.address,
|
|
9203
|
+
directAddress,
|
|
9204
|
+
chainPubkey: addrInfo.publicKey,
|
|
9205
|
+
nametag,
|
|
9206
|
+
hidden: false,
|
|
9207
|
+
createdAt: now,
|
|
9208
|
+
updatedAt: now
|
|
9209
|
+
};
|
|
9210
|
+
this._trackedAddresses.set(index, entry);
|
|
9211
|
+
this._addressIdToIndex.set(addressId, index);
|
|
9212
|
+
await this.persistTrackedAddresses();
|
|
9213
|
+
this.emitEvent("address:activated", { address: { ...entry } });
|
|
9214
|
+
return entry;
|
|
9215
|
+
}
|
|
9216
|
+
/**
|
|
9217
|
+
* Persist nametag cache to storage.
|
|
9218
|
+
* Format: { addressId: { "0": "alice", "1": "alice2" } }
|
|
9219
|
+
*/
|
|
9220
|
+
async persistAddressNametags() {
|
|
9221
|
+
const result = {};
|
|
9222
|
+
for (const [addressId, nametags] of this._addressNametags.entries()) {
|
|
9223
|
+
const obj = {};
|
|
9224
|
+
for (const [idx, tag] of nametags.entries()) {
|
|
9225
|
+
obj[idx.toString()] = tag;
|
|
9226
|
+
}
|
|
9227
|
+
result[addressId] = obj;
|
|
9228
|
+
}
|
|
9229
|
+
await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
|
|
9230
|
+
}
|
|
9231
|
+
/**
|
|
9232
|
+
* Load nametag cache from storage.
|
|
9233
|
+
*/
|
|
9234
|
+
async loadAddressNametags() {
|
|
9235
|
+
this._addressNametags.clear();
|
|
9236
|
+
try {
|
|
9237
|
+
const data = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
9238
|
+
if (!data) return;
|
|
9239
|
+
const parsed = JSON.parse(data);
|
|
9240
|
+
for (const [addressId, nametags] of Object.entries(parsed)) {
|
|
9241
|
+
const map = /* @__PURE__ */ new Map();
|
|
9242
|
+
for (const [idx, tag] of Object.entries(nametags)) {
|
|
9243
|
+
map.set(parseInt(idx, 10), tag);
|
|
9244
|
+
}
|
|
9245
|
+
this._addressNametags.set(addressId, map);
|
|
9246
|
+
}
|
|
9247
|
+
} catch {
|
|
7570
9248
|
}
|
|
7571
|
-
|
|
9249
|
+
}
|
|
9250
|
+
/**
|
|
9251
|
+
* Publish identity binding via transport.
|
|
9252
|
+
* Always publishes base identity (chainPubkey, l1Address, directAddress).
|
|
9253
|
+
* If nametag is set, also publishes nametag hash, proxy address, encrypted nametag.
|
|
9254
|
+
*/
|
|
9255
|
+
async syncIdentityWithTransport() {
|
|
9256
|
+
if (!this._transport.publishIdentityBinding) {
|
|
7572
9257
|
return;
|
|
7573
9258
|
}
|
|
7574
9259
|
try {
|
|
7575
|
-
const
|
|
7576
|
-
|
|
9260
|
+
const nametag = this._identity?.nametag;
|
|
9261
|
+
const success = await this._transport.publishIdentityBinding(
|
|
7577
9262
|
this._identity.chainPubkey,
|
|
7578
|
-
this._identity.
|
|
9263
|
+
this._identity.l1Address,
|
|
9264
|
+
this._identity.directAddress || "",
|
|
9265
|
+
nametag || void 0
|
|
7579
9266
|
);
|
|
7580
9267
|
if (success) {
|
|
7581
|
-
console.log(`[Sphere]
|
|
7582
|
-
} else {
|
|
9268
|
+
console.log(`[Sphere] Identity binding published${nametag ? ` with nametag @${nametag}` : ""}`);
|
|
9269
|
+
} else if (nametag) {
|
|
7583
9270
|
console.warn(`[Sphere] Nametag @${nametag} is taken by another pubkey`);
|
|
7584
9271
|
}
|
|
7585
9272
|
} catch (error) {
|
|
7586
|
-
console.warn(`[Sphere]
|
|
9273
|
+
console.warn(`[Sphere] Identity binding sync failed:`, error);
|
|
7587
9274
|
}
|
|
7588
9275
|
}
|
|
7589
9276
|
/**
|
|
7590
|
-
* Recover nametag from
|
|
9277
|
+
* Recover nametag from transport after wallet import.
|
|
7591
9278
|
* Searches for encrypted nametag events authored by this wallet's pubkey
|
|
7592
|
-
* and decrypts them to restore the nametag association
|
|
9279
|
+
* and decrypts them to restore the nametag association.
|
|
7593
9280
|
*/
|
|
7594
|
-
async
|
|
9281
|
+
async recoverNametagFromTransport() {
|
|
7595
9282
|
if (this._identity?.nametag) {
|
|
7596
9283
|
return;
|
|
7597
9284
|
}
|
|
@@ -7605,22 +9292,21 @@ var Sphere = class _Sphere {
|
|
|
7605
9292
|
this._identity.nametag = recoveredNametag;
|
|
7606
9293
|
await this._updateCachedProxyAddress();
|
|
7607
9294
|
}
|
|
7608
|
-
const
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
this._addressNametags.set(addressId, nametagsMap);
|
|
7614
|
-
}
|
|
7615
|
-
const nextIndex = nametagsMap.size;
|
|
7616
|
-
nametagsMap.set(nextIndex, recoveredNametag);
|
|
9295
|
+
const entry = await this.ensureAddressTracked(this._currentAddressIndex);
|
|
9296
|
+
let nametags = this._addressNametags.get(entry.addressId);
|
|
9297
|
+
if (!nametags) {
|
|
9298
|
+
nametags = /* @__PURE__ */ new Map();
|
|
9299
|
+
this._addressNametags.set(entry.addressId, nametags);
|
|
7617
9300
|
}
|
|
9301
|
+
const nextIndex = nametags.size;
|
|
9302
|
+
nametags.set(nextIndex, recoveredNametag);
|
|
7618
9303
|
await this.persistAddressNametags();
|
|
7619
|
-
if (this._transport.
|
|
7620
|
-
await this._transport.
|
|
7621
|
-
recoveredNametag,
|
|
9304
|
+
if (this._transport.publishIdentityBinding) {
|
|
9305
|
+
await this._transport.publishIdentityBinding(
|
|
7622
9306
|
this._identity.chainPubkey,
|
|
7623
|
-
this._identity.
|
|
9307
|
+
this._identity.l1Address,
|
|
9308
|
+
this._identity.directAddress || "",
|
|
9309
|
+
recoveredNametag
|
|
7624
9310
|
);
|
|
7625
9311
|
}
|
|
7626
9312
|
this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
|
|
@@ -7648,6 +9334,9 @@ var Sphere = class _Sphere {
|
|
|
7648
9334
|
await this._oracle.disconnect();
|
|
7649
9335
|
this._initialized = false;
|
|
7650
9336
|
this._identity = null;
|
|
9337
|
+
this._trackedAddresses.clear();
|
|
9338
|
+
this._addressIdToIndex.clear();
|
|
9339
|
+
this._addressNametags.clear();
|
|
7651
9340
|
this.eventHandlers.clear();
|
|
7652
9341
|
if (_Sphere.instance === this) {
|
|
7653
9342
|
_Sphere.instance = null;
|
|
@@ -7745,14 +9434,14 @@ var Sphere = class _Sphere {
|
|
|
7745
9434
|
if (this._identity) {
|
|
7746
9435
|
this._storage.setIdentity(this._identity);
|
|
7747
9436
|
}
|
|
9437
|
+
await this.loadTrackedAddresses();
|
|
7748
9438
|
await this.loadAddressNametags();
|
|
9439
|
+
const trackedEntry = await this.ensureAddressTracked(this._currentAddressIndex);
|
|
9440
|
+
const nametag = this._addressNametags.get(trackedEntry.addressId)?.get(0);
|
|
7749
9441
|
if (this._currentAddressIndex > 0 && this._masterKey) {
|
|
7750
|
-
const addressInfo = this.
|
|
9442
|
+
const addressInfo = this._deriveAddressInternal(this._currentAddressIndex, false);
|
|
7751
9443
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
7752
9444
|
const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
|
|
7753
|
-
const addressId = getAddressId(predicateAddress);
|
|
7754
|
-
const nametagsMap = this._addressNametags.get(addressId);
|
|
7755
|
-
const nametag = nametagsMap?.get(0);
|
|
7756
9445
|
this._identity = {
|
|
7757
9446
|
privateKey: addressInfo.privateKey,
|
|
7758
9447
|
chainPubkey: addressInfo.publicKey,
|
|
@@ -7763,13 +9452,8 @@ var Sphere = class _Sphere {
|
|
|
7763
9452
|
};
|
|
7764
9453
|
this._storage.setIdentity(this._identity);
|
|
7765
9454
|
console.log(`[Sphere] Restored to address ${this._currentAddressIndex}:`, this._identity.l1Address);
|
|
7766
|
-
} else if (this._identity) {
|
|
7767
|
-
|
|
7768
|
-
const nametagsMap = addressId ? this._addressNametags.get(addressId) : void 0;
|
|
7769
|
-
const nametag = nametagsMap?.get(0);
|
|
7770
|
-
if (nametag) {
|
|
7771
|
-
this._identity.nametag = nametag;
|
|
7772
|
-
}
|
|
9455
|
+
} else if (this._identity && nametag) {
|
|
9456
|
+
this._identity.nametag = nametag;
|
|
7773
9457
|
}
|
|
7774
9458
|
await this._updateCachedProxyAddress();
|
|
7775
9459
|
}
|
|
@@ -7827,7 +9511,7 @@ var Sphere = class _Sphere {
|
|
|
7827
9511
|
// ===========================================================================
|
|
7828
9512
|
async initializeProviders() {
|
|
7829
9513
|
this._storage.setIdentity(this._identity);
|
|
7830
|
-
this._transport.setIdentity(this._identity);
|
|
9514
|
+
await this._transport.setIdentity(this._identity);
|
|
7831
9515
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
7832
9516
|
provider.setIdentity(this._identity);
|
|
7833
9517
|
}
|
|
@@ -7848,7 +9532,8 @@ var Sphere = class _Sphere {
|
|
|
7848
9532
|
oracle: this._oracle,
|
|
7849
9533
|
emitEvent,
|
|
7850
9534
|
// Pass chain code for L1 HD derivation
|
|
7851
|
-
chainCode: this._masterKey?.chainCode
|
|
9535
|
+
chainCode: this._masterKey?.chainCode,
|
|
9536
|
+
price: this._priceProvider ?? void 0
|
|
7852
9537
|
});
|
|
7853
9538
|
this._communications.initialize({
|
|
7854
9539
|
identity: this._identity,
|
|
@@ -7930,6 +9615,65 @@ function formatAmount(amount, options = {}) {
|
|
|
7930
9615
|
return symbol ? `${readable} ${symbol}` : readable;
|
|
7931
9616
|
}
|
|
7932
9617
|
|
|
9618
|
+
// types/payment-session.ts
|
|
9619
|
+
function createPaymentSession(params) {
|
|
9620
|
+
const now = Date.now();
|
|
9621
|
+
const deadlineMs = params.deadlineMs ?? 3e5;
|
|
9622
|
+
return {
|
|
9623
|
+
id: crypto.randomUUID(),
|
|
9624
|
+
direction: params.direction,
|
|
9625
|
+
status: "INITIATED",
|
|
9626
|
+
createdAt: now,
|
|
9627
|
+
updatedAt: now,
|
|
9628
|
+
deadline: now + deadlineMs,
|
|
9629
|
+
error: null,
|
|
9630
|
+
sourceTokenId: params.sourceTokenId,
|
|
9631
|
+
recipientNametag: params.recipientNametag,
|
|
9632
|
+
recipientPubkey: params.recipientPubkey,
|
|
9633
|
+
amount: params.amount,
|
|
9634
|
+
coinId: params.coinId,
|
|
9635
|
+
salt: params.salt
|
|
9636
|
+
};
|
|
9637
|
+
}
|
|
9638
|
+
function createSplitPaymentSession(params) {
|
|
9639
|
+
const now = Date.now();
|
|
9640
|
+
return {
|
|
9641
|
+
id: crypto.randomUUID(),
|
|
9642
|
+
direction: "SEND",
|
|
9643
|
+
sourceTokenId: params.sourceTokenId,
|
|
9644
|
+
paymentAmount: params.paymentAmount,
|
|
9645
|
+
changeAmount: params.changeAmount,
|
|
9646
|
+
recipientNametag: params.recipientNametag,
|
|
9647
|
+
recipientPubkey: params.recipientPubkey,
|
|
9648
|
+
splitGroupId: params.splitGroupId,
|
|
9649
|
+
phases: {
|
|
9650
|
+
burn: "PENDING",
|
|
9651
|
+
mints: "PENDING",
|
|
9652
|
+
transfer: "PENDING"
|
|
9653
|
+
},
|
|
9654
|
+
timing: {},
|
|
9655
|
+
createdAt: now,
|
|
9656
|
+
updatedAt: now,
|
|
9657
|
+
error: null
|
|
9658
|
+
};
|
|
9659
|
+
}
|
|
9660
|
+
function isPaymentSessionTimedOut(session) {
|
|
9661
|
+
if (!("deadline" in session) || !session.deadline) return false;
|
|
9662
|
+
return Date.now() > session.deadline;
|
|
9663
|
+
}
|
|
9664
|
+
function isPaymentSessionTerminal(session) {
|
|
9665
|
+
return session.status === "COMPLETED" || session.status === "FAILED" || session.status === "TIMED_OUT";
|
|
9666
|
+
}
|
|
9667
|
+
function createPaymentSessionError(code, message, recoverable = false, details) {
|
|
9668
|
+
return {
|
|
9669
|
+
code,
|
|
9670
|
+
message,
|
|
9671
|
+
timestamp: Date.now(),
|
|
9672
|
+
recoverable,
|
|
9673
|
+
details
|
|
9674
|
+
};
|
|
9675
|
+
}
|
|
9676
|
+
|
|
7933
9677
|
// types/index.ts
|
|
7934
9678
|
var SphereError = class extends Error {
|
|
7935
9679
|
code;
|
|
@@ -8117,14 +9861,30 @@ var TokenValidator = class {
|
|
|
8117
9861
|
}
|
|
8118
9862
|
}
|
|
8119
9863
|
/**
|
|
8120
|
-
* Check which tokens are spent
|
|
9864
|
+
* Check which tokens are spent using SDK Token object to calculate state hash.
|
|
9865
|
+
*
|
|
9866
|
+
* Follows the same approach as the Sphere webgui TokenValidationService:
|
|
9867
|
+
* 1. Parse TXF using SDK's Token.fromJSON()
|
|
9868
|
+
* 2. Calculate CURRENT state hash via sdkToken.state.calculateHash()
|
|
9869
|
+
* 3. Create RequestId via RequestId.create(walletPubKey, calculatedHash)
|
|
9870
|
+
*
|
|
9871
|
+
* Uses wallet's own pubkey (not source state predicate key) because "spent" means
|
|
9872
|
+
* the CURRENT OWNER committed this state. Using the source state key would falsely
|
|
9873
|
+
* detect received tokens as "spent" (sender's commitment matches source state).
|
|
8121
9874
|
*/
|
|
8122
9875
|
async checkSpentTokens(tokens, publicKey, options) {
|
|
8123
9876
|
const spentTokens = [];
|
|
8124
9877
|
const errors = [];
|
|
9878
|
+
if (!this.aggregatorClient) {
|
|
9879
|
+
errors.push("Aggregator client not available");
|
|
9880
|
+
return { spentTokens, errors };
|
|
9881
|
+
}
|
|
8125
9882
|
const batchSize = options?.batchSize ?? 3;
|
|
8126
9883
|
const total = tokens.length;
|
|
8127
9884
|
let completed = 0;
|
|
9885
|
+
const { Token: SdkToken3 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
9886
|
+
const { RequestId } = await import("@unicitylabs/state-transition-sdk/lib/api/RequestId");
|
|
9887
|
+
const pubKeyBytes = Buffer.from(publicKey, "hex");
|
|
8128
9888
|
for (let i = 0; i < tokens.length; i += batchSize) {
|
|
8129
9889
|
const batch = tokens.slice(i, i + batchSize);
|
|
8130
9890
|
const batchResults = await Promise.allSettled(
|
|
@@ -8134,13 +9894,39 @@ var TokenValidator = class {
|
|
|
8134
9894
|
if (!txf) {
|
|
8135
9895
|
return { tokenId: token.id, localId: token.id, stateHash: "", spent: false, error: "Invalid TXF" };
|
|
8136
9896
|
}
|
|
8137
|
-
const tokenId = txf.genesis
|
|
8138
|
-
const
|
|
8139
|
-
|
|
8140
|
-
|
|
9897
|
+
const tokenId = txf.genesis?.data?.tokenId || token.id;
|
|
9898
|
+
const sdkToken = await SdkToken3.fromJSON(txf);
|
|
9899
|
+
const calculatedStateHash = await sdkToken.state.calculateHash();
|
|
9900
|
+
const calculatedStateHashStr = calculatedStateHash.toJSON();
|
|
9901
|
+
const cacheKey = `${tokenId}:${calculatedStateHashStr}:${publicKey}`;
|
|
9902
|
+
const cached = this.spentStateCache.get(cacheKey);
|
|
9903
|
+
if (cached !== void 0) {
|
|
9904
|
+
if (cached.isSpent) {
|
|
9905
|
+
return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: true };
|
|
9906
|
+
}
|
|
9907
|
+
if (Date.now() - cached.timestamp < this.UNSPENT_CACHE_TTL_MS) {
|
|
9908
|
+
return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: false };
|
|
9909
|
+
}
|
|
9910
|
+
}
|
|
9911
|
+
const { DataHash } = await import("@unicitylabs/state-transition-sdk/lib/hash/DataHash");
|
|
9912
|
+
const stateHashObj = DataHash.fromJSON(calculatedStateHashStr);
|
|
9913
|
+
const requestId2 = await RequestId.create(pubKeyBytes, stateHashObj);
|
|
9914
|
+
const response = await this.aggregatorClient.getInclusionProof(requestId2);
|
|
9915
|
+
let isSpent = false;
|
|
9916
|
+
if (response.inclusionProof) {
|
|
9917
|
+
const proof = response.inclusionProof;
|
|
9918
|
+
const pathResult = await proof.merkleTreePath.verify(
|
|
9919
|
+
requestId2.toBitString().toBigInt()
|
|
9920
|
+
);
|
|
9921
|
+
if (pathResult.isPathValid && pathResult.isPathIncluded && proof.authenticator !== null) {
|
|
9922
|
+
isSpent = true;
|
|
9923
|
+
}
|
|
8141
9924
|
}
|
|
8142
|
-
|
|
8143
|
-
|
|
9925
|
+
this.spentStateCache.set(cacheKey, {
|
|
9926
|
+
isSpent,
|
|
9927
|
+
timestamp: Date.now()
|
|
9928
|
+
});
|
|
9929
|
+
return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: isSpent };
|
|
8144
9930
|
} catch (err) {
|
|
8145
9931
|
return {
|
|
8146
9932
|
tokenId: token.id,
|
|
@@ -8199,8 +9985,8 @@ var TokenValidator = class {
|
|
|
8199
9985
|
}
|
|
8200
9986
|
async verifyWithSdk(txfToken) {
|
|
8201
9987
|
try {
|
|
8202
|
-
const { Token:
|
|
8203
|
-
const sdkToken = await
|
|
9988
|
+
const { Token: Token5 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
9989
|
+
const sdkToken = await Token5.fromJSON(txfToken);
|
|
8204
9990
|
if (!this.trustBase) {
|
|
8205
9991
|
return { success: true };
|
|
8206
9992
|
}
|
|
@@ -8223,9 +10009,117 @@ var TokenValidator = class {
|
|
|
8223
10009
|
function createTokenValidator(options) {
|
|
8224
10010
|
return new TokenValidator(options);
|
|
8225
10011
|
}
|
|
10012
|
+
|
|
10013
|
+
// price/CoinGeckoPriceProvider.ts
|
|
10014
|
+
var CoinGeckoPriceProvider = class {
|
|
10015
|
+
platform = "coingecko";
|
|
10016
|
+
cache = /* @__PURE__ */ new Map();
|
|
10017
|
+
apiKey;
|
|
10018
|
+
cacheTtlMs;
|
|
10019
|
+
timeout;
|
|
10020
|
+
debug;
|
|
10021
|
+
baseUrl;
|
|
10022
|
+
constructor(config) {
|
|
10023
|
+
this.apiKey = config?.apiKey;
|
|
10024
|
+
this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
|
|
10025
|
+
this.timeout = config?.timeout ?? 1e4;
|
|
10026
|
+
this.debug = config?.debug ?? false;
|
|
10027
|
+
this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
|
|
10028
|
+
}
|
|
10029
|
+
async getPrices(tokenNames) {
|
|
10030
|
+
if (tokenNames.length === 0) {
|
|
10031
|
+
return /* @__PURE__ */ new Map();
|
|
10032
|
+
}
|
|
10033
|
+
const now = Date.now();
|
|
10034
|
+
const result = /* @__PURE__ */ new Map();
|
|
10035
|
+
const uncachedNames = [];
|
|
10036
|
+
for (const name of tokenNames) {
|
|
10037
|
+
const cached = this.cache.get(name);
|
|
10038
|
+
if (cached && cached.expiresAt > now) {
|
|
10039
|
+
if (cached.price !== null) {
|
|
10040
|
+
result.set(name, cached.price);
|
|
10041
|
+
}
|
|
10042
|
+
} else {
|
|
10043
|
+
uncachedNames.push(name);
|
|
10044
|
+
}
|
|
10045
|
+
}
|
|
10046
|
+
if (uncachedNames.length === 0) {
|
|
10047
|
+
return result;
|
|
10048
|
+
}
|
|
10049
|
+
try {
|
|
10050
|
+
const ids = uncachedNames.join(",");
|
|
10051
|
+
const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
|
|
10052
|
+
const headers = { Accept: "application/json" };
|
|
10053
|
+
if (this.apiKey) {
|
|
10054
|
+
headers["x-cg-pro-api-key"] = this.apiKey;
|
|
10055
|
+
}
|
|
10056
|
+
if (this.debug) {
|
|
10057
|
+
console.log(`[CoinGecko] Fetching prices for: ${uncachedNames.join(", ")}`);
|
|
10058
|
+
}
|
|
10059
|
+
const response = await fetch(url, {
|
|
10060
|
+
headers,
|
|
10061
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
10062
|
+
});
|
|
10063
|
+
if (!response.ok) {
|
|
10064
|
+
throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
|
|
10065
|
+
}
|
|
10066
|
+
const data = await response.json();
|
|
10067
|
+
for (const [name, values] of Object.entries(data)) {
|
|
10068
|
+
if (values && typeof values === "object") {
|
|
10069
|
+
const price = {
|
|
10070
|
+
tokenName: name,
|
|
10071
|
+
priceUsd: values.usd ?? 0,
|
|
10072
|
+
priceEur: values.eur,
|
|
10073
|
+
change24h: values.usd_24h_change,
|
|
10074
|
+
timestamp: now
|
|
10075
|
+
};
|
|
10076
|
+
this.cache.set(name, { price, expiresAt: now + this.cacheTtlMs });
|
|
10077
|
+
result.set(name, price);
|
|
10078
|
+
}
|
|
10079
|
+
}
|
|
10080
|
+
for (const name of uncachedNames) {
|
|
10081
|
+
if (!result.has(name)) {
|
|
10082
|
+
this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
|
|
10083
|
+
}
|
|
10084
|
+
}
|
|
10085
|
+
if (this.debug) {
|
|
10086
|
+
console.log(`[CoinGecko] Fetched ${result.size} prices`);
|
|
10087
|
+
}
|
|
10088
|
+
} catch (error) {
|
|
10089
|
+
if (this.debug) {
|
|
10090
|
+
console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
|
|
10091
|
+
}
|
|
10092
|
+
for (const name of uncachedNames) {
|
|
10093
|
+
const stale = this.cache.get(name);
|
|
10094
|
+
if (stale?.price) {
|
|
10095
|
+
result.set(name, stale.price);
|
|
10096
|
+
}
|
|
10097
|
+
}
|
|
10098
|
+
}
|
|
10099
|
+
return result;
|
|
10100
|
+
}
|
|
10101
|
+
async getPrice(tokenName) {
|
|
10102
|
+
const prices = await this.getPrices([tokenName]);
|
|
10103
|
+
return prices.get(tokenName) ?? null;
|
|
10104
|
+
}
|
|
10105
|
+
clearCache() {
|
|
10106
|
+
this.cache.clear();
|
|
10107
|
+
}
|
|
10108
|
+
};
|
|
10109
|
+
|
|
10110
|
+
// price/index.ts
|
|
10111
|
+
function createPriceProvider(config) {
|
|
10112
|
+
switch (config.platform) {
|
|
10113
|
+
case "coingecko":
|
|
10114
|
+
return new CoinGeckoPriceProvider(config);
|
|
10115
|
+
default:
|
|
10116
|
+
throw new Error(`Unsupported price platform: ${String(config.platform)}`);
|
|
10117
|
+
}
|
|
10118
|
+
}
|
|
8226
10119
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8227
10120
|
0 && (module.exports = {
|
|
8228
10121
|
COIN_TYPES,
|
|
10122
|
+
CoinGeckoPriceProvider,
|
|
8229
10123
|
CommunicationsModule,
|
|
8230
10124
|
DEFAULT_AGGREGATOR_TIMEOUT,
|
|
8231
10125
|
DEFAULT_AGGREGATOR_URL,
|
|
@@ -8261,8 +10155,12 @@ function createTokenValidator(options) {
|
|
|
8261
10155
|
createCommunicationsModule,
|
|
8262
10156
|
createKeyPair,
|
|
8263
10157
|
createL1PaymentsModule,
|
|
10158
|
+
createPaymentSession,
|
|
10159
|
+
createPaymentSessionError,
|
|
8264
10160
|
createPaymentsModule,
|
|
10161
|
+
createPriceProvider,
|
|
8265
10162
|
createSphere,
|
|
10163
|
+
createSplitPaymentSession,
|
|
8266
10164
|
createTokenValidator,
|
|
8267
10165
|
decodeBech32,
|
|
8268
10166
|
decryptCMasterKey,
|
|
@@ -8300,7 +10198,12 @@ function createTokenValidator(options) {
|
|
|
8300
10198
|
initSphere,
|
|
8301
10199
|
isArchivedKey,
|
|
8302
10200
|
isForkedKey,
|
|
10201
|
+
isInstantSplitBundle,
|
|
10202
|
+
isInstantSplitBundleV4,
|
|
10203
|
+
isInstantSplitBundleV5,
|
|
8303
10204
|
isKnownToken,
|
|
10205
|
+
isPaymentSessionTerminal,
|
|
10206
|
+
isPaymentSessionTimedOut,
|
|
8304
10207
|
isSQLiteDatabase,
|
|
8305
10208
|
isTextWalletEncrypted,
|
|
8306
10209
|
isTokenKey,
|