From 27ef53ddb63e20767afe68f5b56441c24c4a0f75 Mon Sep 17 00:00:00 2001 From: slowbackspace Date: Sat, 28 Sep 2024 12:08:49 +0200 Subject: [PATCH] blockbook --- .vscode/settings.json | 3 + src/api/extension/index.js | 331 +++++++++++++++++++++++- src/api/util.js | 30 ++- src/config/config.js | 14 + src/config/provider.js | 8 + src/features/analytics/config.ts | 2 +- src/ui/app/components/asset.jsx | 159 +++++++++--- src/ui/app/components/assetsViewer.jsx | 1 + src/ui/app/components/historyViewer.jsx | 96 ++++++- src/ui/app/components/transaction.jsx | 82 +++++- src/ui/app/pages/settings.jsx | 16 +- src/ui/app/pages/wallet.jsx | 81 +++++- src/ui/app/tabs/createWallet.jsx | 3 +- src/ui/store.jsx | 17 +- 14 files changed, 758 insertions(+), 85 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..50ec69c6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["bitcoinmainnet", "txslight"] +} diff --git a/src/api/extension/index.js b/src/api/extension/index.js index 6e151167..abc1d180 100644 --- a/src/api/extension/index.js +++ b/src/api/extension/index.js @@ -163,9 +163,54 @@ export const getPoolMetadata = async (poolId) => { }; }; +export const getBtcBalance = async (addr, node = NODE.bitcoinmainnet) => { + const result = await blockfrostRequest( + `/address/${addr}`, + undefined, + undefined, + undefined, + node + ); + if (result.error) { + if (result.status_code === 400) throw APIError.InvalidRequest; + else if (result.status_code === 500) throw APIError.InternalError; + else return '0'; + } + + console.log('result.balance', result.balance); + return result.balance; +}; + export const getBalance = async () => { await Loader.load(); const currentAccount = await getCurrentAccount(); + + const network = await getNetwork(); + + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet || + network.id === NETWORK_ID.dogecoinmainnet || + network.id === NETWORK_ID.litecoinmainnet + ) { + const addr = await selectBitcoinAccountAddress(); + + const result = await blockfrostRequest(`/address/${addr}`); + if (result.error) { + if (result.status_code === 400) throw APIError.InvalidRequest; + else if (result.status_code === 500) throw APIError.InternalError; + else return Loader.Cardano.Value.new(Loader.Cardano.BigNum.from_str('0')); + } + + const value = await assetsToValue([ + { + unit: 'lovelace', + quantity: result.balance, + }, + ]); + return value; + } + const result = await blockfrostRequest( `/addresses/${currentAccount.paymentKeyHashBech32}` ); @@ -180,9 +225,36 @@ export const getBalance = async () => { export const getBalanceExtended = async () => { const currentAccount = await getCurrentAccount(); + const network = await getNetwork(); + + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet || + network.id === NETWORK_ID.dogecoinmainnet || + network.id === NETWORK_ID.litecoinmainnet + ) { + const addr = await selectBitcoinAccountAddress(); + + const result = await blockfrostRequest(`/address/${addr}`); + if (result.error) { + if (result.status_code === 400) throw APIError.InvalidRequest; + else if (result.status_code === 500) throw APIError.InternalError; + else return []; + } + return [ + { + unit: 'lovelace', + quantity: result.balance, + decimals: 8, + has_nft_onchain_metadata: false, + }, + ]; + } + const result = await blockfrostRequest( `/addresses/${currentAccount.paymentKeyHashBech32}/extended` ); + if (result.error) { if (result.status_code === 400) throw APIError.InvalidRequest; else if (result.status_code === 500) throw APIError.InternalError; @@ -202,8 +274,52 @@ export const getFullBalance = async () => { ).toString(); }; +export const selectBitcoinAccountAddress = async () => { + const currentAccount = await getCurrentAccount(); + const network = await getNetwork(); + + if (network.id === NETWORK_ID.bitcoinmainnet) { + return currentAccount.bitcoinmainnet.paymentAddr; + } else if (network.id === NETWORK_ID.bitcointestnet) { + return currentAccount.bitcointestnet.paymentAddr; + } else if (network.id === NETWORK_ID.dogecoinmainnet) { + return currentAccount.dogecoinmainnet.paymentAddr; + } else if (network.id === NETWORK_ID.litecoinmainnet) { + return currentAccount.litecoinmainnet.paymentAddr; + } +}; + export const getTransactions = async (paginate = 1, count = 10) => { const currentAccount = await getCurrentAccount(); + const network = await getNetwork(); + + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet + ) { + const addr = await selectBitcoinAccountAddress(); + + const result = await blockfrostRequest( + `/address/${addr}?details=txslight&page=${paginate}` + ); + + return [ + { + txHash: + '9b151508940dbdb104d6811d79ca1407e8eb3004c651b63b2b9f3250a79aaa10', + txIndex: 0, + blockHeight: 3005927, + }, + ]; + + if (!result || result.error) return []; + return result.transactions.map((tx) => ({ + txHash: tx.txid, + txIndex: 0, + blockHeight: tx.blockHeight, + })); + } + const result = await blockfrostRequest( `/addresses/${currentAccount.paymentKeyHashBech32}/transactions?page=${paginate}&order=desc&count=${count}` ); @@ -216,24 +332,143 @@ export const getTransactions = async (paginate = 1, count = 10) => { }; export const getTxInfo = async (txHash) => { + const network = await getNetwork(); + + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet + ) { + return { + hash: '9b151508940dbdb104d6811d79ca1407e8eb3004c651b63b2b9f3250a79aaa10', + block: '0000000068fedf9bf1eb29f28e38ccbe7e0b0107b72443489ec32e3b3fad63c1', + block_height: 123456, + block_time: 1635505891, + slot: 42000000, + index: 1, + output_amount: [ + { + unit: 'lovelace', + quantity: '42000000', + }, + { + unit: 'b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e', + quantity: '12', + }, + ], + fees: '182485', + deposit: '0', + size: 433, + invalid_before: null, + invalid_hereafter: '13885913', + utxo_count: 4, + withdrawal_count: 0, + mir_cert_count: 0, + delegation_count: 0, + stake_cert_count: 0, + pool_update_count: 0, + pool_retire_count: 0, + asset_mint_or_burn_count: 0, + redeemer_count: 0, + valid_contract: true, + }; + + const result = await blockfrostRequest(`/tx-specific/${txHash}`); + if (!result || result.error) return null; + return result; + } + const result = await blockfrostRequest(`/txs/${txHash}`); if (!result || result.error) return null; return result; }; export const getBlock = async (blockHashOrNumb) => { + const network = await getNetwork(); + + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet + ) { + const result = await blockfrostRequest(`/block/${blockHashOrNumb}`); + if (!result || result.error) return null; + return result; + } + const result = await blockfrostRequest(`/blocks/${blockHashOrNumb}`); if (!result || result.error) return null; return result; }; export const getTxUTxOs = async (txHash) => { + const network = await getNetwork(); + + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet + ) { + const result = await blockfrostRequest(`/tx-specific/${txHash}`); + // if (!result || result.error) return []; + return [ + { + address: 'n3EmnmHcFAsWutzthg54v26mEaxGsDM6WX', + tx_hash: + '9b151508940dbdb104d6811d79ca1407e8eb3004c651b63b2b9f3250a79aaa10', + output_index: 0, + amount: [ + { + unit: 'lovelace', + quantity: '12345678', + }, + ], + block: + '7eb8e27d18686c7db9a18f8bbcfe34e3fed6e047afaa2d969904d15e934847e6', + data_hash: null, + inline_datum: null, + reference_script_hash: null, + inputs: [ + { + address: 'n3EmnmHcFAsWutzthg54v26mEaxGsDM6WX', + amount: [ + { + unit: 'lovelace', + quantity: '14656250', + }, + ], + tx_hash: + 'e852cdcff2c866a49e44a0edf310563c0844c286917cd9407b7fae6332ccb11f', + }, + ], + outputs: [ + { + address: 'n3EmnmHcFAsWutzthg54v26mEaxGsDM6WX', + amount: [ + { + unit: 'lovelace', + quantity: '14570750', + }, + ], + output_index: 0, + }, + ], + }, + ]; + } + const result = await blockfrostRequest(`/txs/${txHash}/utxos`); if (!result || result.error) return null; return result; }; export const getTxMetadata = async (txHash) => { + const network = await getNetwork(); + + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet + ) { + return null; + } + const result = await blockfrostRequest(`/txs/${txHash}/metadata`); if (!result || result.error) return null; return result; @@ -278,6 +513,15 @@ export const setTxDetail = async (txObject) => { }; export const getSpecificUtxo = async (txHash, txId) => { + const network = await getNetwork(); + + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet + ) { + return null; + } + const result = await blockfrostRequest(`/txs/${txHash}/utxos`); if (!result || result.error) return null; return result.outputs[txId]; @@ -293,6 +537,16 @@ export const getSpecificUtxo = async (txHash, txId) => { */ export const getUtxos = async (amount = undefined, paginate = undefined) => { const currentAccount = await getCurrentAccount(); + + const network = await getNetwork(); + + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet + ) { + return []; + } + let result = []; let page = paginate && paginate.page ? paginate.page + 1 : 1; const limit = paginate && paginate.limit ? `&count=${paginate.limit}` : ''; @@ -350,6 +604,13 @@ export const getUtxos = async (amount = undefined, paginate = undefined) => { }; const checkCollateral = async (currentAccount, network, checkTx) => { + if ( + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet + ) { + return; + } + if (checkTx) { const transactions = await getTransactions(); if ( @@ -474,6 +735,12 @@ export const setNetwork = async (network) => { } else if (network.id === NETWORK_ID.preview) { id = NETWORK_ID.preview; node = NODE.preview; + } else if (network.id === NETWORK_ID.bitcoinmainnet) { + id = NETWORK_ID.bitcoinmainnet; + node = NODE.bitcoinmainnet; + } else if (network.id === NETWORK_ID.bitcoitestnet) { + id = NETWORK_ID.bitcoitestnet; + node = NODE.bitcoitestnet; } else { id = NETWORK_ID.preprod; node = NODE.preprod; @@ -493,6 +760,18 @@ export const setNetwork = async (network) => { }; const accountToNetworkSpecific = (account, network) => { + // if (!account[network.id]) { + // account[network.id] = { + // lovelace: null, + // minAda: 0, + // assets: [], + // history: { confirmed: [], details: {} }, + // paymentAddr: 'bc1q36fsqr0pl9sky30ex9y3tta3sg4ag9wd3f0gdq', + // paymentAddr: 'bc1q36fsqr0pl9sky30ex9y3tta3sg4ag9wd3f0gdq', + // rewardAddr: null, + // }; + // } + const assets = account[network.id].assets; const lovelace = account[network.id].lovelace; const history = account[network.id].history; @@ -1338,6 +1617,13 @@ export const createAccount = async (name, password, accountIndex = null) => { history: { confirmed: [], details: {} }, }; + const btcBalance = await getBtcBalance( + 'bc1p7l2cywf6qr9gwca3vsv6mwdlkfl3f7agw9kdm98re7jmn087q86suc2lpk', + NODE.bitcoinmainnet + ); + + // console.log('btcBalance', btcBalance); + const newAccount = { [index]: { index, @@ -1366,6 +1652,22 @@ export const createAccount = async (name, password, accountIndex = null) => { paymentAddr: paymentAddrTestnet, rewardAddr: rewardAddrTestnet, }, + [NETWORK_ID.bitcoinmainnet]: { + ...networkDefault, + paymentAddr: + 'bc1p7l2cywf6qr9gwca3vsv6mwdlkfl3f7agw9kdm98re7jmn087q86suc2lpk', + rewardAddr: rewardAddrTestnet, + lovelace: btcBalance, + }, + [NETWORK_ID.dogecoinmainnet]: { + ...networkDefault, + paymentAddr: 'DGZVagHvLrGv3bCjk6Yb1bLriFLKuhfEJg', + rewardAddr: rewardAddrTestnet, + lovelace: await getBtcBalance( + 'DGZVagHvLrGv3bCjk6Yb1bLriFLKuhfEJg', + NODE.dogecoinmainnet + ), + }, avatar: Math.random().toString(), }, }; @@ -1558,12 +1860,12 @@ export const getAdaHandle = async (assetName) => { const network = await getNetwork(); if (!network) return null; let handleUrl; - switch (network.id){ + switch (network.id) { case 'mainnet': - handleUrl = 'https://api.handle.me' + handleUrl = 'https://api.handle.me'; break; case 'preprod': - handleUrl = 'https://preprod.api.handle.me' + handleUrl = 'https://preprod.api.handle.me'; break; default: return null; @@ -1769,7 +2071,9 @@ export const getAsset = async (unit) => { const metadata = metadataDatum && Data.toJson(metadataDatum.fields[0]); asset.displayName = metadata.name; - asset.image = metadata.image ? linkToSrc(convertMetadataPropToString(metadata.image)) : ''; + asset.image = metadata.image + ? linkToSrc(convertMetadataPropToString(metadata.image)) + : ''; asset.decimals = 0; } catch (_e) { asset.displayName = asset.name; @@ -1796,7 +2100,8 @@ export const getAsset = async (unit) => { const metadata = metadataDatum && Data.toJson(metadataDatum.fields[0]); asset.displayName = metadata.name; - asset.image = linkToSrc(convertMetadataPropToString(metadata.logo)) || ''; + asset.image = + linkToSrc(convertMetadataPropToString(metadata.logo)) || ''; asset.decimals = metadata.decimals || 0; } catch (_e) { asset.displayName = asset.name; @@ -1961,6 +2266,22 @@ export const updateAccount = async (forceUpdate = false) => { await updateBalance(currentAccount, network); + console.log('before update balance', currentAccount); + console.log( + 'updating btc balance', + currentAccount['bitcoinmainnet'].paymentAddr + ); + currentAccount['bitcoinmainnet'].lovelace = await getBtcBalance( + currentAccount['bitcoinmainnet'].paymentAddr, + NODE.bitcoinmainnet + ); + currentAccount['dogecoinmainnet'].lovelace = await getBtcBalance( + currentAccount['dogecoinmainnet'].paymentAddr, + NODE.dogecoinmainnet + ); + + console.log('after update balance', currentAccount); + currentAccount[network.id].lastUpdate = currentAccount[network.id].history.confirmed[0]; diff --git a/src/api/util.js b/src/api/util.js index f7e29e9d..f43abfcd 100644 --- a/src/api/util.js +++ b/src/api/util.js @@ -43,7 +43,7 @@ export async function delay(delayInMs) { }); } -export async function blockfrostRequest(endpoint, headers, body, signal) { +export async function blockfrostRequest(endpoint, headers, body, signal, node) { const network = await getNetwork(); let result; @@ -51,17 +51,21 @@ export async function blockfrostRequest(endpoint, headers, body, signal) { if (result) { await delay(100); } - const rawResult = await fetch(provider.api.base(network.node) + endpoint, { - headers: { - ...provider.api.key(network.name || network.id), - ...provider.api.header, - ...headers, - 'Cache-Control': 'no-cache', - }, - method: body ? 'POST' : 'GET', - body, - signal, - }); + console.log('url', provider.api.base(node ?? network.node) + endpoint); + const rawResult = await fetch( + provider.api.base(node ?? network.node) + endpoint, + { + headers: { + ...provider.api.key(network.name || network.id), + ...provider.api.header, + ...headers, + 'Cache-Control': 'no-cache', + }, + method: body ? 'POST' : 'GET', + body, + signal, + } + ); result = await rawResult.json(); } @@ -91,6 +95,8 @@ export const networkNameToId = (name) => { [NETWORK_ID.testnet]: 0, [NETWORK_ID.preview]: 0, [NETWORK_ID.preprod]: 0, + [NETWORK_ID.bitcoinmainnet]: 2, + [NETWORK_ID.bitcointestnet]: 3, }; return names[name]; }; diff --git a/src/config/config.js b/src/config/config.js index 58b92666..2bf362e9 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -71,6 +71,12 @@ export const NODE = { testnet: 'https://cardano-testnet.blockfrost.io/api/v0', preview: 'https://cardano-preview.blockfrost.io/api/v0', preprod: 'https://cardano-preprod.blockfrost.io/api/v0', + bitcoinmainnet: 'https://btc1.trezor.io/api/v2', + dogecoinmainnet: 'https://doge1.trezor.io/api/v2', + litecoinmainnet: 'https://ltc1.trezor.io/api/v2', + bitcointestnet: 'https://tbtc1.trezor.io/api/v2', + // bitcoinmainnet: + // 'https://bitcoin1.mainnet.core.blockfrost.io/blockbook/api/v2', }; export const NETWORK_ID = { @@ -78,6 +84,10 @@ export const NETWORK_ID = { testnet: 'testnet', preview: 'preview', preprod: 'preprod', + bitcoinmainnet: 'bitcoinmainnet', + dogecoinmainnet: 'dogecoinmainnet', + litecoinmainnet: 'litecoinmainnet', + bitcointestnet: 'bitcointestnet', }; export const NETWORKD_ID_NUMBER = { @@ -85,6 +95,10 @@ export const NETWORKD_ID_NUMBER = { testnet: 0, preview: 0, preprod: 0, + bitcoinmainnet: 2, + bitcointestnet: 3, + litecoinmainnet: 4, + dogecoinmainnet: 5, }; export const POPUP = { diff --git a/src/config/provider.js b/src/config/provider.js index 67b9860c..6de8b2c1 100644 --- a/src/config/provider.js +++ b/src/config/provider.js @@ -7,6 +7,8 @@ const networkToProjectId = { testnet: secrets.PROJECT_ID_TESTNET, preprod: secrets.PROJECT_ID_PREPROD, preview: secrets.PROJECT_ID_PREVIEW, + btcmainnet: secrets.PROJECT_ID_BTCMAINNET, + btctestnet: secrets.PROJECT_ID_BTCTESTNET, }; export default { @@ -17,6 +19,12 @@ export default { key: (network = 'mainnet') => ({ project_id: networkToProjectId[network], }), + priceBTC: (currency = 'usd') => + fetch( + `https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${currency}` + ) + .then((res) => res.json()) + .then((res) => res.bitcoin[currency]), price: (currency = 'usd') => fetch( `https://api.coingecko.com/api/v3/simple/price?ids=cardano&vs_currencies=${currency}` diff --git a/src/features/analytics/config.ts b/src/features/analytics/config.ts index 16161880..689bbcd8 100644 --- a/src/features/analytics/config.ts +++ b/src/features/analytics/config.ts @@ -1,6 +1,6 @@ export const PUBLIC_POSTHOG_HOST = 'https://eu.posthog.com'; -export const PRODUCTION_TRACKING_MODE_ENABLED = 'true'; +export const PRODUCTION_TRACKING_MODE_ENABLED = 'false'; export const POSTHOG_API_KEY = 'phc_5dRKCjaa549fL0kqSKAGz4tRvjHzQuaxVSrdmigUpBe'; diff --git a/src/ui/app/components/asset.jsx b/src/ui/app/components/asset.jsx index 89c9169a..dff54376 100644 --- a/src/ui/app/components/asset.jsx +++ b/src/ui/app/components/asset.jsx @@ -13,7 +13,7 @@ import Copy from './copy'; import UnitDisplay from './unitDisplay'; import { useNavigate } from 'react-router-dom'; import { BsArrowUpRight } from 'react-icons/bs'; -import { getAsset } from '../../../api/extension'; +import { getAsset, getNetwork } from '../../../api/extension'; const useIsMounted = () => { const isMounted = React.useRef(false); @@ -24,6 +24,19 @@ const useIsMounted = () => { return isMounted; }; +export const getDisplayName = (assetUnit: string) => { + if (assetUnit === 'bitcoin') { + return 'BTC'; + }; + if (assetUnit === 'doge') { + return 'DOGE'; + }; + if (assetUnit === 'litecoin') { + return 'LTC'; + }; + return 'NA'; +} + const Asset = ({ asset, enableSend, ...props }) => { const isMounted = useIsMounted(); const [token, setToken] = React.useState(null); @@ -36,29 +49,43 @@ const Asset = ({ asset, enableSend, ...props }) => { const navigate = useNavigate(); const settings = useStoreState((state) => state.settings.settings); + const fetchMetadata = async () => { + const isBitcoin = asset.unit === 'bitcoin' || asset.unit === 'doge'; let detailedConstructedAsset; - if (asset.unit !== 'lovelace') { - detailedConstructedAsset = await getAsset(asset.unit); - } + if (isBitcoin) { + if (!isMounted.current) return; + + const detailedAsset = { + ...asset, + displayName: getDisplayName(asset.unit), + decimals: 8, + }; - const detailedAsset = - asset.unit === 'lovelace' - ? { - ...asset, - displayName: 'Ada', - decimals: 6, - } - : { - ...detailedConstructedAsset, - quantity: asset.quantity, - input: asset.input, - fingerprint: - asset.fingerprint ?? detailedConstructedAsset.fingerprint, - }; - if (!isMounted.current) return; - setToken(detailedAsset); + setToken(detailedAsset); + } else { + if (asset.unit !== 'lovelace') { + detailedConstructedAsset = await getAsset(asset.unit); + } + + const detailedAsset = + asset.unit === 'lovelace' + ? { + ...asset, + displayName: 'Ada', + decimals: 6, + } + : { + ...detailedConstructedAsset, + quantity: asset.quantity, + input: asset.input, + fingerprint: + asset.fingerprint ?? detailedConstructedAsset.fingerprint, + }; + if (!isMounted.current) return; + setToken(detailedAsset); + } }; React.useEffect(() => { @@ -91,27 +118,77 @@ const Asset = ({ asset, enableSend, ...props }) => { width="full" src={token.image} fallback={ - !token.image ? ( - token.unit === 'lovelace' ? ( - - {settings.adaSymbol} - - ) : ( - - ) - ) : ( - - ) + <> + {token.image && ( + + )} + + {token.unit === 'lovelace' && ( + + {settings.adaSymbol} + + )} + + {token.unit === 'bitcoin' && ( + + ₿ + + )} + + {token.unit === 'doge' && ( + + Ð + + )} + + {token.unit === 'litecoin' && ( + + Ł + + )} + + {token.unit !== 'lovelace' && token.unit !== 'doge' && token.unit !== 'bitcoin' && token.unit !== 'litecoin' && } + } /> diff --git a/src/ui/app/components/assetsViewer.jsx b/src/ui/app/components/assetsViewer.jsx index 1f21de45..5b38b1d8 100644 --- a/src/ui/app/components/assetsViewer.jsx +++ b/src/ui/app/components/assetsViewer.jsx @@ -22,6 +22,7 @@ const AssetsViewer = ({ assets }) => { const [assetsArray, setAssetsArray] = React.useState(null); const [search, setSearch] = React.useState(''); const [total, setTotal] = React.useState(0); + const createArray = async () => { if (!assets) { setAssetsArray(null); diff --git a/src/ui/app/components/historyViewer.jsx b/src/ui/app/components/historyViewer.jsx index 17a95d91..ba08a513 100644 --- a/src/ui/app/components/historyViewer.jsx +++ b/src/ui/app/components/historyViewer.jsx @@ -23,6 +23,61 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { const [page, setPage] = React.useState(1); const [final, setFinal] = React.useState(false); const [loadNext, setLoadNext] = React.useState(false); + + const getBitcoinTx = (txHash, chain, amount, blockTime = 1727431131) => { + return { + block: { + confirmations: 0, + hash: txHash, + height: 2522850, + time: blockTime, + }, + info: { + chain: chain, + block: + '016ea9f25fbd19e1fc777eb1211d00d756cf51d40f3b0aa3dbae9309f1bfad51', + block_height: 2522850, + block_time: blockTime, + deposit: '0', + fees: '200000', + hash: txHash, + output_amount: [ + { + quantity: '2000000000', + unit: 'lovelace', + }, + ], + }, + metadata: [], + utxos: { + hash: txHash, + inputs: [ + { + address: + 'addr_test1vqeux7xwusdju9dvsj8h7mca9aup2k439kfmwy773xxc2hcu7zy99', + amount: [ + { + quantity: '13000200000', + unit: 'lovelace', + }, + ], + }, + ], + outputs: [ + { + address: currentAddr, + amount: [ + { + quantity: amount, + unit: 'lovelace', + }, + ], + }, + ], + }, + }; + }; + const getTxs = async () => { if (!history) { slice = []; @@ -37,7 +92,22 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { ); if (slice.length < page * BATCH) { - const txs = await getTransactions(page, BATCH); + let txs = await getTransactions(page, BATCH); + + txs = txs.concat([ + { + txHash: + '9b151508940dbdb104d6811d79ca1407e8eb3004c651b63b2b9f3250a79aaa10', + txIndex: 0, + blockHeight: 2522850, + }, + { + txHash: + '52014d1f6f16f60306c25526ff79d375ec2cdfcfb406a876065229f0014d68b0', + txIndex: 0, + blockHeight: 2522850, + }, + ]); if (txs.length <= 0) { setFinal(true); @@ -102,8 +172,31 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { }} > {historySlice.map((txHash, index) => { + // console.log('history.details[txHash]', history.details[txHash]); if (!history.details[txHash]) history.details[txHash] = {}; + console.log('history.details[txHash] ', history.details[txHash]); + switch (txHash) { + case '9b151508940dbdb104d6811d79ca1407e8eb3004c651b63b2b9f3250a79aaa10': + history.details[txHash] = getBitcoinTx( + txHash, + 'bitcoin', + 14300000, + 1727231131 + ); + break; + case '52014d1f6f16f60306c25526ff79d375ec2cdfcfb406a876065229f0014d68b0': + history.details[txHash] = getBitcoinTx( + txHash, + 'dogecoin', + 500000000, + 1726431131 + ); + break; + default: + break; + } + return ( { @@ -115,6 +208,7 @@ const HistoryViewer = ({ history, network, currentAddr, addresses }) => { currentAddr={currentAddr} addresses={addresses} network={network} + chain={history.details[txHash]?.info?.chain} /> ); })} diff --git a/src/ui/app/components/transaction.jsx b/src/ui/app/components/transaction.jsx index a23f8526..021e0e86 100644 --- a/src/ui/app/components/transaction.jsx +++ b/src/ui/app/components/transaction.jsx @@ -90,6 +90,7 @@ const Transaction = ({ currentAddr, addresses, network, + chain, onLoad, }) => { const settings = useStoreState((state) => state.settings.settings); @@ -98,13 +99,61 @@ const Transaction = ({ genDisplayInfo(txHash, detail, currentAddr, addresses) ); - const colorMode = { - iconBg: useColorModeValue('white', 'gray.800'), - txBg: useColorModeValue('teal.50', 'gray.700'), - txBgHover: useColorModeValue('teal.100', 'gray.600'), - assetsBtnHover: useColorModeValue('teal.200', 'gray.700'), + const getColorMode = (chain) => { + if (chain === 'bitcoin') { + return { + iconBg: useColorModeValue('white', 'gray.800'), + txBg: useColorModeValue('orange.100', 'gray.700'), + txBgHover: useColorModeValue('orange.200', 'gray.600'), + assetsBtnHover: useColorModeValue('orange.200', 'gray.700'), + }; + } + + if (chain === 'litecoin') { + return { + iconBg: useColorModeValue('white', 'gray.800'), + txBg: useColorModeValue('blue.50', 'gray.700'), + txBgHover: useColorModeValue('blue.100', 'gray.600'), + assetsBtnHover: useColorModeValue('blue.200', 'gray.700'), + }; + } + + if (chain === 'dogecoin') { + return { + iconBg: useColorModeValue('white', 'gray.800'), + txBg: useColorModeValue('yellow.100', 'gray.700'), + txBgHover: useColorModeValue('yellow.200', 'gray.600'), + assetsBtnHover: useColorModeValue('yellow.200', 'gray.700'), + }; + } + + return { + iconBg: useColorModeValue('white', 'gray.800'), + txBg: useColorModeValue('teal.50', 'gray.700'), + txBgHover: useColorModeValue('teal.100', 'gray.600'), + assetsBtnHover: useColorModeValue('teal.200', 'gray.700'), + }; }; + const getSymbol = (chain) => { + switch (chain) { + case 'bitcoin': + return '₿'; + case 'litecoin': + return 'Ł'; + + case 'dogecoin': + return 'Ð'; + + case 'litecoin': + return 'Ł'; + default: + return settings.adaSymbol; + } + }; + + const colorMode = getColorMode(chain); + const getTxDetail = async () => { if (!displayInfo) { let txDetail = await updateTxInfo(txHash); @@ -172,8 +221,8 @@ const Transaction = ({ : txTypeColor.externalOut } quantity={displayInfo.lovelace} - decimals={6} - symbol={settings.adaSymbol} + decimals={chain ? 8 : 6} + symbol={getSymbol(chain)} /> ) : displayInfo.extra.length ? ( @@ -237,7 +286,11 @@ const Transaction = ({ )} {displayInfo && ( - + )} @@ -306,7 +359,7 @@ const TxIcon = ({ txType, extra }) => { ); }; -const TxDetail = ({ displayInfo, network }) => { +const TxDetail = ({ displayInfo, network, chain }) => { const capture = useCaptureEvent(); const colorMode = { extraDetail: useColorModeValue('black', 'white'), @@ -330,6 +383,17 @@ const TxDetail = ({ displayInfo, network }) => { color="teal" href={ (() => { + switch (chain) { + case 'bitcoin': + return 'https://blockchair.com/bitcoin/transaction/'; + case 'litecoin': + return 'https://blockchair.com/litecoin/transaction/'; + case 'dogecoin': + return 'https://blockchair.com/dogecoin/transaction/'; + default: + break; + } + switch (network.id) { case NETWORK_ID.mainnet: return 'https://cardanoscan.io/transaction/'; diff --git a/src/ui/app/pages/settings.jsx b/src/ui/app/pages/settings.jsx index 6887e309..4ea2b365 100644 --- a/src/ui/app/pages/settings.jsx +++ b/src/ui/app/pages/settings.jsx @@ -47,6 +47,7 @@ import { ChangePasswordModal } from '../components/changePasswordModal'; import { useCaptureEvent } from '../../../features/analytics/hooks'; import { Events } from '../../../features/analytics/events'; import { LegalSettings } from '../../../features/settings/legal/LegalSettings'; +import { getNetworkSymbol } from '../../store'; const Settings = () => { const navigate = useNavigate(); @@ -484,19 +485,24 @@ const Network = () => { const id = e.target.value; - setSettings({ + const s = { ...settings, network: { ...settings.network, id: NETWORK_ID[id], node: NODE[id], + adaSymbol: getNetworkSymbol(settings.network.id), }, - }); + }; + + setSettings(s); }} > - - - + + + + + diff --git a/src/ui/app/pages/wallet.jsx b/src/ui/app/pages/wallet.jsx index 5732d4df..6086bf6a 100644 --- a/src/ui/app/pages/wallet.jsx +++ b/src/ui/app/pages/wallet.jsx @@ -197,14 +197,18 @@ const Wallet = () => { currentAccount.nft.push(asset); else currentAccount.ft.push(asset); }); + const network = await getNetwork(); let price = fiatPrice.current; try { if (!fiatPrice.current) { - price = await provider.api.price(settings.currency); + price = + network.id === NETWORK_ID.bitcoinmainnet || + network.id === NETWORK_ID.bitcointestnet + ? await provider.api.priceBTC(settings.currency) + : await provider.api.price(settings.currency); fiatPrice.current = price; } } catch (e) {} - const network = await getNetwork(); const delegation = await getDelegation(); if (!isMounted.current) return; setState((s) => ({ @@ -231,6 +235,23 @@ const Wallet = () => { }; }, []); + const isBitcoin = state.network.id.startsWith('bitcoin'); + + console.log('TU state.account', state.account); + const btcAssets = [ + { + unit: 'bitcoin', + quantity: state.account?.bitcoinmainnet.lovelace ?? '0', + }, + { unit: 'doge', quantity: state.account?.dogecoinmainnet.lovelace ?? '0' }, + ]; + + console.log('btcAssets', btcAssets); + console.log( + 'state.account.bitcoinmainnet.lovelace', + state.account?.bitcoinmainnet.lovelace + ); + return ( <> { > {Object.keys(info.accounts).map((accountIndex) => { const accountInfo = info.accounts[accountIndex]; + const account = state.accounts && state.accounts[accountIndex]; return ( @@ -404,7 +426,7 @@ const Wallet = () => { ) ).toString() } - decimals={6} + decimals={isBitcoin ? 8 : 6} symbol={settings.adaSymbol} /> ) : ( @@ -540,7 +562,7 @@ const Wallet = () => { ) ).toString() } - decimals={6} + decimals={isBitcoin ? 8 : 6} symbol={settings.adaSymbol} /> {state.account && @@ -593,13 +615,53 @@ const Wallet = () => { '' )} + {/* Bitcoin */} + + + {/* Doge */} + + + + { ? state.account.collateral.lovelace : 0 ) - ).toString() + ).toString(), + isBitcoin ? 8 : 6 ) * state.fiatPrice * 10 ** 2 @@ -744,7 +807,11 @@ const Wallet = () => { - + {