|
1 | | -import { Observable, ReplaySubject, Subscription, firstValueFrom, combineLatest } from 'rxjs'; |
2 | | -import { map, filter } from 'rxjs/operators'; |
3 | | -import { PriceProvider } from './types'; |
4 | | -import { TimestampedValue } from '@open-web3/orml-types/interfaces'; |
5 | | -import { |
6 | | - AnyApi, |
7 | | - FixedPointNumber, |
8 | | - FixedPointNumber as FN, |
9 | | - forceToCurrencyName, |
10 | | - isLiquidCrowdloanName, |
11 | | - MaybeCurrency |
12 | | -} from '@acala-network/sdk-core'; |
13 | | -import { OracleKey } from '@acala-network/types/interfaces'; |
| 1 | +import { Observable, firstValueFrom, of } from 'rxjs'; |
| 2 | +import { Option } from '@polkadot/types-codec'; |
| 3 | +import { map, switchMap } from 'rxjs/operators'; |
| 4 | +import { OracleConfig, PriceProvider } from './types'; |
| 5 | +import { AnyApi, FixedPointNumber, FixedPointNumber as FN, Token } from '@acala-network/sdk-core'; |
14 | 6 | import { Storage } from '../../utils/storage'; |
15 | | -import { AcalaPrimitivesCurrencyCurrencyId } from '@polkadot/types/lookup'; |
16 | | -import { SignedBlock } from '@polkadot/types/interfaces'; |
17 | | -import { getAllLiquidCrowdloanTokenPrice } from '../utils/get-liquid-crowdloan-token-price'; |
| 7 | +import { OrmlOracleModuleTimestampedValue } from '@polkadot/types/lookup'; |
| 8 | +import { subscribeLiquidCrowdloanTokenPrice } from '../utils/get-liquid-crowdloan-token-price'; |
| 9 | +import { TokenProvider } from '../token-provider/type'; |
| 10 | + |
| 11 | +const DEFAULT_ORACLE_STRATEGIES: OracleConfig['strategies'] = { |
| 12 | + taiKSM: ['AS', 'KSM'], |
| 13 | + tDOT: ['AS', 'DOT'], |
| 14 | + lcDOT: ['LIQUID_CROWDLOAN', 13] |
| 15 | +}; |
18 | 16 |
|
19 | 17 | export class OraclePriceProvider implements PriceProvider { |
20 | 18 | private api: AnyApi; |
21 | | - private oracleProvider: string; |
22 | | - private subject: ReplaySubject<Record<string, FN>>; |
23 | | - private liquidCrowdloanSubject: ReplaySubject<Record<string, FN>>; |
24 | | - private processSubscriber: Subscription; |
25 | | - private crowdloanPriceProcessSubscriber: Subscription; |
26 | | - private consts: { |
27 | | - stakingCurrency: AcalaPrimitivesCurrencyCurrencyId; |
28 | | - liquidCurrency: AcalaPrimitivesCurrencyCurrencyId; |
29 | | - }; |
| 19 | + private stakingToken: Token; |
| 20 | + private strategies: OracleConfig['strategies']; |
| 21 | + private tokenProvider: TokenProvider; |
30 | 22 |
|
31 | | - constructor(api: AnyApi, oracleProvider = 'Aggregated') { |
| 23 | + constructor(api: AnyApi, config: OracleConfig) { |
32 | 24 | this.api = api; |
33 | | - this.oracleProvider = oracleProvider; |
34 | | - this.subject = new ReplaySubject<Record<string, FN>>(1); |
35 | | - this.liquidCrowdloanSubject = new ReplaySubject(1); |
36 | | - |
37 | | - this.consts = { |
38 | | - stakingCurrency: api.consts.prices.getStakingCurrencyId, |
39 | | - liquidCurrency: api.consts.prices.getLiquidCurrencyId |
40 | | - }; |
41 | | - |
42 | | - this.processSubscriber = this.process(); |
43 | | - this.crowdloanPriceProcessSubscriber = this.liquidCrowdloanPriceProcess(); |
44 | | - } |
45 | 25 |
|
46 | | - public unsub(): void { |
47 | | - this.processSubscriber.unsubscribe(); |
48 | | - this.crowdloanPriceProcessSubscriber.unsubscribe(); |
| 26 | + this.stakingToken = config.stakingToken; |
| 27 | + this.tokenProvider = config.tokenPrivoder; |
| 28 | + this.strategies = config?.strategies || DEFAULT_ORACLE_STRATEGIES; |
49 | 29 | } |
50 | 30 |
|
51 | | - private oraclePrice$(name: string) { |
52 | | - return this.subject.pipe( |
53 | | - filter((values) => !!values), |
54 | | - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
55 | | - map((values) => values![name]) |
56 | | - ); |
57 | | - } |
58 | | - |
59 | | - private liquidCrowdloanPriceProcess = () => { |
60 | | - const storage$ = Storage.create<SignedBlock>({ |
| 31 | + private queryFormOracle(token: Token) { |
| 32 | + const storage$ = Storage.create<Option<OrmlOracleModuleTimestampedValue>>({ |
61 | 33 | api: this.api, |
62 | | - path: 'rpc.chain.getBlock', |
63 | | - params: [] |
| 34 | + path: 'query.acalaOracle.values', |
| 35 | + params: [token.toChainData()] |
64 | 36 | }).observable; |
65 | 37 |
|
66 | | - return combineLatest({ |
67 | | - signedBlock: storage$, |
68 | | - stakingTokenPrice: this.oraclePrice$(forceToCurrencyName(this.consts.stakingCurrency)) |
69 | | - }).subscribe({ |
70 | | - next: ({ signedBlock, stakingTokenPrice }) => { |
71 | | - if (!stakingTokenPrice) return; |
72 | | - |
73 | | - const prices = getAllLiquidCrowdloanTokenPrice(this.api, signedBlock, stakingTokenPrice); |
| 38 | + return storage$.pipe( |
| 39 | + map((value) => { |
| 40 | + const price = value.isEmpty ? '0' : value.value.value.toString(); |
74 | 41 |
|
75 | | - if (prices) { |
76 | | - // update all crowdloan token price |
77 | | - this.liquidCrowdloanSubject.next(prices); |
78 | | - } |
79 | | - } |
80 | | - }); |
81 | | - }; |
| 42 | + return FixedPointNumber.fromInner(price, 18); |
| 43 | + }) |
| 44 | + ); |
| 45 | + } |
82 | 46 |
|
83 | | - private process = () => { |
84 | | - const storage$ = Storage.create<[[OracleKey, TimestampedValue]]>({ |
85 | | - api: this.api, |
86 | | - path: 'rpc.oracle.getAllValues', |
87 | | - params: [this.oracleProvider], |
88 | | - events: [{ section: '*', method: 'NewFeedData' }] |
89 | | - }).observable; |
| 47 | + public subscribe(token: Token): Observable<FN> { |
| 48 | + const strategies = this.strategies; |
90 | 49 |
|
91 | | - return storage$.subscribe({ |
92 | | - next: (result) => { |
93 | | - const formated = Object.fromEntries( |
94 | | - result.map((item) => { |
95 | | - const currency = forceToCurrencyName(item[0]); |
96 | | - const price = FN.fromInner( |
97 | | - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access |
98 | | - (item[1]?.value as any)?.value.toString() || '0' |
99 | | - ); |
| 50 | + if (!strategies) return of(FixedPointNumber.ZERO); |
100 | 51 |
|
101 | | - return [currency, price]; |
102 | | - }) |
103 | | - ); |
104 | | - this.subject.next(formated); |
105 | | - } |
106 | | - }); |
107 | | - }; |
| 52 | + const symbol = token.symbol; |
108 | 53 |
|
109 | | - public subscribe(currency: MaybeCurrency): Observable<FN> { |
110 | | - return combineLatest({ |
111 | | - oracle: this.subject, |
112 | | - liquidCrowdloanPrices: this.liquidCrowdloanSubject |
113 | | - }).pipe( |
114 | | - filter(({ oracle }) => oracle !== undefined), |
115 | | - map(({ oracle, liquidCrowdloanPrices }) => { |
116 | | - const name = forceToCurrencyName(currency); |
| 54 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment |
| 55 | + const [strategy, addon] = strategies[symbol] || ['STORAGE', '']; |
117 | 56 |
|
118 | | - if (isLiquidCrowdloanName(name)) { |
119 | | - return liquidCrowdloanPrices[name] || FixedPointNumber.ZERO; |
120 | | - } |
| 57 | + if (strategy === 'AS') { |
| 58 | + return this.subscribe(this.tokenProvider.getToken(addon as string)); |
| 59 | + } |
121 | 60 |
|
122 | | - // TODO: should check the token symbol |
123 | | - if (name === 'sa://0') { |
124 | | - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
125 | | - return oracle!.KSM || FixedPointNumber.ZERO; |
126 | | - } |
| 61 | + if (strategy === 'LIQUID_CROWDLOAN') { |
| 62 | + return this.subscribe(this.stakingToken).pipe( |
| 63 | + switchMap((price) => { |
| 64 | + return subscribeLiquidCrowdloanTokenPrice(this.api, price, addon as number); |
| 65 | + }) |
| 66 | + ); |
| 67 | + } |
127 | 68 |
|
128 | | - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
129 | | - return oracle![forceToCurrencyName(currency)] || FixedPointNumber.ZERO; |
130 | | - }) |
131 | | - ); |
| 69 | + return this.queryFormOracle(token); |
132 | 70 | } |
133 | 71 |
|
134 | | - public async query(currency: MaybeCurrency): Promise<FN> { |
| 72 | + public async query(currency: Token): Promise<FN> { |
135 | 73 | return firstValueFrom(this.subscribe(currency)); |
136 | 74 | } |
137 | 75 | } |
0 commit comments