@augustdigital/sdk 8.20.1 → 8.21.1

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.
@@ -7,7 +7,7 @@ import * as SolanaConstants from './constants';
7
7
  import * as SolanaGetters from './getters';
8
8
  import * as SolanaActions from './vault.actions';
9
9
  import type { AnchorProvider, web3 } from '@coral-xyz/anchor';
10
- import { Connection, PublicKey, type Transaction } from '@solana/web3.js';
10
+ import { type Commitment, Connection, PublicKey, type Transaction } from '@solana/web3.js';
11
11
  import type { ISolanaNetwork, ISolanaRpcEndpoint } from './types';
12
12
  import type { SendTransactionOptions } from '@solana/wallet-adapter-base';
13
13
  export declare const Solana: {
@@ -61,11 +61,11 @@ export declare const Solana: {
61
61
  fetchUserTokenBalance: ({ connection, publicKey, depositMint, }: {
62
62
  publicKey: PublicKey | string;
63
63
  depositMint?: PublicKey | string;
64
- } & import("./types").ISolanaConnectionOptions) => Promise<any>;
64
+ } & import("./types").ISolanaConnectionOptions) => Promise<string>;
65
65
  fetchUserShareBalance: ({ connection, publicKey, shareMint, }: {
66
66
  publicKey: PublicKey | string;
67
67
  shareMint?: PublicKey | string;
68
- } & import("./types").ISolanaConnectionOptions) => Promise<any>;
68
+ } & import("./types").ISolanaConnectionOptions) => Promise<number>;
69
69
  fetchUserShareBalanceRaw: ({ connection, publicKey, shareMint, }: {
70
70
  publicKey: PublicKey | string;
71
71
  shareMint?: PublicKey | string;
@@ -119,7 +119,23 @@ declare class SolanaAdapter {
119
119
  private _network;
120
120
  private _connection;
121
121
  private _provider;
122
- constructor(endpoint: ISolanaRpcEndpoint, network?: ISolanaNetwork);
122
+ /**
123
+ * @param commitment - Commitment for every read this adapter makes *and* for
124
+ * the confirmation of every write, so the two always describe the same
125
+ * chain state — the vault handlers take it from this same `Connection`.
126
+ *
127
+ * Defaults to `'finalized'`, which is what this adapter has always used in
128
+ * practice: it previously passed no commitment at all, so both reads and
129
+ * confirmations fell through to the RPC's own `'finalized'` default. The
130
+ * default is stated explicitly now rather than inherited, but it is
131
+ * unchanged.
132
+ *
133
+ * Pass `'confirmed'` for a markedly faster round trip — seconds rather
134
+ * than tens of seconds — accepting that the state you act on can still, in
135
+ * principle, be rolled back. Also settable as the `commitment` field of
136
+ * the `solana` config passed to `AugustSDK`.
137
+ */
138
+ constructor(endpoint: ISolanaRpcEndpoint, network?: ISolanaNetwork, commitment?: Commitment);
123
139
  get endpoint(): `https://${string}`;
124
140
  get network(): ISolanaNetwork;
125
141
  get connection(): web3.Connection;
@@ -171,8 +187,8 @@ declare class SolanaAdapter {
171
187
  image: string;
172
188
  }>;
173
189
  getTokenSymbol(mintAddress: string | PublicKey): Promise<string>;
174
- fetchUserTokenBalance(publicKey: PublicKey | string, depositMint: PublicKey | string): Promise<any>;
175
- fetchUserShareBalance(publicKey: PublicKey | string, shareMint: PublicKey | string): Promise<any>;
190
+ fetchUserTokenBalance(publicKey: PublicKey | string, depositMint: PublicKey | string): Promise<string>;
191
+ fetchUserShareBalance(publicKey: PublicKey | string, shareMint: PublicKey | string): Promise<number>;
176
192
  /**
177
193
  * BigInt-safe variant of {@link fetchUserShareBalance}. Returns the raw u64
178
194
  * `amount` (as a base-units string) plus the mint's `decimals` (or `null`
@@ -212,13 +228,31 @@ declare class SolanaAdapter {
212
228
  /**
213
229
  * Deposit funds into a Solana August vault.
214
230
  * @param depositAmount `bigint` (raw on-chain units) or `number` (UI amount).
231
+ * @param sendTransaction - **Ignored; scheduled for removal (AUGUST-7221).**
232
+ * Share-account creation is now prepended to the deposit instruction, so
233
+ * the SDK sends no second transaction and never invokes this callback —
234
+ * signing goes through the provider's wallet.
235
+ *
236
+ * **Pass `undefined` here; do not delete the argument.** It sits *before*
237
+ * `vaultAddress`, so removing it shifts your vault address into this slot
238
+ * and leaves `vaultAddress` undefined — which silently falls back to the
239
+ * legacy single-vault PDA derivation and targets a different vault.
215
240
  */
216
- vaultDeposit(vaultProgramId: PublicKey | string, idl: any, publicKey: PublicKey | string, depositAmount: number | bigint, sendTransaction: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>, vaultAddress?: PublicKey | string): Promise<any>;
241
+ vaultDeposit(vaultProgramId: PublicKey | string, idl: any, publicKey: PublicKey | string, depositAmount: number | bigint, sendTransaction?: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>, vaultAddress?: PublicKey | string): Promise<any>;
217
242
  /**
218
243
  * Redeem vault shares from a Solana August vault.
219
244
  * @param redeemShares `bigint` (raw share units) or `number` (UI amount).
245
+ * @param sendTransaction - **Ignored; scheduled for removal (AUGUST-7221).**
246
+ * Payout- and fee-recipient-account creation is now prepended to the redeem
247
+ * instruction, so the SDK sends no second transaction and never invokes
248
+ * this callback — signing goes through the provider's wallet.
249
+ *
250
+ * **Pass `undefined` here; do not delete the argument.** It sits *before*
251
+ * `vaultAddress`, so removing it shifts your vault address into this slot
252
+ * and leaves `vaultAddress` undefined — which silently falls back to the
253
+ * legacy single-vault PDA derivation and targets a different vault.
220
254
  */
221
- vaultRedeem(vaultProgramId: PublicKey | string, idl: any, publicKey: PublicKey | string, redeemShares: number | bigint, sendTransaction: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>, vaultAddress?: PublicKey | string): Promise<any>;
255
+ vaultRedeem(vaultProgramId: PublicKey | string, idl: any, publicKey: PublicKey | string, redeemShares: number | bigint, sendTransaction?: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>, vaultAddress?: PublicKey | string): Promise<any>;
222
256
  /**
223
257
  * Canonical program id for a program type on this adapter's network.
224
258
  *
@@ -68,10 +68,29 @@ class SolanaAdapter {
68
68
  _network;
69
69
  _connection;
70
70
  _provider;
71
- constructor(endpoint, network = utils_1.SolanaUtils.fallbackNetwork) {
71
+ /**
72
+ * @param commitment - Commitment for every read this adapter makes *and* for
73
+ * the confirmation of every write, so the two always describe the same
74
+ * chain state — the vault handlers take it from this same `Connection`.
75
+ *
76
+ * Defaults to `'finalized'`, which is what this adapter has always used in
77
+ * practice: it previously passed no commitment at all, so both reads and
78
+ * confirmations fell through to the RPC's own `'finalized'` default. The
79
+ * default is stated explicitly now rather than inherited, but it is
80
+ * unchanged.
81
+ *
82
+ * Pass `'confirmed'` for a markedly faster round trip — seconds rather
83
+ * than tens of seconds — accepting that the state you act on can still, in
84
+ * principle, be rolled back. Also settable as the `commitment` field of
85
+ * the `solana` config passed to `AugustSDK`.
86
+ */
87
+ constructor(endpoint, network = utils_1.SolanaUtils.fallbackNetwork, commitment = 'finalized') {
72
88
  this._endpoint = endpoint;
73
89
  this._network = network;
74
- const connection = new web3_js_1.Connection(endpoint);
90
+ // Explicit rather than inherited: left unset, web3.js omits the parameter
91
+ // and the RPC applies its own default, which is the same 'finalized' —
92
+ // but then nothing ties it to the commitment writes confirm at.
93
+ const connection = new web3_js_1.Connection(endpoint, commitment);
75
94
  this._connection = connection;
76
95
  this._provider = utils_1.SolanaUtils.getReadOnlyProvider({
77
96
  network: this._network,
@@ -238,6 +257,15 @@ class SolanaAdapter {
238
257
  /**
239
258
  * Deposit funds into a Solana August vault.
240
259
  * @param depositAmount `bigint` (raw on-chain units) or `number` (UI amount).
260
+ * @param sendTransaction - **Ignored; scheduled for removal (AUGUST-7221).**
261
+ * Share-account creation is now prepended to the deposit instruction, so
262
+ * the SDK sends no second transaction and never invokes this callback —
263
+ * signing goes through the provider's wallet.
264
+ *
265
+ * **Pass `undefined` here; do not delete the argument.** It sits *before*
266
+ * `vaultAddress`, so removing it shifts your vault address into this slot
267
+ * and leaves `vaultAddress` undefined — which silently falls back to the
268
+ * legacy single-vault PDA derivation and targets a different vault.
241
269
  */
242
270
  async vaultDeposit(vaultProgramId, idl, publicKey, depositAmount, sendTransaction, vaultAddress) {
243
271
  return await (0, vault_actions_1.handleSolanaDeposit)({
@@ -255,6 +283,15 @@ class SolanaAdapter {
255
283
  /**
256
284
  * Redeem vault shares from a Solana August vault.
257
285
  * @param redeemShares `bigint` (raw share units) or `number` (UI amount).
286
+ * @param sendTransaction - **Ignored; scheduled for removal (AUGUST-7221).**
287
+ * Payout- and fee-recipient-account creation is now prepended to the redeem
288
+ * instruction, so the SDK sends no second transaction and never invokes
289
+ * this callback — signing goes through the provider's wallet.
290
+ *
291
+ * **Pass `undefined` here; do not delete the argument.** It sits *before*
292
+ * `vaultAddress`, so removing it shifts your vault address into this slot
293
+ * and leaves `vaultAddress` undefined — which silently falls back to the
294
+ * legacy single-vault PDA derivation and targets a different vault.
258
295
  */
259
296
  async vaultRedeem(vaultProgramId, idl, publicKey, redeemShares, sendTransaction, vaultAddress) {
260
297
  return await (0, vault_actions_1.handleSolanaRedeem)({
@@ -1,5 +1,5 @@
1
1
  import { AnchorProvider, BN, Program, type web3 } from '@coral-xyz/anchor';
2
- import { PublicKey, type Transaction } from '@solana/web3.js';
2
+ import { PublicKey, type ParsedAccountData, type Transaction } from '@solana/web3.js';
3
3
  import type { ISolanaConnectionOptions, ISolanaVaultState } from './types';
4
4
  declare function getExplorerLink({ signature, type, network, }: {
5
5
  signature: string;
@@ -58,14 +58,65 @@ declare function getToken({ mintAddress, endpoint, connection, }: {
58
58
  declare function getTokenSymbol({ endpoint, mintAddress, }: {
59
59
  mintAddress: string | PublicKey;
60
60
  } & ISolanaConnectionOptions): Promise<string | null>;
61
+ /**
62
+ * A wallet's token account of record for one mint: the account holding the
63
+ * largest balance.
64
+ *
65
+ * **This is the single selection rule for the whole Solana adapter.** Reads
66
+ * (balances shown in a UI) and writes (the account a transfer actually names)
67
+ * must agree on which account represents the user, or the two describe
68
+ * different money: a balance read from account A while a deposit mints into
69
+ * account B looks, to the user, like a successful transaction that never
70
+ * arrived. Every reader in this file and both vault handlers call this, so the
71
+ * rule cannot drift.
72
+ *
73
+ * Largest-balance rather than "the first account the RPC returned":
74
+ * `getParsedTokenAccountsByOwner` guarantees no ordering, so taking `[0]` can
75
+ * select an empty leftover while the user's funds sit in a sibling account —
76
+ * which fails on-chain as an insufficient-funds revert *after* they sign.
77
+ *
78
+ * **Spendable accounts win over frozen ones**, regardless of balance: a token
79
+ * issuer can freeze an account, and neither a transfer out of nor a transfer
80
+ * into a frozen account can succeed. Picking the richest account outright would
81
+ * hand a frozen one to the program and revert with SPL `0x11` after the user
82
+ * signs, while a perfectly usable sibling sat next to it. When *every* account
83
+ * is frozen the richest frozen one is still returned, flagged — a reader should
84
+ * show that balance, because the user does own it, and a writer should refuse
85
+ * before asking for a signature.
86
+ *
87
+ * Internal to the adapter: exported for `vault.actions.ts`, deliberately not
88
+ * re-exported on `SolanaUtils`. It takes a raw RPC response shape, which is not
89
+ * a contract worth freezing on the public surface.
90
+ *
91
+ * @param accounts - The `.value` array from `getParsedTokenAccountsByOwner`.
92
+ * @returns The selected account with its raw balance, decimals, the RPC's own
93
+ * `uiAmount`, and whether it is frozen — or `undefined` when the wallet owns
94
+ * no account for the mint.
95
+ * @throws AugustSDKError when an account's balance cannot be read. An
96
+ * unreadable balance is *unknown*, not zero — scoring it zero would silently
97
+ * demote a funded account below a dust one and reinstate the `[0]` behaviour
98
+ * this function exists to prevent.
99
+ */
100
+ export declare function selectRichestTokenAccount(accounts: {
101
+ pubkey: PublicKey;
102
+ account: {
103
+ data: ParsedAccountData | Buffer;
104
+ };
105
+ }[]): {
106
+ pubkey: PublicKey;
107
+ amount: bigint;
108
+ decimals: number | null;
109
+ uiAmount: number | null;
110
+ frozen: boolean;
111
+ } | undefined;
61
112
  declare function fetchUserTokenBalance({ connection, publicKey, depositMint, }: {
62
113
  publicKey: PublicKey | string;
63
114
  depositMint?: PublicKey | string;
64
- } & ISolanaConnectionOptions): Promise<any>;
115
+ } & ISolanaConnectionOptions): Promise<string>;
65
116
  declare function fetchUserShareBalance({ connection, publicKey, shareMint, }: {
66
117
  publicKey: PublicKey | string;
67
118
  shareMint?: PublicKey | string;
68
- } & ISolanaConnectionOptions): Promise<any>;
119
+ } & ISolanaConnectionOptions): Promise<number>;
69
120
  /**
70
121
  * Same on-chain lookup as `fetchUserShareBalance`, but returns the raw u64
71
122
  * `amount` string and the mint's `decimals` — or `null` when the scale can't
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SolanaUtils = exports.isSolanaAddress = void 0;
4
+ exports.selectRichestTokenAccount = selectRichestTokenAccount;
4
5
  exports.uiAmountToRawBn = uiAmountToRawBn;
5
6
  const anchor_1 = require("@coral-xyz/anchor");
6
7
  const web3_js_1 = require("@solana/web3.js");
@@ -103,7 +104,11 @@ function getProvider({ connection, publicKey, signTransaction, }) {
103
104
  const signedTransactions = await Promise.all(txs.map(async (tx) => signTransaction(tx)));
104
105
  return signedTransactions;
105
106
  },
106
- }, { commitment: 'confirmed' });
107
+ },
108
+ // Inherited from the connection, not hard-coded: a caller who configured
109
+ // the adapter for 'finalized' must not have writes issued through
110
+ // `getProgram(...).methods…rpc()` confirm at something weaker.
111
+ { commitment: connection.commitment ?? 'finalized' });
107
112
  }
108
113
  // Read-only provider for data fetching without wallet connection
109
114
  function getReadOnlyProvider({ connection }) {
@@ -117,8 +122,10 @@ function getReadOnlyProvider({ connection }) {
117
122
  throw new core_1.AugustValidationError('INVALID_INPUT', 'Cannot sign transactions with read-only provider');
118
123
  },
119
124
  };
125
+ // See `getProvider` — the connection is the single source of truth for
126
+ // commitment, so reads through this provider match reads made directly.
120
127
  return new anchor_1.AnchorProvider(connection, dummyWallet, {
121
- commitment: 'confirmed',
128
+ commitment: connection.commitment ?? 'finalized',
122
129
  });
123
130
  }
124
131
  // ============================================================================
@@ -440,6 +447,87 @@ async function getTokenSymbol({ endpoint, mintAddress, }) {
440
447
  return null;
441
448
  }
442
449
  }
450
+ /**
451
+ * A wallet's token account of record for one mint: the account holding the
452
+ * largest balance.
453
+ *
454
+ * **This is the single selection rule for the whole Solana adapter.** Reads
455
+ * (balances shown in a UI) and writes (the account a transfer actually names)
456
+ * must agree on which account represents the user, or the two describe
457
+ * different money: a balance read from account A while a deposit mints into
458
+ * account B looks, to the user, like a successful transaction that never
459
+ * arrived. Every reader in this file and both vault handlers call this, so the
460
+ * rule cannot drift.
461
+ *
462
+ * Largest-balance rather than "the first account the RPC returned":
463
+ * `getParsedTokenAccountsByOwner` guarantees no ordering, so taking `[0]` can
464
+ * select an empty leftover while the user's funds sit in a sibling account —
465
+ * which fails on-chain as an insufficient-funds revert *after* they sign.
466
+ *
467
+ * **Spendable accounts win over frozen ones**, regardless of balance: a token
468
+ * issuer can freeze an account, and neither a transfer out of nor a transfer
469
+ * into a frozen account can succeed. Picking the richest account outright would
470
+ * hand a frozen one to the program and revert with SPL `0x11` after the user
471
+ * signs, while a perfectly usable sibling sat next to it. When *every* account
472
+ * is frozen the richest frozen one is still returned, flagged — a reader should
473
+ * show that balance, because the user does own it, and a writer should refuse
474
+ * before asking for a signature.
475
+ *
476
+ * Internal to the adapter: exported for `vault.actions.ts`, deliberately not
477
+ * re-exported on `SolanaUtils`. It takes a raw RPC response shape, which is not
478
+ * a contract worth freezing on the public surface.
479
+ *
480
+ * @param accounts - The `.value` array from `getParsedTokenAccountsByOwner`.
481
+ * @returns The selected account with its raw balance, decimals, the RPC's own
482
+ * `uiAmount`, and whether it is frozen — or `undefined` when the wallet owns
483
+ * no account for the mint.
484
+ * @throws AugustSDKError when an account's balance cannot be read. An
485
+ * unreadable balance is *unknown*, not zero — scoring it zero would silently
486
+ * demote a funded account below a dust one and reinstate the `[0]` behaviour
487
+ * this function exists to prevent.
488
+ */
489
+ function selectRichestTokenAccount(accounts) {
490
+ const parsed = accounts.map((entry) => {
491
+ const info = entry.account.data?.parsed?.info;
492
+ const tokenAmount = info?.tokenAmount;
493
+ const raw = tokenAmount?.amount;
494
+ // A u64 string is the only shape the RPC promises here. Anything else
495
+ // (a base64 `data` the node could not jsonParse, a reshaping proxy) means
496
+ // we do not know this balance.
497
+ if (typeof raw !== 'string' || !/^\d+$/.test(raw)) {
498
+ throw new core_1.AugustSDKError('UNKNOWN', `Could not read the balance of token account ${entry.pubkey.toBase58()}. ` +
499
+ 'The RPC returned an unrecognized response; please retry or switch RPC.', {
500
+ context: {
501
+ account: entry.pubkey.toBase58(),
502
+ rawAmount: String(raw),
503
+ },
504
+ });
505
+ }
506
+ return {
507
+ pubkey: entry.pubkey,
508
+ amount: BigInt(raw),
509
+ decimals: typeof tokenAmount?.decimals === 'number' ? tokenAmount.decimals : null,
510
+ // Passed through from the RPC rather than recomputed from `amount`, so
511
+ // the lossy float these legacy readers return is not made lossier.
512
+ uiAmount: typeof tokenAmount?.uiAmount === 'number' ? tokenAmount.uiAmount : null,
513
+ frozen: info?.state === 'frozen',
514
+ };
515
+ });
516
+ const richest = (candidates) => candidates.reduce((best, candidate) => {
517
+ if (!best)
518
+ return candidate;
519
+ if (candidate.amount !== best.amount)
520
+ return candidate.amount > best.amount ? candidate : best;
521
+ // Equal balances: break the tie on the address. Falling back to "whichever
522
+ // the RPC listed first" would leave exactly the ordering dependence this
523
+ // function exists to remove — harmless while the balances match, but it
524
+ // means two calls can name different accounts for the same wallet.
525
+ return candidate.pubkey.toBase58() < best.pubkey.toBase58()
526
+ ? candidate
527
+ : best;
528
+ }, undefined);
529
+ return richest(parsed.filter((a) => !a.frozen)) ?? richest(parsed);
530
+ }
443
531
  async function fetchUserTokenBalance({ connection, publicKey, depositMint, }) {
444
532
  if (!publicKey || !depositMint)
445
533
  return '0';
@@ -449,12 +537,9 @@ async function fetchUserTokenBalance({ connection, publicKey, depositMint, }) {
449
537
  const tokenAccounts = await connection.getParsedTokenAccountsByOwner(_publicKey, {
450
538
  mint: _depositMint,
451
539
  });
452
- if (tokenAccounts.value.length > 0) {
453
- const balance = tokenAccounts?.value?.[0]?.account.data.parsed.info.tokenAmount
454
- .uiAmount;
455
- return balance?.toString() || '0';
456
- }
457
- return '0';
540
+ // Same rule the write path uses — see `selectRichestTokenAccount`.
541
+ const richest = selectRichestTokenAccount(tokenAccounts.value);
542
+ return richest?.uiAmount?.toString() || '0';
458
543
  }
459
544
  catch (e) {
460
545
  const error = e;
@@ -473,12 +558,9 @@ async function fetchUserShareBalance({ connection, publicKey, shareMint, }) {
473
558
  const shareAccounts = await connection.getParsedTokenAccountsByOwner(_publicKey, {
474
559
  mint: _shareMint,
475
560
  });
476
- if (shareAccounts.value.length > 0) {
477
- const balance = shareAccounts?.value?.[0]?.account.data.parsed.info.tokenAmount
478
- .uiAmount;
479
- return balance ?? 0;
480
- }
481
- return 0;
561
+ // Same rule the write path uses — see `selectRichestTokenAccount`.
562
+ const richest = selectRichestTokenAccount(shareAccounts.value);
563
+ return richest?.uiAmount ?? 0;
482
564
  }
483
565
  catch (e) {
484
566
  const error = e;
@@ -519,12 +601,13 @@ async function fetchUserShareBalanceRaw({ connection, publicKey, shareMint, }) {
519
601
  const shareAccounts = await connection.getParsedTokenAccountsByOwner(_publicKey, {
520
602
  mint: _shareMint,
521
603
  });
522
- const tokenAmount = shareAccounts?.value?.[0]?.account?.data?.parsed?.info?.tokenAmount;
523
- if (!tokenAmount)
604
+ // Same rule the write path uses — see `selectRichestTokenAccount`.
605
+ const richest = selectRichestTokenAccount(shareAccounts?.value ?? []);
606
+ if (!richest)
524
607
  return { amount: '0', decimals: null };
525
608
  return {
526
- amount: String(tokenAmount.amount ?? '0'),
527
- decimals: typeof tokenAmount.decimals === 'number' ? tokenAmount.decimals : null,
609
+ amount: richest.amount.toString(),
610
+ decimals: richest.decimals,
528
611
  };
529
612
  }
530
613
  catch (e) {
@@ -1,5 +1,5 @@
1
1
  import { type web3 } from '@coral-xyz/anchor';
2
- import { type Connection, PublicKey, Transaction } from '@solana/web3.js';
2
+ import { type Connection, PublicKey, type Transaction } from '@solana/web3.js';
3
3
  import type { ISolanaConnectionOptions } from './types';
4
4
  import type { SendTransactionOptions } from '@solana/wallet-adapter-base';
5
5
  /**
@@ -26,18 +26,33 @@ export declare function describeSolanaError(e: unknown): string;
26
26
  * @param depositAmount `bigint` is treated as raw on-chain units (preferred
27
27
  * — no JS-float round-trip). `number` is treated as a UI amount and
28
28
  * scaled by the deposit mint's decimals via {@link uiAmountToRawBn}.
29
- * @throws AugustValidationError on missing wallet/programId or invalid amount.
29
+ * @throws AugustValidationError on missing wallet/programId or invalid amount,
30
+ * or when the wallet's deposit-mint balance is below `depositAmount`.
31
+ * @throws AugustSDKError when a candidate token account's balance cannot be
32
+ * read, or wrapping any downstream failure (`cause` preserved).
33
+ * @remarks **Side effects.** Submits exactly one transaction, signed by the
34
+ * provider's wallet. When the wallet has no share account, that transaction
35
+ * also creates one, and `publicKey` pays its rent — roughly 0.00204 SOL,
36
+ * non-refundable while the account stays open — on top of the network fee.
37
+ * Callers should ensure the wallet holds enough SOL for both before
38
+ * prompting the user to sign.
30
39
  * @remarks The on-chain `deposit` instruction has no `min_shares_out`
31
40
  * parameter; slippage cannot be enforced on-chain until the program ships
32
41
  * that argument.
33
42
  */
34
- export declare function handleSolanaDeposit({ provider, connection, network, vaultProgramId, vaultAddress, depositAmount, publicKey, sendTransaction, idl, }: {
43
+ export declare function handleSolanaDeposit({ provider, connection, network, vaultProgramId, vaultAddress, depositAmount, publicKey, idl, }: {
35
44
  vaultProgramId: PublicKey | string;
36
45
  vaultAddress?: PublicKey | string;
37
46
  idl: any;
38
47
  publicKey: PublicKey | string;
39
48
  depositAmount: number | bigint;
40
- sendTransaction: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>;
49
+ /**
50
+ * @deprecated Accepted and ignored. Share-account creation is now prepended
51
+ * to the deposit instruction, so there is no longer a second transaction to
52
+ * send. Kept so existing callers keep compiling; will be removed in the next
53
+ * major.
54
+ */
55
+ sendTransaction?: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>;
41
56
  } & ISolanaConnectionOptions): Promise<any>;
42
57
  /**
43
58
  * Redeem vault shares back into the underlying mint on a Solana August vault.
@@ -45,13 +60,28 @@ export declare function handleSolanaDeposit({ provider, connection, network, vau
45
60
  * @param redeemShares `bigint` is treated as raw share-token units;
46
61
  * `number` is treated as a UI amount. See {@link handleSolanaDeposit} for
47
62
  * the same caveat about the missing on-chain slippage guard.
48
- * @throws AugustValidationError on missing wallet/programId or invalid amount.
63
+ * @throws AugustValidationError on missing wallet/programId or invalid amount,
64
+ * or when the wallet holds fewer shares than `redeemShares`.
65
+ * @throws AugustSDKError when a candidate token account's balance cannot be
66
+ * read, or wrapping any downstream failure (`cause` preserved).
67
+ * @remarks **Side effects.** Submits exactly one transaction, signed by the
68
+ * provider's wallet. That transaction also creates the payout account when
69
+ * the wallet has none, and the vault's fee-recipient account when *it* has
70
+ * none — `publicKey` pays rent for both, roughly 0.00204 SOL each and
71
+ * non-refundable, on top of the network fee. Callers should ensure the
72
+ * wallet holds enough SOL for all of it before prompting the user to sign.
49
73
  */
50
- export declare function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultAddress, publicKey, redeemShares, sendTransaction, idl, }: {
74
+ export declare function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultAddress, publicKey, redeemShares, idl, }: {
51
75
  idl: any;
52
76
  vaultProgramId: PublicKey | string;
53
77
  vaultAddress?: PublicKey | string;
54
78
  publicKey: PublicKey | string;
55
79
  redeemShares: number | bigint;
56
- sendTransaction: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>;
80
+ /**
81
+ * @deprecated Accepted and ignored. Payout- and fee-recipient-account
82
+ * creation is now prepended to the redeem instruction, so there is no longer
83
+ * a second transaction to send. Kept so existing callers keep compiling;
84
+ * will be removed in the next major.
85
+ */
86
+ sendTransaction?: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>;
57
87
  } & ISolanaConnectionOptions): Promise<any>;
@@ -49,18 +49,114 @@ function describeSolanaError(e) {
49
49
  ? str
50
50
  : REASONLESS_SOLANA_ERROR;
51
51
  }
52
+ /**
53
+ * Pick which token account to hand the vault program for a given mint, and
54
+ * describe how to create it when the user has none.
55
+ *
56
+ * The vault program constrains these accounts by `token::mint` +
57
+ * `token::authority` (not `associated_token::…`), so any account the signer
58
+ * owns qualifies. We therefore keep an existing account when there is one —
59
+ * that is where the user's balance already lives — and fall back to the
60
+ * associated token account otherwise.
61
+ *
62
+ * Two deliberate choices:
63
+ *
64
+ * - **The create is returned, not sent.** Prepended to the deposit/redeem via
65
+ * `.preInstructions()` it is atomic with the transfer, so there is no window
66
+ * in which the vault instruction can reference an account that has not
67
+ * landed. Sent as its own transaction — as this module used to — it raced
68
+ * its own confirmation. Observed in production (AUGUST-7221, 2026-08-11):
69
+ * a deposit failed with `Simulation failed … Logs: []` while the share-ATA
70
+ * create was still in flight, then succeeded 17s later on retry. The empty
71
+ * log array means the transaction never reached program execution, so there
72
+ * was nothing on-chain to debug from.
73
+ * - **Idempotent.** `createAssociatedTokenAccountIdempotentInstruction` is a
74
+ * no-op when the account already exists, so a concurrent create — another
75
+ * tab, a wallet's auto-provisioning — cannot turn into an
76
+ * `SystemError::AccountAlreadyInUse` ("already in use") failure between our
77
+ * read and our submit.
78
+ *
79
+ * Which account wins when several exist is decided by
80
+ * {@link selectRichestTokenAccount} — the same rule every balance *read* in
81
+ * this adapter uses, so the account a UI reports and the account a transfer
82
+ * names are never different accounts.
83
+ *
84
+ * Classic SPL Token only: the ATA is derived under `TOKEN_PROGRAM_ID`, matching
85
+ * the `tokenProgram` both handlers pass, even though the program's
86
+ * `TokenInterface` would also accept Token-2022.
87
+ *
88
+ * @param owner - Wallet that owns the account and pays rent for any creation.
89
+ * @throws AugustSDKError when a candidate account's balance cannot be read
90
+ * (propagated from the shared selector), and `TokenOwnerOffCurveError` when
91
+ * `owner` is off-curve and an ATA has to be derived.
92
+ */
93
+ async function resolveUserTokenAccount({ connection, owner, mint, }) {
94
+ const existing = await connection.getParsedTokenAccountsByOwner(owner, {
95
+ mint,
96
+ });
97
+ const richest = (0, utils_1.selectRichestTokenAccount)(existing.value);
98
+ if (richest)
99
+ return {
100
+ address: richest.pubkey,
101
+ balance: richest.amount,
102
+ frozen: richest.frozen,
103
+ };
104
+ const ata = await (0, spl_token_1.getAssociatedTokenAddress)(mint, owner);
105
+ return {
106
+ address: ata,
107
+ balance: BigInt(0),
108
+ frozen: false,
109
+ createIx: (0, spl_token_1.createAssociatedTokenAccountIdempotentInstruction)(owner, // payer
110
+ ata, // associated token account
111
+ owner, // owner
112
+ mint),
113
+ };
114
+ }
115
+ /**
116
+ * Resolve the two token accounts a vault instruction needs, concurrently.
117
+ *
118
+ * The lookups are independent, so running them in sequence doubles the
119
+ * round-trip latency on the hot path for no benefit — both results are needed
120
+ * before anything is submitted either way.
121
+ *
122
+ * Rejections are surfaced in argument order rather than in whichever order the
123
+ * RPC happened to fail, so the error a user sees does not depend on network
124
+ * timing. `Promise.all` would reject with whichever call failed *first*, which
125
+ * on a malformed RPC response (both lookups throwing) would name a different
126
+ * account run to run.
127
+ *
128
+ * @param mints - `[first, second]`; the returned tuple matches this order.
129
+ */
130
+ async function resolveUserTokenAccountPair({ connection, owner, mints, }) {
131
+ const settled = await Promise.allSettled(mints.map((mint) => resolveUserTokenAccount({ connection, owner, mint })));
132
+ const resolved = settled.map((outcome) => {
133
+ if (outcome.status === 'rejected')
134
+ throw outcome.reason;
135
+ return outcome.value;
136
+ });
137
+ return [resolved[0], resolved[1]];
138
+ }
52
139
  /**
53
140
  * Deposit funds into a Solana August vault and mint share tokens.
54
141
  *
55
142
  * @param depositAmount `bigint` is treated as raw on-chain units (preferred
56
143
  * — no JS-float round-trip). `number` is treated as a UI amount and
57
144
  * scaled by the deposit mint's decimals via {@link uiAmountToRawBn}.
58
- * @throws AugustValidationError on missing wallet/programId or invalid amount.
145
+ * @throws AugustValidationError on missing wallet/programId or invalid amount,
146
+ * or when the wallet's deposit-mint balance is below `depositAmount`.
147
+ * @throws AugustSDKError when a candidate token account's balance cannot be
148
+ * read, or wrapping any downstream failure (`cause` preserved).
149
+ * @remarks **Side effects.** Submits exactly one transaction, signed by the
150
+ * provider's wallet. When the wallet has no share account, that transaction
151
+ * also creates one, and `publicKey` pays its rent — roughly 0.00204 SOL,
152
+ * non-refundable while the account stays open — on top of the network fee.
153
+ * Callers should ensure the wallet holds enough SOL for both before
154
+ * prompting the user to sign.
59
155
  * @remarks The on-chain `deposit` instruction has no `min_shares_out`
60
156
  * parameter; slippage cannot be enforced on-chain until the program ships
61
157
  * that argument.
62
158
  */
63
- async function handleSolanaDeposit({ provider, connection, network = constants_1.fallbackNetwork, vaultProgramId, vaultAddress, depositAmount, publicKey, sendTransaction, idl, }) {
159
+ async function handleSolanaDeposit({ provider, connection, network = constants_1.fallbackNetwork, vaultProgramId, vaultAddress, depositAmount, publicKey, idl, }) {
64
160
  try {
65
161
  if (!publicKey)
66
162
  throw new core_1.AugustValidationError('INVALID_INPUT', 'handleSolanaDeposit: wallet not connected');
@@ -119,50 +215,70 @@ async function handleSolanaDeposit({ provider, connection, network = constants_1
119
215
  }
120
216
  depositAmountRaw = (0, utils_1.uiAmountToRawBn)(depositAmount, decimals);
121
217
  }
122
- // Find user's token accounts
123
- const userTokenAccounts = await connection.getParsedTokenAccountsByOwner(_publicKey, {
124
- mint: _depositMint,
125
- });
126
- core_1.Logger.log.info('handleSolanaDeposit', 'User token accounts', userTokenAccounts);
127
- const userShareAccounts = await connection.getParsedTokenAccountsByOwner(_publicKey, {
128
- mint: shareMintAddr,
218
+ // Re-check *after* scaling. The guard above tests the caller's number, but
219
+ // a positive UI amount below one raw unit (1e-7 into a 6-decimal mint)
220
+ // truncates to zero here. Zero would then pass the balance gate below
221
+ // (`0n < 0n` is false) and submit a no-op transfer — from an account that
222
+ // may not even exist, since a zero balance is also what an absent account
223
+ // reports.
224
+ if (depositAmountRaw.lten(0)) {
225
+ throw new core_1.AugustValidationError('INVALID_INPUT', `Deposit amount is too small: ${depositAmount} is below one raw unit ` +
226
+ `of a ${decimals}-decimal mint.`);
227
+ }
228
+ // Match whatever this connection *reads* at. A `Connection` built without
229
+ // a commitment omits the parameter, so its reads take the RPC's default —
230
+ // `finalized`. Confirming writes at `confirmed` there would put reads
231
+ // behind writes again, which is the mismatch this whole change removes.
232
+ // `SolanaAdapter` always sets one explicitly, so this fallback only
233
+ // applies to callers reaching these handlers directly via `Solana.actions`.
234
+ const txCommitment = connection.commitment ?? 'finalized';
235
+ // `depositAccount` funds the transfer; `shareAccount` receives the minted
236
+ // shares. Independent lookups, so they run concurrently — but the guards
237
+ // below stay in a fixed order so the first problem a user hits is always
238
+ // reported the same way.
239
+ const [depositAccount, shareAccount] = await resolveUserTokenAccountPair({
240
+ connection,
241
+ owner: _publicKey,
242
+ mints: [_depositMint, shareMintAddr],
129
243
  });
130
- core_1.Logger.log.info('handleSolanaDeposit', 'User share accounts', userShareAccounts);
131
- if (userTokenAccounts.value.length === 0) {
132
- throw new core_1.AugustValidationError('INVALID_INPUT', 'No token account found for deposit mint. Please create a token account first.');
244
+ // Gate on the balance, not on whether an account exists: an empty leftover
245
+ // account is a real state, and letting it through only trades this error
246
+ // for an insufficient-funds revert the user pays a signature to discover.
247
+ // The balance is already in hand from the lookup, so this costs no extra
248
+ // RPC.
249
+ if (depositAccount.frozen) {
250
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'Your token account for the deposit mint is frozen by the token issuer, ' +
251
+ 'so the deposit cannot be transferred out of it.');
133
252
  }
134
- let senderShareAccount;
135
- if (userShareAccounts.value.length === 0) {
136
- core_1.Logger.log.info('handleSolanaDeposit', 'Creating share account');
137
- // Create share account if it doesn't exist
138
- senderShareAccount = await (0, spl_token_1.getAssociatedTokenAddress)(shareMintAddr, _publicKey);
139
- core_1.Logger.log.info('handleSolanaDeposit', 'Sender share account', senderShareAccount.toBase58());
140
- // Create the associated token account
141
- const createAtaIx = (0, spl_token_1.createAssociatedTokenAccountInstruction)(_publicKey, // payer
142
- senderShareAccount, // associated token account
143
- _publicKey, // owner
144
- shareMintAddr);
145
- core_1.Logger.log.info('handleSolanaDeposit', 'Create ATA Ix', createAtaIx.programId.toBase58());
146
- const transaction = new web3_js_1.Transaction().add(createAtaIx);
147
- const signature = await sendTransaction(transaction, connection);
148
- core_1.Logger.log.info('handleSolanaDeposit', 'Created share account:', signature.toString());
253
+ if (depositAccount.balance < BigInt(depositAmountRaw.toString())) {
254
+ throw new core_1.AugustValidationError('INVALID_INPUT', `Insufficient balance to deposit: this wallet holds ${depositAccount.balance} ` +
255
+ `raw units of the deposit mint, and the deposit requires ${depositAmountRaw.toString()}.`);
149
256
  }
150
- else {
151
- senderShareAccount = userShareAccounts.value[0]?.pubkey;
257
+ const senderTokenAccount = depositAccount.address;
258
+ // A missing share account is routine — every first-time depositor starts
259
+ // here, as does anyone whose account was closed after redeeming their full
260
+ // balance — so it is created inline rather than rejecting the deposit.
261
+ // Minting into a *frozen* one fails just as transferring out of one does,
262
+ // so this destination needs the same guard as the funding account above.
263
+ if (shareAccount.frozen) {
264
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'Your share account is frozen by the token issuer, so it cannot ' +
265
+ 'receive the shares this deposit would mint.');
152
266
  }
153
- const senderTokenAccount = userTokenAccounts.value[0]?.pubkey;
154
- core_1.Logger.log.info('handleSolanaDeposit', 'Sender token account', senderTokenAccount);
267
+ const senderShareAccount = shareAccount.address;
268
+ const preInstructions = shareAccount.createIx
269
+ ? [shareAccount.createIx]
270
+ : [];
271
+ core_1.Logger.log.info('handleSolanaDeposit', 'Sender token account', senderTokenAccount.toBase58());
272
+ core_1.Logger.log.info('handleSolanaDeposit', shareAccount.createIx
273
+ ? 'Sender share account (created in this tx)'
274
+ : 'Sender share account (existing)', senderShareAccount.toBase58());
155
275
  core_1.Logger.log.info('handleSolanaDeposit', '\n\nDeposit TX Params:\n');
156
276
  core_1.Logger.log.info('handleSolanaDeposit', 'Program', depositAmountRaw);
157
277
  core_1.Logger.log.info('handleSolanaDeposit', 'Vault state PDA', vaultStatePda.toBase58());
158
278
  core_1.Logger.log.info('handleSolanaDeposit', 'Vault token ATA PDA', vaultTokenAtaPda.toBase58());
159
- core_1.Logger.log.info('handleSolanaDeposit', 'Sender token account', senderTokenAccount?.toBase58());
160
- core_1.Logger.log.info('handleSolanaDeposit', 'Sender share account', senderShareAccount?.toBase58());
161
279
  core_1.Logger.log.info('handleSolanaDeposit', 'Share mint', shareMintAddr.toBase58());
162
280
  core_1.Logger.log.info('handleSolanaDeposit', 'Deposit mint', _depositMint?.toBase58());
163
281
  core_1.Logger.log.info('handleSolanaDeposit', 'Signer', _publicKey.toBase58());
164
- // Get a fresh blockhash to avoid "Blockhash not found" errors
165
- const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
166
282
  // TODO(slippage): pass `min_shares_out` here when the program ships it.
167
283
  const tx = await program.methods
168
284
  .deposit(depositAmountRaw)
@@ -172,21 +288,25 @@ async function handleSolanaDeposit({ provider, connection, network = constants_1
172
288
  senderTokenAccount,
173
289
  senderShareAccount,
174
290
  shareMint: shareMintAddr,
175
- depositMint: depositMint,
176
- signer: publicKey,
291
+ depositMint: _depositMint,
292
+ signer: _publicKey,
177
293
  tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
178
294
  })
295
+ .preInstructions(preInstructions)
296
+ // `.rpc()` builds, signs, submits and confirms in one step. The extra
297
+ // `getLatestBlockhash` + `confirmTransaction` this module used to run
298
+ // afterwards re-confirmed an already-confirmed signature against a
299
+ // blockhash the transaction never carried.
300
+ //
301
+ // Commitment is taken from the same `Connection` every read in this
302
+ // adapter uses, so a caller who asks for 'finalized' gets it on both
303
+ // sides rather than finalized reads racing confirmed writes.
179
304
  .rpc({
180
305
  skipPreflight: false,
181
- preflightCommitment: 'confirmed',
306
+ preflightCommitment: txCommitment,
307
+ commitment: txCommitment,
182
308
  });
183
309
  core_1.Logger.log.info('handleSolanaDeposit', 'Deposit successful:', tx);
184
- // Wait for confirmation with the blockhash we used
185
- await connection.confirmTransaction({
186
- signature: tx,
187
- blockhash,
188
- lastValidBlockHeight,
189
- }, 'confirmed');
190
310
  return tx;
191
311
  }
192
312
  catch (e) {
@@ -216,9 +336,18 @@ async function handleSolanaDeposit({ provider, connection, network = constants_1
216
336
  * @param redeemShares `bigint` is treated as raw share-token units;
217
337
  * `number` is treated as a UI amount. See {@link handleSolanaDeposit} for
218
338
  * the same caveat about the missing on-chain slippage guard.
219
- * @throws AugustValidationError on missing wallet/programId or invalid amount.
339
+ * @throws AugustValidationError on missing wallet/programId or invalid amount,
340
+ * or when the wallet holds fewer shares than `redeemShares`.
341
+ * @throws AugustSDKError when a candidate token account's balance cannot be
342
+ * read, or wrapping any downstream failure (`cause` preserved).
343
+ * @remarks **Side effects.** Submits exactly one transaction, signed by the
344
+ * provider's wallet. That transaction also creates the payout account when
345
+ * the wallet has none, and the vault's fee-recipient account when *it* has
346
+ * none — `publicKey` pays rent for both, roughly 0.00204 SOL each and
347
+ * non-refundable, on top of the network fee. Callers should ensure the
348
+ * wallet holds enough SOL for all of it before prompting the user to sign.
220
349
  */
221
- async function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultAddress, publicKey, redeemShares, sendTransaction, idl, }) {
350
+ async function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultAddress, publicKey, redeemShares, idl, }) {
222
351
  try {
223
352
  if (!publicKey)
224
353
  throw new core_1.AugustValidationError('INVALID_INPUT', 'handleSolanaRedeem: wallet not connected');
@@ -276,21 +405,48 @@ async function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultA
276
405
  }
277
406
  redeemSharesRaw = (0, utils_1.uiAmountToRawBn)(redeemShares, shareDecimals);
278
407
  }
279
- // Find user's token accounts
280
- const userTokenAccounts = await connection.getParsedTokenAccountsByOwner(_publicKey, {
281
- mint: _depositMint,
282
- });
283
- const userShareAccounts = await connection.getParsedTokenAccountsByOwner(_publicKey, {
284
- mint: shareMintAddr,
408
+ // See `handleSolanaDeposit` a positive UI amount can truncate to zero,
409
+ // which would pass the share-balance gate below and burn nothing.
410
+ if (redeemSharesRaw.lten(0)) {
411
+ throw new core_1.AugustValidationError('INVALID_INPUT', `Redemption amount is too small: ${redeemShares} is below one raw unit ` +
412
+ `of a ${shareDecimals}-decimal share mint.`);
413
+ }
414
+ // Match whatever this connection *reads* at. A `Connection` built without
415
+ // a commitment omits the parameter, so its reads take the RPC's default —
416
+ // `finalized`. Confirming writes at `confirmed` there would put reads
417
+ // behind writes again, which is the mismatch this whole change removes.
418
+ // `SolanaAdapter` always sets one explicitly, so this fallback only
419
+ // applies to callers reaching these handlers directly via `Solana.actions`.
420
+ const txCommitment = connection.commitment ?? 'finalized';
421
+ // `shareAccount` is burned from; `payoutAccount` receives the underlying.
422
+ // Independent lookups, so they run concurrently — guards stay ordered.
423
+ const [shareAccount, payoutAccount] = await resolveUserTokenAccountPair({
424
+ connection,
425
+ owner: _publicKey,
426
+ mints: [shareMintAddr, _depositMint],
285
427
  });
286
- if (userTokenAccounts.value.length === 0) {
287
- throw new core_1.AugustValidationError('INVALID_INPUT', 'No token account found for deposit mint. Please create a token account first.');
428
+ // Gated on balance for the same reason as the deposit's funding account.
429
+ if (shareAccount.frozen) {
430
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'Your share account is frozen by the token issuer, so these shares ' +
431
+ 'cannot be redeemed.');
432
+ }
433
+ if (shareAccount.balance < BigInt(redeemSharesRaw.toString())) {
434
+ throw new core_1.AugustValidationError('INVALID_INPUT', `Insufficient shares to redeem: this wallet holds ${shareAccount.balance} ` +
435
+ `raw share units, and the redemption requires ${redeemSharesRaw.toString()}.`);
288
436
  }
289
- if (userShareAccounts.value.length === 0) {
290
- throw new core_1.AugustValidationError('INVALID_INPUT', 'No share account found. You need to deposit first to get shares before you can redeem.');
437
+ const senderShareAccount = shareAccount.address;
438
+ // Redeeming is precisely how a holder first receives the deposit mint, so
439
+ // requiring the payout account to pre-exist (as this module used to) left
440
+ // shares unredeemable through this SDK for everyone who acquired them any
441
+ // way other than depositing from this same wallet — a transfer, an
442
+ // airdrop, a market buy — as well as any depositor who later closed their
443
+ // deposit-mint account. A frozen account cannot receive either, so the
444
+ // payout would revert.
445
+ if (payoutAccount.frozen) {
446
+ throw new core_1.AugustValidationError('INVALID_INPUT', 'Your token account for the deposit mint is frozen by the token issuer, ' +
447
+ 'so it cannot receive the redemption proceeds.');
291
448
  }
292
- const senderTokenAccount = userTokenAccounts?.value[0]?.pubkey;
293
- const senderShareAccount = userShareAccounts?.value[0]?.pubkey;
449
+ const senderTokenAccount = payoutAccount.address;
294
450
  // Get the fee recipient from vault state
295
451
  const readOnlyProvider = utils_1.SolanaUtils.getReadOnlyProvider({ connection });
296
452
  const readOnlyProgram = utils_1.SolanaUtils.getProgram({
@@ -301,22 +457,20 @@ async function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultA
301
457
  const vaultStateData = await readOnlyProgram.account.vaultState.fetch(vaultStatePda);
302
458
  // Get the fee recipient's token account
303
459
  const feeRecipientTokenAccount = await (0, spl_token_1.getAssociatedTokenAddress)(_depositMint, vaultStateData.feeRecipient);
304
- // Check if the fee recipient token account exists, if not create it
305
- const feeRecipientAccountInfo = await connection.getAccountInfo(feeRecipientTokenAccount);
306
- if (!feeRecipientAccountInfo) {
307
- const createFeeRecipientAtaIx = (0, spl_token_1.createAssociatedTokenAccountInstruction)(_publicKey, // payer (user pays for the account creation)
460
+ // Every account this handler might have to create is created in the same
461
+ // transaction that uses it. The fee recipient's ATA is created
462
+ // unconditionally: the idempotent instruction is a no-op when the account
463
+ // already exists (costing the user nothing), which buys us one fewer RPC
464
+ // round trip and removes the read-then-create window entirely. The user
465
+ // pays the rent when it is genuinely absent, as before.
466
+ const preInstructions = [
467
+ (0, spl_token_1.createAssociatedTokenAccountIdempotentInstruction)(_publicKey, // payer (user pays for the account creation)
308
468
  feeRecipientTokenAccount, // associated token account
309
469
  vaultStateData.feeRecipient, // owner (the actual fee recipient)
310
- _depositMint);
311
- const createAccountTx = new web3_js_1.Transaction().add(createFeeRecipientAtaIx);
312
- const createAccountSignature = await sendTransaction(createAccountTx, connection);
313
- core_1.Logger.log.info('handleSolanaRedeem', 'Fee recipient token account created:', createAccountSignature);
314
- // Wait a moment for the account to be created
315
- await new Promise((resolve) => setTimeout(resolve, 1000));
316
- }
317
- else {
318
- core_1.Logger.log.info('handleSolanaRedeem', 'Fee recipient token account already exists');
319
- }
470
+ _depositMint),
471
+ ];
472
+ if (payoutAccount.createIx)
473
+ preInstructions.push(payoutAccount.createIx);
320
474
  core_1.Logger.log.info('handleSolanaRedeem', '\n\nRedeem TX Params:\n');
321
475
  core_1.Logger.log.info('handleSolanaRedeem', 'Redeem shares (UI):', redeemShares);
322
476
  core_1.Logger.log.info('handleSolanaRedeem', 'Redeem shares (raw):', redeemSharesRaw);
@@ -329,8 +483,6 @@ async function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultA
329
483
  core_1.Logger.log.info('handleSolanaRedeem', 'Share mint:', shareMintAddr.toBase58());
330
484
  core_1.Logger.log.info('handleSolanaRedeem', 'Deposit mint:', _depositMint?.toBase58());
331
485
  core_1.Logger.log.info('handleSolanaRedeem', 'Signer:', _publicKey.toBase58());
332
- // Get a fresh blockhash to avoid "Blockhash not found" errors
333
- const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
334
486
  const tx = await program.methods
335
487
  .redeem(redeemSharesRaw)
336
488
  .accounts({
@@ -344,17 +496,15 @@ async function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultA
344
496
  signer: _publicKey,
345
497
  tokenProgram: spl_token_1.TOKEN_PROGRAM_ID,
346
498
  })
499
+ .preInstructions(preInstructions)
500
+ // See `handleSolanaDeposit` for why `.rpc()` owns the confirmation and
501
+ // why the commitment comes from the connection.
347
502
  .rpc({
348
503
  skipPreflight: false,
349
- preflightCommitment: 'confirmed',
504
+ preflightCommitment: txCommitment,
505
+ commitment: txCommitment,
350
506
  });
351
507
  core_1.Logger.log.info('handleSolanaRedeem', 'Redeem successful:', tx);
352
- // Wait for confirmation with the blockhash we used
353
- await connection.confirmTransaction({
354
- signature: tx,
355
- blockhash,
356
- lastValidBlockHeight,
357
- }, 'confirmed');
358
508
  return tx;
359
509
  }
360
510
  catch (e) {
@@ -3,4 +3,4 @@
3
3
  * Generated during publish from package.json version
4
4
  * This file is gitignored and created at publish time
5
5
  */
6
- export declare const SDK_VERSION = "8.20.1";
6
+ export declare const SDK_VERSION = "8.21.1";
@@ -6,5 +6,5 @@ exports.SDK_VERSION = void 0;
6
6
  * Generated during publish from package.json version
7
7
  * This file is gitignored and created at publish time
8
8
  */
9
- exports.SDK_VERSION = '8.20.1';
9
+ exports.SDK_VERSION = '8.21.1';
10
10
  //# sourceMappingURL=version.js.map
@@ -32,7 +32,8 @@ export declare const NATIVE_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEe
32
32
  /**
33
33
  * Decimal precision of the native gas token on every EVM chain this SDK
34
34
  * supports (ETH, Base, Arbitrum, Avalanche, Polygon, BNB, HyperEVM, Unichain,
35
- * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo).
35
+ * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo, X Layer —
36
+ * whose native OKB is also 18 decimals).
36
37
  *
37
38
  * All of them use 18-decimal native tokens, so this is a single constant rather
38
39
  * than a per-chain map. It exists because the native token has no ERC-20
@@ -73,7 +74,8 @@ export declare const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a17397
73
74
  /**
74
75
  * Chains where the canonical Multicall3 deployment was **verified on-chain**
75
76
  * (`eth_getCode` returned the 3808-byte runtime at {@link MULTICALL3_ADDRESS};
76
- * checked per chain on 2026-07-14, Tempo checked separately on 2026-07-15) —
77
+ * checked per chain on 2026-07-14, Tempo checked separately on 2026-07-15,
78
+ * X Layer on 2026-08-11) —
77
79
  * deterministic-deployer presence is NOT assumed. Chains outside this set
78
80
  * keep the per-call read path.
79
81
  *
@@ -40,7 +40,8 @@ exports.NATIVE_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
40
40
  /**
41
41
  * Decimal precision of the native gas token on every EVM chain this SDK
42
42
  * supports (ETH, Base, Arbitrum, Avalanche, Polygon, BNB, HyperEVM, Unichain,
43
- * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo).
43
+ * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo, X Layer —
44
+ * whose native OKB is also 18 decimals).
44
45
  *
45
46
  * All of them use 18-decimal native tokens, so this is a single constant rather
46
47
  * than a per-chain map. It exists because the native token has no ERC-20
@@ -160,6 +161,11 @@ exports.NETWORKS = {
160
161
  chainId: 4217,
161
162
  explorer: 'https://explore.tempo.xyz',
162
163
  },
164
+ 196: {
165
+ name: 'X Layer',
166
+ chainId: 196,
167
+ explorer: 'https://xlayerscan.com',
168
+ },
163
169
  };
164
170
  exports.AVAILABLE_CHAINS = Object.keys(exports.NETWORKS).map((c) => Number(c));
165
171
  /**
@@ -171,7 +177,8 @@ exports.MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
171
177
  /**
172
178
  * Chains where the canonical Multicall3 deployment was **verified on-chain**
173
179
  * (`eth_getCode` returned the 3808-byte runtime at {@link MULTICALL3_ADDRESS};
174
- * checked per chain on 2026-07-14, Tempo checked separately on 2026-07-15) —
180
+ * checked per chain on 2026-07-14, Tempo checked separately on 2026-07-15,
181
+ * X Layer on 2026-08-11) —
175
182
  * deterministic-deployer presence is NOT assumed. Chains outside this set
176
183
  * keep the per-call read path.
177
184
  *
@@ -197,6 +204,7 @@ exports.MULTICALL3_VERIFIED_CHAINS = new Set([
197
204
  747474, // Katana
198
205
  25363, // Fluent
199
206
  4217, // Tempo
207
+ 196, // X Layer
200
208
  ]);
201
209
  /**
202
210
  * Fallbacks
@@ -225,5 +233,6 @@ exports.FALLBACK_RPC_URLS = {
225
233
  4114: ['https://rpc.mainnet.citrea.xyz'],
226
234
  25363: ['https://rpc.fluent.xyz'],
227
235
  4217: ['https://rpc.mainnet.tempo.xyz'],
236
+ 196: ['https://rpc.xlayer.tech'],
228
237
  };
229
238
  //# sourceMappingURL=web3.js.map
package/lib/main.js CHANGED
@@ -90,21 +90,27 @@ class AugustSDK extends core_1.AugustBase {
90
90
  return {
91
91
  rpcUrl: baseConfig.solana.rpcUrl,
92
92
  network: baseConfig.solana.network,
93
+ commitment: baseConfig.solana.commitment,
93
94
  };
94
95
  }
95
96
  const legacyRpcUrl = baseConfig.providers?.[core_1.SPECIAL_CHAINS.solana.chainId];
96
97
  if (legacyRpcUrl) {
98
+ // No commitment here: `ISolanaConfig` requires `rpcUrl` + `network`, so
99
+ // a commitment-only `solana` object alongside this legacy path is not
100
+ // expressible in TypeScript. Set `solana: { rpcUrl, network,
101
+ // commitment }` to configure it.
97
102
  return {
98
103
  rpcUrl: legacyRpcUrl,
99
104
  network: legacyRpcUrl.includes('devnet')
100
105
  ? 'devnet'
101
106
  : 'mainnet-beta',
107
+ commitment: undefined,
102
108
  };
103
109
  }
104
110
  return null;
105
111
  })();
106
112
  if (solanaConfig) {
107
- this.solana = new solana_1.default(solanaConfig.rpcUrl, solanaConfig.network);
113
+ this.solana = new solana_1.default(solanaConfig.rpcUrl, solanaConfig.network, solanaConfig.commitment);
108
114
  (0, analytics_1.instrumentClass)(this.solana, () => core_1.SPECIAL_CHAINS.solana.chainId);
109
115
  }
110
116
  this.sui = new sui_1.default();
package/lib/sdk.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { Abi } from 'abitype';
2
2
  import type { AbiParametersToPrimitiveTypes } from 'abitype';
3
3
  import type { AnchorProvider } from '@coral-xyz/anchor';
4
4
  import type { BaseContract } from 'ethers';
5
+ import { Commitment } from '@solana/web3.js';
5
6
  import { Connection } from '@solana/web3.js';
6
7
  import type { ContractTransaction } from 'ethers';
7
8
  import type { ContractTransactionResponse } from 'ethers';
@@ -17567,7 +17568,8 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
17567
17568
  /**
17568
17569
  * Decimal precision of the native gas token on every EVM chain this SDK
17569
17570
  * supports (ETH, Base, Arbitrum, Avalanche, Polygon, BNB, HyperEVM, Unichain,
17570
- * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo).
17571
+ * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo, X Layer —
17572
+ * whose native OKB is also 18 decimals).
17571
17573
  *
17572
17574
  * All of them use 18-decimal native tokens, so this is a single constant rather
17573
17575
  * than a per-chain map. It exists because the native token has no ERC-20
@@ -19436,18 +19438,33 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
19436
19438
  * @param depositAmount `bigint` is treated as raw on-chain units (preferred
19437
19439
  * — no JS-float round-trip). `number` is treated as a UI amount and
19438
19440
  * scaled by the deposit mint's decimals via {@link uiAmountToRawBn}.
19439
- * @throws AugustValidationError on missing wallet/programId or invalid amount.
19441
+ * @throws AugustValidationError on missing wallet/programId or invalid amount,
19442
+ * or when the wallet's deposit-mint balance is below `depositAmount`.
19443
+ * @throws AugustSDKError when a candidate token account's balance cannot be
19444
+ * read, or wrapping any downstream failure (`cause` preserved).
19445
+ * @remarks **Side effects.** Submits exactly one transaction, signed by the
19446
+ * provider's wallet. When the wallet has no share account, that transaction
19447
+ * also creates one, and `publicKey` pays its rent — roughly 0.00204 SOL,
19448
+ * non-refundable while the account stays open — on top of the network fee.
19449
+ * Callers should ensure the wallet holds enough SOL for both before
19450
+ * prompting the user to sign.
19440
19451
  * @remarks The on-chain `deposit` instruction has no `min_shares_out`
19441
19452
  * parameter; slippage cannot be enforced on-chain until the program ships
19442
19453
  * that argument.
19443
19454
  */
19444
- declare function handleSolanaDeposit({ provider, connection, network, vaultProgramId, vaultAddress, depositAmount, publicKey, sendTransaction, idl, }: {
19455
+ declare function handleSolanaDeposit({ provider, connection, network, vaultProgramId, vaultAddress, depositAmount, publicKey, idl, }: {
19445
19456
  vaultProgramId: PublicKey | string;
19446
19457
  vaultAddress?: PublicKey | string;
19447
19458
  idl: any;
19448
19459
  publicKey: PublicKey | string;
19449
19460
  depositAmount: number | bigint;
19450
- sendTransaction: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>;
19461
+ /**
19462
+ * @deprecated Accepted and ignored. Share-account creation is now prepended
19463
+ * to the deposit instruction, so there is no longer a second transaction to
19464
+ * send. Kept so existing callers keep compiling; will be removed in the next
19465
+ * major.
19466
+ */
19467
+ sendTransaction?: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>;
19451
19468
  } & ISolanaConnectionOptions): Promise<any>;
19452
19469
 
19453
19470
  /**
@@ -19456,15 +19473,30 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
19456
19473
  * @param redeemShares `bigint` is treated as raw share-token units;
19457
19474
  * `number` is treated as a UI amount. See {@link handleSolanaDeposit} for
19458
19475
  * the same caveat about the missing on-chain slippage guard.
19459
- * @throws AugustValidationError on missing wallet/programId or invalid amount.
19476
+ * @throws AugustValidationError on missing wallet/programId or invalid amount,
19477
+ * or when the wallet holds fewer shares than `redeemShares`.
19478
+ * @throws AugustSDKError when a candidate token account's balance cannot be
19479
+ * read, or wrapping any downstream failure (`cause` preserved).
19480
+ * @remarks **Side effects.** Submits exactly one transaction, signed by the
19481
+ * provider's wallet. That transaction also creates the payout account when
19482
+ * the wallet has none, and the vault's fee-recipient account when *it* has
19483
+ * none — `publicKey` pays rent for both, roughly 0.00204 SOL each and
19484
+ * non-refundable, on top of the network fee. Callers should ensure the
19485
+ * wallet holds enough SOL for all of it before prompting the user to sign.
19460
19486
  */
19461
- declare function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultAddress, publicKey, redeemShares, sendTransaction, idl, }: {
19487
+ declare function handleSolanaRedeem({ provider, connection, vaultProgramId, vaultAddress, publicKey, redeemShares, idl, }: {
19462
19488
  idl: any;
19463
19489
  vaultProgramId: PublicKey | string;
19464
19490
  vaultAddress?: PublicKey | string;
19465
19491
  publicKey: PublicKey | string;
19466
19492
  redeemShares: number | bigint;
19467
- sendTransaction: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>;
19493
+ /**
19494
+ * @deprecated Accepted and ignored. Payout- and fee-recipient-account
19495
+ * creation is now prepended to the redeem instruction, so there is no longer
19496
+ * a second transaction to send. Kept so existing callers keep compiling;
19497
+ * will be removed in the next major.
19498
+ */
19499
+ sendTransaction?: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>;
19468
19500
  } & ISolanaConnectionOptions): Promise<any>;
19469
19501
 
19470
19502
  /**
@@ -21229,6 +21261,20 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
21229
21261
  export declare interface ISolanaConfig {
21230
21262
  rpcUrl: string;
21231
21263
  network: ISolanaNetwork;
21264
+ /**
21265
+ * Commitment for every Solana read *and* for the confirmation of every
21266
+ * Solana write, so the two always describe the same chain state.
21267
+ *
21268
+ * Defaults to `'finalized'` — unchanged from the commitment the SDK has
21269
+ * always inherited from the RPC. Pass `'confirmed'` for a markedly faster
21270
+ * round trip (seconds rather than tens of seconds), accepting that the state
21271
+ * you act on can still, in principle, be rolled back.
21272
+ *
21273
+ * Only read from this object. Configuring Solana through the legacy
21274
+ * `providers` map instead leaves the commitment at its default, since this
21275
+ * type requires `rpcUrl` and `network` alongside it.
21276
+ */
21277
+ commitment?: Commitment;
21232
21278
  }
21233
21279
 
21234
21280
  declare interface ISolanaConnectionOptions {
@@ -25100,11 +25146,11 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
25100
25146
  fetchUserTokenBalance: ({ connection, publicKey, depositMint, }: {
25101
25147
  publicKey: PublicKey | string;
25102
25148
  depositMint?: PublicKey | string;
25103
- } & ISolanaConnectionOptions) => Promise<any>;
25149
+ } & ISolanaConnectionOptions) => Promise<string>;
25104
25150
  fetchUserShareBalance: ({ connection, publicKey, shareMint, }: {
25105
25151
  publicKey: PublicKey | string;
25106
25152
  shareMint?: PublicKey | string;
25107
- } & ISolanaConnectionOptions) => Promise<any>;
25153
+ } & ISolanaConnectionOptions) => Promise<number>;
25108
25154
  fetchUserShareBalanceRaw: ({ connection, publicKey, shareMint, }: {
25109
25155
  publicKey: PublicKey | string;
25110
25156
  shareMint?: PublicKey | string;
@@ -25167,7 +25213,23 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
25167
25213
  private _network;
25168
25214
  private _connection;
25169
25215
  private _provider;
25170
- constructor(endpoint: ISolanaRpcEndpoint, network?: ISolanaNetwork_2);
25216
+ /**
25217
+ * @param commitment - Commitment for every read this adapter makes *and* for
25218
+ * the confirmation of every write, so the two always describe the same
25219
+ * chain state — the vault handlers take it from this same `Connection`.
25220
+ *
25221
+ * Defaults to `'finalized'`, which is what this adapter has always used in
25222
+ * practice: it previously passed no commitment at all, so both reads and
25223
+ * confirmations fell through to the RPC's own `'finalized'` default. The
25224
+ * default is stated explicitly now rather than inherited, but it is
25225
+ * unchanged.
25226
+ *
25227
+ * Pass `'confirmed'` for a markedly faster round trip — seconds rather
25228
+ * than tens of seconds — accepting that the state you act on can still, in
25229
+ * principle, be rolled back. Also settable as the `commitment` field of
25230
+ * the `solana` config passed to `AugustSDK`.
25231
+ */
25232
+ constructor(endpoint: ISolanaRpcEndpoint, network?: ISolanaNetwork_2, commitment?: Commitment);
25171
25233
  get endpoint(): `https://${string}`;
25172
25234
  get network(): ISolanaNetwork_2;
25173
25235
  get connection(): web3.Connection;
@@ -25219,8 +25281,8 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
25219
25281
  image: string;
25220
25282
  }>;
25221
25283
  getTokenSymbol(mintAddress: string | PublicKey): Promise<string>;
25222
- fetchUserTokenBalance(publicKey: PublicKey | string, depositMint: PublicKey | string): Promise<any>;
25223
- fetchUserShareBalance(publicKey: PublicKey | string, shareMint: PublicKey | string): Promise<any>;
25284
+ fetchUserTokenBalance(publicKey: PublicKey | string, depositMint: PublicKey | string): Promise<string>;
25285
+ fetchUserShareBalance(publicKey: PublicKey | string, shareMint: PublicKey | string): Promise<number>;
25224
25286
  /**
25225
25287
  * BigInt-safe variant of {@link fetchUserShareBalance}. Returns the raw u64
25226
25288
  * `amount` (as a base-units string) plus the mint's `decimals` (or `null`
@@ -25260,13 +25322,31 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
25260
25322
  /**
25261
25323
  * Deposit funds into a Solana August vault.
25262
25324
  * @param depositAmount `bigint` (raw on-chain units) or `number` (UI amount).
25325
+ * @param sendTransaction - **Ignored; scheduled for removal (AUGUST-7221).**
25326
+ * Share-account creation is now prepended to the deposit instruction, so
25327
+ * the SDK sends no second transaction and never invokes this callback —
25328
+ * signing goes through the provider's wallet.
25329
+ *
25330
+ * **Pass `undefined` here; do not delete the argument.** It sits *before*
25331
+ * `vaultAddress`, so removing it shifts your vault address into this slot
25332
+ * and leaves `vaultAddress` undefined — which silently falls back to the
25333
+ * legacy single-vault PDA derivation and targets a different vault.
25263
25334
  */
25264
- vaultDeposit(vaultProgramId: PublicKey | string, idl: any, publicKey: PublicKey | string, depositAmount: number | bigint, sendTransaction: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>, vaultAddress?: PublicKey | string): Promise<any>;
25335
+ vaultDeposit(vaultProgramId: PublicKey | string, idl: any, publicKey: PublicKey | string, depositAmount: number | bigint, sendTransaction?: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>, vaultAddress?: PublicKey | string): Promise<any>;
25265
25336
  /**
25266
25337
  * Redeem vault shares from a Solana August vault.
25267
25338
  * @param redeemShares `bigint` (raw share units) or `number` (UI amount).
25339
+ * @param sendTransaction - **Ignored; scheduled for removal (AUGUST-7221).**
25340
+ * Payout- and fee-recipient-account creation is now prepended to the redeem
25341
+ * instruction, so the SDK sends no second transaction and never invokes
25342
+ * this callback — signing goes through the provider's wallet.
25343
+ *
25344
+ * **Pass `undefined` here; do not delete the argument.** It sits *before*
25345
+ * `vaultAddress`, so removing it shifts your vault address into this slot
25346
+ * and leaves `vaultAddress` undefined — which silently falls back to the
25347
+ * legacy single-vault PDA derivation and targets a different vault.
25268
25348
  */
25269
- vaultRedeem(vaultProgramId: PublicKey | string, idl: any, publicKey: PublicKey | string, redeemShares: number | bigint, sendTransaction: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>, vaultAddress?: PublicKey | string): Promise<any>;
25349
+ vaultRedeem(vaultProgramId: PublicKey | string, idl: any, publicKey: PublicKey | string, redeemShares: number | bigint, sendTransaction?: (transaction: Transaction | web3.VersionedTransaction, connection: Connection, options?: SendTransactionOptions) => Promise<web3.TransactionSignature>, vaultAddress?: PublicKey | string): Promise<any>;
25270
25350
  /**
25271
25351
  * Canonical program id for a program type on this adapter's network.
25272
25352
  *
@@ -1,3 +1,4 @@
1
+ import type { Commitment } from '@solana/web3.js';
1
2
  import type { Signer, Provider } from 'ethers';
2
3
  export type IAddress = `0x${string}`;
3
4
  /**
@@ -33,6 +34,20 @@ export type IStellarNetwork = 'mainnet' | 'testnet';
33
34
  export interface ISolanaConfig {
34
35
  rpcUrl: string;
35
36
  network: ISolanaNetwork;
37
+ /**
38
+ * Commitment for every Solana read *and* for the confirmation of every
39
+ * Solana write, so the two always describe the same chain state.
40
+ *
41
+ * Defaults to `'finalized'` — unchanged from the commitment the SDK has
42
+ * always inherited from the RPC. Pass `'confirmed'` for a markedly faster
43
+ * round trip (seconds rather than tens of seconds), accepting that the state
44
+ * you act on can still, in principle, be rolled back.
45
+ *
46
+ * Only read from this object. Configuring Solana through the legacy
47
+ * `providers` map instead leaves the commitment at its default, since this
48
+ * type requires `rpcUrl` and `network` alongside it.
49
+ */
50
+ commitment?: Commitment;
36
51
  }
37
52
  /**
38
53
  * Stellar-specific configuration for `new AugustSDK({ stellar })`. All
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augustdigital/sdk",
3
- "version": "8.20.1",
3
+ "version": "8.21.1",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/sdk.d.ts",
6
6
  "keywords": [