· 6 years ago · Jan 17, 2020, 12:48 PM
1const StellarSdk = require('zagg-stellar-sdk');
2const fetch = require('node-fetch');
3const DLTW = require('./dltWrapper');
4const Account = require('../model/Account');
5const { Network, Keypair, TransactionBuilder, Operation, Memo, Server, Asset } = StellarSdk;
6const { TimeoutInfinite } = TransactionBuilder;
7
8const {
9 MISSING_ROOT_KEY,
10 MISSING_PASSPHRASE,
11 MISSING_URL,
12 MISSING_DLT_URL,
13 MISSING_TX_URL,
14 MISSING_ALIAS,
15 MISSING_SENDER_KEY,
16 MISSING_RECEIVER_KEY,
17 DEFAULT_BALANCE,
18 DEFAULT_MAX_TX_SET_SIZE,
19 DEFAULT_INTERVAL,
20 DLT_SERVER_DOWN } = require('../utils/constants');
21
22function sleep(ms) {
23 return new Promise(resolve => setTimeout(resolve, ms));
24}
25
26module.exports = function (config) {
27
28 // Local variables
29 // ------------------------------------------------------------------------
30
31 let { root, passphrase, url, balance, maxTxSetSize, interval } = config;
32 let mConfigBalance = {};
33 let mMaxTxSetSize;
34 let mInterval = {};
35
36 // sanity checks
37 // ------------------------------------------------------------------------
38
39 if (!root || root == "") {
40 throw new Error(MISSING_ROOT_KEY);
41 }
42
43 if (!passphrase || passphrase == "") {
44 throw new Error(MISSING_PASSPHRASE);
45 }
46
47 if (!url || Object.keys(url).length == 0) {
48 throw new Error(MISSING_URL);
49 }
50
51 if (!url.dltURL || url.dltURL == "") {
52 throw new Error(MISSING_DLT_URL);
53 }
54
55 if (!url.transactionURL || url.transactionURL == "") {
56 throw new Error(MISSING_TX_URL);
57 }
58
59 if (!balance || Object.keys(balance).length == 0) {
60 mConfigBalance = DEFAULT_BALANCE;
61 } else {
62 mConfigBalance = { ...DEFAULT_BALANCE, ...balance };
63 }
64
65 if (maxTxSetSize == undefined || maxTxSetSize == null || maxTxSetSize <= 0) {
66 mMaxTxSetSize = DEFAULT_MAX_TX_SET_SIZE;
67 }
68
69 if (!interval || Object.keys(interval).length == 0) {
70 mInterval = DEFAULT_INTERVAL;
71 } else {
72 mInterval = { ...DEFAULT_INTERVAL, ...interval };
73 }
74
75 // Local Constants
76 // -----------------------------------------------------------------------
77
78 // Stellar network connection
79 Network.use(new Network(passphrase));
80
81 // Stellar API connection
82 const DLT = DLTW(url.dltURL);
83
84 // Stellar core tx
85 const transactionURL = url.transactionURL
86
87 // BASE_FEE
88 const BASE_FEE = mConfigBalance.baseFee;
89 const BASE_RESERVE = mConfigBalance.baseReserve;
90
91 // Root Account
92 const rootAccount = Keypair.fromSecret(root);
93
94 // private method
95 // ------------------------------------------------------------------------
96
97 const loadAccount = (accountId) => {
98 return new Promise(async (resolve, reject) => {
99 const response = await DLT.getAccountById(accountId);
100 if (!response || !response.message || !response.message.account) {
101 return reject(new Error(`${accountId} not found in DLT`));
102 }
103 const { account } = response.message;
104 return resolve(new Account(account.accountid, account.seqnum, account.balance));
105 })
106 }
107
108 const buildSubmitTx = blob => transactionURL + '/tx?blob=' + blob;
109
110 const submitTransaction = (transaction, source, signers, forced = false) => {
111 return new Promise((resolve, reject) => {
112 let finalResponse = null;
113 let timer = setInterval(async () => {
114 let sourceAccount;
115 try {
116 sourceAccount = await loadAccount(source.publicKey());
117 } catch (e) {
118 clearInterval(timer);
119 timer = null;
120 return reject(e);
121 }
122 const tx = transaction(sourceAccount);
123 signers.forEach(kp => tx.sign(kp));
124 const hash = tx.hash().toString('hex');
125 const blob = encodeURIComponent(tx.toEnvelope().toXDR('base64'));
126 let result = null;
127 try {
128 result = await (await fetch(buildSubmitTx(blob))).json();
129 } catch (e) {
130 reject(e);
131 }
132 finalResponse = { result, hash, xdr: blob, sender: { publicKey: source.publicKey(), seqnum: sourceAccount.sequenceNumber().toString() } };
133
134 if (!forced) {
135 clearInterval(timer);
136 timer = null;
137 return resolve(finalResponse);
138 }
139
140 if (result.status == 'PENDING') {
141 clearInterval(timer);
142 timer = null;
143 resolve(finalResponse);
144 }
145 if (result.exception) {
146 clearInterval(timer);
147 timer = null;
148 reject(result);
149 }
150 }, mInterval.submitRetryInterval);
151 setTimeout(() => {
152 if (timer) {
153 clearInterval(timer);
154 timer = null;
155 return reject(finalResponse);
156 }
157 }, mInterval.submitRetryTimeout);
158 }).then((finalResponse) => {
159 if (!forced) {
160 return finalResponse;
161 }
162 return new Promise((resolve, reject) => {
163 let timer = setInterval(async () => {
164 const response = await DLT.getLedgerStatus(finalResponse.hash)
165 if (!response || response.status == 500) {
166 clearInterval(timer);
167 timer = null;
168 return resolve(finalResponse);
169 }
170
171 const { message } = response;
172 if (message.status) {
173 clearInterval(timer);
174 timer = null;
175 return resolve(finalResponse);
176 };
177 }, mInterval.submitRetryInterval);
178 setTimeout(() => {
179 if (timer) {
180 clearInterval(timer);
181 timer = null;
182 return reject(finalResponse);
183 }
184 }, mInterval.submitRetryTimeout);
185 });
186 })
187 };
188
189 const createAccount = (destination, startingBalance, memoText) => {
190 return function (source) {
191 return tx = new TransactionBuilder(source, { fee: BASE_FEE })
192 .addOperation(Operation.createAccount({ destination, startingBalance }))
193 .addMemo(Memo.text(memoText))
194 .setTimeout(TimeoutInfinite)
195 .build();
196 }
197
198 }
199
200 const createBatchAccount = (destinations, startingBalance, memoText) => {
201 return function (source) {
202 const tx = new TransactionBuilder(source, { fee: BASE_FEE })
203 .addMemo(Memo.text(memoText))
204 .setTimeout(TimeoutInfinite)
205
206 for (var i = 0; i < destinations.length; i++) {
207 const destination = destinations[i];
208 tx.addOperation(Operation.createAccount({ destination, startingBalance }))
209 }
210
211 return tx.build();
212 }
213 }
214
215 const setAlias = function (aliasName, alias, memoText) {
216 return function (source) {
217 return new TransactionBuilder(source, { fee: BASE_FEE })
218 .addOperation(Operation.manageData({ name: aliasName, value: alias }))
219 .addMemo(Memo.text(memoText))
220 .setTimeout(TimeoutInfinite)
221 .build();
222 }
223 }
224
225 const setSigningPermission = function (inThreshold, signers, memoText) {
226 return function (source) {
227 let threshold = { ...inThreshold };
228 if (signers.length > 0) {
229 threshold.signer = {
230 ed25519PublicKey: signers[0].key,
231 weight: signers[0].weight
232 }
233 }
234
235 const tx = new TransactionBuilder(source, { fee: BASE_FEE })
236 .addOperation(Operation.setOptions(threshold))
237 .addMemo(Memo.text(memoText));
238
239 for (var i = 1; i < signers.length; i++) {
240 const signer = signers[i];
241 tx.addOperation(Operation.setOptions({
242 signer: {
243 ed25519PublicKey: signer.key,
244 weight: signer.weight
245 }
246 }))
247 }
248
249 return tx.setTimeout(TimeoutInfinite).build();
250 }
251 }
252
253 const addManageData = function (dataArray, memoText) {
254 return function (source) {
255 const tx = new TransactionBuilder(source, { fee: BASE_FEE });
256
257 for (var i = 0; i < dataArray.length; i++) {
258 const { name, value } = dataArray[i];
259 if (!name || name == "") {
260 continue;
261 }
262 tx.addOperation(Operation.manageData({ name, value }))
263 }
264
265 return tx.addMemo(Memo.text(memoText))
266 .setTimeout(TimeoutInfinite)
267 .build();
268 }
269
270 }
271
272 // public method
273 // ------------------------------------------------------------------------
274
275 this.getStellarStatus = () => {
276 return DLT.getServerStatus().catch(e => {
277 throw new Error(DLT_SERVER_DOWN);
278 });
279 }
280
281 this.batchRegister = (alias, count = 1, startingBalance = BASE_RESERVE, aliasName = 'ACCOUNT_ALIAS') => {
282 // sanity check
283 if (!alias || alias == "") {
284 return Promise.reject(new Error(MISSING_ALIAS));
285 }
286 const newAccounts = new Array(count).fill(0).map(() => Keypair.random());
287 const createBatchAccountTransaction = createBatchAccount(newAccounts.map(z => z.publicKey()), startingBalance, "creating new account");
288 const setAliasTransaction = setAlias(aliasName, alias, "setting alias");
289 return submitTransaction(createBatchAccountTransaction, rootAccount, [rootAccount], true).then(() => {
290 const txArray = newAccounts.map(newAccount => submitTransaction(setAliasTransaction, newAccount, [newAccount]));
291 return Promise.all(txArray).then(() => {
292 return newAccounts.map(z => {
293 return {
294 privateKey: z.secret(),
295 publicKey: z.publicKey(),
296 aliasName,
297 alias
298 };
299 })
300 })
301 });
302 }
303
304 this.register = (alias, startingBalance = BASE_RESERVE, aliasName = 'ACCOUNT_ALIAS') => {
305 // sanity check
306 if (!alias || alias == "") {
307 return Promise.reject(new Error(MISSING_ALIAS));
308 }
309 const newAccount = Keypair.random();
310 const createAccountTransaction = createAccount(newAccount.publicKey(), startingBalance, "creating new account");
311 const setAliasTransaction = setAlias(aliasName, alias, "setting alias");
312 return submitTransaction(createAccountTransaction, rootAccount, [rootAccount], true).then(() => {
313 return submitTransaction(setAliasTransaction, newAccount, [newAccount]).then(() => {
314 return {
315 privateKey: newAccount.secret(),
316 publicKey: newAccount.publicKey(),
317 aliasName,
318 alias
319 };
320 });
321 });
322 }
323
324 this.deployMultiSenderReceiverMessageChannel = (senderPublicKeys, receiverPublicKeys, alias) => {
325 // sanity check
326 if (!senderPublicKeys || !(senderPublicKeys instanceof Array) || senderPublicKeys.length == 0) {
327 return Promise.reject(new Error(MISSING_SENDER_KEY));
328 }
329 if (!receiverPublicKeys || !(receiverPublicKeys instanceof Array) || receiverPublicKeys.length == 0) {
330 return Promise.reject(new Error(MISSING_RECEIVER_KEY));
331 }
332 if (!alias || alias == "") {
333 return Promise.reject(new Error(MISSING_ALIAS));
334 }
335 return this.register(alias, BASE_RESERVE, 'CHANNEL_ALIAS').then(messageChannel => {
336
337 const signers = senderPublicKeys.map(pkey => {
338 return { key: pkey, weight: 1 };
339 }).concat(receiverPublicKeys.map(pkey => {
340 return { key: pkey, weight: 1 };
341 }));
342
343 const setSigningPermissionTransaction = setSigningPermission({
344 masterWeight: 0,
345 lowThreshold: 1,
346 medThreshold: 1,
347 highThreshold: 1
348 }, signers, "setup message channel");
349
350 const messageChannelKeyPair = Keypair.fromSecret(messageChannel.privateKey);
351
352 return submitTransaction(setSigningPermissionTransaction, messageChannelKeyPair, [messageChannelKeyPair]).then(response => {
353 return messageChannel;
354 });
355
356 });
357
358 }
359 this.deployMessageChannel = (senderPublicKey, receiverPublicKey, alias) => {
360 // sanity check
361 if (!senderPublicKey || senderPublicKey == "") {
362 return Promise.reject(new Error(MISSING_SENDER_KEY));
363 }
364 if (!receiverPublicKey || receiverPublicKey == "") {
365 return Promise.reject(new Error(MISSING_RECEIVER_KEY));
366 }
367 if (!alias || alias == "") {
368 return Promise.reject(new Error(MISSING_ALIAS));
369 }
370
371 return this.register(alias, BASE_RESERVE, 'CHANNEL_ALIAS').then(messageChannel => {
372 const signers = [{ key: senderPublicKey, weight: 1 }, { key: receiverPublicKey, weight: 1 }];
373
374 const setSigningPermissionTransaction = setSigningPermission({
375 masterWeight: 0,
376 lowThreshold: 1,
377 medThreshold: 1,
378 highThreshold: 1
379 }, signers, "setup message channel");
380
381 const messageChannelKeyPair = Keypair.fromSecret(messageChannel.privateKey);
382 return submitTransaction(setSigningPermissionTransaction, messageChannelKeyPair, [messageChannelKeyPair]).then(response => {
383 return messageChannel;
384 });
385 });
386 }
387
388 this.sendMessage = (senderPrivateKey, messageChannelPublicKey, messageArray) => {
389 const senderKP = Keypair.fromSecret(senderPrivateKey);
390 const messageChannelKP = Keypair.fromPublicKey(messageChannelPublicKey);
391 const addManageDataTransaction = addManageData(messageArray, "adding data");
392 return submitTransaction(addManageDataTransaction, messageChannelKP, [senderKP], true);
393 }
394
395 this.getConfigBalance = () => mConfigBalance;
396
397 this.getMaxTxSetSize = () => mMaxTxSetSize;
398}