Add resilient entitlement seeding and control-plane e2e smoke
Some checks are pending
check / contracts (push) Waiting to run
Some checks are pending
check / contracts (push) Waiting to run
This commit is contained in:
parent
d758c3e41f
commit
4818b2a0a5
20
README.md
20
README.md
@ -63,6 +63,10 @@ Copy `.env.example` values into your shell/session before deploy:
|
||||
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`)
|
||||
14. `SEED_RETRIES` (optional, default `2`)
|
||||
15. `SEED_RETRY_DELAY_MS` (optional, default `1200`)
|
||||
16. `SEED_ONLY` (optional, `true` attaches to an existing entitlement contract and only seeds offers)
|
||||
17. `ENTITLEMENT_CONTRACT_ADDRESS` (required when `SEED_ONLY=true`)
|
||||
|
||||
`update:membership:price:*` requires:
|
||||
|
||||
@ -78,6 +82,14 @@ Smoke flow optional vars:
|
||||
1. `E2E_IDENTITY_ASSURANCE_LEVEL`
|
||||
2. `E2E_IDENTITY_ATTESTED_BY`
|
||||
3. `E2E_IDENTITY_ATTESTATION_ID`
|
||||
4. `E2E_OFFER_ID`
|
||||
5. `E2E_ORG_ROOT_ID`
|
||||
6. `E2E_PRINCIPAL_ID`
|
||||
7. `E2E_PRINCIPAL_ROLE`
|
||||
8. `E2E_WORKSPACE_ID`
|
||||
9. `E2E_DEVICE_ID`
|
||||
10. `E2E_PLATFORM`
|
||||
11. `E2E_LAUNCHER_VERSION`
|
||||
|
||||
Example (Sepolia):
|
||||
|
||||
@ -87,6 +99,14 @@ export $(grep -v '^#' .env | xargs)
|
||||
npm run deploy:sepolia
|
||||
```
|
||||
|
||||
Full control-plane smoke (membership + marketplace + governance install/status):
|
||||
|
||||
```bash
|
||||
cd /Users/vsg/Documents/VSG\ Codex/contracts
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
npm run smoke:e2e:controlplane:sepolia
|
||||
```
|
||||
|
||||
## Boundary
|
||||
|
||||
Contracts are settlement primitives. Runtime execution remains off-chain and fail-closed by entitlement state.
|
||||
|
||||
@ -13,6 +13,7 @@ Template:
|
||||
- `membership-deploy.template.json`
|
||||
- `entitlement-deploy.template.json`
|
||||
- `offers.template.json`
|
||||
- `runtime-addresses.base-sepolia.json` (runtime-wired snapshot for cross-repo address parity)
|
||||
|
||||
Recommended process:
|
||||
|
||||
@ -22,3 +23,8 @@ Recommended process:
|
||||
3. Offer override files may include non-contract metadata (for example `execution_profile`) for downstream catalog parity; deploy script ignores unknown keys and only applies on-chain fields.
|
||||
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`.
|
||||
|
||||
Address parity rule:
|
||||
|
||||
1. Keep `runtime-addresses.base-sepolia.json` synchronized with `/Users/vsg/Documents/VSG Codex/web/docs/deployment/contract-addresses.base-sepolia.json`.
|
||||
2. Any runtime address change must update both repos in the same change set.
|
||||
|
||||
13
deploy/entitlement-deploy.sepolia.json
Normal file
13
deploy/entitlement-deploy.sepolia.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"network": "baseSepolia",
|
||||
"chainId": 84532,
|
||||
"deployer": "0xD148d4dFA882007e5226C90287622b3Af6eB56D7",
|
||||
"treasury": "0xD148d4dFA882007e5226C90287622b3Af6eB56D7",
|
||||
"paymentToken": "0x0000000000000000000000000000000000000000",
|
||||
"membershipContract": "0x3EEb3342751D1Cfc0F90C9393e0B1cd5AcE6FfD8",
|
||||
"offerPriceAtomic": "1000000000",
|
||||
"entitlementContract": "0xA1c06066206d0ea63a77A093FD38327Fd5663a43",
|
||||
"txHash": null,
|
||||
"source": "web/docs/deployment/contract-addresses.base-sepolia.json",
|
||||
"notes": "Offer seeding must be verified with deploy + readback smoke before checkout is enabled."
|
||||
}
|
||||
@ -3,9 +3,10 @@
|
||||
"chainId": 84532,
|
||||
"deployer": "0xD148d4dFA882007e5226C90287622b3Af6eB56D7",
|
||||
"treasury": "0xD148d4dFA882007e5226C90287622b3Af6eB56D7",
|
||||
"mintCurrency": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
||||
"mintAmountAtomic": "100000000",
|
||||
"membershipContract": "0x51C3c35532f395B1555CF5b9d9850909365d0a4f",
|
||||
"txHash": "0x048ef7fd5444b3fb66a063aa24702c6fbbaea8e3469d618971c51a0ceb36af86",
|
||||
"mintPriceUpdatedTxHash": "0x5df32570f674e6e46d9a2907e26b70205c01ad1d68899ddd75102bfc9764f69c"
|
||||
"mintCurrency": "0x0000000000000000000000000000000000000000",
|
||||
"mintAmountAtomic": "1",
|
||||
"membershipContract": "0x3EEb3342751D1Cfc0F90C9393e0B1cd5AcE6FfD8",
|
||||
"txHash": null,
|
||||
"source": "api.edut.dev runtime environment",
|
||||
"notes": "Runtime-wired Sepolia snapshot for cross-repo parity; regenerate from deploy output when redeploying."
|
||||
}
|
||||
|
||||
13
deploy/runtime-addresses.base-sepolia.json
Normal file
13
deploy/runtime-addresses.base-sepolia.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"network": "base-sepolia",
|
||||
"chain_id": 84532,
|
||||
"membership_contract": "0x3EEb3342751D1Cfc0F90C9393e0B1cd5AcE6FfD8",
|
||||
"entitlement_contract": "0xA1c06066206d0ea63a77A093FD38327Fd5663a43",
|
||||
"offer_registry_contract": "0xA1c06066206d0ea63a77A093FD38327Fd5663a43",
|
||||
"treasury_wallet": "0xD148d4dFA882007e5226C90287622b3Af6eB56D7",
|
||||
"mint_currency_mode": "ETH_TEST",
|
||||
"mint_amount_atomic": "1",
|
||||
"usdc_contract": "0x0000000000000000000000000000000000000000",
|
||||
"source": "api.edut.dev runtime environment",
|
||||
"version": "v1"
|
||||
}
|
||||
@ -13,7 +13,8 @@
|
||||
"update:membership:price:mainnet": "hardhat run scripts/update-membership-price.cjs --network base",
|
||||
"deploy:entitlement:sepolia": "hardhat run scripts/deploy-entitlement.cjs --network baseSepolia",
|
||||
"deploy:entitlement:mainnet": "hardhat run scripts/deploy-entitlement.cjs --network base",
|
||||
"smoke:e2e:sepolia": "node scripts/e2e-membership-flow.cjs"
|
||||
"smoke:e2e:sepolia": "node scripts/e2e-membership-flow.cjs",
|
||||
"smoke:e2e:controlplane:sepolia": "node scripts/e2e-control-plane-flow.cjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nomiclabs/hardhat-ethers": "^2.2.3",
|
||||
|
||||
@ -18,6 +18,26 @@ function requiredEnv(name) {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function envInt(name, fallback) {
|
||||
const raw = (process.env[name] || "").trim();
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
throw new Error(`Invalid integer for ${name}: ${raw}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function envBool(name, fallback = false) {
|
||||
const raw = (process.env[name] || "").trim().toLowerCase();
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
||||
}
|
||||
|
||||
function requireAddress(name, value, { allowZero = false } = {}) {
|
||||
if (!hre.ethers.utils.isAddress(value)) {
|
||||
throw new Error(`Invalid address for ${name}: ${value}`);
|
||||
@ -96,6 +116,65 @@ function buildOfferPlan(defaultPriceAtomic) {
|
||||
});
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function normalizeOfferConfig(configResult) {
|
||||
return {
|
||||
priceAtomic: configResult.priceAtomic.toString(),
|
||||
active: Boolean(configResult.active),
|
||||
membershipRequired: Boolean(configResult.membershipRequired),
|
||||
exists: Boolean(configResult.exists),
|
||||
};
|
||||
}
|
||||
|
||||
function offerConfigMatches(current, expected) {
|
||||
return (
|
||||
current.exists &&
|
||||
current.priceAtomic === expected.priceAtomic.toString() &&
|
||||
current.active === expected.active &&
|
||||
current.membershipRequired === expected.membershipRequired
|
||||
);
|
||||
}
|
||||
|
||||
async function upsertOfferWithRetry(contract, offer, retries, retryDelayMs) {
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= retries + 1; attempt += 1) {
|
||||
try {
|
||||
const tx = await contract.upsertOffer(
|
||||
offer.offerID,
|
||||
offer.priceAtomic,
|
||||
offer.active,
|
||||
offer.membershipRequired
|
||||
);
|
||||
const receipt = await tx.wait();
|
||||
const cfg = normalizeOfferConfig(await contract.offerConfig(offer.offerID));
|
||||
if (!offerConfigMatches(cfg, offer)) {
|
||||
throw new Error(
|
||||
`post-upsert verification mismatch for ${offer.offerID}: got=${JSON.stringify(cfg)}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
status: "seeded",
|
||||
attempts: attempt,
|
||||
txHash: receipt.transactionHash,
|
||||
};
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
if (attempt > retries) {
|
||||
break;
|
||||
}
|
||||
const delay = retryDelayMs * attempt;
|
||||
console.warn(
|
||||
`offer_seed_retry: ${offer.offerID} attempt=${attempt} delay_ms=${delay} err=${err.message}`
|
||||
);
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
throw new Error(`failed to seed ${offer.offerID}: ${lastError?.message || "unknown error"}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const treasury = requireAddress(
|
||||
"ENTITLEMENT_TREASURY_WALLET",
|
||||
@ -114,6 +193,10 @@ async function main() {
|
||||
"OFFER_PRICE_ATOMIC",
|
||||
(process.env.OFFER_PRICE_ATOMIC || "1000000000").trim(),
|
||||
);
|
||||
const seedRetries = envInt("SEED_RETRIES", 2);
|
||||
const seedRetryDelayMs = envInt("SEED_RETRY_DELAY_MS", 1200);
|
||||
const seedOnly = envBool("SEED_ONLY", false);
|
||||
const existingEntitlementContract = (process.env.ENTITLEMENT_CONTRACT_ADDRESS || "").trim();
|
||||
|
||||
const [deployer] = await hre.ethers.getSigners();
|
||||
console.log("deployer:", deployer.address);
|
||||
@ -124,26 +207,53 @@ async function main() {
|
||||
console.log("offer_price_atomic:", offerPriceAtomic.toString());
|
||||
|
||||
const factory = await hre.ethers.getContractFactory("EdutOfferEntitlement");
|
||||
const contract = await factory.deploy(treasury, paymentToken, membershipContract);
|
||||
await contract.deployed();
|
||||
console.log("entitlement_contract:", contract.address);
|
||||
let contract;
|
||||
let deployTxHash = null;
|
||||
if (seedOnly) {
|
||||
if (!existingEntitlementContract) {
|
||||
throw new Error("SEED_ONLY=true requires ENTITLEMENT_CONTRACT_ADDRESS");
|
||||
}
|
||||
requireAddress("ENTITLEMENT_CONTRACT_ADDRESS", existingEntitlementContract);
|
||||
contract = factory.attach(existingEntitlementContract);
|
||||
console.log("entitlement_contract_attached:", contract.address);
|
||||
} else {
|
||||
contract = await factory.deploy(treasury, paymentToken, membershipContract);
|
||||
await contract.deployed();
|
||||
deployTxHash = contract.deployTransaction.hash;
|
||||
console.log("entitlement_contract:", contract.address);
|
||||
}
|
||||
|
||||
const offerPlan = buildOfferPlan(offerPriceAtomic);
|
||||
const seededOffers = [];
|
||||
for (const offer of offerPlan) {
|
||||
const tx = await contract.upsertOffer(
|
||||
offer.offerID,
|
||||
offer.priceAtomic,
|
||||
offer.active,
|
||||
offer.membershipRequired
|
||||
);
|
||||
const receipt = await tx.wait();
|
||||
const current = normalizeOfferConfig(await contract.offerConfig(offer.offerID));
|
||||
if (offerConfigMatches(current, offer)) {
|
||||
seededOffers.push({
|
||||
offerID: offer.offerID,
|
||||
priceAtomic: offer.priceAtomic.toString(),
|
||||
active: offer.active,
|
||||
membershipRequired: offer.membershipRequired,
|
||||
status: "already_aligned",
|
||||
attempts: 0,
|
||||
});
|
||||
console.log(
|
||||
"offer_already_aligned:",
|
||||
offer.offerID,
|
||||
offer.priceAtomic.toString(),
|
||||
offer.active,
|
||||
offer.membershipRequired
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const seeded = await upsertOfferWithRetry(contract, offer, seedRetries, seedRetryDelayMs);
|
||||
seededOffers.push({
|
||||
offerID: offer.offerID,
|
||||
priceAtomic: offer.priceAtomic.toString(),
|
||||
active: offer.active,
|
||||
membershipRequired: offer.membershipRequired,
|
||||
txHash: receipt.transactionHash,
|
||||
status: seeded.status,
|
||||
attempts: seeded.attempts,
|
||||
txHash: seeded.txHash,
|
||||
});
|
||||
console.log(
|
||||
"offer_seeded:",
|
||||
@ -151,7 +261,8 @@ async function main() {
|
||||
offer.priceAtomic.toString(),
|
||||
offer.active,
|
||||
offer.membershipRequired,
|
||||
receipt.transactionHash
|
||||
seeded.txHash,
|
||||
`attempts=${seeded.attempts}`
|
||||
);
|
||||
}
|
||||
|
||||
@ -165,7 +276,10 @@ async function main() {
|
||||
offerPriceAtomic: offerPriceAtomic.toString(),
|
||||
defaultOfferPriceAtomic: offerPriceAtomic.toString(),
|
||||
entitlementContract: contract.address,
|
||||
txHash: contract.deployTransaction.hash,
|
||||
txHash: deployTxHash,
|
||||
seedOnly,
|
||||
seedRetries,
|
||||
seedRetryDelayMs,
|
||||
seededOffers,
|
||||
};
|
||||
|
||||
|
||||
333
scripts/e2e-control-plane-flow.cjs
Normal file
333
scripts/e2e-control-plane-flow.cjs
Normal file
@ -0,0 +1,333 @@
|
||||
const { ethers } = require("ethers");
|
||||
|
||||
const API_BASE = (process.env.SECRET_API_BASE_URL || "https://api.edut.dev").trim();
|
||||
const ORIGIN = (process.env.SECRET_API_ORIGIN || "https://edut.ai").trim();
|
||||
const LOCALE = (process.env.SECRET_API_LOCALE || "en-US").trim();
|
||||
const CHAIN_ID = Number((process.env.SECRET_API_CHAIN_ID || "84532").trim());
|
||||
const PRIVATE_KEY = (process.env.DEPLOYER_PRIVATE_KEY || "").trim();
|
||||
const RPC_URL = (process.env.BASE_SEPOLIA_RPC_URL || "").trim();
|
||||
const GAS_PRICE_WEI = (process.env.E2E_GAS_PRICE_WEI || "").trim();
|
||||
|
||||
const OFFER_ID = (process.env.E2E_OFFER_ID || "edut.workspace.core").trim();
|
||||
const ORG_ROOT_ID = (process.env.E2E_ORG_ROOT_ID || "org.e2e.workspace").trim();
|
||||
const PRINCIPAL_ID = (process.env.E2E_PRINCIPAL_ID || "principal.e2e.owner").trim();
|
||||
const PRINCIPAL_ROLE = (process.env.E2E_PRINCIPAL_ROLE || "org_root_owner").trim();
|
||||
const WORKSPACE_ID = (process.env.E2E_WORKSPACE_ID || "workspace.e2e.alpha").trim();
|
||||
const DEVICE_ID = (process.env.E2E_DEVICE_ID || "device-e2e-001").trim();
|
||||
const PLATFORM = (process.env.E2E_PLATFORM || "macos").trim();
|
||||
const LAUNCHER_VERSION = (process.env.E2E_LAUNCHER_VERSION || "0.0.0-e2e").trim();
|
||||
|
||||
const IDENTITY_ASSURANCE = (process.env.E2E_IDENTITY_ASSURANCE_LEVEL || "").trim();
|
||||
const IDENTITY_ATTESTED_BY = (process.env.E2E_IDENTITY_ATTESTED_BY || "").trim();
|
||||
const IDENTITY_ATTESTATION_ID = (process.env.E2E_IDENTITY_ATTESTATION_ID || "").trim();
|
||||
|
||||
function required(name, value) {
|
||||
if (!value) {
|
||||
throw new Error(`Missing required env: ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function requestHeaders(sessionToken) {
|
||||
const headers = { "content-type": "application/json" };
|
||||
if (sessionToken) {
|
||||
headers.authorization = `Bearer ${sessionToken}`;
|
||||
headers["x-edut-session"] = sessionToken;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function requestJSON(method, path, payload, sessionToken) {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
method,
|
||||
headers: requestHeaders(sessionToken),
|
||||
body: payload === undefined ? undefined : JSON.stringify(payload),
|
||||
});
|
||||
const text = await response.text();
|
||||
let data;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : {};
|
||||
} catch (err) {
|
||||
throw new Error(`${method} ${path} returned non-json response: ${text}`);
|
||||
}
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async function expectOK(method, path, payload, sessionToken) {
|
||||
const result = await requestJSON(method, path, payload, sessionToken);
|
||||
if (!result.ok) {
|
||||
throw new Error(`${method} ${path} failed (${result.status}): ${JSON.stringify(result.data)}`);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async function submitTransaction(signer, txLike) {
|
||||
const txRequest = {
|
||||
to: txLike.to,
|
||||
data: txLike.data,
|
||||
value: ethers.BigNumber.from(txLike.value || "0x0"),
|
||||
};
|
||||
if (GAS_PRICE_WEI) {
|
||||
txRequest.gasPrice = ethers.BigNumber.from(GAS_PRICE_WEI);
|
||||
}
|
||||
const tx = await signer.sendTransaction(txRequest);
|
||||
await tx.wait(1);
|
||||
return tx.hash;
|
||||
}
|
||||
|
||||
function summarizeSkip(reasonCode, httpStatus, detail) {
|
||||
return {
|
||||
status: "skipped",
|
||||
reason_code: reasonCode || "unknown",
|
||||
http_status: httpStatus,
|
||||
detail,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
required("DEPLOYER_PRIVATE_KEY", PRIVATE_KEY);
|
||||
required("BASE_SEPOLIA_RPC_URL", RPC_URL);
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
|
||||
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
|
||||
const wallet = await signer.getAddress();
|
||||
|
||||
const result = {
|
||||
api_base: API_BASE,
|
||||
wallet,
|
||||
chain_id: CHAIN_ID,
|
||||
stages: {},
|
||||
};
|
||||
|
||||
const statusBefore = await expectOK(
|
||||
"GET",
|
||||
`/secret/membership/status?wallet=${encodeURIComponent(wallet)}`,
|
||||
undefined,
|
||||
null
|
||||
);
|
||||
result.stages.membership_status_before = statusBefore;
|
||||
|
||||
const intent = await expectOK("POST", "/secret/wallet/intent", {
|
||||
address: wallet,
|
||||
origin: ORIGIN,
|
||||
locale: LOCALE,
|
||||
chain_id: CHAIN_ID,
|
||||
});
|
||||
|
||||
const domain = {
|
||||
name: intent.domain_name,
|
||||
version: "1",
|
||||
chainId: intent.chain_id,
|
||||
verifyingContract: intent.verifying_contract,
|
||||
};
|
||||
const types = {
|
||||
DesignationIntent: [
|
||||
{ name: "designationCode", type: "string" },
|
||||
{ name: "designationToken", type: "string" },
|
||||
{ name: "nonce", type: "string" },
|
||||
{ name: "issuedAt", type: "string" },
|
||||
{ name: "origin", type: "string" },
|
||||
],
|
||||
};
|
||||
const message = {
|
||||
designationCode: intent.designation_code,
|
||||
designationToken: intent.display_token,
|
||||
nonce: intent.nonce,
|
||||
issuedAt: intent.issued_at,
|
||||
origin: ORIGIN,
|
||||
};
|
||||
const signature = await signer._signTypedData(domain, types, message);
|
||||
|
||||
const verify = await expectOK("POST", "/secret/wallet/verify", {
|
||||
intent_id: intent.intent_id,
|
||||
address: wallet,
|
||||
chain_id: CHAIN_ID,
|
||||
signature,
|
||||
});
|
||||
const sessionToken = verify.session_token;
|
||||
result.stages.wallet_session = {
|
||||
status: verify.status,
|
||||
expires_at: verify.session_expires_at,
|
||||
};
|
||||
|
||||
if ((statusBefore.status || "").toLowerCase() !== "active") {
|
||||
const quote = await expectOK("POST", "/secret/membership/quote", {
|
||||
designation_code: intent.designation_code,
|
||||
address: wallet,
|
||||
chain_id: CHAIN_ID,
|
||||
});
|
||||
|
||||
const membershipTxHash = await submitTransaction(signer, quote.tx || {});
|
||||
const confirmPayload = {
|
||||
designation_code: intent.designation_code,
|
||||
quote_id: quote.quote_id,
|
||||
tx_hash: membershipTxHash,
|
||||
address: wallet,
|
||||
chain_id: CHAIN_ID,
|
||||
};
|
||||
if (IDENTITY_ASSURANCE) {
|
||||
confirmPayload.identity_assurance_level = IDENTITY_ASSURANCE;
|
||||
}
|
||||
if (IDENTITY_ATTESTED_BY) {
|
||||
confirmPayload.identity_attested_by = IDENTITY_ATTESTED_BY;
|
||||
}
|
||||
if (IDENTITY_ATTESTATION_ID) {
|
||||
confirmPayload.identity_attestation_id = IDENTITY_ATTESTATION_ID;
|
||||
}
|
||||
|
||||
const confirm = await expectOK("POST", "/secret/membership/confirm", confirmPayload);
|
||||
result.stages.membership_activation = {
|
||||
status: confirm.status,
|
||||
tx_hash: membershipTxHash,
|
||||
identity_assurance_level: confirm.identity_assurance_level || null,
|
||||
};
|
||||
} else {
|
||||
result.stages.membership_activation = {
|
||||
status: "skipped_already_active",
|
||||
};
|
||||
}
|
||||
|
||||
const statusAfter = await expectOK(
|
||||
"GET",
|
||||
`/secret/membership/status?wallet=${encodeURIComponent(wallet)}`,
|
||||
undefined,
|
||||
null
|
||||
);
|
||||
result.stages.membership_status_after = statusAfter;
|
||||
|
||||
const offers = await expectOK("GET", "/marketplace/offers", undefined, sessionToken);
|
||||
const targetOffer = (offers.offers || []).find((offer) => offer.offer_id === OFFER_ID);
|
||||
if (!targetOffer) {
|
||||
throw new Error(`marketplace offer not found: ${OFFER_ID}`);
|
||||
}
|
||||
|
||||
const checkoutQuoteRes = await requestJSON(
|
||||
"POST",
|
||||
"/marketplace/checkout/quote",
|
||||
{
|
||||
wallet,
|
||||
offer_id: OFFER_ID,
|
||||
org_root_id: ORG_ROOT_ID,
|
||||
principal_id: PRINCIPAL_ID,
|
||||
principal_role: PRINCIPAL_ROLE,
|
||||
workspace_id: WORKSPACE_ID,
|
||||
include_membership_if_missing: false,
|
||||
},
|
||||
sessionToken
|
||||
);
|
||||
|
||||
if (!checkoutQuoteRes.ok) {
|
||||
result.stages.marketplace_checkout = summarizeSkip(
|
||||
checkoutQuoteRes.data.code,
|
||||
checkoutQuoteRes.status,
|
||||
checkoutQuoteRes.data.error
|
||||
);
|
||||
} else {
|
||||
const checkoutQuote = checkoutQuoteRes.data;
|
||||
const txData = String((checkoutQuote.tx || {}).data || "").toLowerCase();
|
||||
if (!txData || txData === "0x") {
|
||||
result.stages.marketplace_checkout = summarizeSkip(
|
||||
"entitlement_transaction_unconfigured",
|
||||
200,
|
||||
"checkout quote returned empty calldata"
|
||||
);
|
||||
} else {
|
||||
const checkoutTxHash = await submitTransaction(signer, checkoutQuote.tx || {});
|
||||
const checkoutConfirm = await expectOK(
|
||||
"POST",
|
||||
"/marketplace/checkout/confirm",
|
||||
{
|
||||
quote_id: checkoutQuote.quote_id,
|
||||
wallet,
|
||||
offer_id: OFFER_ID,
|
||||
org_root_id: ORG_ROOT_ID,
|
||||
principal_id: PRINCIPAL_ID,
|
||||
principal_role: PRINCIPAL_ROLE,
|
||||
workspace_id: WORKSPACE_ID,
|
||||
tx_hash: checkoutTxHash,
|
||||
chain_id: CHAIN_ID,
|
||||
},
|
||||
sessionToken
|
||||
);
|
||||
const entitlements = await expectOK(
|
||||
"GET",
|
||||
`/marketplace/entitlements?wallet=${encodeURIComponent(wallet)}`,
|
||||
undefined,
|
||||
sessionToken
|
||||
);
|
||||
result.stages.marketplace_checkout = {
|
||||
status: checkoutConfirm.status,
|
||||
entitlement_id: checkoutConfirm.entitlement_id,
|
||||
tx_hash: checkoutTxHash,
|
||||
entitlement_count: Array.isArray(entitlements.entitlements)
|
||||
? entitlements.entitlements.length
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const installTokenRes = await requestJSON(
|
||||
"POST",
|
||||
"/governance/install/token",
|
||||
{
|
||||
wallet,
|
||||
org_root_id: ORG_ROOT_ID,
|
||||
principal_id: PRINCIPAL_ID,
|
||||
principal_role: PRINCIPAL_ROLE,
|
||||
device_id: DEVICE_ID,
|
||||
launcher_version: LAUNCHER_VERSION,
|
||||
platform: PLATFORM,
|
||||
},
|
||||
sessionToken
|
||||
);
|
||||
|
||||
if (!installTokenRes.ok) {
|
||||
result.stages.governance_install = summarizeSkip(
|
||||
installTokenRes.data.code,
|
||||
installTokenRes.status,
|
||||
installTokenRes.data.error
|
||||
);
|
||||
} else {
|
||||
const installToken = installTokenRes.data;
|
||||
const installConfirm = await expectOK(
|
||||
"POST",
|
||||
"/governance/install/confirm",
|
||||
{
|
||||
install_token: installToken.install_token,
|
||||
wallet,
|
||||
device_id: DEVICE_ID,
|
||||
entitlement_id: installToken.entitlement_id,
|
||||
package_hash: installToken.package.package_hash,
|
||||
runtime_version: installToken.package.runtime_version,
|
||||
installed_at: new Date().toISOString(),
|
||||
},
|
||||
sessionToken
|
||||
);
|
||||
|
||||
const installStatus = await expectOK(
|
||||
"GET",
|
||||
`/governance/install/status?wallet=${encodeURIComponent(wallet)}&device_id=${encodeURIComponent(
|
||||
DEVICE_ID
|
||||
)}`,
|
||||
undefined,
|
||||
sessionToken
|
||||
);
|
||||
result.stages.governance_install = {
|
||||
status: installConfirm.status,
|
||||
runtime_version: installConfirm.runtime_version,
|
||||
activation_status: installStatus.activation_status,
|
||||
availability_state: installStatus.availability_state,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user