Environment
- OneKey Desktop 6.5.0 (macOS arm64, in-app dApp browser)
- Hardware: OneKey device, firmware 4.21.0 (hardware itself signs the tx correctly — verified separately)
- dApp: Sagat multisig (
sagat.mystenlabs.com), Sui mainnet
- dApp SDK:
@mysten/sui 2.16.0, via wallet-standard sui:signTransaction
Summary
When a dApp asks OneKey to sign a Sui transaction whose sender is not the connected account (the standard multisig flow: sender = the multisig address, signer = one member), the sign modal gets stuck at fee estimation with a node error.
Root cause (pinned by direct API probing, see below): on non-custom networks the client does no local dry-run; it POSTs to /wallet/v1/account/estimate-fee with accountAddress = connected account. The backend then simulates the transaction using accountAddress as the sender, ignoring the sender embedded in encodedTx.rawTx. The client-side code paths all preserve the embedded sender correctly — the swap happens server-side.
For transactions using SIP-58 FundsWithdrawal (withdrawFrom: Sender), this is fatal: the withdrawal is resolved against the member's (empty) address balance instead of the multisig's, and the node rejects it:
Error checking transaction input objects: Invalid withdraw reservation:
Available amount in account for object id 0xa0391889481986c37fe2a170a9fe2ed9835a47a3aac74d0f8ca5ce97ae993afc
is less than requested: 0 < 1
Steps to reproduce
- Create a Sui multisig (threshold N) where one member key lives on a OneKey device; fund the multisig's address balance (SIP-58 funds, e.g. USDC) — not Coin objects.
- In Sagat, create a proposal that spends the address balance (
0x2::balance::redeem_funds + send_funds, input FundsWithdrawal { withdrawFrom: Sender }).
- Open Sagat in OneKey Desktop 6.5.0's in-app browser, connect the member account, and try to sign the proposal.
Expected: OneKey signs the exact transaction (sender = multisig 0x7a44c9…).
Actual: error above; signing is blocked.
Evidence that the sender was swapped
The accumulator object id in the error is a deterministic fingerprint of (address, coin type):
(multisig 0x7a44c99ce19af6d7…, USDC) → 0x4a237736613e2e14… (holds 9000 base units)
(member 0x182c1fe77c44a1f3…, USDC) → 0xa0391889481986c3… ← the id in the error (holds 0)
So the checked transaction's sender was the member, not the multisig. Reproducible locally:
// dry-run the same PTB with sender = member → byte-identical error, same object id
// dry-run with sender = multisig → { status: "success" }
Meanwhile the dApp-side transaction is provably correct: the proposal's stored transactionBytes decode to sender = 0x7a44c9… (multisig) and dry-run successfully as-is. The requested amount is also unchanged (1 base unit in both), so only the sender differs.
Root cause: pinned by probing the estimate-fee API directly
We replayed the exact payload the desktop client sends (rawTx = the dApp transaction serialized via toJSON, with sender = multisig embedded) against POST https://wallet.onekeycn.com/wallet/v1/account/estimate-fee, varying only the two sender-ish fields:
| # |
accountAddress |
encodedTx.sender |
embedded sender in rawTx |
result |
| A (what the client sends) |
member |
member |
multisig |
the exact error above |
| B |
multisig |
multisig |
multisig |
Success |
| C |
member |
multisig |
multisig |
the exact error above |
| D |
multisig |
member |
multisig |
Success |
C vs D is conclusive: the backend takes the simulation sender from accountAddress and ignores both encodedTx.sender and the sender embedded in rawTx.
Client-side paths are all correct — we traced the 6.5.0 app.asar end to end and every step preserves an existing sender (inpage toJSON, ProviderApiSui uses the connected address only as a fallback, keyring toTransaction uses setSenderIfNotSet, the local estimateFeeByRpc prefers getData().sender). But estimateFeeByRpc only runs for custom networks; on mainnet estimateFeeByApi is used and the server-side override above kicks in, so the sign modal errors out before the user can ever sign.
Suggested fix: the backend should use the sender embedded in the transaction when present, falling back to accountAddress only for sender-less transactions (the client-side estimateFeeByRpc already implements exactly this precedence). Alternatively, the client could fall back to local RPC estimation when tx.sender ≠ accountAddress.
Note: this is unrelated to the (already fixed) 6.4.0 issue where the bundled @mysten/sui 1.28.0 could not parse FundsWithdrawal at all (Invalid type: Expected Object but received Object / Unknown value 2 for enum CallArg). 6.5.0 parses the transaction fine — this report is about the sender being replaced afterwards.
Impact
Any flow where the tx sender ≠ connected account fails at the fee-estimation step — most importantly every Sui multisig (Sagat or otherwise), on Desktop and (since it shares the same background code and API) presumably the extension as well. Because the fix is server-side, no client release is needed for the main issue.
Related: is there a plan to ship 6.5.0 to the Chrome extension? The Web Store still serves 6.4.0 (listing last updated Jun 16), whose bundled @mysten/sui 1.28.0 cannot parse FundsWithdrawal transactions at all.
Environment
sagat.mystenlabs.com), Sui mainnet@mysten/sui2.16.0, via wallet-standardsui:signTransactionSummary
When a dApp asks OneKey to sign a Sui transaction whose
senderis not the connected account (the standard multisig flow: sender = the multisig address, signer = one member), the sign modal gets stuck at fee estimation with a node error.Root cause (pinned by direct API probing, see below): on non-custom networks the client does no local dry-run; it POSTs to
/wallet/v1/account/estimate-feewithaccountAddress = connected account. The backend then simulates the transaction usingaccountAddressas the sender, ignoring the sender embedded inencodedTx.rawTx. The client-side code paths all preserve the embedded sender correctly — the swap happens server-side.For transactions using SIP-58
FundsWithdrawal(withdrawFrom: Sender), this is fatal: the withdrawal is resolved against the member's (empty) address balance instead of the multisig's, and the node rejects it:Steps to reproduce
0x2::balance::redeem_funds+send_funds, inputFundsWithdrawal { withdrawFrom: Sender }).Expected: OneKey signs the exact transaction (sender = multisig
0x7a44c9…).Actual: error above; signing is blocked.
Evidence that the sender was swapped
The accumulator object id in the error is a deterministic fingerprint of (address, coin type):
(multisig 0x7a44c99ce19af6d7…, USDC)→0x4a237736613e2e14…(holds 9000 base units)(member 0x182c1fe77c44a1f3…, USDC)→0xa0391889481986c3…← the id in the error (holds 0)So the checked transaction's sender was the member, not the multisig. Reproducible locally:
Meanwhile the dApp-side transaction is provably correct: the proposal's stored
transactionBytesdecode tosender = 0x7a44c9…(multisig) and dry-run successfully as-is. The requested amount is also unchanged (1 base unit in both), so only the sender differs.Root cause: pinned by probing the estimate-fee API directly
We replayed the exact payload the desktop client sends (
rawTx= the dApp transaction serialized viatoJSON, withsender = multisigembedded) againstPOST https://wallet.onekeycn.com/wallet/v1/account/estimate-fee, varying only the two sender-ish fields:accountAddressencodedTx.senderrawTxC vs D is conclusive: the backend takes the simulation sender from
accountAddressand ignores bothencodedTx.senderand the sender embedded inrawTx.Client-side paths are all correct — we traced the 6.5.0
app.asarend to end and every step preserves an existing sender (inpagetoJSON,ProviderApiSuiuses the connected address only as a fallback, keyringtoTransactionusessetSenderIfNotSet, the localestimateFeeByRpcprefersgetData().sender). ButestimateFeeByRpconly runs for custom networks; on mainnetestimateFeeByApiis used and the server-side override above kicks in, so the sign modal errors out before the user can ever sign.Suggested fix: the backend should use the sender embedded in the transaction when present, falling back to
accountAddressonly for sender-less transactions (the client-sideestimateFeeByRpcalready implements exactly this precedence). Alternatively, the client could fall back to local RPC estimation whentx.sender ≠ accountAddress.Note: this is unrelated to the (already fixed) 6.4.0 issue where the bundled
@mysten/sui1.28.0 could not parseFundsWithdrawalat all (Invalid type: Expected Object but received Object/Unknown value 2 for enum CallArg). 6.5.0 parses the transaction fine — this report is about the sender being replaced afterwards.Impact
Any flow where the tx sender ≠ connected account fails at the fee-estimation step — most importantly every Sui multisig (Sagat or otherwise), on Desktop and (since it shares the same background code and API) presumably the extension as well. Because the fix is server-side, no client release is needed for the main issue.
Related: is there a plan to ship 6.5.0 to the Chrome extension? The Web Store still serves 6.4.0 (listing last updated Jun 16), whose bundled
@mysten/sui1.28.0 cannot parseFundsWithdrawaltransactions at all.