Skip to content

Commit d6bde3d

Browse files
committed
wip
1 parent 0a93dbf commit d6bde3d

1 file changed

Lines changed: 167 additions & 133 deletions

File tree

src/acpJobOffering.ts

Lines changed: 167 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "./configs/acpConfigs";
1717
import { USDC_TOKEN_ADDRESS } from "./constants";
1818
import { AcpAccount } from "./acpAccount";
19-
import { ISubscriptionCheckResponse } from "./interfaces";
19+
import { IAcpAccount, ISubscriptionCheckResponse } from "./interfaces";
2020

2121
export enum PriceType {
2222
FIXED = "fixed",
@@ -45,6 +45,48 @@ class AcpJobOffering {
4545
expiredAt: Date = new Date(Date.now() + 1000 * 60 * 60 * 24), // default: 1 day
4646
preferredSubscriptionTier?: string,
4747
) {
48+
this.validateRequest(serviceRequirement);
49+
50+
const subscriptionRequired = this.isSubscriptionRequired(preferredSubscriptionTier);
51+
this.validateSubscriptionTier(preferredSubscriptionTier);
52+
53+
const effectivePrice = subscriptionRequired ? 0 : this.price;
54+
const effectivePriceType = subscriptionRequired
55+
? PriceType.SUBSCRIPTION
56+
: this.priceType === PriceType.SUBSCRIPTION
57+
? PriceType.FIXED
58+
: this.priceType;
59+
60+
const fareAmount = new FareAmount(
61+
effectivePriceType === PriceType.FIXED ? effectivePrice : 0,
62+
this.acpContractClient.config.baseFare,
63+
);
64+
65+
const account = await this.resolveAccount(
66+
subscriptionRequired,
67+
preferredSubscriptionTier,
68+
);
69+
70+
const jobId = await this.createJob(
71+
account,
72+
evaluatorAddress,
73+
expiredAt,
74+
fareAmount,
75+
subscriptionRequired,
76+
preferredSubscriptionTier ?? "",
77+
);
78+
79+
await this.sendInitialMemo(jobId, fareAmount, subscriptionRequired, {
80+
name: this.name,
81+
requirement: serviceRequirement,
82+
priceValue: effectivePrice,
83+
priceType: effectivePriceType,
84+
});
85+
86+
return jobId;
87+
}
88+
89+
private validateRequest(serviceRequirement: Object | string) {
4890
if (this.providerAddress === this.acpClient.walletAddress) {
4991
throw new AcpError(
5092
"Provider address cannot be the same as the client address",
@@ -53,117 +95,112 @@ class AcpJobOffering {
5395

5496
if (this.requirement && typeof this.requirement === "object") {
5597
const validator = this.ajv.compile(this.requirement);
56-
const valid = validator(serviceRequirement);
57-
58-
if (!valid) {
98+
if (!validator(serviceRequirement)) {
5999
throw new AcpError(this.ajv.errorsText(validator.errors));
60100
}
61101
}
102+
}
62103

63-
let account: AcpAccount | null = null;
64-
let subscriptionTier = "";
65-
66-
// Subscription flow only activates when a tier is explicitly requested
67-
// or the offering is a subscription type with tiers available.
104+
private isSubscriptionRequired(preferredSubscriptionTier?: string): boolean {
68105
const hasSubscriptionTiers = this.subscriptionTiers.length > 0;
69-
const isSubscriptionOffering =
106+
return (
70107
preferredSubscriptionTier != null ||
71-
(this.priceType === PriceType.SUBSCRIPTION && hasSubscriptionTiers);
108+
(this.priceType === PriceType.SUBSCRIPTION && hasSubscriptionTiers)
109+
);
110+
}
111+
112+
private validateSubscriptionTier(preferredSubscriptionTier?: string) {
113+
if (!preferredSubscriptionTier) return;
72114

115+
if (this.subscriptionTiers.length === 0) {
116+
throw new AcpError(
117+
`Offering "${this.name}" does not support subscription tiers`,
118+
);
119+
}
120+
if (!this.subscriptionTiers.includes(preferredSubscriptionTier)) {
121+
throw new AcpError(
122+
`Preferred subscription tier "${preferredSubscriptionTier}" is not offered. Available: ${this.subscriptionTiers.join(", ")}`,
123+
);
124+
}
125+
}
126+
127+
/**
128+
* Resolve the account to use for the job.
129+
*
130+
* For non-subscription jobs: returns the existing account if found.
131+
* For subscription jobs, priority:
132+
* 1. Valid account matching preferred tier
133+
* 2. Any valid (non-expired) account
134+
* 3. Expired/unactivated account (expiry = 0) to reuse
135+
* 4. null — createJob will create a new one
136+
*/
137+
private async resolveAccount(
138+
subscriptionRequired: boolean,
139+
preferredSubscriptionTier?: string,
140+
): Promise<AcpAccount | null> {
73141
const raw = await this.acpClient.getByClientAndProvider(
74142
this.acpContractClient.walletAddress,
75143
this.providerAddress,
76144
this.acpContractClient,
77-
isSubscriptionOffering ? this.name : undefined,
145+
subscriptionRequired ? this.name : undefined,
78146
);
147+
148+
if (!subscriptionRequired) {
149+
return raw instanceof AcpAccount ? raw : null;
150+
}
151+
79152
const subscriptionCheck =
80153
raw && typeof raw === "object" && "accounts" in raw
81154
? (raw as ISubscriptionCheckResponse)
82155
: null;
83156

84-
console.log("raw: ", raw);
157+
if (!subscriptionCheck) return null;
85158

86-
const effectivePriceType = isSubscriptionOffering
87-
? PriceType.SUBSCRIPTION
88-
: this.priceType === PriceType.SUBSCRIPTION
89-
? PriceType.FIXED
90-
: this.priceType;
91-
const effectivePrice = effectivePriceType === PriceType.SUBSCRIPTION ? 0 : this.price;
159+
const now = Math.floor(Date.now() / 1000);
160+
const allAccounts = subscriptionCheck.accounts ?? [];
92161

93-
const subscriptionRequired = effectivePriceType === PriceType.SUBSCRIPTION;
162+
const matchedAccount =
163+
this.findPreferredAccount(allAccounts, preferredSubscriptionTier, now)
164+
?? allAccounts.find((a) => a.expiry != null && a.expiry > now)
165+
?? allAccounts.find((a) => a.expiry == null || a.expiry === 0);
94166

95-
const finalServiceRequirement: Record<string, any> = {
96-
name: this.name,
97-
requirement: serviceRequirement,
98-
priceValue: subscriptionRequired ? 0 : effectivePrice,
99-
priceType: effectivePriceType,
100-
};
167+
if (!matchedAccount) return null;
101168

102-
const fareAmount = new FareAmount(
103-
effectivePriceType === PriceType.FIXED ? effectivePrice : 0,
104-
this.acpContractClient.config.baseFare,
169+
return new AcpAccount(
170+
this.acpContractClient,
171+
matchedAccount.id,
172+
matchedAccount.clientAddress,
173+
matchedAccount.providerAddress,
174+
matchedAccount.metadata,
175+
matchedAccount.expiry,
105176
);
177+
}
106178

107-
// Validate preferred tier against offering's subscriptionTiers
108-
if (preferredSubscriptionTier) {
109-
if (!hasSubscriptionTiers) {
110-
throw new AcpError(
111-
`Offering "${this.name}" does not support subscription tiers`,
112-
);
113-
}
114-
if (!this.subscriptionTiers.includes(preferredSubscriptionTier)) {
115-
throw new AcpError(
116-
`Preferred subscription tier "${preferredSubscriptionTier}" is not offered. Available: ${this.subscriptionTiers.join(", ")}`,
117-
);
118-
}
119-
}
120-
121-
// Subscription account selection priority:
122-
// 1. Valid account matching preferred tier
123-
// 2. Any valid account
124-
// 3. Expired account (expiry = 0), reuse it
125-
// 4. No account — createJob will create a new one
126-
127-
if (!subscriptionRequired) {
128-
account = raw instanceof AcpAccount ? raw : null;
129-
} else if (subscriptionCheck) {
130-
const now = Math.floor(Date.now() / 1000);
131-
const allAccounts = subscriptionCheck.accounts ?? [];
132-
133-
const preferredValidAccount = preferredSubscriptionTier
134-
? allAccounts.find((a) => {
135-
if (a.expiry == null || a.expiry <= now) return false;
136-
const meta =
137-
typeof a.metadata === "string"
138-
? (() => { try { return JSON.parse(a.metadata); } catch { return {}; } })()
139-
: (a.metadata ?? {});
140-
return meta?.name === preferredSubscriptionTier;
141-
})
142-
: undefined;
143-
144-
const anyValidAccount = preferredValidAccount
145-
?? allAccounts.find((a) => a.expiry != null && a.expiry > now);
146-
147-
const reusableAccount = anyValidAccount
148-
?? allAccounts.find((a) => a.expiry == null || a.expiry === 0);
149-
150-
if (reusableAccount) {
151-
account = new AcpAccount(
152-
this.acpContractClient,
153-
reusableAccount.id,
154-
reusableAccount.clientAddress,
155-
reusableAccount.providerAddress,
156-
reusableAccount.metadata,
157-
reusableAccount.expiry,
158-
);
159-
} else {
160-
subscriptionTier = preferredSubscriptionTier ?? "";
161-
}
162-
} else {
163-
// No accounts at all — new account will be created by createJob
164-
subscriptionTier = preferredSubscriptionTier ?? "";
165-
}
179+
private findPreferredAccount(
180+
accounts: IAcpAccount[],
181+
preferredTier: string | undefined,
182+
now: number,
183+
): IAcpAccount | undefined {
184+
if (!preferredTier) return undefined;
185+
186+
return accounts.find((a) => {
187+
if (a.expiry == null || a.expiry <= now) return false;
188+
const meta =
189+
typeof a.metadata === "string"
190+
? (() => { try { return JSON.parse(a.metadata); } catch { return {}; } })()
191+
: (a.metadata ?? {});
192+
return meta?.name === preferredTier;
193+
});
194+
}
166195

196+
private async createJob(
197+
account: AcpAccount | null,
198+
evaluatorAddress: Address | undefined,
199+
expiredAt: Date,
200+
fareAmount: FareAmount,
201+
subscriptionRequired: boolean,
202+
subscriptionTier: string,
203+
): Promise<number> {
167204
const isV1 = [
168205
baseSepoliaAcpConfig.contractAddress,
169206
baseSepoliaAcpX402Config.contractAddress,
@@ -173,80 +210,77 @@ class AcpJobOffering {
173210

174211
const chainId = this.acpContractClient.config.chain
175212
.id as keyof typeof USDC_TOKEN_ADDRESS;
176-
177213
const isUsdcPaymentToken =
178214
USDC_TOKEN_ADDRESS[chainId].toLowerCase() ===
179215
fareAmount.fare.contractAddress.toLowerCase();
180-
181216
const isX402Job =
182217
this.acpContractClient.config.x402Config && isUsdcPaymentToken;
183218

184-
// For subscription jobs, include tier name only as account metadata (before payment)
219+
const budget = subscriptionRequired ? 0n : fareAmount.amount;
185220
const subscriptionMetadata = JSON.stringify({ name: subscriptionTier });
186221

187-
const createJobOperations: OperationPayload[] = [];
188-
189-
if (isV1 || !account) {
190-
createJobOperations.push(
191-
this.acpContractClient.createJob(
192-
this.providerAddress,
193-
evaluatorAddress || this.acpContractClient.walletAddress,
194-
expiredAt,
195-
fareAmount.fare.contractAddress,
196-
subscriptionRequired ? 0n : fareAmount.amount,
197-
subscriptionMetadata,
198-
isX402Job,
199-
),
200-
);
201-
} else {
202-
createJobOperations.push(
203-
this.acpContractClient.createJobWithAccount(
204-
account.id,
205-
evaluatorAddress || zeroAddress,
206-
subscriptionRequired ? 0n : fareAmount.amount,
207-
fareAmount.fare.contractAddress,
208-
expiredAt,
209-
isX402Job,
210-
),
211-
);
212-
}
213-
214-
const { userOpHash } =
215-
await this.acpContractClient.handleOperation(createJobOperations);
216-
217-
const jobId = await this.acpContractClient.getJobId(
222+
const operation =
223+
isV1 || !account
224+
? this.acpContractClient.createJob(
225+
this.providerAddress,
226+
evaluatorAddress || this.acpContractClient.walletAddress,
227+
expiredAt,
228+
fareAmount.fare.contractAddress,
229+
budget,
230+
subscriptionMetadata,
231+
isX402Job,
232+
)
233+
: this.acpContractClient.createJobWithAccount(
234+
account.id,
235+
evaluatorAddress || zeroAddress,
236+
budget,
237+
fareAmount.fare.contractAddress,
238+
expiredAt,
239+
isX402Job,
240+
);
241+
242+
const { userOpHash } = await this.acpContractClient.handleOperation([
243+
operation,
244+
]);
245+
246+
return this.acpContractClient.getJobId(
218247
userOpHash,
219248
this.acpContractClient.walletAddress,
220249
this.providerAddress,
221250
);
251+
}
222252

253+
private async sendInitialMemo(
254+
jobId: number,
255+
fareAmount: FareAmount,
256+
subscriptionRequired: boolean,
257+
serviceRequirement: Record<string, any>,
258+
) {
223259
const payloads: OperationPayload[] = [];
224-
const setBudgetWithPaymentTokenPayload =
225-
this.acpContractClient.setBudgetWithPaymentToken(
226-
jobId,
227-
fareAmount.amount,
228-
fareAmount.fare.contractAddress,
229-
);
230260

231261
if (!subscriptionRequired) {
232-
if (setBudgetWithPaymentTokenPayload) {
233-
payloads.push(setBudgetWithPaymentTokenPayload);
262+
const setBudgetPayload =
263+
this.acpContractClient.setBudgetWithPaymentToken(
264+
jobId,
265+
fareAmount.amount,
266+
fareAmount.fare.contractAddress,
267+
);
268+
if (setBudgetPayload) {
269+
payloads.push(setBudgetPayload);
234270
}
235271
}
236272

237273
payloads.push(
238274
this.acpContractClient.createMemo(
239275
jobId,
240-
JSON.stringify(finalServiceRequirement),
276+
JSON.stringify(serviceRequirement),
241277
MemoType.MESSAGE,
242278
true,
243279
AcpJobPhases.NEGOTIATION,
244280
),
245281
);
246282

247283
await this.acpContractClient.handleOperation(payloads);
248-
249-
return jobId;
250284
}
251285
}
252286

0 commit comments

Comments
 (0)