From 72bc5f3d0215d8f196e05da485e33f82dabdff5a Mon Sep 17 00:00:00 2001 From: Jeff Shuo Date: Wed, 16 Mar 2022 15:16:22 +0800 Subject: [PATCH 01/11] merged with master --- package.json | 8 +- public/img/access_icons/day/secux.svg | 11 + public/img/access_icons/night/secux.svg | 11 + src/components/Secux/SecuXButton.vue | 182 +++ src/components/Secux/SecuXButtonBle.vue | 183 +++ .../HdDerivationList/HDDerivationList.vue | 3 +- .../modals/HdDerivationList/HdChainTable.vue | 3 +- .../HdDerivationList/HdDerivationListRow.vue | 1 + .../TopCards/AddressCard/AddressCard.vue | 1 + .../advanced/SignMessage/SearchAddress.vue | 3 +- src/js/wallets/MnemonicWallet.ts | 1 + src/js/wallets/SecuXWallet.ts | 1009 +++++++++++++++++ src/js/wallets/types.ts | 5 +- src/store/index.ts | 9 + src/store/modules/secux/secux.ts | 38 + src/store/modules/secux/types.ts | 16 + src/store/types.ts | 6 + src/views/access/Menu.vue | 6 + vue.config.js | 2 +- yarn.lock | 210 +++- 20 files changed, 1695 insertions(+), 13 deletions(-) create mode 100644 public/img/access_icons/day/secux.svg create mode 100644 public/img/access_icons/night/secux.svg create mode 100644 src/components/Secux/SecuXButton.vue create mode 100644 src/components/Secux/SecuXButtonBle.vue create mode 100644 src/js/wallets/SecuXWallet.ts create mode 100644 src/store/modules/secux/secux.ts create mode 100644 src/store/modules/secux/types.ts diff --git a/package.json b/package.json index 7f98f2da3..55c6d6135 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,12 @@ "@ledgerhq/hw-transport-u2f": "5.22.0", "@ledgerhq/hw-transport-webhid": "^5.51.1", "@ledgerhq/hw-transport-webusb": "5.22.0", - "@obsidiansystems/hw-app-avalanche": "0.2.2", + "@secux/hw-app-avalanche": "https://github.com/jshuo/hw-app-avalanche.git#249c5b5dafaff7a58ba16d5e1a0cfad49a483e98", "@openzeppelin/contracts": "^3.4.2", + "@secux/app-eth": "^2.1.6", + "@secux/protocol-transaction": "^2.1.3", + "@secux/transport-webusb": "^2.1.2", + "@secux/transport-webble": "^2.1.2", "@types/big.js": "4.0.5", "@types/create-hash": "1.2.2", "@types/crypto-js": "^4.0.2", @@ -134,7 +138,7 @@ "sass": "^1.24.4", "sass-loader": "^8.0.2", "ts-jest": "^26.1.1", - "typescript": "~3.9.5", + "typescript": "^3.9.10", "vue-cli-plugin-vuetify": "^2.0.3", "vue-jest": "^3.0.5", "vue-template-compiler": "2.6.12", diff --git a/public/img/access_icons/day/secux.svg b/public/img/access_icons/day/secux.svg new file mode 100644 index 000000000..0b0504748 --- /dev/null +++ b/public/img/access_icons/day/secux.svg @@ -0,0 +1,11 @@ + + + +Layer 1 + + + + + + + \ No newline at end of file diff --git a/public/img/access_icons/night/secux.svg b/public/img/access_icons/night/secux.svg new file mode 100644 index 000000000..9c23c9847 --- /dev/null +++ b/public/img/access_icons/night/secux.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/Secux/SecuXButton.vue b/src/components/Secux/SecuXButton.vue new file mode 100644 index 000000000..efbbea7ce --- /dev/null +++ b/src/components/Secux/SecuXButton.vue @@ -0,0 +1,182 @@ + + + diff --git a/src/components/Secux/SecuXButtonBle.vue b/src/components/Secux/SecuXButtonBle.vue new file mode 100644 index 000000000..5b23661a7 --- /dev/null +++ b/src/components/Secux/SecuXButtonBle.vue @@ -0,0 +1,183 @@ + + + diff --git a/src/components/modals/HdDerivationList/HDDerivationList.vue b/src/components/modals/HdDerivationList/HDDerivationList.vue index f7220114c..af0ac5c9d 100644 --- a/src/components/modals/HdDerivationList/HDDerivationList.vue +++ b/src/components/modals/HdDerivationList/HDDerivationList.vue @@ -46,6 +46,7 @@ import Big from 'big.js' import AvaAsset from '@/js/AvaAsset' import { DerivationListBalanceDict } from '@/components/modals/HdDerivationList/types' import { LedgerWallet } from '../../../js/wallets/LedgerWallet' +import { SecuXWallet } from '../../../js/wallets/SecuXWallet' import { bnToBig } from '@/helpers/helper' import { BN } from 'avalanche' import HdChainTable from '@/components/modals/HdDerivationList/HdChainTable.vue' @@ -56,7 +57,7 @@ import HdChainTable from '@/components/modals/HdDerivationList/HdChainTable.vue' }, }) export default class HDDerivationList extends Vue { - @Prop() wallet!: MnemonicWallet | LedgerWallet + @Prop() wallet!: MnemonicWallet | LedgerWallet | SecuXWallet addrsExternal: string[] = [] addrsInternal: string[] = [] diff --git a/src/components/modals/HdDerivationList/HdChainTable.vue b/src/components/modals/HdDerivationList/HdChainTable.vue index 370705113..4a9ce8549 100644 --- a/src/components/modals/HdDerivationList/HdChainTable.vue +++ b/src/components/modals/HdDerivationList/HdChainTable.vue @@ -40,6 +40,7 @@ import { Vue, Component, Prop } from 'vue-property-decorator' import MnemonicWallet from '@/js/wallets/MnemonicWallet' import { LedgerWallet } from '@/js/wallets/LedgerWallet' +import { SecuXWallet } from '@/js/wallets/SecuXWallet' import { DerivationListBalanceDict } from '@/components/modals/HdDerivationList/types' import HdDerivationListRow from '@/components/modals/HdDerivationList/HdDerivationListRow.vue' import HdEmptyAddressRow from '@/components/modals/HdDerivationList/HdEmptyAddressRow.vue' @@ -48,7 +49,7 @@ import { HdHelper } from '@/js/HdHelper' components: { HdEmptyAddressRow, HdDerivationListRow }, }) export default class HdChainTable extends Vue { - @Prop() wallet!: MnemonicWallet | LedgerWallet + @Prop() wallet!: MnemonicWallet | LedgerWallet | SecuXWallet @Prop() addresses!: string[] @Prop() balanceDict!: DerivationListBalanceDict[] @Prop() path!: number diff --git a/src/components/modals/HdDerivationList/HdDerivationListRow.vue b/src/components/modals/HdDerivationList/HdDerivationListRow.vue index b1f7ffb9d..cd612d029 100644 --- a/src/components/modals/HdDerivationList/HdDerivationListRow.vue +++ b/src/components/modals/HdDerivationList/HdDerivationListRow.vue @@ -25,6 +25,7 @@ import { Vue, Component, Prop } from 'vue-property-decorator' import Big from 'big.js' import { DerivationListBalanceDict } from '@/components/modals/HdDerivationList/types' import { LedgerWallet } from '@/js/wallets/LedgerWallet' +import { SecuXWallet } from '@/js/wallets/SecuXWallet' import { WalletType } from '@/js/wallets/types' import { ava } from '@/AVA' diff --git a/src/components/wallet/TopCards/AddressCard/AddressCard.vue b/src/components/wallet/TopCards/AddressCard/AddressCard.vue index 518ec9607..19ad094fd 100644 --- a/src/components/wallet/TopCards/AddressCard/AddressCard.vue +++ b/src/components/wallet/TopCards/AddressCard/AddressCard.vue @@ -64,6 +64,7 @@ import MnemonicWallet, { LEDGER_ETH_ACCOUNT_PATH, } from '@/js/wallets/MnemonicWallet' import { LedgerWallet } from '@/js/wallets/LedgerWallet' +import { SecuXWallet } from '@/js/wallets/SecuXWallet' import ChainSelect from '@/components/wallet/TopCards/AddressCard/ChainSelect.vue' import { ChainIdType } from '@/constants' diff --git a/src/components/wallet/advanced/SignMessage/SearchAddress.vue b/src/components/wallet/advanced/SignMessage/SearchAddress.vue index 7fdca0cc8..9d8be922b 100644 --- a/src/components/wallet/advanced/SignMessage/SearchAddress.vue +++ b/src/components/wallet/advanced/SignMessage/SearchAddress.vue @@ -30,11 +30,12 @@ import { Vue, Component, Prop, Model } from 'vue-property-decorator' import MnemonicWallet from '@/js/wallets/MnemonicWallet' import { LedgerWallet } from '@/js/wallets/LedgerWallet' +import { SecuXWallet } from '@/js/wallets/SecuXWallet' @Component export default class SearchAddress extends Vue { @Model('change', { type: String }) readonly selectedAddress!: string | null - @Prop() wallet!: MnemonicWallet | LedgerWallet + @Prop() wallet!: MnemonicWallet | LedgerWallet | SecuXWallet address: string = '' matchingAddrs: string[] = [] diff --git a/src/js/wallets/MnemonicWallet.ts b/src/js/wallets/MnemonicWallet.ts index 0dd1bf57a..9aed38831 100644 --- a/src/js/wallets/MnemonicWallet.ts +++ b/src/js/wallets/MnemonicWallet.ts @@ -55,6 +55,7 @@ const AVA_TOKEN_INDEX: string = '9000' export const AVA_ACCOUNT_PATH: string = `m/44'/${AVA_TOKEN_INDEX}'/0'` // Change and index left out export const ETH_ACCOUNT_PATH: string = `m/44'/60'/0'` export const LEDGER_ETH_ACCOUNT_PATH = ETH_ACCOUNT_PATH + '/0/0' +export const SECUX_ETH_ACCOUNT_PATH = ETH_ACCOUNT_PATH + '/0/0' const INDEX_RANGE: number = 20 // a gap of at least 20 indexes is needed to claim an index unused const SCAN_SIZE: number = 70 // the total number of utxos to look at initially to calculate last index diff --git a/src/js/wallets/SecuXWallet.ts b/src/js/wallets/SecuXWallet.ts new file mode 100644 index 000000000..42fe99a05 --- /dev/null +++ b/src/js/wallets/SecuXWallet.ts @@ -0,0 +1,1009 @@ +// import AppBtc from "@ledgerhq/hw-app-btc"; +//@ts-ignore +import AppAvax from '@secux/hw-app-avalanche' +//@ts-ignore + +// import { SecuxTransactionTool } from "@secux/protocol-transaction"; + +import EthereumjsCommon from '@ethereumjs/common' +import { Transaction } from '@ethereumjs/tx' + +import moment from 'moment' +import { Buffer, BN } from 'avalanche' +import HDKey from 'hdkey' +import { ava, avm, bintools, cChain, pChain } from '@/AVA' +const bippath = require('bip32-path') +import createHash from 'create-hash' +import store from '@/store' +import { importPublic, publicToAddress, bnToRlp, bnToHex, rlp } from 'ethereumjs-util' + +import { UTXO as AVMUTXO, UTXO, UTXOSet as AVMUTXOSet } from 'avalanche/dist/apis/avm/utxos' +import { AvaWalletCore } from '@/js/wallets/types' +import { ITransaction } from '@/components/wallet/transfer/types' +import { + AVMConstants, + OperationTx, + SelectCredentialClass as AVMSelectCredentialClass, + TransferableOperation, + Tx as AVMTx, + UnsignedTx as AVMUnsignedTx, + ImportTx as AVMImportTx, +} from 'avalanche/dist/apis/avm' + +import { + ImportTx as PlatformImportTx, + ExportTx as PlatformExportTx, + Tx as PlatformTx, + UTXO as PlatformUTXO, + UnsignedTx as PlatformUnsignedTx, + PlatformVMConstants, + SelectCredentialClass as PlatformSelectCredentialClass, + AddDelegatorTx, + AddValidatorTx, +} from 'avalanche/dist/apis/platformvm' + +import { + UnsignedTx as EVMUnsignedTx, + ImportTx as EVMImportTx, + ExportTx as EVMExportTx, + Tx as EvmTx, + EVMConstants, + EVMInput, + SelectCredentialClass as EVMSelectCredentialClass, +} from 'avalanche/dist/apis/evm' + +import { Credential, SigIdx, Signature, UTXOResponse, Address } from 'avalanche/dist/common' +import { getPreferredHRP, PayloadBase } from 'avalanche/dist/utils' +import { HdWalletCore } from '@/js/wallets/HdWalletCore' +import { ISecuXConfig } from '@/store/types' +import { WalletNameType } from '@/js/wallets/types' +import { bnToBig, digestMessage } from '@/helpers/helper' +import { abiDecoder, web3 } from '@/evm' +import { AVA_ACCOUNT_PATH, ETH_ACCOUNT_PATH, SECUX_ETH_ACCOUNT_PATH } from './MnemonicWallet' +import { ChainIdType } from '@/constants' +import { ParseableAvmTxEnum, ParseablePlatformEnum, ParseableEvmTxEnum } from '../TxHelper' +import { ISecuXBlockMessage } from '../../store/modules/secux/types' +import Erc20Token from '@/js/Erc20Token' +import { WalletHelper } from '@/helpers/wallet_helper' +import { Utils, NetworkHelper, Network } from '@avalabs/avalanche-wallet-sdk' + +export const MIN_EVM_SUPPORT_V = '0.5.3' + +class SecuXWallet extends HdWalletCore implements AvaWalletCore { + type: WalletNameType = 'SecuX' + ethAddress: string + ethBalance: BN + + constructor( + public app: AppAvax, + public transport: any, + public hdkey: HDKey, + public config: ISecuXConfig, + public hdEth: HDKey, + public eth: any + ) { + super(hdkey, hdEth) + this.type = 'SecuX' + + if (hdEth) { + const ethKey = hdEth + const ethPublic = importPublic(ethKey.publicKey) + this.ethAddress = publicToAddress(ethPublic).toString('hex') + this.ethBalance = new BN(0) + } else { + this.ethAddress = '' + this.ethBalance = new BN(0) + } + } + + static async fromApp(app: any, eth: any, transport: any, config: ISecuXConfig) { + let ethRes = await transport.getXPublickey(SECUX_ETH_ACCOUNT_PATH, false) + let res = await transport.getXPublickey(AVA_ACCOUNT_PATH, false) + + let hd = new HDKey() + hd.publicKey = res.publicKey + hd.chainCode = res.chainCode + let hdEth = new HDKey() + // @ts-ignore + hdEth.publicKey = ethRes.publicKey + // @ts-ignore + hdEth.chainCode = ethRes.chainCode + // @ts-ignore + return new SecuXWallet(app, transport, hd, config, hdEth, eth) + } + + // Returns an array of derivation paths that need to sign this transaction + // Used with signTransactionHash and signTransactionParsable + getTransactionPaths( + unsignedTx: UnsignedTx, + chainId: ChainIdType + ): { paths: string[]; isAvaxOnly: boolean } { + // TODO: This is a nasty fix. Remove when AJS is updated. + unsignedTx.toBuffer() + let tx = unsignedTx.getTransaction() + let txType = tx.getTxType() + + let ins = tx.getIns() + let operations: TransferableOperation[] = [] + + // Try to get operations, it will fail if there are none, ignore and continue + try { + operations = (tx as OperationTx).getOperations() + } catch (e) { + console.log(e) + } + + let items = ins + if ( + (txType === AVMConstants.IMPORTTX && chainId === 'X') || + (txType === PlatformVMConstants.IMPORTTX && chainId === 'P') + ) { + items = ((tx as AVMImportTx) || PlatformImportTx).getImportInputs() + } + + let hrp = getPreferredHRP(ava.getNetworkID()) + let paths: string[] = [] + + let isAvaxOnly = true + + // Collect derivation paths for source addresses + for (let i = 0; i < items.length; i++) { + let item = items[i] + + let assetId = bintools.cb58Encode(item.getAssetID()) + // @ts-ignore + if (assetId !== store.state.Assets.AVA_ASSET_ID) { + isAvaxOnly = false + } + + let sigidxs: SigIdx[] = item.getInput().getSigIdxs() + let sources = sigidxs.map((sigidx) => sigidx.getSource()) + let addrs: string[] = sources.map((source) => { + return bintools.addressToString(hrp, chainId, source) + }) + + for (let j = 0; j < addrs.length; j++) { + let srcAddr = addrs[j] + let pathStr = this.getPathFromAddress(srcAddr) // returns change/index + + paths.push(pathStr) + } + } + + // Do the Same for operational inputs, if there are any... + for (let i = 0; i < operations.length; i++) { + let op = operations[i] + let sigidxs: SigIdx[] = op.getOperation().getSigIdxs() + let sources = sigidxs.map((sigidx) => sigidx.getSource()) + let addrs: string[] = sources.map((source) => { + return bintools.addressToString(hrp, chainId, source) + }) + + for (let j = 0; j < addrs.length; j++) { + let srcAddr = addrs[j] + let pathStr = this.getPathFromAddress(srcAddr) // returns change/index + + paths.push(pathStr) + } + } + + return { paths, isAvaxOnly } + } + + pathsToUniqueBipPaths(paths: string[]) { + let uniquePaths = paths.filter((val: any, i: number) => { + return paths.indexOf(val) === i + }) + + let bip32Paths = uniquePaths.map((path) => { + return bippath.fromString(path, false) + }) + + return bip32Paths + } + + getChangeBipPath( + unsignedTx: UnsignedTx, + chainId: ChainIdType + ) { + if (chainId === 'C') { + return null + } + + let tx = unsignedTx.getTransaction() + let txType = tx.getTxType() + + const chainChangePath = this.getChangePath(chainId).split('m/')[1] + let changeIdx = this.getChangeIndex(chainId) + // If change and destination paths are the same + // it can cause secux to not display the destination amt. + // Since platform helper does not have internal/external + // path for change (it uses the next address) + // there can be an address collisions. + if ( + (txType === PlatformVMConstants.IMPORTTX || txType === PlatformVMConstants.EXPORTTX) && + this.platformHelper.hdIndex === this.externalHelper.hdIndex + ) { + return null + } else if ( + txType === PlatformVMConstants.ADDVALIDATORTX || + txType === PlatformVMConstants.ADDDELEGATORTX + ) { + changeIdx = this.platformHelper.getFirstAvailableIndex() + } + + return bippath.fromString(`${AVA_ACCOUNT_PATH}/${chainChangePath}/${changeIdx}`) + } + + getCredentials( + unsignedTx: UnsignedTx, + paths: string[], + sigMap: any, + chainId: ChainIdType + ): Credential[] { + let creds: Credential[] = [] + let tx = unsignedTx.getTransaction() + let txType = tx.getTxType() + + // @ts-ignore + let ins = tx.getIns ? tx.getIns() : [] + let operations: TransferableOperation[] = [] + let evmInputs: EVMInput[] = [] + + let items = ins + if ( + (txType === AVMConstants.IMPORTTX && chainId === 'X') || + (txType === PlatformVMConstants.IMPORTTX && chainId === 'P') || + (txType === EVMConstants.IMPORTTX && chainId === 'C') + ) { + items = ((tx as AVMImportTx) || PlatformImportTx || EVMImportTx).getImportInputs() + } + + // Try to get operations, it will fail if there are none, ignore and continue + try { + operations = (tx as OperationTx).getOperations() + } catch (e) { + console.error(e) + } + + let CredentialClass + if (chainId === 'X') { + CredentialClass = AVMSelectCredentialClass + } else if (chainId === 'P') { + CredentialClass = PlatformSelectCredentialClass + } else { + CredentialClass = EVMSelectCredentialClass + } + + // Try to get evm inputs, it will fail if there are none, ignore and continue + try { + evmInputs = (tx as EVMExportTx).getInputs() + } catch (e) { + console.error(e) + } + + for (let i = 0; i < items.length; i++) { + const sigidxs: SigIdx[] = items[i].getInput().getSigIdxs() + const cred: Credential = CredentialClass(items[i].getInput().getCredentialID()) + + for (let j = 0; j < sigidxs.length; j++) { + let pathIndex = i + j + let pathStr = paths[pathIndex] + + let sigRaw = sigMap.get(pathStr) + let sigBuff = Buffer.from(sigRaw) + const sig: Signature = new Signature() + sig.fromBuffer(sigBuff) + cred.addSignature(sig) + } + creds.push(cred) + } + + for (let i = 0; i < operations.length; i++) { + let op = operations[i].getOperation() + const sigidxs: SigIdx[] = op.getSigIdxs() + const cred: Credential = CredentialClass(op.getCredentialID()) + + for (let j = 0; j < sigidxs.length; j++) { + let pathIndex = items.length + i + j + let pathStr = paths[pathIndex] + + let sigRaw = sigMap.get(pathStr) + let sigBuff = Buffer.from(sigRaw) + const sig: Signature = new Signature() + sig.fromBuffer(sigBuff) + cred.addSignature(sig) + } + creds.push(cred) + } + + for (let i = 0; i < evmInputs.length; i++) { + let evmInput = evmInputs[i] + const sigidxs: SigIdx[] = evmInput.getSigIdxs() + const cred: Credential = CredentialClass(evmInput.getCredentialID()) + + for (let j = 0; j < sigidxs.length; j++) { + let pathIndex = items.length + i + j + let pathStr = paths[pathIndex] + + let sigRaw = sigMap.get(pathStr) + let sigBuff = Buffer.from(sigRaw) + const sig: Signature = new Signature() + sig.fromBuffer(sigBuff) + cred.addSignature(sig) + } + creds.push(cred) + } + + return creds + } + + // Used for non parsable transactions. + // Ideally we wont use this function at all, but secux is not ready yet. + async signTransactionHash< + UnsignedTx extends AVMUnsignedTx | PlatformUnsignedTx | EVMUnsignedTx, + SignedTx extends AVMTx | PlatformTx | EvmTx + >(unsignedTx: UnsignedTx, paths: string[], chainId: ChainIdType): Promise { + let txbuff = unsignedTx.toBuffer() + const msg: Buffer = Buffer.from(createHash('sha256').update(txbuff).digest()) + + try { + store.commit('SecuX/openModal', { + title: 'Sign Hash', + messages: [], + info: msg.toString('hex').toUpperCase(), + }) + + let bip32Paths = this.pathsToUniqueBipPaths(paths) + + // Sign the msg with secux + const accountPathSource = chainId === 'C' ? ETH_ACCOUNT_PATH : AVA_ACCOUNT_PATH + const accountPath = bippath.fromString(`${accountPathSource}`) + let sigMap = await await this.app.signHash(accountPath, bip32Paths, msg) + store.commit('SecuX/closeModal') + + let creds: Credential[] = this.getCredentials( + unsignedTx, + paths, + sigMap, + chainId + ) + + let signedTx + switch (chainId) { + case 'X': + signedTx = new AVMTx(unsignedTx as AVMUnsignedTx, creds) + break + case 'P': + signedTx = new PlatformTx(unsignedTx as PlatformUnsignedTx, creds) + break + case 'C': + signedTx = new EvmTx(unsignedTx as EVMUnsignedTx, creds) + break + } + + return signedTx as SignedTx + } catch (e) { + store.commit('SecuX/closeModal') + console.error(e) + throw e + } + } + + // Used for signing transactions that are parsable + async signTransactionParsable< + UnsignedTx extends AVMUnsignedTx | PlatformUnsignedTx | EVMUnsignedTx, + SignedTx extends AVMTx | PlatformTx | EvmTx + >(unsignedTx: UnsignedTx, paths: string[], chainId: ChainIdType): Promise { + let tx = unsignedTx.getTransaction() + let txType = tx.getTxType() + let parseableTxs = { + X: ParseableAvmTxEnum, + P: ParseablePlatformEnum, + C: ParseableEvmTxEnum, + }[chainId] + + let title = `Sign ${parseableTxs[txType]}` + + let bip32Paths = this.pathsToUniqueBipPaths(paths) + const accountPathSource = chainId === 'C' ? ETH_ACCOUNT_PATH : AVA_ACCOUNT_PATH + let txbuff = unsignedTx.toBuffer() + let changePath = this.getChangeBipPath(unsignedTx, chainId) + let messages = this.getTransactionMessages(unsignedTx, chainId, changePath) + + try { + store.commit('SecuX/openModal', { + title: title, + messages: messages, + info: null, + }) + + let SecuXSignedTx = await this.app.signTransaction(accountPathSource, paths, txbuff) + + let sigMap = SecuXSignedTx.signatures + let creds = this.getCredentials(unsignedTx, paths, sigMap, chainId) + + let signedTx + switch (chainId) { + case 'X': + signedTx = new AVMTx(unsignedTx as AVMUnsignedTx, creds) + break + case 'P': + signedTx = new PlatformTx(unsignedTx as PlatformUnsignedTx, creds) + break + case 'C': + signedTx = new EvmTx(unsignedTx as EVMUnsignedTx, creds) + break + } + + return signedTx as SignedTx + } catch (e) { + store.commit('SecuX/closeModal') + console.error(e) + throw e + } + } + + getOutputMsgs( + unsignedTx: UnsignedTx, + chainId: ChainIdType, + changePath: null | { toPathArray: () => number[] } + ): ISecuXBlockMessage[] { + let messages: ISecuXBlockMessage[] = [] + let hrp = getPreferredHRP(ava.getNetworkID()) + let tx = unsignedTx.getTransaction() + let txType = tx.getTxType() + + // @ts-ignore + let outs + if ( + (txType === AVMConstants.EXPORTTX && chainId === 'X') || + (txType === PlatformVMConstants.EXPORTTX && chainId === 'P') + ) { + outs = (tx as PlatformExportTx).getExportOutputs() + } else if (txType === EVMConstants.EXPORTTX && chainId === 'C') { + outs = (tx as EVMExportTx).getExportedOutputs() + } else { + outs = (tx as PlatformExportTx).getOuts() + } + + let destinationChain = chainId + if (chainId === 'C' && txType === EVMConstants.EXPORTTX) destinationChain = 'X' + + if (destinationChain === 'C') { + for (let i = 0; i < outs.length; i++) { + // @ts-ignore + const value = outs[i].getAddress() + const addr = bintools.addressToString(hrp, chainId, value) + // @ts-ignore + const amt = bnToBig(outs[i].getAmount(), 9) + + messages.push({ + title: 'Output', + value: `${addr} - ${amt.toString()} AVAX`, + }) + } + } else { + let changeIdx = changePath?.toPathArray()[changePath?.toPathArray().length - 1] + let changeAddr = this.getChangeFromIndex(changeIdx, destinationChain) + + for (let i = 0; i < outs.length; i++) { + outs[i] + .getOutput() + .getAddresses() + .forEach((value) => { + const addr = bintools.addressToString(hrp, chainId, value) + // @ts-ignore + const amt = bnToBig(outs[i].getOutput().getAmount(), 9) + + if (!changePath || changeAddr !== addr) + messages.push({ + title: 'Output', + value: `${addr} - ${amt.toString()} AVAX`, + }) + }) + } + } + + return messages + } + + getValidateDelegateMsgs( + unsignedTx: UnsignedTx, + chainId: ChainIdType + ): ISecuXBlockMessage[] { + let tx = + ((unsignedTx as + | AVMUnsignedTx + | PlatformUnsignedTx).getTransaction() as AddValidatorTx) || AddDelegatorTx + let txType = tx.getTxType() + let messages: ISecuXBlockMessage[] = [] + + if ( + (txType === PlatformVMConstants.ADDDELEGATORTX && chainId === 'P') || + (txType === PlatformVMConstants.ADDVALIDATORTX && chainId === 'P') + ) { + const format = 'YYYY-MM-DD H:mm:ss UTC' + + const nodeID = bintools.cb58Encode(tx.getNodeID()) + const startTime = moment(tx.getStartTime().toNumber() * 1000) + .utc() + .format(format) + + const endTime = moment(tx.getEndTime().toNumber() * 1000) + .utc() + .format(format) + + const stakeAmt = bnToBig(tx.getStakeAmount(), 9) + + const rewardOwners = tx.getRewardOwners() + let hrp = ava.getHRP() + const rewardAddrs = rewardOwners + .getOutput() + .getAddresses() + .map((addr) => { + return bintools.addressToString(hrp, chainId, addr) + }) + + messages.push({ title: 'NodeID', value: nodeID }) + messages.push({ title: 'Start Time', value: startTime }) + messages.push({ title: 'End Time', value: endTime }) + messages.push({ title: 'Total Stake', value: `${stakeAmt} AVAX` }) + messages.push({ + title: 'Stake', + value: `${stakeAmt} to ${this.platformHelper.getCurrentAddress()}`, + }) + messages.push({ + title: 'Reward to', + value: `${rewardAddrs.join('\n')}`, + }) + // @ts-ignore + if (tx.delegationFee) { + // @ts-ignore + messages.push({ title: 'Delegation Fee', value: `${tx.delegationFee}%` }) + } + messages.push({ title: 'Fee', value: '0' }) + } + + return messages + } + + getFeeMsgs( + unsignedTx: UnsignedTx, + chainId: ChainIdType + ): ISecuXBlockMessage[] { + let tx = unsignedTx.getTransaction() + let txType = tx.getTxType() + let messages = [] + + if ( + (txType === AVMConstants.BASETX && chainId === 'X') || + (txType === AVMConstants.EXPORTTX && chainId === 'X') || + (txType === AVMConstants.IMPORTTX && chainId === 'X') || + (txType === PlatformVMConstants.EXPORTTX && chainId === 'P') || + (txType === PlatformVMConstants.IMPORTTX && chainId === 'P') || + (txType === EVMConstants.EXPORTTX && chainId === 'C') || + (txType === EVMConstants.IMPORTTX && chainId === 'C') + ) { + messages.push({ title: 'Fee', value: `${0.001} AVAX` }) + } + + return messages + } + + // Given the unsigned transaction returns an array of messages that will be displayed on ledgegr window + getTransactionMessages( + unsignedTx: UnsignedTx, + chainId: ChainIdType, + changePath: null | { toPathArray: () => number[] } + ): ISecuXBlockMessage[] { + let messages: ISecuXBlockMessage[] = [] + + const outputMessages = this.getOutputMsgs(unsignedTx, chainId, changePath) + messages.push(...outputMessages) + + const validateDelegateMessages = this.getValidateDelegateMsgs( + unsignedTx as AVMUnsignedTx | PlatformUnsignedTx, + chainId + ) + messages.push(...validateDelegateMessages) + + const feeMessages = this.getFeeMsgs(unsignedTx, chainId) + messages.push(...feeMessages) + + return messages + } + + getEvmTransactionMessages(tx: Transaction): ISecuXBlockMessage[] { + let gasPrice = tx.gasPrice + let gasLimit = tx.gasLimit + let totFee = gasPrice.mul(new BN(gasLimit)) + let feeNano = Utils.bnToBig(totFee, 9) + + let msgs: ISecuXBlockMessage[] = [] + try { + let test = '0x' + tx.data.toString('hex') + let data = abiDecoder.decodeMethod(test) + + let callMsg: ISecuXBlockMessage = { + title: 'Contract Call', + value: data.name, + } + let paramMsgs: ISecuXBlockMessage[] = data.params.map((param: any) => { + return { + title: param.name, + value: param.value, + } + }) + + let feeMsg: ISecuXBlockMessage = { + title: 'Fee', + value: feeNano.toLocaleString() + ' nAVAX', + } + + msgs = [callMsg, ...paramMsgs, feeMsg] + } catch (e) { + console.log(e) + } + return msgs + } + + async signX(unsignedTx: AVMUnsignedTx): Promise { + let tx = unsignedTx.getTransaction() + let txType = tx.getTxType() + let chainId: ChainIdType = 'X' + + let parseableTxs = ParseableAvmTxEnum + let { paths, isAvaxOnly } = this.getTransactionPaths(unsignedTx, chainId) + + // If SecuX doesnt support parsing, sign hash + let canSecuXParse = this.config.version >= '2.8.0' + let isParsableType = txType in parseableTxs && isAvaxOnly + + let signedTx + if (canSecuXParse && isParsableType) { + signedTx = await this.signTransactionParsable( + unsignedTx, + paths, + chainId + ) + } else { + signedTx = await this.signTransactionHash( + unsignedTx, + paths, + chainId + ) + } + + store.commit('SecuX/closeModal') + return signedTx + } + + async signP(unsignedTx: PlatformUnsignedTx): Promise { + let tx = unsignedTx.getTransaction() + let txType = tx.getTxType() + let chainId: ChainIdType = 'P' + let parseableTxs = ParseablePlatformEnum + + let { paths, isAvaxOnly } = this.getTransactionPaths( + unsignedTx, + chainId + ) + // If SecuX doesnt support parsing, sign hash + let canSecuXParse = this.config.version >= '0.3.1' + let isParsableType = txType in parseableTxs && isAvaxOnly + + // TODO: Remove after SecuX is fixed + // If UTXOS contain lockedStakeable funds always use sign hash + let txIns = unsignedTx.getTransaction().getIns() + for (var i = 0; i < txIns.length; i++) { + let typeID = txIns[i].getInput().getTypeID() + if (typeID === PlatformVMConstants.STAKEABLELOCKINID) { + canSecuXParse = false + break + } + } + + // TODO: Remove after SecuX update + // SecuX is not able to parse P/C atomic transactions + if (txType === PlatformVMConstants.EXPORTTX) { + const destChainBuff = (tx as PlatformExportTx).getDestinationChain() + // If destination chain is C chain, sign hash + const destChain = Network.idToChainAlias(bintools.cb58Encode(destChainBuff)) + if (destChain === 'C') { + canSecuXParse = false + } + } + // TODO: Remove after SecuX update + if (txType === PlatformVMConstants.IMPORTTX) { + const sourceChainBuff = (tx as PlatformImportTx).getSourceChain() + // If destination chain is C chain, sign hash + const sourceChain = Network.idToChainAlias(bintools.cb58Encode(sourceChainBuff)) + if (sourceChain === 'C') { + canSecuXParse = false + } + } + + let signedTx + if (canSecuXParse && isParsableType) { + signedTx = await this.signTransactionParsable( + unsignedTx, + paths, + chainId + ) + } else { + signedTx = await this.signTransactionHash( + unsignedTx, + paths, + chainId + ) + } + store.commit('SecuX/closeModal') + return signedTx + } + + async signC(unsignedTx: EVMUnsignedTx): Promise { + // TODO: Might need to upgrade paths array to: + // paths = Array(utxoSet.getAllUTXOs().length).fill('0/0'), + let tx = unsignedTx.getTransaction() + let typeId = tx.getTxType() + + let canSecuXParse = true + + let paths = ['0/0'] + if (typeId === EVMConstants.EXPORTTX) { + let ins = (tx as EVMExportTx).getInputs() + paths = ins.map((input) => '0/0') + } else if (typeId === EVMConstants.IMPORTTX) { + let ins = (tx as EVMImportTx).getImportInputs() + paths = ins.map((input) => '0/0') + } + + // TODO: Remove after SecuX update + // SecuX is not able to parse P/C atomic transactions + if (typeId === EVMConstants.EXPORTTX) { + const destChainBuff = (tx as EVMExportTx).getDestinationChain() + // If destination chain is C chain, sign hash + const destChain = Network.idToChainAlias(bintools.cb58Encode(destChainBuff)) + if (destChain === 'P') { + canSecuXParse = false + } + } + // TODO: Remove after SecuX update + if (typeId === EVMConstants.IMPORTTX) { + const sourceChainBuff = (tx as EVMImportTx).getSourceChain() + // If destination chain is C chain, sign hash + const sourceChain = Network.idToChainAlias(bintools.cb58Encode(sourceChainBuff)) + if (sourceChain === 'P') { + canSecuXParse = false + } + } + + let txSigned + if (canSecuXParse) { + txSigned = (await this.signTransactionParsable(unsignedTx, paths, 'C')) as EvmTx + } else { + txSigned = (await this.signTransactionHash(unsignedTx, paths, 'C')) as EvmTx + } + store.commit('SecuX/closeModal') + return txSigned + } + + async signEvm(tx: Transaction) { + const rawUnsignedTx = rlp.encode([ + bnToRlp(tx.nonce), + bnToRlp(tx.gasPrice), + bnToRlp(tx.gasLimit), + tx.to !== undefined ? tx.to.buf : Buffer.from([]), + bnToRlp(tx.value), + tx.data, + bnToRlp(new BN(tx.getChainId())), + Buffer.from([]), + Buffer.from([]), + ]) + + try { + let msgs = this.getEvmTransactionMessages(tx) + + // Open Modal Prompt + store.commit('SecuX/openModal', { + title: 'Transfer', + messages: msgs, + info: null, + }) + + const chainId = await web3.eth.getChainId() + const response = await this.eth.signTransaction( + this.transport, + SECUX_ETH_ACCOUNT_PATH, + { + chainId: chainId, + nonce: tx.nonce.toNumber(), + gasPrice: tx.gasPrice.toNumber(), + gasLimit: tx.gasLimit.toNumber(), + to: tx.to?.toString(), + value: bnToHex(tx.value), + } + ) + store.commit('SecuX/closeModal') + + const signatureBN = { + v: new BN(response.signature.slice(64), 16), + r: new BN(response.signature.slice(0, 32), 16), + s: new BN(response.signature.slice(32, 64), 16), + } + const networkId = await web3.eth.net.getId() + const chainParams = { + common: EthereumjsCommon.forCustomChain( + 'mainnet', + { networkId, chainId }, + 'istanbul' + ), + } + + const signedTx = Transaction.fromTxData( + { + nonce: tx.nonce, + gasPrice: tx.gasPrice, + gasLimit: tx.gasLimit, + to: tx.to, + value: tx.value, + data: tx.data, + ...signatureBN, + }, + chainParams + ) + return signedTx + } catch (e) { + store.commit('SecuX/closeModal') + console.error(e) + throw e + } + } + + getEvmAddress(): string { + return this.ethAddress + } + + async getStake(): Promise { + this.stakeAmount = await WalletHelper.getStake(this) + return this.stakeAmount + } + + async getEthBalance() { + let bal = await WalletHelper.getEthBalance(this) + this.ethBalance = bal + return bal + } + + async getUTXOs(): Promise { + // TODO: Move to shared file + this.isFetchUtxos = true + // If we are waiting for helpers to initialize delay the call + let isInit = + this.externalHelper.isInit && this.internalHelper.isInit && this.platformHelper.isInit + if (!isInit) { + setTimeout(() => { + this.getUTXOs() + }, 1000) + return + } + + super.getUTXOs() + this.getStake() + this.getEthBalance() + return + } + + getPathFromAddress(address: string) { + let externalAddrs = this.externalHelper.getExtendedAddresses() + let internalAddrs = this.internalHelper.getExtendedAddresses() + let platformAddrs = this.platformHelper.getExtendedAddresses() + + let extIndex = externalAddrs.indexOf(address) + let intIndex = internalAddrs.indexOf(address) + let platformIndex = platformAddrs.indexOf(address) + + if (extIndex >= 0) { + return `0/${extIndex}` + } else if (intIndex >= 0) { + return `1/${intIndex}` + } else if (platformIndex >= 0) { + return `0/${platformIndex}` + } else if (address[0] === 'C') { + return '0/0' + } else { + throw 'Unable to find source address.' + } + } + + async issueBatchTx( + orders: (ITransaction | AVMUTXO)[], + addr: string, + memo: Buffer | undefined + ): Promise { + return await WalletHelper.issueBatchTx(this, orders, addr, memo) + } + + async delegate( + nodeID: string, + amt: BN, + start: Date, + end: Date, + rewardAddress?: string, + utxos?: PlatformUTXO[] + ): Promise { + return await WalletHelper.delegate(this, nodeID, amt, start, end, rewardAddress, utxos) + } + + async validate( + nodeID: string, + amt: BN, + start: Date, + end: Date, + delegationFee: number, + rewardAddress?: string, + utxos?: PlatformUTXO[] + ): Promise { + return await WalletHelper.validate( + this, + nodeID, + amt, + start, + end, + delegationFee, + rewardAddress, + utxos + ) + } + + async signHashByExternalIndex(index: number, hash: Buffer) { + let pathStr = `0/${index}` + const addressPath = bippath.fromString(pathStr, false) + const accountPath = bippath.fromString(`${AVA_ACCOUNT_PATH}`) + + store.commit('SecuX/openModal', { + title: `Sign Hash`, + info: hash.toString('hex').toUpperCase(), + }) + + try { + let sigMap = await this.transport.signHash(accountPath, [addressPath], hash) + store.commit('SecuX/closeModal') + let signed = sigMap.get(pathStr) + return bintools.cb58Encode(signed) + } catch (e) { + store.commit('SecuX/closeModal') + throw e + } + } + + async createNftFamily(name: string, symbol: string, groupNum: number) { + return await WalletHelper.createNftFamily(this, name, symbol, groupNum) + } + + async mintNft(mintUtxo: AVMUTXO, payload: PayloadBase, quantity: number) { + return await WalletHelper.mintNft(this, mintUtxo, payload, quantity) + } + + async sendEth(to: string, amount: BN, gasPrice: BN, gasLimit: number) { + return await WalletHelper.sendEth(this, to, amount, gasPrice, gasLimit) + } + + async estimateGas(to: string, amount: BN, token: Erc20Token): Promise { + return await WalletHelper.estimateGas(this, to, amount, token) + } + + async sendERC20( + to: string, + amount: BN, + gasPrice: BN, + gasLimit: number, + token: Erc20Token + ): Promise { + // throw 'Not Implemented' + return await WalletHelper.sendErc20(this, to, amount, gasPrice, gasLimit, token) + } +} + +export { SecuXWallet } diff --git a/src/js/wallets/types.ts b/src/js/wallets/types.ts index 2d9e2d361..0560cf2de 100644 --- a/src/js/wallets/types.ts +++ b/src/js/wallets/types.ts @@ -30,6 +30,7 @@ import Erc20Token from '@/js/Erc20Token' import { Transaction } from '@ethereumjs/tx' import MnemonicWallet from '@/js/wallets/MnemonicWallet' import { LedgerWallet } from '@/js/wallets/LedgerWallet' +import { SecuXWallet } from '@/js/wallets/SecuXWallet' import { SingletonWallet } from '@/js/wallets/SingletonWallet' import { ExportChainsC, ExportChainsP, ExportChainsX } from '@avalabs/avalanche-wallet-sdk' import { UTXOSet as EVMUTXOSet } from 'avalanche/dist/apis/evm/utxos' @@ -42,8 +43,8 @@ export type ChainAlias = 'X' | 'P' export type AvmImportChainType = 'P' | 'C' export type AvmExportChainType = 'P' | 'C' -export type WalletNameType = 'mnemonic' | 'ledger' | 'singleton' -export type WalletType = MnemonicWallet | LedgerWallet | SingletonWallet +export type WalletNameType = 'mnemonic' | 'ledger' | 'singleton' | 'SecuX' +export type WalletType = MnemonicWallet | LedgerWallet | SingletonWallet | SecuXWallet interface IAddressManager { getCurrentAddressAvm(): string diff --git a/src/store/index.ts b/src/store/index.ts index eaf834cde..7fc81b4df 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -34,6 +34,7 @@ import { readKeyFile, } from '@/js/Keystore' import { LedgerWallet } from '@/js/wallets/LedgerWallet' +import { SecuXWallet } from '@/js/wallets/SecuXWallet' import { SingletonWallet } from '@/js/wallets/SingletonWallet' import { Buffer } from 'avalanche' import { privateToAddress } from 'ethereumjs-util' @@ -128,6 +129,14 @@ export default new Vuex.Store({ dispatch('onAccess') }, + async accessWalletSecuX({ state, dispatch }, wallet: SecuXWallet) { + state.wallets = [wallet] + + await dispatch('activateWallet', wallet) + + dispatch('onAccess') + }, + async accessWalletSingleton({ state, dispatch }, key: string) { let wallet = await dispatch('addWalletSingleton', key) await dispatch('activateWallet', wallet) diff --git a/src/store/modules/secux/secux.ts b/src/store/modules/secux/secux.ts new file mode 100644 index 000000000..b93086023 --- /dev/null +++ b/src/store/modules/secux/secux.ts @@ -0,0 +1,38 @@ +import { Module } from 'vuex' +import { RootState } from '@/store/types' +import { SecuXState } from '@/store/modules/secux/types' + +const secux_module: Module = { + namespaced: true, + state: { + isBlock: false, // if true a modal blocks the window + isPrompt: false, + isUpgradeRequired: false, + isWalletLoading: false, + messages: [], + title: 'title', + info: `info'`, + }, + mutations: { + openModal(state, input) { + state.title = input.title + state.info = input.info + state.messages = input.messages + state.isPrompt = input.isPrompt + state.isBlock = true + }, + closeModal(state) { + state.messages = [] + state.isBlock = false + }, + setIsUpgradeRequired(state, val) { + state.isUpgradeRequired = val + }, + setIsWalletLoading(state, val) { + state.isWalletLoading = val + }, + }, + actions: {}, +} + +export default secux_module diff --git a/src/store/modules/secux/types.ts b/src/store/modules/secux/types.ts new file mode 100644 index 000000000..0e2ab46b3 --- /dev/null +++ b/src/store/modules/secux/types.ts @@ -0,0 +1,16 @@ +export interface ISecuXBlockMessage { + title: string + value: string +} + +export interface SecuXState { + isBlock: boolean + isPrompt: boolean + isUpgradeRequired: boolean + isWalletLoading: boolean + messages: ISecuXBlockMessage[] + title: string + info: string +} + +export const LEDGER_EXCHANGE_TIMEOUT = 90_000 diff --git a/src/store/types.ts b/src/store/types.ts index b047a95eb..6dea54909 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -27,6 +27,12 @@ export interface ILedgerAppConfig { name: 'Avalanche' } +export interface ISecuXConfig { + version: string + commit: string + name: 'Avalanche' +} + export interface priceDict { usd: number } diff --git a/src/views/access/Menu.vue b/src/views/access/Menu.vue index d414bf994..29ae2ddf3 100644 --- a/src/views/access/Menu.vue +++ b/src/views/access/Menu.vue @@ -27,6 +27,8 @@ > + + @@ -39,6 +41,8 @@