@gauntlet-xyz/sdk 2.0.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +134 -1
  2. package/dist/api/activity.d.ts +70 -0
  3. package/dist/api/activity.d.ts.map +1 -0
  4. package/dist/api/activity.js +154 -0
  5. package/dist/api/activity.js.map +1 -0
  6. package/dist/api/caip.d.ts +29 -0
  7. package/dist/api/caip.d.ts.map +1 -0
  8. package/dist/api/caip.js +38 -0
  9. package/dist/api/caip.js.map +1 -0
  10. package/dist/api/client.d.ts +135 -0
  11. package/dist/api/client.d.ts.map +1 -0
  12. package/dist/api/client.js +119 -0
  13. package/dist/api/client.js.map +1 -0
  14. package/dist/api/decimal.d.ts +21 -0
  15. package/dist/api/decimal.d.ts.map +1 -0
  16. package/dist/api/decimal.js +44 -0
  17. package/dist/api/decimal.js.map +1 -0
  18. package/dist/api/errors.d.ts +32 -0
  19. package/dist/api/errors.d.ts.map +1 -0
  20. package/dist/api/errors.js +41 -0
  21. package/dist/api/errors.js.map +1 -0
  22. package/dist/api/generated.d.ts +1505 -0
  23. package/dist/api/generated.d.ts.map +1 -0
  24. package/dist/api/generated.js +6 -0
  25. package/dist/api/generated.js.map +1 -0
  26. package/dist/api/http.d.ts +16 -0
  27. package/dist/api/http.d.ts.map +1 -0
  28. package/dist/api/http.js +39 -0
  29. package/dist/api/http.js.map +1 -0
  30. package/dist/api/index.d.ts +14 -0
  31. package/dist/api/index.d.ts.map +1 -0
  32. package/dist/api/index.js +8 -0
  33. package/dist/api/index.js.map +1 -0
  34. package/dist/api/positionHistory.d.ts +44 -0
  35. package/dist/api/positionHistory.d.ts.map +1 -0
  36. package/dist/api/positionHistory.js +126 -0
  37. package/dist/api/positionHistory.js.map +1 -0
  38. package/dist/client.d.ts +6 -0
  39. package/dist/client.d.ts.map +1 -1
  40. package/dist/client.js +2 -0
  41. package/dist/client.js.map +1 -1
  42. package/dist/errors.d.ts +11 -0
  43. package/dist/errors.d.ts.map +1 -1
  44. package/dist/errors.js +21 -2
  45. package/dist/errors.js.map +1 -1
  46. package/dist/evm/abis/priceAndFeeCalculatorV2.d.ts +44 -0
  47. package/dist/evm/abis/priceAndFeeCalculatorV2.d.ts.map +1 -1
  48. package/dist/evm/abis/priceAndFeeCalculatorV2.js +22 -0
  49. package/dist/evm/abis/priceAndFeeCalculatorV2.js.map +1 -1
  50. package/dist/evm/abis/provisionerV2.d.ts +280 -0
  51. package/dist/evm/abis/provisionerV2.d.ts.map +1 -1
  52. package/dist/evm/abis/provisionerV2.js +92 -0
  53. package/dist/evm/abis/provisionerV2.js.map +1 -1
  54. package/dist/evm/adapters/aera.d.ts.map +1 -1
  55. package/dist/evm/adapters/aera.js +41 -10
  56. package/dist/evm/adapters/aera.js.map +1 -1
  57. package/dist/evm/adapters/types.d.ts +2 -0
  58. package/dist/evm/adapters/types.d.ts.map +1 -1
  59. package/dist/evm/aeraContracts/priceAndFeeCalculator.d.ts +10 -5
  60. package/dist/evm/aeraContracts/priceAndFeeCalculator.d.ts.map +1 -1
  61. package/dist/evm/aeraContracts/priceAndFeeCalculator.js +39 -5
  62. package/dist/evm/aeraContracts/priceAndFeeCalculator.js.map +1 -1
  63. package/dist/evm/aeraContracts/readOptions.d.ts +8 -0
  64. package/dist/evm/aeraContracts/readOptions.d.ts.map +1 -0
  65. package/dist/evm/aeraContracts/readOptions.js +4 -0
  66. package/dist/evm/aeraContracts/readOptions.js.map +1 -0
  67. package/dist/evm/aeraContracts/v2.d.ts +1932 -217
  68. package/dist/evm/aeraContracts/v2.d.ts.map +1 -1
  69. package/dist/evm/aeraContracts/v2.js +84 -28
  70. package/dist/evm/aeraContracts/v2.js.map +1 -1
  71. package/dist/evm/aeraContracts.d.ts +9 -3
  72. package/dist/evm/aeraContracts.d.ts.map +1 -1
  73. package/dist/evm/aeraContracts.js +69 -8
  74. package/dist/evm/aeraContracts.js.map +1 -1
  75. package/dist/evm/deposit.d.ts.map +1 -1
  76. package/dist/evm/deposit.js +1 -1
  77. package/dist/evm/deposit.js.map +1 -1
  78. package/dist/evm/index.d.ts +8 -0
  79. package/dist/evm/index.d.ts.map +1 -1
  80. package/dist/evm/index.js +3 -0
  81. package/dist/evm/index.js.map +1 -1
  82. package/dist/evm/tokenModes.d.ts +22 -0
  83. package/dist/evm/tokenModes.d.ts.map +1 -0
  84. package/dist/evm/tokenModes.js +34 -0
  85. package/dist/evm/tokenModes.js.map +1 -0
  86. package/dist/evm/withdraw.d.ts +8 -0
  87. package/dist/evm/withdraw.d.ts.map +1 -1
  88. package/dist/evm/withdraw.js +85 -5
  89. package/dist/evm/withdraw.js.map +1 -1
  90. package/dist/evm/withdrawQuote.d.ts +127 -0
  91. package/dist/evm/withdrawQuote.d.ts.map +1 -0
  92. package/dist/evm/withdrawQuote.js +164 -0
  93. package/dist/evm/withdrawQuote.js.map +1 -0
  94. package/dist/index.d.ts +8 -1
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +4 -1
  97. package/dist/index.js.map +1 -1
  98. package/dist/privy/index.d.ts +52 -0
  99. package/dist/privy/index.d.ts.map +1 -0
  100. package/dist/privy/index.js +51 -0
  101. package/dist/privy/index.js.map +1 -0
  102. package/manifest/vaults.json +255 -3
  103. package/package.json +2 -2
  104. package/src/api/activity.ts +219 -0
  105. package/src/api/caip.ts +61 -0
  106. package/src/api/client.ts +234 -0
  107. package/src/api/decimal.ts +49 -0
  108. package/src/api/errors.ts +64 -0
  109. package/src/api/generated.ts +1505 -0
  110. package/src/api/http.ts +62 -0
  111. package/src/api/index.ts +70 -0
  112. package/src/api/positionHistory.ts +167 -0
  113. package/src/client.ts +7 -1
  114. package/src/errors.ts +32 -2
  115. package/src/evm/abis/priceAndFeeCalculatorV2.ts +22 -0
  116. package/src/evm/abis/provisionerV2.ts +92 -0
  117. package/src/evm/adapters/aera.ts +50 -23
  118. package/src/evm/adapters/types.ts +2 -0
  119. package/src/evm/aeraContracts/priceAndFeeCalculator.ts +72 -5
  120. package/src/evm/aeraContracts/readOptions.ts +3 -0
  121. package/src/evm/aeraContracts/v2.ts +158 -34
  122. package/src/evm/aeraContracts.ts +81 -8
  123. package/src/evm/deposit.ts +2 -1
  124. package/src/evm/index.ts +32 -0
  125. package/src/evm/tokenModes.ts +54 -0
  126. package/src/evm/withdraw.ts +131 -7
  127. package/src/evm/withdrawQuote.ts +360 -0
  128. package/src/index.ts +18 -0
  129. package/src/privy/index.ts +95 -0
@@ -113,7 +113,8 @@ export async function getDepositTx(
113
113
  const tokenModeSupport = await resolveAeraTokenModeSupport(
114
114
  publicClient,
115
115
  aeraRuntime,
116
- token.address
116
+ token.address,
117
+ { includeSyncModes: requestedDepositMode !== 'async' }
117
118
  );
118
119
  modifiedDepositMode = resolveOperationMode(params.vaultId, requestedDepositMode, {
119
120
  async: tokenModeSupport.asyncDeposit,
package/src/evm/index.ts CHANGED
@@ -1,10 +1,30 @@
1
1
  export { getDepositTx } from './deposit';
2
2
  export { getDepositReceiverApprovalTx } from './depositReceiverApproval';
3
3
  export { getWithdrawTx } from './withdraw';
4
+ export { getSyncWithdrawQuote } from './withdrawQuote';
5
+ export { getAeraTokenModeSupport } from './tokenModes';
4
6
  export { getUserCurrentBalance } from './userCurrentBalance';
5
7
  export { getVaults, VaultId } from './vaults';
6
8
  export { ContractVersion } from './types';
7
9
  export { resolveAeraRuntimeContracts, resolveContractVersion } from './aeraContracts';
10
+ export {
11
+ GauntletSDKError,
12
+ AccountRequiredError,
13
+ AccountMismatchError,
14
+ ChainMismatchError,
15
+ InvalidSlippageBPSError,
16
+ InvalidSolverTipError,
17
+ InvalidSyncWithdrawBoundError,
18
+ InvalidWithdrawParamsError,
19
+ RpcNotConfiguredError,
20
+ StalePriceError,
21
+ UnsupportedAssetError,
22
+ UnsupportedDepositModeError,
23
+ UnsupportedFeatureError,
24
+ UnsupportedProtocolError,
25
+ UnimplementedFeatureError,
26
+ VaultNotFoundError,
27
+ } from '../errors';
8
28
  export {
9
29
  Rounding,
10
30
  convertTokenToUnits,
@@ -19,7 +39,18 @@ export {
19
39
  export type { EvmDepositParams } from './deposit';
20
40
  export type { EvmDepositReceiverApprovalParams } from './depositReceiverApproval';
21
41
  export type { EvmWithdrawParams } from './withdraw';
42
+ export type {
43
+ SyncWithdrawQuote,
44
+ SyncWithdrawQuoteParams,
45
+ SyncWithdrawCapacity,
46
+ SyncWithdrawQuoteContext,
47
+ SyncWithdrawQuoteRequest,
48
+ SyncWithdrawQuoteBounds,
49
+ } from './withdrawQuote';
50
+ export type { AeraTokenModeSupportParams } from './tokenModes';
51
+ export type { SyncRedeemRate } from './aeraContracts/v2';
22
52
  export type { EvmTxStep } from './adapters/types';
53
+ export type { PreparedTx } from '../attribution';
23
54
  export type {
24
55
  VaultInfo,
25
56
  VaultDeployment,
@@ -31,3 +62,4 @@ export type { VaultFilter } from './vaults';
31
62
  export type { UserCurrentBalanceParams, UserCurrentBalance } from './userCurrentBalance';
32
63
  export type { AeraRuntimeContracts, AeraTokenModeSupport } from './aeraContracts';
33
64
  export type { NormalizedVaultPriceState } from './aeraContracts/priceAndFeeCalculator';
65
+ export type { SyncWithdrawBound } from '../errors';
@@ -0,0 +1,54 @@
1
+ import type { GauntletClient } from '../client';
2
+ import { UnsupportedAssetError, UnsupportedFeatureError, VaultNotFoundError } from '../errors';
3
+ import { resolveVault } from './vaults';
4
+ import {
5
+ resolveAeraRuntimeContracts,
6
+ resolveAeraTokenModeSupport,
7
+ type AeraTokenModeSupport,
8
+ } from './aeraContracts';
9
+
10
+ export interface AeraTokenModeSupportParams {
11
+ vaultId: string;
12
+ /** EVM chain ID. Defaults to the vault's primary chain (Base for current multichain vaults). */
13
+ chainId?: number;
14
+ /** Required for multiasset vaults. */
15
+ assetSymbol?: string;
16
+ }
17
+
18
+ /**
19
+ * Reads which deposit/redeem modes (sync vs async) are currently enabled on-chain for a vault token.
20
+ *
21
+ * Use this to decide which entry points to offer a user (e.g. whether an instant withdraw is
22
+ * available) before any amount is entered. Mode availability includes the provisioner's token
23
+ * settings and live runtime gates. A paused V2 solving gate disables both sync modes.
24
+ *
25
+ * @throws {VaultNotFoundError} If the vault ID is not found.
26
+ * @throws {UnsupportedFeatureError} If the vault is not an Aera deployment.
27
+ * @throws {UnsupportedAssetError} If the asset symbol is not accepted by the vault.
28
+ */
29
+ export async function getAeraTokenModeSupport(
30
+ client: GauntletClient,
31
+ params: AeraTokenModeSupportParams
32
+ ): Promise<AeraTokenModeSupport> {
33
+ const resolved = await resolveVault(client, params.vaultId, params.chainId);
34
+ if (!resolved) throw new VaultNotFoundError(params.vaultId);
35
+
36
+ const { vault, protocol } = resolved;
37
+ if (protocol !== 'aera') {
38
+ throw new UnsupportedFeatureError('Token mode support is only available for Aera vaults');
39
+ }
40
+
41
+ const chainId = params.chainId ?? vault.chainId;
42
+ const publicClient = client.getPublicClient(chainId);
43
+
44
+ const token =
45
+ vault.supplyToken.length > 1
46
+ ? vault.supplyToken.find((tInfo) => tInfo.symbol === params.assetSymbol)
47
+ : vault.supplyToken[0];
48
+ if (token === undefined) {
49
+ throw new UnsupportedAssetError(params.assetSymbol ?? 'unknown', params.vaultId);
50
+ }
51
+
52
+ const runtime = await resolveAeraRuntimeContracts(publicClient, vault);
53
+ return resolveAeraTokenModeSupport(publicClient, runtime, token.address);
54
+ }
@@ -1,15 +1,19 @@
1
1
  import type { Address } from 'viem';
2
2
  import type { GauntletClient } from '../client';
3
3
  import type { AdapterWithdrawParams } from './adapters/types';
4
+ import type { SyncWithdrawQuoteBounds } from './withdrawQuote';
4
5
  import { getAdapter } from './adapters';
5
6
  import { encodeTransactionWithAttribution, PreparedTx } from '../attribution';
6
7
  import {
8
+ AccountMismatchError,
7
9
  AccountRequiredError,
8
10
  VaultNotFoundError,
9
11
  UnsupportedAssetError,
10
12
  InvalidSlippageBPSError,
13
+ InvalidWithdrawParamsError,
11
14
  } from '../errors';
12
15
  import {
16
+ requireAeraSyncRedeemRuntime,
13
17
  resolveAeraRuntimeContracts,
14
18
  resolveAeraTokenModeSupport,
15
19
  type AeraRuntimeContracts,
@@ -22,6 +26,7 @@ import {
22
26
  resolveSyncOnlyOperationMode,
23
27
  type OperationMode,
24
28
  } from './operationMode';
29
+ import { ContractVersion } from './types';
25
30
 
26
31
  export type EvmWithdrawParams = {
27
32
  vaultId: string;
@@ -39,12 +44,94 @@ export type EvmWithdrawParams = {
39
44
  solverTip?: bigint;
40
45
  /** Maximum price age passed to async Aera provisioner requests. Defaults to 10 days. */
41
46
  maxPriceAge?: bigint;
47
+ /**
48
+ * Current sync quote bounds to pin instant withdraw transaction construction.
49
+ * Supplying bounds implies `depositMode: 'sync'`; explicit async requests are rejected.
50
+ */
51
+ syncWithdrawQuote?: SyncWithdrawQuoteBounds;
42
52
  } & (
43
53
  | { shares: bigint; amount?: never; entireAmount?: never }
44
54
  | { amount: bigint; shares?: never; entireAmount?: never }
45
- | { entireAmount: true; shares?: never; amount?: never }
55
+ | { account?: Address; entireAmount: true; shares?: never; amount?: never }
46
56
  );
47
57
 
58
+ function validateWithdrawParams(params: EvmWithdrawParams) {
59
+ const modes = [
60
+ 'amount' in params && params.amount != null,
61
+ 'shares' in params && params.shares != null,
62
+ 'entireAmount' in params && params.entireAmount === true,
63
+ ].filter(Boolean).length;
64
+
65
+ if (modes !== 1) throw new InvalidWithdrawParamsError();
66
+ }
67
+
68
+ function sameAddress(left: Address, right: Address): boolean {
69
+ return left.toLowerCase() === right.toLowerCase();
70
+ }
71
+
72
+ function validateSyncWithdrawQuote({
73
+ params,
74
+ account,
75
+ chainId,
76
+ slippageBps,
77
+ tokenAddress,
78
+ }: {
79
+ params: EvmWithdrawParams;
80
+ account: Address;
81
+ chainId: number;
82
+ slippageBps: number;
83
+ tokenAddress: Address;
84
+ }) {
85
+ const quote = params.syncWithdrawQuote;
86
+ if (quote === undefined) return;
87
+
88
+ const { context } = quote;
89
+ if (
90
+ context.vaultId !== params.vaultId ||
91
+ context.chainId !== chainId ||
92
+ !sameAddress(context.tokenAddress, tokenAddress) ||
93
+ context.slippageBps !== slippageBps
94
+ ) {
95
+ throw new InvalidWithdrawParamsError();
96
+ }
97
+
98
+ if (context.account !== undefined && !sameAddress(context.account, account)) {
99
+ throw new InvalidWithdrawParamsError();
100
+ }
101
+
102
+ switch (context.request.mode) {
103
+ case 'amount':
104
+ if (
105
+ quote.kind !== 'withdraw' ||
106
+ !('amount' in params && params.amount != null) ||
107
+ context.request.amount !== params.amount ||
108
+ quote.tokensOut !== params.amount
109
+ ) {
110
+ throw new InvalidWithdrawParamsError();
111
+ }
112
+ return;
113
+ case 'shares':
114
+ if (
115
+ quote.kind !== 'redeem' ||
116
+ !('shares' in params && params.shares != null) ||
117
+ context.request.shares !== params.shares ||
118
+ quote.shares !== params.shares
119
+ ) {
120
+ throw new InvalidWithdrawParamsError();
121
+ }
122
+ return;
123
+ case 'entireAmount':
124
+ if (
125
+ quote.kind !== 'redeem' ||
126
+ !('entireAmount' in params && params.entireAmount) ||
127
+ !sameAddress(context.request.account, account)
128
+ ) {
129
+ throw new InvalidWithdrawParamsError();
130
+ }
131
+ return;
132
+ }
133
+ }
134
+
48
135
  /**
49
136
  * Builds the ordered list of EVM transactions required to withdraw from a Gauntlet vault.
50
137
  *
@@ -76,6 +163,7 @@ export type EvmWithdrawParams = {
76
163
  * const txs = await getWithdrawTx(client, {
77
164
  * vaultId: 'baseUsdcPrime',
78
165
  * entireAmount: true,
166
+ * account: walletAddress,
79
167
  * });
80
168
  * ```
81
169
  */
@@ -83,6 +171,8 @@ export async function getWithdrawTx(
83
171
  client: GauntletClient,
84
172
  params: EvmWithdrawParams
85
173
  ): Promise<PreparedTx[]> {
174
+ validateWithdrawParams(params);
175
+
86
176
  if (
87
177
  params.slippageBps !== undefined &&
88
178
  (!Number.isInteger(params.slippageBps) ||
@@ -99,6 +189,11 @@ export async function getWithdrawTx(
99
189
 
100
190
  const account = client.wallet?.account?.address;
101
191
  if (!account) throw new AccountRequiredError();
192
+ if ('entireAmount' in params && params.entireAmount && params.account !== undefined) {
193
+ if (!sameAddress(params.account, account)) {
194
+ throw new AccountMismatchError(account, params.account);
195
+ }
196
+ }
102
197
 
103
198
  const { vault, protocol } = resolved;
104
199
 
@@ -114,20 +209,48 @@ export async function getWithdrawTx(
114
209
  const adapter = getAdapter(protocol);
115
210
  const publicClient = client.getPublicClient(chainId);
116
211
  const requestedWithdrawMode = parseOperationMode(params.vaultId, params.depositMode);
212
+ const slippageBps =
213
+ params.slippageBps ?? params.syncWithdrawQuote?.context.slippageBps ?? DEFAULT_BPS;
117
214
  let modifiedDepositMode: OperationMode;
118
215
  let aeraRuntime: AeraRuntimeContracts | undefined;
119
216
 
217
+ if (params.syncWithdrawQuote !== undefined) {
218
+ if (protocol !== 'aera' || requestedWithdrawMode === 'async') {
219
+ throw new InvalidWithdrawParamsError();
220
+ }
221
+
222
+ validateSyncWithdrawQuote({
223
+ params,
224
+ account,
225
+ chainId,
226
+ slippageBps,
227
+ tokenAddress: token.address,
228
+ });
229
+ }
230
+
120
231
  if (protocol === 'aera') {
121
232
  aeraRuntime = await resolveAeraRuntimeContracts(publicClient, vault);
233
+ if (
234
+ aeraRuntime.provisioner.version === ContractVersion.V2 &&
235
+ (requestedWithdrawMode === 'sync' || params.syncWithdrawQuote !== undefined)
236
+ ) {
237
+ requireAeraSyncRedeemRuntime(aeraRuntime);
238
+ }
122
239
  const tokenModeSupport = await resolveAeraTokenModeSupport(
123
240
  publicClient,
124
241
  aeraRuntime,
125
- token.address
242
+ token.address,
243
+ { includeSyncModes: requestedWithdrawMode !== 'async' }
126
244
  );
127
- modifiedDepositMode = resolveOperationMode(params.vaultId, requestedWithdrawMode, {
128
- async: tokenModeSupport.asyncRedeem,
129
- sync: tokenModeSupport.syncRedeem,
130
- });
245
+ modifiedDepositMode = resolveOperationMode(
246
+ params.vaultId,
247
+ params.syncWithdrawQuote !== undefined ? 'sync' : requestedWithdrawMode,
248
+ {
249
+ async: tokenModeSupport.asyncRedeem,
250
+ sync: tokenModeSupport.syncRedeem,
251
+ }
252
+ );
253
+ if (modifiedDepositMode === 'sync') requireAeraSyncRedeemRuntime(aeraRuntime);
131
254
  } else {
132
255
  modifiedDepositMode = resolveSyncOnlyOperationMode(params.vaultId, requestedWithdrawMode);
133
256
  }
@@ -139,10 +262,11 @@ export async function getWithdrawTx(
139
262
  async: modifiedDepositMode === 'async',
140
263
  asset: token,
141
264
  publicClient,
142
- slippageBps: params.slippageBps ?? DEFAULT_BPS,
265
+ slippageBps,
143
266
  solverTip: params.solverTip,
144
267
  maxPriceAge: params.maxPriceAge,
145
268
  aeraRuntime,
269
+ syncWithdrawQuote: params.syncWithdrawQuote,
146
270
  ...('shares' in params && params.shares != null ? { shares: params.shares } : {}),
147
271
  ...('amount' in params && params.amount != null ? { amount: params.amount } : {}),
148
272
  ...('entireAmount' in params && params.entireAmount ? { entireAmount: true as const } : {}),
@@ -0,0 +1,360 @@
1
+ import type { Address } from 'viem';
2
+ import type { GauntletClient } from '../client';
3
+ import {
4
+ AccountRequiredError,
5
+ InvalidSlippageBPSError,
6
+ InvalidWithdrawParamsError,
7
+ requireNonZeroSyncWithdrawBound,
8
+ UnsupportedAssetError,
9
+ UnsupportedFeatureError,
10
+ VaultNotFoundError,
11
+ } from '../errors';
12
+ import { DEFAULT_BPS, MAX_BPS } from '../constants';
13
+ import { resolveVault } from './vaults';
14
+ import {
15
+ applySlippageDown,
16
+ applySlippageUp,
17
+ requireAeraSyncRedeemRuntime,
18
+ resolveAeraRuntimeContracts,
19
+ resolveAeraTokenModeSupport,
20
+ } from './aeraContracts';
21
+ import { multiDepositorVaultAbi } from './abis/multiDepositorVault';
22
+ import {
23
+ convertNumeraireToToken,
24
+ convertTokenToNumeraire,
25
+ } from './aeraContracts/priceAndFeeCalculator';
26
+ import {
27
+ getSyncRedeemEpochState,
28
+ getSyncRedeemRate,
29
+ getSyncRedeemTokenOut,
30
+ getSyncWithdrawUnitsIn,
31
+ getUserUnitsRefundableUntil,
32
+ type SyncRedeemRate,
33
+ } from './aeraContracts/v2';
34
+ import { resolveOperationMode } from './operationMode';
35
+
36
+ /** Epoch capacity accounting for the global sync redeem cap. */
37
+ export interface SyncWithdrawCapacity {
38
+ /** Effective epoch cap (min of relative and absolute), in numeraire. */
39
+ epochCapNumeraire: bigint;
40
+ /** Numeraire already redeemed globally in the current epoch. */
41
+ epochRedeemedNumeraire: bigint;
42
+ /** Remaining global sync redeem capacity this epoch, in numeraire. */
43
+ remainingNumeraire: bigint;
44
+ /** Remaining capacity expressed in the withdraw token. */
45
+ remainingTokens: bigint;
46
+ /** This redemption's size in numeraire (matches the on-chain epoch accounting). */
47
+ requestNumeraire: bigint;
48
+ /** True when this redemption alone exceeds remaining epoch capacity (the tx would revert). */
49
+ exceedsCapacity: boolean;
50
+ }
51
+
52
+ export type SyncWithdrawQuoteRequest =
53
+ | { mode: 'amount'; amount: bigint }
54
+ | { mode: 'shares'; shares: bigint }
55
+ | { mode: 'entireAmount'; account: Address };
56
+
57
+ export interface SyncWithdrawQuoteContext {
58
+ vaultId: string;
59
+ chainId: number;
60
+ tokenAddress: Address;
61
+ /** Account used for balance and lock reads when the quote is account-scoped. */
62
+ account?: Address;
63
+ /** Slippage tolerance used to derive `minTokensOut` or `maxUnitsIn`. */
64
+ slippageBps: number;
65
+ /** Block snapshot used for all quote reads. */
66
+ blockNumber: bigint;
67
+ /** Exact sizing request that produced this quote. */
68
+ request: SyncWithdrawQuoteRequest;
69
+ }
70
+
71
+ interface SyncWithdrawQuoteDetails {
72
+ /** Vault units (shares) burned: exact for `'redeem'`, estimated for `'withdraw'`. */
73
+ shares: bigint;
74
+ /** Upper bound on shares burned after slippage (equals `shares` for `'redeem'`). */
75
+ maxUnitsIn: bigint;
76
+ /** Tokens received: exact (requested) for `'withdraw'`, estimated for `'redeem'`. */
77
+ tokensOut: bigint;
78
+ /** Lower bound on tokens received after slippage (equals `tokensOut` for `'withdraw'`). */
79
+ minTokensOut: bigint;
80
+ /** Live multiplier breakdown (base, dynamic premium, effective), in bps. */
81
+ rate: SyncRedeemRate;
82
+ capacity: SyncWithdrawCapacity;
83
+ /** Timestamp until which the account's units are locked; undefined when no account was quoted. */
84
+ unitsLockedUntil?: bigint;
85
+ /** Context that must match any transaction build using these quote bounds. */
86
+ context: SyncWithdrawQuoteContext;
87
+ }
88
+
89
+ /** Bounds consumed by one of the two on-chain sync withdraw entry points. */
90
+ export type SyncWithdrawQuoteBounds =
91
+ | {
92
+ kind: 'redeem';
93
+ shares: bigint;
94
+ minTokensOut: bigint;
95
+ context: SyncWithdrawQuoteContext;
96
+ }
97
+ | {
98
+ kind: 'withdraw';
99
+ tokensOut: bigint;
100
+ maxUnitsIn: bigint;
101
+ context: SyncWithdrawQuoteContext;
102
+ };
103
+
104
+ /**
105
+ * A live quote for an instant (sync) withdraw.
106
+ *
107
+ * `kind` distinguishes the two on-chain entry points:
108
+ * - `'redeem'`: exact shares burned, estimated tokens out (used for by-shares / entire-balance exits)
109
+ * - `'withdraw'`: exact tokens out, estimated shares burned (used for by-token-amount exits)
110
+ */
111
+ export type SyncWithdrawQuote =
112
+ | (SyncWithdrawQuoteDetails & { kind: 'redeem' })
113
+ | (SyncWithdrawQuoteDetails & { kind: 'withdraw' });
114
+
115
+ type SyncWithdrawQuoteBaseParams = {
116
+ vaultId: string;
117
+ /** EVM chain ID. Defaults to the vault's primary chain (Base for current multichain vaults). */
118
+ chainId?: number;
119
+ /** Required for multiasset vaults. */
120
+ assetSymbol?: string;
121
+ /** Optional for explicit amount/shares quotes; required for full-position quotes. */
122
+ account?: Address;
123
+ /**
124
+ * Slippage tolerance in basis points (e.g. 100 = 1%). Defaults to 100.
125
+ * A value of 10000 is invalid for share-sized quotes because it makes `minTokensOut` zero.
126
+ */
127
+ slippageBps?: number;
128
+ };
129
+
130
+ export type SyncWithdrawQuoteParams =
131
+ | (SyncWithdrawQuoteBaseParams & { shares: bigint; amount?: never; entireAmount?: never })
132
+ | (SyncWithdrawQuoteBaseParams & { amount: bigint; shares?: never; entireAmount?: never })
133
+ | (SyncWithdrawQuoteBaseParams & {
134
+ account: Address;
135
+ entireAmount: true;
136
+ shares?: never;
137
+ amount?: never;
138
+ });
139
+
140
+ function validateSyncWithdrawQuoteParams(params: SyncWithdrawQuoteParams) {
141
+ const modes = [
142
+ 'amount' in params && params.amount != null,
143
+ 'shares' in params && params.shares != null,
144
+ 'entireAmount' in params && params.entireAmount === true,
145
+ ].filter(Boolean).length;
146
+
147
+ if (modes !== 1) throw new InvalidWithdrawParamsError();
148
+ if ('entireAmount' in params && params.entireAmount === true && !params.account) {
149
+ throw new AccountRequiredError();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Builds a live quote for an instant (sync) withdraw without sending a transaction.
155
+ *
156
+ * Surfaces everything the on-chain `redeem`/`withdraw` would apply or check but that a plain
157
+ * transaction build does not pre-validate: the effective rate (including the price-age dynamic
158
+ * premium), slippage bounds, the global per-epoch redeem capacity, and whether the caller's units
159
+ * are still locked from a recent sync deposit.
160
+ *
161
+ * @throws {VaultNotFoundError} If the vault ID is not found.
162
+ * @throws {UnsupportedAssetError} If the asset symbol is not accepted by the vault.
163
+ * @throws {UnsupportedFeatureError} If the vault is not an Aera V2 deployment.
164
+ * @throws {UnsupportedDepositModeError} If sync redeem is not enabled for the token.
165
+ * @throws {StalePriceError} If the oracle price is too stale for an instant redeem.
166
+ */
167
+ export async function getSyncWithdrawQuote(
168
+ client: GauntletClient,
169
+ params: SyncWithdrawQuoteParams
170
+ ): Promise<SyncWithdrawQuote> {
171
+ validateSyncWithdrawQuoteParams(params);
172
+
173
+ if (
174
+ params.slippageBps !== undefined &&
175
+ (!Number.isInteger(params.slippageBps) ||
176
+ params.slippageBps < 0 ||
177
+ params.slippageBps > Number(MAX_BPS))
178
+ ) {
179
+ throw new InvalidSlippageBPSError(params.slippageBps);
180
+ }
181
+ const slippageBps = params.slippageBps ?? DEFAULT_BPS;
182
+
183
+ const resolved = await resolveVault(client, params.vaultId, params.chainId);
184
+ if (!resolved) throw new VaultNotFoundError(params.vaultId);
185
+
186
+ const { vault, protocol } = resolved;
187
+ if (protocol !== 'aera') {
188
+ throw new UnsupportedFeatureError('Sync withdraw quote is only available for Aera vaults');
189
+ }
190
+
191
+ const chainId = params.chainId ?? vault.chainId;
192
+ const publicClient = client.getPublicClient(chainId);
193
+
194
+ const token =
195
+ vault.supplyToken.length > 1
196
+ ? vault.supplyToken.find((tInfo) => tInfo.symbol === params.assetSymbol)
197
+ : vault.supplyToken[0];
198
+ if (token === undefined) {
199
+ throw new UnsupportedAssetError(params.assetSymbol ?? 'unknown', params.vaultId);
200
+ }
201
+
202
+ const quoteBlock = await publicClient.getBlock();
203
+ if (quoteBlock.number === null) {
204
+ throw new UnsupportedFeatureError('Aera: sync withdraw quote requires a numbered block');
205
+ }
206
+ const quoteReadContext = {
207
+ blockNumber: quoteBlock.number,
208
+ blockTimestamp: BigInt(quoteBlock.timestamp),
209
+ };
210
+
211
+ const runtime = await resolveAeraRuntimeContracts(publicClient, vault, quoteReadContext);
212
+ requireAeraSyncRedeemRuntime(runtime);
213
+
214
+ const support = await resolveAeraTokenModeSupport(
215
+ publicClient,
216
+ runtime,
217
+ token.address,
218
+ quoteReadContext
219
+ );
220
+ resolveOperationMode(params.vaultId, 'sync', {
221
+ async: support.asyncRedeem,
222
+ sync: support.syncRedeem,
223
+ });
224
+
225
+ const provisioner = runtime.provisioner.address;
226
+ const feeCalculator = runtime.feeCalculator.address;
227
+ const feeVersion = runtime.feeCalculator.version;
228
+ const vaultAddress = vault.vaultAddress;
229
+
230
+ const rate = await getSyncRedeemRate(
231
+ publicClient,
232
+ provisioner,
233
+ vaultAddress,
234
+ token.address,
235
+ feeCalculator,
236
+ quoteReadContext
237
+ );
238
+
239
+ let kind: SyncWithdrawQuote['kind'];
240
+ let shares: bigint;
241
+ let tokensOut: bigint;
242
+ let minTokensOut: bigint;
243
+ let maxUnitsIn: bigint;
244
+ let request: SyncWithdrawQuoteRequest;
245
+
246
+ if ('amount' in params && params.amount != null) {
247
+ // Exact token out: shares burned is the estimate, bounded above by slippage.
248
+ kind = 'withdraw';
249
+ request = { mode: 'amount', amount: params.amount };
250
+ tokensOut = params.amount;
251
+ requireNonZeroSyncWithdrawBound(tokensOut, 'tokensOut');
252
+ minTokensOut = params.amount;
253
+ shares = await getSyncWithdrawUnitsIn(
254
+ publicClient,
255
+ provisioner,
256
+ feeCalculator,
257
+ feeVersion,
258
+ vaultAddress,
259
+ token.address,
260
+ params.amount,
261
+ rate.effectiveMultiplierBps,
262
+ quoteReadContext
263
+ );
264
+ maxUnitsIn = applySlippageUp(shares, slippageBps);
265
+ requireNonZeroSyncWithdrawBound(maxUnitsIn, 'maxUnitsIn');
266
+ } else {
267
+ // Exact shares in: tokens out is the estimate, bounded below by slippage.
268
+ kind = 'redeem';
269
+ if ('entireAmount' in params && params.entireAmount) {
270
+ if (!params.account) throw new AccountRequiredError();
271
+ request = { mode: 'entireAmount', account: params.account };
272
+ shares = await publicClient.readContract({
273
+ address: vaultAddress,
274
+ abi: multiDepositorVaultAbi,
275
+ functionName: 'balanceOf',
276
+ args: [params.account],
277
+ blockNumber: quoteReadContext.blockNumber,
278
+ });
279
+ } else if ('shares' in params && params.shares != null) {
280
+ request = { mode: 'shares', shares: params.shares };
281
+ shares = params.shares;
282
+ } else {
283
+ throw new InvalidWithdrawParamsError();
284
+ }
285
+ requireNonZeroSyncWithdrawBound(shares, 'unitsIn');
286
+ tokensOut = await getSyncRedeemTokenOut(
287
+ publicClient,
288
+ provisioner,
289
+ feeCalculator,
290
+ feeVersion,
291
+ vaultAddress,
292
+ token.address,
293
+ shares,
294
+ rate.effectiveMultiplierBps,
295
+ quoteReadContext
296
+ );
297
+ minTokensOut = applySlippageDown(tokensOut, slippageBps);
298
+ requireNonZeroSyncWithdrawBound(minTokensOut, 'minTokensOut');
299
+ maxUnitsIn = shares;
300
+ }
301
+
302
+ const [epoch, requestNumeraire, unitsLockedUntil] = await Promise.all([
303
+ getSyncRedeemEpochState(publicClient, provisioner, quoteReadContext),
304
+ convertTokenToNumeraire(
305
+ publicClient,
306
+ feeCalculator,
307
+ feeVersion,
308
+ vaultAddress,
309
+ token.address,
310
+ tokensOut,
311
+ quoteReadContext
312
+ ),
313
+ params.account
314
+ ? getUserUnitsRefundableUntil(publicClient, provisioner, params.account, quoteReadContext)
315
+ : Promise.resolve(undefined),
316
+ ]);
317
+
318
+ const remainingNumeraire =
319
+ epoch.epochCapNumeraire > epoch.epochRedeemedNumeraire
320
+ ? epoch.epochCapNumeraire - epoch.epochRedeemedNumeraire
321
+ : 0n;
322
+ const remainingTokens = await convertNumeraireToToken(
323
+ publicClient,
324
+ feeCalculator,
325
+ feeVersion,
326
+ vaultAddress,
327
+ token.address,
328
+ remainingNumeraire,
329
+ quoteReadContext
330
+ );
331
+
332
+ const quoteDetails: SyncWithdrawQuoteDetails = {
333
+ shares,
334
+ maxUnitsIn,
335
+ tokensOut,
336
+ minTokensOut,
337
+ rate,
338
+ capacity: {
339
+ epochCapNumeraire: epoch.epochCapNumeraire,
340
+ epochRedeemedNumeraire: epoch.epochRedeemedNumeraire,
341
+ remainingNumeraire,
342
+ remainingTokens,
343
+ requestNumeraire,
344
+ exceedsCapacity: requestNumeraire > remainingNumeraire,
345
+ },
346
+ unitsLockedUntil,
347
+ context: {
348
+ vaultId: params.vaultId,
349
+ chainId,
350
+ tokenAddress: token.address,
351
+ ...(params.account ? { account: params.account } : {}),
352
+ slippageBps,
353
+ blockNumber: quoteReadContext.blockNumber,
354
+ request,
355
+ },
356
+ };
357
+
358
+ if (kind === 'redeem') return { kind, ...quoteDetails };
359
+ return { kind, ...quoteDetails };
360
+ }