diff --git a/packages/crypto-wallet-core/src/transactions/bch/index.ts b/packages/crypto-wallet-core/src/transactions/bch/index.ts index fdb3314ab3e..7da29189550 100644 --- a/packages/crypto-wallet-core/src/transactions/bch/index.ts +++ b/packages/crypto-wallet-core/src/transactions/bch/index.ts @@ -1,25 +1,27 @@ import BitcoreLibCash from '@bitpay-labs/bitcore-lib-cash'; -import { BTCTxProvider } from '../btc'; +import { BTCTxProvider, EveryUtxoType } from '../btc'; export class BCHTxProvider extends BTCTxProvider { lib = BitcoreLibCash; - create({ recipients, utxos = [], change, fee = 20000, isSweep }) { - const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / 1e8, - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + create(params: BchCreateParams): string { + const { recipients, utxos = [], change, fee = 20000, isSweep } = params; + const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, Number(fee)); + const btcUtxos = filteredUtxos.map(utxo => this.standardizeUtxo(utxo)); const tx = new this.lib.Transaction().from(btcUtxos).feePerByte(Number(fee) + 2); if (change) { tx.change(change); } for (const recipient of recipients) { - tx.to(recipient.address, parseInt(recipient.amount)); + tx.to(recipient.address, Number(recipient.amount)); } return tx.uncheckedSerialize(); } } + +export interface BchCreateParams { + recipients: Array<{ address: string; amount: number }>; + utxos?: EveryUtxoType[]; + change?: string; + fee?: number | string; + isSweep?: boolean; +}; diff --git a/packages/crypto-wallet-core/src/transactions/btc/index.ts b/packages/crypto-wallet-core/src/transactions/btc/index.ts index e172e8ad8a0..230cac248be 100644 --- a/packages/crypto-wallet-core/src/transactions/btc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/btc/index.ts @@ -15,42 +15,52 @@ export class BTCTxProvider { lib = BitcoreLib; selectCoins( - recipients: Array<{ amount: number }>, - utxos: Array<{ - value: number; - mintHeight: number; - txid?: string; - mintTxid?: string; - mintIndex?: number; - }>, - fee: number - ) { - utxos = utxos.sort(function(a, b) { - return a.mintHeight - b.mintHeight; - }); + recipients: Array<{ amount: number | string }>, + utxos: EveryUtxoType[], + fee?: number + ): EveryUtxoType[] { + // Only sort by block height if utxos are bitcore-node style + if (utxos.length > 0 && utxos[0].mintHeight != undefined) { + utxos = utxos.sort(function(a, b) { + return a.mintHeight - b.mintHeight; + }); + } let index = 0; let utxoSum = 0; const recepientSum = recipients.reduce((sum, cur) => sum + Number(cur.amount), fee || 0); while (utxoSum < recepientSum) { + assert(index < utxos.length, 'insufficient funds'); const utxo = utxos[index]; - utxoSum += Number(utxo.value); + utxoSum += Number(utxo.value ?? utxo.satoshis ?? this.lib.Unit.fromBTC(utxo.amount).toSatoshis()); index += 1; } const filteredUtxos = utxos.slice(0, index); return filteredUtxos; } - create({ recipients, utxos = [], change, feeRate, fee, isSweep, replaceByFee, lockUntilDate, lockUntilBlock }) { - const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / 1e8, - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + /** + * Standardize utxo for internal funcionality. + * Accepts either a bitcore-node or a lib (bitcore-lib, bitcore-lib-cash, etc.) utxo. + * Handles both lib style utxos: UnspentOutput properties and UnspentOutput.toObject properties. + * + * @param utxos either a bitcore-node or lib utxo + * @returns utxo in the standard, internaly used format + */ + standardizeUtxo(utxo: EveryUtxoType): UtxoType { + return { + satoshis: Number(utxo.satoshis ?? utxo.value ?? this.lib.Unit.fromBTC(utxo.amount ?? 0).toSatoshis()), + txId: utxo.txId ?? utxo.mintTxid ?? utxo.txid, + outputIndex: Number(utxo.outputIndex ?? utxo.mintIndex ?? utxo.vout ?? 0), + script: utxo.scriptPubKey ?? new this.lib.Script(utxo.script).toHex(), + address: utxo.address != undefined ? new this.lib.Address(utxo.address).toString() : undefined + }; + } + + create(params: BtcCreateParams): string { + const { recipients, utxos = [], change, feeRate, fee, isSweep, replaceByFee, lockUntilDate, lockUntilBlock } = params; + const filteredUtxos = isSweep ? utxos : this.selectCoins(recipients, utxos, Number(fee)); + const btcUtxos = filteredUtxos.map(utxo => this.standardizeUtxo(utxo)); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); @@ -62,7 +72,7 @@ export class BTCTxProvider { tx.change(change); } for (const recipient of recipients) { - tx.to(recipient.address, parseInt(recipient.amount)); + tx.to(recipient.address, Number(recipient.amount)); } if (replaceByFee && typeof tx.enableRBF === 'function') { tx.enableRBF(); @@ -79,7 +89,7 @@ export class BTCTxProvider { throw new Error('function getSignature not implemented for UTXO coins'); } - transformSignatureObject(params: { obj: any; sigtype?: number }) { + transformSignatureObject(params: BtcTransformSignatureObjectParams): string { const { obj, sigtype } = params; const { v } = obj; let { r, s, i, nhashtype } = obj; @@ -107,7 +117,7 @@ export class BTCTxProvider { return new this.lib.crypto.Signature({ r, s, i, nhashtype }).toString(); } - applySignature(params: { tx: BitcoreLib.Transaction; signature: SignatureType; index: number; sigtype?: number }) { + applySignature(params: BtcApplySignatureParams): BitcoreLib.Transaction { const { index, sigtype, tx } = params; let { signature } = params; assert(tx instanceof this.lib.Transaction, 'tx must be an instance of Transaction'); @@ -128,20 +138,21 @@ export class BTCTxProvider { return tx; } - getHash(params: { tx: string }) { + getHash(params: BtcGetHashParams): string { const bitcoreTx = new this.lib.Transaction(params.tx); return bitcoreTx.hash; } - sign(params: { tx: string; keys: Array; utxos: any[]; pubkeys?: any[]; threshold?: number; opts: any }) { + sign(params: BtcSignParams): string { const { tx, keys, pubkeys, threshold, opts } = params; const utxos = params.utxos || []; const bitcoreTx = new this.lib.Transaction(tx); + const btcUtxos = utxos.map(utxo => this.standardizeUtxo(utxo)); const applicableUtxos = this.getRelatedUtxos({ outputs: bitcoreTx.inputs, - utxos + utxos: btcUtxos }); - bitcoreTx.associateInputs(applicableUtxos, pubkeys, threshold, opts); + bitcoreTx.associateInputs(applicableUtxos.map(utxo => new this.lib.Transaction.UnspentOutput(utxo)), pubkeys, threshold, opts); const uniqePrivKeys = Object.values(keys.reduce((map, key) => { // Need to preserve (un)compressed property, so don't use key.privKey.toString(); const pk = new this.lib.PrivateKey(key.privKey); @@ -152,51 +163,39 @@ export class BTCTxProvider { return signedTx; } - getRelatedUtxos({ outputs, utxos }) { + getRelatedUtxos(params: { + outputs: BitcoreLib.Transaction.Input[]; + utxos: UtxoType[]; + }): UtxoType[] { + const { outputs, utxos } = params; const txids = outputs.map(output => output.toObject().prevTxId); - const applicableUtxos = utxos.filter(utxo => txids.includes(utxo.txid || utxo.mintTxid)); - return applicableUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / Math.pow(10, 8), - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + return utxos.filter(utxo => txids.includes(utxo.txId)); } - getOutputsFromTx({ tx }) { - return tx.outputs.map(({ script, satoshis }) => { + getOutputsFromTx(params: { + tx: BitcoreLib.Transaction; + }): Array<{ address: string | BitcoreLib.Script; satoshis: number }> { + return params.tx.outputs.map(({ script, satoshis }) => { const address = script; return { address, satoshis }; }); } - getSigningAddresses({ tx, utxos }): string[] { + getSigningAddresses(params: { + tx: TransactionType; + utxos: EveryUtxoType[]; + }): (string | undefined)[] { + const { tx, utxos } = params; const bitcoreTx = new this.lib.Transaction(tx); + const btcUtxos = utxos.map(utxo => this.standardizeUtxo(utxo)); const applicableUtxos = this.getRelatedUtxos({ outputs: bitcoreTx.inputs, - utxos + utxos: btcUtxos }); return applicableUtxos.map(utxo => utxo.address); } - getSighash(params: { - tx: string | BitcoreLib.Transaction; - index: number; - utxos?: BitcoreLib.Transaction.UnspentOutput[]; - pubKey?: string | BitcoreLib.PublicKey | BitcoreLib.HDPublicKey; - path?: string; - sigtype?: number; - // Multisig params for `associateInputs()` - /** Multisig public keys */ - pubKeys?: string[] | BitcoreLib.PublicKey[]; - /** Threshold for multisig */ - threshold?: number; - /** Options for multisig */ - opts?: any; - // end Multisig params for `associateInputs()` - }): string { + getSighash(params: BtcGetSighashParams): string { const { index, utxos, path, sigtype, pubKeys, threshold, opts } = params; let { tx, pubKey } = params; @@ -204,7 +203,8 @@ export class BTCTxProvider { tx = new this.lib.Transaction(tx); } if (utxos) { - tx.associateInputs(utxos.map(this.lib.Transaction.UnspentOutput), pubKeys, threshold, opts); + const btcUtxos = utxos.map(utxo => this.standardizeUtxo(utxo)); + tx.associateInputs(btcUtxos.map(utxo => new this.lib.Transaction.UnspentOutput(utxo)), pubKeys, threshold, opts); } $.checkState(tx.inputs[index].output instanceof this.lib.Transaction.Output, 'Input must have all utxo info'); @@ -224,4 +224,90 @@ export class BTCTxProvider { } } -type SignatureType = BitcoreLib.Transaction.Signature | BitcoreLib.crypto.Signature | TssSig; \ No newline at end of file +type SignatureType = BitcoreLib.Transaction.Signature | BitcoreLib.crypto.Signature | TssSig; + +/** Transaction data that can be converted into a Transaction via Transaction(tx) */ +type TransactionType = BitcoreLib.Transaction | string | Buffer | object; + +/** + * Standard utxo type use for internal processing. + * Property names are from bitcore-lib's UnspentOutput. + * Note, UnspentOutput addresses and scripts are Address and Script classes respectively, + * here they are both strings. + */ +export type UtxoType = { + txId: string; + outputIndex: number; + satoshis: number; + script: string; + address?: string; +}; + +/** + * Utxo type for functions were the received utxo type is unknown. + * Could either be in the format of UnspentOutput, UnspentOutput.toObject, or from bitcore-node. + */ +export type EveryUtxoType = Partial; + +export interface BtcCreateParams { + recipients: Array<{ address: string; amount: number | string }>; + utxos?: EveryUtxoType[]; + change?: string; + feeRate?: number | string; + fee?: number | string; + isSweep?: boolean; + replaceByFee?: boolean; + lockUntilDate?: number; + lockUntilBlock?: number; +}; + +export interface BtcSignParams { + tx: TransactionType; + keys: Key[]; + utxos: EveryUtxoType[]; + pubkeys?: any[]; + threshold?: number; + opts?: any; +}; + +export interface BtcApplySignatureParams { + tx: BitcoreLib.Transaction; + signature: SignatureType; + index: number; + sigtype?: number; +}; + +export interface BtcGetHashParams { tx: TransactionType }; + +export interface BtcTransformSignatureObjectParams { obj: any; sigtype?: number }; + +export interface BtcGetSighashParams { + tx: TransactionType; + index: number; + utxos?: EveryUtxoType[]; + pubKey?: string | BitcoreLib.PublicKey | BitcoreLib.HDPublicKey; + path?: string; + sigtype?: number; + // Multisig params for `associateInputs()` + /** Multisig public keys */ + pubKeys?: string[] | BitcoreLib.PublicKey[]; + /** Threshold for multisig */ + threshold?: number; + /** Options for multisig */ + opts?: any; + // end Multisig params for `associateInputs()` +}; diff --git a/packages/crypto-wallet-core/src/transactions/doge/index.ts b/packages/crypto-wallet-core/src/transactions/doge/index.ts index b4e97280056..a357e790d08 100644 --- a/packages/crypto-wallet-core/src/transactions/doge/index.ts +++ b/packages/crypto-wallet-core/src/transactions/doge/index.ts @@ -1,18 +1,12 @@ import BitcoreLibDoge from '@bitpay-labs/bitcore-lib-doge'; -import { BTCTxProvider } from '../btc'; +import { BTCTxProvider, EveryUtxoType } from '../btc'; export class DOGETxProvider extends BTCTxProvider { lib = BitcoreLibDoge; - create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { - const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / 1e8, - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + create(params: DogeCreateParams): string { + const { recipients, utxos = [], change, feeRate, fee = 20000 } = params; + const filteredUtxos = this.selectCoins(recipients, utxos, Number(fee)); + const btcUtxos = filteredUtxos.map(utxo => this.standardizeUtxo(utxo)); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); @@ -24,8 +18,16 @@ export class DOGETxProvider extends BTCTxProvider { tx.change(change); } for (const recipient of recipients) { - tx.to(recipient.address, parseInt(recipient.amount)); + tx.to(recipient.address, Number(recipient.amount)); } return tx.uncheckedSerialize(); } } + +export interface DogeCreateParams { + recipients: Array<{ address: string; amount: number }>; + utxos?: EveryUtxoType[]; + change?: string; + feeRate?: number | string; + fee?: number | string; +}; diff --git a/packages/crypto-wallet-core/src/transactions/erc20/index.ts b/packages/crypto-wallet-core/src/transactions/erc20/index.ts index 49496ab32b7..1e87b9cd9d3 100644 --- a/packages/crypto-wallet-core/src/transactions/erc20/index.ts +++ b/packages/crypto-wallet-core/src/transactions/erc20/index.ts @@ -10,19 +10,7 @@ export class ERC20TxProvider extends ETHTxProvider { return contract; } - create(params: { - recipients: Array<{ address: string; amount: string }>; - nonce: number; - gasPrice?: number; - data: string; - gasLimit: number; - tokenAddress: string; - network: string; - chainId?: number; - contractAddress?: string; - maxGasFee?: number; - priorityGasFee?: number; - }) { + create(params: Erc20CreateParams) { const { tokenAddress, contractAddress } = params; const data = this.encodeData(params); const recipients = [{ address: contractAddress || tokenAddress, amount: '0' }]; @@ -31,7 +19,7 @@ export class ERC20TxProvider extends ETHTxProvider { } encodeData(params: { - recipients: Array<{ address: string; amount: string }>; + recipients: Array<{ address: string; amount: number | string }>; tokenAddress: string; contractAddress?: string; }) { @@ -55,3 +43,17 @@ export class ERC20TxProvider extends ETHTxProvider { } } } + +export interface Erc20CreateParams { + recipients: Array<{ address: string; amount: number | string }>; + nonce: number; + gasPrice?: number; + gasLimit?: number; + tokenAddress: string; + data?: string; + network: string; + chainId?: number; + contractAddress?: string; + maxGasFee?: number; + priorityGasFee?: number; +}; diff --git a/packages/crypto-wallet-core/src/transactions/eth/index.ts b/packages/crypto-wallet-core/src/transactions/eth/index.ts index c9fa2c49c3e..778b5bdb663 100644 --- a/packages/crypto-wallet-core/src/transactions/eth/index.ts +++ b/packages/crypto-wallet-core/src/transactions/eth/index.ts @@ -16,19 +16,7 @@ export class ETHTxProvider { this.chain = chain; } - create(params: { - recipients: Array<{ address: string; amount: string }>; - nonce: number; - gasPrice?: number; - data: string; - gasLimit: number; - network: string; - chainId?: number; - contractAddress?: string; - maxGasFee?: number; - priorityGasFee?: number; - txType?: number; - }) { + create(params: EthCreateParams) { const { recipients, nonce, gasPrice, gasLimit, network, contractAddress, maxGasFee, priorityGasFee, txType } = params; let { data } = params; let to; @@ -116,18 +104,18 @@ export class ETHTxProvider { return signingKey.sign(ethers.keccak256(tx)); } - getSignature(params: { tx: string; key: Key }) { + getSignature(params: EthGetSignatureParams) { const signatureHex = this.getSignatureObject(params).serialized; return signatureHex; } - getHash(params: { tx: string }) { + getHash(params: EthGetHashParams) { const { tx } = params; // tx must be signed for hash to exist return ethers.Transaction.from(tx).hash; } - applySignature(params: { tx: string; signature: any }) { + applySignature(params: EthApplySignatureParams) { const { tx, signature } = params; const parsedTx = ethers.Transaction.from(tx); const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = parsedTx; @@ -160,7 +148,7 @@ export class ETHTxProvider { return signedTx.serialized; } - sign(params: { tx: string; key: Key }) { + sign(params: EthSignParams) { const { tx, key } = params; const signature = this.getSignatureObject({ tx, key }); return this.applySignature({ tx, signature }); @@ -171,13 +159,39 @@ export class ETHTxProvider { // Web3.utils.toHex('20000') -> '0x323030303030' because it calls utf8ToHex for strings return value != null ? Web3.utils.toHex(BigInt(value)) : undefined; } - transformSignatureObject(params: { obj: any }) { + transformSignatureObject(params: EthTransformSignatureObjectParams) { const { obj } = params; return ethers.Signature.from(obj).serialized; } - getSighash(params: { tx: string }): string { + getSighash(params: EthGetSighashParams): string { const { tx } = params; return ethers.keccak256(tx).slice(2); // remove 0x prefix } } + +export interface EthCreateParams { + recipients: Array<{ address: string; amount: number | bigint | string }>; + nonce: number; + network: string; + data?: string; + gasLimit?: number; + gasPrice?: number; + chainId?: number; + contractAddress?: string; + maxGasFee?: number; + priorityGasFee?: number; + txType?: number; +}; + +export interface EthGetSignatureParams { tx: string; key: Key }; + +export interface EthGetHashParams { tx: string }; + +export interface EthApplySignatureParams { tx: string; signature: any }; + +export interface EthSignParams { tx: string; key: Key }; + +export interface EthTransformSignatureObjectParams { obj: any }; + +export interface EthGetSighashParams { tx: string }; diff --git a/packages/crypto-wallet-core/src/transactions/index.ts b/packages/crypto-wallet-core/src/transactions/index.ts index 47fcbe0076f..e40ebfbe615 100644 --- a/packages/crypto-wallet-core/src/transactions/index.ts +++ b/packages/crypto-wallet-core/src/transactions/index.ts @@ -6,22 +6,22 @@ import { BASEERC20TxProvider, BASETxProvider } from './base'; -import { BCHTxProvider } from './bch'; -import { BTCTxProvider } from './btc'; -import { DOGETxProvider } from './doge'; -import { ERC20TxProvider } from './erc20'; -import { ETHTxProvider } from './eth'; +import { BCHTxProvider, BchCreateParams } from './bch'; +import { BTCTxProvider, BtcApplySignatureParams, BtcCreateParams, BtcGetHashParams, BtcGetSighashParams, BtcSignParams, BtcTransformSignatureObjectParams } from './btc'; +import { DOGETxProvider, DogeCreateParams } from './doge'; +import { ERC20TxProvider, Erc20CreateParams } from './erc20'; +import { ETHTxProvider, EthApplySignatureParams, EthCreateParams, EthGetHashParams, EthGetSignatureParams, EthSignParams, EthTransformSignatureObjectParams } from './eth'; import { ETHMULTISIGTxProvider } from './eth-multisig'; -import { LTCTxProvider } from './ltc'; +import { LTCTxProvider, LtcCreateParams } from './ltc'; import { MATICERC20TxProvider, MATICTxProvider } from './matic'; -import { MATICMULTISIGTxProvider } from './matic-multisig'; +import { MATICMULTISIGTxProvider, MaticMultisigCreateParams } from './matic-multisig'; import { OPERC20TxProvider, OPTxProvider } from './op'; -import { SOLTxProvider } from './sol'; -import { SPLTxProvider } from './spl'; -import { XRPTxProvider } from './xrp'; +import { SOLTxProvider, SolApplySignatureParams, SolCreateParams, SolGetHashParams, SolGetSighashParams, SolGetSignatureParams, SolSignParams } from './sol'; +import { SPLTxProvider, SplCreateParams } from './spl'; +import { XRPTxProvider, XrpApplySignatureParams, XrpCreateParams, XrpGetHashParams, XrpGetSighashParams, XrpSignParams, XrpTransformSignatureObjectParams } from './xrp'; const providers = { BTC: new BTCTxProvider(), @@ -48,38 +48,85 @@ const providers = { }; export class TransactionsProxy { - get({ chain }) { - const normalizedChain = chain.toUpperCase(); + get(params: DefaultParams) { + const normalizedChain = params.chain.toUpperCase(); return providers[normalizedChain]; } - create(params) { + create(params: BtcCreateParams & { chain: 'BTC' }): string; + create(params: BchCreateParams & { chain: 'BCH' }): string; + create(params: LtcCreateParams & { chain: 'LTC' }): string; + create(params: DogeCreateParams & { chain: 'DOGE' }): string; + create(params: MaticMultisigCreateParams & { chain: 'MATICMULTISIG' }): string; + create(params: Erc20CreateParams & { chain: Erc20Chain }): string; + create(params: EthCreateParams & { chain: Exclude }): string; + create(params: SolCreateParams & { chain: 'SOL' }): string; + create(params: SplCreateParams & { chain: 'SOLSPL' }): string; + create(params: XrpCreateParams & { chain: 'XRP' }): string; + create(params: DefaultParams): string { return this.get(params).create(params); } - sign(params): string { + sign(params: BtcSignParams & { chain: UtxoChain }): string; + sign(params: EthSignParams & { chain: EvmChain }): string; + sign(params: SolSignParams & { chain: SolChain }): string; + sign(params: XrpSignParams & { chain: 'XRP' }): string; + sign(params: DefaultParams): string { return this.get(params).sign(params); } - getSignature(params): string { + getSignature(params: Record & { chain: UtxoChain }): any; // not implemented + getSignature(params: EthGetSignatureParams & { chain: EvmChain }): string; + getSignature(params: SolGetSignatureParams & { chain: SolChain }): string; + getSignature(params: XrpGetSighashParams & { chain: 'XRP' }): string; + getSignature(params: DefaultParams): string { return this.get(params).getSignature(params); } - applySignature(params) { + applySignature(params: BtcApplySignatureParams & { chain: UtxoChain }): string; + applySignature(params: EthApplySignatureParams & { chain: EvmChain }): string; + applySignature(params: SolApplySignatureParams & { chain: SolChain }): string; + applySignature(params: XrpApplySignatureParams & { chain: 'XRP' }): string; + applySignature(params: DefaultParams): string { return this.get(params).applySignature(params); } - getHash(params) { + getHash(params: BtcGetHashParams & { chain: UtxoChain }): string; + getHash(params: EthGetHashParams & { chain: EvmChain }): string; + getHash(params: SolGetHashParams & { chain: SolChain }): string; + getHash(params: XrpGetHashParams & { chain: 'XRP' }): string; + getHash(params: DefaultParams): string { return this.get(params).getHash(params); } - transformSignatureObject(params) { + transformSignatureObject(params: BtcTransformSignatureObjectParams & { chain: UtxoChain }): string; + transformSignatureObject(params: EthTransformSignatureObjectParams & { chain: EvmChain }): string; + // not implemented for solana + transformSignatureObject(params: XrpTransformSignatureObjectParams & { chain: 'XRP' }): string; + transformSignatureObject(params: DefaultParams): string { return this.get(params).transformSignatureObject(params); } - getSighash(params): string { + getSighash(params: BtcGetSighashParams & { chain: UtxoChain }): string; + getSighash(params: EthGetSignatureParams & { chain: EvmChain }): string; + getSighash(params: SolGetSighashParams & { chain: SolChain }): string; + getSighash(params: XrpGetSighashParams & { chain: 'XRP' }): string; + getSighash(params: DefaultParams): string { return this.get(params).getSighash(params); } } +type UtxoChain = 'BTC' | 'BCH' | 'LTC' | 'DOGE'; +type Erc20Chain = + 'ETHERC20' | 'MATICERC20' | 'ARBERC20' + | 'BASEERC20' | 'OPERC20' | 'ARCERC20'; +type EvmChain = Erc20Chain | + 'ETH' |'ETHMULTISIG' | 'MATIC' + | 'MATICMULTISIG' | 'ARB' | 'ARC' + | 'OP' | 'OPERC20' | 'BASE'; +type SolChain = 'SOL' | 'SOLSPL'; +type Chain = UtxoChain | EvmChain | SolChain | 'XRP'; + +type DefaultParams = { chain: Chain }; + export default new TransactionsProxy(); diff --git a/packages/crypto-wallet-core/src/transactions/ltc/index.ts b/packages/crypto-wallet-core/src/transactions/ltc/index.ts index 41cd1a5ef8b..a1ec804c2f1 100644 --- a/packages/crypto-wallet-core/src/transactions/ltc/index.ts +++ b/packages/crypto-wallet-core/src/transactions/ltc/index.ts @@ -1,18 +1,12 @@ import BitcoreLibLtc from '@bitpay-labs/bitcore-lib-ltc'; -import { BTCTxProvider } from '../btc'; +import { BTCTxProvider, EveryUtxoType } from '../btc'; export class LTCTxProvider extends BTCTxProvider { lib = BitcoreLibLtc; - create({ recipients, utxos = [], change, feeRate, fee = 20000 }) { - const filteredUtxos = this.selectCoins(recipients, utxos, fee); - const btcUtxos = filteredUtxos.map(utxo => { - const btcUtxo = Object.assign({}, utxo, { - amount: utxo.value / 1e8, - txid: utxo.mintTxid, - outputIndex: utxo.mintIndex - }); - return new this.lib.Transaction.UnspentOutput(btcUtxo); - }); + create(params: LtcCreateParams): string { + const { recipients, utxos = [], change, feeRate, fee = 20000 } = params; + const filteredUtxos = this.selectCoins(recipients, utxos, Number(fee)); + const btcUtxos = filteredUtxos.map(utxo => this.standardizeUtxo(utxo)); const tx = new this.lib.Transaction().from(btcUtxos); if (fee) { tx.fee(fee); @@ -24,8 +18,16 @@ export class LTCTxProvider extends BTCTxProvider { tx.change(change); } for (const recipient of recipients) { - tx.to(recipient.address, parseInt(recipient.amount)); + tx.to(recipient.address, Number(recipient.amount)); } return tx.uncheckedSerialize(); } } + +export interface LtcCreateParams { + recipients: Array<{ address: string; amount: number }>; + utxos?: EveryUtxoType[]; + change?: string; + feeRate?: number | string; + fee?: number | string; +}; diff --git a/packages/crypto-wallet-core/src/transactions/matic-multisig/index.ts b/packages/crypto-wallet-core/src/transactions/matic-multisig/index.ts index f7be55524b3..eda5566ea67 100644 --- a/packages/crypto-wallet-core/src/transactions/matic-multisig/index.ts +++ b/packages/crypto-wallet-core/src/transactions/matic-multisig/index.ts @@ -10,16 +10,7 @@ export class MATICMULTISIGTxProvider extends MATICTxProvider { return contract; } - create(params: { - recipients: Array<{ address: string; amount: string }>; - nonce: number; - gasPrice: number; - data: string; - gasLimit: number; - multisigContractAddress: string; - network: string; - chainId?: number; - }) { + create(params: MaticMultisigCreateParams) { const { multisigContractAddress } = params; const recipients = [{ address: multisigContractAddress, amount: '0' }]; const newParams = { ...params, recipients }; @@ -170,3 +161,14 @@ export class MATICMULTISIGTxProvider extends MATICTxProvider { return contract.methods.submitTransaction(address, amountStr, data).encodeABI(); } } + +export interface MaticMultisigCreateParams { + recipients: Array<{ address: string; amount: string }>; + nonce: number; + gasPrice: number; + data: string; + gasLimit: number; + multisigContractAddress: string; + network: string; + chainId?: number; +} diff --git a/packages/crypto-wallet-core/src/transactions/sol/index.ts b/packages/crypto-wallet-core/src/transactions/sol/index.ts index 0f6d78386af..2a8a847d0d6 100644 --- a/packages/crypto-wallet-core/src/transactions/sol/index.ts +++ b/packages/crypto-wallet-core/src/transactions/sol/index.ts @@ -11,27 +11,7 @@ export class SOLTxProvider { MAX_TRANSFERS = 12; MINIMUM_PRIORITY_FEE = 1000; - create(params: { - recipients: Array<{ address: string; amount: string; addressKeyPair?: SolKit.KeyPairSigner }>; - from: string; - fee?: number; - feeRate: number; - txType?: 'legacy' | '0'; // legacy, version 0 - category?: 'transfer' | 'createAccount'; // transfer, create account - nonce?: string; // nonce is represented as a transaction id - nonceAddress?: string; - blockHash?: string; - blockHeight?: number; - priorityFee?: number; - computeUnits?: number; - memo?: string; - txInstructions?: Array; - // account creation fields - fromKeyPair?: any; - space?: number; // amount of space to reserve a new account in bytes - mint?: string; // mint address for createATA - ataAddress?: any; // ATA address for createATA - }) { + create(params: SolCreateParams) { const { recipients, from, nonce, nonceAddress, category, space, blockHash, blockHeight, priorityFee, txInstructions, computeUnits, fromKeyPair, memo } = params; const fromAddress = SolKit.address(from); const txType: SolKit.TransactionVersion = ['0', 0].includes(params?.txType) ? 0 : 'legacy'; @@ -180,7 +160,7 @@ export class SOLTxProvider { return SolKit.decompileTransactionMessage(compiledTransactionMessage); } - async sign(params: { tx: string; key: Key }): Promise { + async sign(params: SolSignParams): Promise { const { tx, key } = params; const decodedTx = this.decodeRawTransaction({ rawTx: tx, decodeTransactionMessage: false }); const privKeyBytes = SolKit.getBase58Encoder().encode(key.privKey); @@ -206,7 +186,7 @@ export class SOLTxProvider { return SolKit.getBase58Decoder().decode(signedBytes); } - async getSignature(params: { tx: string; keys: Array }) { + async getSignature(params: SolGetSignatureParams) { const { tx, keys } = params; const key = keys[0]; const signedTx = await this.sign({ tx, key }); @@ -215,7 +195,7 @@ export class SOLTxProvider { return SolKit.getBase58Decoder().decode(sigEncoding); } - applySignature(params: { tx: string; signature: string }): string { + applySignature(params: SolApplySignatureParams): string { const { tx, signature } = params; const signatures = [SolKit.getBase58Encoder().encode(signature)]; const transaction = SolKit.getBase64Encoder().encode(tx); @@ -262,7 +242,7 @@ export class SOLTxProvider { return SolKit.getBase64EncodedWireTransaction(signedTx); } - getHash(params: { tx: string }): string { + getHash(params: SolGetHashParams): string { const { tx } = params; const decodedTx = this.decodeRawTransaction({ rawTx: tx, decodeTransactionMessage: false }); const pubKeys = Object.keys(decodedTx.signatures); @@ -286,7 +266,39 @@ export class SOLTxProvider { return SolKit.getBase58Decoder().decode(signature); } - getSighash(_params: { tx: string }): string { + getSighash(_params: SolGetSighashParams): string { return null; // TODO } -} \ No newline at end of file +} + +export interface SolCreateParams { + recipients: Array<{ address: string; amount: number | string; addressKeyPair?: SolKit.KeyPairSigner }>; + from: string; + fee?: number; + feeRate?: number; + txType?: 'legacy' | '0'; // legacy, version 0 + category?: 'transfer' | 'createAccount'; // transfer, create account + nonce?: string; // nonce is represented as a transaction id + nonceAddress?: string; + blockHash?: string; + blockHeight?: number; + priorityFee?: number; + computeUnits?: number; + memo?: string; + txInstructions?: Array; + // account creation fields + fromKeyPair?: any; + space?: number; // amount of space to reserve a new account in bytes + mint?: string; // mint address for createATA + ataAddress?: any; // ATA address for createATA +}; + +export interface SolSignParams { tx: string; key: Key }; + +export interface SolGetSignatureParams { tx: string; keys: Array }; + +export interface SolApplySignatureParams { tx: string; signature: string }; + +export interface SolGetHashParams { tx: string }; + +export interface SolGetSighashParams { tx: string }; diff --git a/packages/crypto-wallet-core/src/transactions/spl/index.ts b/packages/crypto-wallet-core/src/transactions/spl/index.ts index afa0d6de210..4ceda01af4e 100644 --- a/packages/crypto-wallet-core/src/transactions/spl/index.ts +++ b/packages/crypto-wallet-core/src/transactions/spl/index.ts @@ -4,7 +4,7 @@ import { SOLTxProvider } from '../sol'; export class SPLTxProvider extends SOLTxProvider { - create(params: CreateParams) { + create(params: SplCreateParams) { // Reuse exposed TransactionProxy API (Create) // @ts-expect-error - so public api is minimally changed if (params.category === 'recoverNestedAssociatedToken') { @@ -143,7 +143,7 @@ export class SPLTxProvider extends SOLTxProvider { } } -interface CreateParams { +export interface SplCreateParams { recipients: Array<{ address: string; amount: string; addressKeyPair?: SolKit.KeyPairSigner }>; from: string; fee?: number; diff --git a/packages/crypto-wallet-core/src/transactions/xrp/index.ts b/packages/crypto-wallet-core/src/transactions/xrp/index.ts index 00f4167cb13..a28ea151a30 100644 --- a/packages/crypto-wallet-core/src/transactions/xrp/index.ts +++ b/packages/crypto-wallet-core/src/transactions/xrp/index.ts @@ -8,17 +8,7 @@ import { BTCTxProvider } from '../btc'; import type { Key } from '../../types/derivation'; export class XRPTxProvider { - create(params: { - recipients: Array<{ address: string; amount: string; tag?: number }>; - tag?: number; - from: string; - invoiceID?: string; - fee: number; - feeRate: number; - nonce: number; - txType?: string; - flags?: number | string; - }) { + create(params: XrpCreateParams) { const { recipients, tag, @@ -87,19 +77,19 @@ export class XRPTxProvider { return { signedTransaction: signedTx.tx_blob, hash: signedTx.hash }; } - getSignature(params: { tx: string; key: Key }): string { + getSignature(params: XrpGetSignatureParams): string { const { signedTransaction } = this.getSignatureObject(params); const decoded = (xrpl.decode(signedTransaction) as any) as xrpl.Transaction; return decoded.TxnSignature; } - getHash(params: { tx: string }): string { + getHash(params: XrpGetHashParams): string { const { tx } = params; const prefix = HashPrefix.transactionID.toString('hex').toUpperCase(); return this.sha512Half(prefix + tx); } - applySignature(params: { tx: string; signature: string; pubKey: string }): string { + applySignature(params: XrpApplySignatureParams): string { const { tx, signature, pubKey } = params; const txJSON = (xrpl.decode(tx) as any) as xrpl.Transaction; txJSON.TxnSignature = signature; @@ -108,7 +98,7 @@ export class XRPTxProvider { return signedTx; } - sign(params: { tx: string; key: Key }): string { + sign(params: XrpSignParams): string { const { tx, key } = params; const signature = this.getSignature({ tx, key }); return this.applySignature({ tx, signature, pubKey: key.pubKey }); @@ -122,12 +112,12 @@ export class XRPTxProvider { .slice(0, 64); } - transformSignatureObject(params: { obj: any }) { + transformSignatureObject(params: XrpTransformSignatureObjectParams) { const { obj } = params; return new BTCTxProvider().transformSignatureObject({ obj }); } - getSighash(params: { tx: string; pubKey: string }) { + getSighash(params: XrpGetSighashParams) { const { tx, pubKey } = params; const decoded = RBC.decode(tx); decoded.SigningPubKey = pubKey; @@ -155,4 +145,28 @@ export class XRPTxProvider { return acc; }, {} as T); } -} \ No newline at end of file +} + +export interface XrpCreateParams { + recipients: Array<{ address: string; amount: string; tag?: number }>; + tag?: number; + from: string; + invoiceID?: string; + fee: number; + feeRate?: number; + nonce: number; + txType?: string; + flags?: number | string; +}; + +export interface XrpGetSignatureParams { tx: string; key: Key } + +export interface XrpGetHashParams { tx: string }; + +export interface XrpApplySignatureParams { tx: string; signature: string; pubKey: string }; + +export interface XrpSignParams { tx: string; key: Key }; + +export interface XrpTransformSignatureObjectParams { obj: any }; + +export interface XrpGetSighashParams { tx: string; pubKey: string }; diff --git a/packages/crypto-wallet-core/src/types/derivation.ts b/packages/crypto-wallet-core/src/types/derivation.ts index babb0a87890..dfe53fc2c65 100644 --- a/packages/crypto-wallet-core/src/types/derivation.ts +++ b/packages/crypto-wallet-core/src/types/derivation.ts @@ -1,6 +1,6 @@ export interface Key { - address: string; - privKey?: string; + address?: string; + privKey: string; pubKey?: string; } @@ -31,4 +31,4 @@ export interface IDeriver { * Temporary - converts decrypted private key buffer to lib-specific private key format */ bufferToPrivateKey_TEMP(buf: Buffer, network: string): string; -} \ No newline at end of file +} diff --git a/packages/crypto-wallet-core/test/transactions.test.ts b/packages/crypto-wallet-core/test/transactions.test.ts index 52f12d405d8..3fd74a1c8ee 100644 --- a/packages/crypto-wallet-core/test/transactions.test.ts +++ b/packages/crypto-wallet-core/test/transactions.test.ts @@ -30,7 +30,7 @@ describe('Transaction', function() { } ]; const fee = 7440; - const tx = Transactions.create({ chain: 'BTC', recipients, change, utxos, fee, rbf: true }); + const tx = Transactions.create({ chain: 'BTC', recipients, change, utxos, fee }); const keys = [ { @@ -661,7 +661,7 @@ describe('Transaction', function() { it('should be able to create a XRP tx', () => { const recipients = [{ address: 'rEqj9WKSH7wEkPvWf6b4gCi26Y3F7HbKUF', amount: '123456' }]; const xrpParams = { - chain: 'XRP', + chain: 'XRP' as const, recipients, from: 'rEqj9WKSH7wEkPvWf6b4gCi26Y3F7HbKUF', tag: 123456, @@ -680,7 +680,7 @@ describe('Transaction', function() { try { const recipients = [{ address: 'rEqj9WKSH7wEkPvWf6b4gCi26Y3F7HbKUF', amount: '123456' }]; const xrpParams = { - chain: 'XRP', + chain: 'XRP' as const, recipients, from: 'rEqj9WKSH7wEkPvWf6b4gCi26Y3F7HbKUF', tag: 123456, @@ -699,7 +699,7 @@ describe('Transaction', function() { try { const recipients = [{ address: 'rEqj9WKSH7wEkPvWf6b4gCi26Y3F7HbKUF', amount: '123456' }]; const xrpParams = { - chain: 'XRP', + chain: 'XRP' as const, recipients, from: 'rEqj9WKSH7wEkPvWf6b4gCF', tag: 123456, @@ -715,14 +715,16 @@ describe('Transaction', function() { }); it('should create an AccountSet tx with string flag', () => { + const recipients = [{ address: 'rEqj9WKSH7wEkPvWf6b4gCi26Y3F7HbKUF', amount: '123456' }]; const xrpParams = { - chain: 'XRP', + chain: 'XRP' as const, network: 'testnet', from: 'rMmUqMZRzKKnzrTnN3B6Zcz4qQQvmHowt8', fee: 10, nonce: 11876358, txType: 'accountset', - flags: 'tfRequireDestTag' + flags: 'tfRequireDestTag', + recipients }; const cryptoTx = Transactions.create(xrpParams); const expectedTx = '12000322000100002400B5380668400000000000000A8114E3BEB23E9931CEE681B8CBFDA2F9203EFC18C5BA'; @@ -731,13 +733,14 @@ describe('Transaction', function() { it('should create an AccountSet tx with comma-delimited string flags', () => { const xrpParams = { - chain: 'XRP', + chain: 'XRP' as const, network: 'testnet', from: 'rMmUqMZRzKKnzrTnN3B6Zcz4qQQvmHowt8', fee: 10, nonce: 11876358, txType: 'accountset', - flags: 'tfRequireDestTag,tfDisallowXRP' + flags: 'tfRequireDestTag,tfDisallowXRP', + recipients: [] }; const cryptoTx = Transactions.create(xrpParams); const expectedTx = '12000322001100002400B5380668400000000000000A8114E3BEB23E9931CEE681B8CBFDA2F9203EFC18C5BA'; @@ -746,13 +749,14 @@ describe('Transaction', function() { it('should create an AccountSet tx with number flag', () => { const xrpParams = { - chain: 'XRP', + chain: 'XRP' as const, network: 'testnet', from: 'rMmUqMZRzKKnzrTnN3B6Zcz4qQQvmHowt8', fee: 10, nonce: 11876358, txType: 'accountset', - flags: 65536 // tfRequireDestTag + flags: 65536, // tfRequireDestTag + recipients: [] }; const cryptoTx = Transactions.create(xrpParams); const expectedTx = '12000322000100002400B5380668400000000000000A8114E3BEB23E9931CEE681B8CBFDA2F9203EFC18C5BA'; @@ -761,12 +765,13 @@ describe('Transaction', function() { it('should throw on invalid flag(s)', () => { const xrpParams = { - chain: 'XRP', + chain: 'XRP' as const, network: 'testnet', from: 'rMmUqMZRzKKnzrTnN3B6Zcz4qQQvmHowt8', fee: 10, nonce: 11876358, - txType: 'accountset' + txType: 'accountset', + recipients: [] }; expect(() => Transactions.create({ ...xrpParams, flags: undefined })).to.throw('No XRP flag(s) provided'); expect(() => Transactions.create({ ...xrpParams, flags: null })).to.throw('No XRP flag(s) provided'); @@ -796,7 +801,7 @@ describe('Transaction', function() { } ]; const fee = 7440; - const tx = Transactions.create({ chain: 'DOGE', recipients, change, utxos, fee, rbf: true }); + const tx = Transactions.create({ chain: 'DOGE', recipients, change, utxos, fee }); const keys = [ { @@ -833,7 +838,7 @@ describe('Transaction', function() { } ]; const fee = 7440; - const tx = Transactions.create({ chain: 'LTC', recipients, change, utxos, fee, rbf: true }); + const tx = Transactions.create({ chain: 'LTC', recipients, change, utxos, fee }); const keys = [ { @@ -848,13 +853,14 @@ describe('Transaction', function() { expect(signed).to.eq(expected); }); + // TODO: typing is incorrect it('should be able to create a livenet SOL tx', () => { const rawMaticTx = { network: 'livenet', value: 3896000000000000, to: 'F7FknkRckx4yvA3Gexnx1H3nwPxndMxVt58BwAzEQhcY', from: '8WyoNvKsmfdG6zrbzNBVN8DETyLra3ond61saU9C52YR', - category: 'transfer', + category: 'transfer' as const, blockHash: 'GtV1Hb3FvP3HURHAsj8mGwEqCumvP3pv3i6CVCzYNj3d', blockHeight: 531575, }; @@ -869,8 +875,6 @@ describe('Transaction', function() { 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDb6/gH5XxrVl86CZd+DpqA1jN8YSz91e8yXxOlyeS8tLRnckLdZVIkhi0iAExccvYpTw5tIfPZ8z/OJGQtnvg9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7A+XJrI4siFXUreDo+M94DBeuJwm0Oq5kHqeWuAw7xgBAgIAAQwCAAAAAIALMGTXDQA='; expect(cryptoTx).to.equal(expectedTx); }); - - }); describe('sign', () => { @@ -1740,30 +1744,6 @@ describe('Transaction', function() { expect(hash.length).to.equal(64); expect(hash).to.equal(expectedHash); }); - - it('should get XRP testnet signed tx hash', () => { - const hash = Transactions.getHash({ - chain: 'XRP', - network: 'testnet', - tx: - '120000228000000024000000012E0001E2405011101234567890123456789012345671012345678901234567890156789012345661400000000001E24068400000000000000C732103DBEEC5E9E76DA09C5B502A67136BC2D73423E8902A7C35A8CBC0C5A6AC0469E874473045022100D5C19360E77D691A11CA693F6E8D8472DA6749D16A06E072ED1110EB3FD9E2C80220169F95E55943C3575CEAA46413FE660E4F8F2E7158FAC235DC3CB9C9F26918098114A2C8E8CD9A9133CAD90F2668159AAF572612A5028314A2C8E8CD9A9133CAD90F2668159AAF572612A502' - }); - const expectedHash = '61EA6DF3BD1E435283BA0B06311C7BA683A32A80E465196D9F16A23A439EF6F4'; - expect(hash.length).to.equal(64); - expect(hash).to.equal(expectedHash); - }); - - it('should get XRP livenet signed tx hash', () => { - const hash = Transactions.getHash({ - chain: 'XRP', - network: 'livenet', - tx: - '120000228000000024000000012E0001E2405011101234567890123456789012345671012345678901234567890156789012345661400000000001E24068400000000000000C732103DBEEC5E9E76DA09C5B502A67136BC2D73423E8902A7C35A8CBC0C5A6AC0469E874473045022100D5C19360E77D691A11CA693F6E8D8472DA6749D16A06E072ED1110EB3FD9E2C80220169F95E55943C3575CEAA46413FE660E4F8F2E7158FAC235DC3CB9C9F26918098114A2C8E8CD9A9133CAD90F2668159AAF572612A5028314A2C8E8CD9A9133CAD90F2668159AAF572612A502' - }); - const expectedHash = '61EA6DF3BD1E435283BA0B06311C7BA683A32A80E465196D9F16A23A439EF6F4'; - expect(hash.length).to.equal(64); - expect(hash).to.equal(expectedHash); - }); }); @@ -1885,6 +1865,5 @@ describe('Transaction', function() { const result = ETHTxProvider._toHex('0x200000'); expect(result).to.equal('0x200000'); }); - }); -}); \ No newline at end of file +});