nara-sdk 1.0.45 → 1.0.49

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
@@ -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.45",
3
+ "version": "1.0.49",
4
4
  "description": "SDK for the Nara chain (Solana-compatible)",
5
5
  "module": "index.ts",
6
6
  "main": "index.ts",
package/src/constants.ts CHANGED
@@ -39,8 +39,9 @@ export const DEFAULT_AGENT_REGISTRY_PROGRAM_ID =
39
39
  process.env.AGENT_REGISTRY_PROGRAM_ID || "AgentRegistry111111111111111111111111111111";
40
40
 
41
41
  /**
42
- * Address Lookup Table address for transaction optimization.
43
- * When set, all SDK transactions use VersionedTransaction with this ALT.
42
+ * Address Lookup Table addresses for transaction optimization.
43
+ * Supports comma-separated list for multiple ALTs.
44
+ * When set, all SDK transactions use VersionedTransaction with these ALTs.
44
45
  * When empty, uses legacy transactions.
45
46
  */
46
- export const DEFAULT_ALT_ADDRESS = process.env.ALT_ADDRESS || "";
47
+ export const DEFAULT_ALT_ADDRESS = process.env.ALT_ADDRESS || "7u3uwQof8YnTrdYE2ZXgAYQLsDxW9cEJjqC3zJHrZXGo";
@@ -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) {
@@ -649,34 +698,40 @@ export async function initializeQuest(
649
698
  }
650
699
 
651
700
  /**
652
- * Set the maximum reward count (authority only).
701
+ * Set the reward config (authority only).
653
702
  */
654
- export async function setMaxRewardCount(
703
+ export async function setRewardConfig(
655
704
  connection: Connection,
656
705
  wallet: Keypair,
706
+ minRewardCount: number,
657
707
  maxRewardCount: number,
658
708
  options?: QuestOptions
659
709
  ): Promise<string> {
660
710
  const program = createProgram(connection, wallet, options?.programId);
661
711
  const ix = await program.methods
662
- .setMaxRewardCount(maxRewardCount)
712
+ .setRewardConfig(minRewardCount, maxRewardCount)
663
713
  .accounts({ authority: wallet.publicKey } as any)
664
714
  .instruction();
665
715
  return sendTx(connection, wallet, [ix]);
666
716
  }
667
717
 
668
718
  /**
669
- * 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
670
723
  */
671
- export async function setMinRewardCount(
724
+ export async function setStakeConfig(
672
725
  connection: Connection,
673
726
  wallet: Keypair,
674
- minRewardCount: number,
727
+ bpsHigh: number,
728
+ bpsLow: number,
729
+ decayMs: number,
675
730
  options?: QuestOptions
676
731
  ): Promise<string> {
677
732
  const program = createProgram(connection, wallet, options?.programId);
678
733
  const ix = await program.methods
679
- .setMinRewardCount(minRewardCount)
734
+ .setStakeConfig(new BN(bpsHigh), new BN(bpsLow), new BN(decayMs))
680
735
  .accounts({ authority: wallet.publicKey } as any)
681
736
  .instruction();
682
737
  return sendTx(connection, wallet, [ix]);
@@ -705,7 +760,14 @@ export async function transferQuestAuthority(
705
760
  export async function getQuestConfig(
706
761
  connection: Connection,
707
762
  options?: QuestOptions
708
- ): 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
+ }> {
709
771
  const kp = Keypair.generate();
710
772
  const program = createProgram(connection, kp, options?.programId);
711
773
  const programId = new PublicKey(options?.programId ?? DEFAULT_QUEST_PROGRAM_ID);
@@ -718,5 +780,8 @@ export async function getQuestConfig(
718
780
  authority: config.authority,
719
781
  minRewardCount: config.minRewardCount,
720
782
  maxRewardCount: config.maxRewardCount,
783
+ stakeBpsHigh: Number(config.stakeBpsHigh.toString()),
784
+ stakeBpsLow: Number(config.stakeBpsLow.toString()),
785
+ decayMs: Number(config.decayMs.toString()),
721
786
  };
722
787
  }
package/src/tx.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Shared transaction sending utility with optional Address Lookup Table support.
3
3
  *
4
- * When DEFAULT_ALT_ADDRESS is set, transactions are sent as VersionedTransaction
5
- * with the ALT for smaller on-chain size. Otherwise, legacy Transaction is used.
4
+ * When ALT addresses are configured, transactions are sent as VersionedTransaction
5
+ * with ALTs for smaller on-chain size. Otherwise, legacy Transaction is used.
6
6
  */
7
7
 
8
8
  import {
@@ -18,44 +18,63 @@ import {
18
18
  } from "@solana/web3.js";
19
19
  import { DEFAULT_ALT_ADDRESS } from "./constants";
20
20
 
21
- let _cachedAlt: AddressLookupTableAccount | null = null;
22
- let _cachedAltAddress: string = "";
23
- let _overrideAltAddress: string | null = null;
21
+ let _cachedAlts: AddressLookupTableAccount[] = [];
22
+ let _cachedAltKey: string = "";
23
+ let _overrideAltAddresses: string[] | null = null;
24
24
 
25
25
  /**
26
- * Set a global ALT address at runtime (overrides DEFAULT_ALT_ADDRESS / env).
27
- * Pass empty string or null to disable ALT.
26
+ * Set global ALT addresses at runtime (overrides DEFAULT_ALT_ADDRESS / env).
27
+ * Pass empty array or null to disable ALT.
28
28
  */
29
- export function setAltAddress(address: string | null): void {
30
- _overrideAltAddress = address;
31
- // Invalidate cache when address changes
32
- _cachedAlt = null;
33
- _cachedAltAddress = "";
29
+ export function setAltAddress(addresses: string | string[] | null): void {
30
+ if (addresses === null) {
31
+ _overrideAltAddresses = [];
32
+ } else if (typeof addresses === "string") {
33
+ _overrideAltAddresses = addresses ? [addresses] : [];
34
+ } else {
35
+ _overrideAltAddresses = addresses.filter(Boolean);
36
+ }
37
+ // Invalidate cache when addresses change
38
+ _cachedAlts = [];
39
+ _cachedAltKey = "";
34
40
  }
35
41
 
36
42
  /**
37
- * Get the current effective ALT address.
43
+ * Get the current effective ALT addresses.
38
44
  */
39
- export function getAltAddress(): string {
40
- return _overrideAltAddress ?? DEFAULT_ALT_ADDRESS;
45
+ export function getAltAddress(): string[] {
46
+ if (_overrideAltAddresses !== null) return _overrideAltAddresses;
47
+ if (!DEFAULT_ALT_ADDRESS) return [];
48
+ // env supports comma-separated list
49
+ return DEFAULT_ALT_ADDRESS.split(",").map((s) => s.trim()).filter(Boolean);
41
50
  }
42
51
 
43
- async function loadAlt(
52
+ async function loadAlts(
44
53
  connection: Connection
45
- ): Promise<AddressLookupTableAccount | null> {
46
- const addr = getAltAddress();
47
- if (!addr) return null;
54
+ ): Promise<AddressLookupTableAccount[]> {
55
+ const addrs = getAltAddress();
56
+ if (!addrs.length) return [];
48
57
 
49
- // Cache the ALT account to avoid repeated fetches
50
- if (_cachedAlt && _cachedAltAddress === addr) return _cachedAlt;
58
+ const key = addrs.join(",");
59
+ if (_cachedAlts.length && _cachedAltKey === key) return _cachedAlts;
51
60
 
52
- const result = await connection.getAddressLookupTable(new PublicKey(addr));
53
- if (!result.value) {
54
- throw new Error(`Address Lookup Table not found: ${addr}`);
61
+ const results: AddressLookupTableAccount[] = [];
62
+ for (const addr of addrs) {
63
+ try {
64
+ const result = await connection.getAddressLookupTable(new PublicKey(addr));
65
+ if (result.value) {
66
+ results.push(result.value);
67
+ } else {
68
+ console.warn(`[nara-sdk] ALT not found: ${addr}, skipping`);
69
+ }
70
+ } catch (e) {
71
+ console.warn(`[nara-sdk] Failed to load ALT ${addr}: ${e}, skipping`);
72
+ }
55
73
  }
56
- _cachedAlt = result.value;
57
- _cachedAltAddress = addr;
58
- return _cachedAlt;
74
+
75
+ _cachedAlts = results;
76
+ _cachedAltKey = key;
77
+ return _cachedAlts;
59
78
  }
60
79
 
61
80
  /**
@@ -75,7 +94,7 @@ export async function getRecentPriorityFee(
75
94
 
76
95
  /**
77
96
  * Send a transaction with optional ALT support.
78
- * If DEFAULT_ALT_ADDRESS is configured, uses VersionedTransaction.
97
+ * If ALT addresses are configured, uses VersionedTransaction.
79
98
  * Otherwise, uses legacy Transaction.
80
99
  *
81
100
  * opts.computeUnitLimit - set CU limit (ComputeBudgetProgram.setComputeUnitLimit)
@@ -113,18 +132,18 @@ export async function sendTx(
113
132
  }
114
133
  const allInstructions = [...budgetIxs, ...instructions];
115
134
 
116
- const alt = await loadAlt(connection);
135
+ const alts = await loadAlts(connection);
117
136
  const { blockhash, lastValidBlockHeight } =
118
137
  await connection.getLatestBlockhash("confirmed");
119
138
 
120
139
  let signature: string;
121
140
 
122
- if (alt) {
141
+ if (alts.length) {
123
142
  const message = new TransactionMessage({
124
143
  payerKey: payer.publicKey,
125
144
  recentBlockhash: blockhash,
126
145
  instructions: allInstructions,
127
- }).compileToV0Message([alt]);
146
+ }).compileToV0Message(alts);
128
147
 
129
148
  const tx = new VersionedTransaction(message);
130
149
  const allSigners = [payer, ...(signers ?? [])];
@@ -160,14 +179,38 @@ export async function sendTx(
160
179
  });
161
180
  }
162
181
 
163
- const confirmation = await connection.confirmTransaction(
164
- { signature, blockhash, lastValidBlockHeight },
165
- "confirmed"
166
- );
167
- if (confirmation.value.err) {
168
- throw new Error(
169
- `Transaction ${signature} failed: ${JSON.stringify(confirmation.value.err)}`
170
- );
182
+ // Poll for confirmation (avoid confirmTransaction which uses WebSocket)
183
+ const startTime = Date.now();
184
+ const TIMEOUT_MS = 20_000;
185
+ const POLL_INTERVAL_MS = 1_000;
186
+
187
+ while (Date.now() - startTime < TIMEOUT_MS) {
188
+ const currentBlockHeight = await connection.getBlockHeight("confirmed");
189
+ if (currentBlockHeight > lastValidBlockHeight) {
190
+ throw new Error(
191
+ `Transaction ${signature} expired: block height exceeded`
192
+ );
193
+ }
194
+
195
+ const statusResult = await connection.getSignatureStatuses([signature]);
196
+ const status = statusResult?.value?.[0];
197
+
198
+ if (status) {
199
+ if (status.err) {
200
+ throw new Error(
201
+ `Transaction ${signature} failed: ${JSON.stringify(status.err)}`
202
+ );
203
+ }
204
+ if (
205
+ status.confirmationStatus === "confirmed" ||
206
+ status.confirmationStatus === "finalized"
207
+ ) {
208
+ return signature;
209
+ }
210
+ }
211
+
212
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
171
213
  }
172
- return signature;
214
+
215
+ throw new Error(`Transaction ${signature} confirmation timeout`);
173
216
  }