Skip to content

Commit 25772a0

Browse files
committed
feat: added a test sale program demoing wen-distribution-program features with tests
1 parent cb5a007 commit 25772a0

File tree

29 files changed

+2634
-223
lines changed

29 files changed

+2634
-223
lines changed

Anchor.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ resolution = true
55
skip-lint = false
66

77
[programs.localnet]
8-
test_sale = "7EhFwHKHPF4ffuFtsh1929XfsnbHKaA5QNbouX5PYpSj"
8+
test_sale = "saLeHtY1jcSpuy5NKGX4pryocQ51WGUYqSSCKJNsgrP"
99
wen_new_standard = "wns1gDLt8fgLcGhWi5MqAqgXpwEP1JftKE9eZnXS1HM"
1010
wen_royalty_distribution = "diste3nXmK7ddDTs1zb6uday6j4etCa9RChD8fJ1xay"
1111

programs/test_sale/src/constants.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub const TEST_SALE: &[u8] = b"test_sale";
2+
pub const SALE: &[u8] = b"sale";
3+
pub const LISTING: &[u8] = b"listing";

programs/test_sale/src/errors.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use anchor_lang::prelude::*;
2+
3+
#[error_code]
4+
pub enum TestSaleError {
5+
#[msg("Buy amount mismatch with listing amount")]
6+
ListingAmountMismatch,
7+
#[msg("SPL Payment token account required")]
8+
PaymentTokenAccountNotExistant,
9+
#[msg("Invalid SPL Payment token account")]
10+
InvalidPaymentTokenAccount,
11+
#[msg("Arithmetic error")]
12+
ArithmeticError,
13+
}
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
use anchor_lang::{
2+
prelude::*,
3+
system_program::{transfer, Transfer},
4+
};
5+
use anchor_spl::{
6+
associated_token::AssociatedToken,
7+
token_2022::spl_token_2022::{extension::StateWithExtensions, state::Mint as StateMint},
8+
token_2022::{transfer_checked, Token2022, TransferChecked},
9+
token_interface::{Mint, TokenAccount},
10+
};
11+
use wen_new_standard::{
12+
cpi::{
13+
accounts::{ApproveTransfer, ThawDelegatedAccount},
14+
approve_transfer, thaw_mint_account,
15+
},
16+
program::WenNewStandard,
17+
};
18+
use wen_royalty_distribution::{program::WenRoyaltyDistribution, DistributionAccount};
19+
20+
use crate::{
21+
constants::*,
22+
utils::{
23+
assert_right_associated_token_account, transfer_checked_with_hook, TransferCheckedWithHook,
24+
},
25+
};
26+
use crate::{errors::*, utils::create_associated_token_account};
27+
use crate::{state::*, utils::calculate_royalties};
28+
29+
#[derive(Accounts)]
30+
#[instruction(args: FulfillListingArgs)]
31+
pub struct FulfillListing<'info> {
32+
#[account(mut)]
33+
pub payer: Signer<'info>,
34+
35+
#[account(
36+
mut,
37+
seeds = [
38+
TEST_SALE,
39+
LISTING,
40+
listing.seller.as_ref(),
41+
listing.mint.as_ref(),
42+
],
43+
bump = listing.bump,
44+
has_one = sale,
45+
has_one = mint,
46+
has_one = seller,
47+
has_one = seller_token_account,
48+
constraint = args.buy_amount.eq(&args.buy_amount) @ TestSaleError::ListingAmountMismatch
49+
)]
50+
pub listing: Account<'info, Listing>,
51+
52+
#[account(
53+
mut,
54+
seeds = [
55+
TEST_SALE,
56+
SALE,
57+
sale.group.key().as_ref(),
58+
sale.distribution.key().as_ref()
59+
],
60+
bump = sale.bump,
61+
// has_one = distribution,
62+
)]
63+
pub sale: Account<'info, Sale>,
64+
65+
/// CHECK: Could be SOL or SPL, checked in distribution program
66+
pub payment_mint: UncheckedAccount<'info>,
67+
68+
#[account(mut)]
69+
pub buyer: Signer<'info>,
70+
71+
/// CHECK: Checked based on payment_mint
72+
#[account(mut)]
73+
pub buyer_payment_token_account: UncheckedAccount<'info>,
74+
75+
#[account(
76+
mut,
77+
has_one = payment_mint,
78+
constraint = listing.payment_mint.eq(&distribution.payment_mint)
79+
)]
80+
pub distribution: Account<'info, DistributionAccount>,
81+
82+
/// CHECK: Created and checked based on payment_mint
83+
#[account(mut)]
84+
pub distribution_payment_token_account: UncheckedAccount<'info>,
85+
86+
#[account(mut)]
87+
pub mint: InterfaceAccount<'info, Mint>,
88+
89+
#[account(
90+
mut,
91+
token::mint = mint,
92+
token::authority = seller,
93+
)]
94+
pub seller_token_account: InterfaceAccount<'info, TokenAccount>,
95+
96+
#[account(
97+
init_if_needed,
98+
payer = payer,
99+
associated_token::mint = mint,
100+
associated_token::authority = buyer,
101+
)]
102+
pub buyer_token_account: InterfaceAccount<'info, TokenAccount>,
103+
104+
/// CHECK: Constraint checked with listing. Could be any account
105+
#[account(mut)]
106+
pub seller: SystemAccount<'info>,
107+
108+
/// CHECK: Checked based on payment_mint
109+
#[account(mut)]
110+
pub seller_payment_token_account: UncheckedAccount<'info>,
111+
112+
/// CHECK: Checked inside WNS program
113+
pub manager: UncheckedAccount<'info>,
114+
/// CHECK: Checked inside Token extensions program
115+
pub extra_metas_account: UncheckedAccount<'info>,
116+
/// CHECK: Checked inside WNS program
117+
#[account(mut)]
118+
pub approve_account: UncheckedAccount<'info>,
119+
120+
pub wns_program: Program<'info, WenNewStandard>,
121+
pub distribution_program: Program<'info, WenRoyaltyDistribution>,
122+
pub associated_token_program: Program<'info, AssociatedToken>,
123+
pub token_program: Program<'info, Token2022>,
124+
pub system_program: Program<'info, System>,
125+
}
126+
127+
pub fn handler(ctx: Context<FulfillListing>, args: FulfillListingArgs) -> Result<()> {
128+
let listing = &mut ctx.accounts.listing;
129+
let sale = &ctx.accounts.sale;
130+
131+
let is_payment_mint_spl = ctx.accounts.payment_mint.key.ne(&Pubkey::default());
132+
133+
let signer_seeds: &[&[&[u8]]] = &[&[
134+
TEST_SALE,
135+
SALE,
136+
sale.group.as_ref(),
137+
sale.distribution.as_ref(),
138+
&[sale.bump],
139+
]];
140+
141+
// Thaw NFT
142+
thaw_mint_account(CpiContext::new_with_signer(
143+
ctx.accounts.wns_program.to_account_info(),
144+
ThawDelegatedAccount {
145+
payer: ctx.accounts.payer.to_account_info(),
146+
delegate_authority: ctx.accounts.sale.to_account_info(),
147+
manager: ctx.accounts.manager.to_account_info(),
148+
mint: ctx.accounts.mint.to_account_info(),
149+
mint_token_account: ctx.accounts.seller_token_account.to_account_info(),
150+
token_program: ctx.accounts.token_program.to_account_info(),
151+
user: ctx.accounts.seller.to_account_info(),
152+
},
153+
signer_seeds,
154+
))?;
155+
156+
// Transfer (listing_amount - royalty) to seller
157+
// Checking payment mint and creating seller token account if necessary
158+
let royalty_funds = calculate_royalties(&ctx.accounts.mint.to_account_info(), args.buy_amount)?;
159+
160+
let funds_to_send = listing
161+
.listing_amount
162+
.checked_sub(royalty_funds)
163+
.ok_or(TestSaleError::ArithmeticError)?;
164+
165+
if is_payment_mint_spl {
166+
let payment_mint = &ctx.accounts.payment_mint.try_borrow_data()?;
167+
let payment_mint_data = StateWithExtensions::<StateMint>::unpack(payment_mint)?;
168+
169+
require_neq!(
170+
ctx.accounts.buyer_payment_token_account.key,
171+
&Pubkey::default(),
172+
TestSaleError::PaymentTokenAccountNotExistant
173+
);
174+
175+
require_neq!(
176+
ctx.accounts.buyer_payment_token_account.key,
177+
&Pubkey::default(),
178+
TestSaleError::PaymentTokenAccountNotExistant
179+
);
180+
181+
assert_right_associated_token_account(
182+
ctx.accounts.buyer.key,
183+
ctx.accounts.payment_mint.key,
184+
ctx.accounts.buyer_payment_token_account.key,
185+
)?;
186+
187+
if ctx.accounts.seller_payment_token_account.data_is_empty() {
188+
create_associated_token_account(
189+
ctx.accounts.payer.to_account_info(),
190+
ctx.accounts.seller.to_account_info(),
191+
ctx.accounts.payment_mint.to_account_info(),
192+
ctx.accounts.seller_payment_token_account.to_account_info(),
193+
ctx.accounts.associated_token_program.to_account_info(),
194+
ctx.accounts.token_program.to_account_info(),
195+
ctx.accounts.system_program.to_account_info(),
196+
)?;
197+
}
198+
199+
transfer_checked(
200+
CpiContext::new(
201+
ctx.accounts.token_program.to_account_info(),
202+
TransferChecked {
203+
authority: ctx.accounts.buyer.to_account_info(),
204+
from: ctx.accounts.buyer_payment_token_account.to_account_info(),
205+
to: ctx.accounts.seller_payment_token_account.to_account_info(),
206+
mint: ctx.accounts.payment_mint.to_account_info(),
207+
},
208+
),
209+
funds_to_send,
210+
payment_mint_data.base.decimals,
211+
)?;
212+
} else {
213+
transfer(
214+
CpiContext::new(
215+
ctx.accounts.system_program.to_account_info(),
216+
Transfer {
217+
from: ctx.accounts.buyer.to_account_info(),
218+
to: ctx.accounts.seller.to_account_info(),
219+
},
220+
),
221+
funds_to_send,
222+
)?;
223+
}
224+
225+
// Approve Transfer
226+
if is_payment_mint_spl
227+
&& ctx
228+
.accounts
229+
.distribution_payment_token_account
230+
.data_is_empty()
231+
{
232+
create_associated_token_account(
233+
ctx.accounts.payer.to_account_info(),
234+
ctx.accounts.distribution.to_account_info(),
235+
ctx.accounts.payment_mint.to_account_info(),
236+
ctx.accounts
237+
.distribution_payment_token_account
238+
.to_account_info(),
239+
ctx.accounts.associated_token_program.to_account_info(),
240+
ctx.accounts.token_program.to_account_info(),
241+
ctx.accounts.system_program.to_account_info(),
242+
)?;
243+
}
244+
245+
approve_transfer(
246+
CpiContext::new(
247+
ctx.accounts.wns_program.to_account_info(),
248+
ApproveTransfer {
249+
payer: ctx.accounts.payer.to_account_info(),
250+
authority: ctx.accounts.buyer.to_account_info(),
251+
payment_mint: ctx.accounts.payment_mint.to_account_info(),
252+
mint: ctx.accounts.mint.to_account_info(),
253+
distribution_account: ctx.accounts.distribution.to_account_info(),
254+
authority_token_account: ctx.accounts.buyer_payment_token_account.to_account_info(),
255+
distribution_token_account: ctx
256+
.accounts
257+
.distribution_payment_token_account
258+
.to_account_info(),
259+
approve_account: ctx.accounts.approve_account.to_account_info(),
260+
distribution_program: ctx.accounts.distribution_program.to_account_info(),
261+
associated_token_program: ctx.accounts.associated_token_program.to_account_info(),
262+
token_program: ctx.accounts.token_program.to_account_info(),
263+
system_program: ctx.accounts.system_program.to_account_info(),
264+
},
265+
),
266+
args.buy_amount,
267+
)?;
268+
269+
// // Transfer NFT to buyer
270+
transfer_checked_with_hook(
271+
CpiContext::new_with_signer(
272+
ctx.accounts.token_program.to_account_info(),
273+
TransferCheckedWithHook {
274+
authority: ctx.accounts.sale.to_account_info(),
275+
mint: ctx.accounts.mint.to_account_info(),
276+
from: ctx.accounts.seller_token_account.to_account_info(),
277+
to: ctx.accounts.buyer_token_account.to_account_info(),
278+
extra_metas_account: ctx.accounts.extra_metas_account.to_account_info(),
279+
approve_account: ctx.accounts.approve_account.to_account_info(),
280+
wns_program: ctx.accounts.wns_program.to_account_info(),
281+
},
282+
signer_seeds,
283+
),
284+
1,
285+
0,
286+
)?;
287+
288+
// Close listing
289+
listing.close(ctx.accounts.payer.to_account_info())?;
290+
291+
Ok(())
292+
}
293+
294+
#[derive(AnchorSerialize, AnchorDeserialize)]
295+
pub struct FulfillListingArgs {
296+
pub buy_amount: u64,
297+
}

0 commit comments

Comments
 (0)