UNPKG

11.2 kBPlain TextView Raw
1/**
2 * @hidden
3 */
4
5/**
6 */
7// Pending Approval Object
8// Handles approving, rejecting and getting information on pending approvals
9//
10// Copyright 2015, BitGo, Inc. All Rights Reserved.
11//
12import * as common from './common';
13import * as bitcoin from '@bitgo/utxo-lib';
14
15import * as Bluebird from 'bluebird';
16import * as _ from 'lodash';
17
18//
19// Constructor
20//
21const PendingApproval = function(bitgo, pendingApproval, wallet) {
22 this.bitgo = bitgo;
23 this.pendingApproval = pendingApproval;
24 this.wallet = wallet;
25};
26
27//
28// id
29// Get the id of this pending approval.
30//
31PendingApproval.prototype.id = function() {
32 return this.pendingApproval.id;
33};
34
35//
36// ownerType
37// Get the owner type (wallet or enterprise)
38// Pending approvals can be approved or modified by different scopes (depending on how they were created)
39// If a pending approval is owned by a wallet, then it can be approved by administrators of the wallet
40// If a pending approval is owned by an enterprise, then it can be approved by administrators of the enterprise
41//
42PendingApproval.prototype.ownerType = function(params, callback) {
43 params = params || {};
44 common.validateParams(params, [], [], callback);
45
46 if (this.pendingApproval.walletId) {
47 return 'wallet';
48 } else if (this.pendingApproval.enterprise) {
49 return 'enterprise';
50 } else {
51 throw new Error('unexpected pending approval owner: neither walletId nor enterprise was present');
52 }
53};
54
55//
56// walletId
57// Get the wallet ID that owns / is associated with the pending approval
58//
59PendingApproval.prototype.walletId = function() {
60 return this.pendingApproval.walletId;
61};
62
63//
64// enterpriseId
65// Get the enterprise ID that owns / is associated with the pending approval
66//
67PendingApproval.prototype.enterpriseId = function() {
68 return this.pendingApproval.enterprise;
69};
70
71//
72// state
73// Get the state of the pending approval
74//
75PendingApproval.prototype.state = function() {
76 return this.pendingApproval.state;
77};
78
79//
80// creator
81// Get the id of the user that performed the action resulting in this pending approval
82//
83PendingApproval.prototype.creator = function() {
84 return this.pendingApproval.creator;
85};
86
87//
88// type
89// Get the type of the pending approval (what it approves)
90// Example: transactionRequest, tagUpdateRequest, policyRuleRequest
91//
92PendingApproval.prototype.type = function() {
93 if (!this.pendingApproval.info) {
94 throw new Error('pending approval info is not available');
95 }
96 return this.pendingApproval.info.type;
97};
98
99//
100// type
101// Get information about the pending approval
102//
103PendingApproval.prototype.info = function() {
104 return this.pendingApproval.info;
105};
106
107//
108// approvalsRequired
109// get the number of approvals that are required for this pending approval to be approved.
110// Defaults to 1 if approvalsRequired doesn't exist on the object
111//
112PendingApproval.prototype.approvalsRequired = function() {
113 return this.pendingApproval.approvalsRequired || 1;
114};
115
116//
117// url
118// Gets the url for this pending approval
119//
120PendingApproval.prototype.url = function(extra) {
121 extra = extra || '';
122 return this.bitgo.url('/pendingapprovals/' + this.id() + extra);
123};
124
125//
126// get
127// Refetches this pending approval and returns it
128//
129PendingApproval.prototype.get = function(params, callback) {
130 params = params || {};
131 common.validateParams(params, [], [], callback);
132
133 const self = this;
134 return this.bitgo.get(this.url())
135 .result()
136 .then(function(res) {
137 self.pendingApproval = res;
138 return self;
139 })
140 .nodeify(callback);
141};
142
143//
144// Helper function to ensure that self.wallet is set
145//
146PendingApproval.prototype.populateWallet = function() {
147 const self = this;
148 if (!self.wallet) {
149 return self.bitgo.wallets().get({ id: self.info().transactionRequest.sourceWallet })
150 .then(function(wallet) {
151 if (!wallet) {
152 throw new Error('unexpected - unable to get wallet using sourcewallet');
153 }
154 self.wallet = wallet;
155 });
156 }
157
158 if (self.wallet.id() !== self.info().transactionRequest.sourceWallet) {
159 throw new Error('unexpected source wallet for pending approval');
160 }
161
162 return Promise.resolve(); // otherwise returns undefined
163};
164
165//
166// helper function to recreate and sign a transaction on a wallet
167// we should hopefully be able to move this logic server side soon
168//
169PendingApproval.prototype.recreateAndSignTransaction = function(params, callback) {
170 params = _.extend({}, params);
171 common.validateParams(params, ['txHex'], [], callback);
172
173 const transaction = bitcoin.Transaction.fromHex(params.txHex);
174 if (!transaction.outs) {
175 throw new Error('transaction had no outputs or failed to parse successfully');
176 }
177
178 const network = bitcoin.networks[common.Environments[this.bitgo.getEnv()].network];
179 params.recipients = {};
180
181 const self = this;
182
183 return Bluebird.try(function() {
184 if (self.info().transactionRequest.recipients) {
185 // recipients object found on the pending approvals - use it
186 params.recipients = self.info().transactionRequest.recipients;
187 return;
188 }
189 if (transaction.outs.length <= 2) {
190 transaction.outs.forEach(function(out) {
191 const outAddress = bitcoin.address.fromOutputScript(out.script, network).toBase58Check();
192 if (self.info().transactionRequest.destinationAddress === outAddress) {
193 // If this is the destination, then spend to it
194 params.recipients[outAddress] = out.value;
195 }
196 });
197 return;
198 }
199
200 // This looks like a sendmany
201 // Attempt to figure out the outputs by choosing all outputs that were not going back to the wallet as change addresses
202 return self.wallet.addresses({ chain: 1, sort: -1, limit: 500 })
203 .then(function(result) {
204 const changeAddresses = _.keyBy(result.addresses, 'address');
205 transaction.outs.forEach(function(out) {
206 const outAddress = bitcoin.address.fromOutputScript(out.script, network).toBase58Check();
207 if (!changeAddresses[outAddress]) {
208 // If this is not a change address, then spend to it
209 params.recipients[outAddress] = out.value;
210 }
211 });
212 });
213 })
214 .then(function() {
215 return self.wallet.createAndSignTransaction(params);
216 });
217};
218
219//
220// constructApprovalTx
221// constructs/signs a transaction for this pending approval, returning the txHex (but not sending it)
222//
223PendingApproval.prototype.constructApprovalTx = function(params, callback) {
224 params = params || {};
225 common.validateParams(params, [], ['walletPassphrase'], callback);
226
227 if (this.type() === 'transactionRequest' && !(params.walletPassphrase || params.xprv)) {
228 throw new Error('wallet passphrase or xprv required to approve a transactionRequest');
229 }
230
231 if (params.useOriginalFee) {
232 if (!_.isBoolean(params.useOriginalFee)) {
233 throw new Error('invalid type for useOriginalFeeRate');
234 }
235 if (params.fee || params.feeRate || params.feeTxConfirmTarget) {
236 throw new Error('cannot specify a fee/feerate/feeTxConfirmTarget as well as useOriginalFee');
237 }
238 }
239
240 const self = this;
241 return Bluebird.try(function() {
242 if (self.type() === 'transactionRequest') {
243 const extendParams: any = { txHex: self.info().transactionRequest.transaction };
244 if (params.useOriginalFee) {
245 extendParams.fee = self.info().transactionRequest.fee;
246 }
247 return self.populateWallet()
248 .then(function() {
249 return self.recreateAndSignTransaction(_.extend(params, extendParams));
250 });
251 }
252 });
253};
254
255//
256// approve
257// sets the pending approval to an approved state
258//
259PendingApproval.prototype.approve = function(params, callback) {
260 params = params || {};
261 common.validateParams(params, [], ['walletPassphrase', 'otp'], callback);
262
263 let canRecreateTransaction = true;
264 if (this.type() === 'transactionRequest') {
265 if (!params.walletPassphrase && !params.xprv) {
266 canRecreateTransaction = false;
267 }
268
269 // check the wallet balance and compare it with the transaction amount and fee
270 if (_.isUndefined(params.forceRecreate) && _.isObject(_.get(this, 'wallet.wallet'))) {
271 const requestedAmount = this.pendingApproval.info.transactionRequest.requestedAmount || 0;
272 const walletBalance = this.wallet.wallet.spendableBalance;
273 const delta = Math.abs(requestedAmount - walletBalance);
274 if (delta <= 10000) {
275 // it's a sweep because we're within 10k satoshis of the wallet balance
276 canRecreateTransaction = false;
277 }
278 }
279 }
280
281 const self = this;
282 return Bluebird.try(function() {
283 if (self.type() === 'transactionRequest') {
284 if (params.tx) {
285 // the approval tx was reconstructed and explicitly specified - pass it through
286 return {
287 tx: params.tx
288 };
289 }
290
291 // this user may not have spending privileges or a passphrase may not have been passed in
292 if (!canRecreateTransaction) {
293 return {
294 tx: self.info().transactionRequest.transaction
295 };
296 }
297
298 return self.populateWallet()
299 .then(function() {
300 const recreationParams = _.extend({}, params, { txHex: self.info().transactionRequest.transaction }, self.info().transactionRequest.buildParams);
301 // delete the old build params because we want 'recreateAndSign' to recreate the transaction
302 delete recreationParams.fee;
303 delete recreationParams.unspents;
304 delete recreationParams.txInfo;
305 delete recreationParams.estimatedSize;
306 delete recreationParams.changeAddresses;
307 return self.recreateAndSignTransaction(recreationParams);
308 });
309 }
310 })
311 .then(function(transaction) {
312 const approvalParams: any = { state: 'approved', otp: params.otp };
313 if (transaction) {
314 approvalParams.tx = transaction.tx;
315 }
316 return self.bitgo.put(self.url())
317 .send(approvalParams)
318 .result()
319 .nodeify(callback);
320 })
321 .catch(function(error) {
322 if (!canRecreateTransaction &&
323 (
324 error.message.indexOf('could not find unspent output for input') !== -1 ||
325 error.message.indexOf('transaction conflicts with an existing transaction in the send queue') !== -1)
326 ) {
327 throw new Error('unspents expired, wallet passphrase or xprv required to recreate transaction');
328 }
329 if (_.isUndefined(params.forceRecreate) && error.message.indexOf('could not find unspent output for input') !== -1 ) {
330 // if the unspents can't be found, we must retry with a newly constructed transaction, so we delete the tx and try again
331 // deleting params.tx will force the code to reach the 'recreateAndSignTransaction' function
332 delete params.tx;
333 params.forceRecreate = true;
334 self.approve(params, callback);
335 } else {
336 throw error;
337 }
338 });
339};
340
341//
342// rejected
343// sets the pending approval to a rejected state
344//
345PendingApproval.prototype.reject = function(params, callback) {
346 params = params || {};
347 common.validateParams(params, [], [], callback);
348
349 return this.bitgo.put(this.url())
350 .send({ state: 'rejected' })
351 .result()
352 .nodeify(callback);
353};
354
355//
356// cancel
357// rejects the pending approval
358//
359PendingApproval.prototype.cancel = function(params, callback) {
360 return this.reject(params, callback);
361};
362
363export = PendingApproval;