marsxchain 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ ## Build Ethereum From Scratch - Smart Contracts and More
2
+
3
+ ![Course Logo](course_logo_udemy.png)
4
+
5
+ This repository accompanies the "Build Ethereum From Scratch - Smart Contracts and More"
6
+ course by David Katz.
7
+
8
+ #### Take the course here:
9
+
10
+ [https://www.udemy.com/build-ethereum-from-scratch?couponCode=GITHUB](https://www.udemy.com/build-ethereum-from-scratch?couponCode=GITHUB)
11
+
12
+ In the course, you will build your own version of Ethereum. Ethereum can be described in two words. It's a:
13
+
14
+ #### Decentralized Computer.
15
+
16
+ A decentralized computer is like a normal computer. A normal computer executes a program using one machine.
17
+
18
+ But a decentralized computer executes a program using multiple machines. Every machine needs to agree upon the output of the program for its results to become official.
19
+
20
+ To build a decentralized computer, here are the essential elements:
21
+
22
+ 1) A smart contract language.
23
+ 2) A blockchain.
24
+ 3) A network.
25
+ 4) Transactions and accounts.
26
+ 5) A state management data structure.
27
+
28
+ Definitely take the course if you're interested in exploring the concepts behind this project more deeply. The course is a line-by-line tutorial of this entire repository. And by the end of the course, you'll have your own version of Ethereum.
@@ -0,0 +1,43 @@
1
+ const { ec, keccakHash } = require('../util');
2
+ const { STARTING_BALANCE } = require('../config');
3
+
4
+ class Account {
5
+ constructor({ code } = {}) {
6
+ this.keyPair = ec.genKeyPair();
7
+ this.address = this.keyPair.getPublic().encode('hex');
8
+ this.balance = STARTING_BALANCE;
9
+ this.code = code || [];
10
+ this.generateCodeHash();
11
+ }
12
+
13
+ generateCodeHash() {
14
+ this.codeHash = this.code.length > 0
15
+ ? keccakHash(this.address + this.code)
16
+ : null;
17
+ }
18
+
19
+ sign(data) {
20
+ return this.keyPair.sign(keccakHash(data));
21
+ }
22
+
23
+ toJSON() {
24
+ return {
25
+ address: this.address,
26
+ balance: this.balance,
27
+ code: this.code,
28
+ codeHash: this.codeHash
29
+ };
30
+ }
31
+
32
+ static verifySignature({ publicKey, data, signature }) {
33
+ const keyFromPublic = ec.keyFromPublic(publicKey, 'hex');
34
+
35
+ return keyFromPublic.verify(keccakHash(data), signature);
36
+ }
37
+
38
+ static calculateBalance({ address, state }) {
39
+ return state.getAccount({ address }).balance;
40
+ }
41
+ }
42
+
43
+ module.exports = Account;
@@ -0,0 +1,29 @@
1
+ const Account = require('./index');
2
+
3
+ describe('Account', () => {
4
+ let account, data, signature;
5
+
6
+ beforeEach(() => {
7
+ account = new Account();
8
+ data = { foo: 'foo' };
9
+ signature = account.sign(data);
10
+ });
11
+
12
+ describe('verifySignature()', () => {
13
+ it('validates a signature generated by the account', () => {
14
+ expect(Account.verifySignature({
15
+ publicKey: account.address,
16
+ data,
17
+ signature
18
+ })).toBe(true);
19
+ });
20
+
21
+ it('invalidates a signature not generated by the account', () => {
22
+ expect(Account.verifySignature({
23
+ publicKey: new Account().address,
24
+ data,
25
+ signature
26
+ })).toBe(false);
27
+ });
28
+ });
29
+ });
package/api/index.js ADDED
@@ -0,0 +1,96 @@
1
+ const express = require('express');
2
+ const bodyParser = require('body-parser');
3
+ const request = require('request');
4
+ const Account = require('../account');
5
+ const Blockchain = require('../blockchain');
6
+ const Block = require('../blockchain/block');
7
+ const PubSub = require('./pubsub');
8
+ const State = require('../store/state');
9
+ const Transaction = require('../transaction');
10
+ const TransactionQueue = require('../transaction/transaction-queue');
11
+
12
+ const app = express();
13
+ app.use(bodyParser.json());
14
+
15
+ const state = new State();
16
+ const blockchain = new Blockchain({ state });
17
+ const transactionQueue = new TransactionQueue();
18
+ const pubsub = new PubSub({ blockchain, transactionQueue });
19
+ const account = new Account();
20
+ const transaction = Transaction.createTransaction({ account });
21
+
22
+ setTimeout(() => {
23
+ pubsub.broadcastTransaction(transaction);
24
+ }, 500);
25
+
26
+ app.get('/blockchain', (req, res, next) => {
27
+ const { chain } = blockchain;
28
+
29
+ res.json({ chain });
30
+ });
31
+
32
+ app.get('/blockchain/mine', (req, res, next) => {
33
+ const lastBlock = blockchain.chain[blockchain.chain.length-1];
34
+ const block = Block.mineBlock({
35
+ lastBlock,
36
+ beneficiary: account.address,
37
+ transactionSeries: transactionQueue.getTransactionSeries(),
38
+ stateRoot: state.getStateRoot()
39
+ });
40
+
41
+ blockchain.addBlock({ block, transactionQueue })
42
+ .then(() => {
43
+ pubsub.broadcastBlock(block);
44
+
45
+ res.json({ block });
46
+ })
47
+ .catch(next);
48
+ });
49
+
50
+ app.post('/account/transact', (req, res, next) => {
51
+ const { code, gasLimit, to, value } = req.body;
52
+ const transaction = Transaction.createTransaction({
53
+ account: !to ? new Account({ code }) : account,
54
+ gasLimit,
55
+ to,
56
+ value
57
+ });
58
+
59
+ pubsub.broadcastTransaction(transaction);
60
+
61
+ res.json({ transaction });
62
+ });
63
+
64
+ app.get('/account/balance', (req, res, next) => {
65
+ const { address } = req.query;
66
+
67
+ const balance = Account.calculateBalance({
68
+ address: address || account.address,
69
+ state
70
+ });
71
+
72
+ res.json({ balance });
73
+ });
74
+
75
+ app.use((err, req, res, next) => {
76
+ console.error('Internal server error:', err);
77
+
78
+ res.status(500).json({ message: err.message });
79
+ });
80
+
81
+ const peer = process.argv.includes('--peer');
82
+ const PORT = peer
83
+ ? Math.floor(2000 + Math.random() * 1000)
84
+ : 3000;
85
+
86
+ if (peer) {
87
+ request('http://localhost:3000/blockchain', (error, response, body) => {
88
+ const { chain } = JSON.parse(body);
89
+
90
+ blockchain.replaceChain({ chain })
91
+ .then(() => console.log('Synchronized blockchain with the root node'))
92
+ .catch(error => console.error('Synchronization error:', error.message));
93
+ });
94
+ }
95
+
96
+ app.listen(PORT, () => console.log(`Listening at PORT: ${PORT}`));
package/api/pubsub.js ADDED
@@ -0,0 +1,81 @@
1
+ const PubNub = require('pubnub');
2
+ const Transaction = require('../transaction');
3
+
4
+ const credentials = {
5
+ publishKey: 'pub-c-1624883b-89a7-4a67-8438-447deac585a1',
6
+ subscribeKey: 'sub-c-94cf1061-53fc-447d-a7a6-dc2ce2cbc335',
7
+ secretKey: 'sec-c-OWUxOTFmNzctMTk1ZS00ZmM5LWJmOGYtZmE0YTIzMDNjNDY0'
8
+ };
9
+
10
+ const CHANNELS_MAP = {
11
+ TEST: 'TEST',
12
+ BLOCK: 'BLOCK',
13
+ TRANSACTION: 'TRANSACTION'
14
+ };
15
+
16
+ class PubSub {
17
+ constructor({ blockchain, transactionQueue }) {
18
+ this.pubnub = new PubNub(credentials);
19
+ this.blockchain = blockchain;
20
+ this.transactionQueue = transactionQueue;
21
+ this.subscribeToChannels();
22
+ this.listen();
23
+ }
24
+
25
+ subscribeToChannels() {
26
+ this.pubnub.subscribe({
27
+ channels: Object.values(CHANNELS_MAP)
28
+ });
29
+ }
30
+
31
+ publish({ channel, message }) {
32
+ this.pubnub.publish({ channel, message });
33
+ }
34
+
35
+ listen() {
36
+ this.pubnub.addListener({
37
+ message: messageObject => {
38
+ const { channel, message } = messageObject;
39
+ const parsedMessage = JSON.parse(message);
40
+
41
+ console.log('Message received. Channel:', channel);
42
+
43
+ switch (channel) {
44
+ case CHANNELS_MAP.BLOCK:
45
+ console.log('block message', message);
46
+
47
+ this.blockchain.addBlock({
48
+ block: parsedMessage,
49
+ transactionQueue: this.transactionQueue
50
+ }).then(() => console.log('New block accepted', parsedMessage))
51
+ .catch(error => console.error('New block rejected:', error.message));
52
+ break;
53
+ case CHANNELS_MAP.TRANSACTION:
54
+ console.log(`Received transaction: ${parsedMessage.id}`);
55
+
56
+ this.transactionQueue.add(new Transaction(parsedMessage));
57
+
58
+ break;
59
+ default:
60
+ return;
61
+ }
62
+ }
63
+ });
64
+ }
65
+
66
+ broadcastBlock(block) {
67
+ this.publish({
68
+ channel: CHANNELS_MAP.BLOCK,
69
+ message: JSON.stringify(block)
70
+ });
71
+ }
72
+
73
+ broadcastTransaction(transaction) {
74
+ this.publish({
75
+ channel: CHANNELS_MAP.TRANSACTION,
76
+ message: JSON.stringify(transaction)
77
+ });
78
+ }
79
+ }
80
+
81
+ module.exports = PubSub;
package/api-test.js ADDED
@@ -0,0 +1,112 @@
1
+ const request = require('request');
2
+
3
+ const { OPCODE_MAP } = require('./interpreter');
4
+ const { STOP, ADD, PUSH, STORE, LOAD } = OPCODE_MAP;
5
+
6
+ const BASE_URL = 'http://localhost:3000';
7
+
8
+ const postTransact = ({ code, to, value, gasLimit }) => {
9
+ return new Promise((resolve, reject) => {
10
+ request(`${BASE_URL}/account/transact`, {
11
+ method: 'POST',
12
+ headers: { 'Content-Type': 'application/json' },
13
+ body: JSON.stringify({ code, to, value, gasLimit })
14
+ },(error, response, body) => {
15
+ return resolve(JSON.parse(body));
16
+ });
17
+ });
18
+ }
19
+
20
+ const getMine = () => {
21
+ return new Promise((resolve, reject) => {
22
+ setTimeout(() => {
23
+ request(`${BASE_URL}/blockchain/mine`, (error, response, body) => {
24
+ return resolve(JSON.parse(body));
25
+ });
26
+ }, 3000);
27
+ });
28
+ }
29
+
30
+ const getAccountBalance = ({ address } = {}) => {
31
+ return new Promise((resolve, reject) => {
32
+ request(
33
+ `${BASE_URL}/account/balance` + (address ? `?address=${address}` : ''),
34
+ (error, response, body) => {
35
+ return resolve(JSON.parse(body));
36
+ }
37
+ );
38
+ });
39
+ }
40
+
41
+ let toAccountData;
42
+ let smartContractAccountData;
43
+
44
+ postTransact({})
45
+ .then(postTransactResponse => {
46
+ console.log(
47
+ 'postTransactResponse (Create Account Transaction)',
48
+ postTransactResponse
49
+ );
50
+
51
+ toAccountData = postTransactResponse.transaction.data.accountData;
52
+
53
+ return getMine();
54
+ }).then(getMineResponse => {
55
+ console.log('getMineResponse', getMineResponse);
56
+
57
+ return postTransact({ to: toAccountData.address, value: 20 });
58
+ })
59
+ .then(postTransactResponse2 => {
60
+ console.log(
61
+ 'postTransactResponse2 (Standard Transaction)',
62
+ postTransactResponse2
63
+ );
64
+
65
+ const key = 'foo';
66
+ const value = 'bar';
67
+ const code = [PUSH, value, PUSH, key, STORE, PUSH, key, LOAD, STOP];
68
+
69
+ return postTransact({ code });
70
+ })
71
+ .then(postTransactResponse3 => {
72
+ console.log(
73
+ 'postTransactResponse3 (Smart Contract)',
74
+ postTransactResponse3
75
+ );
76
+
77
+ smartContractAccountData = postTransactResponse3
78
+ .transaction
79
+ .data
80
+ .accountData;
81
+
82
+ return getMine();
83
+ })
84
+ .then(getMineResponse2 => {
85
+ console.log('getMineResponse2', getMineResponse2);
86
+
87
+ return postTransact({
88
+ to: smartContractAccountData.codeHash,
89
+ value: 0,
90
+ gasLimit: 100
91
+ });
92
+ })
93
+ .then(postTransactResponse4 => {
94
+ console.log(
95
+ 'postTransactResponse4 (to the smart contract)',
96
+ postTransactResponse4
97
+ );
98
+ return getMine();
99
+ })
100
+ .then(getMineResponse3 => {
101
+ console.log('getMineResponse3', getMineResponse3);
102
+
103
+ return getAccountBalance();
104
+ })
105
+ .then(getAccountBalanceResponse => {
106
+ console.log('getAccountBalanceResponse', getAccountBalanceResponse);
107
+
108
+ return getAccountBalance({ address: toAccountData.address });
109
+ })
110
+ .then(getAccountBalanceResponse2 => {
111
+ console.log('getAccountBalanceResponse2', getAccountBalanceResponse2);
112
+ });
@@ -0,0 +1,144 @@
1
+ const { GENESIS_DATA, MINE_RATE } = require('../config');
2
+ const { keccakHash } = require('../util');
3
+ const Transaction = require('../transaction');
4
+ const Trie = require('../store/trie');
5
+
6
+ const HASH_LENGTH = 64;
7
+ const MAX_HASH_VALUE = parseInt('f'.repeat(HASH_LENGTH), 16);
8
+ const MAX_NONCE_VALUE = 2 ** 64;
9
+
10
+ class Block {
11
+ constructor({ blockHeaders, transactionSeries }) {
12
+ this.blockHeaders = blockHeaders;
13
+ this.transactionSeries = transactionSeries;
14
+ }
15
+
16
+ static calculateBlockTargetHash({ lastBlock }) {
17
+ const value = (MAX_HASH_VALUE / lastBlock.blockHeaders.difficulty).toString(16);
18
+
19
+ if (value.length > HASH_LENGTH) {
20
+ return 'f'.repeat(HASH_LENGTH);
21
+ }
22
+
23
+ return '0'.repeat(HASH_LENGTH - value.length) + value;
24
+ }
25
+
26
+ static adjustDifficulty({ lastBlock, timestamp }) {
27
+ const { difficulty } = lastBlock.blockHeaders;
28
+
29
+ if ((timestamp - lastBlock.blockHeaders.timestamp) > MINE_RATE) {
30
+ return difficulty - 1;
31
+ }
32
+
33
+ if (difficulty < 1) {
34
+ return 1;
35
+ }
36
+
37
+ return difficulty + 1;
38
+ }
39
+
40
+ static mineBlock({
41
+ lastBlock,
42
+ beneficiary,
43
+ transactionSeries,
44
+ stateRoot
45
+ }) {
46
+ const target = Block.calculateBlockTargetHash({ lastBlock });
47
+ const miningRewardTransaction = Transaction.createTransaction({
48
+ beneficiary
49
+ });
50
+ transactionSeries.push(miningRewardTransaction);
51
+ const transactionsTrie = Trie.buildTrie({ items: transactionSeries });
52
+ let timestamp, truncatedBlockHeaders, header, nonce, underTargetHash;
53
+
54
+ do {
55
+ timestamp = Date.now();
56
+ truncatedBlockHeaders = {
57
+ parentHash: keccakHash(lastBlock.blockHeaders),
58
+ beneficiary,
59
+ difficulty: Block.adjustDifficulty({ lastBlock, timestamp }),
60
+ number: lastBlock.blockHeaders.number + 1,
61
+ timestamp,
62
+ transactionsRoot: transactionsTrie.rootHash,
63
+ stateRoot
64
+ };
65
+ header = keccakHash(truncatedBlockHeaders);
66
+ nonce = Math.floor(Math.random() * MAX_NONCE_VALUE);
67
+
68
+ underTargetHash = keccakHash(header + nonce);
69
+ } while (underTargetHash > target);
70
+
71
+ return new this({
72
+ blockHeaders: { ...truncatedBlockHeaders, nonce },
73
+ transactionSeries
74
+ });
75
+ }
76
+
77
+ static genesis() {
78
+ return new Block(GENESIS_DATA);
79
+ }
80
+
81
+ static validateBlock({ lastBlock, block, state }) {
82
+ return new Promise((resolve, reject) => {
83
+ if (keccakHash(block) === keccakHash(Block.genesis())) {
84
+ return resolve();
85
+ }
86
+
87
+ if (keccakHash(lastBlock.blockHeaders) !== block.blockHeaders.parentHash) {
88
+ return reject(
89
+ new Error("The parent hash must be a hash of the last block's headers")
90
+ );
91
+ }
92
+
93
+ if (block.blockHeaders.number !== lastBlock.blockHeaders.number + 1) {
94
+ return reject(new Error('The block must increment the number by 1'));
95
+ }
96
+
97
+ if (
98
+ Math.abs(lastBlock.blockHeaders.difficulty - block.blockHeaders.difficulty) > 1
99
+ ) {
100
+ return reject(new Error('The difficulty must only adjust by 1'));
101
+ }
102
+
103
+ const rebuiltTransactionsTrie = Trie.buildTrie({
104
+ items: block.transactionSeries
105
+ });
106
+
107
+ if (rebuiltTransactionsTrie.rootHash !== block.blockHeaders.transactionsRoot) {
108
+ return reject(
109
+ new Error(
110
+ `The rebuilt transactions root does not match the block's ` +
111
+ `transactions root: ${block.blockHeaders.transactionRoot}`
112
+ )
113
+ );
114
+ }
115
+
116
+ const target = Block.calculateBlockTargetHash({ lastBlock });
117
+ const { blockHeaders } = block;
118
+ const { nonce } = blockHeaders;
119
+ const truncatedBlockHeaders = { ...blockHeaders };
120
+ delete truncatedBlockHeaders.nonce;
121
+ const header = keccakHash(truncatedBlockHeaders);
122
+ const underTargetHash = keccakHash(header + nonce);
123
+
124
+ if (underTargetHash > target) {
125
+ return reject(new Error(
126
+ 'The block does not meet the proof of work requirement'
127
+ ));
128
+ }
129
+
130
+ Transaction.validateTransactionSeries({
131
+ state, transactionSeries: block.transactionSeries
132
+ }).then(resolve)
133
+ .catch(reject);
134
+ });
135
+ }
136
+
137
+ static runBlock({ block, state }) {
138
+ for (let transaction of block.transactionSeries) {
139
+ Transaction.runTransaction({ transaction, state });
140
+ }
141
+ }
142
+ }
143
+
144
+ module.exports = Block;
@@ -0,0 +1,160 @@
1
+ const Block = require('./block');
2
+ const State = require('../store/state');
3
+ const { keccakHash } = require('../util');
4
+
5
+ describe('Block', () => {
6
+ describe('calculateBlockTargetHash()', () => {
7
+ it('calculates the maximum hash when the last block difficulty is 1', () => {
8
+ expect(
9
+ Block
10
+ .calculateBlockTargetHash({ lastBlock: { blockHeaders: { difficulty: 1 } } })
11
+ ).toEqual('f'.repeat(64));
12
+ });
13
+
14
+ it('calculates a low hash value when the last block difficulty is high', () => {
15
+ expect(
16
+ Block
17
+ .calculateBlockTargetHash({ lastBlock: { blockHeaders: { difficulty: 500 } } })
18
+ < '1'
19
+ ).toBe(true);
20
+ });
21
+ });
22
+
23
+ describe('mineBlock()', () => {
24
+ let lastBlock, minedBlock;
25
+
26
+ beforeEach(() => {
27
+ lastBlock = Block.genesis();
28
+ minedBlock = Block.mineBlock({
29
+ lastBlock,
30
+ beneficiary: 'beneficiary',
31
+ transactionSeries: []
32
+ });
33
+ });
34
+
35
+ it('mines a block', () => {
36
+ expect(minedBlock).toBeInstanceOf(Block);
37
+ });
38
+
39
+ it('mines a block that meets the proof of work requirement', () => {
40
+ const target = Block.calculateBlockTargetHash({ lastBlock });
41
+ const { blockHeaders } = minedBlock;
42
+ const { nonce } = blockHeaders;
43
+ const truncatedBlockHeaders = { ...blockHeaders };
44
+ delete truncatedBlockHeaders.nonce;
45
+ const header = keccakHash(truncatedBlockHeaders);
46
+ const underTargetHash = keccakHash(header + nonce);
47
+
48
+ expect(underTargetHash < target).toBe(true);
49
+ });
50
+ });
51
+
52
+ describe('adjustDifficulty()', () => {
53
+ it('keeps the difficulty above 0', () => {
54
+ expect(
55
+ Block.adjustDifficulty({
56
+ lastBlock: { blockHeaders: { difficulty: 0 } },
57
+ timestamp: Date.now()
58
+ })
59
+ ).toEqual(1);
60
+ });
61
+
62
+ it('increases the difficulty for a quickly mined block', () => {
63
+ expect(
64
+ Block.adjustDifficulty({
65
+ lastBlock: { blockHeaders: { difficulty: 5, timestamp: 1000 } },
66
+ timestamp: 3000
67
+ })
68
+ ).toEqual(6);
69
+ });
70
+
71
+ it('decreases the difficulty for a slowly mined block', () => {
72
+ expect(
73
+ Block.adjustDifficulty({
74
+ lastBlock: { blockHeaders: { difficulty: 5, timestamp: 1000 } },
75
+ timestamp: 20000
76
+ })
77
+ ).toEqual(4);
78
+ });
79
+ });
80
+
81
+ describe('validateBlock()', () => {
82
+ let block, lastBlock, state;
83
+
84
+ beforeEach(() => {
85
+ lastBlock = Block.genesis();
86
+ block = Block.mineBlock({
87
+ lastBlock,
88
+ beneficiary: 'beneficiary',
89
+ transactionSeries: []
90
+ });
91
+ state = new State();
92
+ });
93
+
94
+ it('resolves when the block is the genesis block', () => {
95
+ expect(Block.validateBlock({
96
+ block: Block.genesis(),
97
+ state
98
+ })).resolves;
99
+ });
100
+
101
+ it('resolves if block is valid', () => {
102
+ expect(Block.validateBlock({ lastBlock, block, state })).resolves;
103
+ });
104
+
105
+ it('rejects when the parentHash is invalid', () => {
106
+ block.blockHeaders.parentHash = 'foo';
107
+
108
+ expect(Block.validateBlock({ lastBlock, block, state }))
109
+ .rejects
110
+ .toMatchObject({
111
+ message: "The parent hash must be a hash of the last block's headers"
112
+ });
113
+ });
114
+
115
+ it('rejects when the number is not increased by one', () => {
116
+ block.blockHeaders.number = 500;
117
+
118
+ expect(Block.validateBlock({ lastBlock, block, state }))
119
+ .rejects
120
+ .toMatchObject({
121
+ message: 'The block must increment the number by 1'
122
+ });
123
+ });
124
+
125
+ it('rejects when the difficulty adjusts by more than 1', () => {
126
+ block.blockHeaders.difficulty = 999;
127
+
128
+ expect(Block.validateBlock({ lastBlock, block, state }))
129
+ .rejects
130
+ .toMatchObject({
131
+ message: 'The difficulty must only adjust by 1'
132
+ });
133
+ });
134
+
135
+ it('rejects when the proof of work requirement is not met', () => {
136
+ const originalCalculateBlockTargetHash = Block.calculateBlockTargetHash;
137
+ Block.calculateBlockTargetHash = () => {
138
+ return '0'.repeat(64);
139
+ }
140
+
141
+ expect(Block.validateBlock({ lastBlock, block, state }))
142
+ .rejects
143
+ .toMatchObject({
144
+ message: 'The block does not meet the proof of work requirement'
145
+ });
146
+
147
+ Block.calculateBlockTargetHash = originalCalculateBlockTargetHash;
148
+ });
149
+
150
+ it('rejects when the transactionSeries is not valid', () => {
151
+ block.transactionSeries = ['foo'];
152
+
153
+ expect(Block.validateBlock({ state, lastBlock, block }))
154
+ .rejects
155
+ .toMatchObject({
156
+ message: /rebuilt transactions root does not match/
157
+ });
158
+ });
159
+ });
160
+ });