Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.rhinestone.dev/llms.txt

Use this file to discover all available pages before exploring further.

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.

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],
})

Removed and relocated helpers

  • 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.