|
| 1 | +/** |
| 2 | + * Subscription Example - Buyer (Client) |
| 3 | + * |
| 4 | + * Run a specific scenario via --scenario flag: |
| 5 | + * npx ts-node buyer.ts --scenario 1 # Subscription offering |
| 6 | + * npx ts-node buyer.ts --scenario 2 # Non-subscription offering (fixed-price) |
| 7 | + * |
| 8 | + * Default: scenario 1 |
| 9 | + * |
| 10 | + * Scenarios: |
| 11 | + * 1. Subscription Offering |
| 12 | + * - Seller creates PAYABLE_REQUEST_SUBSCRIPTION memo |
| 13 | + * - Buyer pays subscription; job proceeds to delivery |
| 14 | + * |
| 15 | + * 2. Non-Subscription Offering (Fixed-Price) |
| 16 | + * - Uses a non-subscription offering (jobOfferings[1]) |
| 17 | + * - Seller accepts and creates a payable requirement |
| 18 | + * - Buyer pays and advances to delivery |
| 19 | + */ |
| 20 | +import AcpClient, { |
| 21 | + AcpContractClientV2, |
| 22 | + AcpJobPhases, |
| 23 | + AcpJob, |
| 24 | + AcpMemo, |
| 25 | + MemoType, |
| 26 | + AcpAgentSort, |
| 27 | + AcpGraduationStatus, |
| 28 | + AcpOnlineStatus, |
| 29 | + baseSepoliaAcpConfigV2, |
| 30 | +} from "../../../src/index"; |
| 31 | +import { |
| 32 | + BUYER_AGENT_WALLET_ADDRESS, |
| 33 | + BUYER_ENTITY_ID, |
| 34 | + WHITELISTED_WALLET_PRIVATE_KEY, |
| 35 | +} from "./env"; |
| 36 | + |
| 37 | +// Subscription tier name — adjust to match your offering config |
| 38 | +const SUBSCRIPTION_TIER = "sub_premium"; |
| 39 | + |
| 40 | +// Parse --scenario N from argv |
| 41 | +const scenarioArg = process.argv.indexOf("--scenario"); |
| 42 | +const SCENARIO = scenarioArg !== -1 ? parseInt(process.argv[scenarioArg + 1], 10) : 1; |
| 43 | + |
| 44 | +async function buyer() { |
| 45 | + console.log(`=== Subscription Example - Buyer (Scenario ${SCENARIO}) ===\n`); |
| 46 | + |
| 47 | + const acpClient = new AcpClient({ |
| 48 | + acpContractClient: await AcpContractClientV2.build( |
| 49 | + WHITELISTED_WALLET_PRIVATE_KEY, |
| 50 | + BUYER_ENTITY_ID, |
| 51 | + BUYER_AGENT_WALLET_ADDRESS, |
| 52 | + baseSepoliaAcpConfigV2, |
| 53 | + ), |
| 54 | + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { |
| 55 | + console.log( |
| 56 | + `Buyer: onNewTask - Job ${job.id}, phase: ${AcpJobPhases[job.phase]}, ` + |
| 57 | + `memoToSign: ${memoToSign?.id ?? "None"}, ` + |
| 58 | + `nextPhase: ${memoToSign?.nextPhase !== undefined ? AcpJobPhases[memoToSign.nextPhase] : "None"}`, |
| 59 | + ); |
| 60 | + |
| 61 | + // Subscription payment requested (Scenario 1) |
| 62 | + if ( |
| 63 | + job.phase === AcpJobPhases.NEGOTIATION && |
| 64 | + memoToSign?.type === MemoType.PAYABLE_REQUEST_SUBSCRIPTION |
| 65 | + ) { |
| 66 | + console.log( |
| 67 | + `Buyer: Job ${job.id} — Subscription payment requested: ${memoToSign.content}`, |
| 68 | + ); |
| 69 | + console.log(`Buyer: Job ${job.id} — Amount: ${memoToSign.payableDetails?.amount}`); |
| 70 | + const { txnHash: subPayTx } = await job.paySubscription( |
| 71 | + `Subscription payment for ${SUBSCRIPTION_TIER}`, |
| 72 | + ); |
| 73 | + console.log(`Buyer: Job ${job.id} — Subscription paid (tx: ${subPayTx})`); |
| 74 | + |
| 75 | + // Fixed-price requirement — pay and advance to delivery (Scenario 3) |
| 76 | + } else if ( |
| 77 | + job.phase === AcpJobPhases.NEGOTIATION && |
| 78 | + memoToSign?.type === MemoType.PAYABLE_REQUEST |
| 79 | + ) { |
| 80 | + console.log(`Buyer: Job ${job.id} — Fixed-price requirement, paying now`); |
| 81 | + const payResult = await job.payAndAcceptRequirement("Payment for job"); |
| 82 | + console.log(`Buyer: Job ${job.id} — Paid and advanced to TRANSACTION phase (tx: ${payResult?.txnHash})`); |
| 83 | + |
| 84 | + // Valid subscription — accept requirement without payment (Scenario 2) |
| 85 | + } else if ( |
| 86 | + job.phase === AcpJobPhases.NEGOTIATION && |
| 87 | + memoToSign?.type === MemoType.MESSAGE && |
| 88 | + memoToSign?.nextPhase === AcpJobPhases.TRANSACTION |
| 89 | + ) { |
| 90 | + console.log(`Buyer: Job ${job.id} — Subscription active, accepting without payment`); |
| 91 | + const { txnHash: signMemoTx } = await job.acceptRequirement( |
| 92 | + memoToSign, |
| 93 | + "Subscription verified, proceeding to delivery", |
| 94 | + ); |
| 95 | + console.log(`Buyer: Job ${job.id} — Advanced to TRANSACTION phase (tx: ${signMemoTx})`); |
| 96 | + } else if (job.phase === AcpJobPhases.COMPLETED) { |
| 97 | + console.log(`Buyer: Job ${job.id} — Completed! Deliverable:`, job.deliverable); |
| 98 | + } else if (job.phase === AcpJobPhases.REJECTED) { |
| 99 | + console.log(`Buyer: Job ${job.id} — Rejected. Reason:`, job.rejectionReason); |
| 100 | + } else { |
| 101 | + console.log( |
| 102 | + `Buyer: Job ${job.id} — Unhandled event (phase: ${AcpJobPhases[job.phase]}, ` + |
| 103 | + `memoType: ${memoToSign?.type !== undefined ? MemoType[memoToSign.type] : "None"}, ` + |
| 104 | + `nextPhase: ${memoToSign?.nextPhase !== undefined ? AcpJobPhases[memoToSign.nextPhase] : "None"})`, |
| 105 | + ); |
| 106 | + } |
| 107 | + }, |
| 108 | + }); |
| 109 | + |
| 110 | + // Browse available agents |
| 111 | + const relevantAgents = await acpClient.browseAgents("", { |
| 112 | + sortBy: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], |
| 113 | + topK: 5, |
| 114 | + graduationStatus: AcpGraduationStatus.ALL, |
| 115 | + onlineStatus: AcpOnlineStatus.ALL, |
| 116 | + showHiddenOfferings: true, |
| 117 | + }); |
| 118 | + |
| 119 | + if (!relevantAgents || relevantAgents.length === 0) { |
| 120 | + console.error("No agents found"); |
| 121 | + return; |
| 122 | + } |
| 123 | + |
| 124 | + const chosenAgent = relevantAgents[0]; |
| 125 | + const subscriptionOffering = chosenAgent.jobOfferings[0]; |
| 126 | + const fixedOffering = chosenAgent.jobOfferings[1]; |
| 127 | + |
| 128 | + switch (SCENARIO) { |
| 129 | + case 1: { |
| 130 | + console.log("--- Scenario 1: Subscription Offering ---\n"); |
| 131 | + const jobId1 = await subscriptionOffering.initiateJob( |
| 132 | + {}, |
| 133 | + undefined, |
| 134 | + new Date(Date.now() + 1000 * 60 * 15), // 15 min job expiry |
| 135 | + SUBSCRIPTION_TIER, |
| 136 | + ); |
| 137 | + console.log(`\nBuyer: [Scenario 1 — Subscription Offering] Job ${jobId1} initiated`); |
| 138 | + break; |
| 139 | + } |
| 140 | + |
| 141 | + case 2: { |
| 142 | + console.log("--- Scenario 2: Non-Subscription Offering (Fixed-Price) ---\n"); |
| 143 | + const jobId2 = await fixedOffering.initiateJob( |
| 144 | + {}, |
| 145 | + undefined, |
| 146 | + new Date(Date.now() + 1000 * 60 * 15), // 15 min job expiry |
| 147 | + ); |
| 148 | + console.log(`\nBuyer: [Scenario 2 — Fixed-Price Job] Job ${jobId2} initiated`); |
| 149 | + break; |
| 150 | + } |
| 151 | + |
| 152 | + default: |
| 153 | + console.error(`Unknown scenario: ${SCENARIO}. Use --scenario 1 or 2.`); |
| 154 | + process.exit(1); |
| 155 | + } |
| 156 | +} |
| 157 | + |
| 158 | +buyer().catch((error) => { |
| 159 | + console.error("Buyer error:", error); |
| 160 | + process.exit(1); |
| 161 | +}); |
0 commit comments