@ocap/tx-protocols 1.22.3 → 1.23.1

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.
@@ -1,23 +1,22 @@
1
1
  const { CustomError: Error } = require('@ocap/util/lib/error');
2
2
  const { Joi, schemas } = require('@arcblock/validator');
3
- const { BN, isSameDid, fromUnitToToken } = require('@ocap/util');
3
+ const { BN, fromUnitToToken } = require('@ocap/util');
4
4
  const { Runner, pipes } = require('@ocap/tx-pipeline');
5
5
  const { account, tokenFactory, token } = require('@ocap/state');
6
+ const pick = require('lodash/pick');
6
7
 
7
8
  const EnsureTxGas = require('../../pipes/ensure-gas');
8
9
  const EnsureTxCost = require('../../pipes/ensure-cost');
9
- const CalcReserveAmount = require('./pipes/calc-reserve');
10
- const { applyTokenChange } = require('../../util');
10
+ const CalcReserve = require('./pipes/calc-reserve');
11
+ const { applyTokenChange, applyTokenUpdates } = require('../../util');
11
12
 
12
13
  const runner = new Runner();
13
14
 
14
- runner.use(pipes.VerifyMultiSig(0));
15
-
16
15
  const schema = Joi.object({
17
16
  tokenFactory: Joi.DID().prefix().role('ROLE_TOKEN_FACTORY').required(),
18
17
  receiver: schemas.tokenHolder.required(),
19
- amount: Joi.BN().greater(0).required(),
20
18
  minReserve: Joi.alternatives().try(Joi.BN().greater(0), Joi.equal('')).optional(),
19
+ inputsList: schemas.multiInput.min(1).required(),
21
20
  data: Joi.any().optional().allow(null),
22
21
  }).options({ stripUnknown: true, noDefaults: false });
23
22
 
@@ -30,6 +29,23 @@ runner.use(({ itx }, next) => {
30
29
  return next();
31
30
  });
32
31
 
32
+ // verify inputs
33
+ runner.use(
34
+ pipes.VerifyTxInput({
35
+ fieldKey: 'itx.inputs',
36
+ inputsKey: 'inputs',
37
+ sendersKey: 'senders',
38
+ tokensKey: 'tokens',
39
+ assetsKey: null,
40
+ })
41
+ );
42
+
43
+ // verify itx size: set hard limit here because more inputs leads to longer tx execute time
44
+ runner.use(pipes.VerifyListSize({ listKey: ['inputs', 'senders', 'tokens', 'assets'] }));
45
+
46
+ // verify multi sig
47
+ runner.use(pipes.VerifyMultiSigV2({ signersKey: 'senders' }));
48
+
33
49
  // verify token factory
34
50
  runner.use(
35
51
  pipes.ExtractState({
@@ -40,14 +56,37 @@ runner.use(
40
56
  })
41
57
  );
42
58
 
59
+ // verify inputs token
60
+ runner.use((context, next) => {
61
+ const { tokenFactoryState, inputs } = context;
62
+
63
+ const isAccepted = inputs.every(
64
+ (input) =>
65
+ input.tokensList.length &&
66
+ input.tokensList.every((x) => x.address === tokenFactoryState.tokenAddress) &&
67
+ !input.assetsList.length
68
+ );
69
+
70
+ if (!isAccepted) {
71
+ return next(new Error('INVALID_TX', `Inputs only accept ${tokenFactoryState.tokenAddress}`));
72
+ }
73
+
74
+ return next();
75
+ });
76
+
43
77
  // ensure sender
44
- runner.use(
45
- pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })
46
- );
78
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'OK', table: 'account' }));
47
79
  runner.use(pipes.VerifyAccountMigration({ stateKey: 'senderState', addressKey: 'tx.from' }));
48
80
 
81
+ runner.use(pipes.ExtractState({ from: 'senders', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
82
+ runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', stateKey: 'senderState', addressKey: 'tx.from' }));
83
+
49
84
  // ensure receiver
50
85
  runner.use(pipes.ExtractState({ from: 'itx.receiver', to: 'receiverState', status: 'OK', table: 'account' }));
86
+ runner.use(pipes.VerifyAccountMigration({ stateKey: 'receiverState', addressKey: 'itx.receiver' }));
87
+
88
+ // verify blocked
89
+ runner.use(pipes.VerifyBlocked({ stateKeys: ['signerStates', 'receiverState'] }));
51
90
 
52
91
  // ensure owner
53
92
  runner.use(
@@ -59,9 +98,6 @@ runner.use(
59
98
  })
60
99
  );
61
100
 
62
- // verify blocked
63
- runner.use(pipes.VerifyBlocked({ stateKeys: ['senderState', 'receiverState'] }));
64
-
65
101
  // ensure token state
66
102
  runner.use(
67
103
  pipes.ExtractState({
@@ -72,13 +108,32 @@ runner.use(
72
108
  })
73
109
  );
74
110
 
75
- // calculate reserve amount
111
+ // calculate burn amount
112
+ runner.use((context, next) => {
113
+ const { inputs, tokenFactoryState } = context;
114
+ let amount = new BN('0');
115
+
116
+ for (const { tokensList } of inputs) {
117
+ const delta = tokensList.find((x) => x.address === tokenFactoryState.tokenAddress)?.value;
118
+ if (!delta) {
119
+ return next(new Error('INVALID_TX', 'Invalid inputs'));
120
+ }
121
+ amount = amount.add(new BN(delta));
122
+ }
123
+
124
+ context.burnAmount = amount.toString();
125
+
126
+ return next();
127
+ });
128
+
129
+ // calculate reserve amount / fee
76
130
  runner.use(
77
- CalcReserveAmount({
131
+ CalcReserve({
78
132
  tokenFactoryKey: 'tokenFactoryState',
79
133
  tokenStateKey: 'tokenState',
80
134
  reserveKey: 'reserveAmount',
81
135
  feeKey: 'reserveFee',
136
+ amountKey: 'burnAmount',
82
137
  direction: 'burn',
83
138
  })
84
139
  );
@@ -88,7 +143,7 @@ runner.use((context, next) => {
88
143
  const { reserveAmount, reserveFee } = context;
89
144
  const { minReserve } = context.itx;
90
145
 
91
- if (minReserve && new BN(reserveAmount).lt(new BN(minReserve).add(new BN(reserveFee)))) {
146
+ if (minReserve && new BN(reserveAmount).lt(new BN(minReserve).add(new BN(reserveFee || '0')))) {
92
147
  return next(
93
148
  new Error(
94
149
  'SLIPPAGE_EXCEEDED',
@@ -97,36 +152,48 @@ runner.use((context, next) => {
97
152
  );
98
153
  }
99
154
 
100
- next();
155
+ return next();
101
156
  });
102
157
 
103
- // verify sender tokens
104
- runner.use((context, next) => {
105
- const { tokenFactoryState, itx, reserveFee } = context;
106
-
107
- context.tokenConditions = {
108
- owner: context.senderState.address,
109
- tokens: [{ address: tokenFactoryState.tokenAddress, value: itx.amount }],
110
- };
158
+ // verify balance
159
+ runner.use(pipes.ExtractState({ from: 'tokens', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
160
+ runner.use(pipes.VerifyTokenBalance({ ownerKey: 'signerStates', conditionKey: 'inputs' }));
111
161
 
112
- if (reserveFee) {
113
- context.tokenConditions.tokens.push({ address: tokenFactoryState.reserveAddress, value: reserveFee });
162
+ // verify token factory balance
163
+ runner.use((context, next) => {
164
+ const { tokenFactoryState } = context;
165
+ if (new BN(tokenFactoryState.currentSupply).lt(new BN(context.burnAmount))) {
166
+ return next(new Error('INSUFFICIENT_FUND', 'Token factory supply is not enough'));
114
167
  }
115
-
116
- next();
168
+ if (new BN(tokenFactoryState.reserveBalance).lt(new BN(context.reserveAmount))) {
169
+ return next(new Error('INSUFFICIENT_FUND', 'Token factory reserve balance is not enough'));
170
+ }
171
+ return next();
117
172
  });
118
- runner.use(pipes.VerifyTokenBalance({ ownerKey: 'senderState', conditionKey: 'tokenConditions' }));
119
173
 
120
174
  // Ensure tx fee and gas
121
175
  runner.use(
122
176
  EnsureTxGas((context) => {
123
- const result = { create: 0, update: 3, payment: 0 };
177
+ const result = { create: 0, update: 2, payment: 0 };
178
+
179
+ if (context.senderState) {
180
+ result.update += 1;
181
+ } else {
182
+ result.create += 1;
183
+ }
124
184
 
125
185
  if (context.receiverState) {
126
186
  result.update += 1;
187
+ } else {
188
+ result.create += 1;
127
189
  }
190
+
128
191
  if (context.reserveFee) {
129
- result.update += 1;
192
+ result.update += 1; // owner
193
+ }
194
+
195
+ if (context.signerStates) {
196
+ result.update += context.signerStates.length;
130
197
  }
131
198
 
132
199
  return result;
@@ -153,62 +220,78 @@ runner.use(
153
220
  reserveAmount,
154
221
  reserveFee,
155
222
  itx,
223
+ inputs,
224
+ signerStates,
225
+ burnAmount,
156
226
  } = context;
157
- const { amount, receiver } = itx;
227
+ const { receiver } = itx;
158
228
  const { reserveAddress, tokenAddress } = tokenFactoryState;
159
229
 
160
- const { tokens: senderTokens = {} } = senderState;
161
- const { tokens: receiverTokens = {} } = receiverState || {};
162
- const { tokens: ownerTokens = {} } = ownerState;
163
-
164
- senderTokens[tokenAddress] = new BN(senderTokens[tokenAddress]).sub(new BN(amount)).toString();
165
- receiverTokens[reserveAddress] = new BN(receiverTokens[reserveAddress] || '0')
166
- .add(new BN(reserveAmount))
167
- .toString();
168
-
169
- if (isSameDid(senderState.address, receiver)) {
170
- senderTokens[reserveAddress] = receiverTokens[reserveAddress];
230
+ const accountUpdates = {};
231
+ const inputChanges = [];
232
+
233
+ // update signer burns
234
+ inputs.forEach(({ owner, tokensList }) => {
235
+ const tokenChange = tokensList.find((item) => item.address === tokenAddress);
236
+
237
+ accountUpdates[owner] = applyTokenUpdates(
238
+ [tokenChange],
239
+ signerStates.find((s) => s.address === owner),
240
+ 'sub'
241
+ );
242
+
243
+ inputChanges.push({
244
+ address: owner,
245
+ token: tokenAddress,
246
+ delta: new BN(tokenChange.value).neg().toString(),
247
+ });
248
+ });
249
+
250
+ // update sender
251
+ if (senderChange) {
252
+ accountUpdates[senderChange.address] = applyTokenChange(accountUpdates[senderChange.address], senderChange);
171
253
  }
172
- if (reserveFee) {
173
- const fee = new BN(reserveFee);
174
- senderTokens[reserveAddress] = new BN(senderTokens[reserveAddress]).sub(fee).toString();
175
-
176
- if (isSameDid(receiver, ownerState.address)) {
177
- receiverTokens[reserveAddress] = new BN(receiverTokens[reserveAddress] || '0').add(fee).toString();
178
- } else {
179
- ownerTokens[reserveAddress] = new BN(ownerTokens[reserveAddress] || '0').add(fee).toString();
180
- }
254
+ // Ensure the sender exists in accountUpdates, because even if there is no balance change, we need to update his nonce
255
+ if (!accountUpdates[tx.from]) {
256
+ accountUpdates[tx.from] = senderState || {};
181
257
  }
182
258
 
183
- const senderUpdates = senderChange
184
- ? applyTokenChange({ tokens: senderTokens }, senderChange)
185
- : { tokens: senderTokens };
259
+ // update owner
260
+ if (reserveFee && new BN(reserveFee).gt(0)) {
261
+ accountUpdates[ownerState.address] = applyTokenChange(accountUpdates[ownerState.address] || ownerState, {
262
+ address: ownerState.address,
263
+ token: reserveAddress,
264
+ delta: reserveFee,
265
+ });
266
+ }
186
267
 
187
- const [newSenderState, newReceiverState, newOwnerState, newTokenFactoryState, newTokenState] = await Promise.all([
188
- // Update sender state
189
- statedb.account.update(
190
- senderState.address,
191
- account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
192
- context
193
- ),
268
+ // update receiver
269
+ accountUpdates[receiver] = applyTokenChange(
270
+ accountUpdates[receiver] || receiverState || { address: receiver, tokens: {} },
271
+ {
272
+ address: receiver,
273
+ token: reserveAddress,
274
+ delta: new BN(reserveAmount).sub(new BN(reserveFee || '0')).toString(), // reserveAmount - reserveFee
275
+ }
276
+ );
194
277
 
195
- // Update receiver state
196
- !isSameDid(senderState.address, receiver)
197
- ? statedb.account.updateOrCreate(
198
- receiverState,
199
- account.updateOrCreate(receiverState, { address: receiver, tokens: receiverTokens }, context),
200
- context
201
- )
202
- : null,
203
-
204
- // Update owner state
205
- !isSameDid(senderState.address, ownerState.address) && !isSameDid(receiver, ownerState.address)
206
- ? statedb.account.update(
207
- ownerState.address,
208
- account.update(ownerState, { tokens: ownerTokens }, context),
209
- context
210
- )
211
- : null,
278
+ const [newAccountStates, newTokenFactoryState, newTokenState] = await Promise.all([
279
+ // update accounts
280
+ Promise.all(
281
+ Object.entries(accountUpdates).map(([address, updates]) => {
282
+ // We can use updateOrCreate here because the owner and signer have already been validated to exist earlier,
283
+ // the sender and receiver are allowed to be created in the transaction.
284
+ const state = [senderState, receiverState, ownerState, ...signerStates].find((x) => x?.address === address);
285
+
286
+ // Should only update nonce and pk for sender and new accounts
287
+ const actualUpdates =
288
+ address === tx.from || !state
289
+ ? { ...updates, address, nonce: tx.nonce, pk: tx.pk }
290
+ : pick(updates, ['tokens']);
291
+
292
+ return statedb.account.updateOrCreate(state, account.updateOrCreate(state, actualUpdates, context), context);
293
+ })
294
+ ),
212
295
 
213
296
  // Update token factory state
214
297
  statedb.tokenFactory.update(
@@ -216,7 +299,7 @@ runner.use(
216
299
  tokenFactory.update(
217
300
  tokenFactoryState,
218
301
  {
219
- currentSupply: new BN(tokenFactoryState.currentSupply).sub(new BN(amount)).toString(),
302
+ currentSupply: new BN(tokenFactoryState.currentSupply).sub(new BN(burnAmount)).toString(),
220
303
  reserveBalance: new BN(tokenFactoryState.reserveBalance).sub(new BN(reserveAmount)).toString(),
221
304
  },
222
305
  context
@@ -229,7 +312,7 @@ runner.use(
229
312
  token.update(
230
313
  tokenState,
231
314
  {
232
- totalSupply: new BN(tokenState.totalSupply).sub(new BN(amount)).toString(),
315
+ totalSupply: new BN(tokenState.totalSupply).sub(new BN(burnAmount)).toString(),
233
316
  },
234
317
  context
235
318
  ),
@@ -237,12 +320,17 @@ runner.use(
237
320
  ),
238
321
  ]);
239
322
 
240
- context.senderState = newSenderState;
241
- context.receiverState = newReceiverState;
242
- context.ownerState = newOwnerState;
323
+ context.senderState = newAccountStates.find((x) => x.address === tx.from);
324
+ context.receiverState = newAccountStates.find((x) => x.address === receiver);
325
+ // owner maybe not updated
326
+ context.ownerState = newAccountStates.find((x) => x.address === ownerState.address) || ownerState;
327
+ context.signerStates = newAccountStates.filter((x) => signerStates.find((s) => s.address === x.address));
243
328
  context.tokenFactoryState = newTokenFactoryState;
244
329
  context.tokenState = newTokenState;
245
330
 
331
+ // save this for receipt generation
332
+ context.inputChanges = inputChanges;
333
+
246
334
  await updateVaults();
247
335
 
248
336
  next();
@@ -3,14 +3,14 @@ const cloneDeep = require('lodash/cloneDeep');
3
3
  const { Joi } = require('@arcblock/validator');
4
4
  const { CustomError: Error } = require('@ocap/util/lib/error');
5
5
  const { Runner, pipes } = require('@ocap/tx-pipeline');
6
- const { account, token, tokenFactory } = require('@ocap/state');
6
+ const { account, token, tokenFactory, delegation } = require('@ocap/state');
7
7
  const { toTokenFactoryAddress, toTokenAddress } = require('@arcblock/did-util');
8
8
  const { applyTokenChange } = require('../../util');
9
9
 
10
10
  // eslint-disable-next-line global-require, import/order
11
11
  const debug = require('debug')(`${require('../../../package.json').name}:create-token-factory`);
12
12
 
13
- const { decodeAnySafe } = require('../../util');
13
+ const { decodeAnySafe, getDelegationRequirements } = require('../../util');
14
14
 
15
15
  const EnsureTxGas = require('../../pipes/ensure-gas');
16
16
  const EnsureTxCost = require('../../pipes/ensure-cost');
@@ -121,6 +121,18 @@ runner.use(pipes.VerifyAccountMigration({ stateKey: 'senderState', addressKey: '
121
121
 
122
122
  runner.use(pipes.VerifyBlocked({ stateKeys: ['senderState'] }));
123
123
 
124
+ // Ensure delegation
125
+ runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK', table: 'account' }));
126
+ runner.use(pipes.VerifyAccountMigration({ stateKey: 'delegatorState', addressKey: 'tx.delegator' }));
127
+ runner.use(
128
+ pipes.VerifyDelegation({
129
+ type: 'signature',
130
+ signerKey: 'senderState',
131
+ delegatorKey: 'delegatorState',
132
+ getRequirements: getDelegationRequirements,
133
+ })
134
+ );
135
+
124
136
  // Ensure uniqueness of token symbol
125
137
  runner.use(
126
138
  async function EnsureTokenSymbol(context, next) {
@@ -138,8 +150,14 @@ runner.use(
138
150
 
139
151
  // Ensure tx fee and gas
140
152
  runner.use(
141
- EnsureTxGas(() => {
142
- return { create: 1, update: 2, payment: 0 };
153
+ EnsureTxGas((context) => {
154
+ const result = { create: 2, update: 1, payment: 0 };
155
+
156
+ if (context.isDelegationChanged) {
157
+ result.update += 1;
158
+ }
159
+
160
+ return result;
143
161
  })
144
162
  );
145
163
  runner.use(EnsureTxCost({ attachSenderChanges: true }));
@@ -150,9 +168,9 @@ runner.use(pipes.TakeStateSnapshot());
150
168
  // Update sender state, token factory state
151
169
  runner.use(
152
170
  async (context, next) => {
153
- const { tx, itx, statedb, senderState, senderChange, updateVaults } = context;
171
+ const { tx, itx, statedb, senderState, delegatorState, delegationState, senderChange, updateVaults } = context;
154
172
  const data = decodeAnySafe(itx.data);
155
- const owner = senderState.address;
173
+ const owner = delegatorState ? delegatorState.address : senderState.address;
156
174
 
157
175
  const itxToken = itx.token;
158
176
 
@@ -162,7 +180,7 @@ runner.use(
162
180
  ? applyTokenChange({ tokens: senderTokens }, senderChange)
163
181
  : { tokens: senderTokens };
164
182
 
165
- const [newSenderState, newTokenState, newTokenFactoryState] = await Promise.all([
183
+ const [newSenderState, newTokenState, newTokenFactoryState, newDelegationState] = await Promise.all([
166
184
  statedb.account.update(
167
185
  senderState.address,
168
186
  account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
@@ -189,6 +207,11 @@ runner.use(
189
207
  tokenFactory.create({ ...cloneDeep(itx), owner, tokenAddress: itxToken.address, data }, context),
190
208
  context
191
209
  ),
210
+
211
+ // Update delegation state
212
+ context.isDelegationChanged
213
+ ? statedb.delegation.update(delegationState.address, delegation.update(delegationState, {}, context), context)
214
+ : delegationState,
192
215
  ]);
193
216
 
194
217
  await updateVaults();
@@ -196,6 +219,7 @@ runner.use(
196
219
  context.senderState = newSenderState;
197
220
  context.tokenState = newTokenState;
198
221
  context.tokenFactoryState = newTokenFactoryState;
222
+ context.delegationState = newDelegationState;
199
223
 
200
224
  debug('create token factory', newTokenFactoryState);
201
225
 
@@ -1,24 +1,23 @@
1
1
  const { CustomError: Error } = require('@ocap/util/lib/error');
2
2
  const { Joi, schemas } = require('@arcblock/validator');
3
- const { BN, isSameDid, fromUnitToToken } = require('@ocap/util');
3
+ const { BN, fromUnitToToken } = require('@ocap/util');
4
4
  const { Runner, pipes } = require('@ocap/tx-pipeline');
5
5
  const { account, tokenFactory, token } = require('@ocap/state');
6
+ const pick = require('lodash/pick');
6
7
 
7
8
  const EnsureTxGas = require('../../pipes/ensure-gas');
8
9
  const EnsureTxCost = require('../../pipes/ensure-cost');
9
10
  const CalcReserve = require('./pipes/calc-reserve');
10
- const { applyTokenChange } = require('../../util');
11
+ const { applyTokenChange, applyTokenUpdates } = require('../../util');
11
12
 
12
13
  const runner = new Runner();
13
14
 
14
- runner.use(pipes.VerifyMultiSig(0));
15
-
16
15
  const schema = Joi.object({
17
16
  tokenFactory: Joi.DID().prefix().role('ROLE_TOKEN_FACTORY').required(),
18
17
  receiver: schemas.tokenHolder.required(),
19
18
  amount: Joi.BN().greater(0).required(),
20
- maxReserve: Joi.alternatives().try(Joi.BN().greater(0), Joi.equal('')).optional(),
21
19
  data: Joi.any().optional().allow(null),
20
+ inputsList: schemas.multiInput.min(1).required(),
22
21
  }).options({ stripUnknown: true, noDefaults: false });
23
22
 
24
23
  // verify itx
@@ -30,6 +29,23 @@ runner.use(({ itx }, next) => {
30
29
  return next();
31
30
  });
32
31
 
32
+ // verify inputs
33
+ runner.use(
34
+ pipes.VerifyTxInput({
35
+ fieldKey: 'itx.inputs',
36
+ inputsKey: 'inputs',
37
+ sendersKey: 'senders',
38
+ tokensKey: 'tokens',
39
+ assetsKey: null,
40
+ })
41
+ );
42
+
43
+ // verify itx size: set hard limit here because more inputs leads to longer tx execute time
44
+ runner.use(pipes.VerifyListSize({ listKey: ['inputs', 'senders', 'tokens', 'assets'] }));
45
+
46
+ // verify multi sig
47
+ runner.use(pipes.VerifyMultiSigV2({ signersKey: 'senders' }));
48
+
33
49
  // verify token factory
34
50
  runner.use(
35
51
  pipes.ExtractState({
@@ -40,14 +56,35 @@ runner.use(
40
56
  })
41
57
  );
42
58
 
59
+ // verify inputs token
60
+ runner.use((context, next) => {
61
+ const { tokenFactoryState, inputs } = context;
62
+
63
+ const isAccepted = inputs.every(
64
+ (input) =>
65
+ input.tokensList.length &&
66
+ input.tokensList.every((x) => x.address === tokenFactoryState.reserveAddress) &&
67
+ !input.assetsList.length
68
+ );
69
+
70
+ if (!isAccepted) {
71
+ return next(new Error('INVALID_TX', `Inputs only accept ${tokenFactoryState.reserveAddress}`));
72
+ }
73
+
74
+ return next();
75
+ });
76
+
43
77
  // ensure sender
44
- runner.use(
45
- pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })
46
- );
47
- runner.use(pipes.VerifyAccountMigration({ stateKey: 'senderState', addressKey: 'tx.from' }));
78
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'OK', table: 'account' }));
79
+ runner.use(pipes.ExtractState({ from: 'senders', to: 'signerStates', status: 'INVALID_SIGNER_STATE', table: 'account' })); // prettier-ignore
80
+ runner.use(pipes.VerifyAccountMigration({ signerKey: 'signerStates', stateKey: 'senderState', addressKey: 'tx.from' }));
48
81
 
49
82
  // ensure receiver
50
83
  runner.use(pipes.ExtractState({ from: 'itx.receiver', to: 'receiverState', status: 'OK', table: 'account' }));
84
+ runner.use(pipes.VerifyAccountMigration({ stateKey: 'receiverState', addressKey: 'itx.receiver' }));
85
+
86
+ // verify blocked
87
+ runner.use(pipes.VerifyBlocked({ stateKeys: ['signerStates', 'receiverState'] }));
51
88
 
52
89
  // ensure owner
53
90
  runner.use(
@@ -59,9 +96,6 @@ runner.use(
59
96
  })
60
97
  );
61
98
 
62
- // verify blocked
63
- runner.use(pipes.VerifyBlocked({ stateKeys: ['senderState', 'receiverState'] }));
64
-
65
99
  // ensure token state
66
100
  runner.use(
67
101
  pipes.ExtractState({
@@ -74,20 +108,32 @@ runner.use(
74
108
 
75
109
  // calculate reserve amount
76
110
  runner.use(
77
- CalcReserve({ tokenFactoryKey: 'tokenFactoryState', tokenStateKey: 'tokenState', reserveKey: 'reserveAmount' })
111
+ CalcReserve({
112
+ tokenFactoryKey: 'tokenFactoryState',
113
+ tokenStateKey: 'tokenState',
114
+ reserveKey: 'reserveAmount',
115
+ amountKey: 'itx.amount',
116
+ })
78
117
  );
79
118
 
80
119
  // verify slippage
81
120
  runner.use((context, next) => {
82
- const { reserveAmount, reserveFee } = context;
83
- const { maxReserve } = context.itx;
121
+ const { reserveAmount, reserveFee, inputs, tokenFactoryState } = context;
122
+
123
+ const maxReserve = inputs.reduce((total, input) => {
124
+ const reserveToken = input.tokensList.find((x) => x.address === tokenFactoryState.reserveAddress);
125
+ if (reserveToken) {
126
+ return total.add(new BN(reserveToken.value));
127
+ }
128
+ return total;
129
+ }, new BN(0));
84
130
 
85
131
  const actual = new BN(reserveAmount).add(new BN(reserveFee || '0'));
86
- if (maxReserve && actual.gt(new BN(maxReserve))) {
132
+ if (actual.gt(maxReserve)) {
87
133
  return next(
88
134
  new Error(
89
135
  'SLIPPAGE_EXCEEDED',
90
- `Mint token failed due to price movement. Expected maximum: ${fromUnitToToken(maxReserve)}, actual: ${fromUnitToToken(actual)}. Try increasing your maxReserve.`
136
+ `Mint token failed due to price movement. Expected maximum: ${fromUnitToToken(maxReserve)}, actual: ${fromUnitToToken(actual)}. Try increasing your inputs.`
91
137
  )
92
138
  );
93
139
  }
@@ -95,32 +141,33 @@ runner.use((context, next) => {
95
141
  next();
96
142
  });
97
143
 
98
- // verify sender balance
99
- runner.use((context, next) => {
100
- const { tokenFactoryState } = context;
101
- context.tokenConditions = {
102
- owner: context.senderState.address,
103
- tokens: [
104
- {
105
- address: tokenFactoryState.reserveAddress,
106
- value: new BN(context.reserveAmount).add(new BN(context.reserveFee || '0')).toString(),
107
- },
108
- ],
109
- };
110
- next();
111
- });
112
- runner.use(pipes.VerifyTokenBalance({ ownerKey: 'senderState', conditionKey: 'tokenConditions' }));
144
+ // verify balance
145
+ runner.use(pipes.ExtractState({ from: 'tokens', to: 'tokenStates', status: 'INVALID_TOKEN', table: 'token' }));
146
+ runner.use(pipes.VerifyTokenBalance({ ownerKey: 'signerStates', conditionKey: 'inputs' }));
113
147
 
114
148
  // Ensure tx fee and gas
115
149
  runner.use(
116
150
  EnsureTxGas((context) => {
117
- const result = { create: 0, update: 3, payment: 0 };
151
+ const result = { create: 0, update: 2, payment: 0 };
152
+
153
+ if (context.senderState) {
154
+ result.update += 1;
155
+ } else {
156
+ result.create += 1;
157
+ }
118
158
 
119
159
  if (context.receiverState) {
120
160
  result.update += 1;
161
+ } else {
162
+ result.create += 1;
121
163
  }
164
+
122
165
  if (context.reserveFee) {
123
- result.update += 1;
166
+ result.update += 1; // owner
167
+ }
168
+
169
+ if (context.signerStates) {
170
+ result.update += context.signerStates.length;
124
171
  }
125
172
 
126
173
  return result;
@@ -143,65 +190,93 @@ runner.use(
143
190
  senderChange,
144
191
  updateVaults,
145
192
  tokenFactoryState,
193
+ signerStates,
146
194
  tokenState,
147
195
  reserveAmount,
148
196
  reserveFee,
149
197
  itx,
198
+ inputs,
150
199
  } = context;
151
200
  const { amount, receiver } = itx;
152
201
  const { reserveAddress, tokenAddress } = tokenFactoryState;
153
202
 
154
- const { tokens: senderTokens = {} } = senderState;
155
- const { tokens: receiverTokens = {} } = receiverState || {};
156
- const { tokens: ownerTokens = {} } = ownerState;
203
+ const totalReserveCost = new BN(reserveAmount).add(new BN(reserveFee || '0'));
204
+ let currentReserveCost = new BN('0');
157
205
 
158
- senderTokens[reserveAddress] = new BN(senderTokens[reserveAddress]).sub(new BN(reserveAmount)).toString();
159
- receiverTokens[tokenAddress] = new BN(receiverTokens[tokenAddress] || '0').add(new BN(amount)).toString();
206
+ const accountUpdates = {};
207
+ const inputChanges = [];
160
208
 
161
- // merge receiver tokens to sender tokens
162
- if (isSameDid(senderState.address, receiver)) {
163
- senderTokens[tokenAddress] = receiverTokens[tokenAddress];
164
- }
165
- if (reserveFee) {
166
- const fee = new BN(reserveFee);
167
- senderTokens[reserveAddress] = new BN(senderTokens[reserveAddress]).sub(fee).toString();
168
-
169
- if (isSameDid(receiver, ownerState.address)) {
170
- receiverTokens[reserveAddress] = new BN(receiverTokens[reserveAddress] || '0').add(fee).toString();
171
- } else {
172
- ownerTokens[reserveAddress] = new BN(ownerTokens[reserveAddress] || '0').add(fee).toString();
209
+ // update signer costs
210
+ inputs.forEach(({ owner, tokensList }) => {
211
+ const reserveTokenChange = tokensList.find((item) => item.address === reserveAddress);
212
+ let reserveValue = new BN(reserveTokenChange.value);
213
+
214
+ // The inputs represent the estimated maximum cost from the frontend.
215
+ // If the actual cost is less than the maximum cost, we need to adjust the deduction correctly.
216
+ if (currentReserveCost.add(reserveValue).gt(totalReserveCost)) {
217
+ reserveTokenChange.value = totalReserveCost.sub(currentReserveCost).toString();
218
+ reserveValue = new BN(reserveTokenChange.value);
173
219
  }
220
+ currentReserveCost = currentReserveCost.add(reserveValue);
221
+
222
+ accountUpdates[owner] = applyTokenUpdates(
223
+ [reserveTokenChange],
224
+ signerStates.find((s) => s.address === owner),
225
+ 'sub'
226
+ );
227
+
228
+ inputChanges.push({
229
+ address: owner,
230
+ token: reserveAddress,
231
+ delta: reserveValue.neg().toString(),
232
+ });
233
+ });
234
+
235
+ // update sender
236
+ if (senderChange) {
237
+ accountUpdates[senderChange.address] = applyTokenChange(accountUpdates[senderChange.address], senderChange);
238
+ }
239
+ // Ensure the sender exists in accountUpdates, because even if there is no balance change, we need to update his nonce
240
+ if (!accountUpdates[tx.from]) {
241
+ accountUpdates[tx.from] = senderState || {};
174
242
  }
175
243
 
176
- const senderUpdates = senderChange
177
- ? applyTokenChange({ tokens: senderTokens }, senderChange)
178
- : { tokens: senderTokens };
244
+ // update owner
245
+ if (reserveFee && new BN(reserveFee).gt(0)) {
246
+ accountUpdates[ownerState.address] = applyTokenChange(accountUpdates[ownerState.address] || ownerState, {
247
+ address: ownerState.address,
248
+ token: reserveAddress,
249
+ delta: reserveFee,
250
+ });
251
+ }
179
252
 
180
- const [newSenderState, newReceiverState, newOwnerState, newTokenFactoryState, newTokenState] = await Promise.all([
181
- // Update sender state
182
- statedb.account.update(
183
- senderState.address,
184
- account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
185
- context
186
- ),
253
+ // update receiver
254
+ accountUpdates[receiver] = applyTokenChange(
255
+ accountUpdates[receiver] || receiverState || { address: receiver, tokens: {} },
256
+ {
257
+ address: receiver,
258
+ token: tokenAddress,
259
+ delta: amount,
260
+ }
261
+ );
187
262
 
188
- // Update receiver state
189
- !isSameDid(senderState.address, receiver)
190
- ? statedb.account.updateOrCreate(
191
- receiverState,
192
- account.updateOrCreate(receiverState, { address: receiver, tokens: receiverTokens }, context),
193
- context
194
- )
195
- : null,
196
-
197
- // Update owner state
198
- !isSameDid(senderState.address, ownerState.address) && !isSameDid(receiver, ownerState.address)
199
- ? statedb.account.update(
200
- ownerState.address,
201
- account.update(ownerState, { tokens: ownerTokens }, context),
202
- context
203
- )
204
- : null,
263
+ const [newAccountStates, newTokenFactoryState, newTokenState] = await Promise.all([
264
+ // update accounts
265
+ Promise.all(
266
+ Object.entries(accountUpdates).map(([address, updates]) => {
267
+ // We can use updateOrCreate here because the owner and signer have already been validated to exist earlier,
268
+ // the sender and receiver are allowed to be created in the transaction.
269
+ const state = [senderState, receiverState, ownerState, ...signerStates].find((x) => x?.address === address);
270
+
271
+ // Should only update nonce and pk for sender and new accounts
272
+ const actualUpdates =
273
+ address === tx.from || !state
274
+ ? { ...updates, address, nonce: tx.nonce, pk: tx.pk }
275
+ : pick(updates, ['tokens']);
276
+
277
+ return statedb.account.updateOrCreate(state, account.updateOrCreate(state, actualUpdates, context), context);
278
+ })
279
+ ),
205
280
 
206
281
  // Update token factory state
207
282
  statedb.tokenFactory.update(
@@ -230,12 +305,17 @@ runner.use(
230
305
  ),
231
306
  ]);
232
307
 
233
- context.senderState = newSenderState;
234
- context.receiverState = newReceiverState;
235
- context.ownerState = newOwnerState;
308
+ context.senderState = newAccountStates.find((x) => x.address === tx.from);
309
+ context.receiverState = newAccountStates.find((x) => x.address === receiver);
310
+ // owner maybe not updated
311
+ context.ownerState = newAccountStates.find((x) => x.address === ownerState.address) || ownerState;
312
+ context.signerStates = newAccountStates.filter((x) => signerStates.find((s) => s.address === x.address));
236
313
  context.tokenFactoryState = newTokenFactoryState;
237
314
  context.tokenState = newTokenState;
238
315
 
316
+ // save this for receipt generation
317
+ context.inputChanges = inputChanges;
318
+
239
319
  await updateVaults();
240
320
 
241
321
  next();
@@ -7,15 +7,15 @@ module.exports =
7
7
  ({
8
8
  tokenFactoryKey = 'tokenFactoryState',
9
9
  tokenStateKey = 'tokenState',
10
- senderStateKey = 'senderState',
11
10
  reserveKey = 'reserveAmount',
12
11
  feeKey = 'reserveFee',
12
+ amountKey = 'itx.amount',
13
13
  direction = 'mint',
14
14
  } = {}) =>
15
15
  (context, next) => {
16
16
  const tokenFactoryState = get(context, tokenFactoryKey);
17
17
  const tokenState = get(context, tokenStateKey);
18
- const senderState = get(context, senderStateKey);
18
+ const amount = get(context, amountKey);
19
19
 
20
20
  if (!tokenFactoryState) {
21
21
  return next(new Error('INVALID_TOKEN_FACTORY', 'Token factory state not found'));
@@ -26,15 +26,12 @@ module.exports =
26
26
 
27
27
  const { curve, currentSupply, feeRate } = tokenFactoryState;
28
28
  const { decimal } = tokenState;
29
- const { amount } = context.itx;
30
29
 
31
30
  const reserveAmount = calcCost({ amount, decimal, currentSupply, direction, curve });
32
31
  set(context, reserveKey, reserveAmount.toString());
33
32
 
34
- if (senderState.address !== tokenFactoryState.owner) {
35
- const reserveFee = calcFee({ reserveAmount, feeRate });
36
- set(context, feeKey, reserveFee.toString());
37
- }
33
+ const reserveFee = calcFee({ reserveAmount, feeRate });
34
+ set(context, feeKey, reserveFee.toString());
38
35
 
39
- next();
36
+ return next();
40
37
  };
@@ -1,8 +1,8 @@
1
1
  const { Joi } = require('@arcblock/validator');
2
2
  const { CustomError: Error } = require('@ocap/util/lib/error');
3
3
  const { Runner, pipes } = require('@ocap/tx-pipeline');
4
- const { account, tokenFactory } = require('@ocap/state');
5
- const { applyTokenChange } = require('../../util');
4
+ const { account, tokenFactory, delegation } = require('@ocap/state');
5
+ const { applyTokenChange, getDelegationRequirements } = require('../../util');
6
6
 
7
7
  // eslint-disable-next-line global-require, import/order
8
8
  const debug = require('debug')(`${require('../../../package.json').name}:update-token-factory`);
@@ -40,25 +40,44 @@ runner.use(
40
40
  })
41
41
  );
42
42
 
43
+ // Ensure sender
44
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
45
+ runner.use(pipes.VerifyAccountMigration({ stateKey: 'senderState', addressKey: 'tx.from' }));
46
+ runner.use(pipes.VerifyBlocked({ stateKeys: ['senderState'] }));
47
+
48
+ // Ensure delegation
49
+ runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK', table: 'account' }));
50
+ runner.use(pipes.VerifyAccountMigration({ stateKey: 'delegatorState', addressKey: 'tx.delegator' }));
51
+ runner.use(
52
+ pipes.VerifyDelegation({
53
+ type: 'signature',
54
+ signerKey: 'senderState',
55
+ delegatorKey: 'delegatorState',
56
+ getRequirements: getDelegationRequirements,
57
+ })
58
+ );
59
+
43
60
  // verify owner
44
61
  runner.use((context, next) => {
45
- const { tx, tokenFactoryState } = context;
46
- if (tx.from !== tokenFactoryState.owner) {
62
+ const { tokenFactoryState, delegatorState, senderState } = context;
63
+ const owner = delegatorState ? delegatorState.address : senderState.address;
64
+ if (owner !== tokenFactoryState.owner) {
47
65
  return next(new Error('FORBIDDEN', 'Token factory can only be updated by owner'));
48
66
  }
49
67
 
50
68
  return next();
51
69
  });
52
70
 
53
- // Ensure sender
54
- runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
55
- runner.use(pipes.VerifyAccountMigration({ stateKey: 'senderState', addressKey: 'tx.from' }));
56
- runner.use(pipes.VerifyBlocked({ stateKeys: ['senderState'] }));
57
-
58
71
  // Ensure tx fee and gas
59
72
  runner.use(
60
- EnsureTxGas(() => {
61
- return { create: 0, update: 2, payment: 0 };
73
+ EnsureTxGas((context) => {
74
+ const result = { create: 0, update: 2, payment: 0 };
75
+
76
+ if (context.isDelegationChanged) {
77
+ result.update += 1;
78
+ }
79
+
80
+ return result;
62
81
  })
63
82
  );
64
83
  runner.use(EnsureTxCost({ attachSenderChanges: true }));
@@ -69,7 +88,7 @@ runner.use(pipes.TakeStateSnapshot());
69
88
  // Update sender state, token factory state
70
89
  runner.use(
71
90
  async (context, next) => {
72
- const { tx, itx, statedb, senderState, tokenFactoryState, senderChange, updateVaults } = context;
91
+ const { tx, itx, statedb, senderState, tokenFactoryState, senderChange, updateVaults, delegationState } = context;
73
92
 
74
93
  const { tokens: senderTokens = {} } = senderState;
75
94
 
@@ -77,7 +96,7 @@ runner.use(
77
96
  ? applyTokenChange({ tokens: senderTokens }, senderChange)
78
97
  : { tokens: senderTokens };
79
98
 
80
- const [newSenderState, newTokenFactoryState] = await Promise.all([
99
+ const [newSenderState, newTokenFactoryState, newDelegationState] = await Promise.all([
81
100
  statedb.account.update(
82
101
  senderState.address,
83
102
  account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
@@ -89,12 +108,18 @@ runner.use(
89
108
  tokenFactory.update(tokenFactoryState, { feeRate: itx.feeRate }, context),
90
109
  context
91
110
  ),
111
+
112
+ // Update delegation state
113
+ context.isDelegationChanged
114
+ ? statedb.delegation.update(delegationState.address, delegation.update(delegationState, {}, context), context)
115
+ : delegationState,
92
116
  ]);
93
117
 
94
118
  await updateVaults();
95
119
 
96
120
  context.senderState = newSenderState;
97
121
  context.tokenFactoryState = newTokenFactoryState;
122
+ context.delegationState = newDelegationState;
98
123
 
99
124
  debug('update token factory', newTokenFactoryState);
100
125
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.22.3",
6
+ "version": "1.23.1",
7
7
  "description": "Predefined tx pipeline sets to execute certain type of transactions",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -18,19 +18,19 @@
18
18
  "empty-value": "^1.0.1",
19
19
  "lodash": "^4.17.21",
20
20
  "url-join": "^4.0.1",
21
- "@arcblock/did": "1.22.3",
22
- "@arcblock/did-util": "1.22.3",
23
- "@arcblock/jwt": "1.22.3",
24
- "@arcblock/validator": "1.22.3",
25
- "@ocap/asset": "1.22.3",
26
- "@ocap/client": "1.22.3",
27
- "@ocap/merkle-tree": "1.22.3",
28
- "@ocap/mcrypto": "1.22.3",
29
- "@ocap/message": "1.22.3",
30
- "@ocap/state": "1.22.3",
31
- "@ocap/tx-pipeline": "1.22.3",
32
- "@ocap/util": "1.22.3",
33
- "@ocap/wallet": "1.22.3"
21
+ "@arcblock/did": "1.23.1",
22
+ "@arcblock/did-util": "1.23.1",
23
+ "@arcblock/jwt": "1.23.1",
24
+ "@arcblock/validator": "1.23.1",
25
+ "@ocap/asset": "1.23.1",
26
+ "@ocap/client": "1.23.1",
27
+ "@ocap/mcrypto": "1.23.1",
28
+ "@ocap/merkle-tree": "1.23.1",
29
+ "@ocap/message": "1.23.1",
30
+ "@ocap/state": "1.23.1",
31
+ "@ocap/tx-pipeline": "1.23.1",
32
+ "@ocap/util": "1.23.1",
33
+ "@ocap/wallet": "1.23.1"
34
34
  },
35
35
  "resolutions": {
36
36
  "bn.js": "5.2.1",
@@ -39,8 +39,8 @@
39
39
  "devDependencies": {
40
40
  "jest": "^29.7.0",
41
41
  "start-server-and-test": "^1.14.0",
42
- "@ocap/e2e-test": "1.22.3",
43
- "@ocap/statedb-memory": "1.22.3"
42
+ "@ocap/e2e-test": "1.23.1",
43
+ "@ocap/statedb-memory": "1.23.1"
44
44
  },
45
45
  "scripts": {
46
46
  "lint": "eslint tests lib",