ソースを参照

add tokens/external-delegate-token-master/anchor (#194)

Vijaygopal Balasa 9 ヶ月 前
コミット
ff2ab18478

+ 16 - 0
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"

+ 34 - 0
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"
+  }
+}

+ 160 - 0
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<Initialize>) -> 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<SetEthereumAddress>, 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<TransferTokens>, 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<AuthorityTransfer>, 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()
+}

+ 146 - 0
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<any>, retries = 5, delay = 500): Promise<any> {
+  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();
+    }
+  });
+});

+ 17 - 0
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<void>;
+    terminate: () => Promise<void>;
+}
+
+export interface UserAccount {
+    authority: PublicKey;
+    ethereumAddress: number[];
+}

+ 48 - 0
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"
+  ]
+}