@ledgerhq/coin-tezos
Version:
244 lines (216 loc) • 9.22 kB
text/typescript
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0
import type { TezosCoinConfig, TezosConfig, TezosContext } from '../config'
import { localForger } from '@taquito/local-forging'
import { OpKind } from '@taquito/rpc'
import { createTzktApi } from '../network/tzkt'
import { createApi } from '.'
/**
* Unrevealed mainnet account (see tzkt explorer).
*
* If this account is revealed, tests will fail and will need to be updated with a new one.
* New unreveal account if necessary
* Hash: tz1Y7xGN7jVjTTjaz6fpAeCMLiEENRkzHk4i
* Public Key: edpkuidtssPLLHKZCo9uKDGy4nnXKeBn1Kv5h4DdWZ3d8G5tcUBy4B
*/
const UNREVEALED_SENDER = 'tz2RZHPmNVQY7h1oopsvDBMrp8i48zhc9cAL'
const UNREVEALED_PUBLIC_KEY = '0268eae382350b43ba1af0e1140e4b68dc9d2084836494554860c1be0605a7af2d'
const defaultConfig: TezosConfig = {
baker: {
url: 'https://baker.example.com',
},
explorer: {
url: 'https://xtz-tzkt-explorer.api.vault.ledger.com',
maxTxQuery: 100,
},
node: {
url: 'https://xtz-node.api.live.ledger.com',
},
fees: {
minGasLimit: 600,
minRevealGasLimit: 300,
minStorageLimit: 0,
minFees: 500,
minEstimatedFees: 500,
},
}
/** Decoded operation. */
type DecodedOperation = {
kind: number
fee?: string
gas_limit?: string
storage_limit?: string
delegate?: string
source?: string
}
/**
* Mainnet-specific integration tests
* Please implement your tests in testnet-specific tests file (see index.integ.test.ts)
* Use this test suite in last resort
*/
describe('Tezos Api - Mainnet', () => {
const module = createApi()
const context: TezosContext = {
config: async () => defaultConfig as TezosCoinConfig,
logger: () => undefined,
}
const tzktApi = createTzktApi(defaultConfig as TezosCoinConfig)
describe('estimateFees', () => {
it('fallback to an estimation without the public key', async () => {
// When
const result = await module.estimateFees(context, {
intentType: 'transaction',
asset: { type: 'native' },
type: 'send',
sender: 'tz2BHzkaizWwCmhYswwTQCycgT8mXFH8QTL5',
senderPublicKey: '038ad002bc17d427794e148c9b9b464e977dc3cbc4d76148e97641ae86e46ec176',
recipient: 'tz2JpHaYBjGdTq7b5mWFpG9AP3L3CckdRJcA',
amount: BigInt(100),
})
expect(result.value).toBeGreaterThanOrEqual(BigInt(0))
expect(result.parameters?.gasLimit).toBeGreaterThanOrEqual(BigInt(0))
expect(result.parameters?.storageLimit).toBeGreaterThanOrEqual(BigInt(0))
})
})
describe('encode', () => {
it('encode a reveal operation without failing', async () => {
// When
const result = await module.craftTransaction(
context,
{
intentType: 'transaction',
asset: { type: 'native' },
type: 'send',
sender: 'tz2F4XnSd1wjwWsthemvZQjoPER7NVSt35k3',
senderPublicKey: '03576c19462a7d0cc3d121b1b00e92258b5f71d643c99a599fc1683f03abb7a1c2',
recipient: 'tz2TLTEWhG87pPKK7MxYLavitGGwje1znRwQ',
amount: BigInt(500000),
},
{ customFees: { value: BigInt(832) } }
)
expect(result).toEqual({ transaction: expect.stringMatching(/^([A-Fa-f0-9]{2})+$/) })
})
it('includes a properly estimated reveal operation when delegating from an unrevealed account with senderPublicKey', async () => {
const sender = UNREVEALED_SENDER
const account = await tzktApi.getAccountByAddress(sender)
expect(account).toMatchObject({
type: 'user',
revealed: false,
})
const delegate = 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D'
const intent = {
intentType: 'transaction' as const,
asset: { type: 'native' as const },
type: 'delegate' as const,
sender,
senderPublicKey: UNREVEALED_PUBLIC_KEY,
recipient: delegate,
amount: BigInt(0),
}
const { transaction: encodedTransaction } = await module.craftTransaction(context, intent)
const decoded = await localForger.parse(encodedTransaction.slice(2)) // strip 03 prefix
expect(decoded.contents.length).toBe(2)
const reveal = decoded.contents[0] as DecodedOperation
expect(reveal.kind).toBe(OpKind.REVEAL)
expect(BigInt(reveal.fee ?? '0')).toBeGreaterThanOrEqual(500)
expect(Number(reveal.gas_limit ?? '0')).toBeGreaterThanOrEqual(300)
expect(Number(reveal.storage_limit ?? '0')).toBeGreaterThanOrEqual(0)
expect(reveal.source).toEqual(sender)
const delegation = decoded.contents[1] as DecodedOperation
expect(delegation.kind).toBe(OpKind.DELEGATION)
expect(BigInt(delegation.fee ?? '0')).toBeGreaterThanOrEqual(500)
expect(Number(delegation.gas_limit ?? '0')).toBeGreaterThanOrEqual(2500)
expect(Number(delegation.storage_limit ?? '0')).toBeGreaterThanOrEqual(0)
expect(delegation.delegate).toEqual(delegate)
expect(delegation.source).toEqual(sender)
const total = decoded.contents.reduce((sum, op) => sum + BigInt(op.fee ?? '0'), 0n)
expect(total).toBeGreaterThanOrEqual(1000n)
})
})
describe('with custom fees settings', () => {
const contextCustomFees: TezosContext = {
config: async () =>
({
...defaultConfig,
fees: {
minGasLimit: 600,
minRevealGasLimit: 300,
minStorageLimit: 0,
minFees: 1200,
minEstimatedFees: 1100,
},
}) as TezosCoinConfig,
logger: () => undefined,
}
describe('estimateFees', () => {
it('respects minFees on a composite transaction', async () => {
const result = await module.estimateFees(contextCustomFees, {
intentType: 'transaction',
asset: { type: 'native' },
type: 'delegate',
sender: UNREVEALED_SENDER,
senderPublicKey: UNREVEALED_PUBLIC_KEY,
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
amount: BigInt(0),
})
// minFees should be used for both operations, so total should be minFees * 2
expect(result.value).toBe(2400n)
})
describe('craftTransaction', () => {
it('respects customFee on a composite transaction if greater than default estimation', async () => {
const { transaction: encodedTransaction } = await module.craftTransaction(
contextCustomFees,
{
intentType: 'transaction',
asset: { type: 'native' },
type: 'delegate',
sender: UNREVEALED_SENDER,
senderPublicKey: UNREVEALED_PUBLIC_KEY,
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
amount: BigInt(0),
},
{ customFees: { value: 3000n } }
)
const decoded = await localForger.parse(encodedTransaction.slice(2))
const reveal = decoded.contents[0] as DecodedOperation
expect(reveal.kind).toBe(OpKind.REVEAL)
expect(BigInt(reveal.fee ?? '0')).toEqual(1200n)
const delegation = decoded.contents[1] as DecodedOperation
expect(delegation.kind).toBe(OpKind.DELEGATION)
expect(BigInt(delegation.fee ?? '0')).toBeGreaterThanOrEqual(1800n)
// The provided customFee exceeds the default estimation. In this case, we expect the fee to be increased on
// the main operation (delegate) so that total fee paid respects customFee.
const totalFees = decoded.contents.reduce((sum, op) => sum + BigInt(op.fee ?? '0'), 0n)
expect(totalFees).toBe(3000n)
})
it('respects customFee on a composite transaction if lesser than default estimation', async () => {
const { transaction: encodedTransaction } = await module.craftTransaction(
contextCustomFees,
{
intentType: 'transaction',
asset: { type: 'native' },
type: 'delegate',
sender: 'tz1Y7xGN7jVjTTjaz6fpAeCMLiEENRkzHk4i',
senderPublicKey: 'edpkuidtssPLLHKZCo9uKDGy4nnXKeBn1Kv5h4DdWZ3d8G5tcUBy4B',
recipient: 'tz3Vq38qYD3GEbWcXHMLt5PaASZrkDtEiA8D',
amount: BigInt(0),
},
{ customFees: { value: 2000n } }
)
const decoded = await localForger.parse(encodedTransaction.slice(2))
const reveal = decoded.contents[0] as DecodedOperation
expect(reveal.kind).toBe(OpKind.REVEAL)
expect(BigInt(reveal.fee ?? '0')).toEqual(1200n)
const delegation = decoded.contents[1] as DecodedOperation
expect(delegation.kind).toBe(OpKind.DELEGATION)
expect(BigInt(delegation.fee ?? '0')).toBeGreaterThanOrEqual(800n)
// The provided customFee is less than the default estimation. In this case, we expect the fee to be decreased
// on the main operation (delegate) so that total fee paid respects customFee. Transaction could fail, but
// that's caller responsibility.
const totalFees = decoded.contents.reduce((sum, op) => sum + BigInt(op.fee ?? '0'), 0n)
expect(totalFees).toBe(2000n)
})
})
})
})
})