What changed
Account, Deposit, Withdraw, Supply, Pay and Lock now acquire balances and evaluate asset eligibility through shared SDK inventory and policy. Supply, Pay, Lock and custom action presentations useuseSpiceSupply for planning and execution. useSpiceLock and the existing useSpiceAction are thin
adapters. Deposit and rollup Withdraw retain their distinct execution semantics; Account wallet
withdrawal uses the shared fee-only action path.
The benefit is one implementation of shared behavior across stock and custom views. A correction to
fees or inventory no longer needs a separate client implementation. The tradeoff is a larger set of
consumers to validate when shared behavior changes, plus migration work for integrations that used
to execute destination transactions themselves. This refactor is not a promise of a smaller bundle
or faster execution on every route.
Integration checklist
- Record the current app pin and resolved SDK/core versions. Compare them with the intended published package, its exports and declarations. A higher version on another prerelease branch does not prove it contains this refactor.
- Keep the existing wallet provider stack and import
styles.cssonce. Configure SDKrpcOverridesfor every source, conversion and destination chain the app can exercise. wagmi transports alone do not configure the SDK. See Configuration. - Keep the host UI. Use the stock component when it fits, or the controlled action hook. Replace host-owned balance timers, fee estimation, funding orchestration and execution state with the shared SDK owners.
- Audit the callbacks and contract ownership described below. Preserve approved asset restrictions, beneficiary semantics and application behavior; do not remove a restriction merely to pass a test.
- Validate the exact candidate in each affected app, then update its exact pin and lockfile through the app’s release process. Document pending migrations instead of claiming universal compatibility.
Callback and ownership migration
The three execution overrides above now produce integration errors. Keeping their export/type names
does not make them functional compatibility paths. This change does not remove every
onDepositExecute or onWithdrawExecute contract; inspect those distinct flows before changing them.
External mode selects the external source wallet, funds the embedded wallet, and then uses the same
downstream action executor as embedded mode. The source wallet is not necessarily the destination
contract caller. Use BuildActionCallsContext.executorAddress and beneficiary deliberately.
For example, a contract offering stakeFor(beneficiary, amount) can preserve a user’s position owner;
a contract assigning ownership only to msg.sender needs a separate integration decision. Verify
NFT receiver callbacks as well as token balances. Do not delete an override and silently change who
owns the resulting position.
Same-chain destination assets do not automatically bypass the SDK
The behavior below describes published consistency candidate
.16. The unpublished
SDK PR #104 restores automatic direct execution inside the shared
controller for External mode and a matching wallet token/chain. It uses the same host action
definition, needs no embedded signer, and charges no Spicenet fee. Network gas is paid in the
external wallet, with a gas reserve for native Max. Calls are sequential, with receipt tracking
and recovery of unfinished calls; success callbacks are still awaited. Other tokens/chains,
Embedded mode, fee-only/proceeds-funded actions and zero-amount payments retain their existing
behavior. Existing funded actions are reconciled on their original path. Do not assume .16
includes this restoration; adopt and verify an exact new candidate after publication.executeDirectExternalAction. Lock had the equivalent executeDirectExternalLock branch.
The host supplied protocol calls; the SDK selected the direct path and submitted those calls
with the external signer, without first funding the embedded wallet. See the
pre-refactor Supply executor
and Lock executor.
Implementing a separate native action in the host is therefore a workaround for a removed SDK
capability, not an unchanged client responsibility. If a host already has such an action,
audit its routing before claiming it was preserved. The External mode toggle does not provide
that bypass in .16. The unpublished restoration above keeps this choice in the shared SDK
controller, so clients continue to supply one action definition.
Amounts, fees and Max
Preserve raw integer amounts and the decimals of the token each amount belongs to. Use the
controller’s accepted preview and
canExecute; do not recompute readiness from a nonempty input.
Use resolveMaxAmount(asset) for Max. A null result means Max is unavailable, not permission to
use the full balance.
An accepted plan binds the source identity/cap, beneficiary, destination minimum, route/calls, fee
bounds, expiry and provider/policy configuration. A stale or changed plan must be reviewed again.
Some changing-fee failures are intentional: execution must fit the accepted bounds. The refactor
does not guarantee zero quote retries.
Deposit gas and rollup withdrawal rules remain distinct from these action fees. Account wallet
withdrawal uses a fee-only Supply action; do not apply a blanket “withdrawals never need a quote”
rule to that path.
Inventory and refresh
UseuseSpiceInventory for custom holdings and the action controller’s asset list for action
selection. assetPolicy is shared internal logic, not a new provider prop. Clients configure
allowedTokens, chain scope, RPCs and venues.
- Keep owner, wallet/rollup location, chain and token/receipt identity. Use
assetPositionId(asset)for row keys and selection matching; equal symbols do not mean equal positions. - Complete raw inventory retains dust and unpriced positions. Default visibility is separate: discoveries outside configured tokens need a finite positive price and at least $0.01. Account also hides configured priced dust; configured tokens retain the missing-price exception.
- Preserve displayed data during background refresh. Confirmed zero replaces the old balance;
stale/failed balance reads cannot authorize spending. A price-only failure is different from a
balance failure. Do not turn
refreshinginto an empty list or an initial loading screen. - Extra assets can supply metadata and otherwise-undiscovered positions. They cannot overwrite an SDK-acquired balance, including zero. Apply the same client policy across views.
Completion and recovery
Render the controller’s status until required credit/delivery and destination receipts complete. Await application updates in the success callback and make them safe to retry. A failed application update can be retried withretryFinalization() without resubmitting the transaction.
Use the existing pending-action journal for uncertain results. Preserve the original action inputs,
beneficiary and configuration when resuming. Keep lock duration and other host-owned terms available
after reload; automatic restoration of those host inputs is not guaranteed. If
fundingQuoteReview is present, show both destination minima and obtain explicit review before
calling its accept() action. That review cannot widen the source cap or authorize another transfer.
Updating the SDK from now on
- Start from fresh SDK main and change the shared owner. Audit affected stock and controlled consumers instead of applying equivalent patches in each widget or app.
- Run focused regressions and the SDK’s required
bun test,bun run typecheck, andbun run build. Validate affected app builds and rendered refresh/error behavior. Exercise authorized financial cases across relevant wallet modes, routes and recovery states. - Record the exact tested source, package artifact, app/configuration and coverage. A prior candidate’s financial results do not automatically apply to a later candidate or merged main.
- If core changes are required, release core before updating the UI dependency. An inventory or controller-only fix does not inherently require a core release. Coordinate API changes before their consumers rely on them.
- Publish an approved prerelease with an explicit non-
latesttag, verify its registry artifact, and pin that exact version in the test app. Verify the deployed bundle as well as the lockfile. - Treat stable promotion, individual client adoption and docs targeting as separate release steps. Choose stable versioning based on behavioral breaks as well as type changes; the candidate’s patch-like numbering is not evidence of a drop-in patch upgrade. Match the docs’ SDK drift check to the documented package when promoting a version.
Evidence and limits
The.16 candidate passed 978 SDK tests, typecheck/build, rendered filtering tests and authenticated
Account refresh checks. The preceding .15 candidate completed 28 financial cases and an additional
outage/reload withdrawal test in a test integration. That full financial matrix was not rerun on
.16. Frozen builds of five consumers passed on .15, while existing full-typecheck debt remained
in two. These results support the tested cases; they do not establish settlement or caller
compatibility for every client. See the SDK repository’s CONSISTENCY-MIGRATION.md for the release
evidence and open limits.