Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fun build_invalid_pyth_price_feed_id(): vector<u8> {
vector::tabulate!(manager::pyth_price_identifier_length() - 1, |_| 0)
}

/// Asserts that `expected_event` of type `T` was emitted within current transaction
/// Asserts that `expected_event` of type `T` was emitted within current transaction
/// (before `test_scenario::next_tx`).
macro fun assert_emitted<$T>($expected_event: $T) {
let events = sui::event::events_by_type<$T>();
Expand Down
204 changes: 204 additions & 0 deletions packages/dapp/src/scripts/owner/amm-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* Creates a new shared AMM config for the target network.
*/
import yargs from "yargs"

import {
DEFAULT_BASE_SPREAD_BPS,
DEFAULT_VOLATILITY_MULTIPLIER_BPS,
getAmmConfigOverview,
resolveAmmConfigInputs
} from "@sui-amm/domain-core/models/amm"
import { buildCreateAmmConfigTransaction } from "@sui-amm/domain-core/ptb/amm"
import { resolveAmmPackageId } from "@sui-amm/domain-node/amm"
import { emitJsonOutput } from "@sui-amm/tooling-node/json"
import type { Tooling } from "@sui-amm/tooling-node/factory"
import { runSuiScript } from "@sui-amm/tooling-node/process"
import { findCreatedArtifactBySuffix } from "@sui-amm/tooling-node/transactions"
import {
logAmmConfigOverview,
resolveAmmAdminCapIdFromArtifacts,
resolvePythPriceFeedIdHex
} from "../../utils/amm.ts"

type CreateAmmArguments = {
adminCapId?: string
baseSpreadBps?: string
volatilityMultiplierBps?: string
useLaser?: boolean
pythPriceFeedId?: string
pythPriceFeedLabel?: string
ammPackageId?: string
devInspect?: boolean
dryRun?: boolean
json?: boolean
}

const resolveAdminCapId = async ({
tooling,
cliArguments,
ammPackageId
}: {
tooling: Pick<Tooling, "network" | "suiClient">
cliArguments: CreateAmmArguments
ammPackageId: string
}): Promise<string> => {
const explicitAdminCapId = cliArguments.adminCapId?.trim()
if (explicitAdminCapId) {
return explicitAdminCapId
}

return resolveAmmAdminCapIdFromArtifacts({
tooling,
ammPackageId
})
}

runSuiScript(
async (tooling, cliArguments: CreateAmmArguments) => {
const ammPackageId = await resolveAmmPackageId({
networkName: tooling.network.networkName,
ammPackageId: cliArguments.ammPackageId
})
const adminCapId = await resolveAdminCapId({
tooling,
cliArguments,
ammPackageId
})

const ammConfigInputs = resolveAmmConfigInputs({
pythPriceFeedIdHex: await resolvePythPriceFeedIdHex({
networkName: tooling.network.networkName,
pythPriceFeedId: cliArguments.pythPriceFeedId,
pythPriceFeedLabel: cliArguments.pythPriceFeedLabel
}),
volatilityMultiplierBps: cliArguments.volatilityMultiplierBps,
baseSpreadBps: cliArguments.baseSpreadBps,
useLaser: cliArguments.useLaser
})

const createAmmTransaction = buildCreateAmmConfigTransaction({
packageId: ammPackageId,
adminCapId,
baseSpreadBps: ammConfigInputs.baseSpreadBps,
volatilityMultiplierBps: ammConfigInputs.volatilityMultiplierBps,
useLaser: ammConfigInputs.useLaser,
pythPriceFeedIdBytes: ammConfigInputs.pythPriceFeedIdBytes
})

const { execution, summary } = await tooling.executeTransactionWithSummary({
transaction: createAmmTransaction,
signer: tooling.loadedEd25519KeyPair,
summaryLabel: "create-amm",
devInspect: cliArguments.devInspect,
dryRun: cliArguments.dryRun
})

if (!execution) {
return
}

const createdArtifacts = execution.objectArtifacts.created
const createdAmmConfig = findCreatedArtifactBySuffix(
createdArtifacts,
"::manager::AMMConfig"
)

if (!createdAmmConfig) {
throw new Error(
"Expected an AMM config object to be created, but it was not found in transaction artifacts."
)
}

const ammConfigOverview = await getAmmConfigOverview(
createdAmmConfig.objectId,
tooling.suiClient
)

if (
emitJsonOutput(
{
ammConfig: ammConfigOverview,
adminCapId,
digest: createdAmmConfig.digest,
initialSharedVersion: createdAmmConfig.initialSharedVersion,
pythPriceFeedIdHex: ammConfigInputs.pythPriceFeedIdHex,
transactionSummary: summary
},
cliArguments.json
)
) {
return
}

logAmmConfigOverview(ammConfigOverview, {
initialSharedVersion: createdAmmConfig.initialSharedVersion
})
},
yargs()
.option("adminCapId", {
alias: ["admin-cap-id"],
type: "string",
description:
"Admin cap object id for AMM config creation; inferred from the selected AMM publish when omitted.",
demandOption: false
})
.option("baseSpreadBps", {
alias: ["base-spread-bps"],
type: "string",
description: "Base spread in basis points (u64).",
default: DEFAULT_BASE_SPREAD_BPS,
demandOption: false
})
.option("volatilityMultiplierBps", {
alias: ["volatility-multiplier-bps"],
type: "string",
description: "Volatility multiplier in basis points (u64).",
default: DEFAULT_VOLATILITY_MULTIPLIER_BPS,
demandOption: false
})
.option("useLaser", {
alias: ["use-laser"],
type: "boolean",
default: false,
description: "Enable the laser pricing path for the AMM."
})
.option("pythPriceFeedId", {
alias: ["pyth-price-feed-id", "pyth-feed-id"],
type: "string",
description: "Pyth price feed id (32 bytes hex).",
demandOption: false
})
.option("pythPriceFeedLabel", {
alias: ["pyth-price-feed-label", "pyth-feed-label"],
type: "string",
description:
"Localnet artifact feed label to resolve the feed id when --pyth-price-feed-id is omitted.",
demandOption: false
})
.option("ammPackageId", {
alias: ["amm-package-id"],
type: "string",
description:
"Package ID for the AMM Move package; inferred from the latest publish entry in deployments/deployment.<network>.json when omitted.",
demandOption: false
})
.option("devInspect", {
alias: ["dev-inspect", "debug"],
type: "boolean",
default: false,
description: "Run a dev-inspect and log VM error details."
})
.option("dryRun", {
alias: ["dry-run"],
type: "boolean",
default: false,
description: "Run dev-inspect and exit without executing the transaction."
})
.option("json", {
type: "boolean",
default: false,
description: "Output results as JSON."
})
.strict()
)
160 changes: 160 additions & 0 deletions packages/dapp/src/scripts/owner/test-integration/amm-create.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { readFile } from "node:fs/promises"
import path from "node:path"
import { describe, expect, it } from "vitest"

import {
AMM_CONFIG_TYPE_SUFFIX,
type AmmConfigOverview
} from "@sui-amm/domain-core/models/amm"
import { normalizeHex } from "@sui-amm/tooling-core/hex"
import { extractInitialSharedVersion } from "@sui-amm/tooling-core/shared-object"
import { createSuiLocalnetTestEnv } from "@sui-amm/tooling-node/testing/env"
import {
resolveDappMoveRoot,
resolveDappRoot
} from "@sui-amm/tooling-node/testing/paths"
import {
createSuiScriptRunner,
parseJsonFromScriptOutput
} from "@sui-amm/tooling-node/testing/scripts"
import { DEFAULT_LOCALNET_PYTH_PRICE_FEED_ID } from "../../../utils/amm.ts"

type AmmCreateOutput = {
adminCapId?: string
ammConfig?: AmmConfigOverview
digest?: string
initialSharedVersion?: string
pythPriceFeedIdHex?: string
transactionSummary?: { label?: string }
}

type ObjectArtifact = {
objectId?: string
objectType?: string
initialSharedVersion?: string
}

const resolveKeepTemp = () => process.env.SUI_IT_KEEP_TEMP === "1"

const resolveWithFaucet = () => process.env.SUI_IT_WITH_FAUCET !== "0"

const resolveOwnerScriptPath = (scriptName: string) =>
path.join(
resolveDappRoot(),
"src",
"scripts",
"owner",
scriptName.endsWith(".ts") ? scriptName : `${scriptName}.ts`
)

const resolveObjectArtifactsPath = (artifactsDir: string) =>
path.join(artifactsDir, "objects.localnet.json")

const readObjectArtifacts = async (artifactsDir: string) => {
const contents = await readFile(
resolveObjectArtifactsPath(artifactsDir),
"utf8"
)

return JSON.parse(contents) as ObjectArtifact[]
}

const findObjectArtifactById = (
artifacts: ObjectArtifact[],
objectId: string
) => artifacts.find((artifact) => artifact.objectId === objectId)

const testEnv = createSuiLocalnetTestEnv({
mode: "test",
keepTemp: resolveKeepTemp(),
withFaucet: resolveWithFaucet(),
moveSourceRootPath: resolveDappMoveRoot()
})

describe("owner amm-create integration", () => {
it("creates a shared AMM config and records artifacts", async () => {
await testEnv.withTestContext("owner-amm-create", async (context) => {
const publisher = context.createAccount("publisher")
await context.fundAccount(publisher, { minimumCoinObjects: 2 })

await context.publishPackage("prop-amm", publisher, {
withUnpublishedDependencies: true
})
Comment on lines +80 to +82
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert adminCapId against the package published in this test.

Lines 80-82 discard the publish metadata, and Lines 115-117 only check that adminCapId is non-empty. A resolver or serialization bug can return a stale/unrelated cap ID and this test would still pass. Capture the publish digest and compare the output to the admin cap derived from that digest.

Also applies to: 115-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dapp/src/scripts/owner/test-integration/amm-create.test.ts` around
lines 80 - 82, Capture the result of context.publishPackage (e.g., const
publishRes = await context.publishPackage(...)) instead of discarding it,
extract the package digest from that result, then derive the expected admin
capability id from that digest and assert adminCapId === expectedAdminCapId;
replace the current non-empty check of adminCapId with this equality assertion
and use existing helpers (e.g., any compute/derive function for admin cap ids or
a context.getAdminCapIdForDigest/computeAdminCapId helper) to compute the
expected value from publishRes.digest.


const baseSpreadBps = "37"
const volatilityMultiplierBps = "420"
const useLaser = true
const pythPriceFeedId = DEFAULT_LOCALNET_PYTH_PRICE_FEED_ID

const scriptRunner = createSuiScriptRunner(context)
const result = await scriptRunner.runScript(
resolveOwnerScriptPath("amm-create"),
{
account: publisher,
args: {
json: true,
baseSpreadBps,
volatilityMultiplierBps,
useLaser
}
}
)

expect(result.exitCode).toBe(0)

const output = parseJsonFromScriptOutput<AmmCreateOutput>(
result.stdout,
"amm-create output"
)
if (!output.ammConfig) {
throw new Error("amm-create output did not include ammConfig.")
}
if (!output.initialSharedVersion) {
throw new Error("amm-create output did not include shared version.")
}
if (!output.adminCapId) {
throw new Error("amm-create output did not include adminCapId.")
}

expect(output.digest).toBeTruthy()
expect(output.transactionSummary?.label).toBe("create-amm")
expect(output.ammConfig.baseSpreadBps).toBe(baseSpreadBps)
expect(output.ammConfig.volatilityMultiplierBps).toBe(
volatilityMultiplierBps
)
expect(output.ammConfig.useLaser).toBe(useLaser)
expect(output.ammConfig.tradingPaused).toBe(false)
expect(normalizeHex(output.ammConfig.pythPriceFeedIdHex)).toBe(
normalizeHex(pythPriceFeedId)
)
expect(normalizeHex(output.pythPriceFeedIdHex ?? "")).toBe(
normalizeHex(pythPriceFeedId)
)

const objectResponse = await context.suiClient.getObject({
id: output.ammConfig.configId,
options: { showOwner: true }
})
if (!objectResponse.data) {
throw new Error("AMM config object could not be loaded from localnet.")
}
const onChainSharedVersion = extractInitialSharedVersion(
objectResponse.data
)

expect(onChainSharedVersion).toBe(output.initialSharedVersion)

const objectArtifacts = await readObjectArtifacts(context.artifactsDir)
const createdArtifact = findObjectArtifactById(
objectArtifacts,
output.ammConfig.configId
)
expect(
createdArtifact?.objectType?.endsWith(AMM_CONFIG_TYPE_SUFFIX)
).toBe(true)
expect(createdArtifact?.initialSharedVersion).toBe(
output.initialSharedVersion
)
})
})
})
Loading
Loading