-
Notifications
You must be signed in to change notification settings - Fork 31
feat: add margin feature to enable zero-transfer fee #241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat: add margin feature to enable zero-transfer fee #241
Conversation
🦋 Changeset detectedLatest commit: dacf39e The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for apiccc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for liveccc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for docsccc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for appccc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary of Changes
Hello @ashuralyk, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
I've implemented a significant feature to enable zero-transfer fees within the Spore SDK. This involved introducing a 'margin' concept to cell outputs, allowing them to carry excess capacity that can be consumed to cover transaction fees. The core transaction completion logic has been updated to prioritize using this margin, providing a more flexible and efficient way to handle fees without requiring additional inputs or change outputs. This change streamlines the transaction process, particularly for Spore transfers, by making it possible to execute them without explicit fee deductions from the sender's primary balance.
Highlights
- Zero-Transfer Fee Mechanism: I've introduced a new 'margin' concept to
CellOutput
objects, allowing them to hold extra capacity beyond their minimum occupied size. This margin can then be used to cover transaction fees. - Capacity Margin Management: I've added a
margin
method to theCellOutput
class and newgetOutputCapacityMargin
andgetOutputsCapacityMargin
methods to theTransaction
class. These enable the calculation and retrieval of available margin capacity within transaction outputs. - Flexible Fee Payment: The
completeFee
and related methods in theTransaction
class now accept apayFeeFromMargin
option. When enabled, the transaction will attempt to pay fees by reducing the margin of specified outputs, eliminating the need for additional inputs or change outputs. - Environment Variable Refactoring: I've refactored environment variable handling by removing
env.example.js
and introducing.example.env
andpackages/spore/.env.js
. Thedotenv
package has been integrated to load these variables, improving configuration management. - New Test Coverage: A new test case,
zeroFeeTransfer.test.ts
, has been added to thoroughly test and demonstrate the new zero-transfer fee functionality, ensuring its correct behavior.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command>
or @gemini-code-assist <command>
. Below is a summary of the supported commands.
Feature | Command | Description |
---|---|---|
Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/
folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments or fill out our survey to provide feedback.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a valuable feature for enabling zero-transfer fees by utilizing capacity margins in output cells. The overall approach is sound, but I've identified two critical bugs in the implementation of margin calculation and fee payment that could lead to invalid transactions. Additionally, I've suggested a refactoring for one of the new methods to enhance its readability and maintainability. Addressing these issues will ensure the feature is robust and reliable.
packages/core/src/ckb/transaction.ts
Outdated
} | ||
|
||
margin(dataLen: NumLike = 0): Num { | ||
return this.capacity - fixedPointFrom(this.occupiedSize) + numFrom(dataLen); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The calculation for the cell's margin appears to be incorrect. The capacity cost of the data (dataLen
) should be subtracted from the total capacity, but it is currently being added. This will result in an incorrect margin calculation, which could cause issues with the fee payment logic.
return this.capacity - fixedPointFrom(this.occupiedSize) + numFrom(dataLen); | |
return this.capacity - fixedPointFrom(this.occupiedSize) - numFrom(dataLen); |
packages/core/src/ckb/transaction.ts
Outdated
break; | ||
} else { | ||
leastExtraCapacity -= margin; | ||
output.capacity = fixedPointFrom(output.occupiedSize); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When consuming the entire margin of an output to pay for fees, the new capacity is set to fixedPointFrom(output.occupiedSize)
. This is incorrect as it doesn't account for the capacity required for the cell's data, which will lead to an invalid transaction if the cell contains data. The capacity should be set to the minimum required for both the structure and its data.
output.capacity = fixedPointFrom(
output.occupiedSize + bytesFrom(tx.outputsData[index] ?? "0x").length,
);
packages/core/src/ckb/transaction.ts
Outdated
getOutputsCapacityMargin( | ||
filter: | ||
| Array<Omit<ScriptLike, "args"> & { args?: HexLike }> | ||
| Array<number> = [], | ||
): { | ||
marginCapacity: Num; | ||
collectedOutputIndices: Array<number>; | ||
} { | ||
let marginCapacity: Num = numFrom(0); | ||
const collectedOutputIndices: Array<number> = []; | ||
|
||
const marginOutputs = this.outputs | ||
.map((output, i) => { | ||
return { | ||
script: output.type, | ||
margin: this.getOutputCapacityMargin(i), | ||
index: i, | ||
}; | ||
}) | ||
.filter((output) => output.margin > 0); | ||
|
||
if (filter.length === 0) { | ||
marginCapacity = marginOutputs.reduce((acc, output) => { | ||
collectedOutputIndices.push(output.index); | ||
return acc + output.margin; | ||
}, numFrom(0)); | ||
} else if (typeof filter[0] === "number") { | ||
// filter is array of output indices | ||
marginCapacity = marginOutputs.reduce((acc, output) => { | ||
if ((filter as Array<number>).includes(output.index)) { | ||
collectedOutputIndices.push(output.index); | ||
return acc + output.margin; | ||
} | ||
return acc; | ||
}, numFrom(0)); | ||
} else { | ||
// filter is array of ScriptLike | ||
marginCapacity = marginOutputs.reduce((acc, output) => { | ||
if ( | ||
(filter as Array<ScriptLike>).some( | ||
(script) => | ||
hexFrom(script.codeHash) === output.script?.codeHash && | ||
hashTypeFrom(script.hashType) === output.script?.hashType && | ||
(script.args === undefined || | ||
output.script?.args.startsWith(hexFrom(script.args))), | ||
) | ||
) { | ||
collectedOutputIndices.push(output.index); | ||
return acc + output.margin; | ||
} | ||
return acc; | ||
}, numFrom(0)); | ||
} | ||
|
||
return { | ||
marginCapacity, | ||
collectedOutputIndices, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getOutputsCapacityMargin
method has a lot of duplicated code across the different filter conditions. This can be refactored to be more concise and maintainable by using a single loop and a predicate function to handle the filtering logic.
getOutputsCapacityMargin(
filter:
| Array<Omit<ScriptLike, "args"> & { args?: HexLike }>
| Array<number> = [],
): {
marginCapacity: Num;
collectedOutputIndices: Array<number>;
} {
const marginOutputs = this.outputs
.map((output, i) => ({
script: output.type,
margin: this.getOutputCapacityMargin(i),
index: i,
}))
.filter((output) => output.margin > 0);
let predicate: (output: (typeof marginOutputs)[0]) => boolean;
if (filter.length === 0) {
predicate = () => true;
} else if (typeof filter[0] === "number") {
const indices = new Set(filter as number[]);
predicate = (output) => indices.has(output.index);
} else {
const scripts = filter as Array<ScriptLike>;
predicate = (output) =>
scripts.some(
(script) =>
hexFrom(script.codeHash) === output.script?.codeHash &&
hashTypeFrom(script.hashType) === output.script?.hashType &&
(script.args === undefined ||
output.script?.args.startsWith(hexFrom(script.args))),
);
}
let marginCapacity: Num = numFrom(0);
const collectedOutputIndices: Array<number> = [];
for (const output of marginOutputs) {
if (predicate(output)) {
marginCapacity += output.margin;
collectedOutputIndices.push(output.index);
}
}
return {
marginCapacity,
collectedOutputIndices,
};
}
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a 'margin' feature to enable zero-fee transfers for Spores. The changes are well-implemented across the core transaction logic and the Spore package. The core logic in transaction.ts
is updated to handle margin capacity in outputs and to pay fees from this margin. The Spore-specific logic correctly utilizes this new core functionality. I've included a few suggestions to improve code clarity, error handling, and testing practices.
packages/core/src/ckb/transaction.ts
Outdated
const collectedOutputIndices: Array<number> = []; | ||
const marginCapacity = marginOutputs.reduce((acc, output) => { | ||
if (predicate(output)) { | ||
collectedOutputIndices.push(output.index); | ||
return acc + output.margin; | ||
} | ||
return acc; | ||
}, numFrom(0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reduce
function here has a side effect of populating collectedOutputIndices
. This can make the code harder to understand and maintain. It would be clearer to first filter the outputs and then use reduce
only for summation.
const collectedOutputIndices: Array<number> = []; | |
const marginCapacity = marginOutputs.reduce((acc, output) => { | |
if (predicate(output)) { | |
collectedOutputIndices.push(output.index); | |
return acc + output.margin; | |
} | |
return acc; | |
}, numFrom(0)); | |
const collectedOutputs = marginOutputs.filter(predicate); | |
const collectedOutputIndices = collectedOutputs.map( | |
(output) => output.index, | |
); | |
const marginCapacity = collectedOutputs.reduce( | |
(acc, output) => acc + output.margin, | |
numFrom(0), | |
); |
packages/core/src/ckb/transaction.ts
Outdated
throw new Error( | ||
`Insufficient margin capacity to pay fee, need ${leastExtraCapacity}, actual ${margins.marginCapacity}`, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throwing a generic Error
makes it difficult for callers to programmatically handle this specific error case. Consider creating and throwing a custom error class, for example ErrorTransactionInsufficientMarginCapacity
, similar to how ErrorTransactionInsufficientCapacity
is used elsewhere in this file. This would improve error handling and make the API more robust.
console.log(JSON.stringify(JsonRpcTransformers.transactionFrom(tx))); | ||
|
||
// Send transaction | ||
const txHash = await signer.client.sendTransaction(tx); | ||
console.log(txHash); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test uses console.log
to output results. While this is useful for debugging, in automated tests, it's better to use assertions to verify the expected behavior. For example, you could assert that txHash
is a valid hex string of the correct length. This makes the test more robust and self-verifying.
39ea5f6
to
e103def
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think injecting this logic into Transaction.complete
is a good idea. This makes things over-complicated. Do we have other ways to implement the pay fee by margin feature?
df9b96a
to
6d25c00
Compare
/gemini review |
It looks like you've used |
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a 'margin' feature to enable zero-fee transfers for Spores. The changes include adding logic to createSpore
to set a capacity margin and to transferSpore
to use this margin to pay for transaction fees. New helper methods are added to the core Transaction
class to support this. The changes are well-structured and include a new test case. However, I've found a few issues in the implementation logic that could prevent the feature from working as intended, including a potential crash, a logic error in how the margin is applied, and an incorrect fee rate check. I've also spotted a minor code redundancy.
addOutput( | ||
cellOrOutputLike: CellAnyLike | CellOutputLike, | ||
outputDataLike?: HexLike | null, | ||
): number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
solve gemini high priority issues Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Description
Current spore sdk implementation didn't achieve a zero-transfer-fee feature when transfering spore, this feature should come up with a little change on low-level transaction fee completion logic