@ocap/tx-protocols 1.21.3 → 1.22.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.
package/lib/index.js CHANGED
@@ -31,6 +31,10 @@ const resumeRollup = require('./protocols/rollup/resume');
31
31
  const createRollupBlock = require('./protocols/rollup/create-block');
32
32
  const claimBlockReward = require('./protocols/rollup/claim-reward');
33
33
  const migrateRollup = require('./protocols/rollup/migrate');
34
+ const createTokenFactory = require('./protocols/token-factory/create');
35
+ const updateTokenFactory = require('./protocols/token-factory/update');
36
+ const mintToken = require('./protocols/token-factory/mint');
37
+ const burnToken = require('./protocols/token-factory/burn');
34
38
 
35
39
  const executor = require('./execute');
36
40
 
@@ -62,6 +66,10 @@ const createExecutor = ({ filter, runAsLambda }) => {
62
66
  createToken,
63
67
  depositTokenV2,
64
68
  withdrawTokenV2,
69
+ createTokenFactory,
70
+ updateTokenFactory,
71
+ mintToken,
72
+ burnToken,
65
73
 
66
74
  // governance
67
75
  stake,
@@ -31,6 +31,7 @@ const schema = Joi.object({
31
31
  totalSupply: Joi.BN().greater(0).max(MAX_TOTAL_SUPPLY).required(),
32
32
  initialSupply: Joi.BN().greater(0).max(Joi.ref('totalSupply')).required(),
33
33
  foreignToken: schemas.foreignToken.optional().allow(null).default(null),
34
+ tokenFactoryAddress: Joi.forbidden(),
34
35
  data: Joi.any().optional().allow(null),
35
36
  }).options({ stripUnknown: true, noDefaults: false });
36
37
 
@@ -0,0 +1,255 @@
1
+ const { CustomError: Error } = require('@ocap/util/lib/error');
2
+ const { Joi, schemas } = require('@arcblock/validator');
3
+ const { BN, isSameDid, fromUnitToToken } = require('@ocap/util');
4
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
5
+ const { account, tokenFactory, token } = require('@ocap/state');
6
+
7
+ const EnsureTxGas = require('../../pipes/ensure-gas');
8
+ const EnsureTxCost = require('../../pipes/ensure-cost');
9
+ const CalcReserveAmount = require('./pipes/calc-reserve');
10
+ const { applyTokenChange } = require('../../util');
11
+
12
+ const runner = new Runner();
13
+
14
+ runner.use(pipes.VerifyMultiSig(0));
15
+
16
+ const schema = Joi.object({
17
+ tokenFactory: Joi.DID().prefix().role('ROLE_TOKEN_FACTORY').required(),
18
+ receiver: schemas.tokenHolder.required(),
19
+ amount: Joi.BN().greater(0).required(),
20
+ minReserve: Joi.alternatives().try(Joi.BN().greater(0), Joi.equal('')).optional(),
21
+ data: Joi.any().optional().allow(null),
22
+ }).options({ stripUnknown: true, noDefaults: false });
23
+
24
+ // verify itx
25
+ runner.use(({ itx }, next) => {
26
+ const { error } = schema.validate(itx);
27
+ if (error) {
28
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
29
+ }
30
+ return next();
31
+ });
32
+
33
+ // verify token factory
34
+ runner.use(
35
+ pipes.ExtractState({
36
+ from: 'itx.tokenFactory',
37
+ to: 'tokenFactoryState',
38
+ status: 'INVALID_TOKEN_FACTORY',
39
+ table: 'tokenFactory',
40
+ })
41
+ );
42
+
43
+ // 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' }));
48
+
49
+ // ensure receiver
50
+ runner.use(pipes.ExtractState({ from: 'itx.receiver', to: 'receiverState', status: 'OK', table: 'account' }));
51
+
52
+ // ensure owner
53
+ runner.use(
54
+ pipes.ExtractState({
55
+ from: 'tokenFactoryState.owner',
56
+ to: 'ownerState',
57
+ status: 'INVALID_OWNER_STATE',
58
+ table: 'account',
59
+ })
60
+ );
61
+
62
+ // verify blocked
63
+ runner.use(pipes.VerifyBlocked({ stateKeys: ['senderState', 'receiverState'] }));
64
+
65
+ // ensure token state
66
+ runner.use(
67
+ pipes.ExtractState({
68
+ from: 'tokenFactoryState.tokenAddress',
69
+ to: 'tokenState',
70
+ status: 'INVALID_TOKEN',
71
+ table: 'token',
72
+ })
73
+ );
74
+
75
+ // calculate reserve amount
76
+ runner.use(
77
+ CalcReserveAmount({
78
+ tokenFactoryKey: 'tokenFactoryState',
79
+ tokenStateKey: 'tokenState',
80
+ reserveKey: 'reserveAmount',
81
+ feeKey: 'reserveFee',
82
+ direction: 'burn',
83
+ })
84
+ );
85
+
86
+ // verify slippage
87
+ runner.use((context, next) => {
88
+ const { reserveAmount, reserveFee } = context;
89
+ const { minReserve } = context.itx;
90
+
91
+ if (minReserve && new BN(reserveAmount).lt(new BN(minReserve).add(new BN(reserveFee)))) {
92
+ return next(
93
+ new Error(
94
+ 'SLIPPAGE_EXCEEDED',
95
+ `Burn token failed due to price movement. Expected minimum: ${fromUnitToToken(minReserve)}, actual: ${fromUnitToToken(reserveAmount)}. Try reducing your minReserve.`
96
+ )
97
+ );
98
+ }
99
+
100
+ next();
101
+ });
102
+
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
+ };
111
+
112
+ if (reserveFee) {
113
+ context.tokenConditions.tokens.push({ address: tokenFactoryState.reserveAddress, value: reserveFee });
114
+ }
115
+
116
+ next();
117
+ });
118
+ runner.use(pipes.VerifyTokenBalance({ ownerKey: 'senderState', conditionKey: 'tokenConditions' }));
119
+
120
+ // Ensure tx fee and gas
121
+ runner.use(
122
+ EnsureTxGas((context) => {
123
+ const result = { create: 0, update: 3, payment: 0 };
124
+
125
+ if (context.receiverState) {
126
+ result.update += 1;
127
+ }
128
+ if (context.reserveFee) {
129
+ result.update += 1;
130
+ }
131
+
132
+ return result;
133
+ })
134
+ );
135
+ runner.use(EnsureTxCost({ attachSenderChanges: true }));
136
+
137
+ // Save context snapshot before updating states
138
+ runner.use(pipes.TakeStateSnapshot());
139
+
140
+ // update statedb: transfer tokens to new owner
141
+ runner.use(
142
+ async (context, next) => {
143
+ const {
144
+ tx,
145
+ senderState,
146
+ receiverState,
147
+ ownerState,
148
+ statedb,
149
+ senderChange,
150
+ updateVaults,
151
+ tokenFactoryState,
152
+ tokenState,
153
+ reserveAmount,
154
+ reserveFee,
155
+ itx,
156
+ } = context;
157
+ const { amount, receiver } = itx;
158
+ const { reserveAddress, tokenAddress } = tokenFactoryState;
159
+
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];
171
+ }
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
+ }
181
+ }
182
+
183
+ const senderUpdates = senderChange
184
+ ? applyTokenChange({ tokens: senderTokens }, senderChange)
185
+ : { tokens: senderTokens };
186
+
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
+ ),
194
+
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,
212
+
213
+ // Update token factory state
214
+ statedb.tokenFactory.update(
215
+ tokenFactoryState.address,
216
+ tokenFactory.update(
217
+ tokenFactoryState,
218
+ {
219
+ currentSupply: new BN(tokenFactoryState.currentSupply).sub(new BN(amount)).toString(),
220
+ reserveBalance: new BN(tokenFactoryState.reserveBalance).sub(new BN(reserveAmount)).toString(),
221
+ },
222
+ context
223
+ ),
224
+ context
225
+ ),
226
+
227
+ statedb.token.update(
228
+ tokenAddress,
229
+ token.update(
230
+ tokenState,
231
+ {
232
+ totalSupply: new BN(tokenState.totalSupply).sub(new BN(amount)).toString(),
233
+ },
234
+ context
235
+ ),
236
+ context
237
+ ),
238
+ ]);
239
+
240
+ context.senderState = newSenderState;
241
+ context.receiverState = newReceiverState;
242
+ context.ownerState = newOwnerState;
243
+ context.tokenFactoryState = newTokenFactoryState;
244
+ context.tokenState = newTokenState;
245
+
246
+ await updateVaults();
247
+
248
+ next();
249
+ },
250
+ { persistError: true }
251
+ );
252
+
253
+ runner.use(pipes.VerifyStateDiff());
254
+
255
+ module.exports = runner;
@@ -0,0 +1,205 @@
1
+ const isEmpty = require('empty-value');
2
+ const cloneDeep = require('lodash/cloneDeep');
3
+ const { Joi } = require('@arcblock/validator');
4
+ const { CustomError: Error } = require('@ocap/util/lib/error');
5
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
6
+ const { account, token, tokenFactory } = require('@ocap/state');
7
+ const { toTokenFactoryAddress, toTokenAddress } = require('@arcblock/did-util');
8
+ const { applyTokenChange } = require('../../util');
9
+
10
+ // eslint-disable-next-line global-require, import/order
11
+ const debug = require('debug')(`${require('../../../package.json').name}:create-token-factory`);
12
+
13
+ const { decodeAnySafe } = require('../../util');
14
+
15
+ const EnsureTxGas = require('../../pipes/ensure-gas');
16
+ const EnsureTxCost = require('../../pipes/ensure-cost');
17
+
18
+ const runner = new Runner();
19
+
20
+ runner.use(pipes.VerifyMultiSig(0));
21
+
22
+ const schema = Joi.object({
23
+ address: Joi.DID().prefix().role('ROLE_TOKEN_FACTORY').required(),
24
+ feeRate: Joi.number().min(0).max(2000).required(),
25
+ curve: tokenFactory.curveSchema.required(),
26
+ reserveAddress: Joi.DID().prefix().role('ROLE_TOKEN').required(),
27
+ token: Joi.object({
28
+ name: Joi.string().min(1).max(32).required(),
29
+ description: Joi.string().min(1).max(128).required(),
30
+ symbol: Joi.string().min(2).max(6).uppercase().required(),
31
+ unit: Joi.string().min(1).max(6).lowercase().required(),
32
+ decimal: Joi.number().min(6).max(18).required(),
33
+ icon: Joi.string().optional().allow(null).valid(''),
34
+ }).required(),
35
+ data: Joi.any().optional().allow(null),
36
+ }).options({ stripUnknown: true, noDefaults: false });
37
+
38
+ // 1. verify itx
39
+ runner.use((context, next) => {
40
+ const { error, value } = schema.validate(context.itx);
41
+ if (error) {
42
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
43
+ }
44
+
45
+ // filter by curve type
46
+ context.itx.curve = value.curve;
47
+
48
+ return next();
49
+ });
50
+
51
+ // Ensure factory and token address is valid
52
+ runner.use(
53
+ pipes.VerifyInfo([
54
+ {
55
+ error: 'INVALID_TOKEN',
56
+ message: 'Token address is not valid',
57
+ fn: (context) => {
58
+ return toTokenAddress({ ...context.itx.token, address: '' }) === context.itx.token.address;
59
+ },
60
+ },
61
+ {
62
+ error: 'INVALID_TOKEN_FACTORY',
63
+ message: 'Token factory address is not valid',
64
+ fn: (context) => {
65
+ const itx = cloneDeep(context.itx);
66
+ itx.data = decodeAnySafe(itx.data);
67
+ itx.address = '';
68
+
69
+ return toTokenFactoryAddress(itx) === context.itx.address;
70
+ },
71
+ },
72
+ ])
73
+ );
74
+
75
+ // Ensure reserve address is config.token.address
76
+ runner.use(
77
+ pipes.VerifyInfo([
78
+ {
79
+ error: 'INVALID_RESERVE_ADDRESS',
80
+ message: 'Reserve address is not valid',
81
+ fn: (context) => context.itx.reserveAddress === context.config.token.address,
82
+ },
83
+ ])
84
+ );
85
+
86
+ // Ensure token factory not exist
87
+ runner.use(pipes.ExtractState({ from: 'itx.address', to: 'tokenFactoryState', table: 'tokenFactory', status: 'OK' }));
88
+ runner.use(
89
+ pipes.VerifyInfo([
90
+ {
91
+ error: 'DUPLICATE_TOKEN_FACTORY',
92
+ message: 'Token factory address already exists on chain',
93
+ fn: (context) => isEmpty(context.tokenFactoryState),
94
+ },
95
+ ])
96
+ );
97
+
98
+ // Ensure token not exist
99
+ runner.use(pipes.ExtractState({ from: 'itx.token.address', to: 'tokenState', table: 'token', status: 'OK' }));
100
+ runner.use(
101
+ pipes.VerifyInfo([
102
+ {
103
+ error: 'DUPLICATE_TOKEN',
104
+ message: 'Token address already exists on chain',
105
+ fn: (context) => isEmpty(context.tokenState),
106
+ },
107
+ ])
108
+ );
109
+
110
+ // Ensure sender exist
111
+ runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
112
+ runner.use(pipes.VerifyAccountMigration({ stateKey: 'senderState', addressKey: 'tx.from' }));
113
+
114
+ runner.use(pipes.VerifyBlocked({ stateKeys: ['senderState'] }));
115
+
116
+ // Ensure uniqueness of token symbol
117
+ runner.use(
118
+ async function EnsureTokenSymbol(context, next) {
119
+ const { symbol } = context.config.token;
120
+ 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
+
125
+ const exist = await context.statedb.token.existBySymbol(itxToken.symbol, context);
126
+ if (exist) {
127
+ return next(new Error('DUPLICATE_SYMBOL', 'Token symbol already exists'));
128
+ }
129
+
130
+ return next();
131
+ },
132
+ { persistError: true }
133
+ );
134
+
135
+ // Ensure tx fee and gas
136
+ runner.use(
137
+ EnsureTxGas(() => {
138
+ return { create: 1, update: 2, payment: 0 };
139
+ })
140
+ );
141
+ runner.use(EnsureTxCost({ attachSenderChanges: true }));
142
+
143
+ // Save context snapshot before updating states
144
+ runner.use(pipes.TakeStateSnapshot());
145
+
146
+ // Update sender state, token factory state
147
+ runner.use(
148
+ async (context, next) => {
149
+ const { tx, itx, statedb, senderState, senderChange, updateVaults } = context;
150
+ const data = decodeAnySafe(itx.data);
151
+ const owner = senderState.address;
152
+
153
+ const itxToken = itx.token;
154
+
155
+ const { tokens: senderTokens = {} } = senderState;
156
+
157
+ const senderUpdates = senderChange
158
+ ? applyTokenChange({ tokens: senderTokens }, senderChange)
159
+ : { tokens: senderTokens };
160
+
161
+ const [newSenderState, newTokenState, newTokenFactoryState] = await Promise.all([
162
+ statedb.account.update(
163
+ senderState.address,
164
+ account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
165
+ context
166
+ ),
167
+
168
+ statedb.token.create(
169
+ itxToken.address,
170
+ token.create(
171
+ {
172
+ ...cloneDeep(itxToken),
173
+ totalSupply: '0',
174
+ initialSupply: '0',
175
+ issuer: owner,
176
+ tokenFactoryAddress: itx.address,
177
+ },
178
+ context
179
+ ),
180
+ context
181
+ ),
182
+
183
+ statedb.tokenFactory.create(
184
+ itx.address,
185
+ tokenFactory.create({ ...cloneDeep(itx), owner, tokenAddress: itxToken.address, data }, context),
186
+ context
187
+ ),
188
+ ]);
189
+
190
+ await updateVaults();
191
+
192
+ context.senderState = newSenderState;
193
+ context.tokenState = newTokenState;
194
+ context.tokenFactoryState = newTokenFactoryState;
195
+
196
+ debug('create token factory', newTokenFactoryState);
197
+
198
+ next();
199
+ },
200
+ { persistError: true }
201
+ );
202
+
203
+ runner.use(pipes.VerifyStateDiff());
204
+
205
+ module.exports = runner;
@@ -0,0 +1,248 @@
1
+ const { CustomError: Error } = require('@ocap/util/lib/error');
2
+ const { Joi, schemas } = require('@arcblock/validator');
3
+ const { BN, isSameDid, fromUnitToToken } = require('@ocap/util');
4
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
5
+ const { account, tokenFactory, token } = require('@ocap/state');
6
+
7
+ const EnsureTxGas = require('../../pipes/ensure-gas');
8
+ const EnsureTxCost = require('../../pipes/ensure-cost');
9
+ const CalcReserve = require('./pipes/calc-reserve');
10
+ const { applyTokenChange } = require('../../util');
11
+
12
+ const runner = new Runner();
13
+
14
+ runner.use(pipes.VerifyMultiSig(0));
15
+
16
+ const schema = Joi.object({
17
+ tokenFactory: Joi.DID().prefix().role('ROLE_TOKEN_FACTORY').required(),
18
+ receiver: schemas.tokenHolder.required(),
19
+ amount: Joi.BN().greater(0).required(),
20
+ maxReserve: Joi.alternatives().try(Joi.BN().greater(0), Joi.equal('')).optional(),
21
+ data: Joi.any().optional().allow(null),
22
+ }).options({ stripUnknown: true, noDefaults: false });
23
+
24
+ // verify itx
25
+ runner.use(({ itx }, next) => {
26
+ const { error } = schema.validate(itx);
27
+ if (error) {
28
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
29
+ }
30
+ return next();
31
+ });
32
+
33
+ // verify token factory
34
+ runner.use(
35
+ pipes.ExtractState({
36
+ from: 'itx.tokenFactory',
37
+ to: 'tokenFactoryState',
38
+ status: 'INVALID_TOKEN_FACTORY',
39
+ table: 'tokenFactory',
40
+ })
41
+ );
42
+
43
+ // 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' }));
48
+
49
+ // ensure receiver
50
+ runner.use(pipes.ExtractState({ from: 'itx.receiver', to: 'receiverState', status: 'OK', table: 'account' }));
51
+
52
+ // ensure owner
53
+ runner.use(
54
+ pipes.ExtractState({
55
+ from: 'tokenFactoryState.owner',
56
+ to: 'ownerState',
57
+ status: 'INVALID_OWNER_STATE',
58
+ table: 'account',
59
+ })
60
+ );
61
+
62
+ // verify blocked
63
+ runner.use(pipes.VerifyBlocked({ stateKeys: ['senderState', 'receiverState'] }));
64
+
65
+ // ensure token state
66
+ runner.use(
67
+ pipes.ExtractState({
68
+ from: 'tokenFactoryState.tokenAddress',
69
+ to: 'tokenState',
70
+ status: 'INVALID_TOKEN',
71
+ table: 'token',
72
+ })
73
+ );
74
+
75
+ // calculate reserve amount
76
+ runner.use(
77
+ CalcReserve({ tokenFactoryKey: 'tokenFactoryState', tokenStateKey: 'tokenState', reserveKey: 'reserveAmount' })
78
+ );
79
+
80
+ // verify slippage
81
+ runner.use((context, next) => {
82
+ const { reserveAmount, reserveFee } = context;
83
+ const { maxReserve } = context.itx;
84
+
85
+ const actual = new BN(reserveAmount).add(new BN(reserveFee || '0'));
86
+ if (maxReserve && actual.gt(new BN(maxReserve))) {
87
+ return next(
88
+ new Error(
89
+ 'SLIPPAGE_EXCEEDED',
90
+ `Mint token failed due to price movement. Expected maximum: ${fromUnitToToken(maxReserve)}, actual: ${fromUnitToToken(actual)}. Try increasing your maxReserve.`
91
+ )
92
+ );
93
+ }
94
+
95
+ next();
96
+ });
97
+
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' }));
113
+
114
+ // Ensure tx fee and gas
115
+ runner.use(
116
+ EnsureTxGas((context) => {
117
+ const result = { create: 0, update: 3, payment: 0 };
118
+
119
+ if (context.receiverState) {
120
+ result.update += 1;
121
+ }
122
+ if (context.reserveFee) {
123
+ result.update += 1;
124
+ }
125
+
126
+ return result;
127
+ })
128
+ );
129
+ runner.use(EnsureTxCost({ attachSenderChanges: true }));
130
+
131
+ // Save context snapshot before updating states
132
+ runner.use(pipes.TakeStateSnapshot());
133
+
134
+ // update statedb: transfer tokens to new owner
135
+ runner.use(
136
+ async (context, next) => {
137
+ const {
138
+ tx,
139
+ senderState,
140
+ receiverState,
141
+ ownerState,
142
+ statedb,
143
+ senderChange,
144
+ updateVaults,
145
+ tokenFactoryState,
146
+ tokenState,
147
+ reserveAmount,
148
+ reserveFee,
149
+ itx,
150
+ } = context;
151
+ const { amount, receiver } = itx;
152
+ const { reserveAddress, tokenAddress } = tokenFactoryState;
153
+
154
+ const { tokens: senderTokens = {} } = senderState;
155
+ const { tokens: receiverTokens = {} } = receiverState || {};
156
+ const { tokens: ownerTokens = {} } = ownerState;
157
+
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();
160
+
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();
173
+ }
174
+ }
175
+
176
+ const senderUpdates = senderChange
177
+ ? applyTokenChange({ tokens: senderTokens }, senderChange)
178
+ : { tokens: senderTokens };
179
+
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
+ ),
187
+
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,
205
+
206
+ // Update token factory state
207
+ statedb.tokenFactory.update(
208
+ tokenFactoryState.address,
209
+ tokenFactory.update(
210
+ tokenFactoryState,
211
+ {
212
+ currentSupply: new BN(tokenFactoryState.currentSupply).add(new BN(amount)).toString(),
213
+ reserveBalance: new BN(tokenFactoryState.reserveBalance).add(new BN(reserveAmount)).toString(),
214
+ },
215
+ context
216
+ ),
217
+ context
218
+ ),
219
+
220
+ statedb.token.update(
221
+ tokenAddress,
222
+ token.update(
223
+ tokenState,
224
+ {
225
+ totalSupply: new BN(tokenState.totalSupply).add(new BN(amount)).toString(),
226
+ },
227
+ context
228
+ ),
229
+ context
230
+ ),
231
+ ]);
232
+
233
+ context.senderState = newSenderState;
234
+ context.receiverState = newReceiverState;
235
+ context.ownerState = newOwnerState;
236
+ context.tokenFactoryState = newTokenFactoryState;
237
+ context.tokenState = newTokenState;
238
+
239
+ await updateVaults();
240
+
241
+ next();
242
+ },
243
+ { persistError: true }
244
+ );
245
+
246
+ runner.use(pipes.VerifyStateDiff());
247
+
248
+ module.exports = runner;
@@ -0,0 +1,40 @@
1
+ const { CustomError: Error } = require('@ocap/util/lib/error');
2
+ const get = require('lodash/get');
3
+ const set = require('lodash/set');
4
+ const { calcCost, calcFee } = require('@ocap/util/lib/curve');
5
+
6
+ module.exports =
7
+ ({
8
+ tokenFactoryKey = 'tokenFactoryState',
9
+ tokenStateKey = 'tokenState',
10
+ senderStateKey = 'senderState',
11
+ reserveKey = 'reserveAmount',
12
+ feeKey = 'reserveFee',
13
+ direction = 'mint',
14
+ } = {}) =>
15
+ (context, next) => {
16
+ const tokenFactoryState = get(context, tokenFactoryKey);
17
+ const tokenState = get(context, tokenStateKey);
18
+ const senderState = get(context, senderStateKey);
19
+
20
+ if (!tokenFactoryState) {
21
+ return next(new Error('INVALID_TOKEN_FACTORY', 'Token factory state not found'));
22
+ }
23
+ if (!tokenState) {
24
+ return next(new Error('INVALID_TOKEN', 'Token state not found'));
25
+ }
26
+
27
+ const { curve, currentSupply, feeRate } = tokenFactoryState;
28
+ const { decimal } = tokenState;
29
+ const { amount } = context.itx;
30
+
31
+ const reserveAmount = calcCost({ amount, decimal, currentSupply, direction, curve });
32
+ set(context, reserveKey, reserveAmount.toString());
33
+
34
+ if (senderState.address !== tokenFactoryState.owner) {
35
+ const reserveFee = calcFee({ reserveAmount, feeRate });
36
+ set(context, feeKey, reserveFee.toString());
37
+ }
38
+
39
+ next();
40
+ };
@@ -0,0 +1,108 @@
1
+ const { Joi } = require('@arcblock/validator');
2
+ const { CustomError: Error } = require('@ocap/util/lib/error');
3
+ const { Runner, pipes } = require('@ocap/tx-pipeline');
4
+ const { account, tokenFactory } = require('@ocap/state');
5
+ const { applyTokenChange } = require('../../util');
6
+
7
+ // eslint-disable-next-line global-require, import/order
8
+ const debug = require('debug')(`${require('../../../package.json').name}:update-token-factory`);
9
+
10
+ const EnsureTxGas = require('../../pipes/ensure-gas');
11
+ const EnsureTxCost = require('../../pipes/ensure-cost');
12
+
13
+ const runner = new Runner();
14
+
15
+ runner.use(pipes.VerifyMultiSig(0));
16
+
17
+ const schema = Joi.object({
18
+ address: Joi.DID().prefix().role('ROLE_TOKEN_FACTORY').required(),
19
+ feeRate: Joi.number().min(0).max(2000).required(),
20
+ data: Joi.any().optional().allow(null),
21
+ }).options({ stripUnknown: true, noDefaults: false });
22
+
23
+ // 1. verify itx
24
+ runner.use((context, next) => {
25
+ const { error } = schema.validate(context.itx);
26
+ if (error) {
27
+ return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
28
+ }
29
+
30
+ return next();
31
+ });
32
+
33
+ // ensure token factory exists
34
+ runner.use(
35
+ pipes.ExtractState({
36
+ from: 'itx.address',
37
+ to: 'tokenFactoryState',
38
+ table: 'tokenFactory',
39
+ status: 'INVALID_TOKEN_FACTORY',
40
+ })
41
+ );
42
+
43
+ // verify owner
44
+ runner.use((context, next) => {
45
+ const { tx, tokenFactoryState } = context;
46
+ if (tx.from !== tokenFactoryState.owner) {
47
+ return next(new Error('FORBIDDEN', 'Token factory can only be updated by owner'));
48
+ }
49
+
50
+ return next();
51
+ });
52
+
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
+ // Ensure tx fee and gas
59
+ runner.use(
60
+ EnsureTxGas(() => {
61
+ return { create: 0, update: 2, payment: 0 };
62
+ })
63
+ );
64
+ runner.use(EnsureTxCost({ attachSenderChanges: true }));
65
+
66
+ // Save context snapshot before updating states
67
+ runner.use(pipes.TakeStateSnapshot());
68
+
69
+ // Update sender state, token factory state
70
+ runner.use(
71
+ async (context, next) => {
72
+ const { tx, itx, statedb, senderState, tokenFactoryState, senderChange, updateVaults } = context;
73
+
74
+ const { tokens: senderTokens = {} } = senderState;
75
+
76
+ const senderUpdates = senderChange
77
+ ? applyTokenChange({ tokens: senderTokens }, senderChange)
78
+ : { tokens: senderTokens };
79
+
80
+ const [newSenderState, newTokenFactoryState] = await Promise.all([
81
+ statedb.account.update(
82
+ senderState.address,
83
+ account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
84
+ context
85
+ ),
86
+
87
+ statedb.tokenFactory.update(
88
+ tokenFactoryState.address,
89
+ tokenFactory.update(tokenFactoryState, { feeRate: itx.feeRate }, context),
90
+ context
91
+ ),
92
+ ]);
93
+
94
+ await updateVaults();
95
+
96
+ context.senderState = newSenderState;
97
+ context.tokenFactoryState = newTokenFactoryState;
98
+
99
+ debug('update token factory', newTokenFactoryState);
100
+
101
+ next();
102
+ },
103
+ { persistError: true }
104
+ );
105
+
106
+ runner.use(pipes.VerifyStateDiff());
107
+
108
+ module.exports = runner;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.21.3",
6
+ "version": "1.22.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.21.3",
22
- "@arcblock/did-util": "1.21.3",
23
- "@arcblock/jwt": "1.21.3",
24
- "@arcblock/validator": "1.21.3",
25
- "@ocap/asset": "1.21.3",
26
- "@ocap/client": "1.21.3",
27
- "@ocap/mcrypto": "1.21.3",
28
- "@ocap/merkle-tree": "1.21.3",
29
- "@ocap/message": "1.21.3",
30
- "@ocap/state": "1.21.3",
31
- "@ocap/tx-pipeline": "1.21.3",
32
- "@ocap/util": "1.21.3",
33
- "@ocap/wallet": "1.21.3"
21
+ "@arcblock/did": "1.22.1",
22
+ "@arcblock/did-util": "1.22.1",
23
+ "@arcblock/jwt": "1.22.1",
24
+ "@arcblock/validator": "1.22.1",
25
+ "@ocap/client": "1.22.1",
26
+ "@ocap/asset": "1.22.1",
27
+ "@ocap/mcrypto": "1.22.1",
28
+ "@ocap/merkle-tree": "1.22.1",
29
+ "@ocap/message": "1.22.1",
30
+ "@ocap/state": "1.22.1",
31
+ "@ocap/tx-pipeline": "1.22.1",
32
+ "@ocap/util": "1.22.1",
33
+ "@ocap/wallet": "1.22.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.21.3",
43
- "@ocap/statedb-memory": "1.21.3"
42
+ "@ocap/e2e-test": "1.22.1",
43
+ "@ocap/statedb-memory": "1.22.1"
44
44
  },
45
45
  "scripts": {
46
46
  "lint": "eslint tests lib",