@ledgerhq/hw-app-eth
Version:
Ledger Hardware Wallet Ethereum Application API
229 lines (203 loc) • 7.03 kB
text/typescript
// This implements the resolution of a Transaction using Ledger's own API
import { log } from "@ledgerhq/logs";
import { Interface } from "@ethersproject/abi";
import {
signDomainResolution,
signAddressResolution,
} from "@ledgerhq/domain-service/signers/index";
import { LedgerEthTransactionResolution, LedgerEthTransactionService, LoadConfig } from "../types";
import { byContractAddressAndChainId, findERC20SignaturesInfo } from "./erc20";
import { loadInfosForContractMethod } from "./contracts";
import { getNFTInfo, loadNftPlugin } from "./nfts";
import { decodeTxInfo, tokenSelectors, nftSelectors, mergeResolutions } from "../../utils";
type PotentialResolutions = {
token: boolean | undefined;
nft: boolean | undefined;
externalPlugins: boolean | undefined;
};
/**
* @ignore for external documentation
*
* Providing additionnal data for some transactions (Token or NFT related) can enable clear signing
* of initially impossible to decode data.
* This method will add necessary APDUs to the resolution paramter in order to provide this data to the nano app
*/
const getAdditionalDataForContract = async (
contractAddress: string,
chainIdTruncated: number,
loadConfig: LoadConfig,
shouldResolve: PotentialResolutions,
): Promise<Pick<LedgerEthTransactionResolution, "nfts" | "erc20Tokens">> => {
const resolution: Pick<LedgerEthTransactionResolution, "nfts" | "erc20Tokens"> = {
nfts: [],
erc20Tokens: [],
};
if (shouldResolve.nft) {
const nftInfo = await getNFTInfo(contractAddress, chainIdTruncated, loadConfig);
if (nftInfo) {
log(
"ethereum",
"loaded nft info for " + nftInfo.contractAddress + " (" + nftInfo.collectionName + ")",
);
resolution.nfts.push(nftInfo.data);
} else {
log("ethereum", "couldn't load nft info for " + contractAddress);
}
}
if (shouldResolve.token) {
const erc20SignaturesBlob = await findERC20SignaturesInfo(loadConfig, chainIdTruncated);
const erc20Info = byContractAddressAndChainId(
contractAddress,
chainIdTruncated,
erc20SignaturesBlob,
);
if (erc20Info) {
log(
"ethereum",
"loaded erc20token info for " + erc20Info.contractAddress + " (" + erc20Info.ticker + ")",
);
resolution.erc20Tokens.push(erc20Info.data.toString("hex"));
} else {
log("ethereum", "couldn't load erc20token info for " + contractAddress);
}
}
return resolution;
};
/**
* @ignore for external documentation
*
* Depending on the transaction, it might be necessary to load internal plugins in the nano app
* in order to clear sign it.
* This method will add necessary APDUs to the resolution parameter in order to load those internal plugins
*/
const loadNanoAppPlugins = async (
contractAddress: string,
selector: string,
decodedTx,
chainIdTruncated: number,
loadConfig: LoadConfig,
shouldResolve: PotentialResolutions,
): Promise<LedgerEthTransactionResolution> => {
let resolution: LedgerEthTransactionResolution = {
externalPlugin: [],
plugin: [],
nfts: [],
erc20Tokens: [],
domains: [],
};
if (shouldResolve.nft) {
const nftPluginPayload = await loadNftPlugin(
contractAddress,
selector,
chainIdTruncated,
loadConfig,
);
if (nftPluginPayload) {
resolution.plugin.push(nftPluginPayload);
} else {
log(
"ethereum",
"no NFT plugin payload for selector " + selector + " and address " + contractAddress,
);
}
}
if (shouldResolve.externalPlugins) {
const contractMethodInfos = await loadInfosForContractMethod(
contractAddress,
selector,
chainIdTruncated,
loadConfig,
);
if (contractMethodInfos) {
const { plugin, payload, signature, erc20OfInterest, abi } = contractMethodInfos;
if (plugin) {
log("ethereum", `found plugin (${plugin}) for selector: ${selector}`);
resolution.externalPlugin.push({ payload, signature });
}
if (erc20OfInterest && erc20OfInterest.length && abi) {
const contract = new Interface(abi);
const args = contract.parseTransaction(decodedTx).args;
for (const path of erc20OfInterest) {
const erc20ContractAddress = path.split(".").reduce((value, seg) => {
if (seg === "-1" && Array.isArray(value)) {
return value[value.length - 1];
}
return value[seg];
}, args) as unknown as string; // impossible(?) to type correctly as the initializer is different from the returned type
const externalPluginResolution = await getAdditionalDataForContract(
erc20ContractAddress,
chainIdTruncated,
loadConfig,
{
nft: false,
externalPlugins: false,
token: true, // enforcing resolution of tokens for external plugins that need info on assets (e.g. for a swap)
},
);
resolution = mergeResolutions([resolution, externalPluginResolution]);
}
}
} else {
log("ethereum", "no infos for selector " + selector);
}
}
return resolution;
};
/**
* @ignore for external documentation
*
* In charge of collecting the different APDUs necessary for clear signing
* a transaction based on a specified configuration.
*/
const resolveTransaction: LedgerEthTransactionService["resolveTransaction"] = async (
rawTxHex,
loadConfig,
resolutionConfig,
) => {
const rawTx = Buffer.from(rawTxHex, "hex");
const { decodedTx, chainIdTruncated } = decodeTxInfo(rawTx);
const { domains } = resolutionConfig;
const contractAddress = decodedTx.to;
const selector = decodedTx.data.length >= 10 && decodedTx.data.substring(0, 10);
const resolutions: Partial<LedgerEthTransactionResolution>[] = [];
if (selector) {
const shouldResolve: PotentialResolutions = {
token: resolutionConfig.erc20 && tokenSelectors.includes(selector),
nft: resolutionConfig.nft && nftSelectors.includes(selector),
externalPlugins: resolutionConfig.externalPlugins,
};
const pluginsResolution = await loadNanoAppPlugins(
contractAddress,
selector,
decodedTx,
chainIdTruncated,
loadConfig,
shouldResolve,
);
if (pluginsResolution) {
resolutions.push(pluginsResolution);
}
const contractResolution = await getAdditionalDataForContract(
contractAddress,
chainIdTruncated,
loadConfig,
shouldResolve,
);
if (contractResolution) {
resolutions.push(contractResolution);
}
}
// Passthrough to be accessible to the Domains module
if (domains) {
const domainResolutions: Partial<LedgerEthTransactionResolution> = {
domains,
};
resolutions.push(domainResolutions);
}
return mergeResolutions(resolutions);
};
export default {
resolveTransaction,
signDomainResolution,
signAddressResolution,
} as LedgerEthTransactionService;