@strkfarm/sdk 2.0.0-dev.5 → 2.0.0-dev.51
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/dist/cli.js +190 -36
- package/dist/cli.mjs +188 -34
- package/dist/index.browser.global.js +118889 -92229
- package/dist/index.browser.mjs +13381 -11153
- package/dist/index.d.ts +2284 -1938
- package/dist/index.js +13794 -11360
- package/dist/index.mjs +14253 -11843
- package/package.json +59 -60
- package/src/data/avnu.abi.json +840 -0
- package/src/data/ekubo-price-fethcer.abi.json +265 -0
- package/src/data/redeem-request-nft.abi.json +752 -0
- package/src/data/universal-vault.abi.json +8 -7
- package/src/dataTypes/_bignumber.ts +13 -4
- package/src/dataTypes/bignumber.browser.ts +10 -1
- package/src/dataTypes/bignumber.node.ts +10 -1
- package/src/dataTypes/index.ts +3 -2
- package/src/dataTypes/mynumber.ts +141 -0
- package/src/global.ts +280 -233
- package/src/index.browser.ts +2 -1
- package/src/interfaces/common.tsx +229 -6
- package/src/modules/apollo-client-config.ts +28 -0
- package/src/modules/avnu.ts +21 -12
- package/src/modules/ekubo-pricer.ts +99 -0
- package/src/modules/ekubo-quoter.ts +48 -30
- package/src/modules/erc20.ts +17 -0
- package/src/modules/harvests.ts +43 -29
- package/src/modules/index.ts +2 -1
- package/src/modules/pragma.ts +23 -8
- package/src/modules/pricer-avnu-api.ts +114 -0
- package/src/modules/pricer-from-api.ts +159 -15
- package/src/modules/pricer-lst.ts +1 -1
- package/src/modules/pricer-quote-utils.ts +54 -0
- package/src/modules/pricer.ts +157 -54
- package/src/modules/pricerBase.ts +2 -1
- package/src/modules/zkLend.ts +3 -2
- package/src/node/deployer.ts +36 -1
- package/src/node/pricer-redis.ts +3 -1
- package/src/strategies/base-strategy.ts +168 -16
- package/src/strategies/constants.ts +8 -3
- package/src/strategies/ekubo-cl-vault.tsx +1048 -355
- package/src/strategies/factory.ts +199 -0
- package/src/strategies/index.ts +5 -3
- package/src/strategies/registry.ts +262 -0
- package/src/strategies/sensei.ts +354 -10
- package/src/strategies/svk-strategy.ts +292 -31
- package/src/strategies/token-boosted-xstrk-carry-strategy.tsx +1261 -0
- package/src/strategies/types.ts +4 -0
- package/src/strategies/universal-adapters/adapter-utils.ts +4 -1
- package/src/strategies/universal-adapters/avnu-adapter.ts +196 -272
- package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
- package/src/strategies/universal-adapters/common-adapter.ts +206 -203
- package/src/strategies/universal-adapters/index.ts +10 -8
- package/src/strategies/universal-adapters/svk-troves-adapter.ts +511 -0
- package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
- package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
- package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +525 -0
- package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +866 -860
- package/src/strategies/universal-adapters/vesu-position-common.ts +258 -0
- package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
- package/src/strategies/universal-lst-muliplier-strategy.tsx +895 -416
- package/src/strategies/universal-strategy.tsx +1332 -1173
- package/src/strategies/vesu-rebalance.tsx +254 -153
- package/src/strategies/yoloVault.ts +1096 -0
- package/src/utils/cacheClass.ts +11 -2
- package/src/utils/health-factor-math.ts +33 -1
- package/src/utils/index.ts +3 -1
- package/src/utils/logger.browser.ts +22 -4
- package/src/utils/logger.node.ts +259 -24
- package/src/utils/starknet-call-parser.ts +1036 -0
- package/src/utils/strategy-utils.ts +61 -0
- package/src/modules/ExtendedWrapperSDk/index.ts +0 -62
- package/src/modules/ExtendedWrapperSDk/types.ts +0 -311
- package/src/modules/ExtendedWrapperSDk/wrapper.ts +0 -395
- package/src/strategies/universal-adapters/extended-adapter.ts +0 -662
- package/src/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
- package/src/strategies/vesu-extended-strategy/services/operationService.ts +0 -34
- package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +0 -77
- package/src/strategies/vesu-extended-strategy/utils/constants.ts +0 -49
- package/src/strategies/vesu-extended-strategy/utils/helper.ts +0 -372
- package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +0 -1140
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { ContractAddr, Web3Number } from "@/dataTypes";
|
|
2
|
+
import { Protocols } from "@/interfaces";
|
|
3
|
+
import {
|
|
4
|
+
BaseAdapter,
|
|
5
|
+
BaseAdapterConfig,
|
|
6
|
+
SupportedPosition,
|
|
7
|
+
PositionInfo,
|
|
8
|
+
PositionAPY,
|
|
9
|
+
APYType,
|
|
10
|
+
ManageCall,
|
|
11
|
+
AdapterLeafType,
|
|
12
|
+
GenerateCallFn,
|
|
13
|
+
DepositParams,
|
|
14
|
+
WithdrawParams,
|
|
15
|
+
PositionAmount,
|
|
16
|
+
} from "./baseAdapter";
|
|
17
|
+
import { SIMPLE_SANITIZER, SVK_SIMPLE_SANITIZER, toBigInt } from "./adapter-utils";
|
|
18
|
+
import { hash, uint256, Contract } from "starknet";
|
|
19
|
+
import { logger } from "@/utils";
|
|
20
|
+
// Troves SVK universal vault ABI: ERC-4626 + SVK views (e.g. due_assets_from_owner).
|
|
21
|
+
import universalVaultAbi from "@/data/universal-vault.abi.json";
|
|
22
|
+
import redeemRequestNftAbi from "@/data/redeem-request-nft.abi.json";
|
|
23
|
+
|
|
24
|
+
/** Public Troves strategies feed (APY + metadata). Override in config for tests. */
|
|
25
|
+
export const DEFAULT_TROVES_STRATEGIES_API = "https://app.troves.fi/api/strategies";
|
|
26
|
+
|
|
27
|
+
export interface SvkTrovesAdapterConfig extends BaseAdapterConfig {
|
|
28
|
+
/**
|
|
29
|
+
* On-chain Troves / SVK strategy vault: ERC-4626-style `deposit` / `withdraw` on the underlying asset,
|
|
30
|
+
* plus `due_assets_from_owner` for redemption NFT value still owed to an owner.
|
|
31
|
+
*/
|
|
32
|
+
strategyVault: ContractAddr;
|
|
33
|
+
/**
|
|
34
|
+
* Troves API `strategies[].id` string (e.g. `hyper_xstrk`). Used to resolve APY from the public JSON feed.
|
|
35
|
+
*/
|
|
36
|
+
trovesStrategyId: string;
|
|
37
|
+
/**
|
|
38
|
+
* Address whose vault **share** balance and **pending** redemption assets are measured.
|
|
39
|
+
* Defaults to `vaultAllocator` when omitted (typical STRKFarm vault wiring).
|
|
40
|
+
*/
|
|
41
|
+
positionOwner?: ContractAddr;
|
|
42
|
+
/** Optional APY endpoint (defaults to {@link DEFAULT_TROVES_STRATEGIES_API}). */
|
|
43
|
+
trovesStrategiesApiUrl?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Optional redeem request NFT contract address. When provided, pending assets are calculated
|
|
46
|
+
* using `getPendingAssetsFromOwnerNFTMethod` instead of `due_assets_from_owner`.
|
|
47
|
+
*/
|
|
48
|
+
redeemRequestNFT?: ContractAddr;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type TrovesStrategiesApiPayload = {
|
|
52
|
+
strategies?: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
apy?: number | string;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function parseTrovesApyField(raw: unknown): number {
|
|
59
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
60
|
+
return raw;
|
|
61
|
+
}
|
|
62
|
+
if (typeof raw === "string") {
|
|
63
|
+
const n = Number.parseFloat(raw);
|
|
64
|
+
if (Number.isFinite(n)) {
|
|
65
|
+
return n;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Universal adapter for **Starknet Vault Kit (SVK)** style Troves strategies:
|
|
73
|
+
* approve underlying → `deposit(assets, receiver)`, and `withdraw(assets, receiver, owner)` on the strategy vault.
|
|
74
|
+
*
|
|
75
|
+
* **Position sizing** (underlying asset, same decimals as `baseToken`):
|
|
76
|
+
* - Liquid: `convert_to_assets(balance_of(positionOwner))` on the strategy vault (vault shares).
|
|
77
|
+
* - Pending redemptions: `due_assets_from_owner(positionOwner)` on the same vault (NFT / queue claims not yet settled).
|
|
78
|
+
*
|
|
79
|
+
* **APY**: Fetched from Troves public API by `trovesStrategyId` (numeric `apy` field; non-numeric marketing values fall back to `0`).
|
|
80
|
+
*/
|
|
81
|
+
export class SvkTrovesAdapter extends BaseAdapter<DepositParams, WithdrawParams> {
|
|
82
|
+
readonly config: SvkTrovesAdapterConfig;
|
|
83
|
+
|
|
84
|
+
constructor(config: SvkTrovesAdapterConfig) {
|
|
85
|
+
super(config, SvkTrovesAdapter.name, Protocols.TROVES);
|
|
86
|
+
this.config = config;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Owner used for share balance + `due_assets_from_owner`. */
|
|
90
|
+
private _positionOwner(): ContractAddr {
|
|
91
|
+
return this.config.positionOwner ?? this.config.vaultAllocator;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Proof readable IDs must stay ≤ 31 chars (Cairo short string). We derive a short ASCII suffix from
|
|
96
|
+
* `strategyVault` address so multiple SVK adapters in one tree stay distinct.
|
|
97
|
+
*/
|
|
98
|
+
private _proofSuffix(): string {
|
|
99
|
+
return this.config.strategyVault.address.replace(/^0x/, "").slice(-6);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private _depositApproveProofReadableId(): string {
|
|
103
|
+
return `appr_dep_svk_${this._proofSuffix()}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private _depositCallProofReadableId(): string {
|
|
107
|
+
return `dep_svk_${this._proofSuffix()}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private _withdrawCallProofReadableId(): string {
|
|
111
|
+
return `wtdrw_svk_${this._proofSuffix()}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected async getAPY(supportedPosition: SupportedPosition): Promise<PositionAPY> {
|
|
115
|
+
const CACHE_KEY = `svk_apy_${this.config.trovesStrategyId}`;
|
|
116
|
+
const cached = this.getCache<PositionAPY>(CACHE_KEY);
|
|
117
|
+
if (cached) {
|
|
118
|
+
return cached;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const url = this.config.trovesStrategiesApiUrl ?? DEFAULT_TROVES_STRATEGIES_API;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(url);
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
logger.warn(`${SvkTrovesAdapter.name}::getAPY: HTTP ${res.status} from ${url}`);
|
|
127
|
+
const fallback = { apy: 0, type: APYType.BASE };
|
|
128
|
+
this.setCache(CACHE_KEY, fallback, 300_000);
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
const body = (await res.json()) as TrovesStrategiesApiPayload;
|
|
132
|
+
const row = body.strategies?.find((s) => s.id === this.config.trovesStrategyId);
|
|
133
|
+
if (!row) {
|
|
134
|
+
logger.warn(
|
|
135
|
+
`${SvkTrovesAdapter.name}::getAPY: strategy id not found: ${this.config.trovesStrategyId}`,
|
|
136
|
+
);
|
|
137
|
+
const fallback = { apy: 0, type: APYType.BASE };
|
|
138
|
+
this.setCache(CACHE_KEY, fallback, 300_000);
|
|
139
|
+
return fallback;
|
|
140
|
+
}
|
|
141
|
+
const apy = parseTrovesApyField(row.apy);
|
|
142
|
+
const result: PositionAPY = { apy, type: APYType.BASE };
|
|
143
|
+
this.setCache(CACHE_KEY, result, 300_000);
|
|
144
|
+
return result;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error(`${SvkTrovesAdapter.name}::getAPY:`, error);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
protected async getPosition(supportedPosition: SupportedPosition): Promise<PositionAmount | null> {
|
|
152
|
+
const CACHE_KEY = `svk_pos_${this.config.strategyVault.address}_${this._positionOwner().address}`;
|
|
153
|
+
const cached = this.getCache<PositionAmount>(CACHE_KEY);
|
|
154
|
+
if (cached) {
|
|
155
|
+
return cached;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const vault = new Contract({
|
|
160
|
+
abi: universalVaultAbi as never,
|
|
161
|
+
address: this.config.strategyVault.address,
|
|
162
|
+
providerOrAccount: this.config.networkConfig.provider,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const owner = this._positionOwner();
|
|
166
|
+
const decimals = supportedPosition.asset.decimals;
|
|
167
|
+
const shares = await vault.balance_of(owner.address);
|
|
168
|
+
const shareU256 = uint256.bnToUint256(shares);
|
|
169
|
+
const liquidAssetsRaw: any = await vault.convert_to_assets(shareU256);
|
|
170
|
+
const liquid = Web3Number.fromWei(liquidAssetsRaw.toString(), decimals);
|
|
171
|
+
|
|
172
|
+
let pending = Web3Number.fromWei("0", decimals);
|
|
173
|
+
if (this.config.redeemRequestNFT) {
|
|
174
|
+
pending = await this.getPendingAssetsFromOwnerNFTMethod(
|
|
175
|
+
this.config.redeemRequestNFT,
|
|
176
|
+
owner,
|
|
177
|
+
decimals,
|
|
178
|
+
);
|
|
179
|
+
} else {
|
|
180
|
+
try {
|
|
181
|
+
const dueRaw: any = await vault.call("due_assets_from_owner", [owner.address]);
|
|
182
|
+
pending = Web3Number.fromWei(dueRaw.toString(), decimals);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
logger.warn(
|
|
185
|
+
`${SvkTrovesAdapter.name}::getPosition: due_assets_from_owner failed (treating as 0): ${(e as Error).message}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const total = liquid.plus(pending);
|
|
191
|
+
const remarks = `Troves ${this.config.trovesStrategyId} holdings`
|
|
192
|
+
|
|
193
|
+
const result: PositionAmount = {
|
|
194
|
+
amount: total,
|
|
195
|
+
remarks,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
this.setCache(CACHE_KEY, result, 60_000);
|
|
199
|
+
return result;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
logger.error(`${SvkTrovesAdapter.name}::getPosition:`, error);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async getPendingAssetsFromOwner(
|
|
207
|
+
owner: ContractAddr = this._positionOwner(),
|
|
208
|
+
decimals: number = this.config.baseToken.decimals,
|
|
209
|
+
): Promise<Web3Number> {
|
|
210
|
+
const vault = new Contract({
|
|
211
|
+
abi: universalVaultAbi as never,
|
|
212
|
+
address: this.config.strategyVault.address,
|
|
213
|
+
providerOrAccount: this.config.networkConfig.provider,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const dueRaw: any = await vault.call("due_assets_from_owner", [owner.address]);
|
|
218
|
+
return Web3Number.fromWei(dueRaw.toString(), decimals);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
logger.warn(
|
|
221
|
+
`${SvkTrovesAdapter.name}::getPendingAssetsFromOwner: due_assets_from_owner failed (treating as 0): ${(e as Error).message}`,
|
|
222
|
+
);
|
|
223
|
+
return Web3Number.fromWei("0", decimals);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get pending assets from owner by scanning redeem request NFTs.
|
|
229
|
+
* This method iterates backwards through NFT IDs to find all pending redemptions for a specific owner.
|
|
230
|
+
*
|
|
231
|
+
* @param redeemRequestNFT - The redeem request NFT contract address
|
|
232
|
+
* @param owner - The owner address to check for pending redemptions (defaults to positionOwner)
|
|
233
|
+
* @param decimals - Token decimals for conversion (defaults to baseToken decimals)
|
|
234
|
+
* @returns Total pending assets from all NFTs owned by the specified address
|
|
235
|
+
*/
|
|
236
|
+
async getPendingAssetsFromOwnerNFTMethod(
|
|
237
|
+
redeemRequestNFT: ContractAddr,
|
|
238
|
+
owner: ContractAddr = this._positionOwner(),
|
|
239
|
+
decimals: number = this.config.baseToken.decimals,
|
|
240
|
+
): Promise<Web3Number> {
|
|
241
|
+
try {
|
|
242
|
+
const nftContract = new Contract({
|
|
243
|
+
abi: redeemRequestNftAbi as never,
|
|
244
|
+
address: redeemRequestNFT.address,
|
|
245
|
+
providerOrAccount: this.config.networkConfig.provider,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Step 1: Get the latest NFT ID (the one that is not yet used)
|
|
249
|
+
const idLenRaw: any = await nftContract.id_len();
|
|
250
|
+
const latestId = BigInt(idLenRaw.toString());
|
|
251
|
+
|
|
252
|
+
if (latestId === 0n) {
|
|
253
|
+
logger.info(
|
|
254
|
+
`${SvkTrovesAdapter.name}::getPendingAssetsFromOwnerNFTMethod: No NFTs minted yet`,
|
|
255
|
+
);
|
|
256
|
+
return Web3Number.fromWei("0", decimals);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Step 2: Start from (latestId - 1) and loop backwards to find all pending NFTs
|
|
260
|
+
const matchingIds: bigint[] = [];
|
|
261
|
+
let currentId = latestId - 1n;
|
|
262
|
+
|
|
263
|
+
while (currentId >= 0n) {
|
|
264
|
+
try {
|
|
265
|
+
// Convert currentId to u256 format
|
|
266
|
+
const idU256 = uint256.bnToUint256(currentId);
|
|
267
|
+
const ownerRaw: any = await nftContract.owner_of(idU256);
|
|
268
|
+
|
|
269
|
+
// Step 3: Filter IDs that have the owner matching our parameter
|
|
270
|
+
// Use ContractAddr.from to normalize addresses before comparison
|
|
271
|
+
const nftOwnerAddr = ContractAddr.from(ownerRaw.toString());
|
|
272
|
+
if (nftOwnerAddr.eq(owner)) {
|
|
273
|
+
matchingIds.push(currentId);
|
|
274
|
+
logger.debug(
|
|
275
|
+
`${SvkTrovesAdapter.name}::getPendingAssetsFromOwnerNFTMethod: Found matching NFT ID ${currentId} for owner ${owner.address}`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
currentId--;
|
|
280
|
+
} catch (e) {
|
|
281
|
+
// When owner_of throws an error, it means we've reached the last pending NFT
|
|
282
|
+
logger.info(
|
|
283
|
+
`${SvkTrovesAdapter.name}::getPendingAssetsFromOwnerNFTMethod: Reached last pending NFT at id ${currentId}`,
|
|
284
|
+
);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (matchingIds.length === 0) {
|
|
290
|
+
logger.info(
|
|
291
|
+
`${SvkTrovesAdapter.name}::getPendingAssetsFromOwnerNFTMethod: No matching NFTs found for owner ${owner.address}`,
|
|
292
|
+
);
|
|
293
|
+
return Web3Number.fromWei("0", decimals);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Step 4: For each matching ID, call id_to_info and sum up all nominal values
|
|
297
|
+
let totalNominal = 0n;
|
|
298
|
+
|
|
299
|
+
for (const nftId of matchingIds) {
|
|
300
|
+
try {
|
|
301
|
+
const idU256 = uint256.bnToUint256(nftId);
|
|
302
|
+
const infoRaw: any = await nftContract.id_to_info(idU256);
|
|
303
|
+
|
|
304
|
+
// The response is a struct with { epoch: u256, nominal: u256 }
|
|
305
|
+
const nominal = BigInt(infoRaw.nominal.toString());
|
|
306
|
+
totalNominal += nominal;
|
|
307
|
+
|
|
308
|
+
logger.debug(
|
|
309
|
+
`${SvkTrovesAdapter.name}::getPendingAssetsFromOwnerNFTMethod: NFT ID ${nftId} has nominal ${nominal}`,
|
|
310
|
+
);
|
|
311
|
+
} catch (e) {
|
|
312
|
+
logger.warn(
|
|
313
|
+
`${SvkTrovesAdapter.name}::getPendingAssetsFromOwnerNFTMethod: Failed to get info for NFT ID ${nftId}: ${(e as Error).message}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
logger.info(
|
|
319
|
+
`${SvkTrovesAdapter.name}::getPendingAssetsFromOwnerNFTMethod: Found ${matchingIds.length} NFTs with total nominal ${totalNominal}`,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
return Web3Number.fromWei(totalNominal.toString(), decimals);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logger.error(
|
|
325
|
+
`${SvkTrovesAdapter.name}::getPendingAssetsFromOwnerNFTMethod: ${(error as Error).message}`,
|
|
326
|
+
);
|
|
327
|
+
return Web3Number.fromWei("0", decimals);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async maxDeposit(amount?: Web3Number): Promise<PositionInfo> {
|
|
332
|
+
const baseToken = this.config.baseToken;
|
|
333
|
+
if (!amount) {
|
|
334
|
+
return {
|
|
335
|
+
tokenInfo: baseToken,
|
|
336
|
+
amount: new Web3Number("999999999999999999999999999", baseToken.decimals),
|
|
337
|
+
usdValue: 999999999999999999999999999,
|
|
338
|
+
remarks: "Max deposit (unbounded placeholder)",
|
|
339
|
+
apy: await this.getAPY({ asset: baseToken, isDebt: false }),
|
|
340
|
+
protocol: this.protocol,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const usdValue = await this.getUSDValue(baseToken, amount);
|
|
344
|
+
return {
|
|
345
|
+
tokenInfo: baseToken,
|
|
346
|
+
amount,
|
|
347
|
+
usdValue,
|
|
348
|
+
remarks: "Deposit amount",
|
|
349
|
+
apy: await this.getAPY({ asset: baseToken, isDebt: false }),
|
|
350
|
+
protocol: this.protocol,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async maxWithdraw(): Promise<PositionInfo> {
|
|
355
|
+
const baseToken = this.config.baseToken;
|
|
356
|
+
const current = await this.getPosition({ asset: baseToken, isDebt: false });
|
|
357
|
+
const pos = current ?? { amount: new Web3Number("0", baseToken.decimals), remarks: "" };
|
|
358
|
+
const usdValue = await this.getUSDValue(baseToken, pos.amount);
|
|
359
|
+
return {
|
|
360
|
+
tokenInfo: baseToken,
|
|
361
|
+
amount: pos.amount,
|
|
362
|
+
usdValue,
|
|
363
|
+
remarks: "Max withdraw (liquid + pending redemption, underlying units)",
|
|
364
|
+
apy: await this.getAPY({ asset: baseToken, isDebt: false }),
|
|
365
|
+
protocol: this.protocol,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
protected _getDepositLeaf(): {
|
|
370
|
+
target: ContractAddr;
|
|
371
|
+
method: string;
|
|
372
|
+
packedArguments: bigint[];
|
|
373
|
+
sanitizer: ContractAddr;
|
|
374
|
+
id: string;
|
|
375
|
+
}[] {
|
|
376
|
+
const baseToken = this.config.baseToken;
|
|
377
|
+
const strategyVault = this.config.strategyVault;
|
|
378
|
+
const receiver = this.config.vaultAllocator;
|
|
379
|
+
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
target: baseToken.address,
|
|
383
|
+
method: "approve",
|
|
384
|
+
packedArguments: [strategyVault.toBigInt()],
|
|
385
|
+
sanitizer: SIMPLE_SANITIZER,
|
|
386
|
+
id: this._depositApproveProofReadableId(),
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
target: strategyVault,
|
|
390
|
+
method: "deposit",
|
|
391
|
+
packedArguments: [receiver.toBigInt()],
|
|
392
|
+
sanitizer: SIMPLE_SANITIZER,
|
|
393
|
+
id: this._depositCallProofReadableId(),
|
|
394
|
+
},
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
protected _getWithdrawLeaf(): {
|
|
399
|
+
target: ContractAddr;
|
|
400
|
+
method: string;
|
|
401
|
+
packedArguments: bigint[];
|
|
402
|
+
sanitizer: ContractAddr;
|
|
403
|
+
id: string;
|
|
404
|
+
}[] {
|
|
405
|
+
const strategyVault = this.config.strategyVault;
|
|
406
|
+
const recv = this.config.vaultAllocator;
|
|
407
|
+
const owner = this.config.vaultAllocator;
|
|
408
|
+
|
|
409
|
+
return [
|
|
410
|
+
{
|
|
411
|
+
target: strategyVault,
|
|
412
|
+
method: "request_redeem",
|
|
413
|
+
packedArguments: [recv.toBigInt(), owner.toBigInt()],
|
|
414
|
+
sanitizer: SVK_SIMPLE_SANITIZER,
|
|
415
|
+
id: this._withdrawCallProofReadableId(),
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
getDepositAdapter(): AdapterLeafType<DepositParams> {
|
|
421
|
+
const leafConfigs = this._getDepositLeaf();
|
|
422
|
+
const leaves = leafConfigs.map((config) => {
|
|
423
|
+
const { target, method, packedArguments, sanitizer, id } = config;
|
|
424
|
+
return this.constructSimpleLeafData({ id, target, method, packedArguments }, sanitizer);
|
|
425
|
+
});
|
|
426
|
+
return { leaves, callConstructor: this.getDepositCall.bind(this) as unknown as GenerateCallFn<DepositParams> };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
getWithdrawAdapter(): AdapterLeafType<WithdrawParams> {
|
|
430
|
+
const leafConfigs = this._getWithdrawLeaf();
|
|
431
|
+
const leaves = leafConfigs.map((config) => {
|
|
432
|
+
const { target, method, packedArguments, sanitizer, id } = config;
|
|
433
|
+
return this.constructSimpleLeafData({ id, target, method, packedArguments }, sanitizer);
|
|
434
|
+
});
|
|
435
|
+
return { leaves, callConstructor: this.getWithdrawCall.bind(this) as unknown as GenerateCallFn<WithdrawParams> };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async getDepositCall(params: DepositParams): Promise<ManageCall[]> {
|
|
439
|
+
const baseToken = this.config.baseToken;
|
|
440
|
+
const strategyVault = this.config.strategyVault;
|
|
441
|
+
const amount = params.amount;
|
|
442
|
+
const uint256Amount = uint256.bnToUint256(amount.toWei());
|
|
443
|
+
const receiver = this.config.vaultAllocator;
|
|
444
|
+
|
|
445
|
+
return [
|
|
446
|
+
{
|
|
447
|
+
proofReadableId: this._depositApproveProofReadableId(),
|
|
448
|
+
sanitizer: SIMPLE_SANITIZER,
|
|
449
|
+
call: {
|
|
450
|
+
contractAddress: baseToken.address,
|
|
451
|
+
selector: hash.getSelectorFromName("approve"),
|
|
452
|
+
calldata: [
|
|
453
|
+
strategyVault.toBigInt(),
|
|
454
|
+
toBigInt(uint256Amount.low.toString()),
|
|
455
|
+
toBigInt(uint256Amount.high.toString()),
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
proofReadableId: this._depositCallProofReadableId(),
|
|
461
|
+
sanitizer: SIMPLE_SANITIZER,
|
|
462
|
+
call: {
|
|
463
|
+
contractAddress: strategyVault,
|
|
464
|
+
selector: hash.getSelectorFromName("deposit"),
|
|
465
|
+
calldata: [
|
|
466
|
+
toBigInt(uint256Amount.low.toString()),
|
|
467
|
+
toBigInt(uint256Amount.high.toString()),
|
|
468
|
+
receiver.toBigInt(),
|
|
469
|
+
],
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async getWithdrawCall(params: WithdrawParams): Promise<ManageCall[]> {
|
|
476
|
+
const strategyVault = this.config.strategyVault;
|
|
477
|
+
const amount = params.amount;
|
|
478
|
+
const uint256Amount = uint256.bnToUint256(amount.toWei());
|
|
479
|
+
const recv = this.config.vaultAllocator;
|
|
480
|
+
const owner = this.config.vaultAllocator;
|
|
481
|
+
|
|
482
|
+
const vault = new Contract({
|
|
483
|
+
abi: universalVaultAbi as never,
|
|
484
|
+
address: strategyVault.address,
|
|
485
|
+
providerOrAccount: this.config.networkConfig.provider,
|
|
486
|
+
});
|
|
487
|
+
const sharesRaw: any = await vault.convert_to_shares(uint256Amount);
|
|
488
|
+
const sharesU256 = uint256.bnToUint256(sharesRaw.toString());
|
|
489
|
+
|
|
490
|
+
return [
|
|
491
|
+
{
|
|
492
|
+
proofReadableId: this._withdrawCallProofReadableId(),
|
|
493
|
+
sanitizer: SVK_SIMPLE_SANITIZER,
|
|
494
|
+
call: {
|
|
495
|
+
contractAddress: strategyVault,
|
|
496
|
+
selector: hash.getSelectorFromName("request_redeem"),
|
|
497
|
+
calldata: [
|
|
498
|
+
toBigInt(sharesU256.low.toString()),
|
|
499
|
+
toBigInt(sharesU256.high.toString()),
|
|
500
|
+
recv.toBigInt(),
|
|
501
|
+
owner.toBigInt(),
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
getHealthFactor(): Promise<number> {
|
|
509
|
+
return Promise.resolve(10);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { ContractAddr, Web3Number } from "@/dataTypes";
|
|
2
|
+
import {
|
|
3
|
+
APYType,
|
|
4
|
+
BaseAdapter,
|
|
5
|
+
BaseAdapterConfig,
|
|
6
|
+
DepositParams,
|
|
7
|
+
ManageCall,
|
|
8
|
+
PositionAmount,
|
|
9
|
+
PositionAPY,
|
|
10
|
+
PositionInfo,
|
|
11
|
+
SupportedPosition,
|
|
12
|
+
WithdrawParams,
|
|
13
|
+
} from "./baseAdapter";
|
|
14
|
+
import { hash, uint256 } from "starknet";
|
|
15
|
+
import { ERC20, TokenMarketData } from "@/modules";
|
|
16
|
+
import { Protocols } from "@/interfaces";
|
|
17
|
+
import { SIMPLE_SANITIZER, toBigInt, TRANSFER_SANITIZER } from "./adapter-utils";
|
|
18
|
+
import { logger } from "@/utils";
|
|
19
|
+
|
|
20
|
+
export interface TokenTransferAdapterConfig extends BaseAdapterConfig {
|
|
21
|
+
/** Address that funds are sent FROM during deposit (and returned TO during withdraw) */
|
|
22
|
+
fromAddress: ContractAddr;
|
|
23
|
+
/** Address that funds are sent TO during deposit (and pulled FROM during withdraw) */
|
|
24
|
+
toAddress: ContractAddr;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Adapter for transferring a single token between two fixed addresses.
|
|
29
|
+
*
|
|
30
|
+
* Deposit: transfers baseToken from `fromAddress` → `toAddress`
|
|
31
|
+
* Withdraw: transfers baseToken from `toAddress` → `fromAddress` (requires toAddress to approve fromAddress/VA)
|
|
32
|
+
*
|
|
33
|
+
* Proof IDs are derived from `tr_<symbol>_<toAddrShort>` to be unique per token+destination pair.
|
|
34
|
+
*/
|
|
35
|
+
export class TokenTransferAdapter extends BaseAdapter<DepositParams, WithdrawParams> {
|
|
36
|
+
readonly config: TokenTransferAdapterConfig;
|
|
37
|
+
readonly tokenMarketData: TokenMarketData;
|
|
38
|
+
|
|
39
|
+
constructor(config: TokenTransferAdapterConfig) {
|
|
40
|
+
super(config, TokenTransferAdapter.name, Protocols.NONE);
|
|
41
|
+
this.config = config;
|
|
42
|
+
this.tokenMarketData = new TokenMarketData(this.config.pricer, this.config.networkConfig);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private _idBase(): string {
|
|
46
|
+
return `tr_${this.config.baseToken.symbol}_${this.config.toAddress.shortString()}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private _depositCallProofReadableId(): string {
|
|
50
|
+
return `${this._idBase()}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private _withdrawCallProofReadableId(): string {
|
|
54
|
+
return `tr_wd_${this.config.baseToken.symbol}_${this.config.toAddress.shortString()}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected async getAPY(_supportedPosition: SupportedPosition): Promise<PositionAPY> {
|
|
58
|
+
const isSupported = this.tokenMarketData.isAPYSupported(this.config.baseToken);
|
|
59
|
+
const apy = isSupported ? await this.tokenMarketData.getAPY(this.config.baseToken) : 0;
|
|
60
|
+
return { apy, type: isSupported ? APYType.LST : APYType.BASE };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected async getPosition(supportedPosition: SupportedPosition): Promise<PositionAmount> {
|
|
64
|
+
// only measure balance of toAddress, bcz fromAddress usually gets trakced in unused balance or a previous token transfer adapter
|
|
65
|
+
try {
|
|
66
|
+
const balance = await new ERC20(this.config.networkConfig).balanceOf(
|
|
67
|
+
supportedPosition.asset.address,
|
|
68
|
+
this.config.toAddress.address,
|
|
69
|
+
supportedPosition.asset.decimals,
|
|
70
|
+
);
|
|
71
|
+
return { amount: balance, remarks: `Unused balance [${this.config.toAddress.shortString()}]` };
|
|
72
|
+
} catch (_e) {
|
|
73
|
+
logger.error(`${TokenTransferAdapter.name}::getPosition: failed for ${supportedPosition.asset.symbol}`);
|
|
74
|
+
throw new Error(`${TokenTransferAdapter.name}: failed to get balance for ${supportedPosition.asset.symbol}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async maxDeposit(amount?: Web3Number): Promise<PositionInfo> {
|
|
79
|
+
const baseToken = this.config.baseToken;
|
|
80
|
+
const fromBalance = await new ERC20(this.config.networkConfig).balanceOf(
|
|
81
|
+
baseToken.address,
|
|
82
|
+
this.config.fromAddress.address,
|
|
83
|
+
baseToken.decimals,
|
|
84
|
+
);
|
|
85
|
+
const depositable = amount && amount.toNumber() <= fromBalance.toNumber() ? amount : fromBalance;
|
|
86
|
+
const usdValue = await this.getUSDValue(baseToken, depositable);
|
|
87
|
+
return {
|
|
88
|
+
tokenInfo: baseToken,
|
|
89
|
+
amount: depositable,
|
|
90
|
+
usdValue,
|
|
91
|
+
remarks: "Max deposit (from-address balance)",
|
|
92
|
+
apy: await this.getAPY({ asset: baseToken, isDebt: false }),
|
|
93
|
+
protocol: this.protocol,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async maxWithdraw(): Promise<PositionInfo> {
|
|
98
|
+
const baseToken = this.config.baseToken;
|
|
99
|
+
const toBalance = await new ERC20(this.config.networkConfig).balanceOf(
|
|
100
|
+
baseToken.address,
|
|
101
|
+
this.config.toAddress.address,
|
|
102
|
+
baseToken.decimals,
|
|
103
|
+
);
|
|
104
|
+
const usdValue = await this.getUSDValue(baseToken, toBalance);
|
|
105
|
+
return {
|
|
106
|
+
tokenInfo: baseToken,
|
|
107
|
+
amount: toBalance,
|
|
108
|
+
usdValue,
|
|
109
|
+
remarks: "Max withdraw (to-address balance)",
|
|
110
|
+
apy: await this.getAPY({ asset: baseToken, isDebt: false }),
|
|
111
|
+
protocol: this.protocol,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
protected _getDepositLeaf(): {
|
|
116
|
+
target: ContractAddr;
|
|
117
|
+
method: string;
|
|
118
|
+
packedArguments: bigint[];
|
|
119
|
+
sanitizer: ContractAddr;
|
|
120
|
+
id: string;
|
|
121
|
+
}[] {
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
target: this.config.baseToken.address,
|
|
125
|
+
method: "transfer",
|
|
126
|
+
packedArguments: [this.config.toAddress.toBigInt()],
|
|
127
|
+
sanitizer: TRANSFER_SANITIZER,
|
|
128
|
+
id: this._depositCallProofReadableId(),
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
protected _getWithdrawLeaf(): {
|
|
134
|
+
target: ContractAddr;
|
|
135
|
+
method: string;
|
|
136
|
+
packedArguments: bigint[];
|
|
137
|
+
sanitizer: ContractAddr;
|
|
138
|
+
id: string;
|
|
139
|
+
}[] {
|
|
140
|
+
// note, to address should have given allowance to from address
|
|
141
|
+
return [
|
|
142
|
+
{
|
|
143
|
+
target: this.config.baseToken.address,
|
|
144
|
+
method: "transfer_from",
|
|
145
|
+
packedArguments: [
|
|
146
|
+
this.config.toAddress.toBigInt(), // from
|
|
147
|
+
this.config.fromAddress.toBigInt(), // to
|
|
148
|
+
],
|
|
149
|
+
sanitizer: TRANSFER_SANITIZER,
|
|
150
|
+
id: this._withdrawCallProofReadableId(),
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getDepositCall(params: DepositParams): Promise<ManageCall[]> {
|
|
156
|
+
const amount = params.amount;
|
|
157
|
+
const uint256Amount = uint256.bnToUint256(amount.toWei());
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
proofReadableId: this._depositCallProofReadableId(),
|
|
161
|
+
sanitizer: TRANSFER_SANITIZER,
|
|
162
|
+
call: {
|
|
163
|
+
contractAddress: this.config.baseToken.address,
|
|
164
|
+
selector: hash.getSelectorFromName("transfer"),
|
|
165
|
+
calldata: [
|
|
166
|
+
this.config.toAddress.toBigInt(),
|
|
167
|
+
toBigInt(uint256Amount.low.toString()),
|
|
168
|
+
toBigInt(uint256Amount.high.toString()),
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async getWithdrawCall(params: WithdrawParams): Promise<ManageCall[]> {
|
|
176
|
+
const amount = params.amount;
|
|
177
|
+
const uint256Amount = uint256.bnToUint256(amount.toWei());
|
|
178
|
+
// note, from address should have given allowance to to address
|
|
179
|
+
return [
|
|
180
|
+
{
|
|
181
|
+
proofReadableId: this._withdrawCallProofReadableId(),
|
|
182
|
+
sanitizer: TRANSFER_SANITIZER,
|
|
183
|
+
call: {
|
|
184
|
+
contractAddress: this.config.baseToken.address,
|
|
185
|
+
selector: hash.getSelectorFromName("transfer_from"),
|
|
186
|
+
calldata: [
|
|
187
|
+
this.config.toAddress.toBigInt(), // from
|
|
188
|
+
this.config.fromAddress.toBigInt(), // to
|
|
189
|
+
toBigInt(uint256Amount.low.toString()),
|
|
190
|
+
toBigInt(uint256Amount.high.toString()),
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async getHealthFactor(): Promise<number> {
|
|
198
|
+
return Promise.resolve(10);
|
|
199
|
+
}
|
|
200
|
+
}
|