@ledgerhq/coin-canton
Version:
304 lines • 11.4 kB
JavaScript
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