diff --git a/README.md b/README.md index d0b2fad..0730432 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,11 @@ Copy `.env.example` values into your shell/session before deploy: 9. `PAYMENT_TOKEN_ADDRESS` 10. `OFFER_PRICE_ATOMIC` 11. `ENTITLEMENT_DEPLOY_OUTPUT_PATH` (optional) +12. `OFFERS_JSON` (optional path to per-offer seed config JSON) +13. `OFFERS_INLINE_JSON` (optional inline JSON array alternative to `OFFERS_JSON`) + +If no offer override JSON is provided, deploy script seeds default offers at `OFFER_PRICE_ATOMIC`. +Use `deploy/offers.template.json` to define per-offer prices and policy flags. Example (Sepolia): diff --git a/deploy/README.md b/deploy/README.md index 37cc907..7339a26 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -12,6 +12,7 @@ Template: - `membership-deploy.template.json` - `entitlement-deploy.template.json` +- `offers.template.json` Recommended process: @@ -19,3 +20,4 @@ Recommended process: `npm run deploy:entitlement:sepolia` / `npm run deploy:entitlement:mainnet` for offer entitlements. 2. Copy the matching template to a dated file (for example `membership-base-sepolia-2026-02-18.json`). 3. Fill all deployment fields from script output and explorer links. +4. If you need per-offer pricing, copy `offers.template.json`, edit values, and pass it via `OFFERS_JSON=/path/to/file.json`. diff --git a/deploy/offers.template.json b/deploy/offers.template.json new file mode 100644 index 0000000..c1fff81 --- /dev/null +++ b/deploy/offers.template.json @@ -0,0 +1,32 @@ +[ + { + "offer_id": "edut.solo.core", + "price_atomic": "1000000000", + "active": true, + "membership_required": true + }, + { + "offer_id": "edut.workspace.core", + "price_atomic": "1000000000", + "active": true, + "membership_required": true + }, + { + "offer_id": "edut.workspace.ai", + "price_atomic": "1000000000", + "active": true, + "membership_required": true + }, + { + "offer_id": "edut.workspace.lane24", + "price_atomic": "1000000000", + "active": true, + "membership_required": true + }, + { + "offer_id": "edut.workspace.sovereign", + "price_atomic": "1000000000", + "active": true, + "membership_required": true + } +] diff --git a/scripts/deploy-entitlement.cjs b/scripts/deploy-entitlement.cjs index 9b9d58a..5241b3f 100644 --- a/scripts/deploy-entitlement.cjs +++ b/scripts/deploy-entitlement.cjs @@ -40,6 +40,62 @@ function parseUint(name, value) { } } +function readOfferOverrides() { + const inlineRaw = (process.env.OFFERS_INLINE_JSON || "").trim(); + if (inlineRaw) { + try { + return JSON.parse(inlineRaw); + } catch (err) { + throw new Error(`Invalid OFFERS_INLINE_JSON (${err.message})`); + } + } + + const filePath = (process.env.OFFERS_JSON || "").trim(); + if (!filePath) { + return null; + } + const absolute = path.resolve(filePath); + const raw = fs.readFileSync(absolute, "utf8"); + try { + return JSON.parse(raw); + } catch (err) { + throw new Error(`Invalid OFFERS_JSON at ${absolute} (${err.message})`); + } +} + +function buildOfferPlan(defaultPriceAtomic) { + const overrides = readOfferOverrides(); + if (!overrides) { + return DEFAULT_OFFERS.map((offerID) => ({ + offerID, + priceAtomic: defaultPriceAtomic, + active: true, + membershipRequired: true, + })); + } + if (!Array.isArray(overrides) || overrides.length === 0) { + throw new Error("Offer override list must be a non-empty JSON array"); + } + + return overrides.map((entry, index) => { + if (!entry || typeof entry !== "object") { + throw new Error(`Offer override at index ${index} must be an object`); + } + const offerID = String(entry.offer_id || "").trim(); + if (!offerID) { + throw new Error(`Offer override at index ${index} missing offer_id`); + } + const rawPrice = String( + entry.price_atomic !== undefined ? entry.price_atomic : defaultPriceAtomic.toString() + ).trim(); + const priceAtomic = parseUint(`offer[${offerID}].price_atomic`, rawPrice); + const active = entry.active !== undefined ? Boolean(entry.active) : true; + const membershipRequired = + entry.membership_required !== undefined ? Boolean(entry.membership_required) : true; + return { offerID, priceAtomic, active, membershipRequired }; + }); +} + async function main() { const treasury = requireAddress( "ENTITLEMENT_TREASURY_WALLET", @@ -72,12 +128,31 @@ async function main() { await contract.deployed(); console.log("entitlement_contract:", contract.address); + const offerPlan = buildOfferPlan(offerPriceAtomic); const seededOffers = []; - for (const offerID of DEFAULT_OFFERS) { - const tx = await contract.upsertOffer(offerID, offerPriceAtomic, true, true); + for (const offer of offerPlan) { + const tx = await contract.upsertOffer( + offer.offerID, + offer.priceAtomic, + offer.active, + offer.membershipRequired + ); const receipt = await tx.wait(); - seededOffers.push({ offerID, txHash: receipt.transactionHash }); - console.log("offer_seeded:", offerID, receipt.transactionHash); + seededOffers.push({ + offerID: offer.offerID, + priceAtomic: offer.priceAtomic.toString(), + active: offer.active, + membershipRequired: offer.membershipRequired, + txHash: receipt.transactionHash, + }); + console.log( + "offer_seeded:", + offer.offerID, + offer.priceAtomic.toString(), + offer.active, + offer.membershipRequired, + receipt.transactionHash + ); } const output = { @@ -88,6 +163,7 @@ async function main() { paymentToken, membershipContract, offerPriceAtomic: offerPriceAtomic.toString(), + defaultOfferPriceAtomic: offerPriceAtomic.toString(), entitlementContract: contract.address, txHash: contract.deployTransaction.hash, seededOffers,