Skip to content

Commit 5e7ebdb

Browse files
committed
feat(v2): Accept Into<Address> in address = <expr>
1 parent 76d8780 commit 5e7ebdb

3 files changed

Lines changed: 109 additions & 2 deletions

File tree

lang-v2/derive/src/parse.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,8 +1334,14 @@ pub fn parse_field(
13341334
};
13351335
constraints.push(quote! {
13361336
{
1337-
// Bind the expected address to a local for `address_eq`.
1338-
let __expected: anchor_lang_v2::Address = #addr;
1337+
// Accept any `T: Into<Address>` on the RHS — `Address`
1338+
// itself goes through the blanket `From<T> for T`,
1339+
// `&Address`, `[u8; 32]`, and any user-defined wrapper
1340+
// with an `Into<Address>` impl all flow through the
1341+
// same conversion. Still binds to a local first so
1342+
// `address_eq` sees a stable reference.
1343+
let __expected: anchor_lang_v2::Address =
1344+
core::convert::Into::into(#addr);
13391345
if !anchor_lang_v2::address_eq(#field_name.account().address(), &__expected) {
13401346
return Err(#err);
13411347
}

tests-v2/programs/constraints/src/lib.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,22 @@ pub mod constraints {
167167
pub fn check_multiple_constraints(_ctx: &mut Context<CheckMultipleConstraints>) -> Result<()> {
168168
Ok(())
169169
}
170+
171+
/// `address = <expr>` accepting an `Into<Address>` RHS. The fixture
172+
/// uses `[u8; 32]` (which has `From<[u8; 32]> for Address`) and a
173+
/// helper returning `&Address` (`From<&Address> for Address`) — both
174+
/// would have failed the prior `let __expected: Address = #expr`
175+
/// type-ascription path.
176+
#[discrim = 17]
177+
pub fn check_address_into(_ctx: &mut Context<CheckAddressInto>) -> Result<()> {
178+
Ok(())
179+
}
180+
181+
/// Same but feeding `address` from a function returning `&Address`.
182+
#[discrim = 18]
183+
pub fn check_address_into_ref(_ctx: &mut Context<CheckAddressIntoRef>) -> Result<()> {
184+
Ok(())
185+
}
170186
}
171187

172188
// -- Accounts structs --------------------------------------------------------
@@ -309,11 +325,36 @@ pub struct CheckSigner {
309325
pub user: UncheckedAccount,
310326
}
311327

328+
/// `[u8; 32]` form of `PINNED_ADDRESS`. Used in the `Into<Address>` test
329+
/// fixture so the RHS goes through `From<[u8; 32]> for Address` rather
330+
/// than the trivial identity coercion.
331+
pub const PINNED_BYTES: [u8; 32] = PINNED_ADDRESS.to_bytes();
332+
333+
/// Helper returning `&Address`. Exercises `From<&Address> for Address` on
334+
/// the constraint RHS — different impl from the `[u8; 32]` form.
335+
pub fn pinned_address_ref() -> &'static Address {
336+
&PINNED_ADDRESS
337+
}
338+
312339
// 16. Multiple `constraint`s on a single field, mixing both spellings.
313340
//
314341
// Order of evaluation matters: the first failing check is the one whose
315342
// error surfaces. The integration tests trip each entry in turn to
316343
// confirm that.
344+
// 17. address = <[u8; 32] expr> — RHS converts via `Into<Address>`.
345+
#[derive(Accounts)]
346+
pub struct CheckAddressInto {
347+
#[account(address = PINNED_BYTES)]
348+
pub pinned: UncheckedAccount,
349+
}
350+
351+
// 18. address = <&Address expr> — RHS converts via `Into<Address>`.
352+
#[derive(Accounts)]
353+
pub struct CheckAddressIntoRef {
354+
#[account(address = pinned_address_ref())]
355+
pub pinned: UncheckedAccount,
356+
}
357+
317358
#[derive(Accounts)]
318359
pub struct CheckMultipleConstraints {
319360
pub a: UncheckedAccount,

tests-v2/tests/constraints.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,66 @@ fn signer_on_unchecked_ok_when_signed() {
703703
.expect("signed");
704704
}
705705

706+
// ---- 17/18. address = <Into<Address> expr> -------------------------------
707+
//
708+
// Asserts the constraint accepts any `Into<Address>` RHS, not just an
709+
// `Address` literal. Two fixtures cover the two non-trivial impls in
710+
// `solana-address`:
711+
// - `From<[u8; 32]> for Address` — `address = SOME_BYTES`
712+
// - `From<&Address> for Address` — `address = some_fn_returning_ref()`
713+
714+
#[test]
715+
fn address_into_from_byte_array_match_ok() {
716+
let (mut svm, payer, _) = setup();
717+
call(
718+
&mut svm,
719+
&payer,
720+
17,
721+
vec![AccountMeta::new_readonly(pinned_address(), false)],
722+
&[],
723+
)
724+
.expect("address from [u8; 32]");
725+
}
726+
727+
#[test]
728+
fn address_into_from_byte_array_mismatch_rejected() {
729+
let (mut svm, payer, _) = setup();
730+
let result = call_raw(
731+
&mut svm,
732+
&payer,
733+
17,
734+
vec![AccountMeta::new_readonly(Pubkey::new_unique(), false)],
735+
&[],
736+
);
737+
assert_err_contains(&result, "InvalidAccountData");
738+
}
739+
740+
#[test]
741+
fn address_into_from_ref_match_ok() {
742+
let (mut svm, payer, _) = setup();
743+
call(
744+
&mut svm,
745+
&payer,
746+
18,
747+
vec![AccountMeta::new_readonly(pinned_address(), false)],
748+
&[],
749+
)
750+
.expect("address from &Address");
751+
}
752+
753+
#[test]
754+
fn address_into_from_ref_mismatch_rejected() {
755+
let (mut svm, payer, _) = setup();
756+
let result = call_raw(
757+
&mut svm,
758+
&payer,
759+
18,
760+
vec![AccountMeta::new_readonly(Pubkey::new_unique(), false)],
761+
&[],
762+
);
763+
assert_err_contains(&result, "InvalidAccountData");
764+
}
765+
706766
// ---- 16. Multiple constraints on a single field --------------------------
707767
//
708768
// `CheckMultipleConstraints` carries three `constraint`s on its `c` field,

0 commit comments

Comments
 (0)