Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c88ac55
feat: :green_heart: added gitlab ci
a0ngo Jun 3, 2025
b69f244
Merge branch 'update-ci' into 'main'
Jun 3, 2025
d68e656
fix: fixed gitlab ci
a0ngo Jun 3, 2025
9b96863
test: new-version label test
a0ngo Jun 3, 2025
66f01e0
Merge branch 'test-ci' into 'main'
Jun 3, 2025
e18921a
fix: :lock: updated encryption of generated rsa key
a0ngo Jun 4, 2025
4bdf952
Merge Hidden branch into Main
tomer-shoham Sep 18, 2025
3fed138
Merge branch 'hidden' into 'main'
SlavaSereb Sep 18, 2025
5726581
feat: add support for XLM wallet operations and enhance account handling
tomer-shoham Sep 28, 2025
d0a18c6
feat: add support for additional Ethereum testnets and basechain assets
tomer-shoham Sep 28, 2025
a0873d5
chore: bump version numbers
tomer-shoham Sep 28, 2025
1a72a86
feat: add support for XRP wallet operations and enhance asset handling
tomer-shoham Sep 29, 2025
3809e07
fix: update Ethereum Sepolia testnet RPC URL
tomer-shoham Sep 30, 2025
e31d104
chore: update version to 1.8.2 in package.json
tomer-shoham Sep 30, 2025
d27bc73
feat: add API key support for blockchair based assets
tomer-shoham Oct 8, 2025
a09c4e0
feat: integrate API key handling
tomer-shoham Oct 8, 2025
8ea42d9
feat: enhance BTC wallet utils with API key support and refactor methods
tomer-shoham Oct 8, 2025
aa4b525
fix: update the context state immediately
tomer-shoham Oct 8, 2025
be78619
feat: add support for broadcasting transactions via Blockstream on Bi…
tomer-shoham Oct 8, 2025
821a05a
chore: update package version to 1.8.2
tomer-shoham Oct 8, 2025
ec54647
feat: enhance transaction broadcasting with asset ID support and logg…
tomer-shoham Oct 8, 2025
dcdeade
Merge branch 'feat/btc-api-key' into 'main'
SlavaSereb Oct 8, 2025
3033891
fix: blockchair API rate limits
SlavaSereb Oct 12, 2025
3df71ec
chore: update comments
SlavaSereb Oct 12, 2025
8fd3db2
fix: correct AVAX and AVAXTEST RPC URLs for Avalanche C-Chain and Tes…
tomer-shoham Nov 17, 2025
b24a127
chore: update package version to 1.8.3
tomer-shoham Nov 17, 2025
0ff0af8
chore: update app version
tomer-shoham Dec 2, 2025
a15c1ba
fix: support for xrp explorer url
tomer-shoham Dec 2, 2025
185d45e
feat: query reserve requirements dynamically from network
tomer-shoham Dec 2, 2025
09e54e7
Merge branch 'fix/missing-evm-keys' into 'main'
SlavaSereb Feb 17, 2026
3b5bb15
Merge branch 'fix/btc-api-key' into 'main'
SlavaSereb Feb 17, 2026
512bf8f
Merge branch 'fix/avax-rpcs' into 'main'
SlavaSereb Feb 17, 2026
e60b62d
Merge branch 'main' into 'fix/xrp-dynamic-reserves'
SlavaSereb Feb 17, 2026
457c80f
Merge branch 'fix/xrp-dynamic-reserves' into 'main'
SlavaSereb Feb 17, 2026
dae8799
remove e2e flag fow showing QR URL
SlavaSereb Feb 17, 2026
b9945cb
set version to 1.8.3
SlavaSereb Feb 17, 2026
37f3133
fix versions
SlavaSereb Feb 17, 2026
2d82f7c
Merge branch 'feat/show-qr-url-prod' into 'main'
SlavaSereb Feb 19, 2026
7a679b1
Merge remote-tracking branch 'github/main' into chore/align-github
SlavaSereb Apr 28, 2026
43ade5c
update readme
SlavaSereb Apr 28, 2026
3f36f35
untrack gitlab-ci file
SlavaSereb Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-gorillas-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fireblocks/recovery-shared': patch
---

updated x-key types for import and vault shared pages
5 changes: 5 additions & 0 deletions .changeset/dry-paws-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fireblocks/recovery-utility': minor
---

Display each keyset with their enabled keys
5 changes: 5 additions & 0 deletions .changeset/eleven-cheetahs-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fireblocks/recovery-utility': minor
---

get specific keyset when x-keys are required
6 changes: 6 additions & 0 deletions .changeset/giant-cups-explode.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions .changeset/old-drinks-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fireblocks/recovery-shared': minor
---

Added type distinction for relay and utility in workspace definition and support for keyset account mapping
5 changes: 5 additions & 0 deletions .changeset/orange-bobcats-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fireblocks/extended-key-recovery': patch
---

Updated tests for multiple keysets
6 changes: 6 additions & 0 deletions .changeset/tender-turtles-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@fireblocks/recovery-utility': minor
'@fireblocks/recovery-shared': minor
---

Moved x-key page to utility only
5 changes: 5 additions & 0 deletions .changeset/violet-ducks-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fireblocks/extended-key-recovery': minor
---

Added recovery for multiple keysets
6 changes: 6 additions & 0 deletions .changeset/wild-timers-think.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ dependencies
node_modules
.pnp
.pnp.js

.claude/
# Generated dependency attributions
ATTRIBUTION.md
.gitlab-ci.yml

# Testing
coverage
Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
Recover Fireblocks assets and keys in a disaster, verify a Recovery Kit, or generate keys to set up a new Recovery Kit.
<br />
<br />
<p align="center">
⬇️ Latest version 1.8.1 - Download for macOS / Linux via the Fireblocks Console
</p>
<br />
<br />
<a href="#" target="_blank">
<img src="docs/img/splash.png" alt="Screenshot" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const getRPCKey = (
return baseAsset;
};

export const getAssetURL = (
export const getAssetURLAndApiKey = (
assetId: string,
RPCs: Record<
string,
Expand All @@ -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)) {
Expand All @@ -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<number, VaultAccount<Derivation>>, accountId?: number, assetId?: string) => {
Expand Down Expand Up @@ -152,8 +154,9 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
const values = watch();

const fromAddress = values.fromAddress ?? defaultValues.fromAddress;

const derivation = wallet?.derivations?.get(getDerivationMapKey(asset?.id, fromAddress));
const derivation = fromAddress
? wallet?.derivations?.get(getDerivationMapKey(asset?.id, fromAddress)) ?? wallet?.derivations?.get(fromAddress)
: undefined;

// TODO: Show both original balance and adjusted balance in create tx UI

Expand All @@ -164,7 +167,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 ?? '<empty>'}`);
throw new Error(`No RPC Url for: ${derivation?.assetId}`);
Expand All @@ -185,6 +193,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);
},
Expand Down Expand Up @@ -328,6 +343,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 && (
<>
<br />
{prepareQuery.error.message}
</>
)}
</Typography>
)}
</Grid>
Expand All @@ -337,7 +358,11 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
id={addressExplorerId}
variant='outlined'
component={NextLinkComposed}
to={asset.getExplorerUrl?.('address')(fromAddress ?? '')}
to={
asset.nativeAsset === 'XRP'
? asset.getExplorerUrl?.('accounts')(fromAddress ?? '')
: asset.getExplorerUrl?.('address')(fromAddress ?? '')
}
target='_blank'
rel='noopener noreferrer'
>
Expand Down
30 changes: 26 additions & 4 deletions apps/recovery-relay/components/WithdrawModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -83,11 +83,18 @@ export const WithdrawModal = () => {
const { assetId } = params.signedTx;
const wallet = accounts.get(params.accountId)?.wallets.get(assetId);

const derivation = wallet?.derivations?.get(getDerivationMapKey(assetId, params.signedTx.from));
const fromAddress = params.signedTx.from;
const derivation = fromAddress
? wallet?.derivations?.get(getDerivationMapKey(assetId, fromAddress)) ?? wallet?.derivations?.get(fromAddress)
: undefined;
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) {
Expand All @@ -98,12 +105,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 });
Expand All @@ -118,6 +133,13 @@ export const WithdrawModal = () => {

logger.info('Outbound URL', { outboundRelayUrl });

if (outboundRelayUrl) {
console.log('='.repeat(80));
console.log('COPY THIS RELAY URL:');
console.log(outboundRelayUrl);
console.log('='.repeat(80));
}

return (
<BaseModal
open={!!action?.startsWith('tx') && !!asset}
Expand Down
10 changes: 8 additions & 2 deletions apps/recovery-relay/context/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 49 additions & 8 deletions apps/recovery-relay/lib/defaultRPCs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,40 @@ 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' },
ALGO_TEST: { url: null, allowedEmptyValue: true, enabled: true, name: 'Algorand Testnet' },
AVAX: { url: 'https://api.avax-test.network/ext/bc/C/rpc', name: 'Avalance C-Chain', enabled: true, allowedEmptyValue: false },
AVAX: { url: 'https://api.avax.network/ext/bc/C/rpc', name: 'Avalance C-Chain', enabled: true, allowedEmptyValue: false },
AVAXTEST: {
url: 'https://api.avax.network/ext/bc/C/rpc',
url: 'https://api.avax-test.network/ext/bc/C/rpc',
name: 'Avalance C-Chain Testnet',
enabled: true,
allowedEmptyValue: false,
},
FLR: { url: 'https://flare-api.flare.network/ext/C/rpc', name: 'Flare', enabled: true, allowedEmptyValue: false },
BTC: { url: 'https://api.blockchair.com/bitcoin', name: 'Bitcoin', enabled: true, allowedEmptyValue: false },
BTC: {
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 },
Expand All @@ -33,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: {
Expand All @@ -64,7 +105,7 @@ export const defaultRPCs: Record<
allowedEmptyValue: false,
},
ETH_TEST5: {
url: 'https://ethereum-sepolia-rpc.publicnode.com',
url: 'https://sepolia.drpc.org',
name: 'Ethereum Sepolia Testnet',
enabled: true,
allowedEmptyValue: false,
Expand Down
Loading