@unicitylabs/sphere-sdk 0.5.1 → 0.5.3
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/dist/connect/index.cjs +3 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +3 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +669 -277
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +57 -2
- package/dist/core/index.d.ts +57 -2
- package/dist/core/index.js +669 -277
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +3 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +3 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +11 -3
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +11 -3
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +9 -2
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +9 -2
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +3 -1
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.js +3 -1
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +11 -3
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +7 -0
- package/dist/impl/nodejs/index.d.ts +7 -0
- package/dist/impl/nodejs/index.js +11 -3
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +671 -277
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -3
- package/dist/index.d.ts +128 -3
- package/dist/index.js +670 -277
- package/dist/index.js.map +1 -1
- package/dist/l1/index.cjs +3 -1
- package/dist/l1/index.cjs.map +1 -1
- package/dist/l1/index.js +3 -1
- package/dist/l1/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -109,7 +109,9 @@ var init_constants = __esm({
|
|
|
109
109
|
/** Group chat: processed event IDs for deduplication */
|
|
110
110
|
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
111
111
|
/** Processed V5 split group IDs for Nostr re-delivery dedup */
|
|
112
|
-
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids"
|
|
112
|
+
PROCESSED_SPLIT_GROUP_IDS: "processed_split_group_ids",
|
|
113
|
+
/** Processed V6 combined transfer IDs for Nostr re-delivery dedup */
|
|
114
|
+
PROCESSED_COMBINED_TRANSFER_IDS: "processed_combined_transfer_ids"
|
|
113
115
|
};
|
|
114
116
|
STORAGE_KEYS = {
|
|
115
117
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -792,6 +794,7 @@ __export(index_exports, {
|
|
|
792
794
|
identityFromMnemonicSync: () => identityFromMnemonicSync,
|
|
793
795
|
initSphere: () => initSphere,
|
|
794
796
|
isArchivedKey: () => isArchivedKey,
|
|
797
|
+
isCombinedTransferBundleV6: () => isCombinedTransferBundleV6,
|
|
795
798
|
isForkedKey: () => isForkedKey,
|
|
796
799
|
isInstantSplitBundle: () => isInstantSplitBundle,
|
|
797
800
|
isInstantSplitBundleV4: () => isInstantSplitBundleV4,
|
|
@@ -2872,7 +2875,7 @@ init_constants();
|
|
|
2872
2875
|
// types/txf.ts
|
|
2873
2876
|
var ARCHIVED_PREFIX = "archived-";
|
|
2874
2877
|
var FORKED_PREFIX = "_forked_";
|
|
2875
|
-
var RESERVED_KEYS = ["_meta", "_nametag", "_nametags", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity"];
|
|
2878
|
+
var RESERVED_KEYS = ["_meta", "_nametag", "_nametags", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity", "_history"];
|
|
2876
2879
|
function isTokenKey(key) {
|
|
2877
2880
|
return key.startsWith("_") && !key.startsWith(ARCHIVED_PREFIX) && !key.startsWith(FORKED_PREFIX) && !RESERVED_KEYS.includes(key);
|
|
2878
2881
|
}
|
|
@@ -3494,6 +3497,9 @@ async function buildTxfStorageData(tokens, meta, options) {
|
|
|
3494
3497
|
if (options?.invalidatedNametags && options.invalidatedNametags.length > 0) {
|
|
3495
3498
|
storageData._invalidatedNametags = options.invalidatedNametags;
|
|
3496
3499
|
}
|
|
3500
|
+
if (options?.historyEntries && options.historyEntries.length > 0) {
|
|
3501
|
+
storageData._history = options.historyEntries;
|
|
3502
|
+
}
|
|
3497
3503
|
for (const token of tokens) {
|
|
3498
3504
|
const txf = tokenToTxf(token);
|
|
3499
3505
|
if (txf) {
|
|
@@ -3527,6 +3533,7 @@ function parseTxfStorageData(data) {
|
|
|
3527
3533
|
outboxEntries: [],
|
|
3528
3534
|
mintOutboxEntries: [],
|
|
3529
3535
|
invalidatedNametags: [],
|
|
3536
|
+
historyEntries: [],
|
|
3530
3537
|
validationErrors: []
|
|
3531
3538
|
};
|
|
3532
3539
|
if (!data || typeof data !== "object") {
|
|
@@ -3580,6 +3587,13 @@ function parseTxfStorageData(data) {
|
|
|
3580
3587
|
}
|
|
3581
3588
|
}
|
|
3582
3589
|
}
|
|
3590
|
+
if (Array.isArray(storageData._history)) {
|
|
3591
|
+
for (const entry of storageData._history) {
|
|
3592
|
+
if (typeof entry === "object" && entry !== null && typeof entry.dedupKey === "string" && typeof entry.type === "string") {
|
|
3593
|
+
result.historyEntries.push(entry);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3583
3597
|
for (const key of Object.keys(storageData)) {
|
|
3584
3598
|
if (isTokenKey(key)) {
|
|
3585
3599
|
const tokenId = tokenIdFromKey(key);
|
|
@@ -3742,14 +3756,149 @@ var InstantSplitExecutor = class {
|
|
|
3742
3756
|
this.devMode = config.devMode ?? false;
|
|
3743
3757
|
}
|
|
3744
3758
|
/**
|
|
3745
|
-
*
|
|
3759
|
+
* Build a V5 split bundle WITHOUT sending it via transport.
|
|
3746
3760
|
*
|
|
3747
|
-
*
|
|
3761
|
+
* Steps 1-5 of the V5 flow:
|
|
3748
3762
|
* 1. Create and submit burn commitment
|
|
3749
3763
|
* 2. Wait for burn proof
|
|
3750
3764
|
* 3. Create mint commitments with SplitMintReason
|
|
3751
3765
|
* 4. Create transfer commitment (no mint proof needed)
|
|
3752
|
-
* 5.
|
|
3766
|
+
* 5. Package V5 bundle
|
|
3767
|
+
*
|
|
3768
|
+
* The caller is responsible for sending the bundle and then calling
|
|
3769
|
+
* `startBackground()` on the result to begin mint proof + change token creation.
|
|
3770
|
+
*/
|
|
3771
|
+
async buildSplitBundle(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, options) {
|
|
3772
|
+
const splitGroupId = crypto.randomUUID();
|
|
3773
|
+
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3774
|
+
console.log(`[InstantSplit] Building V5 bundle for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3775
|
+
const coinId = new import_CoinId3.CoinId(fromHex2(coinIdHex));
|
|
3776
|
+
const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
|
|
3777
|
+
const recipientTokenId = new import_TokenId3.TokenId(await sha2563(seedString));
|
|
3778
|
+
const senderTokenId = new import_TokenId3.TokenId(await sha2563(seedString + "_sender"));
|
|
3779
|
+
const recipientSalt = await sha2563(seedString + "_recipient_salt");
|
|
3780
|
+
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3781
|
+
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3782
|
+
tokenToSplit.type,
|
|
3783
|
+
this.signingService.algorithm,
|
|
3784
|
+
this.signingService.publicKey,
|
|
3785
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3786
|
+
);
|
|
3787
|
+
const senderAddress = await senderAddressRef.toAddress();
|
|
3788
|
+
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3789
|
+
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3790
|
+
builder.createToken(
|
|
3791
|
+
recipientTokenId,
|
|
3792
|
+
tokenToSplit.type,
|
|
3793
|
+
new Uint8Array(0),
|
|
3794
|
+
coinDataA,
|
|
3795
|
+
senderAddress,
|
|
3796
|
+
// Mint to sender first, then transfer
|
|
3797
|
+
recipientSalt,
|
|
3798
|
+
null
|
|
3799
|
+
);
|
|
3800
|
+
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3801
|
+
builder.createToken(
|
|
3802
|
+
senderTokenId,
|
|
3803
|
+
tokenToSplit.type,
|
|
3804
|
+
new Uint8Array(0),
|
|
3805
|
+
coinDataB,
|
|
3806
|
+
senderAddress,
|
|
3807
|
+
senderSalt,
|
|
3808
|
+
null
|
|
3809
|
+
);
|
|
3810
|
+
const split = await builder.build(tokenToSplit);
|
|
3811
|
+
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3812
|
+
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3813
|
+
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3814
|
+
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3815
|
+
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3816
|
+
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3817
|
+
}
|
|
3818
|
+
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3819
|
+
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3820
|
+
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3821
|
+
console.log(`[InstantSplit] Burn proof received`);
|
|
3822
|
+
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3823
|
+
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3824
|
+
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3825
|
+
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3826
|
+
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3827
|
+
const recipientMintCommitment = mintCommitments.find(
|
|
3828
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3829
|
+
);
|
|
3830
|
+
const senderMintCommitment = mintCommitments.find(
|
|
3831
|
+
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3832
|
+
);
|
|
3833
|
+
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3834
|
+
throw new Error("Failed to find expected mint commitments");
|
|
3835
|
+
}
|
|
3836
|
+
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3837
|
+
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3838
|
+
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3839
|
+
recipientMintCommitment.transactionData,
|
|
3840
|
+
recipientAddress,
|
|
3841
|
+
transferSalt,
|
|
3842
|
+
this.signingService
|
|
3843
|
+
);
|
|
3844
|
+
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3845
|
+
recipientTokenId,
|
|
3846
|
+
tokenToSplit.type,
|
|
3847
|
+
this.signingService,
|
|
3848
|
+
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3849
|
+
recipientSalt
|
|
3850
|
+
);
|
|
3851
|
+
const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
|
|
3852
|
+
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3853
|
+
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3854
|
+
let nametagTokenJson;
|
|
3855
|
+
const recipientAddressStr = recipientAddress.toString();
|
|
3856
|
+
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3857
|
+
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3858
|
+
}
|
|
3859
|
+
const bundle = {
|
|
3860
|
+
version: "5.0",
|
|
3861
|
+
type: "INSTANT_SPLIT",
|
|
3862
|
+
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3863
|
+
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3864
|
+
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3865
|
+
amount: splitAmount.toString(),
|
|
3866
|
+
coinId: coinIdHex,
|
|
3867
|
+
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3868
|
+
splitGroupId,
|
|
3869
|
+
senderPubkey,
|
|
3870
|
+
recipientSaltHex: toHex2(recipientSalt),
|
|
3871
|
+
transferSaltHex: toHex2(transferSalt),
|
|
3872
|
+
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3873
|
+
finalRecipientStateJson: "",
|
|
3874
|
+
// Recipient creates their own
|
|
3875
|
+
recipientAddressJson: recipientAddressStr,
|
|
3876
|
+
nametagTokenJson
|
|
3877
|
+
};
|
|
3878
|
+
return {
|
|
3879
|
+
bundle,
|
|
3880
|
+
splitGroupId,
|
|
3881
|
+
startBackground: async () => {
|
|
3882
|
+
if (!options?.skipBackground) {
|
|
3883
|
+
await this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3884
|
+
signingService: this.signingService,
|
|
3885
|
+
tokenType: tokenToSplit.type,
|
|
3886
|
+
coinId,
|
|
3887
|
+
senderTokenId,
|
|
3888
|
+
senderSalt,
|
|
3889
|
+
onProgress: options?.onBackgroundProgress,
|
|
3890
|
+
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3891
|
+
onStorageSync: options?.onStorageSync
|
|
3892
|
+
});
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Execute an instant split transfer with V5 optimized flow.
|
|
3899
|
+
*
|
|
3900
|
+
* Builds the bundle via buildSplitBundle(), sends via transport,
|
|
3901
|
+
* and starts background processing.
|
|
3753
3902
|
*
|
|
3754
3903
|
* @param tokenToSplit - The SDK token to split
|
|
3755
3904
|
* @param splitAmount - Amount to send to recipient
|
|
@@ -3763,117 +3912,19 @@ var InstantSplitExecutor = class {
|
|
|
3763
3912
|
*/
|
|
3764
3913
|
async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
|
|
3765
3914
|
const startTime = performance.now();
|
|
3766
|
-
const splitGroupId = crypto.randomUUID();
|
|
3767
|
-
const tokenIdHex = toHex2(tokenToSplit.id.bytes);
|
|
3768
|
-
console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
|
|
3769
3915
|
try {
|
|
3770
|
-
const
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
const senderSalt = await sha2563(seedString + "_sender_salt");
|
|
3776
|
-
const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
|
|
3777
|
-
tokenToSplit.type,
|
|
3778
|
-
this.signingService.algorithm,
|
|
3779
|
-
this.signingService.publicKey,
|
|
3780
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256
|
|
3781
|
-
);
|
|
3782
|
-
const senderAddress = await senderAddressRef.toAddress();
|
|
3783
|
-
const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
|
|
3784
|
-
const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
|
|
3785
|
-
builder.createToken(
|
|
3786
|
-
recipientTokenId,
|
|
3787
|
-
tokenToSplit.type,
|
|
3788
|
-
new Uint8Array(0),
|
|
3789
|
-
coinDataA,
|
|
3790
|
-
senderAddress,
|
|
3791
|
-
// Mint to sender first, then transfer
|
|
3792
|
-
recipientSalt,
|
|
3793
|
-
null
|
|
3794
|
-
);
|
|
3795
|
-
const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
|
|
3796
|
-
builder.createToken(
|
|
3797
|
-
senderTokenId,
|
|
3798
|
-
tokenToSplit.type,
|
|
3799
|
-
new Uint8Array(0),
|
|
3800
|
-
coinDataB,
|
|
3801
|
-
senderAddress,
|
|
3802
|
-
senderSalt,
|
|
3803
|
-
null
|
|
3804
|
-
);
|
|
3805
|
-
const split = await builder.build(tokenToSplit);
|
|
3806
|
-
console.log("[InstantSplit] Step 1: Creating and submitting burn...");
|
|
3807
|
-
const burnSalt = await sha2563(seedString + "_burn_salt");
|
|
3808
|
-
const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
|
|
3809
|
-
const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
|
|
3810
|
-
if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
|
|
3811
|
-
throw new Error(`Burn submission failed: ${burnResponse.status}`);
|
|
3812
|
-
}
|
|
3813
|
-
console.log("[InstantSplit] Step 2: Waiting for burn proof...");
|
|
3814
|
-
const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
|
|
3815
|
-
const burnTransaction = burnCommitment.toTransaction(burnProof);
|
|
3816
|
-
const burnDuration = performance.now() - startTime;
|
|
3817
|
-
console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
|
|
3818
|
-
options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
|
|
3819
|
-
console.log("[InstantSplit] Step 3: Creating mint commitments...");
|
|
3820
|
-
const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
|
|
3821
|
-
const recipientIdHex = toHex2(recipientTokenId.bytes);
|
|
3822
|
-
const senderIdHex = toHex2(senderTokenId.bytes);
|
|
3823
|
-
const recipientMintCommitment = mintCommitments.find(
|
|
3824
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
|
|
3825
|
-
);
|
|
3826
|
-
const senderMintCommitment = mintCommitments.find(
|
|
3827
|
-
(c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
|
|
3828
|
-
);
|
|
3829
|
-
if (!recipientMintCommitment || !senderMintCommitment) {
|
|
3830
|
-
throw new Error("Failed to find expected mint commitments");
|
|
3831
|
-
}
|
|
3832
|
-
console.log("[InstantSplit] Step 4: Creating transfer commitment...");
|
|
3833
|
-
const transferSalt = await sha2563(seedString + "_transfer_salt");
|
|
3834
|
-
const transferCommitment = await this.createTransferCommitmentFromMintData(
|
|
3835
|
-
recipientMintCommitment.transactionData,
|
|
3916
|
+
const buildResult = await this.buildSplitBundle(
|
|
3917
|
+
tokenToSplit,
|
|
3918
|
+
splitAmount,
|
|
3919
|
+
remainderAmount,
|
|
3920
|
+
coinIdHex,
|
|
3836
3921
|
recipientAddress,
|
|
3837
|
-
|
|
3838
|
-
this.signingService
|
|
3839
|
-
);
|
|
3840
|
-
const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
|
|
3841
|
-
recipientTokenId,
|
|
3842
|
-
tokenToSplit.type,
|
|
3843
|
-
this.signingService,
|
|
3844
|
-
import_HashAlgorithm3.HashAlgorithm.SHA256,
|
|
3845
|
-
recipientSalt
|
|
3922
|
+
options
|
|
3846
3923
|
);
|
|
3847
|
-
|
|
3848
|
-
console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
|
|
3924
|
+
console.log("[InstantSplit] Sending via transport...");
|
|
3849
3925
|
const senderPubkey = toHex2(this.signingService.publicKey);
|
|
3850
|
-
let nametagTokenJson;
|
|
3851
|
-
const recipientAddressStr = recipientAddress.toString();
|
|
3852
|
-
if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
|
|
3853
|
-
nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
|
|
3854
|
-
}
|
|
3855
|
-
const bundle = {
|
|
3856
|
-
version: "5.0",
|
|
3857
|
-
type: "INSTANT_SPLIT",
|
|
3858
|
-
burnTransaction: JSON.stringify(burnTransaction.toJSON()),
|
|
3859
|
-
recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
|
|
3860
|
-
transferCommitment: JSON.stringify(transferCommitment.toJSON()),
|
|
3861
|
-
amount: splitAmount.toString(),
|
|
3862
|
-
coinId: coinIdHex,
|
|
3863
|
-
tokenTypeHex: toHex2(tokenToSplit.type.bytes),
|
|
3864
|
-
splitGroupId,
|
|
3865
|
-
senderPubkey,
|
|
3866
|
-
recipientSaltHex: toHex2(recipientSalt),
|
|
3867
|
-
transferSaltHex: toHex2(transferSalt),
|
|
3868
|
-
mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
|
|
3869
|
-
finalRecipientStateJson: "",
|
|
3870
|
-
// Recipient creates their own
|
|
3871
|
-
recipientAddressJson: recipientAddressStr,
|
|
3872
|
-
nametagTokenJson
|
|
3873
|
-
};
|
|
3874
|
-
console.log("[InstantSplit] Step 6: Sending via transport...");
|
|
3875
3926
|
const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
|
|
3876
|
-
token: JSON.stringify(bundle),
|
|
3927
|
+
token: JSON.stringify(buildResult.bundle),
|
|
3877
3928
|
proof: null,
|
|
3878
3929
|
// Proof is included in the bundle
|
|
3879
3930
|
memo: options?.memo,
|
|
@@ -3884,25 +3935,13 @@ var InstantSplitExecutor = class {
|
|
|
3884
3935
|
const criticalPathDuration = performance.now() - startTime;
|
|
3885
3936
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3886
3937
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3887
|
-
|
|
3888
|
-
if (!options?.skipBackground) {
|
|
3889
|
-
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3890
|
-
signingService: this.signingService,
|
|
3891
|
-
tokenType: tokenToSplit.type,
|
|
3892
|
-
coinId,
|
|
3893
|
-
senderTokenId,
|
|
3894
|
-
senderSalt,
|
|
3895
|
-
onProgress: options?.onBackgroundProgress,
|
|
3896
|
-
onChangeTokenCreated: options?.onChangeTokenCreated,
|
|
3897
|
-
onStorageSync: options?.onStorageSync
|
|
3898
|
-
});
|
|
3899
|
-
}
|
|
3938
|
+
const backgroundPromise = buildResult.startBackground();
|
|
3900
3939
|
return {
|
|
3901
3940
|
success: true,
|
|
3902
3941
|
nostrEventId,
|
|
3903
|
-
splitGroupId,
|
|
3942
|
+
splitGroupId: buildResult.splitGroupId,
|
|
3904
3943
|
criticalPathDurationMs: criticalPathDuration,
|
|
3905
|
-
backgroundStarted:
|
|
3944
|
+
backgroundStarted: true,
|
|
3906
3945
|
backgroundPromise
|
|
3907
3946
|
};
|
|
3908
3947
|
} catch (error) {
|
|
@@ -3911,7 +3950,6 @@ var InstantSplitExecutor = class {
|
|
|
3911
3950
|
console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
|
|
3912
3951
|
return {
|
|
3913
3952
|
success: false,
|
|
3914
|
-
splitGroupId,
|
|
3915
3953
|
criticalPathDurationMs: duration,
|
|
3916
3954
|
error: errorMessage,
|
|
3917
3955
|
backgroundStarted: false
|
|
@@ -4116,6 +4154,11 @@ function isInstantSplitBundleV4(obj) {
|
|
|
4116
4154
|
function isInstantSplitBundleV5(obj) {
|
|
4117
4155
|
return isInstantSplitBundle(obj) && obj.version === "5.0";
|
|
4118
4156
|
}
|
|
4157
|
+
function isCombinedTransferBundleV6(obj) {
|
|
4158
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
4159
|
+
const b = obj;
|
|
4160
|
+
return b.version === "6.0" && b.type === "COMBINED_TRANSFER";
|
|
4161
|
+
}
|
|
4119
4162
|
|
|
4120
4163
|
// modules/payments/InstantSplitProcessor.ts
|
|
4121
4164
|
function fromHex3(hex) {
|
|
@@ -4440,6 +4483,7 @@ function computeHistoryDedupKey(type, tokenId, transferId) {
|
|
|
4440
4483
|
if (tokenId) return `${type}_${tokenId}`;
|
|
4441
4484
|
return `${type}_${crypto.randomUUID()}`;
|
|
4442
4485
|
}
|
|
4486
|
+
var MAX_SYNCED_HISTORY_ENTRIES = 5e3;
|
|
4443
4487
|
function enrichWithRegistry(info) {
|
|
4444
4488
|
const registry = TokenRegistry.getInstance();
|
|
4445
4489
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -4769,6 +4813,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4769
4813
|
// Survives page reloads via KV storage so Nostr re-deliveries are ignored
|
|
4770
4814
|
// even when the confirmed token's in-memory ID differs from v5split_{id}.
|
|
4771
4815
|
processedSplitGroupIds = /* @__PURE__ */ new Set();
|
|
4816
|
+
// Persistent dedup: tracks V6 combined transfer IDs that have been processed.
|
|
4817
|
+
processedCombinedTransferIds = /* @__PURE__ */ new Set();
|
|
4772
4818
|
// Storage event subscriptions (push-based sync)
|
|
4773
4819
|
storageEventUnsubscribers = [];
|
|
4774
4820
|
syncDebounceTimer = null;
|
|
@@ -4862,6 +4908,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4862
4908
|
const result = await provider.load();
|
|
4863
4909
|
if (result.success && result.data) {
|
|
4864
4910
|
this.loadFromStorageData(result.data);
|
|
4911
|
+
const txfData = result.data;
|
|
4912
|
+
if (txfData._history && txfData._history.length > 0) {
|
|
4913
|
+
await this.importRemoteHistoryEntries(txfData._history);
|
|
4914
|
+
}
|
|
4865
4915
|
this.log(`Loaded metadata from provider ${id}`);
|
|
4866
4916
|
break;
|
|
4867
4917
|
}
|
|
@@ -4869,10 +4919,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4869
4919
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4870
4920
|
}
|
|
4871
4921
|
}
|
|
4922
|
+
for (const [id, token] of this.tokens) {
|
|
4923
|
+
try {
|
|
4924
|
+
if (token.sdkData) {
|
|
4925
|
+
const data = JSON.parse(token.sdkData);
|
|
4926
|
+
if (data?._placeholder) {
|
|
4927
|
+
this.tokens.delete(id);
|
|
4928
|
+
console.log(`[Payments] Removed stale placeholder token: ${id}`);
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
} catch {
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4872
4934
|
const loadedTokens = Array.from(this.tokens.values()).map((t) => `${t.id.slice(0, 12)}(${t.status})`);
|
|
4873
4935
|
console.log(`[Payments][DEBUG] load(): from TXF providers: ${this.tokens.size} tokens [${loadedTokens.join(", ")}]`);
|
|
4874
4936
|
await this.loadPendingV5Tokens();
|
|
4875
4937
|
await this.loadProcessedSplitGroupIds();
|
|
4938
|
+
await this.loadProcessedCombinedTransferIds();
|
|
4876
4939
|
await this.loadHistory();
|
|
4877
4940
|
const pending2 = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
4878
4941
|
if (pending2) {
|
|
@@ -4964,12 +5027,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4964
5027
|
token.status = "transferring";
|
|
4965
5028
|
this.tokens.set(token.id, token);
|
|
4966
5029
|
}
|
|
5030
|
+
await this.save();
|
|
4967
5031
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4968
5032
|
result.status = "submitted";
|
|
4969
5033
|
const recipientNametag = peerInfo?.nametag || (request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0);
|
|
4970
5034
|
const transferMode = request.transferMode ?? "instant";
|
|
4971
|
-
if (
|
|
4972
|
-
if (
|
|
5035
|
+
if (transferMode === "conservative") {
|
|
5036
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4973
5037
|
this.log("Executing conservative split...");
|
|
4974
5038
|
const splitExecutor = new TokenSplitExecutor({
|
|
4975
5039
|
stateTransitionClient: stClient,
|
|
@@ -5013,27 +5077,59 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5013
5077
|
requestIdHex: splitRequestIdHex
|
|
5014
5078
|
});
|
|
5015
5079
|
this.log(`Conservative split transfer completed`);
|
|
5016
|
-
}
|
|
5017
|
-
|
|
5018
|
-
const
|
|
5080
|
+
}
|
|
5081
|
+
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
5082
|
+
const token = tokenWithAmount.uiToken;
|
|
5083
|
+
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
5084
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
5085
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
5086
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5087
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
5088
|
+
}
|
|
5089
|
+
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
5090
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
5091
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5092
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
5093
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
5094
|
+
memo: request.memo
|
|
5095
|
+
});
|
|
5096
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
5097
|
+
const requestIdBytes = commitment.requestId;
|
|
5098
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5099
|
+
result.tokenTransfers.push({
|
|
5100
|
+
sourceTokenId: token.id,
|
|
5101
|
+
method: "direct",
|
|
5102
|
+
requestIdHex
|
|
5103
|
+
});
|
|
5104
|
+
this.log(`Token ${token.id} sent via CONSERVATIVE, requestId: ${requestIdHex}`);
|
|
5105
|
+
await this.removeToken(token.id);
|
|
5106
|
+
}
|
|
5107
|
+
} else {
|
|
5108
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
5109
|
+
const senderPubkey = this.deps.identity.chainPubkey;
|
|
5110
|
+
let changeTokenPlaceholderId = null;
|
|
5111
|
+
let builtSplit = null;
|
|
5112
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
5113
|
+
this.log("Building instant split bundle...");
|
|
5019
5114
|
const executor = new InstantSplitExecutor({
|
|
5020
5115
|
stateTransitionClient: stClient,
|
|
5021
5116
|
trustBase,
|
|
5022
5117
|
signingService,
|
|
5023
5118
|
devMode
|
|
5024
5119
|
});
|
|
5025
|
-
|
|
5120
|
+
builtSplit = await executor.buildSplitBundle(
|
|
5026
5121
|
splitPlan.tokenToSplit.sdkToken,
|
|
5027
5122
|
splitPlan.splitAmount,
|
|
5028
5123
|
splitPlan.remainderAmount,
|
|
5029
5124
|
splitPlan.coinId,
|
|
5030
5125
|
recipientAddress,
|
|
5031
|
-
this.deps.transport,
|
|
5032
|
-
recipientPubkey,
|
|
5033
5126
|
{
|
|
5034
5127
|
memo: request.memo,
|
|
5035
5128
|
onChangeTokenCreated: async (changeToken) => {
|
|
5036
5129
|
const changeTokenData = changeToken.toJSON();
|
|
5130
|
+
if (changeTokenPlaceholderId && this.tokens.has(changeTokenPlaceholderId)) {
|
|
5131
|
+
this.tokens.delete(changeTokenPlaceholderId);
|
|
5132
|
+
}
|
|
5037
5133
|
const uiToken = {
|
|
5038
5134
|
id: crypto.randomUUID(),
|
|
5039
5135
|
coinId: request.coinId,
|
|
@@ -5056,65 +5152,103 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5056
5152
|
}
|
|
5057
5153
|
}
|
|
5058
5154
|
);
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
this.
|
|
5064
|
-
|
|
5155
|
+
this.log(`Split bundle built: splitGroupId=${builtSplit.splitGroupId}`);
|
|
5156
|
+
}
|
|
5157
|
+
const directCommitments = await Promise.all(
|
|
5158
|
+
splitPlan.tokensToTransferDirectly.map(
|
|
5159
|
+
(tw) => this.createSdkCommitment(tw.uiToken, recipientAddress, signingService)
|
|
5160
|
+
)
|
|
5161
|
+
);
|
|
5162
|
+
const directTokenEntries = splitPlan.tokensToTransferDirectly.map(
|
|
5163
|
+
(tw, i) => ({
|
|
5164
|
+
sourceToken: JSON.stringify(tw.sdkToken.toJSON()),
|
|
5165
|
+
commitmentData: JSON.stringify(directCommitments[i].toJSON()),
|
|
5166
|
+
amount: tw.uiToken.amount,
|
|
5167
|
+
coinId: tw.uiToken.coinId,
|
|
5168
|
+
tokenId: extractTokenIdFromSdkData(tw.uiToken.sdkData) || void 0
|
|
5169
|
+
})
|
|
5170
|
+
);
|
|
5171
|
+
const combinedBundle = {
|
|
5172
|
+
version: "6.0",
|
|
5173
|
+
type: "COMBINED_TRANSFER",
|
|
5174
|
+
transferId: result.id,
|
|
5175
|
+
splitBundle: builtSplit?.bundle ?? null,
|
|
5176
|
+
directTokens: directTokenEntries,
|
|
5177
|
+
totalAmount: request.amount.toString(),
|
|
5178
|
+
coinId: request.coinId,
|
|
5179
|
+
senderPubkey,
|
|
5180
|
+
memo: request.memo
|
|
5181
|
+
};
|
|
5182
|
+
console.log(
|
|
5183
|
+
`[Payments] Sending V6 combined bundle: transfer=${result.id.slice(0, 8)}... split=${!!builtSplit} direct=${directTokenEntries.length}`
|
|
5184
|
+
);
|
|
5185
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5186
|
+
token: JSON.stringify(combinedBundle),
|
|
5187
|
+
proof: null,
|
|
5188
|
+
memo: request.memo,
|
|
5189
|
+
sender: { transportPubkey: senderPubkey }
|
|
5190
|
+
});
|
|
5191
|
+
console.log(`[Payments] V6 combined bundle sent successfully`);
|
|
5192
|
+
if (builtSplit) {
|
|
5193
|
+
const bgPromise = builtSplit.startBackground();
|
|
5194
|
+
this.pendingBackgroundTasks.push(bgPromise);
|
|
5195
|
+
}
|
|
5196
|
+
if (builtSplit && splitPlan.remainderAmount) {
|
|
5197
|
+
changeTokenPlaceholderId = crypto.randomUUID();
|
|
5198
|
+
const placeholder = {
|
|
5199
|
+
id: changeTokenPlaceholderId,
|
|
5200
|
+
coinId: request.coinId,
|
|
5201
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
5202
|
+
name: this.getCoinName(request.coinId),
|
|
5203
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
5204
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
5205
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
5206
|
+
status: "transferring",
|
|
5207
|
+
createdAt: Date.now(),
|
|
5208
|
+
updatedAt: Date.now(),
|
|
5209
|
+
sdkData: JSON.stringify({ _placeholder: true })
|
|
5210
|
+
};
|
|
5211
|
+
this.tokens.set(placeholder.id, placeholder);
|
|
5212
|
+
this.log(`Placeholder change token created: ${placeholder.id} (${placeholder.amount})`);
|
|
5213
|
+
}
|
|
5214
|
+
for (const commitment of directCommitments) {
|
|
5215
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
5216
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
5217
|
+
);
|
|
5218
|
+
}
|
|
5219
|
+
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
5065
5220
|
await this.removeToken(splitPlan.tokenToSplit.uiToken.id);
|
|
5066
5221
|
result.tokenTransfers.push({
|
|
5067
5222
|
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
5068
5223
|
method: "split",
|
|
5069
|
-
splitGroupId:
|
|
5070
|
-
nostrEventId: instantResult.nostrEventId
|
|
5224
|
+
splitGroupId: builtSplit.splitGroupId
|
|
5071
5225
|
});
|
|
5072
|
-
this.log(`Instant split transfer completed`);
|
|
5073
5226
|
}
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
}
|
|
5084
|
-
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
5085
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
5086
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5087
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
5088
|
-
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
5089
|
-
memo: request.memo
|
|
5090
|
-
});
|
|
5091
|
-
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
5092
|
-
} else {
|
|
5093
|
-
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
5094
|
-
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
5095
|
-
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
5096
|
-
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
5097
|
-
memo: request.memo
|
|
5227
|
+
for (let i = 0; i < splitPlan.tokensToTransferDirectly.length; i++) {
|
|
5228
|
+
const token = splitPlan.tokensToTransferDirectly[i].uiToken;
|
|
5229
|
+
const commitment = directCommitments[i];
|
|
5230
|
+
const requestIdBytes = commitment.requestId;
|
|
5231
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5232
|
+
result.tokenTransfers.push({
|
|
5233
|
+
sourceTokenId: token.id,
|
|
5234
|
+
method: "direct",
|
|
5235
|
+
requestIdHex
|
|
5098
5236
|
});
|
|
5099
|
-
|
|
5100
|
-
stClient.submitTransferCommitment(commitment).catch(
|
|
5101
|
-
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
5102
|
-
);
|
|
5237
|
+
await this.removeToken(token.id);
|
|
5103
5238
|
}
|
|
5104
|
-
|
|
5105
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5106
|
-
result.tokenTransfers.push({
|
|
5107
|
-
sourceTokenId: token.id,
|
|
5108
|
-
method: "direct",
|
|
5109
|
-
requestIdHex
|
|
5110
|
-
});
|
|
5111
|
-
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
5112
|
-
await this.removeToken(token.id);
|
|
5239
|
+
this.log(`V6 combined transfer completed`);
|
|
5113
5240
|
}
|
|
5114
5241
|
result.status = "delivered";
|
|
5115
5242
|
await this.save();
|
|
5116
5243
|
await this.removeFromOutbox(result.id);
|
|
5117
5244
|
result.status = "completed";
|
|
5245
|
+
const tokenMap = new Map(result.tokens.map((t) => [t.id, t]));
|
|
5246
|
+
const sentTokenIds = result.tokenTransfers.map((tt) => ({
|
|
5247
|
+
id: tt.sourceTokenId,
|
|
5248
|
+
// For split tokens, use splitAmount (the portion sent), not the original token amount
|
|
5249
|
+
amount: tt.method === "split" ? splitPlan.splitAmount?.toString() || "0" : tokenMap.get(tt.sourceTokenId)?.amount || "0",
|
|
5250
|
+
source: tt.method === "split" ? "split" : "direct"
|
|
5251
|
+
}));
|
|
5118
5252
|
const sentTokenId = result.tokens[0] ? extractTokenIdFromSdkData(result.tokens[0].sdkData) : void 0;
|
|
5119
5253
|
await this.addToHistory({
|
|
5120
5254
|
type: "SENT",
|
|
@@ -5127,7 +5261,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5127
5261
|
recipientAddress: peerInfo?.directAddress || recipientAddress?.toString() || recipientPubkey,
|
|
5128
5262
|
memo: request.memo,
|
|
5129
5263
|
transferId: result.id,
|
|
5130
|
-
tokenId: sentTokenId || void 0
|
|
5264
|
+
tokenId: sentTokenId || void 0,
|
|
5265
|
+
tokenIds: sentTokenIds.length > 0 ? sentTokenIds : void 0
|
|
5131
5266
|
});
|
|
5132
5267
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
5133
5268
|
return result;
|
|
@@ -5297,6 +5432,267 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5297
5432
|
};
|
|
5298
5433
|
}
|
|
5299
5434
|
}
|
|
5435
|
+
// ===========================================================================
|
|
5436
|
+
// Shared Helpers for V5 and V6 Receiver Processing
|
|
5437
|
+
// ===========================================================================
|
|
5438
|
+
/**
|
|
5439
|
+
* Save a V5 split bundle as an unconfirmed token (shared by V5 standalone and V6 combined).
|
|
5440
|
+
* Returns the created UI token, or null if deduped.
|
|
5441
|
+
*
|
|
5442
|
+
* @param deferPersistence - If true, skip addToken/save calls (caller batches them).
|
|
5443
|
+
* The token is still added to the in-memory map for dedup; caller must call save().
|
|
5444
|
+
*/
|
|
5445
|
+
async saveUnconfirmedV5Token(bundle, senderPubkey, deferPersistence = false) {
|
|
5446
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
5447
|
+
if (this.tokens.has(deterministicId) || this.processedSplitGroupIds.has(bundle.splitGroupId)) {
|
|
5448
|
+
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5449
|
+
return null;
|
|
5450
|
+
}
|
|
5451
|
+
const registry = TokenRegistry.getInstance();
|
|
5452
|
+
const pendingData = {
|
|
5453
|
+
type: "v5_bundle",
|
|
5454
|
+
stage: "RECEIVED",
|
|
5455
|
+
bundleJson: JSON.stringify(bundle),
|
|
5456
|
+
senderPubkey,
|
|
5457
|
+
savedAt: Date.now(),
|
|
5458
|
+
attemptCount: 0
|
|
5459
|
+
};
|
|
5460
|
+
const uiToken = {
|
|
5461
|
+
id: deterministicId,
|
|
5462
|
+
coinId: bundle.coinId,
|
|
5463
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5464
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5465
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5466
|
+
amount: bundle.amount,
|
|
5467
|
+
status: "submitted",
|
|
5468
|
+
// UNCONFIRMED
|
|
5469
|
+
createdAt: Date.now(),
|
|
5470
|
+
updatedAt: Date.now(),
|
|
5471
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5472
|
+
};
|
|
5473
|
+
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5474
|
+
if (deferPersistence) {
|
|
5475
|
+
this.tokens.set(uiToken.id, uiToken);
|
|
5476
|
+
} else {
|
|
5477
|
+
await this.addToken(uiToken);
|
|
5478
|
+
await this.saveProcessedSplitGroupIds();
|
|
5479
|
+
}
|
|
5480
|
+
return uiToken;
|
|
5481
|
+
}
|
|
5482
|
+
/**
|
|
5483
|
+
* Save a commitment-only (NOSTR-FIRST) token and start proof polling.
|
|
5484
|
+
* Shared by standalone NOSTR-FIRST handler and V6 combined handler.
|
|
5485
|
+
* Returns the created UI token, or null if deduped/tombstoned.
|
|
5486
|
+
*
|
|
5487
|
+
* @param deferPersistence - If true, skip save() and commitment submission
|
|
5488
|
+
* (caller batches them). Token is added to in-memory map + proof polling is queued.
|
|
5489
|
+
* @param skipGenesisDedup - If true, skip genesis-ID-only dedup. V6 handler sets this
|
|
5490
|
+
* because bundle-level dedup protects against replays, and split children share genesis IDs.
|
|
5491
|
+
*/
|
|
5492
|
+
async saveCommitmentOnlyToken(sourceTokenInput, commitmentInput, senderPubkey, deferPersistence = false, skipGenesisDedup = false) {
|
|
5493
|
+
const tokenInfo = await parseTokenInfo(sourceTokenInput);
|
|
5494
|
+
const sdkData = typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput);
|
|
5495
|
+
const nostrTokenId = extractTokenIdFromSdkData(sdkData);
|
|
5496
|
+
const nostrStateHash = extractStateHashFromSdkData(sdkData);
|
|
5497
|
+
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
5498
|
+
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
5499
|
+
return null;
|
|
5500
|
+
}
|
|
5501
|
+
if (nostrTokenId) {
|
|
5502
|
+
for (const existing of this.tokens.values()) {
|
|
5503
|
+
const existingTokenId = extractTokenIdFromSdkData(existing.sdkData);
|
|
5504
|
+
if (existingTokenId !== nostrTokenId) continue;
|
|
5505
|
+
const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
|
|
5506
|
+
if (nostrStateHash && existingStateHash === nostrStateHash) {
|
|
5507
|
+
console.log(
|
|
5508
|
+
`[Payments] NOSTR-FIRST: Skipping duplicate token state ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`
|
|
5509
|
+
);
|
|
5510
|
+
return null;
|
|
5511
|
+
}
|
|
5512
|
+
if (!skipGenesisDedup) {
|
|
5513
|
+
console.log(
|
|
5514
|
+
`[Payments] NOSTR-FIRST: Skipping replay of finalized token ${nostrTokenId.slice(0, 8)}...`
|
|
5515
|
+
);
|
|
5516
|
+
return null;
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
const token = {
|
|
5521
|
+
id: crypto.randomUUID(),
|
|
5522
|
+
coinId: tokenInfo.coinId,
|
|
5523
|
+
symbol: tokenInfo.symbol,
|
|
5524
|
+
name: tokenInfo.name,
|
|
5525
|
+
decimals: tokenInfo.decimals,
|
|
5526
|
+
iconUrl: tokenInfo.iconUrl,
|
|
5527
|
+
amount: tokenInfo.amount,
|
|
5528
|
+
status: "submitted",
|
|
5529
|
+
// NOSTR-FIRST: unconfirmed until proof
|
|
5530
|
+
createdAt: Date.now(),
|
|
5531
|
+
updatedAt: Date.now(),
|
|
5532
|
+
sdkData
|
|
5533
|
+
};
|
|
5534
|
+
this.tokens.set(token.id, token);
|
|
5535
|
+
if (!deferPersistence) {
|
|
5536
|
+
await this.save();
|
|
5537
|
+
}
|
|
5538
|
+
try {
|
|
5539
|
+
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
5540
|
+
const requestIdBytes = commitment.requestId;
|
|
5541
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
5542
|
+
if (!deferPersistence) {
|
|
5543
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5544
|
+
if (stClient) {
|
|
5545
|
+
const response = await stClient.submitTransferCommitment(commitment);
|
|
5546
|
+
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
this.addProofPollingJob({
|
|
5550
|
+
tokenId: token.id,
|
|
5551
|
+
requestIdHex,
|
|
5552
|
+
commitmentJson: JSON.stringify(commitmentInput),
|
|
5553
|
+
startedAt: Date.now(),
|
|
5554
|
+
attemptCount: 0,
|
|
5555
|
+
lastAttemptAt: 0,
|
|
5556
|
+
onProofReceived: async (tokenId) => {
|
|
5557
|
+
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
5558
|
+
}
|
|
5559
|
+
});
|
|
5560
|
+
} catch (err) {
|
|
5561
|
+
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
5562
|
+
}
|
|
5563
|
+
return token;
|
|
5564
|
+
}
|
|
5565
|
+
// ===========================================================================
|
|
5566
|
+
// Combined Transfer V6 — Receiver
|
|
5567
|
+
// ===========================================================================
|
|
5568
|
+
/**
|
|
5569
|
+
* Process a received COMBINED_TRANSFER V6 bundle.
|
|
5570
|
+
*
|
|
5571
|
+
* Unpacks a single Nostr message into its component tokens:
|
|
5572
|
+
* - Optional V5 split bundle (saved as unconfirmed, resolved lazily)
|
|
5573
|
+
* - Zero or more direct tokens (saved as unconfirmed, proof-polled)
|
|
5574
|
+
*
|
|
5575
|
+
* Emits ONE transfer:incoming event and records ONE history entry.
|
|
5576
|
+
*/
|
|
5577
|
+
async processCombinedTransferBundle(bundle, senderPubkey) {
|
|
5578
|
+
this.ensureInitialized();
|
|
5579
|
+
if (!this.loaded && this.loadedPromise) {
|
|
5580
|
+
await this.loadedPromise;
|
|
5581
|
+
}
|
|
5582
|
+
if (this.processedCombinedTransferIds.has(bundle.transferId)) {
|
|
5583
|
+
console.log(`[Payments] V6 combined transfer ${bundle.transferId.slice(0, 12)}... already processed, skipping`);
|
|
5584
|
+
return;
|
|
5585
|
+
}
|
|
5586
|
+
console.log(
|
|
5587
|
+
`[Payments] Processing V6 combined transfer ${bundle.transferId.slice(0, 12)}... (split=${!!bundle.splitBundle}, direct=${bundle.directTokens.length})`
|
|
5588
|
+
);
|
|
5589
|
+
const allTokens = [];
|
|
5590
|
+
const tokenBreakdown = [];
|
|
5591
|
+
const parsedDirectEntries = bundle.directTokens.map((entry) => ({
|
|
5592
|
+
sourceToken: typeof entry.sourceToken === "string" ? JSON.parse(entry.sourceToken) : entry.sourceToken,
|
|
5593
|
+
commitment: typeof entry.commitmentData === "string" ? JSON.parse(entry.commitmentData) : entry.commitmentData
|
|
5594
|
+
}));
|
|
5595
|
+
if (bundle.splitBundle) {
|
|
5596
|
+
const splitToken = await this.saveUnconfirmedV5Token(bundle.splitBundle, senderPubkey, true);
|
|
5597
|
+
if (splitToken) {
|
|
5598
|
+
allTokens.push(splitToken);
|
|
5599
|
+
tokenBreakdown.push({ id: splitToken.id, amount: splitToken.amount, source: "split" });
|
|
5600
|
+
} else {
|
|
5601
|
+
console.warn(`[Payments] V6: split token was deduped/failed \u2014 amount=${bundle.splitBundle.amount}`);
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
const directResults = await Promise.all(
|
|
5605
|
+
parsedDirectEntries.map(
|
|
5606
|
+
({ sourceToken, commitment }) => this.saveCommitmentOnlyToken(sourceToken, commitment, senderPubkey, true, true)
|
|
5607
|
+
)
|
|
5608
|
+
);
|
|
5609
|
+
for (let i = 0; i < directResults.length; i++) {
|
|
5610
|
+
const token = directResults[i];
|
|
5611
|
+
if (token) {
|
|
5612
|
+
allTokens.push(token);
|
|
5613
|
+
tokenBreakdown.push({ id: token.id, amount: token.amount, source: "direct" });
|
|
5614
|
+
} else {
|
|
5615
|
+
const entry = bundle.directTokens[i];
|
|
5616
|
+
console.warn(
|
|
5617
|
+
`[Payments] V6: direct token #${i} dropped (amount=${entry.amount}, tokenId=${entry.tokenId?.slice(0, 12) ?? "N/A"})`
|
|
5618
|
+
);
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
if (allTokens.length === 0) {
|
|
5622
|
+
console.log(`[Payments] V6 combined transfer: all tokens deduped, nothing to save`);
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
this.processedCombinedTransferIds.add(bundle.transferId);
|
|
5626
|
+
const [senderInfo] = await Promise.all([
|
|
5627
|
+
this.resolveSenderInfo(senderPubkey),
|
|
5628
|
+
this.save(),
|
|
5629
|
+
this.saveProcessedCombinedTransferIds(),
|
|
5630
|
+
...bundle.splitBundle ? [this.saveProcessedSplitGroupIds()] : []
|
|
5631
|
+
]);
|
|
5632
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5633
|
+
if (stClient) {
|
|
5634
|
+
for (const { commitment } of parsedDirectEntries) {
|
|
5635
|
+
import_TransferCommitment4.TransferCommitment.fromJSON(commitment).then(
|
|
5636
|
+
(c) => stClient.submitTransferCommitment(c)
|
|
5637
|
+
).catch(
|
|
5638
|
+
(err) => console.error("[Payments] V6 background commitment submit failed:", err)
|
|
5639
|
+
);
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
5643
|
+
id: bundle.transferId,
|
|
5644
|
+
senderPubkey,
|
|
5645
|
+
senderNametag: senderInfo.senderNametag,
|
|
5646
|
+
tokens: allTokens,
|
|
5647
|
+
memo: bundle.memo,
|
|
5648
|
+
receivedAt: Date.now()
|
|
5649
|
+
});
|
|
5650
|
+
const actualAmount = allTokens.reduce((sum, t) => sum + BigInt(t.amount || "0"), 0n).toString();
|
|
5651
|
+
await this.addToHistory({
|
|
5652
|
+
type: "RECEIVED",
|
|
5653
|
+
amount: actualAmount,
|
|
5654
|
+
coinId: bundle.coinId,
|
|
5655
|
+
symbol: allTokens[0]?.symbol || bundle.coinId,
|
|
5656
|
+
timestamp: Date.now(),
|
|
5657
|
+
senderPubkey,
|
|
5658
|
+
...senderInfo,
|
|
5659
|
+
memo: bundle.memo,
|
|
5660
|
+
transferId: bundle.transferId,
|
|
5661
|
+
tokenId: allTokens[0]?.id,
|
|
5662
|
+
tokenIds: tokenBreakdown
|
|
5663
|
+
});
|
|
5664
|
+
if (bundle.splitBundle) {
|
|
5665
|
+
this.resolveUnconfirmed().catch(() => {
|
|
5666
|
+
});
|
|
5667
|
+
this.scheduleResolveUnconfirmed();
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
/**
|
|
5671
|
+
* Persist processed combined transfer IDs to KV storage.
|
|
5672
|
+
*/
|
|
5673
|
+
async saveProcessedCombinedTransferIds() {
|
|
5674
|
+
const ids = Array.from(this.processedCombinedTransferIds);
|
|
5675
|
+
if (ids.length > 0) {
|
|
5676
|
+
await this.deps.storage.set(
|
|
5677
|
+
STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS,
|
|
5678
|
+
JSON.stringify(ids)
|
|
5679
|
+
);
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
/**
|
|
5683
|
+
* Load processed combined transfer IDs from KV storage.
|
|
5684
|
+
*/
|
|
5685
|
+
async loadProcessedCombinedTransferIds() {
|
|
5686
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PROCESSED_COMBINED_TRANSFER_IDS);
|
|
5687
|
+
if (!data) return;
|
|
5688
|
+
try {
|
|
5689
|
+
const ids = JSON.parse(data);
|
|
5690
|
+
for (const id of ids) {
|
|
5691
|
+
this.processedCombinedTransferIds.add(id);
|
|
5692
|
+
}
|
|
5693
|
+
} catch {
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5300
5696
|
/**
|
|
5301
5697
|
* Process a received INSTANT_SPLIT bundle.
|
|
5302
5698
|
*
|
|
@@ -5320,36 +5716,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5320
5716
|
return this.processInstantSplitBundleSync(bundle, senderPubkey, memo);
|
|
5321
5717
|
}
|
|
5322
5718
|
try {
|
|
5323
|
-
const
|
|
5324
|
-
if (
|
|
5325
|
-
console.log(`[Payments] V5 bundle ${bundle.splitGroupId.slice(0, 12)}... already processed, skipping`);
|
|
5719
|
+
const uiToken = await this.saveUnconfirmedV5Token(bundle, senderPubkey);
|
|
5720
|
+
if (!uiToken) {
|
|
5326
5721
|
return { success: true, durationMs: 0 };
|
|
5327
5722
|
}
|
|
5328
|
-
const registry = TokenRegistry.getInstance();
|
|
5329
|
-
const pendingData = {
|
|
5330
|
-
type: "v5_bundle",
|
|
5331
|
-
stage: "RECEIVED",
|
|
5332
|
-
bundleJson: JSON.stringify(bundle),
|
|
5333
|
-
senderPubkey,
|
|
5334
|
-
savedAt: Date.now(),
|
|
5335
|
-
attemptCount: 0
|
|
5336
|
-
};
|
|
5337
|
-
const uiToken = {
|
|
5338
|
-
id: deterministicId,
|
|
5339
|
-
coinId: bundle.coinId,
|
|
5340
|
-
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
5341
|
-
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
5342
|
-
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
5343
|
-
amount: bundle.amount,
|
|
5344
|
-
status: "submitted",
|
|
5345
|
-
// UNCONFIRMED
|
|
5346
|
-
createdAt: Date.now(),
|
|
5347
|
-
updatedAt: Date.now(),
|
|
5348
|
-
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
5349
|
-
};
|
|
5350
|
-
await this.addToken(uiToken);
|
|
5351
|
-
this.processedSplitGroupIds.add(bundle.splitGroupId);
|
|
5352
|
-
await this.saveProcessedSplitGroupIds();
|
|
5353
5723
|
const senderInfo = await this.resolveSenderInfo(senderPubkey);
|
|
5354
5724
|
await this.addToHistory({
|
|
5355
5725
|
type: "RECEIVED",
|
|
@@ -5360,7 +5730,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5360
5730
|
senderPubkey,
|
|
5361
5731
|
...senderInfo,
|
|
5362
5732
|
memo,
|
|
5363
|
-
tokenId:
|
|
5733
|
+
tokenId: uiToken.id
|
|
5364
5734
|
});
|
|
5365
5735
|
this.deps.emitEvent("transfer:incoming", {
|
|
5366
5736
|
id: bundle.splitGroupId,
|
|
@@ -6010,16 +6380,18 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6010
6380
|
}
|
|
6011
6381
|
/**
|
|
6012
6382
|
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
6013
|
-
* Excludes tokens with status 'spent'
|
|
6383
|
+
* Excludes tokens with status 'spent' or 'invalid'.
|
|
6384
|
+
* Tokens with status 'transferring' are counted as unconfirmed (visible in UI as "Sending").
|
|
6014
6385
|
*/
|
|
6015
6386
|
aggregateTokens(coinId) {
|
|
6016
6387
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
6017
6388
|
for (const token of this.tokens.values()) {
|
|
6018
|
-
if (token.status === "spent" || token.status === "invalid"
|
|
6389
|
+
if (token.status === "spent" || token.status === "invalid") continue;
|
|
6019
6390
|
if (coinId && token.coinId !== coinId) continue;
|
|
6020
6391
|
const key = token.coinId;
|
|
6021
6392
|
const amount = BigInt(token.amount);
|
|
6022
6393
|
const isConfirmed = token.status === "confirmed";
|
|
6394
|
+
const isTransferring = token.status === "transferring";
|
|
6023
6395
|
const existing = assetsMap.get(key);
|
|
6024
6396
|
if (existing) {
|
|
6025
6397
|
if (isConfirmed) {
|
|
@@ -6029,6 +6401,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6029
6401
|
existing.unconfirmedAmount += amount;
|
|
6030
6402
|
existing.unconfirmedTokenCount++;
|
|
6031
6403
|
}
|
|
6404
|
+
if (isTransferring) existing.transferringTokenCount++;
|
|
6032
6405
|
} else {
|
|
6033
6406
|
assetsMap.set(key, {
|
|
6034
6407
|
coinId: token.coinId,
|
|
@@ -6039,7 +6412,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6039
6412
|
confirmedAmount: isConfirmed ? amount : 0n,
|
|
6040
6413
|
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
6041
6414
|
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
6042
|
-
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
6415
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1,
|
|
6416
|
+
transferringTokenCount: isTransferring ? 1 : 0
|
|
6043
6417
|
});
|
|
6044
6418
|
}
|
|
6045
6419
|
}
|
|
@@ -6057,6 +6431,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6057
6431
|
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
6058
6432
|
confirmedTokenCount: raw.confirmedTokenCount,
|
|
6059
6433
|
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
6434
|
+
transferringTokenCount: raw.transferringTokenCount,
|
|
6060
6435
|
priceUsd: null,
|
|
6061
6436
|
priceEur: null,
|
|
6062
6437
|
change24h: null,
|
|
@@ -6909,6 +7284,33 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6909
7284
|
}
|
|
6910
7285
|
}
|
|
6911
7286
|
}
|
|
7287
|
+
/**
|
|
7288
|
+
* Import history entries from remote TXF data into local store.
|
|
7289
|
+
* Delegates to the local TokenStorageProvider's importHistoryEntries() for
|
|
7290
|
+
* persistent storage, with in-memory fallback.
|
|
7291
|
+
* Reused by both load() (initial IPFS fetch) and _doSync() (merge result).
|
|
7292
|
+
*/
|
|
7293
|
+
async importRemoteHistoryEntries(entries) {
|
|
7294
|
+
if (entries.length === 0) return 0;
|
|
7295
|
+
const provider = this.getLocalTokenStorageProvider();
|
|
7296
|
+
if (provider?.importHistoryEntries) {
|
|
7297
|
+
const imported2 = await provider.importHistoryEntries(entries);
|
|
7298
|
+
if (imported2 > 0) {
|
|
7299
|
+
this._historyCache = await provider.getHistoryEntries();
|
|
7300
|
+
}
|
|
7301
|
+
return imported2;
|
|
7302
|
+
}
|
|
7303
|
+
const existingKeys = new Set(this._historyCache.map((e) => e.dedupKey));
|
|
7304
|
+
let imported = 0;
|
|
7305
|
+
for (const entry of entries) {
|
|
7306
|
+
if (!existingKeys.has(entry.dedupKey)) {
|
|
7307
|
+
this._historyCache.push(entry);
|
|
7308
|
+
existingKeys.add(entry.dedupKey);
|
|
7309
|
+
imported++;
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
return imported;
|
|
7313
|
+
}
|
|
6912
7314
|
/**
|
|
6913
7315
|
* Get the first local token storage provider (for history operations).
|
|
6914
7316
|
*/
|
|
@@ -7156,6 +7558,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7156
7558
|
if (this.nametags.length === 0 && savedNametags.length > 0) {
|
|
7157
7559
|
this.nametags = savedNametags;
|
|
7158
7560
|
}
|
|
7561
|
+
const txfData = result.merged;
|
|
7562
|
+
if (txfData._history && txfData._history.length > 0) {
|
|
7563
|
+
const imported = await this.importRemoteHistoryEntries(txfData._history);
|
|
7564
|
+
if (imported > 0) {
|
|
7565
|
+
this.log(`Imported ${imported} history entries from IPFS sync`);
|
|
7566
|
+
}
|
|
7567
|
+
}
|
|
7159
7568
|
totalAdded += result.added;
|
|
7160
7569
|
totalRemoved += result.removed;
|
|
7161
7570
|
}
|
|
@@ -7454,7 +7863,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7454
7863
|
/**
|
|
7455
7864
|
* Handle NOSTR-FIRST commitment-only transfer (recipient side)
|
|
7456
7865
|
* This is called when receiving a transfer with only commitmentData and no proof yet.
|
|
7457
|
-
*
|
|
7866
|
+
* Delegates to saveCommitmentOnlyToken() helper, then emits event + records history.
|
|
7458
7867
|
*/
|
|
7459
7868
|
async handleCommitmentOnlyTransfer(transfer, payload) {
|
|
7460
7869
|
try {
|
|
@@ -7464,41 +7873,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7464
7873
|
console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
|
|
7465
7874
|
return;
|
|
7466
7875
|
}
|
|
7467
|
-
const
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
decimals: tokenInfo.decimals,
|
|
7474
|
-
iconUrl: tokenInfo.iconUrl,
|
|
7475
|
-
amount: tokenInfo.amount,
|
|
7476
|
-
status: "submitted",
|
|
7477
|
-
// NOSTR-FIRST: unconfirmed until proof
|
|
7478
|
-
createdAt: Date.now(),
|
|
7479
|
-
updatedAt: Date.now(),
|
|
7480
|
-
sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
|
|
7481
|
-
};
|
|
7482
|
-
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7483
|
-
const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
7484
|
-
if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
|
|
7485
|
-
this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
|
|
7486
|
-
return;
|
|
7487
|
-
}
|
|
7488
|
-
this.tokens.set(token.id, token);
|
|
7489
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: saving token id=${token.id.slice(0, 16)} status=${token.status} sdkData.length=${token.sdkData?.length}`);
|
|
7490
|
-
await this.save();
|
|
7491
|
-
console.log(`[Payments][DEBUG] NOSTR-FIRST: save() completed, tokens.size=${this.tokens.size}`);
|
|
7876
|
+
const token = await this.saveCommitmentOnlyToken(
|
|
7877
|
+
sourceTokenInput,
|
|
7878
|
+
commitmentInput,
|
|
7879
|
+
transfer.senderTransportPubkey
|
|
7880
|
+
);
|
|
7881
|
+
if (!token) return;
|
|
7492
7882
|
const senderInfo = await this.resolveSenderInfo(transfer.senderTransportPubkey);
|
|
7493
|
-
|
|
7883
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
7494
7884
|
id: transfer.id,
|
|
7495
7885
|
senderPubkey: transfer.senderTransportPubkey,
|
|
7496
7886
|
senderNametag: senderInfo.senderNametag,
|
|
7497
7887
|
tokens: [token],
|
|
7498
7888
|
memo: payload.memo,
|
|
7499
7889
|
receivedAt: transfer.timestamp
|
|
7500
|
-
};
|
|
7501
|
-
|
|
7890
|
+
});
|
|
7891
|
+
const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
7502
7892
|
await this.addToHistory({
|
|
7503
7893
|
type: "RECEIVED",
|
|
7504
7894
|
amount: token.amount,
|
|
@@ -7510,29 +7900,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7510
7900
|
memo: payload.memo,
|
|
7511
7901
|
tokenId: nostrTokenId || token.id
|
|
7512
7902
|
});
|
|
7513
|
-
try {
|
|
7514
|
-
const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
|
|
7515
|
-
const requestIdBytes = commitment.requestId;
|
|
7516
|
-
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
7517
|
-
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
7518
|
-
if (stClient) {
|
|
7519
|
-
const response = await stClient.submitTransferCommitment(commitment);
|
|
7520
|
-
this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
|
|
7521
|
-
}
|
|
7522
|
-
this.addProofPollingJob({
|
|
7523
|
-
tokenId: token.id,
|
|
7524
|
-
requestIdHex,
|
|
7525
|
-
commitmentJson: JSON.stringify(commitmentInput),
|
|
7526
|
-
startedAt: Date.now(),
|
|
7527
|
-
attemptCount: 0,
|
|
7528
|
-
lastAttemptAt: 0,
|
|
7529
|
-
onProofReceived: async (tokenId) => {
|
|
7530
|
-
await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput);
|
|
7531
|
-
}
|
|
7532
|
-
});
|
|
7533
|
-
} catch (err) {
|
|
7534
|
-
console.error("[Payments] Failed to parse commitment for proof polling:", err);
|
|
7535
|
-
}
|
|
7536
7903
|
} catch (error) {
|
|
7537
7904
|
console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
|
|
7538
7905
|
}
|
|
@@ -7651,6 +8018,28 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7651
8018
|
try {
|
|
7652
8019
|
const payload = transfer.payload;
|
|
7653
8020
|
console.log("[Payments][DEBUG] handleIncomingTransfer: keys=", Object.keys(payload).join(","));
|
|
8021
|
+
let combinedBundle = null;
|
|
8022
|
+
if (isCombinedTransferBundleV6(payload)) {
|
|
8023
|
+
combinedBundle = payload;
|
|
8024
|
+
} else if (payload.token) {
|
|
8025
|
+
try {
|
|
8026
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
8027
|
+
if (isCombinedTransferBundleV6(inner)) {
|
|
8028
|
+
combinedBundle = inner;
|
|
8029
|
+
}
|
|
8030
|
+
} catch {
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8033
|
+
if (combinedBundle) {
|
|
8034
|
+
this.log("Processing COMBINED_TRANSFER V6 bundle...");
|
|
8035
|
+
try {
|
|
8036
|
+
await this.processCombinedTransferBundle(combinedBundle, transfer.senderTransportPubkey);
|
|
8037
|
+
this.log("COMBINED_TRANSFER V6 processed successfully");
|
|
8038
|
+
} catch (err) {
|
|
8039
|
+
console.error("[Payments] COMBINED_TRANSFER V6 processing error:", err);
|
|
8040
|
+
}
|
|
8041
|
+
return;
|
|
8042
|
+
}
|
|
7654
8043
|
let instantBundle = null;
|
|
7655
8044
|
if (isInstantSplitBundle(payload)) {
|
|
7656
8045
|
instantBundle = payload;
|
|
@@ -7802,17 +8191,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7802
8191
|
memo: payload.memo,
|
|
7803
8192
|
tokenId: incomingTokenId || token.id
|
|
7804
8193
|
});
|
|
8194
|
+
const incomingTransfer = {
|
|
8195
|
+
id: transfer.id,
|
|
8196
|
+
senderPubkey: transfer.senderTransportPubkey,
|
|
8197
|
+
senderNametag: senderInfo.senderNametag,
|
|
8198
|
+
tokens: [token],
|
|
8199
|
+
memo: payload.memo,
|
|
8200
|
+
receivedAt: transfer.timestamp
|
|
8201
|
+
};
|
|
8202
|
+
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
8203
|
+
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
8204
|
+
} else {
|
|
8205
|
+
this.log(`Duplicate transfer ignored: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7805
8206
|
}
|
|
7806
|
-
const incomingTransfer = {
|
|
7807
|
-
id: transfer.id,
|
|
7808
|
-
senderPubkey: transfer.senderTransportPubkey,
|
|
7809
|
-
senderNametag: senderInfo.senderNametag,
|
|
7810
|
-
tokens: [token],
|
|
7811
|
-
memo: payload.memo,
|
|
7812
|
-
receivedAt: transfer.timestamp
|
|
7813
|
-
};
|
|
7814
|
-
this.deps.emitEvent("transfer:incoming", incomingTransfer);
|
|
7815
|
-
this.log(`Incoming transfer processed: ${token.id}, ${token.amount} ${token.symbol}`);
|
|
7816
8207
|
} catch (error) {
|
|
7817
8208
|
console.error("[Payments] Failed to process incoming transfer:", error);
|
|
7818
8209
|
}
|
|
@@ -7881,6 +8272,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7881
8272
|
return data ? JSON.parse(data) : [];
|
|
7882
8273
|
}
|
|
7883
8274
|
async createStorageData() {
|
|
8275
|
+
const sorted = [...this._historyCache].sort((a, b) => b.timestamp - a.timestamp);
|
|
7884
8276
|
return await buildTxfStorageData(
|
|
7885
8277
|
Array.from(this.tokens.values()),
|
|
7886
8278
|
{
|
|
@@ -7892,7 +8284,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7892
8284
|
nametags: this.nametags,
|
|
7893
8285
|
tombstones: this.tombstones,
|
|
7894
8286
|
archivedTokens: this.archivedTokens,
|
|
7895
|
-
forkedTokens: this.forkedTokens
|
|
8287
|
+
forkedTokens: this.forkedTokens,
|
|
8288
|
+
historyEntries: sorted.slice(0, MAX_SYNCED_HISTORY_ENTRIES)
|
|
7896
8289
|
}
|
|
7897
8290
|
);
|
|
7898
8291
|
}
|
|
@@ -16925,6 +17318,7 @@ function createPriceProvider(config) {
|
|
|
16925
17318
|
identityFromMnemonicSync,
|
|
16926
17319
|
initSphere,
|
|
16927
17320
|
isArchivedKey,
|
|
17321
|
+
isCombinedTransferBundleV6,
|
|
16928
17322
|
isForkedKey,
|
|
16929
17323
|
isInstantSplitBundle,
|
|
16930
17324
|
isInstantSplitBundleV4,
|