Skip to content

Commit cffad89

Browse files
committed
feat: recovery support for Hedera EVM
ticket: win-7852
1 parent 2e80147 commit cffad89

File tree

5 files changed

+209
-7
lines changed

5 files changed

+209
-7
lines changed

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,11 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
902902
},
903903
apiKey
904904
);
905+
906+
if (result && result?.nonce) {
907+
return Number(result.nonce);
908+
}
909+
905910
if (!result || !Array.isArray(result.result)) {
906911
throw new Error('Unable to find next nonce from Etherscan, got: ' + JSON.stringify(result));
907912
}

modules/abstract-eth/src/lib/utils.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,3 +1047,193 @@ export async function recoveryBlockchainExplorerQuery(
10471047
export function getDefaultExpireTime(): number {
10481048
return Math.floor(new Date().getTime() / 1000) + 60 * 60 * 24 * 7;
10491049
}
1050+
1051+
export async function recovery_HBAREVM_BlockchainExplorerQuery(
1052+
query: Record<string, string>,
1053+
explorerUrl: string,
1054+
token?: string
1055+
): Promise<Record<string, unknown>> {
1056+
// Hedera Mirror Node API does not use API keys, but we keep this for compatibility
1057+
if (token) {
1058+
query.apikey = token;
1059+
}
1060+
1061+
const { module, action } = query;
1062+
1063+
// Remove trailing slash from explorerUrl if present
1064+
const baseUrl = explorerUrl.replace(/\/$/, '');
1065+
1066+
try {
1067+
switch (`${module}.${action}`) {
1068+
case 'account.balance':
1069+
return await queryAddressBalanceHedera(query, baseUrl);
1070+
1071+
case 'account.txlist':
1072+
return await getAddressNonceHedera(query, baseUrl);
1073+
1074+
case 'account.tokenbalance':
1075+
return await queryTokenBalanceHedera(query, baseUrl);
1076+
1077+
case 'proxy.eth_gasPrice':
1078+
return await getGasPriceHedera(query, baseUrl);
1079+
1080+
case 'proxy.eth_estimateGas':
1081+
return await getGasLimitHedera(query, baseUrl);
1082+
1083+
case 'proxy.eth_call':
1084+
return await querySequenceIdHedera(query, baseUrl);
1085+
1086+
default:
1087+
throw new Error(`Unsupported API call: ${module}.${action}`);
1088+
}
1089+
} catch (error) {
1090+
throw error;
1091+
}
1092+
}
1093+
1094+
/**
1095+
* 1. Gets address balance using Hedera Mirror Node API
1096+
*/
1097+
async function queryAddressBalanceHedera(
1098+
query: Record<string, string>,
1099+
baseUrl: string
1100+
): Promise<Record<string, unknown>> {
1101+
const address = query.address;
1102+
const url = `${baseUrl}/accounts/${address}`;
1103+
const response = await request.get(url).send();
1104+
1105+
if (!response.ok) {
1106+
throw new Error('could not reach explorer');
1107+
}
1108+
1109+
const balance = response.body.balance?.balance || '0';
1110+
1111+
return balance;
1112+
}
1113+
1114+
/**
1115+
* 2. Gets nonce using Hedera Mirror Node API
1116+
*/
1117+
async function getAddressNonceHedera(query: Record<string, string>, baseUrl: string): Promise<Record<string, unknown>> {
1118+
const address = query.address;
1119+
const accountUrl = `${baseUrl}/accounts/${address}`;
1120+
const response = await request.get(accountUrl).send();
1121+
1122+
if (!response.ok) {
1123+
throw new Error('could not reach explorer');
1124+
}
1125+
1126+
const nonce = response.body.ethereum_nonce || 0;
1127+
1128+
return { nonce };
1129+
}
1130+
1131+
/**
1132+
* 3. Gets token balance using Hedera Mirror Node API
1133+
*/
1134+
async function queryTokenBalanceHedera(
1135+
query: Record<string, string>,
1136+
baseUrl: string
1137+
): Promise<Record<string, unknown>> {
1138+
const contractAddress = query.contractaddress;
1139+
const address = query.address;
1140+
1141+
// Get token balances for the account
1142+
const url = `${baseUrl}/accounts/${address}/tokens`;
1143+
const response = await request.get(url).send();
1144+
1145+
if (!response.ok) {
1146+
throw new Error('could not reach explorer');
1147+
}
1148+
1149+
// Find the specific token balance
1150+
const tokens = response.body.tokens || [];
1151+
const tokenBalance = tokens.find(
1152+
(token: { token_id: string; contract_address: string; balance: number }) =>
1153+
token.token_id === contractAddress || token.contract_address === contractAddress
1154+
);
1155+
1156+
const balance = tokenBalance ? tokenBalance.balance.toString() : '0';
1157+
1158+
return balance;
1159+
}
1160+
1161+
/**
1162+
* 4. Gets sequence ID using Hedera Mirror Node API or rpc call
1163+
*/
1164+
async function querySequenceIdHedera(query: Record<string, string>, baseUrl: string): Promise<Record<string, unknown>> {
1165+
const { to, data } = query;
1166+
1167+
const url = 'https://testnet.hashio.io/api';
1168+
1169+
const requestBody = {
1170+
jsonrpc: '2.0',
1171+
method: 'eth_call',
1172+
params: [
1173+
{
1174+
to: to,
1175+
data: data,
1176+
},
1177+
],
1178+
id: 1,
1179+
};
1180+
1181+
const response = await request.post(url).send(requestBody).set('Content-Type', 'application/json');
1182+
1183+
if (!response.ok) {
1184+
throw new Error('could not reach explorer');
1185+
}
1186+
1187+
return response.body;
1188+
}
1189+
1190+
/**
1191+
* 5. getGasPriceFromExternalAPI - Gets gas price using Hedera Mirror Node API
1192+
*/
1193+
async function getGasPriceHedera(query: Record<string, string>, baseUrl: string): Promise<Record<string, unknown>> {
1194+
const url = 'https://testnet.hashio.io/api';
1195+
1196+
const requestBody = {
1197+
jsonrpc: '2.0',
1198+
method: 'eth_gasPrice',
1199+
params: [],
1200+
id: 1,
1201+
};
1202+
1203+
const response = await request.post(url).send(requestBody).set('Content-Type', 'application/json');
1204+
1205+
if (!response.ok) {
1206+
throw new Error('could not reach explorer');
1207+
}
1208+
1209+
return response.body;
1210+
}
1211+
1212+
/**
1213+
* 6. getGasLimitFromExternalAPI - Gets gas limit using Hedera Mirror Node API
1214+
*/
1215+
async function getGasLimitHedera(query: Record<string, string>, baseUrl: string): Promise<Record<string, unknown>> {
1216+
const url = 'https://testnet.hashio.io/api';
1217+
1218+
const { from, to, data } = query;
1219+
1220+
const requestBody = {
1221+
jsonrpc: '2.0',
1222+
method: 'eth_estimateGas',
1223+
params: [
1224+
{
1225+
from,
1226+
to,
1227+
data,
1228+
},
1229+
],
1230+
id: 1,
1231+
};
1232+
const response = await request.post(url).send(requestBody).set('Content-Type', 'application/json');
1233+
1234+
if (!response.ok) {
1235+
throw new Error('could not reach explorer');
1236+
}
1237+
1238+
return response.body;
1239+
}

modules/sdk-coin-evm/src/evmCoin.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
* @prettier
33
*/
44
import { BaseCoin, BitGoBase, common, MPCAlgorithm, MultisigType, multisigTypes } from '@bitgo/sdk-core';
5-
import { BaseCoin as StaticsBaseCoin, CoinFeature, coins } from '@bitgo/statics';
5+
import { BaseCoin as StaticsBaseCoin, CoinFeature, coins, CoinFamily } from '@bitgo/statics';
66
import {
77
AbstractEthLikeNewCoins,
88
OfflineVaultTxInfo,
99
RecoverOptions,
1010
recoveryBlockchainExplorerQuery,
11+
recovery_HBAREVM_BlockchainExplorerQuery,
1112
TransactionBuilder as EthLikeTransactionBuilder,
1213
UnsignedSweepTxMPCv2,
1314
VerifyEthTransactionOptions,
@@ -78,7 +79,13 @@ export class EvmCoin extends AbstractEthLikeNewCoins {
7879

7980
const apiToken = apiKey || evmConfig[this.getFamily()].apiToken;
8081
const explorerUrl = evmConfig[this.getFamily()].baseUrl;
81-
return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken as string);
82+
83+
switch (this.getFamily()) {
84+
case CoinFamily.HBAREVM:
85+
return await recovery_HBAREVM_BlockchainExplorerQuery(query, explorerUrl as string, apiToken as string);
86+
default:
87+
return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken as string);
88+
}
8289
}
8390

8491
/** @inheritDoc */

modules/sdk-core/src/bitgo/environments.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,8 @@ const mainnetBase: EnvironmentTemplate = {
269269
megaeth: {
270270
baseUrl: 'https://carrot.megaeth.com/rpc', //TODO: add mainnet url when available
271271
},
272-
hedera: {
273-
baseUrl: 'https://server-verify.hashscan.io/verify',
272+
hbarevm: {
273+
baseUrl: 'https://mainnet.mirrornode.hedera.com/api/v1',
274274
},
275275
fluenteth: {
276276
baseUrl: 'https://testnet.fluentscan.xyz/api/', //TODO: COIN-6478: add mainnet url when available
@@ -415,8 +415,8 @@ const testnetBase: EnvironmentTemplate = {
415415
plume: {
416416
baseUrl: 'https://testnet-explorer.plume.org',
417417
},
418-
hedera: {
419-
baseUrl: 'https://server-verify.hashscan.io/verify',
418+
hbarevm: {
419+
baseUrl: 'https://testnet.mirrornode.hedera.com/api/v1',
420420
},
421421
fluenteth: {
422422
baseUrl: 'https://testnet.fluentscan.xyz/api/',

modules/statics/src/networks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2035,7 +2035,7 @@ class Plume extends Mainnet implements EthereumNetwork {
20352035
class HederaEVMTestnet extends Testnet implements EthereumNetwork {
20362036
name = 'Testnet Hedera EVM';
20372037
family = CoinFamily.HBAREVM;
2038-
explorerUrl = 'https://hashscan.io/mainnet/transactions/';
2038+
explorerUrl = 'https://hashscan.io/mainnet/transaction/';
20392039
accountExplorerUrl = 'https://hashscan.io/mainnet/account/';
20402040
chainId = 296;
20412041
nativeCoinOperationHashPrefix = '296';

0 commit comments

Comments
 (0)