@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 +17 -0
- package/README.md +199 -0
- package/dist/index.d.mts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +372 -0
- package/dist/index.mjs +335 -0
- package/package.json +61 -0
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
|
+
[](https://www.npmjs.com/package/@styxstack/whisperdrop-sdk)
|
|
6
|
+
[](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**
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|