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/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 00000000..22d5c44c
--- /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/fireblocks/mirror-test.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
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/components/WithdrawModal/CreateTransaction/index.tsx b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
index 3aa15302..47829f59 100644
--- a/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/CreateTransaction/index.tsx
@@ -60,7 +60,7 @@ const getRPCKey = (
return baseAsset;
};
-export const getAssetURL = (
+export const getAssetURLAndApiKey = (
assetId: string,
RPCs: Record<
string,
@@ -69,16 +69,18 @@ export const getAssetURL = (
allowedEmptyValue: boolean;
name: string;
url?: string | null | undefined;
+ requiresApiKey?: boolean;
+ apiKey?: string | null;
}
>,
-): string | null | undefined => {
+): { url: string; requiresApiKey?: boolean; apiKey?: string | null } | null | undefined => {
const rpcKey = getRPCKey(assetId, RPCs);
if (rpcKey === undefined) {
return undefined;
}
const assetRPCData = RPCs[rpcKey];
- const { url } = assetRPCData;
+ const { url, requiresApiKey, apiKey } = assetRPCData;
logger.info(`RPC URL for ${rpcKey} is ${assetRPCData.enabled ? 'enabled' : 'disabled'} and is ${url}`);
if (assetRPCData.allowedEmptyValue && (url === null || url === undefined)) {
@@ -88,7 +90,7 @@ export const getAssetURL = (
// If not enabled we shouldn't be getting a request to the relay for it regardless.
return undefined;
}
- return url;
+ return { url: url as string, requiresApiKey, apiKey };
};
const getWallet = (accounts: Map>, accountId?: number, assetId?: string) => {
@@ -164,7 +166,12 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
enabled: !!derivation,
queryFn: async () => {
logger.debug(`Querying prepare transaction ${toAddress}`);
- const rpcUrl = getAssetURL(derivation?.assetId ?? '', RPCs);
+ const data = getAssetURLAndApiKey(derivation?.assetId ?? '', RPCs);
+ if (!data) {
+ throw new Error(`No RPC data for: ${derivation?.assetId}`);
+ }
+ const { url: rpcUrl, requiresApiKey, apiKey } = data;
+
if (rpcUrl === undefined) {
logger.error(`Unknown URL for ${derivation?.assetId ?? ''}`);
throw new Error(`No RPC Url for: ${derivation?.assetId}`);
@@ -185,6 +192,13 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
(derivation as ERC20).setToAddress(toAddress);
}
if (rpcUrl !== null) derivation!.setRPCUrl(rpcUrl); // this must remain the last method called on derivation for ERC20 support
+ if (requiresApiKey) {
+ if (!apiKey || apiKey === '') {
+ throw new Error(`RPC for ${derivation?.assetId} requires an API key. Please set one in the Settings page.`);
+ } else {
+ derivation!.setAPIKey(apiKey);
+ }
+ }
return await derivation!.prepare?.(toAddress, values.memo);
},
@@ -328,6 +342,12 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
{prepareQuery.error || typeof prepareQuery.data?.balance === 'undefined'
? 'Could not get balance'
: `${prepareQuery.data.balance} ${asset.id}`}
+ {prepareQuery.error && (
+ <>
+
+ {prepareQuery.error.message}
+ >
+ )}
)}
diff --git a/apps/recovery-relay/components/WithdrawModal/index.tsx b/apps/recovery-relay/components/WithdrawModal/index.tsx
index c7f80332..c6b9d5ec 100644
--- a/apps/recovery-relay/components/WithdrawModal/index.tsx
+++ b/apps/recovery-relay/components/WithdrawModal/index.tsx
@@ -17,7 +17,7 @@ import { sanatize } from '@fireblocks/recovery-shared/lib/sanatize';
import { getAssetConfig, isTransferableToken } from '@fireblocks/asset-config/util';
import { SignOrBroadcastTransaction } from '@fireblocks/recovery-shared/components';
import { useWorkspace } from '../../context/Workspace';
-import { CreateTransaction, getAssetURL } from './CreateTransaction';
+import { CreateTransaction, getAssetURLAndApiKey } from './CreateTransaction';
import { LateInitConnectedWallet } from '../../lib/wallets/LateInitConnectedWallet';
import { useSettings } from '../../context/Settings';
import { ERC20 } from '../../lib/wallets/ERC20';
@@ -87,7 +87,11 @@ export const WithdrawModal = () => {
if (isTransferableToken(assetId) && derivation instanceof ERC20) {
(derivation as ERC20).setNativeAsset(getAssetConfig(assetId)!.nativeAsset);
}
- const rpcUrl = getAssetURL(derivation?.assetId ?? '', RPCs);
+ const data = getAssetURLAndApiKey(derivation?.assetId ?? '', RPCs);
+ if (!data) {
+ throw new Error(`No RPC data for: ${derivation?.assetId}`);
+ }
+ const { url: rpcUrl, requiresApiKey, apiKey } = data;
if (rpcUrl === undefined) {
throw new Error(`No RPC URL for asset ${derivation?.assetId}`);
} else if (rpcUrl === null) {
@@ -98,12 +102,20 @@ export const WithdrawModal = () => {
derivation?.setRPCUrl(rpcUrl);
}
+ if (requiresApiKey) {
+ if (!apiKey || apiKey === '') {
+ throw new Error(`RPC for ${derivation?.assetId} requires an API key`);
+ } else {
+ derivation!.setAPIKey(apiKey);
+ }
+ }
+
const signedTxHex = params.signedTx.hex;
const cleanDerivation = derivation ? sanatize(derivation) : undefined;
logger.info('Derivation and signed transaction hash:', { cleanDerivation, signedTxHex });
try {
- const newTxHash = await derivation?.broadcastTx(signedTxHex);
+ const newTxHash = await derivation?.broadcastTx(signedTxHex, logger, derivation?.assetId);
setTxHash(newTxHash);
logger.info({ newTxHash });
diff --git a/apps/recovery-relay/context/Settings.tsx b/apps/recovery-relay/context/Settings.tsx
index 1e4b6e50..ac578b6a 100644
--- a/apps/recovery-relay/context/Settings.tsx
+++ b/apps/recovery-relay/context/Settings.tsx
@@ -37,8 +37,14 @@ export const SettingsProvider = ({ children }: Props) => {
}, []);
const saveSettings = async (data: RelaySettingsInput) => {
- logger.info('Storing new settings', data);
- window.localStorage.setItem('settings', JSON.stringify(data));
+ try {
+ logger.info('Storing new settings', data);
+ window.localStorage.setItem('settings', JSON.stringify(data));
+ setSettings(data);
+ } catch (error) {
+ logger.error('Failed to save settings', error);
+ throw error;
+ }
};
// eslint-disable-next-line react/jsx-no-constructed-context-values
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..bf1d621a 100644
--- a/apps/recovery-relay/lib/defaultRPCs.ts
+++ b/apps/recovery-relay/lib/defaultRPCs.ts
@@ -2,9 +2,12 @@ export const defaultRPCs: Record<
string,
{
url: string | undefined | null;
+ broadcastUrl?: string;
name: string;
enabled: boolean;
allowedEmptyValue: boolean;
+ requiresApiKey?: boolean;
+ apiKey?: string | null;
}
> = {
ALGO: { url: null, allowedEmptyValue: true, enabled: true, name: 'Algorand' },
@@ -16,12 +19,23 @@ export const defaultRPCs: Record<
enabled: true,
allowedEmptyValue: false,
},
- BTC: { url: 'https://api.blockchair.com/bitcoin', name: 'Bitcoin', 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,
+ requiresApiKey: true,
+ apiKey: null,
+ },
BTC_TEST: {
url: 'https://api.blockchair.com/bitcoin/testnet',
+ broadcastUrl: 'https://blockstream.info/testnet/api/tx',
name: 'Bitcoin Testnet',
enabled: true,
allowedEmptyValue: false,
+ requiresApiKey: true,
+ apiKey: null,
},
BCH: { url: 'https://rest.bch.actorforth.org/v2', name: 'Bitcoin Cash', enabled: true, allowedEmptyValue: false },
BCH_TEST: { url: undefined, name: 'Bitcoin Cash Testnet', enabled: false, allowedEmptyValue: false },
@@ -32,16 +46,44 @@ export const defaultRPCs: Record<
enabled: true,
allowedEmptyValue: false,
},
- DOGE: { url: 'https://api.blockchair.com/dogecoin', name: 'Dogecoin', enabled: true, allowedEmptyValue: false },
+ DOGE: {
+ url: 'https://api.blockchair.com/dogecoin',
+ name: 'Dogecoin',
+ enabled: true,
+ allowedEmptyValue: false,
+ requiresApiKey: true,
+ apiKey: null,
+ },
DOGE_TEST: { url: undefined, name: 'Dogecoin Testnet', enabled: false, allowedEmptyValue: false },
DOT: { url: 'wss://rpc.polkadot.io', name: 'Polkadot', enabled: true, allowedEmptyValue: false },
WND: { url: 'wss://westend-rpc.polkadot.io', name: 'Westend (Polkadot Testnet)', enabled: true, allowedEmptyValue: false },
KSM: { url: 'wss://kusama-rpc.polkadot.io', name: 'Kusama', enabled: true, allowedEmptyValue: false },
- LTC: { url: 'https://api.blockchair.com/litecoin', name: 'Litecoin', enabled: true, allowedEmptyValue: false },
+ LTC: {
+ url: 'https://api.blockchair.com/litecoin',
+ name: 'Litecoin',
+ enabled: true,
+ allowedEmptyValue: false,
+ requiresApiKey: true,
+ apiKey: null,
+ },
LTC_TEST: { url: undefined, name: 'Litecoin Testnet', enabled: false, allowedEmptyValue: false },
- ZEC: { url: 'https://api.blockchair.com/zcash', name: 'Litecoin', enabled: true, allowedEmptyValue: false },
+ ZEC: {
+ url: 'https://api.blockchair.com/zcash',
+ name: 'Litecoin',
+ enabled: true,
+ allowedEmptyValue: false,
+ requiresApiKey: true,
+ apiKey: null,
+ },
ZEC_TEST: { url: undefined, name: 'ZCash Testnet', enabled: false, allowedEmptyValue: false },
- DASH: { url: 'https://api.blockchair.com/Dash', name: 'Dash', enabled: true, allowedEmptyValue: false },
+ DASH: {
+ url: 'https://api.blockchair.com/Dash',
+ name: 'Dash',
+ enabled: true,
+ allowedEmptyValue: false,
+ requiresApiKey: true,
+ apiKey: null,
+ },
FTM_FANTOM: { url: 'https://rpcapi.fantom.network', name: 'Fantom Testnet', enabled: true, allowedEmptyValue: false },
ETC: { url: 'https://geth-de.etc-network.info', name: 'Ethereum Classic', enabled: true, allowedEmptyValue: false },
ETC_TEST: {
diff --git a/apps/recovery-relay/lib/wallets/BTCBased/BTC.ts b/apps/recovery-relay/lib/wallets/BTCBased/BTC.ts
index 48ca7fc9..472d4f1d 100644
--- a/apps/recovery-relay/lib/wallets/BTCBased/BTC.ts
+++ b/apps/recovery-relay/lib/wallets/BTCBased/BTC.ts
@@ -11,102 +11,108 @@ export class Bitcoin extends BaseBTC implements ConnectedWallet {
private static readonly satsPerBtc = 100000000;
public rpcURL: string | undefined;
+ public apiKey: string | null = null;
private utils: BTCRelayWalletUtils | undefined;
constructor(input: Input) {
super(input);
- // Legacy requires a custom site
if (this.isLegacy) {
- // When calling any custom function provided as part of the relay wallet utils
- // we bind it to `this` from the BTCRelayWallet class, thus every internal reference to this
- // within the custom functions must be considered as a call to the BTCRelayWalletUtils and not
- // overarching wallet type (BSV in this case)
-
- this.utils = new (class {
- btcWalletUtils;
-
- constructor(baseUrl: string) {
- this.btcWalletUtils = new StandardBTCRelayWalletUtils(baseUrl);
- }
-
- async getAddressUTXOs(address: string): Promise {
- const utxoSummary = await this.btcWalletUtils.requestJson.bind(this)(`/address/${address}/utxo`);
- return utxoSummary.map((utxo) => ({
- transaction_hash: utxo.txid,
- value: utxo.value,
- index: utxo.vout,
- block_id: utxo.status.block_height ?? -1,
- }));
- }
-
- async getAddressBalance(address: string): Promise {
- const { chain_stats: chainStats } = await this.btcWalletUtils.requestJson.bind(this)(
- `/address/${address}`,
- );
- return chainStats.funded_txo_sum - chainStats.spent_txo_sum;
- }
-
- async getFeeRate(): Promise {
- const feeEstimate = await this.btcWalletUtils.requestJson.bind(this)<{ [key: string]: number }>('/fee-estimates');
- const feeRate = feeEstimate['1'];
- return feeRate;
- }
-
- async getLegacyFullUTXO(utxo: StandardUTXO): Promise {
- const { transaction_hash: hash, index } = utxo;
- const rawTxRes = await this.btcWalletUtils.request(`/tx/${hash}/raw`);
- const rawTx = await rawTxRes.arrayBuffer();
- const nonWitnessUtxo = Buffer.from(rawTx);
-
- return {
- hash,
- index,
- nonWitnessUtxo,
- confirmed: true,
- value: BTCRelayWallet._satsToBtc(utxo.value),
- };
- }
-
- async getSegwitUTXO(utxo: StandardUTXO): Promise {
- const { transaction_hash: hash, index } = utxo;
- const fullUtxo = await this.btcWalletUtils.requestJson.bind(this)(`/tx/${hash}`);
- const { scriptpubkey, value } = fullUtxo.vout[index];
-
- return {
- hash,
- index,
- witnessUtxo: { script: scriptpubkey, value },
- confirmed: true,
- value: BTCRelayWallet._satsToBtc(value),
- };
- }
+ this.initializeUtils();
+ }
+ }
- async broadcastTx(txHex: string, logger: CustomElectronLogger): Promise {
- try {
- const txBroadcastRes = await this.btcWalletUtils.request('/tx', {
- method: 'POST',
- body: txHex,
- });
-
- const txHash = await txBroadcastRes.text();
- if (txHash.length !== 64) {
- throw new Error(txHash);
- }
- return txHash;
- } catch (e) {
- logger.error(`BTC: Error broadcasting tx: ${JSON.stringify(e, null, 2)}`);
- throw e;
+ private initializeUtils() {
+ const btcWalletUtils = new StandardBTCRelayWalletUtils(this.rpcURL!, undefined, false, this.apiKey);
+ btcWalletUtils.setAPIKey(this.apiKey);
+ this.utils = {
+ getAddressUTXOs: async (address: string): Promise => {
+ const utxoSummary = await btcWalletUtils.requestJson(
+ `/address/${address}/utxo${this.apiKey ? `?key=${this.apiKey}` : ''}`,
+ );
+ return utxoSummary.map((utxo) => ({
+ transaction_hash: utxo.txid,
+ value: utxo.value,
+ index: utxo.vout,
+ block_id: utxo.status.block_height ?? -1,
+ }));
+ },
+
+ getAddressBalance: async (address: string): Promise => {
+ const { chain_stats: chainStats } = await btcWalletUtils.requestJson(
+ `/address/${address}${this.apiKey ? `?key=${this.apiKey}` : ''}`,
+ );
+ return chainStats.funded_txo_sum - chainStats.spent_txo_sum;
+ },
+
+ getFeeRate: async (): Promise => {
+ const feeEstimate = await btcWalletUtils.requestJson<{ [key: string]: number }>(
+ `/fee-estimates${this.apiKey ? `?key=${this.apiKey}` : ''}`,
+ );
+ return feeEstimate['1'];
+ },
+
+ getLegacyFullUTXO: async (utxo: StandardUTXO): Promise => {
+ const { transaction_hash: hash, index } = utxo;
+ const rawTxRes = await btcWalletUtils.request(`/tx/${hash}/raw${this.apiKey ? `?key=${this.apiKey}` : ''}`);
+ const rawTx = await rawTxRes.arrayBuffer();
+ const nonWitnessUtxo = Buffer.from(rawTx);
+
+ return {
+ hash,
+ index,
+ nonWitnessUtxo,
+ confirmed: true,
+ value: BTCRelayWallet._satsToBtc(utxo.value),
+ };
+ },
+
+ getSegwitUTXO: async (utxo: StandardUTXO): Promise => {
+ const { transaction_hash: hash, index } = utxo;
+ const fullUtxo = await btcWalletUtils.requestJson(`/tx/${hash}${this.apiKey ? `?key=${this.apiKey}` : ''}`);
+ const { scriptpubkey, value } = fullUtxo.vout[index];
+
+ return {
+ hash,
+ index,
+ witnessUtxo: { script: scriptpubkey, value },
+ confirmed: true,
+ value: BTCRelayWallet._satsToBtc(value),
+ };
+ },
+
+ broadcastTx: async (txHex: string, logger: CustomElectronLogger, assetId?: string | undefined): Promise => {
+ try {
+ const txBroadcastRes = await btcWalletUtils.request(`/tx${this.apiKey ? `?key=${this.apiKey}` : ''}`, {
+ method: 'POST',
+ body: txHex,
+ });
+
+ const txHash = await txBroadcastRes.text();
+ if (txHash.length !== 64) {
+ throw new Error(txHash);
}
+ return txHash;
+ } catch (e) {
+ logger.error(`BTC: Error broadcasting tx: ${JSON.stringify(e, null, 2)}`);
+ throw e;
}
- })(this.isTestnet ? 'https://blockstream.info/testnet/api' : 'https://blockstream.info/api') as BTCRelayWalletUtils;
- }
+ },
+ } as BTCRelayWalletUtils;
}
public setRPCUrl(url: string): void {
this.rpcURL = url;
}
+ public setAPIKey(apiKey: string | null): void {
+ this.apiKey = apiKey;
+ // Reinitialize utils with new API key
+ if (this.isLegacy) {
+ this.initializeUtils();
+ }
+ }
+
public async getBalance(): Promise {
return BTCRelayWallet.prototype.getBalance.bind(this)();
}
@@ -115,7 +121,7 @@ export class Bitcoin extends BaseBTC implements ConnectedWallet {
return BTCRelayWallet.prototype.prepare.bind(this)();
}
- public async broadcastTx(txHex: string): Promise {
- return BTCRelayWallet.prototype.broadcastTx.bind(this)(txHex);
+ public async broadcastTx(txHex: string, logger: CustomElectronLogger, assetId?: string | undefined): Promise {
+ return BTCRelayWallet.prototype.broadcastTx.bind(this)(txHex, logger, assetId);
}
}
diff --git a/apps/recovery-relay/lib/wallets/BTCBased/BTCRelayWallet.ts b/apps/recovery-relay/lib/wallets/BTCBased/BTCRelayWallet.ts
index 03034d96..f7d87a37 100644
--- a/apps/recovery-relay/lib/wallets/BTCBased/BTCRelayWallet.ts
+++ b/apps/recovery-relay/lib/wallets/BTCBased/BTCRelayWallet.ts
@@ -1,3 +1,4 @@
+import { CustomElectronLogger } from '@fireblocks/recovery-shared/lib/getLogger';
import { AccountData, LegacyUTXOType, SegwitUTXOType } from '../types';
import { BTCRelayWalletUtils, StandardBTCRelayWalletUtils } from './BTCRelayWalletUtils';
@@ -9,9 +10,9 @@ export class BTCRelayWallet {
}
public async getBalance(): Promise {
- // @ts-ignore
- const utils = (this.utils as BTCRelayWalletUtils) || new StandardBTCRelayWalletUtils(this.rpcURL);
- // @ts-ignore
+ const utils =
+ // @ts-ignore
+ (this.utils as BTCRelayWalletUtils) || new StandardBTCRelayWalletUtils(this.rpcURL, undefined, false, this.apiKey); // @ts-ignore
const balance = await utils.getAddressBalance(this.address);
const btcBalance = BTCRelayWallet._satsToBtc(balance);
return btcBalance;
@@ -19,10 +20,10 @@ export class BTCRelayWallet {
public async prepare(): Promise {
// @ts-ignore
- const { isLegacy, relayLogger: logger, rpcURL, address } = this;
+ const { isLegacy, relayLogger: logger, rpcURL, address, apiKey } = this;
// @ts-ignore
- const utils = (this.utils as BTCRelayWalletUtils) || new StandardBTCRelayWalletUtils(rpcURL);
+ const utils = (this.utils as BTCRelayWalletUtils) || new StandardBTCRelayWalletUtils(rpcURL, undefined, false, apiKey);
const balance = await BTCRelayWallet.prototype.getBalance.bind(this)();
if (balance === 0) {
@@ -52,17 +53,17 @@ export class BTCRelayWallet {
return preparedData as AccountData;
}
- public async broadcastTx(txHex: string): Promise {
+ public async broadcastTx(txHex: string, logger?: CustomElectronLogger, assetId?: string | undefined): Promise {
// BTC Tx are automatically signed and resulting hex is signed, so no need to do anything special.
// const tx = Psbt.fromHex(txHex, { network: this.network });
// @ts-ignore
- const { relayLogger: logger, rpcURL } = this;
+ const { relayLogger: relayLogger, rpcURL, apiKey } = this;
// @ts-ignore
- const utils = (this.utils as BTCRelayWalletUtils) || new StandardBTCRelayWalletUtils(rpcURL);
+ const utils = (this.utils as BTCRelayWalletUtils) || new StandardBTCRelayWalletUtils(rpcURL, undefined, false, apiKey);
// @ts-ignore
- return utils.broadcastTx(txHex, logger);
+ return utils.broadcastTx(txHex, relayLogger, assetId);
}
}
diff --git a/apps/recovery-relay/lib/wallets/BTCBased/BTCRelayWalletUtils.ts b/apps/recovery-relay/lib/wallets/BTCBased/BTCRelayWalletUtils.ts
index 44a5216a..b825645b 100644
--- a/apps/recovery-relay/lib/wallets/BTCBased/BTCRelayWalletUtils.ts
+++ b/apps/recovery-relay/lib/wallets/BTCBased/BTCRelayWalletUtils.ts
@@ -3,6 +3,7 @@ import { ipcRenderer } from 'electron';
import { BTCLegacyUTXO, BTCSegwitUTXO } from '../types';
import { BTCRelayWallet } from './BTCRelayWallet';
import { StandardAddressSummary, StandardBlockchainStats, StandardFullUTXO, StandardUTXO } from './types';
+import { defaultRPCs } from '../../defaultRPCs';
export interface BTCRelayWalletUtils {
getAddressUTXOs: (address: string) => Promise;
@@ -10,11 +11,24 @@ export interface BTCRelayWalletUtils {
getFeeRate: () => Promise;
getLegacyFullUTXO?: (utxo: StandardUTXO) => Promise;
getSegwitUTXO: (utxo: StandardUTXO) => Promise;
- broadcastTx?: (txHex: string, logger: CustomElectronLogger) => Promise;
+ broadcastTx?: (txHex: string, logger: CustomElectronLogger, assetId?: string | undefined) => Promise;
}
export class StandardBTCRelayWalletUtils implements BTCRelayWalletUtils {
- constructor(private baseUrl: string, private overrides?: BTCRelayWalletUtils, private fetchOnMain = false) {}
+ constructor(
+ private baseUrl: string,
+ private overrides?: BTCRelayWalletUtils,
+ private fetchOnMain = false,
+ private apiKey: string | null = null,
+ ) {}
+
+ public setAPIKey(apiKey: string | null): void {
+ this.apiKey = apiKey;
+ }
+
+ public getApiKey(): string | null {
+ return this.apiKey;
+ }
async request(path: string, init?: RequestInit) {
let res: Response;
@@ -37,7 +51,9 @@ export class StandardBTCRelayWalletUtils implements BTCRelayWalletUtils {
if (this.overrides && this.overrides.getAddressUTXOs) {
return this.overrides.getAddressUTXOs(address);
}
- const addressSummary = await this.requestJson(`/dashboards/address/${address}?limit=0,10000`);
+ const addressSummary = await this.requestJson(
+ `/dashboards/address/${address}?limit=0,10000&key=${this.apiKey}`,
+ );
return addressSummary.data[address].utxo;
}
@@ -46,8 +62,9 @@ export class StandardBTCRelayWalletUtils implements BTCRelayWalletUtils {
if (this.overrides && this.overrides.getAddressBalance) {
return this.overrides.getAddressBalance(address);
}
- const { balance } = (await this.requestJson(`/dashboards/address/${address}?limit=0,0`)).data[address]
- .address;
+ const { balance } = (
+ await this.requestJson(`/dashboards/address/${address}?limit=0,0&key=${this.apiKey}`)
+ ).data[address].address;
return balance;
}
@@ -56,7 +73,7 @@ export class StandardBTCRelayWalletUtils implements BTCRelayWalletUtils {
if (this.overrides && this.overrides.getFeeRate) {
return this.overrides.getFeeRate();
}
- const bcStats = await this.requestJson('/stats');
+ const bcStats = await this.requestJson(`/stats?key=${this.apiKey}`);
const feeRate = bcStats.data.suggested_transaction_fee_per_byte_sat;
return feeRate;
}
@@ -68,7 +85,7 @@ export class StandardBTCRelayWalletUtils implements BTCRelayWalletUtils {
// }
const { transaction_hash: hash, index } = utxo;
- const rawTxRes = await this.request(`/tx/${hash}/raw`);
+ const rawTxRes = await this.request(`/tx/${hash}/raw?key=${this.apiKey}`);
const rawTx = await rawTxRes.arrayBuffer();
const nonWitnessUtxo = Buffer.from(rawTx);
@@ -87,7 +104,7 @@ export class StandardBTCRelayWalletUtils implements BTCRelayWalletUtils {
}
const { transaction_hash: hash, index } = utxo;
- const fullUtxo = await this.requestJson(`/dashboards/transaction/${hash}`);
+ const fullUtxo = await this.requestJson(`/dashboards/transaction/${hash}?key=${this.apiKey}`);
if (fullUtxo.data[hash].transaction.block_id === -1) {
return undefined;
}
@@ -102,17 +119,41 @@ export class StandardBTCRelayWalletUtils implements BTCRelayWalletUtils {
};
}
- async broadcastTx(txHex: string, logger: CustomElectronLogger): Promise {
+ async broadcastTx(txHex: string, logger: CustomElectronLogger, assetId?: string | undefined): Promise {
if (this.overrides && this.overrides.broadcastTx) {
return this.overrides.broadcastTx(txHex, logger);
}
+ // Use Blockstream for Bitcoin Testnet as Blockhiar returns 500
+ if (assetId === 'BTC_TEST') {
+ const broadcastUrl = defaultRPCs.BTC_TEST.broadcastUrl;
+
+ if (!broadcastUrl) {
+ throw new Error('No broadcast URL for BTC Testnet');
+ }
+
+ logger.info('Broadcasting via Blockstream testnet...');
+ const res = await fetch(broadcastUrl, {
+ method: 'POST',
+ headers: { 'Content-Type': 'text/plain' },
+ body: txHex,
+ });
+
+ const text = await res.text();
+ if (!res.ok) {
+ throw new Error(`Blockstream broadcast failed: ${res.status} ${text}`);
+ }
+
+ logger.info(`Broadcast successful via Blockstream: ${text}`);
+ return text.trim();
+ }
+
try {
const txBroadcastRes: {
data?: { transaction_hash: string; [key: string]: any };
context: { code: number; error: string; [key: string]: any };
} = await (
- await this.request('/push/transaction', {
+ await this.request(`/push/transaction?key=${this.apiKey}`, {
method: 'POST',
body: `data=${txHex}`,
headers: [['Content-Type', 'application/x-www-form-urlencoded']],
diff --git a/apps/recovery-relay/lib/wallets/BTCBased/DASH.ts b/apps/recovery-relay/lib/wallets/BTCBased/DASH.ts
index 041e8bf9..4ec57326 100644
--- a/apps/recovery-relay/lib/wallets/BTCBased/DASH.ts
+++ b/apps/recovery-relay/lib/wallets/BTCBased/DASH.ts
@@ -5,6 +5,7 @@ import { BTCRelayWallet } from './BTCRelayWallet';
export class DASH extends BaseDASH implements ConnectedWallet {
public rpcURL: string | undefined;
+ public apiKey: string | null = null;
constructor(input: Input) {
super(input);
@@ -18,6 +19,10 @@ export class DASH extends BaseDASH implements ConnectedWallet {
this.rpcURL = url;
}
+ public setAPIKey(apiKey: string | null): void {
+ this.apiKey = apiKey;
+ }
+
public async getBalance(): Promise {
return BTCRelayWallet.prototype.getBalance.bind(this)();
}
diff --git a/apps/recovery-relay/lib/wallets/BTCBased/DOGE.ts b/apps/recovery-relay/lib/wallets/BTCBased/DOGE.ts
index 27203eb1..2bfc996c 100644
--- a/apps/recovery-relay/lib/wallets/BTCBased/DOGE.ts
+++ b/apps/recovery-relay/lib/wallets/BTCBased/DOGE.ts
@@ -7,11 +7,16 @@ export class DOGE extends BaseDOGE implements ConnectedWallet {
private static readonly satsPerBtc = 100000000;
public rpcURL: string | undefined;
+ public apiKey: string | null = null;
public setRPCUrl(url: string): void {
this.rpcURL = url;
}
+ public setAPIKey(apiKey: string | null): void {
+ this.apiKey = apiKey;
+ }
+
public async prepare(): Promise {
return BTCRelayWallet.prototype.prepare.bind(this)();
}
diff --git a/apps/recovery-relay/lib/wallets/BTCBased/LTC.ts b/apps/recovery-relay/lib/wallets/BTCBased/LTC.ts
index e4fad6e2..cc658bb7 100644
--- a/apps/recovery-relay/lib/wallets/BTCBased/LTC.ts
+++ b/apps/recovery-relay/lib/wallets/BTCBased/LTC.ts
@@ -7,11 +7,16 @@ export class LTC extends BaseLTC implements ConnectedWallet {
private static readonly satsPerBtc = 100000000;
public rpcURL: string | undefined;
+ public apiKey: string | null = null;
public setRPCUrl(url: string): void {
this.rpcURL = url;
}
+ public setAPIKey(apiKey: string | null): void {
+ this.apiKey = apiKey;
+ }
+
public async getBalance(): Promise {
return BTCRelayWallet.prototype.getBalance.bind(this)();
}
diff --git a/apps/recovery-relay/lib/wallets/BTCBased/ZEC.ts b/apps/recovery-relay/lib/wallets/BTCBased/ZEC.ts
index 35a6706a..bad1869e 100644
--- a/apps/recovery-relay/lib/wallets/BTCBased/ZEC.ts
+++ b/apps/recovery-relay/lib/wallets/BTCBased/ZEC.ts
@@ -8,11 +8,16 @@ export class ZEC extends BaseZEC implements ConnectedWallet {
private static readonly satsPerBtc = 100000000;
public rpcURL: string | undefined;
+ public apiKey: string | null = null;
public setRPCUrl(url: string): void {
this.rpcURL = url;
}
+ public setAPIKey(apiKey: string | null): void {
+ this.apiKey = apiKey;
+ }
+
public async prepare(): Promise {
const currentBlock = await this._getCurrentBlockHeight();
const extraParams = new Map();
@@ -30,7 +35,7 @@ export class ZEC extends BaseZEC implements ConnectedWallet {
}
private async _getCurrentBlockHeight() {
- const utils = new StandardBTCRelayWalletUtils(this.rpcURL!);
+ const utils = new StandardBTCRelayWalletUtils(this.rpcURL!, undefined, false, this.apiKey);
const stats = await utils.requestJson<{
data: {
blocks: number;
diff --git a/apps/recovery-relay/lib/wallets/ConnectedWallet.ts b/apps/recovery-relay/lib/wallets/ConnectedWallet.ts
index a895425c..194f2702 100644
--- a/apps/recovery-relay/lib/wallets/ConnectedWallet.ts
+++ b/apps/recovery-relay/lib/wallets/ConnectedWallet.ts
@@ -1,5 +1,6 @@
import { BaseWallet } from '@fireblocks/wallet-derivation';
import { AccountData } from './types';
+import { CustomElectronLogger } from '@fireblocks/recovery-shared/lib/getLogger';
export abstract class ConnectedWallet extends BaseWallet {
public rpcURL: string | undefined;
@@ -8,7 +9,7 @@ export abstract class ConnectedWallet extends BaseWallet {
public abstract prepare(to?: string, memo?: string): Promise;
- public abstract broadcastTx(txHex: string): Promise;
+ public abstract broadcastTx(txHex: string, logger?: CustomElectronLogger, assetId?: string | undefined): Promise;
public abstract setRPCUrl(url: string): void;
}
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..baa82060 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.8.2",
"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-relay/pages/settings.tsx b/apps/recovery-relay/pages/settings.tsx
index a1970640..9c895f63 100644
--- a/apps/recovery-relay/pages/settings.tsx
+++ b/apps/recovery-relay/pages/settings.tsx
@@ -5,7 +5,7 @@ import { Button, settingsInput, getLogger, DataGrid, useWrappedState } from '@fi
import { Alert, Box, Grid, Typography } from '@mui/material';
import { LOGGER_NAME_RELAY } from '@fireblocks/recovery-shared/constants';
import { shell, ipcRenderer } from 'electron';
-import { BaseModal, EditIcon, TextField } from '@fireblocks/recovery-shared/components';
+import { BaseModal, EditIcon, KeyIcon, TextField } from '@fireblocks/recovery-shared/components';
import { GridActionsCellItem, GridCellParams, GridColDef, GridToolbar } from '@mui/x-data-grid';
import React, { useMemo, useState } from 'react';
import { useSettings } from '../context/Settings';
@@ -20,6 +20,8 @@ type RowData = {
name: string;
enabled: boolean;
allowedEmptyValue: boolean;
+ requiresApiKey?: boolean;
+ apiKey?: string | null;
};
const logger = getLogger(LOGGER_NAME_RELAY);
@@ -27,6 +29,10 @@ const logger = getLogger(LOGGER_NAME_RELAY);
const Settings = () => {
const { saveSettings, RPCs: currentRPCs } = useSettings();
const [editModalData, setEditModalData] = useWrappedState('settingsRpc-editModalData', undefined);
+ const [addAPIKeyModalData, setAddAPIKeyModalData] = useWrappedState(
+ 'settingsRpc-addAPIKeyModalData',
+ undefined,
+ );
const [rpcs, setRPCs] = useState<
Record<
string,
@@ -35,6 +41,8 @@ const Settings = () => {
enabled: boolean;
allowedEmptyValue: boolean;
url?: string | null | undefined;
+ requiresApiKey?: boolean;
+ apiKey?: string | null;
}
>
>(currentRPCs);
@@ -42,14 +50,27 @@ const Settings = () => {
shell.openPath(await ipcRenderer.invoke('logs/get_path'));
};
+ // Form for URL editing
const {
- register,
- handleSubmit,
- formState: { errors },
+ register: registerUrl,
+ handleSubmit: handleSubmitUrl,
+ formState: { errors: urlErrors },
} = useForm({
resolver: zodResolver(settingsInput.RELAY),
defaultValues: {
- RPCs: defaultRPCs,
+ RPCs: currentRPCs,
+ },
+ });
+
+ // Form for API Key editing
+ const {
+ register: registerApiKey,
+ handleSubmit: handleSubmitApiKey,
+ formState: { errors: apiKeyErrors },
+ } = useForm({
+ resolver: zodResolver(settingsInput.RELAY),
+ defaultValues: {
+ RPCs: currentRPCs,
},
});
@@ -57,7 +78,7 @@ const Settings = () => {
() =>
Object.keys(rpcs)
.map((chain) => {
- const { url, name, enabled, allowedEmptyValue } = rpcs[chain];
+ const { url, name, enabled, allowedEmptyValue, requiresApiKey, apiKey } = rpcs[chain];
if (name === undefined) {
throw new Error(`Blockchain without name`);
}
@@ -68,6 +89,8 @@ const Settings = () => {
url: enabled ? url : 'No support for this network',
enabled,
allowedEmptyValue,
+ requiresApiKey,
+ apiKey,
},
];
})
@@ -83,12 +106,23 @@ const Settings = () => {
console.log(rowInfo);
setEditModalData(rowInfo);
};
+
+ const handleAddAPIKeyModal = (rowInfo: RowData | undefined) => {
+ console.log(rowInfo);
+ setAddAPIKeyModalData(rowInfo);
+ };
+
const handleCloseEditModal = () => {
setEditModalData(undefined);
};
+ const handleCloseAddAPIKeyModal = () => {
+ setAddAPIKeyModalData(undefined);
+ };
+
const onSubmitModal = async (formData: FormData) => {
handleCloseEditModal();
+ handleCloseAddAPIKeyModal();
setRPCs(formData.RPCs);
await saveSettings(formData);
};
@@ -99,9 +133,22 @@ const Settings = () => {
return `RPCs.${blockchain}.url`;
};
+ const getApiKeyReactHookDotNotationPropertyAccess = (): `RPCs` | `RPCs.${string}` => {
+ if (addAPIKeyModalData === undefined) return 'RPCs';
+ const blockchain = addAPIKeyModalData.id;
+ return `RPCs.${blockchain}.apiKey`;
+ };
+
+ const getApiKeyErrorMessage = (): string | undefined => {
+ if (addAPIKeyModalData === undefined) return undefined;
+ const blockchain = addAPIKeyModalData.id;
+ const apiKeyError = apiKeyErrors.RPCs?.[blockchain]?.apiKey;
+ return apiKeyError?.message;
+ };
+
const getErrorMessage = (): string | undefined => {
const dotNotationPropertyAccess = getReactHookDotNotationPropertyAccess().replace('.url', '.root');
- let currentProperty: any = errors;
+ let currentProperty: any = urlErrors;
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const property of dotNotationPropertyAccess.split('.')) {
currentProperty = currentProperty[property];
@@ -190,6 +237,23 @@ const Settings = () => {
/>,
],
},
+ {
+ field: 'api-key',
+ headerName: '',
+ type: 'actions',
+ editable: false,
+ sortable: false,
+ getActions: (params) => [
+ }
+ disabled={!params.row.enabled || !params.row.requiresApiKey}
+ label='Edit Api Key'
+ onClick={() => handleAddAPIKeyModal(params.row as RowData)}
+ />,
+ ],
+ },
] as GridColDef[]
}
initialState={{
@@ -215,7 +279,7 @@ const Settings = () => {
>
}
// eslint-disable-next-line react/no-unstable-nested-components
- WrapperComponent={(props) => }
+ WrapperComponent={(props) => }
>
@@ -230,12 +294,39 @@ const Settings = () => {
label={`${editModalData?.bc} RPC URL`}
defaultValue={editModalData?.url}
error={getErrorMessage()}
- {...register(getReactHookDotNotationPropertyAccess())}
+ {...registerUrl(getReactHookDotNotationPropertyAccess())}
/>
+
+
+
+
+ >
+ }
+ WrapperComponent={(props) => }
+ >
+
+
+
+
+
+
);
};
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..f7786e81 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.2",
"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]}`}>
+
+
+ {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) => (
-
+
Recovery Utility{utilityPackage.version}
-
+
Wallet Derivation{walletDerivationPackage.version}
-
+
Extended Key Recovery
{extendedKeyRecoveryPackage.version}
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',
});
diff --git a/packages/asset-config/assets.ts b/packages/asset-config/assets.ts
index c91eb384..f79927a1 100644
--- a/packages/asset-config/assets.ts
+++ b/packages/asset-config/assets.ts
@@ -1,8 +1,36 @@
import { globalAssets } from './data/globalAssets';
import { nativeAssetPatches } from './config/patches';
import { AssetsConfig } from './types';
+import { splAssets } from '../../apps/recovery-relay/lib/wallets/SPL/solana_assets';
+import { unsupportedSplAssets } from '../../apps/recovery-relay/lib/wallets/SPL/unsupported_solana_assets';
-export const assets = globalAssets.reduce(
+// Transform SPL assets to match globalAssets structure
+function transformSplAssets(splAssetsList: any[]) {
+ return splAssetsList.map((asset) => ({
+ id: asset.legacyId,
+ symbol: asset.onchain.symbol,
+ name: asset.onchain.name,
+ decimals: asset.onchain.decimals ?? 6,
+ address: asset.onchain.address,
+ nativeAsset: 'SOL',
+ protocol: 'SOL',
+ testnet: false,
+ }));
+}
+
+const allSplAssets = [...splAssets, ...unsupportedSplAssets];
+const transformedSplAssets = transformSplAssets(allSplAssets);
+const allAssets = [...globalAssets, ...transformedSplAssets];
+
+// Remove duplicates by ID (prioritize globalAssets over SPL assets)
+const uniqueAssets = allAssets.reduce((acc, asset) => {
+ if (!acc.find((existing) => existing.id === asset.id)) {
+ acc.push(asset);
+ }
+ return acc;
+}, [] as typeof allAssets);
+
+export const assets = uniqueAssets.reduce(
(acc, asset) => ({
...acc,
[asset.id]: {
@@ -13,6 +41,17 @@ export const assets = globalAssets.reduce(
{},
);
+export function getAllSpls(): string[] {
+ const splTokens = [];
+ const allSplAssets = [...splAssets, ...unsupportedSplAssets];
+ for (const asset of allSplAssets) {
+ if (asset.blockchainId === 'dd754e7a-f4d5-466b-a847-45f31d4e5403' && asset.onchain.address) {
+ splTokens.push(asset.legacyId);
+ }
+ }
+ return splTokens;
+}
+
export function getAllJettons(): string[] {
const jettons = [];
for (const asset of globalAssets) {
diff --git a/packages/asset-config/config/patches.ts b/packages/asset-config/config/patches.ts
index 04d6fcf9..d95d1712 100644
--- a/packages/asset-config/config/patches.ts
+++ b/packages/asset-config/config/patches.ts
@@ -103,6 +103,7 @@ export const nativeAssetPatches: NativeAssetPatches = {
},
AVAX: evm('cchain.explorer.avax.network', 'https://api.avax.network/ext/bc/C/rpc'),
AVAXTEST: evm('subnets-test.avax.network/c-chain', 'https://api.avax-test.network/ext/bc/C/rpc'),
+ FLR: evm('https://flarescan.com', 'https://flare-api.flare.network/ext/C/rpc'),
BCH: btc('blockexplorer.one/bitcoin-cash/mainnet', false),
BCH_TEST: btc('blockexplorer.one/bitcoin-cash/testnet', false, true, false),
BNB_BSC: evm('bscscan.com'),
diff --git a/packages/asset-config/data/globalAssets.ts b/packages/asset-config/data/globalAssets.ts
index 35e415cb..fb5e6453 100644
--- a/packages/asset-config/data/globalAssets.ts
+++ b/packages/asset-config/data/globalAssets.ts
@@ -1964,6 +1964,38 @@ export const globalAssets = [
protocol: 'ETH',
testnet: false,
},
+
+ {
+ id: 'IFNA',
+ symbol: 'IFNA',
+ name: 'Innofactory AG Shares',
+ decimals: 0,
+ address: '0x366012e00ffcFD782bE19a9ea5b47AD640998895',
+ nativeAsset: 'ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
+
+ {
+ id: 'REALU',
+ symbol: 'REALU',
+ name: 'RealUnit Shares',
+ decimals: 0,
+ address: '0x553C7f9C780316FC1D34b8e14ac2465Ab22a090B',
+ nativeAsset: 'ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
+ {
+ id: 'IFPS',
+ symbol: 'IFPS',
+ name: 'Innofactory AG Participation Certificates',
+ decimals: 0,
+ address: '0xA891de65bDd776168AD155943B6d731bdAaCb901',
+ nativeAsset: 'ETH',
+ protocol: 'ETH',
+ testnet: false,
+ },
{
id: 'BSV',
symbol: 'BSV',
diff --git a/packages/asset-config/index.ts b/packages/asset-config/index.ts
index dd9dcef8..e89ab033 100644
--- a/packages/asset-config/index.ts
+++ b/packages/asset-config/index.ts
@@ -1,10 +1,10 @@
import { orderId, orderAssetById } from './config/sort';
import { isNativeAssetId, isDerivableAssetId, isTestnetAsset } from './util';
-import { assets, getAllJettons, getAllERC20s, getAllTRC20s } from './assets';
+import { assets, getAllJettons, getAllERC20s, getAllTRC20s, getAllSpls } from './assets';
export { getAssetConfig, getNativeAssetConfig, getDerivableAssetConfig, isExplorerUrl } from './util';
-export { assets, getAllJettons, getAllERC20s, getAllTRC20s };
+export { assets, getAllJettons, getAllERC20s, getAllTRC20s, getAllSpls };
export type * from './types';
diff --git a/packages/e2e-tests/utils.ts b/packages/e2e-tests/utils.ts
index 0e8501fa..4dbe3531 100644
--- a/packages/e2e-tests/utils.ts
+++ b/packages/e2e-tests/utils.ts
@@ -118,7 +118,7 @@ const testFailed = async (onApp: 'utility' | 'relay', assetId: string) => {
try {
fs.mkdirSync('./failed-tests/', { recursive: true });
} catch {}
- fs.writeFileSync(`./failed-tests/${assetId}-${Date.now()}.zip`, zipBuffer);
+ fs.writeFileSync(`./failed-tests/${assetId}-${Date.now()}.zip`, new Uint8Array(zipBuffer));
throw new UtilityUnknownError(`Failed to transfer ${assetId} due to console error`);
};
diff --git a/packages/extended-key-recovery/src/algorithms.ts b/packages/extended-key-recovery/src/algorithms.ts
new file mode 100644
index 00000000..e00a81fc
--- /dev/null
+++ b/packages/extended-key-recovery/src/algorithms.ts
@@ -0,0 +1,27 @@
+export type Algorithm = 'MPC_ECDSA_SECP256K1' | 'MPC_EDDSA_ED25519' | 'MPC_CMP_ECDSA_SECP256K1' | 'MPC_CMP_EDDSA_ED25519';
+
+export const ECDSA_CMP: Algorithm = 'MPC_CMP_ECDSA_SECP256K1';
+export const ECDSA: Algorithm = 'MPC_ECDSA_SECP256K1';
+export const EDDSA_CMP: Algorithm = 'MPC_CMP_EDDSA_ED25519';
+export const EDDSA: Algorithm = 'MPC_EDDSA_ED25519';
+
+export class InvalidAlgorithmError extends Error {
+ constructor(algo: string) {
+ super(`Unknown algorithm: ${algo}`);
+ }
+}
+
+export const getAlgorithmFromString = (algo: string): Algorithm => {
+ switch (algo) {
+ case 'MPC_CMP_ECDSA_SECP256K1':
+ return ECDSA_CMP;
+ case 'MPC_ECDSA_SECP256K1':
+ return ECDSA;
+ case 'MPC_CMP_EDDSA_ED25519':
+ return EDDSA_CMP;
+ case 'MPC_EDDSA_ED25519':
+ return EDDSA;
+ default:
+ throw new InvalidAlgorithmError(algo);
+ }
+};
diff --git a/packages/extended-key-recovery/src/decrypt.ts b/packages/extended-key-recovery/src/decrypt.ts
index fdd1fada..04a5b0ed 100644
--- a/packages/extended-key-recovery/src/decrypt.ts
+++ b/packages/extended-key-recovery/src/decrypt.ts
@@ -15,10 +15,14 @@ const _unpad = (text: Buffer, k = 16): Buffer => {
export const decryptMobilePrivateKey = (pass: string, userId: string, encryptedKey: Buffer): Buffer => {
const wrappedKey = forge.pkcs5.pbkdf2(pass, userId, 10000, 32, 'sha1');
- const decipher = forge.cipher.createDecipher('AES-CBC', forge.util.createBuffer(Buffer.from(wrappedKey, 'binary')));
- decipher.start({ iv: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] });
- decipher.update(forge.util.createBuffer(encryptedKey));
+ const decipher = forge.cipher.createDecipher(
+ 'AES-CBC',
+ forge.util.createBuffer(new Uint8Array(Buffer.from(wrappedKey, 'binary'))),
+ );
+ decipher.start({ iv: new Array(16).fill(0) });
+ decipher.update(forge.util.createBuffer(new Uint8Array(encryptedKey)));
decipher.finish();
+
const decryptedBytes = Buffer.from(decipher.output.toHex(), 'hex');
if (decryptedBytes.length === 48) {
const unpaddedData = _unpad(decryptedBytes);
diff --git a/packages/extended-key-recovery/src/metadata.ts b/packages/extended-key-recovery/src/metadata.ts
index e5e7b3b6..13ee6eb9 100644
--- a/packages/extended-key-recovery/src/metadata.ts
+++ b/packages/extended-key-recovery/src/metadata.ts
@@ -1,35 +1,122 @@
/* eslint-disable guard-for-in */
/* eslint-disable no-restricted-syntax */
-import { NCWWalletMasterMetadata, RecoveryPackageMetadata, SigningKeyMetadata, UnknownChainCodeError } from './types';
+import { Algorithm, getAlgorithmFromString } from './algorithms';
+import { NCWWalletMasterMetadata, SigningKeyMetadata, UnknownChainCodeError } from './types';
-export const parseMetadataFile = (metadataFile: string): RecoveryPackageMetadata => {
- const metadataObj = JSON.parse(metadataFile);
- const defaultChainCode = Buffer.from(metadataObj.chainCode, 'hex');
+export type RecoveryPackageMetadata = {
+ signingKeys: SigningKeysMap;
+ ncwWalletMasters?: NCWWalletMastersMap;
+ keysetThMapping: KeysetThresholdMap;
+ maxKeysetId: number;
+};
- const signingKeys: { [key: string]: SigningKeyMetadata } = {};
- const masterKeys: { [key: string]: NCWWalletMasterMetadata } = {};
+export type SigningKeysMap = { [key: string]: SigningKeyMetadata };
+export type NCWWalletMastersMap = { [key: string]: NCWWalletMasterMetadata };
+export type KeysetThresholdMap = { [key in Algorithm]?: { [keyset: number]: number } };
+
+type RecoveryKitMetadata = {
+ chainCode: string;
+ tenantId: string;
+ keys?: RecoveryKitKeyShardDefinitions; // Old format only has keyId
+ processId?: string;
+ keysetMapping?: KeysetThresholdDefinition[];
+
+ // Old format:
+ keyId?: string;
+ publicKey?: string;
+
+ // NCW
+ masterKeys?: NCWMasterKeyDefinitions;
+};
+
+type NCWMasterKeyDefinitions = {
+ [keyId: string]: NCWWalletMasterMetadata;
+};
- let keysInKit: { [key: string]: SigningKeyMetadata } = {};
- if (!metadataObj.keys) {
- keysInKit = {
- [metadataObj.keyId]: {
- publicKey: metadataObj.publicKey,
+type RecoveryKitKeyShardDefinitions = {
+ [keyId: string]: RecoveryKitKeyShardsEntry;
+};
+
+type RecoveryKitKeyShardsEntry = {
+ publicKey: string;
+ keysetId?: number;
+ chainCode?: string;
+ algo: string;
+ cosigners: RecoveryKitKeyCosignerEntry[];
+};
+
+type RecoveryKitKeyCosignerEntry = {
+ id: string;
+ cosignerType: string;
+};
+
+type KeysetThresholdDefinition = {
+ keysetId: number;
+ algo: string;
+ minAccount: number;
+};
+
+const getKeysFromKit = (metadata: RecoveryKitMetadata): { [key: string]: SigningKeyMetadata } => {
+ let keys: { [key: string]: SigningKeyMetadata } = {};
+ if (!metadata.keys) {
+ keys = {
+ [metadata.keyId!]: {
+ publicKey: metadata.publicKey!,
algo: 'MPC_ECDSA_SECP256K1',
+ keysetId: 1,
},
};
} else {
- for (const key in metadataObj.keys) {
- keysInKit[key] = metadataObj.keys[key];
+ for (const [key, entry] of Object.entries(metadata.keys)) {
+ keys[key] = {
+ publicKey: entry.publicKey,
+ chainCode: Uint8Array.from(Buffer.from(entry.chainCode ?? metadata.chainCode, 'hex')),
+ algo: getAlgorithmFromString(entry.algo),
+ keysetId: entry.keysetId ?? 1,
+ };
}
}
+ return keys;
+};
+
+const getKeysetThresholdMap = (metadata: RecoveryKitMetadata): KeysetThresholdMap => {
+ const keysetMap: KeysetThresholdMap = {};
+
+ if (metadata.keysetMapping) {
+ for (const thMap of metadata.keysetMapping) {
+ const { algo: algoStr, keysetId, minAccount } = thMap;
+ const algo = getAlgorithmFromString(algoStr);
+ if (!keysetMap[algo]) {
+ keysetMap[algo] = {};
+ }
+ keysetMap[algo]![keysetId] = minAccount;
+ }
+ } else {
+ // Set a default - all keys from keyset 1
+ keysetMap.MPC_ECDSA_SECP256K1 = { 1: 0 };
+ keysetMap.MPC_EDDSA_ED25519 = { 1: 0 };
+ }
+
+ return keysetMap;
+};
+
+export const parseMetadataFile = (metadataFile: string): RecoveryPackageMetadata => {
+ const metadata = JSON.parse(metadataFile) as RecoveryKitMetadata;
+ const defaultChainCode = Buffer.from(metadata.chainCode, 'hex');
+
+ const signingKeys: { [key: string]: SigningKeyMetadata } = {};
+ const masterKeys: { [key: string]: NCWWalletMasterMetadata } = {};
+
+ const keysInKit = getKeysFromKit(metadata);
+ let maxKeysetId = 1;
+
for (const key in keysInKit) {
- const metadataPublicKey = keysInKit[key].publicKey;
- const metadataAlgo = keysInKit[key].algo;
+ const { publicKey: metadataPublicKey, algo: metadataAlgo, keysetId } = keysInKit[key];
let metadataChainCode: Buffer;
if (keysInKit[key].chainCode) {
if (typeof keysInKit[key].chainCode === 'string') {
- metadataChainCode = Buffer.from(keysInKit[key].chainCode! as string, 'hex');
+ metadataChainCode = Buffer.from(keysInKit[key].chainCode! as unknown as string, 'hex');
} else {
metadataChainCode = keysInKit[key].chainCode! as unknown as Buffer;
}
@@ -45,17 +132,25 @@ export const parseMetadataFile = (metadataFile: string): RecoveryPackageMetadata
publicKey: metadataPublicKey,
chainCode: metadataChainCode,
algo: metadataAlgo,
+ keysetId,
};
+
+ if (keysetId > maxKeysetId) {
+ maxKeysetId = keysetId;
+ }
}
- if (!metadataObj.masterKeys) {
+ const keysetThMapping = getKeysetThresholdMap(metadata);
+
+ if (!metadata.masterKeys) {
return {
signingKeys,
- ncwWalletMasters: {},
+ keysetThMapping,
+ maxKeysetId,
};
}
- const masterKeysInKit: { [key: string]: NCWWalletMasterMetadata } = metadataObj.masterKeys;
+ const masterKeysInKit: NCWWalletMastersMap = metadata.masterKeys;
for (const key in masterKeysInKit) {
const keyMetadata = masterKeysInKit[key];
const keyType = keyMetadata.type;
@@ -84,5 +179,7 @@ export const parseMetadataFile = (metadataFile: string): RecoveryPackageMetadata
return {
signingKeys,
ncwWalletMasters: masterKeys,
+ keysetThMapping,
+ maxKeysetId,
};
};
diff --git a/packages/extended-key-recovery/src/ncw.ts b/packages/extended-key-recovery/src/ncw.ts
index 3fc1cd38..9b8d7274 100644
--- a/packages/extended-key-recovery/src/ncw.ts
+++ b/packages/extended-key-recovery/src/ncw.ts
@@ -1,3 +1,6 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+/* eslint-disable no-continue */
+/* eslint-disable no-restricted-syntax */
import forge from 'node-forge';
import { IZipEntry } from 'adm-zip';
import { _uuidToBuffer } from './players';
@@ -66,8 +69,8 @@ export const recoverNCWMaster = (
}
return {
- assetSeed: (assetSeed as Buffer).toString('hex'),
- walletSeed: (walletSeed as Buffer).toString('hex'),
+ assetSeed: (assetSeed as unknown as Buffer).toString('hex'),
+ walletSeed: (walletSeed as unknown as Buffer).toString('hex'),
masterKeyForCosigner: cosignerKeys,
};
};
diff --git a/packages/extended-key-recovery/src/reconstructKeys.ts b/packages/extended-key-recovery/src/reconstructKeys.ts
index 7a5ff9ab..92a41719 100644
--- a/packages/extended-key-recovery/src/reconstructKeys.ts
+++ b/packages/extended-key-recovery/src/reconstructKeys.ts
@@ -1,9 +1,16 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+/* eslint-disable @typescript-eslint/naming-convention */
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+/* eslint-disable no-continue */
+/* eslint-disable no-restricted-syntax */
import { secp256k1 } from '@noble/curves/secp256k1';
import { ed25519 } from '@noble/curves/ed25519';
import { modPow } from 'bigint-mod-arith';
import { encode as encode_b58check, decode as decode_b58check } from 'bs58check';
-import { Algorithm, CalculatedPrivateKey, PlayerData, SigningKeyMetadata, UnknownAlgorithmError } from './types';
+import { CalculatedPrivateKeysets, PlayerData, UnknownAlgorithmError } from './types';
+import { Algorithm } from './algorithms';
+import { SigningKeysMap } from './metadata';
const _lagrangeCoefficient = (myId: string, allIds: string[], field: bigint): bigint => {
function _primeModInverse(x: bigint, p: bigint): bigint {
@@ -28,7 +35,7 @@ const _encodeKey = (key: string, algorithm: Algorithm, chainCode: Buffer, isPub:
const keyBuffer = isPub
? Buffer.from(key.length === 66 ? key : '0'.repeat(66 - key.length) + key, 'hex')
: Buffer.from(key.length === 64 ? key : '0'.repeat(64 - key.length) + key, 'hex');
- const isECDSA = algorithm === 'MPC_ECDSA_SECP256K1' || algorithm === 'MPC_CMP_ECDSA_SECP256K1';
+ const isECDSA = algorithm === 'MPC_CMP_ECDSA_SECP256K1' || algorithm === 'MPC_ECDSA_SECP256K1';
if (isPub) {
buffers.push(Buffer.from(isECDSA ? '0488B21E' : '03273e4b', 'hex'));
} else {
@@ -40,6 +47,7 @@ const _encodeKey = (key: string, algorithm: Algorithm, chainCode: Buffer, isPub:
buffers.push(Buffer.from('00', 'hex'));
}
buffers.push(keyBuffer);
+ // @ts-ignore
return encode_b58check(Buffer.concat(buffers));
};
@@ -100,14 +108,13 @@ const calculateKeys = (keyId: string, playerData: { [key: string]: bigint }, alg
return [prvKey.toString(16).replace('0x', ''), pubKey as string];
};
-export const reconstructKeys = (
- players: PlayerData,
- signingKeys: { [key: string]: SigningKeyMetadata },
-): CalculatedPrivateKey | undefined => {
- const privateKeys: CalculatedPrivateKey = {};
- for (const keyId of Object.keys(players).filter((key) => key in signingKeys)) {
+export const reconstructKeys = (players: PlayerData, signingKeys: SigningKeysMap): CalculatedPrivateKeysets | undefined => {
+ const privateKeys: CalculatedPrivateKeysets = {};
+
+ const knownKeyIds = Object.keys(players).filter((key) => key in signingKeys);
+ for (const keyId of knownKeyIds) {
const playerDataForKey = players[keyId];
- const { algo, chainCode } = signingKeys[keyId];
+ const { algo, chainCode, keysetId } = signingKeys[keyId];
const [prvKey, pubKey] = calculateKeys(keyId, playerDataForKey, algo);
const pubFromMetadata = signingKeys[keyId].publicKey;
if (pubFromMetadata !== pubKey) {
@@ -119,13 +126,16 @@ export const reconstructKeys = (
}
if (Object.keys(prvKey).includes(algo)) {
continue;
- } else {
- privateKeys[algo] = {
- prvKey: _encodeKey(prvKey, algo, chainCode as Buffer, false),
- pubKey: _encodeKey(pubKey, algo, chainCode as Buffer, true),
- chainCode: (chainCode as Buffer).toString('hex'),
- };
}
+
+ if (!privateKeys[keysetId]) {
+ privateKeys[keysetId] = {};
+ }
+ privateKeys[keysetId][algo] = {
+ prvKey: _encodeKey(prvKey, algo, chainCode as unknown as Buffer, false),
+ pubKey: _encodeKey(pubKey, algo, chainCode as unknown as Buffer, true),
+ chainCode: Buffer.from(chainCode!).toString('hex'),
+ };
}
return privateKeys;
diff --git a/packages/extended-key-recovery/src/recover.ts b/packages/extended-key-recovery/src/recover.ts
index e241f0fb..3534125f 100644
--- a/packages/extended-key-recovery/src/recover.ts
+++ b/packages/extended-key-recovery/src/recover.ts
@@ -9,8 +9,6 @@ import path from 'path';
import AdmZip, { IZipEntry } from 'adm-zip';
import forge from 'node-forge';
import {
- Algorithm,
- CalculatedPrivateKey,
DecryptRSAPrivateKeyError,
InvalidRSAPrivateKeyError,
InvalidRecoveryKitError,
@@ -22,6 +20,8 @@ import {
PlayerData,
RecoveredKeys,
SigningKeyMetadata,
+ MissingMasterKeyFile,
+ CalculatedPrivateKeysets,
} from './types';
import { recoverAutoGeneratedPassphrase } from './recoverAGP';
import { parseMetadataFile } from './metadata';
@@ -29,6 +29,7 @@ import { recoverMobileKeyShare } from './mobileKey';
import { getPlayerId } from './players';
import { reconstructKeys } from './reconstructKeys';
import { recoverNCWMaster } from './ncw';
+import { Algorithm, ECDSA, ECDSA_CMP, EDDSA, EDDSA_CMP } from './algorithms';
const recoverKeysShares = (
zipFiles: IZipEntry[],
@@ -64,7 +65,7 @@ const recoverKeysShares = (
if (keyId) {
let privateKey: forge.pki.rsa.PrivateKey;
- let data: any;
+ let data: unknown;
try {
data = file.getData().toString('binary');
privateKey = forge.pki.decryptRsaPrivateKey(rsaFileData, rsaPass);
@@ -76,7 +77,7 @@ const recoverKeysShares = (
throw new InvalidRSAPrivateKeyError();
}
- let decrypted: unknown;
+ let decrypted: any;
try {
// @ts-ignore
decrypted = Buffer.from(privateKey.decrypt(data, 'RSA-OAEP'), 'binary').toString('hex');
@@ -96,6 +97,27 @@ const recoverKeysShares = (
return players;
};
+const tryGetRecoveredKeyForKeyset = (
+ keysets: CalculatedPrivateKeysets,
+ keysetId: number,
+ isEcdsa: boolean,
+): [boolean, string?, string?, string?] => {
+ const privateKeyForKeyset = keysets[keysetId];
+ if (privateKeyForKeyset === undefined) {
+ return [false, undefined, undefined, undefined];
+ }
+ const valuesToCheck: Algorithm[] = isEcdsa ? [ECDSA, ECDSA_CMP] : [EDDSA, EDDSA_CMP];
+
+ for (const algo of valuesToCheck) {
+ if (privateKeyForKeyset[algo] !== undefined) {
+ const { prvKey, pubKey, chainCode } = privateKeyForKeyset[algo]!;
+ return [true, prvKey, pubKey, chainCode];
+ }
+ }
+
+ return [false, undefined, undefined, undefined];
+};
+
export const recoverKeys = (params: KeyRecoveryConfig): RecoveredKeys => {
const zipData: Buffer =
'zipBase64' in params ? Buffer.from(params.zipBase64, 'base64') : fs.readFileSync(path.resolve(params.zipPath));
@@ -134,18 +156,25 @@ export const recoverKeys = (params: KeyRecoveryConfig): RecoveredKeys => {
}
const mobilePass = 'mobilePass' in params ? params.mobilePass! : recoverAutoGeneratedPassphrase(params, agpFile!);
- const { signingKeys, ncwWalletMasters: masterKeys } = parseMetadataFile(metadataFile.getData().toString());
+ const {
+ signingKeys,
+ ncwWalletMasters: masterKeys,
+ keysetThMapping,
+ maxKeysetId,
+ } = parseMetadataFile(metadataFile.getData().toString());
if (params.recoverOnlyNCW) {
let walletMaster: WalletMaster | undefined;
- if (Object.keys(masterKeys).length > 0) {
+ if (masterKeys && Object.keys(masterKeys).length > 0) {
walletMaster = recoverNCWMaster(zipFiles, forge.pki.decryptRsaPrivateKey(rsaFileData, params.rsaPass), masterKeys);
+ } else {
+ throw new MissingMasterKeyFile();
}
- return { xpub: '', fpub: '', ncwWalletMaster: walletMaster };
+ return { ncwWalletMaster: walletMaster };
}
- const players = recoverKeysShares(zipFiles, signingKeys, mobilePass, rsaFileData, params.rsaPass);
+ const players: PlayerData = recoverKeysShares(zipFiles, signingKeys, mobilePass, rsaFileData, params.rsaPass);
for (const keyId in signingKeys) {
if (!Object.keys(players).includes(keyId)) {
@@ -153,43 +182,70 @@ export const recoverKeys = (params: KeyRecoveryConfig): RecoveredKeys => {
}
}
- const tmp: CalculatedPrivateKey | undefined = reconstructKeys(players, signingKeys);
+ const tmp: CalculatedPrivateKeysets | undefined = reconstructKeys(players, signingKeys);
if (!tmp) {
throw new Error('Mismatch between recovered keys and metadata - unable to continue');
}
- const privateKeys = tmp!;
-
- const keyAlgorithms: Algorithm[] = Object.keys(privateKeys) as Algorithm[];
- let ecdsa: Algorithm;
- let eddsa: Algorithm;
- if (keyAlgorithms.includes('MPC_ECDSA_SECP256K1') || keyAlgorithms.includes('MPC_CMP_ECDSA_SECP256K1')) {
- ecdsa = keyAlgorithms.includes('MPC_ECDSA_SECP256K1') ? 'MPC_ECDSA_SECP256K1' : 'MPC_CMP_ECDSA_SECP256K1';
- }
- if (keyAlgorithms.includes('MPC_EDDSA_ED25519') || keyAlgorithms.includes('MPC_CMP_EDDSA_ED25519')) {
- eddsa = keyAlgorithms.includes('MPC_EDDSA_ED25519') ? 'MPC_EDDSA_ED25519' : 'MPC_CMP_EDDSA_ED25519';
- }
-
- const keys = {
- xpub: privateKeys[ecdsa!]!.pubKey,
- xprv: privateKeys[ecdsa!] ? privateKeys[ecdsa!]!.prvKey : undefined,
- chainCodeEcdsa: privateKeys[ecdsa!]!.chainCode ?? undefined,
- } as RecoveredKeys;
+ const decryptedKeysets = tmp!;
+
+ const recoveredKeys: RecoveredKeys = {};
+
+ for (let keysetId = 1; keysetId <= maxKeysetId; keysetId += 1) {
+ recoveredKeys[keysetId] = { ecdsaExists: false, eddsaExists: false, ecdsaMinAccount: -1, eddsaMinAccount: -1 };
+ let [exists, prv, pub, chaincode] = tryGetRecoveredKeyForKeyset(decryptedKeysets, keysetId, true);
+ recoveredKeys[keysetId].ecdsaExists = exists;
+ if (exists) {
+ const keysetThMappingForAlgo = keysetThMapping[ECDSA]!;
+ let minAccount = keysetId === 1 ? 0 : -1;
+ // If ECDSA exists but there is not ECDSA mapping for threshold, this means that this is the only key and is the minimal account;
+ if (keysetThMappingForAlgo === undefined) {
+ minAccount = 0;
+ }
+ // Even if we have the key of a keyset, it might not exist in the threshold mapping
+ else if (keysetThMappingForAlgo[keysetId] !== undefined) {
+ minAccount = keysetThMappingForAlgo[keysetId];
+ }
+ recoveredKeys[keysetId] = {
+ xprv: prv,
+ xpub: pub,
+ chainCodeEcdsa: chaincode,
+ ...recoveredKeys[keysetId],
+ ecdsaMinAccount: minAccount,
+ };
+ }
+ [exists, prv, pub, chaincode] = tryGetRecoveredKeyForKeyset(decryptedKeysets, keysetId, false);
+ recoveredKeys[keysetId].eddsaExists = exists;
+ if (exists) {
+ const keysetThMappingForAlgo = keysetThMapping[EDDSA]!;
+ // Even if we have the key of a keyset, it might not exist in the threshold mapping
+ let minAccount = keysetId === 1 ? 0 : -1;
+ // If EDDSA exists but there is not EDDSA mapping for threshold, this means that this is the only key and is the minimal account;
+ if (keysetThMappingForAlgo === undefined) {
+ minAccount = 0;
+ } else if (keysetThMappingForAlgo[keysetId] !== undefined) {
+ minAccount = keysetThMappingForAlgo[keysetId];
+ }
- if (privateKeys[eddsa!]) {
- keys.fpub = privateKeys[eddsa!]!.pubKey;
- keys.fprv = privateKeys[eddsa!]!.prvKey ?? undefined;
- keys.chainCodeEddsa = privateKeys[eddsa!]!.chainCode ?? undefined;
- }
+ recoveredKeys[keysetId] = {
+ fprv: prv,
+ fpub: pub,
+ chainCodeEddsa: chaincode,
+ ...recoveredKeys[keysetId],
+ eddsaMinAccount: minAccount,
+ };
+ }
- if (!params.recoveryPrv) {
- delete keys.xprv;
- delete keys.fprv;
+ if (!params.recoveryPrv) {
+ delete recoveredKeys[keysetId].xprv;
+ delete recoveredKeys[keysetId].fprv;
+ }
}
let walletMaster: WalletMaster | undefined;
- if (Object.keys(masterKeys).length > 0) {
+ if (masterKeys && Object.keys(masterKeys).length > 0) {
walletMaster = recoverNCWMaster(zipFiles, forge.pki.decryptRsaPrivateKey(rsaFileData, params.rsaPass), masterKeys);
+ recoveredKeys.ncwWalletMaster = walletMaster;
}
- return { ...keys, ncwWalletMaster: walletMaster };
+ return recoveredKeys;
};
diff --git a/packages/extended-key-recovery/src/types.ts b/packages/extended-key-recovery/src/types.ts
index 715126d8..0da9eea3 100644
--- a/packages/extended-key-recovery/src/types.ts
+++ b/packages/extended-key-recovery/src/types.ts
@@ -1,6 +1,9 @@
+// eslint-disable-next-line max-classes-per-file
+import { Algorithm } from './algorithms';
+
export type CosignerType = 'cloud' | 'mobile';
-export type Algorithm = 'MPC_ECDSA_SECP256K1' | 'MPC_EDDSA_ED25519' | 'MPC_CMP_ECDSA_SECP256K1' | 'MPC_CMP_EDDSA_ED25519';
+// export type Algorithm = 'MPC_ECDSA_SECP256K1' | 'MPC_EDDSA_ED25519' | 'MPC_CMP_ECDSA_SECP256K1' | 'MPC_CMP_EDDSA_ED25519';
export type CosignerMetadata = {
id: string;
@@ -13,7 +16,8 @@ export type BaseMetadataKey = {
};
export type SigningKeyMetadata = BaseMetadataKey & {
- chainCode?: Uint8Array | string;
+ chainCode?: Uint8Array;
+ keysetId: number;
};
export type NCWWalletMasterMetadata = {
@@ -23,11 +27,6 @@ export type NCWWalletMasterMetadata = {
cosigners: CosignerMetadata[];
};
-export type RecoveryPackageMetadata = {
- signingKeys: { [key: string]: SigningKeyMetadata };
- ncwWalletMasters: { [key: string]: NCWWalletMasterMetadata };
-};
-
export type WalletMaster = {
walletSeed: string;
assetSeed: string;
@@ -126,7 +125,7 @@ export class InvalidMasterKey extends Error {
}
}
-export type PlayerData = { [key: string]: { [key: string]: bigint } };
+export type PlayerData = { [keyId: string]: { [playerId: string]: bigint } };
export type MobileKeyShare = {
encryptedKey: string;
@@ -136,21 +135,31 @@ export type MobileKeyShare = {
encryptionAlgorithm: string;
};
-export type RecoveredKeys = {
- xpub: string;
- fpub: string;
+export type RecoveredKey = {
+ xpub?: string;
+ fpub?: string;
xprv?: string;
fprv?: string;
chainCodeEcdsa?: string;
chainCodeEddsa?: string;
+ ecdsaExists: boolean;
+ eddsaExists: boolean;
+ ecdsaMinAccount: number;
+ eddsaMinAccount: number;
+};
+
+export type RecoveredKeys = {
+ [keyset: number]: RecoveredKey;
ncwWalletMaster?: WalletMaster;
};
-export type CalculatedPrivateKey = {
- [key in Algorithm]?: {
- prvKey: string;
- pubKey: string;
- chainCode: string;
+export type CalculatedPrivateKeysets = {
+ [keysetId: number]: {
+ [algo in Algorithm]?: {
+ prvKey: string;
+ pubKey: string;
+ chainCode: string;
+ };
};
};
diff --git a/packages/extended-key-recovery/tests/index.test.ts b/packages/extended-key-recovery/tests/index.test.ts
index fbb62e5c..c5fa3804 100644
--- a/packages/extended-key-recovery/tests/index.test.ts
+++ b/packages/extended-key-recovery/tests/index.test.ts
@@ -13,10 +13,11 @@ describe('Extended Key Recovery', () => {
};
const recovered = recoverKeys(recoveryConfig);
- expect(recovered.xprv).toEqual(
+ // Only one keyset
+ expect(recovered[1].xprv).toEqual(
'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF9aunJDs4SsrmoxycAo6xxBTHawSz5sYxEy8TpCkv66Sci373DJ',
);
- expect(recovered.xpub).toEqual(
+ expect(recovered[1].xpub).toEqual(
'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6QJJZSgiCXT6sq7wa2jCk5t4Vv1r1E4q1venKghAAdyzieufGyX',
);
});
@@ -31,20 +32,21 @@ describe('Extended Key Recovery', () => {
};
const recovered = recoverKeys(recoveryConfig);
- expect(recovered.xprv).toEqual(
+ // Only one keyset
+ expect(recovered[1].xprv).toEqual(
'xprv9s21ZrQH143K2zPNSbKDKusTNW4XVwvTCCEFvcLkeNyauqJJd9UjZg3AtfZbmXa22TFph2NdACUPoWR4sCqMCKQM1j7jRvLuBCF3YoapsX6',
);
- expect(recovered.xpub).toEqual(
+ expect(recovered[1].xpub).toEqual(
'xpub661MyMwAqRbcFUTqYcrDh3pBvXu1uQeJZR9rizkNCiWZnddTAgnz7UMejwX7u4xLmh2JMTtL7DdZmBWGUKa7v836UarassQ3DVFATMzRycV',
);
- expect(recovered.fprv).toEqual(
+ expect(recovered[1].fprv).toEqual(
'fprv4LsXPWzhTTp9ax8NGVwbnRFuT3avVQ4ydHNWcu8hCGZd18TRKxgAzbrpY9bLJRe4Y2AyX9TfQdDPbmqEYoDCTju9QFZbUgdsxsmUgfvuEDK',
);
- expect(recovered.fpub).toEqual(
+ expect(recovered[1].fpub).toEqual(
'fpub8sZZXw2wbqVpURAAA9cCBpv2256rejFtCayHuRAzcYN1qciBxMVmB6UgiDAQTUZh5EP9JZciPQPjKAHyqPYHELqEHWkvo1sxreEJgLyfCJj',
);
- expect(recovered.chainCodeEcdsa).toEqual('5d90bd21d2273a25d0aea082716bdc4529e007823260ad3479182f6672c25cc4');
- expect(recovered.chainCodeEddsa).toEqual('5d90bd21d2273a25d0aea082716bdc4529e007823260ad3479182f6672c25cc4');
+ expect(recovered[1].chainCodeEcdsa).toEqual('5d90bd21d2273a25d0aea082716bdc4529e007823260ad3479182f6672c25cc4');
+ expect(recovered[1].chainCodeEddsa).toEqual('5d90bd21d2273a25d0aea082716bdc4529e007823260ad3479182f6672c25cc4');
});
it('Should recover old format package', async () => {
@@ -57,10 +59,10 @@ describe('Extended Key Recovery', () => {
};
const recovered = recoverKeys(recoveryConfig);
- expect(recovered.xprv).toEqual(
+ expect(recovered[1].xprv).toEqual(
'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF9aunJDs4SsrmoxycAo6xxBTHawSz5sYxEy8TpCkv66Sci373DJ',
);
- expect(recovered.xpub).toEqual(
+ expect(recovered[1].xpub).toEqual(
'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6QJJZSgiCXT6sq7wa2jCk5t4Vv1r1E4q1venKghAAdyzieufGyX',
);
});
@@ -75,16 +77,16 @@ describe('Extended Key Recovery', () => {
};
const recovered = recoverKeys(recoveryConfig);
- expect(recovered.xprv).toEqual(
+ expect(recovered[1].xprv).toEqual(
'xprv9s21ZrQH143K3PhnQQqPZm38HtkJ3bjcVmwc1SfGG8ddw3jXtrhSBNFNcVVx7VUL8vPpmMg1dqxhecVq8WJ1VHn9yoeRM88qfYEnEEi6XaQ',
);
- expect(recovered.xpub).toEqual(
+ expect(recovered[1].xpub).toEqual(
'xpub661MyMwAqRbcFsnFWSNPvtyrqvanT4TTrzsCoq4spUAcor4gSQ1gjAZrTkzR1o8XZ5uPq6WELaga3Zh1eJyfXLvfkWTfV7AjdFU5VuWMpPp',
);
- expect(recovered.fprv).toEqual(
+ expect(recovered[1].fprv).toEqual(
'fprv4LsXPWzhTTp9bMSnEKTn2GRaNSGh33t8vs5rhjTCp2Dg2LtebftscJ52FxRRKeHGLfK6X5Lg3LcsGxQyHZ8ovvPsP2s9PLbZC2VFHc64vFH',
);
- expect(recovered.fpub).toEqual(
+ expect(recovered[1].fpub).toEqual(
'fpub8sZZXw2wbqVpUpUa7y8NRg5gwTndCP53WAgdzFVWEJ24rq9RE4iTnngtS2FeusezUsAJb2sZiMvSDqYGeGVSs65wJqYcGzQRuZGM9NHHqog',
);
});
@@ -99,20 +101,20 @@ describe('Extended Key Recovery', () => {
};
const recovered = recoverKeys(recoveryConfig);
- expect(recovered.xprv).toEqual(
+ expect(recovered[1].xprv).toEqual(
'xprv9s21ZrQH143K3PwZ9jrXG7MZXgj92u6eeCz6M8w8a5RGYJoNmWQRA2eso47rJHr9qawKR9tQVTRki8XUPwVSuBPSnVxT6mQb99XUbruDGk7',
);
- expect(recovered.xpub).toEqual(
+ expect(recovered[1].xpub).toEqual(
'xpub661MyMwAqRbcFt22FmPXdFJJ5iZdSMpW1Ruh9XLk8QxFR78XK3ifhpyMeL5NRqEUapho5bQ7SUavfocg14EDcz2CFMhJYiTjBSXbWQcdkrR',
);
- expect(recovered.fprv).toEqual(
+ expect(recovered[1].fprv).toEqual(
'fprv4LsXPWzhTTp9ax8NGVwbnRFuT3avVQ4ydHNWcu8hCGZd18TRKxgAzbrpY9bLJRe4Y2AyX9TfQdDPbmqEYoDCTju9QFZbUgdsxsmUgfvuEDK',
);
- expect(recovered.fpub).toEqual(
+ expect(recovered[1].fpub).toEqual(
'fpub8sZZXw2wbqVpURAAA9cCBpv2256rejFtCayHuRAzcYN1qciBxMVmB6UgiDAQTUZh5EP9JZciPQPjKAHyqPYHELqEHWkvo1sxreEJgLyfCJj',
);
- expect(recovered.chainCodeEcdsa).toEqual('865b4d6e745c64afc98a7fe32103d6ea775910d4d58e00fe17d2fdd4f8f8f1d0');
- expect(recovered.chainCodeEddsa).toEqual('5d90bd21d2273a25d0aea082716bdc4529e007823260ad3479182f6672c25cc4');
+ expect(recovered[1].chainCodeEcdsa).toEqual('865b4d6e745c64afc98a7fe32103d6ea775910d4d58e00fe17d2fdd4f8f8f1d0');
+ expect(recovered[1].chainCodeEddsa).toEqual('5d90bd21d2273a25d0aea082716bdc4529e007823260ad3479182f6672c25cc4');
});
it('Should recover two custom chaincode package', () => {
//@ts-ignore
@@ -124,20 +126,20 @@ describe('Extended Key Recovery', () => {
};
const recovered = recoverKeys(recoveryConfig);
- expect(recovered.xprv).toEqual(
+ expect(recovered[1].xprv).toEqual(
'xprv9s21ZrQH143K3PwZ9jrXG7MZXgj92u6eeCz6M8w8a5RGYJoNmWQRA2eso47rJHr9qawKR9tQVTRki8XUPwVSuBPSnVxT6mQb99XUbruDGk7',
);
- expect(recovered.xpub).toEqual(
+ expect(recovered[1].xpub).toEqual(
'xpub661MyMwAqRbcFt22FmPXdFJJ5iZdSMpW1Ruh9XLk8QxFR78XK3ifhpyMeL5NRqEUapho5bQ7SUavfocg14EDcz2CFMhJYiTjBSXbWQcdkrR',
);
- expect(recovered.fprv).toEqual(
+ expect(recovered[1].fprv).toEqual(
'fprv4LsXPWzhTTp9bPcFjmqM7U4drbDYRi1YzzFgrfnWAH3hsnLWeJioBzvyvwYJ5p5SuXjwhVd41wrB3tR1Ep41U2DpkJM3J9JGkuCKiBAyyGz',
);
- expect(recovered.fpub).toEqual(
+ expect(recovered[1].fpub).toEqual(
'fpub8sZZXw2wbqVpUre3dRVwWsikRcjUb3CTaHrU9BpoaYr6iGbHGhYPNVYr717NEs15Sjx7Uun6zj2WmGskXQP6Ed9udZYNcUYMeff9hsYTcyr',
);
- expect(recovered.chainCodeEcdsa).toEqual('865b4d6e745c64afc98a7fe32103d6ea775910d4d58e00fe17d2fdd4f8f8f1d0');
- expect(recovered.chainCodeEddsa).toEqual('89b11d04462618fa6d3981f891f2ae8968d8762f268fdec0a4c440ecafb072dd');
+ expect(recovered[1].chainCodeEcdsa).toEqual('865b4d6e745c64afc98a7fe32103d6ea775910d4d58e00fe17d2fdd4f8f8f1d0');
+ expect(recovered[1].chainCodeEddsa).toEqual('89b11d04462618fa6d3981f891f2ae8968d8762f268fdec0a4c440ecafb072dd');
});
it('Should recover from a json share package', () => {
@@ -150,20 +152,20 @@ describe('Extended Key Recovery', () => {
};
const recovered = recoverKeys(recoveryConfig);
- expect(recovered.xprv).toEqual(
+ expect(recovered[1].xprv).toEqual(
'xprv9s21ZrQH143K3t8KzyhL6Vme2HdTM5NitiiLLurTdwUUeKuquVi5QFxz4BwXHdp7Pj1efmkgzPSJp92nTPTMsE7sqABVrAnfgcU9yWYxaNL',
);
- expect(recovered.xpub).toEqual(
+ expect(recovered[1].xpub).toEqual(
'xpub661MyMwAqRbcGNCo71ELTdiNaKTwkY6aFwdw9JG5CH1TX8EzT32Kx4HTuSnnZHg2d2GEsHt8ZHSEGC68mSxcfkA1xaqKzAgSPAHz11yytGK',
);
- expect(recovered.fprv).toEqual(
+ expect(recovered[1].fprv).toEqual(
'fprv4LsXPWzhTTp9bqsKptKiZ1A66q9rLXXFKorb3CeQBq4Wjd4xcJuWqBndhfkSrMeRaojHBsEv7HoHuGGn5uqWQ43MZFTfuYB6gQgGTdGggTm',
);
- expect(recovered.fpub).toEqual(
+ expect(recovered[1].fpub).toEqual(
'fpub8sZZXw2wbqVpVJu7iXzJxQpCfrfnVri9u7TNKighc6rua7KjEhj71gQVsivkxG6tV5xNmXLrU3EpaRP8cJZhk9otAfzsZwJsjEqGCjmWEg8',
);
- expect(recovered.chainCodeEcdsa).toEqual('b72ad958e75f981589e172fd40cad7536c4b04e5d2866f5a5ec53ce6ed12865a');
- expect(recovered.chainCodeEddsa).toEqual('b72ad958e75f981589e172fd40cad7536c4b04e5d2866f5a5ec53ce6ed12865a');
+ expect(recovered[1].chainCodeEcdsa).toEqual('b72ad958e75f981589e172fd40cad7536c4b04e5d2866f5a5ec53ce6ed12865a');
+ expect(recovered[1].chainCodeEddsa).toEqual('b72ad958e75f981589e172fd40cad7536c4b04e5d2866f5a5ec53ce6ed12865a');
});
it('Should recover from a NCW package', () => {
@@ -173,25 +175,25 @@ describe('Extended Key Recovery', () => {
mobilePass: '2#0Iw0Qov@&QP09p',
rsaPass: 'SECRET',
recoveryPrv: true,
+ recoverOnlyNCW: false,
};
const recovered = recoverKeys(recoveryConfig);
- expect(recovered.xprv).toEqual(
+ expect(recovered[1].xprv).toEqual(
'xprv9s21ZrQH143K2Hrbqf9FU48JZrgBSVyQiTiF4NVSeoRQ6gfNhKLC7qSewjGD67sDevao46VE3rdYEBuaN216a9SdgzC425MpMqoVLEB16TF',
);
- expect(recovered.xpub).toEqual(
+ expect(recovered[1].xpub).toEqual(
'xpub661MyMwAqRbcEmw4wggFqC537tWfqxhG5gdqrku4D8xNyUzXEreSfdm8o1BR6F6fKPDysmcLjrR1Q99AANhVHyk8VDkQNngdE3SYHekMGgJ',
);
- expect(recovered.fprv).toEqual(
+ expect(recovered[1].fprv).toEqual(
'fprv4LsXPWzhTTp9aFbbfZmdvZWkeQCaRx7w9YrVkfHPCh1SBypVQ8XdYmGJbCXK5SepZuo9UGTjegUmgaPFunv8vn1TaEggMnaFPXbAmDqSkjZ',
);
- expect(recovered.fpub).toEqual(
+ expect(recovered[1].fpub).toEqual(
'fpub8sZZXw2wbqVpTidPZDSEKyAsDRiWbHJqirTH3BKgcxoq2U5G2XMDjFtAmH9GJekgiEW9o6hYVn8TJvmk64FRYcJiWaZFmpLirqhEThcHu2s',
);
- console.log(recovered.masterKey);
- expect(recovered.masterKey).not.toBeUndefined();
- expect(recovered.masterKey!.masterKeyForCosigner['21926ecc-4a8a-4614-bbac-7c591aa7efdd']).toEqual(
+ expect(recovered.ncwWalletMaster).not.toBeUndefined();
+ expect(recovered.ncwWalletMaster!.masterKeyForCosigner['21926ecc-4a8a-4614-bbac-7c591aa7efdd']).toEqual(
'0de5a6cf9a4b2f6ba69a7f8348b9fb54df48d5af176c2564d9349425a7efe31c',
);
});
diff --git a/packages/shared/components/DerivationPathInput/index.tsx b/packages/shared/components/DerivationPathInput/index.tsx
new file mode 100644
index 00000000..f42f5479
--- /dev/null
+++ b/packages/shared/components/DerivationPathInput/index.tsx
@@ -0,0 +1,146 @@
+import React, { useState, ChangeEvent } from 'react';
+import { Box, Chip, TextField, Typography } from '@mui/material';
+import { HDPath } from '@fireblocks/wallet-derivation';
+
+interface DerivationPathInputProps {
+ onChange?: (path: HDPath) => void;
+ disabled?: boolean;
+ defaultValues?: Partial;
+}
+
+const DerivationPathInput: React.FC = (props) => {
+ const { onChange, disabled, defaultValues = {} } = props;
+ const [coinType, setCoinType] = useState(defaultValues.coinType || 0);
+ const [account, setAccount] = useState(defaultValues.account || 0);
+ const [change, setChange] = useState(defaultValues.changeIndex || 0);
+ const [addressIndex, setAddressIndex] = useState(defaultValues.addressIndex || 0);
+
+ const handleCoinTypeChange = (e: ChangeEvent) => {
+ const updatedCoinType = Number(e.target.value);
+ setCoinType(updatedCoinType);
+ updatePath(updatedCoinType, account, change, addressIndex);
+ };
+
+ const handleAccountChange = (e: ChangeEvent) => {
+ const updatedAccount = Number(e.target.value);
+ setAccount(updatedAccount);
+ updatePath(coinType, updatedAccount, change, addressIndex);
+ };
+
+ const handleChangeChange = (e: ChangeEvent) => {
+ const updatedChange = Number(e.target.value);
+ setChange(updatedChange);
+ updatePath(coinType, account, updatedChange, addressIndex);
+ };
+
+ const handleAddressIndexChange = (e: ChangeEvent) => {
+ const updatedAddressIndex = Number(e.target.value);
+ setAddressIndex(updatedAddressIndex);
+ updatePath(coinType, account, change, updatedAddressIndex);
+ };
+
+ const updatePath = (coinType: number, account: number, changeIndex: number, addressIndex: number) => {
+ if (onChange) {
+ const path = { coinType, account, changeIndex, addressIndex };
+ onChange(path);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ /
+
+
+
+
+
+ /
+
+
+
+
+
+ /
+
+
+
+
+
+ /
+
+
+
+
+
+
+ Format: m / purpose / coin_type / account / change / address_index
+
+
+ );
+};
+
+export default DerivationPathInput;
diff --git a/packages/shared/components/Modals/AddressesModal/index.tsx b/packages/shared/components/Modals/AddressesModal/index.tsx
index 41a7b75d..fa2f331b 100644
--- a/packages/shared/components/Modals/AddressesModal/index.tsx
+++ b/packages/shared/components/Modals/AddressesModal/index.tsx
@@ -51,7 +51,7 @@ function Addresses({ type, derivations, hasSegwit }: AddressProps) {
{data.map(({ address, label }, index, arr) => (
number;
+ addAccount: (name: string, id?: number, mapToNextKeyset?: boolean, ecdsa?: boolean) => number;
};
-const defaultValues: RecoverAccountInput = { name: '', id: null };
+const defaultValues: RecoverAccountInput = { name: '', id: null, incrementEcdsaKeyset: null, incrementEddsaKeyset: null };
-export const RecoverAccountModal = ({ open, onClose: _onClose, accountsKeys, addAccount }: Props) => {
+export const RecoverAccountModal = ({
+ open,
+ hasMultipleKeysets,
+ currentKeysetIndex,
+ totalKeysetCount,
+ onClose: _onClose,
+ accountsKeys,
+ addAccount,
+}: Props) => {
const router = useRouter();
const {
@@ -40,9 +65,15 @@ export const RecoverAccountModal = ({ open, onClose: _onClose, accountsKeys, add
};
const onSubmit = (formData: RecoverAccountInput) => {
+ const mapToNextKeyset =
+ hasMultipleKeysets &&
+ ((formData.incrementEcdsaKeyset == null ? false : formData.incrementEcdsaKeyset) ||
+ (formData.incrementEddsaKeyset === null ? false : formData.incrementEddsaKeyset));
const accountId = addAccount(
formData.name,
formData.id !== null && formData.id < 0 ? undefined : formData.id !== null ? formData.id : undefined,
+ mapToNextKeyset,
+ formData.incrementEcdsaKeyset === null,
);
router.push({
@@ -101,6 +132,53 @@ export const RecoverAccountModal = ({ open, onClose: _onClose, accountsKeys, add
}
/>
+ {hasMultipleKeysets && currentKeysetIndex && totalKeysetCount && (
+
+
+
+
+ Multiple Keysets with no Threshold Mapping
+
+
+
+ {currentKeysetIndex[0] < totalKeysetCount[0] && (
+
+
+
+ ECDSA Total of {totalKeysetCount[0]} keysets, using keyset{' '}
+ {currentKeysetIndex[0]}{' '}
+
+
+ } label={`Continue using keyset ${currentKeysetIndex[0]}`} />
+ }
+ label={`Switch to keyset ${currentKeysetIndex[0] + 1} for this vault account and moving forward`}
+ />
+
+
+
+ )}
+ {currentKeysetIndex[1] < totalKeysetCount[1] && (
+
+
+
+ EDDSA Total of {totalKeysetCount[1]} keysets, using keyset{' '}
+ {currentKeysetIndex[1]}{' '}
+
+
+ } label={`Continue using keyset ${currentKeysetIndex[1]}`} />
+ }
+ label={`Switch to keyset ${currentKeysetIndex[1] + 1} for this vault account and moving forward`}
+ />
+
+
+
+ )}
+
+ )}
diff --git a/packages/shared/components/RawSigningForm/Signature/index.tsx b/packages/shared/components/RawSigningForm/Signature/index.tsx
new file mode 100644
index 00000000..7625c071
--- /dev/null
+++ b/packages/shared/components/RawSigningForm/Signature/index.tsx
@@ -0,0 +1,146 @@
+import { Grid, Typography } from '@mui/material';
+import { SigningAlgorithms } from '../../../reducers/rawSignReducer';
+import { TextField } from '../../TextField';
+
+interface SignatureProps {
+ selectedAlgorithm: SigningAlgorithms;
+ signature: string;
+}
+
+const Signature: React.FC = (props) => {
+ const { selectedAlgorithm, signature } = props;
+ return (
+
+ {selectedAlgorithm === SigningAlgorithms.ECDSA ? (
+
+
+ ECDSA Signature Components:
+
+
+
+ Full Signature:
+
+
+
+
+
+ r:
+
+
+
+
+
+ s:
+
+
+
+
+
+ v:
+
+
+
+
+ ) : selectedAlgorithm === SigningAlgorithms.EDDSA ? (
+
+
+ Signature
+
+
+
+ ) : null}
+
+ );
+};
+
+export default Signature;
diff --git a/packages/shared/components/RawSigningForm/index.tsx b/packages/shared/components/RawSigningForm/index.tsx
new file mode 100644
index 00000000..6cae5b51
--- /dev/null
+++ b/packages/shared/components/RawSigningForm/index.tsx
@@ -0,0 +1,221 @@
+import { useCallback } from 'react';
+import { useRawSign } from '../../hooks/useRawSign';
+import { HDPath } from '@fireblocks/wallet-derivation';
+import DerivationPathInput from '../DerivationPathInput';
+import { Button, Select, TextField, VaultAccount } from '@fireblocks/recovery-shared';
+import { Box, Checkbox, FormControlLabel, FormGroup, Typography } from '@mui/material';
+import { SigningWallet } from '../../../../apps/recovery-utility/renderer/lib/wallets/SigningWallet';
+
+// const logger = getLogger('utility');
+
+export type SignMessageParams = {
+ unsignedMessage: string;
+ rawSignMethod: RawSignMethod;
+ selectedWallet: SigningWalletWithSign | null;
+ inputChangeIndex: number;
+ inputAdressIndex: number;
+ derivationPath: HDPath;
+ dpAlgorithm: SigningAlgorithms;
+};
+
+type RawSigningFormProps = {
+ accounts: Map>;
+ onSubmit: ({
+ unsignedMessage,
+ rawSignMethod,
+ selectedWallet,
+ inputChangeIndex,
+ inputAdressIndex,
+ derivationPath,
+ dpAlgorithm,
+ }: SignMessageParams) => Promise;
+};
+
+enum RawSignMethod {
+ ACCOUNT = 'account',
+ DERIVATION_PATH = 'derivationPath',
+}
+
+enum SigningAlgorithms {
+ ECDSA = 'ECDSA',
+ EDDSA = 'EDDSA',
+}
+
+type SigningWalletWithSign = SigningWallet & {
+ sign?: (message: Uint8Array | string, hasher?: (...msgs: Uint8Array[]) => Promise) => Promise;
+};
+
+const RawSigningForm: React.FC = (props) => {
+ const { accounts, onSubmit } = props;
+ const {
+ rawSignMethod,
+ selectedAccountId,
+ accountsArray,
+ selectedWallet,
+ walletOptions,
+ inputAdressIndex,
+ inputChangeIndex,
+ dpAlgorithm,
+ unsignedTx,
+ appProtocol,
+ derivationPath,
+ handleRawSigningMethod,
+ handleWalletChange,
+ setSelectedAccountId,
+ setInputChangeIndex,
+ setSelectedWallet,
+ setSignedTx,
+ setInputAdressIndex,
+ setDpAlgorithm,
+ setDerivationPath,
+ setUnignedTx,
+ } = useRawSign(accounts);
+
+ const handleOnSubmit = useCallback(async () => {
+ if (!unsignedTx || (rawSignMethod === RawSignMethod.ACCOUNT && !selectedWallet)) {
+ throw new Error('transaction was not provided');
+ }
+ await onSubmit({
+ unsignedMessage: unsignedTx,
+ rawSignMethod,
+ selectedWallet,
+ inputChangeIndex,
+ inputAdressIndex,
+ derivationPath,
+ dpAlgorithm,
+ });
+ }, [unsignedTx, rawSignMethod, selectedWallet, inputChangeIndex, inputAdressIndex, derivationPath, dpAlgorithm]);
+
+ return (
+ <>
+
+
+ Raw Signing
+
+
+ Use this tool to sign messages with your Fireblocks keys by selecting assets either from your accounts or by specifying
+ a custom derivation path. This secure signing process allows you to create cryptographic signatures for non-supported
+ blockchain operations without exposing your private keys.
+
+
+
+ Select Asset For Transaction Signature
+
+
+
+ }
+ checked={rawSignMethod === RawSignMethod.ACCOUNT}
+ onChange={(_, checked) => {
+ checked && handleRawSigningMethod(RawSignMethod.ACCOUNT);
+ }}
+ />
+
+
+
+
+
+ }
+ checked={rawSignMethod === RawSignMethod.DERIVATION_PATH}
+ onChange={(_, checked) => {
+ checked && handleRawSigningMethod(RawSignMethod.DERIVATION_PATH);
+ }}
+ />
+
+
+ setDerivationPath(path)}
+ />
+ ({ value: algo, children: algo.toUpperCase() }))}
+ onChange={(event) => {
+ setDpAlgorithm(event.target.value as SigningAlgorithms);
+ }}
+ sx={{ width: '124px' }}
+ disabled={rawSignMethod !== RawSignMethod.DERIVATION_PATH}
+ />
+
+
+
+
+ {
+ setUnignedTx(e.target.value);
+ }}
+ sx={{ ml: 2, mr: 4 }}
+ />
+
+
+
+
+ >
+ );
+};
+
+export default RawSigningForm;
diff --git a/packages/shared/components/TextField/index.tsx b/packages/shared/components/TextField/index.tsx
index 38b7a378..8ec4d182 100644
--- a/packages/shared/components/TextField/index.tsx
+++ b/packages/shared/components/TextField/index.tsx
@@ -132,8 +132,7 @@ export const TextField = forwardRef(
aria-describedby={`${id}-helper-text`}
color={error ? 'error' : 'primary'}
type={revealed ? 'text' : type}
- value={value}
- defaultValue={defaultValue || value}
+ {...(value !== undefined ? { value } : { defaultValue })}
error={!!error}
readOnly={readOnly || enableCopy}
inputRef={inputRef}
diff --git a/packages/shared/components/index.ts b/packages/shared/components/index.ts
index 94799bd6..9c7c4cd1 100644
--- a/packages/shared/components/index.ts
+++ b/packages/shared/components/index.ts
@@ -1,6 +1,7 @@
export * from './BaseModal';
export * from './Button';
export * from './DataGrid';
+export * from './DerivationPathInput';
export * from './EmotionHeadTags';
export * from './ErrorBoundary';
export * from './Glyph';
@@ -12,6 +13,7 @@ export * from './LogoHero';
export * from './Modals';
export * from './QrCode';
export * from './QrCodeScanner';
+export * from './RawSigningForm';
export * from './RelayRxTx';
export * from './Select';
export * from './SharedProviders';
diff --git a/packages/shared/hooks/useBaseWorkspace/index.ts b/packages/shared/hooks/useBaseWorkspace/index.ts
index 81aef9f9..2aaf7c1f 100644
--- a/packages/shared/hooks/useBaseWorkspace/index.ts
+++ b/packages/shared/hooks/useBaseWorkspace/index.ts
@@ -2,9 +2,10 @@ import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { Input, BaseWallet } from '@fireblocks/wallet-derivation';
import { LocalFile } from 'papaparse';
+import { RecoveredKey } from '@fireblocks/extended-key-recovery/src/types';
import { AddressesCsv } from '../../schemas/addressesCsv';
// import { BalancesCsv } from '../../schemas/balancesCsv';
-import type { ExtendedKeys } from '../../schemas';
+import { type UtilityExtendedKeys, type KeysetMap, RelayExtendedKeys } from '../../schemas';
import { Wallet, VaultAccount, Transaction } from '../../types';
import { BaseWorkspaceInput, BaseWorkspace, BaseWorkspaceContext } from './types';
import { csvImport } from '../../lib/csv';
@@ -20,7 +21,7 @@ export type { BaseWorkspace, BaseWorkspaceContext };
const logger = getLogger(LOGGER_NAME_SHARED);
-const defaultBaseWorkspaceInput: BaseWorkspaceInput = {
+const defaultBaseWorkspaceInput: BaseWorkspaceInput<'utility', BaseWallet> = {
extendedKeys: undefined,
accounts: new Map>(),
transactions: new Map(),
@@ -37,6 +38,7 @@ export const defaultBaseWorkspaceContext: BaseWorkspaceContext = {
resetInboundRelayUrl: () => {},
getOutboundRelayUrl: () => '',
setExtendedKeys: () => undefined,
+ getExtendedKeysForAccountId: () => undefined,
importCsv: () => Promise.resolve(),
addAccount: () => 0,
addWallet: () => undefined,
@@ -60,27 +62,94 @@ export const useBaseWorkspace = >(
+ const [workspace, setWorkspace] = useWrappedState>(
'workspace',
- defaultBaseWorkspaceInput as BaseWorkspaceInput,
+ defaultBaseWorkspaceInput as BaseWorkspaceInput,
true,
);
const account = typeof query.accountId === 'string' ? workspace.accounts.get(parseInt(query.accountId, 10)) : undefined;
- const setExtendedKeys = ({ xpub, fpub, xprv, fprv, ncwMaster }: Partial) =>
- setWorkspace((prev) => ({
- ...prev,
- extendedKeys: {
- ...prev.extendedKeys,
- ...(app === 'utility' ? { xprv, fprv, ncwMaster } : {}),
- xpub,
- fpub,
- },
- }));
+ const getExtendedKeysForAccountId = (
+ accountId: number,
+ ): { xpub: string; fpub: string; xprv?: string; fprv?: string } | undefined => {
+ if (app === 'relay') {
+ return undefined;
+ }
+
+ const keysets = Object.entries(workspace.extendedKeys || {}).filter(([keysetId]) => keysetId !== 'ncwMaster') as [
+ string,
+ RecoveredKey,
+ ][];
+ if (keysets.length === 0) {
+ return undefined;
+ }
+
+ // We want to find the xpub and fpub for the extended keys that match the required accountId.
+ let xpub: string | undefined;
+ let xprv: string | undefined;
+ let fpub: string | undefined;
+ let fprv: string | undefined;
+ const ecdsaKeyset = Object.values(keysets).findLast(([, key]) =>
+ key.ecdsaExists ? key.ecdsaMinAccount <= accountId : false,
+ );
+ if (ecdsaKeyset !== undefined) {
+ xpub = ecdsaKeyset[1].xpub;
+ xprv = ecdsaKeyset[1].xprv;
+ }
+ const eddsaKeyset = Object.values(keysets).findLast(([, key]) =>
+ key.eddsaExists ? key.eddsaMinAccount <= accountId : false,
+ );
+ if (eddsaKeyset !== undefined) {
+ fpub = eddsaKeyset[1].fpub;
+ fprv = eddsaKeyset[1].fprv;
+ }
+
+ if (!xpub || !fpub) {
+ return undefined;
+ }
+ return { xpub, fpub, xprv, fprv };
+ };
+
+ const setExtendedKeys = (extendedKeys: App extends 'utility' ? UtilityExtendedKeys : RelayExtendedKeys) =>
+ setWorkspace((prev) => {
+ if (app === 'utility') {
+ const keys = extendedKeys as UtilityExtendedKeys;
+ const newExtendedKeys: KeysetMap = { ...prev.extendedKeys };
+ const newWorkspace = {
+ ...prev,
+ ncwMaster: keys.ncwMaster,
+ };
+ Object.entries(keys).forEach(([keysetId, value]) => {
+ if (keysetId === 'ncwMaster') {
+ return;
+ }
+ const keysetEntry = value as RecoveredKey;
+ newExtendedKeys[Number(keysetId)] = {
+ ...keysetEntry,
+ ...(app === 'utility' ? { xprv: keysetEntry.xprv, fprv: keysetEntry.fprv } : {}),
+ };
+ });
+
+ newWorkspace.extendedKeys = { ...newExtendedKeys };
+
+ return newWorkspace;
+ }
+ const { xpub, fpub } = extendedKeys as RelayExtendedKeys;
+ return {
+ ...prev,
+ extendedKeys: {
+ ...prev.extendedKeys,
+ xpub,
+ fpub,
+ },
+ };
+ });
const setDerivation = (
- derivationInput: Omit, 'accounts' | 'deriveWallet'> & { extendedKeys?: ExtendedKeys },
+ derivationInput: Omit, 'accounts' | 'deriveWallet'> & {
+ extendedKeys?: UtilityExtendedKeys;
+ },
setDerivationError?: (err: string) => void,
) => {
setWorkspace((prev) => {
@@ -162,12 +231,37 @@ export const useBaseWorkspace = {
+ (name: string, newAccountId?: number, mapAccountToNextKeysetEntry?: boolean, ecdsa?: boolean) => {
let resolvedAccountId = newAccountId;
logger.info(`Adding new vault account: ${name} ${newAccountId ? ` - ${newAccountId}` : ''}`);
setWorkspace((prev) => {
const accounts = new Map(prev.accounts);
+ const { extendedKeys } = prev;
+
+ if (mapAccountToNextKeysetEntry === true) {
+ const entry = Object.entries(extendedKeys || {})
+ .filter(([keyset]) => keyset !== 'ncwMaster')
+ .find(([, value]) => {
+ const key = value as RecoveredKey;
+ if (ecdsa) return key.ecdsaExists && key.ecdsaMinAccount === -1;
+ if (!ecdsa) return key.eddsaExists && key.eddsaMinAccount === -1;
+ return false;
+ });
+
+ if (entry === undefined) {
+ throw new Error(`Could not find vacant key mapping for ${ecdsa ? 'ECDSA' : 'EDDSA'} algorithm.`);
+ }
+
+ if (ecdsa) {
+ (extendedKeys as Record)[Number(entry[0])].ecdsaMinAccount =
+ !newAccountId || newAccountId < 0 ? accounts.size : newAccountId;
+ }
+ if (!ecdsa) {
+ (extendedKeys as Record)[Number(entry[0])].eddsaMinAccount =
+ !newAccountId || newAccountId < 0 ? accounts.size : newAccountId;
+ }
+ }
if (typeof resolvedAccountId === 'undefined') {
if (accounts.size > 0) {
@@ -188,7 +282,7 @@ export const useBaseWorkspace = {
- setWorkspace(defaultBaseWorkspaceInput as BaseWorkspaceInput);
+ setWorkspace(defaultBaseWorkspaceInput as BaseWorkspaceInput);
push('/');
};
@@ -267,6 +361,7 @@ export const useBaseWorkspace = = {
- extendedKeys?: ExtendedKeys;
+export type DerivationReducerInput = {
+ extendedKeys?: App extends 'utility' ? UtilityExtendedKeys : RelayExtendedKeys;
accounts: Map>;
assetId: string;
accountId: number;
@@ -32,7 +34,9 @@ export type DerivationReducerInput = {
export const testIsLegacy = (assetId: string, address: string) =>
(assetId === 'BTC' && !address.startsWith('bc1')) || (assetId.startsWith('BTC_') && !address.startsWith('tb1'));
-export const reduceDerivations = (input: DerivationReducerInput) => {
+export const reduceDerivations = (
+ input: DerivationReducerInput,
+) => {
const {
extendedKeys,
accounts,
@@ -104,7 +108,48 @@ export const reduceDerivations = (input: Deri
isLegacy,
};
- const { xpub, fpub, xprv, fprv } = extendedKeys ?? {};
+ // No option to inspect App at runtime, need to rely on values in extendedKeys
+
+ let keys;
+ if ('xpub' in (extendedKeys || {})) {
+ // Will only happen when extendedKeys is defined + is in relay app
+ keys = { ...(extendedKeys as RelayExtendedKeys), xprv: undefined, fprv: undefined };
+ } else {
+ const keysets = Object.entries((extendedKeys as UtilityExtendedKeys) || {}).filter(
+ ([keysetId]) => keysetId !== 'ncwMaster',
+ ) as [string, RecoveredKey][];
+ if (keysets.length === 0) {
+ keys = undefined;
+ }
+
+ // We want to find the xpub and fpub for the extended keys that match the required accountId.
+ let xpub: string | undefined;
+ let xprv: string | undefined;
+ let fpub: string | undefined;
+ let fprv: string | undefined;
+ const ecdsaKeyset = Object.values(keysets).findLast(([, key]) =>
+ key.ecdsaExists ? key.ecdsaMinAccount !== -1 && key.ecdsaMinAccount <= accountId : false,
+ );
+ if (ecdsaKeyset !== undefined) {
+ xpub = ecdsaKeyset[1].xpub;
+ xprv = ecdsaKeyset[1].xprv;
+ }
+ const eddsaKeyset = Object.values(keysets).findLast(([, key]) =>
+ key.eddsaExists ? key.eddsaMinAccount !== -1 && key.eddsaMinAccount <= accountId : false,
+ );
+ if (eddsaKeyset !== undefined) {
+ fpub = eddsaKeyset[1].fpub;
+ fprv = eddsaKeyset[1].xprv;
+ }
+
+ if (!xpub || !fpub) {
+ keys = undefined;
+ } else {
+ keys = { xpub, fpub, xprv, fprv };
+ }
+ }
+
+ const { xpub, fpub, xprv, fprv } = keys ?? {};
const hasXpub = !!xpub || !!fpub;
diff --git a/packages/shared/hooks/useBaseWorkspace/types.ts b/packages/shared/hooks/useBaseWorkspace/types.ts
index e15203d0..5191fb30 100644
--- a/packages/shared/hooks/useBaseWorkspace/types.ts
+++ b/packages/shared/hooks/useBaseWorkspace/types.ts
@@ -1,10 +1,10 @@
import { BaseWallet } from '@fireblocks/wallet-derivation';
import type { LocalFile } from 'papaparse';
-import type { ExtendedKeys, RelayRequestParams, RelayResponseParams } from '../../schemas';
+import type { UtilityExtendedKeys, RelayRequestParams, RelayResponseParams, RelayExtendedKeys } from '../../schemas';
import type { VaultAccount, Transaction, Wallet } from '../../types';
-export type BaseWorkspaceInput = {
- extendedKeys?: ExtendedKeys;
+export type BaseWorkspaceInput = {
+ extendedKeys?: App extends 'utility' ? UtilityExtendedKeys : RelayExtendedKeys;
accounts: Map>;
transactions: Map;
derivationError?: string;
@@ -13,7 +13,7 @@ export type BaseWorkspaceInput = {
export type BaseWorkspace<
Derivation extends BaseWallet = BaseWallet,
App extends 'utility' | 'relay' = 'utility',
-> = BaseWorkspaceInput & {
+> = BaseWorkspaceInput & {
account?: VaultAccount;
inboundRelayParams?: App extends 'utility' ? RelayResponseParams : RelayRequestParams;
};
@@ -27,7 +27,8 @@ export type BaseWorkspaceContext<
getOutboundRelayUrl: (
params: Params,
) => string;
- setExtendedKeys: (extendedKeys: ExtendedKeys) => void;
+ setExtendedKeys: (extendedKeys: App extends 'utility' ? UtilityExtendedKeys : RelayExtendedKeys) => void;
+ getExtendedKeysForAccountId: (accoutId: number) => { xpub: string; fpub: string; xprv?: string; fprv?: string } | undefined;
importCsv: (addressesCsv?: LocalFile, balancesCsv?: LocalFile) => Promise;
setTransaction: (transaction: Transaction) => void;
addAccount: (name: string, accountId?: number) => number;
diff --git a/packages/shared/hooks/useRawSign.ts b/packages/shared/hooks/useRawSign.ts
new file mode 100644
index 00000000..722c425a
--- /dev/null
+++ b/packages/shared/hooks/useRawSign.ts
@@ -0,0 +1,101 @@
+import { useCallback, useMemo, useReducer } from 'react';
+import { VaultAccount } from '../types';
+import { SelectChangeEvent } from '@mui/material';
+import { HDPath } from '@fireblocks/wallet-derivation';
+import { getAssetConfig } from '@fireblocks/asset-config';
+import { getDerivationMapKey } from '../lib/getDerivation';
+import { DeploymentStore } from '../../../apps/recovery-utility/main/store/deployment';
+import { SigningWallet } from '../../../apps/recovery-utility/renderer/lib/wallets/SigningWallet';
+import {
+ RawSignMethod,
+ rawSignReducer,
+ RawSignState,
+ SigningAlgorithms,
+ SigningWalletWithSign,
+} from '../reducers/rawSignReducer';
+
+export const useRawSign = (accounts: Map>) => {
+ const initialState: RawSignState = {
+ rawSignMethod: RawSignMethod.ACCOUNT,
+ selectedAccountId: null,
+ selectedWallet: null,
+ inputChangeIndex: 0,
+ inputAdressIndex: 0,
+ derivationPath: { coinType: 0, account: 0, changeIndex: 0, addressIndex: 0 },
+ dpAlgorithm: SigningAlgorithms.ECDSA,
+ unsignedTx: null,
+ signedTx: null,
+ qrData: null,
+ };
+
+ const [state, dispatch] = useReducer(rawSignReducer, initialState);
+
+ const deployment = DeploymentStore.get();
+ const appProtocol = deployment.protocol;
+
+ const accountsArray = useMemo(() => Array.from(accounts.values()), [accounts]);
+
+ const handleRawSigningMethod = (method: RawSignMethod) => {
+ dispatch({ type: 'SET_RAW_SIGN_METHOD', payload: method });
+ };
+
+ const walletOptions = useMemo(() => {
+ if (state.selectedAccountId === null) return [];
+
+ const selectedAccount = accountsArray.find((acc) => acc.id === state.selectedAccountId);
+ const currentWallets = selectedAccount?.wallets;
+
+ return currentWallets
+ ? Array.from(currentWallets).map(([walletId, wallet]) => ({
+ value: walletId,
+ children: wallet.assetId,
+ }))
+ : [];
+ }, [state.selectedAccountId, accountsArray]);
+
+ const handleWalletChange = useCallback(
+ (event: SelectChangeEvent) => {
+ dispatch({ type: 'RESET_WALLET_STATE' });
+
+ const walletId = event.target.value as string;
+
+ const selectedAccount = accountsArray.find((acc) => acc.id === state.selectedAccountId);
+ const wallet = selectedAccount?.wallets.get(walletId) || null;
+ if (!wallet) return;
+
+ const assetId = wallet.assetId;
+ const selectedAsset = getAssetConfig(assetId);
+ if (!selectedAsset) return;
+
+ const firstEntry = Array.from(wallet.derivations.values()).find((entry: any) => entry.address);
+ const address = firstEntry?.address;
+ if (!address) return;
+
+ const derivationMapKey = getDerivationMapKey(selectedAsset.id, address);
+ const derivation = wallet.derivations.get(derivationMapKey);
+ if (!derivation) return;
+
+ dispatch({ type: 'SET_SELECTED_WALLET', payload: derivation });
+ dispatch({ type: 'SET_DP_ALGORITHM', payload: derivation.algorithm as SigningAlgorithms });
+ },
+ [state.selectedAccountId, accountsArray],
+ );
+
+ return {
+ ...state,
+ accountsArray,
+ walletOptions,
+ appProtocol,
+ handleRawSigningMethod,
+ handleWalletChange,
+ setSelectedAccountId: (id: number | null) => dispatch({ type: 'SET_SELECTED_ACCOUNT_ID', payload: id }),
+ setSelectedWallet: (wallet: SigningWalletWithSign | null) => dispatch({ type: 'SET_SELECTED_WALLET', payload: wallet }),
+ setInputChangeIndex: (index: number) => dispatch({ type: 'SET_INPUT_CHANGE_INDEX', payload: index }),
+ setInputAdressIndex: (index: number) => dispatch({ type: 'SET_INPUT_ADDRESS_INDEX', payload: index }),
+ setDerivationPath: (path: HDPath) => dispatch({ type: 'SET_DERIVATION_PATH', payload: path }),
+ setDpAlgorithm: (algo: SigningAlgorithms) => dispatch({ type: 'SET_DP_ALGORITHM', payload: algo }),
+ setUnignedTx: (tx: string | null) => dispatch({ type: 'SET_UNSIGNED_TX', payload: tx }),
+ setSignedTx: (tx: string | null) => dispatch({ type: 'SET_SIGNED_TX', payload: tx }),
+ setQrData: (data: string | null) => dispatch({ type: 'SET_QR_DATA', payload: data }),
+ };
+};
diff --git a/packages/shared/hooks/useRawSignMessage.ts b/packages/shared/hooks/useRawSignMessage.ts
new file mode 100644
index 00000000..dd06f8e8
--- /dev/null
+++ b/packages/shared/hooks/useRawSignMessage.ts
@@ -0,0 +1,161 @@
+import { useState, useCallback } from 'react';
+import { Buffer } from 'buffer';
+import { SignMessageParams } from '../components';
+import { HDPathParts, Input } from '@fireblocks/wallet-derivation';
+import { getLogger, RelayExtendedKeys, UtilityExtendedKeys } from '@fireblocks/recovery-shared';
+import { LOGGER_NAME_UTILITY } from '@fireblocks/recovery-shared/constants';
+import { RawSignMethod, SigningAlgorithms } from '../reducers/rawSignReducer';
+import { ECDSAWallet } from '@fireblocks/wallet-derivation/wallets/ECDSAWallet';
+import { EdDSAWallet, sha512 } from '@fireblocks/wallet-derivation/wallets/EdDSAWallet';
+
+class DerivationPathECDSAWallet extends ECDSAWallet {
+ protected getAddress(evmAddress?: string): string {
+ return evmAddress || '';
+ }
+ async signMessage(message: Uint8Array) {
+ return await this.sign(message);
+ }
+}
+
+class DerivationPathEDDSAWallet extends EdDSAWallet {
+ protected getAddress(evmAddress?: string): string {
+ return evmAddress || '';
+ }
+
+ async signMessage(message: string | Uint8Array, hasher: (...msgs: Uint8Array[]) => Promise = sha512) {
+ return await this.sign(message, hasher);
+ }
+}
+
+const formatECDSASignature = (signatureHex: string) => {
+ if (signatureHex.startsWith('0x')) {
+ signatureHex = signatureHex.slice(2);
+ }
+
+ if (signatureHex.length !== 130) {
+ throw new Error('Invalid signature length');
+ }
+
+ const r = '0x' + signatureHex.slice(0, 64);
+ const s = '0x' + signatureHex.slice(64, 128);
+ const v = parseInt(signatureHex.slice(128, 130), 16);
+ return {
+ signature: signatureHex,
+ r,
+ s,
+ v,
+ };
+};
+
+export const useRawSignMessage = (extendedKeys?: UtilityExtendedKeys | RelayExtendedKeys) => {
+ const [signature, setSignature] = useState(null);
+ const [selectedAlgorithm, setSelectedAlgorithm] = useState(SigningAlgorithms.ECDSA);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const logger = getLogger(LOGGER_NAME_UTILITY);
+
+ const generateSignature = useCallback(
+ async ({
+ unsignedMessage,
+ rawSignMethod,
+ selectedWallet,
+ inputChangeIndex,
+ inputAdressIndex,
+ derivationPath,
+ dpAlgorithm,
+ }: SignMessageParams) => {
+ setIsLoading(true);
+ try {
+ if (!unsignedMessage) {
+ throw new Error('transaction was not provided');
+ }
+ setSignature(null);
+ const messageHashBuffer = Buffer.from(unsignedMessage, 'hex');
+
+ const message = Uint8Array.from(messageHashBuffer);
+ setSelectedAlgorithm(dpAlgorithm);
+
+ if (rawSignMethod === RawSignMethod.ACCOUNT) {
+ if (!selectedWallet || !selectedWallet.sign) {
+ throw new Error('selected wallet error');
+ }
+ selectedWallet.path = {
+ ...selectedWallet.path,
+ changeIndex: inputChangeIndex,
+ addressIndex: inputAdressIndex,
+ };
+ const newPathParts: HDPathParts = [
+ ...selectedWallet.pathParts.slice(0, -2),
+ inputChangeIndex,
+ inputAdressIndex,
+ ] as HDPathParts;
+
+ selectedWallet.pathParts = newPathParts;
+ console.log('sign message - selectedWallet', selectedWallet);
+ logger.info(`Signing message using ${selectedWallet.constructor.name} at path ${selectedWallet.pathParts.join('/')}`);
+
+ const sigHex = await selectedWallet.sign(message);
+
+ const walletAlgorithm = selectedWallet.algorithm;
+
+ switch (walletAlgorithm) {
+ case 'EDDSA':
+ const signatureString = Buffer.from(sigHex).toString('hex');
+ // console.log(signatureString);
+ setSignature(signatureString || 'error');
+ break;
+ case 'ECDSA':
+ const formattedSig = formatECDSASignature(sigHex);
+ setSignature(JSON.stringify(formattedSig));
+ break;
+ default:
+ throw new Error('Unknown wallet algorithm');
+ }
+ } else if (rawSignMethod === RawSignMethod.DERIVATION_PATH) {
+ if (!extendedKeys || Object.values(extendedKeys).length === 0) {
+ throw new Error('Extended keys not provided');
+ }
+
+ const walletInput: Input = {
+ ...(extendedKeys as UtilityExtendedKeys | RelayExtendedKeys),
+ assetId: 'assetId',
+ path: derivationPath,
+ };
+
+ switch (dpAlgorithm) {
+ case SigningAlgorithms.ECDSA:
+ const dpECDSAWallet = new DerivationPathECDSAWallet(walletInput, derivationPath.coinType);
+ const ecdsaSigHex = await dpECDSAWallet.signMessage(message);
+ const formattedSig = formatECDSASignature(ecdsaSigHex);
+ setSignature(JSON.stringify(formattedSig));
+ break;
+ case SigningAlgorithms.EDDSA:
+ const dpEDDSAWallet = new DerivationPathEDDSAWallet(walletInput, derivationPath.coinType);
+ const eddsaSigHex = await dpEDDSAWallet.signMessage(message);
+ const signatureString = Buffer.from(eddsaSigHex).toString('hex');
+ // console.log(signatureString);
+ setSignature(signatureString || 'error');
+ break;
+ default:
+ throw new Error('Derivation path algorithm error');
+ }
+ }
+ } catch (error) {
+ console.error(`generateSignature error - ${error}`);
+ logger.error(error instanceof Error ? error.message : 'Unknown error');
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [extendedKeys],
+ );
+
+ return {
+ generateSignature,
+ signature,
+ selectedAlgorithm,
+ isLoading,
+ setSignature,
+ setSelectedAlgorithm,
+ };
+};
diff --git a/packages/shared/index.ts b/packages/shared/index.ts
index ae0985e7..0a435321 100644
--- a/packages/shared/index.ts
+++ b/packages/shared/index.ts
@@ -30,7 +30,6 @@ export type { ButtonProps, LayoutProps, ScanResult, StatusBoxProps } from './com
// Pages
export { ImportExportBasePage } from './pages/csv';
-export { ExtendedKeysBasePage } from './pages/keys';
export { RelayBasePage } from './pages/relay';
export { VaultBasePage } from './pages/accounts/vault';
export { VaultAccountBasePage } from './pages/accounts/vault/[accountId]';
diff --git a/packages/shared/lib/getLogger.ts b/packages/shared/lib/getLogger.ts
index 9faa6332..7d8548cb 100644
--- a/packages/shared/lib/getLogger.ts
+++ b/packages/shared/lib/getLogger.ts
@@ -1,7 +1,55 @@
import log from 'electron-log';
+import path from 'path';
+import fs from 'fs';
+import os from 'os';
export type LoggerName = 'utility' | 'relay' | 'shared';
+const formatDate = (): string => {
+ 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 appName = process.env.NODE_ENV === 'development' ? '@fireblocks/recovery-utility' : 'Fireblocks Recovery Utility';
+ switch (os.platform()) {
+ case 'darwin':
+ return path.join(os.homedir(), 'Library', 'Logs', appName);
+ case 'linux':
+ return path.join(os.homedir(), '.config', appName, 'logs');
+ case 'win32':
+ return path.join(process.env.USERPROFILE || os.homedir(), 'AppData', 'Roaming', appName, 'logs');
+ default:
+ throw new Error('Unsupported platform');
+ }
+};
+
+const sessionFolder = path.join(getLogBaseDir(), formatDate());
+const baseLogDir = getLogBaseDir();
+if (!fs.existsSync(sessionFolder)) {
+ fs.mkdirSync(sessionFolder, { recursive: true });
+
+ // Limit to 7 daily folders, delete oldest if needed
+ const folders = fs
+ .readdirSync(baseLogDir)
+ .filter((f) => /^\d{4}_\d{2}_\d{2}$/.test(f))
+ .map((f) => ({ name: f }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+ if (folders.length > 7) {
+ const toDelete = folders.slice(0, folders.length - 7);
+ toDelete.forEach(({ name }) => {
+ try {
+ fs.rmSync(path.join(baseLogDir, name), { recursive: true, force: true });
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ }
+}
+
+log.transports.file.resolvePath = (variables) => path.join(sessionFolder, variables.fileName ?? 'default.log');
+
export interface CustomElectronLogger extends log.ElectronLog {
logSigningTx: (chain: string, value: unknown) => void;
logPreparedData: (chain: string, value: unknown) => void;
@@ -17,6 +65,7 @@ export const getLogger: (name: LoggerName) => CustomElectronLogger = (name: Logg
const logger = log.create(name) as CustomElectronLogger;
logger.transports.file.fileName = `${name}.log`;
+ logger.transports.file.resolvePath = log.transports.file.resolvePath; // today's folder
logger.transports.file.maxSize = 1024 * 1024 * 10; // 10MB
logger.transports.file.level = 'debug';
logger.catchErrors({
diff --git a/packages/shared/lib/relayUrl.ts b/packages/shared/lib/relayUrl.ts
index ce9beb32..2e20bed7 100644
--- a/packages/shared/lib/relayUrl.ts
+++ b/packages/shared/lib/relayUrl.ts
@@ -7,9 +7,11 @@ import {
relayCreateTxRequestParams,
relayBroadcastTxRequestParams,
relaySignTxResponseParams,
+ relayRawSignTxResponseParams,
RelayParams,
RelayRequestParams,
RelayResponseParams,
+ relayRawSignTxRequestParams,
} from '../schemas/relayUrl';
import { getLogger } from './getLogger';
import { LOGGER_NAME_SHARED } from '../constants';
@@ -34,13 +36,24 @@ const getSchema = (action: RelayParams['action'], app: 'utility' | 'relay') => {
return relayCreateTxRequestParams;
case 'tx/broadcast':
return relayBroadcastTxRequestParams;
+ case 'tx/raw-sign':
+ return relayRawSignTxRequestParams;
default:
throw error;
}
}
- if (app === 'utility' && action === 'tx/sign') {
- return relaySignTxResponseParams;
+ if (app === 'utility') {
+ switch (action) {
+ case 'tx/sign':
+ return relaySignTxResponseParams;
+
+ case 'tx/broadcast-raw-sign':
+ return relayRawSignTxResponseParams;
+
+ default:
+ throw error;
+ }
}
throw error;
diff --git a/packages/shared/lib/validateAddress.ts b/packages/shared/lib/validateAddress.ts
index 7b48d5ca..a9986657 100644
--- a/packages/shared/lib/validateAddress.ts
+++ b/packages/shared/lib/validateAddress.ts
@@ -31,7 +31,7 @@ export class AddressValidator {
public isValidAddress(address: string, networkProtocol: string | undefined, assetId: string): boolean {
try {
- this.patchValidator();
+ // this.patchValidator();
let isValid = false;
let validationOptions: WAValidator.ValidateOpts = {};
let asset = assetId;
diff --git a/packages/shared/pages/accounts/vault/index.tsx b/packages/shared/pages/accounts/vault/index.tsx
index 6c42eda8..02d231e9 100644
--- a/packages/shared/pages/accounts/vault/index.tsx
+++ b/packages/shared/pages/accounts/vault/index.tsx
@@ -4,7 +4,7 @@ import { Box, Grid, Typography, Breadcrumbs } from '@mui/material';
import { GridToolbarQuickFilter, GridActionsCellItem, GridColDef, GridRowsProp } from '@mui/x-data-grid';
import { Add } from '@mui/icons-material';
import { VaultAccount } from '../../../types';
-import { ExtendedKeys } from '../../../schemas';
+import { KeysetKeys } from '../../../schemas';
import { RecoverAccountModal } from '../../../components/Modals/RecoverAccountModal';
import { Button } from '../../../components/Button';
import { DataGrid } from '../../../components/DataGrid';
@@ -54,9 +54,12 @@ function GridToolbar({ onClickAddAccount }: GridToolbarProps) {
}
type Props = {
- extendedKeys?: ExtendedKeys;
+ extendedKeys?: KeysetKeys;
accounts: Map;
- addAccount: (name: string, id?: number) => number;
+ addAccount: (name: string, id?: number, mapToNextKeyset?: boolean, ecdsa?: boolean) => number;
+ currentKeysetIndex?: [number, number];
+ totalKeysetCount?: [number, number];
+ hasMultipleKeysets?: boolean;
withdrawModal?: ComponentType<{
accountId: number;
assetId?: string;
@@ -65,7 +68,15 @@ type Props = {
}>;
};
-export const VaultBasePage = ({ extendedKeys, accounts, addAccount, withdrawModal: WithdrawModal }: Props) => {
+export const VaultBasePage = ({
+ currentKeysetIndex,
+ totalKeysetCount,
+ hasMultipleKeysets,
+ extendedKeys,
+ accounts,
+ addAccount,
+ withdrawModal: WithdrawModal,
+}: Props) => {
const router = useRouter();
const [isRecoverAccountModalOpen, setIsRecoverAccountModalOpen] = useWrappedState(
@@ -236,6 +247,9 @@ export const VaultBasePage = ({ extendedKeys, accounts, addAccount, withdrawModa
accountsKeys={Array.from(accounts.keys())}
onClose={handleCloseRecoverAccountModal}
addAccount={addAccount}
+ totalKeysetCount={totalKeysetCount}
+ hasMultipleKeysets={hasMultipleKeysets || false}
+ currentKeysetIndex={currentKeysetIndex}
/>
{!!WithdrawModal && (
;
importCsv: (addressesCsv?: File, balancesCsv?: File) => Promise;
};
diff --git a/packages/shared/pages/keys.tsx b/packages/shared/pages/keys.tsx
deleted file mode 100644
index 516586ab..00000000
--- a/packages/shared/pages/keys.tsx
+++ /dev/null
@@ -1,218 +0,0 @@
-import React, { ChangeEvent, useState } from 'react';
-import { useRouter } from 'next/router';
-import { Box, Grid, Typography, InputAdornment, Checkbox, FormControlLabel } from '@mui/material';
-import { CheckCircle } from '@mui/icons-material';
-import { useForm } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { z } from 'zod';
-import { monospaceFontFamily } from '../theme';
-import { Link } from '../components/Link';
-import { TextField, Button } from '../components';
-import { extendedKeys as extendedKeysInput } from '../schemas';
-import { getPubsFromPrvs } from '@fireblocks/extended-key-recovery';
-
-type FormData = z.infer;
-
-type Props = {
- supportsPrivateKeys: boolean;
- extendedKeys?: FormData;
- setExtendedKeys: (data: FormData) => void;
-};
-
-export const ExtendedKeysBasePage = ({ supportsPrivateKeys, extendedKeys, setExtendedKeys }: Props) => {
- const router = useRouter();
-
- const [manualxPrvfPrvInput, setManulxPrvfPrvInput] = useState(false);
-
- const hasExtendedPublicKeys = !!extendedKeys?.xpub || !!extendedKeys?.fpub;
- const hasExtendedPrivateKeys = !!extendedKeys?.xprv || !!extendedKeys?.fprv;
- const showPrivate = supportsPrivateKeys && hasExtendedPrivateKeys;
-
- const {
- register,
- handleSubmit,
- setValue,
- formState: { errors },
- } = useForm({
- resolver: zodResolver(extendedKeysInput),
- defaultValues: {
- xpub: extendedKeys?.xpub,
- fpub: extendedKeys?.fpub,
- ...(supportsPrivateKeys && {
- xprv: extendedKeys?.xprv,
- fprv: extendedKeys?.fprv,
- }),
- },
- });
-
- const handleKeyInput = (type: 'x' | 'f') => (event: ChangeEvent) => {
- const keys = getPubsFromPrvs(type === 'x' ? event.target.value : undefined, type === 'f' ? event.target.value : undefined);
- if (type === 'x') {
- setValue('xpub', keys[0]);
- setValue('xprv', event.target.value);
- } else {
- setValue('fpub', keys[1]);
- setValue('fprv', event.target.value);
- }
- };
-
- console.info({ errors });
-
- const onSubmit = (data: FormData) => {
- setExtendedKeys(data);
-
- router.push('/accounts/vault');
- };
-
- 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.
- >
- )}
-
- )}
-
-
-
- Public Keys
-
-
-
-
-
- Valid
-
- ) : null
- }
- {...register('xpub')}
- />
-
-
-
-
- Valid
-
- ) : null
- }
- {...register('fpub')}
- />
-
- {(hasExtendedPrivateKeys || manualxPrvfPrvInput) && (
- <>
-
-
- Private Keys
-
-
-
-
-
-
-
-
- >
- )}
-
-
- {!(extendedKeys?.fprv && extendedKeys?.xprv) && (
-
- setManulxPrvfPrvInput(!manualxPrvfPrvInput)} />}
- label='Manually provide xprv and fprv'
- />
-
- )}
-
-
-
-
-
- );
-};
diff --git a/packages/shared/reducers/rawSignReducer.ts b/packages/shared/reducers/rawSignReducer.ts
new file mode 100644
index 00000000..f1684075
--- /dev/null
+++ b/packages/shared/reducers/rawSignReducer.ts
@@ -0,0 +1,76 @@
+import { HDPath } from '@fireblocks/wallet-derivation';
+import { SigningWallet } from '../../../apps/recovery-utility/renderer/lib/wallets/SigningWallet';
+
+export enum RawSignMethod {
+ ACCOUNT = 'account',
+ DERIVATION_PATH = 'derivationPath',
+}
+
+export enum SigningAlgorithms {
+ ECDSA = 'ECDSA',
+ EDDSA = 'EDDSA',
+}
+
+export type SigningWalletWithSign = SigningWallet & {
+ sign?: (message: string | Uint8Array, hasher?: (...msgs: Uint8Array[]) => Promise) => Promise;
+};
+
+export type RawSignState = {
+ rawSignMethod: RawSignMethod;
+ selectedAccountId: number | null;
+ selectedWallet: SigningWalletWithSign | null;
+ inputChangeIndex: number;
+ inputAdressIndex: number;
+ derivationPath: HDPath;
+ dpAlgorithm: SigningAlgorithms;
+ unsignedTx: string | null;
+ signedTx: string | null;
+ qrData: string | null;
+};
+
+export type RawSignAction =
+ | { type: 'SET_RAW_SIGN_METHOD'; payload: RawSignMethod }
+ | { type: 'SET_SELECTED_ACCOUNT_ID'; payload: number | null }
+ | { type: 'SET_SELECTED_WALLET'; payload: SigningWalletWithSign | null }
+ | { type: 'SET_INPUT_CHANGE_INDEX'; payload: number }
+ | { type: 'SET_INPUT_ADDRESS_INDEX'; payload: number }
+ | { type: 'SET_DERIVATION_PATH'; payload: HDPath }
+ | { type: 'SET_DP_ALGORITHM'; payload: SigningAlgorithms }
+ | { type: 'SET_UNSIGNED_TX'; payload: string | null }
+ | { type: 'SET_SIGNED_TX'; payload: string | null }
+ | { type: 'SET_QR_DATA'; payload: string | null }
+ | { type: 'RESET_WALLET_STATE' };
+
+export const rawSignReducer = (state: RawSignState, action: RawSignAction): RawSignState => {
+ switch (action.type) {
+ case 'SET_RAW_SIGN_METHOD':
+ return { ...state, rawSignMethod: action.payload };
+ case 'SET_SELECTED_ACCOUNT_ID':
+ return { ...state, selectedAccountId: action.payload };
+ case 'SET_SELECTED_WALLET':
+ return { ...state, selectedWallet: action.payload };
+ case 'SET_INPUT_CHANGE_INDEX':
+ return { ...state, inputChangeIndex: action.payload };
+ case 'SET_INPUT_ADDRESS_INDEX':
+ return { ...state, inputAdressIndex: action.payload };
+ case 'SET_DERIVATION_PATH':
+ return { ...state, derivationPath: action.payload };
+ case 'SET_DP_ALGORITHM':
+ return { ...state, dpAlgorithm: action.payload };
+ case 'SET_UNSIGNED_TX':
+ return { ...state, unsignedTx: action.payload };
+ case 'SET_SIGNED_TX':
+ return { ...state, signedTx: action.payload };
+ case 'SET_QR_DATA':
+ return { ...state, qrData: action.payload };
+ case 'RESET_WALLET_STATE':
+ return {
+ ...state,
+ signedTx: null,
+ inputChangeIndex: 0,
+ inputAdressIndex: 0,
+ };
+ default:
+ return state;
+ }
+};
diff --git a/packages/shared/schemas/extendedKeys.ts b/packages/shared/schemas/extendedKeys.ts
index 321a7ce1..7f2204c4 100644
--- a/packages/shared/schemas/extendedKeys.ts
+++ b/packages/shared/schemas/extendedKeys.ts
@@ -1,5 +1,8 @@
+/* eslint-disable no-continue */
+/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { WalletMaster } from '@fireblocks/extended-key-recovery';
+import { RecoveredKey } from '@fireblocks/extended-key-recovery/src/types';
import { z } from 'zod';
export const getExtendedKeySchema = (prefix: 'xpub' | 'xprv' | 'fpub' | 'fprv') =>
@@ -9,27 +12,78 @@ export const getExtendedKeySchema = (prefix: 'xpub' | 'xprv' | 'fpub' | 'fprv')
.trim()
.regex(new RegExp(`${prefix}[0-9a-zA-Z]{107}$`), `Invalid ${prefix}`);
-export const extendedKeys = z
+const keysetMapEntry = z.custom(
+ (val: unknown): val is RecoveredKey =>
+ typeof val === 'object' &&
+ val !== null &&
+ 'ecdsaExists' in val &&
+ 'eddsaExists' in val &&
+ 'ecdsaMinAccount' in val &&
+ 'eddsaMinAccount' in val &&
+ (!val.ecdsaExists || 'xpub' in val) &&
+ (!val.eddsaExists || 'fpub' in val),
+);
+
+export const keysetKeys = z.object({
+ xpub: getExtendedKeySchema('xpub').optional(),
+ fpub: getExtendedKeySchema('fpub').optional(),
+ xprv: getExtendedKeySchema('xprv').optional(),
+ fprv: getExtendedKeySchema('fprv').optional(),
+});
+
+const ncwMasterPart = z.object({
+ ncwMaster: z
+ .custom((val) =>
+ typeof val === 'object'
+ ? // @ts-ignore
+ val.walletSeed &&
+ // @ts-ignore
+ val.assetSeed &&
+ // @ts-ignore
+ typeof val.masterKeyForCosigner === 'object' &&
+ // @ts-ignore
+ Object.keys(val.masterKeyForCosigner).every((v) => typeof v === 'string')
+ : false,
+ )
+ .optional(),
+});
+
+const keysetMap = z.record(z.number(), keysetMapEntry);
+
+export const utilityExtendedKeys = z.intersection(ncwMasterPart, keysetMap).refine((data) => {
+ let allHaveMinimumRequiredFields = false;
+ for (const key in Object.keys(data)) {
+ if (key === 'ncwMaster') {
+ continue;
+ }
+
+ const keyset = data[key] as RecoveredKey;
+ if (!keyset.ecdsaExists && !keyset.eddsaExists) {
+ continue;
+ }
+
+ if (keyset.ecdsaExists) {
+ allHaveMinimumRequiredFields = allHaveMinimumRequiredFields && (keyset.xprv || keyset.xpub) && keyset.ecdsaMinAccount >= 0;
+ }
+ if (keyset.eddsaExists) {
+ allHaveMinimumRequiredFields = allHaveMinimumRequiredFields && (keyset.fprv || keyset.fpub) && keyset.eddsaMinAccount >= 0;
+ }
+
+ if (!allHaveMinimumRequiredFields) {
+ return false;
+ }
+ }
+ return true;
+}, 'At least one extended key is required');
+
+export const relayExtendedKeys = z
.object({
xpub: getExtendedKeySchema('xpub').optional(),
fpub: getExtendedKeySchema('fpub').optional(),
- xprv: getExtendedKeySchema('xprv').optional(),
- fprv: getExtendedKeySchema('fprv').optional(),
- ncwMaster: z
- .custom((val) =>
- typeof val === 'object'
- ? // @ts-ignore
- val.walletSeed &&
- // @ts-ignore
- val.assetSeed &&
- // @ts-ignore
- typeof val.masterKeyForCosigner === 'object' &&
- // @ts-ignore
- Object.keys(val.masterKeyForCosigner).every((v) => typeof v === 'string')
- : false,
- )
- .optional(),
})
- .refine((data) => data.xpub || data.fpub || data.xprv || data.fprv, 'At least one extended key is required');
+ .refine((data) => data.xpub || data.fpub, 'At least one extended key is required');
-export type ExtendedKeys = z.infer;
+export type KeysetMap = z.infer;
+export type UtilityExtendedKeys = z.infer;
+export type RelayExtendedKeys = z.infer;
+export type KeysetKeys = z.infer;
diff --git a/packages/shared/schemas/recoverAccountInput.ts b/packages/shared/schemas/recoverAccountInput.ts
index 22b56cfe..e6c029fa 100644
--- a/packages/shared/schemas/recoverAccountInput.ts
+++ b/packages/shared/schemas/recoverAccountInput.ts
@@ -12,8 +12,16 @@ const numberFromString = z
.transform((v) => (v === null ? -1 : Number(v)))
.pipe(z.number().nullable());
+const booleanFromYNString = z
+ .string()
+ .nullable()
+ .transform((v) => (v === null ? false : v === 'yes'))
+ .nullable();
+
export const recoverAccountInput = z.object({
name: nonEmptyString('Account name is required').describe('Vault Account name'),
+ incrementEcdsaKeyset: booleanFromYNString,
+ incrementEddsaKeyset: booleanFromYNString,
id: numberFromString,
});
@@ -22,6 +30,8 @@ export const recoverAccountInputByAccounts = (accountKeys: number[]) =>
.object({
name: nonEmptyString('Account name is required').describe('Vault Account name'),
id: numberFromString,
+ incrementEcdsaKeyset: booleanFromYNString,
+ incrementEddsaKeyset: booleanFromYNString,
})
.superRefine((data, ctx) => {
if (data.id !== null && accountKeys.includes(data.id)) {
diff --git a/packages/shared/schemas/recoverKeysInput.ts b/packages/shared/schemas/recoverKeysInput.ts
index f4a0089b..9ce38aa3 100644
--- a/packages/shared/schemas/recoverKeysInput.ts
+++ b/packages/shared/schemas/recoverKeysInput.ts
@@ -10,6 +10,13 @@ export const recoverKeysInput = z.object({
agpRsaKey: z.string().optional().describe('The auto generated passphrase Base64 encoded RSA file'),
agpRsaPassphrase: z.string().optional().describe('The auto generated passphrase RSA passphrase'),
recoverOnlyNCW: z.boolean().describe('Should recover only NCW').default(false),
+ passphraseJson: z
+ .object({
+ passphrase: z.string().optional(),
+ rsaKeyPassphrase: z.string().optional(),
+ })
+ .nullable()
+ .describe('JSON object containing passphrases'),
});
export type RecoverKeysInput = z.infer;
diff --git a/packages/shared/schemas/relayUrl.ts b/packages/shared/schemas/relayUrl.ts
index 28c181a2..81cd13dd 100644
--- a/packages/shared/schemas/relayUrl.ts
+++ b/packages/shared/schemas/relayUrl.ts
@@ -76,6 +76,26 @@ export const relayBroadcastTxRequestParams = relayBaseRequestParams.extend({
/** Request transaction broadcast from Recovery Relay */
export type RelayBroadcastTxRequestParams = z.infer;
+export const relayRawSignTxRequestParams = relayBaseRequestParams.extend({
+ action: actionSchema('tx/raw-sign'),
+ algorithm: z.enum(['ECDSA', 'EDDSA']),
+ derivationPath: z.object({
+ coinType: z.number().nonnegative(),
+ account: z.number().nonnegative(),
+ changeIndex: z.number().nonnegative(),
+ addressIndex: z.number().nonnegative(),
+ }),
+ message: z.instanceof(Uint8Array),
+ accountId: z.number().optional(),
+ version: z.string().optional(),
+ platform: z.string().optional(),
+ xpub: z.string().optional(),
+ fpub: z.string().optional(),
+});
+
+/** creating unsigned message on Relay, sending to Utility */
+export type RelayRawSignTxRequestParams = z.infer;
+
// Responses (Relay -> Utility)
const relayBaseResponseParams = z.object({
@@ -124,6 +144,17 @@ export const relaySignTxResponseParams = relayBaseResponseParams.extend({
/** Respond to Recovery Utility with unsigned transaction for signing */
export type RelaySignTxResponseParams = z.infer;
+export const relayRawSignTxResponseParams = relayBaseResponseParams.extend({
+ action: actionSchema('tx/broadcast-raw-sign'),
+ signature: z.string(),
+ algorithm: z.enum(['ECDSA', 'EDDSA']),
+ accountId: z.number().optional(),
+ version: z.string().optional(),
+});
+
+/** singing message on Utility, sending back to Relay */
+export type RelayRawSignTxResponseParams = z.infer;
+
// export const relayBalancesResponseParams = relayBaseResponseParams.extend({
// action: actionSchema('balances/update'),
// balances: z.array(z.tuple([z.string(), z.number().nonnegative()])),
@@ -133,10 +164,14 @@ export type RelaySignTxResponseParams = z.infer;
/** Relay URL parameters for sending data to Recovery Relay */
-export type RelayRequestParams = RelayImportRequestParams | RelayCreateTxRequestParams | RelayBroadcastTxRequestParams;
+export type RelayRequestParams =
+ | RelayImportRequestParams
+ | RelayCreateTxRequestParams
+ | RelayBroadcastTxRequestParams
+ | RelayRawSignTxRequestParams;
/** Relay URL parameters for sending data to Recovery Utility */
-export type RelayResponseParams = RelaySignTxResponseParams; // | RelayBalancesResponseParams;
+export type RelayResponseParams = RelaySignTxResponseParams | RelayRawSignTxResponseParams; // | RelayBalancesResponseParams;
/** Relay URL parameters */
export type RelayParams = RelayRequestParams | RelayResponseParams;
diff --git a/packages/shared/schemas/settingsInput.ts b/packages/shared/schemas/settingsInput.ts
index f0389a1e..40776545 100644
--- a/packages/shared/schemas/settingsInput.ts
+++ b/packages/shared/schemas/settingsInput.ts
@@ -10,6 +10,8 @@ export const settingsInput = {
url: z.string().nullish(),
enabled: z.boolean().default(true),
allowedEmptyValue: z.oboolean().default(false),
+ apiKey: z.string().nullish(),
+ requiresApiKey: z.boolean().default(false).optional(),
})
.refine(
(values) =>
diff --git a/packages/wallet-derivation/types.ts b/packages/wallet-derivation/types.ts
index c99d5c0a..0e890709 100644
--- a/packages/wallet-derivation/types.ts
+++ b/packages/wallet-derivation/types.ts
@@ -1,3 +1,5 @@
+import { RelayExtendedKeys, UtilityExtendedKeys } from '@fireblocks/recovery-shared';
+
export type Algorithm = 'ECDSA' | 'EDDSA';
export type HDPath = Readonly<{
@@ -11,15 +13,8 @@ export type HDPathParts = [44, number, number, number, number];
export type HDPathInput = Partial;
-export type ExtendedKeys = {
- xprv?: string;
- fprv?: string;
- xpub?: string;
- fpub?: string;
-};
-
export type Input = Readonly<
- ExtendedKeys & {
+ (UtilityExtendedKeys | RelayExtendedKeys) & {
assetId: string;
path: HDPathInput;
isTestnet?: boolean;
diff --git a/packages/wallet-derivation/wallets/BaseWallet.ts b/packages/wallet-derivation/wallets/BaseWallet.ts
index cfd9a04e..a9367288 100644
--- a/packages/wallet-derivation/wallets/BaseWallet.ts
+++ b/packages/wallet-derivation/wallets/BaseWallet.ts
@@ -1,5 +1,6 @@
import { getLogger } from '@fireblocks/recovery-shared';
import { LOGGER_NAME_RELAY, LOGGER_NAME_SHARED, LOGGER_NAME_UTILITY } from '@fireblocks/recovery-shared/constants';
+import { RecoveredKey } from '@fireblocks/extended-key-recovery/src/types';
import { Algorithm, HDPath, HDPathParts, Input, KeyDerivation, Derivation } from '../types';
export abstract class BaseWallet implements Derivation {
@@ -43,7 +44,9 @@ export abstract class BaseWallet implements Derivation {
public isLegacy: boolean;
public relayLogger = getLogger(LOGGER_NAME_RELAY);
+
public utilityLogger = getLogger(LOGGER_NAME_UTILITY);
+
public sharedLogger = getLogger(LOGGER_NAME_SHARED);
constructor(input: Input, defaultCoinType: number, algorithm: Algorithm) {
@@ -63,15 +66,44 @@ export abstract class BaseWallet implements Derivation {
this.type = this.path.addressIndex > 0 ? 'Deposit' : 'Permanent';
- const isXprvDerivation = typeof input.xprv === 'string' || typeof input.fprv === 'string';
-
const isEdDSA = algorithm === 'EDDSA';
+ let key;
+
+ if ('xpub' in input) {
+ // Only in relay
+ const { xpub, fpub } = input;
+ key = { xprv: undefined, fprv: undefined, xpub, fpub };
+ } else {
+ const keysetWithRecoveredKey = Object.entries(input)
+ .filter(([keysetId]) => !['ncwMaster', 'assetId', 'path', 'isTestnet', 'isLegacy'].includes(keysetId))
+ .sort(([keysetId1], [keysetId2]) => Number(keysetId1) - Number(keysetId2))
+ .findLast(([, value]) => {
+ const recoveredKey = value as RecoveredKey;
+ if (isEdDSA && recoveredKey.eddsaExists) {
+ return recoveredKey.eddsaMinAccount !== -1 && recoveredKey.eddsaMinAccount <= this.path.account;
+ }
+ if (!isEdDSA && recoveredKey.ecdsaExists) {
+ return recoveredKey.ecdsaMinAccount !== -1 && recoveredKey.ecdsaMinAccount <= this.path.account;
+ }
+
+ return false;
+ }) as [string, RecoveredKey | undefined];
+ if (keysetWithRecoveredKey[1] === undefined) {
+ // This should technically never happen
+ throw new Error('No keysets defined for minimal account');
+ }
+ const { xprv, fprv, xpub, fpub } = keysetWithRecoveredKey[1];
+ key = { xprv, fprv, xpub, fpub };
+ }
+
+ const isXprvDerivation = typeof key.xprv === 'string' || typeof key.fprv === 'string';
+
const xprvKey = isEdDSA ? 'fprv' : 'xprv';
const xpubKey = isEdDSA ? 'fpub' : 'xpub';
- const extendedKey = isXprvDerivation ? input[xprvKey] : input[xpubKey];
+ const extendedKey = isXprvDerivation ? key[xprvKey] : key[xpubKey];
if (!extendedKey) {
throw new Error(`${algorithm} extended key is required (${xprvKey} or ${xpubKey})`);
diff --git a/packages/wallet-derivation/wallets/ECDSAWallet.ts b/packages/wallet-derivation/wallets/ECDSAWallet.ts
index 7a9422b5..563931f1 100644
--- a/packages/wallet-derivation/wallets/ECDSAWallet.ts
+++ b/packages/wallet-derivation/wallets/ECDSAWallet.ts
@@ -1,4 +1,4 @@
-import { sha256, encodeBase58, HDNodeWallet } from 'ethers';
+import { sha256, encodeBase58, HDNodeWallet, getBytes, SigningKey, concat } from 'ethers';
import { Input, KeyDerivation } from '../types';
import { BaseWallet } from './BaseWallet';
@@ -41,4 +41,20 @@ export abstract class ECDSAWallet extends BaseWallet {
wif,
};
}
+
+ protected async sign(message: Uint8Array | string) {
+ if (!this.privateKey) {
+ throw new Error('Cannot sign without a derived private key');
+ }
+
+ const signingKey = new SigningKey(this.privateKey);
+
+ const signatureObj = signingKey.sign(message);
+
+ const r = getBytes(signatureObj.r);
+ const s = getBytes(signatureObj.s);
+ const v = new Uint8Array([signatureObj.v]);
+
+ return concat([r, s, v]);
+ }
}
diff --git a/packages/wallet-derivation/wallets/EdDSAWallet.ts b/packages/wallet-derivation/wallets/EdDSAWallet.ts
index cea8d2e6..402b0369 100644
--- a/packages/wallet-derivation/wallets/EdDSAWallet.ts
+++ b/packages/wallet-derivation/wallets/EdDSAWallet.ts
@@ -7,6 +7,7 @@ import { ExtendedPoint, CURVE as edCURVE, etc } from '@noble/ed25519';
import { bytesToNumberLE, concatBytes, numberToBytesBE, numberToBytesLE, hexToNumber } from '@noble/curves/abstract/utils';
import { Input, KeyDerivation } from '../types';
import { BaseWallet } from './BaseWallet';
+import { hexToBytes } from '@noble/hashes/utils';
/**
* Get a SHA-512 digest of concatenated byte array messages.
@@ -14,7 +15,7 @@ import { BaseWallet } from './BaseWallet';
* @param messages list of byte arrays
* @returns byte array of SHA-512 digest
*/
-const sha512 = async (...messages: Uint8Array[]) => {
+export const sha512 = async (...messages: Uint8Array[]) => {
const buffer = concatBytes(...messages);
const digest = await crypto.subtle.digest('SHA-512', buffer);
@@ -45,7 +46,14 @@ const _0n = 0n;
* @param number number
* @returns 4-byte long byte array in big-endian order
*/
-const numberTo4BytesBE = (number: number) => Buffer.from([number >> 24, number >> 16, number >> 8, number]);
+const numberTo4BytesBE = (n: number): Uint8Array => {
+ const arr = new Uint8Array(4);
+ arr[0] = (n >> 24) & 0xff;
+ arr[1] = (n >> 16) & 0xff;
+ arr[2] = (n >> 8) & 0xff;
+ arr[3] = n & 0xff;
+ return arr;
+};
const flatten = (p: ExtendedPoint): ExtendedPoint => ExtendedPoint.fromAffine(p.toAffine());
@@ -56,8 +64,8 @@ export abstract class EdDSAWallet extends BaseWallet {
private static hashForDerive(pubKey: ExtendedPoint, chainCode: Uint8Array, idx: number) {
const hmac = createHmac('sha512', chainCode);
- hmac.update(Buffer.from(pubKey.toHex(), 'hex'));
- hmac.update(Buffer.from([0x0]));
+ hmac.update(hexToBytes(pubKey.toHex()));
+ hmac.update(Uint8Array.of(0x00));
hmac.update(numberTo4BytesBE(idx));
return hmac.digest();
}
@@ -69,7 +77,7 @@ export abstract class EdDSAWallet extends BaseWallet {
idx: number,
): [ExtendedPoint, bigint, Uint8Array] {
const hash = EdDSAWallet.hashForDerive(pubKey, chainCode, idx);
- const derivedChainCode = hash.subarray(32);
+ const derivedChainCode = new Uint8Array(hash.subarray(32));
const exp = toBigIntBE(hash.subarray(undefined, 32));
const tmpPoint = flatten(ExtendedPoint.BASE.mul(exp % edCURVE.n));
const derivedPubKey = flatten(pubKey.add(tmpPoint));
@@ -141,11 +149,11 @@ export abstract class EdDSAWallet extends BaseWallet {
const privateKeyInt = hexToNumber(this.privateKey.slice(2));
const privateKeyBytes = numberToBytesLE(privateKeyInt, 32);
- const messagesBytes = typeof message === 'string' ? Buffer.from(message, 'hex') : message;
+ const messagesBytes = typeof message === 'string' ? new Uint8Array(Buffer.from(message, 'hex')) : message;
const messageBytes = concatBytes(messagesBytes);
- const seed = randomBytes();
-
+ const seedBuffer = randomBytes();
+ const seed = Buffer.isBuffer(seedBuffer) ? new Uint8Array(seedBuffer) : seedBuffer;
const nonceDigest = await hasher(seed, privateKeyBytes, messageBytes);
const nonce = etc.mod(bytesToNumberLE(nonceDigest), edCURVE.n);
diff --git a/packages/wallet-derivation/wallets/chains/EVM.ts b/packages/wallet-derivation/wallets/chains/EVM.ts
index 1e9268fc..8a65abc1 100644
--- a/packages/wallet-derivation/wallets/chains/EVM.ts
+++ b/packages/wallet-derivation/wallets/chains/EVM.ts
@@ -2,8 +2,8 @@ import { Input } from '../../types';
import { ECDSAWallet } from '../ECDSAWallet';
export class EVMWallet extends ECDSAWallet {
- constructor(input: Input) {
- super(input, 60);
+ constructor(input: Input, chainId: number = 60) {
+ super(input, chainId);
}
protected getAddress(evmAddress?: string) {
diff --git a/scripts/build-cross-platform.sh b/scripts/build-cross-platform.sh
new file mode 100755
index 00000000..535a918d
--- /dev/null
+++ b/scripts/build-cross-platform.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Build script for cross-platform Electron app builds
+# Builds for Linux and Mac on both x64 and arm64 architectures
+
+set -e
+
+echo "Starting cross-platform build..."
+
+# Build the dependencies first
+echo "Building dependencies..."
+yarn turbo run build --filter=!@fireblocks/e2e-tests --filter=!@fireblocks/recovery-utility
+
+# Navigate to the Electron app directory
+cd apps/recovery-utility
+
+# Clean previous builds
+echo "Cleaning previous builds..."
+rm -rf dist
+
+# Build for all platforms and architectures in a single command
+# This prevents electron-builder from cleaning between builds
+echo "Building for all platforms and architectures..."
+yarn nextron build --mac --linux --x64 --arm64
+
+# Return to root directory
+cd ../..
+
+echo "Cross-platform build completed!"
+echo "Build artifacts can be found in apps/recovery-utility/dist/"
+ls -la apps/recovery-utility/dist/
\ No newline at end of file
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
diff --git a/yarn.lock b/yarn.lock
index fc1a81e1..e850c45c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1225,6 +1225,11 @@
dependencies:
regenerator-runtime "^0.14.0"
+"@babel/runtime@^7.25.0":
+ version "7.27.6"
+ resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6"
+ integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==
+
"@babel/template@^7.22.15", "@babel/template@^7.23.9", "@babel/template@^7.3.3":
version "7.23.9"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a"
@@ -3122,6 +3127,13 @@
dependencies:
"@noble/hashes" "1.3.3"
+"@noble/curves@^1.4.2":
+ version "1.9.2"
+ resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz#73388356ce733922396214a933ff7c95afcef911"
+ integrity sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==
+ dependencies:
+ "@noble/hashes" "1.8.0"
+
"@noble/ed25519@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-2.0.0.tgz#5964c8190a4b4b804985717ca566113b93379e43"
@@ -3142,6 +3154,11 @@
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
+"@noble/hashes@1.8.0", "@noble/hashes@^1.4.0":
+ version "1.8.0"
+ resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a"
+ integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -3254,9 +3271,9 @@
rxjs "^7.8.1"
tslib "^2.5.3"
-"@polkadot/keyring@^12.2.1", "@polkadot/keyring@^12.3.1":
+"@polkadot/keyring@12.3.2", "@polkadot/keyring@^12.2.1", "@polkadot/keyring@^12.3.1":
version "12.3.2"
- resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.3.2.tgz#112a0c28816a1f47edad6260dc94222c29465a54"
+ resolved "https://registry.npmjs.org/@polkadot/keyring/-/keyring-12.3.2.tgz#112a0c28816a1f47edad6260dc94222c29465a54"
integrity sha512-NTdtDeI0DP9l/45hXynNABeP5VB8piw5YR+CbUxK2e36xpJWVXwbcOepzslg5ghE9rs8UKJb30Z/HqTU4sBY0Q==
dependencies:
"@polkadot/util" "12.3.2"
@@ -3440,9 +3457,9 @@
"@polkadot/wasm-util" "7.2.1"
tslib "^2.5.0"
-"@polkadot/wasm-crypto@^7.2.1":
+"@polkadot/wasm-crypto@7.2.1", "@polkadot/wasm-crypto@^7.2.1":
version "7.2.1"
- resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.1.tgz#db671dcb73f1646dc13478b5ffc3be18c64babe1"
+ resolved "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-7.2.1.tgz#db671dcb73f1646dc13478b5ffc3be18c64babe1"
integrity sha512-SA2+33S9TAwGhniKgztVN6pxUKpGfN4Tre/eUZGUfpgRkT92wIUT2GpGWQE+fCCqGQgADrNiBcwt6XwdPqMQ4Q==
dependencies:
"@polkadot/wasm-bridge" "7.2.1"
@@ -3467,9 +3484,9 @@
"@polkadot/x-global" "12.3.2"
tslib "^2.5.3"
-"@polkadot/x-fetch@^12.3.1":
+"@polkadot/x-fetch@12.3.2", "@polkadot/x-fetch@^12.3.1":
version "12.3.2"
- resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.3.2.tgz#7e8d2113268e792dd5d1b259ef13839c6aa77996"
+ resolved "https://registry.npmjs.org/@polkadot/x-fetch/-/x-fetch-12.3.2.tgz#7e8d2113268e792dd5d1b259ef13839c6aa77996"
integrity sha512-3IEuZ5S+RI/t33NsdPLIIa5COfDCfpUW2sbaByEczn75aD1jLqJZSEDwiBniJ2osyNd4uUxBf6e5jw7LAZeZJg==
dependencies:
"@polkadot/x-global" "12.3.2"
@@ -3507,9 +3524,9 @@
"@polkadot/x-global" "12.3.2"
tslib "^2.5.3"
-"@polkadot/x-ws@^12.3.1":
+"@polkadot/x-ws@12.3.2", "@polkadot/x-ws@^12.3.1":
version "12.3.2"
- resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.3.2.tgz#422559dfbdaac4c965d5e1b406b6cc4529214f94"
+ resolved "https://registry.npmjs.org/@polkadot/x-ws/-/x-ws-12.3.2.tgz#422559dfbdaac4c965d5e1b406b6cc4529214f94"
integrity sha512-yM9Z64pLNlHpJE43+Xtr+iUXmYpFFY5u5hrke2PJt13O48H8f9Vb9cRaIh94appLyICoS0aekGhDkGH+MCspBA==
dependencies:
"@polkadot/x-global" "12.3.2"
@@ -3657,13 +3674,155 @@
dependencies:
"@sinonjs/commons" "^3.0.0"
-"@solana/buffer-layout@^4.0.1":
+"@solana/buffer-layout-utils@^0.2.0":
+ version "0.2.0"
+ resolved "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca"
+ integrity sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==
+ dependencies:
+ "@solana/buffer-layout" "^4.0.0"
+ "@solana/web3.js" "^1.32.0"
+ bigint-buffer "^1.1.5"
+ bignumber.js "^9.0.1"
+
+"@solana/buffer-layout@^4.0.0", "@solana/buffer-layout@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15"
integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==
dependencies:
buffer "~6.0.3"
+"@solana/codecs-core@2.0.0-rc.1":
+ version "2.0.0-rc.1"
+ resolved "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz#1a2d76b9c7b9e7b7aeb3bd78be81c2ba21e3ce22"
+ integrity sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==
+ dependencies:
+ "@solana/errors" "2.0.0-rc.1"
+
+"@solana/codecs-core@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.1.1.tgz#5d09d7f35b0266789d7c1f9306c08051128a6a64"
+ integrity sha512-iPQW3UZ2Vi7QFBo2r9tw0NubtH8EdrhhmZulx6lC8V5a+qjaxovtM/q/UW2BTNpqqHLfO0tIcLyBLrNH4HTWPg==
+ dependencies:
+ "@solana/errors" "2.1.1"
+
+"@solana/codecs-data-structures@2.0.0-rc.1":
+ version "2.0.0-rc.1"
+ resolved "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz#d47b2363d99fb3d643f5677c97d64a812982b888"
+ integrity sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==
+ dependencies:
+ "@solana/codecs-core" "2.0.0-rc.1"
+ "@solana/codecs-numbers" "2.0.0-rc.1"
+ "@solana/errors" "2.0.0-rc.1"
+
+"@solana/codecs-numbers@2.0.0-rc.1":
+ version "2.0.0-rc.1"
+ resolved "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz#f34978ddf7ea4016af3aaed5f7577c1d9869a614"
+ integrity sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==
+ dependencies:
+ "@solana/codecs-core" "2.0.0-rc.1"
+ "@solana/errors" "2.0.0-rc.1"
+
+"@solana/codecs-numbers@^2.1.0":
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.1.1.tgz#b7a69024e2397e236bbfb11b75ff4a077236b9d2"
+ integrity sha512-m20IUPJhPUmPkHSlZ2iMAjJ7PaYUvlMtFhCQYzm9BEBSI6OCvXTG3GAPpAnSGRBfg5y+QNqqmKn4QHU3B6zzCQ==
+ dependencies:
+ "@solana/codecs-core" "2.1.1"
+ "@solana/errors" "2.1.1"
+
+"@solana/codecs-strings@2.0.0-rc.1":
+ version "2.0.0-rc.1"
+ resolved "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz#e1d9167075b8c5b0b60849f8add69c0f24307018"
+ integrity sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==
+ dependencies:
+ "@solana/codecs-core" "2.0.0-rc.1"
+ "@solana/codecs-numbers" "2.0.0-rc.1"
+ "@solana/errors" "2.0.0-rc.1"
+
+"@solana/codecs@2.0.0-rc.1":
+ version "2.0.0-rc.1"
+ resolved "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz#146dc5db58bd3c28e04b4c805e6096c2d2a0a875"
+ integrity sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==
+ dependencies:
+ "@solana/codecs-core" "2.0.0-rc.1"
+ "@solana/codecs-data-structures" "2.0.0-rc.1"
+ "@solana/codecs-numbers" "2.0.0-rc.1"
+ "@solana/codecs-strings" "2.0.0-rc.1"
+ "@solana/options" "2.0.0-rc.1"
+
+"@solana/errors@2.0.0-rc.1":
+ version "2.0.0-rc.1"
+ resolved "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz#3882120886eab98a37a595b85f81558861b29d62"
+ integrity sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==
+ dependencies:
+ chalk "^5.3.0"
+ commander "^12.1.0"
+
+"@solana/errors@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/@solana/errors/-/errors-2.1.1.tgz#009ebf387b0c014a8fc60a59d65757fef942e4fd"
+ integrity sha512-sj6DaWNbSJFvLzT8UZoabMefQUfSW/8tXK7NTiagsDmh+Q87eyQDDC9L3z+mNmx9b6dEf6z660MOIplDD2nfEw==
+ dependencies:
+ chalk "^5.4.1"
+ commander "^13.1.0"
+
+"@solana/options@2.0.0-rc.1":
+ version "2.0.0-rc.1"
+ resolved "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz#06924ba316dc85791fc46726a51403144a85fc4d"
+ integrity sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==
+ dependencies:
+ "@solana/codecs-core" "2.0.0-rc.1"
+ "@solana/codecs-data-structures" "2.0.0-rc.1"
+ "@solana/codecs-numbers" "2.0.0-rc.1"
+ "@solana/codecs-strings" "2.0.0-rc.1"
+ "@solana/errors" "2.0.0-rc.1"
+
+"@solana/spl-token-group@^0.0.7":
+ version "0.0.7"
+ resolved "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz#83c00f0cd0bda33115468cd28b89d94f8ec1fee4"
+ integrity sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==
+ dependencies:
+ "@solana/codecs" "2.0.0-rc.1"
+
+"@solana/spl-token-metadata@^0.1.6":
+ version "0.1.6"
+ resolved "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz#d240947aed6e7318d637238022a7b0981b32ae80"
+ integrity sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==
+ dependencies:
+ "@solana/codecs" "2.0.0-rc.1"
+
+"@solana/spl-token@^0.4.13":
+ version "0.4.13"
+ resolved "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.13.tgz#8f65c3c2b315e1a00a91b8d0f60922c6eb71de62"
+ integrity sha512-cite/pYWQZZVvLbg5lsodSovbetK/eA24gaR0eeUeMuBAMNrT8XFCwaygKy0N2WSg3gSyjjNpIeAGBAKZaY/1w==
+ dependencies:
+ "@solana/buffer-layout" "^4.0.0"
+ "@solana/buffer-layout-utils" "^0.2.0"
+ "@solana/spl-token-group" "^0.0.7"
+ "@solana/spl-token-metadata" "^0.1.6"
+ buffer "^6.0.3"
+
+"@solana/web3.js@^1.32.0":
+ version "1.98.2"
+ resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.2.tgz#45167a5cfb64436944bf4dc1e8be8482bd6d4c14"
+ integrity sha512-BqVwEG+TaG2yCkBMbD3C4hdpustR4FpuUFRPUmqRZYYlPI9Hg4XMWxHWOWRzHE9Lkc9NDjzXFX7lDXSgzC7R1A==
+ dependencies:
+ "@babel/runtime" "^7.25.0"
+ "@noble/curves" "^1.4.2"
+ "@noble/hashes" "^1.4.0"
+ "@solana/buffer-layout" "^4.0.1"
+ "@solana/codecs-numbers" "^2.1.0"
+ agentkeepalive "^4.5.0"
+ bn.js "^5.2.1"
+ borsh "^0.7.0"
+ bs58 "^4.0.1"
+ buffer "6.0.3"
+ fast-stable-stringify "^1.0.0"
+ jayson "^4.1.1"
+ node-fetch "^2.7.0"
+ rpc-websockets "^9.0.2"
+ superstruct "^2.0.2"
+
"@solana/web3.js@^1.78.0":
version "1.89.1"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.89.1.tgz#52df6820f2d088c4558aa359af40580a03d10ec9"
@@ -3874,6 +4033,13 @@
dependencies:
tslib "^2.4.0"
+"@swc/helpers@^0.5.11":
+ version "0.5.17"
+ resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz#5a7be95ac0f0bf186e7e6e890e7a6f6cda6ce971"
+ integrity sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==
+ dependencies:
+ tslib "^2.8.0"
+
"@szmarczak/http-timer@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
@@ -4566,6 +4732,11 @@
resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.25.tgz#ac92b53e674c3b108decdbe88dc5f444a2f42f6a"
integrity sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==
+"@types/uuid@^8.3.4":
+ version "8.3.4"
+ resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
+ integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
+
"@types/uuid@^9.0.2":
version "9.0.7"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8"
@@ -4590,6 +4761,13 @@
dependencies:
"@types/node" "*"
+"@types/ws@^8.2.2":
+ version "8.18.1"
+ resolved "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9"
+ integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==
+ dependencies:
+ "@types/node" "*"
+
"@types/yargs-parser@*":
version "21.0.3"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
@@ -6303,6 +6481,11 @@ chalk@^2.1.0, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
+chalk@^5.3.0, chalk@^5.4.1:
+ version "5.4.1"
+ resolved "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8"
+ integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==
+
char-regex@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
@@ -6512,6 +6695,16 @@ commander@2.9.0:
dependencies:
graceful-readlink ">= 1.0.0"
+commander@^12.1.0:
+ version "12.1.0"
+ resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
+ integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
+
+commander@^13.1.0:
+ version "13.1.0"
+ resolved "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz#776167db68c78f38dcce1f9b8d7b8b9a488abf46"
+ integrity sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==
+
commander@^2.20.0, commander@^2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -7961,11 +8154,6 @@ eventsource@^1.1.1:
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.2.tgz#bc75ae1c60209e7cb1541231980460343eaea7c2"
integrity sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==
-evm-chains@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/evm-chains/-/evm-chains-0.2.0.tgz#401354aa2b474bb9b34e796d328f281e67d07349"
- integrity sha512-iMpFpmKT9VydfIUpRFoQHNzLdZ3WSNagiq6PABqWDSRwAA9OW+dpKYczkvJ/WSUesGWgcfzYoKRvfusn/9bBYA==
-
execa@5.1.1, execa@^5.0.0, execa@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -9420,6 +9608,24 @@ jayson@^4.1.0:
uuid "^8.3.2"
ws "^7.4.5"
+jayson@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/jayson/-/jayson-4.2.0.tgz#b71762393fa40bc9637eaf734ca6f40d3b8c0c93"
+ integrity sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg==
+ dependencies:
+ "@types/connect" "^3.4.33"
+ "@types/node" "^12.12.54"
+ "@types/ws" "^7.4.4"
+ commander "^2.20.3"
+ delay "^5.0.0"
+ es6-promisify "^5.0.0"
+ eyes "^0.1.8"
+ isomorphic-ws "^4.0.1"
+ json-stringify-safe "^5.0.1"
+ stream-json "^1.9.1"
+ uuid "^8.3.2"
+ ws "^7.5.10"
+
jest-changed-files@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a"
@@ -12263,6 +12469,22 @@ rpc-websockets@^7.5.1:
bufferutil "^4.0.1"
utf-8-validate "^5.0.2"
+rpc-websockets@^9.0.2:
+ version "9.1.1"
+ resolved "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.1.1.tgz#5764336f3623ee1c5cc8653b7335183e3c0c78bd"
+ integrity sha512-1IXGM/TfPT6nfYMIXkJdzn+L4JEsmb0FL1O2OBjaH03V3yuUDdKFulGLMFG6ErV+8pZ5HVC0limve01RyO+saA==
+ dependencies:
+ "@swc/helpers" "^0.5.11"
+ "@types/uuid" "^8.3.4"
+ "@types/ws" "^8.2.2"
+ buffer "^6.0.3"
+ eventemitter3 "^5.0.1"
+ uuid "^8.3.2"
+ ws "^8.5.0"
+ optionalDependencies:
+ bufferutil "^4.0.1"
+ utf-8-validate "^5.0.2"
+
run-async@^2.2.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
@@ -12761,6 +12983,18 @@ stellar-sdk@^10.4.1:
urijs "^1.19.1"
utility-types "^3.7.0"
+stream-chain@^2.2.5:
+ version "2.2.5"
+ resolved "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09"
+ integrity sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==
+
+stream-json@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz#e3fec03e984a503718946c170db7d74556c2a187"
+ integrity sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==
+ dependencies:
+ stream-chain "^2.2.5"
+
stream-transform@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/stream-transform/-/stream-transform-2.1.3.tgz#a1c3ecd72ddbf500aa8d342b0b9df38f5aa598e3"
@@ -12969,6 +13203,11 @@ superstruct@^0.14.2:
resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b"
integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==
+superstruct@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54"
+ integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==
+
supports-color@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -13392,6 +13631,11 @@ tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.5
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
+tslib@^2.8.0:
+ version "2.8.1"
+ resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
@@ -14027,6 +14271,11 @@ ws@^7, ws@^7.4.5, ws@^7.5.9:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
+ws@^7.5.10:
+ version "7.5.10"
+ resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
+ integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
+
ws@^8.13.0, ws@^8.2.2, ws@^8.5.0, ws@^8.8.1:
version "8.16.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"