Skip to content

Commit a94dde6

Browse files
committed
refactor(sdk): decompose preflight into reusable primitives
- Add PaymentsService.checkServiceReadiness() for any service - Rename calculateStorageCost() to calculateUploadCost(), expose floor pricing - Simplify prepareStorageUpload() signature (dataSize only, monthly cost only) - Remove checkAllowanceForStorage() - Add getContractAddress(), export ServiceReadinessCheck type
1 parent 51c642e commit a94dde6

File tree

9 files changed

+574
-435
lines changed

9 files changed

+574
-435
lines changed

packages/synapse-sdk/src/payments/service.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { ethers } from 'ethers'
7-
import type { RailInfo, SettlementResult, TokenAmount, TokenIdentifier } from '../types.ts'
7+
import type { RailInfo, ServiceReadinessCheck, SettlementResult, TokenAmount, TokenIdentifier } from '../types.ts'
88
import {
99
CHAIN_IDS,
1010
CONTRACT_ABIS,
@@ -602,6 +602,69 @@ export class PaymentsService {
602602
}
603603
}
604604

605+
/**
606+
* Check if the user is ready to use a service with given requirements
607+
* Performs comprehensive checks including approval status, allowances, and funds
608+
* @param service - Service contract address
609+
* @param requirements - Required rate, lockup, and lockup period
610+
* @param token - The token to check (defaults to USDFC)
611+
* @returns Readiness status with detailed checks, current state, and gaps
612+
*/
613+
async checkServiceReadiness(
614+
service: string,
615+
requirements: {
616+
rateNeeded: bigint
617+
lockupNeeded: bigint
618+
lockupPeriodNeeded: bigint
619+
},
620+
token: TokenIdentifier = TOKENS.USDFC
621+
): Promise<ServiceReadinessCheck> {
622+
// Fetch approval and account info in parallel
623+
const [approval, accountInfo] = await Promise.all([this.serviceApproval(service, token), this.accountInfo(token)])
624+
625+
// Perform all checks
626+
const checks = {
627+
isOperatorApproved: approval.rateAllowance > 0n || approval.lockupAllowance > 0n,
628+
hasSufficientFunds: accountInfo.availableFunds >= requirements.lockupNeeded,
629+
hasRateAllowance: approval.rateAllowance >= approval.rateUsed + requirements.rateNeeded,
630+
hasLockupAllowance: approval.lockupAllowance >= approval.lockupUsed + requirements.lockupNeeded,
631+
hasValidLockupPeriod: approval.maxLockupPeriod >= requirements.lockupPeriodNeeded,
632+
}
633+
634+
const sufficient = Object.values(checks).every((check) => check)
635+
636+
// Calculate gaps if not sufficient
637+
const gaps: {
638+
fundsNeeded?: bigint
639+
rateAllowanceNeeded?: bigint
640+
lockupAllowanceNeeded?: bigint
641+
lockupPeriodNeeded?: bigint
642+
} = {}
643+
644+
if (!checks.hasSufficientFunds) {
645+
gaps.fundsNeeded = requirements.lockupNeeded - accountInfo.availableFunds
646+
}
647+
if (!checks.hasRateAllowance) {
648+
gaps.rateAllowanceNeeded = approval.rateUsed + requirements.rateNeeded - approval.rateAllowance
649+
}
650+
if (!checks.hasLockupAllowance) {
651+
gaps.lockupAllowanceNeeded = approval.lockupUsed + requirements.lockupNeeded - approval.lockupAllowance
652+
}
653+
if (!checks.hasValidLockupPeriod) {
654+
gaps.lockupPeriodNeeded = requirements.lockupPeriodNeeded - approval.maxLockupPeriod
655+
}
656+
657+
return {
658+
sufficient,
659+
checks,
660+
currentState: {
661+
approval,
662+
accountInfo,
663+
},
664+
gaps: sufficient ? undefined : gaps,
665+
}
666+
}
667+
605668
async deposit(
606669
amount: TokenAmount,
607670
token: TokenIdentifier = TOKENS.USDFC,

packages/synapse-sdk/src/storage/context.ts

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import {
5454
getCurrentEpoch,
5555
METADATA_KEYS,
5656
SIZE_CONSTANTS,
57+
TIME_CONSTANTS,
58+
TOKENS,
5759
timeUntilEpoch,
5860
} from '../utils/index.ts'
5961
import { combineMetadata, metadataMatches, objectToEntries, validatePieceMetadata } from '../utils/metadata.ts'
@@ -785,34 +787,70 @@ export class StorageContext {
785787

786788
/**
787789
* Static method to perform preflight checks for an upload
788-
* @param size - The size of data to upload in bytes
789-
* @param withCDN - Whether CDN is enabled
790+
* Composes cost calculation from WarmStorageService and readiness checks from PaymentsService
790791
* @param warmStorageService - WarmStorageService instance
791792
* @param paymentsService - PaymentsService instance
793+
* @param size - The size of data to upload in bytes
792794
* @returns Preflight check results without provider/dataSet specifics
793795
*/
794796
static async performPreflightCheck(
795797
warmStorageService: WarmStorageService,
796798
paymentsService: PaymentsService,
797-
size: number,
798-
withCDN: boolean
799+
size: number
799800
): Promise<PreflightInfo> {
800801
// Validate size before proceeding
801802
StorageContext.validateRawSize(size, 'preflightUpload')
802803

803-
// Check allowances and get costs in a single call
804-
const allowanceCheck = await warmStorageService.checkAllowanceForStorage(size, withCDN, paymentsService)
804+
// Calculate upload cost
805+
const cost = await warmStorageService.calculateUploadCost(size)
805806

806-
// Return preflight info
807-
return {
808-
estimatedCost: {
809-
perEpoch: allowanceCheck.costs.perEpoch,
810-
perDay: allowanceCheck.costs.perDay,
811-
perMonth: allowanceCheck.costs.perMonth,
807+
// Calculate rate per epoch from floor-adjusted monthly price
808+
const pricing = await warmStorageService.getServicePrice()
809+
const ratePerEpoch = cost.withFloorPerMonth / pricing.epochsPerMonth
810+
811+
// Calculate lockup requirements
812+
const lockupEpochs = BigInt(TIME_CONSTANTS.DEFAULT_LOCKUP_DAYS * TIME_CONSTANTS.EPOCHS_PER_DAY)
813+
const lockupNeeded = ratePerEpoch * lockupEpochs
814+
815+
// Check payment readiness
816+
const readiness = await paymentsService.checkServiceReadiness(
817+
warmStorageService.getContractAddress(),
818+
{
819+
rateNeeded: ratePerEpoch,
820+
lockupNeeded,
821+
lockupPeriodNeeded: lockupEpochs,
812822
},
823+
TOKENS.USDFC
824+
)
825+
826+
// Build error message if not sufficient
827+
let message: string | undefined
828+
if (!readiness.sufficient) {
829+
const issues: string[] = []
830+
if (!readiness.checks.hasSufficientFunds && readiness.gaps?.fundsNeeded) {
831+
issues.push(`Insufficient funds: need ${readiness.gaps.fundsNeeded} more`)
832+
}
833+
if (!readiness.checks.isOperatorApproved) {
834+
issues.push('Operator not approved for service')
835+
}
836+
if (!readiness.checks.hasRateAllowance && readiness.gaps?.rateAllowanceNeeded) {
837+
issues.push(`Insufficient rate allowance: need ${readiness.gaps.rateAllowanceNeeded} more`)
838+
}
839+
if (!readiness.checks.hasLockupAllowance && readiness.gaps?.lockupAllowanceNeeded) {
840+
issues.push(`Insufficient lockup allowance: need ${readiness.gaps.lockupAllowanceNeeded} more`)
841+
}
842+
if (!readiness.checks.hasValidLockupPeriod && readiness.gaps?.lockupPeriodNeeded) {
843+
issues.push(`Lockup period too short: need ${readiness.gaps.lockupPeriodNeeded} more epochs`)
844+
}
845+
message = issues.join('; ')
846+
}
847+
848+
return {
849+
estimatedCostPerMonth: cost.withFloorPerMonth,
813850
allowanceCheck: {
814-
sufficient: allowanceCheck.sufficient,
815-
message: allowanceCheck.message,
851+
sufficient: readiness.sufficient,
852+
message,
853+
checks: readiness.checks,
816854
},
817855
selectedProvider: null,
818856
selectedDataSetId: null,
@@ -829,8 +867,7 @@ export class StorageContext {
829867
const preflightResult = await StorageContext.performPreflightCheck(
830868
this._warmStorageService,
831869
this._synapse.payments,
832-
size,
833-
this._withCDN
870+
size
834871
)
835872

836873
// Return preflight info with provider and dataSet specifics

packages/synapse-sdk/src/storage/manager.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,22 +213,19 @@ export class StorageManager {
213213
size: number,
214214
options?: { withCDN?: boolean; metadata?: Record<string, string> }
215215
): Promise<PreflightInfo> {
216-
// Determine withCDN from metadata if provided, otherwise use option > manager default
217-
let withCDN = options?.withCDN ?? this._withCDN
218-
219-
// Check metadata for withCDN key - this takes precedence
216+
// Note: withCDN from metadata is checked here for validation but doesn't affect base storage costs
217+
// CDN pricing is usage-based (egress charges), so base storage cost is the same regardless
220218
if (options?.metadata != null && METADATA_KEYS.WITH_CDN in options.metadata) {
221219
// The withCDN metadata entry should always have an empty string value by convention,
222220
// but the contract only checks for key presence, not value
223221
const value = options.metadata[METADATA_KEYS.WITH_CDN]
224222
if (value !== '') {
225223
console.warn(`Warning: withCDN metadata entry has unexpected value "${value}". Expected empty string.`)
226224
}
227-
withCDN = true // Enable CDN when key exists (matches contract behavior)
228225
}
229226

230227
// Use the static method from StorageContext for core logic
231-
return await StorageContext.performPreflightCheck(this._warmStorageService, this._synapse.payments, size, withCDN)
228+
return await StorageContext.performPreflightCheck(this._warmStorageService, this._synapse.payments, size)
232229
}
233230

234231
/**

0 commit comments

Comments
 (0)