phygital-token-sdk 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +94 -0
  2. package/dist/index.d.ts +6 -8
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -8
  5. package/dist/index.js.map +1 -1
  6. package/dist/instructions/transfer.d.ts +3 -11
  7. package/dist/instructions/transfer.d.ts.map +1 -1
  8. package/dist/instructions/transfer.js +10 -32
  9. package/dist/instructions/transfer.js.map +1 -1
  10. package/dist/instructions/verifyAsset.d.ts +14 -9
  11. package/dist/instructions/verifyAsset.d.ts.map +1 -1
  12. package/dist/instructions/verifyAsset.js +44 -19
  13. package/dist/instructions/verifyAsset.js.map +1 -1
  14. package/dist/utils/assetCredential.d.ts +1 -1
  15. package/dist/utils/assetCredential.d.ts.map +1 -1
  16. package/dist/utils/assetCredential.js +2 -2
  17. package/dist/utils/assetCredential.js.map +1 -1
  18. package/dist/utils/consts.d.ts +1 -1
  19. package/dist/utils/consts.d.ts.map +1 -1
  20. package/dist/utils/consts.js +1 -1
  21. package/dist/utils/consts.js.map +1 -1
  22. package/dist/utils/gating.d.ts +195 -0
  23. package/dist/utils/gating.d.ts.map +1 -0
  24. package/dist/utils/gating.js +481 -0
  25. package/dist/utils/gating.js.map +1 -0
  26. package/dist/utils/passkey/nfc/authDataExtensions.d.ts.map +1 -1
  27. package/dist/utils/passkey/nfc/authDataExtensions.js +2 -2
  28. package/dist/utils/passkey/nfc/authDataExtensions.js.map +1 -1
  29. package/dist/utils/passkey/secp256r1.d.ts +9 -6
  30. package/dist/utils/passkey/secp256r1.d.ts.map +1 -1
  31. package/dist/utils/passkey/secp256r1.js +14 -23
  32. package/dist/utils/passkey/secp256r1.js.map +1 -1
  33. package/dist/utils/slotHash.d.ts +1 -2
  34. package/dist/utils/slotHash.d.ts.map +1 -1
  35. package/dist/utils/slotHash.js +0 -12
  36. package/dist/utils/slotHash.js.map +1 -1
  37. package/dist/utils/verify.d.ts +2 -9
  38. package/dist/utils/verify.d.ts.map +1 -1
  39. package/dist/utils/verify.js +12 -104
  40. package/dist/utils/verify.js.map +1 -1
  41. package/docs/gating/README.md +83 -0
  42. package/docs/gating/evaluation-and-errors.md +134 -0
  43. package/docs/gating/filters-and-composition.md +120 -0
  44. package/docs/gating/overview.md +84 -0
  45. package/docs/gating/predicates.md +122 -0
  46. package/docs/gating/recipes.md +199 -0
  47. package/docs/gating/tiers.md +158 -0
  48. package/package.json +11 -10
  49. package/dist/__tests__/card-instance.test.d.ts +0 -2
  50. package/dist/__tests__/card-instance.test.d.ts.map +0 -1
  51. package/dist/__tests__/card-instance.test.js +0 -38
  52. package/dist/__tests__/card-instance.test.js.map +0 -1
  53. package/dist/__tests__/crypto-parity.test.d.ts +0 -2
  54. package/dist/__tests__/crypto-parity.test.d.ts.map +0 -1
  55. package/dist/__tests__/crypto-parity.test.js +0 -60
  56. package/dist/__tests__/crypto-parity.test.js.map +0 -1
  57. package/dist/__tests__/low-s-normalization.test.d.ts +0 -2
  58. package/dist/__tests__/low-s-normalization.test.d.ts.map +0 -1
  59. package/dist/__tests__/low-s-normalization.test.js +0 -44
  60. package/dist/__tests__/low-s-normalization.test.js.map +0 -1
  61. package/dist/__tests__/metadata-limits.test.d.ts +0 -2
  62. package/dist/__tests__/metadata-limits.test.d.ts.map +0 -1
  63. package/dist/__tests__/metadata-limits.test.js +0 -33
  64. package/dist/__tests__/metadata-limits.test.js.map +0 -1
  65. package/dist/__tests__/nfc-cbor.test.d.ts +0 -2
  66. package/dist/__tests__/nfc-cbor.test.d.ts.map +0 -1
  67. package/dist/__tests__/nfc-cbor.test.js +0 -72
  68. package/dist/__tests__/nfc-cbor.test.js.map +0 -1
  69. package/dist/consts.d.ts +0 -19
  70. package/dist/consts.d.ts.map +0 -1
  71. package/dist/consts.js +0 -19
  72. package/dist/consts.js.map +0 -1
  73. package/dist/generated/accounts/cardInstance.d.ts +0 -35
  74. package/dist/generated/accounts/cardInstance.d.ts.map +0 -1
  75. package/dist/generated/accounts/cardInstance.js +0 -61
  76. package/dist/generated/accounts/cardInstance.js.map +0 -1
  77. package/dist/generated/accounts/domainConfig.d.ts +0 -28
  78. package/dist/generated/accounts/domainConfig.d.ts.map +0 -1
  79. package/dist/generated/accounts/domainConfig.js +0 -58
  80. package/dist/generated/accounts/domainConfig.js.map +0 -1
  81. package/dist/generated/errors/phygitalNfts.d.ts +0 -62
  82. package/dist/generated/errors/phygitalNfts.d.ts.map +0 -1
  83. package/dist/generated/errors/phygitalNfts.js +0 -87
  84. package/dist/generated/errors/phygitalNfts.js.map +0 -1
  85. package/dist/generated/instructions/createCollectionMint.d.ts +0 -108
  86. package/dist/generated/instructions/createCollectionMint.d.ts.map +0 -1
  87. package/dist/generated/instructions/createCollectionMint.js +0 -175
  88. package/dist/generated/instructions/createCollectionMint.js.map +0 -1
  89. package/dist/generated/instructions/createDesignMint.d.ts +0 -97
  90. package/dist/generated/instructions/createDesignMint.d.ts.map +0 -1
  91. package/dist/generated/instructions/createDesignMint.js +0 -164
  92. package/dist/generated/instructions/createDesignMint.js.map +0 -1
  93. package/dist/generated/instructions/createDomainConfig.d.ts +0 -46
  94. package/dist/generated/instructions/createDomainConfig.d.ts.map +0 -1
  95. package/dist/generated/instructions/createDomainConfig.js +0 -83
  96. package/dist/generated/instructions/createDomainConfig.js.map +0 -1
  97. package/dist/generated/instructions/createGroupToken.d.ts +0 -94
  98. package/dist/generated/instructions/createGroupToken.d.ts.map +0 -1
  99. package/dist/generated/instructions/createGroupToken.js +0 -150
  100. package/dist/generated/instructions/createGroupToken.js.map +0 -1
  101. package/dist/generated/instructions/createToken.d.ts +0 -88
  102. package/dist/generated/instructions/createToken.d.ts.map +0 -1
  103. package/dist/generated/instructions/createToken.js +0 -182
  104. package/dist/generated/instructions/createToken.js.map +0 -1
  105. package/dist/generated/instructions/editDomainConfig.d.ts +0 -50
  106. package/dist/generated/instructions/editDomainConfig.d.ts.map +0 -1
  107. package/dist/generated/instructions/editDomainConfig.js +0 -90
  108. package/dist/generated/instructions/editDomainConfig.js.map +0 -1
  109. package/dist/generated/instructions/executeSpend.d.ts +0 -86
  110. package/dist/generated/instructions/executeSpend.d.ts.map +0 -1
  111. package/dist/generated/instructions/executeSpend.js +0 -187
  112. package/dist/generated/instructions/executeSpend.js.map +0 -1
  113. package/dist/generated/instructions/setTransferConfig.d.ts +0 -88
  114. package/dist/generated/instructions/setTransferConfig.d.ts.map +0 -1
  115. package/dist/generated/instructions/setTransferConfig.js +0 -188
  116. package/dist/generated/instructions/setTransferConfig.js.map +0 -1
  117. package/dist/generated/instructions/updateCounter.d.ts +0 -45
  118. package/dist/generated/instructions/updateCounter.d.ts.map +0 -1
  119. package/dist/generated/instructions/updateCounter.js +0 -84
  120. package/dist/generated/instructions/updateCounter.js.map +0 -1
  121. package/dist/generated/instructions/updateDomainConfig.d.ts +0 -43
  122. package/dist/generated/instructions/updateDomainConfig.d.ts.map +0 -1
  123. package/dist/generated/instructions/updateDomainConfig.js +0 -81
  124. package/dist/generated/instructions/updateDomainConfig.js.map +0 -1
  125. package/dist/generated/pdas/spendAuthority.d.ts +0 -8
  126. package/dist/generated/pdas/spendAuthority.d.ts.map +0 -1
  127. package/dist/generated/pdas/spendAuthority.js +0 -15
  128. package/dist/generated/pdas/spendAuthority.js.map +0 -1
  129. package/dist/generated/programs/phygitalNfts.d.ts +0 -72
  130. package/dist/generated/programs/phygitalNfts.d.ts.map +0 -1
  131. package/dist/generated/programs/phygitalNfts.js +0 -136
  132. package/dist/generated/programs/phygitalNfts.js.map +0 -1
  133. package/dist/instructions/collection.d.ts +0 -14
  134. package/dist/instructions/collection.d.ts.map +0 -1
  135. package/dist/instructions/collection.js +0 -59
  136. package/dist/instructions/collection.js.map +0 -1
  137. package/dist/instructions/index.d.ts +0 -2
  138. package/dist/instructions/index.d.ts.map +0 -1
  139. package/dist/instructions/index.js +0 -2
  140. package/dist/instructions/index.js.map +0 -1
  141. package/dist/instructions/secp256r1Verify.d.ts +0 -29
  142. package/dist/instructions/secp256r1Verify.d.ts.map +0 -1
  143. package/dist/instructions/secp256r1Verify.js +0 -132
  144. package/dist/instructions/secp256r1Verify.js.map +0 -1
  145. package/dist/instructions/setLockState.d.ts +0 -19
  146. package/dist/instructions/setLockState.d.ts.map +0 -1
  147. package/dist/instructions/setLockState.js +0 -17
  148. package/dist/instructions/setLockState.js.map +0 -1
  149. package/dist/instructions/setTransferConfig.d.ts +0 -25
  150. package/dist/instructions/setTransferConfig.d.ts.map +0 -1
  151. package/dist/instructions/setTransferConfig.js +0 -20
  152. package/dist/instructions/setTransferConfig.js.map +0 -1
  153. package/dist/instructions/updateCounter.d.ts +0 -15
  154. package/dist/instructions/updateCounter.d.ts.map +0 -1
  155. package/dist/instructions/updateCounter.js +0 -20
  156. package/dist/instructions/updateCounter.js.map +0 -1
  157. package/dist/mint-metadata.d.ts +0 -13
  158. package/dist/mint-metadata.d.ts.map +0 -1
  159. package/dist/mint-metadata.js +0 -127
  160. package/dist/mint-metadata.js.map +0 -1
  161. package/dist/passkeys/index.d.ts +0 -18
  162. package/dist/passkeys/index.d.ts.map +0 -1
  163. package/dist/passkeys/index.js +0 -30
  164. package/dist/passkeys/index.js.map +0 -1
  165. package/dist/passkeys/internal.d.ts +0 -15
  166. package/dist/passkeys/internal.d.ts.map +0 -1
  167. package/dist/passkeys/internal.js +0 -113
  168. package/dist/passkeys/internal.js.map +0 -1
  169. package/dist/secp256r1.d.ts +0 -24
  170. package/dist/secp256r1.d.ts.map +0 -1
  171. package/dist/secp256r1.js +0 -38
  172. package/dist/secp256r1.js.map +0 -1
  173. package/dist/transfer.d.ts +0 -28
  174. package/dist/transfer.d.ts.map +0 -1
  175. package/dist/transfer.js +0 -109
  176. package/dist/transfer.js.map +0 -1
  177. package/dist/utils/encoding.d.ts +0 -3
  178. package/dist/utils/encoding.d.ts.map +0 -1
  179. package/dist/utils/encoding.js +0 -14
  180. package/dist/utils/encoding.js.map +0 -1
  181. package/dist/utils/passkey/index.d.ts +0 -17
  182. package/dist/utils/passkey/index.d.ts.map +0 -1
  183. package/dist/utils/passkey/index.js +0 -21
  184. package/dist/utils/passkey/index.js.map +0 -1
  185. package/dist/utils/passkey/nfc/base64url.d.ts +0 -2
  186. package/dist/utils/passkey/nfc/base64url.d.ts.map +0 -1
  187. package/dist/utils/passkey/nfc/base64url.js +0 -13
  188. package/dist/utils/passkey/nfc/base64url.js.map +0 -1
  189. package/dist/utils/pdas/domainConfig.d.ts +0 -3
  190. package/dist/utils/pdas/domainConfig.d.ts.map +0 -1
  191. package/dist/utils/pdas/domainConfig.js +0 -15
  192. package/dist/utils/pdas/domainConfig.js.map +0 -1
  193. package/dist/utils/sendInstructions.d.ts +0 -7
  194. package/dist/utils/sendInstructions.d.ts.map +0 -1
  195. package/dist/utils/sendInstructions.js +0 -13
  196. package/dist/utils/sendInstructions.js.map +0 -1
  197. package/dist/utils/tokenOwner.d.ts +0 -7
  198. package/dist/utils/tokenOwner.d.ts.map +0 -1
  199. package/dist/utils/tokenOwner.js +0 -24
  200. package/dist/utils/tokenOwner.js.map +0 -1
  201. package/dist/utils.d.ts +0 -10
  202. package/dist/utils.d.ts.map +0 -1
  203. package/dist/utils.js +0 -42
  204. package/dist/utils.js.map +0 -1
@@ -0,0 +1,134 @@
1
+ # Evaluation & errors
2
+
3
+ ## Entry points
4
+
5
+ | Function | When to use |
6
+ |----------|-------------|
7
+ | `evaluateAssetGating` | Production — resolves owner, fetches DAS assets, evaluates tiers |
8
+ | `evaluateGatingTiers` | You already have the owner's DAS asset list |
9
+ | `evaluateGatingFilter` | You already have assets and a single filter tree |
10
+ | `assetMatchesPredicate` | Test one asset row against a predicate |
11
+
12
+ ## `evaluateAssetGating` flow
13
+
14
+ ```
15
+ assetPublicKey → find asset PDA → read owner
16
+ owner → searchAssets (paginated) → DasAsset[]
17
+ DasAsset[] + tiers → evaluateGatingTiers → GatingEvaluationResult
18
+ ```
19
+
20
+ ## Filter result tree
21
+
22
+ Every evaluation returns a structured `filterResult` (per tier) you can inspect:
23
+
24
+ ```ts
25
+ const gold = result.tiers.find((t) => t.id === "gold");
26
+ const tree = gold?.filterResult;
27
+
28
+ // tree.kind: "count" | "totalBalance" | "and" | "or" | "not"
29
+ // tree.passed: boolean
30
+
31
+ if (tree?.kind === "count") {
32
+ console.log(tree.matchCount); // how many assets matched
33
+ }
34
+
35
+ if (tree?.kind === "totalBalance") {
36
+ console.log(tree.total); // summed raw balance
37
+ }
38
+
39
+ if (tree?.kind === "and") {
40
+ console.log(tree.children); // per-child results
41
+ }
42
+ ```
43
+
44
+ Use this for admin dashboards, debug panels, or analytics.
45
+
46
+ ## Failure messages
47
+
48
+ Human-readable summaries when gating fails:
49
+
50
+ ```ts
51
+ import {
52
+ summarizeGatingEvaluationFailure,
53
+ summarizeGatingTierFailure,
54
+ summarizeGatingFailure,
55
+ } from "phygital-token-sdk";
56
+
57
+ // No tier passed — all tier failures
58
+ const reasons = summarizeGatingEvaluationFailure(result);
59
+ // [
60
+ // 'Tier "bronze":',
61
+ // ' Need at least 5 asset(s) matching collection = ...; found 2.',
62
+ // 'Tier "gold":',
63
+ // ' Need at least 9000000 raw balance for mint ...; found 1500000.',
64
+ // ]
65
+
66
+ // One tier
67
+ const gold = result.tiers.find((t) => t.id === "gold");
68
+ summarizeGatingTierFailure(gold!);
69
+
70
+ // Single filter tree (no tiers)
71
+ summarizeGatingFailure(filterResult);
72
+ ```
73
+
74
+ `summarizeGatingEvaluationFailure` returns `[]` when **any** tier passed. To explain why a specific tier failed even when another passed, use `summarizeGatingTierFailure` on that tier.
75
+
76
+ ### Predicate formatting
77
+
78
+ ```ts
79
+ import { formatGatingPredicate } from "phygital-token-sdk";
80
+
81
+ formatGatingPredicate({
82
+ collection: Gating.in("ColA", "ColB"),
83
+ traits: Gating.traitsAll(
84
+ Gating.trait("Level", GatingTraitValue.gte(5)),
85
+ ),
86
+ });
87
+ // "collection in (ColA, ColB), traits all [Level >= 5]"
88
+ ```
89
+
90
+ ## UX patterns
91
+
92
+ ### Show why access was denied
93
+
94
+ ```ts
95
+ if (!result.passed) {
96
+ const message = summarizeGatingEvaluationFailure(result).join("\n");
97
+ showError(message);
98
+ }
99
+ ```
100
+
101
+ ### Show progress toward next tier
102
+
103
+ ```ts
104
+ const silver = result.tiers.find((t) => t.id === "silver");
105
+ if (silver?.filterResult.kind === "count") {
106
+ const need = 3;
107
+ const have = silver.filterResult.matchCount;
108
+ showProgress(`Collection NFTs: ${have} / ${need}`);
109
+ }
110
+ ```
111
+
112
+ ## Common failure modes
113
+
114
+ | Symptom | Likely cause |
115
+ |---------|--------------|
116
+ | Always fails on traits | Traits split across NFTs — use one predicate with `traitsAll` |
117
+ | `count(1, …)` fails but user "has the NFT" | Predicate includes extra fields (`collection` + wrong `mint`) |
118
+ | Balance gate fails | Using UI amount instead of raw units (multiply by `10^decimals`) |
119
+ | Collection not detected | DAS grouping missing or non-standard collection key |
120
+ | Empty wallet | Owner has no indexed DAS assets yet |
121
+
122
+ ## Raw vs UI token amounts
123
+
124
+ `balance` and `totalBalance` use **raw** on-chain units. For a 6-decimal token:
125
+
126
+ | UI amount | Raw units |
127
+ |-----------|-----------|
128
+ | 1 token | `1_000_000n` |
129
+ | 1.5 tokens | `1_500_000n` |
130
+
131
+ ```ts
132
+ const RAW = 10n ** 6n;
133
+ Gating.totalBalance(USDC_MINT, 100n * RAW); // 100 USDC
134
+ ```
@@ -0,0 +1,120 @@
1
+ # Filters & composition
2
+
3
+ A `GatingFilter` is a composable tree evaluated against the owner's full wallet.
4
+
5
+ ## Leaf nodes
6
+
7
+ ### `Gating.count(min, predicate, max?)`
8
+
9
+ Counts owned assets matching the predicate. Passes when count is `≥ min` and (if set) `≤ max`.
10
+
11
+ ```ts
12
+ // At least one NFT from collection (existence)
13
+ Gating.count(1, { collection: Gating.eq("CollectionMint...") })
14
+
15
+ // At least 3 from collection
16
+ Gating.count(3, { collection: Gating.eq("CollectionMint...") })
17
+
18
+ // Exactly 1 Gold NFT from collection
19
+ Gating.count(1, {
20
+ collection: Gating.eq("CollectionMint..."),
21
+ traits: Gating.traitsAll(
22
+ Gating.trait("Rarity", GatingTraitValue.eq("Gold")),
23
+ ),
24
+ }, 1)
25
+
26
+ // Between 2 and 5 matching assets
27
+ Gating.count(2, { collection: Gating.eq("CollectionMint...") }, 5)
28
+ ```
29
+
30
+ `Gating.count(1, predicate)` is the standard existence check. There is no separate `match` API.
31
+
32
+ ### `Gating.totalBalance(mint, min?, max?)`
33
+
34
+ Sums **raw** balance for `mint` across all owned asset rows (wallet-wide):
35
+
36
+ ```ts
37
+ // At least 1 USDC (6 decimals → 1_000_000 raw units)
38
+ Gating.totalBalance("EPjFWdd5...", 1_000_000n)
39
+
40
+ // Between 1 and 10 USDC
41
+ Gating.totalBalance("EPjFWdd5...", 1_000_000n, 10_000_000n)
42
+ ```
43
+
44
+ Use `totalBalance` for fungible gates. Use `balance` on a predicate when checking a single token account row together with `mint`.
45
+
46
+ ## Compositors
47
+
48
+ ### `Gating.and(...filters)`
49
+
50
+ Every child must pass.
51
+
52
+ ```ts
53
+ Gating.and(
54
+ Gating.count(2, { collection: Gating.eq("Col...") }),
55
+ Gating.totalBalance("TokenMint...", 1_000_000n),
56
+ )
57
+ ```
58
+
59
+ ### `Gating.or(...filters)`
60
+
61
+ At least one child must pass.
62
+
63
+ ```ts
64
+ Gating.or(
65
+ Gating.count(1, { mint: Gating.eq("VIPPassMint...") }),
66
+ Gating.and(
67
+ Gating.count(3, { collection: Gating.eq("Col...") }),
68
+ Gating.totalBalance("RewardToken...", 100n),
69
+ ),
70
+ )
71
+ ```
72
+
73
+ ### `Gating.not(filter)`
74
+
75
+ Inverts a child. Passes when the inner filter **fails**.
76
+
77
+ ```ts
78
+ // Must NOT hold from banned collection
79
+ Gating.not(Gating.count(1, { collection: Gating.eq("BannedCol...") }))
80
+ ```
81
+
82
+ ## Example campaign rule
83
+
84
+ Require 2+ collection NFTs, 1 Gold with level, and enough reward tokens:
85
+
86
+ ```ts
87
+ const campaignFilter = Gating.and(
88
+ Gating.count(2, { collection: Gating.eq("CollectionMint...") }),
89
+ Gating.count(1, {
90
+ collection: Gating.eq("CollectionMint..."),
91
+ traits: Gating.traitsAll(
92
+ Gating.trait("Rarity", GatingTraitValue.eq("Gold")),
93
+ Gating.trait("Level", GatingTraitValue.gte(5)),
94
+ ),
95
+ }),
96
+ Gating.totalBalance("RewardTokenMint...", 1_000_000n),
97
+ );
98
+ ```
99
+
100
+ Wrap this in `Gating.tier("vip", campaignFilter)` when using multi-tier evaluation — see [Tiers](./tiers.md).
101
+
102
+ ## Evaluating without RPC
103
+
104
+ For unit tests or when you already have DAS assets:
105
+
106
+ ```ts
107
+ import { evaluateGatingFilter } from "phygital-token-sdk";
108
+
109
+ const result = evaluateGatingFilter(dasAssets, campaignFilter);
110
+ console.log(result.passed);
111
+ console.log(result.kind); // "and" | "count" | ...
112
+
113
+ if (result.kind === "count") {
114
+ console.log(result.matchCount);
115
+ }
116
+ ```
117
+
118
+ ## Conflicting rules
119
+
120
+ The evaluator does not detect impossible rules upfront. Unsatisfiable `and` trees return `passed: false`. See [Evaluation & errors](./evaluation-and-errors.md) for failure messages and common footguns.
@@ -0,0 +1,84 @@
1
+ # Overview
2
+
3
+ ## What gating does
4
+
5
+ 1. **Resolve owner** — looks up the on-chain owner of the phygital asset from its secp256r1 public key.
6
+ 2. **Load wallet** — paginates through DAS `searchAssets` for that owner (NFTs, compressed NFTs, fungible tokens).
7
+ 3. **Evaluate rules** — runs your filter tree(s) against the loaded assets.
8
+
9
+ Gating answers: *"Given what this person holds in their wallet, do they qualify?"*
10
+
11
+ ## Four dimensions
12
+
13
+ Every rule filters on up to four fields. When combined in a single predicate, **all set fields must match on the same asset row**:
14
+
15
+ | Dimension | Source (DAS) | Example |
16
+ |-----------|--------------|---------|
17
+ | `collection` | `grouping` where key is `collection` | Hold any NFT from a collection mint |
18
+ | `mint` | asset `id` | Hold a specific NFT or token mint |
19
+ | `traits` | `content.metadata.attributes` | Rarity = Gold, Level ≥ 5 |
20
+ | `balance` | `token_info.balance` on that row | Token account balance in raw units |
21
+
22
+ Wallet-level aggregations (`count`, `totalBalance`) sit on top of these per-asset predicates.
23
+
24
+ ## Mental model
25
+
26
+ ```
27
+ ┌─────────────────────────────────────────────────────────┐
28
+ │ evaluateAssetGating({ assetPublicKey, rpc, tiers }) │
29
+ └───────────────────────────┬─────────────────────────────┘
30
+
31
+ ┌───────────────▼───────────────┐
32
+ │ Owner wallet (DAS assets) │
33
+ └───────────────┬───────────────┘
34
+
35
+ ┌──────────────────┼──────────────────┐
36
+ ▼ ▼ ▼
37
+ Tier "bronze" Tier "silver" Tier "gold"
38
+ (filter tree) (filter tree) (filter tree)
39
+ │ │ │
40
+ └──────────────────┴──────────────────┘
41
+
42
+ passedTierIds
43
+ ```
44
+
45
+ Each **tier** has its own `GatingFilter` tree. Tiers are evaluated **independently** — a user can pass multiple tiers at once.
46
+
47
+ ## Filter tree layers
48
+
49
+ | Layer | API | SQL analogue |
50
+ |-------|-----|--------------|
51
+ | Per-asset predicate | `{ collection, mint, traits, balance }` | `WHERE` on one row |
52
+ | Count | `Gating.count(min, predicate, max?)` | `COUNT … HAVING` |
53
+ | Sum balance | `Gating.totalBalance(mint, min?, max?)` | `SUM(balance)` |
54
+ | Boolean | `Gating.and` / `or` / `not` | `AND` / `OR` / `NOT` |
55
+
56
+ Use `Gating.count(1, predicate)` for existence checks ("hold at least one asset matching …").
57
+
58
+ ## Cardinality cheat sheet
59
+
60
+ | Intent | API |
61
+ |--------|-----|
62
+ | At least 1 matching asset | `Gating.count(1, { ... })` |
63
+ | At least N matching assets | `Gating.count(N, { ... })` |
64
+ | Exactly 1 matching asset | `Gating.count(1, { ... }, 1)` |
65
+ | Between N and M matching assets | `Gating.count(N, { ... }, M)` |
66
+ | Wallet-wide token total | `Gating.totalBalance(mint, min?, max?)` |
67
+
68
+ ## Same asset vs same wallet
69
+
70
+ This is the most common source of bugs:
71
+
72
+ | Intent | Correct pattern |
73
+ |--------|-----------------|
74
+ | One NFT has Gold **and** Level ≥ 5 | Single predicate with `traits: Gating.traitsAll(...)` |
75
+ | Wallet holds mint A **and** mint B (any two assets) | `Gating.and(Gating.count(1, { mint: A }), Gating.count(1, { mint: B }))` |
76
+ | Wallet holds 3+ from collection | `Gating.count(3, { collection: ... })` |
77
+
78
+ See [Recipes](./recipes.md) for more patterns.
79
+
80
+ ## Next steps
81
+
82
+ - [Predicates](./predicates.md) — operators on collection, mint, traits, balance
83
+ - [Filters & composition](./filters-and-composition.md) — building filter trees
84
+ - [Tiers](./tiers.md) — assigning users to bronze / silver / gold
@@ -0,0 +1,122 @@
1
+ # Predicates
2
+
3
+ A **predicate** (`GatingAssetPredicate`) describes one owned asset row. Every field you set must match on the **same** asset.
4
+
5
+ ```ts
6
+ type GatingAssetPredicate = {
7
+ collection?: GatingStringOp;
8
+ mint?: GatingStringOp;
9
+ traits?: GatingTraits;
10
+ balance?: GatingBalance;
11
+ };
12
+ ```
13
+
14
+ Predicates are passed to `Gating.count` (and nowhere else at the leaf level). Use the `Gating` and `GatingTraitValue` builders — do not construct raw op objects unless you are serializing rules.
15
+
16
+ ## Collection & mint
17
+
18
+ String operators via `Gating.eq`, `Gating.neq`, `Gating.in`, `Gating.notIn`:
19
+
20
+ ```ts
21
+ // Exact collection
22
+ { collection: Gating.eq("CollectionMintABC...") }
23
+
24
+ // Any of several collections
25
+ { collection: Gating.in("ColA...", "ColB...") }
26
+
27
+ // Exclude a collection
28
+ { collection: Gating.notIn("BannedCol...") }
29
+
30
+ // Specific NFT mint
31
+ { mint: Gating.eq("NftMint...") }
32
+
33
+ // One of several mints
34
+ { mint: Gating.in("MintA...", "MintB...") }
35
+ ```
36
+
37
+ Collection is read from DAS `grouping` entries where the key is `"collection"`.
38
+
39
+ ## Traits (attributes)
40
+
41
+ Traits use `Gating.trait(trait_type, op)` with operators from `GatingTraitValue`:
42
+
43
+ | Operator | Builder | Example |
44
+ |----------|---------|---------|
45
+ | equals | `GatingTraitValue.eq(v)` | Rarity = Gold |
46
+ | not equals | `GatingTraitValue.neq(v)` | Rarity ≠ Common |
47
+ | in list | `GatingTraitValue.in(...)` | Rarity in (Gold, Platinum) |
48
+ | not in list | `GatingTraitValue.notIn(...)` | Rarity not in (Banned) |
49
+ | ≥ | `GatingTraitValue.gte(n)` | Level ≥ 5 |
50
+ | ≤ | `GatingTraitValue.lte(n)` | Level ≤ 10 |
51
+ | range | `GatingTraitValue.between(min, max)` | Level between 4 and 6 |
52
+
53
+ Combine traits with **all** or **any** on the same NFT:
54
+
55
+ ```ts
56
+ // Gold AND Level >= 5 on the SAME NFT
57
+ traits: Gating.traitsAll(
58
+ Gating.trait("Rarity", GatingTraitValue.eq("Gold")),
59
+ Gating.trait("Level", GatingTraitValue.gte(5)),
60
+ )
61
+
62
+ // Gold OR Platinum on the SAME NFT
63
+ traits: Gating.traitsAny(
64
+ Gating.trait("Rarity", GatingTraitValue.in("Gold", "Platinum")),
65
+ )
66
+ ```
67
+
68
+ ### Trait matching notes
69
+
70
+ - Trait names and string values are compared **case-insensitively** (trimmed).
71
+ - Numeric comparisons (`gte`, `lte`, `between`) parse string attribute values when possible (`"5"` → `5`).
72
+ - `neq` / `notIn` pass when the trait type is **absent** on the asset.
73
+
74
+ ## Balance
75
+
76
+ `Gating.balance(min?, max?)` filters the **raw** `token_info.balance` on that asset row (not UI decimals):
77
+
78
+ ```ts
79
+ // This token row has between 1M and 2M raw units
80
+ {
81
+ mint: Gating.eq("TokenMint..."),
82
+ balance: Gating.balance(1_000_000n, 2_000_000n),
83
+ }
84
+
85
+ // Minimum only
86
+ { balance: Gating.balance(1000n) }
87
+
88
+ // Maximum only
89
+ { balance: Gating.balance(undefined, 5000n) }
90
+ ```
91
+
92
+ For **wallet-wide** token totals (summing across accounts), use `Gating.totalBalance` instead — see [Filters & composition](./filters-and-composition.md).
93
+
94
+ NFT rows typically have no `token_info.balance`; balance checks on NFT-only predicates will fail unless the row is a fungible token.
95
+
96
+ ## Full predicate example
97
+
98
+ ```ts
99
+ Gating.count(1, {
100
+ collection: Gating.eq("CollectionMint..."),
101
+ mint: Gating.in("NftMint1...", "NftMint2..."),
102
+ traits: Gating.traitsAll(
103
+ Gating.trait("Rarity", GatingTraitValue.in("Gold", "Platinum")),
104
+ Gating.trait("Level", GatingTraitValue.between(5, 10)),
105
+ ),
106
+ balance: Gating.balance(0n), // optional; rarely needed on NFTs
107
+ })
108
+ ```
109
+
110
+ ## Debugging predicates
111
+
112
+ ```ts
113
+ import { formatGatingPredicate } from "phygital-token-sdk";
114
+
115
+ console.log(formatGatingPredicate({
116
+ collection: Gating.eq("Col..."),
117
+ traits: Gating.traitsAll(
118
+ Gating.trait("Rarity", GatingTraitValue.eq("Gold")),
119
+ ),
120
+ }));
121
+ // → "collection = Col..., traits all [Rarity = Gold]"
122
+ ```
@@ -0,0 +1,199 @@
1
+ # Recipes
2
+
3
+ Copy-paste patterns for common gating scenarios.
4
+
5
+ ## Hold any NFT from a collection
6
+
7
+ ```ts
8
+ Gating.count(1, { collection: Gating.eq("CollectionMint...") })
9
+ ```
10
+
11
+ ## Hold a specific NFT mint
12
+
13
+ ```ts
14
+ Gating.count(1, { mint: Gating.eq("NftMint...") })
15
+ ```
16
+
17
+ ## Hold N NFTs from a collection
18
+
19
+ ```ts
20
+ Gating.count(5, { collection: Gating.eq("CollectionMint...") })
21
+ ```
22
+
23
+ ## Gold NFT from collection (traits on same asset)
24
+
25
+ ```ts
26
+ Gating.count(1, {
27
+ collection: Gating.eq("CollectionMint..."),
28
+ traits: Gating.traitsAll(
29
+ Gating.trait("Rarity", GatingTraitValue.eq("Gold")),
30
+ ),
31
+ })
32
+ ```
33
+
34
+ ## Gold OR Platinum from collection
35
+
36
+ ```ts
37
+ Gating.count(1, {
38
+ collection: Gating.eq("CollectionMint..."),
39
+ traits: Gating.traitsAny(
40
+ Gating.trait("Rarity", GatingTraitValue.in("Gold", "Platinum")),
41
+ ),
42
+ })
43
+ ```
44
+
45
+ ## Minimum token balance (wallet-wide)
46
+
47
+ ```ts
48
+ Gating.totalBalance("TokenMint...", 1_000_000n)
49
+ ```
50
+
51
+ ## Hold NFT A and NFT B (different assets OK)
52
+
53
+ ```ts
54
+ Gating.and(
55
+ Gating.count(1, { mint: Gating.eq("MintA...") }),
56
+ Gating.count(1, { mint: Gating.eq("MintB...") }),
57
+ )
58
+ ```
59
+
60
+ ## VIP pass OR (collection holder + token balance)
61
+
62
+ ```ts
63
+ Gating.or(
64
+ Gating.count(1, { mint: Gating.eq("VIPPassMint...") }),
65
+ Gating.and(
66
+ Gating.count(1, { collection: Gating.eq("CollectionMint...") }),
67
+ Gating.totalBalance("TokenMint...", 5_000_000n),
68
+ ),
69
+ )
70
+ ```
71
+
72
+ ## Exclude banned collection
73
+
74
+ ```ts
75
+ Gating.not(Gating.count(1, { collection: Gating.eq("BannedCol...") }))
76
+ ```
77
+
78
+ ## Exclude specific mints
79
+
80
+ ```ts
81
+ Gating.count(1, { mint: Gating.notIn("Blocked1...", "Blocked2...") })
82
+ ```
83
+
84
+ ## Bronze / silver / gold tiers
85
+
86
+ ```ts
87
+ const tiers = [
88
+ Gating.tier("bronze", Gating.count(1, {
89
+ collection: Gating.eq(COLLECTION),
90
+ })),
91
+ Gating.tier("silver", Gating.count(3, {
92
+ collection: Gating.eq(COLLECTION),
93
+ })),
94
+ Gating.tier("gold", Gating.and(
95
+ Gating.count(1, {
96
+ collection: Gating.eq(COLLECTION),
97
+ traits: Gating.traitsAll(
98
+ Gating.trait("Rarity", GatingTraitValue.eq("Gold")),
99
+ Gating.trait("Level", GatingTraitValue.gte(10)),
100
+ ),
101
+ }),
102
+ Gating.totalBalance(REWARD_TOKEN, 10_000_000n),
103
+ )),
104
+ ];
105
+ ```
106
+
107
+ ## Full app integration
108
+
109
+ ```ts
110
+ import {
111
+ evaluateAssetGating,
112
+ Gating,
113
+ GatingTraitValue,
114
+ summarizeGatingEvaluationFailure,
115
+ } from "phygital-token-sdk";
116
+
117
+ async function gateExperience(assetPublicKey: string, rpc: Rpc) {
118
+ const result = await evaluateAssetGating({
119
+ assetPublicKey,
120
+ rpc,
121
+ tiers: [
122
+ Gating.tier("bronze", Gating.count(1, {
123
+ collection: Gating.eq(process.env.COLLECTION_MINT!),
124
+ })),
125
+ Gating.tier("gold", Gating.count(1, {
126
+ collection: Gating.eq(process.env.COLLECTION_MINT!),
127
+ traits: Gating.traitsAll(
128
+ Gating.trait("Rarity", GatingTraitValue.eq("Gold")),
129
+ ),
130
+ })),
131
+ ],
132
+ });
133
+
134
+ if (!result.passed) {
135
+ return {
136
+ allowed: false,
137
+ reasons: summarizeGatingEvaluationFailure(result),
138
+ };
139
+ }
140
+
141
+ return {
142
+ allowed: true,
143
+ passedTierIds: result.passedTierIds,
144
+ owner: result.owner,
145
+ };
146
+ }
147
+ ```
148
+
149
+ ## Footguns
150
+
151
+ ### ❌ Traits split across NFTs
152
+
153
+ ```ts
154
+ // WRONG — requires one NFT to be both Gold and Silver
155
+ Gating.count(1, {
156
+ traits: Gating.traitsAll(
157
+ Gating.trait("Rarity", GatingTraitValue.eq("Gold")),
158
+ Gating.trait("Rarity", GatingTraitValue.eq("Silver")),
159
+ ),
160
+ })
161
+ ```
162
+
163
+ ### ❌ Using two count(1) for traits on one NFT
164
+
165
+ ```ts
166
+ // WRONG — checks two different assets
167
+ Gating.and(
168
+ Gating.count(1, { traits: Gating.traitsAll(Gating.trait("Rarity", GatingTraitValue.eq("Gold"))) }),
169
+ Gating.count(1, { traits: Gating.traitsAll(Gating.trait("Level", GatingTraitValue.gte(5))) }),
170
+ )
171
+
172
+ // RIGHT — one NFT, all traits
173
+ Gating.count(1, {
174
+ traits: Gating.traitsAll(
175
+ Gating.trait("Rarity", GatingTraitValue.eq("Gold")),
176
+ Gating.trait("Level", GatingTraitValue.gte(5)),
177
+ ),
178
+ })
179
+ ```
180
+
181
+ ### ❌ Contradictory AND
182
+
183
+ ```ts
184
+ // Always fails
185
+ Gating.and(
186
+ Gating.count(1, { collection: Gating.eq("ColA") }),
187
+ Gating.not(Gating.count(1, { collection: Gating.eq("ColA") })),
188
+ )
189
+ ```
190
+
191
+ ### ❌ UI decimals in balance
192
+
193
+ ```ts
194
+ // WRONG for 6-decimal token — this is 0.000001 tokens
195
+ Gating.totalBalance("USDC...", 1n)
196
+
197
+ // RIGHT — 1 USDC
198
+ Gating.totalBalance("USDC...", 1_000_000n)
199
+ ```