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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions scripts/consts.ts

This file was deleted.

3 changes: 2 additions & 1 deletion scripts/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ async function main() {

const program = anchor.workspace.TokenMigrator as Program<TokenMigrator>;

// 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"),
Expand Down
8 changes: 6 additions & 2 deletions scripts/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<TokenMigrator>;

const [vault] = PublicKey.findProgramAddressSync(
[
Buffer.from("vault"),
ADMIN_PUBLIC_KEY.toBuffer(),
VAULT_ADMIN.toBuffer(),
MINT_FROM.toBuffer(),
MINT_TO.toBuffer()
],
Expand Down
2 changes: 1 addition & 1 deletion sdk/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
121 changes: 83 additions & 38 deletions sdk/src/v0.1/TokenMigratorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
);
Expand All @@ -146,7 +161,7 @@ export class TokenMigratorClient {
return this.tokenMigrator.methods
.initialize(mintFrom, mintTo, strategy)
.accounts({
admin: TOKEN_MIGRATOR_ADMIN,
admin,
})
.preInstructions([
createAssociatedTokenAccountIdempotentInstruction(
Expand All @@ -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,
),
])
);
}

/**
Expand Down Expand Up @@ -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<Array<{ publicKey: PublicKey; account: Vault }>> {
// `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<boolean> {
async vaultExists(
mintFrom: PublicKey,
mintTo: PublicKey,
admin: PublicKey = this.wallet.publicKey,
): Promise<boolean> {
const [vault] = getVaultAddr(
this.tokenMigrator.programId,
TOKEN_MIGRATOR_ADMIN,
admin,
mintFrom,
mintTo,
);
Expand Down
7 changes: 7 additions & 0 deletions sdk/src/v0.1/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
7 changes: 7 additions & 0 deletions sdk/src/v0.1/idl/token_migrator.json
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,13 @@
]
}
],
"errors": [
{
"code": 6000,
"name": "ExponentOutOfRange",
"msg": "Fixed strategy exponent out of range (|e| must be <= 19)"
}
],
"types": [
{
"name": "MigrateEvent",
Expand Down
7 changes: 7 additions & 0 deletions sdk/src/v0.1/types/token_migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down