feat(frontend): send imported EVM balances to the user's own wallet - #13650
Draft
sbpublic wants to merge 5 commits into
Draft
feat(frontend): send imported EVM balances to the user's own wallet#13650sbpublic wants to merge 5 commits into
sbpublic wants to merge 5 commits into
Conversation
The signature comes from the source wallet's helper canister — those keys are threshold keys it alone can sign for — and OISY broadcasts the result through its own provider, so we see the transaction hash and any RPC error directly. Gas is paid in the network's native coin out of the *imported* account, which shapes everything here. A native send can only move balance minus gas; sending the full balance always fails. A token send needs native coin sitting in that same account, a common gap for someone who only ever received tokens. Both are enforced here against live fee data rather than trusted from the caller, because the fee moves between the moment a row renders and the moment the user confirms. An absent fee ceiling stops the send rather than defaulting: signing a transaction whose gas price is unknown risks spending the whole balance on gas. Fee data comes from getEthFeeDataWithProvider, which already applies the per-chain floors, and the nonce from OISY's own provider rather than the canister's transaction_count — keeping a third party off the path for everything but the signature. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Routes EVM rows to the new send path and reports why a blocked one cannot move. An unsupported chain, a missing gas balance and a balance below its own fee are three different problems with three different remedies, so each says so rather than sharing a greyed-out button. Fixes two things the EVM case exposed in the existing rows: A row's identity is network plus symbol. Symbol alone is not unique and neither is the address — the same token enabled on several EVM networks yields rows sharing both — so keying the in-flight send on symbol would have spun two rows at once. The confirmation said the fee is deducted "from this token", which is true for ICRC and native EVM but wrong for an ERC20, where gas is paid in the network's native coin. Token transfers now name that coin instead. The EVM destination is the user's own OISY EVM address, not their principal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Marks the approach decision resolved — sign at the helper canister, broadcast from OISY — and records why the reverse is impossible: the management canister's public-key methods take a canister_id while the signing methods derive from the caller, so these addresses are readable by anyone and signable only by that canister. Also notes the gas arithmetic, that a row's identity is network plus symbol, and that BTC and SOL remain send-from-source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported from a real wallet: three vault tokens on Base showed identical- looking balances that were actually the account's ETH balance, formatted with each vault's own decimals (bAutopilot_USDC at 8 decimals and bAutopilot_wETH at 20 both render the same 0.0002 ETH as 2000000 and 0.000002). Cause: the balance dispatch had a branch for erc20 and then a catch-all native-EVM branch, but none for erc4626, so every vault row fell through to native and reported ETH. ERC-4626 is a superset of ERC-20 — the vault share implements balanceOf and transfer on its own contract — so the fix is to treat the two alike everywhere via a single isPlugEvmContractToken predicate: read via balanceOf, send via send_erc20, gate on native gas, and never count a vault as the native coin. getErc20FeeData is widened to accept either shape; it only reads address/symbol/name, which both provide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rontend/ic-seed-phrase-import-evm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
PR2 moves the ICP and ICRC balances of an imported wallet. This does the same for Ethereum and the other EVM networks — the first chain where OISY cannot sign for itself.
Those addresses are threshold keys rooted in the source wallet's own helper canister. On the management canister,
ecdsa_public_keytakes acanister_idargument butsign_with_ecdsadoes not — it derives from the caller. So the addresses are readable by anyone (which is how PR1 derives them offline) and signable only by that canister. We ask it for a signature and broadcast the result ourselves.Stacked on #13649; review that first. This PR targets its branch.
Note
Draft, and this is the first PR in the stack to request a real signature from that canister.
eth_addressconfirmed it accepts outside callers, but no signing method has been exercised. Worth one small real send before this merges — that single test retires the main risk for the BTC and SOL PRs too.Changes
PRODUCT.mdand the spec; the PR3-approach decision is now marked resolved.isPlugEvmContractTokenpredicate — read viabalanceOf, send viasend_erc20, gate on native gas.getErc20FeeDatais widened to accept either shape (it only reads address/symbol/name).Gas shapes the whole thing. It is paid in the network's native coin out of the imported account:
balance - gas. Sending the full balance always fails.Reuse over rebuild: fee data comes from
getEthFeeDataWithProvider, which already applies the per-chain floors (BSC minimums) — readinggetFeeDataraw would have silently dropped those. The nonce comes from OISY's own provider rather than the canister'stransaction_count, keeping a third party off the path for everything but the signature.Two bugs the EVM case exposed in the rows PR2 shipped, fixed here:
Also: the EVM destination is the user's own OISY EVM address, not their principal — unlike the IC sweep, where the principal is the destination.
Tests
plug-evm.services.spec.ts(7 cases): gas is reserved out of a native amount; the signed transaction is broadcast and its hash returned; a balance that cannot cover its own gas is refused without signing; an absent fee ceiling refuses without signing; a token sends its full balance; a token with no native coin is refused without signing; and a token never takes the native signing method.PlugImportAccount.spec.ts(14 cases, up from 6): native EVM and funded-ERC20 rows offer an action; an ERC20 with no gas is blocked and names the coin; plus the cross-network symbol-collision regression.The "without signing" assertions are the ones worth having — a refusal that still asked the canister to sign would burn its cycles and could leave a signed transaction in flight.
Local gates, all green:
npm run formatnpm run lint -- --max-warnings 0(whole repo)npm run checknpm run check:testsvitestNot run locally: the full suite (
MenuThemeSelectorfails 8 cases on unmodifiedmain— Node'slocalStorageclobbers jsdom) andtest-coverage.compare-sizeswill fail as it does across this stack; the increase is known and accepted.🤖 Generated with Claude Code