Skip to content

Commit

Permalink
Implemented NFT creation with Liquidity Pool (#224)
Browse files Browse the repository at this point in the history
# Pull Request Description

## Changes Made
This PR adds the following changes:

- Implemented NFT creation with integrated liquidity pools in 3land
create NFT functionality

  
## Implementation Details

- Each NFT can be created with an associated liquidity pool
- Liquidity pools are identified by a unique poolName per wallet
- Multiple NFT editions can be created for the same liquidity pool
- Liquidity pools support any SPL token
- The SPL token used for the liquidity pool matches the NFT edition
token
- Sale proceeds (primary and secondary) are directed to the liquidity
pool
- Royalties from sales are automatically added to the liquidity pool
- NFT holders can burn their NFT to reclaim their investment
- Wallet addresses are restricted to one liquidity pool per poolName per
SPL token

## Transaction executed when testing
Example transactions created using solana-agent-kit:

1. First NFT edition with pool (pool 1):
https://dev.3.land/item/fXajTSwvrFCzaSL9mKE1KtTZ9wpChHC8egeXarMx3oD

2. Second edition with same pool (pool 1):
https://dev.3.land/item/8LdRScybmPQXkDdF2wafMSQ159ZDwckG1Pq7EkwGMFwW

3. Third edition with new pool (same SPL token):
https://dev.3.land/item/4HogLfgrgMYPpDqQK6Kw4kyGVtc7iu5ftBZuVRiPLm94

4. Fourth edition with new pool (different SPL token):
https://dev.3.land/item/FmPSPrNw9AoCCvUceHoXHuaFp2nC7GZEqyBsGpWEsMDM

## Additional Notes

- All features have been thoroughly tested and verified working
- The implementation maintains backward compatibility with existing NFT
creation
- Liquidity pool operations are atomic and maintain consistent state
- Pool naming restrictions prevent confusion and potential conflicts

## Checklist
- [X ] I have tested these changes locally
- [ X] I have updated the documentation
- [X ] I have added a transaction link
  • Loading branch information
thearyanag authored Jan 17, 2025
2 parents 975dd79 + 6bbb6d4 commit 6c124ac
Show file tree
Hide file tree
Showing 8 changed files with 420 additions and 64 deletions.
23 changes: 9 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,7 @@ console.log("Token Mint Address:", result.mint.toString());
```
### Create NFT Collection on 3Land
```typescript
const optionsWithBase58: StoreInitOptions = {
privateKey: "",
isMainnet: true, // if false, collection will be created on devnet 3.land (dev.3.land)
};
const isDevnet = true; // (Optional) if not present TX takes place in Mainnet

const collectionOpts: CreateCollectionOptions = {
collectionName: "",
Expand All @@ -147,18 +144,16 @@ const optionsWithBase58: StoreInitOptions = {
};

const result = await agent.create3LandCollection(
optionsWithBase58,
collectionOpts
collectionOpts,
isDevnet, // (Optional) if not present TX takes place in Mainnet
);
```

### Create NFT on 3Land
When creating an NFT using 3Land's tool, it automatically goes for sale on 3.land website
```typescript
const optionsWithBase58: StoreInitOptions = {
privateKey: "",
isMainnet: true, // if false, listing will be on devnet 3.land (dev.3.land)
};
const isDevnet = true; // (Optional) if not present TX takes place in Mainnet
const withPool = true; // (Optional) only present if NFT will be created with a Liquidity Pool for a specific SPL token
const collectionAccount = ""; //hash for the collection
const createItemOptions: CreateSingleOptions = {
itemName: "",
Expand All @@ -170,15 +165,15 @@ const createItemOptions: CreateSingleOptions = {
{ trait_type: "", value: "" },
],
price: 0, //100000000 == 0.1 sol, can be set to 0 for a free mint
splHash: "", //present if listing is on a specific SPL token, if not present sale will be on $SOL, must be present if "withPool" is true
poolName: "", // Only present if "withPool" is true
mainImageUrl: "",
splHash: "", //present if listing is on a specific SPL token, if not present sale will be on $SOL
};
const isMainnet = true;
const result = await agent.create3LandNft(
optionsWithBase58,
collectionAccount,
createItemOptions,
isMainnet
isDevnet, // (Optional) if not present TX takes place in Mainnet
withPool
);

```
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"author": "sendaifun",
"license": "Apache-2.0",
"dependencies": {
"@3land/listings-sdk": "^0.0.4",
"@3land/listings-sdk": "^0.0.6",
"@ai-sdk/openai": "^1.0.11",
"@bonfida/spl-name-service": "^3.0.7",
"@cks-systems/manifest-sdk": "0.1.59",
Expand Down
371 changes: 355 additions & 16 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

27 changes: 23 additions & 4 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,24 +631,43 @@ export class SolanaAgentKit {
}

async create3LandCollection(
optionsWithBase58: StoreInitOptions,
collectionOpts: CreateCollectionOptions,
isDevnet: boolean = false,
): Promise<string> {
let optionsWithBase58: StoreInitOptions = {
privateKey: this.wallet.secretKey,
};
if (isDevnet) {
optionsWithBase58.isMainnet = false;
} else {
optionsWithBase58.isMainnet = true;
}

const tx = await createCollection(optionsWithBase58, collectionOpts);
return `Transaction: ${tx}`;
}

async create3LandNft(
optionsWithBase58: StoreInitOptions,
collectionAccount: string,
createItemOptions: CreateSingleOptions,
isMainnet: boolean,
isDevnet: boolean = false,
withPool: boolean = false,
): Promise<string> {
let optionsWithBase58: StoreInitOptions = {
privateKey: this.wallet.secretKey,
};
if (isDevnet) {
optionsWithBase58.isMainnet = false;
} else {
optionsWithBase58.isMainnet = true;
}

const tx = await createSingle(
optionsWithBase58,
collectionAccount,
createItemOptions,
isMainnet,
!isDevnet,
withPool,
);
return `Transaction: ${tx}`;
}
Expand Down
9 changes: 1 addition & 8 deletions src/langchain/3land/create_collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export class Solana3LandCreateCollection extends Tool {
description = `Creates an NFT Collection that you can visit on 3.land's website (3.land/collection/{collectionAccount})
Inputs:
privateKey (required): represents the privateKey of the wallet - can be an array of numbers, Uint8Array or base58 string
isMainnet (required): defines is the tx takes places in mainnet
collectionSymbol (required): the symbol of the collection
collectionName (required): the name of the collection
Expand All @@ -26,14 +25,8 @@ export class Solana3LandCreateCollection extends Tool {
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const privateKey = inputFormat.privateKey;
const isMainnet = inputFormat.isMainnet;

const optionsWithBase58: StoreInitOptions = {
...(privateKey && { privateKey }),
...(isMainnet && { isMainnet }),
};

const collectionSymbol = inputFormat?.collectionSymbol;
const collectionName = inputFormat?.collectionName;
const collectionDescription = inputFormat?.collectionDescription;
Expand All @@ -49,8 +42,8 @@ export class Solana3LandCreateCollection extends Tool {
};

const tx = await this.solanaKit.create3LandCollection(
optionsWithBase58,
collectionOpts,
!isMainnet,
);
return JSON.stringify({
status: "success",
Expand Down
27 changes: 17 additions & 10 deletions src/langchain/3land/create_single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export class Solana3LandCreateSingle extends Tool {
description = `Creates an NFT and lists it on 3.land's website
Inputs:
privateKey (required): represents the privateKey of the wallet - can be an array of numbers, Uint8Array or base58 string
collectionAccount (optional): represents the account for the nft collection
itemName (required): the name of the NFT
sellerFee (required): the fee of the seller
Expand All @@ -21,7 +20,9 @@ export class Solana3LandCreateSingle extends Tool {
mainImageUrl (required): the main image of the NFT
coverImageUrl (optional): the cover image of the NFT
splHash (optional): the hash of the spl token, if not provided listing will be in $SOL
isMainnet (required): defines is the tx takes places in mainnet
poolName (optional): the name of the pool
isMainnet (required): defines if the tx takes places in mainnet
withPool (optional): defines if minted edition will be tied to a liquidity pool
`;

constructor(private solanaKit: SolanaAgentKit) {
Expand All @@ -31,13 +32,9 @@ export class Solana3LandCreateSingle extends Tool {
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const privateKey = inputFormat.privateKey;
const isMainnet = inputFormat.isMainnet;

const optionsWithBase58: StoreInitOptions = {
...(privateKey && { privateKey }),
...(isMainnet && { isMainnet }),
};
const withPool = inputFormat.withPool;
const poolName = inputFormat.poolName;

const collectionAccount = inputFormat.collectionAccount;

Expand All @@ -52,6 +49,15 @@ export class Solana3LandCreateSingle extends Tool {
const coverImageUrl = inputFormat?.coverImageUrl;
const splHash = inputFormat?.splHash;

if (withPool) {
if (!poolName) {
throw new Error("poolName is required when withPool is true");
}
if (!splHash) {
throw new Error("splHash is required when withPool is true");
}
}

const createItemOptions: CreateSingleOptions = {
...(itemName && { itemName }),
...(sellerFee && { sellerFee }),
Expand All @@ -63,17 +69,18 @@ export class Solana3LandCreateSingle extends Tool {
...(mainImageUrl && { mainImageUrl }),
...(coverImageUrl && { coverImageUrl }),
...(splHash && { splHash }),
...(poolName && { poolName }),
};

if (!collectionAccount) {
throw new Error("Collection account is required");
}

const tx = await this.solanaKit.create3LandNft(
optionsWithBase58,
collectionAccount,
createItemOptions,
isMainnet,
!isMainnet,
withPool,
);
return JSON.stringify({
status: "success",
Expand Down
5 changes: 4 additions & 1 deletion src/tools/3land/create_3land_collectible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export async function createSingle(
optionsWithBase58: StoreInitOptions,
collectionAccount: string,
createItemOptions: CreateSingleOptions,
isMainnet: boolean,
isMainnet: boolean = false,
withPool: boolean = false,
) {
try {
const landStore = isMainnet
Expand All @@ -49,6 +50,8 @@ export async function createSingle(
landStore,
collectionAccount,
createItemOptions,
true, //isAI
withPool,
);
return singleEditionTx;
} catch (error: any) {
Expand Down
20 changes: 10 additions & 10 deletions test/tools/3land.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ const agent = new SolanaAgentKit(
{ OPENAI_API_KEY: process.env.OPENAI_API_KEY! },
);

const optionsWithBase58: StoreInitOptions = {
privateKey: process.env.SOLANA_PRIVATE_KEY!,
isMainnet: false,
};
const isDevnet = true;

/****************************** CREATING COLLECTION ******************************** */

Expand All @@ -29,8 +26,8 @@ const collectionOpts: CreateCollectionOptions = {

(async () => {
const collection = await agent.create3LandCollection(
optionsWithBase58,
collectionOpts,
isDevnet,
);

console.log("collection: ", collection);
Expand All @@ -41,21 +38,24 @@ const collectionAccount = "";
const createItemOptions: CreateSingleOptions = {
itemName: "",
sellerFee: 500, //5%
itemAmount: 100,
itemAmount: 333,
itemSymbol: "",
itemDescription: "",
traits: [{ trait_type: "", value: "" }],
price: 0, //100000000 == 0.1 sol
price: 100000000, //100000000 == 0.1 sol,
splHash: "",
poolName: "",
mainImageUrl: "",
};

const isMainnet = false;
const withPool = true;

(async () => {
const result = agent.create3LandNft(
optionsWithBase58,
collectionAccount,
createItemOptions,
isMainnet,
isDevnet,
withPool,
);
console.log("result: ", result);
})();
Expand Down

0 comments on commit 6c124ac

Please sign in to comment.