solotto 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -19,6 +19,8 @@ A JavaScript SDK for interacting with the Solotto on-chain lottery program on So
19
19
  - [RandomDraw](#randomdraw)
20
20
  - [LockLottery](#locklottery)
21
21
  - [ClaimExpired](#claimexpired)
22
+ - [Boost](#boost)
23
+ - [GetBoosters](#getboosters)
22
24
  - [Lottery](#lottery-api)
23
25
  - [BuyTickets](#buytickets)
24
26
  - [ClaimTicket](#claimticket)
@@ -213,7 +215,89 @@ const result = await manager.ClaimExpired(authority, lotteryId, encoded);
213
215
  | `lotteryId` | `String` | — | The lottery ID. |
214
216
  | `encoded` | `Boolean` | `false` | If `true`, returns encoded transaction. |
215
217
 
216
- **Returns:** Updated lottery state object on finalization, or the transaction object when encoded.
218
+ **Returns:** Updated lottery state object on finalization, `"Prize has already been claimed"` if the prize was already claimed, or the transaction object when encoded.
219
+
220
+ ---
221
+
222
+ #### Boost
223
+
224
+ Boosts a lottery's prize pool by transferring SOL from any wallet. Can be called by anyone, not just the authority. Optionally attaches a memo message to the transaction.
225
+
226
+ ```js
227
+ // Boost lottery #1 with 0.5 SOL
228
+ const result = await manager.Boost(authority, lotteryId, booster, 0.5);
229
+
230
+ // Boost with a memo message
231
+ const result = await manager.Boost(authority, lotteryId, booster, 1.0, "Good luck everyone!");
232
+ ```
233
+
234
+ | Parameter | Type | Default | Description |
235
+ |---|---|---|---|
236
+ | `authority` | `{publicKey}` | — | The lottery authority (only `publicKey` is needed). |
237
+ | `lotteryId` | `String` | — | The lottery ID. |
238
+ | `booster` | `Keypair` | — | The keypair of the wallet sending the boost. |
239
+ | `amount` | `Number` | — | Amount of SOL to boost (e.g. `0.5` for 0.5 SOL). |
240
+ | `message` | `String \| false` | `false` | Optional memo string attached to the transaction. |
241
+ | `encoded` | `Boolean` | `false` | If `true`, returns encoded transaction. |
242
+
243
+ **Returns:** `"boosted"` on success, `"Draw initiated, cannot boost this prize pool"` if the draw has already started, or the transaction object when encoded.
244
+
245
+ > **Note:** When a `message` is provided, the SDK automatically prepends a structured memo in the format `:booster:authority,lotteryId,booster,amount:booster:` before your message. This structured prefix is used by `GetBoosters` to index boost history from on-chain transaction memos.
246
+
247
+ ---
248
+
249
+ #### GetBoosters
250
+
251
+ Retrieves boost history by scanning on-chain transaction memos. Can filter by authority, lottery ID, or both, and optionally group results by booster wallet address.
252
+
253
+ ```js
254
+ // Get all boosters for a specific lottery
255
+ const boosters = await manager.GetBoosters(authority, lotteryId);
256
+
257
+ // Get all boosters across all lotteries (up to 500 transactions)
258
+ const allBoosters = await manager.GetBoosters(false, false, false, 500);
259
+
260
+ // Get boosters grouped by wallet address
261
+ const grouped = await manager.GetBoosters(authority, lotteryId, true);
262
+ ```
263
+
264
+ | Parameter | Type | Default | Description |
265
+ |---|---|---|---|
266
+ | `authority` | `{publicKey} \| false` | `false` | Filter by lottery authority. Pass `false` to include all authorities. |
267
+ | `lotteryId` | `Number \| false` | `false` | Filter by lottery ID. Pass `false` to include all lotteries. |
268
+ | `group` | `Boolean` | `false` | If `true`, groups results by booster wallet address. |
269
+ | `limit` | `Number` | `1000` | Maximum number of recent transactions to scan (max 1000). |
270
+
271
+ **Returns (ungrouped):** An array of booster objects:
272
+
273
+ ```js
274
+ [
275
+ {
276
+ lotteryId: 1,
277
+ authority: "Pubkey...",
278
+ booster: "Pubkey...",
279
+ amount: 0.5,
280
+ signature: "TxSignature...",
281
+ },
282
+ // ...
283
+ ]
284
+ ```
285
+
286
+ **Returns (grouped, `group = true`):** An object keyed by booster wallet address:
287
+
288
+ ```js
289
+ {
290
+ "BoosterPubkey...": {
291
+ boost: [
292
+ { lotteryId: 1, authority: "Pubkey...", booster: "Pubkey...", amount: 0.5, signature: "TxSig..." },
293
+ // ...
294
+ ],
295
+ total: 1.5, // Sum of all boost amounts in SOL
296
+ count: 3, // Number of boosts
297
+ },
298
+ // ...
299
+ }
300
+ ```
217
301
 
218
302
  ---
219
303
 
@@ -237,7 +321,7 @@ const result = await lottery.BuyTickets(buyer, authority, lotteryId, amount, enc
237
321
  | `amount` | `Number` | `1` | Number of tickets to purchase (1–4). |
238
322
  | `encoded` | `Boolean` | `false` | If `true`, returns encoded transaction. |
239
323
 
240
- **Returns:** `"finalized"` on success, or the transaction object when encoded.
324
+ **Returns:** `"finalized"` on success, `"Lottery is not active, no tickets can be sold"` if the lottery is inactive, or the transaction object when encoded.
241
325
 
242
326
  **Example:**
243
327
 
@@ -274,7 +358,7 @@ const result = await lottery.ClaimTicket(authority, lotteryId, winner, encoded);
274
358
  | `winner` | `Keypair` | — | The keypair of the winning ticket's owner. |
275
359
  | `encoded` | `Boolean` | `false` | If `true`, returns encoded transaction. |
276
360
 
277
- **Returns:** `"finalized"` on success, or the transaction object when encoded.
361
+ **Returns:** `"finalized"` on success, the simulation log array (`string[]`) if the transaction fails simulation, or the transaction object when encoded.
278
362
 
279
363
  ---
280
364
 
@@ -492,7 +576,7 @@ const status = await network.Status(signature, maxRetries, intervalSeconds);
492
576
 
493
577
  ## Transaction Modes
494
578
 
495
- Every write method (`Initialize`, `RandomDraw`, `LockLottery`, `BuyTickets`, `ClaimTicket`) supports two modes controlled by the `encoded` parameter:
579
+ Every write method (`Initialize`, `RandomDraw`, `LockLottery`, `ClaimExpired`, `Boost`, `BuyTickets`, `ClaimTicket`) supports two modes controlled by the `encoded` parameter:
496
580
 
497
581
  **Direct Mode** (`encoded = false`, default) — The SDK signs, sends, and confirms the transaction. Requires the keypair to have a `secretKey`. Returns the final status or lottery state.
498
582
 
package/SECURITY.md ADDED
@@ -0,0 +1,15 @@
1
+ # Security Policy
2
+
3
+ <a name="reporting"></a>
4
+ ## Reporting Security Problems
5
+
6
+ **DO NOT CREATE A GITHUB ISSUE** to report a security problem.
7
+
8
+ Please contact the maintainers:
9
+
10
+ X: @SolDapper
11
+
12
+ <a name="process"></a>
13
+ ## Incident Response Process
14
+
15
+ Maintainers will respond to security incidents as fast as possible, and will keep you informed of our progress. Thank you.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solotto",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Solana lottery client",
5
5
  "type": "module",
6
6
  "types": "./solotto.d.ts",
package/solotto.d.ts CHANGED
@@ -138,6 +138,34 @@ declare module "solotto" {
138
138
  reconnecting: (event: ReconnectingEvent) => void;
139
139
  }
140
140
 
141
+ // ── Booster Types ──────────────────────────────────────────────────
142
+
143
+ interface BoosterRecord {
144
+ /** Lottery numeric identifier. */
145
+ lotteryId: number;
146
+ /** Lottery authority public key. */
147
+ authority: string;
148
+ /** Booster wallet public key. */
149
+ booster: string;
150
+ /** Boost amount in SOL. */
151
+ amount: number;
152
+ /** Transaction signature. */
153
+ signature: string;
154
+ }
155
+
156
+ interface GroupedBooster {
157
+ /** Array of individual boost records. */
158
+ boost: BoosterRecord[];
159
+ /** Total SOL boosted by this wallet. */
160
+ total: number;
161
+ /** Number of boosts from this wallet. */
162
+ count: number;
163
+ }
164
+
165
+ interface GroupedBoostersResult {
166
+ [boosterAddress: string]: GroupedBooster;
167
+ }
168
+
141
169
  // ── Authority-like objects ────────────────────────────────────────────
142
170
 
143
171
  /** An object with at least a `publicKey` property (e.g. a Keypair without the secret key). */
@@ -237,7 +265,7 @@ declare module "solotto" {
237
265
  lotteryId: number,
238
266
  winner: Keypair,
239
267
  encoded?: boolean
240
- ): Promise<string | TxResult>;
268
+ ): Promise<string | string[] | TxResult>;
241
269
 
242
270
  /** Fetch the on-chain state of a lottery. */
243
271
  GetLottery(
@@ -337,5 +365,49 @@ declare module "solotto" {
337
365
  lotteryId: number,
338
366
  encoded?: boolean
339
367
  ): Promise<LotteryState | string | TxResult | undefined>;
368
+
369
+ /**
370
+ * Boost a lottery's prize pool by transferring SOL from any wallet.
371
+ * @param authority - The lottery authority (only `publicKey` needed).
372
+ * @param lotteryId - Lottery numeric identifier.
373
+ * @param booster - The keypair of the wallet sending the boost.
374
+ * @param amount - Amount of SOL to boost (e.g. `0.5` for 0.5 SOL).
375
+ * @param message - Optional memo string attached to the transaction.
376
+ * @param encoded - If `true`, return a base64-encoded transaction.
377
+ */
378
+ Boost(
379
+ authority: HasPublicKey,
380
+ lotteryId: number,
381
+ booster: Keypair,
382
+ amount: number,
383
+ message?: string | false,
384
+ encoded?: boolean
385
+ ): Promise<string | TxResult | undefined>;
386
+
387
+ /**
388
+ * Retrieve boost history from on-chain transaction memos.
389
+ * @param authority - Filter by lottery authority, or `false` for all.
390
+ * @param lotteryId - Filter by lottery ID, or `false` for all.
391
+ * @param group - If `true`, group results by booster wallet address.
392
+ * @param limit - Maximum number of recent transactions to scan (max 1000).
393
+ */
394
+ GetBoosters(
395
+ authority?: HasPublicKey | false,
396
+ lotteryId?: number | false,
397
+ group?: false,
398
+ limit?: number
399
+ ): Promise<BoosterRecord[]>;
400
+ GetBoosters(
401
+ authority: HasPublicKey | false,
402
+ lotteryId: number | false,
403
+ group: true,
404
+ limit?: number
405
+ ): Promise<GroupedBoostersResult>;
406
+ GetBoosters(
407
+ authority?: HasPublicKey | false,
408
+ lotteryId?: number | false,
409
+ group?: boolean,
410
+ limit?: number
411
+ ): Promise<BoosterRecord[] | GroupedBoostersResult>;
340
412
  }
341
413
  }
package/solotto.js CHANGED
@@ -1,10 +1,10 @@
1
- import {Connection, PublicKey, TransactionMessage, TransactionInstruction, VersionedTransaction, ComputeBudgetProgram, SystemProgram, Keypair, SYSVAR_SLOT_HASHES_PUBKEY, SYSVAR_CLOCK_PUBKEY, SYSVAR_RECENT_BLOCKHASHES_PUBKEY} from '@solana/web3.js';
1
+ import {Connection, PublicKey, TransactionMessage, TransactionInstruction, VersionedTransaction, ComputeBudgetProgram, SystemProgram, Keypair, SYSVAR_SLOT_HASHES_PUBKEY, LAMPORTS_PER_SOL} from '@solana/web3.js';
2
2
  import bs58 from 'bs58';
3
3
  import BN from 'bn.js';
4
4
  import {createMemoInstruction} from '@solana/spl-memo';
5
5
  import BufferLayout from "buffer-layout";
6
6
  const publicKey=(property="publicKey")=>{return BufferLayout.blob(32,property);};const uint64=(property="uint64")=>{return BufferLayout.blob(8,property);}
7
- import {createSolanaRpcSubscriptions} from "@solana/kit";
7
+ import {createSolanaRpcSubscriptions, SOLANA_ERROR__ADDRESSES__FAILED_TO_FIND_VIABLE_PDA_BUMP_SEED} from "@solana/kit";
8
8
  import {EventEmitter} from 'events';
9
9
 
10
10
  const INSTRUCTIONS = {
@@ -14,6 +14,7 @@ const INSTRUCTIONS = {
14
14
  CLAIM_PRIZE: 3,
15
15
  LOCK_LOTTERY: 4,
16
16
  RELEASE_EXPIRED: 5,
17
+ BOOST_LOTTERY: 6,
17
18
  };
18
19
 
19
20
  class LotteryNetwork {
@@ -284,6 +285,9 @@ class Lottery extends EventEmitter {
284
285
  _tx_.encode = false;
285
286
  }
286
287
  const tx = await network.Tx(_tx_);
288
+ if(tx.logs){
289
+ return tx.logs;
290
+ }
287
291
  if(winner.secretKey && !encoded){
288
292
  tx.transaction.sign([winner]);
289
293
  const sig = await network.Send(tx.transaction);
@@ -321,6 +325,9 @@ class Lottery extends EventEmitter {
321
325
  _tx_.encode = false;
322
326
  }
323
327
  const tx = await network.Tx(_tx_);
328
+ if(tx.logs && tx.logs.includes("Program log: Lottery is not active, no more tickets can be sold")){
329
+ return "Lottery is not active, no tickets can be sold";
330
+ }
324
331
  if(buyer.secretKey && !encoded){
325
332
  tx.transaction.sign([buyer]);
326
333
  const sig = await network.Send(tx.transaction);
@@ -793,7 +800,11 @@ class LotteryManager {
793
800
  _tx_.serialize = false;
794
801
  _tx_.encode = false;
795
802
  }
796
- const tx = await network.Tx(_tx_); // build the tx
803
+ const tx = await network.Tx(_tx_);
804
+ //
805
+ if(tx.logs && tx.logs.includes("Program log: Prize has already been claimed")){
806
+ return "Prize has already been claimed";
807
+ }
797
808
  if(tx.status !== "ok"){return tx;}
798
809
  if(authority.secretKey && !encoded){
799
810
  tx.transaction.sign([authority]);
@@ -812,6 +823,137 @@ class LotteryManager {
812
823
  }
813
824
  }
814
825
 
826
+ /**
827
+ * @param {Keypair} authority - Keypair
828
+ * @param {String} lotteryId - The lottery id
829
+ * @param {Keypair} booster - The booster's keypair
830
+ * @param {Number} amount - The amount of sol to boost
831
+ * @param {Boolean} encoded - true returns encoded transaction
832
+ */
833
+ async Boost(authority, lotteryId, booster, amount, message = false, encoded = false) {
834
+ try{
835
+ async function boostData(lotId, amount) {
836
+ const lamports = parseInt(amount * LAMPORTS_PER_SOL);
837
+ const buffer = Buffer.alloc(17); // 1 byte discriminator + 1 bytes price + 8 bytes id
838
+ buffer.writeUInt8(INSTRUCTIONS.BOOST_LOTTERY, 0); // boostLottery discriminator
839
+ buffer.writeBigUInt64LE(BigInt(lotId), 1);
840
+ buffer.writeBigUInt64LE(BigInt(lamports), 9);
841
+ return buffer;
842
+ }
843
+ if(message){
844
+ message = ":booster:"+authority.publicKey.toString()+","+lotteryId+","+booster.publicKey.toString()+","+amount+":booster:"+message;
845
+ }
846
+ const lottery = new Lottery(this.connection, false, this.program);
847
+ const network = new LotteryNetwork(this.connection);
848
+ const [lotteryPDA] = await lottery.DeriveLotteryPDA(authority.publicKey, lotteryId);
849
+ const LOTTO = await lottery.GetLottery(authority, lotteryId);
850
+ const keys = [
851
+ { pubkey: booster.publicKey, isSigner: true, isWritable: true },
852
+ { pubkey: lotteryPDA, isSigner: false, isWritable: true },
853
+ { pubkey: new PublicKey(LOTTO.prizePoolAddress), isSigner: false, isWritable: true },
854
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
855
+ ];
856
+ const ix = new TransactionInstruction(
857
+ {programId: this.program, keys, data: await boostData(lotteryId, amount)}
858
+ );
859
+ const _tx_ = {};
860
+ _tx_.account = booster.publicKey.toString(); // string : required
861
+ _tx_.instructions = [ix]; // array : required
862
+ _tx_.signers = false; // array : default false
863
+ _tx_.table = false; // array : default false
864
+ _tx_.tolerance = 1.2; // int : default 1.1
865
+ _tx_.compute = true; // bool : default true
866
+ _tx_.fees = true; // bool : default true
867
+ _tx_.priority = "Low"; // string : default Low
868
+ _tx_.memo = message;
869
+ if(encoded){
870
+ _tx_.serialize = true;
871
+ _tx_.encode = true;
872
+ }
873
+ else{
874
+ _tx_.serialize = false;
875
+ _tx_.encode = false;
876
+ }
877
+ const tx = await network.Tx(_tx_);
878
+ if(tx.logs && tx.logs.includes("Program log: Lottery draw has been initiated, cannot boost prize pool")){
879
+ return "Draw initiated, cannot boost this prize pool";
880
+ }
881
+ if(tx.status !== "ok"){return tx;}
882
+ if(booster.secretKey && !encoded){
883
+ tx.transaction.sign([booster]);
884
+ const sig = await network.Send(tx.transaction);
885
+ console.log("Signature:", sig);
886
+ const status = await network.Status(sig);
887
+ if(status == "finalized"){
888
+ return "boosted";
889
+ }
890
+ else{return status;}
891
+ }
892
+ else{return tx;}
893
+ }
894
+ catch (error) {
895
+ console.log(error);
896
+ }
897
+ }
898
+
899
+ /**
900
+ * @param {Keypair} authority - Keypair
901
+ * @param {String} lotteryId - The lottery id
902
+ * @param {Boolean} group - if true, groups results by booster wallet address
903
+ * @param {Number} limit - the results to request (max 1000)
904
+ * @returns {Array|Object} - Array of booster objects or Object grouped by booster address
905
+ * Booster objects are returned if the transaction memo includes ":booster:" and matches the authority and lotteryId (if provided)
906
+ * Booster memo format: ":booster:authorityPublicKey,lotteryId,boosterPublicKey,amount:booster:optionalMessage"
907
+ * Example return object: { authority: "authorityPublicKey", lotteryId: "1", booster: "boosterPublicKey", amount: 1.5, signature: "transactionSignature" }
908
+ * If authority is provided, only boosters from that authority are returned
909
+ * If lotteryId is provided, only boosters for that lotteryId are returned
910
+ * If both authority and lotteryId are provided, only boosters matching both are returned
911
+ * If neither is provided, all boosters are returned up to the specified limit
912
+ * If group is true, returns object with booster addresses as keys, each containing array of boost objects and total amount
913
+ * Example grouped return: { "boosterPublicKey": { boost: [...], total: 5.5 } }
914
+ */
915
+ async GetBoosters(authority=false, lotteryId=false, group=false, limit=1000) {
916
+ try{
917
+ const result = [];
918
+ const signatures = await this.connection.getSignaturesForAddress(this.program, {limit: limit,});
919
+ for await (const row of signatures) {
920
+ if(row.memo && row.memo.includes(":booster:")){
921
+ const memo = row.memo.split(":booster:")[1];
922
+ const [auth, lotId, booster, amount] = memo.split(",");
923
+ if((authority ? authority.publicKey.toString() === auth : true) &&
924
+ (lotteryId ? lotteryId.toString() === lotId : true)
925
+ ){
926
+ result.push({
927
+ lotteryId: Number(lotId),
928
+ authority: auth,
929
+ booster: booster,
930
+ amount: parseFloat(amount),
931
+ signature: row.signature
932
+ });
933
+ }
934
+ }
935
+ }
936
+ if (group) {
937
+ const grouped = {};
938
+ result.forEach(item => {
939
+ if (!grouped[item.booster]) {
940
+ grouped[item.booster] = {
941
+ boost: [],
942
+ total: 0
943
+ }
944
+ }
945
+ grouped[item.booster].boost.push(item);
946
+ grouped[item.booster].total += item.amount;
947
+ grouped[item.booster].count = grouped[item.booster].boost.length;
948
+ });
949
+ return grouped;
950
+ }
951
+
952
+ return result;
953
+ }
954
+ catch (error) {return error;}
955
+ }
956
+
815
957
  }
816
958
 
817
959
  export {