@augustdigital/sdk 9.2.0 → 9.2.2
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/getters.js +8 -1
- package/lib/adapters/solana/utils.js +16 -2
- package/lib/core/analytics/sentry.js +38 -5
- package/lib/core/analytics/version.d.ts +1 -1
- package/lib/core/analytics/version.js +1 -1
- package/lib/modules/vaults/getters.d.ts +6 -2
- package/lib/modules/vaults/getters.js +24 -11
- package/lib/sdk.d.ts +6 -2
- package/lib/services/untangled/types.d.ts +3 -2
- package/lib/services/untangled/utils.d.ts +11 -4
- package/lib/services/untangled/utils.js +122 -14
- package/package.json +1 -1
|
@@ -48,7 +48,14 @@ const utils_2 = require("../../modules/vaults/utils");
|
|
|
48
48
|
* @returns Formatted vault object with Solana-specific fields
|
|
49
49
|
*/
|
|
50
50
|
const getSolanaVault = async (tokenizedVault, options) => {
|
|
51
|
-
|
|
51
|
+
// `options.rpcUrl` is resolved upstream as `providers[vault.chain]`, and that
|
|
52
|
+
// map is keyed by EVM chain id — Solana's synthetic id is never in it, so the
|
|
53
|
+
// field arrives undefined for every Solana vault however well the app has
|
|
54
|
+
// configured its Solana RPC. The connection built from that RPC *is* passed
|
|
55
|
+
// (`solanaService`), so take the endpoint off it rather than fetching against
|
|
56
|
+
// `undefined`.
|
|
57
|
+
const solanaRpcUrl = (options.rpcUrl ??
|
|
58
|
+
options.solanaService?.connection?.rpcEndpoint);
|
|
52
59
|
const solanaMetadata = tokenizedVault.solana_vault_metadata;
|
|
53
60
|
const programId = utils_1.SolanaUtils.resolveProgramId(tokenizedVault.address, solanaMetadata);
|
|
54
61
|
// Fallback decimals: prefer backend metadata, then the application default
|
|
@@ -333,6 +333,17 @@ async function getVaultMints({ vaultProgramId, vaultAddress, connection, idl, })
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
async function getToken({ mintAddress, endpoint, connection, }) {
|
|
336
|
+
// `endpoint` is optional on ISolanaConnectionOptions, so a caller with no
|
|
337
|
+
// Solana RPC configured reaches `fetch(undefined, ...)`, which throws
|
|
338
|
+
// "Cannot read properties of undefined (reading 'toString')" from inside the
|
|
339
|
+
// runtime's fetch wrapper — an unactionable error masquerading as a token
|
|
340
|
+
// failure. Bail before the request instead.
|
|
341
|
+
if (!endpoint) {
|
|
342
|
+
core_1.Logger.log.warn('getToken', 'No Solana RPC endpoint configured', {
|
|
343
|
+
mintAddress: String(mintAddress),
|
|
344
|
+
});
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
336
347
|
try {
|
|
337
348
|
const cacheKey = `solana_token-${mintAddress}`;
|
|
338
349
|
const cachedData = await (0, core_1.getSafeCache)(cacheKey);
|
|
@@ -425,8 +436,11 @@ async function getToken({ mintAddress, endpoint, connection, }) {
|
|
|
425
436
|
return null;
|
|
426
437
|
}
|
|
427
438
|
catch (error) {
|
|
428
|
-
|
|
429
|
-
|
|
439
|
+
// Callers fall back to backend metadata when this returns null, so a failed
|
|
440
|
+
// lookup is degraded-but-handled — warn, don't raise it as an error.
|
|
441
|
+
core_1.Logger.log.warn('getToken', 'Error fetching token metadata', {
|
|
442
|
+
mintAddress: String(mintAddress),
|
|
443
|
+
error,
|
|
430
444
|
});
|
|
431
445
|
return null;
|
|
432
446
|
}
|
|
@@ -143,13 +143,34 @@ const ERROR_DEDUPE_MAX_KEYS = 500;
|
|
|
143
143
|
* true rate. `resetAnalytics()` clears this map for the whole process.
|
|
144
144
|
*/
|
|
145
145
|
const errorDedupeState = new Map();
|
|
146
|
+
/**
|
|
147
|
+
* Strip the per-request variables out of an error message.
|
|
148
|
+
*
|
|
149
|
+
* Truncating at 120 characters is not enough on its own: the variable part is
|
|
150
|
+
* usually a vault or wallet address near the FRONT of the message
|
|
151
|
+
* (`Request timeout after 90s: /subaccount/0x3F13…/otc_positions`), so one
|
|
152
|
+
* failure mode still produced a distinct signature — and therefore its own
|
|
153
|
+
* private rate-limit allowance and its own Sentry issue — per address. That is
|
|
154
|
+
* how a single backend timeout became 5k events across a dozen issues in a day.
|
|
155
|
+
*
|
|
156
|
+
* Placeholders are kept structural (`/subaccount/<addr>/otc_positions`) so
|
|
157
|
+
* genuinely different endpoints and failure modes stay separate.
|
|
158
|
+
*
|
|
159
|
+
* @param message - The raw error message.
|
|
160
|
+
* @returns The message with addresses, numbers, and query strings masked.
|
|
161
|
+
*/
|
|
162
|
+
function normalizeErrorMessage(message) {
|
|
163
|
+
return message
|
|
164
|
+
.replace(/0x[0-9a-fA-F]{6,}/g, '<addr>')
|
|
165
|
+
.replace(/\?[^\s]*/g, '')
|
|
166
|
+
.replace(/\b\d{4,}\b/g, '<n>');
|
|
167
|
+
}
|
|
146
168
|
/**
|
|
147
169
|
* Build a low-cardinality signature for an error.
|
|
148
170
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* unique keys.
|
|
171
|
+
* The message is normalized (see {@link normalizeErrorMessage}) and then capped
|
|
172
|
+
* at 120 characters — enough to separate distinct failure modes without
|
|
173
|
+
* splitting one failure mode into thousands of unique keys.
|
|
153
174
|
*
|
|
154
175
|
* @param error - The normalized error being captured.
|
|
155
176
|
* @param origin - The `sdk.origin` call-site tag, when present.
|
|
@@ -157,7 +178,8 @@ const errorDedupeState = new Map();
|
|
|
157
178
|
*/
|
|
158
179
|
function errorSignature(error, origin) {
|
|
159
180
|
const tag = typeof origin === 'string' ? origin : '';
|
|
160
|
-
|
|
181
|
+
const message = normalizeErrorMessage(error.message || '').slice(0, 120);
|
|
182
|
+
return `${tag}|${error.name}|${message}`;
|
|
161
183
|
}
|
|
162
184
|
/**
|
|
163
185
|
* Decide whether an error should be sent to Sentry, applying a per-signature
|
|
@@ -265,6 +287,17 @@ function createSentrySink() {
|
|
|
265
287
|
if (admission.suppressed > 0) {
|
|
266
288
|
scope.setExtra('sdk.suppressed_since_last', admission.suppressed);
|
|
267
289
|
}
|
|
290
|
+
// Group by the same normalized signature the rate limiter uses.
|
|
291
|
+
// Sentry's default grouping keys on the raw message, so one backend
|
|
292
|
+
// timeout fans out into an issue per vault/wallet address — the
|
|
293
|
+
// per-issue notification volume that flooded the on-call webhook.
|
|
294
|
+
// Addresses stay recoverable from the event message and tags.
|
|
295
|
+
scope.setFingerprint?.([
|
|
296
|
+
'sdk',
|
|
297
|
+
typeof context?.tag === 'string' ? context.tag : 'untagged',
|
|
298
|
+
normalized.name,
|
|
299
|
+
normalizeErrorMessage(normalized.message || '').slice(0, 120),
|
|
300
|
+
]);
|
|
268
301
|
sdk.captureException(normalized);
|
|
269
302
|
});
|
|
270
303
|
}
|
|
@@ -20,8 +20,12 @@ import type { IVaultBaseOptions } from './types';
|
|
|
20
20
|
* Routes to appropriate chain adapter (EVM v1/v2, Solana, or Stellar) based on vault version.
|
|
21
21
|
* Optionally enriches with loan and allocation data.
|
|
22
22
|
* @param vault - Vault contract address (EVM hex, Stellar C-address, or Solana program ID)
|
|
23
|
-
* @param loans - Include active loan data
|
|
24
|
-
*
|
|
23
|
+
* @param loans - Include active loan data. EVM vaults only — there is no
|
|
24
|
+
* non-EVM loan book to read, so other families keep the empty default.
|
|
25
|
+
* @param allocations - Include DeFi/CeFi allocation breakdowns. Resolved for EVM
|
|
26
|
+
* vaults (via DeBank) and Stellar vaults (via the Untangled portfolio API).
|
|
27
|
+
* Solana and Sui have no vault-level portfolio provider and keep the empty
|
|
28
|
+
* default.
|
|
25
29
|
* @param options - RPC and service configuration
|
|
26
30
|
* @param tokenizedVault - Optional pre-fetched backend row for `vault`. When a
|
|
27
31
|
* caller already holds the row (e.g. `getVaults` fetched the whole list one
|
|
@@ -110,8 +110,12 @@ const errors_1 = require("../../core/errors");
|
|
|
110
110
|
* Routes to appropriate chain adapter (EVM v1/v2, Solana, or Stellar) based on vault version.
|
|
111
111
|
* Optionally enriches with loan and allocation data.
|
|
112
112
|
* @param vault - Vault contract address (EVM hex, Stellar C-address, or Solana program ID)
|
|
113
|
-
* @param loans - Include active loan data
|
|
114
|
-
*
|
|
113
|
+
* @param loans - Include active loan data. EVM vaults only — there is no
|
|
114
|
+
* non-EVM loan book to read, so other families keep the empty default.
|
|
115
|
+
* @param allocations - Include DeFi/CeFi allocation breakdowns. Resolved for EVM
|
|
116
|
+
* vaults (via DeBank) and Stellar vaults (via the Untangled portfolio API).
|
|
117
|
+
* Solana and Sui have no vault-level portfolio provider and keep the empty
|
|
118
|
+
* default.
|
|
115
119
|
* @param options - RPC and service configuration
|
|
116
120
|
* @param tokenizedVault - Optional pre-fetched backend row for `vault`. When a
|
|
117
121
|
* caller already holds the row (e.g. `getVaults` fetched the whole list one
|
|
@@ -153,24 +157,33 @@ async function getVault({ vault, loans = false, allocations = false, options, lo
|
|
|
153
157
|
core_1.Logger.log.error('getVault', err, { vault });
|
|
154
158
|
throw new Error(`#getVault::${vault}: ${err?.message}`);
|
|
155
159
|
}
|
|
156
|
-
// Loans
|
|
157
|
-
//
|
|
160
|
+
// Loans enrichment stays EVM-only — there is no non-EVM loan book to read.
|
|
161
|
+
// Allocations additionally covers Stellar, whose exposure resolves through the
|
|
162
|
+
// Untangled portfolio API rather than DeBank; gating it on `isEvmVault` left
|
|
163
|
+
// `getVaultAllocations` unreachable for the one chain family that needed the
|
|
164
|
+
// new path, so callers got the empty default while the standalone getter
|
|
165
|
+
// returned real data. Solana and Sui have no vault-level provider, so they
|
|
166
|
+
// keep the empty defaults.
|
|
158
167
|
const isEvmVault = returnedVault.version !== 'sol-0' &&
|
|
159
168
|
returnedVault.version !== 'stellar-0' &&
|
|
160
169
|
returnedVault.version !== 'sui-0';
|
|
161
|
-
|
|
162
|
-
|
|
170
|
+
const supportsLoans = isEvmVault;
|
|
171
|
+
const supportsAllocations = isEvmVault || returnedVault.version === 'stellar-0';
|
|
172
|
+
const readLoans = loans && supportsLoans;
|
|
173
|
+
const readAllocations = allocations && supportsAllocations;
|
|
174
|
+
if ((loans && !supportsLoans) || (allocations && !supportsAllocations)) {
|
|
175
|
+
core_1.Logger.log.warn('getVault', 'Loans/allocations enrichment is not supported for this vault — skipping', { vault, version: returnedVault.version });
|
|
163
176
|
}
|
|
164
|
-
if (
|
|
177
|
+
if (!(0, core_1.isBadVault)(vault) && (readLoans || readAllocations)) {
|
|
165
178
|
const [loansResult, allocationsResult] = await Promise.allSettled([
|
|
166
|
-
|
|
179
|
+
readLoans
|
|
167
180
|
? getVaultLoans(returnedVault, options)
|
|
168
181
|
: Promise.resolve(undefined),
|
|
169
|
-
|
|
182
|
+
readAllocations
|
|
170
183
|
? getVaultAllocations(vault, options)
|
|
171
184
|
: Promise.resolve(undefined),
|
|
172
185
|
]);
|
|
173
|
-
if (
|
|
186
|
+
if (readLoans) {
|
|
174
187
|
if (loansResult.status === 'fulfilled') {
|
|
175
188
|
returnedVault = {
|
|
176
189
|
...returnedVault,
|
|
@@ -183,7 +196,7 @@ async function getVault({ vault, loans = false, allocations = false, options, lo
|
|
|
183
196
|
});
|
|
184
197
|
}
|
|
185
198
|
}
|
|
186
|
-
if (
|
|
199
|
+
if (readAllocations) {
|
|
187
200
|
if (allocationsResult.status === 'fulfilled') {
|
|
188
201
|
returnedVault = {
|
|
189
202
|
...returnedVault,
|
package/lib/sdk.d.ts
CHANGED
|
@@ -19372,8 +19372,12 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
|
|
|
19372
19372
|
* Routes to appropriate chain adapter (EVM v1/v2, Solana, or Stellar) based on vault version.
|
|
19373
19373
|
* Optionally enriches with loan and allocation data.
|
|
19374
19374
|
* @param vault - Vault contract address (EVM hex, Stellar C-address, or Solana program ID)
|
|
19375
|
-
* @param loans - Include active loan data
|
|
19376
|
-
*
|
|
19375
|
+
* @param loans - Include active loan data. EVM vaults only — there is no
|
|
19376
|
+
* non-EVM loan book to read, so other families keep the empty default.
|
|
19377
|
+
* @param allocations - Include DeFi/CeFi allocation breakdowns. Resolved for EVM
|
|
19378
|
+
* vaults (via DeBank) and Stellar vaults (via the Untangled portfolio API).
|
|
19379
|
+
* Solana and Sui have no vault-level portfolio provider and keep the empty
|
|
19380
|
+
* default.
|
|
19377
19381
|
* @param options - RPC and service configuration
|
|
19378
19382
|
* @param tokenizedVault - Optional pre-fetched backend row for `vault`. When a
|
|
19379
19383
|
* caller already holds the row (e.g. `getVaults` fetched the whole list one
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
export interface IUntangledProtocolExposure {
|
|
16
16
|
/**
|
|
17
17
|
* Untangled protocol slug, e.g. `blend`, `templar`, `aquarius`. Two slugs are
|
|
18
|
-
*
|
|
19
|
-
* own idle buffer
|
|
18
|
+
* special: `stellar-wallet` is custody wallet cash, and `upshift` is the
|
|
19
|
+
* vault's own idle buffer plus any position it holds in another Upshift
|
|
20
|
+
* vault — only the part beyond the `idle` location is deployed capital.
|
|
20
21
|
*/
|
|
21
22
|
protocolId: string;
|
|
22
23
|
/** USD value held in this protocol. */
|
|
@@ -30,13 +30,20 @@ export declare const UNTANGLED_PROTOCOL_REGISTRY: Record<string, IUntangledProto
|
|
|
30
30
|
* - Each `byProtocol` entry becomes one position with a single supplying token,
|
|
31
31
|
* valued in USD (`price: 1`), on the chain from
|
|
32
32
|
* {@link UNTANGLED_PROTOCOL_REGISTRY}.
|
|
33
|
-
* - `stellar-wallet`
|
|
34
|
-
*
|
|
33
|
+
* - `stellar-wallet` becomes a wallet token, because custody cash is undeployed
|
|
34
|
+
* capital rather than protocol exposure.
|
|
35
|
+
* - `upshift` is split: the vault's own idle buffer (the `idle` location) joins
|
|
36
|
+
* that wallet token, and whatever is left is a real position in another
|
|
37
|
+
* Upshift vault, named after its receipt token. Untangled reports one
|
|
38
|
+
* `upshift` slice however many vaults that is, so a vault holding receipts in
|
|
39
|
+
* two others emits one merged position under the generic name — the value is
|
|
40
|
+
* still right, only the label is coarser.
|
|
35
41
|
* - Dust is left in place: `filterOutBySize` in the parser applies the same
|
|
36
42
|
* threshold Stellar and EVM vaults share.
|
|
37
43
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
44
|
+
* Only the `idle` entries of `locations` are consumed, to size the buffer inside
|
|
45
|
+
* `upshift`. `asOf`, `stale` and `tvlUsd` are not — TVL already comes from
|
|
46
|
+
* `getVaultTvl`, and freshness is not yet surfaced on `IVaultAllocations`.
|
|
40
47
|
*
|
|
41
48
|
* @param exposure - A resolved Untangled report, or `null`/`undefined` when the
|
|
42
49
|
* fetch failed.
|
|
@@ -14,14 +14,87 @@ exports.UNTANGLED_PROTOCOL_REGISTRY = {
|
|
|
14
14
|
blend: { name: 'Blend', chain: 'stellar' },
|
|
15
15
|
templar: { name: 'Templar (NEAR)', chain: 'near' },
|
|
16
16
|
aquarius: { name: 'Aquarius', chain: 'stellar' },
|
|
17
|
+
upshift: { name: 'Upshift', chain: 'stellar' },
|
|
17
18
|
};
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* treatment EVM vaults get for funds sitting outside a protocol.
|
|
20
|
+
* Custody wallet cash — undeployed capital, which the exposure breakdown renders
|
|
21
|
+
* as wallet holdings, the same treatment EVM vaults get for funds sitting
|
|
22
|
+
* outside a protocol.
|
|
23
23
|
*/
|
|
24
|
-
const
|
|
24
|
+
const CUSTODY_WALLET_PROTOCOL_ID = 'stellar-wallet';
|
|
25
|
+
/**
|
|
26
|
+
* Capital held inside Upshift itself. This slug is NOT purely idle: it is the
|
|
27
|
+
* vault's own idle buffer PLUS any receipt tokens the vault holds in another
|
|
28
|
+
* Upshift vault. Gami earnXLM on 2026-09-11 reported `upshift: $1,371,484` made
|
|
29
|
+
* up of a `$200,572` idle buffer and `$1,170,913` of earnUSDC shares — counting
|
|
30
|
+
* all of it as idle hid a third of the vault's deployed capital and overstated
|
|
31
|
+
* the idle bucket almost sixfold.
|
|
32
|
+
*/
|
|
33
|
+
const UPSHIFT_PROTOCOL_ID = 'upshift';
|
|
34
|
+
/** `locations[].label` for the capital still sitting in the vault contract. */
|
|
35
|
+
const IDLE_LOCATION_LABEL = 'idle';
|
|
36
|
+
/** Tightness of the `byAsset` lookup in {@link matchingAssetSymbol}. */
|
|
37
|
+
const ASSET_MATCH_TOLERANCE = 0.001;
|
|
38
|
+
/**
|
|
39
|
+
* Smallest `upshift` remainder treated as a real position rather than as float
|
|
40
|
+
* disagreement between the `idle` location and the `byProtocol` slice. One cent
|
|
41
|
+
* sits orders of magnitude above double-precision noise on million-dollar
|
|
42
|
+
* values and orders of magnitude below any holding worth naming.
|
|
43
|
+
*/
|
|
44
|
+
const MIN_POSITION_USD = 0.01;
|
|
45
|
+
/**
|
|
46
|
+
* USD the vault holds in its own contract, summed from the `idle` locations.
|
|
47
|
+
*
|
|
48
|
+
* Returns `null` when no location carries that label, so a caller can tell
|
|
49
|
+
* "nothing is idle" from "this report does not say".
|
|
50
|
+
*
|
|
51
|
+
* The whole sum is charged against the `upshift` slice, which assumes idle
|
|
52
|
+
* capital never belongs to another protocol. That holds for the shape Untangled
|
|
53
|
+
* serves — the buffer sitting in the vault contract is exactly what it files
|
|
54
|
+
* under `upshift` — and the `Math.min` at the call site caps the subtraction, so
|
|
55
|
+
* a report that broke the assumption would understate the position rather than
|
|
56
|
+
* invent one.
|
|
57
|
+
*/
|
|
58
|
+
function idleLocationUsd(exposure) {
|
|
59
|
+
let total = null;
|
|
60
|
+
(exposure.locations || []).forEach((location) => {
|
|
61
|
+
if (location?.label?.toLowerCase() !== IDLE_LOCATION_LABEL)
|
|
62
|
+
return;
|
|
63
|
+
total = (total ?? 0) + (Number(location.valueUsd) || 0);
|
|
64
|
+
});
|
|
65
|
+
return total;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The `byAsset` symbol whose value matches this position's, within
|
|
69
|
+
* {@link ASSET_MATCH_TOLERANCE}.
|
|
70
|
+
*
|
|
71
|
+
* Untangled splits assets vault-wide, so a position's asset can normally only be
|
|
72
|
+
* guessed. A vault's holding in another Upshift vault is the exception: the
|
|
73
|
+
* receipt token is its own `byAsset` entry carrying exactly that position's
|
|
74
|
+
* value, so it can be named instead of falling back to the vault's dominant
|
|
75
|
+
* asset.
|
|
76
|
+
*
|
|
77
|
+
* This only ever chooses a *label* — the USD value is taken from `byProtocol`
|
|
78
|
+
* either way — so it is built to abstain rather than guess. Two cases reach it
|
|
79
|
+
* legitimately and get no answer: a vault holding receipts in two other Upshift
|
|
80
|
+
* vaults, which Untangled reports as one `upshift` slice whose remainder matches
|
|
81
|
+
* neither entry on its own, and a remainder small enough that the relative
|
|
82
|
+
* window closes around rounding noise.
|
|
83
|
+
*
|
|
84
|
+
* @param valueUsd - The position's USD value. The caller has already cleared it
|
|
85
|
+
* past {@link MIN_POSITION_USD}, so the relative window is never taken around
|
|
86
|
+
* zero.
|
|
87
|
+
* @returns The matching symbol, or `null` when no entry matches or more than one
|
|
88
|
+
* does — the caller names the position generically instead.
|
|
89
|
+
*/
|
|
90
|
+
function matchingAssetSymbol(exposure, valueUsd) {
|
|
91
|
+
const matches = (exposure.byAsset || []).filter((asset) => {
|
|
92
|
+
const assetUsd = Number(asset?.valueUsd) || 0;
|
|
93
|
+
return (assetUsd > 0 &&
|
|
94
|
+
Math.abs(assetUsd - valueUsd) <= valueUsd * ASSET_MATCH_TOLERANCE);
|
|
95
|
+
});
|
|
96
|
+
return matches.length === 1 ? matches[0].symbol || null : null;
|
|
97
|
+
}
|
|
25
98
|
const DEFAULT_CHAIN = 'stellar';
|
|
26
99
|
/**
|
|
27
100
|
* Stellar's asset precision, carried only to satisfy the DeBank token shape.
|
|
@@ -78,13 +151,20 @@ function dominantAssetSymbol(exposure) {
|
|
|
78
151
|
* - Each `byProtocol` entry becomes one position with a single supplying token,
|
|
79
152
|
* valued in USD (`price: 1`), on the chain from
|
|
80
153
|
* {@link UNTANGLED_PROTOCOL_REGISTRY}.
|
|
81
|
-
* - `stellar-wallet`
|
|
82
|
-
*
|
|
154
|
+
* - `stellar-wallet` becomes a wallet token, because custody cash is undeployed
|
|
155
|
+
* capital rather than protocol exposure.
|
|
156
|
+
* - `upshift` is split: the vault's own idle buffer (the `idle` location) joins
|
|
157
|
+
* that wallet token, and whatever is left is a real position in another
|
|
158
|
+
* Upshift vault, named after its receipt token. Untangled reports one
|
|
159
|
+
* `upshift` slice however many vaults that is, so a vault holding receipts in
|
|
160
|
+
* two others emits one merged position under the generic name — the value is
|
|
161
|
+
* still right, only the label is coarser.
|
|
83
162
|
* - Dust is left in place: `filterOutBySize` in the parser applies the same
|
|
84
163
|
* threshold Stellar and EVM vaults share.
|
|
85
164
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
165
|
+
* Only the `idle` entries of `locations` are consumed, to size the buffer inside
|
|
166
|
+
* `upshift`. `asOf`, `stale` and `tvlUsd` are not — TVL already comes from
|
|
167
|
+
* `getVaultTvl`, and freshness is not yet surfaced on `IVaultAllocations`.
|
|
88
168
|
*
|
|
89
169
|
* @param exposure - A resolved Untangled report, or `null`/`undefined` when the
|
|
90
170
|
* fetch failed.
|
|
@@ -106,20 +186,48 @@ function transformUntangledToDebank(exposure) {
|
|
|
106
186
|
}
|
|
107
187
|
const symbol = dominantAssetSymbol(exposure);
|
|
108
188
|
const vaultChain = exposure.vault?.chain || DEFAULT_CHAIN;
|
|
189
|
+
const idleUsd = idleLocationUsd(exposure);
|
|
109
190
|
const positions = [];
|
|
110
191
|
let walletValueUsd = 0;
|
|
111
192
|
(exposure.byProtocol || []).forEach((entry) => {
|
|
112
193
|
if (!entry?.protocolId)
|
|
113
194
|
return;
|
|
114
195
|
const valueUsd = Number(entry.valueUsd) || 0;
|
|
115
|
-
if (
|
|
196
|
+
if (entry.protocolId === CUSTODY_WALLET_PROTOCOL_ID) {
|
|
116
197
|
walletValueUsd += valueUsd;
|
|
117
198
|
return;
|
|
118
199
|
}
|
|
200
|
+
let positionUsd = valueUsd;
|
|
201
|
+
let positionSymbol = symbol;
|
|
202
|
+
let receiptSymbol = null;
|
|
203
|
+
if (entry.protocolId === UPSHIFT_PROTOCOL_ID) {
|
|
204
|
+
// A report with no idle location cannot say how much of this is the
|
|
205
|
+
// buffer, so it claims the whole slice. That leaves a remainder of exactly
|
|
206
|
+
// zero, which the guard below turns back into the pre-split behaviour —
|
|
207
|
+
// the two are coupled, so don't change one without the other.
|
|
208
|
+
const idleShare = idleUsd === null ? valueUsd : Math.min(valueUsd, idleUsd);
|
|
209
|
+
positionUsd = valueUsd - idleShare;
|
|
210
|
+
// The buffer reaches us twice, from two different fields, so a vault that
|
|
211
|
+
// holds nothing in another Upshift vault can still leave a sub-cent
|
|
212
|
+
// remainder behind. That is disagreement between the fields, not a
|
|
213
|
+
// position: keep the slice whole in the wallet bucket rather than emit a
|
|
214
|
+
// phantom the parser's size filter would have to clean up. Genuine dust is
|
|
215
|
+
// still passed through for that filter to judge.
|
|
216
|
+
if (positionUsd < MIN_POSITION_USD) {
|
|
217
|
+
walletValueUsd += valueUsd;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
walletValueUsd += idleShare;
|
|
221
|
+
receiptSymbol = matchingAssetSymbol(exposure, positionUsd);
|
|
222
|
+
positionSymbol = receiptSymbol ?? symbol;
|
|
223
|
+
}
|
|
119
224
|
const metadata = exports.UNTANGLED_PROTOCOL_REGISTRY[entry.protocolId];
|
|
120
225
|
const chain = metadata?.chain || vaultChain;
|
|
121
|
-
const
|
|
226
|
+
const baseName = metadata?.name ||
|
|
122
227
|
entry.protocolId.charAt(0).toUpperCase() + entry.protocolId.slice(1);
|
|
228
|
+
// "Upshift" alone reads as the whole product inside Upshift's own UI, so the
|
|
229
|
+
// slice names the vault it sits in whenever the receipt token identifies it.
|
|
230
|
+
const name = receiptSymbol ? `${baseName} (${receiptSymbol})` : baseName;
|
|
123
231
|
positions.push({
|
|
124
232
|
id: entry.protocolId,
|
|
125
233
|
chain,
|
|
@@ -130,13 +238,13 @@ function transformUntangledToDebank(exposure) {
|
|
|
130
238
|
{
|
|
131
239
|
name,
|
|
132
240
|
stats: {
|
|
133
|
-
asset_usd_value:
|
|
241
|
+
asset_usd_value: positionUsd,
|
|
134
242
|
debt_usd_value: 0,
|
|
135
|
-
net_usd_value:
|
|
243
|
+
net_usd_value: positionUsd,
|
|
136
244
|
},
|
|
137
245
|
detail: {
|
|
138
246
|
supply_token_list: [
|
|
139
|
-
toDebankToken(
|
|
247
|
+
toDebankToken(positionSymbol, chain, positionUsd, 'supply', entry.protocolId),
|
|
140
248
|
],
|
|
141
249
|
borrow_token_list: [],
|
|
142
250
|
},
|