UNPKG

@ledgerhq/coin-tezos

Version:
206 lines 8.45 kB
// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import { DerivationType } from '@taquito/ledger-signer'; import { compressPublicKey } from '@taquito/ledger-signer/dist/lib/utils'; import { validatePublicKey, ValidationResult, b58Encode, PrefixV2 } from '@taquito/utils'; import { hasManagerKey } from './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 */ export const DUST_MARGIN_MUTEZ = 500; // Tezos rejects `empty_implicit_delegated_contract`; stake-max leaves this buffer. export const 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. */ export function computeMaxStakeAmount(balance, stakedBalance, unstakedBalance, fees) { const { spendable: liquid } = partitionNativeBalance(balance, stakedBalance, unstakedBalance); const reserved = fees + 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. */ export 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. */ export const 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. */ export const 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). */ export 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). */ export 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) */ export 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. */ export function normalizePublicKeyForAddress(maybeKey, address) { if (!maybeKey) return undefined; // If it's already a valid base58 public key, keep it if (validatePublicKey(maybeKey) === 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 = DerivationType.ED25519; let prefix = PrefixV2.Ed25519PublicKey; if (address.startsWith('tz2')) { derivationType = DerivationType.SECP256K1; prefix = PrefixV2.Secp256k1PublicKey; } else if (address.startsWith('tz3')) { derivationType = DerivationType.P256; prefix = PrefixV2.P256PublicKey; } const RAW_ED25519_KEY_LENGTH = 32; const COMPRESSED_SEC1_LENGTH = 33; const UNCOMPRESSED_SEC1_LENGTH = 65; const isEd25519 = derivationType === DerivationType.ED25519; if (isEd25519) { if (keyBuf.length === COMPRESSED_SEC1_LENGTH) { return b58Encode(keyBuf.slice(1), prefix); } if (keyBuf.length !== RAW_ED25519_KEY_LENGTH) { return undefined; } return b58Encode(keyBuf, prefix); } if (keyBuf.length === UNCOMPRESSED_SEC1_LENGTH) { return b58Encode(compressPublicKey(keyBuf, derivationType), prefix); } return 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> */ export 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; } export 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 */ export 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. */ export function hasEmptyBalance(account) { return (hasManagerKey(account) && account.balance === 0) || account.type === 'empty'; } //# sourceMappingURL=utils.js.map