@augustdigital/sdk 9.1.0 → 9.2.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/core/analytics/version.d.ts +1 -1
- package/lib/core/analytics/version.js +1 -1
- package/lib/main.d.ts +6 -0
- package/lib/main.js +6 -0
- package/lib/modules/vaults/getters.d.ts +9 -0
- package/lib/modules/vaults/getters.js +25 -0
- package/lib/sdk.d.ts +15 -0
- package/lib/services/untangled/fetcher.d.ts +40 -0
- package/lib/services/untangled/fetcher.js +110 -0
- package/lib/services/untangled/index.d.ts +3 -0
- package/lib/services/untangled/index.js +20 -0
- package/lib/services/untangled/types.d.ts +82 -0
- package/lib/services/untangled/types.js +10 -0
- package/lib/services/untangled/utils.d.ts +55 -0
- package/lib/services/untangled/utils.js +157 -0
- package/package.json +1 -1
package/lib/main.d.ts
CHANGED
|
@@ -126,6 +126,12 @@ export declare class AugustSDK extends AugustBase {
|
|
|
126
126
|
* `cefi` / `otc` arrays back the drill-downs. See the "Rendering the Vault
|
|
127
127
|
* Exposure section" guide in the vaults docs for a faithful reproduction.
|
|
128
128
|
*
|
|
129
|
+
* Stellar vaults resolve their DeFi exposure through the Untangled portfolio
|
|
130
|
+
* API instead of DeBank — one extra HTTP call, covering the idle buffer,
|
|
131
|
+
* Stellar protocol positions, custody wallets and bridged capital. If that
|
|
132
|
+
* call fails the exposure legs come back empty rather than throwing, so CeFi,
|
|
133
|
+
* OTC and loan allocations still resolve.
|
|
134
|
+
*
|
|
129
135
|
* @param props - Vault address and chain ID
|
|
130
136
|
* @returns Detailed breakdown of vault allocations by category
|
|
131
137
|
* @example
|
package/lib/main.js
CHANGED
|
@@ -254,6 +254,12 @@ class AugustSDK extends core_1.AugustBase {
|
|
|
254
254
|
* `cefi` / `otc` arrays back the drill-downs. See the "Rendering the Vault
|
|
255
255
|
* Exposure section" guide in the vaults docs for a faithful reproduction.
|
|
256
256
|
*
|
|
257
|
+
* Stellar vaults resolve their DeFi exposure through the Untangled portfolio
|
|
258
|
+
* API instead of DeBank — one extra HTTP call, covering the idle buffer,
|
|
259
|
+
* Stellar protocol positions, custody wallets and bridged capital. If that
|
|
260
|
+
* call fails the exposure legs come back empty rather than throwing, so CeFi,
|
|
261
|
+
* OTC and loan allocations still resolve.
|
|
262
|
+
*
|
|
257
263
|
* @param props - Vault address and chain ID
|
|
258
264
|
* @returns Detailed breakdown of vault allocations by category
|
|
259
265
|
* @example
|
|
@@ -55,6 +55,15 @@ export declare function getVaultSubaccountLoans(vault: VaultAddress | IVault, op
|
|
|
55
55
|
* Fetch comprehensive vault asset allocation breakdown.
|
|
56
56
|
* Includes DeFi protocols (via DeBank), CeFi balances, OTC positions, and loans.
|
|
57
57
|
* Categorizes exposures by borrowing, supplying, lending, and wallet holdings.
|
|
58
|
+
*
|
|
59
|
+
* DeFi exposure resolves per chain family: EVM vaults via DeBank, Solana
|
|
60
|
+
* borrowers via Octav.fi, and Stellar vaults via the Untangled portfolio API —
|
|
61
|
+
* one vault-level call covering the idle buffer, Stellar protocol positions,
|
|
62
|
+
* custody wallets, and capital bridged to other chains. The Stellar path adds
|
|
63
|
+
* exactly 1 HTTP call and zero for every other chain; when it fails, the
|
|
64
|
+
* exposure section comes back empty and the CeFi, OTC and loan allocations are
|
|
65
|
+
* still returned.
|
|
66
|
+
*
|
|
58
67
|
* @param vault - Vault address
|
|
59
68
|
* @param options - RPC configuration
|
|
60
69
|
* @returns Detailed allocation data with exposure categorization
|
|
@@ -84,6 +84,7 @@ const TokenizedVaultV2_1 = require("../../abis/TokenizedVaultV2");
|
|
|
84
84
|
const TokenizedVaultV2Receipt_1 = require("../../abis/TokenizedVaultV2Receipt");
|
|
85
85
|
const ethereum_block_by_date_1 = __importDefault(require("ethereum-block-by-date"));
|
|
86
86
|
const octavfi_1 = require("../../services/octavfi");
|
|
87
|
+
const untangled_1 = require("../../services/untangled");
|
|
87
88
|
const date_utils_1 = require("./utils/date-utils");
|
|
88
89
|
const call_data_decoder_1 = require("./utils/call-data-decoder");
|
|
89
90
|
const deposits_1 = require("../../services/layerzero/deposits");
|
|
@@ -433,6 +434,15 @@ function httpStatusFromError(error) {
|
|
|
433
434
|
* Fetch comprehensive vault asset allocation breakdown.
|
|
434
435
|
* Includes DeFi protocols (via DeBank), CeFi balances, OTC positions, and loans.
|
|
435
436
|
* Categorizes exposures by borrowing, supplying, lending, and wallet holdings.
|
|
437
|
+
*
|
|
438
|
+
* DeFi exposure resolves per chain family: EVM vaults via DeBank, Solana
|
|
439
|
+
* borrowers via Octav.fi, and Stellar vaults via the Untangled portfolio API —
|
|
440
|
+
* one vault-level call covering the idle buffer, Stellar protocol positions,
|
|
441
|
+
* custody wallets, and capital bridged to other chains. The Stellar path adds
|
|
442
|
+
* exactly 1 HTTP call and zero for every other chain; when it fails, the
|
|
443
|
+
* exposure section comes back empty and the CeFi, OTC and loan allocations are
|
|
444
|
+
* still returned.
|
|
445
|
+
*
|
|
436
446
|
* @param vault - Vault address
|
|
437
447
|
* @param options - RPC configuration
|
|
438
448
|
* @returns Detailed allocation data with exposure categorization
|
|
@@ -532,6 +542,21 @@ async function getVaultAllocations(vault, options) {
|
|
|
532
542
|
}
|
|
533
543
|
}
|
|
534
544
|
}
|
|
545
|
+
else if ((0, core_1.getAddressChainType)(vault) === 'stellar') {
|
|
546
|
+
// Untangled reconciles the whole Stellar vault in one call — idle buffer,
|
|
547
|
+
// Stellar protocol positions, custody wallets, and value bridged to other
|
|
548
|
+
// chains. It is keyed by vault, not by borrower, so it runs here rather
|
|
549
|
+
// than in the per-borrower loop below, which would multiply the totals.
|
|
550
|
+
const untangledExposure = await (0, untangled_1.fetchUntangledVaultExposure)(vault);
|
|
551
|
+
// A miss leaves the exposure section empty — never `debankErr`. See the
|
|
552
|
+
// `no_fetcher` branch below for why throwing here was wrong.
|
|
553
|
+
if (untangledExposure) {
|
|
554
|
+
const untangledRes = (0, untangled_1.transformUntangledToDebank)(untangledExposure);
|
|
555
|
+
(0, debank_1.parseVaultLevelDebank)(untangledRes, protocolExposure, tokenExposure, vault, exposurePerCategory, netValue);
|
|
556
|
+
unfilteredTokens = untangledRes.subaccount.tokens;
|
|
557
|
+
defiPerBorrower[vault] = (0, debank_1.parseLoanLevelDebank)(untangledRes);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
535
560
|
// Fetch portfolio data for non-EVM borrowers (EVM handled by fetchVaultDebankResponse above).
|
|
536
561
|
// CeFi and OTC fetches below run for ALL borrowers regardless of chain type.
|
|
537
562
|
for (const borrower of uniqueBorrowers) {
|
package/lib/sdk.d.ts
CHANGED
|
@@ -16071,6 +16071,12 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
|
|
|
16071
16071
|
* `cefi` / `otc` arrays back the drill-downs. See the "Rendering the Vault
|
|
16072
16072
|
* Exposure section" guide in the vaults docs for a faithful reproduction.
|
|
16073
16073
|
*
|
|
16074
|
+
* Stellar vaults resolve their DeFi exposure through the Untangled portfolio
|
|
16075
|
+
* API instead of DeBank — one extra HTTP call, covering the idle buffer,
|
|
16076
|
+
* Stellar protocol positions, custody wallets and bridged capital. If that
|
|
16077
|
+
* call fails the exposure legs come back empty rather than throwing, so CeFi,
|
|
16078
|
+
* OTC and loan allocations still resolve.
|
|
16079
|
+
*
|
|
16074
16080
|
* @param props - Vault address and chain ID
|
|
16075
16081
|
* @returns Detailed breakdown of vault allocations by category
|
|
16076
16082
|
* @example
|
|
@@ -19399,6 +19405,15 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
|
|
|
19399
19405
|
* Fetch comprehensive vault asset allocation breakdown.
|
|
19400
19406
|
* Includes DeFi protocols (via DeBank), CeFi balances, OTC positions, and loans.
|
|
19401
19407
|
* Categorizes exposures by borrowing, supplying, lending, and wallet holdings.
|
|
19408
|
+
*
|
|
19409
|
+
* DeFi exposure resolves per chain family: EVM vaults via DeBank, Solana
|
|
19410
|
+
* borrowers via Octav.fi, and Stellar vaults via the Untangled portfolio API —
|
|
19411
|
+
* one vault-level call covering the idle buffer, Stellar protocol positions,
|
|
19412
|
+
* custody wallets, and capital bridged to other chains. The Stellar path adds
|
|
19413
|
+
* exactly 1 HTTP call and zero for every other chain; when it fails, the
|
|
19414
|
+
* exposure section comes back empty and the CeFi, OTC and loan allocations are
|
|
19415
|
+
* still returned.
|
|
19416
|
+
*
|
|
19402
19417
|
* @param vault - Vault address
|
|
19403
19418
|
* @param options - RPC configuration
|
|
19404
19419
|
* @returns Detailed allocation data with exposure categorization
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { IUntangledVaultExposure } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Fetch a vault's full capital exposure from the Untangled portfolio API.
|
|
4
|
+
*
|
|
5
|
+
* Untangled reconciles Stellar vaults end to end — the vault's idle buffer,
|
|
6
|
+
* capital deployed into Stellar protocols, custody wallet balances, and value
|
|
7
|
+
* bridged to other chains. DeBank and Octav.fi cover none of that, which is why
|
|
8
|
+
* Stellar vaults previously rendered an empty exposure breakdown.
|
|
9
|
+
*
|
|
10
|
+
* The endpoint is keyed by **vault** address and already aggregates every
|
|
11
|
+
* borrower, so it must be called once per vault — never once per borrower, which
|
|
12
|
+
* would multiply the same totals.
|
|
13
|
+
*
|
|
14
|
+
* Requests are plain `GET`s with no custom headers. That is deliberate: it keeps
|
|
15
|
+
* them preflight-free in the browser, where the API's `access-control-allow-origin: *`
|
|
16
|
+
* covers the simple request but a bare `OPTIONS` is rejected.
|
|
17
|
+
*
|
|
18
|
+
* Never throws. Every failure — unknown vault, upstream error, transport
|
|
19
|
+
* failure, malformed body — resolves to `null` so callers can degrade to an
|
|
20
|
+
* empty exposure section instead of losing the rest of their response.
|
|
21
|
+
*
|
|
22
|
+
* Worst case: exactly **1 HTTP call**. No retry, no caching (the API is edge
|
|
23
|
+
* cached at `s-maxage=60`, well inside its 60 req/min budget for one call per
|
|
24
|
+
* vault page load).
|
|
25
|
+
*
|
|
26
|
+
* @param vault - Vault address, e.g. a Stellar contract address (`C…`).
|
|
27
|
+
* @returns The resolved exposure report, or `null` when Untangled does not
|
|
28
|
+
* reconcile this vault or the request failed.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const exposure = await fetchUntangledVaultExposure(
|
|
33
|
+
* 'CCL3WITWFFXIHV2I52ECV5DPIEOFSTU3PBPR53ILPLF2IP5KHECXRUTY',
|
|
34
|
+
* );
|
|
35
|
+
* // exposure?.tvlUsd → 24845637.05
|
|
36
|
+
* // exposure?.byProtocol → [{ protocolId: 'stellar-wallet', … }, { protocolId: 'blend', … }, …]
|
|
37
|
+
* const normalized = transformUntangledToDebank(exposure);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function fetchUntangledVaultExposure(vault: string): Promise<IUntangledVaultExposure | null>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchUntangledVaultExposure = fetchUntangledVaultExposure;
|
|
4
|
+
const core_1 = require("../../core");
|
|
5
|
+
const BASE_URL = 'https://api-portfolio-octopos-mainnet.untangled.finance';
|
|
6
|
+
/**
|
|
7
|
+
* Read a response body as JSON without throwing.
|
|
8
|
+
*
|
|
9
|
+
* Used on the error path, where a malformed or empty body must not mask the
|
|
10
|
+
* HTTP status we actually want to report.
|
|
11
|
+
*
|
|
12
|
+
* @param response - The fetch response to drain.
|
|
13
|
+
* @returns The parsed body, or `null` when it is absent or not JSON.
|
|
14
|
+
*/
|
|
15
|
+
async function readJsonSafely(response) {
|
|
16
|
+
try {
|
|
17
|
+
return await response.json();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetch a vault's full capital exposure from the Untangled portfolio API.
|
|
25
|
+
*
|
|
26
|
+
* Untangled reconciles Stellar vaults end to end — the vault's idle buffer,
|
|
27
|
+
* capital deployed into Stellar protocols, custody wallet balances, and value
|
|
28
|
+
* bridged to other chains. DeBank and Octav.fi cover none of that, which is why
|
|
29
|
+
* Stellar vaults previously rendered an empty exposure breakdown.
|
|
30
|
+
*
|
|
31
|
+
* The endpoint is keyed by **vault** address and already aggregates every
|
|
32
|
+
* borrower, so it must be called once per vault — never once per borrower, which
|
|
33
|
+
* would multiply the same totals.
|
|
34
|
+
*
|
|
35
|
+
* Requests are plain `GET`s with no custom headers. That is deliberate: it keeps
|
|
36
|
+
* them preflight-free in the browser, where the API's `access-control-allow-origin: *`
|
|
37
|
+
* covers the simple request but a bare `OPTIONS` is rejected.
|
|
38
|
+
*
|
|
39
|
+
* Never throws. Every failure — unknown vault, upstream error, transport
|
|
40
|
+
* failure, malformed body — resolves to `null` so callers can degrade to an
|
|
41
|
+
* empty exposure section instead of losing the rest of their response.
|
|
42
|
+
*
|
|
43
|
+
* Worst case: exactly **1 HTTP call**. No retry, no caching (the API is edge
|
|
44
|
+
* cached at `s-maxage=60`, well inside its 60 req/min budget for one call per
|
|
45
|
+
* vault page load).
|
|
46
|
+
*
|
|
47
|
+
* @param vault - Vault address, e.g. a Stellar contract address (`C…`).
|
|
48
|
+
* @returns The resolved exposure report, or `null` when Untangled does not
|
|
49
|
+
* reconcile this vault or the request failed.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const exposure = await fetchUntangledVaultExposure(
|
|
54
|
+
* 'CCL3WITWFFXIHV2I52ECV5DPIEOFSTU3PBPR53ILPLF2IP5KHECXRUTY',
|
|
55
|
+
* );
|
|
56
|
+
* // exposure?.tvlUsd → 24845637.05
|
|
57
|
+
* // exposure?.byProtocol → [{ protocolId: 'stellar-wallet', … }, { protocolId: 'blend', … }, …]
|
|
58
|
+
* const normalized = transformUntangledToDebank(exposure);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
async function fetchUntangledVaultExposure(vault) {
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(`${BASE_URL}/v1/vaults/${encodeURIComponent(vault)}/exposure`, { method: 'GET' });
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const body = await readJsonSafely(response);
|
|
66
|
+
// 404 is the documented "we don't reconcile this vault" answer, not a
|
|
67
|
+
// fault — most vaults are not Untangled-covered. Keep it a breadcrumb so
|
|
68
|
+
// a known, permanent gap does not fill Sentry.
|
|
69
|
+
if (response.status === 404) {
|
|
70
|
+
core_1.Logger.log.warn('fetchUntangledVaultExposure:unresolved', {
|
|
71
|
+
vault,
|
|
72
|
+
status: response.status,
|
|
73
|
+
reason: body?.reason ?? 'unknown_vault',
|
|
74
|
+
});
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
core_1.Logger.log.error('fetchUntangledVaultExposure', new Error(`HTTP ${response.status} ${response.statusText}`), { vault, status: response.status });
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
82
|
+
core_1.Logger.log.error('fetchUntangledVaultExposure.unexpected-type', new Error(`unexpected response type: ${typeof data}`), { vault });
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (data.resolved !== true) {
|
|
86
|
+
core_1.Logger.log.warn('fetchUntangledVaultExposure:unresolved', {
|
|
87
|
+
vault,
|
|
88
|
+
status: response.status,
|
|
89
|
+
reason: data.reason ?? 'unknown_vault',
|
|
90
|
+
});
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(data.byProtocol)) {
|
|
94
|
+
core_1.Logger.log.error('fetchUntangledVaultExposure.malformed', new Error('resolved response is missing byProtocol[]'), { vault });
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (data.stale) {
|
|
98
|
+
core_1.Logger.log.warn('fetchUntangledVaultExposure:stale', {
|
|
99
|
+
vault,
|
|
100
|
+
asOf: data.asOf,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
core_1.Logger.log.error('fetchUntangledVaultExposure', error, { vault });
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=fetcher.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./fetcher"), exports);
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
__exportStar(require("./utils"), exports);
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response types for the Untangled portfolio API.
|
|
3
|
+
*
|
|
4
|
+
* Shape verified live on 2026-09-01 against
|
|
5
|
+
* `GET https://api-portfolio-octopos-mainnet.untangled.finance/v1/vaults/{address}/exposure`
|
|
6
|
+
* for the Gami earnUSDC Stellar vault.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* One protocol slice of a vault's TVL.
|
|
10
|
+
*
|
|
11
|
+
* Untangled reports the whole vault, so the slices cover capital held in the
|
|
12
|
+
* vault contract itself, in custody wallets, and bridged to other chains — not
|
|
13
|
+
* just on-chain Stellar protocol positions.
|
|
14
|
+
*/
|
|
15
|
+
export interface IUntangledProtocolExposure {
|
|
16
|
+
/**
|
|
17
|
+
* Untangled protocol slug, e.g. `blend`, `templar`, `aquarius`. Two slugs are
|
|
18
|
+
* not protocols: `stellar-wallet` (custody wallets) and `upshift` (the vault's
|
|
19
|
+
* own idle buffer).
|
|
20
|
+
*/
|
|
21
|
+
protocolId: string;
|
|
22
|
+
/** USD value held in this protocol. */
|
|
23
|
+
valueUsd: number;
|
|
24
|
+
/** Share of the vault's `tvlUsd`, as a percentage (`23.54` means 23.54%). */
|
|
25
|
+
pct: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One asset slice of a vault's TVL. Vault-wide — the API does not currently
|
|
29
|
+
* break assets down per protocol.
|
|
30
|
+
*/
|
|
31
|
+
export interface IUntangledAssetExposure {
|
|
32
|
+
/** Asset ticker, e.g. `USDC`, `XLM`. */
|
|
33
|
+
symbol: string;
|
|
34
|
+
/** USD value held in this asset across the whole vault. */
|
|
35
|
+
valueUsd: number;
|
|
36
|
+
/** Share of the vault's `tvlUsd`, as a percentage. */
|
|
37
|
+
pct: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One address holding part of a vault's capital — the vault contract, a custody
|
|
41
|
+
* wallet, or a bridged account on another chain.
|
|
42
|
+
*/
|
|
43
|
+
export interface IUntangledLocation {
|
|
44
|
+
/** Address on `chain`. Not necessarily a Stellar address (NEAR accounts appear here). */
|
|
45
|
+
address: string;
|
|
46
|
+
/** Chain slug the address lives on, e.g. `stellar`, `near`. */
|
|
47
|
+
chain: string;
|
|
48
|
+
/** Human-readable label, e.g. `idle`, `ForDefi`, `Templar`. */
|
|
49
|
+
label: string;
|
|
50
|
+
/** USD value held at this address. */
|
|
51
|
+
valueUsd: number;
|
|
52
|
+
/** ISO-8601 timestamp this location was last reconciled. */
|
|
53
|
+
asOf: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A resolved vault exposure report.
|
|
57
|
+
*
|
|
58
|
+
* Untangled answers with `{ resolved: false, reason }` and HTTP 404 for vaults
|
|
59
|
+
* it does not reconcile; `fetchUntangledVaultExposure` maps that to `null`, so
|
|
60
|
+
* `resolved` is always `true` on values that reach a caller.
|
|
61
|
+
*/
|
|
62
|
+
export interface IUntangledVaultExposure {
|
|
63
|
+
/** Always `true` — unresolved reports never reach callers. */
|
|
64
|
+
resolved: true;
|
|
65
|
+
/** The vault this report describes. */
|
|
66
|
+
vault: {
|
|
67
|
+
address: string;
|
|
68
|
+
chain: string;
|
|
69
|
+
};
|
|
70
|
+
/** Total USD value Untangled reconciled across every location. */
|
|
71
|
+
tvlUsd: number;
|
|
72
|
+
/** Protocol-level split of `tvlUsd`. */
|
|
73
|
+
byProtocol: IUntangledProtocolExposure[];
|
|
74
|
+
/** Asset-level split of `tvlUsd`, vault-wide. */
|
|
75
|
+
byAsset: IUntangledAssetExposure[];
|
|
76
|
+
/** Per-address drill-down of where the capital sits. */
|
|
77
|
+
locations: IUntangledLocation[];
|
|
78
|
+
/** ISO-8601 timestamp the report was assembled. */
|
|
79
|
+
asOf: string;
|
|
80
|
+
/** `true` when Untangled served a report it could not refresh in time. */
|
|
81
|
+
stale: boolean;
|
|
82
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Response types for the Untangled portfolio API.
|
|
4
|
+
*
|
|
5
|
+
* Shape verified live on 2026-09-01 against
|
|
6
|
+
* `GET https://api-portfolio-octopos-mainnet.untangled.finance/v1/vaults/{address}/exposure`
|
|
7
|
+
* for the Gami earnUSDC Stellar vault.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { IDebankNormalizedResponse } from '../debank/utils';
|
|
2
|
+
import type { IUntangledVaultExposure } from './types';
|
|
3
|
+
/** Display metadata for an Untangled protocol slug. */
|
|
4
|
+
export interface IUntangledProtocolMetadata {
|
|
5
|
+
/** Name rendered in the exposure breakdown. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Chain the protocol position lives on — not always the vault's own chain. */
|
|
8
|
+
chain: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Display metadata for the protocol slugs Untangled reports today.
|
|
12
|
+
*
|
|
13
|
+
* Untangled does not return protocol names or chains, so they are mapped here.
|
|
14
|
+
* Unknown slugs fall back to a capitalized slug on the vault's chain rather than
|
|
15
|
+
* being dropped, so a newly integrated protocol still shows up (unlabelled)
|
|
16
|
+
* instead of silently vanishing from the breakdown.
|
|
17
|
+
*/
|
|
18
|
+
export declare const UNTANGLED_PROTOCOL_REGISTRY: Record<string, IUntangledProtocolMetadata>;
|
|
19
|
+
/**
|
|
20
|
+
* Transform an Untangled vault exposure report into the DeBank-normalized shape
|
|
21
|
+
* that `parseVaultLevelDebank` and `parseLoanLevelDebank` consume.
|
|
22
|
+
*
|
|
23
|
+
* This is the Stellar equivalent of `transformOctavfiToDebank`: a foreign
|
|
24
|
+
* portfolio shape is mapped onto the one normalized structure the vault
|
|
25
|
+
* allocation parser understands, so Stellar vaults populate the same
|
|
26
|
+
* `exposurePerCategory` / `protocolExposure` / `tokenExposure` outputs as EVM
|
|
27
|
+
* vaults with no parser changes.
|
|
28
|
+
*
|
|
29
|
+
* Mapping:
|
|
30
|
+
* - Each `byProtocol` entry becomes one position with a single supplying token,
|
|
31
|
+
* valued in USD (`price: 1`), on the chain from
|
|
32
|
+
* {@link UNTANGLED_PROTOCOL_REGISTRY}.
|
|
33
|
+
* - `stellar-wallet` and `upshift` are summed into one wallet token, because
|
|
34
|
+
* both are undeployed capital rather than protocol exposure.
|
|
35
|
+
* - Dust is left in place: `filterOutBySize` in the parser applies the same
|
|
36
|
+
* threshold Stellar and EVM vaults share.
|
|
37
|
+
*
|
|
38
|
+
* `locations`, `asOf`, `stale` and `tvlUsd` are not consumed — TVL already comes
|
|
39
|
+
* from `getVaultTvl`, and freshness is not yet surfaced on `IVaultAllocations`.
|
|
40
|
+
*
|
|
41
|
+
* @param exposure - A resolved Untangled report, or `null`/`undefined` when the
|
|
42
|
+
* fetch failed.
|
|
43
|
+
* @returns A DeBank-normalized response. Empty positions and tokens when
|
|
44
|
+
* `exposure` is absent, so callers can pass a failed fetch straight through.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const exposure = await fetchUntangledVaultExposure(vault);
|
|
49
|
+
* const normalized = transformUntangledToDebank(exposure);
|
|
50
|
+
* parseVaultLevelDebank(
|
|
51
|
+
* normalized, protocolExposure, tokenExposure, vault, exposurePerCategory, netValue,
|
|
52
|
+
* );
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function transformUntangledToDebank(exposure: IUntangledVaultExposure | null | undefined): IDebankNormalizedResponse;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UNTANGLED_PROTOCOL_REGISTRY = void 0;
|
|
4
|
+
exports.transformUntangledToDebank = transformUntangledToDebank;
|
|
5
|
+
/**
|
|
6
|
+
* Display metadata for the protocol slugs Untangled reports today.
|
|
7
|
+
*
|
|
8
|
+
* Untangled does not return protocol names or chains, so they are mapped here.
|
|
9
|
+
* Unknown slugs fall back to a capitalized slug on the vault's chain rather than
|
|
10
|
+
* being dropped, so a newly integrated protocol still shows up (unlabelled)
|
|
11
|
+
* instead of silently vanishing from the breakdown.
|
|
12
|
+
*/
|
|
13
|
+
exports.UNTANGLED_PROTOCOL_REGISTRY = {
|
|
14
|
+
blend: { name: 'Blend', chain: 'stellar' },
|
|
15
|
+
templar: { name: 'Templar (NEAR)', chain: 'near' },
|
|
16
|
+
aquarius: { name: 'Aquarius', chain: 'stellar' },
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Slugs that are not protocol positions: `stellar-wallet` is custody wallet
|
|
20
|
+
* cash and `upshift` is the vault's own idle buffer. Both are undeployed
|
|
21
|
+
* capital, which the exposure breakdown renders as wallet holdings — the same
|
|
22
|
+
* treatment EVM vaults get for funds sitting outside a protocol.
|
|
23
|
+
*/
|
|
24
|
+
const WALLET_PROTOCOL_IDS = new Set(['stellar-wallet', 'upshift']);
|
|
25
|
+
const DEFAULT_CHAIN = 'stellar';
|
|
26
|
+
/**
|
|
27
|
+
* Stellar's asset precision, carried only to satisfy the DeBank token shape.
|
|
28
|
+
* The synthesized tokens hold USD values (`price: 1`), so no consumer scales by
|
|
29
|
+
* this.
|
|
30
|
+
*/
|
|
31
|
+
const SYNTHETIC_TOKEN_DECIMALS = 7;
|
|
32
|
+
/**
|
|
33
|
+
* Build a DeBank-shaped token carrying a USD value.
|
|
34
|
+
*
|
|
35
|
+
* `parseVaultLevelDebank` computes USD as `price * amount`, so a value already
|
|
36
|
+
* denominated in dollars is expressed as `price: 1, amount: <usd>`.
|
|
37
|
+
*/
|
|
38
|
+
function toDebankToken(symbol, chain, valueUsd, exposureType, protocolId) {
|
|
39
|
+
return {
|
|
40
|
+
id: symbol,
|
|
41
|
+
chain,
|
|
42
|
+
name: symbol,
|
|
43
|
+
symbol,
|
|
44
|
+
optimized_symbol: symbol,
|
|
45
|
+
decimals: SYNTHETIC_TOKEN_DECIMALS,
|
|
46
|
+
logo_url: '',
|
|
47
|
+
amount: valueUsd,
|
|
48
|
+
price: 1,
|
|
49
|
+
protocol_id: protocolId ?? '',
|
|
50
|
+
supplying_exposure: exposureType === 'supply' ? 'true' : undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The asset holding most of the vault's value.
|
|
55
|
+
*
|
|
56
|
+
* Untangled splits assets vault-wide rather than per protocol, so every
|
|
57
|
+
* synthesized position is tagged with this one symbol. For the Gami vaults it is
|
|
58
|
+
* an exact description (earnUSDC is ~100% USDC); an exact per-protocol split
|
|
59
|
+
* needs `byProtocol[].assets[]` from Untangled.
|
|
60
|
+
*/
|
|
61
|
+
function dominantAssetSymbol(exposure) {
|
|
62
|
+
const dominant = (exposure.byAsset || []).reduce((best, asset) => !best || Number(asset?.valueUsd || 0) > Number(best.valueUsd || 0)
|
|
63
|
+
? asset
|
|
64
|
+
: best, null);
|
|
65
|
+
return dominant?.symbol || 'UNKNOWN';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Transform an Untangled vault exposure report into the DeBank-normalized shape
|
|
69
|
+
* that `parseVaultLevelDebank` and `parseLoanLevelDebank` consume.
|
|
70
|
+
*
|
|
71
|
+
* This is the Stellar equivalent of `transformOctavfiToDebank`: a foreign
|
|
72
|
+
* portfolio shape is mapped onto the one normalized structure the vault
|
|
73
|
+
* allocation parser understands, so Stellar vaults populate the same
|
|
74
|
+
* `exposurePerCategory` / `protocolExposure` / `tokenExposure` outputs as EVM
|
|
75
|
+
* vaults with no parser changes.
|
|
76
|
+
*
|
|
77
|
+
* Mapping:
|
|
78
|
+
* - Each `byProtocol` entry becomes one position with a single supplying token,
|
|
79
|
+
* valued in USD (`price: 1`), on the chain from
|
|
80
|
+
* {@link UNTANGLED_PROTOCOL_REGISTRY}.
|
|
81
|
+
* - `stellar-wallet` and `upshift` are summed into one wallet token, because
|
|
82
|
+
* both are undeployed capital rather than protocol exposure.
|
|
83
|
+
* - Dust is left in place: `filterOutBySize` in the parser applies the same
|
|
84
|
+
* threshold Stellar and EVM vaults share.
|
|
85
|
+
*
|
|
86
|
+
* `locations`, `asOf`, `stale` and `tvlUsd` are not consumed — TVL already comes
|
|
87
|
+
* from `getVaultTvl`, and freshness is not yet surfaced on `IVaultAllocations`.
|
|
88
|
+
*
|
|
89
|
+
* @param exposure - A resolved Untangled report, or `null`/`undefined` when the
|
|
90
|
+
* fetch failed.
|
|
91
|
+
* @returns A DeBank-normalized response. Empty positions and tokens when
|
|
92
|
+
* `exposure` is absent, so callers can pass a failed fetch straight through.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const exposure = await fetchUntangledVaultExposure(vault);
|
|
97
|
+
* const normalized = transformUntangledToDebank(exposure);
|
|
98
|
+
* parseVaultLevelDebank(
|
|
99
|
+
* normalized, protocolExposure, tokenExposure, vault, exposurePerCategory, netValue,
|
|
100
|
+
* );
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
function transformUntangledToDebank(exposure) {
|
|
104
|
+
if (!exposure) {
|
|
105
|
+
return { subaccount: { positions: [], tokens: [] } };
|
|
106
|
+
}
|
|
107
|
+
const symbol = dominantAssetSymbol(exposure);
|
|
108
|
+
const vaultChain = exposure.vault?.chain || DEFAULT_CHAIN;
|
|
109
|
+
const positions = [];
|
|
110
|
+
let walletValueUsd = 0;
|
|
111
|
+
(exposure.byProtocol || []).forEach((entry) => {
|
|
112
|
+
if (!entry?.protocolId)
|
|
113
|
+
return;
|
|
114
|
+
const valueUsd = Number(entry.valueUsd) || 0;
|
|
115
|
+
if (WALLET_PROTOCOL_IDS.has(entry.protocolId)) {
|
|
116
|
+
walletValueUsd += valueUsd;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const metadata = exports.UNTANGLED_PROTOCOL_REGISTRY[entry.protocolId];
|
|
120
|
+
const chain = metadata?.chain || vaultChain;
|
|
121
|
+
const name = metadata?.name ||
|
|
122
|
+
entry.protocolId.charAt(0).toUpperCase() + entry.protocolId.slice(1);
|
|
123
|
+
positions.push({
|
|
124
|
+
id: entry.protocolId,
|
|
125
|
+
chain,
|
|
126
|
+
name,
|
|
127
|
+
logo_url: '',
|
|
128
|
+
site_url: '',
|
|
129
|
+
portfolio_item_list: [
|
|
130
|
+
{
|
|
131
|
+
name,
|
|
132
|
+
stats: {
|
|
133
|
+
asset_usd_value: valueUsd,
|
|
134
|
+
debt_usd_value: 0,
|
|
135
|
+
net_usd_value: valueUsd,
|
|
136
|
+
},
|
|
137
|
+
detail: {
|
|
138
|
+
supply_token_list: [
|
|
139
|
+
toDebankToken(symbol, chain, valueUsd, 'supply', entry.protocolId),
|
|
140
|
+
],
|
|
141
|
+
borrow_token_list: [],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
const tokens = [];
|
|
148
|
+
if (walletValueUsd > 0) {
|
|
149
|
+
tokens.push({
|
|
150
|
+
...toDebankToken(symbol, vaultChain, walletValueUsd, 'wallet'),
|
|
151
|
+
is_wallet: true,
|
|
152
|
+
is_core: true,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return { subaccount: { positions, tokens } };
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=utils.js.map
|