From c88ac553939012d227f6949a5dd15361888e8f9e Mon Sep 17 00:00:00 2001 From: agoltzman <106151463+agoltzman@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:06:57 +0300 Subject: [PATCH 01/30] feat: :green_heart: added gitlab ci --- .gitlab-ci.yml | 168 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..7e9ebbba --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,168 @@ +stages: + - detect + - version + - build + - sync + +variables: + EXCLUDE_TAG: + description: "Git tag to exclude from syncing commits" + value: "" # Required manual input when triggering the pipeline + + GIT_DEPTH: 2 + NODE_ENV: development + CHECKPOINT: + description: "Checkpoint from which to start mirroring to github from main branch" + value: "" + EXECUTABLES_DIR: "apps/recovery-utility/dist" + GITHUB_REPO: "https://oauth2:${GITHUB_TOKEN}@github.com/youruser/yourrepo.git" + +default: + image: node:lts-gallium + before_script: + - apt-get update && apt-get install -y curl jq git + - corepack enable + - | + if [ -z "$EXCLUDE_TAG" ]; then + echo "❌ ERROR: EXCLUDE_TAG must be provided when triggering the pipeline."; + exit 1; + fi + if [ -z "$CHECKPOINT" ]; then + echo "❌ ERROR: CHECKPOINT must be provided when triggering the pipeline."; + exit 1; + fi + +fetch_label_state: + stage: detect + rules: + - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "hidden"' + when: always + - when: never + script: | + HEAD_SHA=$(git rev-parse HEAD) + echo "Waiting for MR with commit $HEAD_SHA to be merged..." + + timeout=300 + interval=10 + elapsed=0 + + while true; do + MR_JSON=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/repository/commits/$HEAD_SHA/merge_requests") + MR_STATE=$(echo "$MR_JSON" | jq -r '.[0].state') + MR_LABELS=$(echo "$MR_JSON" | jq -r '.[0].labels | join(",")') + echo "MR State: $MR_STATE" + + if [ "$MR_STATE" = "merged" ]; then + echo "$MR_LABELS" | grep -q "new-version" && echo "TRIGGER=true" >> flags.env || echo "TRIGGER=false" >> flags.env + break + fi + + if [ $elapsed -ge $timeout ]; then + echo "Timeout waiting for MR to be merged." + exit 1 + fi + + echo "Still waiting... ($elapsed seconds)" + sleep $interval + elapsed=$((elapsed + interval)) + done + artifacts: + reports: + dotenv: flags.env + +version_packages: + stage: version + needs: [fetch_label_state] + rules: + - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "hidden"' + when: always + - when: never + script: + - if [ "$TRIGGER" = "true" ]; then + yarn install; + npx changeset status; + npx changeset version; + npx changeset publish; + else + echo "Skipping changeset — TRIGGER=$TRIGGER"; + fi + +build_linux: + stage: build + needs: [version_packages] + rules: + - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "hidden"' + when: always + - when: never + script: + - if [ "$TRIGGER" = "true" ]; then + yarn install --frozen-lockfile --network-timeout 100000; + export NODE_OPTIONS="--max_old_space_size=4096"; + yarn forcebuild; + else + echo "Skipping build — TRIGGER=$TRIGGER"; + fi + artifacts: + name: "Recovery-Build-Linux" + paths: + - apps/recovery-utility/dist/*.AppImage + - apps/recovery-utility/dist/*.exe + - apps/recovery-utility/dist/*.zip + - apps/recovery-relay/out/ + expire_in: 7 days + +sync_to_github: + stage: sync + image: alpine:latest + needs: [build_linux] + rules: + - if: '$CI_COMMIT_BRANCH == "main"' + when: always + - when: never + before_script: + - apk add --no-cache git bash + - | + if [ -z "$EXCLUDE_TAG" ]; then + echo "❌ ERROR: EXCLUDE_TAG must be provided."; + exit 1; + fi + script: + - if [ "$TRIGGER" != "true" ]; then + echo "Skipping GitHub sync — TRIGGER=$TRIGGER"; + exit 0; + fi + + - git config --global user.email "ci@gitlab.com" + - git config --global user.name "GitLab CI" + - git fetch --unshallow || true + + - COMMITS=$(git log ${CHECKPOINT}..HEAD --oneline --no-merges | awk '{print $1}') + + - echo "Filtering commits..." + - for COMMIT in $COMMITS; do + TAGS=$(git tag --contains $COMMIT); + if [[ "$TAGS" != *"${EXCLUDE_TAG}"* ]]; then + echo $COMMIT >> selected_commits.txt; + fi; + done + + - echo "Selected commits:" + - cat selected_commits.txt || echo "No commits to sync" + + - git clone "$GITHUB_REPO" github-mirror + - cd github-mirror + + - | + while read COMMIT; do + git cherry-pick $COMMIT || echo "Cherry-pick failed or already applied: $COMMIT"; + done < ../selected_commits.txt + + - cd .. + - mkdir -p github-mirror/${EXECUTABLES_DIR} + - cp -r ${EXECUTABLES_DIR}/* github-mirror/${EXECUTABLES_DIR}/ || echo "No executables found." + + - cd github-mirror + - git add . + - | + git commit -m "Sync from GitLab CI: commits + executables" || echo "Nothing to commit" + - git push origin main From d68e6562e19ea949abbf5ee339554f9dd3a8af15 Mon Sep 17 00:00:00 2001 From: agoltzman <106151463+agoltzman@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:31:30 +0300 Subject: [PATCH 02/30] fix: fixed gitlab ci --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7e9ebbba..22d5c44c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,7 +15,7 @@ variables: description: "Checkpoint from which to start mirroring to github from main branch" value: "" EXECUTABLES_DIR: "apps/recovery-utility/dist" - GITHUB_REPO: "https://oauth2:${GITHUB_TOKEN}@github.com/youruser/yourrepo.git" + GITHUB_REPO: "https://oauth2:${GITHUB_TOKEN}@github.com/fireblocks/mirror-test.git" default: image: node:lts-gallium From 9b96863410a189c0fd1285b405b2e033361700d5 Mon Sep 17 00:00:00 2001 From: agoltzman <106151463+agoltzman@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:32:44 +0300 Subject: [PATCH 03/30] test: new-version label test --- test.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 test.txt diff --git a/test.txt b/test.txt new file mode 100644 index 00000000..76110cc1 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +test sync Tue Jun 3 13:32:44 IDT 2025 From e18921aef87ea08fbd9ba0e2a24c9525f6bc53fa Mon Sep 17 00:00:00 2001 From: agoltzman <106151463+agoltzman@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:32:04 +0300 Subject: [PATCH 04/30] fix: :lock: updated encryption of generated rsa key --- apps/recovery-utility/renderer/pages/setup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/recovery-utility/renderer/pages/setup.tsx b/apps/recovery-utility/renderer/pages/setup.tsx index da119590..9a956baf 100644 --- a/apps/recovery-utility/renderer/pages/setup.tsx +++ b/apps/recovery-utility/renderer/pages/setup.tsx @@ -63,7 +63,7 @@ const generateRsaKeypair = (passphrase: string) => { const { privateKey, publicKey } = keypair; const privateKeyPem = pki.encryptRsaPrivateKey(privateKey, passphrase, { - legacy: true, + legacy: false, algorithm: 'aes128', prfAlgorithm: 'sha256', }); From 4bdf9520faea4f78bd7e795d36c3bb63ed9ea58e Mon Sep 17 00:00:00 2001 From: Tomer Shoham Date: Thu, 18 Sep 2025 19:47:38 +0000 Subject: [PATCH 05/30] Merge Hidden branch into Main --- .changeset/blue-gorillas-rush.md | 5 + .changeset/dry-paws-thank.md | 5 + .changeset/eleven-cheetahs-rest.md | 5 + .changeset/giant-cups-explode.md | 6 + .changeset/old-drinks-camp.md | 5 + .changeset/orange-bobcats-taste.md | 5 + .changeset/tender-turtles-destroy.md | 6 + .changeset/violet-ducks-joke.md | 5 + .changeset/wild-timers-think.md | 6 + .../components/Layout/index.tsx | 9 +- .../components/RawSigningModal/index.tsx | 148 ++ apps/recovery-relay/context/Workspace.tsx | 9 +- apps/recovery-relay/lib/defaultRPCs.ts | 1 + .../recovery-relay/lib/wallets/ERC20/index.ts | 2 +- apps/recovery-relay/lib/wallets/EVM/FLR.ts | 103 + apps/recovery-relay/lib/wallets/SPL/index.ts | 151 ++ .../lib/wallets/SPL/solana_assets.ts | 1900 +++++++++++++++++ .../wallets/SPL/unsupported_solana_assets.ts | 113 + apps/recovery-relay/lib/wallets/index.ts | 23 +- apps/recovery-relay/package.json | 5 +- apps/recovery-relay/pages/raw-signing.tsx | 88 + apps/recovery-utility/electron-builder.json | 21 +- apps/recovery-utility/main/background.ts | 6 +- apps/recovery-utility/main/ipc/getLogs.ts | 116 +- apps/recovery-utility/package.json | 5 +- .../renderer/components/Layout/index.tsx | 10 +- .../Modals/RawSigningModal/index.tsx | 142 ++ .../WithdrawModal/SignTransaction/index.tsx | 124 +- .../components/Modals/WithdrawModal/index.tsx | 12 +- .../renderer/components/Pages/keys.tsx | 246 +++ .../components/RecoveryForm/index.tsx | 113 +- .../renderer/context/Workspace.tsx | 15 +- .../renderer/lib/recoverExtendedKeys.ts | 7 - .../renderer/lib/wallets/ERC20/index.ts | 12 +- .../renderer/lib/wallets/EVM/index.ts | 5 +- .../renderer/lib/wallets/FLR/index.ts | 8 + .../renderer/lib/wallets/SPL/index.ts | 71 + .../renderer/lib/wallets/index.ts | 18 + .../accounts/vault/[accountId]/index.tsx | 5 +- .../renderer/pages/accounts/vault/index.tsx | 55 +- apps/recovery-utility/renderer/pages/csv.tsx | 5 +- .../recovery-utility/renderer/pages/index.tsx | 9 +- apps/recovery-utility/renderer/pages/keys.tsx | 6 +- apps/recovery-utility/renderer/pages/ncw.tsx | 2 +- .../renderer/pages/raw-signing.tsx | 122 ++ .../renderer/pages/settings.tsx | 6 +- packages/asset-config/assets.ts | 41 +- packages/asset-config/config/patches.ts | 1 + packages/asset-config/data/globalAssets.ts | 32 + packages/asset-config/index.ts | 4 +- packages/e2e-tests/utils.ts | 2 +- .../extended-key-recovery/src/algorithms.ts | 27 + packages/extended-key-recovery/src/decrypt.ts | 10 +- .../extended-key-recovery/src/metadata.ts | 135 +- packages/extended-key-recovery/src/ncw.ts | 7 +- .../src/reconstructKeys.ts | 40 +- packages/extended-key-recovery/src/recover.ts | 128 +- packages/extended-key-recovery/src/types.ts | 41 +- .../extended-key-recovery/tests/index.test.ts | 80 +- .../components/DerivationPathInput/index.tsx | 146 ++ .../Modals/AddressesModal/index.tsx | 2 +- .../Modals/RecoverAccountModal/index.tsx | 86 +- .../RawSigningForm/Signature/index.tsx | 146 ++ .../components/RawSigningForm/index.tsx | 221 ++ .../shared/components/TextField/index.tsx | 3 +- packages/shared/components/index.ts | 2 + .../shared/hooks/useBaseWorkspace/index.ts | 131 +- .../useBaseWorkspace/reduceDerivations.ts | 55 +- .../shared/hooks/useBaseWorkspace/types.ts | 11 +- packages/shared/hooks/useRawSign.ts | 101 + packages/shared/hooks/useRawSignMessage.ts | 161 ++ packages/shared/index.ts | 1 - packages/shared/lib/getLogger.ts | 49 + packages/shared/lib/relayUrl.ts | 17 +- packages/shared/lib/validateAddress.ts | 2 +- .../shared/pages/accounts/vault/index.tsx | 22 +- packages/shared/pages/csv.tsx | 4 +- packages/shared/pages/keys.tsx | 218 -- packages/shared/reducers/rawSignReducer.ts | 76 + packages/shared/schemas/extendedKeys.ts | 92 +- .../shared/schemas/recoverAccountInput.ts | 10 + packages/shared/schemas/recoverKeysInput.ts | 7 + packages/shared/schemas/relayUrl.ts | 39 +- packages/wallet-derivation/types.ts | 11 +- .../wallet-derivation/wallets/BaseWallet.ts | 38 +- .../wallet-derivation/wallets/ECDSAWallet.ts | 18 +- .../wallet-derivation/wallets/EdDSAWallet.ts | 24 +- .../wallet-derivation/wallets/chains/EVM.ts | 4 +- scripts/build-cross-platform.sh | 31 + yarn.lock | 277 ++- 90 files changed, 5638 insertions(+), 656 deletions(-) create mode 100644 .changeset/blue-gorillas-rush.md create mode 100644 .changeset/dry-paws-thank.md create mode 100644 .changeset/eleven-cheetahs-rest.md create mode 100644 .changeset/giant-cups-explode.md create mode 100644 .changeset/old-drinks-camp.md create mode 100644 .changeset/orange-bobcats-taste.md create mode 100644 .changeset/tender-turtles-destroy.md create mode 100644 .changeset/violet-ducks-joke.md create mode 100644 .changeset/wild-timers-think.md create mode 100644 apps/recovery-relay/components/RawSigningModal/index.tsx create mode 100644 apps/recovery-relay/lib/wallets/EVM/FLR.ts create mode 100644 apps/recovery-relay/lib/wallets/SPL/index.ts create mode 100644 apps/recovery-relay/lib/wallets/SPL/solana_assets.ts create mode 100644 apps/recovery-relay/lib/wallets/SPL/unsupported_solana_assets.ts create mode 100644 apps/recovery-relay/pages/raw-signing.tsx create mode 100644 apps/recovery-utility/renderer/components/Modals/RawSigningModal/index.tsx create mode 100644 apps/recovery-utility/renderer/components/Pages/keys.tsx create mode 100644 apps/recovery-utility/renderer/lib/wallets/FLR/index.ts create mode 100644 apps/recovery-utility/renderer/lib/wallets/SPL/index.ts create mode 100644 apps/recovery-utility/renderer/pages/raw-signing.tsx create mode 100644 packages/extended-key-recovery/src/algorithms.ts create mode 100644 packages/shared/components/DerivationPathInput/index.tsx create mode 100644 packages/shared/components/RawSigningForm/Signature/index.tsx create mode 100644 packages/shared/components/RawSigningForm/index.tsx create mode 100644 packages/shared/hooks/useRawSign.ts create mode 100644 packages/shared/hooks/useRawSignMessage.ts delete mode 100644 packages/shared/pages/keys.tsx create mode 100644 packages/shared/reducers/rawSignReducer.ts create mode 100755 scripts/build-cross-platform.sh diff --git a/.changeset/blue-gorillas-rush.md b/.changeset/blue-gorillas-rush.md new file mode 100644 index 00000000..4775f36d --- /dev/null +++ b/.changeset/blue-gorillas-rush.md @@ -0,0 +1,5 @@ +--- +'@fireblocks/recovery-shared': patch +--- + +updated x-key types for import and vault shared pages diff --git a/.changeset/dry-paws-thank.md b/.changeset/dry-paws-thank.md new file mode 100644 index 00000000..ffedbbb9 --- /dev/null +++ b/.changeset/dry-paws-thank.md @@ -0,0 +1,5 @@ +--- +'@fireblocks/recovery-utility': minor +--- + +Display each keyset with their enabled keys diff --git a/.changeset/eleven-cheetahs-rest.md b/.changeset/eleven-cheetahs-rest.md new file mode 100644 index 00000000..bdaf5db1 --- /dev/null +++ b/.changeset/eleven-cheetahs-rest.md @@ -0,0 +1,5 @@ +--- +'@fireblocks/recovery-utility': minor +--- + +get specific keyset when x-keys are required diff --git a/.changeset/giant-cups-explode.md b/.changeset/giant-cups-explode.md new file mode 100644 index 00000000..02a7c082 --- /dev/null +++ b/.changeset/giant-cups-explode.md @@ -0,0 +1,6 @@ +--- +'@fireblocks/recovery-utility': minor +'@fireblocks/recovery-shared': minor +--- + +allow to map keyset when no keyset threshold mapping is defined in drs kit diff --git a/.changeset/old-drinks-camp.md b/.changeset/old-drinks-camp.md new file mode 100644 index 00000000..277003dd --- /dev/null +++ b/.changeset/old-drinks-camp.md @@ -0,0 +1,5 @@ +--- +'@fireblocks/recovery-shared': minor +--- + +Added type distinction for relay and utility in workspace definition and support for keyset account mapping diff --git a/.changeset/orange-bobcats-taste.md b/.changeset/orange-bobcats-taste.md new file mode 100644 index 00000000..562c69ea --- /dev/null +++ b/.changeset/orange-bobcats-taste.md @@ -0,0 +1,5 @@ +--- +'@fireblocks/extended-key-recovery': patch +--- + +Updated tests for multiple keysets diff --git a/.changeset/tender-turtles-destroy.md b/.changeset/tender-turtles-destroy.md new file mode 100644 index 00000000..c3814ca7 --- /dev/null +++ b/.changeset/tender-turtles-destroy.md @@ -0,0 +1,6 @@ +--- +'@fireblocks/recovery-utility': minor +'@fireblocks/recovery-shared': minor +--- + +Moved x-key page to utility only diff --git a/.changeset/violet-ducks-joke.md b/.changeset/violet-ducks-joke.md new file mode 100644 index 00000000..f758d303 --- /dev/null +++ b/.changeset/violet-ducks-joke.md @@ -0,0 +1,5 @@ +--- +'@fireblocks/extended-key-recovery': minor +--- + +Added recovery for multiple keysets diff --git a/.changeset/wild-timers-think.md b/.changeset/wild-timers-think.md new file mode 100644 index 00000000..e7637147 --- /dev/null +++ b/.changeset/wild-timers-think.md @@ -0,0 +1,6 @@ +--- +'@fireblocks/wallet-derivation': minor +'@fireblocks/recovery-shared': minor +--- + +Make derivation and base wallet to use correct keyset for account created or used diff --git a/apps/recovery-relay/components/Layout/index.tsx b/apps/recovery-relay/components/Layout/index.tsx index e1e0f301..d6381553 100644 --- a/apps/recovery-relay/components/Layout/index.tsx +++ b/apps/recovery-relay/components/Layout/index.tsx @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; import { Alert, AlertTitle } from '@mui/material'; import { LeakAdd, ImportExport, Settings } from '@mui/icons-material'; +import DrawIcon from '@mui/icons-material/Draw'; import semver from 'semver'; import { Layout as BaseLayout, @@ -70,6 +71,12 @@ export const Layout = ({ children }: Props) => { icon: ImportExport, disabled: !hasExtendedPublicKeys, }, + { + label: 'Raw Signing', + path: '/raw-signing', + icon: DrawIcon, + disabled: !hasExtendedPublicKeys, + }, { label: 'Settings', path: '/settings', @@ -96,7 +103,7 @@ export const Layout = ({ children }: Props) => { )} {children} - ; + ); }; diff --git a/apps/recovery-relay/components/RawSigningModal/index.tsx b/apps/recovery-relay/components/RawSigningModal/index.tsx new file mode 100644 index 00000000..3a9ebadb --- /dev/null +++ b/apps/recovery-relay/components/RawSigningModal/index.tsx @@ -0,0 +1,148 @@ +import { ReactNode, useState } from 'react'; +import QrScanner from 'qr-scanner'; +import { Typography, Box, Grid, CircularProgress, lighten } from '@mui/material'; +import { LOGGER_NAME_RELAY } from '@fireblocks/recovery-shared/constants'; +import { + BaseModal, + getLogger, + QrCodeScanner, + getRelayParams, + QrCode, + RelayRawSignTxResponseParams, +} from '@fireblocks/recovery-shared'; +import { SigningAlgorithms } from '@fireblocks/recovery-shared/reducers/rawSignReducer'; +import Signature from '@fireblocks/recovery-shared/components/RawSigningForm/Signature'; +import { CallMade, CallReceived } from '@mui/icons-material'; + +type RawSigningModalProps = { + open: boolean; + qrData: string; + onClose: VoidFunction; +}; + +const logger = getLogger(LOGGER_NAME_RELAY); + +const RawSigningModal: React.FC = (props) => { + const { open, qrData, onClose: onCloseModal } = props; + + const [isProcessing, setIsProcessing] = useState(false); + const [signature, setSignature] = useState(null); + const [signingAlgorithm, setSigningAlgorithm] = useState(null); + + const onDecode = async (data: QrScanner.ScanResult) => { + try { + setIsProcessing(true); + const decodedData = data.data; + const parsed = getRelayParams('utility', decodedData) as RelayRawSignTxResponseParams; + const parsedMessage = parsed.signature; + setSignature(parsedMessage); + const parsedAlgorithm = parsed.algorithm as SigningAlgorithms; + setSigningAlgorithm(parsedAlgorithm); + setTimeout(() => { + setIsProcessing(false); + }, 1250); + } catch (error) { + console.error('Error processing QR code:', error); + logger.error('Error processing QR code:', error); + setIsProcessing(false); + } + }; + + const onClose = () => { + onCloseModal(); + }; + + return ( + + + Raw Signing + + + ) as ReactNode + } + > + {!signature || !signingAlgorithm ? ( + <> + ({ + color: t.palette.primary.main, + border: `solid 1px ${t.palette.primary.main}`, + background: lighten(t.palette.primary.main, 0.95), + width: '100%', + borderRadius: '0.5rem', + padding: '0.5rem', + display: 'flex', + alignItems: 'center', + marginBottom: '1em', + })} + > + + + + Sending + + + Message, derivation path, algorithm + + + + + + Receiving + + + Signature + + + + + + Scan the QR code using the Recovery Utility to sign a message. Then, scan the QR code produced by the Recovery Utility + to obtain the signature of the message. + + + + + + + + + + + ) : isProcessing ? ( + + + + ) : ( + /* */ + + + + )} + + ); +}; + +export default RawSigningModal; diff --git a/apps/recovery-relay/context/Workspace.tsx b/apps/recovery-relay/context/Workspace.tsx index 9a4821cb..71a8ba77 100644 --- a/apps/recovery-relay/context/Workspace.tsx +++ b/apps/recovery-relay/context/Workspace.tsx @@ -11,10 +11,10 @@ import { useOfflineQuery, } from '@fireblocks/recovery-shared'; import { getAssetConfig } from '@fireblocks/asset-config'; -import packageJson from '../package.json'; -import { WalletClasses, Derivation } from '../lib/wallets'; import { LOGGER_NAME_RELAY } from '@fireblocks/recovery-shared/constants'; import { isTransferableToken } from '@fireblocks/asset-config/util'; +import packageJson from '../package.json'; +import { WalletClasses, Derivation } from '../lib/wallets'; type DistributiveOmit = T extends any ? Omit : never; @@ -106,6 +106,7 @@ export const WorkspaceProvider = ({ children }: Props) => { resetInboundRelayUrl, getOutboundRelayUrl: baseGetOutboundRelayUrl, importCsv, + getExtendedKeysForAccountId, setExtendedKeys, setTransaction, addAccount, @@ -120,9 +121,8 @@ export const WorkspaceProvider = ({ children }: Props) => { if (input.assetId in WalletClasses) { logger.debug(`Dervied wallet for a token: ${input.assetId}`); return new WalletClasses[input.assetId as keyof typeof WalletClasses](input, 0); - } else { - throw new Error(`Unsupported token: ${input.assetId}`); } + throw new Error(`Unsupported token: ${input.assetId}`); } const nativeAssetId = (getAssetConfig(input.assetId)?.nativeAsset ?? input.assetId) as keyof typeof WalletClasses; @@ -231,6 +231,7 @@ export const WorkspaceProvider = ({ children }: Props) => { resetInboundRelayUrl, getOutboundRelayUrl, importCsv, + getExtendedKeysForAccountId, setExtendedKeys, setTransaction, addAccount, diff --git a/apps/recovery-relay/lib/defaultRPCs.ts b/apps/recovery-relay/lib/defaultRPCs.ts index 0586a5b0..180d9492 100644 --- a/apps/recovery-relay/lib/defaultRPCs.ts +++ b/apps/recovery-relay/lib/defaultRPCs.ts @@ -16,6 +16,7 @@ export const defaultRPCs: Record< enabled: true, allowedEmptyValue: false, }, + FLR: { url: 'https://flare-api.flare.network/ext/C/rpc', name: 'Flare', enabled: true, allowedEmptyValue: false }, BTC: { url: 'https://api.blockchair.com/bitcoin', name: 'Bitcoin', enabled: true, allowedEmptyValue: false }, BTC_TEST: { url: 'https://api.blockchair.com/bitcoin/testnet', diff --git a/apps/recovery-relay/lib/wallets/ERC20/index.ts b/apps/recovery-relay/lib/wallets/ERC20/index.ts index 45bdcaed..e0a25785 100644 --- a/apps/recovery-relay/lib/wallets/ERC20/index.ts +++ b/apps/recovery-relay/lib/wallets/ERC20/index.ts @@ -50,7 +50,7 @@ export class ERC20 extends EVMBase implements ConnectedWallet { public setDecimals(decimals: number) { this.decimals = decimals; - if (!this.decimals) { + if (this.decimals === undefined || this.decimals === null) { this.relayLogger.error(`ERC20 Token decimals are unavailable: ${this.assetId}`); throw new Error(`ERC20 Token decimals are unavailable: ${this.assetId}`); } diff --git a/apps/recovery-relay/lib/wallets/EVM/FLR.ts b/apps/recovery-relay/lib/wallets/EVM/FLR.ts new file mode 100644 index 00000000..0ba08bdd --- /dev/null +++ b/apps/recovery-relay/lib/wallets/EVM/FLR.ts @@ -0,0 +1,103 @@ +import { ConnectedWallet } from '../ConnectedWallet'; +import { JsonRpcProvider, formatEther } from 'ethers'; +import { EVMWallet as EVMBase, Input } from '@fireblocks/wallet-derivation'; +import { AccountData } from '../types'; +import BigNumber from 'bignumber.js'; + +export class Flare extends EVMBase implements ConnectedWallet { + protected provider: JsonRpcProvider | undefined; + + protected weiBalance: bigint | string | undefined = BigInt(0); + + public rpcURL: string | undefined; + public chainId = 554; // for derivation + public evmChainId = 14; // for EVM tx + + constructor(input: Input) { + super(input, 554); + + this.relayLogger.info('Creating FLR wallet:', { chainId: this.chainId, input }); + } + + public setRPCUrl(url: string): void { + this.rpcURL = url; + this.relayLogger.info('setting FLR provider', { rpcURL: this.rpcURL, chainId: this.chainId }); + this.provider = new JsonRpcProvider(this.rpcURL, this.evmChainId, { cacheTimeout: -1 }); + } + + public async getBalance() { + try { + this.relayLogger.info('Getting FLR balance for address:', { address: this.address, provider: this.provider }); + this.weiBalance = await this.provider!.getBalance(this.address); + const balance = formatEther(this.weiBalance); + const flrBalance = Number(balance); + + this.relayLogger.info('FLR balance info', { flrBalance }); + + return flrBalance; + } catch (error) { + this.relayLogger.error('Error getting FLR balance:', error); + throw new Error(`Error getting FLR balance: ${error instanceof Error ? error.message : String(error)}`); + } + } + + public async prepare(): Promise { + const displayBalance = await this.getBalance(); + + if (displayBalance === 0) { + return { + balance: 0, + insufficientBalance: true, + }; + } + + const nonce = await this.provider!.getTransactionCount(this.address, 'latest'); + + // Should we use maxGasPrice? i.e. EIP1559. + const { gasPrice } = await this.provider!.getFeeData(); + + if (!gasPrice) { + throw new Error('No gas price found'); + } + + const gas = gasPrice * 21000n; + //@ts-ignore + const balance = new BigNumber(this.weiBalance.toString()); + + const adjustedBalance = balance.minus(new BigNumber(gas.toString())); + + if (adjustedBalance.lt(new BigNumber(0))) { + this.relayLogger.error('Insufficient balance'); + } + + const extraParams = new Map(); + extraParams.set(this.KEY_EVM_WEI_BALANCE, adjustedBalance.toString(16)); + + const preparedData = { + balance: displayBalance, + nonce, + gasPrice, + chainId: parseInt(this.evmChainId.toString()), + extraParams, + }; + + this.relayLogger.logPreparedData('FLR', preparedData); + return preparedData; + } + + public async broadcastTx(tx: string): Promise { + try { + const txRes = await this.provider!.broadcastTransaction(tx); + this.relayLogger.debug(`FLR: Tx broadcasted: ${JSON.stringify(txRes, null, 2)}`); + return txRes.hash; + } catch (e) { + this.relayLogger.error('FLR: Error broadcasting tx:', e); + if ((e as Error).message.includes('insufficient funds for intrinsic transaction cost')) { + throw new Error( + 'Insufficient funds for transfer, this might be due to a spike in network fees, please wait and try again', + ); + } + throw e; + } + } +} diff --git a/apps/recovery-relay/lib/wallets/SPL/index.ts b/apps/recovery-relay/lib/wallets/SPL/index.ts new file mode 100644 index 00000000..7a517f0c --- /dev/null +++ b/apps/recovery-relay/lib/wallets/SPL/index.ts @@ -0,0 +1,151 @@ +import * as web3 from '@solana/web3.js'; +import { getAssociatedTokenAddress, createTransferInstruction, getAccount, TokenAccountNotFoundError } from '@solana/spl-token'; +import { Solana as BaseSolana, Input } from '@fireblocks/wallet-derivation'; +import { ConnectedWallet } from '../ConnectedWallet'; +import { AccountData } from '../types'; +import { ipcRenderer } from 'electron'; +import { splAssets } from './solana_assets'; +import { unsupportedSplAssets } from './unsupported_solana_assets'; + +export class SPL extends BaseSolana implements ConnectedWallet { + public rpcURL: string | undefined; + public solConnection: web3.Connection | undefined; + + public mint?: string; + public amount?: number; + public decimals?: number; + + constructor(input: Input) { + super(input); + } + + public setRPCUrl(url: string): void { + this.rpcURL = url; + this.solConnection = new web3.Connection(url, { + commitment: 'confirmed', + fetch: async (input: string | URL | Request, init) => { + const res = await ipcRenderer.invoke('main_proc_fetch', input, init); + return new Response(res); + }, + }); + } + + public async getBalance(): Promise { + try { + if (!this.mint) throw new Error('mint not set'); + + const mintPubkey = new web3.PublicKey(this.mint); + const ata = await getAssociatedTokenAddress(mintPubkey, this.web3PubKey); + ata.toString(); + try { + const { value } = await this.solConnection!.getTokenAccountBalance(ata); + + this.relayLogger.debug('SPL Token balance info', value); + + const amount = parseFloat(value.amount); + this.decimals = value.decimals; + + return amount / 10 ** this.decimals; + } catch (err) { + if ((err as Error).message.includes('could not find account')) { + this.relayLogger.warn('SPL Token account does not exist. Returning 0 balance.'); + return 0; + } + + this.relayLogger.error('Error fetching SPL token balance', err); + throw err; + } + } catch (err) { + this.relayLogger.error('Error in getBalance()', err); + throw err; + } + } + public async prepare(toAddress: string): Promise { + try { + const allSplAssets = [...splAssets, ...unsupportedSplAssets]; + this.mint = allSplAssets.find((asset) => asset.legacyId === this.assetId)?.onchain.address; + if (!this.mint) throw new Error('mint not set'); + const mintPubkey = new web3.PublicKey(this.mint); + + const toPubkey = new web3.PublicKey(toAddress); + + const fromATA = await getAssociatedTokenAddress(mintPubkey, this.web3PubKey); + const toATA = await getAssociatedTokenAddress(mintPubkey, this.web3PubKey); // dummy to self + + let { blockhash } = await this.solConnection!.getLatestBlockhash(); + + if (!this.decimals) { + const mintInfo = await this.solConnection!.getParsedAccountInfo(mintPubkey); + if (!mintInfo.value?.data || !('parsed' in mintInfo.value.data)) { + throw new Error('Invalid mint account'); + } + this.decimals = mintInfo.value.data.parsed.info.decimals; + } + + // Build a dummy SPL token transfer tx (just for fee estimation) + const dummyTx = new web3.Transaction({ + feePayer: this.web3PubKey, + blockhash, + lastValidBlockHeight: 999_999, // safe default + }); + + dummyTx.add( + createTransferInstruction( + fromATA, + toATA, + this.web3PubKey, + 1, // 1 unit for simulation + ), + ); + + const feeForTxLamports = (await this.solConnection!.getFeeForMessage(dummyTx.compileMessage())).value ?? 0; + const feeForTx = feeForTxLamports / web3.LAMPORTS_PER_SOL; + + const accountBalance = await this.getBalance(); + const balanceAfterFee = Math.floor((accountBalance - feeForTx) * web3.LAMPORTS_PER_SOL) / web3.LAMPORTS_PER_SOL; + + // Refresh blockhash to give real tx more time + blockhash = (await this.solConnection!.getLatestBlockhash()).blockhash; + + const extraParams = new Map(); + extraParams.set(this.KEY_RECENT_BLOCKHASH, blockhash); + extraParams.set('mint', this.mint!); + extraParams.set('decimals', this.decimals!.toString()); + try { + const toAccount = await getAssociatedTokenAddress(mintPubkey, toPubkey, false); + await getAccount(this.solConnection!, toAccount); + } catch (err) { + if (err instanceof TokenAccountNotFoundError) { + this.utilityLogger.warn('SPL: To ATA does not exist. Signer must add ATA creation instruction'); + extraParams.set('createATA', 'true'); + } else { + this.utilityLogger.warn('SPL Error: ', err); + throw err; + } + } + + const preparedData: AccountData = { + balance: balanceAfterFee, + insufficientBalance: balanceAfterFee < 0.00000001, + extraParams, + }; + + this.relayLogger.logPreparedData('SPL', preparedData); + return preparedData; + } catch (err) { + this.relayLogger.error('Error in prepare()', err); + throw err; + } + } + + public async broadcastTx(txHex: string): Promise { + try { + const hash = await this.solConnection!.sendRawTransaction(Buffer.from(txHex, 'hex')); + this.relayLogger.debug(`SPL: Tx broadcasted: ${hash}`); + return hash; + } catch (e) { + this.relayLogger.error(`SPL: Error broadcasting tx: ${e}`); + throw e; + } + } +} diff --git a/apps/recovery-relay/lib/wallets/SPL/solana_assets.ts b/apps/recovery-relay/lib/wallets/SPL/solana_assets.ts new file mode 100644 index 00000000..41e76643 --- /dev/null +++ b/apps/recovery-relay/lib/wallets/SPL/solana_assets.ts @@ -0,0 +1,1900 @@ +export const splAssets = [ + { + id: '048d0a77-3843-4af2-b5c0-3154b8bbe0a7', + legacyId: 'CSOL_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Solend SOL', + displaySymbol: 'cSOL', + assetClass: 'FT', + onchain: { + symbol: 'cSOL', + name: 'Solend SOL', + address: '5h6ssFpeDeRbzsEHDbTQNH7nVGgsKrZydxdSTnLm6QdV', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '06636d85-4dd7-4cbc-a78c-26478c9a6fa8', + legacyId: 'SAMO_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Samoyed Coin', + displaySymbol: 'SAMO', + assetClass: 'FT', + onchain: { + symbol: 'SAMO', + name: 'Samoyed Coin', + address: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '066770ba-72f9-4af9-9e1c-bc5dfeeb7fff', + legacyId: 'RUN_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Run Token', + displaySymbol: 'RUN', + assetClass: 'FT', + onchain: { + symbol: 'RUN', + name: 'Run Token', + address: '6F9XriABHfWhit6zmMUYAQBSy6XK5VF1cHXuW5LDpRtC', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '06cd2424-2930-4991-a827-76fcec95fe77', + legacyId: 'LDO_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Lido DAO Token (Wormhole)', + displaySymbol: 'LDO', + assetClass: 'FT', + onchain: { + symbol: 'LDO', + name: 'Lido DAO Token (Wormhole)', + address: 'HZRCwxP2Vq9PCpPXooayhJ2bxTpo5xfpQrwB1svh332p', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: '0b7cf7b0-b932-450f-a5fc-991af54f83f5', + legacyId: '$WIF_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'dogwifhat', + displaySymbol: '$WIF', + assetClass: 'FT', + onchain: { + symbol: '$WIF', + name: 'dogwifhat', + address: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '0c2e82f9-f6a8-4817-8a49-32bd5d3311aa', + legacyId: 'TTT_SPL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'TabTrader Token', + displaySymbol: 'TTT', + assetClass: 'FT', + onchain: { + symbol: 'TTT', + name: 'TabTrader Token', + address: 'FNFKRV3V8DtA3gVJN6UshMiLGYA8izxFwkNWmJbFjmRj', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '188835b3-5241-4bd9-a80e-d809000748c6', + legacyId: 'MNDE_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Marinade', + displaySymbol: 'MNDE', + assetClass: 'FT', + onchain: { + symbol: 'MNDE', + name: 'Marinade', + address: 'MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '19a44b48-4b45-436a-9f6d-d284e2c745f2', + legacyId: 'ORCA_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/4fa785f4-5a48-4a0d-bdff-6186b4776458', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/4c582f72-f4a5-4c88-b337-f95408f0e9a7', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Orca', + displaySymbol: 'ORCA', + assetClass: 'FT', + onchain: { + symbol: 'ORCA', + name: 'Orca', + address: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '1ba55819-5ce2-464e-a4c3-efeab0c3222f', + legacyId: 'RENDER_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/6d6a5903-9757-4c75-ad68-9b99a01bce6d', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/1436e0a1-b703-4a91-8d6b-71a054d4f5b3', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Render Token', + displaySymbol: 'RENDER', + assetClass: 'FT', + onchain: { + symbol: 'RENDER', + name: 'Render Token', + address: 'rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: '1f18c7e2-d852-4604-81a0-1a41ad78734a', + legacyId: 'TRUMP_B6KKMHK5_E95U', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'OFFICIAL TRUMP', + displaySymbol: 'TRUMP', + assetClass: 'FT', + onchain: { + symbol: 'TRUMP', + name: 'OFFICIAL TRUMP', + address: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '1f241dd0-d164-4d2f-9f5b-95e1a551e12f', + legacyId: 'SONAR_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Sonar Watch', + displaySymbol: 'SONAR', + assetClass: 'FT', + onchain: { + symbol: 'SONAR', + name: 'Sonar Watch', + address: 'sonarX4VtVkQemriJeLm6CKeW3GDMyiBnnAEMw1MRAE', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '25756c57-5127-4a95-a5d1-bdbd50eac8e1', + legacyId: 'FANT_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Phantasia', + displaySymbol: 'FANT', + assetClass: 'FT', + onchain: { + symbol: 'FANT', + name: 'Phantasia', + address: 'FANTafPFBAt93BNJVpdu25pGPmca3RfwdsDsRrT3LX1r', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '25d98f19-d85a-44ef-a844-cde6b6093b88', + legacyId: 'YARD_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'SolYard Finance', + displaySymbol: 'YARD', + assetClass: 'FT', + onchain: { + symbol: 'YARD', + name: 'SolYard Finance', + address: '8RYSc3rrS4X4bvBCtSJnhcpPpMaAJkXnVKZPzANxQHgz', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '2a73686b-8cd6-40c6-ab20-8b3c65c4d6e3', + legacyId: 'RAY_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/867a6e4e-cc04-4f73-a656-2daa76411267', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/77651e4d-5e4d-4657-9407-68a09bc0d92c', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Raydium', + displaySymbol: 'RAY', + assetClass: 'FT', + onchain: { + symbol: 'RAY', + name: 'Raydium', + address: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '2c7f0372-11df-4882-b773-5289240c9125', + legacyId: 'STSOL_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Lido Staked SOL', + displaySymbol: 'stSOL', + assetClass: 'FT', + onchain: { + symbol: 'stSOL', + name: 'Lido Staked SOL', + address: '7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '2e7a601d-14ba-4963-824b-1b9004e72396', + legacyId: 'SYP_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Sypool', + displaySymbol: 'SYP', + assetClass: 'FT', + onchain: { + symbol: 'SYP', + name: 'Sypool', + address: 'FnKE9n6aGjQoNWRBZXy4RW6LZVao7qwBonUbiD7edUmZ', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '2f15859d-e8a1-462f-ad31-9eace32c47d5', + legacyId: 'JTO_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/aea0b27a-b8fb-453b-b810-f9146dc424fa', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/c103eeee-1374-42f3-aa56-a7585d2106b5', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'JITO', + displaySymbol: 'JTO', + assetClass: 'FT', + onchain: { + symbol: 'JTO', + name: 'JITO', + address: 'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '35a26b01-d78f-4983-87f7-f7ff239b0ee8', + legacyId: 'NINJA_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'NINJA', + displaySymbol: 'NINJA', + assetClass: 'FT', + onchain: { + symbol: 'NINJA', + name: 'NINJA', + address: 'FgX1WD9WzMU3yLwXaFSarPfkgzjLb2DZCqmkx9ExpuvJ', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '361e7db7-a94a-405e-9e4a-3e3c4ec67cea', + legacyId: 'SLRS_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Solrise Finance', + displaySymbol: 'SLRS', + assetClass: 'FT', + onchain: { + symbol: 'SLRS', + name: 'Solrise Finance', + address: 'SLRSSpSLUTP7okbCUBYStWCo1vUgyt775faPqz8HUMr', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '36e58a32-0873-4978-af6e-cc394c742462', + legacyId: 'SOL_MAPS_FD7T', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'MAPS', + displaySymbol: 'MAPS', + assetClass: 'FT', + onchain: { + symbol: 'MAPS', + name: 'MAPS', + address: 'MAPS41MDahZ9QdKXhVa4dWB9RuyfV4XqhyAZ8XcYepb', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '3917b4d7-ebb3-489c-94ff-73e3e7dd352a', + legacyId: 'SHILL_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Project SEED Token', + displaySymbol: 'SHILL', + assetClass: 'FT', + onchain: { + symbol: 'SHILL', + name: 'Project SEED Token', + address: '6cVgJUqo4nmvQpbgrDZwyfd6RwWw5bfnCamS3M9N1fd', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '3bdaa635-8f77-4965-bc26-2b8c7f7cd512', + legacyId: 'SUNNY_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Sunny Governance Token', + displaySymbol: 'SUNNY', + assetClass: 'FT', + onchain: { + symbol: 'SUNNY', + name: 'Sunny Governance Token', + address: 'SUNNYWgPQmFxe9wTZzNK7iPnJ3vYDrkgnxJRJm1s3ag', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '43bf91bf-feb6-4267-b56f-0fdc8a067af6', + legacyId: 'UST_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'UST (Wormhole)', + displaySymbol: 'UST', + assetClass: 'FT', + onchain: { + symbol: 'UST', + name: 'UST (Wormhole)', + address: '9vMJfxuKxXBoEa7rM12mYLMwTacLMLDJqHozw96WQL8i', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '47c8526e-6f27-459d-9831-f59780603ec6', + legacyId: 'SOL_USDT_EWAY', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/d60cba87-6fd7-444a-b5cc-d0c903ca03e1', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/d64cdec8-5a7b-4a34-bee2-c5126197b8ab', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'USDT', + displaySymbol: 'USDT', + assetClass: 'FT', + onchain: { + symbol: 'USDT', + name: 'USDT', + address: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '4913a031-92e5-4c0a-bb7b-e547c34a2a4b', + legacyId: 'UNI_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Uniswap (Wormhole)', + displaySymbol: 'UNI', + assetClass: 'FT', + onchain: { + symbol: 'UNI', + name: 'Uniswap (Wormhole)', + address: '8FU95xFJhUUkyyCLU13HSzDLs7oC4QZdXQHL6SCeab36', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: '512c2224-e6e4-43c9-a57a-03474cc3394e', + legacyId: 'SBR_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Saber Protocol Token', + displaySymbol: 'SBR', + assetClass: 'FT', + onchain: { + symbol: 'SBR', + name: 'Saber Protocol Token', + address: 'Saber2gLauYim4Mvftnrasomsv6NvAuncvMEZwcLpD1', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '543bc343-98e8-4b85-bfdd-6e43d402453b', + legacyId: 'GOFX_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'GooseFX', + displaySymbol: 'GOFX', + assetClass: 'FT', + onchain: { + symbol: 'GOFX', + name: 'GooseFX', + address: 'GFX1ZjR2P15tmrSwow6FjyDYcEkoFb4p4gJCpLBjaxHD', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '55c35272-7608-4621-8ad2-2311ac3f0460', + legacyId: 'HNT_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/baab1916-03d1-436e-9705-ae500fe7e32b', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/91c33434-6aef-4d56-81d2-c84da797ea99', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Helium Network Token', + displaySymbol: 'HNT', + assetClass: 'FT', + onchain: { + symbol: 'HNT', + name: 'Helium Network Token', + address: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: '59a10d2b-6231-4080-8606-9690f68b472b', + legacyId: 'SOL_SRM_R62M', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/26b6ea10-fb88-4476-9487-6346c57d9709', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/61e4fd91-93d2-4da4-841c-a8d80ff9158d', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Serum', + displaySymbol: 'SRM', + assetClass: 'FT', + onchain: { + symbol: 'SRM', + name: 'Serum', + address: 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '59fdcd8e-36a1-41bb-ad65-903aed9f3034', + legacyId: 'SB_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'SuperBonds Token', + displaySymbol: 'SB', + assetClass: 'FT', + onchain: { + symbol: 'SB', + name: 'SuperBonds Token', + address: 'SuperbZyz7TsSdSoFAZ6RYHfAWe9NmjXBLVQpS8hqdx', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '5b531b9c-9a08-43a1-a55e-cde93b2bd1be', + legacyId: 'TRYB_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'BiLira', + displaySymbol: 'TRYB', + assetClass: 'FT', + onchain: { + symbol: 'TRYB', + name: 'BiLira', + address: 'A94X2fRy3wydNShU4dRaDyap2UuoeWJGWyATtyp61WZf', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '5c4e9fae-2992-4773-bb0e-3beb09d66ed9', + legacyId: 'PYUSD_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/cb3875a1-8df7-4290-be87-27497e5bdb38', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/7334f146-df94-4aa0-ac3d-86299d0d09a2', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'PayPal USD', + displaySymbol: 'PYUSD', + assetClass: 'FT', + onchain: { + symbol: 'PYUSD', + name: 'PayPal USD', + address: '2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo', + decimals: 6, + standards: ['TOKEN2022'], + }, + }, + { + id: '5d81cbd4-3133-4b97-9563-5059f8c65836', + legacyId: 'UNQ_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Unique Venture Clubs', + displaySymbol: 'UNQ', + assetClass: 'FT', + onchain: { + symbol: 'UNQ', + name: 'Unique Venture Clubs', + address: 'UNQtEecZ5Zb4gSSVHCAWUQEoNnSVEbWiKCi1v9kdUJJ', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '5da296d8-b09c-40a7-8b60-e24f8c173e5b', + legacyId: 'HBB_SPL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Hubble Protocol Token', + displaySymbol: 'HBB', + assetClass: 'FT', + onchain: { + symbol: 'HBB', + name: 'Hubble Protocol Token', + address: 'HBB111SCo9jkCejsZfz8Ec8nH7T6THF8KEKSnvwT6XK6', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '5f0a4d87-f874-4d91-b721-020b416d494f', + legacyId: 'W_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/b9720ca9-1478-4e3d-b693-b7c6e2aa6c10', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/0b999681-8536-41d1-b41d-11937628b69e', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Wormhole Token', + displaySymbol: 'W', + assetClass: 'FT', + onchain: { + symbol: 'W', + name: 'Wormhole Token', + address: '85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '61874879-d7ca-42ea-999f-95132047799f', + legacyId: 'CWAR_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Cryowar Token', + displaySymbol: 'CWAR', + assetClass: 'FT', + onchain: { + symbol: 'CWAR', + name: 'Cryowar Token', + address: 'HfYFjMKNZygfMC8LsQ8LtpPsPxEJoXJx4M6tqi75Hajo', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '64b44c8c-35e3-4cc8-ae13-69e49638fd62', + legacyId: 'FTT_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'FTT (Wormhole)', + displaySymbol: 'FTX Token', + assetClass: 'FT', + onchain: { + symbol: 'FTX Token', + name: 'FTT (Wormhole)', + address: 'EzfgjvkSwthhgHaceR3LnKXUoRkP6NUhfghdaHAj1tUv', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: '652b18be-bb27-4fa5-ac2a-3688e60924d3', + legacyId: 'GMT_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/6fe89196-8ea9-45f7-9ddd-415ab9d25559', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/90c26676-483c-4d50-9339-6f016a7e97f3', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'GMT', + displaySymbol: 'GMT', + assetClass: 'FT', + onchain: { + symbol: 'GMT', + name: 'GMT', + address: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '667c0ee4-62a7-4001-a3e1-940258223ef5', + legacyId: 'PYTH_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/cb862799-e954-4e5a-82b9-d58d30f8bc44', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/b1b7d543-90bf-4f00-bef1-f5a22d5d0136', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Pyth Network', + displaySymbol: 'PYTH', + assetClass: 'FT', + onchain: { + symbol: 'PYTH', + name: 'Pyth Network', + address: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '68bdefae-e6d8-453b-9c72-0e7f380e36e2', + legacyId: 'RIN_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Aldrin', + displaySymbol: 'RIN', + assetClass: 'FT', + onchain: { + symbol: 'RIN', + name: 'Aldrin', + address: 'E5ndSkaB17Dm7CsD22dvcjfrYSDLCxFcMd6z8ddCk5wp', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '693dadfc-3ca9-492d-a719-05a95b8d5c6f', + legacyId: 'CRP_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'CropperFinance', + displaySymbol: 'CRP', + assetClass: 'FT', + onchain: { + symbol: 'CRP', + name: 'CropperFinance', + address: 'DubwWZNWiNGMMeeQHPnMATNj77YZPZSAz2WVR5WjLJqz', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '6cfe04a0-5f3e-44e7-babf-7ea96cfd0928', + legacyId: 'SLND_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Solend', + displaySymbol: 'SLND', + assetClass: 'FT', + onchain: { + symbol: 'SLND', + name: 'Solend', + address: 'SLNDpmoWTVADgEdndyvWzroNL7zSi1dF9PC3xHGtPwp', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '6f8748ef-3dc8-4566-8fec-9b946753eb26', + legacyId: 'ETH_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/688cd5dc-a243-42fe-a33b-1a3dc4949e3c', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/6051b84b-e5fe-40c0-a05f-447e0cd85bb0', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Wrapped Ether (Wormhole)', + displaySymbol: 'WETH', + assetClass: 'FT', + onchain: { + symbol: 'WETH', + name: 'Wrapped Ether (Wormhole)', + address: '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: '700483bb-2efa-4ac0-9b68-e55f7404394f', + legacyId: 'SHIB_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'SHIBA INU (Wormhole)', + displaySymbol: 'SHIB', + assetClass: 'FT', + onchain: { + symbol: 'SHIB', + name: 'SHIBA INU (Wormhole)', + address: 'CiKu4eHsVrc1eueVQeHn7qhXTcVu95gSQmBpX4utjL9z', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: '71c54a64-e74a-4aaf-9a53-82eace9b340c', + legacyId: 'LIKE_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Only1 (LIKE)', + displaySymbol: 'LIKE', + assetClass: 'FT', + onchain: { + symbol: 'LIKE', + name: 'Only1 (LIKE)', + address: '3bRTivrVsitbmCTGtqwp7hxXPsybkjn4XLNtPsHqa3zR', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '7798016f-d801-4d98-93c1-d25baaaf81a1', + legacyId: 'GARI_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Gari', + displaySymbol: 'GARI', + assetClass: 'FT', + onchain: { + symbol: 'GARI', + name: 'Gari', + address: 'CKaKtYvz6dKPyMvYq9Rh3UBrnNqYZAyd7iF4hJtjUvks', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '78d2f31a-5917-436c-9802-7584944db637', + legacyId: 'SOL_OXY_4TTN', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Oxygen Protocol', + displaySymbol: 'OXY', + assetClass: 'FT', + onchain: { + symbol: 'OXY', + name: 'Oxygen Protocol', + address: 'z3dn17yLaGMKffVogeFHQ9zWVcXgqgf3PQnDsNs2g6M', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '857ffa09-b635-478c-9e51-622d0594ae38', + legacyId: 'POLIS_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Star Atlas DAO', + displaySymbol: 'POLIS', + assetClass: 'FT', + onchain: { + symbol: 'POLIS', + name: 'Star Atlas DAO', + address: 'poLisWXnNRwC6oBu1vHiuKQzFjGL4XDSu4g9qjz9qVk', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: '870e896e-dac4-4992-8b8a-c7e1ae2a7677', + legacyId: 'STEP_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Step', + displaySymbol: 'STEP', + assetClass: 'FT', + onchain: { + symbol: 'STEP', + name: 'Step', + address: 'StepAscQoEioFxxWGnh2sLBDFp9d8rvKz2Yp39iDpyT', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '89091898-14a4-463b-b661-2c0d557c6e52', + legacyId: 'BONK_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Bonk', + displaySymbol: 'Bonk', + assetClass: 'FT', + onchain: { + symbol: 'Bonk', + name: 'Bonk', + address: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', + decimals: 5, + standards: ['SPL'], + }, + }, + { + id: '8c9ecf3e-9641-4986-8e57-822697df8338', + legacyId: 'SOLX_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Soldex', + displaySymbol: 'SOLX', + assetClass: 'FT', + onchain: { + symbol: 'SOLX', + name: 'Soldex', + address: 'CH74tuRLTYcxG7qNJCsV9rghfLXJCQJbsu7i52a8F1Gn', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '97ee6227-ff95-4caf-b339-c827e30acb5b', + legacyId: 'STR_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Solster', + displaySymbol: 'STR', + assetClass: 'FT', + onchain: { + symbol: 'STR', + name: 'Solster', + address: '9zoqdwEBKWEi9G5Ze8BSkdmppxGgVv1Kw4LuigDiNr9m', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: '99388502-9e39-4057-9d7b-7d2bf4876078', + legacyId: 'DXL_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Dexlab', + displaySymbol: 'DXL', + assetClass: 'FT', + onchain: { + symbol: 'DXL', + name: 'Dexlab', + address: 'GsNzxJfFn6zQdJGeYsupJWzUAm57Ba7335mfhWvFiE9Z', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: '9c4a0c48-4547-4d77-b364-c1ef68ef64f5', + legacyId: 'ATLAS_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Star Atlas', + displaySymbol: 'ATLAS', + assetClass: 'FT', + onchain: { + symbol: 'ATLAS', + name: 'Star Atlas', + address: 'ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: '9e54daa7-2e7e-4583-9850-12c151d89ab8', + legacyId: 'MBS_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'MonkeyBucks', + displaySymbol: 'MBS', + assetClass: 'FT', + onchain: { + symbol: 'MBS', + name: 'MonkeyBucks', + address: 'Fm9rHUTF5v3hwMLbStjZXqNBBoZyGriQaFM6sTFz3K8A', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'a5e7a0e7-8728-4c8b-956c-4846315aa9ca', + legacyId: 'NAXAR_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Naxar', + displaySymbol: 'NAXAR', + assetClass: 'FT', + onchain: { + symbol: 'NAXAR', + name: 'Naxar', + address: 'Fp4gjLpTsPqBN6xDGpDHwtnuEofjyiZKxxZxzvJnjxV6', + decimals: 4, + standards: ['SPL'], + }, + }, + { + id: 'a618e176-a979-4fd1-ab33-8b48692fedbe', + legacyId: 'SPWN_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Bitspawn Token', + displaySymbol: 'SPWN', + assetClass: 'FT', + onchain: { + symbol: 'SPWN', + name: 'Bitspawn Token', + address: '5U9QqCPhqXAJcEv9uyzFJd5zhN93vuPk1aNNkXnUfPnt', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: 'a8999ded-66b5-42ed-a1cd-43cdfda3282b', + legacyId: 'SLC_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Solice', + displaySymbol: 'SLC', + assetClass: 'FT', + onchain: { + symbol: 'SLC', + name: 'Solice', + address: 'METAmTMXwdb8gYzyCPfXXFmZZw4rUsXX58PNsDg7zjL', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'aadba0da-3f62-406a-9556-5ed47075443d', + legacyId: 'REAL_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Realy Token', + displaySymbol: 'REAL', + assetClass: 'FT', + onchain: { + symbol: 'REAL', + name: 'Realy Token', + address: 'AD27ov5fVU2XzwsbvnFvb1JpCBaCB5dRXrczV9CqSVGb', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: 'ad2bc979-0239-435e-9a20-d5275552f917', + legacyId: 'MSOL_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/50ba8ead-1446-4813-9cca-e7142129d30f', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/33e45edc-ab72-4610-a06b-d1770936838e', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Marinade staked SOL (mSOL)', + displaySymbol: 'mSOL', + assetClass: 'FT', + onchain: { + symbol: 'mSOL', + name: 'Marinade staked SOL (mSOL)', + address: 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: 'ae633088-4f52-4061-8917-4a791dc1a29b', + legacyId: 'PUMP_B6KKMHK5_V7UT', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Pump', + displaySymbol: 'PUMP', + assetClass: 'FT', + onchain: { + symbol: 'PUMP', + name: 'Pump', + address: 'pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn', + decimals: 6, + standards: ['TOKEN2022'], + }, + }, + { + id: 'ae786247-969a-4ce4-af43-2281aeab05d0', + legacyId: 'GENE_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Genopets', + displaySymbol: 'GENE', + assetClass: 'FT', + onchain: { + symbol: 'GENE', + name: 'Genopets', + address: 'GENEtH5amGSi8kHAtQoezp1XEXwZJ8vcuePYnXdKrMYz', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: 'b56a163c-edd4-4ae1-ab24-068fab674609', + legacyId: 'MNGO_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Mango', + displaySymbol: 'MNGO', + assetClass: 'FT', + onchain: { + symbol: 'MNGO', + name: 'Mango', + address: 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'b5fc110c-1a33-4c5b-96b2-3f418d64e7da', + legacyId: 'BLT_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Blocto', + displaySymbol: 'BLT', + assetClass: 'FT', + onchain: { + symbol: 'BLT', + name: 'Blocto', + address: 'BLT1noyNr3GttckEVrtcfC6oyK6yV1DpPgSyXbncMwef', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: 'bbbb54e5-e2d6-41ea-bc46-5077daa95c82', + legacyId: 'AI16Z_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'ai16z (Solana)', + displaySymbol: 'AI16Z', + assetClass: 'FT', + onchain: { + symbol: 'AI16Z', + name: 'ai16z (Solana)', + address: 'HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC', + decimals: 9, + standards: ['TOKEN2022'], + }, + }, + { + id: 'bdc4361f-6cf9-42b2-8e5a-f5614dfd83e4', + legacyId: 'JUP_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/a247daca-a17a-418d-afa3-82e63bb8f92b', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/a49956c2-f6f7-4cf8-9bb8-593d6b33adcf', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Jupiter', + displaySymbol: 'JUP', + assetClass: 'FT', + onchain: { + symbol: 'JUP', + name: 'Jupiter', + address: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'c05082f1-e562-4460-a614-329343d0b24a', + legacyId: 'LARIX_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Larix', + displaySymbol: 'LARIX', + assetClass: 'FT', + onchain: { + symbol: 'LARIX', + name: 'Larix', + address: 'Lrxqnh6ZHKbGy3dcrCED43nsoLkM1LTzU2jRfWe8qUC', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'c926f074-337a-4d25-b699-0d80b3394595', + legacyId: 'SCY_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Synchrony', + displaySymbol: 'SCY', + assetClass: 'FT', + onchain: { + symbol: 'SCY', + name: 'Synchrony', + address: 'SCYfrGCw8aDiqdgcpdGjV6jp4UVVQLuphxTDLNWu36f', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: 'ca0a1d5d-da9c-44a1-965b-0aa337e4504b', + legacyId: 'MELANIA_B6KKMHK5_RMZH', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Melania Meme', + displaySymbol: 'MELANIA', + assetClass: 'FT', + onchain: { + symbol: 'MELANIA', + name: 'Melania Meme', + address: 'FUAfBo2jgks6gB4Z4LfZkqSZgzNucisEHqnNebaRxM1P', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'cb257b52-1a54-482d-82c1-4600b98e44e5', + legacyId: 'PRT_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'PRT (Parrot Protocol)', + displaySymbol: 'PRT', + assetClass: 'FT', + onchain: { + symbol: 'PRT', + name: 'PRT (Parrot Protocol)', + address: 'PRT88RkA4Kg5z7pKnezeNH4mafTvtQdfFgpQTGRjz44', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'cdd6d3b3-7dc1-43c1-b9bf-206975b89d94', + legacyId: 'NEST_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Nest Arcade', + displaySymbol: 'NESTA', + assetClass: 'FT', + onchain: { + symbol: 'NESTA', + name: 'Nest Arcade', + address: 'Czt7Fc4dz6BpLh2vKiSYyotNK2uPPDhvbWrrLeD9QxhV', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: 'd120959a-ee6a-42d1-be5d-b7f03488c5a8', + legacyId: 'CAVE_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Crypto Cavemen', + displaySymbol: 'CAVE', + assetClass: 'FT', + onchain: { + symbol: 'CAVE', + name: 'Crypto Cavemen', + address: '4SZjjNABoqhbd4hnapbvoEPEqT8mnNkfbEoAwALf1V8t', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'd25d8833-b0aa-4b80-987d-f7539fa02338', + legacyId: 'SCT_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'SolClout', + displaySymbol: 'SCT', + assetClass: 'FT', + onchain: { + symbol: 'SCT', + name: 'SolClout', + address: '4Te4KJgjtnZe4aE2zne8G4NPfrPjCwDmaiEx9rKnyDVZ', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: 'd4db1508-0b66-41dd-9998-55e044f6e53a', + legacyId: 'MEAN_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'MEAN', + displaySymbol: 'MEAN', + assetClass: 'FT', + onchain: { + symbol: 'MEAN', + name: 'MEAN', + address: 'MEANeD3XDdUmNMsRGjASkSWdC8prLYsoRJ61pPeHctD', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'e6d5d685-e2bd-49e9-b3a1-562216e27268', + legacyId: 'SOETH_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Wrapped Ethereum (Sollet)', + displaySymbol: 'ETH', + assetClass: 'FT', + onchain: { + symbol: 'ETH', + name: 'Wrapped Ethereum (Sollet)', + address: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'e70d8784-2f47-47bb-9824-c89080705536', + legacyId: 'MDF_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'MatrixETF DAO Finance', + displaySymbol: 'MDF', + assetClass: 'FT', + onchain: { + symbol: 'MDF', + name: 'MatrixETF DAO Finance', + address: 'ALQ9KMWjFmxVbew3vMkJj3ypbAKuorSgGst6svCHEe2z', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'e9b5bddc-b1e6-4d69-b8fd-cffe68fc24fe', + legacyId: 'BOT_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Starbots Token', + displaySymbol: 'BOT', + assetClass: 'FT', + onchain: { + symbol: 'BOT', + name: 'Starbots Token', + address: 'AkhdZGVbJXPuQZ53u2LrimCjkRP6ZyxG1SoM85T98eE1', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: 'ec75b437-9c6d-40dd-a3da-4acab3782b43', + legacyId: 'CYS_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Cyclos', + displaySymbol: 'CYS', + assetClass: 'FT', + onchain: { + symbol: 'CYS', + name: 'Cyclos', + address: 'BRLsMczKuaR5w9vSubF4j8HwEGGprVAyyVgS4EX7DKEg', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'ed60acdc-b681-4269-b46d-32016b9190ef', + legacyId: 'CHICKS_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'SolChicks', + displaySymbol: 'CHICKS', + assetClass: 'FT', + onchain: { + symbol: 'CHICKS', + name: 'SolChicks', + address: 'cxxShYRVcepDudXhe7U62QHvw8uBJoKFifmzggGKVC2', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: 'efa2a18d-9c2f-4969-b998-f0f4f8a51037', + legacyId: 'SLIM_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Solanium', + displaySymbol: 'SLIM', + assetClass: 'FT', + onchain: { + symbol: 'SLIM', + name: 'Solanium', + address: 'xxxxa1sKNGwFtw2kFn8XauW9xq8hBZ5kVtcSesTT9fW', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'f4c63b55-e3f3-4cf5-ab37-4e71966d109a', + legacyId: 'ACM_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Acumen Governance Token', + displaySymbol: 'ACM', + assetClass: 'FT', + onchain: { + symbol: 'ACM', + name: 'Acumen Governance Token', + address: 'ACUMENkbnxQPAsN8XrNA11sY3NmXDNKVCqS82EiDqMYB', + decimals: 9, + standards: ['SPL'], + }, + }, + { + id: 'f6b14580-42bc-40d8-a920-16aa838c4551', + legacyId: 'SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/21ce5b0f-7be2-4ec0-a9fe-56c68695401d', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/f7c9fd20-3190-4569-929a-8bedfca9f0bc', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Solana', + displaySymbol: 'SOL', + assetClass: 'NATIVE', + onchain: { + symbol: 'SOL', + name: 'Solana', + decimals: 9, + }, + }, + { + id: 'f790b374-aab1-4dc1-8485-6f5d1b541a51', + legacyId: 'KIN_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'KIN', + displaySymbol: 'KIN', + assetClass: 'FT', + onchain: { + symbol: 'KIN', + name: 'KIN', + address: 'kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6', + decimals: 5, + standards: ['SPL'], + }, + }, + { + id: 'f9b22cda-585e-4c22-ae1b-9c3b5096d241', + legacyId: 'SUSHI_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'SushiToken (Wormhole)', + displaySymbol: 'SUSHI', + assetClass: 'FT', + onchain: { + symbol: 'SUSHI', + name: 'SushiToken (Wormhole)', + address: 'ChVzxWRmrTeSgwd3Ui3UumcN8KX7VK3WaD4KGeSKpypj', + decimals: 8, + standards: ['SPL'], + }, + }, + { + id: 'fba1c2ee-1c0f-4894-8eca-6951c5b77aff', + legacyId: 'SVT_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Solvent', + displaySymbol: 'SVT', + assetClass: 'FT', + onchain: { + symbol: 'SVT', + name: 'Solvent', + address: 'svtMpL5eQzdmB3uqK9NXaQkq8prGZoKQFNVJghdWCkV', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'fcdc0b95-1038-4529-85e9-7c5b6d972638', + legacyId: 'SOL_USDC_PTHX', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + media: [ + { + url: 'https://static.fireblocks.io/storage-service/44453cd6-8948-4e2d-a4a9-c7ef7dcb7b88', + type: 'image/svg+xml', + }, + { + url: 'https://static.fireblocks.io/storage-service/fb27edb5-3501-4737-9a31-7f891c4907b9', + type: 'image/png', + }, + ], + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'USD Coin', + displaySymbol: 'USDC', + assetClass: 'FT', + onchain: { + symbol: 'USDC', + name: 'USD Coin', + address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + decimals: 6, + standards: ['SPL'], + }, + }, + { + id: 'ffc4506d-1adc-40ce-bfc6-c51e87af5095', + legacyId: 'DYDX_SOL', + metadata: { + scope: 'GLOBAL', + verified: false, + deprecated: false, + }, + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'dYdX (Wormhole)', + displaySymbol: 'DYDX', + assetClass: 'FT', + onchain: { + symbol: 'DYDX', + name: 'dYdX (Wormhole)', + address: '4Hx6Bj56eGyw8EJrrheM6LBQAvVYRikYCWsALeTrwyRU', + decimals: 8, + standards: ['SPL'], + }, + }, +]; diff --git a/apps/recovery-relay/lib/wallets/SPL/unsupported_solana_assets.ts b/apps/recovery-relay/lib/wallets/SPL/unsupported_solana_assets.ts new file mode 100644 index 00000000..0cced19c --- /dev/null +++ b/apps/recovery-relay/lib/wallets/SPL/unsupported_solana_assets.ts @@ -0,0 +1,113 @@ +export const unsupportedSplAssets = [ + { + legacyId: 'PENGU_B6KKMHK5_8MGR', + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Pudgy Penguins', + displaySymbol: 'PENGU', + onchain: { + symbol: 'PENGU', + name: 'Pudgy Penguins', + decimals: 6, + address: '2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv', + standards: ['SPL'], + }, + }, + + { + legacyId: 'GRASS_B6KKMHK5_RDML', + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'GRASS', + displaySymbol: 'GRASS', + onchain: { + symbol: 'GRASS', + name: 'GRASS', + decimals: 9, + address: 'Grass7B4RdKfBCjTKgSqnXkqjwiGvQyFbuSCUJr3XXjs', + standards: ['SPL'], + }, + }, + + { + legacyId: 'MEW_SOL', + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'cat in a dogs world', + displaySymbol: 'MEW', + onchain: { + symbol: 'MEW', + name: 'cat in a dogs world', + decimals: 5, + address: 'MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5', + standards: ['SPL'], + }, + }, + + { + legacyId: 'ME_B6KKMHK5_SVCC', + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Magic Eden', + displaySymbol: 'ME', + onchain: { + symbol: 'ME', + name: 'Magic Eden', + decimals: 6, + address: 'MEFNBXixkEbait3xn9bkm8WsJzXtVsaJEn4c8Sam21u', + standards: ['SPL'], + }, + }, + + { + legacyId: 'ACT_B6KKMHK5_QI8V', + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Act I : The AI Prophecy', + displaySymbol: 'ACT', + onchain: { + symbol: 'ACT', + name: 'Act I : The AI Prophecy', + decimals: 6, + address: 'GJAFwWjJ3vnTsrQVabjBVK2TYB1YtRCQXRDfDgUnpump', + standards: ['SPL'], + }, + }, + + { + legacyId: 'PNUT__B6KKMHK5_UY26', + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Peanut the Squirrel', + displaySymbol: 'PNUT', + onchain: { + symbol: 'PNUT', + name: 'Peanut the Squirrel', + decimals: 6, + address: '2qEHjDLDLbuBgRYvsxhc5D6uDWAivNFZGan56P1tpump', + standards: ['SPL'], + }, + }, + + { + legacyId: 'BOME_SOL', + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'BOOK OF MEME', + displaySymbol: 'BOME', + onchain: { + symbol: 'BOME', + name: 'BOOK OF MEME', + decimals: 6, + address: 'ukHH6c7mMyiWCf1b9pnWe25TSpkDDt3H5pQZgZ74J82', + standards: ['SPL'], + }, + }, + + { + legacyId: 'TNSR_SOL', + blockchainId: 'dd754e7a-f4d5-466b-a847-45f31d4e5403', + displayName: 'Tensor', + displaySymbol: 'TNSR', + onchain: { + symbol: 'TNSR', + name: 'Tensor', + decimals: 6, + address: 'TNSRxcUxoT9xBG3de7PiJyTDYu7kskLqcpddxnEJAS6', + standards: ['SPL'], + }, + }, +]; diff --git a/apps/recovery-relay/lib/wallets/index.ts b/apps/recovery-relay/lib/wallets/index.ts index 38dc8a7b..ae471831 100644 --- a/apps/recovery-relay/lib/wallets/index.ts +++ b/apps/recovery-relay/lib/wallets/index.ts @@ -41,6 +41,10 @@ import { Ton } from './TON'; import { Jetton } from './Jetton'; import { ERC20 } from './ERC20'; import { TRC20 } from './TRC20'; +import { SPL } from './SPL'; +import { getAllSpls } from '@fireblocks/asset-config/assets'; + +import { Flare } from './EVM/FLR'; export { ConnectedWallet } from './ConnectedWallet'; const fillJettons = () => { @@ -82,11 +86,25 @@ const fillTRC20s = () => { return erc20Tokens; }; +const fillSpls = () => { + const splsList = getAllSpls(); + const spls = splsList.reduce( + (prev, curr) => ({ + ...prev, + [curr]: SPL, + }), + {}, + ) as any; + Object.keys(spls).forEach((key) => (spls[key] === undefined ? delete spls[key] : {})); + return spls; +}; + export const WalletClasses = { ALGO: Algorand, ALGO_TEST: Algorand, AVAX: Avalanche, AVAXTEST: Avalanche, + FLR: Flare, BTC: Bitcoin, BTC_TEST: Bitcoin, BCH: BitcoinCash, @@ -116,6 +134,10 @@ export const WalletClasses = { CELO: Celo, AURORA_DEV: Aurora, SOL: Solana, + + SOL_TEST: Solana, + ...fillSpls(), + RON: Ronin, XDC: XinFin, XRP: Ripple, @@ -127,7 +149,6 @@ export const WalletClasses = { MOVR: Moonriver, VLX_VLX: Velas, VLX_TEST: Velas, - SOL_TEST: Solana, TKX: TokenX, ADA: Cardano, ADA_TEST: Cardano, diff --git a/apps/recovery-relay/package.json b/apps/recovery-relay/package.json index 09055a57..ddccf6fd 100644 --- a/apps/recovery-relay/package.json +++ b/apps/recovery-relay/package.json @@ -4,7 +4,7 @@ "author": "Fireblocks (https://www.fireblocks.com)", "repository": "https://github.com/fireblocks/recovery/tree/main/apps/recovery-relay", "license": "GPL-3.0-or-later", - "version": "1.5.0", + "version": "1.6.0", "private": true, "scripts": { "dev": "next dev", @@ -30,6 +30,7 @@ "@mui/x-data-grid": "^6.9.2", "@polkadot/api": "10.9.1", "@polkadot/types": "10.9.1", + "@solana/spl-token": "^0.4.13", "@solana/web3.js": "^1.78.0", "@tanstack/react-query": "^4.29.19", "@taquito/signer": "^16.2.0", @@ -84,4 +85,4 @@ "eslint-config-next": "^13.4.9", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/apps/recovery-relay/pages/raw-signing.tsx b/apps/recovery-relay/pages/raw-signing.tsx new file mode 100644 index 00000000..afd9eecc --- /dev/null +++ b/apps/recovery-relay/pages/raw-signing.tsx @@ -0,0 +1,88 @@ +'use client'; +import React, { Suspense, useState } from 'react'; +import { useWorkspace } from '../context/Workspace'; +import { HDPath } from '@fireblocks/wallet-derivation'; +import { useSettings } from '../../recovery-utility/renderer/context/Settings'; +import { DeploymentStore } from '../../recovery-utility/main/store/deployment'; +import { useRelayUrl } from '@fireblocks/recovery-shared/hooks/useBaseWorkspace/useRelayUrl'; +import { RawSignMethod, SigningAlgorithms } from '@fireblocks/recovery-shared/reducers/rawSignReducer'; +import RawSigningForm, { SignMessageParams } from '@fireblocks/recovery-shared/components/RawSigningForm'; + +const RawSigningModal = React.lazy(() => import('../components/RawSigningModal')); + +const RawSigning: React.FC = () => { + const { accounts } = useWorkspace(); + const settings = useSettings(); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [qrData, setQrData] = useState(null); + + const { relayBaseUrl } = settings; + const deployment = DeploymentStore.get(); + const appProtocol = deployment.protocol; + if (!appProtocol?.toUpperCase()) { + console.error('app protocol error'); + } + const { getOutboundRelayUrl } = useRelayUrl('utility', relayBaseUrl); + + const generateQr = async ({ + unsignedMessage, + rawSignMethod, + selectedWallet, + inputChangeIndex, + inputAdressIndex, + derivationPath, + dpAlgorithm, + }: SignMessageParams) => { + try { + if (!getOutboundRelayUrl) { + throw new Error('getOutboundRelayUrl error'); + } + const txHashBuffer = Buffer.from(unsignedMessage, 'hex'); + const message = Uint8Array.from(txHashBuffer); + let data; + let currentDerivationPath: HDPath = { coinType: 0, account: 0, changeIndex: 0, addressIndex: 0 }; + let algorithm: SigningAlgorithms = SigningAlgorithms.ECDSA; + if (rawSignMethod === RawSignMethod.ACCOUNT) { + if (!selectedWallet || !selectedWallet.sign) { + throw new Error('selected wallet error'); + } + currentDerivationPath = { ...selectedWallet.path, changeIndex: inputChangeIndex, addressIndex: inputAdressIndex }; + algorithm = selectedWallet.algorithm as SigningAlgorithms; + } else if (rawSignMethod === RawSignMethod.DERIVATION_PATH) { + currentDerivationPath = derivationPath; + algorithm = dpAlgorithm; + } + data = getOutboundRelayUrl({ + action: 'tx/raw-sign', + algorithm: algorithm, + derivationPath: currentDerivationPath, + message: message, + }); + const baseUrl = relayBaseUrl !== '' ? relayBaseUrl : 'fireblocks-recovery:/'; + setQrData(`${baseUrl}${data}`); + setIsModalOpen(true); + } catch (e) { + console.error(e); + } + }; + + return ( + <> + + {qrData && isModalOpen && ( + + { + setIsModalOpen(false); + }} + /> + + )} + + ); +}; + +export default RawSigning; diff --git a/apps/recovery-utility/electron-builder.json b/apps/recovery-utility/electron-builder.json index 78a0be53..da4456ae 100644 --- a/apps/recovery-utility/electron-builder.json +++ b/apps/recovery-utility/electron-builder.json @@ -29,7 +29,12 @@ "afterSign": "electron-builder-notarize", "publish": null, "mac": { - "target": "zip", + "target": [ + { + "target": "zip", + "arch": ["x64", "arm64"] + } + ], "category": "public.app-category.developer-tools", "gatekeeperAssess": false, "hardenedRuntime": true, @@ -37,10 +42,20 @@ "entitlementsInherit": "./entitlements.plist" }, "win": { - "target": "nsis" + "target": [ + { + "target": "nsis", + "arch": ["x64", "ia32"] + } + ] }, "linux": { - "target": "AppImage", + "target": [ + { + "target": "AppImage", + "arch": ["x64", "arm64"] + } + ], "category": "Office" } } diff --git a/apps/recovery-utility/main/background.ts b/apps/recovery-utility/main/background.ts index 59c03296..0f4c2841 100644 --- a/apps/recovery-utility/main/background.ts +++ b/apps/recovery-utility/main/background.ts @@ -35,7 +35,7 @@ let protocol = deployment.exp : DEFAULT_PROTOCOL; const relay = app.commandLine.hasSwitch('relay') || process.env.MODE_RELAY === '1'; const util = app.commandLine.hasSwitch('util') || process.env.MODE_UTIL === '1'; -resetLogs(); +// resetLogs(); console.log(`Command line specified: relay: ${relay}, util: ${util}.`); if (relay && util) { console.error('Both relay and util flags were used, ignoring.'); @@ -79,7 +79,7 @@ const handleRelayUrl = (url = relayUrl) => { }; export async function createWindow() { - resetLogs(); + // resetLogs(); // Create the browser window. win = new BrowserWindow({ width: 845, @@ -285,5 +285,5 @@ app.on('will-quit', () => { }); const preQuit = () => { - resetLogs(); + // resetLogs(); }; diff --git a/apps/recovery-utility/main/ipc/getLogs.ts b/apps/recovery-utility/main/ipc/getLogs.ts index a36cd7f0..238f4b2a 100644 --- a/apps/recovery-utility/main/ipc/getLogs.ts +++ b/apps/recovery-utility/main/ipc/getLogs.ts @@ -2,11 +2,33 @@ import { ipcMain } from 'electron'; import path from 'path'; import fs from 'fs'; import os from 'os'; -import archiver from 'archiver'; -import stream from 'stream'; const appName = process.env.NODE_ENV === 'development' ? '@fireblocks/recovery-utility' : 'Fireblocks Recovery Utility'; +const getTodayLogFolder = () => { + const now = new Date(); + const pad = (n: number) => n.toString().padStart(2, '0'); + return [now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate())].join('_'); +}; + +const getLogBaseDir = (): string => { + const dailyFolder = getTodayLogFolder(); + switch (os.platform()) { + case 'darwin': + // macOS + return path.join(os.homedir(), 'Library', 'Logs', appName, dailyFolder); + case 'win32': + // Windows + return path.join(process.env.USERPROFILE || os.homedir(), 'AppData', 'Roaming', appName, 'logs', dailyFolder); + case 'linux': + // Linux + return path.join(os.homedir(), '.config', appName, 'logs', dailyFolder); + default: + throw new Error('Unsupported platform'); + } +}; + +// Get a log path for a given process name inside the daily folder const getLogPath = ( processName: | 'utility' @@ -20,92 +42,36 @@ const getLogPath = ( | 'main.old' | 'renderer.old' | '', -) => { +): string => { const fileName = processName === '' ? '' : `${processName}.log`; - switch (os.platform()) { - case 'darwin': - // macOS - return path.join(os.homedir(), 'Library', 'Logs', appName, fileName); - case 'win32': - // Windows - return path.join(process.env.USERPROFILE || os.homedir(), 'AppData', 'Roaming', appName, 'logs', fileName); - case 'linux': - // Linux - return path.join(os.homedir(), '.config', appName, 'logs', fileName); - default: - throw new Error('Unsupported platform'); - } + return path.join(getLogBaseDir(), fileName); }; -const createZipFromFiles = async (...filePaths: string[]) => - new Promise((resolve, reject) => { - console.info(`Collecting logs`, filePaths); - // Create an in-memory stream to collect the ZIP archive - const bufferStream = new stream.PassThrough(); - const zipData: any[] = []; - - bufferStream.on('data', (chunk) => zipData.push(chunk)); - - bufferStream.on('end', () => resolve(Buffer.concat(zipData))); - - // Set up Archiver to zip the files - const archive = archiver('zip'); - - // If there's an error, reject the promise - archive.on('error', (err) => reject(err)); - - // Pipe the ZIP archive data to the in-memory stream - archive.pipe(bufferStream); - - // Append files to the archive - filePaths.forEach((filePath) => { - if (fs.existsSync(filePath)) { - archive.append(fs.createReadStream(filePath), { name: path.basename(filePath) }); - } - }); - - // Finalize the archive (this step is important!) - archive.finalize(); - }); - ipcMain.handle('logs/get_path', async () => getLogPath('')); ipcMain.handle('logs/get', async () => { return; - createZipFromFiles( - getLogPath('utility'), - getLogPath('relay'), - getLogPath('shared'), - getLogPath('main'), - getLogPath('renderer'), - getLogPath('utility.old'), - getLogPath('relay.old'), - getLogPath('shared.old'), - getLogPath('main.old'), - getLogPath('renderer.old'), - ); }); ipcMain.handle('logs/reset', async () => resetLogs()); export const resetLogs = () => { - const logFiles = [ - getLogPath('utility'), - getLogPath('relay'), - getLogPath('shared'), - getLogPath('main'), - getLogPath('renderer'), - getLogPath('utility.old'), - getLogPath('relay.old'), - getLogPath('shared.old'), - getLogPath('main.old'), - getLogPath('renderer.old'), + const processNames = [ + 'utility', + 'relay', + 'shared', + 'main', + 'renderer', + 'utility.old', + 'relay.old', + 'shared.old', + 'main.old', + 'renderer.old', ]; - logFiles.forEach((file) => { - if (!fs.existsSync(file)) { - return; + processNames.forEach((name: any) => { + const file = getLogPath(name); + if (file && fs.existsSync(file)) { + fs.truncateSync(file, 0); } - - fs.truncateSync(file, 0); }); }; diff --git a/apps/recovery-utility/package.json b/apps/recovery-utility/package.json index e3765481..c000dc7a 100644 --- a/apps/recovery-utility/package.json +++ b/apps/recovery-utility/package.json @@ -4,7 +4,7 @@ "author": "Fireblocks (https://www.fireblocks.com)", "repository": "https://github.com/fireblocks/recovery/tree/main/apps/recovery-utility", "homepage": "https://www.fireblocks.com", - "version": "1.6.0", + "version": "1.8.1", "license": "GPL-3.0-or-later", "private": true, "main": "app/background.js", @@ -13,6 +13,7 @@ "audit": "npx electronegativity -i ./ -x LimitNavigationGlobalCheck,PermissionRequestHandlerGlobalCheck", "dev": "nextron dev", "build": "nextron build", + "build:cross": "nextron build --mac --linux --x64 --arm64", "release": "nextron build --publish always", "lint": "next lint --fix renderer", "format": "prettier --write \"**/*.{ts,tsx,md}\"" @@ -106,4 +107,4 @@ "ts-node": "^10.9.1", "typescript": "^5.1.6" } -} \ No newline at end of file +} diff --git a/apps/recovery-utility/renderer/components/Layout/index.tsx b/apps/recovery-utility/renderer/components/Layout/index.tsx index 2cc6d2fe..98e885d6 100644 --- a/apps/recovery-utility/renderer/components/Layout/index.tsx +++ b/apps/recovery-utility/renderer/components/Layout/index.tsx @@ -1,5 +1,6 @@ import { ReactNode, useEffect } from 'react'; import { Report, Restore, Verified, /* LeakAdd, */ ImportExport, ManageHistory, Settings } from '@mui/icons-material'; +import DrawIcon from '@mui/icons-material/Draw'; import { Layout as BaseLayout, LayoutProps as BaseLayoutProps, @@ -23,7 +24,7 @@ const logger = getLogger(LOGGER_NAME_UTILITY); export const Layout = ({ children }: Props) => { const { isOnline } = useConnectionTest(); - const { extendedKeys: { xpub, fpub, xprv, fprv, ncwMaster } = {} } = useWorkspace(); + const { extendedKeys: { ncwMaster } = {}, getExtendedKeysForAccountId } = useWorkspace(); const [protocol, setProtocol] = useWrappedState<'UTILITY' | 'RELAY' | null>('protocol', null); @@ -35,6 +36,7 @@ export const Layout = ({ children }: Props) => { [], ); + const { xpub, fpub, xprv, fprv } = getExtendedKeysForAccountId(0) || {}; const hasExtendedPrivateKeys = !!xprv || !!fprv; const hasExtendedPublicKeys = !!xpub || !!fpub; const hasOnlyExtendedPublicKeys = hasExtendedPublicKeys && !hasExtendedPrivateKeys; @@ -95,6 +97,12 @@ export const Layout = ({ children }: Props) => { path: '/keys', icon: KeyIcon, }, + { + label: 'Raw Signing', + path: '/raw-signing', + icon: DrawIcon, + disabled: !hasExtendedKeys, + }, { label: 'Settings', path: '/settings', diff --git a/apps/recovery-utility/renderer/components/Modals/RawSigningModal/index.tsx b/apps/recovery-utility/renderer/components/Modals/RawSigningModal/index.tsx new file mode 100644 index 00000000..8afb5f72 --- /dev/null +++ b/apps/recovery-utility/renderer/components/Modals/RawSigningModal/index.tsx @@ -0,0 +1,142 @@ +import { ReactNode, useEffect, useMemo, useState } from 'react'; +import QrScanner from 'qr-scanner'; +import { Typography, Box, Grid, CircularProgress } from '@mui/material'; +import { useSettings } from '../../../context/Settings'; +import { useWorkspace } from '../../../context/Workspace'; +import { DeploymentStore } from '../../../../main/store/deployment'; +import { LOGGER_NAME_UTILITY } from '@fireblocks/recovery-shared/constants'; +import { useRawSignMessage } from '@fireblocks/recovery-shared/hooks/useRawSignMessage'; +import { useRelayUrl } from '@fireblocks/recovery-shared/hooks/useBaseWorkspace/useRelayUrl'; +import { RawSignMethod, SigningAlgorithms } from '@fireblocks/recovery-shared/reducers/rawSignReducer'; +import { + BaseModal, + getLogger, + QrCodeScanner, + getRelayParams, + QrCode, + RelayRawSignTxRequestParams, +} from '@fireblocks/recovery-shared'; + +type RawSigningModalProps = { + assetId?: string; + accountId?: number; + open: boolean; + onClose: VoidFunction; +}; + +const logger = getLogger(LOGGER_NAME_UTILITY); + +const RawSigningModal: React.FC = (props) => { + const { open, onClose: onCloseModal } = props; + const { extendedKeys } = useWorkspace(); + + const settings = useSettings(); + const { relayBaseUrl } = settings; + + const deployment = DeploymentStore.get(); + const appProtocol = deployment.protocol; + if (!appProtocol?.toUpperCase()) { + console.error('app protocol error'); + } + const { getOutboundRelayUrl } = useRelayUrl('relay', relayBaseUrl); + + const { generateSignature, signature, setSignature, selectedAlgorithm } = useRawSignMessage(extendedKeys); + + const [isProcessing, setIsProcessing] = useState(false); + + const onDecode = async (data: QrScanner.ScanResult) => { + try { + setIsProcessing(true); + const decodedData = data.data; + const parsed = getRelayParams('relay', decodedData) as RelayRawSignTxRequestParams; + // console.log('Parsed data:', parsed.message); + const unsignedMessage = Buffer.from(parsed.message).toString('hex'); + + await generateSignature({ + unsignedMessage, + rawSignMethod: RawSignMethod.DERIVATION_PATH, + selectedWallet: null, + inputChangeIndex: parsed.derivationPath.changeIndex, + inputAdressIndex: parsed.derivationPath.addressIndex, + derivationPath: parsed.derivationPath, + dpAlgorithm: parsed.algorithm as SigningAlgorithms, + }); + setTimeout(() => { + setIsProcessing(false); + }, 1250); + } catch (error) { + console.error('Error processing QR code:', error); + setIsProcessing(false); + } + }; + + const formattedSignedMessage = useMemo(() => { + if (!signature) return null; + const data = getOutboundRelayUrl({ + action: 'tx/broadcast-raw-sign', + algorithm: selectedAlgorithm, + signature: signature, + }); + + return data; + }, [signature, selectedAlgorithm, relayBaseUrl, getOutboundRelayUrl]); + + const onClose = () => { + setSignature(null); + onCloseModal(); + }; + + return ( + + + Raw Signing + + + ) as ReactNode + } + > + {!formattedSignedMessage ? ( + + + + ) : isProcessing ? ( + + + + ) : ( + + )} + + ); +}; + +export default RawSigningModal; diff --git a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx index b596e2c1..63ea0df8 100644 --- a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx +++ b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/SignTransaction/index.tsx @@ -46,71 +46,75 @@ export const SignTransaction = ({ txId, account, asset, inboundRelayParams }: Pr const { unsignedTx } = inboundRelayParams; - const { extendedKeys, getOutboundRelayUrl } = useWorkspace(); + const { getOutboundRelayUrl, getExtendedKeysForAccountId } = useWorkspace(); const [outboundRelayUrl, setOutboundRelayUrl] = useWrappedState('outboundRelayUrl', undefined); const onApproveTransaction = async () => { - logger.info(`Trying to approve transaction.`); - - const { xprv, fprv } = extendedKeys || {}; - - if (!xprv || !fprv) { - return; + try { + logger.info(`Trying to approve transaction.`); + + const { xprv, fprv } = getExtendedKeysForAccountId(account.id)!; + + if (!xprv || !fprv) { + return; + } + + const { to, amount, misc } = unsignedTx; + + const derivation = account.wallets + .get(asset.id) + ?.derivations.get(getDerivationMapKey(inboundRelayParams?.unsignedTx.assetId, inboundRelayParams?.unsignedTx.from)); + + if (!derivation) { + throw new Error('Derivation not found'); + } + + const sanatizedDerivation = sanatize(derivation); + logger.debug(`About to sign tx to ${to}`, { sanatizedDerivation }); + + const utxos = misc + ? misc?.utxoType === BaseUTXOType + ? (misc?.utxos as StdUTXO[]) + : misc?.utxoType === SegwitUTXOType + ? (misc?.utxos as BTCSegwitUTXO[]) + : (misc?.utxos as BTCLegacyUTXO[]) + : undefined; + + const { tx } = await (derivation as SigningWallet).generateTx({ + to, + amount, + utxos, // TODO: Fix type + feeRate: misc?.feeRate, + nonce: misc?.nonce, + gasPrice: misc?.gasPrice, + memo: misc?.memo, + // blockHash: misc?.blockHash, + extraParams: misc?.extraParams, + chainId: misc?.chainId, + } as GenerateTxInput); + + logger.info({ tx }); + + setOutboundRelayUrl( + getOutboundRelayUrl({ + action: 'tx/broadcast', + accountId: account.id, + signedTx: { + id: unsignedTx.id, + assetId: unsignedTx.assetId, + path: unsignedTx.path, + from: unsignedTx.from, + to: unsignedTx.to, + amount: unsignedTx.amount, + hex: tx, + }, + endpoint: misc?.endpoint, + }), + ); + } catch (error) { + logger.error('Failed to approve transaction', error); } - - const { to, amount, misc } = unsignedTx; - - const derivation = account.wallets - .get(asset.id) - ?.derivations.get(getDerivationMapKey(inboundRelayParams?.unsignedTx.assetId, inboundRelayParams?.unsignedTx.from)); - - if (!derivation) { - throw new Error('Derivation not found'); - } - - const sanatizedDerivation = sanatize(derivation); - logger.debug(`About to sign tx to ${to}`, { sanatizedDerivation }); - - const utxos = misc - ? misc?.utxoType === BaseUTXOType - ? (misc?.utxos as StdUTXO[]) - : misc?.utxoType === SegwitUTXOType - ? (misc?.utxos as BTCSegwitUTXO[]) - : (misc?.utxos as BTCLegacyUTXO[]) - : undefined; - - const { tx } = await (derivation as SigningWallet).generateTx({ - to, - amount, - utxos, // TODO: Fix type - feeRate: misc?.feeRate, - nonce: misc?.nonce, - gasPrice: misc?.gasPrice, - memo: misc?.memo, - // blockHash: misc?.blockHash, - extraParams: misc?.extraParams, - chainId: misc?.chainId, - } as GenerateTxInput); - - logger.info({ tx }); - - setOutboundRelayUrl( - getOutboundRelayUrl({ - action: 'tx/broadcast', - accountId: account.id, - signedTx: { - id: unsignedTx.id, - assetId: unsignedTx.assetId, - path: unsignedTx.path, - from: unsignedTx.from, - to: unsignedTx.to, - amount: unsignedTx.amount, - hex: tx, - }, - endpoint: misc?.endpoint, - }), - ); }; if (inboundRelayParams.accountId !== account.id || unsignedTx.path[2] !== account.id) { diff --git a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/index.tsx b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/index.tsx index 8f3685e3..cd084f4a 100644 --- a/apps/recovery-utility/renderer/components/Modals/WithdrawModal/index.tsx +++ b/apps/recovery-utility/renderer/components/Modals/WithdrawModal/index.tsx @@ -55,8 +55,14 @@ const getHighestBalanceAssetId = (account?: VaultAccount) => { }; export const WithdrawModal = ({ assetId, accountId, open, onClose: onCloseModal }: Props) => { - const { extendedKeys, accounts, inboundRelayParams, getOutboundRelayUrl, resetInboundRelayUrl, setInboundRelayUrl } = - useWorkspace(); + const { + accounts, + inboundRelayParams, + getOutboundRelayUrl, + resetInboundRelayUrl, + setInboundRelayUrl, + getExtendedKeysForAccountId, + } = useWorkspace(); const accountsArray = useMemo(() => Array.from(accounts.values()), [accounts]); @@ -86,7 +92,7 @@ export const WithdrawModal = ({ assetId, accountId, open, onClose: onCloseModal }; const onInitiateTransaction = (data: TransactionInitInput) => { - const { xpub, fpub } = extendedKeys || {}; + const { xpub, fpub } = getExtendedKeysForAccountId(data.accountId) || {}; if (!xpub || !fpub || typeof data.accountId !== 'number' || typeof data.assetId !== 'string' || typeof data.to !== 'string') { logger.warn( diff --git a/apps/recovery-utility/renderer/components/Pages/keys.tsx b/apps/recovery-utility/renderer/components/Pages/keys.tsx new file mode 100644 index 00000000..8e72dd62 --- /dev/null +++ b/apps/recovery-utility/renderer/components/Pages/keys.tsx @@ -0,0 +1,246 @@ +import React from 'react'; +import { Box, Grid, Typography, InputAdornment, Divider, Tooltip } from '@mui/material'; +import { CheckCircle } from '@mui/icons-material'; +import { Link, monospaceFontFamily, TextField, UtilityExtendedKeys } from '@fireblocks/recovery-shared'; +import { RecoveredKey } from '@fireblocks/extended-key-recovery/src/types'; +import { sha256 } from '@noble/hashes/sha256'; + +type FormData = UtilityExtendedKeys; + +type Props = { + supportsPrivateKeys: boolean; + extendedKeys?: FormData; +}; + +export const ExtendedKeysBasePage = ({ supportsPrivateKeys, extendedKeys }: Props) => { + // const router = useRouter(); + const [hasExtendedPrivateKeys, hasExtendedPublicKeys] = Object.entries(extendedKeys || {}) + .filter(([keysetId]) => keysetId !== 'ncwMaster') + .map(([, value]) => { + const key = value as RecoveredKey; + let xpub = false; + let fpub = false; + let xprv = false; + let fprv = false; + if (key.ecdsaExists) { + xprv = key.xprv !== undefined; + xpub = key.xpub !== undefined; + } + if (key.eddsaExists) { + fprv = key.fprv !== undefined; + fpub = key.fpub !== undefined; + } + + return [xpub, xprv, fpub, fprv]; + }) + .reduce((prev, curr) => [prev[0] || curr[1] || curr[3], prev[1] || curr[0] || curr[2]], [false, false]) as [boolean, boolean]; + const showPrivate = supportsPrivateKeys && hasExtendedPrivateKeys; + + return ( + + Extended {showPrivate ? '' : 'Public '}Keys + {hasExtendedPublicKeys && ( + + Check that the recovered Fireblocks extended public keys match the keys in your Fireblocks Console Settings. + + )} + + Addresses, public keys, and private keys of all of Fireblocks wallets are derived from your workspace's extended public + and private key. + + + ECDSA extended keys ( + + xpub/xprv + + ) can be used with wallet software that imports BIP32 extended private keys. Fireblocks EdDSA extended keys ( + + fpub/fprv + + ) are Fireblocks-specific and can only be used by Fireblocks software. + + {!hasExtendedPublicKeys && ( + + {/* Set your extended public keys ( + + xpub/fpub + + ) to derive wallet addresses and public keys for recovery verification. */} + {supportsPrivateKeys && ( + <> + {' '} + In a disaster recovery scenario, you can set your private keys ( + + xprv/fprv + + ) to derive wallet private keys and sign transactions. Use your Recovery Kit to obtain + your private keys. + + )} + + )} + + {!hasExtendedPrivateKeys && !hasExtendedPublicKeys && ( + + No keys defined, please recover wallet in order to see public keys + + )} + + + {Object.entries(extendedKeys || {}) + .filter(([keysetId]) => keysetId !== 'ncwMaster') + .map(([keysetId, value], index) => { + const key = value as RecoveredKey; + if (!key.ecdsaExists && !key.eddsaExists) { + return Keyset {index + 1} is not used ; + } + return ( + `solid 1px ${_theme.palette.grey[300]}`}> + + + {Object.entries(extendedKeys || {}).filter(([id]) => id !== 'ncwMaster').length > 1 + ? `Keyset ${index + 1}` + : 'Workspace Keys'} + + + + {key.ecdsaExists && ( + <> + + + + ECDSA Extended Keys {key.ecdsaExists ? '' : ' - Disabled'} + + + + + + + + ) : null, + } + : { disabled: true, sx: { backgroundColor: '#cacaca' } })} + /> + + {hasExtendedPrivateKeys && ( + + + + )} + + + + + + + )} + {key.eddsaExists && ( + <> + + + + EdDSA Extended Keys {key.eddsaExists ? '' : ' - Disabled'} + + + + + + + + + ) : null, + } + : { disabled: true, sx: { backgroundColor: '#cacaca' } })} + /> + + {hasExtendedPrivateKeys && ( + + + + )} + + + + + + + )} + + + ); + })} + + + ); +}; diff --git a/apps/recovery-utility/renderer/components/RecoveryForm/index.tsx b/apps/recovery-utility/renderer/components/RecoveryForm/index.tsx index 97eb14b6..36a0e592 100644 --- a/apps/recovery-utility/renderer/components/RecoveryForm/index.tsx +++ b/apps/recovery-utility/renderer/components/RecoveryForm/index.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router'; -import { useState } from 'react'; +import { forwardRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; @@ -11,11 +11,15 @@ import { BaseModal, useWrappedState, useOfflineMutation, + UtilityExtendedKeys, } from '@fireblocks/recovery-shared'; -import { Checkbox, FormControlLabel, Typography, Box, Grid, Tooltip } from '@mui/material'; +import { Checkbox, FormControlLabel, Typography, Box, Grid, Tooltip, Divider, Chip } from '@mui/material'; import { readFileToBase64 } from '@fireblocks/recovery-shared/lib/readFile'; +import { WalletMaster } from '@fireblocks/extended-key-recovery'; +import { RecoveredKey } from '@fireblocks/extended-key-recovery/src/types'; import { useWorkspace } from '../../context/Workspace'; import { recoverExtendedKeys } from '../../lib/recoverExtendedKeys'; +import { TextFieldProps } from '@fireblocks/recovery-shared/components'; type FormData = z.infer; @@ -32,6 +36,9 @@ export const RecoveryForm = ({ verifyOnly }: Props) => { const [recoveryData, setRecoveryData] = useState(undefined); // Do not track, confidential data const [recoveryConfirmed, setRecoveryConfirmed] = useWrappedState('recoveryConfirmed', false); + const [jsonPassphrases, setJsonPassphrases] = useState<{ passphrase?: string; rsaKeyPassphrase?: string } | null>(null); + const [jsonUploadError, setJsonUploadError] = useState(undefined); + const { register, setValue, @@ -47,6 +54,7 @@ export const RecoveryForm = ({ verifyOnly }: Props) => { rsaKeyPassphrase: '', agpRsaKey: '', agpRsaPassphrase: '', + passphraseJson: null, }, }); @@ -62,29 +70,44 @@ export const RecoveryForm = ({ verifyOnly }: Props) => { mutationFn: async (formData: FormData) => recoverExtendedKeys({ zip: formData.backupZip, - mobilePassphrase: formData.autoGeneratedPass ? formData.agpRsaPassphrase : formData.passphrase, + mobilePassphrase: + formData.passphraseJson !== null + ? formData.passphraseJson.passphrase + : formData.autoGeneratedPass + ? formData.agpRsaPassphrase + : formData.passphrase, rsaKey: formData.rsaKey, - rsaKeyPassphrase: formData.rsaKeyPassphrase, + + rsaKeyPassphrase: formData.passphraseJson !== null ? formData.passphraseJson.rsaKeyPassphrase : formData.rsaKeyPassphrase, + autoGeneratedPassphrase: formData.autoGeneratedPass, mobileRsa: formData.agpRsaKey, recoverOnlyNCW: formData.recoverOnlyNCW, dangerouslyRecoverPrivateKeys: !verifyOnly, }), - onSuccess: async ({ xpub, fpub, xprv, fprv, ncwWalletMaster }) => { + onSuccess: async (recoveredKeys) => { setRecoveryError(undefined); if (recoverOnlyNCW) { - const ncwKey = { ncwMaster: ncwWalletMaster }; + const ncwKey = { ncwMaster: recoveredKeys.ncwWalletMaster }; setExtendedKeys(ncwKey); router.push('/ncw'); return; } - const maskedExtendedKeys = { - xpub, - fpub, - ...(verifyOnly ? {} : { xprv, fprv, ncwMaster: ncwWalletMaster }), - }; + const maskedExtendedKeys: UtilityExtendedKeys = {}; + + Object.entries(recoveredKeys).forEach(([keysetId, value]) => { + if (keysetId === 'ncwMaster' && !verifyOnly) { + maskedExtendedKeys.ncwMaster = value as WalletMaster | undefined; + return; + } + + maskedExtendedKeys[Number(keysetId)] = { + ...(value as RecoveredKey), + ...(verifyOnly ? { xprv: undefined, fprv: undefined } : {}), + }; + }); setExtendedKeys(maskedExtendedKeys); @@ -109,11 +132,54 @@ export const RecoveryForm = ({ verifyOnly }: Props) => { const onDropAGPRsaPrivateKey = async (file: File) => setValue('agpRsaKey', await readFileToBase64(file)); + const onDropPassphraseJson = async (file: File) => { + try { + const base64Content = await readFileToBase64(file); + const jsonContent = atob(base64Content.split(',')[1] || base64Content); + const parsedJson = JSON.parse(jsonContent); + + // Validate JSON structure + if (typeof parsedJson !== 'object' || parsedJson === null) { + throw new Error('Invalid JSON format'); + } + + // Create the passphrase object with only the two required fields + const passphraseObject = { + passphrase: parsedJson.passphrase || undefined, + rsaKeyPassphrase: parsedJson.rsaKeyPassphrase || undefined, + }; + + if (!passphraseObject.passphrase && !passphraseObject.rsaKeyPassphrase) { + throw new Error('JSON must contain at least one of: passphrase, rsaKeyPassphrase'); + } + + // Set the object directly to the form + setValue('passphraseJson', passphraseObject); + setJsonPassphrases(passphraseObject); + setJsonUploadError(undefined); + + // Auto-populate form fields if JSON upload is successful + if (passphraseObject.passphrase) { + setValue('passphrase', passphraseObject.passphrase); + } + if (passphraseObject.rsaKeyPassphrase) { + setValue('rsaKeyPassphrase', passphraseObject.rsaKeyPassphrase); + } + } catch (error) { + setJsonUploadError(error instanceof Error ? error.message : 'Failed to parse JSON file'); + setValue('passphraseJson', null); // Set to null instead of undefined + } + }; + const onSubmit = (formData: FormData) => verifyOnly || recoverOnlyNCW ? recoverMutation.mutate(formData) : setRecoveryData(formData); const onConfirmRecover = () => recoverMutation.mutate(recoveryData as FormData); + const CustomTextField = forwardRef((props, ref) => { + return ; + }); + return ( <> { } }} > - {verifyOnly ? 'Verify Recovery Kit' : 'Recover Private Keys'} + + {verifyOnly ? 'Verify Recovery Kit' : 'Recover Private Keys'} + {verifyOnly ? ( Use this tool to recover your Fireblocks extended public keys, then check that they match the keys in your Fireblocks @@ -176,7 +244,7 @@ export const RecoveryForm = ({ verifyOnly }: Props) => { /> ) : ( - { {...register('rsaKeyPassphrase')} /> + } @@ -230,6 +299,24 @@ export const RecoveryForm = ({ verifyOnly }: Props) => { /> + + + + + + + + + + theme.palette.error.main}> diff --git a/apps/recovery-utility/renderer/context/Workspace.tsx b/apps/recovery-utility/renderer/context/Workspace.tsx index b33dbae8..7b5084a2 100644 --- a/apps/recovery-utility/renderer/context/Workspace.tsx +++ b/apps/recovery-utility/renderer/context/Workspace.tsx @@ -10,18 +10,19 @@ import { import { getAssetConfig } from '@fireblocks/asset-config'; import { useRouter } from 'next/router'; +import { RecoveredKey } from '@fireblocks/extended-key-recovery/src/types'; +import { isTransferableToken } from '@fireblocks/asset-config/util'; +import { LOGGER_NAME_UTILITY } from '@fireblocks/recovery-shared/constants'; import { WalletClasses } from '../lib/wallets'; import packageJson from '../../package.json'; import { initIdleDetector } from '../lib/idleDetector'; import { handleRelayUrl } from '../lib/ipc/handleRelayUrl'; import { SigningWallet } from '../lib/wallets/SigningWallet'; import { useSettings } from './Settings'; -import { LOGGER_NAME_UTILITY } from '@fireblocks/recovery-shared/constants'; -import { isTransferableToken } from '@fireblocks/asset-config/util'; type DistributiveOmit = T extends any ? Omit : never; -type RelayRequestParamsInput = DistributiveOmit; +export type RelayRequestParamsInput = DistributiveOmit; type WorkspaceContext = Omit, 'setWalletBalance' | 'getOutboundRelayUrl'> & { getOutboundRelayUrl: (params: Params) => string; @@ -57,6 +58,7 @@ export const WorkspaceProvider = ({ children }: Props) => { resetInboundRelayUrl, getOutboundRelayUrl: baseGetOutboundRelayUrl, importCsv, + getExtendedKeysForAccountId, setExtendedKeys, setTransaction, addAccount, @@ -98,8 +100,12 @@ export const WorkspaceProvider = ({ children }: Props) => { }); const getOutboundRelayUrl = (params: Params) => { - const { xpub, fpub } = extendedKeys || {}; + const keys = (params.accountId !== undefined ? getExtendedKeysForAccountId(params.accountId) : {}) as { + xpub?: string; + fpub?: string; + }; + const { xpub, fpub } = keys; if (!xpub || !fpub) { throw new Error('Missing extended keys'); } @@ -153,6 +159,7 @@ export const WorkspaceProvider = ({ children }: Props) => { getOutboundRelayUrl, importCsv, setExtendedKeys, + getExtendedKeysForAccountId, setTransaction, addAccount, addWallet, diff --git a/apps/recovery-utility/renderer/lib/recoverExtendedKeys.ts b/apps/recovery-utility/renderer/lib/recoverExtendedKeys.ts index 41e103ee..81be3c4f 100644 --- a/apps/recovery-utility/renderer/lib/recoverExtendedKeys.ts +++ b/apps/recovery-utility/renderer/lib/recoverExtendedKeys.ts @@ -13,13 +13,6 @@ export type Args = { dangerouslyRecoverPrivateKeys?: boolean; }; -export type ExtendedKeys = { - xpub: string; - fpub: string; - xprv?: string; - fprv?: string; -}; - export const recoverExtendedKeys = async (args: Args) => { const logger = getLogger(LOGGER_NAME_UTILITY); let config; diff --git a/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts b/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts index 5d4b5a67..70cf7aea 100644 --- a/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts +++ b/apps/recovery-utility/renderer/lib/wallets/ERC20/index.ts @@ -1,10 +1,15 @@ import { ethers, Wallet } from 'ethers'; -import { EVMWallet as EVMBase } from '@fireblocks/wallet-derivation'; +import { EVMWallet as EVMBase, Input } from '@fireblocks/wallet-derivation'; import { TxPayload, GenerateTxInput } from '../types'; import { SigningWallet } from '../SigningWallet'; import { erc20Abi } from './erc20.abi'; export class ERC20 extends EVMBase implements SigningWallet { + constructor(input: Input, chainId: number = 60) { + // If chainId is not provided or is 0, default to 60 + super(input, chainId && chainId !== 0 ? chainId : 60); + } + public async generateTx({ to, extraParams, nonce, chainId }: GenerateTxInput): Promise { if (!this.privateKey) { throw new Error('No private key found'); @@ -14,8 +19,11 @@ export class ERC20 extends EVMBase implements SigningWallet { const tokenAddress = extraParams?.get('tokenAddress'); - const maxPriorityFeePerGas = (BigInt(extraParams?.get('priorityFee')) * 115n) / 100n; //increase priority fee by 15% to increase chance of tx to be included in next block const maxFeePerGas = BigInt(extraParams?.get('maxFee')); + const maxPriorityFeePerGas = + (BigInt(extraParams?.get('priorityFee')) * 115n) / 100n >= maxFeePerGas + ? maxFeePerGas + : (BigInt(extraParams?.get('priorityFee')) * 115n) / 100n; //increase priority fee by 15% to increase chance of tx to be included in next block const gasLimit = BigInt(extraParams?.get('gasLimit')); const gasPrice = BigInt(extraParams?.get('gasPrice')); diff --git a/apps/recovery-utility/renderer/lib/wallets/EVM/index.ts b/apps/recovery-utility/renderer/lib/wallets/EVM/index.ts index 9cc30b4e..698d0869 100644 --- a/apps/recovery-utility/renderer/lib/wallets/EVM/index.ts +++ b/apps/recovery-utility/renderer/lib/wallets/EVM/index.ts @@ -6,8 +6,9 @@ import { TxPayload, GenerateTxInput } from '../types'; import { SigningWallet } from '../SigningWallet'; export class EVM extends EVMBase implements SigningWallet { - constructor(input: Input, chainId?: number) { - super(input); + constructor(input: Input, chainId: number = 60) { + // If chainId is not provided or is 0, default to 60 + super(input, chainId && chainId !== 0 ? chainId : 60); } public async generateTx({ diff --git a/apps/recovery-utility/renderer/lib/wallets/FLR/index.ts b/apps/recovery-utility/renderer/lib/wallets/FLR/index.ts new file mode 100644 index 00000000..00cba689 --- /dev/null +++ b/apps/recovery-utility/renderer/lib/wallets/FLR/index.ts @@ -0,0 +1,8 @@ +import { Input } from '@fireblocks/wallet-derivation'; +import { EVM } from '../EVM'; + +export class FLR extends EVM { + constructor(input: Input) { + super(input, 554); + } +} diff --git a/apps/recovery-utility/renderer/lib/wallets/SPL/index.ts b/apps/recovery-utility/renderer/lib/wallets/SPL/index.ts new file mode 100644 index 00000000..d6bfb4a5 --- /dev/null +++ b/apps/recovery-utility/renderer/lib/wallets/SPL/index.ts @@ -0,0 +1,71 @@ +import * as web3 from '@solana/web3.js'; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createTransferInstruction, + getAssociatedTokenAddress, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; +import { Solana as SolanaBase, Input } from '@fireblocks/wallet-derivation'; +import { SigningWallet } from '../SigningWallet'; +import { TxPayload, GenerateTxInput } from '../types'; + +export class SPL extends SolanaBase implements SigningWallet { + private readonly connection: web3.Connection; + + constructor(input: Input) { + super(input); + const endpoint = input.isTestnet ? web3.clusterApiUrl('devnet') : web3.clusterApiUrl('mainnet-beta'); + this.connection = new web3.Connection(endpoint, 'confirmed'); + } + + public async generateTx({ to, amount, extraParams }: GenerateTxInput): Promise { + const mint = extraParams?.get('mint'); + const createATA = extraParams?.get('createATA') === 'true'; + const decimals = extraParams?.get('decimals') || 6; + + if (!mint) throw new Error('Missing "mint" in extraParams'); + + const recentBlockhash = extraParams?.get(this.KEY_RECENT_BLOCKHASH); + if (!recentBlockhash) throw new Error('Missing "recentBlockhash" in extraParams'); + + if (!this.privateKey) throw new Error('No private key found'); + + const mintPubkey = new web3.PublicKey(mint); + const from = this.web3PubKey; + const toPubkey = new web3.PublicKey(to); + + const fromTokenAccount = await getAssociatedTokenAddress(mintPubkey, from); + const toTokenAccount = await getAssociatedTokenAddress(mintPubkey, toPubkey); + + const amountRaw = Math.round(amount * Math.pow(10, decimals)); + + const tx = new web3.Transaction(); + tx.feePayer = from; + tx.recentBlockhash = recentBlockhash; + if (createATA) { + tx.add( + createAssociatedTokenAccountInstruction( + from, // payer + toTokenAccount, + toPubkey, // owner + mintPubkey, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, + ), + ); + } + + tx.add(createTransferInstruction(fromTokenAccount, toTokenAccount, from, amountRaw)); + + this.utilityLogger.logSigningTx('SPL', tx); + + const serializedMsg = tx.serializeMessage(); + const signature = await this.sign(Uint8Array.from(serializedMsg)); + tx.addSignature(from, Buffer.from(signature)); + + return { + tx: tx.serialize().toString('hex'), + }; + } +} diff --git a/apps/recovery-utility/renderer/lib/wallets/index.ts b/apps/recovery-utility/renderer/lib/wallets/index.ts index f56c5252..2c03f36c 100644 --- a/apps/recovery-utility/renderer/lib/wallets/index.ts +++ b/apps/recovery-utility/renderer/lib/wallets/index.ts @@ -7,6 +7,7 @@ import { EOS } from './EOS'; import { BitcoinCash } from './BCH'; import { EVM } from './EVM'; import { Solana } from './SOL'; +import { SPL } from './SPL'; import { Tron } from './TRON'; import { Luna } from './LUNA'; import { Cardano } from './ADA'; @@ -24,6 +25,8 @@ import { Ton } from './TON'; import { Jetton } from './Jetton'; import { ERC20 } from './ERC20'; import { TRC20 } from './TRC20'; +import { getAllSpls } from '@fireblocks/asset-config/assets'; +import { FLR } from './FLR'; const fillEVMs = () => { const evms = Object.keys(assets).reduce( @@ -60,6 +63,17 @@ const fillTRC20s = () => { return trc20s; }; +const fillSpls = () => { + const splsList = getAllSpls(); + const spls: { [key: string]: any } = {}; + for (const spl of splsList) { + if (spl !== 'SOL') { + spls[spl] = SPL; + } + } + return spls; +}; + export { SigningWallet as BaseWallet } from './SigningWallet'; export const WalletClasses = { @@ -103,9 +117,13 @@ export const WalletClasses = { ...fillEVMs(), ...fillTRC20s(), + FLR: FLR, + // EDDSA SOL: Solana, SOL_TEST: Solana, + ...fillSpls(), + XLM: Stellar, XLM_TEST: Stellar, NEAR: Near, diff --git a/apps/recovery-utility/renderer/pages/accounts/vault/[accountId]/index.tsx b/apps/recovery-utility/renderer/pages/accounts/vault/[accountId]/index.tsx index f59b1715..c3b4331f 100644 --- a/apps/recovery-utility/renderer/pages/accounts/vault/[accountId]/index.tsx +++ b/apps/recovery-utility/renderer/pages/accounts/vault/[accountId]/index.tsx @@ -3,9 +3,10 @@ import { useWorkspace } from '../../../../context/Workspace'; import { WithdrawModal } from '../../../../components/Modals/WithdrawModal'; const VaultAccount = () => { - const { extendedKeys, account, addWallet } = useWorkspace(); + const { account, addWallet, getExtendedKeysForAccountId } = useWorkspace(); - const hasExtendedPrivateKeys = !!extendedKeys?.xprv && !!extendedKeys?.fprv; + const { xprv, fprv } = getExtendedKeysForAccountId(account?.id || 0) || {}; + const hasExtendedPrivateKeys = !!xprv && !!fprv; return ( { - const { extendedKeys, accounts, addAccount } = useWorkspace(); + const { extendedKeys, accounts, addAccount, getExtendedKeysForAccountId } = useWorkspace(); - const hasExtendedPrivateKeys = !!extendedKeys?.xprv && !!extendedKeys?.fprv; + const { xprv, fprv, xpub, fpub } = getExtendedKeysForAccountId(0) || {}; + const hasExtendedPrivateKeys = !!xprv && !!fprv; + + const totalKeysetCount = Object.keys(extendedKeys || {}).filter( + (keysetId) => !['xpub', 'xprv', 'fprv', 'fpub', 'ncwMaster'].includes(keysetId), + ).length; + + const totalUnmappedKeysets = Object.entries(extendedKeys || {}).filter(([keysetId, value]) => { + if (['xpub', 'xprv', 'fprv', 'fpub', 'ncwMaster'].includes(keysetId)) { + return false; + } + let res = false; + const key = value as RecoveredKey; + if (key.eddsaExists) { + res = res || key.eddsaMinAccount === -1; + } + if (key.ecdsaExists) { + res = res || key.ecdsaMinAccount === -1; + } + return res; + }).length; + + const hasMultipleKeysets = totalKeysetCount > 1 && totalUnmappedKeysets > 0; + + const totalEcdsaCount = Object.entries(extendedKeys || {}).filter( + ([keysetId, value]) => + !['xpub', 'xprv', 'fprv', 'fpub', 'ncwMaster'].includes(keysetId) && (value as RecoveredKey).ecdsaExists, + ).length; + const totalEddsaCount = Object.entries(extendedKeys || {}).filter( + ([keysetId, value]) => + !['xpub', 'xprv', 'fprv', 'fpub', 'ncwMaster'].includes(keysetId) && (value as RecoveredKey).eddsaExists, + ).length; + + const firstUnmappedECDSAIndex = Object.entries(extendedKeys || {}).findLast( + ([keysetId, value]) => + !['xpub', 'xprv', 'fprv', 'fpub', 'ncwMaster'].includes(keysetId) && + (value as RecoveredKey).ecdsaExists && + (value as RecoveredKey).ecdsaMinAccount !== -1, + ); + + const firstUnmappedEddsaIndex = Object.entries(extendedKeys || {}).findLast( + ([keysetId, value]) => + !['xpub', 'xprv', 'fprv', 'fpub', 'ncwMaster'].includes(keysetId) && + (value as RecoveredKey).eddsaExists && + (value as RecoveredKey).eddsaMinAccount !== -1, + ); return ( ); diff --git a/apps/recovery-utility/renderer/pages/csv.tsx b/apps/recovery-utility/renderer/pages/csv.tsx index cd63fb88..ceee3348 100644 --- a/apps/recovery-utility/renderer/pages/csv.tsx +++ b/apps/recovery-utility/renderer/pages/csv.tsx @@ -2,9 +2,10 @@ import { ImportExportBasePage } from '@fireblocks/recovery-shared'; import { useWorkspace } from '../context/Workspace'; const ImportExport = () => { - const { extendedKeys, accounts, importCsv } = useWorkspace(); + const { accounts, importCsv, getExtendedKeysForAccountId } = useWorkspace(); - return ; + // Since we expect there to be at least 1 keyset which is associated to account 0, this is a good substitution + return ; }; export default ImportExport; diff --git a/apps/recovery-utility/renderer/pages/index.tsx b/apps/recovery-utility/renderer/pages/index.tsx index 9eeb7298..8499ac5b 100644 --- a/apps/recovery-utility/renderer/pages/index.tsx +++ b/apps/recovery-utility/renderer/pages/index.tsx @@ -2,9 +2,9 @@ import { ReactNode, useEffect } from 'react'; import { Grid, Typography, SxProps, CircularProgress, Box } from '@mui/material'; import { ImportExport, ManageHistory, Restore, Verified, Warning, LeakAdd } from '@mui/icons-material'; import { Button, NextLinkComposed, KeyIcon, VaultAccountIcon, getLogger, useWrappedState } from '@fireblocks/recovery-shared'; +import { LOGGER_NAME_UTILITY } from '@fireblocks/recovery-shared/constants'; import { useWorkspace } from '../context/Workspace'; import { getDeployment, useDeployment } from '../lib/ipc'; -import { LOGGER_NAME_UTILITY } from '@fireblocks/recovery-shared/constants'; import { resetDeployment } from '../lib/ipc/useDeployment'; import { resetLogs } from '../lib/ipc/getLogs'; @@ -32,7 +32,7 @@ type BoxButtonProps = { onClick?: VoidFunction; }; -const BoxButton = ({ icon: Icon, title, description, color = 'primary', href, disabled, onClick }: BoxButtonProps) => ( +export const BoxButton = ({ icon: Icon, title, description, color = 'primary', href, disabled, onClick }: BoxButtonProps) => (