Skip to content

Commit 46f1760

Browse files
committed
feat(core): try to avoid extra udt occupation
1 parent be30a2a commit 46f1760

3 files changed

Lines changed: 342 additions & 11 deletions

File tree

.changeset/olive-ears-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ckb-ccc/core": minor
3+
---
4+
5+
feat(core): try to avoid extra udt occupation
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { ccc } from "../index.js";
3+
4+
let client: ccc.Client;
5+
let signer: ccc.Signer;
6+
let lock: ccc.Script;
7+
8+
let type: ccc.Script;
9+
10+
beforeEach(async () => {
11+
client = new ccc.ClientPublicTestnet();
12+
signer = new ccc.SignerCkbPublicKey(
13+
client,
14+
"0x026f3255791f578cc5e38783b6f2d87d4709697b797def6bf7b3b9af4120e2bfd9",
15+
);
16+
lock = (await signer.getRecommendedAddressObj()).script;
17+
18+
type = await ccc.Script.fromKnownScript(
19+
client,
20+
ccc.KnownScript.XUdt,
21+
"0xf8f94a13dfe1b87c10312fb9678ab5276eefbe1e0b2c62b4841b1f393494eff2",
22+
);
23+
});
24+
25+
describe("Transaction", () => {
26+
describe("completeInputsByUdt", () => {
27+
// Mock cells with 100 UDT each (10 cells total = 1000 UDT)
28+
let mockUdtCells: ccc.Cell[];
29+
30+
beforeEach(async () => {
31+
// Create mock cells after type is initialized
32+
mockUdtCells = Array.from({ length: 10 }, (_, i) =>
33+
ccc.Cell.from({
34+
outPoint: {
35+
txHash: `0x${"0".repeat(63)}${i.toString(16)}`,
36+
index: 0,
37+
},
38+
cellOutput: {
39+
capacity: ccc.fixedPointFrom(142),
40+
lock,
41+
type,
42+
},
43+
outputData: ccc.numLeToBytes(100, 16), // 100 UDT tokens
44+
}),
45+
);
46+
});
47+
48+
beforeEach(() => {
49+
// Mock the findCells method to return our mock UDT cells
50+
vi.spyOn(signer, "findCells").mockImplementation(
51+
async function* (filter) {
52+
if (filter.script && ccc.Script.from(filter.script).eq(type)) {
53+
for (const cell of mockUdtCells) {
54+
yield cell;
55+
}
56+
}
57+
},
58+
);
59+
60+
// Mock client.getCell to return the cell data for inputs
61+
vi.spyOn(client, "getCell").mockImplementation(async (outPoint) => {
62+
const cell = mockUdtCells.find((c) => c.outPoint.eq(outPoint));
63+
return cell;
64+
});
65+
});
66+
67+
it("should return 0 when no UDT balance is needed", async () => {
68+
const tx = ccc.Transaction.from({
69+
outputs: [],
70+
});
71+
72+
const addedCount = await tx.completeInputsByUdt(signer, type);
73+
expect(addedCount).toBe(0);
74+
});
75+
76+
it("should collect exactly the required UDT balance", async () => {
77+
const tx = ccc.Transaction.from({
78+
outputs: [
79+
{
80+
lock,
81+
type,
82+
},
83+
],
84+
outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT
85+
});
86+
87+
const addedCount = await tx.completeInputsByUdt(signer, type);
88+
89+
// Should add 2 cells (200 UDT total) to have at least 2 inputs
90+
expect(addedCount).toBe(2);
91+
expect(tx.inputs.length).toBe(2);
92+
93+
// Verify the inputs are UDT cells
94+
const inputBalance = await tx.getInputsUdtBalance(client, type);
95+
expect(inputBalance).toBe(ccc.numFrom(200));
96+
});
97+
98+
it("should collect exactly one cell when amount matches exactly", async () => {
99+
const tx = ccc.Transaction.from({
100+
outputs: [
101+
{
102+
lock,
103+
type,
104+
},
105+
],
106+
outputsData: [ccc.numLeToBytes(100, 16)], // Need exactly 100 UDT
107+
});
108+
109+
const addedCount = await tx.completeInputsByUdt(signer, type);
110+
111+
// Should add only 1 cell since it matches exactly
112+
expect(addedCount).toBe(1);
113+
expect(tx.inputs.length).toBe(1);
114+
115+
const inputBalance = await tx.getInputsUdtBalance(client, type);
116+
expect(inputBalance).toBe(ccc.numFrom(100));
117+
});
118+
119+
it("should handle balanceTweak parameter", async () => {
120+
const tx = ccc.Transaction.from({
121+
outputs: [
122+
{
123+
lock,
124+
type,
125+
},
126+
],
127+
outputsData: [ccc.numLeToBytes(100, 16)], // Need 100 UDT
128+
});
129+
130+
// Add 50 extra UDT requirement via balanceTweak
131+
const addedCount = await tx.completeInputsByUdt(signer, type, 50);
132+
133+
// Should add 2 cells to cover 150 UDT total requirement
134+
expect(addedCount).toBe(2);
135+
expect(tx.inputs.length).toBe(2);
136+
137+
const inputBalance = await tx.getInputsUdtBalance(client, type);
138+
expect(inputBalance).toBe(ccc.numFrom(200));
139+
});
140+
141+
it("should return 0 when existing inputs already satisfy the requirement", async () => {
142+
const tx = ccc.Transaction.from({
143+
inputs: [
144+
{
145+
previousOutput: mockUdtCells[0].outPoint,
146+
},
147+
{
148+
previousOutput: mockUdtCells[1].outPoint,
149+
},
150+
],
151+
outputs: [
152+
{
153+
lock,
154+
type,
155+
},
156+
],
157+
outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT, already have 200
158+
});
159+
160+
const addedCount = await tx.completeInputsByUdt(signer, type);
161+
162+
// Should not add any inputs since we already have enough
163+
expect(addedCount).toBe(0);
164+
expect(tx.inputs.length).toBe(2);
165+
});
166+
167+
it("should throw error when insufficient UDT balance available", async () => {
168+
const tx = ccc.Transaction.from({
169+
outputs: [
170+
{
171+
lock,
172+
type,
173+
},
174+
],
175+
outputsData: [ccc.numLeToBytes(1500, 16)], // Need 1500 UDT, only have 1000 available
176+
});
177+
178+
await expect(tx.completeInputsByUdt(signer, type)).rejects.toThrow(
179+
"Insufficient coin, need 500 extra coin",
180+
);
181+
});
182+
183+
it("should handle multiple UDT outputs correctly", async () => {
184+
const tx = ccc.Transaction.from({
185+
outputs: [
186+
{
187+
lock,
188+
type,
189+
},
190+
{
191+
lock,
192+
type,
193+
},
194+
],
195+
outputsData: [
196+
ccc.numLeToBytes(100, 16), // First output: 100 UDT
197+
ccc.numLeToBytes(150, 16), // Second output: 150 UDT
198+
], // Total: 250 UDT needed
199+
});
200+
201+
const addedCount = await tx.completeInputsByUdt(signer, type);
202+
203+
// Should add 3 cells to cover 250 UDT requirement (300 UDT total)
204+
expect(addedCount).toBe(3);
205+
expect(tx.inputs.length).toBe(3);
206+
207+
const inputBalance = await tx.getInputsUdtBalance(client, type);
208+
expect(inputBalance).toBe(ccc.numFrom(300));
209+
210+
const outputBalance = tx.getOutputsUdtBalance(type);
211+
expect(outputBalance).toBe(ccc.numFrom(250));
212+
});
213+
214+
it("should skip cells that are already used as inputs", async () => {
215+
// Pre-add one of the mock cells as input
216+
const tx = ccc.Transaction.from({
217+
inputs: [
218+
{
219+
previousOutput: mockUdtCells[0].outPoint,
220+
},
221+
],
222+
outputs: [
223+
{
224+
lock,
225+
type,
226+
},
227+
],
228+
outputsData: [ccc.numLeToBytes(150, 16)], // Need 150 UDT, already have 100
229+
});
230+
231+
const addedCount = await tx.completeInputsByUdt(signer, type);
232+
233+
// Should add 1 more cell (since we already have 1 input with 100 UDT)
234+
expect(addedCount).toBe(1);
235+
expect(tx.inputs.length).toBe(2);
236+
237+
const inputBalance = await tx.getInputsUdtBalance(client, type);
238+
expect(inputBalance).toBe(ccc.numFrom(200));
239+
});
240+
241+
it("should add two cells when user has multiple cells but only needs one to avoid change fees", async () => {
242+
const tx = ccc.Transaction.from({
243+
outputs: [
244+
{
245+
lock,
246+
type,
247+
},
248+
],
249+
outputsData: [ccc.numLeToBytes(50, 16)], // Need only 50 UDT (less than one cell)
250+
});
251+
252+
const addedCount = await tx.completeInputsByUdt(signer, type);
253+
254+
// Should add 2 cells even though 1 cell (100 UDT) would be enough
255+
// This avoids the need for a change cell
256+
expect(addedCount).toBe(2);
257+
expect(tx.inputs.length).toBe(2);
258+
259+
const inputBalance = await tx.getInputsUdtBalance(client, type);
260+
expect(inputBalance).toBe(ccc.numFrom(200));
261+
});
262+
263+
it("should use only one cell when user has only one cell available", async () => {
264+
// Mock signer to return only one cell
265+
vi.spyOn(signer, "findCells").mockImplementation(
266+
async function* (filter) {
267+
if (filter.script && ccc.Script.from(filter.script).eq(type)) {
268+
yield mockUdtCells[0]; // Only yield the first cell
269+
}
270+
},
271+
);
272+
273+
const tx = ccc.Transaction.from({
274+
outputs: [
275+
{
276+
lock,
277+
type,
278+
},
279+
],
280+
outputsData: [ccc.numLeToBytes(50, 16)], // Need only 50 UDT
281+
});
282+
283+
const addedCount = await tx.completeInputsByUdt(signer, type);
284+
285+
// Should use only 1 cell since that's all that's available
286+
expect(addedCount).toBe(1);
287+
expect(tx.inputs.length).toBe(1);
288+
289+
const inputBalance = await tx.getInputsUdtBalance(client, type);
290+
expect(inputBalance).toBe(ccc.numFrom(100));
291+
});
292+
});
293+
});

0 commit comments

Comments
 (0)