diff --git a/.changeset/silent-nails-lay.md b/.changeset/silent-nails-lay.md new file mode 100644 index 00000000..deeb6046 --- /dev/null +++ b/.changeset/silent-nails-lay.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": patch +--- + +feat(core): ClientCache.revertTransaction diff --git a/packages/core/src/client/cache/cache.ts b/packages/core/src/client/cache/cache.ts index cdbdebdc..b09bfab7 100644 --- a/packages/core/src/client/cache/cache.ts +++ b/packages/core/src/client/cache/cache.ts @@ -8,23 +8,93 @@ import { import { HexLike } from "../../hex/index.js"; import { ClientCollectableSearchKeyLike } from "../clientTypes.advanced.js"; -export interface ClientCache { - markUsable(...cellLikes: (CellLike | CellLike[])[]): Promise; - markUnusable( +export abstract class ClientCache { + abstract markUsable(...cellLikes: (CellLike | CellLike[])[]): Promise; + abstract markUnusable( ...outPointLike: (OutPointLike | OutPointLike[])[] ): Promise; - markTransactions( + async markTransactions( ...transactionLike: (TransactionLike | TransactionLike[])[] - ): Promise; + ): Promise { + await Promise.all([ + this.recordTransactions(...transactionLike), + ...transactionLike.flat().map((transactionLike) => { + const tx = Transaction.from(transactionLike); + const txHash = tx.hash(); + + return Promise.all([ + ...tx.inputs.map((i) => this.markUnusable(i.previousOutput)), + ...tx.outputs.map((o, i) => + this.markUsable({ + cellOutput: o, + outputData: tx.outputsData[i], + outPoint: { + txHash, + index: i, + }, + }), + ), + ]); + }), + ]); + } + async revertTransactions( + ...transactionLike: (TransactionLike | TransactionLike[])[] + ): Promise { + await Promise.all([ + this.recordTransactions(...transactionLike), + ...transactionLike.flat().map((transactionLike) => { + const tx = Transaction.from(transactionLike); + const txHash = tx.hash(); - isUnusable(outPointLike: OutPointLike): Promise; + return Promise.all([ + ...tx.inputs.map(async (i) => { + const cell = await this.getCell(i.previousOutput); + if (cell) { + return this.markUsable(cell); + } + }), + ...tx.outputs.map((_, i) => + this.markUnusable({ + txHash, + index: i, + }), + ), + ]); + }), + ]); + } + abstract findCells( + filter: ClientCollectableSearchKeyLike, + ): AsyncGenerator; + /** + * Get a known cell by out point + * @param _outPoint + */ + abstract getCell(_outPoint: OutPointLike): Promise; + abstract isUnusable(outPointLike: OutPointLike): Promise; - recordTransactions( - ...transactions: (TransactionLike | TransactionLike[])[] - ): Promise; - getTransaction(txHash: HexLike): Promise; + /** + * Record known transactions + * Implement this method to enable transactions query caching + * @param _transactions + */ + async recordTransactions( + ..._transactions: (TransactionLike | TransactionLike[])[] + ): Promise {} + /** + * Get a known transaction by hash + * Implement this method to enable transactions query caching + * @param _txHash + */ + async getTransaction(_txHash: HexLike): Promise { + return; + } - recordCells(...cells: (CellLike | CellLike[])[]): Promise; - getCell(outPoint: OutPointLike): Promise; - findCells(filter: ClientCollectableSearchKeyLike): AsyncGenerator; + /** + * Record known cells + * Implement this method to enable cells query caching + * @param _cells + */ + async recordCells(..._cells: (CellLike | CellLike[])[]): Promise {} } diff --git a/packages/core/src/client/cache/memory.ts b/packages/core/src/client/cache/memory.ts index ee593040..de74fc96 100644 --- a/packages/core/src/client/cache/memory.ts +++ b/packages/core/src/client/cache/memory.ts @@ -11,24 +11,32 @@ import { ClientCollectableSearchKeyLike } from "../clientTypes.advanced.js"; import { ClientCache } from "./cache.js"; import { filterCell } from "./memory.advanced.js"; -export class ClientCacheMemory implements ClientCache { - private readonly unusableOutPoints: OutPoint[] = []; - private readonly usableCells: Cell[] = []; - private readonly knownTransactions: Transaction[] = []; - private readonly knownCells: Cell[] = []; +export class ClientCacheMemory extends ClientCache { + /** + * OutPoint => [isLive, Cell | OutPoint] + */ + private readonly cells: Map< + string, + | [ + false, + Pick & + Partial>, + ] + | [true, Cell] + | [undefined, Cell] + > = new Map(); + + /** + * TX Hash => Transaction + */ + private readonly knownTransactions: Map = new Map(); async markUsable(...cellLikes: (CellLike | CellLike[])[]): Promise { cellLikes.flat().forEach((cellLike) => { const cell = Cell.from(cellLike).clone(); - this.usableCells.push(cell); - this.knownCells.push(cell); + const outPointStr = hexFrom(cell.outPoint.toBytes()); - const index = this.unusableOutPoints.findIndex((o) => - cell.outPoint.eq(o), - ); - if (index !== -1) { - this.unusableOutPoints.splice(index, 1); - } + this.cells.set(outPointStr, [true, cell]); }); } @@ -37,74 +45,68 @@ export class ClientCacheMemory implements ClientCache { ): Promise { outPointLikes.flat().forEach((outPointLike) => { const outPoint = OutPoint.from(outPointLike); - this.unusableOutPoints.push(outPoint.clone()); + const outPointStr = hexFrom(outPoint.toBytes()); - const index = this.usableCells.findIndex((c) => c.outPoint.eq(outPoint)); - if (index !== -1) { - this.usableCells.splice(index, 1); + const existed = this.cells.get(outPointStr); + if (existed) { + existed[0] = false; + return; } + this.cells.set(outPointStr, [false, { outPoint }]); }); } - async markTransactions( - ...transactionLike: (TransactionLike | TransactionLike[])[] - ): Promise { - await Promise.all( - transactionLike.flat().map(async (transactionLike) => { - const tx = Transaction.from(transactionLike); - const txHash = tx.hash(); - - await Promise.all( - tx.inputs.map((i) => this.markUnusable(i.previousOutput)), - ); - await Promise.all( - tx.outputs.map((o, i) => - this.markUsable({ - cellOutput: o, - outputData: tx.outputsData[i], - outPoint: { - txHash, - index: i, - }, - }), - ), - ); - }), - ); - } - async *findCells( keyLike: ClientCollectableSearchKeyLike, ): AsyncGenerator { - for (const cell of this.usableCells) { + for (const [isLive, cell] of this.cells.values()) { + if (!isLive) { + continue; + } if (!filterCell(keyLike, cell)) { continue; } - yield cell; + yield cell.clone(); + } + } + async getCell(outPointLike: OutPointLike): Promise { + const outPoint = OutPoint.from(outPointLike); + + const cell = this.cells.get(hexFrom(outPoint.toBytes()))?.[1]; + if (cell && cell.cellOutput && cell.outputData) { + return Cell.from((cell as Cell).clone()); } } async isUnusable(outPointLike: OutPointLike): Promise { const outPoint = OutPoint.from(outPointLike); - return this.unusableOutPoints.find((o) => o.eq(outPoint)) !== undefined; + + return !(this.cells.get(hexFrom(outPoint.toBytes()))?.[0] ?? true); } async recordTransactions( ...transactions: (TransactionLike | TransactionLike[])[] ): Promise { - this.knownTransactions.push(...transactions.flat().map(Transaction.from)); + transactions.flat().map((txLike) => { + const tx = Transaction.from(txLike); + this.knownTransactions.set(tx.hash(), tx); + }); } async getTransaction(txHashLike: HexLike): Promise { const txHash = hexFrom(txHashLike); - return this.knownTransactions.find((tx) => tx.hash() === txHash); + return this.knownTransactions.get(txHash)?.clone(); } async recordCells(...cells: (CellLike | CellLike[])[]): Promise { - this.knownCells.push(...cells.flat().map(Cell.from)); - } - async getCell(outPointLike: OutPointLike): Promise { - const outPoint = OutPoint.from(outPointLike); - return this.knownCells.find((cell) => cell.outPoint.eq(outPoint)); + cells.flat().map((cellLike) => { + const cell = Cell.from(cellLike); + const outPointStr = hexFrom(cell.outPoint.toBytes()); + + if (this.cells.get(outPointStr)) { + return; + } + this.cells.set(outPointStr, [undefined, cell]); + }); } }