437 lines
13 KiB
JavaScript
437 lines
13 KiB
JavaScript
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 GAS_LIMIT = (process.env.E2E_GAS_LIMIT || "500000").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_LIMIT && GAS_LIMIT !== "0") {
|
|
txRequest.gasLimit = ethers.BigNumber.from(GAS_LIMIT);
|
|
}
|
|
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,
|
|
};
|
|
}
|
|
|
|
function isInsufficientFundsError(err) {
|
|
const text = String(err?.message || err || "").toLowerCase();
|
|
return text.includes("insufficient funds");
|
|
}
|
|
|
|
function isTxRevertError(err) {
|
|
const code = String(err?.code || "").toUpperCase();
|
|
const text = String(err?.message || err || "").toLowerCase();
|
|
return code === "CALL_EXCEPTION" || text.includes("transaction failed");
|
|
}
|
|
|
|
function isTxPendingResponse(result) {
|
|
if (!result || result.ok) {
|
|
return false;
|
|
}
|
|
const code = String(result.data?.code || "").toLowerCase();
|
|
const detail = String(result.data?.error || "").toLowerCase();
|
|
if (result.status === 409 && code.includes("verification") && detail.includes("pending")) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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,
|
|
});
|
|
|
|
let membershipTxHash;
|
|
try {
|
|
membershipTxHash = await submitTransaction(signer, quote.tx || {});
|
|
} catch (err) {
|
|
if (isInsufficientFundsError(err)) {
|
|
result.stages.membership_activation = summarizeSkip(
|
|
"insufficient_funds",
|
|
0,
|
|
String(err?.message || err)
|
|
);
|
|
} else if (isTxRevertError(err)) {
|
|
result.stages.membership_activation = summarizeSkip(
|
|
"tx_reverted",
|
|
0,
|
|
String(err?.message || err)
|
|
);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
if (!membershipTxHash) {
|
|
console.log(JSON.stringify(result, null, 2));
|
|
return;
|
|
}
|
|
|
|
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 confirmRes = await requestJSON("POST", "/secret/membership/confirm", confirmPayload);
|
|
if (!confirmRes.ok) {
|
|
if (isTxPendingResponse(confirmRes)) {
|
|
result.stages.membership_activation = summarizeSkip(
|
|
"tx_pending",
|
|
confirmRes.status,
|
|
confirmRes.data?.error || JSON.stringify(confirmRes.data || {})
|
|
);
|
|
} else {
|
|
throw new Error(
|
|
`POST /secret/membership/confirm failed (${confirmRes.status}): ${JSON.stringify(confirmRes.data)}`
|
|
);
|
|
}
|
|
} else {
|
|
result.stages.membership_activation = {
|
|
status: confirmRes.data.status,
|
|
tx_hash: membershipTxHash,
|
|
identity_assurance_level: confirmRes.data.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 {
|
|
let checkoutTxHash;
|
|
try {
|
|
checkoutTxHash = await submitTransaction(signer, checkoutQuote.tx || {});
|
|
} catch (err) {
|
|
if (isInsufficientFundsError(err)) {
|
|
result.stages.marketplace_checkout = summarizeSkip(
|
|
"insufficient_funds",
|
|
0,
|
|
String(err?.message || err)
|
|
);
|
|
} else if (isTxRevertError(err)) {
|
|
result.stages.marketplace_checkout = summarizeSkip(
|
|
"tx_reverted",
|
|
0,
|
|
String(err?.message || err)
|
|
);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
if (!checkoutTxHash) {
|
|
console.log(JSON.stringify(result, null, 2));
|
|
return;
|
|
}
|
|
|
|
const checkoutConfirmRes = await requestJSON(
|
|
"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
|
|
);
|
|
if (!checkoutConfirmRes.ok) {
|
|
if (isTxPendingResponse(checkoutConfirmRes)) {
|
|
result.stages.marketplace_checkout = summarizeSkip(
|
|
"tx_pending",
|
|
checkoutConfirmRes.status,
|
|
checkoutConfirmRes.data?.error || JSON.stringify(checkoutConfirmRes.data || {})
|
|
);
|
|
} else {
|
|
throw new Error(
|
|
`POST /marketplace/checkout/confirm failed (${checkoutConfirmRes.status}): ${JSON.stringify(checkoutConfirmRes.data)}`
|
|
);
|
|
}
|
|
} else {
|
|
const entitlements = await expectOK(
|
|
"GET",
|
|
`/marketplace/entitlements?wallet=${encodeURIComponent(wallet)}`,
|
|
undefined,
|
|
sessionToken
|
|
);
|
|
result.stages.marketplace_checkout = {
|
|
status: checkoutConfirmRes.data.status,
|
|
entitlement_id: checkoutConfirmRes.data.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);
|
|
});
|