Skip to content

Commit

Permalink
Dynamic NFT support (#479)
Browse files Browse the repository at this point in the history
  • Loading branch information
tifrel authored Mar 7, 2024
1 parent b5efd83 commit ffbd4ba
Show file tree
Hide file tree
Showing 25 changed files with 440 additions and 45 deletions.
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

0 comments on commit ffbd4ba

Please sign in to comment.