Get Program Accounts

RPC method는 프로그램에 의해 소유된 모든 Account들을 반환한다. 현재 pagination은 지원하지 않습니다. getProgramAccounts 요청은 응답 시간을 향상시키고 의도된 결과만을 반환하기 위해 dataSlice 그리고/또는 filters 파라미터를 포함해야 합니다.

Facts

Parameters

  • programId: string - 질의할 Program의 Pubkey, base58 인코딩 문자열
  • (optional) configOrCommitment: object - 아래의 optional field들을 포함하는 Configuration 파라미터들
    • (optional) commitment: string - State commitmentopen in new window
    • (optional) encoding: string - Account Data에 대한 인코딩, either: base58, base64, or jsonParsed. 주의, web3js 사용자는 getParsedProgramAccountsopen in new window를 사용해야 한다.
    • (optional) dataSlice: object - 반환되는 Account Data에 대한 제한 설정
      • offset: number - 반환을 시작할 Account Data 바이트 숫자
      • length: number - 반환할 Account Data의 바이트 수
    • (optional) filters: array - 아래의 filter 객체들을 사용하는 Filter 결과들
      • memcmp: object - 일련의 바이트와 Account Data의 비교:
        • offset: number - 비교를 시작할 Account Data 바이트 숫자
        • bytes: string - 비교할 Data, 129 bytes 제한되며 base58 인코딩 된 문자열
      • dataSize: number - Account Data의 길이와 데이터 사이즈의 비교
    • (optional) withContext: boolean - 결과를 포장할 RpcResponse JSON objectopen in new window
Response

getProgramAccounts는 기본적으로 아래의 구조를 갖는 JSON 객체들을 담고 있는 배열을 반환합니다.

  • pubkey: string - Account pubkey, base58 인코딩 된 문자열
  • account: object - 아래의 서브 속성들을 갖고 있는 JSON 객체
    • lamports: number, Account에 할당된 lamports의 수
    • owner: string, base58 인코딩 되어 Account에 할당된 Program의 pubkey
    • data: string | object - Account와 연관된 데이터, 인코딩 파라미터로 넘어온 값에 따라 인코딩 된 binary data 또는 JSON 형식
    • executable: boolean, Account가 Program을 포함하는지에 대한 표시
    • rentEpoch: number, Account가 rent 지불할 다음 epoch

Deep Dive

getProgramAccounts는 Program이 소유한 모든 Account들을 리턴하는 다재다능한 RPC method 입니다. 우리는 아래와 같이 몇 가지 유용한 쿼리를 위해 getProgramAccounts를 사용할 수 있습니다.

  • 특정 지갑에 대한 모든 Token Account들 조회
  • 특정 mint에 대한 모든 Token Account들 조회 (i.e. All SRMopen in new window holders)
  • 특정 Program에 대한 모든 custom Account들 조회 (i.e. All Mangoopen in new window users)

이렇게 유용함에도 불구하고, getProgramAccounts는 현재 제약사항들 때문에 자주 오해받습니다. getProgramAccounts에 의해 지원되는 많은 쿼리들은 대량의 데이터 셋을 스캔하기 위해 RPC 노드들을 요구합니다. 이런 스캔 작업들은 메모리와 자원 집중적이다. 결과적으로, 너무 자주 혹은 너무 크게 호출하는 것은 connection timeout을 야기할 수 있습니다. 뿐만 아니라, 이 글을 쓰는 시점에는, getProgramAccounts의 endpoint는 pagination을 지원하지 않습니다. 만약 쿼리의 결과가 너무 크다면, 응답 값은 잘릴 것입니다.

현재의 이런 제약사항들을 피하기 위해서, getProgramAccounts는 몇 가지 유용한 파라미터들을 제공합니다: dataSlice, filters, memcpm 그리고 dataSize. 이 파라미터들을 조합해 인자로 넘김으로써, 우리가 쿼리 할 영역을 관리 가능하고 예측 가능한 크기로 줄일 수 있습니다.

getProgramAccounts의 흔한 예제는 SPL-Token Programopen in new window과 통신하는 것입니다. basic call을 가지고 Token Program이 소유한 모든 Account를 요청하는 것은 막대한 양의 데이터를 호출하게 될 것입니다. 그러나, 파라미터들을 이용함으로써 우리는 효과적으로 우리가 사용하고자 하는 데이터만 요청할 수 있습니다.

filters

getProgramAccounts를 사용하기 위한 가장 흔한 파라미터는 filters array다. 이 array에는 dataSizememcmp 두 가지 타입의 필터가 들어갈 수 있습니다. 이 필터들을 이용하기 전에, 우리가 요청하고 있는 데이터들이 어떻게 놓여있고 직렬화되는지에 대해 익숙해져야 합니다.

dataSize

Token Program의 경우, 우리는 Token Account가 165 bytes의 길이open in new window를 가진다는 것을 알 수 있습니다. 구체적으로, Token Account는 각각 예측 가능한 bytes 수를 요구하는 8개의 다른 필드들을 가지고 있습니다. 우리는 아래의 그림을 통해 이 데이터가 어떻게 놓여있는지 확인할 수 있습니다.

Account Size

만약 우리가 우리의 지갑 Address가 소유한 모든 Token Account들을 알고 싶다면, 우리는 정확히 165 bytes 길이인 Account들에 대해 쿼리 영역을 좁게 만드는 { dataSize: 165 }filters array에 추가할 수 있을 것입니다. 그러나 이것만으로는 충분하지 않습니다. 우리는 또한 우리의 Addres가 소유한 Account들을 찾는 필터를 추가할 필요가 있습니다. 우리는 이것을 memcmp 필터를 통해 할 수 있습니다.

memcmp

memcmp 필터 혹은 "메모리 비교" 필터는 우리의 Account에 저장된 어떤 속성에 있는 데이터를 비교할 수 있게 해 줍니다. 구체적으로, 우리는 특정 포지션에 있는 특정 bytes 집합에 맞춰 Account들을 질의할 수 있다. memcmp는 두 가지 인자를 요구합니다:

  • offset: 데이터 비교를 시작할 위치. 이 위치는 bytes로 측정되며 integer로 표현됩니다.
  • bytes: Account의 데이터와 매칭 되어야 하는 데이터. 이것은 base-58로 인코딩 된 문자열로 표현되며 129 bytes 이하로 제한됩니다.

memcmpbytes가 정확히 매칭 된 경우에만 결과를 반환한다는 사실을 아는 것이 중요합니다. 현재, 우리가 제공할 bytes보다 크거나 작은 값에 대한 비교는 지원하지 않습니다.

Token Program 예제와 함께 계속해서, 우리는 우리의 지갑 Address가 소유한 Token Account들만 반환하도록 쿼리를 고칠 수 있습니다. Token Account를 봤을 때, 우리는 Token Account에 저장된 처음 두 필드가 모두 pubkey라는 것과, 각 pubkey는 32 bytes 길이인 것을 알 수 있습니다. owner가 두 번째 필드인 것을 고려하면, 우리는 memcmpoffset 32 bytes에서 시작해야합니다. 여기서부터, 우리는 owner 필드가 우리의 지갑 Address와 매칭되는 Account들을 찾을 것입니다.

Account Size

우리는 아래의 예제를 통해 이 쿼리를 호출할 수 있습니다.

import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { clusterApiUrl, Connection } from "@solana/web3.js";

(async () => {
  const MY_WALLET_ADDRESS = "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T";
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  const accounts = await connection.getParsedProgramAccounts(
    TOKEN_PROGRAM_ID, // new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
    {
      filters: [
        {
          dataSize: 165, // number of bytes
        },
        {
          memcmp: {
            offset: 32, // number of bytes
            bytes: MY_WALLET_ADDRESS, // base58 encoded string
          },
        },
      ],
    }
  );

  console.log(
    `Found ${accounts.length} token account(s) for wallet ${MY_WALLET_ADDRESS}: `
  );
  accounts.forEach((account, i) => {
    console.log(
      `-- Token Account Address ${i + 1}: ${account.pubkey.toString()} --`
    );
    console.log(`Mint: ${account.account.data["parsed"]["info"]["mint"]}`);
    console.log(
      `Amount: ${account.account.data["parsed"]["info"]["tokenAmount"]["uiAmount"]}`
    );
  });
  /*
    // Output

    Found 2 token account(s) for wallet FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T: 
    -- Token Account Address 0:  H12yCcKLHFJFfohkeKiN8v3zgaLnUMwRcnJTyB4igAsy --
    Mint: CKKDsBT6KiT4GDKs3e39Ue9tDkhuGUKM3cC2a7pmV9YK
    Amount: 1
    -- Token Account Address 1:  Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb --
    Mint: BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf
    Amount: 3
  */
})();
use solana_client::{
  rpc_client::RpcClient, 
  rpc_filter::{RpcFilterType, Memcmp, MemcmpEncodedBytes, MemcmpEncoding},
  rpc_config::{RpcProgramAccountsConfig, RpcAccountInfoConfig},
};
use solana_sdk::{commitment_config::CommitmentConfig, program_pack::Pack};
use spl_token::{state::{Mint, Account}};
use solana_account_decoder::{UiAccountEncoding};

fn main() {
  const MY_WALLET_ADDRESS: &str = "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T";

  let rpc_url = String::from("http://api.devnet.solana.com");
  let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

  let filters = Some(vec![
      RpcFilterType::Memcmp(Memcmp::new(
        32, // number of bytes
        MemcmpEncodedBytes::Base58(MY_WALLET_ADDRESS.to_string()),
      )),
      RpcFilterType::DataSize(165),
  ]);

  let accounts = connection.get_program_accounts_with_config(
      &spl_token::ID,
      RpcProgramAccountsConfig {
          filters,
          account_config: RpcAccountInfoConfig {
              encoding: Some(UiAccountEncoding::Base64),
              commitment: Some(connection.commitment()),
              ..RpcAccountInfoConfig::default()
          },
          ..RpcProgramAccountsConfig::default()
      },
  ).unwrap();

  println!("Found {:?} token account(s) for wallet {MY_WALLET_ADDRESS}: ", accounts.len());

  for (i, account) in accounts.iter().enumerate() {
      println!("-- Token Account Address {:?}:  {:?} --", i, account.0);

      let mint_token_account = Account::unpack_from_slice(account.1.data.as_slice()).unwrap();
      println!("Mint: {:?}", mint_token_account.mint);

      let mint_account_data = connection.get_account_data(&mint_token_account.mint).unwrap();
      let mint = Mint::unpack_from_slice(mint_account_data.as_slice()).unwrap();
      println!("Amount: {:?}", mint_token_account.amount as f64 /10usize.pow(mint.decimals as u32) as f64);
  }
}

/*
// Output

Found 2 token account(s) for wallet FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T: 
-- Token Account Address 0:  H12yCcKLHFJFfohkeKiN8v3zgaLnUMwRcnJTyB4igAsy --
Mint: CKKDsBT6KiT4GDKs3e39Ue9tDkhuGUKM3cC2a7pmV9YK
Amount: 1.0
-- Token Account Address 1:  Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb --
Mint: BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf
Amount: 3.0
*/
curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "encoding": "jsonParsed",
        "filters": [
          {
            "dataSize": 165
          },
          {
            "memcmp": {
              "offset": 32,
              "bytes": "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T"
            }
          }
        ]
      }
    ]
  }
'

# Output: 
# {
#   "jsonrpc": "2.0",
#   "result": [
#     {
#       "account": {
#         "data": {
#           "parsed": {
#             "info": {
#               "isNative": false,
#               "mint": "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf",
#               "owner": "FriELggez2Dy3phZeHHAdpcoEXkKQVkv6tx3zDtCVP8T",
#               "state": "initialized",
#               "tokenAmount": {
#                 "amount": "998999999000000000",
#                 "decimals": 9,
#                 "uiAmount": 998999999,
#                 "uiAmountString": "998999999"
#               }
#             },
#             "type": "account"
#           },
#           "program": "spl-token",
#           "space": 165
#         },
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 313
#       },
#       "pubkey": "Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb"
#     }
#   ],
#   "id": 1
# }

dataSlice

위 두 개의 필터 파라미터 밖에, getProgramAccounts의 세 번쨰로 흔한 파라미터는 dataSlice입니다. filters 파라미터와 다르게 dataSlice는 쿼리에 의해 반환되는 Account들의 수를 줄이지는 않을 것입니다. 대신에, dataSlice는 각 Account의 데이터 양을 제한할 것입니다.

memcmp와 유사하게, dataSlice는 아래의 두 개의 인자를 받습니다:

  • offset: Account Data 반환을 시작할 위치 (in number of bytes)
  • length: 반환되어야 하는 bytes의 개수

dataSlice는 우리가 실제 Account Data 자체에는 신경 쓰지 않는 큰 데이터 셋에 쿼리를 요청할 때 특히 유용합니다. 예로, 우리는 특정 Token mint에 대한 Token Accounts(i.e. Token 보유자의 수)들의 수를 알고 싶은 경우입니다.

import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { clusterApiUrl, Connection } from "@solana/web3.js";

(async () => {
  const MY_TOKEN_MINT_ADDRESS = "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf";
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  const accounts = await connection.getProgramAccounts(
    TOKEN_PROGRAM_ID, // new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
    {
      dataSlice: {
        offset: 0, // number of bytes
        length: 0, // number of bytes
      },
      filters: [
        {
          dataSize: 165, // number of bytes
        },
        {
          memcmp: {
            offset: 0, // number of bytes
            bytes: MY_TOKEN_MINT_ADDRESS, // base58 encoded string
          },
        },
      ],
    }
  );
  console.log(
    `Found ${accounts.length} token account(s) for mint ${MY_TOKEN_MINT_ADDRESS}`
  );
  console.log(accounts);

  /*
  // Output (notice the empty <Buffer > at acccount.data)
  
  Found 3 token account(s) for mint BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf
  [
    {
      account: {
        data: <Buffer >,
        executable: false,
        lamports: 2039280,
        owner: [PublicKey],
        rentEpoch: 228
      },
      pubkey: PublicKey {
        _bn: <BN: a8aca7a3132e74db2ca37bfcd66f4450f4631a5464b62fffbd83c48ef814d8d7>
      }
    },
    {
      account: {
        data: <Buffer >,
        executable: false,
        lamports: 2039280,
        owner: [PublicKey],
        rentEpoch: 228
      },
      pubkey: PublicKey {
        _bn: <BN: ce3b7b906c2ff6c6b62dc4798136ec017611078443918b2fad1cadff3c2e0448>
      }
    },
    {
      account: {
        data: <Buffer >,
        executable: false,
        lamports: 2039280,
        owner: [PublicKey],
        rentEpoch: 228
      },
      pubkey: PublicKey {
        _bn: <BN: d4560e42cb24472b0e1203ff4b0079d6452b19367b701643fa4ac33e0501cb1>
      }
    }
  ]
  */
})();
use solana_client::{
  rpc_client::RpcClient, 
  rpc_filter::{RpcFilterType, Memcmp, MemcmpEncodedBytes, MemcmpEncoding},
  rpc_config::{RpcProgramAccountsConfig, RpcAccountInfoConfig},
};
use solana_sdk::{commitment_config::CommitmentConfig};
use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig};

pub fn main() {
  const MY_TOKEN_MINT_ADDRESS: &str = "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf";

  let rpc_url = String::from("http://api.devnet.solana.com");
  let connection = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

  let filters = Some(vec![
      RpcFilterType::Memcmp(Memcmp::new(
        0, // number of bytes
        MemcmpEncodedBytes::Base58(MY_TOKEN_MINT_ADDRESS.to_string()),
      )),
      RpcFilterType::DataSize(165), // number of bytes
  ]);

  let accounts = connection.get_program_accounts_with_config(
      &spl_token::ID,
      RpcProgramAccountsConfig {
          filters,
          account_config: RpcAccountInfoConfig {
              data_slice: Some(UiDataSliceConfig {
                  offset: 0, // number of bytes
                  length: 0, // number of bytes
              }),
              encoding: Some(UiAccountEncoding::Base64),
              commitment: Some(connection.commitment()),
              ..RpcAccountInfoConfig::default()
          },
          ..RpcProgramAccountsConfig::default()
      },
  ).unwrap();

  println!("Found {:?} token account(s) for mint {MY_TOKEN_MINT_ADDRESS}: ", accounts.len());
  println!("{:#?}", accounts);
}

/*
// Output (notice zero `len` in `data` of `Account`s)

Found 3 token account(s) for mint BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf: 
[
  (
      tofD3NzLfZ5pWG91JcnbfsAbfMcFF2SRRp3ChnjeTcL,
      Account {
          lamports: 2039280,
          data.len: 0,
          owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
          executable: false,
          rent_epoch: 319,
      },
  ),
  (
      CMSC2GeWDsTPjfnhzCZHEqGRjKseBhrWaC2zNcfQQuGS,
      Account {
          lamports: 2039280,
          data.len: 0,
          owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
          executable: false,
          rent_epoch: 318,
      },
  ),
  (
      Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb,
      Account {
          lamports: 2039280,
          data.len: 0,
          owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,
          executable: false,
          rent_epoch: 318,
      },
  ),
]
*/
# Note: encoding only available for "base58", "base64" or "base64+zstd"
curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "encoding": "base64",
        "dataSlice": {
          "offset": 0,
          "length": 0
        },
        "filters": [
          {
            "dataSize": 165
          },
          {
            "memcmp": {
              "offset": 0,
              "bytes": "BUGuuhPsHpk8YZrL2GctsCtXGneL1gmT5zYb7eMHZDWf"
            }
          }
        ]
      }
    ]
  }
'

# Output:
# {
#   "jsonrpc": "2.0",
#   "result": [
#     {
#       "account": {
#         "data": [
#           "",
#           "base64"
#         ],
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 313
#       },
#       "pubkey": "FqWyVSLQgyRWyG1FuUGtHdTQHrEaBzXh1y9K6uPVTRZ4"
#     },
#     {
#       "account": {
#         "data": [
#           "",
#           "base64"
#         ],
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 314
#       },
#       "pubkey": "CMSC2GeWDsTPjfnhzCZHEqGRjKseBhrWaC2zNcfQQuGS"
#     },
#     {
#       "account": {
#         "data": [
#           "",
#           "base64"
#         ],
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 314
#       },
#       "pubkey": "61NfACb21WvuEzxyiJoxBrivpiLQ79gLBxzFo85BiJ2U"
#     },
#     {
#       "account": {
#         "data": [
#           "",
#           "base64"
#         ],
#         "executable": false,
#         "lamports": 2039280,
#         "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#         "rentEpoch": 313
#       },
#       "pubkey": "Et3bNDxe2wP1yE5ao6mMvUByQUHg8nZTndpJNvfKLdCb"
#     }
#   ],
#   "id": 1
# }

세 가지 파라미터(dataSlice, dataSize, and memcmp)들을 조합함으로써 우리는 질의할 영역을 제한할 수 있고 우리가 관심 있는 데이터만 효과적으로 리턴할 수 있습니다.

Other Resources

Last Updated:
Contributors: TaeGit