diff --git a/source/renderer/app/stores/HardwareWalletsStore.ts b/source/renderer/app/stores/HardwareWalletsStore.ts index a04be6b82c..24b5c567fb 100644 --- a/source/renderer/app/stores/HardwareWalletsStore.ts +++ b/source/renderer/app/stores/HardwareWalletsStore.ts @@ -38,28 +38,28 @@ import { waitForLedgerDevicesToConnectChannel, } from '../ipc/getHardwareWalletChannel'; import { - prepareLedgerInput, - prepareLedgerOutput, - prepareTxAux, - prepareBody, - prepareLedgerCertificate, - prepareLedgerWithdrawal, - CachedDeriveXpubFactory, - ShelleyTxWitnessShelley, - ShelleyTxInputFromUtxo, - ShelleyTxOutput, - ShelleyTxCert, - ShelleyTxWithdrawal, - cborizeTxAuxiliaryVotingData, - prepareLedgerAuxiliaryData, - CATALYST_VOTING_REGISTRATION_TYPE, + toLedgerInput, + toLedgerOutput, + toLedgerCertificate, + toLedgerWithdrawal, + toLedgerAuxiliaryData, } from '../utils/shelleyLedger'; import { - prepareTrezorInput, - prepareTrezorOutput, - prepareTrezorCertificate, - prepareTrezorWithdrawal, - prepareTrezorAuxiliaryData, + toTxInput, + toTxOutput, + toTxCertificate, + toTxWithdrawal, + toTxWitness, + toTxBody, + toTxVotingAux, + getTxCBOR, +} from '../utils/dataSerialization'; +import { + toTrezorInput, + toTrezorOutput, + toTrezorCertificate, + toTrezorWithdrawal, + toTrezorAuxiliaryData, TrezorTransactionSigningMode, } from '../utils/shelleyTrezor'; import { @@ -73,7 +73,10 @@ import { formattedAmountToLovelace } from '../utils/formatters'; import { TransactionStates } from '../domains/WalletTransaction'; import { CERTIFICATE_TYPE, + CATALYST_VOTING_REGISTRATION_TYPE, getParamsFromPath, + CachedDeriveXpubFactory, + deriveXpub, } from '../utils/hardwareWalletUtils'; import type { HwDeviceStatus } from '../domains/Wallet'; import type { @@ -260,7 +263,7 @@ export default class HardwareWalletsStore extends Store { @observable transportDevice: TransportDevice | null | undefined = null; @observable - txBody: string | null | undefined = null; + signedTx: string | null | undefined = null; @observable isTransactionPending = false; @observable @@ -513,7 +516,7 @@ export default class HardwareWalletsStore extends Store { try { // @ts-ignore ts-migrate(1320) FIXME: Type of 'await' operand must either be a valid pro... Remove this comment to see the full error message const transaction = await this.sendMoneyRequest.execute({ - signedTransactionBlob: this.txBody, + signedTransactionBlob: this.signedTx, }); if (!isDelegationTransaction) { @@ -543,7 +546,7 @@ export default class HardwareWalletsStore extends Store { } catch (e) { this.setTransactionPendingState(false); runInAction('HardwareWalletsStore:: reset Transaction verifying', () => { - this.txBody = null; + this.signedTx = null; this.activeDevicePath = null; this.unfinishedWalletTxSigning = null; this.votingData = null; @@ -2199,29 +2202,30 @@ export default class HardwareWalletsStore extends Store { 'chainCodeHex', ]); const xpubHex = `${publicKeyHex}${chainCodeHex}`; - const unsignedTxInputs = []; - const inputsData = map(inputs, (input) => { - const shelleyTxInput = ShelleyTxInputFromUtxo(input); - unsignedTxInputs.push(shelleyTxInput); - return prepareTrezorInput(input); + const txInputs = []; + const txOutputs = []; + + const trezorInputs = map(inputs, (input) => { + const txInput = toTxInput(input); + txInputs.push(txInput); + return toTrezorInput(input); }); - const unsignedTxOutputs = []; - const outputsData = []; + const trezorOutputs = []; for (const output of outputs) { const { address_style: addressStyle, } = await this.stores.addresses._inspectAddress({ addressId: output.address, }); - const shelleyTxOutput = ShelleyTxOutput(output, addressStyle); - unsignedTxOutputs.push(shelleyTxOutput); - const ledgerOutput = prepareTrezorOutput(output); - outputsData.push(ledgerOutput); + const txOutput = toTxOutput(output, addressStyle); + txOutputs.push(txOutput); + const ledgerOutput = toTrezorOutput(output); + trezorOutputs.push(ledgerOutput); } // Construct certificates - const unsignedTxCerts = []; + const txCertificates = []; const _certificatesData = map(certificates, async (certificate) => { const accountAddress = await this._getRewardAccountAddress( @@ -2229,26 +2233,26 @@ export default class HardwareWalletsStore extends Store { certificate.rewardAccountPath ); - const shelleyTxCert = ShelleyTxCert({ + const txCertificate = toTxCertificate({ accountAddress, pool: certificate.pool, // @ts-ignore ts-migrate(2322) FIXME: Type 'number' is not assignable to type 'string'. type: CERTIFICATE_TYPE[certificate.certificateType], }); - unsignedTxCerts.push(shelleyTxCert); - return prepareTrezorCertificate(certificate); + txCertificates.push(txCertificate); + return toTrezorCertificate(certificate); }); - const certificatesData = await Promise.all(_certificatesData); + const trezorCertificates = await Promise.all(_certificatesData); // Construct Withdrawals const _withdrawalsData = map(withdrawals, async (withdrawal) => - prepareTrezorWithdrawal(withdrawal) + toTrezorWithdrawal(withdrawal) ); - const withdrawalsData = await Promise.all(_withdrawalsData); + const trezorWithdrawals = await Promise.all(_withdrawalsData); let unsignedTxAuxiliaryData = null; - let auxiliaryData = null; + let trezorAuxiliaryData = null; if (this.votingData) { const { address, stakeKey, votingKey, nonce } = this.votingData; @@ -2263,7 +2267,7 @@ export default class HardwareWalletsStore extends Store { type: CATALYST_VOTING_REGISTRATION_TYPE, votingPubKey: votingKey, }; - auxiliaryData = prepareTrezorAuxiliaryData({ + trezorAuxiliaryData = toTrezorAuxiliaryData({ address, votingKey, nonce: nonce.toString(), @@ -2321,16 +2325,16 @@ export default class HardwareWalletsStore extends Store { try { const signedTransaction = await signTransactionTrezorChannel.request({ - inputs: inputsData, - outputs: outputsData, + inputs: trezorInputs, + outputs: trezorOutputs, fee: fee.toString(), ttl: ttl.toString(), networkId: hardwareWalletsNetworkConfig.networkId, protocolMagic: hardwareWalletsNetworkConfig.protocolMagic, - certificates: certificatesData, - withdrawals: withdrawalsData, + certificates: trezorCertificates, + withdrawals: trezorWithdrawals, signingMode: TrezorTransactionSigningMode.ORDINARY_TRANSACTION, - auxiliaryData, + auxiliaryData: trezorAuxiliaryData, }); if (signedTransaction && !signedTransaction.success) { @@ -2344,7 +2348,7 @@ export default class HardwareWalletsStore extends Store { runInAction( 'HardwareWalletsStore:: transaction successfully signed', () => { - this.txBody = serializedTx; + this.signedTx = serializedTx; this.hwDeviceStatus = HwDeviceStatuses.VERIFYING_TRANSACTION_SUCCEEDED; } @@ -2352,16 +2356,16 @@ export default class HardwareWalletsStore extends Store { return; } - const unsignedTxWithdrawals = - withdrawals.length > 0 ? ShelleyTxWithdrawal(withdrawals) : null; + const txWithdrawals = + withdrawals.length > 0 ? toTxWithdrawal(withdrawals) : null; // Prepare unsigned transaction structure for serialzation - let txAuxData = { - txInputs: unsignedTxInputs, - txOutputs: unsignedTxOutputs, + let txPartials = { + txInputs, + txOutputs, fee, ttl, - certificates: unsignedTxCerts, - withdrawals: unsignedTxWithdrawals, + certificates: txCertificates, + withdrawals: txWithdrawals, }; let txAuxiliaryData = null; const auxiliaryDataSupplement = get(signedTransaction, [ @@ -2370,19 +2374,19 @@ export default class HardwareWalletsStore extends Store { ]); if (unsignedTxAuxiliaryData && auxiliaryDataSupplement) { - txAuxData = { - ...txAuxData, + txPartials = { + ...txPartials, // @ts-ignore ts-migrate(2322) FIXME: Type '{ txAuxiliaryData: any; txAuxiliaryDataHash:... Remove this comment to see the full error message txAuxiliaryData: unsignedTxAuxiliaryData, txAuxiliaryDataHash: auxiliaryDataSupplement.auxiliaryDataHash, }; - txAuxiliaryData = cborizeTxAuxiliaryVotingData( + txAuxiliaryData = toTxVotingAux( unsignedTxAuxiliaryData, auxiliaryDataSupplement.catalystSignature ); } - const unsignedTx = prepareTxAux(txAuxData); + const txBody = toTxBody(txPartials); const witnesses = get(signedTransaction, ['payload', 'witnesses'], []); const signedWitnesses = await this._signWitnesses(witnesses, xpubHex); const txWitnesses = new Map(); @@ -2392,14 +2396,10 @@ export default class HardwareWalletsStore extends Store { } // Prepare serialized transaction with unsigned data and signed witnesses - const txBody = await prepareBody( - unsignedTx, - txWitnesses, - txAuxiliaryData - ); + const signedTx = await getTxCBOR(txBody, txWitnesses, txAuxiliaryData); runInAction('HardwareWalletsStore:: set Transaction verified', () => { this.hwDeviceStatus = HwDeviceStatuses.VERIFYING_TRANSACTION_SUCCEEDED; - this.txBody = txBody; + this.signedTx = signedTx; this.activeDevicePath = null; }); } catch (error) { @@ -2438,6 +2438,7 @@ export default class HardwareWalletsStore extends Store { return signedWitnesses; }; + ShelleyWitness = async ( witness: TrezorWitness | Witness, xpubHex: string @@ -2454,7 +2455,7 @@ export default class HardwareWalletsStore extends Store { // @ts-ignore ts-migrate(2339) FIXME: Property 'path' does not exist on type 'TrezorWitn... Remove this comment to see the full error message } else if (witness.path && witness.witnessSignatureHex) { // @ts-ignore ts-migrate(2339) FIXME: Property 'path' does not exist on type 'TrezorWitn... Remove this comment to see the full error message - const xpub = await this._deriveXpub(witness.path, xpubHex); + const xpub = await deriveXpub(witness.path, xpubHex); publicKey = xpub.slice(0, 32); // @ts-ignore ts-migrate(2339) FIXME: Property 'witnessSignatureHex' does not exist on t... Remove this comment to see the full error message witnessSignatureHex = witness.witnessSignatureHex; @@ -2462,14 +2463,12 @@ export default class HardwareWalletsStore extends Store { if (witnessSignatureHex && publicKey) { const signature = Buffer.from(witnessSignatureHex, 'hex'); - return ShelleyTxWitnessShelley(publicKey, signature); + return toTxWitness(publicKey, signature); } return null; }; - _deriveXpub = CachedDeriveXpubFactory(async (xpubHex) => { - return Buffer.from(xpubHex, 'hex'); - }); + _getRewardAccountAddress = async (walletId: string, path: Array) => { const pathParams = getParamsFromPath(path); // @ts-ignore ts-migrate(1320) FIXME: Type of 'await' operand must either be a valid pro... Remove this comment to see the full error message @@ -2522,55 +2521,52 @@ export default class HardwareWalletsStore extends Store { 'chainCodeHex', ]); const xpubHex = `${publicKeyHex}${chainCodeHex}`; - const unsignedTxInputs = []; - const inputsData = map(inputs, (input) => { - const shelleyTxInput = ShelleyTxInputFromUtxo(input); - unsignedTxInputs.push(shelleyTxInput); - return prepareLedgerInput(input); + const txInputs = []; + const txOutputs = []; + const txCertificates = []; + const txWithdrawals = + withdrawals.length > 0 ? toTxWithdrawal(withdrawals) : null; + + const ledgerInputs = map(inputs, (input) => { + const txInput = toTxInput(input); + txInputs.push(txInput); + return toLedgerInput(input); }); - const unsignedTxOutputs = []; - const outputsData = []; + const ledgerOutputs = []; for (const output of outputs) { const { address_style: addressStyle, } = await this.stores.addresses._inspectAddress({ addressId: output.address, }); - const shelleyTxOutput = ShelleyTxOutput(output, addressStyle); - unsignedTxOutputs.push(shelleyTxOutput); - const ledgerOutput = prepareLedgerOutput(output, addressStyle); - outputsData.push(ledgerOutput); + const txOutput = toTxOutput(output, addressStyle); + txOutputs.push(txOutput); + const ledgerOutput = toLedgerOutput(output, addressStyle); + ledgerOutputs.push(ledgerOutput); } - // Construct certificates - const unsignedTxCerts = []; - const _certificatesData = map(certificates, async (certificate) => { const accountAddress = await this._getRewardAccountAddress( walletId, certificate.rewardAccountPath ); - const shelleyTxCert = ShelleyTxCert({ + const txCertificate = toTxCertificate({ accountAddress, pool: certificate.pool, // @ts-ignore ts-migrate(2322) FIXME: Type 'number' is not assignable to type 'string'. type: CERTIFICATE_TYPE[certificate.certificateType], }); - unsignedTxCerts.push(shelleyTxCert); - return prepareLedgerCertificate(certificate); + txCertificates.push(txCertificate); + return toLedgerCertificate(certificate); }); + const ledgerCertificates = await Promise.all(_certificatesData); - const certificatesData = await Promise.all(_certificatesData); - - // Construct Withdrawals const _withdrawalsData = map(withdrawals, async (withdrawal) => - prepareLedgerWithdrawal(withdrawal) + toLedgerWithdrawal(withdrawal) ); - - const withdrawalsData = await Promise.all(_withdrawalsData); + const ledgerWithdrawals = await Promise.all(_withdrawalsData); const fee = formattedAmountToLovelace(flatFee.toString()); - const ttl = this._getTtl(); let unsignedTxAuxiliaryData = null; @@ -2590,38 +2586,37 @@ export default class HardwareWalletsStore extends Store { }; } - const auxiliaryData = unsignedTxAuxiliaryData - ? prepareLedgerAuxiliaryData(unsignedTxAuxiliaryData) + const ledgerAuxiliaryData = unsignedTxAuxiliaryData + ? toLedgerAuxiliaryData(unsignedTxAuxiliaryData) : null; try { const signedTransaction = await signTransactionLedgerChannel.request({ - inputs: inputsData, - outputs: outputsData, + inputs: ledgerInputs, + outputs: ledgerOutputs, fee: fee.toString(), ttl: ttl.toString(), validityIntervalStartStr: null, networkId: hardwareWalletsNetworkConfig.networkId, protocolMagic: hardwareWalletsNetworkConfig.protocolMagic, // @ts-ignore ts-migrate(2322) FIXME: Type '{ type: number; params: { stakeCredential: {... Remove this comment to see the full error message - certificates: certificatesData, + certificates: ledgerCertificates, // @ts-ignore ts-migrate(2322) FIXME: Type '{ stakeCredential: { type: StakeCredentialPa... Remove this comment to see the full error message - withdrawals: withdrawalsData, + withdrawals: ledgerWithdrawals, signingMode: TransactionSigningMode.ORDINARY_TRANSACTION, additionalWitnessPaths: [], - auxiliaryData, + auxiliaryData: ledgerAuxiliaryData, devicePath, }); - const unsignedTxWithdrawals = - withdrawals.length > 0 ? ShelleyTxWithdrawal(withdrawals) : null; + // Prepare unsigned transaction structure for serialzation - let txAuxData = { - txInputs: unsignedTxInputs, - txOutputs: unsignedTxOutputs, + let txPartials = { + txInputs, + txOutputs, fee, ttl, - certificates: unsignedTxCerts, - withdrawals: unsignedTxWithdrawals, + certificates: txCertificates, + withdrawals: txWithdrawals, }; let txAuxiliaryData = null; @@ -2630,21 +2625,21 @@ export default class HardwareWalletsStore extends Store { signedTransaction && signedTransaction.auxiliaryDataSupplement ) { - txAuxData = { - ...txAuxData, + txPartials = { + ...txPartials, // @ts-ignore ts-migrate(2322) FIXME: Type '{ txAuxiliaryData: any; txAuxiliaryDataHash:... Remove this comment to see the full error message txAuxiliaryData: unsignedTxAuxiliaryData, txAuxiliaryDataHash: signedTransaction.auxiliaryDataSupplement.auxiliaryDataHashHex, }; - txAuxiliaryData = cborizeTxAuxiliaryVotingData( + txAuxiliaryData = toTxVotingAux( unsignedTxAuxiliaryData, signedTransaction.auxiliaryDataSupplement .cip36VoteRegistrationSignatureHex ); } - const unsignedTx = prepareTxAux(txAuxData); + const txBody = toTxBody(txPartials); const witnesses = get(signedTransaction, 'witnesses', []); const signedWitnesses = await this._signWitnesses(witnesses, xpubHex); const txWitnesses = new Map(); @@ -2654,14 +2649,10 @@ export default class HardwareWalletsStore extends Store { } // Prepare serialized transaction with unsigned data and signed witnesses - const txBody = await prepareBody( - unsignedTx, - txWitnesses, - txAuxiliaryData - ); + const signedTx = await getTxCBOR(txBody, txWitnesses, txAuxiliaryData); runInAction('HardwareWalletsStore:: set Transaction verified', () => { this.hwDeviceStatus = HwDeviceStatuses.VERIFYING_TRANSACTION_SUCCEEDED; - this.txBody = txBody; + this.signedTx = signedTx; this.activeDevicePath = null; }); } catch (error) { @@ -2863,7 +2854,7 @@ export default class HardwareWalletsStore extends Store { logger.debug('[HW-DEBUG] unfinishedWalletTxSigning UNSET'); runInAction('HardwareWalletsStore:: reset Transaction verifying', () => { this.hwDeviceStatus = HwDeviceStatuses.READY; - this.txBody = null; + this.signedTx = null; this.activeDevicePath = null; this.unfinishedWalletTxSigning = null; this.activeDelegationWalletId = null; diff --git a/source/renderer/app/utils/dataSerialization.ts b/source/renderer/app/utils/dataSerialization.ts new file mode 100644 index 0000000000..ac110b5981 --- /dev/null +++ b/source/renderer/app/utils/dataSerialization.ts @@ -0,0 +1,400 @@ +import { map } from 'lodash'; +import blakejs from 'blakejs'; +import { encode } from 'borc'; +import { utils } from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import { base58_decode } from '@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address'; +import { AddressStyles } from '../domains/WalletAddress'; +import { + derivationPathToLedgerPath, + groupTokensByPolicyId, +} from './hardwareWalletUtils'; + +import type { AddressStyle } from '../api/addresses/types'; +import type { + CoinSelectionInput, + CoinSelectionWithdrawal, + CoinSelectionOutput, + CoinSelectionAssetsType, +} from '../api/transactions/types'; + +import type { + BIP32Path, + Certificate, +} from '../../../common/types/hardware-wallets.types'; + +export type RewardDestinationAddressType = { + address: { + id: string; + spendingPath: string; + }; + stakingPath: BIP32Path; +}; + +export type TxAuxiliaryData = { + nonce: number; + rewardDestinationAddress: RewardDestinationAddressType; + stakePubKey: string; + type: 'CATALYST_VOTING'; + votingPubKey: string; +}; + +export type TxInputType = { + coins: number; + address: string; + txid: string; + outputNo: number; + encodeCBOR: (...args: Array) => any; +}; + +export type TxOutputType = { + address: string; + coins: number | [number, Map>]; + isChange: boolean; + spendingPath: BIP32Path | null | undefined; + stakingPath: BIP32Path | null | undefined; + encodeCBOR: (...args: Array) => any; +}; + +export type TxFeeType = { + fee: number; + encodeCBOR: (...args: Array) => any; +}; +export type TxTtlType = { + ttl: number; + encodeCBOR: (...args: Array) => any; +}; + +export type TxWitnessType = { + publicKey: string; + signature: Buffer; + encodeCBOR: (...args: Array) => any; +}; + +export type TxWithdrawalsType = { + withdrawals: Array; + encodeCBOR: (...args: Array) => any; +}; + +export type TxBodyType = { + getId: (...args: Array) => any; + inputs: Array; + outputs: Array; + fee: TxFeeType; + ttl: TxTtlType; + certs: Array; + withdrawals: TxWithdrawalsType | null | undefined; + encodeCBOR: (...args: Array) => any; +}; + +export type CborizedVotingRegistrationMetadata = [ + Map>, + [] +]; + +export const toTxInput = (utxoInput: CoinSelectionInput) => { + const { address, amount, id, index } = utxoInput; + const coins = amount.quantity; + const outputNo = index; + const txHash = Buffer.from(id, 'hex'); + + function encodeCBOR(encoder: any) { + return encoder.pushAny([txHash, outputNo]); + } + + return { + txid: id, + coins, + address, + outputNo, + encodeCBOR, + }; +}; + +export function toTxOutput( + output: CoinSelectionOutput, + addressStyle: AddressStyle +) { + const { address, amount, derivationPath, assets } = output; + const adaCoinQuantity = amount.quantity; + const coins = + assets && assets.length > 0 + ? [adaCoinQuantity, toTxOutputAssets(assets)] + : adaCoinQuantity; + + function encodeCBOR(encoder: any) { + const addressBuff = + addressStyle === AddressStyles.ADDRESS_SHELLEY + ? utils.bech32_decodeAddress(address) + : base58_decode(address); + return encoder.pushAny([addressBuff, coins]); + } + const isChange = derivationPath !== null; + return { + address, + coins, + isChange, + spendingPath: isChange ? derivationPathToLedgerPath(derivationPath) : null, + stakingPath: isChange ? [2147485500, 2147485463, 2147483648, 2, 0] : null, + encodeCBOR, + }; +} + +export const toTxFee = (fee: number) => { + function encodeCBOR(encoder: any) { + return encoder.pushAny(fee); + } + + return { + fee, + encodeCBOR, + }; +}; +export const toTxTtl = (ttl: number) => { + function encodeCBOR(encoder: any) { + return encoder.pushAny(ttl); + } + + return { + ttl, + encodeCBOR, + }; +}; + +export const toTxOutputAssets = (assets: CoinSelectionAssetsType) => { + const policyIdMap = new Map>(); + const tokenObject = groupTokensByPolicyId(assets); + Object.entries(tokenObject).forEach(([policyId, tokens]) => { + const assetMap = new Map(); + + // @ts-ignore ts-migrate(2769) FIXME: No overload matches this call. + map(tokens, (token) => { + // @ts-ignore ts-migrate(2339) FIXME: Property 'assetName' does not exist on type 'unkno... Remove this comment to see the full error message + assetMap.set(Buffer.from(token.assetName, 'hex'), token.quantity); + }); + + policyIdMap.set(Buffer.from(policyId, 'hex'), assetMap); + }); + return policyIdMap; +}; + +export function toTxCertificate(cert: { + type: string; + accountAddress: string; + pool: string | null | undefined; +}) { + const { type, accountAddress, pool } = cert; + let hash; + let poolHash; + + if (pool) { + poolHash = utils.buf_to_hex(utils.bech32_decodeAddress(pool)); + hash = Buffer.from(poolHash, 'hex'); + } + + function encodeCBOR(encoder: any) { + const accountAddressHash = utils + .bech32_decodeAddress(accountAddress) + .slice(1); + const account = [0, accountAddressHash]; + const encodedCertsTypes = { + [0]: [type, account], + [1]: [type, account], + [2]: [type, account, hash], + }; + return encoder.pushAny(encodedCertsTypes[type]); + } + + return { + address: accountAddress, + type, + accountAddress, + poolHash: poolHash || null, + encodeCBOR, + }; +} + +export const toTxWithdrawal = (withdrawals: Array) => { + function encodeCBOR(encoder: any) { + const withdrawalMap = new Map(); + + map(withdrawals, (withdrawal) => { + const rewardAccount = utils.bech32_decodeAddress(withdrawal.stakeAddress); + const coin = withdrawal.amount.quantity; + withdrawalMap.set(rewardAccount, coin); + }); + + return encoder.pushAny(withdrawalMap); + } + + return { + withdrawals, + encodeCBOR, + }; +}; + +export const toTxWitness = (publicKey: Buffer, signature: Buffer) => { + function encodeCBOR(encoder: any) { + return encoder.pushAny([publicKey, signature]); + } + + return { + publicKey, + signature, + encodeCBOR, + }; +}; + +export const toTxVotingAux = ( + txAuxiliaryData: TxAuxiliaryData, + signatureHex: string +) => [ + // @ts-ignore ts-migrate(2769) FIXME: No overload matches this call. + new Map>([ + cborizeTxVotingRegistration(txAuxiliaryData), + [ + 61285, + new Map([[1, Buffer.from(signatureHex, 'hex')]]), + ], + ]), + [], +]; + +export const TxBody = ( + inputs: Array, + outputs: Array, + fee: TxFeeType, + ttl: TxTtlType, + certs: Array, + withdrawals: TxWithdrawalsType | null | undefined, + auxiliaryData: TxAuxiliaryData | null | undefined, + auxiliaryDataHash: string | null | undefined +) => { + const blake2b = (data) => blakejs.blake2b(data, null, 32); + + function getId() { + return blake2b( + encode( + TxBody( + inputs, + outputs, + fee, + ttl, + certs, + withdrawals, + auxiliaryData, + auxiliaryDataHash + ) + ) // 32 + ).toString('hex'); + } + + function encodeCBOR(encoder: any) { + const txMap = new Map(); + txMap.set(0, inputs); + txMap.set(1, outputs); + txMap.set(2, fee); + txMap.set(3, ttl); + if (certs && certs.length) txMap.set(4, certs); + if (withdrawals) txMap.set(5, withdrawals); + if (auxiliaryDataHash) txMap.set(7, Buffer.from(auxiliaryDataHash, 'hex')); + return encoder.pushAny(txMap); + } + + return { + getId, + inputs, + outputs, + fee, + ttl, + certs, + withdrawals, + auxiliaryData, + auxiliaryDataHash, + encodeCBOR, + }; +}; + +export const cborizeTxVotingRegistration = ({ + votingPubKey, + stakePubKey, + rewardDestinationAddress, + nonce, +}: TxAuxiliaryData) => { + return [ + 61284, + new Map([ + [1, Buffer.from(votingPubKey, 'hex')], + [2, Buffer.from(stakePubKey, 'hex')], + [3, utils.bech32_decodeAddress(rewardDestinationAddress.address.id)], + [4, Number(nonce)], + ]), + ]; +}; + +export const toTxBody = ({ + txInputs, + txOutputs, + fee, + ttl, + certificates, + withdrawals, + txAuxiliaryData, + txAuxiliaryDataHash, +}: { + txInputs: Array; + txOutputs: Array; + txAuxiliaryData?: TxAuxiliaryData; + txAuxiliaryDataHash?: string; + fee: number; + ttl: number; + certificates: Array; + withdrawals: TxWithdrawalsType | null | undefined; +}) => { + const txFee = toTxFee(fee); + const txTtl = toTxTtl(ttl); + const txCerts = certificates; + const txWithdrawals = withdrawals; + return TxBody( + txInputs, + txOutputs, + txFee, + txTtl, + txCerts, + txWithdrawals, + txAuxiliaryData, + txAuxiliaryDataHash + ); +}; + +// CDDL +// transaction = +// [ transaction_body +// , transaction_witness_set +// , bool +// , auxiliary_data / null +// ] +export const getTxCBOR = ( + txBody: TxBodyType, + txWitnesses: any, + txAuxiliaryData: CborizedVotingRegistrationMetadata | null | undefined +) => { + function getId() { + return txBody.getId(); + } + // The isValid flag was added in Alonzo era (onwards), mary era transactions only have three fields. + // Since we are using '/v2/proxy/transactions' to send tx this flag has to be enabled to support Babbage tx format. + const isValid = true; + function encodeCBOR(encoder: any) { + return encoder.pushAny([txBody, txWitnesses, isValid, txAuxiliaryData]); + } + + const tx = { + getId, + txWitnesses, + txBody, + txAuxiliaryData, + isValid, + encodeCBOR, + }; + return encode(tx).toString('hex'); +}; diff --git a/source/renderer/app/utils/hardwareWalletUtils.ts b/source/renderer/app/utils/hardwareWalletUtils.ts index d5fe96abf5..c741fcdf53 100644 --- a/source/renderer/app/utils/hardwareWalletUtils.ts +++ b/source/renderer/app/utils/hardwareWalletUtils.ts @@ -1,6 +1,9 @@ import _ from 'lodash'; import { bech32 } from 'bech32'; import { str_to_path } from '@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address'; +import { HexString } from '@cardano-foundation/ledgerjs-hw-app-cardano/dist/types/internal'; +import { utils } from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import { deriveXpubChannel } from '../ipc/getHardwareWalletChannel'; import { HARDENED } from '../config/hardwareWalletsConfig'; // Types import type { CoinSelectionAssetsType } from '../api/transactions/types'; @@ -47,6 +50,14 @@ const receiverAddressTypes: Set = new Set([ 7, 8, ]); +export const CATALYST_VOTING_REGISTRATION_TYPE = 'CATALYST_VOTING'; +export const HARDENED_THRESHOLD = 0x80000000; +export const derivationScheme = { + type: 'v2', + ed25519Mode: 2, + keyfileVersion: '2.0.0', +}; + export const isReceiverAddressType = (addressType: AddressType) => receiverAddressTypes.has(addressType); // [1852H, 1815H, 0H] => m/1852'/1815'/0' @@ -158,3 +169,61 @@ export const groupTokensByPolicyId = (assets: CoinSelectionAssetsType) => { return groupedAssets; }; + +export const indexIsHardened = (index: number) => { + return index >= HARDENED_THRESHOLD; +}; + +export const CachedDeriveXpubFactory = ( + deriveXpubHardenedFn: (...args: Array) => any +) => { + const derivedXpubs = {}; + let xpubMemo; + + const deriveXpub = async ( + absDerivationPath: Array, + xpubHex: string | null | undefined + ) => { + if (xpubHex) xpubMemo = xpubHex; + const memoKey = JSON.stringify(absDerivationPath); + let derivedXpubsMemo = await derivedXpubs[memoKey]; + + if (!derivedXpubsMemo) { + const deriveHardened = + absDerivationPath.length === 0 || + indexIsHardened(absDerivationPath.slice(-1)[0]); + derivedXpubsMemo = deriveHardened + ? await deriveXpubHardenedFn(xpubMemo) + : await deriveXpubNonhardenedFn(absDerivationPath); + } + + /* + * the derivedXpubs map stores promises instead of direct results + * to deal with concurrent requests to derive the same xpub + */ + return derivedXpubsMemo; + }; + + const deriveXpubNonhardenedFn = async (derivationPath) => { + const lastIndex = derivationPath.slice(-1)[0]; + const parentXpub = await deriveXpub(derivationPath.slice(0, -1), null); + + try { + const parentXpubHex = utils.buf_to_hex(parentXpub); + const derivedXpub = await deriveXpubChannel.request({ + parentXpubHex, + lastIndex, + derivationScheme: derivationScheme.ed25519Mode, + }); + return utils.hex_to_buf(derivedXpub as HexString); + } catch (e) { + throw e; + } + }; + + return deriveXpub; +}; + +export const deriveXpub = CachedDeriveXpubFactory(async (xpubHex) => { + return Buffer.from(xpubHex, 'hex'); +}); diff --git a/source/renderer/app/utils/shelleyLedger.ts b/source/renderer/app/utils/shelleyLedger.ts index 74d9554184..57115ce283 100644 --- a/source/renderer/app/utils/shelleyLedger.ts +++ b/source/renderer/app/utils/shelleyLedger.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { utils, TxOutputDestinationType, @@ -10,18 +11,14 @@ import { str_to_path, base58_decode, } from '@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address'; -import { HexString } from '@cardano-foundation/ledgerjs-hw-app-cardano/dist/types/internal'; -import { encode } from 'borc'; -import blakejs from 'blakejs'; -import _ from 'lodash'; import { derivationPathToLedgerPath, CERTIFICATE_TYPE, groupTokensByPolicyId, + CATALYST_VOTING_REGISTRATION_TYPE, } from './hardwareWalletUtils'; -import { deriveXpubChannel } from '../ipc/getHardwareWalletChannel'; import { AddressStyles } from '../domains/WalletAddress'; -// Types +import type { AddressStyle } from '../api/addresses/types'; import type { CoinSelectionInput, CoinSelectionOutput, @@ -29,127 +26,9 @@ import type { CoinSelectionWithdrawal, CoinSelectionAssetsType, } from '../api/transactions/types'; -import type { - BIP32Path, - Certificate, -} from '../../../common/types/hardware-wallets.types'; -import type { AddressStyle } from '../api/addresses/types'; - -export const CATALYST_VOTING_REGISTRATION_TYPE = 'CATALYST_VOTING'; -export type ShelleyTxInputType = { - coins: number; - address: string; - txid: string; - outputNo: number; - encodeCBOR: (...args: Array) => any; -}; -export type ShelleyTxOutputType = { - address: string; - coins: number | [number, Map>]; - isChange: boolean; - spendingPath: BIP32Path | null | undefined; - stakingPath: BIP32Path | null | undefined; - encodeCBOR: (...args: Array) => any; -}; -export type ShelleyFeeType = { - fee: number; - encodeCBOR: (...args: Array) => any; -}; -export type ShelleyTtlType = { - ttl: number; - encodeCBOR: (...args: Array) => any; -}; -export type ShelleyTxWitnessType = { - publicKey: string; - signature: Buffer; - encodeCBOR: (...args: Array) => any; -}; -export type ShelleyTxAuxType = { - getId: (...args: Array) => any; - inputs: Array; - outputs: Array; - fee: ShelleyFeeType; - ttl: ShelleyTtlType; - certs: Array; - withdrawals: ShelleyTxWithdrawalsType | null | undefined; - encodeCBOR: (...args: Array) => any; -}; -export type ShelleyTxWithdrawalsType = { - withdrawals: Array; - encodeCBOR: (...args: Array) => any; -}; -export type TxAuxiliaryData = { - nonce: number; - rewardDestinationAddress: RewardDestinationAddressType; - stakePubKey: string; - type: 'CATALYST_VOTING'; - votingPubKey: string; -}; -export type RewardDestinationAddressType = { - address: { - id: string; - spendingPath: string; - }; - // type of "address.id" - stakingPath: BIP32Path; -}; -// Constants -export const HARDENED_THRESHOLD = 0x80000000; -export const derivationScheme = { - type: 'v2', - ed25519Mode: 2, - keyfileVersion: '2.0.0', -}; -// Constructors -export const ShelleyTxWitnessShelley = ( - publicKey: Buffer, - signature: Buffer -) => { - function encodeCBOR(encoder: any) { - return encoder.pushAny([publicKey, signature]); - } +import { TxAuxiliaryData } from './dataSerialization'; - return { - publicKey, - signature, - encodeCBOR, - }; -}; -export const ShelleyTxInputFromUtxo = (utxoInput: CoinSelectionInput) => { - const { address, amount, id, index } = utxoInput; - const coins = amount.quantity; - const outputNo = index; - const txHash = Buffer.from(id, 'hex'); - - function encodeCBOR(encoder: any) { - return encoder.pushAny([txHash, outputNo]); - } - - return { - txid: id, - coins, - address, - outputNo, - encodeCBOR, - }; -}; -export const ShelleyTxOutputAssets = (assets: CoinSelectionAssetsType) => { - const policyIdMap = new Map>(); - const tokenObject = groupTokensByPolicyId(assets); - Object.entries(tokenObject).forEach(([policyId, tokens]) => { - const assetMap = new Map(); - - // @ts-ignore ts-migrate(2769) FIXME: No overload matches this call. - _.map(tokens, (token) => { - // @ts-ignore ts-migrate(2339) FIXME: Property 'assetName' does not exist on type 'unkno... Remove this comment to see the full error message - assetMap.set(Buffer.from(token.assetName, 'hex'), token.quantity); - }); - - policyIdMap.set(Buffer.from(policyId, 'hex'), assetMap); - }); - return policyIdMap; -}; -export const prepareTokenBundle = (assets: CoinSelectionAssetsType) => { +export const toTokenBundle = (assets: CoinSelectionAssetsType) => { const tokenObject = groupTokensByPolicyId(assets); const tokenObjectEntries = Object.entries(tokenObject); @@ -167,91 +46,8 @@ export const prepareTokenBundle = (assets: CoinSelectionAssetsType) => { return tokenBundle; }; -export function ShelleyTxOutput( - output: CoinSelectionOutput, - addressStyle: AddressStyle -) { - const { address, amount, derivationPath, assets } = output; - const adaCoinQuantity = amount.quantity; - const coins = - assets && assets.length > 0 - ? [adaCoinQuantity, ShelleyTxOutputAssets(assets)] - : adaCoinQuantity; - - function encodeCBOR(encoder: any) { - const addressBuff = - addressStyle === AddressStyles.ADDRESS_SHELLEY - ? utils.bech32_decodeAddress(address) - : base58_decode(address); - return encoder.pushAny([addressBuff, coins]); - } - const isChange = derivationPath !== null; - return { - address, - coins, - isChange, - spendingPath: isChange ? derivationPathToLedgerPath(derivationPath) : null, - stakingPath: isChange ? [2147485500, 2147485463, 2147483648, 2, 0] : null, - encodeCBOR, - }; -} -export function ShelleyTxCert(cert: { - type: string; - accountAddress: string; - pool: string | null | undefined; -}) { - const { type, accountAddress, pool } = cert; - let hash; - let poolHash; - - if (pool) { - poolHash = utils.buf_to_hex(utils.bech32_decodeAddress(pool)); - hash = Buffer.from(poolHash, 'hex'); - } - - function encodeCBOR(encoder: any) { - const accountAddressHash = utils - .bech32_decodeAddress(accountAddress) - .slice(1); - const account = [0, accountAddressHash]; - const encodedCertsTypes = { - [0]: [type, account], - [1]: [type, account], - [2]: [type, account, hash], - }; - return encoder.pushAny(encodedCertsTypes[type]); - } - - return { - address: accountAddress, - type, - accountAddress, - poolHash: poolHash || null, - encodeCBOR, - }; -} -export const ShelleyTxWithdrawal = ( - withdrawals: Array -) => { - function encodeCBOR(encoder: any) { - const withdrawalMap = new Map(); - - _.map(withdrawals, (withdrawal) => { - const rewardAccount = utils.bech32_decodeAddress(withdrawal.stakeAddress); - const coin = withdrawal.amount.quantity; - withdrawalMap.set(rewardAccount, coin); - }); - - return encoder.pushAny(withdrawalMap); - } - - return { - withdrawals, - encodeCBOR, - }; -}; -export const prepareLedgerCertificate = (cert: CoinSelectionCertificate) => { +export const toLedgerCertificate = (cert: CoinSelectionCertificate) => { return { type: CERTIFICATE_TYPE[cert.certificateType], params: { @@ -265,9 +61,8 @@ export const prepareLedgerCertificate = (cert: CoinSelectionCertificate) => { }, }; }; -export const prepareLedgerWithdrawal = ( - withdrawal: CoinSelectionWithdrawal -) => { + +export const toLedgerWithdrawal = (withdrawal: CoinSelectionWithdrawal) => { return { stakeCredential: { type: StakeCredentialParamsType.KEY_PATH, @@ -276,162 +71,16 @@ export const prepareLedgerWithdrawal = ( amount: withdrawal.amount.quantity.toString(), }; }; -export const ShelleyFee = (fee: number) => { - function encodeCBOR(encoder: any) { - return encoder.pushAny(fee); - } - - return { - fee, - encodeCBOR, - }; -}; -export const ShelleyTtl = (ttl: number) => { - function encodeCBOR(encoder: any) { - return encoder.pushAny(ttl); - } - - return { - ttl, - encodeCBOR, - }; -}; -export const ShelleyTxAux = ( - inputs: Array, - outputs: Array, - fee: ShelleyFeeType, - ttl: ShelleyTtlType, - certs: Array, - withdrawals: ShelleyTxWithdrawalsType | null | undefined, - auxiliaryData: TxAuxiliaryData | null | undefined, - auxiliaryDataHash: string | null | undefined -) => { - const blake2b = (data) => blakejs.blake2b(data, null, 32); - - function getId() { - return blake2b( - encode( - ShelleyTxAux( - inputs, - outputs, - fee, - ttl, - certs, - withdrawals, - auxiliaryData, - auxiliaryDataHash - ) - ) // 32 - ).toString('hex'); - } - - function encodeCBOR(encoder: any) { - const txMap = new Map(); - txMap.set(0, inputs); - txMap.set(1, outputs); - txMap.set(2, fee); - txMap.set(3, ttl); - if (certs && certs.length) txMap.set(4, certs); - if (withdrawals) txMap.set(5, withdrawals); - if (auxiliaryDataHash) txMap.set(7, Buffer.from(auxiliaryDataHash, 'hex')); - return encoder.pushAny(txMap); - } - - return { - getId, - inputs, - outputs, - fee, - ttl, - certs, - withdrawals, - auxiliaryData, - auxiliaryDataHash, - encodeCBOR, - }; -}; -export const ShelleySignedTransactionStructured = ( - txAux: ShelleyTxAuxType, - witnesses: Map, - txAuxiliaryData: CborizedVotingRegistrationMetadata | null | undefined -) => { - function getId() { - return txAux.getId(); - } - - function encodeCBOR(encoder: any) { - return encoder.pushAny([txAux, witnesses, txAuxiliaryData]); - } - return { - getId, - witnesses, - txAux, - txAuxiliaryData, - encodeCBOR, - }; -}; -export const CachedDeriveXpubFactory = ( - deriveXpubHardenedFn: (...args: Array) => any -) => { - const derivedXpubs = {}; - let xpubMemo; - - const deriveXpub = async ( - absDerivationPath: Array, - xpubHex: string | null | undefined - ) => { - if (xpubHex) xpubMemo = xpubHex; - const memoKey = JSON.stringify(absDerivationPath); - let derivedXpubsMemo = await derivedXpubs[memoKey]; - - if (!derivedXpubsMemo) { - const deriveHardened = - absDerivationPath.length === 0 || - indexIsHardened(absDerivationPath.slice(-1)[0]); - derivedXpubsMemo = deriveHardened - ? await deriveXpubHardenedFn(xpubMemo) - : await deriveXpubNonhardenedFn(absDerivationPath); - } - - /* - * the derivedXpubs map stores promises instead of direct results - * to deal with concurrent requests to derive the same xpub - */ - return derivedXpubsMemo; - }; - - const deriveXpubNonhardenedFn = async (derivationPath) => { - const lastIndex = derivationPath.slice(-1)[0]; - const parentXpub = await deriveXpub(derivationPath.slice(0, -1), null); - - try { - const parentXpubHex = utils.buf_to_hex(parentXpub); - const derivedXpub = await deriveXpubChannel.request({ - parentXpubHex, - lastIndex, - derivationScheme: derivationScheme.ed25519Mode, - }); - return utils.hex_to_buf(derivedXpub as HexString); - } catch (e) { - throw e; - } - }; - - return deriveXpub; -}; -// Helpers -export const indexIsHardened = (index: number) => { - return index >= HARDENED_THRESHOLD; -}; -export const prepareLedgerInput = (input: CoinSelectionInput) => { +export const toLedgerInput = (input: CoinSelectionInput) => { return { txHashHex: input.id, outputIndex: input.index, path: derivationPathToLedgerPath(input.derivationPath), }; }; -export const prepareLedgerOutput = ( + +export const toLedgerOutput = ( output: CoinSelectionOutput, addressStyle: AddressStyle ) => { @@ -439,7 +88,7 @@ export const prepareLedgerOutput = ( let tokenBundle = []; if (output.assets) { - tokenBundle = prepareTokenBundle(output.assets); + tokenBundle = toTokenBundle(output.assets); } if (isChange) { @@ -473,9 +122,8 @@ export const prepareLedgerOutput = ( tokenBundle, }; }; -export const prepareLedgerAuxiliaryData = ( - txAuxiliaryData: TxAuxiliaryData -) => { + +export const toLedgerAuxiliaryData = (txAuxiliaryData: TxAuxiliaryData) => { const { votingPubKey, rewardDestinationAddress, type } = txAuxiliaryData; if (type === CATALYST_VOTING_REGISTRATION_TYPE) { return { @@ -504,83 +152,3 @@ export const prepareLedgerAuxiliaryData = ( // Regular tx has no voting metadata return null; }; -export type CborizedVotingRegistrationMetadata = [ - Map>, - [] -]; -export const cborizeTxVotingRegistration = ({ - votingPubKey, - stakePubKey, - rewardDestinationAddress, - nonce, -}: TxAuxiliaryData) => { - return [ - 61284, - new Map([ - [1, Buffer.from(votingPubKey, 'hex')], - [2, Buffer.from(stakePubKey, 'hex')], - [3, utils.bech32_decodeAddress(rewardDestinationAddress.address.id)], - [4, Number(nonce)], - ]), - ]; -}; -export const cborizeTxAuxiliaryVotingData = ( - txAuxiliaryData: TxAuxiliaryData, - signatureHex: string -) => [ - // @ts-ignore ts-migrate(2769) FIXME: No overload matches this call. - new Map>([ - cborizeTxVotingRegistration(txAuxiliaryData), - [ - 61285, - new Map([[1, Buffer.from(signatureHex, 'hex')]]), - ], - ]), - [], -]; -export const prepareTxAux = ({ - txInputs, - txOutputs, - fee, - ttl, - certificates, - withdrawals, - txAuxiliaryData, - txAuxiliaryDataHash, -}: { - txInputs: Array; - txOutputs: Array; - txAuxiliaryData?: TxAuxiliaryData; - txAuxiliaryDataHash?: string; - fee: number; - ttl: number; - certificates: Array; - withdrawals: ShelleyTxWithdrawalsType | null | undefined; -}) => { - const txFee = ShelleyFee(fee); - const txTtl = ShelleyTtl(ttl); - const txCerts = certificates; - const txWithdrawals = withdrawals; - return ShelleyTxAux( - txInputs, - txOutputs, - txFee, - txTtl, - txCerts, - txWithdrawals, - txAuxiliaryData, - txAuxiliaryDataHash - ); -}; -export const prepareBody = ( - unsignedTx: ShelleyTxAuxType, - txWitnesses: any, - txAuxiliaryData: CborizedVotingRegistrationMetadata | null | undefined -) => { - const signedTransactionStructure = ShelleySignedTransactionStructured( - unsignedTx, - txWitnesses, - txAuxiliaryData - ); - return encode(signedTransactionStructure).toString('hex'); -}; diff --git a/source/renderer/app/utils/shelleyTrezor.ts b/source/renderer/app/utils/shelleyTrezor.ts index 0cfef1f7da..6f3a53be67 100644 --- a/source/renderer/app/utils/shelleyTrezor.ts +++ b/source/renderer/app/utils/shelleyTrezor.ts @@ -18,18 +18,18 @@ export const TrezorTransactionSigningMode = { ORDINARY_TRANSACTION: 0, POOL_REGISTRATION_AS_OWNER: 1, }; -export const prepareTrezorInput = (input: CoinSelectionInput) => { +export const toTrezorInput = (input: CoinSelectionInput) => { return { path: derivationPathToString(input.derivationPath), prev_hash: input.id, prev_index: input.index, }; }; -export const prepareTrezorOutput = (output: CoinSelectionOutput) => { +export const toTrezorOutput = (output: CoinSelectionOutput) => { let tokenBundle = []; if (output.assets) { - tokenBundle = prepareTokenBundle(output.assets); + tokenBundle = toTokenBundle(output.assets); } if (output.derivationPath) { @@ -52,7 +52,7 @@ export const prepareTrezorOutput = (output: CoinSelectionOutput) => { tokenBundle, }; }; -export const prepareTrezorCertificate = (cert: CoinSelectionCertificate) => { +export const toTrezorCertificate = (cert: CoinSelectionCertificate) => { if (cert.pool) { return { type: CERTIFICATE_TYPE[cert.certificateType], @@ -66,9 +66,7 @@ export const prepareTrezorCertificate = (cert: CoinSelectionCertificate) => { path: derivationPathToString(cert.rewardAccountPath), }; }; -export const prepareTrezorWithdrawal = ( - withdrawal: CoinSelectionWithdrawal -) => { +export const toTrezorWithdrawal = (withdrawal: CoinSelectionWithdrawal) => { return { path: derivationPathToString(withdrawal.derivationPath), amount: withdrawal.amount.quantity.toString(), @@ -83,7 +81,7 @@ export type TrezorVotingDataType = { nonce: string; }; -export const prepareTrezorAuxiliaryData = ({ +export const toTrezorAuxiliaryData = ({ address, votingKey, nonce, @@ -101,7 +99,7 @@ export const prepareTrezorAuxiliaryData = ({ }, }); // Helper Methods -export const prepareTokenBundle = (assets: CoinSelectionAssetsType) => { +export const toTokenBundle = (assets: CoinSelectionAssetsType) => { const tokenObject = groupTokensByPolicyId(assets); const tokenObjectEntries = Object.entries(tokenObject); const tokenBundle = map(tokenObjectEntries, ([policyId, tokens]) => {