@ocap/tx-protocols 1.20.1 → 1.20.3

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/README.md CHANGED
@@ -5,7 +5,7 @@ Defines the interface of OCAP query resolver.
5
5
  ## Usage
6
6
 
7
7
  ```shell
8
- yarn add @ocap/tx-pipeline
8
+ pnpm install @ocap/tx-pipeline
9
9
  ```
10
10
 
11
11
  Then:
package/lib/execute.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable consistent-return */
2
- const { promisify } = require('util');
3
2
  const get = require('lodash/get');
4
3
  const pick = require('lodash/pick');
4
+ const merge = require('lodash/merge');
5
5
  const camelCase = require('lodash/camelCase');
6
6
  const { CustomError: Error } = require('@ocap/util/lib/error');
7
7
  const { Runner, pipes } = require('@ocap/tx-pipeline');
@@ -9,9 +9,8 @@ const { Runner, pipes } = require('@ocap/tx-pipeline');
9
9
  const createEnsureGasFn = require('./pipes/ensure-gas');
10
10
  const createEnsureCostFn = require('./pipes/ensure-cost');
11
11
 
12
- // FIXME: DeprecationWarning: Calling promisify on a function that returns a Promise is likely a mistake.
13
- const ensureTxGas = promisify(createEnsureGasFn(() => ({ create: 0, update: 2 })));
14
- const ensureTxCost = promisify(createEnsureCostFn({ attachSenderChanges: true, gasOnly: true }));
12
+ const ensureTxGas = createEnsureGasFn(() => ({ create: 0, update: 2 }));
13
+ const ensureTxCost = createEnsureCostFn({ attachSenderChanges: true, gasOnly: true });
15
14
 
16
15
  const getTxName = (typeUrl) => camelCase(typeUrl.split(':').pop());
17
16
 
@@ -50,24 +49,45 @@ module.exports = ({ filter, runAsLambda }) => {
50
49
  pre.use(pipes.VerifySignature);
51
50
 
52
51
  // charge gas fee for errored tx
53
- const ensureGasFeePaid = async (context) => {
54
- try {
55
- await ensureTxGas(context);
56
- await ensureTxCost(context);
52
+ const ensureGasFeePaid = (context) => {
53
+ const gasRunner = new Runner();
54
+
55
+ gasRunner.use(ensureTxGas);
56
+ gasRunner.use(ensureTxCost);
57
+ gasRunner.use(async (ctx, next) => {
58
+ const { tx, statedb, senderUpdates, updateVaults } = ctx;
59
+
60
+ // If senderState does not exist and there is no stakeId for gas, an error will be thrown in ensureTxCost before here.
61
+ // So if there is no senderState here, it means there has a stake for gas, so allowed to continue.
62
+ if (ctx.senderState) {
63
+ const senderState = ctx.stateSnapshot?.[ctx.senderState.address] || ctx.senderState;
64
+ await statedb.account.update(
65
+ senderState.address,
66
+ ctx.states.account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, ctx),
67
+ ctx
68
+ );
69
+ await updateVaults();
70
+ }
57
71
 
58
- const { tx, statedb, senderState, senderUpdates, updateVaults } = context;
72
+ return next();
73
+ });
59
74
 
60
- await Promise.all([
61
- statedb.account.update(
62
- senderState.address,
63
- context.states.account.update(senderState, { nonce: tx.nonce, pk: tx.pk, ...senderUpdates }, context),
64
- context
65
- ),
66
- ]);
67
- await updateVaults();
68
- } catch (err) {
69
- context.logger?.error('Failed to charge gas fee for errored tx', { error: err, txHash: context.txHash });
70
- }
75
+ return new Promise((resolve, reject) => {
76
+ gasRunner.run(context, (err) => {
77
+ if (err) {
78
+ context.logger?.error('Failed to charge gas fee for errored tx', {
79
+ error: err,
80
+ tx: context.tx,
81
+ txBase64: context.txBase64,
82
+ txHash: context.txHash,
83
+ });
84
+ reject(err);
85
+ return;
86
+ }
87
+
88
+ resolve();
89
+ });
90
+ });
71
91
  };
72
92
 
73
93
  // eslint-disable-next-line require-await
@@ -86,124 +106,131 @@ module.exports = ({ filter, runAsLambda }) => {
86
106
  }
87
107
 
88
108
  // eslint-disable-next-line no-shadow
89
- protocol.run(context, async (err) => {
90
- // we should only flush events when retry is not supported
91
- // otherwise the outer caller should handle these 2
92
- if (shouldPersistTx(err) && isRetrySupported === false) {
93
- if (err) {
94
- await ensureGasFeePaid(context);
109
+ protocol.run(context, async (error) => {
110
+ if (isRetrySupported) {
111
+ if (error) {
112
+ return reject(error);
95
113
  }
114
+ return resolve(context);
115
+ }
116
+
117
+ const txStatus = error ? error.code || 'INTERNAL' : 'OK';
118
+ let txState = context.states.tx.create(context, txStatus);
96
119
 
97
- let txState;
120
+ if (shouldPersistTx(error)) {
98
121
  try {
99
- txState = context.states.tx.create(context, err ? err.code || 'INTERNAL' : 'OK');
122
+ if (error) {
123
+ await ensureGasFeePaid(context);
124
+ // Recreate tx to pick up gas related fields
125
+ txState = context.states.tx.create(context, txStatus);
126
+ }
127
+
100
128
  await context.statedb.tx.create(txState.hash, txState, context);
101
- context.logger?.info('Tx finalized', {
102
- txHash: context.txHash,
103
- txStatus: err ? err.code || 'INTERNAL' : 'OK',
104
- txState,
105
- error: err,
106
- });
129
+ flushEvents(context, { txState });
107
130
  } catch (e) {
108
- context.logger?.error('Failed to save invalid transaction to statedb', {
131
+ context.logger?.error('Failed to save transaction to statedb', {
109
132
  error: e,
133
+ txError: error,
110
134
  txHash: context.txHash,
111
135
  txState,
112
136
  });
113
- return reject(e);
137
+ // If we get an internal error here, should return the original tx error to client
138
+ return reject(error);
114
139
  }
115
-
116
- flushEvents(context, { txState });
117
140
  }
118
141
 
119
- // after executing the transaction
120
- if (err) {
121
- context.logger?.error('Failed to execute transaction', { error: err, txHash: context.txHash });
122
- return reject(err);
123
- }
142
+ context.logger?.info('Tx finalized', {
143
+ txHash: context.txHash,
144
+ txStatus,
145
+ txState,
146
+ error,
147
+ });
124
148
 
125
- resolve(context);
149
+ if (error) {
150
+ return reject(error);
151
+ }
152
+ return resolve(context);
126
153
  });
127
154
  });
128
155
  });
129
156
 
130
157
  if (typeof runAsLambda === 'function') {
131
158
  return async (context, protocols) => {
132
- let ctx = null;
133
- let error = null;
134
- let runCount = 0;
135
- const retryLimit = context.statedb?.config?.retryLimit || 0;
136
- const shouldRetry = context.statedb?.config?.shouldRetry || (() => false);
159
+ let ctx = context;
137
160
  const startTime = Date.now();
138
161
 
139
- await runAsLambda(async (txn) => {
140
- runCount += 1;
162
+ try {
163
+ const txState = await runAsLambda(
164
+ async (txn) => {
165
+ // create a new context each time in case we are retrying
166
+ ctx = pick(context, ['txBase64', 'statedb', 'indexdb', 'config', 'states', 'filter', 'extra', 'logger']);
167
+ ctx.txn = txn;
141
168
 
142
- // create a new context each time in case we are retrying
143
- ctx = pick(context, ['txBase64', 'statedb', 'indexdb', 'config', 'states', 'filter', 'extra', 'logger']);
144
- Object.defineProperty(ctx, 'txn', { value: txn });
169
+ await execute(ctx, protocols, true);
145
170
 
146
- try {
147
- await execute(ctx, protocols, true);
148
- } catch (err) {
149
- if (runCount <= retryLimit && shouldRetry(err)) {
150
- // throw the error to retry
151
- throw err;
152
- }
153
- error = err;
154
- }
171
+ const state = context.states.tx.create(ctx, 'OK');
172
+ await context.statedb.tx.create(state.hash, state, ctx);
155
173
 
156
- if (error && !shouldPersistTx(error)) {
157
- ctx.logger?.error('Failed to execute transaction', { error, txHash: ctx.txHash });
158
- // throw the error to the abort transaction
159
- throw error;
160
- }
161
-
162
- // create tx
163
- const txStatus = error ? error.code || 'INTERNAL' : 'OK';
164
- try {
165
- let txState = context.states.tx.create(ctx, txStatus);
166
- flushEvents(ctx, { txState });
167
-
168
- if (error) {
169
- await ensureGasFeePaid(ctx);
170
- // Recreate tx to pick up gas related fields
171
- txState = context.states.tx.create(ctx, txStatus);
172
- }
174
+ return state;
175
+ },
176
+ { cleanWorkingSet: true, verifyHash: true }
177
+ );
173
178
 
174
- await context.statedb.tx.create(txState.hash, txState, ctx);
175
- flushEvents(ctx, { txState });
179
+ // update indexdb after statedb commit
180
+ flushEvents(ctx, { txState });
176
181
 
177
- ctx.logger?.info('Tx finalized', {
178
- txHash: ctx.txHash,
179
- txStatus,
180
- txState,
181
- error,
182
- runCount,
183
- duration: Date.now() - startTime,
184
- });
185
- } catch (err) {
186
- if (runCount <= retryLimit && shouldRetry(err)) {
187
- // throw error to retry
188
- throw err;
182
+ ctx.logger?.info('Tx finalized', {
183
+ txHash: ctx.txHash,
184
+ txState,
185
+ txStatus: 'OK',
186
+ duration: Date.now() - startTime,
187
+ });
188
+ } catch (error) {
189
+ const txStatus = error.code || 'INTERNAL';
190
+ let txState = ctx.tx ? context.states.tx.create(ctx, txStatus, false) : null;
191
+
192
+ ctx.logger?.error('Failed to execute transaction', { error, txHash: ctx.txHash, txState });
193
+
194
+ if (txState && shouldPersistTx(error)) {
195
+ try {
196
+ txState = await runAsLambda(async (txn) => {
197
+ ctx = Object.assign({}, ctx, { txn, cacheStates: null });
198
+
199
+ await ensureGasFeePaid(ctx);
200
+
201
+ // Recreate tx to pick up gas related fields
202
+ const state = context.states.tx.create(ctx, txStatus);
203
+ await ctx.statedb.tx.create(state.hash, state, ctx);
204
+
205
+ return state;
206
+ });
207
+
208
+ // update indexdb after statedb commit
209
+ flushEvents(ctx, { txState });
210
+
211
+ ctx.logger?.info('Tx finalized', {
212
+ txHash: ctx.txHash,
213
+ txState,
214
+ txStatus,
215
+ error,
216
+ duration: Date.now() - startTime,
217
+ });
218
+ } catch (err) {
219
+ ctx.logger?.error('Failed to save invalid transaction to statedb', {
220
+ error: err,
221
+ txError: error,
222
+ txHash: ctx.txHash,
223
+ txState,
224
+ });
225
+ // If we get an error here, should return the original tx error to client
226
+ throw error;
189
227
  }
190
-
191
- const txState = context.states.tx.create(ctx, txStatus, false);
192
- ctx.logger?.error('Failed to save invalid transaction to statedb', {
193
- error: err,
194
- protocolError: error,
195
- txHash: ctx.txHash,
196
- txState,
197
- duration: Date.now() - startTime,
198
- });
199
-
200
- // throw error to abort transaction
201
- throw err;
202
228
  }
203
- });
204
229
 
205
- if (error) {
206
230
  throw error;
231
+ } finally {
232
+ // Merge new ctx to original context
233
+ merge(context, ctx);
207
234
  }
208
235
 
209
236
  return ctx;
@@ -24,7 +24,7 @@ module.exports = function CreateEnsureTxCostPipe({
24
24
  } = {}) {
25
25
  return async function EnsureTxCost(context, next) {
26
26
  // TODO: we are using the sender as gas payer, this may change in future
27
- const { config, statedb, txType, senderState, gasEstimate, gasVault, totalGas } = context;
27
+ const { config, statedb, txType, senderState, gasEstimate, totalGas } = context;
28
28
 
29
29
  // verify gas staking headers
30
30
  const { tx, extra = {}, txHash } = context;
@@ -83,11 +83,11 @@ module.exports = function CreateEnsureTxCostPipe({
83
83
  }
84
84
 
85
85
  let isCostCharged = false;
86
- if (senderState && txCost.gt(ZERO)) {
86
+ if (txCost.gt(ZERO)) {
87
87
  const expected = new BN(gasEstimate.payment || 0).add(txCost);
88
- const actual = new BN(senderState.tokens[config.token.address] || 0);
88
+ const actual = new BN(senderState?.tokens?.[config.token.address] || 0);
89
89
  // If we have someone with enough balance to pay for this tx
90
- if (actual.gte(expected)) {
90
+ if (senderState && actual.gte(expected)) {
91
91
  isCostCharged = true;
92
92
 
93
93
  // to be merged into later pipe
@@ -108,7 +108,7 @@ module.exports = function CreateEnsureTxCostPipe({
108
108
  context.updateVaults = async function updateVaults() {
109
109
  const [feeVaultState, gasVaultState] = await Promise.all([
110
110
  changes.fee ? statedb.account.get(context.feeVault, context) : null,
111
- changes.gas ? statedb.account.get(gasVault, context) : null,
111
+ changes.gas ? statedb.account.get(context.gasVault, context) : null,
112
112
  ]);
113
113
 
114
114
  const [newFeeVaultState, newGasVaultState] = await Promise.all([
@@ -167,11 +167,13 @@ module.exports = function CreateEnsureTxCostPipe({
167
167
 
168
168
  context.gasPaid = true;
169
169
  };
170
+ } else if (!senderState) {
171
+ return next(new Error('INVALID_GAS_PAYER', `Gas payer ${tx.from} does not exist on chain`));
170
172
  } else if (throwOnInsufficientFund) {
171
173
  return next(
172
174
  new Error(
173
175
  'INSUFFICIENT_FUND',
174
- `Insufficient fund to pay for tx cost from ${senderState.address}, expected ${fromUnitToToken(
176
+ `Insufficient fund to pay for tx cost from ${senderState?.address || tx.from}, expected ${fromUnitToToken(
175
177
  expected,
176
178
  config.token.decimal
177
179
  )}, got ${fromUnitToToken(actual, config.token.decimal)}`
@@ -69,7 +69,6 @@ runner.use(
69
69
  })
70
70
  );
71
71
  runner.use(EnsureTxCost({ attachSenderChanges: true, throwOnInsufficientFund: false }));
72
- runner.use(pipes.VerifyGasPayer());
73
72
 
74
73
  // Save context snapshot before updating states
75
74
  runner.use(pipes.TakeStateSnapshot());
@@ -178,7 +178,6 @@ runner.use(
178
178
  })
179
179
  );
180
180
  runner.use(EnsureTxCost({ attachSenderChanges: true }));
181
- runner.use(pipes.VerifyGasPayer());
182
181
 
183
182
  // Save context snapshot before updating states
184
183
  runner.use(pipes.TakeStateSnapshot());
package/package.json CHANGED
@@ -3,41 +3,36 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.20.1",
6
+ "version": "1.20.3",
7
7
  "description": "Predefined tx pipeline sets to execute certain type of transactions",
8
8
  "main": "lib/index.js",
9
9
  "files": [
10
10
  "lib"
11
11
  ],
12
- "scripts": {
13
- "lint": "eslint tests lib",
14
- "lint:fix": "eslint --fix tests lib",
15
- "start": "node tools/start-chain.js",
16
- "test": "jest --forceExit --detectOpenHandles",
17
- "test:ci": "jest --forceExit --detectOpenHandles --coverage",
18
- "coverage": "start-server-and-test start http://127.0.0.1:4001 test:ci"
19
- },
20
12
  "keywords": [],
21
13
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
22
14
  "license": "MIT",
23
15
  "dependencies": {
24
- "@arcblock/did": "1.20.1",
25
- "@arcblock/did-util": "1.20.1",
26
- "@arcblock/jwt": "1.20.1",
27
- "@arcblock/validator": "1.20.1",
28
- "@ocap/asset": "1.20.1",
29
- "@ocap/mcrypto": "1.20.1",
30
- "@ocap/merkle-tree": "1.20.1",
31
- "@ocap/message": "1.20.1",
32
- "@ocap/state": "1.20.1",
33
- "@ocap/tx-pipeline": "1.20.1",
34
- "@ocap/util": "1.20.1",
35
- "@ocap/wallet": "1.20.1",
36
16
  "debug": "^4.3.6",
37
17
  "deep-diff": "^1.0.2",
38
18
  "empty-value": "^1.0.1",
39
19
  "lodash": "^4.17.21",
40
- "url-join": "^4.0.1"
20
+ "url-join": "^4.0.1",
21
+ "@arcblock/did": "1.20.3",
22
+ "@arcblock/did-util": "1.20.3",
23
+ "@arcblock/jwt": "1.20.3",
24
+ "@arcblock/validator": "1.20.3",
25
+ "@ocap/asset": "1.20.3",
26
+ "@ocap/mcrypto": "1.20.3",
27
+ "@ocap/merkle-tree": "1.20.3",
28
+ "@ocap/message": "1.20.3",
29
+ "@ocap/state": "1.20.3",
30
+ "@ocap/util": "1.20.3",
31
+ "@ocap/wallet": "1.20.3",
32
+ "@ocap/tx-pipeline": "1.20.3",
33
+ "@ocap/e2e-test": "1.20.3",
34
+ "@ocap/client": "1.20.3",
35
+ "@ocap/statedb-memory": "1.20.3"
41
36
  },
42
37
  "resolutions": {
43
38
  "bn.js": "5.2.1",
@@ -47,5 +42,12 @@
47
42
  "jest": "^29.7.0",
48
43
  "start-server-and-test": "^1.14.0"
49
44
  },
50
- "gitHead": "f73bddbe4b86106fd348e43ce9e19a626acdc9f6"
51
- }
45
+ "scripts": {
46
+ "lint": "eslint tests lib",
47
+ "lint:fix": "eslint --fix tests lib",
48
+ "start": "node tools/start-chain.js",
49
+ "test": "jest --forceExit --detectOpenHandles",
50
+ "test:ci": "jest --forceExit --detectOpenHandles --coverage",
51
+ "coverage": "start-server-and-test start http://127.0.0.1:4001 test:ci"
52
+ }
53
+ }