diff --git a/packages/extension/package.json b/packages/extension/package.json index adb0f898c..caed76c70 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -4,14 +4,16 @@ "private": true, "scripts": { "zip": "cd dist; zip -r release.zip *;", - "build:chrome": "cross-env BROWSER='chrome' vue-cli-service build && yarn build:rollup", - "build:firefox": "cross-env BROWSER='firefox' vue-cli-service build && yarn build:rollup && node configs/get-system-info.js", + "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", "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", @@ -74,6 +76,7 @@ }, "devDependencies": { "@babel/plugin-transform-class-static-block": "^7.23.4", + "@kadena/pactjs-cli": "^1.7.0", "@polkadot/api": "^10.11.2", "@polkadot/extension-inject": "^0.46.6", "@polkadot/keyring": "^12.6.2", 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/providers/kadena/libs/accounts-state/index.ts b/packages/extension/src/providers/kadena/libs/accounts-state/index.ts index ba84a0e05..486805a88 100644 --- a/packages/extension/src/providers/kadena/libs/accounts-state/index.ts +++ b/packages/extension/src/providers/kadena/libs/accounts-state/index.ts @@ -1,6 +1,7 @@ import { InternalStorageNamespace } from "@/types/provider"; import BrowserStorage from "@/libs/common/browser-storage"; import { IState, StorageKeys } from "./types"; + class AccountState { #storage: BrowserStorage; constructor() { 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 index 2f19f4c12..a800d44f7 100644 --- 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 @@ -13,7 +13,6 @@ const getAddressActivity = async ( height: number ): Promise => { const url = `${endpoint}txs/account/${address}?minheight=${height}&limit=200&token=coin`; - return cacheFetch({ url }, ttl) .then((res) => { return res ? res : []; @@ -72,13 +71,25 @@ export default async ( : "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: activity.fromAccount, - to: activity.toAccount, + 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(), diff --git a/packages/extension/src/providers/kadena/libs/api.ts b/packages/extension/src/providers/kadena/libs/api.ts index 32b4407d4..16839c9a5 100644 --- a/packages/extension/src/providers/kadena/libs/api.ts +++ b/packages/extension/src/providers/kadena/libs/api.ts @@ -3,11 +3,15 @@ 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; @@ -15,6 +19,7 @@ class API implements ProviderAPIInterface { networkId: string; chainId: string; apiHost: string; + domainState: DomainState; displayAddress: (address: string) => string; constructor(node: string, options: KadenaNetworkOptions) { @@ -24,29 +29,53 @@ class API implements ProviderAPIInterface { 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 getTransactionStatus(hash: string): Promise { - const Pact = require("pact-lang-api"); - - const cmd = { requestKeys: [hash] }; - const transactions = await Pact.fetch.poll(cmd, this.apiHost); + async getChainId(): Promise { + return this.domainState.getSelectedSubNetWork().then((id) => { + if (id) return id; + return "0"; + }); + } - return transactions[hash]; + 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 getBalance(address: string): Promise { - const balance = await this.getBalanceAPI(this.displayAddress(address)); + async getBalanceByChainId(address: string, chainId: string): Promise { + const balance = await this.getBalanceAPI( + this.displayAddress(address), + chainId + ); - if (balance.result.error) { - return toBase("0", this.decimals); + 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( @@ -56,49 +85,50 @@ class API implements ProviderAPIInterface { return toBase(balanceValue, this.decimals); } - async getBalanceAPI(account: string) { - const Pact = require("pact-lang-api"); - - const cmd = { - networkId: this.networkId, - pactCode: `(coin.get-balance "${account}")`, - envData: {}, - meta: { - creationTime: Math.round(new Date().getTime() / 1000), - ttl: 600, - gasLimit: 600, - chainId: this.chainId, - gasPrice: 0.0000001, - sender: "", - }, - }; - - return Pact.fetch.local(cmd, this.apiHost); + 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 client = createClient(this.apiHost); + const chainId = await this.getChainId(); + const client = createClient(this.getApiHost(chainId)); return client.local(signedTranscation as ICommand); } async sendTransaction( signedTranscation: ICommand ): Promise { - const client = createClient(this.apiHost); + const chainId = await this.getChainId(); + const client = createClient(this.getApiHost(chainId)); return client.submit(signedTranscation as ICommand); } async listen( transactionDescriptor: ITransactionDescriptor ): Promise { - const client = createClient(this.apiHost); + const chainId = await this.getChainId(); + const client = createClient(this.getApiHost(chainId)); return client.listen(transactionDescriptor); } - async dirtyRead(signedTranscation: ICommand): Promise { - const client = createClient(this.apiHost); + async dirtyRead( + signedTranscation: ICommand | IUnsignedCommand + ): Promise { + const chainId = await this.getChainId(); + const client = createClient(this.getApiHost(chainId)); return client.dirtyRead(signedTranscation); } } diff --git a/packages/extension/src/providers/kadena/methods/kda_getBalance.ts b/packages/extension/src/providers/kadena/methods/kda_getBalance.ts index f42dddb54..f3acb9988 100644 --- a/packages/extension/src/providers/kadena/methods/kda_getBalance.ts +++ b/packages/extension/src/providers/kadena/methods/kda_getBalance.ts @@ -2,6 +2,7 @@ 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, @@ -11,12 +12,15 @@ const method: MiddlewareFunction = function ( ): void { if (payload.method !== "kda_getBalance") return next(); else { - if (!payload.params || payload.params.length < 1) { + 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.getBalance(payload.params![0]).then((bal) => { + (api as KadenaAPI).getBalanceByChainId(address, chainId).then((bal) => { res(null, bal); }); }); diff --git a/packages/extension/src/providers/kadena/methods/kda_requestAccounts.ts b/packages/extension/src/providers/kadena/methods/kda_requestAccounts.ts index 909a744b0..ff111b049 100644 --- a/packages/extension/src/providers/kadena/methods/kda_requestAccounts.ts +++ b/packages/extension/src/providers/kadena/methods/kda_requestAccounts.ts @@ -97,28 +97,46 @@ const method: MiddlewareFunction = function ( isAccountAccessPending = true; const accountsState = new AccountState(); - accountsState.isApproved(_payload.options.domain).then((isApproved) => { - if (isApproved) { - getAccounts().then((acc) => { - _res(null, acc); - handleRemainingPromises(); - }); - } else { - const windowPromise = new WindowPromise(); - windowPromise - .getResponse( - this.getUIPath(this.UIRoutes.kdaAccounts.path), - JSON.stringify(payload) - ) - .then(({ error }) => { - if (error) res(error); - else getAccounts().then((acc) => res(null, acc)); - }) - .finally(handleRemainingPromises); - } - }); + 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(); } }; 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 index dde41250e..5b2c7066b 100644 --- a/packages/extension/src/providers/kadena/networks/icons/kadena-kda-logo.svg +++ b/packages/extension/src/providers/kadena/networks/icons/kadena-kda-logo.svg @@ -1,24 +1,5 @@ - - - - - - - - - - - - - - - + + + + diff --git a/packages/extension/src/providers/kadena/networks/kadena-testnet.ts b/packages/extension/src/providers/kadena/networks/kadena-testnet.ts index 1cdfbf9e6..c42e09c87 100644 --- a/packages/extension/src/providers/kadena/networks/kadena-testnet.ts +++ b/packages/extension/src/providers/kadena/networks/kadena-testnet.ts @@ -22,6 +22,15 @@ const kadenaOptions: KadenaNetworkOptions = { 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, diff --git a/packages/extension/src/providers/kadena/networks/kadena.ts b/packages/extension/src/providers/kadena/networks/kadena.ts index c81034e6f..d122438a8 100644 --- a/packages/extension/src/providers/kadena/networks/kadena.ts +++ b/packages/extension/src/providers/kadena/networks/kadena.ts @@ -23,6 +23,14 @@ const kadenaOptions: KadenaNetworkOptions = { 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:"), diff --git a/packages/extension/src/providers/kadena/types/kadena-network.ts b/packages/extension/src/providers/kadena/types/kadena-network.ts index 31d7d90cd..785afba38 100644 --- a/packages/extension/src/providers/kadena/types/kadena-network.ts +++ b/packages/extension/src/providers/kadena/types/kadena-network.ts @@ -1,5 +1,9 @@ import { Activity } from "@/types/activity"; -import { BaseNetwork, BaseNetworkOptions } from "@/types/base-network"; +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"; @@ -31,11 +35,13 @@ export interface KadenaNetworkOptions { 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 @@ -59,7 +65,7 @@ export class KadenaNetwork extends BaseNetwork { }; const baseOptions: BaseNetworkOptions = { - basePath: "m/44'/626'/0'/0'", + basePath: "m/44'/626'", identicon: createIcon, signer: [SignerType.ed25519kda], provider: ProviderName.kadena, diff --git a/packages/extension/src/providers/kadena/types/kda-token.ts b/packages/extension/src/providers/kadena/types/kda-token.ts index 06195be90..3f2d1e6c4 100644 --- a/packages/extension/src/providers/kadena/types/kda-token.ts +++ b/packages/extension/src/providers/kadena/types/kda-token.ts @@ -19,6 +19,12 @@ export abstract class KDABaseToken extends BaseToken { account: string, network: KadenaNetwork ): Promise; + + public abstract getBalance( + api: KadenaAPI, + pubkey: string, + chainId?: string + ): Promise; } export class KDAToken extends KDABaseToken { @@ -26,10 +32,11 @@ export class KDAToken extends KDABaseToken { super(options); } - public async getLatestUserBalance( - api: KadenaAPI, - pubkey: string - ): Promise { + 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); } @@ -45,6 +52,8 @@ export class KDAToken extends KDABaseToken { ): 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( @@ -65,7 +74,8 @@ export class KDAToken extends KDABaseToken { withCap("coin.GAS"), ]) .setMeta({ - chainId: network.options.kadenaApiOptions.chainId as ChainId, + chainId: (chainID ?? + network.options.kadenaApiOptions.chainId) as ChainId, senderAccount: network.displayAddress(from.address), }) .setNetworkId(network.options.kadenaApiOptions.networkId) @@ -94,14 +104,17 @@ export class KDAToken extends KDABaseToken { 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: network.options.kadenaApiOptions.chainId as ChainId }) + .setMeta({ + chainId: (chainID ?? + network.options.kadenaApiOptions.chainId) as ChainId, + }) .setNetworkId(network.options.kadenaApiOptions.networkId) .createTransaction(); - - const api = (await network.api()) as KadenaAPI; const response = await api.dirtyRead(unsignedTransaction as ICommand); return response.result; 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 index c4b422e70..17fe6891c 100644 --- 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 @@ -84,9 +84,7 @@ const address = computed({ } }, set: (value) => { - if (value) { - emit("update:inputAddress", value); - } + emit("update:inputAddress", value); }, }); 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 @@ +
+ +

{{ errorMsg }}

+
+ + + + + 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 index 535dcd24e..d163cf024 100644 --- 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 @@ -4,7 +4,7 @@ v-model="amount" type="number" placeholder="0" - :class="{ error: !hasEnoughBalance }" + :class="{ error: !isValid }" @focus="changeFocus" @blur="changeFocus" @input="emit('update:inputSetMax', false)" @@ -34,7 +34,7 @@ const emit = defineEmits<{ const isFocus = ref(false); const props = defineProps({ - hasEnoughBalance: { + isValid: { type: Boolean, default: false, }, diff --git a/packages/extension/src/providers/kadena/ui/send-transaction/index.vue b/packages/extension/src/providers/kadena/ui/send-transaction/index.vue index aafeedcf0..433c010f6 100644 --- a/packages/extension/src/providers/kadena/ui/send-transaction/index.vue +++ b/packages/extension/src/providers/kadena/ui/send-transaction/index.vue @@ -34,7 +34,7 @@ ref="addressInputTo" :value="addressTo" :network="network" - :is-address="addressToIsValid" + :is-address="fieldsValidation.addressTo.valueOf" @update:input-address="inputAddressTo" @toggle:show-contacts="toggleSelectContactTo" /> @@ -66,16 +66,17 @@ + +
@@ -104,6 +105,7 @@ import SendTokenSelect from "./components/send-token-select.vue"; import AssetsSelectList from "@action/views/assets-select-list/index.vue"; import SendInputAmount from "./components/send-input-amount.vue"; import SendFeeSelect from "./components/send-fee-select.vue"; +import SendAlert from "./components/send-alert.vue"; import BaseButton from "@action/components/base-button/index.vue"; import { AccountsHeaderData } from "@action/types/account"; import { GasFeeInfo } from "@/providers/ethereum/ui/types"; @@ -136,6 +138,7 @@ const props = defineProps({ const route = useRoute(); const router = useRouter(); const nameResolver = new GenericNameResolver(); +const errorMsg = ref(""); const addressInputTo = ref(); const addressInputFrom = ref(); @@ -157,46 +160,16 @@ const selectedAsset = ref>( decimals: props.network.decimals, }) ); -const hasEnough = ref(false); const sendMax = ref(false); -const addressToIsValid = ref(false); + +const fieldsValidation = ref({ + addressTo: false, + amount: false, +}); const selected: string = route.params.id as string; const isLoadingAssets = ref(true); -const edWarn = computed(() => { - if (!fee.value) { - return undefined; - } - - if (!amount.value) { - return false; - } - - if (!isValidDecimals(amount.value ?? "0", selectedAsset.value.decimals!)) { - return false; - } - - const rawAmount = toBN( - toBase(amount.value.toString(), selectedAsset.value.decimals ?? 0) - ); - const ed = selectedAsset.value.existentialDeposit ?? toBN(0); - const userBalance = toBN(selectedAsset.value.balance ?? 0); - - if (!sendMax.value && userBalance.sub(rawAmount).lte(ed)) { - return true; - } - - const txFee = toBN( - toBase(fee.value.nativeValue, selectedAsset.value.decimals!) - ); - if (!sendMax.value && userBalance.sub(txFee).sub(rawAmount).lt(ed)) { - return true; - } else { - return false; - } -}); - const isAddress = computed(() => { return addressTo.value.length > 3 && addressTo.value.length < 256; }); @@ -207,92 +180,134 @@ onMounted(() => { }); const validateFields = async () => { - if (selectedAsset.value && isAddress.value) { - const to = props.network.displayAddress(addressTo.value); - - if (props.network.isAddress(to)) { - addressToIsValid.value = true; - } else { - const accountDetail = await accountAssets.value[0].getAccountDetails( - to, - props.network - ); - if (accountDetail.error) { - addressToIsValid.value = false; - } else { - addressToIsValid.value = true; - } - } - } + errorMsg.value = ""; - if (!isValidDecimals(amount.value || "0", selectedAsset.value.decimals!)) { - hasEnough.value = false; - return; - } - let rawAmount = toBN( - toBase( - amount.value ? amount.value.toString() : "0", - selectedAsset.value.decimals! - ) - ); + fieldsValidation.value = { + addressTo: true, + amount: true, + }; - if (rawAmount.lten(0)) { - hasEnough.value = false; + if (!selectedAsset.value) { return; } - const localTransaction = await selectedAsset.value.buildTransaction!( - addressTo.value, - props.accountInfo.selectedAccount, - rawAmount.toString(), - props.network - ); - - const networkApi = (await props.network.api()) as KadenaAPI; - const transactionResult = await networkApi.sendLocalTransaction( - localTransaction - ); + try { + if (isAddress.value) { + const to = props.network.displayAddress(addressTo.value); + const from = props.network.displayAddress(addressFrom.value); + + if (!props.network.isAddress(to)) { + const accountDetail = await accountAssets.value[0].getAccountDetails( + to, + props.network + ); + + if (accountDetail.error) { + fieldsValidation.value.addressTo = false; + errorMsg.value = 'Invalid "To" address'; + return; + } + } - const gasLimit = transactionResult.metaData?.publicMeta?.gasLimit; - const gasPrice = transactionResult.metaData?.publicMeta?.gasPrice; - const gasFee = gasLimit && gasPrice ? gasLimit * gasPrice : 0; + if (to == from) { + fieldsValidation.value.addressTo = false; + errorMsg.value = '"To" address cannot be the same as "From" address'; + return; + } + } - const rawFee = toBN(toBase(gasFee.toString(), selectedAsset.value.decimals!)); - const rawBalance = toBN(selectedAsset.value.balance!); + let rawAmount = toBN(toBase("0", selectedAsset.value.decimals!)); - if ( - sendMax.value && - selectedAsset.value.name === accountAssets.value[0].name - ) { - rawAmount = rawBalance.sub(rawFee); + if (amount.value) { + if (!isValidDecimals(amount.value, selectedAsset.value.decimals!)) { + fieldsValidation.value.amount = false; + errorMsg.value = `Amount cannot have more than ${selectedAsset.value.decimals} decimals`; + return; + } - if (rawAmount.gtn(0)) { - amount.value = fromBase( - rawAmount.toString(), - selectedAsset.value.decimals! + rawAmount = toBN( + toBase(amount.value.toString(), selectedAsset.value.decimals!) ); + + if (rawAmount.lten(0)) { + fieldsValidation.value.amount = false; + errorMsg.value = "Amount must be greater than 0"; + return; + } } - } - if (rawAmount.add(rawFee).gt(rawBalance)) { - hasEnough.value = false; - } else { - hasEnough.value = true; - } + if (amount.value || sendMax.value) { + const localTransaction = await selectedAsset.value.buildTransaction!( + addressTo.value, + props.accountInfo.selectedAccount, + sendMax.value ? "0.000000000001" : amount.value!, + props.network + ); - const nativeAsset = accountAssets.value[0]; - const txFeeHuman = fromBase(rawFee?.toString() ?? "", nativeAsset.decimals!); + const networkApi = (await props.network.api()) as KadenaAPI; - const txPrice = new BigNumber(nativeAsset.price!).times(txFeeHuman); + const transactionResult = await networkApi.sendLocalTransaction( + localTransaction + ); - fee.value = { - fiatSymbol: "USD", - fiatValue: txPrice.toString(), - nativeSymbol: nativeAsset.symbol ?? "", - nativeValue: txFeeHuman.toString(), - }; + if (transactionResult.result.status !== "success") { + fieldsValidation.value.amount = false; + errorMsg.value = + (transactionResult.result.error as any).message || + "An error occurred"; + return; + } + + const gasLimit = transactionResult.metaData?.publicMeta?.gasLimit; + const gasPrice = transactionResult.metaData?.publicMeta?.gasPrice; + const gasFee = gasLimit && gasPrice ? gasLimit * gasPrice : 0; + + const rawFee = toBN( + toBase(gasFee.toString(), selectedAsset.value.decimals!) + ); + const rawBalance = toBN(selectedAsset.value.balance!); + + if ( + sendMax.value && + selectedAsset.value.name === accountAssets.value[0].name + ) { + rawAmount = rawBalance.sub(rawFee); + + if (rawAmount.gtn(0)) { + amount.value = fromBase( + rawAmount.toString(), + selectedAsset.value.decimals! + ); + } + } + + if (rawAmount.add(rawFee).gt(rawBalance)) { + fieldsValidation.value.amount = false; + errorMsg.value = "Insufficient funds"; + return; + } + + const nativeAsset = accountAssets.value[0]; + const txFeeHuman = fromBase( + rawFee?.toString() ?? "", + nativeAsset.decimals! + ); + + const txPrice = new BigNumber(nativeAsset.price!).times(txFeeHuman); + + fee.value = { + fiatSymbol: "USD", + fiatValue: txPrice.toString(), + nativeSymbol: nativeAsset.symbol ?? "", + nativeValue: txFeeHuman.toString(), + }; + } + } catch (error: any) { + errorMsg.value = error.message || "An error occurred"; + } }; -watch([selectedAsset, addressTo], validateFields); + +watch([selectedAsset, addressTo, amount, sendMax], validateFields); watch(addressFrom, () => { fetchTokens(); @@ -304,7 +319,7 @@ const fetchTokens = async () => { const pricePromises = networkAssets.map((asset) => asset.getLatestPrice()); const balancePromises = networkAssets.map((asset) => { if (!asset.balance) { - return asset.getLatestUserBalance(networkApi.api, addressFrom.value); + return asset.getBalance(networkApi.api, addressFrom.value); } return Promise.resolve(asset.balance); @@ -377,8 +392,7 @@ const selectToken = (token: KDAToken | Partial) => { const inputAmount = (number: string | undefined) => { sendMax.value = false; - amount.value = number ? (parseFloat(number) < 0 ? "0" : number) : number; - validateFields(); + amount.value = number ? (parseFloat(number) < 0 ? "" : number) : number; }; const sendButtonTitle = computed(() => { @@ -401,43 +415,24 @@ const setSendMax = (max: boolean) => { } if (selectedAsset.value) { - const humanBalance = fromBase( - selectedAsset.value.balance!, - selectedAsset.value.decimals! - ); - amount.value = humanBalance; - validateFields(); sendMax.value = true; } }; const isDisabled = computed(() => { - let isDisabled = true; - let addressIsValid = false; - - try { - props.network.displayAddress(addressTo.value); - addressIsValid = true; - } catch { - addressIsValid = false; - } - - if ( - amount.value !== undefined && - amount.value !== "" && - hasEnough.value && - addressIsValid && - addressToIsValid.value && - !edWarn.value && - edWarn.value !== undefined - ) - isDisabled = false; - return isDisabled; + return ( + !addressTo.value || + !amount.value || + !fieldsValidation.value.amount || + !fieldsValidation.value.addressTo + ); }); const sendAction = async () => { const keyring = new PublicKeyRing(); const fromAccount = await keyring.getAccount(addressFrom.value); + const networkApi = (await props.network.api()) as KadenaAPI; + const chainId = await networkApi.getChainId(); const txVerifyInfo: VerifyTransactionParams = { TransactionData: { from: fromAccount.address, @@ -456,6 +451,7 @@ const sendAction = async () => { name: selectedAsset.value.name || "", price: selectedAsset.value.price || "0", }, + chainId: chainId, fromAddress: fromAccount.address, fromAddressName: fromAccount.name, txFee: fee.value!, 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 index f6df46500..cf577fa63 100644 --- 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 @@ -12,13 +12,7 @@
-

- {{ errorMsg }} -

+

Double check the information and confirm transaction

@@ -42,6 +36,10 @@
+
+ +
+
(); +const chainId = ref(); const kdaToken = ref(); const KeyRing = new PublicKeyRing(); const route = useRoute(); @@ -124,6 +124,7 @@ const network = ref(DEFAULT_KADENA_NETWORK); onBeforeMount(async () => { network.value = (await getNetworkByName(selectedNetwork))!; account.value = await KeyRing.getAccount(txData.fromAddress); + chainId.value = txData.chainId; isWindowPopup.value = account.value.isHardware; kdaToken.value = new KDAToken({ icon: network.value.icon, @@ -150,18 +151,23 @@ const sendAction = async () => { txData.toAddress, account.value!, txData.TransactionData.value, - network.value as KadenaNetwork + network.value as KadenaNetwork, + chainId.value! ); const networkApi = (await network.value.api()) as KadenaAPI; - const transactionDescriptor = await networkApi.sendTransaction(transaction); + const transactionDescriptor = await networkApi.sendTransaction( + transaction, + chainId.value! + ); const txActivity: Activity = { - from: txData.fromAddress, - to: txData.toAddress, + from: network.value.displayAddress(txData.fromAddress), + to: network.value.displayAddress(txData.toAddress), isIncoming: txData.fromAddress === txData.toAddress, network: network.value.name, status: ActivityStatus.pending, + chainId: chainId.value!, timestamp: new Date().getTime(), token: { decimals: txData.toToken.decimals, @@ -187,7 +193,7 @@ const sendAction = async () => { if (getCurrentContext() === "popup") { setTimeout(() => { isProcessing.value = false; - router.go(-2); + router.push({ name: "activity", params: { id: network.value.name } }); }, 2500); } else { setTimeout(() => { @@ -343,5 +349,12 @@ const isHasScroll = () => { right: 0 !important; } } + + &__error { + position: absolute; + top: 480px; + width: 100%; + background-color: white; + } } diff --git a/packages/extension/src/providers/kadena/ui/types/index.ts b/packages/extension/src/providers/kadena/ui/types/index.ts index b8854f46e..e2add6875 100644 --- a/packages/extension/src/providers/kadena/ui/types/index.ts +++ b/packages/extension/src/providers/kadena/ui/types/index.ts @@ -19,6 +19,7 @@ export interface SendTransactionDataType { export interface VerifyTransactionParams { fromAddress: string; fromAddressName: string; + chainId: string; toAddress: string; toToken: ToTokenData; txFee: TxFeeInfo; diff --git a/packages/extension/src/types/activity.ts b/packages/extension/src/types/activity.ts index 26f5b5ace..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,12 +67,21 @@ interface SubstrateRawInfo { asset_type: string; } -interface KadenaRawInfo { - gas: number; - result: { status: string; data: string }; - reqKey: string; - logs: string; - txId: number; +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 { @@ -93,6 +103,8 @@ interface Activity { network: NetworkNames; from: string; to: string; + chainId?: string; + crossChainId?: number; value: string; timestamp: number; nonce?: string; @@ -120,4 +132,5 @@ export { BTCRawInfo, SwapRawInfo, KadenaRawInfo, + KadenaDBInfo, }; diff --git a/packages/extension/src/types/base-network.ts b/packages/extension/src/types/base-network.ts index dfcc2efe3..988babb7e 100644 --- a/packages/extension/src/types/base-network.ts +++ b/packages/extension/src/types/base-network.ts @@ -8,6 +8,10 @@ import { Activity } from "./activity"; import { BaseToken } from "./base-token"; import { BNLike } from "ethereumjs-util"; +export interface SubNetworkOptions { + id: string; + name: string; +} export interface BaseNetworkOptions { name: NetworkNames; name_long: string; @@ -27,6 +31,7 @@ export interface BaseNetworkOptions { coingeckoPlatform?: CoingeckoPlatform; identicon: (address: string) => string; basePath: string; + subNetworks?: SubNetworkOptions[]; api: () => | Promise | Promise @@ -54,6 +59,7 @@ export abstract class BaseNetwork { public identicon: (address: string) => string; public basePath: string; public decimals: number; + public subNetworks?: SubNetworkOptions[]; public api: () => | Promise | Promise @@ -82,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/ui/action/App.vue b/packages/extension/src/ui/action/App.vue index 3a58b2bbe..2d0ee27f8 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,46 +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/deposit/index.vue b/packages/extension/src/ui/action/views/deposit/index.vue index f56c80717..448e75137 100644 --- a/packages/extension/src/ui/action/views/deposit/index.vue +++ b/packages/extension/src/ui/action/views/deposit/index.vue @@ -10,8 +10,7 @@

Your {{ network.name_long }} address

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

@@ -55,20 +54,20 @@