Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tokens/external-delegate-token-master/anchor #194

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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