@styxstack/whisperdrop-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2024-2026 @moonmanquark (Bluefoot Labs)
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # @styxstack/whisperdrop-sdk
2
+
3
+ TypeScript SDK for **WhisperDrop** - Privacy-Preserving Airdrops on Solana.
4
+
5
+ [![npm version](https://badge.fury.io/js/@styx-stack%2Fwhisperdrop-sdk.svg)](https://www.npmjs.com/package/@styxstack/whisperdrop-sdk)
6
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
+
8
+ ## Features
9
+
10
+ - 🔒 **Privacy-Preserving** - Recipient lists never published on-chain
11
+ - 🌳 **Merkle Proofs** - Efficient verification for 1M+ recipients
12
+ - 🎫 **Token Gating** - SPL, Token-2022, NFT, and cNFT support
13
+ - ⏰ **Time-Locked** - Automatic expiry with reclaim
14
+ - âš¡ **Ultra-Efficient** - ~5K compute units per claim
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @styxstack/whisperdrop-sdk @solana/web3.js
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import { Connection, Keypair, PublicKey } from '@solana/web3.js';
26
+ import {
27
+ WhisperDrop,
28
+ buildMerkleTree,
29
+ generateCampaignId,
30
+ generateNonce,
31
+ GateType
32
+ } from '@styxstack/whisperdrop-sdk';
33
+
34
+ const connection = new Connection('https://api.mainnet-beta.solana.com');
35
+ const whisperdrop = new WhisperDrop(connection);
36
+
37
+ // 1. Create allocations
38
+ const allocations = [
39
+ { recipient: new PublicKey('...'), amount: 1000n, nonce: generateNonce() },
40
+ { recipient: new PublicKey('...'), amount: 2000n, nonce: generateNonce() },
41
+ // ... up to 1M+ recipients
42
+ ];
43
+
44
+ // 2. Build Merkle tree
45
+ const campaignId = generateCampaignId();
46
+ const { root, proofs } = buildMerkleTree(campaignId, allocations);
47
+
48
+ // 3. Initialize campaign with Token-2022 gating
49
+ const { campaignPDA, escrowPDA } = await whisperdrop.initCampaign(
50
+ authority,
51
+ tokenMint,
52
+ {
53
+ campaignId,
54
+ merkleRoot: root,
55
+ expiryUnix: BigInt(Date.now() / 1000 + 86400 * 30), // 30 days
56
+ gateType: GateType.Token22Holder,
57
+ gateMint: membershipToken, // Require Token-2022 token to claim
58
+ }
59
+ );
60
+
61
+ // 4. Recipients claim with proof
62
+ const proof = proofs.get(recipient.publicKey.toBase58())!;
63
+ await whisperdrop.claim(
64
+ payer,
65
+ recipient,
66
+ campaignPDA,
67
+ allocation,
68
+ proof,
69
+ recipientATA,
70
+ gateTokenAccount // Token-2022 token account
71
+ );
72
+ ```
73
+
74
+ ## Token Gating
75
+
76
+ WhisperDrop supports selective claiming based on multiple token standards:
77
+
78
+ ### SPL Token (Original Token Program)
79
+
80
+ ```typescript
81
+ // Require any amount of SPL token
82
+ { gateType: GateType.SplTokenHolder, gateMint: tokenMint }
83
+
84
+ // Require minimum balance
85
+ { gateType: GateType.SplMinBalance, gateMint: tokenMint, gateAmount: 1000_000_000n }
86
+ ```
87
+
88
+ ### Token-2022 (Token Extensions)
89
+
90
+ ```typescript
91
+ // Require any amount of Token-2022 token
92
+ { gateType: GateType.Token22Holder, gateMint: token22Mint }
93
+
94
+ // Require minimum balance
95
+ { gateType: GateType.Token22MinBalance, gateMint: token22Mint, gateAmount: 500_000_000n }
96
+ ```
97
+
98
+ ### NFT (Metaplex Standard)
99
+
100
+ ```typescript
101
+ // Require specific NFT
102
+ { gateType: GateType.NftHolder, gateMint: nftMint }
103
+
104
+ // Require NFT from collection
105
+ { gateType: GateType.NftCollection, gateMint: collectionMint }
106
+ ```
107
+
108
+ ### cNFT (Compressed NFT via Bubblegum)
109
+
110
+ ```typescript
111
+ // Require specific cNFT (asset ID as mint)
112
+ { gateType: GateType.CnftHolder, gateMint: assetId }
113
+
114
+ // Require cNFT from collection
115
+ { gateType: GateType.CnftCollection, gateMint: collectionId }
116
+ ```
117
+
118
+ ### GateType.NftCollection
119
+ Must hold an NFT from a specific collection (future: Metaplex verification).
120
+
121
+ ## API Reference
122
+
123
+ ### WhisperDrop Class
124
+
125
+ ```typescript
126
+ const wd = new WhisperDrop(connection, programId?);
127
+ ```
128
+
129
+ #### `initCampaign(authority, mint, config)`
130
+ Initialize a new airdrop campaign.
131
+
132
+ #### `claim(payer, recipient, campaignPDA, allocation, proof, recipientATA, gateTokenAccount?)`
133
+ Claim tokens with Merkle proof.
134
+
135
+ #### `reclaim(authority, campaignPDA, authorityATA)`
136
+ Reclaim remaining tokens after expiry.
137
+
138
+ #### `hasClaimed(campaignPDA, recipient)`
139
+ Check if recipient has already claimed.
140
+
141
+ #### `getCampaign(campaignPDA)`
142
+ Get campaign state.
143
+
144
+ ### Merkle Tree Utilities
145
+
146
+ ```typescript
147
+ import {
148
+ buildMerkleTree,
149
+ verifyMerkleProof,
150
+ computeLeafHash,
151
+ computeNodeHash,
152
+ } from '@styxstack/whisperdrop-sdk';
153
+ ```
154
+
155
+ ### PDA Derivation
156
+
157
+ ```typescript
158
+ import {
159
+ deriveCampaignPDA,
160
+ deriveEscrowPDA,
161
+ deriveNullifierPDA,
162
+ } from '@styxstack/whisperdrop-sdk';
163
+ ```
164
+
165
+ ## Program IDs
166
+
167
+ | Network | Program ID |
168
+ |---------|------------|
169
+ | **Mainnet** | `GhstFNnEbixAGQgLnWg1nWetJQgGfSUMhnxdBA6hWu5e` |
170
+ | Devnet | `BPM5VuX9YrG7CgueWGxtqQZBQwMTacc315ppWCtTCJ5q` |
171
+
172
+ ## Use Cases
173
+
174
+ ### 1. NFT Holder Airdrops
175
+ Airdrop tokens only to holders of your NFT collection.
176
+
177
+ ### 2. Staker Rewards
178
+ Distribute rewards to users who stake above a threshold.
179
+
180
+ ### 3. Community Rewards
181
+ Private distribution to community members without revealing the list.
182
+
183
+ ### 4. Retroactive Airdrops
184
+ Reward early users without publishing their addresses.
185
+
186
+ ## Security
187
+
188
+ - **Nullifiers** prevent double-claiming
189
+ - **Time-locked reclaim** protects unclaimed funds
190
+ - **Immutable Merkle root** ensures allocation integrity
191
+ - **PDA escrow** for secure token custody
192
+
193
+ ## License
194
+
195
+ Apache-2.0
196
+
197
+ ---
198
+
199
+ Built by [@moonmanquark](https://github.com/moonmanquark) at **Bluefoot Labs**
@@ -0,0 +1,175 @@
1
+ import { PublicKey, Connection, Keypair } from '@solana/web3.js';
2
+
3
+ /**
4
+ * @styxstack/whisperdrop-sdk
5
+ *
6
+ * TypeScript SDK for WhisperDrop - Privacy-Preserving Airdrops on Solana
7
+ *
8
+ * Features:
9
+ * - Merkle tree generation and proof creation
10
+ * - Token-gated claiming (NFT, token balance)
11
+ * - Campaign creation and management
12
+ * - Claim and reclaim operations
13
+ *
14
+ * @author @moonmanquark (Bluefoot Labs)
15
+ * @license Apache-2.0
16
+ */
17
+
18
+ /** Mainnet Program ID */
19
+ declare const WHISPERDROP_PROGRAM_ID: PublicKey;
20
+ /** Devnet Program ID */
21
+ declare const WHISPERDROP_DEVNET_PROGRAM_ID: PublicKey;
22
+ /** Token Program ID (SPL Token) */
23
+ declare const TOKEN_PROGRAM_ID: PublicKey;
24
+ /** Token-2022 Program ID */
25
+ declare const TOKEN_2022_PROGRAM_ID: PublicKey;
26
+ /** Metaplex Token Metadata Program ID */
27
+ declare const METADATA_PROGRAM_ID: PublicKey;
28
+ /** Bubblegum (cNFT) Program ID */
29
+ declare const BUBBLEGUM_PROGRAM_ID: PublicKey;
30
+ /**
31
+ * Gate type for selective claiming
32
+ *
33
+ * Supports multiple token standards:
34
+ * - SPL Token (original Solana token program)
35
+ * - Token-2022 (Token Extensions)
36
+ * - NFT (Metaplex standard)
37
+ * - cNFT (Compressed NFT via Bubblegum)
38
+ */
39
+ declare enum GateType {
40
+ /** No gate - anyone with valid proof can claim */
41
+ None = 0,
42
+ /** Must hold any amount of SPL token */
43
+ SplTokenHolder = 1,
44
+ /** Must hold minimum balance of SPL token */
45
+ SplMinBalance = 2,
46
+ /** Must hold any amount of Token-2022 token */
47
+ Token22Holder = 3,
48
+ /** Must hold minimum balance of Token-2022 token */
49
+ Token22MinBalance = 4,
50
+ /** Must hold specific NFT */
51
+ NftHolder = 5,
52
+ /** Must hold NFT from verified collection */
53
+ NftCollection = 6,
54
+ /** Must hold specific cNFT */
55
+ CnftHolder = 7,
56
+ /** Must hold cNFT from collection */
57
+ CnftCollection = 8
58
+ }
59
+ /**
60
+ * Helper to determine which token program a gate type uses
61
+ */
62
+ declare function getTokenProgramForGate(gateType: GateType): PublicKey | null;
63
+ /**
64
+ * Check if gate type requires minimum balance check
65
+ */
66
+ declare function requiresMinBalance(gateType: GateType): boolean;
67
+ /**
68
+ * Check if gate type is for compressed NFTs
69
+ */
70
+ declare function isCnftGate(gateType: GateType): boolean;
71
+ /** Recipient allocation for Merkle tree */
72
+ interface Allocation {
73
+ recipient: PublicKey;
74
+ amount: bigint;
75
+ nonce: Uint8Array;
76
+ }
77
+ /** Campaign configuration */
78
+ interface CampaignConfig {
79
+ campaignId: Uint8Array;
80
+ merkleRoot: Uint8Array;
81
+ expiryUnix: bigint;
82
+ gateType?: GateType;
83
+ gateMint?: PublicKey;
84
+ gateAmount?: bigint;
85
+ }
86
+ /** Merkle proof for claiming */
87
+ interface MerkleProof {
88
+ allocation: Allocation;
89
+ proof: Uint8Array[];
90
+ }
91
+ /** Campaign state (on-chain) */
92
+ interface Campaign {
93
+ authority: PublicKey;
94
+ mint: PublicKey;
95
+ campaignId: Uint8Array;
96
+ merkleRoot: Uint8Array;
97
+ expiryUnix: bigint;
98
+ gateType: GateType;
99
+ gateMint: PublicKey;
100
+ gateAmount: bigint;
101
+ }
102
+ /**
103
+ * Compute leaf hash for an allocation
104
+ */
105
+ declare function computeLeafHash(campaignId: Uint8Array, recipient: PublicKey, amount: bigint, nonce: Uint8Array): Uint8Array;
106
+ /**
107
+ * Compute node hash (sorted pair)
108
+ */
109
+ declare function computeNodeHash(left: Uint8Array, right: Uint8Array): Uint8Array;
110
+ /**
111
+ * Build Merkle tree from allocations
112
+ * Returns root hash and all proofs
113
+ */
114
+ declare function buildMerkleTree(campaignId: Uint8Array, allocations: Allocation[]): {
115
+ root: Uint8Array;
116
+ proofs: Map<string, Uint8Array[]>;
117
+ };
118
+ /**
119
+ * Verify Merkle proof
120
+ */
121
+ declare function verifyMerkleProof(campaignId: Uint8Array, allocation: Allocation, proof: Uint8Array[], root: Uint8Array): boolean;
122
+ /**
123
+ * Derive campaign PDA
124
+ */
125
+ declare function deriveCampaignPDA(campaignId: Uint8Array, programId?: PublicKey): [PublicKey, number];
126
+ /**
127
+ * Derive escrow PDA
128
+ */
129
+ declare function deriveEscrowPDA(campaignPDA: PublicKey, programId?: PublicKey): [PublicKey, number];
130
+ /**
131
+ * Derive nullifier PDA
132
+ */
133
+ declare function deriveNullifierPDA(campaignPDA: PublicKey, recipient: PublicKey, programId?: PublicKey): [PublicKey, number];
134
+ /**
135
+ * WhisperDrop SDK
136
+ */
137
+ declare class WhisperDrop {
138
+ connection: Connection;
139
+ programId: PublicKey;
140
+ constructor(connection: Connection, programId?: PublicKey);
141
+ /**
142
+ * Initialize a new airdrop campaign
143
+ */
144
+ initCampaign(authority: Keypair, mint: PublicKey, config: CampaignConfig): Promise<{
145
+ signature: string;
146
+ campaignPDA: PublicKey;
147
+ escrowPDA: PublicKey;
148
+ }>;
149
+ /**
150
+ * Claim tokens with Merkle proof
151
+ */
152
+ claim(payer: Keypair, recipient: Keypair, campaignPDA: PublicKey, allocation: Allocation, proof: Uint8Array[], recipientATA: PublicKey, gateTokenAccount?: PublicKey): Promise<string>;
153
+ /**
154
+ * Reclaim remaining tokens after expiry
155
+ */
156
+ reclaim(authority: Keypair, campaignPDA: PublicKey, authorityATA: PublicKey): Promise<string>;
157
+ /**
158
+ * Check if recipient has already claimed
159
+ */
160
+ hasClaimed(campaignPDA: PublicKey, recipient: PublicKey): Promise<boolean>;
161
+ /**
162
+ * Get campaign state
163
+ */
164
+ getCampaign(campaignPDA: PublicKey): Promise<Campaign | null>;
165
+ }
166
+ /**
167
+ * Generate random nonce for allocation
168
+ */
169
+ declare function generateNonce(): Uint8Array;
170
+ /**
171
+ * Generate campaign ID
172
+ */
173
+ declare function generateCampaignId(): Uint8Array;
174
+
175
+ export { type Allocation, BUBBLEGUM_PROGRAM_ID, type Campaign, type CampaignConfig, GateType, METADATA_PROGRAM_ID, type MerkleProof, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, WHISPERDROP_DEVNET_PROGRAM_ID, WHISPERDROP_PROGRAM_ID, WhisperDrop, buildMerkleTree, computeLeafHash, computeNodeHash, WhisperDrop as default, deriveCampaignPDA, deriveEscrowPDA, deriveNullifierPDA, generateCampaignId, generateNonce, getTokenProgramForGate, isCnftGate, requiresMinBalance, verifyMerkleProof };
@@ -0,0 +1,175 @@
1
+ import { PublicKey, Connection, Keypair } from '@solana/web3.js';
2
+
3
+ /**
4
+ * @styxstack/whisperdrop-sdk
5
+ *
6
+ * TypeScript SDK for WhisperDrop - Privacy-Preserving Airdrops on Solana
7
+ *
8
+ * Features:
9
+ * - Merkle tree generation and proof creation
10
+ * - Token-gated claiming (NFT, token balance)
11
+ * - Campaign creation and management
12
+ * - Claim and reclaim operations
13
+ *
14
+ * @author @moonmanquark (Bluefoot Labs)
15
+ * @license Apache-2.0
16
+ */
17
+
18
+ /** Mainnet Program ID */
19
+ declare const WHISPERDROP_PROGRAM_ID: PublicKey;
20
+ /** Devnet Program ID */
21
+ declare const WHISPERDROP_DEVNET_PROGRAM_ID: PublicKey;
22
+ /** Token Program ID (SPL Token) */
23
+ declare const TOKEN_PROGRAM_ID: PublicKey;
24
+ /** Token-2022 Program ID */
25
+ declare const TOKEN_2022_PROGRAM_ID: PublicKey;
26
+ /** Metaplex Token Metadata Program ID */
27
+ declare const METADATA_PROGRAM_ID: PublicKey;
28
+ /** Bubblegum (cNFT) Program ID */
29
+ declare const BUBBLEGUM_PROGRAM_ID: PublicKey;
30
+ /**
31
+ * Gate type for selective claiming
32
+ *
33
+ * Supports multiple token standards:
34
+ * - SPL Token (original Solana token program)
35
+ * - Token-2022 (Token Extensions)
36
+ * - NFT (Metaplex standard)
37
+ * - cNFT (Compressed NFT via Bubblegum)
38
+ */
39
+ declare enum GateType {
40
+ /** No gate - anyone with valid proof can claim */
41
+ None = 0,
42
+ /** Must hold any amount of SPL token */
43
+ SplTokenHolder = 1,
44
+ /** Must hold minimum balance of SPL token */
45
+ SplMinBalance = 2,
46
+ /** Must hold any amount of Token-2022 token */
47
+ Token22Holder = 3,
48
+ /** Must hold minimum balance of Token-2022 token */
49
+ Token22MinBalance = 4,
50
+ /** Must hold specific NFT */
51
+ NftHolder = 5,
52
+ /** Must hold NFT from verified collection */
53
+ NftCollection = 6,
54
+ /** Must hold specific cNFT */
55
+ CnftHolder = 7,
56
+ /** Must hold cNFT from collection */
57
+ CnftCollection = 8
58
+ }
59
+ /**
60
+ * Helper to determine which token program a gate type uses
61
+ */
62
+ declare function getTokenProgramForGate(gateType: GateType): PublicKey | null;
63
+ /**
64
+ * Check if gate type requires minimum balance check
65
+ */
66
+ declare function requiresMinBalance(gateType: GateType): boolean;
67
+ /**
68
+ * Check if gate type is for compressed NFTs
69
+ */
70
+ declare function isCnftGate(gateType: GateType): boolean;
71
+ /** Recipient allocation for Merkle tree */
72
+ interface Allocation {
73
+ recipient: PublicKey;
74
+ amount: bigint;
75
+ nonce: Uint8Array;
76
+ }
77
+ /** Campaign configuration */
78
+ interface CampaignConfig {
79
+ campaignId: Uint8Array;
80
+ merkleRoot: Uint8Array;
81
+ expiryUnix: bigint;
82
+ gateType?: GateType;
83
+ gateMint?: PublicKey;
84
+ gateAmount?: bigint;
85
+ }
86
+ /** Merkle proof for claiming */
87
+ interface MerkleProof {
88
+ allocation: Allocation;
89
+ proof: Uint8Array[];
90
+ }
91
+ /** Campaign state (on-chain) */
92
+ interface Campaign {
93
+ authority: PublicKey;
94
+ mint: PublicKey;
95
+ campaignId: Uint8Array;
96
+ merkleRoot: Uint8Array;
97
+ expiryUnix: bigint;
98
+ gateType: GateType;
99
+ gateMint: PublicKey;
100
+ gateAmount: bigint;
101
+ }
102
+ /**
103
+ * Compute leaf hash for an allocation
104
+ */
105
+ declare function computeLeafHash(campaignId: Uint8Array, recipient: PublicKey, amount: bigint, nonce: Uint8Array): Uint8Array;
106
+ /**
107
+ * Compute node hash (sorted pair)
108
+ */
109
+ declare function computeNodeHash(left: Uint8Array, right: Uint8Array): Uint8Array;
110
+ /**
111
+ * Build Merkle tree from allocations
112
+ * Returns root hash and all proofs
113
+ */
114
+ declare function buildMerkleTree(campaignId: Uint8Array, allocations: Allocation[]): {
115
+ root: Uint8Array;
116
+ proofs: Map<string, Uint8Array[]>;
117
+ };
118
+ /**
119
+ * Verify Merkle proof
120
+ */
121
+ declare function verifyMerkleProof(campaignId: Uint8Array, allocation: Allocation, proof: Uint8Array[], root: Uint8Array): boolean;
122
+ /**
123
+ * Derive campaign PDA
124
+ */
125
+ declare function deriveCampaignPDA(campaignId: Uint8Array, programId?: PublicKey): [PublicKey, number];
126
+ /**
127
+ * Derive escrow PDA
128
+ */
129
+ declare function deriveEscrowPDA(campaignPDA: PublicKey, programId?: PublicKey): [PublicKey, number];
130
+ /**
131
+ * Derive nullifier PDA
132
+ */
133
+ declare function deriveNullifierPDA(campaignPDA: PublicKey, recipient: PublicKey, programId?: PublicKey): [PublicKey, number];
134
+ /**
135
+ * WhisperDrop SDK
136
+ */
137
+ declare class WhisperDrop {
138
+ connection: Connection;
139
+ programId: PublicKey;
140
+ constructor(connection: Connection, programId?: PublicKey);
141
+ /**
142
+ * Initialize a new airdrop campaign
143
+ */
144
+ initCampaign(authority: Keypair, mint: PublicKey, config: CampaignConfig): Promise<{
145
+ signature: string;
146
+ campaignPDA: PublicKey;
147
+ escrowPDA: PublicKey;
148
+ }>;
149
+ /**
150
+ * Claim tokens with Merkle proof
151
+ */
152
+ claim(payer: Keypair, recipient: Keypair, campaignPDA: PublicKey, allocation: Allocation, proof: Uint8Array[], recipientATA: PublicKey, gateTokenAccount?: PublicKey): Promise<string>;
153
+ /**
154
+ * Reclaim remaining tokens after expiry
155
+ */
156
+ reclaim(authority: Keypair, campaignPDA: PublicKey, authorityATA: PublicKey): Promise<string>;
157
+ /**
158
+ * Check if recipient has already claimed
159
+ */
160
+ hasClaimed(campaignPDA: PublicKey, recipient: PublicKey): Promise<boolean>;
161
+ /**
162
+ * Get campaign state
163
+ */
164
+ getCampaign(campaignPDA: PublicKey): Promise<Campaign | null>;
165
+ }
166
+ /**
167
+ * Generate random nonce for allocation
168
+ */
169
+ declare function generateNonce(): Uint8Array;
170
+ /**
171
+ * Generate campaign ID
172
+ */
173
+ declare function generateCampaignId(): Uint8Array;
174
+
175
+ export { type Allocation, BUBBLEGUM_PROGRAM_ID, type Campaign, type CampaignConfig, GateType, METADATA_PROGRAM_ID, type MerkleProof, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, WHISPERDROP_DEVNET_PROGRAM_ID, WHISPERDROP_PROGRAM_ID, WhisperDrop, buildMerkleTree, computeLeafHash, computeNodeHash, WhisperDrop as default, deriveCampaignPDA, deriveEscrowPDA, deriveNullifierPDA, generateCampaignId, generateNonce, getTokenProgramForGate, isCnftGate, requiresMinBalance, verifyMerkleProof };
package/dist/index.js ADDED
@@ -0,0 +1,372 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BUBBLEGUM_PROGRAM_ID: () => BUBBLEGUM_PROGRAM_ID,
24
+ GateType: () => GateType,
25
+ METADATA_PROGRAM_ID: () => METADATA_PROGRAM_ID,
26
+ TOKEN_2022_PROGRAM_ID: () => TOKEN_2022_PROGRAM_ID,
27
+ TOKEN_PROGRAM_ID: () => TOKEN_PROGRAM_ID,
28
+ WHISPERDROP_DEVNET_PROGRAM_ID: () => WHISPERDROP_DEVNET_PROGRAM_ID,
29
+ WHISPERDROP_PROGRAM_ID: () => WHISPERDROP_PROGRAM_ID,
30
+ WhisperDrop: () => WhisperDrop,
31
+ buildMerkleTree: () => buildMerkleTree,
32
+ computeLeafHash: () => computeLeafHash,
33
+ computeNodeHash: () => computeNodeHash,
34
+ default: () => index_default,
35
+ deriveCampaignPDA: () => deriveCampaignPDA,
36
+ deriveEscrowPDA: () => deriveEscrowPDA,
37
+ deriveNullifierPDA: () => deriveNullifierPDA,
38
+ generateCampaignId: () => generateCampaignId,
39
+ generateNonce: () => generateNonce,
40
+ getTokenProgramForGate: () => getTokenProgramForGate,
41
+ isCnftGate: () => isCnftGate,
42
+ requiresMinBalance: () => requiresMinBalance,
43
+ verifyMerkleProof: () => verifyMerkleProof
44
+ });
45
+ module.exports = __toCommonJS(index_exports);
46
+ var import_web3 = require("@solana/web3.js");
47
+ var import_sha256 = require("@noble/hashes/sha256");
48
+ var WHISPERDROP_PROGRAM_ID = new import_web3.PublicKey("GhstFNnEbixAGQgLnWg1nWetJQgGfSUMhnxdBA6hWu5e");
49
+ var WHISPERDROP_DEVNET_PROGRAM_ID = new import_web3.PublicKey("BPM5VuX9YrG7CgueWGxtqQZBQwMTacc315ppWCtTCJ5q");
50
+ var TOKEN_PROGRAM_ID = new import_web3.PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
51
+ var TOKEN_2022_PROGRAM_ID = new import_web3.PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
52
+ var METADATA_PROGRAM_ID = new import_web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
53
+ var BUBBLEGUM_PROGRAM_ID = new import_web3.PublicKey("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY");
54
+ var SEED_CAMPAIGN = Buffer.from("wd_campaign");
55
+ var SEED_ESCROW = Buffer.from("wd_escrow");
56
+ var SEED_NULLIFIER = Buffer.from("wd_null");
57
+ var LEAF_DOMAIN = Buffer.from("whisperdrop:leaf:v1");
58
+ var NODE_DOMAIN = Buffer.from("whisperdrop:node:v1");
59
+ var TAG_INIT_CAMPAIGN = 0;
60
+ var TAG_CLAIM = 1;
61
+ var TAG_RECLAIM = 2;
62
+ var GateType = /* @__PURE__ */ ((GateType2) => {
63
+ GateType2[GateType2["None"] = 0] = "None";
64
+ GateType2[GateType2["SplTokenHolder"] = 1] = "SplTokenHolder";
65
+ GateType2[GateType2["SplMinBalance"] = 2] = "SplMinBalance";
66
+ GateType2[GateType2["Token22Holder"] = 3] = "Token22Holder";
67
+ GateType2[GateType2["Token22MinBalance"] = 4] = "Token22MinBalance";
68
+ GateType2[GateType2["NftHolder"] = 5] = "NftHolder";
69
+ GateType2[GateType2["NftCollection"] = 6] = "NftCollection";
70
+ GateType2[GateType2["CnftHolder"] = 7] = "CnftHolder";
71
+ GateType2[GateType2["CnftCollection"] = 8] = "CnftCollection";
72
+ return GateType2;
73
+ })(GateType || {});
74
+ function getTokenProgramForGate(gateType) {
75
+ switch (gateType) {
76
+ case 1 /* SplTokenHolder */:
77
+ case 2 /* SplMinBalance */:
78
+ case 5 /* NftHolder */:
79
+ case 6 /* NftCollection */:
80
+ return TOKEN_PROGRAM_ID;
81
+ case 3 /* Token22Holder */:
82
+ case 4 /* Token22MinBalance */:
83
+ return TOKEN_2022_PROGRAM_ID;
84
+ case 7 /* CnftHolder */:
85
+ case 8 /* CnftCollection */:
86
+ return null;
87
+ default:
88
+ return null;
89
+ }
90
+ }
91
+ function requiresMinBalance(gateType) {
92
+ return gateType === 2 /* SplMinBalance */ || gateType === 4 /* Token22MinBalance */;
93
+ }
94
+ function isCnftGate(gateType) {
95
+ return gateType === 7 /* CnftHolder */ || gateType === 8 /* CnftCollection */;
96
+ }
97
+ function computeLeafHash(campaignId, recipient, amount, nonce) {
98
+ const amountBytes = Buffer.alloc(8);
99
+ amountBytes.writeBigUInt64LE(amount);
100
+ const data = Buffer.concat([
101
+ LEAF_DOMAIN,
102
+ Buffer.from(campaignId),
103
+ recipient.toBytes(),
104
+ amountBytes,
105
+ Buffer.from(nonce)
106
+ ]);
107
+ return (0, import_sha256.sha256)(data);
108
+ }
109
+ function computeNodeHash(left, right) {
110
+ const [first, second] = Buffer.compare(Buffer.from(left), Buffer.from(right)) <= 0 ? [left, right] : [right, left];
111
+ return (0, import_sha256.sha256)(Buffer.concat([
112
+ NODE_DOMAIN,
113
+ Buffer.from(first),
114
+ Buffer.from(second)
115
+ ]));
116
+ }
117
+ function buildMerkleTree(campaignId, allocations) {
118
+ if (allocations.length === 0) {
119
+ throw new Error("Cannot build tree with no allocations");
120
+ }
121
+ const leaves = allocations.map(
122
+ (a) => computeLeafHash(campaignId, a.recipient, a.amount, a.nonce)
123
+ );
124
+ let currentLevel = leaves;
125
+ const tree = [currentLevel];
126
+ while (currentLevel.length > 1) {
127
+ const nextLevel = [];
128
+ for (let i = 0; i < currentLevel.length; i += 2) {
129
+ const left = currentLevel[i];
130
+ const right = currentLevel[i + 1] || left;
131
+ nextLevel.push(computeNodeHash(left, right));
132
+ }
133
+ tree.push(nextLevel);
134
+ currentLevel = nextLevel;
135
+ }
136
+ const root = tree[tree.length - 1][0];
137
+ const proofs = /* @__PURE__ */ new Map();
138
+ for (let leafIndex = 0; leafIndex < allocations.length; leafIndex++) {
139
+ const proof = [];
140
+ let index = leafIndex;
141
+ for (let level = 0; level < tree.length - 1; level++) {
142
+ const isLeft = index % 2 === 0;
143
+ const siblingIndex = isLeft ? index + 1 : index - 1;
144
+ if (siblingIndex < tree[level].length) {
145
+ proof.push(tree[level][siblingIndex]);
146
+ } else {
147
+ proof.push(tree[level][index]);
148
+ }
149
+ index = Math.floor(index / 2);
150
+ }
151
+ proofs.set(allocations[leafIndex].recipient.toBase58(), proof);
152
+ }
153
+ return { root, proofs };
154
+ }
155
+ function verifyMerkleProof(campaignId, allocation, proof, root) {
156
+ let current = computeLeafHash(
157
+ campaignId,
158
+ allocation.recipient,
159
+ allocation.amount,
160
+ allocation.nonce
161
+ );
162
+ for (const sibling of proof) {
163
+ current = computeNodeHash(current, sibling);
164
+ }
165
+ return Buffer.from(current).equals(Buffer.from(root));
166
+ }
167
+ function deriveCampaignPDA(campaignId, programId = WHISPERDROP_PROGRAM_ID) {
168
+ return import_web3.PublicKey.findProgramAddressSync(
169
+ [SEED_CAMPAIGN, Buffer.from(campaignId)],
170
+ programId
171
+ );
172
+ }
173
+ function deriveEscrowPDA(campaignPDA, programId = WHISPERDROP_PROGRAM_ID) {
174
+ return import_web3.PublicKey.findProgramAddressSync(
175
+ [SEED_ESCROW, campaignPDA.toBytes()],
176
+ programId
177
+ );
178
+ }
179
+ function deriveNullifierPDA(campaignPDA, recipient, programId = WHISPERDROP_PROGRAM_ID) {
180
+ return import_web3.PublicKey.findProgramAddressSync(
181
+ [SEED_NULLIFIER, campaignPDA.toBytes(), recipient.toBytes()],
182
+ programId
183
+ );
184
+ }
185
+ var WhisperDrop = class {
186
+ constructor(connection, programId) {
187
+ this.connection = connection;
188
+ this.programId = programId || WHISPERDROP_PROGRAM_ID;
189
+ }
190
+ /**
191
+ * Initialize a new airdrop campaign
192
+ */
193
+ async initCampaign(authority, mint, config) {
194
+ const [campaignPDA] = deriveCampaignPDA(config.campaignId, this.programId);
195
+ const [escrowPDA] = deriveEscrowPDA(campaignPDA, this.programId);
196
+ const expiryBuf = Buffer.alloc(8);
197
+ expiryBuf.writeBigInt64LE(config.expiryUnix);
198
+ const gateAmountBuf = Buffer.alloc(8);
199
+ gateAmountBuf.writeBigUInt64LE(config.gateAmount || 0n);
200
+ const data = Buffer.concat([
201
+ Buffer.from([TAG_INIT_CAMPAIGN]),
202
+ Buffer.from(config.campaignId),
203
+ Buffer.from(config.merkleRoot),
204
+ expiryBuf,
205
+ Buffer.from([config.gateType || 0 /* None */]),
206
+ (config.gateMint || import_web3.PublicKey.default).toBytes(),
207
+ gateAmountBuf
208
+ ]);
209
+ const instruction = new import_web3.TransactionInstruction({
210
+ programId: this.programId,
211
+ keys: [
212
+ { pubkey: authority.publicKey, isSigner: true, isWritable: true },
213
+ { pubkey: campaignPDA, isSigner: false, isWritable: true },
214
+ { pubkey: escrowPDA, isSigner: false, isWritable: true },
215
+ { pubkey: mint, isSigner: false, isWritable: false },
216
+ { pubkey: import_web3.SystemProgram.programId, isSigner: false, isWritable: false },
217
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
218
+ { pubkey: import_web3.SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }
219
+ ],
220
+ data
221
+ });
222
+ const tx = new import_web3.Transaction().add(instruction);
223
+ const signature = await (0, import_web3.sendAndConfirmTransaction)(this.connection, tx, [authority]);
224
+ return { signature, campaignPDA, escrowPDA };
225
+ }
226
+ /**
227
+ * Claim tokens with Merkle proof
228
+ */
229
+ async claim(payer, recipient, campaignPDA, allocation, proof, recipientATA, gateTokenAccount) {
230
+ const [escrowPDA] = deriveEscrowPDA(campaignPDA, this.programId);
231
+ const [nullifierPDA] = deriveNullifierPDA(campaignPDA, recipient.publicKey, this.programId);
232
+ const amountBuf = Buffer.alloc(8);
233
+ amountBuf.writeBigUInt64LE(allocation.amount);
234
+ const proofData = Buffer.concat(proof.map((p) => Buffer.from(p)));
235
+ const data = Buffer.concat([
236
+ Buffer.from([TAG_CLAIM]),
237
+ amountBuf,
238
+ Buffer.from(allocation.nonce),
239
+ Buffer.from([proof.length]),
240
+ proofData
241
+ ]);
242
+ const keys = [
243
+ { pubkey: payer.publicKey, isSigner: true, isWritable: true },
244
+ { pubkey: recipient.publicKey, isSigner: true, isWritable: false },
245
+ { pubkey: campaignPDA, isSigner: false, isWritable: false },
246
+ { pubkey: escrowPDA, isSigner: false, isWritable: true },
247
+ { pubkey: recipientATA, isSigner: false, isWritable: true },
248
+ { pubkey: nullifierPDA, isSigner: false, isWritable: true },
249
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
250
+ { pubkey: import_web3.SystemProgram.programId, isSigner: false, isWritable: false }
251
+ ];
252
+ if (gateTokenAccount) {
253
+ keys.push({ pubkey: gateTokenAccount, isSigner: false, isWritable: false });
254
+ }
255
+ const instruction = new import_web3.TransactionInstruction({
256
+ programId: this.programId,
257
+ keys,
258
+ data
259
+ });
260
+ const tx = new import_web3.Transaction().add(instruction);
261
+ const signers = payer.publicKey.equals(recipient.publicKey) ? [payer] : [payer, recipient];
262
+ return await (0, import_web3.sendAndConfirmTransaction)(this.connection, tx, signers);
263
+ }
264
+ /**
265
+ * Reclaim remaining tokens after expiry
266
+ */
267
+ async reclaim(authority, campaignPDA, authorityATA) {
268
+ const [escrowPDA] = deriveEscrowPDA(campaignPDA, this.programId);
269
+ const data = Buffer.from([TAG_RECLAIM]);
270
+ const instruction = new import_web3.TransactionInstruction({
271
+ programId: this.programId,
272
+ keys: [
273
+ { pubkey: authority.publicKey, isSigner: true, isWritable: false },
274
+ { pubkey: campaignPDA, isSigner: false, isWritable: false },
275
+ { pubkey: escrowPDA, isSigner: false, isWritable: true },
276
+ { pubkey: authorityATA, isSigner: false, isWritable: true },
277
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }
278
+ ],
279
+ data
280
+ });
281
+ const tx = new import_web3.Transaction().add(instruction);
282
+ return await (0, import_web3.sendAndConfirmTransaction)(this.connection, tx, [authority]);
283
+ }
284
+ /**
285
+ * Check if recipient has already claimed
286
+ */
287
+ async hasClaimed(campaignPDA, recipient) {
288
+ const [nullifierPDA] = deriveNullifierPDA(campaignPDA, recipient, this.programId);
289
+ const account = await this.connection.getAccountInfo(nullifierPDA);
290
+ return account !== null && account.data.length > 0;
291
+ }
292
+ /**
293
+ * Get campaign state
294
+ */
295
+ async getCampaign(campaignPDA) {
296
+ const account = await this.connection.getAccountInfo(campaignPDA);
297
+ if (!account) return null;
298
+ const data = account.data;
299
+ if (data.length < 180 || data[0] !== 1) return null;
300
+ let offset = 1;
301
+ const authority = new import_web3.PublicKey(data.slice(offset, offset + 32));
302
+ offset += 32;
303
+ const mint = new import_web3.PublicKey(data.slice(offset, offset + 32));
304
+ offset += 32;
305
+ const campaignId = data.slice(offset, offset + 32);
306
+ offset += 32;
307
+ const merkleRoot = data.slice(offset, offset + 32);
308
+ offset += 32;
309
+ const expiryUnix = data.readBigInt64LE(offset);
310
+ offset += 8;
311
+ offset += 2;
312
+ const gateType = data[offset];
313
+ offset += 1;
314
+ const gateMint = new import_web3.PublicKey(data.slice(offset, offset + 32));
315
+ offset += 32;
316
+ const gateAmount = data.readBigUInt64LE(offset);
317
+ return {
318
+ authority,
319
+ mint,
320
+ campaignId,
321
+ merkleRoot,
322
+ expiryUnix,
323
+ gateType,
324
+ gateMint,
325
+ gateAmount
326
+ };
327
+ }
328
+ };
329
+ function generateNonce() {
330
+ return crypto.getRandomValues(new Uint8Array(16));
331
+ }
332
+ function generateCampaignId() {
333
+ return crypto.getRandomValues(new Uint8Array(32));
334
+ }
335
+ var index_default = WhisperDrop;
336
+ // Annotate the CommonJS export names for ESM import in node:
337
+ 0 && (module.exports = {
338
+ BUBBLEGUM_PROGRAM_ID,
339
+ GateType,
340
+ METADATA_PROGRAM_ID,
341
+ TOKEN_2022_PROGRAM_ID,
342
+ TOKEN_PROGRAM_ID,
343
+ WHISPERDROP_DEVNET_PROGRAM_ID,
344
+ WHISPERDROP_PROGRAM_ID,
345
+ WhisperDrop,
346
+ buildMerkleTree,
347
+ computeLeafHash,
348
+ computeNodeHash,
349
+ deriveCampaignPDA,
350
+ deriveEscrowPDA,
351
+ deriveNullifierPDA,
352
+ generateCampaignId,
353
+ generateNonce,
354
+ getTokenProgramForGate,
355
+ isCnftGate,
356
+ requiresMinBalance,
357
+ verifyMerkleProof
358
+ });
359
+ /**
360
+ * @styxstack/whisperdrop-sdk
361
+ *
362
+ * TypeScript SDK for WhisperDrop - Privacy-Preserving Airdrops on Solana
363
+ *
364
+ * Features:
365
+ * - Merkle tree generation and proof creation
366
+ * - Token-gated claiming (NFT, token balance)
367
+ * - Campaign creation and management
368
+ * - Claim and reclaim operations
369
+ *
370
+ * @author @moonmanquark (Bluefoot Labs)
371
+ * @license Apache-2.0
372
+ */
package/dist/index.mjs ADDED
@@ -0,0 +1,335 @@
1
+ // src/index.ts
2
+ import {
3
+ PublicKey,
4
+ Transaction,
5
+ TransactionInstruction,
6
+ SystemProgram,
7
+ SYSVAR_RENT_PUBKEY,
8
+ sendAndConfirmTransaction
9
+ } from "@solana/web3.js";
10
+ import { sha256 } from "@noble/hashes/sha256";
11
+ var WHISPERDROP_PROGRAM_ID = new PublicKey("GhstFNnEbixAGQgLnWg1nWetJQgGfSUMhnxdBA6hWu5e");
12
+ var WHISPERDROP_DEVNET_PROGRAM_ID = new PublicKey("BPM5VuX9YrG7CgueWGxtqQZBQwMTacc315ppWCtTCJ5q");
13
+ var TOKEN_PROGRAM_ID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
14
+ var TOKEN_2022_PROGRAM_ID = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
15
+ var METADATA_PROGRAM_ID = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
16
+ var BUBBLEGUM_PROGRAM_ID = new PublicKey("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY");
17
+ var SEED_CAMPAIGN = Buffer.from("wd_campaign");
18
+ var SEED_ESCROW = Buffer.from("wd_escrow");
19
+ var SEED_NULLIFIER = Buffer.from("wd_null");
20
+ var LEAF_DOMAIN = Buffer.from("whisperdrop:leaf:v1");
21
+ var NODE_DOMAIN = Buffer.from("whisperdrop:node:v1");
22
+ var TAG_INIT_CAMPAIGN = 0;
23
+ var TAG_CLAIM = 1;
24
+ var TAG_RECLAIM = 2;
25
+ var GateType = /* @__PURE__ */ ((GateType2) => {
26
+ GateType2[GateType2["None"] = 0] = "None";
27
+ GateType2[GateType2["SplTokenHolder"] = 1] = "SplTokenHolder";
28
+ GateType2[GateType2["SplMinBalance"] = 2] = "SplMinBalance";
29
+ GateType2[GateType2["Token22Holder"] = 3] = "Token22Holder";
30
+ GateType2[GateType2["Token22MinBalance"] = 4] = "Token22MinBalance";
31
+ GateType2[GateType2["NftHolder"] = 5] = "NftHolder";
32
+ GateType2[GateType2["NftCollection"] = 6] = "NftCollection";
33
+ GateType2[GateType2["CnftHolder"] = 7] = "CnftHolder";
34
+ GateType2[GateType2["CnftCollection"] = 8] = "CnftCollection";
35
+ return GateType2;
36
+ })(GateType || {});
37
+ function getTokenProgramForGate(gateType) {
38
+ switch (gateType) {
39
+ case 1 /* SplTokenHolder */:
40
+ case 2 /* SplMinBalance */:
41
+ case 5 /* NftHolder */:
42
+ case 6 /* NftCollection */:
43
+ return TOKEN_PROGRAM_ID;
44
+ case 3 /* Token22Holder */:
45
+ case 4 /* Token22MinBalance */:
46
+ return TOKEN_2022_PROGRAM_ID;
47
+ case 7 /* CnftHolder */:
48
+ case 8 /* CnftCollection */:
49
+ return null;
50
+ default:
51
+ return null;
52
+ }
53
+ }
54
+ function requiresMinBalance(gateType) {
55
+ return gateType === 2 /* SplMinBalance */ || gateType === 4 /* Token22MinBalance */;
56
+ }
57
+ function isCnftGate(gateType) {
58
+ return gateType === 7 /* CnftHolder */ || gateType === 8 /* CnftCollection */;
59
+ }
60
+ function computeLeafHash(campaignId, recipient, amount, nonce) {
61
+ const amountBytes = Buffer.alloc(8);
62
+ amountBytes.writeBigUInt64LE(amount);
63
+ const data = Buffer.concat([
64
+ LEAF_DOMAIN,
65
+ Buffer.from(campaignId),
66
+ recipient.toBytes(),
67
+ amountBytes,
68
+ Buffer.from(nonce)
69
+ ]);
70
+ return sha256(data);
71
+ }
72
+ function computeNodeHash(left, right) {
73
+ const [first, second] = Buffer.compare(Buffer.from(left), Buffer.from(right)) <= 0 ? [left, right] : [right, left];
74
+ return sha256(Buffer.concat([
75
+ NODE_DOMAIN,
76
+ Buffer.from(first),
77
+ Buffer.from(second)
78
+ ]));
79
+ }
80
+ function buildMerkleTree(campaignId, allocations) {
81
+ if (allocations.length === 0) {
82
+ throw new Error("Cannot build tree with no allocations");
83
+ }
84
+ const leaves = allocations.map(
85
+ (a) => computeLeafHash(campaignId, a.recipient, a.amount, a.nonce)
86
+ );
87
+ let currentLevel = leaves;
88
+ const tree = [currentLevel];
89
+ while (currentLevel.length > 1) {
90
+ const nextLevel = [];
91
+ for (let i = 0; i < currentLevel.length; i += 2) {
92
+ const left = currentLevel[i];
93
+ const right = currentLevel[i + 1] || left;
94
+ nextLevel.push(computeNodeHash(left, right));
95
+ }
96
+ tree.push(nextLevel);
97
+ currentLevel = nextLevel;
98
+ }
99
+ const root = tree[tree.length - 1][0];
100
+ const proofs = /* @__PURE__ */ new Map();
101
+ for (let leafIndex = 0; leafIndex < allocations.length; leafIndex++) {
102
+ const proof = [];
103
+ let index = leafIndex;
104
+ for (let level = 0; level < tree.length - 1; level++) {
105
+ const isLeft = index % 2 === 0;
106
+ const siblingIndex = isLeft ? index + 1 : index - 1;
107
+ if (siblingIndex < tree[level].length) {
108
+ proof.push(tree[level][siblingIndex]);
109
+ } else {
110
+ proof.push(tree[level][index]);
111
+ }
112
+ index = Math.floor(index / 2);
113
+ }
114
+ proofs.set(allocations[leafIndex].recipient.toBase58(), proof);
115
+ }
116
+ return { root, proofs };
117
+ }
118
+ function verifyMerkleProof(campaignId, allocation, proof, root) {
119
+ let current = computeLeafHash(
120
+ campaignId,
121
+ allocation.recipient,
122
+ allocation.amount,
123
+ allocation.nonce
124
+ );
125
+ for (const sibling of proof) {
126
+ current = computeNodeHash(current, sibling);
127
+ }
128
+ return Buffer.from(current).equals(Buffer.from(root));
129
+ }
130
+ function deriveCampaignPDA(campaignId, programId = WHISPERDROP_PROGRAM_ID) {
131
+ return PublicKey.findProgramAddressSync(
132
+ [SEED_CAMPAIGN, Buffer.from(campaignId)],
133
+ programId
134
+ );
135
+ }
136
+ function deriveEscrowPDA(campaignPDA, programId = WHISPERDROP_PROGRAM_ID) {
137
+ return PublicKey.findProgramAddressSync(
138
+ [SEED_ESCROW, campaignPDA.toBytes()],
139
+ programId
140
+ );
141
+ }
142
+ function deriveNullifierPDA(campaignPDA, recipient, programId = WHISPERDROP_PROGRAM_ID) {
143
+ return PublicKey.findProgramAddressSync(
144
+ [SEED_NULLIFIER, campaignPDA.toBytes(), recipient.toBytes()],
145
+ programId
146
+ );
147
+ }
148
+ var WhisperDrop = class {
149
+ constructor(connection, programId) {
150
+ this.connection = connection;
151
+ this.programId = programId || WHISPERDROP_PROGRAM_ID;
152
+ }
153
+ /**
154
+ * Initialize a new airdrop campaign
155
+ */
156
+ async initCampaign(authority, mint, config) {
157
+ const [campaignPDA] = deriveCampaignPDA(config.campaignId, this.programId);
158
+ const [escrowPDA] = deriveEscrowPDA(campaignPDA, this.programId);
159
+ const expiryBuf = Buffer.alloc(8);
160
+ expiryBuf.writeBigInt64LE(config.expiryUnix);
161
+ const gateAmountBuf = Buffer.alloc(8);
162
+ gateAmountBuf.writeBigUInt64LE(config.gateAmount || 0n);
163
+ const data = Buffer.concat([
164
+ Buffer.from([TAG_INIT_CAMPAIGN]),
165
+ Buffer.from(config.campaignId),
166
+ Buffer.from(config.merkleRoot),
167
+ expiryBuf,
168
+ Buffer.from([config.gateType || 0 /* None */]),
169
+ (config.gateMint || PublicKey.default).toBytes(),
170
+ gateAmountBuf
171
+ ]);
172
+ const instruction = new TransactionInstruction({
173
+ programId: this.programId,
174
+ keys: [
175
+ { pubkey: authority.publicKey, isSigner: true, isWritable: true },
176
+ { pubkey: campaignPDA, isSigner: false, isWritable: true },
177
+ { pubkey: escrowPDA, isSigner: false, isWritable: true },
178
+ { pubkey: mint, isSigner: false, isWritable: false },
179
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
180
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
181
+ { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }
182
+ ],
183
+ data
184
+ });
185
+ const tx = new Transaction().add(instruction);
186
+ const signature = await sendAndConfirmTransaction(this.connection, tx, [authority]);
187
+ return { signature, campaignPDA, escrowPDA };
188
+ }
189
+ /**
190
+ * Claim tokens with Merkle proof
191
+ */
192
+ async claim(payer, recipient, campaignPDA, allocation, proof, recipientATA, gateTokenAccount) {
193
+ const [escrowPDA] = deriveEscrowPDA(campaignPDA, this.programId);
194
+ const [nullifierPDA] = deriveNullifierPDA(campaignPDA, recipient.publicKey, this.programId);
195
+ const amountBuf = Buffer.alloc(8);
196
+ amountBuf.writeBigUInt64LE(allocation.amount);
197
+ const proofData = Buffer.concat(proof.map((p) => Buffer.from(p)));
198
+ const data = Buffer.concat([
199
+ Buffer.from([TAG_CLAIM]),
200
+ amountBuf,
201
+ Buffer.from(allocation.nonce),
202
+ Buffer.from([proof.length]),
203
+ proofData
204
+ ]);
205
+ const keys = [
206
+ { pubkey: payer.publicKey, isSigner: true, isWritable: true },
207
+ { pubkey: recipient.publicKey, isSigner: true, isWritable: false },
208
+ { pubkey: campaignPDA, isSigner: false, isWritable: false },
209
+ { pubkey: escrowPDA, isSigner: false, isWritable: true },
210
+ { pubkey: recipientATA, isSigner: false, isWritable: true },
211
+ { pubkey: nullifierPDA, isSigner: false, isWritable: true },
212
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
213
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
214
+ ];
215
+ if (gateTokenAccount) {
216
+ keys.push({ pubkey: gateTokenAccount, isSigner: false, isWritable: false });
217
+ }
218
+ const instruction = new TransactionInstruction({
219
+ programId: this.programId,
220
+ keys,
221
+ data
222
+ });
223
+ const tx = new Transaction().add(instruction);
224
+ const signers = payer.publicKey.equals(recipient.publicKey) ? [payer] : [payer, recipient];
225
+ return await sendAndConfirmTransaction(this.connection, tx, signers);
226
+ }
227
+ /**
228
+ * Reclaim remaining tokens after expiry
229
+ */
230
+ async reclaim(authority, campaignPDA, authorityATA) {
231
+ const [escrowPDA] = deriveEscrowPDA(campaignPDA, this.programId);
232
+ const data = Buffer.from([TAG_RECLAIM]);
233
+ const instruction = new TransactionInstruction({
234
+ programId: this.programId,
235
+ keys: [
236
+ { pubkey: authority.publicKey, isSigner: true, isWritable: false },
237
+ { pubkey: campaignPDA, isSigner: false, isWritable: false },
238
+ { pubkey: escrowPDA, isSigner: false, isWritable: true },
239
+ { pubkey: authorityATA, isSigner: false, isWritable: true },
240
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }
241
+ ],
242
+ data
243
+ });
244
+ const tx = new Transaction().add(instruction);
245
+ return await sendAndConfirmTransaction(this.connection, tx, [authority]);
246
+ }
247
+ /**
248
+ * Check if recipient has already claimed
249
+ */
250
+ async hasClaimed(campaignPDA, recipient) {
251
+ const [nullifierPDA] = deriveNullifierPDA(campaignPDA, recipient, this.programId);
252
+ const account = await this.connection.getAccountInfo(nullifierPDA);
253
+ return account !== null && account.data.length > 0;
254
+ }
255
+ /**
256
+ * Get campaign state
257
+ */
258
+ async getCampaign(campaignPDA) {
259
+ const account = await this.connection.getAccountInfo(campaignPDA);
260
+ if (!account) return null;
261
+ const data = account.data;
262
+ if (data.length < 180 || data[0] !== 1) return null;
263
+ let offset = 1;
264
+ const authority = new PublicKey(data.slice(offset, offset + 32));
265
+ offset += 32;
266
+ const mint = new PublicKey(data.slice(offset, offset + 32));
267
+ offset += 32;
268
+ const campaignId = data.slice(offset, offset + 32);
269
+ offset += 32;
270
+ const merkleRoot = data.slice(offset, offset + 32);
271
+ offset += 32;
272
+ const expiryUnix = data.readBigInt64LE(offset);
273
+ offset += 8;
274
+ offset += 2;
275
+ const gateType = data[offset];
276
+ offset += 1;
277
+ const gateMint = new PublicKey(data.slice(offset, offset + 32));
278
+ offset += 32;
279
+ const gateAmount = data.readBigUInt64LE(offset);
280
+ return {
281
+ authority,
282
+ mint,
283
+ campaignId,
284
+ merkleRoot,
285
+ expiryUnix,
286
+ gateType,
287
+ gateMint,
288
+ gateAmount
289
+ };
290
+ }
291
+ };
292
+ function generateNonce() {
293
+ return crypto.getRandomValues(new Uint8Array(16));
294
+ }
295
+ function generateCampaignId() {
296
+ return crypto.getRandomValues(new Uint8Array(32));
297
+ }
298
+ var index_default = WhisperDrop;
299
+ export {
300
+ BUBBLEGUM_PROGRAM_ID,
301
+ GateType,
302
+ METADATA_PROGRAM_ID,
303
+ TOKEN_2022_PROGRAM_ID,
304
+ TOKEN_PROGRAM_ID,
305
+ WHISPERDROP_DEVNET_PROGRAM_ID,
306
+ WHISPERDROP_PROGRAM_ID,
307
+ WhisperDrop,
308
+ buildMerkleTree,
309
+ computeLeafHash,
310
+ computeNodeHash,
311
+ index_default as default,
312
+ deriveCampaignPDA,
313
+ deriveEscrowPDA,
314
+ deriveNullifierPDA,
315
+ generateCampaignId,
316
+ generateNonce,
317
+ getTokenProgramForGate,
318
+ isCnftGate,
319
+ requiresMinBalance,
320
+ verifyMerkleProof
321
+ };
322
+ /**
323
+ * @styxstack/whisperdrop-sdk
324
+ *
325
+ * TypeScript SDK for WhisperDrop - Privacy-Preserving Airdrops on Solana
326
+ *
327
+ * Features:
328
+ * - Merkle tree generation and proof creation
329
+ * - Token-gated claiming (NFT, token balance)
330
+ * - Campaign creation and management
331
+ * - Claim and reclaim operations
332
+ *
333
+ * @author @moonmanquark (Bluefoot Labs)
334
+ * @license Apache-2.0
335
+ */
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@styxstack/whisperdrop-sdk",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript SDK for WhisperDrop - Privacy-Preserving Airdrops on Solana",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "solana",
22
+ "airdrop",
23
+ "privacy",
24
+ "merkle",
25
+ "token-gating",
26
+ "nft",
27
+ "web3",
28
+ "whisperdrop"
29
+ ],
30
+ "author": "@moonmanquark <Bluefoot Labs>",
31
+ "license": "Apache-2.0",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/QuarksBlueFoot/StyxStack.git",
35
+ "directory": "packages/whisperdrop-sdk"
36
+ },
37
+ "homepage": "https://github.com/QuarksBlueFoot/StyxStack/tree/main/packages/whisperdrop-sdk#readme",
38
+ "peerDependencies": {
39
+ "@solana/web3.js": "^1.87.0 || ^2.0.0"
40
+ },
41
+ "dependencies": {
42
+ "@noble/hashes": "^1.5.0"
43
+ },
44
+ "devDependencies": {
45
+ "@solana/web3.js": "^1.98.0",
46
+ "@solana/spl-token": "^0.4.0",
47
+ "@types/node": "^20.0.0",
48
+ "tsup": "^8.0.0",
49
+ "typescript": "^5.3.0",
50
+ "vitest": "^2.0.0"
51
+ },
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup src/index.ts --format cjs,esm --dts",
57
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "vitest run"
60
+ }
61
+ }