Skip to content
This repository was archived by the owner on Apr 21, 2023. It is now read-only.
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,26 @@ class NormalOptInCtrl {
this.statusLoading = true;
this.isOptedIn = false;
this.optinStopped = false;

this.loadingSymbolLedgerInfo = false;
}

exportSymbolAddress() {
this.loadingSymbolLedgerInfo = true;
this._CatapultOptin.getSymbolOptInAddress(DEFAULT_ACCOUNT_PATH).then( publicAccount => {
this.loadingSymbolLedgerInfo = false;
this.formData.optinAddress = publicAccount.address.pretty();
this.formData.optinAccount = {
address: publicAccount.address,
keyPair: {
publicKey: publicAccount.publicKey
},
publicAccount,
};
}).catch(e => {
this.loadingSymbolLedgerInfo = false;
console.log(e);
});
}

/**
Expand Down Expand Up @@ -304,6 +324,7 @@ class NormalOptInCtrl {

getEntropy() {
// Prepare
if (this._Wallet.algo) return;
let elem = document.getElementById("pBarOptIn");
this.formData.entropyWidth = 0;
this.formData.entropy = "";
Expand Down
54 changes: 48 additions & 6 deletions src/app/modules/catapultOptin/catapultOptin/catapultOptin.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ <h4 style="margin-top: 15px;"><a href="https://nemplatform.com/symbol-migration"
</div>

<!-- STEP 1.0: ENTROPY -->
<div class="row container-div" ng-show="$ctrl.step == 10">
<div class="row container-div" ng-show="$ctrl.step == 10 && $ctrl._Wallet.algo != 'ledger'">
<div class="col-md-12">
<img src="images/account.png">
<h1>{{'CREATE_SYMBOL_ACCOUNT' | translate}}</h1>
</div>
</div>
<div class="row text-center" ng-show="$ctrl.step == 10">
<div class="row text-center" ng-show="$ctrl.step == 10 && $ctrl._Wallet.algo != 'ledger'">
<div class="col-md-12 centered-col" style="margin-bottom: 50px;">
<img src="images/logo_white.png" style="width: 15%;">
</div>
Expand Down Expand Up @@ -77,6 +77,49 @@ <h1>{{'CREATE_SYMBOL_ACCOUNT' | translate}}</h1>
</div>
</div>

<!-- STEP 1.0.1: LEDGER - SYMBOL ACCOUNT -->
<div class="row container-div" ng-show="$ctrl.step == 10 && $ctrl._Wallet.algo == 'ledger'">
<div class="col-md-12">
<img src="images/account.png">
<h1>{{'CREATE_SYMBOL_ACCOUNT' | translate}}</h1>
</div>
</div>
<div class="row text-center" ng-show="$ctrl.step == 10 && $ctrl._Wallet.algo == 'ledger'">
<div class="col-md-12 centered-col" style="margin-bottom: 50px;">
<img src="images/logo_white.png" style="width: 15%;">
</div>

<div class="col-md-12 text-center">
<!-- Entropy info -->
<div>
<p>{{'LEDGER_CREATE_AND_EXPORT_SYMBOL_ADDRESS' | translate}}</p>
<p><b>{{'SWAP_TO_SYMBOL_BOLOS_APP' | translate}}</b></p>
</div>
<div class="mt-4">
<div class="row">
<p ng-show="$ctrl.loadingSymbolLedgerInfo">{{ 'EXPORTING_ADDRESS_FOLLOW_INSTRUCTIONS' | translate }}</p>
<button class="btn symbol-button" ng-click="$ctrl.exportSymbolAddress()" ng-show="!$ctrl.loadingSymbolLedgerInfo && !$ctrl.formData.optinAddress"> {{ 'GENERAL_START' | translate }}</button>
<div class="form-group" ng-show="$ctrl.formData.optinAddress">
<p>{{ 'YOUR_SYMBOL_ADDRESS' | translate }}</p>
<label class="text-center optin-label-form">{{ $ctrl.formData.optinAddress }}</label>
<img ng-click="$ctrl.copyToClipboard($ctrl.formData.optinAddress);" src="images/Clipboard.png" class="clipboard-icon">
</div>
</div>
</div>
<!-- Wallet type buttons -->
<div class="col-md-6 col-md-offset-3 mt-4" ng-show="$ctrl.formData.optinAddress">
<div class="row">
<div class="col-md-6">
<button class="btn symbol-button" style="float:right;" ng-click="$ctrl.step = 0;"><span class="fa fa-chevron-left" aria-hidden="true"></span> {{ 'GENERAL_BACK' | translate }}</button>
</div>
<div class="col-md-6">
<button class="btn symbol-button" style="float:left;" ng-show="$ctrl.formData.optinAddress" ng-click="$ctrl.step = 4">{{ 'GENERAL_NEXT' | translate }} <span class="fa fa-chevron-right" aria-hidden="true"></span></button>
</div>
</div>
</div>
</div>
</div>

<!-- STEP 1.1: YOUR SYMBOL ACCOUNT -->
<div class="row container-div" ng-show="$ctrl.step == 11">
<div class="col-md-12">
Expand All @@ -92,7 +135,7 @@ <h1>{{'OPTIN_SYMBOL_READY' | translate}}</h1>
<img ng-click="$ctrl.copyToClipboard($ctrl.formData.optinAddress);" src="images/Clipboard.png" class="clipboard-icon">
</div>

<div class="form-group" style="margin-bottom: 4em;">
<div class="form-group" style="margin-bottom: 4em;" ng-show="$ctrl.formData.optinMnemonic">
<p>{{ 'YOUR_SYMBOL_MNEMONIC' | translate }}</p>
<label class="text-center optin-label-form">{{ $ctrl.formData.optinMnemonic }}</label>
<img ng-click="$ctrl.copyToClipboard($ctrl.formData.optinMnemonic);" src="images/Clipboard.png" class="clipboard-icon">
Expand All @@ -104,7 +147,6 @@ <h1>{{'OPTIN_SYMBOL_READY' | translate}}</h1>
</div>
</div>


<!-- STEP 1.2: PASSWORD TO BACKUP -->
<div class="row container-div" ng-show="$ctrl.step == 12">
<div class="col-md-12">
Expand Down Expand Up @@ -268,7 +310,7 @@ <h4>
<div class="row">

<div class="col-md-6">
<button class="btn symbol-button symbol-button__block" style="float:right;" ng-click="$ctrl.step = 3; $ctrl.formData.backupConfirm = false; $ctrl.formData.understandConfirm = false; $ctrl.formData.acceptTerms = false;"> <span class="fa fa-chevron-left" aria-hidden="true"></span> {{ 'GENERAL_BACK' | translate }}</button>
<button class="btn symbol-button symbol-button__block" style="float:right;" ng-click="$ctrl.step = $ctrl._Wallet.algo ? 10: 3; $ctrl.formData.backupConfirm = false; $ctrl.formData.understandConfirm = false; $ctrl.formData.acceptTerms = false;"> <span class="fa fa-chevron-left" aria-hidden="true"></span> {{ 'GENERAL_BACK' | translate }}</button>
</div>
<div class="col-md-6">
<button class="btn symbol-button symbol-button__block" style="float:left; background: rgb(255,0,255);
Expand Down Expand Up @@ -301,7 +343,7 @@ <h4>{{ 'OPTIN_CONFIRM_INFO' | translate }}</h4>
<p>{{ 'YOUR_SYMBOL_ADDRESS' | translate }}</p>
<label class="text-center optin-label-form" style="width: 100%;">{{$ctrl.formData.optinAddress}}</label>
</div>
<div class="mt-4">
<div class="mt-4" ng-show="$ctrl.formData.optinMnemonic">
<p>{{ 'YOUR_SYMBOL_MNEMONIC' | translate }}</p>
<label class="text-center optin-label-form" style="width: 100%;">{{$ctrl.formData.optinMnemonic}}</label>
</div>
Expand Down
5 changes: 4 additions & 1 deletion src/app/modules/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -992,8 +992,11 @@ function EnglishProvider($translateProvider) {
CREATE_SYMBOL_ACCOUNT: 'Create your Symbol account',
OPTIN_SYMBOL_READY: 'Your Symbol account is ready',
OPTIN_COPY_SUCCESS: 'Copied!',
CATAPULT_OPT_IN_ERROR_TOO_MUCH_COSIGNATORIES: 'This account has more than 8 cosignatories. Opt In protocol only allows multisig accounts with less than 9 cosignatories'
CATAPULT_OPT_IN_ERROR_TOO_MUCH_COSIGNATORIES: 'This account has more than 8 cosignatories. Opt In protocol only allows multisig accounts with less than 9 cosignatories',

LEDGER_CREATE_AND_EXPORT_SYMBOL_ADDRESS: 'You must create a symbol account on ledger to receive the funds',
EXPORTING_ADDRESS_FOLLOW_INSTRUCTIONS: 'Exporting address, follow instructions on your ledger device',
SWAP_TO_SYMBOL_BOLOS_APP: 'Open the Symbol app on ledger',
});
}

Expand Down
219 changes: 219 additions & 0 deletions src/app/modules/ledger/hw-app-symbol.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// internal dependencies
import * as BIPPath from 'bip32-path';
// configuration
import { Transaction, SignedTransaction, Convert, CosignatureSignedTransaction, AggregateTransaction } from 'symbol-sdk';

const SUPPORT_VERSION = { LEDGER_MAJOR_VERSION: '0', LEDGER_MINOR_VERSION: '0', LEDGER_PATCH_VERSION: '4' };
const CLA_FIELD = 0xe0;
/**
* Symbol's API
*
* @example
* import { SymbolLedger } from '@/core/utils/Ledger'
* const sym = new SymbolLedger();
"44'/4343'/account'/change/accountIndex"
*/

export default class SymbolHw {

constructor(transport, scrambleKey = "NEM") {
this.transport = transport;
transport.decorateAppAPIMethods(this,
['isAppSupported', 'getAccount', 'signTransaction', 'signCosignatureTransaction'],
scrambleKey);
}

/**
* Return true if app version is above the supported Symbol BOLOS app version
* @return {boolean}
*/
async isAppSupported() {
const appVersion = await this.getAppVersion();
if (appVersion.majorVersion < SUPPORT_VERSION.LEDGER_MAJOR_VERSION) {
return false;
} else if (appVersion.minorVersion < SUPPORT_VERSION.LEDGER_MINOR_VERSION) {
return false;
} else if (appVersion.patchVersion < SUPPORT_VERSION.LEDGER_PATCH_VERSION) {
return false;
} else {
return true;
}
}

/**
* Get Symbol BOLOS app version
*
* @return an object contain major, minor, patch version of the Symbol BOLOS app
*/
async getAppVersion() {
// APDU fields configuration
const apdu = {
cla: 0xe0,
ins: 0x06,
p1: 0x00,
p2: 0x00,
data: Buffer.alloc(1, 0x00, 'hex'),
};
// Response from Ledger
const response = await this.transport.send(apdu.cla, apdu.ins, apdu.p1, apdu.p2, apdu.data);
const result = {
majorVersion: '',
minorVersion: '',
patchVersion: '',
};
result.majorVersion = response[1];
result.minorVersion = response[2];
result.patchVersion = response[3];
return result;
}

/**
* get Symbol's address for a given BIP 44 path from the Ledger
*
* @param path a path in BIP 44 format
* @param display optionally enable or not the display
* @param chainCode optionally enable or not the chainCode request
* @param ed25519
* @return an object with a publicKey, address and (optionally) chainCode
* @example
* const result = await Ledger.getAccount(bip44path);
* const { publicKey } = result;
*/
async getAccount(path, networkType, display) {
const GET_ACCOUNT_INS_FIELD = 0x02;
const chainCode = false;

const bipPath = BIPPath.fromString(path).toPathArray();
const curveMask = 0x80; // use Curve25519

// APDU fields configuration
const apdu = {
cla: CLA_FIELD,
ins: GET_ACCOUNT_INS_FIELD,
p1: display ? 0x01 : 0x00,
p2: curveMask | (chainCode ? 0x01 : 0x00),
data: Buffer.alloc(1 + bipPath.length * 4 + 1),
};

apdu.data.writeInt8(bipPath.length, 0);
bipPath.forEach((segment, index) => {
apdu.data.writeUInt32BE(segment, 1 + index * 4);
});
apdu.data.writeUInt8(networkType, 1 + bipPath.length * 4);

// Response from Ledger
const response = await this.transport.send(apdu.cla, apdu.ins, apdu.p1, apdu.p2, apdu.data);
const result = {
publicKey: '',
};

const publicKeyLength = response[0];
result.publicKey = response.slice(1, 1 + publicKeyLength).toString('hex');
return result;
}

/**
* TODO: sign a Symbol transaction by account on Ledger at given BIP 44 path
*
* @param path a path in BIP 44 format
* @param transferTransaction a transfer transaction needs to be signed
* @param networkGenerationHash the network generation hash of block 1
* @param signerPublicKey the public key of signer
* @return a signed Transaction which is signed by account at path on Ledger
*/
async signTransaction(path, transferTransaction, networkGenerationHash, signerPublicKey) {
const rawPayload = transferTransaction.serialize();
const signingBytes = networkGenerationHash + rawPayload.slice(216);
const rawTx = Buffer.from(signingBytes, 'hex');
const response = await this.ledgerMessageHandler(path, rawTx);
// Response from Ledger
const h = response.toString('hex');
const signature = h.slice(0, 128);
const payload = rawPayload.slice(0, 16) + signature + signerPublicKey + rawPayload.slice(16 + 128 + 64, rawPayload.length);
const generationHashBytes = Array.from(Convert.hexToUint8(networkGenerationHash));
const transactionHash = Transaction.createTransactionHash(payload, generationHashBytes);
const signedTransaction = new SignedTransaction(
payload,
transactionHash,
signerPublicKey,
transferTransaction.type,
transferTransaction.networkType,
);
return signedTransaction;
}

/**
* TODO: sign a Symbol Cosignature transaction with a given BIP 44 path
*
* @param path a path in BIP 44 format
* @param transferTransaction a transfer transaction needs to be signed
* @param signerPublicKey the public key of signer
* @return a Signed Cosignature Transaction
*/
async signCosignatureTransaction(path, cosignatureTransaction, signerPublicKey) {
const rawPayload = cosignatureTransaction.serialize();
const signingBytes = cosignatureTransaction.transactionInfo.hash + rawPayload.slice(216);
const rawTx = Buffer.from(signingBytes, 'hex');
const response = await this.ledgerMessageHandler(path, rawTx);
// Response from Ledger
const h = response.toString('hex');
const signature = h.slice(0, 128);
const cosignatureSignedTransaction = new CosignatureSignedTransaction(
cosignatureTransaction.transactionInfo.hash,
signature,
signerPublicKey,
);
return cosignatureSignedTransaction;
}
/**
* handle sending and receiving packages between Ledger and Wallet
* @param path a path in BIP 44 format
* @param rawTx a raw payload transaction hex string
* @returns respond package from Ledger
*/
async ledgerMessageHandler(path, rawTx) {
const TX_INS_FIELD = 0x04;
const MAX_CHUNK_SIZE = 255;
const CONTINUE_SENDING = '0x9000';

const chainCode = false;

const curveMask = 0x80; // use Curve25519
const bipPath = BIPPath.fromString(path).toPathArray();
const apduArray = [];
let offset = 0;

while (offset !== rawTx.length) {
const maxChunkSize = offset === 0 ? MAX_CHUNK_SIZE - 1 - bipPath.length * 4 : MAX_CHUNK_SIZE;
const chunkSize = offset + maxChunkSize > rawTx.length ? rawTx.length - offset : maxChunkSize;
// APDU fields configuration
const apdu = {
cla: CLA_FIELD,
ins: TX_INS_FIELD,
p1: offset === 0 ? (chunkSize < maxChunkSize ? 0x00 : 0x80) : chunkSize < maxChunkSize ? 0x01 : 0x81,
p2: curveMask | (chainCode ? 0x01 : 0x00),
data: offset === 0 ? Buffer.alloc(1 + bipPath.length * 4 + chunkSize) : Buffer.alloc(chunkSize),
};

if (offset === 0) {
apdu.data.writeInt8(bipPath.length, 0);
bipPath.forEach((segment, index) => {
apdu.data.writeUInt32BE(segment, 1 + index * 4);
});
rawTx.copy(apdu.data, 1 + bipPath.length * 4, offset, offset + chunkSize);
} else {
rawTx.copy(apdu.data, 0, offset, offset + chunkSize);
}
apduArray.push(apdu);
offset += chunkSize;
}
let response = Buffer.alloc(0);
for (const apdu of apduArray) {
response = await this.transport.send(apdu.cla, apdu.ins, apdu.p1, apdu.p2, apdu.data);
}

if (response.toString() != CONTINUE_SENDING) {
return response;
}
}
}
Loading