UNPKG

11.4 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 { common } from '@bitgo/sdk-core';
13import * as utxolib 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 Bluebird.resolve(
135 this.bitgo.get(this.url()).result()
136 ).then(function (res) {
137 self.pendingApproval = res;
138 return self;
139 }).nodeify(callback);
140};
141
142//
143// Helper function to ensure that self.wallet is set
144//
145PendingApproval.prototype.populateWallet = function () {
146 const self = this;
147 if (!self.wallet) {
148 return self.bitgo.wallets().get({ id: self.info().transactionRequest.sourceWallet })
149 .then(function (wallet) {
150 if (!wallet) {
151 throw new Error('unexpected - unable to get wallet using sourcewallet');
152 }
153 self.wallet = wallet;
154 });
155 }
156
157 if (self.wallet.id() !== self.info().transactionRequest.sourceWallet) {
158 throw new Error('unexpected source wallet for pending approval');
159 }
160
161 return Promise.resolve(); // otherwise returns undefined
162};
163
164//
165// helper function to recreate and sign a transaction on a wallet
166// we should hopefully be able to move this logic server side soon
167//
168PendingApproval.prototype.recreateAndSignTransaction = function (params, callback) {
169 params = _.extend({}, params);
170 common.validateParams(params, ['txHex'], [], callback);
171
172 const transaction = utxolib.bitgo.createTransactionFromHex(params.txHex, utxolib.networks.bitcoin);
173 if (!transaction.outs) {
174 throw new Error('transaction had no outputs or failed to parse successfully');
175 }
176
177 const network = utxolib.networks[common.Environments[this.bitgo.getEnv()].network];
178 params.recipients = {};
179
180 const self = this;
181
182 return Bluebird.try(function () {
183 if (self.info().transactionRequest.recipients) {
184 // recipients object found on the pending approvals - use it
185 params.recipients = self.info().transactionRequest.recipients;
186 return;
187 }
188 if (transaction.outs.length <= 2) {
189 transaction.outs.forEach(function (out) {
190 const outAddress = utxolib.address.fromOutputScript(out.script, network);
191 if (self.info().transactionRequest.destinationAddress === outAddress) {
192 // If this is the destination, then spend to it
193 params.recipients[outAddress] = out.value;
194 }
195 });
196 return;
197 }
198
199 // This looks like a sendmany
200 // Attempt to figure out the outputs by choosing all outputs that were not going back to the wallet as change addresses
201 return self.wallet.addresses({ chain: 1, sort: -1, limit: 500 })
202 .then(function (result) {
203 const changeAddresses = _.keyBy(result.addresses, 'address');
204 transaction.outs.forEach(function (out) {
205 const outAddress = utxolib.address.fromOutputScript(out.script, network);
206 if (!changeAddresses[outAddress]) {
207 // If this is not a change address, then spend to it
208 params.recipients[outAddress] = out.value;
209 }
210 });
211 });
212 })
213 .then(function () {
214 return self.wallet.createAndSignTransaction(params);
215 });
216};
217
218//
219// constructApprovalTx
220// constructs/signs a transaction for this pending approval, returning the txHex (but not sending it)
221//
222PendingApproval.prototype.constructApprovalTx = function (params, callback) {
223 params = params || {};
224 common.validateParams(params, [], ['walletPassphrase'], callback);
225
226 if (this.type() === 'transactionRequest' && !(params.walletPassphrase || params.xprv)) {
227 throw new Error('wallet passphrase or xprv required to approve a transactionRequest');
228 }
229
230 if (params.useOriginalFee) {
231 if (!_.isBoolean(params.useOriginalFee)) {
232 throw new Error('invalid type for useOriginalFeeRate');
233 }
234 if (params.fee || params.feeRate || params.feeTxConfirmTarget) {
235 throw new Error('cannot specify a fee/feerate/feeTxConfirmTarget as well as useOriginalFee');
236 }
237 }
238
239 const self = this;
240 return Bluebird.try(function () {
241 if (self.type() === 'transactionRequest') {
242 const extendParams: any = { txHex: self.info().transactionRequest.transaction };
243 if (params.useOriginalFee) {
244 extendParams.fee = self.info().transactionRequest.fee;
245 }
246 return self.populateWallet()
247 .then(function () {
248 return self.recreateAndSignTransaction(_.extend(params, extendParams));
249 });
250 }
251 });
252};
253
254//
255// approve
256// sets the pending approval to an approved state
257//
258PendingApproval.prototype.approve = function (params, callback) {
259 params = params || {};
260 common.validateParams(params, [], ['walletPassphrase', 'otp'], callback);
261
262 let canRecreateTransaction = true;
263 if (this.type() === 'transactionRequest') {
264 if (!params.walletPassphrase && !params.xprv) {
265 canRecreateTransaction = false;
266 }
267
268 // check the wallet balance and compare it with the transaction amount and fee
269 if (_.isUndefined(params.forceRecreate) && _.isObject(_.get(this, 'wallet.wallet'))) {
270 const requestedAmount = this.pendingApproval.info.transactionRequest.requestedAmount || 0;
271 const walletBalance = this.wallet.wallet.spendableBalance;
272 const delta = Math.abs(requestedAmount - walletBalance);
273 if (delta <= 10000) {
274 // it's a sweep because we're within 10k satoshis of the wallet balance
275 canRecreateTransaction = false;
276 }
277 }
278 }
279
280 const self = this;
281 return Bluebird.try(function () {
282 if (self.type() === 'transactionRequest') {
283 if (params.tx) {
284 // the approval tx was reconstructed and explicitly specified - pass it through
285 return {
286 tx: params.tx,
287 };
288 }
289
290 // this user may not have spending privileges or a passphrase may not have been passed in
291 if (!canRecreateTransaction) {
292 return {
293 tx: self.info().transactionRequest.transaction,
294 };
295 }
296
297 return self.populateWallet()
298 .then(function () {
299 const recreationParams = _.extend({}, params, { txHex: self.info().transactionRequest.transaction }, self.info().transactionRequest.buildParams);
300 // delete the old build params because we want 'recreateAndSign' to recreate the transaction
301 delete recreationParams.fee;
302 delete recreationParams.unspents;
303 delete recreationParams.txInfo;
304 delete recreationParams.estimatedSize;
305 delete recreationParams.changeAddresses;
306 return self.recreateAndSignTransaction(recreationParams);
307 });
308 }
309 })
310 .then(function (transaction) {
311 const approvalParams: any = { state: 'approved', otp: params.otp };
312 if (transaction) {
313 approvalParams.tx = transaction.tx;
314 }
315 return Bluebird.resolve(
316 self.bitgo.put(self.url()).send(approvalParams).result()
317 ).nodeify(callback);
318 })
319 .catch(function (error) {
320 if (!canRecreateTransaction &&
321 (
322 error.message.indexOf('could not find unspent output for input') !== -1 ||
323 error.message.indexOf('transaction conflicts with an existing transaction in the send queue') !== -1)
324 ) {
325 throw new Error('unspents expired, wallet passphrase or xprv required to recreate transaction');
326 }
327 if (_.isUndefined(params.forceRecreate) && error.message.indexOf('could not find unspent output for input') !== -1 ) {
328 // if the unspents can't be found, we must retry with a newly constructed transaction, so we delete the tx and try again
329 // deleting params.tx will force the code to reach the 'recreateAndSignTransaction' function
330 delete params.tx;
331 params.forceRecreate = true;
332 self.approve(params, callback);
333 } else {
334 throw error;
335 }
336 });
337};
338
339//
340// rejected
341// sets the pending approval to a rejected state
342//
343PendingApproval.prototype.reject = function (params, callback) {
344 params = params || {};
345 common.validateParams(params, [], [], callback);
346
347 return Bluebird.resolve(
348 this.bitgo.put(this.url()).send({ state: 'rejected' }).result()
349 ).nodeify(callback);
350};
351
352//
353// cancel
354// rejects the pending approval
355//
356PendingApproval.prototype.cancel = function (params, callback) {
357 return this.reject(params, callback);
358};
359
360export = PendingApproval;