Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/green-peaches-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xchainjs/xchain-zcash': patch
---

adding client ledger
4 changes: 2 additions & 2 deletions packages/xchain-zcash/__e2e__/client.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('Zcash client', () => {
const hash = await client.transfer({
walletIndex: 0,
amount: assetToBase(assetAmount('0.1', 8)),
recipient: address
recipient: address,
})
console.log('hash', hash)
})
Expand All @@ -37,7 +37,7 @@ describe('Zcash client', () => {
const hash = await client.transfer({
amount: assetToBase(assetAmount('0.1', 8)),
recipient: address,
memo: 'test'
memo: 'test',
})
console.log('hash', hash)
})
Expand Down
71 changes: 71 additions & 0 deletions packages/xchain-zcash/__e2e__/zcash-ledger-client.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import { Network } from '@xchainjs/xchain-client'
import { assetAmount, assetToBase } from '@xchainjs/xchain-util'
import { UtxoClientParams } from '@xchainjs/xchain-utxo'

import { ClientLedger } from '../src/clientLedger'
import { AssetZEC, LOWER_FEE_BOUND, NownodesProviders, UPPER_FEE_BOUND, zcashExplorerProviders } from '../src/const'

jest.setTimeout(200000)

const defaultZECParams: UtxoClientParams = {
network: Network.Mainnet,
phrase: '',
explorerProviders: zcashExplorerProviders,
dataProviders: [NownodesProviders],
rootDerivationPaths: {
[Network.Mainnet]: `m/44'/133'/0'/0/`,
[Network.Testnet]: `m/44'/1'/0'/0/`,
[Network.Stagenet]: `m/44'/133'/0'/0/`,
},
feeBounds: {
lower: LOWER_FEE_BOUND,
upper: UPPER_FEE_BOUND,
},
}
Comment on lines +11 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Avoid mainnet by default in tests; allow network selection via env

Mainnet usage in tests risks real funds and depends on mainnet-only providers. Default to testnet and allow overrides via an env var (ZCASH_NETWORK). Keep providers as-is; the underlying client should skip undefined per-network entries.

-const defaultZECParams: UtxoClientParams = {
-  network: Network.Mainnet,
+const selectedNetwork =
+  (process.env.ZCASH_NETWORK as keyof typeof Network) ? Network[process.env.ZCASH_NETWORK as keyof typeof Network] : Network.Testnet
+
+const defaultZECParams: UtxoClientParams = {
+  network: selectedNetwork,
   phrase: '',
   explorerProviders: zcashExplorerProviders,
   dataProviders: [NownodesProviders],
   rootDerivationPaths: {
-    [Network.Mainnet]: `m/44'/133'/0'/0/`,
-    [Network.Testnet]: `m/44'/1'/0'/0/`,
-    [Network.Stagenet]: `m/44'/133'/0'/0/`,
+    [Network.Mainnet]: `m/44'/133'/0'/0/`,
+    [Network.Testnet]: `m/44'/1'/0'/0/`,
+    [Network.Stagenet]: `m/44'/133'/0'/0/`,
   },
   feeBounds: {
     lower: LOWER_FEE_BOUND,
     upper: UPPER_FEE_BOUND,
   },
 }

Please confirm your explorer/data providers correctly support the selected network in your environment.


🏁 Script executed:

#!/usr/bin/env bash
# Print the first 200 lines of the E2E test to inspect default network usage
sed -n '1,200p' packages/xchain-zcash/__e2e__/zcash-ledger-client.e2e.ts

Length of output: 2277


🏁 Script executed:

#!/usr/bin/env bash
echo "Listing all E2E test files and default network usage"
find packages -type f -path "*/__e2e__/*.e2e.ts" | while read f; do
  echo "=== $f ==="
  sed -n '1,30p' "$f"
  echo
done

echo "Inspecting const.ts in xchain-zcash"
sed -n '1,200p' packages/xchain-zcash/src/const.ts

Length of output: 71803


Ensure Testnet Data Provider is Configured Before Defaulting to Testnet in E2E Tests

The current NownodesProviders mapping in packages/xchain-zcash/src/const.ts sets Network.Testnet to undefined, so if you switch the default network to Testnet, balance lookups and transfers will fail due to the missing data provider.

• packages/xchain-zcash/src/const.ts

  • Update NownodesProviders to include a Testnet provider instance, for example:
    const testnetNownodesProvider = new NownodesProvider(
      'https://testnet-nodes.nownodes.io/api/v2',
      ZECChain,
      AssetZEC,
      ZEC_DECIMAL,
      process.env.NOWNODES_API_KEY || ''
    )
    
    export const NownodesProviders: UtxoOnlineDataProviders = {
      [Network.Testnet]: testnetNownodesProvider,
      [Network.Stagenet]: mainnetNownodesProvider,
      [Network.Mainnet]: mainnetNownodesProvider,
    }

• packages/xchain-zcash/e2e/zcash-ledger-client.e2e.ts

  • Introduce dynamic network selection, defaulting to Testnet when ZCASH_NETWORK is not set:
    - const defaultZECParams: UtxoClientParams = {
    -   network: Network.Mainnet,
    + const selectedNetwork = process.env.ZCASH_NETWORK && (Network as any)[process.env.ZCASH_NETWORK]
    +   ? (Network as any)[process.env.ZCASH_NETWORK]
    +   : Network.Testnet
    +
    + const defaultZECParams: UtxoClientParams = {
    +   network: selectedNetwork,
        phrase: '',
        explorerProviders: zcashExplorerProviders,
        dataProviders: [NownodesProviders],
        rootDerivationPaths: {
          [Network.Mainnet]: `m/44'/133'/0'/0/`,
          [Network.Testnet]: `m/44'/1'/0'/0/`,
          [Network.Stagenet]: `m/44'/133'/0'/0/`,
        },
        feeBounds: {
          lower: LOWER_FEE_BOUND,
          upper: UPPER_FEE_BOUND,
        },
      }

• Validation Steps

  • Confirm that the updated NownodesProviders[Testnet] works for balance fetch and transaction broadcast.
  • Run the E2E suite against Testnet (ZCASH_NETWORK=Testnet) to ensure getBalance and transfer succeed.
🤖 Prompt for AI Agents
In packages/xchain-zcash/__e2e__/zcash-ledger-client.e2e.ts around lines 11-25,
the test currently hardcodes Mainnet and uses NownodesProviders that leave
Network.Testnet undefined; add dynamic network selection (read ZCASH_NETWORK env
var and default to Network.Testnet) so tests run against Testnet by default, and
update packages/xchain-zcash/src/const.ts to create and export a Testnet
NownodesProvider instance (configured with the testnet nownodes URL and API key
from env) and assign it to NownodesProviders[Network.Testnet] so balance lookups
and broadcasts succeed when Testnet is selected.


describe('Zcash Client Ledger', () => {
let zcashClient: ClientLedger
beforeAll(async () => {
const transport = await TransportNodeHid.create()

zcashClient = new ClientLedger({
transport,
...defaultZECParams,
})
})
it('get address async without verification', async () => {
const address = await zcashClient.getAddressAsync()
console.log('address', address)
expect(address).toMatch(/^t/) // Transparent addresses start with t
})

it('get address async with verification', async () => {
const address = await zcashClient.getAddressAsync(0, true)
console.log('address', address)
expect(address).toMatch(/^t/) // Transparent addresses start with t
})

it('get balance', async () => {
const address = await zcashClient.getAddressAsync()
const balance = await zcashClient.getBalance(address)
console.log('balance', balance[0].amount.amount().toString())
})

it('transfer', async () => {
try {
const to = await zcashClient.getAddressAsync(1)
const amount = assetToBase(assetAmount('0.00002'))
const txid = await zcashClient.transfer({
asset: AssetZEC,
recipient: to,
amount,
memo: 'test',
})
console.log(JSON.stringify(txid, null, 2))
} catch (err) {
console.error('ERR running test', err)
fail()
}
})
Comment on lines +55 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Assert the expected signing limitation error instead of failing the test

Transfers are not production-ready and should surface a clear error about missing raw transaction hex. Assert this error rather than failing generically. Also, specify ZEC decimals to avoid implicit assumptions.

-  it('transfer', async () => {
-    try {
-      const to = await zcashClient.getAddressAsync(1)
-      const amount = assetToBase(assetAmount('0.00002'))
-      const txid = await zcashClient.transfer({
-        asset: AssetZEC,
-        recipient: to,
-        amount,
-        memo: 'test',
-      })
-      console.log(JSON.stringify(txid, null, 2))
-    } catch (err) {
-      console.error('ERR running test', err)
-      fail()
-    }
-  })
+  it('transfer should reject with missing raw tx hex limitation', async () => {
+    const to = await zcashClient.getAddressAsync(1)
+    const amount = assetToBase(assetAmount('0.00002', 8)) // ZEC has 8 decimals
+    await expect(
+      zcashClient.transfer({
+        asset: AssetZEC,
+        recipient: to,
+        amount,
+        memo: 'test',
+      }),
+    ).rejects.toThrow(/raw\s+(transaction|tx)\s+(hex|data)/i)
+  })

If you prefer keeping the manual try/catch, at least assert the message and remove fail().

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('transfer', async () => {
try {
const to = await zcashClient.getAddressAsync(1)
const amount = assetToBase(assetAmount('0.00002'))
const txid = await zcashClient.transfer({
asset: AssetZEC,
recipient: to,
amount,
memo: 'test',
})
console.log(JSON.stringify(txid, null, 2))
} catch (err) {
console.error('ERR running test', err)
fail()
}
})
it('transfer should reject with missing raw tx hex limitation', async () => {
const to = await zcashClient.getAddressAsync(1)
const amount = assetToBase(assetAmount('0.00002', 8)) // ZEC has 8 decimals
await expect(
zcashClient.transfer({
asset: AssetZEC,
recipient: to,
amount,
memo: 'test',
}),
).rejects.toThrow(/raw\s+(transaction|tx)\s+(hex|data)/i)
})
🤖 Prompt for AI Agents
In packages/xchain-zcash/__e2e__/zcash-ledger-client.e2e.ts around lines 55 to
70, the test currently catches any error and calls fail(); instead assert the
expected "missing raw transaction hex" (signing limitation) error message so the
test passes when the client correctly indicates transfers are not supported, and
remove the generic fail() and console.error; also make the amount creation
explicit by specifying ZEC decimals (8) when calling assetAmount/assetToBase so
the test does not rely on implicit decimals. Ensure the test asserts the error
message/content (or uses an expect().rejects pattern) rather than treating any
thrown error as a test failure.

})
99 changes: 99 additions & 0 deletions packages/xchain-zcash/__tests__/clientLedger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ClientLedger, defaultZECParams } from '../src'

// Mock transport for testing
const mockTransport = {
exchange: jest.fn(),
setExchangeTimeout: jest.fn(),
close: jest.fn(),
}
Comment on lines +3 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Stabilize transport mock: include send and/or avoid hitting transport via spies

Tests that fail with “this.transport.send is not a function” are brittle because they depend on internal implementation details. Add a no-op send on the mock to avoid this class of failures.

 const mockTransport = {
-  exchange: jest.fn(),
+  exchange: jest.fn(),
+  send: jest.fn(), // add to stabilize tests that call into App
   setExchangeTimeout: jest.fn(),
   close: jest.fn(),
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Mock transport for testing
const mockTransport = {
exchange: jest.fn(),
setExchangeTimeout: jest.fn(),
close: jest.fn(),
}
// Mock transport for testing
const mockTransport = {
exchange: jest.fn(),
send: jest.fn(), // add to stabilize tests that call into App
setExchangeTimeout: jest.fn(),
close: jest.fn(),
}
🤖 Prompt for AI Agents
In packages/xchain-zcash/__tests__/clientLedger.test.ts around lines 3 to 8, the
transport mock lacks a send method which causes brittle failures like
"this.transport.send is not a function"; add a no-op send function to the mock
(e.g., send: jest.fn() or send: () => Promise.resolve()) or adjust tests to
avoid spying on internal transport.send—ensure the mock implements send so tests
don't hit missing-method errors.


describe('Zcash Ledger Client', () => {
let client: ClientLedger

beforeEach(() => {
client = new ClientLedger({
...defaultZECParams,
transport: mockTransport,
})
})

afterEach(() => {
jest.clearAllMocks()
})

describe('Constructor', () => {
it('should create a ClientLedger instance', () => {
expect(client).toBeInstanceOf(ClientLedger)
})

it('should throw error for sync getAddress method', () => {
expect(() => client.getAddress()).toThrow('Sync method not supported for Ledger')
})
})

describe('Address Operations', () => {
it('should have getApp method', async () => {
expect(client.getApp).toBeDefined()
expect(typeof client.getApp).toBe('function')
})

it('should have getAddressAsync method', async () => {
expect(client.getAddressAsync).toBeDefined()
expect(typeof client.getAddressAsync).toBe('function')
})
})

describe('Transaction Operations', () => {
it('should have transfer method', () => {
expect(client.transfer).toBeDefined()
expect(typeof client.transfer).toBe('function')
})

it('should throw error when transport is not properly configured', async () => {
const mockAmount = {
amount: () => ({ toNumber: () => 1000000 }),
}

const transferParams = {
recipient: 't1d4ZFodUN3sJz1zL6SfKSV6kmkwYm8N5s9',
amount: mockAmount,
memo: 'test',
}

// Since we're using a mock transport, it should fail at the transport level
await expect(client.transfer(transferParams as Parameters<typeof client.transfer>[0])).rejects.toThrow(
'this.transport.send is not a function',
)
})
Comment on lines +52 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Assert the documented signing limitation instead of relying on a TypeError

Per PR objectives, transfer should fail with a clear “raw tx hex” limitation. Stub getAddressAsync to avoid transport usage and assert on that error, not on a TypeError about send.

-    it('should throw error when transport is not properly configured', async () => {
-      const mockAmount = {
-        amount: () => ({ toNumber: () => 1000000 }),
-      }
-
-      const transferParams = {
-        recipient: 't1d4ZFodUN3sJz1zL6SfKSV6kmkwYm8N5s9',
-        amount: mockAmount,
-        memo: 'test',
-      }
-
-      // Since we're using a mock transport, it should fail at the transport level
-      await expect(client.transfer(transferParams as Parameters<typeof client.transfer>[0])).rejects.toThrow(
-        'this.transport.send is not a function',
-      )
-    })
+    it('should surface Ledger signing limitation when raw tx hex is unavailable', async () => {
+      // Avoid transport by stubbing sender derivation
+      jest.spyOn(client, 'getAddressAsync').mockResolvedValue('t1d4ZFodUN3sJz1zL6SfKSV6kmkwYm8N5s9')
+      const mockAmount = { amount: () => ({ toNumber: () => 1000000 }) }
+      const transferParams = {
+        recipient: 't1d4ZFodUN3sJz1zL6SfKSV6kmkwYm8N5s9',
+        amount: mockAmount,
+        memo: 'test',
+        // include asset if transfer expects it
+        asset: { chain: 'ZEC', symbol: 'ZEC', ticker: 'ZEC', type: 'NATIVE' } as any,
+      }
+      await expect(client.transfer(transferParams as Parameters<typeof client.transfer>[0])).rejects.toThrow(
+        /raw\s+(transaction|tx)\s+(hex|data)/i,
+      )
+    })

If you prefer, I can update the test to import AssetZEC instead of using a loose cast.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should throw error when transport is not properly configured', async () => {
const mockAmount = {
amount: () => ({ toNumber: () => 1000000 }),
}
const transferParams = {
recipient: 't1d4ZFodUN3sJz1zL6SfKSV6kmkwYm8N5s9',
amount: mockAmount,
memo: 'test',
}
// Since we're using a mock transport, it should fail at the transport level
await expect(client.transfer(transferParams as Parameters<typeof client.transfer>[0])).rejects.toThrow(
'this.transport.send is not a function',
)
})
it('should surface Ledger signing limitation when raw tx hex is unavailable', async () => {
// Avoid transport by stubbing sender derivation
jest.spyOn(client, 'getAddressAsync').mockResolvedValue('t1d4ZFodUN3sJz1zL6SfKSV6kmkwYm8N5s9')
const mockAmount = { amount: () => ({ toNumber: () => 1000000 }) }
const transferParams = {
recipient: 't1d4ZFodUN3sJz1zL6SfKSV6kmkwYm8N5s9',
amount: mockAmount,
memo: 'test',
// include asset if transfer expects it
asset: { chain: 'ZEC', symbol: 'ZEC', ticker: 'ZEC', type: 'NATIVE' } as any,
}
await expect(client.transfer(transferParams as Parameters<typeof client.transfer>[0])).rejects.toThrow(
/raw\s+(transaction|tx)\s+(hex|data)/i,
)
})
🤖 Prompt for AI Agents
In packages/xchain-zcash/__tests__/clientLedger.test.ts around lines 52 to 67,
the test currently relies on a TypeError from a mocked transport; instead stub
the client's getAddressAsync (or mock the ledger method that returns the
address) so transport.send is not invoked and the flow reaches the documented
signing limitation; then assert the rejection message matches the expected "raw
tx hex" limitation (e.g., use expect(...).rejects.toThrow('raw tx hex')) so the
test verifies the intended error; optionally replace the loose cast with an
imported AssetZEC when constructing transferParams for clarity.

})

describe('Network and Asset Info', () => {
it('should inherit getAssetInfo from base client', () => {
const assetInfo = client.getAssetInfo()
expect(assetInfo).toBeDefined()
expect(assetInfo.asset).toBeDefined()
expect(assetInfo.decimal).toBeDefined()
})

it('should inherit validateAddress from base client', () => {
// Test with a valid Zcash testnet address format
const isValid = client.validateAddress('t1d4ZFodUN3sJz1zL6SfKSV6kmkwYm8N5s9')
expect(typeof isValid).toBe('boolean')
})
})

describe('Fee Operations', () => {
it('should inherit fee methods from base client', async () => {
expect(client.getFees).toBeDefined()
expect(typeof client.getFees).toBe('function')
})

it('should throw error for fee rates (Zcash uses flat fees)', async () => {
await expect(client.getFeeRates()).rejects.toThrow('Error Zcash has flat fee. Fee rates not supported')
})

it('should throw error for fees with rates', async () => {
await expect(client.getFeesWithRates()).rejects.toThrow('Error Zcash has flat fee. Fee rates not supported')
})
})
})
2 changes: 1 addition & 1 deletion packages/xchain-zcash/jest.config.e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export default {
testPathIgnorePatterns: ['<rootDir>/node_modules', '<rootDir>/lib'],
testMatch: ['<rootDir>/__e2e__/**/*.[jt]s?(x)'],
testTimeout: 60000,
}
}
1 change: 1 addition & 0 deletions packages/xchain-zcash/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"directory": "release/package"
},
"devDependencies": {
"@ledgerhq/hw-transport-node-hid": "^6.28.6",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Dev dep looks fine, but runtime dependency on '@ledgerhq/hw-app-btc' is missing.

This package imports '@ledgerhq/hw-app-btc' in src/clientLedger.ts, but it’s not declared in dependencies. Relying on workspace hoisting is brittle and will break consumers.

Apply this diff to add the runtime dependency (version aligned to your transport to avoid API drift):

   "dependencies": {
     "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3",
     "@mayaprotocol/zcash-js": "1.0.7",
     "@scure/bip32": "^1.7.0",
     "@xchainjs/xchain-client": "workspace:*",
     "@xchainjs/xchain-crypto": "workspace:*",
     "@xchainjs/xchain-util": "workspace:*",
     "@xchainjs/xchain-utxo": "workspace:*",
     "@xchainjs/xchain-utxo-providers": "workspace:*",
-    "ecpair": "2.1.0"
+    "ecpair": "2.1.0",
+    "@ledgerhq/hw-app-btc": "^6.28.6"
   },

Optionally, if you only need types from '@ledgerhq/hw-transport', add it as a devDependency:

   "devDependencies": {
     "@ledgerhq/hw-transport-node-hid": "^6.28.6",
+    "@ledgerhq/hw-transport": "^6.28.6",
     "@types/blake2b-wasm": "^2.4.3",

Verify there’s no accidental transitive reliance:


🏁 Script executed:

#!/bin/bash
# Check direct imports vs declared deps for '@ledgerhq/hw-app-btc'
rg -n "@ledgerhq/hw-app-btc" packages/xchain-zcash/src | cat
jq -r '.dependencies | keys[]' packages/xchain-zcash/package.json | rg -n "^@ledgerhq/hw-app-btc$" -n || echo "MISSING in dependencies"

Length of output: 307


Add missing runtime dependency for '@ledgerhq/hw-app-btc'

The packages/xchain-zcash package imports @ledgerhq/hw-app-btc but does not declare it under "dependencies", causing builds or consumers to break when workspace hoisting isn’t in effect (verified via ripgrep and jq).

• packages/xchain-zcash/src/clientLedger.ts (line 1):
​ import AppBtc from '@ledgerhq/hw-app-btc'

• packages/xchain-zcash/package.json: no "@ledgerhq/hw-app-btc" key under "dependencies"

Apply this diff to ensure the runtime dependency is declared and version-aligned with your transport:

   "dependencies": {
     "@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3",
     "@mayaprotocol/zcash-js": "1.0.7",
     "@scure/bip32": "^1.7.0",
     "@xchainjs/xchain-client": "workspace:*",
     "@xchainjs/xchain-crypto": "workspace:*",
     "@xchainjs/xchain-util": "workspace:*",
     "@xchainjs/xchain-utxo": "workspace:*",
     "@xchainjs/xchain-utxo-providers": "workspace:*",
     "ecpair": "2.1.0",
+    "@ledgerhq/hw-app-btc": "^6.28.6"
   },

Optionally, if you depend only on types from @ledgerhq/hw-transport, declare it as a dev dependency:

   "devDependencies": {
     "@ledgerhq/hw-transport-node-hid": "^6.28.6",
+    "@ledgerhq/hw-transport": "^6.28.6",
     "@types/blake2b-wasm": "^2.4.3",
     …
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@ledgerhq/hw-transport-node-hid": "^6.28.6",
++ b/packages/xchain-zcash/package.json
@@ "dependencies": {
"@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3",
"@mayaprotocol/zcash-js": "1.0.7",
"@scure/bip32": "^1.7.0",
"@xchainjs/xchain-client": "workspace:*",
"@xchainjs/xchain-crypto": "workspace:*",
"@xchainjs/xchain-util": "workspace:*",
"@xchainjs/xchain-utxo": "workspace:*",
"@xchainjs/xchain-utxo-providers": "workspace:*",
"ecpair": "2.1.0",
"@ledgerhq/hw-app-btc": "^6.28.6"
},
@@ "devDependencies": {
"@ledgerhq/hw-transport-node-hid": "^6.28.6",
"@ledgerhq/hw-transport": "^6.28.6",
"@types/blake2b-wasm": "^2.4.3",
}
🤖 Prompt for AI Agents
In packages/xchain-zcash/package.json at line ~51, the package imports
'@ledgerhq/hw-app-btc' (see packages/xchain-zcash/src/clientLedger.ts) but
doesn't declare it as a runtime dependency; add "@ledgerhq/hw-app-btc":
"^6.28.6" (or match the same version range used for
"@ledgerhq/hw-transport-node-hid") to the "dependencies" section so
consumers/builds without workspace hoisting can resolve it; if you only need
types from a transport package instead, add that package as a devDependency
instead.

"@types/blake2b-wasm": "^2.4.3",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.13",
Expand Down
98 changes: 86 additions & 12 deletions packages/xchain-zcash/src/clientLedger.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,103 @@
import { TxHash } from '@xchainjs/xchain-client'
import AppBtc from '@ledgerhq/hw-app-btc'
import { buildTx } from '@mayaprotocol/zcash-js'
import { TxHash, checkFeeBounds } from '@xchainjs/xchain-client'
import { Address } from '@xchainjs/xchain-util'
import { UtxoClientParams } from '@xchainjs/xchain-utxo'

import { TxParams, UtxoClientParams } from '@xchainjs/xchain-utxo'
import { Client } from './client'

/**
* Custom Ledger Zcash client
*/
class ClientLedger extends Client {
constructor(params: UtxoClientParams) {
// Reference to the Ledger transport object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private transport: any
private app: AppBtc | undefined

// Constructor
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(params: UtxoClientParams & { transport: any }) {
super(params)
throw Error('Ledger client not supported for Zcash.')
this.transport = params.transport
}

public async getApp() {
throw Error('Not implemented.')
// Get the Ledger BTC application instance configured for Zcash
public async getApp(): Promise<AppBtc> {
if (this.app) {
return this.app
}
this.app = new AppBtc({ transport: this.transport, currency: 'zcash' })
return this.app
}
Comment on lines +24 to 31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect AppBtc construction — will produce this.transport.send is not a function.

new AppBtc({ transport, currency: 'zcash' }) passes an object where a Transport instance is expected. This will break calls like getWalletPublicKey. Use the Transport instance directly; if you need Zcash coin params, use the version of the API that supports coin options (factory), otherwise stick to plain constructor and pass coin-specific options at signing time.

Apply this minimal fix:

-    this.app = new AppBtc({ transport: this.transport, currency: 'zcash' })
+    this.app = new AppBtc(this.transport)

Additionally, strongly consider typing the transport to catch this at compile time:

// at top of file
import type Transport from '@ledgerhq/hw-transport'

// fields
private transport: Transport

If your installed '@ledgerhq/hw-app-btc' supports a factory with currency (varies by version), prefer that instead:

// Pseudocode (verify availability in your version)
import { createAppBtc } from '@ledgerhq/hw-app-btc'
this.app = createAppBtc(this.transport, { currency: 'zcash' })

See the verification block below to confirm which API your version exposes.

🤖 Prompt for AI Agents
In packages/xchain-zcash/src/clientLedger.ts around lines 24 to 31, the AppBtc
is being constructed with an object containing transport which causes runtime
errors like "this.transport.send is not a function"; replace the current
construction so you pass the Transport instance directly to AppBtc (or use the
library's factory that accepts coin options if available), i.e. call the
constructor/factory with the transport variable rather than an object, and add a
Transport type import and type the transport field (import type Transport from
'@ledgerhq/hw-transport' and declare private transport: Transport) so TypeScript
catches incorrect usage.


// Get the current address synchronously
getAddress(): string {
throw Error('Not implemented.')
throw Error('Sync method not supported for Ledger')
}

async getAddressAsync(): Promise<Address> {
throw Error('Not implemented.')
// Get the current address asynchronously
async getAddressAsync(index = 0, verify = false): Promise<Address> {
const app = await this.getApp()
const result = await app.getWalletPublicKey(this.getFullDerivationPath(index), {
format: 'legacy',
verify,
})
return result.bitcoinAddress
}

async transfer(): Promise<TxHash> {
throw Error('Not implemented.')
// Transfer ZEC from Ledger
async transfer(params: TxParams): Promise<TxHash> {
const fromAddressIndex = params?.walletIndex || 0

// Get sender address
const sender = await this.getAddressAsync(fromAddressIndex)

// Prepare transaction using base client method (handles flat fee)
const { rawUnsignedTx } = await this.prepareTx({
...params,
sender,
feeRate: 0, // Ignored for Zcash
})

// Parse the transaction data
const txData = JSON.parse(rawUnsignedTx)

// Build the actual transaction for signing
const tx = await buildTx(
txData.height,
txData.from,
txData.to,
txData.amount,
txData.utxos,
txData.isMainnet,
txData.memo,
)

// Check fee bounds (already done in prepareTx but double-check)
checkFeeBounds(this.feeBounds, tx.fee)

Comment on lines +76 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fee bounds check is using total fee instead of a fee rate — likely incorrect.

checkFeeBounds expects a fee rate, but tx.fee is an absolute fee (zatoshis). This can throw incorrectly and is redundant if prepareTx already validated fees.

Apply this diff to remove the erroneous check:

-    // Check fee bounds (already done in prepareTx but double-check)
-    checkFeeBounds(this.feeBounds, tx.fee)

If you do want a bounds check here, compute a synthetic fee rate from tx size and fee (if available) and compare like-for-like.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check fee bounds (already done in prepareTx but double-check)
checkFeeBounds(this.feeBounds, tx.fee)
🤖 Prompt for AI Agents
In packages/xchain-zcash/src/clientLedger.ts around lines 76 to 78, the code
calls checkFeeBounds(this.feeBounds, tx.fee) but checkFeeBounds expects a fee
rate while tx.fee is an absolute amount; remove this erroneous call (since
prepareTx already validated fees) or if you want to keep a guard, compute a
synthetic fee rate by dividing tx.fee by tx.size (or available tx byte size) and
pass that rate to checkFeeBounds instead, ensuring like-for-like comparison.

// LIMITATION: Zcash Ledger transaction signing requires raw transaction hex data
// for previous transactions (UTXOs), but Zcash data providers only return
// parsed transaction objects without the raw hex.
//
// This is different from Bitcoin where the prepareTx method fetches and includes
// the raw transaction hex (txHex field) for each UTXO.
//
// To fully implement Zcash Ledger signing, we would need:
// 1. A Zcash data provider that returns raw transaction hex
// 2. Or use the dedicated Zcash Ledger app instead of Bitcoin app
// 3. Or implement a custom serialization from the transaction object

throw new Error(
'Zcash Ledger transfers require raw transaction data that is not available from current data providers. ' +
'The transaction has been built successfully with fee: ' +
tx.fee +
' zatoshis. ' +
'To complete Ledger signing, either:\n' +
'1. Use the keystore client for transfers, or\n' +
'2. Use the dedicated Zcash Ledger app, or\n' +
'3. Implement a data provider that returns raw transaction hex',
)
}
}

Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4428,6 +4428,7 @@ __metadata:
resolution: "@xchainjs/xchain-zcash@workspace:packages/xchain-zcash"
dependencies:
"@bitcoin-js/tiny-secp256k1-asmjs": "npm:^2.2.3"
"@ledgerhq/hw-transport-node-hid": "npm:^6.28.6"
"@mayaprotocol/zcash-js": "npm:1.0.7"
"@scure/bip32": "npm:^1.7.0"
"@types/blake2b-wasm": "npm:^2.4.3"
Expand Down
Loading