Skip to content

change constraints to support unique fungible assets #67

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions clients/js/src/generated/errors/tensorMarketplace.ts
Original file line number Diff line number Diff line change
@@ -110,6 +110,8 @@ export const TENSOR_MARKETPLACE_ERROR__MISSING_CREATOR_A_T_A = 0x1801; // 6145
export const TENSOR_MARKETPLACE_ERROR__MISSING_WHITELIST_METHOD = 0x1802; // 6146
/** EditionDataEmpty: Edition data is empty */
export const TENSOR_MARKETPLACE_ERROR__EDITION_DATA_EMPTY = 0x1803; // 6147
/** InvalidMint: Invalid mint */
export const TENSOR_MARKETPLACE_ERROR__INVALID_MINT = 0x1804; // 6148

export type TensorMarketplaceError =
| typeof TENSOR_MARKETPLACE_ERROR__ARITHMETIC_ERROR
@@ -140,6 +142,7 @@ export type TensorMarketplaceError =
| typeof TENSOR_MARKETPLACE_ERROR__INSUFFICIENT_BALANCE
| typeof TENSOR_MARKETPLACE_ERROR__INSUFFICIENT_REMAINING_ACCOUNTS
| typeof TENSOR_MARKETPLACE_ERROR__INVALID_FEE_ACCOUNT
| typeof TENSOR_MARKETPLACE_ERROR__INVALID_MINT
| typeof TENSOR_MARKETPLACE_ERROR__INVALID_TOKEN_ACCOUNT
| typeof TENSOR_MARKETPLACE_ERROR__LISTING_EXPIRED
| typeof TENSOR_MARKETPLACE_ERROR__LISTING_NOT_YET_EXPIRED
@@ -194,6 +197,7 @@ if (process.env.NODE_ENV !== 'production') {
[TENSOR_MARKETPLACE_ERROR__INSUFFICIENT_BALANCE]: `insufficient balance`,
[TENSOR_MARKETPLACE_ERROR__INSUFFICIENT_REMAINING_ACCOUNTS]: `insufficient remaining accounts`,
[TENSOR_MARKETPLACE_ERROR__INVALID_FEE_ACCOUNT]: `invalid fee account`,
[TENSOR_MARKETPLACE_ERROR__INVALID_MINT]: `Invalid mint`,
[TENSOR_MARKETPLACE_ERROR__INVALID_TOKEN_ACCOUNT]: `invalid token account`,
[TENSOR_MARKETPLACE_ERROR__LISTING_EXPIRED]: `listing has expired`,
[TENSOR_MARKETPLACE_ERROR__LISTING_NOT_YET_EXPIRED]: `listing not yet expired`,
169 changes: 169 additions & 0 deletions clients/js/test/legacy/_common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getSetComputeUnitLimitInstruction } from '@solana-program/compute-budget';
import {
Address,
KeyPairSigner,
airdropFactory,
appendTransactionMessageInstruction,
assertAccountExists,
@@ -9,13 +10,17 @@ import {
pipe,
} from '@solana/web3.js';
import {
Nft,
TokenStandard,
createDefaultNft,
fetchMetadata,
mintNft,
printSupply,
} from '@tensor-foundation/mpl-token-metadata';
import {
Client,
ONE_SOL,
TOKEN_PROGRAM_ID,
createDefaultSolanaClient,
createDefaultTransaction,
createKeyPairSigner,
@@ -198,3 +203,167 @@ export async function setupLegacyTest(
listing: listing ?? undefined,
};
}

export async function setupFungibleAssetTest(
params: SetupTestParams
): Promise<LegacyTest> {
const {
t,
action,
listingPrice = DEFAULT_LISTING_PRICE,
bidPrice = DEFAULT_BID_PRICE,
useCosigner = false,
} = params;

const client = createDefaultSolanaClient();
const signers = await getTestSigners(client);

const { payer, buyer, nftOwner, nftUpdateAuthority, cosigner } = signers;

const standard = TokenStandard.FungibleAsset;

const { mint, metadata, masterEdition } = await mintFungibleAsset({
client,
payer,
nftOwner: nftOwner.address,
nftUpdateAuthority,
});

let listing;
let bid;

switch (action) {
case TestAction.List: {
// List the NFT.
const listLegacyIx = await getListLegacyInstructionAsync({
owner: nftOwner,
mint,
amount: listingPrice,
cosigner: useCosigner ? cosigner : undefined,
tokenStandard: standard,
edition: masterEdition,
});

await pipe(
await createDefaultTransaction(client, nftOwner),
(tx) => appendTransactionMessageInstruction(computeIx, tx),
(tx) => appendTransactionMessageInstruction(listLegacyIx, tx),
(tx) => signAndSendTransaction(client, tx)
);

// Listing was created.
[listing] = await findListStatePda({
mint,
});
assertAccountExists(await fetchEncodedAccount(client.rpc, listing));

// NFT is now escrowed in the listing.
await assertTokenNftOwnedBy({ t, client, mint, owner: listing });
break;
}
case TestAction.Bid: {
// Bid on the NFT.
const bidIx = await getBidInstructionAsync({
owner: buyer,
amount: bidPrice,
target: Target.AssetId,
targetId: mint,
cosigner: useCosigner ? cosigner : undefined,
});

[bid] = await findBidStatePda({
owner: buyer.address,
bidId: mint,
});

await pipe(
await createDefaultTransaction(client, signers.buyer),
(tx) => appendTransactionMessageInstruction(computeIx, tx),
(tx) => appendTransactionMessageInstruction(bidIx, tx),
(tx) => signAndSendTransaction(client, tx)
);

const bidState = await fetchBidStateFromSeeds(client.rpc, {
owner: buyer.address,
bidId: mint,
});
t.like(bidState, {
data: {
owner: buyer.address,
amount: 1n,
target: Target.AssetId,
targetId: mint,
cosigner: null,
},
});
break;
}
default:
throw new Error(`Unknown action: ${action}`);
}

const md = (await fetchMetadata(client.rpc, metadata)).data;
const { sellerFeeBasisPoints } = md;

// Calculate the max or min price from the price +/- royalties.
const price = listingPrice
? listingPrice! +
(listingPrice! * BigInt(sellerFeeBasisPoints)) / BASIS_POINTS
: bidPrice! - (bidPrice! * BigInt(sellerFeeBasisPoints)) / BASIS_POINTS;

return {
client,
signers,
mint,
bid: bid ?? undefined,
price,
bidPrice: bidPrice ?? undefined,
listingPrice: listingPrice ?? undefined,
listing: listing ?? undefined,
};
}

export async function mintFungibleAsset({
client,
payer,
nftOwner,
nftUpdateAuthority,
decimals = 0,
}: {
client: Client;
payer: KeyPairSigner;
nftOwner: Address;
nftUpdateAuthority: KeyPairSigner;
decimals?: number;
}): Promise<Nft> {
const standard = TokenStandard.FungibleAsset;

// Mint an NFT.
const data = {
name: 'Example NFT',
symbol: 'EXNFT',
uri: 'https://example.com/nft',
sellerFeeBasisPoints: 500,
creators: [
{
address: nftUpdateAuthority.address,
verified: true,
share: 100,
},
],
printSupply: printSupply('Zero'),
tokenStandard: standard,
collection: undefined,
ruleSet: undefined,
decimals,
};

const accounts = {
authority: nftUpdateAuthority,
owner: nftOwner,
payer,
tokenProgram: TOKEN_PROGRAM_ID,
};

return mintNft(client, accounts, data);
}
53 changes: 52 additions & 1 deletion clients/js/test/legacy/buy.test.ts
Original file line number Diff line number Diff line change
@@ -43,7 +43,11 @@ import {
TAKER_FEE_BPS,
TestAction,
} from '../_common.js';
import { computeIx, setupLegacyTest } from './_common.js';
import {
computeIx,
setupFungibleAssetTest,
setupLegacyTest,
} from './_common.js';

test('it can buy an NFT', async (t) => {
const {
@@ -177,6 +181,53 @@ test('it cannot buy a Programmable NFT with a lower amount', async (t) => {
}
});

test('it can list and buy a Fungible Asset w/ supply of 1', async (t) => {
// Fungible Assets technically allow supply > 1, but require decimals of 0.
// However, we are allowing Fungible Assets used as NFTs such as SNS NFTs which have a supply of 1.
const {
client,
signers,
mint,
price: maxPrice,
listing,
} = await setupFungibleAssetTest({
t,
action: TestAction.List,
});
const { buyer, nftOwner, nftUpdateAuthority } = signers;

// When a buyer buys the NFT.
const buyLegacyIx = await getBuyLegacyInstructionAsync({
owner: nftOwner.address,
payer: buyer,
mint,
maxAmount: maxPrice,
creators: [nftUpdateAuthority.address],
});

await pipe(
await createDefaultTransaction(client, buyer),
(tx) => appendTransactionMessageInstruction(buyLegacyIx, tx),
(tx) => signAndSendTransaction(client, tx)
);

// Then the listing account should have been closed.
t.false((await fetchEncodedAccount(client.rpc, listing!)).exists);

// And the listing token account should have been closed.
t.false(
(
await fetchEncodedAccount(
client.rpc,
(await findAtaPda({ mint, owner: listing! }))[0]
)
).exists
);

// And the buyer has the NFT.
await assertTokenNftOwnedBy({ t, client, mint, owner: buyer.address });
});

test('it can buy an NFT with a cosigner', async (t) => {
const {
client,
Loading