Skip to content
Merged
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
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ module.exports = {
transformIgnorePatterns: ['^.+\\.js$'],
modulePaths: ['<rootDir>/src', '<rootDir>/tests'],
runner: 'jest-serial-runner',
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
}
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@
"dependencies": {
"@ledgerhq/hw-transport": "6.31.13",
"@zondax/ledger-js": "^1.3.1",
"axios": "^1.13.2"
"axios": "^1.13.2",
"buffer": "^6.0.3"
},
"devDependencies": {
"@ledgerhq/hw-transport-mocker": "^6.29.13",
"@ledgerhq/hw-transport-node-hid": "^6.29.14",
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
"@types/jest": "30.0.0",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"@types/node": "^24.10.2",
"@typescript-eslint/eslint-plugin": "^8.49.0",
"@typescript-eslint/parser": "^8.49.0",
"bip32": "^5.0.0",
"bip32-ed25519": "https://github.com/Zondax/bip32-ed25519",
"bip39": "^3.1.0",
Expand All @@ -63,7 +64,7 @@
"jest-runner": "^30.2.0",
"jest-serial-runner": "^1.2.2",
"prettier": "^3.7.4",
"sort-package-json": "^3.5.0",
"sort-package-json": "^3.5.1",
"ts-jest": "29.4.6",
"typescript": "5.9.3"
},
Expand Down
50 changes: 50 additions & 0 deletions src/__tests__/toBuffer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { toBuffer } from '../common'

describe('toBuffer', () => {
it('should pass through Buffer unchanged', () => {
const input = Buffer.from('deadbeef', 'hex')
const result = toBuffer(input)

expect(result).toBe(input)
expect(result.toString('hex')).toBe('deadbeef')
})

it('should convert Uint8Array to Buffer', () => {
const input = new Uint8Array([0xde, 0xad, 0xbe, 0xef])
const result = toBuffer(input)

expect(Buffer.isBuffer(result)).toBe(true)
expect(result.toString('hex')).toBe('deadbeef')
})

it('should convert hex string to Buffer', () => {
const input = 'deadbeef'
const result = toBuffer(input)

expect(Buffer.isBuffer(result)).toBe(true)
expect(result.toString('hex')).toBe('deadbeef')
})

it('should convert hex string with 0x prefix to Buffer', () => {
const input = '0xdeadbeef'
const result = toBuffer(input)

expect(Buffer.isBuffer(result)).toBe(true)
expect(result.toString('hex')).toBe('deadbeef')
})

it('should handle empty inputs', () => {
expect(toBuffer(Buffer.alloc(0)).length).toBe(0)
expect(toBuffer(new Uint8Array(0)).length).toBe(0)
expect(toBuffer('').length).toBe(0)
expect(toBuffer('0x').length).toBe(0)
})

it('should handle large inputs', () => {
const largeHex = 'ab'.repeat(1000)
const result = toBuffer(largeHex)

expect(result.length).toBe(1000)
expect(result.toString('hex')).toBe(largeHex)
})
})
28 changes: 28 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Buffer as BufferPolyfill } from 'buffer/'

import type Transport from '@ledgerhq/hw-transport'

/** ******************************************************************************
Expand All @@ -17,8 +19,34 @@ import type Transport from '@ledgerhq/hw-transport'
* limitations under the License.
******************************************************************************* */

// Use global Buffer if available (Node.js), otherwise use polyfill (browser)
const BufferImpl = typeof Buffer !== 'undefined' ? Buffer : BufferPolyfill

/** Input type for transaction blobs - accepts Buffer, Uint8Array, or hex string */
export type TransactionBlobInput = Buffer | Uint8Array | string
/** Input type for transaction metadata - accepts Buffer, Uint8Array, or hex string */
export type TransactionMetadataBlobInput = Buffer | Uint8Array | string

// Keep original types for backwards compatibility
export type TransactionMetadataBlob = Buffer
export type TransactionBlob = Buffer

/**
* Converts various input types to Buffer
* @param input - Buffer, Uint8Array, or hex string
* @returns Buffer
*/
export function toBuffer(input: Buffer | Uint8Array | string): Buffer {
if (BufferImpl.isBuffer(input)) {
return input as Buffer
}
if (input instanceof Uint8Array) {
return BufferImpl.from(input) as Buffer
}
// Assume hex string
const hex = input.startsWith('0x') ? input.slice(2) : input
return BufferImpl.from(hex, 'hex') as Buffer
}
export type SS58Prefix = number

/**
Expand Down
89 changes: 53 additions & 36 deletions src/generic_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ import {
SCHEME,
SS58Prefix,
TransactionBlob,
TransactionBlobInput,
TransactionMetadataBlob,
TransactionMetadataBlobInput,
TxMetadata,
toBuffer,
} from './common'

export class PolkadotGenericApp extends BaseApp {
Expand Down Expand Up @@ -69,13 +72,17 @@ export class PolkadotGenericApp extends BaseApp {

/**
* Retrieves transaction metadata from the metadata service.
* @param txBlob - The transaction blob.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @param txMetadataChainId - The optional chain ID for the transaction metadata service. This value temporarily overrides the one set in the constructor.
* @param txMetadataSrvUrl - The optional URL for the transaction metadata service. This value temporarily overrides the one set in the constructor.
* @returns The transaction metadata.
* @throws {ResponseError} - If the txMetadataSrvUrl is not defined.
*/
async getTxMetadata(txBlob: TransactionBlob, txMetadataChainId?: string, txMetadataSrvUrl?: string): Promise<TransactionMetadataBlob> {
async getTxMetadata(
txBlob: TransactionBlobInput,
txMetadataChainId?: string,
txMetadataSrvUrl?: string
): Promise<TransactionMetadataBlob> {
const txMetadataChainIdVal = txMetadataChainId ?? this.txMetadataChainId
const txMetadataSrvUrlVal = txMetadataSrvUrl ?? this.txMetadataSrvUrl

Expand All @@ -93,8 +100,9 @@ export class PolkadotGenericApp extends BaseApp {
)
}

const txBlobBuffer = toBuffer(txBlob)
const resp = await axios.post<TxMetadata>(txMetadataSrvUrlVal, {
txBlob: txBlob.toString('hex'),
txBlob: txBlobBuffer.toString('hex'),
chain: { id: txMetadataChainIdVal },
})

Expand Down Expand Up @@ -295,12 +303,12 @@ export class PolkadotGenericApp extends BaseApp {
* @deprecated Use signEcdsa or signEd25519 instead. This method will be removed in a future version.
* Signs a transaction blob retrieving the correct metadata from a metadata service.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @param scheme - The scheme to use for the signing. Default is ED25519.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async sign(path: BIP32Path, txBlob: TransactionBlob, scheme = SCHEME.ED25519) {
async sign(path: BIP32Path, txBlob: TransactionBlobInput, scheme = SCHEME.ED25519) {
if (scheme != SCHEME.ECDSA && scheme != SCHEME.ED25519) {
throw new ResponseError(LedgerError.ConditionsOfUseNotSatisfied, `Unexpected scheme ${scheme}. Needs to be ECDSA (2) or ED25519 (0)`)
}
Expand All @@ -313,11 +321,11 @@ export class PolkadotGenericApp extends BaseApp {
/**
* Signs a transaction blob using the ED25519 scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signEd25519(path: BIP32Path, txBlob: TransactionBlob) {
async signEd25519(path: BIP32Path, txBlob: TransactionBlobInput) {
if (!this.txMetadataSrvUrl) {
throw new ResponseError(
LedgerError.GenericError,
Expand All @@ -332,22 +340,23 @@ export class PolkadotGenericApp extends BaseApp {
)
}

const txMetadata = await this.getTxMetadata(txBlob)
return await this.signImplEd25519(path, this.INS.SIGN, txBlob, txMetadata)
const txBlobBuffer = toBuffer(txBlob)
const txMetadata = await this.getTxMetadata(txBlobBuffer)
return await this.signImplEd25519(path, this.INS.SIGN, txBlobBuffer, txMetadata)
}

/**
* Signs a transaction blob using the ECDSA scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status. For ECDSA, the signature is in RSV format:
* - R: First 32 bytes (signature.slice(0, 32))
* - S: Next 32 bytes (signature.slice(32, 64))
* - V: Last byte (signature.slice(64, 65))
* @see parseEcdsaSignature - Use this utility function to easily parse the signature into R, S, V components
*/
async signEcdsa(path: BIP32Path, txBlob: TransactionBlob) {
async signEcdsa(path: BIP32Path, txBlob: TransactionBlobInput) {
if (!this.txMetadataSrvUrl) {
throw new ResponseError(
LedgerError.GenericError,
Expand All @@ -362,20 +371,21 @@ export class PolkadotGenericApp extends BaseApp {
)
}

const txMetadata = await this.getTxMetadata(txBlob)
return await this.signImplEcdsa(path, this.INS.SIGN, txBlob, txMetadata)
const txBlobBuffer = toBuffer(txBlob)
const txMetadata = await this.getTxMetadata(txBlobBuffer)
return await this.signImplEcdsa(path, this.INS.SIGN, txBlobBuffer, txMetadata)
}

/**
* Signs a transaction blob with provided metadata.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @param txMetadataChainId - The optional chain ID for the transaction metadata service. This value temporarily overrides the one set in the constructor.
* @param txMetadataSrvUrl - The optional URL for the transaction metadata service. This value temporarily overrides the one set in the constructor.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signMigration(path: BIP32Path, txBlob: TransactionBlob, txMetadataChainId?: string, txMetadataSrvUrl?: string) {
async signMigration(path: BIP32Path, txBlob: TransactionBlobInput, txMetadataChainId?: string, txMetadataSrvUrl?: string) {
if (!this.txMetadataSrvUrl) {
throw new ResponseError(
LedgerError.GenericError,
Expand All @@ -390,19 +400,20 @@ export class PolkadotGenericApp extends BaseApp {
)
}

const txMetadata = await this.getTxMetadata(txBlob, txMetadataChainId, txMetadataSrvUrl)
return await this.signImplEd25519(path, this.INS.SIGN, txBlob, txMetadata)
const txBlobBuffer = toBuffer(txBlob)
const txMetadata = await this.getTxMetadata(txBlobBuffer, txMetadataChainId, txMetadataSrvUrl)
return await this.signImplEd25519(path, this.INS.SIGN, txBlobBuffer, txMetadata)
}
/**
* @deprecated Use signRawEcdsa or signRawEd25519 instead. This method will be removed in a future version.
* Signs a raw transaction blob.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @param scheme - The scheme to use for the signing. Default is ED25519.
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signRaw(path: BIP32Path, txBlob: TransactionBlob, scheme = SCHEME.ED25519) {
async signRaw(path: BIP32Path, txBlob: TransactionBlobInput, scheme = SCHEME.ED25519) {
if (scheme != SCHEME.ECDSA && scheme != SCHEME.ED25519) {
throw new ResponseError(LedgerError.ConditionsOfUseNotSatisfied, `Unexpected scheme ${scheme}. Needs to be ECDSA (2) or ED25519 (0)`)
}
Expand All @@ -415,39 +426,41 @@ export class PolkadotGenericApp extends BaseApp {
/**
* Signs a raw transaction blob using the ED25519 scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signRawEd25519(path: BIP32Path, txBlob: TransactionBlob) {
return await this.signImplEd25519(path, this.INS.SIGN_RAW, txBlob)
async signRawEd25519(path: BIP32Path, txBlob: TransactionBlobInput) {
const txBlobBuffer = toBuffer(txBlob)
return await this.signImplEd25519(path, this.INS.SIGN_RAW, txBlobBuffer)
}

/**
* Signs a raw transaction blob using the ECDSA scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status. For ECDSA, the signature is in RSV format:
* - R: First 32 bytes (signature.slice(0, 32))
* - S: Next 32 bytes (signature.slice(32, 64))
* - V: Last byte (signature.slice(64, 65))
* @see parseEcdsaSignature - Use this utility function to easily parse the signature into R, S, V components
*/
async signRawEcdsa(path: BIP32Path, txBlob: TransactionBlob) {
return await this.signImplEcdsa(path, this.INS.SIGN_RAW, txBlob)
async signRawEcdsa(path: BIP32Path, txBlob: TransactionBlobInput) {
const txBlobBuffer = toBuffer(txBlob)
return await this.signImplEcdsa(path, this.INS.SIGN_RAW, txBlobBuffer)
}

/**
* @deprecated Use signWithMetadataEd25519 or signWithMetadataEcdsa instead. This method will be removed in a future version.
* [Expert-only Method] Signs a transaction blob with provided metadata (this could be used also with a migration app)
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txMetadata - The transaction metadata.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @param txMetadata - The transaction metadata (Buffer, Uint8Array, or hex string).
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signWithMetadata(path: BIP32Path, txBlob: TransactionBlob, txMetadata: TransactionMetadataBlob, scheme = SCHEME.ED25519) {
async signWithMetadata(path: BIP32Path, txBlob: TransactionBlobInput, txMetadata: TransactionMetadataBlobInput, scheme = SCHEME.ED25519) {
if (scheme != SCHEME.ECDSA && scheme != SCHEME.ED25519) {
throw new ResponseError(LedgerError.ConditionsOfUseNotSatisfied, `Unexpected scheme ${scheme}. Needs to be ECDSA (2) or ED25519 (0)`)
}
Expand All @@ -460,29 +473,33 @@ export class PolkadotGenericApp extends BaseApp {
/**
* Signs a transaction blob with provided metadata using the ECDSA scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txMetadata - The transaction metadata.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @param txMetadata - The transaction metadata (Buffer, Uint8Array, or hex string).
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status. For ECDSA, the signature is in RSV format:
* - R: First 32 bytes (signature.slice(0, 32))
* - S: Next 32 bytes (signature.slice(32, 64))
* - V: Last byte (signature.slice(64, 65))
* @see parseEcdsaSignature - Use this utility function to easily parse the signature into R, S, V components
*/
async signWithMetadataEcdsa(path: BIP32Path, txBlob: TransactionBlob, txMetadata: TransactionMetadataBlob) {
return await this.signImplEcdsa(path, this.INS.SIGN, txBlob, txMetadata)
async signWithMetadataEcdsa(path: BIP32Path, txBlob: TransactionBlobInput, txMetadata: TransactionMetadataBlobInput) {
const txBlobBuffer = toBuffer(txBlob)
const txMetadataBuffer = toBuffer(txMetadata)
return await this.signImplEcdsa(path, this.INS.SIGN, txBlobBuffer, txMetadataBuffer)
}

/**
* Signs a transaction blob with provided metadata using the ED25519 scheme.
* @param path - The BIP44 path.
* @param txBlob - The transaction blob.
* @param txMetadata - The transaction metadata.
* @param txBlob - The transaction blob (Buffer, Uint8Array, or hex string).
* @param txMetadata - The transaction metadata (Buffer, Uint8Array, or hex string).
* @throws {ResponseError} If the response from the device indicates an error.
* @returns The response containing the signature and status.
*/
async signWithMetadataEd25519(path: BIP32Path, txBlob: TransactionBlob, txMetadata: TransactionMetadataBlob) {
return await this.signImplEd25519(path, this.INS.SIGN, txBlob, txMetadata)
async signWithMetadataEd25519(path: BIP32Path, txBlob: TransactionBlobInput, txMetadata: TransactionMetadataBlobInput) {
const txBlobBuffer = toBuffer(txBlob)
const txMetadataBuffer = toBuffer(txMetadata)
return await this.signImplEd25519(path, this.INS.SIGN, txBlobBuffer, txMetadataBuffer)
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
******************************************************************************* */

export * from './generic_app'
export { toBuffer, type TransactionBlobInput, type TransactionMetadataBlobInput } from './common'
Loading