feat(sdk-core): add wrap() and unwrap() to DefiVault - #9569
feat(sdk-core): add wrap() and unwrap() to DefiVault#9569kamleshmugdiya wants to merge 1 commit into
Conversation
| // that carry no txParams — notably pendingApproval.approve() → | ||
| // recreateTxRequest() → signTxRequest() with no txParams — only ever see the | ||
| // kebab-case spelling. | ||
| 'wrapNative', |
There was a problem hiding this comment.
🟡 Could we also add the corresponding calldata check in abstractEthLikeNewCoins.verifyTssTransaction? The existing client-side defense-in-depth check covers defiApprove, defiDeposit, and defiWithdraw, but not these newly exempted value-moving intents. For wrap-native, verify the WETH9 deposit() selector and txJson.value; for unwrap-native, verify withdraw(uint256) and the decoded amount. Otherwise a malformed prebuild can pass the SDK verification layer unchecked.
| throw new Error('amount is required'); | ||
| } | ||
|
|
||
| const result = await this.wallet.sendMany({ |
There was a problem hiding this comment.
🟡 Could this path fail fast for coins that do not support a wrapped-native DeFi vault? wallet.defi is available broadly, so unsupported coins currently reach wallet-platform and return an opaque prebuild error. Please reuse/add a shared support guard (or otherwise document why server-side rejection is intentional) and add a clear client-side failure for unsupported coins.
| // and intent.intentType (kebab-case, as WP persists it). Signing paths that carry | ||
| // no txParams — pendingApproval.approve() → recreateTxRequest() → signTxRequest() | ||
| // — only ever see the kebab-case spelling, so both must be registered. | ||
| it('does not throw when the type comes from buildParams.type (camelCase)', function () { |
There was a problem hiding this comment.
🟡 The direct resolveEffectiveTxParams tests cover the set membership, but there is no test exercising a wrap/unwrap intent through the actual TSS signing/verification path with no recipients. Because this exemption deliberately bypasses a security-sensitive recipient check, could we add one end-to-end regression test through signTransactionTss/verifyTssTransaction to pin the camelCase-to-kebab-case wiring and prevent future drift?
| * tracking arrives in milestone M5. | ||
| */ | ||
| private async sendWrapIntent(type: 'wrapNative' | 'unwrapNative', params: WrapOptions): Promise<WrapResult> { | ||
| if (!params.vaultId) { |
There was a problem hiding this comment.
🟡 The public wrap/unwrap boundary currently only rejects falsy values, while the downstream codec accepts negative and hexadecimal strings (for example -1 and 0xabc) and zero. Since these values are forwarded into a value-moving WETH9 operation, could we validate an unsigned decimal amount here, decide explicitly whether zero is allowed, and reject/trim blank vaultId values before calling sendMany? Please add boundary tests for the accepted/rejected forms.
Expose ETH -> WETH wrap and WETH -> ETH unwrap on wallet.defi so the UI can call them directly. Both are thin orchestrators over a single wallet.sendMany, modelled on the existing withdrawFromVault; WP builds the WETH9 calldata and resolves the contract address from the vault binding. - iDefiVault: WrapOptions / WrapResult, plus wrap/unwrap on IDefiVault. operationId is declared optional now even though nothing populates it, so that adding operation tracking in M5 is not a breaking change to a published SDK type. - defiVault: wrap/unwrap share sendWrapIntent, which deliberately does not call extractOperationId - no operation is minted in v1. - wallet: wrapNative / unwrapNative cases map to the wrap-native / unwrap-native intents, decoding defiParams with decodeWithCodec rather than a bare cast so an 18dp amount stays a string. - mpcUtils: both sites - the recipients-required exemption list and the EVM intent-shape switch. Omitting the first makes the recipients assertion throw before the switch is reached. - recipientUtils: register both the camelCase and kebab-case spellings. NO_RECIPIENT_TX_TYPES is matched against txParams.type (camelCase, from buildParams) and against intent.intentType (kebab-case, as WP persists it); signing paths that carry no txParams only ever see the latter. @bitgo/public-types is intentionally left at 6.58.0: sdk-core imports no intent codec from it, so the 6.60.0 publish carrying WrapNativeIntent / UnwrapNativeIntent is only needed by wallet-platform. DEFI-661
ee36234 to
c6e5e3d
Compare
Implements DEFI-661 —
wallet.defi.wrap()andwallet.defi.unwrap().Both are thin orchestrators over a single
wallet.sendMany, modelled on the existingwithdrawFromVault. WP builds the WETH9 calldata and resolves the contract address from the vault binding; the SDK only forwardsvaultIdandamount.Changes
defi/iDefiVault.tsWrapOptions/WrapResult;wrap/unwraponIDefiVaultdefi/defiVault.tswrap/unwrapsharing a privatesendWrapIntentwallet/wallet.tswrapNative/unwrapNative→wrap-native/unwrap-nativeintentsutils/mpcUtils.tsutils/tss/recipientUtils.tsNO_RECIPIENT_TX_TYPES, in both spellingsexamples/ts/defi-vault-wrap.tsTwo deliberate choices worth flagging in review:
WrapResult.operationIdis declared optional even though nothing populates it. No operation is minted for wrap/unwrap in v1, andsendWrapIntentdeliberately does not callextractOperationId— it would only ever returnundefined. Declaring the field now keeps M5's operation tracking from being a breaking change to a published SDK type.vaultIdis required. Binding the wrap to a vault is what supplies the per-enterprise authorization gate and the address-whitelist path server-side (TDD §3.6). M7 relaxes it to optional, which is backward-compatible.defiParamsis decoded withdecodeWithCodec(thedefiWithdrawpattern) rather than the bareascastsdefiApprove/defiDeposituse, so an 18dp amount is validated as a numeric string and stays a string end-to-end.Both spellings in
NO_RECIPIENT_TX_TYPESNO_RECIPIENT_TX_TYPESis matched against two different sources:txParams.type— which isbuildParams.type, the camelCase value passed towallet.sendManytxRequest.intent.intentType— kebab-case, as WP persists itSigning paths that carry no
txParams—pendingApproval.approve()→recreateTxRequest()→signTxRequest()— only ever see the kebab-case spelling. Registering only one spelling leaves the other path throwingInvalidTransactionErrorbefore signing, so all four strings are registered and there is a regression test per path.Pre-existing bug found, filed separately
That asymmetry is already live for the three existing DeFi types:
defi-approve/defi-deposit/defi-withdraware registered in camelCase only, so approving a pending approval for any of them throws today. Filed as DEFI-688 rather than folded in here — the fix also requires changing an existing assertion (recipientUtils.ts:67asserts'defi-deposit'must not be in the set) that deserves its own review. The new wrap/unwrap types are not affected.public-types is intentionally not bumped
DEFI-661 asks to bump
@bitgo/public-typesto the version from DEFI-657 (6.60.0). Not done here, on purpose:GetVaultResponse/VaultProtocol/ MPC types. Intents are plain objects built bympcUtils.populateIntent, soWrapNativeIntent/UnwrapNativeIntentare needed by wallet-platform, not by the SDK.867580cf71).build:commit.wallet-platform still needs
@bitgo/public-types@6.60.0for DEFI-659.Security note
Registering
wrapNativeinNO_RECIPIENT_TX_TYPESsuppresses the SDK's missing-recipients guard, andverifyTssTransaction's client-side amount/destination comparison only runs whentxParams.type === 'transfer'(abstractEthLikeNewCoins.ts:3218), so it never fires for DeFi types. After this change WP's server-side assertion is the only control on how much ETH a wrap moves. That is the accepted design (TDD §5.2); nothing here weakens it further.Testing
modules/sdk-core— 642 passing, 1 pending (pre-existing). Adds 22 wrap/unwrap cases: sendMany shape (type+defiParamsasserted exactly),txRequestIdfrom bothfullandliteresponse shapes,operationIdstaysundefined, passphrase forwarded/omitted, missingvaultId/amountthrowing with no network call, anddefiParamscodec validation.modules/sdk-core/test/.../recipientUtils.ts— regression tests for camelCase, kebab-case, andtxParams: undefined.modules/bitgo/test/v2/unit/wallet.ts—populateIntentfor both intents, proving the recipients assertion does not throw and the intent carries a plainamountrather thanshareTokenAmount. Full file passes.tsc --noEmitclean across sdk-core (src + test); eslint and prettier clean.End-to-end only works once DEFI-659 is deployed; this is unit-tested against mocks.
Out of scope
No
autoWrap/ chaining intodepositToVault(the UI drives that, M4), no confirmation polling, no operation tracking (M5).TICKET: DEFI-661
🤖 Generated with Claude Code