@ocap/tx-protocols 1.24.9 → 1.25.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.
@@ -23,7 +23,7 @@ runner.use(pipes.VerifyMultiSig(0));
23
23
  const schema = Joi.object({
24
24
  address: Joi.DID().prefix().role('ROLE_TOKEN').required(),
25
25
  name: Joi.string().min(1).max(32).required(),
26
- description: Joi.string().min(1).max(128).required(),
26
+ description: Joi.string().min(16).max(128).required(),
27
27
  symbol: Joi.string().min(2).max(6).uppercase().required(),
28
28
  unit: Joi.string().min(1).max(6).lowercase().required(),
29
29
  decimal: Joi.number().min(6).max(18).required(),
@@ -3,20 +3,24 @@ 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, delegation } = require('@ocap/state');
7
- const { toTokenFactoryAddress, toTokenAddress } = require('@arcblock/did-util');
8
- const { applyTokenChange } = require('../../util');
6
+ const { account, token, tokenFactory, delegation, stake } = require('@ocap/state');
7
+ const { toTokenFactoryAddress, toTokenAddress, toStakeAddress } = require('@arcblock/did-util');
8
+ const { BN, fromTokenToUnit } = require('@ocap/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
-
13
- const { decodeAnySafe, getDelegationRequirements } = require('../../util');
12
+ const { applyTokenChange, decodeAnySafe, getDelegationRequirements } = require('../../util');
14
13
 
15
14
  const EnsureTxGas = require('../../pipes/ensure-gas');
16
15
  const EnsureTxCost = require('../../pipes/ensure-cost');
16
+ const verifyIcon = require('./pipes/verify-icon');
17
+ const verifyUrl = require('./pipes/verify-url');
18
+ const verifyOwnership = require('./pipes/verify-ownership');
17
19
 
18
20
  const runner = new Runner();
19
21
 
22
+ const isTest = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' || process.env.CI;
23
+
20
24
  runner.use(pipes.VerifyMultiSig(0));
21
25
 
22
26
  const schema = Joi.object({
@@ -26,12 +30,21 @@ const schema = Joi.object({
26
30
  reserveAddress: Joi.DID().prefix().role('ROLE_TOKEN').required(),
27
31
  token: Joi.object({
28
32
  name: Joi.string().min(1).max(32).required(),
29
- description: Joi.string().min(1).max(128).required(),
33
+ description: Joi.string().min(16).max(128).required(),
30
34
  symbol: Joi.string().min(2).max(6).uppercase().required(),
31
35
  unit: Joi.string().min(1).max(6).lowercase().required(),
32
36
  decimal: Joi.number().min(6).max(18).required(),
33
- icon: Joi.string().optional().allow(null).valid(''),
37
+ icon: Joi.string().optional().allow(null, ''),
34
38
  maxTotalSupply: Joi.alternatives().try(Joi.BN().greater(0), Joi.string().valid('')).optional().allow(null),
39
+ website: Joi.string()
40
+ .uri({
41
+ scheme: isTest ? ['http', 'https'] : ['https'],
42
+ allowRelative: false,
43
+ })
44
+ .max(256)
45
+ .optional()
46
+ .allow(null, ''),
47
+ metadata: Joi.any().required(),
35
48
  }).required(),
36
49
  data: Joi.any().optional().allow(null),
37
50
  }).options({ stripUnknown: true, noDefaults: false });
@@ -46,6 +59,16 @@ runner.use((context, next) => {
46
59
  // filter by curve type
47
60
  context.itx.curve = value.curve;
48
61
 
62
+ // decode and verify metadata
63
+ if (context.itx.token.metadata) {
64
+ const metadata = decodeAnySafe(context.itx.token.metadata);
65
+ const { error: metadataError, value: metadataValue } = token.metadataSchema.validate(metadata.value);
66
+ if (metadataError) {
67
+ return next(new Error('INVALID_TX', `Invalid metadata: ${metadataError.message}`));
68
+ }
69
+ context.itx.token.metadata = { ...metadata, value: metadataValue };
70
+ }
71
+
49
72
  return next();
50
73
  });
51
74
 
@@ -80,6 +103,10 @@ runner.use(
80
103
  ])
81
104
  );
82
105
 
106
+ runner.use(verifyIcon());
107
+ runner.use(verifyUrl({ urlKeys: ['itx.token.website', 'itx.token.metadata.value.communityUrl'] }));
108
+ runner.use(verifyOwnership({ tokenKey: 'itx.token' }));
109
+
83
110
  // Ensure symbol is not internal
84
111
  runner.use((context, next) => {
85
112
  const { reservedSymbols } = context.config;
@@ -122,6 +149,36 @@ runner.use(pipes.VerifyAccountMigration({ stateKey: 'senderState', addressKey: '
122
149
 
123
150
  runner.use(pipes.VerifyBlocked({ stateKeys: ['senderState'] }));
124
151
 
152
+ // verify staking: get address, extract state, verify amount
153
+ runner.use((context, next) => {
154
+ const { tx, itx } = context;
155
+ context.stakeAddress = toStakeAddress(tx.from, tx.from, itx.token.symbol);
156
+ return next();
157
+ });
158
+ runner.use(
159
+ pipes.ExtractState({ from: 'stakeAddress', to: 'stakeState', status: 'INVALID_STAKE_STATE', table: 'stake' })
160
+ );
161
+ runner.use((context, next) => {
162
+ const { stakeState, config } = context;
163
+
164
+ if (stakeState.revocable === false) {
165
+ return next(new Error('INVALID_STAKE_STATE', `Staking for token creating already locked: ${stakeState.address}`));
166
+ }
167
+
168
+ const actualStake = new BN(stakeState.tokens[config.token.address] || 0);
169
+ const requiredStake = fromTokenToUnit(config.transaction.txStake.createToken, config.token.decimal);
170
+ if (actualStake.lt(requiredStake)) {
171
+ return next(
172
+ new Error(
173
+ 'INVALID_STAKE_STATE',
174
+ `Insufficient stake amount: required ${config.transaction.txStake.createToken} ${config.token.symbol}`
175
+ )
176
+ );
177
+ }
178
+
179
+ return next();
180
+ });
181
+
125
182
  // Ensure delegation
126
183
  runner.use(pipes.ExtractState({ from: 'tx.delegator', to: 'delegatorState', status: 'OK', table: 'account' }));
127
184
  runner.use(pipes.VerifyAccountMigration({ stateKey: 'delegatorState', addressKey: 'tx.delegator' }));
@@ -152,7 +209,7 @@ runner.use(
152
209
  // Ensure tx fee and gas
153
210
  runner.use(
154
211
  EnsureTxGas((context) => {
155
- const result = { create: 2, update: 1, payment: 0 };
212
+ const result = { create: 2, update: 2, payment: 0 };
156
213
 
157
214
  if (context.isDelegationChanged) {
158
215
  result.update += 1;
@@ -166,10 +223,22 @@ runner.use(EnsureTxCost({ attachSenderChanges: true }));
166
223
  // Save context snapshot before updating states
167
224
  runner.use(pipes.TakeStateSnapshot());
168
225
 
169
- // Update sender state, token factory state
226
+ // Update sender state, token factory state, and lock stake
170
227
  runner.use(
171
228
  async (context, next) => {
172
- const { tx, itx, statedb, senderState, delegatorState, delegationState, senderChange, updateVaults } = context;
229
+ const {
230
+ tx,
231
+ itx,
232
+ statedb,
233
+ senderState,
234
+ delegatorState,
235
+ delegationState,
236
+ senderChange,
237
+ stakeState,
238
+ stakeAddress,
239
+ updateVaults,
240
+ config,
241
+ } = context;
173
242
  const data = decodeAnySafe(itx.data);
174
243
  const owner = delegatorState ? delegatorState.address : senderState.address;
175
244
 
@@ -181,7 +250,7 @@ runner.use(
181
250
  ? applyTokenChange({ tokens: senderTokens }, senderChange)
182
251
  : { tokens: senderTokens };
183
252
 
184
- const [newSenderState, newTokenState, newTokenFactoryState, newDelegationState] = await Promise.all([
253
+ const [newSenderState, newTokenState, newTokenFactoryState, newDelegationState, newStakeState] = await Promise.all([
185
254
  statedb.account.update(
186
255
  senderState.address,
187
256
  account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
@@ -213,6 +282,17 @@ runner.use(
213
282
  context.isDelegationChanged
214
283
  ? statedb.delegation.update(delegationState.address, delegation.update(delegationState, {}, context), context)
215
284
  : delegationState,
285
+
286
+ // Lock the stake after successful token creation
287
+ statedb.stake.update(
288
+ stakeAddress,
289
+ stake.update(
290
+ stakeState,
291
+ { revocable: false, revokeWaitingPeriod: config.transaction.txStake.createTokenLockPeriod },
292
+ context
293
+ ),
294
+ context
295
+ ),
216
296
  ]);
217
297
 
218
298
  await updateVaults();
@@ -221,6 +301,7 @@ runner.use(
221
301
  context.tokenState = newTokenState;
222
302
  context.tokenFactoryState = newTokenFactoryState;
223
303
  context.delegationState = newDelegationState;
304
+ context.stakeState = newStakeState;
224
305
 
225
306
  debug('create token factory', newTokenFactoryState);
226
307
 
@@ -0,0 +1,27 @@
1
+ const get = require('lodash/get');
2
+ const set = require('lodash/set');
3
+ const { CustomError: Error } = require('@ocap/util/lib/error');
4
+ const { isSvgFile, sanitizeSvg } = require('@blocklet/xss');
5
+
6
+ module.exports =
7
+ ({ iconKey = 'itx.token.icon', size = 16 * 1024 } = {}) =>
8
+ (context, next) => {
9
+ const icon = get(context, iconKey);
10
+
11
+ if (!icon) return next();
12
+
13
+ if (!isSvgFile(icon)) {
14
+ return next(new Error('INVALID_ICON', 'Token Icon is not a valid SVG file'));
15
+ }
16
+
17
+ const buffer = Buffer.from(icon, 'utf8');
18
+ if (buffer.length > size) {
19
+ return next(new Error('INVALID_ICON', `Token Icon must be less than ${Math.floor(size / 1024)}k`));
20
+ }
21
+
22
+ const sanitizedIcon = sanitizeSvg(icon);
23
+
24
+ set(context, iconKey, sanitizedIcon);
25
+
26
+ return next();
27
+ };
@@ -0,0 +1,58 @@
1
+ const get = require('lodash/get');
2
+ const { CustomError: Error } = require('@ocap/util/lib/error');
3
+ const { verify: verifyVC } = require('@arcblock/vc');
4
+
5
+ module.exports =
6
+ ({ tokenKey = 'itx.token' } = {}) =>
7
+ async (context, next) => {
8
+ const { symbol, address, website } = get(context, tokenKey) || {};
9
+
10
+ if (!website) return next();
11
+
12
+ const {
13
+ tx: { from, delegator },
14
+ config: { chainId },
15
+ } = context;
16
+
17
+ const sender = delegator || from;
18
+ const verificationUrl = `${website}/.well-known/ocap/tokens.json`;
19
+
20
+ try {
21
+ const response = await fetch(verificationUrl);
22
+ const data = await response.json();
23
+
24
+ const valids = await Promise.all(
25
+ data
26
+ .filter(Boolean)
27
+ .filter((item) => {
28
+ if (item.type !== 'TokenIssueCredential') return false;
29
+ if (item.issuer.id !== sender) return false;
30
+
31
+ const { id, issued } = item.credentialSubject || {};
32
+ return (
33
+ id === sender &&
34
+ issued.address === address &&
35
+ issued.symbol === symbol &&
36
+ issued.chainId === chainId &&
37
+ issued.website === website
38
+ );
39
+ })
40
+ .map((item) => {
41
+ return verifyVC({
42
+ vc: item,
43
+ ownerDid: sender,
44
+ trustedIssuers: sender,
45
+ });
46
+ })
47
+ );
48
+
49
+ if (valids.some((valid) => valid)) {
50
+ return next();
51
+ }
52
+
53
+ context.logger?.warn('Website verification failed', { address, symbol, website, data });
54
+ return next(new Error('INVALID_WEBSITE', `Website ${website} verification failed`));
55
+ } catch (error) {
56
+ return next(new Error('INVALID_WEBSITE', `Website ${website} verification failed: ${error.message}`));
57
+ }
58
+ };
@@ -0,0 +1,17 @@
1
+ const get = require('lodash/get');
2
+ const { CustomError: Error } = require('@ocap/util/lib/error');
3
+ const { verifyUrl } = require('@ocap/util/lib/url');
4
+
5
+ module.exports =
6
+ ({ urlKeys = ['itx.token.website', 'itx.token.metadata.value.communityUrl'] } = {}) =>
7
+ (context, next) => {
8
+ for (const key of urlKeys) {
9
+ const url = get(context, key);
10
+ if (url && !verifyUrl(url)) {
11
+ const name = key.split('.').pop();
12
+ return next(new Error('INVALID_TX', `${name} is not valid`));
13
+ }
14
+ }
15
+
16
+ return next();
17
+ };
@@ -1,22 +1,41 @@
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, delegation } = require('@ocap/state');
5
- const { applyTokenChange, getDelegationRequirements } = require('../../util');
4
+ const { account, tokenFactory, delegation, token } = require('@ocap/state');
5
+ const { applyTokenChange, getDelegationRequirements, decodeAnySafe } = 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`);
9
9
 
10
10
  const EnsureTxGas = require('../../pipes/ensure-gas');
11
11
  const EnsureTxCost = require('../../pipes/ensure-cost');
12
+ const verifyIcon = require('./pipes/verify-icon');
13
+ const verifyUrl = require('./pipes/verify-url');
14
+ const verifyOwnership = require('./pipes/verify-ownership');
12
15
 
13
16
  const runner = new Runner();
14
17
 
18
+ const isTest = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' || process.env.CI;
19
+
15
20
  runner.use(pipes.VerifyMultiSig(0));
16
21
 
17
22
  const schema = Joi.object({
18
23
  address: Joi.DID().prefix().role('ROLE_TOKEN_FACTORY').required(),
19
- feeRate: Joi.number().min(0).max(2000).required(),
24
+ feeRate: Joi.number().min(0).max(2000).optional().allow(null),
25
+ token: Joi.object({
26
+ icon: Joi.string().optional().allow(null, ''),
27
+ website: Joi.string()
28
+ .uri({
29
+ scheme: isTest ? ['http', 'https'] : ['https'],
30
+ allowRelative: false,
31
+ })
32
+ .max(256)
33
+ .optional()
34
+ .allow(null, ''),
35
+ metadata: Joi.any().optional().allow(null),
36
+ })
37
+ .optional()
38
+ .allow(null),
20
39
  data: Joi.any().optional().allow(null),
21
40
  }).options({ stripUnknown: true, noDefaults: false });
22
41
 
@@ -27,9 +46,22 @@ runner.use((context, next) => {
27
46
  return next(new Error('INVALID_TX', `Invalid itx: ${error.message}`));
28
47
  }
29
48
 
49
+ // decode and verify metadata
50
+ if (context.itx.token?.metadata) {
51
+ const metadata = decodeAnySafe(context.itx.token.metadata);
52
+ const { error: metadataError, value: metadataValue } = token.metadataSchema.validate(metadata.value);
53
+ if (metadataError) {
54
+ return next(new Error('INVALID_TX', `Invalid metadata: ${metadataError.message}`));
55
+ }
56
+ context.itx.token.metadata = { ...metadata, value: metadataValue };
57
+ }
58
+
30
59
  return next();
31
60
  });
32
61
 
62
+ runner.use(verifyIcon({ iconKey: 'itx.token.icon' }));
63
+ runner.use(verifyUrl({ urlKeys: ['itx.token.website', 'itx.token.metadata.value.communityUrl'] }));
64
+
33
65
  // ensure token factory exists
34
66
  runner.use(
35
67
  pipes.ExtractState({
@@ -40,6 +72,24 @@ runner.use(
40
72
  })
41
73
  );
42
74
 
75
+ // ensure token exists
76
+ runner.use(
77
+ pipes.ExtractState({
78
+ from: 'tokenFactoryState.tokenAddress',
79
+ to: 'tokenState',
80
+ table: 'token',
81
+ status: 'INVALID_TOKEN',
82
+ })
83
+ );
84
+
85
+ // verify website
86
+ runner.use((context, next) => {
87
+ const { tokenState, itx } = context;
88
+ context.itxToken = { symbol: tokenState.symbol, address: tokenState.address, website: itx.token?.website };
89
+ return next();
90
+ });
91
+ runner.use(verifyOwnership({ tokenKey: 'itxToken' }));
92
+
43
93
  // Ensure sender
44
94
  runner.use(pipes.ExtractState({ from: 'tx.from', to: 'senderState', status: 'INVALID_SENDER_STATE', table: 'account' })); // prettier-ignore
45
95
  runner.use(pipes.VerifyAccountMigration({ stateKey: 'senderState', addressKey: 'tx.from' }));
@@ -88,7 +138,17 @@ runner.use(pipes.TakeStateSnapshot());
88
138
  // Update sender state, token factory state
89
139
  runner.use(
90
140
  async (context, next) => {
91
- const { tx, itx, statedb, senderState, tokenFactoryState, senderChange, updateVaults, delegationState } = context;
141
+ const {
142
+ tx,
143
+ itx,
144
+ statedb,
145
+ senderState,
146
+ tokenFactoryState,
147
+ tokenState,
148
+ senderChange,
149
+ updateVaults,
150
+ delegationState,
151
+ } = context;
92
152
 
93
153
  const { tokens: senderTokens = {} } = senderState;
94
154
 
@@ -96,7 +156,16 @@ runner.use(
96
156
  ? applyTokenChange({ tokens: senderTokens }, senderChange)
97
157
  : { tokens: senderTokens };
98
158
 
99
- const [newSenderState, newTokenFactoryState, newDelegationState] = await Promise.all([
159
+ const factoryUpdates = itx.feeRate ? { feeRate: itx.feeRate } : {};
160
+ const tokenUpdates = itx.token
161
+ ? {
162
+ icon: itx.token?.icon || tokenState.icon,
163
+ website: itx.token?.website || tokenState.website,
164
+ metadata: itx.token?.metadata || tokenState.metadata,
165
+ }
166
+ : null;
167
+
168
+ const [newSenderState, newTokenFactoryState, newTokenState, newDelegationState] = await Promise.all([
100
169
  statedb.account.update(
101
170
  senderState.address,
102
171
  account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
@@ -105,10 +174,14 @@ runner.use(
105
174
 
106
175
  statedb.tokenFactory.update(
107
176
  tokenFactoryState.address,
108
- tokenFactory.update(tokenFactoryState, { feeRate: itx.feeRate }, context),
177
+ tokenFactory.update(tokenFactoryState, factoryUpdates, context),
109
178
  context
110
179
  ),
111
180
 
181
+ tokenUpdates
182
+ ? statedb.token.update(tokenState.address, token.update(tokenState, tokenUpdates, context), context)
183
+ : tokenState,
184
+
112
185
  // Update delegation state
113
186
  context.isDelegationChanged
114
187
  ? statedb.delegation.update(delegationState.address, delegation.update(delegationState, {}, context), context)
@@ -119,9 +192,10 @@ runner.use(
119
192
 
120
193
  context.senderState = newSenderState;
121
194
  context.tokenFactoryState = newTokenFactoryState;
195
+ context.tokenState = newTokenState;
122
196
  context.delegationState = newDelegationState;
123
197
 
124
- debug('update token factory', newTokenFactoryState);
198
+ debug('update token factory', newTokenFactoryState, newTokenState);
125
199
 
126
200
  next();
127
201
  },
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.24.9",
6
+ "version": "1.25.1",
7
7
  "description": "Predefined tx pipeline sets to execute certain type of transactions",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -13,24 +13,26 @@
13
13
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
14
14
  "license": "MIT",
15
15
  "dependencies": {
16
+ "@blocklet/xss": "^0.2.7",
16
17
  "debug": "^4.3.6",
17
18
  "deep-diff": "^1.0.2",
18
19
  "empty-value": "^1.0.1",
19
20
  "lodash": "^4.17.21",
20
21
  "url-join": "^4.0.1",
21
- "@arcblock/did": "1.24.9",
22
- "@arcblock/did-util": "1.24.9",
23
- "@arcblock/jwt": "1.24.9",
24
- "@arcblock/validator": "1.24.9",
25
- "@ocap/asset": "1.24.9",
26
- "@ocap/client": "1.24.9",
27
- "@ocap/mcrypto": "1.24.9",
28
- "@ocap/merkle-tree": "1.24.9",
29
- "@ocap/message": "1.24.9",
30
- "@ocap/state": "1.24.9",
31
- "@ocap/util": "1.24.9",
32
- "@ocap/wallet": "1.24.9",
33
- "@ocap/tx-pipeline": "1.24.9"
22
+ "@arcblock/did": "1.25.1",
23
+ "@arcblock/did-util": "1.25.1",
24
+ "@arcblock/jwt": "1.25.1",
25
+ "@arcblock/validator": "1.25.1",
26
+ "@arcblock/vc": "1.25.1",
27
+ "@ocap/client": "1.25.1",
28
+ "@ocap/mcrypto": "1.25.1",
29
+ "@ocap/merkle-tree": "1.25.1",
30
+ "@ocap/state": "1.25.1",
31
+ "@ocap/message": "1.25.1",
32
+ "@ocap/util": "1.25.1",
33
+ "@ocap/tx-pipeline": "1.25.1",
34
+ "@ocap/wallet": "1.25.1",
35
+ "@ocap/asset": "1.25.1"
34
36
  },
35
37
  "resolutions": {
36
38
  "bn.js": "5.2.2",
@@ -39,8 +41,8 @@
39
41
  "devDependencies": {
40
42
  "jest": "^29.7.0",
41
43
  "start-server-and-test": "^1.14.0",
42
- "@ocap/e2e-test": "1.24.9",
43
- "@ocap/statedb-memory": "1.24.9"
44
+ "@ocap/e2e-test": "1.25.1",
45
+ "@ocap/statedb-memory": "1.25.1"
44
46
  },
45
47
  "scripts": {
46
48
  "lint": "eslint tests lib",