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

Dynamic NFT support #479

Merged
merged 11 commits into from
Mar 7, 2024
8 changes: 4 additions & 4 deletions packages/sdk/src/batchChangeCreators/batchChangeCreators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ export const batchChangeCreators = (
args: BatchChangeCreatorsArgs,
): NearContractCall<BatchChangeMintersArgsResponse> => {
const { addCreators = [], removeCreators = [], contractAddress = mbjs.keys.contractAddress } = args;

if (!contractAddress) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

if (!isStoreV2(contractAddress)) {
throw new Error(ERROR_MESSAGES.ONLY_V2);
}

if (contractAddress == null) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

if (addCreators.length === 0 && removeCreators.length === 0) {
throw new Error(ERROR_MESSAGES.BATCH_CHANGE_CREATORS_NO_CHANGE);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/sdk/src/batchChangeMinters/batchChangeMinters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ export const batchChangeMinters = (
args: BatchChangeMintersArgs,
): NearContractCall<BatchChangeMintersArgsResponse> => {
const { addMinters = [], removeMinters = [], contractAddress = mbjs.keys.contractAddress } = args;

if (!contractAddress) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

if (!isStoreV1(contractAddress)) {
throw new Error(ERROR_MESSAGES.ONLY_V1);
}

if (contractAddress == null) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

if (addMinters.length === 0 && removeMinters.length === 0) {
throw new Error(ERROR_MESSAGES.BATCH_CHANGE_MINTERS_NO_CHANGE);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/burn/burn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ERROR_MESSAGES } from '../errorMessages';
*/
export const burn = ({ tokenIds, contractAddress = mbjs.keys.contractAddress }: BurnArgs): NearContractCall<BurnArgsResponse> => {

if (contractAddress == null) {
if (!contractAddress) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/buy/buy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { BuyArgs, BuyArgsFtResponse, BuyArgsResponse, FT_METHOD_NAMES, MARKET_ME
export const buy = (args: BuyArgs): NearContractCall<BuyArgsResponse | BuyArgsFtResponse> => {
const { contractAddress = mbjs.keys.contractAddress, tokenId, referrerId = null, marketId = mbjs.keys.marketAddress, price, affiliateAccount } = args;

if (contractAddress == null) {
if (!contractAddress) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/config/config.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const TESTNET_MOCK = {
network: NEAR_NETWORKS.TESTNET,
connectProxyAddress: null,
ftAddresses: { usdc: USDC_ADDRESS.testnet, usdt: USDT_ADDRESS.testnet },
checkVersions: true,
};


Expand All @@ -31,4 +32,5 @@ export const MAINNET_MOCK = {
network: NEAR_NETWORKS.MAINNET,
connectProxyAddress: null,
ftAddresses: { usdc: USDC_ADDRESS.mainnet, usdt: USDT_ADDRESS.mainnet },
checkVersions: true,
};
2 changes: 2 additions & 0 deletions packages/sdk/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const startupConfig: MbJsKeysObject = {
debugMode: isDebugMode ? true : false,
ftAddresses: isProcessEnv ? FT_ADDRESSES[process.env.NEAR_NETWORK] : FT_ADDRESSES[NEAR_NETWORKS.MAINNET],
isSet: isProcessEnv ? true : false,
checkVersions: true,
};

// config is scoped globally as to avoid version mismatches from conflicting
Expand All @@ -64,6 +65,7 @@ export const setGlobalEnv = (configObj: ConfigOptions): MbJsKeysObject => {
connectProxyAddress: null,
ftAddresses: FT_ADDRESSES[configObj.network],
isSet: true,
checkVersions: true,
};

config.network = globalConfig.network;
Expand Down
6 changes: 4 additions & 2 deletions packages/sdk/src/createMetadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The reference material is typically uploaded to IPFS or Arweave and can be easil

Royalties can be configured to provide a customized flow of funds as explained below.

You can restrict minting via an allowlist of NEAR account IDs that are allowed to mint (`minter_allowslist`), via a maximum supply that will be enforced by the smart contract (`max_supply`), and via an expiry date (`last_possible_mint`).
You can restrict minting via an allowlist of NEAR account IDs that are allowed to mint (`mintersAllowslist`), via a maximum supply that will be enforced by the smart contract (`maxSupply`), and via an expiry date (`lastPossibleMint`). You can opt-in to making an NFT dynamic (`isDynamic`) to allow [future updates](../updateMetadata/README.md), and [lock the metadata](../lockMetadata/README.md) at a later time.

The `nftContactId` can be supplied as an argument or through the `TOKEN_CONTRACT` environment variable.

Expand Down Expand Up @@ -44,6 +44,8 @@ export type CreateMetadataArgs = {
noMedia?: boolean;
// explicit opt-in to NFT without reference
noReference?: boolean;
// explicit opt-in to make the NFT dynamic and allow future updates
isDynamic?: boolean;
};

export type TokenMetadata = {
Expand All @@ -65,7 +67,7 @@ export type TokenMetadata = {
## React example

Example usage of mint method in a hypothetical React component:
{% code title="MintComponent.ts" overflow="wrap" lineNumbers="true" %}
{% code title="CreateMetadataComponent.ts" overflow="wrap" lineNumbers="true" %}

```typescript
import { useState } from 'react';
Expand Down
47 changes: 41 additions & 6 deletions packages/sdk/src/createMetadata/createMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ describe('createMetadata method tests', () => {
minters_allowlist: null,
max_supply: null,
last_possible_mint: null,
is_dynamic: null,
},
deposit: '2270000000000000000000',
deposit: '2950000000000000000000',
gas: GAS,
});
});
Expand All @@ -59,8 +60,9 @@ describe('createMetadata method tests', () => {
minters_allowlist: null,
max_supply: null,
last_possible_mint: null,
is_dynamic: null,
},
deposit: '2270000000000000000000',
deposit: '2950000000000000000000',
gas: GAS,
});
});
Expand All @@ -87,8 +89,9 @@ describe('createMetadata method tests', () => {
minters_allowlist: ['foo', 'bar'],
max_supply: null,
last_possible_mint: null,
is_dynamic: null,
},
deposit: '2270000000000000000000',
deposit: '4550000000000000000000',
gas: GAS,
});
});
Expand All @@ -115,8 +118,9 @@ describe('createMetadata method tests', () => {
minters_allowlist: null,
max_supply: 10,
last_possible_mint: null,
is_dynamic: null,
},
deposit: '2270000000000000000000',
deposit: '2950000000000000000000',
gas: GAS,
});
});
Expand All @@ -143,8 +147,38 @@ describe('createMetadata method tests', () => {
minters_allowlist: null,
max_supply: null,
last_possible_mint: '1640995200000000000',
is_dynamic: null,
},
deposit: '2270000000000000000000',
deposit: '2950000000000000000000',
gas: GAS,
});
});

test('createMetadata which for dynamic NFTs', () => {
const args = createMetadata({
contractAddress: contractAddress,
metadata: { reference, media },
isDynamic: true,
price: 1,
});

expect(args).toEqual({
contractAddress: contractAddress,
methodName: TOKEN_METHOD_NAMES.CREATE_METADATA,
args: {
metadata: {
reference: reference,
media: media,
},
price: `1${'0'.repeat(24)}`,
metadata_id: null,
royalty_args: null,
minters_allowlist: null,
max_supply: null,
is_dynamic: true,
last_possible_mint: null,
},
deposit: '2950000000000000000000',
gas: GAS,
});
});
Expand Down Expand Up @@ -178,8 +212,9 @@ describe('createMetadata method tests', () => {
minters_allowlist: null,
max_supply: null,
last_possible_mint: null,
is_dynamic: null,
},
deposit: '4670000000000000000000',
deposit: '5350000000000000000000',
gas: GAS,
});
});
Expand Down
20 changes: 13 additions & 7 deletions packages/sdk/src/createMetadata/createMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,19 @@ export const createMetadata = (
mintersAllowlist = null,
maxSupply = null,
lastPossibleMint = null,
isDynamic = null,
noMedia = false,
noReference = false,
} = args;

if (!contractAddress) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

if (!isStoreV2(contractAddress)) {
throw new Error(ERROR_MESSAGES.ONLY_V2);
}

if (contractAddress == null) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

// Reference and media need to be present or explicitly opted out of
if (!noReference && !metadata.reference) {
throw new Error(ERROR_MESSAGES.NO_REFERENCE);
Expand All @@ -58,30 +59,35 @@ export const createMetadata = (
minters_allowlist: mintersAllowlist,
max_supply: maxSupply,
last_possible_mint: lastPossibleMint ? (+lastPossibleMint * 1e6).toString() : null,
is_dynamic: isDynamic,
price: new BN(price * 1e6).mul(new BN(`1${'0'.repeat(18)}`)).toString(),
},
methodName: TOKEN_METHOD_NAMES.CREATE_METADATA,
gas: GAS,
deposit: createMetadataDeposit({
nRoyalties: !royalties ? 0 : Object.keys(royalties)?.length,
nMinters: !mintersAllowlist? 0 : mintersAllowlist.length,
metadata,
}),
};
};

export function createMetadataDeposit({
nRoyalties,
nMinters,
metadata,
}: {
nRoyalties: number;
nMinters: number;
metadata: TokenMetadata;
}): string {
const metadataBytesEstimate = JSON.stringify(metadata).length;

const totalBytes = STORAGE_BYTES.MINTING_BASE +
// storage + nRoyalties * common + nMinters * common + 2 * common
const totalBytes = 2 * STORAGE_BYTES.COMMON +
STORAGE_BYTES.MINTING_FEE +
metadataBytesEstimate +
STORAGE_BYTES.COMMON * nRoyalties;
STORAGE_BYTES.COMMON * nRoyalties +
STORAGE_BYTES.COMMON * nMinters;

return `${Math.ceil(totalBytes)}${'0'.repeat(STORAGE_PRICE_PER_BYTE_EXPONENT)}`;
}
3 changes: 1 addition & 2 deletions packages/sdk/src/delist/delist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ export const delist = (
args: DelistArgs,
): NearContractCall<DelistArgsResponse> => {
const { contractAddress = mbjs.keys.contractAddress, tokenIds, marketAddress = mbjs.keys.marketAddress, oldMarket = false } = args;


if (contractAddress == null) {
if (!contractAddress) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export * from './burn/burn';
export * from './constants';
export * from './createMetadata/createMetadata';
export * from './mintOnMetadata/mintOnMetadata';
export * from './updateMetadata/updateMetadata';
export * from './lockMetadata/lockMetadata';
export * from './execute/execute';
export * from './execute/execute.utils';
export * from './transfer/transfer';
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/list/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ListArgs, ListArgsResponse, MARKET_METHOD_NAMES, NearContractCall } fro
export const list = (args: ListArgs): NearContractCall<ListArgsResponse> => {
const { contractAddress = mbjs.keys.contractAddress, tokenId, marketAddress = mbjs.keys.marketAddress, price } = args;

if (contractAddress == null) {
if (!contractAddress) {
throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS);
}

Expand Down
60 changes: 60 additions & 0 deletions packages/sdk/src/lockMetadata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[//]: # `{ "title": "lockMetadata", "order": 0.21 }`

# Lock Metadata (v2)

Lock [previously created metadata](../createMetadata/README.md). This is only possible if the metadata has been marked as dynamic while creating it, and will prevent any further updates. This operation is irrevertible. Only the creator of the metadata is allowed to lock it.

The `nftContactId` can be supplied as an argument or through the `TOKEN_CONTRACT` environment variable.

This only works on v2 smart contracts and no equivalent feature exists for v1.

**As with all new SDK api methods, this call should be wrapped in [execute](../#execute) and passed a signing method. For a guide showing how to make a contract call with mintbase-js click [here](https://docs.mintbase.xyz/dev/getting-started/make-your-first-contract-call-deploycontract)**

## lockMetadata(args: UpdateMetadataArgs): NearContractCall

`lockMetadata` takes a single argument of type `LockMetadataArgs`

```typescript
export type UpdateMetadataArgs = {
//the contractId from which you want to mint, this can be statically defined via the mbjs config file
contractAddress?: string;
//the ID of the metadata you wish to lock
metadataId: string;
};
```

## React example

Example usage of mint method in a hypothetical React component:
{% code title="LockMetadataComponent.ts" overflow="wrap" lineNumbers="true" %}

```typescript
import { useState } from 'react';
import { useWallet } from '@mintbase-js/react';
import { execute, lockMetadata, LockMetadataArgs } from '@mintbase-js/sdk';


export const LockMetadataComponent = ({ contractAddress, metadataId }: LockMetadataArgs): JSX.Element => {

const { selector } = useWallet();

const handleLockMetadata = async (): Promise<void> => {

const wallet = await selector.wallet();

await execute(
lockMetadata({ contractAddress, metadataId })
);

}

return (
<div>
<button onClick={handleLockMetadata}>
Lock metadata
</button>
</div>
);
};
```
{% endcode %}
27 changes: 27 additions & 0 deletions packages/sdk/src/lockMetadata/lockMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { GAS } from '../constants';
import { TOKEN_METHOD_NAMES } from '../types';
import { lockMetadata } from './lockMetadata';
import { mbjs } from '../config/config';

describe('updateMetadata method tests', () => {
const contractAddress = `test.${mbjs.keys.mbContractV2}`;
// const contractAddressV2 = 'test.mintbase2.near';
const ownerId = 'test';

test('updateMetadata without options', () => {
const args = lockMetadata({
contractAddress: contractAddress,
metadataId: '1',
});

expect(args).toEqual({
contractAddress: contractAddress,
methodName: TOKEN_METHOD_NAMES.LOCK_METADATA,
args: {
metadata_id: '1',
},
deposit: '1',
gas: GAS,
});
});
});
Loading