Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/orange-cloths-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ckb-ccc/core": patch
---

Change a little low-level logic in fee completion from a transaction

6 changes: 6 additions & 0 deletions .changeset/tall-clocks-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ckb-ccc/spore": patch
---

Add margin option in createSpore for zero-transfer-fee feature

1 change: 1 addition & 0 deletions .example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
2 changes: 0 additions & 2 deletions env.example.js

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@changesets/cli": "^2.29.6",
"@types/jest": "^29.5.14",
"@vitest/coverage-v8": "3.2.2",
"dotenv": "^16.4.5",
"jest": "30.0.0-alpha.6",
"ts-jest": "^29.4.1",
"typedoc": "^0.26.11",
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/ckb/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ export class CellOutput extends mol.Entity.Base<CellOutputLike, CellOutput>() {
clone(): CellOutput {
return new CellOutput(this.capacity, this.lock.clone(), this.type?.clone());
}

margin(dataLen: NumLike = 0): Num {
return this.capacity - fixedPointFrom(this.occupiedSize) - numFrom(dataLen);
}
}
export const CellOutputVec = mol.vector(CellOutput);

Expand Down Expand Up @@ -1776,6 +1780,10 @@ export class Transaction extends mol.Entity.Base<
outputLike: CellOutputLike,
outputDataLike?: HexLike | null,
): number;
addOutput(
cellOrOutputLike: CellAnyLike | CellOutputLike,
outputDataLike?: HexLike | null,
): number;
addOutput(
cellOrOutputLike: CellAnyLike | CellOutputLike,
outputDataLike?: HexLike | null,
Expand Down Expand Up @@ -1947,6 +1955,14 @@ export class Transaction extends mol.Entity.Base<
}, numFrom(0));
}

getOutputCapacityMargin(index: number): Num {
const output = this.outputs[index];
if (output === undefined) {
return Zero;
}
return output.margin(bytesFrom(this.outputsData[index] ?? "0x").length);
}

async completeInputs<T>(
from: Signer,
filter: ClientCollectableSearchKeyFilterLike,
Expand Down
45 changes: 45 additions & 0 deletions packages/spore/src/__examples__/zeroFeeTransfer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ccc } from "@ckb-ccc/core";
import { JsonRpcTransformers } from "@ckb-ccc/core/advanced";
import { describe, expect, it } from "vitest";
import { transferSpore } from "../index.js";

describe("transferSpore [testnet]", () => {
expect(process.env.PRIVATE_KEY).toBeDefined();

it("should transfer a Spore cell by sporeId", async () => {
const client = new ccc.ClientPublicTestnet();
const signer = new ccc.SignerCkbPrivateKey(
client,
process.env.PRIVATE_KEY!,
);

// Create a new owner
const owner = await ccc.Address.fromString(
"ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqv5puz2ee96nuh9nmc6rtm0n8v7agju4rgdmxlnk",
signer.client,
);

// Build transaction
let { tx, zeroFeeApplied } = await transferSpore({
signer,
// Change this if you have a different sporeId
id: "0xe1b98f485de4a7cec6161a15a3ae1fc06a9d7170df04d27ba453023254b2c5e3",
to: owner.script,
zeroTransferFeeRate: 1000,
});

// Complete transaction if zero fee is not applied
if (!zeroFeeApplied) {
await tx.completeFeeBy(signer);
console.log("zero-transfer-fee is not applied, complete fee by signer");
} else {
console.log("zero-transfer-fee is applied, skip complete fee");
}
tx = await signer.signTransaction(tx);
console.log(JSON.stringify(JsonRpcTransformers.transactionFrom(tx)));

// Send transaction
const txHash = await signer.client.sendTransaction(tx);
console.log(txHash);
}, 60000);
});
2 changes: 2 additions & 0 deletions packages/spore/src/helper/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ccc } from "@ckb-ccc/core";
import { SporeScriptInfo, SporeScriptInfoLike } from "../predefined/index.js";

export const ONE_CKB = ccc.numFrom(10 ** 8);

export async function findSingletonCellByArgs(
client: ccc.Client,
args: ccc.HexLike,
Expand Down
63 changes: 56 additions & 7 deletions packages/spore/src/spore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
packRawSporeData,
unpackToRawSporeData,
} from "../codec/index.js";
import { findSingletonCellByArgs } from "../helper/index.js";
import { ONE_CKB, findSingletonCellByArgs } from "../helper/index.js";
import {
SporeScriptInfo,
SporeScriptInfoLike,
Expand Down Expand Up @@ -80,6 +80,7 @@ export async function assertSpore(
* @param params.tx the transaction skeleton, if not provided, a new one will be created
* @param params.scriptInfo the script info of Spore cell, if not provided, the default script info will be used
* @param params.scriptInfoHash the script info hash used in cobuild
* @param params.marginCapacity the extra capacity added into spore cell to enable the zero-transfer fee feature, default sets to 1 CKB
* @returns
* - **tx**: a new transaction that contains created Spore cells
* - **id**: the sporeId of created Spore cell
Expand All @@ -92,11 +93,13 @@ export async function createSpore(params: {
tx?: ccc.TransactionLike;
scriptInfo?: SporeScriptInfoLike;
scriptInfoHash?: ccc.HexLike;
marginCapacity?: ccc.NumLike;
}): Promise<{
tx: ccc.Transaction;
id: ccc.Hex;
}> {
const { signer, data, to, clusterMode, scriptInfoHash } = params;
const { signer, data, to, clusterMode, scriptInfoHash, marginCapacity } =
params;
const scriptInfo = params.scriptInfo ?? getSporeScriptInfo(signer.client);

// prepare transaction
Expand All @@ -112,7 +115,7 @@ export async function createSpore(params: {
ids.push(id);

const packedData = packRawSporeData(data);
tx.addOutput(
const outputLen = tx.addOutput(
{
lock: to ?? lock,
type: {
Expand All @@ -123,6 +126,14 @@ export async function createSpore(params: {
packedData,
);

// Add margin capacity if specified
if (marginCapacity) {
const margin = ccc.numFrom(marginCapacity);
tx.outputs[outputLen - 1].capacity += margin;
} else {
tx.outputs[outputLen - 1].capacity += ONE_CKB;
}

// create spore action
if (scriptInfo.cobuild) {
const output = tx.outputs[tx.outputs.length - 1];
Expand Down Expand Up @@ -156,8 +167,10 @@ export async function createSpore(params: {
* @param params.to Spore's new owner
* @param params.tx the transaction skeleton, if not provided, a new one will be created
* @param params.scriptInfoHash the script info hash used in cobuild
* @param params.zeroTransferFeeRate a fee rate to calculate the fee that paid by by margin from last spore cell
* @returns
* - **tx**: a new transaction that contains transferred Spore cells
* - **zeroFeeApplied**: whether the zero transfer fee is applied
*/
export async function transferSpore(params: {
signer: ccc.Signer;
Expand All @@ -166,19 +179,33 @@ export async function transferSpore(params: {
tx?: ccc.TransactionLike;
scripts?: SporeScriptInfoLike[];
scriptInfoHash?: ccc.HexLike;
zeroTransferFeeRate?: ccc.NumLike;
}): Promise<{
tx: ccc.Transaction;
zeroFeeApplied?: boolean;
}> {
const { signer, id, to, scripts, scriptInfoHash } = params;
const { signer, id, to, scripts, scriptInfoHash, zeroTransferFeeRate } =
params;

// prepare transaction
const tx = ccc.Transaction.from(params.tx ?? {});
let tx = ccc.Transaction.from(params.tx ?? {});

const { cell: sporeCell, scriptInfo: sporeScriptInfo } = await assertSpore(
signer.client,
id,
scripts,
);
const signerAddress = await signer.getRecommendedAddressObj();
if (!sporeCell.cellOutput.lock.eq(signerAddress.script)) {
const sporeOwner = ccc.Address.fromScript(
sporeCell.cellOutput.lock,
signer.client,
);
throw new Error(
`Spore cell's owner is not the same as signer's address, spore owner: ${sporeOwner.toString()}, signer: ${signerAddress.toString()}`,
);
}

await tx.addCellDepInfos(signer.client, sporeScriptInfo.cellDeps);
tx.addInput(sporeCell);
tx.addOutput(
Expand All @@ -189,18 +216,40 @@ export async function transferSpore(params: {
sporeCell.outputData,
);

// adjust capacity to cover previous margin
const outputIndex = tx.outputs.length - 1;
if (tx.outputs[outputIndex].capacity < sporeCell.cellOutput.capacity) {
tx.outputs[outputIndex].capacity = sporeCell.cellOutput.capacity;
}

const actions = sporeScriptInfo.cobuild
? [
assembleTransferSporeAction(
sporeCell.cellOutput,
tx.outputs[tx.outputs.length - 1],
tx.outputs[outputIndex],
scriptInfoHash,
),
]
: [];

tx = await prepareSporeTransaction(signer, tx, actions);

let zeroFeeApplied = false;
if (zeroTransferFeeRate !== undefined) {
const minimalFeeRate = await signer.client.getFeeRate();
if (minimalFeeRate <= ccc.numFrom(zeroTransferFeeRate)) {
const fee = tx.estimateFee(zeroTransferFeeRate);
const margin = tx.getOutputCapacityMargin(outputIndex);
if (margin >= fee) {
tx.outputs[outputIndex].capacity -= fee;
zeroFeeApplied = true;
}
}
}

return {
tx: await prepareSporeTransaction(signer, tx, actions),
tx,
zeroFeeApplied,
};
}

Expand Down
Loading