@ocap/tx-protocols 1.13.61 → 1.13.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,9 +61,9 @@ runner.use(
61
61
  );
62
62
 
63
63
  // Ensure sender/receiver exist
64
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
64
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'OK', table: 'account' }));
65
65
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
66
- runner.use(pipes.ExtractState({ from: 'itx.to', to: 'receiverState', status: 'INVALID_RECEIVER_STATE' }));
66
+ runner.use(pipes.ExtractState({ from: 'itx.to', to: 'receiverState', status: 'OK', table: 'account' }));
67
67
 
68
68
  // Extract delegation
69
69
  runner.use(pipes.ExtractState({ from: 'itx.address', to: 'delegationState', table: 'delegation', status: 'OK' }));
@@ -71,7 +71,7 @@ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'delegationState', tabl
71
71
  // Create delegation state
72
72
  runner.use(
73
73
  async (context, next) => {
74
- const { tx, itx, ops, statedb, senderState, delegationState } = context;
74
+ const { tx, itx, ops, statedb, senderState, receiverState, delegationState } = context;
75
75
 
76
76
  const opsObj = ops.reduce(
77
77
  (acc, x) => {
@@ -86,8 +86,23 @@ runner.use(
86
86
  delegationState ? delegationState.ops : {}
87
87
  );
88
88
 
89
- const [newSenderState, newDelegationState] = await Promise.all([
90
- statedb.account.update(senderState.address, account.update(senderState, { nonce: tx.nonce }, context), context),
89
+ const sender = senderState ? senderState.address : tx.from;
90
+ const receiver = receiverState ? receiverState.address : itx.to;
91
+
92
+ const [newSenderState, newReceiverState, newDelegationState] = await Promise.all([
93
+ // update sender
94
+ statedb.account.updateOrCreate(
95
+ senderState,
96
+ account.updateOrCreate(senderState, { address: sender, nonce: tx.nonce, pk: tx.pk }, context),
97
+ context
98
+ ),
99
+
100
+ // update receiver
101
+ receiverState
102
+ ? Promise.resolve(receiverState)
103
+ : statedb.account.create(receiver, account.create({ address: receiver }, context), context),
104
+
105
+ // update delegation
91
106
  delegationState
92
107
  ? statedb.delegation.update(itx.address, delegation.update(delegationState, { ...itx, ops: opsObj }, context), context) // prettier-ignore
93
108
  : statedb.delegation.create(itx.address, delegation.create({ ...itx, ops: opsObj }, context), context),
@@ -97,6 +112,7 @@ runner.use(
97
112
 
98
113
  // Update context
99
114
  context.senderState = newSenderState;
115
+ context.receiverState = newReceiverState;
100
116
  context.delegationState = newDelegationState;
101
117
 
102
118
  return next();
@@ -35,7 +35,7 @@ runner.use(
35
35
  );
36
36
 
37
37
  // Ensure sender exist
38
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
38
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
39
39
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
40
40
 
41
41
  // Create account state, and update old accounts
@@ -48,7 +48,7 @@ runner.use(
48
48
 
49
49
  // Ensure receiver does not exist
50
50
  if (receiver) {
51
- return next(new Error('INVALID_SENDER_STATE', 'Can not migrate to an existing account'));
51
+ return next(new Error('INVALID_RECEIVER_STATE', 'Can not migrate to an existing account'));
52
52
  }
53
53
 
54
54
  // Create new account
@@ -53,10 +53,9 @@ runner.use(
53
53
  );
54
54
 
55
55
  // Ensure sender/receiver/delegation exist
56
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
56
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'delegationState', status: 'INVALID_DELEGATION', table: 'delegation' })); // prettier-ignore
57
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
57
58
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
58
- runner.use(pipes.ExtractState({ from: 'itx.to', to: 'receiverState', status: 'INVALID_RECEIVER_STATE' }));
59
- runner.use(pipes.ExtractState({ from: 'itx.address', to: 'delegationState', status: 'INVALID_DELEGATION' }));
60
59
 
61
60
  // Update delegation state
62
61
  runner.use(
@@ -24,7 +24,7 @@ const runner = new Runner();
24
24
  runner.use(pipes.VerifyMultiSig(0));
25
25
  runner.use(verifyAcquireParams('acquire-v2'));
26
26
 
27
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
27
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
28
28
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
29
29
 
30
30
  runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK' }));
@@ -43,7 +43,7 @@ runner.use(
43
43
 
44
44
  // Though tokens are verified on factory creating phase, we need them here to construct tokenSymbols
45
45
  runner.use(extractFactoryTokens);
46
- runner.use(pipes.ExtractState({ from: 'tokenAddress', to: 'tokenStates', status: 'INVALID_TOKEN' }));
46
+ runner.use(pipes.ExtractState({ from: 'tokenAddress', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
47
47
 
48
48
  // Verify itx
49
49
  runner.use(verifyMintLimit);
@@ -86,6 +86,7 @@ runner.use(
86
86
  // Sender updates
87
87
  const senderUpdates = {
88
88
  nonce: tx.nonce,
89
+ pk: tx.pk,
89
90
  ...applyTokenUpdates(factoryTokens, senderState, 'sub'),
90
91
  };
91
92
 
@@ -82,9 +82,9 @@ runner.use(
82
82
  );
83
83
 
84
84
  // 5. verify sender & signer & owner
85
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
86
- runner.use(pipes.ExtractState({ from: 'senders', to: 'signerStates', status: 'INVALID_SIGNER_STATE' }));
87
- runner.use(pipes.ExtractState({ from: 'itx.owner', to: 'ownerState', status: 'INVALID_RECEIVER_STATE' }));
85
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
86
+ runner.use(pipes.ExtractState({ from: 'senders', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
87
+ runner.use(pipes.ExtractState({ from: 'itx.owner', to: 'ownerState', status: 'OK', table: 'account' }));
88
88
  runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
89
89
 
90
90
  // 6. verify token state and balance
@@ -153,6 +153,7 @@ runner.use(
153
153
  });
154
154
 
155
155
  const isAlsoSigner = !!signerUpdates[senderState.address];
156
+ const owner = ownerState ? ownerState.address : itx.owner;
156
157
 
157
158
  // Factory updates
158
159
  const factoryUpdates = { numMinted: factoryState.numMinted + 1 };
@@ -163,44 +164,53 @@ runner.use(
163
164
  context.tokenUpdates = applyTokenUpdates(factoryTokens, { tokens: {} }, 'add');
164
165
  }
165
166
 
166
- const [newSenderState, assetState, newSignerStates, newFactoryState, newAssetStates] = await Promise.all([
167
- // Update sender state
168
- statedb.account.update(
169
- senderState.address,
170
- account.update(
171
- senderState,
172
- Object.assign({ nonce: tx.nonce }, signerUpdates[senderState.address] || {}),
167
+ const [newSenderState, newReceiverState, assetState, newSignerStates, newFactoryState, newAssetStates] =
168
+ await Promise.all([
169
+ // Update sender state
170
+ statedb.account.update(
171
+ senderState.address,
172
+ account.update(
173
+ senderState,
174
+ Object.assign({ nonce: tx.nonce, pk: tx.pk }, signerUpdates[senderState.address] || {}),
175
+ context
176
+ ),
173
177
  context
174
178
  ),
175
- context
176
- ),
177
-
178
- // Create asset
179
- statedb.asset.create(
180
- itx.address,
181
- asset.create({ ...mintedAsset, owner: ownerState.address, address: mintedAddress }, context),
182
- context
183
- ),
184
-
185
- // Update signer state
186
- Promise.all(
187
- signerStates
188
- .filter((x) => x.address !== senderState.address)
189
- .map((x) => statedb.account.update(x.address, account.update(x, signerUpdates[x.address], context), context))
190
- ),
191
-
192
- // Update factory state
193
- statedb.factory.update(factoryState.address, factory.update(factoryState, factoryUpdates, context), context),
194
-
195
- // Mark asset as consumed
196
- Promise.all(
197
- assetStates.map((x) =>
198
- statedb.asset.update(x.address, asset.update(x, { consumedTime: txTime }, context), context)
199
- )
200
- ),
201
- ]);
179
+
180
+ // create receiver if not exist: asset owner
181
+ ownerState
182
+ ? Promise.resolve(ownerState)
183
+ : statedb.account.create(owner, account.create({ address: owner }, context), context),
184
+
185
+ // Create asset
186
+ statedb.asset.create(
187
+ itx.address,
188
+ asset.create({ ...mintedAsset, owner, address: mintedAddress }, context),
189
+ context
190
+ ),
191
+
192
+ // Update signer state
193
+ Promise.all(
194
+ signerStates
195
+ .filter((x) => x.address !== senderState.address)
196
+ .map((x) =>
197
+ statedb.account.update(x.address, account.update(x, signerUpdates[x.address], context), context)
198
+ )
199
+ ),
200
+
201
+ // Update factory state
202
+ statedb.factory.update(factoryState.address, factory.update(factoryState, factoryUpdates, context), context),
203
+
204
+ // Mark asset as consumed
205
+ Promise.all(
206
+ assetStates.map((x) =>
207
+ statedb.asset.update(x.address, asset.update(x, { consumedTime: txTime }, context), context)
208
+ )
209
+ ),
210
+ ]);
202
211
 
203
212
  context.senderState = newSenderState;
213
+ context.receiverState = newReceiverState;
204
214
  context.signerStates = isAlsoSigner ? newSignerStates.concat(newSenderState) : newSignerStates;
205
215
  context.assetState = assetState;
206
216
  context.factoryState = newFactoryState;
@@ -64,7 +64,7 @@ runner.use(
64
64
  );
65
65
 
66
66
  // Ensure sender exist
67
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
67
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
68
68
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
69
69
 
70
70
  // Check if parent is a factory
@@ -83,7 +83,7 @@ runner.use(
83
83
  );
84
84
 
85
85
  // Ensure parent exist
86
- runner.use(pipes.ExtractState({ from: 'itx.parent', to: 'parentAsset', status: 'INVALID_ASSET' }));
86
+ runner.use(pipes.ExtractState({ from: 'itx.parent', to: 'parentAsset', status: 'INVALID_ASSET', table: 'asset' }));
87
87
 
88
88
  // Update asset state
89
89
  runner.use(
@@ -92,7 +92,11 @@ runner.use(
92
92
 
93
93
  const [newSenderState, assetState] = await Promise.all([
94
94
  // Update owner state
95
- statedb.account.update(senderState.address, account.update(senderState, { nonce: tx.nonce }, context), context),
95
+ statedb.account.update(
96
+ senderState.address,
97
+ account.update(senderState, { nonce: tx.nonce, pk: tx.pk }, context),
98
+ context
99
+ ),
96
100
  // Create asset state
97
101
  statedb.asset.create(
98
102
  itx.address,
@@ -17,17 +17,17 @@ runner.use(pipes.VerifyMultiSig(0));
17
17
  runner.use(verifyAcquireParams('mint'));
18
18
 
19
19
  // Ensure sender exist
20
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
20
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
21
21
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
22
22
 
23
23
  // Ensure factory exist
24
24
  runner.use(pipes.ExtractState({ from: 'itx.factory', to: 'factoryState', status: 'INVALID_FACTORY_STATE', table: 'factory' })); // prettier-ignore
25
25
 
26
26
  // Used to assemble issuer object
27
- runner.use(pipes.ExtractState({ from: 'factoryState.owner', to: 'factoryOwnerState', status: 'INVALID_OWNER' }));
27
+ runner.use(pipes.ExtractState({ from: 'factoryState.owner', to: 'factoryOwnerState', status: 'INVALID_OWNER', table: 'account' })); // prettier-ignore
28
28
 
29
29
  // Ensure factory owner exist and equal to sender
30
- runner.use(pipes.ExtractState({ from: 'itx.owner', to: 'ownerState', status: 'INVALID_OWNER' }));
30
+ runner.use(pipes.ExtractState({ from: 'itx.owner', to: 'ownerState', status: 'OK', table: 'account' }));
31
31
  runner.use(
32
32
  pipes.VerifyInfo([
33
33
  {
@@ -72,20 +72,30 @@ runner.use(
72
72
  assetStates = [],
73
73
  } = context;
74
74
 
75
- // account updates
76
- const senderUpdates = { nonce: tx.nonce };
75
+ const owner = ownerState ? ownerState.address : itx.owner;
77
76
 
78
- // Factory updates
77
+ const senderUpdates = { nonce: tx.nonce, pk: tx.pk };
79
78
  const factoryUpdates = { numMinted: factoryState.numMinted + 1 };
80
79
 
81
- const [newSenderState, assetState, newFactoryState, newAssetStates] = await Promise.all([
80
+ const [newSenderState, newReceiverState, assetState, newFactoryState, newAssetStates] = await Promise.all([
81
+ // update sender
82
82
  statedb.account.update(senderState.address, account.update(senderState, senderUpdates, context), context),
83
+
84
+ // create receiver if not exist: asset owner
85
+ ownerState
86
+ ? Promise.resolve(ownerState)
87
+ : statedb.account.create(owner, account.create({ address: owner }, context), context),
88
+
89
+ // create asset
83
90
  statedb.asset.create(
84
91
  itx.address,
85
- asset.create({ ...mintedAsset, owner: ownerState.address, address: mintedAddress }, context),
92
+ asset.create({ ...mintedAsset, owner, address: mintedAddress }, context),
86
93
  context
87
94
  ),
95
+
96
+ // update factory
88
97
  statedb.factory.update(factoryState.address, factory.update(factoryState, factoryUpdates, context), context),
98
+
89
99
  // mark input assets as consumed
90
100
  Promise.all(
91
101
  assetStates.map((x) =>
@@ -95,6 +105,7 @@ runner.use(
95
105
  ]);
96
106
 
97
107
  context.senderState = newSenderState;
108
+ context.receiverState = newReceiverState;
98
109
  context.assetState = assetState;
99
110
  context.factoryState = newFactoryState;
100
111
  context.assetStates = newAssetStates;
@@ -10,11 +10,12 @@ module.exports = (mode) => (context, next) => {
10
10
  if (mode === 'acquire-v2') {
11
11
  owner = delegatorState || senderState;
12
12
  issuer = itx.issuer;
13
+ context.assetOwner = owner;
13
14
  } else if (mode === 'acquire-v3') {
14
- owner = ownerState;
15
+ owner = ownerState || { address: itx.owner };
15
16
  issuer = itx.issuer;
16
17
  } else if (mode === 'mint') {
17
- owner = ownerState;
18
+ owner = ownerState || { address: itx.owner };
18
19
  issuer = {
19
20
  id: factoryOwnerState.address,
20
21
  pk: factoryOwnerState.pk,
@@ -33,7 +34,6 @@ module.exports = (mode) => (context, next) => {
33
34
  return next(new Error('INVALID_ASSET', 'Invalid itx.address: does not match with minted asset address'));
34
35
  }
35
36
 
36
- context.assetOwner = owner;
37
37
  context.mintedAsset = minted.asset;
38
38
  context.mintedAddress = minted.address;
39
39
 
@@ -52,7 +52,11 @@ runner.use(
52
52
 
53
53
  const [newSenderState, newAssetState] = await Promise.all([
54
54
  // update owner state
55
- statedb.account.update(senderState.address, account.update(senderState, { nonce: tx.nonce }, context), context),
55
+ statedb.account.update(
56
+ senderState.address,
57
+ account.update(senderState, { nonce: tx.nonce, pk: tx.pk }, context),
58
+ context
59
+ ),
56
60
 
57
61
  // update asset state
58
62
  statedb.asset.update(
@@ -79,23 +79,15 @@ runner.use(
79
79
  );
80
80
 
81
81
  // Ensure sender exist
82
- runner.use(
83
- pipes.ExtractState({ from: 'tx.from', to: 'senderState', table: 'account', status: 'INVALID_SENDER_STATE' })
84
- );
82
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', table: 'account', status: 'INVALID_SENDER_STATE' })); // prettier-ignore
85
83
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
86
84
 
87
85
  // Ensure tokens exist if we are creating an factory that consume tokens
88
- runner.use(
89
- pipes.ExtractState({ from: 'factoryTokens', to: 'tokenStates', table: 'token', status: 'INVALID_FACTORY_INPUT' })
90
- );
86
+ runner.use(pipes.ExtractState({ from: 'factoryTokens', to: 'tokenStates', table: 'token', status: 'INVALID_FACTORY_INPUT' })); // prettier-ignore
91
87
 
92
88
  // ensure input.assets all exist on chain
93
- runner.use(
94
- pipes.ExtractState({ from: 'factoryProps.input.assets', to: 'inputAssetStates', table: 'asset', status: 'OK' })
95
- );
96
- runner.use(
97
- pipes.ExtractState({ from: 'factoryProps.input.assets', to: 'inputFactoryStates', table: 'factory', status: 'OK' })
98
- );
89
+ runner.use(pipes.ExtractState({ from: 'factoryProps.input.assets', to: 'inputAssetStates', table: 'asset', status: 'OK' })); // prettier-ignore
90
+ runner.use(pipes.ExtractState({ from: 'factoryProps.input.assets', to: 'inputFactoryStates', table: 'factory', status: 'OK' })); // prettier-ignore
99
91
  runner.use((context, next) => {
100
92
  const { inputAssetStates = [], inputFactoryStates = [], factoryProps } = context;
101
93
  if (inputAssetStates.some((x) => !!x.consumedTime)) {
@@ -118,7 +110,12 @@ runner.use(
118
110
 
119
111
  const [newSenderState, factoryState] = await Promise.all([
120
112
  // Update owner state
121
- statedb.account.update(senderState.address, account.update(senderState, { nonce: tx.nonce }, context), context),
113
+ statedb.account.update(
114
+ senderState.address,
115
+ account.update(senderState, { nonce: tx.nonce, pk: tx.pk }, context),
116
+ context
117
+ ),
118
+
122
119
  // Create factory state
123
120
  statedb.factory.create(
124
121
  itx.address,
@@ -122,7 +122,7 @@ runner.use(
122
122
  senderState.address,
123
123
  account.update(
124
124
  senderState,
125
- Object.assign({ nonce: tx.nonce }, signerUpdates[senderState.address] || {}),
125
+ Object.assign({ nonce: tx.nonce, pk: tx.pk }, signerUpdates[senderState.address] || {}),
126
126
  context
127
127
  ),
128
128
  context
@@ -235,7 +235,7 @@ runner.use(pipes.ExtractState({ from: 'stakeAddress', to: 'stakeStates', status:
235
235
  runner.use((context, next) => {
236
236
  const { itx, txStates, rollupState } = context;
237
237
  try {
238
- const result = ensureBlockReward(rollupState.address, rollupState.tokenAddress, itx.minReward, txStates);
238
+ const result = ensureBlockReward(rollupState, itx.minReward, txStates);
239
239
  context.senders = Object.keys(result.accountUpdates);
240
240
  Object.assign(context, result);
241
241
  return next();
@@ -56,7 +56,7 @@ runner.use(
56
56
  );
57
57
 
58
58
  // Ensure sender exist
59
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
59
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
60
60
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
61
61
 
62
62
  // Ensure token not exist
@@ -98,7 +98,7 @@ runner.use(
98
98
  const [newSenderState, tokenState] = await Promise.all([
99
99
  statedb.account.update(
100
100
  senderState.address,
101
- account.update(senderState, { tokens: senderTokens, nonce: tx.nonce }, context),
101
+ account.update(senderState, { tokens: senderTokens, nonce: tx.nonce, pk: tx.pk }, context),
102
102
  context
103
103
  ),
104
104
  statedb.token.create(itx.address, token.create({ ...itx, data, issuer: senderState.address }, context), context),
@@ -11,7 +11,7 @@ const debug = require('debug')(`${require('../../../package.json').name}:deposit
11
11
 
12
12
  const VerifySigners = require('../rollup/pipes/verify-signers');
13
13
  const VerifyPaused = require('../rollup/pipes/verify-paused');
14
- const { applyTokenUpdates, getTxFee, getRewardLocker } = require('../../util');
14
+ const { applyTokenUpdates, getTxFee, getBNSum, getRewardLocker } = require('../../util');
15
15
 
16
16
  const schema = Joi.object({
17
17
  token: Joi.tokenInputSchema.required(),
@@ -138,15 +138,15 @@ runner.use((context, next) => {
138
138
  const { itx, rollupState, tokenState } = context;
139
139
  const { depositFeeRate, maxDepositFee, minDepositFee } = rollupState;
140
140
 
141
- context.txFee = getTxFee({
141
+ const { reward } = getTxFee({
142
142
  amount: itx.token.value,
143
143
  feeRate: depositFeeRate,
144
144
  maxFee: maxDepositFee,
145
145
  minFee: minDepositFee,
146
146
  });
147
147
 
148
- if (new BN(itx.actualFee).lt(new BN(context.txFee.reward))) {
149
- const expected = fromUnitToToken(context.txFee.reward, tokenState.decimal);
148
+ if (new BN(itx.actualFee).lt(new BN(reward))) {
149
+ const expected = fromUnitToToken(reward, tokenState.decimal);
150
150
  const actual = fromUnitToToken(itx.actualFee, tokenState.decimal);
151
151
  return next(new Error('INVALID_TX', `itx.actualFee too low, expect at least ${expected}, got ${actual}`));
152
152
  }
@@ -155,28 +155,33 @@ runner.use((context, next) => {
155
155
  });
156
156
 
157
157
  // 7. verify sender and signer states
158
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
158
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'OK', table: 'account' }));
159
159
  runner.use(pipes.ExtractState({ from: 'signers', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
160
160
  runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
161
161
 
162
162
  // 8. update state: the token minting is done when deposit finalized in rollup-block
163
163
  runner.use(
164
164
  async (context, next) => {
165
- const { tx, itx, txFee, statedb, senderState, stakeState, stakeAddress, lockerState, lockerAddress } = context;
165
+ const { tx, itx, statedb, senderState, stakeState, stakeAddress, lockerState, lockerAddress } = context;
166
166
 
167
- const stakeUpdates = applyTokenUpdates([{ address: itx.token.address, value: txFee.total }], stakeState, 'sub');
168
- const senderUpdates = applyTokenUpdates([{ address: itx.token.address, value: txFee.user }], senderState, 'add');
167
+ const user = itx.token.value;
168
+ const fee = itx.actualFee;
169
+ const total = getBNSum(user, fee);
170
+
171
+ const stakeUpdates = applyTokenUpdates([{ address: itx.token.address, value: total }], stakeState, 'sub');
172
+ const senderUpdates = applyTokenUpdates([{ address: itx.token.address, value: user }], senderState || {}, 'add');
169
173
  const lockerUpdates = applyTokenUpdates(
170
- [{ address: itx.token.address, value: txFee.reward }],
174
+ [{ address: itx.token.address, value: fee }],
171
175
  lockerState || { tokens: {} },
172
176
  'add'
173
177
  );
174
178
 
179
+ const sender = senderState ? senderState.address : tx.from;
175
180
  const [newSenderState, newStakeState, newLockerState, evidenceState] = await Promise.all([
176
- // update user account
177
- statedb.account.update(
178
- senderState.address,
179
- account.update(senderState, { nonce: tx.nonce, ...senderUpdates }, context),
181
+ // updateOrCreate user account
182
+ statedb.account.updateOrCreate(
183
+ senderState,
184
+ account.updateOrCreate(senderState, { address: sender, nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
180
185
  context
181
186
  ),
182
187
 
@@ -218,11 +223,11 @@ runner.use(
218
223
 
219
224
  context.updatedAccounts = [
220
225
  // stake for tx proposer is decreased
221
- { address: stakeAddress, token: itx.token.address, delta: `-${txFee.total}`, action: 'unlock' },
226
+ { address: stakeAddress, token: itx.token.address, delta: `-${total}`, action: 'unlock' },
222
227
  // mint to depositor from stake
223
- { address: senderState.address, token: itx.token.address, delta: txFee.user, action: 'unlock' },
224
- // block reward is locked for later claiming
225
- { address: lockerAddress, token: itx.token.address, delta: txFee.reward, action: 'pending' },
228
+ { address: sender, token: itx.token.address, delta: user, action: 'unlock' },
229
+ // tx fee is locked for later claiming
230
+ { address: lockerAddress, token: itx.token.address, delta: fee, action: 'pending' },
226
231
  ];
227
232
 
228
233
  debug('deposit-token-v2', itx);
@@ -99,15 +99,15 @@ runner.use((context, next) => {
99
99
 
100
100
  const isFixedFee = new BN(itx.maxFee).isZero();
101
101
  if (isFixedFee) {
102
- context.txFee = getTxFee({
102
+ const { reward } = getTxFee({
103
103
  amount: itx.token.value,
104
104
  feeRate: withdrawFeeRate,
105
105
  maxFee: maxWithdrawFee,
106
106
  minFee: minWithdrawFee,
107
107
  });
108
108
 
109
- if (new BN(itx.actualFee).lt(new BN(context.txFee.reward))) {
110
- const expected = fromUnitToToken(context.txFee.reward, tokenState.decimal);
109
+ if (new BN(itx.actualFee).lt(new BN(reward))) {
110
+ const expected = fromUnitToToken(reward, tokenState.decimal);
111
111
  const actual = fromUnitToToken(itx.actualFee, tokenState.decimal);
112
112
  return next(new Error('INVALID_TX', `itx.actualFee too low, expect at least ${expected}, got ${actual}`));
113
113
  }
@@ -81,8 +81,8 @@ runner.use(pipes.VerifyListSize({ listKey: ['senderTokens', 'receiverTokens'] })
81
81
 
82
82
  // TODO: anti-replay-exchange-attack
83
83
 
84
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
85
- runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK' }));
84
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
85
+ runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK', table: 'account' }));
86
86
  runner.use(pipes.VerifyDelegation({ type: 'signature', signerKey: 'senderState', delegatorKey: 'delegatorState' }));
87
87
  runner.use(pipes.ExtractSigner({ signerKey: 'signerStates', delegatorKey: 'delegatorStates' }));
88
88
  runner.use(
@@ -94,9 +94,9 @@ runner.use(
94
94
  })
95
95
  );
96
96
 
97
- runner.use(pipes.ExtractState({ from: 'tokenAddress', to: 'tokenStates', status: 'INVALID_TOKEN' }));
97
+ runner.use(pipes.ExtractState({ from: 'tokenAddress', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
98
98
 
99
- runner.use(pipes.ExtractState({ from: 'receiver', to: 'receiverState', status: 'INVALID_RECEIVER_STATE' }));
99
+ runner.use(pipes.ExtractState({ from: 'receiver', to: 'receiverState', status: 'INVALID_RECEIVER_STATE', table: 'account' })); // prettier-ignore
100
100
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState', signerKey: 'signerStates' }));
101
101
  runner.use((context, next) => {
102
102
  context.senderTokenConditions = {
@@ -114,11 +114,11 @@ runner.use(pipes.VerifyTokenBalance({ ownerKey: 'receiverState', conditionKey: '
114
114
 
115
115
  runner.use(pipes.AntiLandAttack({ senderState: 'senderState', receiverState: 'receiverState' }));
116
116
 
117
- runner.use(pipes.ExtractState({ from: 'senderAssets', to: 'priv.senderAssets', status: 'INVALID_ASSET' }));
117
+ runner.use(pipes.ExtractState({ from: 'senderAssets', to: 'priv.senderAssets', status: 'INVALID_ASSET', table: 'asset' })); // prettier-ignore
118
118
  runner.use(pipes.VerifyTransferrable({ assets: 'priv.senderAssets' }));
119
119
  runner.use(pipes.VerifyUpdater({ assetKey: 'priv.senderAssets', ownerKey: 'senderState' }));
120
120
 
121
- runner.use(pipes.ExtractState({ from: 'receiverAssets', to: 'priv.receiverAssets', status: 'INVALID_ASSET' }));
121
+ runner.use(pipes.ExtractState({ from: 'receiverAssets', to: 'priv.receiverAssets', status: 'INVALID_ASSET', table: 'asset' })); // prettier-ignore
122
122
  runner.use(pipes.VerifyTransferrable({ assets: 'priv.receiverAssets' }));
123
123
  runner.use(pipes.VerifyUpdater({ assetKey: 'priv.receiverAssets', ownerKey: 'receiverState' }));
124
124
 
@@ -62,10 +62,10 @@ runner.use((context, next) => {
62
62
  next();
63
63
  });
64
64
 
65
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
65
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
66
66
  runner.use(pipes.VerifyAccountMigration({ senderKey: 'senderState' }));
67
67
 
68
- runner.use(pipes.ExtractState({ from: 'tokenAddress', to: 'tokenStates', status: 'INVALID_TOKEN' }));
68
+ runner.use(pipes.ExtractState({ from: 'tokenAddress', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
69
69
  runner.use((context, next) => {
70
70
  context.tokenConditions = {
71
71
  owner: context.senderState.address,
@@ -75,24 +75,32 @@ runner.use((context, next) => {
75
75
  });
76
76
  runner.use(pipes.VerifyTokenBalance({ ownerKey: 'senderState', conditionKey: 'tokenConditions' }));
77
77
 
78
- runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK' }));
78
+ runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK', table: 'account' }));
79
79
  runner.use(pipes.VerifyDelegation({ type: 'signature', signerKey: 'senderState', delegatorKey: 'delegatorState' }));
80
80
 
81
- runner.use(pipes.ExtractReceiver({ from: 'itx.to' }));
82
- runner.use(pipes.ExtractState({ from: 'receiver', to: 'receiverState', status: 'INVALID_RECEIVER_STATE' }));
81
+ runner.use(pipes.ExtractReceiver({ from: 'itx.to', to: 'receiver' }));
82
+ runner.use(pipes.ExtractState({ from: 'receiver', to: 'receiverState', status: 'OK', table: 'account' }));
83
83
  runner.use(pipes.AntiLandAttack({ senderState: 'senderState', receiverState: 'receiverState' }));
84
84
 
85
- runner.use(pipes.ExtractState({ from: 'assets', to: 'assetStates', status: 'INVALID_ASSET' }));
85
+ runner.use(pipes.ExtractState({ from: 'assets', to: 'assetStates', status: 'INVALID_ASSET', table: 'asset' }));
86
86
  runner.use(pipes.VerifyTransferrable({ assets: 'assetStates' }));
87
87
  runner.use(pipes.VerifyUpdater({ assetKey: 'assetStates', ownerKey: 'senderState' }));
88
88
 
89
- runner.use(pipes.UpdateOwner({ assets: 'assetStates', owner: 'receiverState' }));
89
+ // transfer assets to new owner
90
+ runner.use((context, next) => {
91
+ const { itx, receiverState } = context;
92
+ context.receiverAddr = receiverState ? receiverState.address : itx.to;
93
+ return next();
94
+ });
95
+ runner.use(pipes.UpdateOwner({ assets: 'assetStates', owner: 'receiverAddr' }));
96
+
97
+ // update statedb: transfer tokens to new owner
90
98
  runner.use(
91
99
  async (context, next) => {
92
- const { tx, itx, tokens, senderState, receiverState, statedb } = context;
100
+ const { tx, itx, tokens, senderState, receiverAddr, receiverState, statedb } = context;
93
101
 
94
102
  const { tokens: senderTokens = {} } = senderState;
95
- const { tokens: receiverTokens = {} } = receiverState;
103
+ const { tokens: receiverTokens = {} } = receiverState || {};
96
104
  for (const token of tokens) {
97
105
  const delta = new BN(token.value);
98
106
  senderTokens[token.address] = new BN(senderTokens[token.address]).sub(delta).toString();
@@ -103,14 +111,14 @@ runner.use(
103
111
  // Update sender state
104
112
  statedb.account.update(
105
113
  senderState.address,
106
- account.update(senderState, { nonce: tx.nonce, tokens: senderTokens }, context),
114
+ account.update(senderState, { nonce: tx.nonce, pk: tx.pk, tokens: senderTokens }, context),
107
115
  context
108
116
  ),
109
117
 
110
118
  // Update receiver state
111
- statedb.account.update(
112
- receiverState.address,
113
- account.update(receiverState, { tokens: receiverTokens }, context),
119
+ statedb.account.updateOrCreate(
120
+ receiverState,
121
+ account.updateOrCreate(receiverState, { address: receiverAddr, tokens: receiverTokens }, context),
114
122
  context
115
123
  ),
116
124
  ]);
@@ -108,9 +108,9 @@ runner.use(
108
108
  runner.use(pipes.VerifyMultiSigV2({ signersKey: 'senders' }));
109
109
 
110
110
  // 4. verify sender & signer & receiver
111
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE' }));
112
- runner.use(pipes.ExtractState({ from: 'senders', to: 'signerStates', status: 'INVALID_SIGNER_STATE' }));
113
- runner.use(pipes.ExtractState({ from: 'receivers', to: 'receiverStates', status: 'INVALID_RECEIVER_STATE' }));
111
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
112
+ runner.use(pipes.ExtractState({ from: 'senders', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
113
+ runner.use(pipes.ExtractState({ from: 'receivers', to: 'receiverStates', status: 'OK', table: 'account' }));
114
114
  runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', senderKey: 'senderState' }));
115
115
 
116
116
  // 5. verify token state and balance
@@ -140,7 +140,17 @@ runner.use(async (context, next) => {
140
140
 
141
141
  runner.use(
142
142
  async (context, next) => {
143
- const { tx, inputs, outputs, senderState, signerStates, receiverStates, assetStates = [], statedb } = context;
143
+ const {
144
+ tx,
145
+ inputs,
146
+ outputs,
147
+ receivers,
148
+ senderState,
149
+ signerStates,
150
+ receiverStates = [],
151
+ assetStates = [],
152
+ statedb,
153
+ } = context;
144
154
 
145
155
  const signerUpdates = {};
146
156
  inputs.forEach((x) => {
@@ -157,7 +167,7 @@ runner.use(
157
167
  const { owner, tokensList } = x;
158
168
  receiverUpdates[owner] = applyTokenUpdates(
159
169
  tokensList,
160
- receiverStates.find((s) => s.address === owner),
170
+ receiverStates.find((s) => s.address === owner) || {},
161
171
  'add'
162
172
  );
163
173
  });
@@ -175,7 +185,6 @@ runner.use(
175
185
 
176
186
  debug('transfer-v3', { signerUpdates, receiverUpdates, assetUpdates, isAlsoSigner, isAlsoReceiver });
177
187
 
178
- // FIXME: skip statedb update if account not changed at all
179
188
  const [newSenderState, newSignerStates, newReceiverStates, newAssetStates] = await Promise.all([
180
189
  // Update sender state
181
190
  statedb.account.update(
@@ -183,7 +192,7 @@ runner.use(
183
192
  account.update(
184
193
  senderState,
185
194
  Object.assign(
186
- { nonce: tx.nonce },
195
+ { nonce: tx.nonce, pk: tx.pk },
187
196
  signerUpdates[senderState.address] || {},
188
197
  receiverUpdates[senderState.address] || {}
189
198
  ),
@@ -199,13 +208,18 @@ runner.use(
199
208
  .map((x) => statedb.account.update(x.address, account.update(x, signerUpdates[x.address], context), context))
200
209
  ),
201
210
 
202
- // Update receiver state
211
+ // UpdateOrCreate receiver state
203
212
  Promise.all(
204
- receiverStates
205
- .filter((x) => x.address !== senderState.address)
206
- .map((x) =>
207
- statedb.account.update(x.address, account.update(x, receiverUpdates[x.address], context), context)
208
- )
213
+ receivers
214
+ .filter((x) => x !== senderState.address)
215
+ .map((x) => {
216
+ const receiverState = receiverStates.find((s) => s.address === x);
217
+ return statedb.account.updateOrCreate(
218
+ receiverState,
219
+ account.updateOrCreate(receiverState, { ...receiverUpdates[x], address: x }, context),
220
+ context
221
+ );
222
+ })
209
223
  ),
210
224
 
211
225
  // Update asset state
package/lib/util.js CHANGED
@@ -7,6 +7,7 @@ const { toStakeAddress } = require('@arcblock/did-util');
7
7
  const cloneDeep = require('lodash/cloneDeep');
8
8
 
9
9
  const ZERO = new BN(0);
10
+ const RATE_BASE = new BN(10000);
10
11
 
11
12
  const decodeAnyNested = (encoded) => {
12
13
  if (!encoded) {
@@ -95,12 +96,24 @@ const fixTokenInput = (input, config) => {
95
96
  return input;
96
97
  };
97
98
 
98
- const RATE_BASE = new BN(10000);
99
99
  const getTxFee = ({ amount, feeRate, maxFee, minFee, stringify = true }) => {
100
100
  const userAmount = new BN(amount);
101
101
  const maxFeeAmount = new BN(maxFee);
102
102
  const minFeeAmount = new BN(minFee);
103
103
 
104
+ if (feeRate < 0) {
105
+ throw new Error('FORBIDDEN', 'Unexpected negative feeRate when getTxFee, abort!');
106
+ }
107
+ if (userAmount.lt(ZERO)) {
108
+ throw new Error('FORBIDDEN', 'Unexpected negative amount when getTxFee, abort!');
109
+ }
110
+ if (maxFeeAmount.lt(ZERO)) {
111
+ throw new Error('FORBIDDEN', 'Unexpected negative maxFee when getTxFee, abort!');
112
+ }
113
+ if (minFeeAmount.lt(ZERO)) {
114
+ throw new Error('FORBIDDEN', 'Unexpected negative minFee when getTxFee, abort!');
115
+ }
116
+
104
117
  // total fee
105
118
  let rewardAmount = userAmount.mul(new BN(feeRate)).div(RATE_BASE);
106
119
  if (rewardAmount.lt(minFeeAmount)) {
@@ -130,6 +143,15 @@ const getTxFee = ({ amount, feeRate, maxFee, minFee, stringify = true }) => {
130
143
 
131
144
  const splitTxFee = ({ total, shares = {}, stringify = true }) => {
132
145
  const totalAmount = new BN(total);
146
+ if (totalAmount.lt(ZERO)) {
147
+ throw new Error('FORBIDDEN', 'Unexpected negative total when splitTxFee, abort!');
148
+ }
149
+ Object.keys(shares).forEach((key) => {
150
+ if (shares[key] < 0) {
151
+ throw new Error('FORBIDDEN', `Unexpected negative shares[${key}] when splitTxFee, abort!`);
152
+ }
153
+ });
154
+
133
155
  const rewardShares = Object.keys(shares).reduce((acc, x) => {
134
156
  acc[x] = totalAmount.mul(new BN(shares[x])).div(RATE_BASE);
135
157
  return acc;
@@ -143,10 +165,11 @@ const splitTxFee = ({ total, shares = {}, stringify = true }) => {
143
165
 
144
166
  const getRewardLocker = (rollupAddress) => toStakeAddress(rollupAddress, rollupAddress);
145
167
  const getBNSum = (...args) => flattenDeep(args).reduce((sum, x) => sum.add(new BN(x)), new BN(0)).toString(10); // prettier-ignore
146
- const isFixedFee = (x) => new BN(x.tx.itxJson.maxFee).isZero();
168
+ const isFixedFee = (x) => !x.tx.itxJson.maxFee || new BN(x.tx.itxJson.maxFee).isZero();
147
169
 
148
- const ensureBlockReward = (rollupAddress, tokenAddress, minReward, txStates) => {
149
- const locker = toStakeAddress(rollupAddress, rollupAddress);
170
+ const ensureBlockReward = (rollupState, minReward, txStates) => {
171
+ const { address, withdrawFeeRate, minWithdrawFee, maxWithdrawFee, tokenAddress } = rollupState;
172
+ const locker = getRewardLocker(address);
150
173
 
151
174
  const result = {
152
175
  mintedAmount: new BN(0),
@@ -177,7 +200,25 @@ const ensureBlockReward = (rollupAddress, tokenAddress, minReward, txStates) =>
177
200
  const changes = { stake: [], account: [] };
178
201
  dynamicFeeTxs.forEach((x) => {
179
202
  const maxFee = new BN(x.tx.itxJson.maxFee);
180
- const actualFee = totalMissingFee.mul(maxFee).div(totalDynamicFee);
203
+ let actualFee = new BN(0);
204
+ // If totalMissingFee is less than 0, then the tx will be charged for fixedFee
205
+ if (totalMissingFee.lt(ZERO)) {
206
+ const fee = getTxFee({
207
+ amount: x.tx.itxJson.token.value,
208
+ feeRate: withdrawFeeRate,
209
+ maxFee: maxWithdrawFee,
210
+ minFee: minWithdrawFee,
211
+ stringify: false,
212
+ });
213
+ actualFee = fee.reward;
214
+ } else {
215
+ // Else the tx is charged for a portion of totalMissingFee
216
+ actualFee = totalMissingFee.mul(maxFee).div(totalDynamicFee);
217
+ }
218
+
219
+ if (actualFee.lt(ZERO)) {
220
+ throw new Error('FORBIDDEN', 'Got negative actualFee for tx, abort!');
221
+ }
181
222
 
182
223
  // If the actualFee is less than the maxFee, user will have a refund
183
224
  if (actualFee.lt(maxFee)) {
@@ -209,7 +250,7 @@ const ensureBlockReward = (rollupAddress, tokenAddress, minReward, txStates) =>
209
250
 
210
251
  // mint tokens for deposit proposer
211
252
  changes.stake.push({
212
- address: toStakeAddress(x.tx.itxJson.proposer, rollupAddress),
253
+ address: toStakeAddress(x.tx.itxJson.proposer, address),
213
254
  delta: total,
214
255
  action: 'mint',
215
256
  });
@@ -219,7 +260,7 @@ const ensureBlockReward = (rollupAddress, tokenAddress, minReward, txStates) =>
219
260
 
220
261
  // burn tokens from locked withdraws: user amount
221
262
  changes.stake.push({
222
- address: toStakeAddress(x.tx.from, rollupAddress),
263
+ address: toStakeAddress(x.tx.from, address),
223
264
  delta: `-${user}`,
224
265
  action: 'burn',
225
266
  });
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.13.61",
6
+ "version": "1.13.65",
7
7
  "description": "Predefined tx pipeline sets to execute certain type of transactions",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -19,16 +19,16 @@
19
19
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
- "@arcblock/did": "1.13.61",
23
- "@arcblock/did-util": "1.13.61",
24
- "@ocap/asset": "1.13.61",
25
- "@ocap/mcrypto": "1.13.61",
26
- "@ocap/merkle-tree": "1.13.61",
27
- "@ocap/message": "1.13.61",
28
- "@ocap/state": "1.13.61",
29
- "@ocap/tx-pipeline": "1.13.61",
30
- "@ocap/util": "1.13.61",
31
- "@ocap/wallet": "1.13.61",
22
+ "@arcblock/did": "1.13.65",
23
+ "@arcblock/did-util": "1.13.65",
24
+ "@ocap/asset": "1.13.65",
25
+ "@ocap/mcrypto": "1.13.65",
26
+ "@ocap/merkle-tree": "1.13.65",
27
+ "@ocap/message": "1.13.65",
28
+ "@ocap/state": "1.13.65",
29
+ "@ocap/tx-pipeline": "1.13.65",
30
+ "@ocap/util": "1.13.65",
31
+ "@ocap/wallet": "1.13.65",
32
32
  "debug": "^4.3.2",
33
33
  "empty-value": "^1.0.1",
34
34
  "lodash": "^4.17.21",
@@ -41,5 +41,5 @@
41
41
  "devDependencies": {
42
42
  "jest": "^27.3.1"
43
43
  },
44
- "gitHead": "79d07bbde4df1d0d37afe76aaa1eaa2f30cc55a7"
44
+ "gitHead": "4011996e1800845142aa5c889b58726129a99ec3"
45
45
  }