Skip to content

feat(wallet(-dashboard)): support partial unstake#10064

Open
Copilot wants to merge 18 commits intodevelopfrom
copilot/add-partial-unstake-functionality
Open

feat(wallet(-dashboard)): support partial unstake#10064
Copilot wants to merge 18 commits intodevelopfrom
copilot/add-partial-unstake-functionality

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 4, 2026

Description of change

Implements partial unstaking for both normal and timelocked stakes. Users can unstake portions of their principal while receiving proportionally calculated rewards. Enforces Move contract's MIN_STAKING_THRESHOLD (1 IOTA) for both unstake and remaining amounts.

Example: 100 IOTA staked + 10 IOTA rewards → unstake 10 IOTA → receive 11 IOTA (10 principal + 1 proportional reward)

image Unstake tx: https://explorer.iota.org/txblock/J935pv8zmvbCqjCdtXoPcAYADQdMyMkJsLFKYdGfEh1R?network=devnet

Implementation

Transaction Builders

  • createPartialUnstakeTransaction: Calls 0x3::staking_pool::split then 0x3::iota_system::request_withdraw_stake
  • createPartialTimelockedUnstakeTransaction: Uses 0x3::timelocked_staking::split variant

Hooks

  • useNewPartialUnstakeTransaction: Builds transaction, dry-runs for gas estimation
  • useNewPartialUnstakeTimelockedTransaction: Timelocked variant with max size handling

UI Components (wallet-dashboard & wallet app)

  • Toggle between full/partial unstake modes
  • Numeric input with real-time proportional reward calculation
  • Validation: unstakeAmount ≥ 1 IOTA && (remainingAmount ≥ 1 IOTA || remainingAmount === 0)
  • Helpful UI hints about minimum staking requirements

Proportional Rewards Formula

proportionalRewards = (totalRewards × unstakeAmount) ÷ principalAmount

Validation Examples

Principal: 2 IOTA, Rewards: 0.4 IOTA
✓ Unstake 1 IOTA → receive 1.2 IOTA (1 IOTA remains)
✗ Unstake 0.5 IOTA → error: minimum 1 IOTA
✗ Unstake 1.5 IOTA → error: remaining 0.5 IOTA < minimum
✓ Unstake 2 IOTA → receive 2.4 IOTA (full unstake)

How the change has been tested

  • Basic tests (linting, compilation, formatting, unit/integration tests)
  • Patch-specific tests (correctness, functionality coverage)
  • I have added tests that prove my fix is effective or that my feature works
  • I have checked that new and existing unit tests pass locally with my changes

Validation logic tested with 8 edge cases including MIN_STAKING_THRESHOLD enforcement. CodeQL security scan passed (0 alerts). Proportional rewards calculation verified against reference implementation. Input parsing tested for edge cases (NaN, empty strings, invalid inputs).

Original prompt

This section details on the original issue you should resolve

<issue_title>Allow partial unstake</issue_title>
<issue_description>> the API of both StakedIota and TimelockedStakedIota supports splitting already, so partial unstaking is just a matter of adding the func in the wallet

We should study how best we could do this and try implementing it for both normal and timelocked stake</issue_description>

<agent_instructions>If a user unstakes some amount, he also gets the rewards proportional to that amount back, make this clear in the UI, show the unstaked amount and the rewards for that
So for example if one has 100 IOTA staked and 10 IOTA as rewards, if one unstakes 10 IOTA, one would get 11 back, because of the rewards for the 10 IOTA are 1 IOTA

Unstake tx can be built like this

let stakedIotaObjectId = '0x1e0a56e50e4c194fe42dd27206248163842d81431db6c29f8ce6f5d0606dabe9';
let timelocked = false;
let unstakeAmount = 1_000_000_000 // amount in NANO
const tx = new Transaction();
const splitStakedIota = tx.moveCall({
    target: timelocked ? '0x3::timelocked_staking::split' : '0x3::staking_pool::split',
    arguments: [tx.object(stakedIotaObjectId), tx.pure.u64(unstakeAmount)],
})
tx.moveCall({
    target: timelocked ? '0x3::timelocked_staking::request_withdraw_stake' : '0x3::iota_system::request_withdraw_stake',
    arguments: [
        tx.object('0x5'), // system state object
        splitStakedIota
    ],
});
tx.setSender(address);

</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@Thoralf-M Might be useful ``` # Partially withdraw staked IOTAs (potential rewards are also split and withdrawn additionally to the provided amount): iota client ptb \ --assign staked_iota @0x08dff8b855cc48c5f4e1911a4b20d4d3805d12443fc824bc263172f0ce68aaaa \ --assign amount_to_withdraw 1000000000 \ --move-call 0x3::staking_pool::split staked_iota amount_to_withdraw \ --assign split_staked_iota \ --move-call 0x3::iota_system::request_withdraw_stake @0x5 split_staked_iota \ --gas-budget 100000000 ```

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
apps-backend Ready Ready Preview, Comment Apr 14, 2026 8:39am
iota-evm-bridge Ready Ready Preview, Comment Apr 14, 2026 8:39am
rebased-explorer Ready Ready Preview, Comment Apr 14, 2026 8:39am
wallet-dashboard Ready Ready Preview Apr 14, 2026 8:39am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
apps-ui-kit Ignored Ignored Preview Apr 14, 2026 8:39am
iota-multisig-toolkit Skipped Skipped Apr 14, 2026 8:39am

Request Review

@vercel vercel bot temporarily deployed to Preview – wallet-dashboard February 4, 2026 08:12 Inactive
@vercel vercel bot temporarily deployed to Preview – rebased-explorer February 4, 2026 08:12 Inactive
@vercel vercel bot temporarily deployed to Preview – apps-backend February 4, 2026 08:12 Inactive
@vercel vercel bot temporarily deployed to Preview – apps-ui-kit February 4, 2026 08:12 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-evm-bridge February 4, 2026 08:12 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-multisig-toolkit February 4, 2026 08:12 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-multisig-toolkit February 4, 2026 08:17 Inactive
@vercel vercel bot temporarily deployed to Preview – apps-backend February 4, 2026 08:19 Inactive
@vercel vercel bot temporarily deployed to Preview – apps-backend February 4, 2026 08:19 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-evm-bridge February 4, 2026 08:19 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-multisig-toolkit February 4, 2026 08:19 Inactive
@vercel vercel bot temporarily deployed to Preview – rebased-explorer February 4, 2026 08:19 Inactive
Comment thread apps/wallet-dashboard/components/dialogs/unstake/views/UnstakeView.tsx Outdated
Comment thread apps/core/src/hooks/stake/useNewPartialUnstakeTransaction.ts Outdated
@vercel vercel bot temporarily deployed to Preview – iota-evm-bridge February 4, 2026 08:21 Inactive
@vercel vercel bot temporarily deployed to Preview – apps-backend February 4, 2026 08:21 Inactive
@vercel vercel bot temporarily deployed to Preview – rebased-explorer February 4, 2026 08:21 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-multisig-toolkit February 4, 2026 08:21 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-evm-bridge February 4, 2026 08:22 Inactive
@vercel vercel bot temporarily deployed to Preview – rebased-explorer February 4, 2026 08:22 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-multisig-toolkit February 4, 2026 08:22 Inactive
@vercel vercel bot temporarily deployed to Preview – apps-backend February 4, 2026 08:22 Inactive
@vercel vercel bot temporarily deployed to Preview – apps-backend February 4, 2026 08:26 Inactive
@vercel vercel bot temporarily deployed to Preview – apps-backend February 15, 2026 13:31 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-evm-bridge February 15, 2026 13:31 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-multisig-toolkit February 15, 2026 13:31 Inactive
@vercel vercel bot temporarily deployed to Preview – rebased-explorer February 15, 2026 13:31 Inactive
@vercel vercel bot temporarily deployed to Preview – apps-backend February 15, 2026 19:08 Inactive
@vercel vercel bot temporarily deployed to Preview – iota-evm-bridge February 15, 2026 19:08 Inactive
@Thoralf-M
Copy link
Copy Markdown
Member

Let me know if I should revert the wallet changes to keep the code less complex there

Copilot AI and others added 13 commits March 30, 2026 09:37
Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com>
Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com>
…ages

Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com>
Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com>
Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com>
Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com>
Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com>
(!isPartialUnstake && remainingAmount < MIN_STAKING_THRESHOLD);

// Determine the appropriate error message
const getErrorMessage = () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For staking we use yup validation. Better use it instead of manual manage errors.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When click on "partial unstake" button, user see error. Better to don't trigger validation before user change value. I think it can be fixed by validationSchema.

currentEpochEndTime > 0 ? currentEpochEndTimeAgo : `Epoch #${epoch}`;

const maxUnstakeAmount = Number(principalAmount) / Number(NANOS_PER_IOTA);
const MIN_STAKING_THRESHOLD = 1_000_000_000n; // 1 IOTA in nanos
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to keep this value in the apps/core folder, so we can reuse it.

const currentEpochEndTimeFormatted =
currentEpochEndTime > 0 ? currentEpochEndTimeAgo : `Epoch #${epoch}`;

const maxUnstakeAmount = Number(principalAmount) / Number(NANOS_PER_IOTA);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd think about moving partial unstake logic to separate hook. If it's possible - reuse it between dashboard & wallet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or at least create utility reusable functions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For utility function would be good to cover it by unit tests.

Copy link
Copy Markdown
Contributor

@panteleymonchuk panteleymonchuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me and works ⭐️

I'd suggest to improve it a bit to make code more clear.

@Thoralf-M
Copy link
Copy Markdown
Member

Looks good to me and works ⭐️

I'd suggest to improve it a bit to make code more clear.

Thanks for the review, I did some changes in c79c124
Feel free to also just push changes if you want something in a different way as I'm not familiar with the codebase

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

wallet Issues related to the IOTA Wallet wallet-dashboard Issues related to the Wallet Dashboard dApp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow partial unstake

4 participants