import { delay , inject , singleton } from "tsyringe" ; import { TYPES } from "../types" ; import { Env } from "@keplr-wallet/router" ; import { ChainInfo , AppCurrency , CW20Currency , Secret20Currency , } from "@keplr-wallet/types" ; import { CurrencySchema , CW20CurrencyShema , Secret20CurrencyShema , } from "../chains" ; import { Bech32Address , ChainIdHelper } from "@keplr-wallet/cosmos" ; import { ChainsService } from "../chains" ; import { KeyRingService } from "../keyring" ; import { KVStore } from "@keplr-wallet/common" ; import { KeyRingStatus } from "../keyring" ; import { InteractionService } from "../interaction" ; import { PermissionService } from "../permission" ; import { Buffer } from "buffer/" ; import { SuggestTokenMsg } from "./messages" ; import { getSecret20ViewingKeyPermissionType } from "./types" ; @ singleton ( ) export class TokensService { constructor ( @ inject ( TYPES . TokensStore ) protected readonly kvStore , @ inject ( delay ( ( ) => InteractionService ) ) protected readonly interactionService , @ inject ( delay ( ( ) => PermissionService ) ) public readonly permissionService , @ inject ( ChainsService ) protected readonly chainsService , @ inject ( delay ( ( ) => KeyRingService ) ) protected readonly keyRingService ) { this . chainsService . addChainRemovedHandler ( this . onChainRemoved ) ; } protected readonly onChainRemoved = ( chainId ) => { this . clearTokens ( chainId ) ; } ; async suggestToken ( env , chainId , contractAddress , viewingKey ? ) { const chainInfo = await this . chainsService . getChainInfo ( chainId ) ; const find = ( await this . getTokens ( chainId ) ) . find ( ( currency ) => "contractAddress" in currency && currency . contractAddress === contractAddress ) ; if ( find ) { if ( viewingKey ) { if ( "type" in find && find . type === "secret20" ) { await this . addToken ( chainId , { ... find , viewingKey , } ) ; } return ; } return ; } Bech32Address . validate ( contractAddress , chainInfo . bech32Config . bech32PrefixAccAddr ) ; const params = { chainId , contractAddress , viewingKey , } ; const appCurrency = await this . interactionService . waitApprove ( env , "/setting/token/add" , SuggestTokenMsg . type ( ) , params ) ; await this . addToken ( chainId , appCurrency as AppCurrency ) ; } async addToken ( chainId , currency ) { const chainInfo = await this . chainsService . getChainInfo ( chainId ) ; currency = await TokensService . validateCurrency ( chainInfo , currency ) ; const chainCurrencies = await this . getTokens ( chainId ) ; const isTokenForAccount = "type" in currency && currency . type === "secret20" ; let isCurrencyUpdated = false ; for ( const chainCurrency of chainCurrencies ) { if ( currency . coinMinimalDenom === chainCurrency . coinMinimalDenom ) { if ( ! isTokenForAccount ) { return ; } isCurrencyUpdated = true ; } } if ( ! isTokenForAccount ) { const currencies = await this . getTokensFromChain ( chainId ) ; currencies . push ( currency ) ; await this . saveTokensToChain ( chainId , currencies ) ; } else { const currencies = await this . getTokensFromChainAndAccount ( chainId ) ; if ( ! isCurrencyUpdated ) { currencies . push ( currency ) ; await this . saveTokensToChainAndAccount ( chainId , currencies ) ; } else { const index = currencies . findIndex ( ( cur ) => cur . coinMinimalDenom === currency . coinMinimalDenom ) ; if ( index >= 0 ) { currencies [ index ] = currency ; await this . saveTokensToChainAndAccount ( chainId , currencies ) ; } } } } async removeToken ( chainId , currency ) { const chainInfo = await this . chainsService . getChainInfo ( chainId ) ; currency = await TokensService . validateCurrency ( chainInfo , currency ) ; const chainCurrencies = await this . getTokens ( chainId ) ; const isTokenForAccount = "type" in currency && currency . type === "secret20" ; let isFoundCurrency = false ; for ( const chainCurrency of chainCurrencies ) { if ( currency . coinMinimalDenom === chainCurrency . coinMinimalDenom ) { isFoundCurrency = true ; break ; } } if ( ! isFoundCurrency ) { return ; } if ( ! isTokenForAccount ) { const currencies = ( await this . getTokensFromChain ( chainId ) ) . filter ( ( cur ) => cur . coinMinimalDenom !== currency . coinMinimalDenom ) ; await this . saveTokensToChain ( chainId , currencies ) ; } else { const currencies = ( await this . getTokensFromChainAndAccount ( chainId ) ) . filter ( ( cur ) => cur . coinMinimalDenom !== currency . coinMinimalDenom ) ; await this . saveTokensToChainAndAccount ( chainId , currencies ) ; } } public async getTokens ( chainId ) { const chainIdHelper = ChainIdHelper . parse ( chainId ) ; const chainCurrencies = ( await this . kvStore . get < AppCurrency [ ] > ( chainIdHelper . identifier ) ) ?? [ ] ; let keyCurrencies = [ ] ; if ( this . keyRingService . keyRingStatus === KeyRingStatus . UNLOCKED ) { const currentKey = await this . keyRingService . getKey ( chainId ) ; keyCurrencies = ( await this . kvStore . get < AppCurrency [ ] > ( `${ chainIdHelper . identifier }-${ Buffer . from ( currentKey . address ) . toString ( "hex" ) }` ) ) ?? [ ] ; } return chainCurrencies . concat ( keyCurrencies ) ; } public async clearTokens ( chainId ) { const chainIdHelper = ChainIdHelper . parse ( chainId ) ; await this . kvStore . set ( chainIdHelper . identifier , null ) ; const reverse = await this . getTokensToAccountReverse ( chainId ) ; for ( const hexAddress of reverse ) { await this . kvStore . set ( `${ chainIdHelper . identifier }-${ hexAddress }` , null ) ; } await this . setTokensToAccountReverse ( chainId , [ ] ) ; } private async getTokensFromChain ( chainId ) { const chainIdHelper = ChainIdHelper . parse ( chainId ) ; return ( ( await this . kvStore . get < AppCurrency [ ] > ( chainIdHelper . identifier ) ) ?? [ ] ) ; } private async saveTokensToChain ( chainId , currencies ) { const chainIdHelper = ChainIdHelper . parse ( chainId ) ; await this . kvStore . set ( chainIdHelper . identifier , currencies ) ; } private async getTokensFromChainAndAccount ( chainId ) { const chainIdHelper = ChainIdHelper . parse ( chainId ) ; const currentKey = await this . keyRingService . getKey ( chainId ) ; return ( ( await this . kvStore . get < Promise < AppCurrency [ ] > > ( `${ chainIdHelper . identifier }-${ Buffer . from ( currentKey . address ) . toString ( "hex" ) }` ) ) ?? [ ] ) ; } private async saveTokensToChainAndAccount ( chainId , currencies ) { const chainIdHelper = ChainIdHelper . parse ( chainId ) ; const currentKey = await this . keyRingService . getKey ( chainId ) ; const hexAddress = Buffer . from ( currentKey . address ) . toString ( "hex" ) ; await this . kvStore . set ( `${ chainIdHelper . identifier }-${ hexAddress }` , currencies ) ; await this . insertTokensToAccountReverse ( chainId , hexAddress ) ; } private async getTokensToAccountReverse ( chainId ) { const chainIdHelper = ChainIdHelper . parse ( chainId ) ; return ( ( await this . kvStore . get ( `${ chainIdHelper . identifier }-addresses` ) ) ?? [ ] ) ; } private async setTokensToAccountReverse ( chainId , addresses ) { const chainIdHelper = ChainIdHelper . parse ( chainId ) ; await this . kvStore . set ( `${ chainIdHelper . identifier }-addresses` , addresses ) ; } private async insertTokensToAccountReverse ( chainId , address ) { const reverse = await this . getTokensToAccountReverse ( chainId ) ; if ( reverse . indexOf ( address ) < 0 ) { reverse . push ( address ) ; await this . setTokensToAccountReverse ( chainId , reverse ) ; } } async getSecret20ViewingKey ( chainId , contractAddress ) { const tokens = await this . getTokens ( chainId ) ; for ( const currency of tokens ) { if ( "type" in currency && currency . type === "secret20" ) { if ( currency . contractAddress === contractAddress ) { return currency . viewingKey ; } } } throw new Error ( "There is no matched secret20" ) ; } async checkOrGrantSecret20ViewingKeyPermission ( env , chainId , contractAddress , origin ) { await this . getSecret20ViewingKey ( chainId , contractAddress ) ; const type = getSecret20ViewingKeyPermissionType ( contractAddress ) ; if ( ! this . permissionService . hasPermisson ( chainId , type , origin ) ) { await this . permissionService . grantPermission ( env , "/access/viewing-key" , [ chainId ] , type , [ origin ] ) ; } this . permissionService . checkPermission ( env , chainId , type , origin ) ; } static async validateCurrency ( chainInfo , currency ) { if ( "type" in currency ) { switch ( currency . type ) { case "cw20" : currency = await TokensService . validateCW20Currency ( chainInfo , currency ) ; break ; case "secret20" : currency = await TokensService . validateSecret20Currency ( chainInfo , currency ) ; break ; default : throw new Error ( "Unknown type of currency" ) ; } } else { currency = await CurrencySchema . validateAsync ( currency ) ; } return currency ; } static async validateCW20Currency ( chainInfo , currency ) { currency = await CW20CurrencyShema . validateAsync ( currency ) ; Bech32Address . validate ( currency . contractAddress , chainInfo . bech32Config . bech32PrefixAccAddr ) ; return currency ; } static async validateSecret20Currency ( chainInfo , currency ) { currency = await Secret20CurrencyShema . validateAsync ( currency ) ; Bech32Address . validate ( currency . contractAddress , chainInfo . bech32Config . bech32PrefixAccAddr ) ; return currency ; } }