diff --git a/scripts/consts.ts b/scripts/consts.ts deleted file mode 100644 index 5d57fdb..0000000 --- a/scripts/consts.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; - -// NOTE: this should match the admin in the initialize file -export const ADMIN_PUBLIC_KEY = new PublicKey("ELT1uRmtFvYP6WSrc4mCZaW7VVbcdkcKAj39aHSVCmwH"); \ No newline at end of file diff --git a/scripts/initialize.ts b/scripts/initialize.ts index ec08b9f..f209275 100644 --- a/scripts/initialize.ts +++ b/scripts/initialize.ts @@ -18,7 +18,8 @@ async function main() { const program = anchor.workspace.TokenMigrator as Program; - // Derive vault PDA + // Derive vault PDA. The running wallet (`payer`) becomes the vault admin — + // initialization is permissionless, so no special admin key is required. const [vaultPda] = PublicKey.findProgramAddressSync( [ Buffer.from("vault"), diff --git a/scripts/migrate.ts b/scripts/migrate.ts index ed9efd3..25528c0 100644 --- a/scripts/migrate.ts +++ b/scripts/migrate.ts @@ -9,7 +9,6 @@ import { getAccount } from "@solana/spl-token"; import BN from "bn.js"; -import { ADMIN_PUBLIC_KEY } from "./consts"; const PROGRAM_ID = new PublicKey("gr8tqq2ripsM6N46gLWpSDXtdrH6J9jaXoyya1ELC9t"); const MINT_FROM = new PublicKey("METADDFL6wWMWEoKTFJwcThTbUmtarRJZjRpzUvkxhr"); // This is inbound from the user @@ -19,13 +18,18 @@ const AMOUNT = new BN(10_000_000); // raw amount const provider = anchor.AnchorProvider.env(); const payer = provider.wallet["payer"]; +// Admin/creator of the vault to migrate into. Defaults to your own wallet so it +// matches `initialize.ts` (which records the running wallet as the vault admin). +// Set this to another creator's pubkey to migrate into a vault they created. +const VAULT_ADMIN = payer.publicKey; + async function main() { const program = anchor.workspace.TokenMigrator as Program; const [vault] = PublicKey.findProgramAddressSync( [ Buffer.from("vault"), - ADMIN_PUBLIC_KEY.toBuffer(), + VAULT_ADMIN.toBuffer(), MINT_FROM.toBuffer(), MINT_TO.toBuffer() ], diff --git a/sdk/package.json b/sdk/package.json index c37adeb..ecd041e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@metadaoproject/token-migrator", - "version": "0.1.0-alpha.5", + "version": "0.1.0-alpha.6", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/sdk/src/v0.1/TokenMigratorClient.ts b/sdk/src/v0.1/TokenMigratorClient.ts index 67bd917..5bf967d 100644 --- a/sdk/src/v0.1/TokenMigratorClient.ts +++ b/sdk/src/v0.1/TokenMigratorClient.ts @@ -12,7 +12,6 @@ import { } from "@solana/web3.js"; import type { TokenMigrator } from "./types/token_migrator.js"; import TokenMigratorIDL from "./idl/token_migrator.json" with { type: "json" }; -import { TOKEN_MIGRATOR_ADMIN } from "./constants.js"; import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, @@ -117,21 +116,37 @@ export class TokenMigratorClient { } /** - * Initialize a new token migration vault (admin only) + * Initialize a new token migration vault. Permissionless: anyone can create a + * vault and becomes its `admin`. The vault PDA is seeded by `admin`, so the + * same `(mintFrom, mintTo)` pair can have one vault per admin. + * + * The program sets `payer = admin` for the vault account, so `admin` signs the + * transaction and pays its rent; the separate `payer` only funds the + * pre-instruction ATAs. + * + * NOTE: `initialize` requires `vaultToAta` to already hold a balance (the + * program enforces `amount > 0`); the idempotent ATA pre-instructions here do + * NOT fund it. Make sure it is funded before the `initialize` step runs — + * either in a prior transaction (the idempotent creates then no-op), or by + * appending a mint/transfer into `vaultToAta` to this builder's + * `.preInstructions()`, which all execute before `initialize`. + * * @param mintFrom - Source mint * @param mintTo - Destination mint * @param strategy - { proRata: {} } or { fixed: { e } } - * @param payer - Defaults to this.wallet.publicKey + * @param admin - Vault admin/signer; defaults to this.wallet.publicKey + * @param payer - Funds the vault ATA accounts creation; defaults to `admin` */ initializeIx( mintFrom: PublicKey, mintTo: PublicKey, strategy: Strategy, - payer: PublicKey = this.wallet.publicKey, + admin: PublicKey = this.wallet.publicKey, + payer: PublicKey = admin, ) { const [vault] = getVaultAddr( this.tokenMigrator.programId, - TOKEN_MIGRATOR_ADMIN, + admin, mintFrom, mintTo, ); @@ -146,7 +161,7 @@ export class TokenMigratorClient { return this.tokenMigrator.methods .initialize(mintFrom, mintTo, strategy) .accounts({ - admin: TOKEN_MIGRATOR_ADMIN, + admin, }) .preInstructions([ createAssociatedTokenAccountIdempotentInstruction( @@ -165,41 +180,65 @@ export class TokenMigratorClient { } /** - * Build migrate instruction(s) + * Build migrate instruction(s). + * + * The vault must be passed explicitly: on-chain its PDA is seeded by its own + * stored `admin` (`vault.admin`), which Anchor cannot auto-resolve. A UI + * usually already has the vault (e.g. from `getAllVaults`); if you only have + * the admin, derive it first with + * `getVaultAddr(programId, admin, mintFrom, mintTo)`. + * * @param mintFrom - Source mint * @param mintTo - Destination mint * @param amount - Amount to migrate + * @param vault - The vault to migrate into * @param user - Defaults to this.wallet.publicKey - * @param payer - Defaults to this.wallet.publicKey + * @param payer - Funds the user's destination ATA account creation; defaults to `user` */ migrateIx( mintFrom: PublicKey, mintTo: PublicKey, amount: BN, + vault: PublicKey, user: PublicKey = this.wallet.publicKey, - payer: PublicKey = this.wallet.publicKey, + payer: PublicKey = user, ) { + const [vaultFromAta] = getVaultFromAtaAddr( + vault, + mintFrom, + TOKEN_PROGRAM_ID, + ); + const [vaultToAta] = getVaultToAtaAddr(vault, mintTo, TOKEN_PROGRAM_ID); const userFromTa = getAssociatedTokenAddressSync(mintFrom, user, true); const userToTa = getAssociatedTokenAddressSync(mintTo, user, true); - return this.tokenMigrator.methods - .migrate(amount) - .accounts({ - user, - mintFrom, - mintTo, - userFromTa, - userToTa, - program: this.tokenMigrator.programId, - }) - .preInstructions([ - createAssociatedTokenAccountIdempotentInstruction( - payer, - userToTa, + return ( + this.tokenMigrator.methods + .migrate(amount) + // accountsPartial (not accounts): we pass the PDA accounts explicitly + // because `vault`'s seeds reference its own `vault.admin`, which Anchor + // cannot auto-resolve. The remaining accounts (eventAuthority, + // tokenProgram) still resolve automatically. + .accountsPartial({ user, + mintFrom, mintTo, - ), - ]); + userFromTa, + userToTa, + vault, + vaultFromAta, + vaultToAta, + program: this.tokenMigrator.programId, + }) + .preInstructions([ + createAssociatedTokenAccountIdempotentInstruction( + payer, + userToTa, + user, + mintTo, + ), + ]) + ); } /** @@ -229,23 +268,29 @@ export class TokenMigratorClient { throw new Error("Unknown strategy type"); } - async getAllVaults(): Promise< - Array<{ publicKey: PublicKey; account: Vault }> - > { - return await this.tokenMigrator.account.vault.all([ - { - memcmp: { - offset: 8, // discriminator - bytes: TOKEN_MIGRATOR_ADMIN.toBase58(), - }, - }, - ]); + /** + * Fetch all vaults. Pass `admin` to return only that admin's vaults; omit it + * to enumerate every vault (the program is permissionless, so vaults exist + * across many admins). + */ + async getAllVaults( + admin?: PublicKey, + ): Promise> { + // `admin` is the first field after the 1-byte account discriminator + // (`#[account(discriminator = [1])]`), so it lives at offset 1. + return await this.tokenMigrator.account.vault.all( + admin ? [{ memcmp: { offset: 1, bytes: admin.toBase58() } }] : [], + ); } - async vaultExists(mintFrom: PublicKey, mintTo: PublicKey): Promise { + async vaultExists( + mintFrom: PublicKey, + mintTo: PublicKey, + admin: PublicKey = this.wallet.publicKey, + ): Promise { const [vault] = getVaultAddr( this.tokenMigrator.programId, - TOKEN_MIGRATOR_ADMIN, + admin, mintFrom, mintTo, ); diff --git a/sdk/src/v0.1/constants.ts b/sdk/src/v0.1/constants.ts index e6a14fe..1fa6365 100644 --- a/sdk/src/v0.1/constants.ts +++ b/sdk/src/v0.1/constants.ts @@ -4,6 +4,13 @@ export const TOKEN_MIGRATOR_PROGRAM_ID = new PublicKey( "gr8tqq2ripsM6N46gLWpSDXtdrH6J9jaXoyya1ELC9t", ); +/** + * @deprecated Initialization is now permissionless — any signer can create a + * vault and becomes its own admin, so there is no single "token migrator admin". + * This is the key from the program's original admin-gated era, kept for + * backwards compatibility and as a reference to those original vaults (e.g. + * `client.getAllVaults(TOKEN_MIGRATOR_ADMIN)`). The SDK does not use it anywhere. + */ export const TOKEN_MIGRATOR_ADMIN = new PublicKey( "ELT1uRmtFvYP6WSrc4mCZaW7VVbcdkcKAj39aHSVCmwH", ); diff --git a/sdk/src/v0.1/idl/token_migrator.json b/sdk/src/v0.1/idl/token_migrator.json index 192726b..83d0d92 100644 --- a/sdk/src/v0.1/idl/token_migrator.json +++ b/sdk/src/v0.1/idl/token_migrator.json @@ -564,6 +564,13 @@ ] } ], + "errors": [ + { + "code": 6000, + "name": "ExponentOutOfRange", + "msg": "Fixed strategy exponent out of range (|e| must be <= 19)" + } + ], "types": [ { "name": "MigrateEvent", diff --git a/sdk/src/v0.1/types/token_migrator.ts b/sdk/src/v0.1/types/token_migrator.ts index 97675cc..1e77748 100644 --- a/sdk/src/v0.1/types/token_migrator.ts +++ b/sdk/src/v0.1/types/token_migrator.ts @@ -550,6 +550,13 @@ export type TokenMigrator = { discriminator: [0]; }, ]; + errors: [ + { + code: 6000; + name: "exponentOutOfRange"; + msg: "Fixed strategy exponent out of range (|e| must be <= 19)"; + }, + ]; types: [ { name: "migrateEvent";