From 09a558eaf074aee029b8aaecfa2fa6108bd68037 Mon Sep 17 00:00:00 2001 From: Matt Hess Date: Fri, 13 Feb 2026 23:37:58 +0000 Subject: [PATCH] =?UTF-8?q?=20=20Support=20pre-CARROT=20STAKE=E2=86=94RETU?= =?UTF-8?q?RN=20matching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The return pubkey is explicit in the tx prefix for all STAKE types: CARROT (v>=4) stores it in protocol_tx_data.return_pubkey, pre-CARROT stores it as a top-level prefix field. Use it as the primary matching key instead of relying solely on the CARROT _returnOutputMap path. - Extract return_pubkey from prefix when recording StakeRecords - Match PROTOCOL outputs by publicKey against stored return_pubkey as fallback when CARROT returnOriginKey isn't set - Pass return_address/return_pubkey/protocol_tx_data through _convertJsonToTx so JSON fallback path has the same fields --- src/wallet-sync.js | 62 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/src/wallet-sync.js b/src/wallet-sync.js index dd0dbe1..67f3a04 100644 --- a/src/wallet-sync.js +++ b/src/wallet-sync.js @@ -869,7 +869,7 @@ export class WalletSync { await this.storage.putTransaction(walletTx); // Record stake lifecycle (STAKE creation / PROTOCOL return matching) - await this._recordStakeLifecycle(txType, isProtocolTx, txHash, header, ownedOutputs, spentOutputs, walletTx); + await this._recordStakeLifecycle(txType, isProtocolTx, txHash, header, ownedOutputs, spentOutputs, walletTx, tx); if (ownedOutputs.length > 0) { this._emit('outputReceived', { @@ -974,7 +974,7 @@ export class WalletSync { await this.storage.putTransaction(walletTx); // Record stake lifecycle (STAKE creation / PROTOCOL return matching) - await this._recordStakeLifecycle(txType, isProtocolTx, txHash, header, ownedOutputs, spentOutputs, walletTx); + await this._recordStakeLifecycle(txType, isProtocolTx, txHash, header, ownedOutputs, spentOutputs, walletTx, tx); // Emit events if (ownedOutputs.length > 0) { @@ -1110,7 +1110,17 @@ export class WalletSync { vout, extra, type: txJson.type || 0, - amount_burnt: this._safeBigInt(txJson.amount_burnt) + amount_burnt: this._safeBigInt(txJson.amount_burnt), + // Salvium staking/return fields + return_address: txJson.return_address ? hexToBytes(txJson.return_address) : null, + return_pubkey: txJson.return_pubkey ? hexToBytes(txJson.return_pubkey) : null, + protocol_tx_data: txJson.protocol_tx_data ? { + version: txJson.protocol_tx_data.version || 0, + return_address: txJson.protocol_tx_data.return_address ? hexToBytes(txJson.protocol_tx_data.return_address) : null, + return_pubkey: txJson.protocol_tx_data.return_pubkey ? hexToBytes(txJson.protocol_tx_data.return_pubkey) : null, + return_view_tag: txJson.protocol_tx_data.return_view_tag ? hexToBytes(txJson.protocol_tx_data.return_view_tag) : null, + return_anchor_enc: txJson.protocol_tx_data.return_anchor_enc ? hexToBytes(txJson.protocol_tx_data.return_anchor_enc) : null + } : null }; // RCT signatures (minimal for coinbase) @@ -1313,7 +1323,7 @@ export class WalletSync { await this.storage.putTransaction(walletTx); // Record stake lifecycle (STAKE creation / PROTOCOL return matching) - await this._recordStakeLifecycle(txType, isProtocolTx, txHash, header, ownedOutputs, spentOutputs, walletTx); + await this._recordStakeLifecycle(txType, isProtocolTx, txHash, header, ownedOutputs, spentOutputs, walletTx, tx); // Emit events if (ownedOutputs.length > 0) { @@ -2123,6 +2133,28 @@ export class WalletSync { return null; } + /** + * Extract the return pubkey from a STAKE transaction prefix. + * CARROT (v>=4): protocol_tx_data.return_pubkey + * Pre-CARROT: prefix.return_pubkey (top-level) + * @private + * @returns {string|null} Return pubkey as hex, or null + */ + _extractReturnPubkey(tx) { + const prefix = tx.prefix || tx; + // CARROT STAKE (version >= 4): protocol_tx_data.return_pubkey + if (prefix.protocol_tx_data?.return_pubkey) { + const rp = prefix.protocol_tx_data.return_pubkey; + return typeof rp === 'string' ? rp : bytesToHex(rp); + } + // Pre-CARROT: top-level return_pubkey + if (prefix.return_pubkey) { + const rp = prefix.return_pubkey; + return typeof rp === 'string' ? rp : bytesToHex(rp); + } + return null; + } + /** * Record stake lifecycle events (STAKE creation + PROTOCOL return matching) * @private @@ -2133,10 +2165,14 @@ export class WalletSync { * @param {Array} ownedOutputs - WalletOutput instances we own in this tx * @param {Array} spentOutputs - Outputs spent by this tx * @param {WalletTransaction} walletTx - The wallet transaction record + * @param {Object} tx - The parsed transaction (for prefix field access) */ - async _recordStakeLifecycle(txType, isProtocolTx, txHash, header, ownedOutputs, spentOutputs, walletTx) { + async _recordStakeLifecycle(txType, isProtocolTx, txHash, header, ownedOutputs, spentOutputs, walletTx, tx) { // Record new STAKE if (txType === TX_TYPE.STAKE && spentOutputs.length > 0) { + // Prefer explicit return_pubkey from prefix (works for both CARROT and pre-CARROT). + // Fall back to CARROT change output key for self-send-based matching. + const returnPubkey = this._extractReturnPubkey(tx); const changeOutput = ownedOutputs.find(o => o); await this.storage.putStake(new StakeRecord({ stakeTxHash: txHash, @@ -2145,13 +2181,14 @@ export class WalletSync { amountStaked: walletTx.amountBurnt, fee: walletTx.fee, assetType: changeOutput?.assetType || 'SAL', - changeOutputKey: changeOutput?.publicKey || null + changeOutputKey: returnPubkey || changeOutput?.publicKey || null })); } // Match PROTOCOL returns to their originating STAKE if (isProtocolTx && ownedOutputs.length > 0) { for (const output of ownedOutputs) { + // Path 1: CARROT — returnOriginKey set by _scanCarrotOutput via _returnOutputMap if (output.returnOriginKey) { const stake = await this.storage.getStakeByOutputKey(output.returnOriginKey); if (stake) { @@ -2161,6 +2198,19 @@ export class WalletSync { returnTimestamp: header.timestamp, returnAmount: output.amount }); + continue; + } + } + // Path 2: Pre-CARROT — match output public key against stored return_pubkey + if (output.publicKey) { + const stake = await this.storage.getStakeByOutputKey(output.publicKey); + if (stake && stake.status === 'locked') { + await this.storage.markStakeReturned(stake.stakeTxHash, { + returnTxHash: txHash, + returnHeight: header.height, + returnTimestamp: header.timestamp, + returnAmount: output.amount + }); } } }