From f3dde225b4ca7990318e8ff860946704240d3f4a Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:48:56 -0800 Subject: [PATCH 1/4] devop: merge --- .github/workflows/release.yml | 2 +- .gitignore | 1 + README.md | 1 + .../src/libs/keyring/public-keyring.ts | 10 ++ .../bitcoin/libs/activity-handlers/index.ts | 3 +- .../activity-handlers/providers/firo/index.ts | 107 ++++++++++++ .../src/providers/bitcoin/libs/api-firo.ts | 158 ++++++++++++++++++ .../bitcoin/networks/firo-testnet.ts | 55 ++++++ .../src/providers/bitcoin/networks/firo.ts | 55 ++++++ .../providers/bitcoin/networks/icons/firo.svg | 22 +++ .../src/providers/bitcoin/networks/index.ts | 4 + .../bitcoin/types/bitcoin-network.ts | 3 +- .../src/providers/bitcoin/types/index.ts | 27 +++ .../hw-wallets/src/ledger/bitcoin/configs.ts | 2 +- packages/types/src/networks.ts | 2 + 15 files changed, 448 insertions(+), 4 deletions(-) create mode 100644 packages/extension/src/providers/bitcoin/libs/activity-handlers/providers/firo/index.ts create mode 100644 packages/extension/src/providers/bitcoin/libs/api-firo.ts create mode 100644 packages/extension/src/providers/bitcoin/networks/firo-testnet.ts create mode 100644 packages/extension/src/providers/bitcoin/networks/firo.ts create mode 100644 packages/extension/src/providers/bitcoin/networks/icons/firo.svg diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d3024576..e1e454de9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: mkdir release docker run --rm --user "$(id -u):$(id -g)" -v `pwd`:/home:rw enkrypt-build-container /bin/bash -c "yarn build:all" docker run --rm --user "$(id -u):$(id -g)" -v `pwd`:/home:rw enkrypt-build-container /bin/bash -c "cd packages/extension && yarn build:chrome && yarn zip" - mv packages/extension/dist/release.zip release/enkrypt-chrome-edge-opera-${{ steps.get_release_tag.outputs.VERSION }}.zip + mv packages/extension/dist/release.zip release/enkrypt-chrome-edge-${{ steps.get_release_tag.outputs.VERSION }}.zip docker run --rm --user "$(id -u):$(id -g)" -v `pwd`:/home:rw enkrypt-build-container /bin/bash -c "cd packages/extension && yarn build:opera && yarn zip" mv packages/extension/dist/release.zip release/enkrypt-opera-${{ steps.get_release_tag.outputs.VERSION }}.zip docker run --rm --user "$(id -u):$(id -g)" -v `pwd`:/home:rw enkrypt-build-container /bin/bash -c "cd packages/extension && yarn build:firefox && yarn zip" diff --git a/.gitignore b/.gitignore index 306c1fc2b..70fd182c8 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,4 @@ dist # IDE .history +.idea diff --git a/README.md b/README.md index 43c3415c9..db71f1a9a 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Enkrypt is a web3 wallet built from the ground up to support the multi-chain fut - ZChains - zkSync - zkSync Goerli +- Firo - 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/src/libs/keyring/public-keyring.ts b/packages/extension/src/libs/keyring/public-keyring.ts index bdfd73589..9a36076d1 100644 --- a/packages/extension/src/libs/keyring/public-keyring.ts +++ b/packages/extension/src/libs/keyring/public-keyring.ts @@ -77,6 +77,16 @@ class PublicKeyRing { walletType: WalletType.mnemonic, isHardware: false, }; + allKeys["TMSnbcpSw9JhteaJqioT2sz2sW1Qhqyf2Q"] = { + address: "TMSnbcpSw9JhteaJqioT2sz2sW1Qhqyf2Q", + basePath: "m/44'/1'/0'/0", + name: "fake firo account #1", + pathIndex: 0, + publicKey: "0x0", + signerType: SignerType.secp256k1btc, + walletType: WalletType.mnemonic, + isHardware: false, + }; allKeys['77hREDDaAiimedtD9bR1JDMgYLW3AA5yPvD91pvrueRp'] = { address: '77hREDDaAiimedtD9bR1JDMgYLW3AA5yPvD91pvrueRp', basePath: "m/44'/501'/0'/1", diff --git a/packages/extension/src/providers/bitcoin/libs/activity-handlers/index.ts b/packages/extension/src/providers/bitcoin/libs/activity-handlers/index.ts index 52fb5cc0e..c96995c3a 100644 --- a/packages/extension/src/providers/bitcoin/libs/activity-handlers/index.ts +++ b/packages/extension/src/providers/bitcoin/libs/activity-handlers/index.ts @@ -1,3 +1,4 @@ import haskoinHandler from './providers/haskoin'; import ssHandler from './providers/ss'; -export { haskoinHandler, ssHandler }; +import firoHandler from "./providers/firo"; +export { haskoinHandler, ssHandler, firoHandler }; diff --git a/packages/extension/src/providers/bitcoin/libs/activity-handlers/providers/firo/index.ts b/packages/extension/src/providers/bitcoin/libs/activity-handlers/providers/firo/index.ts new file mode 100644 index 000000000..49b234745 --- /dev/null +++ b/packages/extension/src/providers/bitcoin/libs/activity-handlers/providers/firo/index.ts @@ -0,0 +1,107 @@ +import MarketData from "@/libs/market-data"; +import { FiroTxType } from "@/providers/bitcoin/types"; +import { Activity, ActivityStatus, ActivityType } from "@/types/activity"; +import { BaseNetwork } from "@/types/base-network"; + +export default async ( + network: BaseNetwork, + pubkey: string +): Promise => { + return fetch( + `${network.node}/insight-api-zcoin/txs?address=${network.displayAddress( + pubkey + )}&pageSize=40` + ) + .then((res) => res.json()) + .then(async (txs: { txs: FiroTxType[] }) => { + if ((txs as any).message) return []; + let tokenPrice = "0"; + if (network.coingeckoID) { + const marketData = new MarketData(); + await marketData + .getTokenPrice(network.coingeckoID) + .then((mdata) => (tokenPrice = mdata || "0")); + } + + const address = network.displayAddress(pubkey); + + const cleanedTxs = txs.txs.map((tx) => { + return { + ...tx, + vin: tx.vin.filter((vi) => vi.addr), + vout: tx.vout.filter((vo) => vo.scriptPubKey.addresses), + }; + }); + + return cleanedTxs.map((tx) => { + const isIncoming = !tx.vin.find((i) => i.addr === address); + + let toAddress = ""; + let value = 0; + + if (isIncoming) { + const relevantOut = tx.vout.find( + (tx) => tx.scriptPubKey.addresses![0] === address + ); + if (relevantOut) { + toAddress = relevantOut.scriptPubKey.addresses![0]; + value = Number(relevantOut.value); + } + } else { + const relevantOut = tx.vout.find( + (tx) => tx.scriptPubKey.addresses![0] !== address + ); + if (relevantOut) { + toAddress = relevantOut.scriptPubKey.addresses![0]; + value = Number(relevantOut.value); + } else { + toAddress = tx.vout[0].scriptPubKey.addresses![0]; + value = Number(tx.vout[0].value); + } + } + + const act: Activity = { + from: tx.vin?.[0]?.addr, + isIncoming, + network: network.name, + status: + tx.blockheight > 0 + ? ActivityStatus.success + : ActivityStatus.pending, + timestamp: Number(tx.time) * 1000, + to: toAddress, + token: { + decimals: network.decimals, + icon: network.icon, + name: network.name_long, + symbol: network.currencyName, + coingeckoID: network.coingeckoID, + price: tokenPrice, + }, + transactionHash: tx.txid, + type: ActivityType.transaction, + value: (+value * 100000000).toString(), + rawInfo: { + blockNumber: tx.blockheight, + fee: Number(tx?.fees), + inputs: tx.vin.map((input) => ({ + address: input.addr, + value: Number(input.value), + })), + outputs: tx.vout.map((output) => ({ + address: output.scriptPubKey.addresses![0], + value: Number(output.value), + pkscript: output.scriptPubKey.hex, + })), + transactionHash: tx.txid, + timestamp: Number(tx.time) * 1000, + }, + }; + return act; + }); + }) + .catch((error) => { + console.log({ error }); + return []; + }); +}; diff --git a/packages/extension/src/providers/bitcoin/libs/api-firo.ts b/packages/extension/src/providers/bitcoin/libs/api-firo.ts new file mode 100644 index 000000000..f30d26ca7 --- /dev/null +++ b/packages/extension/src/providers/bitcoin/libs/api-firo.ts @@ -0,0 +1,158 @@ +import { BTCRawInfo } from "@/types/activity"; +import { ProviderAPIInterface } from "@/types/provider"; +import { + BitcoinNetworkInfo, + FiroTxType, + FiroUnspentType, + HaskoinUnspentType, +} from "../types"; +import { toBN } from "web3-utils"; +import cacheFetch from "@/libs/cache-fetch"; +import { getAddress as getBitcoinAddress } from "../types/bitcoin-network"; +import { filterOutOrdinals } from "./filter-ordinals"; + +class API implements ProviderAPIInterface { + node: string; + networkInfo: BitcoinNetworkInfo; + + constructor(node: string, networkInfo: BitcoinNetworkInfo) { + this.node = node; + this.networkInfo = networkInfo; + } + + public get api() { + return this; + } + + private getAddress(pubkey: string) { + return getBitcoinAddress(pubkey, this.networkInfo); + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + async init(): Promise {} + + async getRawTransaction(hash: string): Promise { + return fetch(`${this.node}/insight-api-zcoin/rawtx/${hash}`) + .then((res) => res.json()) + .then((tx: { hex: string; error: unknown }) => { + if ((tx as any).error) return null; + if (!tx.hex) return null; + return `0x${tx.hex}`; + }); + } + async getTransactionStatus(hash: string): Promise { + return fetch(`${this.node}/insight-api-zcoin/tx/${hash}`) + .then((res) => res.json()) + .then((tx: FiroTxType) => { + if ((tx as any).message) return null; + if (tx.blockheight < 0) return null; + const rawInfo: BTCRawInfo = { + blockNumber: tx.blockheight, + fee: Number(tx.fees), + inputs: tx.vin + .filter((t) => t.addresses && t.addresses.length) + .map((input) => ({ + address: input.addresses![0], + value: Number(input.value), + })), + outputs: tx.vout + .filter( + (t) => t.scriptPubKey.addresses && t.scriptPubKey.addresses.length + ) + .map((output) => ({ + address: output.scriptPubKey.addresses![0], + value: Number(output.value), + pkscript: output.scriptPubKey.hex, + })), + transactionHash: tx.txid, + timestamp: tx.time * 1000, + }; + return rawInfo; + }); + } + + async getBalance(pubkey: string): Promise { + const address = pubkey.length < 64 ? pubkey : this.getAddress(pubkey); + return fetch(`${this.node}/insight-api-zcoin/addr/${address}/?noTxList=1`) + .then((res) => res.json()) + .then( + (balance: { balanceSat: string; unconfirmedBalanceSat: string }) => { + if ((balance as any).message) return "0"; + return toBN(balance.balanceSat) + .add(toBN(balance.unconfirmedBalanceSat)) + .toString(); + } + ) + .catch(() => "0"); + } + + async broadcastTx(rawtx: string): Promise { + return fetch(`${this.node}/insight-api-zcoin/tx/send`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ rawtx }), + }) + .then((res) => res.json()) + .then((response) => { + if (response.error) { + return Promise.reject(response.message); + } + return true; + }); + } + + async FiroToHaskoinUTXOs( + FiroUTXOs: FiroUnspentType[], + address: string + ): Promise { + const ret: HaskoinUnspentType[] = []; + for (const utx of FiroUTXOs) { + const rawTxRes = (await cacheFetch({ + url: `${this.node}/insight-api-zcoin/rawtx/${utx.txid}`, + })) as { rawtx: string }; + const res = (await cacheFetch({ + url: `${this.node}/insight-api-zcoin/tx/${utx.txid}`, + })) as FiroTxType; + + ret.push({ + address, + block: { + height: res.blockheight, + position: 0, + }, + index: utx.vout, + pkscript: res.vout[utx.vout].scriptPubKey.hex, + txid: utx.txid, + value: Number(utx.satoshis), + raw: rawTxRes.rawtx, + }); + } + ret.sort((a, b) => { + return a.value - b.value; + }); + return [ret.at(-1)!]; // TODO: check or filter same values + } + + async getUTXOs(pubkey: string): Promise { + const address = pubkey.length < 64 ? pubkey : this.getAddress(pubkey); + return fetch(`${this.node}/insight-api-zcoin/addr/${address}/utxo`) + .then((res) => res.json()) + .then(async (utxos: FiroUnspentType[]) => { + if ((utxos as any).message || !utxos.length) return []; + return filterOutOrdinals( + address, + this.networkInfo.name, + await this.FiroToHaskoinUTXOs(utxos, address) + ).then((futxos) => { + futxos.sort((a, b) => { + return a.value - b.value; + }); + return futxos; + }); + }); + } +} +export default API; diff --git a/packages/extension/src/providers/bitcoin/networks/firo-testnet.ts b/packages/extension/src/providers/bitcoin/networks/firo-testnet.ts new file mode 100644 index 000000000..d4907f402 --- /dev/null +++ b/packages/extension/src/providers/bitcoin/networks/firo-testnet.ts @@ -0,0 +1,55 @@ +import { NetworkNames } from "@enkryptcom/types"; +import { + BitcoinNetwork, + BitcoinNetworkOptions, + PaymentType, +} from "../types/bitcoin-network"; +import { firoHandler } from "../libs/activity-handlers"; +import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import FiroApi from "@/providers/bitcoin/libs/api-firo"; +import { GasPriceTypes } from "@/providers/common/types"; + +const firoOptions: BitcoinNetworkOptions = { + name: NetworkNames.FiroTest, + name_long: "Firo Testnet", + homePage: "https://testexplorer.firo.org", + blockExplorerTX: "https://testexplorer.firo.org/tx/[[txHash]]", + blockExplorerAddr: "https://testexplorer.firo.org/address/[[address]]", + isTestNetwork: true, + currencyName: "tFIRO", + currencyNameLong: "tFiro", + icon: require("./icons/firo.svg"), + decimals: 8, + node: "https://testexplorer.firo.org", + coingeckoID: "zcoin", + dust: 0.0001, + apiType: FiroApi, + activityHandler: wrapActivityHandler(firoHandler), + basePath: "m/44'/1'/0'/0", + feeHandler: () => + Promise.resolve({ + [GasPriceTypes.FASTEST]: 25, + [GasPriceTypes.FAST]: 20, + [GasPriceTypes.REGULAR]: 10, + [GasPriceTypes.ECONOMY]: 5, + }), + networkInfo: { + name: NetworkNames.FiroTest, + messagePrefix: "\x18Zcoin Signed Message:\n", + bech32: "tb", + bip32: { + public: 0x043587cf, + private: 0x04358394, + }, + pubKeyHash: 0x41, + scriptHash: 0xb2, + wif: 0xb9, + dustThreshold: null, + paymentType: PaymentType.P2PKH, + maxFeeRate: 5000 * 2, + }, +}; + +const firoTestnet = new BitcoinNetwork(firoOptions); + +export default firoTestnet; diff --git a/packages/extension/src/providers/bitcoin/networks/firo.ts b/packages/extension/src/providers/bitcoin/networks/firo.ts new file mode 100644 index 000000000..a450cc014 --- /dev/null +++ b/packages/extension/src/providers/bitcoin/networks/firo.ts @@ -0,0 +1,55 @@ +import { NetworkNames } from "@enkryptcom/types"; +import { + BitcoinNetwork, + BitcoinNetworkOptions, + PaymentType, +} from "../types/bitcoin-network"; +import { firoHandler } from "../libs/activity-handlers"; +import wrapActivityHandler from "@/libs/activity-state/wrap-activity-handler"; +import FiroApi from "@/providers/bitcoin/libs/api-firo"; +import { GasPriceTypes } from "@/providers/common/types"; + +const firoOptions: BitcoinNetworkOptions = { + name: NetworkNames.Firo, + name_long: "Firo", + homePage: "https://explorer.firo.org", + blockExplorerTX: "https://explorer.firo.org/tx/[[txHash]]", + blockExplorerAddr: "https://explorer.firo.org/address/[[address]]", + isTestNetwork: false, + currencyName: "FIRO", + currencyNameLong: "Firo", + icon: require("./icons/firo.svg"), + decimals: 8, + node: "https://explorer.firo.org", + coingeckoID: "zcoin", + dust: 0.0001, + apiType: FiroApi, + activityHandler: wrapActivityHandler(firoHandler), + basePath: "m/44'/136'/0'/0", + feeHandler: () => + Promise.resolve({ + [GasPriceTypes.FASTEST]: 25, + [GasPriceTypes.FAST]: 20, + [GasPriceTypes.REGULAR]: 10, + [GasPriceTypes.ECONOMY]: 5, + }), + networkInfo: { + name: NetworkNames.Firo, + messagePrefix: "\x18Zcoin Signed Message:\n", + bech32: "bc", + bip32: { + public: 0x0488b21e, + private: 0x0488ade4, + }, + pubKeyHash: 0x52, + scriptHash: 0x07, + wif: 0xd2, + dustThreshold: null, + paymentType: PaymentType.P2PKH, + maxFeeRate: 5000 * 2, + }, +}; + +const firo = new BitcoinNetwork(firoOptions); + +export default firo; diff --git a/packages/extension/src/providers/bitcoin/networks/icons/firo.svg b/packages/extension/src/providers/bitcoin/networks/icons/firo.svg new file mode 100644 index 000000000..5f09597a6 --- /dev/null +++ b/packages/extension/src/providers/bitcoin/networks/icons/firo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/packages/extension/src/providers/bitcoin/networks/index.ts b/packages/extension/src/providers/bitcoin/networks/index.ts index f72d5b451..104fedf6c 100644 --- a/packages/extension/src/providers/bitcoin/networks/index.ts +++ b/packages/extension/src/providers/bitcoin/networks/index.ts @@ -2,10 +2,14 @@ import btcNode from './bitcoin'; import btcTestNode from './bitcoin-testnet'; import ltcNode from './litecoin'; import dogeNode from './dogecoin'; +import firoTestnet from "./firo-testnet"; +import firo from "./firo"; export default { bitcoin: btcNode, bitcoinTest: btcTestNode, + firoTest: firoTestnet, + firo: firo, litecoin: ltcNode, dogecoin: dogeNode, }; diff --git a/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts b/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts index 82425336a..ddac2f801 100644 --- a/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts +++ b/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts @@ -21,6 +21,7 @@ import { BTCToken } from './btc-token'; import { GasPriceTypes } from '@/providers/common/types'; import type HaskoinAPI from '@/providers/bitcoin/libs/api'; import type SSAPI from '@/providers/bitcoin/libs/api-ss'; +import type FiroAPI from "@/providers/bitcoin/libs/api-firo"; import { NFTCollection } from '@/types/nft'; export enum PaymentType { @@ -52,7 +53,7 @@ export interface BitcoinNetworkOptions { network: BaseNetwork, address: string, ) => Promise; - apiType: typeof HaskoinAPI | typeof SSAPI; + apiType: typeof HaskoinAPI | typeof SSAPI | typeof FiroAPI; } export const getAddress = (pubkey: string, network: BitcoinNetworkInfo) => { diff --git a/packages/extension/src/providers/bitcoin/types/index.ts b/packages/extension/src/providers/bitcoin/types/index.ts index 4a9fab70f..232966d51 100644 --- a/packages/extension/src/providers/bitcoin/types/index.ts +++ b/packages/extension/src/providers/bitcoin/types/index.ts @@ -50,6 +50,18 @@ export interface SSUnspentType { height: number; confirmations: number; } + +export interface FiroUnspentType { + address: string; + amount: number; + confirmations: number; + height: number; + satoshis: number; + scriptPubKey: string; + txid: string; + vout: number; +} + export interface HaskoinTxType { txid: string; size: number; @@ -86,6 +98,7 @@ export interface SSTxType { vin: { txid: string; addresses?: string[]; + addr: string; value: string; }[]; vout: { @@ -97,6 +110,20 @@ export interface SSTxType { }[]; } +export interface FiroTxType + extends Omit { + fees: number; + time: number; + blockheight: number; + vout: { + value: string; + scriptPubKey: { + addresses?: string[]; + hex: string; + }; + }[]; +} + export interface RPCTxType { to: string; value: number; diff --git a/packages/hw-wallets/src/ledger/bitcoin/configs.ts b/packages/hw-wallets/src/ledger/bitcoin/configs.ts index 015b0e323..a2d4e7b35 100644 --- a/packages/hw-wallets/src/ledger/bitcoin/configs.ts +++ b/packages/hw-wallets/src/ledger/bitcoin/configs.ts @@ -1,5 +1,5 @@ import { NetworkNames } from "@enkryptcom/types"; -import { bip44Paths } from "../../configs"; +import { bip44Paths } from "@src/configs"; const supportedPaths = { [NetworkNames.Bitcoin]: [bip44Paths.bitcoinSegwitLedger], diff --git a/packages/types/src/networks.ts b/packages/types/src/networks.ts index a93524fa2..162a4ce6e 100755 --- a/packages/types/src/networks.ts +++ b/packages/types/src/networks.ts @@ -18,6 +18,8 @@ export enum NetworkNames { Westend = "WND", Bitcoin = "BTC", BitcoinTest = "BTCTest", + Firo = "Firo", + FiroTest = "FiroTest", Astar = "ASTR", Shiden = "SDN", ShidenEVM = "SDNEVM", From 0d16daa09631011d0f0be1cd56c86fc87a4ef3a4 Mon Sep 17 00:00:00 2001 From: narekpetrosyan Date: Tue, 10 Dec 2024 18:48:12 +0400 Subject: [PATCH 2/4] feature: add firo network spark address generation and vew functionalities --- .../src/libs/spark-handler/callRPC.ts | 29 ++ .../extension/src/libs/spark-handler/index.ts | 20 ++ packages/extension/src/ui/action/App.vue | 30 ++- .../components/accounts-header/index.vue | 4 + .../extension/src/ui/action/types/account.ts | 11 + .../src/ui/action/views/deposit/index.vue | 251 +++++++++++++++--- .../action/views/network-activity/index.vue | 5 +- .../ui/action/views/network-assets/index.vue | 1 + 8 files changed, 314 insertions(+), 37 deletions(-) create mode 100644 packages/extension/src/libs/spark-handler/callRPC.ts create mode 100644 packages/extension/src/libs/spark-handler/index.ts diff --git a/packages/extension/src/libs/spark-handler/callRPC.ts b/packages/extension/src/libs/spark-handler/callRPC.ts new file mode 100644 index 000000000..9dce2af48 --- /dev/null +++ b/packages/extension/src/libs/spark-handler/callRPC.ts @@ -0,0 +1,29 @@ +import axios from "axios"; + +const rpcURL = "https://firo-rpc.publicnode.com/"; + +export async function callRPC( + method: string, + params = [] +): Promise { + try { + const response = await axios.post( + rpcURL, + { + jsonrpc: "1.0", + id: "js-client", + method: method, + params: params, + }, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + return response.data.result; + } catch (error) { + console.error("RPC Error:", error); + throw error; + } +} diff --git a/packages/extension/src/libs/spark-handler/index.ts b/packages/extension/src/libs/spark-handler/index.ts new file mode 100644 index 000000000..e2c52a5c1 --- /dev/null +++ b/packages/extension/src/libs/spark-handler/index.ts @@ -0,0 +1,20 @@ +import { SparkAccount } from "@/ui/action/types/account"; +import { callRPC } from "./callRPC"; + +export async function getSparkState(): Promise { + const [allAddresses, sparkBalance] = await Promise.all([ + callRPC>("getallsparkaddresses"), + callRPC("getsparkbalance"), + ]); + + return { + defaultAddress: Object.values(allAddresses)?.at(-1) ?? "", + allAddresses: Object.values(allAddresses), + sparkBalance, + }; +} + +export async function generateSparkAddress(): Promise { + const newSparkAddress = await callRPC("getnewsparkaddress"); + return newSparkAddress[0]; +} diff --git a/packages/extension/src/ui/action/App.vue b/packages/extension/src/ui/action/App.vue index 311ac7647..3101b1eb0 100644 --- a/packages/extension/src/ui/action/App.vue +++ b/packages/extension/src/ui/action/App.vue @@ -49,6 +49,7 @@ @address-changed="onSelectedAddressChanged" @select:subnetwork="onSelectedSubnetworkChange" @toggle:deposit="toggleDepositWindow" + @action:generate-new-spark="generateNewSparkAddress" /> @@ -60,6 +61,7 @@ :account-info="accountHeaderData" @update:init="init" @toggle:deposit="toggleDepositWindow" + @action:generate-new-spark="generateNewSparkAddress" @open:buy-action="openBuyPage" /> @@ -131,7 +133,7 @@ import HoldIcon from './icons/common/hold-icon.vue'; import LogoMin from './icons/common/logo-min.vue'; import ManageNetworksIcon from './icons/common/manage-networks-icon.vue'; import SettingsIcon from './icons/common/settings-icon.vue'; -import { AccountsHeaderData } from './types/account'; +import { AccountsHeaderData, SparkAccount } from './types/account'; import AddNetwork from './views/add-network/index.vue'; import ModalRate from './views/modal-rate/index.vue'; import Settings from './views/settings/index.vue'; @@ -144,6 +146,7 @@ import { trackBuyEvents, trackNetworkSelected } from '@/libs/metrics'; import { getLatestEnkryptVersion } from '@action/utils/browser'; import { gt as semverGT } from 'semver'; import { BuyEventType, NetworkChangeEvents } from '@/libs/metrics/types'; +import { generateSparkAddress, getSparkState } from "@/libs/spark-handler"; const domainState = new DomainState(); const networksState = new NetworksState(); @@ -155,6 +158,7 @@ const accountHeaderData = ref({ inactiveAccounts: [], selectedAccount: null, activeBalances: [], + sparkAccount: null, }); const isOpenMore = ref(false); let timeout: ReturnType | null = null; @@ -275,8 +279,19 @@ onMounted(async () => { const updateGradient = (newGradient: string) => { //hack may be there is a better way. less.modifyVars doesnt work if (appMenuRef.value) - (appMenuRef.value as HTMLElement).style.background = - `radial-gradient(137.35% 97% at 100% 50%, rgba(250, 250, 250, 0.94) 0%, rgba(250, 250, 250, 0.96) 28.91%, rgba(250, 250, 250, 0.98) 100%), linear-gradient(180deg, ${newGradient} 80%, #684CFF 100%)`; + ( + appMenuRef.value as HTMLElement + ).style.background = `radial-gradient(137.35% 97% at 100% 50%, rgba(250, 250, 250, 0.94) 0%, rgba(250, 250, 250, 0.96) 28.91%, rgba(250, 250, 250, 0.98) 100%), linear-gradient(180deg, ${newGradient} 80%, #684CFF 100%)`; +}; +const generateNewSparkAddress = async () => { + const newSparkAddressResponse = await generateSparkAddress(); + if (accountHeaderData.value.sparkAccount) { + accountHeaderData.value.sparkAccount.defaultAddress = + newSparkAddressResponse; + accountHeaderData.value.sparkAccount.allAddresses.push( + newSparkAddressResponse + ); + } }; const setNetwork = async (network: BaseNetwork) => { trackNetworkSelected(NetworkChangeEvents.NetworkChangePopup, { @@ -298,12 +313,21 @@ const setNetwork = async (network: BaseNetwork) => { if (found) selectedAccount = found; } + let sparkAccount: SparkAccount | null = null; + + if (network.name === NetworkNames.Firo) { + const sparkAccountResponse = await getSparkState(); + sparkAccount = { ...sparkAccountResponse }; + } + accountHeaderData.value = { activeAccounts, inactiveAccounts, selectedAccount, activeBalances: activeAccounts.map(() => '~'), + sparkAccount, }; + currentNetwork.value = network; router.push({ name: 'assets', params: { id: network.name } }); const tabId = await domainState.getCurrentTabId(); 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 29d2a70d1..46bc87b6b 100644 --- a/packages/extension/src/ui/action/components/accounts-header/index.vue +++ b/packages/extension/src/ui/action/components/accounts-header/index.vue @@ -9,6 +9,7 @@ :network="network" v-bind="$attrs" @toggle:deposit="$emit('toggle:deposit')" + @action:generate-new-spark="$emit('action:generate-new-spark')" @select:subnetwork="$emit('select:subnetwork', $event)" /> @@ -23,9 +24,11 @@ @@ -41,6 +44,7 @@ import { BaseNetwork } from '@/types/base-network'; const router = useRouter(); defineEmits<{ (e: 'toggle:deposit'): void; + (e: "action:generate-new-spark"): void; (e: 'select:subnetwork', id: string): void; }>(); const showAccounts = ref(false); diff --git a/packages/extension/src/ui/action/types/account.ts b/packages/extension/src/ui/action/types/account.ts index 3d801a2e4..d744cb066 100644 --- a/packages/extension/src/ui/action/types/account.ts +++ b/packages/extension/src/ui/action/types/account.ts @@ -8,9 +8,20 @@ export interface Account { primaryToken: Token; } +export interface SparkAccount { + defaultAddress: string; + allAddresses: string[]; + sparkBalance: { + availableBalance: number; + unconfirmedBalance: number; + fullBalance: number; + }; +} + export interface AccountsHeaderData { selectedAccount: EnkryptAccount | null; activeAccounts: EnkryptAccount[]; inactiveAccounts: EnkryptAccount[]; activeBalances: string[]; + sparkAccount: SparkAccount | null; } diff --git a/packages/extension/src/ui/action/views/deposit/index.vue b/packages/extension/src/ui/action/views/deposit/index.vue index 2f4eca623..c37c8416b 100644 --- a/packages/extension/src/ui/action/views/deposit/index.vue +++ b/packages/extension/src/ui/action/views/deposit/index.vue @@ -13,48 +13,114 @@ {{ depositCopy }}

-
- +
+ +
- + + diff --git a/packages/extension/src/ui/action/App.vue b/packages/extension/src/ui/action/App.vue index 3101b1eb0..7bda88cc6 100644 --- a/packages/extension/src/ui/action/App.vue +++ b/packages/extension/src/ui/action/App.vue @@ -63,6 +63,7 @@ @toggle:deposit="toggleDepositWindow" @action:generate-new-spark="generateNewSparkAddress" @open:buy-action="openBuyPage" + @update:spark-state-changed="getSparkAccountState" /> @@ -293,6 +294,15 @@ const generateNewSparkAddress = async () => { ); } }; +const getSparkAccountState = async (network: BaseNetwork) => { + if ( + network.name === NetworkNames.Firo || + network.name === NetworkNames.FiroTest + ) { + const sparkAccountResponse = await getSparkState(); + accountHeaderData.value.sparkAccount = { ...sparkAccountResponse }; + } +}; const setNetwork = async (network: BaseNetwork) => { trackNetworkSelected(NetworkChangeEvents.NetworkChangePopup, { provider: network.provider, @@ -315,7 +325,10 @@ const setNetwork = async (network: BaseNetwork) => { let sparkAccount: SparkAccount | null = null; - if (network.name === NetworkNames.Firo) { + if ( + network.name === NetworkNames.Firo || + network.name === NetworkNames.FiroTest + ) { const sparkAccountResponse = await getSparkState(); sparkAccount = { ...sparkAccountResponse }; } diff --git a/packages/extension/src/ui/action/router/index.ts b/packages/extension/src/ui/action/router/index.ts index b68e90e00..5c552122a 100644 --- a/packages/extension/src/ui/action/router/index.ts +++ b/packages/extension/src/ui/action/router/index.ts @@ -9,6 +9,7 @@ import Intro from '@action/views/intro/index.vue'; import Swap from '@action/views/swap/index.vue'; import SwapBestOffer from '@action/views/swap/views/swap-best-offer/index.vue'; import VerifyTransaction from '@action/views/verify-transaction/index.vue'; +import VerifySendToSparkTransaction from "@action/views/verify-send-to-spark-transaction/index.vue"; import SendTransaction from '@action/views/send-transaction/index.vue'; const routes = { @@ -68,6 +69,13 @@ const routes = { }, name: 'verify-transaction', }, + verifySendToSpark: { + path: "/verify-send-to-spark-transaction/:id?", + components: { + view: VerifySendToSparkTransaction, + }, + name: "verify-send-to-spark", + }, swap: { path: '/swap/:id?', components: { diff --git a/packages/extension/src/ui/action/views/deposit/index.vue b/packages/extension/src/ui/action/views/deposit/index.vue index c37c8416b..565fb1db8 100644 --- a/packages/extension/src/ui/action/views/deposit/index.vue +++ b/packages/extension/src/ui/action/views/deposit/index.vue @@ -28,7 +28,7 @@
-
+
+
+
+
+ +
+

Verify Transaction

+ + + +
+ +

+ Double check the information and confirm transaction +

+
+ + + + + + {{ errorMsg }} +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+ + + + + From 643b114b40e704deceb94959999c1f258c45a563 Mon Sep 17 00:00:00 2001 From: narekpetrosyan Date: Tue, 24 Dec 2024 14:15:00 +0400 Subject: [PATCH 4/4] feature: add send from spark to spark address functionality --- .../src/libs/spark-handler/callRPC.ts | 21 +- .../extension/src/libs/spark-handler/index.ts | 15 + .../src/providers/bitcoin/libs/api-firo.ts | 6 +- .../bitcoin/networks/firo-testnet.ts | 3 +- .../src/providers/bitcoin/networks/firo.ts | 3 +- .../bitcoin/types/bitcoin-network.ts | 4 +- .../components/send-spark-address-input.vue | 166 + .../bitcoin/ui/send-transaction/index.vue | 566 +- .../send-transaction/tabs/spark-send-tab.vue | 302 + .../tabs/transparent-send-tab.vue | 79 +- packages/extension/src/ui/action/App.vue | 10 +- .../extension/src/ui/action/router/index.ts | 8 + .../index.vue | 348 + .../index.vue | 14 +- .../hw-wallets/src/ledger/bitcoin/configs.ts | 2 +- yarn.lock | 6892 +++++++++-------- 16 files changed, 4650 insertions(+), 3789 deletions(-) create mode 100644 packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-spark-address-input.vue create mode 100644 packages/extension/src/providers/bitcoin/ui/send-transaction/tabs/spark-send-tab.vue create mode 100644 packages/extension/src/ui/action/views/verify-send-from-spark-transaction/index.vue diff --git a/packages/extension/src/libs/spark-handler/callRPC.ts b/packages/extension/src/libs/spark-handler/callRPC.ts index f369aa27f..2901adfb2 100644 --- a/packages/extension/src/libs/spark-handler/callRPC.ts +++ b/packages/extension/src/libs/spark-handler/callRPC.ts @@ -1,14 +1,25 @@ import axios from "axios"; -const rpcURL = "https://firo-rpc.publicnode.com/"; +const DEFAULT_TIMEOUT = 30000; + +const RPC_URLS = { + mainnet: "https://firo-rpc.publicnode.com/", +}; + +const axiosInstance = axios.create({ + timeout: DEFAULT_TIMEOUT, + headers: { + "Content-Type": "application/json", + } +}); export async function callRPC( method: string, params?: object ): Promise { try { - const response = await axios.post( - rpcURL, + const response = await axiosInstance.post( + RPC_URLS['mainnet'], { jsonrpc: "1.0", id: "js-client", @@ -21,6 +32,10 @@ export async function callRPC( }, } ); + + if (!response.data || response.data.result === undefined) { + throw new Error('Invalid RPC response structure'); + } return response.data.result; } catch (error) { console.error("RPC Error:", error); diff --git a/packages/extension/src/libs/spark-handler/index.ts b/packages/extension/src/libs/spark-handler/index.ts index b6eda8da6..686b20a4c 100644 --- a/packages/extension/src/libs/spark-handler/index.ts +++ b/packages/extension/src/libs/spark-handler/index.ts @@ -26,3 +26,18 @@ export async function sendToSparkAddress(to: string, amount: string) { }, ]); } + +export async function sendFromSparkAddress( + to: string, + amount: string, + subtractFee = false +): Promise { + return await callRPC("spendspark", [ + { + [to]: { + amount: Number(amount), + subtractFee, + }, + }, + ]); +} diff --git a/packages/extension/src/providers/bitcoin/libs/api-firo.ts b/packages/extension/src/providers/bitcoin/libs/api-firo.ts index f30d26ca7..f45201aeb 100644 --- a/packages/extension/src/providers/bitcoin/libs/api-firo.ts +++ b/packages/extension/src/providers/bitcoin/libs/api-firo.ts @@ -28,7 +28,7 @@ class API implements ProviderAPIInterface { return getBitcoinAddress(pubkey, this.networkInfo); } - // eslint-disable-next-line @typescript-eslint/no-empty-function + async init(): Promise {} async getRawTransaction(hash: string): Promise { @@ -133,7 +133,7 @@ class API implements ProviderAPIInterface { ret.sort((a, b) => { return a.value - b.value; }); - return [ret.at(-1)!]; // TODO: check or filter same values + return ret; } async getUTXOs(pubkey: string): Promise { @@ -145,7 +145,7 @@ class API implements ProviderAPIInterface { return filterOutOrdinals( address, this.networkInfo.name, - await this.FiroToHaskoinUTXOs(utxos, address) + [(await this.FiroToHaskoinUTXOs(utxos, address)).at(-1)!] ).then((futxos) => { futxos.sort((a, b) => { return a.value - b.value; diff --git a/packages/extension/src/providers/bitcoin/networks/firo-testnet.ts b/packages/extension/src/providers/bitcoin/networks/firo-testnet.ts index d4907f402..b1bee81de 100644 --- a/packages/extension/src/providers/bitcoin/networks/firo-testnet.ts +++ b/packages/extension/src/providers/bitcoin/networks/firo-testnet.ts @@ -1,4 +1,5 @@ import { NetworkNames } from "@enkryptcom/types"; +import icon from './icons/firo.svg'; import { BitcoinNetwork, BitcoinNetworkOptions, @@ -18,7 +19,7 @@ const firoOptions: BitcoinNetworkOptions = { isTestNetwork: true, currencyName: "tFIRO", currencyNameLong: "tFiro", - icon: require("./icons/firo.svg"), + icon, decimals: 8, node: "https://testexplorer.firo.org", coingeckoID: "zcoin", diff --git a/packages/extension/src/providers/bitcoin/networks/firo.ts b/packages/extension/src/providers/bitcoin/networks/firo.ts index a450cc014..5c36a1f59 100644 --- a/packages/extension/src/providers/bitcoin/networks/firo.ts +++ b/packages/extension/src/providers/bitcoin/networks/firo.ts @@ -1,4 +1,5 @@ import { NetworkNames } from "@enkryptcom/types"; +import icon from './icons/firo.svg'; import { BitcoinNetwork, BitcoinNetworkOptions, @@ -18,7 +19,7 @@ const firoOptions: BitcoinNetworkOptions = { isTestNetwork: false, currencyName: "FIRO", currencyNameLong: "Firo", - icon: require("./icons/firo.svg"), + icon, decimals: 8, node: "https://explorer.firo.org", coingeckoID: "zcoin", diff --git a/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts b/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts index ddac2f801..af4157692 100644 --- a/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts +++ b/packages/extension/src/providers/bitcoin/types/bitcoin-network.ts @@ -57,7 +57,9 @@ export interface BitcoinNetworkOptions { } export const getAddress = (pubkey: string, network: BitcoinNetworkInfo) => { - if (pubkey.length < 64) return pubkey; + if (pubkey.length >= 144 || pubkey.length < 64) { + return pubkey; + } const { address } = payments[network.paymentType]({ network, pubkey: hexToBuffer(pubkey), diff --git a/packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-spark-address-input.vue b/packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-spark-address-input.vue new file mode 100644 index 000000000..47626c946 --- /dev/null +++ b/packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-spark-address-input.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue b/packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue index 66138e361..c9a35d3d8 100644 --- a/packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue +++ b/packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue @@ -8,166 +8,57 @@ @toggle-type="toggleSelector" /> - +
+ + +
- - - - - - - - - - - - - - - - - - -
-
- -
-
- -
-
+ + diff --git a/packages/extension/src/providers/bitcoin/ui/send-transaction/tabs/transparent-send-tab.vue b/packages/extension/src/providers/bitcoin/ui/send-transaction/tabs/transparent-send-tab.vue index 41cef2344..b93d4457a 100644 --- a/packages/extension/src/providers/bitcoin/ui/send-transaction/tabs/transparent-send-tab.vue +++ b/packages/extension/src/providers/bitcoin/ui/send-transaction/tabs/transparent-send-tab.vue @@ -23,33 +23,27 @@ />
-
- - -
- + - - -
@@ -221,7 +210,6 @@ const loadingAsset = new BTCToken({ }); const addressInputTo = ref(); -const sparkAddressInput = ref(); const isSendSpark = ref(false); const selected: string = route.params.id as string; const paramNFTData: NFTItem = JSON.parse( @@ -321,17 +309,12 @@ const sendButtonTitle = computed(() => { }); const isInputsValid = computed(() => { - if (isSendSpark.value) { - if (!isSparkAddress(sparkAddressInput.value)) { - return false; - } - } else { - if ( - !isAddress(addressTo.value, (props.network as BitcoinNetwork).networkInfo) - ) - return false; + if ( + !isSparkAddress(addressTo.value) && + !isAddress(addressTo.value, (props.network as BitcoinNetwork).networkInfo) + ) { + return false; } - if ( props.isSendToken && !isValidDecimals(sendAmount.value, selectedAsset.value.decimals!) @@ -411,10 +394,6 @@ const inputAddressTo = (text: string) => { addressTo.value = text; }; -const inputSparkAddressTo = (address: string) => { - sparkAddressInput.value = address; -}; - const toggleSelectContactFrom = (open: boolean) => { isOpenSelectContactFrom.value = open; }; @@ -447,10 +426,6 @@ const toggleSelectFee = () => { isOpenSelectFee.value = !isOpenSelectFee.value; }; -const toggleIsSendSpark = (value: boolean) => { - isSendSpark.value = value; -}; - const selectFee = (type: GasPriceTypes) => { selectedFee.value = type; isOpenSelectFee.value = false; @@ -469,8 +444,8 @@ const selectNFT = (item: NFTItemWithCollectionName) => { const sendSparkAction = async () => { const keyring = new PublicKeyRing(); const fromAccountInfo = await keyring.getAccount(addressFrom.value); - const toAmount = toBN(toBase(sendAmount.value, selectedAsset.value.decimals)); + router.push({ name: RouterNames.verifySendToSpark.name, query: { @@ -492,7 +467,7 @@ const sendSparkAction = async () => { fromAddressName: fromAccountInfo.name, gasFee: gasCostValues.value[selectedFee.value], gasPriceType: selectedFee.value, - toAddress: sparkAddressInput.value, + toAddress: addressTo.value, }), "utf8" ).toString("base64"), @@ -514,7 +489,7 @@ const sendAction = async () => { if (props.isSendToken) { txInfo.outputs.push({ - address: isSendSpark.value ? sparkAddressInput.value : addressTo.value, + address: addressTo.value, value: toAmount.toNumber(), }); } else { @@ -600,8 +575,8 @@ const sendAction = async () => { diff --git a/packages/extension/src/ui/action/views/verify-send-to-spark-transaction/index.vue b/packages/extension/src/ui/action/views/verify-send-to-spark-transaction/index.vue index b6a308333..0aaafb4cb 100644 --- a/packages/extension/src/ui/action/views/verify-send-to-spark-transaction/index.vue +++ b/packages/extension/src/ui/action/views/verify-send-to-spark-transaction/index.vue @@ -27,7 +27,7 @@ :network="network" /> @@ -95,6 +95,7 @@ import { sendToSparkAddress } from "@/libs/spark-handler"; import { isAxiosError } from "axios"; import { fromBase } from "@enkryptcom/utils"; import { BaseNetwork } from "@/types/base-network"; +import { isAddress } from "@/providers/bitcoin/libs/utils"; const emits = defineEmits<{ (e: "update:spark-state-changed", network: BaseNetwork): void; @@ -122,9 +123,12 @@ defineExpose({ verifyScrollRef }); onBeforeMount(async () => { network.value = (await getNetworkByName(selectedNetwork)!) as BitcoinNetwork; trackSendEvents(SendEventType.SendVerify, { network: network.value.name }); - account.value = await KeyRing.getAccount(txData.fromAddress); - isWindowPopup.value = account.value.isHardware; + if (isAddress(txData.fromAddress, network.value.networkInfo)) { + account.value = await KeyRing.getAccount(txData.fromAddress); + isWindowPopup.value = account.value.isHardware; + } }); + const close = () => { if (getCurrentContext() === "popup") { router.go(-1); @@ -212,8 +216,8 @@ const isHasScroll = () => {