UNPKG

@ledgerhq/hw-app-helium

Version:
389 lines (334 loc) 9.94 kB
import Transport from "@ledgerhq/hw-transport"; import { StatusCodes } from "@ledgerhq/errors"; import Address from "@helium/address"; import { PaymentV2, SecurityExchangeV1, StakeValidatorV1, TokenBurnV1, TransferValidatorStakeV1, UnstakeValidatorV1, } from "@helium/transactions"; import proto from "@helium/proto"; import { pathToBuffer, serializePaymentV2, serializeSecurityExchangeV1, serializeStakeValidatorV1, serializeTokenBurnV1, serializeTransferValidatorStakeV1, serializeUnstakeValidatorV1, } from "./serialization"; const P1_NON_CONFIRM = 0x00; const P1_CONFIRM = 0x01; const LEDGER_CLA = 0xe0; const CLA_OFFSET = 0x00; const INS = { GET_VERSION: 0x01, GET_ADDR: 0x02, SIGN_PAYMENT: 0x08, SIGN_VALIDATOR_STAKE: 0x09, SIGN_VALIDATOR_TXFER: 0x0a, SIGN_VALIDATOR_UNSTAKE: 0x0b, SIGN_TOKEN_BURN: 0x0c, SIGN_TRANSFER_SEC_TXN: 0x0d, }; /** * Helium API * * @param transport a transport for sending commands to a device * @param scrambleKey a scramble key * * @example * import Helium from "@ledgerhq/hw-app-helium"; * const helium = new Helium(transport); */ export default class Helium { private transport: Transport; constructor(transport: Transport, scrambleKey = "helium_default_scramble_key") { this.transport = transport; this.transport.decorateAppAPIMethods( this, [ "getVersion", "getAddress", "signPaymentV2", "signTokenBurnV1", "signStakeValidatorV1", "signUnstakeValidatorV1", "signTransferValidatorStakeV1", ], scrambleKey, ); } /** * Get application version. * * @returns version object * * @example * helium.getVersion().then(r => r.version) */ async getVersion(): Promise<{ version: string }> { const [major, minor, patch] = await this.sendToDevice( INS.GET_VERSION, P1_NON_CONFIRM, 0, Buffer.from([]), ); return { version: `${major}.${minor}.${patch}`, }; } /** * Get Helium address (public key) for a BIP32 path. * * @param path a BIP32 path * @param display flag to show display * @param accountIndex index of account address * @returns an object with the address field * * @example * helium.getAddress("44'/904'/0'/0'/0'").then(r => r.address) */ async getAddress( path: string, display?: boolean, accountIndex = 0, ): Promise<{ index: number; address: string; publicKey: string }> { const pathBuffer = pathToBuffer(path); const addressBuffer = await this.sendToDevice( INS.GET_ADDR, display ? P1_CONFIRM : P1_NON_CONFIRM, accountIndex, pathBuffer, ); const address = Address.fromBin(addressBuffer.slice(1, 34)); return { index: addressBuffer.slice(0, 1)[0], address: address.b58, publicKey: Buffer.from(address.publicKey).toString("hex"), }; } /** * Sign a Helium `PaymentV2` transaction. * * @param txn a PaymentV2 transaction * @param accountIndex index of account address * * @returns an object with the signed transaction and signature * * @example * import { PaymentV2 } from '@helium/transactions' * const txn = new PaymentV2({ ... }) * helium.signTransaction(txn).then(r => r.signature) */ async signPaymentV2( txn: PaymentV2, accountIndex = 0, ): Promise<{ signature: Buffer; txn: PaymentV2 }> { const payload = serializePaymentV2(txn); const response = await this.sendToDevice(INS.SIGN_PAYMENT, accountIndex, CLA_OFFSET, payload); if (response.length === 1) throw "User has declined."; const decoded = proto.helium.blockchain_txn_payment_v2.decode(response); const signature = decoded.signature as Buffer; txn.signature = signature; return { signature, txn, }; } /** * Sign a Helium `TokenBurnV1` transaction. * * @param txn a TokenBurnV1 transaction * @param accountIndex index of account address * * @returns an object with the signed transaction and signature * * @example * import { TokenBurnV1 } from '@helium/transactions' * const txn = new TokenBurnV1({ ... }) * helium.signTransaction(txn).then(r => r.signature) */ async signTokenBurnV1( txn: TokenBurnV1, accountIndex = 0, ): Promise<{ signature: Buffer; txn: TokenBurnV1 }> { const payload = serializeTokenBurnV1(txn); const response = await this.sendToDevice( INS.SIGN_TOKEN_BURN, accountIndex, CLA_OFFSET, payload, ); if (response.length === 1) throw "User has declined."; const decoded = proto.helium.blockchain_txn_token_burn_v1.decode(response); const signature = decoded.signature as Buffer; txn.signature = signature; return { signature, txn, }; } /** * Sign a Helium `StakeValidatorV1` transaction. * * @param txn a StakeValidatorV1 transaction * @param accountIndex index of account address * * @returns an object with the signed transaction and signature * * @example * import { StakeValidatorV1 } from '@helium/transactions' * const txn = new StakeValidatorV1({ ... }) * helium.signTransaction(txn).then(r => r.signature) */ async signStakeValidatorV1( txn: StakeValidatorV1, accountIndex = 0, ): Promise<{ signature: Buffer; txn: StakeValidatorV1 }> { const payload = serializeStakeValidatorV1(txn); const response = await this.sendToDevice( INS.SIGN_VALIDATOR_STAKE, accountIndex, CLA_OFFSET, payload, ); if (response.length === 1) throw "User has declined."; const decoded = proto.helium.blockchain_txn_stake_validator_v1.decode(response); const signature = decoded.ownerSignature as Buffer; txn.ownerSignature = signature; return { signature, txn, }; } /** * Sign a Helium `UnstakeValidatorV1` transaction. * * @param txn a UnstakeValidatorV1 transaction * @param accountIndex index of account address * * @returns an object with the signed transaction and signature * * @example * import { UnstakeValidatorV1 } from '@helium/transactions' * const txn = new UnstakeValidatorV1({ ... }) * helium.signTransaction(txn).then(r => r.signature) */ async signUnstakeValidatorV1( txn: UnstakeValidatorV1, accountIndex = 0, ): Promise<{ signature: Buffer; txn: UnstakeValidatorV1 }> { const payload = serializeUnstakeValidatorV1(txn); const response = await this.sendToDevice( INS.SIGN_VALIDATOR_UNSTAKE, accountIndex, CLA_OFFSET, payload, ); if (response.length === 1) throw "User has declined."; const decoded = proto.helium.blockchain_txn_unstake_validator_v1.decode(response); const signature = decoded.ownerSignature as Buffer; txn.ownerSignature = signature; return { signature, txn, }; } /** * Sign a Helium `TransferValidatorStakeV1` transaction. * * @param txn a TransferValidatorStakeV1 transaction * @param ownerType whether to sign as the old or new owner in the transfer * @param accountIndex index of account address * * @returns an object with the signed transaction and signature * * @example * import { TransferValidatorStakeV1 } from '@helium/transactions' * const txn = new TransferValidatorStakeV1({ ... }, 'old') * helium.signTransaction(txn).then(r => r.signature) */ async signTransferValidatorStakeV1( txn: TransferValidatorStakeV1, ownerType: "old" | "new", accountIndex = 0, ): Promise<{ signature: Buffer; txn: TransferValidatorStakeV1 }> { const payload = serializeTransferValidatorStakeV1(txn); const response = await this.sendToDevice( INS.SIGN_VALIDATOR_TXFER, accountIndex, CLA_OFFSET, payload, ); if (response.length === 1) throw "User has declined."; const decoded = proto.helium.blockchain_txn_transfer_validator_stake_v1.decode(response); const signature = decoded.oldOwnerSignature as Buffer; if (ownerType === "old") { txn.oldOwnerSignature = signature; } if (ownerType === "new") { txn.newOwnerSignature = signature; } return { signature, txn, }; } /** * Sign a Helium `SecurityExchangeV1` transaction. * * @param txn a SecurityExchangeV1 transaction * @param accountIndex index of account address * * @returns an object with the signed transaction and signature * * @example * import { SecurityExchangeV1 } from '@helium/transactions' * const txn = new SecurityExchangeV1({ ... }) * helium.signTransaction(txn).then(r => r.signature) */ async signSecurityExchangeV1( txn: SecurityExchangeV1, accountIndex = 0, ): Promise<{ signature: Buffer; txn: SecurityExchangeV1 }> { const payload = serializeSecurityExchangeV1(txn); const response = await this.sendToDevice( INS.SIGN_TRANSFER_SEC_TXN, accountIndex, CLA_OFFSET, payload, ); if (response.length === 1) throw "User has declined."; const decoded = proto.helium.blockchain_txn_security_exchange_v1.decode(response); const signature = decoded.signature as Buffer; txn.signature = signature; return { signature, txn, }; } private async sendToDevice(instruction: number, p1: number, p2 = 0x00, payload: Buffer) { const acceptStatusList = [StatusCodes.OK]; const reply = await this.transport.send( LEDGER_CLA, instruction, p1, p2, payload, acceptStatusList, ); this.throwOnFailure(reply); return reply.slice(0, reply.length - 2); } private throwOnFailure(reply: Buffer) { // transport makes sure reply has a valid length const status = reply.readUInt16BE(reply.length - 2); switch (status) { default: return; } } }