@strkfarm/sdk 2.0.0-dev.3 → 2.0.0-dev.31
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 +78475 -45620
- package/dist/index.browser.mjs +19580 -9901
- package/dist/index.d.ts +3763 -1424
- package/dist/index.js +20977 -11063
- package/dist/index.mjs +20945 -11087
- package/package.json +1 -1
- package/src/data/avnu.abi.json +840 -0
- package/src/data/ekubo-price-fethcer.abi.json +265 -0
- package/src/dataTypes/_bignumber.ts +13 -4
- package/src/dataTypes/bignumber.browser.ts +6 -1
- package/src/dataTypes/bignumber.node.ts +5 -1
- package/src/dataTypes/index.ts +3 -2
- package/src/dataTypes/mynumber.ts +141 -0
- package/src/global.ts +76 -41
- package/src/index.browser.ts +2 -1
- package/src/interfaces/common.tsx +175 -3
- package/src/modules/ExtendedWrapperSDk/types.ts +28 -5
- package/src/modules/ExtendedWrapperSDk/wrapper.ts +275 -59
- package/src/modules/apollo-client-config.ts +28 -0
- package/src/modules/avnu.ts +4 -4
- package/src/modules/ekubo-pricer.ts +79 -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/pragma.ts +23 -8
- package/src/modules/pricer-from-api.ts +156 -15
- package/src/modules/pricer-lst.ts +1 -1
- package/src/modules/pricer.ts +40 -4
- package/src/modules/pricerBase.ts +2 -1
- package/src/node/deployer.ts +36 -1
- package/src/node/pricer-redis.ts +2 -1
- package/src/strategies/base-strategy.ts +78 -10
- package/src/strategies/ekubo-cl-vault.tsx +906 -347
- package/src/strategies/factory.ts +159 -0
- package/src/strategies/index.ts +7 -1
- package/src/strategies/registry.ts +239 -0
- package/src/strategies/sensei.ts +335 -7
- package/src/strategies/svk-strategy.ts +97 -27
- package/src/strategies/types.ts +4 -0
- package/src/strategies/universal-adapters/adapter-utils.ts +2 -1
- package/src/strategies/universal-adapters/avnu-adapter.ts +180 -265
- package/src/strategies/universal-adapters/baseAdapter.ts +263 -251
- package/src/strategies/universal-adapters/common-adapter.ts +206 -203
- package/src/strategies/universal-adapters/extended-adapter.ts +490 -316
- package/src/strategies/universal-adapters/index.ts +11 -8
- package/src/strategies/universal-adapters/svk-troves-adapter.ts +364 -0
- package/src/strategies/universal-adapters/token-transfer-adapter.ts +200 -0
- package/src/strategies/universal-adapters/usdc<>usdce-adapter.ts +200 -0
- package/src/strategies/universal-adapters/vesu-adapter.ts +120 -82
- package/src/strategies/universal-adapters/vesu-modify-position-adapter.ts +476 -0
- package/src/strategies/universal-adapters/vesu-multiply-adapter.ts +1067 -704
- package/src/strategies/universal-adapters/vesu-position-common.ts +251 -0
- package/src/strategies/universal-adapters/vesu-supply-only-adapter.ts +18 -3
- package/src/strategies/universal-lst-muliplier-strategy.tsx +397 -204
- package/src/strategies/universal-strategy.tsx +1426 -1173
- package/src/strategies/vesu-extended-strategy/services/executionService.ts +2233 -0
- package/src/strategies/vesu-extended-strategy/services/extended-vesu-state-manager.ts +4087 -0
- package/src/strategies/vesu-extended-strategy/services/ltv-imbalance-rebalance-math.ts +783 -0
- package/src/strategies/vesu-extended-strategy/services/operationService.ts +38 -16
- package/src/strategies/vesu-extended-strategy/types/transaction-metadata.ts +88 -0
- package/src/strategies/vesu-extended-strategy/utils/config.runtime.ts +1 -0
- package/src/strategies/vesu-extended-strategy/utils/constants.ts +5 -6
- package/src/strategies/vesu-extended-strategy/utils/helper.ts +259 -103
- package/src/strategies/vesu-extended-strategy/vesu-extended-strategy.tsx +688 -817
- package/src/strategies/vesu-rebalance.tsx +255 -152
- package/src/utils/cacheClass.ts +11 -2
- package/src/utils/health-factor-math.ts +4 -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/strategies/universal-adapters/unused-balance-adapter.ts +0 -109
|
@@ -41,36 +41,57 @@ export class EkuboQuoter {
|
|
|
41
41
|
this.tokenMarketData = new TokenMarketData(pricer, config);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
* @param fromToken
|
|
47
|
-
* @param toToken
|
|
48
|
-
* @param amount Can be negative too, which would mean to get exact amount out
|
|
49
|
-
* @returns
|
|
50
|
-
*/
|
|
51
|
-
async getQuote(fromToken: string, toToken: string, amount: Web3Number, retry = 0): Promise<EkuboQuote> {
|
|
52
|
-
// let _fromToken = amount.gt(0) ? fromToken : toToken;
|
|
53
|
-
// let _toToken = amount.gt(0) ? toToken : fromToken;
|
|
54
|
-
|
|
44
|
+
private async _callQuoterApi(fromToken: string, toToken: string, amount: Web3Number, retry = 0): Promise<EkuboQuote> {
|
|
55
45
|
try {
|
|
56
|
-
const url = this.ENDPOINT.replace("{{AMOUNT}}", amount.
|
|
57
|
-
|
|
46
|
+
const url = this.ENDPOINT.replace("{{AMOUNT}}", amount.toWei()).replace("{{TOKEN_FROM_ADDRESS}}", fromToken).replace("{{TOKEN_TO_ADDRESS}}", toToken);
|
|
47
|
+
logger.verbose(`EkuboQuoter::_callQuoterApi url: ${url}`);
|
|
58
48
|
const quote = await axios.get(url);
|
|
59
|
-
|
|
49
|
+
// console.log('quote', quote.data);
|
|
50
|
+
// console.log('')
|
|
51
|
+
return quote.data as EkuboQuote;
|
|
60
52
|
} catch (error: any) {
|
|
61
|
-
logger.error(
|
|
53
|
+
logger.error(`EkuboQuoter::_callQuoterApi error: ${error.message}`);
|
|
62
54
|
if (retry < 3) {
|
|
63
55
|
await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 5000));
|
|
64
|
-
return await this.
|
|
56
|
+
return await this._callQuoterApi(fromToken, toToken, amount, retry + 1);
|
|
65
57
|
}
|
|
66
58
|
throw error;
|
|
67
59
|
}
|
|
68
60
|
}
|
|
69
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Given exactly `inputAmount` of `fromToken`, how much `toToken` do I receive?
|
|
64
|
+
* @param fromToken - address of the token being sold
|
|
65
|
+
* @param toToken - address of the token being bought
|
|
66
|
+
* @param inputAmount - must be positive (the amount of fromToken to sell)
|
|
67
|
+
* @returns EkuboQuote where `total_calculated` is the output amount (positive)
|
|
68
|
+
*/
|
|
69
|
+
async getQuoteExactInput(fromToken: string, toToken: string, inputAmount: Web3Number): Promise<EkuboQuote> {
|
|
70
|
+
if (inputAmount.isNegative() || inputAmount.isZero()) {
|
|
71
|
+
throw new Error(`EkuboQuoter::getQuoteExactInput inputAmount must be positive, got ${inputAmount.toFixed()}`);
|
|
72
|
+
}
|
|
73
|
+
return this._callQuoterApi(fromToken, toToken, inputAmount);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* To receive exactly `outputAmount` of `toToken`, how much `fromToken` must I provide?
|
|
78
|
+
* @param fromToken - address of the token being sold
|
|
79
|
+
* @param toToken - address of the token being bought
|
|
80
|
+
* @param outputAmount - must be positive (the desired amount of toToken to receive)
|
|
81
|
+
* @returns EkuboQuote where `total_calculated` is the required input amount (negative per Ekubo convention)
|
|
82
|
+
*/
|
|
83
|
+
async getQuoteExactOutput(fromToken: string, toToken: string, outputAmount: Web3Number): Promise<EkuboQuote> {
|
|
84
|
+
if (outputAmount.isNegative() || outputAmount.isZero()) {
|
|
85
|
+
throw new Error(`EkuboQuoter::getQuoteExactOutput outputAmount must be positive, got ${outputAmount.toFixed()}`);
|
|
86
|
+
}
|
|
87
|
+
const negatedAmount = new Web3Number(outputAmount.multipliedBy(-1).toFixed(Math.min(outputAmount.decimals, 15)), outputAmount.decimals);
|
|
88
|
+
return this._callQuoterApi(toToken, fromToken, negatedAmount);
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
async getDexPrice(baseToken: TokenInfo, quoteToken: TokenInfo, amount: Web3Number) {
|
|
71
92
|
const lstTokenInfo = baseToken;
|
|
72
93
|
const lstUnderlyingTokenInfo = quoteToken;
|
|
73
|
-
const quote = await this.
|
|
94
|
+
const quote = await this.getQuoteExactInput(
|
|
74
95
|
lstTokenInfo.address.address,
|
|
75
96
|
lstUnderlyingTokenInfo.address.address,
|
|
76
97
|
amount
|
|
@@ -95,26 +116,23 @@ export class EkuboQuoter {
|
|
|
95
116
|
logger.verbose(`${EkuboQuoter.name}:: LST true Exchange Rate: ${exchangeRate}`);
|
|
96
117
|
return exchangeRate;
|
|
97
118
|
}
|
|
98
|
-
// debt collateral
|
|
119
|
+
// debt collateral
|
|
99
120
|
async getSwapLimitAmount(fromToken: TokenInfo, toToken: TokenInfo, amount: Web3Number, max_slippage: number = 0.002): Promise<Web3Number> {
|
|
100
121
|
const isExactAmountIn = amount.greaterThanOrEqualTo(0);
|
|
101
122
|
logger.verbose(`${EkuboQuoter.name}::getSwapLimitAmount isExactAmountIn: ${isExactAmountIn}, fromToken: ${fromToken.symbol}, toToken: ${toToken.symbol}, amount: ${amount}`);
|
|
102
123
|
const isYieldToken = this.tokenMarketData.isAPYSupported(toToken);
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
|
|
105
125
|
// if LST, get true exchange rate else use dex price
|
|
106
126
|
// wbtc
|
|
107
|
-
const baseToken = isExactAmountIn ? toToken : fromToken; // fromToken -> wbtc,
|
|
108
|
-
const quoteToken = isExactAmountIn ? fromToken : toToken; // toToken -> usdc,
|
|
127
|
+
const baseToken = isExactAmountIn ? toToken : fromToken; // fromToken -> wbtc,
|
|
128
|
+
const quoteToken = isExactAmountIn ? fromToken : toToken; // toToken -> usdc,
|
|
109
129
|
// need dex price of from token in toToken
|
|
110
130
|
// from baseToken to underlying token
|
|
111
131
|
// for withdraw, usdc to btc with amount negative
|
|
112
132
|
const dexPrice = await this.getDexPrice(baseToken, quoteToken, amount);
|
|
113
133
|
const trueExchangeRate = isYieldToken ? await this.tokenMarketData.getTruePrice(baseToken) : dexPrice;
|
|
114
|
-
console.log("trueExchangeRate", trueExchangeRate);
|
|
115
134
|
if (isExactAmountIn) {
|
|
116
135
|
let minLSTReceived = amount.dividedBy(dexPrice).multipliedBy(1 - max_slippage); // used for increase
|
|
117
|
-
console.log("minLSTReceived", minLSTReceived);
|
|
118
136
|
const minLSTReceivedAsPerTruePrice = amount.dividedBy(trueExchangeRate); // execution output to be <= True LST price
|
|
119
137
|
if (minLSTReceived < minLSTReceivedAsPerTruePrice) {
|
|
120
138
|
minLSTReceived = minLSTReceivedAsPerTruePrice; // the execution shouldn't be bad than True price logi
|
|
@@ -122,26 +140,26 @@ export class EkuboQuoter {
|
|
|
122
140
|
logger.verbose(`${EkuboQuoter.name}::getModifyLeverCall minLSTReceivedAsPerTruePrice: ${minLSTReceivedAsPerTruePrice}, minLSTReceived: ${minLSTReceived}`);
|
|
123
141
|
return minLSTReceived;
|
|
124
142
|
}
|
|
125
|
-
|
|
143
|
+
|
|
126
144
|
let maxUsedCollateral = amount.abs().dividedBy(dexPrice).multipliedBy(1 + max_slippage); // +ve for exact amount out, used for decrease
|
|
127
145
|
const maxUsedCollateralInLST = amount.abs().dividedBy(trueExchangeRate).multipliedBy(1.005); // 0.5% slippage, worst case based on true price
|
|
128
146
|
logger.verbose(`${EkuboQuoter.name}::getModifyLeverCall maxUsedCollateralInLST: ${maxUsedCollateralInLST}, maxUsedCollateral: ${maxUsedCollateral}`);
|
|
129
147
|
if (maxUsedCollateralInLST > maxUsedCollateral) {
|
|
130
148
|
maxUsedCollateral = maxUsedCollateralInLST;
|
|
131
149
|
}
|
|
132
|
-
|
|
150
|
+
|
|
133
151
|
return maxUsedCollateral;
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
/**
|
|
137
155
|
* Formats Ekubo response for Vesu multiple use
|
|
138
|
-
* @param quote
|
|
139
|
-
* @param fromTokenInfo
|
|
140
|
-
* @returns
|
|
156
|
+
* @param quote
|
|
157
|
+
* @param fromTokenInfo
|
|
158
|
+
* @returns
|
|
141
159
|
*/
|
|
142
160
|
getVesuMultiplyQuote(quote: EkuboQuote, fromTokenInfo: TokenInfo, toTokenInfo: TokenInfo): Swap[] {
|
|
143
161
|
return quote.splits.map(split => {
|
|
144
|
-
|
|
162
|
+
|
|
145
163
|
const isNegativeAmount = BigInt(split.amount_specified) <= 0n;
|
|
146
164
|
const token = isNegativeAmount ? toTokenInfo : fromTokenInfo;
|
|
147
165
|
return {
|
package/src/modules/erc20.ts
CHANGED
|
@@ -58,6 +58,23 @@ export class ERC20 {
|
|
|
58
58
|
return transferCall;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
transferFrom(
|
|
62
|
+
token: string | ContractAddr,
|
|
63
|
+
from: string | ContractAddr,
|
|
64
|
+
to: string | ContractAddr,
|
|
65
|
+
amount: Web3Number
|
|
66
|
+
) {
|
|
67
|
+
const contract = this.contract(token);
|
|
68
|
+
const amountUint256 = uint256.bnToUint256(amount.toWei());
|
|
69
|
+
const transferFromCall = contract.populate("transferFrom", [
|
|
70
|
+
from.toString(),
|
|
71
|
+
to.toString(),
|
|
72
|
+
amountUint256.low.toString(),
|
|
73
|
+
amountUint256.high.toString(),
|
|
74
|
+
]);
|
|
75
|
+
return transferFromCall;
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
approve(
|
|
62
79
|
token: string | ContractAddr,
|
|
63
80
|
spender: string | ContractAddr,
|
package/src/modules/harvests.ts
CHANGED
|
@@ -29,28 +29,36 @@ export class Harvests {
|
|
|
29
29
|
|
|
30
30
|
async getUnHarvestedRewards(addr: ContractAddr) {
|
|
31
31
|
const rewards = await this.getHarvests(addr);
|
|
32
|
+
logger.verbose(`${Harvests.name}: getUnHarvestedRewards => rewards length: ${rewards.length}`);
|
|
32
33
|
if (rewards.length == 0) return [];
|
|
33
34
|
|
|
34
35
|
const unClaimed: HarvestInfo[] = [];
|
|
35
36
|
|
|
36
37
|
// use the latest one
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const isClaimed = await contract.call('is_claimed', [reward.claim.id]);
|
|
42
|
-
logger.verbose(`${Harvests.name}: isClaimed: ${isClaimed}`);
|
|
43
|
-
if (isClaimed) {
|
|
44
|
-
return unClaimed;
|
|
45
|
-
}
|
|
46
|
-
// rewards contract must have enough balance to claim
|
|
47
|
-
const bal = await (new ERC20(this.config)).balanceOf(reward.token, reward.rewardsContract.address, 18);
|
|
48
|
-
if (bal.lessThan(reward.claim.amount)) {
|
|
49
|
-
logger.verbose(`${Harvests.name}: balance: ${bal.toString()}, amount: ${reward.claim.amount.toString()}`);
|
|
50
|
-
return unClaimed;
|
|
38
|
+
const sortedRewards = rewards.sort((a, b) => b.endDate.getTime() - a.endDate.getTime());
|
|
39
|
+
if (sortedRewards.length == 0) {
|
|
40
|
+
logger.verbose(`${Harvests.name}: no rewards found`);
|
|
41
|
+
return [];
|
|
51
42
|
}
|
|
52
43
|
|
|
53
|
-
|
|
44
|
+
const cls = await this.config.provider.getClassAt(sortedRewards[0].rewardsContract.address);
|
|
45
|
+
|
|
46
|
+
for (const reward of sortedRewards) {
|
|
47
|
+
const contract = new Contract({abi: cls.abi, address: reward.rewardsContract.address, providerOrAccount: this.config.provider});
|
|
48
|
+
const isClaimed = await contract.call('is_claimed', [reward.claim.id]);
|
|
49
|
+
logger.verbose(`${Harvests.name}: isClaimed: ${isClaimed}, claim id: ${reward.claim.id}, address: ${reward.rewardsContract.address}`);
|
|
50
|
+
if (isClaimed) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// rewards contract must have enough balance to claim
|
|
54
|
+
const bal = await (new ERC20(this.config)).balanceOf(reward.token, reward.rewardsContract.address, 18);
|
|
55
|
+
if (bal.lessThan(reward.claim.amount)) {
|
|
56
|
+
logger.verbose(`${Harvests.name}: balance: ${bal.toString()}, amount: ${reward.claim.amount.toString()}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
unClaimed.push(reward); // to ensure older harvest is first
|
|
61
|
+
}
|
|
54
62
|
return unClaimed;
|
|
55
63
|
}
|
|
56
64
|
}
|
|
@@ -59,26 +67,28 @@ const STRK = '0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d'
|
|
|
59
67
|
|
|
60
68
|
export class EkuboHarvests extends Harvests {
|
|
61
69
|
async getHarvests(addr: ContractAddr) {
|
|
62
|
-
|
|
70
|
+
logger.verbose(`${EkuboHarvests.name}: getHarvests => addr: ${addr.address}`);
|
|
71
|
+
const EKUBO_API = `https://prod-api.ekubo.org/claims/${addr.address}`
|
|
63
72
|
const resultEkubo = await fetch(EKUBO_API);
|
|
64
|
-
const
|
|
65
|
-
|
|
73
|
+
const data = (await resultEkubo.json());
|
|
74
|
+
const claims = data.claims || [];
|
|
75
|
+
logger.verbose(`${EkuboHarvests.name}: getHarvests => claims length: ${claims.length}`);
|
|
66
76
|
const rewards: HarvestInfo[] = [];
|
|
67
|
-
for (let i=0; i<
|
|
68
|
-
const
|
|
69
|
-
assert(
|
|
77
|
+
for (let i=0; i<claims.length; ++i) {
|
|
78
|
+
const claimData = claims[i];
|
|
79
|
+
assert(claimData.key.token == STRK, 'expected strk token only')
|
|
70
80
|
rewards.push({
|
|
71
|
-
rewardsContract: ContractAddr.from(
|
|
81
|
+
rewardsContract: ContractAddr.from(claimData.dropAddress),
|
|
72
82
|
token: ContractAddr.from(STRK),
|
|
73
|
-
startDate: new Date(
|
|
74
|
-
endDate: new Date(
|
|
83
|
+
startDate: new Date(0),
|
|
84
|
+
endDate: new Date(0),
|
|
75
85
|
claim: {
|
|
76
|
-
id:
|
|
77
|
-
amount: Web3Number.fromWei(
|
|
78
|
-
claimee: ContractAddr.from(
|
|
86
|
+
id: claimData.claim.index,
|
|
87
|
+
amount: Web3Number.fromWei(claimData.claim.amount, 18),
|
|
88
|
+
claimee: ContractAddr.from(claimData.claim.account)
|
|
79
89
|
},
|
|
80
|
-
actualReward: Web3Number.fromWei(
|
|
81
|
-
proof:
|
|
90
|
+
actualReward: Web3Number.fromWei(claimData.claim.amount, 18),
|
|
91
|
+
proof: claimData.proof
|
|
82
92
|
});
|
|
83
93
|
}
|
|
84
94
|
return rewards.sort((a, b) => b.endDate.getTime() - a.endDate.getTime());
|
|
@@ -101,6 +111,10 @@ export class VesuHarvests extends Harvests {
|
|
|
101
111
|
logger.verbose(`${VesuHarvests.name}: claimed_amount: ${claimed_amount.toString()}`);
|
|
102
112
|
|
|
103
113
|
const data = _data.data['defiSpring'];
|
|
114
|
+
if (!data) {
|
|
115
|
+
logger.verbose(`${VesuHarvests.name}: no defiSpring data found`);
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
104
118
|
|
|
105
119
|
// get the actual reward
|
|
106
120
|
const actualReward = Web3Number.fromWei(data.amount, 18).minus(claimed_amount);
|
package/src/modules/pragma.ts
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
|
-
import { Contract, RpcProvider } from "starknet";
|
|
1
|
+
import { Contract, RpcProvider, BlockIdentifier } from "starknet";
|
|
2
2
|
import PragmaAbi from '@/data/pragma.abi.json';
|
|
3
3
|
import { logger } from "@/utils/logger";
|
|
4
|
+
import { PricerBase } from "./pricerBase";
|
|
5
|
+
import { IConfig, TokenInfo } from "@/interfaces";
|
|
6
|
+
import { PriceInfo } from "./pricer";
|
|
4
7
|
|
|
5
|
-
export class Pragma {
|
|
8
|
+
export class Pragma extends PricerBase {
|
|
6
9
|
contractAddr = '0x023fb3afbff2c0e3399f896dcf7400acf1a161941cfb386e34a123f228c62832';
|
|
7
10
|
readonly contract: Contract;
|
|
8
11
|
|
|
9
|
-
constructor(
|
|
10
|
-
|
|
12
|
+
constructor(config: IConfig, tokens: TokenInfo[]) {
|
|
13
|
+
super(config, tokens);
|
|
14
|
+
this.contract = new Contract({
|
|
15
|
+
abi: PragmaAbi,
|
|
16
|
+
address: this.contractAddr,
|
|
17
|
+
providerOrAccount: config.provider as RpcProvider
|
|
18
|
+
});
|
|
11
19
|
}
|
|
12
20
|
|
|
13
|
-
async getPrice(tokenAddr: string) {
|
|
21
|
+
async getPrice(tokenAddr: string, blockIdentifier: BlockIdentifier = 'latest'): Promise<PriceInfo> {
|
|
14
22
|
if (!tokenAddr) {
|
|
15
23
|
throw new Error(`Pragma:getPrice - no token`)
|
|
16
24
|
}
|
|
17
|
-
const result: any = await this.contract.call(
|
|
25
|
+
const result: any = await this.contract.call(
|
|
26
|
+
'get_price',
|
|
27
|
+
[tokenAddr],
|
|
28
|
+
{ blockIdentifier }
|
|
29
|
+
);
|
|
18
30
|
const price = Number(result.price) / 10**8;
|
|
19
|
-
logger.verbose(`Pragma:${tokenAddr}: ${price}`);
|
|
20
|
-
return
|
|
31
|
+
logger.verbose(`Pragma:${tokenAddr}: ${price} at block ${blockIdentifier}`);
|
|
32
|
+
return {
|
|
33
|
+
price,
|
|
34
|
+
timestamp: new Date()
|
|
35
|
+
};
|
|
21
36
|
}
|
|
22
37
|
}
|
|
@@ -1,21 +1,64 @@
|
|
|
1
|
-
|
|
1
|
+
import { PriceInfo } from "./pricer";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { IConfig, TokenInfo } from "@/interfaces";
|
|
4
4
|
import { PricerBase } from "./pricerBase";
|
|
5
5
|
import { logger } from "@/utils/logger";
|
|
6
|
+
import { createApolloClient } from "./apollo-client-config";
|
|
7
|
+
import { gql } from "@apollo/client";
|
|
8
|
+
import { ContractAddr } from "@/dataTypes";
|
|
9
|
+
import { Pragma } from "./pragma";
|
|
10
|
+
import { EkuboPricer } from "./ekubo-pricer";
|
|
11
|
+
import { BlockIdentifier } from "starknet";
|
|
6
12
|
|
|
7
13
|
export class PricerFromApi extends PricerBase {
|
|
14
|
+
private apolloClient: ReturnType<typeof createApolloClient>;
|
|
15
|
+
private pragma: Pragma;
|
|
16
|
+
private ekuboPricer: EkuboPricer;
|
|
17
|
+
|
|
18
|
+
// Tokens supported by Pragma and GraphQL for historical prices (standardized addresses)
|
|
19
|
+
// Note: xSTRK is not supported by Pragma RPC, so it will use Ekubo pricer instead
|
|
20
|
+
private readonly PRAGMA_SUPPORTED_TOKENS = [
|
|
21
|
+
ContractAddr.from('0x3fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac').address, // WBTC
|
|
22
|
+
ContractAddr.from('0x53c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8').address, // USDC
|
|
23
|
+
ContractAddr.from('0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d').address, // STRK
|
|
24
|
+
// xSTRK removed - not supported by Pragma RPC, will use Ekubo pricer
|
|
25
|
+
// ContractAddr.from('0x28d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a').address, // xSTRK
|
|
26
|
+
ContractAddr.from('0x68f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8').address, // USDT
|
|
27
|
+
ContractAddr.from('0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7').address, // ETH
|
|
28
|
+
];
|
|
29
|
+
|
|
8
30
|
constructor(config: IConfig, tokens: TokenInfo[]) {
|
|
9
31
|
super(config, tokens);
|
|
32
|
+
this.apolloClient = createApolloClient(config);
|
|
33
|
+
this.pragma = new Pragma(config, tokens);
|
|
34
|
+
this.ekuboPricer = new EkuboPricer(config, tokens);
|
|
10
35
|
}
|
|
11
36
|
|
|
12
|
-
async getPrice(tokenSymbol: string): Promise<PriceInfo> {
|
|
37
|
+
async getPrice(tokenSymbol: string, blockNumber?: BlockIdentifier): Promise<PriceInfo> {
|
|
38
|
+
const tokenInfo = this.tokens.find(t => t.symbol === tokenSymbol);
|
|
39
|
+
if (!tokenInfo) {
|
|
40
|
+
throw new Error(`Token ${tokenSymbol} not found in configured tokens`);
|
|
41
|
+
}
|
|
42
|
+
const symbol = tokenInfo.priceProxySymbol || tokenInfo.symbol;
|
|
43
|
+
// If blockNumber is provided, fetch historical price from GraphQL
|
|
44
|
+
if (blockNumber !== undefined) {
|
|
45
|
+
try {
|
|
46
|
+
return await this.getHistoricalPrice(symbol, blockNumber);
|
|
47
|
+
} catch (e: any) {
|
|
48
|
+
logger.error(`Historical price fetch failed for ${tokenSymbol}: ${e.message}`);
|
|
49
|
+
throw new Error('Server Error in fetching historical price')
|
|
50
|
+
// Fall back to current price if historical price is not available
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Otherwise, fetch current price
|
|
13
55
|
try {
|
|
14
|
-
return await this.getPriceFromMyAPI(
|
|
56
|
+
return await this.getPriceFromMyAPI(symbol);
|
|
15
57
|
} catch (e: any) {
|
|
16
|
-
logger.warn(
|
|
58
|
+
logger.warn(`API price fetch failed for ${tokenSymbol}: ${e.message}, trying coinbase`);
|
|
17
59
|
}
|
|
18
|
-
|
|
60
|
+
|
|
61
|
+
logger.info(`Using Coinbase price for ${tokenSymbol}`);
|
|
19
62
|
let retry = 0;
|
|
20
63
|
const MAX_RETRIES = 5;
|
|
21
64
|
for (retry = 1; retry < MAX_RETRIES + 1; retry++) {
|
|
@@ -41,22 +84,120 @@ export class PricerFromApi extends PricerBase {
|
|
|
41
84
|
}
|
|
42
85
|
|
|
43
86
|
async getPriceFromMyAPI(tokenSymbol: string) {
|
|
44
|
-
logger.verbose(`getPrice from api: ${tokenSymbol}`);
|
|
45
87
|
const endpoint = 'https://proxy.api.troves.fi'
|
|
46
88
|
const url = `${endpoint}/api/price/${tokenSymbol}`;
|
|
47
89
|
const priceInfoRes = await fetch(url);
|
|
48
90
|
const priceInfo = await priceInfoRes.json();
|
|
49
|
-
const now = new Date();
|
|
50
|
-
const priceTime = new Date(priceInfo.timestamp);
|
|
51
|
-
// if (now.getTime() - priceTime.getTime() > 900000) {
|
|
52
|
-
// // 15 mins
|
|
53
|
-
// logger.verbose(`Price is stale: ${tokenSymbol}, timestamp: ${priceInfo.timestamp}, price: ${priceInfo.price}`);
|
|
54
|
-
// throw new Error('Price is stale');
|
|
55
|
-
// }
|
|
56
91
|
const price = Number(priceInfo.price);
|
|
57
92
|
return {
|
|
58
93
|
price,
|
|
59
94
|
timestamp: new Date(priceInfo.timestamp)
|
|
60
95
|
}
|
|
61
|
-
|
|
62
|
-
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fetches historical price for a token at a specific block number
|
|
100
|
+
* @param tokenSymbol - The token symbol to get price for
|
|
101
|
+
* @param blockNumber - The block number to query
|
|
102
|
+
* @returns PriceInfo with price at the closest block <= blockNumber
|
|
103
|
+
*/
|
|
104
|
+
async getHistoricalPrice(tokenSymbol: string, blockNumber: BlockIdentifier): Promise<PriceInfo> {
|
|
105
|
+
const tokenInfo = this.tokens.find(t => t.symbol === tokenSymbol);
|
|
106
|
+
if (!tokenInfo) {
|
|
107
|
+
throw new Error(`Token ${tokenSymbol} not found in configured tokens`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If blockNumber is not a number, fall back to current price
|
|
111
|
+
if (typeof blockNumber !== 'number') {
|
|
112
|
+
logger.info(`Non-numeric blockIdentifier '${blockNumber}' provided, fetching current price for ${tokenSymbol}`);
|
|
113
|
+
return await this.getPriceFromMyAPI(tokenSymbol);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const standardizedAddress = ContractAddr.from(tokenInfo.address.address).address;
|
|
117
|
+
const isPragmaSupported = this.PRAGMA_SUPPORTED_TOKENS.includes(standardizedAddress);
|
|
118
|
+
|
|
119
|
+
// Step 1: Try GraphQL first for ALL tokens
|
|
120
|
+
let data;
|
|
121
|
+
try {
|
|
122
|
+
const result = await this.apolloClient.query({
|
|
123
|
+
query: gql`
|
|
124
|
+
query GetHistoricalPrice($asset: String!, $blockNumber: Int!) {
|
|
125
|
+
findFirstPrices(
|
|
126
|
+
where: {
|
|
127
|
+
asset: { equals: $asset },
|
|
128
|
+
block_number: { lte: $blockNumber }
|
|
129
|
+
}
|
|
130
|
+
orderBy: { block_number: desc }
|
|
131
|
+
) {
|
|
132
|
+
price
|
|
133
|
+
timestamp
|
|
134
|
+
block_number
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`,
|
|
138
|
+
variables: {
|
|
139
|
+
asset: standardizedAddress,
|
|
140
|
+
blockNumber: blockNumber,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
data = result.data;
|
|
144
|
+
} catch (graphqlError: any) {
|
|
145
|
+
logger.error(`GraphQL query failed for ${tokenSymbol}: ${graphqlError.message}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Step 2: If GraphQL has data, check block difference and use it if acceptable
|
|
149
|
+
if (data?.findFirstPrices) {
|
|
150
|
+
const priceData = data.findFirstPrices;
|
|
151
|
+
const blockDifference = blockNumber - priceData.block_number;
|
|
152
|
+
const MAX_BLOCK_DIFFERENCE = 6800; // ~6 hours worth of blocks
|
|
153
|
+
|
|
154
|
+
if (blockDifference <= MAX_BLOCK_DIFFERENCE) {
|
|
155
|
+
logger.info(`Using GraphQL price for ${tokenSymbol} at block ${priceData.block_number}`);
|
|
156
|
+
return {
|
|
157
|
+
price: Number(priceData.price),
|
|
158
|
+
timestamp: new Date(priceData.timestamp * 1000),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger.info(
|
|
163
|
+
`Block difference ${blockDifference} exceeds limit for ${tokenSymbol}. ` +
|
|
164
|
+
`Will try fallback sources at block ${blockNumber}`
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
logger.info(`No GraphQL data for ${tokenSymbol} at block ${blockNumber}, trying fallback sources`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Step 3: Fallback to Pragma (for supported tokens) or Ekubo (for non-supported tokens)
|
|
171
|
+
if (isPragmaSupported) {
|
|
172
|
+
logger.info(`Attempting Pragma RPC fetch for ${tokenSymbol} at block ${blockNumber}`);
|
|
173
|
+
try {
|
|
174
|
+
const priceInfo = await this.pragma.getPrice(tokenInfo.address.address, blockNumber);
|
|
175
|
+
logger.info(`Using Pragma price for ${tokenSymbol}: ${priceInfo.price}`);
|
|
176
|
+
return {
|
|
177
|
+
price: priceInfo.price,
|
|
178
|
+
timestamp: priceInfo.timestamp,
|
|
179
|
+
};
|
|
180
|
+
} catch (error: any) {
|
|
181
|
+
logger.error(`Pragma RPC failed for ${tokenSymbol} at block ${blockNumber}: ${error.message}`);
|
|
182
|
+
throw new Error(`Server Error at Pragma token ${tokenSymbol} at block ${blockNumber}`);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// For non-Pragma tokens, use Ekubo pricer
|
|
186
|
+
logger.info(`Attempting Ekubo price fetch for ${tokenSymbol} at block ${blockNumber}`);
|
|
187
|
+
try {
|
|
188
|
+
const priceInfo = await this.ekuboPricer.getPrice(
|
|
189
|
+
tokenInfo.address.address,
|
|
190
|
+
blockNumber
|
|
191
|
+
);
|
|
192
|
+
logger.info(`Using Ekubo price for ${tokenSymbol}: ${priceInfo.price}`);
|
|
193
|
+
return {
|
|
194
|
+
price: priceInfo.price,
|
|
195
|
+
timestamp: priceInfo.timestamp,
|
|
196
|
+
};
|
|
197
|
+
} catch (error: any) {
|
|
198
|
+
logger.error(`Ekubo RPC failed for ${tokenSymbol} at block ${blockNumber}: ${error.message}`);
|
|
199
|
+
throw new Error(`Server Error at Ekubo token ${tokenSymbol} at block ${blockNumber}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -6,7 +6,7 @@ import axios from "axios";
|
|
|
6
6
|
|
|
7
7
|
export class PricerLST extends Pricer {
|
|
8
8
|
private tokenMaps: {lst: TokenInfo, underlying: TokenInfo}[];
|
|
9
|
-
protected EKUBO_API = 'https://
|
|
9
|
+
protected EKUBO_API = 'https://prod-api-quoter.ekubo.org/23448594291968334/{{AMOUNT}}/{{TOKEN_ADDRESS}}/{{UNDERLYING_ADDRESS}}'; // e.g. xSTRK/STRK
|
|
10
10
|
|
|
11
11
|
constructor(config: IConfig, tokenMaps: {lst: TokenInfo, underlying: TokenInfo}[]) {
|
|
12
12
|
const refreshInterval = 5000;
|
package/src/modules/pricer.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { IConfig } from "@/interfaces/common";
|
|
|
5
5
|
import { Web3Number } from "@/dataTypes";
|
|
6
6
|
import { PricerBase } from "./pricerBase";
|
|
7
7
|
import { logger } from "@/utils/logger";
|
|
8
|
+
import { AvnuWrapper } from "./avnu";
|
|
9
|
+
import { BlockIdentifier } from "starknet";
|
|
8
10
|
|
|
9
11
|
export interface PriceInfo {
|
|
10
12
|
price: number,
|
|
@@ -21,13 +23,14 @@ export class Pricer extends PricerBase {
|
|
|
21
23
|
|
|
22
24
|
// code populates this map during runtime to determine which method to use for a given token
|
|
23
25
|
// The method set will be the first one to try after first attempt
|
|
24
|
-
protected methodToUse: {[tokenSymbol: string]: 'Ekubo' | 'Coinbase' | 'Coinmarketcap'} = {};
|
|
26
|
+
protected methodToUse: {[tokenSymbol: string]: 'Ekubo' | 'Coinbase' | 'Coinmarketcap' | 'Avnu'} = {};
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* TOKENA and TOKENB are the two token names to get price of TokenA in terms of TokenB
|
|
28
30
|
*/
|
|
31
|
+
// ! switch to USDC (new) later
|
|
29
32
|
protected PRICE_API = `https://api.coinbase.com/v2/prices/{{PRICER_KEY}}/buy`;
|
|
30
|
-
protected EKUBO_API = 'https://
|
|
33
|
+
protected EKUBO_API = 'https://prod-api-quoter.ekubo.org/23448594291968334/{{AMOUNT}}/{{TOKEN_ADDRESS}}/0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb'; // e.g. ETH/USDC
|
|
31
34
|
|
|
32
35
|
constructor(config: IConfig, tokens: TokenInfo[], refreshInterval = 30000, staleTime = 60000) {
|
|
33
36
|
super(config, tokens);
|
|
@@ -81,7 +84,7 @@ export class Pricer extends PricerBase {
|
|
|
81
84
|
Global.assert(!this.isStale(timestamp, tokenName), `Price of ${tokenName} is stale`);
|
|
82
85
|
|
|
83
86
|
}
|
|
84
|
-
async getPrice(tokenSymbol: string) {
|
|
87
|
+
async getPrice(tokenSymbol: string, blockNumber?: BlockIdentifier) {
|
|
85
88
|
Global.assert(this.prices[tokenSymbol], `Price of ${tokenSymbol} not found`);
|
|
86
89
|
this.assertNotStale(this.prices[tokenSymbol].timestamp, tokenSymbol);
|
|
87
90
|
return this.prices[tokenSymbol];
|
|
@@ -93,7 +96,7 @@ export class Pricer extends PricerBase {
|
|
|
93
96
|
let retry = 0;
|
|
94
97
|
while (retry < MAX_RETRIES) {
|
|
95
98
|
try {
|
|
96
|
-
if (token.symbol === 'USDT') {
|
|
99
|
+
if (token.symbol === 'USDT' || token.symbol === 'USDC') {
|
|
97
100
|
this.prices[token.symbol] = {
|
|
98
101
|
price: 1,
|
|
99
102
|
timestamp: new Date()
|
|
@@ -174,6 +177,15 @@ export class Pricer extends PricerBase {
|
|
|
174
177
|
console.warn(`Ekubo: price err [${token.symbol}]: `, Object.keys(error));
|
|
175
178
|
// do nothing, try next
|
|
176
179
|
}
|
|
180
|
+
case 'Avnu':
|
|
181
|
+
try {
|
|
182
|
+
const result = await this._getAvnuPrice(token, new Web3Number(token.priceCheckAmount ? token.priceCheckAmount : 1, token.decimals));
|
|
183
|
+
this.methodToUse[token.symbol] = 'Avnu';
|
|
184
|
+
return result;
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
console.warn(`Avnu: price err [${token.symbol}]: `, error.message);
|
|
187
|
+
console.warn(`Avnu: price err [${token.symbol}]: `, Object.keys(error));
|
|
188
|
+
}
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
// if methodToUse is the default one, pass Coinbase to try all from start
|
|
@@ -200,7 +212,31 @@ export class Pricer extends PricerBase {
|
|
|
200
212
|
throw new Error("Not implemented");
|
|
201
213
|
}
|
|
202
214
|
|
|
215
|
+
async _getAvnuPrice(token: TokenInfo, amountIn = new Web3Number(1, token.decimals), retry = 0): Promise<number> {
|
|
216
|
+
logger.verbose(`Getting price of ${token.symbol} using Ekubo, amountIn: ${amountIn.toWei()}`);
|
|
217
|
+
|
|
218
|
+
const avnuWrapper = new AvnuWrapper();
|
|
219
|
+
const usdcAddress = '0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb';
|
|
220
|
+
const quote = await avnuWrapper.getQuotes(token.address.toString(), usdcAddress, amountIn.toWei(), '0x1');
|
|
221
|
+
const multiplier = 1 / amountIn.toNumber();
|
|
222
|
+
const outputUSDC = Number(Web3Number.fromWei(quote.buyAmount.toString(), 6).toFixed(6)) * multiplier;
|
|
223
|
+
logger.verbose(`Avnu: ${token.symbol} -> USDC: ${outputUSDC}, retry: ${retry}`);
|
|
224
|
+
if (outputUSDC === 0 && retry < 3) {
|
|
225
|
+
// try again with a higher amount
|
|
226
|
+
const amountIn = new Web3Number(100, token.decimals); // 100 unit of token
|
|
227
|
+
return await this._getAvnuPrice(token, amountIn, retry + 1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// if usdc depegs, it will not longer be 1 USD
|
|
231
|
+
// so we need to get the price of USDC in USD
|
|
232
|
+
// and then convert the outputUSDC to USD
|
|
233
|
+
const usdcPrice = 1; // (await this.getPrice('USDC')).price;
|
|
234
|
+
logger.verbose(`USDC Price: ${usdcPrice}`);
|
|
235
|
+
return outputUSDC * usdcPrice;
|
|
236
|
+
}
|
|
237
|
+
|
|
203
238
|
async _getPriceEkubo(token: TokenInfo, amountIn = new Web3Number(1, token.decimals), retry = 0): Promise<number> {
|
|
239
|
+
logger.verbose(`Getting price of ${token.symbol} using Ekubo, amountIn: ${amountIn.toWei()}`);
|
|
204
240
|
const url = this.EKUBO_API.replace("{{TOKEN_ADDRESS}}", token.address.toString()).replace("{{AMOUNT}}", amountIn.toWei());
|
|
205
241
|
const result = await axios.get(url);
|
|
206
242
|
const data: any = result.data;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IConfig, TokenInfo } from "@/interfaces";
|
|
2
2
|
import { PriceInfo } from "./pricer";
|
|
3
|
+
import { BlockIdentifier } from "starknet";
|
|
3
4
|
|
|
4
5
|
export abstract class PricerBase {
|
|
5
6
|
readonly config: IConfig;
|
|
@@ -9,7 +10,7 @@ export abstract class PricerBase {
|
|
|
9
10
|
this.tokens = tokens;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
async getPrice(tokenSymbol: string): Promise<PriceInfo> {
|
|
13
|
+
async getPrice(tokenSymbol: string, blockNumber?: BlockIdentifier): Promise<PriceInfo> {
|
|
13
14
|
throw new Error('Method not implemented');
|
|
14
15
|
}
|
|
15
16
|
}
|