Skip to content

Commit

Permalink
add tokens/external-delegate-token-master/anchor (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
vijaygopalbalasa authored Jan 2, 2025
1 parent 9b6c969 commit ff2ab18
Show file tree
Hide file tree
Showing 6 changed files with 421 additions and 0 deletions.
16 changes: 16 additions & 0 deletions tokens/external-delegate-token-master/anchor/Anchor.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 34 additions & 0 deletions tokens/external-delegate-token-master/anchor/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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 changes: 17 additions & 0 deletions tokens/external-delegate-token-master/anchor/tests/types.js
Original file line number Diff line number Diff line change
@@ -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[];
}
Loading

0 comments on commit ff2ab18

Please sign in to comment.