UNPKG

9.64 kBPlain TextView Raw
1/**
2 * @hidden
3 */
4
5/**
6 */
7//
8// TravelRule Object
9// BitGo accessor for a specific enterprise
10//
11// Copyright 2014, BitGo, Inc. All Rights Reserved.
12//
13
14import * as bip32 from 'bip32';
15import * as utxolib from '@bitgo/utxo-lib';
16import * as Bluebird from 'bluebird';
17import * as _ from 'lodash';
18
19import { common } from '@bitgo/sdk-core';
20import { getNetwork, makeRandomKey } from './bitcoin';
21import { sanitizeLegacyPath } from '@bitgo/sdk-api';
22import { getSharedSecret } from './ecdh';
23
24interface DecryptReceivedTravelRuleOptions {
25 tx?: {
26 receivedTravelInfo?: {
27 toPubKeyPath: string;
28 fromPubKey: string;
29 encryptedTravelInfo: string;
30 travelInfo: string;
31 transactionId: string;
32 outputIndex: number;
33 }[];
34 };
35 keychain?: {
36 xprv?: string;
37 };
38 hdnode?: bip32.BIP32Interface;
39}
40
41interface Recipient {
42 enterprise: string;
43 pubKey: string;
44 outputIndex: string;
45}
46
47//
48// Constructor
49//
50const TravelRule = function (bitgo) {
51 this.bitgo = bitgo;
52};
53
54TravelRule.prototype.url = function (extra) {
55 extra = extra || '';
56 return this.bitgo.url('/travel/' + extra);
57};
58
59
60/**
61 * Get available travel-rule info recipients for a transaction
62 * @param params
63 * txid: transaction id
64 * @param callback
65 * @returns {*}
66 */
67TravelRule.prototype.getRecipients = function (params, callback) {
68 params = params || {};
69 params.txid = params.txid || params.hash;
70 common.validateParams(params, ['txid'], [], callback);
71
72 const url = this.url(params.txid + '/recipients');
73 return Bluebird.resolve(
74 this.bitgo.get(url).result('recipients')
75 ).nodeify(callback);
76};
77
78TravelRule.prototype.validateTravelInfo = function (info) {
79 const fields = {
80 amount: { type: 'number' },
81 toAddress: { type: 'string' },
82 toEnterprise: { type: 'string' },
83 fromUserName: { type: 'string' },
84 fromUserAccount: { type: 'string' },
85 fromUserAddress: { type: 'string' },
86 toUserName: { type: 'string' },
87 toUserAccount: { type: 'string' },
88 toUserAddress: { type: 'string' },
89 extra: { type: 'object' },
90 };
91
92 _.forEach(fields, function (field: any, fieldName) {
93 // No required fields yet -- should there be?
94 if (field.required) {
95 if (info[fieldName] === undefined) {
96 throw new Error('missing required field ' + fieldName + ' in travel info');
97 }
98 }
99 if (info[fieldName] && typeof(info[fieldName]) !== field.type) {
100 throw new Error('incorrect type for field ' + fieldName + ' in travel info, expected ' + field.type);
101 }
102 });
103
104 // Strip out any other fields we don't know about
105 const result = _.pick(info, _.keys(fields));
106 if (_.isEmpty(result)) {
107 throw new Error('empty travel data');
108 }
109 return result;
110};
111
112/**
113 * Takes a transaction object as returned by getTransaction or listTransactions, along
114 * with a keychain (or hdnode object), and attempts to decrypt any encrypted travel
115 * info included in the transaction's receivedTravelInfo field.
116 * Parameters:
117 * tx: a transaction object
118 * keychain: keychain object (with xprv)
119 * Returns:
120 * the tx object, augmented with decrypted travelInfo fields
121 */
122TravelRule.prototype.decryptReceivedTravelInfo = function (params: DecryptReceivedTravelRuleOptions = {}) {
123 const tx = params.tx;
124 if (!_.isObject(tx)) {
125 throw new Error('expecting tx param to be object');
126 }
127
128 if (!tx.receivedTravelInfo || !tx.receivedTravelInfo.length) {
129 return tx;
130 }
131
132 const keychain = params.keychain;
133 if (!_.isObject(keychain) || !_.isString(keychain.xprv)) {
134 throw new Error('expecting keychain param with xprv');
135 }
136 const hdNode = bip32.fromBase58(keychain.xprv);
137
138 tx.receivedTravelInfo.forEach((info) => {
139 const key = hdNode.derivePath(sanitizeLegacyPath(info.toPubKeyPath));
140 const secret = getSharedSecret(key, Buffer.from(info.fromPubKey, 'hex')).toString('hex');
141 try {
142 const decrypted = this.bitgo.decrypt({
143 input: info.encryptedTravelInfo,
144 password: secret,
145 });
146 info.travelInfo = JSON.parse(decrypted);
147 } catch (err) {
148 console.error('failed to decrypt or parse travel info for ', info.transactionId + ':' + info.outputIndex);
149 }
150 });
151
152 return tx;
153};
154
155TravelRule.prototype.prepareParams = function (params) {
156 params = params || {};
157 params.txid = params.txid || params.hash;
158 common.validateParams(params, ['txid'], ['fromPrivateInfo']);
159 const txid = params.txid;
160 const recipient: Recipient | undefined = params.recipient;
161 let travelInfo = params.travelInfo;
162 if (!recipient || !_.isObject(recipient)) {
163 throw new Error('invalid or missing recipient');
164 }
165 if (!travelInfo || !_.isObject(travelInfo)) {
166 throw new Error('invalid or missing travelInfo');
167 }
168 if (!params.noValidate) {
169 travelInfo = this.validateTravelInfo(travelInfo);
170 }
171
172 // Fill in toEnterprise if not already filled
173 if (!travelInfo.toEnterprise && recipient.enterprise) {
174 travelInfo.toEnterprise = recipient.enterprise;
175 }
176
177 // If a key was not provided, create a new random key
178 let fromKey = params.fromKey && utxolib.ECPair.fromWIF(params.fromKey, getNetwork() as utxolib.BitcoinJSNetwork);
179 if (!fromKey) {
180 fromKey = makeRandomKey();
181 }
182
183 // Compute the shared key for encryption
184 const sharedSecret = getSharedSecret(fromKey, Buffer.from(recipient.pubKey, 'hex')).toString('hex');
185
186 // JSON-ify and encrypt the payload
187 const travelInfoJSON = JSON.stringify(travelInfo);
188 const encryptedTravelInfo = this.bitgo.encrypt({
189 input: travelInfoJSON,
190 password: sharedSecret,
191 });
192
193 const result = {
194 txid: txid,
195 outputIndex: recipient.outputIndex,
196 toPubKey: recipient.pubKey,
197 fromPubKey: fromKey.publicKey.toString('hex'),
198 encryptedTravelInfo: encryptedTravelInfo,
199 fromPrivateInfo: undefined,
200 };
201
202 if (params.fromPrivateInfo) {
203 result.fromPrivateInfo = params.fromPrivateInfo;
204 }
205
206 return result;
207};
208
209/**
210 * Send travel data to the server for a transaction
211 */
212TravelRule.prototype.send = function (params, callback) {
213 params = params || {};
214 params.txid = params.txid || params.hash;
215 common.validateParams(params, ['txid', 'toPubKey', 'encryptedTravelInfo'], ['fromPubKey', 'fromPrivateInfo'], callback);
216
217 if (!_.isNumber(params.outputIndex)) {
218 throw new Error('invalid outputIndex');
219 }
220
221 return Bluebird.resolve(
222 this.bitgo.post(this.url(params.txid + '/' + params.outputIndex)).send(params).result()
223 ).nodeify(callback);
224};
225
226/**
227 * Send multiple travel rule infos for the outputs of a single transaction.
228 * Parameters:
229 * - txid (or hash): txid of the transaction (must be a sender of the tx)
230 * - travelInfos: array of travelInfo objects which look like the following:
231 * {
232 * outputIndex: number, // tx output index
233 * fromUserName: string, // name of the sending user
234 * fromUserAccount: string, // account id of the sending user
235 * fromUserAddress: string, // mailing address of the sending user
236 * toUserName: string, // name of the receiving user
237 * toUserAccount: string, // account id of the receiving user
238 * toUserAddress: string // mailing address of the receiving user
239 * }
240 * All fields aside from outputIndex are optional, but at least one must
241 * be defined.
242 *
243 * It is not necessary to provide travelInfo for all output indices.
244 * End-to-end encryption of the travel info is handled automatically by this method.
245 *
246 */
247TravelRule.prototype.sendMany = function (params, callback) {
248 params = params || {};
249 params.txid = params.txid || params.hash;
250 common.validateParams(params, ['txid'], callback);
251
252 const travelInfos = params.travelInfos;
253 if (!_.isArray(travelInfos)) {
254 throw new Error('expected parameter travelInfos to be array');
255 }
256
257 const self = this;
258 const travelInfoMap = _(travelInfos)
259 .keyBy('outputIndex')
260 .mapValues(function (travelInfo) {
261 return self.validateTravelInfo(travelInfo);
262 })
263 .value();
264
265 return self.getRecipients({ txid: params.txid })
266 .then(function (recipients) {
267
268 // Build up data to post
269 const sendParamsList: any[] = [];
270 // don't regenerate a new random key for each recipient
271 const fromKey = params.fromKey || makeRandomKey().toWIF();
272
273 recipients.forEach(function (recipient) {
274 const outputIndex = recipient.outputIndex;
275 const info = travelInfoMap[outputIndex];
276 if (info) {
277 if (info.amount && info.amount !== recipient.amount) {
278 throw new Error('amount did not match for output index ' + outputIndex);
279 }
280 const sendParams = self.prepareParams({
281 txid: params.txid,
282 recipient: recipient,
283 travelInfo: info,
284 fromKey: fromKey,
285 noValidate: true, // don't re-validate
286 });
287 sendParamsList.push(sendParams);
288 }
289 });
290
291 const result: {
292 matched: number;
293 results: {
294 result?: any;
295 error?: string;
296 }[];
297 } = {
298 matched: sendParamsList.length,
299 results: [],
300 };
301
302 const sendSerial = function () {
303 const sendParams = sendParamsList.shift();
304 if (!sendParams) {
305 return result;
306 }
307 return self.send(sendParams)
308 .then(function (res) {
309 result.results.push({ result: res });
310 return sendSerial();
311 })
312 .catch(function (err) {
313 result.results.push({ error: err.toString() });
314 return sendSerial();
315 });
316 };
317
318 return sendSerial();
319 });
320};
321
322module.exports = TravelRule;