@ocap/tx-protocols 1.22.2 → 1.23.0

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