diff --git a/Cargo.lock b/Cargo.lock index f56974b..b888417 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3308,6 +3308,7 @@ name = "mpl-agent-reputation-program" version = "0.1.0" dependencies = [ "bytemuck", + "mpl-bubblegum", "mpl-core", "mpl-utils", "num-derive", diff --git a/clients/js/src/generated/reputation/accounts/index.ts b/clients/js/src/generated/reputation/accounts/index.ts index c141086..fc2ad80 100644 --- a/clients/js/src/generated/reputation/accounts/index.ts +++ b/clients/js/src/generated/reputation/accounts/index.ts @@ -6,4 +6,5 @@ * @see https://github.com/metaplex-foundation/kinobi */ -export * from './agentReputationV1'; +export * from './reviewRecordV1'; +export * from './standalonePdas'; diff --git a/clients/js/src/generated/reputation/accounts/agentReputationV1.ts b/clients/js/src/generated/reputation/accounts/reviewRecordV1.ts similarity index 55% rename from clients/js/src/generated/reputation/accounts/agentReputationV1.ts rename to clients/js/src/generated/reputation/accounts/reviewRecordV1.ts index dd81a30..a536dd7 100644 --- a/clients/js/src/generated/reputation/accounts/agentReputationV1.ts +++ b/clients/js/src/generated/reputation/accounts/reviewRecordV1.ts @@ -30,102 +30,100 @@ import { } from '@metaplex-foundation/umi/serializers'; import { Key, KeyArgs, getKeySerializer } from '../types'; -export type AgentReputationV1 = Account; +export type ReviewRecordV1 = Account; -export type AgentReputationV1AccountData = { +export type ReviewRecordV1AccountData = { key: Key; bump: number; padding: Array; - asset: PublicKey; + reviewer: PublicKey; + receiptAssetId: PublicKey; }; -export type AgentReputationV1AccountDataArgs = { +export type ReviewRecordV1AccountDataArgs = { key: KeyArgs; bump: number; - asset: PublicKey; + reviewer: PublicKey; + receiptAssetId: PublicKey; }; -export function getAgentReputationV1AccountDataSerializer(): Serializer< - AgentReputationV1AccountDataArgs, - AgentReputationV1AccountData +export function getReviewRecordV1AccountDataSerializer(): Serializer< + ReviewRecordV1AccountDataArgs, + ReviewRecordV1AccountData > { return mapSerializer< - AgentReputationV1AccountDataArgs, + ReviewRecordV1AccountDataArgs, any, - AgentReputationV1AccountData + ReviewRecordV1AccountData >( - struct( + struct( [ ['key', getKeySerializer()], ['bump', u8()], ['padding', array(u8(), { size: 6 })], - ['asset', publicKeySerializer()], + ['reviewer', publicKeySerializer()], + ['receiptAssetId', publicKeySerializer()], ], - { description: 'AgentReputationV1AccountData' } + { description: 'ReviewRecordV1AccountData' } ), (value) => ({ ...value, padding: [0, 0, 0, 0, 0, 0] }) - ) as Serializer< - AgentReputationV1AccountDataArgs, - AgentReputationV1AccountData - >; + ) as Serializer; } -export function deserializeAgentReputationV1( +export function deserializeReviewRecordV1( rawAccount: RpcAccount -): AgentReputationV1 { +): ReviewRecordV1 { return deserializeAccount( rawAccount, - getAgentReputationV1AccountDataSerializer() + getReviewRecordV1AccountDataSerializer() ); } -export async function fetchAgentReputationV1( +export async function fetchReviewRecordV1( context: Pick, publicKey: PublicKey | Pda, options?: RpcGetAccountOptions -): Promise { +): Promise { const maybeAccount = await context.rpc.getAccount( toPublicKey(publicKey, false), options ); - assertAccountExists(maybeAccount, 'AgentReputationV1'); - return deserializeAgentReputationV1(maybeAccount); + assertAccountExists(maybeAccount, 'ReviewRecordV1'); + return deserializeReviewRecordV1(maybeAccount); } -export async function safeFetchAgentReputationV1( +export async function safeFetchReviewRecordV1( context: Pick, publicKey: PublicKey | Pda, options?: RpcGetAccountOptions -): Promise { +): Promise { const maybeAccount = await context.rpc.getAccount( toPublicKey(publicKey, false), options ); - return maybeAccount.exists - ? deserializeAgentReputationV1(maybeAccount) - : null; + return maybeAccount.exists ? deserializeReviewRecordV1(maybeAccount) : null; } -export async function fetchAllAgentReputationV1( +export async function fetchAllReviewRecordV1( context: Pick, publicKeys: Array, options?: RpcGetAccountsOptions -): Promise { +): Promise { const maybeAccounts = await context.rpc.getAccounts( publicKeys.map((key) => toPublicKey(key, false)), options ); return maybeAccounts.map((maybeAccount) => { - assertAccountExists(maybeAccount, 'AgentReputationV1'); - return deserializeAgentReputationV1(maybeAccount); + assertAccountExists(maybeAccount, 'ReviewRecordV1'); + return deserializeReviewRecordV1(maybeAccount); }); } -export async function safeFetchAllAgentReputationV1( +export async function safeFetchAllReviewRecordV1( context: Pick, publicKeys: Array, options?: RpcGetAccountsOptions -): Promise { +): Promise { const maybeAccounts = await context.rpc.getAccounts( publicKeys.map((key) => toPublicKey(key, false)), options @@ -133,11 +131,11 @@ export async function safeFetchAllAgentReputationV1( return maybeAccounts .filter((maybeAccount) => maybeAccount.exists) .map((maybeAccount) => - deserializeAgentReputationV1(maybeAccount as RpcAccount) + deserializeReviewRecordV1(maybeAccount as RpcAccount) ); } -export function getAgentReputationV1GpaBuilder( +export function getReviewRecordV1GpaBuilder( context: Pick ) { const programId = context.programs.getPublicKey( @@ -149,27 +147,29 @@ export function getAgentReputationV1GpaBuilder( key: KeyArgs; bump: number; padding: Array; - asset: PublicKey; + reviewer: PublicKey; + receiptAssetId: PublicKey; }>({ key: [0, getKeySerializer()], bump: [1, u8()], padding: [2, array(u8(), { size: 6 })], - asset: [8, publicKeySerializer()], + reviewer: [8, publicKeySerializer()], + receiptAssetId: [40, publicKeySerializer()], }) - .deserializeUsing((account) => - deserializeAgentReputationV1(account) + .deserializeUsing((account) => + deserializeReviewRecordV1(account) ); } -export function getAgentReputationV1Size(): number { - return 40; +export function getReviewRecordV1Size(): number { + return 72; } -export function findAgentReputationV1Pda( +export function findReviewRecordV1Pda( context: Pick, seeds: { - /** The address of the asset */ - asset: PublicKey; + /** Bubblegum asset id of the work receipt */ + receiptAssetId: PublicKey; } ): Pda { const programId = context.programs.getPublicKey( @@ -177,31 +177,31 @@ export function findAgentReputationV1Pda( 'REPREG5c1gPHuHukEyANpksLdHFaJCiTrm6zJgNhRZR' ); return context.eddsa.findPda(programId, [ - string({ size: 'variable' }).serialize('agent_reputation'), - publicKeySerializer().serialize(seeds.asset), + string({ size: 'variable' }).serialize('review_record'), + publicKeySerializer().serialize(seeds.receiptAssetId), ]); } -export async function fetchAgentReputationV1FromSeeds( +export async function fetchReviewRecordV1FromSeeds( context: Pick, - seeds: Parameters[1], + seeds: Parameters[1], options?: RpcGetAccountOptions -): Promise { - return fetchAgentReputationV1( +): Promise { + return fetchReviewRecordV1( context, - findAgentReputationV1Pda(context, seeds), + findReviewRecordV1Pda(context, seeds), options ); } -export async function safeFetchAgentReputationV1FromSeeds( +export async function safeFetchReviewRecordV1FromSeeds( context: Pick, - seeds: Parameters[1], + seeds: Parameters[1], options?: RpcGetAccountOptions -): Promise { - return safeFetchAgentReputationV1( +): Promise { + return safeFetchReviewRecordV1( context, - findAgentReputationV1Pda(context, seeds), + findReviewRecordV1Pda(context, seeds), options ); } diff --git a/clients/js/src/generated/reputation/accounts/standalonePdas.ts b/clients/js/src/generated/reputation/accounts/standalonePdas.ts new file mode 100644 index 0000000..341ef86 --- /dev/null +++ b/clients/js/src/generated/reputation/accounts/standalonePdas.ts @@ -0,0 +1,47 @@ +/** + * Hand-written PDA helpers for standalone PDAs (collections, authority, + * trees). Emitted by the kinobi-reputation config because kinobi 1.0-alpha + * doesn't render find*Pda helpers for PDAs added via addPdasVisitor. + */ + +import { Context, Pda } from '@metaplex-foundation/umi'; +import { string, u64 } from '@metaplex-foundation/umi/serializers'; + +const PROGRAM_ID = 'REPREG5c1gPHuHukEyANpksLdHFaJCiTrm6zJgNhRZR'; + +function pda( + context: Pick, + seeds: Uint8Array[] +): Pda { + const programId = context.programs.getPublicKey( + 'mplAgentReputation', + PROGRAM_ID + ); + return context.eddsa.findPda(programId, seeds); +} + +export function findReviewsCollectionPda( + context: Pick +): Pda { + return pda(context, [ + string({ size: 'variable' }).serialize('reviews_collection'), + ]); +} + +export function findReviewsAuthorityPda( + context: Pick +): Pda { + return pda(context, [ + string({ size: 'variable' }).serialize('reviews_authority'), + ]); +} + +export function findReviewsTreePda( + context: Pick, + seeds: { treeIndex: number | bigint } +): Pda { + return pda(context, [ + string({ size: 'variable' }).serialize('reviews_tree'), + u64().serialize(seeds.treeIndex), + ]); +} diff --git a/clients/js/src/generated/reputation/errors/mplAgentReputation.ts b/clients/js/src/generated/reputation/errors/mplAgentReputation.ts index b758e1a..38d5aeb 100644 --- a/clients/js/src/generated/reputation/errors/mplAgentReputation.ts +++ b/clients/js/src/generated/reputation/errors/mplAgentReputation.ts @@ -80,20 +80,181 @@ export class InvalidCoreAssetError extends ProgramError { codeToErrorMap.set(0x4, InvalidCoreAssetError); nameToErrorMap.set('InvalidCoreAsset', InvalidCoreAssetError); -/** AgentReputationAlreadyRegistered: Agent Reputation already registered */ -export class AgentReputationAlreadyRegisteredError extends ProgramError { - override readonly name: string = 'AgentReputationAlreadyRegistered'; +/** InvalidReviewRating: Invalid review rating (must be 1..=5) */ +export class InvalidReviewRatingError extends ProgramError { + override readonly name: string = 'InvalidReviewRating'; readonly code: number = 0x5; // 5 constructor(program: Program, cause?: Error) { - super('Agent Reputation already registered', program, cause); + super('Invalid review rating (must be 1..=5)', program, cause); } } -codeToErrorMap.set(0x5, AgentReputationAlreadyRegisteredError); +codeToErrorMap.set(0x5, InvalidReviewRatingError); +nameToErrorMap.set('InvalidReviewRating', InvalidReviewRatingError); + +/** FeedbackUriInvalid: Feedback URI must be non-empty and within size limits */ +export class FeedbackUriInvalidError extends ProgramError { + override readonly name: string = 'FeedbackUriInvalid'; + + readonly code: number = 0x6; // 6 + + constructor(program: Program, cause?: Error) { + super( + 'Feedback URI must be non-empty and within size limits', + program, + cause + ); + } +} +codeToErrorMap.set(0x6, FeedbackUriInvalidError); +nameToErrorMap.set('FeedbackUriInvalid', FeedbackUriInvalidError); + +/** LeafOwnerMismatch: Leaf owner does not match the reviewed asset owner */ +export class LeafOwnerMismatchError extends ProgramError { + override readonly name: string = 'LeafOwnerMismatch'; + + readonly code: number = 0x7; // 7 + + constructor(program: Program, cause?: Error) { + super('Leaf owner does not match the reviewed asset owner', program, cause); + } +} +codeToErrorMap.set(0x7, LeafOwnerMismatchError); +nameToErrorMap.set('LeafOwnerMismatch', LeafOwnerMismatchError); + +/** InvalidBubblegumProgram: Invalid Bubblegum Program */ +export class InvalidBubblegumProgramError extends ProgramError { + override readonly name: string = 'InvalidBubblegumProgram'; + + readonly code: number = 0x8; // 8 + + constructor(program: Program, cause?: Error) { + super('Invalid Bubblegum Program', program, cause); + } +} +codeToErrorMap.set(0x8, InvalidBubblegumProgramError); +nameToErrorMap.set('InvalidBubblegumProgram', InvalidBubblegumProgramError); + +/** InvalidCompressionProgram: Invalid Compression Program */ +export class InvalidCompressionProgramError extends ProgramError { + override readonly name: string = 'InvalidCompressionProgram'; + + readonly code: number = 0x9; // 9 + + constructor(program: Program, cause?: Error) { + super('Invalid Compression Program', program, cause); + } +} +codeToErrorMap.set(0x9, InvalidCompressionProgramError); +nameToErrorMap.set('InvalidCompressionProgram', InvalidCompressionProgramError); + +/** ReviewAlreadyExists: A review already exists for this work receipt */ +export class ReviewAlreadyExistsError extends ProgramError { + override readonly name: string = 'ReviewAlreadyExists'; + + readonly code: number = 0xa; // 10 + + constructor(program: Program, cause?: Error) { + super('A review already exists for this work receipt', program, cause); + } +} +codeToErrorMap.set(0xa, ReviewAlreadyExistsError); +nameToErrorMap.set('ReviewAlreadyExists', ReviewAlreadyExistsError); + +/** InvalidReviewsCollection: Invalid reviews collection PDA derivation */ +export class InvalidReviewsCollectionError extends ProgramError { + override readonly name: string = 'InvalidReviewsCollection'; + + readonly code: number = 0xb; // 11 + + constructor(program: Program, cause?: Error) { + super('Invalid reviews collection PDA derivation', program, cause); + } +} +codeToErrorMap.set(0xb, InvalidReviewsCollectionError); +nameToErrorMap.set('InvalidReviewsCollection', InvalidReviewsCollectionError); + +/** InvalidReviewsAuthority: Invalid reviews authority PDA derivation */ +export class InvalidReviewsAuthorityError extends ProgramError { + override readonly name: string = 'InvalidReviewsAuthority'; + + readonly code: number = 0xc; // 12 + + constructor(program: Program, cause?: Error) { + super('Invalid reviews authority PDA derivation', program, cause); + } +} +codeToErrorMap.set(0xc, InvalidReviewsAuthorityError); +nameToErrorMap.set('InvalidReviewsAuthority', InvalidReviewsAuthorityError); + +/** ReviewsCollectionAlreadyInitialized: Reviews collection already initialized */ +export class ReviewsCollectionAlreadyInitializedError extends ProgramError { + override readonly name: string = 'ReviewsCollectionAlreadyInitialized'; + + readonly code: number = 0xd; // 13 + + constructor(program: Program, cause?: Error) { + super('Reviews collection already initialized', program, cause); + } +} +codeToErrorMap.set(0xd, ReviewsCollectionAlreadyInitializedError); +nameToErrorMap.set( + 'ReviewsCollectionAlreadyInitialized', + ReviewsCollectionAlreadyInitializedError +); + +/** InvalidReviewsTreeDerivation: Invalid reviews tree PDA derivation */ +export class InvalidReviewsTreeDerivationError extends ProgramError { + override readonly name: string = 'InvalidReviewsTreeDerivation'; + + readonly code: number = 0xe; // 14 + + constructor(program: Program, cause?: Error) { + super('Invalid reviews tree PDA derivation', program, cause); + } +} +codeToErrorMap.set(0xe, InvalidReviewsTreeDerivationError); +nameToErrorMap.set( + 'InvalidReviewsTreeDerivation', + InvalidReviewsTreeDerivationError +); + +/** InvalidReceiptsCollection: Supplied receipts collection is not the canonical mpl-agent-tools receipts collection PDA */ +export class InvalidReceiptsCollectionError extends ProgramError { + override readonly name: string = 'InvalidReceiptsCollection'; + + readonly code: number = 0xf; // 15 + + constructor(program: Program, cause?: Error) { + super( + 'Supplied receipts collection is not the canonical mpl-agent-tools receipts collection PDA', + program, + cause + ); + } +} +codeToErrorMap.set(0xf, InvalidReceiptsCollectionError); +nameToErrorMap.set('InvalidReceiptsCollection', InvalidReceiptsCollectionError); + +/** InvalidReceiptsTreeDerivation: Supplied receipts merkle tree is not the canonical mpl-agent-tools receipts tree PDA */ +export class InvalidReceiptsTreeDerivationError extends ProgramError { + override readonly name: string = 'InvalidReceiptsTreeDerivation'; + + readonly code: number = 0x10; // 16 + + constructor(program: Program, cause?: Error) { + super( + 'Supplied receipts merkle tree is not the canonical mpl-agent-tools receipts tree PDA', + program, + cause + ); + } +} +codeToErrorMap.set(0x10, InvalidReceiptsTreeDerivationError); nameToErrorMap.set( - 'AgentReputationAlreadyRegistered', - AgentReputationAlreadyRegisteredError + 'InvalidReceiptsTreeDerivation', + InvalidReceiptsTreeDerivationError ); /** diff --git a/clients/js/src/generated/reputation/instructions/registerReputationV1.ts b/clients/js/src/generated/reputation/instructions/createReviewsCollectionV1.ts similarity index 64% rename from clients/js/src/generated/reputation/instructions/registerReputationV1.ts rename to clients/js/src/generated/reputation/instructions/createReviewsCollectionV1.ts index 5fcf543..ffe36d7 100644 --- a/clients/js/src/generated/reputation/instructions/registerReputationV1.ts +++ b/clients/js/src/generated/reputation/instructions/createReviewsCollectionV1.ts @@ -21,26 +21,21 @@ import { struct, u8, } from '@metaplex-foundation/umi/serializers'; -import { findAgentReputationV1Pda } from '../accounts'; +import { findReviewsAuthorityPda, findReviewsCollectionPda } from '../accounts'; import { ResolvedAccount, ResolvedAccountsWithIndices, - expectPublicKey, getAccountMetasAndSigners, } from '../shared'; // Accounts. -export type RegisterReputationV1InstructionAccounts = { - /** The agent reputation PDA */ - agentReputation?: PublicKey | Pda; - /** The address of the Core asset */ - asset: PublicKey | Pda; - /** The address of the collection */ - collection?: PublicKey | Pda; - /** The payer for additional rent */ +export type CreateReviewsCollectionV1InstructionAccounts = { + /** Funds the collection's rent */ payer?: Signer; - /** Authority for the collection. If not provided, the payer will be used. */ - authority?: Signer; + /** Reviews collection PDA at ["reviews_collection"] */ + collection?: PublicKey | Pda; + /** Reviews authority PDA at ["reviews_authority"] — becomes the collection's update_authority */ + authority?: PublicKey | Pda; /** The MPL Core program */ mplCoreProgram?: PublicKey | Pda; /** The system program */ @@ -48,43 +43,43 @@ export type RegisterReputationV1InstructionAccounts = { }; // Data. -export type RegisterReputationV1InstructionData = { +export type CreateReviewsCollectionV1InstructionData = { discriminator: number; padding: Array; }; -export type RegisterReputationV1InstructionDataArgs = {}; +export type CreateReviewsCollectionV1InstructionDataArgs = {}; -export function getRegisterReputationV1InstructionDataSerializer(): Serializer< - RegisterReputationV1InstructionDataArgs, - RegisterReputationV1InstructionData +export function getCreateReviewsCollectionV1InstructionDataSerializer(): Serializer< + CreateReviewsCollectionV1InstructionDataArgs, + CreateReviewsCollectionV1InstructionData > { return mapSerializer< - RegisterReputationV1InstructionDataArgs, + CreateReviewsCollectionV1InstructionDataArgs, any, - RegisterReputationV1InstructionData + CreateReviewsCollectionV1InstructionData >( - struct( + struct( [ ['discriminator', u8()], ['padding', array(u8(), { size: 7 })], ], - { description: 'RegisterReputationV1InstructionData' } + { description: 'CreateReviewsCollectionV1InstructionData' } ), - (value) => ({ ...value, discriminator: 0, padding: [0, 0, 0, 0, 0, 0, 0] }) + (value) => ({ ...value, discriminator: 1, padding: [0, 0, 0, 0, 0, 0, 0] }) ) as Serializer< - RegisterReputationV1InstructionDataArgs, - RegisterReputationV1InstructionData + CreateReviewsCollectionV1InstructionDataArgs, + CreateReviewsCollectionV1InstructionData >; } // Instruction discriminator. -export const registerReputationV1InstructionDiscriminator = 0; +export const createReviewsCollectionV1InstructionDiscriminator = 1; // Instruction. -export function registerReputationV1( +export function createReviewsCollectionV1( context: Pick, - input: RegisterReputationV1InstructionAccounts + input: CreateReviewsCollectionV1InstructionAccounts ): TransactionBuilder { // Program ID. const programId = context.programs.getPublicKey( @@ -94,52 +89,43 @@ export function registerReputationV1( // Accounts. const resolvedAccounts = { - agentReputation: { + payer: { index: 0, isWritable: true as boolean, - value: input.agentReputation ?? null, - }, - asset: { - index: 1, - isWritable: true as boolean, - value: input.asset ?? null, + value: input.payer ?? null, }, collection: { - index: 2, + index: 1, isWritable: true as boolean, value: input.collection ?? null, }, - payer: { - index: 3, - isWritable: true as boolean, - value: input.payer ?? null, - }, authority: { - index: 4, + index: 2, isWritable: false as boolean, value: input.authority ?? null, }, mplCoreProgram: { - index: 5, + index: 3, isWritable: false as boolean, value: input.mplCoreProgram ?? null, }, systemProgram: { - index: 6, + index: 4, isWritable: false as boolean, value: input.systemProgram ?? null, }, } satisfies ResolvedAccountsWithIndices; // Default values. - if (!resolvedAccounts.agentReputation.value) { - resolvedAccounts.agentReputation.value = findAgentReputationV1Pda(context, { - asset: expectPublicKey(resolvedAccounts.asset.value), - }); - } if (!resolvedAccounts.payer.value) { resolvedAccounts.payer.value = context.payer; } + if (!resolvedAccounts.collection.value) { + resolvedAccounts.collection.value = findReviewsCollectionPda(context); + } + if (!resolvedAccounts.authority.value) { + resolvedAccounts.authority.value = findReviewsAuthorityPda(context); + } if (!resolvedAccounts.mplCoreProgram.value) { resolvedAccounts.mplCoreProgram.value = context.programs.getPublicKey( 'mplCore', @@ -168,7 +154,8 @@ export function registerReputationV1( ); // Data. - const data = getRegisterReputationV1InstructionDataSerializer().serialize({}); + const data = + getCreateReviewsCollectionV1InstructionDataSerializer().serialize({}); // Bytes Created On Chain. const bytesCreatedOnChain = 0; diff --git a/clients/js/src/generated/reputation/instructions/index.ts b/clients/js/src/generated/reputation/instructions/index.ts index 763d22c..cda121a 100644 --- a/clients/js/src/generated/reputation/instructions/index.ts +++ b/clients/js/src/generated/reputation/instructions/index.ts @@ -6,4 +6,6 @@ * @see https://github.com/metaplex-foundation/kinobi */ -export * from './registerReputationV1'; +export * from './createReviewsCollectionV1'; +export * from './leaveReviewV1'; +export * from './registerReviewsTreeV1'; diff --git a/clients/js/src/generated/reputation/instructions/leaveReviewV1.ts b/clients/js/src/generated/reputation/instructions/leaveReviewV1.ts new file mode 100644 index 0000000..643374c --- /dev/null +++ b/clients/js/src/generated/reputation/instructions/leaveReviewV1.ts @@ -0,0 +1,320 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Context, + Pda, + PublicKey, + Signer, + TransactionBuilder, + transactionBuilder, +} from '@metaplex-foundation/umi'; +import { + Serializer, + array, + bytes, + mapSerializer, + string, + struct, + u32, + u64, + u8, +} from '@metaplex-foundation/umi/serializers'; +import { findReviewsAuthorityPda, findReviewsCollectionPda } from '../accounts'; +import { + ResolvedAccount, + ResolvedAccountsWithIndices, + getAccountMetasAndSigners, +} from '../shared'; + +// Accounts. +export type LeaveReviewV1InstructionAccounts = { + /** Pays for the review cNFT mint and the review record PDA */ + payer?: Signer; + /** The wallet leaving the review; must own the work receipt */ + reviewer: Signer; + /** The Core asset being reviewed (the agent) */ + asset: PublicKey | Pda; + /** The owner of the new review cNFT leaf - must equal asset.owner */ + leafOwner: PublicKey | Pda; + /** Reviews authority PDA at ["reviews_authority"] — signs the Bubblegum CPI as tree_creator/collection_authority via invoke_signed */ + authority?: PublicKey | Pda; + /** Bubblegum tree config PDA for the reviews tree */ + treeConfig: PublicKey | Pda; + /** Reviews merkle tree at PDA ["reviews_tree", reviews_tree_index_le] */ + merkleTree: PublicKey | Pda; + /** Reviews collection PDA at ["reviews_collection"] */ + coreCollection?: PublicKey | Pda; + /** Bubblegum's mpl-core CPI signer PDA */ + mplCoreCpiSigner: PublicKey | Pda; + /** MPL Noop / log wrapper program */ + logWrapper?: PublicKey | Pda; + /** MPL Account Compression program (used for both Bubblegum mint and verify_leaf CPI) */ + compressionProgram?: PublicKey | Pda; + /** The MPL Core program */ + mplCoreProgram?: PublicKey | Pda; + /** The MPL Bubblegum program */ + bubblegumProgram?: PublicKey | Pda; + /** Receipts Bubblegum merkle tree holding the receipt being referenced */ + receiptsMerkleTree: PublicKey | Pda; + /** Canonical receipts collection PDA from mpl-agent-tools at ["receipts_collection"] */ + receiptsCollection: PublicKey | Pda; + /** ReviewRecordV1 PDA seeded with the receipt's bubblegum asset id - idempotency gate */ + reviewRecord: PublicKey | Pda; + /** The system program */ + systemProgram?: PublicKey | Pda; +}; + +// Data. +export type LeaveReviewV1InstructionData = { + discriminator: number; + rating: number; + pad: Array; + reviewsTreeIndex: bigint; + receiptsTreeIndex: bigint; + receiptNonce: bigint; + receiptIndex: number; + receiptFlags: number; + pad2: Array; + receiptRoot: Uint8Array; + receiptDataHash: Uint8Array; + receiptAssetDataHash: Uint8Array; + feedbackUri: string; +}; + +export type LeaveReviewV1InstructionDataArgs = { + rating: number; + reviewsTreeIndex: number | bigint; + receiptsTreeIndex: number | bigint; + receiptNonce: number | bigint; + receiptIndex: number; + receiptFlags: number; + receiptRoot: Uint8Array; + receiptDataHash: Uint8Array; + receiptAssetDataHash: Uint8Array; + feedbackUri: string; +}; + +export function getLeaveReviewV1InstructionDataSerializer(): Serializer< + LeaveReviewV1InstructionDataArgs, + LeaveReviewV1InstructionData +> { + return mapSerializer< + LeaveReviewV1InstructionDataArgs, + any, + LeaveReviewV1InstructionData + >( + struct( + [ + ['discriminator', u8()], + ['rating', u8()], + ['pad', array(u8(), { size: 6 })], + ['reviewsTreeIndex', u64()], + ['receiptsTreeIndex', u64()], + ['receiptNonce', u64()], + ['receiptIndex', u32()], + ['receiptFlags', u8()], + ['pad2', array(u8(), { size: 3 })], + ['receiptRoot', bytes({ size: 32 })], + ['receiptDataHash', bytes({ size: 32 })], + ['receiptAssetDataHash', bytes({ size: 32 })], + ['feedbackUri', string()], + ], + { description: 'LeaveReviewV1InstructionData' } + ), + (value) => ({ + ...value, + discriminator: 0, + pad: [0, 0, 0, 0, 0, 0], + pad2: [0, 0, 0], + }) + ) as Serializer< + LeaveReviewV1InstructionDataArgs, + LeaveReviewV1InstructionData + >; +} + +// Args. +export type LeaveReviewV1InstructionArgs = LeaveReviewV1InstructionDataArgs; + +// Instruction discriminator. +export const leaveReviewV1InstructionDiscriminator = 0; + +// Instruction. +export function leaveReviewV1( + context: Pick, + input: LeaveReviewV1InstructionAccounts & LeaveReviewV1InstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey( + 'mplAgentReputation', + 'REPREG5c1gPHuHukEyANpksLdHFaJCiTrm6zJgNhRZR' + ); + + // Accounts. + const resolvedAccounts = { + payer: { + index: 0, + isWritable: true as boolean, + value: input.payer ?? null, + }, + reviewer: { + index: 1, + isWritable: false as boolean, + value: input.reviewer ?? null, + }, + asset: { + index: 2, + isWritable: false as boolean, + value: input.asset ?? null, + }, + leafOwner: { + index: 3, + isWritable: false as boolean, + value: input.leafOwner ?? null, + }, + authority: { + index: 4, + isWritable: false as boolean, + value: input.authority ?? null, + }, + treeConfig: { + index: 5, + isWritable: true as boolean, + value: input.treeConfig ?? null, + }, + merkleTree: { + index: 6, + isWritable: true as boolean, + value: input.merkleTree ?? null, + }, + coreCollection: { + index: 7, + isWritable: true as boolean, + value: input.coreCollection ?? null, + }, + mplCoreCpiSigner: { + index: 8, + isWritable: false as boolean, + value: input.mplCoreCpiSigner ?? null, + }, + logWrapper: { + index: 9, + isWritable: false as boolean, + value: input.logWrapper ?? null, + }, + compressionProgram: { + index: 10, + isWritable: false as boolean, + value: input.compressionProgram ?? null, + }, + mplCoreProgram: { + index: 11, + isWritable: false as boolean, + value: input.mplCoreProgram ?? null, + }, + bubblegumProgram: { + index: 12, + isWritable: false as boolean, + value: input.bubblegumProgram ?? null, + }, + receiptsMerkleTree: { + index: 13, + isWritable: false as boolean, + value: input.receiptsMerkleTree ?? null, + }, + receiptsCollection: { + index: 14, + isWritable: false as boolean, + value: input.receiptsCollection ?? null, + }, + reviewRecord: { + index: 15, + isWritable: true as boolean, + value: input.reviewRecord ?? null, + }, + systemProgram: { + index: 16, + isWritable: false as boolean, + value: input.systemProgram ?? null, + }, + } satisfies ResolvedAccountsWithIndices; + + // Arguments. + const resolvedArgs: LeaveReviewV1InstructionArgs = { ...input }; + + // Default values. + if (!resolvedAccounts.payer.value) { + resolvedAccounts.payer.value = context.payer; + } + if (!resolvedAccounts.authority.value) { + resolvedAccounts.authority.value = findReviewsAuthorityPda(context); + } + if (!resolvedAccounts.coreCollection.value) { + resolvedAccounts.coreCollection.value = findReviewsCollectionPda(context); + } + if (!resolvedAccounts.logWrapper.value) { + resolvedAccounts.logWrapper.value = context.programs.getPublicKey( + 'mplNoop', + 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3' + ); + resolvedAccounts.logWrapper.isWritable = false; + } + if (!resolvedAccounts.compressionProgram.value) { + resolvedAccounts.compressionProgram.value = context.programs.getPublicKey( + 'mplAccountCompression', + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' + ); + resolvedAccounts.compressionProgram.isWritable = false; + } + if (!resolvedAccounts.mplCoreProgram.value) { + resolvedAccounts.mplCoreProgram.value = context.programs.getPublicKey( + 'mplCore', + 'CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d' + ); + resolvedAccounts.mplCoreProgram.isWritable = false; + } + if (!resolvedAccounts.bubblegumProgram.value) { + resolvedAccounts.bubblegumProgram.value = context.programs.getPublicKey( + 'mplBubblegum', + 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY' + ); + resolvedAccounts.bubblegumProgram.isWritable = false; + } + if (!resolvedAccounts.systemProgram.value) { + resolvedAccounts.systemProgram.value = context.programs.getPublicKey( + 'splSystem', + '11111111111111111111111111111111' + ); + resolvedAccounts.systemProgram.isWritable = false; + } + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values( + resolvedAccounts + ).sort((a, b) => a.index - b.index); + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners( + orderedAccounts, + 'programId', + programId + ); + + // Data. + const data = getLeaveReviewV1InstructionDataSerializer().serialize( + resolvedArgs as LeaveReviewV1InstructionDataArgs + ); + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0; + + return transactionBuilder([ + { instruction: { keys, programId, data }, signers, bytesCreatedOnChain }, + ]); +} diff --git a/clients/js/src/generated/reputation/instructions/registerReviewsTreeV1.ts b/clients/js/src/generated/reputation/instructions/registerReviewsTreeV1.ts new file mode 100644 index 0000000..5814314 --- /dev/null +++ b/clients/js/src/generated/reputation/instructions/registerReviewsTreeV1.ts @@ -0,0 +1,229 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { + Context, + Pda, + PublicKey, + Signer, + TransactionBuilder, + transactionBuilder, +} from '@metaplex-foundation/umi'; +import { + Serializer, + array, + mapSerializer, + struct, + u32, + u64, + u8, +} from '@metaplex-foundation/umi/serializers'; +import { findReviewsAuthorityPda } from '../accounts'; +import { + ResolvedAccount, + ResolvedAccountsWithIndices, + getAccountMetasAndSigners, +} from '../shared'; + +// Accounts. +export type RegisterReviewsTreeV1InstructionAccounts = { + /** Funds the tree rent */ + payer?: Signer; + /** Reviews authority PDA at ["reviews_authority"] — set as tree_creator */ + authority?: PublicKey | Pda; + /** Reviews merkle tree PDA at ["reviews_tree", tree_index_le] */ + merkleTree: PublicKey | Pda; + /** Bubblegum tree config PDA (derived from merkle_tree) */ + treeConfig: PublicKey | Pda; + /** MPL Noop / log wrapper program */ + logWrapper?: PublicKey | Pda; + /** MPL Account Compression program */ + compressionProgram?: PublicKey | Pda; + /** The MPL Bubblegum program */ + bubblegumProgram?: PublicKey | Pda; + /** The system program */ + systemProgram?: PublicKey | Pda; +}; + +// Data. +export type RegisterReviewsTreeV1InstructionData = { + discriminator: number; + pad: Array; + treeIndex: bigint; + maxDepth: number; + maxBufferSize: number; + canopyDepth: number; + pad2: Array; +}; + +export type RegisterReviewsTreeV1InstructionDataArgs = { + treeIndex: number | bigint; + maxDepth: number; + maxBufferSize: number; + canopyDepth: number; +}; + +export function getRegisterReviewsTreeV1InstructionDataSerializer(): Serializer< + RegisterReviewsTreeV1InstructionDataArgs, + RegisterReviewsTreeV1InstructionData +> { + return mapSerializer< + RegisterReviewsTreeV1InstructionDataArgs, + any, + RegisterReviewsTreeV1InstructionData + >( + struct( + [ + ['discriminator', u8()], + ['pad', array(u8(), { size: 7 })], + ['treeIndex', u64()], + ['maxDepth', u32()], + ['maxBufferSize', u32()], + ['canopyDepth', u32()], + ['pad2', array(u8(), { size: 4 })], + ], + { description: 'RegisterReviewsTreeV1InstructionData' } + ), + (value) => ({ + ...value, + discriminator: 2, + pad: [0, 0, 0, 0, 0, 0, 0], + pad2: [0, 0, 0, 0], + }) + ) as Serializer< + RegisterReviewsTreeV1InstructionDataArgs, + RegisterReviewsTreeV1InstructionData + >; +} + +// Args. +export type RegisterReviewsTreeV1InstructionArgs = + RegisterReviewsTreeV1InstructionDataArgs; + +// Instruction discriminator. +export const registerReviewsTreeV1InstructionDiscriminator = 2; + +// Instruction. +export function registerReviewsTreeV1( + context: Pick, + input: RegisterReviewsTreeV1InstructionAccounts & + RegisterReviewsTreeV1InstructionArgs +): TransactionBuilder { + // Program ID. + const programId = context.programs.getPublicKey( + 'mplAgentReputation', + 'REPREG5c1gPHuHukEyANpksLdHFaJCiTrm6zJgNhRZR' + ); + + // Accounts. + const resolvedAccounts = { + payer: { + index: 0, + isWritable: true as boolean, + value: input.payer ?? null, + }, + authority: { + index: 1, + isWritable: false as boolean, + value: input.authority ?? null, + }, + merkleTree: { + index: 2, + isWritable: true as boolean, + value: input.merkleTree ?? null, + }, + treeConfig: { + index: 3, + isWritable: true as boolean, + value: input.treeConfig ?? null, + }, + logWrapper: { + index: 4, + isWritable: false as boolean, + value: input.logWrapper ?? null, + }, + compressionProgram: { + index: 5, + isWritable: false as boolean, + value: input.compressionProgram ?? null, + }, + bubblegumProgram: { + index: 6, + isWritable: false as boolean, + value: input.bubblegumProgram ?? null, + }, + systemProgram: { + index: 7, + isWritable: false as boolean, + value: input.systemProgram ?? null, + }, + } satisfies ResolvedAccountsWithIndices; + + // Arguments. + const resolvedArgs: RegisterReviewsTreeV1InstructionArgs = { ...input }; + + // Default values. + if (!resolvedAccounts.payer.value) { + resolvedAccounts.payer.value = context.payer; + } + if (!resolvedAccounts.authority.value) { + resolvedAccounts.authority.value = findReviewsAuthorityPda(context); + } + if (!resolvedAccounts.logWrapper.value) { + resolvedAccounts.logWrapper.value = context.programs.getPublicKey( + 'mplNoop', + 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3' + ); + resolvedAccounts.logWrapper.isWritable = false; + } + if (!resolvedAccounts.compressionProgram.value) { + resolvedAccounts.compressionProgram.value = context.programs.getPublicKey( + 'mplAccountCompression', + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' + ); + resolvedAccounts.compressionProgram.isWritable = false; + } + if (!resolvedAccounts.bubblegumProgram.value) { + resolvedAccounts.bubblegumProgram.value = context.programs.getPublicKey( + 'mplBubblegum', + 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY' + ); + resolvedAccounts.bubblegumProgram.isWritable = false; + } + if (!resolvedAccounts.systemProgram.value) { + resolvedAccounts.systemProgram.value = context.programs.getPublicKey( + 'splSystem', + '11111111111111111111111111111111' + ); + resolvedAccounts.systemProgram.isWritable = false; + } + + // Accounts in order. + const orderedAccounts: ResolvedAccount[] = Object.values( + resolvedAccounts + ).sort((a, b) => a.index - b.index); + + // Keys and Signers. + const [keys, signers] = getAccountMetasAndSigners( + orderedAccounts, + 'programId', + programId + ); + + // Data. + const data = getRegisterReviewsTreeV1InstructionDataSerializer().serialize( + resolvedArgs as RegisterReviewsTreeV1InstructionDataArgs + ); + + // Bytes Created On Chain. + const bytesCreatedOnChain = 0; + + return transactionBuilder([ + { instruction: { keys, programId, data }, signers, bytesCreatedOnChain }, + ]); +} diff --git a/clients/js/src/generated/reputation/types/key.ts b/clients/js/src/generated/reputation/types/key.ts index 8469137..131347d 100644 --- a/clients/js/src/generated/reputation/types/key.ts +++ b/clients/js/src/generated/reputation/types/key.ts @@ -10,7 +10,7 @@ import { Serializer, scalarEnum } from '@metaplex-foundation/umi/serializers'; export enum Key { Uninitialized, - AgentReputationV1, + ReviewRecordV1, } export type KeyArgs = Key; diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts index 01d7872..713b466 100644 --- a/clients/js/src/index.ts +++ b/clients/js/src/index.ts @@ -1,5 +1,6 @@ export * from './api'; export * from './plugin'; +export * from './reputation'; // Full namespace exports (includes everything: types, errors, shared helpers) export * as identity from './generated/identity'; diff --git a/clients/js/src/reputation/index.ts b/clients/js/src/reputation/index.ts new file mode 100644 index 0000000..6c13268 --- /dev/null +++ b/clients/js/src/reputation/index.ts @@ -0,0 +1,16 @@ +/** + * Constants and helpers that complement the auto-generated reputation client. + */ + +import { PublicKey } from '@metaplex-foundation/umi'; + +/** Maximum length, in bytes, of the feedback URI accepted by the program. */ +export const MAX_FEEDBACK_URI_LEN = 200; + +// Useful constants for callers wiring up the LeaveReviewV1 instruction. +export const MPL_BUBBLEGUM_PROGRAM_ID = + 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY' as PublicKey; +export const MPL_NOOP_PROGRAM_ID = + 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3' as PublicKey; +export const MPL_ACCOUNT_COMPRESSION_PROGRAM_ID = + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' as PublicKey; diff --git a/clients/js/test/_receiptsReviews.ts b/clients/js/test/_receiptsReviews.ts index 815438d..75d280a 100644 --- a/clients/js/test/_receiptsReviews.ts +++ b/clients/js/test/_receiptsReviews.ts @@ -19,6 +19,12 @@ import { Umi, } from '@metaplex-foundation/umi'; +import { + createReviewsCollectionV1, + findReviewsCollectionPda, + findReviewsTreePda, + registerReviewsTreeV1, +} from '../src/generated/reputation'; import { createReceiptsCollectionV1, delegateExecutionV1, @@ -73,6 +79,8 @@ async function maybe(p: Promise, expectedHex: string): Promise { /** Custom program error code for MplAgentToolsError::ReceiptsCollectionAlreadyInitialized. */ const RECEIPTS_COLLECTION_ALREADY_INITIALIZED_HEX = '0x14'; +/** Custom program error code for MplAgentReputationError::ReviewsCollectionAlreadyInitialized. */ +const REVIEWS_COLLECTION_ALREADY_INITIALIZED_HEX = '0xd'; export interface ReceiptsBootstrap { receiptsCollection: PublicKey; @@ -80,6 +88,12 @@ export interface ReceiptsBootstrap { receiptsTreeIndex: bigint; } +export interface ReceiptsReviewsBootstrap extends ReceiptsBootstrap { + reviewsCollection: PublicKey; + reviewsTree: PublicKey; + reviewsTreeIndex: bigint; +} + /** * Permissionless bootstrap: idempotently create the canonical receipts * collection, then allocate a fresh receipts tree at a random index for @@ -114,6 +128,44 @@ export async function bootstrapReceipts(umi: Umi): Promise { }; } +/** + * Extends `bootstrapReceipts` with the reviews-side counterpart: + * idempotently creates the reviews collection and allocates a fresh + * reviews tree at a random index. Returns the combined context every + * `LeaveReviewV1` test needs. + */ +export async function bootstrapReceiptsAndReviews( + umi: Umi +): Promise { + const receipts = await bootstrapReceipts(umi); + const reviewsCollection = publicKey(findReviewsCollectionPda(umi)); + + await maybe( + createReviewsCollectionV1(umi, {}).sendAndConfirm(umi), + REVIEWS_COLLECTION_ALREADY_INITIALIZED_HEX + ); + + const reviewsTreeIndex = randomU64(); + const reviewsTree = publicKey( + findReviewsTreePda(umi, { treeIndex: reviewsTreeIndex }) + ); + await registerReviewsTreeV1(umi, { + merkleTree: reviewsTree, + treeConfig: findTreeConfigPda(umi, { merkleTree: reviewsTree }), + treeIndex: reviewsTreeIndex, + maxDepth: TREE_MAX_DEPTH, + maxBufferSize: TREE_MAX_BUFFER, + canopyDepth: 0, + }).sendAndConfirm(umi); + + return { + ...receipts, + reviewsCollection, + reviewsTree, + reviewsTreeIndex, + }; +} + function randomU64(): bigint { // 56 random bits — comfortably within u64, no chance of collision in a // test run. diff --git a/clients/js/test/reputation/leaveReview.test.ts b/clients/js/test/reputation/leaveReview.test.ts new file mode 100644 index 0000000..6b0c075 --- /dev/null +++ b/clients/js/test/reputation/leaveReview.test.ts @@ -0,0 +1,125 @@ +import test from 'ava'; +import { + findTreeConfigPda, + findLeafAssetIdPda, + mplBubblegum, +} from '@metaplex-foundation/mpl-bubblegum'; +import { + generateSigner, + publicKey, + publicKeyBytes, +} from '@metaplex-foundation/umi'; + +import { + fetchReviewRecordV1, + findReviewRecordV1Pda, + Key as ReputationKey, + leaveReviewV1, +} from '../../src/generated/reputation'; +import { createUmi } from '../_setup'; +import { + bootstrapReceiptsAndReviews, + DEFAULT_ASSET_DATA_HASH, + getCurrentTreeRoot, + MPL_CORE_CPI_SIGNER, + receiptDataHash, + setupAgentWithExecutive, +} from '../_receiptsReviews'; +import { mintWorkReceiptV1 } from '../../src/generated/tools'; + +test('program-managed trees: full receipt → review flow', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + + const { + receiptsCollection, + reviewsCollection, + receiptsTree, + receiptsTreeIndex, + reviewsTree, + reviewsTreeIndex, + } = await bootstrapReceiptsAndReviews(umi); + + const { agent, executive, executionDelegateRecord } = + await setupAgentWithExecutive(umi); + + const client = generateSigner(umi); + await umi.rpc.airdrop(client.publicKey, { + basisPoints: 100_000_000n, + identifier: 'SOL', + decimals: 9, + }); + + // Mint the work receipt. + const receiptUri = 'https://example.com/job-1-receipt.json'; + await mintWorkReceiptV1(umi, { + executiveAuthority: executive, + executionDelegateRecord, + agentAsset: agent, + client: client.publicKey, + treeConfig: findTreeConfigPda(umi, { merkleTree: receiptsTree }), + merkleTree: receiptsTree, + coreCollection: receiptsCollection, + mplCoreCpiSigner: MPL_CORE_CPI_SIGNER, + receiptUri, + treeIndex: receiptsTreeIndex, + }).sendAndConfirm(umi); + + const [receiptAssetId] = findLeafAssetIdPda(umi, { + merkleTree: receiptsTree, + leafIndex: 0, + }); + const reviewRecord = findReviewRecordV1Pda(umi, { + receiptAssetId: publicKey(receiptAssetId), + }); + + const sharedArgs = { + payer: client, + reviewer: client, + asset: agent, + leafOwner: umi.payer.publicKey, + treeConfig: findTreeConfigPda(umi, { merkleTree: reviewsTree }), + merkleTree: reviewsTree, + coreCollection: reviewsCollection, + mplCoreCpiSigner: MPL_CORE_CPI_SIGNER, + receiptsMerkleTree: receiptsTree, + receiptsCollection, + reviewRecord, + reviewsTreeIndex, + receiptsTreeIndex, + receiptNonce: 0n, + receiptIndex: 0, + receiptRoot: publicKeyBytes( + publicKey(await getCurrentTreeRoot(umi, receiptsTree)) + ), + receiptDataHash: receiptDataHash({ + receiptUri, + agent, + client: client.publicKey, + receiptsCollection, + }), + receiptAssetDataHash: DEFAULT_ASSET_DATA_HASH, + receiptFlags: 0, + } as const; + + await leaveReviewV1(umi, { + ...sharedArgs, + rating: 5, + feedbackUri: 'https://example.com/job-1-review.json', + }).sendAndConfirm(umi); + + const record = await fetchReviewRecordV1(umi, reviewRecord); + t.is(record.key, ReputationKey.ReviewRecordV1); + t.is(record.reviewer, client.publicKey); + t.is(record.receiptAssetId, publicKey(receiptAssetId)); + + // Second review on the same receipt fails with ReviewAlreadyExists (0xa) + // — the ReviewRecord PDA is the idempotency gate. + await t.throwsAsync( + leaveReviewV1(umi, { + ...sharedArgs, + rating: 1, + feedbackUri: 'https://example.com/job-1-review-2.json', + }).sendAndConfirm(umi), + { message: /custom program error: 0xa\b/ } + ); +}); diff --git a/clients/js/test/reputation/leaveReviewValidation.test.ts b/clients/js/test/reputation/leaveReviewValidation.test.ts new file mode 100644 index 0000000..6a54b5b --- /dev/null +++ b/clients/js/test/reputation/leaveReviewValidation.test.ts @@ -0,0 +1,304 @@ +import test from 'ava'; +import { + findTreeConfigPda, + findLeafAssetIdPda, + mplBubblegum, +} from '@metaplex-foundation/mpl-bubblegum'; +import { + generateSigner, + publicKey, + publicKeyBytes, + PublicKey, + Signer, +} from '@metaplex-foundation/umi'; + +import { + findReviewRecordV1Pda, + leaveReviewV1, +} from '../../src/generated/reputation'; +import { mintWorkReceiptV1 } from '../../src/generated/tools'; +import { findReceiptsTreePda, findReviewsTreePda } from '../../src'; +import { createUmi } from '../_setup'; +import { + bootstrapReceiptsAndReviews, + DEFAULT_ASSET_DATA_HASH, + getCurrentTreeRoot, + MPL_CORE_CPI_SIGNER, + receiptDataHash, + setupAgentWithExecutive, +} from '../_receiptsReviews'; + +/** + * Common setup: bootstrap + agent + client + a real minted receipt, plus + * a `sharedArgs` object pre-populated with everything LeaveReviewV1 needs + * for the happy path. Each test mutates one field to flip into a negative. + */ +async function setupContext(umi: Awaited>) { + const ctx = await bootstrapReceiptsAndReviews(umi); + const agentSetup = await setupAgentWithExecutive(umi); + + const client: Signer = generateSigner(umi); + await umi.rpc.airdrop(client.publicKey, { + basisPoints: 100_000_000n, + identifier: 'SOL', + decimals: 9, + }); + + const receiptUri = 'https://example.com/job/receipt.json'; + await mintWorkReceiptV1(umi, { + executiveAuthority: agentSetup.executive, + executionDelegateRecord: agentSetup.executionDelegateRecord, + agentAsset: agentSetup.agent, + client: client.publicKey, + treeConfig: findTreeConfigPda(umi, { merkleTree: ctx.receiptsTree }), + merkleTree: ctx.receiptsTree, + coreCollection: ctx.receiptsCollection, + mplCoreCpiSigner: MPL_CORE_CPI_SIGNER, + receiptUri, + treeIndex: ctx.receiptsTreeIndex, + }).sendAndConfirm(umi); + + const [receiptAssetId] = findLeafAssetIdPda(umi, { + merkleTree: ctx.receiptsTree, + leafIndex: 0, + }); + const reviewRecord = findReviewRecordV1Pda(umi, { + receiptAssetId: publicKey(receiptAssetId), + }); + + const sharedArgs = { + payer: client, + reviewer: client, + asset: agentSetup.agent, + leafOwner: umi.payer.publicKey, + treeConfig: findTreeConfigPda(umi, { merkleTree: ctx.reviewsTree }), + merkleTree: ctx.reviewsTree, + coreCollection: ctx.reviewsCollection, + mplCoreCpiSigner: MPL_CORE_CPI_SIGNER, + receiptsMerkleTree: ctx.receiptsTree, + receiptsCollection: ctx.receiptsCollection, + reviewRecord, + reviewsTreeIndex: ctx.reviewsTreeIndex, + receiptsTreeIndex: ctx.receiptsTreeIndex, + receiptNonce: 0n, + receiptIndex: 0, + receiptRoot: publicKeyBytes( + publicKey(await getCurrentTreeRoot(umi, ctx.receiptsTree)) + ), + receiptDataHash: receiptDataHash({ + receiptUri, + agent: agentSetup.agent, + client: client.publicKey, + receiptsCollection: ctx.receiptsCollection, + }), + receiptAssetDataHash: DEFAULT_ASSET_DATA_HASH, + receiptFlags: 0, + rating: 5, + feedbackUri: 'https://example.com/review.json', + } as const; + + return { ctx, agentSetup, client, sharedArgs }; +} + +// Custom-program error codes for MplAgentReputationError, mapped by +// variant position. Keep in sync with programs/mpl-agent-reputation/src/error.rs. +const ERR_INVALID_REVIEW_RATING = /custom program error: 0x5\b/; +const ERR_FEEDBACK_URI_INVALID = /custom program error: 0x6\b/; +const ERR_LEAF_OWNER_MISMATCH = /custom program error: 0x7\b/; +const ERR_INVALID_REVIEWS_COLLECTION = /custom program error: 0xb\b/; +const ERR_INVALID_REVIEWS_TREE_DERIVATION = /custom program error: 0xe\b/; +const ERR_INVALID_RECEIPTS_COLLECTION = /custom program error: 0xf\b/; +const ERR_INVALID_RECEIPTS_TREE_DERIVATION = /custom program error: 0x10\b/; + +test('leaveReviewV1 — rejects rating 0', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + await t.throwsAsync( + leaveReviewV1(umi, { ...sharedArgs, rating: 0 }).sendAndConfirm(umi), + { message: ERR_INVALID_REVIEW_RATING } + ); +}); + +test('leaveReviewV1 — rejects rating 6', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + await t.throwsAsync( + leaveReviewV1(umi, { ...sharedArgs, rating: 6 }).sendAndConfirm(umi), + { message: ERR_INVALID_REVIEW_RATING } + ); +}); + +test('leaveReviewV1 — rejects empty feedback URI', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + await t.throwsAsync( + leaveReviewV1(umi, { ...sharedArgs, feedbackUri: '' }).sendAndConfirm(umi), + { message: ERR_FEEDBACK_URI_INVALID } + ); +}); + +test('leaveReviewV1 — rejects leafOwner that does not match asset owner', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + const wrong = generateSigner(umi).publicKey; + await t.throwsAsync( + leaveReviewV1(umi, { ...sharedArgs, leafOwner: wrong }).sendAndConfirm(umi), + { message: ERR_LEAF_OWNER_MISMATCH } + ); +}); + +test('leaveReviewV1 — rejects mismatched reviews collection', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + const wrongCollection: PublicKey = generateSigner(umi).publicKey; + await t.throwsAsync( + leaveReviewV1(umi, { + ...sharedArgs, + coreCollection: wrongCollection, + }).sendAndConfirm(umi), + { message: ERR_INVALID_REVIEWS_COLLECTION } + ); +}); + +test('leaveReviewV1 — rejects mismatched receipts collection', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + const wrongCollection: PublicKey = generateSigner(umi).publicKey; + await t.throwsAsync( + leaveReviewV1(umi, { + ...sharedArgs, + receiptsCollection: wrongCollection, + }).sendAndConfirm(umi), + { message: ERR_INVALID_RECEIPTS_COLLECTION } + ); +}); + +test('leaveReviewV1 — rejects reviews tree at wrong PDA index', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + + // Bump the tree_index arg so the merkle_tree no longer matches the PDA + // derivation. Bubblegum's tree_config also won't match, but our check + // for the tree-PDA derivation trips first. + await t.throwsAsync( + leaveReviewV1(umi, { + ...sharedArgs, + reviewsTreeIndex: sharedArgs.reviewsTreeIndex + 5n, + }).sendAndConfirm(umi), + { message: ERR_INVALID_REVIEWS_TREE_DERIVATION } + ); +}); + +test('leaveReviewV1 — rejects merkle_tree that does not match reviewsTreeIndex', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + + // Swap to a tree at a different PDA index — the on-chain check + // (`check_reviews_tree_pda`) compares the supplied tree to + // `["reviews_tree", reviewsTreeIndex_le]`. + const fakeTree = publicKey(findReviewsTreePda(umi, { treeIndex: 9999n })); + await t.throwsAsync( + leaveReviewV1(umi, { + ...sharedArgs, + merkleTree: fakeTree, + treeConfig: findTreeConfigPda(umi, { merkleTree: fakeTree }), + }).sendAndConfirm(umi), + { message: ERR_INVALID_REVIEWS_TREE_DERIVATION } + ); +}); + +test('leaveReviewV1 — rejects receipts_merkle_tree that is not the canonical PDA', async (t) => { + // Without this PDA check, an attacker could stand up their own + // Bubblegum-compatible compression tree, append a forged work-receipt + // leaf to it (the attacker is the tree authority), then pass it as + // `receipts_merkle_tree`. The on-chain `verify_leaf_cpi` would happily + // confirm the forged leaf is in the attacker's tree and a fake review + // would be minted against any target agent. + // + // The fix derives the canonical receipts-tree PDA from the supplied + // `receipts_tree_index` arg and rejects any tree that isn't at that + // exact PDA. Substituting a different (still-canonical) receipts tree + // here trips the check before verify_leaf is even reached. + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + + const otherTree = publicKey(findReceiptsTreePda(umi, { treeIndex: 9999n })); + await t.throwsAsync( + leaveReviewV1(umi, { + ...sharedArgs, + receiptsMerkleTree: otherTree, + }).sendAndConfirm(umi), + { message: ERR_INVALID_RECEIPTS_TREE_DERIVATION } + ); +}); + +test('leaveReviewV1 — rejects bogus receipt data_hash (verify_leaf fails)', async (t) => { + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs } = await setupContext(umi); + + // Flip a byte of the data_hash — the reconstructed leaf hash won't + // match what's actually in the receipts tree, so mpl-account- + // compression's verify_leaf CPI returns an error. + const bogus = new Uint8Array(sharedArgs.receiptDataHash); + bogus[0] ^= 0xff; + + // Same shape as the receipt-replay test below: the compression program + // panics after logging the semantic leaf-mismatch error, so the message + // is just "Program failed to complete". Assert the precise log signal + // so this test can't silently pass on an unrelated rejection. + const err: any = await t.throwsAsync( + leaveReviewV1(umi, { + ...sharedArgs, + receiptDataHash: bogus, + }).sendAndConfirm(umi) + ); + const logs: string[] = + err?.cause?.logs ?? err?.logs ?? err?.transactionLogs ?? []; + t.true( + logs.some((l) => + l.includes('current leaf value does not match the supplied proof') + ), + `expected leaf-mismatch log, got: ${JSON.stringify(logs)}` + ); +}); + +test('leaveReviewV1 — rejects receipt replay against a different agent', async (t) => { + // A receipt minted for AgentA must not be usable to fake a review for + // AgentB. The on-chain code computes the expected creator_hash from + // `ctx.accounts.asset.key`, so the reconstructed leaf hash differs + // from the real leaf in the receipts tree → verify_leaf rejects. + const umi = (await createUmi()).use(mplBubblegum()); + const { sharedArgs, agentSetup: agentASetup } = await setupContext(umi); + + // Stand up a second, unrelated agent (different asset, different + // executive, different delegate record). Nothing was minted for them. + const agentBSetup = await setupAgentWithExecutive(umi); + + // Reuse AgentA's receipt proof params verbatim, but flip the reviewed + // asset to AgentB. The failure must come from mpl-account-compression's + // verify_leaf with the specific "leaf value does not match" semantic + // log — that's the exact signal of leaf-hash mismatch. Asserting that + // specific log message prevents the test from silently passing if some + // earlier check ever starts rejecting the call first. + const err: any = await t.throwsAsync( + leaveReviewV1(umi, { + ...sharedArgs, + asset: agentBSetup.agent, + }).sendAndConfirm(umi) + ); + // The message itself is just "Program failed to complete" (the compression + // program panics after logging the semantic error). Assert the precise + // leaf-mismatch signal from the logs instead, so this test can't silently + // pass on some unrelated earlier rejection. + const logs: string[] = + err?.cause?.logs ?? err?.logs ?? err?.transactionLogs ?? []; + t.true( + logs.some((l) => + l.includes('current leaf value does not match the supplied proof') + ), + `expected leaf-mismatch log, got: ${JSON.stringify(logs)}` + ); + + // Quiet unused-var lint: + void agentASetup; +}); diff --git a/clients/js/test/reputation/register.test.ts b/clients/js/test/reputation/register.test.ts deleted file mode 100644 index f38fc36..0000000 --- a/clients/js/test/reputation/register.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import test from 'ava'; -import { fetchAsset } from '@metaplex-foundation/mpl-core'; -import { publicKey } from '@metaplex-foundation/umi'; -import { - fetchAgentReputationV1, - findAgentReputationV1Pda, - Key, - registerReputationV1, -} from '../../src/generated/reputation'; -import { createCollectionAndAsset, createUmi } from '../_setup'; - -test('it can register an asset', async (t) => { - // Given a Umi instance and a new signer. - const umi = await createUmi(); - // Create the collection and asset. - const { collection, asset } = await createCollectionAndAsset(umi); - - // When we register the asset. - await registerReputationV1(umi, { - asset, - collection, - }).sendAndConfirm(umi); - - // And there's an Agent Reputation PDA. - const agentReputationPda = findAgentReputationV1Pda(umi, { asset }); - const agentReputation = await fetchAgentReputationV1(umi, agentReputationPda); - t.is(agentReputation.key, Key.AgentReputationV1); - t.is(agentReputation.bump, agentReputationPda[1]); - - // Then the asset has a AppData plugin. - const assetData = await fetchAsset(umi, asset); - t.is(assetData?.appDatas?.length, 1); - t.like(assetData?.appDatas?.[0], { - dataAuthority: { type: 'Address', address: publicKey(agentReputationPda) }, - authority: { type: 'UpdateAuthority' }, - }); -}); diff --git a/clients/rust-reputation/src/generated/accounts/mod.rs b/clients/rust-reputation/src/generated/accounts/mod.rs index 602113f..2cb2a14 100644 --- a/clients/rust-reputation/src/generated/accounts/mod.rs +++ b/clients/rust-reputation/src/generated/accounts/mod.rs @@ -5,6 +5,6 @@ //! [https://github.com/metaplex-foundation/kinobi] //! -pub(crate) mod r#agent_reputation_v1; +pub(crate) mod r#review_record_v1; -pub use self::r#agent_reputation_v1::*; +pub use self::r#review_record_v1::*; diff --git a/clients/rust-reputation/src/generated/accounts/agent_reputation_v1.rs b/clients/rust-reputation/src/generated/accounts/review_record_v1.rs similarity index 71% rename from clients/rust-reputation/src/generated/accounts/agent_reputation_v1.rs rename to clients/rust-reputation/src/generated/accounts/review_record_v1.rs index a417c0f..223f7d2 100644 --- a/clients/rust-reputation/src/generated/accounts/agent_reputation_v1.rs +++ b/clients/rust-reputation/src/generated/accounts/review_record_v1.rs @@ -16,7 +16,7 @@ use solana_program::pubkey::Pubkey; #[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] #[derive(Clone, Debug, Eq, PartialEq)] -pub struct AgentReputationV1 { +pub struct ReviewRecordV1 { pub key: Key, pub bump: u8, pub padding: [u8; 6], @@ -24,33 +24,42 @@ pub struct AgentReputationV1 { feature = "serde", serde(with = "serde_with::As::") )] - pub asset: Pubkey, + pub reviewer: Pubkey, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub receipt_asset_id: Pubkey, } -impl AgentReputationV1 { - pub const LEN: usize = 40; +impl ReviewRecordV1 { + pub const LEN: usize = 72; /// Prefix values used to generate a PDA for this account. /// /// Values are positional and appear in the following order: /// - /// 0. `AgentReputationV1::PREFIX` - /// 1. asset (`Pubkey`) - pub const PREFIX: &'static [u8] = "agent_reputation".as_bytes(); + /// 0. `ReviewRecordV1::PREFIX` + /// 1. receipt_asset_id (`Pubkey`) + pub const PREFIX: &'static [u8] = "review_record".as_bytes(); pub fn create_pda( - asset: Pubkey, + receipt_asset_id: Pubkey, bump: u8, ) -> Result { solana_program::pubkey::Pubkey::create_program_address( - &["agent_reputation".as_bytes(), asset.as_ref(), &[bump]], + &[ + "review_record".as_bytes(), + receipt_asset_id.as_ref(), + &[bump], + ], &crate::MPL_AGENT_REPUTATION_ID, ) } - pub fn find_pda(asset: &Pubkey) -> (solana_program::pubkey::Pubkey, u8) { + pub fn find_pda(receipt_asset_id: &Pubkey) -> (solana_program::pubkey::Pubkey, u8) { solana_program::pubkey::Pubkey::find_program_address( - &["agent_reputation".as_bytes(), asset.as_ref()], + &["review_record".as_bytes(), receipt_asset_id.as_ref()], &crate::MPL_AGENT_REPUTATION_ID, ) } @@ -62,7 +71,7 @@ impl AgentReputationV1 { } } -impl<'a> TryFrom<&solana_program::account_info::AccountInfo<'a>> for AgentReputationV1 { +impl<'a> TryFrom<&solana_program::account_info::AccountInfo<'a>> for ReviewRecordV1 { type Error = std::io::Error; fn try_from( diff --git a/clients/rust-reputation/src/generated/errors/mpl_agent_reputation.rs b/clients/rust-reputation/src/generated/errors/mpl_agent_reputation.rs index 0441e5f..6a51eea 100644 --- a/clients/rust-reputation/src/generated/errors/mpl_agent_reputation.rs +++ b/clients/rust-reputation/src/generated/errors/mpl_agent_reputation.rs @@ -26,9 +26,46 @@ pub enum MplAgentReputationError { /// 4 (0x4) - Invalid Core Asset #[error("Invalid Core Asset")] InvalidCoreAsset, - /// 5 (0x5) - Agent Reputation already registered - #[error("Agent Reputation already registered")] - AgentReputationAlreadyRegistered, + /// 5 (0x5) - Invalid review rating (must be 1..=5) + #[error("Invalid review rating (must be 1..=5)")] + InvalidReviewRating, + /// 6 (0x6) - Feedback URI must be non-empty and within size limits + #[error("Feedback URI must be non-empty and within size limits")] + FeedbackUriInvalid, + /// 7 (0x7) - Leaf owner does not match the reviewed asset owner + #[error("Leaf owner does not match the reviewed asset owner")] + LeafOwnerMismatch, + /// 8 (0x8) - Invalid Bubblegum Program + #[error("Invalid Bubblegum Program")] + InvalidBubblegumProgram, + /// 9 (0x9) - Invalid Compression Program + #[error("Invalid Compression Program")] + InvalidCompressionProgram, + /// 10 (0xA) - A review already exists for this work receipt + #[error("A review already exists for this work receipt")] + ReviewAlreadyExists, + /// 11 (0xB) - Invalid reviews collection PDA derivation + #[error("Invalid reviews collection PDA derivation")] + InvalidReviewsCollection, + /// 12 (0xC) - Invalid reviews authority PDA derivation + #[error("Invalid reviews authority PDA derivation")] + InvalidReviewsAuthority, + /// 13 (0xD) - Reviews collection already initialized + #[error("Reviews collection already initialized")] + ReviewsCollectionAlreadyInitialized, + /// 14 (0xE) - Invalid reviews tree PDA derivation + #[error("Invalid reviews tree PDA derivation")] + InvalidReviewsTreeDerivation, + /// 15 (0xF) - Supplied receipts collection is not the canonical mpl-agent-tools receipts collection PDA + #[error( + "Supplied receipts collection is not the canonical mpl-agent-tools receipts collection PDA" + )] + InvalidReceiptsCollection, + /// 16 (0x10) - Supplied receipts merkle tree is not the canonical mpl-agent-tools receipts tree PDA + #[error( + "Supplied receipts merkle tree is not the canonical mpl-agent-tools receipts tree PDA" + )] + InvalidReceiptsTreeDerivation, } impl From for ProgramError { @@ -46,7 +83,18 @@ impl TryFrom for MplAgentReputationError { 2 => Ok(MplAgentReputationError::InvalidAccountData), 3 => Ok(MplAgentReputationError::InvalidMplCoreProgram), 4 => Ok(MplAgentReputationError::InvalidCoreAsset), - 5 => Ok(MplAgentReputationError::AgentReputationAlreadyRegistered), + 5 => Ok(MplAgentReputationError::InvalidReviewRating), + 6 => Ok(MplAgentReputationError::FeedbackUriInvalid), + 7 => Ok(MplAgentReputationError::LeafOwnerMismatch), + 8 => Ok(MplAgentReputationError::InvalidBubblegumProgram), + 9 => Ok(MplAgentReputationError::InvalidCompressionProgram), + 10 => Ok(MplAgentReputationError::ReviewAlreadyExists), + 11 => Ok(MplAgentReputationError::InvalidReviewsCollection), + 12 => Ok(MplAgentReputationError::InvalidReviewsAuthority), + 13 => Ok(MplAgentReputationError::ReviewsCollectionAlreadyInitialized), + 14 => Ok(MplAgentReputationError::InvalidReviewsTreeDerivation), + 15 => Ok(MplAgentReputationError::InvalidReceiptsCollection), + 16 => Ok(MplAgentReputationError::InvalidReceiptsTreeDerivation), _ => Err(ProgramError::InvalidArgument), } } @@ -55,14 +103,23 @@ impl TryFrom for MplAgentReputationError { impl ToStr for MplAgentReputationError { fn to_str(&self) -> &'static str { match self { - MplAgentReputationError::InvalidSystemProgram => "Invalid System Program", - MplAgentReputationError::InvalidInstructionData => "Invalid instruction data", - MplAgentReputationError::InvalidAccountData => "Invalid account data", - MplAgentReputationError::InvalidMplCoreProgram => "Invalid MPL Core Program", - MplAgentReputationError::InvalidCoreAsset => "Invalid Core Asset", - MplAgentReputationError::AgentReputationAlreadyRegistered => { - "Agent Reputation already registered" - } - } + MplAgentReputationError::InvalidSystemProgram => "Invalid System Program", + MplAgentReputationError::InvalidInstructionData => "Invalid instruction data", + MplAgentReputationError::InvalidAccountData => "Invalid account data", + MplAgentReputationError::InvalidMplCoreProgram => "Invalid MPL Core Program", + MplAgentReputationError::InvalidCoreAsset => "Invalid Core Asset", + MplAgentReputationError::InvalidReviewRating => "Invalid review rating (must be 1..=5)", + MplAgentReputationError::FeedbackUriInvalid => "Feedback URI must be non-empty and within size limits", + MplAgentReputationError::LeafOwnerMismatch => "Leaf owner does not match the reviewed asset owner", + MplAgentReputationError::InvalidBubblegumProgram => "Invalid Bubblegum Program", + MplAgentReputationError::InvalidCompressionProgram => "Invalid Compression Program", + MplAgentReputationError::ReviewAlreadyExists => "A review already exists for this work receipt", + MplAgentReputationError::InvalidReviewsCollection => "Invalid reviews collection PDA derivation", + MplAgentReputationError::InvalidReviewsAuthority => "Invalid reviews authority PDA derivation", + MplAgentReputationError::ReviewsCollectionAlreadyInitialized => "Reviews collection already initialized", + MplAgentReputationError::InvalidReviewsTreeDerivation => "Invalid reviews tree PDA derivation", + MplAgentReputationError::InvalidReceiptsCollection => "Supplied receipts collection is not the canonical mpl-agent-tools receipts collection PDA", + MplAgentReputationError::InvalidReceiptsTreeDerivation => "Supplied receipts merkle tree is not the canonical mpl-agent-tools receipts tree PDA", + } } } diff --git a/clients/rust-reputation/src/generated/instructions/register_reputation_v1.rs b/clients/rust-reputation/src/generated/instructions/create_reviews_collection_v1.rs similarity index 59% rename from clients/rust-reputation/src/generated/instructions/register_reputation_v1.rs rename to clients/rust-reputation/src/generated/instructions/create_reviews_collection_v1.rs index 7046848..ef5e821 100644 --- a/clients/rust-reputation/src/generated/instructions/register_reputation_v1.rs +++ b/clients/rust-reputation/src/generated/instructions/create_reviews_collection_v1.rs @@ -11,24 +11,20 @@ use anchor_lang::prelude::{AnchorDeserialize, AnchorSerialize}; use borsh::{BorshDeserialize, BorshSerialize}; /// Accounts. -pub struct RegisterReputationV1 { - /// The agent reputation PDA - pub agent_reputation: solana_program::pubkey::Pubkey, - /// The address of the Core asset - pub asset: solana_program::pubkey::Pubkey, - /// The address of the collection - pub collection: Option, - /// The payer for additional rent +pub struct CreateReviewsCollectionV1 { + /// Funds the collection's rent pub payer: solana_program::pubkey::Pubkey, - /// Authority for the collection. If not provided, the payer will be used. - pub authority: Option, + /// Reviews collection PDA at ["reviews_collection"] + pub collection: solana_program::pubkey::Pubkey, + /// Reviews authority PDA at ["reviews_authority"] — becomes the collection's update_authority + pub authority: solana_program::pubkey::Pubkey, /// The MPL Core program pub mpl_core_program: solana_program::pubkey::Pubkey, /// The system program pub system_program: solana_program::pubkey::Pubkey, } -impl RegisterReputationV1 { +impl CreateReviewsCollectionV1 { pub fn instruction(&self) -> solana_program::instruction::Instruction { self.instruction_with_remaining_accounts(&[]) } @@ -37,37 +33,18 @@ impl RegisterReputationV1 { &self, remaining_accounts: &[solana_program::instruction::AccountMeta], ) -> solana_program::instruction::Instruction { - let mut accounts = Vec::with_capacity(7 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( - self.agent_reputation, - false, + self.payer, true, )); accounts.push(solana_program::instruction::AccountMeta::new( - self.asset, false, + self.collection, + false, )); - if let Some(collection) = self.collection { - accounts.push(solana_program::instruction::AccountMeta::new( - collection, false, - )); - } else { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( - crate::MPL_AGENT_REPUTATION_ID, - false, - )); - } - accounts.push(solana_program::instruction::AccountMeta::new( - self.payer, true, + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.authority, + false, )); - if let Some(authority) = self.authority { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( - authority, true, - )); - } else { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( - crate::MPL_AGENT_REPUTATION_ID, - false, - )); - } accounts.push(solana_program::instruction::AccountMeta::new_readonly( self.mpl_core_program, false, @@ -77,7 +54,7 @@ impl RegisterReputationV1 { false, )); accounts.extend_from_slice(remaining_accounts); - let data = borsh::to_vec(&(RegisterReputationV1InstructionData::new())).unwrap(); + let data = borsh::to_vec(&(CreateReviewsCollectionV1InstructionData::new())).unwrap(); solana_program::instruction::Instruction { program_id: crate::MPL_AGENT_REPUTATION_ID, @@ -89,80 +66,59 @@ impl RegisterReputationV1 { #[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] -pub struct RegisterReputationV1InstructionData { +pub struct CreateReviewsCollectionV1InstructionData { discriminator: u8, padding: [u8; 7], } -impl RegisterReputationV1InstructionData { +impl CreateReviewsCollectionV1InstructionData { pub fn new() -> Self { Self { - discriminator: 0, + discriminator: 1, padding: [0, 0, 0, 0, 0, 0, 0], } } } -/// Instruction builder for `RegisterReputationV1`. +/// Instruction builder for `CreateReviewsCollectionV1`. /// /// ### Accounts: /// -/// 0. `[writable]` agent_reputation -/// 1. `[writable]` asset -/// 2. `[writable, optional]` collection -/// 3. `[writable, signer]` payer -/// 4. `[signer, optional]` authority -/// 5. `[optional]` mpl_core_program (default to `CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d`) -/// 6. `[optional]` system_program (default to `11111111111111111111111111111111`) +/// 0. `[writable, signer]` payer +/// 1. `[writable]` collection +/// 2. `[]` authority +/// 3. `[optional]` mpl_core_program (default to `CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d`) +/// 4. `[optional]` system_program (default to `11111111111111111111111111111111`) #[derive(Default)] -pub struct RegisterReputationV1Builder { - agent_reputation: Option, - asset: Option, - collection: Option, +pub struct CreateReviewsCollectionV1Builder { payer: Option, + collection: Option, authority: Option, mpl_core_program: Option, system_program: Option, __remaining_accounts: Vec, } -impl RegisterReputationV1Builder { +impl CreateReviewsCollectionV1Builder { pub fn new() -> Self { Self::default() } - /// The agent reputation PDA + /// Funds the collection's rent #[inline(always)] - pub fn agent_reputation( - &mut self, - agent_reputation: solana_program::pubkey::Pubkey, - ) -> &mut Self { - self.agent_reputation = Some(agent_reputation); - self - } - /// The address of the Core asset - #[inline(always)] - pub fn asset(&mut self, asset: solana_program::pubkey::Pubkey) -> &mut Self { - self.asset = Some(asset); + pub fn payer(&mut self, payer: solana_program::pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); self } - /// `[optional account]` - /// The address of the collection + /// Reviews collection PDA at ["reviews_collection"] #[inline(always)] - pub fn collection(&mut self, collection: Option) -> &mut Self { - self.collection = collection; + pub fn collection(&mut self, collection: solana_program::pubkey::Pubkey) -> &mut Self { + self.collection = Some(collection); self } - /// The payer for additional rent + /// Reviews authority PDA at ["reviews_authority"] — becomes the collection's update_authority #[inline(always)] - pub fn payer(&mut self, payer: solana_program::pubkey::Pubkey) -> &mut Self { - self.payer = Some(payer); - self - } - /// `[optional account]` - /// Authority for the collection. If not provided, the payer will be used. - #[inline(always)] - pub fn authority(&mut self, authority: Option) -> &mut Self { - self.authority = authority; + pub fn authority(&mut self, authority: solana_program::pubkey::Pubkey) -> &mut Self { + self.authority = Some(authority); self } /// `[optional account, default to 'CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d']` @@ -202,12 +158,10 @@ impl RegisterReputationV1Builder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_program::instruction::Instruction { - let accounts = RegisterReputationV1 { - agent_reputation: self.agent_reputation.expect("agent_reputation is not set"), - asset: self.asset.expect("asset is not set"), - collection: self.collection, + let accounts = CreateReviewsCollectionV1 { payer: self.payer.expect("payer is not set"), - authority: self.authority, + collection: self.collection.expect("collection is not set"), + authority: self.authority.expect("authority is not set"), mpl_core_program: self.mpl_core_program.unwrap_or(solana_program::pubkey!( "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d" )), @@ -220,55 +174,45 @@ impl RegisterReputationV1Builder { } } -/// `register_reputation_v1` CPI accounts. -pub struct RegisterReputationV1CpiAccounts<'a, 'b> { - /// The agent reputation PDA - pub agent_reputation: &'b solana_program::account_info::AccountInfo<'a>, - /// The address of the Core asset - pub asset: &'b solana_program::account_info::AccountInfo<'a>, - /// The address of the collection - pub collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, - /// The payer for additional rent +/// `create_reviews_collection_v1` CPI accounts. +pub struct CreateReviewsCollectionV1CpiAccounts<'a, 'b> { + /// Funds the collection's rent pub payer: &'b solana_program::account_info::AccountInfo<'a>, - /// Authority for the collection. If not provided, the payer will be used. - pub authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// Reviews collection PDA at ["reviews_collection"] + pub collection: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews authority PDA at ["reviews_authority"] — becomes the collection's update_authority + pub authority: &'b solana_program::account_info::AccountInfo<'a>, /// The MPL Core program pub mpl_core_program: &'b solana_program::account_info::AccountInfo<'a>, /// The system program pub system_program: &'b solana_program::account_info::AccountInfo<'a>, } -/// `register_reputation_v1` CPI instruction. -pub struct RegisterReputationV1Cpi<'a, 'b> { +/// `create_reviews_collection_v1` CPI instruction. +pub struct CreateReviewsCollectionV1Cpi<'a, 'b> { /// The program to invoke. pub __program: &'b solana_program::account_info::AccountInfo<'a>, - /// The agent reputation PDA - pub agent_reputation: &'b solana_program::account_info::AccountInfo<'a>, - /// The address of the Core asset - pub asset: &'b solana_program::account_info::AccountInfo<'a>, - /// The address of the collection - pub collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, - /// The payer for additional rent + /// Funds the collection's rent pub payer: &'b solana_program::account_info::AccountInfo<'a>, - /// Authority for the collection. If not provided, the payer will be used. - pub authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// Reviews collection PDA at ["reviews_collection"] + pub collection: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews authority PDA at ["reviews_authority"] — becomes the collection's update_authority + pub authority: &'b solana_program::account_info::AccountInfo<'a>, /// The MPL Core program pub mpl_core_program: &'b solana_program::account_info::AccountInfo<'a>, /// The system program pub system_program: &'b solana_program::account_info::AccountInfo<'a>, } -impl<'a, 'b> RegisterReputationV1Cpi<'a, 'b> { +impl<'a, 'b> CreateReviewsCollectionV1Cpi<'a, 'b> { pub fn new( program: &'b solana_program::account_info::AccountInfo<'a>, - accounts: RegisterReputationV1CpiAccounts<'a, 'b>, + accounts: CreateReviewsCollectionV1CpiAccounts<'a, 'b>, ) -> Self { Self { __program: program, - agent_reputation: accounts.agent_reputation, - asset: accounts.asset, - collection: accounts.collection, payer: accounts.payer, + collection: accounts.collection, authority: accounts.authority, mpl_core_program: accounts.mpl_core_program, system_program: accounts.system_program, @@ -307,41 +251,19 @@ impl<'a, 'b> RegisterReputationV1Cpi<'a, 'b> { bool, )], ) -> solana_program::entrypoint::ProgramResult { - let mut accounts = Vec::with_capacity(7 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( - *self.agent_reputation.key, - false, + *self.payer.key, + true, )); accounts.push(solana_program::instruction::AccountMeta::new( - *self.asset.key, + *self.collection.key, false, )); - if let Some(collection) = self.collection { - accounts.push(solana_program::instruction::AccountMeta::new( - *collection.key, - false, - )); - } else { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( - crate::MPL_AGENT_REPUTATION_ID, - false, - )); - } - accounts.push(solana_program::instruction::AccountMeta::new( - *self.payer.key, - true, + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.authority.key, + false, )); - if let Some(authority) = self.authority { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( - *authority.key, - true, - )); - } else { - accounts.push(solana_program::instruction::AccountMeta::new_readonly( - crate::MPL_AGENT_REPUTATION_ID, - false, - )); - } accounts.push(solana_program::instruction::AccountMeta::new_readonly( *self.mpl_core_program.key, false, @@ -357,24 +279,18 @@ impl<'a, 'b> RegisterReputationV1Cpi<'a, 'b> { is_signer: remaining_account.2, }) }); - let data = borsh::to_vec(&(RegisterReputationV1InstructionData::new())).unwrap(); + let data = borsh::to_vec(&(CreateReviewsCollectionV1InstructionData::new())).unwrap(); let instruction = solana_program::instruction::Instruction { program_id: crate::MPL_AGENT_REPUTATION_ID, accounts, data, }; - let mut account_infos = Vec::with_capacity(7 + 1 + remaining_accounts.len()); + let mut account_infos = Vec::with_capacity(5 + 1 + remaining_accounts.len()); account_infos.push(self.__program.clone()); - account_infos.push(self.agent_reputation.clone()); - account_infos.push(self.asset.clone()); - if let Some(collection) = self.collection { - account_infos.push(collection.clone()); - } account_infos.push(self.payer.clone()); - if let Some(authority) = self.authority { - account_infos.push(authority.clone()); - } + account_infos.push(self.collection.clone()); + account_infos.push(self.authority.clone()); account_infos.push(self.mpl_core_program.clone()); account_infos.push(self.system_program.clone()); remaining_accounts @@ -389,29 +305,25 @@ impl<'a, 'b> RegisterReputationV1Cpi<'a, 'b> { } } -/// Instruction builder for `RegisterReputationV1` via CPI. +/// Instruction builder for `CreateReviewsCollectionV1` via CPI. /// /// ### Accounts: /// -/// 0. `[writable]` agent_reputation -/// 1. `[writable]` asset -/// 2. `[writable, optional]` collection -/// 3. `[writable, signer]` payer -/// 4. `[signer, optional]` authority -/// 5. `[]` mpl_core_program -/// 6. `[]` system_program -pub struct RegisterReputationV1CpiBuilder<'a, 'b> { - instruction: Box>, +/// 0. `[writable, signer]` payer +/// 1. `[writable]` collection +/// 2. `[]` authority +/// 3. `[]` mpl_core_program +/// 4. `[]` system_program +pub struct CreateReviewsCollectionV1CpiBuilder<'a, 'b> { + instruction: Box>, } -impl<'a, 'b> RegisterReputationV1CpiBuilder<'a, 'b> { +impl<'a, 'b> CreateReviewsCollectionV1CpiBuilder<'a, 'b> { pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { - let instruction = Box::new(RegisterReputationV1CpiBuilderInstruction { + let instruction = Box::new(CreateReviewsCollectionV1CpiBuilderInstruction { __program: program, - agent_reputation: None, - asset: None, - collection: None, payer: None, + collection: None, authority: None, mpl_core_program: None, system_program: None, @@ -419,45 +331,28 @@ impl<'a, 'b> RegisterReputationV1CpiBuilder<'a, 'b> { }); Self { instruction } } - /// The agent reputation PDA + /// Funds the collection's rent #[inline(always)] - pub fn agent_reputation( - &mut self, - agent_reputation: &'b solana_program::account_info::AccountInfo<'a>, - ) -> &mut Self { - self.instruction.agent_reputation = Some(agent_reputation); - self - } - /// The address of the Core asset - #[inline(always)] - pub fn asset(&mut self, asset: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { - self.instruction.asset = Some(asset); + pub fn payer(&mut self, payer: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.payer = Some(payer); self } - /// `[optional account]` - /// The address of the collection + /// Reviews collection PDA at ["reviews_collection"] #[inline(always)] pub fn collection( &mut self, - collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + collection: &'b solana_program::account_info::AccountInfo<'a>, ) -> &mut Self { - self.instruction.collection = collection; - self - } - /// The payer for additional rent - #[inline(always)] - pub fn payer(&mut self, payer: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { - self.instruction.payer = Some(payer); + self.instruction.collection = Some(collection); self } - /// `[optional account]` - /// Authority for the collection. If not provided, the payer will be used. + /// Reviews authority PDA at ["reviews_authority"] — becomes the collection's update_authority #[inline(always)] pub fn authority( &mut self, - authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + authority: &'b solana_program::account_info::AccountInfo<'a>, ) -> &mut Self { - self.instruction.authority = authority; + self.instruction.authority = Some(authority); self } /// The MPL Core program @@ -519,21 +414,14 @@ impl<'a, 'b> RegisterReputationV1CpiBuilder<'a, 'b> { &self, signers_seeds: &[&[&[u8]]], ) -> solana_program::entrypoint::ProgramResult { - let instruction = RegisterReputationV1Cpi { + let instruction = CreateReviewsCollectionV1Cpi { __program: self.instruction.__program, - agent_reputation: self - .instruction - .agent_reputation - .expect("agent_reputation is not set"), - - asset: self.instruction.asset.expect("asset is not set"), - - collection: self.instruction.collection, - payer: self.instruction.payer.expect("payer is not set"), - authority: self.instruction.authority, + collection: self.instruction.collection.expect("collection is not set"), + + authority: self.instruction.authority.expect("authority is not set"), mpl_core_program: self .instruction @@ -552,12 +440,10 @@ impl<'a, 'b> RegisterReputationV1CpiBuilder<'a, 'b> { } } -struct RegisterReputationV1CpiBuilderInstruction<'a, 'b> { +struct CreateReviewsCollectionV1CpiBuilderInstruction<'a, 'b> { __program: &'b solana_program::account_info::AccountInfo<'a>, - agent_reputation: Option<&'b solana_program::account_info::AccountInfo<'a>>, - asset: Option<&'b solana_program::account_info::AccountInfo<'a>>, - collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, mpl_core_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, system_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, diff --git a/clients/rust-reputation/src/generated/instructions/leave_review_v1.rs b/clients/rust-reputation/src/generated/instructions/leave_review_v1.rs new file mode 100644 index 0000000..ba602e7 --- /dev/null +++ b/clients/rust-reputation/src/generated/instructions/leave_review_v1.rs @@ -0,0 +1,1226 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! [https://github.com/metaplex-foundation/kinobi] +//! + +#[cfg(feature = "anchor")] +use anchor_lang::prelude::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Accounts. +pub struct LeaveReviewV1 { + /// Pays for the review cNFT mint and the review record PDA + pub payer: solana_program::pubkey::Pubkey, + /// The wallet leaving the review; must own the work receipt + pub reviewer: solana_program::pubkey::Pubkey, + /// The Core asset being reviewed (the agent) + pub asset: solana_program::pubkey::Pubkey, + /// The owner of the new review cNFT leaf - must equal asset.owner + pub leaf_owner: solana_program::pubkey::Pubkey, + /// Reviews authority PDA at ["reviews_authority"] — signs the Bubblegum CPI as tree_creator/collection_authority via invoke_signed + pub authority: solana_program::pubkey::Pubkey, + /// Bubblegum tree config PDA for the reviews tree + pub tree_config: solana_program::pubkey::Pubkey, + /// Reviews merkle tree at PDA ["reviews_tree", reviews_tree_index_le] + pub merkle_tree: solana_program::pubkey::Pubkey, + /// Reviews collection PDA at ["reviews_collection"] + pub core_collection: solana_program::pubkey::Pubkey, + /// Bubblegum's mpl-core CPI signer PDA + pub mpl_core_cpi_signer: solana_program::pubkey::Pubkey, + /// MPL Noop / log wrapper program + pub log_wrapper: solana_program::pubkey::Pubkey, + /// MPL Account Compression program (used for both Bubblegum mint and verify_leaf CPI) + pub compression_program: solana_program::pubkey::Pubkey, + /// The MPL Core program + pub mpl_core_program: solana_program::pubkey::Pubkey, + /// The MPL Bubblegum program + pub bubblegum_program: solana_program::pubkey::Pubkey, + /// Receipts Bubblegum merkle tree holding the receipt being referenced + pub receipts_merkle_tree: solana_program::pubkey::Pubkey, + /// Canonical receipts collection PDA from mpl-agent-tools at ["receipts_collection"] + pub receipts_collection: solana_program::pubkey::Pubkey, + /// ReviewRecordV1 PDA seeded with the receipt's bubblegum asset id - idempotency gate + pub review_record: solana_program::pubkey::Pubkey, + /// The system program + pub system_program: solana_program::pubkey::Pubkey, +} + +impl LeaveReviewV1 { + pub fn instruction( + &self, + args: LeaveReviewV1InstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: LeaveReviewV1InstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(17 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + self.payer, true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.reviewer, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.asset, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.leaf_owner, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.authority, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.tree_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.merkle_tree, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.core_collection, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.mpl_core_cpi_signer, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.log_wrapper, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.compression_program, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.mpl_core_program, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.bubblegum_program, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.receipts_merkle_tree, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.receipts_collection, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.review_record, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = borsh::to_vec(&(LeaveReviewV1InstructionData::new())).unwrap(); + let mut args = borsh::to_vec(&args).unwrap(); + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: crate::MPL_AGENT_REPUTATION_ID, + accounts, + data, + } + } +} + +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +pub struct LeaveReviewV1InstructionData { + discriminator: u8, + pad: [u8; 6], + pad2: [u8; 3], +} + +impl LeaveReviewV1InstructionData { + pub fn new() -> Self { + Self { + discriminator: 0, + pad: [0, 0, 0, 0, 0, 0], + pad2: [0, 0, 0], + } + } +} + +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LeaveReviewV1InstructionArgs { + pub rating: u8, + pub reviews_tree_index: u64, + pub receipts_tree_index: u64, + pub receipt_nonce: u64, + pub receipt_index: u32, + pub receipt_flags: u8, + pub receipt_root: [u8; 32], + pub receipt_data_hash: [u8; 32], + pub receipt_asset_data_hash: [u8; 32], + pub feedback_uri: String, +} + +/// Instruction builder for `LeaveReviewV1`. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[signer]` reviewer +/// 2. `[]` asset +/// 3. `[]` leaf_owner +/// 4. `[]` authority +/// 5. `[writable]` tree_config +/// 6. `[writable]` merkle_tree +/// 7. `[writable]` core_collection +/// 8. `[]` mpl_core_cpi_signer +/// 9. `[optional]` log_wrapper (default to `mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3`) +/// 10. `[optional]` compression_program (default to `mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW`) +/// 11. `[optional]` mpl_core_program (default to `CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d`) +/// 12. `[optional]` bubblegum_program (default to `BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY`) +/// 13. `[]` receipts_merkle_tree +/// 14. `[]` receipts_collection +/// 15. `[writable]` review_record +/// 16. `[optional]` system_program (default to `11111111111111111111111111111111`) +#[derive(Default)] +pub struct LeaveReviewV1Builder { + payer: Option, + reviewer: Option, + asset: Option, + leaf_owner: Option, + authority: Option, + tree_config: Option, + merkle_tree: Option, + core_collection: Option, + mpl_core_cpi_signer: Option, + log_wrapper: Option, + compression_program: Option, + mpl_core_program: Option, + bubblegum_program: Option, + receipts_merkle_tree: Option, + receipts_collection: Option, + review_record: Option, + system_program: Option, + rating: Option, + reviews_tree_index: Option, + receipts_tree_index: Option, + receipt_nonce: Option, + receipt_index: Option, + receipt_flags: Option, + receipt_root: Option<[u8; 32]>, + receipt_data_hash: Option<[u8; 32]>, + receipt_asset_data_hash: Option<[u8; 32]>, + feedback_uri: Option, + __remaining_accounts: Vec, +} + +impl LeaveReviewV1Builder { + pub fn new() -> Self { + Self::default() + } + /// Pays for the review cNFT mint and the review record PDA + #[inline(always)] + pub fn payer(&mut self, payer: solana_program::pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); + self + } + /// The wallet leaving the review; must own the work receipt + #[inline(always)] + pub fn reviewer(&mut self, reviewer: solana_program::pubkey::Pubkey) -> &mut Self { + self.reviewer = Some(reviewer); + self + } + /// The Core asset being reviewed (the agent) + #[inline(always)] + pub fn asset(&mut self, asset: solana_program::pubkey::Pubkey) -> &mut Self { + self.asset = Some(asset); + self + } + /// The owner of the new review cNFT leaf - must equal asset.owner + #[inline(always)] + pub fn leaf_owner(&mut self, leaf_owner: solana_program::pubkey::Pubkey) -> &mut Self { + self.leaf_owner = Some(leaf_owner); + self + } + /// Reviews authority PDA at ["reviews_authority"] — signs the Bubblegum CPI as tree_creator/collection_authority via invoke_signed + #[inline(always)] + pub fn authority(&mut self, authority: solana_program::pubkey::Pubkey) -> &mut Self { + self.authority = Some(authority); + self + } + /// Bubblegum tree config PDA for the reviews tree + #[inline(always)] + pub fn tree_config(&mut self, tree_config: solana_program::pubkey::Pubkey) -> &mut Self { + self.tree_config = Some(tree_config); + self + } + /// Reviews merkle tree at PDA ["reviews_tree", reviews_tree_index_le] + #[inline(always)] + pub fn merkle_tree(&mut self, merkle_tree: solana_program::pubkey::Pubkey) -> &mut Self { + self.merkle_tree = Some(merkle_tree); + self + } + /// Reviews collection PDA at ["reviews_collection"] + #[inline(always)] + pub fn core_collection( + &mut self, + core_collection: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.core_collection = Some(core_collection); + self + } + /// Bubblegum's mpl-core CPI signer PDA + #[inline(always)] + pub fn mpl_core_cpi_signer( + &mut self, + mpl_core_cpi_signer: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.mpl_core_cpi_signer = Some(mpl_core_cpi_signer); + self + } + /// `[optional account, default to 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3']` + /// MPL Noop / log wrapper program + #[inline(always)] + pub fn log_wrapper(&mut self, log_wrapper: solana_program::pubkey::Pubkey) -> &mut Self { + self.log_wrapper = Some(log_wrapper); + self + } + /// `[optional account, default to 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW']` + /// MPL Account Compression program (used for both Bubblegum mint and verify_leaf CPI) + #[inline(always)] + pub fn compression_program( + &mut self, + compression_program: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.compression_program = Some(compression_program); + self + } + /// `[optional account, default to 'CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d']` + /// The MPL Core program + #[inline(always)] + pub fn mpl_core_program( + &mut self, + mpl_core_program: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.mpl_core_program = Some(mpl_core_program); + self + } + /// `[optional account, default to 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY']` + /// The MPL Bubblegum program + #[inline(always)] + pub fn bubblegum_program( + &mut self, + bubblegum_program: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.bubblegum_program = Some(bubblegum_program); + self + } + /// Receipts Bubblegum merkle tree holding the receipt being referenced + #[inline(always)] + pub fn receipts_merkle_tree( + &mut self, + receipts_merkle_tree: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.receipts_merkle_tree = Some(receipts_merkle_tree); + self + } + /// Canonical receipts collection PDA from mpl-agent-tools at ["receipts_collection"] + #[inline(always)] + pub fn receipts_collection( + &mut self, + receipts_collection: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.receipts_collection = Some(receipts_collection); + self + } + /// ReviewRecordV1 PDA seeded with the receipt's bubblegum asset id - idempotency gate + #[inline(always)] + pub fn review_record(&mut self, review_record: solana_program::pubkey::Pubkey) -> &mut Self { + self.review_record = Some(review_record); + self + } + /// `[optional account, default to '11111111111111111111111111111111']` + /// The system program + #[inline(always)] + pub fn system_program(&mut self, system_program: solana_program::pubkey::Pubkey) -> &mut Self { + self.system_program = Some(system_program); + self + } + #[inline(always)] + pub fn rating(&mut self, rating: u8) -> &mut Self { + self.rating = Some(rating); + self + } + #[inline(always)] + pub fn reviews_tree_index(&mut self, reviews_tree_index: u64) -> &mut Self { + self.reviews_tree_index = Some(reviews_tree_index); + self + } + #[inline(always)] + pub fn receipts_tree_index(&mut self, receipts_tree_index: u64) -> &mut Self { + self.receipts_tree_index = Some(receipts_tree_index); + self + } + #[inline(always)] + pub fn receipt_nonce(&mut self, receipt_nonce: u64) -> &mut Self { + self.receipt_nonce = Some(receipt_nonce); + self + } + #[inline(always)] + pub fn receipt_index(&mut self, receipt_index: u32) -> &mut Self { + self.receipt_index = Some(receipt_index); + self + } + #[inline(always)] + pub fn receipt_flags(&mut self, receipt_flags: u8) -> &mut Self { + self.receipt_flags = Some(receipt_flags); + self + } + #[inline(always)] + pub fn receipt_root(&mut self, receipt_root: [u8; 32]) -> &mut Self { + self.receipt_root = Some(receipt_root); + self + } + #[inline(always)] + pub fn receipt_data_hash(&mut self, receipt_data_hash: [u8; 32]) -> &mut Self { + self.receipt_data_hash = Some(receipt_data_hash); + self + } + #[inline(always)] + pub fn receipt_asset_data_hash(&mut self, receipt_asset_data_hash: [u8; 32]) -> &mut Self { + self.receipt_asset_data_hash = Some(receipt_asset_data_hash); + self + } + #[inline(always)] + pub fn feedback_uri(&mut self, feedback_uri: String) -> &mut Self { + self.feedback_uri = Some(feedback_uri); + self + } + /// Add an aditional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_program::instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_program::instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_program::instruction::Instruction { + let accounts = LeaveReviewV1 { + payer: self.payer.expect("payer is not set"), + reviewer: self.reviewer.expect("reviewer is not set"), + asset: self.asset.expect("asset is not set"), + leaf_owner: self.leaf_owner.expect("leaf_owner is not set"), + authority: self.authority.expect("authority is not set"), + tree_config: self.tree_config.expect("tree_config is not set"), + merkle_tree: self.merkle_tree.expect("merkle_tree is not set"), + core_collection: self.core_collection.expect("core_collection is not set"), + mpl_core_cpi_signer: self + .mpl_core_cpi_signer + .expect("mpl_core_cpi_signer is not set"), + log_wrapper: self.log_wrapper.unwrap_or(solana_program::pubkey!( + "mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3" + )), + compression_program: self.compression_program.unwrap_or(solana_program::pubkey!( + "mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW" + )), + mpl_core_program: self.mpl_core_program.unwrap_or(solana_program::pubkey!( + "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d" + )), + bubblegum_program: self.bubblegum_program.unwrap_or(solana_program::pubkey!( + "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY" + )), + receipts_merkle_tree: self + .receipts_merkle_tree + .expect("receipts_merkle_tree is not set"), + receipts_collection: self + .receipts_collection + .expect("receipts_collection is not set"), + review_record: self.review_record.expect("review_record is not set"), + system_program: self + .system_program + .unwrap_or(solana_program::pubkey!("11111111111111111111111111111111")), + }; + let args = LeaveReviewV1InstructionArgs { + rating: self.rating.clone().expect("rating is not set"), + reviews_tree_index: self + .reviews_tree_index + .clone() + .expect("reviews_tree_index is not set"), + receipts_tree_index: self + .receipts_tree_index + .clone() + .expect("receipts_tree_index is not set"), + receipt_nonce: self + .receipt_nonce + .clone() + .expect("receipt_nonce is not set"), + receipt_index: self + .receipt_index + .clone() + .expect("receipt_index is not set"), + receipt_flags: self + .receipt_flags + .clone() + .expect("receipt_flags is not set"), + receipt_root: self.receipt_root.clone().expect("receipt_root is not set"), + receipt_data_hash: self + .receipt_data_hash + .clone() + .expect("receipt_data_hash is not set"), + receipt_asset_data_hash: self + .receipt_asset_data_hash + .clone() + .expect("receipt_asset_data_hash is not set"), + feedback_uri: self.feedback_uri.clone().expect("feedback_uri is not set"), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `leave_review_v1` CPI accounts. +pub struct LeaveReviewV1CpiAccounts<'a, 'b> { + /// Pays for the review cNFT mint and the review record PDA + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + /// The wallet leaving the review; must own the work receipt + pub reviewer: &'b solana_program::account_info::AccountInfo<'a>, + /// The Core asset being reviewed (the agent) + pub asset: &'b solana_program::account_info::AccountInfo<'a>, + /// The owner of the new review cNFT leaf - must equal asset.owner + pub leaf_owner: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews authority PDA at ["reviews_authority"] — signs the Bubblegum CPI as tree_creator/collection_authority via invoke_signed + pub authority: &'b solana_program::account_info::AccountInfo<'a>, + /// Bubblegum tree config PDA for the reviews tree + pub tree_config: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews merkle tree at PDA ["reviews_tree", reviews_tree_index_le] + pub merkle_tree: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews collection PDA at ["reviews_collection"] + pub core_collection: &'b solana_program::account_info::AccountInfo<'a>, + /// Bubblegum's mpl-core CPI signer PDA + pub mpl_core_cpi_signer: &'b solana_program::account_info::AccountInfo<'a>, + /// MPL Noop / log wrapper program + pub log_wrapper: &'b solana_program::account_info::AccountInfo<'a>, + /// MPL Account Compression program (used for both Bubblegum mint and verify_leaf CPI) + pub compression_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The MPL Core program + pub mpl_core_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The MPL Bubblegum program + pub bubblegum_program: &'b solana_program::account_info::AccountInfo<'a>, + /// Receipts Bubblegum merkle tree holding the receipt being referenced + pub receipts_merkle_tree: &'b solana_program::account_info::AccountInfo<'a>, + /// Canonical receipts collection PDA from mpl-agent-tools at ["receipts_collection"] + pub receipts_collection: &'b solana_program::account_info::AccountInfo<'a>, + /// ReviewRecordV1 PDA seeded with the receipt's bubblegum asset id - idempotency gate + pub review_record: &'b solana_program::account_info::AccountInfo<'a>, + /// The system program + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `leave_review_v1` CPI instruction. +pub struct LeaveReviewV1Cpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + /// Pays for the review cNFT mint and the review record PDA + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + /// The wallet leaving the review; must own the work receipt + pub reviewer: &'b solana_program::account_info::AccountInfo<'a>, + /// The Core asset being reviewed (the agent) + pub asset: &'b solana_program::account_info::AccountInfo<'a>, + /// The owner of the new review cNFT leaf - must equal asset.owner + pub leaf_owner: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews authority PDA at ["reviews_authority"] — signs the Bubblegum CPI as tree_creator/collection_authority via invoke_signed + pub authority: &'b solana_program::account_info::AccountInfo<'a>, + /// Bubblegum tree config PDA for the reviews tree + pub tree_config: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews merkle tree at PDA ["reviews_tree", reviews_tree_index_le] + pub merkle_tree: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews collection PDA at ["reviews_collection"] + pub core_collection: &'b solana_program::account_info::AccountInfo<'a>, + /// Bubblegum's mpl-core CPI signer PDA + pub mpl_core_cpi_signer: &'b solana_program::account_info::AccountInfo<'a>, + /// MPL Noop / log wrapper program + pub log_wrapper: &'b solana_program::account_info::AccountInfo<'a>, + /// MPL Account Compression program (used for both Bubblegum mint and verify_leaf CPI) + pub compression_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The MPL Core program + pub mpl_core_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The MPL Bubblegum program + pub bubblegum_program: &'b solana_program::account_info::AccountInfo<'a>, + /// Receipts Bubblegum merkle tree holding the receipt being referenced + pub receipts_merkle_tree: &'b solana_program::account_info::AccountInfo<'a>, + /// Canonical receipts collection PDA from mpl-agent-tools at ["receipts_collection"] + pub receipts_collection: &'b solana_program::account_info::AccountInfo<'a>, + /// ReviewRecordV1 PDA seeded with the receipt's bubblegum asset id - idempotency gate + pub review_record: &'b solana_program::account_info::AccountInfo<'a>, + /// The system program + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: LeaveReviewV1InstructionArgs, +} + +impl<'a, 'b> LeaveReviewV1Cpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: LeaveReviewV1CpiAccounts<'a, 'b>, + args: LeaveReviewV1InstructionArgs, + ) -> Self { + Self { + __program: program, + payer: accounts.payer, + reviewer: accounts.reviewer, + asset: accounts.asset, + leaf_owner: accounts.leaf_owner, + authority: accounts.authority, + tree_config: accounts.tree_config, + merkle_tree: accounts.merkle_tree, + core_collection: accounts.core_collection, + mpl_core_cpi_signer: accounts.mpl_core_cpi_signer, + log_wrapper: accounts.log_wrapper, + compression_program: accounts.compression_program, + mpl_core_program: accounts.mpl_core_program, + bubblegum_program: accounts.bubblegum_program, + receipts_merkle_tree: accounts.receipts_merkle_tree, + receipts_collection: accounts.receipts_collection, + review_record: accounts.review_record, + system_program: accounts.system_program, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + let mut accounts = Vec::with_capacity(17 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.payer.key, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.reviewer.key, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.asset.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.leaf_owner.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.authority.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.tree_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.merkle_tree.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.core_collection.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.mpl_core_cpi_signer.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.log_wrapper.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.compression_program.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.mpl_core_program.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.bubblegum_program.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.receipts_merkle_tree.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.receipts_collection.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.review_record.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.system_program.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_program::instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_writable: remaining_account.1, + is_signer: remaining_account.2, + }) + }); + let mut data = borsh::to_vec(&(LeaveReviewV1InstructionData::new())).unwrap(); + let mut args = borsh::to_vec(&self.__args).unwrap(); + data.append(&mut args); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::MPL_AGENT_REPUTATION_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(17 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.payer.clone()); + account_infos.push(self.reviewer.clone()); + account_infos.push(self.asset.clone()); + account_infos.push(self.leaf_owner.clone()); + account_infos.push(self.authority.clone()); + account_infos.push(self.tree_config.clone()); + account_infos.push(self.merkle_tree.clone()); + account_infos.push(self.core_collection.clone()); + account_infos.push(self.mpl_core_cpi_signer.clone()); + account_infos.push(self.log_wrapper.clone()); + account_infos.push(self.compression_program.clone()); + account_infos.push(self.mpl_core_program.clone()); + account_infos.push(self.bubblegum_program.clone()); + account_infos.push(self.receipts_merkle_tree.clone()); + account_infos.push(self.receipts_collection.clone()); + account_infos.push(self.review_record.clone()); + account_infos.push(self.system_program.clone()); + remaining_accounts + .iter() + .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); + + if signers_seeds.is_empty() { + solana_program::program::invoke(&instruction, &account_infos) + } else { + solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds) + } + } +} + +/// Instruction builder for `LeaveReviewV1` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[signer]` reviewer +/// 2. `[]` asset +/// 3. `[]` leaf_owner +/// 4. `[]` authority +/// 5. `[writable]` tree_config +/// 6. `[writable]` merkle_tree +/// 7. `[writable]` core_collection +/// 8. `[]` mpl_core_cpi_signer +/// 9. `[]` log_wrapper +/// 10. `[]` compression_program +/// 11. `[]` mpl_core_program +/// 12. `[]` bubblegum_program +/// 13. `[]` receipts_merkle_tree +/// 14. `[]` receipts_collection +/// 15. `[writable]` review_record +/// 16. `[]` system_program +pub struct LeaveReviewV1CpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> LeaveReviewV1CpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(LeaveReviewV1CpiBuilderInstruction { + __program: program, + payer: None, + reviewer: None, + asset: None, + leaf_owner: None, + authority: None, + tree_config: None, + merkle_tree: None, + core_collection: None, + mpl_core_cpi_signer: None, + log_wrapper: None, + compression_program: None, + mpl_core_program: None, + bubblegum_program: None, + receipts_merkle_tree: None, + receipts_collection: None, + review_record: None, + system_program: None, + rating: None, + reviews_tree_index: None, + receipts_tree_index: None, + receipt_nonce: None, + receipt_index: None, + receipt_flags: None, + receipt_root: None, + receipt_data_hash: None, + receipt_asset_data_hash: None, + feedback_uri: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + /// Pays for the review cNFT mint and the review record PDA + #[inline(always)] + pub fn payer(&mut self, payer: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.payer = Some(payer); + self + } + /// The wallet leaving the review; must own the work receipt + #[inline(always)] + pub fn reviewer( + &mut self, + reviewer: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.reviewer = Some(reviewer); + self + } + /// The Core asset being reviewed (the agent) + #[inline(always)] + pub fn asset(&mut self, asset: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.asset = Some(asset); + self + } + /// The owner of the new review cNFT leaf - must equal asset.owner + #[inline(always)] + pub fn leaf_owner( + &mut self, + leaf_owner: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.leaf_owner = Some(leaf_owner); + self + } + /// Reviews authority PDA at ["reviews_authority"] — signs the Bubblegum CPI as tree_creator/collection_authority via invoke_signed + #[inline(always)] + pub fn authority( + &mut self, + authority: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.authority = Some(authority); + self + } + /// Bubblegum tree config PDA for the reviews tree + #[inline(always)] + pub fn tree_config( + &mut self, + tree_config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.tree_config = Some(tree_config); + self + } + /// Reviews merkle tree at PDA ["reviews_tree", reviews_tree_index_le] + #[inline(always)] + pub fn merkle_tree( + &mut self, + merkle_tree: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.merkle_tree = Some(merkle_tree); + self + } + /// Reviews collection PDA at ["reviews_collection"] + #[inline(always)] + pub fn core_collection( + &mut self, + core_collection: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.core_collection = Some(core_collection); + self + } + /// Bubblegum's mpl-core CPI signer PDA + #[inline(always)] + pub fn mpl_core_cpi_signer( + &mut self, + mpl_core_cpi_signer: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.mpl_core_cpi_signer = Some(mpl_core_cpi_signer); + self + } + /// MPL Noop / log wrapper program + #[inline(always)] + pub fn log_wrapper( + &mut self, + log_wrapper: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.log_wrapper = Some(log_wrapper); + self + } + /// MPL Account Compression program (used for both Bubblegum mint and verify_leaf CPI) + #[inline(always)] + pub fn compression_program( + &mut self, + compression_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.compression_program = Some(compression_program); + self + } + /// The MPL Core program + #[inline(always)] + pub fn mpl_core_program( + &mut self, + mpl_core_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.mpl_core_program = Some(mpl_core_program); + self + } + /// The MPL Bubblegum program + #[inline(always)] + pub fn bubblegum_program( + &mut self, + bubblegum_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.bubblegum_program = Some(bubblegum_program); + self + } + /// Receipts Bubblegum merkle tree holding the receipt being referenced + #[inline(always)] + pub fn receipts_merkle_tree( + &mut self, + receipts_merkle_tree: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.receipts_merkle_tree = Some(receipts_merkle_tree); + self + } + /// Canonical receipts collection PDA from mpl-agent-tools at ["receipts_collection"] + #[inline(always)] + pub fn receipts_collection( + &mut self, + receipts_collection: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.receipts_collection = Some(receipts_collection); + self + } + /// ReviewRecordV1 PDA seeded with the receipt's bubblegum asset id - idempotency gate + #[inline(always)] + pub fn review_record( + &mut self, + review_record: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.review_record = Some(review_record); + self + } + /// The system program + #[inline(always)] + pub fn system_program( + &mut self, + system_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.system_program = Some(system_program); + self + } + #[inline(always)] + pub fn rating(&mut self, rating: u8) -> &mut Self { + self.instruction.rating = Some(rating); + self + } + #[inline(always)] + pub fn reviews_tree_index(&mut self, reviews_tree_index: u64) -> &mut Self { + self.instruction.reviews_tree_index = Some(reviews_tree_index); + self + } + #[inline(always)] + pub fn receipts_tree_index(&mut self, receipts_tree_index: u64) -> &mut Self { + self.instruction.receipts_tree_index = Some(receipts_tree_index); + self + } + #[inline(always)] + pub fn receipt_nonce(&mut self, receipt_nonce: u64) -> &mut Self { + self.instruction.receipt_nonce = Some(receipt_nonce); + self + } + #[inline(always)] + pub fn receipt_index(&mut self, receipt_index: u32) -> &mut Self { + self.instruction.receipt_index = Some(receipt_index); + self + } + #[inline(always)] + pub fn receipt_flags(&mut self, receipt_flags: u8) -> &mut Self { + self.instruction.receipt_flags = Some(receipt_flags); + self + } + #[inline(always)] + pub fn receipt_root(&mut self, receipt_root: [u8; 32]) -> &mut Self { + self.instruction.receipt_root = Some(receipt_root); + self + } + #[inline(always)] + pub fn receipt_data_hash(&mut self, receipt_data_hash: [u8; 32]) -> &mut Self { + self.instruction.receipt_data_hash = Some(receipt_data_hash); + self + } + #[inline(always)] + pub fn receipt_asset_data_hash(&mut self, receipt_asset_data_hash: [u8; 32]) -> &mut Self { + self.instruction.receipt_asset_data_hash = Some(receipt_asset_data_hash); + self + } + #[inline(always)] + pub fn feedback_uri(&mut self, feedback_uri: String) -> &mut Self { + self.instruction.feedback_uri = Some(feedback_uri); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_program::account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction + .__remaining_accounts + .push((account, is_writable, is_signer)); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + let args = LeaveReviewV1InstructionArgs { + rating: self.instruction.rating.clone().expect("rating is not set"), + reviews_tree_index: self + .instruction + .reviews_tree_index + .clone() + .expect("reviews_tree_index is not set"), + receipts_tree_index: self + .instruction + .receipts_tree_index + .clone() + .expect("receipts_tree_index is not set"), + receipt_nonce: self + .instruction + .receipt_nonce + .clone() + .expect("receipt_nonce is not set"), + receipt_index: self + .instruction + .receipt_index + .clone() + .expect("receipt_index is not set"), + receipt_flags: self + .instruction + .receipt_flags + .clone() + .expect("receipt_flags is not set"), + receipt_root: self + .instruction + .receipt_root + .clone() + .expect("receipt_root is not set"), + receipt_data_hash: self + .instruction + .receipt_data_hash + .clone() + .expect("receipt_data_hash is not set"), + receipt_asset_data_hash: self + .instruction + .receipt_asset_data_hash + .clone() + .expect("receipt_asset_data_hash is not set"), + feedback_uri: self + .instruction + .feedback_uri + .clone() + .expect("feedback_uri is not set"), + }; + let instruction = LeaveReviewV1Cpi { + __program: self.instruction.__program, + + payer: self.instruction.payer.expect("payer is not set"), + + reviewer: self.instruction.reviewer.expect("reviewer is not set"), + + asset: self.instruction.asset.expect("asset is not set"), + + leaf_owner: self.instruction.leaf_owner.expect("leaf_owner is not set"), + + authority: self.instruction.authority.expect("authority is not set"), + + tree_config: self + .instruction + .tree_config + .expect("tree_config is not set"), + + merkle_tree: self + .instruction + .merkle_tree + .expect("merkle_tree is not set"), + + core_collection: self + .instruction + .core_collection + .expect("core_collection is not set"), + + mpl_core_cpi_signer: self + .instruction + .mpl_core_cpi_signer + .expect("mpl_core_cpi_signer is not set"), + + log_wrapper: self + .instruction + .log_wrapper + .expect("log_wrapper is not set"), + + compression_program: self + .instruction + .compression_program + .expect("compression_program is not set"), + + mpl_core_program: self + .instruction + .mpl_core_program + .expect("mpl_core_program is not set"), + + bubblegum_program: self + .instruction + .bubblegum_program + .expect("bubblegum_program is not set"), + + receipts_merkle_tree: self + .instruction + .receipts_merkle_tree + .expect("receipts_merkle_tree is not set"), + + receipts_collection: self + .instruction + .receipts_collection + .expect("receipts_collection is not set"), + + review_record: self + .instruction + .review_record + .expect("review_record is not set"), + + system_program: self + .instruction + .system_program + .expect("system_program is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +struct LeaveReviewV1CpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + reviewer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + asset: Option<&'b solana_program::account_info::AccountInfo<'a>>, + leaf_owner: Option<&'b solana_program::account_info::AccountInfo<'a>>, + authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + tree_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + merkle_tree: Option<&'b solana_program::account_info::AccountInfo<'a>>, + core_collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + mpl_core_cpi_signer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + log_wrapper: Option<&'b solana_program::account_info::AccountInfo<'a>>, + compression_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + mpl_core_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + bubblegum_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + receipts_merkle_tree: Option<&'b solana_program::account_info::AccountInfo<'a>>, + receipts_collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + review_record: Option<&'b solana_program::account_info::AccountInfo<'a>>, + system_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + rating: Option, + reviews_tree_index: Option, + receipts_tree_index: Option, + receipt_nonce: Option, + receipt_index: Option, + receipt_flags: Option, + receipt_root: Option<[u8; 32]>, + receipt_data_hash: Option<[u8; 32]>, + receipt_asset_data_hash: Option<[u8; 32]>, + feedback_uri: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/clients/rust-reputation/src/generated/instructions/mod.rs b/clients/rust-reputation/src/generated/instructions/mod.rs index 7b6ad40..f0e4ae6 100644 --- a/clients/rust-reputation/src/generated/instructions/mod.rs +++ b/clients/rust-reputation/src/generated/instructions/mod.rs @@ -5,6 +5,10 @@ //! [https://github.com/metaplex-foundation/kinobi] //! -pub(crate) mod r#register_reputation_v1; +pub(crate) mod r#create_reviews_collection_v1; +pub(crate) mod r#leave_review_v1; +pub(crate) mod r#register_reviews_tree_v1; -pub use self::r#register_reputation_v1::*; +pub use self::r#create_reviews_collection_v1::*; +pub use self::r#leave_review_v1::*; +pub use self::r#register_reviews_tree_v1::*; diff --git a/clients/rust-reputation/src/generated/instructions/register_reviews_tree_v1.rs b/clients/rust-reputation/src/generated/instructions/register_reviews_tree_v1.rs new file mode 100644 index 0000000..5d86939 --- /dev/null +++ b/clients/rust-reputation/src/generated/instructions/register_reviews_tree_v1.rs @@ -0,0 +1,703 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! [https://github.com/metaplex-foundation/kinobi] +//! + +#[cfg(feature = "anchor")] +use anchor_lang::prelude::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Accounts. +pub struct RegisterReviewsTreeV1 { + /// Funds the tree rent + pub payer: solana_program::pubkey::Pubkey, + /// Reviews authority PDA at ["reviews_authority"] — set as tree_creator + pub authority: solana_program::pubkey::Pubkey, + /// Reviews merkle tree PDA at ["reviews_tree", tree_index_le] + pub merkle_tree: solana_program::pubkey::Pubkey, + /// Bubblegum tree config PDA (derived from merkle_tree) + pub tree_config: solana_program::pubkey::Pubkey, + /// MPL Noop / log wrapper program + pub log_wrapper: solana_program::pubkey::Pubkey, + /// MPL Account Compression program + pub compression_program: solana_program::pubkey::Pubkey, + /// The MPL Bubblegum program + pub bubblegum_program: solana_program::pubkey::Pubkey, + /// The system program + pub system_program: solana_program::pubkey::Pubkey, +} + +impl RegisterReviewsTreeV1 { + pub fn instruction( + &self, + args: RegisterReviewsTreeV1InstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: RegisterReviewsTreeV1InstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(8 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + self.payer, true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.authority, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.merkle_tree, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.tree_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.log_wrapper, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.compression_program, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.bubblegum_program, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = borsh::to_vec(&(RegisterReviewsTreeV1InstructionData::new())).unwrap(); + let mut args = borsh::to_vec(&args).unwrap(); + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: crate::MPL_AGENT_REPUTATION_ID, + accounts, + data, + } + } +} + +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +pub struct RegisterReviewsTreeV1InstructionData { + discriminator: u8, + pad: [u8; 7], + pad2: [u8; 4], +} + +impl RegisterReviewsTreeV1InstructionData { + pub fn new() -> Self { + Self { + discriminator: 2, + pad: [0, 0, 0, 0, 0, 0, 0], + pad2: [0, 0, 0, 0], + } + } +} + +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RegisterReviewsTreeV1InstructionArgs { + pub tree_index: u64, + pub max_depth: u32, + pub max_buffer_size: u32, + pub canopy_depth: u32, +} + +/// Instruction builder for `RegisterReviewsTreeV1`. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[]` authority +/// 2. `[writable]` merkle_tree +/// 3. `[writable]` tree_config +/// 4. `[optional]` log_wrapper (default to `mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3`) +/// 5. `[optional]` compression_program (default to `mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW`) +/// 6. `[optional]` bubblegum_program (default to `BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY`) +/// 7. `[optional]` system_program (default to `11111111111111111111111111111111`) +#[derive(Default)] +pub struct RegisterReviewsTreeV1Builder { + payer: Option, + authority: Option, + merkle_tree: Option, + tree_config: Option, + log_wrapper: Option, + compression_program: Option, + bubblegum_program: Option, + system_program: Option, + tree_index: Option, + max_depth: Option, + max_buffer_size: Option, + canopy_depth: Option, + __remaining_accounts: Vec, +} + +impl RegisterReviewsTreeV1Builder { + pub fn new() -> Self { + Self::default() + } + /// Funds the tree rent + #[inline(always)] + pub fn payer(&mut self, payer: solana_program::pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); + self + } + /// Reviews authority PDA at ["reviews_authority"] — set as tree_creator + #[inline(always)] + pub fn authority(&mut self, authority: solana_program::pubkey::Pubkey) -> &mut Self { + self.authority = Some(authority); + self + } + /// Reviews merkle tree PDA at ["reviews_tree", tree_index_le] + #[inline(always)] + pub fn merkle_tree(&mut self, merkle_tree: solana_program::pubkey::Pubkey) -> &mut Self { + self.merkle_tree = Some(merkle_tree); + self + } + /// Bubblegum tree config PDA (derived from merkle_tree) + #[inline(always)] + pub fn tree_config(&mut self, tree_config: solana_program::pubkey::Pubkey) -> &mut Self { + self.tree_config = Some(tree_config); + self + } + /// `[optional account, default to 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3']` + /// MPL Noop / log wrapper program + #[inline(always)] + pub fn log_wrapper(&mut self, log_wrapper: solana_program::pubkey::Pubkey) -> &mut Self { + self.log_wrapper = Some(log_wrapper); + self + } + /// `[optional account, default to 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW']` + /// MPL Account Compression program + #[inline(always)] + pub fn compression_program( + &mut self, + compression_program: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.compression_program = Some(compression_program); + self + } + /// `[optional account, default to 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY']` + /// The MPL Bubblegum program + #[inline(always)] + pub fn bubblegum_program( + &mut self, + bubblegum_program: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.bubblegum_program = Some(bubblegum_program); + self + } + /// `[optional account, default to '11111111111111111111111111111111']` + /// The system program + #[inline(always)] + pub fn system_program(&mut self, system_program: solana_program::pubkey::Pubkey) -> &mut Self { + self.system_program = Some(system_program); + self + } + #[inline(always)] + pub fn tree_index(&mut self, tree_index: u64) -> &mut Self { + self.tree_index = Some(tree_index); + self + } + #[inline(always)] + pub fn max_depth(&mut self, max_depth: u32) -> &mut Self { + self.max_depth = Some(max_depth); + self + } + #[inline(always)] + pub fn max_buffer_size(&mut self, max_buffer_size: u32) -> &mut Self { + self.max_buffer_size = Some(max_buffer_size); + self + } + #[inline(always)] + pub fn canopy_depth(&mut self, canopy_depth: u32) -> &mut Self { + self.canopy_depth = Some(canopy_depth); + self + } + /// Add an aditional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_program::instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_program::instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_program::instruction::Instruction { + let accounts = RegisterReviewsTreeV1 { + payer: self.payer.expect("payer is not set"), + authority: self.authority.expect("authority is not set"), + merkle_tree: self.merkle_tree.expect("merkle_tree is not set"), + tree_config: self.tree_config.expect("tree_config is not set"), + log_wrapper: self.log_wrapper.unwrap_or(solana_program::pubkey!( + "mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3" + )), + compression_program: self.compression_program.unwrap_or(solana_program::pubkey!( + "mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW" + )), + bubblegum_program: self.bubblegum_program.unwrap_or(solana_program::pubkey!( + "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY" + )), + system_program: self + .system_program + .unwrap_or(solana_program::pubkey!("11111111111111111111111111111111")), + }; + let args = RegisterReviewsTreeV1InstructionArgs { + tree_index: self.tree_index.clone().expect("tree_index is not set"), + max_depth: self.max_depth.clone().expect("max_depth is not set"), + max_buffer_size: self + .max_buffer_size + .clone() + .expect("max_buffer_size is not set"), + canopy_depth: self.canopy_depth.clone().expect("canopy_depth is not set"), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `register_reviews_tree_v1` CPI accounts. +pub struct RegisterReviewsTreeV1CpiAccounts<'a, 'b> { + /// Funds the tree rent + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews authority PDA at ["reviews_authority"] — set as tree_creator + pub authority: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews merkle tree PDA at ["reviews_tree", tree_index_le] + pub merkle_tree: &'b solana_program::account_info::AccountInfo<'a>, + /// Bubblegum tree config PDA (derived from merkle_tree) + pub tree_config: &'b solana_program::account_info::AccountInfo<'a>, + /// MPL Noop / log wrapper program + pub log_wrapper: &'b solana_program::account_info::AccountInfo<'a>, + /// MPL Account Compression program + pub compression_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The MPL Bubblegum program + pub bubblegum_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The system program + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `register_reviews_tree_v1` CPI instruction. +pub struct RegisterReviewsTreeV1Cpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + /// Funds the tree rent + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews authority PDA at ["reviews_authority"] — set as tree_creator + pub authority: &'b solana_program::account_info::AccountInfo<'a>, + /// Reviews merkle tree PDA at ["reviews_tree", tree_index_le] + pub merkle_tree: &'b solana_program::account_info::AccountInfo<'a>, + /// Bubblegum tree config PDA (derived from merkle_tree) + pub tree_config: &'b solana_program::account_info::AccountInfo<'a>, + /// MPL Noop / log wrapper program + pub log_wrapper: &'b solana_program::account_info::AccountInfo<'a>, + /// MPL Account Compression program + pub compression_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The MPL Bubblegum program + pub bubblegum_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The system program + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: RegisterReviewsTreeV1InstructionArgs, +} + +impl<'a, 'b> RegisterReviewsTreeV1Cpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: RegisterReviewsTreeV1CpiAccounts<'a, 'b>, + args: RegisterReviewsTreeV1InstructionArgs, + ) -> Self { + Self { + __program: program, + payer: accounts.payer, + authority: accounts.authority, + merkle_tree: accounts.merkle_tree, + tree_config: accounts.tree_config, + log_wrapper: accounts.log_wrapper, + compression_program: accounts.compression_program, + bubblegum_program: accounts.bubblegum_program, + system_program: accounts.system_program, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + let mut accounts = Vec::with_capacity(8 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.payer.key, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.authority.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.merkle_tree.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.tree_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.log_wrapper.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.compression_program.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.bubblegum_program.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.system_program.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_program::instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_writable: remaining_account.1, + is_signer: remaining_account.2, + }) + }); + let mut data = borsh::to_vec(&(RegisterReviewsTreeV1InstructionData::new())).unwrap(); + let mut args = borsh::to_vec(&self.__args).unwrap(); + data.append(&mut args); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::MPL_AGENT_REPUTATION_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(8 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.payer.clone()); + account_infos.push(self.authority.clone()); + account_infos.push(self.merkle_tree.clone()); + account_infos.push(self.tree_config.clone()); + account_infos.push(self.log_wrapper.clone()); + account_infos.push(self.compression_program.clone()); + account_infos.push(self.bubblegum_program.clone()); + account_infos.push(self.system_program.clone()); + remaining_accounts + .iter() + .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); + + if signers_seeds.is_empty() { + solana_program::program::invoke(&instruction, &account_infos) + } else { + solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds) + } + } +} + +/// Instruction builder for `RegisterReviewsTreeV1` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[]` authority +/// 2. `[writable]` merkle_tree +/// 3. `[writable]` tree_config +/// 4. `[]` log_wrapper +/// 5. `[]` compression_program +/// 6. `[]` bubblegum_program +/// 7. `[]` system_program +pub struct RegisterReviewsTreeV1CpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> RegisterReviewsTreeV1CpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(RegisterReviewsTreeV1CpiBuilderInstruction { + __program: program, + payer: None, + authority: None, + merkle_tree: None, + tree_config: None, + log_wrapper: None, + compression_program: None, + bubblegum_program: None, + system_program: None, + tree_index: None, + max_depth: None, + max_buffer_size: None, + canopy_depth: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + /// Funds the tree rent + #[inline(always)] + pub fn payer(&mut self, payer: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.payer = Some(payer); + self + } + /// Reviews authority PDA at ["reviews_authority"] — set as tree_creator + #[inline(always)] + pub fn authority( + &mut self, + authority: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.authority = Some(authority); + self + } + /// Reviews merkle tree PDA at ["reviews_tree", tree_index_le] + #[inline(always)] + pub fn merkle_tree( + &mut self, + merkle_tree: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.merkle_tree = Some(merkle_tree); + self + } + /// Bubblegum tree config PDA (derived from merkle_tree) + #[inline(always)] + pub fn tree_config( + &mut self, + tree_config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.tree_config = Some(tree_config); + self + } + /// MPL Noop / log wrapper program + #[inline(always)] + pub fn log_wrapper( + &mut self, + log_wrapper: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.log_wrapper = Some(log_wrapper); + self + } + /// MPL Account Compression program + #[inline(always)] + pub fn compression_program( + &mut self, + compression_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.compression_program = Some(compression_program); + self + } + /// The MPL Bubblegum program + #[inline(always)] + pub fn bubblegum_program( + &mut self, + bubblegum_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.bubblegum_program = Some(bubblegum_program); + self + } + /// The system program + #[inline(always)] + pub fn system_program( + &mut self, + system_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.system_program = Some(system_program); + self + } + #[inline(always)] + pub fn tree_index(&mut self, tree_index: u64) -> &mut Self { + self.instruction.tree_index = Some(tree_index); + self + } + #[inline(always)] + pub fn max_depth(&mut self, max_depth: u32) -> &mut Self { + self.instruction.max_depth = Some(max_depth); + self + } + #[inline(always)] + pub fn max_buffer_size(&mut self, max_buffer_size: u32) -> &mut Self { + self.instruction.max_buffer_size = Some(max_buffer_size); + self + } + #[inline(always)] + pub fn canopy_depth(&mut self, canopy_depth: u32) -> &mut Self { + self.instruction.canopy_depth = Some(canopy_depth); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_program::account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction + .__remaining_accounts + .push((account, is_writable, is_signer)); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + let args = RegisterReviewsTreeV1InstructionArgs { + tree_index: self + .instruction + .tree_index + .clone() + .expect("tree_index is not set"), + max_depth: self + .instruction + .max_depth + .clone() + .expect("max_depth is not set"), + max_buffer_size: self + .instruction + .max_buffer_size + .clone() + .expect("max_buffer_size is not set"), + canopy_depth: self + .instruction + .canopy_depth + .clone() + .expect("canopy_depth is not set"), + }; + let instruction = RegisterReviewsTreeV1Cpi { + __program: self.instruction.__program, + + payer: self.instruction.payer.expect("payer is not set"), + + authority: self.instruction.authority.expect("authority is not set"), + + merkle_tree: self + .instruction + .merkle_tree + .expect("merkle_tree is not set"), + + tree_config: self + .instruction + .tree_config + .expect("tree_config is not set"), + + log_wrapper: self + .instruction + .log_wrapper + .expect("log_wrapper is not set"), + + compression_program: self + .instruction + .compression_program + .expect("compression_program is not set"), + + bubblegum_program: self + .instruction + .bubblegum_program + .expect("bubblegum_program is not set"), + + system_program: self + .instruction + .system_program + .expect("system_program is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +struct RegisterReviewsTreeV1CpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, + merkle_tree: Option<&'b solana_program::account_info::AccountInfo<'a>>, + tree_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + log_wrapper: Option<&'b solana_program::account_info::AccountInfo<'a>>, + compression_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + bubblegum_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + system_program: Option<&'b solana_program::account_info::AccountInfo<'a>>, + tree_index: Option, + max_depth: Option, + max_buffer_size: Option, + canopy_depth: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/clients/rust-reputation/src/generated/types/key.rs b/clients/rust-reputation/src/generated/types/key.rs index 0db0557..e911a77 100644 --- a/clients/rust-reputation/src/generated/types/key.rs +++ b/clients/rust-reputation/src/generated/types/key.rs @@ -17,5 +17,5 @@ use num_derive::FromPrimitive; #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Hash, FromPrimitive)] pub enum Key { Uninitialized, - AgentReputationV1, + ReviewRecordV1, } diff --git a/clients/rust-reputation/tests/create.rs b/clients/rust-reputation/tests/create.rs deleted file mode 100644 index f13dd2e..0000000 --- a/clients/rust-reputation/tests/create.rs +++ /dev/null @@ -1,109 +0,0 @@ -#![cfg(feature = "test-sbf")] - -use mpl_agent_reputation::{ - accounts::AgentReputationV1, instructions::RegisterReputationV1Builder, types::Key, -}; -use mpl_core::instructions::{CreateCollectionV1Builder, CreateV1Builder}; -use solana_program_test::{tokio, ProgramTest}; -use solana_sdk::{ - pubkey::Pubkey, - signature::{Keypair, Signer}, - transaction::Transaction, -}; - -const MPL_CORE_ID: Pubkey = solana_program::pubkey!("CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d"); - -fn setup() -> ProgramTest { - let mut program_test = ProgramTest::new( - "mpl_agent_reputation_program", - mpl_agent_reputation::ID, - None, - ); - program_test.add_program("mpl_core", MPL_CORE_ID, None); - program_test -} - -async fn create_collection_and_asset( - context: &mut solana_program_test::ProgramTestContext, -) -> (Pubkey, Pubkey) { - let collection = Keypair::new(); - let asset = Keypair::new(); - - // Create collection. - let create_collection_ix = CreateCollectionV1Builder::new() - .collection(collection.pubkey()) - .payer(context.payer.pubkey()) - .name("Test Collection".to_string()) - .uri("https://example.com/collection.json".to_string()) - .instruction(); - - let tx = Transaction::new_signed_with_payer( - &[create_collection_ix], - Some(&context.payer.pubkey()), - &[&context.payer, &collection], - context.last_blockhash, - ); - context.banks_client.process_transaction(tx).await.unwrap(); - - // Create asset in collection. - let create_asset_ix = CreateV1Builder::new() - .asset(asset.pubkey()) - .collection(Some(collection.pubkey())) - .payer(context.payer.pubkey()) - .name("Test Asset".to_string()) - .uri("https://example.com/asset.json".to_string()) - .instruction(); - - let tx = Transaction::new_signed_with_payer( - &[create_asset_ix], - Some(&context.payer.pubkey()), - &[&context.payer, &asset], - context.last_blockhash, - ); - context.banks_client.process_transaction(tx).await.unwrap(); - - (collection.pubkey(), asset.pubkey()) -} - -/// Equivalent of JS test: reputation/register.test.ts - "it can register an asset" -#[tokio::test] -async fn register_reputation() { - let mut context = setup().start_with_context().await; - - // Create the collection and asset. - let (collection, asset) = create_collection_and_asset(&mut context).await; - - // Derive the agent reputation PDA. - let (agent_reputation_pda, expected_bump) = AgentReputationV1::find_pda(&asset); - - // When we register the asset. - let ix = RegisterReputationV1Builder::new() - .agent_reputation(agent_reputation_pda) - .asset(asset) - .collection(Some(collection)) - .payer(context.payer.pubkey()) - .instruction(); - - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - context.banks_client.process_transaction(tx).await.unwrap(); - - // Then there's an Agent Reputation PDA with the correct data. - let account = context - .banks_client - .get_account(agent_reputation_pda) - .await - .unwrap() - .unwrap(); - - assert_eq!(account.data.len(), AgentReputationV1::LEN); - - let agent_reputation = AgentReputationV1::from_bytes(&account.data).unwrap(); - assert_eq!(agent_reputation.key, Key::AgentReputationV1); - assert_eq!(agent_reputation.bump, expected_bump); - assert_eq!(agent_reputation.asset, asset); -} diff --git a/configs/kinobi-reputation.cjs b/configs/kinobi-reputation.cjs index ca61fc5..120d715 100644 --- a/configs/kinobi-reputation.cjs +++ b/configs/kinobi-reputation.cjs @@ -1,4 +1,5 @@ const path = require("path"); +const fs = require("fs"); const k = require("@metaplex-foundation/kinobi"); // Paths. @@ -20,26 +21,111 @@ kinobi.update( // Update accounts. kinobi.update( new k.updateAccountsVisitor({ - agentReputationV1: { + reviewRecordV1: { seeds: [ - k.constantPdaSeedNodeFromString("agent_reputation"), + k.constantPdaSeedNodeFromString("review_record"), k.variablePdaSeedNode( - "asset", + "receiptAssetId", k.publicKeyTypeNode(), - "The address of the asset", + "Bubblegum asset id of the work receipt", ), ], }, }), ); +// Stateless PDAs (not backed by an account struct — declared via addPdasVisitor +// so we can use them as defaults on instruction accounts). +kinobi.update( + new k.addPdasVisitor({ + mplAgentReputation: [ + k.pdaNode("reviewsCollection", [ + k.constantPdaSeedNodeFromString("reviews_collection"), + ]), + k.pdaNode("reviewsAuthority", [ + k.constantPdaSeedNodeFromString("reviews_authority"), + ]), + k.pdaNode("reviewsTree", [ + k.constantPdaSeedNodeFromString("reviews_tree"), + k.variablePdaSeedNode( + "treeIndex", + k.numberTypeNode("u64"), + "The reviews tree index", + ), + ]), + ], + }), +); + +// Well-known program IDs we want to default in the generated client. +const MPL_CORE_ID = "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d"; +const BUBBLEGUM_ID = "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY"; +const MPL_NOOP_ID = "mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3"; +const COMPRESSION_ID = "mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW"; + // Update instructions. kinobi.update( new k.updateInstructionsVisitor({ - registerReputationV1: { + leaveReviewV1: { + accounts: { + authority: { + defaultValue: k.pdaValueNode("reviewsAuthority"), + }, + coreCollection: { + defaultValue: k.pdaValueNode("reviewsCollection"), + }, + mplCoreProgram: { + defaultValue: k.publicKeyValueNode(MPL_CORE_ID, "mplCore"), + }, + bubblegumProgram: { + defaultValue: k.publicKeyValueNode( + BUBBLEGUM_ID, + "mplBubblegum", + ), + }, + logWrapper: { + defaultValue: k.publicKeyValueNode(MPL_NOOP_ID, "mplNoop"), + }, + compressionProgram: { + defaultValue: k.publicKeyValueNode( + COMPRESSION_ID, + "mplAccountCompression", + ), + }, + }, + }, + createReviewsCollectionV1: { + accounts: { + collection: { + defaultValue: k.pdaValueNode("reviewsCollection"), + }, + authority: { + defaultValue: k.pdaValueNode("reviewsAuthority"), + }, + mplCoreProgram: { + defaultValue: k.publicKeyValueNode(MPL_CORE_ID, "mplCore"), + }, + }, + }, + registerReviewsTreeV1: { accounts: { - agentReputation: { - defaultValue: k.pdaValueNode("agentReputationV1"), + authority: { + defaultValue: k.pdaValueNode("reviewsAuthority"), + }, + bubblegumProgram: { + defaultValue: k.publicKeyValueNode( + BUBBLEGUM_ID, + "mplBubblegum", + ), + }, + logWrapper: { + defaultValue: k.publicKeyValueNode(MPL_NOOP_ID, "mplNoop"), + }, + compressionProgram: { + defaultValue: k.publicKeyValueNode( + COMPRESSION_ID, + "mplAccountCompression", + ), }, }, }, @@ -60,3 +146,70 @@ kinobi.accept( crateFolder: crateDir, }), ); + +// Post-process: write standalone PDA helpers expected by kinobi-emitted +// instruction code (kinobi 1.0-alpha doesn't render find*Pda helpers for +// PDAs added via addPdasVisitor, but it still emits references to them). +// The source below is pre-formatted to match the repo's prettier config +// (2-space indent, single quotes, trailing commas) so `pnpm generate` is +// idempotent and CI's "working directory is clean" check passes. +const pdaHelperFile = path.join(jsDir, "accounts", "standalonePdas.ts"); +fs.writeFileSync( + pdaHelperFile, + `/** + * Hand-written PDA helpers for standalone PDAs (collections, authority, + * trees). Emitted by the kinobi-reputation config because kinobi 1.0-alpha + * doesn't render find*Pda helpers for PDAs added via addPdasVisitor. + */ + +import { Context, Pda } from '@metaplex-foundation/umi'; +import { string, u64 } from '@metaplex-foundation/umi/serializers'; + +const PROGRAM_ID = 'REPREG5c1gPHuHukEyANpksLdHFaJCiTrm6zJgNhRZR'; + +function pda( + context: Pick, + seeds: Uint8Array[] +): Pda { + const programId = context.programs.getPublicKey( + 'mplAgentReputation', + PROGRAM_ID + ); + return context.eddsa.findPda(programId, seeds); +} + +export function findReviewsCollectionPda( + context: Pick +): Pda { + return pda(context, [ + string({ size: 'variable' }).serialize('reviews_collection'), + ]); +} + +export function findReviewsAuthorityPda( + context: Pick +): Pda { + return pda(context, [ + string({ size: 'variable' }).serialize('reviews_authority'), + ]); +} + +export function findReviewsTreePda( + context: Pick, + seeds: { treeIndex: number | bigint } +): Pda { + return pda(context, [ + string({ size: 'variable' }).serialize('reviews_tree'), + u64().serialize(seeds.treeIndex), + ]); +} +` +); + +// Patch the accounts/index.ts to re-export from standalonePdas. +const accountsIndex = path.join(jsDir, "accounts", "index.ts"); +let indexContent = fs.readFileSync(accountsIndex, "utf-8"); +if (!indexContent.includes("standalonePdas")) { + indexContent += "export * from './standalonePdas';\n"; + fs.writeFileSync(accountsIndex, indexContent); +} diff --git a/idls/mpl_agent_reputation.json b/idls/mpl_agent_reputation.json index 5422a77..f362def 100644 --- a/idls/mpl_agent_reputation.json +++ b/idls/mpl_agent_reputation.json @@ -3,48 +3,183 @@ "name": "mpl_agent_reputation_program", "instructions": [ { - "name": "RegisterReputationV1", + "name": "LeaveReviewV1", "accounts": [ { - "name": "agentReputation", + "name": "payer", "isMut": true, - "isSigner": false, + "isSigner": true, + "docs": [ + "Pays for the review cNFT mint and the review record PDA" + ] + }, + { + "name": "reviewer", + "isMut": false, + "isSigner": true, "docs": [ - "The agent reputation PDA" + "The wallet leaving the review; must own the work receipt" ] }, { "name": "asset", + "isMut": false, + "isSigner": false, + "docs": [ + "The Core asset being reviewed (the agent)" + ] + }, + { + "name": "leafOwner", + "isMut": false, + "isSigner": false, + "docs": [ + "The owner of the new review cNFT leaf - must equal asset.owner" + ] + }, + { + "name": "authority", + "isMut": false, + "isSigner": false, + "docs": [ + "Reviews authority PDA at [\"reviews_authority\"] — signs the Bubblegum CPI as tree_creator/collection_authority via invoke_signed" + ] + }, + { + "name": "treeConfig", "isMut": true, "isSigner": false, "docs": [ - "The address of the Core asset" + "Bubblegum tree config PDA for the reviews tree" ] }, { - "name": "collection", + "name": "merkleTree", + "isMut": true, + "isSigner": false, + "docs": [ + "Reviews merkle tree at PDA [\"reviews_tree\", reviews_tree_index_le]" + ] + }, + { + "name": "coreCollection", + "isMut": true, + "isSigner": false, + "docs": [ + "Reviews collection PDA at [\"reviews_collection\"]" + ] + }, + { + "name": "mplCoreCpiSigner", + "isMut": false, + "isSigner": false, + "docs": [ + "Bubblegum's mpl-core CPI signer PDA" + ] + }, + { + "name": "logWrapper", + "isMut": false, + "isSigner": false, + "docs": [ + "MPL Noop / log wrapper program" + ] + }, + { + "name": "compressionProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "MPL Account Compression program (used for both Bubblegum mint and verify_leaf CPI)" + ] + }, + { + "name": "mplCoreProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The MPL Core program" + ] + }, + { + "name": "bubblegumProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The MPL Bubblegum program" + ] + }, + { + "name": "receiptsMerkleTree", + "isMut": false, + "isSigner": false, + "docs": [ + "Receipts Bubblegum merkle tree holding the receipt being referenced" + ] + }, + { + "name": "receiptsCollection", + "isMut": false, + "isSigner": false, + "docs": [ + "Canonical receipts collection PDA from mpl-agent-tools at [\"receipts_collection\"]" + ] + }, + { + "name": "reviewRecord", "isMut": true, "isSigner": false, - "isOptional": true, "docs": [ - "The address of the collection" + "ReviewRecordV1 PDA seeded with the receipt's bubblegum asset id - idempotency gate" ] }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The system program" + ] + } + ], + "args": [ + { + "name": "leaveReviewV1Args", + "type": { + "defined": "LeaveReviewV1Args" + } + } + ], + "discriminant": { + "type": "u8", + "value": 0 + } + }, + { + "name": "CreateReviewsCollectionV1", + "accounts": [ { "name": "payer", "isMut": true, "isSigner": true, "docs": [ - "The payer for additional rent" + "Funds the collection's rent" + ] + }, + { + "name": "collection", + "isMut": true, + "isSigner": false, + "docs": [ + "Reviews collection PDA at [\"reviews_collection\"]" ] }, { "name": "authority", "isMut": false, - "isSigner": true, - "isOptional": true, + "isSigner": false, "docs": [ - "Authority for the collection. If not provided, the payer will be used." + "Reviews authority PDA at [\"reviews_authority\"] — becomes the collection's update_authority" ] }, { @@ -66,21 +201,102 @@ ], "args": [ { - "name": "registerReputationV1Args", + "name": "createReviewsCollectionV1Args", "type": { - "defined": "RegisterReputationV1Args" + "defined": "CreateReviewsCollectionV1Args" } } ], "discriminant": { "type": "u8", - "value": 0 + "value": 1 + } + }, + { + "name": "RegisterReviewsTreeV1", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true, + "docs": [ + "Funds the tree rent" + ] + }, + { + "name": "authority", + "isMut": false, + "isSigner": false, + "docs": [ + "Reviews authority PDA at [\"reviews_authority\"] — set as tree_creator" + ] + }, + { + "name": "merkleTree", + "isMut": true, + "isSigner": false, + "docs": [ + "Reviews merkle tree PDA at [\"reviews_tree\", tree_index_le]" + ] + }, + { + "name": "treeConfig", + "isMut": true, + "isSigner": false, + "docs": [ + "Bubblegum tree config PDA (derived from merkle_tree)" + ] + }, + { + "name": "logWrapper", + "isMut": false, + "isSigner": false, + "docs": [ + "MPL Noop / log wrapper program" + ] + }, + { + "name": "compressionProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "MPL Account Compression program" + ] + }, + { + "name": "bubblegumProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The MPL Bubblegum program" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "The system program" + ] + } + ], + "args": [ + { + "name": "registerReviewsTreeV1Args", + "type": { + "defined": "RegisterReviewsTreeV1Args" + } + } + ], + "discriminant": { + "type": "u8", + "value": 2 } } ], "accounts": [ { - "name": "AgentReputationV1", + "name": "ReviewRecordV1", "type": { "kind": "struct", "fields": [ @@ -110,7 +326,11 @@ ] }, { - "name": "asset", + "name": "reviewer", + "type": "publicKey" + }, + { + "name": "receiptAssetId", "type": "publicKey" } ] @@ -119,7 +339,7 @@ ], "types": [ { - "name": "RegisterReputationV1Args", + "name": "CreateReviewsCollectionV1Args", "type": { "kind": "struct", "fields": [ @@ -138,6 +358,144 @@ ] } }, + { + "name": "LeaveReviewV1Args", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rating", + "type": "u8" + }, + { + "name": "pad", + "type": { + "array": [ + "u8", + 6 + ] + }, + "attrs": [ + "padding" + ] + }, + { + "name": "reviewsTreeIndex", + "type": "u64" + }, + { + "name": "receiptsTreeIndex", + "type": "u64" + }, + { + "name": "receiptNonce", + "type": "u64" + }, + { + "name": "receiptIndex", + "type": "u32" + }, + { + "name": "receiptFlags", + "type": "u8" + }, + { + "name": "pad2", + "type": { + "array": [ + "u8", + 3 + ] + }, + "attrs": [ + "padding" + ] + }, + { + "name": "receiptRoot", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "receiptDataHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "receiptAssetDataHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "feedbackUri", + "type": "string", + "attrs": [ + "idl-type" + ] + } + ] + } + }, + { + "name": "RegisterReviewsTreeV1Args", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pad", + "type": { + "array": [ + "u8", + 7 + ] + }, + "attrs": [ + "padding" + ] + }, + { + "name": "treeIndex", + "type": "u64" + }, + { + "name": "maxDepth", + "type": "u32" + }, + { + "name": "maxBufferSize", + "type": "u32" + }, + { + "name": "canopyDepth", + "type": "u32" + }, + { + "name": "pad2", + "type": { + "array": [ + "u8", + 4 + ] + }, + "attrs": [ + "padding" + ] + } + ] + } + }, { "name": "Key", "type": { @@ -147,7 +505,7 @@ "name": "Uninitialized" }, { - "name": "AgentReputationV1" + "name": "ReviewRecordV1" } ] } @@ -181,8 +539,63 @@ }, { "code": 5, - "name": "AgentReputationAlreadyRegistered", - "msg": "Agent Reputation already registered" + "name": "InvalidReviewRating", + "msg": "Invalid review rating (must be 1..=5)" + }, + { + "code": 6, + "name": "FeedbackUriInvalid", + "msg": "Feedback URI must be non-empty and within size limits" + }, + { + "code": 7, + "name": "LeafOwnerMismatch", + "msg": "Leaf owner does not match the reviewed asset owner" + }, + { + "code": 8, + "name": "InvalidBubblegumProgram", + "msg": "Invalid Bubblegum Program" + }, + { + "code": 9, + "name": "InvalidCompressionProgram", + "msg": "Invalid Compression Program" + }, + { + "code": 10, + "name": "ReviewAlreadyExists", + "msg": "A review already exists for this work receipt" + }, + { + "code": 11, + "name": "InvalidReviewsCollection", + "msg": "Invalid reviews collection PDA derivation" + }, + { + "code": 12, + "name": "InvalidReviewsAuthority", + "msg": "Invalid reviews authority PDA derivation" + }, + { + "code": 13, + "name": "ReviewsCollectionAlreadyInitialized", + "msg": "Reviews collection already initialized" + }, + { + "code": 14, + "name": "InvalidReviewsTreeDerivation", + "msg": "Invalid reviews tree PDA derivation" + }, + { + "code": 15, + "name": "InvalidReceiptsCollection", + "msg": "Supplied receipts collection is not the canonical mpl-agent-tools receipts collection PDA" + }, + { + "code": 16, + "name": "InvalidReceiptsTreeDerivation", + "msg": "Supplied receipts merkle tree is not the canonical mpl-agent-tools receipts tree PDA" } ], "metadata": { diff --git a/programs/mpl-agent-reputation/Cargo.toml b/programs/mpl-agent-reputation/Cargo.toml index cfaa38b..d5cb2fe 100644 --- a/programs/mpl-agent-reputation/Cargo.toml +++ b/programs/mpl-agent-reputation/Cargo.toml @@ -20,3 +20,4 @@ solana-system-interface = "2.0" thiserror = "^2.0" mpl-utils = { version = "0.5.0", default-features = false } mpl-core = "0.12.0" +mpl-bubblegum = "3.0.0" diff --git a/programs/mpl-agent-reputation/src/error.rs b/programs/mpl-agent-reputation/src/error.rs index f784fb4..6099c48 100644 --- a/programs/mpl-agent-reputation/src/error.rs +++ b/programs/mpl-agent-reputation/src/error.rs @@ -2,6 +2,19 @@ use num_derive::FromPrimitive; use solana_program::program_error::ProgramError; use thiserror::Error; +/// On-chain error codes for the reputation program. +/// +/// **Order is load-bearing.** `FromPrimitive` + `ProgramError::Custom(e as u32)` +/// turn each variant into its zero-based positional index — that integer is +/// what shows up in transaction logs as `custom program error: 0xNN` and is +/// what kinobi's auto-generated JS error map keys on. +/// +/// Tests and JS bootstrap helpers (notably +/// `clients/js/test/_receiptsReviews.ts`) hardcode hex strings for specific +/// errors (e.g. `0xd` for `ReviewsCollectionAlreadyInitialized`). Reordering, +/// inserting, or removing variants without re-running `pnpm generate` AND +/// updating those hardcoded hex constants will silently break the mapping. +/// Add new variants at the end of the enum. #[derive(Error, Clone, Debug, Eq, PartialEq, FromPrimitive)] pub enum MplAgentReputationError { /// 0 - Invalid System Program @@ -24,9 +37,59 @@ pub enum MplAgentReputationError { #[error("Invalid Core Asset")] InvalidCoreAsset, - /// 5 - Agent Reputation already registered - #[error("Agent Reputation already registered")] - AgentReputationAlreadyRegistered, + /// 5 - Invalid review rating (must be 1..=5) + #[error("Invalid review rating (must be 1..=5)")] + InvalidReviewRating, + + /// 6 - Feedback URI missing or too long + #[error("Feedback URI must be non-empty and within size limits")] + FeedbackUriInvalid, + + /// 7 - Leaf owner does not match asset owner + #[error("Leaf owner does not match the reviewed asset owner")] + LeafOwnerMismatch, + + /// 8 - Invalid Bubblegum Program + #[error("Invalid Bubblegum Program")] + InvalidBubblegumProgram, + + /// 9 - Invalid Compression Program + #[error("Invalid Compression Program")] + InvalidCompressionProgram, + + /// 10 - A review already exists for this receipt + #[error("A review already exists for this work receipt")] + ReviewAlreadyExists, + + /// 11 - Invalid reviews collection PDA derivation + #[error("Invalid reviews collection PDA derivation")] + InvalidReviewsCollection, + + /// 12 - Invalid reviews authority PDA derivation + #[error("Invalid reviews authority PDA derivation")] + InvalidReviewsAuthority, + + /// 13 - Reviews collection already initialized + #[error("Reviews collection already initialized")] + ReviewsCollectionAlreadyInitialized, + + /// 14 - Invalid reviews tree PDA derivation + #[error("Invalid reviews tree PDA derivation")] + InvalidReviewsTreeDerivation, + + /// 15 - Receipts collection mismatch — supplied account is not the + /// canonical mpl-agent-tools receipts collection PDA. + #[error( + "Supplied receipts collection is not the canonical mpl-agent-tools receipts collection PDA" + )] + InvalidReceiptsCollection, + + /// 16 - Receipts tree mismatch — supplied account is not the + /// canonical mpl-agent-tools receipts tree PDA. + #[error( + "Supplied receipts merkle tree is not the canonical mpl-agent-tools receipts tree PDA" + )] + InvalidReceiptsTreeDerivation, } impl From for ProgramError { diff --git a/programs/mpl-agent-reputation/src/instruction.rs b/programs/mpl-agent-reputation/src/instruction.rs index 1465948..c69d9dc 100644 --- a/programs/mpl-agent-reputation/src/instruction.rs +++ b/programs/mpl-agent-reputation/src/instruction.rs @@ -1,13 +1,17 @@ use shank::{ShankContext, ShankInstruction}; -use crate::processor::RegisterReputationV1Args; +use crate::processor::{ + CreateReviewsCollectionV1Args, LeaveReviewV1Args, RegisterReviewsTreeV1Args, +}; /// Instruction discriminants for routing. /// The first byte of instruction data determines which instruction to execute. #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum MplAgentReputationInstructionDiscriminant { - RegisterReputationV1 = 0, + LeaveReviewV1 = 0, + CreateReviewsCollectionV1 = 1, + RegisterReviewsTreeV1 = 2, } impl TryFrom for MplAgentReputationInstructionDiscriminant { @@ -15,25 +19,70 @@ impl TryFrom for MplAgentReputationInstructionDiscriminant { fn try_from(value: u8) -> Result { match value { - 0 => Ok(MplAgentReputationInstructionDiscriminant::RegisterReputationV1), + 0 => Ok(MplAgentReputationInstructionDiscriminant::LeaveReviewV1), + 1 => Ok(MplAgentReputationInstructionDiscriminant::CreateReviewsCollectionV1), + 2 => Ok(MplAgentReputationInstructionDiscriminant::RegisterReviewsTreeV1), _ => Err(()), } } } /// Instruction enum for Shank IDL generation. -/// Note: We keep Shank attributes for IDL generation but use zero-copy -/// for actual instruction deserialization in the processor. #[derive(Clone, Debug, ShankContext, ShankInstruction)] #[rustfmt::skip] pub enum MplAgentReputationInstruction { - /// Register an Agent Reputation. - #[account(0, writable, name="agent_reputation", desc = "The agent reputation PDA")] - #[account(1, writable, name="asset", desc = "The address of the Core asset")] - #[account(2, writable, optional, name="collection", desc = "The address of the collection")] - #[account(3, writable, signer, name="payer", desc = "The payer for additional rent")] - #[account(4, optional, signer, name="authority", desc = "Authority for the collection. If not provided, the payer will be used.")] - #[account(5, name="mpl_core_program", desc = "The MPL Core program")] - #[account(6, name="system_program", desc = "The system program")] - RegisterReputationV1(RegisterReputationV1Args), + /// Leave a star review for an Agent, backed by an existing work receipt + /// cNFT owned by the reviewer. Mints a non-transferable review cNFT to + /// the agent's wallet via Bubblegum CPI signed by the reviews authority + /// PDA. Reviews+receipts collections + reviews tree must all be the + /// canonical PDAs (cross-program for receipts). A `ReviewRecordV1` PDA + /// seeded with the receipt's bubblegum asset id gates against + /// double-review. Any accounts beyond `system_program` are treated as + /// the merkle proof path for the receipt `verify_leaf` CPI. + #[account(0, writable, signer, name="payer", desc = "Pays for the review cNFT mint and the review record PDA")] + #[account(1, signer, name="reviewer", desc = "The wallet leaving the review; must own the work receipt")] + #[account(2, name="asset", desc = "The Core asset being reviewed (the agent)")] + #[account(3, name="leaf_owner", desc = "The owner of the new review cNFT leaf - must equal asset.owner")] + #[account(4, name="authority", desc = "Reviews authority PDA at [\"reviews_authority\"] — signs the Bubblegum CPI as tree_creator/collection_authority via invoke_signed")] + #[account(5, writable, name="tree_config", desc = "Bubblegum tree config PDA for the reviews tree")] + #[account(6, writable, name="merkle_tree", desc = "Reviews merkle tree at PDA [\"reviews_tree\", reviews_tree_index_le]")] + #[account(7, writable, name="core_collection", desc = "Reviews collection PDA at [\"reviews_collection\"]")] + #[account(8, name="mpl_core_cpi_signer", desc = "Bubblegum's mpl-core CPI signer PDA")] + #[account(9, name="log_wrapper", desc = "MPL Noop / log wrapper program")] + #[account(10, name="compression_program", desc = "MPL Account Compression program (used for both Bubblegum mint and verify_leaf CPI)")] + #[account(11, name="mpl_core_program", desc = "The MPL Core program")] + #[account(12, name="bubblegum_program", desc = "The MPL Bubblegum program")] + #[account(13, name="receipts_merkle_tree", desc = "Receipts Bubblegum merkle tree holding the receipt being referenced")] + #[account(14, name="receipts_collection", desc = "Canonical receipts collection PDA from mpl-agent-tools at [\"receipts_collection\"]")] + #[account(15, writable, name="review_record", desc = "ReviewRecordV1 PDA seeded with the receipt's bubblegum asset id - idempotency gate")] + #[account(16, name="system_program", desc = "The system program")] + LeaveReviewV1(LeaveReviewV1Args), + + /// Permissionless idempotent bootstrap: create the canonical reviews + /// collection at [\"reviews_collection\"] PDA with update_authority = + /// [\"reviews_authority\"] PDA. Anyone may call. A hostile first caller + /// cannot capture authority because it's program-derived, not + /// caller-derived. Second call fails because the collection account is + /// already initialized. + #[account(0, writable, signer, name="payer", desc = "Funds the collection's rent")] + #[account(1, writable, name="collection", desc = "Reviews collection PDA at [\"reviews_collection\"]")] + #[account(2, name="authority", desc = "Reviews authority PDA at [\"reviews_authority\"] — becomes the collection's update_authority")] + #[account(3, name="mpl_core_program", desc = "The MPL Core program")] + #[account(4, name="system_program", desc = "The system program")] + CreateReviewsCollectionV1(CreateReviewsCollectionV1Args), + + /// Permissionless tree registration: caller picks an unused + /// `tree_index` and pays the rent. Tree is created at PDA + /// [\"reviews_tree\", tree_index_le]. Bubblegum is configured with + /// `tree_creator = [\"reviews_authority\"]` PDA so LeaveReviewV1 can sign + /// every future mint without the original creator. + #[account(0, writable, signer, name="payer", desc = "Funds the tree rent")] + #[account(1, name="authority", desc = "Reviews authority PDA at [\"reviews_authority\"] — set as tree_creator")] + #[account(2, writable, name="merkle_tree", desc = "Reviews merkle tree PDA at [\"reviews_tree\", tree_index_le]")] + #[account(3, writable, name="tree_config", desc = "Bubblegum tree config PDA (derived from merkle_tree)")] + #[account(4, name="log_wrapper", desc = "MPL Noop / log wrapper program")] + #[account(5, name="compression_program", desc = "MPL Account Compression program")] + #[account(6, name="bubblegum_program", desc = "The MPL Bubblegum program")] + #[account(7, name="system_program", desc = "The system program")] + RegisterReviewsTreeV1(RegisterReviewsTreeV1Args), } diff --git a/programs/mpl-agent-reputation/src/processor/create_reviews_collection_v1.rs b/programs/mpl-agent-reputation/src/processor/create_reviews_collection_v1.rs new file mode 100644 index 0000000..1250c60 --- /dev/null +++ b/programs/mpl-agent-reputation/src/processor/create_reviews_collection_v1.rs @@ -0,0 +1,97 @@ +use bytemuck::{Pod, Zeroable}; +use mpl_core::{ + instructions::CreateCollectionV2CpiBuilder, + types::{BubblegumV2, PermanentFreezeDelegate, Plugin, PluginAuthority, PluginAuthorityPair}, +}; +use mpl_utils::assert_signer; +use shank::ShankType; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, +}; +use solana_system_interface::program as system_program; + +use crate::{ + error::MplAgentReputationError, + instruction::accounts::CreateReviewsCollectionV1Accounts, + state::{check_reviews_authority_pda, check_reviews_collection_pda, REVIEWS_COLLECTION_PREFIX}, +}; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable, ShankType)] +pub struct CreateReviewsCollectionV1Args { + #[skip] + pub discriminator: u8, + #[padding] + pub _padding: [u8; 7], +} +const _: () = assert!(core::mem::size_of::() == 8); + +/// Permissionless, idempotent bootstrap: create the canonical reviews +/// collection at `["reviews_collection"]` PDA with the program's +/// `["reviews_authority"]` PDA as `update_authority`. Anyone may call +/// — but because the authority is a program-derived PDA (not the +/// caller), a hostile first caller cannot capture control. A second +/// call fails at MPL Core's CreateCollectionV2 because the account is +/// already initialized. +pub fn create_reviews_collection_v1<'a>( + accounts: &'a [AccountInfo<'a>], + _args: &CreateReviewsCollectionV1Args, +) -> ProgramResult { + let ctx = CreateReviewsCollectionV1Accounts::context(accounts)?; + + assert_signer(ctx.accounts.payer)?; + + if *ctx.accounts.mpl_core_program.key != mpl_core::ID { + return Err(MplAgentReputationError::InvalidMplCoreProgram.into()); + } + if *ctx.accounts.system_program.key != system_program::id() { + return Err(MplAgentReputationError::InvalidSystemProgram.into()); + } + + let collection_bump = check_reviews_collection_pda(ctx.accounts.collection)?; + let _ = check_reviews_authority_pda(ctx.accounts.authority)?; + + if ctx.accounts.collection.data_len() != 0 + || *ctx.accounts.collection.owner != system_program::id() + { + return Err(MplAgentReputationError::ReviewsCollectionAlreadyInitialized.into()); + } + + let collection_signer_seeds: &[&[u8]] = &[REVIEWS_COLLECTION_PREFIX, &[collection_bump]]; + + CreateCollectionV2CpiBuilder::new(ctx.accounts.mpl_core_program) + .collection(ctx.accounts.collection) + .update_authority(Some(ctx.accounts.authority)) + .payer(ctx.accounts.payer) + .system_program(ctx.accounts.system_program) + .name("Agent Feedback".to_string()) + .uri("".to_string()) + .plugins(vec![ + PluginAuthorityPair { + plugin: Plugin::BubblegumV2(BubblegumV2 {}), + authority: None, + }, + // Soulbound: every cNFT minted into this collection inherits + // `permanent_lvl_frozen=true`. Authority = UpdateAuthority + // (the reviews authority PDA), so only this program could + // ever thaw — and it never exposes a thaw path. + PluginAuthorityPair { + plugin: Plugin::PermanentFreezeDelegate(PermanentFreezeDelegate { frozen: true }), + authority: Some(PluginAuthority::UpdateAuthority), + }, + ]) + .invoke_signed(&[collection_signer_seeds])?; + + Ok(()) +} + +pub fn cast_create_reviews_collection_args( + data: &[u8], +) -> Result<&CreateReviewsCollectionV1Args, ProgramError> { + if data.len() < core::mem::size_of::() { + return Err(MplAgentReputationError::InvalidInstructionData.into()); + } + Ok(bytemuck::from_bytes( + &data[..core::mem::size_of::()], + )) +} diff --git a/programs/mpl-agent-reputation/src/processor/leave_review.rs b/programs/mpl-agent-reputation/src/processor/leave_review.rs new file mode 100644 index 0000000..5974571 --- /dev/null +++ b/programs/mpl-agent-reputation/src/processor/leave_review.rs @@ -0,0 +1,410 @@ +use bytemuck::{from_bytes, Pod, Zeroable}; +use mpl_bubblegum::{ + instructions::MintV2CpiBuilder, + types::{Creator, MetadataArgsV2, TokenStandard}, + utils::get_asset_id, + ID as BUBBLEGUM_ID, +}; +use mpl_core::types::Key as MplCoreKey; +use mpl_utils::assert_signer; +use shank::ShankType; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, keccak, msg, program::invoke, + program_error::ProgramError, pubkey::Pubkey, +}; +use solana_system_interface::program as system_program; + +use crate::{ + error::MplAgentReputationError, + instruction::accounts::LeaveReviewV1Accounts, + state::{ + check_receipts_collection_pda, check_receipts_tree_pda, check_reviews_authority_pda, + check_reviews_collection_pda, check_reviews_tree_pda, ReviewRecordV1, + MPL_ACCOUNT_COMPRESSION_ID, REVIEWS_AUTHORITY_PREFIX, + }, +}; + +/// `verify_leaf` Anchor instruction discriminator = +/// `sha256("global:verify_leaf")[..8]`. +const VERIFY_LEAF_DISCRIMINATOR: [u8; 8] = [124, 220, 22, 223, 104, 10, 250, 224]; + +/// Number of named accounts in `LeaveReviewV1` (everything before the +/// merkle proof remaining accounts). Must stay in sync with the +/// `#[account]` list in `instruction.rs`. +const LEAVE_REVIEW_NAMED_ACCOUNTS: usize = 17; + +/// Maximum length of the off-chain review JSON URI, in bytes. +pub const MAX_FEEDBACK_URI_LEN: usize = 200; + +/// Fixed-head Pod args for the LeaveReviewV1 instruction. The +/// `feedback_uri` is a zero-sized sentinel; the actual UTF-8 bytes are +/// length-prefixed (u32 LE) and appended after this struct in the +/// instruction data. Kinobi renders `feedbackUri: string` in the +/// generated clients via the `#[idl_type("String")]` annotation. +/// +/// The review references a work receipt by its `(merkle_tree, nonce)` +/// pair — the Bubblegum asset id is deterministically derived from +/// those. Proof data accompanies the call so we can validate the +/// receipt on-chain. +#[repr(C)] +#[derive(Pod, Zeroable, PartialEq, Eq, Debug, Clone, Copy, ShankType)] +pub struct LeaveReviewV1Args { + #[skip] + pub discriminator: u8, + /// Star rating, 1..=5. + pub rating: u8, + /// Padding to align the `u64` fields that follow. + #[padding] + pub _pad: [u8; 6], + /// Index of the reviews tree this review will be minted into (must + /// match `["reviews_tree", reviews_tree_index_le]`). + pub reviews_tree_index: u64, + + // --- Receipt merkle proof ------------------------------------------------ + /// Index of the receipts tree the work-receipt lives in (must match the + /// canonical mpl-agent-tools PDA `["receipts_tree", + /// receipts_tree_index_le]`). Binds the supplied `receipts_merkle_tree` + /// account to a tree the tools program actually created, preventing an + /// attacker from substituting a self-controlled compression tree with a + /// forged leaf. + pub receipts_tree_index: u64, + /// Receipt leaf's nonce within its tree. + pub receipt_nonce: u64, + /// Receipt leaf's index within its tree. + pub receipt_index: u32, + /// Receipt leaf flags. + pub receipt_flags: u8, + /// Padding to keep subsequent fields on their natural alignment. + #[padding] + pub _pad2: [u8; 3], + /// Current root of the receipts merkle tree. + pub receipt_root: [u8; 32], + /// Hash of the receipt's MetadataArgsV2 + seller_fee_basis_points. + pub receipt_data_hash: [u8; 32], + /// Hash of the receipt's `asset_data` blob (DEFAULT_ASSET_DATA_HASH). + pub receipt_asset_data_hash: [u8; 32], + /// URI of the off-chain review JSON file. Zero-sized sentinel; the + /// real UTF-8 bytes are length-prefixed and appended after this + /// struct in the instruction data. + #[idl_type("String")] + pub feedback_uri: [u8; 0], +} +const _: () = assert!(core::mem::size_of::() == 136); +const _: () = assert!(core::mem::align_of::() == 8); + +pub fn leave_review_v1<'a>( + accounts: &'a [AccountInfo<'a>], + instruction_data: &[u8], +) -> ProgramResult { + /****************************************************/ + /****************** Account Setup *******************/ + /****************************************************/ + + let ctx = LeaveReviewV1Accounts::context(accounts)?; + // Remaining accounts after the named ones form the merkle proof path. + let proof_accounts = if accounts.len() > LEAVE_REVIEW_NAMED_ACCOUNTS { + &accounts[LEAVE_REVIEW_NAMED_ACCOUNTS..] + } else { + &[][..] + }; + + // Split raw instruction data into the Pod head and the + // length-prefixed feedback URI tail. + if instruction_data.len() < core::mem::size_of::() { + return Err(MplAgentReputationError::InvalidInstructionData.into()); + } + let (args_bytes, tail) = instruction_data.split_at(core::mem::size_of::()); + let args: &LeaveReviewV1Args = from_bytes(args_bytes); + let feedback_uri = read_length_prefixed_string(tail, MAX_FEEDBACK_URI_LEN)?; + + /****************************************************/ + /****************** Account Guards ******************/ + /****************************************************/ + + assert_signer(ctx.accounts.payer)?; + assert_signer(ctx.accounts.reviewer)?; + + // Validate the agent asset is an MPL Core AssetV1 and read its owner. + if ctx.accounts.asset.owner != &mpl_core::ID { + return Err(MplAgentReputationError::InvalidCoreAsset.into()); + } + { + let asset_data = ctx.accounts.asset.try_borrow_data()?; + // BaseAssetV1 = key (1) + owner (32) + ... so any legitimate + // AssetV1 is at least 33 bytes. Reject short buffers explicitly + // so the `asset_data[1..33]` slice below cannot panic. + if asset_data.len() < 33 || asset_data[0] != MplCoreKey::AssetV1 as u8 { + return Err(MplAgentReputationError::InvalidCoreAsset.into()); + } + let asset_owner = Pubkey::new_from_array( + asset_data[1..33] + .try_into() + .map_err(|_| MplAgentReputationError::InvalidCoreAsset)?, + ); + // The leaf owner of the review cNFT must be the agent's wallet. + if *ctx.accounts.leaf_owner.key != asset_owner { + return Err(MplAgentReputationError::LeafOwnerMismatch.into()); + } + } + + // Validate program account keys. + if *ctx.accounts.mpl_core_program.key != mpl_core::ID { + return Err(MplAgentReputationError::InvalidMplCoreProgram.into()); + } + if *ctx.accounts.bubblegum_program.key != BUBBLEGUM_ID { + return Err(MplAgentReputationError::InvalidBubblegumProgram.into()); + } + if *ctx.accounts.compression_program.key != MPL_ACCOUNT_COMPRESSION_ID { + return Err(MplAgentReputationError::InvalidCompressionProgram.into()); + } + if *ctx.accounts.system_program.key != system_program::id() { + return Err(MplAgentReputationError::InvalidSystemProgram.into()); + } + + // Reviews collection + authority + tree: all canonical PDAs. + check_reviews_collection_pda(ctx.accounts.core_collection)?; + let authority_bump = check_reviews_authority_pda(ctx.accounts.authority)?; + check_reviews_tree_pda(ctx.accounts.merkle_tree, args.reviews_tree_index)?; + // Receipts collection + tree: canonical PDAs from mpl-agent-tools. The + // tree check is what binds the work-receipt proof to a tree only + // MintWorkReceiptV1 can append to — without it, anyone could verify a + // forged leaf against their own compression tree. + check_receipts_collection_pda(ctx.accounts.receipts_collection)?; + check_receipts_tree_pda(ctx.accounts.receipts_merkle_tree, args.receipts_tree_index)?; + + /****************************************************/ + /***************** Argument Guards ******************/ + /****************************************************/ + + if args.rating == 0 || args.rating > 5 { + return Err(MplAgentReputationError::InvalidReviewRating.into()); + } + if feedback_uri.is_empty() { + return Err(MplAgentReputationError::FeedbackUriInvalid.into()); + } + + /****************************************************/ + /************ Verify Work-Receipt Proof *************/ + /****************************************************/ + // + // The receipts cNFT lives in `receipts_merkle_tree`. We reconstruct its + // LeafSchemaV2 hash using: + // - id = bubblegum_asset_id(receipts_merkle_tree, nonce) + // - owner = reviewer (the leaf belongs to the wallet leaving the review) + // - delegate = reviewer (MintV2 with leaf_delegate=None defaults to owner) + // - collection_hash = keccak(receipts_collection_pubkey) + // - data_hash / creator_hash / asset_data_hash / flags = caller-supplied + // + // The CPI to MPL Account Compression's `verify_leaf` then proves this + // exact leaf hash is in the tree at the supplied index using the + // remaining_accounts as the proof path. + + let receipts_merkle_tree = ctx.accounts.receipts_merkle_tree.key; + let receipts_collection = ctx.accounts.receipts_collection.key; + let receipt_asset_id = get_asset_id(receipts_merkle_tree, args.receipt_nonce); + let receipt_owner = *ctx.accounts.reviewer.key; + let receipt_delegate = receipt_owner; + let collection_hash = keccak::hashv(&[receipts_collection.as_ref()]).to_bytes(); + + // Bind the receipt to the reviewed agent AND the reviewing client. + // MintWorkReceiptV1 always writes: + // creators = [ + // {address: agent_asset, verified: false, share: 100}, + // {address: client, verified: false, share: 0 }, + // ] + // so we can compute the expected creator_hash on-chain from + // `ctx.accounts.asset.key` and `ctx.accounts.reviewer.key`. This: + // 1. Prevents replay of a real receipt for AgentA against a review + // for AgentB — the reconstructed leaf hash would no longer match + // the receipt's actual leaf in the tree. + // 2. Cryptographically gates LeaveReviewV1 on the reviewer being + // the client the receipt was minted for, with no signer + // handshake at mint time. A stranger who somehow owned the leaf + // could not reproduce this hash with their own pubkey — the + // verify_leaf CPI would reject. + let expected_creator_hash = keccak::hashv(&[ + ctx.accounts.asset.key.as_ref(), + &[0u8], + &[100u8], + ctx.accounts.reviewer.key.as_ref(), + &[0u8], + &[0u8], + ]) + .to_bytes(); + + let leaf_hash = keccak::hashv(&[ + // LeafSchemaV2 version byte + &[2u8], + receipt_asset_id.as_ref(), + receipt_owner.as_ref(), + receipt_delegate.as_ref(), + &args.receipt_nonce.to_le_bytes(), + &args.receipt_data_hash, + &expected_creator_hash, + &collection_hash, + &args.receipt_asset_data_hash, + &[args.receipt_flags], + ]) + .to_bytes(); + + verify_leaf_cpi( + ctx.accounts.compression_program, + ctx.accounts.receipts_merkle_tree, + args.receipt_root, + leaf_hash, + args.receipt_index, + proof_accounts, + )?; + + /****************************************************/ + /************ Create Review Record PDA **************/ + /****************************************************/ + // + // The PDA's existence is the idempotency guarantee — a second + // `LeaveReviewV1` against the same receipt fails at account creation. + + let record_bump = + ReviewRecordV1::check_pda_derivation(ctx.accounts.review_record, &receipt_asset_id)?; + + // Pre-flight check so we return a clean error rather than the + // system-program create-account failure when re-reviewing. + if ctx.accounts.review_record.data_len() != 0 + || *ctx.accounts.review_record.owner != system_program::id() + { + return Err(MplAgentReputationError::ReviewAlreadyExists.into()); + } + + ReviewRecordV1::create_account( + ctx.accounts.review_record, + ctx.accounts.system_program, + ctx.accounts.payer, + &receipt_asset_id, + record_bump, + )?; + + { + let mut data = ctx.accounts.review_record.try_borrow_mut_data()?; + let record: &mut ReviewRecordV1 = + bytemuck::from_bytes_mut(&mut data[..core::mem::size_of::()]); + record.initialize(record_bump, ctx.accounts.reviewer.key, &receipt_asset_id); + } + + /****************************************************/ + /**************** Mint review cNFT ******************/ + /****************************************************/ + + msg!( + "Review rating={} reviewer={} agent={} receipt={}", + args.rating, + ctx.accounts.reviewer.key, + ctx.accounts.asset.key, + receipt_asset_id, + ); + + let metadata = MetadataArgsV2 { + name: format!("Agent Feedback ({}★)", args.rating), + symbol: "AGENTFB".to_string(), + uri: feedback_uri, + seller_fee_basis_points: 0, + primary_sale_happened: false, + is_mutable: false, + token_standard: Some(TokenStandard::NonFungible), + creators: vec![Creator { + address: *ctx.accounts.reviewer.key, + verified: false, + share: 100, + }], + collection: Some(*ctx.accounts.core_collection.key), + }; + + // The reviews_authority PDA was registered as tree_creator at + // RegisterReviewsTreeV1 time AND is the reviews collection's + // update_authority. Signing the MintV2 CPI with this single PDA + // satisfies both `tree_creator_or_delegate` and the default + // `collection_authority`. + let authority_seeds: &[&[u8]] = &[REVIEWS_AUTHORITY_PREFIX, &[authority_bump]]; + + MintV2CpiBuilder::new(ctx.accounts.bubblegum_program) + .tree_config(ctx.accounts.tree_config) + .payer(ctx.accounts.payer) + .tree_creator_or_delegate(Some(ctx.accounts.authority)) + .collection_authority(None) + .leaf_owner(ctx.accounts.leaf_owner) + .leaf_delegate(None) + .merkle_tree(ctx.accounts.merkle_tree) + .core_collection(Some(ctx.accounts.core_collection)) + .mpl_core_cpi_signer(Some(ctx.accounts.mpl_core_cpi_signer)) + .log_wrapper(ctx.accounts.log_wrapper) + .compression_program(ctx.accounts.compression_program) + .mpl_core_program(ctx.accounts.mpl_core_program) + .system_program(ctx.accounts.system_program) + .metadata(metadata) + .invoke_signed(&[authority_seeds])?; + + Ok(()) +} + +/// Construct + invoke MPL Account Compression's `verify_leaf` instruction +/// without depending on the SDK crate (whose newer versions conflict with +/// our pinned solana-program 2.3). Anchor wire format: +/// data = [discriminator(8) || root(32) || leaf(32) || index(u32 LE)] +/// accounts = [merkle_tree(read), ...proof_nodes(read)] +fn verify_leaf_cpi<'a>( + compression_program: &AccountInfo<'a>, + merkle_tree: &AccountInfo<'a>, + root: [u8; 32], + leaf: [u8; 32], + index: u32, + proof_accounts: &[AccountInfo<'a>], +) -> ProgramResult { + let mut data = Vec::with_capacity(8 + 32 + 32 + 4); + data.extend_from_slice(&VERIFY_LEAF_DISCRIMINATOR); + data.extend_from_slice(&root); + data.extend_from_slice(&leaf); + data.extend_from_slice(&index.to_le_bytes()); + + let mut metas = Vec::with_capacity(1 + proof_accounts.len()); + metas.push(solana_program::instruction::AccountMeta::new_readonly( + *merkle_tree.key, + false, + )); + for proof in proof_accounts { + metas.push(solana_program::instruction::AccountMeta::new_readonly( + *proof.key, false, + )); + } + + let ix = solana_program::instruction::Instruction { + program_id: MPL_ACCOUNT_COMPRESSION_ID, + accounts: metas, + data, + }; + + let mut infos = Vec::with_capacity(2 + proof_accounts.len()); + infos.push(compression_program.clone()); + infos.push(merkle_tree.clone()); + for proof in proof_accounts { + infos.push(proof.clone()); + } + + invoke(&ix, &infos) +} + +/// Parse a Borsh-style length-prefixed UTF-8 string from `data`: +/// `[u32 LE length][bytes...]`. Rejects if the buffer is too short, the +/// declared length exceeds `max_len`, or the bytes aren't valid UTF-8. +fn read_length_prefixed_string(data: &[u8], max_len: usize) -> Result { + if data.len() < 4 { + return Err(MplAgentReputationError::InvalidInstructionData.into()); + } + let len = u32::from_le_bytes( + data[..4] + .try_into() + .map_err(|_| MplAgentReputationError::InvalidInstructionData)?, + ) as usize; + if len > max_len || data.len() < 4 + len { + return Err(MplAgentReputationError::FeedbackUriInvalid.into()); + } + String::from_utf8(data[4..4 + len].to_vec()) + .map_err(|_| MplAgentReputationError::InvalidInstructionData.into()) +} diff --git a/programs/mpl-agent-reputation/src/processor/mod.rs b/programs/mpl-agent-reputation/src/processor/mod.rs index dbe68f4..a43542b 100644 --- a/programs/mpl-agent-reputation/src/processor/mod.rs +++ b/programs/mpl-agent-reputation/src/processor/mod.rs @@ -1,39 +1,46 @@ -mod register; +mod create_reviews_collection_v1; +mod leave_review; +mod register_reviews_tree_v1; -use bytemuck::try_from_bytes; use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; use crate::error::MplAgentReputationError; use crate::instruction::MplAgentReputationInstructionDiscriminant; -pub use register::{register_reputation_v1, RegisterReputationV1Args}; +pub use create_reviews_collection_v1::{ + cast_create_reviews_collection_args, create_reviews_collection_v1, + CreateReviewsCollectionV1Args, +}; +pub use leave_review::{leave_review_v1, LeaveReviewV1Args, MAX_FEEDBACK_URI_LEN}; +pub use register_reviews_tree_v1::{ + cast_register_reviews_tree_args, register_reviews_tree_v1, RegisterReviewsTreeV1Args, +}; /// Process incoming instructions. -/// -/// # Arguments -/// * `_program_id` - The program ID (unused but available for validation) -/// * `accounts` - The accounts required for the instruction -/// * `instruction_data` - The instruction data containing the discriminant and arguments #[inline] pub fn process_instruction<'a>( _program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], instruction_data: &[u8], ) -> ProgramResult { - // Ensure we have at least 1 byte for the discriminant. if instruction_data.is_empty() { return Err(MplAgentReputationError::InvalidInstructionData.into()); } - // Route by discriminant (first byte). match MplAgentReputationInstructionDiscriminant::try_from(instruction_data[0]) { - Ok(MplAgentReputationInstructionDiscriminant::RegisterReputationV1) => { - msg!("Instruction: RegisterReputationV1"); - register_reputation_v1( - accounts, - try_from_bytes(instruction_data) - .map_err(|_| MplAgentReputationError::InvalidInstructionData)?, - ) + Ok(MplAgentReputationInstructionDiscriminant::LeaveReviewV1) => { + msg!("Instruction: LeaveReviewV1"); + leave_review_v1(accounts, instruction_data) + } + Ok(MplAgentReputationInstructionDiscriminant::CreateReviewsCollectionV1) => { + msg!("Instruction: CreateReviewsCollectionV1"); + let args = cast_create_reviews_collection_args(instruction_data)?; + create_reviews_collection_v1(accounts, args) + } + Ok(MplAgentReputationInstructionDiscriminant::RegisterReviewsTreeV1) => { + msg!("Instruction: RegisterReviewsTreeV1"); + let args = cast_register_reviews_tree_args(instruction_data)?; + register_reviews_tree_v1(accounts, args) } Err(_) => Err(MplAgentReputationError::InvalidInstructionData.into()), } diff --git a/programs/mpl-agent-reputation/src/processor/register.rs b/programs/mpl-agent-reputation/src/processor/register.rs deleted file mode 100644 index 3138f98..0000000 --- a/programs/mpl-agent-reputation/src/processor/register.rs +++ /dev/null @@ -1,166 +0,0 @@ -use bytemuck::{Pod, Zeroable}; -use mpl_core::accounts::BaseAssetV1; -use mpl_core::fetch_wrapped_external_plugin_adapter; -use mpl_core::instructions::{ - AddExternalPluginAdapterV1Cpi, AddExternalPluginAdapterV1InstructionArgs, -}; -use mpl_core::types::{ - AppDataInitInfo, ExternalPluginAdapterInitInfo, ExternalPluginAdapterKey, - ExternalPluginAdapterSchema, Key as MplCoreKey, PluginAuthority, -}; -use mpl_utils::assert_signer; -use shank::ShankType; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult}; -use solana_system_interface::program as system_program; - -use crate::{ - error::MplAgentReputationError, instruction::accounts::RegisterReputationV1Accounts, - state::AgentReputationV1, -}; - -/// Arguments for the RegisterReputationV1 instruction. -/// -/// # Layout -/// - discriminator: 1 byte (instruction discriminant, excluded from IDL) -/// - _padding: 1 byte (alignment) -/// - arg1: 2 bytes -/// - arg2: 4 bytes -/// -/// Total: 8 bytes (8-byte aligned) -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable, ShankType)] -pub struct RegisterReputationV1Args { - /// Instruction discriminator (not included in IDL). - #[skip] - pub discriminator: u8, - /// Padding for alignment. - #[padding] - pub _padding: [u8; 7], -} - -// Compile-time assertion to ensure struct is properly sized. -const _: () = assert!(core::mem::size_of::() == 8); - -/// RegisterReputationV1 a new Agent Reputation. -/// -/// # Accounts -/// 0. `[writable, signer]` agent_reputation - The address of the new agent reputation -/// 1. `[]` authority - The authority of the agent reputation -/// 2. `[writable, signer]` payer - The account paying for the storage fees -/// 3. `[]` system_program - The system program -/// -/// # Arguments -/// * `accounts` - The accounts required for the instruction -/// * `args` - The instruction arguments (zero-copy reference) -pub fn register_reputation_v1<'a>( - accounts: &'a [AccountInfo<'a>], - _args: &RegisterReputationV1Args, -) -> ProgramResult { - /****************************************************/ - /****************** Account Setup *******************/ - /****************************************************/ - - let ctx = RegisterReputationV1Accounts::context(accounts)?; - - /****************************************************/ - /****************** Account Guards ******************/ - /****************************************************/ - - let agent_reputation_bump = AgentReputationV1::check_pda_derivation( - ctx.accounts.agent_reputation, - ctx.accounts.asset.key, - )?; - - // Agent reputation PDA must not already be initialized. System CreateAccount - // would fail anyway, but checking up front gives a clear error and - // defends against any future change in the downstream helper. - if ctx.accounts.agent_reputation.data_len() != 0 - || *ctx.accounts.agent_reputation.owner != system_program::id() - { - return Err(MplAgentReputationError::AgentReputationAlreadyRegistered.into()); - } - - // Assert that the asset exists and is a Core asset. - if ctx.accounts.asset.owner != &mpl_core::ID - || ctx.accounts.asset.try_borrow_data()?[0] != MplCoreKey::AssetV1 as u8 - { - return Err(MplAgentReputationError::InvalidCoreAsset.into()); - } - - // Payer must sign. - assert_signer(ctx.accounts.payer)?; - - // If an explicit authority is passed it must also sign. When the AppData - // plugin already exists we skip the MPL Core CPI below — which is the - // only implicit authority check — so enforcing the signer here is the - // only thing preventing unauthorized reputation registration in that - // branch. - if let Some(authority) = ctx.accounts.authority { - assert_signer(authority)?; - } - - // Validate the MPL Core program. - if *ctx.accounts.mpl_core_program.key != mpl_core::ID { - return Err(MplAgentReputationError::InvalidMplCoreProgram.into()); - } - - // Validate system program. - if *ctx.accounts.system_program.key != system_program::id() { - return Err(MplAgentReputationError::InvalidSystemProgram.into()); - } - - /****************************************************/ - /***************** Argument Guards ******************/ - /****************************************************/ - - // Add any argument validation here. - // Example: if args.arg1 == 0 { return Err(MplAgentReputationError::InvalidArgument.into()); } - - /****************************************************/ - /********************* Actions **********************/ - /****************************************************/ - // Create the agent identity account. - AgentReputationV1::create_account(&ctx.accounts, agent_reputation_bump)?; - - // Initialize the account using zero-copy. - // Borrow the account data mutably and cast to our struct. - let mut data = ctx.accounts.agent_reputation.try_borrow_mut_data()?; - let agent_reputation: &mut AgentReputationV1 = - bytemuck::from_bytes_mut(&mut data[..core::mem::size_of::()]); - - agent_reputation.initialize(agent_reputation_bump, ctx.accounts.asset.key); - - // Check if the asset already has a AppData plugin. - let result = fetch_wrapped_external_plugin_adapter::( - ctx.accounts.asset, - None, - &ExternalPluginAdapterKey::AppData(PluginAuthority::Address { - address: *ctx.accounts.agent_reputation.key, - }), - ); - - // If the asset already has a AppData plugin, move on, otherwise create it. - if result.is_err() { - AddExternalPluginAdapterV1Cpi { - __program: ctx.accounts.mpl_core_program, - asset: ctx.accounts.asset, - collection: ctx.accounts.collection, - payer: ctx.accounts.payer, - authority: ctx.accounts.authority, - system_program: ctx.accounts.system_program, - log_wrapper: None, - __args: AddExternalPluginAdapterV1InstructionArgs { - init_info: ExternalPluginAdapterInitInfo::AppData(AppDataInitInfo { - data_authority: PluginAuthority::Address { - address: *ctx.accounts.agent_reputation.key, - }, - init_plugin_authority: None, - schema: Some(ExternalPluginAdapterSchema::Binary), - }), - }, - } - .invoke()?; - } - - Ok(()) -} diff --git a/programs/mpl-agent-reputation/src/processor/register_reviews_tree_v1.rs b/programs/mpl-agent-reputation/src/processor/register_reviews_tree_v1.rs new file mode 100644 index 0000000..5790ec8 --- /dev/null +++ b/programs/mpl-agent-reputation/src/processor/register_reviews_tree_v1.rs @@ -0,0 +1,159 @@ +use bytemuck::{Pod, Zeroable}; +use mpl_bubblegum::{instructions::CreateTreeConfigV2CpiBuilder, ID as BUBBLEGUM_ID}; +use mpl_utils::assert_signer; +use shank::ShankType; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, msg, program::invoke_signed, + program_error::ProgramError, rent::Rent, sysvar::Sysvar, +}; +use solana_system_interface::{instruction as system_instruction, program as system_program}; + +use crate::{ + error::MplAgentReputationError, + instruction::accounts::RegisterReviewsTreeV1Accounts, + state::{ + check_reviews_authority_pda, check_reviews_tree_pda, MPL_ACCOUNT_COMPRESSION_ID, + REVIEWS_AUTHORITY_PREFIX, REVIEWS_TREE_PREFIX, + }, +}; + +/// Permissionless tree registration: the caller picks an unused +/// `tree_index` and pays the rent. The tree is created at PDA +/// `["reviews_tree", index_le]` and Bubblegum is configured with +/// `tree_creator = ["reviews_authority"]` PDA, so the program signs +/// every future mint. +/// +/// First-come-first-served: if two callers race for the same index the +/// loser's CreateAccount fails because the account is already +/// initialised. The loser just retries with a higher index. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable, ShankType)] +pub struct RegisterReviewsTreeV1Args { + #[skip] + pub discriminator: u8, + #[padding] + pub _pad: [u8; 7], + pub tree_index: u64, + pub max_depth: u32, + pub max_buffer_size: u32, + pub canopy_depth: u32, + #[padding] + pub _pad2: [u8; 4], +} +const _: () = assert!(core::mem::size_of::() == 32); + +pub fn register_reviews_tree_v1<'a>( + accounts: &'a [AccountInfo<'a>], + args: &RegisterReviewsTreeV1Args, +) -> ProgramResult { + let ctx = RegisterReviewsTreeV1Accounts::context(accounts)?; + + assert_signer(ctx.accounts.payer)?; + + if *ctx.accounts.bubblegum_program.key != BUBBLEGUM_ID { + return Err(MplAgentReputationError::InvalidBubblegumProgram.into()); + } + if *ctx.accounts.compression_program.key != MPL_ACCOUNT_COMPRESSION_ID { + return Err(MplAgentReputationError::InvalidCompressionProgram.into()); + } + if *ctx.accounts.system_program.key != system_program::id() { + return Err(MplAgentReputationError::InvalidSystemProgram.into()); + } + + let authority_bump = check_reviews_authority_pda(ctx.accounts.authority)?; + + let tree_bump = check_reviews_tree_pda(ctx.accounts.merkle_tree, args.tree_index)?; + if ctx.accounts.merkle_tree.data_len() != 0 + || *ctx.accounts.merkle_tree.owner != system_program::id() + { + return Err(MplAgentReputationError::InvalidAccountData.into()); + } + + let size = merkle_tree_account_size( + args.max_depth as usize, + args.max_buffer_size as usize, + args.canopy_depth as usize, + )?; + let rent_lamports = Rent::get()?.minimum_balance(size); + + let index_bytes = args.tree_index.to_le_bytes(); + let tree_seeds: &[&[u8]] = &[REVIEWS_TREE_PREFIX, &index_bytes, &[tree_bump]]; + let authority_seeds: &[&[u8]] = &[REVIEWS_AUTHORITY_PREFIX, &[authority_bump]]; + + invoke_signed( + &system_instruction::create_account( + ctx.accounts.payer.key, + ctx.accounts.merkle_tree.key, + rent_lamports, + size as u64, + &MPL_ACCOUNT_COMPRESSION_ID, + ), + &[ + ctx.accounts.payer.clone(), + ctx.accounts.merkle_tree.clone(), + ctx.accounts.system_program.clone(), + ], + &[tree_seeds], + )?; + + CreateTreeConfigV2CpiBuilder::new(ctx.accounts.bubblegum_program) + .tree_config(ctx.accounts.tree_config) + .merkle_tree(ctx.accounts.merkle_tree) + .payer(ctx.accounts.payer) + .tree_creator(Some(ctx.accounts.authority)) + .log_wrapper(ctx.accounts.log_wrapper) + .compression_program(ctx.accounts.compression_program) + .system_program(ctx.accounts.system_program) + .max_depth(args.max_depth) + .max_buffer_size(args.max_buffer_size) + .public(false) + .invoke_signed(&[authority_seeds])?; + + msg!( + "Registered reviews tree #{} at {}", + args.tree_index, + ctx.accounts.merkle_tree.key, + ); + + Ok(()) +} + +/// Compute the merkle tree account size in bytes, matching +/// spl-concurrent-merkle-tree's struct layout. `Path` has no +/// leaf field — verified empirically against Bubblegum's canopy check. +fn merkle_tree_account_size( + max_depth: usize, + max_buffer_size: usize, + canopy_depth: usize, +) -> Result { + const HEADER_SIZE: usize = 88; + const NODE_SIZE: usize = 32; + let change_log_size = NODE_SIZE + max_depth * NODE_SIZE + 4 + 4; + let change_logs_total = change_log_size + .checked_mul(max_buffer_size) + .ok_or(ProgramError::ArithmeticOverflow)?; + let scalars = 24; + let path_size = max_depth * NODE_SIZE + 4 + 4; + let canopy_size = if canopy_depth == 0 { + 0 + } else { + let nodes = (1usize << (canopy_depth + 1)) + .checked_sub(2) + .ok_or(ProgramError::ArithmeticOverflow)?; + nodes + .checked_mul(NODE_SIZE) + .ok_or(ProgramError::ArithmeticOverflow)? + }; + Ok(HEADER_SIZE + scalars + change_logs_total + path_size + canopy_size) +} + +pub fn cast_register_reviews_tree_args( + data: &[u8], +) -> Result<&RegisterReviewsTreeV1Args, ProgramError> { + if data.len() < core::mem::size_of::() { + return Err(MplAgentReputationError::InvalidInstructionData.into()); + } + Ok(bytemuck::from_bytes( + &data[..core::mem::size_of::()], + )) +} diff --git a/programs/mpl-agent-reputation/src/state/agent_reputation.rs b/programs/mpl-agent-reputation/src/state/agent_reputation.rs deleted file mode 100644 index af66a7c..0000000 --- a/programs/mpl-agent-reputation/src/state/agent_reputation.rs +++ /dev/null @@ -1,75 +0,0 @@ -use bytemuck::{Pod, Zeroable}; -use mpl_utils::{assert_derivation, create_or_allocate_account_raw}; -use shank::ShankAccount; -use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, - pubkey::Pubkey, -}; - -use crate::{error::MplAgentReputationError, instruction::accounts::RegisterReputationV1Accounts}; - -use super::Key; - -/// PDA account structure using zero-copy patterns. -/// -/// # Layout -/// - key: 1 byte (account discriminator) -/// - bump: 1 byte (PDA bump seed) -/// - _padding: 6 bytes (alignment to 8 bytes) -/// -/// Total: 8 bytes (8-byte aligned) -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable, ShankAccount)] -pub struct AgentReputationV1 { - /// Account discriminator. - #[idl_type(Key)] - pub key: u8, - /// PDA bump seed. - pub bump: u8, - /// Padding for 8-byte alignment. - #[padding] - pub _padding: [u8; 6], - /// The address of the asset. - pub asset: Pubkey, -} - -// Compile-time assertion to ensure struct is 8-byte aligned. -const _: () = assert!(core::mem::size_of::() % 8 == 0); -const _: () = assert!(core::mem::size_of::() == 40); - -impl AgentReputationV1 { - /// PDA seed prefix for this account type. - pub const PREFIX: &'static [u8] = b"agent_reputation"; - - pub fn check_pda_derivation(address: &AccountInfo, asset: &Pubkey) -> Result { - solana_program::msg!("Checking PDA derivation for asset"); - assert_derivation( - &crate::ID, - address, - &[Self::PREFIX, asset.as_ref()], - MplAgentReputationError::InvalidAccountData, - ) - } - - pub fn create_account(accounts: &RegisterReputationV1Accounts, bump: u8) -> ProgramResult { - solana_program::msg!("Creating agent reputation account"); - create_or_allocate_account_raw( - crate::ID, - accounts.agent_reputation, - accounts.system_program, - accounts.payer, - core::mem::size_of::(), - &[Self::PREFIX, accounts.asset.key.as_ref(), &[bump]], - ) - } - - /// Initialize the account with the given bump seed. - #[inline] - pub fn initialize(&mut self, bump: u8, asset: &Pubkey) { - solana_program::msg!("Initializing agent reputation account"); - self.key = Key::AgentReputationV1 as u8; - self.bump = bump; - self._padding = [0u8; 6]; - self.asset = *asset; - } -} diff --git a/programs/mpl-agent-reputation/src/state/mod.rs b/programs/mpl-agent-reputation/src/state/mod.rs index 3f1e9ad..2114de2 100644 --- a/programs/mpl-agent-reputation/src/state/mod.rs +++ b/programs/mpl-agent-reputation/src/state/mod.rs @@ -1,23 +1,24 @@ -mod agent_reputation; +mod review_record; +mod seeds; -pub use agent_reputation::*; +pub use review_record::*; +pub use seeds::*; use shank::ShankType; /// Account discriminator enum. -/// Stored as a u8 in account data but represented as an enum for type safety. #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, ShankType)] pub enum Key { Uninitialized, - AgentReputationV1, + ReviewRecordV1, } impl From for Key { fn from(value: u8) -> Self { match value { 0 => Key::Uninitialized, - 1 => Key::AgentReputationV1, + 1 => Key::ReviewRecordV1, _ => Key::Uninitialized, } } diff --git a/programs/mpl-agent-reputation/src/state/review_record.rs b/programs/mpl-agent-reputation/src/state/review_record.rs new file mode 100644 index 0000000..25c22ee --- /dev/null +++ b/programs/mpl-agent-reputation/src/state/review_record.rs @@ -0,0 +1,93 @@ +use bytemuck::{Pod, Zeroable}; +use mpl_utils::{assert_derivation, create_or_allocate_account_raw}; +use shank::ShankAccount; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::error::MplAgentReputationError; + +use super::Key; + +/// PDA recorded once per (work-receipt cNFT, review) pair so the program can +/// prove "this receipt has been reviewed" without trusting any off-chain +/// signal. The account's existence itself is the gate — a second +/// `LeaveReviewV1` against the same receipt fails because +/// `create_or_allocate_account_raw` cannot create over a non-system +/// account. +/// +/// # Layout +/// - key: 1 byte +/// - bump: 1 byte +/// - _padding: 6 bytes +/// - reviewer: 32 bytes +/// - receipt_asset_id: 32 bytes +/// +/// Total: 72 bytes (8-byte aligned). +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable, ShankAccount)] +pub struct ReviewRecordV1 { + /// Account discriminator. + #[idl_type(Key)] + pub key: u8, + /// PDA bump seed. + pub bump: u8, + /// Padding for 8-byte alignment. + #[padding] + pub _padding: [u8; 6], + /// The wallet that left the review. + pub reviewer: Pubkey, + /// The Bubblegum asset id of the work receipt this review references. + pub receipt_asset_id: Pubkey, +} + +const _: () = assert!(core::mem::size_of::() % 8 == 0); +const _: () = assert!(core::mem::size_of::() == 72); + +impl ReviewRecordV1 { + /// Seed prefix. + pub const PREFIX: &'static [u8] = b"review_record"; + + /// Verify that the supplied account is at the canonical PDA for the + /// given receipt asset id, and return the bump. + pub fn check_pda_derivation( + address: &AccountInfo, + receipt_asset_id: &Pubkey, + ) -> Result { + assert_derivation( + &crate::ID, + address, + &[Self::PREFIX, receipt_asset_id.as_ref()], + MplAgentReputationError::InvalidAccountData, + ) + } + + /// Allocate and own a new ReviewRecord account, signed by PDA seeds. + pub fn create_account<'a>( + account: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, + payer: &AccountInfo<'a>, + receipt_asset_id: &Pubkey, + bump: u8, + ) -> ProgramResult { + create_or_allocate_account_raw( + crate::ID, + account, + system_program, + payer, + core::mem::size_of::(), + &[Self::PREFIX, receipt_asset_id.as_ref(), &[bump]], + ) + } + + /// Initialize the account fields. + #[inline] + pub fn initialize(&mut self, bump: u8, reviewer: &Pubkey, receipt_asset_id: &Pubkey) { + self.key = Key::ReviewRecordV1 as u8; + self.bump = bump; + self._padding = [0u8; 6]; + self.reviewer = *reviewer; + self.receipt_asset_id = *receipt_asset_id; + } +} diff --git a/programs/mpl-agent-reputation/src/state/seeds.rs b/programs/mpl-agent-reputation/src/state/seeds.rs new file mode 100644 index 0000000..09a5d3a --- /dev/null +++ b/programs/mpl-agent-reputation/src/state/seeds.rs @@ -0,0 +1,102 @@ +//! Stateless PDA derivations for the reviews collection, the program's +//! signing authority, and per-tree merkle accounts. Also carries the +//! cross-program checks (receipts collection + receipts tree) that bind +//! LeaveReviewV1 to canonical mpl-agent-tools state. + +use mpl_utils::assert_derivation; +use solana_program::{ + account_info::AccountInfo, program_error::ProgramError, pubkey, pubkey::Pubkey, +}; + +use crate::error::MplAgentReputationError; + +/// Seeds for the canonical reviews collection. +pub const REVIEWS_COLLECTION_PREFIX: &[u8] = b"reviews_collection"; + +/// Seeds for the program's signing authority. Update authority on the +/// reviews collection AND tree_creator on every reviews tree. +pub const REVIEWS_AUTHORITY_PREFIX: &[u8] = b"reviews_authority"; + +/// Seeds for per-tree merkle accounts: `["reviews_tree", index_le]`. +pub const REVIEWS_TREE_PREFIX: &[u8] = b"reviews_tree"; + +/// Seeds for the canonical receipts collection (lives under mpl-agent-tools). +pub const RECEIPTS_COLLECTION_PREFIX: &[u8] = b"receipts_collection"; + +/// Seeds for per-tree receipts merkle accounts under mpl-agent-tools: +/// `["receipts_tree", index_le]`. +pub const RECEIPTS_TREE_PREFIX: &[u8] = b"receipts_tree"; + +/// Program id of mpl-agent-tools. Inlined as a constant (rather than +/// pulled from `mpl_agent_tools::ID` via the rust-tools client crate) to +/// keep this program free of generated client code. Must match the +/// `declare_id!` in `programs/mpl-agent-tools/src/lib.rs`. +pub const MPL_AGENT_TOOLS_ID: Pubkey = pubkey!("TLREGni9ZEyGC3vnPZtqUh95xQ8oPqJSvNjvB7FGK8S"); + +/// Program id of MPL Account Compression — the on-chain home of every +/// Bubblegum V2 tree. Pinned at the boundary of every CPI into it +/// (verify_leaf, CreateTreeConfigV2) to defeat compression-program +/// spoofing. +pub const MPL_ACCOUNT_COMPRESSION_ID: Pubkey = + pubkey!("mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW"); + +pub fn check_reviews_collection_pda(address: &AccountInfo) -> Result { + assert_derivation( + &crate::ID, + address, + &[REVIEWS_COLLECTION_PREFIX], + MplAgentReputationError::InvalidReviewsCollection, + ) +} + +pub fn check_reviews_authority_pda(address: &AccountInfo) -> Result { + assert_derivation( + &crate::ID, + address, + &[REVIEWS_AUTHORITY_PREFIX], + MplAgentReputationError::InvalidReviewsAuthority, + ) +} + +pub fn check_reviews_tree_pda(address: &AccountInfo, index: u64) -> Result { + assert_derivation( + &crate::ID, + address, + &[REVIEWS_TREE_PREFIX, &index.to_le_bytes()], + MplAgentReputationError::InvalidReviewsTreeDerivation, + ) +} + +/// Verify the supplied account is the canonical receipts collection PDA +/// (derived from `mpl-agent-tools`'s program id + `b"receipts_collection"`). +/// This is the cross-program canonicalization check — LeaveReviewV1 must +/// reference a receipt minted by mpl-agent-tools. +pub fn check_receipts_collection_pda(address: &AccountInfo) -> Result { + assert_derivation( + &MPL_AGENT_TOOLS_ID, + address, + &[RECEIPTS_COLLECTION_PREFIX], + MplAgentReputationError::InvalidReceiptsCollection, + ) +} + +/// Verify the supplied account is the canonical receipts tree PDA for +/// `index` (derived from `mpl-agent-tools`'s program id + +/// `[b"receipts_tree", index_le]`). Without this check a caller could pass +/// any attacker-controlled compression tree and forge a receipt leaf, +/// bypassing the work-receipt gate on LeaveReviewV1. +pub fn check_receipts_tree_pda(address: &AccountInfo, index: u64) -> Result { + assert_derivation( + &MPL_AGENT_TOOLS_ID, + address, + &[RECEIPTS_TREE_PREFIX, &index.to_le_bytes()], + MplAgentReputationError::InvalidReceiptsTreeDerivation, + ) +} + +/// Helper: derive the receipts collection address without account check. +pub fn receipts_collection_address() -> Pubkey { + let (address, _bump) = + Pubkey::find_program_address(&[RECEIPTS_COLLECTION_PREFIX], &MPL_AGENT_TOOLS_ID); + address +}