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
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
},
"dependencies": {
"@coral-xyz/anchor": "0.31.1",
"@solana/spl-token": "^0.4.13",
"@solana/web3.js": "^1.98.4"
"@coral-xyz/anchor": "=0.31.1",
"@solana/spl-token": "=0.4.13",
"@solana/web3.js": "=1.98.4"
},
"devDependencies": {
"@types/bn.js": "5.2.0",
Expand Down
2 changes: 2 additions & 0 deletions sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
tsconfig.tsbuildinfo
32 changes: 32 additions & 0 deletions sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@metadaoproject/token-migrator",
"version": "0.1.0-alpha.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"license": "BSL-1.0",
"files": [
"/dist"
],
"exports": {
".": "./dist/index.js",
"./v0.1": "./dist/v0.1/index.js"
},
"scripts": {
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check",
"build": "rm -rf ./dist && mkdir -p ./src/v0.1/idl ./src/v0.1/types && cp ../target/types/* ./src/v0.1/types || true && cp ../target/idl/token_migrator.json ./src/v0.1/idl/ || true && yarn lint:fix && yarn tsc"
},
"dependencies": {
"@coral-xyz/anchor": "=0.31.1",
"@solana/web3.js": "=1.98.4",
"bn.js": "^5.2.1"
},
"devDependencies": {
"@types/chai": "^5.2.2",
"@types/mocha": "^10.0.10",
"prettier": "^3.3.3",
"typescript": "^5.5.5"
}
}
1 change: 1 addition & 0 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { sha256 } from "@noble/hashes/sha256";
233 changes: 233 additions & 0 deletions sdk/src/v0.1/TokenMigratorClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { AnchorProvider, Program } from "@coral-xyz/anchor";
import { PublicKey, AccountInfo } from "@solana/web3.js";
// Import TypeScript types from the generated types file
import type { TokenMigrator } from "./types/token_migrator.js";
// Import the JSON IDL with the required type assertion for ESM
import TokenMigratorIDL from "./idl/token_migrator.json" with { type: "json" };
import { TOKEN_MIGRATOR_ADMIN } from "./constants.js";
import {
createAssociatedTokenAccountIdempotentInstruction,
getAssociatedTokenAddressSync,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import BN from "bn.js";
import { Vault, Strategy } from "./types/index.js";
import {
getVaultAddr,
getVaultFromAtaAddr,
getVaultToAtaAddr,
} from "./utils/pda.js";

export type CreateTokenMigratorClientParams = {
provider: AnchorProvider;
tokenMigratorProgramId?: PublicKey;
};

export class TokenMigratorClient {
public tokenMigrator: Program<TokenMigrator>;
public provider: AnchorProvider;

private constructor(params: CreateTokenMigratorClientParams) {
this.provider = params.provider;
this.tokenMigrator = new Program(TokenMigratorIDL as any, this.provider);
}

static createClient(
params: CreateTokenMigratorClientParams,
): TokenMigratorClient {
return new TokenMigratorClient(params);
}

getProgramId(): PublicKey {
return this.tokenMigrator.programId;
}

async getVault(vault: PublicKey): Promise<Vault> {
return await this.tokenMigrator.account.vault.fetch(vault);
}

async fetchVault(vault: PublicKey): Promise<Vault | null> {
return await this.tokenMigrator.account.vault.fetchNullable(vault);
}

deserializeVault(accountInfo: AccountInfo<Buffer>): Vault {
return this.tokenMigrator.coder.accounts.decode("vault", accountInfo.data);
}

deserializeMigrateEvent(data: Buffer): any {
// Returns the raw decoded event - cast to MigrateEvent type as needed
return this.tokenMigrator.coder.events.decode(data.toString("hex"));
}

/**
* Creates a ProRata strategy object
*/
createProRataStrategy(): Strategy {
return { proRata: {} };
}

/**
* Creates a Fixed strategy object with specified exponent
* @param e - The exponent for 10^e scaling
*/
createFixedStrategy(e: number): Strategy {
return { fixed: { e } };
}

/**
* Initialize a new token migration vault (admin only)
* @param mintFrom - The mint we are migrating from
* @param mintTo - The mint we are migrating to
* @param strategy - The migration strategy (ProRata or Fixed)
* @param payer - The payer for account creation (defaults to provider.publicKey)
*/
initializeIx(
mintFrom: PublicKey,
mintTo: PublicKey,
strategy: Strategy,
payer: PublicKey = this.provider.publicKey,
) {
const [vault] = getVaultAddr(
this.tokenMigrator.programId,
TOKEN_MIGRATOR_ADMIN,
mintFrom,
mintTo,
);

const [vaultFromAta] = getVaultFromAtaAddr(
vault,
mintFrom,
TOKEN_PROGRAM_ID,
);

const [vaultToAta] = getVaultToAtaAddr(vault, mintTo, TOKEN_PROGRAM_ID);

return this.tokenMigrator.methods
.initialize(mintFrom, mintTo, strategy)
.accounts({
admin: TOKEN_MIGRATOR_ADMIN,
})
.preInstructions([
// Create vault's token accounts if they don't exist
createAssociatedTokenAccountIdempotentInstruction(
payer,
vaultFromAta,
vault,
mintFrom,
),
createAssociatedTokenAccountIdempotentInstruction(
payer,
vaultToAta,
vault,
mintTo,
),
]);
}

/**
* Migrate tokens from old mint to new mint
* @param mintFrom - The mint we are migrating from
* @param mintTo - The mint we are migrating to
* @param amount - The amount of tokens to migrate
* @param user - The user performing the migration (defaults to provider.publicKey)
* @param payer - The payer for account creation (defaults to provider.publicKey)
*/
migrateIx(
mintFrom: PublicKey,
mintTo: PublicKey,
amount: BN,
user: PublicKey = this.provider.publicKey,
payer: PublicKey = this.provider.publicKey,
) {
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([
// Create user's destination token account if it doesn't exist
createAssociatedTokenAccountIdempotentInstruction(
payer,
userToTa,
user,
mintTo,
),
]);
}

/**
* Helper method to calculate expected output amount for a migration
* @param vault - The vault account data
* @param amount - The input amount
* @param mintFromSupply - Total supply of the from mint (for ProRata)
* @param mintToSupply - Total supply of the to mint (for ProRata)
* @param mintFromDecimals - Decimals of the from mint (for Fixed)
* @param mintToDecimals - Decimals of the to mint (for Fixed)
*/
calculateMigrationAmount(
vault: Vault,
amount: BN,
mintFromSupply?: BN,
mintToSupply?: BN,
): BN {
if ("proRata" in vault.strategy) {
if (!mintFromSupply || !mintToSupply) {
throw new Error("Mint supplies required for ProRata strategy");
}
// ProRata: withdraw_amount = (amount * mintToSupply) / mintFromSupply
return amount.mul(mintToSupply).div(mintFromSupply);
} else if ("fixed" in vault.strategy) {
const e = vault.strategy.fixed.e;
// Fixed: withdraw_amount = amount * 10^e
if (e >= 0) {
return amount.mul(new BN(10).pow(new BN(e)));
} else {
return amount.div(new BN(10).pow(new BN(-e)));
}
}
throw new Error("Unknown strategy type");
}

/**
* Fetch all vaults initialized by the admin
*/
async getAllVaults(): Promise<
Array<{
publicKey: PublicKey;
account: Vault;
}>
> {
const vaults = await this.tokenMigrator.account.vault.all([
{
memcmp: {
offset: 8, // After discriminator
bytes: TOKEN_MIGRATOR_ADMIN.toBase58(),
},
},
]);
return vaults;
}

/**
* Check if a vault exists for a given migration pair
*/
async vaultExists(mintFrom: PublicKey, mintTo: PublicKey): Promise<boolean> {
const [vault] = getVaultAddr(
this.tokenMigrator.programId,
TOKEN_MIGRATOR_ADMIN,
mintFrom,
mintTo,
);
const vaultAccount = await this.fetchVault(vault);
return vaultAccount !== null;
}
}
9 changes: 9 additions & 0 deletions sdk/src/v0.1/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PublicKey } from "@solana/web3.js";

export const TOKEN_MIGRATOR_PROGRAM_ID = new PublicKey(
"gr8tqq2ripsM6N46gLWpSDXtdrH6J9jaXoyya1ELC9t",
);

export const TOKEN_MIGRATOR_ADMIN = new PublicKey(
"ELT1uRmtFvYP6WSrc4mCZaW7VVbcdkcKAj39aHSVCmwH",
);
Loading