UNPKG

@ledgerhq/coin-tezos

Version:
1,087 lines (988 loc) 34.2 kB
// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { TezosCoinConfig, TezosContext } from '../config' import type { APIAccount } from '../network/types' import type { BalanceOptions, Operation } from '@ledgerhq/coin-module-framework/api/types' import type { TransactionIntent } from '@ledgerhq/coin-module-framework/api/types' import { createApi } from './index' // The tzkt factory `createTzktApi(config)` builds a fresh object on every call, so a spy on a // single instance would not intercept the calls the source makes internally. We mock the module // so `createTzktApi` always returns the SAME shared object, and expose its `getAccountByAddress` // jest.fn as `mockGetAccountByAddress` for the assertions below (which used to target // `mockGetAccountByAddress`). const mockGetAccountByAddress = jest.fn() jest.mock('../network/tzkt', () => ({ createTzktApi: () => ({ getAccountByAddress: mockGetAccountByAddress }), })) const DEFAULT_ESTIMATED_FEES = 300n const DEFAULT_GAS_LIMIT = 30n const DEFAULT_STORAGE_LIMIT = 40n const logicGetTransactions = jest.fn() const logicEstimateFees = jest.fn() const logicCraftTransactionMock = jest.fn( (_context: unknown, _account: unknown, _transaction: { fee: { fees: string } }) => { return { type: undefined, contents: undefined } } ) const logicCraftRawOperationsMock = jest.fn() jest.mock('../logic', () => ({ listOperations: async () => logicGetTransactions(), estimateFees: (...args: unknown[]) => logicEstimateFees(...args), craftTransaction: (context: unknown, account: unknown, transaction: { fee: { fees: string } }) => logicCraftTransactionMock(context, account, transaction), craftRawOperations: (...args: unknown[]) => logicCraftRawOperationsMock(...args), rawEncode: () => Promise.resolve('tz1heMGVHQnx7ALDcDKqez8fan64Eyicw4DJ'), })) const mockGetTezosToolkit = jest.fn() jest.mock('../logic/tezosToolkit', () => ({ getTezosToolkit: () => mockGetTezosToolkit(), })) mockGetAccountByAddress.mockResolvedValue({ type: 'user', balance: 1000, revealed: true, address: 'tz1test', publicKey: 'edpktest', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount) const api = createApi() /** Builds a TezosContext carrying the given coin config (see logic/getBalance.test.ts). */ function makeContext(config: unknown): TezosContext { return { config: async () => config as TezosCoinConfig, logger: () => undefined, } } const context = makeContext({ baker: { url: 'https://baker.example.com', }, explorer: { url: 'foo', maxTxQuery: 1, }, node: { url: 'bar', }, fees: { minGasLimit: 1, minRevealGasLimit: 1, minStorageLimit: 1, minFees: 1, minEstimatedFees: 2, }, }) describe('craftRawTransaction', () => { afterEach(() => { logicCraftRawOperationsMock.mockClear() }) it('delegates to craftRawOperations and wraps the result', async () => { logicCraftRawOperationsMock.mockResolvedValue('03deadbeef') const rawTransaction = '[{"kind":"transaction","destination":"tz1recipient","amount":"1000"}]' const result = await api.craftRawTransaction( context, rawTransaction, 'tz1sender', 'edpkpublickey', 5n ) expect(logicCraftRawOperationsMock).toHaveBeenCalledWith( expect.anything(), rawTransaction, 'tz1sender', 'edpkpublickey', 5n ) expect(result).toEqual({ transaction: '03deadbeef' }) }) }) describe('get operations', () => { afterEach(() => { logicGetTransactions.mockClear() }) it('could return no operation', async () => { logicGetTransactions.mockResolvedValue([[], '']) const { items: operations, next: token } = await api.listOperations(context, 'addr', { minHeight: 100, order: 'asc', }) expect(operations).toEqual([]) expect(token).toBeUndefined() }) const op: Operation = { id: 'blockhash', asset: { type: 'native' }, tx: { hash: 'opHash', fees: BigInt(100), block: { hash: 'blockHash', height: 123456, time: new Date(), }, date: new Date(), failed: false, }, type: 'transaction', value: BigInt(1000), senders: ['tz1Sender'], recipients: ['tz1Recipient'], } it('only does 1 iteration', async () => { logicGetTransactions.mockResolvedValue([[op], '888']) const { items: operations, next: token } = await api.listOperations(context, 'addr', { minHeight: 100, order: 'asc', }) expect(logicGetTransactions).toHaveBeenCalledTimes(1) expect(operations.length).toBe(1) expect(token).toEqual('888') }) }) describe('craftTransaction', () => { beforeEach(() => jest.clearAllMocks()) describe('when suggested fee is above or below minFees floor', () => { it('craft transaction when default estimation is greater than minFees', async () => { const estimatedFees = 500n logicEstimateFees.mockResolvedValue({ estimatedFees, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, parameters: { gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, txFee: 500n, }, }) await api.craftTransaction(context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 1000n, } as TransactionIntent) expect(logicEstimateFees).toHaveBeenCalledTimes(1) expect(logicCraftTransactionMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ address: 'tz1test' }), expect.objectContaining({ fee: expect.objectContaining({ fees: estimatedFees.toString(), gasLimit: DEFAULT_GAS_LIMIT.toString(), storageLimit: DEFAULT_STORAGE_LIMIT.toString(), }), }) ) }) it('craft transaction when default estimation is lesser than minFees', async () => { const estimatedFees = 50n logicEstimateFees.mockResolvedValue({ estimatedFees, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, parameters: { gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, txFee: 50n, }, }) await api.craftTransaction(context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 1000n, } as TransactionIntent) expect(logicCraftTransactionMock).toHaveBeenCalledWith( expect.anything(), expect.any(Object), expect.objectContaining({ fee: expect.objectContaining({ fees: estimatedFees.toString() }), }) ) }) }) describe('with customFee', () => { it('craft transaction with customFee when default estimation is greater than customFee', async () => { const defaultEstimatedFees = 500n const customFee = 100n logicEstimateFees.mockResolvedValue({ estimatedFees: defaultEstimatedFees, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, parameters: { gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }, }) await api.craftTransaction( context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 1000n, } as TransactionIntent, { customFees: { value: customFee } } ) expect(logicCraftTransactionMock).toHaveBeenCalledWith( expect.anything(), expect.any(Object), expect.objectContaining({ fee: expect.objectContaining({ fees: customFee.toString() }), }) ) }) it('craft transaction with customFee when default estimation is lesser than customFee', async () => { const customFee = 500n logicEstimateFees.mockResolvedValue({ estimatedFees: 100n, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, parameters: { gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }, }) await api.craftTransaction( context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 1000n, } as TransactionIntent, { customFees: { value: customFee } } ) expect(logicCraftTransactionMock).toHaveBeenCalledWith( expect.anything(), expect.any(Object), expect.objectContaining({ fee: expect.objectContaining({ fees: customFee.toString() }), }) ) }) it('craft transaction with customFee splits total for unrevealed delegate so total equals customFee', async () => { const minFees = 1000 const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V' const delegateRecipient = 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D' const totalCustomFee = BigInt(minFees * 2) const contextMinFees1000 = makeContext({ baker: { url: 'https://baker.example.com' }, explorer: { url: 'foo', maxTxQuery: 1 }, node: { url: 'bar' }, fees: { minGasLimit: 600, minRevealGasLimit: 300, minStorageLimit: 0, minFees, minEstimatedFees: minFees, }, }) const unrevealedAccount = { type: 'user' as const, balance: 1000000, revealed: false, address: unrevealedSender, publicKey: 'sppktest', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount mockGetAccountByAddress .mockResolvedValueOnce(unrevealedAccount) .mockResolvedValueOnce(unrevealedAccount) logicEstimateFees.mockResolvedValue({ estimatedFees: totalCustomFee, fees: 1000n, gasLimit: 10000n, storageLimit: 0n, parameters: { gasLimit: 10000n, storageLimit: 0n, txFee: 1000n, }, }) await api.craftTransaction( contextMinFees1000, { intentType: 'staking', type: 'delegate', sender: unrevealedSender, senderPublicKey: '021bab48f41fc555e0fcf322a28e31b56f4369242f65324758ec8bbae3e84109a5', recipient: delegateRecipient, amount: 0n, } as TransactionIntent, { value: totalCustomFee } ) expect(logicCraftTransactionMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ address: unrevealedSender }), expect.objectContaining({ fee: expect.objectContaining({ fees: String(minFees), gasLimit: '10000', storageLimit: '0', }), }) ) }) }) it.each(['stake', 'unstake'] as const)( 'passes %s mode and explicit amount through to craftTransaction', async (type) => { logicEstimateFees.mockResolvedValue({ estimatedFees: DEFAULT_ESTIMATED_FEES, fees: DEFAULT_ESTIMATED_FEES, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }) await api.craftTransaction(context, { intentType: 'staking', type, sender: 'tz1test', recipient: '', amount: 1234n, } as TransactionIntent) expect(logicCraftTransactionMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ address: 'tz1test' }), expect.objectContaining({ type, amount: 1234n, }) ) } ) it('passes finalize_unstake mode with zero amount through to craftTransaction', async () => { logicEstimateFees.mockResolvedValue({ estimatedFees: DEFAULT_ESTIMATED_FEES, fees: DEFAULT_ESTIMATED_FEES, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }) await api.craftTransaction(context, { intentType: 'staking', type: 'finalize_unstake', sender: 'tz1test', recipient: '', amount: 999n, } as TransactionIntent) expect(logicCraftTransactionMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ address: 'tz1test' }), expect.objectContaining({ type: 'finalize_unstake', amount: 0n, }) ) }) }) describe('estimateFees', () => { beforeEach(() => jest.clearAllMocks()) describe('when suggested fee is above or below minFees floor', () => { it('estimate fees when default estimation is greater than minFees', async () => { const estimatedFees = 500n logicEstimateFees.mockResolvedValue({ estimatedFees, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }) const result = await api.estimateFees(context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 1000n, } as TransactionIntent) expect(result.value).toBe(estimatedFees) expect(result.parameters?.gasLimit).toBe(DEFAULT_GAS_LIMIT) expect(result.parameters?.storageLimit).toBe(DEFAULT_STORAGE_LIMIT) }) it('estimate fees when default estimation is lesser than minFees', async () => { const estimatedFees = 100n logicEstimateFees.mockResolvedValue({ estimatedFees, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }) const result = await api.estimateFees(context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 1000n, } as TransactionIntent) expect(result.value).toBe(estimatedFees) expect(result.parameters?.gasLimit).toBe(DEFAULT_GAS_LIMIT) expect(result.parameters?.storageLimit).toBe(DEFAULT_STORAGE_LIMIT) }) }) it('returns estimation from logic module', async () => { logicEstimateFees.mockResolvedValue({ estimatedFees: DEFAULT_ESTIMATED_FEES, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }) const result = await api.estimateFees(context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 1000n, } as TransactionIntent) expect(result).toEqual({ value: DEFAULT_ESTIMATED_FEES, parameters: { gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }, }) }) it('forwards stakedBalance and unstakedBalance to the logic estimate for send-max', async () => { mockGetAccountByAddress.mockResolvedValueOnce({ type: 'user', balance: 1000, revealed: true, address: 'tz1test', publicKey: 'edpktest', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', stakedBalance: 200, unstakedBalance: 300, }) logicEstimateFees.mockResolvedValue({ estimatedFees: DEFAULT_ESTIMATED_FEES, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, amount: 500n, }) await api.estimateFees(context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 0n, useAllAmount: true, } as TransactionIntent) expect(logicEstimateFees).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ account: expect.objectContaining({ stakedBalance: 200n, unstakedBalance: 300n }), }) ) }) it('should throw taquito errors', async () => { logicEstimateFees.mockResolvedValue({ estimatedFees: DEFAULT_ESTIMATED_FEES, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, taquitoError: 'test', }) await expect( api.estimateFees(context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 1000n, } as TransactionIntent) ).rejects.toThrow('Fees estimation failed: test') }) it('should not throw for FA2 script_rejected errors (returns estimation)', async () => { logicEstimateFees.mockResolvedValue({ estimatedFees: DEFAULT_ESTIMATED_FEES, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, taquitoError: 'proto.024-PtTALLiN.michelson_v1.script_rejected', }) const result = await api.estimateFees(context, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz1recipient', amount: 1000n, asset: { type: 'token', assetReference: 'KT1CpeSQKdkhWi4pinYcseCFKmDhs5M74BkU:0' }, } as TransactionIntent) expect(result.value).toBe(DEFAULT_ESTIMATED_FEES) expect(result.parameters?.gasLimit).toBe(DEFAULT_GAS_LIMIT) expect(result.parameters?.storageLimit).toBe(DEFAULT_STORAGE_LIMIT) }) it('should not throw for delegate.unchanged errors', async () => { logicEstimateFees.mockResolvedValue({ estimatedFees: DEFAULT_ESTIMATED_FEES, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, taquitoError: 'proto.022-PsRiotum.delegate.unchanged', }) const result = await api.estimateFees(context, { intentType: 'staking', type: 'delegate', sender: 'tz1test', recipient: 'tz1validator', amount: 0n, } as TransactionIntent) expect(result).toEqual({ value: DEFAULT_ESTIMATED_FEES, parameters: { gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }, }) }) it('should not throw for cannot_stake_with_unfinalizable_unstake errors', async () => { logicEstimateFees.mockResolvedValue({ estimatedFees: DEFAULT_ESTIMATED_FEES, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, taquitoError: 'proto.alpha.cannot_stake_with_unfinalizable_unstake_requests_to_another_delegate', }) const result = await api.estimateFees(context, { intentType: 'staking', type: 'stake', sender: 'tz1test', recipient: 'tz1validator', amount: 1000n, } as TransactionIntent) expect(result).toEqual({ value: DEFAULT_ESTIMATED_FEES, parameters: { gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }, }) }) it('returns total estimation for unrevealed delegate when minFees applied (composite)', async () => { const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V' mockGetAccountByAddress.mockResolvedValueOnce({ type: 'user', balance: 1000000, revealed: false, address: unrevealedSender, publicKey: undefined, counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount) const expectedTotalFees = 2000n logicEstimateFees.mockResolvedValue({ estimatedFees: expectedTotalFees, fees: 1000n, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }) const result = await api.estimateFees(context, { intentType: 'staking', type: 'delegate', sender: unrevealedSender, recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D', amount: 0n, } as TransactionIntent) expect(result.value).toBe(expectedTotalFees) expect(result.parameters.gasLimit).toBe(DEFAULT_GAS_LIMIT) expect(result.parameters.storageLimit).toBe(DEFAULT_STORAGE_LIMIT) }) it.each([ ['stake', 1234n, 1234n], ['unstake', 1234n, 1234n], ['finalize_unstake', 999n, 0n], ] as const)('delegates %s estimation to the logic layer', async (type, intentAmount, amount) => { logicEstimateFees.mockResolvedValue({ estimatedFees: DEFAULT_ESTIMATED_FEES, fees: DEFAULT_ESTIMATED_FEES, gasLimit: DEFAULT_GAS_LIMIT, storageLimit: DEFAULT_STORAGE_LIMIT, }) const result = await api.estimateFees(context, { intentType: 'staking', type, sender: 'tz1test', recipient: '', amount: intentAmount, } as TransactionIntent) expect(result.value).toBe(DEFAULT_ESTIMATED_FEES) expect(logicEstimateFees).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ transaction: expect.objectContaining({ mode: type, amount, }), }) ) }) it('fallback when Taquito throws Public key not found returns total with minFees for reveal (unrevealed)', async () => { const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V' const delegateRecipient = 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D' const minFees = 1000 const contextMinFees1000 = makeContext({ baker: { url: 'https://baker.example.com' }, explorer: { url: 'foo', maxTxQuery: 1 }, node: { url: 'bar' }, fees: { minGasLimit: 600, minRevealGasLimit: 300, minStorageLimit: 0, minFees, minEstimatedFees: minFees, }, }) logicEstimateFees.mockImplementation(() => Promise.reject(new Error('Public key not found'))) const defaultUserForTry = { type: 'user' as const, balance: 1000, revealed: true, address: unrevealedSender, publicKey: 'edpktest', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount const recipientAccount = { type: 'user' as const, balance: 1000000, address: delegateRecipient, counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount const senderUnrevealedAccount = { type: 'user' as const, balance: 1000000, revealed: false, address: unrevealedSender, publicKey: undefined, counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount mockGetAccountByAddress .mockImplementationOnce(() => Promise.resolve(defaultUserForTry)) .mockImplementationOnce(() => Promise.resolve(recipientAccount)) .mockImplementationOnce(() => Promise.resolve(senderUnrevealedAccount)) mockGetTezosToolkit.mockReturnValueOnce({ estimate: { transfer: jest.fn().mockResolvedValue({ suggestedFeeMutez: 100, gasLimit: 10000, storageLimit: 0, burnFeeMutez: 0, opSize: 100, }), }, }) const result = await api.estimateFees(contextMinFees1000, { intentType: 'staking', type: 'delegate', sender: unrevealedSender, recipient: delegateRecipient, amount: 0n, } as TransactionIntent) expect(result.value).toBe(2000n) expect(result.parameters?.txFee).toBe(1000n) expect(result.parameters?.gasLimit).toBe(10000n) expect(result.parameters?.storageLimit).toBe(0n) logicEstimateFees.mockReset() }) it('uses static fallback gas when both logicEstimate and toolkit.estimate.transfer throw (revealed sender)', async () => { // SAFE_FALLBACK_GAS = 2500, FALLBACK_OP_SIZE_BYTES = 154 // baseTxFee = max(minFees=1, ceil(120 + 0.1*2500 + 154)) = max(1, 524) = 524 // createApi here to reset the global coinConfig to minFees=1 (other tests may leave it at a higher value) const contextLowMinFees = makeContext({ baker: { url: 'https://baker.example.com' }, explorer: { url: 'foo', maxTxQuery: 1 }, node: { url: 'bar' }, fees: { minGasLimit: 1, minRevealGasLimit: 1, minStorageLimit: 1, minFees: 1, minEstimatedFees: 2, }, }) const revealedSender = 'tz1test' const recipient = 'tz2recipient' const revealedAccount = { type: 'user' as const, balance: 1000000, revealed: true, address: revealedSender, publicKey: 'edpktest', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount const recipientAccount = { type: 'user' as const, balance: 1000000, revealed: true, address: recipient, publicKey: 'edpkrecipient', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount mockGetAccountByAddress .mockResolvedValueOnce(revealedAccount) // initial sender lookup .mockResolvedValueOnce(recipientAccount) // recipient lookup in catch block .mockResolvedValueOnce(revealedAccount) // sender lookup for needsReveal in catch block logicEstimateFees.mockRejectedValue(new Error('Public key not found')) mockGetTezosToolkit.mockReturnValueOnce({ estimate: { transfer: jest.fn().mockRejectedValue(new Error('Public key not found')), }, }) const result = await api.estimateFees(contextLowMinFees, { intentType: 'transaction', type: 'send', sender: revealedSender, recipient, amount: 1000n, } as TransactionIntent) // No reveal needed → revealFee = 0 → total = baseTxFee = 524 expect(result.value).toBe(524n) expect(result.parameters?.txFee).toBe(524n) expect(result.parameters?.gasLimit).toBe(2500n) expect(result.parameters?.storageLimit).toBe(0n) logicEstimateFees.mockReset() }) it('uses static fallback gas and adds reveal fee when sender is unrevealed', async () => { // baseTxFee = max(minFees=1, ceil(120 + 0.1*2500 + 154)) = 524 // revealFee = max(0, getRevealFee(tz2address)) — tz2 addresses have a non-zero reveal fee // createApi here to reset the global coinConfig to minFees=1 (other tests may leave it at a higher value) const contextLowMinFees = makeContext({ baker: { url: 'https://baker.example.com' }, explorer: { url: 'foo', maxTxQuery: 1 }, node: { url: 'bar' }, fees: { minGasLimit: 1, minRevealGasLimit: 1, minStorageLimit: 1, minFees: 1, minEstimatedFees: 2, }, }) const unrevealedSender = 'tz2TaTpo31sAiX2HBJUTLLdUnqVJR4QjLy1V' const recipient = 'tz1recipient' const unrevealedAccount = { type: 'user' as const, balance: 1000000, revealed: false, address: unrevealedSender, publicKey: undefined, counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount const recipientAccount = { type: 'user' as const, balance: 1000000, revealed: true, address: recipient, publicKey: 'edpkrecipient', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount mockGetAccountByAddress .mockResolvedValueOnce(unrevealedAccount) // initial sender lookup .mockResolvedValueOnce(recipientAccount) // recipient lookup in catch block .mockResolvedValueOnce(unrevealedAccount) // sender lookup for needsReveal in catch block logicEstimateFees.mockRejectedValue(new Error('Public key not found')) mockGetTezosToolkit.mockReturnValueOnce({ estimate: { transfer: jest.fn().mockRejectedValue(new Error('Public key not found')), }, }) const result = await api.estimateFees(contextLowMinFees, { intentType: 'transaction', type: 'send', sender: unrevealedSender, recipient, amount: 1000n, } as TransactionIntent) // baseTxFee = 524n; revealFee > 0 (tz2 secp256k1 address) // total > 524n and txFee stays at 524n expect(result.parameters?.txFee).toBe(524n) expect(result.parameters?.gasLimit).toBe(2500n) expect(result.parameters?.storageLimit).toBe(0n) expect(result.value).toBeGreaterThan(524n) logicEstimateFees.mockReset() }) it('uses minFees floor when it exceeds the static fallback calculation', async () => { // With minFees = 2000 > ceil(120 + 0.1*2500 + 154) = 524 → baseTxFee = 2000 const minFees = 2000 const contextHighMinFees = makeContext({ baker: { url: 'https://baker.example.com' }, explorer: { url: 'foo', maxTxQuery: 1 }, node: { url: 'bar' }, fees: { minGasLimit: 1, minRevealGasLimit: 1, minStorageLimit: 1, minFees, minEstimatedFees: minFees, }, }) const revealedAccount = { type: 'user' as const, balance: 1000000, revealed: true, address: 'tz1test', publicKey: 'edpktest', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount const recipientAccount = { type: 'user' as const, balance: 1000000, revealed: true, address: 'tz2recipient', publicKey: 'edpkrecipient', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount mockGetAccountByAddress .mockResolvedValueOnce(revealedAccount) .mockResolvedValueOnce(recipientAccount) .mockResolvedValueOnce(revealedAccount) logicEstimateFees.mockRejectedValue(new Error('Public key not found')) mockGetTezosToolkit.mockReturnValueOnce({ estimate: { transfer: jest.fn().mockRejectedValue(new Error('Public key not found')), }, }) const result = await api.estimateFees(contextHighMinFees, { intentType: 'transaction', type: 'send', sender: 'tz1test', recipient: 'tz2recipient', amount: 1000n, } as TransactionIntent) expect(result.value).toBe(BigInt(minFees)) expect(result.parameters?.txFee).toBe(BigInt(minFees)) expect(result.parameters?.gasLimit).toBe(2500n) logicEstimateFees.mockReset() }) }) describe('getBalance', () => { it('should throw an exception when options is provided', async () => { await expect( api.getBalance(context, 'random address', {} as unknown as BalanceOptions) ).rejects.toMatchObject({ name: 'InvalidParameterError' }) }) }) describe('getAccountInfo', () => { // `getAccountInfo` is optional on the generic CoinModuleApi (ADR-045 / LIVE-33431); coin-tezos // always implements it, so the non-null assertions below are safe. afterEach(() => { mockGetAccountByAddress.mockClear() }) it('returns revealed:true for a revealed user account', async () => { mockGetAccountByAddress.mockResolvedValueOnce({ type: 'user', balance: 1000, revealed: true, address: 'tz1test', publicKey: 'edpktest', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount) await expect(api.getAccountInfo!(context, 'tz1test')).resolves.toEqual({ type: 'tezos', revealed: true, }) expect(mockGetAccountByAddress).toHaveBeenCalledWith('tz1test') }) it('returns revealed:false for an unrevealed user account', async () => { mockGetAccountByAddress.mockResolvedValueOnce({ type: 'user', balance: 1000, revealed: false, address: 'tz2unrevealed', publicKey: '', counter: 0, delegationLevel: 0, delegationTime: '2021-01-01T00:00:00Z', numTransactions: 0, firstActivityTime: '2021-01-01T00:00:00Z', } as APIAccount) await expect(api.getAccountInfo!(context, 'tz2unrevealed')).resolves.toEqual({ type: 'tezos', revealed: false, }) expect(mockGetAccountByAddress).toHaveBeenCalledWith('tz2unrevealed') }) it('returns revealed:true for a registered baker (delegate) account', async () => { // tzkt reports bakers with type "delegate" (not "user"), still carrying revealed:true. // Regression test for LIVE-34256: bakers must not be reported as unrevealed. mockGetAccountByAddress.mockResolvedValueOnce({ type: 'delegate', balance: 616109, revealed: true, address: 'tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9', publicKey: 'p2pktest', counter: 18, } as APIAccount) await expect( api.getAccountInfo!(context, 'tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9') ).resolves.toEqual({ type: 'tezos', revealed: true, }) expect(mockGetAccountByAddress).toHaveBeenCalledWith('tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9') }) it('returns revealed:false for an empty / non-existent account', async () => { mockGetAccountByAddress.mockResolvedValueOnce({ type: 'empty', address: 'tz1empty', counter: 0, } as APIAccount) await expect(api.getAccountInfo!(context, 'tz1empty')).resolves.toEqual({ type: 'tezos', revealed: false, }) expect(mockGetAccountByAddress).toHaveBeenCalledWith('tz1empty') }) it('propagates errors from getAccountByAddress', async () => { mockGetAccountByAddress.mockRejectedValueOnce(new Error('boom')) await expect(api.getAccountInfo!(context, 'tz1boom')).rejects.toThrow('boom') }) })