Support JSON-configured per-offer entitlement seeding

This commit is contained in:
Joshua 2026-02-18 14:22:57 -08:00
parent 16a1c5836c
commit 7300612b71
4 changed files with 119 additions and 4 deletions

View File

@ -59,6 +59,11 @@ Copy `.env.example` values into your shell/session before deploy:
9. `PAYMENT_TOKEN_ADDRESS` 9. `PAYMENT_TOKEN_ADDRESS`
10. `OFFER_PRICE_ATOMIC` 10. `OFFER_PRICE_ATOMIC`
11. `ENTITLEMENT_DEPLOY_OUTPUT_PATH` (optional) 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): Example (Sepolia):

View File

@ -12,6 +12,7 @@ Template:
- `membership-deploy.template.json` - `membership-deploy.template.json`
- `entitlement-deploy.template.json` - `entitlement-deploy.template.json`
- `offers.template.json`
Recommended process: Recommended process:
@ -19,3 +20,4 @@ Recommended process:
`npm run deploy:entitlement:sepolia` / `npm run deploy:entitlement:mainnet` for offer entitlements. `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`). 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. 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`.

View File

@ -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
}
]

View File

@ -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() { async function main() {
const treasury = requireAddress( const treasury = requireAddress(
"ENTITLEMENT_TREASURY_WALLET", "ENTITLEMENT_TREASURY_WALLET",
@ -72,12 +128,31 @@ async function main() {
await contract.deployed(); await contract.deployed();
console.log("entitlement_contract:", contract.address); console.log("entitlement_contract:", contract.address);
const offerPlan = buildOfferPlan(offerPriceAtomic);
const seededOffers = []; const seededOffers = [];
for (const offerID of DEFAULT_OFFERS) { for (const offer of offerPlan) {
const tx = await contract.upsertOffer(offerID, offerPriceAtomic, true, true); const tx = await contract.upsertOffer(
offer.offerID,
offer.priceAtomic,
offer.active,
offer.membershipRequired
);
const receipt = await tx.wait(); const receipt = await tx.wait();
seededOffers.push({ offerID, txHash: receipt.transactionHash }); seededOffers.push({
console.log("offer_seeded:", offerID, receipt.transactionHash); 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 = { const output = {
@ -88,6 +163,7 @@ async function main() {
paymentToken, paymentToken,
membershipContract, membershipContract,
offerPriceAtomic: offerPriceAtomic.toString(), offerPriceAtomic: offerPriceAtomic.toString(),
defaultOfferPriceAtomic: offerPriceAtomic.toString(),
entitlementContract: contract.address, entitlementContract: contract.address,
txHash: contract.deployTransaction.hash, txHash: contract.deployTransaction.hash,
seededOffers, seededOffers,