UNPKG

@ledgerhq/coin-canton

Version:
304 lines 11.4 kB
import { getEnv } from "@ledgerhq/live-env"; import network from "@ledgerhq/live-network"; import { makeLRUCache, minutes } from "@ledgerhq/live-network/cache"; import coinConfig from "../config"; import { TopologyChangeError } from "../types/errors"; import { TransactionType } from "../types/gateway"; export const SEPARATOR = "____"; export const DEFAULT_TAP_REQUEST_AMOUNT = "100000000000000000000000000000000000000"; export const getKey = (id, adminId) => `${id}${SEPARATOR}${adminId}`; const getGatewayUrl = (currency) => coinConfig.getCoinConfig(currency.id).gatewayUrl; const getNodeId = (currency) => { const overrideNodeId = getEnv("CANTON_NODE_ID_OVERRIDE"); if (overrideNodeId) { return overrideNodeId; } return coinConfig.getCoinConfig(currency.id).nodeId || "ledger-live-devnet"; }; export const getNetworkType = (currency) => coinConfig.getCoinConfig(currency.id).networkType; export function isGatewayEnabled(currency) { return coinConfig.getCoinConfig(currency.id).useGateway === true; } export const isPartyNotFound = (error) => { if (error instanceof Error) { const errorMessage = error.message.toLowerCase().replace(/_/g, " "); return errorMessage.includes("party") && errorMessage.includes("not found"); } return false; }; export const isPartyAlreadyExists = (error) => { if (error instanceof Error) { const errorMessage = error.message.toLowerCase().replace(/_/g, " "); return errorMessage.includes("party") && errorMessage.includes("already exists"); } return false; }; const gatewayNetwork = async (req) => { const API_KEY = getEnv("CANTON_API_KEY"); try { return await network({ ...req, headers: { ...(req.headers || {}), ...(API_KEY ? { "X-Ledger-Canton-Api-Key": API_KEY } : {}), }, }); } catch (error) { if (isPartyNotFound(error)) { throw new TopologyChangeError("Topology change detected. Re-onboarding required."); } throw error; } }; /* * Parties * @see https://canton-gateway.api.live.ledger.com/docs/openapi/redoc/index.html#tag/Parties */ export async function getPartyById(currency, partyId) { return await getParty(currency, partyId, "party-id"); } export async function getPartyByPubKey(currency, pubKey) { return await getParty(currency, pubKey, "public-key"); } async function getParty(currency, identifier, by) { const { data } = await gatewayNetwork({ method: "GET", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${identifier}?by=${by}`, }); return data; } export async function prepareOnboarding(currency, pubKey) { const gatewayUrl = getGatewayUrl(currency); const nodeId = getNodeId(currency); const fullUrl = `${gatewayUrl}/v1/node/${nodeId}/onboarding/prepare`; const { data } = await gatewayNetwork({ method: "POST", url: fullUrl, data: { public_key: pubKey, public_key_type: "ed25519", }, }); return data; } export async function isTopologyChangeRequired(currency, pubKey) { try { const response = await prepareOnboarding(currency, pubKey); // if response is not undefined (we have a transaction to sign) topology change is required if (response) { return true; } return false; } catch (error) { if (isPartyAlreadyExists(error)) { return false; } throw error; } } const getIsTopologyChangeRequiredCacheKey = (currency, pubKey) => { const nodeId = getNodeId(currency); return `${pubKey}_${nodeId}`; }; export const isTopologyChangeRequiredCached = makeLRUCache(isTopologyChangeRequired, getIsTopologyChangeRequiredCacheKey, minutes(10)); export function clearIsTopologyChangeRequiredCache(currency, pubKey) { const cacheKey = getIsTopologyChangeRequiredCacheKey(currency, pubKey); isTopologyChangeRequiredCached.clear(cacheKey); } export async function getEnabledInstruments(currency) { try { const { data } = await gatewayNetwork({ method: "GET", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/instruments`, }); return new Set(data.map(({ id, admin }) => getKey(id, admin))); } catch (error) { // If API fails, return empty array (fail-safe: only native instrument will work) console.error("Failed to fetch enabled instruments:", error); return new Set(); } } const getEnabledInstrumentsCacheKey = (currency) => { const nodeId = getNodeId(currency); return `instruments_${nodeId}`; }; export const getEnabledInstrumentsCached = makeLRUCache(getEnabledInstruments, getEnabledInstrumentsCacheKey, minutes(15)); export function clearEnabledInstrumentsCache(currency) { const cacheKey = getEnabledInstrumentsCacheKey(currency); getEnabledInstrumentsCached.clear(cacheKey); } export async function submitOnboarding(currency, publicKey, prepareResponse, { signature, applicationSignature }) { try { const { data } = await gatewayNetwork({ method: "POST", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/onboarding/submit`, data: { prepare_request: { public_key: publicKey, public_key_type: "ed25519", }, prepare_response: prepareResponse, signature, ...(applicationSignature ? { application_signature: applicationSignature } : {}), }, }); return data; } catch (error) { if (isPartyAlreadyExists(error)) { // If party already exists, use party_id from prepare response // The network layer strips custom properties from errors, so we can't extract partyId from error return { party: { party_id: prepareResponse.party_id, public_key: publicKey, }, }; } throw error; } } /* * State * @see https://canton-gateway.api.live.ledger.com/docs/openapi/redoc/index.html#tag/State */ export async function getBalance(currency, partyId) { const { data } = await gatewayNetwork({ method: "GET", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/balance`, }); return Array.isArray(data) ? data : (data.balances ?? []); } export async function getOperations(currency, partyId, options) { const { data } = await gatewayNetwork({ method: "GET", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/operations`, params: options, }); return data; } export async function getPendingTransferProposals(currency, partyId) { const { data } = await gatewayNetwork({ method: "GET", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transfer-proposals?timestamp=${Date.now()}`, }); return data; } export async function getTransferPreApproval(currency, partyId) { const { data } = await gatewayNetwork({ method: "GET", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transfer-preapproval`, }); return data; } export async function getLedgerEnd(currency) { const { data } = await gatewayNetwork({ method: "GET", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/ledger-end`, }); return data; } /* * Transaction * @see https://canton-gateway.api.live.ledger.com/docs/openapi/redoc/index.html#tag/Transaction */ export async function prepare(currency, partyId, params) { const { data } = await gatewayNetwork({ method: "POST", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/prepare`, data: params, }); return data; } export async function prepareTransferRequest(currency, partyId, params) { return prepare(currency, partyId, params); } export async function prepareTransferInstruction(currency, partyId, params) { return prepare(currency, partyId, params); } export async function preparePreApprovalTransaction(currency, partyId) { const { data } = await gatewayNetwork({ method: "POST", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/prepare`, data: { type: TransactionType.TRANSFER_PRE_APPROVAL_PROPOSAL, receiver: partyId, }, }); return data; } export async function prepareTapRequest(currency, { partyId, amount }) { // Default to 1.0 in fixed-point representation (1 * 10^38) const fixedPointAmount = amount ?? DEFAULT_TAP_REQUEST_AMOUNT; const { data } = await gatewayNetwork({ method: "POST", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/prepare`, data: { amount: fixedPointAmount.toString(), type: TransactionType.TAP_REQUEST, }, }); return data; } export async function submit(currency, partyId, serialized, signature) { const { data } = await gatewayNetwork({ method: "POST", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/submit`, data: { serialized, signature, }, }); return data; } export async function submitTransferInstruction(currency, partyId, serialized, signature) { return submit(currency, partyId, serialized, signature); } export async function submitPreApprovalTransaction(currency, partyId, { serialized }, signature) { const { data } = await gatewayNetwork({ method: "POST", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/submit`, data: { serialized, signature, }, }); return { isApproved: true, submissionId: data.submission_id, updateId: data.update_id, }; } export async function submitTapRequest(currency, { partyId, serialized, signature }) { const { data } = await gatewayNetwork({ method: "POST", url: `${getGatewayUrl(currency)}/v1/node/${getNodeId(currency)}/party/${partyId}/transaction/submit`, data: { serialized, signature, }, }); return data; } /** * Fetch Canton tokens from CAL service and create a map of id -> token_identifier */ async function getCalTokens(currency) { const calUrl = getEnv("CAL_SERVICE_URL"); const { data: calTokens } = await gatewayNetwork({ method: "GET", url: `${calUrl}/v1/tokens?network=${currency.id}&output=id,name,ticker,network,contract_address,token_identifier,units,standard`, }); // Map id -> token_identifier const tokenIdentifierMap = new Map(); for (const token of calTokens) { tokenIdentifierMap.set(token.id, token.token_identifier); } return tokenIdentifierMap; } const getCalTokensCacheKey = (currency) => currency.id; export const getCalTokensCached = makeLRUCache(getCalTokens, getCalTokensCacheKey, minutes(30)); //# sourceMappingURL=gateway.js.map