-
Notifications
You must be signed in to change notification settings - Fork 324
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add tokens/external-delegate-token-master/anchor (#194)
- Loading branch information
1 parent
9b6c969
commit ff2ab18
Showing
6 changed files
with
421 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
160 changes: 160 additions & 0 deletions
160
.../external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
146 changes: 146 additions & 0 deletions
146
tokens/external-delegate-token-master/anchor/tests/external-delegate-token-master.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
17
tokens/external-delegate-token-master/anchor/tests/types.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[]; | ||
} |
Oops, something went wrong.