/**
 * wallet sdk support to query info about token list, token balance, token price and token value
 */ import { memoize } from '@polkadot/util' ; import { FixedPointNumber as FN , AnyApi , Token , MaybeCurrency , forceToCurrencyName , TokenType , unzipDexShareName , isDexShareName , createLiquidCroadloanName } from '@acala-network/sdk-core' ; import { AccountInfo , Balance , RuntimeDispatchInfo } from '@polkadot/types/interfaces' ; import { OrmlAccountData } from '@open-web3/orml-types/interfaces' ; import { BehaviorSubject , combineLatest , Observable , of , firstValueFrom } from 'rxjs' ; import { map , switchMap , shareReplay , filter } from 'rxjs/operators' ; import { TokenRecord , WalletConsts , BalanceData , PresetTokens , TokenPriceFetchSource } from './type' ; import { ChainType , CurrencyNotFound , Homa , Liquidity , SDKNotReady } from '..' ; import { getMaxAvailableBalance } from './utils/get-max-available-balance' ; import { MarketPriceProvider } from './price-provider/market-price-provider' ; import { OraclePriceProvider } from './price-provider/oracle-price-provider' ; import { PriceProvider , PriceProviderType } from './price-provider/types' ; import { createTokenList } from './utils/create-token-list' ; import { BaseSDK } from '../types' ; import { createStorages } from './storages' ; import tokenList from '../configs/token-list' ; import { TokenProvider } from '../base-provider' ; import { defaultTokenPriceFetchSource } from './price-provider/default-token-price-fetch-source-config' ; import { subscribeDexShareTokenPrice } from './utils/get-dex-share-token-price' ; import { getChainType } from '../utils/get-chain-type' ; type PriceProviders = Partial < { [ k in PriceProviderType ] : PriceProvider ; } > ; export class Wallet implements BaseSDK , TokenProvider { private api : AnyApi ; private priceProviders : PriceProviders ; private tokens$ : BehaviorSubject < TokenRecord > ; private storages : ReturnType < typeof createStorages > ; private tokenPriceFetchSource : TokenPriceFetchSource ; public isReady$ : BehaviorSubject < boolean > ; public consts ! : WalletConsts ; public constructor ( api , tokenPriceFetchSource = defaultTokenPriceFetchSource , priceProviders ? ) { this . api = api ; this . isReady$ = new BehaviorSubject < boolean > ( false ) ; this . tokens$ = new BehaviorSubject < TokenRecord > ( { } ) ; this . tokenPriceFetchSource = tokenPriceFetchSource ; this . priceProviders = { ... this . defaultPriceProviders , ... priceProviders } ; this . storages = createStorages ( this . api ) ; this . init ( ) ; } public get isReady ( ) : Promise < boolean > { return firstValueFrom ( this . isReady$ . asObservable ( ) . pipe ( filter ( ( i ) => i ) ) ) ; } private init ( ) { this . initConsts ( ) ; this . initTokens ( ) ; } private initConsts ( ) { this . consts = { runtimeChain : this . api . runtimeChain . toString ( ) , nativeCurrency : this . api . registry . chainTokens [ 0 ] . toString ( ) } ; } private get defaultPriceProviders ( ) : PriceProviders { return { [ PriceProviderType . MARKET ] : new MarketPriceProvider ( ) , [ PriceProviderType . ORACLE ] : new OraclePriceProvider ( this . api ) } ; } private initTokens ( ) { const chainDecimals = this . api . registry . chainDecimals ; const chainTokens = this . api . registry . chainTokens ; const tradingPairs$ = this . storages . tradingPairs ( ) . observable ; const assetMetadatas$ = this . storages . assetMetadatas ( ) . observable ; const basicTokens = Object . fromEntries ( chainTokens . map ( ( token , i ) => { const config = tokenList . getToken ( token , this . consts . runtimeChain ) ; return [ token , new Token ( token , { ... config , type : TokenType . BASIC , decimals : chainDecimals [ i ] ?? 12 } ) ] ; } ) ) ; if ( getChainType ( this . consts . runtimeChain ) === ChainType . ACALA ) { const name = createLiquidCroadloanName ( 13 ) ; basicTokens [ name ] = new Token ( name , { decimals : basicTokens . DOT . decimals , ed : basicTokens . DOT . ed } ) ; } return combineLatest ( { tradingPairs : tradingPairs$ , assetMetadatas : assetMetadatas$ } ) . subscribe ( { next : ( { tradingPairs , assetMetadatas } ) => { const list = createTokenList ( basicTokens , tradingPairs , assetMetadatas ) ; this . tokens$ . next ( list ) ; this . isReady$ . next ( true ) ; } } ) ; } name param type description public subscribeTokens = memoize ( ( type ? ) : Observable < TokenRecord > => { return this . isReady$ . pipe ( filter ( ( i ) => i ) , switchMap ( ( ) => { return this . tokens$ . pipe ( map ( ( data ) => { if ( ! type ) return data ; return Object . fromEntries ( Object . entries ( data ) . filter ( ( [ , value ] ) => { return value . type === type ; } ) ) ; } ) , shareReplay ( 1 ) ) ; } ) ) ; } ) ; public async getTokens ( type ? ) { return firstValueFrom ( this . subscribeTokens ( type ) ) ; } name description public subscribeToken = memoize ( ( target ) : Observable < Token > => { const name = forceToCurrencyName ( target ) ; return this . subscribeTokens ( ) . pipe ( map ( ( all ) => { const result = Object . values ( all ) . find ( ( item ) => item . name === name ) ; if ( ! result ) throw new CurrencyNotFound ( name ) ; return result ; } ) ) ; } ) ; public async getToken ( target ) { return firstValueFrom ( this . subscribeToken ( target ) ) ; } name description param token param address public subscribeBalance = memoize ( ( token , address ) : Observable < BalanceData > => { return this . subscribeToken ( token ) . pipe ( switchMap ( ( token ) => { const { nativeCurrency } = this . consts ; const isNativeToken = nativeCurrency === token . name ; const handleNative = ( data , token ) => { const free = FN . fromInner ( data . data . free . toString ( ) , token . decimals ) ; const locked = FN . fromInner ( data . data . miscFrozen . toString ( ) , token . decimals ) . max ( FN . fromInner ( data . data . feeFrozen . toString ( ) , token . decimals ) ) ; const reserved = FN . fromInner ( data . data . reserved . toString ( ) , token . decimals ) ; const available = free . sub ( locked ) . max ( FN . ZERO ) ; return { available , token , free , locked , reserved } ; } ; const handleNonNative = ( data , token ) => { const free = FN . fromInner ( data . free . toString ( ) , token . decimals ) ; const locked = FN . fromInner ( data . frozen . toString ( ) , token . decimals ) ; const reserved = FN . fromInner ( data . reserved . toString ( ) , token . decimals ) ; const available = free . sub ( locked ) . max ( FN . ZERO ) ; return { available , token , free , locked , reserved } ; } ; if ( isNativeToken ) { const storage = this . storages . nativeBalance ( address ) ; return storage . observable . pipe ( map ( ( data ) => handleNative ( data , token ) ) ) ; } const storage = this . storages . nonNativeBalance ( token , address ) ; return storage . observable . pipe ( map ( ( data ) => handleNonNative ( data , token ) ) ) ; } ) ) ; } ) ; public async getBalance ( token , address ) { return firstValueFrom ( this . subscribeBalance ( token , address ) ) ; } name description param token public subscribeIssuance = memoize ( ( token ) : Observable < FN > => { return this . subscribeToken ( token ) . pipe ( switchMap ( ( token ) => { const storage = this . storages . issuance ( token ) ; const handleIssuance = ( data , token ) => FN . fromInner ( data . toString ( ) , token . decimals ) ; return storage . observable . pipe ( map ( ( data ) => handleIssuance ( data , token ) ) ) ; } ) ) ; } ) ; public async getIssuance ( token ) { return firstValueFrom ( this . subscribeIssuance ( token ) ) ; } name description params params public subscribeSuggestInput = memoize ( ( token , address , isAllowDeath , paymentInfo , feeFactor = 1.2 ) : Observable < FN > => { const handleNativeResult = ( accountInfo , token , nativeToken ) => { const providers = accountInfo . providers . toNumber ( ) ; const consumers = accountInfo . consumers . toNumber ( ) ; const nativeFreeBalance = FN . fromInner ( accountInfo . data . free . toString ( ) , nativeToken . decimals ) ; const nativeLockedBalance = FN . fromInner ( accountInfo . data . miscFrozen . toString ( ) , nativeToken . decimals ) . max ( FN . fromInner ( accountInfo . data . feeFrozen . toString ( ) , nativeToken . decimals ) ) ; const ed = token . ed ; const fee = FN . fromInner ( paymentInfo . partialFee . toString ( ) , nativeToken . decimals ) . mul ( new FN ( feeFactor ) ) ; return getMaxAvailableBalance ( { isNativeToken : true , isAllowDeath , providers , consumers , nativeFreeBalance , nativeLockedBalance , targetFreeBalance : FN . ZERO , targetLockedBalance : FN . ZERO , ed , fee } ) ; } ; const handleNonNativeResult = ( accountInfo , tokenInfo , token , nativeToken ) => { const providers = accountInfo . providers . toNumber ( ) ; const consumers = accountInfo . consumers . toNumber ( ) ; const nativeFreeBalance = FN . fromInner ( accountInfo . data . free . toString ( ) , nativeToken . decimals ) ; const nativeLockedBalance = FN . fromInner ( accountInfo . data . miscFrozen . toString ( ) , nativeToken . decimals ) . max ( FN . fromInner ( accountInfo . data . feeFrozen . toString ( ) , nativeToken . decimals ) ) ; const targetFreeBalance = FN . fromInner ( tokenInfo . free . toString ( ) , token . decimals ) ; const targetLockedBalance = FN . fromInner ( tokenInfo . frozen . toString ( ) , token . decimals ) ; const ed = token . ed ; const fee = FN . fromInner ( paymentInfo . partialFee . toString ( ) , nativeToken . decimals ) . mul ( new FN ( feeFactor ) ) ; return getMaxAvailableBalance ( { isNativeToken : false , isAllowDeath , providers , consumers , nativeFreeBalance , nativeLockedBalance , targetFreeBalance , targetLockedBalance , ed , fee } ) ; } ; return combineLatest ( { token : this . subscribeToken ( token ) , nativeToken : this . subscribeToken ( this . consts . nativeCurrency ) } ) . pipe ( switchMap ( ( { token , nativeToken } ) => { const isNativeToken = forceToCurrencyName ( token ) === nativeToken . name ; if ( isNativeToken ) { return this . storages . nativeBalance ( address ) . observable . pipe ( map ( ( data ) => handleNativeResult ( data , token , nativeToken ) ) ) ; } return combineLatest ( { accountInfo : this . storages . nativeBalance ( address ) . observable , tokenInfo : this . storages . nonNativeBalance ( token , address ) . observable } ) . pipe ( map ( ( { accountInfo , tokenInfo } ) => handleNonNativeResult ( accountInfo , tokenInfo , token , nativeToken ) ) ) ; } ) ) ; } ) ; public async getSuggestInput ( token , address , isAllowDeath , paymentInfo , feeFactor = 1.2 ) { return firstValueFrom ( this . subscribeSuggestInput ( token , address , isAllowDeath , paymentInfo , feeFactor ) ) ; } public getPresetTokens ( ) { if ( this . isReady$ . value === false ) { throw new SDKNotReady ( 'wallet' ) ; } const tokens = this . tokens$ . value ; const data = { nativeToken : tokens [ forceToCurrencyName ( this . consts . nativeCurrency ) ] } ; if ( this . api . consts ?. cdpEngine . getStableCurrencyId ) { data . stableToken = tokens [ forceToCurrencyName ( this . api . consts . cdpEngine . getStableCurrencyId ) ] ; } data . liquidToken = tokens [ forceToCurrencyName ( this . api . consts . homa ?. liquidCurrencyId || this . api . consts . homaLite ?. liquidCurrencyId || '' ) ] ; data . stakingToken = tokens [ forceToCurrencyName ( this . api . consts . homa ?. stakingCurrencyId || this . api . consts . homaLite ?. stakingCurrencyId || '' ) ] ; return data ; } name description public subscribePrice = memoize ( ( token , type ? ) : Observable < FN > => { let priceProvider ; const name = forceToCurrencyName ( token ) ; const isDexShare = isDexShareName ( name ) ; const presetTokens = this . getPresetTokens ( ) ; const isLiquidToken = presetTokens ?. liquidToken ?. name === name ; const stakingToken = presetTokens . stableToken ; const _type = type || this . tokenPriceFetchSource ?. [ getChainType ( this . consts . runtimeChain ) ] ?. [ name ] || PriceProviderType . MARKET ; if ( _type === PriceProviderType . MARKET ) { priceProvider = this . priceProviders ?. [ PriceProviderType . MARKET ] ; } if ( _type === PriceProviderType . ORACLE ) { priceProvider = this . priceProviders ?. [ PriceProviderType . ORACLE ] ; } if ( ! priceProvider ) return of ( FN . ZERO ) ; if ( isDexShare ) { const [ token0 , token1 ] = unzipDexShareName ( name ) ; return subscribeDexShareTokenPrice ( this . subscribePrice ( token0 ) , this . subscribePrice ( token1 ) , new Liquidity ( this . api , this ) . subscribePoolDetail ( name ) ) ; } if ( isLiquidToken && stakingToken ) { return new Homa ( this . api , this ) . subscribeConvertor ( ) . pipe ( switchMap ( ( convertor ) => { return this . subscribePrice ( stakingToken ) . pipe ( map ( ( price ) => convertor . convertStakingToLiquid ( price ) ) ) ; } ) ) ; } return this . subscribeToken ( token ) . pipe ( switchMap ( ( token ) => priceProvider ! . subscribe ( token . name ) ) ) ; } ) ; public getPrice ( token , type ? ) { return firstValueFrom ( this . subscribePrice ( token , type ) ) ; } }