> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sqd.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate Solana SDK squids to Portal

> Replace gateway and RPC data sources with the public Portal for real-time Solana data

<Card title="← Back to Portal Setup" icon="arrow-left" href="/en/portal/migration">
  Return to the Portal setup overview to explore other deployment options.
</Card>

The newest version of the SQD Network portal serves real-time data. It can replace both gateways of the open private version of SQD Network and RPC endpoints.

## Prerequisites

* An existing Squid SDK-based Solana squid using `@subsquid/solana-stream` with `.setGateway()` and/or `.setRpc()`
* Node.js and npm installed
* Access to an SQD Portal

## Migration steps

<Steps>
  <Step title="Upgrade SDK packages">
    From your squid's folder, upgrade every `@subsquid/*` package to its latest release:

    ```bash theme={"system"}
    npx --yes npm-check-updates --filter "@subsquid/*" --target "@latest" --upgrade
    ```

    This should bump `@subsquid/solana-stream` to a `1.x.x` version.

    Then install:

    <Tabs>
      <Tab title="NPM">
        ```bash theme={"system"}
        npm install
        ```
      </Tab>

      <Tab title="Yarn">
        ```bash theme={"system"}
        yarn install
        ```
      </Tab>

      <Tab title="PNPM">
        ```bash theme={"system"}
        pnpm install
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Update your code">
    **A.** Replace all existing data sources with the portal:

    ```diff theme={"system"}
    +  .setPortal({
    +    url: 'https://portal.sqd.dev/datasets/solana-mainnet',
    +      http: {
    +        retryAttempts: Infinity
    +      }
    +   })
    -  .setGateway('https://v2.archive.subsquid.io/network/solana-mainnet')
    -  .setRpc({
    -    client: new SolanaRpcClient({
    -      url: process.env.SOLANA_NODE
    -    })
    -  })
    ```

    Also, please remove any mentions of `SolanaRpcClient`. For example:

    ```diff theme={"system"}
    -import {DataSourceBuilder, SolanaRpcClient} from '@subsquid/solana-stream'
    +import {DataSourceBuilder} from '@subsquid/solana-stream'
    ```

    **B.** Replace any block height literals with **slot number** literals.

    ```diff theme={"system"}
    +  .setBlockRange({from: 325000000})
    -  .setBlockRange({from: 303262650})
    ```

    Use this converter to translate block heights into slot numbers. It bisects the chain through the public Portal:

    {(() => {
        const { useState } = React;
        const [height, setHeight] = useState('');
        const [status, setStatus] = useState('');
        const [result, setResult] = useState(null);
        const [logs, setLogs] = useState([]);
        const [busy, setBusy] = useState(false);

        // Flip to true to render the per-step bisection trace below the result.
        const SHOW_TRACE = false;

        const STREAM_URL = 'https://portal.sqd.dev/datasets/solana-mainnet/finalized-stream';
        const HEAD_URL = 'https://portal.sqd.dev/datasets/solana-mainnet/finalized-head';

        async function probeSlot(slot) {
          const res = await fetch(STREAM_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              type: 'solana',
              fields: { block: { number: true, height: true } },
              fromBlock: slot,
              toBlock: slot,
            }),
          });
          if (res.status === 204) return null;
          if (!res.ok) throw new Error(`Portal returned HTTP ${res.status}`);
          const text = await res.text();
          const line = text.split('\n').find((l) => l.trim());
          if (!line) return null;
          const obj = JSON.parse(line);
          return { slot: obj.header.number, height: obj.header.height };
        }

        async function findBlockNear(mid, lo, hi) {
          let up = mid;
          let down = mid - 1;
          let probed = 0;
          while (up <= hi || down >= lo) {
            if (up <= hi) {
              probed++;
              const b = await probeSlot(up);
              if (b) return { block: b, probed };
              up++;
            }
            if (down >= lo) {
              probed++;
              const b = await probeSlot(down);
              if (b) return { block: b, probed };
              down--;
            }
          }
          return { block: null, probed };
        }

        async function convert() {
          const target = Number.parseInt(height, 10);
          if (!Number.isFinite(target) || target < 0) {
            setStatus('Enter a non-negative integer block height.');
            return;
          }
          setBusy(true);
          setResult(null);
          setLogs([]);
          setStatus('Fetching the finalized head from Portal…');
          try {
            const headRes = await fetch(HEAD_URL);
            if (!headRes.ok) throw new Error(`Portal returned HTTP ${headRes.status}`);
            const head = await headRes.json();
            let lo = 0;
            let hi = head.number;
            let best = null;
            const trace = [];
            let iter = 0;
            let totalProbes = 0;
            while (lo <= hi && iter < 80) {
              iter++;
              const mid = Math.floor((lo + hi) / 2);
              const { block, probed } = await findBlockNear(mid, lo, hi);
              totalProbes += probed;
              let line = `#${iter}: range [${lo}, ${hi}], mid ${mid}, probed ${probed} slot${probed === 1 ? '' : 's'}`;
              if (!block) {
                line += ' → no block in range';
                break;
              } else if (block.height < target) {
                line += ` → slot ${block.slot} height ${block.height} (too low)`;
                lo = block.slot + 1;
              } else {
                line += ` → slot ${block.slot} height ${block.height} (≥ target)`;
                best = block.slot;
                hi = block.slot - 1;
              }
              trace.push(line);
              setLogs([...trace]);
              setStatus(`Bisecting… ${iter} step${iter === 1 ? '' : 's'}, ${totalProbes} probes`);
            }
            if (best === null) {
              setStatus(`No slot found with height ${target}. The chain may not have reached this height yet.`);
              return;
            }
            const verify = await probeSlot(best);
            totalProbes++;
            if (verify && verify.height === target) {
              setResult(best);
              setStatus(`Done in ${totalProbes} probes — block height ${target} is at slot ${best}.`);
            } else {
              const closestHeight = verify ? verify.height : null;
              setResult(best);
              setStatus(
                closestHeight !== null
                  ? `Exact height ${target} not found. Closest slot with height ≥ target is ${best} (height ${closestHeight}). Total probes: ${totalProbes}.`
                  : `Exact height ${target} not found. Closest candidate slot is ${best}. Total probes: ${totalProbes}.`
              );
            }
          } catch (e) {
            setStatus(`Error: ${e.message}`);
          } finally {
            setBusy(false);
          }
        }

        return (
          <div style={{ border: '1px solid #d0d7de', borderRadius: '6px', padding: '1rem', margin: '1rem 0' }}>
            <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
              <label htmlFor="height-input" style={{ fontWeight: 600 }}>Block height:</label>
              <input
                id="height-input"
                type="number"
                min="0"
                value={height}
                onChange={(e) => setHeight(e.target.value)}
                placeholder="e.g. 303262650"
                disabled={busy}
                style={{ flex: '1 1 200px', padding: '0.4rem 0.6rem', borderRadius: '4px', border: '1px solid #d0d7de' }}
              />
              <button
                type="button"
                onClick={convert}
                disabled={busy}
                style={{
                  padding: '0.4rem 0.9rem',
                  borderRadius: '4px',
                  border: '1px solid #d0d7de',
                  background: busy ? '#f3f4f6' : '#0969da',
                  color: busy ? '#666' : '#fff',
                  cursor: busy ? 'wait' : 'pointer',
                  fontWeight: 600,
                }}
              >
                {busy ? 'Searching…' : 'Convert to slot'}
              </button>
            </div>
            {status && <div style={{ marginTop: '0.6rem', fontSize: '0.9rem' }}>{status}</div>}
            {result !== null && (
              <div style={{ marginTop: '0.4rem', fontSize: '1.05rem' }}>
                Slot: <code>{result}</code>
              </div>
            )}
            {SHOW_TRACE && logs.length > 0 && (
              <pre style={{ fontSize: '0.75rem', overflowX: 'auto', marginTop: '0.4rem' }}>{logs.join('\n')}</pre>
            )}
          </div>
        );
        })()}

    **C.** If you used the `slot` field of block headers anywhere in your code, replace it with `.number`:

    ```diff theme={"system"}
    -  slot: block.header.slot,
    +  slot: block.header.number,
    ```

    **D.** Expand your `.setFields()` call to enumerate every field your batch handler actually reads.

    <Warning>
      **Every field your handler reads must be listed in `.setFields()`.** Earlier `@subsquid/solana-stream` releases merged a built-in default set (`block.timestamp`, `transaction.signatures`/`err`, `instruction.programId`/`accounts`/`data`/`isCommitted`, `log.programId`/`kind`/`message`, `balance.pre`/`post`, the `tokenBalance.pre*`/`post*` family, `reward.lamports`/`rewardType`) on top of whatever you passed, so partial selections worked even when the handler accessed unlisted fields. The current release fetches only the fields you list from the portal and, at the type level, `BlockHeader<F>` / `Transaction<F>` / `Instruction<F>` / etc. expose only those fields. TypeScript will reject `transaction.signatures` or `block.header.timestamp` accesses if you forgot to request them.
    </Warning>

    For example, if you want to keep using the block height (to stay compatible with your old code), request it explicitly — `block.header.number` (the slot) is always available, but `height` is not:

    ```diff theme={"system"}
       .setFields({
         block: { // block header fields
           timestamp: true,
    +      height: true
         },
    ```

    Add `transaction`/`instruction`/`log`/`balance`/`tokenBalance`/`reward` entries the same way. TypeScript errors at compile time will point you at any field still missing from the selection.
  </Step>

  <Step title="Test by re-syncing">
    We highly recommend that all squids migrated to Portal are tested by re-syncing them. This will allow you to make sure that everything works as expected for the whole length of the chain and catch any bugs early.

    To resync your squid, follow the zero-downtime update procedure:

    1. Deploy your squid into a new slot.
    2. Wait for it to sync, observing the improved data fetching.
    3. Assign your production tag to the new deployment to redirect the GraphQL requests there.

    See [the slots and tags guide](/en/cloud/resources/slots-and-tags#zero-downtime-updates) for details.

    <Accordion title="A workaround that allows continuous operation without a resync (not recommended)">
      Assuming your squid is version-controlled and has one processor:

      1. Commit your updated squid code.

      2. Stop your Cloud squid deployment.

         1. Reset your repo to the state before the updates.
         2. Add the `to` field to the argument of the [`.setBlockRange`](/en/sdk/squid-sdk/solana-indexing/sdk/solana-batch/general#set-block-range) `DataSourceBuilder` call (likely in `./src/main.ts`). Set it to the current slot number.
         3. [Update your deployment](/en/sdk/squid-sdk/squid-cli/deploy).
         4. Take a look at your squid's logs. It should be repeatedly terminating (due to having nothing to do) and restarting.

      3. Connect to the squid's database and update the `height` field of the [status schema](/en/sdk/squid-sdk/faq#how-do-squids-keep-track-of-their-sync-progress) to contain **slot** instead of the block height of the block mentioned there.

      4. Verify that the `height` field updated successfully by re-reading the status schema.

      5. Reset your codebase to its updated version and redeploy your squid again.
    </Accordion>
  </Step>
</Steps>

<Check>
  Your squid is now configured to source data from Portal.
</Check>
