Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add candy machine gpa builders #95

Merged
merged 13 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/plugins/candyMachineModule/CandyMachineClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
CreateCandyMachineOutput,
} from './createCandyMachine';
import { findCandyMachineByAdddressOperation } from './findCandyMachineByAddress';
import { findCandyMachinesByPublicKeyFieldOperation } from './findCandyMachinesByPublicKeyField';
import { CandyMachine } from './CandyMachine';

export type CandyMachineInitFromConfigOpts = {
Expand All @@ -22,6 +23,24 @@ export class CandyMachineClient extends ModuleClient {
return this.metaplex.operations().execute(operation);
}

findCandyMachinesByWallet(wallet: PublicKey): Promise<CandyMachine[]> {
return this.metaplex.operations().execute(
findCandyMachinesByPublicKeyFieldOperation({
type: 'wallet',
publicKey: wallet,
})
);
}

findCandyMachinesByAuthority(authority: PublicKey): Promise<CandyMachine[]> {
return this.metaplex.operations().execute(
findCandyMachinesByPublicKeyFieldOperation({
type: 'authority',
publicKey: authority,
})
);
}

async createCandyMachine(
input: CreateCandyMachineInput
): Promise<CreateCandyMachineOutput & { candyMachine: CandyMachine }> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { PublicKey } from '@solana/web3.js';
import { Operation, OperationHandler, useOperation } from '@/types';
import { Metaplex } from '@/Metaplex';
import { CandyMachine } from './CandyMachine';
import { CandyMachineAccount, CandyMachineProgram } from '../../programs';
import { UnreachableCaseError } from '../../errors';

// -----------------
// Operation
// -----------------
const Key = 'FindCandyMachinesByPublicKeyOperation' as const;

export const findCandyMachinesByPublicKeyFieldOperation =
useOperation<FindCandyMachinesByPublicKeyFieldOperation>(Key);

export type FindCandyMachinesByPublicKeyFieldInput = {
type: 'authority' | 'wallet';
publicKey: PublicKey;
};
export type FindCandyMachinesByPublicKeyFieldOperation = Operation<
typeof Key,
FindCandyMachinesByPublicKeyFieldInput,
CandyMachine[]
>;

// -----------------
// Handler
// -----------------
export const findCandyMachinesByPublicKeyFieldOnChainOperationHandler: OperationHandler<FindCandyMachinesByPublicKeyFieldOperation> =
{
handle: async (
operation: FindCandyMachinesByPublicKeyFieldOperation,
metaplex: Metaplex
): Promise<CandyMachine[]> => {
const { type, publicKey } = operation.input;
const accounts = CandyMachineProgram.accounts(metaplex);
let candyMachineQuery;
switch (type) {
case 'authority':
candyMachineQuery = accounts.candyMachineAccountsForAuthority(publicKey);
break;
case 'wallet':
candyMachineQuery = accounts.candyMachineAccountsForWallet(publicKey);
break;
default:
throw new UnreachableCaseError(type);
}

const candyMachineUnparseds = await candyMachineQuery.get();
return candyMachineUnparseds.map(CandyMachineAccount.from).map(CandyMachine.fromAccount);
},
};
8 changes: 8 additions & 0 deletions src/plugins/candyMachineModule/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,20 @@ import {
findCandyMachineByAdddressOperation,
findCandyMachineByAdddressOperationHandler,
} from './findCandyMachineByAddress';
import {
findCandyMachinesByPublicKeyFieldOperation,
findCandyMachinesByPublicKeyFieldOnChainOperationHandler,
} from './findCandyMachinesByPublicKeyField';

export const candyMachineModule = (): MetaplexPlugin => ({
install(metaplex: Metaplex) {
const op = metaplex.operations();
op.register(createCandyMachineOperation, createCandyMachineOperationHandler);
op.register(findCandyMachineByAdddressOperation, findCandyMachineByAdddressOperationHandler);
op.register(
findCandyMachinesByPublicKeyFieldOperation,
findCandyMachinesByPublicKeyFieldOnChainOperationHandler
);

metaplex.candyMachines = function () {
return new CandyMachineClient(this);
Expand Down
11 changes: 11 additions & 0 deletions src/programs/candyMachine/CandyMachineProgram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PROGRAM_ID } from '@metaplex-foundation/mpl-candy-machine';
import { CandyMachineGpaBuilder } from './gpaBuilders';
import { Metaplex } from '@/Metaplex';

export const CandyMachineProgram = {
publicKey: PROGRAM_ID,

accounts(metaplex: Metaplex) {
return new CandyMachineGpaBuilder(metaplex, this.publicKey);
},
};
29 changes: 29 additions & 0 deletions src/programs/candyMachine/gpaBuilders/CandyMachineGpaBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { GpaBuilder } from '@/utils';
import { PublicKey } from '@solana/web3.js';

type AccountDiscriminator = [number, number, number, number, number, number, number, number];
// TODO(thlorenz): copied from candy machine SDK
// SDK should either provide a GPA builder or expose this discriminator
const candyMachineDiscriminator: AccountDiscriminator = [51, 173, 177, 113, 25, 241, 109, 189];

const AUTHORITY = candyMachineDiscriminator.length;
const WALLET = AUTHORITY + PublicKey.default.toBytes().byteLength;

export class CandyMachineGpaBuilder extends GpaBuilder {
whereDiscriminator(discrimator: AccountDiscriminator) {
return this.where(0, Buffer.from(discrimator));
}

candyMachineAccounts() {
return this.whereDiscriminator(candyMachineDiscriminator);
}

// wallet same as solTreasury
candyMachineAccountsForWallet(wallet: PublicKey) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remind me to have a chat with you about this. I have recently been creating GpaBuilder classes for each account type (that extend a program GpaBuild when needed) where the constructor enforces the account constraint. I think this is good for now but something to discuss when we think about auto-generating them.

return this.candyMachineAccounts().where(WALLET, wallet.toBase58());
}

candyMachineAccountsForAuthority(authority: PublicKey) {
return this.candyMachineAccounts().where(AUTHORITY, authority.toBase58());
}
}
1 change: 1 addition & 0 deletions src/programs/candyMachine/gpaBuilders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CandyMachineGpaBuilder';
2 changes: 2 additions & 0 deletions src/programs/candyMachine/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './accounts';
export * from './CandyMachineProgram';
export * from './gpaBuilders';
export * from './transactionBuilders';
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import test from 'tape';
import spok from 'spok';
import { killStuckProcess, metaplex, spokSamePubkey } from '../../helpers';
import { createCandyMachineWithMinimalConfig } from './helpers';

killStuckProcess();

// -----------------
// Wallet
// -----------------
test('candyMachineGPA: candyMachineAccountsForWallet for wallet with one candy machine created', async (t) => {
// Given I create one candy machine with a wallet
const mx = await metaplex();
const { candyMachineSigner, authorityAddress, walletAddress } =
await createCandyMachineWithMinimalConfig(mx);

// When I get the candy machines for the wallet
const candyMachines = await mx.candyMachines().findCandyMachinesByWallet(walletAddress);

// It returns that candy machine
t.equal(candyMachines.length, 1, 'returns one account');
const cm = candyMachines[0];
spok(t, cm, {
$topic: 'candyMachine',
authorityAddress: spokSamePubkey(authorityAddress),
walletAddress: spokSamePubkey(walletAddress),
});
t.ok(
candyMachineSigner.publicKey.toBase58().startsWith(cm.uuid),
'candy machine uuid matches candyMachineSigner'
);
});

test('candyMachineGPA: candyMachineAccountsForWallet for wallet with two candy machines created for that wallet and one for another', async (t) => {
// Given I create one candy machine with wallet1 and two with wallet2

// Other wallet
{
const mx = await metaplex();
await createCandyMachineWithMinimalConfig(mx);
}

const mx = await metaplex();
// This wallet
{
await createCandyMachineWithMinimalConfig(mx);
await createCandyMachineWithMinimalConfig(mx);
}

// When I get the candy machines for the wallet
const candyMachines = await mx.candyMachines().findCandyMachinesByWallet(mx.identity().publicKey);

// It returns the two candy machine of wallet2
t.equal(candyMachines.length, 2, 'returns two machines');

for (const cm of candyMachines) {
t.ok(cm.walletAddress.equals(mx.identity().publicKey), 'wallet matches');
}
});

// -----------------
// Authority
// -----------------
test('candyMachineGPA: candyMachineAccountsForAuthority for authority with one candy machine created', async (t) => {
// Given I create a candy machine with a specific auhority
const mx = await metaplex();
const { candyMachineSigner, authorityAddress, walletAddress } =
await createCandyMachineWithMinimalConfig(mx);

// When I get the candy machines for that authority
const candyMachines = await mx.candyMachines().findCandyMachinesByAuthority(authorityAddress);

// It returns that one candy machine for that authority
t.equal(candyMachines.length, 1, 'returns one account');
const cm = candyMachines[0];
spok(t, cm, {
$topic: 'candyMachine',
authorityAddress: spokSamePubkey(authorityAddress),
walletAddress: spokSamePubkey(walletAddress),
});
t.ok(
candyMachineSigner.publicKey.toBase58().startsWith(cm.uuid),
'candy machine uuid matches candyMachineSigner'
);
});
59 changes: 59 additions & 0 deletions test/plugins/candyMachineModule/helpers/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Keypair } from '@solana/web3.js';
import { amman, SKIP_PREFLIGHT } from '../../../helpers';
import { CandyMachineConfigWithoutStorage } from '@/plugins/candyMachineModule/config';
import { Metaplex } from '@/Metaplex';

/**
* Creates a candy machine using the mx.identity as signer as well as
* solTreasurySigner aka wallet.
*/
export async function createCandyMachineWithMinimalConfig(mx: Metaplex) {
const payer = mx.identity();

const solTreasurySigner = payer;
await amman.airdrop(mx.connection, solTreasurySigner.publicKey, 100);

const config: CandyMachineConfigWithoutStorage = {
price: 1.0,
number: 10,
sellerFeeBasisPoints: 0,
solTreasuryAccount: solTreasurySigner.publicKey.toBase58(),
goLiveDate: '25 Dec 2021 00:00:00 GMT',
retainAuthority: true,
isMutable: false,
};

const opts = {
candyMachine: Keypair.generate(),
confirmOptions: SKIP_PREFLIGHT,
};
await amman.addr.addLabels({ ...config, ...opts, payer });

const cm = mx.candyMachines();
const {
transactionId,
confirmResponse,
candyMachine,
payerSigner,
candyMachineSigner,
authorityAddress,
walletAddress,
} = await cm.createCandyMachineFromConfig(config, opts);

await amman.addr.addLabel('create: candy-machine', transactionId);

return {
cm,

transactionId,
confirmResponse,
candyMachine,
config,

solTreasurySigner,
payerSigner,
candyMachineSigner,
authorityAddress,
walletAddress,
};
}
1 change: 1 addition & 0 deletions test/plugins/candyMachineModule/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './asserts';
export * from './create';