@ledgerhq/hw-app-helium
Version:
Ledger Hardware Wallet Helium Application API
389 lines (334 loc) • 9.94 kB
text/typescript
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;
}
}
}