@abcpros/bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
449 lines (397 loc) • 11.5 kB
text/typescript
import { Transactions } from '@abcpros/crypto-wallet-core';
import _ from 'lodash';
import { ChainService } from '../chain/index';
import logger from '../logger';
import { TxProposalLegacy } from './txproposal_legacy';
import { TxProposalAction } from './txproposalaction';
const $ = require('preconditions').singleton();
const Uuid = require('uuid');
const Common = require('../common');
const Constants = Common.Constants,
Defaults = Common.Defaults,
Utils = Common.Utils;
export interface ITxProposal {
type: string;
creatorName: string;
createdOn: number;
txid: string;
id: string;
walletId: string;
creatorId: string;
coin: string;
chain: string;
network: string;
message: string;
messageOnChain: string;
payProUrl: string;
from: string;
changeAddress: string;
inputs: any[];
outputs: Array<{
amount: number;
address: string;
toAddress?: string;
message?: string;
data?: string;
gasLimit?: number;
script?: string;
}>;
outputOrder: number;
walletM: number;
walletN: number;
requiredSignatures: number;
requiredRejections: number;
status: string;
actions: [];
feeLevel: number;
feePerKb: number;
excludeUnconfirmedUtxos: boolean;
addressType: string;
customData: any;
amount: string;
fee: number;
version: number;
broadcastedOn: number;
inputPaths: string;
proposalSignature: string;
proposalSignaturePubKey: string;
proposalSignaturePubKeySig: string;
signingMethod: string;
lowFees: boolean;
nonce?: number;
gasPrice?: number;
gasLimit?: number; // Backward compatibility for BWC <= 8.9.0
data?: string; // Backward compatibility for BWC <= 8.9.0
tokenAddress?: string;
multisigContractAddress?: string;
destinationTag?: string;
invoiceID?: string;
lockUntilBlockHeight?: number;
isTokenSwap?: boolean;
}
export class TxProposal {
type: string;
creatorName: string;
createdOn: number;
id: string;
txid: string;
walletId: string;
creatorId: string;
coin: string;
chain: string;
network: string;
message: string;
messageOnChain: string;
payProUrl: string;
from: string;
changeAddress: any;
inputs: any[];
outputs: Array<{
amount: number;
address?: string;
toAddress?: string;
message?: string;
data?: string;
gasLimit?: number;
script?: string;
satoshis?: number;
}>;
outputOrder: number[];
walletM: number;
walletN: number;
requiredSignatures: number;
requiredRejections: number;
status: string;
actions: any[] = [];
feeLevel: number;
feePerKb: number;
excludeUnconfirmedUtxos: boolean;
addressType: string;
customData: any;
amount: string | number;
fee: number;
version: number;
broadcastedOn: number;
inputPaths: string | any[];
proposalSignature: string;
proposalSignaturePubKey: string;
proposalSignaturePubKeySig: string;
signingMethod: string;
raw?: Array<string> | string;
nonce?: number;
gasPrice?: number;
gasLimit?: number; // Backward compatibility for BWC <= 8.9.0
data?: string; // Backward compatibility for BWC <= 8.9.0
tokenAddress?: string;
multisigContractAddress?: string;
multisigTxId?: string;
destinationTag?: string;
invoiceID?: string;
lockUntilBlockHeight?: number;
isDonation?: boolean;
receiveLotusAddress?: string;
isTokenSwap?: boolean;
static create(opts) {
opts = opts || {};
$.checkArgument(Utils.checkValueInCollection(opts.network, Constants.NETWORKS));
const x = new TxProposal();
// allow creating legacy tx version == 3 only for testing
if (opts.version) {
$.checkArgument(opts.version >= 3);
}
x.isDonation = opts.isDonation;
x.receiveLotusAddress = opts.receiveLotusAddress;
// x.version = opts.version || 5; // DISABLED 2020-04-07
x.version = opts.version || 3;
$.checkState(x.version <= 3, 'Failed state: txp version 4 not allowed yet at <create()>');
const now = Date.now();
x.createdOn = Math.floor(now / 1000);
x.id = opts.id || Uuid.v4();
x.walletId = opts.walletId;
x.creatorId = opts.creatorId;
x.coin = opts.coin;
x.chain = opts.chain;
x.network = opts.network;
x.signingMethod = opts.signingMethod;
x.message = opts.message;
x.messageOnChain = opts.messageOnChain;
x.payProUrl = opts.payProUrl;
x.changeAddress = opts.changeAddress;
x.outputs = _.map(opts.outputs, output => {
return _.pick(output, ['amount', 'toAddress', 'message', 'data', 'gasLimit', 'script']);
});
x.outputOrder = _.range(x.outputs.length + 1);
if (!opts.noShuffleOutputs) {
x.outputOrder = _.shuffle(x.outputOrder);
}
x.walletM = opts.walletM;
x.walletN = opts.walletN;
x.requiredSignatures = x.walletM;
(x.requiredRejections = Math.min(x.walletM, x.walletN - x.walletM + 1)), (x.status = 'temporary');
x.actions = [];
x.feeLevel = opts.feeLevel;
x.feePerKb = opts.feePerKb;
x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos;
x.addressType = opts.addressType || (x.walletN > 1 ? Constants.SCRIPT_TYPES.P2SH : Constants.SCRIPT_TYPES.P2PKH);
$.checkState(
Utils.checkValueInCollection(x.addressType, Constants.SCRIPT_TYPES),
'Failed state: addressType not in ScriptTypes at <create()>'
);
x.customData = opts.customData;
x.amount = opts.amount ? opts.amount : x.getTotalAmount();
x.setInputs(opts.inputs);
x.fee = opts.fee;
if (x.version === 4) {
x.lockUntilBlockHeight = opts.lockUntilBlockHeight;
}
// Coin specific features
// ETH
x.gasPrice = opts.gasPrice;
x.from = opts.from;
x.nonce = opts.nonce;
x.gasLimit = opts.gasLimit; // Backward compatibility for BWC <= 8.9.0
x.data = opts.data; // Backward compatibility for BWC <= 8.9.0
x.tokenAddress = opts.tokenAddress;
x.isTokenSwap = opts.isTokenSwap;
x.multisigContractAddress = opts.multisigContractAddress;
// XRP
x.destinationTag = opts.destinationTag;
x.invoiceID = opts.invoiceID;
return x;
}
static fromObj(obj) {
if (!(obj.version >= 3)) {
return TxProposalLegacy.fromObj(obj);
}
const x = new TxProposal();
x.isDonation = obj.isDonation;
x.receiveLotusAddress = obj.receiveLotusAddress;
x.version = obj.version;
x.createdOn = obj.createdOn;
x.id = obj.id;
x.walletId = obj.walletId;
x.creatorId = obj.creatorId;
x.coin = obj.coin || Defaults.COIN;
x.chain = obj.chain ? obj.chain : ChainService.getChain(x.coin);
x.network = obj.network;
x.outputs = obj.outputs;
x.amount = obj.amount;
x.message = obj.message;
x.messageOnChain = obj.messageOnChain;
x.payProUrl = obj.payProUrl;
x.changeAddress = obj.changeAddress;
x.inputs = obj.inputs;
x.walletM = obj.walletM;
x.walletN = obj.walletN;
x.requiredSignatures = obj.requiredSignatures;
x.requiredRejections = obj.requiredRejections;
x.status = obj.status;
x.txid = obj.txid;
x.broadcastedOn = obj.broadcastedOn;
x.inputPaths = obj.inputPaths;
x.actions = _.map(obj.actions, action => {
return TxProposalAction.fromObj(action);
});
x.outputOrder = obj.outputOrder;
x.fee = obj.fee;
x.feeLevel = obj.feeLevel;
x.feePerKb = obj.feePerKb;
x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos;
x.addressType = obj.addressType;
x.customData = obj.customData;
x.proposalSignature = obj.proposalSignature;
x.signingMethod = obj.signingMethod;
x.proposalSignaturePubKey = obj.proposalSignaturePubKey;
x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig;
x.lockUntilBlockHeight = obj.lockUntilBlockHeight;
// ETH
x.gasPrice = obj.gasPrice;
x.from = obj.from;
x.nonce = obj.nonce;
x.gasLimit = obj.gasLimit; // Backward compatibility for BWC <= 8.9.0
x.data = obj.data; // Backward compatibility for BWC <= 8.9.0
x.tokenAddress = obj.tokenAddress;
x.isTokenSwap = obj.isTokenSwap;
x.multisigContractAddress = obj.multisigContractAddress;
x.multisigTxId = obj.multisigTxId;
// XRP
x.destinationTag = obj.destinationTag;
x.invoiceID = obj.invoiceID;
if (x.status == 'broadcasted') {
x.raw = obj.raw;
}
return x;
}
toObject() {
const x: any = _.cloneDeep(this);
x.isPending = this.isPending();
return x;
}
setInputs(inputs) {
this.inputs = inputs || [];
this.inputPaths = _.map(inputs, 'path') || [];
}
_updateStatus() {
if (this.status != 'pending') return;
if (this.isRejected()) {
this.status = 'rejected';
} else if (this.isAccepted()) {
this.status = 'accepted';
}
}
getCurrentSignatures() {
const acceptedActions = _.filter(this.actions, a => {
return a.type == 'accept';
});
return _.map(acceptedActions, x => {
return {
signatures: x.signatures,
xpub: x.xpub
};
});
}
getRawTx() {
const t = ChainService.getBitcoreTx(this);
return t.uncheckedSerialize();
}
/**
* getTotalAmount
*
* @return {Number} total amount of all outputs excluding change output
*/
getTotalAmount() {
return _.sumBy(this.outputs, 'amount');
}
/**
* getActors
*
* @return {String[]} copayerIds that performed actions in this proposal (accept / reject)
*/
getActors() {
return _.map(this.actions, 'copayerId');
}
/**
* getApprovers
*
* @return {String[]} copayerIds that approved the tx proposal (accept)
*/
getApprovers() {
return _.map(
_.filter(this.actions, a => {
return a.type == 'accept';
}),
'copayerId'
);
}
/**
* getActionBy
*
* @param {String} copayerId
* @return {Object} type / createdOn
*/
getActionBy(copayerId) {
return _.find(this.actions, {
copayerId
});
}
addAction(copayerId, type, comment, signatures?, xpub?) {
const action = TxProposalAction.create({
copayerId,
type,
signatures,
xpub,
comment
});
this.actions.push(action);
this._updateStatus();
}
sign(copayerId, signatures, xpub) {
try {
// Tests signatures are OK
const tx = ChainService.getBitcoreTx(this);
ChainService.addSignaturesToBitcoreTx(
this.chain,
tx,
this.inputs,
this.inputPaths,
signatures,
xpub,
this.signingMethod
);
this.addAction(copayerId, 'accept', null, signatures, xpub);
if (this.status == 'accepted') {
this.raw = tx.uncheckedSerialize();
this.txid = tx.id;
}
return true;
} catch (e) {
logger.debug(e);
return false;
}
}
reject(copayerId, reason) {
this.addAction(copayerId, 'reject', reason);
}
isTemporary() {
return this.status == 'temporary';
}
isPending() {
return !_.includes(['temporary', 'broadcasted', 'rejected'], this.status);
}
isAccepted() {
const votes = _.countBy(this.actions, 'type');
return votes['accept'] >= this.requiredSignatures;
}
isRejected() {
const votes = _.countBy(this.actions, 'type');
return votes['reject'] >= this.requiredRejections;
}
isBroadcasted() {
return this.status == 'broadcasted';
}
setBroadcasted() {
$.checkState(this.txid, 'Failed state: this.txid at <setBroadcasted()>');
this.status = 'broadcasted';
this.broadcastedOn = Math.floor(Date.now() / 1000);
}
}