Skip to content

feat: New synapse costs API and improvs#632

Open
lordshashank wants to merge 2 commits intoFilOzone:masterfrom
lordshashank:feat/costs-api
Open

feat: New synapse costs API and improvs#632
lordshashank wants to merge 2 commits intoFilOzone:masterfrom
lordshashank:feat/costs-api

Conversation

@lordshashank
Copy link
Contributor

@lordshashank lordshashank commented Feb 25, 2026

Resolves #360 , #437

Complete architecture and thought process: https://hackmd.io/@lordforever/SJ6Lwv2dWx (plz read to understand better and add comments)

@github-project-automation github-project-automation bot moved this to 📌 Triage in FOC Feb 25, 2026
@rjan90 rjan90 moved this from 📌 Triage to 🔎 Awaiting review in FOC Feb 25, 2026
@rjan90 rjan90 added this to the M4.1: mainnet ready milestone Feb 25, 2026
@rjan90 rjan90 linked an issue Feb 25, 2026 that may be closed by this pull request
@lordshashank lordshashank force-pushed the feat/costs-api branch 2 times, most recently from bdd0623 to e269ba9 Compare February 26, 2026 08:18
@juliangruber juliangruber self-requested a review February 26, 2026 10:18
- **Needs deposit + approval**: calls `depositWithPermitAndApproveOperator`
- **Needs approval only**: calls `approveService`
- **Needs deposit only**: calls `depositWithPermit`
- **Already ready**: returns `transaction: null`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should exact API specs live outside of developer guides?

* @param params - Raw account fields + current epoch
* @returns Settled account info including fundedUntilEpoch
*/
export function getAccountInfoIfSettled(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect this method to return the account info if settled. There is no condition here, it always returns account info. What does "if settled" mean here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +15 to +18
/** max(0, funds - totalOwed) */
availableFunds: bigint
/** Epoch when account runs out (maxUint256 if lockupRate == 0) */
fundedUntilEpoch: bigint
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these two are out of scope for the function. The consumer can simply do this themselves:

const [debt, info] = await Promise.all([
  calculateAccountDebt(),
  getAccountInfoIfSettled()
])

I would suggest to only return the debt from this function and not return other possibly useful but already easy to get information.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that as we needed fundedUntilEpoch in for buffer calcn, etc so made it more informative, can change to different functions if everyone wants to.

@github-project-automation github-project-automation bot moved this from 🔎 Awaiting review to ⌨️ In Progress in FOC Feb 27, 2026

export namespace depositWithPermitSync {
export type OptionsType = Simplify<depositWithPermit.OptionsType & ActionSyncCallback>
export type OutputType = ActionSyncOutput<typeof extractDepositWithPermitEvent>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think <typeof extractDepositWithPermitEvent> can be inferred

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractDepositWithPermitEvent is defined below in the same file and the typeof is needed because this is a type declaration, not a value expression. wdym here?

}

if (needsApproval && options.amount === 0n) {
return setOperatorApproval(client, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code path is surprising: I would not expect a fund() method to not actually fund and only approve.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, handling the edge cases


return {
ratePerEpoch,
ratePerMonth: ratePerEpoch * TIME_CONSTANTS.EPOCHS_PER_MONTH,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a convenience field and always wrong, wdyt about never returning per-month data, always only the correct per-epoch data, and exposing an extra method for converting per-epoch to per-month? This way the developer has to opt in to the conversion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way, there's higher confidence in the return values of core methods, and it's more clear that the per-month value is rather for display or human readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#375 (comment),

Actually on chain is per month so convenience field is per epoch one, kept it to let user calculate anuthing and is more generic

* @returns Array of data set sizes in bytes {@link getMultiDatasetSize.OutputType}
* @throws Errors {@link getMultiDatasetSize.ErrorType}
*/
export async function getMultiDatasetSize(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this method actually adventageous over calling getDataSetSize multiple times in parallel? If the benefit is not obvious already, I would suggest to remove this utility method, to reduce the maintenance cost

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it reduces rpc calls for multi context cases in prepare through multicall, probably we can just keep this and remove single one and rename this to getDatasetsSize but I am ok with two functions as well let me know if this feels critical

* Default extra runway in epochs beyond the required lockup.
* 0n means no additional runway beyond the lockup period.
*/
export const DEFAULT_RUNWAY_EPOCHS = 0n
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the decision to go with 0 documented somewhere?

Comment on lines +63 to +83
if (currentDataSetSize > 0n && !isNewDataset) {
// Existing dataset: compute delta between new and current rates
const newRate = calculateEffectiveRate({
...rateParams,
sizeInBytes: currentDataSetSize + dataSize,
})
const currentRate = calculateEffectiveRate({
...rateParams,
sizeInBytes: currentDataSetSize,
})
rateDeltaPerEpoch = newRate.ratePerEpoch - currentRate.ratePerEpoch
// Floor-to-floor: if both sizes are below floor, delta is 0
if (rateDeltaPerEpoch < 0n) rateDeltaPerEpoch = 0n
} else {
// New dataset or unknown current size: full rate for new data
const newRate = calculateEffectiveRate({
...rateParams,
sizeInBytes: dataSize,
})
rateDeltaPerEpoch = newRate.ratePerEpoch
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor: less branches

Suggested change
if (currentDataSetSize > 0n && !isNewDataset) {
// Existing dataset: compute delta between new and current rates
const newRate = calculateEffectiveRate({
...rateParams,
sizeInBytes: currentDataSetSize + dataSize,
})
const currentRate = calculateEffectiveRate({
...rateParams,
sizeInBytes: currentDataSetSize,
})
rateDeltaPerEpoch = newRate.ratePerEpoch - currentRate.ratePerEpoch
// Floor-to-floor: if both sizes are below floor, delta is 0
if (rateDeltaPerEpoch < 0n) rateDeltaPerEpoch = 0n
} else {
// New dataset or unknown current size: full rate for new data
const newRate = calculateEffectiveRate({
...rateParams,
sizeInBytes: dataSize,
})
rateDeltaPerEpoch = newRate.ratePerEpoch
}
const newRate = calculateEffectiveRate({
...rateParams,
sizeInBytes: currentDataSetSize + dataSize,
})
const currentRate = calculateEffectiveRate({
...rateParams,
sizeInBytes: currentDataSetSize,
})
rateDeltaPerEpoch = newRate.ratePerEpoch - currentRate.ratePerEpoch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is wrong as for the case of new dataset it would return zero as calculateEffectiveRate would return base price for both

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ⌨️ In Progress

Development

Successfully merging this pull request may close these issues.

Add intelligent synapse.payments.fund(amount) (improve golden path) New Costs API

3 participants