@soltracer/nft-staking 0.2.9 → 0.3.1
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/INTEGRATION.md +195 -47
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/traitProof.d.ts +223 -24
- package/dist/traitProof.d.ts.map +1 -1
- package/dist/traitProof.js +161 -59
- package/dist/traitProof.js.map +1 -1
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -280,70 +280,218 @@ Early-unstake penalties apply on exit when configured.
|
|
|
280
280
|
#### Aggregating per-trait bonuses
|
|
281
281
|
|
|
282
282
|
The on-chain instruction accepts a **single** `u64` `traitBonusRate` per claim — it has no notion of per-trait or
|
|
283
|
-
per-NFT granularity
|
|
284
|
-
|
|
283
|
+
per-NFT granularity, and it cannot read NFT metadata. Aggregation lives entirely off-chain in the trait-authority
|
|
284
|
+
server. The intended flow:
|
|
285
|
+
|
|
286
|
+
1. **Client request.** The wallet calls your API with `{ wallet, poolId, mints }` (the mints it wants to claim against).
|
|
287
|
+
2. **Ownership / stake validation.** The API resolves the wallet's active stake entries
|
|
288
|
+
(`client.fetchStakeEntriesByOwner(poolId, wallet)`) and **drops** any mint not present — never trust the client's
|
|
289
|
+
list. Anything that survives is provably staked by `wallet` in `pool`.
|
|
290
|
+
3. **Trait resolution.** For each surviving mint, fetch its trait list from your own source of truth (collection JSON,
|
|
291
|
+
Metaplex on-chain metadata, indexer, internal DB). The chain stores no trait data.
|
|
292
|
+
4. **Catalog lookup.** Each `(traitType, value)` pair is checked against your project's bonus catalog. Traits without
|
|
293
|
+
an entry contribute nothing.
|
|
294
|
+
5. **Mode-aware sum.** Bonuses are summed according to the pool's `rewardConfig.traitBonusMode`:
|
|
295
|
+
- `FixedExtra`: catalog uses `fixedExtra` (raw reward per `rateInterval`); each matched trait adds its value.
|
|
296
|
+
- `BaseMultiplier`: catalog uses `bonusBps` (10_000 = +100%); each matched trait adds its BPS to a single boost that
|
|
297
|
+
scales the staker's current `effective_rate` for the accrual window.
|
|
298
|
+
6. **Sign and return.** Sign the resulting `u64` with the pool's `traitAuthority` keypair; return signature + rate to
|
|
299
|
+
the client.
|
|
300
|
+
|
|
301
|
+
The SDK ships `computeTraitBonusRate` for steps 4–5 (mode-aware aggregation only) and `prepareTraitProof` for the full
|
|
302
|
+
pipeline (steps 2–5 plus the 80-byte signable message). Pick whichever fits how much of the flow your stack already
|
|
303
|
+
owns.
|
|
304
|
+
|
|
305
|
+
#### Managing the bonus catalog (any DB)
|
|
306
|
+
|
|
307
|
+
The admin UI (Trait Bonuses card) writes a row per `(projectId, poolId, traitType, traitValue)` to your DB. Sample
|
|
308
|
+
Prisma schema used by the reference app:
|
|
309
|
+
|
|
310
|
+
```prisma
|
|
311
|
+
model TraitBonusConfig {
|
|
312
|
+
id String @id @default(cuid())
|
|
313
|
+
projectId String @map("project_id")
|
|
314
|
+
poolId Int @map("pool_id")
|
|
315
|
+
traitType String @map("trait_type") @db.VarChar(64)
|
|
316
|
+
traitValue String @map("trait_value") @db.VarChar(64)
|
|
317
|
+
bonusAmount Int @map("bonus_amount") // bonusBps (mode 2) or fixedExtra (mode 1)
|
|
318
|
+
imageUrl String? @map("image_url")
|
|
319
|
+
createdAt DateTime @default(now())
|
|
320
|
+
updatedAt DateTime @updatedAt
|
|
321
|
+
|
|
322
|
+
@@unique([projectId, poolId, traitType, traitValue])
|
|
323
|
+
@@map("trait_bonus_configs")
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
You can model this however you like — Drizzle, raw Postgres, Mongo, KV, Firestore, an in-memory map for testing. The
|
|
328
|
+
SDK only needs a `TraitBonusStore` adapter:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import type { TraitBonusStore } from "@soltracer/nft-staking"
|
|
332
|
+
|
|
333
|
+
// Prisma example
|
|
334
|
+
export const prismaTraitBonusStore: TraitBonusStore = {
|
|
335
|
+
async list({ poolId, projectId }) {
|
|
336
|
+
const rows = await prisma.traitBonusConfig.findMany({
|
|
337
|
+
where: { poolId, ...(projectId ? { projectId: String(projectId) } : {}) },
|
|
338
|
+
})
|
|
339
|
+
return rows.map((r) => ({
|
|
340
|
+
traitType: r.traitType,
|
|
341
|
+
value: r.traitValue,
|
|
342
|
+
// Pool in BaseMultiplier mode? use bonusBps. FixedExtra? use fixedExtra.
|
|
343
|
+
bonusBps: r.bonusAmount,
|
|
344
|
+
}))
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Drizzle example (drop-in replacement)
|
|
349
|
+
export const drizzleTraitBonusStore: TraitBonusStore = {
|
|
350
|
+
async list({ poolId, projectId }) {
|
|
351
|
+
const rows = await db
|
|
352
|
+
.select()
|
|
353
|
+
.from(traitBonusConfig)
|
|
354
|
+
.where(and(eq(traitBonusConfig.poolId, poolId), projectId ? eq(traitBonusConfig.projectId, String(projectId)) : undefined))
|
|
355
|
+
return rows.map((r) => ({ traitType: r.traitType, value: r.traitValue, bonusBps: r.bonusAmount }))
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// In-memory (for tests / examples)
|
|
360
|
+
export function makeMemoryStore(seed: TraitBonusCatalogEntry[]): TraitBonusStore {
|
|
361
|
+
return { async list() { return seed } }
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Recommended CRUD endpoints (the reference app exposes these as tRPC; HTTP/GraphQL works identically):
|
|
285
366
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
367
|
+
| Operation | Inputs | Behavior |
|
|
368
|
+
| --- | --- | --- |
|
|
369
|
+
| `list` | `{ projectId, poolId }` | Returns all catalog rows for the pool. Used by the admin UI and by `TraitBonusStore.list`. |
|
|
370
|
+
| `create` | `{ projectId, poolId, traitType, traitValue, bonusAmount, imageUrl? }` | Inserts one row. |
|
|
371
|
+
| `update` | `{ id, bonusAmount?, imageUrl? }` | Updates a single row. |
|
|
372
|
+
| `delete` | `{ id }` | Deletes a row. |
|
|
373
|
+
| `upsert` (bulk) | `{ projectId, poolId, bonuses: [...] }` | Idempotent bulk replace via the unique `(projectId, poolId, traitType, traitValue)` index. |
|
|
293
374
|
|
|
294
|
-
|
|
295
|
-
|
|
375
|
+
Treat all five as admin-only. Gate them behind a project-authority check (e.g. signed-message session for the
|
|
376
|
+
project's `authority` wallet) so non-admins can't rewrite reward rules.
|
|
377
|
+
|
|
378
|
+
#### Full backend flow with `prepareTraitProof`
|
|
379
|
+
|
|
380
|
+
`prepareTraitProof` wires together ownership validation, trait resolution, the store, and the 80-byte message into a
|
|
381
|
+
single async call. You only supply the four pieces that depend on your infrastructure:
|
|
296
382
|
|
|
297
383
|
```ts
|
|
298
384
|
import {
|
|
299
|
-
|
|
300
|
-
|
|
385
|
+
prepareTraitProof,
|
|
386
|
+
ownershipFromClient,
|
|
387
|
+
type NftTraitResolver,
|
|
301
388
|
} from "@soltracer/nft-staking"
|
|
389
|
+
import nacl from "tweetnacl"
|
|
302
390
|
|
|
303
|
-
//
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
391
|
+
// One-time wiring:
|
|
392
|
+
const traitBonusStore = prismaTraitBonusStore // from §"Managing the bonus catalog"
|
|
393
|
+
const resolveTraits: NftTraitResolver = async (mint) => {
|
|
394
|
+
// Your trait source: Metaplex metadata, indexer, internal DB, collection JSON, etc.
|
|
395
|
+
const nft = await metadataIndex.getByMint(mint)
|
|
396
|
+
return nft.attributes.map((a) => ({ traitType: a.trait_type, value: a.value }))
|
|
397
|
+
}
|
|
398
|
+
const validateOwnership = ownershipFromClient(client) // or your own stake-state index
|
|
399
|
+
|
|
400
|
+
// ── POST /api/staking/trait-proof
|
|
401
|
+
async function handleProofRequest(req: { wallet: PublicKey; projectId: string; poolId: number; mints: string[] }) {
|
|
402
|
+
const pool = await client.fetchStakePool(undefined, req.poolId)
|
|
403
|
+
const staker = await client.fetchStakerAccount(req.poolId, req.wallet)
|
|
404
|
+
|
|
405
|
+
const prepared = await prepareTraitProof({
|
|
406
|
+
pool: { address: pool.address, rewardConfig: pool.rewardConfig },
|
|
407
|
+
projectId: req.projectId,
|
|
408
|
+
poolId: req.poolId,
|
|
409
|
+
wallet: req.wallet,
|
|
410
|
+
requestedMints: req.mints,
|
|
411
|
+
store: traitBonusStore,
|
|
412
|
+
resolveTraits,
|
|
413
|
+
validateOwnership,
|
|
414
|
+
totalClaimed: staker.totalClaimed,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const signature = nacl.sign.detached(prepared.message, traitAuthority.secretKey)
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
traitBonusRate: prepared.traitBonusRate.toString(),
|
|
421
|
+
signature: Buffer.from(signature).toString("base64"),
|
|
422
|
+
traitAuthority: traitAuthority.publicKey.toBase58(),
|
|
423
|
+
validatedMints: prepared.validatedMints,
|
|
424
|
+
rejectedMints: prepared.rejectedMints,
|
|
425
|
+
perNft: prepared.perNft, // [{ mint, bonus, matchedTraits }] — display in the claim UI
|
|
316
426
|
}
|
|
317
427
|
}
|
|
428
|
+
```
|
|
318
429
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const traitBonusRate = aggregateTraitBonusRate(
|
|
323
|
-
traitBonuses,
|
|
324
|
-
pool.rewardConfig.traitBonusMode as 0 | 1 | 2,
|
|
325
|
-
)
|
|
430
|
+
Client side, pass the returned `signature` and `traitBonusRate` (as `BN`) into `client.claimRewards(poolId, { traitBonusRate, fee })`
|
|
431
|
+
and prepend the `Ed25519Program.createInstructionWithPublicKey({ publicKey: traitAuthority, message, signature })`
|
|
432
|
+
instruction to the same transaction.
|
|
326
433
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
434
|
+
The original lower-level helpers remain available if you want to compose the pipeline yourself:
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import {
|
|
438
|
+
buildTraitProofMessage,
|
|
439
|
+
computeTraitBonusRate,
|
|
440
|
+
type NftTraitInput,
|
|
441
|
+
type TraitBonusCatalogEntry,
|
|
442
|
+
} from "@soltracer/nft-staking"
|
|
443
|
+
import nacl from "tweetnacl"
|
|
444
|
+
|
|
445
|
+
// ── Server: POST /claim/trait-proof body: { wallet, poolId, mints }
|
|
446
|
+
async function buildProof(req: { wallet: PublicKey; poolId: number; mints: string[] }) {
|
|
447
|
+
// 2. Validate ownership / current stake state.
|
|
448
|
+
const entries = await client.fetchStakeEntriesByOwner(req.poolId, req.wallet)
|
|
449
|
+
const stakedSet = new Set(entries.map((e) => e.nftMint.toBase58()))
|
|
450
|
+
const ownedMints = req.mints.filter((m) => stakedSet.has(m))
|
|
451
|
+
|
|
452
|
+
// 3. Resolve traits from your collection catalog.
|
|
453
|
+
const nfts: NftTraitInput[] = await Promise.all(
|
|
454
|
+
ownedMints.map(async (mint) => ({
|
|
455
|
+
mint,
|
|
456
|
+
traits: await catalog.getTraits(mint), // [{ traitType, value }, ..]
|
|
457
|
+
})),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
// 4 + 5. Mode-aware aggregation against the project's bonus catalog.
|
|
461
|
+
const pool = await client.fetchStakePool(undefined, req.poolId)
|
|
462
|
+
const bonusCatalog: TraitBonusCatalogEntry[] = [
|
|
463
|
+
{ traitType: "Background", value: "Mythic", bonusBps: 2500 }, // +25%
|
|
464
|
+
{ traitType: "Eyes", value: "Laser", bonusBps: 1000 }, // +10%
|
|
465
|
+
{ traitType: "Head", value: "Gold Crown", bonusBps: 500 }, // +5%
|
|
466
|
+
]
|
|
467
|
+
const { traitBonusRate, perNft } = computeTraitBonusRate({
|
|
468
|
+
traitBonusMode: pool.rewardConfig.traitBonusMode as 0 | 1 | 2,
|
|
469
|
+
catalog: bonusCatalog,
|
|
470
|
+
nfts,
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
// 6. Sign the aggregated rate.
|
|
474
|
+
const stakerAccount = await client.fetchStakerAccount(req.poolId, req.wallet)
|
|
475
|
+
const message = buildTraitProofMessage({
|
|
476
|
+
pool: pool.address,
|
|
477
|
+
wallet: req.wallet,
|
|
478
|
+
traitBonusRate,
|
|
479
|
+
totalClaimed: stakerAccount.totalClaimed,
|
|
480
|
+
})
|
|
481
|
+
const signature = nacl.sign.detached(message, traitAuthority.secretKey)
|
|
482
|
+
return { traitBonusRate: traitBonusRate.toString(), signature, perNft }
|
|
483
|
+
}
|
|
336
484
|
```
|
|
337
485
|
|
|
338
|
-
> ⚠️ Re-
|
|
339
|
-
>
|
|
486
|
+
> ⚠️ Re-run the entire pipeline on every claim. The staked set, NFT traits, and the staker's `totalClaimed` nonce can
|
|
487
|
+
> all change between claims; a stale proof is rejected on-chain.
|
|
340
488
|
|
|
341
489
|
Examples per mode:
|
|
342
490
|
|
|
343
|
-
| Mode |
|
|
491
|
+
| Mode | Catalog field | What 1 matched trait contributes | Final `traitBonusRate` |
|
|
344
492
|
| --- | --- | --- | --- |
|
|
345
|
-
| `FixedExtra` | `fixedExtra: 100` | +100 reward tokens per `rateInterval` | Sum
|
|
346
|
-
| `BaseMultiplier` | `bonusBps: 500` | +5% over the staker's current effective rate | Sum
|
|
493
|
+
| `FixedExtra` | `fixedExtra: 100` | +100 reward tokens per `rateInterval` | Sum across every matched trait on every validated NFT. |
|
|
494
|
+
| `BaseMultiplier` | `bonusBps: 500` | +5% over the staker's current effective rate | Sum across every matched trait on every validated NFT. `10_000` ⇒ 2x base for that claim window. |
|
|
347
495
|
|
|
348
496
|
Trait proof messages bind the pool, wallet, signed rate, and the staker account's strictly-monotonic `totalClaimed`
|
|
349
497
|
counter. A new proof is required after every successful claim because `totalClaimed` advances.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export { type NftStaking as NftStakingIDLType } from "./idl";
|
|
|
4
4
|
export { default as NftStakingIDL } from "./idl.json";
|
|
5
5
|
export * from "./errors";
|
|
6
6
|
export * from "./helpers";
|
|
7
|
-
export { buildTraitProofMessage, TRAIT_PROOF_MSG_LEN, aggregateTraitBonusRate } from "./traitProof";
|
|
7
|
+
export { buildTraitProofMessage, TRAIT_PROOF_MSG_LEN, aggregateTraitBonusRate, computeTraitBonusRate, prepareTraitProof, ownershipFromClient, type TraitBonusCatalogEntry, type NftTraitInput, type ComputeTraitBonusResult, type TraitBonusStore, type NftTraitResolver, type StakedOwnershipResolver, type PreparedTraitProof, } from "./traitProof";
|
|
8
8
|
export { GATE_TYPE_NONE, GATE_TYPE_WALLET, GATE_TYPE_TOKEN_MINT, type GateType, type MerkleTree, hashLeaf, hashPair, buildMerkleTree, getMerkleProof, proofToAnchorArg, rootToAnchorArg, } from "./merkle";
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,GACf,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,KAAK,UAAU,IAAI,iBAAiB,EAAE,MAAM,OAAO,CAAA;AAC5D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AACrD,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,GACf,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,KAAK,UAAU,IAAI,iBAAiB,EAAE,MAAM,OAAO,CAAA;AAC5D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AACrD,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,UAAU,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,6 @@ export { NFT_STAKING_PROGRAM_ID, getStakeConfigPda, getStakePoolPda, getStakeEnt
|
|
|
3
3
|
export { default as NftStakingIDL } from "./idl.json";
|
|
4
4
|
export * from "./errors";
|
|
5
5
|
export * from "./helpers";
|
|
6
|
-
export { buildTraitProofMessage, TRAIT_PROOF_MSG_LEN, aggregateTraitBonusRate } from "./traitProof";
|
|
6
|
+
export { buildTraitProofMessage, TRAIT_PROOF_MSG_LEN, aggregateTraitBonusRate, computeTraitBonusRate, prepareTraitProof, ownershipFromClient, } from "./traitProof";
|
|
7
7
|
export { GATE_TYPE_NONE, GATE_TYPE_WALLET, GATE_TYPE_TOKEN_MINT, hashLeaf, hashPair, buildMerkleTree, getMerkleProof, proofToAnchorArg, rootToAnchorArg, } from "./merkle";
|
|
8
8
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GAKjB,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,GACf,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AACrD,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GAKjB,MAAM,UAAU,CAAA;AACjB,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,WAAW,EACX,WAAW,EACX,UAAU,EACV,cAAc,GACf,MAAM,iBAAiB,CAAA;AAExB,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AACrD,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,GAQpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EAGpB,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,eAAe,GAChB,MAAM,UAAU,CAAA"}
|
package/dist/traitProof.d.ts
CHANGED
|
@@ -63,37 +63,236 @@ export declare function buildTraitProofMessage(params: {
|
|
|
63
63
|
* The on-chain `claim_rewards` instruction accepts a **single** `u64`
|
|
64
64
|
* `traitBonusRate` per claim — the chain knows nothing about per-trait
|
|
65
65
|
* granularity. Aggregation across N staked NFTs (each with M traits) is the
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* -
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
66
|
+
* trait-authority server's responsibility.
|
|
67
|
+
*
|
|
68
|
+
* Intended backend flow:
|
|
69
|
+
*
|
|
70
|
+
* 1. Client sends `{ wallet, poolId, mints: [..] }` to your API.
|
|
71
|
+
* 2. API validates `mints` are currently staked by `wallet` in `pool`
|
|
72
|
+
* (e.g. via `fetchStakeEntriesByOwner`) and DROPS any it cannot prove.
|
|
73
|
+
* 3. API resolves each surviving NFT's trait list from its own metadata
|
|
74
|
+
* source (off-chain JSON, on-chain metaplex data, indexer, etc.).
|
|
75
|
+
* 4. API looks up each `(traitType, value)` pair in the project's bonus
|
|
76
|
+
* catalog. Unmatched traits contribute nothing.
|
|
77
|
+
* 5. API sums the matched bonuses according to the pool's
|
|
78
|
+
* `rewardConfig.traitBonusMode` and signs the resulting `u64`.
|
|
79
|
+
*
|
|
80
|
+
* Modes:
|
|
81
|
+
*
|
|
82
|
+
* - `0` / `None` — proofs disabled. Always returns `0n`.
|
|
83
|
+
* - `1` / `FixedExtra` — bonuses are **absolute reward per `rateInterval`**.
|
|
84
|
+
* Summed and added on top of the staker's effective rate for the accrual
|
|
85
|
+
* window. Catalog entries MUST use `fixedExtra`.
|
|
86
|
+
* - `2` / `BaseMultiplier` — bonuses are **BPS over the staker's current
|
|
87
|
+
* `effective_rate`** (which already includes lock-tier rates). `10_000`
|
|
88
|
+
* means +100% (2x base). Summed BPS apply once to the staker rate.
|
|
89
|
+
* Catalog entries MUST use `bonusBps`.
|
|
90
|
+
*
|
|
91
|
+
* Catalog entries with both `bonusBps` and `fixedExtra` set, or with the wrong
|
|
92
|
+
* field for the active mode, are rejected. Mints can repeat the same trait
|
|
93
|
+
* type/value — each NFT contributes independently.
|
|
94
|
+
*
|
|
95
|
+
* @returns `{ traitBonusRate, perNft }` where `perNft` is the per-mint
|
|
96
|
+
* breakdown (useful for receipts, debugging, or surfacing a tooltip in the UI).
|
|
81
97
|
*
|
|
82
98
|
* @example
|
|
83
99
|
* ```ts
|
|
84
|
-
* //
|
|
85
|
-
* const
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
100
|
+
* // Backend after ownership validation:
|
|
101
|
+
* const result = computeTraitBonusRate({
|
|
102
|
+
* traitBonusMode: pool.rewardConfig.traitBonusMode as 0 | 1 | 2,
|
|
103
|
+
* catalog: [
|
|
104
|
+
* { traitType: "Background", value: "Mythic", bonusBps: 2500 },
|
|
105
|
+
* { traitType: "Eyes", value: "Laser", bonusBps: 1000 },
|
|
106
|
+
* { traitType: "Head", value: "Gold Crown", bonusBps: 500 },
|
|
107
|
+
* ],
|
|
108
|
+
* nfts: validatedStakedNfts, // [{ mint, traits: [{ traitType, value }] }, ..]
|
|
109
|
+
* })
|
|
110
|
+
*
|
|
111
|
+
* const message = buildTraitProofMessage({
|
|
112
|
+
* pool, wallet,
|
|
113
|
+
* traitBonusRate: result.traitBonusRate,
|
|
114
|
+
* totalClaimed: stakerAccount.totalClaimed,
|
|
115
|
+
* })
|
|
93
116
|
* ```
|
|
94
117
|
*/
|
|
118
|
+
export type TraitBonusCatalogEntry = {
|
|
119
|
+
traitType: string;
|
|
120
|
+
value: string;
|
|
121
|
+
/** Bonus BPS — required when `traitBonusMode === 2` (BaseMultiplier). */
|
|
122
|
+
bonusBps?: number | bigint;
|
|
123
|
+
/** Extra reward per `rateInterval` — required when `traitBonusMode === 1` (FixedExtra). */
|
|
124
|
+
fixedExtra?: number | bigint;
|
|
125
|
+
};
|
|
126
|
+
export type NftTraitInput = {
|
|
127
|
+
mint: PkInput | string;
|
|
128
|
+
/** Ordered or unordered list of an NFT's traits, after ownership validation. */
|
|
129
|
+
traits: ReadonlyArray<{
|
|
130
|
+
traitType: string;
|
|
131
|
+
value: string;
|
|
132
|
+
}>;
|
|
133
|
+
};
|
|
134
|
+
export type ComputeTraitBonusResult = {
|
|
135
|
+
/** Aggregated u64 rate to sign and include in the claim. */
|
|
136
|
+
traitBonusRate: bigint;
|
|
137
|
+
/** Per-NFT breakdown (mint → total bonus contributed + matched-trait count). */
|
|
138
|
+
perNft: Array<{
|
|
139
|
+
mint: string;
|
|
140
|
+
bonus: bigint;
|
|
141
|
+
matchedTraits: number;
|
|
142
|
+
}>;
|
|
143
|
+
};
|
|
144
|
+
export declare function computeTraitBonusRate(params: {
|
|
145
|
+
traitBonusMode: 0 | 1 | 2;
|
|
146
|
+
catalog: ReadonlyArray<TraitBonusCatalogEntry>;
|
|
147
|
+
nfts: ReadonlyArray<NftTraitInput>;
|
|
148
|
+
}): ComputeTraitBonusResult;
|
|
149
|
+
/**
|
|
150
|
+
* @deprecated Use `computeTraitBonusRate` instead — it accepts the real per-NFT
|
|
151
|
+
* trait input and a bonus catalog, mirroring the backend flow. Retained for
|
|
152
|
+
* call sites that already pre-resolved a flat bonus array.
|
|
153
|
+
*/
|
|
95
154
|
export declare function aggregateTraitBonusRate(traits: ReadonlyArray<{
|
|
96
155
|
bonusBps?: number | bigint;
|
|
97
156
|
fixedExtra?: number | bigint;
|
|
98
157
|
}>, traitBonusMode: 0 | 1 | 2): bigint;
|
|
158
|
+
/**
|
|
159
|
+
* Storage adapter for a project's trait-bonus catalog.
|
|
160
|
+
*
|
|
161
|
+
* The SDK does not care where you persist this data — Prisma, Drizzle, raw
|
|
162
|
+
* Postgres, Mongo, KV, in-memory, REST — implement this interface against
|
|
163
|
+
* whatever backs your admin UI. Each `(traitType, value)` row carries the
|
|
164
|
+
* bonus for that pair.
|
|
165
|
+
*/
|
|
166
|
+
export interface TraitBonusStore {
|
|
167
|
+
/**
|
|
168
|
+
* Return every catalog entry for a given pool. The orchestrator does not
|
|
169
|
+
* filter by trait — it expects the full set so unmatched traits naturally
|
|
170
|
+
* contribute nothing.
|
|
171
|
+
*/
|
|
172
|
+
list(params: {
|
|
173
|
+
poolId: number;
|
|
174
|
+
projectId?: string | number;
|
|
175
|
+
}): Promise<TraitBonusCatalogEntry[]>;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the on-chain trait list for a given NFT mint.
|
|
179
|
+
*
|
|
180
|
+
* Wire this to whatever your project uses as a source of truth: an indexer,
|
|
181
|
+
* Metaplex on-chain metadata, your own collection JSON, an internal DB, etc.
|
|
182
|
+
* Returning an empty array is fine — that NFT simply contributes no bonus.
|
|
183
|
+
*/
|
|
184
|
+
export type NftTraitResolver = (mint: string) => Promise<ReadonlyArray<{
|
|
185
|
+
traitType: string;
|
|
186
|
+
value: string;
|
|
187
|
+
}>>;
|
|
188
|
+
/** Validates which `requestedMints` are actually staked in `poolId` by `wallet`. */
|
|
189
|
+
export type StakedOwnershipResolver = (params: {
|
|
190
|
+
wallet: string;
|
|
191
|
+
poolId: number;
|
|
192
|
+
requestedMints: ReadonlyArray<string>;
|
|
193
|
+
}) => Promise<ReadonlyArray<string>>;
|
|
194
|
+
export type PreparedTraitProof = {
|
|
195
|
+
/** Aggregated u64 rate ready to sign. */
|
|
196
|
+
traitBonusRate: bigint;
|
|
197
|
+
/** Mints the user requested that survived ownership validation. */
|
|
198
|
+
validatedMints: string[];
|
|
199
|
+
/** Mints the user requested that were rejected (not staked). */
|
|
200
|
+
rejectedMints: string[];
|
|
201
|
+
/** Per-NFT contribution breakdown (useful for UI tooltips / audit logs). */
|
|
202
|
+
perNft: ComputeTraitBonusResult["perNft"];
|
|
203
|
+
/** Convenience: the raw 80-byte message to feed into `nacl.sign.detached`. */
|
|
204
|
+
message: Buffer;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* End-to-end backend orchestrator for the trait-proof flow.
|
|
208
|
+
*
|
|
209
|
+
* Combines the four user-supplied dependencies — ownership validation, trait
|
|
210
|
+
* resolution, the bonus-catalog store, and the on-chain staker nonce — into
|
|
211
|
+
* the single signable payload the on-chain claim instruction expects.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* import {
|
|
216
|
+
* prepareTraitProof,
|
|
217
|
+
* type TraitBonusStore,
|
|
218
|
+
* } from "@soltracer/nft-staking"
|
|
219
|
+
*
|
|
220
|
+
* // 1) Implement the store against your DB once (any DB works).
|
|
221
|
+
* const traitBonusStore: TraitBonusStore = {
|
|
222
|
+
* async list({ poolId, projectId }) {
|
|
223
|
+
* const rows = await prisma.traitBonusConfig.findMany({
|
|
224
|
+
* where: { poolId, ...(projectId ? { projectId: String(projectId) } : {}) },
|
|
225
|
+
* })
|
|
226
|
+
* return rows.map((r) => ({
|
|
227
|
+
* traitType: r.traitType,
|
|
228
|
+
* value: r.traitValue,
|
|
229
|
+
* // pool is BaseMultiplier? use bonusBps. FixedExtra? use fixedExtra.
|
|
230
|
+
* bonusBps: r.bonusAmount,
|
|
231
|
+
* }))
|
|
232
|
+
* },
|
|
233
|
+
* }
|
|
234
|
+
*
|
|
235
|
+
* // 2) Prepare the proof on each claim request.
|
|
236
|
+
* const prepared = await prepareTraitProof({
|
|
237
|
+
* client, // NftStakingClient
|
|
238
|
+
* pool, // already-fetched StakePool (has rewardConfig + address)
|
|
239
|
+
* wallet: req.wallet,
|
|
240
|
+
* requestedMints: req.mints,
|
|
241
|
+
* store: traitBonusStore,
|
|
242
|
+
* resolveTraits: (mint) => catalog.getTraits(mint),
|
|
243
|
+
* })
|
|
244
|
+
*
|
|
245
|
+
* // 3) Sign and respond.
|
|
246
|
+
* const signature = nacl.sign.detached(prepared.message, traitAuthority.secretKey)
|
|
247
|
+
* return {
|
|
248
|
+
* traitBonusRate: prepared.traitBonusRate.toString(),
|
|
249
|
+
* signature: Buffer.from(signature).toString("base64"),
|
|
250
|
+
* perNft: prepared.perNft,
|
|
251
|
+
* rejectedMints: prepared.rejectedMints,
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export declare function prepareTraitProof(params: {
|
|
256
|
+
/** Pool config — must expose `address` and `rewardConfig.traitBonusMode`. */
|
|
257
|
+
pool: {
|
|
258
|
+
address: PkInput;
|
|
259
|
+
rewardConfig: {
|
|
260
|
+
traitBonusMode: number;
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
/** Project ID hint forwarded to the store (DB scoping, optional). */
|
|
264
|
+
projectId?: string | number;
|
|
265
|
+
poolId?: number;
|
|
266
|
+
wallet: PkInput;
|
|
267
|
+
requestedMints: ReadonlyArray<string>;
|
|
268
|
+
/** Storage adapter for the bonus catalog. */
|
|
269
|
+
store: TraitBonusStore;
|
|
270
|
+
/** Resolves trait list for a single mint. */
|
|
271
|
+
resolveTraits: NftTraitResolver;
|
|
272
|
+
/**
|
|
273
|
+
* Validates which `requestedMints` are actually staked in this pool by
|
|
274
|
+
* `wallet`. If omitted, the orchestrator trusts the caller — DO NOT skip
|
|
275
|
+
* this in production. The SDK can drive this for you when you pass a
|
|
276
|
+
* `client` and `poolId` (see `ownershipFromClient`).
|
|
277
|
+
*/
|
|
278
|
+
validateOwnership: StakedOwnershipResolver;
|
|
279
|
+
/**
|
|
280
|
+
* Current `StakerAccount.totalClaimed` nonce. MUST be the live value;
|
|
281
|
+
* advance-then-sign in the same request to avoid same-second replay.
|
|
282
|
+
*/
|
|
283
|
+
totalClaimed: BnInput;
|
|
284
|
+
}): Promise<PreparedTraitProof>;
|
|
285
|
+
/**
|
|
286
|
+
* Convenience `StakedOwnershipResolver` that uses an `NftStakingClient` to
|
|
287
|
+
* walk the wallet's stake entries and only allow mints currently staked in
|
|
288
|
+
* `poolId`. Wire this into `prepareTraitProof({ validateOwnership })` if you
|
|
289
|
+
* don't already have your own stake index.
|
|
290
|
+
*/
|
|
291
|
+
export declare function ownershipFromClient(client: {
|
|
292
|
+
fetchStakeEntriesByOwner: (poolId: number, wallet: PkInput) => Promise<Array<{
|
|
293
|
+
nftMint: {
|
|
294
|
+
toBase58(): string;
|
|
295
|
+
};
|
|
296
|
+
}>>;
|
|
297
|
+
}): StakedOwnershipResolver;
|
|
99
298
|
//# sourceMappingURL=traitProof.d.ts.map
|
package/dist/traitProof.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"traitProof.d.ts","sourceRoot":"","sources":["../src/traitProof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAElE,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAcrC;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;IACf,cAAc,EAAE,OAAO,CAAA;IACvB,8DAA8D;IAC9D,YAAY,EAAE,OAAO,CAAA;CACtB,GAAG,MAAM,CAOT;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH
|
|
1
|
+
{"version":3,"file":"traitProof.d.ts","sourceRoot":"","sources":["../src/traitProof.ts"],"names":[],"mappings":"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"}
|
package/dist/traitProof.js
CHANGED
|
@@ -44,64 +44,67 @@ export function buildTraitProofMessage(params) {
|
|
|
44
44
|
u64LE(params.totalClaimed).copy(msg, 72);
|
|
45
45
|
return msg;
|
|
46
46
|
}
|
|
47
|
+
function normalizeKey(s) {
|
|
48
|
+
return s.trim().toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
function bonusFromCatalogEntry(entry, traitBonusMode) {
|
|
51
|
+
const bps = BigInt(entry.bonusBps ?? 0);
|
|
52
|
+
const extra = BigInt(entry.fixedExtra ?? 0);
|
|
53
|
+
if (bps !== 0n && extra !== 0n) {
|
|
54
|
+
throw new Error(`computeTraitBonusRate: catalog entry "${entry.traitType}=${entry.value}" sets both bonusBps and fixedExtra; pick one.`);
|
|
55
|
+
}
|
|
56
|
+
if (traitBonusMode === 1) {
|
|
57
|
+
if (bps !== 0n) {
|
|
58
|
+
throw new Error(`computeTraitBonusRate: traitBonusMode = FixedExtra but catalog entry "${entry.traitType}=${entry.value}" uses bonusBps. Use fixedExtra (raw reward per interval).`);
|
|
59
|
+
}
|
|
60
|
+
return extra;
|
|
61
|
+
}
|
|
62
|
+
if (extra !== 0n) {
|
|
63
|
+
throw new Error(`computeTraitBonusRate: traitBonusMode = BaseMultiplier but catalog entry "${entry.traitType}=${entry.value}" uses fixedExtra. Use bonusBps (10_000 = +100%).`);
|
|
64
|
+
}
|
|
65
|
+
return bps;
|
|
66
|
+
}
|
|
67
|
+
export function computeTraitBonusRate(params) {
|
|
68
|
+
if (params.traitBonusMode === 0) {
|
|
69
|
+
return { traitBonusRate: 0n, perNft: [] };
|
|
70
|
+
}
|
|
71
|
+
const mode = params.traitBonusMode;
|
|
72
|
+
// Build a lookup keyed by normalized "type::value".
|
|
73
|
+
// Validates every catalog entry up-front so misconfigured entries are
|
|
74
|
+
// surfaced even if no NFT references them.
|
|
75
|
+
const lookup = new Map();
|
|
76
|
+
for (const entry of params.catalog) {
|
|
77
|
+
const key = `${normalizeKey(entry.traitType)}::${normalizeKey(entry.value)}`;
|
|
78
|
+
if (lookup.has(key)) {
|
|
79
|
+
throw new Error(`computeTraitBonusRate: duplicate catalog entry for "${entry.traitType}=${entry.value}".`);
|
|
80
|
+
}
|
|
81
|
+
lookup.set(key, bonusFromCatalogEntry(entry, mode));
|
|
82
|
+
}
|
|
83
|
+
let total = 0n;
|
|
84
|
+
const perNft = [];
|
|
85
|
+
for (const nft of params.nfts) {
|
|
86
|
+
let nftBonus = 0n;
|
|
87
|
+
let matched = 0;
|
|
88
|
+
for (const trait of nft.traits) {
|
|
89
|
+
const hit = lookup.get(`${normalizeKey(trait.traitType)}::${normalizeKey(trait.value)}`);
|
|
90
|
+
if (hit === undefined || hit === 0n)
|
|
91
|
+
continue;
|
|
92
|
+
nftBonus += hit;
|
|
93
|
+
matched += 1;
|
|
94
|
+
}
|
|
95
|
+
total += nftBonus;
|
|
96
|
+
perNft.push({
|
|
97
|
+
mint: typeof nft.mint === "string" ? nft.mint : toPk(nft.mint).toBase58(),
|
|
98
|
+
bonus: nftBonus,
|
|
99
|
+
matchedTraits: matched,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return { traitBonusRate: total, perNft };
|
|
103
|
+
}
|
|
47
104
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* import nacl from "tweetnacl"
|
|
52
|
-
* import { Ed25519Program } from "@solana/web3.js"
|
|
53
|
-
* import { buildTraitProofMessage } from "@soltracer/nft-staking"
|
|
54
|
-
*
|
|
55
|
-
* const message = buildTraitProofMessage({
|
|
56
|
-
* pool,
|
|
57
|
-
* wallet: staker,
|
|
58
|
-
* traitBonusRate,
|
|
59
|
-
* totalClaimed: stakerAccount.totalClaimed,
|
|
60
|
-
* })
|
|
61
|
-
* const signature = nacl.sign.detached(message, traitAuthority.secretKey)
|
|
62
|
-
* const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({
|
|
63
|
-
* publicKey: traitAuthority.publicKey.toBytes(),
|
|
64
|
-
* message,
|
|
65
|
-
* signature,
|
|
66
|
-
* })
|
|
67
|
-
* const claimIx = await client.claimRewards(poolId, { traitBonusRate })
|
|
68
|
-
* await provider.sendAndConfirm(new Transaction().add(ed25519Ix, claimIx))
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
/**
|
|
72
|
-
* Mode-aware trait-bonus rate aggregator.
|
|
73
|
-
*
|
|
74
|
-
* The on-chain `claim_rewards` instruction accepts a **single** `u64`
|
|
75
|
-
* `traitBonusRate` per claim — the chain knows nothing about per-trait
|
|
76
|
-
* granularity. Aggregation across N staked NFTs (each with M traits) is the
|
|
77
|
-
* caller's responsibility, and the math depends on the pool's
|
|
78
|
-
* `rewardConfig.traitBonusMode`:
|
|
79
|
-
*
|
|
80
|
-
* - `0` / `None` — proofs disabled. Any non-zero rate is rejected.
|
|
81
|
-
* - `1` / `FixedExtra` — `traitBonusRate` is **absolute extra reward per
|
|
82
|
-
* `rateInterval`** added to the staker's effective rate for the claim
|
|
83
|
-
* accrual window. Sum per-trait fixed bonuses across every active staked
|
|
84
|
-
* NFT (and every bonus-bearing trait on each).
|
|
85
|
-
* - `2` / `BaseMultiplier` — `traitBonusRate` is **bonus BPS** applied to the
|
|
86
|
-
* staker's CURRENT `effective_rate` (already includes lock-tier rates).
|
|
87
|
-
* `10_000` = +100% (2x base for the window). Sum per-trait BPS bonuses the
|
|
88
|
-
* same way — the bonus is a scalar over the whole staker rate.
|
|
89
|
-
*
|
|
90
|
-
* `bonusBps` and `fixedExtra` fields are mutually exclusive; mixing both in
|
|
91
|
-
* one trait entry throws. Traits without bonuses can be omitted.
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* ```ts
|
|
95
|
-
* // Pool is in BaseMultiplier mode. Each staked NFT exposes its trait list.
|
|
96
|
-
* const traits = [
|
|
97
|
-
* // NFT 1 — "Gold" head (+5%), "Laser Eyes" (+10%)
|
|
98
|
-
* { bonusBps: 500 }, { bonusBps: 1000 },
|
|
99
|
-
* // NFT 2 — "Mythic Background" (+25%)
|
|
100
|
-
* { bonusBps: 2500 },
|
|
101
|
-
* ]
|
|
102
|
-
* const rate = aggregateTraitBonusRate(traits, 2) // → 4000 (i.e. +40% for the window)
|
|
103
|
-
* const message = buildTraitProofMessage({ pool, wallet, traitBonusRate: rate, totalClaimed })
|
|
104
|
-
* ```
|
|
105
|
+
* @deprecated Use `computeTraitBonusRate` instead — it accepts the real per-NFT
|
|
106
|
+
* trait input and a bonus catalog, mirroring the backend flow. Retained for
|
|
107
|
+
* call sites that already pre-resolved a flat bonus array.
|
|
105
108
|
*/
|
|
106
109
|
export function aggregateTraitBonusRate(traits, traitBonusMode) {
|
|
107
110
|
if (traitBonusMode === 0) {
|
|
@@ -118,14 +121,12 @@ export function aggregateTraitBonusRate(traits, traitBonusMode) {
|
|
|
118
121
|
throw new Error("aggregateTraitBonusRate: a single trait must set bonusBps OR fixedExtra, not both.");
|
|
119
122
|
}
|
|
120
123
|
if (traitBonusMode === 1) {
|
|
121
|
-
// FixedExtra — sum absolute per-interval bonuses.
|
|
122
124
|
if (bps !== 0n) {
|
|
123
125
|
throw new Error("aggregateTraitBonusRate: traitBonusMode = FixedExtra expects fixedExtra (per-interval reward), not bonusBps.");
|
|
124
126
|
}
|
|
125
127
|
sum += extra;
|
|
126
128
|
}
|
|
127
129
|
else {
|
|
128
|
-
// BaseMultiplier — sum BPS bonuses.
|
|
129
130
|
if (extra !== 0n) {
|
|
130
131
|
throw new Error("aggregateTraitBonusRate: traitBonusMode = BaseMultiplier expects bonusBps (10_000 = +100%), not fixedExtra.");
|
|
131
132
|
}
|
|
@@ -134,4 +135,105 @@ export function aggregateTraitBonusRate(traits, traitBonusMode) {
|
|
|
134
135
|
}
|
|
135
136
|
return sum;
|
|
136
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* End-to-end backend orchestrator for the trait-proof flow.
|
|
140
|
+
*
|
|
141
|
+
* Combines the four user-supplied dependencies — ownership validation, trait
|
|
142
|
+
* resolution, the bonus-catalog store, and the on-chain staker nonce — into
|
|
143
|
+
* the single signable payload the on-chain claim instruction expects.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* import {
|
|
148
|
+
* prepareTraitProof,
|
|
149
|
+
* type TraitBonusStore,
|
|
150
|
+
* } from "@soltracer/nft-staking"
|
|
151
|
+
*
|
|
152
|
+
* // 1) Implement the store against your DB once (any DB works).
|
|
153
|
+
* const traitBonusStore: TraitBonusStore = {
|
|
154
|
+
* async list({ poolId, projectId }) {
|
|
155
|
+
* const rows = await prisma.traitBonusConfig.findMany({
|
|
156
|
+
* where: { poolId, ...(projectId ? { projectId: String(projectId) } : {}) },
|
|
157
|
+
* })
|
|
158
|
+
* return rows.map((r) => ({
|
|
159
|
+
* traitType: r.traitType,
|
|
160
|
+
* value: r.traitValue,
|
|
161
|
+
* // pool is BaseMultiplier? use bonusBps. FixedExtra? use fixedExtra.
|
|
162
|
+
* bonusBps: r.bonusAmount,
|
|
163
|
+
* }))
|
|
164
|
+
* },
|
|
165
|
+
* }
|
|
166
|
+
*
|
|
167
|
+
* // 2) Prepare the proof on each claim request.
|
|
168
|
+
* const prepared = await prepareTraitProof({
|
|
169
|
+
* client, // NftStakingClient
|
|
170
|
+
* pool, // already-fetched StakePool (has rewardConfig + address)
|
|
171
|
+
* wallet: req.wallet,
|
|
172
|
+
* requestedMints: req.mints,
|
|
173
|
+
* store: traitBonusStore,
|
|
174
|
+
* resolveTraits: (mint) => catalog.getTraits(mint),
|
|
175
|
+
* })
|
|
176
|
+
*
|
|
177
|
+
* // 3) Sign and respond.
|
|
178
|
+
* const signature = nacl.sign.detached(prepared.message, traitAuthority.secretKey)
|
|
179
|
+
* return {
|
|
180
|
+
* traitBonusRate: prepared.traitBonusRate.toString(),
|
|
181
|
+
* signature: Buffer.from(signature).toString("base64"),
|
|
182
|
+
* perNft: prepared.perNft,
|
|
183
|
+
* rejectedMints: prepared.rejectedMints,
|
|
184
|
+
* }
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export async function prepareTraitProof(params) {
|
|
188
|
+
const wallet = toPk(params.wallet);
|
|
189
|
+
const poolPk = toPk(params.pool.address);
|
|
190
|
+
const mode = params.pool.rewardConfig.traitBonusMode;
|
|
191
|
+
// 1. Ownership / stake validation — drop anything the user does not actually have staked.
|
|
192
|
+
const validated = await params.validateOwnership({
|
|
193
|
+
wallet: wallet.toBase58(),
|
|
194
|
+
poolId: params.poolId ?? 0,
|
|
195
|
+
requestedMints: params.requestedMints,
|
|
196
|
+
});
|
|
197
|
+
const validatedSet = new Set(validated);
|
|
198
|
+
const validatedMints = params.requestedMints.filter((m) => validatedSet.has(m));
|
|
199
|
+
const rejectedMints = params.requestedMints.filter((m) => !validatedSet.has(m));
|
|
200
|
+
// 2 + 3. Resolve traits for each validated mint and load the bonus catalog.
|
|
201
|
+
const [nftTraits, catalog] = await Promise.all([
|
|
202
|
+
Promise.all(validatedMints.map(async (mint) => ({
|
|
203
|
+
mint,
|
|
204
|
+
traits: await params.resolveTraits(mint),
|
|
205
|
+
}))),
|
|
206
|
+
params.store.list({
|
|
207
|
+
poolId: params.poolId ?? 0,
|
|
208
|
+
projectId: params.projectId,
|
|
209
|
+
}),
|
|
210
|
+
]);
|
|
211
|
+
// 4. Mode-aware aggregation.
|
|
212
|
+
const { traitBonusRate, perNft } = computeTraitBonusRate({
|
|
213
|
+
traitBonusMode: mode,
|
|
214
|
+
catalog,
|
|
215
|
+
nfts: nftTraits,
|
|
216
|
+
});
|
|
217
|
+
// 5. Build the signable 80-byte message.
|
|
218
|
+
const message = buildTraitProofMessage({
|
|
219
|
+
pool: poolPk,
|
|
220
|
+
wallet,
|
|
221
|
+
traitBonusRate,
|
|
222
|
+
totalClaimed: params.totalClaimed,
|
|
223
|
+
});
|
|
224
|
+
return { traitBonusRate, validatedMints, rejectedMints, perNft, message };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Convenience `StakedOwnershipResolver` that uses an `NftStakingClient` to
|
|
228
|
+
* walk the wallet's stake entries and only allow mints currently staked in
|
|
229
|
+
* `poolId`. Wire this into `prepareTraitProof({ validateOwnership })` if you
|
|
230
|
+
* don't already have your own stake index.
|
|
231
|
+
*/
|
|
232
|
+
export function ownershipFromClient(client) {
|
|
233
|
+
return async ({ wallet, poolId, requestedMints }) => {
|
|
234
|
+
const entries = await client.fetchStakeEntriesByOwner(poolId, wallet);
|
|
235
|
+
const staked = new Set(entries.map((e) => e.nftMint.toBase58()));
|
|
236
|
+
return requestedMints.filter((m) => staked.has(m));
|
|
237
|
+
};
|
|
238
|
+
}
|
|
137
239
|
//# sourceMappingURL=traitProof.js.map
|
package/dist/traitProof.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"traitProof.js","sourceRoot":"","sources":["../src/traitProof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,IAAI,EAA8B,MAAM,iBAAiB,CAAA;AAElE,sEAAsE;AACtE,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAErC,SAAS,KAAK,CAAC,KAAc;IAC3B,MAAM,EAAE,GACN,OAAO,KAAK,KAAK,QAAQ;QACvB,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ;YACzB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YACf,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAA;IAChC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC3B,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;IACxB,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAMtC;IACC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAA;IAC7C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;IACzC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAC5C,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAC1C,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IACxC,OAAO,GAAG,CAAA;AACZ,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"traitProof.js","sourceRoot":"","sources":["../src/traitProof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,IAAI,EAA8B,MAAM,iBAAiB,CAAA;AAElE,sEAAsE;AACtE,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAErC,SAAS,KAAK,CAAC,KAAc;IAC3B,MAAM,EAAE,GACN,OAAO,KAAK,KAAK,QAAQ;QACvB,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ;YACzB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YACf,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAA;IAChC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC3B,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;IACxB,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAMtC;IACC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAA;IAC7C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;IACzC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAC5C,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAC1C,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IACxC,OAAO,GAAG,CAAA;AACZ,CAAC;AA4GD,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;AAC/B,CAAC;AAED,SAAS,qBAAqB,CAC5B,KAA6B,EAC7B,cAAqB;IAErB,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAA;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC,CAAA;IAC3C,IAAI,GAAG,KAAK,EAAE,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,yCAAyC,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,KAAK,gDAAgD,CACxH,CAAA;IACH,CAAC;IACD,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,yEAAyE,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,KAAK,4DAA4D,CACpK,CAAA;QACH,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,6EAA6E,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,KAAK,mDAAmD,CAC/J,CAAA;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAIrC;IACC,IAAI,MAAM,CAAC,cAAc,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;IAC3C,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,CAAA;IAElC,oDAAoD;IACpD,sEAAsE;IACtE,2CAA2C;IAC3C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAA;QAC5E,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACb,uDAAuD,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,KAAK,IAAI,CAC1F,CAAA;QACH,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,qBAAqB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;IACrD,CAAC;IAED,IAAI,KAAK,GAAG,EAAE,CAAA;IACd,MAAM,MAAM,GAAsC,EAAE,CAAA;IACpD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAC9B,IAAI,QAAQ,GAAG,EAAE,CAAA;QACjB,IAAI,OAAO,GAAG,CAAC,CAAA;QACf,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;YAC/B,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YACxF,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;gBAAE,SAAQ;YAC7C,QAAQ,IAAI,GAAG,CAAA;YACf,OAAO,IAAI,CAAC,CAAA;QACd,CAAC;QACD,KAAK,IAAI,QAAQ,CAAA;QACjB,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;YACzE,KAAK,EAAE,QAAQ;YACf,aAAa,EAAE,OAAO;SACvB,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAmF,EACnF,cAAyB;IAEzB,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAC7E,MAAM,IAAI,KAAK,CACb,uGAAuG,CACxG,CAAA;QACH,CAAC;QACD,OAAO,EAAE,CAAA;IACX,CAAC;IACD,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAA;QACnC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,CAAA;QACvC,IAAI,GAAG,KAAK,EAAE,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CACb,oFAAoF,CACrF,CAAA;QACH,CAAC;QACD,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;YACzB,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CACb,8GAA8G,CAC/G,CAAA;YACH,CAAC;YACD,GAAG,IAAI,KAAK,CAAA;QACd,CAAC;aAAM,CAAC;YACN,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CACb,6GAA6G,CAC9G,CAAA;YACH,CAAC;YACD,GAAG,IAAI,GAAG,CAAA;QACZ,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MA2BvC;IACC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,cAA2B,CAAA;IAEjE,0FAA0F;IAC1F,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC;QAC/C,MAAM,EAAE,MAAM,CAAC,QAAQ,EAAE;QACzB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC;QAC1B,cAAc,EAAE,MAAM,CAAC,cAAc;KACtC,CAAC,CAAA;IACF,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAA;IACvC,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/E,MAAM,aAAa,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAE/E,4EAA4E;IAC5E,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC7C,OAAO,CAAC,GAAG,CACT,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;YAClC,IAAI;YACJ,MAAM,EAAE,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;SACzC,CAAC,CAAC,CACJ;QACD,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;YAChB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC;YAC1B,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC;KACH,CAAC,CAAA;IAEF,6BAA6B;IAC7B,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,qBAAqB,CAAC;QACvD,cAAc,EAAE,IAAI;QACpB,OAAO;QACP,IAAI,EAAE,SAAS;KAChB,CAAC,CAAA;IAEF,yCAAyC;IACzC,MAAM,OAAO,GAAG,sBAAsB,CAAC;QACrC,IAAI,EAAE,MAAM;QACZ,MAAM;QACN,cAAc;QACd,YAAY,EAAE,MAAM,CAAC,YAAY;KAClC,CAAC,CAAA;IAEF,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;AAC3E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAKC;IAED,OAAO,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE;QAClD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,wBAAwB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACrE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;QAChE,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IACpD,CAAC,CAAA;AACH,CAAC"}
|