Skip to main content

Migrating from 1.x SDK

To use the latest version of the SDK:
npm i @rhinestone/sdk

ESM-only build

The SDK is now ESM-only. require('@rhinestone/sdk') no longer works — use ESM import syntax. Internal subpath imports were also removed; use the curated entry points (./actions/*, ./errors, ./utils, ./smart-sessions, ./jwt-server).

sendTransaction removed

The account.sendTransaction(transaction) shortcut is gone. Use the explicit prepareTransaction → signTransaction → submitTransaction flow:
// Before
const result = await rhinestoneAccount.sendTransaction({
  targetChain,
  calls,
  tokenRequests,
})

// After
const prepared = await rhinestoneAccount.prepareTransaction({
  targetChain,
  calls,
  tokenRequests,
})
const signed = await rhinestoneAccount.signTransaction(prepared)
const result = await rhinestoneAccount.submitTransaction(signed)
sendUserOperation for ERC-4337 flows is unchanged.

Session permissions are ABI-driven

Session.actions is gone. Build sessions with toSession({ chain, owners, permissions }) instead — an ABI-driven definition the SDK resolves into a low-level Session. Each permission is an { abi, address, functions } entry; function selectors and param calldata offsets are derived from the ABI, and param value types are checked against ABI input types:
import { toSession } from '@rhinestone/sdk/smart-sessions'

// Before
const session: Session = {
  chain: base,
  owners: { type: 'ecdsa', accounts: [sessionOwner] },
  actions: [
    {
      target: usdcAddress,
      selector: toFunctionSelector(
        getAbiItem({ abi: erc20Abi, name: 'transfer' }),
      ),
      policies: [
        {
          type: 'universal-action',
          rules: [
            {
              condition: 'equal',
              calldataOffset: 0n,
              referenceValue: recipient,
            },
          ],
        },
      ],
    },
  ],
}

// After
const session = toSession({
  chain: base,
  owners: { type: 'ecdsa', accounts: [sessionOwner] },
  permissions: [
    {
      abi: erc20Abi,
      address: usdcAddress,
      functions: {
        transfer: {
          params: {
            recipient: { condition: 'equal', value: recipient },
          },
        },
      },
    },
  ],
})
The hand-written shape is now SessionDefinition; Session is the resolved output of toSession.

Session policies are declarative

In 1.x, each action carried a raw policies array that you assembled by hand. Those arrays are gone — policies are now expressed through fields on the permission’s function config, and toSession compiles them into the right on-chain policies:
  • params: { x: { anyOf: [a, b] } } — allowlist a parameter against several values (compiles to an arg-policy OR chain). A single { condition, value } stays a cheaper universal-action.
  • maxUses — cap how many times the function may be called.
  • validUntil / validAfter — restrict the function to a time window (Date).
  • valueLimit — cap cumulative ETH value (payable functions only; rejected at compile time otherwise).
  • spendingLimit: { token, amount } — cap cumulative ERC-20 spend (only on transfer / transferFrom / approve / increaseAllowance-shaped functions).
const session = toSession({
  chain: base,
  owners: { type: 'ecdsa', accounts: [sessionOwner] },
  permissions: [
    {
      abi: erc20Abi,
      address: usdcAddress,
      functions: {
        transfer: {
          params: { recipient: { anyOf: [alice, bob] } },
          spendingLimit: { token: usdcAddress, amount: 1_000_000n },
          maxUses: 10n,
        },
      },
    },
  ],
})
Passing a raw policies array on a permission now throws instead of being silently dropped. See Policies for the full set.

Session policy addresses

The smart session policies were redeployed, and toSession now bakes the new addresses into the session digest by default. If an account already enabled sessions against the previous deployments, trying to use the existing session with the new policies results in an onchain revert due to the digest mismatch. Pin the affected policies back to the addresses the account was enabled against via SessionDefinition.policyAddresses:
const session = toSession({
  chain: base,
  owners: { type: 'ecdsa', accounts: [sessionOwner] },
  permissions: [/* … */],
  policyAddresses: {
    // legacy V1 sudo policy
    sudo: '0x0000003111cD8e92337C100F22B7A9dbf8DEE301',
  },
})
The singletons that changed between V1 and V2 — pin the ones your account’s sessions were enabled against:
PolicyKeyV1 addressV2 address (default)
Sudosudo0x0000003111cD8e92337C100F22B7A9dbf8DEE3010x0000000000FEEc8D74e3143fBaBbca515358d869
CalluniversalAction0x0000006DDA6c463511C4e9B05CFc34C1247fCF1F0x0000000000714Cf48FcF88A0bFBa70d313415032
Arg policyargPolicy— (new in V2)0x0000000000167edE64D8751daACDdC0312565a73
Spending limitspendingLimits0x00000088D48cF102A8Cdb0137A9b173f957c63430x000000000033212E272655D8a22402Db819477A6
TimeframetimeFrame0x8177451511dE0577b911C254E9551D981C26dc720x0000000000D30f611fA3bf652ac6879428586930
Usage limitusageLimit0x1F34eF8311345A3A4a4566aF321b313052F514930x00000000001d4479FA2A947026204d0283ceDe4B
Value limitvalueLimit0x730DA93267E7E513e932301B47F2ac7D062abC830x000000000021dC45451291BCDfc9f0B46d6f0278

Quote selection

prepareTransaction now returns quotes: { best, all } instead of a single quote. Existing prepare → sign → submit code keeps working — signTransaction defaults to quotes.best:
const prepared = await rhinestoneAccount.prepareTransaction({
  // …
})
const signed = await rhinestoneAccount.signTransaction(prepared)
const result = await rhinestoneAccount.submitTransaction(signed)
To sign a non-default route, pass an intentId from prepared.quotes.all:
const prepared = await rhinestoneAccount.prepareTransaction({
  // …
})
const chosen =
  prepared.quotes.all.find((q) => q.settlementLayer === 'across') ??
  prepared.quotes.best
const signed = await rhinestoneAccount.signTransaction(prepared, {
  intentId: chosen.intentId,
})
getTransactionMessages(prepared, { intentId }) accepts the same selection so external signers see the route signTransaction will sign.

Settlement layer filter

settlementLayers is no longer a bare array. It’s a discriminated union with include / exclude so you can blacklist a single layer without listing all the others:
// Before
await rhinestoneAccount.prepareTransaction({
  // …
  settlementLayers: ['ACROSS', 'ECO'],
})

// After
await rhinestoneAccount.prepareTransaction({
  // …
  settlementLayers: { include: ['ACROSS', 'ECO'] },
})
The same shape applies to splitIntents. See Settlement Layers for usage patterns.

submitTransaction options bag

submitTransaction now takes an options object instead of positional arguments:
// Before
await rhinestoneAccount.submitTransaction(signed, authorizations)

// After
await rhinestoneAccount.submitTransaction(signed, { authorizations })

waitForExecution no longer accepts preconfirmations

The acceptsPreconfirmations parameter is removed. waitForExecution always waits for FILLED / COMPLETED and never treats PRECONFIRMED as terminal:
// Before
await rhinestoneAccount.waitForExecution(result, false)

// After
await rhinestoneAccount.waitForExecution(result)

verifyExecutions removed from session signers

SingleSessionSignerSet, PerChainSessionSignerSet, and ChainSessionConfig no longer accept verifyExecutions. The SDK now derives it from the session shape — sessions with permissions use emissary execution validation, claim-only sessions use the EIP-1271 path — so the flag is redundant. Drop it from your signer set:
// Before
const signers = {
  type: 'experimental_session',
  session,
  verifyExecutions: true,
  enableData,
}

// After
const signers = {
  type: 'experimental_session',
  session,
  enableData,
}

Passport account removed

account.type: 'passport' is no longer accepted. The PassportAccount type and the passport member of AccountType / AccountProviderConfig are removed.

Intent status by ID

getIntentStatus now takes a string instead of a bigint. If you persist intent IDs across runs, switch the storage type to string.

Portfolio shape

PortfolioToken no longer carries a token-level decimals or aggregate balances. decimals now lives on each per-chain chains[] entry alongside address and amount, since the same logical token can have different decimals across chains (e.g., USDC is 6 on Ethereum, 18 on BSC). Read the per-chain entry directly when rendering balances.

Permit2 claim policy renames

If you constructed Permit2ClaimPolicy values directly, the type tag and field names changed to be chain-aware:
// Before
const policy: Permit2ClaimPolicy = {
  type: 'permit2-claim',
  arbiters: [spender],
  tokensIn: [{ chainId: base.id, token: usdcAddress }],
  tokensOut: [{ chainId: base.id, token: outputToken }],
  recipients: [{ chainId: base.id, recipient: 'any' }],
  recipientIsSponsor: true,
  expiryBounds: { min: 1n, max: 100n },
  fillExpiryBounds: [{ chainId: base.id, min: 1n, max: 100n }],
}

// After
const policy: Permit2ClaimPolicy = {
  type: 'permit2',
  spenders: [spender],
  sourceTokens: [{ chain: base, address: usdcAddress }],
  destinationTokens: [{ chain: base, address: outputToken }],
  recipients: [{ chain: base, address: 'any' }],
  recipientIsAccount: true,
  permitDeadline: { min: 1n, max: 100n },
  fillDeadline: [{ chain: base, min: 1n, max: 100n }],
}

Token registry helpers removed

getSupportedTokens, getTokenAddress, getTokenDecimals, and getAllSupportedChainsAndTokens are removed. Fetch the equivalent data from the orchestrator’s /chains endpoint:
const response = await fetch('https://v1.orchestrator.rhinestone.dev/chains', {
  headers: { 'x-api-key': apiKey },
})
const chains = await response.json()

deployAccountsForOwners removed

Create a backend deployer account, take a view-only reference to each user account, and submit a sponsored intent that calls deploy(userAccount). Pass multiple deploy(...) calls in one intent to batch deployments.
import { RhinestoneSDK } from '@rhinestone/sdk'
import { deploy } from '@rhinestone/sdk/actions'
import { toViewOnlyAccount } from '@rhinestone/sdk/utils'

const rhinestone = new RhinestoneSDK({ apiKey })

const deployerAccount = await rhinestone.createAccount({
  owners: { type: 'ecdsa', accounts: [deployerSigner] },
})

const userAccount = await rhinestone.createAccount({
  owners: {
    type: 'ecdsa',
    accounts: [toViewOnlyAccount(userAddress)],
  },
})

const prepared = await deployerAccount.prepareTransaction({
  chain,
  calls: [deploy(userAccount)],
  sponsored: true,
})
const signed = await deployerAccount.signTransaction(prepared)
await deployerAccount.submitTransaction(signed)

checkERC20AllowanceDirect removed

Read allowances directly with viem’s readContract:
const allowance = await publicClient.readContract({
  address: tokenAddress,
  abi: erc20Abi,
  functionName: 'allowance',
  args: [owner, spender],
})

ENS validator owner set

ENSValidatorConfig couples each owner with its expiry instead of using parallel arrays. Omit expiration for an owner that never expires:
// Before — parallel arrays, expirations in unix seconds
const validator = {
  type: 'ens',
  accounts: [alice, bob],
  ownerExpirations: [1893456000, 1893456000],
}

// After — each owner carries its own expiry (omit `expiration` to never expire)
const validator = {
  type: 'ens',
  owners: [
    { account: alice, expiration: new Date('2030-01-01') },
    { account: bob, expiration: new Date('2030-01-01') },
  ],
}

Removed and relocated helpers

  • createRhinestoneAccount is removed. Use new RhinestoneSDK({ apiKey }).createAccount(config).
  • Account recovery. The @rhinestone/sdk/actions/recovery subpackage (enable, recoverEcdsaOwnership, recoverPasskeyOwnership), the recovery field on the account config, and the guardian signer set are removed.
  • account.deploy() session param. The unused session option on account.deploy() is removed (it was a no-op).
  • Compact-bound surface. The @rhinestone/sdk/actions/compact subpackage, the lockFunds transaction option, and Account.emissaryConfig are removed alongside the orchestrator’s compact-based deposit/withdrawal flow.
  • Permit2 signing helpers. signPermit2Batch, signPermit2Sequential, and the related MultiChainPermit2Config / MultiChainPermit2Result / BatchPermit2Result types are removed. Signing now uses orchestrator-provided EIP-712 typed data internally.
  • getPermit2Address is removed. Permit2 lives at 0x000000000022D473030F116dDEE9F6B43aC78BA3 on every supported chain — hardcode the constant.
  • walletClientToAccount and wrapParaAccount moved from the package root to @rhinestone/sdk/utils.

Migrating from 1.x Alpha SDK

New entry point

RhinestoneSDK is now the main entry point to the SDK functionality. To migrate, change the account creation code:
const rhinestoneAccount = await createRhinestoneAccount({
  // Optional
  account: {
    'type': 'nexus'
  },
  owners: {
    type: "ecdsa" as const,
    accounts: [owner],
  },
  // …
  // Optional
  rhinestoneApiKey,
  // Optional
  orchestratorUrl,
});
to this:
const rhinestone = new RhinestoneSDK({
  apiKey: rhinestoneApiKey,
  // Optional
  endpointUrl: orchestratorUrl,
})
const rhinestoneAccount = await rhinestone.createAccount({
  // Optional
  account: {
    'type': 'nexus'
  },
  owners: {
    type: "ecdsa" as const,
    accounts: [owner],
  },
  // …
});

Transaction utilities (actions)

Action utilities related to using modules and resource locking (e.g., installModule, addOwner, recoverEcdsaOwnership) were moved to separate subpackages:
// Before
import { addPasskeyOwner } from '@rhinestone/sdk'
// After
import { addOwner as addPasskeyOwner } from '@rhinestone/sdk/actions/passkeys'
Additionally, you don’t need to pass rhinestoneAccount, address, chain, and provider params anymore when using actions:
// Before
await rhinestoneAccount.sendTransaction({
  calls: [
    ...installModule({
      rhinestoneAccount,
      module,
    })
  ],
  // …
})

// After
await rhinestoneAccount.sendTransaction({
  calls: [
    // Note that you don't need to spread the actions anymore
    installModule(module)
  ],
  // …
})
  • /actions:
    • installModule to install a module
    • uninstallModule to uninstall a module
  • /actions/compact (resource locking with TheCompact):
    • depositEther to deposit ETH into TheCompact
    • enableEtherWithdrawal to enable permissionless ETH withdrawal (starts reset period)
    • disableEtherWithdrawal to cancel permissionless ETH withdrawal
    • withdrawEther to withdraw ETH after the reset period
    • approveErc20 to approve an ERC-20 token for deposit
    • depositErc20 to deposit ERC-20 into TheCompact
    • enableErc20Withdrawal to enable permissionless ERC-20 withdrawal (starts reset period)
    • disableErc20Withdrawal to cancel permissionless ERC-20 withdrawal
    • withdrawErc20 to withdraw ERC-20 after the reset period
  • /actions/ecdsa (ECDSA validator):
    • enable to enable the validator
    • disable to disable the validator
    • addOwner to add an owner
    • removeOwner to remove an owner
    • changeThreshold to change the signature threshold
  • /actions/mfa (multi-factor authorization):
    • enable to enable the validator
    • disable to disable the validator
    • setSubValidator to add a sub-validator to the MFA set
    • removeSubValidator to remove a sub-validator from the MFA set
    • changeThreshold to change the MFA signature threshold
  • /actions/passkeys (passkey validator):
    • enable to enable the validator
    • disable to disable the validator
    • addOwner to add an owner
    • removeOwner to remove an owner
    • changeThreshold to change the signature threshold
  • /actions/recovery (social recovery):
    • enable to enable the validator
    • recoverEcdsaOwnership to recover ownership to a new ECDSA owner
    • recoverPasskeyOwnership to recover ownership to a new passkey owner

Errors

Error classes were moved to a separate subpackage:
// Before
import { isAccountError, AccountError, SigningNotSupportedForAccountError } from '@rhinestone/sdk'
// After
import { isAccountError, AccountError, SigningNotSupportedForAccountError } from '@rhinestone/sdk/errors'

Using ERC-4337 flow

All transactions executed with sendTransaction and prepareTransaction now use Rhinestone intents. Using the ERC-4337 user operations (for example, when using a social recovery) now requires a separate flow. This change lets us improve type-safety and DX around using intents. To keep using user operations for specific flows, change your code from:
const result = await rhinestoneAccount.sendTransaction(
  // …
)
and
const data = await rhinestoneAccount.prepareTransaction({
  // …
})
const signedData = await rhinestoneAccount.signTransaction(data)
const result = await rhinestoneAccount.submitTransaction(signedData)
to:
const result = await rhinestoneAccount.sendUserOperation(
  // …
)
and:
const data = await rhinestoneAccount.prepareUserOperation({
  // …
})
const signedData = await rhinestoneAccount.signUserOperation(data)
const result = await rhinestoneAccount.submitUserOperation(signedData)

Migrating from 0.x SDK

To use the latest version of the SDK, install it with the alpha tag:
npm i @rhinestone/sdk@alpha
Note that the deployerAccount parameter has been removed, as all deployments are now handled via the Orchestrator. Also, sourceChains now accepts a list of chains instead of a single chain.
Due to the module address changes, you’d need to redeploy and refund the accounts.

Migrating from Orchestrator SDK

This guide provides a detailed breakdown of the changes between the Orchestrator SDK and the new SDK. If you’re looking for a fresh start, see our Quickstart.

Installation

Previously:
npm i @rhinestone/module-sdk @rhinestone/orchestrator-sdk permissionless viem
Now:
npm i @rhinestone/sdk viem

Account Creation

Choosing an account implementation

Before, you’d need to construct a smart account client with permissionless:
const sourceSafeAccount = await toSafeSmartAccount({
  version: "1.4.1",
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
  // …
});
 
const smartAccountClient = createSmartAccountClient({
  account: sourceSafeAccount,
  chain: sourceChain,
  // …
}).extend(erc7579Actions());
Now:
const account = await createRhinestoneAccount({
  account: {
    type: 'safe'
  }
})
The account object is multi-chain; you don’t need to create separate instances for each chain.
See Smart Account Providers for details on choosing the account implementation.

Choosing a validator

Before, you’d specify the validator config in your smart account setup:
const owner = privateKeyToAccount(generatePrivateKey());

const ownableValidator = getOwnableValidator({
  owners: [owner.address],
  threshold: 1,
});

const sourceSafeAccount = await toSafeSmartAccount({
  validators: [
    {
      address: ownableValidator.address,
      context: ownableValidator.initData,
    },
  ],
  // …
});

const sourceSmartAccountClient = createSmartAccountClient({
  account: sourceSafeAccount,
  chain: sourceChain,
  bundlerTransport: http(
    `https://api.pimlico.io/v2/${sourceChain.id}/rpc?apikey=${pimlicoApiKey}`,
  ),
  paymaster: sourcePimlicoClient,
  userOperation: {
    estimateFeesPerGas: async () => {
      return (await sourcePimlicoClient.getUserOperationGasPrice()).fast;
    },
  },
}).extend(erc7579Actions());
Now, you can use owners when creating the account:
const owner = privateKeyToAccount(generatePrivateKey());

const account = await createRhinestoneAccount({
  owners: {
    type: 'ecdsa',
    accounts: [owner],
  },
})
Learn more about using the ECDSA and passkey validators as the account owner. If you are using Smart Sessions, see the relevant guide.

Setting up Omni Account modules

Before, you’d need to provide the module configurations for the Omni Account manually:
const sourceSafeAccount = await toSafeSmartAccount({
  // …
  executors: [
    {
      address: getSameChainModuleAddress(targetChain.id),
      context: "0x",
    },
    {
      address: getTargetModuleAddress(targetChain.id),
      context: "0x",
    },
    {
      address: getHookAddress(targetChain.id),
      context: "0x",
    },
  ],
  hooks: [
    {
      address: getHookAddress(targetChain.id),
      context: encodeAbiParameters(
        [
          { name: "hookType", type: "uint256" },
          { name: "hookId", type: "bytes4" },
          { name: "data", type: "bytes" },
        ],
        [
          0n,
          "0x00000000",
          encodeAbiParameters([{ name: "value", type: "bool" }], [true]),
        ],
      ),
    },
  ],
  fallbacks: [
    {
      address: getTargetModuleAddress(targetChain.id),
      context: encodeAbiParameters(
        [
          { name: "selector", type: "bytes4" },
          { name: "flags", type: "bytes1" },
          { name: "data", type: "bytes" },
        ],
        ["0x3a5be8cb", "0x00", "0x"],
      ),
    },
  ],
});
Now, that is handled automatically for you.
Under the hood, the SDK installs a single executor module that handles chain abstraction operations.

Initializing the Orchestrator Client

Before, you’d initialize an Orchestrator API client:
const orchestrator = getOrchestrator(orchestratorApiKey);
Now, you need to pass the API key directly to the account instance:
const account = privateKeyToAccount(generatePrivateKey());

const account = await createRhinestoneAccount({
  rhinestoneApiKey: orchestratorApiKey,
  // …
})

Funding

As before, you can send the tokens or ETH directly to the account to fund it.

Deploying

Before, you’d deploy the smart account using an ERC-4337 bundler:
const opHash = await sourceSmartAccountClient.sendTransaction({
  to: zeroAddress,
  data: "0x11111111",
});

await sourcePublicClient.waitForTransactionReceipt({
  hash: opHash,
});
Now, you can use the deploy method:
await rhinestoneAccount.deploy(chain)

Fetching the Order Path

Before, you define the intent and use getOrderPath to get the path.
const usdcAddress = getTokenAddress("USDC", targetChain.id);
const usdcAmount = 2n;
const recipient = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';

const metaIntent: MetaIntent = {
  targetChainId: targetChain.id,
  tokenRequests: [
    {
      tokenAddress: usdcAddress,
      amount: usdcAmount,
    },
  ],
  targetAccount: targetSafeAccount.address,
  targetExecutions: [
    {
      to: usdcAddress,
      value: 0n,
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: "transfer",
        args: [recipient, usdcAmount],
      }),
    },
  ],
};

const orderPath = await orchestrator.getOrderPath(
  metaIntent,
  targetAccount.address,
);
Now, you can call prepareTransaction:
const usdcAmount = 2n;
const recipient = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';

const transactionData = await rhinestoneAccount.prepareTransaction({
  targetChain,
  calls: [
    {
      to: 'USDC',
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: 'transfer',
        args: [recipient, usdcAmount],
      }),
    },
  ],
  tokenRequests: [
    {
      address: 'USDC',
      amount: usdcAmount,
    },
  ],
})

Signing the Intent

Before, you’d craft the packed signature and pass that to the bundle structure:
const orderBundleHash = getOrderBundleHash(orderPath[0].orderBundle);
 
const bundleSignature = await owner.signMessage({
  message: { raw: orderBundleHash },
});
const packedSig = encodePacked(
  ["address", "bytes"],
  [ownableValidator.address, bundleSignature],
);
 
const signedOrderBundle: SignedMultiChainCompact = {
  ...orderPath[0].orderBundle,
  originSignatures: Array(orderPath[0].orderBundle.segments.length).fill(
    packedSig,
  ),
  targetSignature: packedSig,
};
Now, you can use the signTransaction method:
const signedTransactionData =
  await rhinestoneAccount.signTransaction(transactionData)

Sending the Intent

Before, you’d use the postSignedOrderBundle to submit the intent to the orchestrator:
const bundleResults: PostOrderBundleResult = await orchestrator.postSignedOrderBundle([
  {
    signedOrderBundle,
  },
]);
Now, you can use the submitTransaction method:
const result = await rhinestoneAccount.submitTransaction(signedTransactionData)

Getting the Intent Status

Before, you’d poll the getBundleStatus method to get bundle status updates:
const bundleStatus = await orchestrator.getBundleStatus(
  bundleResults[0].bundleId,
);
Now, you can use the waitForExecution method:
const status = await rhinestoneAccount.waitForExecution(result)

Using with Existing Accounts

For now, using the SDK with existing accounts is not possible. Users would need to create a new smart account. We’re working on making it possible to use the SDK with existing (deployed) smart accounts. Reach out if you need this.