@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.js
CHANGED
|
@@ -1539,7 +1539,6 @@ var L1PaymentsModule = class {
|
|
|
1539
1539
|
_initialized = false;
|
|
1540
1540
|
_config;
|
|
1541
1541
|
_identity;
|
|
1542
|
-
_chainCode;
|
|
1543
1542
|
_addresses = [];
|
|
1544
1543
|
_wallet;
|
|
1545
1544
|
_transport;
|
|
@@ -1553,7 +1552,6 @@ var L1PaymentsModule = class {
|
|
|
1553
1552
|
}
|
|
1554
1553
|
async initialize(deps) {
|
|
1555
1554
|
this._identity = deps.identity;
|
|
1556
|
-
this._chainCode = deps.chainCode;
|
|
1557
1555
|
this._addresses = deps.addresses ?? [];
|
|
1558
1556
|
this._transport = deps.transport;
|
|
1559
1557
|
this._wallet = {
|
|
@@ -1589,7 +1587,6 @@ var L1PaymentsModule = class {
|
|
|
1589
1587
|
}
|
|
1590
1588
|
this._initialized = false;
|
|
1591
1589
|
this._identity = void 0;
|
|
1592
|
-
this._chainCode = void 0;
|
|
1593
1590
|
this._addresses = [];
|
|
1594
1591
|
this._wallet = void 0;
|
|
1595
1592
|
}
|
|
@@ -1624,10 +1621,10 @@ var L1PaymentsModule = class {
|
|
|
1624
1621
|
* Resolve nametag to L1 address using transport provider
|
|
1625
1622
|
*/
|
|
1626
1623
|
async resolveNametagToL1Address(nametag) {
|
|
1627
|
-
if (!this._transport?.
|
|
1628
|
-
throw new Error("Transport provider does not support
|
|
1624
|
+
if (!this._transport?.resolve) {
|
|
1625
|
+
throw new Error("Transport provider does not support resolution");
|
|
1629
1626
|
}
|
|
1630
|
-
const info = await this._transport.
|
|
1627
|
+
const info = await this._transport.resolve(nametag);
|
|
1631
1628
|
if (!info) {
|
|
1632
1629
|
throw new Error(`Nametag not found: ${nametag}`);
|
|
1633
1630
|
}
|
|
@@ -1904,25 +1901,11 @@ var TokenSplitCalculator = class {
|
|
|
1904
1901
|
* 3. If no exact match, determine which token to split
|
|
1905
1902
|
*/
|
|
1906
1903
|
async calculateOptimalSplit(availableTokens, targetAmount, targetCoinIdHex) {
|
|
1907
|
-
console.log(
|
|
1908
|
-
`[SplitCalculator] Calculating split for ${targetAmount} of ${targetCoinIdHex}`
|
|
1909
|
-
);
|
|
1910
|
-
console.log(`[SplitCalculator] Available tokens: ${availableTokens.length}`);
|
|
1911
1904
|
const candidates = [];
|
|
1912
1905
|
for (const t of availableTokens) {
|
|
1913
|
-
|
|
1914
|
-
if (t.
|
|
1915
|
-
|
|
1916
|
-
continue;
|
|
1917
|
-
}
|
|
1918
|
-
if (t.status !== "confirmed") {
|
|
1919
|
-
console.log(`[SplitCalculator] Skipping token ${t.id}: status is ${t.status}`);
|
|
1920
|
-
continue;
|
|
1921
|
-
}
|
|
1922
|
-
if (!t.sdkData) {
|
|
1923
|
-
console.log(`[SplitCalculator] Skipping token ${t.id}: no sdkData`);
|
|
1924
|
-
continue;
|
|
1925
|
-
}
|
|
1906
|
+
if (t.coinId !== targetCoinIdHex) continue;
|
|
1907
|
+
if (t.status !== "confirmed") continue;
|
|
1908
|
+
if (!t.sdkData) continue;
|
|
1926
1909
|
try {
|
|
1927
1910
|
const parsed = JSON.parse(t.sdkData);
|
|
1928
1911
|
const sdkToken = await SdkToken.fromJSON(parsed);
|
|
@@ -1950,14 +1933,12 @@ var TokenSplitCalculator = class {
|
|
|
1950
1933
|
}
|
|
1951
1934
|
const exactMatch = candidates.find((t) => t.amount === targetAmount);
|
|
1952
1935
|
if (exactMatch) {
|
|
1953
|
-
console.log("[SplitCalculator] Found exact match token");
|
|
1954
1936
|
return this.createDirectPlan([exactMatch], targetAmount, targetCoinIdHex);
|
|
1955
1937
|
}
|
|
1956
1938
|
const maxCombinationSize = Math.min(5, candidates.length);
|
|
1957
1939
|
for (let size = 2; size <= maxCombinationSize; size++) {
|
|
1958
1940
|
const combo = this.findCombinationOfSize(candidates, targetAmount, size);
|
|
1959
1941
|
if (combo) {
|
|
1960
|
-
console.log(`[SplitCalculator] Found exact combination of ${size} tokens`);
|
|
1961
1942
|
return this.createDirectPlan(combo, targetAmount, targetCoinIdHex);
|
|
1962
1943
|
}
|
|
1963
1944
|
}
|
|
@@ -1974,9 +1955,6 @@ var TokenSplitCalculator = class {
|
|
|
1974
1955
|
} else {
|
|
1975
1956
|
const neededFromThisToken = targetAmount - currentSum;
|
|
1976
1957
|
const remainderForSender = candidate.amount - neededFromThisToken;
|
|
1977
|
-
console.log(
|
|
1978
|
-
`[SplitCalculator] Split required. Sending: ${neededFromThisToken}, Remainder: ${remainderForSender}`
|
|
1979
|
-
);
|
|
1980
1958
|
return {
|
|
1981
1959
|
tokensToTransferDirectly: toTransfer,
|
|
1982
1960
|
tokenToSplit: candidate,
|
|
@@ -1995,16 +1973,10 @@ var TokenSplitCalculator = class {
|
|
|
1995
1973
|
*/
|
|
1996
1974
|
getTokenBalance(sdkToken, coinIdHex) {
|
|
1997
1975
|
try {
|
|
1998
|
-
if (!sdkToken.coins)
|
|
1999
|
-
console.log("[SplitCalculator] Token has no coins");
|
|
2000
|
-
return 0n;
|
|
2001
|
-
}
|
|
1976
|
+
if (!sdkToken.coins) return 0n;
|
|
2002
1977
|
const coinId = CoinId.fromJSON(coinIdHex);
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
return balance ?? 0n;
|
|
2006
|
-
} catch (e) {
|
|
2007
|
-
console.error("[SplitCalculator] Error getting token balance:", e);
|
|
1978
|
+
return sdkToken.coins.get(coinId) ?? 0n;
|
|
1979
|
+
} catch {
|
|
2008
1980
|
return 0n;
|
|
2009
1981
|
}
|
|
2010
1982
|
}
|
|
@@ -2233,20 +2205,18 @@ var NametagMinter = class {
|
|
|
2233
2205
|
const cleanNametag = nametag.replace("@", "").trim();
|
|
2234
2206
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2235
2207
|
try {
|
|
2236
|
-
const isAvailable = await this.isNametagAvailable(cleanNametag);
|
|
2237
|
-
if (!isAvailable) {
|
|
2238
|
-
return {
|
|
2239
|
-
success: false,
|
|
2240
|
-
error: `Nametag "${cleanNametag}" is already taken`
|
|
2241
|
-
};
|
|
2242
|
-
}
|
|
2243
2208
|
const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
|
|
2244
2209
|
const nametagTokenType = new TokenType(
|
|
2245
2210
|
Buffer.from(UNICITY_TOKEN_TYPE_HEX, "hex")
|
|
2246
2211
|
);
|
|
2247
|
-
const
|
|
2248
|
-
|
|
2249
|
-
|
|
2212
|
+
const nametagBytes = new TextEncoder().encode(cleanNametag);
|
|
2213
|
+
const pubKey = this.signingService.publicKey;
|
|
2214
|
+
const saltInput = new Uint8Array(pubKey.length + nametagBytes.length);
|
|
2215
|
+
saltInput.set(pubKey, 0);
|
|
2216
|
+
saltInput.set(nametagBytes, pubKey.length);
|
|
2217
|
+
const saltBuffer = await crypto.subtle.digest("SHA-256", saltInput);
|
|
2218
|
+
const salt = new Uint8Array(saltBuffer);
|
|
2219
|
+
this.log("Generated deterministic salt");
|
|
2250
2220
|
const mintData = await MintTransactionData.createFromNametag(
|
|
2251
2221
|
cleanNametag,
|
|
2252
2222
|
nametagTokenType,
|
|
@@ -2369,8 +2339,10 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
2369
2339
|
WALLET_EXISTS: "wallet_exists",
|
|
2370
2340
|
/** Current active address index */
|
|
2371
2341
|
CURRENT_ADDRESS_INDEX: "current_address_index",
|
|
2372
|
-
/**
|
|
2373
|
-
ADDRESS_NAMETAGS: "address_nametags"
|
|
2342
|
+
/** Nametag cache per address (separate from tracked addresses registry) */
|
|
2343
|
+
ADDRESS_NAMETAGS: "address_nametags",
|
|
2344
|
+
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
2345
|
+
TRACKED_ADDRESSES: "tracked_addresses"
|
|
2374
2346
|
};
|
|
2375
2347
|
var STORAGE_KEYS_ADDRESS = {
|
|
2376
2348
|
/** Pending transfers for this address */
|
|
@@ -2819,11 +2791,16 @@ function getCurrentStateHash(txf) {
|
|
|
2819
2791
|
if (lastTx?.newStateHash) {
|
|
2820
2792
|
return lastTx.newStateHash;
|
|
2821
2793
|
}
|
|
2822
|
-
|
|
2794
|
+
if (lastTx?.inclusionProof?.authenticator?.stateHash) {
|
|
2795
|
+
return lastTx.inclusionProof.authenticator.stateHash;
|
|
2796
|
+
}
|
|
2823
2797
|
}
|
|
2824
2798
|
if (txf._integrity?.currentStateHash) {
|
|
2825
2799
|
return txf._integrity.currentStateHash;
|
|
2826
2800
|
}
|
|
2801
|
+
if (txf.genesis?.inclusionProof?.authenticator?.stateHash) {
|
|
2802
|
+
return txf.genesis.inclusionProof.authenticator.stateHash;
|
|
2803
|
+
}
|
|
2827
2804
|
return void 0;
|
|
2828
2805
|
}
|
|
2829
2806
|
function hasValidTxfData(token) {
|
|
@@ -3184,16 +3161,733 @@ function getCoinIdByName(name) {
|
|
|
3184
3161
|
return TokenRegistry.getInstance().getCoinIdByName(name);
|
|
3185
3162
|
}
|
|
3186
3163
|
|
|
3187
|
-
// modules/payments/
|
|
3188
|
-
import { Token as
|
|
3164
|
+
// modules/payments/InstantSplitExecutor.ts
|
|
3165
|
+
import { Token as Token3 } from "@unicitylabs/state-transition-sdk/lib/token/Token";
|
|
3166
|
+
import { TokenId as TokenId3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenId";
|
|
3167
|
+
import { TokenState as TokenState3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
3189
3168
|
import { CoinId as CoinId3 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
|
|
3169
|
+
import { TokenCoinData as TokenCoinData2 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData";
|
|
3170
|
+
import { TokenSplitBuilder as TokenSplitBuilder2 } from "@unicitylabs/state-transition-sdk/lib/transaction/split/TokenSplitBuilder";
|
|
3171
|
+
import { HashAlgorithm as HashAlgorithm3 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
3172
|
+
import { UnmaskedPredicate as UnmaskedPredicate3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
3173
|
+
import { UnmaskedPredicateReference as UnmaskedPredicateReference2 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
|
|
3190
3174
|
import { TransferCommitment as TransferCommitment2 } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment";
|
|
3175
|
+
import { waitInclusionProof as waitInclusionProof3 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
3176
|
+
async function sha2563(input) {
|
|
3177
|
+
const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
3178
|
+
const buffer = new ArrayBuffer(data.length);
|
|
3179
|
+
new Uint8Array(buffer).set(data);
|
|
3180
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
|
|
3181
|
+
return new Uint8Array(hashBuffer);
|
|
3182
|
+
}
|
|
3183
|
+
function toHex2(bytes) {
|
|
3184
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3185
|
+
}
|
|
3186
|
+
function fromHex2(hex) {
|
|
3187
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3188
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3189
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
3190
|
+
}
|
|
3191
|
+
return bytes;
|
|
3192
|
+
}
|
|
3193
|
+
var InstantSplitExecutor = class {
|
|
3194
|
+
client;
|
|
3195
|
+
trustBase;
|
|
3196
|
+
signingService;
|
|
3197
|
+
devMode;
|
|
3198
|
+
constructor(config) {
|
|
3199
|
+
this.client = config.stateTransitionClient;
|
|
3200
|
+
this.trustBase = config.trustBase;
|
|
3201
|
+
this.signingService = config.signingService;
|
|
3202
|
+
this.devMode = config.devMode ?? false;
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
3206
|
+
*
|
|
3207
|
+
* Critical path (~2.3s):
|
|
3208
|
+
* 1. Create and submit burn commitment
|
|
3209
|
+
* 2. Wait for burn proof
|
|
3210
|
+
* 3. Create mint commitments with SplitMintReason
|
|
3211
|
+
* 4. Create transfer commitment (no mint proof needed)
|
|
3212
|
+
* 5. Send bundle via transport
|
|
3213
|
+
*
|
|
3214
|
+
* @param tokenToSplit - The SDK token to split
|
|
3215
|
+
* @param splitAmount - Amount to send to recipient
|
|
3216
|
+
* @param remainderAmount - Amount to keep as change
|
|
3217
|
+
* @param coinIdHex - Coin ID in hex format
|
|
3218
|
+
* @param recipientAddress - Recipient's address (PROXY or DIRECT)
|
|
3219
|
+
* @param transport - Transport provider for sending the bundle
|
|
3220
|
+
* @param recipientPubkey - Recipient's transport public key
|
|
3221
|
+
* @param options - Optional configuration
|
|
3222
|
+
* @returns InstantSplitResult with success status and timing info
|
|
3223
|
+
*/
|
|
3224
|
+
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3225
|
+
const startTime = performance.now();
|
|
3226
|
+
const splitGroupId = crypto.randomUUID();
|
|
3227
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3228
|
+
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3229
|
+
try {
|
|
3230
|
+
const coinId = new CoinId3(fromHex2(coinIdHex));
|
|
3231
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
3232
|
+
const recipientTokenId = new TokenId3(await sha2563(seedString));
|
|
3233
|
+
const senderTokenId = new TokenId3(await sha2563(seedString + "_sender"));
|
|
3234
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
3235
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3236
|
+
const senderAddressRef = await UnmaskedPredicateReference2.create(
|
|
3237
|
+
tokenToSplit.type,
|
|
3238
|
+
this.signingService.algorithm,
|
|
3239
|
+
this.signingService.publicKey,
|
|
3240
|
+
HashAlgorithm3.SHA256
|
|
3241
|
+
);
|
|
3242
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
3243
|
+
const builder = new TokenSplitBuilder2();
|
|
3244
|
+
const coinDataA = TokenCoinData2.create([[coinId, splitAmount]]);
|
|
3245
|
+
builder.createToken(
|
|
3246
|
+
recipientTokenId,
|
|
3247
|
+
tokenToSplit.type,
|
|
3248
|
+
new Uint8Array(0),
|
|
3249
|
+
coinDataA,
|
|
3250
|
+
senderAddress,
|
|
3251
|
+
// Mint to sender first, then transfer
|
|
3252
|
+
recipientSalt,
|
|
3253
|
+
null
|
|
3254
|
+
);
|
|
3255
|
+
const coinDataB = TokenCoinData2.create([[coinId, remainderAmount]]);
|
|
3256
|
+
builder.createToken(
|
|
3257
|
+
senderTokenId,
|
|
3258
|
+
tokenToSplit.type,
|
|
3259
|
+
new Uint8Array(0),
|
|
3260
|
+
coinDataB,
|
|
3261
|
+
senderAddress,
|
|
3262
|
+
senderSalt,
|
|
3263
|
+
null
|
|
3264
|
+
);
|
|
3265
|
+
const split = await builder.build(tokenToSplit);
|
|
3266
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3267
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3268
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3269
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3270
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3271
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3272
|
+
}
|
|
3273
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3274
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await waitInclusionProof3(this.trustBase, this.client, burnCommitment);
|
|
3275
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3276
|
+
const burnDuration = performance.now() - startTime;
|
|
3277
|
+
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3278
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3279
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3280
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3281
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3282
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3283
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3284
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3285
|
+
);
|
|
3286
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3287
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3288
|
+
);
|
|
3289
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3290
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3291
|
+
}
|
|
3292
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3293
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3294
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3295
|
+
recipientMintCommitment.transactionData,
|
|
3296
|
+
recipientAddress,
|
|
3297
|
+
transferSalt,
|
|
3298
|
+
this.signingService
|
|
3299
|
+
);
|
|
3300
|
+
const mintedPredicate = await UnmaskedPredicate3.create(
|
|
3301
|
+
recipientTokenId,
|
|
3302
|
+
tokenToSplit.type,
|
|
3303
|
+
this.signingService,
|
|
3304
|
+
HashAlgorithm3.SHA256,
|
|
3305
|
+
recipientSalt
|
|
3306
|
+
);
|
|
3307
|
+
const mintedState = new TokenState3(mintedPredicate, null);
|
|
3308
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3309
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3310
|
+
let nametagTokenJson;
|
|
3311
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3312
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3313
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3314
|
+
}
|
|
3315
|
+
const bundle = {
|
|
3316
|
+
version: "5.0",
|
|
3317
|
+
type: "INSTANT_SPLIT",
|
|
3318
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3319
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3320
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3321
|
+
amount: splitAmount.toString(),
|
|
3322
|
+
coinId: coinIdHex,
|
|
3323
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3324
|
+
splitGroupId,
|
|
3325
|
+
senderPubkey,
|
|
3326
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3327
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3328
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3329
|
+
finalRecipientStateJson: "",
|
|
3330
|
+
// Recipient creates their own
|
|
3331
|
+
recipientAddressJson: recipientAddressStr,
|
|
3332
|
+
nametagTokenJson
|
|
3333
|
+
};
|
|
3334
|
+
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3335
|
+
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3336
|
+
token: JSON.stringify(bundle),
|
|
3337
|
+
proof: null,
|
|
3338
|
+
// Proof is included in the bundle
|
|
3339
|
+
memo: "INSTANT_SPLIT_V5",
|
|
3340
|
+
sender: {
|
|
3341
|
+
transportPubkey: senderPubkey
|
|
3342
|
+
}
|
|
3343
|
+
});
|
|
3344
|
+
const criticalPathDuration = performance.now() - startTime;
|
|
3345
|
+
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3346
|
+
options?.onNostrDelivered?.(nostrEventId);
|
|
3347
|
+
if (!options?.skipBackground) {
|
|
3348
|
+
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3349
|
+
signingService: this.signingService,
|
|
3350
|
+
tokenType: tokenToSplit.type,
|
|
3351
|
+
coinId,
|
|
3352
|
+
senderTokenId,
|
|
3353
|
+
senderSalt,
|
|
3354
|
+
onProgress: options?.onBackgroundProgress,
|
|
3355
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3356
|
+
onStorageSync: options?.onStorageSync
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
return {
|
|
3360
|
+
success: true,
|
|
3361
|
+
nostrEventId,
|
|
3362
|
+
splitGroupId,
|
|
3363
|
+
criticalPathDurationMs: criticalPathDuration,
|
|
3364
|
+
backgroundStarted: !options?.skipBackground
|
|
3365
|
+
};
|
|
3366
|
+
} catch (error) {
|
|
3367
|
+
const duration = performance.now() - startTime;
|
|
3368
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3369
|
+
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3370
|
+
return {
|
|
3371
|
+
success: false,
|
|
3372
|
+
splitGroupId,
|
|
3373
|
+
criticalPathDurationMs: duration,
|
|
3374
|
+
error: errorMessage,
|
|
3375
|
+
backgroundStarted: false
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Create a TransferCommitment from MintTransactionData WITHOUT waiting for mint proof.
|
|
3381
|
+
*
|
|
3382
|
+
* Key insight: TransferCommitment.create() only needs token.state and token.nametagTokens.
|
|
3383
|
+
* It does NOT need the genesis transaction or mint proof.
|
|
3384
|
+
*/
|
|
3385
|
+
async createTransferCommitmentFromMintData(mintData, recipientAddress, transferSalt, signingService, nametagTokens) {
|
|
3386
|
+
const predicate = await UnmaskedPredicate3.create(
|
|
3387
|
+
mintData.tokenId,
|
|
3388
|
+
mintData.tokenType,
|
|
3389
|
+
signingService,
|
|
3390
|
+
HashAlgorithm3.SHA256,
|
|
3391
|
+
mintData.salt
|
|
3392
|
+
);
|
|
3393
|
+
const state = new TokenState3(predicate, null);
|
|
3394
|
+
const minimalToken = {
|
|
3395
|
+
state,
|
|
3396
|
+
nametagTokens: nametagTokens || [],
|
|
3397
|
+
id: mintData.tokenId,
|
|
3398
|
+
type: mintData.tokenType
|
|
3399
|
+
};
|
|
3400
|
+
const transferCommitment = await TransferCommitment2.create(
|
|
3401
|
+
minimalToken,
|
|
3402
|
+
recipientAddress,
|
|
3403
|
+
transferSalt,
|
|
3404
|
+
null,
|
|
3405
|
+
// recipientData
|
|
3406
|
+
null,
|
|
3407
|
+
// recipientDataHash
|
|
3408
|
+
signingService
|
|
3409
|
+
);
|
|
3410
|
+
return transferCommitment;
|
|
3411
|
+
}
|
|
3412
|
+
/**
|
|
3413
|
+
* V5 background submission.
|
|
3414
|
+
*
|
|
3415
|
+
* Submits mint commitments to aggregator in PARALLEL after transport delivery.
|
|
3416
|
+
* Then waits for sender's mint proof, reconstructs change token, and saves it.
|
|
3417
|
+
*/
|
|
3418
|
+
submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, context) {
|
|
3419
|
+
console.log("[InstantSplit] Background: Starting parallel mint submission...");
|
|
3420
|
+
const startTime = performance.now();
|
|
3421
|
+
const submissions = Promise.all([
|
|
3422
|
+
this.client.submitMintCommitment(senderMintCommitment).then((res) => ({ type: "senderMint", status: res.status })).catch((err) => ({ type: "senderMint", status: "ERROR", error: err })),
|
|
3423
|
+
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3424
|
+
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3425
|
+
]);
|
|
3426
|
+
submissions.then(async (results) => {
|
|
3427
|
+
const submitDuration = performance.now() - startTime;
|
|
3428
|
+
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3429
|
+
context.onProgress?.({
|
|
3430
|
+
stage: "MINTS_SUBMITTED",
|
|
3431
|
+
message: `All commitments submitted in ${submitDuration.toFixed(0)}ms`
|
|
3432
|
+
});
|
|
3433
|
+
const senderMintResult = results.find((r) => r.type === "senderMint");
|
|
3434
|
+
if (senderMintResult?.status !== "SUCCESS" && senderMintResult?.status !== "REQUEST_ID_EXISTS") {
|
|
3435
|
+
console.error("[InstantSplit] Background: Sender mint failed - cannot save change token");
|
|
3436
|
+
context.onProgress?.({
|
|
3437
|
+
stage: "FAILED",
|
|
3438
|
+
message: "Sender mint submission failed",
|
|
3439
|
+
error: String(senderMintResult?.error)
|
|
3440
|
+
});
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
console.log("[InstantSplit] Background: Waiting for sender mint proof...");
|
|
3444
|
+
const proofStartTime = performance.now();
|
|
3445
|
+
try {
|
|
3446
|
+
const senderMintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(senderMintCommitment) : await waitInclusionProof3(this.trustBase, this.client, senderMintCommitment);
|
|
3447
|
+
const proofDuration = performance.now() - proofStartTime;
|
|
3448
|
+
console.log(`[InstantSplit] Background: Sender mint proof received in ${proofDuration.toFixed(0)}ms`);
|
|
3449
|
+
context.onProgress?.({
|
|
3450
|
+
stage: "MINTS_PROVEN",
|
|
3451
|
+
message: `Mint proof received in ${proofDuration.toFixed(0)}ms`
|
|
3452
|
+
});
|
|
3453
|
+
const mintTransaction = senderMintCommitment.toTransaction(senderMintProof);
|
|
3454
|
+
const predicate = await UnmaskedPredicate3.create(
|
|
3455
|
+
context.senderTokenId,
|
|
3456
|
+
context.tokenType,
|
|
3457
|
+
context.signingService,
|
|
3458
|
+
HashAlgorithm3.SHA256,
|
|
3459
|
+
context.senderSalt
|
|
3460
|
+
);
|
|
3461
|
+
const state = new TokenState3(predicate, null);
|
|
3462
|
+
const changeToken = await Token3.mint(this.trustBase, state, mintTransaction);
|
|
3463
|
+
if (!this.devMode) {
|
|
3464
|
+
const verification = await changeToken.verify(this.trustBase);
|
|
3465
|
+
if (!verification.isSuccessful) {
|
|
3466
|
+
throw new Error(`Change token verification failed`);
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
console.log("[InstantSplit] Background: Change token created");
|
|
3470
|
+
context.onProgress?.({
|
|
3471
|
+
stage: "CHANGE_TOKEN_SAVED",
|
|
3472
|
+
message: "Change token created and verified"
|
|
3473
|
+
});
|
|
3474
|
+
if (context.onChangeTokenCreated) {
|
|
3475
|
+
await context.onChangeTokenCreated(changeToken);
|
|
3476
|
+
console.log("[InstantSplit] Background: Change token saved");
|
|
3477
|
+
}
|
|
3478
|
+
if (context.onStorageSync) {
|
|
3479
|
+
try {
|
|
3480
|
+
const syncSuccess = await context.onStorageSync();
|
|
3481
|
+
console.log(`[InstantSplit] Background: Storage sync ${syncSuccess ? "completed" : "deferred"}`);
|
|
3482
|
+
context.onProgress?.({
|
|
3483
|
+
stage: "STORAGE_SYNCED",
|
|
3484
|
+
message: syncSuccess ? "Storage synchronized" : "Sync deferred"
|
|
3485
|
+
});
|
|
3486
|
+
} catch (syncError) {
|
|
3487
|
+
console.warn("[InstantSplit] Background: Storage sync error:", syncError);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
const totalDuration = performance.now() - startTime;
|
|
3491
|
+
console.log(`[InstantSplit] Background: Complete in ${totalDuration.toFixed(0)}ms`);
|
|
3492
|
+
context.onProgress?.({
|
|
3493
|
+
stage: "COMPLETED",
|
|
3494
|
+
message: `Background processing complete in ${totalDuration.toFixed(0)}ms`
|
|
3495
|
+
});
|
|
3496
|
+
} catch (proofError) {
|
|
3497
|
+
console.error("[InstantSplit] Background: Failed to get sender mint proof:", proofError);
|
|
3498
|
+
context.onProgress?.({
|
|
3499
|
+
stage: "FAILED",
|
|
3500
|
+
message: "Failed to get mint proof",
|
|
3501
|
+
error: String(proofError)
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
}).catch((err) => {
|
|
3505
|
+
console.error("[InstantSplit] Background: Submission batch failed:", err);
|
|
3506
|
+
context.onProgress?.({
|
|
3507
|
+
stage: "FAILED",
|
|
3508
|
+
message: "Background submission failed",
|
|
3509
|
+
error: String(err)
|
|
3510
|
+
});
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
/**
|
|
3514
|
+
* Dev mode bypass for waitInclusionProof.
|
|
3515
|
+
* In dev mode, we create a mock proof for testing.
|
|
3516
|
+
*/
|
|
3517
|
+
async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
|
|
3518
|
+
if (this.devMode) {
|
|
3519
|
+
try {
|
|
3520
|
+
return await Promise.race([
|
|
3521
|
+
waitInclusionProof3(this.trustBase, this.client, commitment),
|
|
3522
|
+
new Promise(
|
|
3523
|
+
(_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
|
|
3524
|
+
)
|
|
3525
|
+
]);
|
|
3526
|
+
} catch {
|
|
3527
|
+
console.log("[InstantSplit] Dev mode: Using mock proof");
|
|
3528
|
+
return {
|
|
3529
|
+
toJSON: () => ({ mock: true })
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
return waitInclusionProof3(this.trustBase, this.client, commitment);
|
|
3534
|
+
}
|
|
3535
|
+
};
|
|
3536
|
+
|
|
3537
|
+
// modules/payments/InstantSplitProcessor.ts
|
|
3538
|
+
import { Token as Token4 } from "@unicitylabs/state-transition-sdk/lib/token/Token";
|
|
3539
|
+
import { TokenState as TokenState4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
3540
|
+
import { TokenType as TokenType2 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
3541
|
+
import { HashAlgorithm as HashAlgorithm4 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
3542
|
+
import { UnmaskedPredicate as UnmaskedPredicate4 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
3543
|
+
import { TransferCommitment as TransferCommitment3 } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment";
|
|
3191
3544
|
import { TransferTransaction } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction";
|
|
3545
|
+
import { MintCommitment as MintCommitment2 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment";
|
|
3546
|
+
import { MintTransactionData as MintTransactionData2 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData";
|
|
3547
|
+
import { waitInclusionProof as waitInclusionProof4 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
3548
|
+
|
|
3549
|
+
// types/instant-split.ts
|
|
3550
|
+
function isInstantSplitBundle(obj) {
|
|
3551
|
+
if (typeof obj !== "object" || obj === null) {
|
|
3552
|
+
return false;
|
|
3553
|
+
}
|
|
3554
|
+
const bundle = obj;
|
|
3555
|
+
if (bundle.type !== "INSTANT_SPLIT") return false;
|
|
3556
|
+
if (typeof bundle.recipientMintData !== "string") return false;
|
|
3557
|
+
if (typeof bundle.transferCommitment !== "string") return false;
|
|
3558
|
+
if (typeof bundle.amount !== "string") return false;
|
|
3559
|
+
if (typeof bundle.coinId !== "string") return false;
|
|
3560
|
+
if (typeof bundle.splitGroupId !== "string") return false;
|
|
3561
|
+
if (typeof bundle.senderPubkey !== "string") return false;
|
|
3562
|
+
if (typeof bundle.recipientSaltHex !== "string") return false;
|
|
3563
|
+
if (typeof bundle.transferSaltHex !== "string") return false;
|
|
3564
|
+
if (bundle.version === "4.0") {
|
|
3565
|
+
return typeof bundle.burnCommitment === "string";
|
|
3566
|
+
} else if (bundle.version === "5.0") {
|
|
3567
|
+
return typeof bundle.burnTransaction === "string" && typeof bundle.mintedTokenStateJson === "string" && typeof bundle.finalRecipientStateJson === "string" && typeof bundle.recipientAddressJson === "string";
|
|
3568
|
+
}
|
|
3569
|
+
return false;
|
|
3570
|
+
}
|
|
3571
|
+
function isInstantSplitBundleV4(obj) {
|
|
3572
|
+
return isInstantSplitBundle(obj) && obj.version === "4.0";
|
|
3573
|
+
}
|
|
3574
|
+
function isInstantSplitBundleV5(obj) {
|
|
3575
|
+
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
// modules/payments/InstantSplitProcessor.ts
|
|
3579
|
+
function fromHex3(hex) {
|
|
3580
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3581
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3582
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
3583
|
+
}
|
|
3584
|
+
return bytes;
|
|
3585
|
+
}
|
|
3586
|
+
var InstantSplitProcessor = class {
|
|
3587
|
+
client;
|
|
3588
|
+
trustBase;
|
|
3589
|
+
devMode;
|
|
3590
|
+
constructor(config) {
|
|
3591
|
+
this.client = config.stateTransitionClient;
|
|
3592
|
+
this.trustBase = config.trustBase;
|
|
3593
|
+
this.devMode = config.devMode ?? false;
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Process a received INSTANT_SPLIT bundle.
|
|
3597
|
+
*
|
|
3598
|
+
* @param bundle - The received bundle (V4 or V5)
|
|
3599
|
+
* @param signingService - Recipient's signing service
|
|
3600
|
+
* @param senderPubkey - Sender's public key (for verification)
|
|
3601
|
+
* @param options - Processing options
|
|
3602
|
+
* @returns Processing result with finalized token if successful
|
|
3603
|
+
*/
|
|
3604
|
+
async processReceivedBundle(bundle, signingService, senderPubkey, options) {
|
|
3605
|
+
if (isInstantSplitBundleV5(bundle)) {
|
|
3606
|
+
return this.processV5Bundle(bundle, signingService, senderPubkey, options);
|
|
3607
|
+
} else if (isInstantSplitBundleV4(bundle)) {
|
|
3608
|
+
return this.processV4Bundle(bundle, signingService, senderPubkey, options);
|
|
3609
|
+
}
|
|
3610
|
+
return {
|
|
3611
|
+
success: false,
|
|
3612
|
+
error: `Unknown bundle version: ${bundle.version}`,
|
|
3613
|
+
durationMs: 0
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
/**
|
|
3617
|
+
* Process a V5 bundle (production mode).
|
|
3618
|
+
*
|
|
3619
|
+
* V5 Flow:
|
|
3620
|
+
* 1. Burn transaction already has proof (just validate)
|
|
3621
|
+
* 2. Submit mint commitment -> wait for proof
|
|
3622
|
+
* 3. Reconstruct minted token (use sender's state from bundle)
|
|
3623
|
+
* 4. Submit transfer commitment -> wait for proof
|
|
3624
|
+
* 5. Create recipient's final state and finalize token
|
|
3625
|
+
*/
|
|
3626
|
+
async processV5Bundle(bundle, signingService, senderPubkey, options) {
|
|
3627
|
+
console.log("[InstantSplitProcessor] Processing V5 bundle...");
|
|
3628
|
+
const startTime = performance.now();
|
|
3629
|
+
try {
|
|
3630
|
+
if (bundle.senderPubkey !== senderPubkey) {
|
|
3631
|
+
console.warn("[InstantSplitProcessor] Sender pubkey mismatch (non-fatal)");
|
|
3632
|
+
}
|
|
3633
|
+
const burnTxJson = JSON.parse(bundle.burnTransaction);
|
|
3634
|
+
const burnTransaction = await TransferTransaction.fromJSON(burnTxJson);
|
|
3635
|
+
console.log("[InstantSplitProcessor] Burn transaction validated");
|
|
3636
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
3637
|
+
const mintData = await MintTransactionData2.fromJSON(mintDataJson);
|
|
3638
|
+
const mintCommitment = await MintCommitment2.create(mintData);
|
|
3639
|
+
console.log("[InstantSplitProcessor] Mint commitment recreated");
|
|
3640
|
+
const mintResponse = await this.client.submitMintCommitment(mintCommitment);
|
|
3641
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3642
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
3643
|
+
}
|
|
3644
|
+
console.log(`[InstantSplitProcessor] Mint submitted: ${mintResponse.status}`);
|
|
3645
|
+
const mintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(mintCommitment, options?.proofTimeoutMs) : await waitInclusionProof4(this.trustBase, this.client, mintCommitment);
|
|
3646
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
3647
|
+
console.log("[InstantSplitProcessor] Mint proof received");
|
|
3648
|
+
const tokenType = new TokenType2(fromHex3(bundle.tokenTypeHex));
|
|
3649
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
3650
|
+
const tokenJson = {
|
|
3651
|
+
version: "2.0",
|
|
3652
|
+
state: senderMintedStateJson,
|
|
3653
|
+
genesis: mintTransaction.toJSON(),
|
|
3654
|
+
transactions: [],
|
|
3655
|
+
nametags: []
|
|
3656
|
+
};
|
|
3657
|
+
const mintedToken = await Token4.fromJSON(tokenJson);
|
|
3658
|
+
console.log("[InstantSplitProcessor] Minted token reconstructed from sender state");
|
|
3659
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
3660
|
+
const transferCommitment = await TransferCommitment3.fromJSON(transferCommitmentJson);
|
|
3661
|
+
const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
|
|
3662
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3663
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
3664
|
+
}
|
|
3665
|
+
console.log(`[InstantSplitProcessor] Transfer submitted: ${transferResponse.status}`);
|
|
3666
|
+
const transferProof = this.devMode ? await this.waitInclusionProofWithDevBypass(transferCommitment, options?.proofTimeoutMs) : await waitInclusionProof4(this.trustBase, this.client, transferCommitment);
|
|
3667
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
3668
|
+
console.log("[InstantSplitProcessor] Transfer proof received");
|
|
3669
|
+
const transferSalt = fromHex3(bundle.transferSaltHex);
|
|
3670
|
+
const finalRecipientPredicate = await UnmaskedPredicate4.create(
|
|
3671
|
+
mintData.tokenId,
|
|
3672
|
+
tokenType,
|
|
3673
|
+
signingService,
|
|
3674
|
+
HashAlgorithm4.SHA256,
|
|
3675
|
+
transferSalt
|
|
3676
|
+
);
|
|
3677
|
+
const finalRecipientState = new TokenState4(finalRecipientPredicate, null);
|
|
3678
|
+
console.log("[InstantSplitProcessor] Final recipient state created");
|
|
3679
|
+
let nametagTokens = [];
|
|
3680
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
3681
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
3682
|
+
console.log("[InstantSplitProcessor] PROXY address detected, finding nametag token...");
|
|
3683
|
+
if (bundle.nametagTokenJson) {
|
|
3684
|
+
try {
|
|
3685
|
+
const nametagToken = await Token4.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
3686
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
3687
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
3688
|
+
if (proxy.address !== recipientAddressStr) {
|
|
3689
|
+
console.warn("[InstantSplitProcessor] Nametag PROXY address mismatch, ignoring bundle token");
|
|
3690
|
+
} else {
|
|
3691
|
+
nametagTokens = [nametagToken];
|
|
3692
|
+
console.log("[InstantSplitProcessor] Using nametag token from bundle (address validated)");
|
|
3693
|
+
}
|
|
3694
|
+
} catch (err) {
|
|
3695
|
+
console.warn("[InstantSplitProcessor] Failed to parse nametag token from bundle:", err);
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
if (nametagTokens.length === 0 && options?.findNametagToken) {
|
|
3699
|
+
const token = await options.findNametagToken(recipientAddressStr);
|
|
3700
|
+
if (token) {
|
|
3701
|
+
nametagTokens = [token];
|
|
3702
|
+
console.log("[InstantSplitProcessor] Found nametag token via callback");
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
if (nametagTokens.length === 0 && !this.devMode) {
|
|
3706
|
+
throw new Error(
|
|
3707
|
+
`PROXY address transfer requires nametag token for verification. Address: ${recipientAddressStr}`
|
|
3708
|
+
);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
let finalToken;
|
|
3712
|
+
if (this.devMode) {
|
|
3713
|
+
console.log("[InstantSplitProcessor] Dev mode: finalizing without verification");
|
|
3714
|
+
const tokenJson2 = mintedToken.toJSON();
|
|
3715
|
+
tokenJson2.state = finalRecipientState.toJSON();
|
|
3716
|
+
tokenJson2.transactions = [transferTransaction.toJSON()];
|
|
3717
|
+
finalToken = await Token4.fromJSON(tokenJson2);
|
|
3718
|
+
} else {
|
|
3719
|
+
finalToken = await this.client.finalizeTransaction(
|
|
3720
|
+
this.trustBase,
|
|
3721
|
+
mintedToken,
|
|
3722
|
+
finalRecipientState,
|
|
3723
|
+
transferTransaction,
|
|
3724
|
+
nametagTokens
|
|
3725
|
+
);
|
|
3726
|
+
}
|
|
3727
|
+
console.log("[InstantSplitProcessor] Token finalized");
|
|
3728
|
+
if (!this.devMode) {
|
|
3729
|
+
const verification = await finalToken.verify(this.trustBase);
|
|
3730
|
+
if (!verification.isSuccessful) {
|
|
3731
|
+
throw new Error(`Token verification failed`);
|
|
3732
|
+
}
|
|
3733
|
+
console.log("[InstantSplitProcessor] Token verified");
|
|
3734
|
+
}
|
|
3735
|
+
const duration = performance.now() - startTime;
|
|
3736
|
+
console.log(`[InstantSplitProcessor] V5 bundle processed in ${duration.toFixed(0)}ms`);
|
|
3737
|
+
return {
|
|
3738
|
+
success: true,
|
|
3739
|
+
token: finalToken,
|
|
3740
|
+
durationMs: duration
|
|
3741
|
+
};
|
|
3742
|
+
} catch (error) {
|
|
3743
|
+
const duration = performance.now() - startTime;
|
|
3744
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3745
|
+
console.error(`[InstantSplitProcessor] V5 processing failed:`, error);
|
|
3746
|
+
return {
|
|
3747
|
+
success: false,
|
|
3748
|
+
error: errorMessage,
|
|
3749
|
+
durationMs: duration
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* Process a V4 bundle (dev mode only).
|
|
3755
|
+
*
|
|
3756
|
+
* V4 Flow:
|
|
3757
|
+
* 1. Submit burn commitment -> wait for proof
|
|
3758
|
+
* 2. Submit mint commitment -> wait for proof
|
|
3759
|
+
* 3. Reconstruct minted token
|
|
3760
|
+
* 4. Submit transfer commitment -> wait for proof
|
|
3761
|
+
* 5. Finalize token
|
|
3762
|
+
*/
|
|
3763
|
+
async processV4Bundle(bundle, signingService, _senderPubkey, options) {
|
|
3764
|
+
if (!this.devMode) {
|
|
3765
|
+
return {
|
|
3766
|
+
success: false,
|
|
3767
|
+
error: "INSTANT_SPLIT V4 is only supported in dev mode",
|
|
3768
|
+
durationMs: 0
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
console.log("[InstantSplitProcessor] Processing V4 bundle (dev mode)...");
|
|
3772
|
+
const startTime = performance.now();
|
|
3773
|
+
try {
|
|
3774
|
+
const burnCommitmentJson = JSON.parse(bundle.burnCommitment);
|
|
3775
|
+
const burnCommitment = await TransferCommitment3.fromJSON(burnCommitmentJson);
|
|
3776
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3777
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3778
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3779
|
+
}
|
|
3780
|
+
await this.waitInclusionProofWithDevBypass(burnCommitment, options?.proofTimeoutMs);
|
|
3781
|
+
console.log("[InstantSplitProcessor] V4: Burn proof received");
|
|
3782
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
3783
|
+
const mintData = await MintTransactionData2.fromJSON(mintDataJson);
|
|
3784
|
+
const mintCommitment = await MintCommitment2.create(mintData);
|
|
3785
|
+
const mintResponse = await this.client.submitMintCommitment(mintCommitment);
|
|
3786
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3787
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
3788
|
+
}
|
|
3789
|
+
const mintProof = await this.waitInclusionProofWithDevBypass(
|
|
3790
|
+
mintCommitment,
|
|
3791
|
+
options?.proofTimeoutMs
|
|
3792
|
+
);
|
|
3793
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
3794
|
+
console.log("[InstantSplitProcessor] V4: Mint proof received");
|
|
3795
|
+
const tokenType = new TokenType2(fromHex3(bundle.tokenTypeHex));
|
|
3796
|
+
const recipientSalt = fromHex3(bundle.recipientSaltHex);
|
|
3797
|
+
const recipientPredicate = await UnmaskedPredicate4.create(
|
|
3798
|
+
mintData.tokenId,
|
|
3799
|
+
tokenType,
|
|
3800
|
+
signingService,
|
|
3801
|
+
HashAlgorithm4.SHA256,
|
|
3802
|
+
recipientSalt
|
|
3803
|
+
);
|
|
3804
|
+
const recipientState = new TokenState4(recipientPredicate, null);
|
|
3805
|
+
const tokenJson = {
|
|
3806
|
+
version: "2.0",
|
|
3807
|
+
state: recipientState.toJSON(),
|
|
3808
|
+
genesis: mintTransaction.toJSON(),
|
|
3809
|
+
transactions: [],
|
|
3810
|
+
nametags: []
|
|
3811
|
+
};
|
|
3812
|
+
const mintedToken = await Token4.fromJSON(tokenJson);
|
|
3813
|
+
console.log("[InstantSplitProcessor] V4: Minted token reconstructed");
|
|
3814
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
3815
|
+
const transferCommitment = await TransferCommitment3.fromJSON(transferCommitmentJson);
|
|
3816
|
+
const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
|
|
3817
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3818
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
3819
|
+
}
|
|
3820
|
+
const transferProof = await this.waitInclusionProofWithDevBypass(
|
|
3821
|
+
transferCommitment,
|
|
3822
|
+
options?.proofTimeoutMs
|
|
3823
|
+
);
|
|
3824
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
3825
|
+
console.log("[InstantSplitProcessor] V4: Transfer proof received");
|
|
3826
|
+
const transferSalt = fromHex3(bundle.transferSaltHex);
|
|
3827
|
+
const finalPredicate = await UnmaskedPredicate4.create(
|
|
3828
|
+
mintData.tokenId,
|
|
3829
|
+
tokenType,
|
|
3830
|
+
signingService,
|
|
3831
|
+
HashAlgorithm4.SHA256,
|
|
3832
|
+
transferSalt
|
|
3833
|
+
);
|
|
3834
|
+
const finalState = new TokenState4(finalPredicate, null);
|
|
3835
|
+
const finalTokenJson = mintedToken.toJSON();
|
|
3836
|
+
finalTokenJson.state = finalState.toJSON();
|
|
3837
|
+
finalTokenJson.transactions = [transferTransaction.toJSON()];
|
|
3838
|
+
const finalToken = await Token4.fromJSON(finalTokenJson);
|
|
3839
|
+
console.log("[InstantSplitProcessor] V4: Token finalized");
|
|
3840
|
+
const duration = performance.now() - startTime;
|
|
3841
|
+
console.log(`[InstantSplitProcessor] V4 bundle processed in ${duration.toFixed(0)}ms`);
|
|
3842
|
+
return {
|
|
3843
|
+
success: true,
|
|
3844
|
+
token: finalToken,
|
|
3845
|
+
durationMs: duration
|
|
3846
|
+
};
|
|
3847
|
+
} catch (error) {
|
|
3848
|
+
const duration = performance.now() - startTime;
|
|
3849
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3850
|
+
console.error(`[InstantSplitProcessor] V4 processing failed:`, error);
|
|
3851
|
+
return {
|
|
3852
|
+
success: false,
|
|
3853
|
+
error: errorMessage,
|
|
3854
|
+
durationMs: duration
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
/**
|
|
3859
|
+
* Dev mode bypass for waitInclusionProof.
|
|
3860
|
+
*/
|
|
3861
|
+
async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
|
|
3862
|
+
if (this.devMode) {
|
|
3863
|
+
try {
|
|
3864
|
+
return await Promise.race([
|
|
3865
|
+
waitInclusionProof4(this.trustBase, this.client, commitment),
|
|
3866
|
+
new Promise(
|
|
3867
|
+
(_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
|
|
3868
|
+
)
|
|
3869
|
+
]);
|
|
3870
|
+
} catch {
|
|
3871
|
+
console.log("[InstantSplitProcessor] Dev mode: Using mock proof");
|
|
3872
|
+
return {
|
|
3873
|
+
toJSON: () => ({ mock: true })
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
return waitInclusionProof4(this.trustBase, this.client, commitment);
|
|
3878
|
+
}
|
|
3879
|
+
};
|
|
3880
|
+
|
|
3881
|
+
// modules/payments/PaymentsModule.ts
|
|
3882
|
+
import { Token as SdkToken2 } from "@unicitylabs/state-transition-sdk/lib/token/Token";
|
|
3883
|
+
import { CoinId as CoinId4 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
|
|
3884
|
+
import { TransferCommitment as TransferCommitment4 } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment";
|
|
3885
|
+
import { TransferTransaction as TransferTransaction2 } from "@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction";
|
|
3192
3886
|
import { SigningService } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
|
|
3193
3887
|
import { AddressScheme } from "@unicitylabs/state-transition-sdk/lib/address/AddressScheme";
|
|
3194
|
-
import { UnmaskedPredicate as
|
|
3195
|
-
import { TokenState as
|
|
3196
|
-
import { HashAlgorithm as
|
|
3888
|
+
import { UnmaskedPredicate as UnmaskedPredicate5 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
3889
|
+
import { TokenState as TokenState5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
3890
|
+
import { HashAlgorithm as HashAlgorithm5 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
3197
3891
|
function enrichWithRegistry(info) {
|
|
3198
3892
|
const registry = TokenRegistry.getInstance();
|
|
3199
3893
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -3232,7 +3926,7 @@ async function parseTokenInfo(tokenData) {
|
|
|
3232
3926
|
if (Array.isArray(firstCoin) && firstCoin.length === 2) {
|
|
3233
3927
|
[coinIdObj, amount] = firstCoin;
|
|
3234
3928
|
}
|
|
3235
|
-
if (coinIdObj instanceof
|
|
3929
|
+
if (coinIdObj instanceof CoinId4) {
|
|
3236
3930
|
const coinIdHex = coinIdObj.toJSON();
|
|
3237
3931
|
return enrichWithRegistry({
|
|
3238
3932
|
coinId: coinIdHex,
|
|
@@ -3365,21 +4059,48 @@ function extractStateHashFromSdkData(sdkData) {
|
|
|
3365
4059
|
if (!sdkData) return "";
|
|
3366
4060
|
try {
|
|
3367
4061
|
const txf = JSON.parse(sdkData);
|
|
3368
|
-
|
|
4062
|
+
const stateHash = getCurrentStateHash(txf);
|
|
4063
|
+
if (!stateHash) {
|
|
4064
|
+
if (txf.state?.hash) {
|
|
4065
|
+
return txf.state.hash;
|
|
4066
|
+
}
|
|
4067
|
+
if (txf.stateHash) {
|
|
4068
|
+
return txf.stateHash;
|
|
4069
|
+
}
|
|
4070
|
+
if (txf.currentStateHash) {
|
|
4071
|
+
return txf.currentStateHash;
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
return stateHash || "";
|
|
3369
4075
|
} catch {
|
|
3370
4076
|
return "";
|
|
3371
4077
|
}
|
|
3372
4078
|
}
|
|
3373
|
-
function
|
|
3374
|
-
|
|
4079
|
+
function createTokenStateKey(tokenId, stateHash) {
|
|
4080
|
+
return `${tokenId}_${stateHash}`;
|
|
4081
|
+
}
|
|
4082
|
+
function extractTokenStateKey(token) {
|
|
4083
|
+
const tokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
4084
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
4085
|
+
if (!tokenId || !stateHash) return null;
|
|
4086
|
+
return createTokenStateKey(tokenId, stateHash);
|
|
4087
|
+
}
|
|
4088
|
+
function hasSameGenesisTokenId(t1, t2) {
|
|
3375
4089
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
3376
4090
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
3377
4091
|
return !!(id1 && id2 && id1 === id2);
|
|
3378
4092
|
}
|
|
4093
|
+
function isSameTokenState(t1, t2) {
|
|
4094
|
+
const key1 = extractTokenStateKey(t1);
|
|
4095
|
+
const key2 = extractTokenStateKey(t2);
|
|
4096
|
+
return !!(key1 && key2 && key1 === key2);
|
|
4097
|
+
}
|
|
3379
4098
|
function createTombstoneFromToken(token) {
|
|
3380
4099
|
const tokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
3381
|
-
if (!tokenId) return null;
|
|
3382
4100
|
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
4101
|
+
if (!tokenId || !stateHash) {
|
|
4102
|
+
return null;
|
|
4103
|
+
}
|
|
3383
4104
|
return {
|
|
3384
4105
|
tokenId,
|
|
3385
4106
|
stateHash,
|
|
@@ -3445,7 +4166,7 @@ function findBestTokenVersion(tokenId, archivedTokens, forkedTokens) {
|
|
|
3445
4166
|
candidates.sort((a, b) => countCommittedTxns(b) - countCommittedTxns(a));
|
|
3446
4167
|
return candidates[0];
|
|
3447
4168
|
}
|
|
3448
|
-
var PaymentsModule = class {
|
|
4169
|
+
var PaymentsModule = class _PaymentsModule {
|
|
3449
4170
|
moduleConfig;
|
|
3450
4171
|
deps = null;
|
|
3451
4172
|
/** L1 (ALPHA blockchain) payments sub-module (null if disabled) */
|
|
@@ -3470,6 +4191,13 @@ var PaymentsModule = class {
|
|
|
3470
4191
|
unsubscribeTransfers = null;
|
|
3471
4192
|
unsubscribePaymentRequests = null;
|
|
3472
4193
|
unsubscribePaymentRequestResponses = null;
|
|
4194
|
+
// NOSTR-FIRST proof polling (background proof verification)
|
|
4195
|
+
proofPollingJobs = /* @__PURE__ */ new Map();
|
|
4196
|
+
proofPollingInterval = null;
|
|
4197
|
+
static PROOF_POLLING_INTERVAL_MS = 2e3;
|
|
4198
|
+
// Poll every 2s
|
|
4199
|
+
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4200
|
+
// Max 30 attempts (~60s)
|
|
3473
4201
|
constructor(config) {
|
|
3474
4202
|
this.moduleConfig = {
|
|
3475
4203
|
autoSync: config?.autoSync ?? true,
|
|
@@ -3485,6 +4213,8 @@ var PaymentsModule = class {
|
|
|
3485
4213
|
getConfig() {
|
|
3486
4214
|
return this.moduleConfig;
|
|
3487
4215
|
}
|
|
4216
|
+
/** Price provider (optional) */
|
|
4217
|
+
priceProvider = null;
|
|
3488
4218
|
log(...args) {
|
|
3489
4219
|
if (this.moduleConfig.debug) {
|
|
3490
4220
|
console.log("[PaymentsModule]", ...args);
|
|
@@ -3497,7 +4227,21 @@ var PaymentsModule = class {
|
|
|
3497
4227
|
* Initialize module with dependencies
|
|
3498
4228
|
*/
|
|
3499
4229
|
initialize(deps) {
|
|
4230
|
+
this.unsubscribeTransfers?.();
|
|
4231
|
+
this.unsubscribeTransfers = null;
|
|
4232
|
+
this.unsubscribePaymentRequests?.();
|
|
4233
|
+
this.unsubscribePaymentRequests = null;
|
|
4234
|
+
this.unsubscribePaymentRequestResponses?.();
|
|
4235
|
+
this.unsubscribePaymentRequestResponses = null;
|
|
4236
|
+
this.tokens.clear();
|
|
4237
|
+
this.pendingTransfers.clear();
|
|
4238
|
+
this.tombstones = [];
|
|
4239
|
+
this.archivedTokens.clear();
|
|
4240
|
+
this.forkedTokens.clear();
|
|
4241
|
+
this.transactionHistory = [];
|
|
4242
|
+
this.nametag = null;
|
|
3500
4243
|
this.deps = deps;
|
|
4244
|
+
this.priceProvider = deps.price ?? null;
|
|
3501
4245
|
if (this.l1) {
|
|
3502
4246
|
this.l1.initialize({
|
|
3503
4247
|
identity: deps.identity,
|
|
@@ -3568,6 +4312,8 @@ var PaymentsModule = class {
|
|
|
3568
4312
|
this.unsubscribePaymentRequestResponses = null;
|
|
3569
4313
|
this.paymentRequestHandlers.clear();
|
|
3570
4314
|
this.paymentRequestResponseHandlers.clear();
|
|
4315
|
+
this.stopProofPolling();
|
|
4316
|
+
this.proofPollingJobs.clear();
|
|
3571
4317
|
for (const [, resolver] of this.pendingResponseResolvers) {
|
|
3572
4318
|
clearTimeout(resolver.timeout);
|
|
3573
4319
|
resolver.reject(new Error("Module destroyed"));
|
|
@@ -3592,8 +4338,9 @@ var PaymentsModule = class {
|
|
|
3592
4338
|
tokens: []
|
|
3593
4339
|
};
|
|
3594
4340
|
try {
|
|
3595
|
-
const
|
|
3596
|
-
const
|
|
4341
|
+
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
4342
|
+
const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
|
|
4343
|
+
const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
|
|
3597
4344
|
const signingService = await this.createSigningService();
|
|
3598
4345
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
3599
4346
|
if (!stClient) {
|
|
@@ -3613,7 +4360,6 @@ var PaymentsModule = class {
|
|
|
3613
4360
|
if (!splitPlan) {
|
|
3614
4361
|
throw new Error("Insufficient balance");
|
|
3615
4362
|
}
|
|
3616
|
-
this.log(`Split plan: requiresSplit=${splitPlan.requiresSplit}, directTokens=${splitPlan.tokensToTransferDirectly.length}`);
|
|
3617
4363
|
const tokensToSend = splitPlan.tokensToTransferDirectly.map((t) => t.uiToken);
|
|
3618
4364
|
if (splitPlan.tokenToSplit) {
|
|
3619
4365
|
tokensToSend.push(splitPlan.tokenToSplit.uiToken);
|
|
@@ -3656,11 +4402,13 @@ var PaymentsModule = class {
|
|
|
3656
4402
|
};
|
|
3657
4403
|
await this.addToken(changeToken, true);
|
|
3658
4404
|
this.log(`Change token saved: ${changeToken.id}, amount: ${changeToken.amount}`);
|
|
4405
|
+
console.log(`[Payments] Sending split token to ${recipientPubkey.slice(0, 8)}... via Nostr`);
|
|
3659
4406
|
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
3660
4407
|
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
3661
4408
|
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
3662
4409
|
memo: request.memo
|
|
3663
4410
|
});
|
|
4411
|
+
console.log(`[Payments] Split token sent successfully`);
|
|
3664
4412
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
3665
4413
|
result.txHash = "split-" + Date.now().toString(16);
|
|
3666
4414
|
this.log(`Split transfer completed`);
|
|
@@ -3679,11 +4427,13 @@ var PaymentsModule = class {
|
|
|
3679
4427
|
const transferTx = commitment.toTransaction(inclusionProof);
|
|
3680
4428
|
const requestIdBytes = commitment.requestId;
|
|
3681
4429
|
result.txHash = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4430
|
+
console.log(`[Payments] Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}... via Nostr`);
|
|
3682
4431
|
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
3683
4432
|
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
3684
4433
|
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
3685
4434
|
memo: request.memo
|
|
3686
4435
|
});
|
|
4436
|
+
console.log(`[Payments] Direct token sent successfully`);
|
|
3687
4437
|
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
3688
4438
|
await this.removeToken(token.id, recipientNametag);
|
|
3689
4439
|
}
|
|
@@ -3737,26 +4487,255 @@ var PaymentsModule = class {
|
|
|
3737
4487
|
return TokenRegistry.getInstance().getIconUrl(coinId) ?? void 0;
|
|
3738
4488
|
}
|
|
3739
4489
|
// ===========================================================================
|
|
3740
|
-
// Public API -
|
|
4490
|
+
// Public API - Instant Split (V5 Optimized)
|
|
3741
4491
|
// ===========================================================================
|
|
3742
4492
|
/**
|
|
3743
|
-
* Send
|
|
3744
|
-
*
|
|
3745
|
-
*
|
|
3746
|
-
*
|
|
4493
|
+
* Send tokens using INSTANT_SPLIT V5 optimized flow.
|
|
4494
|
+
*
|
|
4495
|
+
* This achieves ~2.3s critical path latency instead of ~42s by:
|
|
4496
|
+
* 1. Waiting only for burn proof (required)
|
|
4497
|
+
* 2. Creating transfer commitment from mint data (no mint proof needed)
|
|
4498
|
+
* 3. Sending bundle via Nostr immediately
|
|
4499
|
+
* 4. Processing mints in background
|
|
4500
|
+
*
|
|
4501
|
+
* @param request - Transfer request with recipient, amount, and coinId
|
|
4502
|
+
* @param options - Optional instant split configuration
|
|
4503
|
+
* @returns InstantSplitResult with timing info
|
|
3747
4504
|
*/
|
|
3748
|
-
async
|
|
4505
|
+
async sendInstant(request, options) {
|
|
3749
4506
|
this.ensureInitialized();
|
|
3750
|
-
|
|
3751
|
-
return {
|
|
3752
|
-
success: false,
|
|
3753
|
-
error: "Transport provider does not support payment requests"
|
|
3754
|
-
};
|
|
3755
|
-
}
|
|
4507
|
+
const startTime = performance.now();
|
|
3756
4508
|
try {
|
|
3757
|
-
const
|
|
3758
|
-
const
|
|
3759
|
-
|
|
4509
|
+
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
4510
|
+
const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
|
|
4511
|
+
const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
|
|
4512
|
+
const signingService = await this.createSigningService();
|
|
4513
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
4514
|
+
if (!stClient) {
|
|
4515
|
+
throw new Error("State transition client not available");
|
|
4516
|
+
}
|
|
4517
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
4518
|
+
if (!trustBase) {
|
|
4519
|
+
throw new Error("Trust base not available");
|
|
4520
|
+
}
|
|
4521
|
+
const calculator = new TokenSplitCalculator();
|
|
4522
|
+
const availableTokens = Array.from(this.tokens.values());
|
|
4523
|
+
const splitPlan = await calculator.calculateOptimalSplit(
|
|
4524
|
+
availableTokens,
|
|
4525
|
+
BigInt(request.amount),
|
|
4526
|
+
request.coinId
|
|
4527
|
+
);
|
|
4528
|
+
if (!splitPlan) {
|
|
4529
|
+
throw new Error("Insufficient balance");
|
|
4530
|
+
}
|
|
4531
|
+
if (!splitPlan.requiresSplit || !splitPlan.tokenToSplit) {
|
|
4532
|
+
this.log("No split required, falling back to standard send()");
|
|
4533
|
+
const result2 = await this.send(request);
|
|
4534
|
+
return {
|
|
4535
|
+
success: result2.status === "completed",
|
|
4536
|
+
criticalPathDurationMs: performance.now() - startTime,
|
|
4537
|
+
error: result2.error
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
this.log(`InstantSplit: amount=${splitPlan.splitAmount}, remainder=${splitPlan.remainderAmount}`);
|
|
4541
|
+
const tokenToSplit = splitPlan.tokenToSplit.uiToken;
|
|
4542
|
+
tokenToSplit.status = "transferring";
|
|
4543
|
+
this.tokens.set(tokenToSplit.id, tokenToSplit);
|
|
4544
|
+
const devMode = options?.devMode ?? this.deps.oracle.isDevMode?.() ?? false;
|
|
4545
|
+
const executor = new InstantSplitExecutor({
|
|
4546
|
+
stateTransitionClient: stClient,
|
|
4547
|
+
trustBase,
|
|
4548
|
+
signingService,
|
|
4549
|
+
devMode
|
|
4550
|
+
});
|
|
4551
|
+
const result = await executor.executeSplitInstant(
|
|
4552
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4553
|
+
splitPlan.splitAmount,
|
|
4554
|
+
splitPlan.remainderAmount,
|
|
4555
|
+
splitPlan.coinId,
|
|
4556
|
+
recipientAddress,
|
|
4557
|
+
this.deps.transport,
|
|
4558
|
+
recipientPubkey,
|
|
4559
|
+
{
|
|
4560
|
+
...options,
|
|
4561
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4562
|
+
const changeTokenData = changeToken.toJSON();
|
|
4563
|
+
const uiToken = {
|
|
4564
|
+
id: crypto.randomUUID(),
|
|
4565
|
+
coinId: request.coinId,
|
|
4566
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4567
|
+
name: this.getCoinName(request.coinId),
|
|
4568
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4569
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4570
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4571
|
+
status: "confirmed",
|
|
4572
|
+
createdAt: Date.now(),
|
|
4573
|
+
updatedAt: Date.now(),
|
|
4574
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4575
|
+
};
|
|
4576
|
+
await this.addToken(uiToken, true);
|
|
4577
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4578
|
+
},
|
|
4579
|
+
onStorageSync: async () => {
|
|
4580
|
+
await this.save();
|
|
4581
|
+
return true;
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
);
|
|
4585
|
+
if (result.success) {
|
|
4586
|
+
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4587
|
+
await this.removeToken(tokenToSplit.id, recipientNametag);
|
|
4588
|
+
await this.addToHistory({
|
|
4589
|
+
type: "SENT",
|
|
4590
|
+
amount: request.amount,
|
|
4591
|
+
coinId: request.coinId,
|
|
4592
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4593
|
+
timestamp: Date.now(),
|
|
4594
|
+
recipientNametag
|
|
4595
|
+
});
|
|
4596
|
+
await this.save();
|
|
4597
|
+
} else {
|
|
4598
|
+
tokenToSplit.status = "confirmed";
|
|
4599
|
+
this.tokens.set(tokenToSplit.id, tokenToSplit);
|
|
4600
|
+
}
|
|
4601
|
+
return result;
|
|
4602
|
+
} catch (error) {
|
|
4603
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4604
|
+
return {
|
|
4605
|
+
success: false,
|
|
4606
|
+
criticalPathDurationMs: performance.now() - startTime,
|
|
4607
|
+
error: errorMessage
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
/**
|
|
4612
|
+
* Process a received INSTANT_SPLIT bundle.
|
|
4613
|
+
*
|
|
4614
|
+
* This should be called when receiving an instant split bundle via transport.
|
|
4615
|
+
* It handles the recipient-side processing:
|
|
4616
|
+
* 1. Validate burn transaction
|
|
4617
|
+
* 2. Submit and wait for mint proof
|
|
4618
|
+
* 3. Submit and wait for transfer proof
|
|
4619
|
+
* 4. Finalize and save the token
|
|
4620
|
+
*
|
|
4621
|
+
* @param bundle - The received InstantSplitBundle (V4 or V5)
|
|
4622
|
+
* @param senderPubkey - Sender's public key for verification
|
|
4623
|
+
* @returns Processing result with finalized token
|
|
4624
|
+
*/
|
|
4625
|
+
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4626
|
+
this.ensureInitialized();
|
|
4627
|
+
try {
|
|
4628
|
+
const signingService = await this.createSigningService();
|
|
4629
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
4630
|
+
if (!stClient) {
|
|
4631
|
+
throw new Error("State transition client not available");
|
|
4632
|
+
}
|
|
4633
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
4634
|
+
if (!trustBase) {
|
|
4635
|
+
throw new Error("Trust base not available");
|
|
4636
|
+
}
|
|
4637
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4638
|
+
const processor = new InstantSplitProcessor({
|
|
4639
|
+
stateTransitionClient: stClient,
|
|
4640
|
+
trustBase,
|
|
4641
|
+
devMode
|
|
4642
|
+
});
|
|
4643
|
+
const result = await processor.processReceivedBundle(
|
|
4644
|
+
bundle,
|
|
4645
|
+
signingService,
|
|
4646
|
+
senderPubkey,
|
|
4647
|
+
{
|
|
4648
|
+
findNametagToken: async (proxyAddress) => {
|
|
4649
|
+
if (this.nametag?.token) {
|
|
4650
|
+
try {
|
|
4651
|
+
const nametagToken = await SdkToken2.fromJSON(this.nametag.token);
|
|
4652
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
4653
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
4654
|
+
if (proxy.address === proxyAddress) {
|
|
4655
|
+
return nametagToken;
|
|
4656
|
+
}
|
|
4657
|
+
this.log(`Nametag PROXY address mismatch: ${proxy.address} !== ${proxyAddress}`);
|
|
4658
|
+
return null;
|
|
4659
|
+
} catch (err) {
|
|
4660
|
+
this.log("Failed to parse nametag token:", err);
|
|
4661
|
+
return null;
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
return null;
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
);
|
|
4668
|
+
if (result.success && result.token) {
|
|
4669
|
+
const tokenData = result.token.toJSON();
|
|
4670
|
+
const info = await parseTokenInfo(tokenData);
|
|
4671
|
+
const uiToken = {
|
|
4672
|
+
id: crypto.randomUUID(),
|
|
4673
|
+
coinId: info.coinId,
|
|
4674
|
+
symbol: info.symbol,
|
|
4675
|
+
name: info.name,
|
|
4676
|
+
decimals: info.decimals,
|
|
4677
|
+
iconUrl: info.iconUrl,
|
|
4678
|
+
amount: bundle.amount,
|
|
4679
|
+
status: "confirmed",
|
|
4680
|
+
createdAt: Date.now(),
|
|
4681
|
+
updatedAt: Date.now(),
|
|
4682
|
+
sdkData: JSON.stringify(tokenData)
|
|
4683
|
+
};
|
|
4684
|
+
await this.addToken(uiToken);
|
|
4685
|
+
await this.addToHistory({
|
|
4686
|
+
type: "RECEIVED",
|
|
4687
|
+
amount: bundle.amount,
|
|
4688
|
+
coinId: info.coinId,
|
|
4689
|
+
symbol: info.symbol,
|
|
4690
|
+
timestamp: Date.now(),
|
|
4691
|
+
senderPubkey
|
|
4692
|
+
});
|
|
4693
|
+
await this.save();
|
|
4694
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
4695
|
+
id: bundle.splitGroupId,
|
|
4696
|
+
senderPubkey,
|
|
4697
|
+
tokens: [uiToken],
|
|
4698
|
+
receivedAt: Date.now()
|
|
4699
|
+
});
|
|
4700
|
+
}
|
|
4701
|
+
return result;
|
|
4702
|
+
} catch (error) {
|
|
4703
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4704
|
+
return {
|
|
4705
|
+
success: false,
|
|
4706
|
+
error: errorMessage,
|
|
4707
|
+
durationMs: 0
|
|
4708
|
+
};
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
/**
|
|
4712
|
+
* Check if a payload is an instant split bundle
|
|
4713
|
+
*/
|
|
4714
|
+
isInstantSplitBundle(payload) {
|
|
4715
|
+
return isInstantSplitBundle(payload);
|
|
4716
|
+
}
|
|
4717
|
+
// ===========================================================================
|
|
4718
|
+
// Public API - Payment Requests
|
|
4719
|
+
// ===========================================================================
|
|
4720
|
+
/**
|
|
4721
|
+
* Send a payment request to someone
|
|
4722
|
+
* @param recipientPubkeyOrNametag - Recipient's pubkey or @nametag
|
|
4723
|
+
* @param request - Payment request details
|
|
4724
|
+
* @returns Result with event ID
|
|
4725
|
+
*/
|
|
4726
|
+
async sendPaymentRequest(recipientPubkeyOrNametag, request) {
|
|
4727
|
+
this.ensureInitialized();
|
|
4728
|
+
if (!this.deps.transport.sendPaymentRequest) {
|
|
4729
|
+
return {
|
|
4730
|
+
success: false,
|
|
4731
|
+
error: "Transport provider does not support payment requests"
|
|
4732
|
+
};
|
|
4733
|
+
}
|
|
4734
|
+
try {
|
|
4735
|
+
const peerInfo = await this.deps.transport.resolve?.(recipientPubkeyOrNametag) ?? null;
|
|
4736
|
+
const recipientPubkey = this.resolveTransportPubkey(recipientPubkeyOrNametag, peerInfo);
|
|
4737
|
+
const payload = {
|
|
4738
|
+
amount: request.amount,
|
|
3760
4739
|
coinId: request.coinId,
|
|
3761
4740
|
message: request.message,
|
|
3762
4741
|
recipientNametag: request.recipientNametag,
|
|
@@ -4058,47 +5037,46 @@ var PaymentsModule = class {
|
|
|
4058
5037
|
// Public API - Balance & Tokens
|
|
4059
5038
|
// ===========================================================================
|
|
4060
5039
|
/**
|
|
4061
|
-
*
|
|
5040
|
+
* Set or update price provider
|
|
4062
5041
|
*/
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
});
|
|
5042
|
+
setPriceProvider(provider) {
|
|
5043
|
+
this.priceProvider = provider;
|
|
5044
|
+
}
|
|
5045
|
+
/**
|
|
5046
|
+
* Get total portfolio value in USD
|
|
5047
|
+
* Returns null if PriceProvider is not configured
|
|
5048
|
+
*/
|
|
5049
|
+
async getBalance() {
|
|
5050
|
+
const assets = await this.getAssets();
|
|
5051
|
+
if (!this.priceProvider) {
|
|
5052
|
+
return null;
|
|
5053
|
+
}
|
|
5054
|
+
let total = 0;
|
|
5055
|
+
let hasAnyPrice = false;
|
|
5056
|
+
for (const asset of assets) {
|
|
5057
|
+
if (asset.fiatValueUsd != null) {
|
|
5058
|
+
total += asset.fiatValueUsd;
|
|
5059
|
+
hasAnyPrice = true;
|
|
4082
5060
|
}
|
|
4083
5061
|
}
|
|
4084
|
-
return
|
|
5062
|
+
return hasAnyPrice ? total : null;
|
|
4085
5063
|
}
|
|
4086
5064
|
/**
|
|
4087
|
-
* Get aggregated assets (tokens grouped by coinId)
|
|
5065
|
+
* Get aggregated assets (tokens grouped by coinId) with price data
|
|
4088
5066
|
* Only includes confirmed tokens
|
|
4089
5067
|
*/
|
|
4090
|
-
getAssets(coinId) {
|
|
4091
|
-
const
|
|
5068
|
+
async getAssets(coinId) {
|
|
5069
|
+
const assetsMap = /* @__PURE__ */ new Map();
|
|
4092
5070
|
for (const token of this.tokens.values()) {
|
|
4093
5071
|
if (token.status !== "confirmed") continue;
|
|
4094
5072
|
if (coinId && token.coinId !== coinId) continue;
|
|
4095
5073
|
const key = token.coinId;
|
|
4096
|
-
const existing =
|
|
5074
|
+
const existing = assetsMap.get(key);
|
|
4097
5075
|
if (existing) {
|
|
4098
5076
|
existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
|
|
4099
5077
|
existing.tokenCount++;
|
|
4100
5078
|
} else {
|
|
4101
|
-
|
|
5079
|
+
assetsMap.set(key, {
|
|
4102
5080
|
coinId: token.coinId,
|
|
4103
5081
|
symbol: token.symbol,
|
|
4104
5082
|
name: token.name,
|
|
@@ -4109,7 +5087,66 @@ var PaymentsModule = class {
|
|
|
4109
5087
|
});
|
|
4110
5088
|
}
|
|
4111
5089
|
}
|
|
4112
|
-
|
|
5090
|
+
const rawAssets = Array.from(assetsMap.values());
|
|
5091
|
+
let priceMap = null;
|
|
5092
|
+
if (this.priceProvider && rawAssets.length > 0) {
|
|
5093
|
+
const registry = TokenRegistry.getInstance();
|
|
5094
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5095
|
+
for (const asset of rawAssets) {
|
|
5096
|
+
const def = registry.getDefinition(asset.coinId);
|
|
5097
|
+
if (def?.name) {
|
|
5098
|
+
const existing = nameToCoins.get(def.name);
|
|
5099
|
+
if (existing) {
|
|
5100
|
+
existing.push(asset.coinId);
|
|
5101
|
+
} else {
|
|
5102
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
if (nameToCoins.size > 0) {
|
|
5107
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
5108
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5109
|
+
priceMap = /* @__PURE__ */ new Map();
|
|
5110
|
+
for (const [name, coinIds] of nameToCoins) {
|
|
5111
|
+
const price = prices.get(name);
|
|
5112
|
+
if (price) {
|
|
5113
|
+
for (const cid of coinIds) {
|
|
5114
|
+
priceMap.set(cid, {
|
|
5115
|
+
priceUsd: price.priceUsd,
|
|
5116
|
+
priceEur: price.priceEur,
|
|
5117
|
+
change24h: price.change24h
|
|
5118
|
+
});
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
return rawAssets.map((raw) => {
|
|
5125
|
+
const price = priceMap?.get(raw.coinId);
|
|
5126
|
+
let fiatValueUsd = null;
|
|
5127
|
+
let fiatValueEur = null;
|
|
5128
|
+
if (price) {
|
|
5129
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5130
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5131
|
+
if (price.priceEur != null) {
|
|
5132
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
return {
|
|
5136
|
+
coinId: raw.coinId,
|
|
5137
|
+
symbol: raw.symbol,
|
|
5138
|
+
name: raw.name,
|
|
5139
|
+
decimals: raw.decimals,
|
|
5140
|
+
iconUrl: raw.iconUrl,
|
|
5141
|
+
totalAmount: raw.totalAmount,
|
|
5142
|
+
tokenCount: raw.tokenCount,
|
|
5143
|
+
priceUsd: price?.priceUsd ?? null,
|
|
5144
|
+
priceEur: price?.priceEur ?? null,
|
|
5145
|
+
change24h: price?.change24h ?? null,
|
|
5146
|
+
fiatValueUsd,
|
|
5147
|
+
fiatValueEur
|
|
5148
|
+
};
|
|
5149
|
+
});
|
|
4113
5150
|
}
|
|
4114
5151
|
/**
|
|
4115
5152
|
* Get all tokens
|
|
@@ -4135,14 +5172,52 @@ var PaymentsModule = class {
|
|
|
4135
5172
|
// ===========================================================================
|
|
4136
5173
|
/**
|
|
4137
5174
|
* Add a token
|
|
4138
|
-
*
|
|
5175
|
+
* Tokens are uniquely identified by (tokenId, stateHash) composite key.
|
|
5176
|
+
* Multiple historic states of the same token can coexist.
|
|
5177
|
+
* @returns false if exact duplicate (same tokenId AND same stateHash)
|
|
4139
5178
|
*/
|
|
4140
5179
|
async addToken(token, skipHistory = false) {
|
|
4141
5180
|
this.ensureInitialized();
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
5181
|
+
const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5182
|
+
const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
5183
|
+
const incomingStateKey = incomingTokenId && incomingStateHash ? createTokenStateKey(incomingTokenId, incomingStateHash) : null;
|
|
5184
|
+
if (incomingTokenId && incomingStateHash && this.isStateTombstoned(incomingTokenId, incomingStateHash)) {
|
|
5185
|
+
this.log(`Rejecting tombstoned token: ${incomingTokenId.slice(0, 8)}..._${incomingStateHash.slice(0, 8)}...`);
|
|
5186
|
+
return false;
|
|
5187
|
+
}
|
|
5188
|
+
if (incomingStateKey) {
|
|
5189
|
+
for (const [existingId, existing] of this.tokens) {
|
|
5190
|
+
if (isSameTokenState(existing, token)) {
|
|
5191
|
+
this.log(`Duplicate token state ignored: ${incomingTokenId?.slice(0, 8)}..._${incomingStateHash?.slice(0, 8)}...`);
|
|
5192
|
+
return false;
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
}
|
|
5196
|
+
for (const [existingId, existing] of this.tokens) {
|
|
5197
|
+
if (hasSameGenesisTokenId(existing, token)) {
|
|
5198
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
5199
|
+
if (incomingStateHash && existingStateHash && incomingStateHash === existingStateHash) {
|
|
5200
|
+
continue;
|
|
5201
|
+
}
|
|
5202
|
+
if (existing.status === "spent" || existing.status === "invalid") {
|
|
5203
|
+
this.log(`Replacing spent/invalid token ${incomingTokenId?.slice(0, 8)}...`);
|
|
5204
|
+
this.tokens.delete(existingId);
|
|
5205
|
+
break;
|
|
5206
|
+
}
|
|
5207
|
+
if (incomingStateHash && existingStateHash && incomingStateHash !== existingStateHash) {
|
|
5208
|
+
this.log(`Token ${incomingTokenId?.slice(0, 8)}... state updated: ${existingStateHash.slice(0, 8)}... -> ${incomingStateHash.slice(0, 8)}...`);
|
|
5209
|
+
await this.archiveToken(existing);
|
|
5210
|
+
this.tokens.delete(existingId);
|
|
5211
|
+
break;
|
|
5212
|
+
}
|
|
5213
|
+
if (!incomingStateHash || !existingStateHash) {
|
|
5214
|
+
if (existingId !== token.id) {
|
|
5215
|
+
this.log(`Token ${incomingTokenId?.slice(0, 8)}... .id changed, replacing`);
|
|
5216
|
+
await this.archiveToken(existing);
|
|
5217
|
+
this.tokens.delete(existingId);
|
|
5218
|
+
break;
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
4146
5221
|
}
|
|
4147
5222
|
}
|
|
4148
5223
|
this.tokens.set(token.id, token);
|
|
@@ -4297,8 +5372,10 @@ var PaymentsModule = class {
|
|
|
4297
5372
|
);
|
|
4298
5373
|
if (!alreadyTombstoned) {
|
|
4299
5374
|
this.tombstones.push(tombstone);
|
|
4300
|
-
this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}...`);
|
|
5375
|
+
this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}..._${tombstone.stateHash.slice(0, 8)}...`);
|
|
4301
5376
|
}
|
|
5377
|
+
} else {
|
|
5378
|
+
this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
|
|
4302
5379
|
}
|
|
4303
5380
|
this.tokens.delete(tokenId);
|
|
4304
5381
|
if (!skipHistory && token.coinId && token.amount) {
|
|
@@ -4616,15 +5693,15 @@ var PaymentsModule = class {
|
|
|
4616
5693
|
}
|
|
4617
5694
|
try {
|
|
4618
5695
|
const signingService = await this.createSigningService();
|
|
4619
|
-
const { UnmaskedPredicateReference:
|
|
4620
|
-
const { TokenType:
|
|
5696
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5697
|
+
const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
4621
5698
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
4622
|
-
const tokenType = new
|
|
4623
|
-
const addressRef = await
|
|
5699
|
+
const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5700
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
4624
5701
|
tokenType,
|
|
4625
5702
|
signingService.algorithm,
|
|
4626
5703
|
signingService.publicKey,
|
|
4627
|
-
|
|
5704
|
+
HashAlgorithm5.SHA256
|
|
4628
5705
|
);
|
|
4629
5706
|
const ownerAddress = await addressRef.toAddress();
|
|
4630
5707
|
const minter = new NametagMinter({
|
|
@@ -4791,40 +5868,22 @@ var PaymentsModule = class {
|
|
|
4791
5868
|
* Detect if a string is an L3 address (not a nametag)
|
|
4792
5869
|
* Returns true for: hex pubkeys (64+ chars), PROXY:, DIRECT: prefixed addresses
|
|
4793
5870
|
*/
|
|
4794
|
-
isL3Address(value) {
|
|
4795
|
-
if (value.startsWith("PROXY:") || value.startsWith("DIRECT:")) {
|
|
4796
|
-
return true;
|
|
4797
|
-
}
|
|
4798
|
-
if (value.length >= 64 && /^[0-9a-fA-F]+$/.test(value)) {
|
|
4799
|
-
return true;
|
|
4800
|
-
}
|
|
4801
|
-
return false;
|
|
4802
|
-
}
|
|
4803
5871
|
/**
|
|
4804
|
-
* Resolve recipient to
|
|
4805
|
-
*
|
|
5872
|
+
* Resolve recipient to transport pubkey for messaging.
|
|
5873
|
+
* Uses pre-resolved PeerInfo if available, otherwise resolves via transport.
|
|
4806
5874
|
*/
|
|
4807
|
-
|
|
4808
|
-
if (
|
|
4809
|
-
|
|
4810
|
-
const pubkey = await this.deps.transport.resolveNametag?.(nametag);
|
|
4811
|
-
if (!pubkey) {
|
|
4812
|
-
throw new Error(`Nametag not found: ${nametag}`);
|
|
4813
|
-
}
|
|
4814
|
-
return pubkey;
|
|
5875
|
+
resolveTransportPubkey(recipient, peerInfo) {
|
|
5876
|
+
if (peerInfo?.transportPubkey) {
|
|
5877
|
+
return peerInfo.transportPubkey;
|
|
4815
5878
|
}
|
|
4816
|
-
if (
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
if (this.deps?.transport.resolveNametag) {
|
|
4820
|
-
const pubkey = await this.deps.transport.resolveNametag(recipient);
|
|
4821
|
-
if (pubkey) {
|
|
4822
|
-
this.log(`Resolved "${recipient}" as nametag to pubkey`);
|
|
4823
|
-
return pubkey;
|
|
5879
|
+
if (recipient.length >= 64 && /^[0-9a-fA-F]+$/.test(recipient)) {
|
|
5880
|
+
if (recipient.length === 66 && (recipient.startsWith("02") || recipient.startsWith("03"))) {
|
|
5881
|
+
return recipient.slice(2);
|
|
4824
5882
|
}
|
|
5883
|
+
return recipient;
|
|
4825
5884
|
}
|
|
4826
5885
|
throw new Error(
|
|
4827
|
-
`
|
|
5886
|
+
`Cannot resolve transport pubkey for "${recipient}". No binding event found. The recipient must publish their identity first.`
|
|
4828
5887
|
);
|
|
4829
5888
|
}
|
|
4830
5889
|
/**
|
|
@@ -4834,7 +5893,7 @@ var PaymentsModule = class {
|
|
|
4834
5893
|
const tokenData = token.sdkData ? typeof token.sdkData === "string" ? JSON.parse(token.sdkData) : token.sdkData : token;
|
|
4835
5894
|
const sdkToken = await SdkToken2.fromJSON(tokenData);
|
|
4836
5895
|
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
4837
|
-
const commitment = await
|
|
5896
|
+
const commitment = await TransferCommitment4.create(
|
|
4838
5897
|
sdkToken,
|
|
4839
5898
|
recipientAddress,
|
|
4840
5899
|
salt,
|
|
@@ -4860,75 +5919,264 @@ var PaymentsModule = class {
|
|
|
4860
5919
|
* Create DirectAddress from a public key using UnmaskedPredicateReference
|
|
4861
5920
|
*/
|
|
4862
5921
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
4863
|
-
const { UnmaskedPredicateReference:
|
|
4864
|
-
const { TokenType:
|
|
5922
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5923
|
+
const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
4865
5924
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
4866
|
-
const tokenType = new
|
|
5925
|
+
const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
4867
5926
|
const pubkeyBytes = new Uint8Array(
|
|
4868
5927
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
4869
5928
|
);
|
|
4870
|
-
const addressRef = await
|
|
5929
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
4871
5930
|
tokenType,
|
|
4872
5931
|
"secp256k1",
|
|
4873
5932
|
pubkeyBytes,
|
|
4874
|
-
|
|
5933
|
+
HashAlgorithm5.SHA256
|
|
4875
5934
|
);
|
|
4876
5935
|
return addressRef.toAddress();
|
|
4877
5936
|
}
|
|
4878
5937
|
/**
|
|
4879
|
-
* Resolve
|
|
4880
|
-
*
|
|
5938
|
+
* Resolve recipient to IAddress for L3 transfers.
|
|
5939
|
+
* Uses pre-resolved PeerInfo when available to avoid redundant network queries.
|
|
4881
5940
|
*/
|
|
4882
|
-
async
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
5941
|
+
async resolveRecipientAddress(recipient, addressMode = "auto", peerInfo) {
|
|
5942
|
+
const { AddressFactory } = await import("@unicitylabs/state-transition-sdk/lib/address/AddressFactory");
|
|
5943
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5944
|
+
if (recipient.startsWith("PROXY:") || recipient.startsWith("DIRECT:")) {
|
|
5945
|
+
return AddressFactory.createAddress(recipient);
|
|
4886
5946
|
}
|
|
4887
|
-
|
|
5947
|
+
if (recipient.length === 66 && /^[0-9a-fA-F]+$/.test(recipient)) {
|
|
5948
|
+
this.log(`Creating DirectAddress from 33-byte compressed pubkey`);
|
|
5949
|
+
return this.createDirectAddressFromPubkey(recipient);
|
|
5950
|
+
}
|
|
5951
|
+
const info = peerInfo ?? await this.deps?.transport.resolve?.(recipient) ?? null;
|
|
4888
5952
|
if (!info) {
|
|
4889
|
-
|
|
4890
|
-
|
|
5953
|
+
throw new Error(
|
|
5954
|
+
`Recipient "${recipient}" not found. Use @nametag, a valid PROXY:/DIRECT: address, or a 33-byte hex pubkey.`
|
|
5955
|
+
);
|
|
4891
5956
|
}
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
5957
|
+
const nametag = recipient.startsWith("@") ? recipient.slice(1) : info.nametag || recipient;
|
|
5958
|
+
if (addressMode === "proxy") {
|
|
5959
|
+
console.log(`[Payments] Using PROXY address for "${nametag}" (forced)`);
|
|
5960
|
+
return ProxyAddress.fromNameTag(nametag);
|
|
4895
5961
|
}
|
|
4896
|
-
|
|
5962
|
+
if (addressMode === "direct") {
|
|
5963
|
+
if (!info.directAddress) {
|
|
5964
|
+
throw new Error(`"${nametag}" has no DirectAddress stored. It may be a legacy registration.`);
|
|
5965
|
+
}
|
|
5966
|
+
console.log(`[Payments] Using DirectAddress for "${nametag}" (forced): ${info.directAddress.slice(0, 30)}...`);
|
|
5967
|
+
return AddressFactory.createAddress(info.directAddress);
|
|
5968
|
+
}
|
|
5969
|
+
if (info.directAddress) {
|
|
5970
|
+
this.log(`Using DirectAddress for "${nametag}": ${info.directAddress.slice(0, 30)}...`);
|
|
5971
|
+
return AddressFactory.createAddress(info.directAddress);
|
|
5972
|
+
}
|
|
5973
|
+
this.log(`Using PROXY address for legacy nametag "${nametag}"`);
|
|
5974
|
+
return ProxyAddress.fromNameTag(nametag);
|
|
4897
5975
|
}
|
|
4898
5976
|
/**
|
|
4899
|
-
*
|
|
4900
|
-
*
|
|
5977
|
+
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
5978
|
+
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
5979
|
+
* We create the token as 'submitted', submit commitment (idempotent), and poll for proof.
|
|
4901
5980
|
*/
|
|
4902
|
-
async
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
const
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
return this.createDirectAddressFromPubkey(publicKey2);
|
|
5981
|
+
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
5982
|
+
try {
|
|
5983
|
+
const sourceTokenInput = typeof payload.sourceToken === "string" ? JSON.parse(payload.sourceToken) : payload.sourceToken;
|
|
5984
|
+
const commitmentInput = typeof payload.commitmentData === "string" ? JSON.parse(payload.commitmentData) : payload.commitmentData;
|
|
5985
|
+
if (!sourceTokenInput || !commitmentInput) {
|
|
5986
|
+
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
5987
|
+
return;
|
|
4910
5988
|
}
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
5989
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
5990
|
+
const token = {
|
|
5991
|
+
id: tokenInfo.tokenId ?? crypto.randomUUID(),
|
|
5992
|
+
coinId: tokenInfo.coinId,
|
|
5993
|
+
symbol: tokenInfo.symbol,
|
|
5994
|
+
name: tokenInfo.name,
|
|
5995
|
+
decimals: tokenInfo.decimals,
|
|
5996
|
+
iconUrl: tokenInfo.iconUrl,
|
|
5997
|
+
amount: tokenInfo.amount,
|
|
5998
|
+
status: "submitted",
|
|
5999
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
6000
|
+
createdAt: Date.now(),
|
|
6001
|
+
updatedAt: Date.now(),
|
|
6002
|
+
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
6003
|
+
};
|
|
6004
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6005
|
+
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
6006
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
6007
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
6008
|
+
return;
|
|
6009
|
+
}
|
|
6010
|
+
this.tokens.set(token.id, token);
|
|
6011
|
+
await this.save();
|
|
6012
|
+
this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
|
|
6013
|
+
const incomingTransfer = {
|
|
6014
|
+
id: transfer.id,
|
|
6015
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
6016
|
+
tokens: [token],
|
|
6017
|
+
memo: payload.memo,
|
|
6018
|
+
receivedAt: transfer.timestamp
|
|
6019
|
+
};
|
|
6020
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
6021
|
+
try {
|
|
6022
|
+
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
6023
|
+
const requestIdBytes = commitment.requestId;
|
|
6024
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
6025
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6026
|
+
if (stClient) {
|
|
6027
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
6028
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
6029
|
+
}
|
|
6030
|
+
this.addProofPollingJob({
|
|
6031
|
+
tokenId: token.id,
|
|
6032
|
+
requestIdHex,
|
|
6033
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
6034
|
+
startedAt: Date.now(),
|
|
6035
|
+
attemptCount: 0,
|
|
6036
|
+
lastAttemptAt: 0,
|
|
6037
|
+
onProofReceived: async (tokenId) => {
|
|
6038
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
|
|
6039
|
+
}
|
|
6040
|
+
});
|
|
6041
|
+
} catch (err) {
|
|
6042
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
6043
|
+
}
|
|
6044
|
+
} catch (error) {
|
|
6045
|
+
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
4919
6046
|
}
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
6047
|
+
}
|
|
6048
|
+
/**
|
|
6049
|
+
* Shared finalization logic for received transfers.
|
|
6050
|
+
* Handles both PROXY (with nametag token + address validation) and DIRECT schemes.
|
|
6051
|
+
*/
|
|
6052
|
+
async finalizeTransferToken(sourceToken, transferTx, stClient, trustBase) {
|
|
6053
|
+
const recipientAddress = transferTx.data.recipient;
|
|
6054
|
+
const addressScheme = recipientAddress.scheme;
|
|
6055
|
+
const signingService = await this.createSigningService();
|
|
6056
|
+
const transferSalt = transferTx.data.salt;
|
|
6057
|
+
const recipientPredicate = await UnmaskedPredicate5.create(
|
|
6058
|
+
sourceToken.id,
|
|
6059
|
+
sourceToken.type,
|
|
6060
|
+
signingService,
|
|
6061
|
+
HashAlgorithm5.SHA256,
|
|
6062
|
+
transferSalt
|
|
6063
|
+
);
|
|
6064
|
+
const recipientState = new TokenState5(recipientPredicate, null);
|
|
6065
|
+
let nametagTokens = [];
|
|
6066
|
+
if (addressScheme === AddressScheme.PROXY) {
|
|
6067
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
6068
|
+
if (!this.nametag?.token) {
|
|
6069
|
+
throw new Error("Cannot finalize PROXY transfer - no nametag token");
|
|
6070
|
+
}
|
|
6071
|
+
const nametagToken = await SdkToken2.fromJSON(this.nametag.token);
|
|
6072
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
6073
|
+
if (proxy.address !== recipientAddress.address) {
|
|
6074
|
+
throw new Error(
|
|
6075
|
+
`PROXY address mismatch: nametag resolves to ${proxy.address} but transfer targets ${recipientAddress.address}`
|
|
6076
|
+
);
|
|
6077
|
+
}
|
|
6078
|
+
nametagTokens = [nametagToken];
|
|
4924
6079
|
}
|
|
4925
|
-
|
|
4926
|
-
|
|
6080
|
+
return stClient.finalizeTransaction(
|
|
6081
|
+
trustBase,
|
|
6082
|
+
sourceToken,
|
|
6083
|
+
recipientState,
|
|
6084
|
+
transferTx,
|
|
6085
|
+
nametagTokens
|
|
4927
6086
|
);
|
|
4928
6087
|
}
|
|
6088
|
+
/**
|
|
6089
|
+
* Finalize a received token after proof is available
|
|
6090
|
+
*/
|
|
6091
|
+
async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
|
|
6092
|
+
try {
|
|
6093
|
+
const token = this.tokens.get(tokenId);
|
|
6094
|
+
if (!token) {
|
|
6095
|
+
this.log(`Token ${tokenId} not found for finalization`);
|
|
6096
|
+
return;
|
|
6097
|
+
}
|
|
6098
|
+
const commitment = await TransferCommitment4.fromJSON(commitmentInput);
|
|
6099
|
+
if (!this.deps.oracle.waitForProofSdk) {
|
|
6100
|
+
this.log("Cannot finalize - no waitForProofSdk");
|
|
6101
|
+
token.status = "confirmed";
|
|
6102
|
+
token.updatedAt = Date.now();
|
|
6103
|
+
await this.save();
|
|
6104
|
+
return;
|
|
6105
|
+
}
|
|
6106
|
+
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
6107
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
6108
|
+
const sourceToken = await SdkToken2.fromJSON(sourceTokenInput);
|
|
6109
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6110
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
6111
|
+
if (!stClient || !trustBase) {
|
|
6112
|
+
this.log("Cannot finalize - missing state transition client or trust base");
|
|
6113
|
+
token.status = "confirmed";
|
|
6114
|
+
token.updatedAt = Date.now();
|
|
6115
|
+
await this.save();
|
|
6116
|
+
return;
|
|
6117
|
+
}
|
|
6118
|
+
const finalizedSdkToken = await this.finalizeTransferToken(
|
|
6119
|
+
sourceToken,
|
|
6120
|
+
transferTx,
|
|
6121
|
+
stClient,
|
|
6122
|
+
trustBase
|
|
6123
|
+
);
|
|
6124
|
+
const finalizedToken = {
|
|
6125
|
+
...token,
|
|
6126
|
+
status: "confirmed",
|
|
6127
|
+
updatedAt: Date.now(),
|
|
6128
|
+
sdkData: JSON.stringify(finalizedSdkToken.toJSON())
|
|
6129
|
+
};
|
|
6130
|
+
this.tokens.set(tokenId, finalizedToken);
|
|
6131
|
+
await this.save();
|
|
6132
|
+
await this.saveTokenToFileStorage(finalizedToken);
|
|
6133
|
+
this.log(`NOSTR-FIRST: Token ${tokenId.slice(0, 8)}... finalized and confirmed`);
|
|
6134
|
+
this.deps.emitEvent("transfer:confirmed", {
|
|
6135
|
+
id: crypto.randomUUID(),
|
|
6136
|
+
status: "completed",
|
|
6137
|
+
tokens: [finalizedToken]
|
|
6138
|
+
});
|
|
6139
|
+
await this.addToHistory({
|
|
6140
|
+
type: "RECEIVED",
|
|
6141
|
+
amount: finalizedToken.amount,
|
|
6142
|
+
coinId: finalizedToken.coinId,
|
|
6143
|
+
symbol: finalizedToken.symbol,
|
|
6144
|
+
timestamp: Date.now(),
|
|
6145
|
+
senderPubkey
|
|
6146
|
+
});
|
|
6147
|
+
} catch (error) {
|
|
6148
|
+
console.error("[Payments] Failed to finalize received token:", error);
|
|
6149
|
+
const token = this.tokens.get(tokenId);
|
|
6150
|
+
if (token && token.status === "submitted") {
|
|
6151
|
+
token.status = "confirmed";
|
|
6152
|
+
token.updatedAt = Date.now();
|
|
6153
|
+
await this.save();
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
4929
6157
|
async handleIncomingTransfer(transfer) {
|
|
4930
6158
|
try {
|
|
4931
6159
|
const payload = transfer.payload;
|
|
6160
|
+
if (isInstantSplitBundle(payload)) {
|
|
6161
|
+
this.log("Processing INSTANT_SPLIT bundle...");
|
|
6162
|
+
try {
|
|
6163
|
+
if (!this.nametag) {
|
|
6164
|
+
await this.loadNametagFromFileStorage();
|
|
6165
|
+
}
|
|
6166
|
+
const result = await this.processInstantSplitBundle(
|
|
6167
|
+
payload,
|
|
6168
|
+
transfer.senderTransportPubkey
|
|
6169
|
+
);
|
|
6170
|
+
if (result.success) {
|
|
6171
|
+
this.log("INSTANT_SPLIT processed successfully");
|
|
6172
|
+
} else {
|
|
6173
|
+
console.warn("[Payments] INSTANT_SPLIT processing failed:", result.error);
|
|
6174
|
+
}
|
|
6175
|
+
} catch (err) {
|
|
6176
|
+
console.error("[Payments] INSTANT_SPLIT processing error:", err);
|
|
6177
|
+
}
|
|
6178
|
+
return;
|
|
6179
|
+
}
|
|
4932
6180
|
let tokenData;
|
|
4933
6181
|
let finalizedSdkToken = null;
|
|
4934
6182
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -4939,82 +6187,71 @@ var PaymentsModule = class {
|
|
|
4939
6187
|
console.warn("[Payments] Invalid Sphere wallet transfer format");
|
|
4940
6188
|
return;
|
|
4941
6189
|
}
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
);
|
|
4963
|
-
const recipientState = new TokenState3(recipientPredicate, null);
|
|
4964
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
4965
|
-
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
4966
|
-
if (!stClient || !trustBase) {
|
|
4967
|
-
console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
|
|
4968
|
-
return;
|
|
4969
|
-
}
|
|
4970
|
-
finalizedSdkToken = await stClient.finalizeTransaction(
|
|
4971
|
-
trustBase,
|
|
4972
|
-
sourceToken,
|
|
4973
|
-
recipientState,
|
|
4974
|
-
transferTx,
|
|
4975
|
-
[nametagToken]
|
|
4976
|
-
);
|
|
4977
|
-
tokenData = finalizedSdkToken.toJSON();
|
|
4978
|
-
this.log("Token finalized successfully");
|
|
4979
|
-
} catch (finalizeError) {
|
|
4980
|
-
console.error("[Payments] Finalization failed:", finalizeError);
|
|
6190
|
+
let sourceToken;
|
|
6191
|
+
let transferTx;
|
|
6192
|
+
try {
|
|
6193
|
+
sourceToken = await SdkToken2.fromJSON(sourceTokenInput);
|
|
6194
|
+
} catch (err) {
|
|
6195
|
+
console.error("[Payments] Failed to parse sourceToken:", err);
|
|
6196
|
+
return;
|
|
6197
|
+
}
|
|
6198
|
+
try {
|
|
6199
|
+
const hasInclusionProof = transferTxInput.inclusionProof !== void 0;
|
|
6200
|
+
const hasData = transferTxInput.data !== void 0;
|
|
6201
|
+
const hasTransactionData = transferTxInput.transactionData !== void 0;
|
|
6202
|
+
const hasAuthenticator = transferTxInput.authenticator !== void 0;
|
|
6203
|
+
if (hasData && hasInclusionProof) {
|
|
6204
|
+
transferTx = await TransferTransaction2.fromJSON(transferTxInput);
|
|
6205
|
+
} else if (hasTransactionData && hasAuthenticator) {
|
|
6206
|
+
const commitment = await TransferCommitment4.fromJSON(transferTxInput);
|
|
6207
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6208
|
+
if (!stClient) {
|
|
6209
|
+
console.error("[Payments] Cannot process commitment - no state transition client");
|
|
4981
6210
|
return;
|
|
4982
6211
|
}
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
recipientState,
|
|
5007
|
-
transferTx,
|
|
5008
|
-
[]
|
|
5009
|
-
// No nametag tokens needed for DIRECT
|
|
5010
|
-
);
|
|
5011
|
-
tokenData = finalizedSdkToken.toJSON();
|
|
5012
|
-
this.log("DIRECT transfer finalized successfully");
|
|
6212
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
6213
|
+
if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
|
|
6214
|
+
console.error("[Payments] Transfer commitment submission failed:", response.status);
|
|
6215
|
+
return;
|
|
6216
|
+
}
|
|
6217
|
+
if (!this.deps.oracle.waitForProofSdk) {
|
|
6218
|
+
console.error("[Payments] Cannot wait for proof - missing oracle method");
|
|
6219
|
+
return;
|
|
6220
|
+
}
|
|
6221
|
+
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
6222
|
+
transferTx = commitment.toTransaction(inclusionProof);
|
|
6223
|
+
} else {
|
|
6224
|
+
try {
|
|
6225
|
+
transferTx = await TransferTransaction2.fromJSON(transferTxInput);
|
|
6226
|
+
} catch {
|
|
6227
|
+
const commitment = await TransferCommitment4.fromJSON(transferTxInput);
|
|
6228
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6229
|
+
if (!stClient || !this.deps.oracle.waitForProofSdk) {
|
|
6230
|
+
throw new Error("Cannot submit commitment - missing oracle methods");
|
|
6231
|
+
}
|
|
6232
|
+
await stClient.submitTransferCommitment(commitment);
|
|
6233
|
+
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
6234
|
+
transferTx = commitment.toTransaction(inclusionProof);
|
|
5013
6235
|
}
|
|
5014
|
-
} catch (finalizeError) {
|
|
5015
|
-
this.log("DIRECT finalization failed, using source token:", finalizeError);
|
|
5016
|
-
tokenData = sourceTokenInput;
|
|
5017
6236
|
}
|
|
6237
|
+
} catch (err) {
|
|
6238
|
+
console.error("[Payments] Failed to parse transferTx:", err);
|
|
6239
|
+
return;
|
|
6240
|
+
}
|
|
6241
|
+
try {
|
|
6242
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6243
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
6244
|
+
if (!stClient || !trustBase) {
|
|
6245
|
+
console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
finalizedSdkToken = await this.finalizeTransferToken(sourceToken, transferTx, stClient, trustBase);
|
|
6249
|
+
tokenData = finalizedSdkToken.toJSON();
|
|
6250
|
+
const addressScheme = transferTx.data.recipient.scheme;
|
|
6251
|
+
this.log(`${addressScheme === AddressScheme.PROXY ? "PROXY" : "DIRECT"} finalization successful`);
|
|
6252
|
+
} catch (finalizeError) {
|
|
6253
|
+
console.error(`[Payments] Finalization FAILED - token rejected:`, finalizeError);
|
|
6254
|
+
return;
|
|
5018
6255
|
}
|
|
5019
6256
|
} else if (payload.token) {
|
|
5020
6257
|
tokenData = payload.token;
|
|
@@ -5041,12 +6278,6 @@ var PaymentsModule = class {
|
|
|
5041
6278
|
updatedAt: Date.now(),
|
|
5042
6279
|
sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
|
|
5043
6280
|
};
|
|
5044
|
-
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5045
|
-
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
5046
|
-
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
5047
|
-
this.log(`Rejected tombstoned token ${sdkTokenId.slice(0, 8)}...`);
|
|
5048
|
-
return;
|
|
5049
|
-
}
|
|
5050
6281
|
await this.addToken(token);
|
|
5051
6282
|
const incomingTransfer = {
|
|
5052
6283
|
id: transfer.id,
|
|
@@ -5134,14 +6365,159 @@ var PaymentsModule = class {
|
|
|
5134
6365
|
}
|
|
5135
6366
|
loadFromStorageData(data) {
|
|
5136
6367
|
const parsed = parseTxfStorageData(data);
|
|
6368
|
+
this.tombstones = parsed.tombstones;
|
|
5137
6369
|
this.tokens.clear();
|
|
5138
6370
|
for (const token of parsed.tokens) {
|
|
6371
|
+
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6372
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
6373
|
+
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
6374
|
+
this.log(`Skipping tombstoned token ${sdkTokenId.slice(0, 8)}... during load (exact state match)`);
|
|
6375
|
+
continue;
|
|
6376
|
+
}
|
|
5139
6377
|
this.tokens.set(token.id, token);
|
|
5140
6378
|
}
|
|
5141
|
-
this.tombstones = parsed.tombstones;
|
|
5142
6379
|
this.archivedTokens = parsed.archivedTokens;
|
|
5143
6380
|
this.forkedTokens = parsed.forkedTokens;
|
|
5144
|
-
|
|
6381
|
+
if (parsed.nametag !== null) {
|
|
6382
|
+
this.nametag = parsed.nametag;
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
6385
|
+
// ===========================================================================
|
|
6386
|
+
// Private: NOSTR-FIRST Proof Polling
|
|
6387
|
+
// ===========================================================================
|
|
6388
|
+
/**
|
|
6389
|
+
* Submit commitment to aggregator and start background proof polling
|
|
6390
|
+
* (NOSTR-FIRST pattern: fire-and-forget submission)
|
|
6391
|
+
*/
|
|
6392
|
+
async submitAndPollForProof(tokenId, commitment, requestIdHex, onProofReceived) {
|
|
6393
|
+
try {
|
|
6394
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6395
|
+
if (!stClient) {
|
|
6396
|
+
this.log("Cannot submit commitment - no state transition client");
|
|
6397
|
+
return;
|
|
6398
|
+
}
|
|
6399
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
6400
|
+
if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
|
|
6401
|
+
this.log(`Transfer commitment submission failed: ${response.status}`);
|
|
6402
|
+
const token = this.tokens.get(tokenId);
|
|
6403
|
+
if (token) {
|
|
6404
|
+
token.status = "invalid";
|
|
6405
|
+
token.updatedAt = Date.now();
|
|
6406
|
+
this.tokens.set(tokenId, token);
|
|
6407
|
+
await this.save();
|
|
6408
|
+
}
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
this.addProofPollingJob({
|
|
6412
|
+
tokenId,
|
|
6413
|
+
requestIdHex,
|
|
6414
|
+
commitmentJson: JSON.stringify(commitment.toJSON()),
|
|
6415
|
+
startedAt: Date.now(),
|
|
6416
|
+
attemptCount: 0,
|
|
6417
|
+
lastAttemptAt: 0,
|
|
6418
|
+
onProofReceived
|
|
6419
|
+
});
|
|
6420
|
+
} catch (error) {
|
|
6421
|
+
this.log("submitAndPollForProof error:", error);
|
|
6422
|
+
}
|
|
6423
|
+
}
|
|
6424
|
+
/**
|
|
6425
|
+
* Add a proof polling job to the queue
|
|
6426
|
+
*/
|
|
6427
|
+
addProofPollingJob(job) {
|
|
6428
|
+
this.proofPollingJobs.set(job.tokenId, job);
|
|
6429
|
+
this.log(`Added proof polling job for token ${job.tokenId.slice(0, 8)}...`);
|
|
6430
|
+
this.startProofPolling();
|
|
6431
|
+
}
|
|
6432
|
+
/**
|
|
6433
|
+
* Start the proof polling interval if not already running
|
|
6434
|
+
*/
|
|
6435
|
+
startProofPolling() {
|
|
6436
|
+
if (this.proofPollingInterval) return;
|
|
6437
|
+
if (this.proofPollingJobs.size === 0) return;
|
|
6438
|
+
this.log("Starting proof polling...");
|
|
6439
|
+
this.proofPollingInterval = setInterval(
|
|
6440
|
+
() => this.processProofPollingQueue(),
|
|
6441
|
+
_PaymentsModule.PROOF_POLLING_INTERVAL_MS
|
|
6442
|
+
);
|
|
6443
|
+
}
|
|
6444
|
+
/**
|
|
6445
|
+
* Stop the proof polling interval
|
|
6446
|
+
*/
|
|
6447
|
+
stopProofPolling() {
|
|
6448
|
+
if (this.proofPollingInterval) {
|
|
6449
|
+
clearInterval(this.proofPollingInterval);
|
|
6450
|
+
this.proofPollingInterval = null;
|
|
6451
|
+
this.log("Stopped proof polling");
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
/**
|
|
6455
|
+
* Process all pending proof polling jobs
|
|
6456
|
+
*/
|
|
6457
|
+
async processProofPollingQueue() {
|
|
6458
|
+
if (this.proofPollingJobs.size === 0) {
|
|
6459
|
+
this.stopProofPolling();
|
|
6460
|
+
return;
|
|
6461
|
+
}
|
|
6462
|
+
const completedJobs = [];
|
|
6463
|
+
for (const [tokenId, job] of this.proofPollingJobs) {
|
|
6464
|
+
try {
|
|
6465
|
+
job.attemptCount++;
|
|
6466
|
+
job.lastAttemptAt = Date.now();
|
|
6467
|
+
if (job.attemptCount >= _PaymentsModule.PROOF_POLLING_MAX_ATTEMPTS) {
|
|
6468
|
+
this.log(`Proof polling timeout for token ${tokenId.slice(0, 8)}...`);
|
|
6469
|
+
const token2 = this.tokens.get(tokenId);
|
|
6470
|
+
if (token2 && token2.status === "submitted") {
|
|
6471
|
+
token2.status = "invalid";
|
|
6472
|
+
token2.updatedAt = Date.now();
|
|
6473
|
+
this.tokens.set(tokenId, token2);
|
|
6474
|
+
}
|
|
6475
|
+
completedJobs.push(tokenId);
|
|
6476
|
+
continue;
|
|
6477
|
+
}
|
|
6478
|
+
const commitment = await TransferCommitment4.fromJSON(JSON.parse(job.commitmentJson));
|
|
6479
|
+
let inclusionProof = null;
|
|
6480
|
+
try {
|
|
6481
|
+
const abortController = new AbortController();
|
|
6482
|
+
const timeoutId = setTimeout(() => abortController.abort(), 500);
|
|
6483
|
+
if (this.deps.oracle.waitForProofSdk) {
|
|
6484
|
+
inclusionProof = await Promise.race([
|
|
6485
|
+
this.deps.oracle.waitForProofSdk(commitment, abortController.signal),
|
|
6486
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 500))
|
|
6487
|
+
]);
|
|
6488
|
+
} else {
|
|
6489
|
+
const proof = await this.deps.oracle.getProof(job.requestIdHex);
|
|
6490
|
+
if (proof) {
|
|
6491
|
+
inclusionProof = proof;
|
|
6492
|
+
}
|
|
6493
|
+
}
|
|
6494
|
+
clearTimeout(timeoutId);
|
|
6495
|
+
} catch (err) {
|
|
6496
|
+
continue;
|
|
6497
|
+
}
|
|
6498
|
+
if (!inclusionProof) {
|
|
6499
|
+
continue;
|
|
6500
|
+
}
|
|
6501
|
+
const token = this.tokens.get(tokenId);
|
|
6502
|
+
if (token) {
|
|
6503
|
+
token.status = "spent";
|
|
6504
|
+
token.updatedAt = Date.now();
|
|
6505
|
+
this.tokens.set(tokenId, token);
|
|
6506
|
+
await this.save();
|
|
6507
|
+
this.log(`Proof received for token ${tokenId.slice(0, 8)}..., status: spent`);
|
|
6508
|
+
}
|
|
6509
|
+
job.onProofReceived?.(tokenId);
|
|
6510
|
+
completedJobs.push(tokenId);
|
|
6511
|
+
} catch (error) {
|
|
6512
|
+
this.log(`Proof polling attempt ${job.attemptCount} for ${tokenId.slice(0, 8)}...: ${error}`);
|
|
6513
|
+
}
|
|
6514
|
+
}
|
|
6515
|
+
for (const tokenId of completedJobs) {
|
|
6516
|
+
this.proofPollingJobs.delete(tokenId);
|
|
6517
|
+
}
|
|
6518
|
+
if (this.proofPollingJobs.size === 0) {
|
|
6519
|
+
this.stopProofPolling();
|
|
6520
|
+
}
|
|
5145
6521
|
}
|
|
5146
6522
|
// ===========================================================================
|
|
5147
6523
|
// Private: Helpers
|
|
@@ -5156,6 +6532,14 @@ function createPaymentsModule(config) {
|
|
|
5156
6532
|
return new PaymentsModule(config);
|
|
5157
6533
|
}
|
|
5158
6534
|
|
|
6535
|
+
// modules/payments/TokenRecoveryService.ts
|
|
6536
|
+
import { TokenId as TokenId4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenId";
|
|
6537
|
+
import { TokenState as TokenState6 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
6538
|
+
import { TokenType as TokenType3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
6539
|
+
import { CoinId as CoinId5 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
|
|
6540
|
+
import { HashAlgorithm as HashAlgorithm6 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
6541
|
+
import { UnmaskedPredicate as UnmaskedPredicate6 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
6542
|
+
|
|
5159
6543
|
// modules/communications/CommunicationsModule.ts
|
|
5160
6544
|
var CommunicationsModule = class {
|
|
5161
6545
|
config;
|
|
@@ -6124,20 +7508,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
6124
7508
|
|
|
6125
7509
|
// core/Sphere.ts
|
|
6126
7510
|
import { SigningService as SigningService2 } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
|
|
6127
|
-
import { TokenType as
|
|
6128
|
-
import { HashAlgorithm as
|
|
6129
|
-
import { UnmaskedPredicateReference as
|
|
7511
|
+
import { TokenType as TokenType4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
7512
|
+
import { HashAlgorithm as HashAlgorithm7 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
7513
|
+
import { UnmaskedPredicateReference as UnmaskedPredicateReference3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
|
|
6130
7514
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
6131
7515
|
async function deriveL3PredicateAddress(privateKey) {
|
|
6132
7516
|
const secret = Buffer.from(privateKey, "hex");
|
|
6133
7517
|
const signingService = await SigningService2.createFromSecret(secret);
|
|
6134
7518
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
6135
|
-
const tokenType = new
|
|
6136
|
-
const predicateRef =
|
|
7519
|
+
const tokenType = new TokenType4(tokenTypeBytes);
|
|
7520
|
+
const predicateRef = UnmaskedPredicateReference3.create(
|
|
6137
7521
|
tokenType,
|
|
6138
7522
|
signingService.algorithm,
|
|
6139
7523
|
signingService.publicKey,
|
|
6140
|
-
|
|
7524
|
+
HashAlgorithm7.SHA256
|
|
6141
7525
|
);
|
|
6142
7526
|
return (await (await predicateRef).toAddress()).toString();
|
|
6143
7527
|
}
|
|
@@ -6153,7 +7537,11 @@ var Sphere = class _Sphere {
|
|
|
6153
7537
|
_derivationMode = "bip32";
|
|
6154
7538
|
_basePath = DEFAULT_BASE_PATH;
|
|
6155
7539
|
_currentAddressIndex = 0;
|
|
6156
|
-
/**
|
|
7540
|
+
/** Registry of all tracked (activated) addresses, keyed by HD index */
|
|
7541
|
+
_trackedAddresses = /* @__PURE__ */ new Map();
|
|
7542
|
+
/** Reverse lookup: addressId -> HD index */
|
|
7543
|
+
_addressIdToIndex = /* @__PURE__ */ new Map();
|
|
7544
|
+
/** Nametag cache: addressId -> (nametagIndex -> nametag). Separate from tracked addresses. */
|
|
6157
7545
|
_addressNametags = /* @__PURE__ */ new Map();
|
|
6158
7546
|
/** Cached PROXY address (computed once when nametag is set) */
|
|
6159
7547
|
_cachedProxyAddress = void 0;
|
|
@@ -6162,6 +7550,7 @@ var Sphere = class _Sphere {
|
|
|
6162
7550
|
_tokenStorageProviders = /* @__PURE__ */ new Map();
|
|
6163
7551
|
_transport;
|
|
6164
7552
|
_oracle;
|
|
7553
|
+
_priceProvider;
|
|
6165
7554
|
// Modules
|
|
6166
7555
|
_payments;
|
|
6167
7556
|
_communications;
|
|
@@ -6170,10 +7559,11 @@ var Sphere = class _Sphere {
|
|
|
6170
7559
|
// ===========================================================================
|
|
6171
7560
|
// Constructor (private)
|
|
6172
7561
|
// ===========================================================================
|
|
6173
|
-
constructor(storage, transport, oracle, tokenStorage, l1Config) {
|
|
7562
|
+
constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider) {
|
|
6174
7563
|
this._storage = storage;
|
|
6175
7564
|
this._transport = transport;
|
|
6176
7565
|
this._oracle = oracle;
|
|
7566
|
+
this._priceProvider = priceProvider ?? null;
|
|
6177
7567
|
if (tokenStorage) {
|
|
6178
7568
|
this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
|
|
6179
7569
|
}
|
|
@@ -6233,7 +7623,8 @@ var Sphere = class _Sphere {
|
|
|
6233
7623
|
transport: options.transport,
|
|
6234
7624
|
oracle: options.oracle,
|
|
6235
7625
|
tokenStorage: options.tokenStorage,
|
|
6236
|
-
l1: options.l1
|
|
7626
|
+
l1: options.l1,
|
|
7627
|
+
price: options.price
|
|
6237
7628
|
});
|
|
6238
7629
|
return { sphere: sphere2, created: false };
|
|
6239
7630
|
}
|
|
@@ -6257,7 +7648,8 @@ var Sphere = class _Sphere {
|
|
|
6257
7648
|
tokenStorage: options.tokenStorage,
|
|
6258
7649
|
derivationPath: options.derivationPath,
|
|
6259
7650
|
nametag: options.nametag,
|
|
6260
|
-
l1: options.l1
|
|
7651
|
+
l1: options.l1,
|
|
7652
|
+
price: options.price
|
|
6261
7653
|
});
|
|
6262
7654
|
return { sphere, created: true, generatedMnemonic };
|
|
6263
7655
|
}
|
|
@@ -6276,7 +7668,8 @@ var Sphere = class _Sphere {
|
|
|
6276
7668
|
options.transport,
|
|
6277
7669
|
options.oracle,
|
|
6278
7670
|
options.tokenStorage,
|
|
6279
|
-
options.l1
|
|
7671
|
+
options.l1,
|
|
7672
|
+
options.price
|
|
6280
7673
|
);
|
|
6281
7674
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
|
|
6282
7675
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
@@ -6285,10 +7678,12 @@ var Sphere = class _Sphere {
|
|
|
6285
7678
|
await sphere.finalizeWalletCreation();
|
|
6286
7679
|
sphere._initialized = true;
|
|
6287
7680
|
_Sphere.instance = sphere;
|
|
7681
|
+
await sphere.ensureAddressTracked(0);
|
|
6288
7682
|
if (options.nametag) {
|
|
6289
7683
|
await sphere.registerNametag(options.nametag);
|
|
6290
7684
|
} else {
|
|
6291
|
-
await sphere.
|
|
7685
|
+
await sphere.syncIdentityWithTransport();
|
|
7686
|
+
await sphere.recoverNametagFromTransport();
|
|
6292
7687
|
}
|
|
6293
7688
|
return sphere;
|
|
6294
7689
|
}
|
|
@@ -6304,14 +7699,28 @@ var Sphere = class _Sphere {
|
|
|
6304
7699
|
options.transport,
|
|
6305
7700
|
options.oracle,
|
|
6306
7701
|
options.tokenStorage,
|
|
6307
|
-
options.l1
|
|
7702
|
+
options.l1,
|
|
7703
|
+
options.price
|
|
6308
7704
|
);
|
|
6309
7705
|
await sphere.loadIdentityFromStorage();
|
|
6310
7706
|
await sphere.initializeProviders();
|
|
6311
7707
|
await sphere.initializeModules();
|
|
6312
|
-
await sphere.
|
|
7708
|
+
await sphere.syncIdentityWithTransport();
|
|
6313
7709
|
sphere._initialized = true;
|
|
6314
7710
|
_Sphere.instance = sphere;
|
|
7711
|
+
if (sphere._identity?.nametag && !sphere._payments.hasNametag()) {
|
|
7712
|
+
console.log(`[Sphere] Nametag @${sphere._identity.nametag} has no token, attempting to mint...`);
|
|
7713
|
+
try {
|
|
7714
|
+
const result = await sphere.mintNametag(sphere._identity.nametag);
|
|
7715
|
+
if (result.success) {
|
|
7716
|
+
console.log(`[Sphere] Nametag token minted successfully on load`);
|
|
7717
|
+
} else {
|
|
7718
|
+
console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
|
|
7719
|
+
}
|
|
7720
|
+
} catch (err) {
|
|
7721
|
+
console.warn(`[Sphere] Nametag token mint failed:`, err);
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
6315
7724
|
return sphere;
|
|
6316
7725
|
}
|
|
6317
7726
|
/**
|
|
@@ -6327,7 +7736,8 @@ var Sphere = class _Sphere {
|
|
|
6327
7736
|
options.transport,
|
|
6328
7737
|
options.oracle,
|
|
6329
7738
|
options.tokenStorage,
|
|
6330
|
-
options.l1
|
|
7739
|
+
options.l1,
|
|
7740
|
+
options.price
|
|
6331
7741
|
);
|
|
6332
7742
|
if (options.mnemonic) {
|
|
6333
7743
|
if (!_Sphere.validateMnemonic(options.mnemonic)) {
|
|
@@ -6352,11 +7762,12 @@ var Sphere = class _Sphere {
|
|
|
6352
7762
|
await sphere.initializeProviders();
|
|
6353
7763
|
await sphere.initializeModules();
|
|
6354
7764
|
if (!options.nametag) {
|
|
6355
|
-
await sphere.
|
|
7765
|
+
await sphere.recoverNametagFromTransport();
|
|
6356
7766
|
}
|
|
6357
7767
|
await sphere.finalizeWalletCreation();
|
|
6358
7768
|
sphere._initialized = true;
|
|
6359
7769
|
_Sphere.instance = sphere;
|
|
7770
|
+
await sphere.ensureAddressTracked(0);
|
|
6360
7771
|
if (options.nametag) {
|
|
6361
7772
|
await sphere.registerNametag(options.nametag);
|
|
6362
7773
|
}
|
|
@@ -6392,6 +7803,7 @@ var Sphere = class _Sphere {
|
|
|
6392
7803
|
await storage.remove(STORAGE_KEYS_GLOBAL.DERIVATION_MODE);
|
|
6393
7804
|
await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_SOURCE);
|
|
6394
7805
|
await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_EXISTS);
|
|
7806
|
+
await storage.remove(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
|
|
6395
7807
|
await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
6396
7808
|
await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
6397
7809
|
await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
|
|
@@ -6516,6 +7928,13 @@ var Sphere = class _Sphere {
|
|
|
6516
7928
|
hasTokenStorageProvider(providerId) {
|
|
6517
7929
|
return this._tokenStorageProviders.has(providerId);
|
|
6518
7930
|
}
|
|
7931
|
+
/**
|
|
7932
|
+
* Set or update the price provider after initialization
|
|
7933
|
+
*/
|
|
7934
|
+
setPriceProvider(provider) {
|
|
7935
|
+
this._priceProvider = provider;
|
|
7936
|
+
this._payments.setPriceProvider(provider);
|
|
7937
|
+
}
|
|
6519
7938
|
getTransport() {
|
|
6520
7939
|
return this._transport;
|
|
6521
7940
|
}
|
|
@@ -7003,10 +8422,9 @@ var Sphere = class _Sphere {
|
|
|
7003
8422
|
* @returns Primary nametag (index 0) or undefined if not registered
|
|
7004
8423
|
*/
|
|
7005
8424
|
getNametagForAddress(addressId) {
|
|
7006
|
-
const id = addressId ?? this.
|
|
8425
|
+
const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
|
|
7007
8426
|
if (!id) return void 0;
|
|
7008
|
-
|
|
7009
|
-
return nametagsMap?.get(0);
|
|
8427
|
+
return this._addressNametags.get(id)?.get(0);
|
|
7010
8428
|
}
|
|
7011
8429
|
/**
|
|
7012
8430
|
* Get all nametags for a specific address
|
|
@@ -7015,29 +8433,89 @@ var Sphere = class _Sphere {
|
|
|
7015
8433
|
* @returns Map of nametagIndex to nametag, or undefined if no nametags
|
|
7016
8434
|
*/
|
|
7017
8435
|
getNametagsForAddress(addressId) {
|
|
7018
|
-
const id = addressId ?? this.
|
|
8436
|
+
const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
|
|
7019
8437
|
if (!id) return void 0;
|
|
7020
|
-
const
|
|
7021
|
-
return
|
|
8438
|
+
const nametags = this._addressNametags.get(id);
|
|
8439
|
+
return nametags && nametags.size > 0 ? new Map(nametags) : void 0;
|
|
7022
8440
|
}
|
|
7023
8441
|
/**
|
|
7024
8442
|
* Get all registered address nametags
|
|
7025
|
-
*
|
|
8443
|
+
* @deprecated Use getActiveAddresses() or getAllTrackedAddresses() instead
|
|
7026
8444
|
* @returns Map of addressId to (nametagIndex -> nametag)
|
|
7027
8445
|
*/
|
|
7028
8446
|
getAllAddressNametags() {
|
|
7029
8447
|
const result = /* @__PURE__ */ new Map();
|
|
7030
|
-
this._addressNametags.
|
|
7031
|
-
|
|
7032
|
-
|
|
8448
|
+
for (const [addressId, nametags] of this._addressNametags.entries()) {
|
|
8449
|
+
if (nametags.size > 0) {
|
|
8450
|
+
result.set(addressId, new Map(nametags));
|
|
8451
|
+
}
|
|
8452
|
+
}
|
|
7033
8453
|
return result;
|
|
7034
8454
|
}
|
|
7035
8455
|
/**
|
|
7036
|
-
* Get
|
|
8456
|
+
* Get all active (non-hidden) tracked addresses.
|
|
8457
|
+
* Returns addresses that have been activated through create, switchToAddress,
|
|
8458
|
+
* registerNametag, or nametag recovery.
|
|
8459
|
+
*
|
|
8460
|
+
* @returns Array of TrackedAddress entries sorted by index, excluding hidden ones
|
|
8461
|
+
*/
|
|
8462
|
+
getActiveAddresses() {
|
|
8463
|
+
this.ensureReady();
|
|
8464
|
+
const result = [];
|
|
8465
|
+
for (const entry of this._trackedAddresses.values()) {
|
|
8466
|
+
if (!entry.hidden) {
|
|
8467
|
+
const nametag = this._addressNametags.get(entry.addressId)?.get(0);
|
|
8468
|
+
result.push({ ...entry, nametag });
|
|
8469
|
+
}
|
|
8470
|
+
}
|
|
8471
|
+
return result.sort((a, b) => a.index - b.index);
|
|
8472
|
+
}
|
|
8473
|
+
/**
|
|
8474
|
+
* Get all tracked addresses, including hidden ones.
|
|
8475
|
+
*
|
|
8476
|
+
* @returns Array of all TrackedAddress entries sorted by index
|
|
8477
|
+
*/
|
|
8478
|
+
getAllTrackedAddresses() {
|
|
8479
|
+
this.ensureReady();
|
|
8480
|
+
const result = [];
|
|
8481
|
+
for (const entry of this._trackedAddresses.values()) {
|
|
8482
|
+
const nametag = this._addressNametags.get(entry.addressId)?.get(0);
|
|
8483
|
+
result.push({ ...entry, nametag });
|
|
8484
|
+
}
|
|
8485
|
+
return result.sort((a, b) => a.index - b.index);
|
|
8486
|
+
}
|
|
8487
|
+
/**
|
|
8488
|
+
* Get tracked address info by index.
|
|
8489
|
+
*
|
|
8490
|
+
* @param index - Address index
|
|
8491
|
+
* @returns TrackedAddress or undefined if not tracked
|
|
8492
|
+
*/
|
|
8493
|
+
getTrackedAddress(index) {
|
|
8494
|
+
this.ensureReady();
|
|
8495
|
+
const entry = this._trackedAddresses.get(index);
|
|
8496
|
+
if (!entry) return void 0;
|
|
8497
|
+
const nametag = this._addressNametags.get(entry.addressId)?.get(0);
|
|
8498
|
+
return { ...entry, nametag };
|
|
8499
|
+
}
|
|
8500
|
+
/**
|
|
8501
|
+
* Set visibility of a tracked address.
|
|
8502
|
+
* Hidden addresses are not returned by getActiveAddresses() but remain tracked.
|
|
8503
|
+
*
|
|
8504
|
+
* @param index - Address index to hide/unhide
|
|
8505
|
+
* @param hidden - true to hide, false to show
|
|
8506
|
+
* @throws Error if address index is not tracked
|
|
7037
8507
|
*/
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
8508
|
+
async setAddressHidden(index, hidden) {
|
|
8509
|
+
this.ensureReady();
|
|
8510
|
+
const entry = this._trackedAddresses.get(index);
|
|
8511
|
+
if (!entry) {
|
|
8512
|
+
throw new Error(`Address at index ${index} is not tracked. Switch to it first.`);
|
|
8513
|
+
}
|
|
8514
|
+
if (entry.hidden === hidden) return;
|
|
8515
|
+
entry.hidden = hidden;
|
|
8516
|
+
await this.persistTrackedAddresses();
|
|
8517
|
+
const eventType = hidden ? "address:hidden" : "address:unhidden";
|
|
8518
|
+
this.emitEvent(eventType, { index, addressId: entry.addressId });
|
|
7041
8519
|
}
|
|
7042
8520
|
/**
|
|
7043
8521
|
* Switch to a different address by index
|
|
@@ -7058,7 +8536,7 @@ var Sphere = class _Sphere {
|
|
|
7058
8536
|
* await sphere.switchToAddress(0);
|
|
7059
8537
|
* ```
|
|
7060
8538
|
*/
|
|
7061
|
-
async switchToAddress(index) {
|
|
8539
|
+
async switchToAddress(index, options) {
|
|
7062
8540
|
this.ensureReady();
|
|
7063
8541
|
if (!this._masterKey) {
|
|
7064
8542
|
throw new Error("HD derivation requires master key with chain code. Cannot switch addresses.");
|
|
@@ -7066,12 +8544,28 @@ var Sphere = class _Sphere {
|
|
|
7066
8544
|
if (index < 0) {
|
|
7067
8545
|
throw new Error("Address index must be non-negative");
|
|
7068
8546
|
}
|
|
8547
|
+
const newNametag = options?.nametag?.startsWith("@") ? options.nametag.slice(1) : options?.nametag;
|
|
8548
|
+
if (newNametag && !this.validateNametag(newNametag)) {
|
|
8549
|
+
throw new Error("Invalid nametag format. Use alphanumeric characters, 3-20 chars.");
|
|
8550
|
+
}
|
|
7069
8551
|
const addressInfo = this.deriveAddress(index, false);
|
|
7070
8552
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
7071
8553
|
const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
|
|
8554
|
+
await this.ensureAddressTracked(index);
|
|
7072
8555
|
const addressId = getAddressId(predicateAddress);
|
|
7073
|
-
|
|
7074
|
-
|
|
8556
|
+
if (newNametag) {
|
|
8557
|
+
const existing = await this._transport.resolveNametag?.(newNametag);
|
|
8558
|
+
if (existing) {
|
|
8559
|
+
throw new Error(`Nametag @${newNametag} is already taken`);
|
|
8560
|
+
}
|
|
8561
|
+
let nametags = this._addressNametags.get(addressId);
|
|
8562
|
+
if (!nametags) {
|
|
8563
|
+
nametags = /* @__PURE__ */ new Map();
|
|
8564
|
+
this._addressNametags.set(addressId, nametags);
|
|
8565
|
+
}
|
|
8566
|
+
nametags.set(0, newNametag);
|
|
8567
|
+
}
|
|
8568
|
+
const nametag = this._addressNametags.get(addressId)?.get(0);
|
|
7075
8569
|
this._identity = {
|
|
7076
8570
|
privateKey: addressInfo.privateKey,
|
|
7077
8571
|
chainPubkey: addressInfo.publicKey,
|
|
@@ -7084,11 +8578,47 @@ var Sphere = class _Sphere {
|
|
|
7084
8578
|
await this._updateCachedProxyAddress();
|
|
7085
8579
|
await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
|
|
7086
8580
|
this._storage.setIdentity(this._identity);
|
|
7087
|
-
this._transport.setIdentity(this._identity);
|
|
8581
|
+
await this._transport.setIdentity(this._identity);
|
|
7088
8582
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
7089
8583
|
provider.setIdentity(this._identity);
|
|
8584
|
+
await provider.initialize();
|
|
7090
8585
|
}
|
|
7091
8586
|
await this.reinitializeModulesForNewAddress();
|
|
8587
|
+
if (this._identity.nametag) {
|
|
8588
|
+
await this.syncIdentityWithTransport();
|
|
8589
|
+
}
|
|
8590
|
+
if (newNametag) {
|
|
8591
|
+
await this.persistAddressNametags();
|
|
8592
|
+
if (!this._payments.hasNametag()) {
|
|
8593
|
+
console.log(`[Sphere] Minting nametag token for @${newNametag}...`);
|
|
8594
|
+
try {
|
|
8595
|
+
const result = await this.mintNametag(newNametag);
|
|
8596
|
+
if (result.success) {
|
|
8597
|
+
console.log(`[Sphere] Nametag token minted successfully`);
|
|
8598
|
+
} else {
|
|
8599
|
+
console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
|
|
8600
|
+
}
|
|
8601
|
+
} catch (err) {
|
|
8602
|
+
console.warn(`[Sphere] Nametag token mint failed:`, err);
|
|
8603
|
+
}
|
|
8604
|
+
}
|
|
8605
|
+
this.emitEvent("nametag:registered", {
|
|
8606
|
+
nametag: newNametag,
|
|
8607
|
+
addressIndex: index
|
|
8608
|
+
});
|
|
8609
|
+
} else if (this._identity.nametag && !this._payments.hasNametag()) {
|
|
8610
|
+
console.log(`[Sphere] Nametag @${this._identity.nametag} has no token after switch, minting...`);
|
|
8611
|
+
try {
|
|
8612
|
+
const result = await this.mintNametag(this._identity.nametag);
|
|
8613
|
+
if (result.success) {
|
|
8614
|
+
console.log(`[Sphere] Nametag token minted successfully after switch`);
|
|
8615
|
+
} else {
|
|
8616
|
+
console.warn(`[Sphere] Could not mint nametag token after switch: ${result.error}`);
|
|
8617
|
+
}
|
|
8618
|
+
} catch (err) {
|
|
8619
|
+
console.warn(`[Sphere] Nametag token mint failed after switch:`, err);
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
7092
8622
|
this.emitEvent("identity:changed", {
|
|
7093
8623
|
l1Address: this._identity.l1Address,
|
|
7094
8624
|
directAddress: this._identity.directAddress,
|
|
@@ -7110,7 +8640,8 @@ var Sphere = class _Sphere {
|
|
|
7110
8640
|
transport: this._transport,
|
|
7111
8641
|
oracle: this._oracle,
|
|
7112
8642
|
emitEvent,
|
|
7113
|
-
chainCode: this._masterKey?.chainCode
|
|
8643
|
+
chainCode: this._masterKey?.chainCode,
|
|
8644
|
+
price: this._priceProvider ?? void 0
|
|
7114
8645
|
});
|
|
7115
8646
|
this._communications.initialize({
|
|
7116
8647
|
identity: this._identity,
|
|
@@ -7143,6 +8674,14 @@ var Sphere = class _Sphere {
|
|
|
7143
8674
|
*/
|
|
7144
8675
|
deriveAddress(index, isChange = false) {
|
|
7145
8676
|
this.ensureReady();
|
|
8677
|
+
return this._deriveAddressInternal(index, isChange);
|
|
8678
|
+
}
|
|
8679
|
+
/**
|
|
8680
|
+
* Internal address derivation without ensureReady() check.
|
|
8681
|
+
* Used during initialization (loadTrackedAddresses, ensureAddressTracked)
|
|
8682
|
+
* when _initialized is still false.
|
|
8683
|
+
*/
|
|
8684
|
+
_deriveAddressInternal(index, isChange = false) {
|
|
7146
8685
|
if (!this._masterKey) {
|
|
7147
8686
|
throw new Error("HD derivation requires master key with chain code");
|
|
7148
8687
|
}
|
|
@@ -7281,6 +8820,22 @@ var Sphere = class _Sphere {
|
|
|
7281
8820
|
getProxyAddress() {
|
|
7282
8821
|
return this._cachedProxyAddress;
|
|
7283
8822
|
}
|
|
8823
|
+
/**
|
|
8824
|
+
* Resolve any identifier to full peer information.
|
|
8825
|
+
* Accepts @nametag, bare nametag, DIRECT://, PROXY://, L1 address, or transport pubkey.
|
|
8826
|
+
*
|
|
8827
|
+
* @example
|
|
8828
|
+
* ```ts
|
|
8829
|
+
* const peer = await sphere.resolve('@alice');
|
|
8830
|
+
* const peer = await sphere.resolve('DIRECT://...');
|
|
8831
|
+
* const peer = await sphere.resolve('alpha1...');
|
|
8832
|
+
* const peer = await sphere.resolve('ab12cd...'); // 64-char hex transport pubkey
|
|
8833
|
+
* ```
|
|
8834
|
+
*/
|
|
8835
|
+
async resolve(identifier) {
|
|
8836
|
+
this.ensureReady();
|
|
8837
|
+
return this._transport.resolve?.(identifier) ?? null;
|
|
8838
|
+
}
|
|
7284
8839
|
/** Compute and cache the PROXY address from the current nametag */
|
|
7285
8840
|
async _updateCachedProxyAddress() {
|
|
7286
8841
|
const nametag = this._identity?.nametag;
|
|
@@ -7319,11 +8874,12 @@ var Sphere = class _Sphere {
|
|
|
7319
8874
|
if (this._identity?.nametag) {
|
|
7320
8875
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
7321
8876
|
}
|
|
7322
|
-
if (this._transport.
|
|
7323
|
-
const success = await this._transport.
|
|
7324
|
-
cleanNametag,
|
|
8877
|
+
if (this._transport.publishIdentityBinding) {
|
|
8878
|
+
const success = await this._transport.publishIdentityBinding(
|
|
7325
8879
|
this._identity.chainPubkey,
|
|
7326
|
-
this._identity.
|
|
8880
|
+
this._identity.l1Address,
|
|
8881
|
+
this._identity.directAddress || "",
|
|
8882
|
+
cleanNametag
|
|
7327
8883
|
);
|
|
7328
8884
|
if (!success) {
|
|
7329
8885
|
throw new Error("Failed to register nametag. It may already be taken.");
|
|
@@ -7331,14 +8887,14 @@ var Sphere = class _Sphere {
|
|
|
7331
8887
|
}
|
|
7332
8888
|
this._identity.nametag = cleanNametag;
|
|
7333
8889
|
await this._updateCachedProxyAddress();
|
|
7334
|
-
const
|
|
7335
|
-
if (
|
|
7336
|
-
let
|
|
7337
|
-
if (!
|
|
7338
|
-
|
|
7339
|
-
this._addressNametags.set(
|
|
8890
|
+
const currentAddressId = this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
|
|
8891
|
+
if (currentAddressId) {
|
|
8892
|
+
let nametags = this._addressNametags.get(currentAddressId);
|
|
8893
|
+
if (!nametags) {
|
|
8894
|
+
nametags = /* @__PURE__ */ new Map();
|
|
8895
|
+
this._addressNametags.set(currentAddressId, nametags);
|
|
7340
8896
|
}
|
|
7341
|
-
|
|
8897
|
+
nametags.set(0, cleanNametag);
|
|
7342
8898
|
}
|
|
7343
8899
|
await this.persistAddressNametags();
|
|
7344
8900
|
if (!this._payments.hasNametag()) {
|
|
@@ -7357,19 +8913,19 @@ var Sphere = class _Sphere {
|
|
|
7357
8913
|
console.log(`[Sphere] Nametag registered for address ${this._currentAddressIndex}:`, cleanNametag);
|
|
7358
8914
|
}
|
|
7359
8915
|
/**
|
|
7360
|
-
* Persist
|
|
7361
|
-
* Format: { "DIRECT://abc...xyz": { "0": "alice", "1": "alice2" }, ... }
|
|
8916
|
+
* Persist tracked addresses to storage (only minimal fields via StorageProvider)
|
|
7362
8917
|
*/
|
|
7363
|
-
async
|
|
7364
|
-
const
|
|
7365
|
-
this.
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
8918
|
+
async persistTrackedAddresses() {
|
|
8919
|
+
const entries = [];
|
|
8920
|
+
for (const entry of this._trackedAddresses.values()) {
|
|
8921
|
+
entries.push({
|
|
8922
|
+
index: entry.index,
|
|
8923
|
+
hidden: entry.hidden,
|
|
8924
|
+
createdAt: entry.createdAt,
|
|
8925
|
+
updatedAt: entry.updatedAt
|
|
7369
8926
|
});
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
|
|
8927
|
+
}
|
|
8928
|
+
await this._storage.saveTrackedAddresses(entries);
|
|
7373
8929
|
}
|
|
7374
8930
|
/**
|
|
7375
8931
|
* Mint a nametag token on-chain (like Sphere wallet and lottery)
|
|
@@ -7403,63 +8959,184 @@ var Sphere = class _Sphere {
|
|
|
7403
8959
|
return this._payments.isNametagAvailable(nametag);
|
|
7404
8960
|
}
|
|
7405
8961
|
/**
|
|
7406
|
-
* Load
|
|
7407
|
-
*
|
|
7408
|
-
* And legacy format: { "0": "alice" } (migrates to new format on save)
|
|
8962
|
+
* Load tracked addresses from storage.
|
|
8963
|
+
* Falls back to migrating from old ADDRESS_NAMETAGS format.
|
|
7409
8964
|
*/
|
|
7410
|
-
async
|
|
8965
|
+
async loadTrackedAddresses() {
|
|
8966
|
+
this._trackedAddresses.clear();
|
|
8967
|
+
this._addressIdToIndex.clear();
|
|
7411
8968
|
try {
|
|
7412
|
-
const
|
|
7413
|
-
if (
|
|
7414
|
-
const
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
}
|
|
8969
|
+
const entries = await this._storage.loadTrackedAddresses();
|
|
8970
|
+
if (entries.length > 0) {
|
|
8971
|
+
for (const stored of entries) {
|
|
8972
|
+
const addrInfo = this._deriveAddressInternal(stored.index, false);
|
|
8973
|
+
const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
|
|
8974
|
+
const addressId = getAddressId(directAddress);
|
|
8975
|
+
const entry = {
|
|
8976
|
+
...stored,
|
|
8977
|
+
addressId,
|
|
8978
|
+
l1Address: addrInfo.address,
|
|
8979
|
+
directAddress,
|
|
8980
|
+
chainPubkey: addrInfo.publicKey
|
|
8981
|
+
};
|
|
8982
|
+
this._trackedAddresses.set(entry.index, entry);
|
|
8983
|
+
this._addressIdToIndex.set(addressId, entry.index);
|
|
7425
8984
|
}
|
|
8985
|
+
return;
|
|
8986
|
+
}
|
|
8987
|
+
const oldData = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
8988
|
+
if (oldData) {
|
|
8989
|
+
const parsed = JSON.parse(oldData);
|
|
8990
|
+
await this.migrateFromOldNametagFormat(parsed);
|
|
8991
|
+
await this.persistTrackedAddresses();
|
|
7426
8992
|
}
|
|
7427
8993
|
} catch {
|
|
7428
8994
|
}
|
|
7429
8995
|
}
|
|
7430
8996
|
/**
|
|
7431
|
-
*
|
|
7432
|
-
*
|
|
8997
|
+
* Migrate from old ADDRESS_NAMETAGS format to tracked addresses.
|
|
8998
|
+
* Scans HD indices 0..19 to match addressIds from the old format.
|
|
8999
|
+
* Populates both _trackedAddresses and _addressNametags.
|
|
7433
9000
|
*/
|
|
7434
|
-
async
|
|
7435
|
-
const
|
|
7436
|
-
|
|
7437
|
-
|
|
9001
|
+
async migrateFromOldNametagFormat(parsed) {
|
|
9002
|
+
const addressIdToNametags = /* @__PURE__ */ new Map();
|
|
9003
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
9004
|
+
if (typeof value === "object" && value !== null) {
|
|
9005
|
+
addressIdToNametags.set(key, value);
|
|
9006
|
+
}
|
|
9007
|
+
}
|
|
9008
|
+
if (addressIdToNametags.size === 0 || !this._masterKey) return;
|
|
9009
|
+
const SCAN_LIMIT = 20;
|
|
9010
|
+
for (let i = 0; i < SCAN_LIMIT && addressIdToNametags.size > 0; i++) {
|
|
9011
|
+
try {
|
|
9012
|
+
const addrInfo = this._deriveAddressInternal(i, false);
|
|
9013
|
+
const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
|
|
9014
|
+
const addressId = getAddressId(directAddress);
|
|
9015
|
+
if (addressIdToNametags.has(addressId)) {
|
|
9016
|
+
const nametagsObj = addressIdToNametags.get(addressId);
|
|
9017
|
+
const nametagMap = /* @__PURE__ */ new Map();
|
|
9018
|
+
for (const [idx, tag] of Object.entries(nametagsObj)) {
|
|
9019
|
+
nametagMap.set(parseInt(idx, 10), tag);
|
|
9020
|
+
}
|
|
9021
|
+
if (nametagMap.size > 0) {
|
|
9022
|
+
this._addressNametags.set(addressId, nametagMap);
|
|
9023
|
+
}
|
|
9024
|
+
const now = Date.now();
|
|
9025
|
+
const entry = {
|
|
9026
|
+
index: i,
|
|
9027
|
+
addressId,
|
|
9028
|
+
l1Address: addrInfo.address,
|
|
9029
|
+
directAddress,
|
|
9030
|
+
chainPubkey: addrInfo.publicKey,
|
|
9031
|
+
nametag: nametagMap.get(0),
|
|
9032
|
+
hidden: false,
|
|
9033
|
+
createdAt: now,
|
|
9034
|
+
updatedAt: now
|
|
9035
|
+
};
|
|
9036
|
+
this._trackedAddresses.set(i, entry);
|
|
9037
|
+
this._addressIdToIndex.set(addressId, i);
|
|
9038
|
+
addressIdToNametags.delete(addressId);
|
|
9039
|
+
}
|
|
9040
|
+
} catch {
|
|
9041
|
+
}
|
|
9042
|
+
}
|
|
9043
|
+
await this.persistAddressNametags();
|
|
9044
|
+
}
|
|
9045
|
+
/**
|
|
9046
|
+
* Ensure an address is tracked in the registry.
|
|
9047
|
+
* If not yet tracked, derives full info and creates the entry.
|
|
9048
|
+
*/
|
|
9049
|
+
async ensureAddressTracked(index) {
|
|
9050
|
+
const existing = this._trackedAddresses.get(index);
|
|
9051
|
+
if (existing) return existing;
|
|
9052
|
+
const addrInfo = this._deriveAddressInternal(index, false);
|
|
9053
|
+
const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
|
|
9054
|
+
const addressId = getAddressId(directAddress);
|
|
9055
|
+
const now = Date.now();
|
|
9056
|
+
const nametag = this._addressNametags.get(addressId)?.get(0);
|
|
9057
|
+
const entry = {
|
|
9058
|
+
index,
|
|
9059
|
+
addressId,
|
|
9060
|
+
l1Address: addrInfo.address,
|
|
9061
|
+
directAddress,
|
|
9062
|
+
chainPubkey: addrInfo.publicKey,
|
|
9063
|
+
nametag,
|
|
9064
|
+
hidden: false,
|
|
9065
|
+
createdAt: now,
|
|
9066
|
+
updatedAt: now
|
|
9067
|
+
};
|
|
9068
|
+
this._trackedAddresses.set(index, entry);
|
|
9069
|
+
this._addressIdToIndex.set(addressId, index);
|
|
9070
|
+
await this.persistTrackedAddresses();
|
|
9071
|
+
this.emitEvent("address:activated", { address: { ...entry } });
|
|
9072
|
+
return entry;
|
|
9073
|
+
}
|
|
9074
|
+
/**
|
|
9075
|
+
* Persist nametag cache to storage.
|
|
9076
|
+
* Format: { addressId: { "0": "alice", "1": "alice2" } }
|
|
9077
|
+
*/
|
|
9078
|
+
async persistAddressNametags() {
|
|
9079
|
+
const result = {};
|
|
9080
|
+
for (const [addressId, nametags] of this._addressNametags.entries()) {
|
|
9081
|
+
const obj = {};
|
|
9082
|
+
for (const [idx, tag] of nametags.entries()) {
|
|
9083
|
+
obj[idx.toString()] = tag;
|
|
9084
|
+
}
|
|
9085
|
+
result[addressId] = obj;
|
|
9086
|
+
}
|
|
9087
|
+
await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
|
|
9088
|
+
}
|
|
9089
|
+
/**
|
|
9090
|
+
* Load nametag cache from storage.
|
|
9091
|
+
*/
|
|
9092
|
+
async loadAddressNametags() {
|
|
9093
|
+
this._addressNametags.clear();
|
|
9094
|
+
try {
|
|
9095
|
+
const data = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
9096
|
+
if (!data) return;
|
|
9097
|
+
const parsed = JSON.parse(data);
|
|
9098
|
+
for (const [addressId, nametags] of Object.entries(parsed)) {
|
|
9099
|
+
const map = /* @__PURE__ */ new Map();
|
|
9100
|
+
for (const [idx, tag] of Object.entries(nametags)) {
|
|
9101
|
+
map.set(parseInt(idx, 10), tag);
|
|
9102
|
+
}
|
|
9103
|
+
this._addressNametags.set(addressId, map);
|
|
9104
|
+
}
|
|
9105
|
+
} catch {
|
|
7438
9106
|
}
|
|
7439
|
-
|
|
9107
|
+
}
|
|
9108
|
+
/**
|
|
9109
|
+
* Publish identity binding via transport.
|
|
9110
|
+
* Always publishes base identity (chainPubkey, l1Address, directAddress).
|
|
9111
|
+
* If nametag is set, also publishes nametag hash, proxy address, encrypted nametag.
|
|
9112
|
+
*/
|
|
9113
|
+
async syncIdentityWithTransport() {
|
|
9114
|
+
if (!this._transport.publishIdentityBinding) {
|
|
7440
9115
|
return;
|
|
7441
9116
|
}
|
|
7442
9117
|
try {
|
|
7443
|
-
const
|
|
7444
|
-
|
|
9118
|
+
const nametag = this._identity?.nametag;
|
|
9119
|
+
const success = await this._transport.publishIdentityBinding(
|
|
7445
9120
|
this._identity.chainPubkey,
|
|
7446
|
-
this._identity.
|
|
9121
|
+
this._identity.l1Address,
|
|
9122
|
+
this._identity.directAddress || "",
|
|
9123
|
+
nametag || void 0
|
|
7447
9124
|
);
|
|
7448
9125
|
if (success) {
|
|
7449
|
-
console.log(`[Sphere]
|
|
7450
|
-
} else {
|
|
9126
|
+
console.log(`[Sphere] Identity binding published${nametag ? ` with nametag @${nametag}` : ""}`);
|
|
9127
|
+
} else if (nametag) {
|
|
7451
9128
|
console.warn(`[Sphere] Nametag @${nametag} is taken by another pubkey`);
|
|
7452
9129
|
}
|
|
7453
9130
|
} catch (error) {
|
|
7454
|
-
console.warn(`[Sphere]
|
|
9131
|
+
console.warn(`[Sphere] Identity binding sync failed:`, error);
|
|
7455
9132
|
}
|
|
7456
9133
|
}
|
|
7457
9134
|
/**
|
|
7458
|
-
* Recover nametag from
|
|
9135
|
+
* Recover nametag from transport after wallet import.
|
|
7459
9136
|
* Searches for encrypted nametag events authored by this wallet's pubkey
|
|
7460
|
-
* and decrypts them to restore the nametag association
|
|
9137
|
+
* and decrypts them to restore the nametag association.
|
|
7461
9138
|
*/
|
|
7462
|
-
async
|
|
9139
|
+
async recoverNametagFromTransport() {
|
|
7463
9140
|
if (this._identity?.nametag) {
|
|
7464
9141
|
return;
|
|
7465
9142
|
}
|
|
@@ -7473,22 +9150,21 @@ var Sphere = class _Sphere {
|
|
|
7473
9150
|
this._identity.nametag = recoveredNametag;
|
|
7474
9151
|
await this._updateCachedProxyAddress();
|
|
7475
9152
|
}
|
|
7476
|
-
const
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
this._addressNametags.set(addressId, nametagsMap);
|
|
7482
|
-
}
|
|
7483
|
-
const nextIndex = nametagsMap.size;
|
|
7484
|
-
nametagsMap.set(nextIndex, recoveredNametag);
|
|
9153
|
+
const entry = await this.ensureAddressTracked(this._currentAddressIndex);
|
|
9154
|
+
let nametags = this._addressNametags.get(entry.addressId);
|
|
9155
|
+
if (!nametags) {
|
|
9156
|
+
nametags = /* @__PURE__ */ new Map();
|
|
9157
|
+
this._addressNametags.set(entry.addressId, nametags);
|
|
7485
9158
|
}
|
|
9159
|
+
const nextIndex = nametags.size;
|
|
9160
|
+
nametags.set(nextIndex, recoveredNametag);
|
|
7486
9161
|
await this.persistAddressNametags();
|
|
7487
|
-
if (this._transport.
|
|
7488
|
-
await this._transport.
|
|
7489
|
-
recoveredNametag,
|
|
9162
|
+
if (this._transport.publishIdentityBinding) {
|
|
9163
|
+
await this._transport.publishIdentityBinding(
|
|
7490
9164
|
this._identity.chainPubkey,
|
|
7491
|
-
this._identity.
|
|
9165
|
+
this._identity.l1Address,
|
|
9166
|
+
this._identity.directAddress || "",
|
|
9167
|
+
recoveredNametag
|
|
7492
9168
|
);
|
|
7493
9169
|
}
|
|
7494
9170
|
this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
|
|
@@ -7516,6 +9192,9 @@ var Sphere = class _Sphere {
|
|
|
7516
9192
|
await this._oracle.disconnect();
|
|
7517
9193
|
this._initialized = false;
|
|
7518
9194
|
this._identity = null;
|
|
9195
|
+
this._trackedAddresses.clear();
|
|
9196
|
+
this._addressIdToIndex.clear();
|
|
9197
|
+
this._addressNametags.clear();
|
|
7519
9198
|
this.eventHandlers.clear();
|
|
7520
9199
|
if (_Sphere.instance === this) {
|
|
7521
9200
|
_Sphere.instance = null;
|
|
@@ -7613,14 +9292,14 @@ var Sphere = class _Sphere {
|
|
|
7613
9292
|
if (this._identity) {
|
|
7614
9293
|
this._storage.setIdentity(this._identity);
|
|
7615
9294
|
}
|
|
9295
|
+
await this.loadTrackedAddresses();
|
|
7616
9296
|
await this.loadAddressNametags();
|
|
9297
|
+
const trackedEntry = await this.ensureAddressTracked(this._currentAddressIndex);
|
|
9298
|
+
const nametag = this._addressNametags.get(trackedEntry.addressId)?.get(0);
|
|
7617
9299
|
if (this._currentAddressIndex > 0 && this._masterKey) {
|
|
7618
|
-
const addressInfo = this.
|
|
9300
|
+
const addressInfo = this._deriveAddressInternal(this._currentAddressIndex, false);
|
|
7619
9301
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
7620
9302
|
const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
|
|
7621
|
-
const addressId = getAddressId(predicateAddress);
|
|
7622
|
-
const nametagsMap = this._addressNametags.get(addressId);
|
|
7623
|
-
const nametag = nametagsMap?.get(0);
|
|
7624
9303
|
this._identity = {
|
|
7625
9304
|
privateKey: addressInfo.privateKey,
|
|
7626
9305
|
chainPubkey: addressInfo.publicKey,
|
|
@@ -7631,13 +9310,8 @@ var Sphere = class _Sphere {
|
|
|
7631
9310
|
};
|
|
7632
9311
|
this._storage.setIdentity(this._identity);
|
|
7633
9312
|
console.log(`[Sphere] Restored to address ${this._currentAddressIndex}:`, this._identity.l1Address);
|
|
7634
|
-
} else if (this._identity) {
|
|
7635
|
-
|
|
7636
|
-
const nametagsMap = addressId ? this._addressNametags.get(addressId) : void 0;
|
|
7637
|
-
const nametag = nametagsMap?.get(0);
|
|
7638
|
-
if (nametag) {
|
|
7639
|
-
this._identity.nametag = nametag;
|
|
7640
|
-
}
|
|
9313
|
+
} else if (this._identity && nametag) {
|
|
9314
|
+
this._identity.nametag = nametag;
|
|
7641
9315
|
}
|
|
7642
9316
|
await this._updateCachedProxyAddress();
|
|
7643
9317
|
}
|
|
@@ -7695,7 +9369,7 @@ var Sphere = class _Sphere {
|
|
|
7695
9369
|
// ===========================================================================
|
|
7696
9370
|
async initializeProviders() {
|
|
7697
9371
|
this._storage.setIdentity(this._identity);
|
|
7698
|
-
this._transport.setIdentity(this._identity);
|
|
9372
|
+
await this._transport.setIdentity(this._identity);
|
|
7699
9373
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
7700
9374
|
provider.setIdentity(this._identity);
|
|
7701
9375
|
}
|
|
@@ -7716,7 +9390,8 @@ var Sphere = class _Sphere {
|
|
|
7716
9390
|
oracle: this._oracle,
|
|
7717
9391
|
emitEvent,
|
|
7718
9392
|
// Pass chain code for L1 HD derivation
|
|
7719
|
-
chainCode: this._masterKey?.chainCode
|
|
9393
|
+
chainCode: this._masterKey?.chainCode,
|
|
9394
|
+
price: this._priceProvider ?? void 0
|
|
7720
9395
|
});
|
|
7721
9396
|
this._communications.initialize({
|
|
7722
9397
|
identity: this._identity,
|
|
@@ -7798,6 +9473,65 @@ function formatAmount(amount, options = {}) {
|
|
|
7798
9473
|
return symbol ? `${readable} ${symbol}` : readable;
|
|
7799
9474
|
}
|
|
7800
9475
|
|
|
9476
|
+
// types/payment-session.ts
|
|
9477
|
+
function createPaymentSession(params) {
|
|
9478
|
+
const now = Date.now();
|
|
9479
|
+
const deadlineMs = params.deadlineMs ?? 3e5;
|
|
9480
|
+
return {
|
|
9481
|
+
id: crypto.randomUUID(),
|
|
9482
|
+
direction: params.direction,
|
|
9483
|
+
status: "INITIATED",
|
|
9484
|
+
createdAt: now,
|
|
9485
|
+
updatedAt: now,
|
|
9486
|
+
deadline: now + deadlineMs,
|
|
9487
|
+
error: null,
|
|
9488
|
+
sourceTokenId: params.sourceTokenId,
|
|
9489
|
+
recipientNametag: params.recipientNametag,
|
|
9490
|
+
recipientPubkey: params.recipientPubkey,
|
|
9491
|
+
amount: params.amount,
|
|
9492
|
+
coinId: params.coinId,
|
|
9493
|
+
salt: params.salt
|
|
9494
|
+
};
|
|
9495
|
+
}
|
|
9496
|
+
function createSplitPaymentSession(params) {
|
|
9497
|
+
const now = Date.now();
|
|
9498
|
+
return {
|
|
9499
|
+
id: crypto.randomUUID(),
|
|
9500
|
+
direction: "SEND",
|
|
9501
|
+
sourceTokenId: params.sourceTokenId,
|
|
9502
|
+
paymentAmount: params.paymentAmount,
|
|
9503
|
+
changeAmount: params.changeAmount,
|
|
9504
|
+
recipientNametag: params.recipientNametag,
|
|
9505
|
+
recipientPubkey: params.recipientPubkey,
|
|
9506
|
+
splitGroupId: params.splitGroupId,
|
|
9507
|
+
phases: {
|
|
9508
|
+
burn: "PENDING",
|
|
9509
|
+
mints: "PENDING",
|
|
9510
|
+
transfer: "PENDING"
|
|
9511
|
+
},
|
|
9512
|
+
timing: {},
|
|
9513
|
+
createdAt: now,
|
|
9514
|
+
updatedAt: now,
|
|
9515
|
+
error: null
|
|
9516
|
+
};
|
|
9517
|
+
}
|
|
9518
|
+
function isPaymentSessionTimedOut(session) {
|
|
9519
|
+
if (!("deadline" in session) || !session.deadline) return false;
|
|
9520
|
+
return Date.now() > session.deadline;
|
|
9521
|
+
}
|
|
9522
|
+
function isPaymentSessionTerminal(session) {
|
|
9523
|
+
return session.status === "COMPLETED" || session.status === "FAILED" || session.status === "TIMED_OUT";
|
|
9524
|
+
}
|
|
9525
|
+
function createPaymentSessionError(code, message, recoverable = false, details) {
|
|
9526
|
+
return {
|
|
9527
|
+
code,
|
|
9528
|
+
message,
|
|
9529
|
+
timestamp: Date.now(),
|
|
9530
|
+
recoverable,
|
|
9531
|
+
details
|
|
9532
|
+
};
|
|
9533
|
+
}
|
|
9534
|
+
|
|
7801
9535
|
// types/index.ts
|
|
7802
9536
|
var SphereError = class extends Error {
|
|
7803
9537
|
code;
|
|
@@ -7985,14 +9719,30 @@ var TokenValidator = class {
|
|
|
7985
9719
|
}
|
|
7986
9720
|
}
|
|
7987
9721
|
/**
|
|
7988
|
-
* Check which tokens are spent
|
|
9722
|
+
* Check which tokens are spent using SDK Token object to calculate state hash.
|
|
9723
|
+
*
|
|
9724
|
+
* Follows the same approach as the Sphere webgui TokenValidationService:
|
|
9725
|
+
* 1. Parse TXF using SDK's Token.fromJSON()
|
|
9726
|
+
* 2. Calculate CURRENT state hash via sdkToken.state.calculateHash()
|
|
9727
|
+
* 3. Create RequestId via RequestId.create(walletPubKey, calculatedHash)
|
|
9728
|
+
*
|
|
9729
|
+
* Uses wallet's own pubkey (not source state predicate key) because "spent" means
|
|
9730
|
+
* the CURRENT OWNER committed this state. Using the source state key would falsely
|
|
9731
|
+
* detect received tokens as "spent" (sender's commitment matches source state).
|
|
7989
9732
|
*/
|
|
7990
9733
|
async checkSpentTokens(tokens, publicKey, options) {
|
|
7991
9734
|
const spentTokens = [];
|
|
7992
9735
|
const errors = [];
|
|
9736
|
+
if (!this.aggregatorClient) {
|
|
9737
|
+
errors.push("Aggregator client not available");
|
|
9738
|
+
return { spentTokens, errors };
|
|
9739
|
+
}
|
|
7993
9740
|
const batchSize = options?.batchSize ?? 3;
|
|
7994
9741
|
const total = tokens.length;
|
|
7995
9742
|
let completed = 0;
|
|
9743
|
+
const { Token: SdkToken3 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
9744
|
+
const { RequestId } = await import("@unicitylabs/state-transition-sdk/lib/api/RequestId");
|
|
9745
|
+
const pubKeyBytes = Buffer.from(publicKey, "hex");
|
|
7996
9746
|
for (let i = 0; i < tokens.length; i += batchSize) {
|
|
7997
9747
|
const batch = tokens.slice(i, i + batchSize);
|
|
7998
9748
|
const batchResults = await Promise.allSettled(
|
|
@@ -8002,13 +9752,39 @@ var TokenValidator = class {
|
|
|
8002
9752
|
if (!txf) {
|
|
8003
9753
|
return { tokenId: token.id, localId: token.id, stateHash: "", spent: false, error: "Invalid TXF" };
|
|
8004
9754
|
}
|
|
8005
|
-
const tokenId = txf.genesis
|
|
8006
|
-
const
|
|
8007
|
-
|
|
8008
|
-
|
|
9755
|
+
const tokenId = txf.genesis?.data?.tokenId || token.id;
|
|
9756
|
+
const sdkToken = await SdkToken3.fromJSON(txf);
|
|
9757
|
+
const calculatedStateHash = await sdkToken.state.calculateHash();
|
|
9758
|
+
const calculatedStateHashStr = calculatedStateHash.toJSON();
|
|
9759
|
+
const cacheKey = `${tokenId}:${calculatedStateHashStr}:${publicKey}`;
|
|
9760
|
+
const cached = this.spentStateCache.get(cacheKey);
|
|
9761
|
+
if (cached !== void 0) {
|
|
9762
|
+
if (cached.isSpent) {
|
|
9763
|
+
return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: true };
|
|
9764
|
+
}
|
|
9765
|
+
if (Date.now() - cached.timestamp < this.UNSPENT_CACHE_TTL_MS) {
|
|
9766
|
+
return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: false };
|
|
9767
|
+
}
|
|
9768
|
+
}
|
|
9769
|
+
const { DataHash } = await import("@unicitylabs/state-transition-sdk/lib/hash/DataHash");
|
|
9770
|
+
const stateHashObj = DataHash.fromJSON(calculatedStateHashStr);
|
|
9771
|
+
const requestId2 = await RequestId.create(pubKeyBytes, stateHashObj);
|
|
9772
|
+
const response = await this.aggregatorClient.getInclusionProof(requestId2);
|
|
9773
|
+
let isSpent = false;
|
|
9774
|
+
if (response.inclusionProof) {
|
|
9775
|
+
const proof = response.inclusionProof;
|
|
9776
|
+
const pathResult = await proof.merkleTreePath.verify(
|
|
9777
|
+
requestId2.toBitString().toBigInt()
|
|
9778
|
+
);
|
|
9779
|
+
if (pathResult.isPathValid && pathResult.isPathIncluded && proof.authenticator !== null) {
|
|
9780
|
+
isSpent = true;
|
|
9781
|
+
}
|
|
8009
9782
|
}
|
|
8010
|
-
|
|
8011
|
-
|
|
9783
|
+
this.spentStateCache.set(cacheKey, {
|
|
9784
|
+
isSpent,
|
|
9785
|
+
timestamp: Date.now()
|
|
9786
|
+
});
|
|
9787
|
+
return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: isSpent };
|
|
8012
9788
|
} catch (err) {
|
|
8013
9789
|
return {
|
|
8014
9790
|
tokenId: token.id,
|
|
@@ -8067,8 +9843,8 @@ var TokenValidator = class {
|
|
|
8067
9843
|
}
|
|
8068
9844
|
async verifyWithSdk(txfToken) {
|
|
8069
9845
|
try {
|
|
8070
|
-
const { Token:
|
|
8071
|
-
const sdkToken = await
|
|
9846
|
+
const { Token: Token5 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
9847
|
+
const sdkToken = await Token5.fromJSON(txfToken);
|
|
8072
9848
|
if (!this.trustBase) {
|
|
8073
9849
|
return { success: true };
|
|
8074
9850
|
}
|
|
@@ -8091,8 +9867,116 @@ var TokenValidator = class {
|
|
|
8091
9867
|
function createTokenValidator(options) {
|
|
8092
9868
|
return new TokenValidator(options);
|
|
8093
9869
|
}
|
|
9870
|
+
|
|
9871
|
+
// price/CoinGeckoPriceProvider.ts
|
|
9872
|
+
var CoinGeckoPriceProvider = class {
|
|
9873
|
+
platform = "coingecko";
|
|
9874
|
+
cache = /* @__PURE__ */ new Map();
|
|
9875
|
+
apiKey;
|
|
9876
|
+
cacheTtlMs;
|
|
9877
|
+
timeout;
|
|
9878
|
+
debug;
|
|
9879
|
+
baseUrl;
|
|
9880
|
+
constructor(config) {
|
|
9881
|
+
this.apiKey = config?.apiKey;
|
|
9882
|
+
this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
|
|
9883
|
+
this.timeout = config?.timeout ?? 1e4;
|
|
9884
|
+
this.debug = config?.debug ?? false;
|
|
9885
|
+
this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
|
|
9886
|
+
}
|
|
9887
|
+
async getPrices(tokenNames) {
|
|
9888
|
+
if (tokenNames.length === 0) {
|
|
9889
|
+
return /* @__PURE__ */ new Map();
|
|
9890
|
+
}
|
|
9891
|
+
const now = Date.now();
|
|
9892
|
+
const result = /* @__PURE__ */ new Map();
|
|
9893
|
+
const uncachedNames = [];
|
|
9894
|
+
for (const name of tokenNames) {
|
|
9895
|
+
const cached = this.cache.get(name);
|
|
9896
|
+
if (cached && cached.expiresAt > now) {
|
|
9897
|
+
if (cached.price !== null) {
|
|
9898
|
+
result.set(name, cached.price);
|
|
9899
|
+
}
|
|
9900
|
+
} else {
|
|
9901
|
+
uncachedNames.push(name);
|
|
9902
|
+
}
|
|
9903
|
+
}
|
|
9904
|
+
if (uncachedNames.length === 0) {
|
|
9905
|
+
return result;
|
|
9906
|
+
}
|
|
9907
|
+
try {
|
|
9908
|
+
const ids = uncachedNames.join(",");
|
|
9909
|
+
const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
|
|
9910
|
+
const headers = { Accept: "application/json" };
|
|
9911
|
+
if (this.apiKey) {
|
|
9912
|
+
headers["x-cg-pro-api-key"] = this.apiKey;
|
|
9913
|
+
}
|
|
9914
|
+
if (this.debug) {
|
|
9915
|
+
console.log(`[CoinGecko] Fetching prices for: ${uncachedNames.join(", ")}`);
|
|
9916
|
+
}
|
|
9917
|
+
const response = await fetch(url, {
|
|
9918
|
+
headers,
|
|
9919
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
9920
|
+
});
|
|
9921
|
+
if (!response.ok) {
|
|
9922
|
+
throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
|
|
9923
|
+
}
|
|
9924
|
+
const data = await response.json();
|
|
9925
|
+
for (const [name, values] of Object.entries(data)) {
|
|
9926
|
+
if (values && typeof values === "object") {
|
|
9927
|
+
const price = {
|
|
9928
|
+
tokenName: name,
|
|
9929
|
+
priceUsd: values.usd ?? 0,
|
|
9930
|
+
priceEur: values.eur,
|
|
9931
|
+
change24h: values.usd_24h_change,
|
|
9932
|
+
timestamp: now
|
|
9933
|
+
};
|
|
9934
|
+
this.cache.set(name, { price, expiresAt: now + this.cacheTtlMs });
|
|
9935
|
+
result.set(name, price);
|
|
9936
|
+
}
|
|
9937
|
+
}
|
|
9938
|
+
for (const name of uncachedNames) {
|
|
9939
|
+
if (!result.has(name)) {
|
|
9940
|
+
this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
9943
|
+
if (this.debug) {
|
|
9944
|
+
console.log(`[CoinGecko] Fetched ${result.size} prices`);
|
|
9945
|
+
}
|
|
9946
|
+
} catch (error) {
|
|
9947
|
+
if (this.debug) {
|
|
9948
|
+
console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
|
|
9949
|
+
}
|
|
9950
|
+
for (const name of uncachedNames) {
|
|
9951
|
+
const stale = this.cache.get(name);
|
|
9952
|
+
if (stale?.price) {
|
|
9953
|
+
result.set(name, stale.price);
|
|
9954
|
+
}
|
|
9955
|
+
}
|
|
9956
|
+
}
|
|
9957
|
+
return result;
|
|
9958
|
+
}
|
|
9959
|
+
async getPrice(tokenName) {
|
|
9960
|
+
const prices = await this.getPrices([tokenName]);
|
|
9961
|
+
return prices.get(tokenName) ?? null;
|
|
9962
|
+
}
|
|
9963
|
+
clearCache() {
|
|
9964
|
+
this.cache.clear();
|
|
9965
|
+
}
|
|
9966
|
+
};
|
|
9967
|
+
|
|
9968
|
+
// price/index.ts
|
|
9969
|
+
function createPriceProvider(config) {
|
|
9970
|
+
switch (config.platform) {
|
|
9971
|
+
case "coingecko":
|
|
9972
|
+
return new CoinGeckoPriceProvider(config);
|
|
9973
|
+
default:
|
|
9974
|
+
throw new Error(`Unsupported price platform: ${String(config.platform)}`);
|
|
9975
|
+
}
|
|
9976
|
+
}
|
|
8094
9977
|
export {
|
|
8095
9978
|
COIN_TYPES,
|
|
9979
|
+
CoinGeckoPriceProvider,
|
|
8096
9980
|
CommunicationsModule,
|
|
8097
9981
|
DEFAULT_AGGREGATOR_TIMEOUT,
|
|
8098
9982
|
DEFAULT_AGGREGATOR_URL,
|
|
@@ -8128,8 +10012,12 @@ export {
|
|
|
8128
10012
|
createCommunicationsModule,
|
|
8129
10013
|
createKeyPair,
|
|
8130
10014
|
createL1PaymentsModule,
|
|
10015
|
+
createPaymentSession,
|
|
10016
|
+
createPaymentSessionError,
|
|
8131
10017
|
createPaymentsModule,
|
|
10018
|
+
createPriceProvider,
|
|
8132
10019
|
createSphere,
|
|
10020
|
+
createSplitPaymentSession,
|
|
8133
10021
|
createTokenValidator,
|
|
8134
10022
|
decodeBech32,
|
|
8135
10023
|
decryptCMasterKey,
|
|
@@ -8167,7 +10055,12 @@ export {
|
|
|
8167
10055
|
initSphere,
|
|
8168
10056
|
isArchivedKey,
|
|
8169
10057
|
isForkedKey,
|
|
10058
|
+
isInstantSplitBundle,
|
|
10059
|
+
isInstantSplitBundleV4,
|
|
10060
|
+
isInstantSplitBundleV5,
|
|
8170
10061
|
isKnownToken,
|
|
10062
|
+
isPaymentSessionTerminal,
|
|
10063
|
+
isPaymentSessionTimedOut,
|
|
8171
10064
|
isSQLiteDatabase,
|
|
8172
10065
|
isTextWalletEncrypted,
|
|
8173
10066
|
isTokenKey,
|