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.
- package/README.md +94 -0
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -8
- package/dist/index.js.map +1 -1
- package/dist/instructions/transfer.d.ts +3 -11
- package/dist/instructions/transfer.d.ts.map +1 -1
- package/dist/instructions/transfer.js +10 -32
- package/dist/instructions/transfer.js.map +1 -1
- package/dist/instructions/verifyAsset.d.ts +14 -9
- package/dist/instructions/verifyAsset.d.ts.map +1 -1
- package/dist/instructions/verifyAsset.js +44 -19
- package/dist/instructions/verifyAsset.js.map +1 -1
- package/dist/utils/assetCredential.d.ts +1 -1
- package/dist/utils/assetCredential.d.ts.map +1 -1
- package/dist/utils/assetCredential.js +2 -2
- package/dist/utils/assetCredential.js.map +1 -1
- package/dist/utils/consts.d.ts +1 -1
- package/dist/utils/consts.d.ts.map +1 -1
- package/dist/utils/consts.js +1 -1
- package/dist/utils/consts.js.map +1 -1
- package/dist/utils/gating.d.ts +195 -0
- package/dist/utils/gating.d.ts.map +1 -0
- package/dist/utils/gating.js +481 -0
- package/dist/utils/gating.js.map +1 -0
- package/dist/utils/passkey/nfc/authDataExtensions.d.ts.map +1 -1
- package/dist/utils/passkey/nfc/authDataExtensions.js +2 -2
- package/dist/utils/passkey/nfc/authDataExtensions.js.map +1 -1
- package/dist/utils/passkey/secp256r1.d.ts +9 -6
- package/dist/utils/passkey/secp256r1.d.ts.map +1 -1
- package/dist/utils/passkey/secp256r1.js +14 -23
- package/dist/utils/passkey/secp256r1.js.map +1 -1
- package/dist/utils/slotHash.d.ts +1 -2
- package/dist/utils/slotHash.d.ts.map +1 -1
- package/dist/utils/slotHash.js +0 -12
- package/dist/utils/slotHash.js.map +1 -1
- package/dist/utils/verify.d.ts +2 -9
- package/dist/utils/verify.d.ts.map +1 -1
- package/dist/utils/verify.js +12 -104
- package/dist/utils/verify.js.map +1 -1
- package/docs/gating/README.md +83 -0
- package/docs/gating/evaluation-and-errors.md +134 -0
- package/docs/gating/filters-and-composition.md +120 -0
- package/docs/gating/overview.md +84 -0
- package/docs/gating/predicates.md +122 -0
- package/docs/gating/recipes.md +199 -0
- package/docs/gating/tiers.md +158 -0
- package/package.json +11 -10
- package/dist/__tests__/card-instance.test.d.ts +0 -2
- package/dist/__tests__/card-instance.test.d.ts.map +0 -1
- package/dist/__tests__/card-instance.test.js +0 -38
- package/dist/__tests__/card-instance.test.js.map +0 -1
- package/dist/__tests__/crypto-parity.test.d.ts +0 -2
- package/dist/__tests__/crypto-parity.test.d.ts.map +0 -1
- package/dist/__tests__/crypto-parity.test.js +0 -60
- package/dist/__tests__/crypto-parity.test.js.map +0 -1
- package/dist/__tests__/low-s-normalization.test.d.ts +0 -2
- package/dist/__tests__/low-s-normalization.test.d.ts.map +0 -1
- package/dist/__tests__/low-s-normalization.test.js +0 -44
- package/dist/__tests__/low-s-normalization.test.js.map +0 -1
- package/dist/__tests__/metadata-limits.test.d.ts +0 -2
- package/dist/__tests__/metadata-limits.test.d.ts.map +0 -1
- package/dist/__tests__/metadata-limits.test.js +0 -33
- package/dist/__tests__/metadata-limits.test.js.map +0 -1
- package/dist/__tests__/nfc-cbor.test.d.ts +0 -2
- package/dist/__tests__/nfc-cbor.test.d.ts.map +0 -1
- package/dist/__tests__/nfc-cbor.test.js +0 -72
- package/dist/__tests__/nfc-cbor.test.js.map +0 -1
- package/dist/consts.d.ts +0 -19
- package/dist/consts.d.ts.map +0 -1
- package/dist/consts.js +0 -19
- package/dist/consts.js.map +0 -1
- package/dist/generated/accounts/cardInstance.d.ts +0 -35
- package/dist/generated/accounts/cardInstance.d.ts.map +0 -1
- package/dist/generated/accounts/cardInstance.js +0 -61
- package/dist/generated/accounts/cardInstance.js.map +0 -1
- package/dist/generated/accounts/domainConfig.d.ts +0 -28
- package/dist/generated/accounts/domainConfig.d.ts.map +0 -1
- package/dist/generated/accounts/domainConfig.js +0 -58
- package/dist/generated/accounts/domainConfig.js.map +0 -1
- package/dist/generated/errors/phygitalNfts.d.ts +0 -62
- package/dist/generated/errors/phygitalNfts.d.ts.map +0 -1
- package/dist/generated/errors/phygitalNfts.js +0 -87
- package/dist/generated/errors/phygitalNfts.js.map +0 -1
- package/dist/generated/instructions/createCollectionMint.d.ts +0 -108
- package/dist/generated/instructions/createCollectionMint.d.ts.map +0 -1
- package/dist/generated/instructions/createCollectionMint.js +0 -175
- package/dist/generated/instructions/createCollectionMint.js.map +0 -1
- package/dist/generated/instructions/createDesignMint.d.ts +0 -97
- package/dist/generated/instructions/createDesignMint.d.ts.map +0 -1
- package/dist/generated/instructions/createDesignMint.js +0 -164
- package/dist/generated/instructions/createDesignMint.js.map +0 -1
- package/dist/generated/instructions/createDomainConfig.d.ts +0 -46
- package/dist/generated/instructions/createDomainConfig.d.ts.map +0 -1
- package/dist/generated/instructions/createDomainConfig.js +0 -83
- package/dist/generated/instructions/createDomainConfig.js.map +0 -1
- package/dist/generated/instructions/createGroupToken.d.ts +0 -94
- package/dist/generated/instructions/createGroupToken.d.ts.map +0 -1
- package/dist/generated/instructions/createGroupToken.js +0 -150
- package/dist/generated/instructions/createGroupToken.js.map +0 -1
- package/dist/generated/instructions/createToken.d.ts +0 -88
- package/dist/generated/instructions/createToken.d.ts.map +0 -1
- package/dist/generated/instructions/createToken.js +0 -182
- package/dist/generated/instructions/createToken.js.map +0 -1
- package/dist/generated/instructions/editDomainConfig.d.ts +0 -50
- package/dist/generated/instructions/editDomainConfig.d.ts.map +0 -1
- package/dist/generated/instructions/editDomainConfig.js +0 -90
- package/dist/generated/instructions/editDomainConfig.js.map +0 -1
- package/dist/generated/instructions/executeSpend.d.ts +0 -86
- package/dist/generated/instructions/executeSpend.d.ts.map +0 -1
- package/dist/generated/instructions/executeSpend.js +0 -187
- package/dist/generated/instructions/executeSpend.js.map +0 -1
- package/dist/generated/instructions/setTransferConfig.d.ts +0 -88
- package/dist/generated/instructions/setTransferConfig.d.ts.map +0 -1
- package/dist/generated/instructions/setTransferConfig.js +0 -188
- package/dist/generated/instructions/setTransferConfig.js.map +0 -1
- package/dist/generated/instructions/updateCounter.d.ts +0 -45
- package/dist/generated/instructions/updateCounter.d.ts.map +0 -1
- package/dist/generated/instructions/updateCounter.js +0 -84
- package/dist/generated/instructions/updateCounter.js.map +0 -1
- package/dist/generated/instructions/updateDomainConfig.d.ts +0 -43
- package/dist/generated/instructions/updateDomainConfig.d.ts.map +0 -1
- package/dist/generated/instructions/updateDomainConfig.js +0 -81
- package/dist/generated/instructions/updateDomainConfig.js.map +0 -1
- package/dist/generated/pdas/spendAuthority.d.ts +0 -8
- package/dist/generated/pdas/spendAuthority.d.ts.map +0 -1
- package/dist/generated/pdas/spendAuthority.js +0 -15
- package/dist/generated/pdas/spendAuthority.js.map +0 -1
- package/dist/generated/programs/phygitalNfts.d.ts +0 -72
- package/dist/generated/programs/phygitalNfts.d.ts.map +0 -1
- package/dist/generated/programs/phygitalNfts.js +0 -136
- package/dist/generated/programs/phygitalNfts.js.map +0 -1
- package/dist/instructions/collection.d.ts +0 -14
- package/dist/instructions/collection.d.ts.map +0 -1
- package/dist/instructions/collection.js +0 -59
- package/dist/instructions/collection.js.map +0 -1
- package/dist/instructions/index.d.ts +0 -2
- package/dist/instructions/index.d.ts.map +0 -1
- package/dist/instructions/index.js +0 -2
- package/dist/instructions/index.js.map +0 -1
- package/dist/instructions/secp256r1Verify.d.ts +0 -29
- package/dist/instructions/secp256r1Verify.d.ts.map +0 -1
- package/dist/instructions/secp256r1Verify.js +0 -132
- package/dist/instructions/secp256r1Verify.js.map +0 -1
- package/dist/instructions/setLockState.d.ts +0 -19
- package/dist/instructions/setLockState.d.ts.map +0 -1
- package/dist/instructions/setLockState.js +0 -17
- package/dist/instructions/setLockState.js.map +0 -1
- package/dist/instructions/setTransferConfig.d.ts +0 -25
- package/dist/instructions/setTransferConfig.d.ts.map +0 -1
- package/dist/instructions/setTransferConfig.js +0 -20
- package/dist/instructions/setTransferConfig.js.map +0 -1
- package/dist/instructions/updateCounter.d.ts +0 -15
- package/dist/instructions/updateCounter.d.ts.map +0 -1
- package/dist/instructions/updateCounter.js +0 -20
- package/dist/instructions/updateCounter.js.map +0 -1
- package/dist/mint-metadata.d.ts +0 -13
- package/dist/mint-metadata.d.ts.map +0 -1
- package/dist/mint-metadata.js +0 -127
- package/dist/mint-metadata.js.map +0 -1
- package/dist/passkeys/index.d.ts +0 -18
- package/dist/passkeys/index.d.ts.map +0 -1
- package/dist/passkeys/index.js +0 -30
- package/dist/passkeys/index.js.map +0 -1
- package/dist/passkeys/internal.d.ts +0 -15
- package/dist/passkeys/internal.d.ts.map +0 -1
- package/dist/passkeys/internal.js +0 -113
- package/dist/passkeys/internal.js.map +0 -1
- package/dist/secp256r1.d.ts +0 -24
- package/dist/secp256r1.d.ts.map +0 -1
- package/dist/secp256r1.js +0 -38
- package/dist/secp256r1.js.map +0 -1
- package/dist/transfer.d.ts +0 -28
- package/dist/transfer.d.ts.map +0 -1
- package/dist/transfer.js +0 -109
- package/dist/transfer.js.map +0 -1
- package/dist/utils/encoding.d.ts +0 -3
- package/dist/utils/encoding.d.ts.map +0 -1
- package/dist/utils/encoding.js +0 -14
- package/dist/utils/encoding.js.map +0 -1
- package/dist/utils/passkey/index.d.ts +0 -17
- package/dist/utils/passkey/index.d.ts.map +0 -1
- package/dist/utils/passkey/index.js +0 -21
- package/dist/utils/passkey/index.js.map +0 -1
- package/dist/utils/passkey/nfc/base64url.d.ts +0 -2
- package/dist/utils/passkey/nfc/base64url.d.ts.map +0 -1
- package/dist/utils/passkey/nfc/base64url.js +0 -13
- package/dist/utils/passkey/nfc/base64url.js.map +0 -1
- package/dist/utils/pdas/domainConfig.d.ts +0 -3
- package/dist/utils/pdas/domainConfig.d.ts.map +0 -1
- package/dist/utils/pdas/domainConfig.js +0 -15
- package/dist/utils/pdas/domainConfig.js.map +0 -1
- package/dist/utils/sendInstructions.d.ts +0 -7
- package/dist/utils/sendInstructions.d.ts.map +0 -1
- package/dist/utils/sendInstructions.js +0 -13
- package/dist/utils/sendInstructions.js.map +0 -1
- package/dist/utils/tokenOwner.d.ts +0 -7
- package/dist/utils/tokenOwner.d.ts.map +0 -1
- package/dist/utils/tokenOwner.js +0 -24
- package/dist/utils/tokenOwner.js.map +0 -1
- package/dist/utils.d.ts +0 -10
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -42
- 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
|
+
```
|