Skip to content

Commit

Permalink
Merge pull request #21 from ckb-cell/feat/optimize-process
Browse files Browse the repository at this point in the history
feat: optimize transaction process
  • Loading branch information
ahonn authored Mar 29, 2024
2 parents e75f3f3 + 3be2245 commit 553ed1d
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 82 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^3.0.0",
"@nervosnetwork/ckb-sdk-utils": "^0.109.1",
"@rgbpp-sdk/btc": "0.0.0-snap-20240327091939",
"@rgbpp-sdk/ckb": "0.0.0-snap-20240327091939",
"@rgbpp-sdk/service": "0.0.0-snap-20240327091939",
"@rgbpp-sdk/btc": "0.0.0-snap-20240329092526",
"@rgbpp-sdk/ckb": "0.0.0-snap-20240329092526",
"@rgbpp-sdk/service": "0.0.0-snap-20240329092526",
"@sentry/node": "^7.102.1",
"@sentry/profiling-node": "^7.102.1",
"awilix": "^10.0.1",
Expand Down
30 changes: 15 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 47 additions & 1 deletion src/routes/rgbpp/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const transactionRoute: FastifyPluginCallback<Record<never, never>, Server, ZodT
response: {
200: z.object({
state: z.string().describe('The state of the transaction'),
attempts: z.number().describe('The number of attempts made to process the transaction'),
failedReason: z.string().optional().describe('The reason why the transaction failed'),
}),
},
Expand All @@ -147,13 +148,58 @@ const transactionRoute: FastifyPluginCallback<Record<never, never>, Server, ZodT
return;
}
const state = await job.getState();
const attempts = job.attemptsMade;
if (state === 'failed') {
return {
state,
attempts,
failedReason: job.failedReason,
};
}
return { state };
return {
state,
attempts,
};
},
);

fastify.post(
'/retry',
{
schema: {
description: 'Retry a failed transaction by BTC txid, only failed transactions can be retried.',
tags: ['RGB++'],
body: z.object({
btc_txid: z.string(),
}),
response: {
200: z.object({
success: z.boolean().describe('Whether the transaction has been retried successfully'),
state: z.string().describe('The state of the transaction'),
}),
},
},
},
async (request, reply) => {
const { btc_txid } = request.body;
const job = await fastify.transactionManager.getTransactionRequest(btc_txid);
if (!job) {
reply.status(404);
return;
}
const state = await job.getState();
if (state === 'failed') {
await job.retry('failed');
const newState = await job.getState();
return {
success: true,
state: newState,
};
}
return {
success: false,
state,
};
},
);

Expand Down
112 changes: 80 additions & 32 deletions src/services/paymaster.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Cell, helpers } from '@ckb-lumos/lumos';
import { Cradle } from '../container';
import { Queue, Worker } from 'bullmq';
import { DelayedError, Queue, Worker } from 'bullmq';
import { AppendPaymasterCellAndSignTxParams, IndexerCell, appendPaymasterCellAndSignCkbTx } from '@rgbpp-sdk/ckb';
import { hd, config, BI } from '@ckb-lumos/lumos';
import * as Sentry from '@sentry/node';
Expand Down Expand Up @@ -69,6 +69,25 @@ export default class Paymaster implements IPaymaster {
return lockScript;
}

/**
* Get the paymaster cell job by the raw transaction
* @param rawTx - the raw transaction may contains an input using one paymaster cell
*/
private getPaymasterCellJobByRawTx(rawTx: CKBComponents.RawTransaction) {
for (const input of rawTx.inputs) {
const outPoint = input.previousOutput;
if (!outPoint) {
continue;
}
const id = `${outPoint.txHash}:${outPoint.index}`;
const job = this.queue.getJob(id);
if (job) {
return job;
}
}
return null;
}

public get privateKey() {
return this.cradle.env.PAYMASTER_PRIVATE_KEY;
}
Expand Down Expand Up @@ -151,22 +170,26 @@ export default class Paymaster implements IPaymaster {

const collector = this.cradle.ckbIndexer.collector({
lock: this.lockScript,
type: 'empty',
outputCapacityRange: [BI.from(this.cellCapacity).toHexString(), BI.from(this.cellCapacity + 1).toHexString()],
});
const cells = collector.collect();

for await (const cell of cells) {
const outPoint = cell.outPoint!;
this.cradle.logger.info(
`[Paymaster] Refill paymaster cell: ${outPoint.txHash}:${outPoint.index}, ${cell.cellOutput.capacity} CKB`,
);
await this.queue.add(PAYMASTER_CELL_QUEUE_NAME, cell, {
// use the outPoint as the jobId to avoid duplicate cells
jobId: `${outPoint.txHash}:${outPoint.index}`,
});
// count the filled cells, it maybe less than the cells we added
// because we may have duplicate cells, but it's work fine
const jobId = `${outPoint.txHash}:${outPoint.index}`;

// check if the cell is already in the queue
const job = await this.queue.getJob(jobId);
if (job) {
this.cradle.logger.info(`[Paymaster] Paymaster cell already in the queue: ${jobId}`);
continue;
}
// add the cell to the queue
await this.queue.add(PAYMASTER_CELL_QUEUE_NAME, cell, { jobId });
this.cradle.logger.info(`[Paymaster] Refill paymaster cell: ${jobId}`);
filled += 1;
// break if the filled cells are enough
if (queueSize + filled >= this.presetCount) {
break;
}
Expand All @@ -183,23 +206,31 @@ export default class Paymaster implements IPaymaster {
token: string,
params: Pick<AppendPaymasterCellAndSignTxParams, 'ckbRawTx' | 'sumInputsCapacity'>,
) {
const { ckbRawTx, sumInputsCapacity } = params;
const paymasterCell = await this.getNextCell(token);
this.cradle.logger.info(`[Paymaster] Get paymaster cell: ${JSON.stringify(paymasterCell)}`);
try {
const { ckbRawTx, sumInputsCapacity } = params;
const paymasterCell = await this.getNextCell(token);
this.cradle.logger.info(`[Paymaster] Get paymaster cell: ${JSON.stringify(paymasterCell)}`);

if (!paymasterCell) {
throw new PaymasterCellNotEnoughError('No paymaster cell available');
}
if (!paymasterCell) {
throw new PaymasterCellNotEnoughError('No paymaster cell available');
}

const signedTx = await appendPaymasterCellAndSignCkbTx({
ckbRawTx,
sumInputsCapacity,
paymasterCell,
secp256k1PrivateKey: this.privateKey,
isMainnet: this.cradle.env.NETWORK === 'mainnet',
});
this.cradle.logger.info(`[Paymaster] Signed transaction: ${JSON.stringify(signedTx)}`);
return signedTx;
const signedTx = await appendPaymasterCellAndSignCkbTx({
ckbRawTx,
sumInputsCapacity,
paymasterCell,
secp256k1PrivateKey: this.privateKey,
isMainnet: this.cradle.env.NETWORK === 'mainnet',
});
this.cradle.logger.info(`[Paymaster] Signed transaction: ${JSON.stringify(signedTx)}`);
return signedTx;
} catch (err) {
if (err instanceof PaymasterCellNotEnoughError) {
// delay the job to retry later if the paymaster cell is not enough
throw new DelayedError();
}
throw err;
}
}

/**
Expand All @@ -208,16 +239,33 @@ export default class Paymaster implements IPaymaster {
* @param signedTx - the signed transaction to get the paymaster cell input to mark as spent
*/
public async markPaymasterCellAsSpent(token: string, signedTx: CKBComponents.RawTransaction) {
for await (const input of signedTx.inputs) {
const outPoint = input.previousOutput;
if (!outPoint) {
continue;
}
const id = `${outPoint.txHash}:${outPoint.index}`;
const job = await this.queue.getJob(id);
try {
const job = await this.getPaymasterCellJobByRawTx(signedTx);
if (job) {
await job.moveToCompleted(null, token, false);
}
} catch (err) {
this.cradle.logger.error(`[Paymaster] Mark paymaster cell as spent failed: ${token}`);
Sentry.captureException(err);
// XXX: Don't throw the error to avoid the transaction marked as failed
}
}

/**
* Mark the paymaster cell as unspent after the transaction is failed
* @param token - the job token moved from the queue to the delayed
* @param signedTx - the signed transaction to get the paymaster cell input to mark as unspent
*/
public async markPaymasterCellAsUnspent(token: string, signedTx: CKBComponents.RawTransaction) {
try {
const job = await this.getPaymasterCellJobByRawTx(signedTx);
if (job) {
await job.moveToDelayed(Date.now(), token);
}
} catch (err) {
this.cradle.logger.error(`[Paymaster] Mark paymaster cell as spent failed: ${token}`);
Sentry.captureException(err);
// XXX: Don't throw the error to avoid the transaction marked as failed
}
}
}
Loading

0 comments on commit 553ed1d

Please sign in to comment.