UNPKG

@ledgerhq/coin-tezos

Version:
233 lines 10.9 kB
"use strict"; // SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 Object.defineProperty(exports, "__esModule", { value: true }); exports.craftRawOperations = craftRawOperations; const rpc_1 = require("@taquito/rpc"); const utils_1 = require("@taquito/utils"); const errors_1 = require("../types/errors"); const utils_2 = require("../utils"); const craftTransaction_1 = require("./craftTransaction"); const estimateRevealLimits_1 = require("./estimateRevealLimits"); const tezosToolkit_1 = require("./tezosToolkit"); /** * Craft a forged (but unsigned) Tezos operation from a JSON-serialized array of * partial operation contents ({@link PartialTezosOperation}). * * Fills the fields a caller cannot specify up front: `source`, a sequential `counter` * starting at `sequence`, and any missing `fee`/`gas_limit`/`storage_limit` (estimated * against the node). Prepends a REVEAL when the sender's manager key is not yet * revealed on-chain. * * Returns the `0x03`-watermarked forged hex produced by {@link rawEncode} — the same * format the structured `craft()` path returns and broadcasts, so the existing * `signer.signTransaction` + `combine` + broadcast path applies to it unchanged. */ async function craftRawOperations(context, rawTransaction, sender, publicKey, sequence) { const config = await context.config(); if (sequence < 0n || sequence > BigInt(Number.MAX_SAFE_INTEGER)) { throw new Error('craftRawOperations: sequence is out of the safe integer range'); } if (!isImplicitAddress(sender)) { throw new Error('craftRawOperations: `sender` must be a valid implicit account address'); } const operations = parseOperations(rawTransaction); const tezosToolkit = (0, tezosToolkit_1.getTezosToolkit)(config); const normalizedPk = (0, utils_2.normalizePublicKeyForAddress)(publicKey, sender); // Mock signer lets Taquito estimate without a connected device. tezosToolkit.setProvider({ signer: (0, utils_2.createMockSigner)(sender, normalizedPk ?? '') }); const feesConfig = config.fees; const minFees = feesConfig.minFees; let counter = Number(sequence); const contents = []; // A manager key that is not yet revealed on-chain requires a leading REVEAL op. const managerKey = await tezosToolkit.rpc.getManagerKey(sender); const needsReveal = !managerKey; if (needsReveal) { if (!normalizedPk || (0, utils_1.validatePublicKey)(normalizedPk) !== utils_1.ValidationResult.VALID) { throw new Error('craftRawOperations: a valid public key is required to reveal the sender'); } // The key must belong to the sender, otherwise the node rejects it as inconsistent_hash. if ((0, utils_1.getPkhfromPk)(normalizedPk) !== sender) { throw new Error('craftRawOperations: public key does not match the sender address'); } const reveal = await (0, estimateRevealLimits_1.estimateRevealLimits)(tezosToolkit, sender, feesConfig); contents.push({ kind: rpc_1.OpKind.REVEAL, source: sender, counter: counter.toString(), fee: reveal.fee.toString(), gas_limit: reveal.gasLimit.toString(), storage_limit: reveal.storageLimit.toString(), public_key: normalizedPk, }); counter += 1; } const estimates = await estimateOperations(operations, sender, needsReveal, tezosToolkit); operations.forEach((op, index) => { contents.push(buildOperationContent(op, sender, counter, minFees, estimates?.[index])); counter += 1; }); return (0, craftTransaction_1.rawEncode)(config, contents); } function parseOperations(rawTransaction) { let parsed; try { parsed = JSON.parse(rawTransaction); } catch { throw new Error('craftRawOperations: rawTransaction must be a JSON array of operations'); } if (!Array.isArray(parsed) || parsed.length === 0) { throw new Error('craftRawOperations: expected a non-empty array of operations'); } return parsed.map(validateOperation); } function validateOperation(op) { if (!op || typeof op !== 'object') { throw new Error('craftRawOperations: invalid operation'); } const candidate = op; assertOptionalLimits(candidate); switch (candidate.kind) { case 'transaction': if (!isValidAddress(candidate.destination)) { throw new Error('craftRawOperations: a transaction requires a valid `destination` address'); } assertMutezAmount(candidate.amount); return candidate; case 'delegation': // A delegation with no `delegate` is an undelegation; when present it must be a valid // implicit account (an empty/invalid string must not be silently treated as absent). if (candidate.delegate !== undefined && !isImplicitAddress(candidate.delegate)) { throw new Error('craftRawOperations: `delegate` must be a valid implicit account address when present'); } return candidate; default: throw new errors_1.UnsupportedOperationKind('unsupported operation kind', { kind: String(candidate.kind), }); } } function isValidAddress(value) { return typeof value === 'string' && (0, utils_1.validateAddress)(value) === utils_1.ValidationResult.VALID; } function isImplicitAddress(value) { return typeof value === 'string' && (0, utils_1.validateKeyHash)(value) === utils_1.ValidationResult.VALID; } function assertMutezAmount(amount) { if (typeof amount !== 'string' || !/^\d+$/.test(amount)) { throw new Error('craftRawOperations: `amount` must be a non-negative integer string (mutez)'); } if (BigInt(amount) > BigInt(Number.MAX_SAFE_INTEGER)) { throw new Error('craftRawOperations: `amount` exceeds the safe integer range'); } } function assertOptionalLimits(candidate) { for (const field of ['fee', 'gas_limit', 'storage_limit']) { const value = candidate[field]; if (value !== undefined && (typeof value !== 'string' || !/^\d+$/.test(value))) { throw new Error(`craftRawOperations: \`${field}\` must be a non-negative integer string`); } } } /** * Estimate the whole batch in a single simulation so that gas/storage for operations * that depend on earlier ones in the same batch are accounted for (a per-op estimate * would simulate each against head state and can under-estimate). Returns one estimate * per operation, or `undefined` when every operation already carries all three limits. */ async function estimateOperations(operations, sender, needsReveal, tezosToolkit) { const needsEstimation = operations.some((op) => op.fee === undefined || op.gas_limit === undefined || op.storage_limit === undefined); if (!needsEstimation) return undefined; const params = operations.map((op) => toBatchParam(op, sender)); const estimates = await tezosToolkit.estimate.batch(params); // Taquito may prepend a single reveal estimate when the source is unrevealed. Accept N // (no prepend) or N+1 (prepended) only in that case; for a revealed source require // exactly N so a stray estimate fails loudly instead of silently misaligning per-op. const maxExpected = operations.length + (needsReveal ? 1 : 0); if (estimates.length < operations.length || estimates.length > maxExpected) { const expected = needsReveal ? `${operations.length} or ${maxExpected}` : `${operations.length}`; throw new Error(`craftRawOperations: expected ${expected} estimate(s) for ${operations.length} operation(s), got ${estimates.length}`); } // Keep the trailing N estimates, dropping a prepended reveal estimate if present. The // REVEAL op itself uses estimateRevealLimits (config-clamped, consistent with // craftTransaction) rather than this batch-prepended value, which is discarded. return estimates.slice(estimates.length - operations.length); } function toBatchParam(op, sender) { switch (op.kind) { case 'transaction': return { kind: rpc_1.OpKind.TRANSACTION, source: sender, to: op.destination, amount: Number(op.amount), mutez: true, ...(op.parameters ? { parameter: op.parameters } : {}), }; case 'delegation': return { kind: rpc_1.OpKind.DELEGATION, source: sender, ...(op.delegate === undefined ? {} : { delegate: op.delegate }), }; default: return assertNever(op); } } function buildOperationContent(op, sender, counter, minFees, estimate) { const limits = resolveLimits(op, minFees, estimate); switch (op.kind) { case 'transaction': return { kind: rpc_1.OpKind.TRANSACTION, source: sender, destination: op.destination, amount: op.amount, counter: counter.toString(), fee: limits.fee, gas_limit: limits.gasLimit, storage_limit: limits.storageLimit, ...(op.parameters ? { parameters: op.parameters } : {}), }; case 'delegation': return { kind: rpc_1.OpKind.DELEGATION, source: sender, counter: counter.toString(), fee: limits.fee, gas_limit: limits.gasLimit, storage_limit: limits.storageLimit, ...(op.delegate === undefined ? {} : { delegate: op.delegate }), }; default: return assertNever(op); } } // Compile-time exhaustiveness guard: a new PartialTezosOperation kind becomes a type error // here. validateOperation already rejects unknown kinds at runtime before crafting. function assertNever(op) { throw new errors_1.UnsupportedOperationKind('unsupported operation kind', { kind: String(op.kind), }); } /** * Honour any fee/gas/storage the caller already provided; fall back to the batch * estimate for the missing ones. */ function resolveLimits(op, minFees, estimate) { if (op.fee !== undefined && op.gas_limit !== undefined && op.storage_limit !== undefined) { return { fee: op.fee, gasLimit: op.gas_limit, storageLimit: op.storage_limit }; } if (!estimate) { throw new Error('craftRawOperations: missing fee estimation for operation'); } return { fee: op.fee ?? Math.max(minFees, estimate.suggestedFeeMutez).toString(), gasLimit: op.gas_limit ?? estimate.gasLimit.toString(), storageLimit: op.storage_limit ?? estimate.storageLimit.toString(), }; } //# sourceMappingURL=craftRawOperations.js.map