diff --git a/.amman/genesis.so b/.amman/genesis.so new file mode 100644 index 0000000..61f9816 Binary files /dev/null and b/.amman/genesis.so differ diff --git a/.validator.cjs b/.validator.cjs index f92ddc7..fd7a403 100644 --- a/.validator.cjs +++ b/.validator.cjs @@ -52,6 +52,11 @@ module.exports = { programId: 'cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK', deployPath: './.amman/spl_account_compression.so', }, + { + label: 'Genesis', + programId: 'GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B', + deployPath: './.amman/genesis.so', + }, ], commitment: 'processed', }, diff --git a/package.json b/package.json index dd27405..803e6af 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,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.18.0", "@metaplex-foundation/mpl-bubblegum": "^5.0.2", "@metaplex-foundation/mpl-core": "^1.4.0", "@metaplex-foundation/mpl-core-candy-machine": "^0.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1fa309..a316d21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@ardrive/turbo-sdk': specifier: ^1.25.0 - version: 1.25.0(typescript@5.8.3) + version: 1.25.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) '@inquirer/prompts': specifier: ^7.5.3 version: 7.5.3(@types/node@18.19.108) @@ -29,6 +29,9 @@ importers: '@metaplex-foundation/digital-asset-standard-api': specifier: ^2.0.0 version: 2.0.0(@metaplex-foundation/umi@1.4.1) + '@metaplex-foundation/genesis': + specifier: ^0.18.0 + version: 0.18.0(@metaplex-foundation/umi@1.4.1) '@metaplex-foundation/mpl-bubblegum': specifier: ^5.0.2 version: 5.0.2(@metaplex-foundation/umi@1.4.1) @@ -37,10 +40,10 @@ importers: version: 1.4.0(@metaplex-foundation/umi@1.4.1)(@noble/hashes@1.8.0) '@metaplex-foundation/mpl-core-candy-machine': specifier: ^0.3.0 - version: 0.3.0(@metaplex-foundation/mpl-core@1.4.0)(@metaplex-foundation/umi@1.4.1) + version: 0.3.0(@metaplex-foundation/mpl-core@1.4.0(@metaplex-foundation/umi@1.4.1)(@noble/hashes@1.8.0))(@metaplex-foundation/umi@1.4.1) '@metaplex-foundation/mpl-distro': specifier: ^0.3.2 - version: 0.3.4(@metaplex-foundation/mpl-core@1.4.0)(@metaplex-foundation/mpl-toolbox@0.10.0)(@metaplex-foundation/umi@1.4.1) + version: 0.3.4(@metaplex-foundation/mpl-core@1.4.0(@metaplex-foundation/umi@1.4.1)(@noble/hashes@1.8.0))(@metaplex-foundation/mpl-toolbox@0.10.0(@metaplex-foundation/umi@1.4.1))(@metaplex-foundation/umi@1.4.1) '@metaplex-foundation/mpl-token-metadata': specifier: ^3.4.0 version: 3.4.0(@metaplex-foundation/umi@1.4.1) @@ -52,22 +55,22 @@ importers: version: 1.4.1 '@metaplex-foundation/umi-bundle-defaults': specifier: 1.0.0 - version: 1.0.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) + version: 1.0.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) '@metaplex-foundation/umi-signer-wallet-adapters': specifier: 1.0.0 - version: 1.0.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) + version: 1.0.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) '@metaplex-foundation/umi-uploader-irys': specifier: 1.0.1 - version: 1.0.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2)(arweave@1.15.7)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + version: 1.0.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(arweave@1.15.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) '@metaplex-foundation/umi-web3js-adapters': specifier: 1.2.0 - version: 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) + version: 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) '@oclif/plugin-autocomplete': specifier: ^3.2.29 version: 3.2.29 '@oclif/plugin-commands': specifier: ^4.1.25 - version: 4.1.25 + version: 4.1.25(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@oclif/plugin-help': specifier: ^6.2.28 version: 6.2.28 @@ -82,7 +85,7 @@ importers: version: 2.2.28 '@solana/web3.js': specifier: ^1.98.2 - version: 1.98.2(typescript@5.8.3) + version: 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) ansis: specifier: ^3.17.0 version: 3.17.0 @@ -116,10 +119,10 @@ importers: devDependencies: '@metaplex-foundation/amman': specifier: ^0.12.1 - version: 0.12.1(typescript@5.8.3) + version: 0.12.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) '@metaplex-foundation/amman-client': specifier: ^0.2.4 - version: 0.2.4(typescript@5.8.3) + version: 0.2.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) '@oclif/core': specifier: ^4.3.0 version: 4.3.0 @@ -839,6 +842,11 @@ packages: peerDependencies: '@metaplex-foundation/umi': '>= 0.8.2 <= 1' + '@metaplex-foundation/genesis@0.18.0': + resolution: {integrity: sha512-Uzj9PYiW+6JkAGu1Kjhu5jm8p0+dSSApyIEbtfhu4FgIA9S/apx9rbt2mNL+0MgIghbQTj2zh0ooFxx6YmP6mQ==} + peerDependencies: + '@metaplex-foundation/umi': ^1.4.1 + '@metaplex-foundation/mpl-account-compression@0.0.1': resolution: {integrity: sha512-bzuftabEduBPBzib9b7NESsX051bREOnhrKSl7ZIQ32+68lxcqQCXg7ChB1F+iJ/Nzfij5HtFL+ersbb2B3wkg==} peerDependencies: @@ -5041,17 +5049,17 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 4.0.0 - '@ardrive/turbo-sdk@1.25.0(typescript@5.8.3)': + '@ardrive/turbo-sdk@1.25.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@cosmjs/amino': 0.32.4 '@cosmjs/crypto': 0.32.4 '@cosmjs/encoding': 0.32.4 '@cosmjs/proto-signing': 0.32.4 - '@dha-team/arbundles': 1.0.3 + '@dha-team/arbundles': 1.0.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@ethersproject/signing-key': 5.8.0 - '@kyvejs/sdk': 1.4.5(bitcoinjs-lib@6.1.7)(starknet@6.24.1) + '@kyvejs/sdk': 1.4.5(bitcoinjs-lib@6.1.7)(bufferutil@4.0.9)(starknet@6.24.1)(utf-8-validate@5.0.10) '@permaweb/aoconnect': 0.0.57 - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) arweave: 1.15.7 axios: 1.4.0 axios-retry: 3.9.1 @@ -5059,7 +5067,7 @@ snapshots: bitcoinjs-lib: 6.1.7 bs58: 5.0.0 commander: 12.1.0 - ethers: 6.14.3 + ethers: 6.14.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) mime-types: 2.1.35 plimit-lit: 3.0.1 prompts: 2.4.2 @@ -5640,17 +5648,17 @@ snapshots: '@cosmjs/utils': 0.32.4 cosmjs-types: 0.9.0 - '@cosmjs/socket@0.32.4': + '@cosmjs/socket@0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@cosmjs/stream': 0.32.4 - isomorphic-ws: 4.0.1(ws@7.5.10) - ws: 7.5.10 + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) xstream: 11.14.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@cosmjs/stargate@0.32.4': + '@cosmjs/stargate@0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@confio/ics23': 0.6.8 '@cosmjs/amino': 0.32.4 @@ -5658,7 +5666,7 @@ snapshots: '@cosmjs/math': 0.32.4 '@cosmjs/proto-signing': 0.32.4 '@cosmjs/stream': 0.32.4 - '@cosmjs/tendermint-rpc': 0.32.4 + '@cosmjs/tendermint-rpc': 0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@cosmjs/utils': 0.32.4 cosmjs-types: 0.9.0 xstream: 11.14.0 @@ -5671,13 +5679,13 @@ snapshots: dependencies: xstream: 11.14.0 - '@cosmjs/tendermint-rpc@0.32.4': + '@cosmjs/tendermint-rpc@0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@cosmjs/crypto': 0.32.4 '@cosmjs/encoding': 0.32.4 '@cosmjs/json-rpc': 0.32.4 '@cosmjs/math': 0.32.4 - '@cosmjs/socket': 0.32.4 + '@cosmjs/socket': 0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@cosmjs/stream': 0.32.4 '@cosmjs/utils': 0.32.4 axios: 1.9.0 @@ -5702,11 +5710,11 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 - '@dha-team/arbundles@1.0.3': + '@dha-team/arbundles@1.0.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/hash': 5.8.0 - '@ethersproject/providers': 5.8.0 + '@ethersproject/providers': 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@ethersproject/signing-key': 5.8.0 '@ethersproject/transactions': 5.8.0 '@ethersproject/wallet': 5.8.0 @@ -5886,7 +5894,7 @@ snapshots: dependencies: '@ethersproject/logger': 5.8.0 - '@ethersproject/providers@5.8.0': + '@ethersproject/providers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@ethersproject/abstract-provider': 5.8.0 '@ethersproject/abstract-signer': 5.8.0 @@ -5907,7 +5915,7 @@ snapshots: '@ethersproject/transactions': 5.8.0 '@ethersproject/web': 5.8.0 bech32: 1.1.4 - ws: 8.18.0 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -6014,9 +6022,10 @@ snapshots: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/figures': 1.0.12 '@inquirer/type': 3.0.7(@types/node@18.19.108) - '@types/node': 18.19.108 ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 18.19.108 '@inquirer/confirm@3.2.0': dependencies: @@ -6027,19 +6036,21 @@ snapshots: dependencies: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/type': 3.0.7(@types/node@18.19.108) + optionalDependencies: '@types/node': 18.19.108 '@inquirer/core@10.1.13(@types/node@18.19.108)': dependencies: '@inquirer/figures': 1.0.12 '@inquirer/type': 3.0.7(@types/node@18.19.108) - '@types/node': 18.19.108 ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 18.19.108 '@inquirer/core@9.2.1': dependencies: @@ -6060,15 +6071,17 @@ snapshots: dependencies: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/type': 3.0.7(@types/node@18.19.108) - '@types/node': 18.19.108 external-editor: 3.1.0 + optionalDependencies: + '@types/node': 18.19.108 '@inquirer/expand@4.0.15(@types/node@18.19.108)': dependencies: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/type': 3.0.7(@types/node@18.19.108) - '@types/node': 18.19.108 yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 18.19.108 '@inquirer/figures@1.0.12': {} @@ -6081,20 +6094,23 @@ snapshots: dependencies: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/type': 3.0.7(@types/node@18.19.108) + optionalDependencies: '@types/node': 18.19.108 '@inquirer/number@3.0.15(@types/node@18.19.108)': dependencies: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/type': 3.0.7(@types/node@18.19.108) + optionalDependencies: '@types/node': 18.19.108 '@inquirer/password@4.0.15(@types/node@18.19.108)': dependencies: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/type': 3.0.7(@types/node@18.19.108) - '@types/node': 18.19.108 ansi-escapes: 4.3.2 + optionalDependencies: + '@types/node': 18.19.108 '@inquirer/prompts@7.5.3(@types/node@18.19.108)': dependencies: @@ -6108,22 +6124,25 @@ snapshots: '@inquirer/rawlist': 4.1.3(@types/node@18.19.108) '@inquirer/search': 3.0.15(@types/node@18.19.108) '@inquirer/select': 4.2.3(@types/node@18.19.108) + optionalDependencies: '@types/node': 18.19.108 '@inquirer/rawlist@4.1.3(@types/node@18.19.108)': dependencies: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/type': 3.0.7(@types/node@18.19.108) - '@types/node': 18.19.108 yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 18.19.108 '@inquirer/search@3.0.15(@types/node@18.19.108)': dependencies: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/figures': 1.0.12 '@inquirer/type': 3.0.7(@types/node@18.19.108) - '@types/node': 18.19.108 yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 18.19.108 '@inquirer/select@2.5.0': dependencies: @@ -6138,16 +6157,18 @@ snapshots: '@inquirer/core': 10.1.13(@types/node@18.19.108) '@inquirer/figures': 1.0.12 '@inquirer/type': 3.0.7(@types/node@18.19.108) - '@types/node': 18.19.108 ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 18.19.108 '@inquirer/testing@2.1.47(@types/node@18.19.108)': dependencies: '@inquirer/type': 3.0.7(@types/node@18.19.108) - '@types/node': 18.19.108 ansi-escapes: 4.3.2 mute-stream: 2.0.0 + optionalDependencies: + '@types/node': 18.19.108 '@inquirer/type@1.5.5': dependencies: @@ -6158,7 +6179,7 @@ snapshots: mute-stream: 1.0.0 '@inquirer/type@3.0.7(@types/node@18.19.108)': - dependencies: + optionalDependencies: '@types/node': 18.19.108 '@irys/arweave@0.0.2': @@ -6171,11 +6192,11 @@ snapshots: transitivePeerDependencies: - debug - '@irys/bundles@0.0.1(arweave@1.15.7)': + '@irys/bundles@0.0.1(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/hash': 5.8.0 - '@ethersproject/providers': 5.8.0 + '@ethersproject/providers': 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@ethersproject/signing-key': 5.8.0 '@ethersproject/transactions': 5.8.0 '@ethersproject/wallet': 5.8.0 @@ -6198,11 +6219,11 @@ snapshots: - encoding - utf-8-validate - '@irys/bundles@0.0.3(arweave@1.15.7)': + '@irys/bundles@0.0.3(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@ethersproject/bytes': 5.8.0 '@ethersproject/hash': 5.8.0 - '@ethersproject/providers': 5.8.0 + '@ethersproject/providers': 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@ethersproject/signing-key': 5.8.0 '@ethersproject/transactions': 5.8.0 '@ethersproject/wallet': 5.8.0 @@ -6233,9 +6254,9 @@ snapshots: transitivePeerDependencies: - debug - '@irys/upload-core@0.0.10(arweave@1.15.7)': + '@irys/upload-core@0.0.10(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: - '@irys/bundles': 0.0.3(arweave@1.15.7) + '@irys/bundles': 0.0.3(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@irys/query': 0.0.9 '@supercharge/promise-pool': 3.2.0 async-retry: 1.3.3 @@ -6249,9 +6270,9 @@ snapshots: - encoding - utf-8-validate - '@irys/upload-core@0.0.9(arweave@1.15.7)': + '@irys/upload-core@0.0.9(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: - '@irys/bundles': 0.0.1(arweave@1.15.7) + '@irys/bundles': 0.0.1(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@irys/query': 0.0.9 '@supercharge/promise-pool': 3.2.0 async-retry: 1.3.3 @@ -6265,13 +6286,13 @@ snapshots: - encoding - utf-8-validate - '@irys/upload-solana@0.1.8(arweave@1.15.7)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@irys/upload-solana@0.1.8(arweave@1.15.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: - '@irys/bundles': 0.0.3(arweave@1.15.7) - '@irys/upload': 0.0.15(arweave@1.15.7) - '@irys/upload-core': 0.0.10(arweave@1.15.7) - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@irys/bundles': 0.0.3(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/upload': 0.0.15(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/upload-core': 0.0.10(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) async-retry: 1.3.3 bignumber.js: 9.3.0 bs58: 5.0.0 @@ -6285,10 +6306,10 @@ snapshots: - typescript - utf-8-validate - '@irys/upload@0.0.14(arweave@1.15.7)': + '@irys/upload@0.0.14(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: - '@irys/bundles': 0.0.1(arweave@1.15.7) - '@irys/upload-core': 0.0.9(arweave@1.15.7) + '@irys/bundles': 0.0.1(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/upload-core': 0.0.9(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) async-retry: 1.3.3 axios: 1.9.0 base64url: 3.0.1 @@ -6304,10 +6325,10 @@ snapshots: - encoding - utf-8-validate - '@irys/upload@0.0.15(arweave@1.15.7)': + '@irys/upload@0.0.15(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: - '@irys/bundles': 0.0.3(arweave@1.15.7) - '@irys/upload-core': 0.0.10(arweave@1.15.7) + '@irys/bundles': 0.0.3(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/upload-core': 0.0.10(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) async-retry: 1.3.3 axios: 1.9.0 base64url: 3.0.1 @@ -6323,13 +6344,13 @@ snapshots: - encoding - utf-8-validate - '@irys/web-upload-solana@0.1.8(arweave@1.15.7)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@irys/web-upload-solana@0.1.8(arweave@1.15.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: - '@irys/bundles': 0.0.3(arweave@1.15.7) - '@irys/upload-core': 0.0.10(arweave@1.15.7) - '@irys/web-upload': 0.0.15(arweave@1.15.7) - '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@irys/bundles': 0.0.3(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/upload-core': 0.0.10(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/web-upload': 0.0.15(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/spl-token': 0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) async-retry: 1.3.3 bignumber.js: 9.3.0 bs58: 5.0.0 @@ -6343,10 +6364,10 @@ snapshots: - typescript - utf-8-validate - '@irys/web-upload@0.0.14(arweave@1.15.7)': + '@irys/web-upload@0.0.14(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: - '@irys/bundles': 0.0.1(arweave@1.15.7) - '@irys/upload-core': 0.0.9(arweave@1.15.7) + '@irys/bundles': 0.0.1(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/upload-core': 0.0.9(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) async-retry: 1.3.3 axios: 1.9.0 base64url: 3.0.1 @@ -6359,10 +6380,10 @@ snapshots: - encoding - utf-8-validate - '@irys/web-upload@0.0.15(arweave@1.15.7)': + '@irys/web-upload@0.0.15(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: - '@irys/bundles': 0.0.3(arweave@1.15.7) - '@irys/upload-core': 0.0.10(arweave@1.15.7) + '@irys/bundles': 0.0.3(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/upload-core': 0.0.10(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) async-retry: 1.3.3 axios: 1.9.0 base64url: 3.0.1 @@ -6443,18 +6464,18 @@ snapshots: transitivePeerDependencies: - starknet - '@kyvejs/sdk@1.4.5(bitcoinjs-lib@6.1.7)(starknet@6.24.1)': + '@kyvejs/sdk@1.4.5(bitcoinjs-lib@6.1.7)(bufferutil@4.0.9)(starknet@6.24.1)(utf-8-validate@5.0.10)': dependencies: '@cosmjs/amino': 0.32.4 '@cosmjs/crypto': 0.32.4 '@cosmjs/encoding': 0.32.4 '@cosmjs/math': 0.32.4 '@cosmjs/proto-signing': 0.32.4 - '@cosmjs/stargate': 0.32.4 + '@cosmjs/stargate': 0.32.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@cosmostation/extension-client': 0.1.15 '@keplr-wallet/cosmos': 0.12.237(bitcoinjs-lib@6.1.7)(starknet@6.24.1) '@kyvejs/types': 1.5.0 - axios: 0.27.2 + axios: 0.27.2(debug@4.3.4) bech32: 2.0.0 bignumber.js: 9.1.2 cosmjs-types: 0.9.0 @@ -6515,15 +6536,15 @@ snapshots: '@ledgerhq/logs@6.12.0': {} - '@metaplex-foundation/amman-client@0.2.4(typescript@5.8.3)': + '@metaplex-foundation/amman-client@0.2.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@metaplex-foundation/cusper': 0.0.2 '@solana/spl-token-registry': 0.2.4574 - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) bn.js: 5.2.2 debug: 4.4.1(supports-color@8.1.1) js-sha3: 0.8.0 - socket.io-client: 4.8.1 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) tweetnacl: 1.0.3 transitivePeerDependencies: - bufferutil @@ -6532,11 +6553,11 @@ snapshots: - typescript - utf-8-validate - '@metaplex-foundation/amman@0.12.1(typescript@5.8.3)': + '@metaplex-foundation/amman@0.12.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: - '@metaplex-foundation/amman-client': 0.2.4(typescript@5.8.3) - '@solana/spl-token': 0.2.0(typescript@5.8.3) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@metaplex-foundation/amman-client': 0.2.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/spl-token': 0.2.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) ansi-colors: 4.1.3 bn.js: 5.2.2 buffer-hexdump: 1.0.0 @@ -6546,7 +6567,7 @@ snapshots: diff: 5.2.0 numeral: 2.0.6 port-pid: 0.0.7 - socket.io: 4.8.1 + socket.io: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) text-table: 0.2.0 timeago.js: 4.0.2 ts-essentials: 9.4.2(typescript@5.8.3) @@ -6569,6 +6590,13 @@ snapshots: dependencies: '@metaplex-foundation/umi': 1.4.1 + '@metaplex-foundation/genesis@0.18.0(@metaplex-foundation/umi@1.4.1)': + dependencies: + '@metaplex-foundation/mpl-token-metadata': 3.4.0(@metaplex-foundation/umi@1.4.1) + '@metaplex-foundation/mpl-toolbox': 0.10.0(@metaplex-foundation/umi@1.4.1) + '@metaplex-foundation/umi': 1.4.1 + '@noble/hashes': 1.8.0 + '@metaplex-foundation/mpl-account-compression@0.0.1(@metaplex-foundation/umi@1.4.1)': dependencies: '@metaplex-foundation/digital-asset-standard-api': 1.0.6(@metaplex-foundation/umi@1.4.1) @@ -6587,7 +6615,7 @@ snapshots: '@noble/hashes': 1.8.0 merkletreejs: 0.3.11 - '@metaplex-foundation/mpl-core-candy-machine@0.3.0(@metaplex-foundation/mpl-core@1.4.0)(@metaplex-foundation/umi@1.4.1)': + '@metaplex-foundation/mpl-core-candy-machine@0.3.0(@metaplex-foundation/mpl-core@1.4.0(@metaplex-foundation/umi@1.4.1)(@noble/hashes@1.8.0))(@metaplex-foundation/umi@1.4.1)': dependencies: '@metaplex-foundation/mpl-core': 1.4.0(@metaplex-foundation/umi@1.4.1)(@noble/hashes@1.8.0) '@metaplex-foundation/mpl-token-metadata': 3.0.0-alpha.27(@metaplex-foundation/umi@1.4.1) @@ -6602,7 +6630,7 @@ snapshots: '@msgpack/msgpack': 3.1.2 '@noble/hashes': 1.8.0 - '@metaplex-foundation/mpl-distro@0.3.4(@metaplex-foundation/mpl-core@1.4.0)(@metaplex-foundation/mpl-toolbox@0.10.0)(@metaplex-foundation/umi@1.4.1)': + '@metaplex-foundation/mpl-distro@0.3.4(@metaplex-foundation/mpl-core@1.4.0(@metaplex-foundation/umi@1.4.1)(@noble/hashes@1.8.0))(@metaplex-foundation/mpl-toolbox@0.10.0(@metaplex-foundation/umi@1.4.1))(@metaplex-foundation/umi@1.4.1)': dependencies: '@metaplex-foundation/mpl-core': 1.4.0(@metaplex-foundation/umi@1.4.1)(@noble/hashes@1.8.0) '@metaplex-foundation/mpl-toolbox': 0.10.0(@metaplex-foundation/umi@1.4.1) @@ -6639,18 +6667,18 @@ snapshots: '@noble/hashes': 1.8.0 merkletreejs: 0.3.11 - '@metaplex-foundation/umi-bundle-defaults@1.0.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2)': + '@metaplex-foundation/umi-bundle-defaults@1.0.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@metaplex-foundation/umi': 1.4.1 '@metaplex-foundation/umi-downloader-http': 1.2.0(@metaplex-foundation/umi@1.4.1) - '@metaplex-foundation/umi-eddsa-web3js': 1.1.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) + '@metaplex-foundation/umi-eddsa-web3js': 1.1.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) '@metaplex-foundation/umi-http-fetch': 1.2.0(@metaplex-foundation/umi@1.4.1) '@metaplex-foundation/umi-program-repository': 1.2.0(@metaplex-foundation/umi@1.4.1) '@metaplex-foundation/umi-rpc-chunk-get-accounts': 1.2.0(@metaplex-foundation/umi@1.4.1) - '@metaplex-foundation/umi-rpc-web3js': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) + '@metaplex-foundation/umi-rpc-web3js': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) '@metaplex-foundation/umi-serializer-data-view': 1.2.0(@metaplex-foundation/umi@1.4.1) - '@metaplex-foundation/umi-transaction-factory-web3js': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@metaplex-foundation/umi-transaction-factory-web3js': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - encoding @@ -6658,12 +6686,12 @@ snapshots: dependencies: '@metaplex-foundation/umi': 1.4.1 - '@metaplex-foundation/umi-eddsa-web3js@1.1.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2)': + '@metaplex-foundation/umi-eddsa-web3js@1.1.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@metaplex-foundation/umi': 1.4.1 - '@metaplex-foundation/umi-web3js-adapters': 1.1.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) + '@metaplex-foundation/umi-web3js-adapters': 1.1.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) '@noble/curves': 1.9.1 - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) yaml: 2.8.0 '@metaplex-foundation/umi-http-fetch@1.2.0(@metaplex-foundation/umi@1.4.1)': @@ -6687,11 +6715,11 @@ snapshots: dependencies: '@metaplex-foundation/umi': 1.4.1 - '@metaplex-foundation/umi-rpc-web3js@1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2)': + '@metaplex-foundation/umi-rpc-web3js@1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@metaplex-foundation/umi': 1.4.1 - '@metaplex-foundation/umi-web3js-adapters': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@metaplex-foundation/umi-web3js-adapters': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) '@metaplex-foundation/umi-serializer-data-view@1.2.0(@metaplex-foundation/umi@1.4.1)': dependencies: @@ -6715,27 +6743,27 @@ snapshots: '@metaplex-foundation/umi-serializers-encodings': 1.4.1 '@metaplex-foundation/umi-serializers-numbers': 1.4.1 - '@metaplex-foundation/umi-signer-wallet-adapters@1.0.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2)': + '@metaplex-foundation/umi-signer-wallet-adapters@1.0.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@metaplex-foundation/umi': 1.4.1 - '@metaplex-foundation/umi-web3js-adapters': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@metaplex-foundation/umi-web3js-adapters': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@metaplex-foundation/umi-transaction-factory-web3js@1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2)': + '@metaplex-foundation/umi-transaction-factory-web3js@1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@metaplex-foundation/umi': 1.4.1 - '@metaplex-foundation/umi-web3js-adapters': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@metaplex-foundation/umi-web3js-adapters': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@metaplex-foundation/umi-uploader-irys@1.0.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2)(arweave@1.15.7)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@metaplex-foundation/umi-uploader-irys@1.0.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(arweave@1.15.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: - '@irys/upload': 0.0.14(arweave@1.15.7) - '@irys/upload-solana': 0.1.8(arweave@1.15.7)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@irys/web-upload': 0.0.14(arweave@1.15.7) - '@irys/web-upload-solana': 0.1.8(arweave@1.15.7)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@irys/upload': 0.0.14(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/upload-solana': 0.1.8(arweave@1.15.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@irys/web-upload': 0.0.14(arweave@1.15.7)(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@irys/web-upload-solana': 0.1.8(arweave@1.15.7)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) '@metaplex-foundation/umi': 1.4.1 - '@metaplex-foundation/umi-web3js-adapters': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@metaplex-foundation/umi-web3js-adapters': 1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) '@supercharge/promise-pool': 3.2.0 bignumber.js: 9.3.0 buffer: 6.0.3 @@ -6748,16 +6776,16 @@ snapshots: - typescript - utf-8-validate - '@metaplex-foundation/umi-web3js-adapters@1.1.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2)': + '@metaplex-foundation/umi-web3js-adapters@1.1.1(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@metaplex-foundation/umi': 1.4.1 - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) buffer: 6.0.3 - '@metaplex-foundation/umi-web3js-adapters@1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2)': + '@metaplex-foundation/umi-web3js-adapters@1.2.0(@metaplex-foundation/umi@1.4.1)(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))': dependencies: '@metaplex-foundation/umi': 1.4.1 - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) buffer: 6.0.3 '@metaplex-foundation/umi@1.4.1': @@ -6845,10 +6873,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@oclif/plugin-commands@4.1.25': + '@oclif/plugin-commands@4.1.25(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@oclif/core': 4.3.0 - '@oclif/table': 0.4.8 + '@oclif/table': 0.4.8(bufferutil@4.0.9)(utf-8-validate@5.0.10) lodash: 4.17.21 object-treeify: 4.0.1 transitivePeerDependencies: @@ -6903,12 +6931,12 @@ snapshots: '@oclif/prettier-config@0.2.1': {} - '@oclif/table@0.4.8': + '@oclif/table@0.4.8(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@types/react': 18.3.23 change-case: 5.4.4 cli-truncate: 4.0.0 - ink: 5.0.1(@types/react@18.3.23)(react@18.3.1) + ink: 5.0.1(@types/react@18.3.23)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10) natural-orderby: 3.0.2 object-hash: 3.0.0 react: 18.3.1 @@ -7356,10 +7384,10 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@solana/buffer-layout-utils@0.2.0(typescript@5.8.3)': + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) bigint-buffer: 1.1.5 bignumber.js: 9.3.0 transitivePeerDependencies: @@ -7443,18 +7471,18 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.2)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - typescript - '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.2)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - typescript @@ -7463,11 +7491,11 @@ snapshots: dependencies: cross-fetch: 3.0.6 - '@solana/spl-token@0.2.0(typescript@5.8.3)': + '@solana/spl-token@0.2.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/buffer-layout-utils': 0.2.0(typescript@5.8.3) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) start-server-and-test: 1.15.4 transitivePeerDependencies: - bufferutil @@ -7476,13 +7504,13 @@ snapshots: - typescript - utf-8-validate - '@solana/spl-token@0.4.13(@solana/web3.js@1.98.2)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)': + '@solana/spl-token@0.4.13(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/buffer-layout-utils': 0.2.0(typescript@5.8.3) - '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.2)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.2)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/web3.js': 1.98.2(typescript@5.8.3) + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/web3.js': 1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) buffer: 6.0.3 transitivePeerDependencies: - bufferutil @@ -7491,7 +7519,7 @@ snapshots: - typescript - utf-8-validate - '@solana/web3.js@1.98.2(typescript@5.8.3)': + '@solana/web3.js@1.98.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.27.3 '@noble/curves': 1.9.1 @@ -7504,7 +7532,7 @@ snapshots: bs58: 4.0.1 buffer: 6.0.3 fast-stable-stringify: 1.0.0 - jayson: 4.2.0 + jayson: 4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) node-fetch: 2.7.0 rpc-websockets: 9.1.1 superstruct: 2.0.2 @@ -7636,7 +7664,7 @@ snapshots: dependencies: '@types/node': 18.19.108 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) @@ -7651,6 +7679,7 @@ snapshots: natural-compare: 1.4.0 semver: 7.7.2 ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7663,6 +7692,7 @@ snapshots: '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.1 + optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7684,6 +7714,7 @@ snapshots: debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7702,6 +7733,7 @@ snapshots: minimatch: 9.0.3 semver: 7.7.2 ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -7716,6 +7748,7 @@ snapshots: minimatch: 9.0.5 semver: 7.7.2 ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8004,13 +8037,6 @@ snapshots: transitivePeerDependencies: - debug - axios@0.27.2: - dependencies: - follow-redirects: 1.15.9(debug@4.4.1) - form-data: 4.0.2 - transitivePeerDependencies: - - debug - axios@0.27.2(debug@4.3.4): dependencies: follow-redirects: 1.15.9(debug@4.3.4) @@ -8020,7 +8046,7 @@ snapshots: axios@1.4.0: dependencies: - follow-redirects: 1.15.9(debug@4.4.1) + follow-redirects: 1.15.9(debug@4.3.4) form-data: 4.0.2 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -8028,7 +8054,7 @@ snapshots: axios@1.9.0: dependencies: - follow-redirects: 1.15.9(debug@4.4.1) + follow-redirects: 1.15.9(debug@4.3.4) form-data: 4.0.2 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -8170,6 +8196,7 @@ snapshots: bufferutil@4.0.9: dependencies: node-gyp-build: 4.8.4 + optional: true builtin-modules@3.3.0: {} @@ -8492,6 +8519,7 @@ snapshots: debug@4.4.1(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: supports-color: 8.1.1 decamelize@4.0.0: {} @@ -8599,12 +8627,12 @@ snapshots: dependencies: once: 1.4.0 - engine.io-client@6.6.3: + engine.io-client@6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 debug: 4.3.7 engine.io-parser: 5.2.3 - ws: 8.17.1 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil @@ -8613,7 +8641,7 @@ snapshots: engine.io-parser@5.2.3: {} - engine.io@6.6.4: + engine.io@6.6.4(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@types/cors': 2.8.18 '@types/node': 18.19.108 @@ -8623,7 +8651,7 @@ snapshots: cors: 2.8.5 debug: 4.3.7 engine.io-parser: 5.2.3 - ws: 8.17.1 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - supports-color @@ -8733,11 +8761,11 @@ snapshots: eslint-config-oclif-typescript@3.1.14(eslint@8.57.1)(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) eslint-config-xo-space: 0.35.0(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 15.7.0(eslint@8.57.1) eslint-plugin-perfectionist: 2.11.0(eslint@8.57.1)(typescript@5.8.3) @@ -8788,19 +8816,21 @@ snapshots: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1(supports-color@8.1.1) eslint: 8.57.1 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.14 unrs-resolver: 1.7.8 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1) @@ -8813,10 +8843,9 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 @@ -8825,7 +8854,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -8836,6 +8865,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -8983,7 +9014,7 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 - ethers@6.14.3: + ethers@6.14.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@adraffy/ens-normalize': 1.10.1 '@noble/curves': 1.2.0 @@ -8991,7 +9022,7 @@ snapshots: '@types/node': 22.7.5 aes-js: 4.0.0-beta.5 tslib: 2.7.0 - ws: 8.17.1 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -9073,7 +9104,7 @@ snapshots: reusify: 1.1.0 fdir@6.4.5(picomatch@4.0.2): - dependencies: + optionalDependencies: picomatch: 4.0.2 fecha@4.2.3: {} @@ -9144,11 +9175,11 @@ snapshots: fn.name@1.1.0: {} follow-redirects@1.15.9(debug@4.3.4): - dependencies: + optionalDependencies: debug: 4.3.4 follow-redirects@1.15.9(debug@4.4.1): - dependencies: + optionalDependencies: debug: 4.4.1(supports-color@8.1.1) for-each@0.3.5: @@ -9424,10 +9455,9 @@ snapshots: ini@1.3.8: {} - ink@5.0.1(@types/react@18.3.23)(react@18.3.1): + ink@5.0.1(@types/react@18.3.23)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10): dependencies: '@alcalzone/ansi-tokenize': 0.1.3 - '@types/react': 18.3.23 ansi-escapes: 7.0.0 ansi-styles: 6.2.1 auto-bind: 5.0.1 @@ -9452,6 +9482,8 @@ snapshots: wrap-ansi: 9.0.0 ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) yoga-wasm-web: 0.3.3 + optionalDependencies: + '@types/react': 18.3.23 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -9660,9 +9692,9 @@ snapshots: transitivePeerDependencies: - encoding - isomorphic-ws@4.0.1(ws@7.5.10): + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: - ws: 7.5.10 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) jake@10.9.2: dependencies: @@ -9671,7 +9703,7 @@ snapshots: filelist: 1.0.4 minimatch: 3.1.2 - jayson@4.2.0: + jayson@4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@types/connect': 3.4.38 '@types/node': 12.20.55 @@ -9680,11 +9712,11 @@ snapshots: delay: 5.0.0 es6-promisify: 5.0.0 eyes: 0.1.8 - isomorphic-ws: 4.0.1(ws@7.5.10) + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) json-stringify-safe: 5.0.1 stream-json: 1.9.1 uuid: 8.3.2 - ws: 7.5.10 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -10682,20 +10714,20 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - socket.io-adapter@2.5.5: + socket.io-adapter@2.5.5(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: debug: 4.3.7 - ws: 8.17.1 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-client@4.8.1: + socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 debug: 4.3.7 - engine.io-client: 6.6.3 + engine.io-client: 6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) socket.io-parser: 4.2.4 transitivePeerDependencies: - bufferutil @@ -10709,14 +10741,14 @@ snapshots: transitivePeerDependencies: - supports-color - socket.io@4.8.1: + socket.io@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 debug: 4.3.7 - engine.io: 6.6.4 - socket.io-adapter: 2.5.5 + engine.io: 6.6.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) + socket.io-adapter: 2.5.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) socket.io-parser: 4.2.4 transitivePeerDependencies: - bufferutil @@ -10973,7 +11005,7 @@ snapshots: typescript: 5.8.3 ts-essentials@9.4.2(typescript@5.8.3): - dependencies: + optionalDependencies: typescript: 5.8.3 ts-mixer@6.0.4: {} @@ -11141,6 +11173,7 @@ snapshots: utf-8-validate@5.0.10: dependencies: node-gyp-build: 4.8.4 + optional: true utf8@3.0.0: {} @@ -11335,14 +11368,23 @@ snapshots: wrappy@1.0.2: {} - ws@7.5.10: {} + ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 - ws@8.17.1: {} + ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 - ws@8.18.0: {} + ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10): - dependencies: + optionalDependencies: bufferutil: 4.0.9 utf-8-validate: 5.0.10 diff --git a/src/commands/genesis/bucket/add-launch-pool.ts b/src/commands/genesis/bucket/add-launch-pool.ts new file mode 100644 index 0000000..c6726ea --- /dev/null +++ b/src/commands/genesis/bucket/add-launch-pool.ts @@ -0,0 +1,305 @@ +import { + addLaunchPoolBucketV2, + safeFetchGenesisAccountV2, + findLaunchPoolBucketV2Pda, +} from '@metaplex-foundation/genesis' +import { publicKey, some, none } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../../TransactionCommand.js' +import { generateExplorerUrl } from '../../../explorers.js' +import { txSignatureToString } from '../../../lib/util.js' +import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js' + +export default class AddLaunchPool extends TransactionCommand { + static override description = `Add a launch pool bucket to a Genesis account. + +Launch pools use a pro-rata allocation model where: +- Everyone gets the same price +- Allocation is based on contribution relative to total contributions +- No frontrunning or sniping possible + +The bucket requires start/end conditions for deposits and claims. +Use Unix timestamps for absolute times.` + + static override examples = [ + '$ mplx genesis bucket add-launch-pool GenesisAddress... --allocation 500000000 --depositStart 1704067200 --depositEnd 1704153600 --claimStart 1704153600 --claimEnd 1704240000', + '$ mplx genesis bucket add-launch-pool GenesisAddress... --allocation 1000000000 --depositStart 1704067200 --depositEnd 1704153600 --claimStart 1704153600 --claimEnd 1704240000 --endBehavior ":10000"', + ] + + static override usage = 'genesis bucket add-launch-pool [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + allocation: Flags.string({ + char: 'a', + description: 'Base token allocation for this bucket (in base units)', + required: true, + }), + depositStart: Flags.string({ + description: 'Unix timestamp when deposits start', + required: true, + }), + depositEnd: Flags.string({ + description: 'Unix timestamp when deposits end', + required: true, + }), + claimStart: Flags.string({ + description: 'Unix timestamp when claims start', + required: true, + }), + claimEnd: Flags.string({ + description: 'Unix timestamp when claims end', + required: true, + }), + bucketIndex: Flags.integer({ + char: 'b', + description: 'Bucket index (default: 0)', + required: false, + }), + endBehavior: Flags.string({ + description: 'End behavior in format : (can specify multiple)', + multiple: true, + required: false, + }), + minimumDeposit: Flags.string({ + description: 'Minimum deposit amount per transaction (in base units)', + required: false, + }), + depositLimit: Flags.string({ + description: 'Maximum deposit limit per user (in base units)', + required: false, + }), + minimumQuoteTokenThreshold: Flags.string({ + description: 'Minimum total quote tokens required for the bucket to succeed', + required: false, + }), + depositPenalty: Flags.string({ + description: 'Deposit penalty schedule as JSON: {"slopeBps":0,"interceptBps":200,"maxBps":200,"startTime":0,"endTime":0}', + required: false, + }), + withdrawPenalty: Flags.string({ + description: 'Withdraw penalty schedule as JSON: {"slopeBps":0,"interceptBps":200,"maxBps":200,"startTime":0,"endTime":0}', + required: false, + }), + bonusSchedule: Flags.string({ + description: 'Bonus schedule as JSON: {"slopeBps":0,"interceptBps":0,"maxBps":0,"startTime":0,"endTime":0}', + required: false, + }), + claimSchedule: Flags.string({ + description: 'Claim vesting schedule as JSON: {"startTime":0,"endTime":0,"period":0,"cliffTime":0,"cliffAmountBps":0}', + required: false, + }), + allowlist: Flags.string({ + description: 'Allowlist config as JSON: {"merkleTreeHeight":10,"merkleRoot":"","endTime":0,"quoteCap":0}', + required: false, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(AddLaunchPool) + const spinner = ora('Adding launch pool bucket...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + if (genesisAccount.finalized) { + spinner.fail('Genesis account is already finalized') + this.error('Cannot add buckets to a finalized Genesis account') + } + + const bucketIndex = flags.bucketIndex ?? 0 + + // Parse timestamps + const depositStart = BigInt(flags.depositStart) + const depositEnd = BigInt(flags.depositEnd) + const claimStart = BigInt(flags.claimStart) + const claimEnd = BigInt(flags.claimEnd) + + // Parse allocation + const allocation = BigInt(flags.allocation) + + // Build conditions (padding must be 47 bytes as required by the Genesis program) + const conditionPadding = new Array(47).fill(0) + + const depositStartCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: depositStart, + triggeredTimestamp: BigInt(0), + } + + const depositEndCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: depositEnd, + triggeredTimestamp: BigInt(0), + } + + const claimStartCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: claimStart, + triggeredTimestamp: BigInt(0), + } + + const claimEndCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: claimEnd, + triggeredTimestamp: BigInt(0), + } + + // Parse end behaviors + const endBehaviors = (flags.endBehavior ?? []).map((behavior: string) => { + const [destinationBucketAddr, percentageBpsStr] = behavior.split(':') + if (!destinationBucketAddr || !percentageBpsStr) { + throw new Error(`Invalid end behavior format: "${behavior}". Expected format: :`) + } + return { + __kind: 'SendQuoteTokenPercentage' as const, + processed: false, + percentageBps: Number(percentageBpsStr), + padding: new Array(4).fill(0), + destinationBucket: publicKey(destinationBucketAddr), + } + }) + + // Parse optional LinearBpsSchedule fields + const parseLinearBpsSchedule = (json: string) => { + const parsed = JSON.parse(json) + return { + slopeBps: BigInt(parsed.slopeBps), + interceptBps: BigInt(parsed.interceptBps), + maxBps: BigInt(parsed.maxBps), + startTime: BigInt(parsed.startTime), + endTime: BigInt(parsed.endTime), + } + } + + // Parse optional ClaimSchedule + const parseClaimSchedule = (json: string) => { + const parsed = JSON.parse(json) + return { + startTime: BigInt(parsed.startTime), + endTime: BigInt(parsed.endTime), + period: BigInt(parsed.period), + cliffTime: BigInt(parsed.cliffTime), + cliffAmountBps: Number(parsed.cliffAmountBps), + reserved: new Array(7).fill(0), + } + } + + // Parse optional Allowlist + const parseAllowlist = (json: string) => { + const parsed = JSON.parse(json) + return { + enabled: true, + merkleTreeHeight: Number(parsed.merkleTreeHeight), + padding: new Array(3).fill(0), + merkleRoot: Uint8Array.from(Buffer.from(parsed.merkleRoot, 'hex')), + endTime: BigInt(parsed.endTime), + quoteCap: BigInt(parsed.quoteCap), + } + } + + // Build the add bucket transaction + spinner.text = 'Adding launch pool bucket...' + const transaction = addLaunchPoolBucketV2(this.context.umi, { + genesisAccount: genesisAddress, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + authority: this.context.signer, + payer: this.context.payer, + bucketIndex, + baseTokenAllocation: allocation, + depositStartCondition, + depositEndCondition, + claimStartCondition, + claimEndCondition, + backendSigner: none(), + depositPenalty: flags.depositPenalty + ? some(parseLinearBpsSchedule(flags.depositPenalty)) + : none(), + withdrawPenalty: flags.withdrawPenalty + ? some(parseLinearBpsSchedule(flags.withdrawPenalty)) + : none(), + bonusSchedule: flags.bonusSchedule + ? some(parseLinearBpsSchedule(flags.bonusSchedule)) + : none(), + depositLimit: flags.depositLimit + ? some({ limit: BigInt(flags.depositLimit) }) + : none(), + allowlist: flags.allowlist + ? some(parseAllowlist(flags.allowlist)) + : none(), + claimSchedule: flags.claimSchedule + ? some(parseClaimSchedule(flags.claimSchedule)) + : none(), + minimumDepositAmount: flags.minimumDeposit + ? some({ amount: BigInt(flags.minimumDeposit) }) + : none(), + minimumQuoteTokenThreshold: flags.minimumQuoteTokenThreshold + ? some({ amount: BigInt(flags.minimumQuoteTokenThreshold) }) + : none(), + endBehaviors, + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + // Get the bucket PDA + const [bucketPda] = findLaunchPoolBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex, + }) + + spinner.succeed('Launch pool bucket added successfully!') + + this.log('') + this.logSuccess(`Launch Pool Bucket Added`) + this.log('') + this.log('Bucket Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Bucket Address: ${bucketPda}`) + this.log(` Bucket Index: ${bucketIndex}`) + this.log(` Token Allocation: ${flags.allocation}`) + this.log('') + this.log('Schedule:') + this.log(` Deposit Start: ${new Date(Number(depositStart) * 1000).toISOString()}`) + this.log(` Deposit End: ${new Date(Number(depositEnd) * 1000).toISOString()}`) + this.log(` Claim Start: ${new Date(Number(claimStart) * 1000).toISOString()}`) + this.log(` Claim End: ${new Date(Number(claimEnd) * 1000).toISOString()}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + + } catch (error) { + spinner.fail('Failed to add launch pool bucket') + throw error + } + } +} diff --git a/src/commands/genesis/bucket/add-presale.ts b/src/commands/genesis/bucket/add-presale.ts new file mode 100644 index 0000000..c88d169 --- /dev/null +++ b/src/commands/genesis/bucket/add-presale.ts @@ -0,0 +1,219 @@ +import { + addPresaleBucketV2, + safeFetchGenesisAccountV2, + findPresaleBucketV2Pda, +} from '@metaplex-foundation/genesis' +import { publicKey, some, none } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../../TransactionCommand.js' +import { generateExplorerUrl } from '../../../explorers.js' +import { txSignatureToString } from '../../../lib/util.js' +import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js' + +export default class AddPresale extends TransactionCommand { + static override description = `Add a presale bucket to a Genesis account. + +Presale buckets offer fixed-price token allocations where: +- Price is determined by quoteCap / allocation +- Deposits are accepted during the deposit period +- Claims are available after the claim period starts + +The bucket requires start/end conditions for deposits and claims. +Use Unix timestamps for absolute times.` + + static override examples = [ + '$ mplx genesis bucket add-presale GenesisAddress... --allocation 500000000 --quoteCap 1000000000 --depositStart 1704067200 --depositEnd 1704153600 --claimStart 1704153600', + '$ mplx genesis bucket add-presale GenesisAddress... --allocation 1000000000 --quoteCap 5000000000 --depositStart 1704067200 --depositEnd 1704153600 --claimStart 1704153600 --claimEnd 1704240000', + ] + + static override usage = 'genesis bucket add-presale [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + allocation: Flags.string({ + char: 'a', + description: 'Base token allocation for this bucket (in base units)', + required: true, + }), + quoteCap: Flags.string({ + description: 'Quote token cap (total quote tokens accepted; price = quoteCap / allocation)', + required: true, + }), + depositStart: Flags.string({ + description: 'Unix timestamp when deposits start', + required: true, + }), + depositEnd: Flags.string({ + description: 'Unix timestamp when deposits end', + required: true, + }), + claimStart: Flags.string({ + description: 'Unix timestamp when claims start', + required: true, + }), + claimEnd: Flags.string({ + description: 'Unix timestamp when claims end (optional, defaults to far future)', + required: false, + }), + bucketIndex: Flags.integer({ + char: 'b', + description: 'Bucket index for this presale bucket', + required: true, + }), + minimumDeposit: Flags.string({ + description: 'Minimum deposit amount per transaction (in quote token base units)', + required: false, + }), + depositLimit: Flags.string({ + description: 'Maximum deposit limit per user (in quote token base units)', + required: false, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(AddPresale) + const spinner = ora('Adding presale bucket...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + if (genesisAccount.finalized) { + spinner.fail('Genesis account is already finalized') + this.error('Cannot add buckets to a finalized Genesis account') + } + + // Determine bucket index (bucketCount deserialization is unreliable, so require explicit index or default to 0) + const bucketIndex = flags.bucketIndex ?? 0 + + // Parse timestamps + const depositStart = BigInt(flags.depositStart) + const depositEnd = BigInt(flags.depositEnd) + const claimStart = BigInt(flags.claimStart) + // Default to far future (year 2100) if not specified + const claimEnd = flags.claimEnd ? BigInt(flags.claimEnd) : BigInt('4102444800') + + // Parse allocation and quote cap + const allocation = BigInt(flags.allocation) + const quoteCap = BigInt(flags.quoteCap) + + // Build conditions (padding must be 47 bytes as required by the Genesis program) + const conditionPadding = new Array(47).fill(0) + + const depositStartCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: depositStart, + triggeredTimestamp: BigInt(0), + } + + const depositEndCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: depositEnd, + triggeredTimestamp: BigInt(0), + } + + const claimStartCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: claimStart, + triggeredTimestamp: BigInt(0), + } + + const claimEndCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: claimEnd, + triggeredTimestamp: BigInt(0), + } + + // Build the add presale bucket transaction + spinner.text = 'Adding presale bucket...' + const transaction = addPresaleBucketV2(this.context.umi, { + genesisAccount: genesisAddress, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + authority: this.context.signer, + payer: this.context.payer, + bucketIndex, + baseTokenAllocation: allocation, + allocationQuoteTokenCap: quoteCap, + depositStartCondition, + depositEndCondition, + claimStartCondition, + claimEndCondition, + backendSigner: none(), + depositLimit: flags.depositLimit + ? some({ limit: BigInt(flags.depositLimit) }) + : none(), + allowlist: none(), + claimSchedule: none(), + minimumDepositAmount: flags.minimumDeposit + ? some({ amount: BigInt(flags.minimumDeposit) }) + : none(), + endBehaviors: [], + depositCooldown: none(), + perCooldownDepositLimit: none(), + steppedDepositLimit: none(), + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + // Get the bucket PDA + const [bucketPda] = findPresaleBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex, + }) + + spinner.succeed('Presale bucket added successfully!') + + this.log('') + this.logSuccess(`Presale Bucket Added`) + this.log('') + this.log('Bucket Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Bucket Address: ${bucketPda}`) + this.log(` Bucket Index: ${bucketIndex}`) + this.log(` Token Allocation: ${flags.allocation}`) + this.log(` Quote Token Cap: ${flags.quoteCap}`) + this.log('') + this.log('Schedule:') + this.log(` Deposit Start: ${new Date(Number(depositStart) * 1000).toISOString()}`) + this.log(` Deposit End: ${new Date(Number(depositEnd) * 1000).toISOString()}`) + this.log(` Claim Start: ${new Date(Number(claimStart) * 1000).toISOString()}`) + this.log(` Claim End: ${new Date(Number(claimEnd) * 1000).toISOString()}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + + } catch (error) { + spinner.fail('Failed to add presale bucket') + throw error + } + } +} diff --git a/src/commands/genesis/bucket/add-unlocked.ts b/src/commands/genesis/bucket/add-unlocked.ts new file mode 100644 index 0000000..51cebbd --- /dev/null +++ b/src/commands/genesis/bucket/add-unlocked.ts @@ -0,0 +1,169 @@ +import { + addUnlockedBucketV2, + safeFetchGenesisAccountV2, + findUnlockedBucketV2Pda, +} from '@metaplex-foundation/genesis' +import { publicKey, none } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../../TransactionCommand.js' +import { generateExplorerUrl } from '../../../explorers.js' +import { txSignatureToString } from '../../../lib/util.js' +import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js' + +export default class AddUnlocked extends TransactionCommand { + static override description = `Add an unlocked (treasury) bucket to a Genesis account. + +Unlocked buckets are used for team/treasury allocations that can be claimed +by a designated recipient after the claim period starts. + +Unlike launch pools or presales, unlocked buckets don't accept deposits. +Instead, they allocate base tokens directly to a recipient.` + + static override examples = [ + '$ mplx genesis bucket add-unlocked GenesisAddress... --recipient RecipientAddress... --claimStart 1704153600', + '$ mplx genesis bucket add-unlocked GenesisAddress... --recipient RecipientAddress... --claimStart 1704153600 --allocation 100000000', + '$ mplx genesis bucket add-unlocked GenesisAddress... --recipient RecipientAddress... --claimStart 1704153600 --claimEnd 1704240000', + ] + + static override usage = 'genesis bucket add-unlocked [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + recipient: Flags.string({ + description: 'Recipient address who can claim the unlocked tokens', + required: true, + }), + claimStart: Flags.string({ + description: 'Unix timestamp when claims start', + required: true, + }), + claimEnd: Flags.string({ + description: 'Unix timestamp when claims end (default: far future)', + required: false, + }), + allocation: Flags.string({ + char: 'a', + description: 'Base token allocation for this bucket (in base units, default: 0)', + default: '0', + }), + bucketIndex: Flags.integer({ + char: 'b', + description: 'Bucket index (default: 0)', + required: false, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(AddUnlocked) + const spinner = ora('Adding unlocked bucket...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + if (genesisAccount.finalized) { + spinner.fail('Genesis account is already finalized') + this.error('Cannot add buckets to a finalized Genesis account') + } + + // Determine bucket index (bucketCount deserialization is unreliable, so require explicit index or default to 0) + const bucketIndex = flags.bucketIndex ?? 0 + + // Parse timestamps + const claimStart = BigInt(flags.claimStart) + // Default to far future (year 2100) if not specified + const claimEnd = flags.claimEnd ? BigInt(flags.claimEnd) : BigInt('4102444800') + + // Parse allocation + const allocation = BigInt(flags.allocation) + + // Build conditions (padding must be 47 bytes as required by the Genesis program) + const conditionPadding = new Array(47).fill(0) + + const claimStartCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: claimStart, + triggeredTimestamp: BigInt(0), + } + + const claimEndCondition = { + __kind: 'TimeAbsolute' as const, + padding: conditionPadding, + time: claimEnd, + triggeredTimestamp: BigInt(0), + } + + // Build the add unlocked bucket transaction + spinner.text = 'Adding unlocked bucket...' + const transaction = addUnlockedBucketV2(this.context.umi, { + genesisAccount: genesisAddress, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + authority: this.context.signer, + payer: this.context.payer, + recipient: publicKey(flags.recipient), + bucketIndex, + baseTokenAllocation: allocation, + claimStartCondition, + claimEndCondition, + backendSigner: none(), + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + // Get the bucket PDA + const [bucketPda] = findUnlockedBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex, + }) + + spinner.succeed('Unlocked bucket added successfully!') + + this.log('') + this.logSuccess(`Unlocked Bucket Added`) + this.log('') + this.log('Bucket Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Bucket Address: ${bucketPda}`) + this.log(` Bucket Index: ${bucketIndex}`) + this.log(` Token Allocation: ${flags.allocation}`) + this.log(` Recipient: ${flags.recipient}`) + this.log('') + this.log('Schedule:') + this.log(` Claim Start: ${new Date(Number(claimStart) * 1000).toISOString()}`) + this.log(` Claim End: ${new Date(Number(claimEnd) * 1000).toISOString()}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + + } catch (error) { + spinner.fail('Failed to add unlocked bucket') + throw error + } + } +} diff --git a/src/commands/genesis/bucket/fetch.ts b/src/commands/genesis/bucket/fetch.ts new file mode 100644 index 0000000..244e017 --- /dev/null +++ b/src/commands/genesis/bucket/fetch.ts @@ -0,0 +1,255 @@ +import { + safeFetchGenesisAccountV2, + findLaunchPoolBucketV2Pda, + safeFetchLaunchPoolBucketV2, + findPresaleBucketV2Pda, + safeFetchPresaleBucketV2, + findUnlockedBucketV2Pda, + safeFetchUnlockedBucketV2, +} from '@metaplex-foundation/genesis' +import { publicKey } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { BaseCommand } from '../../../BaseCommand.js' +import { generateExplorerUrl } from '../../../explorers.js' +import { KEY_TYPES } from '../../../lib/genesis.js' + +function formatCondition(condition: { __kind: string; time?: bigint }): string { + if (condition.__kind === 'TimeAbsolute' && condition.time) { + const timestamp = Number(condition.time) + if (timestamp === 0) return 'Not set' + return new Date(timestamp * 1000).toISOString() + } + return `${condition.__kind}` +} + +export default class BucketFetch extends BaseCommand { + static override description = `Fetch a Genesis bucket by genesis address and bucket index. + +This command retrieves and displays information about a bucket in a Genesis account. +Supports Launch Pool, Presale, and Unlocked bucket types.` + + static override examples = [ + '$ mplx genesis bucket fetch GenesisAddress... --bucketIndex 0', + '$ mplx genesis bucket fetch GenesisAddress... -b 1 --type presale', + '$ mplx genesis bucket fetch GenesisAddress... -b 2 --type unlocked', + ] + + static override usage = 'genesis bucket fetch [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + bucketIndex: Flags.integer({ + char: 'b', + description: 'Index of the bucket to fetch', + default: 0, + }), + type: Flags.option({ + char: 't', + description: 'Type of bucket to fetch', + default: 'launch-pool', + options: ['launch-pool', 'presale', 'unlocked'] as const, + })(), + } + + public async run(): Promise { + const { args, flags } = await this.parse(BucketFetch) + const spinner = ora('Fetching bucket...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account first + spinner.text = 'Fetching Genesis account...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + spinner.text = 'Fetching bucket details...' + + if (flags.type === 'presale') { + await this.fetchPresaleBucket(genesisAddress, flags.bucketIndex, spinner) + } else if (flags.type === 'unlocked') { + await this.fetchUnlockedBucket(genesisAddress, flags.bucketIndex, spinner) + } else { + await this.fetchLaunchPoolBucket(genesisAddress, flags.bucketIndex, spinner) + } + + } catch (error) { + spinner.fail('Failed to fetch bucket') + throw error + } + } + + private async fetchLaunchPoolBucket(genesisAddress: ReturnType, bucketIndex: number, spinner: ReturnType): Promise { + const [bucketPda] = findLaunchPoolBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex, + }) + + const bucket = await safeFetchLaunchPoolBucketV2(this.context.umi, bucketPda) + + if (!bucket) { + spinner.fail('Bucket not found') + this.error(`Launch pool bucket not found at index ${bucketIndex}. It may be a different bucket type or not exist.`) + } + + spinner.succeed('Bucket fetched successfully!') + + this.log('') + this.logSuccess(`Launch Pool Bucket`) + this.log('') + this.log('Bucket Details:') + this.log(` Address: ${bucketPda}`) + this.log(` Type: ${KEY_TYPES[bucket.key] || 'Unknown'}`) + this.log(` Genesis Account: ${bucket.bucket.genesis}`) + this.log(` Bucket Index: ${bucket.bucket.bucketIndex}`) + this.log('') + this.log('Allocation:') + this.log(` Base Token Allocation: ${bucket.bucket.baseTokenAllocation.toString()}`) + this.log(` Base Token Balance: ${bucket.bucket.baseTokenBalance.toString()}`) + this.log('') + this.log('Deposits:') + this.log(` Deposit Count: ${bucket.depositCount.toString()}`) + this.log(` Total Quote Tokens Deposited: ${bucket.quoteTokenDepositTotal.toString()}`) + this.log(` Weighted Quote Token Total: ${bucket.weightedQuoteTokenTotal.toString()}`) + this.log('') + this.log('Claims:') + this.log(` Claim Count: ${bucket.claimCount.toString()}`) + this.log('') + this.log('Schedule:') + this.log(` Deposit Start: ${formatCondition(bucket.depositStartCondition)}`) + this.log(` Deposit End: ${formatCondition(bucket.depositEndCondition)}`) + this.log(` Claim Start: ${formatCondition(bucket.claimStartCondition)}`) + this.log(` Claim End: ${formatCondition(bucket.claimEndCondition)}`) + this.log('') + this.log('Fees:') + this.log(` Deposit Fee: ${bucket.depositFee.toString()}`) + this.log(` Withdraw Fee: ${bucket.withdrawFee.toString()}`) + this.log(` Claim Fee: ${bucket.claimFee.toString()}`) + this.log('') + this.log('View on Explorer:') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + bucketPda, + 'account' + ) + ) + } + + private async fetchPresaleBucket(genesisAddress: ReturnType, bucketIndex: number, spinner: ReturnType): Promise { + const [bucketPda] = findPresaleBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex, + }) + + const bucket = await safeFetchPresaleBucketV2(this.context.umi, bucketPda) + + if (!bucket) { + spinner.fail('Bucket not found') + this.error(`Presale bucket not found at index ${bucketIndex}. It may be a different bucket type or not exist.`) + } + + spinner.succeed('Bucket fetched successfully!') + + this.log('') + this.logSuccess(`Presale Bucket`) + this.log('') + this.log('Bucket Details:') + this.log(` Address: ${bucketPda}`) + this.log(` Type: ${KEY_TYPES[bucket.key] || 'Unknown'}`) + this.log(` Genesis Account: ${bucket.bucket.genesis}`) + this.log(` Bucket Index: ${bucket.bucket.bucketIndex}`) + this.log('') + this.log('Allocation:') + this.log(` Base Token Allocation: ${bucket.bucket.baseTokenAllocation.toString()}`) + this.log(` Base Token Balance: ${bucket.bucket.baseTokenBalance.toString()}`) + this.log(` Quote Token Cap: ${bucket.allocationQuoteTokenCap.toString()}`) + this.log('') + this.log('Deposits:') + this.log(` Deposit Count: ${bucket.depositCount.toString()}`) + this.log(` Total Quote Tokens Deposited: ${bucket.quoteTokenDepositTotal.toString()}`) + this.log('') + this.log('Claims:') + this.log(` Claim Count: ${bucket.claimCount.toString()}`) + this.log('') + this.log('Schedule:') + this.log(` Deposit Start: ${formatCondition(bucket.depositStartCondition)}`) + this.log(` Deposit End: ${formatCondition(bucket.depositEndCondition)}`) + this.log(` Claim Start: ${formatCondition(bucket.claimStartCondition)}`) + this.log(` Claim End: ${formatCondition(bucket.claimEndCondition)}`) + this.log('') + this.log('Fees:') + this.log(` Deposit Fee: ${bucket.depositFee.toString()}`) + this.log(` Claim Fee: ${bucket.claimFee.toString()}`) + this.log('') + this.log('View on Explorer:') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + bucketPda, + 'account' + ) + ) + } + + private async fetchUnlockedBucket(genesisAddress: ReturnType, bucketIndex: number, spinner: ReturnType): Promise { + const [bucketPda] = findUnlockedBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex, + }) + + const bucket = await safeFetchUnlockedBucketV2(this.context.umi, bucketPda) + + if (!bucket) { + spinner.fail('Bucket not found') + this.error(`Unlocked bucket not found at index ${bucketIndex}. It may be a different bucket type or not exist.`) + } + + spinner.succeed('Bucket fetched successfully!') + + this.log('') + this.logSuccess(`Unlocked Bucket`) + this.log('') + this.log('Bucket Details:') + this.log(` Address: ${bucketPda}`) + this.log(` Type: ${KEY_TYPES[bucket.key] || 'Unknown'}`) + this.log(` Genesis Account: ${bucket.bucket.genesis}`) + this.log(` Bucket Index: ${bucket.bucket.bucketIndex}`) + this.log('') + this.log('Allocation:') + this.log(` Base Token Allocation: ${bucket.bucket.baseTokenAllocation.toString()}`) + this.log(` Base Token Balance: ${bucket.bucket.baseTokenBalance.toString()}`) + this.log('') + this.log('Recipient:') + this.log(` Recipient: ${bucket.recipient}`) + this.log(` Claimed: ${bucket.claimed ? 'Yes' : 'No'}`) + this.log('') + this.log('Schedule:') + this.log(` Claim Start: ${formatCondition(bucket.claimStartCondition)}`) + this.log(` Claim End: ${formatCondition(bucket.claimEndCondition)}`) + this.log('') + this.log('View on Explorer:') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + bucketPda, + 'account' + ) + ) + } +} diff --git a/src/commands/genesis/bucket/index.ts b/src/commands/genesis/bucket/index.ts new file mode 100644 index 0000000..890eaf2 --- /dev/null +++ b/src/commands/genesis/bucket/index.ts @@ -0,0 +1,31 @@ +import { Command } from '@oclif/core' + +export default class GenesisBucket extends Command { + static override description = 'Manage Genesis buckets - allocation mechanisms for token launches' + + static override examples = [ + '<%= config.bin %> genesis bucket add-launch-pool GenesisAddress... --allocation 500000000', + '<%= config.bin %> genesis bucket fetch GenesisAddress... --bucketIndex 0', + ] + + public async run(): Promise { + await this.parse(GenesisBucket) + + this.log('Genesis Bucket Commands - Manage allocation mechanisms for token launches') + this.log('') + this.log('Bucket Types:') + this.log(' Launch Pool - Pro-rata allocation based on contributions') + this.log(' Auction - Bid-based price discovery') + this.log(' Presale - Whitelist-based allocations') + this.log(' Vault - Token storage with conditions') + this.log(' Bonding Curve - Dynamic pricing based on supply') + this.log('') + this.log('Available commands:') + this.log(' genesis bucket add-launch-pool Add a launch pool bucket') + this.log(' genesis bucket add-presale Add a presale bucket') + this.log(' genesis bucket add-unlocked Add an unlocked (treasury) bucket') + this.log(' genesis bucket fetch Fetch bucket details') + this.log('') + this.log('Run "mplx genesis bucket --help" for more information.') + } +} diff --git a/src/commands/genesis/claim-unlocked.ts b/src/commands/genesis/claim-unlocked.ts new file mode 100644 index 0000000..579b0c9 --- /dev/null +++ b/src/commands/genesis/claim-unlocked.ts @@ -0,0 +1,129 @@ +import { + claimUnlockedV2, + safeFetchGenesisAccountV2, + findUnlockedBucketV2Pda, + safeFetchUnlockedBucketV2, +} from '@metaplex-foundation/genesis' +import { publicKey } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../TransactionCommand.js' +import { generateExplorerUrl } from '../../explorers.js' +import { txSignatureToString } from '../../lib/util.js' +import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js' + +export default class GenesisClaimUnlocked extends TransactionCommand { + static override description = `Claim tokens from a Genesis unlocked (treasury) bucket. + +This command claims tokens from an unlocked bucket, which is typically used +for treasury or team allocations that vest over time. + +Requirements: +- The Genesis account must be finalized +- The claim period must be active +- You must be the designated recipient of the unlocked bucket` + + static override examples = [ + '$ mplx genesis claim-unlocked GenesisAddress123... --bucketIndex 0', + '$ mplx genesis claim-unlocked GenesisAddress123... -b 1', + '$ mplx genesis claim-unlocked GenesisAddress123... --bucketIndex 0 --recipient RecipientAddress...', + ] + + static override usage = 'genesis claim-unlocked [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + bucketIndex: Flags.integer({ + char: 'b', + description: 'Index of the unlocked bucket (default: 0)', + default: 0, + }), + recipient: Flags.string({ + description: 'Recipient address for claimed tokens (default: signer)', + required: false, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(GenesisClaimUnlocked) + const spinner = ora('Processing claim...').start() + + try { + const genesisAddress = publicKey(args.genesis) + const recipientAddress = flags.recipient + ? publicKey(flags.recipient) + : this.context.signer.publicKey + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + // Find the unlocked bucket PDA + const [bucketPda] = findUnlockedBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex: flags.bucketIndex, + }) + + // Verify the bucket exists + spinner.text = 'Verifying unlocked bucket...' + const bucket = await safeFetchUnlockedBucketV2(this.context.umi, bucketPda) + + if (!bucket) { + spinner.fail('Unlocked bucket not found') + this.error(`Unlocked bucket not found at index ${flags.bucketIndex}. Make sure the bucket has been created.`) + } + + // Build the claim transaction + spinner.text = 'Claiming tokens from unlocked bucket...' + const transaction = claimUnlockedV2(this.context.umi, { + genesisAccount: genesisAddress, + bucket: bucketPda, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + recipient: recipientAddress, + payer: this.context.payer, + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + spinner.succeed('Tokens claimed successfully!') + + this.log('') + this.logSuccess(`Claimed tokens from unlocked bucket`) + this.log('') + this.log('Claim Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Bucket: ${bucketPda}`) + this.log(` Bucket Index: ${flags.bucketIndex}`) + this.log(` Recipient: ${recipientAddress}`) + this.log(` Base Mint: ${genesisAccount.baseMint}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + + } catch (error) { + spinner.fail('Failed to claim tokens') + throw error + } + } +} diff --git a/src/commands/genesis/claim.ts b/src/commands/genesis/claim.ts new file mode 100644 index 0000000..a685e77 --- /dev/null +++ b/src/commands/genesis/claim.ts @@ -0,0 +1,146 @@ +import { + claimLaunchPoolV2, + safeFetchGenesisAccountV2, + findLaunchPoolBucketV2Pda, + findLaunchPoolDepositV2Pda, + safeFetchLaunchPoolBucketV2, + safeFetchLaunchPoolDepositV2, +} from '@metaplex-foundation/genesis' +import { publicKey } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../TransactionCommand.js' +import { generateExplorerUrl } from '../../explorers.js' +import { txSignatureToString } from '../../lib/util.js' +import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js' + +export default class GenesisClaim extends TransactionCommand { + static override description = `Claim tokens from a Genesis launch pool. + +This command claims your allocated tokens from a finalized Genesis launch pool. +Your allocation is based on your deposit relative to total contributions. + +Requirements: +- The Genesis launch must be finalized +- You must have an existing deposit in the launch pool +- The claim period must be active` + + static override examples = [ + '$ mplx genesis claim GenesisAddress123... --bucketIndex 0', + '$ mplx genesis claim GenesisAddress123...', + '$ mplx genesis claim GenesisAddress123... --bucketIndex 1 --recipient RecipientAddress...', + ] + + static override usage = 'genesis claim [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + bucketIndex: Flags.integer({ + char: 'b', + description: 'Index of the launch pool bucket (default: 0)', + default: 0, + }), + recipient: Flags.string({ + description: 'Recipient address for claimed tokens (default: signer)', + required: false, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(GenesisClaim) + const spinner = ora('Processing claim...').start() + + try { + const genesisAddress = publicKey(args.genesis) + const recipientAddress = flags.recipient + ? publicKey(flags.recipient) + : this.context.signer.publicKey + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + // Find the launch pool bucket PDA + const [bucketPda] = findLaunchPoolBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex: flags.bucketIndex, + }) + + // Verify the bucket exists + spinner.text = 'Verifying launch pool bucket...' + const bucket = await safeFetchLaunchPoolBucketV2(this.context.umi, bucketPda) + + if (!bucket) { + spinner.fail('Launch pool bucket not found') + this.error(`Launch pool bucket not found at index ${flags.bucketIndex}`) + } + + // Find and verify the deposit PDA + const depositPda = findLaunchPoolDepositV2Pda(this.context.umi, { + bucket: bucketPda, + recipient: recipientAddress, + }) + + spinner.text = 'Verifying deposit...' + const deposit = await safeFetchLaunchPoolDepositV2(this.context.umi, depositPda) + + if (!deposit) { + spinner.fail('Deposit not found') + this.error(`No deposit found for recipient ${recipientAddress}. Make sure you have deposited into this launch pool.`) + } + + // Build the claim transaction + spinner.text = 'Claiming tokens...' + const transaction = claimLaunchPoolV2(this.context.umi, { + genesisAccount: genesisAddress, + bucket: bucketPda, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + depositPda, + recipient: recipientAddress, + payer: this.context.payer, + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + spinner.succeed('Tokens claimed successfully!') + + this.log('') + this.logSuccess(`Claimed tokens from launch pool`) + this.log('') + this.log('Claim Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Bucket: ${bucketPda}`) + this.log(` Bucket Index: ${flags.bucketIndex}`) + this.log(` Recipient: ${recipientAddress}`) + this.log(` Base Mint: ${genesisAccount.baseMint}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + + } catch (error) { + spinner.fail('Failed to claim tokens') + throw error + } + } +} diff --git a/src/commands/genesis/create.ts b/src/commands/genesis/create.ts new file mode 100644 index 0000000..51d8cec --- /dev/null +++ b/src/commands/genesis/create.ts @@ -0,0 +1,182 @@ +import { + initializeV2, + findGenesisAccountV2Pda, + WRAPPED_SOL_MINT, +} from '@metaplex-foundation/genesis' +import { generateSigner, publicKey } from '@metaplex-foundation/umi' +import { Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../TransactionCommand.js' +import { generateExplorerUrl } from '../../explorers.js' +import { txSignatureToString } from '../../lib/util.js' +import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js' + +// Funding modes for Genesis +const FUNDING_MODE = { + NewMint: 0, // Create a new mint (most common) + Transfer: 1, // Transfer existing tokens +} as const + +export default class GenesisCreate extends TransactionCommand { + static override description = `Create a new Genesis account for a token launch (TGE). + +Genesis is a smart contract framework for Token Generation Events on Solana. +This command initializes a new Genesis account that will coordinate your token launch. + +The Genesis account manages: +- Token supply and allocation +- Launch pools, auctions, and presales +- Integration with DEXs (Raydium, Meteora) + +Funding Modes: +- new-mint: Creates a new token mint (default, most common) +- transfer: Uses an existing mint and transfers tokens from your wallet` + + static override examples = [ + '$ mplx genesis create --name "My Token" --symbol "MTK" --totalSupply 1000000000', + '$ mplx genesis create --name "My Token" --symbol "MTK" --totalSupply 1000000000 --uri "https://example.com/metadata.json"', + '$ mplx genesis create --name "My Token" --symbol "MTK" --totalSupply 1000000000 --quoteMint "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" --decimals 6', + '$ mplx genesis create --name "My Token" --symbol "MTK" --totalSupply 1000000000 --fundingMode transfer --baseMint "ExistingMint123..."', + ] + + static override flags = { + name: Flags.string({ + char: 'n', + description: 'Name of the token', + required: true, + }), + symbol: Flags.string({ + char: 's', + description: 'Symbol of the token (e.g., MTK)', + required: true, + }), + totalSupply: Flags.string({ + description: 'Total supply of tokens (in base units, e.g., 1000000000 for 1B tokens with 9 decimals)', + required: true, + }), + uri: Flags.string({ + char: 'u', + description: 'URI for token metadata JSON', + default: '', + }), + decimals: Flags.integer({ + char: 'd', + description: 'Number of decimals for the token', + default: 9, + }), + quoteMint: Flags.string({ + description: 'Quote token mint address (default: Wrapped SOL)', + required: false, + }), + fundingMode: Flags.option({ + default: 'new-mint', + description: 'Funding mode: new-mint (create new token) or transfer (use existing)', + options: ['new-mint', 'transfer'] as const, + })(), + baseMint: Flags.string({ + description: 'Base token mint address (only used with fundingMode=transfer)', + required: false, + }), + genesisIndex: Flags.integer({ + description: 'Genesis index (default: 0, increment if creating multiple launches for same mint)', + default: 0, + }), + } + + static override usage = 'genesis create [FLAGS]' + + public async run(): Promise { + const { flags } = await this.parse(GenesisCreate) + + const spinner = ora('Creating Genesis account...').start() + + try { + // Determine funding mode + const fundingMode = flags.fundingMode === 'transfer' + ? FUNDING_MODE.Transfer + : FUNDING_MODE.NewMint + + // Handle base mint + let baseMint + if (fundingMode === FUNDING_MODE.Transfer) { + if (!flags.baseMint) { + throw new Error('--baseMint is required when using fundingMode=transfer') + } + baseMint = publicKey(flags.baseMint) + } else { + // Generate a new mint signer for new-mint mode + baseMint = generateSigner(this.context.umi) + } + + // Handle quote mint (default to Wrapped SOL) + const quoteMint = flags.quoteMint + ? publicKey(flags.quoteMint) + : WRAPPED_SOL_MINT + + // Parse and validate total supply + if (!/^\d+$/.test(flags.totalSupply)) { + this.error(`Invalid totalSupply "${flags.totalSupply}". Must be a non-negative integer string (e.g., "1000000000").`) + } + const totalSupply = BigInt(flags.totalSupply) + + // Build the initialize transaction + const transaction = initializeV2(this.context.umi, { + baseMint, + quoteMint, + authority: this.context.signer, + payer: this.context.payer, + fundingMode, + totalSupplyBaseToken: totalSupply, + name: flags.name, + symbol: flags.symbol, + uri: flags.uri, + decimals: flags.decimals, + genesisIndex: flags.genesisIndex, + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + // Get the base mint public key (either from generated signer or from flags) + const baseMintPubkey = 'publicKey' in baseMint ? baseMint.publicKey : baseMint + + // Get the genesis account PDA + const [genesisAccountPda] = findGenesisAccountV2Pda(this.context.umi, { + baseMint: baseMintPubkey, + genesisIndex: flags.genesisIndex, + }) + + spinner.succeed('Genesis account created successfully!') + + this.log('') + this.logSuccess(`Genesis Account: ${genesisAccountPda}`) + this.log(`Base Mint: ${baseMintPubkey}`) + this.log(`Quote Mint: ${quoteMint}`) + this.log(`Name: ${flags.name}`) + this.log(`Symbol: ${flags.symbol}`) + this.log(`Total Supply: ${flags.totalSupply}`) + this.log(`Decimals: ${flags.decimals}`) + this.log(`Funding Mode: ${flags.fundingMode}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + this.log('') + this.log('Next steps:') + this.log(' 1. Add buckets to your Genesis account (launch pool, auction, presale, etc.)') + this.log(' 2. Configure your launch parameters') + this.log(' 3. Finalize the launch when ready') + + } catch (error) { + spinner.fail('Failed to create Genesis account') + throw error + } + } +} diff --git a/src/commands/genesis/deposit.ts b/src/commands/genesis/deposit.ts new file mode 100644 index 0000000..26be81e --- /dev/null +++ b/src/commands/genesis/deposit.ts @@ -0,0 +1,151 @@ +import { + depositLaunchPoolV2, + safeFetchGenesisAccountV2, + findLaunchPoolBucketV2Pda, + findLaunchPoolDepositV2Pda, + safeFetchLaunchPoolBucketV2, +} from '@metaplex-foundation/genesis' +import { publicKey } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../TransactionCommand.js' +import { generateExplorerUrl } from '../../explorers.js' +import { txSignatureToString } from '../../lib/util.js' +import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js' + +export default class GenesisDeposit extends TransactionCommand { + static override description = `Deposit into a Genesis launch pool. + +This command deposits quote tokens (e.g., SOL, USDC) into a launch pool bucket. +You will receive a proportional allocation of tokens based on your share of contributions. + +Launch pools use a pro-rata allocation model where: +- Everyone gets the same price +- Allocation is based on your contribution relative to total contributions +- No frontrunning or sniping possible` + + static override examples = [ + '$ mplx genesis deposit GenesisAddress123... --amount 1000000000 --bucketIndex 0', + '$ mplx genesis deposit GenesisAddress123... --amount 1000000000', + '$ mplx genesis deposit GenesisAddress123... --amount 5000000000 --bucketIndex 1', + ] + + static override usage = 'genesis deposit [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + amount: Flags.string({ + char: 'a', + description: 'Amount of quote tokens to deposit (in base units, e.g., lamports for SOL)', + required: true, + }), + bucketIndex: Flags.integer({ + char: 'b', + description: 'Index of the launch pool bucket (default: 0)', + default: 0, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(GenesisDeposit) + const spinner = ora('Processing deposit...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + // Find the launch pool bucket PDA + const [bucketPda] = findLaunchPoolBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex: flags.bucketIndex, + }) + + // Verify the bucket exists + spinner.text = 'Verifying launch pool bucket...' + const bucket = await safeFetchLaunchPoolBucketV2(this.context.umi, bucketPda) + + if (!bucket) { + spinner.fail('Launch pool bucket not found') + this.error(`Launch pool bucket not found at index ${flags.bucketIndex}. Make sure the bucket has been created.`) + } + + // Parse and validate amount + let amount: bigint + try { + amount = BigInt(flags.amount) + } catch { + this.error(`Invalid amount "${flags.amount}". Must be a non-negative integer.`) + } + + if (amount <= 0n) { + this.error('Deposit amount must be greater than 0.') + } + + // Build the deposit transaction + spinner.text = 'Depositing into launch pool...' + const transaction = depositLaunchPoolV2(this.context.umi, { + genesisAccount: genesisAddress, + bucket: bucketPda, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + depositor: this.context.signer, + recipient: this.context.signer, + rentPayer: this.context.payer, + amountQuoteToken: amount, + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + // Find the deposit PDA for reference + const depositPda = findLaunchPoolDepositV2Pda(this.context.umi, { + bucket: bucketPda, + recipient: this.context.signer.publicKey, + }) + + spinner.succeed('Deposit successful!') + + this.log('') + this.logSuccess(`Deposited ${flags.amount} quote tokens`) + this.log('') + this.log('Deposit Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Bucket: ${bucketPda}`) + this.log(` Bucket Index: ${flags.bucketIndex}`) + this.log(` Deposit PDA: ${depositPda}`) + this.log(` Amount: ${flags.amount}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + this.log('') + this.log('Note: Your token allocation will be calculated pro-rata based on total contributions.') + this.log('Use "mplx genesis claim" to claim your tokens after the launch is finalized.') + + } catch (error) { + spinner.fail('Failed to deposit') + throw error + } + } +} diff --git a/src/commands/genesis/fetch.ts b/src/commands/genesis/fetch.ts new file mode 100644 index 0000000..5f6c860 --- /dev/null +++ b/src/commands/genesis/fetch.ts @@ -0,0 +1,89 @@ +import { + safeFetchGenesisAccountV2, +} from '@metaplex-foundation/genesis' +import { publicKey } from '@metaplex-foundation/umi' +import { Args } from '@oclif/core' +import ora from 'ora' + +import { BaseCommand } from '../../BaseCommand.js' +import { generateExplorerUrl } from '../../explorers.js' +import { KEY_TYPES, FUNDING_MODES } from '../../lib/genesis.js' + +export default class GenesisFetch extends BaseCommand { + static override description = `Fetch a Genesis account by its address. + +This command retrieves and displays information about an existing Genesis account. +Use this to check the status, configuration, and details of any Genesis launch.` + + static override examples = [ + '$ mplx genesis fetch GenesisAddress123...', + '$ mplx genesis fetch 7nVDaSFJWnPpBXH5JQxUvK8YwMGp5VHrYLBhWAe5hJkv', + ] + + static override usage = 'genesis fetch [GENESIS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address to fetch', + required: true, + }), + } + + public async run(): Promise { + const { args } = await this.parse(GenesisFetch) + const spinner = ora('Fetching Genesis account...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + spinner.succeed('Genesis account fetched successfully!') + + this.log('') + this.logSuccess(`Genesis Account: ${genesisAddress}`) + this.log('') + this.log('Account Details:') + this.log(` Account Type: ${KEY_TYPES[genesisAccount.key] || 'Unknown'}`) + this.log(` Authority: ${genesisAccount.authority}`) + this.log(` Base Mint: ${genesisAccount.baseMint}`) + this.log(` Quote Mint: ${genesisAccount.quoteMint}`) + this.log(` Finalized: ${genesisAccount.finalized ? 'Yes' : 'No'}`) + this.log(` Index: ${genesisAccount.index}`) + this.log(` Bucket Count: ${genesisAccount.bucketCount}`) + this.log('') + this.log('Token Supply:') + this.log(` Total Supply (Base Token): ${genesisAccount.totalSupplyBaseToken.toString()}`) + this.log(` Total Allocated Supply: ${genesisAccount.totalAllocatedSupplyBaseToken.toString()}`) + this.log(` Unallocated Supply: ${(genesisAccount.totalSupplyBaseToken - genesisAccount.totalAllocatedSupplyBaseToken).toString()}`) + this.log('') + this.log('Proceeds:') + this.log(` Total Proceeds (Quote Token): ${genesisAccount.totalProceedsQuoteToken.toString()}`) + this.log('') + this.log('Configuration:') + this.log(` Funding Mode: ${FUNDING_MODES[genesisAccount.fundingMode] || `Unknown (${genesisAccount.fundingMode})`}`) + this.log(` Bump: ${genesisAccount.bump}`) + this.log('') + this.log('View on Explorer:') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + genesisAddress, + 'account' + ) + ) + } catch (error) { + spinner.fail('Failed to fetch Genesis account') + if (error instanceof Error && error.message.includes('Account does not exist')) { + this.error(`Genesis account not found at address: ${args.genesis}`) + } + throw error + } + } +} diff --git a/src/commands/genesis/finalize.ts b/src/commands/genesis/finalize.ts new file mode 100644 index 0000000..f5aace0 --- /dev/null +++ b/src/commands/genesis/finalize.ts @@ -0,0 +1,129 @@ +import { + finalizeV2, + safeFetchGenesisAccountV2, + findLaunchPoolBucketV2Pda, + findPresaleBucketV2Pda, + findUnlockedBucketV2Pda, +} from '@metaplex-foundation/genesis' +import { publicKey, AccountMeta } from '@metaplex-foundation/umi' +import { Args } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../TransactionCommand.js' +import { generateExplorerUrl } from '../../explorers.js' +import { txSignatureToString } from '../../lib/util.js' +import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js' + +export default class GenesisFinalize extends TransactionCommand { + static override description = `Finalize a Genesis launch. + +This command finalizes a Genesis account, marking the token launch as complete. +Once finalized, the launch configuration cannot be changed. + +Requirements: +- You must be the authority of the Genesis account +- All buckets should be properly configured before finalizing +- The launch conditions should be met` + + static override examples = [ + '$ mplx genesis finalize GenesisAddress123...', + '$ mplx genesis finalize 7nVDaSFJWnPpBXH5JQxUvK8YwMGp5VHrYLBhWAe5hJkv', + ] + + static override usage = 'genesis finalize [GENESIS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address to finalize', + required: true, + }), + } + + public async run(): Promise { + const { args } = await this.parse(GenesisFinalize) + const spinner = ora('Finalizing Genesis launch...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account to get the base mint + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + if (genesisAccount.finalized) { + spinner.fail('Genesis account is already finalized') + this.error('This Genesis account has already been finalized') + } + + // Discover all bucket PDAs to pass as remaining accounts + spinner.text = 'Discovering bucket accounts...' + const pdaFinders = [ + findLaunchPoolBucketV2Pda, + findPresaleBucketV2Pda, + findUnlockedBucketV2Pda, + ] + + // Build all PDA lookups and fetch in parallel + const pdaLookups: { pda: ReturnType }[] = [] + for (let i = 0; i < genesisAccount.bucketCount; i++) { + for (const finder of pdaFinders) { + const [pda] = finder(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex: i, + }) + pdaLookups.push({ pda }) + } + } + + const accounts = await Promise.all( + pdaLookups.map(({ pda }) => this.context.umi.rpc.getAccount(pda)) + ) + + const bucketAccounts: AccountMeta[] = accounts + .map((account, idx) => ({ account, pda: pdaLookups[idx].pda })) + .filter(({ account }) => account.exists) + .map(({ pda }) => ({ + pubkey: pda, + isSigner: false, + isWritable: true, + })) + + // Build the finalize transaction + spinner.text = 'Finalizing Genesis launch...' + const transaction = finalizeV2(this.context.umi, { + genesisAccount: genesisAddress, + baseMint: genesisAccount.baseMint, + authority: this.context.signer, + }).addRemainingAccounts(bucketAccounts) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + spinner.succeed('Genesis launch finalized successfully!') + + this.log('') + this.logSuccess(`Genesis Account: ${genesisAddress}`) + this.log(`Base Mint: ${genesisAccount.baseMint}`) + this.log(`Status: Finalized`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + + } catch (error) { + spinner.fail('Failed to finalize Genesis launch') + throw error + } + } +} diff --git a/src/commands/genesis/index.ts b/src/commands/genesis/index.ts new file mode 100644 index 0000000..4969b9e --- /dev/null +++ b/src/commands/genesis/index.ts @@ -0,0 +1,36 @@ +import { Command } from '@oclif/core' + +export default class Genesis extends Command { + static override description = 'Genesis Program - Token launch management for TGE (Token Generation Events)' + + static override examples = [ + '<%= config.bin %> genesis create --name "My Token" --symbol "MTK" --totalSupply 1000000000', + '<%= config.bin %> genesis fetch GenesisAddress123...', + '<%= config.bin %> genesis deposit GenesisAddress123... --amount 1000', + '<%= config.bin %> genesis claim GenesisAddress123...', + '<%= config.bin %> genesis finalize GenesisAddress123...', + ] + + public async run(): Promise { + await this.parse(Genesis) + + this.log('Genesis Program - Token launch management for TGE (Token Generation Events)') + this.log('') + this.log('Available commands:') + this.log(' genesis create Create a new Genesis account for a token launch') + this.log(' genesis fetch Fetch Genesis account details') + this.log(' genesis deposit Deposit into a launch pool') + this.log(' genesis withdraw Withdraw from a launch pool') + this.log(' genesis claim Claim tokens from a completed launch') + this.log(' genesis claim-unlocked Claim tokens from an unlocked bucket') + this.log(' genesis transition Execute end behaviors for a bucket') + this.log(' genesis finalize Finalize a Genesis launch') + this.log(' genesis revoke Revoke/cancel a Genesis launch') + this.log('') + this.log('Subcommand groups:') + this.log(' genesis bucket Manage buckets (add-launch-pool, add-presale, add-unlocked, fetch)') + this.log(' genesis presale Presale deposit and claim commands') + this.log('') + this.log('Run "mplx genesis --help" for more information about a command.') + } +} diff --git a/src/commands/genesis/presale/claim.ts b/src/commands/genesis/presale/claim.ts new file mode 100644 index 0000000..2938b0d --- /dev/null +++ b/src/commands/genesis/presale/claim.ts @@ -0,0 +1,146 @@ +import { + claimPresaleV2, + safeFetchGenesisAccountV2, + findPresaleBucketV2Pda, + findPresaleDepositV2Pda, + safeFetchPresaleBucketV2, + safeFetchPresaleDepositV2, +} from '@metaplex-foundation/genesis' +import { publicKey } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../../TransactionCommand.js' +import { generateExplorerUrl } from '../../../explorers.js' +import { txSignatureToString } from '../../../lib/util.js' +import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js' + +export default class PresaleClaim extends TransactionCommand { + static override description = `Claim tokens from a Genesis presale bucket. + +This command claims your allocated tokens from a presale bucket. +Your allocation is based on your deposit amount and the presale price. + +Requirements: +- The Genesis launch must be finalized +- You must have an existing deposit in the presale bucket +- The claim period must be active` + + static override examples = [ + '$ mplx genesis presale claim GenesisAddress123... --bucketIndex 0', + '$ mplx genesis presale claim GenesisAddress123...', + '$ mplx genesis presale claim GenesisAddress123... --bucketIndex 1 --recipient RecipientAddress...', + ] + + static override usage = 'genesis presale claim [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + bucketIndex: Flags.integer({ + char: 'b', + description: 'Index of the presale bucket (default: 0)', + default: 0, + }), + recipient: Flags.string({ + description: 'Recipient address for claimed tokens (default: signer)', + required: false, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(PresaleClaim) + const spinner = ora('Processing presale claim...').start() + + try { + const genesisAddress = publicKey(args.genesis) + const recipientAddress = flags.recipient + ? publicKey(flags.recipient) + : this.context.signer.publicKey + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + // Find the presale bucket PDA + const [bucketPda] = findPresaleBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex: flags.bucketIndex, + }) + + // Verify the bucket exists + spinner.text = 'Verifying presale bucket...' + const bucket = await safeFetchPresaleBucketV2(this.context.umi, bucketPda) + + if (!bucket) { + spinner.fail('Presale bucket not found') + this.error(`Presale bucket not found at index ${flags.bucketIndex}`) + } + + // Find and verify the deposit PDA + const depositPda = findPresaleDepositV2Pda(this.context.umi, { + bucket: bucketPda, + recipient: recipientAddress, + }) + + spinner.text = 'Verifying deposit...' + const deposit = await safeFetchPresaleDepositV2(this.context.umi, depositPda) + + if (!deposit) { + spinner.fail('Deposit not found') + this.error(`No presale deposit found for recipient ${recipientAddress}. Make sure you have deposited into this presale bucket.`) + } + + // Build the claim transaction + spinner.text = 'Claiming presale tokens...' + const transaction = claimPresaleV2(this.context.umi, { + genesisAccount: genesisAddress, + bucket: bucketPda, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + depositPda, + recipient: recipientAddress, + payer: this.context.payer, + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + spinner.succeed('Presale tokens claimed successfully!') + + this.log('') + this.logSuccess(`Claimed tokens from presale bucket`) + this.log('') + this.log('Claim Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Bucket: ${bucketPda}`) + this.log(` Bucket Index: ${flags.bucketIndex}`) + this.log(` Recipient: ${recipientAddress}`) + this.log(` Base Mint: ${genesisAccount.baseMint}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + + } catch (error) { + spinner.fail('Failed to claim presale tokens') + throw error + } + } +} diff --git a/src/commands/genesis/presale/deposit.ts b/src/commands/genesis/presale/deposit.ts new file mode 100644 index 0000000..bb48dfb --- /dev/null +++ b/src/commands/genesis/presale/deposit.ts @@ -0,0 +1,140 @@ +import { + depositPresaleV2, + safeFetchGenesisAccountV2, + findPresaleBucketV2Pda, + safeFetchPresaleBucketV2, +} from '@metaplex-foundation/genesis' +import { publicKey } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../../TransactionCommand.js' +import { generateExplorerUrl } from '../../../explorers.js' +import { txSignatureToString } from '../../../lib/util.js' +import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js' + +export default class PresaleDeposit extends TransactionCommand { + static override description = `Deposit into a Genesis presale bucket. + +This command deposits quote tokens (e.g., SOL, USDC) into a presale bucket. +Presale buckets offer fixed-price allocations with a set quote token cap. + +Requirements: +- The deposit period must be active +- The presale bucket must exist` + + static override examples = [ + '$ mplx genesis presale deposit GenesisAddress123... --amount 1000000000 --bucketIndex 0', + '$ mplx genesis presale deposit GenesisAddress123... --amount 500000000', + ] + + static override usage = 'genesis presale deposit [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + amount: Flags.string({ + char: 'a', + description: 'Amount of quote tokens to deposit (in base units, e.g., lamports for SOL)', + required: true, + }), + bucketIndex: Flags.integer({ + char: 'b', + description: 'Index of the presale bucket (default: 0)', + default: 0, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(PresaleDeposit) + const spinner = ora('Processing presale deposit...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + // Find the presale bucket PDA + const [bucketPda] = findPresaleBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex: flags.bucketIndex, + }) + + // Verify the bucket exists + spinner.text = 'Verifying presale bucket...' + const bucket = await safeFetchPresaleBucketV2(this.context.umi, bucketPda) + + if (!bucket) { + spinner.fail('Presale bucket not found') + this.error(`Presale bucket not found at index ${flags.bucketIndex}. Make sure the bucket has been created.`) + } + + // Parse and validate amount + let amount: bigint + try { + amount = BigInt(flags.amount) + } catch { + this.error(`Invalid amount "${flags.amount}". Must be a non-negative integer.`) + } + + if (amount <= 0n) { + this.error('Deposit amount must be greater than 0.') + } + + // Build the deposit transaction + spinner.text = 'Depositing into presale...' + const transaction = depositPresaleV2(this.context.umi, { + genesisAccount: genesisAddress, + bucket: bucketPda, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + depositor: this.context.signer, + recipient: this.context.signer, + rentPayer: this.context.payer, + amountQuoteToken: amount, + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + spinner.succeed('Presale deposit successful!') + + this.log('') + this.logSuccess(`Deposited ${flags.amount} quote tokens into presale`) + this.log('') + this.log('Deposit Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Bucket: ${bucketPda}`) + this.log(` Bucket Index: ${flags.bucketIndex}`) + this.log(` Amount: ${flags.amount}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + this.log('') + this.log('Use "mplx genesis presale claim" to claim your tokens after the claim period starts.') + + } catch (error) { + spinner.fail('Failed to deposit into presale') + throw error + } + } +} diff --git a/src/commands/genesis/presale/index.ts b/src/commands/genesis/presale/index.ts new file mode 100644 index 0000000..197fa8b --- /dev/null +++ b/src/commands/genesis/presale/index.ts @@ -0,0 +1,24 @@ +import { Command } from '@oclif/core' + +export default class GenesisPresale extends Command { + static override description = 'Genesis Presale Commands - Manage presale bucket deposits and claims' + + static override examples = [ + '<%= config.bin %> genesis presale deposit GenesisAddress... --amount 1000000000', + '<%= config.bin %> genesis presale claim GenesisAddress... --bucketIndex 0', + ] + + public async run(): Promise { + await this.parse(GenesisPresale) + + this.log('Genesis Presale Commands - Manage presale bucket deposits and claims') + this.log('') + this.log('Presale buckets allow fixed-price token allocations with deposit/claim periods.') + this.log('') + this.log('Available commands:') + this.log(' genesis presale deposit Deposit into a presale bucket') + this.log(' genesis presale claim Claim tokens from a presale bucket') + this.log('') + this.log('Run "mplx genesis presale --help" for more information.') + } +} diff --git a/src/commands/genesis/revoke.ts b/src/commands/genesis/revoke.ts new file mode 100644 index 0000000..ea191f4 --- /dev/null +++ b/src/commands/genesis/revoke.ts @@ -0,0 +1,116 @@ +import { + revokeV2, + safeFetchGenesisAccountV2, +} from '@metaplex-foundation/genesis' +import { publicKey } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../TransactionCommand.js' +import { generateExplorerUrl } from '../../explorers.js' +import { txSignatureToString } from '../../lib/util.js' +import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js' + +export default class GenesisRevoke extends TransactionCommand { + static override description = `Revoke mint and/or freeze authority from a Genesis account. + +This command revokes the mint authority and/or freeze authority from the Genesis account. +This is typically done after a token launch is complete to ensure no more tokens can be minted. + +WARNING: This action is irreversible. Once revoked, the authority cannot be restored. + +Options: +- --revokeMint: Revoke the mint authority (no more tokens can be minted) +- --revokeFreeze: Revoke the freeze authority (tokens cannot be frozen)` + + static override examples = [ + '$ mplx genesis revoke GenesisAddress123... --revokeMint --revokeFreeze', + '$ mplx genesis revoke GenesisAddress123... --revokeMint', + '$ mplx genesis revoke GenesisAddress123... --revokeFreeze', + ] + + static override usage = 'genesis revoke [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + revokeMint: Flags.boolean({ + description: 'Revoke the mint authority (no more tokens can be minted)', + default: false, + }), + revokeFreeze: Flags.boolean({ + description: 'Revoke the freeze authority (tokens cannot be frozen)', + default: false, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(GenesisRevoke) + + if (!flags.revokeMint && !flags.revokeFreeze) { + this.error('At least one of --revokeMint or --revokeFreeze must be specified') + } + + const spinner = ora('Revoking authorities...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + // Build the revoke transaction + spinner.text = 'Revoking authorities...' + const transaction = revokeV2(this.context.umi, { + genesisAccount: genesisAddress, + baseMint: genesisAccount.baseMint, + authority: this.context.signer, + baseTokenProgram: publicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), + revokeMintAuthority: flags.revokeMint, + revokeFreezeAuthority: flags.revokeFreeze, + padding: [0, 0, 0, 0, 0], + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + spinner.succeed('Authorities revoked successfully!') + + this.log('') + this.logSuccess('Authorities revoked') + this.log('') + this.log('Revoke Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Base Mint: ${genesisAccount.baseMint}`) + this.log(` Mint Authority Revoked: ${flags.revokeMint ? 'Yes' : 'No'}`) + this.log(` Freeze Authority Revoked: ${flags.revokeFreeze ? 'Yes' : 'No'}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + this.log('') + this.log('WARNING: This action is irreversible. The revoked authorities cannot be restored.') + + } catch (error) { + spinner.fail('Failed to revoke authorities') + throw error + } + } +} diff --git a/src/commands/genesis/transition.ts b/src/commands/genesis/transition.ts new file mode 100644 index 0000000..d317ee7 --- /dev/null +++ b/src/commands/genesis/transition.ts @@ -0,0 +1,142 @@ +import { + transitionV2, + safeFetchGenesisAccountV2, + findLaunchPoolBucketV2Pda, + safeFetchLaunchPoolBucketV2, +} from '@metaplex-foundation/genesis' +import { findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox' +import { publicKey, AccountMeta } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../TransactionCommand.js' +import { generateExplorerUrl } from '../../explorers.js' +import { txSignatureToString } from '../../lib/util.js' +import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js' + +export default class GenesisTransition extends TransactionCommand { + static override description = `Execute end behaviors (transition) for a Genesis bucket. + +This command triggers the end behaviors configured on a launch pool bucket, +such as sending quote tokens to another bucket after the deposit period ends. + +Requirements: +- The Genesis account must be finalized +- The deposit period must have ended +- The bucket must have end behaviors configured` + + static override examples = [ + '$ mplx genesis transition GenesisAddress123... --bucketIndex 0', + '$ mplx genesis transition GenesisAddress123... -b 1', + ] + + static override usage = 'genesis transition [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + bucketIndex: Flags.integer({ + char: 'b', + description: 'Index of the primary bucket whose end behaviors to execute', + required: true, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(GenesisTransition) + const spinner = ora('Processing transition...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + // Find the primary bucket PDA + const [primaryBucketPda] = findLaunchPoolBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex: flags.bucketIndex, + }) + + // Verify the bucket exists + spinner.text = 'Verifying bucket...' + const bucket = await safeFetchLaunchPoolBucketV2(this.context.umi, primaryBucketPda) + + if (!bucket) { + spinner.fail('Bucket not found') + this.error(`Launch pool bucket not found at index ${flags.bucketIndex}. Make sure the bucket has been created.`) + } + + // Collect destination bucket pubkeys from end behaviors + spinner.text = 'Resolving end behavior destinations...' + const destinationBuckets = new Set() + for (const behavior of bucket.endBehaviors) { + if ('destinationBucket' in behavior) { + destinationBuckets.add(behavior.destinationBucket.toString()) + } + } + + // Build remaining accounts: pairs of (bucket, quote_token_ata) + const remainingAccounts: AccountMeta[] = [] + for (const destBucketStr of destinationBuckets) { + const destBucket = publicKey(destBucketStr) + const [quoteTokenAta] = findAssociatedTokenPda(this.context.umi, { + mint: genesisAccount.quoteMint, + owner: destBucket, + }) + remainingAccounts.push( + { pubkey: destBucket, isSigner: false, isWritable: true }, + { pubkey: quoteTokenAta, isSigner: false, isWritable: true }, + ) + } + + // Build the transition transaction + spinner.text = 'Executing transition...' + const transaction = transitionV2(this.context.umi, { + genesisAccount: genesisAddress, + primaryBucket: primaryBucketPda, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + payer: this.context.payer, + }).addRemainingAccounts(remainingAccounts) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + spinner.succeed('Transition executed successfully!') + + this.log('') + this.logSuccess(`Transition completed for bucket index ${flags.bucketIndex}`) + this.log('') + this.log('Transition Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Primary Bucket: ${primaryBucketPda}`) + this.log(` Bucket Index: ${flags.bucketIndex}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + + } catch (error) { + spinner.fail('Failed to execute transition') + throw error + } + } +} diff --git a/src/commands/genesis/withdraw.ts b/src/commands/genesis/withdraw.ts new file mode 100644 index 0000000..8fc3e6d --- /dev/null +++ b/src/commands/genesis/withdraw.ts @@ -0,0 +1,154 @@ +import { + withdrawLaunchPoolV2, + safeFetchGenesisAccountV2, + findLaunchPoolBucketV2Pda, + findLaunchPoolDepositV2Pda, + safeFetchLaunchPoolBucketV2, + safeFetchLaunchPoolDepositV2, +} from '@metaplex-foundation/genesis' +import { publicKey } from '@metaplex-foundation/umi' +import { Args, Flags } from '@oclif/core' +import ora from 'ora' + +import { TransactionCommand } from '../../TransactionCommand.js' +import { generateExplorerUrl } from '../../explorers.js' +import { txSignatureToString } from '../../lib/util.js' +import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js' + +export default class GenesisWithdraw extends TransactionCommand { + static override description = `Withdraw from a Genesis launch pool. + +This command withdraws quote tokens from a launch pool bucket. +You can only withdraw tokens you have previously deposited. + +Requirements: +- The deposit period must still be active +- You must have an existing deposit in the launch pool` + + static override examples = [ + '$ mplx genesis withdraw GenesisAddress123... --amount 1000000000 --bucketIndex 0', + '$ mplx genesis withdraw GenesisAddress123... --amount 500000000', + ] + + static override usage = 'genesis withdraw [GENESIS] [FLAGS]' + + static override args = { + genesis: Args.string({ + description: 'The Genesis account address', + required: true, + }), + } + + static override flags = { + amount: Flags.string({ + char: 'a', + description: 'Amount of quote tokens to withdraw (in base units, e.g., lamports for SOL)', + required: true, + }), + bucketIndex: Flags.integer({ + char: 'b', + description: 'Index of the launch pool bucket (default: 0)', + default: 0, + }), + } + + public async run(): Promise { + const { args, flags } = await this.parse(GenesisWithdraw) + const spinner = ora('Processing withdrawal...').start() + + try { + const genesisAddress = publicKey(args.genesis) + + // Fetch the Genesis account + spinner.text = 'Fetching Genesis account details...' + const genesisAccount = await safeFetchGenesisAccountV2(this.context.umi, genesisAddress) + + if (!genesisAccount) { + spinner.fail('Genesis account not found') + this.error(`Genesis account not found at address: ${args.genesis}`) + } + + // Find the launch pool bucket PDA + const [bucketPda] = findLaunchPoolBucketV2Pda(this.context.umi, { + genesisAccount: genesisAddress, + bucketIndex: flags.bucketIndex, + }) + + // Verify the bucket exists + spinner.text = 'Verifying launch pool bucket...' + const bucket = await safeFetchLaunchPoolBucketV2(this.context.umi, bucketPda) + + if (!bucket) { + spinner.fail('Launch pool bucket not found') + this.error(`Launch pool bucket not found at index ${flags.bucketIndex}. Make sure the bucket has been created.`) + } + + // Verify the deposit exists + const [depositPda] = findLaunchPoolDepositV2Pda(this.context.umi, { + bucket: bucketPda, + recipient: this.context.signer.publicKey, + }) + + spinner.text = 'Verifying deposit...' + const deposit = await safeFetchLaunchPoolDepositV2(this.context.umi, depositPda) + + if (!deposit) { + spinner.fail('Deposit not found') + this.error(`No deposit found for signer ${this.context.signer.publicKey}. Make sure you have deposited into this launch pool.`) + } + + // Parse and validate amount + let amount: bigint + try { + amount = BigInt(flags.amount) + } catch { + this.error(`Invalid amount "${flags.amount}". Must be a non-negative integer.`) + } + + if (amount <= 0n) { + this.error('Withdrawal amount must be greater than 0.') + } + + // Build the withdraw transaction + spinner.text = 'Withdrawing from launch pool...' + const transaction = withdrawLaunchPoolV2(this.context.umi, { + genesisAccount: genesisAddress, + bucket: bucketPda, + baseMint: genesisAccount.baseMint, + quoteMint: genesisAccount.quoteMint, + withdrawer: this.context.signer, + payer: this.context.payer, + amountQuoteToken: amount, + }) + + const result = await umiSendAndConfirmTransaction(this.context.umi, transaction) + + spinner.succeed('Withdrawal successful!') + + this.log('') + this.logSuccess(`Withdrew ${flags.amount} quote tokens`) + this.log('') + this.log('Withdrawal Details:') + this.log(` Genesis Account: ${genesisAddress}`) + this.log(` Bucket: ${bucketPda}`) + this.log(` Bucket Index: ${flags.bucketIndex}`) + this.log(` Deposit PDA: ${depositPda}`) + this.log(` Amount: ${flags.amount}`) + this.log('') + this.log(`Transaction: ${txSignatureToString(result.transaction.signature as Uint8Array)}`) + this.log('') + this.log( + generateExplorerUrl( + this.context.explorer, + this.context.chain, + txSignatureToString(result.transaction.signature as Uint8Array), + 'transaction' + ) + ) + + } catch (error) { + spinner.fail('Failed to withdraw') + throw error + } + } +} diff --git a/src/lib/Context.ts b/src/lib/Context.ts index 26fbbb5..1595808 100644 --- a/src/lib/Context.ts +++ b/src/lib/Context.ts @@ -16,6 +16,7 @@ import { join } from 'node:path' import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata' import { mplToolbox } from '@metaplex-foundation/mpl-toolbox' import { mplDistro } from '@metaplex-foundation/mpl-distro' +import { genesis } from '@metaplex-foundation/genesis' import { IrysUploaderOptions } from '@metaplex-foundation/umi-uploader-irys' import { createSignerFromFile } from './FileSigner.js' import { createSignerFromLedgerPath } from './LedgerSigner.js' @@ -165,6 +166,7 @@ export const createContext = async (configPath: string, overrides: ConfigJson, i .use(mplBubblegum()) .use(mplDistro()) .use(mplCandyMachine()) + .use(genesis()) .use(dasApi()) const storageProvider = await initStorageProvider(config) diff --git a/src/lib/genesis.ts b/src/lib/genesis.ts new file mode 100644 index 0000000..7fced3d --- /dev/null +++ b/src/lib/genesis.ts @@ -0,0 +1,40 @@ +// Shared Genesis constants + +// Key types from Genesis (enum values) +export const KEY_TYPES: Record = { + 0: 'Uninitialized', + 1: 'GenesisAccount', + 2: 'LaunchPoolBucket', + 3: 'LaunchPoolDeposit', + 4: 'StreamflowBucket', + 5: 'UnlockedBucket', + 6: 'MeteoraBucket', + 7: 'PumpBucket', + 8: 'DistributionBucket', + 9: 'PresaleBucket', + 10: 'PresaleDeposit', + 11: 'VaultBucket', + 12: 'VaultDeposit', + 13: 'BondingCurveBucket', + 14: 'AuctionBucket', + 15: 'AuctionBid', + 16: 'AuctionTree', + 17: 'RaydiumCpmmBucket', + 18: 'GenesisAccountV2', + 19: 'PresaleBucketV2', + 20: 'PresaleDepositV2', + 21: 'UnlockedBucketV2', + 22: 'RaydiumCpmmBucketV2', + 23: 'VaultBucketV2', + 24: 'VaultDepositV2', + 25: 'BondingCurveBucketV2', + 26: 'LaunchPoolBucketV2', + 27: 'LaunchPoolDepositV2', +} + +// Funding modes +export const FUNDING_MODES: Record = { + 0: 'NewMint', + 1: 'Transfer', + 2: 'ExistingMint', +} diff --git a/test/commands/genesis/genesis-manual-test.sh b/test/commands/genesis/genesis-manual-test.sh new file mode 100644 index 0000000..9d5eef1 --- /dev/null +++ b/test/commands/genesis/genesis-manual-test.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# Genesis CLI Manual Test Script +# Tests the full lifecycle with optional flags using a local validator with devnet genesis.so +# +# Prerequisites: +# 1. Local validator running: npm run validator +# 2. CLI built: pnpm run build +# 3. RPC set to localhost: ./bin/dev.js config set rpcUrl http://localhost:8899 +# +# Usage: +# bash test/commands/genesis/genesis-manual-test.sh + +set -euo pipefail + +CLI="./bin/dev.js" +WALLET="Tes1zkZkXhgTaMFqVgbgvMsVkRJpq4Y6g54SbDBeKVV" +NOW=$(date +%s) +DEPOSIT_START=$((NOW - 3600)) +DEPOSIT_END=$((NOW + 86400)) +CLAIM_START=$((DEPOSIT_END + 1)) +CLAIM_END=$((NOW + 86400 * 365)) + +pass() { echo " PASS: $1"; } +fail() { echo " FAIL: $1"; exit 1; } + +extract_address() { + echo "$1" | grep -E -o "$2: [A-Za-z0-9]+" | sed "s/^$2: //" | head -1 +} + +echo "=== Genesis CLI Manual Test ===" +echo "" + +# --- Setup --- +echo "[Setup] Airdropping SOL..." +$CLI toolbox sol airdrop 100 "$WALLET" 2>&1 | grep -q "Airdropped" && pass "Airdrop" || fail "Airdrop" +sleep 2 + +echo "[Setup] Wrapping SOL..." +$CLI toolbox sol wrap 50 2>&1 | grep -q "Wrapped" && pass "Wrap SOL" || fail "Wrap SOL" + +echo "" +echo "=== Test 1: Launch Pool with Optional Flags ===" +echo "" + +# Step 1: Create genesis +echo "[1] Creating genesis account..." +CREATE_OUT=$($CLI genesis create --name "TestToken" --symbol "TST" --totalSupply 1000000000000000 --decimals 9 2>&1) +GENESIS=$(extract_address "$CREATE_OUT" "Genesis Account") +echo " Genesis: $GENESIS" +[ -n "$GENESIS" ] && pass "Create genesis" || fail "Create genesis" + +# Step 2: Add unlocked bucket (end behavior destination) +echo "[2] Adding unlocked bucket (index 0)..." +UNLOCKED_OUT=$($CLI genesis bucket add-unlocked "$GENESIS" \ + --recipient "$WALLET" \ + --claimStart "$CLAIM_START" \ + --allocation 0 \ + --bucketIndex 0 2>&1) +UNLOCKED_ADDR=$(extract_address "$UNLOCKED_OUT" "Bucket Address") +echo " Unlocked bucket: $UNLOCKED_ADDR" +[ -n "$UNLOCKED_ADDR" ] && pass "Add unlocked bucket" || fail "Add unlocked bucket" + +# Step 3: Add launch pool with all optional flags +echo "[3] Adding launch pool with optional flags (index 0)..." +LP_OUT=$($CLI genesis bucket add-launch-pool "$GENESIS" \ + --allocation 1000000000000000 \ + --depositStart "$DEPOSIT_START" \ + --depositEnd "$DEPOSIT_END" \ + --claimStart "$CLAIM_START" \ + --claimEnd "$CLAIM_END" \ + --bucketIndex 0 \ + --endBehavior "$UNLOCKED_ADDR:10000" \ + --minimumDeposit 100000000 \ + --depositLimit 10000000000 \ + --minimumQuoteTokenThreshold 500000000 \ + --depositPenalty '{"slopeBps":0,"interceptBps":0,"maxBps":0,"startTime":0,"endTime":0}' \ + --withdrawPenalty '{"slopeBps":0,"interceptBps":0,"maxBps":0,"startTime":0,"endTime":0}' \ + --bonusSchedule '{"slopeBps":0,"interceptBps":0,"maxBps":0,"startTime":0,"endTime":0}' \ + 2>&1) +echo "$LP_OUT" | grep -q "Launch pool bucket added" && pass "Add launch pool with flags" || fail "Add launch pool with flags" + +# Step 4: Finalize +echo "[4] Finalizing genesis..." +$CLI genesis finalize "$GENESIS" 2>&1 | grep -q "finalized successfully" && pass "Finalize" || fail "Finalize" + +# Step 5: Test minimumDeposit enforcement +echo "[5] Depositing below minimum (should fail)..." +if $CLI genesis deposit "$GENESIS" --amount 50000000 --bucketIndex 0 2>&1 | grep -q "below the minimum"; then + pass "Minimum deposit enforced" +else + fail "Minimum deposit NOT enforced" +fi + +# Step 6: Test depositLimit enforcement +echo "[6] Depositing above limit (should fail)..." +if $CLI genesis deposit "$GENESIS" --amount 10000000001 --bucketIndex 0 2>&1 | grep -q "exceeds the deposit limit"; then + pass "Deposit limit enforced" +else + fail "Deposit limit NOT enforced" +fi + +# Step 7: Valid deposit +echo "[7] Depositing 1 SOL (within bounds)..." +$CLI genesis deposit "$GENESIS" --amount 1000000000 --bucketIndex 0 2>&1 | grep -q "Deposit successful" && pass "Deposit 1 SOL" || fail "Deposit 1 SOL" + +# Step 8: Second deposit +echo "[8] Depositing 5 SOL (within bounds)..." +$CLI genesis deposit "$GENESIS" --amount 5000000000 --bucketIndex 0 2>&1 | grep -q "Deposit successful" && pass "Deposit 5 SOL" || fail "Deposit 5 SOL" + +# Step 9: Withdraw +echo "[9] Withdrawing 1 SOL..." +$CLI genesis withdraw "$GENESIS" --amount 1000000000 --bucketIndex 0 2>&1 | grep -q "Withdrawal successful" && pass "Withdraw 1 SOL" || fail "Withdraw 1 SOL" + +# Step 10: Verify bucket state +echo "[10] Fetching bucket state..." +BUCKET_OUT=$($CLI genesis bucket fetch "$GENESIS" --bucketIndex 0 2>&1) +echo "$BUCKET_OUT" | grep -q "Deposit Count: 1" && pass "Deposit count correct" || fail "Deposit count" +echo "$BUCKET_OUT" | grep -q "Claim Count: 0" && pass "Claim count correct" || fail "Claim count" + +# Step 11: Revoke mint +echo "[11] Revoking mint authority..." +$CLI genesis revoke "$GENESIS" --revokeMint 2>&1 | grep -q "Authorities revoked" && pass "Revoke mint" || fail "Revoke mint" + +echo "" +echo "=== Test 2: Presale Workflow ===" +echo "" + +# Create a new genesis for presale test +echo "[12] Creating genesis for presale..." +CREATE2_OUT=$($CLI genesis create --name "PresaleTest" --symbol "PST" --totalSupply 1000000000000000 --decimals 9 2>&1) +GENESIS2=$(extract_address "$CREATE2_OUT" "Genesis Account") +echo " Genesis: $GENESIS2" +[ -n "$GENESIS2" ] && pass "Create presale genesis" || fail "Create presale genesis" + +# Add presale bucket +echo "[13] Adding presale bucket (index 0)..." +$CLI genesis bucket add-presale "$GENESIS2" \ + --allocation 1000000000000000 \ + --quoteCap 10000000000 \ + --depositStart "$DEPOSIT_START" \ + --depositEnd "$DEPOSIT_END" \ + --claimStart "$CLAIM_START" \ + --bucketIndex 0 \ + --minimumDeposit 100000000 \ + --depositLimit 5000000000 \ + 2>&1 | grep -q "Presale bucket added" && pass "Add presale bucket" || fail "Add presale bucket" + +# Fetch presale bucket +echo "[14] Fetching presale bucket..." +$CLI genesis bucket fetch "$GENESIS2" --bucketIndex 0 --type presale 2>&1 | grep -q "Presale Bucket" && pass "Fetch presale bucket" || fail "Fetch presale bucket" + +# Finalize +echo "[15] Finalizing presale genesis..." +$CLI genesis finalize "$GENESIS2" 2>&1 | grep -q "finalized successfully" && pass "Finalize presale" || fail "Finalize presale" + +# Deposit into presale +echo "[16] Depositing 2 SOL into presale..." +$CLI genesis presale deposit "$GENESIS2" --amount 2000000000 --bucketIndex 0 2>&1 | grep -q "Presale deposit successful" && pass "Presale deposit" || fail "Presale deposit" + +# Fetch to verify deposit +echo "[17] Verifying presale bucket state..." +PRESALE_OUT=$($CLI genesis bucket fetch "$GENESIS2" --bucketIndex 0 --type presale 2>&1) +echo "$PRESALE_OUT" | grep -q "Deposit Count: 1" && pass "Presale deposit count" || fail "Presale deposit count" + +echo "" +echo "=== Test 3: Unlocked Bucket Workflow ===" +echo "" + +# Create genesis for unlocked test +echo "[18] Creating genesis for unlocked test..." +CREATE3_OUT=$($CLI genesis create --name "UnlockedTest" --symbol "UNL" --totalSupply 1000000000000000 --decimals 9 2>&1) +GENESIS3=$(extract_address "$CREATE3_OUT" "Genesis Account") +echo " Genesis: $GENESIS3" +[ -n "$GENESIS3" ] && pass "Create unlocked genesis" || fail "Create unlocked genesis" + +# Add unlocked bucket with allocation +echo "[19] Adding unlocked bucket with 1M token allocation..." +$CLI genesis bucket add-unlocked "$GENESIS3" \ + --recipient "$WALLET" \ + --claimStart "$CLAIM_START" \ + --allocation 1000000000000000 \ + --bucketIndex 0 \ + 2>&1 | grep -q "Unlocked bucket added" && pass "Add unlocked with allocation" || fail "Add unlocked with allocation" + +# Fetch unlocked bucket +echo "[20] Fetching unlocked bucket..." +UNLOCK_OUT=$($CLI genesis bucket fetch "$GENESIS3" --bucketIndex 0 --type unlocked 2>&1) +echo "$UNLOCK_OUT" | grep -q "Unlocked Bucket" && pass "Fetch unlocked bucket" || fail "Fetch unlocked bucket" +echo "$UNLOCK_OUT" | grep -q "Claimed: No" && pass "Not yet claimed" || fail "Not yet claimed" + +echo "" +echo "=== All tests passed! ===" diff --git a/test/commands/genesis/genesis.create.test.ts b/test/commands/genesis/genesis.create.test.ts new file mode 100644 index 0000000..dfba8aa --- /dev/null +++ b/test/commands/genesis/genesis.create.test.ts @@ -0,0 +1,177 @@ +import { expect } from 'chai' +import { runCli } from '../../runCli' +import { createGenesisAccount, stripAnsi, extractGenesisAddress, extractBaseMint } from './genesishelpers' + +describe('genesis create and fetch commands', () => { + + before(async () => { + // runCli rejects on non-zero exit, so failures propagate automatically + await runCli( + ["toolbox", "sol", "airdrop", "100", "TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx"] + ) + + await new Promise(resolve => setTimeout(resolve, 10000)) + }) + + it('creates a new genesis account with required flags', async () => { + const cliInput = [ + 'genesis', + 'create', + '--name', + 'Test Token', + '--symbol', + 'TST', + '--totalSupply', + '1000000000', + ] + + const { stdout, stderr, code } = await runCli(cliInput) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + const genesisAddress = extractGenesisAddress(cleanStdout) || extractGenesisAddress(cleanStderr) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Genesis account created successfully') + expect(genesisAddress).to.match(/^[a-zA-Z0-9]+$/) + expect(cleanStdout).to.contain('Name: Test Token') + expect(cleanStdout).to.contain('Symbol: TST') + expect(cleanStdout).to.contain('Total Supply: 1000000000') + expect(cleanStdout).to.contain('Decimals: 9') + expect(cleanStdout).to.contain('Funding Mode: new-mint') + }) + + it('creates a genesis account with custom decimals', async () => { + const cliInput = [ + 'genesis', + 'create', + '--name', + 'Six Decimal Token', + '--symbol', + 'SDT', + '--totalSupply', + '500000000', + '--decimals', + '6', + ] + + const { stdout, stderr, code } = await runCli(cliInput) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Genesis account created successfully') + expect(cleanStdout).to.contain('Decimals: 6') + expect(cleanStdout).to.contain('Name: Six Decimal Token') + expect(cleanStdout).to.contain('Symbol: SDT') + }) + + it('creates a genesis account with a custom URI', async () => { + const cliInput = [ + 'genesis', + 'create', + '--name', + 'URI Token', + '--symbol', + 'URI', + '--totalSupply', + '1000000000', + '--uri', + 'https://example.com/metadata.json', + ] + + const { stdout, stderr, code } = await runCli(cliInput) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Genesis account created successfully') + }) + + it('fetches a genesis account after creation', async () => { + const { genesisAddress } = await createGenesisAccount({ + name: 'Fetch Test Token', + symbol: 'FTT', + totalSupply: '2000000000', + }) + + // Wait for on-chain state + await new Promise(resolve => setTimeout(resolve, 2000)) + + const cliInput = [ + 'genesis', + 'fetch', + genesisAddress, + ] + + const { stdout, stderr, code } = await runCli(cliInput) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Genesis account fetched successfully') + expect(cleanStdout).to.contain(`Genesis Account: ${genesisAddress}`) + expect(cleanStdout).to.contain('Finalized: No') + expect(cleanStdout).to.contain('Total Supply (Base Token): 2000000000') + expect(cleanStdout).to.contain('Funding Mode: NewMint') + expect(cleanStdout).to.contain('Bucket Count: 0') + }) + + it('fails when required flags are missing for create', async () => { + const cliInput = [ + 'genesis', + 'create', + '--name', + 'Incomplete Token', + // Missing --symbol and --totalSupply + ] + + try { + await runCli(cliInput) + expect.fail('Should have thrown an error for missing required flags') + } catch (error) { + expect((error as Error).message).to.contain('Missing required flag') + } + }) + + it('fails when fetching a non-existent genesis account', async () => { + const cliInput = [ + 'genesis', + 'fetch', + '11111111111111111111111111111111', + ] + + try { + await runCli(cliInput) + expect.fail('Should have thrown an error for non-existent account') + } catch (error) { + expect((error as Error).message).to.not.be.empty + } + }) + + it('fails with transfer funding mode when baseMint is missing', async () => { + const cliInput = [ + 'genesis', + 'create', + '--name', + 'Transfer Token', + '--symbol', + 'TFR', + '--totalSupply', + '1000000000', + '--fundingMode', + 'transfer', + // Missing --baseMint + ] + + try { + await runCli(cliInput) + expect.fail('Should have thrown an error for missing baseMint') + } catch (error) { + expect((error as Error).message).to.contain('baseMint is required') + } + }) +}) diff --git a/test/commands/genesis/genesis.integration.test.ts b/test/commands/genesis/genesis.integration.test.ts new file mode 100644 index 0000000..b3b1cfa --- /dev/null +++ b/test/commands/genesis/genesis.integration.test.ts @@ -0,0 +1,353 @@ +import { expect } from 'chai' +import { runCli } from '../../runCli' +import { createGenesisAccount, addLaunchPoolBucket, addUnlockedBucket, stripAnsi } from './genesishelpers' + +describe('genesis integration workflow', () => { + let genesisAddress: string + let bucketAddress: string + let unlockedBucketAddress: string + + // Timestamps for the launch pool + const now = Math.floor(Date.now() / 1000) + const depositStart = (now - 3600).toString() // 1 hour ago + const depositEnd = (now + 86400).toString() // 1 day from now + const claimStart = (now + 86400 + 1).toString() // just after deposit end + const claimEnd = (now + 86400 * 365).toString() // 1 year from now + + before(async () => { + // Airdrop SOL for testing + await runCli([ + "toolbox", "sol", "airdrop", "100", "TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx" + ]) + + await new Promise(resolve => setTimeout(resolve, 10000)) + + // Wrap some SOL to get wrapped SOL tokens (needed for deposits) + await runCli([ + 'toolbox', + 'sol', + 'wrap', + '50', + ]) + }) + + it('creates a genesis account for the workflow', async () => { + const result = await createGenesisAccount({ + name: 'Integration Token', + symbol: 'INT', + totalSupply: '1000000000', + decimals: 9, + }) + + genesisAddress = result.genesisAddress + + expect(genesisAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('adds an unlocked bucket as graduation destination', async () => { + const result = await addUnlockedBucket( + genesisAddress, + 'TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx', + { + allocation: '0', + claimStart, + claimEnd, + } + ) + + unlockedBucketAddress = result.bucketAddress + + expect(unlockedBucketAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('adds a launch pool bucket to the genesis account', async () => { + const result = await addLaunchPoolBucket(genesisAddress, { + allocation: '1000000000', + depositStart, + depositEnd, + claimStart, + claimEnd, + endBehavior: [`${unlockedBucketAddress}:10000`], + }) + + bucketAddress = result.bucketAddress + + expect(bucketAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('fetches the genesis account and verifies bucket was added', async () => { + await new Promise(resolve => setTimeout(resolve, 2000)) + + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'fetch', + genesisAddress, + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Genesis account fetched successfully') + expect(cleanStdout).to.contain(`Genesis Account: ${genesisAddress}`) + expect(cleanStdout).to.contain('Bucket Count: 2') + expect(cleanStdout).to.contain('Finalized: No') + }) + + it('fetches the launch pool bucket details', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'bucket', + 'fetch', + genesisAddress, + '--bucketIndex', + '0', + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Bucket fetched successfully') + expect(cleanStdout).to.contain('Launch Pool Bucket') + expect(cleanStdout).to.contain('Base Token Allocation: 1000000000') + expect(cleanStdout).to.contain('Deposit Count: 0') + expect(cleanStdout).to.contain('Claim Count: 0') + }) + + it('finalizes the genesis launch', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'finalize', + genesisAddress, + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Genesis launch finalized successfully') + expect(cleanStdout).to.contain(`Genesis Account: ${genesisAddress}`) + expect(cleanStdout).to.contain('Status: Finalized') + expect(cleanStdout).to.contain('Transaction:') + }) + + it('deposits into the launch pool', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'deposit', + genesisAddress, + '--amount', + '1000000000', // 1 SOL in lamports + '--bucketIndex', + '0', + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Deposit successful') + expect(cleanStdout).to.contain(`Genesis Account: ${genesisAddress}`) + expect(cleanStdout).to.contain('Bucket Index: 0') + expect(cleanStdout).to.contain('Amount: 1000000000') + expect(cleanStdout).to.contain('Transaction:') + }) + + it('verifies the genesis account is now finalized', async () => { + await new Promise(resolve => setTimeout(resolve, 2000)) + + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'fetch', + genesisAddress, + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStdout).to.contain('Finalized: Yes') + }) + + it('fails to finalize an already-finalized genesis account', async () => { + try { + await runCli([ + 'genesis', + 'finalize', + genesisAddress, + ]) + expect.fail('Should have thrown an error for already-finalized account') + } catch (error) { + expect((error as Error).message).to.contain('already been finalized') + } + }) + + it('fails to add a bucket to a finalized genesis account', async () => { + try { + await runCli([ + 'genesis', + 'bucket', + 'add-launch-pool', + genesisAddress, + '--allocation', + '100000000', + '--depositStart', + depositStart, + '--depositEnd', + depositEnd, + '--claimStart', + claimStart, + '--claimEnd', + claimEnd, + ]) + expect.fail('Should have thrown an error for finalized account') + } catch (error) { + expect((error as Error).message).to.contain('finalized') + } + }) + + it('revokes mint authority', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'revoke', + genesisAddress, + '--revokeMint', + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Authorities revoked successfully') + expect(cleanStdout).to.contain('Mint Authority Revoked: Yes') + expect(cleanStdout).to.contain('Freeze Authority Revoked: No') + expect(cleanStdout).to.contain('WARNING') + }) + + it('fails when no revoke flag is specified', async () => { + try { + await runCli([ + 'genesis', + 'revoke', + genesisAddress, + ]) + expect.fail('Should have thrown an error when no revoke flag is specified') + } catch (error) { + expect((error as Error).message).to.contain('revokeMint') + } + }) + + it('fails to deposit into a non-existent bucket', async () => { + try { + await runCli([ + 'genesis', + 'deposit', + genesisAddress, + '--amount', + '1000000', + '--bucketIndex', + '99', + ]) + expect.fail('Should have thrown an error for non-existent bucket') + } catch (error) { + expect((error as Error).message).to.not.be.empty + } + }) + + it('fails to fetch a non-existent bucket', async () => { + try { + await runCli([ + 'genesis', + 'bucket', + 'fetch', + genesisAddress, + '--bucketIndex', + '99', + ]) + expect.fail('Should have thrown an error for non-existent bucket') + } catch (error) { + expect((error as Error).message).to.contain('not found') + } + }) +}) + +describe('genesis unlocked bucket workflow', () => { + let genesisAddress: string + + const now = Math.floor(Date.now() / 1000) + const claimStart = (now - 3600).toString() // 1 hour ago (so claim is active) + const claimEnd = (now + 86400 * 365).toString() // 1 year from now + + before(async () => { + await runCli([ + "toolbox", "sol", "airdrop", "100", "TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx" + ]) + + await new Promise(resolve => setTimeout(resolve, 10000)) + }) + + it('creates a genesis account with unlocked bucket', async () => { + const result = await createGenesisAccount({ + name: 'Unlocked Token', + symbol: 'UNL', + totalSupply: '1000000000', + decimals: 9, + }) + + genesisAddress = result.genesisAddress + + expect(genesisAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('adds an unlocked bucket', async () => { + const result = await addUnlockedBucket( + genesisAddress, + 'TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx', + { + allocation: '100000000', + claimStart, + claimEnd, + } + ) + + expect(result.bucketAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('fetches the unlocked bucket details', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'bucket', + 'fetch', + genesisAddress, + '--bucketIndex', + '0', + '--type', + 'unlocked', + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Bucket fetched successfully') + expect(cleanStdout).to.contain('Unlocked Bucket') + expect(cleanStdout).to.contain('Base Token Allocation: 100000000') + expect(cleanStdout).to.contain('Claimed: No') + }) + + it('fails to claim unlocked bucket before finalization', async () => { + try { + await runCli([ + 'genesis', + 'claim-unlocked', + genesisAddress, + '--bucketIndex', + '0', + ]) + expect.fail('Should have thrown an error before finalization') + } catch (error) { + expect((error as Error).message).to.not.be.empty + } + }) +}) diff --git a/test/commands/genesis/genesis.presale.test.ts b/test/commands/genesis/genesis.presale.test.ts new file mode 100644 index 0000000..e15e4ef --- /dev/null +++ b/test/commands/genesis/genesis.presale.test.ts @@ -0,0 +1,152 @@ +import { expect } from 'chai' +import { runCli } from '../../runCli' +import { createGenesisAccount, addPresaleBucket, stripAnsi } from './genesishelpers' + +describe('genesis presale workflow', () => { + let genesisAddress: string + let bucketAddress: string + + const now = Math.floor(Date.now() / 1000) + const depositStart = (now - 3600).toString() // 1 hour ago + const depositEnd = (now + 86400).toString() // 1 day from now + const claimStart = (now + 86400 + 1).toString() // just after deposit end + const claimEnd = (now + 86400 * 365).toString() // 1 year from now + + before(async () => { + // runCli rejects on non-zero exit, so failures propagate automatically + await runCli([ + "toolbox", "sol", "airdrop", "100", "TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx" + ]) + + await new Promise(resolve => setTimeout(resolve, 10000)) + + await runCli([ + 'toolbox', + 'sol', + 'wrap', + '50', + ]) + }) + + it('creates a genesis account for presale workflow', async () => { + const result = await createGenesisAccount({ + name: 'Presale Token', + symbol: 'PSL', + totalSupply: '1000000000', + decimals: 9, + }) + + genesisAddress = result.genesisAddress + + expect(genesisAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('adds a presale bucket to the genesis account', async () => { + const result = await addPresaleBucket(genesisAddress, { + allocation: '500000000', + quoteCap: '1000000000', + depositStart, + depositEnd, + claimStart, + claimEnd, + }) + + bucketAddress = result.bucketAddress + + expect(bucketAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('fetches the presale bucket details', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'bucket', + 'fetch', + genesisAddress, + '--bucketIndex', + '0', + '--type', + 'presale', + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Bucket fetched successfully') + expect(cleanStdout).to.contain('Presale Bucket') + expect(cleanStdout).to.contain('Base Token Allocation: 500000000') + expect(cleanStdout).to.contain('Quote Token Cap: 1000000000') + }) + + it('deposits into the presale bucket', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'presale', + 'deposit', + genesisAddress, + '--amount', + '1000000000', + '--bucketIndex', + '0', + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Presale deposit successful') + expect(cleanStdout).to.contain(`Genesis Account: ${genesisAddress}`) + expect(cleanStdout).to.contain('Amount: 1000000000') + expect(cleanStdout).to.contain('Transaction:') + }) + + it('fails to deposit into a non-existent presale bucket', async () => { + try { + await runCli([ + 'genesis', + 'presale', + 'deposit', + genesisAddress, + '--amount', + '1000000', + '--bucketIndex', + '99', + ]) + expect.fail('Should have thrown an error for non-existent bucket') + } catch (error) { + expect((error as Error).message).to.not.be.empty + } + }) + + it('fails to claim from presale with no deposit', async () => { + // Create a new genesis with a presale bucket but no deposit + const newGenesis = await createGenesisAccount({ + name: 'No Deposit Presale', + symbol: 'NDP', + totalSupply: '1000000000', + }) + + await addPresaleBucket(newGenesis.genesisAddress, { + allocation: '500000000', + quoteCap: '1000000000', + depositStart, + depositEnd, + claimStart, + claimEnd, + }) + + try { + await runCli([ + 'genesis', + 'presale', + 'claim', + newGenesis.genesisAddress, + '--bucketIndex', + '0', + ]) + expect.fail('Should have thrown an error for no deposit') + } catch (error) { + expect((error as Error).message).to.not.be.empty + } + }) +}) diff --git a/test/commands/genesis/genesis.withdraw.test.ts b/test/commands/genesis/genesis.withdraw.test.ts new file mode 100644 index 0000000..976c5aa --- /dev/null +++ b/test/commands/genesis/genesis.withdraw.test.ts @@ -0,0 +1,173 @@ +import { expect } from 'chai' +import { runCli } from '../../runCli' +import { createGenesisAccount, addLaunchPoolBucket, addUnlockedBucket, stripAnsi } from './genesishelpers' + +describe('genesis withdraw workflow', () => { + let genesisAddress: string + let bucketAddress: string + let unlockedBucketAddress: string + + const now = Math.floor(Date.now() / 1000) + const depositStart = (now - 3600).toString() // 1 hour ago + const depositEnd = (now + 86400).toString() // 1 day from now + const claimStart = (now + 86400 + 1).toString() // just after deposit end + const claimEnd = (now + 86400 * 365).toString() // 1 year from now + + before(async () => { + await runCli([ + "toolbox", "sol", "airdrop", "100", "TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx" + ]) + + await new Promise(resolve => setTimeout(resolve, 10000)) + + await runCli([ + 'toolbox', + 'sol', + 'wrap', + '50', + ]) + }) + + it('creates a genesis account for withdraw workflow', async () => { + const result = await createGenesisAccount({ + name: 'Withdraw Token', + symbol: 'WTH', + totalSupply: '1000000000', + decimals: 9, + }) + + genesisAddress = result.genesisAddress + + expect(genesisAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('adds an unlocked bucket as graduation destination', async () => { + const result = await addUnlockedBucket( + genesisAddress, + 'TESTfCYwTPxME2cAnPcKvvF5xdPah3PY7naYQEP2kkx', + { + allocation: '0', + claimStart, + claimEnd, + } + ) + + unlockedBucketAddress = result.bucketAddress + + expect(unlockedBucketAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('adds a launch pool bucket', async () => { + const result = await addLaunchPoolBucket(genesisAddress, { + allocation: '1000000000', + depositStart, + depositEnd, + claimStart, + claimEnd, + endBehavior: [`${unlockedBucketAddress}:10000`], + }) + + bucketAddress = result.bucketAddress + + expect(bucketAddress).to.match(/^[a-zA-Z0-9]+$/) + }) + + it('finalizes the genesis account', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'finalize', + genesisAddress, + ]) + + const cleanStderr = stripAnsi(stderr) + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Genesis launch finalized successfully') + }) + + it('deposits into the launch pool', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'deposit', + genesisAddress, + '--amount', + '1000000000', + '--bucketIndex', + '0', + ]) + + const cleanStderr = stripAnsi(stderr) + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Deposit successful') + }) + + it('withdraws from the launch pool', async () => { + const { stdout, stderr, code } = await runCli([ + 'genesis', + 'withdraw', + genesisAddress, + '--amount', + '500000000', + '--bucketIndex', + '0', + ]) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Withdrawal successful') + expect(cleanStdout).to.contain(`Genesis Account: ${genesisAddress}`) + expect(cleanStdout).to.contain('Bucket Index: 0') + expect(cleanStdout).to.contain('Amount: 500000000') + expect(cleanStdout).to.contain('Transaction:') + }) + + it('fails to withdraw from a non-existent bucket', async () => { + try { + await runCli([ + 'genesis', + 'withdraw', + genesisAddress, + '--amount', + '1000000', + '--bucketIndex', + '99', + ]) + expect.fail('Should have thrown an error for non-existent bucket') + } catch (error) { + expect((error as Error).message).to.not.be.empty + } + }) + + it('fails to withdraw without a deposit', async () => { + // Create a new genesis with a launch pool but no deposit + const newGenesis = await createGenesisAccount({ + name: 'No Deposit Token', + symbol: 'NDT', + totalSupply: '1000000000', + }) + + await addLaunchPoolBucket(newGenesis.genesisAddress, { + allocation: '500000000', + depositStart, + depositEnd, + claimStart, + claimEnd, + }) + + try { + await runCli([ + 'genesis', + 'withdraw', + newGenesis.genesisAddress, + '--amount', + '1000000', + '--bucketIndex', + '0', + ]) + expect.fail('Should have thrown an error for no deposit') + } catch (error) { + expect((error as Error).message).to.not.be.empty + } + }) +}) diff --git a/test/commands/genesis/genesishelpers.ts b/test/commands/genesis/genesishelpers.ts new file mode 100644 index 0000000..d36f4df --- /dev/null +++ b/test/commands/genesis/genesishelpers.ts @@ -0,0 +1,263 @@ +import { expect } from 'chai' +import { runCli } from '../../runCli' + +// Helper to strip ANSI color codes +const stripAnsi = (str: string) => str.replace(/\u001b\[[\d;]*m/g, '') + +// Helper to extract Genesis Account address from output +const extractGenesisAddress = (str: string) => { + const patterns = [ + /Genesis Account: ([a-zA-Z0-9]+)/, + ] + + for (const pattern of patterns) { + const match = str.match(pattern) + if (match) return match[1] + } + return null +} + +// Helper to extract Base Mint address from output +const extractBaseMint = (str: string) => { + const patterns = [ + /Base Mint: ([a-zA-Z0-9]+)/, + ] + + for (const pattern of patterns) { + const match = str.match(pattern) + if (match) return match[1] + } + return null +} + +// Helper to extract Bucket Address from output +const extractBucketAddress = (str: string) => { + const patterns = [ + /Bucket Address: ([a-zA-Z0-9]+)/, + /Bucket: ([a-zA-Z0-9]+)/, + ] + + for (const pattern of patterns) { + const match = str.match(pattern) + if (match) return match[1] + } + return null +} + +const createGenesisAccount = async (options?: { + name?: string + symbol?: string + totalSupply?: string + decimals?: number +}): Promise<{ genesisAddress: string; baseMint: string }> => { + const name = options?.name ?? 'Test Token' + const symbol = options?.symbol ?? 'TST' + const totalSupply = options?.totalSupply ?? '1000000000' + const decimals = options?.decimals ?? 9 + + const cliInput = [ + 'genesis', + 'create', + '--name', + name, + '--symbol', + symbol, + '--totalSupply', + totalSupply, + '--decimals', + decimals.toString(), + ] + + const { stdout, stderr, code } = await runCli(cliInput) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + const genesisAddress = extractGenesisAddress(cleanStdout) || extractGenesisAddress(cleanStderr) + const baseMint = extractBaseMint(cleanStdout) || extractBaseMint(cleanStderr) + + if (!genesisAddress) { + throw new Error(`Genesis address not found in output.\nstdout: ${cleanStdout}\nstderr: ${cleanStderr}`) + } + + if (!baseMint) { + throw new Error(`Base mint not found in output.\nstdout: ${cleanStdout}\nstderr: ${cleanStderr}`) + } + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Genesis account created successfully') + expect(genesisAddress).to.match(/^[a-zA-Z0-9]+$/) + + return { genesisAddress, baseMint } +} + +const addLaunchPoolBucket = async ( + genesisAddress: string, + options?: { + allocation?: string + depositStart?: string + depositEnd?: string + claimStart?: string + claimEnd?: string + endBehavior?: string[] + } +): Promise<{ bucketAddress: string }> => { + // Use timestamps in the past so deposits are immediately active + const now = Math.floor(Date.now() / 1000) + const allocation = options?.allocation ?? '500000000' + const depositStart = options?.depositStart ?? (now - 3600).toString() + const depositEnd = options?.depositEnd ?? (now + 86400).toString() + const claimStart = options?.claimStart ?? (now + 86400).toString() + const claimEnd = options?.claimEnd ?? (now + 86400 * 365).toString() + + const cliInput = [ + 'genesis', + 'bucket', + 'add-launch-pool', + genesisAddress, + '--allocation', + allocation, + '--depositStart', + depositStart, + '--depositEnd', + depositEnd, + '--claimStart', + claimStart, + '--claimEnd', + claimEnd, + ] + + if (options?.endBehavior) { + for (const eb of options.endBehavior) { + cliInput.push('--endBehavior', eb) + } + } + + const { stdout, stderr, code } = await runCli(cliInput) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + const bucketAddress = extractBucketAddress(cleanStdout) || extractBucketAddress(cleanStderr) + + if (!bucketAddress) { + throw new Error(`Bucket address not found in output.\nstdout: ${cleanStdout}\nstderr: ${cleanStderr}`) + } + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Launch pool bucket added successfully') + + return { bucketAddress } +} + +const addPresaleBucket = async ( + genesisAddress: string, + options?: { + allocation?: string + quoteCap?: string + depositStart?: string + depositEnd?: string + claimStart?: string + claimEnd?: string + bucketIndex?: number + } +): Promise<{ bucketAddress: string }> => { + const now = Math.floor(Date.now() / 1000) + const allocation = options?.allocation ?? '500000000' + const quoteCap = options?.quoteCap ?? '1000000000' + const depositStart = options?.depositStart ?? (now - 3600).toString() + const depositEnd = options?.depositEnd ?? (now + 86400).toString() + const claimStart = options?.claimStart ?? (now + 86400).toString() + const claimEnd = options?.claimEnd ?? (now + 86400 * 365).toString() + const bucketIndex = (options?.bucketIndex ?? 0).toString() + + const cliInput = [ + 'genesis', + 'bucket', + 'add-presale', + genesisAddress, + '--allocation', + allocation, + '--quoteCap', + quoteCap, + '--depositStart', + depositStart, + '--depositEnd', + depositEnd, + '--claimStart', + claimStart, + '--claimEnd', + claimEnd, + '--bucketIndex', + bucketIndex, + ] + + const { stdout, stderr, code } = await runCli(cliInput) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + const bucketAddress = extractBucketAddress(cleanStdout) || extractBucketAddress(cleanStderr) + + if (!bucketAddress) { + throw new Error(`Bucket address not found in output.\nstdout: ${cleanStdout}\nstderr: ${cleanStderr}`) + } + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Presale bucket added successfully') + + return { bucketAddress } +} + +const addUnlockedBucket = async ( + genesisAddress: string, + recipientAddress: string, + options?: { + allocation?: string + claimStart?: string + claimEnd?: string + } +): Promise<{ bucketAddress: string }> => { + const now = Math.floor(Date.now() / 1000) + const allocation = options?.allocation ?? '100000000' + const claimStart = options?.claimStart ?? (now - 3600).toString() + const claimEnd = options?.claimEnd ?? (now + 86400 * 365).toString() + + const cliInput = [ + 'genesis', + 'bucket', + 'add-unlocked', + genesisAddress, + '--recipient', + recipientAddress, + '--allocation', + allocation, + '--claimStart', + claimStart, + '--claimEnd', + claimEnd, + ] + + const { stdout, stderr, code } = await runCli(cliInput) + + const cleanStderr = stripAnsi(stderr) + const cleanStdout = stripAnsi(stdout) + const bucketAddress = extractBucketAddress(cleanStdout) || extractBucketAddress(cleanStderr) + + if (!bucketAddress) { + throw new Error(`Bucket address not found in output.\nstdout: ${cleanStdout}\nstderr: ${cleanStderr}`) + } + + expect(code).to.equal(0) + expect(cleanStderr).to.contain('Unlocked bucket added successfully') + + return { bucketAddress } +} + +export { + stripAnsi, + extractGenesisAddress, + extractBaseMint, + extractBucketAddress, + createGenesisAccount, + addLaunchPoolBucket, + addPresaleBucket, + addUnlockedBucket, +}