Skip to content

Commit

Permalink
Add erc721 WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ericglau committed Nov 29, 2023
1 parent 5c6815f commit d0c525b
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 18 deletions.
14 changes: 7 additions & 7 deletions packages/core-cairo/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { CommonOptions } from './common-options';
import { printERC20, defaults as erc20defaults, isAccessControlRequired as erc20IsAccessControlRequired, ERC20Options, getInitialSupply } from './erc20';
// import { printERC721, defaults as erc721defaults, isAccessControlRequired as erc721IsAccessControlRequired, ERC721Options } from './erc721';
import { printERC721, defaults as erc721defaults, isAccessControlRequired as erc721IsAccessControlRequired, ERC721Options } from './erc721';
// import { printERC1155, defaults as erc1155defaults, isAccessControlRequired as erc1155IsAccessControlRequired, ERC1155Options } from './erc1155';
// import { printCustom, defaults as customDefaults, isAccessControlRequired as customIsAccessControlRequired, CustomOptions } from './custom';

Expand Down Expand Up @@ -28,7 +28,7 @@ export type ERC20 = WizardContractAPI<ERC20Options> & {
*/
getInitialSupply: (premint: string, decimals: number) => string;
}
// export type ERC721 = WizardContractAPI<ERC721Options>;
export type ERC721 = WizardContractAPI<ERC721Options>;
// export type ERC1155 = WizardContractAPI<ERC1155Options>;
// export type Custom = WizardContractAPI<CustomOptions>;

Expand All @@ -38,11 +38,11 @@ export const erc20: ERC20 = {
isAccessControlRequired: erc20IsAccessControlRequired,
getInitialSupply
}
// export const erc721: ERC721 = {
// print: printERC721,
// defaults: erc721defaults,
// isAccessControlRequired: erc721IsAccessControlRequired
// }
export const erc721: ERC721 = {
print: printERC721,
defaults: erc721defaults,
isAccessControlRequired: erc721IsAccessControlRequired
}
// export const erc1155: ERC1155 = {
// print: printERC1155,
// defaults: erc1155defaults,
Expand Down
8 changes: 4 additions & 4 deletions packages/core-cairo/src/build-generic.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ERC20Options, buildERC20 } from './erc20';
// import { ERC721Options, buildERC721 } from './erc721';
import { ERC721Options, buildERC721 } from './erc721';
// import { ERC1155Options, buildERC1155 } from './erc1155';
// import { CustomOptions, buildCustom } from './custom';

export interface KindedOptions {
ERC20: { kind: 'ERC20' } & ERC20Options;
// ERC721: { kind: 'ERC721' } & ERC721Options;
ERC721: { kind: 'ERC721' } & ERC721Options;
// ERC1155: { kind: 'ERC1155' } & ERC1155Options;
// Custom: { kind: 'Custom' } & CustomOptions;
}
Expand All @@ -17,8 +17,8 @@ export function buildGeneric(opts: GenericOptions) {
case 'ERC20':
return buildERC20(opts);

// case 'ERC721':
// return buildERC721(opts);
case 'ERC721':
return buildERC721(opts);

// case 'ERC1155':
// return buildERC1155(opts);
Expand Down
2 changes: 1 addition & 1 deletion packages/core-cairo/src/erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ const functions = defineFunctions({
'self.erc20._burn(caller, value);'
]
},
mint: {
safeMint: {
kind: 'external(v0)',
args: [
getSelfArg(),
Expand Down
162 changes: 162 additions & 0 deletions packages/core-cairo/src/erc721.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Contract, ContractBuilder, Impl } from './contract';
import { Access, requireAccessControl, setAccessControl } from './set-access-control';
import { addPausable, setPausable } from './add-pausable';
import { defineFunctions } from './utils/define-functions';
import { CommonOptions, withCommonDefaults, getSelfArg } from './common-options';
import { setUpgradeable } from './set-upgradeable';
import { setInfo } from './set-info';
import { OptionsError } from './error';
import { defineComponents } from './utils/define-components';
import { defaults as commonDefaults } from './common-options';
import { printContract } from './print';
// import { importGetCallerAddress } from './common-functions';

export const defaults: Required<ERC721Options> = {
name: 'MyToken',
symbol: 'MTK',
burnable: false,
pausable: false,
mintable: false,
access: commonDefaults.access,
upgradeable: commonDefaults.upgradeable,
info: commonDefaults.info
} as const;

export function printERC721(opts: ERC721Options = defaults): string {
return printContract(buildERC721(opts));
}

export interface ERC721Options extends CommonOptions {
name: string;
symbol: string;
burnable?: boolean;
pausable?: boolean;
mintable?: boolean;
}

function withDefaults(opts: ERC721Options): Required<ERC721Options> {
return {
...opts,
...withCommonDefaults(opts),
burnable: opts.burnable ?? defaults.burnable,
pausable: opts.pausable ?? defaults.pausable,
mintable: opts.mintable ?? defaults.mintable,
};
}

export function isAccessControlRequired(opts: Partial<ERC721Options>): boolean {
return opts.mintable === true || opts.pausable === true || opts.upgradeable === true;
}

export function buildERC721(opts: ERC721Options): Contract {
const c = new ContractBuilder(opts.name);

const allOpts = withDefaults(opts);

addBase(c, allOpts.name, allOpts.symbol);

if (allOpts.burnable) {
addBurnable(c);
}

if (allOpts.pausable) {
addPausable(c, allOpts.access);
if (allOpts.burnable) {
setPausable(c, functions.burn);
}
}

if (allOpts.mintable) {
addMintable(c, allOpts.access);
}

setAccessControl(c, allOpts.access);
setUpgradeable(c, allOpts.upgradeable, allOpts.access);
setInfo(c, allOpts.info);

return c;
}

function addBase(c: ContractBuilder, name: string, symbol: string) {
c.addComponent(
components.ERC721Component,
[
name, symbol
],
true,
);
}

function addBurnable(c: ContractBuilder) {
c.addStandaloneImport('starknet::get_caller_address');
c.addFunction(functions.burn);
}

function addMintable(c: ContractBuilder, access: Access) {
c.addStandaloneImport('starknet::ContractAddress');
c.addFunction(functions.safeMint);
requireAccessControl(c, functions.safeMint, access, 'MINTER');
}

const components = defineComponents( {
ERC721Component: {
path: 'openzeppelin::token::erc721',
substorage: {
name: 'erc721',
type: 'ERC721Component::Storage',
},
event: {
name: 'ERC721Event',
type: 'ERC721Component::Event',
},
impls: [
{
name: 'ERC721Impl',
value: 'ERC721Component::ERC721Impl<ContractState>',
},
{
name: 'ERC721MetadataImpl',
value: 'ERC721Component::ERC721MetadataImpl<ContractState>',
},
{
name: 'ERC721CamelOnly',
value: 'ERC721Component::ERC721CamelOnlyImpl<ContractState>',
},
{
name: 'ERC721MetadataCamelOnly',
value: 'ERC721Component::ERC721MetadataCamelOnlyImpl<ContractState>',
}
],
internalImpl: {
name: 'ERC721InternalImpl',
value: 'ERC721Component::InternalImpl<ContractState>',
},
},
});

const functions = defineFunctions({
burn: {
kind: 'external(v0)',
args: [
getSelfArg(),
{ name: 'tokenId', type: 'u256' }
],
code: [
'// TODO check if caller is the token owner',
'self.erc721._burn(tokenId);'
]
},
safeMint: {
kind: 'external(v0)',
args: [
getSelfArg(),
{ name: 'recipient', type: 'ContractAddress' },
{ name: 'tokenId', type: 'u256' },
{ name: 'tokenURI', type: 'felt252' },
],
code: [
'self.erc721._mint(recipient, tokenId);',
'self.erc721._set_token_uri(tokenId, tokenURI);',
]
}
});
2 changes: 1 addition & 1 deletion packages/core-cairo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ export { sanitizeKind } from './kind';

export { contractsVersion, contractsVersionTag } from './utils/version';

export { erc20 } from './api';
export { erc20, erc721 } from './api';
10 changes: 5 additions & 5 deletions packages/ui/src/cairo/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import hljs from './highlightjs';
import ERC20Controls from './ERC20Controls.svelte';
// import ERC721Controls from './ERC721Controls.svelte';
import ERC721Controls from './ERC721Controls.svelte';
// import ERC1155Controls from './ERC1155Controls.svelte';
// import CustomControls from './CustomControls.svelte';
import CopyIcon from '../icons/CopyIcon.svelte';
Expand Down Expand Up @@ -88,10 +88,10 @@
<button class:selected={tab === 'ERC20'} on:click={() => tab = 'ERC20'}>
ERC20
</button>
<!-- <button class:selected={tab === 'ERC721'} on:click={() => tab = 'ERC721'}>
<button class:selected={tab === 'ERC721'} on:click={() => tab = 'ERC721'}>
ERC721
</button>
<button class:selected={tab === 'ERC1155'} on:click={() => tab = 'ERC1155'}>
<!-- <button class:selected={tab === 'ERC1155'} on:click={() => tab = 'ERC1155'}>
ERC1155
</button>
<button class:selected={tab === 'Custom'} on:click={() => tab = 'Custom'}>
Expand Down Expand Up @@ -133,10 +133,10 @@
<div class:hidden={tab !== 'ERC20'}>
<ERC20Controls bind:opts={allOpts.ERC20} errors={errors.ERC20} />
</div>
<!-- <div class:hidden={tab !== 'ERC721'}>
<div class:hidden={tab !== 'ERC721'}>
<ERC721Controls bind:opts={allOpts.ERC721} />
</div>
<div class:hidden={tab !== 'ERC1155'}>
<!-- <div class:hidden={tab !== 'ERC1155'}>
<ERC1155Controls bind:opts={allOpts.ERC1155} />
</div>
<div class:hidden={tab !== 'Custom'}>
Expand Down
68 changes: 68 additions & 0 deletions packages/ui/src/cairo/ERC721Controls.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script lang="ts">
import HelpTooltip from '../HelpTooltip.svelte';
import type { KindedOptions } from '@openzeppelin/wizard-cairo';
import { erc721, infoDefaults } from '@openzeppelin/wizard-cairo';
import AccessControlSection from './AccessControlSection.svelte';
import UpgradeabilitySection from './UpgradeabilitySection.svelte';
import InfoSection from './InfoSection.svelte';
export const opts: Required<KindedOptions['ERC721']> = {
kind: 'ERC721',
...erc721.defaults,
info: { ...infoDefaults }, // create new object since Info is nested
};
$: requireAccessControl = erc721.isAccessControlRequired(opts);
</script>

<section class="controls-section">
<h1>Settings</h1>

<div class="grid grid-cols-[2fr,1fr] gap-2">
<label class="labeled-input">
<span>Name</span>
<input bind:value={opts.name}>
</label>
<label class="labeled-input">
<span>Symbol</span>
<input bind:value={opts.symbol}>
</label>
</div>
</section>

<section class="controls-section">
<h1>Features</h1>

<div class="checkbox-group">
<label class:checked={opts.mintable}>
<input type="checkbox" bind:checked={opts.mintable}>
Mintable
<HelpTooltip link="https://docs.openzeppelin.com/contracts-cairo/erc721#presets">
Privileged accounts will be able to emit new tokens.
</HelpTooltip>
</label>
<label class:checked={opts.burnable}>
<input type="checkbox" bind:checked={opts.burnable}>
Burnable
<HelpTooltip link="https://docs.openzeppelin.com/contracts-cairo/erc721#presets">
Token holders will be able to destroy their tokens.
</HelpTooltip>
</label>
<label class:checked={opts.pausable}>
<input type="checkbox" bind:checked={opts.pausable}>
Pausable
<HelpTooltip link="https://docs.openzeppelin.com/contracts-cairo/security#pausable">
Privileged accounts will be able to pause the functionality marked with <code>assert_not_paused</code>.
Useful for emergency response.
</HelpTooltip>
</label>
</div>
</section>

<AccessControlSection bind:access={opts.access} required={requireAccessControl} />

<UpgradeabilitySection bind:upgradeable={opts.upgradeable} />

<InfoSection bind:info={opts.info} />

0 comments on commit d0c525b

Please sign in to comment.