From ff2ab184789cb5da50098b1a2b51207b568c16bd Mon Sep 17 00:00:00 2001 From: Vijaygopal Balasa <31928663+vijaygopalbalasa@users.noreply.github.com> Date: Thu, 2 Jan 2025 19:44:25 +0530 Subject: [PATCH] add tokens/external-delegate-token-master/anchor (#194) --- .../anchor/Anchor.toml | 16 ++ .../anchor/package.json | 34 ++++ .../external-delegate-token-master/src/lib.rs | 160 ++++++++++++++++++ .../external-delegate-token-master.test.ts | 146 ++++++++++++++++ .../anchor/tests/types.js | 17 ++ .../anchor/tsconfig.json | 48 ++++++ 6 files changed, 421 insertions(+) create mode 100644 tokens/external-delegate-token-master/anchor/Anchor.toml create mode 100644 tokens/external-delegate-token-master/anchor/package.json create mode 100644 tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs create mode 100644 tokens/external-delegate-token-master/anchor/tests/external-delegate-token-master.test.ts create mode 100644 tokens/external-delegate-token-master/anchor/tests/types.js create mode 100644 tokens/external-delegate-token-master/anchor/tsconfig.json diff --git a/tokens/external-delegate-token-master/anchor/Anchor.toml b/tokens/external-delegate-token-master/anchor/Anchor.toml new file mode 100644 index 000000000..a3838dbb6 --- /dev/null +++ b/tokens/external-delegate-token-master/anchor/Anchor.toml @@ -0,0 +1,16 @@ +[features] +seeds = false +skip-lint = false + +[programs.localnet] +external_delegate_token_master = "FYPkt5VWMvtyWZDMGCwoKFkE3wXTzphicTpnNGuHWVbD" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn test" \ No newline at end of file diff --git a/tokens/external-delegate-token-master/anchor/package.json b/tokens/external-delegate-token-master/anchor/package.json new file mode 100644 index 000000000..af72a7d63 --- /dev/null +++ b/tokens/external-delegate-token-master/anchor/package.json @@ -0,0 +1,34 @@ +{ + "name": "external-delegate-token-master", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "test": "jest --detectOpenHandles --forceExit", + "test:watch": "jest --watch", + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", + "build": "anchor build" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.29.0", + "@solana/spl-token": "^0.3.9", + "@solana/web3.js": "^1.90.0", + "ethers": "^5.7.2" + }, + "devDependencies": { + "@babel/core": "^7.23.7", + "@babel/preset-env": "^7.23.7", + "@babel/preset-typescript": "^7.23.7", + "@types/chai": "^4.3.0", + "@types/jest": "^29.5.11", + "@types/node": "^18.0.0", + "babel-jest": "^29.7.0", + "chai": "^4.3.4", + "jest": "^29.7.0", + "prettier": "^2.6.2", + "solana-bankrun": "^0.2.0", + "ts-jest": "^29.1.1", + "typescript": "^4.9.5", + "@testing-library/jest-dom": "^6.1.6" + } +} \ No newline at end of file diff --git a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs new file mode 100644 index 000000000..833c3116d --- /dev/null +++ b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs @@ -0,0 +1,160 @@ +use anchor_lang::prelude::*; +use anchor_spl::token; +use anchor_spl::token::{Token, TokenAccount, Transfer}; +use solana_program::secp256k1_recover::secp256k1_recover; +use sha3::{Digest, Keccak256}; + +declare_id!("FYPkt5VWMvtyWZDMGCwoKFkE3wXTzphicTpnNGuHWVbD"); + +#[program] +pub mod external_delegate_token_master { + use super::*; + + pub fn initialize(ctx: Context) -> Result<()> { + let user_account = &mut ctx.accounts.user_account; + user_account.authority = ctx.accounts.authority.key(); + user_account.ethereum_address = [0; 20]; + Ok(()) + } + + pub fn set_ethereum_address(ctx: Context, ethereum_address: [u8; 20]) -> Result<()> { + let user_account = &mut ctx.accounts.user_account; + user_account.ethereum_address = ethereum_address; + Ok(()) + } + + pub fn transfer_tokens(ctx: Context, amount: u64, signature: [u8; 65], message: [u8; 32]) -> Result<()> { + let user_account = &ctx.accounts.user_account; + + if !verify_ethereum_signature(&user_account.ethereum_address, &message, &signature) { + return Err(ErrorCode::InvalidSignature.into()); + } + + // Transfer tokens + let transfer_instruction = Transfer { + from: ctx.accounts.user_token_account.to_account_info(), + to: ctx.accounts.recipient_token_account.to_account_info(), + authority: ctx.accounts.user_pda.to_account_info(), + }; + + token::transfer( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + transfer_instruction, + &[&[ + user_account.key().as_ref(), + &[ctx.bumps.user_pda], + ]], + ), + amount, + )?; + + Ok(()) + } + + pub fn authority_transfer(ctx: Context, amount: u64) -> Result<()> { + // Transfer tokens + let transfer_instruction = Transfer { + from: ctx.accounts.user_token_account.to_account_info(), + to: ctx.accounts.recipient_token_account.to_account_info(), + authority: ctx.accounts.user_pda.to_account_info(), + }; + + token::transfer( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + transfer_instruction, + &[&[ + ctx.accounts.user_account.key().as_ref(), + &[ctx.bumps.user_pda], + ]], + ), + amount, + )?; + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account(init, payer = authority, space = 8 + 32 + 20)] // Ensure this is only for user_account + pub user_account: Account<'info, UserAccount>, + #[account(mut)] + pub authority: Signer<'info>, // This should remain as a signer + pub system_program: Program<'info, System>, // Required for initialization +} + +#[derive(Accounts)] +pub struct SetEthereumAddress<'info> { + #[account(mut, has_one = authority)] + pub user_account: Account<'info, UserAccount>, + pub authority: Signer<'info>, +} + +#[derive(Accounts)] +pub struct TransferTokens<'info> { + #[account(has_one = authority)] + pub user_account: Account<'info, UserAccount>, + pub authority: Signer<'info>, + #[account(mut)] + pub user_token_account: Account<'info, TokenAccount>, + #[account(mut)] + pub recipient_token_account: Account<'info, TokenAccount>, + #[account( + seeds = [user_account.key().as_ref()], + bump, + )] + pub user_pda: SystemAccount<'info>, + pub token_program: Program<'info, Token>, +} + +#[derive(Accounts)] +pub struct AuthorityTransfer<'info> { + #[account(has_one = authority)] + pub user_account: Account<'info, UserAccount>, + pub authority: Signer<'info>, + #[account(mut)] + pub user_token_account: Account<'info, TokenAccount>, + #[account(mut)] + pub recipient_token_account: Account<'info, TokenAccount>, + #[account( + seeds = [user_account.key().as_ref()], + bump, + )] + pub user_pda: SystemAccount<'info>, + pub token_program: Program<'info, Token>, +} + +#[account] +pub struct UserAccount { + pub authority: Pubkey, + pub ethereum_address: [u8; 20], +} + +#[error_code] +pub enum ErrorCode { + #[msg("Invalid Ethereum signature")] + InvalidSignature, +} + +fn verify_ethereum_signature(ethereum_address: &[u8; 20], message: &[u8; 32], signature: &[u8; 65]) -> bool { + let recovery_id = signature[64]; + let mut sig = [0u8; 64]; + sig.copy_from_slice(&signature[..64]); + + if let Ok(pubkey) = secp256k1_recover(message, recovery_id, &sig) { + let pubkey_bytes = pubkey.to_bytes(); + let mut recovered_address = [0u8; 20]; + recovered_address.copy_from_slice(&keccak256(&pubkey_bytes[1..])[12..]); + recovered_address == *ethereum_address + } else { + false + } +} + +fn keccak256(data: &[u8]) -> [u8; 32] { + let mut hasher = Keccak256::new(); + hasher.update(data); + hasher.finalize().into() +} \ No newline at end of file diff --git a/tokens/external-delegate-token-master/anchor/tests/external-delegate-token-master.test.ts b/tokens/external-delegate-token-master/anchor/tests/external-delegate-token-master.test.ts new file mode 100644 index 000000000..859d74724 --- /dev/null +++ b/tokens/external-delegate-token-master/anchor/tests/external-delegate-token-master.test.ts @@ -0,0 +1,146 @@ +import { start } from 'solana-bankrun'; +import { expect } from 'chai'; +import { PublicKey, SystemProgram, Keypair, Connection } from '@solana/web3.js'; +import { TOKEN_PROGRAM_ID, createMint, getOrCreateAssociatedTokenAccount, mintTo } from '@solana/spl-token'; + +jest.setTimeout(30000); // Set timeout to 30 seconds + +const ACCOUNT_SIZE = 8 + 32 + 20; // Define your account size here + +async function retryWithBackoff(fn: () => Promise, retries = 5, delay = 500): Promise { + try { + return await fn(); + } catch (err) { + if (retries === 0) throw err; + await new Promise(resolve => setTimeout(resolve, delay)); + return retryWithBackoff(fn, retries - 1, delay * 2); + } +} + +describe('External Delegate Token Master Tests', () => { + let context: any; + let program: any; + let authority: Keypair; + let userAccount: Keypair; + let mint: PublicKey; + let userTokenAccount: PublicKey; + let recipientTokenAccount: PublicKey; + let userPda: PublicKey; + let bumpSeed: number; + + beforeEach(async () => { + authority = Keypair.generate(); + userAccount = Keypair.generate(); + + const programs = [ + { + name: "external_delegate_token_master", + programId: new PublicKey("FYPkt5VWMvtyWZDMGCwoKFkE3wXTzphicTpnNGuHWVbD"), + program: "target/deploy/external_delegate_token_master.so", + }, + ]; + + context = await retryWithBackoff(async () => await start(programs, [])); + + const connection = new Connection("https://api.devnet.solana.com", "confirmed"); + context.connection = connection; + + // Airdrop SOL to authority with retry logic + await retryWithBackoff(async () => { + await connection.requestAirdrop(authority.publicKey, 1000000000); + }); + + // Create mint with retry logic + mint = await retryWithBackoff(async () => + await createMint(connection, authority, authority.publicKey, null, 6) + ); + + const userTokenAccountInfo = await retryWithBackoff(async () => + await getOrCreateAssociatedTokenAccount(connection, authority, mint, authority.publicKey) + ); + userTokenAccount = userTokenAccountInfo.address; + + const recipientTokenAccountInfo = await retryWithBackoff(async () => + await getOrCreateAssociatedTokenAccount(connection, authority, mint, Keypair.generate().publicKey) + ); + recipientTokenAccount = recipientTokenAccountInfo.address; + + // Mint tokens to the user's account + await retryWithBackoff(async () => + await mintTo(connection, authority, mint, userTokenAccount, authority, 1000000000) + ); + + // Find program-derived address (PDA) + [userPda, bumpSeed] = await retryWithBackoff(async () => + await PublicKey.findProgramAddress([userAccount.publicKey.toBuffer()], context.program.programId) + ); + }); + + it('should initialize user account', async () => { + const space = ACCOUNT_SIZE; + const rentExempt = await retryWithBackoff(async () => { + return await context.connection.getMinimumBalanceForRentExemption(space); + }); + + await context.program.methods + .initialize() + .accounts({ + userAccount: userAccount.publicKey, + authority: authority.publicKey, + systemProgram: SystemProgram.programId, + }) + .preInstructions([ + SystemProgram.createAccount({ + fromPubkey: authority.publicKey, + newAccountPubkey: userAccount.publicKey, + lamports: rentExempt, + space: space, + programId: context.program.programId, + }), + ]) + .signers([authority, userAccount]) + .rpc(); + + const account = await context.program.account.userAccount.fetch(userAccount.publicKey); + expect(account.authority.toString()).to.equal(authority.publicKey.toString()); + expect(account.ethereumAddress).to.deep.equal(new Array(20).fill(0)); + }); + + it('should set ethereum address', async () => { + const ethereumAddress = Buffer.from('1C8cd0c38F8DE35d6056c7C7aBFa7e65D260E816', 'hex'); + + await context.program.methods + .setEthereumAddress(ethereumAddress) + .accounts({ + userAccount: userAccount.publicKey, + authority: authority.publicKey, + }) + .signers([authority]) + .rpc(); + + const account = await context.program.account.userAccount.fetch(userAccount.publicKey); + expect(account.ethereumAddress).to.deep.equal(Array.from(ethereumAddress)); + }); + + it('should perform authority transfer', async () => { + const newAuthority = Keypair.generate(); + + await context.program.methods + .transferAuthority(newAuthority.publicKey) + .accounts({ + userAccount: userAccount.publicKey, + authority: authority.publicKey, + }) + .signers([authority]) + .rpc(); + + const account = await context.program.account.userAccount.fetch(userAccount.publicKey); + expect(account.authority.toString()).to.equal(newAuthority.publicKey.toString()); + }); + + afterEach(async () => { + if (context && typeof context.terminate === 'function') { + await context.terminate(); + } + }); +}); diff --git a/tokens/external-delegate-token-master/anchor/tests/types.js b/tokens/external-delegate-token-master/anchor/tests/types.js new file mode 100644 index 000000000..cdbd30999 --- /dev/null +++ b/tokens/external-delegate-token-master/anchor/tests/types.js @@ -0,0 +1,17 @@ +// tests/types.ts +import { PublicKey } from '@solana/web3.js'; + +export interface ProgramTestContext { + connection: any; + programs: { + programId: PublicKey; + program: string; + }[]; + grantLamports: (address: PublicKey, amount: number) => Promise; + terminate: () => Promise; +} + +export interface UserAccount { + authority: PublicKey; + ethereumAddress: number[]; +} \ No newline at end of file diff --git a/tokens/external-delegate-token-master/anchor/tsconfig.json b/tokens/external-delegate-token-master/anchor/tsconfig.json new file mode 100644 index 000000000..b5f6880d5 --- /dev/null +++ b/tokens/external-delegate-token-master/anchor/tsconfig.json @@ -0,0 +1,48 @@ +{ + "compilerOptions": { + "types": [ + "jest", + "node" + ], + "typeRoots": [ + "./node_modules/@types" + ], + "lib": [ + "es2015", + "dom", + "es6", + "es2017", + "esnext.asynciterable" + ], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true, + "resolveJsonModule": true, + "sourceMap": true, + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "allowJs": true, + "strict": true, + "strictNullChecks": true, + "noImplicitAny": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + }, + "outDir": "dist" + }, + "include": [ + "tests/**/*", + "programs/**/*", + "jest.setup.js", + "jest.config.js" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file