diff --git a/packages/core-cairo/CHANGELOG.md b/packages/core-cairo/CHANGELOG.md index 9a952b838..a2417913b 100644 --- a/packages/core-cairo/CHANGELOG.md +++ b/packages/core-cairo/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.19.0 (2024-11-27) + +- Add ERC2981 (RoyaltyInfo) for ERC721 and ERC1155 ([#413](https://github.com/OpenZeppelin/contracts-wizard/pull/413)) + ## 0.18.0 (2024-11-15) - **Breaking changes**: diff --git a/packages/core-cairo/package.json b/packages/core-cairo/package.json index f54540830..0f8d88a9a 100644 --- a/packages/core-cairo/package.json +++ b/packages/core-cairo/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/wizard-cairo", - "version": "0.18.0", + "version": "0.19.0", "description": "A boilerplate generator to get started with OpenZeppelin Contracts for Cairo", "license": "MIT", "repository": "github:OpenZeppelin/contracts-wizard", diff --git a/packages/core-cairo/src/contract.ts b/packages/core-cairo/src/contract.ts index fa635d00b..431b3f321 100644 --- a/packages/core-cairo/src/contract.ts +++ b/packages/core-cairo/src/contract.ts @@ -13,7 +13,7 @@ export interface Contract { superVariables: Variable[]; } -export type Value = string | number | { lit: string } | { note: string, value: Value }; +export type Value = string | number | bigint | { lit: string } | { note: string, value: Value }; export interface Component { name: string; @@ -57,6 +57,7 @@ export interface BaseImplementedTrait { } export interface ImplementedTrait extends BaseImplementedTrait { + superVariables: Variable[]; functions: ContractFunction[]; } @@ -172,6 +173,7 @@ export class ContractBuilder implements Contract { name: baseTrait.name, of: baseTrait.of, tags: [ ...baseTrait.tags ], + superVariables: [], functions: [], priority: baseTrait.priority, }; @@ -180,6 +182,27 @@ export class ContractBuilder implements Contract { } } + addSuperVariableToTrait(baseTrait: BaseImplementedTrait, newVar: Variable): boolean { + const trait = this.addImplementedTrait(baseTrait); + for (const existingVar of trait.superVariables) { + if (existingVar.name === newVar.name) { + if (existingVar.type !== newVar.type) { + throw new Error( + `Tried to add duplicate super var ${newVar.name} with different type: ${newVar.type} instead of ${existingVar.type}.` + ); + } + if (existingVar.value !== newVar.value) { + throw new Error( + `Tried to add duplicate super var ${newVar.name} with different value: ${newVar.value} instead of ${existingVar.value}.` + ); + } + return false; // No need to add, already exists + } + } + trait.superVariables.push(newVar); + return true; + } + addFunction(baseTrait: BaseImplementedTrait, fn: BaseFunction): ContractFunction { const t = this.addImplementedTrait(baseTrait); diff --git a/packages/core-cairo/src/custom.test.ts.md b/packages/core-cairo/src/custom.test.ts.md index 998b9155d..ef3f7ffbb 100644 --- a/packages/core-cairo/src/custom.test.ts.md +++ b/packages/core-cairo/src/custom.test.ts.md @@ -301,7 +301,7 @@ Generated by [AVA](https://avajs.dev). use openzeppelin::upgrades::interface::IUpgradeable;␊ use starknet::ClassHash;␊ use starknet::ContractAddress;␊ - use super::{UPGRADER_ROLE};␊ + use super::UPGRADER_ROLE;␊ ␊ component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);␊ component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ diff --git a/packages/core-cairo/src/custom.test.ts.snap b/packages/core-cairo/src/custom.test.ts.snap index 427426620..08994387b 100644 Binary files a/packages/core-cairo/src/custom.test.ts.snap and b/packages/core-cairo/src/custom.test.ts.snap differ diff --git a/packages/core-cairo/src/erc1155.test.ts b/packages/core-cairo/src/erc1155.test.ts index 266548201..9923de2d5 100644 --- a/packages/core-cairo/src/erc1155.test.ts +++ b/packages/core-cairo/src/erc1155.test.ts @@ -3,12 +3,25 @@ import { erc1155 } from '.'; import { buildERC1155, ERC1155Options } from './erc1155'; import { printContract } from './print'; +import { royaltyInfoOptions } from './set-royalty-info'; + +const NAME = 'MyToken'; +const CUSTOM_NAME = 'CustomToken'; +const BASE_URI = 'https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/'; + +const allFeaturesON: Partial = { + mintable: true, + burnable: true, + pausable: true, + royaltyInfo: royaltyInfoOptions.enabledDefault, + upgradeable: true +} as const; function testERC1155(title: string, opts: Partial) { test(title, t => { const c = buildERC1155({ - name: 'MyToken', - baseUri: 'https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/', + name: NAME, + baseUri: BASE_URI, ...opts, }); t.snapshot(printContract(c)); @@ -18,10 +31,10 @@ function testERC1155(title: string, opts: Partial) { /** * Tests external API for equivalence with internal API */ - function testAPIEquivalence(title: string, opts?: ERC1155Options) { +function testAPIEquivalence(title: string, opts?: ERC1155Options) { test(title, t => { t.is(erc1155.print(opts), printContract(buildERC1155({ - name: 'MyToken', + name: NAME, baseUri: '', ...opts, }))); @@ -59,33 +72,51 @@ testERC1155('mintable + roles', { access: 'roles', }); +testERC1155('royalty info disabled', { + royaltyInfo: royaltyInfoOptions.disabled +}); + +testERC1155('royalty info enabled default + ownable', { + royaltyInfo: royaltyInfoOptions.enabledDefault, + access: 'ownable' +}); + +testERC1155('royalty info enabled default + roles', { + royaltyInfo: royaltyInfoOptions.enabledDefault, + access: 'roles' +}); + +testERC1155('royalty info enabled custom + ownable', { + royaltyInfo: royaltyInfoOptions.enabledCustom, + access: 'ownable' +}); + +testERC1155('royalty info enabled custom + roles', { + royaltyInfo: royaltyInfoOptions.enabledCustom, + access: 'roles' +}); + testERC1155('full non-upgradeable', { - mintable: true, + ...allFeaturesON, access: 'roles', - burnable: true, - pausable: true, upgradeable: false, }); testERC1155('full upgradeable', { - mintable: true, + ...allFeaturesON, access: 'roles', - burnable: true, - pausable: true, upgradeable: true, }); testAPIEquivalence('API default'); -testAPIEquivalence('API basic', { name: 'CustomToken', baseUri: 'https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/' }); +testAPIEquivalence('API basic', { name: CUSTOM_NAME, baseUri: BASE_URI }); testAPIEquivalence('API full upgradeable', { - name: 'CustomToken', - baseUri: 'https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/', - mintable: true, + ...allFeaturesON, + name: CUSTOM_NAME, + baseUri: BASE_URI, access: 'roles', - burnable: true, - pausable: true, upgradeable: true, }); @@ -98,6 +129,8 @@ test('API isAccessControlRequired', async t => { t.is(erc1155.isAccessControlRequired({ updatableUri: false, pausable: true }), true); t.is(erc1155.isAccessControlRequired({ updatableUri: false, upgradeable: true }), true); t.is(erc1155.isAccessControlRequired({ updatableUri: true }), true); - t.is(erc1155.isAccessControlRequired({ updatableUri: false}), false); + t.is(erc1155.isAccessControlRequired({ royaltyInfo: royaltyInfoOptions.enabledDefault }), true); + t.is(erc1155.isAccessControlRequired({ royaltyInfo: royaltyInfoOptions.enabledCustom }), true); + t.is(erc1155.isAccessControlRequired({ updatableUri: false }), false); t.is(erc1155.isAccessControlRequired({}), true); // updatableUri is true by default }); \ No newline at end of file diff --git a/packages/core-cairo/src/erc1155.test.ts.md b/packages/core-cairo/src/erc1155.test.ts.md index 02bd3e393..1d01fbe55 100644 --- a/packages/core-cairo/src/erc1155.test.ts.md +++ b/packages/core-cairo/src/erc1155.test.ts.md @@ -848,6 +848,558 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## royalty info disabled + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::ownable::OwnableComponent;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::erc1155::ERC1155Component;␊ + use openzeppelin::token::erc1155::ERC1155HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + ␊ + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC1155MixinImpl = ERC1155Component::ERC1155MixinImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + impl ERC1155InternalImpl = ERC1155Component::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc1155: ERC1155Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC1155Event: ERC1155Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc1155.initializer("https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + ␊ + #[generate_trait]␊ + #[abi(per_item)]␊ + impl ExternalImpl of ExternalTrait {␊ + #[external(v0)]␊ + fn set_base_uri(ref self: ContractState, base_uri: ByteArray) {␊ + self.ownable.assert_only_owner();␊ + self.erc1155._set_base_uri(base_uri);␊ + }␊ + ␊ + #[external(v0)]␊ + fn setBaseUri(ref self: ContractState, baseUri: ByteArray) {␊ + self.set_base_uri(baseUri);␊ + }␊ + }␊ + }␊ + ` + +## royalty info enabled default + ownable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::ownable::OwnableComponent;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::common::erc2981::DefaultConfig;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ + use openzeppelin::token::erc1155::ERC1155Component;␊ + use openzeppelin::token::erc1155::ERC1155HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + ␊ + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC1155MixinImpl = ERC1155Component::ERC1155MixinImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminOwnableImpl = ERC2981Component::ERC2981AdminOwnableImpl;␊ + ␊ + impl ERC1155InternalImpl = ERC1155Component::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc1155: ERC1155Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC1155Event: ERC1155Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + owner: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + ) {␊ + self.erc1155.initializer("https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/");␊ + self.ownable.initializer(owner);␊ + self.erc2981.initializer(default_royalty_receiver, 500);␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + ␊ + #[generate_trait]␊ + #[abi(per_item)]␊ + impl ExternalImpl of ExternalTrait {␊ + #[external(v0)]␊ + fn set_base_uri(ref self: ContractState, base_uri: ByteArray) {␊ + self.ownable.assert_only_owner();␊ + self.erc1155._set_base_uri(base_uri);␊ + }␊ + ␊ + #[external(v0)]␊ + fn setBaseUri(ref self: ContractState, baseUri: ByteArray) {␊ + self.set_base_uri(baseUri);␊ + }␊ + }␊ + }␊ + ` + +## royalty info enabled default + roles + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + const URI_SETTER_ROLE: felt252 = selector!("URI_SETTER_ROLE");␊ + const UPGRADER_ROLE: felt252 = selector!("UPGRADER_ROLE");␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::accesscontrol::AccessControlComponent;␊ + use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::common::erc2981::DefaultConfig;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ + use openzeppelin::token::erc1155::ERC1155Component;␊ + use openzeppelin::token::erc1155::ERC1155HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + use super::{URI_SETTER_ROLE, UPGRADER_ROLE};␊ + ␊ + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC1155MixinImpl = ERC1155Component::ERC1155MixinImpl;␊ + #[abi(embed_v0)]␊ + impl AccessControlImpl = AccessControlComponent::AccessControlImpl;␊ + #[abi(embed_v0)]␊ + impl AccessControlCamelImpl = AccessControlComponent::AccessControlCamelImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminAccessControlImpl = ERC2981Component::ERC2981AdminAccessControlImpl;␊ + ␊ + impl ERC1155InternalImpl = ERC1155Component::InternalImpl;␊ + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc1155: ERC1155Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + accesscontrol: AccessControlComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC1155Event: ERC1155Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + AccessControlEvent: AccessControlComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + default_admin: ContractAddress,␊ + uri_setter: ContractAddress,␊ + upgrader: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + royalty_admin: ContractAddress,␊ + ) {␊ + self.erc1155.initializer("https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/");␊ + self.accesscontrol.initializer();␊ + self.erc2981.initializer(default_royalty_receiver, 500);␊ + ␊ + self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, default_admin);␊ + self.accesscontrol._grant_role(URI_SETTER_ROLE, uri_setter);␊ + self.accesscontrol._grant_role(UPGRADER_ROLE, upgrader);␊ + self.accesscontrol._grant_role(ERC2981Component::ROYALTY_ADMIN_ROLE, royalty_admin);␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.accesscontrol.assert_only_role(UPGRADER_ROLE);␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + ␊ + #[generate_trait]␊ + #[abi(per_item)]␊ + impl ExternalImpl of ExternalTrait {␊ + #[external(v0)]␊ + fn set_base_uri(ref self: ContractState, base_uri: ByteArray) {␊ + self.accesscontrol.assert_only_role(URI_SETTER_ROLE);␊ + self.erc1155._set_base_uri(base_uri);␊ + }␊ + ␊ + #[external(v0)]␊ + fn setBaseUri(ref self: ContractState, baseUri: ByteArray) {␊ + self.set_base_uri(baseUri);␊ + }␊ + }␊ + }␊ + ` + +## royalty info enabled custom + ownable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::ownable::OwnableComponent;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ + use openzeppelin::token::erc1155::ERC1155Component;␊ + use openzeppelin::token::erc1155::ERC1155HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + ␊ + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC1155MixinImpl = ERC1155Component::ERC1155MixinImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminOwnableImpl = ERC2981Component::ERC2981AdminOwnableImpl;␊ + ␊ + impl ERC1155InternalImpl = ERC1155Component::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc1155: ERC1155Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC1155Event: ERC1155Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + owner: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + ) {␊ + self.erc1155.initializer("https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/");␊ + self.ownable.initializer(owner);␊ + self.erc2981.initializer(default_royalty_receiver, 15125);␊ + }␊ + ␊ + impl ERC2981ImmutableConfig of ERC2981Component::ImmutableConfig {␊ + const FEE_DENOMINATOR: u128 = 100000;␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + ␊ + #[generate_trait]␊ + #[abi(per_item)]␊ + impl ExternalImpl of ExternalTrait {␊ + #[external(v0)]␊ + fn set_base_uri(ref self: ContractState, base_uri: ByteArray) {␊ + self.ownable.assert_only_owner();␊ + self.erc1155._set_base_uri(base_uri);␊ + }␊ + ␊ + #[external(v0)]␊ + fn setBaseUri(ref self: ContractState, baseUri: ByteArray) {␊ + self.set_base_uri(baseUri);␊ + }␊ + }␊ + }␊ + ` + +## royalty info enabled custom + roles + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + const URI_SETTER_ROLE: felt252 = selector!("URI_SETTER_ROLE");␊ + const UPGRADER_ROLE: felt252 = selector!("UPGRADER_ROLE");␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::accesscontrol::AccessControlComponent;␊ + use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ + use openzeppelin::token::erc1155::ERC1155Component;␊ + use openzeppelin::token::erc1155::ERC1155HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + use super::{URI_SETTER_ROLE, UPGRADER_ROLE};␊ + ␊ + component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC1155MixinImpl = ERC1155Component::ERC1155MixinImpl;␊ + #[abi(embed_v0)]␊ + impl AccessControlImpl = AccessControlComponent::AccessControlImpl;␊ + #[abi(embed_v0)]␊ + impl AccessControlCamelImpl = AccessControlComponent::AccessControlCamelImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminAccessControlImpl = ERC2981Component::ERC2981AdminAccessControlImpl;␊ + ␊ + impl ERC1155InternalImpl = ERC1155Component::InternalImpl;␊ + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc1155: ERC1155Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + accesscontrol: AccessControlComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC1155Event: ERC1155Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + AccessControlEvent: AccessControlComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + default_admin: ContractAddress,␊ + uri_setter: ContractAddress,␊ + upgrader: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + royalty_admin: ContractAddress,␊ + ) {␊ + self.erc1155.initializer("https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/");␊ + self.accesscontrol.initializer();␊ + self.erc2981.initializer(default_royalty_receiver, 15125);␊ + ␊ + self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, default_admin);␊ + self.accesscontrol._grant_role(URI_SETTER_ROLE, uri_setter);␊ + self.accesscontrol._grant_role(UPGRADER_ROLE, upgrader);␊ + self.accesscontrol._grant_role(ERC2981Component::ROYALTY_ADMIN_ROLE, royalty_admin);␊ + }␊ + ␊ + impl ERC2981ImmutableConfig of ERC2981Component::ImmutableConfig {␊ + const FEE_DENOMINATOR: u128 = 100000;␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.accesscontrol.assert_only_role(UPGRADER_ROLE);␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + ␊ + #[generate_trait]␊ + #[abi(per_item)]␊ + impl ExternalImpl of ExternalTrait {␊ + #[external(v0)]␊ + fn set_base_uri(ref self: ContractState, base_uri: ByteArray) {␊ + self.accesscontrol.assert_only_role(URI_SETTER_ROLE);␊ + self.erc1155._set_base_uri(base_uri);␊ + }␊ + ␊ + #[external(v0)]␊ + fn setBaseUri(ref self: ContractState, baseUri: ByteArray) {␊ + self.set_base_uri(baseUri);␊ + }␊ + }␊ + }␊ + ` + ## full non-upgradeable > Snapshot 1 @@ -865,6 +1417,8 @@ Generated by [AVA](https://avajs.dev). use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE;␊ use openzeppelin::introspection::src5::SRC5Component;␊ use openzeppelin::security::pausable::PausableComponent;␊ + use openzeppelin::token::common::erc2981::DefaultConfig;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ use openzeppelin::token::erc1155::ERC1155Component;␊ use starknet::ContractAddress;␊ use starknet::get_caller_address;␊ @@ -874,6 +1428,7 @@ Generated by [AVA](https://avajs.dev). component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ component!(path: PausableComponent, storage: pausable, event: PausableEvent);␊ component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ ␊ #[abi(embed_v0)]␊ impl ERC1155MixinImpl = ERC1155Component::ERC1155MixinImpl;␊ @@ -883,10 +1438,17 @@ Generated by [AVA](https://avajs.dev). impl AccessControlImpl = AccessControlComponent::AccessControlImpl;␊ #[abi(embed_v0)]␊ impl AccessControlCamelImpl = AccessControlComponent::AccessControlCamelImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminAccessControlImpl = ERC2981Component::ERC2981AdminAccessControlImpl;␊ ␊ impl ERC1155InternalImpl = ERC1155Component::InternalImpl;␊ impl PausableInternalImpl = PausableComponent::InternalImpl;␊ impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ ␊ #[storage]␊ struct Storage {␊ @@ -898,6 +1460,8 @@ Generated by [AVA](https://avajs.dev). pausable: PausableComponent::Storage,␊ #[substorage(v0)]␊ accesscontrol: AccessControlComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ }␊ ␊ #[event]␊ @@ -911,6 +1475,8 @@ Generated by [AVA](https://avajs.dev). PausableEvent: PausableComponent::Event,␊ #[flat]␊ AccessControlEvent: AccessControlComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ }␊ ␊ #[constructor]␊ @@ -920,14 +1486,18 @@ Generated by [AVA](https://avajs.dev). pauser: ContractAddress,␊ minter: ContractAddress,␊ uri_setter: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + royalty_admin: ContractAddress,␊ ) {␊ self.erc1155.initializer("https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/");␊ self.accesscontrol.initializer();␊ + self.erc2981.initializer(default_royalty_receiver, 500);␊ ␊ self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, default_admin);␊ self.accesscontrol._grant_role(PAUSER_ROLE, pauser);␊ self.accesscontrol._grant_role(MINTER_ROLE, minter);␊ self.accesscontrol._grant_role(URI_SETTER_ROLE, uri_setter);␊ + self.accesscontrol._grant_role(ERC2981Component::ROYALTY_ADMIN_ROLE, royalty_admin);␊ }␊ ␊ impl ERC1155HooksImpl of ERC1155Component::ERC1155HooksTrait {␊ @@ -1058,6 +1628,8 @@ Generated by [AVA](https://avajs.dev). use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE;␊ use openzeppelin::introspection::src5::SRC5Component;␊ use openzeppelin::security::pausable::PausableComponent;␊ + use openzeppelin::token::common::erc2981::DefaultConfig;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ use openzeppelin::token::erc1155::ERC1155Component;␊ use openzeppelin::upgrades::UpgradeableComponent;␊ use openzeppelin::upgrades::interface::IUpgradeable;␊ @@ -1071,6 +1643,7 @@ Generated by [AVA](https://avajs.dev). component!(path: PausableComponent, storage: pausable, event: PausableEvent);␊ component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);␊ component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ ␊ #[abi(embed_v0)]␊ impl ERC1155MixinImpl = ERC1155Component::ERC1155MixinImpl;␊ @@ -1080,11 +1653,18 @@ Generated by [AVA](https://avajs.dev). impl AccessControlImpl = AccessControlComponent::AccessControlImpl;␊ #[abi(embed_v0)]␊ impl AccessControlCamelImpl = AccessControlComponent::AccessControlCamelImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminAccessControlImpl = ERC2981Component::ERC2981AdminAccessControlImpl;␊ ␊ impl ERC1155InternalImpl = ERC1155Component::InternalImpl;␊ impl PausableInternalImpl = PausableComponent::InternalImpl;␊ impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;␊ impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ ␊ #[storage]␊ struct Storage {␊ @@ -1098,6 +1678,8 @@ Generated by [AVA](https://avajs.dev). accesscontrol: AccessControlComponent::Storage,␊ #[substorage(v0)]␊ upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ }␊ ␊ #[event]␊ @@ -1113,6 +1695,8 @@ Generated by [AVA](https://avajs.dev). AccessControlEvent: AccessControlComponent::Event,␊ #[flat]␊ UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ }␊ ␊ #[constructor]␊ @@ -1123,15 +1707,19 @@ Generated by [AVA](https://avajs.dev). minter: ContractAddress,␊ uri_setter: ContractAddress,␊ upgrader: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + royalty_admin: ContractAddress,␊ ) {␊ self.erc1155.initializer("https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/");␊ self.accesscontrol.initializer();␊ + self.erc2981.initializer(default_royalty_receiver, 500);␊ ␊ self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, default_admin);␊ self.accesscontrol._grant_role(PAUSER_ROLE, pauser);␊ self.accesscontrol._grant_role(MINTER_ROLE, minter);␊ self.accesscontrol._grant_role(URI_SETTER_ROLE, uri_setter);␊ self.accesscontrol._grant_role(UPGRADER_ROLE, upgrader);␊ + self.accesscontrol._grant_role(ERC2981Component::ROYALTY_ADMIN_ROLE, royalty_admin);␊ }␊ ␊ impl ERC1155HooksImpl of ERC1155Component::ERC1155HooksTrait {␊ diff --git a/packages/core-cairo/src/erc1155.test.ts.snap b/packages/core-cairo/src/erc1155.test.ts.snap index c97319dfb..7b320e027 100644 Binary files a/packages/core-cairo/src/erc1155.test.ts.snap and b/packages/core-cairo/src/erc1155.test.ts.snap differ diff --git a/packages/core-cairo/src/erc1155.ts b/packages/core-cairo/src/erc1155.ts index bfb421e6a..2aded8abd 100644 --- a/packages/core-cairo/src/erc1155.ts +++ b/packages/core-cairo/src/erc1155.ts @@ -11,6 +11,7 @@ import { printContract } from './print'; import { addSRC5Component } from './common-components'; import { externalTrait } from './external-trait'; import { toByteArray } from './utils/convert-strings'; +import { RoyaltyInfoOptions, setRoyaltyInfo, defaults as royaltyInfoDefaults } from './set-royalty-info'; export const defaults: Required = { name: 'MyToken', @@ -19,6 +20,7 @@ export const defaults: Required = { pausable: false, mintable: false, updatableUri: true, + royaltyInfo: royaltyInfoDefaults, access: commonDefaults.access, upgradeable: commonDefaults.upgradeable, info: commonDefaults.info @@ -35,6 +37,7 @@ export interface ERC1155Options extends CommonContractOptions { pausable?: boolean; mintable?: boolean; updatableUri?: boolean; + royaltyInfo?: RoyaltyInfoOptions; } function withDefaults(opts: ERC1155Options): Required { @@ -45,11 +48,12 @@ function withDefaults(opts: ERC1155Options): Required { pausable: opts.pausable ?? defaults.pausable, mintable: opts.mintable ?? defaults.mintable, updatableUri: opts.updatableUri ?? defaults.updatableUri, + royaltyInfo: opts.royaltyInfo ?? defaults.royaltyInfo, }; } export function isAccessControlRequired(opts: Partial): boolean { - return opts.mintable === true || opts.pausable === true || opts.updatableUri !== false || opts.upgradeable === true; + return opts.mintable === true || opts.pausable === true || opts.updatableUri !== false || opts.upgradeable === true || opts.royaltyInfo?.enabled === true; } export function buildERC1155(opts: ERC1155Options): Contract { @@ -76,11 +80,12 @@ export function buildERC1155(opts: ERC1155Options): Contract { addSetBaseUri(c, allOpts.access); } - addHooks(c, allOpts); - setAccessControl(c, allOpts.access); setUpgradeable(c, allOpts.upgradeable, allOpts.access); setInfo(c, allOpts.info); + setRoyaltyInfo(c, allOpts.royaltyInfo, allOpts.access); + + addHooks(c, allOpts); return c; } diff --git a/packages/core-cairo/src/erc20.ts b/packages/core-cairo/src/erc20.ts index 3e13b6119..6e4a349ed 100644 --- a/packages/core-cairo/src/erc20.ts +++ b/packages/core-cairo/src/erc20.ts @@ -1,4 +1,4 @@ -import { BaseImplementedTrait, Contract, ContractBuilder } from './contract'; +import { Contract, ContractBuilder } from './contract'; import { Access, requireAccessControl, setAccessControl } from './set-access-control'; import { addPausable } from './add-pausable'; import { defineFunctions } from './utils/define-functions'; @@ -10,7 +10,7 @@ import { defineComponents } from './utils/define-components'; import { contractDefaults as commonDefaults } from './common-options'; import { printContract } from './print'; import { externalTrait } from './external-trait'; -import { toByteArray, toFelt252 } from './utils/convert-strings'; +import { toByteArray, toFelt252, toUint } from './utils/convert-strings'; import { addVotesComponent } from './common-components'; @@ -193,7 +193,7 @@ function addPremint(c: ContractBuilder, amount: string) { }); } - const premintAbsolute = getInitialSupply(amount, 18); + const premintAbsolute = toUint(getInitialSupply(amount, 18), 'premint', 'u256'); c.addStandaloneImport('starknet::ContractAddress'); c.addConstructorArgument({ name:'recipient', type:'ContractAddress' }); diff --git a/packages/core-cairo/src/erc721.test.ts b/packages/core-cairo/src/erc721.test.ts index d59400092..6608368fa 100644 --- a/packages/core-cairo/src/erc721.test.ts +++ b/packages/core-cairo/src/erc721.test.ts @@ -2,25 +2,35 @@ import test from 'ava'; import { buildERC721, ERC721Options } from './erc721'; import { printContract } from './print'; +import { royaltyInfoOptions } from './set-royalty-info'; import { erc721, OptionsError } from '.'; +const NAME = 'MyToken'; +const CUSTOM_NAME = 'CustomToken'; +const SYMBOL = 'MTK'; +const CUSTOM_SYMBOL = 'CTK'; +const APP_NAME = 'MY_DAPP_NAME'; +const APP_VERSION = 'MY_DAPP_VERSION'; +const BASE_URI = 'https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/'; + const allFeaturesON: Partial = { mintable: true, burnable: true, pausable: true, enumerable: true, + royaltyInfo: royaltyInfoOptions.enabledDefault, votes: true, - appName: 'MY_DAPP_NAME', - appVersion: 'MY_DAPP_VERSION', + appName: APP_NAME, + appVersion: APP_VERSION, upgradeable: true } as const; function testERC721(title: string, opts: Partial) { test(title, t => { const c = buildERC721({ - name: 'MyToken', - symbol: 'MTK', + name: NAME, + symbol: SYMBOL, ...opts, }); t.snapshot(printContract(c)); @@ -33,8 +43,8 @@ function testERC721(title: string, opts: Partial) { function testAPIEquivalence(title: string, opts?: ERC721Options) { test(title, t => { t.is(erc721.print(opts), printContract(buildERC721({ - name: 'MyToken', - symbol: 'MTK', + name: NAME, + symbol: SYMBOL, ...opts, }))); }); @@ -47,7 +57,7 @@ testERC721('basic non-upgradeable', { testERC721('basic', {}); testERC721('base uri', { - baseUri: 'https://gateway.pinata.cloud/ipfs/QmcP9hxrnC1T5ATPmq2saFeAM1ypFX9BnAswCdHB9JCjLA/', + baseUri: BASE_URI, }); testERC721('burnable', { @@ -76,6 +86,30 @@ testERC721('mintable + roles', { access: 'roles', }); +testERC721('royalty info disabled', { + royaltyInfo: royaltyInfoOptions.disabled +}); + +testERC721('royalty info enabled default + ownable', { + royaltyInfo: royaltyInfoOptions.enabledDefault, + access: 'ownable' +}); + +testERC721('royalty info enabled default + roles', { + royaltyInfo: royaltyInfoOptions.enabledDefault, + access: 'roles' +}); + +testERC721('royalty info enabled custom + ownable', { + royaltyInfo: royaltyInfoOptions.enabledCustom, + access: 'ownable' +}); + +testERC721('royalty info enabled custom + roles', { + royaltyInfo: royaltyInfoOptions.enabledCustom, + access: 'roles' +}); + testERC721('full non-upgradeable', { ...allFeaturesON, upgradeable: false, @@ -83,19 +117,19 @@ testERC721('full non-upgradeable', { testERC721('erc721 votes', { votes: true, - appName: 'MY_DAPP_NAME', + appName: APP_NAME, }); testERC721('erc721 votes, version', { votes: true, - appName: 'MY_DAPP_NAME', - appVersion: 'MY_DAPP_VERSION', + appName: APP_NAME, + appVersion: APP_VERSION, }); test('erc721 votes, no name', async t => { let error = t.throws(() => buildERC721({ - name: 'MyToken', - symbol: 'MTK', + name: NAME, + symbol: SYMBOL, votes: true, })); t.is((error as OptionsError).messages.appName, 'Application Name is required when Votes are enabled'); @@ -103,10 +137,10 @@ test('erc721 votes, no name', async t => { test('erc721 votes, no version', async t => { let error = t.throws(() => buildERC721({ - name: 'MyToken', - symbol: 'MTK', + name: NAME, + symbol: SYMBOL, votes: true, - appName: 'MY_DAPP_NAME', + appName: APP_NAME, appVersion: '' })); t.is((error as OptionsError).messages.appVersion, 'Application Version is required when Votes are enabled'); @@ -114,10 +148,10 @@ test('erc721 votes, no version', async t => { test('erc721 votes, empty version', async t => { let error = t.throws(() => buildERC721({ - name: 'MyToken', - symbol: 'MTK', + name: NAME, + symbol: SYMBOL, votes: true, - appName: 'MY_DAPP_NAME', + appName: APP_NAME, appVersion: '', // avoids default value of v1 })); t.is((error as OptionsError).messages.appVersion, 'Application Version is required when Votes are enabled'); @@ -125,7 +159,7 @@ test('erc721 votes, empty version', async t => { testERC721('erc721 votes, non-upgradeable', { votes: true, - appName: 'MY_DAPP_NAME', + appName: APP_NAME, upgradeable: false, }); @@ -133,12 +167,12 @@ testERC721('full upgradeable', allFeaturesON); testAPIEquivalence('API default'); -testAPIEquivalence('API basic', { name: 'CustomToken', symbol: 'CTK' }); +testAPIEquivalence('API basic', { name: CUSTOM_NAME, symbol: CUSTOM_SYMBOL }); testAPIEquivalence('API full upgradeable', { ...allFeaturesON, - name: 'CustomToken', - symbol: 'CTK' + name: CUSTOM_NAME, + symbol: CUSTOM_SYMBOL }); test('API assert defaults', async t => { @@ -149,6 +183,9 @@ test('API isAccessControlRequired', async t => { t.is(erc721.isAccessControlRequired({ mintable: true }), true); t.is(erc721.isAccessControlRequired({ pausable: true }), true); t.is(erc721.isAccessControlRequired({ upgradeable: true }), true); + t.is(erc721.isAccessControlRequired({ royaltyInfo: royaltyInfoOptions.enabledDefault }), true); + t.is(erc721.isAccessControlRequired({ royaltyInfo: royaltyInfoOptions.enabledCustom }), true); + t.is(erc721.isAccessControlRequired({ royaltyInfo: royaltyInfoOptions.disabled }), false); t.is(erc721.isAccessControlRequired({ burnable: true }), false); t.is(erc721.isAccessControlRequired({ enumerable: true }), false); }); \ No newline at end of file diff --git a/packages/core-cairo/src/erc721.test.ts.md b/packages/core-cairo/src/erc721.test.ts.md index c781aa915..f6bbdf5ee 100644 --- a/packages/core-cairo/src/erc721.test.ts.md +++ b/packages/core-cairo/src/erc721.test.ts.md @@ -814,6 +814,477 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## royalty info disabled + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::ownable::OwnableComponent;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::erc721::ERC721Component;␊ + use openzeppelin::token::erc721::ERC721HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + ␊ + component!(path: ERC721Component, storage: erc721, event: ERC721Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + impl ERC721InternalImpl = ERC721Component::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc721: ERC721Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC721Event: ERC721Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc721.initializer("MyToken", "MTK", "");␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## royalty info enabled default + ownable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::ownable::OwnableComponent;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::common::erc2981::DefaultConfig;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ + use openzeppelin::token::erc721::ERC721Component;␊ + use openzeppelin::token::erc721::ERC721HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + ␊ + component!(path: ERC721Component, storage: erc721, event: ERC721Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminOwnableImpl = ERC2981Component::ERC2981AdminOwnableImpl;␊ + ␊ + impl ERC721InternalImpl = ERC721Component::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc721: ERC721Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC721Event: ERC721Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + owner: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + ) {␊ + self.erc721.initializer("MyToken", "MTK", "");␊ + self.ownable.initializer(owner);␊ + self.erc2981.initializer(default_royalty_receiver, 500);␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## royalty info enabled default + roles + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + const UPGRADER_ROLE: felt252 = selector!("UPGRADER_ROLE");␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::accesscontrol::AccessControlComponent;␊ + use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::common::erc2981::DefaultConfig;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ + use openzeppelin::token::erc721::ERC721Component;␊ + use openzeppelin::token::erc721::ERC721HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + use super::UPGRADER_ROLE;␊ + ␊ + component!(path: ERC721Component, storage: erc721, event: ERC721Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl;␊ + #[abi(embed_v0)]␊ + impl AccessControlImpl = AccessControlComponent::AccessControlImpl;␊ + #[abi(embed_v0)]␊ + impl AccessControlCamelImpl = AccessControlComponent::AccessControlCamelImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminAccessControlImpl = ERC2981Component::ERC2981AdminAccessControlImpl;␊ + ␊ + impl ERC721InternalImpl = ERC721Component::InternalImpl;␊ + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc721: ERC721Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + accesscontrol: AccessControlComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC721Event: ERC721Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + AccessControlEvent: AccessControlComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + default_admin: ContractAddress,␊ + upgrader: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + royalty_admin: ContractAddress,␊ + ) {␊ + self.erc721.initializer("MyToken", "MTK", "");␊ + self.accesscontrol.initializer();␊ + self.erc2981.initializer(default_royalty_receiver, 500);␊ + ␊ + self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, default_admin);␊ + self.accesscontrol._grant_role(UPGRADER_ROLE, upgrader);␊ + self.accesscontrol._grant_role(ERC2981Component::ROYALTY_ADMIN_ROLE, royalty_admin);␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.accesscontrol.assert_only_role(UPGRADER_ROLE);␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## royalty info enabled custom + ownable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::ownable::OwnableComponent;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ + use openzeppelin::token::erc721::ERC721Component;␊ + use openzeppelin::token::erc721::ERC721HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + ␊ + component!(path: ERC721Component, storage: erc721, event: ERC721Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminOwnableImpl = ERC2981Component::ERC2981AdminOwnableImpl;␊ + ␊ + impl ERC721InternalImpl = ERC721Component::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc721: ERC721Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC721Event: ERC721Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + owner: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + ) {␊ + self.erc721.initializer("MyToken", "MTK", "");␊ + self.ownable.initializer(owner);␊ + self.erc2981.initializer(default_royalty_receiver, 15125);␊ + }␊ + ␊ + impl ERC2981ImmutableConfig of ERC2981Component::ImmutableConfig {␊ + const FEE_DENOMINATOR: u128 = 100000;␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## royalty info enabled custom + roles + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.19.0␊ + ␊ + const UPGRADER_ROLE: felt252 = selector!("UPGRADER_ROLE");␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::accesscontrol::AccessControlComponent;␊ + use openzeppelin::access::accesscontrol::DEFAULT_ADMIN_ROLE;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ + use openzeppelin::token::erc721::ERC721Component;␊ + use openzeppelin::token::erc721::ERC721HooksEmptyImpl;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + use super::UPGRADER_ROLE;␊ + ␊ + component!(path: ERC721Component, storage: erc721, event: ERC721Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl;␊ + #[abi(embed_v0)]␊ + impl AccessControlImpl = AccessControlComponent::AccessControlImpl;␊ + #[abi(embed_v0)]␊ + impl AccessControlCamelImpl = AccessControlComponent::AccessControlCamelImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminAccessControlImpl = ERC2981Component::ERC2981AdminAccessControlImpl;␊ + ␊ + impl ERC721InternalImpl = ERC721Component::InternalImpl;␊ + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc721: ERC721Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + accesscontrol: AccessControlComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC721Event: ERC721Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + AccessControlEvent: AccessControlComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(␊ + ref self: ContractState,␊ + default_admin: ContractAddress,␊ + upgrader: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + royalty_admin: ContractAddress,␊ + ) {␊ + self.erc721.initializer("MyToken", "MTK", "");␊ + self.accesscontrol.initializer();␊ + self.erc2981.initializer(default_royalty_receiver, 15125);␊ + ␊ + self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, default_admin);␊ + self.accesscontrol._grant_role(UPGRADER_ROLE, upgrader);␊ + self.accesscontrol._grant_role(ERC2981Component::ROYALTY_ADMIN_ROLE, royalty_admin);␊ + }␊ + ␊ + impl ERC2981ImmutableConfig of ERC2981Component::ImmutableConfig {␊ + const FEE_DENOMINATOR: u128 = 100000;␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.accesscontrol.assert_only_role(UPGRADER_ROLE);␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + ## full non-upgradeable > Snapshot 1 @@ -828,6 +1299,8 @@ Generated by [AVA](https://avajs.dev). use openzeppelin::governance::votes::VotesComponent;␊ use openzeppelin::introspection::src5::SRC5Component;␊ use openzeppelin::security::pausable::PausableComponent;␊ + use openzeppelin::token::common::erc2981::DefaultConfig;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ use openzeppelin::token::erc721::ERC721Component;␊ use openzeppelin::token::erc721::extensions::ERC721EnumerableComponent;␊ use openzeppelin::utils::cryptography::nonces::NoncesComponent;␊ @@ -840,6 +1313,7 @@ Generated by [AVA](https://avajs.dev). component!(path: PausableComponent, storage: pausable, event: PausableEvent);␊ component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ component!(path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ component!(path: NoncesComponent, storage: nonces, event: NoncesEvent);␊ component!(path: VotesComponent, storage: votes, event: VotesEvent);␊ ␊ @@ -852,6 +1326,12 @@ Generated by [AVA](https://avajs.dev). #[abi(embed_v0)]␊ impl ERC721EnumerableImpl = ERC721EnumerableComponent::ERC721EnumerableImpl;␊ #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminOwnableImpl = ERC2981Component::ERC2981AdminOwnableImpl;␊ + #[abi(embed_v0)]␊ impl NoncesImpl = NoncesComponent::NoncesImpl;␊ #[abi(embed_v0)]␊ impl VotesImpl = VotesComponent::VotesImpl;␊ @@ -860,6 +1340,7 @@ Generated by [AVA](https://avajs.dev). impl PausableInternalImpl = PausableComponent::InternalImpl;␊ impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ impl VotesInternalImpl = VotesComponent::InternalImpl;␊ ␊ #[storage]␊ @@ -875,6 +1356,8 @@ Generated by [AVA](https://avajs.dev). #[substorage(v0)]␊ erc721_enumerable: ERC721EnumerableComponent::Storage,␊ #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + #[substorage(v0)]␊ nonces: NoncesComponent::Storage,␊ #[substorage(v0)]␊ votes: VotesComponent::Storage,␊ @@ -894,16 +1377,23 @@ Generated by [AVA](https://avajs.dev). #[flat]␊ ERC721EnumerableEvent: ERC721EnumerableComponent::Event,␊ #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + #[flat]␊ NoncesEvent: NoncesComponent::Event,␊ #[flat]␊ VotesEvent: VotesComponent::Event,␊ }␊ ␊ #[constructor]␊ - fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + fn constructor(␊ + ref self: ContractState,␊ + owner: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + ) {␊ self.erc721.initializer("MyToken", "MTK", "");␊ self.ownable.initializer(owner);␊ self.erc721_enumerable.initializer();␊ + self.erc2981.initializer(default_royalty_receiver, 500);␊ }␊ ␊ impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait {␊ @@ -1301,6 +1791,8 @@ Generated by [AVA](https://avajs.dev). use openzeppelin::governance::votes::VotesComponent;␊ use openzeppelin::introspection::src5::SRC5Component;␊ use openzeppelin::security::pausable::PausableComponent;␊ + use openzeppelin::token::common::erc2981::DefaultConfig;␊ + use openzeppelin::token::common::erc2981::ERC2981Component;␊ use openzeppelin::token::erc721::ERC721Component;␊ use openzeppelin::token::erc721::extensions::ERC721EnumerableComponent;␊ use openzeppelin::upgrades::UpgradeableComponent;␊ @@ -1317,6 +1809,7 @@ Generated by [AVA](https://avajs.dev). component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ component!(path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent);␊ component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: ERC2981Component, storage: erc2981, event: ERC2981Event);␊ component!(path: NoncesComponent, storage: nonces, event: NoncesEvent);␊ component!(path: VotesComponent, storage: votes, event: VotesEvent);␊ ␊ @@ -1329,6 +1822,12 @@ Generated by [AVA](https://avajs.dev). #[abi(embed_v0)]␊ impl ERC721EnumerableImpl = ERC721EnumerableComponent::ERC721EnumerableImpl;␊ #[abi(embed_v0)]␊ + impl ERC2981Impl = ERC2981Component::ERC2981Impl;␊ + #[abi(embed_v0)]␊ + impl ERC2981InfoImpl = ERC2981Component::ERC2981InfoImpl;␊ + #[abi(embed_v0)]␊ + impl ERC2981AdminOwnableImpl = ERC2981Component::ERC2981AdminOwnableImpl;␊ + #[abi(embed_v0)]␊ impl NoncesImpl = NoncesComponent::NoncesImpl;␊ #[abi(embed_v0)]␊ impl VotesImpl = VotesComponent::VotesImpl;␊ @@ -1338,6 +1837,7 @@ Generated by [AVA](https://avajs.dev). impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl;␊ impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl ERC2981InternalImpl = ERC2981Component::InternalImpl;␊ impl VotesInternalImpl = VotesComponent::InternalImpl;␊ ␊ #[storage]␊ @@ -1355,6 +1855,8 @@ Generated by [AVA](https://avajs.dev). #[substorage(v0)]␊ upgradeable: UpgradeableComponent::Storage,␊ #[substorage(v0)]␊ + erc2981: ERC2981Component::Storage,␊ + #[substorage(v0)]␊ nonces: NoncesComponent::Storage,␊ #[substorage(v0)]␊ votes: VotesComponent::Storage,␊ @@ -1376,16 +1878,23 @@ Generated by [AVA](https://avajs.dev). #[flat]␊ UpgradeableEvent: UpgradeableComponent::Event,␊ #[flat]␊ + ERC2981Event: ERC2981Component::Event,␊ + #[flat]␊ NoncesEvent: NoncesComponent::Event,␊ #[flat]␊ VotesEvent: VotesComponent::Event,␊ }␊ ␊ #[constructor]␊ - fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + fn constructor(␊ + ref self: ContractState,␊ + owner: ContractAddress,␊ + default_royalty_receiver: ContractAddress,␊ + ) {␊ self.erc721.initializer("MyToken", "MTK", "");␊ self.ownable.initializer(owner);␊ self.erc721_enumerable.initializer();␊ + self.erc2981.initializer(default_royalty_receiver, 500);␊ }␊ ␊ impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait {␊ diff --git a/packages/core-cairo/src/erc721.test.ts.snap b/packages/core-cairo/src/erc721.test.ts.snap index 34aee36db..af7502f5e 100644 Binary files a/packages/core-cairo/src/erc721.test.ts.snap and b/packages/core-cairo/src/erc721.test.ts.snap differ diff --git a/packages/core-cairo/src/erc721.ts b/packages/core-cairo/src/erc721.ts index 9d0a32c1a..619176cf3 100644 --- a/packages/core-cairo/src/erc721.ts +++ b/packages/core-cairo/src/erc721.ts @@ -12,6 +12,7 @@ import { addSRC5Component, addVotesComponent } from './common-components'; import { externalTrait } from './external-trait'; import { toByteArray, toFelt252 } from './utils/convert-strings'; import { OptionsError } from './error'; +import { RoyaltyInfoOptions, setRoyaltyInfo, defaults as royaltyInfoDefaults } from './set-royalty-info'; export const defaults: Required = { name: 'MyToken', @@ -22,6 +23,7 @@ export const defaults: Required = { mintable: false, enumerable: false, votes: false, + royaltyInfo: royaltyInfoDefaults, appName: '', // Defaults to empty string, but user must provide a non-empty value if votes are enabled appVersion: 'v1', access: commonDefaults.access, @@ -42,6 +44,7 @@ export interface ERC721Options extends CommonContractOptions { mintable?: boolean; enumerable?: boolean; votes?: boolean; + royaltyInfo?: RoyaltyInfoOptions; appName?: string; appVersion?: string; } @@ -55,6 +58,7 @@ function withDefaults(opts: ERC721Options): Required { pausable: opts.pausable ?? defaults.pausable, mintable: opts.mintable ?? defaults.mintable, enumerable: opts.enumerable ?? defaults.enumerable, + royaltyInfo: opts.royaltyInfo ?? defaults.royaltyInfo, votes: opts.votes ?? defaults.votes, appName: opts.appName ?? defaults.appName, appVersion: opts.appVersion ?? defaults.appVersion @@ -62,7 +66,7 @@ function withDefaults(opts: ERC721Options): Required { } export function isAccessControlRequired(opts: Partial): boolean { - return opts.mintable === true || opts.pausable === true || opts.upgradeable === true; + return opts.mintable === true || opts.pausable === true || opts.upgradeable === true || opts.royaltyInfo?.enabled === true; } export function buildERC721(opts: ERC721Options): Contract { @@ -92,6 +96,8 @@ export function buildERC721(opts: ERC721Options): Contract { setAccessControl(c, allOpts.access); setUpgradeable(c, allOpts.upgradeable, allOpts.access); setInfo(c, allOpts.info); + setRoyaltyInfo(c, allOpts.royaltyInfo, allOpts.access); + addHooks(c, allOpts); return c; diff --git a/packages/core-cairo/src/generate/erc1155.ts b/packages/core-cairo/src/generate/erc1155.ts index bb5b55b34..07170fcbd 100644 --- a/packages/core-cairo/src/generate/erc1155.ts +++ b/packages/core-cairo/src/generate/erc1155.ts @@ -2,6 +2,7 @@ import type { ERC1155Options } from '../erc1155'; import { accessOptions } from '../set-access-control'; import { infoOptions } from '../set-info'; import { upgradeableOptions } from '../set-upgradeable'; +import { royaltyInfoOptions } from '../set-royalty-info'; import { generateAlternatives } from './alternatives'; const booleans = [true, false]; @@ -13,8 +14,9 @@ const blueprint = { pausable: booleans, mintable: booleans, updatableUri: booleans, - access: accessOptions, upgradeable: upgradeableOptions, + royaltyInfo: [royaltyInfoOptions.disabled, royaltyInfoOptions.enabledDefault, royaltyInfoOptions.enabledCustom], + access: accessOptions, info: infoOptions, }; diff --git a/packages/core-cairo/src/generate/erc721.ts b/packages/core-cairo/src/generate/erc721.ts index 99f97a120..e78a94f15 100644 --- a/packages/core-cairo/src/generate/erc721.ts +++ b/packages/core-cairo/src/generate/erc721.ts @@ -2,6 +2,7 @@ import type { ERC721Options } from '../erc721'; import { accessOptions } from '../set-access-control'; import { infoOptions } from '../set-info'; import { upgradeableOptions } from '../set-upgradeable'; +import { royaltyInfoOptions } from '../set-royalty-info'; import { generateAlternatives } from './alternatives'; const booleans = [true, false]; @@ -17,6 +18,7 @@ const blueprint = { appVersion: ['v1'], pausable: booleans, mintable: booleans, + royaltyInfo: [royaltyInfoOptions.disabled, royaltyInfoOptions.enabledDefault, royaltyInfoOptions.enabledCustom], access: accessOptions, upgradeable: upgradeableOptions, info: infoOptions, diff --git a/packages/core-cairo/src/index.ts b/packages/core-cairo/src/index.ts index 448e90c47..7551f94aa 100644 --- a/packages/core-cairo/src/index.ts +++ b/packages/core-cairo/src/index.ts @@ -10,10 +10,12 @@ export type { Access } from './set-access-control'; export type { Account } from './account'; export type { Upgradeable } from './set-upgradeable'; export type { Info } from './set-info'; +export type { RoyaltyInfoOptions } from './set-royalty-info'; export { premintPattern } from './erc20'; export { defaults as infoDefaults } from './set-info'; +export { defaults as royaltyInfoDefaults } from './set-royalty-info'; export type { OptionsErrorMessages } from './error'; export { OptionsError } from './error'; diff --git a/packages/core-cairo/src/print.ts b/packages/core-cairo/src/print.ts index 827919a89..153521e00 100644 --- a/packages/core-cairo/src/print.ts +++ b/packages/core-cairo/src/print.ts @@ -37,26 +37,29 @@ function withSemicolons(lines: string[]): string[] { return lines.map(line => line.endsWith(';') ? line : line + ';'); } -function printSuperVariables(contract: Contract) { +function printSuperVariables(contract: Contract): string[] { return withSemicolons(contract.superVariables.map(v => `const ${v.name}: ${v.type} = ${v.value}`)); } -function printImports(contract: Contract) { +function printImports(contract: Contract): string[] { const lines: string[] = []; sortImports(contract).forEach(i => lines.push(`use ${i}`)); return withSemicolons(lines); } -function sortImports(contract: Contract) { +function sortImports(contract: Contract): string[] { const componentImports = contract.components.flatMap(c => `${c.path}::${c.name}`); const allImports = componentImports.concat(contract.standaloneImports); - if (contract.superVariables.length > 0) { - allImports.push(`super::{${contract.superVariables.map(v => v.name).join(', ')}}`); + const superVars = contract.superVariables; + if (superVars.length === 1) { + allImports.push(`super::${superVars[0]!.name}`); + } else if (superVars.length > 1) { + allImports.push(`super::{${superVars.map(v => v.name).join(', ')}}`); } return allImports.sort(); } -function printComponentDeclarations(contract: Contract) { +function printComponentDeclarations(contract: Contract): string[] { const lines = []; for (const component of contract.components) { lines.push(`component!(path: ${component.name}, storage: ${component.substorage.name}, event: ${component.event.name});`); @@ -64,7 +67,7 @@ function printComponentDeclarations(contract: Contract) { return lines; } -function printImpls(contract: Contract) { +function printImpls(contract: Contract): Lines[] { const externalImpls = contract.components.flatMap(c => c.impls); const internalImpls = contract.components.flatMap(c => c.internalImpl ? [c.internalImpl] : []); @@ -74,7 +77,7 @@ function printImpls(contract: Contract) { ); } -function printImpl(impl: Impl, internal = false) { +function printImpl(impl: Impl, internal = false): string[] { const lines = []; if (!internal) { lines.push('#[abi(embed_v0)]'); @@ -83,7 +86,7 @@ function printImpl(impl: Impl, internal = false) { return lines; } -function printStorage(contract: Contract) { +function printStorage(contract: Contract): (string | string[])[] { const lines = []; // storage is required regardless of whether there are components lines.push('#[storage]'); @@ -98,7 +101,7 @@ function printStorage(contract: Contract) { return lines; } -function printEvents(contract: Contract) { +function printEvents(contract: Contract): (string | string[])[] { const lines = []; if (contract.components.length > 0) { lines.push('#[event]'); @@ -115,7 +118,7 @@ function printEvents(contract: Contract) { return lines; } -function printImplementedTraits(contract: Contract) { +function printImplementedTraits(contract: Contract): Lines[] { const impls = []; // sort first by priority, then number of tags, then name @@ -133,15 +136,22 @@ function printImplementedTraits(contract: Contract) { const implLines = []; implLines.push(...trait.tags.map(t => `#[${t}]`)); implLines.push(`impl ${trait.name} of ${trait.of} {`); + + const superVars = withSemicolons( + trait.superVariables.map(v => `const ${v.name}: ${v.type} = ${v.value}`) + ); + implLines.push(superVars); + const fns = trait.functions.map(fn => printFunction(fn)); implLines.push(spaceBetween(...fns)); + implLines.push('}'); impls.push(implLines); } return spaceBetween(...impls); } -function printFunction(fn: ContractFunction) { +function printFunction(fn: ContractFunction): Lines[] { const head = `fn ${fn.name}`; const args = fn.args.map(a => printArgument(a)); @@ -191,7 +201,7 @@ function printConstructor(contract: Contract): Lines[] { } } -function hasInitializer(parent: Component) { +function hasInitializer(parent: Component): boolean { return parent.initializer !== undefined && parent.substorage?.name !== undefined; } @@ -220,6 +230,8 @@ export function printValue(value: Value): string { } else { throw new Error(`Number not representable (${value})`); } + } else if (typeof value === 'bigint') { + return `${value}` } else { return `"${value}"`; } @@ -227,7 +239,14 @@ export function printValue(value: Value): string { // generic for functions and constructors // kindedName = 'fn foo' -function printFunction2(kindedName: string, args: string[], tag: string | undefined, returns: string | undefined, returnLine: string | undefined, code: Lines[]): Lines[] { +function printFunction2( + kindedName: string, + args: string[], + tag: string | undefined, + returns: string | undefined, + returnLine: string | undefined, + code: Lines[] +): Lines[] { const fn = []; if (tag !== undefined) { diff --git a/packages/core-cairo/src/set-access-control.ts b/packages/core-cairo/src/set-access-control.ts index bdaca0f3c..328ad3377 100644 --- a/packages/core-cairo/src/set-access-control.ts +++ b/packages/core-cairo/src/set-access-control.ts @@ -3,13 +3,14 @@ import { defineComponents } from './utils/define-components'; import { addSRC5Component } from './common-components'; export const accessOptions = [false, 'ownable', 'roles'] as const; +export const DEFAULT_ACCESS_CONTROL = 'ownable'; export type Access = typeof accessOptions[number]; /** * Sets access control for the contract by adding inheritance. */ - export function setAccessControl(c: ContractBuilder, access: Access) { + export function setAccessControl(c: ContractBuilder, access: Access): void { switch (access) { case 'ownable': { c.addComponent(components.OwnableComponent, [{ lit: 'owner' }], true); @@ -53,11 +54,17 @@ export type Access = typeof accessOptions[number]; /** * Enables access control for the contract and restricts the given function with access control. */ -export function requireAccessControl(c: ContractBuilder, trait: BaseImplementedTrait, fn: BaseFunction, access: Access, roleIdPrefix: string, roleOwner: string | undefined) { +export function requireAccessControl( + c: ContractBuilder, + trait: BaseImplementedTrait, + fn: BaseFunction, + access: Access, + roleIdPrefix: string, + roleOwner: string | undefined +): void { if (access === false) { - access = 'ownable'; + access = DEFAULT_ACCESS_CONTROL; } - setAccessControl(c, access); switch (access) { diff --git a/packages/core-cairo/src/set-info.ts b/packages/core-cairo/src/set-info.ts index 09d72552a..421a15029 100644 --- a/packages/core-cairo/src/set-info.ts +++ b/packages/core-cairo/src/set-info.ts @@ -8,7 +8,7 @@ export type Info = { license?: string; } -export function setInfo(c: ContractBuilder, info: Info) { +export function setInfo(c: ContractBuilder, info: Info): void { const { license } = info; if (license) { diff --git a/packages/core-cairo/src/set-royalty-info.ts b/packages/core-cairo/src/set-royalty-info.ts new file mode 100644 index 000000000..0a7ef470f --- /dev/null +++ b/packages/core-cairo/src/set-royalty-info.ts @@ -0,0 +1,130 @@ +import type { BaseImplementedTrait, ContractBuilder } from './contract'; +import { defineComponents } from './utils/define-components'; +import { OptionsError } from "./error"; +import { toUint } from './utils/convert-strings'; +import { Access, setAccessControl, DEFAULT_ACCESS_CONTROL } from './set-access-control'; + +const DEFAULT_FEE_DENOMINATOR = BigInt(10_000); + +export const defaults: RoyaltyInfoOptions = { + enabled: false, + defaultRoyaltyFraction: '0', + feeDenominator: DEFAULT_FEE_DENOMINATOR.toString() +}; + +export const royaltyInfoOptions = { + disabled: defaults, + enabledDefault: { + enabled: true, + defaultRoyaltyFraction: '500', + feeDenominator: DEFAULT_FEE_DENOMINATOR.toString(), + }, + enabledCustom: { + enabled: true, + defaultRoyaltyFraction: '15125', + feeDenominator: '100000', + } +} + +export type RoyaltyInfoOptions = { + enabled: boolean, + defaultRoyaltyFraction: string, + feeDenominator: string, +}; + +export function setRoyaltyInfo(c: ContractBuilder, options: RoyaltyInfoOptions, access: Access): void { + if (!options.enabled) { + return; + } + if (access === false) { + access = DEFAULT_ACCESS_CONTROL; + } + setAccessControl(c, access); + + const { defaultRoyaltyFraction, feeDenominator } = getRoyaltyParameters(options); + const initParams = [ + { lit: 'default_royalty_receiver' }, + defaultRoyaltyFraction + ]; + + c.addComponent(components.ERC2981Component, initParams, true); + c.addStandaloneImport('starknet::ContractAddress'); + c.addConstructorArgument({ name: 'default_royalty_receiver', type: 'ContractAddress'}); + + switch (access) { + case 'ownable': + c.addImplToComponent(components.ERC2981Component, { + name: 'ERC2981AdminOwnableImpl', + value: `ERC2981Component::ERC2981AdminOwnableImpl`, + }); + break; + case 'roles': + c.addImplToComponent(components.ERC2981Component, { + name: 'ERC2981AdminAccessControlImpl', + value: `ERC2981Component::ERC2981AdminAccessControlImpl`, + }); + c.addConstructorArgument({ name: 'royalty_admin', type: 'ContractAddress'}); + c.addConstructorCode('self.accesscontrol._grant_role(ERC2981Component::ROYALTY_ADMIN_ROLE, royalty_admin)'); + break; + } + + if (feeDenominator === DEFAULT_FEE_DENOMINATOR) { + c.addStandaloneImport('openzeppelin::token::common::erc2981::DefaultConfig'); + } else { + const trait: BaseImplementedTrait = { + name: 'ERC2981ImmutableConfig', + of: 'ERC2981Component::ImmutableConfig', + tags: [], + }; + c.addImplementedTrait(trait); + c.addSuperVariableToTrait(trait, { + name: 'FEE_DENOMINATOR', + type: 'u128', + value: feeDenominator.toString() + }); + } +} + +function getRoyaltyParameters(opts: Required): { defaultRoyaltyFraction: bigint, feeDenominator: bigint } { + const feeDenominator = toUint(opts.feeDenominator, 'feeDenominator', 'u128'); + if (feeDenominator === BigInt(0)) { + throw new OptionsError({ + feeDenominator: 'Must be greater than 0' + }); + } + const defaultRoyaltyFraction = toUint(opts.defaultRoyaltyFraction, 'defaultRoyaltyFraction', 'u128'); + if (defaultRoyaltyFraction > feeDenominator) { + throw new OptionsError({ + defaultRoyaltyFraction: 'Cannot be greater than fee denominator' + }); + } + return { defaultRoyaltyFraction, feeDenominator }; +} + +const components = defineComponents({ + ERC2981Component: { + path: 'openzeppelin::token::common::erc2981', + substorage: { + name: 'erc2981', + type: 'ERC2981Component::Storage', + }, + event: { + name: 'ERC2981Event', + type: 'ERC2981Component::Event', + }, + impls: [ + { + name: 'ERC2981Impl', + value: 'ERC2981Component::ERC2981Impl', + }, + { + name: 'ERC2981InfoImpl', + value: 'ERC2981Component::ERC2981InfoImpl', + } + ], + internalImpl: { + name: 'ERC2981InternalImpl', + value: 'ERC2981Component::InternalImpl', + }, + }, +}); diff --git a/packages/core-cairo/src/set-upgradeable.ts b/packages/core-cairo/src/set-upgradeable.ts index 3a94b872b..a419439d8 100644 --- a/packages/core-cairo/src/set-upgradeable.ts +++ b/packages/core-cairo/src/set-upgradeable.ts @@ -33,14 +33,14 @@ function setUpgradeableBase(c: ContractBuilder, upgradeable: Upgradeable): BaseI return t; } -export function setUpgradeable(c: ContractBuilder, upgradeable: Upgradeable, access: Access) { +export function setUpgradeable(c: ContractBuilder, upgradeable: Upgradeable, access: Access): void { const trait = setUpgradeableBase(c, upgradeable); if (trait !== undefined) { requireAccessControl(c, trait, functions.upgrade, access, 'UPGRADER', 'upgrader'); } } -export function setAccountUpgradeable(c: ContractBuilder, upgradeable: Upgradeable, type: Account) { +export function setAccountUpgradeable(c: ContractBuilder, upgradeable: Upgradeable, type: Account): void { const trait = setUpgradeableBase(c, upgradeable); if (trait !== undefined) { switch (type) { diff --git a/packages/core-cairo/src/utils/convert-strings.ts b/packages/core-cairo/src/utils/convert-strings.ts index fe8369bc2..75cc1758a 100644 --- a/packages/core-cairo/src/utils/convert-strings.ts +++ b/packages/core-cairo/src/utils/convert-strings.ts @@ -45,4 +45,45 @@ export function toFelt252(str: string, field: string): string { } else { return result; } -} \ No newline at end of file +} + +function maxValueOfUint(bits: number): bigint { + if (bits <= 0) { + throw new Error(`Number of bits must be positive (actual '${bits}').`) + } + if (bits % 8 !== 0) { + throw new Error(`The number of bits must be a multiple of 8 (actual '${bits}').`) + } + const bytes = bits / 8; + return BigInt('0x' + 'ff'.repeat(bytes)) +} + +const UINT_MAX_VALUES = { + 'u8': maxValueOfUint(8), + 'u16': maxValueOfUint(16), + 'u32': maxValueOfUint(32), + 'u64': maxValueOfUint(64), + 'u128': maxValueOfUint(128), + 'u256': maxValueOfUint(256) +} as const; + +export type UintType = keyof typeof UINT_MAX_VALUES; + +/** + * Validates a string value to be a valid uint and converts it to bigint + */ +export function toUint(str: string, field: string, type: UintType): bigint { + const isValidNumber = /^\d+$/.test(str); + if (!isValidNumber) { + throw new OptionsError({ + [field]: 'Not a valid number' + }); + } + const numValue = BigInt(str); + if (numValue > UINT_MAX_VALUES[type]) { + throw new OptionsError({ + [field]: `Value is greater than ${type} max value` + }); + } + return numValue; +} diff --git a/packages/ui/src/cairo/App.svelte b/packages/ui/src/cairo/App.svelte index ba1956cce..f80714492 100644 --- a/packages/ui/src/cairo/App.svelte +++ b/packages/ui/src/cairo/App.svelte @@ -160,7 +160,7 @@
- +
diff --git a/packages/ui/src/cairo/ERC1155Controls.svelte b/packages/ui/src/cairo/ERC1155Controls.svelte index 13c528ea1..a292405ee 100644 --- a/packages/ui/src/cairo/ERC1155Controls.svelte +++ b/packages/ui/src/cairo/ERC1155Controls.svelte @@ -1,11 +1,12 @@ @@ -70,6 +73,8 @@
+ + diff --git a/packages/ui/src/cairo/ERC721Controls.svelte b/packages/ui/src/cairo/ERC721Controls.svelte index 1d78ffe78..62f786016 100644 --- a/packages/ui/src/cairo/ERC721Controls.svelte +++ b/packages/ui/src/cairo/ERC721Controls.svelte @@ -6,6 +6,7 @@ import AccessControlSection from './AccessControlSection.svelte'; import UpgradeabilityField from './UpgradeabilityField.svelte'; + import RoyaltyInfoSection from './RoyaltyInfoSection.svelte'; import InfoSection from './InfoSection.svelte'; import { error } from '../error-tooltip'; @@ -110,6 +111,8 @@ + + \ No newline at end of file diff --git a/packages/ui/src/cairo/RoyaltyInfoSection.svelte b/packages/ui/src/cairo/RoyaltyInfoSection.svelte new file mode 100644 index 000000000..1b225bc08 --- /dev/null +++ b/packages/ui/src/cairo/RoyaltyInfoSection.svelte @@ -0,0 +1,42 @@ + + +
+

+ + +

+ + + + +
diff --git a/packages/ui/src/cairo/inject-hyperlinks.ts b/packages/ui/src/cairo/inject-hyperlinks.ts index c707141b3..0998a203e 100644 --- a/packages/ui/src/cairo/inject-hyperlinks.ts +++ b/packages/ui/src/cairo/inject-hyperlinks.ts @@ -33,7 +33,7 @@ function removeComponentName(libraryPathSegments: Array) { const lastItem = libraryPathSegments[libraryPathSegments.length - 1]; if (lastItem !== undefined && componentMappings[lastItem] !== undefined) { // Replace component name with the name of its .cairo file - libraryPathSegments.splice(-1, 1, componentMappings[lastItem]); + libraryPathSegments.splice(-1, 1, componentMappings[lastItem] as string); } else { libraryPathSegments.pop(); } diff --git a/packages/ui/src/main.ts b/packages/ui/src/main.ts index e2a9cba37..3cb12a50a 100644 --- a/packages/ui/src/main.ts +++ b/packages/ui/src/main.ts @@ -8,7 +8,7 @@ import UnsupportedVersion from './UnsupportedVersion.svelte'; import semver from 'semver'; import { compatibleContractsSemver as compatibleSolidityContractsSemver } from '@openzeppelin/wizard'; import { compatibleContractsSemver as compatibleCairoContractsSemver } from '@openzeppelin/wizard-cairo'; -import { InitialOptions } from './initial-options'; +import type { InitialOptions } from './initial-options'; function postResize() { const { height } = document.documentElement.getBoundingClientRect();