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
191 changes: 57 additions & 134 deletions src/acpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,40 @@ class AcpClient {
return this.acpContractClient.walletAddress;
}

private _hydrateJob(data: IAcpJob["data"]): AcpJob {
const memos = data.memos.map((memo) =>
new AcpMemo(
this.contractClientByAddress(data.contractAddress),
memo.id,
memo.memoType,
memo.content,
memo.nextPhase,
memo.status,
memo.senderAddress,
memo.signedReason,
memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined,
memo.payableDetails,
memo.txHash,
memo.signedTxHash,
)
);

return new AcpJob(
this,
data.id,
data.clientAddress,
data.providerAddress,
data.evaluatorAddress,
data.price,
data.priceTokenAddress,
memos,
data.phase,
data.context,
data.contractAddress,
data.deliverable,
);
}

async init(skipSocketConnection: boolean = false) {
if (skipSocketConnection) {
return;
Expand Down Expand Up @@ -146,39 +180,15 @@ class AcpClient {
callback(true);

if (this.onEvaluate) {
const job = new AcpJob(
this,
data.id,
data.clientAddress,
data.providerAddress,
data.evaluatorAddress,
data.price,
data.priceTokenAddress,
data.memos.map((memo) => {
return new AcpMemo(
this.contractClientByAddress(data.contractAddress),
memo.id,
memo.memoType,
memo.content,
memo.nextPhase,
memo.status,
memo.senderAddress,
memo.signedReason,
memo.expiry
? new Date(parseInt(memo.expiry) * 1000)
: undefined,
memo.payableDetails,
memo.txHash,
memo.signedTxHash,
);
}),
data.phase,
data.context,
data.contractAddress,
data.netPayableAmount,
);

this.onEvaluate(job);
try {
const job = this._hydrateJob(data);
this.onEvaluate(job);
} catch (err) {
console.error(
`Failed to hydrate job ${data.id} in ON_EVALUATE:`,
err
);
}
}
},
);
Expand All @@ -189,42 +199,18 @@ class AcpClient {
callback(true);

if (this.onNewTask) {
const job = new AcpJob(
this,
data.id,
data.clientAddress,
data.providerAddress,
data.evaluatorAddress,
data.price,
data.priceTokenAddress,
data.memos.map((memo) => {
return new AcpMemo(
this.contractClientByAddress(data.contractAddress),
memo.id,
memo.memoType,
memo.content,
memo.nextPhase,
memo.status,
memo.senderAddress,
memo.signedReason,
memo.expiry
? new Date(parseInt(memo.expiry) * 1000)
: undefined,
memo.payableDetails,
memo.txHash,
memo.signedTxHash,
);
}),
data.phase,
data.context,
data.contractAddress,
data.netPayableAmount,
);

this.onNewTask(
job,
job.memos.find((m) => m.id == data.memoToSign),
);
try {
const job = this._hydrateJob(data);
this.onNewTask(
job,
job.memos.find((m) => m.id == data.memoToSign),
);
} catch (err) {
console.error(
`Failed to hydrate job ${data.id} in ON_NEW_TASK:`,
err
);
}
}
},
);
Expand Down Expand Up @@ -499,39 +485,7 @@ class AcpClient {

for (const job of rawJobs) {
try {
const memos = job.memos.map((memo) =>
new AcpMemo(
this.contractClientByAddress(job.contractAddress),
memo.id,
memo.memoType,
memo.content,
memo.nextPhase,
memo.status,
memo.senderAddress,
memo.signedReason,
memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined,
memo.payableDetails,
memo.txHash,
memo.signedTxHash,
)
);

jobs.push(
new AcpJob(
this,
job.id,
job.clientAddress,
job.providerAddress,
job.evaluatorAddress,
job.price,
job.priceTokenAddress,
memos,
job.phase,
job.context,
job.contractAddress,
job.netPayableAmount,
)
);
jobs.push(this._hydrateJob(job));
} catch (err) {
errors.push({ jobId: job.id, error: err as Error });
}
Expand Down Expand Up @@ -582,38 +536,7 @@ class AcpClient {
}

try {
const memos = job.memos.map(
(memo) =>
new AcpMemo(
this.contractClientByAddress(job.contractAddress),
memo.id,
memo.memoType,
memo.content,
memo.nextPhase,
memo.status,
memo.senderAddress,
memo.signedReason,
memo.expiry ? new Date(parseInt(memo.expiry) * 1000) : undefined,
memo.payableDetails,
memo.txHash,
memo.signedTxHash,
)
);

return new AcpJob(
this,
job.id,
job.clientAddress,
job.providerAddress,
job.evaluatorAddress,
job.price,
job.priceTokenAddress,
memos,
job.phase,
job.context,
job.contractAddress,
job.netPayableAmount,
);
return this._hydrateJob(job);
} catch (err) {
throw new AcpError(`Failed to hydrate job ${jobId}`, err);
}
Expand Down
13 changes: 12 additions & 1 deletion src/acpFare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ class Fare {
constructor(public contractAddress: Address, public decimals: number) {}

formatAmount(amount: number) {
return parseUnits(amount.toString(), this.decimals);
if (!Number.isFinite(amount) || amount < 0) {
throw new AcpError(
`Invalid amount: ${amount}. Amount must be a finite, non-negative number.`
);
}

const numStr = amount.toString();
const amountStr = numStr.includes('e')
? amount.toFixed(this.decimals)
: numStr;

return parseUnits(amountStr, this.decimals);
}

static async fromContractAddress(
Expand Down
68 changes: 58 additions & 10 deletions src/acpJob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class AcpJob {
public phase: AcpJobPhases,
public context: Record<string, any>,
public contractAddress: Address,
public netPayableAmount?: number
public deliverable?: DeliverablePayload
) {
const content = this.memos.find(
(m) => m.nextPhase === AcpJobPhases.NEGOTIATION
Expand Down Expand Up @@ -85,20 +85,15 @@ class AcpJob {
return this.acpContractClient.config.baseFare;
}

public get deliverable() {
return this.memos.find((m) => m.nextPhase === AcpJobPhases.COMPLETED)
?.content;
}

public get rejectionReason() {
const requestMemo = this.memos.find(
const rejectedMemo = this.memos.find(
(m) =>
m.nextPhase === AcpJobPhases.NEGOTIATION &&
(m.nextPhase === AcpJobPhases.NEGOTIATION || m.nextPhase === AcpJobPhases.COMPLETED) &&
m.status === AcpMemoStatus.REJECTED
);

if (requestMemo) {
return requestMemo.signedReason;
if (rejectedMemo) {
return rejectedMemo.signedReason;
}

return this.memos.find((m) => m.nextPhase === AcpJobPhases.REJECTED)
Expand All @@ -125,6 +120,59 @@ class AcpJob {
return this.memos[this.memos.length - 1];
}

public get netPayableAmount(): number | undefined {
const payableMemo = this.memos.find(
(m) =>
m.nextPhase === AcpJobPhases.TRANSACTION &&
m.type === MemoType.PAYABLE_REQUEST &&
m.payableDetails
);

if (!payableMemo || !payableMemo.payableDetails) {
return undefined;
}

const decimals =
payableMemo.payableDetails.token.toLowerCase() ===
this.baseFare.contractAddress.toLowerCase()
? this.baseFare.decimals
: 18;

const grossAmount = payableMemo.payableDetails.amount;

let netAmount: bigint;
if (this.priceType === PriceType.PERCENTAGE) {
const feeBasisPoints = BigInt(Math.round(this.priceValue * 10000));
let feeAmount = (grossAmount * feeBasisPoints) / BigInt(10000);

if (feeBasisPoints > BigInt(0) && feeAmount === BigInt(0)) {
feeAmount = BigInt(1);
}

netAmount = grossAmount - feeAmount;
} else {
netAmount = grossAmount;
}

const formattedAmount = formatUnits(netAmount, decimals);
const amountNumber = parseFloat(formattedAmount);

if (!Number.isFinite(amountNumber)) {
throw new AcpError(
`Net payable amount overflow: ${formattedAmount} exceeds safe number range`
);
}

if (amountNumber > Number.MAX_SAFE_INTEGER) {
throw new AcpError(
`Net payable amount ${formattedAmount} exceeds MAX_SAFE_INTEGER (${Number.MAX_SAFE_INTEGER}). Precision may be lost.`
);
}

const factor = Math.pow(10, decimals);
return Math.floor(amountNumber * factor) / factor;
}

async createRequirement(content: string) {
const operations: OperationPayload[] = [];

Expand Down
3 changes: 1 addition & 2 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,12 @@ export interface IAcpJob {
evaluatorAddress: Address;
price: number;
priceTokenAddress: Address;
deliverable: DeliverablePayload | null;
memos: IAcpMemoData[];
context: Record<string, any>;
createdAt: string;
contractAddress: Address;
deliverable?: DeliverablePayload;
memoToSign?: number;
netPayableAmount?: number;
};
error?: Error;
}
Expand Down