diff --git a/README.md b/README.md index 24903f08e..479e219c7 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Enkrypt is a web3 wallet built from the ground up to support the multi-chain fut - Ontology - Puppy Net - Arthera +- Caga Ankara Testnet - More coming soon! Looking to add your project? [Contact us!](https://mewwallet.typeform.com/enkrypt-inquiry?typeform-source=www.enkrypt.com) diff --git a/packages/extension/package.json b/packages/extension/package.json index a40a06676..e9a97d9f4 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,19 +1,21 @@ { "name": "@enkryptcom/extension", - "version": "1.38.0", + "version": "1.39.0", "private": true, "scripts": { "zip": "cd dist; zip -r release.zip *;", - "build:chrome": "cross-env BROWSER='chrome' vue-cli-service build && yarn build:rollup", - "build:operaedge": "cross-env BROWSER='opera-edge' vue-cli-service build && cross-env BROWSER='opera-edge' yarn build:rollup", - "build:firefox": "cross-env BROWSER='firefox' vue-cli-service build && yarn build:rollup && node configs/get-system-info.js", - "build:safari": "cross-env BROWSER='safari' vue-cli-service build && yarn build:rollup", + "prebuild": "yarn kadena:prebuild", + "build:chrome": "yarn prebuild && cross-env BROWSER='chrome' vue-cli-service build && yarn build:rollup", + "build:firefox": "yarn prebuild && cross-env BROWSER='firefox' vue-cli-service build && yarn build:rollup && node configs/get-system-info.js", + "build:operaedge": "yarn prebuild && cross-env BROWSER='opera-edge' vue-cli-service build && cross-env BROWSER='opera-edge' yarn build:rollup", + "build:safari": "yarn prebuild && cross-env BROWSER='safari' vue-cli-service build && yarn build:rollup", "lint": "vue-cli-service lint --fix", "build:rollup": "cross-env minify=on rollup --config configs/rollup.config.contentscript.mjs && cross-env minify=on rollup --config configs/rollup.config.inject.mjs", "inspectWebpack": "vue-cli-service inspect > webpack.log", + "kadena:prebuild": "pactjs contract-generate --contract=coin --api https://api.chainweb.com/chainweb/0.0/mainnet01/chain/1/pact", "test": "ts-mocha --require ./configs/testNullCompiler.js --paths -p configs/tsconfig.test.json ./**/*.mocha.ts", - "watch": "rimraf dist && concurrently 'npm:watch-*(!firefox)'", - "watch:firefox": "concurrently 'npm:watch-*(!chrome)'", + "watch": "yarn prebuild && rimraf dist && concurrently 'npm:watch-*(!firefox)'", + "watch:firefox": "yarn prebuild && concurrently 'npm:watch-*(!chrome)'", "watch-contentscript": "rollup --watch --config configs/rollup.config.contentscript.mjs", "watch-inject": "rollup --watch --config configs/rollup.config.inject.mjs", "watch-vue-chrome": "cross-env BROWSER='chrome' vue-cli-service build --watch --no-clean", @@ -33,6 +35,7 @@ "@ethereumjs/common": "^4.3.0", "@ethereumjs/tx": "^5.3.0", "@ethereumjs/util": "^9.0.3", + "@kadena/client": "^1.2.0", "@ledgerhq/hw-transport-webusb": "^6.28.5", "@metamask/eth-sig-util": "^7.0.1", "@rollup/plugin-replace": "^5.0.5", @@ -58,6 +61,7 @@ "memoize-one": "^6.0.0", "moment": "^2.30.1", "nanoevents": "^9.0.0", + "pact-lang-api": "^4.3.6", "pinia": "^2.1.7", "qrcode.vue": "^3.4.1", "switch-ts": "^1.1.1", @@ -75,6 +79,7 @@ }, "devDependencies": { "@babel/plugin-transform-class-static-block": "^7.24.1", + "@kadena/pactjs-cli": "^1.7.0", "@polkadot/api": "^10.12.4", "@polkadot/extension-inject": "^0.46.9", "@polkadot/keyring": "^12.6.2", diff --git a/packages/extension/src/libs/background/index.ts b/packages/extension/src/libs/background/index.ts index e1705fe58..8e67dcb08 100644 --- a/packages/extension/src/libs/background/index.ts +++ b/packages/extension/src/libs/background/index.ts @@ -47,6 +47,7 @@ class BackgroundHandler { [ProviderName.ethereum]: {}, [ProviderName.polkadot]: {}, [ProviderName.bitcoin]: {}, + [ProviderName.kadena]: {}, }; this.#providers = Providers; } diff --git a/packages/extension/src/libs/background/types.ts b/packages/extension/src/libs/background/types.ts index 52b3bfcec..5e59210d6 100644 --- a/packages/extension/src/libs/background/types.ts +++ b/packages/extension/src/libs/background/types.ts @@ -1,18 +1,20 @@ import BitcoinProvider from "@/providers/bitcoin"; import type EthereumProvider from "@/providers/ethereum"; import type PolkadotProvider from "@/providers/polkadot"; +import type KadenaProvider from "@/providers/kadena"; export interface TabProviderType { [key: string]: Record< number, - EthereumProvider | PolkadotProvider | BitcoinProvider + EthereumProvider | PolkadotProvider | BitcoinProvider | KadenaProvider >; } export interface ProviderType { [key: string]: | typeof EthereumProvider | typeof PolkadotProvider - | typeof BitcoinProvider; + | typeof BitcoinProvider + | typeof KadenaProvider; } export interface ExternalMessageOptions { savePersistentEvents: boolean; diff --git a/packages/extension/src/libs/domain-state/index.ts b/packages/extension/src/libs/domain-state/index.ts index d85bda248..0977a7d9a 100644 --- a/packages/extension/src/libs/domain-state/index.ts +++ b/packages/extension/src/libs/domain-state/index.ts @@ -21,6 +21,16 @@ class DomainState { if (state.selectedNetwork) return state.selectedNetwork; return null; } + async setSelectedSubNetwork(id: string): Promise { + const state = await this.getState(); + state.selectedSubNetworkId = id; + await this.setState(state); + } + async getSelectedSubNetWork(): Promise { + const state = await this.getState(); + if (state.selectedSubNetworkId) return state.selectedSubNetworkId; + return null; + } async setSelectedAddress(address: string): Promise { const state = await this.getState(); state.selectedAddress = address; diff --git a/packages/extension/src/libs/domain-state/types.ts b/packages/extension/src/libs/domain-state/types.ts index 3f0206bad..f9e9cceed 100644 --- a/packages/extension/src/libs/domain-state/types.ts +++ b/packages/extension/src/libs/domain-state/types.ts @@ -3,5 +3,6 @@ export enum StorageKeys { } export interface IState { selectedNetwork?: string; + selectedSubNetworkId?: string; selectedAddress?: string; } diff --git a/packages/extension/src/libs/metrics/index.ts b/packages/extension/src/libs/metrics/index.ts index a83aaa617..b0c338f0a 100644 --- a/packages/extension/src/libs/metrics/index.ts +++ b/packages/extension/src/libs/metrics/index.ts @@ -4,6 +4,7 @@ import Metrics from "./amplitude"; import { BuyEventType, DAppsEventType, + GenericEvents, NFTEventType, NetworkChangeEvents, SendEventType, @@ -13,6 +14,10 @@ import { const metrics = new Metrics(); +const trackGenericEvents = (event: GenericEvents) => { + metrics.track("generic", { event }); +}; + const trackNetworkSelected = ( event: NetworkChangeEvents, options: { provider: ProviderName; network: NetworkNames } @@ -87,4 +92,5 @@ export { trackNFTEvents, trackDAppsEvents, optOutofMetrics, + trackGenericEvents, }; diff --git a/packages/extension/src/libs/metrics/types.ts b/packages/extension/src/libs/metrics/types.ts index b35f023a1..175eb3301 100644 --- a/packages/extension/src/libs/metrics/types.ts +++ b/packages/extension/src/libs/metrics/types.ts @@ -8,6 +8,11 @@ export enum SwapEventType { swapBack = "swap_back", } +export enum GenericEvents { + login_success = "successful_login", + login_error = "login_error", +} + export enum NetworkChangeEvents { NetworkChangePopup = "network_change_popup", NetworkChangeAPI = "network_change_api", diff --git a/packages/extension/src/libs/utils/initialize-wallet.ts b/packages/extension/src/libs/utils/initialize-wallet.ts index 3a67d00ae..3b663caaa 100644 --- a/packages/extension/src/libs/utils/initialize-wallet.ts +++ b/packages/extension/src/libs/utils/initialize-wallet.ts @@ -2,27 +2,49 @@ import KeyRing from "@/libs/keyring/keyring"; import EthereumNetworks from "@/providers/ethereum/networks"; import PolkadotNetworks from "@/providers/polkadot/networks"; import BitcoinNetworks from "@/providers/bitcoin/networks"; -import { WalletType } from "@enkryptcom/types"; -export default async (mnemonic: string, password: string): Promise => { +import KadenaNetworks from "@/providers/kadena/networks"; +import { NetworkNames, WalletType } from "@enkryptcom/types"; +import { getAccountsByNetworkName } from "@/libs/utils/accounts"; +export const initAccounts = async (keyring: KeyRing) => { + const secp256k1btc = await getAccountsByNetworkName(NetworkNames.Bitcoin); + const secp256k1 = await getAccountsByNetworkName(NetworkNames.Ethereum); + const sr25519 = await getAccountsByNetworkName(NetworkNames.Polkadot); + const ed25519kda = await getAccountsByNetworkName(NetworkNames.Kadena); + if (secp256k1.length == 0) + await keyring.saveNewAccount({ + basePath: EthereumNetworks.ethereum.basePath, + name: "EVM Account 1", + signerType: EthereumNetworks.ethereum.signer[0], + walletType: WalletType.mnemonic, + }); + if (sr25519.length == 0) + await keyring.saveNewAccount({ + basePath: PolkadotNetworks.polkadot.basePath, + name: "Substrate Account 1", + signerType: PolkadotNetworks.polkadot.signer[0], + walletType: WalletType.mnemonic, + }); + if (secp256k1btc.length == 0) + await keyring.saveNewAccount({ + basePath: BitcoinNetworks.bitcoin.basePath, + name: "Bitcoin Account 1", + signerType: BitcoinNetworks.bitcoin.signer[0], + walletType: WalletType.mnemonic, + }); + if (ed25519kda.length == 0) + await keyring.saveNewAccount({ + basePath: KadenaNetworks.kadena.basePath, + name: "Kadena Account 1", + signerType: KadenaNetworks.kadena.signer[0], + walletType: WalletType.mnemonic, + }); +}; +export const onboardInitializeWallets = async ( + mnemonic: string, + password: string +): Promise => { const kr = new KeyRing(); await kr.init(mnemonic, password); await kr.unlock(password); - await kr.saveNewAccount({ - basePath: EthereumNetworks.ethereum.basePath, - name: "EVM Account 1", - signerType: EthereumNetworks.ethereum.signer[0], - walletType: WalletType.mnemonic, - }); - await kr.saveNewAccount({ - basePath: PolkadotNetworks.polkadot.basePath, - name: "Substrate Account 1", - signerType: PolkadotNetworks.polkadot.signer[0], - walletType: WalletType.mnemonic, - }); - await kr.saveNewAccount({ - basePath: BitcoinNetworks.bitcoin.basePath, - name: "Bitcoin Account 1", - signerType: BitcoinNetworks.bitcoin.signer[0], - walletType: WalletType.mnemonic, - }); + await initAccounts(kr); }; diff --git a/packages/extension/src/libs/utils/networks.ts b/packages/extension/src/libs/utils/networks.ts index 8de9c623c..021c37e52 100644 --- a/packages/extension/src/libs/utils/networks.ts +++ b/packages/extension/src/libs/utils/networks.ts @@ -3,17 +3,20 @@ import { NetworkNames } from "@enkryptcom/types"; import EthereumNetworks from "@/providers/ethereum/networks"; import PolkadotNetworks from "@/providers/polkadot/networks"; import BitcoinNetworks from "@/providers/bitcoin/networks"; +import KadenaNetworks from "@/providers/kadena/networks"; import { BaseNetwork } from "@/types/base-network"; import CustomNetworksState from "../custom-networks-state"; import { CustomEvmNetwork } from "@/providers/ethereum/types/custom-evm-network"; import Ethereum from "@/providers/ethereum/networks/eth"; import Polkadot from "@/providers/polkadot/networks/polkadot"; import Bitcoin from "@/providers/bitcoin/networks/bitcoin"; +import Kadena from "@/providers/kadena/networks/kadena"; const providerNetworks: Record> = { [ProviderName.ethereum]: EthereumNetworks, [ProviderName.polkadot]: PolkadotNetworks, [ProviderName.bitcoin]: BitcoinNetworks, + [ProviderName.kadena]: KadenaNetworks, [ProviderName.enkrypt]: {}, }; const getAllNetworks = async (): Promise => { @@ -26,6 +29,7 @@ const getAllNetworks = async (): Promise => { return (Object.values(EthereumNetworks) as BaseNetwork[]) .concat(Object.values(PolkadotNetworks) as BaseNetwork[]) .concat(Object.values(BitcoinNetworks) as BaseNetwork[]) + .concat(Object.values(KadenaNetworks) as BaseNetwork[]) .concat(customNetworks); }; const getNetworkByName = async ( @@ -53,10 +57,12 @@ const getProviderNetworkByName = async ( const DEFAULT_EVM_NETWORK_NAME = NetworkNames.Ethereum; const DEFAULT_SUBSTRATE_NETWORK_NAME = NetworkNames.Polkadot; const DEFAULT_BTC_NETWORK_NAME = NetworkNames.Bitcoin; +const DEFAULT_KADENA_NETWORK_NAME = NetworkNames.Kadena; const DEFAULT_EVM_NETWORK = Ethereum; const DEFAULT_SUBSTRATE_NETWORK = Polkadot; const DEFAULT_BTC_NETWORK = Bitcoin; +const DEFAULT_KADENA_NETWORK = Kadena; const POPULAR_NAMES = [ NetworkNames.Bitcoin, @@ -66,6 +72,7 @@ const POPULAR_NAMES = [ NetworkNames.Binance, NetworkNames.Rootstock, NetworkNames.Optimism, + NetworkNames.Kadena, ]; export { getAllNetworks, @@ -78,4 +85,6 @@ export { DEFAULT_EVM_NETWORK, DEFAULT_SUBSTRATE_NETWORK, DEFAULT_BTC_NETWORK, + DEFAULT_KADENA_NETWORK, + DEFAULT_KADENA_NETWORK_NAME, }; diff --git a/packages/extension/src/providers/bitcoin/libs/bip322-message-sign.ts b/packages/extension/src/providers/bitcoin/libs/bip322-message-sign.ts index c9d1dc783..60f62d723 100644 --- a/packages/extension/src/providers/bitcoin/libs/bip322-message-sign.ts +++ b/packages/extension/src/providers/bitcoin/libs/bip322-message-sign.ts @@ -1,6 +1,6 @@ /** - * refference: https://github.com/unisat-wallet/wallet-sdk/blob/master/src/message/bip322-simple.ts - * refference: https://github.com/bitcoinjs/varuint-bitcoin/blob/master/index.js + * reference: https://github.com/unisat-wallet/wallet-sdk/blob/master/src/message/bip322-simple.ts + * reference: https://github.com/bitcoinjs/varuint-bitcoin/blob/master/index.js */ import { BitcoinNetwork, PaymentType } from "../types/bitcoin-network"; diff --git a/packages/extension/src/providers/ethereum/inject.ts b/packages/extension/src/providers/ethereum/inject.ts index 89807fc1e..ca766089a 100644 --- a/packages/extension/src/providers/ethereum/inject.ts +++ b/packages/extension/src/providers/ethereum/inject.ts @@ -147,7 +147,7 @@ const injectDocument = ( const proxiedProvider = new Proxy(provider, ProxyHandler); document["enkrypt"]["providers"][options.name] = provider; if (__IS_OPERA__) { - document[options.name] = proxiedProvider; // Opera expects you to inject immediatly and their wallet switcher will handle conflicts + document[options.name] = proxiedProvider; // Opera expects you to inject immediately and their wallet switcher will handle conflicts } options .sendMessageHandler( diff --git a/packages/extension/src/providers/ethereum/methods/eth_accounts.ts b/packages/extension/src/providers/ethereum/methods/eth_accounts.ts index a7fea6127..a20cc7959 100644 --- a/packages/extension/src/providers/ethereum/methods/eth_accounts.ts +++ b/packages/extension/src/providers/ethereum/methods/eth_accounts.ts @@ -1,110 +1,43 @@ -import { CallbackFunction, MiddlewareFunction } from "@enkryptcom/types"; +import { MiddlewareFunction } from "@enkryptcom/types"; import type EthereumProvider from ".."; import { ProviderRPCRequest } from "@/types/provider"; -import { WindowPromise } from "@/libs/window-promise"; import AccountState from "../libs/accounts-state"; import { getCustomError } from "@/libs/error"; import openOnboard from "@/libs/utils/open-onboard"; -let isAccountAccessPending = false; import { throttle } from "lodash"; const throttledOpenOnboard = throttle(() => openOnboard(), 10000); -const existingErrors: Record = {}; -const pendingPromises: { - payload: ProviderRPCRequest; - res: CallbackFunction; -}[] = []; const method: MiddlewareFunction = async function ( this: EthereumProvider, payload: ProviderRPCRequest, res, next ): Promise { - if ( - payload.method !== "eth_accounts" && - payload.method !== "eth_requestAccounts" && - payload.method !== "eth_coinbase" - ) + if (payload.method !== "eth_accounts" && payload.method !== "eth_coinbase") return next(); else { - if (isAccountAccessPending) { - pendingPromises.push({ - payload, - res, - }); - return; - } - isAccountAccessPending = true; const isInitialized = await this.KeyRing.isInitialized(); - - const handleRemainingPromises = () => { - isAccountAccessPending = false; - if (pendingPromises.length) { - const promi = pendingPromises.pop(); - if (promi) handleAccountAccess(promi.payload, promi.res); + if (payload.options && payload.options.domain) { + if (!isInitialized) { + res(null, payload.method === "eth_coinbase" ? "" : []); + return throttledOpenOnboard(); } - }; - const handleAccountAccess = ( - _payload: ProviderRPCRequest, - _res: CallbackFunction - ) => { - if (_payload.options && _payload.options.domain) { - isAccountAccessPending = true; - if (!isInitialized) { - _res(getCustomError("Enkrypt not initialized")); - throttledOpenOnboard(); - return handleRemainingPromises(); - } - const accountsState = new AccountState(); - if ( - existingErrors[_payload.options.domain] && - existingErrors[_payload.options.domain].time > - new Date().getTime() - 2000 - ) { - _res(existingErrors[_payload.options.domain].error as any); - return handleRemainingPromises(); - } - accountsState - .getApprovedAddresses(_payload.options.domain) - .then((accounts) => { - if (accounts.length) { - _res( - null, - payload.method === "eth_coinbase" ? accounts[0] : accounts - ); - handleRemainingPromises(); - } else { - const windowPromise = new WindowPromise(); - windowPromise - .getResponse( - this.getUIPath(this.UIRoutes.ethConnectDApp.path), - JSON.stringify({ - ..._payload, - params: [this.network.name], - }) - ) - .then(({ error, result }) => { - if (error) { - existingErrors[_payload.options!.domain] = { - time: new Date().getTime(), - error, - }; - return _res(error as any); - } - const accounts = JSON.parse(result || "[]"); - _res( - null, - payload.method === "eth_coinbase" ? accounts[0] : accounts - ); - }) - .finally(handleRemainingPromises); - } - }); - } else { - _res(getCustomError("No domain set!")); - } - }; - handleAccountAccess(payload, res); + const accountsState = new AccountState(); + accountsState + .getApprovedAddresses(payload.options.domain) + .then((accounts) => { + if (accounts.length) { + res( + null, + payload.method === "eth_coinbase" ? accounts[0] : accounts + ); + } else { + res(null, payload.method === "eth_coinbase" ? "" : []); + } + }); + } else { + res(getCustomError("No domain set!")); + } } }; export default method; diff --git a/packages/extension/src/providers/ethereum/methods/eth_requestAccounts.ts b/packages/extension/src/providers/ethereum/methods/eth_requestAccounts.ts new file mode 100644 index 000000000..2436c660a --- /dev/null +++ b/packages/extension/src/providers/ethereum/methods/eth_requestAccounts.ts @@ -0,0 +1,99 @@ +import { CallbackFunction, MiddlewareFunction } from "@enkryptcom/types"; +import type EthereumProvider from ".."; +import { ProviderRPCRequest } from "@/types/provider"; +import { WindowPromise } from "@/libs/window-promise"; +import AccountState from "../libs/accounts-state"; +import { getCustomError } from "@/libs/error"; +import openOnboard from "@/libs/utils/open-onboard"; +let isAccountAccessPending = false; +import { throttle } from "lodash"; + +const throttledOpenOnboard = throttle(() => openOnboard(), 10000); +const existingErrors: Record = {}; +const pendingPromises: { + payload: ProviderRPCRequest; + res: CallbackFunction; +}[] = []; +const method: MiddlewareFunction = async function ( + this: EthereumProvider, + payload: ProviderRPCRequest, + res, + next +): Promise { + if (payload.method !== "eth_requestAccounts") return next(); + else { + if (isAccountAccessPending) { + pendingPromises.push({ + payload, + res, + }); + return; + } + isAccountAccessPending = true; + const isInitialized = await this.KeyRing.isInitialized(); + + const handleRemainingPromises = () => { + isAccountAccessPending = false; + if (pendingPromises.length) { + const promi = pendingPromises.pop(); + if (promi) handleAccountAccess(promi.payload, promi.res); + } + }; + const handleAccountAccess = ( + _payload: ProviderRPCRequest, + _res: CallbackFunction + ) => { + if (_payload.options && _payload.options.domain) { + isAccountAccessPending = true; + if (!isInitialized) { + _res(getCustomError("Enkrypt not initialized")); + throttledOpenOnboard(); + return handleRemainingPromises(); + } + const accountsState = new AccountState(); + if ( + existingErrors[_payload.options.domain] && + existingErrors[_payload.options.domain].time > + new Date().getTime() - 2000 + ) { + _res(existingErrors[_payload.options.domain].error as any); + return handleRemainingPromises(); + } + accountsState + .getApprovedAddresses(_payload.options.domain) + .then((accounts) => { + if (accounts.length) { + _res(null, accounts); + handleRemainingPromises(); + } else { + const windowPromise = new WindowPromise(); + windowPromise + .getResponse( + this.getUIPath(this.UIRoutes.ethConnectDApp.path), + JSON.stringify({ + ..._payload, + params: [this.network.name], + }) + ) + .then(({ error, result }) => { + if (error) { + existingErrors[_payload.options!.domain] = { + time: new Date().getTime(), + error, + }; + return _res(error as any); + } + const accounts = JSON.parse(result || "[]"); + _res(null, accounts); + }) + .finally(handleRemainingPromises); + } + }); + } else { + _res(getCustomError("No domain set!")); + } + }; + handleAccountAccess(payload, res); + } +}; +export default method; diff --git a/packages/extension/src/providers/ethereum/methods/index.ts b/packages/extension/src/providers/ethereum/methods/index.ts index 8f5a84578..d24ab7f8c 100644 --- a/packages/extension/src/providers/ethereum/methods/index.ts +++ b/packages/extension/src/providers/ethereum/methods/index.ts @@ -13,6 +13,7 @@ import watchAsset from "./wallet_watchAsset"; import walletRequestPermissions from "./wallet_requestPermissions"; import ethSendRawTransaction from "./eth_sendRawTransaction"; import enkryptGetPublickKey from "./enkrypt_getPublicKey"; +import ethRequestAccounts from "./eth_requestAccounts"; export default [ ethSendTransaction, ethSign, @@ -29,4 +30,5 @@ export default [ walletRequestPermissions, ethSendRawTransaction, enkryptGetPublickKey, + ethRequestAccounts, ]; diff --git a/packages/extension/src/providers/ethereum/networks/cagaAnkara.ts b/packages/extension/src/providers/ethereum/networks/cagaAnkara.ts new file mode 100644 index 000000000..2f4bb6d85 --- /dev/null +++ b/packages/extension/src/providers/ethereum/networks/cagaAnkara.ts @@ -0,0 +1,24 @@ +import { NetworkNames } from "@enkryptcom/types"; +import { EvmNetwork, EvmNetworkOptions } from "../types/evm-network"; +import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; + +const cagaTestOptions: EvmNetworkOptions = { + name: NetworkNames.CagaAnkara, + name_long: "CAGA Ankara Testnet", + homePage: "https://www.cagacrypto.com/", + blockExplorerTX: + "https://explorer.ankara-cagacrypto.com/transaction/[[txHash]]", + blockExplorerAddr: + "https://explorer.ankara-cagacrypto.com/address/[[address]]", + chainID: "0x11c4a", + isTestNetwork: true, + currencyName: "CAGA", + currencyNameLong: "CAGA", + node: "wss://wss.ankara-cagacrypto.com", + icon: require("./icons/caga.svg"), + activityHandler: wrapActivityHandler(() => Promise.resolve([])), +}; + +const cagaAnkara = new EvmNetwork(cagaTestOptions); + +export default cagaAnkara; diff --git a/packages/extension/src/providers/ethereum/networks/icons/caga.svg b/packages/extension/src/providers/ethereum/networks/icons/caga.svg new file mode 100644 index 000000000..8f84700ef --- /dev/null +++ b/packages/extension/src/providers/ethereum/networks/icons/caga.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/extension/src/providers/ethereum/networks/index.ts b/packages/extension/src/providers/ethereum/networks/index.ts index e45505591..40fc52d31 100644 --- a/packages/extension/src/providers/ethereum/networks/index.ts +++ b/packages/extension/src/providers/ethereum/networks/index.ts @@ -32,6 +32,7 @@ import shibNode from "./shib"; import artheraNode from "./aa"; import formTestnet from "./form-testnet"; import artheraTestNode from "./aat"; +import cagaAnkara from "./cagaAnkara"; export default { sepolia: sepoliaNode, @@ -77,4 +78,5 @@ export default { arthera: artheraNode, formTestnet: formTestnet, artheraTest: artheraTestNode, + cagaAnkara: cagaAnkara, }; diff --git a/packages/extension/src/providers/index.ts b/packages/extension/src/providers/index.ts index 8a9265312..738d6fcec 100644 --- a/packages/extension/src/providers/index.ts +++ b/packages/extension/src/providers/index.ts @@ -1,10 +1,12 @@ import EthereumProvider from "@/providers/ethereum"; import PolkadotProvider from "@/providers/polkadot"; import BitcoinProvider from "@/providers/bitcoin"; +import KadenaProvider from "@/providers/kadena"; import { ProviderName } from "@/types/provider"; export default { [ProviderName.ethereum]: EthereumProvider, [ProviderName.polkadot]: PolkadotProvider, [ProviderName.bitcoin]: BitcoinProvider, + [ProviderName.kadena]: KadenaProvider, }; diff --git a/packages/extension/src/providers/kadena/index.ts b/packages/extension/src/providers/kadena/index.ts new file mode 100644 index 000000000..9dab2a700 --- /dev/null +++ b/packages/extension/src/providers/kadena/index.ts @@ -0,0 +1,83 @@ +import getRequestProvider, { RequestClass } from "@enkryptcom/request"; +import { MiddlewareFunction, OnMessageResponse } from "@enkryptcom/types"; +import Middlewares from "./methods"; +import EventEmitter from "eventemitter3"; +import { + BackgroundProviderInterface, + ProviderName, + ProviderRPCRequest, +} from "@/types/provider"; +import GetUIPath from "@/libs/utils/get-ui-path"; +import PublicKeyRing from "@/libs/keyring/public-keyring"; +import UIRoutes from "./ui/routes/names"; + +import Networks from "./networks"; +import { BaseNetwork } from "@/types/base-network"; +import { KadenaNetwork } from "./types/kadena-network"; + +class KadenaProvider + extends EventEmitter + implements BackgroundProviderInterface +{ + network: KadenaNetwork; + requestProvider: RequestClass; + middlewares: MiddlewareFunction[] = []; + namespace: string; + KeyRing: PublicKeyRing; + UIRoutes = UIRoutes; + toWindow: (message: string) => void; + + constructor( + toWindow: (message: string) => void, + network: KadenaNetwork = Networks.kadena + ) { + super(); + this.network = network; + this.toWindow = toWindow; + this.setMiddleWares(); + this.requestProvider = getRequestProvider("", this.middlewares); + this.requestProvider.on("notification", (notif: any) => { + this.sendNotification(JSON.stringify(notif)); + }); + this.namespace = ProviderName.kadena; + this.KeyRing = new PublicKeyRing(); + } + + private setMiddleWares(): void { + this.middlewares = Middlewares.map((mw) => mw.bind(this)); + } + + setRequestProvider(network: BaseNetwork): void { + this.network = network as KadenaNetwork; + this.requestProvider.changeNetwork(network.node); + } + + request(request: ProviderRPCRequest): Promise { + return this.requestProvider + .request(request) + .then((res: any) => { + return { + result: JSON.stringify(res), + }; + }) + .catch((e: { message: any }) => { + return { + error: JSON.stringify(e.message), + }; + }); + } + + async sendNotification(notif: string): Promise { + return this.toWindow(notif); + } + + async isPersistentEvent(): Promise { + return false; + } + + getUIPath(page: string): string { + return GetUIPath(page, this.namespace); + } +} + +export default KadenaProvider; diff --git a/packages/extension/src/providers/kadena/inject.ts b/packages/extension/src/providers/kadena/inject.ts new file mode 100644 index 000000000..6ba8cee93 --- /dev/null +++ b/packages/extension/src/providers/kadena/inject.ts @@ -0,0 +1,57 @@ +import EventEmitter from "eventemitter3"; +import { handleIncomingMessage } from "./libs/message-handler"; +import { EthereumRequest, EthereumResponse } from "@/providers/ethereum/types"; +import { + ProviderName, + ProviderOptions, + ProviderType, + ProviderInterface, + SendMessageHandler, +} from "@/types/provider"; +import { EnkryptWindow } from "@/types/globals"; +import { KadenaNetworks } from "./types"; + +export class Provider extends EventEmitter implements ProviderInterface { + connected: boolean; + name: ProviderName; + type: ProviderType; + version = __VERSION__; + autoRefreshOnNetworkChange = false; + networks: typeof KadenaNetworks; + sendMessageHandler: SendMessageHandler; + + constructor(options: ProviderOptions) { + super(); + this.connected = true; + this.name = options.name; + this.type = options.type; + this.networks = KadenaNetworks; + this.sendMessageHandler = options.sendMessageHandler; + } + + async request(request: EthereumRequest): Promise { + const res = (await this.sendMessageHandler( + this.name, + JSON.stringify(request) + )) as EthereumResponse; + return res; + } + + isConnected(): boolean { + return this.connected; + } + + handleMessage(msg: string): void { + handleIncomingMessage(this, msg); + } +} + +const injectDocument = ( + document: EnkryptWindow | Window, + options: ProviderOptions +): void => { + const provider = new Provider(options); + document["enkrypt"]["providers"][options.name] = provider; +}; + +export default injectDocument; diff --git a/packages/extension/src/providers/kadena/libs/accounts-state/index.ts b/packages/extension/src/providers/kadena/libs/accounts-state/index.ts new file mode 100644 index 000000000..486805a88 --- /dev/null +++ b/packages/extension/src/providers/kadena/libs/accounts-state/index.ts @@ -0,0 +1,60 @@ +import { InternalStorageNamespace } from "@/types/provider"; +import BrowserStorage from "@/libs/common/browser-storage"; +import { IState, StorageKeys } from "./types"; + +class AccountState { + #storage: BrowserStorage; + constructor() { + this.#storage = new BrowserStorage( + InternalStorageNamespace.kadenaAccountsState + ); + } + async addApprovedDomain(domain: string): Promise { + const state = await this.getStateByDomain(domain); + state.isApproved = true; + await this.setState(state, domain); + } + async removeApprovedDomain(domain: string): Promise { + const state = await this.getStateByDomain(domain); + state.isApproved = false; + await this.setState(state, domain); + } + async isApproved(domain: string): Promise { + const state = await this.getStateByDomain(domain); + return state.isApproved; + } + async deleteState(domain: string): Promise { + const allStates = await this.getAllStates(); + if (allStates[domain]) { + delete allStates[domain]; + await this.#storage.set(StorageKeys.accountsState, allStates); + } + } + async isConnected(domain: string): Promise { + return this.getStateByDomain(domain).then((res) => res.isApproved); + } + async deleteAllStates(): Promise { + return await this.#storage.remove(StorageKeys.accountsState); + } + async setState(state: IState, domain: string): Promise { + const allStates = await this.getAllStates(); + allStates[domain] = state; + await this.#storage.set(StorageKeys.accountsState, allStates); + } + async getStateByDomain(domain: string): Promise { + const allStates: Record = await this.getAllStates(); + if (!allStates[domain]) + return { + isApproved: false, + }; + else return allStates[domain]; + } + async getAllStates(): Promise> { + const allStates: Record = await this.#storage.get( + StorageKeys.accountsState + ); + if (!allStates) return {}; + return allStates; + } +} +export default AccountState; diff --git a/packages/extension/src/providers/kadena/libs/accounts-state/types.ts b/packages/extension/src/providers/kadena/libs/accounts-state/types.ts new file mode 100644 index 000000000..b20ca5a8a --- /dev/null +++ b/packages/extension/src/providers/kadena/libs/accounts-state/types.ts @@ -0,0 +1,6 @@ +export enum StorageKeys { + accountsState = "kadena-accounts-state", +} +export interface IState { + isApproved: boolean; +} diff --git a/packages/extension/src/providers/kadena/libs/activity-handlers/index.ts b/packages/extension/src/providers/kadena/libs/activity-handlers/index.ts new file mode 100644 index 000000000..6212f5ac8 --- /dev/null +++ b/packages/extension/src/providers/kadena/libs/activity-handlers/index.ts @@ -0,0 +1,3 @@ +import kadenaScanActivity from "./providers/kadena"; + +export { kadenaScanActivity }; diff --git a/packages/extension/src/providers/kadena/libs/activity-handlers/providers/kadena/configs.ts b/packages/extension/src/providers/kadena/libs/activity-handlers/providers/kadena/configs.ts new file mode 100644 index 000000000..0f84e345d --- /dev/null +++ b/packages/extension/src/providers/kadena/libs/activity-handlers/providers/kadena/configs.ts @@ -0,0 +1,13 @@ +import { NetworkNames } from "@enkryptcom/types"; + +const NetworkEndpoints = { + [NetworkNames.Kadena]: "https://estats.chainweb.com/", + [NetworkNames.KadenaTestnet]: "https://estats.testnet.chainweb.com/", +}; + +const NetworkTtls = { + [NetworkNames.Kadena]: 30000, + [NetworkNames.KadenaTestnet]: 30000, +}; + +export { NetworkEndpoints, NetworkTtls }; diff --git a/packages/extension/src/providers/kadena/libs/activity-handlers/providers/kadena/index.ts b/packages/extension/src/providers/kadena/libs/activity-handlers/providers/kadena/index.ts new file mode 100644 index 000000000..d3c9ff4b8 --- /dev/null +++ b/packages/extension/src/providers/kadena/libs/activity-handlers/providers/kadena/index.ts @@ -0,0 +1,100 @@ +import cacheFetch from "@/libs/cache-fetch"; +import MarketData from "@/libs/market-data"; +import { Activity, ActivityStatus, ActivityType } from "@/types/activity"; +import { BaseNetwork } from "@/types/base-network"; +import { NetworkEndpoints, NetworkTtls } from "./configs"; +import { toBase } from "@enkryptcom/utils"; + +const getAddressActivity = async ( + address: string, + endpoint: string, + ttl: number, + height: number +): Promise => { + const url = `${endpoint}txs/account/${address}?minheight=${height}&limit=200&token=coin`; + return cacheFetch({ url }, ttl) + .then((res) => { + return res ? res : []; + }) + .catch((error) => { + console.error("Failed to fetch activity:", error); + return []; + }); +}; + +export default async ( + network: BaseNetwork, + address: string +): Promise => { + const networkName = network.name as keyof typeof NetworkEndpoints; + const enpoint = NetworkEndpoints[networkName]; + const ttl = NetworkTtls[networkName]; + const activities = await getAddressActivity( + address, + enpoint, + ttl, + 0 // lastActivity?.rawInfo?.height ?? 0 + ); + + let price = "0"; + + if (network.coingeckoID) { + const marketData = new MarketData(); + await marketData + .getTokenPrice(network.coingeckoID) + .then((mdata) => (price = mdata || "0")); + } + + const groupActivities = activities.reduce((acc: any, activity: any) => { + if (!acc[activity.requestKey]) { + acc[activity.requestKey] = activity; + } + if (activity.idx === 1) { + acc[activity.requestKey] = activity; + } + return acc; + }, {}); + + return Object.values(groupActivities).map((activity: any, i: number) => { + const rawAmount = toBase( + activity.amount + ? parseFloat(activity.amount).toFixed(network.decimals) + : "0", + network.decimals + ); + // note: intentionally not using fromAccount === some-value + // I want to match both null and "" in fromAccount/toAccount + // actual values will be a (truthy) string + let { fromAccount, toAccount } = activity; + if (!fromAccount && activity.crossChainAccount) { + fromAccount = activity.crossChainAccount; + } + if (!toAccount && activity.crossChainAccount) { + toAccount = activity.crossChainAccount; + } + return { + nonce: i.toString(), + from: fromAccount, + to: toAccount, + isIncoming: activity.fromAccount !== address, + network: network.name, + rawInfo: activity, + chainId: activity.chain.toString(), + crossChainId: activity.crossChainId, + status: + activity.idx === 1 ? ActivityStatus.success : ActivityStatus.failed, + timestamp: new Date(activity.blockTime).getTime(), + value: rawAmount, + transactionHash: activity.requestKey, + type: ActivityType.transaction, + token: { + decimals: network.decimals, + icon: network.icon, + name: network.currencyNameLong, + symbol: + activity.token !== "coin" ? activity.token : network.currencyName, + price: price, + }, + }; + }); +}; diff --git a/packages/extension/src/providers/kadena/libs/api.ts b/packages/extension/src/providers/kadena/libs/api.ts new file mode 100644 index 000000000..16839c9a5 --- /dev/null +++ b/packages/extension/src/providers/kadena/libs/api.ts @@ -0,0 +1,136 @@ +import { KadenaRawInfo } from "@/types/activity"; +import { ProviderAPIInterface } from "@/types/provider"; +import { KadenaNetworkOptions } from "../types/kadena-network"; +import { + ICommand, + IUnsignedCommand, + ICommandResult, + ITransactionDescriptor, + createClient, + Pact, + ChainId, +} from "@kadena/client"; +import { toBase } from "@enkryptcom/utils"; +import DomainState from "@/libs/domain-state"; + +class API implements ProviderAPIInterface { + decimals: number; + node: string; + networkId: string; + chainId: string; + apiHost: string; + domainState: DomainState; + displayAddress: (address: string) => string; + + constructor(node: string, options: KadenaNetworkOptions) { + this.decimals = options.decimals; + this.node = node; + this.networkId = options.kadenaApiOptions.networkId; + this.chainId = options.kadenaApiOptions.chainId; + this.apiHost = `${node}/${this.networkId}/chain/${this.chainId}/pact`; + this.displayAddress = options.displayAddress; + this.domainState = new DomainState(); + } + + public get api() { + return this; + } + + private getApiHost(chainId: string) { + return `${this.node}/${this.networkId}/chain/${chainId}/pact`; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + async init(): Promise {} + + async getChainId(): Promise { + return this.domainState.getSelectedSubNetWork().then((id) => { + if (id) return id; + return "0"; + }); + } + + async getTransactionStatus(requestKey: string): Promise { + const chainId = await this.getChainId(); + const networkId = this.networkId; + const { pollStatus } = createClient(this.getApiHost(chainId)); + const responses = await pollStatus({ + requestKey, + networkId, + chainId: chainId as ChainId, + }); + return responses[requestKey]; + } + + async getBalanceByChainId(address: string, chainId: string): Promise { + const balance = await this.getBalanceAPI( + this.displayAddress(address), + chainId + ); + + if (balance.result.status === "failure") { + const error = balance.result.error as { message: string | undefined }; + const message = error.message ?? "Unknown error retrieving balances"; + // expected error when account does not exist on a chain (balance == 0) + if (message.includes("row not found")) { + return toBase("0", this.decimals); + } + throw new Error(message); + } + + const balanceValue = parseFloat(balance.result.data.toString()).toFixed( + this.decimals + ); + + return toBase(balanceValue, this.decimals); + } + + async getBalance(address: string): Promise { + const chainId = await this.getChainId(); + return this.getBalanceByChainId(address, chainId); + } + + async getBalanceAPI(account: string, chainId: string) { + const transaction = Pact.builder + .execution(Pact.modules.coin["get-balance"](account)) + .setMeta({ chainId: chainId as ChainId }) + .setNetworkId(this.networkId) + .createTransaction(); + + return this.dirtyRead(transaction); + } + + async sendLocalTransaction( + signedTranscation: ICommand + ): Promise { + const chainId = await this.getChainId(); + const client = createClient(this.getApiHost(chainId)); + return client.local(signedTranscation as ICommand); + } + + async sendTransaction( + signedTranscation: ICommand + ): Promise { + const chainId = await this.getChainId(); + const client = createClient(this.getApiHost(chainId)); + return client.submit(signedTranscation as ICommand); + } + + async listen( + transactionDescriptor: ITransactionDescriptor + ): Promise { + const chainId = await this.getChainId(); + const client = createClient(this.getApiHost(chainId)); + return client.listen(transactionDescriptor); + } + + async dirtyRead( + signedTranscation: ICommand | IUnsignedCommand + ): Promise { + const chainId = await this.getChainId(); + const client = createClient(this.getApiHost(chainId)); + return client.dirtyRead(signedTranscation); + } +} + +export default API; diff --git a/packages/extension/src/providers/kadena/libs/message-handler.ts b/packages/extension/src/providers/kadena/libs/message-handler.ts new file mode 100644 index 000000000..3b1e62315 --- /dev/null +++ b/packages/extension/src/providers/kadena/libs/message-handler.ts @@ -0,0 +1,34 @@ +import { + ProviderMessage, + MessageMethod, + EmitEvent, +} from "@/providers/kadena/types"; + +import { + KadenaProvider, + handleIncomingMessage as handleIncomingMessageType, +} from "@/types/provider"; + +const handleIncomingMessage: handleIncomingMessageType = ( + provider, + message +): void => { + try { + const _provider = provider as KadenaProvider; + const jsonMsg = JSON.parse(message) as ProviderMessage; + + if (jsonMsg.method === MessageMethod.changeAddress) { + const address = jsonMsg.params[0] as string; + _provider.emit(EmitEvent.accountsChanged, [address]); + } else if (jsonMsg.method === MessageMethod.changeNetwork) { + const networkId = jsonMsg.params[0] as string; + _provider.emit(EmitEvent.networkChanged, [networkId]); + } else { + console.error(`Unable to process message: ${message}`); + } + } catch (e) { + console.error(e); + } +}; + +export { handleIncomingMessage }; diff --git a/packages/extension/src/providers/kadena/libs/network.ts b/packages/extension/src/providers/kadena/libs/network.ts new file mode 100644 index 000000000..f017abc00 --- /dev/null +++ b/packages/extension/src/providers/kadena/libs/network.ts @@ -0,0 +1,21 @@ +import KDANetworks from "../networks"; +import { isHex } from "web3-utils"; + +export const getNetworkInfo = (networkName: string) => { + const networkObject = Object.values(KDANetworks).find( + (n) => n.name === networkName + ); + + return { + name: networkName, + node: networkObject?.node || "", + networkId: networkObject?.options.kadenaApiOptions.networkId || "", + chainId: networkObject?.options.kadenaApiOptions.chainId || "", + explorer: networkObject?.options.blockExplorerTX || "", + }; +}; + +export const isValidAddress = (address: string) => + address.startsWith("k:") && + isHex(address.replace("k:", "")) && + address.length === 66; diff --git a/packages/extension/src/providers/kadena/methods/index.ts b/packages/extension/src/providers/kadena/methods/index.ts new file mode 100644 index 000000000..ac8d42ff6 --- /dev/null +++ b/packages/extension/src/providers/kadena/methods/index.ts @@ -0,0 +1,11 @@ +import kdaRequestAccounts from "./kda_requestAccounts"; +import kdaGetBalance from "./kda_getBalance"; +import kdaSignTransaction from "./kda_signTransaction"; +import kdaSwitchNetwork from "./kda_switchNetwork"; + +export default [ + kdaRequestAccounts, + kdaGetBalance, + kdaSignTransaction, + kdaSwitchNetwork, +]; diff --git a/packages/extension/src/providers/kadena/methods/kda_getBalance.ts b/packages/extension/src/providers/kadena/methods/kda_getBalance.ts new file mode 100644 index 000000000..f3acb9988 --- /dev/null +++ b/packages/extension/src/providers/kadena/methods/kda_getBalance.ts @@ -0,0 +1,29 @@ +import { MiddlewareFunction } from "@enkryptcom/types"; +import KadenaProvider from ".."; +import { ProviderRPCRequest } from "@/types/provider"; +import { getCustomError } from "@/libs/error"; +import type KadenaAPI from "@/providers/kadena/libs/api"; + +const method: MiddlewareFunction = function ( + this: KadenaProvider, + payload: ProviderRPCRequest, + res, + next +): void { + if (payload.method !== "kda_getBalance") return next(); + else { + if (!payload.params || payload.params.length < 2) { + return res(getCustomError("kda_getBalance: invalid params")); + } + + const address = payload.params[0]; + const chainId = + payload.params[1] ?? this.network.options.kadenaApiOptions.chainId; + this.network.api().then((api) => { + (api as KadenaAPI).getBalanceByChainId(address, chainId).then((bal) => { + res(null, bal); + }); + }); + } +}; +export default method; diff --git a/packages/extension/src/providers/kadena/methods/kda_requestAccounts.ts b/packages/extension/src/providers/kadena/methods/kda_requestAccounts.ts new file mode 100644 index 000000000..ff111b049 --- /dev/null +++ b/packages/extension/src/providers/kadena/methods/kda_requestAccounts.ts @@ -0,0 +1,146 @@ +import { + CallbackFunction, + MiddlewareFunction, + SignerType, +} from "@enkryptcom/types"; +import { WindowPromise } from "@/libs/window-promise"; +import PublicKeyRing from "@/libs/keyring/public-keyring"; +import DomainState from "@/libs/domain-state"; +import { getCustomError } from "@/libs/error"; +import { ProviderRPCRequest } from "@/types/provider"; + +import KadenaProvider from ".."; +import AccountState from "../libs/accounts-state"; +import { KadenaNetworks } from "../types"; +import { getNetworkInfo } from "../libs/network"; + +let isAccountAccessPending = false; + +const pendingPromises: { + payload: ProviderRPCRequest; + res: CallbackFunction; +}[] = []; + +const method: MiddlewareFunction = function ( + this: KadenaProvider, + payload: ProviderRPCRequest, + res, + next +): void { + if (payload.method !== "kda_requestAccounts") return next(); + else { + if (isAccountAccessPending) { + pendingPromises.push({ + payload, + res, + }); + return; + } + + isAccountAccessPending = true; + + const handleRemainingPromises = () => { + isAccountAccessPending = false; + + if (pendingPromises.length) { + const promi = pendingPromises.pop(); + if (promi) handleAccountAccess(promi.payload, promi.res); + } + }; + + const getAccounts = () => { + const domainState = new DomainState(); + const publicKeyring = new PublicKeyRing(); + + const selectedAddressPromise = domainState.getSelectedAddress(); + const selectedNetworkPromise = domainState.getSelectedNetWork(); + const accountsPromise = publicKeyring.getAccounts([ + SignerType.ed25519kda, + ]); + + return Promise.all([ + selectedAddressPromise, + selectedNetworkPromise, + accountsPromise, + ]).then(([selectedAddress, selectedNetwork, accounts]) => { + const selectedNetworkName = Object.values(KadenaNetworks).find( + (n) => n === selectedNetwork + ); + + const account = accounts.find((acc) => acc.address === selectedAddress); + + return { + selectedNetwork: selectedNetworkName + ? getNetworkInfo(selectedNetworkName) + : null, + selectedAccountAddress: this.network.displayAddress( + account?.address || "" + ), + accounts: accounts.map((acc) => { + return { + address: this.network.displayAddress(acc.address), + publicKey: acc.publicKey.replace("0x", ""), + genesisHash: "", + name: acc.name, + type: acc.signerType, + }; + }), + }; + }); + }; + + const handleAccountAccess = ( + _payload: ProviderRPCRequest, + _res: CallbackFunction + ) => { + if (_payload.options && _payload.options.domain) { + isAccountAccessPending = true; + const accountsState = new AccountState(); + + accountsState + .isApproved(_payload.options.domain) + .then((isApproved) => { + if (isApproved) { + getAccounts() + .then((acc) => { + _res(null, acc); + }) + .catch((err) => { + throw err; + }); + } else { + const windowPromise = new WindowPromise(); + windowPromise + .getResponse( + this.getUIPath(this.UIRoutes.kdaAccounts.path), + JSON.stringify(payload) + ) + .then(({ error }) => { + if (error) { + throw error; + } else { + getAccounts() + .then((acc) => { + _res(null, acc); + }) + .catch((err) => { + throw err; + }); + } + }); + } + }) + .catch((err) => { + _res(err); + }) + .finally(handleRemainingPromises); + } else { + _res(getCustomError("No domain set!")); + handleRemainingPromises(); + } + }; + + handleAccountAccess(payload, res); + } +}; +export default method; diff --git a/packages/extension/src/providers/kadena/methods/kda_signTransaction.ts b/packages/extension/src/providers/kadena/methods/kda_signTransaction.ts new file mode 100644 index 000000000..17463ec77 --- /dev/null +++ b/packages/extension/src/providers/kadena/methods/kda_signTransaction.ts @@ -0,0 +1,42 @@ +import { MiddlewareFunction } from "@enkryptcom/types"; +import KadenaProvider from ".."; +import { WindowPromise } from "@/libs/window-promise"; +import { ProviderRPCRequest } from "@/types/provider"; +import { getCustomError } from "@/libs/error"; +const method: MiddlewareFunction = function ( + this: KadenaProvider, + payload: ProviderRPCRequest, + res, + next +): void { + if (payload.method !== "kda_signTransaction") return next(); + else { + if (!payload.params?.length) { + return res(getCustomError("Missing Params: kda_signTransaction")); + } + + const reqPayload = payload.params[0]; + + this.KeyRing.getAccount(reqPayload.address.replace("k:", "0x")) + .then((account) => { + const windowPromise = new WindowPromise(); + + windowPromise + .getResponse( + this.getUIPath(this.UIRoutes.kdaSignMessage.path), + JSON.stringify({ + ...payload, + params: [reqPayload, account], + }), + true + ) + .then(({ error, result }) => { + if (error) return res(error); + res(null, result as string); + }); + }) + .catch(res); + } +}; + +export default method; diff --git a/packages/extension/src/providers/kadena/methods/kda_switchNetwork.ts b/packages/extension/src/providers/kadena/methods/kda_switchNetwork.ts new file mode 100644 index 000000000..66161776a --- /dev/null +++ b/packages/extension/src/providers/kadena/methods/kda_switchNetwork.ts @@ -0,0 +1,60 @@ +import { getCustomError } from "@/libs/error"; +import { sendToBackgroundFromBackground } from "@/libs/messenger/extension"; +import { InternalMethods } from "@/types/messenger"; +import { ProviderRPCRequest } from "@/types/provider"; +import { MiddlewareFunction } from "@enkryptcom/types"; +import DomainState from "@/libs/domain-state"; + +import KadenaProvider from ".."; +import KDANetworks from "../networks"; +import { KadenaNetworks } from "../types"; +import { getNetworkInfo } from "../libs/network"; + +const method: MiddlewareFunction = function ( + this: KadenaProvider, + payload: ProviderRPCRequest, + res, + next +): void { + if (payload.method !== "kda_switchNetwork") return next(); + else { + if ( + !payload.params || + payload.params.length < 1 || + !Object.values(KadenaNetworks).includes(payload.params[0]) + ) { + return res(getCustomError("kda_switchNetwork: invalid params")); + } + + const allNetworks = Object.values(KDANetworks); + const validNetwork = allNetworks.find( + (net) => net.name === payload.params![0] + ); + + if (validNetwork) { + sendToBackgroundFromBackground({ + message: JSON.stringify({ + method: InternalMethods.changeNetwork, + params: [validNetwork.name], + }), + provider: validNetwork.provider, + tabId: payload.options?.tabId, + }).then(() => { + const domainState = new DomainState(); + domainState + .setSelectedNetwork(validNetwork.name) + .then(() => res(null, getNetworkInfo(validNetwork.name))); + }); + } else { + return res( + getCustomError( + `kda_switchNetwork: provided network ${ + payload.params![0] + } not supported` + ) + ); + } + } +}; + +export default method; diff --git a/packages/extension/src/providers/kadena/networks/icons/kadena-kda-logo.svg b/packages/extension/src/providers/kadena/networks/icons/kadena-kda-logo.svg new file mode 100644 index 000000000..5b2c7066b --- /dev/null +++ b/packages/extension/src/providers/kadena/networks/icons/kadena-kda-logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/networks/index.ts b/packages/extension/src/providers/kadena/networks/index.ts new file mode 100644 index 000000000..42662492e --- /dev/null +++ b/packages/extension/src/providers/kadena/networks/index.ts @@ -0,0 +1,7 @@ +import kadena from "./kadena"; +import kadenaTestnet from "./kadena-testnet"; + +export default { + kadena, + kadenaTestnet, +}; diff --git a/packages/extension/src/providers/kadena/networks/kadena-testnet.ts b/packages/extension/src/providers/kadena/networks/kadena-testnet.ts new file mode 100644 index 000000000..c42e09c87 --- /dev/null +++ b/packages/extension/src/providers/kadena/networks/kadena-testnet.ts @@ -0,0 +1,41 @@ +import { NetworkNames } from "@enkryptcom/types"; +import { KadenaNetwork, KadenaNetworkOptions } from "../types/kadena-network"; +import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import { kadenaScanActivity } from "../libs/activity-handlers"; +import { isValidAddress } from "../libs/network"; + +const kadenaOptions: KadenaNetworkOptions = { + name: NetworkNames.KadenaTestnet, + name_long: "Kadena Testnet", + homePage: "https://kadena.io/", + blockExplorerTX: "https://explorer.chainweb.com/testnet/tx/[[txHash]]", + blockExplorerAddr: + "https://explorer.chainweb.com/testnet/account/[[address]]?token=coin", + isTestNetwork: true, + currencyName: "KDA", + currencyNameLong: "Kadena", + icon: require("./icons/kadena-kda-logo.svg"), + decimals: 12, + prefix: 0, + node: "https://api.testnet.chainweb.com/chainweb/0.0", + kadenaApiOptions: { + networkId: "testnet04", + chainId: "1", + }, + subNetworks: Array(20) + .fill("") + .map((_, idx) => { + return { + id: idx.toString(), + name: `Chain ${idx}`, + }; + }), + buyLink: "https://tools.kadena.io/faucet/new", + activityHandler: wrapActivityHandler(kadenaScanActivity), + displayAddress: (address: string) => address.replace("0x", "k:"), + isAddress: isValidAddress, +}; + +const kadenaTestnet = new KadenaNetwork(kadenaOptions); + +export default kadenaTestnet; diff --git a/packages/extension/src/providers/kadena/networks/kadena.ts b/packages/extension/src/providers/kadena/networks/kadena.ts new file mode 100644 index 000000000..d122438a8 --- /dev/null +++ b/packages/extension/src/providers/kadena/networks/kadena.ts @@ -0,0 +1,42 @@ +import { CoingeckoPlatform, NetworkNames } from "@enkryptcom/types"; +import { KadenaNetwork, KadenaNetworkOptions } from "../types/kadena-network"; +import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import { kadenaScanActivity } from "../libs/activity-handlers"; +import { isValidAddress } from "../libs/network"; + +const kadenaOptions: KadenaNetworkOptions = { + name: NetworkNames.Kadena, + name_long: "Kadena", + homePage: "https://kadena.io/", + blockExplorerTX: "https://explorer.chainweb.com/mainnet/tx/[[txHash]]", + blockExplorerAddr: + "https://explorer.chainweb.com/mainnet/account/[[address]]?token=coin", + isTestNetwork: false, + currencyName: "KDA", + currencyNameLong: "Kadena", + icon: require("./icons/kadena-kda-logo.svg"), + decimals: 12, + prefix: 0, + node: "https://api.chainweb.com/chainweb/0.0", + kadenaApiOptions: { + networkId: "mainnet01", + chainId: "1", + }, + coingeckoID: "kadena", + subNetworks: Array(20) + .fill("") + .map((_, idx) => { + return { + id: idx.toString(), + name: `Chain ${idx}`, + }; + }), + coingeckoPlatform: CoingeckoPlatform.Kadena, + activityHandler: wrapActivityHandler(kadenaScanActivity), + displayAddress: (address: string) => address.replace("0x", "k:"), + isAddress: isValidAddress, +}; + +const kadena = new KadenaNetwork(kadenaOptions); + +export default kadena; diff --git a/packages/extension/src/providers/kadena/types/index.ts b/packages/extension/src/providers/kadena/types/index.ts new file mode 100644 index 000000000..2170ec354 --- /dev/null +++ b/packages/extension/src/providers/kadena/types/index.ts @@ -0,0 +1,29 @@ +import { NetworkNames } from "@enkryptcom/types"; +import type { Provider as InjectedProvider } from "../inject"; + +export const KadenaNetworks = { + KDA: NetworkNames.Kadena, + KDATestnet: NetworkNames.KadenaTestnet, +}; + +export interface KadenaApiOptions { + networkId: string; + chainId: string; +} + +export interface ProviderMessage { + method: MessageMethod; + params: Array; +} + +export enum MessageMethod { + changeAddress = "changeAddress", + changeNetwork = "changeNetwork", +} + +export enum EmitEvent { + accountsChanged = "accountsChanged", + networkChanged = "networkChanged", +} + +export { InjectedProvider }; diff --git a/packages/extension/src/providers/kadena/types/kadena-network.ts b/packages/extension/src/providers/kadena/types/kadena-network.ts new file mode 100644 index 000000000..785afba38 --- /dev/null +++ b/packages/extension/src/providers/kadena/types/kadena-network.ts @@ -0,0 +1,142 @@ +import { Activity } from "@/types/activity"; +import { + BaseNetwork, + BaseNetworkOptions, + SubNetworkOptions, +} from "@/types/base-network"; +import { BaseTokenOptions } from "@/types/base-token"; +import { AssetsType, ProviderName } from "@/types/provider"; +import { CoingeckoPlatform, NetworkNames, SignerType } from "@enkryptcom/types"; +import KadenaAPI from "@/providers/kadena/libs/api"; + +import createIcon from "@/providers/ethereum/libs/blockies"; +import MarketData from "@/libs/market-data"; +import { CoinGeckoTokenMarket } from "@/libs/market-data/types"; +import Sparkline from "@/libs/sparkline"; +import { + formatFloatingPointValue, + formatFiatValue, +} from "@/libs/utils/number-formatter"; +import { fromBase } from "@enkryptcom/utils"; +import BigNumber from "bignumber.js"; +import { KDABaseToken, KDAToken } from "./kda-token"; +import { KadenaApiOptions } from "."; + +export interface KadenaNetworkOptions { + name: NetworkNames; + name_long: string; + homePage: string; + blockExplorerTX: string; + blockExplorerAddr: string; + isTestNetwork: boolean; + currencyName: string; + currencyNameLong: string; + icon: string; + decimals: number; + prefix: number; + node: string; + buyLink?: string | undefined; + kadenaApiOptions: KadenaApiOptions; + displayAddress: (address: string) => string; + coingeckoID?: string; + coingeckoPlatform?: CoingeckoPlatform; + isAddress: (address: string) => boolean; + subNetworks: SubNetworkOptions[]; + activityHandler: ( + network: BaseNetwork, + address: string + ) => Promise; +} + +export class KadenaNetwork extends BaseNetwork { + public options: KadenaNetworkOptions; + + private activityHandler: ( + network: BaseNetwork, + address: string + ) => Promise; + + public isAddress: (address: string) => boolean; + + constructor(options: KadenaNetworkOptions) { + const api = async () => { + const api = new KadenaAPI(options.node, options); + return api; + }; + + const baseOptions: BaseNetworkOptions = { + basePath: "m/44'/626'", + identicon: createIcon, + signer: [SignerType.ed25519kda], + provider: ProviderName.kadena, + api, + ...options, + }; + + super(baseOptions); + this.options = options; + this.isAddress = options.isAddress; + this.activityHandler = options.activityHandler; + } + + public async getAllTokens(pubkey: string): Promise { + const assets = await this.getAllTokenInfo(pubkey); + + return assets.map((token) => { + const bTokenOptions: BaseTokenOptions = { + decimals: token.decimals, + icon: token.icon, + name: token.name, + symbol: token.symbol, + balance: token.balance, + price: token.value, + coingeckoID: this.coingeckoID, + }; + + return new KDAToken(bTokenOptions); + }); + } + + public async getAllTokenInfo(pubkey: string): Promise { + const balance = await (await this.api()).getBalance(pubkey); + let marketData: (CoinGeckoTokenMarket | null)[] = []; + + if (this.coingeckoID) { + const market = new MarketData(); + marketData = await market.getMarketData([this.coingeckoID]); + } + + const userBalance = fromBase(balance, this.decimals); + const usdBalance = new BigNumber(userBalance).times( + marketData.length ? marketData[0]!.current_price : 0 + ); + + const nativeAsset: AssetsType = { + balance: balance, + balancef: formatFloatingPointValue(userBalance).value, + balanceUSD: usdBalance.toNumber(), + balanceUSDf: formatFiatValue(usdBalance.toString()).value, + icon: this.icon, + name: this.name_long, + symbol: this.currencyName, + value: marketData.length ? marketData[0]!.current_price.toString() : "0", + valuef: formatFiatValue( + marketData.length ? marketData[0]!.current_price.toString() : "0" + ).value, + contract: "", + decimals: this.decimals, + sparkline: marketData.length + ? new Sparkline(marketData[0]!.sparkline_in_7d.price, 25).dataValues + : "", + priceChangePercentage: marketData.length + ? marketData[0]!.price_change_percentage_7d_in_currency + : 0, + }; + + return [nativeAsset]; + } + + public getAllActivity(address: string): Promise { + return this.activityHandler(this, address); + } +} diff --git a/packages/extension/src/providers/kadena/types/kda-token.ts b/packages/extension/src/providers/kadena/types/kda-token.ts new file mode 100644 index 000000000..1d286f7d4 --- /dev/null +++ b/packages/extension/src/providers/kadena/types/kda-token.ts @@ -0,0 +1,128 @@ +import { BaseToken, BaseTokenOptions } from "@/types/base-token"; +import KadenaAPI from "@/providers/kadena/libs/api"; +import { + ChainId, + ICommand, + Pact, + addSignatures, + readKeyset, +} from "@kadena/client"; +import { EnkryptAccount } from "@enkryptcom/types"; +import { blake2AsU8a } from "@polkadot/util-crypto"; +import { KadenaNetwork } from "./kadena-network"; +import { TransactionSigner } from "../ui/libs/signer"; +import { bufferToHex } from "@enkryptcom/utils"; + +export abstract class KDABaseToken extends BaseToken { + public abstract buildTransaction( + to: string, + from: EnkryptAccount, + amount: string, + network: KadenaNetwork + ): Promise; + + public abstract getAccountDetails( + account: string, + network: KadenaNetwork + ): Promise; + + public abstract getBalance( + api: KadenaAPI, + pubkey: string, + chainId?: string + ): Promise; +} + +export class KDAToken extends KDABaseToken { + constructor(options: BaseTokenOptions) { + super(options); + } + + public async getLatestUserBalance(): Promise { + throw new Error("KDA-getLatestUserBalance is not implemented here"); + } + + public async getBalance(api: KadenaAPI, pubkey: string): Promise { + return api.getBalance(pubkey); + } + + public async send(): Promise { + throw new Error("KDA-send is not implemented here"); + } + + public async buildTransaction( + to: string, + from: EnkryptAccount | any, + amount: string, + network: KadenaNetwork + ): Promise { + to = network.displayAddress(to); + // const accountDetails = await this.getAccountDetails(to, network); + const api = (await network.api()) as KadenaAPI; + const chainID = await api.getChainId(); + const keySetAccount = to.startsWith("k:") ? to.replace("k:", "") : to; + const unsignedTransaction = Pact.builder + .execution( + Pact.modules.coin["transfer-create"]( + network.displayAddress(from.address), + to, + readKeyset("ks"), + { + decimal: amount, + } + ) + ) + .addKeyset("ks", "keys-all", keySetAccount) + .addSigner(from.publicKey.replace("0x", ""), (withCap: any) => [ + withCap("coin.TRANSFER", network.displayAddress(from.address), to, { + decimal: amount, + }), + withCap("coin.GAS"), + ]) + .setMeta({ + chainId: (chainID ?? + network.options.kadenaApiOptions.chainId) as ChainId, + senderAccount: network.displayAddress(from.address), + }) + .setNetworkId(network.options.kadenaApiOptions.networkId) + .createTransaction(); + + const transaction = await TransactionSigner({ + account: from, + network: network, + payload: bufferToHex(blake2AsU8a(unsignedTransaction.cmd)), + }).then((res) => { + if (res.error) return Promise.reject(res.error); + else + return { + id: 0, + signature: res.result?.replace("0x", "") as string, + }; + }); + + return addSignatures(unsignedTransaction, { + sig: transaction.signature, + pubKey: from.pubKey, + }) as ICommand; + } + + public async getAccountDetails( + account: string, + network: KadenaNetwork + ): Promise { + const api = (await network.api()) as KadenaAPI; + const chainID = await api.getChainId(); + const modules = Pact.modules as any; + const unsignedTransaction = Pact.builder + .execution(modules.coin.details(account)) + .setMeta({ + chainId: (chainID ?? + network.options.kadenaApiOptions.chainId) as ChainId, + }) + .setNetworkId(network.options.kadenaApiOptions.networkId) + .createTransaction(); + const response = await api.dirtyRead(unsignedTransaction as ICommand); + + return response.result; + } +} diff --git a/packages/extension/src/providers/kadena/ui/index.ts b/packages/extension/src/providers/kadena/ui/index.ts new file mode 100644 index 000000000..b691dd5d5 --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/index.ts @@ -0,0 +1,7 @@ +import { ProviderName, UIExportOptions } from "@/types/provider"; +import getRoutes from "./routes"; +const uiexport: UIExportOptions = { + providerName: ProviderName.kadena, + routes: getRoutes(ProviderName.kadena), +}; +export default uiexport; diff --git a/packages/extension/src/providers/kadena/ui/kda-accounts.vue b/packages/extension/src/providers/kadena/ui/kda-accounts.vue new file mode 100644 index 000000000..34e55b0ae --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/kda-accounts.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/kda-sign-message.vue b/packages/extension/src/providers/kadena/ui/kda-sign-message.vue new file mode 100644 index 000000000..7c0ce94fa --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/kda-sign-message.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/libs/signer.ts b/packages/extension/src/providers/kadena/ui/libs/signer.ts new file mode 100644 index 000000000..770d5016a --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/libs/signer.ts @@ -0,0 +1,27 @@ +import { InternalMethods, InternalOnMessageResponse } from "@/types/messenger"; +import { SignerTransactionOptions } from "../types"; +import { getCustomError } from "@/libs/error"; +import sendUsingInternalMessengers from "@/libs/messenger/internal-messenger"; + +const TransactionSigner = ( + options: SignerTransactionOptions +): Promise => { + const { account, payload } = options; + if (account.isHardware) { + return new Promise((resolve, reject) => { + reject(getCustomError("NOT_IMPLEMENTED")); + }); + } else { + return sendUsingInternalMessengers({ + method: InternalMethods.sign, + params: [payload, account], + }).then((res) => { + if (res.error) return res; + return { + result: JSON.parse(res.result as string), + }; + }); + } +}; + +export { TransactionSigner }; diff --git a/packages/extension/src/providers/kadena/ui/routes/index.ts b/packages/extension/src/providers/kadena/ui/routes/index.ts new file mode 100644 index 000000000..8fd431af2 --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/routes/index.ts @@ -0,0 +1,16 @@ +import kdaAccounts from "../kda-accounts.vue"; +import kdaSignMessage from "../kda-sign-message.vue"; +import { RouteRecordRaw } from "vue-router"; +import RouteNames from "./names"; + +const routes = Object.assign({}, RouteNames); +routes.kdaAccounts.component = kdaAccounts; +routes.kdaSignMessage.component = kdaSignMessage; + +export default (namespace: string): RouteRecordRaw[] => { + return Object.values(routes).map((route) => { + route.path = `/${namespace}/${route.path}`; + route.name = `${namespace}-${String(route.name)}`; + return route; + }); +}; diff --git a/packages/extension/src/providers/kadena/ui/routes/names.ts b/packages/extension/src/providers/kadena/ui/routes/names.ts new file mode 100644 index 000000000..e1fe3d98b --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/routes/names.ts @@ -0,0 +1,12 @@ +export default { + kdaAccounts: { + path: "kda-accounts", + component: {}, + name: "kdaAccounts", + }, + kdaSignMessage: { + path: "kda-sign-message", + component: {}, + name: "kdaSignMessage", + }, +}; diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-address-input.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-address-input.vue new file mode 100644 index 000000000..17fe6891c --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-address-input.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-alert.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-alert.vue new file mode 100644 index 000000000..627b1588b --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-alert.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-contacts-list.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-contacts-list.vue new file mode 100644 index 000000000..a5bd4ef1a --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-contacts-list.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-fee-display.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-fee-display.vue new file mode 100644 index 000000000..808b20e92 --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-fee-display.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-fee-select.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-fee-select.vue new file mode 100644 index 000000000..48bc83b99 --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-fee-select.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-from-contacts-list.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-from-contacts-list.vue new file mode 100644 index 000000000..fa52defff --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-from-contacts-list.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-input-amount.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-input-amount.vue new file mode 100644 index 000000000..8124653ea --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-input-amount.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-token-item.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-token-item.vue new file mode 100644 index 000000000..2703f91fc --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-token-item.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-token-list.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-token-list.vue new file mode 100644 index 000000000..1cb91a29a --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-token-list.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/components/send-token-select.vue b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-token-select.vue new file mode 100644 index 000000000..e64d9a710 --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/components/send-token-select.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/index.vue b/packages/extension/src/providers/kadena/ui/send-transaction/index.vue new file mode 100644 index 000000000..50d5507f8 --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/index.vue @@ -0,0 +1,563 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/verify-transaction/index.vue b/packages/extension/src/providers/kadena/ui/send-transaction/verify-transaction/index.vue new file mode 100644 index 000000000..cf577fa63 --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/send-transaction/verify-transaction/index.vue @@ -0,0 +1,360 @@ + + + + + diff --git a/packages/extension/src/providers/kadena/ui/styles/verify-transaction.less b/packages/extension/src/providers/kadena/ui/styles/verify-transaction.less new file mode 100644 index 000000000..549fff172 --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/styles/verify-transaction.less @@ -0,0 +1,468 @@ +.provider-verify-transaction { + width: 100%; + height: 100%; + padding-top: 44px; + padding-bottom: 76px; + box-sizing: border-box; + &__logo { + margin-bottom: 8px; + } + &__network { + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + position: absolute; + right: 56px; + top: 54px; + + img { + width: 16px; + height: 16px; + margin-right: 8px; + } + + p { + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 16px; + text-align: right; + letter-spacing: 0.5px; + color: @primaryLabel; + margin: 0; + } + } + h2 { + font-style: normal; + font-weight: 700; + font-size: 34px; + line-height: 40px; + letter-spacing: 0.25px; + color: @primaryLabel; + margin: 0 0 16px 0; + word-break: break-all; + } + &__block { + background: @lightBg; + border: 1px solid @gray01; + box-sizing: border-box; + border-radius: 12px; + padding: 10px 16px; + width: 100%; + margin: 0 0 12px 0; + } + &__amount { + display: flex; + justify-content: flex-start; + align-items: flex-start; + flex-direction: row; + + img { + box-shadow: inset 0px 0px 1px rgba(0, 0, 0, 0.16); + width: 32px; + height: 32px; + margin-right: 12px; + border-radius: 100%; + } + + &-info { + h4 { + font-style: normal; + font-weight: 700; + font-size: 24px; + line-height: 32px; + color: @primaryLabel; + margin: 0; + word-break: break-all; + + span { + font-variant: small-caps; + } + } + + p { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: @secondaryLabel; + margin: 0; + word-break: break-all; + } + } + } + &__account { + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + + &.from { + margin-bottom: 12px; + } + + img { + width: 32px; + height: 32px; + margin-right: 12px; + border-radius: 100%; + } + &-info { + h4 { + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: @primaryLabel; + margin: 0; + word-break: break-all; + } + + h6 { + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.5px; + color: @secondaryLabel; + margin: 0; + word-break: break-all; + } + + div { + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + + p { + &:first-child { + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.5px; + color: @secondaryLabel; + margin: 0 8px 0 0; + + span { + font-variant: small-caps; + } + } + + &:last-child { + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.5px; + color: @tertiaryLabel; + margin: 0; + word-break: break-all; + } + } + } + + &-to { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: @primaryLabel; + word-break: break-all; +} + } + } + &__error { + margin: 12px 0 0 0; + border-radius: 10px; + padding: 0 0 0 44px; + position: relative; + box-sizing: border-box; + svg { + position: absolute; + left: 0; + top: 0; + } + p { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: @error; + margin: 0; + a { + color: @error; + &:hover { + text-decoration: none; + } + } + } + } + &__info { + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + padding: 6px 0; + margin-bottom: 6px; + img { + width: 32px; + height: 32px; + margin-right: 12px; + } + &-info { + h4 { + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: @primaryLabel; + margin: 0; + word-break: break-all; + } + + p { + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.5px; + color: @tertiaryLabel; + margin: 0; + word-break: break-all; + } + } + } + &__data { + text-align: center; + padding-top: 4px; + padding-bottom: 20px; + &-link { + border-radius: 6px; + transition: background 300ms ease-in-out; + display: inline-block; + cursor: pointer; + text-decoration: none; + padding: 4px 24px 4px 8px; + position: relative; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.8px; + color: @primaryLabel; + + &:hover { + background: rgba(0, 0, 0, 0.04); + } + + svg { + position: absolute; + right: 4px; + top: 4px; + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); + -webkit-transition: all 0.3s ease-in-out; + -moz-transition: all 0.3s ease-in-out; + -ms-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; + } + + &.open { + svg { + position: absolute; + right: 4px; + top: 4px; + -moz-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); + } + } + } + + &-text { + padding-top: 12px; + text-align: left; + font-family: 'SF Mono', 'Segoe UI Mono', 'Menlo', 'Consolas'; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.5px; + color: @secondaryLabel; + + p { + font-family: 'SF Mono', 'Segoe UI Mono', 'Menlo', 'Consolas'; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.5px; + color: @secondaryLabel; + margin: 0; + word-break: break-all; + + a { + color: @secondaryLabel; + } + } + + li { + list-style: none; + } + } + } + &__buttons { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row; + width: 100%; + box-sizing: border-box; + position: absolute; + left: 0; + bottom: 0; + font-size: 0; + padding: 24px; + box-sizing: border-box; + background-color: @white; + &-cancel { + width: 172px; + } + &-send { + width: 232px; + } + + &.border { + box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.05), + 0px 0px 1px rgba(0, 0, 0, 0.25); + } + } + + &__scroll-area { + position: relative; + margin: auto; + width: calc(~"100% + 16px"); + max-height: 350px; + margin: 0; + padding: 0 16px 0 0 !important; + margin-right: -16px; + box-sizing: border-box; + + &.ps--active-y { + padding-bottom: 0 !important; + } + + & > .ps__rail-y { + right: 0 !important; + } + } + + &__hw { + padding: 2px 0 2px 44px; + position: relative; + + svg { + position: absolute; + left: 0; + top: 50%; + margin-top: -16px; + } + + p { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: @secondaryLabel; + margin: 0; + } + } + + &__fee { + height: 40px; + background: @lightBg; + margin: 0 0 12px 0; + box-sizing: border-box; + border: 1px solid @gray01; + box-sizing: border-box; + border-radius: 10px; + width: 100%; + padding: 16px 10px; + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + position: relative; + cursor: pointer; + text-decoration: none; + + &-value { + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + + &-fiat { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: @secondaryLabel; + margin: 0 8px 0 0; + } + + &-crypto { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: @tertiaryLabel; + margin: 0; + + span { + font-variant: small-caps; + } + } + } + } + + &__error { + margin: 0 0 8px 0; + background: @error01; + border-radius: 10px; + padding: 12px 16px 12px 57px; + position: relative; + box-sizing: border-box; + + svg { + position: absolute; + left: 16px; + top: 50%; + margin-top: -12px; + } + p { + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: @error; + margin: 0; + } + } + } \ No newline at end of file diff --git a/packages/extension/src/providers/kadena/ui/types/index.ts b/packages/extension/src/providers/kadena/ui/types/index.ts new file mode 100644 index 000000000..e2add6875 --- /dev/null +++ b/packages/extension/src/providers/kadena/ui/types/index.ts @@ -0,0 +1,33 @@ +import { BaseNetwork } from "@/types/base-network"; +import { ToTokenData } from "@/ui/action/types/token"; +import { EnkryptAccount } from "@enkryptcom/types"; + +export interface TxFeeInfo { + nativeValue: string; + fiatValue: string; + nativeSymbol: string; + fiatSymbol: string; +} + +export interface SendTransactionDataType { + from: string; + value: string; + to: string; + data: `0x${string}`; +} + +export interface VerifyTransactionParams { + fromAddress: string; + fromAddressName: string; + chainId: string; + toAddress: string; + toToken: ToTokenData; + txFee: TxFeeInfo; + TransactionData: SendTransactionDataType; +} + +export interface SignerTransactionOptions { + payload: string; + network: BaseNetwork; + account: EnkryptAccount; +} diff --git a/packages/extension/src/providers/polkadot/methods/dot_signer_signRaw.ts b/packages/extension/src/providers/polkadot/methods/dot_signer_signRaw.ts index b06aea475..5839f0948 100644 --- a/packages/extension/src/providers/polkadot/methods/dot_signer_signRaw.ts +++ b/packages/extension/src/providers/polkadot/methods/dot_signer_signRaw.ts @@ -1,7 +1,7 @@ import { MiddlewareFunction } from "@enkryptcom/types"; import SubstrateProvider from ".."; import { ProviderRPCRequest } from "@/types/provider"; -import { polkadotEncodeAddress } from "@enkryptcom/utils"; +import { polkadotEncodeAddress, utf8ToHex } from "@enkryptcom/utils"; import { SignerPayloadRaw } from "@polkadot/types/types"; import { getCustomError } from "@/libs/error"; import { WindowPromise } from "@/libs/window-promise"; @@ -18,6 +18,10 @@ const method: MiddlewareFunction = function ( const reqPayload = payload.params[0] as SignerPayloadRaw; if (reqPayload.type !== "bytes" && reqPayload.type !== "payload") return res(getCustomError("type is not bytes: signer_signRaw")); + const data = + reqPayload.type === "payload" + ? utf8ToHex(reqPayload.data) + : reqPayload.data; this.KeyRing.getAccount(polkadotEncodeAddress(reqPayload.address)).then( (account) => { const windowPromise = new WindowPromise(); @@ -26,7 +30,7 @@ const method: MiddlewareFunction = function ( this.getUIPath(this.UIRoutes.dotSignMessage.path), JSON.stringify({ ...payload, - params: [reqPayload.data, account], + params: [data, account], }), true ) diff --git a/packages/extension/src/providers/polkadot/networks/acala/karura.ts b/packages/extension/src/providers/polkadot/networks/acala/karura.ts index d5c4a0f17..3596e5cb2 100644 --- a/packages/extension/src/providers/polkadot/networks/acala/karura.ts +++ b/packages/extension/src/providers/polkadot/networks/acala/karura.ts @@ -7,6 +7,8 @@ import { import { subscanActivity } from "../../libs/activity-handlers"; import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; import ormlAssetHandler from "./libs/assetinfo-orml"; +import { toBN } from "web3-utils"; +import { toBase } from "@enkryptcom/utils"; const karuraOptions: SubstrateNetworkOptions = { name: NetworkNames.Karura, @@ -27,6 +29,7 @@ const karuraOptions: SubstrateNetworkOptions = { "0xbaf5aabe40646d11f0ee8abbdc64f4a4b7674925cba08e4a05ff9ebed6e2126b", activityHandler: wrapActivityHandler(subscanActivity), assetHandler: ormlAssetHandler, + existentialDeposit: toBN(toBase("0.1", 12)), knownTokens: assets, }; diff --git a/packages/extension/src/providers/polkadot/networks/unique/opal.ts b/packages/extension/src/providers/polkadot/networks/unique/opal.ts index ec2afb1b4..ce5cbd8f2 100644 --- a/packages/extension/src/providers/polkadot/networks/unique/opal.ts +++ b/packages/extension/src/providers/polkadot/networks/unique/opal.ts @@ -4,6 +4,7 @@ import { SubstrateNetworkOptions, } from "../../types/substrate-network"; import { getActivityHandler } from "./libs/activity-handler"; +import { toBN } from "web3-utils"; const GRAPHQL_ENDPOINT = "https://api-opal.uniquescan.io/v1/graphql"; @@ -22,6 +23,7 @@ const opalOptions: SubstrateNetworkOptions = { node: "wss://ws-opal.unique.network", genesisHash: "0xc87870ef90a438d574b8e320f17db372c50f62beb52e479c8ff6ee5b460670b9", + existentialDeposit: toBN(0), activityHandler: getActivityHandler(GRAPHQL_ENDPOINT), }; diff --git a/packages/extension/src/providers/polkadot/networks/unique/quartz.ts b/packages/extension/src/providers/polkadot/networks/unique/quartz.ts index e39ab05c5..c61176ce8 100644 --- a/packages/extension/src/providers/polkadot/networks/unique/quartz.ts +++ b/packages/extension/src/providers/polkadot/networks/unique/quartz.ts @@ -5,6 +5,7 @@ import { SubstrateNetworkOptions, } from "../../types/substrate-network"; import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import { toBN } from "web3-utils"; const quartzOptions: SubstrateNetworkOptions = { name: NetworkNames.Quartz, @@ -23,6 +24,7 @@ const quartzOptions: SubstrateNetworkOptions = { coingeckoPlatform: CoingeckoPlatform.Quartz, genesisHash: "0xcd4d732201ebe5d6b014edda071c4203e16867305332301dc8d092044b28e554", + existentialDeposit: toBN(0), activityHandler: wrapActivityHandler(subscanActivity), }; diff --git a/packages/extension/src/providers/polkadot/networks/unique/unique.ts b/packages/extension/src/providers/polkadot/networks/unique/unique.ts index b5b8e9aa1..b3a738fb0 100644 --- a/packages/extension/src/providers/polkadot/networks/unique/unique.ts +++ b/packages/extension/src/providers/polkadot/networks/unique/unique.ts @@ -5,6 +5,7 @@ import { SubstrateNetworkOptions, } from "../../types/substrate-network"; import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import { toBN } from "web3-utils"; const uniqueOptions: SubstrateNetworkOptions = { name: NetworkNames.Unique, @@ -24,6 +25,7 @@ const uniqueOptions: SubstrateNetworkOptions = { genesisHash: "0x84322d9cddbf35088f1e54e9a85c967a41a56a4f43445768125e61af166c7d31", activityHandler: wrapActivityHandler(subscanActivity), + existentialDeposit: toBN(0), }; const unique = new SubstrateNetwork(uniqueOptions); diff --git a/packages/extension/src/providers/polkadot/types/substrate-network.ts b/packages/extension/src/providers/polkadot/types/substrate-network.ts index 4c48a4ec6..f19cbee25 100644 --- a/packages/extension/src/providers/polkadot/types/substrate-network.ts +++ b/packages/extension/src/providers/polkadot/types/substrate-network.ts @@ -36,7 +36,7 @@ export interface SubstrateNetworkOptions { coingeckoPlatform?: CoingeckoPlatform; genesisHash: string; knownTokens?: KnownTokenDisplay[]; - existentialDeposit?: BNType; + existentialDeposit: BNType; activityHandler: ( network: BaseNetwork, address: string diff --git a/packages/extension/src/scripts/inject.ts b/packages/extension/src/scripts/inject.ts index cc1a98631..5b3186114 100644 --- a/packages/extension/src/scripts/inject.ts +++ b/packages/extension/src/scripts/inject.ts @@ -7,6 +7,7 @@ import { ProviderName, ProviderType } from "@/types/provider"; import EthereumProvider from "@/providers/ethereum/inject"; import PolkadotProvider from "@/providers/polkadot/inject"; import BitcoinProvider from "@/providers/bitcoin/inject"; +import KadenaProvider from "@/providers/kadena/inject"; import { InternalMethods } from "@/types/messenger"; @@ -31,6 +32,11 @@ const loadInjectedProviders = () => { type: ProviderType.bitcoin, sendMessageHandler: providerSendMessage, }); + KadenaProvider(window, { + name: ProviderName.kadena, + type: ProviderType.kadena, + sendMessageHandler: providerSendMessage, + }); }; loadInjectedProviders(); diff --git a/packages/extension/src/types/activity.ts b/packages/extension/src/types/activity.ts index 744e252ae..804fdff8d 100644 --- a/packages/extension/src/types/activity.ts +++ b/packages/extension/src/types/activity.ts @@ -5,6 +5,7 @@ import { TokenTypeTo, StatusOptionsResponse, } from "@enkryptcom/swap"; +import { ICommandResult } from "@kadena/client"; interface BTCIns { address: string; @@ -66,6 +67,23 @@ interface SubstrateRawInfo { asset_type: string; } +type KadenaRawInfo = ICommandResult; + +interface KadenaDBInfo { + amount: string; + blockHash: string; + blockTime: string; + chain: number; + crossChainAccount: string | null; + crossChainId: number | null; + fromAccount: string; + height: number; + idx: number; + requestKey: string; + toAccount: string; + token: string; +} + enum ActivityStatus { pending = "pending", success = "success", @@ -85,6 +103,8 @@ interface Activity { network: NetworkNames; from: string; to: string; + chainId?: string; + crossChainId?: number; value: string; timestamp: number; nonce?: string; @@ -98,7 +118,8 @@ interface Activity { | SubstrateRawInfo | SubscanExtrinsicInfo | BTCRawInfo - | SwapRawInfo; + | SwapRawInfo + | KadenaRawInfo; } export { @@ -110,4 +131,6 @@ export { SubscanExtrinsicInfo, BTCRawInfo, SwapRawInfo, + KadenaRawInfo, + KadenaDBInfo, }; diff --git a/packages/extension/src/types/base-network.ts b/packages/extension/src/types/base-network.ts index aef61ef72..a1d54bad8 100644 --- a/packages/extension/src/types/base-network.ts +++ b/packages/extension/src/types/base-network.ts @@ -1,12 +1,17 @@ import EvmAPI from "@/providers/ethereum/libs/api"; import SubstrateAPI from "@/providers/polkadot/libs/api"; import BitcoinAPI from "@/providers/bitcoin/libs/api"; +import KadenaAPI from "@/providers/kadena/libs/api"; import { AssetsType, ProviderName } from "@/types/provider"; import { CoingeckoPlatform, SignerType, NetworkNames } from "@enkryptcom/types"; import { Activity } from "./activity"; import { BaseToken } from "./base-token"; import { BNType } from "@/providers/common/types"; +export interface SubNetworkOptions { + id: string; + name: string; +} export interface BaseNetworkOptions { name: NetworkNames; name_long: string; @@ -26,7 +31,12 @@ export interface BaseNetworkOptions { coingeckoPlatform?: CoingeckoPlatform; identicon: (address: string) => string; basePath: string; - api: () => Promise | Promise | Promise; + subNetworks?: SubNetworkOptions[]; + api: () => + | Promise + | Promise + | Promise + | Promise; customTokens?: boolean; } @@ -49,10 +59,12 @@ export abstract class BaseNetwork { public identicon: (address: string) => string; public basePath: string; public decimals: number; + public subNetworks?: SubNetworkOptions[]; public api: () => | Promise | Promise - | Promise; + | Promise + | Promise; public customTokens: boolean; constructor(options: BaseNetworkOptions) { @@ -76,6 +88,7 @@ export abstract class BaseNetwork { this.customTokens = options.customTokens ?? false; this.coingeckoPlatform = options.coingeckoPlatform; this.currencyNameLong = options.currencyNameLong; + this.subNetworks = options.subNetworks; } public abstract getAllTokens(address: string): Promise; diff --git a/packages/extension/src/types/base-token.ts b/packages/extension/src/types/base-token.ts index 00a76a944..2e7d737c4 100644 --- a/packages/extension/src/types/base-token.ts +++ b/packages/extension/src/types/base-token.ts @@ -2,6 +2,7 @@ import EvmAPI from "@/providers/ethereum/libs/api"; import MarketData from "@/libs/market-data"; import { ApiPromise } from "@polkadot/api"; import BitcoinAPI from "@/providers/bitcoin/libs/api"; +import KadenaAPI from "@/providers/kadena/libs/api"; import { BNType } from "@/providers/common/types"; export type TransferType = "keepAlive" | "all" | "allKeepAlive" | "transfer"; @@ -59,7 +60,7 @@ export abstract class BaseToken { } public abstract getLatestUserBalance( - api: EvmAPI | ApiPromise | BitcoinAPI, + api: EvmAPI | ApiPromise | BitcoinAPI | KadenaAPI, address: string ): Promise; diff --git a/packages/extension/src/types/provider.ts b/packages/extension/src/types/provider.ts index a0262dc49..bfc4905cd 100644 --- a/packages/extension/src/types/provider.ts +++ b/packages/extension/src/types/provider.ts @@ -1,6 +1,7 @@ import type { InjectedProvider as EthereumProvider } from "../providers/ethereum/types"; import type { InjectedProvider as PolkadotProvider } from "@/providers/polkadot/types"; import type { InjectedProvider as BitcoinProvider } from "@/providers/bitcoin/types"; +import type { InjectedProvider as KadenaProvider } from "@/providers/kadena/types"; import EventEmitter from "eventemitter3"; import { MiddlewareFunction, @@ -15,13 +16,19 @@ import { RoutesType } from "./ui"; import { NFTCollection } from "./nft"; import { BaseNetwork } from "./base-network"; import { BaseToken } from "./base-token"; -import { BTCRawInfo, EthereumRawInfo, SubscanExtrinsicInfo } from "./activity"; +import { + BTCRawInfo, + EthereumRawInfo, + SubscanExtrinsicInfo, + KadenaRawInfo, +} from "./activity"; export enum ProviderName { enkrypt = "enkrypt", ethereum = "ethereum", bitcoin = "bitcoin", polkadot = "polkadot", + kadena = "kadena", } export enum InternalStorageNamespace { keyring = "KeyRing", @@ -30,6 +37,7 @@ export enum InternalStorageNamespace { evmAccountsState = "EVMAccountsState", substrateAccountsState = "SubstrateAccountsState", bitcoinAccountsState = "BitcoinAccountsState", + kadenaAccountsState = "KadenaAccountsState", activityState = "ActivityState", marketData = "MarketData", cacheFetch = "CacheFetch", @@ -49,6 +57,7 @@ export enum ProviderType { evm, substrate, bitcoin, + kadena, } export type SendMessageHandler = ( @@ -113,7 +122,9 @@ export abstract class ProviderAPIInterface { abstract getBalance(address: string): Promise; abstract getTransactionStatus( hash: string - ): Promise; + ): Promise< + EthereumRawInfo | SubscanExtrinsicInfo | BTCRawInfo | KadenaRawInfo | null + >; } export type handleIncomingMessage = ( @@ -125,8 +136,12 @@ export type handleOutgoingMessage = ( provider: Provider, message: string ) => Promise; -export { EthereumProvider, PolkadotProvider, BitcoinProvider }; -export type Provider = EthereumProvider | PolkadotProvider | BitcoinProvider; +export { EthereumProvider, PolkadotProvider, BitcoinProvider, KadenaProvider }; +export type Provider = + | EthereumProvider + | PolkadotProvider + | BitcoinProvider + | KadenaProvider; export interface ProviderRequestOptions { url: string; diff --git a/packages/extension/src/ui/action/App.vue b/packages/extension/src/ui/action/App.vue index 39cc233f2..330204969 100644 --- a/packages/extension/src/ui/action/App.vue +++ b/packages/extension/src/ui/action/App.vue @@ -47,6 +47,7 @@ :show-deposit="showDepositWindow" @update:init="init" @address-changed="onSelectedAddressChanged" + @select:subnetwork="onSelectedSubnetworkChange" @toggle:deposit="toggleDepositWindow" /> @@ -55,6 +56,7 @@ :is="Component" :key="$route.fullPath" :network="currentNetwork" + :subnetwork="currentSubNetwork" :account-info="accountHeaderData" @update:init="init" @toggle:deposit="toggleDepositWindow" @@ -92,45 +94,47 @@ + + diff --git a/packages/extension/src/ui/action/components/accounts-header/components/subnet-list.vue b/packages/extension/src/ui/action/components/accounts-header/components/subnet-list.vue new file mode 100644 index 000000000..c5a92d9a2 --- /dev/null +++ b/packages/extension/src/ui/action/components/accounts-header/components/subnet-list.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/packages/extension/src/ui/action/components/accounts-header/index.vue b/packages/extension/src/ui/action/components/accounts-header/index.vue index 29708b184..4a689b21a 100644 --- a/packages/extension/src/ui/action/components/accounts-header/index.vue +++ b/packages/extension/src/ui/action/components/accounts-header/index.vue @@ -7,7 +7,9 @@ :toggle-accounts="toggleAccounts" :active="showAccounts" :network="network" + v-bind="$attrs" @toggle:deposit="$emit('toggle:deposit')" + @select:subnetwork="$emit('select:subnetwork', $event)" /> (); const showAccounts = ref(false); diff --git a/packages/extension/src/ui/action/views/accounts/index.vue b/packages/extension/src/ui/action/views/accounts/index.vue index bc782856f..f476143a7 100644 --- a/packages/extension/src/ui/action/views/accounts/index.vue +++ b/packages/extension/src/ui/action/views/accounts/index.vue @@ -60,7 +60,11 @@ Add hardware wallet account - + Import account from another wallet @@ -118,6 +122,7 @@ import HWwallets from "@enkryptcom/hw-wallets"; import { SignerType } from "@enkryptcom/types"; import { BaseNetwork } from "@/types/base-network"; import { WalletType } from "@enkryptcom/types"; +import { ProviderName } from "@/types/provider"; const emit = defineEmits<{ (e: "addressChanged", account: EnkryptAccount): void; diff --git a/packages/extension/src/ui/action/views/deposit/index.vue b/packages/extension/src/ui/action/views/deposit/index.vue index 6978d7bab..5ce504494 100644 --- a/packages/extension/src/ui/action/views/deposit/index.vue +++ b/packages/extension/src/ui/action/views/deposit/index.vue @@ -10,14 +10,15 @@

Your {{ network.name_long }} address

- You can send {{ network.currencyName }} to this address using - {{ network.name_long }} network. + {{ depositCopy }}