Skip to content
Closed
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
163 changes: 163 additions & 0 deletions packages/dapp/src/scripts/owner/amm-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* 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 { runSuiScript } from "@sui-amm/tooling-node/process"
import { findCreatedArtifactBySuffix } from "@sui-amm/tooling-node/transactions"
import {
logAmmConfigOverview,
resolvePythPriceFeedIdHex
} from "../../utils/amm.ts"

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

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

const ammConfigInputs = await 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,
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,
digest: createdAmmConfig.digest,
initialSharedVersion: createdAmmConfig.initialSharedVersion,
pythPriceFeedIdHex: ammConfigInputs.pythPriceFeedIdHex,
transactionSummary: summary
},
cliArguments.json
)
)
return

logAmmConfigOverview(ammConfigOverview, {
initialSharedVersion: createdAmmConfig.initialSharedVersion
})
},
yargs()
.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 mock 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()
)
157 changes: 157 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,157 @@
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 { DEFAULT_MOCK_PRICE_FEED } from "@sui-amm/domain-core/models/pyth"

Check failure on line 9 in packages/dapp/src/scripts/owner/test-integration/amm-create.test.ts

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find module '@sui-amm/domain-core/models/pyth' or its corresponding type declarations.

Check failure on line 9 in packages/dapp/src/scripts/owner/test-integration/amm-create.test.ts

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find module '@sui-amm/domain-core/models/pyth' or its corresponding type declarations.
import { normalizeHex } from "@sui-amm/tooling-core/hex"
import { extractInitialSharedVersion } from "@sui-amm/tooling-core/shared-object"
import { pickRootNonDependencyArtifact } from "@sui-amm/tooling-node/artifacts"
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"

type AmmCreateOutput = {
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 })

const publishArtifacts = await context.publishPackage(
"prop-amm",
publisher,
{ withUnpublishedDependencies: true }
)
pickRootNonDependencyArtifact(publishArtifacts)

const baseSpreadBps = "37"
const volatilityMultiplierBps = "420"
const useLaser = true
const pythPriceFeedId = DEFAULT_MOCK_PRICE_FEED.feedIdHex

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

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.")

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