Skip to content

Commit

Permalink
add snapshotting token holders for a mint (#345)
Browse files Browse the repository at this point in the history
* add snapshotting token holders for a mint

* update docs

* make amount an option

* don't get zero balance accounts
  • Loading branch information
samuelvanderwaal authored Aug 11, 2024
1 parent 4d1b46d commit e19e47e
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 67 deletions.
15 changes: 7 additions & 8 deletions docs-src/src/snapshot.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Get snapshots of various blockchain states.

**Note**: Most of the snapshot commands rely on the [Digital Asset Standard (DAS) API](https://developers.metaplex.com/bubblegum#metaplex-das-api), which is a read layer for Metaplex NFTs that uses indexed data to serve up information without having to make onerous getProgramAccounts RPC calls to validators. To use these commands you will need to have a RPC URL set with a provider that supports the DAS API. The current official list from Metaplex is [here](https://developers.metaplex.com/rpc-providers).

Metaboss recommends using [Helius](https://helius.dev) for DAS API calls as they are the only provider that fully supported the DAS API spec on both mainnet and devnet when these commands were tested. In addition, they have a very generous free tier that should be sufficient for most casual users.
Metaboss recommends using [Helius](https://helius.dev) for DAS API calls as they are the only provider that fully supported the DAS API spec on both mainnet and devnet when these commands were tested. In addition, they have a very generous free tier that should be sufficient for most casual users. The `snapshot holders` command with the group key set to `mint` is only supported on Helius endpoints currently, as it relies on their `getTokenAccounts` custom addition to the DAS API.

### Snapshot Holders-GPA
(Legacy: not recommended for use.)
Expand Down Expand Up @@ -95,13 +95,12 @@ Creates a JSON file in the output directory with the name format of `<CANDY_MACH

Snapshot all current holders by various group types:

- Mint
Gets all token holders of a specific mint (unimplemented--not supported in DAS yet).
- **Mint:** Gets all token holders of a specific mint (currently only supported on Helius endpoints).

- First Verified Creator Address (FVCA)
- **First Verified Creator Address (FVCA):**
Gets all holders of NFTs with a specific FVCA.

- Metaplex Collection Id (MCC)
- **Metaplex Collection Id (MCC):**
Gets all holders of NFTs with a specific verified MCC ID.

#### Usage
Expand Down Expand Up @@ -138,14 +137,14 @@ metaboss snapshot holders PanbgtcTiZ2PveV96t2FHSffiLHXXjMuhvoabUUKKm8 -g fvca

Snapshot all mint accounts by various group types:

- Authority
- **Authority:**
Gets all NFT mint addresses for a given update authority.
**Warning:** update authority can be set to any address so use this option with caution.

- Creator
- **Creator:**
Gets all NFT mint addresses that have a specific verified creator.

- Metaplex Collection Id (MCC)
- **Metaplex Collection Id (MCC):**
Gets all NFT mint addresses with a specific verified MCC ID.

#### Usage
Expand Down
15 changes: 7 additions & 8 deletions docs/print.html
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ <h4 id="usage-22"><a class="header" href="#usage-22">Usage</a></h4>
<div style="break-before: page; page-break-before: always;"></div><h2 id="snapshot"><a class="header" href="#snapshot">Snapshot</a></h2>
<p>Get snapshots of various blockchain states.</p>
<p><strong>Note</strong>: Most of the snapshot commands rely on the <a href="https://developers.metaplex.com/bubblegum#metaplex-das-api">Digital Asset Standard (DAS) API</a>, which is a read layer for Metaplex NFTs that uses indexed data to serve up information without having to make onerous getProgramAccounts RPC calls to validators. To use these commands you will need to have a RPC URL set with a provider that supports the DAS API. The current official list from Metaplex is <a href="https://developers.metaplex.com/rpc-providers">here</a>.</p>
<p>Metaboss recommends using <a href="https://helius.dev">Helius</a> for DAS API calls as they are the only provider that fully supported the DAS API spec on both mainnet and devnet when these commands were tested. In addition, they have a very generous free tier that should be sufficient for most casual users.</p>
<p>Metaboss recommends using <a href="https://helius.dev">Helius</a> for DAS API calls as they are the only provider that fully supported the DAS API spec on both mainnet and devnet when these commands were tested. In addition, they have a very generous free tier that should be sufficient for most casual users. The <code>snapshot holders</code> command with the group key set to <code>mint</code> is only supported on Helius endpoints currently, as it relies on their <code>getTokenAccounts</code> custom addition to the DAS API.</p>
<h3 id="snapshot-holders-gpa"><a class="header" href="#snapshot-holders-gpa">Snapshot Holders-GPA</a></h3>
<p>(Legacy: not recommended for use.)</p>
<p>Snapshot all current holders of NFTs using the legacy getProgramAccounts method, filtered by verified candy_machine_id/first creator or update_authority.
Expand Down Expand Up @@ -983,15 +983,14 @@ <h3 id="snapshot-holders----das-api"><a class="header" href="#snapshot-holders--
<p>Snapshot all current holders by various group types:</p>
<ul>
<li>
<p>Mint
Gets all token holders of a specific mint (unimplemented--not supported in DAS yet).</p>
<p><strong>Mint:</strong> Gets all token holders of a specific mint (currently only supported on Helius endpoints).</p>
</li>
<li>
<p>First Verified Creator Address (FVCA)
<p><strong>First Verified Creator Address (FVCA):</strong>
Gets all holders of NFTs with a specific FVCA.</p>
</li>
<li>
<p>Metaplex Collection Id (MCC)
<p><strong>Metaplex Collection Id (MCC):</strong>
Gets all holders of NFTs with a specific verified MCC ID.</p>
</li>
</ul>
Expand Down Expand Up @@ -1020,16 +1019,16 @@ <h3 id="snapshot-mints----das-api"><a class="header" href="#snapshot-mints----da
<p>Snapshot all mint accounts by various group types:</p>
<ul>
<li>
<p>Authority
<p><strong>Authority:</strong>
Gets all NFT mint addresses for a given update authority.
<strong>Warning:</strong> update authority can be set to any address so use this option with caution.</p>
</li>
<li>
<p>Creator
<p><strong>Creator:</strong>
Gets all NFT mint addresses that have a specific verified creator.</p>
</li>
<li>
<p>Metaplex Collection Id (MCC)
<p><strong>Metaplex Collection Id (MCC):</strong>
Gets all NFT mint addresses with a specific verified MCC ID.</p>
</li>
</ul>
Expand Down
2 changes: 1 addition & 1 deletion docs/searchindex.js

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions docs/snapshot.html
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ <h1 class="menu-title">Metaboss</h1>
<h2 id="snapshot"><a class="header" href="#snapshot">Snapshot</a></h2>
<p>Get snapshots of various blockchain states.</p>
<p><strong>Note</strong>: Most of the snapshot commands rely on the <a href="https://developers.metaplex.com/bubblegum#metaplex-das-api">Digital Asset Standard (DAS) API</a>, which is a read layer for Metaplex NFTs that uses indexed data to serve up information without having to make onerous getProgramAccounts RPC calls to validators. To use these commands you will need to have a RPC URL set with a provider that supports the DAS API. The current official list from Metaplex is <a href="https://developers.metaplex.com/rpc-providers">here</a>.</p>
<p>Metaboss recommends using <a href="https://helius.dev">Helius</a> for DAS API calls as they are the only provider that fully supported the DAS API spec on both mainnet and devnet when these commands were tested. In addition, they have a very generous free tier that should be sufficient for most casual users.</p>
<p>Metaboss recommends using <a href="https://helius.dev">Helius</a> for DAS API calls as they are the only provider that fully supported the DAS API spec on both mainnet and devnet when these commands were tested. In addition, they have a very generous free tier that should be sufficient for most casual users. The <code>snapshot holders</code> command with the group key set to <code>mint</code> is only supported on Helius endpoints currently, as it relies on their <code>getTokenAccounts</code> custom addition to the DAS API.</p>
<h3 id="snapshot-holders-gpa"><a class="header" href="#snapshot-holders-gpa">Snapshot Holders-GPA</a></h3>
<p>(Legacy: not recommended for use.)</p>
<p>Snapshot all current holders of NFTs using the legacy getProgramAccounts method, filtered by verified candy_machine_id/first creator or update_authority.
Expand Down Expand Up @@ -236,15 +236,14 @@ <h3 id="snapshot-holders----das-api"><a class="header" href="#snapshot-holders--
<p>Snapshot all current holders by various group types:</p>
<ul>
<li>
<p>Mint
Gets all token holders of a specific mint (unimplemented--not supported in DAS yet).</p>
<p><strong>Mint:</strong> Gets all token holders of a specific mint (currently only supported on Helius endpoints).</p>
</li>
<li>
<p>First Verified Creator Address (FVCA)
<p><strong>First Verified Creator Address (FVCA):</strong>
Gets all holders of NFTs with a specific FVCA.</p>
</li>
<li>
<p>Metaplex Collection Id (MCC)
<p><strong>Metaplex Collection Id (MCC):</strong>
Gets all holders of NFTs with a specific verified MCC ID.</p>
</li>
</ul>
Expand Down Expand Up @@ -273,16 +272,16 @@ <h3 id="snapshot-mints----das-api"><a class="header" href="#snapshot-mints----da
<p>Snapshot all mint accounts by various group types:</p>
<ul>
<li>
<p>Authority
<p><strong>Authority:</strong>
Gets all NFT mint addresses for a given update authority.
<strong>Warning:</strong> update authority can be set to any address so use this option with caution.</p>
</li>
<li>
<p>Creator
<p><strong>Creator:</strong>
Gets all NFT mint addresses that have a specific verified creator.</p>
</li>
<li>
<p>Metaplex Collection Id (MCC)
<p><strong>Metaplex Collection Id (MCC):</strong>
Gets all NFT mint addresses with a specific verified MCC ID.</p>
</li>
</ul>
Expand Down
2 changes: 1 addition & 1 deletion src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1364,7 +1364,7 @@ pub enum SnapshotSubcommands {
#[structopt(short, long, default_value = ".")]
output: PathBuf,

/// Delay between DAS API requests in milliseconds; defaults to 500
/// Delay between DAS API requests in milliseconds
#[structopt(short = "D", long, default_value = "500")]
delay: u64,
},
Expand Down
121 changes: 80 additions & 41 deletions src/snapshot/das_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use spl_associated_token_account::get_associated_token_address;

use crate::{
setup::{CliConfig, ClientLike, ClientType},
snapshot::TokenResponse,
spinner::create_spinner,
};

Expand Down Expand Up @@ -63,7 +64,18 @@ pub async fn snapshot_holders(args: HoldersArgs) -> Result<()> {
let config = CliConfig::new(None, Some(args.rpc_url), ClientType::DAS)?;

let query = match args.group_key {
HolderGroupKey::Mint => todo!(),
HolderGroupKey::Mint => Query {
method: "getTokenAccounts".to_string(),
params: json!({
"mint": args.group_value.to_string(),
"page": 1,
"limit": 1000,
"displayOptions": {
"showZeroBalance": false,
}
}),
fvca_filter: false,
},
HolderGroupKey::Fvca => Query {
method: "getAssetsByCreator".to_string(),
params: json!({
Expand Down Expand Up @@ -94,6 +106,7 @@ pub async fn snapshot_holders(args: HoldersArgs) -> Result<()> {
_ => panic!("Wrong client type"),
};

let mut token_holders = Vec::new();
let mut holders = Vec::new();
let mut page = 1;

Expand Down Expand Up @@ -124,55 +137,81 @@ pub async fn snapshot_holders(args: HoldersArgs) -> Result<()> {
bail!("Status: {status}\nResponse: {}", response.text().await?);
}

let res: DasResponse = response.json().await?;
match args.group_key {
HolderGroupKey::Mint => {
let res: TokenResponse = response.json().await?;

if res.result.items.is_empty() {
break;
}
if res.result.token_accounts.is_empty() {
break;
}

page += 1;
body["params"]["page"] = json!(page);
page += 1;
body["params"]["page"] = json!(page);

res.result
.items
.iter()
.filter(|item| {
if query.fvca_filter {
fvca_filter(item)
} else {
true
token_holders.extend(res.result.token_accounts);
}
HolderGroupKey::Fvca | HolderGroupKey::Mcc => {
let res: DasResponse = response.json().await?;

if res.result.items.is_empty() {
break;
}
})
.for_each(|item| {
let mint_address = item.id.clone();
let metadata_pubkey =
derive_metadata_pda(&Pubkey::from_str(mint_address.as_str()).unwrap());
let owner_address = item.ownership.owner.clone();
let ata_pubkey = get_associated_token_address(
&Pubkey::from_str(&owner_address).unwrap(),
&Pubkey::from_str(&mint_address).unwrap(),
);

holders.push(Holder {
owner: owner_address,
mint: item.id.clone(),
metadata: metadata_pubkey.to_string(),
ata: ata_pubkey.to_string(),
});
});

std::thread::sleep(std::time::Duration::from_millis(args.delay));
page += 1;
body["params"]["page"] = json!(page);

res.result
.items
.iter()
.filter(|item| {
if query.fvca_filter {
fvca_filter(item)
} else {
true
}
})
.for_each(|item| {
let mint_address = item.id.clone();
let metadata_pubkey =
derive_metadata_pda(&Pubkey::from_str(mint_address.as_str()).unwrap());
let owner_address = item.ownership.owner.clone();
let ata_pubkey = get_associated_token_address(
&Pubkey::from_str(&owner_address).unwrap(),
&Pubkey::from_str(&mint_address).unwrap(),
);

holders.push(Holder {
owner: owner_address,
mint: item.id.clone(),
metadata: metadata_pubkey.to_string(),
ata: ata_pubkey.to_string(),
});
});

std::thread::sleep(std::time::Duration::from_millis(args.delay));
}
}
}
spinner.finish();

holders.sort();
if !holders.is_empty() {
holders.sort();

// Write to file
let file = File::create(format!(
"{}_{}_holders.json",
args.group_value, args.group_key
))?;
serde_json::to_writer_pretty(file, &holders)?;
// Write to file
let file = File::create(format!(
"{}_{}_holders.json",
args.group_value, args.group_key
))?;
serde_json::to_writer_pretty(file, &holders)?;
}

if !token_holders.is_empty() {
token_holders.sort_by(|a, b| a.owner.cmp(&b.owner));

// Write to file
let file = File::create(format!("{}_token_holders.json", args.group_value))?;
serde_json::to_writer_pretty(file, &token_holders)?;
}

Ok(())
}
Expand Down
28 changes: 28 additions & 0 deletions src/snapshot/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,33 @@ use mpl_token_metadata::types::Creator;
use serde::{Deserialize, Serialize};
use serde_json::Value;

// `getTokenAccounts` has different return types than other DAS methods.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TokenResponse {
id: u32,
jsonrpc: String,
pub result: TokenResult,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenResult {
pub total: u32,
pub limit: u32,
pub page: u32,
pub token_accounts: Vec<TokenAccount>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenAccount {
pub owner: String,
pub mint: String,
pub address: String,
pub amount: Option<u64>, // Some tokens seem to be missing this field
pub delegated_amount: u64,
pub frozen: bool,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DasResponse {
Expand All @@ -69,6 +96,7 @@ pub struct DasResult {
pub page: u32,
pub items: Vec<Item>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ByCreatorResult {
pub total: u32,
Expand Down

0 comments on commit e19e47e

Please sign in to comment.