diff --git a/package.json b/package.json
index cdf113cb..188e4583 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
"@taquito/taquito": "9.0.0",
"@taquito/tzip12": "9.0.0",
"@taquito/tzip16": "9.0.0",
- "@tqtezos/minter-contracts": "1.2.0",
+ "@tqtezos/minter-contracts": "1.3.0",
"@types/lodash": "4.14.165",
"@types/react": "16.9.12",
"@types/react-dom": "16.9.0",
diff --git a/src/components/Collections/TokenDetail/index.tsx b/src/components/Collections/TokenDetail/index.tsx
index 95ef413f..161af931 100644
--- a/src/components/Collections/TokenDetail/index.tsx
+++ b/src/components/Collections/TokenDetail/index.tsx
@@ -30,8 +30,8 @@ import {
getNftAssetContractQuery
} from '../../../reducer/async/queries';
import { TokenMedia } from '../../common/TokenMedia';
-import lk from '../../common/assets/link-icon.svg'
-import tz from '../../common/assets/tezos-sym.svg'
+import lk from '../../common/assets/link-icon.svg';
+import tz from '../../common/assets/tezos-sym.svg';
import { Maximize2 } from 'react-feather';
function NotFound() {
@@ -168,8 +168,8 @@ function TokenDetail({ contractAddress, tokenId }: TokenDetailProps) {
pt={[10, 0]}
pb={[5, 0]}
width={['100%']}
- maxHeight={["30vh", "60vh", "70vh"]}
- height={["100%"]}
+ maxHeight={['30vh', '60vh', '70vh']}
+ height={['100%']}
justifyContent="center"
>
-
+
-
-
+
-
+
{token.title}
Minter:
-
- {token?.metadata?.minter}
+
+
+ {token?.metadata?.minter}
+
+
+
+
Collection:
-
- {state.selectedCollection
- ? state.collections[state.selectedCollection]?.metadata.name : collection?.metadata.name ? collection?.metadata.name : contractAddress }
+
+
+ {state.selectedCollection
+ ? state.collections[state.selectedCollection]
+ ?.metadata.name
+ : collection?.metadata.name
+ ? collection?.metadata.name
+ : contractAddress}
+
+
+
+
+
+
+
+
+ Amount:
+
+ {token?.amount || 1}
{token?.metadata?.attributes?.map(({ name, value }) => (
{name}:
-
+
{value}
@@ -250,13 +343,38 @@ function TokenDetail({ contractAddress, tokenId }: TokenDetailProps) {
-
-
+
+
{token.sale ? (
isOwner ? (
<>
-
- {token.sale.price}
+
+ {token.sale.price}{' '}
+
) : (
<>
-
- {token.sale.price.toFixed(2)}
+
+ {token.sale.price.toFixed(2)}{' '}
+
-
+
>
)
) : isOwner ? (
-
+
) : (
<>>
@@ -319,7 +456,7 @@ function TokenDetail({ contractAddress, tokenId }: TokenDetailProps) {
-
+
);
}
diff --git a/src/components/CreateNonFungiblePage/Form.tsx b/src/components/CreateNonFungiblePage/Form.tsx
index ae894002..eb6f0cd5 100644
--- a/src/components/CreateNonFungiblePage/Form.tsx
+++ b/src/components/CreateNonFungiblePage/Form.tsx
@@ -17,7 +17,9 @@ import { useSelector, useDispatch } from '../../reducer';
import {
addMetadataRow,
deleteMetadataRow,
- updateField,
+ updateName,
+ updateDescription,
+ updateAmount,
updateMetadataRowName,
updateMetadataRowValue
} from '../../reducer/slices/createNft';
@@ -28,8 +30,12 @@ const DESCRIPTION_PLACEHOLDER =
export default function Form() {
const state = useSelector(s => s.createNft);
+ const collections = useSelector(s => s.collections.collections);
+ const collection = state.collectionAddress
+ ? collections[state.collectionAddress] || null
+ : null;
const dispatch = useDispatch();
- const { name, description } = state.fields;
+ const { name, description, amount } = state.fields;
return (
<>
@@ -42,11 +48,25 @@ export default function Form() {
autoFocus={true}
placeholder="Input your asset name"
value={name || ''}
- onChange={e =>
- dispatch(updateField({ name: 'name', value: e.target.value }))
- }
+ onChange={e => dispatch(updateName(e.target.value))}
/>
+ {collection?.fungible ? (
+
+ Number of editions
+ {
+ const amount = parseInt(e.target.value, 10);
+ if (!isNaN(amount) && amount > 0 && Number.isInteger(amount)) {
+ dispatch(updateAmount(amount));
+ }
+ }}
+ />
+
+ ) : null}
Description
@@ -59,11 +79,7 @@ export default function Form() {
fontFamily="mono"
placeholder={DESCRIPTION_PLACEHOLDER}
value={description || ''}
- onChange={e =>
- dispatch(
- updateField({ name: 'description', value: e.target.value })
- )
- }
+ onChange={e => dispatch(updateDescription(e.target.value))}
/>
diff --git a/src/lib/nfts/actions.ts b/src/lib/nfts/actions.ts
index 1b634896..ba85d51d 100644
--- a/src/lib/nfts/actions.ts
+++ b/src/lib/nfts/actions.ts
@@ -1,12 +1,13 @@
-import { MichelsonMap } from '@taquito/taquito';
+import { TezosToolkit, MichelsonMap } from '@taquito/taquito';
import {
- Fa2MultiNftAssetCode,
+ Fa2MultiFtAssetCode,
Fa2MultiNftFaucetCode
} from '@tqtezos/minter-contracts';
import { Buffer } from 'buffer';
import { SystemWithWallet } from '../system';
import { uploadIPFSJSON } from '../util/ipfs';
import { NftMetadata } from './decoders';
+import { getTokenMetadataBigMap } from './queries';
function toHexString(input: string) {
return Buffer.from(input).toString('hex');
@@ -26,7 +27,7 @@ export async function createFaucetContract(
metadataMap.set('', toHexString(resp.data.ipfsUri));
return await system.toolkit.wallet
.originate({
- code: Fa2MultiNftFaucetCode.code,
+ code: [Fa2MultiNftFaucetCode.code],
storage: {
assets: {
ledger: new MichelsonMap(),
@@ -54,13 +55,14 @@ export async function createAssetContract(
metadataMap.set('', toHexString(resp.data.ipfsUri));
return await system.toolkit.wallet
.originate({
- code: Fa2MultiNftAssetCode.code,
+ code: Fa2MultiFtAssetCode.code as any,
storage: {
assets: {
ledger: new MichelsonMap(),
next_token_id: 0,
operators: new MichelsonMap(),
- token_metadata: new MichelsonMap()
+ token_metadata: new MichelsonMap(),
+ token_total_supply: new MichelsonMap()
},
admin: {
admin: system.tzPublicKey,
@@ -76,12 +78,12 @@ export async function createAssetContract(
export async function mintToken(
system: SystemWithWallet,
address: string,
- metadata: NftMetadata
+ metadata: NftMetadata,
+ amount = 1
) {
const contract = await system.toolkit.wallet.at(address);
const storage = await contract.storage();
- const token_id = storage.assets.next_token_id;
const token_info = new MichelsonMap();
const resp = await uploadIPFSJSON(system.config.ipfsApi, {
...metadata,
@@ -90,17 +92,48 @@ export async function mintToken(
});
token_info.set('', toHexString(resp.data.ipfsUri));
- return contract.methods
- .mint([
- {
- owner: system.tzPublicKey,
- token_metadata: {
- token_id,
- token_info
+ if (contract.methods.mint) {
+ const token_id = storage.assets.next_token_id;
+ return contract.methods
+ .mint([
+ {
+ owner: system.tzPublicKey,
+ token_metadata: {
+ token_id,
+ token_info
+ }
}
- }
- ])
- .send();
+ ])
+ .send();
+ }
+
+ if (contract.methods.create_token) {
+ const metadata = await getTokenMetadataBigMap(system.tzkt, address);
+ const last_id = metadata
+ .map(row => parseInt(row.key, 10))
+ .sort()
+ .slice(-1)[0];
+ const token_id = last_id ? last_id + 1 : 0;
+ const tz = new TezosToolkit(system.config.rpc);
+ tz.setWalletProvider(system.wallet);
+ return tz.wallet
+ .batch()
+ .withContractCall(contract.methods.create_token(token_id, token_info))
+ .withContractCall(
+ contract.methods.mint_tokens([
+ {
+ token_id: token_id,
+ owner: system.tzPublicKey,
+ amount
+ }
+ ])
+ )
+ .send();
+ }
+
+ throw Error(
+ `Cannot mint: no "mint" or "create_token" method found in ${address}`
+ );
}
export async function transferToken(
diff --git a/src/lib/nfts/decoders.ts b/src/lib/nfts/decoders.ts
index 5a01db9b..a8501dda 100644
--- a/src/lib/nfts/decoders.ts
+++ b/src/lib/nfts/decoders.ts
@@ -69,9 +69,25 @@ export const AssetMetadataBigMap = t.array(
BigMapRow({ key: t.string, value: t.string })
);
+export type NftLedgerBigMap = t.TypeOf;
+export const NftLedgerBigMap = t.array(
+ BigMapRow({ key: t.string, value: t.string })
+);
+
+export type FtLedgerBigMap = t.TypeOf;
+export const FtLedgerBigMap = t.array(
+ BigMapRow({
+ key: t.type({ nat: t.string, address: t.string }),
+ value: t.string
+ })
+);
+
export type LedgerBigMap = t.TypeOf;
export const LedgerBigMap = t.array(
- BigMapRow({ key: t.string, value: t.string })
+ BigMapRow({
+ key: t.string,
+ value: t.type({ owner: t.string, amount: t.string })
+ })
);
export type TokenMetadataBigMap = t.TypeOf;
@@ -238,7 +254,8 @@ export const Nft = t.intersection([
}),
t.partial({
sale: NftSale,
- address: t.string
+ address: t.string,
+ amount: t.string
})
]);
@@ -252,6 +269,13 @@ export type AssetContract = t.TypeOf;
export const AssetContract = t.intersection([
ContractRow(t.unknown),
t.type({
- metadata: AssetContractMetadata
+ metadata: AssetContractMetadata,
+ fungible: t.boolean
})
]);
+
+//// Contract Entrypoints
+
+export const ContractEntrypoints = t.array(
+ t.type({ name: t.string, jsonParameters: t.unknown, unused: t.boolean })
+);
diff --git a/src/lib/nfts/queries.ts b/src/lib/nfts/queries.ts
index 9adc56a7..7955c080 100644
--- a/src/lib/nfts/queries.ts
+++ b/src/lib/nfts/queries.ts
@@ -24,7 +24,7 @@ async function getAssetMetadataBigMap(
): Promise {
const path = 'metadata';
const data = await tzkt.getContractBigMapKeys(address, path);
- const decoded = D.LedgerBigMap.decode(data);
+ const decoded = D.AssetMetadataBigMap.decode(data);
if (isLeft(decoded)) {
throw Error('Failed to decode `getAssetMetadata` response');
}
@@ -37,14 +37,29 @@ async function getLedgerBigMap(
): Promise {
const path = 'assets.ledger';
const data = await tzkt.getContractBigMapKeys(address, path);
- const decoded = D.LedgerBigMap.decode(data);
- if (isLeft(decoded)) {
- throw Error('Failed to decode `getLedger` response');
+ if (D.NftLedgerBigMap.is(data)) {
+ return data.map(row => ({
+ ...row,
+ value: {
+ owner: row.value,
+ amount: '1'
+ }
+ }));
}
- return decoded.right;
+ if (D.FtLedgerBigMap.is(data)) {
+ return data.map(row => ({
+ ...row,
+ key: row.key.nat,
+ value: {
+ owner: row.key.address,
+ amount: row.value
+ }
+ }));
+ }
+ throw Error('Failed to decode `getLedger` response');
}
-async function getTokenMetadataBigMap(
+export async function getTokenMetadataBigMap(
tzkt: TzKt,
address: string
): Promise {
@@ -118,6 +133,19 @@ async function getContract(
return decoded.right;
}
+async function getCreateEntrypoint(
+ tzkt: TzKt,
+ address: string,
+ params: Params,
+ entries: string[]
+) {
+ const entrypoints = await tzkt.getContractEntrypoints(address, params);
+ if (D.ContractEntrypoints.is(entrypoints)) {
+ return entrypoints.find(row => entries.includes(row.name))?.name;
+ }
+ throw Error('Failed to decode `getCreateEntrypoint` response');
+}
+
//// Main query functions
export async function getContractNfts(
@@ -159,7 +187,8 @@ export async function getContractNfts(
return {
id: parseInt(tokenId, 10),
- owner: ledger.find(e => e.key === tokenId)?.value!,
+ owner: ledger.find(e => e.key === tokenId)?.value.owner!,
+ amount: ledger.find(e => e.key === tokenId)?.value.amount!,
title: metadata.name,
description: metadata.description,
artifactUri: metadata.artifactUri,
@@ -191,7 +220,19 @@ export async function getNftAssetContract(
if (isLeft(decoded)) {
throw Error('Metadata validation failed');
}
- return { ...contract, metadata: decoded.right };
+ const createEntry = await getCreateEntrypoint(system.tzkt, address, {}, [
+ 'mint',
+ 'create_token'
+ ]);
+
+ if (!createEntry) {
+ throw Error('Could not find `mint` or `create_token` entrypoints');
+ }
+ return {
+ ...contract,
+ metadata: decoded.right,
+ fungible: createEntry === 'create_token'
+ };
}
export async function getWalletNftAssetContracts(
@@ -240,6 +281,21 @@ export async function getWalletNftAssetContracts(
continue;
}
try {
+ const entrypoints = await system.tzkt.getContractEntrypoints(
+ row.contract.address
+ );
+ if (!D.ContractEntrypoints.is(entrypoints)) {
+ continue;
+ }
+ const createEntry = await getCreateEntrypoint(
+ system.tzkt,
+ row.contract.address,
+ {},
+ ['mint', 'create_token']
+ );
+ if (!createEntry) {
+ continue;
+ }
const metaUri = row.content.value;
const { metadata } = await system.resolveMetadata(
fromHexString(metaUri),
@@ -247,7 +303,11 @@ export async function getWalletNftAssetContracts(
);
const decoded = D.AssetContractMetadata.decode(metadata);
if (!isLeft(decoded)) {
- results.push({ ...contract, metadata: decoded.right });
+ results.push({
+ ...contract,
+ metadata: decoded.right,
+ fungible: createEntry === 'create_token'
+ });
}
} catch (e) {
console.log(e);
diff --git a/src/lib/service/tzkt.ts b/src/lib/service/tzkt.ts
index 55e1f699..ba9444c0 100644
--- a/src/lib/service/tzkt.ts
+++ b/src/lib/service/tzkt.ts
@@ -69,6 +69,18 @@ export async function getContractStorage(
return response.data;
}
+export async function getContractEntrypoints(
+ config: Config,
+ address: string,
+ params?: Params
+) {
+ const uri = `${
+ config.tzkt.api
+ }/v1/contracts/${address}/entrypoints?${mkQueryParams(params)}`;
+ const response = await axios.get(uri);
+ return response.data;
+}
+
export class TzKt {
config: Config;
@@ -99,4 +111,8 @@ export class TzKt {
getContractStorage(address: string, params?: Params) {
return getContractStorage(this.config, address, params);
}
+
+ getContractEntrypoints(address: string, params?: Params) {
+ return getContractEntrypoints(this.config, address, params);
+ }
}
diff --git a/src/reducer/async/actions.ts b/src/reducer/async/actions.ts
index abb6382a..7057e8d3 100644
--- a/src/reducer/async/actions.ts
+++ b/src/reducer/async/actions.ts
@@ -61,6 +61,7 @@ export const readFileAsDataUrlAction = createAsyncThunk<
try {
return await readFile;
} catch (e) {
+ console.error(e);
return rejectWithValue({
kind: ErrorKind.UknownError,
message: 'Could not read file'
@@ -95,6 +96,7 @@ export const createAssetContractAction = createAsyncThunk<
dispatch(getWalletAssetContractsQuery());
return { name, address };
} catch (e) {
+ console.error(e);
return rejectWithValue({
kind: ErrorKind.CreateAssetContractFailed,
message: 'Collection creation failed'
@@ -159,6 +161,7 @@ export const mintTokenAction = createAsyncThunk<
const blob = await fetched.blob();
file = new File([blob], name, { type });
} catch (e) {
+ console.log(e);
return rejectWithValue({
kind: ErrorKind.UknownError,
message: 'Could not mint token: selected file not found'
@@ -195,6 +198,7 @@ export const mintTokenAction = createAsyncThunk<
const blob = await fetched.blob();
displayFile = new File([blob], name, { type });
} catch (e) {
+ console.log(e);
return rejectWithValue({
kind: ErrorKind.UknownError,
message: 'Could not mint token: video display file not found'
@@ -225,6 +229,7 @@ export const mintTokenAction = createAsyncThunk<
];
}
} catch (e) {
+ console.log(e);
return rejectWithValue({
kind: ErrorKind.IPFSUploadFailed,
message: 'IPFS upload failed'
@@ -245,6 +250,7 @@ export const mintTokenAction = createAsyncThunk<
dispatch(getContractNftsQuery(address));
return { contract: address, metadata };
} catch (e) {
+ console.error(e);
return rejectWithValue({
kind: ErrorKind.MintTokenFailed,
message: 'Mint token failed'
@@ -276,6 +282,7 @@ export const transferTokenAction = createAsyncThunk<
dispatch(getContractNftsQuery(contract));
return args;
} catch (e) {
+ console.error(e);
return rejectWithValue({
kind: ErrorKind.TransferTokenFailed,
message: 'Transfer token failed'
@@ -318,6 +325,7 @@ export const listTokenAction = createAsyncThunk<
dispatch(getContractNftsQuery(contract));
return args;
} catch (e) {
+ console.error(e);
return rejectWithValue({
kind: ErrorKind.ListTokenFailed,
message: 'List token failed'
@@ -355,6 +363,7 @@ export const cancelTokenSaleAction = createAsyncThunk<
dispatch(getContractNftsQuery(contract));
return { contract: contract, tokenId: tokenId };
} catch (e) {
+ console.error(e);
return rejectWithValue({
kind: ErrorKind.CancelTokenSaleFailed,
message: 'Cancel token sale failed'
@@ -400,6 +409,7 @@ export const buyTokenAction = createAsyncThunk<
dispatch(getContractNftsQuery(contract));
return { contract: contract, tokenId: tokenId };
} catch (e) {
+ console.error(e);
return rejectWithValue({
kind: ErrorKind.BuyTokenFailed,
message: 'Purchase token failed'
diff --git a/src/reducer/slices/createNft.ts b/src/reducer/slices/createNft.ts
index ba3ac6fe..e0e4c47b 100644
--- a/src/reducer/slices/createNft.ts
+++ b/src/reducer/slices/createNft.ts
@@ -11,6 +11,7 @@ export const steps: Step[] = ['file_upload', 'asset_details', 'confirm'];
interface Fields {
name: string | null;
description: string | null;
+ amount: number;
}
export enum CreateStatus {
@@ -49,7 +50,8 @@ export const initialState: CreateNftState = {
uploadedArtifact: null,
fields: {
name: null,
- description: null
+ description: null,
+ amount: 1
},
attributes: [],
collectionAddress: null,
@@ -60,7 +62,6 @@ export const initialState: CreateNftState = {
// Reducers & Slice
-type UpdateFieldAction = PayloadAction<{ name: keyof Fields; value: string }>;
type UpdateRowNameAction = PayloadAction<{ key: number; name: string }>;
type UpdateRowValueAction = PayloadAction<{ key: number; value: string }>;
@@ -80,8 +81,14 @@ const slice = createSlice({
state.step = steps[stepIdx - 1];
}
},
- updateField(state, action: UpdateFieldAction) {
- state.fields[action.payload.name] = action.payload.value;
+ updateName(state, action: PayloadAction) {
+ state.fields.name = action.payload;
+ },
+ updateDescription(state, action: PayloadAction) {
+ state.fields.description = action.payload;
+ },
+ updateAmount(state, action: PayloadAction) {
+ state.fields.amount = action.payload;
},
updateSelectedFile(state, action: PayloadAction) {
state.selectedFile = action.payload;
@@ -133,7 +140,9 @@ const slice = createSlice({
export const {
incrementStep,
decrementStep,
- updateField,
+ updateName,
+ updateDescription,
+ updateAmount,
updateSelectedFile,
clearSelectedfile,
updateDisplayImageFile,
diff --git a/yarn.lock b/yarn.lock
index 428cd8cd..b46018c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2404,10 +2404,10 @@
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-7.1.2.tgz#3a71bb8a45a1e08b71a54c9efcee9927f3895e80"
integrity sha512-lDyCVxxgX5lrgCa75ELCfWcdEDyfisjqoDIM3YsghQ+lyViIac/qT67qabQ/HmoVxyikFKovjKwWdn3b/oKhZA==
-"@tqtezos/minter-contracts@1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@tqtezos/minter-contracts/-/minter-contracts-1.2.0.tgz#5b98addeb6428e7cba27a35475fa601a32df6737"
- integrity sha512-KrgdApZnHzTzedUjsNzWxEWYisyeXvd2NIfUj+CcmzL00708EgCbX1zXOo5qLcsRKuffpJGbeARH32rWKUGRAw==
+"@tqtezos/minter-contracts@1.3.0":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@tqtezos/minter-contracts/-/minter-contracts-1.3.0.tgz#e89de3fccdb5401bbed3b947f9718508f9ce9407"
+ integrity sha512-ZxmaBO7s7cxKCK42Fgrv+PbESvjm2HwWqtrnaLqkszdVPy8UIIFIxuSTf9GnzD37lFLJrfv7kpvuVYrwJD3+HQ==
"@tsed/logger@5.5.2":
version "5.5.2"