nara-sdk 1.0.44 → 1.0.48

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/index.ts CHANGED
@@ -18,7 +18,7 @@ export {
18
18
  } from "./src/constants";
19
19
 
20
20
  // Export transaction helper
21
- export { sendTx, setAltAddress, getAltAddress } from "./src/tx";
21
+ export { sendTx, setAltAddress, getAltAddress, getRecentPriorityFee } from "./src/tx";
22
22
 
23
23
  // Export quest functions and types
24
24
  export {
@@ -34,8 +34,8 @@ export {
34
34
  unstake,
35
35
  getStakeInfo,
36
36
  initializeQuest,
37
- setMaxRewardCount,
38
- setMinRewardCount,
37
+ setRewardConfig,
38
+ setStakeConfig,
39
39
  transferQuestAuthority,
40
40
  getQuestConfig,
41
41
  type QuestInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nara-sdk",
3
- "version": "1.0.44",
3
+ "version": "1.0.48",
4
4
  "description": "SDK for the Nara chain (Solana-compatible)",
5
5
  "module": "index.ts",
6
6
  "main": "index.ts",
@@ -204,16 +204,16 @@
204
204
  "args": []
205
205
  },
206
206
  {
207
- "name": "set_max_reward_count",
207
+ "name": "set_reward_config",
208
208
  "discriminator": [
209
- 247,
210
- 62,
211
- 67,
212
- 243,
213
- 249,
214
- 243,
215
- 102,
216
- 62
209
+ 163,
210
+ 34,
211
+ 211,
212
+ 14,
213
+ 25,
214
+ 118,
215
+ 181,
216
+ 233
217
217
  ],
218
218
  "accounts": [
219
219
  {
@@ -247,6 +247,10 @@
247
247
  }
248
248
  ],
249
249
  "args": [
250
+ {
251
+ "name": "min_reward_count",
252
+ "type": "u32"
253
+ },
250
254
  {
251
255
  "name": "max_reward_count",
252
256
  "type": "u32"
@@ -254,16 +258,16 @@
254
258
  ]
255
259
  },
256
260
  {
257
- "name": "set_min_reward_count",
261
+ "name": "set_stake_config",
258
262
  "discriminator": [
259
- 108,
260
- 213,
261
- 24,
262
- 47,
263
- 93,
264
- 149,
265
- 58,
266
- 4
263
+ 84,
264
+ 37,
265
+ 76,
266
+ 39,
267
+ 236,
268
+ 111,
269
+ 214,
270
+ 191
267
271
  ],
268
272
  "accounts": [
269
273
  {
@@ -298,8 +302,16 @@
298
302
  ],
299
303
  "args": [
300
304
  {
301
- "name": "min_reward_count",
302
- "type": "u32"
305
+ "name": "bps_high",
306
+ "type": "u64"
307
+ },
308
+ {
309
+ "name": "bps_low",
310
+ "type": "u64"
311
+ },
312
+ {
313
+ "name": "decay_ms",
314
+ "type": "i64"
303
315
  }
304
316
  ]
305
317
  },
@@ -498,6 +510,30 @@
498
510
  48
499
511
  ],
500
512
  "accounts": [
513
+ {
514
+ "name": "game_config",
515
+ "pda": {
516
+ "seeds": [
517
+ {
518
+ "kind": "const",
519
+ "value": [
520
+ 113,
521
+ 117,
522
+ 101,
523
+ 115,
524
+ 116,
525
+ 95,
526
+ 99,
527
+ 111,
528
+ 110,
529
+ 102,
530
+ 105,
531
+ 103
532
+ ]
533
+ }
534
+ ]
535
+ }
536
+ },
501
537
  {
502
538
  "name": "pool",
503
539
  "writable": true,
@@ -1192,12 +1228,12 @@
1192
1228
  {
1193
1229
  "code": 6008,
1194
1230
  "name": "InvalidMinRewardCount",
1195
- "msg": "min_reward_count must be > 0 and <= max_reward_count"
1231
+ "msg": "Invalid reward config: need 0 < min <= max"
1196
1232
  },
1197
1233
  {
1198
1234
  "code": 6009,
1199
- "name": "InvalidMaxRewardCount",
1200
- "msg": "max_reward_count must be >= min_reward_count"
1235
+ "name": "InvalidStakeConfig",
1236
+ "msg": "Stake config values must be > 0"
1201
1237
  },
1202
1238
  {
1203
1239
  "code": 6010,
@@ -1208,6 +1244,11 @@
1208
1244
  "code": 6011,
1209
1245
  "name": "InsufficientStakeBalance",
1210
1246
  "msg": "Unstake amount exceeds staked balance"
1247
+ },
1248
+ {
1249
+ "code": 6012,
1250
+ "name": "InsufficientStake",
1251
+ "msg": "Stake does not meet dynamic requirement"
1211
1252
  }
1212
1253
  ],
1213
1254
  "types": [
@@ -1260,6 +1301,18 @@
1260
1301
  "name": "max_reward_count",
1261
1302
  "type": "u32"
1262
1303
  },
1304
+ {
1305
+ "name": "stake_bps_high",
1306
+ "type": "u64"
1307
+ },
1308
+ {
1309
+ "name": "stake_bps_low",
1310
+ "type": "u64"
1311
+ },
1312
+ {
1313
+ "name": "decay_ms",
1314
+ "type": "i64"
1315
+ },
1263
1316
  {
1264
1317
  "name": "_padding",
1265
1318
  "type": {
@@ -1319,11 +1372,19 @@
1319
1372
  "type": "u32"
1320
1373
  },
1321
1374
  {
1322
- "name": "stake_requirement",
1375
+ "name": "created_at",
1376
+ "type": "i64"
1377
+ },
1378
+ {
1379
+ "name": "stake_high",
1380
+ "type": "u64"
1381
+ },
1382
+ {
1383
+ "name": "stake_low",
1323
1384
  "type": "u64"
1324
1385
  },
1325
1386
  {
1326
- "name": "min_winner_stake",
1387
+ "name": "avg_participant_stake",
1327
1388
  "type": "u64"
1328
1389
  },
1329
1390
  {
@@ -210,16 +210,16 @@ export type NaraQuest = {
210
210
  "args": []
211
211
  },
212
212
  {
213
- "name": "setMaxRewardCount",
213
+ "name": "setRewardConfig",
214
214
  "discriminator": [
215
- 247,
216
- 62,
217
- 67,
218
- 243,
219
- 249,
220
- 243,
221
- 102,
222
- 62
215
+ 163,
216
+ 34,
217
+ 211,
218
+ 14,
219
+ 25,
220
+ 118,
221
+ 181,
222
+ 233
223
223
  ],
224
224
  "accounts": [
225
225
  {
@@ -253,6 +253,10 @@ export type NaraQuest = {
253
253
  }
254
254
  ],
255
255
  "args": [
256
+ {
257
+ "name": "minRewardCount",
258
+ "type": "u32"
259
+ },
256
260
  {
257
261
  "name": "maxRewardCount",
258
262
  "type": "u32"
@@ -260,16 +264,16 @@ export type NaraQuest = {
260
264
  ]
261
265
  },
262
266
  {
263
- "name": "setMinRewardCount",
267
+ "name": "setStakeConfig",
264
268
  "discriminator": [
265
- 108,
266
- 213,
267
- 24,
268
- 47,
269
- 93,
270
- 149,
271
- 58,
272
- 4
269
+ 84,
270
+ 37,
271
+ 76,
272
+ 39,
273
+ 236,
274
+ 111,
275
+ 214,
276
+ 191
273
277
  ],
274
278
  "accounts": [
275
279
  {
@@ -304,8 +308,16 @@ export type NaraQuest = {
304
308
  ],
305
309
  "args": [
306
310
  {
307
- "name": "minRewardCount",
308
- "type": "u32"
311
+ "name": "bpsHigh",
312
+ "type": "u64"
313
+ },
314
+ {
315
+ "name": "bpsLow",
316
+ "type": "u64"
317
+ },
318
+ {
319
+ "name": "decayMs",
320
+ "type": "i64"
309
321
  }
310
322
  ]
311
323
  },
@@ -504,6 +516,30 @@ export type NaraQuest = {
504
516
  48
505
517
  ],
506
518
  "accounts": [
519
+ {
520
+ "name": "gameConfig",
521
+ "pda": {
522
+ "seeds": [
523
+ {
524
+ "kind": "const",
525
+ "value": [
526
+ 113,
527
+ 117,
528
+ 101,
529
+ 115,
530
+ 116,
531
+ 95,
532
+ 99,
533
+ 111,
534
+ 110,
535
+ 102,
536
+ 105,
537
+ 103
538
+ ]
539
+ }
540
+ ]
541
+ }
542
+ },
507
543
  {
508
544
  "name": "pool",
509
545
  "writable": true,
@@ -1198,12 +1234,12 @@ export type NaraQuest = {
1198
1234
  {
1199
1235
  "code": 6008,
1200
1236
  "name": "invalidMinRewardCount",
1201
- "msg": "min_reward_count must be > 0 and <= max_reward_count"
1237
+ "msg": "Invalid reward config: need 0 < min <= max"
1202
1238
  },
1203
1239
  {
1204
1240
  "code": 6009,
1205
- "name": "invalidMaxRewardCount",
1206
- "msg": "max_reward_count must be >= min_reward_count"
1241
+ "name": "invalidStakeConfig",
1242
+ "msg": "Stake config values must be > 0"
1207
1243
  },
1208
1244
  {
1209
1245
  "code": 6010,
@@ -1214,6 +1250,11 @@ export type NaraQuest = {
1214
1250
  "code": 6011,
1215
1251
  "name": "insufficientStakeBalance",
1216
1252
  "msg": "Unstake amount exceeds staked balance"
1253
+ },
1254
+ {
1255
+ "code": 6012,
1256
+ "name": "insufficientStake",
1257
+ "msg": "Stake does not meet dynamic requirement"
1217
1258
  }
1218
1259
  ],
1219
1260
  "types": [
@@ -1266,6 +1307,18 @@ export type NaraQuest = {
1266
1307
  "name": "maxRewardCount",
1267
1308
  "type": "u32"
1268
1309
  },
1310
+ {
1311
+ "name": "stakeBpsHigh",
1312
+ "type": "u64"
1313
+ },
1314
+ {
1315
+ "name": "stakeBpsLow",
1316
+ "type": "u64"
1317
+ },
1318
+ {
1319
+ "name": "decayMs",
1320
+ "type": "i64"
1321
+ },
1269
1322
  {
1270
1323
  "name": "padding",
1271
1324
  "type": {
@@ -1325,11 +1378,19 @@ export type NaraQuest = {
1325
1378
  "type": "u32"
1326
1379
  },
1327
1380
  {
1328
- "name": "stakeRequirement",
1381
+ "name": "createdAt",
1382
+ "type": "i64"
1383
+ },
1384
+ {
1385
+ "name": "stakeHigh",
1386
+ "type": "u64"
1387
+ },
1388
+ {
1389
+ "name": "stakeLow",
1329
1390
  "type": "u64"
1330
1391
  },
1331
1392
  {
1332
- "name": "minWinnerStake",
1393
+ "name": "avgParticipantStake",
1333
1394
  "type": "u64"
1334
1395
  },
1335
1396
  {
package/src/quest.ts CHANGED
@@ -50,10 +50,16 @@ export interface QuestInfo {
50
50
  deadline: number;
51
51
  timeRemaining: number;
52
52
  expired: boolean;
53
- /** Minimum stake required to submit an answer (in NARA) */
54
- stakeRequirement: number;
55
- /** Minimum stake to be eligible for rewards (in NARA) */
56
- minWinnerStake: number;
53
+ /** High stake requirement for the current round (in NARA, decays over time) */
54
+ stakeHigh: number;
55
+ /** Low stake requirement for the current round (in NARA, floor after decay) */
56
+ stakeLow: number;
57
+ /** Running average participant stake for the current round (in NARA) */
58
+ avgParticipantStake: number;
59
+ /** Unix timestamp when the current question was created */
60
+ createdAt: number;
61
+ /** Current effective stake requirement after parabolic decay (in NARA) */
62
+ effectiveStakeRequirement: number;
57
63
  }
58
64
 
59
65
  export interface StakeInfo {
@@ -242,6 +248,26 @@ function getStakeTokenAccount(stakeRecordPda: PublicKey): PublicKey {
242
248
  return getAssociatedTokenAddressSync(WSOL_MINT, stakeRecordPda, true);
243
249
  }
244
250
 
251
+ /**
252
+ * Compute the effective stake requirement using the parabolic decay formula.
253
+ * effective = stakeHigh - (stakeHigh - stakeLow) * (elapsed / decay)^2
254
+ * All amounts in NARA (not lamports). Times in milliseconds.
255
+ */
256
+ function computeEffectiveStake(
257
+ stakeHigh: number,
258
+ stakeLow: number,
259
+ createdAt: number,
260
+ decayMs: number,
261
+ nowMs: number
262
+ ): number {
263
+ if (decayMs <= 0) return stakeLow;
264
+ const elapsedMs = nowMs - createdAt;
265
+ if (elapsedMs >= decayMs) return stakeLow;
266
+ const range = stakeHigh - stakeLow;
267
+ const ratio = elapsedMs / decayMs;
268
+ return stakeHigh - range * ratio * ratio;
269
+ }
270
+
245
271
  // ─── SDK functions ───────────────────────────────────────────────
246
272
 
247
273
  /**
@@ -263,6 +289,26 @@ export async function getQuestInfo(
263
289
 
264
290
  const active = pool.question.length > 0 && secsLeft > 0;
265
291
 
292
+ const stakeHigh = Number(pool.stakeHigh.toString()) / LAMPORTS_PER_SOL;
293
+ const stakeLow = Number(pool.stakeLow.toString()) / LAMPORTS_PER_SOL;
294
+ const createdAt = pool.createdAt.toNumber();
295
+
296
+ // Fetch decayMs from GameConfig for effective calculation
297
+ const programId = new PublicKey(options?.programId ?? DEFAULT_QUEST_PROGRAM_ID);
298
+ const [configPda] = PublicKey.findProgramAddressSync(
299
+ [new TextEncoder().encode("quest_config")],
300
+ programId
301
+ );
302
+ const config = await program.account.gameConfig.fetch(configPda);
303
+ const decayMs = Number(config.decayMs.toString());
304
+
305
+ // createdAt is unix timestamp (seconds), convert to ms for decay calculation
306
+ const nowMs = Date.now();
307
+ const createdAtMs = createdAt * 1000;
308
+ const effectiveStakeRequirement = computeEffectiveStake(
309
+ stakeHigh, stakeLow, createdAtMs, decayMs, nowMs
310
+ );
311
+
266
312
  return {
267
313
  active,
268
314
  round: pool.round.toString(),
@@ -277,8 +323,11 @@ export async function getQuestInfo(
277
323
  deadline,
278
324
  timeRemaining: secsLeft,
279
325
  expired: secsLeft <= 0,
280
- stakeRequirement: Number(pool.stakeRequirement.toString()) / LAMPORTS_PER_SOL,
281
- minWinnerStake: Number(pool.minWinnerStake.toString()) / LAMPORTS_PER_SOL,
326
+ stakeHigh,
327
+ stakeLow,
328
+ avgParticipantStake: Number(pool.avgParticipantStake.toString()) / LAMPORTS_PER_SOL,
329
+ createdAt,
330
+ effectiveStakeRequirement,
282
331
  };
283
332
  }
284
333
 
@@ -372,7 +421,7 @@ export async function submitAnswer(
372
421
  if (options.stake === "auto") {
373
422
  const quest = await getQuestInfo(connection, wallet, options);
374
423
  const stakeInfo = await getStakeInfo(connection, wallet.publicKey, options);
375
- const required = quest.stakeRequirement;
424
+ const required = quest.effectiveStakeRequirement;
376
425
  const current = stakeInfo?.amount ?? 0;
377
426
  const deficit = required - current;
378
427
  if (deficit > 0) {
@@ -423,7 +472,11 @@ export async function submitAnswer(
423
472
  ixs.push(logIx);
424
473
  }
425
474
 
426
- const signature = await sendTx(connection, wallet, ixs, [], { skipPreflight: true });
475
+ const signature = await sendTx(connection, wallet, ixs, [], {
476
+ skipPreflight: true,
477
+ computeUnitLimit: 500_000,
478
+ computeUnitPrice: "auto",
479
+ });
427
480
  return { signature };
428
481
  }
429
482
 
@@ -645,34 +698,40 @@ export async function initializeQuest(
645
698
  }
646
699
 
647
700
  /**
648
- * Set the maximum reward count (authority only).
701
+ * Set the reward config (authority only).
649
702
  */
650
- export async function setMaxRewardCount(
703
+ export async function setRewardConfig(
651
704
  connection: Connection,
652
705
  wallet: Keypair,
706
+ minRewardCount: number,
653
707
  maxRewardCount: number,
654
708
  options?: QuestOptions
655
709
  ): Promise<string> {
656
710
  const program = createProgram(connection, wallet, options?.programId);
657
711
  const ix = await program.methods
658
- .setMaxRewardCount(maxRewardCount)
712
+ .setRewardConfig(minRewardCount, maxRewardCount)
659
713
  .accounts({ authority: wallet.publicKey } as any)
660
714
  .instruction();
661
715
  return sendTx(connection, wallet, [ix]);
662
716
  }
663
717
 
664
718
  /**
665
- * Set the minimum reward count (authority only).
719
+ * Set the stake config (authority only).
720
+ * @param bpsHigh - Upper bound multiplier in basis points (e.g. 100000 = 10x average)
721
+ * @param bpsLow - Lower bound multiplier in basis points (e.g. 1000 = 0.1x average)
722
+ * @param decayMs - Time window in milliseconds for parabolic decay from high to low
666
723
  */
667
- export async function setMinRewardCount(
724
+ export async function setStakeConfig(
668
725
  connection: Connection,
669
726
  wallet: Keypair,
670
- minRewardCount: number,
727
+ bpsHigh: number,
728
+ bpsLow: number,
729
+ decayMs: number,
671
730
  options?: QuestOptions
672
731
  ): Promise<string> {
673
732
  const program = createProgram(connection, wallet, options?.programId);
674
733
  const ix = await program.methods
675
- .setMinRewardCount(minRewardCount)
734
+ .setStakeConfig(new BN(bpsHigh), new BN(bpsLow), new BN(decayMs))
676
735
  .accounts({ authority: wallet.publicKey } as any)
677
736
  .instruction();
678
737
  return sendTx(connection, wallet, [ix]);
@@ -701,7 +760,14 @@ export async function transferQuestAuthority(
701
760
  export async function getQuestConfig(
702
761
  connection: Connection,
703
762
  options?: QuestOptions
704
- ): Promise<{ authority: PublicKey; minRewardCount: number; maxRewardCount: number }> {
763
+ ): Promise<{
764
+ authority: PublicKey;
765
+ minRewardCount: number;
766
+ maxRewardCount: number;
767
+ stakeBpsHigh: number;
768
+ stakeBpsLow: number;
769
+ decayMs: number;
770
+ }> {
705
771
  const kp = Keypair.generate();
706
772
  const program = createProgram(connection, kp, options?.programId);
707
773
  const programId = new PublicKey(options?.programId ?? DEFAULT_QUEST_PROGRAM_ID);
@@ -714,5 +780,8 @@ export async function getQuestConfig(
714
780
  authority: config.authority,
715
781
  minRewardCount: config.minRewardCount,
716
782
  maxRewardCount: config.maxRewardCount,
783
+ stakeBpsHigh: Number(config.stakeBpsHigh.toString()),
784
+ stakeBpsLow: Number(config.stakeBpsLow.toString()),
785
+ decayMs: Number(config.decayMs.toString()),
717
786
  };
718
787
  }
package/src/tx.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import {
9
9
  AddressLookupTableAccount,
10
+ ComputeBudgetProgram,
10
11
  Connection,
11
12
  Keypair,
12
13
  PublicKey,
@@ -57,11 +58,30 @@ async function loadAlt(
57
58
  return _cachedAlt;
58
59
  }
59
60
 
61
+ /**
62
+ * Get the recent average priority fee (in micro-lamports per CU).
63
+ * Samples the last few slots via getRecentPrioritizationFees.
64
+ */
65
+ export async function getRecentPriorityFee(
66
+ connection: Connection,
67
+ ): Promise<number> {
68
+ const fees = await connection.getRecentPrioritizationFees();
69
+ if (!fees.length) return 0;
70
+ const nonZero = fees.filter((f) => f.prioritizationFee > 0);
71
+ if (!nonZero.length) return 0;
72
+ const avg = nonZero.reduce((s, f) => s + f.prioritizationFee, 0) / nonZero.length;
73
+ return Math.ceil(avg);
74
+ }
75
+
60
76
  /**
61
77
  * Send a transaction with optional ALT support.
62
78
  * If DEFAULT_ALT_ADDRESS is configured, uses VersionedTransaction.
63
79
  * Otherwise, uses legacy Transaction.
64
80
  *
81
+ * opts.computeUnitLimit - set CU limit (ComputeBudgetProgram.setComputeUnitLimit)
82
+ * opts.computeUnitPrice - set CU price in micro-lamports (ComputeBudgetProgram.setComputeUnitPrice)
83
+ * opts.computeUnitPrice = "auto" - auto-fetch recent average priority fee
84
+ *
65
85
  * @returns transaction signature
66
86
  */
67
87
  export async function sendTx(
@@ -69,8 +89,30 @@ export async function sendTx(
69
89
  payer: Keypair,
70
90
  instructions: TransactionInstruction[],
71
91
  signers?: Keypair[],
72
- opts?: { skipPreflight?: boolean }
92
+ opts?: { skipPreflight?: boolean; computeUnitLimit?: number; computeUnitPrice?: number | "auto" }
73
93
  ): Promise<string> {
94
+ // Prepend compute budget instructions
95
+ const budgetIxs: TransactionInstruction[] = [];
96
+ if (opts?.computeUnitLimit) {
97
+ budgetIxs.push(
98
+ ComputeBudgetProgram.setComputeUnitLimit({ units: opts.computeUnitLimit })
99
+ );
100
+ }
101
+ if (opts?.computeUnitPrice !== undefined) {
102
+ let price: number;
103
+ if (opts.computeUnitPrice === "auto") {
104
+ price = await getRecentPriorityFee(connection);
105
+ } else {
106
+ price = opts.computeUnitPrice;
107
+ }
108
+ if (price > 0) {
109
+ budgetIxs.push(
110
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: price })
111
+ );
112
+ }
113
+ }
114
+ const allInstructions = [...budgetIxs, ...instructions];
115
+
74
116
  const alt = await loadAlt(connection);
75
117
  const { blockhash, lastValidBlockHeight } =
76
118
  await connection.getLatestBlockhash("confirmed");
@@ -81,7 +123,7 @@ export async function sendTx(
81
123
  const message = new TransactionMessage({
82
124
  payerKey: payer.publicKey,
83
125
  recentBlockhash: blockhash,
84
- instructions,
126
+ instructions: allInstructions,
85
127
  }).compileToV0Message([alt]);
86
128
 
87
129
  const tx = new VersionedTransaction(message);
@@ -103,7 +145,7 @@ export async function sendTx(
103
145
  tx.recentBlockhash = blockhash;
104
146
  tx.lastValidBlockHeight = lastValidBlockHeight;
105
147
  tx.feePayer = payer.publicKey;
106
- for (const ix of instructions) tx.add(ix);
148
+ for (const ix of allInstructions) tx.add(ix);
107
149
  const allSigners = [payer, ...(signers ?? [])];
108
150
  const seen = new Set<string>();
109
151
  const uniqueSigners = allSigners.filter((s) => {
@@ -118,14 +160,38 @@ export async function sendTx(
118
160
  });
119
161
  }
120
162
 
121
- const confirmation = await connection.confirmTransaction(
122
- { signature, blockhash, lastValidBlockHeight },
123
- "confirmed"
124
- );
125
- if (confirmation.value.err) {
126
- throw new Error(
127
- `Transaction ${signature} failed: ${JSON.stringify(confirmation.value.err)}`
128
- );
163
+ // Poll for confirmation (avoid confirmTransaction which uses WebSocket)
164
+ const startTime = Date.now();
165
+ const TIMEOUT_MS = 20_000;
166
+ const POLL_INTERVAL_MS = 1_000;
167
+
168
+ while (Date.now() - startTime < TIMEOUT_MS) {
169
+ const currentBlockHeight = await connection.getBlockHeight("confirmed");
170
+ if (currentBlockHeight > lastValidBlockHeight) {
171
+ throw new Error(
172
+ `Transaction ${signature} expired: block height exceeded`
173
+ );
174
+ }
175
+
176
+ const statusResult = await connection.getSignatureStatuses([signature]);
177
+ const status = statusResult?.value?.[0];
178
+
179
+ if (status) {
180
+ if (status.err) {
181
+ throw new Error(
182
+ `Transaction ${signature} failed: ${JSON.stringify(status.err)}`
183
+ );
184
+ }
185
+ if (
186
+ status.confirmationStatus === "confirmed" ||
187
+ status.confirmationStatus === "finalized"
188
+ ) {
189
+ return signature;
190
+ }
191
+ }
192
+
193
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
129
194
  }
130
- return signature;
195
+
196
+ throw new Error(`Transaction ${signature} confirmation timeout`);
131
197
  }