Skip to content

Commit

Permalink
Add retry logic for Fireblocks signing operations (#193)
Browse files Browse the repository at this point in the history
* Add retry logic for Fireblocks signing operations

* lint fixes
  • Loading branch information
qrtp authored Aug 28, 2024
1 parent c7f735b commit d08583d
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 19 deletions.
1 change: 1 addition & 0 deletions packages/ui-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"title-case": "^3.0.3",
"truncate-eth-address": "^1.0.2",
"truncate-middle": "^1.0.6",
"ts-retry": "^5.0.1",
"tss-react": "^4.0.0",
"typescript": "5.0.4",
"uns": "https://github.com/unstoppabledomains/uns#v0.8.35",
Expand Down
37 changes: 29 additions & 8 deletions packages/ui-components/src/hooks/useFireblocksMessageSigner.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {retryAsync} from 'ts-retry';
import type {Eip712TypedData} from 'web3';
import {utils as web3utils} from 'web3';

Expand All @@ -11,7 +12,7 @@ import {notifyEvent} from '../lib/error';
import {getFireBlocksClient} from '../lib/fireBlocks/client';
import {getBootstrapState} from '../lib/fireBlocks/storage/state';
import type {GetOperationStatusResponse} from '../lib/types/fireBlocks';
import {EIP_712_KEY} from '../lib/types/fireBlocks';
import {EIP_712_KEY, MAX_RETRIES} from '../lib/types/fireBlocks';
import useFireblocksAccessToken from './useFireblocksAccessToken';
import useFireblocksState from './useFireblocksState';

Expand All @@ -25,8 +26,8 @@ const useFireblocksMessageSigner = (): FireblocksMessageSigner => {
const [state, saveState] = useFireblocksState();
const getAccessToken = useFireblocksAccessToken();

// return the fireblocks client signer
return async (
// define the fireblocks client signer
const signingFn = async (
message: string,
address?: string,
chainId?: number,
Expand Down Expand Up @@ -141,6 +142,11 @@ const useFireblocksMessageSigner = (): FireblocksMessageSigner => {
},
);

// validate and return the signature result
if (!signatureOp?.result?.signature) {
throw new Error('signature failed');
}

// indicate complete with successful signature result
notifyEvent('signature successful', 'info', 'Wallet', 'Signature', {
meta: {
Expand All @@ -149,13 +155,28 @@ const useFireblocksMessageSigner = (): FireblocksMessageSigner => {
signatureOp,
},
});

// validate and return the signature result
if (!signatureOp?.result?.signature) {
throw new Error('signature failed');
}
return signatureOp.result.signature;
};

// wrap the signing function in retry logic to ensure it has a chance to
// succeed if there are intermittent failures
return async (
message: string,
address?: string,
chainId?: number,
): Promise<string> => {
// wrap the signing function in retry logic
return retryAsync(async () => await signingFn(message, address, chainId), {
delay: 100,
maxTry: MAX_RETRIES,
onError: (err: Error, currentTry: number) => {
notifyEvent(err, 'warning', 'Wallet', 'Signature', {
msg: 'encountered signature error in retry logic',
meta: {currentTry},
});
},
});
};
};

export default useFireblocksMessageSigner;
41 changes: 34 additions & 7 deletions packages/ui-components/src/hooks/useFireblocksTxSigner.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {retryAsync} from 'ts-retry';

import {
createTransactionOperation,
signAndWait,
Expand All @@ -6,6 +8,7 @@ import {notifyEvent} from '../lib/error';
import {getFireBlocksClient} from '../lib/fireBlocks/client';
import {getBootstrapState} from '../lib/fireBlocks/storage/state';
import type {GetOperationStatusResponse} from '../lib/types/fireBlocks';
import { MAX_RETRIES} from '../lib/types/fireBlocks';
import useFireblocksAccessToken from './useFireblocksAccessToken';
import useFireblocksState from './useFireblocksState';

Expand All @@ -20,8 +23,8 @@ const useFireblocksTxSigner = (): FireblocksTxSigner => {
const [state, saveState] = useFireblocksState();
const getAccessToken = useFireblocksAccessToken();

// return the fireblocks client signer
return async (
// define the fireblocks client signer
const signingFn = async (
chainId: number,
contractAddress: string,
data: string,
Expand Down Expand Up @@ -99,6 +102,11 @@ const useFireblocksTxSigner = (): FireblocksTxSigner => {
},
);

// validate and return the signature result
if (!txOp?.transaction?.id) {
throw new Error('signature failed');
}

// indicate complete with successful signature result
notifyEvent('signature successful', 'info', 'Wallet', 'Signature', {
meta: {
Expand All @@ -109,13 +117,32 @@ const useFireblocksTxSigner = (): FireblocksTxSigner => {
txOp,
},
});

// validate and return the signature result
if (!txOp?.transaction?.id) {
throw new Error('signature failed');
}
return txOp.transaction.id;
};

// wrap the signing function in retry logic to ensure it has a chance to
// succeed if there are intermittent failures
return async (
chainId: number,
contractAddress: string,
data: string,
value?: string,
): Promise<string> => {
// wrap the signing function in retry logic
return retryAsync(
async () => await signingFn(chainId, contractAddress, data, value),
{
delay: 100,
maxTry: MAX_RETRIES,
onError: (err: Error, currentTry: number) => {
notifyEvent(err, 'warning', 'Wallet', 'Signature', {
msg: 'encountered transaction error in retry logic',
meta: {currentTry},
});
},
},
);
};
};

export default useFireblocksTxSigner;
21 changes: 20 additions & 1 deletion packages/ui-components/src/lib/fireBlocks/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
TEvent,
} from '@fireblocks/ncw-js-sdk';
import {FireblocksNCWFactory} from '@fireblocks/ncw-js-sdk';
import {retryAsync} from 'ts-retry';

import config from '@unstoppabledomains/config';

Expand All @@ -14,6 +15,7 @@ import {
} from '../../actions/fireBlocksActions';
import {notifyEvent} from '../error';
import {sleep} from '../sleep';
import {MAX_RETRIES} from '../types/fireBlocks';
import {LogEventHandler} from './events/logHandler';
import {RpcMessageProvider} from './messages/rpcHandler';
import {StorageFactoryProvider} from './storage/factory';
Expand Down Expand Up @@ -78,7 +80,7 @@ export const initializeClient = async (
recoveryToken?: string;
},
): Promise<boolean> => {
try {
const initializeFn = async () => {
// create a join request for this device
let callbackPromise: Promise<boolean> | undefined;
await client.requestJoinExistingWallet({
Expand Down Expand Up @@ -139,6 +141,23 @@ export const initializeClient = async (
await sleep(FB_WAIT_TIME_MS);
}
throw new Error('fireblocks key status is not ready');
};

// handle initialization errors and return a simple boolean result
// for the bootstrapping process
try {
// wrap the signing function in retry logic so that intermittent
// backend errors do not result in failed login attempts
return await retryAsync(initializeFn, {
delay: 100,
maxTry: MAX_RETRIES,
onError: (err: Error, currentTry: number) => {
notifyEvent(err, 'warning', 'Wallet', 'Authorization', {
msg: 'encountered bootstrap error in retry logic',
meta: {currentTry},
});
},
});
} catch (initError) {
notifyEvent(initError, 'error', 'Wallet', 'Configuration', {
msg: 'unable to initialize client',
Expand Down
8 changes: 5 additions & 3 deletions packages/ui-components/src/lib/types/fireBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ export interface CreateTransaction {
data: string;
value?: string;
}

export const EIP_712_KEY = 'EIP712Domain';
export const FireblocksStateKey = 'fireblocks-state';

export interface GetAccountAssetsResponse {
items: AccountAsset[];
}
Expand Down Expand Up @@ -96,11 +96,11 @@ export interface GetEstimateTransactionResponse {
};
};
}

export interface GetOperationListResponse {
'@type': string;
items: Operation[];
}

export interface GetOperationResponse {
'@type': string;
operation: Operation;
Expand All @@ -119,20 +119,22 @@ export interface GetOperationStatusResponse {
externalVendorTransactionId?: string;
};
}

export interface GetTokenResponse {
code?: 'SUCCESS' | 'PROCESSING';
accessToken: string;
refreshToken: string;
bootstrapToken: string;
}

export interface IDeviceStore {
get(deviceId: string, key: string): Promise<string | null>;
set(deviceId: string, key: string, value: string): Promise<void>;
clear(deviceId: string, key: string): Promise<void>;
getAllKeys(deviceId: string): Promise<string[]>;
}

export const MAX_RETRIES = 5;

export interface Operation {
'@type': string;
id: string;
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5398,6 +5398,7 @@ __metadata:
title-case: ^3.0.3
truncate-eth-address: ^1.0.2
truncate-middle: ^1.0.6
ts-retry: ^5.0.1
tslib: ^2.6.2
tss-react: ^4.0.0
typescript: 5.0.4
Expand Down Expand Up @@ -20675,6 +20676,13 @@ __metadata:
languageName: node
linkType: hard

"ts-retry@npm:^5.0.1":
version: 5.0.1
resolution: "ts-retry@npm:5.0.1"
checksum: 8cf23a09cd260e18baa5775a8220c482445c50fad0450d4b1b3e88a472e4486014140939dc7009db1081cdc5a32041ca8fb07ac3d0d3eb6d5be6dcf8b6b47f4a
languageName: node
linkType: hard

"ts-toolbelt@npm:^6.15.1":
version: 6.15.5
resolution: "ts-toolbelt@npm:6.15.5"
Expand Down

0 comments on commit d08583d

Please sign in to comment.