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

# アカウントサブスクリプションと更新

> Laserstreamを使用して、アカウントの更新を購読し、オンチェーンの状態変化を効率的に追跡する方法を学びます。

オンチェーンの変化に応答するアプリケーションを構築する際、アカウントの更新についてRPCエンドポイントをポーリングすることは非効率で遅いです。アカウントサブスクリプションは、アカウントの状態変化に関するリアルタイムの更新を直接アプリケーションに提供することでこれを解決します。

このガイドでは、アカウントサブスクリプションについて知っておくべきこと、動作原理、特定のユースケースに合わせて最適化する方法を解説します。

***

## アカウントモデルのコンテキスト

<Info>
  Solanaのアカウントとその構造に詳しい場合、このセクションはスキップしてください。
</Info>

Solanaはアカウントベースのモデルを使用しています。すべてのデータはアカウント内に存在し、データとメタデータを保持するコンテナとして機能します。各アカウントは次のものを持ちます:

* **データ**: プログラムの状態、トークン残高、その他の情報を格納する実際のバイト
* **オーナー**: このアカウントを制御し、そのデータを修正できるプログラム
* **Lamports**: 家賃免除のためのアカウントのSOL残高
* **実行可能**: このアカウントがプログラムコードを含むかどうか

プログラムはステートレスであり、内部にデータを保存しません。代わりに、状態を保存するために別々のアカウントを作成し管理します。プログラムとやり取りする際には、読み取るまたは書き込むべきアカウントを渡します。

この設計により、アカウントサブスクリプションが強力になります。特定のアカウント、プログラムが所有するすべてのアカウント、または特定の基準に一致するアカウントの変更を監視できます。

***

## 基本的なアカウントサブスクリプション

トークンアカウントの変更を購読するシンプルな例から始めましょう。このスクリプトはトークンの残高が変わるたびに通知します:

```ts [expandable] theme={"system"}
import { subscribe, CommitmentLevel, SubscribeUpdate, LaserstreamConfig } from 'helius-laserstream';
import bs58 from 'bs58';

// Utility function to recursively convert Buffer objects to base58 strings
function convertBuffersToBase58(obj: any): any {
  if (obj === null || obj === undefined) {
    return obj;
  }
  
  if (Buffer.isBuffer(obj)) {
    return bs58.encode(obj);
  }
  
  if (Array.isArray(obj)) {
    return obj.map(convertBuffersToBase58);
  }
  
  if (typeof obj === 'object') {
    const result: any = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        result[key] = convertBuffersToBase58(obj[key]);
      }
    }
    return result;
  }
  
  return obj;
}

async function main() {
  console.log('🏦 Basic Account Subscription Example');

  const config: LaserstreamConfig = {
    apiKey: 'YOUR_API_KEY', // from https://dashboard.helius.dev/
    endpoint: 'https://laserstream-mainnet-ewr.helius-rpc.com', // pick the closest region
  };

  const request = {
    accounts: {
      "token-accounts": {
        account: [], // Specific account pubkeys (empty = all)
        owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"], // Token program
        filters: [
          {
            // Only token accounts (165 bytes)
            datasize: 165
          }
        ]
      }
    },
    commitment: CommitmentLevel.CONFIRMED,
    transactions: {}, slots: {}, transactionsStatus: {}, blocks: {}, blocksMeta: {}, entry: {}, accountsDataSlice: []
  };

  const stream = await subscribe(
    config,
    request,
    async (update: SubscribeUpdate) => {
      const readableUpdate = convertBuffersToBase58(update);
      console.log('🏦 Account Update:', JSON.stringify(readableUpdate, null, 2));
    },
    async (err) => console.error('❌ Stream error:', err)
  );

  console.log(`✅ Account subscription started (id: ${stream.id})`);

  process.on('SIGINT', () => {
    console.log('\n🛑 Cancelling stream...');
    stream.cancel();
    process.exit(0);
  });
}

main().catch(console.error);
```

この基本的なサブスクリプションを実行すると、リアルタイムのアカウント更新がコンソールにストリームされるのが分かります:

```
🏦 Basic Account Subscription Example
✅ Account subscription started (id: xyz789)

🏦 Account Update: {
  "filters": ["token-accounts"],
  "account": {
    "account": {
      "pubkey": "BKMHWYLAX4un3HUbR7a3u9jPmzCiLNa4mSj1RiX11eWF",
      "lamports": "2039280",
      "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      "rentEpoch": "18446744073709551615",
      "data": "2NUx6Xw9QkmgJCyYUP3d8TPsjJhUpSM7hcy9Fi1juGc6g9DrpPFyGyvBZzu9qiAjFtyEDbNiLHYFJsq1dD5Wxr4LPcF9Dqs4AJa15L1N92pfinnoKVfCsVCcybhV1iwkCCTMeMyxTRA4tqJm6MrLwgKG3HmmwVdhsEuXjSsGJFXGzgfgPHucVzBEgAqcpH9JPpoaQyis2MFwRJLjenxzkE8xJzWHv1Zk2T",
      "writeVersion": "2697618495",
      "txnSignature": "5C9Hr5nG2j8eQz6inxPmfyjbYdmXddzUDyR1iQgEnjYQ3RNvuP4Zzc8t1enLNy7Rk8KNCtQPEQztENYWxkt9GaVD"
    },
    "slot": "352366983"
  },
  "createdAt": "2025-07-10T11:56:22.027Z"
}
```

**何が起こったのでしょうか？** 私たちのサブスクリプションは完璧に機能しました！ Laserstreamにトークンアカウントの変更を通知するよう依頼し、アカウント`BKMHWYLAX4un3HUbR7a3u9jPmzCiLNa4mSj1RiX11eWF`について更新を受け取りました。

このアカウントに含まれるのは:

* **2,039,280ラマポート**（約0.002 SOLの残高 - これはこのトークンアカウントの家賃免除額です）
* **オーナープログラム** `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`（これはSPLトークンプログラムです）
* **トランザクション署名** `5C9Hr5nG2j8eQz6inxPmfyjbYdmXddzUDyR1iQgEnjYQ3RNvuP4Zzc8t1enLNy7Rk8KNCtQPEQztENYWxkt9GaVD`（このアカウントの変更を引き起こした特定のトランザクションを示します）
* **スロット352366983**（この更新がブロックチェーン上で発生したときのもの）
* **データフィールド**（base58でエンコードされた165バイトのアカウントデータが含まれます）

### データサイズを用いたアカウントフィルタリングの理解

データフィールドは重要です - 実際のトークンアカウントの構造を含んでいるからです。**スマートアカウントフィルタリング**のためにこの理解を活用しましょう。

#### なぜデータサイズフィルタリングを使うのか

フィルタリングが必要な理由を理解するために、まずトークンアカウントが何であるかを理解しましょう。**ウォレットが保有する各トークンについて、オンチェーン上に別個のアカウントがあります。** ウォレットが3つの異なるトークンを持っている場合（USDC、BONK、SOL）、実際には1つのウォレットアカウント（メインのSOLアカウント）と3つのトークンアカウント（各トークンタイプごとに1つ）があります。各トークンアカウントはちょうど165バイトであり、どのトークンを保持しているか（ミントアドレス）、誰が所有しているか（ウォレットアドレス）、およびそのトークンの量（量）が格納されています。

トークンプログラムはSolana上に**何百万ものアカウント**を持っていますが、それらすべてがユーザーのバランスを保有する「トークンアカウント」ではありません。フィルタリングの有無で何が起こるかを見てみましょう:

**フィルタリングなし - 大洪水:**

```ts theme={"system"}
accounts: {
  "all-token-program-accounts": {
    owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"] // ❌ Overwhelming!
  }
}
```

これはトークンプログラムが所有するすべてのアカウントを購読します、これには以下が含まれます:

* **トークンアカウント**（165バイト）- ユーザーのバランス: 数百万のアカウント
* **ミントアカウント**（82バイト）- トークン定義: 数十万のアカウント
* **マルチシグアカウント**（355バイト）- 共有ウォレット制御: 数万のアカウント
* **関連トークンプログラムアカウント**（さまざまなサイズ）- 数百万のアカウント

<Warning>
  **結果:** アプリケーションは大量のアカウント更新を常に受け取り、そのほとんどは重要ではありません。
</Warning>

**スマートフィルタリングで精密:**

```ts theme={"system"}
accounts: {
  "token-accounts-only": {
    owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
    filters: [{ datasize: 165 }] // ✅ Only standard token accounts
  }
}
```

これは165バイトのアカウントのみに絞り込み、具体的にはユーザートークン残高アカウントです。トークン転送、残高変動、ポートフォリオ更新を追跡するために正確に必要なものです。

**違い:**

* **フィルタリングなし:** 数百万のアカウント更新（ミント作成、マルチシグの変更など）
* **データサイズフィルタリングあり:** トークン残高の変更のみ

これは、ユーザートークン保有を実際に表すアカウントに焦点を絞ることでノイズを大幅に削減します。

#### なぜ165バイトなのか

これは魔法ではありません - [SPLトークンプログラムのアカウント構造](https://github.com/solana-program/token/blob/d05d10807fe8cf157f6e1f024c708274c30c953a/program/src/state.rs#L87)から来ています。ソースコードを見ると、`Account`の構造体がちょうど165バイトで定義されていることがわかります:

```rust theme={"system"}
pub struct Account {
    pub mint: Pubkey,                    // 32 bytes
    pub owner: Pubkey,                   // 32 bytes  
    pub amount: u64,                     // 8 bytes
    pub delegate: COption<Pubkey>,       // 4 + 32 bytes
    pub state: AccountState,             // 1 byte
    pub is_native: COption<u64>,         // 4 + 8 bytes
    pub delegated_amount: u64,           // 8 bytes
    pub close_authority: COption<Pubkey> // 4 + 32 bytes
}
// Total: 32+32+8+36+1+12+8+36 = 165 bytes
```

この固定サイズにより、標準的なトークンアカウントを正確にフィルタリングし、以下を除外できます:

* ミントアカウント（82バイト）
* マルチシグアカウント（355バイト）
* 関連トークンアカウントプログラムアカウント
* 異なるサイズの他のトークン関連アカウント

他のプログラムでアカウントサイズを計算するには、[アンカースペースリファレンス](https://www.anchor-lang.com/docs/references/space)を参照してください - さまざまなデータ型がどれだけのスペースを占有するかを示しています（Pubkey = 32バイト、u64 = 8バイトなど）。

#### アカウント構造のデコード

165バイトをフィルタリングした理由を理解したうえで、例のアカウント内の内容をデコードしましょう:

```
Base58 data: 2NUx6Xw9QkmgJCyYUP3d8TPsjJhUpSM7hcy9Fi1juGc6g9...
```

165バイトは次のように分解されます:

* **バイト0-31:** ミントアドレス（このアカウントが保有するトークン）
* **バイト32-63:** オーナーアドレス（このトークンアカウントの所有者）
* **バイト64-71:** トークン量（アカウントに含まれるトークン数）
* **バイト72-164:** 追加メタデータ（代理人、状態、クローズオーソリティなど）

この構造化アプローチにより、標準トークンアカウントの更新のみを受信し、他のアカウントタイプからのノイズを排除します。

### フィルタの組み合わせ: データサイズ＋memcmpでレーザ精度

ミントアドレスがバイト0-31にあることがわかったので、さらに特定の情報に絞り込むことができます。例えば、USDCトークンアカウントのみを監視したい場合、`datasize`フィルターを`memcmp`フィルターと組み合わせて、特定のミントアドレスをターゲットにできます:

```ts theme={"system"}
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

const request = {
  accounts: {
    "usdc-only": {
      owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
      filters: [
        { datasize: 165 },                    // Standard token accounts only
        { 
          memcmp: {
            offset: 0,                         // Mint address starts at byte 0
            base58: USDC_MINT                  // Match this specific mint
          }
        }
      ]
    }
  },
  // ... other config
};
```

**進行的なフィルタリング戦略:**

1. **オーナーフィルター:** "トークンプログラムが所有するアカウントを教えて"（数百万のアカウント）
2. **データサイズフィルター:** "だが165バイトの標準トークンアカウントのみ"（数十万）
3. **Memcmpフィルター:** "そしてUSDCを保有するもののみ"（数千）

この広範から特定へと進むことが効率的なアカウントモニタリングの鍵です。各フィルターは結果セットを絞り込み、必要な更新のみを受け取ります。

**重要:** すべてのフィルターはANDロジックを使用します - アカウント更新がトリガーされるためには、すべての条件を満たす必要があります。

### USDCアカウント更新の読み取り: 誰が、どのくらい、どこで？

これらのフィルタリングされた更新には実際に何が含まれるかを見てみましょう。トークンアカウントが変わったときに重要な質問に答えるUSDC専用モニターを作成します:

* **誰が**このトークンアカウントを所有していますか？
* **どのくらいの量**のUSDCが含まれていますか？
* **どこに**（どの特定のアカウントが）変わりましたか？
* **いつ**この変化が起こりましたか？
* **どのトランザクション**がこの変化を引き起こしましたか？

生のアカウント更新にはバイナリデータが含まれています。Solanaはアドレスと署名にbase58エンコーディングを使用しているため、バイナリバッファオブジェクトを可読の文字列に変換するために`bs58.encode()`関数を使用します。

```ts [expandable] theme={"system"}
import { subscribe, CommitmentLevel, SubscribeUpdate, LaserstreamConfig } from 'helius-laserstream';
import bs58 from 'bs58';

async function main() {
  console.log('USDC Account Monitor');

  const config: LaserstreamConfig = {
    apiKey: 'YOUR_API_KEY', // from https://dashboard.helius.dev/
    endpoint: 'https://laserstream-mainnet-ewr.helius-rpc.com', // pick the closest region
  };

  const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

  const request = {
    accounts: {
      "usdc-accounts": {
        account: [],
        owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
        filters: [
          { datasize: 165 },                           // Standard token accounts
          { memcmp: { offset: 0, base58: USDC_MINT } } // Only USDC
        ]
      }
    },
    commitment: CommitmentLevel.CONFIRMED,
    transactions: {}, slots: {}, transactionsStatus: {}, blocks: {}, blocksMeta: {}, entry: {}, accountsDataSlice: []
  };

  const stream = await subscribe(
    config,
    request,
    async (update: SubscribeUpdate) => {
      explainAccountUpdate(update);
    },
    async (err) => console.error('Stream error:', err)
  );

  console.log(`Account monitor started (id: ${stream.id})`);

  process.on('SIGINT', () => {
    console.log('\nCancelling stream...');
    stream.cancel();
    process.exit(0);
  });
}

function explainAccountUpdate(update: SubscribeUpdate) {
  if (!update.account) return;
  
  const account = update.account.account;
  
  // Decode the key addresses
  const tokenAccountAddress = bs58.encode(account.pubkey);
  const transactionSignature = account.txnSignature ? bs58.encode(account.txnSignature) : 'Unknown';
  
  // Extract and decode the token account data (165 bytes)
  const walletOwner = bs58.encode(account.data.slice(32, 64));       // Bytes 32-63: Owner
  const tokenAmount = account.data.readBigUInt64LE(64);              // Bytes 64-71: Amount
  const usdcAmount = Number(tokenAmount) / 1_000_000;                // Convert to USDC (6 decimals)
  
  console.log(`Account: ${tokenAccountAddress}`);
  console.log(`Owner: ${walletOwner}`);
  console.log(`Balance: ${usdcAmount.toLocaleString()} USDC`);
  console.log(`Slot: ${update.account.slot}`);
  console.log(`Transaction: ${transactionSignature.slice(0, 8)}...`);
  console.log('---');
}

main().catch(console.error);
```

このUSDCモニターを実行すると、次のようなクリーンで構造化された出力が表示されます:

```
USDC Account Monitor
Account monitor started (id: abc123)

Account: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU
Owner: 9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM
Balance: 1,500 USDC
Slot: 352154103
Transaction: 5v8fy0eJ...
---
Account: BQy5rNRxLfcaK6554PMzsg4VJsFXzwGnAnayb8TZKgZX
Owner: HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH
Balance: 0 USDC
Slot: 352154103
Transaction: 5v8fy0eJ...
---
```

各ブロックは状態が変わったUSDCアカウントを表しています。最初のアカウントは現在1,500 USDCを保持しており、2番目のアカウントは0 USDCに空にされました。トランザクション後すぐに現在の残高が表示され、どの特定のアカウントが変わったかおよびいつそれが起こったかがわかります。

アカウントサブスクリプションは、各アカウントに何が起こったかの最終結果を示しますが、トランザクションの詳細は示しません。完全なトランザクションコンテキスト（誰が誰に送ったのか、手数料など）を理解する必要がある場合は、表示された署名を使用して完全なトランザクションを取得する必要があります。

## 完全なフィルタリングリファレンス

これまでに使用した基本的な`owner`、`datasize`、`memcmp`フィルタに加えて、アカウントサブスクリプションは結果をさらに絞り込むための追加のフィルタリングオプションをサポートしています:

### 特定アカウントフィルタリング

公開鍵で正確なアカウントを監視します:

```ts theme={"system"}
accounts: {
  "specific-accounts": {
    account: [
      "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "BQy5rNRxLfcaK6554PMzsg4VJsFXzwGnAnayb8TZKgZX"
    ]
  }
}
```

このアプローチは、アプリケーションにとって重要なアカウントを正確に知っている場合に有用です - たとえば、アプリケーションの財務アカウントや特定のユーザーアカウントを監視する場合など。

非常に大きなアカウントセットの場合、明示的な公開鍵リストは高コストになります — 購読リクエストでアカウントごとに32バイトです。約10,000アカウントを超える場合、圧縮された[cuckoo filter](/ja/laserstream/cuckoo-filters)（アカウントあたり約3〜4バイト）を使用して、1つのストリームで数十万のアカウントを追跡します。RustおよびJavaScript SDKで利用可能です。

### 組み合わせたフィルタリング戦略

複数のフィルタタイプを組み合わせることによりパワーが発揮されます。こちらはメンタルモデルです:

1. **広範なネットをキャストする** `owner` - "このプログラムが管理するすべてのアカウントを教えて"
2. **構造でフィルタリングする** `datasize` - "だが特定のタイプのアカウントのみ"
3. **特定のデータをターゲットにする** `memcmp` - "そして特定の情報を含むもののみ"
4. **既知のアカウントを監視する** `account` - "または私が重要だと思う正確なアカウントを監視するだけ"

たとえば、高価値のUSDCアカウントを監視する場合:

```ts theme={"system"}
accounts: {
  "high-value-usdc": {
    owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
    filters: [
      { datasize: 165 },                           // Token accounts only
      { memcmp: { offset: 0, base58: USDC_MINT } } // USDC only
      // Note: You'd implement balance filtering in your callback logic
    ]
  }
}
```

各フィルターが受け取る更新の量を削減するキーインサイトです。フィルタリングしないと、圧倒的な量のアカウント更新を受ける可能性があります。スマートフィルタリングを使用すると、特定のユースケースにとって重要な更新のみを受信します。

### 大きな図を理解する

アカウントサブスクリプションはデータベースの変更のライブフィードを監視するようなものです。Solanaの状態は基本的に巨大なキー・バリューストアであり、各アカウントがエントリです。プログラムが実行されると、これらのアカウントが変更されます。サブスクリプションにより、特定のエントリがリアルタイムで変更されるのを監視できます。

フィルタリングシステムはデータベースインデックスのように機能します - 単に「すべての変更」を監視するのではなく、「これらの基準に一致するアカウントの変更」を監視しているのです。これにより、システムを無関係なデータで圧倒することなく、関連するオンチェーンイベントにすぐに応答するアプリケーションを構築することが可能になります。

## このパターンを他のプログラムに適用する

習得したアプローチは、どのSolanaプログラムにも適用できます。こちらが一般的なパターンです:

1. **アカウント構造を調査する** - プログラムのソースコードまたはドキュメントを確認
2. **オーナーフィルタリングから始める** - アカウントを管理するプログラムをターゲットに
3. **構造的フィルタを適用する** - アカウントのサイズ、データパターン、またはその他の特徴を使用して特定のアカウントタイプに絞り込む
4. **ターゲットフィルタを追加する** - アプリケーションにとって重要な特定のアカウント、状態、またはデータ値に集中する
