Skip to content

Commit ff2ab18

Browse files
add tokens/external-delegate-token-master/anchor (#194)
1 parent 9b6c969 commit ff2ab18

File tree

6 files changed

+421
-0
lines changed

6 files changed

+421
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[features]
2+
seeds = false
3+
skip-lint = false
4+
5+
[programs.localnet]
6+
external_delegate_token_master = "FYPkt5VWMvtyWZDMGCwoKFkE3wXTzphicTpnNGuHWVbD"
7+
8+
[registry]
9+
url = "https://api.apr.dev"
10+
11+
[provider]
12+
cluster = "localnet"
13+
wallet = "~/.config/solana/id.json"
14+
15+
[scripts]
16+
test = "yarn test"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "external-delegate-token-master",
3+
"version": "1.0.0",
4+
"license": "MIT",
5+
"scripts": {
6+
"test": "jest --detectOpenHandles --forceExit",
7+
"test:watch": "jest --watch",
8+
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
9+
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check",
10+
"build": "anchor build"
11+
},
12+
"dependencies": {
13+
"@coral-xyz/anchor": "^0.29.0",
14+
"@solana/spl-token": "^0.3.9",
15+
"@solana/web3.js": "^1.90.0",
16+
"ethers": "^5.7.2"
17+
},
18+
"devDependencies": {
19+
"@babel/core": "^7.23.7",
20+
"@babel/preset-env": "^7.23.7",
21+
"@babel/preset-typescript": "^7.23.7",
22+
"@types/chai": "^4.3.0",
23+
"@types/jest": "^29.5.11",
24+
"@types/node": "^18.0.0",
25+
"babel-jest": "^29.7.0",
26+
"chai": "^4.3.4",
27+
"jest": "^29.7.0",
28+
"prettier": "^2.6.2",
29+
"solana-bankrun": "^0.2.0",
30+
"ts-jest": "^29.1.1",
31+
"typescript": "^4.9.5",
32+
"@testing-library/jest-dom": "^6.1.6"
33+
}
34+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::token;
3+
use anchor_spl::token::{Token, TokenAccount, Transfer};
4+
use solana_program::secp256k1_recover::secp256k1_recover;
5+
use sha3::{Digest, Keccak256};
6+
7+
declare_id!("FYPkt5VWMvtyWZDMGCwoKFkE3wXTzphicTpnNGuHWVbD");
8+
9+
#[program]
10+
pub mod external_delegate_token_master {
11+
use super::*;
12+
13+
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
14+
let user_account = &mut ctx.accounts.user_account;
15+
user_account.authority = ctx.accounts.authority.key();
16+
user_account.ethereum_address = [0; 20];
17+
Ok(())
18+
}
19+
20+
pub fn set_ethereum_address(ctx: Context<SetEthereumAddress>, ethereum_address: [u8; 20]) -> Result<()> {
21+
let user_account = &mut ctx.accounts.user_account;
22+
user_account.ethereum_address = ethereum_address;
23+
Ok(())
24+
}
25+
26+
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64, signature: [u8; 65], message: [u8; 32]) -> Result<()> {
27+
let user_account = &ctx.accounts.user_account;
28+
29+
if !verify_ethereum_signature(&user_account.ethereum_address, &message, &signature) {
30+
return Err(ErrorCode::InvalidSignature.into());
31+
}
32+
33+
// Transfer tokens
34+
let transfer_instruction = Transfer {
35+
from: ctx.accounts.user_token_account.to_account_info(),
36+
to: ctx.accounts.recipient_token_account.to_account_info(),
37+
authority: ctx.accounts.user_pda.to_account_info(),
38+
};
39+
40+
token::transfer(
41+
CpiContext::new_with_signer(
42+
ctx.accounts.token_program.to_account_info(),
43+
transfer_instruction,
44+
&[&[
45+
user_account.key().as_ref(),
46+
&[ctx.bumps.user_pda],
47+
]],
48+
),
49+
amount,
50+
)?;
51+
52+
Ok(())
53+
}
54+
55+
pub fn authority_transfer(ctx: Context<AuthorityTransfer>, amount: u64) -> Result<()> {
56+
// Transfer tokens
57+
let transfer_instruction = Transfer {
58+
from: ctx.accounts.user_token_account.to_account_info(),
59+
to: ctx.accounts.recipient_token_account.to_account_info(),
60+
authority: ctx.accounts.user_pda.to_account_info(),
61+
};
62+
63+
token::transfer(
64+
CpiContext::new_with_signer(
65+
ctx.accounts.token_program.to_account_info(),
66+
transfer_instruction,
67+
&[&[
68+
ctx.accounts.user_account.key().as_ref(),
69+
&[ctx.bumps.user_pda],
70+
]],
71+
),
72+
amount,
73+
)?;
74+
75+
Ok(())
76+
}
77+
}
78+
79+
#[derive(Accounts)]
80+
pub struct Initialize<'info> {
81+
#[account(init, payer = authority, space = 8 + 32 + 20)] // Ensure this is only for user_account
82+
pub user_account: Account<'info, UserAccount>,
83+
#[account(mut)]
84+
pub authority: Signer<'info>, // This should remain as a signer
85+
pub system_program: Program<'info, System>, // Required for initialization
86+
}
87+
88+
#[derive(Accounts)]
89+
pub struct SetEthereumAddress<'info> {
90+
#[account(mut, has_one = authority)]
91+
pub user_account: Account<'info, UserAccount>,
92+
pub authority: Signer<'info>,
93+
}
94+
95+
#[derive(Accounts)]
96+
pub struct TransferTokens<'info> {
97+
#[account(has_one = authority)]
98+
pub user_account: Account<'info, UserAccount>,
99+
pub authority: Signer<'info>,
100+
#[account(mut)]
101+
pub user_token_account: Account<'info, TokenAccount>,
102+
#[account(mut)]
103+
pub recipient_token_account: Account<'info, TokenAccount>,
104+
#[account(
105+
seeds = [user_account.key().as_ref()],
106+
bump,
107+
)]
108+
pub user_pda: SystemAccount<'info>,
109+
pub token_program: Program<'info, Token>,
110+
}
111+
112+
#[derive(Accounts)]
113+
pub struct AuthorityTransfer<'info> {
114+
#[account(has_one = authority)]
115+
pub user_account: Account<'info, UserAccount>,
116+
pub authority: Signer<'info>,
117+
#[account(mut)]
118+
pub user_token_account: Account<'info, TokenAccount>,
119+
#[account(mut)]
120+
pub recipient_token_account: Account<'info, TokenAccount>,
121+
#[account(
122+
seeds = [user_account.key().as_ref()],
123+
bump,
124+
)]
125+
pub user_pda: SystemAccount<'info>,
126+
pub token_program: Program<'info, Token>,
127+
}
128+
129+
#[account]
130+
pub struct UserAccount {
131+
pub authority: Pubkey,
132+
pub ethereum_address: [u8; 20],
133+
}
134+
135+
#[error_code]
136+
pub enum ErrorCode {
137+
#[msg("Invalid Ethereum signature")]
138+
InvalidSignature,
139+
}
140+
141+
fn verify_ethereum_signature(ethereum_address: &[u8; 20], message: &[u8; 32], signature: &[u8; 65]) -> bool {
142+
let recovery_id = signature[64];
143+
let mut sig = [0u8; 64];
144+
sig.copy_from_slice(&signature[..64]);
145+
146+
if let Ok(pubkey) = secp256k1_recover(message, recovery_id, &sig) {
147+
let pubkey_bytes = pubkey.to_bytes();
148+
let mut recovered_address = [0u8; 20];
149+
recovered_address.copy_from_slice(&keccak256(&pubkey_bytes[1..])[12..]);
150+
recovered_address == *ethereum_address
151+
} else {
152+
false
153+
}
154+
}
155+
156+
fn keccak256(data: &[u8]) -> [u8; 32] {
157+
let mut hasher = Keccak256::new();
158+
hasher.update(data);
159+
hasher.finalize().into()
160+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { start } from 'solana-bankrun';
2+
import { expect } from 'chai';
3+
import { PublicKey, SystemProgram, Keypair, Connection } from '@solana/web3.js';
4+
import { TOKEN_PROGRAM_ID, createMint, getOrCreateAssociatedTokenAccount, mintTo } from '@solana/spl-token';
5+
6+
jest.setTimeout(30000); // Set timeout to 30 seconds
7+
8+
const ACCOUNT_SIZE = 8 + 32 + 20; // Define your account size here
9+
10+
async function retryWithBackoff(fn: () => Promise<any>, retries = 5, delay = 500): Promise<any> {
11+
try {
12+
return await fn();
13+
} catch (err) {
14+
if (retries === 0) throw err;
15+
await new Promise(resolve => setTimeout(resolve, delay));
16+
return retryWithBackoff(fn, retries - 1, delay * 2);
17+
}
18+
}
19+
20+
describe('External Delegate Token Master Tests', () => {
21+
let context: any;
22+
let program: any;
23+
let authority: Keypair;
24+
let userAccount: Keypair;
25+
let mint: PublicKey;
26+
let userTokenAccount: PublicKey;
27+
let recipientTokenAccount: PublicKey;
28+
let userPda: PublicKey;
29+
let bumpSeed: number;
30+
31+
beforeEach(async () => {
32+
authority = Keypair.generate();
33+
userAccount = Keypair.generate();
34+
35+
const programs = [
36+
{
37+
name: "external_delegate_token_master",
38+
programId: new PublicKey("FYPkt5VWMvtyWZDMGCwoKFkE3wXTzphicTpnNGuHWVbD"),
39+
program: "target/deploy/external_delegate_token_master.so",
40+
},
41+
];
42+
43+
context = await retryWithBackoff(async () => await start(programs, []));
44+
45+
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
46+
context.connection = connection;
47+
48+
// Airdrop SOL to authority with retry logic
49+
await retryWithBackoff(async () => {
50+
await connection.requestAirdrop(authority.publicKey, 1000000000);
51+
});
52+
53+
// Create mint with retry logic
54+
mint = await retryWithBackoff(async () =>
55+
await createMint(connection, authority, authority.publicKey, null, 6)
56+
);
57+
58+
const userTokenAccountInfo = await retryWithBackoff(async () =>
59+
await getOrCreateAssociatedTokenAccount(connection, authority, mint, authority.publicKey)
60+
);
61+
userTokenAccount = userTokenAccountInfo.address;
62+
63+
const recipientTokenAccountInfo = await retryWithBackoff(async () =>
64+
await getOrCreateAssociatedTokenAccount(connection, authority, mint, Keypair.generate().publicKey)
65+
);
66+
recipientTokenAccount = recipientTokenAccountInfo.address;
67+
68+
// Mint tokens to the user's account
69+
await retryWithBackoff(async () =>
70+
await mintTo(connection, authority, mint, userTokenAccount, authority, 1000000000)
71+
);
72+
73+
// Find program-derived address (PDA)
74+
[userPda, bumpSeed] = await retryWithBackoff(async () =>
75+
await PublicKey.findProgramAddress([userAccount.publicKey.toBuffer()], context.program.programId)
76+
);
77+
});
78+
79+
it('should initialize user account', async () => {
80+
const space = ACCOUNT_SIZE;
81+
const rentExempt = await retryWithBackoff(async () => {
82+
return await context.connection.getMinimumBalanceForRentExemption(space);
83+
});
84+
85+
await context.program.methods
86+
.initialize()
87+
.accounts({
88+
userAccount: userAccount.publicKey,
89+
authority: authority.publicKey,
90+
systemProgram: SystemProgram.programId,
91+
})
92+
.preInstructions([
93+
SystemProgram.createAccount({
94+
fromPubkey: authority.publicKey,
95+
newAccountPubkey: userAccount.publicKey,
96+
lamports: rentExempt,
97+
space: space,
98+
programId: context.program.programId,
99+
}),
100+
])
101+
.signers([authority, userAccount])
102+
.rpc();
103+
104+
const account = await context.program.account.userAccount.fetch(userAccount.publicKey);
105+
expect(account.authority.toString()).to.equal(authority.publicKey.toString());
106+
expect(account.ethereumAddress).to.deep.equal(new Array(20).fill(0));
107+
});
108+
109+
it('should set ethereum address', async () => {
110+
const ethereumAddress = Buffer.from('1C8cd0c38F8DE35d6056c7C7aBFa7e65D260E816', 'hex');
111+
112+
await context.program.methods
113+
.setEthereumAddress(ethereumAddress)
114+
.accounts({
115+
userAccount: userAccount.publicKey,
116+
authority: authority.publicKey,
117+
})
118+
.signers([authority])
119+
.rpc();
120+
121+
const account = await context.program.account.userAccount.fetch(userAccount.publicKey);
122+
expect(account.ethereumAddress).to.deep.equal(Array.from(ethereumAddress));
123+
});
124+
125+
it('should perform authority transfer', async () => {
126+
const newAuthority = Keypair.generate();
127+
128+
await context.program.methods
129+
.transferAuthority(newAuthority.publicKey)
130+
.accounts({
131+
userAccount: userAccount.publicKey,
132+
authority: authority.publicKey,
133+
})
134+
.signers([authority])
135+
.rpc();
136+
137+
const account = await context.program.account.userAccount.fetch(userAccount.publicKey);
138+
expect(account.authority.toString()).to.equal(newAuthority.publicKey.toString());
139+
});
140+
141+
afterEach(async () => {
142+
if (context && typeof context.terminate === 'function') {
143+
await context.terminate();
144+
}
145+
});
146+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// tests/types.ts
2+
import { PublicKey } from '@solana/web3.js';
3+
4+
export interface ProgramTestContext {
5+
connection: any;
6+
programs: {
7+
programId: PublicKey;
8+
program: string;
9+
}[];
10+
grantLamports: (address: PublicKey, amount: number) => Promise<void>;
11+
terminate: () => Promise<void>;
12+
}
13+
14+
export interface UserAccount {
15+
authority: PublicKey;
16+
ethereumAddress: number[];
17+
}

0 commit comments

Comments
 (0)