@augustdigital/sdk 8.14.0 → 8.16.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.
- package/lib/adapters/solana/constants.d.ts +12 -4
- package/lib/adapters/solana/constants.js +23 -6
- package/lib/adapters/solana/idl/vault-idl.d.ts +103 -169
- package/lib/adapters/solana/idl/vault-idl.js +461 -475
- package/lib/adapters/solana/index.d.ts +40 -5
- package/lib/adapters/solana/index.js +47 -3
- package/lib/adapters/solana/utils.d.ts +1 -4
- package/lib/adapters/solana/utils.js +24 -10
- package/lib/core/analytics/method-taxonomy.js +4 -0
- package/lib/core/analytics/version.d.ts +1 -1
- package/lib/core/analytics/version.js +1 -1
- package/lib/core/errors/index.d.ts +39 -1
- package/lib/core/errors/index.js +43 -1
- package/lib/modules/vaults/getters.d.ts +66 -0
- package/lib/modules/vaults/getters.js +74 -0
- package/lib/modules/vaults/utils.d.ts +19 -1
- package/lib/modules/vaults/utils.js +32 -0
- package/lib/sdk.d.ts +10520 -10254
- package/lib/types/vaults.d.ts +95 -0
- package/lib/types/webserver.d.ts +63 -0
- package/package.json +2 -1
|
@@ -19,7 +19,7 @@ export declare const Solana: {
|
|
|
19
19
|
isSolanaAddress: typeof import("./utils").isSolanaAddress;
|
|
20
20
|
isSolana: (signature: string) => boolean;
|
|
21
21
|
getProgram: ({ provider, idl, programId, network, }: {
|
|
22
|
-
idl
|
|
22
|
+
idl?: any;
|
|
23
23
|
programId?: string;
|
|
24
24
|
} & import("./types").ISolanaConnectionOptions) => import("@coral-xyz/anchor").Program<any>;
|
|
25
25
|
getProvider: ({ connection, publicKey, signTransaction, }: {
|
|
@@ -83,9 +83,6 @@ export declare const Solana: {
|
|
|
83
83
|
"mainnet-beta": {
|
|
84
84
|
vault: string;
|
|
85
85
|
};
|
|
86
|
-
testnet: {
|
|
87
|
-
vault: string;
|
|
88
|
-
};
|
|
89
86
|
localnet: {
|
|
90
87
|
vault: string;
|
|
91
88
|
};
|
|
@@ -128,7 +125,36 @@ declare class SolanaAdapter {
|
|
|
128
125
|
get connection(): web3.Connection;
|
|
129
126
|
get provider(): AnchorProvider;
|
|
130
127
|
setWalletProvider(_publicKey: PublicKey | string, signTransaction: (<T extends Transaction | web3.VersionedTransaction>(transaction: T) => Promise<T>) | undefined): void;
|
|
131
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Build an Anchor `Program` bound to this adapter's provider and network.
|
|
130
|
+
*
|
|
131
|
+
* The program id is resolved as: explicit `programId` → the `address` carried
|
|
132
|
+
* by a custom `programIdl` → the canonical deployment for this adapter's
|
|
133
|
+
* network. A copy of the SDK's bundled `vaultIdl` defers to the network
|
|
134
|
+
* default (its embedded `address` is a build stamp, not network-aware), so
|
|
135
|
+
* forwarding it on devnet never silently targets the mainnet program.
|
|
136
|
+
*
|
|
137
|
+
* @param programIdl - Anchor IDL to bind. Defaults to the SDK's bundled
|
|
138
|
+
* `august_vault` IDL. A custom IDL's `address` is honoured when it differs
|
|
139
|
+
* from the bundled build stamp.
|
|
140
|
+
* @param programId - Base58 program id that overrides both the IDL's address
|
|
141
|
+
* and the network default. Use this to target a non-canonical deployment.
|
|
142
|
+
* @returns An Anchor `Program` for the resolved program id. No RPC is made —
|
|
143
|
+
* construction is offline.
|
|
144
|
+
* @throws {@link AugustValidationError} if no program id can be resolved
|
|
145
|
+
* because this adapter's network has no canonical deployment (e.g.
|
|
146
|
+
* `testnet`) and neither `programId` nor a custom IDL address was supplied.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* // canonical program on the adapter's network
|
|
151
|
+
* const program = sdk.solana.getProgram();
|
|
152
|
+
*
|
|
153
|
+
* // a different deployment, same interface
|
|
154
|
+
* const legacy = sdk.solana.getProgram(vaultIdl, '7B8n9vL51b6ibqRAd1adZsi3x3kxtq5NZaondh22Vkyq');
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
getProgram(programIdl?: any, programId?: string): import("@coral-xyz/anchor").Program<any>;
|
|
132
158
|
getVaultState(vaultProgramId: PublicKey | string, idl: any, vaultAddress?: PublicKey | string): Promise<{
|
|
133
159
|
vaultState: unknown | null;
|
|
134
160
|
depositMintDecimals: number | null;
|
|
@@ -193,6 +219,15 @@ declare class SolanaAdapter {
|
|
|
193
219
|
* @param redeemShares `bigint` (raw share units) or `number` (UI amount).
|
|
194
220
|
*/
|
|
195
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>;
|
|
222
|
+
/**
|
|
223
|
+
* Canonical program id for a program type on this adapter's network.
|
|
224
|
+
*
|
|
225
|
+
* @param type - Program to look up. Only `'vault'` (august_vault) exists today.
|
|
226
|
+
* @returns The base58 program id.
|
|
227
|
+
* @throws {@link AugustValidationError} if the canonical program is not
|
|
228
|
+
* deployed on this adapter's network (e.g. `testnet`). Previously an
|
|
229
|
+
* unmapped network threw a bare `TypeError`.
|
|
230
|
+
*/
|
|
196
231
|
getProgramId(type: 'vault'): string;
|
|
197
232
|
}
|
|
198
233
|
export default SolanaAdapter;
|
|
@@ -46,6 +46,7 @@ const web3_js_1 = require("@solana/web3.js");
|
|
|
46
46
|
const utils_1 = require("./utils");
|
|
47
47
|
const vault_actions_1 = require("./vault.actions");
|
|
48
48
|
const constants_1 = require("./constants");
|
|
49
|
+
const core_1 = require("../../core");
|
|
49
50
|
exports.Solana = {
|
|
50
51
|
utils: utils_1.SolanaUtils,
|
|
51
52
|
constants: SolanaConstants,
|
|
@@ -108,11 +109,41 @@ class SolanaAdapter {
|
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Build an Anchor `Program` bound to this adapter's provider and network.
|
|
114
|
+
*
|
|
115
|
+
* The program id is resolved as: explicit `programId` → the `address` carried
|
|
116
|
+
* by a custom `programIdl` → the canonical deployment for this adapter's
|
|
117
|
+
* network. A copy of the SDK's bundled `vaultIdl` defers to the network
|
|
118
|
+
* default (its embedded `address` is a build stamp, not network-aware), so
|
|
119
|
+
* forwarding it on devnet never silently targets the mainnet program.
|
|
120
|
+
*
|
|
121
|
+
* @param programIdl - Anchor IDL to bind. Defaults to the SDK's bundled
|
|
122
|
+
* `august_vault` IDL. A custom IDL's `address` is honoured when it differs
|
|
123
|
+
* from the bundled build stamp.
|
|
124
|
+
* @param programId - Base58 program id that overrides both the IDL's address
|
|
125
|
+
* and the network default. Use this to target a non-canonical deployment.
|
|
126
|
+
* @returns An Anchor `Program` for the resolved program id. No RPC is made —
|
|
127
|
+
* construction is offline.
|
|
128
|
+
* @throws {@link AugustValidationError} if no program id can be resolved
|
|
129
|
+
* because this adapter's network has no canonical deployment (e.g.
|
|
130
|
+
* `testnet`) and neither `programId` nor a custom IDL address was supplied.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* // canonical program on the adapter's network
|
|
135
|
+
* const program = sdk.solana.getProgram();
|
|
136
|
+
*
|
|
137
|
+
* // a different deployment, same interface
|
|
138
|
+
* const legacy = sdk.solana.getProgram(vaultIdl, '7B8n9vL51b6ibqRAd1adZsi3x3kxtq5NZaondh22Vkyq');
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
getProgram(programIdl, programId) {
|
|
112
142
|
return utils_1.SolanaUtils.getProgram({
|
|
113
143
|
network: this._network,
|
|
114
144
|
provider: this._provider,
|
|
115
145
|
idl: programIdl,
|
|
146
|
+
programId,
|
|
116
147
|
});
|
|
117
148
|
}
|
|
118
149
|
async getVaultState(vaultProgramId, idl, vaultAddress) {
|
|
@@ -238,9 +269,22 @@ class SolanaAdapter {
|
|
|
238
269
|
sendTransaction,
|
|
239
270
|
});
|
|
240
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Canonical program id for a program type on this adapter's network.
|
|
274
|
+
*
|
|
275
|
+
* @param type - Program to look up. Only `'vault'` (august_vault) exists today.
|
|
276
|
+
* @returns The base58 program id.
|
|
277
|
+
* @throws {@link AugustValidationError} if the canonical program is not
|
|
278
|
+
* deployed on this adapter's network (e.g. `testnet`). Previously an
|
|
279
|
+
* unmapped network threw a bare `TypeError`.
|
|
280
|
+
*/
|
|
241
281
|
getProgramId(type) {
|
|
242
|
-
const
|
|
243
|
-
|
|
282
|
+
const deployment = (0, constants_1.getDeployedProgramIds)(this._network);
|
|
283
|
+
if (!deployment) {
|
|
284
|
+
throw new core_1.AugustValidationError('INVALID_INPUT', `No canonical august_vault deployment for Solana network '${this._network}'. ` +
|
|
285
|
+
`Deployed networks: ${Object.keys(constants_1.programIds).join(', ')}.`);
|
|
286
|
+
}
|
|
287
|
+
return deployment[type];
|
|
244
288
|
}
|
|
245
289
|
}
|
|
246
290
|
exports.default = SolanaAdapter;
|
|
@@ -9,7 +9,7 @@ import { isSolanaAddress } from '../../core/helpers/chain-address';
|
|
|
9
9
|
export { isSolanaAddress };
|
|
10
10
|
declare function isSolana(signature: string): boolean;
|
|
11
11
|
declare function getProgram({ provider, idl, programId, network, }: {
|
|
12
|
-
idl
|
|
12
|
+
idl?: any;
|
|
13
13
|
programId?: string;
|
|
14
14
|
} & ISolanaConnectionOptions): Program<any>;
|
|
15
15
|
declare function getBestRpcEndpoint({ endpoint, network, connection, }: ISolanaConnectionOptions): Promise<"http://127.0.0.1:8899" | `https://${string}`>;
|
|
@@ -132,9 +132,6 @@ export declare const SolanaUtils: {
|
|
|
132
132
|
"mainnet-beta": {
|
|
133
133
|
vault: string;
|
|
134
134
|
};
|
|
135
|
-
testnet: {
|
|
136
|
-
vault: string;
|
|
137
|
-
};
|
|
138
135
|
localnet: {
|
|
139
136
|
vault: string;
|
|
140
137
|
};
|
|
@@ -39,18 +39,32 @@ function isSolana(signature) {
|
|
|
39
39
|
return (signature.length >= 80 && signature.length <= 100);
|
|
40
40
|
}
|
|
41
41
|
function getProgram({ provider, idl, programId, network, }) {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
const baseIdl = idl ?? constants_1.vaultIdl;
|
|
43
|
+
// Precedence: explicit `programId` → a custom IDL's `address` → the network
|
|
44
|
+
// default → the bundled IDL's embedded `address`.
|
|
45
|
+
//
|
|
46
|
+
// The custom-address check compares by VALUE, not reference. Keying off
|
|
47
|
+
// `idl === vaultIdl` broke for a structurally-identical-but-not-reference-equal
|
|
48
|
+
// copy (second bundle instance, or JSON round-tripped), silently retargeting
|
|
49
|
+
// devnet callers at mainnet. Every copy carries the same build stamp, so it
|
|
50
|
+
// still defers to the network default — but a genuinely custom IDL is honoured.
|
|
51
|
+
const suppliedAddress = typeof baseIdl?.address === 'string' && baseIdl.address !== constants_1.vaultIdl.address
|
|
52
|
+
? baseIdl.address
|
|
53
|
+
: undefined;
|
|
54
|
+
const explicitId = programId ?? suppliedAddress;
|
|
55
|
+
if (explicitId) {
|
|
56
|
+
return new anchor_1.Program({ ...baseIdl, address: explicitId }, provider);
|
|
44
57
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
if (network) {
|
|
59
|
+
const networkDefault = (0, constants_1.getDeployedProgramIds)(network)?.vault;
|
|
60
|
+
if (!networkDefault) {
|
|
61
|
+
throw new core_1.AugustValidationError('INVALID_INPUT', `No canonical august_vault deployment for Solana network '${network}'. ` +
|
|
62
|
+
`Deployed networks: ${Object.keys(constants_1.programIds).join(', ')}. ` +
|
|
63
|
+
`Pass an explicit \`programId\` (or an IDL carrying the target \`address\`) to target a program on this network.`);
|
|
64
|
+
}
|
|
65
|
+
return new anchor_1.Program({ ...baseIdl, address: networkDefault }, provider);
|
|
52
66
|
}
|
|
53
|
-
return new anchor_1.Program(
|
|
67
|
+
return new anchor_1.Program(baseIdl, provider);
|
|
54
68
|
}
|
|
55
69
|
// ============================================================================
|
|
56
70
|
// SOLANA PROVIDERS
|
|
@@ -77,6 +77,10 @@ exports.METHOD_CATEGORIES = {
|
|
|
77
77
|
fetchUserTokenBalance: 'read.solana',
|
|
78
78
|
fetchUserShareBalance: 'read.solana',
|
|
79
79
|
fetchUserShareBalanceRaw: 'read.solana',
|
|
80
|
+
// Both are offline resolvers (no RPC) — classified so the partner dashboard
|
|
81
|
+
// sees program-targeting calls instead of dropping them into 'unknown'.
|
|
82
|
+
getProgram: 'read.solana',
|
|
83
|
+
getProgramId: 'read.solana',
|
|
80
84
|
// submitTransaction broadcasts a signed Soroban tx — it mutates on-chain
|
|
81
85
|
// state and belongs in the write bucket even though Stellar adapter call
|
|
82
86
|
// shape mirrors the read helpers (returns a hash, takes signed XDR).
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
/** Stable error codes attached to SDK errors. */
|
|
18
|
-
export type AugustErrorCode = 'AUTH_MISSING_KEY' | 'AUTH_INVALID_KEY' | 'AUTH_FORBIDDEN' | 'AUTH_UNAUTHORIZED' | 'INVALID_URL' | 'INVALID_INPUT' | 'INVALID_CHAIN' | 'INVALID_ADDRESS' | 'ACCOUNT_NOT_FUNDED' | 'NETWORK_ERROR' | 'TIMEOUT' | 'RATE_LIMITED' | 'SERVER_ERROR' | 'UNKNOWN';
|
|
18
|
+
export type AugustErrorCode = 'AUTH_MISSING_KEY' | 'AUTH_INVALID_KEY' | 'AUTH_FORBIDDEN' | 'AUTH_UNAUTHORIZED' | 'INVALID_URL' | 'INVALID_INPUT' | 'INVALID_CHAIN' | 'INVALID_ADDRESS' | 'ACCOUNT_NOT_FUNDED' | 'NETWORK_ERROR' | 'TIMEOUT' | 'RATE_LIMITED' | 'SERVER_ERROR' | 'HISTORY_UNAVAILABLE' | 'UNKNOWN';
|
|
19
19
|
/** Options accepted by every SDK error constructor. */
|
|
20
20
|
export interface AugustErrorOptions {
|
|
21
21
|
/** Original error preserved for stack-trace chaining. */
|
|
@@ -72,5 +72,43 @@ export declare class AugustServerError extends AugustSDKError {
|
|
|
72
72
|
constructor(status: number, message: string, options?: AugustErrorOptions);
|
|
73
73
|
toJSON(): Record<string, unknown>;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Transaction/position history required to compute a value could not be
|
|
77
|
+
* retrieved or was empty, so the computed result would be wrong rather than
|
|
78
|
+
* merely missing.
|
|
79
|
+
*
|
|
80
|
+
* Motivation — the 2026-07-09 lifetime-PnL over-inflation incident. Lifetime
|
|
81
|
+
* PnL is `currentPosition + totalWithdrawn - totalDeposited`. Every
|
|
82
|
+
* history-fetch failure (deleted subgraph, subgraph deployed against the wrong
|
|
83
|
+
* ABI, missing archive backfill, deposits indexed on a different
|
|
84
|
+
* chain/contract) collapses the deposit/withdrawal history to empty. With
|
|
85
|
+
* `deposited = 0` and `withdrawn = 0` but a live on-chain `position > 0`, the
|
|
86
|
+
* formula degenerates to reporting the entire position as profit. Throwing this
|
|
87
|
+
* typed error makes that unrecoverable state fail loudly instead of returning
|
|
88
|
+
* an inflated number that looks plausible.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* try {
|
|
93
|
+
* await sdk.vaults.getVaultUserLifetimePnl({ vault, wallet, options });
|
|
94
|
+
* } catch (err) {
|
|
95
|
+
* if (err instanceof AugustHistoryUnavailableError) {
|
|
96
|
+
* // History backfill is broken — surface "temporarily unavailable",
|
|
97
|
+
* // never a fabricated PnL number.
|
|
98
|
+
* showUnavailable(err.context);
|
|
99
|
+
* } else throw err;
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare class AugustHistoryUnavailableError extends AugustSDKError {
|
|
104
|
+
/**
|
|
105
|
+
* @param message - Human-readable description of the missing history. Callers
|
|
106
|
+
* pass the same `#<method>::<vault>::<wallet>` prefix the surrounding getter
|
|
107
|
+
* uses so log grouping stays consistent.
|
|
108
|
+
* @param options - Standard SDK error options. `context` should carry the
|
|
109
|
+
* `{ vault, wallet }` (or equivalent) identifiers that scope the failure.
|
|
110
|
+
*/
|
|
111
|
+
constructor(message: string, options?: AugustErrorOptions);
|
|
112
|
+
}
|
|
75
113
|
/** Type guard. Works across realms (e.g. Web Worker / VM contexts). */
|
|
76
114
|
export declare function isAugustSDKError(err: unknown): err is AugustSDKError;
|
package/lib/core/errors/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.AugustServerError = exports.AugustRateLimitError = exports.AugustValidationError = exports.AugustTimeoutError = exports.AugustNetworkError = exports.AugustAuthError = exports.AugustSDKError = void 0;
|
|
19
|
+
exports.AugustHistoryUnavailableError = exports.AugustServerError = exports.AugustRateLimitError = exports.AugustValidationError = exports.AugustTimeoutError = exports.AugustNetworkError = exports.AugustAuthError = exports.AugustSDKError = void 0;
|
|
20
20
|
exports.isAugustSDKError = isAugustSDKError;
|
|
21
21
|
/** Base class for all SDK errors. */
|
|
22
22
|
class AugustSDKError extends Error {
|
|
@@ -131,6 +131,48 @@ class AugustServerError extends AugustSDKError {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
exports.AugustServerError = AugustServerError;
|
|
134
|
+
/**
|
|
135
|
+
* Transaction/position history required to compute a value could not be
|
|
136
|
+
* retrieved or was empty, so the computed result would be wrong rather than
|
|
137
|
+
* merely missing.
|
|
138
|
+
*
|
|
139
|
+
* Motivation — the 2026-07-09 lifetime-PnL over-inflation incident. Lifetime
|
|
140
|
+
* PnL is `currentPosition + totalWithdrawn - totalDeposited`. Every
|
|
141
|
+
* history-fetch failure (deleted subgraph, subgraph deployed against the wrong
|
|
142
|
+
* ABI, missing archive backfill, deposits indexed on a different
|
|
143
|
+
* chain/contract) collapses the deposit/withdrawal history to empty. With
|
|
144
|
+
* `deposited = 0` and `withdrawn = 0` but a live on-chain `position > 0`, the
|
|
145
|
+
* formula degenerates to reporting the entire position as profit. Throwing this
|
|
146
|
+
* typed error makes that unrecoverable state fail loudly instead of returning
|
|
147
|
+
* an inflated number that looks plausible.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* try {
|
|
152
|
+
* await sdk.vaults.getVaultUserLifetimePnl({ vault, wallet, options });
|
|
153
|
+
* } catch (err) {
|
|
154
|
+
* if (err instanceof AugustHistoryUnavailableError) {
|
|
155
|
+
* // History backfill is broken — surface "temporarily unavailable",
|
|
156
|
+
* // never a fabricated PnL number.
|
|
157
|
+
* showUnavailable(err.context);
|
|
158
|
+
* } else throw err;
|
|
159
|
+
* }
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
class AugustHistoryUnavailableError extends AugustSDKError {
|
|
163
|
+
/**
|
|
164
|
+
* @param message - Human-readable description of the missing history. Callers
|
|
165
|
+
* pass the same `#<method>::<vault>::<wallet>` prefix the surrounding getter
|
|
166
|
+
* uses so log grouping stays consistent.
|
|
167
|
+
* @param options - Standard SDK error options. `context` should carry the
|
|
168
|
+
* `{ vault, wallet }` (or equivalent) identifiers that scope the failure.
|
|
169
|
+
*/
|
|
170
|
+
constructor(message, options) {
|
|
171
|
+
super('HISTORY_UNAVAILABLE', message, options);
|
|
172
|
+
this.name = 'AugustHistoryUnavailableError';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
exports.AugustHistoryUnavailableError = AugustHistoryUnavailableError;
|
|
134
176
|
/** Type guard. Works across realms (e.g. Web Worker / VM contexts). */
|
|
135
177
|
function isAugustSDKError(err) {
|
|
136
178
|
return (err instanceof AugustSDKError ||
|
|
@@ -273,6 +273,72 @@ export declare function getYieldLastRealizedOn({ vault, options, }: {
|
|
|
273
273
|
vault: IAddress;
|
|
274
274
|
options: IVaultBaseOptions;
|
|
275
275
|
}): Promise<number>;
|
|
276
|
+
/** Inputs the lifetime-PnL history guard inspects. */
|
|
277
|
+
export interface PnlHistoryConsistencyParams {
|
|
278
|
+
/** Vault contract address, used for the error message and context. */
|
|
279
|
+
vault: IAddress;
|
|
280
|
+
/** User wallet address, used for the error message and context. */
|
|
281
|
+
wallet: IAddress;
|
|
282
|
+
/**
|
|
283
|
+
* Whether the resolved deposit history contains at least one deposit record
|
|
284
|
+
* (subgraph deposits or LayerZero cross-chain deposits).
|
|
285
|
+
*/
|
|
286
|
+
hasDeposits: boolean;
|
|
287
|
+
/** Total withdrawn, in raw base units (asset decimals). */
|
|
288
|
+
totalWithdrawnRaw: bigint;
|
|
289
|
+
/** Current on-chain position value, in raw base units (asset decimals). */
|
|
290
|
+
currentPositionRaw: bigint;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Guard against the 2026-07-09 lifetime-PnL over-inflation failure mode.
|
|
294
|
+
*
|
|
295
|
+
* Lifetime PnL is `currentPosition + totalWithdrawn - totalDeposited`. When the
|
|
296
|
+
* deposit/withdrawal history fails to load it collapses to empty, so both
|
|
297
|
+
* `totalDeposited` and `totalWithdrawn` read as zero. If the wallet still holds
|
|
298
|
+
* a live on-chain position, the formula degenerates to reporting the entire
|
|
299
|
+
* position as pure profit — a plausible-looking but wrong number.
|
|
300
|
+
*
|
|
301
|
+
* This guard rejects exactly that state: a non-zero position with no recorded
|
|
302
|
+
* deposits *and* no recorded withdrawals. Any of the following is treated as
|
|
303
|
+
* consistent and passes:
|
|
304
|
+
* - deposits are present (the normal, computable case);
|
|
305
|
+
* - withdrawals are present (a fully-exited-then-re-entered position is still
|
|
306
|
+
* computable — the withdrawal records prove history loaded);
|
|
307
|
+
* - the current position is zero (the wallet never interacted or fully exited,
|
|
308
|
+
* so a zero PnL is correct, not inflated).
|
|
309
|
+
*
|
|
310
|
+
* Pure and synchronous — no RPC, no I/O — so it is safe to call inside the hot
|
|
311
|
+
* path with no added latency.
|
|
312
|
+
*
|
|
313
|
+
* Known imprecision — vault shares are transferable ERC-20 receipt tokens, so a
|
|
314
|
+
* wallet that *received* shares via a direct token transfer (never deposited,
|
|
315
|
+
* never withdrew) legitimately has `position > 0` with no history records and
|
|
316
|
+
* will throw `HISTORY_UNAVAILABLE` even though its history loaded fine. This is
|
|
317
|
+
* not a regression — the pre-guard formula reported that same position as 100%
|
|
318
|
+
* profit, which was also wrong — but the error message is misleading for that
|
|
319
|
+
* (rare) path. If receipt-token transfer becomes a real user flow, distinguish
|
|
320
|
+
* it here (e.g. by checking for a share-transfer event) rather than widening
|
|
321
|
+
* the guard.
|
|
322
|
+
*
|
|
323
|
+
* @param params - See {@link PnlHistoryConsistencyParams}.
|
|
324
|
+
* @returns `void` when the history is consistent.
|
|
325
|
+
* @throws {@link AugustHistoryUnavailableError} with code `HISTORY_UNAVAILABLE`
|
|
326
|
+
* and `context: { vault, wallet }` when a non-zero position has neither
|
|
327
|
+
* deposits nor withdrawals recorded. The message carries the
|
|
328
|
+
* `#getVaultUserLifetimePnl::<vault>::<wallet>` prefix so it groups with the
|
|
329
|
+
* other errors leaving the getter.
|
|
330
|
+
* @example
|
|
331
|
+
* ```ts
|
|
332
|
+
* assertPnlHistoryConsistent({
|
|
333
|
+
* vault,
|
|
334
|
+
* wallet,
|
|
335
|
+
* hasDeposits: depositsByAsset.size > 0,
|
|
336
|
+
* totalWithdrawnRaw,
|
|
337
|
+
* currentPositionRaw: currentPositionValue.raw,
|
|
338
|
+
* });
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
export declare function assertPnlHistoryConsistent({ vault, wallet, hasDeposits, totalWithdrawnRaw, currentPositionRaw, }: PnlHistoryConsistencyParams): void;
|
|
276
342
|
/**
|
|
277
343
|
* Calculate lifetime PnL for a user in a specific vault.
|
|
278
344
|
*
|
|
@@ -53,6 +53,7 @@ exports.getUserPoints = getUserPoints;
|
|
|
53
53
|
exports.registerUserForPoints = registerUserForPoints;
|
|
54
54
|
exports.fetchPointsLeaderboard = fetchPointsLeaderboard;
|
|
55
55
|
exports.getYieldLastRealizedOn = getYieldLastRealizedOn;
|
|
56
|
+
exports.assertPnlHistoryConsistent = assertPnlHistoryConsistent;
|
|
56
57
|
exports.getVaultUserLifetimePnl = getVaultUserLifetimePnl;
|
|
57
58
|
exports.getVaultPnl = getVaultPnl;
|
|
58
59
|
exports.getVaultHistoricalTimeseries = getVaultHistoricalTimeseries;
|
|
@@ -85,6 +86,7 @@ const date_utils_1 = require("./utils/date-utils");
|
|
|
85
86
|
const call_data_decoder_1 = require("./utils/call-data-decoder");
|
|
86
87
|
const deposits_1 = require("../../services/layerzero/deposits");
|
|
87
88
|
const redeems_1 = require("../../services/layerzero/redeems");
|
|
89
|
+
const errors_1 = require("../../core/errors");
|
|
88
90
|
/**
|
|
89
91
|
* Vault Data Getters
|
|
90
92
|
*
|
|
@@ -1840,6 +1842,62 @@ async function getYieldLastRealizedOn({ vault, options, }) {
|
|
|
1840
1842
|
throw new Error(`#getYieldLastRealizedOn::${vault}:${errorMessage}`);
|
|
1841
1843
|
}
|
|
1842
1844
|
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Guard against the 2026-07-09 lifetime-PnL over-inflation failure mode.
|
|
1847
|
+
*
|
|
1848
|
+
* Lifetime PnL is `currentPosition + totalWithdrawn - totalDeposited`. When the
|
|
1849
|
+
* deposit/withdrawal history fails to load it collapses to empty, so both
|
|
1850
|
+
* `totalDeposited` and `totalWithdrawn` read as zero. If the wallet still holds
|
|
1851
|
+
* a live on-chain position, the formula degenerates to reporting the entire
|
|
1852
|
+
* position as pure profit — a plausible-looking but wrong number.
|
|
1853
|
+
*
|
|
1854
|
+
* This guard rejects exactly that state: a non-zero position with no recorded
|
|
1855
|
+
* deposits *and* no recorded withdrawals. Any of the following is treated as
|
|
1856
|
+
* consistent and passes:
|
|
1857
|
+
* - deposits are present (the normal, computable case);
|
|
1858
|
+
* - withdrawals are present (a fully-exited-then-re-entered position is still
|
|
1859
|
+
* computable — the withdrawal records prove history loaded);
|
|
1860
|
+
* - the current position is zero (the wallet never interacted or fully exited,
|
|
1861
|
+
* so a zero PnL is correct, not inflated).
|
|
1862
|
+
*
|
|
1863
|
+
* Pure and synchronous — no RPC, no I/O — so it is safe to call inside the hot
|
|
1864
|
+
* path with no added latency.
|
|
1865
|
+
*
|
|
1866
|
+
* Known imprecision — vault shares are transferable ERC-20 receipt tokens, so a
|
|
1867
|
+
* wallet that *received* shares via a direct token transfer (never deposited,
|
|
1868
|
+
* never withdrew) legitimately has `position > 0` with no history records and
|
|
1869
|
+
* will throw `HISTORY_UNAVAILABLE` even though its history loaded fine. This is
|
|
1870
|
+
* not a regression — the pre-guard formula reported that same position as 100%
|
|
1871
|
+
* profit, which was also wrong — but the error message is misleading for that
|
|
1872
|
+
* (rare) path. If receipt-token transfer becomes a real user flow, distinguish
|
|
1873
|
+
* it here (e.g. by checking for a share-transfer event) rather than widening
|
|
1874
|
+
* the guard.
|
|
1875
|
+
*
|
|
1876
|
+
* @param params - See {@link PnlHistoryConsistencyParams}.
|
|
1877
|
+
* @returns `void` when the history is consistent.
|
|
1878
|
+
* @throws {@link AugustHistoryUnavailableError} with code `HISTORY_UNAVAILABLE`
|
|
1879
|
+
* and `context: { vault, wallet }` when a non-zero position has neither
|
|
1880
|
+
* deposits nor withdrawals recorded. The message carries the
|
|
1881
|
+
* `#getVaultUserLifetimePnl::<vault>::<wallet>` prefix so it groups with the
|
|
1882
|
+
* other errors leaving the getter.
|
|
1883
|
+
* @example
|
|
1884
|
+
* ```ts
|
|
1885
|
+
* assertPnlHistoryConsistent({
|
|
1886
|
+
* vault,
|
|
1887
|
+
* wallet,
|
|
1888
|
+
* hasDeposits: depositsByAsset.size > 0,
|
|
1889
|
+
* totalWithdrawnRaw,
|
|
1890
|
+
* currentPositionRaw: currentPositionValue.raw,
|
|
1891
|
+
* });
|
|
1892
|
+
* ```
|
|
1893
|
+
*/
|
|
1894
|
+
function assertPnlHistoryConsistent({ vault, wallet, hasDeposits, totalWithdrawnRaw, currentPositionRaw, }) {
|
|
1895
|
+
const positionExists = currentPositionRaw > BigInt(0);
|
|
1896
|
+
const noHistory = !hasDeposits && totalWithdrawnRaw === BigInt(0);
|
|
1897
|
+
if (positionExists && noHistory) {
|
|
1898
|
+
throw new errors_1.AugustHistoryUnavailableError(`#getVaultUserLifetimePnl::${vault}::${wallet}:transaction history unavailable — a live position exists but no deposits or withdrawals were indexed, so lifetime PnL cannot be computed without over-reporting the entire position as profit`, { context: { vault, wallet } });
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1843
1901
|
/**
|
|
1844
1902
|
* Calculate lifetime PnL for a user in a specific vault.
|
|
1845
1903
|
*
|
|
@@ -2171,6 +2229,17 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2171
2229
|
totalWithdrawnRaw += totalWithdrawalsInShares;
|
|
2172
2230
|
}
|
|
2173
2231
|
const totalWithdrawn = (0, core_1.toNormalizedBn)(totalWithdrawnRaw, decimals);
|
|
2232
|
+
// Guard the 2026-07-09 over-inflation incident: a live position with no
|
|
2233
|
+
// deposits *and* no withdrawals means the history failed to load, and the
|
|
2234
|
+
// PnL formula would report the whole position as profit. Fail loudly with a
|
|
2235
|
+
// typed error instead of returning a fabricated number.
|
|
2236
|
+
assertPnlHistoryConsistent({
|
|
2237
|
+
vault,
|
|
2238
|
+
wallet,
|
|
2239
|
+
hasDeposits: depositsByAsset.size > 0,
|
|
2240
|
+
totalWithdrawnRaw,
|
|
2241
|
+
currentPositionRaw: BigInt(currentPositionValue.raw),
|
|
2242
|
+
});
|
|
2174
2243
|
const tokenPriceUsd = await pricePromise;
|
|
2175
2244
|
// Validate token price and log warning if invalid
|
|
2176
2245
|
const effectiveTokenPrice = tokenPriceUsd > 0 ? tokenPriceUsd : 1;
|
|
@@ -2223,6 +2292,11 @@ async function getVaultUserLifetimePnl({ vault, wallet, options, }) {
|
|
|
2223
2292
|
}
|
|
2224
2293
|
catch (e) {
|
|
2225
2294
|
core_1.Logger.log.error('getVaultUserLifetimePnl', e, { vault, wallet });
|
|
2295
|
+
// Preserve the typed history guard so callers can distinguish an
|
|
2296
|
+
// unavailable-history failure from a generic getter error. Its message
|
|
2297
|
+
// already carries the `#getVaultUserLifetimePnl::vault::wallet` prefix.
|
|
2298
|
+
if (e instanceof errors_1.AugustHistoryUnavailableError)
|
|
2299
|
+
throw e;
|
|
2226
2300
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
2227
2301
|
throw new Error(`#getVaultUserLifetimePnl::${vault}::${wallet}:${errorMessage}`);
|
|
2228
2302
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IAddress, IContractRunner, INormalizedNumber, IPoolFunctions, ITokenizedVault, IVault } from '../../types';
|
|
1
|
+
import type { IAddress, IContractRunner, INormalizedNumber, IPoolFunctions, ITokenizedVault, IVault, IVaultFreshness } from '../../types';
|
|
2
2
|
import { ethers } from 'ethers';
|
|
3
3
|
import type { WalletClient } from 'viem';
|
|
4
4
|
/**
|
|
@@ -59,6 +59,24 @@ export declare function getYieldLastRealizedOn(provider: IContractRunner, vaultA
|
|
|
59
59
|
* Shared across all chain adapters (EVM, Solana, Stellar).
|
|
60
60
|
*/
|
|
61
61
|
export declare function formatVaultApy(tokenizedVault: ITokenizedVault): IVault['apy'];
|
|
62
|
+
/**
|
|
63
|
+
* Map the backend `freshness` object onto the camelCased {@link IVaultFreshness}
|
|
64
|
+
* shape carried on `IVault`. Shared across all chain adapters (EVM, Solana,
|
|
65
|
+
* Stellar) so every surface reports provenance identically.
|
|
66
|
+
*
|
|
67
|
+
* Each of the three stamps is passed through independently: a missing member
|
|
68
|
+
* becomes `null` and is never defaulted from one of the others. That is the point
|
|
69
|
+
* of the object — `apy_computed_at` and `share_ratio_snapshot_at` describe two
|
|
70
|
+
* different pipelines, and collapsing them would erase the very staleness signal
|
|
71
|
+
* (snapshot newer than APY) the field exists to expose.
|
|
72
|
+
*
|
|
73
|
+
* @param tokenizedVault - Backend tokenized-vault payload. `freshness` is absent on
|
|
74
|
+
* responses predating the backend change, and may be explicitly null.
|
|
75
|
+
* @returns The mapped freshness object, or `null` when the backend reported none.
|
|
76
|
+
* Values are the backend's RFC 3339 UTC strings verbatim — no reformatting, so
|
|
77
|
+
* the trailing `Z` is preserved.
|
|
78
|
+
*/
|
|
79
|
+
export declare function mapVaultFreshness(tokenizedVault: ITokenizedVault | undefined | null): IVaultFreshness | null;
|
|
62
80
|
/**
|
|
63
81
|
* Map strategists from backend subaccounts or hardcoded list.
|
|
64
82
|
* Shared across all chain adapters.
|
|
@@ -6,6 +6,7 @@ exports.getVaultRewards = getVaultRewards;
|
|
|
6
6
|
exports.getIdleAssets = getIdleAssets;
|
|
7
7
|
exports.getYieldLastRealizedOn = getYieldLastRealizedOn;
|
|
8
8
|
exports.formatVaultApy = formatVaultApy;
|
|
9
|
+
exports.mapVaultFreshness = mapVaultFreshness;
|
|
9
10
|
exports.mapStrategists = mapStrategists;
|
|
10
11
|
exports.mapComposabilityIntegrations = mapComposabilityIntegrations;
|
|
11
12
|
exports.backendTvlToNormalizedBn = backendTvlToNormalizedBn;
|
|
@@ -235,6 +236,33 @@ function formatVaultApy(tokenizedVault) {
|
|
|
235
236
|
underlyingApy: (reported_apy?.underlying_apy ?? 0) * 100,
|
|
236
237
|
};
|
|
237
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Map the backend `freshness` object onto the camelCased {@link IVaultFreshness}
|
|
241
|
+
* shape carried on `IVault`. Shared across all chain adapters (EVM, Solana,
|
|
242
|
+
* Stellar) so every surface reports provenance identically.
|
|
243
|
+
*
|
|
244
|
+
* Each of the three stamps is passed through independently: a missing member
|
|
245
|
+
* becomes `null` and is never defaulted from one of the others. That is the point
|
|
246
|
+
* of the object — `apy_computed_at` and `share_ratio_snapshot_at` describe two
|
|
247
|
+
* different pipelines, and collapsing them would erase the very staleness signal
|
|
248
|
+
* (snapshot newer than APY) the field exists to expose.
|
|
249
|
+
*
|
|
250
|
+
* @param tokenizedVault - Backend tokenized-vault payload. `freshness` is absent on
|
|
251
|
+
* responses predating the backend change, and may be explicitly null.
|
|
252
|
+
* @returns The mapped freshness object, or `null` when the backend reported none.
|
|
253
|
+
* Values are the backend's RFC 3339 UTC strings verbatim — no reformatting, so
|
|
254
|
+
* the trailing `Z` is preserved.
|
|
255
|
+
*/
|
|
256
|
+
function mapVaultFreshness(tokenizedVault) {
|
|
257
|
+
const freshness = tokenizedVault?.freshness;
|
|
258
|
+
if (!freshness)
|
|
259
|
+
return null;
|
|
260
|
+
return {
|
|
261
|
+
apyComputedAt: freshness.apy_computed_at ?? null,
|
|
262
|
+
shareRatioSnapshotAt: freshness.share_ratio_snapshot_at ?? null,
|
|
263
|
+
cachedAt: freshness.cached_at ?? null,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
238
266
|
/**
|
|
239
267
|
* Map strategists from backend subaccounts or hardcoded list.
|
|
240
268
|
* Shared across all chain adapters.
|
|
@@ -330,6 +358,7 @@ function buildBackendVault(tokenizedVault, overrides) {
|
|
|
330
358
|
historical_snapshots: tokenizedVault.historical_snapshots || [],
|
|
331
359
|
address: tokenizedVault.address,
|
|
332
360
|
historical_apy: tokenizedVault.historical_apy,
|
|
361
|
+
historical_compound_apy: tokenizedVault.historical_compound_apy,
|
|
333
362
|
max_drawdown: tokenizedVault.max_drawdown,
|
|
334
363
|
logoUrl: tokenizedVault.vault_logo_url || '/img/strategist/august.svg',
|
|
335
364
|
description: tokenizedVault.description || '',
|
|
@@ -397,6 +426,7 @@ function buildBackendVault(tokenizedVault, overrides) {
|
|
|
397
426
|
composabilityIntegrations: mapComposabilityIntegrations(tokenizedVault),
|
|
398
427
|
strategists: mapStrategists(tokenizedVault),
|
|
399
428
|
cachedAt: tokenizedVault.cached_at ?? null,
|
|
429
|
+
freshness: mapVaultFreshness(tokenizedVault),
|
|
400
430
|
dailyPnlPerShare: mapDailyPnlPerShare(tokenizedVault.daily_pnl_per_share),
|
|
401
431
|
enabled_historical_price_horizons: tokenizedVault.enabled_historical_price_horizons || [],
|
|
402
432
|
latest_reported_tvl: tokenizedVault.latest_reported_tvl,
|
|
@@ -600,6 +630,7 @@ async function buildFormattedVault(provider, tokenizedVault, contractCalls) {
|
|
|
600
630
|
maxDepositAmount: maxDepositAmount,
|
|
601
631
|
receipt: receipt,
|
|
602
632
|
historical_apy: tokenizedVault?.historical_apy,
|
|
633
|
+
historical_compound_apy: tokenizedVault?.historical_compound_apy,
|
|
603
634
|
historical_snapshots: tokenizedVault?.historical_snapshots || [],
|
|
604
635
|
max_drawdown: tokenizedVault?.max_drawdown,
|
|
605
636
|
composabilityIntegrations: mapComposabilityIntegrations(tokenizedVault),
|
|
@@ -613,6 +644,7 @@ async function buildFormattedVault(provider, tokenizedVault, contractCalls) {
|
|
|
613
644
|
defaultApyHorizon: tokenizedVault?.default_apy_horizon ?? null,
|
|
614
645
|
latest_reported_tvl: tokenizedVault?.latest_reported_tvl,
|
|
615
646
|
cachedAt: tokenizedVault?.cached_at ?? null,
|
|
647
|
+
freshness: mapVaultFreshness(tokenizedVault),
|
|
616
648
|
showCapFilled: tokenizedVault?.show_cap_filled ?? null,
|
|
617
649
|
apyOverride: tokenizedVault?.apy_override ?? null,
|
|
618
650
|
};
|