diff --git a/chain_params.prod.json b/chain_params.prod.json index edd2249a..74f36d0f 100644 --- a/chain_params.prod.json +++ b/chain_params.prod.json @@ -32,7 +32,8 @@ "proposalFeeConfirmRequirement": 6, "maxPaymentCycles": 6, "maxPayment": 43200000000000, - "defaultColdStakingAddress": "SdgQDpS8jDRJDX8yK8m9KnTMarsE84zdsy" + "defaultColdStakingAddress": "SdgQDpS8jDRJDX8yK8m9KnTMarsE84zdsy", + "stakeSplitTarget": 50000000000 }, "testnet": { "name": "testnet", @@ -64,6 +65,7 @@ "proposalFeeConfirmRequirement": 3, "maxPaymentCycles": 20, "maxPayment": 144000000000, - "defaultColdStakingAddress": "WmNziUEPyhnUkiVdfsiNX93H6rSJnios44" + "defaultColdStakingAddress": "WmNziUEPyhnUkiVdfsiNX93H6rSJnios44", + "stakeSplitTarget": 50000000000 } } diff --git a/chain_params.test.json b/chain_params.test.json index bea7b0db..3ef7fe57 100644 --- a/chain_params.test.json +++ b/chain_params.test.json @@ -28,7 +28,8 @@ "proposalFeeConfirmRequirement": 6, "maxPaymentCycles": 6, "maxPayment": 43200000000000, - "defaultColdStakingAddress": "SdgQDpS8jDRJDX8yK8m9KnTMarsE84zdsy" + "defaultColdStakingAddress": "SdgQDpS8jDRJDX8yK8m9KnTMarsE84zdsy", + "stakeSplitTarget": 50000000000 }, "testnet": { "name": "testnet", @@ -60,6 +61,7 @@ "proposalFeeConfirmRequirement": 3, "maxPaymentCycles": 20, "maxPayment": 144000000000, - "defaultColdStakingAddress": "WmNziUEPyhnUkiVdfsiNX93H6rSJnios44" + "defaultColdStakingAddress": "WmNziUEPyhnUkiVdfsiNX93H6rSJnios44", + "stakeSplitTarget": 50000000000 } } diff --git a/scripts/wallet.js b/scripts/wallet.js index 1417ee3f..561c5798 100644 --- a/scripts/wallet.js +++ b/scripts/wallet.js @@ -1164,11 +1164,26 @@ export class Wallet { // Add primary output if (isDelegation) { if (!returnAddress) returnAddress = this.getNewChangeAddress(); - transactionBuilder.addColdStakeOutput({ - address: returnAddress, - addressColdStake: address, - value, - }); + // The per-output target for maximum staking efficiency + const nTarget = cChainParams.current.stakeSplitTarget; + // Generate optimal staking outputs + if (value < COIN) { + throw new Error('below consensus'); + } else if (value < nTarget) { + transactionBuilder.addColdStakeOutput({ + address: returnAddress, + addressColdStake: address, + value, + }); + } else { + for (let i = 0; i < Math.floor(value / nTarget); i++) { + transactionBuilder.addColdStakeOutput({ + address: returnAddress, + addressColdStake: address, + value: i === 0 ? nTarget + (value % nTarget) : nTarget, + }); + } + } } else if (isProposal) { transactionBuilder.addProposalOutput({ hash: address, @@ -1198,7 +1213,8 @@ export class Wallet { } const fee = transactionBuilder.getFee(); - const changeValue = transactionBuilder.valueIn - value - fee; + const changeValue = + transactionBuilder.valueIn - transactionBuilder.valueOut - fee; if (changeValue < 0) { if (!subtractFeeFromAmt) { throw new Error('Not enough balance'); diff --git a/tests/unit/wallet/transactions.spec.js b/tests/unit/wallet/transactions.spec.js index 2b2d0673..4a26676a 100644 --- a/tests/unit/wallet/transactions.spec.js +++ b/tests/unit/wallet/transactions.spec.js @@ -14,6 +14,7 @@ import { import 'fake-indexeddb/auto'; import { TransactionBuilder } from '../../../scripts/transaction_builder.js'; +import { cChainParams } from '../../../scripts/chain_params.js'; vi.stubGlobal('localStorage', { length: 0 }); vi.mock('../../../scripts/global.js'); @@ -169,9 +170,11 @@ describe('Wallet transaction tests', () => { }); it('Creates a cold stake tx correctly', async () => { + // Delegate 5250 PIV to test Stake Pre-Splitting + const value = 5250 * 10 ** 8; const tx = wallet.createTransaction( 'SR3L4TFUKKGNsnv2Q4hWTuET2a4vHpm1b9', - 0.05 * 10 ** 8, + value, { isDelegation: true } ); expect(tx.version).toBe(1); @@ -184,26 +187,32 @@ describe('Wallet transaction tests', () => { scriptSig: '76a914f49b25384b79685227be5418f779b98a6be4c73888ac', // Script sig must be the UTXO script since it's not signed }) ); - expect(tx.vout[1]).toStrictEqual( - new CTxOut({ - script: '76a914f49b25384b79685227be5418f779b98a6be4c73888ac', - value: 4997470, - }) - ); + // The 'after split' output expect(tx.vout[0]).toStrictEqual( new CTxOut({ script: '76a97b63d114291a25b5b4d1802e0611e9bf724a1e57d9210e826714f49b25384b79685227be5418f779b98a6be4c7386888ac', - value: 5000000, + value: + cChainParams.current.stakeSplitTarget + + (value % cChainParams.current.stakeSplitTarget), }) ); + // The split outputs (depending on chainparam 'stakeSplitTarget') + for (const cOut of tx.vout.slice(1, tx.vout.length - 1)) { + expect(cOut).toStrictEqual( + new CTxOut({ + script: '76a97b63d114291a25b5b4d1802e0611e9bf724a1e57d9210e826714f49b25384b79685227be5418f779b98a6be4c7386888ac', + value: cChainParams.current.stakeSplitTarget, + }) + ); + } await checkFees(wallet, tx, MIN_FEE_PER_BYTE); }); - it('creates a tx with max balance', async () => { + it('Creates a tx with max balance', async () => { const tx = wallet.createTransaction( 'SR3L4TFUKKGNsnv2Q4hWTuET2a4vHpm1b9', legacyMainnetInitialBalance(), - { isDelegation: true } + { isDelegation: false } ); expect(tx.version).toBe(1); expect(tx.vin).toHaveLength(2); @@ -218,9 +227,10 @@ describe('Wallet transaction tests', () => { ); expect(tx.vout).toHaveLength(1); const fees = await checkFees(wallet, tx, MIN_FEE_PER_BYTE); + // The 'after split' output expect(tx.vout[0]).toStrictEqual( new CTxOut({ - script: '76a97b63d114291a25b5b4d1802e0611e9bf724a1e57d9210e826714f49b25384b79685227be5418f779b98a6be4c7386888ac', + script: '76a914291a25b5b4d1802e0611e9bf724a1e57d9210e8288ac', value: legacyMainnetInitialBalance() - fees, }) );