Skip to content

Commit 771262c

Browse files
committed
feat: update with-nft to support list of identifiers
1 parent 204e848 commit 771262c

File tree

6 files changed

+45
-30
lines changed

6 files changed

+45
-30
lines changed

clarity-types/src/errors/analysis.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ pub enum CheckErrors {
314314
ExpectedAllowanceExpr(String),
315315
WithAllAllowanceNotAllowed,
316316
WithAllAllowanceNotAlone,
317+
WithNftExpectedListOfIdentifiers,
318+
MaxIdentifierLengthExceeded(u32),
317319
}
318320

319321
#[derive(Debug, PartialEq)]
@@ -617,6 +619,8 @@ impl DiagnosableError for CheckErrors {
617619
CheckErrors::ExpectedAllowanceExpr(got_name) => format!("expected an allowance expression, got: {got_name}"),
618620
CheckErrors::WithAllAllowanceNotAllowed => "with-all-assets-unsafe is not allowed here, only in the allowance list for `as-contract?`".into(),
619621
CheckErrors::WithAllAllowanceNotAlone => "with-all-assets-unsafe must not be used along with other allowances".into(),
622+
CheckErrors::WithNftExpectedListOfIdentifiers => "with-nft allowance must include a list of asset identifiers".into(),
623+
CheckErrors::MaxIdentifierLengthExceeded(max_len) => format!("with-nft allowance identifiers list must not exceed 128 elements, got {max_len}"),
620624
}
621625
}
622626

clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use clarity_types::errors::analysis::{check_argument_count, check_arguments_at_l
1717
use clarity_types::errors::{CheckError, CheckErrors};
1818
use clarity_types::representations::SymbolicExpression;
1919
use clarity_types::types::signatures::ASCII_128;
20-
use clarity_types::types::TypeSignature;
20+
use clarity_types::types::{SequenceSubtype, TypeSignature};
2121

2222
use crate::vm::analysis::type_checker::contexts::TypingContext;
2323
use crate::vm::analysis::type_checker::v2_1::TypeChecker;
@@ -202,7 +202,15 @@ fn check_allowance_with_nft(
202202

203203
checker.type_check_expects(&args[0], context, &TypeSignature::PrincipalType)?;
204204
checker.type_check_expects(&args[1], context, &ASCII_128)?;
205-
// Asset ID can be any type
205+
206+
// Asset identifiers must be a Clarity list with any type of elements
207+
let id_list_ty = checker.type_check(&args[2], context)?;
208+
let TypeSignature::SequenceType(SequenceSubtype::ListType(list_data)) = id_list_ty else {
209+
return Err(CheckErrors::WithNftExpectedListOfIdentifiers.into());
210+
};
211+
if list_data.get_max_len() > 128 {
212+
return Err(CheckErrors::MaxIdentifierLengthExceeded(list_data.get_max_len()).into());
213+
}
206214

207215
Ok(false)
208216
}

clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch
199199
),
200200
// multiple allowances
201201
(
202-
"(as-contract? ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" 0x01) (with-stacking u1000)) true)",
202+
"(as-contract? ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" (list 0x01)) (with-stacking u1000)) true)",
203203
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap()
204204
),
205205
// multiple body expressions
@@ -551,47 +551,47 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac
551551
let good = [
552552
// basic usage with shortcut contract principal
553553
(
554-
r#"(restrict-assets? tx-sender ((with-nft .token "token-name" u1000)) true)"#,
554+
r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list u1000))) true)"#,
555555
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(),
556556
),
557557
// full literal principal
558558
(
559-
r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" u1000)) true)"#,
559+
r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" (list u1000))) true)"#,
560560
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(),
561561
),
562562
// variable principal
563563
(
564-
r#"(let ((contract .token)) (restrict-assets? tx-sender ((with-nft contract "token-name" u1000)) true))"#,
564+
r#"(let ((contract .token)) (restrict-assets? tx-sender ((with-nft contract "token-name" (list u1000))) true))"#,
565565
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(),
566566
),
567567
// variable token name
568568
(
569-
r#"(let ((name "token-name")) (restrict-assets? tx-sender ((with-nft .token name u1000)) true))"#,
569+
r#"(let ((name "token-name")) (restrict-assets? tx-sender ((with-nft .token name (list u1000))) true))"#,
570570
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(),
571571
),
572572
// "*" token name
573573
(
574-
r#"(restrict-assets? tx-sender ((with-nft .token "*" u1000)) true)"#,
574+
r#"(restrict-assets? tx-sender ((with-nft .token "*" (list u1000))) true)"#,
575575
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(),
576576
),
577577
// empty token name
578578
(
579-
r#"(restrict-assets? tx-sender ((with-nft .token "" u1000)) true)"#,
579+
r#"(restrict-assets? tx-sender ((with-nft .token "" (list u1000))) true)"#,
580580
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(),
581581
),
582582
// string asset-id
583583
(
584-
r#"(restrict-assets? tx-sender ((with-nft .token "token-name" "asset-123")) true)"#,
584+
r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list "asset-123"))) true)"#,
585585
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(),
586586
),
587587
// buffer asset-id
588588
(
589-
r#"(restrict-assets? tx-sender ((with-nft .token "token-name" 0x0123456789)) true)"#,
589+
r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list 0x0123456789))) true)"#,
590590
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(),
591591
),
592592
// variable asset-id
593593
(
594-
r#"(let ((asset-id u123)) (restrict-assets? tx-sender ((with-nft .token "token-name" asset-id)) true))"#,
594+
r#"(let ((asset-id (list u123))) (restrict-assets? tx-sender ((with-nft .token "token-name" asset-id)) true))"#,
595595
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(),
596596
),
597597
];
@@ -614,20 +614,20 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac
614614
),
615615
// too many arguments
616616
(
617-
r#"(restrict-assets? tx-sender ((with-nft .token "token-name" u123 u456)) true)"#,
617+
r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list u123) (list u456))) true)"#,
618618
CheckErrors::IncorrectArgumentCount(3, 4),
619619
),
620620
// wrong type for contract-id - uint instead of principal
621621
(
622-
r#"(restrict-assets? tx-sender ((with-nft u123 "token-name" u456)) true)"#,
622+
r#"(restrict-assets? tx-sender ((with-nft u123 "token-name" (list u456))) true)"#,
623623
CheckErrors::TypeError(
624624
TypeSignature::PrincipalType.into(),
625625
TypeSignature::UIntType.into(),
626626
),
627627
),
628628
// wrong type for token-name - uint instead of string
629629
(
630-
"(restrict-assets? tx-sender ((with-nft .token u123 u456)) true)",
630+
"(restrict-assets? tx-sender ((with-nft .token u123 (list u456))) true)",
631631
CheckErrors::TypeError(
632632
TypeSignature::new_string_ascii(128).unwrap().into(),
633633
TypeSignature::UIntType.into(),

clarity/src/vm/docs/mod.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2673,12 +2673,12 @@ the contract. When `"*"` is used for the token name, the allowance applies to
26732673
};
26742674

26752675
const ALLOWANCE_WITH_NFT: SpecialAPI = SpecialAPI {
2676-
input_type: "principal (string-ascii 128) T",
2677-
snippet: "with-nft ${1:contract-id} ${2:asset-name} ${3:asset-identifier}",
2676+
input_type: "principal (string-ascii 128) (list 128 T)",
2677+
snippet: "with-nft ${1:contract-id} ${2:asset-name} ${3:asset-identifiers}",
26782678
output_type: "Allowance",
2679-
signature: "(with-nft contract-id asset-name identifier)",
2680-
description: r#"Adds an outflow allowance for the non-fungible token
2681-
identified by `identifier` defined in `contract-id` with name `token-name`
2679+
signature: "(with-nft contract-id asset-name identifiers)",
2680+
description: r#"Adds an outflow allowance for the non-fungible tokens
2681+
identified by `identifiers` defined in `contract-id` with name `token-name`
26822682
from the `asset-owner` of the enclosing `restrict-assets?` or `as-contract?`
26832683
expression. `with-nft` is not allowed outside of `restrict-assets?` or
26842684
`as-contract?` contexts. Note that `token-name` should match the name used in
@@ -2690,11 +2690,11 @@ the token name, the allowance applies to **all** NFTs defined in `contract-id`."
26902690
(nft-mint? stackaroo u124 tx-sender)
26912691
(nft-mint? stackaroo u125 tx-sender)
26922692
(restrict-assets? tx-sender
2693-
((with-nft current-contract "stackaroo" u123))
2693+
((with-nft current-contract "stackaroo" (list u123)))
26942694
(try! (nft-transfer? stackaroo u123 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF))
26952695
) ;; Returns (ok true)
26962696
(restrict-assets? tx-sender
2697-
((with-nft current-contract "stackaroo" u125))
2697+
((with-nft current-contract "stackaroo" (list u125)))
26982698
(try! (nft-transfer? stackaroo u124 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF))
26992699
) ;; Returns (err 0)
27002700
"#,

clarity/src/vm/functions/post_conditions.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub struct FtAllowance {
3838

3939
pub struct NftAllowance {
4040
asset: AssetIdentifier,
41-
asset_id: Value,
41+
asset_ids: Vec<Value>,
4242
}
4343

4444
pub struct StackingAllowance {
@@ -128,9 +128,10 @@ fn eval_allowance(
128128
asset_name,
129129
};
130130

131-
let asset_id = eval(&rest[2], env, context)?;
131+
let asset_id_list = eval(&rest[2], env, context)?;
132+
let asset_ids = asset_id_list.expect_list()?;
132133

133-
Ok(Allowance::Nft(NftAllowance { asset, asset_id }))
134+
Ok(Allowance::Nft(NftAllowance { asset, asset_ids }))
134135
}
135136
"with-stacking" => {
136137
if rest.len() != 1 {
@@ -325,7 +326,9 @@ fn check_allowances(
325326
let (_, set) = nft_allowances
326327
.entry(&nft.asset)
327328
.or_insert_with(|| (i, HashSet::new()));
328-
set.insert(nft.asset_id.serialize_to_hex()?);
329+
for id in &nft.asset_ids {
330+
set.insert(id.serialize_to_hex()?);
331+
}
329332
}
330333
Allowance::Stacking(stacking) => {
331334
stacking_allowances.push((i, stacking.amount));

clarity/src/vm/tests/post_conditions.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ fn test_as_contract_stx_all() {
105105
fn test_as_contract_stx_other_allowances() {
106106
let snippet = r#"
107107
(let ((recipient tx-sender))
108-
(as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123))
108+
(as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123)))
109109
(try! (stx-transfer? u50 tx-sender recipient))
110110
)
111111
)"#;
@@ -156,7 +156,7 @@ fn test_as_contract_stx_burn_all() {
156156
#[test]
157157
fn test_as_contract_stx_burn_other_allowances() {
158158
let snippet = r#"
159-
(as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123))
159+
(as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123)))
160160
(try! (stx-burn? u50 tx-sender))
161161
)"#;
162162
let expected = Value::error(Value::Int(-1)).unwrap();
@@ -280,7 +280,7 @@ fn test_restrict_assets_stx_all() {
280280
#[test]
281281
fn test_restrict_assets_stx_other_allowances() {
282282
let snippet = r#"
283-
(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123))
283+
(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123)))
284284
(try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78))
285285
)"#;
286286
let expected = Value::error(Value::Int(-1)).unwrap();
@@ -330,7 +330,7 @@ fn test_restrict_assets_stx_burn_all() {
330330
#[test]
331331
fn test_restrict_assets_stx_burn_other_allowances() {
332332
let snippet = r#"
333-
(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123))
333+
(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123)))
334334
(try! (stx-burn? u50 tx-sender))
335335
)"#;
336336
let expected = Value::error(Value::Int(-1)).unwrap();

0 commit comments

Comments
 (0)