@ledgerhq/coin-tezos
Version:
219 lines • 9.28 kB
JavaScript
;
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.OP_SIZE_XTZ_TRANSFER = exports.MIN_SUGGESTED_FEE_SMALL_TRANSFER = exports.STAKE_USE_ALL_RESERVE_MUTEZ = exports.DUST_MARGIN_MUTEZ = void 0;
exports.computeMaxStakeAmount = computeMaxStakeAmount;
exports.partitionNativeBalance = partitionNativeBalance;
exports.parseTezosTokenAsset = parseTezosTokenAsset;
exports.resolveTezosOperationMode = resolveTezosOperationMode;
exports.createMockSigner = createMockSigner;
exports.normalizePublicKeyForAddress = normalizePublicKeyForAddress;
exports.convertSecp256k1DERToRaw = convertSecp256k1DERToRaw;
exports.normalizeTo32Bytes = normalizeTo32Bytes;
exports.createFallbackEstimation = createFallbackEstimation;
exports.hasEmptyBalance = hasEmptyBalance;
const ledger_signer_1 = require("@taquito/ledger-signer");
const utils_1 = require("@taquito/ledger-signer/dist/lib/utils");
const utils_2 = require("@taquito/utils");
const types_1 = require("./network/types");
/**
* Dust margin in mutez to prevent transaction failures on send max operations
* This ensures a small safety buffer remains in the account after send max
* Based on production behavior where ~281-287 mutez typically remain
* Increased to 500 to handle edge cases and fee estimation variations
*/
exports.DUST_MARGIN_MUTEZ = 500;
// Tezos rejects `empty_implicit_delegated_contract`; stake-max leaves this buffer.
exports.STAKE_USE_ALL_RESERVE_MUTEZ = 10000n;
/** Stake "Use Max": staked and unstaked-frozen funds count toward `balance` but aren't (re-)stakeable until finalized, so subtract both. */
function computeMaxStakeAmount(balance, stakedBalance, unstakedBalance, fees) {
const { spendable: liquid } = partitionNativeBalance(balance, stakedBalance, unstakedBalance);
const reserved = fees + exports.STAKE_USE_ALL_RESERVE_MUTEZ;
return liquid > reserved ? liquid - reserved : 0n;
}
/**
* Splits a Tezos native balance (which includes frozen funds) into spendable and locked
* (staked + unstaked). `locked` is clamped to `total` so spendable stays >= 0 across TzKT reorgs.
*/
function partitionNativeBalance(total, staked, unstaked) {
const frozen = staked + unstaked;
const locked = frozen > total ? total : frozen;
return { spendable: total - locked, locked };
}
/**
* Suggested fee returned by Taquito for a minimal amount pre-estimation (mutez)
* Used as a stable fallback for send-max when RPC estimation is unavailable.
*/
exports.MIN_SUGGESTED_FEE_SMALL_TRANSFER = 489;
/**
* Typical operation size (bytes) for a simple XTZ transfer used to approximate
* the increased fee component when computing send-max in fallback mode.
* Chosen to match Taquito’s behavior closely in integration tests.
*/
exports.OP_SIZE_XTZ_TRANSFER = 154;
/**
* Parse FA1.2 / FA2 token contract + token id from coin-framework `assetReference`
* (`KT1…` or `KT1…:tokenId` as produced by getBlock / listOperations).
*/
function parseTezosTokenAsset(asset) {
if (!asset || asset.type === 'native')
return null;
const ref = asset.assetReference?.trim();
if (!ref?.startsWith('KT1'))
return null;
const colonIdx = ref.lastIndexOf(':');
if (colonIdx > 0) {
const contractAddress = ref.slice(0, colonIdx);
const tokenId = Number(ref.slice(colonIdx + 1));
if (!Number.isFinite(tokenId) || tokenId < 0)
return null;
return { contractAddress, tokenId };
}
return { contractAddress: ref, tokenId: 0 };
}
/**
* Resolves Tezos operation mode from intent type and asset (native XTZ vs token).
*/
function resolveTezosOperationMode(intentType, asset) {
const base = intentType;
if (base === 'send' && parseTezosTokenAsset(asset) !== null) {
return 'send_token';
}
return base;
}
/**
* Creates a mock signer for Taquito operations (estimation and crafting)
*/
function createMockSigner(publicKeyHash, publicKey) {
return {
publicKeyHash: async () => publicKeyHash,
publicKey: async () => publicKey,
sign: () => Promise.reject(new Error('unsupported')),
secretKey: () => Promise.reject(new Error('unsupported')),
};
}
/**
* Normalize a Tezos public key to base58 format (edpk/sppk/p2pk) based on the
* sender address prefix (tz1/tz2/tz3). Accepts either an already base58-encoded
* key or a hex key returned by the Ledger app. Returns undefined if input is
* empty or cannot be parsed.
*/
function normalizePublicKeyForAddress(maybeKey, address) {
if (!maybeKey)
return undefined;
// If it's already a valid base58 public key, keep it
if ((0, utils_2.validatePublicKey)(maybeKey) === utils_2.ValidationResult.VALID)
return maybeKey;
// Attempt to convert hex → base58 according to address curve
try {
const pkHex = maybeKey.trim().toLowerCase();
if (pkHex === '')
return undefined;
const keyBuf = Buffer.from(pkHex, 'hex');
// Choose curve/prefix from address tz1/tz2/tz3
// default are values for tz1 and fallbacks to it
let derivationType = ledger_signer_1.DerivationType.ED25519;
let prefix = utils_2.PrefixV2.Ed25519PublicKey;
if (address.startsWith('tz2')) {
derivationType = ledger_signer_1.DerivationType.SECP256K1;
prefix = utils_2.PrefixV2.Secp256k1PublicKey;
}
else if (address.startsWith('tz3')) {
derivationType = ledger_signer_1.DerivationType.P256;
prefix = utils_2.PrefixV2.P256PublicKey;
}
const RAW_ED25519_KEY_LENGTH = 32;
const COMPRESSED_SEC1_LENGTH = 33;
const UNCOMPRESSED_SEC1_LENGTH = 65;
const isEd25519 = derivationType === ledger_signer_1.DerivationType.ED25519;
if (isEd25519) {
if (keyBuf.length === COMPRESSED_SEC1_LENGTH) {
return (0, utils_2.b58Encode)(keyBuf.slice(1), prefix);
}
if (keyBuf.length !== RAW_ED25519_KEY_LENGTH) {
return undefined;
}
return (0, utils_2.b58Encode)(keyBuf, prefix);
}
if (keyBuf.length === UNCOMPRESSED_SEC1_LENGTH) {
return (0, utils_2.b58Encode)((0, utils_1.compressPublicKey)(keyBuf, derivationType), prefix);
}
return (0, utils_2.b58Encode)(keyBuf, prefix);
}
catch {
return undefined;
}
}
/**
* Converts a DER-encoded secp256k1/P-256 ECDSA signature (returned by the Tezos Ledger
* app for tz2/tz3 accounts) to the raw 64-byte r||s format the Tezos protocol expects.
* Format: 0x30 <totalLen> 0x02 <rLen> <r_bytes> 0x02 <sLen> <s_bytes>
*/
function convertSecp256k1DERToRaw(derHex) {
if (!/^[0-9a-fA-F]+$/.test(derHex) || derHex.length % 2 !== 0) {
throw new Error('Tezos: device returned an invalid signature');
}
const buf = Buffer.from(derHex, 'hex');
if (buf.length === 64)
return derHex; // already raw r||s
const rStart = 4;
const rLen = buf[3];
const sLenIdx = rStart + rLen + 1;
const sLen = buf[sLenIdx];
const sStart = sLenIdx + 1;
const malformed = buf.length < 8 ||
buf[0] !== 0x30 ||
buf[2] !== 0x02 ||
buf[sLenIdx - 1] !== 0x02 ||
rLen === 0 ||
sLen === 0 ||
rLen > 33 ||
sLen > 33 ||
sStart + sLen !== buf.length;
if (malformed) {
throw new Error('Tezos: device returned an invalid signature');
}
const raw = Buffer.concat([
normalizeTo32Bytes(buf.slice(rStart, rStart + rLen)),
normalizeTo32Bytes(buf.slice(sStart, sStart + sLen)),
]).toString('hex');
// A 33-byte r/s is only valid DER when left-padded with 0x00; anything else normalises
// to a non-64-byte result and is not a usable signature.
if (raw.length !== 128) {
throw new Error('Tezos: device returned an invalid signature');
}
return raw;
}
function normalizeTo32Bytes(bytes) {
if (bytes.length === 33 && bytes[0] === 0x00)
return bytes.slice(1);
if (bytes.length < 32) {
const padded = Buffer.alloc(32, 0);
bytes.copy(padded, 32 - bytes.length);
return padded;
}
return bytes;
}
/**
* Creates default fallback estimation values
*/
function createFallbackEstimation(config) {
return {
fees: BigInt(config.fees.minEstimatedFees),
gasLimit: BigInt(config.fees.minGasLimit),
storageLimit: BigInt(config.fees.minStorageLimit),
estimatedFees: BigInt(config.fees.minEstimatedFees),
};
}
/**
* Whether an account has nothing on-chain that would spare a recipient the allocation
* (storage) burn. Manager-key accounts (`user`/`delegate`) with a zero balance and truly
* non-existent (`empty`) accounts count as empty. A registered baker (`delegate`) is always
* allocated on-chain, so a zero-balance one is unusual — but keying on `hasManagerKey` keeps
* the reveal/storage logic consistent for both wallet and baker recipients.
*/
function hasEmptyBalance(account) {
return ((0, types_1.hasManagerKey)(account) && account.balance === 0) || account.type === 'empty';
}
//# sourceMappingURL=utils.js.map