diff --git a/devnet/solana-devnet.yaml b/devnet/solana-devnet.yaml index 75543f68..4ae46167 100644 --- a/devnet/solana-devnet.yaml +++ b/devnet/solana-devnet.yaml @@ -59,6 +59,9 @@ spec: - --bpf-program - P2WH424242424242424242424242424242424242424 - /opt/solana/deps/pyth2wormhole.so + - --bpf-program + - Ex9bCdVMSfx7EzB3pgSi2R4UHwJAXvTw18rBQm5YQ8gK + - /opt/solana/deps/wormhole_migration.so - --log ports: - containerPort: 8001 diff --git a/docs/devnet.md b/docs/devnet.md index a14dff02..ed4eb715 100644 --- a/docs/devnet.md +++ b/docs/devnet.md @@ -14,6 +14,7 @@ | Bridge Core | SOL | Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o | | | Token Bridge | SOL | B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE | | | NFT Bridge | SOL | NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA | | +| Migration Contract | SOL | Ex9bCdVMSfx7EzB3pgSi2R4UHwJAXvTw18rBQm5YQ8gK | | | Test Wallet | Terra | terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v | Mnemonic: `notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius` | | Example Token | Terra | terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh | Tokens minted to Test Wallet | | Bridge Core | Terra | terra18eezxhys9jwku67cm4w84xhnzt4xjj77w2qt62 | | diff --git a/solana/Dockerfile b/solana/Dockerfile index f6fae882..5fd9ffd9 100644 --- a/solana/Dockerfile +++ b/solana/Dockerfile @@ -48,13 +48,16 @@ RUN --mount=type=cache,target=bridge/target \ --mount=type=cache,target=modules/token_bridge/target \ --mount=type=cache,target=modules/nft_bridge/target \ --mount=type=cache,target=pyth2wormhole/target \ + --mount=type=cache,target=migration/target \ cargo build-bpf --manifest-path "bridge/program/Cargo.toml" && \ cargo build-bpf --manifest-path "bridge/cpi_poster/Cargo.toml" && \ cargo build-bpf --manifest-path "modules/token_bridge/program/Cargo.toml" && \ cargo build-bpf --manifest-path "pyth2wormhole/program/Cargo.toml" && \ cargo build-bpf --manifest-path "modules/nft_bridge/program/Cargo.toml" && \ + cargo build-bpf --manifest-path "migration/Cargo.toml" && \ cp bridge/target/deploy/bridge.so /opt/solana/deps/bridge.so && \ cp bridge/target/deploy/cpi_poster.so /opt/solana/deps/cpi_poster.so && \ + cp migration/target/deploy/wormhole_migration.so /opt/solana/deps/wormhole_migration.so && \ cp modules/token_bridge/target/deploy/token_bridge.so /opt/solana/deps/token_bridge.so && \ cp modules/nft_bridge/target/deploy/nft_bridge.so /opt/solana/deps/nft_bridge.so && \ cp modules/token_bridge/token-metadata/spl_token_metadata.so /opt/solana/deps/spl_token_metadata.so && \ diff --git a/solana/Dockerfile.wasm b/solana/Dockerfile.wasm index a28abdd0..fc37a507 100644 --- a/solana/Dockerfile.wasm +++ b/solana/Dockerfile.wasm @@ -16,6 +16,7 @@ ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" COPY bridge bridge COPY modules modules COPY solitaire solitaire +COPY migration migration # Compile Wormhole RUN --mount=type=cache,target=/root/.cache \ @@ -35,6 +36,11 @@ RUN --mount=type=cache,target=/root/.cache \ --mount=type=cache,target=modules/token_bridge/target \ cd modules/token_bridge/program && /usr/local/cargo/bin/wasm-pack build --target nodejs -d nodejs -- --features wasm +# Compile Migration +RUN --mount=type=cache,target=/root/.cache \ + --mount=type=cache,target=migration/target \ + cd migration && /usr/local/cargo/bin/wasm-pack build --target bundler -d bundler -- --features wasm + # Compile NFT Bridge RUN --mount=type=cache,target=/root/.cache \ --mount=type=cache,target=modules/nft_bridge/target \ @@ -48,6 +54,7 @@ FROM scratch AS export COPY --from=build /usr/src/bridge/bridge/program/bundler sdk/js/src/solana/core COPY --from=build /usr/src/bridge/modules/token_bridge/program/bundler sdk/js/src/solana/token +COPY --from=build /usr/src/bridge/migration/bundler sdk/js/src/solana/migration COPY --from=build /usr/src/bridge/modules/nft_bridge/program/bundler sdk/js/src/solana/nft COPY --from=build /usr/src/bridge/bridge/program/nodejs clients/solana/pkg diff --git a/solana/migration/Cargo.toml b/solana/migration/Cargo.toml new file mode 100644 index 00000000..fd8e6dc2 --- /dev/null +++ b/solana/migration/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "wormhole-migration" +version = "0.1.0" +description = "Created with Rocksalt" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "wormhole_migration" + +[features] +no-entrypoint = ["solitaire/no-entrypoint", "rand"] +trace = ["solitaire/trace"] +wasm = ["no-entrypoint"] +client = ["solitaire-client", "solitaire/client", "no-entrypoint"] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +borsh = "0.8.1" +byteorder = "1.4.3" +rocksalt = { path = "../solitaire/rocksalt" } +solitaire = { path = "../solitaire/program" } +sha3 = "0.9.1" +solana-program = "*" +spl-token = { version = "=3.1.0", features = ["no-entrypoint"] } +solitaire-client = { path = "../solitaire/client", optional = true } +wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"] } +serde = { version = "1.0", features = ["derive"] } +rand = { version = "0.7.3", optional = true } + +[dev-dependencies] +hex = "*" +hex-literal = "0.3.1" +libsecp256k1 = { version = "0.3.5", features = [] } +solana-client = "1.7.0" +solana-sdk = "=1.7.0" +spl-token = { version = "=3.1.0", features = ["no-entrypoint"] } + +[patch.crates-io] +memmap2 = { path = "../bridge/memmap2-rs" } \ No newline at end of file diff --git a/solana/migration/rustfmt.toml b/solana/migration/rustfmt.toml new file mode 100644 index 00000000..3b9ff103 --- /dev/null +++ b/solana/migration/rustfmt.toml @@ -0,0 +1,11 @@ +# Merge similar crates together to avoid multiple use statements. +imports_granularity = "Crate" + +# Consistency in formatting makes tool based searching/editing better. +empty_item_single_line = false + +# Easier editing when arbitrary mixed use statements do not collapse. +imports_layout = "Vertical" + +# Default rustfmt formatting of match arms with branches is awful. +match_arm_leading_pipes = "Preserve" diff --git a/solana/migration/src/accounts.rs b/solana/migration/src/accounts.rs new file mode 100644 index 00000000..f6efe363 --- /dev/null +++ b/solana/migration/src/accounts.rs @@ -0,0 +1,84 @@ +use crate::types::{ + PoolData, + SplAccount, + SplMint, +}; +use solana_program::pubkey::Pubkey; +use solitaire::{ + processors::seeded::Seeded, + AccountState, + Data, + Derive, + Info, +}; + +pub type ShareMint<'a, const STATE: AccountState> = Data<'a, SplMint, { STATE }>; + +pub struct ShareMintDerivationData { + pub pool: Pubkey, +} + +impl<'b, const STATE: AccountState> Seeded<&ShareMintDerivationData> for ShareMint<'b, { STATE }> { + fn seeds(accs: &ShareMintDerivationData) -> Vec> { + vec![ + String::from("share_mint").as_bytes().to_vec(), + accs.pool.to_bytes().to_vec(), + ] + } +} + +pub type FromCustodyTokenAccount<'a, const STATE: AccountState> = Data<'a, SplAccount, { STATE }>; + +pub struct FromCustodyTokenAccountDerivationData { + pub pool: Pubkey, +} + +impl<'b, const STATE: AccountState> Seeded<&FromCustodyTokenAccountDerivationData> + for FromCustodyTokenAccount<'b, { STATE }> +{ + fn seeds(accs: &FromCustodyTokenAccountDerivationData) -> Vec> { + vec![ + String::from("from_custody").as_bytes().to_vec(), + accs.pool.to_bytes().to_vec(), + ] + } +} + +pub type ToCustodyTokenAccount<'a, const STATE: AccountState> = Data<'a, SplAccount, { STATE }>; + +pub struct ToCustodyTokenAccountDerivationData { + pub pool: Pubkey, +} + +impl<'b, const STATE: AccountState> Seeded<&ToCustodyTokenAccountDerivationData> + for ToCustodyTokenAccount<'b, { STATE }> +{ + fn seeds(accs: &ToCustodyTokenAccountDerivationData) -> Vec> { + vec![ + String::from("to_custody").as_bytes().to_vec(), + accs.pool.to_bytes().to_vec(), + ] + } +} + +pub type MigrationPool<'a, const STATE: AccountState> = Data<'a, PoolData, { STATE }>; + +pub struct MigrationPoolDerivationData { + pub from: Pubkey, + pub to: Pubkey, +} + +impl<'b, const STATE: AccountState> Seeded<&MigrationPoolDerivationData> + for MigrationPool<'b, { STATE }> +{ + fn seeds(accs: &MigrationPoolDerivationData) -> Vec> { + vec![ + String::from("pool").as_bytes().to_vec(), + accs.from.to_bytes().to_vec(), + accs.to.to_bytes().to_vec(), + ] + } +} + +pub type CustodySigner<'a> = Derive, "custody_signer">; +pub type AuthoritySigner<'a> = Derive, "authority_signer">; diff --git a/solana/migration/src/api.rs b/solana/migration/src/api.rs new file mode 100644 index 00000000..bb305adf --- /dev/null +++ b/solana/migration/src/api.rs @@ -0,0 +1,4 @@ +pub mod add_liquidity; +pub mod claim_shares; +pub mod create_pool; +pub mod migrate_tokens; diff --git a/solana/migration/src/api/add_liquidity.rs b/solana/migration/src/api/add_liquidity.rs new file mode 100644 index 00000000..0790a163 --- /dev/null +++ b/solana/migration/src/api/add_liquidity.rs @@ -0,0 +1,112 @@ +use crate::{ + accounts::{ + AuthoritySigner, + CustodySigner, + MigrationPool, + ShareMint, + ShareMintDerivationData, + ToCustodyTokenAccount, + ToCustodyTokenAccountDerivationData, + }, + types::{ + SplAccount, + SplMint, + }, + MigrationError::WrongMint, +}; +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; + +use crate::accounts::MigrationPoolDerivationData; +use solitaire::*; + +#[derive(FromAccounts)] +pub struct AddLiquidity<'b> { + pub pool: Mut>, + pub from_mint: Data<'b, SplMint, { AccountState::Initialized }>, + pub to_mint: Data<'b, SplMint, { AccountState::Initialized }>, + pub to_token_custody: Mut>, + pub share_mint: Mut>, + + pub to_lp_acc: Mut>, + pub lp_share_acc: Mut>, + pub custody_signer: CustodySigner<'b>, + pub authority_signer: AuthoritySigner<'b>, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct AddLiquidityData { + pub amount: u64, +} + +pub fn add_liquidity( + ctx: &ExecutionContext, + accs: &mut AddLiquidity, + data: AddLiquidityData, +) -> Result<()> { + if *accs.from_mint.info().key != accs.pool.from { + return Err(WrongMint.into()); + } + if *accs.to_mint.info().key != accs.pool.to { + return Err(WrongMint.into()); + } + if accs.lp_share_acc.mint != *accs.share_mint.info().key { + return Err(WrongMint.into()); + } + accs.to_token_custody.verify_derivation( + ctx.program_id, + &ToCustodyTokenAccountDerivationData { + pool: *accs.pool.info().key, + }, + )?; + accs.share_mint.verify_derivation( + ctx.program_id, + &ShareMintDerivationData { + pool: *accs.pool.info().key, + }, + )?; + accs.pool.verify_derivation( + ctx.program_id, + &MigrationPoolDerivationData { + from: accs.pool.from, + to: accs.pool.to, + }, + )?; + + // Transfer out-tokens in + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + accs.to_lp_acc.info().key, + accs.to_token_custody.info().key, + accs.authority_signer.key, + &[], + data.amount, + )?; + invoke_seeded(&transfer_ix, ctx, &accs.authority_signer, None)?; + + // The share amount should be equal to the amount of from tokens an lp would be getting + let share_amount = if accs.from_mint.decimals > accs.to_mint.decimals { + data.amount + .checked_mul(10u64.pow((accs.from_mint.decimals - accs.to_mint.decimals) as u32)) + .unwrap() + } else { + data.amount + .checked_div(10u64.pow((accs.to_mint.decimals - accs.from_mint.decimals) as u32)) + .unwrap() + }; + + // Mint LP shares + let mint_ix = spl_token::instruction::mint_to( + &spl_token::id(), + accs.from_mint.info().key, + accs.lp_share_acc.info().key, + accs.custody_signer.key, + &[], + share_amount, + )?; + invoke_seeded(&mint_ix, ctx, &accs.custody_signer, None)?; + + Ok(()) +} diff --git a/solana/migration/src/api/claim_shares.rs b/solana/migration/src/api/claim_shares.rs new file mode 100644 index 00000000..1ba6d93e --- /dev/null +++ b/solana/migration/src/api/claim_shares.rs @@ -0,0 +1,103 @@ +use crate::{ + accounts::{ + AuthoritySigner, + CustodySigner, + FromCustodyTokenAccountDerivationData, + MigrationPool, + ShareMint, + ShareMintDerivationData, + ToCustodyTokenAccount, + }, + types::{ + SplAccount, + SplMint, + }, + MigrationError::WrongMint, +}; +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; + +use crate::accounts::MigrationPoolDerivationData; +use solitaire::{ + processors::seeded::{ + invoke_seeded, + Seeded, + }, + *, +}; + +#[derive(FromAccounts)] +pub struct ClaimShares<'b> { + pub pool: Mut>, + pub to_mint: Data<'b, SplMint, { AccountState::Initialized }>, + pub from_token_custody: Mut>, + pub share_mint: Mut>, + + pub from_lp_acc: Mut>, + pub lp_share_acc: Mut>, + pub custody_signer: CustodySigner<'b>, + pub authority_signer: AuthoritySigner<'b>, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct ClaimSharesData { + pub amount: u64, +} + +pub fn claim_shares( + ctx: &ExecutionContext, + accs: &mut ClaimShares, + data: ClaimSharesData, +) -> Result<()> { + if *accs.to_mint.info().key != accs.pool.to { + return Err(WrongMint.into()); + } + if accs.lp_share_acc.mint != *accs.share_mint.info().key { + return Err(WrongMint.into()); + } + accs.from_token_custody.verify_derivation( + ctx.program_id, + &FromCustodyTokenAccountDerivationData { + pool: *accs.pool.info().key, + }, + )?; + accs.share_mint.verify_derivation( + ctx.program_id, + &ShareMintDerivationData { + pool: *accs.pool.info().key, + }, + )?; + accs.pool.verify_derivation( + ctx.program_id, + &MigrationPoolDerivationData { + from: accs.pool.from, + to: accs.pool.to, + }, + )?; + + // Transfer claimed tokens to LP + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + accs.from_token_custody.info().key, + accs.from_lp_acc.info().key, + accs.custody_signer.key, + &[], + data.amount, + )?; + invoke_seeded(&transfer_ix, ctx, &accs.custody_signer, None)?; + + // Burn LP shares + let mint_ix = spl_token::instruction::burn( + &spl_token::id(), + accs.lp_share_acc.info().key, + accs.share_mint.info().key, + accs.authority_signer.key, + &[], + data.amount, + )?; + invoke_seeded(&mint_ix, ctx, &accs.authority_signer, None)?; + + Ok(()) +} diff --git a/solana/migration/src/api/create_pool.rs b/solana/migration/src/api/create_pool.rs new file mode 100644 index 00000000..47a1066f --- /dev/null +++ b/solana/migration/src/api/create_pool.rs @@ -0,0 +1,119 @@ +use crate::{ + accounts::{ + CustodySigner, + FromCustodyTokenAccount, + FromCustodyTokenAccountDerivationData, + MigrationPool, + MigrationPoolDerivationData, + ShareMint, + ShareMintDerivationData, + ToCustodyTokenAccount, + ToCustodyTokenAccountDerivationData, + }, + types::SplMint, +}; +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; +use solana_program::program::invoke_signed; +use solitaire::{ + processors::seeded::Seeded, + CreationLamports::Exempt, + *, +}; + +#[derive(FromAccounts)] +pub struct CreatePool<'b> { + pub payer: Mut>>, + + pub pool: Mut>, + pub from_mint: Data<'b, SplMint, { AccountState::Initialized }>, + pub to_mint: Data<'b, SplMint, { AccountState::Initialized }>, + pub from_token_custody: Mut>, + pub to_token_custody: Mut>, + pub pool_mint: Mut>, + + pub custody_signer: CustodySigner<'b>, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct CreatePoolData {} + +pub fn create_pool( + ctx: &ExecutionContext, + accs: &mut CreatePool, + _data: CreatePoolData, +) -> Result<()> { + // Create from custody account + accs.from_token_custody.create( + &FromCustodyTokenAccountDerivationData { + pool: *accs.pool.info().key, + }, + ctx, + accs.payer.key, + Exempt, + )?; + + let init_ix = spl_token::instruction::initialize_account( + &spl_token::id(), + accs.from_token_custody.info().key, + accs.from_mint.info().key, + accs.custody_signer.info().key, + )?; + invoke_signed(&init_ix, ctx.accounts, &[])?; + + // Create to custody account + accs.to_token_custody.create( + &ToCustodyTokenAccountDerivationData { + pool: *accs.pool.info().key, + }, + ctx, + accs.payer.key, + Exempt, + )?; + + let init_ix = spl_token::instruction::initialize_account( + &spl_token::id(), + accs.to_token_custody.info().key, + accs.to_mint.info().key, + accs.custody_signer.info().key, + )?; + invoke_signed(&init_ix, ctx.accounts, &[])?; + + // Create to pool mint + accs.pool_mint.create( + &ShareMintDerivationData { + pool: *accs.pool.info().key, + }, + ctx, + accs.payer.key, + Exempt, + )?; + + let init_ix = spl_token::instruction::initialize_mint( + &spl_token::id(), + accs.pool_mint.info().key, + accs.custody_signer.info().key, + None, + accs.from_mint.decimals, + )?; + invoke_signed(&init_ix, ctx.accounts, &[])?; + + // Set fields on pool + accs.pool.from = *accs.from_mint.info().key; + accs.pool.to = *accs.to_mint.info().key; + + // Create pool + accs.pool.create( + &MigrationPoolDerivationData { + from: *accs.from_mint.info().key, + to: *accs.to_mint.info().key, + }, + ctx, + accs.payer.key, + Exempt, + )?; + + Ok(()) +} diff --git a/solana/migration/src/api/migrate_tokens.rs b/solana/migration/src/api/migrate_tokens.rs new file mode 100644 index 00000000..942cdddc --- /dev/null +++ b/solana/migration/src/api/migrate_tokens.rs @@ -0,0 +1,121 @@ +use crate::{ + accounts::{ + AuthoritySigner, + CustodySigner, + FromCustodyTokenAccount, + FromCustodyTokenAccountDerivationData, + MigrationPool, + ToCustodyTokenAccount, + ToCustodyTokenAccountDerivationData, + }, + types::{ + SplAccount, + SplMint, + }, + MigrationError::WrongMint, +}; +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; + +use crate::accounts::MigrationPoolDerivationData; +use solitaire::{ + processors::seeded::{ + invoke_seeded, + Seeded, + }, + *, +}; + +#[derive(FromAccounts)] +pub struct MigrateTokens<'b> { + pub pool: Mut>, + pub from_mint: Data<'b, SplMint, { AccountState::Initialized }>, + pub to_mint: Data<'b, SplMint, { AccountState::Initialized }>, + pub to_token_custody: Mut>, + pub from_token_custody: Mut>, + + pub user_from_acc: Mut>, + pub user_to_acc: Mut>, + pub custody_signer: CustodySigner<'b>, + pub authority_signer: AuthoritySigner<'b>, +} + +#[derive(BorshDeserialize, BorshSerialize, Default)] +pub struct MigrateTokensData { + pub amount: u64, +} + +pub fn migrate_tokens( + ctx: &ExecutionContext, + accs: &mut MigrateTokens, + data: MigrateTokensData, +) -> Result<()> { + if *accs.from_mint.info().key != accs.pool.from { + return Err(WrongMint.into()); + } + if *accs.to_mint.info().key != accs.pool.to { + return Err(WrongMint.into()); + } + if accs.user_from_acc.mint != accs.pool.from { + return Err(WrongMint.into()); + } + if accs.user_to_acc.mint != accs.pool.to { + return Err(WrongMint.into()); + } + accs.to_token_custody.verify_derivation( + ctx.program_id, + &ToCustodyTokenAccountDerivationData { + pool: *accs.pool.info().key, + }, + )?; + accs.from_token_custody.verify_derivation( + ctx.program_id, + &FromCustodyTokenAccountDerivationData { + pool: *accs.pool.info().key, + }, + )?; + accs.pool.verify_derivation( + ctx.program_id, + &MigrationPoolDerivationData { + from: accs.pool.from, + to: accs.pool.to, + }, + )?; + + // Transfer in-tokens in + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + accs.user_from_acc.info().key, + accs.from_token_custody.info().key, + accs.authority_signer.key, + &[], + data.amount, + )?; + invoke_seeded(&transfer_ix, ctx, &accs.authority_signer, None)?; + + // The share amount should be equal to the amount of from tokens an lp would be getting + let out_amount = if accs.from_mint.decimals > accs.to_mint.decimals { + data.amount + .checked_div(10u64.pow((accs.from_mint.decimals - accs.to_mint.decimals) as u32)) + .unwrap() + } else { + data.amount + .checked_mul(10u64.pow((accs.to_mint.decimals - accs.from_mint.decimals) as u32)) + .unwrap() + }; + + // Transfer out-tokens to user + let transfer_ix = spl_token::instruction::transfer( + &spl_token::id(), + accs.to_token_custody.info().key, + accs.user_to_acc.info().key, + accs.custody_signer.key, + &[], + out_amount, + )?; + invoke_seeded(&transfer_ix, ctx, &accs.custody_signer, None)?; + + Ok(()) +} diff --git a/solana/migration/src/instructions.rs b/solana/migration/src/instructions.rs new file mode 100644 index 00000000..0b4d0ec5 --- /dev/null +++ b/solana/migration/src/instructions.rs @@ -0,0 +1,242 @@ +use crate::{ + accounts::{ + AuthoritySigner, + CustodySigner, + FromCustodyTokenAccount, + FromCustodyTokenAccountDerivationData, + MigrationPool, + MigrationPoolDerivationData, + ShareMint, + ShareMintDerivationData, + ToCustodyTokenAccount, + ToCustodyTokenAccountDerivationData, + }, + api::{ + add_liquidity::AddLiquidityData, + claim_shares::ClaimSharesData, + create_pool::CreatePoolData, + migrate_tokens::MigrateTokensData, + }, +}; +use borsh::BorshSerialize; +use solana_program::{ + instruction::{ + AccountMeta, + Instruction, + }, + pubkey::Pubkey, +}; +use solitaire::{ + processors::seeded::Seeded, + AccountState, +}; + +pub fn add_liquidity( + program_id: Pubkey, + from_mint: Pubkey, + to_mint: Pubkey, + liquidity_token_account: Pubkey, + lp_share_token_account: Pubkey, + amount: u64, +) -> solitaire::Result { + let pool = MigrationPool::<'_, { AccountState::Initialized }>::key( + &MigrationPoolDerivationData { + from: from_mint, + to: to_mint, + }, + &program_id, + ); + Ok(Instruction { + program_id, + accounts: vec![ + AccountMeta::new(pool, false), + AccountMeta::new_readonly(from_mint, false), + AccountMeta::new_readonly(to_mint, false), + AccountMeta::new( + ToCustodyTokenAccount::<'_, { AccountState::Uninitialized }>::key( + &ToCustodyTokenAccountDerivationData { pool }, + &program_id, + ), + false, + ), + AccountMeta::new( + ShareMint::<'_, { AccountState::Uninitialized }>::key( + &ShareMintDerivationData { pool }, + &program_id, + ), + false, + ), + AccountMeta::new(liquidity_token_account, false), + AccountMeta::new(lp_share_token_account, false), + AccountMeta::new_readonly(CustodySigner::key(None, &program_id), false), + AccountMeta::new_readonly(AuthoritySigner::key(None, &program_id), false), + // Dependencies + AccountMeta::new(solana_program::sysvar::rent::id(), false), + AccountMeta::new(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: ( + crate::instruction::Instruction::AddLiquidity, + AddLiquidityData { amount }, + ) + .try_to_vec()?, + }) +} + +pub fn claim_shares( + program_id: Pubkey, + from_mint: Pubkey, + to_mint: Pubkey, + output_token_account: Pubkey, + lp_share_token_account: Pubkey, + amount: u64, +) -> solitaire::Result { + let pool = MigrationPool::<'_, { AccountState::Initialized }>::key( + &MigrationPoolDerivationData { + from: from_mint, + to: to_mint, + }, + &program_id, + ); + Ok(Instruction { + program_id, + accounts: vec![ + AccountMeta::new(pool, false), + AccountMeta::new_readonly(to_mint, false), + AccountMeta::new( + FromCustodyTokenAccount::<'_, { AccountState::Uninitialized }>::key( + &FromCustodyTokenAccountDerivationData { pool }, + &program_id, + ), + false, + ), + AccountMeta::new( + ShareMint::<'_, { AccountState::Uninitialized }>::key( + &ShareMintDerivationData { pool }, + &program_id, + ), + false, + ), + AccountMeta::new(output_token_account, false), + AccountMeta::new(lp_share_token_account, false), + AccountMeta::new_readonly(CustodySigner::key(None, &program_id), false), + AccountMeta::new_readonly(AuthoritySigner::key(None, &program_id), false), + // Dependencies + AccountMeta::new(solana_program::sysvar::rent::id(), false), + AccountMeta::new(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: ( + crate::instruction::Instruction::ClaimShares, + ClaimSharesData { amount }, + ) + .try_to_vec()?, + }) +} + +pub fn create_pool( + program_id: Pubkey, + payer: Pubkey, + from_mint: Pubkey, + to_mint: Pubkey, +) -> solitaire::Result { + let pool = MigrationPool::<'_, { AccountState::Initialized }>::key( + &MigrationPoolDerivationData { + from: from_mint, + to: to_mint, + }, + &program_id, + ); + Ok(Instruction { + program_id, + accounts: vec![ + AccountMeta::new(payer, true), + AccountMeta::new(pool, false), + AccountMeta::new_readonly(from_mint, false), + AccountMeta::new_readonly(to_mint, false), + AccountMeta::new( + FromCustodyTokenAccount::<'_, { AccountState::Uninitialized }>::key( + &FromCustodyTokenAccountDerivationData { pool }, + &program_id, + ), + false, + ), + AccountMeta::new( + ToCustodyTokenAccount::<'_, { AccountState::Uninitialized }>::key( + &ToCustodyTokenAccountDerivationData { pool }, + &program_id, + ), + false, + ), + AccountMeta::new( + ShareMint::<'_, { AccountState::Uninitialized }>::key( + &ShareMintDerivationData { pool }, + &program_id, + ), + false, + ), + AccountMeta::new_readonly(CustodySigner::key(None, &program_id), false), + // Dependencies + AccountMeta::new(solana_program::sysvar::rent::id(), false), + AccountMeta::new(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: ( + crate::instruction::Instruction::CreatePool, + CreatePoolData {}, + ) + .try_to_vec()?, + }) +} + +pub fn migrate_tokens( + program_id: Pubkey, + from_mint: Pubkey, + to_mint: Pubkey, + input_token_account: Pubkey, + output_token_account: Pubkey, + amount: u64, +) -> solitaire::Result { + let pool = MigrationPool::<'_, { AccountState::Initialized }>::key( + &MigrationPoolDerivationData { + from: from_mint, + to: to_mint, + }, + &program_id, + ); + Ok(Instruction { + program_id, + accounts: vec![ + AccountMeta::new(pool, false), + AccountMeta::new_readonly(from_mint, false), + AccountMeta::new_readonly(to_mint, false), + AccountMeta::new( + ToCustodyTokenAccount::<'_, { AccountState::Uninitialized }>::key( + &ToCustodyTokenAccountDerivationData { pool }, + &program_id, + ), + false, + ), + AccountMeta::new( + FromCustodyTokenAccount::<'_, { AccountState::Uninitialized }>::key( + &FromCustodyTokenAccountDerivationData { pool }, + &program_id, + ), + false, + ), + AccountMeta::new(input_token_account, false), + AccountMeta::new(output_token_account, false), + AccountMeta::new_readonly(CustodySigner::key(None, &program_id), false), + AccountMeta::new_readonly(AuthoritySigner::key(None, &program_id), false), + // Dependencies + AccountMeta::new(solana_program::sysvar::rent::id(), false), + AccountMeta::new(solana_program::system_program::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ], + data: ( + crate::instruction::Instruction::MigrateTokens, + MigrateTokensData { amount }, + ) + .try_to_vec()?, + }) +} diff --git a/solana/migration/src/lib.rs b/solana/migration/src/lib.rs new file mode 100644 index 00000000..d0c95ef4 --- /dev/null +++ b/solana/migration/src/lib.rs @@ -0,0 +1,45 @@ +#![allow(incomplete_features)] +#![feature(const_generics)] + +use api::{ + add_liquidity::*, + claim_shares::*, + create_pool::*, + migrate_tokens::*, +}; +use solitaire::{ + solitaire, + SolitaireError, +}; + +pub mod accounts; +pub mod api; +pub mod types; + +#[cfg(feature = "no-entrypoint")] +pub mod instructions; + +#[cfg(feature = "wasm")] +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +extern crate wasm_bindgen; + +#[cfg(feature = "wasm")] +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +pub mod wasm; + +pub enum MigrationError { + WrongMint, +} + +impl From for SolitaireError { + fn from(t: MigrationError) -> SolitaireError { + SolitaireError::Custom(t as u64) + } +} + +solitaire! { + AddLiquidity(AddLiquidityData) => add_liquidity, + ClaimShares(ClaimSharesData) => claim_shares, + CreatePool(CreatePoolData) => create_pool, + MigrateTokens(MigrateTokensData) => migrate_tokens, +} diff --git a/solana/migration/src/types.rs b/solana/migration/src/types.rs new file mode 100644 index 00000000..9dd78949 --- /dev/null +++ b/solana/migration/src/types.rs @@ -0,0 +1,35 @@ +use borsh::{ + BorshDeserialize, + BorshSerialize, +}; +use serde::{ + Deserialize, + Serialize, +}; +use solana_program::pubkey::Pubkey; +use solitaire::{ + pack_type, + processors::seeded::{ + AccountOwner, + Owned, + }, +}; +use spl_token::state::{ + Account, + Mint, +}; + +#[derive(Default, Clone, Copy, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub struct PoolData { + pub from: Pubkey, + pub to: Pubkey, +} + +impl Owned for PoolData { + fn owner(&self) -> AccountOwner { + AccountOwner::This + } +} + +pack_type!(SplMint, Mint, AccountOwner::Other(spl_token::id())); +pack_type!(SplAccount, Account, AccountOwner::Other(spl_token::id())); diff --git a/solana/migration/src/wasm.rs b/solana/migration/src/wasm.rs new file mode 100644 index 00000000..60a8eaab --- /dev/null +++ b/solana/migration/src/wasm.rs @@ -0,0 +1,194 @@ +use crate::{ + accounts::{ + AuthoritySigner, + FromCustodyTokenAccount, + FromCustodyTokenAccountDerivationData, + MigrationPool, + MigrationPoolDerivationData, + ShareMint, + ShareMintDerivationData, + ToCustodyTokenAccount, + ToCustodyTokenAccountDerivationData, + }, + instructions, + types::PoolData, +}; +use borsh::BorshDeserialize; +use solana_program::pubkey::Pubkey; +use solitaire::{ + processors::seeded::Seeded, + AccountState, +}; +use std::str::FromStr; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub fn add_liquidity( + program_id: String, + from_mint: String, + to_mint: String, + liquidity_token_account: String, + lp_share_token_account: String, + amount: u64, +) -> JsValue { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + let from_mint = Pubkey::from_str(from_mint.as_str()).unwrap(); + let to_mint = Pubkey::from_str(to_mint.as_str()).unwrap(); + let liquidity_token_account = Pubkey::from_str(liquidity_token_account.as_str()).unwrap(); + let lp_share_token_account = Pubkey::from_str(lp_share_token_account.as_str()).unwrap(); + + let ix = instructions::add_liquidity( + program_id, + from_mint, + to_mint, + liquidity_token_account, + lp_share_token_account, + amount, + ) + .unwrap(); + + JsValue::from_serde(&ix).unwrap() +} + +#[wasm_bindgen] +pub fn claim_shares( + program_id: String, + from_mint: String, + to_mint: String, + output_token_account: String, + lp_share_token_account: String, + amount: u64, +) -> JsValue { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + let from_mint = Pubkey::from_str(from_mint.as_str()).unwrap(); + let to_mint = Pubkey::from_str(to_mint.as_str()).unwrap(); + let output_token_account = Pubkey::from_str(output_token_account.as_str()).unwrap(); + let lp_share_token_account = Pubkey::from_str(lp_share_token_account.as_str()).unwrap(); + + let ix = instructions::claim_shares( + program_id, + from_mint, + to_mint, + output_token_account, + lp_share_token_account, + amount, + ) + .unwrap(); + + JsValue::from_serde(&ix).unwrap() +} + +#[wasm_bindgen] +pub fn create_pool( + program_id: String, + payer: String, + from_mint: String, + to_mint: String, +) -> JsValue { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + let payer = Pubkey::from_str(payer.as_str()).unwrap(); + let from_mint = Pubkey::from_str(from_mint.as_str()).unwrap(); + let to_mint = Pubkey::from_str(to_mint.as_str()).unwrap(); + + let ix = instructions::create_pool(program_id, payer, from_mint, to_mint).unwrap(); + + JsValue::from_serde(&ix).unwrap() +} + +#[wasm_bindgen] +pub fn migrate_tokens( + program_id: String, + from_mint: String, + to_mint: String, + input_token_account: String, + output_token_account: String, + amount: u64, +) -> JsValue { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + let from_mint = Pubkey::from_str(from_mint.as_str()).unwrap(); + let to_mint = Pubkey::from_str(to_mint.as_str()).unwrap(); + let input_token_account = Pubkey::from_str(input_token_account.as_str()).unwrap(); + let output_token_account = Pubkey::from_str(output_token_account.as_str()).unwrap(); + + let ix = instructions::migrate_tokens( + program_id, + from_mint, + to_mint, + input_token_account, + output_token_account, + amount, + ) + .unwrap(); + + JsValue::from_serde(&ix).unwrap() +} + +#[wasm_bindgen] +pub fn pool_address(program_id: String, from_mint: String, to_mint: String) -> Vec { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + let from_mint_key = Pubkey::from_str(from_mint.as_str()).unwrap(); + let to_mint_key = Pubkey::from_str(to_mint.as_str()).unwrap(); + + let pool_addr = MigrationPool::<'_, { AccountState::Initialized }>::key( + &MigrationPoolDerivationData { + from: from_mint_key, + to: to_mint_key, + }, + &program_id, + ); + + pool_addr.to_bytes().to_vec() +} + +#[wasm_bindgen] +pub fn authority_address(program_id: String) -> Vec { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + + let authority_addr = AuthoritySigner::key(None, &program_id); + + authority_addr.to_bytes().to_vec() +} + +#[wasm_bindgen] +pub fn share_mint_address(program_id: String, pool: String) -> Vec { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + let pool_key = Pubkey::from_str(pool.as_str()).unwrap(); + + let share_mint_addr = ShareMint::<'_, { AccountState::Initialized }>::key( + &ShareMintDerivationData { pool: pool_key }, + &program_id, + ); + + share_mint_addr.to_bytes().to_vec() +} + +#[wasm_bindgen] +pub fn from_custody_address(program_id: String, pool: String) -> Vec { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + let pool_key = Pubkey::from_str(pool.as_str()).unwrap(); + + let from_custody_addr = FromCustodyTokenAccount::<'_, { AccountState::Initialized }>::key( + &FromCustodyTokenAccountDerivationData { pool: pool_key }, + &program_id, + ); + + from_custody_addr.to_bytes().to_vec() +} + +#[wasm_bindgen] +pub fn to_custody_address(program_id: String, pool: String) -> Vec { + let program_id = Pubkey::from_str(program_id.as_str()).unwrap(); + let pool_key = Pubkey::from_str(pool.as_str()).unwrap(); + + let to_custody_addr = ToCustodyTokenAccount::<'_, { AccountState::Initialized }>::key( + &ToCustodyTokenAccountDerivationData { pool: pool_key }, + &program_id, + ); + + to_custody_addr.to_bytes().to_vec() +} + +#[wasm_bindgen] +pub fn parse_pool(data: Vec) -> JsValue { + JsValue::from_serde(&PoolData::try_from_slice(data.as_slice()).unwrap()).unwrap() +}