-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathindex.ts
More file actions
1861 lines (1802 loc) · 72.1 KB
/
Copy pathindex.ts
File metadata and controls
1861 lines (1802 loc) · 72.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Shared domain types used across all modules.
/**
* EVM chains supported by the server. Intentionally kept narrow so every
* `Record<SupportedChain, …>` table in the codebase continues to represent
* "per-EVM-chain" configuration — viem clients, Aave/Compound/Uniswap
* addresses, numeric chain IDs, etc.
*
* Non-EVM chains (currently only TRON) live in `SupportedNonEvmChain`, and
* the `AnyChain` union below is what cross-chain entry points (tool inputs,
* portfolio summary) accept. This split keeps TRON strictly additive: EVM
* internals don't need to learn that TRON exists.
*/
export type SupportedChain = "ethereum" | "arbitrum" | "polygon" | "base" | "optimism";
export const SUPPORTED_CHAINS: readonly SupportedChain[] = [
"ethereum",
"arbitrum",
"polygon",
"base",
"optimism",
] as const;
/** Non-EVM chains. Kept as its own union so EVM-only tables keep their type. */
export type SupportedNonEvmChain = "tron" | "solana";
export const SUPPORTED_NON_EVM_CHAINS: readonly SupportedNonEvmChain[] = [
"tron",
"solana",
] as const;
/** Any chain the server knows about — EVM or non-EVM. */
export type AnyChain = SupportedChain | SupportedNonEvmChain;
export const ALL_CHAINS: readonly AnyChain[] = [
...SUPPORTED_CHAINS,
...SUPPORTED_NON_EVM_CHAINS,
] as const;
export function isEvmChain(c: AnyChain): c is SupportedChain {
return (SUPPORTED_CHAINS as readonly string[]).includes(c);
}
export type RpcProvider = "infura" | "alchemy" | "custom";
/** Numeric chain IDs for the chains we support. */
export const CHAIN_IDS: Record<SupportedChain, number> = {
ethereum: 1,
arbitrum: 42161,
polygon: 137,
base: 8453,
optimism: 10,
};
export const CHAIN_ID_TO_NAME: Record<number, SupportedChain> = {
1: "ethereum",
42161: "arbitrum",
137: "polygon",
8453: "base",
10: "optimism",
};
/**
* TRON mainnet chain id, as used by the WalletConnect `tron:` namespace and
* the TronGrid mainnet endpoint. The numeric value is 0x2b6653dc (728126428),
* the first 4 bytes of the genesis block hash.
*/
export const TRON_CHAIN_ID = 728126428;
/** A token balance with optional USD valuation. */
export interface TokenAmount {
token: `0x${string}`;
symbol: string;
decimals: number;
/** Raw integer amount as a decimal string (e.g. "1000000" for 1 USDC). */
amount: string;
/** Human-readable amount (e.g. "1.0" for 1 USDC). */
formatted: string;
valueUsd?: number;
priceUsd?: number;
/**
* True when we could not resolve a USD price for this token. `valueUsd` is
* `undefined` rather than 0, and portfolio totals will NOT include this
* balance — callers should flag it to the user instead of silently treating
* it as worthless.
*/
priceMissing?: boolean;
}
/**
* Per-subsystem status reported alongside a portfolio summary, so callers can
* distinguish "no Aave position" (covered:true, positions empty) from "Aave
* fetch failed" (covered:false, errored:true) from "not attempted" (covered:
* false, errored:false — e.g. Morpho Blue, which requires caller-supplied
* market ids and so has no on-chain enumeration path from a wallet).
*/
export interface CoverageStatus {
covered: boolean;
errored?: boolean;
/** Free-form message explaining why `covered` is false when it is. */
note?: string;
}
export interface PortfolioCoverage {
aave: CoverageStatus;
compound: CoverageStatus;
morpho: CoverageStatus;
uniswapV3: CoverageStatus;
staking: CoverageStatus;
/**
* TRON balance fetch coverage. `covered:false, errored:false` means no TRON
* address was queried (treated like Morpho's "not attempted"); errored:true
* means a TronGrid call failed and TRX/TRC-20 are missing from totals.
*/
tron?: CoverageStatus;
/**
* TRON staking fetch coverage — independent of the balance fetch so a
* getReward/account outage doesn't mask that balances loaded fine.
*/
tronStaking?: CoverageStatus;
/**
* Solana balance fetch coverage (SOL + SPL). `covered:false, errored:false`
* means no Solana address was queried; errored:true means the Solana RPC
* call failed and SOL/SPL are missing from totals.
*/
solana?: CoverageStatus;
/**
* MarginFi position fetch coverage. Tracked separately from `solana` so a
* MarginFi-reader failure doesn't mask a successful balance read (mirror of
* `tronStaking` / `tron` split). Absent when no Solana address was queried.
*/
marginfi?: CoverageStatus;
/**
* Kamino position fetch coverage. Same separation rationale as `marginfi` —
* a Kamino-reader failure shouldn't mask a successful balance read. Absent
* when no Solana address was queried.
*/
kamino?: CoverageStatus;
/**
* Solana staking position fetch coverage (Marinade mSOL, Jito jitoSOL,
* native stake accounts). Mirrors the `marginfi` split so a staking-
* reader failure doesn't mask a successful balance read. Absent when no
* Solana address was queried.
*/
solanaStaking?: CoverageStatus;
/**
* Bitcoin balance fetch coverage. `covered:false, errored:false` means
* no Bitcoin address(es) were queried (treated like the TRON / Solana
* "not attempted" semantics); errored:true means the indexer call
* failed and BTC totals are missing.
*/
bitcoin?: CoverageStatus;
/**
* Litecoin balance fetch coverage. Mirrors `bitcoin` — covered:false +
* errored:false means no LTC address was queried; errored:true means
* the indexer call failed and LTC totals are missing. Issue #274.
*/
litecoin?: CoverageStatus;
/** Number of token balances whose USD valuation could not be resolved. */
unpricedAssets: number;
/**
* Structured list of which specific tokens couldn't be priced — one entry
* per affected balance. Previously only `unpricedAssets: N` (a count) was
* surfaced, which left the agent unable to tell the user WHICH balance
* was dropped from USD totals. With this list the agent can produce a
* concrete warning like "705 MATIC on polygon couldn't be priced and isn't
* included in the total" instead of a bare integer. Absent when
* `unpricedAssets === 0` to keep happy-path responses lean (issue #94).
*/
unpricedAssetsDetail?: UnpricedAsset[];
}
/**
* A single unpriced balance the portfolio couldn't value in USD. The chain
* is a string union spanning EVM + TRON + Solana so one array describes the
* cross-chain set without needing per-chain buckets.
*/
export interface UnpricedAsset {
chain: SupportedChain | "tron" | "solana" | "bitcoin" | "litecoin";
symbol: string;
/** Human-readable balance (already-decimals-applied), e.g. "705.141". */
amount: string;
}
/**
* Curve LP position — v0.1 surface (issue stable_ng-only, plain pools only).
*
* Each entry is one (pool, wallet) combination where the wallet has either
* a direct LP balance or a gauge-staked balance (or both). Pools where the
* wallet has zero of both are filtered out at composer level so the
* response stays scannable for users with positions in 3+ pools.
*/
export interface CurvePosition {
protocol: "curve";
chain: SupportedChain;
poolAddress: `0x${string}`;
poolType: "stable-ng-plain";
/**
* The pool's coin addresses, in the order add_liquidity expects.
* For wrapped-native pools (e.g. WETH-paired), addresses point to the
* wrapper (no native ETH special-casing yet — v2 follow-up).
*/
coins: `0x${string}`[];
/** User's direct LP balance (LP token == pool address on stable_ng). */
lpBalance: string;
/** User's gauge-staked LP balance. Zero when no gauge or not staked. */
gaugeStakedBalance: string;
/** Pending claimable CRV. Zero when no gauge or no rewards accrued. */
pendingCrv: string;
/**
* Gauge address for this pool, when one exists. Some stable_ng pools
* have no gauge deployed (factory.get_gauge returns zero address) —
* `null` in that case so callers don't render a "stake in gauge" CTA.
*/
gaugeAddress: `0x${string}` | null;
}
export interface LendingPosition {
protocol: "aave-v3";
chain: SupportedChain;
collateral: TokenAmount[];
debt: TokenAmount[];
totalCollateralUsd: number;
totalDebtUsd: number;
netValueUsd: number;
/** Aave health factor (>1 safe, <1 liquidatable). Infinity if no debt. */
healthFactor: number;
/** Weighted average liquidation threshold (bps, e.g. 8250 = 82.5%). */
liquidationThreshold: number;
/** Weighted average loan-to-value (bps). */
ltv: number;
/**
* Per-asset warnings derived from reserve.isPaused / reserve.isFrozen. Scoped
* to assets the user actually holds or borrows — a pause on a market they
* aren't in isn't a surprise for their position. Paused = all ops blocked
* until governance unpauses; Frozen = no new supplies/borrows but existing
* positions can still withdraw/repay.
*/
warnings?: string[];
}
/**
* A Compound V3 (Comet) position, flattened enough to slot alongside Aave in a unified
* lending bucket. Kept as a thin projection of modules/compound/index.ts#CompoundPosition
* so the types module doesn't need to pull in compound internals.
*/
export interface CompoundLendingPosition {
protocol: "compound-v3";
chain: SupportedChain;
market: string;
marketAddress: `0x${string}`;
baseSupplied: TokenAmount | null;
baseBorrowed: TokenAmount | null;
collateral: TokenAmount[];
totalCollateralUsd: number;
totalDebtUsd: number;
totalSuppliedUsd: number;
netValueUsd: number;
/**
* Governance-paused actions on this Comet market. Subset of
* {supply, transfer, withdraw, absorb, buy}. Omitted when nothing is paused
* so the JSON shape of healthy positions doesn't change.
*/
pausedActions?: ("supply" | "transfer" | "withdraw" | "absorb" | "buy")[];
}
/**
* A Morpho Blue position, flattened enough to slot alongside Aave and Compound in a
* unified lending bucket. Thin projection of modules/morpho/index.ts#MorphoPosition
* so the types module doesn't need to pull in morpho internals.
*/
export interface MorphoLendingPosition {
protocol: "morpho-blue";
chain: SupportedChain;
marketId: `0x${string}`;
loanToken: `0x${string}`;
collateralToken: `0x${string}`;
lltv: string;
supplied: TokenAmount | null;
borrowed: TokenAmount | null;
collateral: TokenAmount | null;
totalCollateralUsd: number;
totalDebtUsd: number;
totalSuppliedUsd: number;
netValueUsd: number;
}
/** Any lending/borrowing position reported by the portfolio aggregator. */
export type LendingPositionUnion =
| LendingPosition
| CompoundLendingPosition
| MorphoLendingPosition;
export interface LPPosition {
protocol: "uniswap-v3";
chain: SupportedChain;
tokenId: string;
token0: TokenAmount;
token1: TokenAmount;
/** Fee tier in hundredths of a bip (500 = 0.05%, 3000 = 0.30%, 10000 = 1.0%). */
feeTier: number;
tickLower: number;
tickUpper: number;
currentTick: number;
inRange: boolean;
liquidity: string;
/**
* Fees that have been checkpointed into NonfungiblePositionManager.tokensOwed
* (e.g. by a prior collect/burn touch). Fees accrued since the last
* checkpoint are NOT included — to see the full collectable amount, the
* caller would need to simulate collect() against fork state. Treat this as
* a LOWER BOUND on what a collect would return.
*/
tokensOwedCached0: TokenAmount;
tokensOwedCached1: TokenAmount;
/**
* USD value derived from token amounts computed at the current tick. This
* is an approximation: withdrawing the position at a different price would
* yield different amounts. Flagged as `valueUsdIsApproximate: true` so
* callers don't display this as a precise number.
*/
totalValueUsd: number;
valueUsdIsApproximate: true;
}
export interface StakingPosition {
protocol: "lido" | "eigenlayer";
chain: SupportedChain;
stakedAmount: TokenAmount;
/** Current APR as a decimal (0.035 = 3.5%). */
apr?: number;
/** Optional delegation info (for EigenLayer). */
delegatedTo?: `0x${string}`;
/** Extra protocol-specific details (e.g. strategy address for EigenLayer). */
meta?: Record<string, string | number | boolean>;
}
export interface PrivilegedRole {
role: string;
holder: `0x${string}`;
isContract: boolean;
isMultisig: boolean;
hasTimelock: boolean;
timelockDelaySeconds?: number;
}
export interface SecurityReport {
address: `0x${string}`;
chain: SupportedChain;
isVerified: boolean;
isProxy: boolean;
implementation?: `0x${string}`;
admin?: `0x${string}`;
dangerousFunctions: string[];
privilegedRoles: PrivilegedRole[];
}
/**
* A TRON token balance. Shaped like TokenAmount but with a base58 `token`
* address (TRC-20 contracts are base58, starting with 'T') and a `chain`
* discriminator so consumers can tell TRC-20 apart from ERC-20 at runtime.
* Kept separate from TokenAmount so existing EVM readers don't grow a
* `chain: "tron"` branch they'd never exercise.
*/
export interface TronBalance {
chain: "tron";
/** Base58 TRC-20 contract address (prefix `T`), or "native" for TRX. */
token: string;
symbol: string;
decimals: number;
amount: string;
formatted: string;
valueUsd?: number;
priceUsd?: number;
priceMissing?: boolean;
}
/**
* Solana balance shape — a parallel to TronBalance for SOL + SPL tokens.
* `token` is a base58 SPL mint address (~32-44 chars), or "native" for SOL.
* SPL balances come from Associated Token Accounts but we surface them by
* mint; the ATA is an implementation detail the caller shouldn't care about.
*/
export interface SolanaBalance {
chain: "solana";
/** Base58 SPL mint address, or "native" for SOL. */
token: string;
symbol: string;
decimals: number;
amount: string;
formatted: string;
valueUsd?: number;
priceUsd?: number;
priceMissing?: boolean;
}
/**
* Solana slice of a portfolio summary. Parallel to TronPortfolioSlice.
* Phase 1 did not enumerate native validator staking; Phase 3 adds
* MarginFi lending.
*/
export interface SolanaPortfolioSlice {
/** Base58 Solana address the balances were resolved for. */
address: string;
native: SolanaBalance[];
spl: SolanaBalance[];
walletBalancesUsd: number;
/**
* MarginFi lending positions (Phase 3). Present only when the wallet has
* at least one MarginfiAccount with non-zero balances — probed via the
* deterministic PDA at accountIndex 0..3. An empty/missing field means
* no MarginFi position, not "reader errored" (errored case is surfaced
* through PortfolioCoverage.marginfi).
*/
marginfi?: SolanaMarginfiPositionSlice[];
/** MarginFi aggregate net USD (sum of netValueUsd across positions). */
marginfiNetUsd?: number;
/**
* Kamino lending positions on the main market. Present when the wallet
* has Kamino userMetadata + obligation with non-zero deposits or borrows.
* Empty/missing means no position; errored case surfaces through
* PortfolioCoverage.kamino.
*/
kamino?: SolanaKaminoPositionSlice[];
/** Kamino aggregate net USD (sum of netValueUsd across positions). */
kaminoNetUsd?: number;
/**
* Solana staking positions — Marinade mSOL, Jito jitoSOL, native stake
* accounts. Present when any of the three sections is non-empty for
* this wallet. Missing means nothing found (errored case surfaces
* through PortfolioCoverage.solanaStaking).
*/
staking?: SolanaStakingPositionSlice;
/** Solana staking aggregate net USD (SOL-equivalent × SOL price). */
stakingNetUsd?: number;
}
/**
* Thin projection of the three staking readers' output
* (`src/modules/positions/solana-staking.ts`). Kept in sync with
* `SolanaStakingPositions` but stripped down — the portfolio JSON doesn't
* need the per-reader wrapper metadata (wallet duplication, protocol
* tags on subtotals).
*/
export interface SolanaStakingPositionSlice {
chain: "solana";
/** mSOL balance + SOL-equivalent via Marinade's on-chain mSolPrice. */
marinade: {
mSolBalance: number;
solEquivalent: number;
exchangeRate: number;
};
/** jitoSOL balance + SOL-equivalent via stake-pool's totalLamports/supply. */
jito: {
jitoSolBalance: number;
solEquivalent: number;
exchangeRate: number;
};
/** One entry per native stake account (SPL stake-program) with activation status. */
nativeStakes: Array<{
stakePubkey: string;
validator?: string;
stakeSol: number;
status: "activating" | "active" | "deactivating" | "inactive";
activationEpoch?: number;
deactivationEpoch?: number;
}>;
/** Sum of SOL-equivalents across Marinade + Jito + native stakes. */
totalSolEquivalent: number;
}
/**
* Thin projection of the full `MarginfiPosition` type exposed by
* `src/modules/positions/marginfi.ts`. Kept here so the portfolio types
* module doesn't pull in the reader module's internals, matching how
* CompoundLendingPosition / MorphoLendingPosition are projections of their
* reader modules.
*/
export interface SolanaMarginfiPositionSlice {
protocol: "marginfi";
chain: "solana";
marginfiAccount: string;
supplied: Array<{ symbol: string; amount: string; valueUsd: number }>;
borrowed: Array<{ symbol: string; amount: string; valueUsd: number }>;
totalSuppliedUsd: number;
totalBorrowedUsd: number;
netValueUsd: number;
healthFactor: number;
warnings: string[];
}
/**
* Thin projection of the full `KaminoPosition` type exposed by
* `src/modules/positions/kamino.ts`. Same shape as MarginFi's slice; the
* `obligation` field is Kamino's per-(wallet, market, kind) state account
* (analogous to `marginfiAccount`).
*/
export interface SolanaKaminoPositionSlice {
protocol: "kamino";
chain: "solana";
obligation: string;
supplied: Array<{ symbol: string; amount: string; valueUsd: number }>;
borrowed: Array<{ symbol: string; amount: string; valueUsd: number }>;
totalSuppliedUsd: number;
totalBorrowedUsd: number;
netValueUsd: number;
healthFactor: number;
warnings: string[];
}
/**
* TRON slice of a portfolio summary. Contains the TRON-specific address the
* balances were fetched for (base58, which can't fit into the `wallet:
* 0x${string}` field on PortfolioSummary), TRX native balance, and TRC-20
* balances. Wallet-level coverage for TRON is tracked via
* PortfolioCoverage.tron.
*/
export interface TronPortfolioSlice {
/** Base58 TRON address the balances were resolved for. */
address: string;
native: TronBalance[];
trc20: TronBalance[];
walletBalancesUsd: number;
/**
* Staking position (frozen TRX, pending unfreezes, claimable rewards).
* Absent when the portfolio aggregator chose not to fetch staking (or
* when the TRON staking fetch failed — see PortfolioCoverage.tronStaking).
*/
staking?: TronStakingSlice;
}
/**
* A single "frozen for resource" entry under TRON's Stake 2.0 model. Users
* freeze TRX to obtain BANDWIDTH or ENERGY; the frozen TRX is what underlies
* their voting rights. Amount is reported in SUN (raw) + TRX (formatted).
*/
export interface TronFrozenEntry {
type: "bandwidth" | "energy";
/** Raw SUN (1 TRX = 1_000_000 SUN). */
amount: string;
/** Human-formatted TRX. */
formatted: string;
valueUsd?: number;
}
/**
* A pending unfreeze — the user initiated unstaking but the lockup window
* (14 days on mainnet) hasn't elapsed yet. `unlockAt` is the ISO timestamp
* after which `withdrawExpireUnfreeze` can claim the TRX back to liquid.
*/
export interface TronPendingUnfreeze {
type: "bandwidth" | "energy";
amount: string;
formatted: string;
/** ISO 8601 timestamp when the TRX becomes withdrawable. */
unlockAt: string;
valueUsd?: number;
}
/**
* Claimable voting rewards (distributed by the Super Representative the user
* voted for). Claiming requires a WithdrawBalance tx, landing in Phase 2.
*/
export interface TronClaimableReward {
amount: string;
formatted: string;
valueUsd?: number;
}
/**
* Live resource meter for a TRON account, in consumable UNITS (not TRX).
* Units are what each contract call charges against; frozen TRX only
* determines how many units you receive per day. `used` rolls off linearly
* over the 24h regen window, so `available = limit - used` is the
* instantaneous remaining headroom.
*/
export interface TronResourceMeter {
/** Units consumed in the current 24h window. */
usedUnits: number;
/** Total units available per 24h window at current freeze level. */
limitUnits: number;
/** `limitUnits - usedUnits` — immediately consumable. */
availableUnits: number;
}
/**
* Live account-resource snapshot from TronGrid's `/wallet/getaccountresource`.
* Distinct from `TronFrozenEntry`: that's the frozen TRX backing the
* resource, this is the units-available-right-now meter.
*
* Bandwidth has two sub-pools: `free` (600 units/day granted to every
* account, independent of stake) and `staked` (proportional to frozen TRX).
* TronGrid returns them as separate fields; we expose both because a fresh
* account with no stake still has the free pool and agents need to reason
* about it.
*/
export interface TronAccountResources {
bandwidth: {
free: TronResourceMeter;
staked: TronResourceMeter;
};
energy: TronResourceMeter;
/**
* Voting power derived from frozen TRX (1 TRX = 1 vote). `used` is how
* many votes are currently cast across all SRs; `available` is the
* unallocated headroom a new `prepare_tron_vote` can spend.
*/
votingPower: TronResourceMeter;
}
/**
* TRON staking view: frozen resources, pending unfreezes, claimable rewards.
* Totals roll up into the portfolio's `tronUsd` via `totalStakedUsd`.
*/
export interface TronStakingSlice {
address: string;
claimableRewards: TronClaimableReward;
frozen: TronFrozenEntry[];
pendingUnfreezes: TronPendingUnfreeze[];
/**
* Live consumable-units meter (independent of frozen TRX). Absent only
* when TronGrid's `/wallet/getaccountresource` fails — the rest of the
* staking slice still returns.
*/
resources?: TronAccountResources;
/**
* Per-SR vote allocation — same shape `list_tron_witnesses(address)`
* exposes via its `userVotes` field. Surfaced here too (issue #271) so
* an agent answering "consolidate my votes onto the SR I'm already
* voting for" or "rebalance freshly-unlocked TRON Power onto the same
* SRs" doesn't have to chain `list_tron_witnesses` after
* `get_tron_staking` (or, worse, fall back to bash + curl against
* TronGrid). Empty array when the wallet has no votes cast.
*
* Each entry's `address` is the base58 SR address; `count` is integer
* votes (1 vote = 1 frozen TRX of TRON Power). `prepare_tron_vote`
* REPLACES the entire vote allocation, so callers wanting to
* consolidate / rebalance must include every existing entry plus
* adjustments — this field gives them the exact baseline to mutate.
*/
votes: TronVoteAllocation[];
/** Frozen + pending-unfreeze + claimable, in TRX (formatted). */
totalStakedTrx: string;
/** USD value of everything above at current TRX price. */
totalStakedUsd: number;
}
/**
* A single Super Representative / SR candidate entry from TronGrid's
* `/wallet/listwitnesses`. Ranks are 1-based by voteCount DESC; active SRs
* are rank ≤ 27 (those that actually produce blocks and distribute voter
* rewards). Candidates have rank > 27 and receive no voter rewards.
*/
export interface TronWitnessInfo {
/** Base58 TRON address (prefix T). */
address: string;
/** SR operator URL (self-declared; not validated). */
url?: string;
/** Total vote weight for this SR, as a decimal string (1 frozen TRX = 1 vote). */
voteCount: string;
/** True iff rank ≤ 27 — this SR produces blocks. */
isActive: boolean;
/** 1-based rank by voteCount DESC. */
rank: number;
totalProduced?: number;
totalMissed?: number;
/**
* Rough annualised voter APR estimate as a decimal fraction (0.04 = 4 %).
* Computed from mainnet reward constants (160 TRX/block voter pool, ~28 800
* blocks/day, 365 days/year) divided by the total vote weight across the
* top 127 witnesses — the APR is therefore roughly uniform for every
* witness in the top 127. Witnesses ranked > 127 get 0. This is an
* ESTIMATE — actual rewards depend on per-SR commission, missed blocks,
* chain-param changes, and competing voters joining/leaving between your
* vote tx and reward claim.
*/
estVoterApr?: number;
}
/** The wallet's current vote allocation from `account.votes`. */
export interface TronVoteAllocation {
/** Base58 SR address the vote is cast for. */
address: string;
/** Integer vote count (1 vote = 1 frozen TRX of TRON Power). */
count: number;
}
export interface TronWitnessList {
witnesses: TronWitnessInfo[];
/** Present only when the caller passed `address`. */
userVotes?: TronVoteAllocation[];
/**
* Total TRON Power available to the caller's wallet (= integer TRX frozen
* under Stake 2.0, summed across bandwidth + energy). Set when `address`
* is passed.
*/
totalTronPower?: number;
/** Sum of userVotes[].count. Set when `address` is passed. */
totalVotesCast?: number;
/** totalTronPower − totalVotesCast, floored at 0. Set when `address` is passed. */
availableVotes?: number;
}
/**
* Bitcoin slice of a portfolio summary. Parallel to `TronPortfolioSlice`
* + `SolanaPortfolioSlice`. Bitcoin has no fungible token model in
* Phase 1 (BRC-20 / Runes / Ordinals deferred), so the slice carries
* only per-address native balances + the rolled-up USD totals.
*
* Multi-address: every BTC address the caller passed via
* `bitcoinAddress` (single) or `bitcoinAddresses` (array) is surfaced
* here. This mirrors `get_btc_balances` shape so callers who already
* use that tool see the same per-address projection inside the
* portfolio response.
*/
export interface BitcoinPortfolioSlice {
/** All addresses queried for this slice — at least one. */
addresses: string[];
/**
* Per-address breakdown. Each entry carries confirmed + mempool +
* total sats, the BTC-decimal projection, the address type, and the
* USD valuation. Identical shape to `BitcoinBalance` from the
* `btc/balances.ts` reader.
*/
balances: Array<{
address: string;
addressType: "p2pkh" | "p2sh" | "p2wpkh" | "p2wsh" | "p2tr";
confirmedSats: string;
mempoolSats: string;
totalSats: string;
confirmedBtc: string;
totalBtc: string;
symbol: "BTC";
decimals: 8;
txCount: number;
valueUsd?: number;
/** True when DefiLlama returned no price; balance is excluded from totals. */
priceMissing?: boolean;
}>;
/** Rolled-up USD value across all addresses (uses confirmed balance). */
walletBalancesUsd: number;
}
/**
* Litecoin slice of a portfolio summary. Mirror of `BitcoinPortfolioSlice`.
* Same UTXO model, same balance projection, different symbol/HRP.
*/
export interface LitecoinPortfolioSlice {
addresses: string[];
balances: Array<{
address: string;
addressType: "p2pkh" | "p2sh" | "p2wpkh" | "p2wsh" | "p2tr";
confirmedSats: string;
mempoolSats: string;
totalSats: string;
confirmedLtc: string;
totalLtc: string;
symbol: "LTC";
decimals: 8;
txCount: number;
valueUsd?: number;
priceMissing?: boolean;
}>;
walletBalancesUsd: number;
}
/** Per-wallet slice of a multi-wallet portfolio, or a stand-alone single-wallet summary. */
export interface PortfolioSummary {
wallet: `0x${string}`;
chains: SupportedChain[];
walletBalancesUsd: number;
lendingNetUsd: number;
lpUsd: number;
stakingUsd: number;
totalUsd: number;
perChain: Record<SupportedChain, number>;
/**
* TRON totals folded into the same number as EVM. Present when the caller
* passed a `tronAddress` (or TRON is in the default chain set and an
* address was resolvable).
*/
tronUsd?: number;
/**
* TRON staking USD (frozen + pending-unfreeze + claimable). Already included
* in `tronUsd` — this field surfaces it separately for UI. Present only when
* staking was fetched successfully.
*/
tronStakingUsd?: number;
/**
* Solana totals folded into the same aggregate as EVM/TRON. Present when
* the caller passed a `solanaAddress`. Phase 1 covers balances; Phase 3
* adds MarginFi lending (surfaced separately via `solanaLendingUsd`).
*/
solanaUsd?: number;
/**
* Solana lending net USD — MarginFi (Phase 3). Parallels `tronStakingUsd`
* as a carve-out that's separately surfaced in UIs but also folded into
* `totalUsd`. Present only when at least one MarginfiAccount was found
* for the wallet.
*/
solanaLendingUsd?: number;
/**
* Solana staking net USD — Marinade mSOL + Jito jitoSOL + native stake
* accounts (roadmap #2). Computed as `totalSolEquivalent * SOL price`
* using the same SOL price that valued the native-SOL balance line.
* Folded into `totalUsd`; carve-out here for UIs. Present only when the
* wallet holds at least some Solana staking.
*/
solanaStakingUsd?: number;
/**
* Bitcoin totals (sum across every address passed via `bitcoinAddress` /
* `bitcoinAddresses`). Present only when the caller supplied at least
* one BTC address. Folded into `totalUsd`.
*/
bitcoinUsd?: number;
/**
* Litecoin totals (sum across every address passed via `litecoinAddress` /
* `litecoinAddresses`). Present only when the caller supplied at least
* one LTC address. Folded into `totalUsd`.
*/
litecoinUsd?: number;
breakdown: {
native: TokenAmount[];
erc20: TokenAmount[];
lending: LendingPositionUnion[];
lp: LPPosition[];
staking: StakingPosition[];
/** TRON slice — absent when no TRON address was queried. */
tron?: TronPortfolioSlice;
/** Solana slice — absent when no Solana address was queried. */
solana?: SolanaPortfolioSlice;
/** Bitcoin slice — absent when no BTC address(es) were queried. */
bitcoin?: BitcoinPortfolioSlice;
/** Litecoin slice — absent when no LTC address(es) were queried. */
litecoin?: LitecoinPortfolioSlice;
};
coverage: PortfolioCoverage;
}
/** Multi-wallet portfolio aggregation. */
export interface MultiWalletPortfolioSummary {
wallets: `0x${string}`[];
chains: SupportedChain[];
totalUsd: number;
walletBalancesUsd: number;
lendingNetUsd: number;
lpUsd: number;
stakingUsd: number;
perChain: Record<SupportedChain, number>;
perWallet: PortfolioSummary[];
/**
* Non-EVM holdings surfaced as PARALLEL siblings of the EVM wallets,
* NOT folded into any specific `perWallet[i]`. Issue #201 — TRON / BTC /
* Solana addresses on a Ledger are independent identities (different
* BIP-44 derivation paths), so attributing them to "the first EVM
* wallet" produced misleading per-wallet rollups.
*
* Each chain's slice is surfaced when the corresponding address arg
* (`tronAddress`/`tronAddresses`, `solanaAddress`/`solanaAddresses`,
* `bitcoinAddress`/`bitcoinAddresses`) was passed to
* `getPortfolioSummary`. The USD rollups below sum across whichever
* slices were fetched.
*/
nonEvm?: {
/** Per-address TRON slice; one entry per requested tronAddress. */
tron?: TronPortfolioSlice[];
/** Per-address Solana slice; one entry per requested solanaAddress. */
solana?: SolanaPortfolioSlice[];
/** Multi-address Bitcoin slice; aggregates every requested btc address. */
bitcoin?: BitcoinPortfolioSlice;
/** Multi-address Litecoin slice; aggregates every requested ltc address. */
litecoin?: LitecoinPortfolioSlice;
};
/** Sum of all TRON wallet balances (TRX + TRC-20) across the queried addresses. */
tronUsd?: number;
/** Sum of TRON staking (frozen TRX + claimable rewards). */
tronStakingUsd?: number;
/** Sum of all Solana wallet balances (SOL + SPL) across queried addresses. */
solanaUsd?: number;
/** Sum of MarginFi + Kamino netValueUsd across queried Solana addresses. */
solanaLendingUsd?: number;
/** Sum of Marinade + Jito + native-stake totals across queried Solana addresses. */
solanaStakingUsd?: number;
/** Sum of BTC × USD-price across queried Bitcoin addresses. */
bitcoinUsd?: number;
/** Sum of LTC × USD-price across queried Litecoin addresses. */
litecoinUsd?: number;
coverage: PortfolioCoverage;
}
/**
* Unsigned TRON transaction. Shape is unavoidably different from EVM:
* TronGrid builds the tx server-side (raw_data + raw_data_hex) and the
* device signs the serialized raw_data_hex. We keep the TRON tx shape
* separate from UnsignedTx so send_transaction's EVM-only security pipeline
* (eth_call re-simulation, chain-id check, spender allowlist) can't be
* silently shortcut by a TRON handle masquerading as an EVM one.
*
* Phase 3 (this release) routes TRON handles through `send_transaction`:
* the USB HID signer (@ledgerhq/hw-app-trx) verifies the device address
* matches `from`, signs `rawDataHex`, and broadcasts via TronGrid.
*/
export interface UnsignedTronTx {
chain: "tron";
/** Discriminator for the preview + future signer branching. */
action:
| "native_send"
| "trc20_send"
| "trc20_approve"
| "claim_rewards"
| "freeze"
| "unfreeze"
| "withdraw_expire_unfreeze"
| "vote"
| "lifi_swap"
| "sunswap_swap";
/** Base58 owner address (prefix T). */
from: string;
/** TronGrid-returned transaction ID (sha256 of raw_data_hex, hex string). */
txID: string;
/**
* TronGrid's raw_data object — opaque to us; serialized in raw_data_hex.
* Required for the standard `/wallet/broadcasttransaction` path. ABSENT
* for `lifi_swap` flows where we receive only `raw_data_hex` from LiFi
* and broadcast via `/wallet/broadcasthex` instead (broadcast.ts branches
* on this).
*/
rawData?: unknown;
/** Hex-encoded raw_data used by the signer. */
rawDataHex: string;
/** Human-readable description for the preview. */
description: string;
decoded: {
functionName: string;
args: Record<string, string>;
/**
* ABI-encoded parameter payload (no `0x`, no selector) for TRC-20 calls.
* Set by the trc20_send / trc20_approve builders so the verification
* layer can compose the full calldata (`0x<selector><parameterHex>`)
* without re-deriving it from the human-readable args.
*/
parameterHex?: string;
};
/**
* Fee limit in SUN, present on contract calls (TRC-20 transfers require it;
* TronGrid rejects triggersmartcontract without one). Absent on native TRX
* sends and WithdrawBalance — those pay bandwidth only.
*/
feeLimitSun?: string;
/**
* Energy units the pre-flight triggerconstantcontract call consumed. Only
* present on contract calls where we pre-flight (TRC-20 transfers). The
* on-chain burn will be within a few percent of this number.
*/
estimatedEnergyUsed?: string;
/**
* Estimated fee in SUN that will actually burn on-chain — energy units
* times the mainnet energy price (420 sun/energy as of 2024-10). The
* preview shows this alongside `feeLimitSun` so the user can see
* "expected ~15 TRX" next to "cap 100 TRX" and not think the cap is the
* charge.
*/
estimatedEnergyCostSun?: string;
/** Opaque handle — see tron-tx-store.ts. Phase 3 signer consumes this. */
handle?: string;
/**
* Pre-sign verification payload, stamped by `issueTronHandle` on every
* prepared TRON tx. Optional during rollout; flipped to required after
* all call sites are updated.
*/
verification?: TxVerification;
/**
* Invariant #14 — durable-binding source-of-truth verification (issue
* #460). Populated by `prepare_tron_vote` (one binding per Super
* Representative voted for). Absent on other TRON op kinds.
*/
durableBindings?: import("../security/durable-binding.js").DurableBinding[];
}
/**
* Unsigned Solana transaction. Parallel to `UnsignedTronTx` — kept separate
* from `UnsignedTx` so `send_transaction`'s EVM-only security pipeline
* (eth_call re-simulation, EIP-1559 pin, spender allowlist) can't be
* silently shortcut by a Solana handle, and parallel to `UnsignedTronTx`
* because Solana's wire format (Ed25519 sig over a serialized tx message)
* is its own thing.
*
* Signing path: USB HID via `@ledgerhq/hw-app-solana` — Ledger Live's
* WalletConnect integration does NOT expose Solana accounts, so we mirror
* the TRON USB HID architecture (see `project_ledger_live_solana_wc.md`).
*/
export interface UnsignedSolanaTx {
chain: "solana";
/**
* Discriminator for the preview + future signer branching.
*
* - `native_send` / `spl_send` — user-facing transfers. Durable-nonce-
* protected (ix[0] = nonceAdvance); every send refuses to build until
* the wallet has an initialized nonce account.
* - `nonce_init` — one-time setup: createAccountWithSeed + nonceInitialize.