diff --git a/.amman/.gitkeep b/.amman/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.amman/genesis.so b/.amman/genesis.so deleted file mode 100644 index bcebbf4..0000000 Binary files a/.amman/genesis.so and /dev/null differ diff --git a/.amman/mpl_account_compression.so b/.amman/mpl_account_compression.so deleted file mode 100644 index f21e0be..0000000 Binary files a/.amman/mpl_account_compression.so and /dev/null differ diff --git a/.amman/mpl_agent_identity.so b/.amman/mpl_agent_identity.so deleted file mode 100644 index 4e12be5..0000000 Binary files a/.amman/mpl_agent_identity.so and /dev/null differ diff --git a/.amman/mpl_agent_tools.so b/.amman/mpl_agent_tools.so deleted file mode 100644 index dd4e7f8..0000000 Binary files a/.amman/mpl_agent_tools.so and /dev/null differ diff --git a/.amman/mpl_bubblegum.so b/.amman/mpl_bubblegum.so deleted file mode 100644 index 60fd6fc..0000000 Binary files a/.amman/mpl_bubblegum.so and /dev/null differ diff --git a/.amman/mpl_core.so b/.amman/mpl_core.so deleted file mode 100644 index 00a2779..0000000 Binary files a/.amman/mpl_core.so and /dev/null differ diff --git a/.amman/mpl_core_candy_guard.so b/.amman/mpl_core_candy_guard.so deleted file mode 100644 index ffcd50f..0000000 Binary files a/.amman/mpl_core_candy_guard.so and /dev/null differ diff --git a/.amman/mpl_core_candy_machine.so b/.amman/mpl_core_candy_machine.so deleted file mode 100644 index a84fb5c..0000000 Binary files a/.amman/mpl_core_candy_machine.so and /dev/null differ diff --git a/.amman/mpl_distro.so b/.amman/mpl_distro.so deleted file mode 100644 index 2ba92a3..0000000 Binary files a/.amman/mpl_distro.so and /dev/null differ diff --git a/.amman/mpl_noop.so b/.amman/mpl_noop.so deleted file mode 100644 index ddea415..0000000 Binary files a/.amman/mpl_noop.so and /dev/null differ diff --git a/.amman/mpl_token_metadata.so b/.amman/mpl_token_metadata.so deleted file mode 100644 index b67330b..0000000 Binary files a/.amman/mpl_token_metadata.so and /dev/null differ diff --git a/.amman/spl_account_compression.so b/.amman/spl_account_compression.so deleted file mode 100644 index a5db971..0000000 Binary files a/.amman/spl_account_compression.so and /dev/null differ diff --git a/.amman/spl_noop.so b/.amman/spl_noop.so deleted file mode 100644 index e250fa0..0000000 Binary files a/.amman/spl_noop.so and /dev/null differ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e553f1c..2a64eea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,32 @@ jobs: with: node-version: ${{ matrix.node_version }} cache: pnpm + - name: Install Solana CLI + uses: metaplex-foundation/actions/install-solana@v1 + with: + version: ${{ env.SOLANA_VERSION }} + cache: true + - name: Dump latest programs from Devnet + run: | + declare -A PROGRAMS=( + [".amman/mpl_token_metadata.so"]="metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + [".amman/mpl_core.so"]="CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d" + [".amman/mpl_distro.so"]="D1STRoZTUiEa6r8TLg2aAbG4nSRT5cDBmgG7jDqCZvU8" + [".amman/mpl_core_candy_machine.so"]="CMACYFENjoBMHzapRXyo1JZkVS6EtaDDzkjMrmQLvr4J" + [".amman/mpl_core_candy_guard.so"]="CMAGAKJ67e9hRZgfC5SFTbZH8MgEmtqazKXjmkaJjWTJ" + [".amman/mpl_bubblegum.so"]="BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY" + [".amman/spl_noop.so"]="noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV" + [".amman/mpl_noop.so"]="mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3" + [".amman/mpl_account_compression.so"]="mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW" + [".amman/spl_account_compression.so"]="cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK" + [".amman/genesis.so"]="GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B" + [".amman/mpl_agent_identity.so"]="1DREGFgysWYxLnRnKQnwrxnJQeSMk2HmGaC6whw2B2p" + [".amman/mpl_agent_tools.so"]="TLREGni9ZEyGC3vnPZtqUh95xQ8oPqJSvNjvB7FGK8S" + ) + for file in "${!PROGRAMS[@]}"; do + echo "Dumping ${PROGRAMS[$file]} -> $file" + solana program dump -u devnet "${PROGRAMS[$file]}" "$file" + done - name: Start validator uses: metaplex-foundation/actions/start-validator@v1 with: diff --git a/.gitignore b/.gitignore index e72e451..cad84bf 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ package-lock.json .claude/ +# Program binaries (pulled fresh from Devnet in CI) +.amman/*.so + # Known created test folders candy1 testCm1 diff --git a/.mocharc.json b/.mocharc.json index 24260e9..3d1d44b 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,6 +1,6 @@ { "require": [ - "ts-node/register" + "tsx" ], "watch-extensions": [ "ts" @@ -10,7 +10,9 @@ "timeout": 60000, "parallel": true, "node-option": [ - "loader=ts-node/esm", - "experimental-specifier-resolution=node" + "import=tsx" + ], + "ignore": [ + "test/commands/distro/**" ] } diff --git a/package.json b/package.json index fa5fb2c..8567ce2 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@ledgerhq/hw-transport": "6.31.4", "@ledgerhq/hw-transport-node-hid-singleton": "6.31.5", "@metaplex-foundation/digital-asset-standard-api": "^2.0.0", - "@metaplex-foundation/genesis": "^0.26.2", + "@metaplex-foundation/genesis": "^0.32.1", "@metaplex-foundation/mpl-agent-registry": "^0.2.5", "@metaplex-foundation/mpl-bubblegum": "^5.0.2", "@metaplex-foundation/mpl-core": "^1.8.0", @@ -78,6 +78,7 @@ "oclif": "^4.17.46", "shx": "^0.3.4", "ts-node": "^10.9.2", + "tsx": "^4.21.0", "typescript": "^5.8.3" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81b73f4..1cd1d60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^2.0.0 version: 2.0.0(@metaplex-foundation/umi@1.5.1) '@metaplex-foundation/genesis': - specifier: ^0.26.2 - version: 0.26.2(@metaplex-foundation/umi@1.5.1) + specifier: ^0.32.1 + version: 0.32.1(@metaplex-foundation/umi@1.5.1) '@metaplex-foundation/mpl-agent-registry': specifier: ^0.2.5 version: 0.2.5(@metaplex-foundation/umi@1.5.1)(@noble/hashes@1.8.0) @@ -174,6 +174,9 @@ importers: ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@18.19.130)(typescript@5.8.3) + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.8.3 version: 5.8.3 @@ -367,6 +370,162 @@ packages: '@emnapi/wasi-threads@1.0.2': resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.6.1': resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -750,8 +909,8 @@ packages: peerDependencies: '@metaplex-foundation/umi': '>= 0.8.2 <= 1' - '@metaplex-foundation/genesis@0.26.2': - resolution: {integrity: sha512-z0QNEIbM0oHlglI0PSGWjVqPI0FQx4vqBHEsQTKdUK10biVc2zlutAutzzXhB9xZGAoO+1K7A44pLlZPPJ38hg==} + '@metaplex-foundation/genesis@0.32.1': + resolution: {integrity: sha512-UwXFuLH7+d7N3DEjCCGOTrQ2I4JG969LJc3GtVa5kgS+pdO3wSvYnlTyq40nvF9wEBakTKG3s5Qzy7AKpk8Buw==} peerDependencies: '@metaplex-foundation/umi': ^1.4.1 @@ -2349,6 +2508,11 @@ packages: es6-promisify@5.0.0: resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4235,6 +4399,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -5100,6 +5269,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + '@eslint-community/eslint-utils@4.6.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -5858,7 +6105,7 @@ snapshots: dependencies: '@metaplex-foundation/umi': 1.5.1 - '@metaplex-foundation/genesis@0.26.2(@metaplex-foundation/umi@1.5.1)': + '@metaplex-foundation/genesis@0.32.1(@metaplex-foundation/umi@1.5.1)': dependencies: '@metaplex-foundation/mpl-token-metadata': 3.4.0(@metaplex-foundation/umi@1.5.1) '@metaplex-foundation/mpl-toolbox': 0.10.0(@metaplex-foundation/umi@1.5.1) @@ -7231,7 +7478,7 @@ snapshots: axios@1.8.4: dependencies: - follow-redirects: 1.15.9(debug@4.3.4) + follow-redirects: 1.15.9(debug@4.4.0) form-data: 4.0.2 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -7843,6 +8090,35 @@ snapshots: dependencies: es6-promise: 4.2.8 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -9955,6 +10231,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.10.0 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 diff --git a/src/commands/genesis/launch/create.ts b/src/commands/genesis/launch/create.ts index 0490acb..8421ca0 100644 --- a/src/commands/genesis/launch/create.ts +++ b/src/commands/genesis/launch/create.ts @@ -17,7 +17,7 @@ import { generateExplorerUrl } from '../../../explorers.js' import { readJsonSync } from '../../../lib/file.js' import { detectSvmNetwork, txSignatureToString } from '../../../lib/util.js' import { promptLaunchWizard, toISOTimestamp } from '../../../lib/genesis/createGenesisWizardPrompt.js' -import { buildLaunchInput } from '../../../lib/genesis/launchApi.js' +import { buildLaunchInput, getDefaultApiUrl } from '../../../lib/genesis/launchApi.js' /* ------------------------------------------------------------------ */ /* Launch strategy types & implementations */ @@ -34,6 +34,10 @@ interface CommonLaunchParams { } network: SvmNetwork quoteMint?: QuoteMintInput + agent?: { + mint: string + setToken: boolean + } } interface LaunchStrategy { @@ -46,7 +50,7 @@ interface LaunchStrategy { const LAUNCH_STRATEGIES: Record = { 'launchpool': { requiredFlags: ['tokenAllocation', 'raiseGoal', 'raydiumLiquidityBps', 'fundsRecipient'], - disallowedFlags: [], + disallowedFlags: ['creatorFeeWallet', 'firstBuyAmount'], validate(flags) { const errors: string[] = [] @@ -91,10 +95,17 @@ const LAUNCH_STRATEGIES: Record = { 'bonding-curve': { requiredFlags: [], - disallowedFlags: ['tokenAllocation', 'raiseGoal', 'raydiumLiquidityBps', 'fundsRecipient', 'lockedAllocations'], + disallowedFlags: ['tokenAllocation', 'raiseGoal', 'raydiumLiquidityBps', 'fundsRecipient', 'lockedAllocations', 'depositStartTime'], - validate() { - return [] + validate(flags) { + const errors: string[] = [] + if (typeof flags.creatorFeeWallet === 'string' && !isPublicKey(flags.creatorFeeWallet)) { + errors.push('--creatorFeeWallet must be a valid public key') + } + if (typeof flags.firstBuyAmount === 'number' && flags.firstBuyAmount < 0) { + errors.push('--firstBuyAmount must be non-negative') + } + return errors }, buildInput(common, flags): CreateBondingCurveLaunchInput { @@ -102,9 +113,8 @@ const LAUNCH_STRATEGIES: Record = { ...common, launchType: 'bondingCurve', launch: { - bondingCurve: { - depositStartTime: flags.depositStartTime as string, - }, + ...(typeof flags.creatorFeeWallet === 'string' && { creatorFeeWallet: flags.creatorFeeWallet }), + ...(typeof flags.firstBuyAmount === 'number' && flags.firstBuyAmount > 0 && { firstBuyAmount: flags.firstBuyAmount }), }, } }, @@ -185,12 +195,21 @@ This is an all-in-one command that: The Genesis API handles creating the genesis account, mint, launch pool bucket, and optional locked allocations in a single flow. +Supports two launch types: + - launchpool: Project-style launch with deposit period, raise goal, and Raydium LP + - bonding-curve: Instant bonding curve launch with optional first buy and creator fees + +Agent mode (--agentMint) wraps transactions for execution by an on-chain agent, +enabling AI agents to launch tokens autonomously. + Use --wizard for an interactive guided setup.` static override examples = [ '$ mplx genesis launch create --wizard', '$ mplx genesis launch create --name "My Token" --symbol "MTK" --image "https://gateway.irys.xyz/abc123" --tokenAllocation 500000000 --depositStartTime 2025-03-01T00:00:00Z --raiseGoal 200 --raydiumLiquidityBps 5000 --fundsRecipient
', - '$ mplx genesis launch create --launchType bonding-curve --name "My Meme" --symbol "MEME" --image "https://gateway.irys.xyz/abc123" --depositStartTime 2025-03-01T00:00:00Z', + '$ mplx genesis launch create --launchType bonding-curve --name "My Meme" --symbol "MEME" --image "https://gateway.irys.xyz/abc123"', + '$ mplx genesis launch create --launchType bonding-curve --name "My Meme" --symbol "MEME" --image "https://gateway.irys.xyz/abc123" --creatorFeeWallet
--firstBuyAmount 0.1', + '$ mplx genesis launch create --launchType bonding-curve --name "Agent Token" --symbol "AGT" --image "https://gateway.irys.xyz/abc123" --agentMint --agentSetToken', '$ mplx genesis launch create --name "My Token" --symbol "MTK" --image "https://gateway.irys.xyz/abc123" --tokenAllocation 500000000 --depositStartTime 2025-03-01T00:00:00Z --raiseGoal 200 --raydiumLiquidityBps 5000 --fundsRecipient
--lockedAllocations allocations.json', ] @@ -242,31 +261,51 @@ Use --wizard for an interactive guided setup.` // Shared config depositStartTime: Flags.string({ - description: 'Deposit start time (ISO date string or unix timestamp). Project: 48h deposit. Memecoin: 1h deposit.', + description: '[launchpool only] Deposit start time (ISO date string or unix timestamp). 48h deposit period.', required: false, }), // Project-only launchpool config tokenAllocation: Flags.integer({ - description: '[project only] Launch pool token allocation (portion of 1B total supply)', + description: '[launchpool only] Launch pool token allocation (portion of 1B total supply)', required: false, }), raiseGoal: Flags.integer({ - description: '[project only] Raise goal in whole units (e.g., 200 for 200 SOL)', + description: '[launchpool only] Raise goal in whole units (e.g., 200 for 200 SOL)', required: false, }), raydiumLiquidityBps: Flags.integer({ - description: '[project only] Raydium liquidity in basis points (2000-10000, i.e. 20%-100%)', + description: '[launchpool only] Raydium liquidity in basis points (2000-10000, i.e. 20%-100%)', required: false, }), fundsRecipient: Flags.string({ - description: '[project only] Funds recipient wallet address', + description: '[launchpool only] Funds recipient wallet address', + required: false, + }), + + // Bonding curve config + creatorFeeWallet: Flags.string({ + description: '[bonding-curve only] Wallet address to receive creator fees (defaults to launching wallet)', + required: false, + }), + firstBuyAmount: Flags.string({ + description: '[bonding-curve only] SOL amount for mandatory first buy (e.g. 0.1 for 0.1 SOL). Omit to disable.', + required: false, + }), + + // Agent mode + agentMint: Flags.string({ + description: 'Agent NFT mint address. Wraps transactions for agent execution, enabling AI agents to launch tokens.', required: false, }), + agentSetToken: Flags.boolean({ + description: 'When using --agentMint, set the launched token on the agent NFT.', + default: false, + }), // Optional lockedAllocations: Flags.string({ - description: '[project only] Path to JSON file with locked allocation configs', + description: '[launchpool only] Path to JSON file with locked allocation configs', required: false, }), quoteMint: Flags.string({ @@ -279,8 +318,7 @@ Use --wizard for an interactive guided setup.` required: false, })(), apiUrl: Flags.string({ - description: 'Genesis API base URL', - default: 'https://api.metaplex.com', + description: 'Genesis API base URL (defaults to https://api.metaplex.com for mainnet, https://api.metaplex.dev for devnet)', required: false, }), } @@ -295,17 +333,33 @@ Use --wizard for an interactive guided setup.` } // Non-wizard mode: validate required flags - const missingFlags = (['name', 'symbol', 'image', 'depositStartTime'] as const).filter(f => !flags[f]) + const missingFlags = (['name', 'symbol', 'image'] as const).filter(f => !flags[f]) if (missingFlags.length > 0) { this.error(`Missing required flag${missingFlags.length > 1 ? 's' : ''}: ${missingFlags.map(f => `--${f}`).join(', ')}. Use --wizard for interactive setup.`) } + // depositStartTime is required for launchpool only + if (flags.launchType === 'launchpool' && !flags.depositStartTime) { + this.error('Missing required flag: --depositStartTime. Required for launchpool launches.') + } + // Normalize depositStartTime to ISO string for the SDK (accepts Date | string) const flagRecord: Record = { ...flags } - try { - flagRecord.depositStartTime = toISOTimestamp(flags.depositStartTime!) - } catch { - this.error('--depositStartTime must be a valid ISO date (e.g. 2025-06-01T00:00:00Z) or unix timestamp') + if (flags.depositStartTime) { + try { + flagRecord.depositStartTime = toISOTimestamp(flags.depositStartTime) + } catch { + this.error('--depositStartTime must be a valid ISO date (e.g. 2025-06-01T00:00:00Z) or unix timestamp') + } + } + + // Parse firstBuyAmount as a number + if (flags.firstBuyAmount) { + const amount = Number(flags.firstBuyAmount) + if (isNaN(amount) || !Number.isFinite(amount) || amount < 0) { + this.error('--firstBuyAmount must be a finite, non-negative number (e.g. 0.1)') + } + flagRecord.firstBuyAmount = amount } const strategy = LAUNCH_STRATEGIES[flags.launchType] @@ -328,6 +382,14 @@ Use --wizard for an interactive guided setup.` this.error(errors.join('\n')) } + // Validate agent flags + if (flags.agentMint && !isPublicKey(flags.agentMint)) { + this.error('--agentMint must be a valid public key (agent NFT mint address)') + } + if (flags.agentSetToken && !flags.agentMint) { + this.error('--agentSetToken requires --agentMint') + } + // Detect network from chain if not specified const network: SvmNetwork = flags.network ?? detectSvmNetwork(this.context.chain) @@ -354,11 +416,17 @@ Use --wizard for an interactive guided setup.` }, network, ...(flags.quoteMint !== 'SOL' && { quoteMint: flags.quoteMint as QuoteMintInput }), + ...(flags.agentMint && { + agent: { + mint: flags.agentMint, + setToken: flags.agentSetToken, + }, + }), } const launchInput = strategy.buildInput(common, flagRecord) - return this.sendLaunch(launchInput, flags.apiUrl) + return this.sendLaunch(launchInput, flags.apiUrl ?? getDefaultApiUrl(network)) } /* ------------------------------------------------------------------ */ @@ -378,6 +446,7 @@ Use --wizard for an interactive guided setup.` const wizardResult = await promptLaunchWizard() const networkOverride = flags.network as SvmNetwork | undefined + const network: SvmNetwork = networkOverride ?? detectSvmNetwork(this.context.chain) const launchInput = buildLaunchInput( this.context.umi.identity.publicKey.toString(), @@ -401,23 +470,29 @@ Use --wizard for an interactive guided setup.` raiseGoal: wizardResult.raiseGoal, raydiumLiquidityBps: wizardResult.raydiumLiquidityBps, fundsRecipient: wizardResult.fundsRecipient, + creatorFeeWallet: wizardResult.creatorFeeWallet, + firstBuyAmount: wizardResult.firstBuyAmount, + agent: wizardResult.agentMint ? { + mint: wizardResult.agentMint, + setToken: wizardResult.agentSetToken ?? false, + } : undefined, }, networkOverride, ) - return this.sendLaunch(launchInput, flags.apiUrl as string | undefined) + return this.sendLaunch(launchInput, (flags.apiUrl as string | undefined) ?? getDefaultApiUrl(network)) } /* ------------------------------------------------------------------ */ /* Send launch (shared by wizard and flag modes) */ /* ------------------------------------------------------------------ */ - private async sendLaunch(launchInput: CreateLaunchInput, apiUrl?: string): Promise { + private async sendLaunch(launchInput: CreateLaunchInput, apiUrl: string): Promise { const spinner = ora('Creating token launch via Genesis API...').start() try { const apiConfig: GenesisApiConfig = { - baseUrl: apiUrl ?? 'https://api.metaplex.com', + baseUrl: apiUrl, } spinner.text = 'Building transactions via Genesis API...' @@ -442,6 +517,9 @@ Use --wizard for an interactive guided setup.` this.log(`Launch ID: ${result.launch.id}`) this.log(`Launch Link: ${result.launch.link}`) this.log(`Token ID: ${result.token.id}`) + if (launchInput.agent) { + this.log(`Agent Mint: ${typeof launchInput.agent.mint === 'string' ? launchInput.agent.mint : launchInput.agent.mint.toString()}`) + } this.log('') this.log('Transactions:') for (const sig of result.signatures) { diff --git a/src/commands/genesis/launch/register.ts b/src/commands/genesis/launch/register.ts index de3cada..6d4966a 100644 --- a/src/commands/genesis/launch/register.ts +++ b/src/commands/genesis/launch/register.ts @@ -10,6 +10,7 @@ import ora from 'ora' import { TransactionCommand } from '../../../TransactionCommand.js' import { readJsonSync } from '../../../lib/file.js' import { detectSvmNetwork } from '../../../lib/util.js' +import { getDefaultApiUrl } from '../../../lib/genesis/launchApi.js' export default class GenesisLaunchRegister extends TransactionCommand { static override description = `Register an existing genesis account with the Metaplex platform. @@ -44,8 +45,7 @@ provided as a JSON file via --launchConfig.` required: false, })(), apiUrl: Flags.string({ - description: 'Genesis API base URL', - default: 'https://api.metaplex.com', + description: 'Genesis API base URL (defaults to https://api.metaplex.com for mainnet, https://api.metaplex.dev for devnet)', required: false, }), } @@ -107,12 +107,17 @@ provided as a JSON file via --launchConfig.` throw new Error('Launchpool config requires "tokenAllocation", "depositStartTime", "raiseGoal", "raydiumLiquidityBps", and "fundsRecipient" in launch.launchpool') } } else { - if (!launch.bondingCurve || typeof launch.bondingCurve !== 'object' || Array.isArray(launch.bondingCurve)) { - throw new Error('Bonding curve config requires a "launch.bondingCurve" object') + // Bonding curve: validate optional fields when present + if (launch.creatorFeeWallet !== undefined) { + if (typeof launch.creatorFeeWallet !== 'string' || launch.creatorFeeWallet.length === 0) { + throw new Error('Bonding curve "launch.creatorFeeWallet" must be a non-empty string (public key)') + } } - const curve = launch.bondingCurve as Record - if (!curve.depositStartTime) { - throw new Error('Bonding curve config requires "depositStartTime" in launch.bondingCurve') + if (launch.firstBuyAmount !== undefined) { + const amount = Number(launch.firstBuyAmount) + if (isNaN(amount) || !Number.isFinite(amount) || amount < 0) { + throw new Error('Bonding curve "launch.firstBuyAmount" must be a finite, non-negative number') + } } } @@ -127,7 +132,7 @@ provided as a JSON file via --launchConfig.` } const apiConfig: GenesisApiConfig = { - baseUrl: flags.apiUrl, + baseUrl: flags.apiUrl ?? getDefaultApiUrl(network), } const result = await registerLaunch(this.context.umi, apiConfig, { diff --git a/src/lib/genesis/createGenesisWizardPrompt.ts b/src/lib/genesis/createGenesisWizardPrompt.ts index 096099b..889b204 100644 --- a/src/lib/genesis/createGenesisWizardPrompt.ts +++ b/src/lib/genesis/createGenesisWizardPrompt.ts @@ -25,7 +25,7 @@ export type UnlockedBucketResult = AddUnlockedParams export type BucketChoice = 'launch-pool' | 'presale' | 'unlocked' | 'done' export interface LaunchWizardResult { - launchType: 'launchpool' + launchType: 'launchpool' | 'bondingCurve' name: string symbol: string image: string @@ -34,11 +34,18 @@ export interface LaunchWizardResult { twitter?: string telegram?: string quoteMint: string - depositStartTime: string - tokenAllocation: number - raiseGoal: number - raydiumLiquidityBps: number - fundsRecipient: string + // launchpool-specific + depositStartTime?: string + tokenAllocation?: number + raiseGoal?: number + raydiumLiquidityBps?: number + fundsRecipient?: string + // bonding-curve-specific + creatorFeeWallet?: string + firstBuyAmount?: number + // agent support + agentMint?: string + agentSetToken?: boolean } export interface RegisterLaunchResult { @@ -134,6 +141,13 @@ function validatePublicKey(v: string): string | true { return true } +function validateOptionalPublicKey(v: string): string | true { + abortOrTrue(v) + if (!v.trim()) return true + if (!isPublicKey(v.trim())) return 'Invalid Solana public key' + return true +} + async function promptPublicKeyInput(message: string): Promise { const value = await input({ message, validate: validatePublicKey }) return value.trim() @@ -288,6 +302,72 @@ export async function promptProjectConfig(quoteMint: string): Promise<{ return { tokenAllocation, raiseGoal, raydiumLiquidityBps, fundsRecipient } } +/* ------------------------------------------------------------------ */ +/* Agent prompts */ +/* ------------------------------------------------------------------ */ + +export async function promptAgentConfig(): Promise<{ agentMint?: string; agentSetToken?: boolean }> { + const useAgent = await confirm({ + message: 'Launch as an agent? (wraps transactions for on-chain agent execution)', + default: false, + }) + + if (!useAgent) return {} + + const agentMint = await promptPublicKeyInput('Agent NFT mint address:') + + const agentSetToken = await confirm({ + message: 'Set the launched token on the agent NFT?', + default: true, + }) + + return { agentMint, agentSetToken } +} + +/* ------------------------------------------------------------------ */ +/* Bonding curve prompts */ +/* ------------------------------------------------------------------ */ + +export async function promptBondingCurveConfig(): Promise<{ + creatorFeeWallet?: string + firstBuyAmount?: number +}> { + const customFeeWallet = await confirm({ + message: 'Set a custom creator fee wallet? (defaults to launching wallet)', + default: false, + }) + + let creatorFeeWallet: string | undefined + if (customFeeWallet) { + creatorFeeWallet = await promptPublicKeyInput('Creator fee wallet address:') + } + + const doFirstBuy = await confirm({ + message: 'Make a mandatory first buy on the bonding curve?', + default: false, + }) + + let firstBuyAmount: number | undefined + if (doFirstBuy) { + const amountStr = await input({ + message: 'First buy amount in SOL (e.g. 0.1):', + validate: (v) => { + abortOrTrue(v) + if (!v.trim()) return 'Required' + const n = Number(v) + if (isNaN(n) || !Number.isFinite(n) || n <= 0) return 'Must be a finite positive number' + return true + }, + }) + firstBuyAmount = Number(amountStr) + } + + return { + ...(creatorFeeWallet && { creatorFeeWallet }), + ...(firstBuyAmount !== undefined && { firstBuyAmount }), + } +} + /* ------------------------------------------------------------------ */ /* Manual wizard prompts */ /* ------------------------------------------------------------------ */ @@ -484,6 +564,16 @@ export async function promptBucketChoice(): Promise { /* ------------------------------------------------------------------ */ export async function promptLaunchWizard(): Promise { + console.log('') + console.log('--- Launch Type ---') + const launchType = await select({ + message: 'Choose launch type:', + choices: [ + { name: 'LaunchPool — Project-style launch with deposit period, raise goal, and Raydium LP', value: 'launchpool' as const }, + { name: 'Bonding Curve — Instant bonding curve launch with optional first buy and creator fees', value: 'bondingCurve' as const }, + ], + }) + console.log('') console.log('--- Token Metadata ---') const tokenMeta = await promptTokenMetadata() @@ -500,17 +590,44 @@ export async function promptLaunchWizard(): Promise { const resolvedQuoteMint = KNOWN_QUOTE_MINTS[quoteMintChoice] ?? quoteMintChoice const quoteName = Object.entries(KNOWN_QUOTE_MINTS).find(([, v]) => v === resolvedQuoteMint)?.[0] ?? 'custom' - const depositStartStr = await input({ - message: 'Deposit start time (ISO date or unix timestamp):', - validate: (v) => validateTimestamp(v, { requireFuture: true }), - }) - const depositStartTime = toISOTimestamp(depositStartStr) + let depositStartTime: string | undefined + let tokenAllocation: number | undefined + let raiseGoal: number | undefined + let raydiumLiquidityBps: number | undefined + let fundsRecipient: string | undefined + let creatorFeeWallet: string | undefined + let firstBuyAmount: number | undefined + + if (launchType === 'launchpool') { + const depositStartStr = await input({ + message: 'Deposit start time (ISO date or unix timestamp):', + validate: (v) => validateTimestamp(v, { requireFuture: true }), + }) + depositStartTime = toISOTimestamp(depositStartStr) + + const projectConfig = await promptProjectConfig(resolvedQuoteMint) + tokenAllocation = projectConfig.tokenAllocation + raiseGoal = projectConfig.raiseGoal + raydiumLiquidityBps = projectConfig.raydiumLiquidityBps + fundsRecipient = projectConfig.fundsRecipient + } else { + // Bonding curve config + console.log('') + console.log('--- Bonding Curve Options ---') + const bcConfig = await promptBondingCurveConfig() + creatorFeeWallet = bcConfig.creatorFeeWallet + firstBuyAmount = bcConfig.firstBuyAmount + } - const projectConfig = await promptProjectConfig(resolvedQuoteMint) + // Agent config + console.log('') + console.log('--- Agent Configuration ---') + const agentConfig = await promptAgentConfig() // Summary console.log('') console.log('=== Launch Summary ===') + console.log(` Launch Type: ${launchType === 'launchpool' ? 'LaunchPool' : 'Bonding Curve'}`) console.log(` Token: ${tokenMeta.name} (${tokenMeta.symbol})`) console.log(` Image: ${tokenMeta.image}`) if (tokenMeta.description) console.log(` Description: ${tokenMeta.description}`) @@ -518,11 +635,21 @@ export async function promptLaunchWizard(): Promise { if (socials.twitter) console.log(` Twitter: ${socials.twitter}`) if (socials.telegram) console.log(` Telegram: ${socials.telegram}`) console.log(` Quote Token: ${quoteName === 'custom' ? resolvedQuoteMint : quoteName}`) - console.log(` Deposit Start: ${depositStartTime}`) - console.log(` Token Allocation: ${projectConfig.tokenAllocation}`) - console.log(` Raise Goal: ${projectConfig.raiseGoal} ${quoteName}`) - console.log(` Raydium Liquidity: ${projectConfig.raydiumLiquidityBps / 100}%`) - console.log(` Funds Recipient: ${projectConfig.fundsRecipient}`) + if (launchType === 'launchpool') { + console.log(` Deposit Start: ${depositStartTime}`) + console.log(` Token Allocation: ${tokenAllocation}`) + console.log(` Raise Goal: ${raiseGoal} ${quoteName}`) + console.log(` Raydium Liquidity: ${raydiumLiquidityBps! / 100}%`) + console.log(` Funds Recipient: ${fundsRecipient}`) + } else { + if (creatorFeeWallet) console.log(` Creator Fee Wallet: ${creatorFeeWallet}`) + if (firstBuyAmount) console.log(` First Buy Amount: ${firstBuyAmount} SOL`) + if (!creatorFeeWallet && !firstBuyAmount) console.log(' Using default bonding curve settings') + } + if (agentConfig.agentMint) { + console.log(` Agent Mint: ${agentConfig.agentMint}`) + console.log(` Set Token on Agent: ${agentConfig.agentSetToken ? 'Yes' : 'No'}`) + } console.log('') const proceed = await confirm({ message: 'Create this launch?', default: true }) @@ -533,12 +660,19 @@ export async function promptLaunchWizard(): Promise { } return { - launchType: 'launchpool', + launchType, ...tokenMeta, ...socials, quoteMint: quoteMintChoice, - depositStartTime, - ...projectConfig, + ...(depositStartTime && { depositStartTime }), + ...(tokenAllocation !== undefined && { tokenAllocation }), + ...(raiseGoal !== undefined && { raiseGoal }), + ...(raydiumLiquidityBps !== undefined && { raydiumLiquidityBps }), + ...(fundsRecipient && { fundsRecipient }), + ...(creatorFeeWallet && { creatorFeeWallet }), + ...(firstBuyAmount !== undefined && { firstBuyAmount }), + ...(agentConfig.agentMint && { agentMint: agentConfig.agentMint }), + ...(agentConfig.agentSetToken !== undefined && { agentSetToken: agentConfig.agentSetToken }), } } diff --git a/src/lib/genesis/launchApi.ts b/src/lib/genesis/launchApi.ts index 272db99..b03a2fb 100644 --- a/src/lib/genesis/launchApi.ts +++ b/src/lib/genesis/launchApi.ts @@ -5,12 +5,20 @@ import { QuoteMintInput, SvmNetwork, } from '@metaplex-foundation/genesis' +import { PublicKeyInput } from '@metaplex-foundation/umi' import { detectSvmNetwork, RpcChain } from '../util.js' /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ +export interface AgentConfig { + /** Agent NFT mint address */ + mint: PublicKeyInput + /** Whether to set the token on the agent */ + setToken: boolean +} + export interface BuildLaunchInputParams { launchType: 'launchpool' | 'bondingCurve' token: { @@ -25,12 +33,30 @@ export interface BuildLaunchInputParams { telegram?: string } quoteMint?: string - depositStartTime: string // launchpool-specific + depositStartTime?: string tokenAllocation?: number raiseGoal?: number raydiumLiquidityBps?: number fundsRecipient?: string + // bonding-curve-specific + creatorFeeWallet?: string + firstBuyAmount?: number + // agent support + agent?: AgentConfig +} + +/* ------------------------------------------------------------------ */ +/* API URL helpers */ +/* ------------------------------------------------------------------ */ + +const API_URLS: Record = { + 'solana-mainnet': 'https://api.metaplex.com', + 'solana-devnet': 'https://api.metaplex.dev', +} + +export function getDefaultApiUrl(network: SvmNetwork): string { + return API_URLS[network] ?? API_URLS['solana-mainnet'] } /* ------------------------------------------------------------------ */ @@ -63,6 +89,7 @@ export function buildLaunchInput( ...(params.quoteMint && params.quoteMint !== 'SOL' && { quoteMint: params.quoteMint as QuoteMintInput, }), + ...(params.agent && { agent: params.agent }), } if (params.launchType === 'bondingCurve') { @@ -70,16 +97,15 @@ export function buildLaunchInput( ...common, launchType: 'bondingCurve', launch: { - bondingCurve: { - depositStartTime: params.depositStartTime, - }, + ...(params.creatorFeeWallet && { creatorFeeWallet: params.creatorFeeWallet }), + ...(params.firstBuyAmount !== undefined && params.firstBuyAmount > 0 && { firstBuyAmount: params.firstBuyAmount }), }, } satisfies CreateBondingCurveLaunchInput } // launchpool - if (!params.tokenAllocation || !params.raiseGoal || !params.raydiumLiquidityBps || !params.fundsRecipient) { - throw new Error('Launchpool requires tokenAllocation, raiseGoal, raydiumLiquidityBps, and fundsRecipient') + if (!params.tokenAllocation || !params.raiseGoal || !params.raydiumLiquidityBps || !params.fundsRecipient || !params.depositStartTime) { + throw new Error('Launchpool requires tokenAllocation, raiseGoal, raydiumLiquidityBps, fundsRecipient, and depositStartTime') } return { diff --git a/src/lib/genesis/wizard.ts b/src/lib/genesis/wizard.ts index 1a4710f..08c91d5 100644 --- a/src/lib/genesis/wizard.ts +++ b/src/lib/genesis/wizard.ts @@ -106,6 +106,12 @@ export async function runApiWizard( raiseGoal: wizardResult.raiseGoal, raydiumLiquidityBps: wizardResult.raydiumLiquidityBps, fundsRecipient: wizardResult.fundsRecipient, + creatorFeeWallet: wizardResult.creatorFeeWallet, + firstBuyAmount: wizardResult.firstBuyAmount, + agent: wizardResult.agentMint ? { + mint: wizardResult.agentMint, + setToken: wizardResult.agentSetToken ?? false, + } : undefined, }, ) diff --git a/test/commands/genesis/genesis.launch.test.ts b/test/commands/genesis/genesis.launch.test.ts index 81a43b4..d1aa29c 100644 --- a/test/commands/genesis/genesis.launch.test.ts +++ b/test/commands/genesis/genesis.launch.test.ts @@ -30,6 +30,21 @@ describe('genesis launch commands', () => { expect(msg).to.contain('name') expect(msg).to.contain('symbol') expect(msg).to.contain('image') + } + }) + + it('fails when depositStartTime is missing for launchpool', async () => { + try { + await runCli([ + 'genesis', 'launch', 'create', + '--name', 'My Token', + '--symbol', 'MTK', + '--image', 'https://gateway.irys.xyz/abc123', + ]) + expect.fail('Should have thrown an error for missing depositStartTime') + } catch (error) { + const msg = (error as Error).message + expect(msg).to.contain('Missing required flag') expect(msg).to.contain('depositStartTime') } }) @@ -39,7 +54,6 @@ describe('genesis launch commands', () => { name: ['--name', 'My Token'], symbol: ['--symbol', 'MTK'], image: ['--image', 'https://gateway.irys.xyz/abc123'], - depositStartTime: ['--depositStartTime', futureIso(7 * 86400)], } for (const omitted of Object.keys(sharedRequiredFlags)) { @@ -59,8 +73,9 @@ describe('genesis launch commands', () => { }) } - // Project-only flags validated at runtime + // Project-only flags validated at runtime (includes depositStartTime for launchpool) const projectOnlyFlags: Record = { + depositStartTime: ['--depositStartTime', futureIso(7 * 86400)], tokenAllocation: ['--tokenAllocation', '500000000'], raiseGoal: ['--raiseGoal', '200'], raydiumLiquidityBps: ['--raydiumLiquidityBps', '5000'], @@ -228,14 +243,13 @@ describe('genesis launch commands', () => { } }) - it.skip('bonding-curve launch only requires depositStartTime (reaches API call)', async () => { + it.skip('bonding-curve launch requires only name, symbol, image (reaches API call)', async () => { const cliInput = [ 'genesis', 'launch', 'create', '--launchType', 'bonding-curve', '--name', 'My Meme', '--symbol', 'MEME', '--image', 'https://gateway.irys.xyz/abc123', - '--depositStartTime', futureIso(30 * 86400), ] try { @@ -249,17 +263,38 @@ describe('genesis launch commands', () => { } }) - it.skip('bonding-curve launch with optional metadata reaches API call', async () => { + it.skip('bonding-curve launch with optional flags reaches API call', async () => { const cliInput = [ 'genesis', 'launch', 'create', '--launchType', 'bonding-curve', '--name', 'My Meme', '--symbol', 'MEME', '--image', 'https://gateway.irys.xyz/abc123', - '--depositStartTime', futureIso(30 * 86400), '--description', 'A bonding curve token for testing', '--twitter', 'https://x.com/mymeme', '--quoteMint', 'USDC', + '--creatorFeeWallet', 'TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx', + '--firstBuyAmount', '0.1', + ] + + try { + await runCli(cliInput) + expect.fail('Should have thrown an API error (API not available on localnet)') + } catch (error) { + const msg = (error as Error).message + expect(msg).to.contain('Failed') + } + }) + + it.skip('bonding-curve launch with agent flags reaches API call', async () => { + const cliInput = [ + 'genesis', 'launch', 'create', + '--launchType', 'bonding-curve', + '--name', 'Agent Token', + '--symbol', 'AGT', + '--image', 'https://gateway.irys.xyz/abc123', + '--agentMint', 'TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx', + '--agentSetToken', ] try { @@ -278,7 +313,6 @@ describe('genesis launch commands', () => { '--name', 'My Meme', '--symbol', 'MEME', '--image', 'https://gateway.irys.xyz/abc123', - '--depositStartTime', futureIso(30 * 86400), '--tokenAllocation', '500000000', ] @@ -291,6 +325,49 @@ describe('genesis launch commands', () => { expect(msg).to.contain('--tokenAllocation') } }) + + it('bonding-curve launch rejects depositStartTime', async () => { + const cliInput = [ + 'genesis', 'launch', 'create', + '--launchType', 'bonding-curve', + '--name', 'My Meme', + '--symbol', 'MEME', + '--image', 'https://gateway.irys.xyz/abc123', + '--depositStartTime', futureIso(30 * 86400), + ] + + try { + await runCli(cliInput) + expect.fail('Should have thrown an error for disallowed flags') + } catch (error) { + const msg = (error as Error).message + expect(msg).to.contain('not allowed for bonding-curve') + expect(msg).to.contain('--depositStartTime') + } + }) + + it('rejects --agentSetToken without --agentMint', async () => { + const cliInput = [ + 'genesis', 'launch', 'create', + '--name', 'My Token', + '--symbol', 'MTK', + '--image', 'https://gateway.irys.xyz/abc123', + '--depositStartTime', futureIso(7 * 86400), + '--tokenAllocation', '500000000', + '--raiseGoal', '200', + '--raydiumLiquidityBps', '5000', + '--fundsRecipient', 'TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx', + '--agentSetToken', + ] + + try { + await runCli(cliInput) + expect.fail('Should have thrown an error for agentSetToken without agentMint') + } catch (error) { + const msg = (error as Error).message + expect(msg).to.contain('--agentSetToken requires --agentMint') + } + }) }) describe('genesis launch register', () => { @@ -427,9 +504,7 @@ describe('genesis launch commands', () => { token: { name: 'Meme', symbol: 'MEME', image: 'https://gateway.irys.xyz/abc' }, launchType: 'bondingCurve', launch: { - bondingCurve: { - depositStartTime: futureIso(30 * 86400), - }, + creatorFeeWallet: 'TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx', }, }))