@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/core/index.cjs
CHANGED
|
@@ -1442,7 +1442,6 @@ var L1PaymentsModule = class {
|
|
|
1442
1442
|
_initialized = false;
|
|
1443
1443
|
_config;
|
|
1444
1444
|
_identity;
|
|
1445
|
-
_chainCode;
|
|
1446
1445
|
_addresses = [];
|
|
1447
1446
|
_wallet;
|
|
1448
1447
|
_transport;
|
|
@@ -1456,7 +1455,6 @@ var L1PaymentsModule = class {
|
|
|
1456
1455
|
}
|
|
1457
1456
|
async initialize(deps) {
|
|
1458
1457
|
this._identity = deps.identity;
|
|
1459
|
-
this._chainCode = deps.chainCode;
|
|
1460
1458
|
this._addresses = deps.addresses ?? [];
|
|
1461
1459
|
this._transport = deps.transport;
|
|
1462
1460
|
this._wallet = {
|
|
@@ -1492,7 +1490,6 @@ var L1PaymentsModule = class {
|
|
|
1492
1490
|
}
|
|
1493
1491
|
this._initialized = false;
|
|
1494
1492
|
this._identity = void 0;
|
|
1495
|
-
this._chainCode = void 0;
|
|
1496
1493
|
this._addresses = [];
|
|
1497
1494
|
this._wallet = void 0;
|
|
1498
1495
|
}
|
|
@@ -1527,10 +1524,10 @@ var L1PaymentsModule = class {
|
|
|
1527
1524
|
* Resolve nametag to L1 address using transport provider
|
|
1528
1525
|
*/
|
|
1529
1526
|
async resolveNametagToL1Address(nametag) {
|
|
1530
|
-
if (!this._transport?.
|
|
1531
|
-
throw new Error("Transport provider does not support
|
|
1527
|
+
if (!this._transport?.resolve) {
|
|
1528
|
+
throw new Error("Transport provider does not support resolution");
|
|
1532
1529
|
}
|
|
1533
|
-
const info = await this._transport.
|
|
1530
|
+
const info = await this._transport.resolve(nametag);
|
|
1534
1531
|
if (!info) {
|
|
1535
1532
|
throw new Error(`Nametag not found: ${nametag}`);
|
|
1536
1533
|
}
|
|
@@ -1804,25 +1801,11 @@ var TokenSplitCalculator = class {
|
|
|
1804
1801
|
* 3. If no exact match, determine which token to split
|
|
1805
1802
|
*/
|
|
1806
1803
|
async calculateOptimalSplit(availableTokens, targetAmount, targetCoinIdHex) {
|
|
1807
|
-
console.log(
|
|
1808
|
-
`[SplitCalculator] Calculating split for ${targetAmount} of ${targetCoinIdHex}`
|
|
1809
|
-
);
|
|
1810
|
-
console.log(`[SplitCalculator] Available tokens: ${availableTokens.length}`);
|
|
1811
1804
|
const candidates = [];
|
|
1812
1805
|
for (const t of availableTokens) {
|
|
1813
|
-
|
|
1814
|
-
if (t.
|
|
1815
|
-
|
|
1816
|
-
continue;
|
|
1817
|
-
}
|
|
1818
|
-
if (t.status !== "confirmed") {
|
|
1819
|
-
console.log(`[SplitCalculator] Skipping token ${t.id}: status is ${t.status}`);
|
|
1820
|
-
continue;
|
|
1821
|
-
}
|
|
1822
|
-
if (!t.sdkData) {
|
|
1823
|
-
console.log(`[SplitCalculator] Skipping token ${t.id}: no sdkData`);
|
|
1824
|
-
continue;
|
|
1825
|
-
}
|
|
1806
|
+
if (t.coinId !== targetCoinIdHex) continue;
|
|
1807
|
+
if (t.status !== "confirmed") continue;
|
|
1808
|
+
if (!t.sdkData) continue;
|
|
1826
1809
|
try {
|
|
1827
1810
|
const parsed = JSON.parse(t.sdkData);
|
|
1828
1811
|
const sdkToken = await import_Token.Token.fromJSON(parsed);
|
|
@@ -1850,14 +1833,12 @@ var TokenSplitCalculator = class {
|
|
|
1850
1833
|
}
|
|
1851
1834
|
const exactMatch = candidates.find((t) => t.amount === targetAmount);
|
|
1852
1835
|
if (exactMatch) {
|
|
1853
|
-
console.log("[SplitCalculator] Found exact match token");
|
|
1854
1836
|
return this.createDirectPlan([exactMatch], targetAmount, targetCoinIdHex);
|
|
1855
1837
|
}
|
|
1856
1838
|
const maxCombinationSize = Math.min(5, candidates.length);
|
|
1857
1839
|
for (let size = 2; size <= maxCombinationSize; size++) {
|
|
1858
1840
|
const combo = this.findCombinationOfSize(candidates, targetAmount, size);
|
|
1859
1841
|
if (combo) {
|
|
1860
|
-
console.log(`[SplitCalculator] Found exact combination of ${size} tokens`);
|
|
1861
1842
|
return this.createDirectPlan(combo, targetAmount, targetCoinIdHex);
|
|
1862
1843
|
}
|
|
1863
1844
|
}
|
|
@@ -1874,9 +1855,6 @@ var TokenSplitCalculator = class {
|
|
|
1874
1855
|
} else {
|
|
1875
1856
|
const neededFromThisToken = targetAmount - currentSum;
|
|
1876
1857
|
const remainderForSender = candidate.amount - neededFromThisToken;
|
|
1877
|
-
console.log(
|
|
1878
|
-
`[SplitCalculator] Split required. Sending: ${neededFromThisToken}, Remainder: ${remainderForSender}`
|
|
1879
|
-
);
|
|
1880
1858
|
return {
|
|
1881
1859
|
tokensToTransferDirectly: toTransfer,
|
|
1882
1860
|
tokenToSplit: candidate,
|
|
@@ -1895,16 +1873,10 @@ var TokenSplitCalculator = class {
|
|
|
1895
1873
|
*/
|
|
1896
1874
|
getTokenBalance(sdkToken, coinIdHex) {
|
|
1897
1875
|
try {
|
|
1898
|
-
if (!sdkToken.coins)
|
|
1899
|
-
console.log("[SplitCalculator] Token has no coins");
|
|
1900
|
-
return 0n;
|
|
1901
|
-
}
|
|
1876
|
+
if (!sdkToken.coins) return 0n;
|
|
1902
1877
|
const coinId = import_CoinId.CoinId.fromJSON(coinIdHex);
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
return balance ?? 0n;
|
|
1906
|
-
} catch (e) {
|
|
1907
|
-
console.error("[SplitCalculator] Error getting token balance:", e);
|
|
1878
|
+
return sdkToken.coins.get(coinId) ?? 0n;
|
|
1879
|
+
} catch {
|
|
1908
1880
|
return 0n;
|
|
1909
1881
|
}
|
|
1910
1882
|
}
|
|
@@ -2133,20 +2105,18 @@ var NametagMinter = class {
|
|
|
2133
2105
|
const cleanNametag = nametag.replace("@", "").trim();
|
|
2134
2106
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2135
2107
|
try {
|
|
2136
|
-
const isAvailable = await this.isNametagAvailable(cleanNametag);
|
|
2137
|
-
if (!isAvailable) {
|
|
2138
|
-
return {
|
|
2139
|
-
success: false,
|
|
2140
|
-
error: `Nametag "${cleanNametag}" is already taken`
|
|
2141
|
-
};
|
|
2142
|
-
}
|
|
2143
2108
|
const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
|
|
2144
2109
|
const nametagTokenType = new import_TokenType.TokenType(
|
|
2145
2110
|
Buffer.from(UNICITY_TOKEN_TYPE_HEX, "hex")
|
|
2146
2111
|
);
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2112
|
+
const nametagBytes = new TextEncoder().encode(cleanNametag);
|
|
2113
|
+
const pubKey = this.signingService.publicKey;
|
|
2114
|
+
const saltInput = new Uint8Array(pubKey.length + nametagBytes.length);
|
|
2115
|
+
saltInput.set(pubKey, 0);
|
|
2116
|
+
saltInput.set(nametagBytes, pubKey.length);
|
|
2117
|
+
const saltBuffer = await crypto.subtle.digest("SHA-256", saltInput);
|
|
2118
|
+
const salt = new Uint8Array(saltBuffer);
|
|
2119
|
+
this.log("Generated deterministic salt");
|
|
2150
2120
|
const mintData = await import_MintTransactionData.MintTransactionData.createFromNametag(
|
|
2151
2121
|
cleanNametag,
|
|
2152
2122
|
nametagTokenType,
|
|
@@ -2268,8 +2238,10 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
2268
2238
|
WALLET_EXISTS: "wallet_exists",
|
|
2269
2239
|
/** Current active address index */
|
|
2270
2240
|
CURRENT_ADDRESS_INDEX: "current_address_index",
|
|
2271
|
-
/**
|
|
2272
|
-
ADDRESS_NAMETAGS: "address_nametags"
|
|
2241
|
+
/** Nametag cache per address (separate from tracked addresses registry) */
|
|
2242
|
+
ADDRESS_NAMETAGS: "address_nametags",
|
|
2243
|
+
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
2244
|
+
TRACKED_ADDRESSES: "tracked_addresses"
|
|
2273
2245
|
};
|
|
2274
2246
|
var STORAGE_KEYS_ADDRESS = {
|
|
2275
2247
|
/** Pending transfers for this address */
|
|
@@ -2612,11 +2584,16 @@ function getCurrentStateHash(txf) {
|
|
|
2612
2584
|
if (lastTx?.newStateHash) {
|
|
2613
2585
|
return lastTx.newStateHash;
|
|
2614
2586
|
}
|
|
2615
|
-
|
|
2587
|
+
if (lastTx?.inclusionProof?.authenticator?.stateHash) {
|
|
2588
|
+
return lastTx.inclusionProof.authenticator.stateHash;
|
|
2589
|
+
}
|
|
2616
2590
|
}
|
|
2617
2591
|
if (txf._integrity?.currentStateHash) {
|
|
2618
2592
|
return txf._integrity.currentStateHash;
|
|
2619
2593
|
}
|
|
2594
|
+
if (txf.genesis?.inclusionProof?.authenticator?.stateHash) {
|
|
2595
|
+
return txf.genesis.inclusionProof.authenticator.stateHash;
|
|
2596
|
+
}
|
|
2620
2597
|
return void 0;
|
|
2621
2598
|
}
|
|
2622
2599
|
|
|
@@ -2914,16 +2891,733 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
2914
2891
|
}
|
|
2915
2892
|
};
|
|
2916
2893
|
|
|
2917
|
-
// modules/payments/
|
|
2894
|
+
// modules/payments/InstantSplitExecutor.ts
|
|
2918
2895
|
var import_Token4 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
2896
|
+
var import_TokenId3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
2897
|
+
var import_TokenState3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
2919
2898
|
var import_CoinId3 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
|
|
2899
|
+
var import_TokenCoinData2 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData");
|
|
2900
|
+
var import_TokenSplitBuilder2 = require("@unicitylabs/state-transition-sdk/lib/transaction/split/TokenSplitBuilder");
|
|
2901
|
+
var import_HashAlgorithm3 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
2902
|
+
var import_UnmaskedPredicate3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
2903
|
+
var import_UnmaskedPredicateReference2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
2920
2904
|
var import_TransferCommitment2 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
|
|
2905
|
+
var import_InclusionProofUtils3 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
2906
|
+
async function sha2563(input) {
|
|
2907
|
+
const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
2908
|
+
const buffer = new ArrayBuffer(data.length);
|
|
2909
|
+
new Uint8Array(buffer).set(data);
|
|
2910
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
|
|
2911
|
+
return new Uint8Array(hashBuffer);
|
|
2912
|
+
}
|
|
2913
|
+
function toHex2(bytes) {
|
|
2914
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2915
|
+
}
|
|
2916
|
+
function fromHex2(hex) {
|
|
2917
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
2918
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
2919
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
2920
|
+
}
|
|
2921
|
+
return bytes;
|
|
2922
|
+
}
|
|
2923
|
+
var InstantSplitExecutor = class {
|
|
2924
|
+
client;
|
|
2925
|
+
trustBase;
|
|
2926
|
+
signingService;
|
|
2927
|
+
devMode;
|
|
2928
|
+
constructor(config) {
|
|
2929
|
+
this.client = config.stateTransitionClient;
|
|
2930
|
+
this.trustBase = config.trustBase;
|
|
2931
|
+
this.signingService = config.signingService;
|
|
2932
|
+
this.devMode = config.devMode ?? false;
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
2936
|
+
*
|
|
2937
|
+
* Critical path (~2.3s):
|
|
2938
|
+
* 1. Create and submit burn commitment
|
|
2939
|
+
* 2. Wait for burn proof
|
|
2940
|
+
* 3. Create mint commitments with SplitMintReason
|
|
2941
|
+
* 4. Create transfer commitment (no mint proof needed)
|
|
2942
|
+
* 5. Send bundle via transport
|
|
2943
|
+
*
|
|
2944
|
+
* @param tokenToSplit - The SDK token to split
|
|
2945
|
+
* @param splitAmount - Amount to send to recipient
|
|
2946
|
+
* @param remainderAmount - Amount to keep as change
|
|
2947
|
+
* @param coinIdHex - Coin ID in hex format
|
|
2948
|
+
* @param recipientAddress - Recipient's address (PROXY or DIRECT)
|
|
2949
|
+
* @param transport - Transport provider for sending the bundle
|
|
2950
|
+
* @param recipientPubkey - Recipient's transport public key
|
|
2951
|
+
* @param options - Optional configuration
|
|
2952
|
+
* @returns InstantSplitResult with success status and timing info
|
|
2953
|
+
*/
|
|
2954
|
+
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
2955
|
+
const startTime = performance.now();
|
|
2956
|
+
const splitGroupId = crypto.randomUUID();
|
|
2957
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
2958
|
+
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
2959
|
+
try {
|
|
2960
|
+
const coinId = new import_CoinId3.CoinId(fromHex2(coinIdHex));
|
|
2961
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
2962
|
+
const recipientTokenId = new import_TokenId3.TokenId(await sha2563(seedString));
|
|
2963
|
+
const senderTokenId = new import_TokenId3.TokenId(await sha2563(seedString + "_sender"));
|
|
2964
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
2965
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
2966
|
+
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
2967
|
+
tokenToSplit.type,
|
|
2968
|
+
this.signingService.algorithm,
|
|
2969
|
+
this.signingService.publicKey,
|
|
2970
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
2971
|
+
);
|
|
2972
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
2973
|
+
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
2974
|
+
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
2975
|
+
builder.createToken(
|
|
2976
|
+
recipientTokenId,
|
|
2977
|
+
tokenToSplit.type,
|
|
2978
|
+
new Uint8Array(0),
|
|
2979
|
+
coinDataA,
|
|
2980
|
+
senderAddress,
|
|
2981
|
+
// Mint to sender first, then transfer
|
|
2982
|
+
recipientSalt,
|
|
2983
|
+
null
|
|
2984
|
+
);
|
|
2985
|
+
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
2986
|
+
builder.createToken(
|
|
2987
|
+
senderTokenId,
|
|
2988
|
+
tokenToSplit.type,
|
|
2989
|
+
new Uint8Array(0),
|
|
2990
|
+
coinDataB,
|
|
2991
|
+
senderAddress,
|
|
2992
|
+
senderSalt,
|
|
2993
|
+
null
|
|
2994
|
+
);
|
|
2995
|
+
const split = await builder.build(tokenToSplit);
|
|
2996
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
2997
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
2998
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
2999
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3000
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3001
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3002
|
+
}
|
|
3003
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3004
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3005
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3006
|
+
const burnDuration = performance.now() - startTime;
|
|
3007
|
+
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3008
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3009
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3010
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3011
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3012
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3013
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3014
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3015
|
+
);
|
|
3016
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3017
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3018
|
+
);
|
|
3019
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3020
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3021
|
+
}
|
|
3022
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3023
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3024
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3025
|
+
recipientMintCommitment.transactionData,
|
|
3026
|
+
recipientAddress,
|
|
3027
|
+
transferSalt,
|
|
3028
|
+
this.signingService
|
|
3029
|
+
);
|
|
3030
|
+
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3031
|
+
recipientTokenId,
|
|
3032
|
+
tokenToSplit.type,
|
|
3033
|
+
this.signingService,
|
|
3034
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3035
|
+
recipientSalt
|
|
3036
|
+
);
|
|
3037
|
+
const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
|
|
3038
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3039
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3040
|
+
let nametagTokenJson;
|
|
3041
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3042
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3043
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3044
|
+
}
|
|
3045
|
+
const bundle = {
|
|
3046
|
+
version: "5.0",
|
|
3047
|
+
type: "INSTANT_SPLIT",
|
|
3048
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3049
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3050
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3051
|
+
amount: splitAmount.toString(),
|
|
3052
|
+
coinId: coinIdHex,
|
|
3053
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3054
|
+
splitGroupId,
|
|
3055
|
+
senderPubkey,
|
|
3056
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3057
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3058
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3059
|
+
finalRecipientStateJson: "",
|
|
3060
|
+
// Recipient creates their own
|
|
3061
|
+
recipientAddressJson: recipientAddressStr,
|
|
3062
|
+
nametagTokenJson
|
|
3063
|
+
};
|
|
3064
|
+
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3065
|
+
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3066
|
+
token: JSON.stringify(bundle),
|
|
3067
|
+
proof: null,
|
|
3068
|
+
// Proof is included in the bundle
|
|
3069
|
+
memo: "INSTANT_SPLIT_V5",
|
|
3070
|
+
sender: {
|
|
3071
|
+
transportPubkey: senderPubkey
|
|
3072
|
+
}
|
|
3073
|
+
});
|
|
3074
|
+
const criticalPathDuration = performance.now() - startTime;
|
|
3075
|
+
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3076
|
+
options?.onNostrDelivered?.(nostrEventId);
|
|
3077
|
+
if (!options?.skipBackground) {
|
|
3078
|
+
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3079
|
+
signingService: this.signingService,
|
|
3080
|
+
tokenType: tokenToSplit.type,
|
|
3081
|
+
coinId,
|
|
3082
|
+
senderTokenId,
|
|
3083
|
+
senderSalt,
|
|
3084
|
+
onProgress: options?.onBackgroundProgress,
|
|
3085
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3086
|
+
onStorageSync: options?.onStorageSync
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
return {
|
|
3090
|
+
success: true,
|
|
3091
|
+
nostrEventId,
|
|
3092
|
+
splitGroupId,
|
|
3093
|
+
criticalPathDurationMs: criticalPathDuration,
|
|
3094
|
+
backgroundStarted: !options?.skipBackground
|
|
3095
|
+
};
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
const duration = performance.now() - startTime;
|
|
3098
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3099
|
+
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3100
|
+
return {
|
|
3101
|
+
success: false,
|
|
3102
|
+
splitGroupId,
|
|
3103
|
+
criticalPathDurationMs: duration,
|
|
3104
|
+
error: errorMessage,
|
|
3105
|
+
backgroundStarted: false
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Create a TransferCommitment from MintTransactionData WITHOUT waiting for mint proof.
|
|
3111
|
+
*
|
|
3112
|
+
* Key insight: TransferCommitment.create() only needs token.state and token.nametagTokens.
|
|
3113
|
+
* It does NOT need the genesis transaction or mint proof.
|
|
3114
|
+
*/
|
|
3115
|
+
async createTransferCommitmentFromMintData(mintData, recipientAddress, transferSalt, signingService, nametagTokens) {
|
|
3116
|
+
const predicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3117
|
+
mintData.tokenId,
|
|
3118
|
+
mintData.tokenType,
|
|
3119
|
+
signingService,
|
|
3120
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3121
|
+
mintData.salt
|
|
3122
|
+
);
|
|
3123
|
+
const state = new import_TokenState3.TokenState(predicate, null);
|
|
3124
|
+
const minimalToken = {
|
|
3125
|
+
state,
|
|
3126
|
+
nametagTokens: nametagTokens || [],
|
|
3127
|
+
id: mintData.tokenId,
|
|
3128
|
+
type: mintData.tokenType
|
|
3129
|
+
};
|
|
3130
|
+
const transferCommitment = await import_TransferCommitment2.TransferCommitment.create(
|
|
3131
|
+
minimalToken,
|
|
3132
|
+
recipientAddress,
|
|
3133
|
+
transferSalt,
|
|
3134
|
+
null,
|
|
3135
|
+
// recipientData
|
|
3136
|
+
null,
|
|
3137
|
+
// recipientDataHash
|
|
3138
|
+
signingService
|
|
3139
|
+
);
|
|
3140
|
+
return transferCommitment;
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* V5 background submission.
|
|
3144
|
+
*
|
|
3145
|
+
* Submits mint commitments to aggregator in PARALLEL after transport delivery.
|
|
3146
|
+
* Then waits for sender's mint proof, reconstructs change token, and saves it.
|
|
3147
|
+
*/
|
|
3148
|
+
submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, context) {
|
|
3149
|
+
console.log("[InstantSplit] Background: Starting parallel mint submission...");
|
|
3150
|
+
const startTime = performance.now();
|
|
3151
|
+
const submissions = Promise.all([
|
|
3152
|
+
this.client.submitMintCommitment(senderMintCommitment).then((res) => ({ type: "senderMint", status: res.status })).catch((err) => ({ type: "senderMint", status: "ERROR", error: err })),
|
|
3153
|
+
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3154
|
+
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3155
|
+
]);
|
|
3156
|
+
submissions.then(async (results) => {
|
|
3157
|
+
const submitDuration = performance.now() - startTime;
|
|
3158
|
+
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3159
|
+
context.onProgress?.({
|
|
3160
|
+
stage: "MINTS_SUBMITTED",
|
|
3161
|
+
message: `All commitments submitted in ${submitDuration.toFixed(0)}ms`
|
|
3162
|
+
});
|
|
3163
|
+
const senderMintResult = results.find((r) => r.type === "senderMint");
|
|
3164
|
+
if (senderMintResult?.status !== "SUCCESS" && senderMintResult?.status !== "REQUEST_ID_EXISTS") {
|
|
3165
|
+
console.error("[InstantSplit] Background: Sender mint failed - cannot save change token");
|
|
3166
|
+
context.onProgress?.({
|
|
3167
|
+
stage: "FAILED",
|
|
3168
|
+
message: "Sender mint submission failed",
|
|
3169
|
+
error: String(senderMintResult?.error)
|
|
3170
|
+
});
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
console.log("[InstantSplit] Background: Waiting for sender mint proof...");
|
|
3174
|
+
const proofStartTime = performance.now();
|
|
3175
|
+
try {
|
|
3176
|
+
const senderMintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(senderMintCommitment) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, senderMintCommitment);
|
|
3177
|
+
const proofDuration = performance.now() - proofStartTime;
|
|
3178
|
+
console.log(`[InstantSplit] Background: Sender mint proof received in ${proofDuration.toFixed(0)}ms`);
|
|
3179
|
+
context.onProgress?.({
|
|
3180
|
+
stage: "MINTS_PROVEN",
|
|
3181
|
+
message: `Mint proof received in ${proofDuration.toFixed(0)}ms`
|
|
3182
|
+
});
|
|
3183
|
+
const mintTransaction = senderMintCommitment.toTransaction(senderMintProof);
|
|
3184
|
+
const predicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3185
|
+
context.senderTokenId,
|
|
3186
|
+
context.tokenType,
|
|
3187
|
+
context.signingService,
|
|
3188
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3189
|
+
context.senderSalt
|
|
3190
|
+
);
|
|
3191
|
+
const state = new import_TokenState3.TokenState(predicate, null);
|
|
3192
|
+
const changeToken = await import_Token4.Token.mint(this.trustBase, state, mintTransaction);
|
|
3193
|
+
if (!this.devMode) {
|
|
3194
|
+
const verification = await changeToken.verify(this.trustBase);
|
|
3195
|
+
if (!verification.isSuccessful) {
|
|
3196
|
+
throw new Error(`Change token verification failed`);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
console.log("[InstantSplit] Background: Change token created");
|
|
3200
|
+
context.onProgress?.({
|
|
3201
|
+
stage: "CHANGE_TOKEN_SAVED",
|
|
3202
|
+
message: "Change token created and verified"
|
|
3203
|
+
});
|
|
3204
|
+
if (context.onChangeTokenCreated) {
|
|
3205
|
+
await context.onChangeTokenCreated(changeToken);
|
|
3206
|
+
console.log("[InstantSplit] Background: Change token saved");
|
|
3207
|
+
}
|
|
3208
|
+
if (context.onStorageSync) {
|
|
3209
|
+
try {
|
|
3210
|
+
const syncSuccess = await context.onStorageSync();
|
|
3211
|
+
console.log(`[InstantSplit] Background: Storage sync ${syncSuccess ? "completed" : "deferred"}`);
|
|
3212
|
+
context.onProgress?.({
|
|
3213
|
+
stage: "STORAGE_SYNCED",
|
|
3214
|
+
message: syncSuccess ? "Storage synchronized" : "Sync deferred"
|
|
3215
|
+
});
|
|
3216
|
+
} catch (syncError) {
|
|
3217
|
+
console.warn("[InstantSplit] Background: Storage sync error:", syncError);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
const totalDuration = performance.now() - startTime;
|
|
3221
|
+
console.log(`[InstantSplit] Background: Complete in ${totalDuration.toFixed(0)}ms`);
|
|
3222
|
+
context.onProgress?.({
|
|
3223
|
+
stage: "COMPLETED",
|
|
3224
|
+
message: `Background processing complete in ${totalDuration.toFixed(0)}ms`
|
|
3225
|
+
});
|
|
3226
|
+
} catch (proofError) {
|
|
3227
|
+
console.error("[InstantSplit] Background: Failed to get sender mint proof:", proofError);
|
|
3228
|
+
context.onProgress?.({
|
|
3229
|
+
stage: "FAILED",
|
|
3230
|
+
message: "Failed to get mint proof",
|
|
3231
|
+
error: String(proofError)
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
}).catch((err) => {
|
|
3235
|
+
console.error("[InstantSplit] Background: Submission batch failed:", err);
|
|
3236
|
+
context.onProgress?.({
|
|
3237
|
+
stage: "FAILED",
|
|
3238
|
+
message: "Background submission failed",
|
|
3239
|
+
error: String(err)
|
|
3240
|
+
});
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3243
|
+
/**
|
|
3244
|
+
* Dev mode bypass for waitInclusionProof.
|
|
3245
|
+
* In dev mode, we create a mock proof for testing.
|
|
3246
|
+
*/
|
|
3247
|
+
async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
|
|
3248
|
+
if (this.devMode) {
|
|
3249
|
+
try {
|
|
3250
|
+
return await Promise.race([
|
|
3251
|
+
(0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, commitment),
|
|
3252
|
+
new Promise(
|
|
3253
|
+
(_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
|
|
3254
|
+
)
|
|
3255
|
+
]);
|
|
3256
|
+
} catch {
|
|
3257
|
+
console.log("[InstantSplit] Dev mode: Using mock proof");
|
|
3258
|
+
return {
|
|
3259
|
+
toJSON: () => ({ mock: true })
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
return (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, commitment);
|
|
3264
|
+
}
|
|
3265
|
+
};
|
|
3266
|
+
|
|
3267
|
+
// modules/payments/InstantSplitProcessor.ts
|
|
3268
|
+
var import_Token5 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
3269
|
+
var import_TokenState4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
3270
|
+
var import_TokenType2 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
3271
|
+
var import_HashAlgorithm4 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
3272
|
+
var import_UnmaskedPredicate4 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
3273
|
+
var import_TransferCommitment3 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
|
|
2921
3274
|
var import_TransferTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction");
|
|
3275
|
+
var import_MintCommitment2 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment");
|
|
3276
|
+
var import_MintTransactionData2 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
|
|
3277
|
+
var import_InclusionProofUtils4 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
3278
|
+
|
|
3279
|
+
// types/instant-split.ts
|
|
3280
|
+
function isInstantSplitBundle(obj) {
|
|
3281
|
+
if (typeof obj !== "object" || obj === null) {
|
|
3282
|
+
return false;
|
|
3283
|
+
}
|
|
3284
|
+
const bundle = obj;
|
|
3285
|
+
if (bundle.type !== "INSTANT_SPLIT") return false;
|
|
3286
|
+
if (typeof bundle.recipientMintData !== "string") return false;
|
|
3287
|
+
if (typeof bundle.transferCommitment !== "string") return false;
|
|
3288
|
+
if (typeof bundle.amount !== "string") return false;
|
|
3289
|
+
if (typeof bundle.coinId !== "string") return false;
|
|
3290
|
+
if (typeof bundle.splitGroupId !== "string") return false;
|
|
3291
|
+
if (typeof bundle.senderPubkey !== "string") return false;
|
|
3292
|
+
if (typeof bundle.recipientSaltHex !== "string") return false;
|
|
3293
|
+
if (typeof bundle.transferSaltHex !== "string") return false;
|
|
3294
|
+
if (bundle.version === "4.0") {
|
|
3295
|
+
return typeof bundle.burnCommitment === "string";
|
|
3296
|
+
} else if (bundle.version === "5.0") {
|
|
3297
|
+
return typeof bundle.burnTransaction === "string" && typeof bundle.mintedTokenStateJson === "string" && typeof bundle.finalRecipientStateJson === "string" && typeof bundle.recipientAddressJson === "string";
|
|
3298
|
+
}
|
|
3299
|
+
return false;
|
|
3300
|
+
}
|
|
3301
|
+
function isInstantSplitBundleV4(obj) {
|
|
3302
|
+
return isInstantSplitBundle(obj) && obj.version === "4.0";
|
|
3303
|
+
}
|
|
3304
|
+
function isInstantSplitBundleV5(obj) {
|
|
3305
|
+
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// modules/payments/InstantSplitProcessor.ts
|
|
3309
|
+
function fromHex3(hex) {
|
|
3310
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3311
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3312
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
3313
|
+
}
|
|
3314
|
+
return bytes;
|
|
3315
|
+
}
|
|
3316
|
+
var InstantSplitProcessor = class {
|
|
3317
|
+
client;
|
|
3318
|
+
trustBase;
|
|
3319
|
+
devMode;
|
|
3320
|
+
constructor(config) {
|
|
3321
|
+
this.client = config.stateTransitionClient;
|
|
3322
|
+
this.trustBase = config.trustBase;
|
|
3323
|
+
this.devMode = config.devMode ?? false;
|
|
3324
|
+
}
|
|
3325
|
+
/**
|
|
3326
|
+
* Process a received INSTANT_SPLIT bundle.
|
|
3327
|
+
*
|
|
3328
|
+
* @param bundle - The received bundle (V4 or V5)
|
|
3329
|
+
* @param signingService - Recipient's signing service
|
|
3330
|
+
* @param senderPubkey - Sender's public key (for verification)
|
|
3331
|
+
* @param options - Processing options
|
|
3332
|
+
* @returns Processing result with finalized token if successful
|
|
3333
|
+
*/
|
|
3334
|
+
async processReceivedBundle(bundle, signingService, senderPubkey, options) {
|
|
3335
|
+
if (isInstantSplitBundleV5(bundle)) {
|
|
3336
|
+
return this.processV5Bundle(bundle, signingService, senderPubkey, options);
|
|
3337
|
+
} else if (isInstantSplitBundleV4(bundle)) {
|
|
3338
|
+
return this.processV4Bundle(bundle, signingService, senderPubkey, options);
|
|
3339
|
+
}
|
|
3340
|
+
return {
|
|
3341
|
+
success: false,
|
|
3342
|
+
error: `Unknown bundle version: ${bundle.version}`,
|
|
3343
|
+
durationMs: 0
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
/**
|
|
3347
|
+
* Process a V5 bundle (production mode).
|
|
3348
|
+
*
|
|
3349
|
+
* V5 Flow:
|
|
3350
|
+
* 1. Burn transaction already has proof (just validate)
|
|
3351
|
+
* 2. Submit mint commitment -> wait for proof
|
|
3352
|
+
* 3. Reconstruct minted token (use sender's state from bundle)
|
|
3353
|
+
* 4. Submit transfer commitment -> wait for proof
|
|
3354
|
+
* 5. Create recipient's final state and finalize token
|
|
3355
|
+
*/
|
|
3356
|
+
async processV5Bundle(bundle, signingService, senderPubkey, options) {
|
|
3357
|
+
console.log("[InstantSplitProcessor] Processing V5 bundle...");
|
|
3358
|
+
const startTime = performance.now();
|
|
3359
|
+
try {
|
|
3360
|
+
if (bundle.senderPubkey !== senderPubkey) {
|
|
3361
|
+
console.warn("[InstantSplitProcessor] Sender pubkey mismatch (non-fatal)");
|
|
3362
|
+
}
|
|
3363
|
+
const burnTxJson = JSON.parse(bundle.burnTransaction);
|
|
3364
|
+
const burnTransaction = await import_TransferTransaction.TransferTransaction.fromJSON(burnTxJson);
|
|
3365
|
+
console.log("[InstantSplitProcessor] Burn transaction validated");
|
|
3366
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
3367
|
+
const mintData = await import_MintTransactionData2.MintTransactionData.fromJSON(mintDataJson);
|
|
3368
|
+
const mintCommitment = await import_MintCommitment2.MintCommitment.create(mintData);
|
|
3369
|
+
console.log("[InstantSplitProcessor] Mint commitment recreated");
|
|
3370
|
+
const mintResponse = await this.client.submitMintCommitment(mintCommitment);
|
|
3371
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3372
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
3373
|
+
}
|
|
3374
|
+
console.log(`[InstantSplitProcessor] Mint submitted: ${mintResponse.status}`);
|
|
3375
|
+
const mintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(mintCommitment, options?.proofTimeoutMs) : await (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, mintCommitment);
|
|
3376
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
3377
|
+
console.log("[InstantSplitProcessor] Mint proof received");
|
|
3378
|
+
const tokenType = new import_TokenType2.TokenType(fromHex3(bundle.tokenTypeHex));
|
|
3379
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
3380
|
+
const tokenJson = {
|
|
3381
|
+
version: "2.0",
|
|
3382
|
+
state: senderMintedStateJson,
|
|
3383
|
+
genesis: mintTransaction.toJSON(),
|
|
3384
|
+
transactions: [],
|
|
3385
|
+
nametags: []
|
|
3386
|
+
};
|
|
3387
|
+
const mintedToken = await import_Token5.Token.fromJSON(tokenJson);
|
|
3388
|
+
console.log("[InstantSplitProcessor] Minted token reconstructed from sender state");
|
|
3389
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
3390
|
+
const transferCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
3391
|
+
const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
|
|
3392
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3393
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
3394
|
+
}
|
|
3395
|
+
console.log(`[InstantSplitProcessor] Transfer submitted: ${transferResponse.status}`);
|
|
3396
|
+
const transferProof = this.devMode ? await this.waitInclusionProofWithDevBypass(transferCommitment, options?.proofTimeoutMs) : await (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, transferCommitment);
|
|
3397
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
3398
|
+
console.log("[InstantSplitProcessor] Transfer proof received");
|
|
3399
|
+
const transferSalt = fromHex3(bundle.transferSaltHex);
|
|
3400
|
+
const finalRecipientPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
|
|
3401
|
+
mintData.tokenId,
|
|
3402
|
+
tokenType,
|
|
3403
|
+
signingService,
|
|
3404
|
+
import_HashAlgorithm4.HashAlgorithm.SHA256,
|
|
3405
|
+
transferSalt
|
|
3406
|
+
);
|
|
3407
|
+
const finalRecipientState = new import_TokenState4.TokenState(finalRecipientPredicate, null);
|
|
3408
|
+
console.log("[InstantSplitProcessor] Final recipient state created");
|
|
3409
|
+
let nametagTokens = [];
|
|
3410
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
3411
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
3412
|
+
console.log("[InstantSplitProcessor] PROXY address detected, finding nametag token...");
|
|
3413
|
+
if (bundle.nametagTokenJson) {
|
|
3414
|
+
try {
|
|
3415
|
+
const nametagToken = await import_Token5.Token.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
3416
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
3417
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
3418
|
+
if (proxy.address !== recipientAddressStr) {
|
|
3419
|
+
console.warn("[InstantSplitProcessor] Nametag PROXY address mismatch, ignoring bundle token");
|
|
3420
|
+
} else {
|
|
3421
|
+
nametagTokens = [nametagToken];
|
|
3422
|
+
console.log("[InstantSplitProcessor] Using nametag token from bundle (address validated)");
|
|
3423
|
+
}
|
|
3424
|
+
} catch (err) {
|
|
3425
|
+
console.warn("[InstantSplitProcessor] Failed to parse nametag token from bundle:", err);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
if (nametagTokens.length === 0 && options?.findNametagToken) {
|
|
3429
|
+
const token = await options.findNametagToken(recipientAddressStr);
|
|
3430
|
+
if (token) {
|
|
3431
|
+
nametagTokens = [token];
|
|
3432
|
+
console.log("[InstantSplitProcessor] Found nametag token via callback");
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
if (nametagTokens.length === 0 && !this.devMode) {
|
|
3436
|
+
throw new Error(
|
|
3437
|
+
`PROXY address transfer requires nametag token for verification. Address: ${recipientAddressStr}`
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
let finalToken;
|
|
3442
|
+
if (this.devMode) {
|
|
3443
|
+
console.log("[InstantSplitProcessor] Dev mode: finalizing without verification");
|
|
3444
|
+
const tokenJson2 = mintedToken.toJSON();
|
|
3445
|
+
tokenJson2.state = finalRecipientState.toJSON();
|
|
3446
|
+
tokenJson2.transactions = [transferTransaction.toJSON()];
|
|
3447
|
+
finalToken = await import_Token5.Token.fromJSON(tokenJson2);
|
|
3448
|
+
} else {
|
|
3449
|
+
finalToken = await this.client.finalizeTransaction(
|
|
3450
|
+
this.trustBase,
|
|
3451
|
+
mintedToken,
|
|
3452
|
+
finalRecipientState,
|
|
3453
|
+
transferTransaction,
|
|
3454
|
+
nametagTokens
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3457
|
+
console.log("[InstantSplitProcessor] Token finalized");
|
|
3458
|
+
if (!this.devMode) {
|
|
3459
|
+
const verification = await finalToken.verify(this.trustBase);
|
|
3460
|
+
if (!verification.isSuccessful) {
|
|
3461
|
+
throw new Error(`Token verification failed`);
|
|
3462
|
+
}
|
|
3463
|
+
console.log("[InstantSplitProcessor] Token verified");
|
|
3464
|
+
}
|
|
3465
|
+
const duration = performance.now() - startTime;
|
|
3466
|
+
console.log(`[InstantSplitProcessor] V5 bundle processed in ${duration.toFixed(0)}ms`);
|
|
3467
|
+
return {
|
|
3468
|
+
success: true,
|
|
3469
|
+
token: finalToken,
|
|
3470
|
+
durationMs: duration
|
|
3471
|
+
};
|
|
3472
|
+
} catch (error) {
|
|
3473
|
+
const duration = performance.now() - startTime;
|
|
3474
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3475
|
+
console.error(`[InstantSplitProcessor] V5 processing failed:`, error);
|
|
3476
|
+
return {
|
|
3477
|
+
success: false,
|
|
3478
|
+
error: errorMessage,
|
|
3479
|
+
durationMs: duration
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* Process a V4 bundle (dev mode only).
|
|
3485
|
+
*
|
|
3486
|
+
* V4 Flow:
|
|
3487
|
+
* 1. Submit burn commitment -> wait for proof
|
|
3488
|
+
* 2. Submit mint commitment -> wait for proof
|
|
3489
|
+
* 3. Reconstruct minted token
|
|
3490
|
+
* 4. Submit transfer commitment -> wait for proof
|
|
3491
|
+
* 5. Finalize token
|
|
3492
|
+
*/
|
|
3493
|
+
async processV4Bundle(bundle, signingService, _senderPubkey, options) {
|
|
3494
|
+
if (!this.devMode) {
|
|
3495
|
+
return {
|
|
3496
|
+
success: false,
|
|
3497
|
+
error: "INSTANT_SPLIT V4 is only supported in dev mode",
|
|
3498
|
+
durationMs: 0
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
console.log("[InstantSplitProcessor] Processing V4 bundle (dev mode)...");
|
|
3502
|
+
const startTime = performance.now();
|
|
3503
|
+
try {
|
|
3504
|
+
const burnCommitmentJson = JSON.parse(bundle.burnCommitment);
|
|
3505
|
+
const burnCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(burnCommitmentJson);
|
|
3506
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3507
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3508
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3509
|
+
}
|
|
3510
|
+
await this.waitInclusionProofWithDevBypass(burnCommitment, options?.proofTimeoutMs);
|
|
3511
|
+
console.log("[InstantSplitProcessor] V4: Burn proof received");
|
|
3512
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
3513
|
+
const mintData = await import_MintTransactionData2.MintTransactionData.fromJSON(mintDataJson);
|
|
3514
|
+
const mintCommitment = await import_MintCommitment2.MintCommitment.create(mintData);
|
|
3515
|
+
const mintResponse = await this.client.submitMintCommitment(mintCommitment);
|
|
3516
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3517
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
3518
|
+
}
|
|
3519
|
+
const mintProof = await this.waitInclusionProofWithDevBypass(
|
|
3520
|
+
mintCommitment,
|
|
3521
|
+
options?.proofTimeoutMs
|
|
3522
|
+
);
|
|
3523
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
3524
|
+
console.log("[InstantSplitProcessor] V4: Mint proof received");
|
|
3525
|
+
const tokenType = new import_TokenType2.TokenType(fromHex3(bundle.tokenTypeHex));
|
|
3526
|
+
const recipientSalt = fromHex3(bundle.recipientSaltHex);
|
|
3527
|
+
const recipientPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
|
|
3528
|
+
mintData.tokenId,
|
|
3529
|
+
tokenType,
|
|
3530
|
+
signingService,
|
|
3531
|
+
import_HashAlgorithm4.HashAlgorithm.SHA256,
|
|
3532
|
+
recipientSalt
|
|
3533
|
+
);
|
|
3534
|
+
const recipientState = new import_TokenState4.TokenState(recipientPredicate, null);
|
|
3535
|
+
const tokenJson = {
|
|
3536
|
+
version: "2.0",
|
|
3537
|
+
state: recipientState.toJSON(),
|
|
3538
|
+
genesis: mintTransaction.toJSON(),
|
|
3539
|
+
transactions: [],
|
|
3540
|
+
nametags: []
|
|
3541
|
+
};
|
|
3542
|
+
const mintedToken = await import_Token5.Token.fromJSON(tokenJson);
|
|
3543
|
+
console.log("[InstantSplitProcessor] V4: Minted token reconstructed");
|
|
3544
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
3545
|
+
const transferCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
3546
|
+
const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
|
|
3547
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3548
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
3549
|
+
}
|
|
3550
|
+
const transferProof = await this.waitInclusionProofWithDevBypass(
|
|
3551
|
+
transferCommitment,
|
|
3552
|
+
options?.proofTimeoutMs
|
|
3553
|
+
);
|
|
3554
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
3555
|
+
console.log("[InstantSplitProcessor] V4: Transfer proof received");
|
|
3556
|
+
const transferSalt = fromHex3(bundle.transferSaltHex);
|
|
3557
|
+
const finalPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
|
|
3558
|
+
mintData.tokenId,
|
|
3559
|
+
tokenType,
|
|
3560
|
+
signingService,
|
|
3561
|
+
import_HashAlgorithm4.HashAlgorithm.SHA256,
|
|
3562
|
+
transferSalt
|
|
3563
|
+
);
|
|
3564
|
+
const finalState = new import_TokenState4.TokenState(finalPredicate, null);
|
|
3565
|
+
const finalTokenJson = mintedToken.toJSON();
|
|
3566
|
+
finalTokenJson.state = finalState.toJSON();
|
|
3567
|
+
finalTokenJson.transactions = [transferTransaction.toJSON()];
|
|
3568
|
+
const finalToken = await import_Token5.Token.fromJSON(finalTokenJson);
|
|
3569
|
+
console.log("[InstantSplitProcessor] V4: Token finalized");
|
|
3570
|
+
const duration = performance.now() - startTime;
|
|
3571
|
+
console.log(`[InstantSplitProcessor] V4 bundle processed in ${duration.toFixed(0)}ms`);
|
|
3572
|
+
return {
|
|
3573
|
+
success: true,
|
|
3574
|
+
token: finalToken,
|
|
3575
|
+
durationMs: duration
|
|
3576
|
+
};
|
|
3577
|
+
} catch (error) {
|
|
3578
|
+
const duration = performance.now() - startTime;
|
|
3579
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3580
|
+
console.error(`[InstantSplitProcessor] V4 processing failed:`, error);
|
|
3581
|
+
return {
|
|
3582
|
+
success: false,
|
|
3583
|
+
error: errorMessage,
|
|
3584
|
+
durationMs: duration
|
|
3585
|
+
};
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Dev mode bypass for waitInclusionProof.
|
|
3590
|
+
*/
|
|
3591
|
+
async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
|
|
3592
|
+
if (this.devMode) {
|
|
3593
|
+
try {
|
|
3594
|
+
return await Promise.race([
|
|
3595
|
+
(0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, commitment),
|
|
3596
|
+
new Promise(
|
|
3597
|
+
(_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
|
|
3598
|
+
)
|
|
3599
|
+
]);
|
|
3600
|
+
} catch {
|
|
3601
|
+
console.log("[InstantSplitProcessor] Dev mode: Using mock proof");
|
|
3602
|
+
return {
|
|
3603
|
+
toJSON: () => ({ mock: true })
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
return (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, commitment);
|
|
3608
|
+
}
|
|
3609
|
+
};
|
|
3610
|
+
|
|
3611
|
+
// modules/payments/PaymentsModule.ts
|
|
3612
|
+
var import_Token6 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
3613
|
+
var import_CoinId4 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
|
|
3614
|
+
var import_TransferCommitment4 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
|
|
3615
|
+
var import_TransferTransaction2 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction");
|
|
2922
3616
|
var import_SigningService = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
|
|
2923
3617
|
var import_AddressScheme = require("@unicitylabs/state-transition-sdk/lib/address/AddressScheme");
|
|
2924
|
-
var
|
|
2925
|
-
var
|
|
2926
|
-
var
|
|
3618
|
+
var import_UnmaskedPredicate5 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
3619
|
+
var import_TokenState5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
3620
|
+
var import_HashAlgorithm5 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
2927
3621
|
function enrichWithRegistry(info) {
|
|
2928
3622
|
const registry = TokenRegistry.getInstance();
|
|
2929
3623
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -2949,7 +3643,7 @@ async function parseTokenInfo(tokenData) {
|
|
|
2949
3643
|
try {
|
|
2950
3644
|
const data = typeof tokenData === "string" ? JSON.parse(tokenData) : tokenData;
|
|
2951
3645
|
try {
|
|
2952
|
-
const sdkToken = await
|
|
3646
|
+
const sdkToken = await import_Token6.Token.fromJSON(data);
|
|
2953
3647
|
if (sdkToken.id) {
|
|
2954
3648
|
defaultInfo.tokenId = sdkToken.id.toString();
|
|
2955
3649
|
}
|
|
@@ -2962,7 +3656,7 @@ async function parseTokenInfo(tokenData) {
|
|
|
2962
3656
|
if (Array.isArray(firstCoin) && firstCoin.length === 2) {
|
|
2963
3657
|
[coinIdObj, amount] = firstCoin;
|
|
2964
3658
|
}
|
|
2965
|
-
if (coinIdObj instanceof
|
|
3659
|
+
if (coinIdObj instanceof import_CoinId4.CoinId) {
|
|
2966
3660
|
const coinIdHex = coinIdObj.toJSON();
|
|
2967
3661
|
return enrichWithRegistry({
|
|
2968
3662
|
coinId: coinIdHex,
|
|
@@ -3095,21 +3789,48 @@ function extractStateHashFromSdkData(sdkData) {
|
|
|
3095
3789
|
if (!sdkData) return "";
|
|
3096
3790
|
try {
|
|
3097
3791
|
const txf = JSON.parse(sdkData);
|
|
3098
|
-
|
|
3792
|
+
const stateHash = getCurrentStateHash(txf);
|
|
3793
|
+
if (!stateHash) {
|
|
3794
|
+
if (txf.state?.hash) {
|
|
3795
|
+
return txf.state.hash;
|
|
3796
|
+
}
|
|
3797
|
+
if (txf.stateHash) {
|
|
3798
|
+
return txf.stateHash;
|
|
3799
|
+
}
|
|
3800
|
+
if (txf.currentStateHash) {
|
|
3801
|
+
return txf.currentStateHash;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
return stateHash || "";
|
|
3099
3805
|
} catch {
|
|
3100
3806
|
return "";
|
|
3101
3807
|
}
|
|
3102
3808
|
}
|
|
3103
|
-
function
|
|
3104
|
-
|
|
3809
|
+
function createTokenStateKey(tokenId, stateHash) {
|
|
3810
|
+
return `${tokenId}_${stateHash}`;
|
|
3811
|
+
}
|
|
3812
|
+
function extractTokenStateKey(token) {
|
|
3813
|
+
const tokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
3814
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
3815
|
+
if (!tokenId || !stateHash) return null;
|
|
3816
|
+
return createTokenStateKey(tokenId, stateHash);
|
|
3817
|
+
}
|
|
3818
|
+
function hasSameGenesisTokenId(t1, t2) {
|
|
3105
3819
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
3106
3820
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
3107
3821
|
return !!(id1 && id2 && id1 === id2);
|
|
3108
3822
|
}
|
|
3823
|
+
function isSameTokenState(t1, t2) {
|
|
3824
|
+
const key1 = extractTokenStateKey(t1);
|
|
3825
|
+
const key2 = extractTokenStateKey(t2);
|
|
3826
|
+
return !!(key1 && key2 && key1 === key2);
|
|
3827
|
+
}
|
|
3109
3828
|
function createTombstoneFromToken(token) {
|
|
3110
3829
|
const tokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
3111
|
-
if (!tokenId) return null;
|
|
3112
3830
|
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
3831
|
+
if (!tokenId || !stateHash) {
|
|
3832
|
+
return null;
|
|
3833
|
+
}
|
|
3113
3834
|
return {
|
|
3114
3835
|
tokenId,
|
|
3115
3836
|
stateHash,
|
|
@@ -3175,7 +3896,7 @@ function findBestTokenVersion(tokenId, archivedTokens, forkedTokens) {
|
|
|
3175
3896
|
candidates.sort((a, b) => countCommittedTxns(b) - countCommittedTxns(a));
|
|
3176
3897
|
return candidates[0];
|
|
3177
3898
|
}
|
|
3178
|
-
var PaymentsModule = class {
|
|
3899
|
+
var PaymentsModule = class _PaymentsModule {
|
|
3179
3900
|
moduleConfig;
|
|
3180
3901
|
deps = null;
|
|
3181
3902
|
/** L1 (ALPHA blockchain) payments sub-module (null if disabled) */
|
|
@@ -3200,6 +3921,13 @@ var PaymentsModule = class {
|
|
|
3200
3921
|
unsubscribeTransfers = null;
|
|
3201
3922
|
unsubscribePaymentRequests = null;
|
|
3202
3923
|
unsubscribePaymentRequestResponses = null;
|
|
3924
|
+
// NOSTR-FIRST proof polling (background proof verification)
|
|
3925
|
+
proofPollingJobs = /* @__PURE__ */ new Map();
|
|
3926
|
+
proofPollingInterval = null;
|
|
3927
|
+
static PROOF_POLLING_INTERVAL_MS = 2e3;
|
|
3928
|
+
// Poll every 2s
|
|
3929
|
+
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
3930
|
+
// Max 30 attempts (~60s)
|
|
3203
3931
|
constructor(config) {
|
|
3204
3932
|
this.moduleConfig = {
|
|
3205
3933
|
autoSync: config?.autoSync ?? true,
|
|
@@ -3215,6 +3943,8 @@ var PaymentsModule = class {
|
|
|
3215
3943
|
getConfig() {
|
|
3216
3944
|
return this.moduleConfig;
|
|
3217
3945
|
}
|
|
3946
|
+
/** Price provider (optional) */
|
|
3947
|
+
priceProvider = null;
|
|
3218
3948
|
log(...args) {
|
|
3219
3949
|
if (this.moduleConfig.debug) {
|
|
3220
3950
|
console.log("[PaymentsModule]", ...args);
|
|
@@ -3227,7 +3957,21 @@ var PaymentsModule = class {
|
|
|
3227
3957
|
* Initialize module with dependencies
|
|
3228
3958
|
*/
|
|
3229
3959
|
initialize(deps) {
|
|
3960
|
+
this.unsubscribeTransfers?.();
|
|
3961
|
+
this.unsubscribeTransfers = null;
|
|
3962
|
+
this.unsubscribePaymentRequests?.();
|
|
3963
|
+
this.unsubscribePaymentRequests = null;
|
|
3964
|
+
this.unsubscribePaymentRequestResponses?.();
|
|
3965
|
+
this.unsubscribePaymentRequestResponses = null;
|
|
3966
|
+
this.tokens.clear();
|
|
3967
|
+
this.pendingTransfers.clear();
|
|
3968
|
+
this.tombstones = [];
|
|
3969
|
+
this.archivedTokens.clear();
|
|
3970
|
+
this.forkedTokens.clear();
|
|
3971
|
+
this.transactionHistory = [];
|
|
3972
|
+
this.nametag = null;
|
|
3230
3973
|
this.deps = deps;
|
|
3974
|
+
this.priceProvider = deps.price ?? null;
|
|
3231
3975
|
if (this.l1) {
|
|
3232
3976
|
this.l1.initialize({
|
|
3233
3977
|
identity: deps.identity,
|
|
@@ -3298,6 +4042,8 @@ var PaymentsModule = class {
|
|
|
3298
4042
|
this.unsubscribePaymentRequestResponses = null;
|
|
3299
4043
|
this.paymentRequestHandlers.clear();
|
|
3300
4044
|
this.paymentRequestResponseHandlers.clear();
|
|
4045
|
+
this.stopProofPolling();
|
|
4046
|
+
this.proofPollingJobs.clear();
|
|
3301
4047
|
for (const [, resolver] of this.pendingResponseResolvers) {
|
|
3302
4048
|
clearTimeout(resolver.timeout);
|
|
3303
4049
|
resolver.reject(new Error("Module destroyed"));
|
|
@@ -3322,8 +4068,9 @@ var PaymentsModule = class {
|
|
|
3322
4068
|
tokens: []
|
|
3323
4069
|
};
|
|
3324
4070
|
try {
|
|
3325
|
-
const
|
|
3326
|
-
const
|
|
4071
|
+
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
4072
|
+
const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
|
|
4073
|
+
const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
|
|
3327
4074
|
const signingService = await this.createSigningService();
|
|
3328
4075
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
3329
4076
|
if (!stClient) {
|
|
@@ -3343,7 +4090,6 @@ var PaymentsModule = class {
|
|
|
3343
4090
|
if (!splitPlan) {
|
|
3344
4091
|
throw new Error("Insufficient balance");
|
|
3345
4092
|
}
|
|
3346
|
-
this.log(`Split plan: requiresSplit=${splitPlan.requiresSplit}, directTokens=${splitPlan.tokensToTransferDirectly.length}`);
|
|
3347
4093
|
const tokensToSend = splitPlan.tokensToTransferDirectly.map((t) => t.uiToken);
|
|
3348
4094
|
if (splitPlan.tokenToSplit) {
|
|
3349
4095
|
tokensToSend.push(splitPlan.tokenToSplit.uiToken);
|
|
@@ -3386,11 +4132,13 @@ var PaymentsModule = class {
|
|
|
3386
4132
|
};
|
|
3387
4133
|
await this.addToken(changeToken, true);
|
|
3388
4134
|
this.log(`Change token saved: ${changeToken.id}, amount: ${changeToken.amount}`);
|
|
4135
|
+
console.log(`[Payments] Sending split token to ${recipientPubkey.slice(0, 8)}... via Nostr`);
|
|
3389
4136
|
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
3390
4137
|
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
3391
4138
|
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
3392
4139
|
memo: request.memo
|
|
3393
4140
|
});
|
|
4141
|
+
console.log(`[Payments] Split token sent successfully`);
|
|
3394
4142
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
3395
4143
|
result.txHash = "split-" + Date.now().toString(16);
|
|
3396
4144
|
this.log(`Split transfer completed`);
|
|
@@ -3409,11 +4157,13 @@ var PaymentsModule = class {
|
|
|
3409
4157
|
const transferTx = commitment.toTransaction(inclusionProof);
|
|
3410
4158
|
const requestIdBytes = commitment.requestId;
|
|
3411
4159
|
result.txHash = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4160
|
+
console.log(`[Payments] Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}... via Nostr`);
|
|
3412
4161
|
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
3413
4162
|
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
3414
4163
|
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
3415
4164
|
memo: request.memo
|
|
3416
4165
|
});
|
|
4166
|
+
console.log(`[Payments] Direct token sent successfully`);
|
|
3417
4167
|
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
3418
4168
|
await this.removeToken(token.id, recipientNametag);
|
|
3419
4169
|
}
|
|
@@ -3467,6 +4217,234 @@ var PaymentsModule = class {
|
|
|
3467
4217
|
return TokenRegistry.getInstance().getIconUrl(coinId) ?? void 0;
|
|
3468
4218
|
}
|
|
3469
4219
|
// ===========================================================================
|
|
4220
|
+
// Public API - Instant Split (V5 Optimized)
|
|
4221
|
+
// ===========================================================================
|
|
4222
|
+
/**
|
|
4223
|
+
* Send tokens using INSTANT_SPLIT V5 optimized flow.
|
|
4224
|
+
*
|
|
4225
|
+
* This achieves ~2.3s critical path latency instead of ~42s by:
|
|
4226
|
+
* 1. Waiting only for burn proof (required)
|
|
4227
|
+
* 2. Creating transfer commitment from mint data (no mint proof needed)
|
|
4228
|
+
* 3. Sending bundle via Nostr immediately
|
|
4229
|
+
* 4. Processing mints in background
|
|
4230
|
+
*
|
|
4231
|
+
* @param request - Transfer request with recipient, amount, and coinId
|
|
4232
|
+
* @param options - Optional instant split configuration
|
|
4233
|
+
* @returns InstantSplitResult with timing info
|
|
4234
|
+
*/
|
|
4235
|
+
async sendInstant(request, options) {
|
|
4236
|
+
this.ensureInitialized();
|
|
4237
|
+
const startTime = performance.now();
|
|
4238
|
+
try {
|
|
4239
|
+
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
4240
|
+
const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
|
|
4241
|
+
const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
|
|
4242
|
+
const signingService = await this.createSigningService();
|
|
4243
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
4244
|
+
if (!stClient) {
|
|
4245
|
+
throw new Error("State transition client not available");
|
|
4246
|
+
}
|
|
4247
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
4248
|
+
if (!trustBase) {
|
|
4249
|
+
throw new Error("Trust base not available");
|
|
4250
|
+
}
|
|
4251
|
+
const calculator = new TokenSplitCalculator();
|
|
4252
|
+
const availableTokens = Array.from(this.tokens.values());
|
|
4253
|
+
const splitPlan = await calculator.calculateOptimalSplit(
|
|
4254
|
+
availableTokens,
|
|
4255
|
+
BigInt(request.amount),
|
|
4256
|
+
request.coinId
|
|
4257
|
+
);
|
|
4258
|
+
if (!splitPlan) {
|
|
4259
|
+
throw new Error("Insufficient balance");
|
|
4260
|
+
}
|
|
4261
|
+
if (!splitPlan.requiresSplit || !splitPlan.tokenToSplit) {
|
|
4262
|
+
this.log("No split required, falling back to standard send()");
|
|
4263
|
+
const result2 = await this.send(request);
|
|
4264
|
+
return {
|
|
4265
|
+
success: result2.status === "completed",
|
|
4266
|
+
criticalPathDurationMs: performance.now() - startTime,
|
|
4267
|
+
error: result2.error
|
|
4268
|
+
};
|
|
4269
|
+
}
|
|
4270
|
+
this.log(`InstantSplit: amount=${splitPlan.splitAmount}, remainder=${splitPlan.remainderAmount}`);
|
|
4271
|
+
const tokenToSplit = splitPlan.tokenToSplit.uiToken;
|
|
4272
|
+
tokenToSplit.status = "transferring";
|
|
4273
|
+
this.tokens.set(tokenToSplit.id, tokenToSplit);
|
|
4274
|
+
const devMode = options?.devMode ?? this.deps.oracle.isDevMode?.() ?? false;
|
|
4275
|
+
const executor = new InstantSplitExecutor({
|
|
4276
|
+
stateTransitionClient: stClient,
|
|
4277
|
+
trustBase,
|
|
4278
|
+
signingService,
|
|
4279
|
+
devMode
|
|
4280
|
+
});
|
|
4281
|
+
const result = await executor.executeSplitInstant(
|
|
4282
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4283
|
+
splitPlan.splitAmount,
|
|
4284
|
+
splitPlan.remainderAmount,
|
|
4285
|
+
splitPlan.coinId,
|
|
4286
|
+
recipientAddress,
|
|
4287
|
+
this.deps.transport,
|
|
4288
|
+
recipientPubkey,
|
|
4289
|
+
{
|
|
4290
|
+
...options,
|
|
4291
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4292
|
+
const changeTokenData = changeToken.toJSON();
|
|
4293
|
+
const uiToken = {
|
|
4294
|
+
id: crypto.randomUUID(),
|
|
4295
|
+
coinId: request.coinId,
|
|
4296
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4297
|
+
name: this.getCoinName(request.coinId),
|
|
4298
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4299
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4300
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4301
|
+
status: "confirmed",
|
|
4302
|
+
createdAt: Date.now(),
|
|
4303
|
+
updatedAt: Date.now(),
|
|
4304
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4305
|
+
};
|
|
4306
|
+
await this.addToken(uiToken, true);
|
|
4307
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4308
|
+
},
|
|
4309
|
+
onStorageSync: async () => {
|
|
4310
|
+
await this.save();
|
|
4311
|
+
return true;
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
);
|
|
4315
|
+
if (result.success) {
|
|
4316
|
+
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4317
|
+
await this.removeToken(tokenToSplit.id, recipientNametag);
|
|
4318
|
+
await this.addToHistory({
|
|
4319
|
+
type: "SENT",
|
|
4320
|
+
amount: request.amount,
|
|
4321
|
+
coinId: request.coinId,
|
|
4322
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4323
|
+
timestamp: Date.now(),
|
|
4324
|
+
recipientNametag
|
|
4325
|
+
});
|
|
4326
|
+
await this.save();
|
|
4327
|
+
} else {
|
|
4328
|
+
tokenToSplit.status = "confirmed";
|
|
4329
|
+
this.tokens.set(tokenToSplit.id, tokenToSplit);
|
|
4330
|
+
}
|
|
4331
|
+
return result;
|
|
4332
|
+
} catch (error) {
|
|
4333
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4334
|
+
return {
|
|
4335
|
+
success: false,
|
|
4336
|
+
criticalPathDurationMs: performance.now() - startTime,
|
|
4337
|
+
error: errorMessage
|
|
4338
|
+
};
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
4342
|
+
* Process a received INSTANT_SPLIT bundle.
|
|
4343
|
+
*
|
|
4344
|
+
* This should be called when receiving an instant split bundle via transport.
|
|
4345
|
+
* It handles the recipient-side processing:
|
|
4346
|
+
* 1. Validate burn transaction
|
|
4347
|
+
* 2. Submit and wait for mint proof
|
|
4348
|
+
* 3. Submit and wait for transfer proof
|
|
4349
|
+
* 4. Finalize and save the token
|
|
4350
|
+
*
|
|
4351
|
+
* @param bundle - The received InstantSplitBundle (V4 or V5)
|
|
4352
|
+
* @param senderPubkey - Sender's public key for verification
|
|
4353
|
+
* @returns Processing result with finalized token
|
|
4354
|
+
*/
|
|
4355
|
+
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4356
|
+
this.ensureInitialized();
|
|
4357
|
+
try {
|
|
4358
|
+
const signingService = await this.createSigningService();
|
|
4359
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
4360
|
+
if (!stClient) {
|
|
4361
|
+
throw new Error("State transition client not available");
|
|
4362
|
+
}
|
|
4363
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
4364
|
+
if (!trustBase) {
|
|
4365
|
+
throw new Error("Trust base not available");
|
|
4366
|
+
}
|
|
4367
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4368
|
+
const processor = new InstantSplitProcessor({
|
|
4369
|
+
stateTransitionClient: stClient,
|
|
4370
|
+
trustBase,
|
|
4371
|
+
devMode
|
|
4372
|
+
});
|
|
4373
|
+
const result = await processor.processReceivedBundle(
|
|
4374
|
+
bundle,
|
|
4375
|
+
signingService,
|
|
4376
|
+
senderPubkey,
|
|
4377
|
+
{
|
|
4378
|
+
findNametagToken: async (proxyAddress) => {
|
|
4379
|
+
if (this.nametag?.token) {
|
|
4380
|
+
try {
|
|
4381
|
+
const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
|
|
4382
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
4383
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
4384
|
+
if (proxy.address === proxyAddress) {
|
|
4385
|
+
return nametagToken;
|
|
4386
|
+
}
|
|
4387
|
+
this.log(`Nametag PROXY address mismatch: ${proxy.address} !== ${proxyAddress}`);
|
|
4388
|
+
return null;
|
|
4389
|
+
} catch (err) {
|
|
4390
|
+
this.log("Failed to parse nametag token:", err);
|
|
4391
|
+
return null;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
return null;
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
);
|
|
4398
|
+
if (result.success && result.token) {
|
|
4399
|
+
const tokenData = result.token.toJSON();
|
|
4400
|
+
const info = await parseTokenInfo(tokenData);
|
|
4401
|
+
const uiToken = {
|
|
4402
|
+
id: crypto.randomUUID(),
|
|
4403
|
+
coinId: info.coinId,
|
|
4404
|
+
symbol: info.symbol,
|
|
4405
|
+
name: info.name,
|
|
4406
|
+
decimals: info.decimals,
|
|
4407
|
+
iconUrl: info.iconUrl,
|
|
4408
|
+
amount: bundle.amount,
|
|
4409
|
+
status: "confirmed",
|
|
4410
|
+
createdAt: Date.now(),
|
|
4411
|
+
updatedAt: Date.now(),
|
|
4412
|
+
sdkData: JSON.stringify(tokenData)
|
|
4413
|
+
};
|
|
4414
|
+
await this.addToken(uiToken);
|
|
4415
|
+
await this.addToHistory({
|
|
4416
|
+
type: "RECEIVED",
|
|
4417
|
+
amount: bundle.amount,
|
|
4418
|
+
coinId: info.coinId,
|
|
4419
|
+
symbol: info.symbol,
|
|
4420
|
+
timestamp: Date.now(),
|
|
4421
|
+
senderPubkey
|
|
4422
|
+
});
|
|
4423
|
+
await this.save();
|
|
4424
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
4425
|
+
id: bundle.splitGroupId,
|
|
4426
|
+
senderPubkey,
|
|
4427
|
+
tokens: [uiToken],
|
|
4428
|
+
receivedAt: Date.now()
|
|
4429
|
+
});
|
|
4430
|
+
}
|
|
4431
|
+
return result;
|
|
4432
|
+
} catch (error) {
|
|
4433
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4434
|
+
return {
|
|
4435
|
+
success: false,
|
|
4436
|
+
error: errorMessage,
|
|
4437
|
+
durationMs: 0
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Check if a payload is an instant split bundle
|
|
4443
|
+
*/
|
|
4444
|
+
isInstantSplitBundle(payload) {
|
|
4445
|
+
return isInstantSplitBundle(payload);
|
|
4446
|
+
}
|
|
4447
|
+
// ===========================================================================
|
|
3470
4448
|
// Public API - Payment Requests
|
|
3471
4449
|
// ===========================================================================
|
|
3472
4450
|
/**
|
|
@@ -3484,7 +4462,8 @@ var PaymentsModule = class {
|
|
|
3484
4462
|
};
|
|
3485
4463
|
}
|
|
3486
4464
|
try {
|
|
3487
|
-
const
|
|
4465
|
+
const peerInfo = await this.deps.transport.resolve?.(recipientPubkeyOrNametag) ?? null;
|
|
4466
|
+
const recipientPubkey = this.resolveTransportPubkey(recipientPubkeyOrNametag, peerInfo);
|
|
3488
4467
|
const payload = {
|
|
3489
4468
|
amount: request.amount,
|
|
3490
4469
|
coinId: request.coinId,
|
|
@@ -3788,47 +4767,46 @@ var PaymentsModule = class {
|
|
|
3788
4767
|
// Public API - Balance & Tokens
|
|
3789
4768
|
// ===========================================================================
|
|
3790
4769
|
/**
|
|
3791
|
-
*
|
|
4770
|
+
* Set or update price provider
|
|
3792
4771
|
*/
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
});
|
|
4772
|
+
setPriceProvider(provider) {
|
|
4773
|
+
this.priceProvider = provider;
|
|
4774
|
+
}
|
|
4775
|
+
/**
|
|
4776
|
+
* Get total portfolio value in USD
|
|
4777
|
+
* Returns null if PriceProvider is not configured
|
|
4778
|
+
*/
|
|
4779
|
+
async getBalance() {
|
|
4780
|
+
const assets = await this.getAssets();
|
|
4781
|
+
if (!this.priceProvider) {
|
|
4782
|
+
return null;
|
|
4783
|
+
}
|
|
4784
|
+
let total = 0;
|
|
4785
|
+
let hasAnyPrice = false;
|
|
4786
|
+
for (const asset of assets) {
|
|
4787
|
+
if (asset.fiatValueUsd != null) {
|
|
4788
|
+
total += asset.fiatValueUsd;
|
|
4789
|
+
hasAnyPrice = true;
|
|
3812
4790
|
}
|
|
3813
4791
|
}
|
|
3814
|
-
return
|
|
4792
|
+
return hasAnyPrice ? total : null;
|
|
3815
4793
|
}
|
|
3816
4794
|
/**
|
|
3817
|
-
* Get aggregated assets (tokens grouped by coinId)
|
|
4795
|
+
* Get aggregated assets (tokens grouped by coinId) with price data
|
|
3818
4796
|
* Only includes confirmed tokens
|
|
3819
4797
|
*/
|
|
3820
|
-
getAssets(coinId) {
|
|
3821
|
-
const
|
|
4798
|
+
async getAssets(coinId) {
|
|
4799
|
+
const assetsMap = /* @__PURE__ */ new Map();
|
|
3822
4800
|
for (const token of this.tokens.values()) {
|
|
3823
4801
|
if (token.status !== "confirmed") continue;
|
|
3824
4802
|
if (coinId && token.coinId !== coinId) continue;
|
|
3825
4803
|
const key = token.coinId;
|
|
3826
|
-
const existing =
|
|
4804
|
+
const existing = assetsMap.get(key);
|
|
3827
4805
|
if (existing) {
|
|
3828
4806
|
existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
|
|
3829
4807
|
existing.tokenCount++;
|
|
3830
4808
|
} else {
|
|
3831
|
-
|
|
4809
|
+
assetsMap.set(key, {
|
|
3832
4810
|
coinId: token.coinId,
|
|
3833
4811
|
symbol: token.symbol,
|
|
3834
4812
|
name: token.name,
|
|
@@ -3839,7 +4817,66 @@ var PaymentsModule = class {
|
|
|
3839
4817
|
});
|
|
3840
4818
|
}
|
|
3841
4819
|
}
|
|
3842
|
-
|
|
4820
|
+
const rawAssets = Array.from(assetsMap.values());
|
|
4821
|
+
let priceMap = null;
|
|
4822
|
+
if (this.priceProvider && rawAssets.length > 0) {
|
|
4823
|
+
const registry = TokenRegistry.getInstance();
|
|
4824
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
4825
|
+
for (const asset of rawAssets) {
|
|
4826
|
+
const def = registry.getDefinition(asset.coinId);
|
|
4827
|
+
if (def?.name) {
|
|
4828
|
+
const existing = nameToCoins.get(def.name);
|
|
4829
|
+
if (existing) {
|
|
4830
|
+
existing.push(asset.coinId);
|
|
4831
|
+
} else {
|
|
4832
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
if (nameToCoins.size > 0) {
|
|
4837
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
4838
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
4839
|
+
priceMap = /* @__PURE__ */ new Map();
|
|
4840
|
+
for (const [name, coinIds] of nameToCoins) {
|
|
4841
|
+
const price = prices.get(name);
|
|
4842
|
+
if (price) {
|
|
4843
|
+
for (const cid of coinIds) {
|
|
4844
|
+
priceMap.set(cid, {
|
|
4845
|
+
priceUsd: price.priceUsd,
|
|
4846
|
+
priceEur: price.priceEur,
|
|
4847
|
+
change24h: price.change24h
|
|
4848
|
+
});
|
|
4849
|
+
}
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
return rawAssets.map((raw) => {
|
|
4855
|
+
const price = priceMap?.get(raw.coinId);
|
|
4856
|
+
let fiatValueUsd = null;
|
|
4857
|
+
let fiatValueEur = null;
|
|
4858
|
+
if (price) {
|
|
4859
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
4860
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
4861
|
+
if (price.priceEur != null) {
|
|
4862
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
return {
|
|
4866
|
+
coinId: raw.coinId,
|
|
4867
|
+
symbol: raw.symbol,
|
|
4868
|
+
name: raw.name,
|
|
4869
|
+
decimals: raw.decimals,
|
|
4870
|
+
iconUrl: raw.iconUrl,
|
|
4871
|
+
totalAmount: raw.totalAmount,
|
|
4872
|
+
tokenCount: raw.tokenCount,
|
|
4873
|
+
priceUsd: price?.priceUsd ?? null,
|
|
4874
|
+
priceEur: price?.priceEur ?? null,
|
|
4875
|
+
change24h: price?.change24h ?? null,
|
|
4876
|
+
fiatValueUsd,
|
|
4877
|
+
fiatValueEur
|
|
4878
|
+
};
|
|
4879
|
+
});
|
|
3843
4880
|
}
|
|
3844
4881
|
/**
|
|
3845
4882
|
* Get all tokens
|
|
@@ -3865,14 +4902,52 @@ var PaymentsModule = class {
|
|
|
3865
4902
|
// ===========================================================================
|
|
3866
4903
|
/**
|
|
3867
4904
|
* Add a token
|
|
3868
|
-
*
|
|
4905
|
+
* Tokens are uniquely identified by (tokenId, stateHash) composite key.
|
|
4906
|
+
* Multiple historic states of the same token can coexist.
|
|
4907
|
+
* @returns false if exact duplicate (same tokenId AND same stateHash)
|
|
3869
4908
|
*/
|
|
3870
4909
|
async addToken(token, skipHistory = false) {
|
|
3871
4910
|
this.ensureInitialized();
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
4911
|
+
const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
4912
|
+
const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
4913
|
+
const incomingStateKey = incomingTokenId && incomingStateHash ? createTokenStateKey(incomingTokenId, incomingStateHash) : null;
|
|
4914
|
+
if (incomingTokenId && incomingStateHash && this.isStateTombstoned(incomingTokenId, incomingStateHash)) {
|
|
4915
|
+
this.log(`Rejecting tombstoned token: ${incomingTokenId.slice(0, 8)}..._${incomingStateHash.slice(0, 8)}...`);
|
|
4916
|
+
return false;
|
|
4917
|
+
}
|
|
4918
|
+
if (incomingStateKey) {
|
|
4919
|
+
for (const [existingId, existing] of this.tokens) {
|
|
4920
|
+
if (isSameTokenState(existing, token)) {
|
|
4921
|
+
this.log(`Duplicate token state ignored: ${incomingTokenId?.slice(0, 8)}..._${incomingStateHash?.slice(0, 8)}...`);
|
|
4922
|
+
return false;
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
for (const [existingId, existing] of this.tokens) {
|
|
4927
|
+
if (hasSameGenesisTokenId(existing, token)) {
|
|
4928
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
4929
|
+
if (incomingStateHash && existingStateHash && incomingStateHash === existingStateHash) {
|
|
4930
|
+
continue;
|
|
4931
|
+
}
|
|
4932
|
+
if (existing.status === "spent" || existing.status === "invalid") {
|
|
4933
|
+
this.log(`Replacing spent/invalid token ${incomingTokenId?.slice(0, 8)}...`);
|
|
4934
|
+
this.tokens.delete(existingId);
|
|
4935
|
+
break;
|
|
4936
|
+
}
|
|
4937
|
+
if (incomingStateHash && existingStateHash && incomingStateHash !== existingStateHash) {
|
|
4938
|
+
this.log(`Token ${incomingTokenId?.slice(0, 8)}... state updated: ${existingStateHash.slice(0, 8)}... -> ${incomingStateHash.slice(0, 8)}...`);
|
|
4939
|
+
await this.archiveToken(existing);
|
|
4940
|
+
this.tokens.delete(existingId);
|
|
4941
|
+
break;
|
|
4942
|
+
}
|
|
4943
|
+
if (!incomingStateHash || !existingStateHash) {
|
|
4944
|
+
if (existingId !== token.id) {
|
|
4945
|
+
this.log(`Token ${incomingTokenId?.slice(0, 8)}... .id changed, replacing`);
|
|
4946
|
+
await this.archiveToken(existing);
|
|
4947
|
+
this.tokens.delete(existingId);
|
|
4948
|
+
break;
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
3876
4951
|
}
|
|
3877
4952
|
}
|
|
3878
4953
|
this.tokens.set(token.id, token);
|
|
@@ -4027,8 +5102,10 @@ var PaymentsModule = class {
|
|
|
4027
5102
|
);
|
|
4028
5103
|
if (!alreadyTombstoned) {
|
|
4029
5104
|
this.tombstones.push(tombstone);
|
|
4030
|
-
this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}...`);
|
|
5105
|
+
this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}..._${tombstone.stateHash.slice(0, 8)}...`);
|
|
4031
5106
|
}
|
|
5107
|
+
} else {
|
|
5108
|
+
this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
|
|
4032
5109
|
}
|
|
4033
5110
|
this.tokens.delete(tokenId);
|
|
4034
5111
|
if (!skipHistory && token.coinId && token.amount) {
|
|
@@ -4346,15 +5423,15 @@ var PaymentsModule = class {
|
|
|
4346
5423
|
}
|
|
4347
5424
|
try {
|
|
4348
5425
|
const signingService = await this.createSigningService();
|
|
4349
|
-
const { UnmaskedPredicateReference:
|
|
4350
|
-
const { TokenType:
|
|
5426
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5427
|
+
const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
4351
5428
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
4352
|
-
const tokenType = new
|
|
4353
|
-
const addressRef = await
|
|
5429
|
+
const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5430
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
4354
5431
|
tokenType,
|
|
4355
5432
|
signingService.algorithm,
|
|
4356
5433
|
signingService.publicKey,
|
|
4357
|
-
|
|
5434
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256
|
|
4358
5435
|
);
|
|
4359
5436
|
const ownerAddress = await addressRef.toAddress();
|
|
4360
5437
|
const minter = new NametagMinter({
|
|
@@ -4521,40 +5598,22 @@ var PaymentsModule = class {
|
|
|
4521
5598
|
* Detect if a string is an L3 address (not a nametag)
|
|
4522
5599
|
* Returns true for: hex pubkeys (64+ chars), PROXY:, DIRECT: prefixed addresses
|
|
4523
5600
|
*/
|
|
4524
|
-
isL3Address(value) {
|
|
4525
|
-
if (value.startsWith("PROXY:") || value.startsWith("DIRECT:")) {
|
|
4526
|
-
return true;
|
|
4527
|
-
}
|
|
4528
|
-
if (value.length >= 64 && /^[0-9a-fA-F]+$/.test(value)) {
|
|
4529
|
-
return true;
|
|
4530
|
-
}
|
|
4531
|
-
return false;
|
|
4532
|
-
}
|
|
4533
5601
|
/**
|
|
4534
|
-
* Resolve recipient to
|
|
4535
|
-
*
|
|
5602
|
+
* Resolve recipient to transport pubkey for messaging.
|
|
5603
|
+
* Uses pre-resolved PeerInfo if available, otherwise resolves via transport.
|
|
4536
5604
|
*/
|
|
4537
|
-
|
|
4538
|
-
if (
|
|
4539
|
-
|
|
4540
|
-
const pubkey = await this.deps.transport.resolveNametag?.(nametag);
|
|
4541
|
-
if (!pubkey) {
|
|
4542
|
-
throw new Error(`Nametag not found: ${nametag}`);
|
|
4543
|
-
}
|
|
4544
|
-
return pubkey;
|
|
4545
|
-
}
|
|
4546
|
-
if (this.isL3Address(recipient)) {
|
|
4547
|
-
return recipient;
|
|
5605
|
+
resolveTransportPubkey(recipient, peerInfo) {
|
|
5606
|
+
if (peerInfo?.transportPubkey) {
|
|
5607
|
+
return peerInfo.transportPubkey;
|
|
4548
5608
|
}
|
|
4549
|
-
if (
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
this.log(`Resolved "${recipient}" as nametag to pubkey`);
|
|
4553
|
-
return pubkey;
|
|
5609
|
+
if (recipient.length >= 64 && /^[0-9a-fA-F]+$/.test(recipient)) {
|
|
5610
|
+
if (recipient.length === 66 && (recipient.startsWith("02") || recipient.startsWith("03"))) {
|
|
5611
|
+
return recipient.slice(2);
|
|
4554
5612
|
}
|
|
5613
|
+
return recipient;
|
|
4555
5614
|
}
|
|
4556
5615
|
throw new Error(
|
|
4557
|
-
`
|
|
5616
|
+
`Cannot resolve transport pubkey for "${recipient}". No binding event found. The recipient must publish their identity first.`
|
|
4558
5617
|
);
|
|
4559
5618
|
}
|
|
4560
5619
|
/**
|
|
@@ -4562,9 +5621,9 @@ var PaymentsModule = class {
|
|
|
4562
5621
|
*/
|
|
4563
5622
|
async createSdkCommitment(token, recipientAddress, signingService) {
|
|
4564
5623
|
const tokenData = token.sdkData ? typeof token.sdkData === "string" ? JSON.parse(token.sdkData) : token.sdkData : token;
|
|
4565
|
-
const sdkToken = await
|
|
5624
|
+
const sdkToken = await import_Token6.Token.fromJSON(tokenData);
|
|
4566
5625
|
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
4567
|
-
const commitment = await
|
|
5626
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.create(
|
|
4568
5627
|
sdkToken,
|
|
4569
5628
|
recipientAddress,
|
|
4570
5629
|
salt,
|
|
@@ -4590,75 +5649,264 @@ var PaymentsModule = class {
|
|
|
4590
5649
|
* Create DirectAddress from a public key using UnmaskedPredicateReference
|
|
4591
5650
|
*/
|
|
4592
5651
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
4593
|
-
const { UnmaskedPredicateReference:
|
|
4594
|
-
const { TokenType:
|
|
5652
|
+
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5653
|
+
const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
4595
5654
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
4596
|
-
const tokenType = new
|
|
5655
|
+
const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
4597
5656
|
const pubkeyBytes = new Uint8Array(
|
|
4598
5657
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
4599
5658
|
);
|
|
4600
|
-
const addressRef = await
|
|
5659
|
+
const addressRef = await UnmaskedPredicateReference4.create(
|
|
4601
5660
|
tokenType,
|
|
4602
5661
|
"secp256k1",
|
|
4603
5662
|
pubkeyBytes,
|
|
4604
|
-
|
|
5663
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256
|
|
4605
5664
|
);
|
|
4606
5665
|
return addressRef.toAddress();
|
|
4607
5666
|
}
|
|
4608
5667
|
/**
|
|
4609
|
-
* Resolve
|
|
4610
|
-
*
|
|
5668
|
+
* Resolve recipient to IAddress for L3 transfers.
|
|
5669
|
+
* Uses pre-resolved PeerInfo when available to avoid redundant network queries.
|
|
4611
5670
|
*/
|
|
4612
|
-
async
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
5671
|
+
async resolveRecipientAddress(recipient, addressMode = "auto", peerInfo) {
|
|
5672
|
+
const { AddressFactory } = await import("@unicitylabs/state-transition-sdk/lib/address/AddressFactory");
|
|
5673
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5674
|
+
if (recipient.startsWith("PROXY:") || recipient.startsWith("DIRECT:")) {
|
|
5675
|
+
return AddressFactory.createAddress(recipient);
|
|
5676
|
+
}
|
|
5677
|
+
if (recipient.length === 66 && /^[0-9a-fA-F]+$/.test(recipient)) {
|
|
5678
|
+
this.log(`Creating DirectAddress from 33-byte compressed pubkey`);
|
|
5679
|
+
return this.createDirectAddressFromPubkey(recipient);
|
|
4616
5680
|
}
|
|
4617
|
-
const info = await this.deps
|
|
5681
|
+
const info = peerInfo ?? await this.deps?.transport.resolve?.(recipient) ?? null;
|
|
4618
5682
|
if (!info) {
|
|
4619
|
-
|
|
4620
|
-
|
|
5683
|
+
throw new Error(
|
|
5684
|
+
`Recipient "${recipient}" not found. Use @nametag, a valid PROXY:/DIRECT: address, or a 33-byte hex pubkey.`
|
|
5685
|
+
);
|
|
4621
5686
|
}
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
5687
|
+
const nametag = recipient.startsWith("@") ? recipient.slice(1) : info.nametag || recipient;
|
|
5688
|
+
if (addressMode === "proxy") {
|
|
5689
|
+
console.log(`[Payments] Using PROXY address for "${nametag}" (forced)`);
|
|
5690
|
+
return ProxyAddress.fromNameTag(nametag);
|
|
5691
|
+
}
|
|
5692
|
+
if (addressMode === "direct") {
|
|
5693
|
+
if (!info.directAddress) {
|
|
5694
|
+
throw new Error(`"${nametag}" has no DirectAddress stored. It may be a legacy registration.`);
|
|
5695
|
+
}
|
|
5696
|
+
console.log(`[Payments] Using DirectAddress for "${nametag}" (forced): ${info.directAddress.slice(0, 30)}...`);
|
|
5697
|
+
return AddressFactory.createAddress(info.directAddress);
|
|
4625
5698
|
}
|
|
4626
|
-
|
|
5699
|
+
if (info.directAddress) {
|
|
5700
|
+
this.log(`Using DirectAddress for "${nametag}": ${info.directAddress.slice(0, 30)}...`);
|
|
5701
|
+
return AddressFactory.createAddress(info.directAddress);
|
|
5702
|
+
}
|
|
5703
|
+
this.log(`Using PROXY address for legacy nametag "${nametag}"`);
|
|
5704
|
+
return ProxyAddress.fromNameTag(nametag);
|
|
4627
5705
|
}
|
|
4628
5706
|
/**
|
|
4629
|
-
*
|
|
4630
|
-
*
|
|
5707
|
+
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
5708
|
+
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
5709
|
+
* We create the token as 'submitted', submit commitment (idempotent), and poll for proof.
|
|
4631
5710
|
*/
|
|
4632
|
-
async
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
return this.createDirectAddressFromPubkey(publicKey2);
|
|
5711
|
+
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
5712
|
+
try {
|
|
5713
|
+
const sourceTokenInput = typeof payload.sourceToken === "string" ? JSON.parse(payload.sourceToken) : payload.sourceToken;
|
|
5714
|
+
const commitmentInput = typeof payload.commitmentData === "string" ? JSON.parse(payload.commitmentData) : payload.commitmentData;
|
|
5715
|
+
if (!sourceTokenInput || !commitmentInput) {
|
|
5716
|
+
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
5717
|
+
return;
|
|
4640
5718
|
}
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
5719
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
5720
|
+
const token = {
|
|
5721
|
+
id: tokenInfo.tokenId ?? crypto.randomUUID(),
|
|
5722
|
+
coinId: tokenInfo.coinId,
|
|
5723
|
+
symbol: tokenInfo.symbol,
|
|
5724
|
+
name: tokenInfo.name,
|
|
5725
|
+
decimals: tokenInfo.decimals,
|
|
5726
|
+
iconUrl: tokenInfo.iconUrl,
|
|
5727
|
+
amount: tokenInfo.amount,
|
|
5728
|
+
status: "submitted",
|
|
5729
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
5730
|
+
createdAt: Date.now(),
|
|
5731
|
+
updatedAt: Date.now(),
|
|
5732
|
+
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
5733
|
+
};
|
|
5734
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5735
|
+
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
5736
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
5737
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
5738
|
+
return;
|
|
5739
|
+
}
|
|
5740
|
+
this.tokens.set(token.id, token);
|
|
5741
|
+
await this.save();
|
|
5742
|
+
this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
|
|
5743
|
+
const incomingTransfer = {
|
|
5744
|
+
id: transfer.id,
|
|
5745
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
5746
|
+
tokens: [token],
|
|
5747
|
+
memo: payload.memo,
|
|
5748
|
+
receivedAt: transfer.timestamp
|
|
5749
|
+
};
|
|
5750
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
5751
|
+
try {
|
|
5752
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
5753
|
+
const requestIdBytes = commitment.requestId;
|
|
5754
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5755
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5756
|
+
if (stClient) {
|
|
5757
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
5758
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
5759
|
+
}
|
|
5760
|
+
this.addProofPollingJob({
|
|
5761
|
+
tokenId: token.id,
|
|
5762
|
+
requestIdHex,
|
|
5763
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
5764
|
+
startedAt: Date.now(),
|
|
5765
|
+
attemptCount: 0,
|
|
5766
|
+
lastAttemptAt: 0,
|
|
5767
|
+
onProofReceived: async (tokenId) => {
|
|
5768
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
|
|
5769
|
+
}
|
|
5770
|
+
});
|
|
5771
|
+
} catch (err) {
|
|
5772
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
5773
|
+
}
|
|
5774
|
+
} catch (error) {
|
|
5775
|
+
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
4649
5776
|
}
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
5777
|
+
}
|
|
5778
|
+
/**
|
|
5779
|
+
* Shared finalization logic for received transfers.
|
|
5780
|
+
* Handles both PROXY (with nametag token + address validation) and DIRECT schemes.
|
|
5781
|
+
*/
|
|
5782
|
+
async finalizeTransferToken(sourceToken, transferTx, stClient, trustBase) {
|
|
5783
|
+
const recipientAddress = transferTx.data.recipient;
|
|
5784
|
+
const addressScheme = recipientAddress.scheme;
|
|
5785
|
+
const signingService = await this.createSigningService();
|
|
5786
|
+
const transferSalt = transferTx.data.salt;
|
|
5787
|
+
const recipientPredicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
|
|
5788
|
+
sourceToken.id,
|
|
5789
|
+
sourceToken.type,
|
|
5790
|
+
signingService,
|
|
5791
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256,
|
|
5792
|
+
transferSalt
|
|
5793
|
+
);
|
|
5794
|
+
const recipientState = new import_TokenState5.TokenState(recipientPredicate, null);
|
|
5795
|
+
let nametagTokens = [];
|
|
5796
|
+
if (addressScheme === import_AddressScheme.AddressScheme.PROXY) {
|
|
5797
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5798
|
+
if (!this.nametag?.token) {
|
|
5799
|
+
throw new Error("Cannot finalize PROXY transfer - no nametag token");
|
|
5800
|
+
}
|
|
5801
|
+
const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
|
|
5802
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5803
|
+
if (proxy.address !== recipientAddress.address) {
|
|
5804
|
+
throw new Error(
|
|
5805
|
+
`PROXY address mismatch: nametag resolves to ${proxy.address} but transfer targets ${recipientAddress.address}`
|
|
5806
|
+
);
|
|
5807
|
+
}
|
|
5808
|
+
nametagTokens = [nametagToken];
|
|
4654
5809
|
}
|
|
4655
|
-
|
|
4656
|
-
|
|
5810
|
+
return stClient.finalizeTransaction(
|
|
5811
|
+
trustBase,
|
|
5812
|
+
sourceToken,
|
|
5813
|
+
recipientState,
|
|
5814
|
+
transferTx,
|
|
5815
|
+
nametagTokens
|
|
4657
5816
|
);
|
|
4658
5817
|
}
|
|
5818
|
+
/**
|
|
5819
|
+
* Finalize a received token after proof is available
|
|
5820
|
+
*/
|
|
5821
|
+
async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
|
|
5822
|
+
try {
|
|
5823
|
+
const token = this.tokens.get(tokenId);
|
|
5824
|
+
if (!token) {
|
|
5825
|
+
this.log(`Token ${tokenId} not found for finalization`);
|
|
5826
|
+
return;
|
|
5827
|
+
}
|
|
5828
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
5829
|
+
if (!this.deps.oracle.waitForProofSdk) {
|
|
5830
|
+
this.log("Cannot finalize - no waitForProofSdk");
|
|
5831
|
+
token.status = "confirmed";
|
|
5832
|
+
token.updatedAt = Date.now();
|
|
5833
|
+
await this.save();
|
|
5834
|
+
return;
|
|
5835
|
+
}
|
|
5836
|
+
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
5837
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
5838
|
+
const sourceToken = await import_Token6.Token.fromJSON(sourceTokenInput);
|
|
5839
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5840
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5841
|
+
if (!stClient || !trustBase) {
|
|
5842
|
+
this.log("Cannot finalize - missing state transition client or trust base");
|
|
5843
|
+
token.status = "confirmed";
|
|
5844
|
+
token.updatedAt = Date.now();
|
|
5845
|
+
await this.save();
|
|
5846
|
+
return;
|
|
5847
|
+
}
|
|
5848
|
+
const finalizedSdkToken = await this.finalizeTransferToken(
|
|
5849
|
+
sourceToken,
|
|
5850
|
+
transferTx,
|
|
5851
|
+
stClient,
|
|
5852
|
+
trustBase
|
|
5853
|
+
);
|
|
5854
|
+
const finalizedToken = {
|
|
5855
|
+
...token,
|
|
5856
|
+
status: "confirmed",
|
|
5857
|
+
updatedAt: Date.now(),
|
|
5858
|
+
sdkData: JSON.stringify(finalizedSdkToken.toJSON())
|
|
5859
|
+
};
|
|
5860
|
+
this.tokens.set(tokenId, finalizedToken);
|
|
5861
|
+
await this.save();
|
|
5862
|
+
await this.saveTokenToFileStorage(finalizedToken);
|
|
5863
|
+
this.log(`NOSTR-FIRST: Token ${tokenId.slice(0, 8)}... finalized and confirmed`);
|
|
5864
|
+
this.deps.emitEvent("transfer:confirmed", {
|
|
5865
|
+
id: crypto.randomUUID(),
|
|
5866
|
+
status: "completed",
|
|
5867
|
+
tokens: [finalizedToken]
|
|
5868
|
+
});
|
|
5869
|
+
await this.addToHistory({
|
|
5870
|
+
type: "RECEIVED",
|
|
5871
|
+
amount: finalizedToken.amount,
|
|
5872
|
+
coinId: finalizedToken.coinId,
|
|
5873
|
+
symbol: finalizedToken.symbol,
|
|
5874
|
+
timestamp: Date.now(),
|
|
5875
|
+
senderPubkey
|
|
5876
|
+
});
|
|
5877
|
+
} catch (error) {
|
|
5878
|
+
console.error("[Payments] Failed to finalize received token:", error);
|
|
5879
|
+
const token = this.tokens.get(tokenId);
|
|
5880
|
+
if (token && token.status === "submitted") {
|
|
5881
|
+
token.status = "confirmed";
|
|
5882
|
+
token.updatedAt = Date.now();
|
|
5883
|
+
await this.save();
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
4659
5887
|
async handleIncomingTransfer(transfer) {
|
|
4660
5888
|
try {
|
|
4661
5889
|
const payload = transfer.payload;
|
|
5890
|
+
if (isInstantSplitBundle(payload)) {
|
|
5891
|
+
this.log("Processing INSTANT_SPLIT bundle...");
|
|
5892
|
+
try {
|
|
5893
|
+
if (!this.nametag) {
|
|
5894
|
+
await this.loadNametagFromFileStorage();
|
|
5895
|
+
}
|
|
5896
|
+
const result = await this.processInstantSplitBundle(
|
|
5897
|
+
payload,
|
|
5898
|
+
transfer.senderTransportPubkey
|
|
5899
|
+
);
|
|
5900
|
+
if (result.success) {
|
|
5901
|
+
this.log("INSTANT_SPLIT processed successfully");
|
|
5902
|
+
} else {
|
|
5903
|
+
console.warn("[Payments] INSTANT_SPLIT processing failed:", result.error);
|
|
5904
|
+
}
|
|
5905
|
+
} catch (err) {
|
|
5906
|
+
console.error("[Payments] INSTANT_SPLIT processing error:", err);
|
|
5907
|
+
}
|
|
5908
|
+
return;
|
|
5909
|
+
}
|
|
4662
5910
|
let tokenData;
|
|
4663
5911
|
let finalizedSdkToken = null;
|
|
4664
5912
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -4669,82 +5917,71 @@ var PaymentsModule = class {
|
|
|
4669
5917
|
console.warn("[Payments] Invalid Sphere wallet transfer format");
|
|
4670
5918
|
return;
|
|
4671
5919
|
}
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
5920
|
+
let sourceToken;
|
|
5921
|
+
let transferTx;
|
|
5922
|
+
try {
|
|
5923
|
+
sourceToken = await import_Token6.Token.fromJSON(sourceTokenInput);
|
|
5924
|
+
} catch (err) {
|
|
5925
|
+
console.error("[Payments] Failed to parse sourceToken:", err);
|
|
5926
|
+
return;
|
|
5927
|
+
}
|
|
5928
|
+
try {
|
|
5929
|
+
const hasInclusionProof = transferTxInput.inclusionProof !== void 0;
|
|
5930
|
+
const hasData = transferTxInput.data !== void 0;
|
|
5931
|
+
const hasTransactionData = transferTxInput.transactionData !== void 0;
|
|
5932
|
+
const hasAuthenticator = transferTxInput.authenticator !== void 0;
|
|
5933
|
+
if (hasData && hasInclusionProof) {
|
|
5934
|
+
transferTx = await import_TransferTransaction2.TransferTransaction.fromJSON(transferTxInput);
|
|
5935
|
+
} else if (hasTransactionData && hasAuthenticator) {
|
|
5936
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferTxInput);
|
|
5937
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5938
|
+
if (!stClient) {
|
|
5939
|
+
console.error("[Payments] Cannot process commitment - no state transition client");
|
|
5940
|
+
return;
|
|
5941
|
+
}
|
|
5942
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
5943
|
+
if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
|
|
5944
|
+
console.error("[Payments] Transfer commitment submission failed:", response.status);
|
|
5945
|
+
return;
|
|
5946
|
+
}
|
|
5947
|
+
if (!this.deps.oracle.waitForProofSdk) {
|
|
5948
|
+
console.error("[Payments] Cannot wait for proof - missing oracle method");
|
|
5949
|
+
return;
|
|
5950
|
+
}
|
|
5951
|
+
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
5952
|
+
transferTx = commitment.toTransaction(inclusionProof);
|
|
5953
|
+
} else {
|
|
4682
5954
|
try {
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
const
|
|
4686
|
-
const recipientPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
4687
|
-
sourceToken.id,
|
|
4688
|
-
sourceToken.type,
|
|
4689
|
-
signingService,
|
|
4690
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
4691
|
-
transferSalt
|
|
4692
|
-
);
|
|
4693
|
-
const recipientState = new import_TokenState3.TokenState(recipientPredicate, null);
|
|
5955
|
+
transferTx = await import_TransferTransaction2.TransferTransaction.fromJSON(transferTxInput);
|
|
5956
|
+
} catch {
|
|
5957
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferTxInput);
|
|
4694
5958
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
|
|
4698
|
-
return;
|
|
5959
|
+
if (!stClient || !this.deps.oracle.waitForProofSdk) {
|
|
5960
|
+
throw new Error("Cannot submit commitment - missing oracle methods");
|
|
4699
5961
|
}
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
recipientState,
|
|
4704
|
-
transferTx,
|
|
4705
|
-
[nametagToken]
|
|
4706
|
-
);
|
|
4707
|
-
tokenData = finalizedSdkToken.toJSON();
|
|
4708
|
-
this.log("Token finalized successfully");
|
|
4709
|
-
} catch (finalizeError) {
|
|
4710
|
-
console.error("[Payments] Finalization failed:", finalizeError);
|
|
4711
|
-
return;
|
|
5962
|
+
await stClient.submitTransferCommitment(commitment);
|
|
5963
|
+
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
5964
|
+
transferTx = commitment.toTransaction(inclusionProof);
|
|
4712
5965
|
}
|
|
4713
5966
|
}
|
|
4714
|
-
}
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
transferSalt
|
|
4725
|
-
);
|
|
4726
|
-
const recipientState = new import_TokenState3.TokenState(recipientPredicate, null);
|
|
4727
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
4728
|
-
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
4729
|
-
if (!stClient || !trustBase) {
|
|
4730
|
-
this.log("Cannot finalize DIRECT transfer - missing client, using source token");
|
|
4731
|
-
tokenData = sourceTokenInput;
|
|
4732
|
-
} else {
|
|
4733
|
-
finalizedSdkToken = await stClient.finalizeTransaction(
|
|
4734
|
-
trustBase,
|
|
4735
|
-
sourceToken,
|
|
4736
|
-
recipientState,
|
|
4737
|
-
transferTx,
|
|
4738
|
-
[]
|
|
4739
|
-
// No nametag tokens needed for DIRECT
|
|
4740
|
-
);
|
|
4741
|
-
tokenData = finalizedSdkToken.toJSON();
|
|
4742
|
-
this.log("DIRECT transfer finalized successfully");
|
|
4743
|
-
}
|
|
4744
|
-
} catch (finalizeError) {
|
|
4745
|
-
this.log("DIRECT finalization failed, using source token:", finalizeError);
|
|
4746
|
-
tokenData = sourceTokenInput;
|
|
5967
|
+
} catch (err) {
|
|
5968
|
+
console.error("[Payments] Failed to parse transferTx:", err);
|
|
5969
|
+
return;
|
|
5970
|
+
}
|
|
5971
|
+
try {
|
|
5972
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5973
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5974
|
+
if (!stClient || !trustBase) {
|
|
5975
|
+
console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
|
|
5976
|
+
return;
|
|
4747
5977
|
}
|
|
5978
|
+
finalizedSdkToken = await this.finalizeTransferToken(sourceToken, transferTx, stClient, trustBase);
|
|
5979
|
+
tokenData = finalizedSdkToken.toJSON();
|
|
5980
|
+
const addressScheme = transferTx.data.recipient.scheme;
|
|
5981
|
+
this.log(`${addressScheme === import_AddressScheme.AddressScheme.PROXY ? "PROXY" : "DIRECT"} finalization successful`);
|
|
5982
|
+
} catch (finalizeError) {
|
|
5983
|
+
console.error(`[Payments] Finalization FAILED - token rejected:`, finalizeError);
|
|
5984
|
+
return;
|
|
4748
5985
|
}
|
|
4749
5986
|
} else if (payload.token) {
|
|
4750
5987
|
tokenData = payload.token;
|
|
@@ -4771,12 +6008,6 @@ var PaymentsModule = class {
|
|
|
4771
6008
|
updatedAt: Date.now(),
|
|
4772
6009
|
sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
|
|
4773
6010
|
};
|
|
4774
|
-
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
4775
|
-
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
4776
|
-
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
4777
|
-
this.log(`Rejected tombstoned token ${sdkTokenId.slice(0, 8)}...`);
|
|
4778
|
-
return;
|
|
4779
|
-
}
|
|
4780
6011
|
await this.addToken(token);
|
|
4781
6012
|
const incomingTransfer = {
|
|
4782
6013
|
id: transfer.id,
|
|
@@ -4864,14 +6095,159 @@ var PaymentsModule = class {
|
|
|
4864
6095
|
}
|
|
4865
6096
|
loadFromStorageData(data) {
|
|
4866
6097
|
const parsed = parseTxfStorageData(data);
|
|
6098
|
+
this.tombstones = parsed.tombstones;
|
|
4867
6099
|
this.tokens.clear();
|
|
4868
6100
|
for (const token of parsed.tokens) {
|
|
6101
|
+
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
6102
|
+
const stateHash = extractStateHashFromSdkData(token.sdkData);
|
|
6103
|
+
if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
|
|
6104
|
+
this.log(`Skipping tombstoned token ${sdkTokenId.slice(0, 8)}... during load (exact state match)`);
|
|
6105
|
+
continue;
|
|
6106
|
+
}
|
|
4869
6107
|
this.tokens.set(token.id, token);
|
|
4870
6108
|
}
|
|
4871
|
-
this.tombstones = parsed.tombstones;
|
|
4872
6109
|
this.archivedTokens = parsed.archivedTokens;
|
|
4873
6110
|
this.forkedTokens = parsed.forkedTokens;
|
|
4874
|
-
|
|
6111
|
+
if (parsed.nametag !== null) {
|
|
6112
|
+
this.nametag = parsed.nametag;
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
// ===========================================================================
|
|
6116
|
+
// Private: NOSTR-FIRST Proof Polling
|
|
6117
|
+
// ===========================================================================
|
|
6118
|
+
/**
|
|
6119
|
+
* Submit commitment to aggregator and start background proof polling
|
|
6120
|
+
* (NOSTR-FIRST pattern: fire-and-forget submission)
|
|
6121
|
+
*/
|
|
6122
|
+
async submitAndPollForProof(tokenId, commitment, requestIdHex, onProofReceived) {
|
|
6123
|
+
try {
|
|
6124
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
6125
|
+
if (!stClient) {
|
|
6126
|
+
this.log("Cannot submit commitment - no state transition client");
|
|
6127
|
+
return;
|
|
6128
|
+
}
|
|
6129
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
6130
|
+
if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
|
|
6131
|
+
this.log(`Transfer commitment submission failed: ${response.status}`);
|
|
6132
|
+
const token = this.tokens.get(tokenId);
|
|
6133
|
+
if (token) {
|
|
6134
|
+
token.status = "invalid";
|
|
6135
|
+
token.updatedAt = Date.now();
|
|
6136
|
+
this.tokens.set(tokenId, token);
|
|
6137
|
+
await this.save();
|
|
6138
|
+
}
|
|
6139
|
+
return;
|
|
6140
|
+
}
|
|
6141
|
+
this.addProofPollingJob({
|
|
6142
|
+
tokenId,
|
|
6143
|
+
requestIdHex,
|
|
6144
|
+
commitmentJson: JSON.stringify(commitment.toJSON()),
|
|
6145
|
+
startedAt: Date.now(),
|
|
6146
|
+
attemptCount: 0,
|
|
6147
|
+
lastAttemptAt: 0,
|
|
6148
|
+
onProofReceived
|
|
6149
|
+
});
|
|
6150
|
+
} catch (error) {
|
|
6151
|
+
this.log("submitAndPollForProof error:", error);
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
/**
|
|
6155
|
+
* Add a proof polling job to the queue
|
|
6156
|
+
*/
|
|
6157
|
+
addProofPollingJob(job) {
|
|
6158
|
+
this.proofPollingJobs.set(job.tokenId, job);
|
|
6159
|
+
this.log(`Added proof polling job for token ${job.tokenId.slice(0, 8)}...`);
|
|
6160
|
+
this.startProofPolling();
|
|
6161
|
+
}
|
|
6162
|
+
/**
|
|
6163
|
+
* Start the proof polling interval if not already running
|
|
6164
|
+
*/
|
|
6165
|
+
startProofPolling() {
|
|
6166
|
+
if (this.proofPollingInterval) return;
|
|
6167
|
+
if (this.proofPollingJobs.size === 0) return;
|
|
6168
|
+
this.log("Starting proof polling...");
|
|
6169
|
+
this.proofPollingInterval = setInterval(
|
|
6170
|
+
() => this.processProofPollingQueue(),
|
|
6171
|
+
_PaymentsModule.PROOF_POLLING_INTERVAL_MS
|
|
6172
|
+
);
|
|
6173
|
+
}
|
|
6174
|
+
/**
|
|
6175
|
+
* Stop the proof polling interval
|
|
6176
|
+
*/
|
|
6177
|
+
stopProofPolling() {
|
|
6178
|
+
if (this.proofPollingInterval) {
|
|
6179
|
+
clearInterval(this.proofPollingInterval);
|
|
6180
|
+
this.proofPollingInterval = null;
|
|
6181
|
+
this.log("Stopped proof polling");
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
/**
|
|
6185
|
+
* Process all pending proof polling jobs
|
|
6186
|
+
*/
|
|
6187
|
+
async processProofPollingQueue() {
|
|
6188
|
+
if (this.proofPollingJobs.size === 0) {
|
|
6189
|
+
this.stopProofPolling();
|
|
6190
|
+
return;
|
|
6191
|
+
}
|
|
6192
|
+
const completedJobs = [];
|
|
6193
|
+
for (const [tokenId, job] of this.proofPollingJobs) {
|
|
6194
|
+
try {
|
|
6195
|
+
job.attemptCount++;
|
|
6196
|
+
job.lastAttemptAt = Date.now();
|
|
6197
|
+
if (job.attemptCount >= _PaymentsModule.PROOF_POLLING_MAX_ATTEMPTS) {
|
|
6198
|
+
this.log(`Proof polling timeout for token ${tokenId.slice(0, 8)}...`);
|
|
6199
|
+
const token2 = this.tokens.get(tokenId);
|
|
6200
|
+
if (token2 && token2.status === "submitted") {
|
|
6201
|
+
token2.status = "invalid";
|
|
6202
|
+
token2.updatedAt = Date.now();
|
|
6203
|
+
this.tokens.set(tokenId, token2);
|
|
6204
|
+
}
|
|
6205
|
+
completedJobs.push(tokenId);
|
|
6206
|
+
continue;
|
|
6207
|
+
}
|
|
6208
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(JSON.parse(job.commitmentJson));
|
|
6209
|
+
let inclusionProof = null;
|
|
6210
|
+
try {
|
|
6211
|
+
const abortController = new AbortController();
|
|
6212
|
+
const timeoutId = setTimeout(() => abortController.abort(), 500);
|
|
6213
|
+
if (this.deps.oracle.waitForProofSdk) {
|
|
6214
|
+
inclusionProof = await Promise.race([
|
|
6215
|
+
this.deps.oracle.waitForProofSdk(commitment, abortController.signal),
|
|
6216
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 500))
|
|
6217
|
+
]);
|
|
6218
|
+
} else {
|
|
6219
|
+
const proof = await this.deps.oracle.getProof(job.requestIdHex);
|
|
6220
|
+
if (proof) {
|
|
6221
|
+
inclusionProof = proof;
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
clearTimeout(timeoutId);
|
|
6225
|
+
} catch (err) {
|
|
6226
|
+
continue;
|
|
6227
|
+
}
|
|
6228
|
+
if (!inclusionProof) {
|
|
6229
|
+
continue;
|
|
6230
|
+
}
|
|
6231
|
+
const token = this.tokens.get(tokenId);
|
|
6232
|
+
if (token) {
|
|
6233
|
+
token.status = "spent";
|
|
6234
|
+
token.updatedAt = Date.now();
|
|
6235
|
+
this.tokens.set(tokenId, token);
|
|
6236
|
+
await this.save();
|
|
6237
|
+
this.log(`Proof received for token ${tokenId.slice(0, 8)}..., status: spent`);
|
|
6238
|
+
}
|
|
6239
|
+
job.onProofReceived?.(tokenId);
|
|
6240
|
+
completedJobs.push(tokenId);
|
|
6241
|
+
} catch (error) {
|
|
6242
|
+
this.log(`Proof polling attempt ${job.attemptCount} for ${tokenId.slice(0, 8)}...: ${error}`);
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
for (const tokenId of completedJobs) {
|
|
6246
|
+
this.proofPollingJobs.delete(tokenId);
|
|
6247
|
+
}
|
|
6248
|
+
if (this.proofPollingJobs.size === 0) {
|
|
6249
|
+
this.stopProofPolling();
|
|
6250
|
+
}
|
|
4875
6251
|
}
|
|
4876
6252
|
// ===========================================================================
|
|
4877
6253
|
// Private: Helpers
|
|
@@ -4886,6 +6262,14 @@ function createPaymentsModule(config) {
|
|
|
4886
6262
|
return new PaymentsModule(config);
|
|
4887
6263
|
}
|
|
4888
6264
|
|
|
6265
|
+
// modules/payments/TokenRecoveryService.ts
|
|
6266
|
+
var import_TokenId4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
6267
|
+
var import_TokenState6 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
6268
|
+
var import_TokenType3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
6269
|
+
var import_CoinId5 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
|
|
6270
|
+
var import_HashAlgorithm6 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
6271
|
+
var import_UnmaskedPredicate6 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
6272
|
+
|
|
4889
6273
|
// modules/communications/CommunicationsModule.ts
|
|
4890
6274
|
var CommunicationsModule = class {
|
|
4891
6275
|
config;
|
|
@@ -5939,20 +7323,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
5939
7323
|
|
|
5940
7324
|
// core/Sphere.ts
|
|
5941
7325
|
var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
|
|
5942
|
-
var
|
|
5943
|
-
var
|
|
5944
|
-
var
|
|
7326
|
+
var import_TokenType4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
7327
|
+
var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
7328
|
+
var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5945
7329
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5946
7330
|
async function deriveL3PredicateAddress(privateKey) {
|
|
5947
7331
|
const secret = Buffer.from(privateKey, "hex");
|
|
5948
7332
|
const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
|
|
5949
7333
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
5950
|
-
const tokenType = new
|
|
5951
|
-
const predicateRef =
|
|
7334
|
+
const tokenType = new import_TokenType4.TokenType(tokenTypeBytes);
|
|
7335
|
+
const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
|
|
5952
7336
|
tokenType,
|
|
5953
7337
|
signingService.algorithm,
|
|
5954
7338
|
signingService.publicKey,
|
|
5955
|
-
|
|
7339
|
+
import_HashAlgorithm7.HashAlgorithm.SHA256
|
|
5956
7340
|
);
|
|
5957
7341
|
return (await (await predicateRef).toAddress()).toString();
|
|
5958
7342
|
}
|
|
@@ -5968,7 +7352,11 @@ var Sphere = class _Sphere {
|
|
|
5968
7352
|
_derivationMode = "bip32";
|
|
5969
7353
|
_basePath = DEFAULT_BASE_PATH;
|
|
5970
7354
|
_currentAddressIndex = 0;
|
|
5971
|
-
/**
|
|
7355
|
+
/** Registry of all tracked (activated) addresses, keyed by HD index */
|
|
7356
|
+
_trackedAddresses = /* @__PURE__ */ new Map();
|
|
7357
|
+
/** Reverse lookup: addressId -> HD index */
|
|
7358
|
+
_addressIdToIndex = /* @__PURE__ */ new Map();
|
|
7359
|
+
/** Nametag cache: addressId -> (nametagIndex -> nametag). Separate from tracked addresses. */
|
|
5972
7360
|
_addressNametags = /* @__PURE__ */ new Map();
|
|
5973
7361
|
/** Cached PROXY address (computed once when nametag is set) */
|
|
5974
7362
|
_cachedProxyAddress = void 0;
|
|
@@ -5977,6 +7365,7 @@ var Sphere = class _Sphere {
|
|
|
5977
7365
|
_tokenStorageProviders = /* @__PURE__ */ new Map();
|
|
5978
7366
|
_transport;
|
|
5979
7367
|
_oracle;
|
|
7368
|
+
_priceProvider;
|
|
5980
7369
|
// Modules
|
|
5981
7370
|
_payments;
|
|
5982
7371
|
_communications;
|
|
@@ -5985,10 +7374,11 @@ var Sphere = class _Sphere {
|
|
|
5985
7374
|
// ===========================================================================
|
|
5986
7375
|
// Constructor (private)
|
|
5987
7376
|
// ===========================================================================
|
|
5988
|
-
constructor(storage, transport, oracle, tokenStorage, l1Config) {
|
|
7377
|
+
constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider) {
|
|
5989
7378
|
this._storage = storage;
|
|
5990
7379
|
this._transport = transport;
|
|
5991
7380
|
this._oracle = oracle;
|
|
7381
|
+
this._priceProvider = priceProvider ?? null;
|
|
5992
7382
|
if (tokenStorage) {
|
|
5993
7383
|
this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
|
|
5994
7384
|
}
|
|
@@ -6048,7 +7438,8 @@ var Sphere = class _Sphere {
|
|
|
6048
7438
|
transport: options.transport,
|
|
6049
7439
|
oracle: options.oracle,
|
|
6050
7440
|
tokenStorage: options.tokenStorage,
|
|
6051
|
-
l1: options.l1
|
|
7441
|
+
l1: options.l1,
|
|
7442
|
+
price: options.price
|
|
6052
7443
|
});
|
|
6053
7444
|
return { sphere: sphere2, created: false };
|
|
6054
7445
|
}
|
|
@@ -6072,7 +7463,8 @@ var Sphere = class _Sphere {
|
|
|
6072
7463
|
tokenStorage: options.tokenStorage,
|
|
6073
7464
|
derivationPath: options.derivationPath,
|
|
6074
7465
|
nametag: options.nametag,
|
|
6075
|
-
l1: options.l1
|
|
7466
|
+
l1: options.l1,
|
|
7467
|
+
price: options.price
|
|
6076
7468
|
});
|
|
6077
7469
|
return { sphere, created: true, generatedMnemonic };
|
|
6078
7470
|
}
|
|
@@ -6091,7 +7483,8 @@ var Sphere = class _Sphere {
|
|
|
6091
7483
|
options.transport,
|
|
6092
7484
|
options.oracle,
|
|
6093
7485
|
options.tokenStorage,
|
|
6094
|
-
options.l1
|
|
7486
|
+
options.l1,
|
|
7487
|
+
options.price
|
|
6095
7488
|
);
|
|
6096
7489
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
|
|
6097
7490
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
@@ -6100,10 +7493,12 @@ var Sphere = class _Sphere {
|
|
|
6100
7493
|
await sphere.finalizeWalletCreation();
|
|
6101
7494
|
sphere._initialized = true;
|
|
6102
7495
|
_Sphere.instance = sphere;
|
|
7496
|
+
await sphere.ensureAddressTracked(0);
|
|
6103
7497
|
if (options.nametag) {
|
|
6104
7498
|
await sphere.registerNametag(options.nametag);
|
|
6105
7499
|
} else {
|
|
6106
|
-
await sphere.
|
|
7500
|
+
await sphere.syncIdentityWithTransport();
|
|
7501
|
+
await sphere.recoverNametagFromTransport();
|
|
6107
7502
|
}
|
|
6108
7503
|
return sphere;
|
|
6109
7504
|
}
|
|
@@ -6119,14 +7514,28 @@ var Sphere = class _Sphere {
|
|
|
6119
7514
|
options.transport,
|
|
6120
7515
|
options.oracle,
|
|
6121
7516
|
options.tokenStorage,
|
|
6122
|
-
options.l1
|
|
7517
|
+
options.l1,
|
|
7518
|
+
options.price
|
|
6123
7519
|
);
|
|
6124
7520
|
await sphere.loadIdentityFromStorage();
|
|
6125
7521
|
await sphere.initializeProviders();
|
|
6126
7522
|
await sphere.initializeModules();
|
|
6127
|
-
await sphere.
|
|
7523
|
+
await sphere.syncIdentityWithTransport();
|
|
6128
7524
|
sphere._initialized = true;
|
|
6129
7525
|
_Sphere.instance = sphere;
|
|
7526
|
+
if (sphere._identity?.nametag && !sphere._payments.hasNametag()) {
|
|
7527
|
+
console.log(`[Sphere] Nametag @${sphere._identity.nametag} has no token, attempting to mint...`);
|
|
7528
|
+
try {
|
|
7529
|
+
const result = await sphere.mintNametag(sphere._identity.nametag);
|
|
7530
|
+
if (result.success) {
|
|
7531
|
+
console.log(`[Sphere] Nametag token minted successfully on load`);
|
|
7532
|
+
} else {
|
|
7533
|
+
console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
|
|
7534
|
+
}
|
|
7535
|
+
} catch (err) {
|
|
7536
|
+
console.warn(`[Sphere] Nametag token mint failed:`, err);
|
|
7537
|
+
}
|
|
7538
|
+
}
|
|
6130
7539
|
return sphere;
|
|
6131
7540
|
}
|
|
6132
7541
|
/**
|
|
@@ -6142,7 +7551,8 @@ var Sphere = class _Sphere {
|
|
|
6142
7551
|
options.transport,
|
|
6143
7552
|
options.oracle,
|
|
6144
7553
|
options.tokenStorage,
|
|
6145
|
-
options.l1
|
|
7554
|
+
options.l1,
|
|
7555
|
+
options.price
|
|
6146
7556
|
);
|
|
6147
7557
|
if (options.mnemonic) {
|
|
6148
7558
|
if (!_Sphere.validateMnemonic(options.mnemonic)) {
|
|
@@ -6167,11 +7577,12 @@ var Sphere = class _Sphere {
|
|
|
6167
7577
|
await sphere.initializeProviders();
|
|
6168
7578
|
await sphere.initializeModules();
|
|
6169
7579
|
if (!options.nametag) {
|
|
6170
|
-
await sphere.
|
|
7580
|
+
await sphere.recoverNametagFromTransport();
|
|
6171
7581
|
}
|
|
6172
7582
|
await sphere.finalizeWalletCreation();
|
|
6173
7583
|
sphere._initialized = true;
|
|
6174
7584
|
_Sphere.instance = sphere;
|
|
7585
|
+
await sphere.ensureAddressTracked(0);
|
|
6175
7586
|
if (options.nametag) {
|
|
6176
7587
|
await sphere.registerNametag(options.nametag);
|
|
6177
7588
|
}
|
|
@@ -6207,6 +7618,7 @@ var Sphere = class _Sphere {
|
|
|
6207
7618
|
await storage.remove(STORAGE_KEYS_GLOBAL.DERIVATION_MODE);
|
|
6208
7619
|
await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_SOURCE);
|
|
6209
7620
|
await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_EXISTS);
|
|
7621
|
+
await storage.remove(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
|
|
6210
7622
|
await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
6211
7623
|
await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
6212
7624
|
await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
|
|
@@ -6331,6 +7743,13 @@ var Sphere = class _Sphere {
|
|
|
6331
7743
|
hasTokenStorageProvider(providerId) {
|
|
6332
7744
|
return this._tokenStorageProviders.has(providerId);
|
|
6333
7745
|
}
|
|
7746
|
+
/**
|
|
7747
|
+
* Set or update the price provider after initialization
|
|
7748
|
+
*/
|
|
7749
|
+
setPriceProvider(provider) {
|
|
7750
|
+
this._priceProvider = provider;
|
|
7751
|
+
this._payments.setPriceProvider(provider);
|
|
7752
|
+
}
|
|
6334
7753
|
getTransport() {
|
|
6335
7754
|
return this._transport;
|
|
6336
7755
|
}
|
|
@@ -6818,10 +8237,9 @@ var Sphere = class _Sphere {
|
|
|
6818
8237
|
* @returns Primary nametag (index 0) or undefined if not registered
|
|
6819
8238
|
*/
|
|
6820
8239
|
getNametagForAddress(addressId) {
|
|
6821
|
-
const id = addressId ?? this.
|
|
8240
|
+
const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
|
|
6822
8241
|
if (!id) return void 0;
|
|
6823
|
-
|
|
6824
|
-
return nametagsMap?.get(0);
|
|
8242
|
+
return this._addressNametags.get(id)?.get(0);
|
|
6825
8243
|
}
|
|
6826
8244
|
/**
|
|
6827
8245
|
* Get all nametags for a specific address
|
|
@@ -6830,29 +8248,89 @@ var Sphere = class _Sphere {
|
|
|
6830
8248
|
* @returns Map of nametagIndex to nametag, or undefined if no nametags
|
|
6831
8249
|
*/
|
|
6832
8250
|
getNametagsForAddress(addressId) {
|
|
6833
|
-
const id = addressId ?? this.
|
|
8251
|
+
const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
|
|
6834
8252
|
if (!id) return void 0;
|
|
6835
|
-
const
|
|
6836
|
-
return
|
|
8253
|
+
const nametags = this._addressNametags.get(id);
|
|
8254
|
+
return nametags && nametags.size > 0 ? new Map(nametags) : void 0;
|
|
6837
8255
|
}
|
|
6838
8256
|
/**
|
|
6839
8257
|
* Get all registered address nametags
|
|
6840
|
-
*
|
|
8258
|
+
* @deprecated Use getActiveAddresses() or getAllTrackedAddresses() instead
|
|
6841
8259
|
* @returns Map of addressId to (nametagIndex -> nametag)
|
|
6842
8260
|
*/
|
|
6843
8261
|
getAllAddressNametags() {
|
|
6844
8262
|
const result = /* @__PURE__ */ new Map();
|
|
6845
|
-
this._addressNametags.
|
|
6846
|
-
|
|
6847
|
-
|
|
8263
|
+
for (const [addressId, nametags] of this._addressNametags.entries()) {
|
|
8264
|
+
if (nametags.size > 0) {
|
|
8265
|
+
result.set(addressId, new Map(nametags));
|
|
8266
|
+
}
|
|
8267
|
+
}
|
|
6848
8268
|
return result;
|
|
6849
8269
|
}
|
|
6850
8270
|
/**
|
|
6851
|
-
* Get
|
|
8271
|
+
* Get all active (non-hidden) tracked addresses.
|
|
8272
|
+
* Returns addresses that have been activated through create, switchToAddress,
|
|
8273
|
+
* registerNametag, or nametag recovery.
|
|
8274
|
+
*
|
|
8275
|
+
* @returns Array of TrackedAddress entries sorted by index, excluding hidden ones
|
|
8276
|
+
*/
|
|
8277
|
+
getActiveAddresses() {
|
|
8278
|
+
this.ensureReady();
|
|
8279
|
+
const result = [];
|
|
8280
|
+
for (const entry of this._trackedAddresses.values()) {
|
|
8281
|
+
if (!entry.hidden) {
|
|
8282
|
+
const nametag = this._addressNametags.get(entry.addressId)?.get(0);
|
|
8283
|
+
result.push({ ...entry, nametag });
|
|
8284
|
+
}
|
|
8285
|
+
}
|
|
8286
|
+
return result.sort((a, b) => a.index - b.index);
|
|
8287
|
+
}
|
|
8288
|
+
/**
|
|
8289
|
+
* Get all tracked addresses, including hidden ones.
|
|
8290
|
+
*
|
|
8291
|
+
* @returns Array of all TrackedAddress entries sorted by index
|
|
8292
|
+
*/
|
|
8293
|
+
getAllTrackedAddresses() {
|
|
8294
|
+
this.ensureReady();
|
|
8295
|
+
const result = [];
|
|
8296
|
+
for (const entry of this._trackedAddresses.values()) {
|
|
8297
|
+
const nametag = this._addressNametags.get(entry.addressId)?.get(0);
|
|
8298
|
+
result.push({ ...entry, nametag });
|
|
8299
|
+
}
|
|
8300
|
+
return result.sort((a, b) => a.index - b.index);
|
|
8301
|
+
}
|
|
8302
|
+
/**
|
|
8303
|
+
* Get tracked address info by index.
|
|
8304
|
+
*
|
|
8305
|
+
* @param index - Address index
|
|
8306
|
+
* @returns TrackedAddress or undefined if not tracked
|
|
8307
|
+
*/
|
|
8308
|
+
getTrackedAddress(index) {
|
|
8309
|
+
this.ensureReady();
|
|
8310
|
+
const entry = this._trackedAddresses.get(index);
|
|
8311
|
+
if (!entry) return void 0;
|
|
8312
|
+
const nametag = this._addressNametags.get(entry.addressId)?.get(0);
|
|
8313
|
+
return { ...entry, nametag };
|
|
8314
|
+
}
|
|
8315
|
+
/**
|
|
8316
|
+
* Set visibility of a tracked address.
|
|
8317
|
+
* Hidden addresses are not returned by getActiveAddresses() but remain tracked.
|
|
8318
|
+
*
|
|
8319
|
+
* @param index - Address index to hide/unhide
|
|
8320
|
+
* @param hidden - true to hide, false to show
|
|
8321
|
+
* @throws Error if address index is not tracked
|
|
6852
8322
|
*/
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
8323
|
+
async setAddressHidden(index, hidden) {
|
|
8324
|
+
this.ensureReady();
|
|
8325
|
+
const entry = this._trackedAddresses.get(index);
|
|
8326
|
+
if (!entry) {
|
|
8327
|
+
throw new Error(`Address at index ${index} is not tracked. Switch to it first.`);
|
|
8328
|
+
}
|
|
8329
|
+
if (entry.hidden === hidden) return;
|
|
8330
|
+
entry.hidden = hidden;
|
|
8331
|
+
await this.persistTrackedAddresses();
|
|
8332
|
+
const eventType = hidden ? "address:hidden" : "address:unhidden";
|
|
8333
|
+
this.emitEvent(eventType, { index, addressId: entry.addressId });
|
|
6856
8334
|
}
|
|
6857
8335
|
/**
|
|
6858
8336
|
* Switch to a different address by index
|
|
@@ -6873,7 +8351,7 @@ var Sphere = class _Sphere {
|
|
|
6873
8351
|
* await sphere.switchToAddress(0);
|
|
6874
8352
|
* ```
|
|
6875
8353
|
*/
|
|
6876
|
-
async switchToAddress(index) {
|
|
8354
|
+
async switchToAddress(index, options) {
|
|
6877
8355
|
this.ensureReady();
|
|
6878
8356
|
if (!this._masterKey) {
|
|
6879
8357
|
throw new Error("HD derivation requires master key with chain code. Cannot switch addresses.");
|
|
@@ -6881,12 +8359,28 @@ var Sphere = class _Sphere {
|
|
|
6881
8359
|
if (index < 0) {
|
|
6882
8360
|
throw new Error("Address index must be non-negative");
|
|
6883
8361
|
}
|
|
8362
|
+
const newNametag = options?.nametag?.startsWith("@") ? options.nametag.slice(1) : options?.nametag;
|
|
8363
|
+
if (newNametag && !this.validateNametag(newNametag)) {
|
|
8364
|
+
throw new Error("Invalid nametag format. Use alphanumeric characters, 3-20 chars.");
|
|
8365
|
+
}
|
|
6884
8366
|
const addressInfo = this.deriveAddress(index, false);
|
|
6885
8367
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
6886
8368
|
const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
|
|
8369
|
+
await this.ensureAddressTracked(index);
|
|
6887
8370
|
const addressId = getAddressId(predicateAddress);
|
|
6888
|
-
|
|
6889
|
-
|
|
8371
|
+
if (newNametag) {
|
|
8372
|
+
const existing = await this._transport.resolveNametag?.(newNametag);
|
|
8373
|
+
if (existing) {
|
|
8374
|
+
throw new Error(`Nametag @${newNametag} is already taken`);
|
|
8375
|
+
}
|
|
8376
|
+
let nametags = this._addressNametags.get(addressId);
|
|
8377
|
+
if (!nametags) {
|
|
8378
|
+
nametags = /* @__PURE__ */ new Map();
|
|
8379
|
+
this._addressNametags.set(addressId, nametags);
|
|
8380
|
+
}
|
|
8381
|
+
nametags.set(0, newNametag);
|
|
8382
|
+
}
|
|
8383
|
+
const nametag = this._addressNametags.get(addressId)?.get(0);
|
|
6890
8384
|
this._identity = {
|
|
6891
8385
|
privateKey: addressInfo.privateKey,
|
|
6892
8386
|
chainPubkey: addressInfo.publicKey,
|
|
@@ -6899,11 +8393,47 @@ var Sphere = class _Sphere {
|
|
|
6899
8393
|
await this._updateCachedProxyAddress();
|
|
6900
8394
|
await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
|
|
6901
8395
|
this._storage.setIdentity(this._identity);
|
|
6902
|
-
this._transport.setIdentity(this._identity);
|
|
8396
|
+
await this._transport.setIdentity(this._identity);
|
|
6903
8397
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
6904
8398
|
provider.setIdentity(this._identity);
|
|
8399
|
+
await provider.initialize();
|
|
6905
8400
|
}
|
|
6906
8401
|
await this.reinitializeModulesForNewAddress();
|
|
8402
|
+
if (this._identity.nametag) {
|
|
8403
|
+
await this.syncIdentityWithTransport();
|
|
8404
|
+
}
|
|
8405
|
+
if (newNametag) {
|
|
8406
|
+
await this.persistAddressNametags();
|
|
8407
|
+
if (!this._payments.hasNametag()) {
|
|
8408
|
+
console.log(`[Sphere] Minting nametag token for @${newNametag}...`);
|
|
8409
|
+
try {
|
|
8410
|
+
const result = await this.mintNametag(newNametag);
|
|
8411
|
+
if (result.success) {
|
|
8412
|
+
console.log(`[Sphere] Nametag token minted successfully`);
|
|
8413
|
+
} else {
|
|
8414
|
+
console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
|
|
8415
|
+
}
|
|
8416
|
+
} catch (err) {
|
|
8417
|
+
console.warn(`[Sphere] Nametag token mint failed:`, err);
|
|
8418
|
+
}
|
|
8419
|
+
}
|
|
8420
|
+
this.emitEvent("nametag:registered", {
|
|
8421
|
+
nametag: newNametag,
|
|
8422
|
+
addressIndex: index
|
|
8423
|
+
});
|
|
8424
|
+
} else if (this._identity.nametag && !this._payments.hasNametag()) {
|
|
8425
|
+
console.log(`[Sphere] Nametag @${this._identity.nametag} has no token after switch, minting...`);
|
|
8426
|
+
try {
|
|
8427
|
+
const result = await this.mintNametag(this._identity.nametag);
|
|
8428
|
+
if (result.success) {
|
|
8429
|
+
console.log(`[Sphere] Nametag token minted successfully after switch`);
|
|
8430
|
+
} else {
|
|
8431
|
+
console.warn(`[Sphere] Could not mint nametag token after switch: ${result.error}`);
|
|
8432
|
+
}
|
|
8433
|
+
} catch (err) {
|
|
8434
|
+
console.warn(`[Sphere] Nametag token mint failed after switch:`, err);
|
|
8435
|
+
}
|
|
8436
|
+
}
|
|
6907
8437
|
this.emitEvent("identity:changed", {
|
|
6908
8438
|
l1Address: this._identity.l1Address,
|
|
6909
8439
|
directAddress: this._identity.directAddress,
|
|
@@ -6925,7 +8455,8 @@ var Sphere = class _Sphere {
|
|
|
6925
8455
|
transport: this._transport,
|
|
6926
8456
|
oracle: this._oracle,
|
|
6927
8457
|
emitEvent,
|
|
6928
|
-
chainCode: this._masterKey?.chainCode
|
|
8458
|
+
chainCode: this._masterKey?.chainCode,
|
|
8459
|
+
price: this._priceProvider ?? void 0
|
|
6929
8460
|
});
|
|
6930
8461
|
this._communications.initialize({
|
|
6931
8462
|
identity: this._identity,
|
|
@@ -6958,6 +8489,14 @@ var Sphere = class _Sphere {
|
|
|
6958
8489
|
*/
|
|
6959
8490
|
deriveAddress(index, isChange = false) {
|
|
6960
8491
|
this.ensureReady();
|
|
8492
|
+
return this._deriveAddressInternal(index, isChange);
|
|
8493
|
+
}
|
|
8494
|
+
/**
|
|
8495
|
+
* Internal address derivation without ensureReady() check.
|
|
8496
|
+
* Used during initialization (loadTrackedAddresses, ensureAddressTracked)
|
|
8497
|
+
* when _initialized is still false.
|
|
8498
|
+
*/
|
|
8499
|
+
_deriveAddressInternal(index, isChange = false) {
|
|
6961
8500
|
if (!this._masterKey) {
|
|
6962
8501
|
throw new Error("HD derivation requires master key with chain code");
|
|
6963
8502
|
}
|
|
@@ -7096,6 +8635,22 @@ var Sphere = class _Sphere {
|
|
|
7096
8635
|
getProxyAddress() {
|
|
7097
8636
|
return this._cachedProxyAddress;
|
|
7098
8637
|
}
|
|
8638
|
+
/**
|
|
8639
|
+
* Resolve any identifier to full peer information.
|
|
8640
|
+
* Accepts @nametag, bare nametag, DIRECT://, PROXY://, L1 address, or transport pubkey.
|
|
8641
|
+
*
|
|
8642
|
+
* @example
|
|
8643
|
+
* ```ts
|
|
8644
|
+
* const peer = await sphere.resolve('@alice');
|
|
8645
|
+
* const peer = await sphere.resolve('DIRECT://...');
|
|
8646
|
+
* const peer = await sphere.resolve('alpha1...');
|
|
8647
|
+
* const peer = await sphere.resolve('ab12cd...'); // 64-char hex transport pubkey
|
|
8648
|
+
* ```
|
|
8649
|
+
*/
|
|
8650
|
+
async resolve(identifier) {
|
|
8651
|
+
this.ensureReady();
|
|
8652
|
+
return this._transport.resolve?.(identifier) ?? null;
|
|
8653
|
+
}
|
|
7099
8654
|
/** Compute and cache the PROXY address from the current nametag */
|
|
7100
8655
|
async _updateCachedProxyAddress() {
|
|
7101
8656
|
const nametag = this._identity?.nametag;
|
|
@@ -7134,11 +8689,12 @@ var Sphere = class _Sphere {
|
|
|
7134
8689
|
if (this._identity?.nametag) {
|
|
7135
8690
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
7136
8691
|
}
|
|
7137
|
-
if (this._transport.
|
|
7138
|
-
const success = await this._transport.
|
|
7139
|
-
cleanNametag,
|
|
8692
|
+
if (this._transport.publishIdentityBinding) {
|
|
8693
|
+
const success = await this._transport.publishIdentityBinding(
|
|
7140
8694
|
this._identity.chainPubkey,
|
|
7141
|
-
this._identity.
|
|
8695
|
+
this._identity.l1Address,
|
|
8696
|
+
this._identity.directAddress || "",
|
|
8697
|
+
cleanNametag
|
|
7142
8698
|
);
|
|
7143
8699
|
if (!success) {
|
|
7144
8700
|
throw new Error("Failed to register nametag. It may already be taken.");
|
|
@@ -7146,14 +8702,14 @@ var Sphere = class _Sphere {
|
|
|
7146
8702
|
}
|
|
7147
8703
|
this._identity.nametag = cleanNametag;
|
|
7148
8704
|
await this._updateCachedProxyAddress();
|
|
7149
|
-
const
|
|
7150
|
-
if (
|
|
7151
|
-
let
|
|
7152
|
-
if (!
|
|
7153
|
-
|
|
7154
|
-
this._addressNametags.set(
|
|
8705
|
+
const currentAddressId = this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
|
|
8706
|
+
if (currentAddressId) {
|
|
8707
|
+
let nametags = this._addressNametags.get(currentAddressId);
|
|
8708
|
+
if (!nametags) {
|
|
8709
|
+
nametags = /* @__PURE__ */ new Map();
|
|
8710
|
+
this._addressNametags.set(currentAddressId, nametags);
|
|
7155
8711
|
}
|
|
7156
|
-
|
|
8712
|
+
nametags.set(0, cleanNametag);
|
|
7157
8713
|
}
|
|
7158
8714
|
await this.persistAddressNametags();
|
|
7159
8715
|
if (!this._payments.hasNametag()) {
|
|
@@ -7172,19 +8728,19 @@ var Sphere = class _Sphere {
|
|
|
7172
8728
|
console.log(`[Sphere] Nametag registered for address ${this._currentAddressIndex}:`, cleanNametag);
|
|
7173
8729
|
}
|
|
7174
8730
|
/**
|
|
7175
|
-
* Persist
|
|
7176
|
-
* Format: { "DIRECT://abc...xyz": { "0": "alice", "1": "alice2" }, ... }
|
|
8731
|
+
* Persist tracked addresses to storage (only minimal fields via StorageProvider)
|
|
7177
8732
|
*/
|
|
7178
|
-
async
|
|
7179
|
-
const
|
|
7180
|
-
this.
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
8733
|
+
async persistTrackedAddresses() {
|
|
8734
|
+
const entries = [];
|
|
8735
|
+
for (const entry of this._trackedAddresses.values()) {
|
|
8736
|
+
entries.push({
|
|
8737
|
+
index: entry.index,
|
|
8738
|
+
hidden: entry.hidden,
|
|
8739
|
+
createdAt: entry.createdAt,
|
|
8740
|
+
updatedAt: entry.updatedAt
|
|
7184
8741
|
});
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
|
|
8742
|
+
}
|
|
8743
|
+
await this._storage.saveTrackedAddresses(entries);
|
|
7188
8744
|
}
|
|
7189
8745
|
/**
|
|
7190
8746
|
* Mint a nametag token on-chain (like Sphere wallet and lottery)
|
|
@@ -7218,63 +8774,184 @@ var Sphere = class _Sphere {
|
|
|
7218
8774
|
return this._payments.isNametagAvailable(nametag);
|
|
7219
8775
|
}
|
|
7220
8776
|
/**
|
|
7221
|
-
* Load
|
|
7222
|
-
*
|
|
7223
|
-
* And legacy format: { "0": "alice" } (migrates to new format on save)
|
|
8777
|
+
* Load tracked addresses from storage.
|
|
8778
|
+
* Falls back to migrating from old ADDRESS_NAMETAGS format.
|
|
7224
8779
|
*/
|
|
7225
|
-
async
|
|
8780
|
+
async loadTrackedAddresses() {
|
|
8781
|
+
this._trackedAddresses.clear();
|
|
8782
|
+
this._addressIdToIndex.clear();
|
|
7226
8783
|
try {
|
|
7227
|
-
const
|
|
7228
|
-
if (
|
|
7229
|
-
const
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
}
|
|
8784
|
+
const entries = await this._storage.loadTrackedAddresses();
|
|
8785
|
+
if (entries.length > 0) {
|
|
8786
|
+
for (const stored of entries) {
|
|
8787
|
+
const addrInfo = this._deriveAddressInternal(stored.index, false);
|
|
8788
|
+
const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
|
|
8789
|
+
const addressId = getAddressId(directAddress);
|
|
8790
|
+
const entry = {
|
|
8791
|
+
...stored,
|
|
8792
|
+
addressId,
|
|
8793
|
+
l1Address: addrInfo.address,
|
|
8794
|
+
directAddress,
|
|
8795
|
+
chainPubkey: addrInfo.publicKey
|
|
8796
|
+
};
|
|
8797
|
+
this._trackedAddresses.set(entry.index, entry);
|
|
8798
|
+
this._addressIdToIndex.set(addressId, entry.index);
|
|
7240
8799
|
}
|
|
8800
|
+
return;
|
|
8801
|
+
}
|
|
8802
|
+
const oldData = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
8803
|
+
if (oldData) {
|
|
8804
|
+
const parsed = JSON.parse(oldData);
|
|
8805
|
+
await this.migrateFromOldNametagFormat(parsed);
|
|
8806
|
+
await this.persistTrackedAddresses();
|
|
7241
8807
|
}
|
|
7242
8808
|
} catch {
|
|
7243
8809
|
}
|
|
7244
8810
|
}
|
|
7245
8811
|
/**
|
|
7246
|
-
*
|
|
7247
|
-
*
|
|
8812
|
+
* Migrate from old ADDRESS_NAMETAGS format to tracked addresses.
|
|
8813
|
+
* Scans HD indices 0..19 to match addressIds from the old format.
|
|
8814
|
+
* Populates both _trackedAddresses and _addressNametags.
|
|
7248
8815
|
*/
|
|
7249
|
-
async
|
|
7250
|
-
const
|
|
7251
|
-
|
|
7252
|
-
|
|
8816
|
+
async migrateFromOldNametagFormat(parsed) {
|
|
8817
|
+
const addressIdToNametags = /* @__PURE__ */ new Map();
|
|
8818
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
8819
|
+
if (typeof value === "object" && value !== null) {
|
|
8820
|
+
addressIdToNametags.set(key, value);
|
|
8821
|
+
}
|
|
8822
|
+
}
|
|
8823
|
+
if (addressIdToNametags.size === 0 || !this._masterKey) return;
|
|
8824
|
+
const SCAN_LIMIT = 20;
|
|
8825
|
+
for (let i = 0; i < SCAN_LIMIT && addressIdToNametags.size > 0; i++) {
|
|
8826
|
+
try {
|
|
8827
|
+
const addrInfo = this._deriveAddressInternal(i, false);
|
|
8828
|
+
const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
|
|
8829
|
+
const addressId = getAddressId(directAddress);
|
|
8830
|
+
if (addressIdToNametags.has(addressId)) {
|
|
8831
|
+
const nametagsObj = addressIdToNametags.get(addressId);
|
|
8832
|
+
const nametagMap = /* @__PURE__ */ new Map();
|
|
8833
|
+
for (const [idx, tag] of Object.entries(nametagsObj)) {
|
|
8834
|
+
nametagMap.set(parseInt(idx, 10), tag);
|
|
8835
|
+
}
|
|
8836
|
+
if (nametagMap.size > 0) {
|
|
8837
|
+
this._addressNametags.set(addressId, nametagMap);
|
|
8838
|
+
}
|
|
8839
|
+
const now = Date.now();
|
|
8840
|
+
const entry = {
|
|
8841
|
+
index: i,
|
|
8842
|
+
addressId,
|
|
8843
|
+
l1Address: addrInfo.address,
|
|
8844
|
+
directAddress,
|
|
8845
|
+
chainPubkey: addrInfo.publicKey,
|
|
8846
|
+
nametag: nametagMap.get(0),
|
|
8847
|
+
hidden: false,
|
|
8848
|
+
createdAt: now,
|
|
8849
|
+
updatedAt: now
|
|
8850
|
+
};
|
|
8851
|
+
this._trackedAddresses.set(i, entry);
|
|
8852
|
+
this._addressIdToIndex.set(addressId, i);
|
|
8853
|
+
addressIdToNametags.delete(addressId);
|
|
8854
|
+
}
|
|
8855
|
+
} catch {
|
|
8856
|
+
}
|
|
8857
|
+
}
|
|
8858
|
+
await this.persistAddressNametags();
|
|
8859
|
+
}
|
|
8860
|
+
/**
|
|
8861
|
+
* Ensure an address is tracked in the registry.
|
|
8862
|
+
* If not yet tracked, derives full info and creates the entry.
|
|
8863
|
+
*/
|
|
8864
|
+
async ensureAddressTracked(index) {
|
|
8865
|
+
const existing = this._trackedAddresses.get(index);
|
|
8866
|
+
if (existing) return existing;
|
|
8867
|
+
const addrInfo = this._deriveAddressInternal(index, false);
|
|
8868
|
+
const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
|
|
8869
|
+
const addressId = getAddressId(directAddress);
|
|
8870
|
+
const now = Date.now();
|
|
8871
|
+
const nametag = this._addressNametags.get(addressId)?.get(0);
|
|
8872
|
+
const entry = {
|
|
8873
|
+
index,
|
|
8874
|
+
addressId,
|
|
8875
|
+
l1Address: addrInfo.address,
|
|
8876
|
+
directAddress,
|
|
8877
|
+
chainPubkey: addrInfo.publicKey,
|
|
8878
|
+
nametag,
|
|
8879
|
+
hidden: false,
|
|
8880
|
+
createdAt: now,
|
|
8881
|
+
updatedAt: now
|
|
8882
|
+
};
|
|
8883
|
+
this._trackedAddresses.set(index, entry);
|
|
8884
|
+
this._addressIdToIndex.set(addressId, index);
|
|
8885
|
+
await this.persistTrackedAddresses();
|
|
8886
|
+
this.emitEvent("address:activated", { address: { ...entry } });
|
|
8887
|
+
return entry;
|
|
8888
|
+
}
|
|
8889
|
+
/**
|
|
8890
|
+
* Persist nametag cache to storage.
|
|
8891
|
+
* Format: { addressId: { "0": "alice", "1": "alice2" } }
|
|
8892
|
+
*/
|
|
8893
|
+
async persistAddressNametags() {
|
|
8894
|
+
const result = {};
|
|
8895
|
+
for (const [addressId, nametags] of this._addressNametags.entries()) {
|
|
8896
|
+
const obj = {};
|
|
8897
|
+
for (const [idx, tag] of nametags.entries()) {
|
|
8898
|
+
obj[idx.toString()] = tag;
|
|
8899
|
+
}
|
|
8900
|
+
result[addressId] = obj;
|
|
7253
8901
|
}
|
|
7254
|
-
|
|
8902
|
+
await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
|
|
8903
|
+
}
|
|
8904
|
+
/**
|
|
8905
|
+
* Load nametag cache from storage.
|
|
8906
|
+
*/
|
|
8907
|
+
async loadAddressNametags() {
|
|
8908
|
+
this._addressNametags.clear();
|
|
8909
|
+
try {
|
|
8910
|
+
const data = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
8911
|
+
if (!data) return;
|
|
8912
|
+
const parsed = JSON.parse(data);
|
|
8913
|
+
for (const [addressId, nametags] of Object.entries(parsed)) {
|
|
8914
|
+
const map = /* @__PURE__ */ new Map();
|
|
8915
|
+
for (const [idx, tag] of Object.entries(nametags)) {
|
|
8916
|
+
map.set(parseInt(idx, 10), tag);
|
|
8917
|
+
}
|
|
8918
|
+
this._addressNametags.set(addressId, map);
|
|
8919
|
+
}
|
|
8920
|
+
} catch {
|
|
8921
|
+
}
|
|
8922
|
+
}
|
|
8923
|
+
/**
|
|
8924
|
+
* Publish identity binding via transport.
|
|
8925
|
+
* Always publishes base identity (chainPubkey, l1Address, directAddress).
|
|
8926
|
+
* If nametag is set, also publishes nametag hash, proxy address, encrypted nametag.
|
|
8927
|
+
*/
|
|
8928
|
+
async syncIdentityWithTransport() {
|
|
8929
|
+
if (!this._transport.publishIdentityBinding) {
|
|
7255
8930
|
return;
|
|
7256
8931
|
}
|
|
7257
8932
|
try {
|
|
7258
|
-
const
|
|
7259
|
-
|
|
8933
|
+
const nametag = this._identity?.nametag;
|
|
8934
|
+
const success = await this._transport.publishIdentityBinding(
|
|
7260
8935
|
this._identity.chainPubkey,
|
|
7261
|
-
this._identity.
|
|
8936
|
+
this._identity.l1Address,
|
|
8937
|
+
this._identity.directAddress || "",
|
|
8938
|
+
nametag || void 0
|
|
7262
8939
|
);
|
|
7263
8940
|
if (success) {
|
|
7264
|
-
console.log(`[Sphere]
|
|
7265
|
-
} else {
|
|
8941
|
+
console.log(`[Sphere] Identity binding published${nametag ? ` with nametag @${nametag}` : ""}`);
|
|
8942
|
+
} else if (nametag) {
|
|
7266
8943
|
console.warn(`[Sphere] Nametag @${nametag} is taken by another pubkey`);
|
|
7267
8944
|
}
|
|
7268
8945
|
} catch (error) {
|
|
7269
|
-
console.warn(`[Sphere]
|
|
8946
|
+
console.warn(`[Sphere] Identity binding sync failed:`, error);
|
|
7270
8947
|
}
|
|
7271
8948
|
}
|
|
7272
8949
|
/**
|
|
7273
|
-
* Recover nametag from
|
|
8950
|
+
* Recover nametag from transport after wallet import.
|
|
7274
8951
|
* Searches for encrypted nametag events authored by this wallet's pubkey
|
|
7275
|
-
* and decrypts them to restore the nametag association
|
|
8952
|
+
* and decrypts them to restore the nametag association.
|
|
7276
8953
|
*/
|
|
7277
|
-
async
|
|
8954
|
+
async recoverNametagFromTransport() {
|
|
7278
8955
|
if (this._identity?.nametag) {
|
|
7279
8956
|
return;
|
|
7280
8957
|
}
|
|
@@ -7288,22 +8965,21 @@ var Sphere = class _Sphere {
|
|
|
7288
8965
|
this._identity.nametag = recoveredNametag;
|
|
7289
8966
|
await this._updateCachedProxyAddress();
|
|
7290
8967
|
}
|
|
7291
|
-
const
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
this._addressNametags.set(addressId, nametagsMap);
|
|
7297
|
-
}
|
|
7298
|
-
const nextIndex = nametagsMap.size;
|
|
7299
|
-
nametagsMap.set(nextIndex, recoveredNametag);
|
|
8968
|
+
const entry = await this.ensureAddressTracked(this._currentAddressIndex);
|
|
8969
|
+
let nametags = this._addressNametags.get(entry.addressId);
|
|
8970
|
+
if (!nametags) {
|
|
8971
|
+
nametags = /* @__PURE__ */ new Map();
|
|
8972
|
+
this._addressNametags.set(entry.addressId, nametags);
|
|
7300
8973
|
}
|
|
8974
|
+
const nextIndex = nametags.size;
|
|
8975
|
+
nametags.set(nextIndex, recoveredNametag);
|
|
7301
8976
|
await this.persistAddressNametags();
|
|
7302
|
-
if (this._transport.
|
|
7303
|
-
await this._transport.
|
|
7304
|
-
recoveredNametag,
|
|
8977
|
+
if (this._transport.publishIdentityBinding) {
|
|
8978
|
+
await this._transport.publishIdentityBinding(
|
|
7305
8979
|
this._identity.chainPubkey,
|
|
7306
|
-
this._identity.
|
|
8980
|
+
this._identity.l1Address,
|
|
8981
|
+
this._identity.directAddress || "",
|
|
8982
|
+
recoveredNametag
|
|
7307
8983
|
);
|
|
7308
8984
|
}
|
|
7309
8985
|
this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
|
|
@@ -7331,6 +9007,9 @@ var Sphere = class _Sphere {
|
|
|
7331
9007
|
await this._oracle.disconnect();
|
|
7332
9008
|
this._initialized = false;
|
|
7333
9009
|
this._identity = null;
|
|
9010
|
+
this._trackedAddresses.clear();
|
|
9011
|
+
this._addressIdToIndex.clear();
|
|
9012
|
+
this._addressNametags.clear();
|
|
7334
9013
|
this.eventHandlers.clear();
|
|
7335
9014
|
if (_Sphere.instance === this) {
|
|
7336
9015
|
_Sphere.instance = null;
|
|
@@ -7428,14 +9107,14 @@ var Sphere = class _Sphere {
|
|
|
7428
9107
|
if (this._identity) {
|
|
7429
9108
|
this._storage.setIdentity(this._identity);
|
|
7430
9109
|
}
|
|
9110
|
+
await this.loadTrackedAddresses();
|
|
7431
9111
|
await this.loadAddressNametags();
|
|
9112
|
+
const trackedEntry = await this.ensureAddressTracked(this._currentAddressIndex);
|
|
9113
|
+
const nametag = this._addressNametags.get(trackedEntry.addressId)?.get(0);
|
|
7432
9114
|
if (this._currentAddressIndex > 0 && this._masterKey) {
|
|
7433
|
-
const addressInfo = this.
|
|
9115
|
+
const addressInfo = this._deriveAddressInternal(this._currentAddressIndex, false);
|
|
7434
9116
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
7435
9117
|
const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
|
|
7436
|
-
const addressId = getAddressId(predicateAddress);
|
|
7437
|
-
const nametagsMap = this._addressNametags.get(addressId);
|
|
7438
|
-
const nametag = nametagsMap?.get(0);
|
|
7439
9118
|
this._identity = {
|
|
7440
9119
|
privateKey: addressInfo.privateKey,
|
|
7441
9120
|
chainPubkey: addressInfo.publicKey,
|
|
@@ -7446,13 +9125,8 @@ var Sphere = class _Sphere {
|
|
|
7446
9125
|
};
|
|
7447
9126
|
this._storage.setIdentity(this._identity);
|
|
7448
9127
|
console.log(`[Sphere] Restored to address ${this._currentAddressIndex}:`, this._identity.l1Address);
|
|
7449
|
-
} else if (this._identity) {
|
|
7450
|
-
|
|
7451
|
-
const nametagsMap = addressId ? this._addressNametags.get(addressId) : void 0;
|
|
7452
|
-
const nametag = nametagsMap?.get(0);
|
|
7453
|
-
if (nametag) {
|
|
7454
|
-
this._identity.nametag = nametag;
|
|
7455
|
-
}
|
|
9128
|
+
} else if (this._identity && nametag) {
|
|
9129
|
+
this._identity.nametag = nametag;
|
|
7456
9130
|
}
|
|
7457
9131
|
await this._updateCachedProxyAddress();
|
|
7458
9132
|
}
|
|
@@ -7510,7 +9184,7 @@ var Sphere = class _Sphere {
|
|
|
7510
9184
|
// ===========================================================================
|
|
7511
9185
|
async initializeProviders() {
|
|
7512
9186
|
this._storage.setIdentity(this._identity);
|
|
7513
|
-
this._transport.setIdentity(this._identity);
|
|
9187
|
+
await this._transport.setIdentity(this._identity);
|
|
7514
9188
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
7515
9189
|
provider.setIdentity(this._identity);
|
|
7516
9190
|
}
|
|
@@ -7531,7 +9205,8 @@ var Sphere = class _Sphere {
|
|
|
7531
9205
|
oracle: this._oracle,
|
|
7532
9206
|
emitEvent,
|
|
7533
9207
|
// Pass chain code for L1 HD derivation
|
|
7534
|
-
chainCode: this._masterKey?.chainCode
|
|
9208
|
+
chainCode: this._masterKey?.chainCode,
|
|
9209
|
+
price: this._priceProvider ?? void 0
|
|
7535
9210
|
});
|
|
7536
9211
|
this._communications.initialize({
|
|
7537
9212
|
identity: this._identity,
|