├── .env.example ├── .gitignore ├── .gitmodules ├── .openzeppelin ├── goerli.json └── unknown-31337.json ├── LICENSE ├── README.md ├── ethereum ├── contracts │ ├── CandyMachine.sol │ ├── CandyMachineFactory.sol │ ├── Contract.sol │ ├── ExampleERC1155.sol │ ├── ExampleERC1155Copyable.sol │ ├── ExampleERC1155Mintable.sol │ ├── ExampleERC20Mintable.sol │ ├── ExampleERC4907.sol │ ├── ExampleERC721Copyable.sol │ ├── ExampleERC721Mintable.sol │ ├── RentableWrapper.sol │ └── VRFCoordinatorV2Mock.sol ├── interfaces │ ├── ICandyMachine.sol │ ├── ICandyMachineFactory.sol │ ├── IERC1155CopyableUpgradeable.sol │ ├── IERC4907Upgradeable.sol │ └── IERC721CopyableUpgradeable.sol ├── scripts │ ├── deploy_contract.js │ ├── deploy_erc1155_contract.js │ ├── deploy_erc1155_copyable_contract.js │ ├── deploy_erc4907_example_contract.js │ ├── deploy_erc721_copyable_contract.js │ ├── deploy_rentable_wrapper_contract.js │ ├── upgrade_candy_machine_factory.js │ ├── upgrade_contract.js │ ├── upgrade_erc1155_copyable_contract.js │ ├── upgrade_erc4907_example_contract.js │ ├── upgrade_erc721_copyable_contract.js │ └── upgrade_rentable_wrapper_contract.js └── test │ ├── 1_RentableWrapper-test.js │ ├── 2_CandyMachine-test.js │ ├── 3_CandyMachineFactory-test.js │ └── 4_Contract-test.js ├── hardhat.config.js ├── out └── CandyMachineFactory_flat.sol ├── package-lock.json ├── package.json └── solana ├── .gitignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── package-lock.json ├── package.json ├── packages └── sdk │ ├── package.json │ ├── src │ ├── cupcake_program.ts │ ├── instructions.ts │ ├── pda.ts │ ├── tsconfig.json │ └── utils │ │ ├── mpl.ts │ │ ├── solana.ts │ │ └── transaction.ts │ └── yarn.lock ├── programs └── cupcake │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ ├── errors.rs │ ├── instructions │ ├── accept_offer │ │ └── mod.rs │ ├── bake_sprinkle │ │ └── mod.rs │ ├── cancel_offer │ │ └── mod.rs │ ├── claim_bought_nft │ │ └── mod.rs │ ├── claim_sprinkle │ │ └── mod.rs │ ├── create_bakery.rs │ ├── delete_listing │ │ └── mod.rs │ ├── make_offer │ │ └── mod.rs │ ├── mod.rs │ ├── modify_listing │ │ └── mod.rs │ └── toggle_vault_nft │ │ └── mod.rs │ ├── lib.rs │ ├── state │ ├── bakery.rs │ ├── marketplace.rs │ ├── mod.rs │ ├── sprinkle.rs │ └── user_info.rs │ └── utils.rs ├── tests ├── marketplace │ └── test.ts ├── programmable │ ├── amount.ts │ ├── noRuleset.ts │ ├── pass.ts │ ├── programOwned.ts │ └── pubkeyMatch.ts └── refillable │ └── refillable1Of1.ts ├── tsconfig.json ├── wip_sdk ├── cucpakeProgram.ts ├── index.ts ├── programmableAssets.ts └── state │ ├── bakery.ts │ ├── sprinkle.ts │ └── userInfo.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | INFURA_API_KEY= 2 | ETHERSCAN_API_KEY= 3 | PRI_KEY= 4 | PRI_KEY_2= 5 | PROXY_ADDR_CONTRACT= 6 | PROXY_ADDR_ERC4907_EXAMPLE= 7 | PROXY_ADDR_ERC721_COPYABLE_EXAMPLE= 8 | PROXY_ADDR_ERC1155_COPYABLE_EXAMPLE= 9 | PROXY_ADDR_RENTABLE_WRAPPER= 10 | PROXY_ADDR_CANDY_MACHINE_FACTORY= 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | id.json 3 | debug/ 4 | target/ 5 | .DS_Store 6 | **/.DS_Store 7 | .anchor/ 8 | 9 | node_modules/ 10 | **/*.rs.bk 11 | build/ 12 | dist/ 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | ./package-lock.json 18 | 19 | .env 20 | .vscode 21 | .idea 22 | .anchor 23 | .crates/ 24 | 25 | .pnp.* 26 | .yarn/* 27 | !.yarn/patches 28 | !.yarn/plugins 29 | !.yarn/releases 30 | !.yarn/sdks 31 | !.yarn/versions 32 | .vercel 33 | 34 | ethereum/artifacts 35 | ethereum/cache 36 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "solana/site"] 2 | path = solana/site 3 | url = git@github.com:cupcake/site.git 4 | branch = new-market-site 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧁 Cupcake Solana 2 | 3 | NPM module is "cupcake-cli", represents what is in the CLI folder 4 | 5 | # 🧁 Cupcake Ethereum 6 | 7 | Cupcake provides six unique asset distribution schemes for making both non-fungible and fungible assets claimable via physical NFC tags. 8 | 9 | ## Ethereum Contract Architecture 10 | 11 | ### Internal Storage Structure 12 | 13 | ``` 14 | enum TagType { 15 | // 16 | // Each claimable NFT is a copy of the master NFT, up to the preset total supply 17 | // NOTE: This is implemented via a modified version of the ERC-1155 standard (see IERC1155CopyableUpgradeable) 18 | // 19 | LimitedOrOpenEdition, 20 | // 21 | // Only one claimable NFT, always with a supply of 1 (a tag can never be refilled or reused) 22 | // 23 | SingleUse1Of1, 24 | // 25 | // One claimable NFT, that can be continually refilled 26 | // 27 | Refillable1Of1, 28 | // 29 | // Claimable fungible tokens (claimed based on an amount per user), up to the preset total supply 30 | // 31 | WalletRestrictedFungible, 32 | // 33 | // Only one NFT that is temporarily held by the claimer, "renter" status is transferred after each claim 34 | // NOTE: This is implemented via the ERC-4907 "Rentable" standard 35 | // 36 | HotPotato, 37 | // 38 | // Each claimable NFT is randomly selected from a predefined set of metadata URIs. 39 | // NOTE: This is implemented via a contract factory pattern (see CandyMachine and CandyMachineFactory) 40 | // 41 | CandyMachineDrop 42 | } 43 | 44 | struct Tag { 45 | // 46 | // The enum type of the tag (from the above) 47 | // 48 | TagType tagType; 49 | // 50 | // The address of the ERC-1155, ERC-721 or ERC-20 compliant claimable token, or the CandyMachine contract address 51 | // 52 | address assetAddress; 53 | // 54 | // The token ID of the NFT claimable token (only used for non-fungible claims) 55 | // 56 | uint256 erc721TokenId; 57 | // 58 | // The address that must have signed the transaction for a claim transaction to be valid 59 | // 60 | address tagAuthority; 61 | // 62 | // Indicates the total claimable supply of the token available 63 | // 64 | uint256 totalSupply; 65 | // 66 | // Indicates the total claimable supply of the token available per user 67 | // 68 | uint256 perUser; 69 | // 70 | // Indicates the amount of fungible token to make claimable per claim (only for fungible claims) 71 | // 72 | uint256 fungiblePerClaim; 73 | // 74 | // The unique uid from the NFC tag 75 | // 76 | uint256 uid; 77 | // 78 | // Indicates the total number of token claims made so far 79 | // 80 | uint256 numClaimed; 81 | // 82 | // Indicates the number of token claims made by address so far 83 | // 84 | mapping ( 85 | address => uint256 86 | ) claimsMade; 87 | } 88 | 89 | mapping ( 90 | // tag's unique identifier, see the hashUniqueTag() function to understand how the bytes32 is generated. 91 | bytes32 => Tag 92 | ) public tags; 93 | 94 | // The following is used pass this data into addOrRefillTag function without exceeding the parameter limit 95 | struct TagPassed { 96 | TagType tagType; 97 | address assetAddress; 98 | uint256 erc721TokenId; 99 | address tagAuthority; 100 | uint256 totalSupply; 101 | uint256 perUser; 102 | uint256 fungiblePerClaim; 103 | uint256 uid; 104 | } 105 | ``` 106 | 107 | ### Interface 108 | 109 | ``` 110 | interface Contract { 111 | /* 112 | * @notice Add or refill assets that can be claimed for a specified tag 113 | */ 114 | function addOrRefillTag( 115 | TagPassed calldata passedTag, 116 | // Indicates if the claimable asset is an NFT that does not support the ERC-1155 standard 117 | // NOTE: Relevent when `tagType` is not HotPotato or CandyMachineDrop 118 | bool isNotErc1155, 119 | // Indicates the metadata URIs to use for the new NFT assets in CandyMachine 120 | // NOTE: Relevent only when `tagType` is CandyMachineDrop 121 | string[] calldata metadataURIs 122 | ) external onlyOwner; 123 | 124 | /* 125 | * @notice Claim an asset for a specified tag 126 | * @returns address representing the address of the newly claimed token 127 | * @returns uint256 representing the tokenId of the newly claimed token (if non-fungible) 128 | * @returns uint256 representing the amount of the newly claimed token (if fungible) 129 | */ 130 | function claimTag( 131 | address recipient, 132 | uint256 uid, 133 | // Indicates if the claimable asset is an NFT that does not support the ERC-1155 standard 134 | // NOTE: Relevent when `tagType` is not HotPotato 135 | bool isNotErc1155, 136 | // Indicates the new (copied) asset's token 137 | // NOTE: Relevent when `tagType` is LimitedOrOpenEdition; this value must not already exist in the collection. 138 | uint256 newTokenId 139 | ) external onlyOwner returns(address, uint256, uint256); 140 | 141 | /* 142 | * @notice Cancel and empty a specified tag 143 | */ 144 | function cancelAndEmpty( 145 | uint256 uid, 146 | // Indicates if the claimable asset is an NFT that does not support the ERC-1155 standard 147 | // NOTE: Relevent when `tagType` is not HotPotato 148 | bool isNotErc1155 149 | ) external onlyOwner; 150 | } 151 | ``` 152 | 153 | ### Token Support 154 | 155 | Below are the tag claim distribution schemes along with their associated lowest permitted claimable-token requirements: 156 | 157 | - **SingleUse1Of1**, **Refillable1Of1**, **CandyMachineDrop**: ERC-1155 (or ERC-721 via the `isNotErc1155` parameter.) 158 | - **WalletRestrictedFungible**: ERC-1155 (or ERC-20 via the `isNotErc1155` parameter.) 159 | - **HotPotato**: ERC-4907 160 | - **LimitedOrOpenEdition**: The following interface which extends ERC-1155: 161 | 162 | ``` 163 | interface IERC1155CopyableUpgradeable is IERC1155MetadataURIUpgradeable { 164 | 165 | /// @notice This emits when the an NFT has been copied. 166 | event Copy(address indexed _to, uint256 indexed _tokenIdMaster, uint256 indexed _tokenIdCopy); 167 | 168 | // @notice Mint a new NFT with exactly the same associated metadata (same return value for the `tokenURI()` function) of an existing NFT in this same collection 169 | // @param _to An address to send the duplicated token to 170 | // @param _tokenIdMaster A token ID that we would like to duplicate the metadata of 171 | // @param _tokenIdCopy A token ID that we would like to duplicate the metadata to 172 | // @return uint256 representing the token ID of the newly minted NFT (via this duplication process) 173 | function mintCopy(address to, uint256 tokenIdMaster, uint256 tokenIdCopy) external; 174 | } 175 | ``` 176 | 177 | ## Usage 178 | 179 | ### Compile, Deploy and Upgrade 180 | 181 | First, ensure that you have implemented the `.env` file following the format of the [`.env.example`](/.env.example) file. 182 | 183 | To deploy: 184 | 185 | ``` 186 | env $(cat .env) npx hardhat run --network goerli ethereum/scripts/deploy_contract.js 187 | ``` 188 | 189 | Ensure that the `PROXY_ADDR_CONTRACT` env variable is set properly based on the newly deployed contract. 190 | 191 | To upgrade: 192 | 193 | ``` 194 | env $(cat .env) npx hardhat run --network goerli ethereum/scripts/upgrade_contract.js 195 | ``` 196 | 197 | The same pattern above for deployment and upgrading can be applied across all of the contracts. 198 | 199 | ### Testnet 200 | 201 | #### NFT Tag: SingleUse1Of1 or Refillable1Of1 202 | 203 | 1) Get some test ETH for the Goerli testnet by clicking [here](https://goerlifaucet.com/). 204 | 2) Mint a test NFT by clicking [here](https://goerli.etherscan.io/address/0x39ec448b891c476e166b3c3242a90830db556661#writeContract#F2) then clicking "Connect to Web3" and inputting the following: 205 | - _to: (your wallet address) 206 | - _tokenId: (any random / unique number - it must not have been used before by someone else) 207 | - _uri: (any url, for example: https://google.com) 208 | 3) Approve the NFT token to be taken by the Cupcake Contract by clicking [here](https://goerli.etherscan.io/address/0x39ec448b891c476e166b3c3242a90830db556661#writeContract#F1) then clicking "Connect to Web3" and inputting the following: 209 | - _approved: 0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000 210 | - _tokenId: (the same _tokenId that you entered in the last step) 211 | 4) Add a new Cupcake tag by running the `addOrRefillTag` function and staking the NFT (that we created in the last step) by clicking [here](https://goerli.etherscan.io/address/0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000#writeProxyContract#F1) then clicking "Connect to Web3" and inputting the following: 212 | - tagType: (one of the following: "1" for SingleUse1Of1 or "2" for Refillable1Of1) 213 | - tokenAddress: 0x39ec448b891c476e166b3c3242a90830db556661 214 | - erc721TokenId: (the same _tokenId that you entered in the last two steps) 215 | - tagAuthority: (your wallet address, this address must send the claim transaction in the next step) 216 | - totalSupply: (the maximum number of claims for this tag that you want to permit in total for all users) 217 | - perUser: (the maximum number of claims for this tag that you want to permit for each user) 218 | - fungiblePerClaim: 0 219 | - uid: (any unique number, this must be a number that hasn't been used before by someone else) 220 | - isNotErc1155: true 221 | 4) Claim the new Cupcake tag (that you created in the last step) by running the `claimTag` function by clicking [here](https://goerli.etherscan.io/address/0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000#writeProxyContract#F2) then clicking "Connect to Web3" and inputting the following: 222 | - receiver: (the wallet address that you would like to receive the claimed NFT) 223 | - uid: (this must be the same "uid" that you provided in the previous step) 224 | - isNotErc1155: true 225 | 226 | #### NFT Tag: HotPotato 227 | 228 | 1) Get some test ETH for the Goerli testnet by clicking [here](https://goerlifaucet.com/). 229 | 2) Mint a test **Rentable** NFT by clicking [here](https://goerli.etherscan.io/token/0x68c81B4d8CEA9880a54963E3Ac4133b59C518AaF#writeProxyContract#F3) then clicking "Connect to Web3" and then clicking "Write". Next, while the transaction processes, click the "View your transaction" button. 230 | 3) Approve the NFT token to be taken by the Cupcake Contract by clicking [here](https://goerli.etherscan.io/token/0x68c81B4d8CEA9880a54963E3Ac4133b59C518AaF#writeProxyContract#F1) then clicking "Connect to Web3" and inputting the following: 231 | - to: 0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000 232 | - tokenId: (use the number after the "ERC-721 Token ID" text on the transaction page that you opened at the end of the last step) 233 | 4) Add a new Cupcake tag by running the `addOrRefillTag` function and staking the NFT (that we created in the last step) by clicking [here](https://goerli.etherscan.io/address/0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000#writeProxyContract#F1) then clicking "Connect to Web3" and inputting the following: 234 | - tagType: 4 235 | - tokenAddress: 0x68c81B4d8CEA9880a54963E3Ac4133b59C518AaF 236 | - erc721TokenId: (the same tokenId that you entered in the last step) 237 | - tagAuthority: (your wallet address, this address must send the claim transaction in the next step) 238 | - totalSupply: 1 239 | - perUser: 1 240 | - fungiblePerClaim: 0 241 | - uid: (any unique number, this must be a number that hasn't been used before by someone else) 242 | - isNotErc1155: true 243 | 4) Claim the new Cupcake tag (that you created in the last step) by running the `claimTag` function by clicking [here](https://goerli.etherscan.io/address/0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000#writeProxyContract#F2) then clicking "Connect to Web3" and inputting the following: 244 | - receiver: (the wallet address that you would like to receive the claimed NFT) 245 | - uid: (this must be the same "uid" that you provided in the previous step) 246 | - isNotErc1155: true 247 | 248 | #### NFT Tag: LimitedOrOpenEdition 249 | 250 | 1) Get some test ETH for the Goerli testnet by clicking [here](https://goerlifaucet.com/). 251 | 2) Mint a test **Copyable** NFT by clicking [here](https://goerli.etherscan.io/address/0x6dC5d9edcdD20543dB4788B26301fB7372c4d4EC#writeProxyContract#F3) then clicking "Connect to Web3" and inputting the following: 252 | - to: (your wallet address) 253 | - tokenId: (any random / unique number - it must not have been used before by someone else) 254 | 3) Approve the NFT token to be taken by the Cupcake Contract by clicking [here](https://goerli.etherscan.io/address/0x6dC5d9edcdD20543dB4788B26301fB7372c4d4EC#writeProxyContract#F1) then clicking "Connect to Web3" and inputting the following: 255 | - to: 0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000 256 | - tokenId: (the same tokenId that you entered in the last step) 257 | 4) Add a new Cupcake tag by running the `addOrRefillTag` function and staking the NFT (that we created in the last step) by clicking [here](https://goerli.etherscan.io/address/0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000#writeProxyContract#F1) then clicking "Connect to Web3" and inputting the following: 258 | - tagType: 0 259 | - tokenAddress: 0x6dC5d9edcdD20543dB4788B26301fB7372c4d4EC 260 | - erc721TokenId: (the same tokenId that you entered in the last step) 261 | - tagAuthority: (your wallet address, this address must send the claim transaction in the next step) 262 | - totalSupply: (the maximum number of claims for this tag that you want to permit in total for all users) 263 | - perUser: (the maximum number of claims for this tag that you want to permit for each user) 264 | - fungiblePerClaim: 0 265 | - uid: (any unique number, this must be a number that hasn't been used before by someone else) 266 | - isNotErc1155: true 267 | 4) Claim the new Cupcake tag (that you created in the last step) by running the `claimTag` function by clicking [here](https://goerli.etherscan.io/address/0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000#writeProxyContract#F2) then clicking "Connect to Web3" and inputting the following: 268 | - receiver: (the wallet address that you would like to receive the claimed NFT) 269 | - uid: (this must be the same "uid" that you provided in the previous step) 270 | - isNotErc1155: true 271 | 272 | #### Fungible Tag: WalletRestrictedFungible 273 | 274 | 1) Get some test ETH for the Goerli testnet by clicking [here](https://goerlifaucet.com/). 275 | 2) Mint a some test ERC-20 token by clicking [here](https://goerli.etherscan.io/address/0xaFF4481D10270F50f203E0763e2597776068CBc5#writeContract#F4) then clicking "Connect to Web3" running the `drip` function. 276 | 3) Approve the ERC-20 token to be taken by the Cupcake Contract by clicking [here](https://goerli.etherscan.io/address/0xaFF4481D10270F50f203E0763e2597776068CBc5#writeContract#F1) then clicking "Connect to Web3" and inputting the following: 277 | - spender: 0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000 278 | - tokens: (a number that is more than the "totalSupply" of the token you want to make claimable in the next step) 279 | 4) Add a new Cupcake tag by running the `addOrRefillTag` function and staking the ERC-20 tokens (that we minted in the last step) by clicking [here](https://goerli.etherscan.io/address/0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000#writeProxyContract#F1) then clicking "Connect to Web3" and inputting the following: 280 | - tagType: 3 281 | - tokenAddress: 0xaFF4481D10270F50f203E0763e2597776068CBc5 282 | - erc721TokenId: 0 283 | - tagAuthority: (your wallet address, this address must send the claim transaction in the next step) 284 | - totalSupply: (the maximum number of claims for this tag that you want to permit in total for all users) 285 | - perUser: (the maximum number of claims for this tag that you want to permit for each user) 286 | - fungiblePerClaim: (the amount of the ERC-20 that you want to giveaway to the user per claim, this must be less than the perUser amount above) 287 | - uid: (any unique number, this must be a number that hasn't been used before by someone else) 288 | - isNotErc1155: true 289 | 4) Claim the new Cupcake tag (that you created in the last step) by running the `claimTag` function by clicking [here](https://goerli.etherscan.io/address/0x0285d1f1a27CD6fE7c8e9DbAA3Fb551EBAc88000#writeProxyContract#F2) then clicking "Connect to Web3" and inputting the following: 290 | - receiver: (the wallet address that you would like to receive the claimed ERC-20 tokens) 291 | - uid: (this must be the same "uid" that you provided in the previous step) 292 | - isNotErc1155: true 293 | 294 | ## Known Concerns 295 | 296 | - Currently the CandyMachine uses a pseudo-random number generator to select assets to distribute. This is acceptable for the time-being since the order of distribution is of negligible financial value currently. (See here for more information on this vulnerability: https://github.com/crytic/slither/wiki/Detector-Documentation#weak-PRNG) 297 | - Currently the `claimsMade` mapping is not cleared when tag deletion occurs. (See here for more information on this vulnerability: https://github.com/crytic/slither/wiki/Detector-Documentation#deletion-on-mapping-containing-a-structure) See the solution below in Further Expansions of how this minor issue can be resolved. 298 | 299 | ## Further Expansions 300 | 301 | - Tracking of `claimsMade` of each tag by changing the value of the mapping to be a tuple that tracks the block number of the last claim and then enable invalidation via a `lastDeleted` varaible containing the block number of the last deleation of that tag (right now `claimsMade` are not cleared when a tag deleation occurs). 302 | - The ability to define a non-sender as the payer for a claim (`minter_pays = true` from the Solana contract). 303 | -------------------------------------------------------------------------------- /ethereum/contracts/CandyMachine.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity ^0.8.7; 3 | 4 | import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol"; 5 | import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; 6 | import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; 7 | import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol"; 8 | 9 | contract CandyMachine is ERC1155URIStorage, VRFConsumerBaseV2 { 10 | 11 | event RandomWordsRequested(uint256 indexed requestId); 12 | event RandomWordsFulfilled(uint256 indexed requestId, uint256 outputWord); 13 | event Cancellation(); 14 | 15 | uint256 internal numURIsExisting; 16 | uint256 internal nonce; 17 | uint256 internal numMintsInProcess; 18 | address internal owner; 19 | 20 | mapping(uint256 => address) public requests; 21 | 22 | 23 | uint32 constant callbackGasLimit = 300000; 24 | uint16 constant requestConfirmations = 3; 25 | uint32 constant numWords = 1; 26 | VRFCoordinatorV2Interface COORDINATOR; 27 | uint64 subscriptionId; 28 | 29 | //////////////////////////////////////////////// 30 | //////// C O N S T R U C T O R 31 | 32 | /* 33 | * Initalizes the state variables. 34 | */ 35 | constructor(string[] memory metadataURIs, address ownerArg, uint64 subscriptionIdArg, address vrfConsumerBaseV2) 36 | ERC1155('') 37 | VRFConsumerBaseV2(vrfConsumerBaseV2) 38 | { 39 | require(metadataURIs.length > 0 && ownerArg != address(0), 'empty metadataURIs or zero owner'); 40 | 41 | for (uint256 i = 0; i < metadataURIs.length; i++) { 42 | _setURI(i, metadataURIs[i]); 43 | } 44 | numURIsExisting = metadataURIs.length; 45 | owner = ownerArg; 46 | COORDINATOR = VRFCoordinatorV2Interface( 47 | vrfConsumerBaseV2 48 | ); 49 | subscriptionId = subscriptionIdArg; 50 | } 51 | 52 | /* 53 | * Modifier to ensure that only the designated "owner" can access the associated function. 54 | * NOTE: This modifier should NOT be confused with OwnableUpgradeable's modifier by the same name. 55 | */ 56 | modifier onlyOwner { 57 | require(msg.sender == owner, 'caller not owner'); 58 | _; 59 | } 60 | 61 | //////////////////////////////////////////////// 62 | //////// F U N C T I O N S 63 | 64 | /* 65 | * @notice Begin process of minting an ERC-1155 asset by requesting a random number from Chainlink. 66 | * @dev Emits a {RandomWordsRequested} event. 67 | */ 68 | function mint(address recipient, bytes32 keyHash) 69 | external 70 | onlyOwner 71 | returns (uint256 requestId) 72 | { 73 | require((nonce + numMintsInProcess) < numURIsExisting, "CandyMachine empty"); 74 | // Will revert if subscription is not set and funded. 75 | requestId = COORDINATOR.requestRandomWords( 76 | keyHash, 77 | subscriptionId, 78 | requestConfirmations, 79 | callbackGasLimit, 80 | numWords 81 | ); 82 | emit RandomWordsRequested(requestId); 83 | numMintsInProcess += 1; 84 | requests[requestId] = recipient; 85 | return requestId; 86 | } 87 | 88 | /* 89 | * @notice Receives the requested random number and mints an ERC-1155 asset using the randomness. 90 | * @dev Emits a {RandomWordsFulfilled} event and a {TransferSingle} event. 91 | */ 92 | function fulfillRandomWords( 93 | uint256 _requestId, 94 | uint256[] memory _randomWords 95 | ) internal override { 96 | require(requests[_requestId] != address(0) && nonce < numURIsExisting, "no request or CM empty"); 97 | 98 | uint256 randomNum = _randomWords[0] % (numURIsExisting - nonce); 99 | 100 | emit RandomWordsFulfilled(_requestId, randomNum); 101 | 102 | string memory temp = uri(nonce + randomNum); 103 | _setURI(nonce + randomNum, uri(nonce)); 104 | _setURI(nonce, temp); 105 | 106 | _mint(requests[_requestId], nonce, 1, "0x00"); 107 | 108 | nonce += 1; 109 | numMintsInProcess -= 1; 110 | } 111 | 112 | /* 113 | * @notice Cancel the CandyMachine. This means that no further NFTs can be minted. 114 | * @dev Emits a {Cancellation} event. 115 | */ 116 | function cancel() external onlyOwner { 117 | emit Cancellation(); 118 | for (uint256 i = nonce; i < numURIsExisting; i++) { 119 | _setURI(i, ""); 120 | } 121 | nonce = numURIsExisting; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /ethereum/contracts/CandyMachineFactory.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity ^0.8.7; 3 | 4 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 5 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 7 | import "../interfaces/ICandyMachineFactory.sol"; 8 | import "./CandyMachine.sol"; 9 | 10 | /* 11 | * The CandyMachineStorage contract contains all of the Contract's state variables which are then inherited by Contract. 12 | * Via this seperation of storage and logic we ensure that Contract's state variables come first in the storage layout 13 | * and that Contract has the ability to change the list of contracts it inherits from in the future via upgradeability. 14 | */ 15 | contract CandyMachineFactory is ICandyMachineFactory, Initializable, UUPSUpgradeable, OwnableUpgradeable { 16 | 17 | event Creation(address indexed newCandyMachine); 18 | 19 | /// @custom:oz-upgrades-unsafe-allow constructor 20 | constructor() { 21 | _disableInitializers(); 22 | } 23 | 24 | /* 25 | * Initalizes the state variables. 26 | */ 27 | function initialize() external initializer { 28 | __Ownable_init(); 29 | } 30 | 31 | /* 32 | * @notice Authorizes contract upgrades only for the contract owner (contract deployer) via the onlyOwner modifier. 33 | */ 34 | function _authorizeUpgrade(address) internal override onlyOwner {} 35 | 36 | /* 37 | * @notice Creates a new CandyMachine contract. 38 | * @dev Initalizes the new CandyMachine using the passed arguments and emits a {Creation} event. 39 | */ 40 | function newCandyMachine(string[] calldata metadataURIs, uint64 subscriptionId, address vrfConsumerBaseV2) external override returns(address) { 41 | CandyMachine candyMachine = new CandyMachine(metadataURIs, msg.sender, subscriptionId, vrfConsumerBaseV2); 42 | emit Creation(address(candyMachine)); 43 | return address(candyMachine); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ethereum/contracts/ExampleERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /* 4 | * Original credit to: @sidarth16 (https://github.com/sidarth16) 5 | * From: https://github.com/sidarth16/Rentable-NFTs/blob/main/contracts/RentableNft.sol 6 | */ 7 | 8 | pragma solidity ^0.8.7; 9 | 10 | import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; 11 | 12 | contract ExampleERC1155 is ERC1155Upgradeable { 13 | 14 | function initialize() external initializer { 15 | __ERC1155_init("http://example.com/json_file_here.json"); 16 | } 17 | 18 | function mintNFT(address to, uint256 tokenId) external { 19 | _mint(to, tokenId, 1, "0x00"); 20 | } 21 | 22 | function mintFungible(address to, uint256 amount) external { 23 | _mint(to, 0, amount, "0x00"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ethereum/contracts/ExampleERC1155Copyable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /* 4 | * Original credit to: @sidarth16 (https://github.com/sidarth16) 5 | * From: https://github.com/sidarth16/Rentable-NFTs/blob/main/contracts/RentableNft.sol 6 | */ 7 | 8 | pragma solidity ^0.8.7; 9 | 10 | import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155URIStorageUpgradeable.sol"; 11 | 12 | import "../interfaces/IERC1155CopyableUpgradeable.sol"; 13 | 14 | contract ExampleERC1155Copyable is ERC1155URIStorageUpgradeable, IERC1155CopyableUpgradeable { 15 | 16 | function initialize() external initializer { 17 | __ERC1155URIStorage_init(); 18 | __ERC1155_init("http://example.com/json_file_here.json"); 19 | } 20 | 21 | function mintCopy(address to, uint256 tokenIdMaster, uint256 tokenIdCopy) override external { 22 | _mint(to, tokenIdCopy, 1, "0x00"); 23 | _setURI(tokenIdCopy, uri(tokenIdMaster)); 24 | } 25 | 26 | function mint(address to, uint256 tokenId) external { 27 | _mint(to, tokenId, 1, "0x00"); 28 | } 29 | 30 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155Upgradeable, IERC165Upgradeable) returns (bool) { 31 | return interfaceId == type(IERC1155CopyableUpgradeable).interfaceId || super.supportsInterface(interfaceId); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ethereum/contracts/ExampleERC1155Mintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /* 4 | * Original credit to: @sidarth16 (https://github.com/sidarth16) 5 | * From: https://github.com/sidarth16/Rentable-NFTs/blob/main/contracts/RentableNft.sol 6 | */ 7 | 8 | pragma solidity ^0.8.7; 9 | 10 | import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; 11 | 12 | contract ExampleERC1155Mintable is ERC1155Upgradeable { 13 | 14 | function initialize(string memory uri) external initializer { 15 | __ERC1155_init(uri); 16 | } 17 | 18 | function mint(address to, uint256 id, uint256 amount, bytes memory data) external { 19 | _mint(to, id, amount, data); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ethereum/contracts/ExampleERC20Mintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /* 4 | * Original credit to: @sidarth16 (https://github.com/sidarth16) 5 | * From: https://github.com/sidarth16/Rentable-NFTs/blob/main/contracts/RentableNft.sol 6 | */ 7 | 8 | pragma solidity ^0.8.7; 9 | 10 | import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; 11 | import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; 12 | 13 | contract ExampleERC20Mintable is ERC20Upgradeable { 14 | using SafeERC20Upgradeable for IERC20Upgradeable; 15 | 16 | function initialize(string memory name_, string memory symbol_) external initializer { 17 | __ERC20_init(name_, symbol_); 18 | } 19 | 20 | function mint(address account, uint256 amount) external { 21 | _mint(account, amount); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ethereum/contracts/ExampleERC4907.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /* 4 | * Original credit to: @sidarth16 (https://github.com/sidarth16) 5 | * From: https://github.com/sidarth16/Rentable-NFTs/blob/main/contracts/RentableNft.sol 6 | */ 7 | 8 | pragma solidity ^0.8.7; 9 | 10 | import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; 11 | import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; 12 | 13 | import "../interfaces/IERC4907Upgradeable.sol"; 14 | 15 | contract ExampleERC4907 is ERC721Upgradeable, IERC4907Upgradeable { 16 | struct UserInfo 17 | { 18 | address user; // address of user role 19 | uint64 expires; // unix timestamp, user expires 20 | } 21 | 22 | // using Strings for uint256; 23 | using CountersUpgradeable for CountersUpgradeable.Counter; 24 | CountersUpgradeable.Counter private _tokenIdCounter; 25 | 26 | mapping (uint256 => UserInfo) private _users; 27 | 28 | function initialize() external initializer { 29 | __ERC721_init("TestRentableNFT","TRN"); 30 | } 31 | 32 | /// @notice set the user and expires of a NFT 33 | /// @dev The zero address indicates there is no user 34 | /// Throws if `tokenId` is not valid NFT 35 | /// @param user The new user of the NFT 36 | /// @param expires UNIX timestamp, The new user could use the NFT before expires 37 | function setUser(uint256 tokenId, address user, uint64 expires) external override virtual{ 38 | require(_isApprovedOrOwner(msg.sender, tokenId),"ERC721Upgradeable: transfer caller is not owner nor approved"); 39 | // require(userOf(tokenId)==address(0),"User already assigned"); 40 | require(expires > block.timestamp, "expires should be in future"); 41 | UserInfo storage info = _users[tokenId]; 42 | info.user = user; 43 | info.expires = expires; 44 | emit UpdateUser(tokenId,user,expires); 45 | } 46 | 47 | /// @notice Get the user address of an NFT 48 | /// @dev The zero address indicates that there is no user or the user is expired 49 | /// @param tokenId The NFT to get the user address for 50 | /// @return The user address for this NFT 51 | function userOf(uint256 tokenId) public view override virtual returns(address){ 52 | if( uint256(_users[tokenId].expires) >= block.timestamp){ 53 | return _users[tokenId].user; 54 | } 55 | return address(0); 56 | } 57 | 58 | /// @notice Get the user expires of an NFT 59 | /// @dev The zero value indicates that there is no user 60 | /// @param tokenId The NFT to get the user expires for 61 | /// @return The user expires for this NFT 62 | function userExpires(uint256 tokenId) external view override virtual returns(uint256){ 63 | return _users[tokenId].expires; 64 | } 65 | 66 | /// @dev See {IERC165-supportsInterface}. 67 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721Upgradeable, IERC165Upgradeable) returns (bool) { 68 | return interfaceId == type(IERC4907Upgradeable).interfaceId || super.supportsInterface(interfaceId); 69 | } 70 | 71 | function nftMint() external returns (uint256){ 72 | _tokenIdCounter.increment(); 73 | uint256 tokenId = _tokenIdCounter.current(); 74 | _safeMint(msg.sender, tokenId); 75 | return tokenId; 76 | } 77 | 78 | function _beforeTokenTransfer( 79 | address from, 80 | address to, 81 | uint256 tokenId 82 | ) internal virtual override{ 83 | super._beforeTokenTransfer(from, to, tokenId); 84 | 85 | if ( 86 | from != to && 87 | _users[tokenId].user != address(0) && //user present 88 | block.timestamp >= _users[tokenId].expires //user expired 89 | ) { 90 | delete _users[tokenId]; 91 | emit UpdateUser(tokenId, address(0), 0); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ethereum/contracts/ExampleERC721Copyable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /* 4 | * Original credit to: @sidarth16 (https://github.com/sidarth16) 5 | * From: https://github.com/sidarth16/Rentable-NFTs/blob/main/contracts/RentableNft.sol 6 | */ 7 | 8 | pragma solidity ^0.8.7; 9 | 10 | import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; 11 | 12 | import "../interfaces/IERC721CopyableUpgradeable.sol"; 13 | 14 | contract ExampleERC721Copyable is ERC721URIStorageUpgradeable, IERC721CopyableUpgradeable { 15 | 16 | function initialize() external initializer { 17 | __ERC721_init("TestCopyableNFT721","TCN"); 18 | } 19 | 20 | function mintCopy(address to, uint256 tokenIdMaster, uint256 tokenIdCopy) override external { 21 | _safeMint(to, tokenIdCopy); 22 | _setTokenURI(tokenIdCopy, tokenURI(tokenIdMaster)); 23 | } 24 | 25 | function mint(address to, uint256 tokenId) external { 26 | _safeMint(to, tokenId); 27 | } 28 | 29 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721Upgradeable, IERC165Upgradeable) returns (bool) { 30 | return interfaceId == type(IERC721CopyableUpgradeable).interfaceId || super.supportsInterface(interfaceId); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ethereum/contracts/ExampleERC721Mintable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /* 4 | * Original credit to: @sidarth16 (https://github.com/sidarth16) 5 | * From: https://github.com/sidarth16/Rentable-NFTs/blob/main/contracts/RentableNft.sol 6 | */ 7 | 8 | pragma solidity ^0.8.7; 9 | 10 | import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; 11 | 12 | contract ExampleERC721Mintable is ERC721URIStorageUpgradeable { 13 | 14 | function initialize(string memory name, string memory symbol) external initializer { 15 | __ERC721_init(name, symbol); 16 | } 17 | 18 | function mint(address to, uint256 tokenId) external { 19 | _safeMint(to, tokenId); 20 | } 21 | 22 | function setTokenURI(uint256 tokenId, string memory tokenURI) external { 23 | _setTokenURI(tokenId, tokenURI); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ethereum/contracts/RentableWrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.7; 4 | 5 | import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721MetadataUpgradeable.sol"; 7 | import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; 8 | import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 9 | import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; 10 | import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; 11 | 12 | import "../interfaces/IERC4907Upgradeable.sol"; 13 | 14 | /* 15 | * The RentableWrapperStorage contract contains all of the RentableWrapper's state variables which are then inherited by RentableWrapper. 16 | * Via this seperation of storage and logic we ensure that RentableWrapper's state variables come first in the storage layout 17 | * and that RentableWrapper has the ability to change the list of contracts it inherits from in the future via upgradeability. 18 | */ 19 | contract RentableWrapperStorage { 20 | using CountersUpgradeable for CountersUpgradeable.Counter; 21 | 22 | /// @notice This emits when an underlying NFT is wrapped. 23 | event Wrap( 24 | address indexed asset, 25 | uint256 indexed underlyingTokenId, 26 | uint256 indexed wrappedTokenId, 27 | address tokenDepositor 28 | ); 29 | 30 | /// @notice This emits when an underlying NFT is unwrapped. 31 | event Unwrap( 32 | address indexed asset, 33 | uint256 indexed underlyingTokenId, 34 | uint256 indexed wrappedTokenId, 35 | address tokenDepositor 36 | ); 37 | 38 | CountersUpgradeable.Counter internal tokenIdCounter; 39 | 40 | struct Token { 41 | IERC721MetadataUpgradeable asset; // the address of the underlying wrapped asset 42 | uint256 underlyingTokenId; // the tokenId of the underlying wrapped asset 43 | address user; // address of user role 44 | uint64 expires; // unix timestamp, user expires 45 | } 46 | 47 | mapping(uint256 => Token) public tokens; 48 | } 49 | 50 | contract RentableWrapper is RentableWrapperStorage, ERC721Upgradeable, IERC4907Upgradeable, ERC721HolderUpgradeable, UUPSUpgradeable, OwnableUpgradeable { 51 | using CountersUpgradeable for CountersUpgradeable.Counter; 52 | 53 | /// @custom:oz-upgrades-unsafe-allow constructor 54 | constructor() { 55 | _disableInitializers(); 56 | } 57 | 58 | function initialize(string memory name, string memory symbol) external initializer { 59 | __Ownable_init(); 60 | __ERC721_init(name, symbol); 61 | } 62 | 63 | /* 64 | * @notice Authorizes contract upgrades only for the contract owner (contract deployer) via the onlyOwner modifier. 65 | */ 66 | function _authorizeUpgrade(address) internal override onlyOwner {} 67 | 68 | /** 69 | * @notice wrap an NFT inside a newly minted wrapper NFT 70 | * @dev The user must own the NFT that is being wrapped 71 | * @param asset The address of the token that we would like to wrap 72 | * @param underlyingTokenId The tokenId of the token that we would like to wrap 73 | * @return newTokenId The tokenId of the newly generated wrapper token 74 | */ 75 | function wrap(IERC721MetadataUpgradeable asset, uint256 underlyingTokenId) external returns(uint256 newTokenId) { 76 | require(asset.ownerOf(underlyingTokenId) == msg.sender, 'asset not owned by msg.sender'); 77 | require(asset.getApproved(underlyingTokenId) == address(this), 'asset not approved'); 78 | require(address(asset) != address(this), 'recursive wrapping not allowed'); 79 | 80 | Token storage token = tokens[tokenIdCounter.current()]; 81 | token.asset = asset; 82 | token.underlyingTokenId = underlyingTokenId; 83 | 84 | _safeMint(msg.sender, tokenIdCounter.current()); 85 | 86 | asset.safeTransferFrom( 87 | msg.sender, 88 | address(this), 89 | underlyingTokenId 90 | ); 91 | 92 | require(asset.ownerOf(underlyingTokenId) == address(this), 'asset transfer failed'); 93 | 94 | tokenIdCounter.increment(); 95 | 96 | emit Wrap( 97 | address(asset), 98 | underlyingTokenId, 99 | tokenIdCounter.current() - 1, 100 | msg.sender 101 | ); 102 | 103 | return tokenIdCounter.current() - 1; 104 | } 105 | 106 | /** 107 | * @notice unwrap an NFT to extract it from the wrapper NFT 108 | * @dev The user must own the wrapped NFT and be the current user ("renter") of the wrapped NFT 109 | * @param wrappedTokenId The tokenId of the NFT that we would like to unwrap 110 | */ 111 | function unwrap(uint256 wrappedTokenId) external { 112 | require(ownerOf(wrappedTokenId) == msg.sender, 'asset not owned by msg.sender'); 113 | require(userOf(wrappedTokenId) == msg.sender || userOf(wrappedTokenId) == address(0), 'curnt user must be owner or 0x0'); 114 | 115 | IERC721MetadataUpgradeable asset = tokens[wrappedTokenId].asset; 116 | uint256 underlyingTokenId = tokens[wrappedTokenId].underlyingTokenId; 117 | 118 | emit Unwrap( 119 | address(asset), 120 | underlyingTokenId, 121 | wrappedTokenId, 122 | msg.sender 123 | ); 124 | 125 | delete tokens[wrappedTokenId]; 126 | 127 | safeTransferFrom( 128 | msg.sender, 129 | address(this), 130 | wrappedTokenId 131 | ); 132 | 133 | asset.safeTransferFrom( 134 | address(this), 135 | msg.sender, 136 | underlyingTokenId 137 | ); 138 | 139 | require(asset.ownerOf(underlyingTokenId) == msg.sender, 'asset transfer failed'); 140 | } 141 | 142 | function isWrapped(uint256 wrappedTokenId) public view returns(bool) { 143 | return address(tokens[wrappedTokenId].asset) != address(0); 144 | } 145 | 146 | /** 147 | * @notice Gets the wrapped Uniform Resource Identifier (URI) for `tokenId` token. 148 | * @param wrappedTokenId The tokenId for the desired tokenURI 149 | */ 150 | function tokenURI(uint256 wrappedTokenId) public view virtual override returns (string memory) { 151 | require(isWrapped(wrappedTokenId), 'token not wrapped'); 152 | 153 | return tokens[wrappedTokenId].asset.tokenURI(tokens[wrappedTokenId].underlyingTokenId); 154 | } 155 | 156 | /** 157 | * @notice set the user and expires of a NFT 158 | * @dev The zero address indicates there is no user 159 | * @param tokenId The tokenId that's user is being changed 160 | * Throws if `tokenId` is not valid NFT 161 | * @param user The new user of the NFT 162 | * @param expires UNIX timestamp, The new user could use the NFT before expires 163 | */ 164 | function setUser(uint256 tokenId, address user, uint64 expires) external override virtual { 165 | require(_isApprovedOrOwner(msg.sender, tokenId),"caller not owner nor approved"); 166 | require(expires > block.timestamp, "expires should be in future"); 167 | 168 | Token storage token = tokens[tokenId]; 169 | token.user = user; 170 | token.expires = expires; 171 | emit UpdateUser(tokenId, user, expires); 172 | } 173 | /** 174 | * @notice Get the user address of an NFT 175 | * @dev The zero address indicates that there is no user or the user is expired 176 | * @param tokenId The NFT to get the user address for 177 | * @return The user address for this NFT 178 | */ 179 | function userOf(uint256 tokenId) public view override virtual returns(address) { 180 | if(tokens[tokenId].expires >= block.timestamp){ 181 | return tokens[tokenId].user; 182 | } 183 | return address(0); 184 | } 185 | 186 | /** 187 | * @notice Get the user expires of an NFT 188 | * @dev The zero value indicates that there is no user 189 | * @param tokenId The NFT to get the user expires for 190 | * @return The user expires for this NFT 191 | */ 192 | function userExpires(uint256 tokenId) external view override virtual returns(uint256) { 193 | return tokens[tokenId].expires; 194 | } 195 | 196 | /** 197 | * @dev See {IERC165-supportsInterface}. 198 | */ 199 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721Upgradeable, IERC165Upgradeable) returns (bool) { 200 | return interfaceId == type(IERC4907Upgradeable).interfaceId || super.supportsInterface(interfaceId); 201 | } 202 | 203 | function _beforeTokenTransfer( 204 | address from, 205 | address to, 206 | uint256 tokenId 207 | ) internal virtual override { 208 | super._beforeTokenTransfer(from, to, tokenId); 209 | require(userOf(tokenId) == address(0) || userOf(tokenId) == ownerOf(tokenId), 'trnsfrs when user is owner or 0x'); 210 | 211 | if ( 212 | from != to && 213 | tokens[tokenId].user != address(0) && 214 | block.timestamp >= tokens[tokenId].expires 215 | ) { 216 | delete tokens[tokenId].user; 217 | delete tokens[tokenId].expires; 218 | emit UpdateUser(tokenId, address(0), 0); 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /ethereum/contracts/VRFCoordinatorV2Mock.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol"; -------------------------------------------------------------------------------- /ethereum/interfaces/ICandyMachine.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity ^0.8.7; 3 | 4 | import "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; 5 | 6 | interface ICandyMachineStorage { 7 | event Cancellation(); 8 | } 9 | 10 | interface ICandyMachine is ICandyMachineStorage, IERC1155MetadataURI { 11 | 12 | //////////////////////////////////////////////// 13 | //////// F U N C T I O N S 14 | 15 | /* 16 | * @notice Mint an ERC-1155 asset with a pseudo-randomly selected metadata URI. 17 | * @dev Emits a {TransferSingle} event. 18 | */ 19 | function mint(address recipient, bytes32 keyHash) external; 20 | 21 | /* 22 | * @notice Cancel the CandyMachine. This means that no further NFTs can be minted. 23 | */ 24 | function cancel() external; 25 | } 26 | -------------------------------------------------------------------------------- /ethereum/interfaces/ICandyMachineFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.7; 4 | 5 | interface ICandyMachineFactory { 6 | function newCandyMachine(string[] calldata metadataURIs, uint64 subscriptionId, address vrfConsumerBaseV2) external returns(address); 7 | } 8 | -------------------------------------------------------------------------------- /ethereum/interfaces/IERC1155CopyableUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.7; 4 | 5 | import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/IERC1155MetadataURIUpgradeable.sol"; 6 | 7 | interface IERC1155CopyableUpgradeable is IERC1155MetadataURIUpgradeable { 8 | 9 | /// @notice This emits when the an NFT has been copied. 10 | event Copy(address indexed _to, uint256 indexed _tokenIdMaster, uint256 indexed _tokenIdCopy); 11 | 12 | // @notice Mint a new NFT with exactly the same associated metadata (same return value for the `tokenURI()` function) of an existing NFT in this same collection 13 | // @param _to An address to send the duplicated token to 14 | // @param _tokenIdMaster A token ID that we would like to duplicate the metadata of 15 | // @param _tokenIdCopy A token ID that we would like to duplicate the metadata to 16 | // @return uint256 representing the token ID of the newly minted NFT (via this duplication process) 17 | function mintCopy(address to, uint256 tokenIdMaster, uint256 tokenIdCopy) external; 18 | } 19 | -------------------------------------------------------------------------------- /ethereum/interfaces/IERC4907Upgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.7; 4 | 5 | import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; 6 | 7 | interface IERC4907Upgradeable is IERC721Upgradeable { 8 | 9 | // Logged when the user of an NFT is changed or expires is changed 10 | /// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed 11 | /// The zero address for user indicates that there is no user address 12 | event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires); 13 | 14 | /// @notice set the user and expires of an NFT 15 | /// @dev The zero address indicates there is no user 16 | /// Throws if `tokenId` is not valid NFT 17 | /// @param user The new user of the NFT 18 | /// @param expires UNIX timestamp, The new user could use the NFT before expires 19 | function setUser(uint256 tokenId, address user, uint64 expires) external; 20 | 21 | /// @notice Get the user address of an NFT 22 | /// @dev The zero address indicates that there is no user or the user is expired 23 | /// @param tokenId The NFT to get the user address for 24 | /// @return The user address for this NFT 25 | function userOf(uint256 tokenId) external view returns(address); 26 | 27 | /// @notice Get the user expires of an NFT 28 | /// @dev The zero value indicates that there is no user 29 | /// @param tokenId The NFT to get the user expires for 30 | /// @return The user expires for this NFT 31 | function userExpires(uint256 tokenId) external view returns(uint256); 32 | } -------------------------------------------------------------------------------- /ethereum/interfaces/IERC721CopyableUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.7; 4 | 5 | import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721MetadataUpgradeable.sol"; 6 | 7 | interface IERC721CopyableUpgradeable is IERC721MetadataUpgradeable { 8 | 9 | /// @notice This emits when the an NFT has been copied. 10 | event Copy(address indexed _to, uint256 indexed _tokenIdMaster, uint256 indexed _tokenIdCopy); 11 | 12 | // @notice Mint a new NFT with exactly the same associated metadata (same return value for the `tokenURI()` function) of an existing NFT in this same collection 13 | // @param _to An address to send the duplicated token to 14 | // @param _tokenIdMaster A token ID that we would like to duplicate the metadata of 15 | // @param _tokenIdCopy A token ID that we would like to duplicate the metadata to 16 | // @return uint256 representing the token ID of the newly minted NFT (via this duplication process) 17 | function mintCopy(address to, uint256 tokenIdMaster, uint256 tokenIdCopy) external; 18 | } 19 | -------------------------------------------------------------------------------- /ethereum/scripts/deploy_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | console.log("Deploying CandyMachineFactory..."); 5 | 6 | const CandyMachineFactory = await ethers.getContractFactory("CandyMachineFactory"); 7 | 8 | const candyMachineFactory = await upgrades.deployProxy(CandyMachineFactory, [], { 9 | initializer: "initialize", 10 | kind: "uups", 11 | }); 12 | await candyMachineFactory.deployed(); 13 | 14 | if (!ethers.utils.isAddress(candyMachineFactory.address)) { 15 | throw new Error('CandyMachineFactory deployment failed!'); 16 | } 17 | 18 | console.log(`CandyMachineFactory deployed at ${candyMachineFactory.address}!`); 19 | 20 | console.log("Deploying Contract..."); 21 | 22 | const Contract = await ethers.getContractFactory("Contract"); 23 | 24 | const contract = await upgrades.deployProxy(Contract, [candyMachineFactory.address], { 25 | initializer: "initialize", 26 | kind: "uups", 27 | }); 28 | await contract.deployed(); 29 | 30 | if (!ethers.utils.isAddress(contract.address)) { 31 | throw new Error('Contract deployment failed!'); 32 | } 33 | 34 | console.log(`Contract deployed at ${contract.address}!`); 35 | } 36 | 37 | main() 38 | .then(() => process.exit(0)) 39 | .catch((error) => { 40 | console.error(error); 41 | process.exit(1); 42 | }); 43 | -------------------------------------------------------------------------------- /ethereum/scripts/deploy_erc1155_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const ExampleERC1155 = await ethers.getContractFactory("ExampleERC1155"); 5 | 6 | const contract = await upgrades.deployProxy(ExampleERC1155, [], { 7 | initializer: "initialize", 8 | }); 9 | await contract.deployed(); 10 | 11 | console.log("Deployed:"); 12 | console.log(contract.address); 13 | } 14 | 15 | main() 16 | .then(() => process.exit(0)) 17 | .catch((error) => { 18 | console.error(error); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /ethereum/scripts/deploy_erc1155_copyable_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const ExampleERC1155Copyable = await ethers.getContractFactory("ExampleERC1155Copyable"); 5 | 6 | const contract = await upgrades.deployProxy(ExampleERC1155Copyable, [], { 7 | initializer: "initialize", 8 | }); 9 | await contract.deployed(); 10 | 11 | console.log("Deployed:"); 12 | console.log(contract.address); 13 | } 14 | 15 | main() 16 | .then(() => process.exit(0)) 17 | .catch((error) => { 18 | console.error(error); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /ethereum/scripts/deploy_erc4907_example_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const ExampleERC4907 = await ethers.getContractFactory("ExampleERC4907"); 5 | 6 | const contract = await upgrades.deployProxy(ExampleERC4907, [], { 7 | initializer: "initialize", 8 | }); 9 | await contract.deployed(); 10 | 11 | console.log("Deployed:"); 12 | console.log(contract.address); 13 | console.log(contract); 14 | } 15 | 16 | main() 17 | .then(() => process.exit(0)) 18 | .catch((error) => { 19 | console.error(error); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /ethereum/scripts/deploy_erc721_copyable_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const ExampleERC721Copyable = await ethers.getContractFactory("ExampleERC721Copyable"); 5 | 6 | const contract = await upgrades.deployProxy(ExampleERC721Copyable, [], { 7 | initializer: "initialize", 8 | }); 9 | await contract.deployed(); 10 | 11 | console.log("Deployed:"); 12 | console.log(contract.address); 13 | } 14 | 15 | main() 16 | .then(() => process.exit(0)) 17 | .catch((error) => { 18 | console.error(error); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /ethereum/scripts/deploy_rentable_wrapper_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const RentableWrapper = await ethers.getContractFactory("RentableWrapper"); 5 | 6 | const rentableWrapper = await upgrades.deployProxy(RentableWrapper, ["TestRentableWrapper","TRW"], { 7 | initializer: "initialize", 8 | kind: "uups", 9 | }); 10 | await rentableWrapper.deployed(); 11 | 12 | if (!ethers.utils.isAddress(rentableWrapper.address)) { 13 | throw new Error('RentableWrapper deployment failed!'); 14 | } 15 | 16 | console.log(`RentableWrapper deployed at ${rentableWrapper.address}!`); 17 | } 18 | 19 | main() 20 | .then(() => process.exit(0)) 21 | .catch((error) => { 22 | console.error(error); 23 | process.exit(1); 24 | }); 25 | -------------------------------------------------------------------------------- /ethereum/scripts/upgrade_candy_machine_factory.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const CandyMachineFactory = await ethers.getContractFactory("CandyMachineFactory"); 5 | 6 | await upgrades.upgradeProxy(process.env.PROXY_ADDR_CANDY_MACHINE_FACTORY, CandyMachineFactory); 7 | console.log("Upgraded!"); 8 | } 9 | 10 | main() 11 | .then(() => process.exit(0)) 12 | .catch((error) => { 13 | console.error(error); 14 | process.exit(1); 15 | }); 16 | -------------------------------------------------------------------------------- /ethereum/scripts/upgrade_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const Contract = await ethers.getContractFactory("Contract"); 5 | 6 | await upgrades.upgradeProxy(process.env.PROXY_ADDR_CONTRACT, Contract); 7 | console.log("Upgraded!"); 8 | } 9 | 10 | main() 11 | .then(() => process.exit(0)) 12 | .catch((error) => { 13 | console.error(error); 14 | process.exit(1); 15 | }); 16 | -------------------------------------------------------------------------------- /ethereum/scripts/upgrade_erc1155_copyable_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const ExampleERC1155Copyable = await ethers.getContractFactory("ExampleERC1155Copyable"); 5 | 6 | await upgrades.upgradeProxy(process.env.PROXY_ADDR_ERC1155_COPYABLE_EXAMPLE, ExampleERC1155Copyable); 7 | console.log("Upgraded!"); 8 | } 9 | 10 | main() 11 | .then(() => process.exit(0)) 12 | .catch((error) => { 13 | console.error(error); 14 | process.exit(1); 15 | }); 16 | -------------------------------------------------------------------------------- /ethereum/scripts/upgrade_erc4907_example_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const ExampleERC4907 = await ethers.getContractFactory("ExampleERC4907"); 5 | 6 | await upgrades.upgradeProxy(process.env.PROXY_ADDR_ERC4907_EXAMPLE, ExampleERC4907); 7 | console.log("Upgraded!"); 8 | } 9 | 10 | main() 11 | .then(() => process.exit(0)) 12 | .catch((error) => { 13 | console.error(error); 14 | process.exit(1); 15 | }); 16 | -------------------------------------------------------------------------------- /ethereum/scripts/upgrade_erc721_copyable_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const ExampleERC721Copyable = await ethers.getContractFactory("ExampleERC721Copyable"); 5 | 6 | await upgrades.upgradeProxy(process.env.PROXY_ADDR_ERC721_COPYABLE_EXAMPLE, ExampleERC721Copyable); 7 | console.log("Upgraded!"); 8 | } 9 | 10 | main() 11 | .then(() => process.exit(0)) 12 | .catch((error) => { 13 | console.error(error); 14 | process.exit(1); 15 | }); 16 | -------------------------------------------------------------------------------- /ethereum/scripts/upgrade_rentable_wrapper_contract.js: -------------------------------------------------------------------------------- 1 | const { ethers, upgrades } = require("hardhat"); 2 | 3 | async function main() { 4 | const RentableWrapper = await ethers.getContractFactory("RentableWrapper"); 5 | 6 | await upgrades.upgradeProxy(process.env.PROXY_ADDR_RENTABLE_WRAPPER, RentableWrapper); 7 | console.log("Upgraded!"); 8 | } 9 | 10 | main() 11 | .then(() => process.exit(0)) 12 | .catch((error) => { 13 | console.error(error); 14 | process.exit(1); 15 | }); 16 | -------------------------------------------------------------------------------- /ethereum/test/2_CandyMachine-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { ethers } = require("hardhat"); 3 | 4 | const keyHash = "0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15"; 5 | 6 | describe('CandyMachine', function () { 7 | 8 | const NULL_ADDR = '0x0000000000000000000000000000000000000000'; 9 | const METADATA_URIS = ['https://google.com', 'https://twitter.com', 'https://facebook.com']; 10 | 11 | let chainlinkReqNonce = 1; 12 | 13 | before(async function () { 14 | this.signer = await ethers.getSigner(); 15 | this.owner = await this.signer.getAddress(); 16 | 17 | this.signer2 = await ethers.getSigner(1); 18 | this.otherAddr = await this.signer2.getAddress(); 19 | 20 | this.CandyMachine = await ethers.getContractFactory('CandyMachine'); 21 | 22 | const vrfCoordinatorV2Mock = await ethers.getContractFactory("VRFCoordinatorV2Mock"); 23 | this.hardhatVrfCoordinatorV2Mock = await vrfCoordinatorV2Mock.deploy(0, 0); 24 | 25 | await this.hardhatVrfCoordinatorV2Mock.createSubscription(); 26 | 27 | await this.hardhatVrfCoordinatorV2Mock.fundSubscription(1, ethers.utils.parseEther("50")); 28 | }); 29 | 30 | describe('constructor', function () { 31 | it('should revert when metadataURIs array is empty', async function () { 32 | await expect(this.CandyMachine.deploy([], this.owner, 1, this.hardhatVrfCoordinatorV2Mock.address)).to.be.reverted; 33 | }); 34 | 35 | it('should revert when ownerArg is the null address', async function () { 36 | await expect(this.CandyMachine.deploy(METADATA_URIS, NULL_ADDR, 1, this.hardhatVrfCoordinatorV2Mock.address)).to.be.reverted; 37 | }); 38 | 39 | it('should populate URIs when passed', async function () { 40 | this.candyMachine = await this.CandyMachine.deploy(METADATA_URIS, this.owner, 1, this.hardhatVrfCoordinatorV2Mock.address); 41 | await this.candyMachine.deployed(); 42 | expect(await this.candyMachine.uri(0)).to.be.equal(METADATA_URIS[0]); 43 | expect(await this.candyMachine.uri(1)).to.be.equal(METADATA_URIS[1]); 44 | expect(await this.candyMachine.uri(2)).to.be.equal(METADATA_URIS[2]); 45 | }); 46 | }); 47 | 48 | describe('mint', function () { 49 | beforeEach(async function () { 50 | this.candyMachine = await this.CandyMachine.deploy(METADATA_URIS, this.owner, 1, this.hardhatVrfCoordinatorV2Mock.address); 51 | await this.candyMachine.deployed(); 52 | await this.hardhatVrfCoordinatorV2Mock.addConsumer(1, this.candyMachine.address); 53 | }); 54 | 55 | it('should set unique metadata URIs for all assets that are minted', async function () { 56 | await new Promise(async resolve => { 57 | let count = 0; 58 | for (var i = 0; i < 3; i++) { 59 | await this.candyMachine.once("RandomWordsRequested", async _ => { 60 | const reqId = chainlinkReqNonce; 61 | chainlinkReqNonce++; 62 | expect( 63 | await this.hardhatVrfCoordinatorV2Mock.fulfillRandomWords(reqId, this.candyMachine.address) 64 | ).to.emit(this.hardhatVrfCoordinatorV2Mock, "RandomWordsFulfilled"); 65 | count++; 66 | if (count >= 3) { 67 | expect(await this.candyMachine.uri(0)).to.not.be.equal(await this.candyMachine.uri(1)); 68 | expect(await this.candyMachine.uri(0)).to.not.be.equal(await this.candyMachine.uri(2)); 69 | expect(await this.candyMachine.uri(1)).to.not.be.equal(await this.candyMachine.uri(2)); 70 | resolve(); 71 | } 72 | }); 73 | await this.candyMachine.mint(this.owner, keyHash, { from: this.owner }); 74 | } 75 | }); 76 | }); 77 | 78 | it('should revert after all assets have already been minted', async function () { 79 | await new Promise(async resolve => { 80 | let count = 0; 81 | for (var i = 0; i < 3; i++) { 82 | await this.candyMachine.once("RandomWordsRequested", async _ => { 83 | const reqId = chainlinkReqNonce; 84 | chainlinkReqNonce++; 85 | expect( 86 | await this.hardhatVrfCoordinatorV2Mock.fulfillRandomWords(reqId, this.candyMachine.address) 87 | ).to.emit(this.hardhatVrfCoordinatorV2Mock, "RandomWordsFulfilled"); 88 | count++; 89 | if (count >= 3) { 90 | resolve(); 91 | } 92 | }); 93 | await this.candyMachine.mint(this.owner, keyHash, { from: this.owner }); 94 | } 95 | }); 96 | await expect(this.candyMachine.mint(this.owner, keyHash, { from: this.owner })).to.be.reverted; 97 | }); 98 | 99 | it('should emit a TransferSingle event when an asset is minted', async function () { 100 | await new Promise(async resolve => { 101 | this.candyMachine.on("RandomWordsRequested", async _ => { 102 | const reqId = chainlinkReqNonce; 103 | chainlinkReqNonce++; 104 | expect( 105 | await this.hardhatVrfCoordinatorV2Mock.fulfillRandomWords(reqId, this.candyMachine.address) 106 | ).to.emit(this.hardhatVrfCoordinatorV2Mock, "TransferSingle"); 107 | resolve(); 108 | }); 109 | await this.candyMachine.mint(this.owner, keyHash, { from: this.owner }); 110 | }); 111 | }); 112 | 113 | it('should revert when a non-owner address calls', async function () { 114 | await expect(this.candyMachine.connect(this.signer2).mint(this.owner, keyHash, { from: this.otherAddr })).to.be.reverted; 115 | }); 116 | }); 117 | 118 | describe('cancel', function () { 119 | beforeEach(async function () { 120 | this.candyMachine = await this.CandyMachine.deploy(METADATA_URIS, this.owner, 1, this.hardhatVrfCoordinatorV2Mock.address); 121 | await this.candyMachine.deployed(); 122 | await this.hardhatVrfCoordinatorV2Mock.addConsumer(1, this.candyMachine.address); 123 | }); 124 | 125 | it('should override all metadata URIs before any assets have been minted', async function () { 126 | await this.candyMachine.connect(this.signer).cancel({ from: this.owner }); 127 | expect(await this.candyMachine.uri(0)).to.be.equal(''); 128 | expect(await this.candyMachine.uri(1)).to.be.equal(''); 129 | expect(await this.candyMachine.uri(2)).to.be.equal(''); 130 | }); 131 | 132 | it('should override two metadata URIs when only one asset has been minted', async function () { 133 | await new Promise(async resolve => { 134 | await this.candyMachine.once("RandomWordsRequested", async _ => { 135 | const reqId = chainlinkReqNonce; 136 | chainlinkReqNonce++; 137 | expect( 138 | await this.hardhatVrfCoordinatorV2Mock.fulfillRandomWords(reqId, this.candyMachine.address) 139 | ).to.emit(this.hardhatVrfCoordinatorV2Mock, "RandomWordsFulfilled"); 140 | resolve(); 141 | }); 142 | await this.candyMachine.mint(this.owner, keyHash, { from: this.owner }); 143 | }); 144 | await this.candyMachine.cancel({ from: this.owner }); 145 | expect(await this.candyMachine.uri(0)).to.not.be.equal(''); 146 | expect(await this.candyMachine.uri(1)).to.be.equal(''); 147 | expect(await this.candyMachine.uri(2)).to.be.equal(''); 148 | }); 149 | 150 | it('should override one metadata URIs when two assets have been minted', async function () { 151 | await new Promise(async resolve => { 152 | let count = 0; 153 | for (var i = 0; i < 2; i++) { 154 | await this.candyMachine.once("RandomWordsRequested", async _ => { 155 | const reqId = chainlinkReqNonce; 156 | chainlinkReqNonce++; 157 | expect( 158 | await this.hardhatVrfCoordinatorV2Mock.fulfillRandomWords(reqId, this.candyMachine.address) 159 | ).to.emit(this.hardhatVrfCoordinatorV2Mock, "RandomWordsFulfilled"); 160 | count++; 161 | if (count >= 2) { 162 | resolve(); 163 | } 164 | }); 165 | await this.candyMachine.mint(this.owner, keyHash, { from: this.owner }); 166 | } 167 | }); 168 | await this.candyMachine.cancel({ from: this.owner }); 169 | expect(await this.candyMachine.uri(0)).to.not.be.equal(''); 170 | expect(await this.candyMachine.uri(1)).to.not.be.equal(''); 171 | expect(await this.candyMachine.uri(2)).to.be.equal(''); 172 | }); 173 | 174 | it('should override zero metadata URIs when all three assets have been minted', async function () { 175 | await new Promise(async resolve => { 176 | let count = 0; 177 | for (var i = 0; i < 3; i++) { 178 | await this.candyMachine.once("RandomWordsRequested", async _ => { 179 | const reqId = chainlinkReqNonce; 180 | chainlinkReqNonce++; 181 | expect( 182 | await this.hardhatVrfCoordinatorV2Mock.fulfillRandomWords(reqId, this.candyMachine.address) 183 | ).to.emit(this.hardhatVrfCoordinatorV2Mock, "RandomWordsFulfilled"); 184 | count++; 185 | if (count >= 3) { 186 | resolve(); 187 | } 188 | }); 189 | await this.candyMachine.mint(this.owner, keyHash, { from: this.owner }); 190 | } 191 | }); 192 | await this.candyMachine.cancel({ from: this.owner }); 193 | expect(await this.candyMachine.uri(0)).to.not.be.equal(''); 194 | expect(await this.candyMachine.uri(1)).to.not.be.equal(''); 195 | expect(await this.candyMachine.uri(2)).to.not.be.equal(''); 196 | }); 197 | 198 | it('should emit a Cancellation event when the cancellation occurs', async function () { 199 | await new Promise(async resolve => { 200 | await this.candyMachine.once("RandomWordsRequested", async _ => { 201 | const reqId = chainlinkReqNonce; 202 | chainlinkReqNonce++; 203 | expect( 204 | await this.hardhatVrfCoordinatorV2Mock.fulfillRandomWords(reqId, this.candyMachine.address) 205 | ).to.emit(this.hardhatVrfCoordinatorV2Mock, "RandomWordsFulfilled"); 206 | resolve(); 207 | }); 208 | await this.candyMachine.mint(this.owner, keyHash, { from: this.owner }); 209 | }); 210 | await expect(this.candyMachine.cancel({ from: this.owner })).to.emit(this.candyMachine, "Cancellation"); 211 | }); 212 | 213 | it('should revert when a non-owner address calls', async function () { 214 | await expect(this.candyMachine.connect(this.signer2).cancel({ from: this.otherAddr })).to.be.reverted; 215 | }); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /ethereum/test/3_CandyMachineFactory-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { ethers, upgrades } = require("hardhat"); 3 | 4 | const keyHash = "0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15"; 5 | 6 | describe('CandyMachineFactory', function () { 7 | 8 | const NULL_ADDR = '0x0000000000000000000000000000000000000000'; 9 | const METADATA_URIS = ['https://google.com', 'https://twitter.com', 'https://facebook.com']; 10 | 11 | let chainlinkReqNonce = 1; 12 | 13 | before(async function () { 14 | const signer = await ethers.getSigner(); 15 | this.owner = await signer.getAddress(); 16 | 17 | this.CandyMachineFactory = await ethers.getContractFactory('CandyMachineFactory'); 18 | 19 | const vrfCoordinatorV2Mock = await ethers.getContractFactory("VRFCoordinatorV2Mock"); 20 | this.hardhatVrfCoordinatorV2Mock = await vrfCoordinatorV2Mock.deploy(0, 0); 21 | 22 | await this.hardhatVrfCoordinatorV2Mock.createSubscription(); 23 | 24 | await this.hardhatVrfCoordinatorV2Mock.fundSubscription(1, ethers.utils.parseEther("50")); 25 | }); 26 | 27 | describe('initialize', function () { 28 | it('should set owner', async function () { 29 | this.candyMachineFactory = await upgrades.deployProxy(this.CandyMachineFactory, [], { 30 | initializer: "initialize", 31 | kind: "uups", 32 | }); 33 | await this.candyMachineFactory.deployed(); 34 | expect(await this.candyMachineFactory.owner()).to.be.equal(this.owner); 35 | }); 36 | }); 37 | 38 | describe('newCandyMachine', function () { 39 | beforeEach(async function () { 40 | this.candyMachineFactory = await upgrades.deployProxy(this.CandyMachineFactory, [], { 41 | initializer: "initialize", 42 | kind: "uups", 43 | }); 44 | await this.candyMachineFactory.deployed(); 45 | }); 46 | 47 | it('should create a new CandyMachine contract that is functional', async function () { 48 | await this.candyMachineFactory.newCandyMachine(METADATA_URIS, 1, this.hardhatVrfCoordinatorV2Mock.address, { from: this.owner }); 49 | let candyMachine; 50 | await new Promise(async resolve => { 51 | this.candyMachineFactory.on("Creation", async (newCandyMachineAddr) => { 52 | candyMachine = await ethers.getContractAt('CandyMachine', newCandyMachineAddr); 53 | await this.hardhatVrfCoordinatorV2Mock.addConsumer(1, newCandyMachineAddr); 54 | resolve(); 55 | }); 56 | }); 57 | 58 | await new Promise(async resolve => { 59 | await candyMachine.mint(this.owner, keyHash, { from: this.owner }); 60 | await candyMachine.once("RandomWordsRequested", async _ => { 61 | const reqId = chainlinkReqNonce; 62 | chainlinkReqNonce++; 63 | expect( 64 | await this.hardhatVrfCoordinatorV2Mock.fulfillRandomWords(reqId, candyMachine.address) 65 | ).to.emit(this.hardhatVrfCoordinatorV2Mock, "RandomWordsFulfilled"); 66 | resolve(); 67 | }); 68 | }); 69 | expect(await candyMachine.uri(0)).to.contain.oneOf(METADATA_URIS) 70 | }); 71 | 72 | it('should revert when trying to create a CandyMachine contract with an empty array of metadata URIs', async function () { 73 | await expect(this.candyMachineFactory.newCandyMachine([], 1, this.hardhatVrfCoordinatorV2Mock.address, { from: this.owner })).to.be.reverted; 74 | }); 75 | 76 | it('should emit a Creation event when new newCandyMachine is created', async function () { 77 | await expect(this.candyMachineFactory.newCandyMachine(METADATA_URIS, 1, this.hardhatVrfCoordinatorV2Mock.address, { from: this.owner })).to.emit(this.candyMachineFactory, "Creation"); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-chai-matchers"); 2 | require("@nomiclabs/hardhat-ethers"); 3 | require("@openzeppelin/hardhat-upgrades"); 4 | require("@nomiclabs/hardhat-etherscan"); 5 | require('hardhat-contract-sizer'); 6 | 7 | module.exports = { 8 | solidity: "0.8.7", 9 | networks: { 10 | goerli: { 11 | url: `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`, 12 | accounts: [process.env.PRI_KEY, process.env.PRI_KEY_2], 13 | }, 14 | }, 15 | etherscan: { 16 | apiKey: process.env.ETHERSCAN_API_KEY, 17 | }, 18 | paths: { 19 | root: "./ethereum" 20 | }, 21 | contractSizer: { 22 | alphaSort: true, 23 | disambiguatePaths: false, 24 | runOnCompile: true, 25 | strict: true, 26 | only: [], 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-project", 3 | "devDependencies": { 4 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.4", 5 | "@nomiclabs/hardhat-ethers": "^2.2.0", 6 | "@nomiclabs/hardhat-etherscan": "^3.1.1", 7 | "@openzeppelin/hardhat-upgrades": "^1.21.0", 8 | "@poanet/solidity-flattener": "^3.0.8", 9 | "chai": "^4.3.7", 10 | "ethers": "^5.7.1", 11 | "hardhat": "^2.12.0", 12 | "hardhat-contract-sizer": "^2.6.1", 13 | "mocha": "^10.1.0" 14 | }, 15 | "dependencies": { 16 | "@chainlink/contracts": "^0.5.1", 17 | "@openzeppelin/contracts": "^4.7.3", 18 | "@openzeppelin/contracts-upgradeable": "^4.7.3", 19 | "@project-serum/anchor": "^0.26.0", 20 | "@solana/spl-token": "^0.3.7" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /solana/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | -------------------------------------------------------------------------------- /solana/Anchor.toml: -------------------------------------------------------------------------------- 1 | [features] 2 | seeds = false 3 | 4 | [programs.localnet] 5 | cupcake = "cakeGJxEdGpZ3MJP8sM3QypwzuzZpko1ueonUQgKLPE" 6 | 7 | [programs.devnet] 8 | cupcake = "cakeGJxEdGpZ3MJP8sM3QypwzuzZpko1ueonUQgKLPE" 9 | 10 | [programs.mainnet] 11 | cupcake = "cakeGJxEdGpZ3MJP8sM3QypwzuzZpko1ueonUQgKLPE" 12 | 13 | [registry] 14 | url = "https://anchor.projectserum.com" 15 | 16 | [provider] 17 | cluster = "localnet" 18 | wallet = "./auth.json" 19 | 20 | [test.validator] 21 | url = "https://api.mainnet-beta.solana.com" 22 | 23 | [[test.validator.clone]] 24 | address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" 25 | [[test.validator.clone]] 26 | address = "auth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmgg" 27 | [[test.validator.clone]] 28 | address = "DsRmdpRZJwagptu4MMN7GJWaPuwPgStWPUSbfAinYCg9" 29 | [[test.validator.clone]] 30 | address = "DbmHBMDepTnKyTnSccvji5FJv8tDGjEoo6ivV2qR4tY2" 31 | 32 | [scripts] 33 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 34 | -------------------------------------------------------------------------------- /solana/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | 6 | [profile.release] 7 | overflow-checks = true 8 | lto = "fat" 9 | codegen-units = 1 10 | 11 | [profile.release.build-override] 12 | opt-level = 3 13 | incremental = false 14 | codegen-units = 1 -------------------------------------------------------------------------------- /solana/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@metaplex-foundation/mpl-token-auth-rules": "^1.2.0", 4 | "@metaplex-foundation/mpl-token-metadata": "^2.9.1", 5 | "@msgpack/msgpack": "^3.0.0-beta2", 6 | "@project-serum/anchor": "0.26.0", 7 | "@solana/web3.js": "1.74.0", 8 | "@solana/spl-token": "0.3.7" 9 | }, 10 | "devDependencies": { 11 | "@types/mocha": "^9.0.0", 12 | "chai": "^4.3.4", 13 | "mocha": "^9.0.3", 14 | "ts-mocha": "^10.0.0", 15 | "typescript": "^4.3.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /solana/packages/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-cupcake-contract", 3 | "version": "1.0.3", 4 | "typings": "dist/cupcake_program", 5 | "main": "./dist/cupcake_program", 6 | "repository": "https://github.com/cupcake/contract.git", 7 | "author": "0xCold and bhgames", 8 | "license": "MIT", 9 | "pkg": { 10 | "scripts": "./build/**/*.{js|json}" 11 | }, 12 | "scripts": { 13 | "build": "tsc -p ./src" 14 | }, 15 | "dependencies": { 16 | "@metaplex-foundation/mpl-token-auth-rules": "^1.2.0", 17 | "@metaplex-foundation/mpl-token-metadata": "^2.9.1", 18 | "@msgpack/msgpack": "^3.0.0-beta2", 19 | "@project-serum/anchor": "0.21.0", 20 | "@solana/spl-token": "^0.3.7", 21 | "@solana/web3.js": "1.32.0", 22 | "loglevel": "^1.8.1", 23 | "ts-node": "^10.7.0", 24 | "typescript": "^4.6.3" 25 | }, 26 | "resolutions": { 27 | "@solana/web3.js": "1.32.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /solana/packages/sdk/src/cupcake_program.ts: -------------------------------------------------------------------------------- 1 | import { MasterEditionV2, Metadata } from '@metaplex-foundation/mpl-token-metadata'; 2 | import { Provider, BN, BorshAccountsCoder, Program, Wallet } from '@project-serum/anchor'; 3 | import NodeWallet from '@project-serum/anchor/dist/cjs/nodewallet'; 4 | import { getAssociatedTokenAddress, getMint } from '@solana/spl-token'; 5 | import { Connection, Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js'; 6 | import { CANDY_MACHINE_ADDRESS, getCandyMachineCreator, getCollectionAuthorityRecordPDA, getCollectionPDA, getMasterEdition, getMetadata } from './utils/mpl'; 7 | import { getCluster, WRAPPED_SOL_MINT } from './utils/solana'; 8 | import { sendTransactions, SequenceType, sendPreppedTransactions } from './utils/transaction'; 9 | import { CupcakeInstruction } from './instructions'; 10 | import { getUserHotPotatoToken } from './pda'; 11 | 12 | export const PREFIX = 'cupcake'; 13 | 14 | export const CUPCAKE_PROGRAM_ID = new PublicKey('cakeGJxEdGpZ3MJP8sM3QypwzuzZpko1ueonUQgKLPE'); 15 | export const TOKEN_METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'); 16 | 17 | export const transactionHelper = { sendPreppedTransactions }; 18 | 19 | export enum TagType { 20 | LimitedOrOpenEdition, 21 | SingleUse1Of1, 22 | CandyMachineDrop, 23 | Refillable1Of1, 24 | WalletRestrictedFungible, 25 | HotPotato, 26 | ProgrammableUnique 27 | } 28 | 29 | export interface AnchorTagType { 30 | limitedOrOpenEdition?: boolean; 31 | singleUse1Of1?: boolean; 32 | candyMachineDrop?: boolean; 33 | refillable1Of1?: boolean; 34 | walletRestrictedFungible?: boolean; 35 | hotPotato?: boolean; 36 | programmableUnique?: boolean; 37 | } 38 | 39 | export interface Config { 40 | authority: PublicKey; 41 | bump: number; 42 | } 43 | 44 | export interface Tag { 45 | uid: BN; 46 | tagType: AnchorTagType; 47 | tagAuthority: PublicKey; 48 | config: PublicKey; 49 | totalSupply: Number; 50 | numClaimed: Number; 51 | perUser: Number; 52 | minterPays: boolean; 53 | tokenMint: PublicKey; 54 | candyMachine: PublicKey; 55 | whitelistMint: PublicKey; 56 | whitelistBurn: PublicKey; 57 | bump: Number; 58 | currentTokenLocation: PublicKey; 59 | } 60 | 61 | export interface UserInfo { 62 | numClaimed: Number; 63 | bump: number; 64 | } 65 | 66 | export interface InitializeAccounts { 67 | authorityKeypair?: Keypair; 68 | authority?: PublicKey; 69 | } 70 | 71 | export interface AddOrRefillTagParams { 72 | uid: BN; 73 | tagType: AnchorTagType; 74 | numClaims: BN; 75 | perUser: BN; 76 | minterPays: boolean; 77 | // candy only 78 | pricePerMint?: BN | null; 79 | whitelistBurn?: boolean; 80 | } 81 | 82 | export interface AddOrRefillTagAccounts { 83 | authority?: PublicKey; 84 | authorityKeypair?: Keypair; 85 | tagAuthorityKeypair?: Keypair; 86 | tagAuthority?: PublicKey; 87 | tokenMint?: PublicKey; 88 | candyMachine?: PublicKey; 89 | whitelistMint?: PublicKey; 90 | paymentTokenMint?: PublicKey; 91 | } 92 | 93 | export interface ClaimTagParams { 94 | creatorBump?: number; 95 | minterPays?: boolean; 96 | } 97 | 98 | export interface ClaimTagAccounts { 99 | userKeypair?: Keypair; 100 | user?: PublicKey; 101 | tagAuthority?: PublicKey; 102 | tagAuthorityKeypair?: Keypair; 103 | tag: PublicKey; 104 | newTokenMint?: PublicKey; 105 | newMintAuthorityKeypair?: Keypair; 106 | newMintAuthority?: PublicKey; 107 | updateAuthority?: PublicKey; 108 | candyMachine?: PublicKey; 109 | candyMachineWallet?: PublicKey; 110 | collectionMint?: PublicKey; 111 | collectionMetadata?: PublicKey; 112 | collectionMasterEdition?: PublicKey; 113 | collectionAuthorityRecord?: PublicKey; 114 | candyMachineAuthority?: PublicKey; 115 | } 116 | 117 | export interface ClaimTagAdditionalArgs { 118 | tag: Tag; 119 | config: Config; 120 | nextEdition?: BN; 121 | createAta: boolean; 122 | candyProgram?: Program; 123 | } 124 | 125 | export class CupcakeProgram { 126 | id: PublicKey; 127 | program: Program; 128 | candyProgram?: Program; 129 | instruction: CupcakeInstruction; 130 | 131 | constructor(args: { id: PublicKey; program: Program }) { 132 | this.id = args.id; 133 | this.program = args.program; 134 | 135 | this.instruction = new CupcakeInstruction({ 136 | id: this.id, 137 | program: this.program, 138 | }); 139 | } 140 | 141 | async getCandyProgram() { 142 | if (this.candyProgram) return this.candyProgram; 143 | 144 | const idl = await Program.fetchIdl(CANDY_MACHINE_ADDRESS, this.program.provider); 145 | const program = new Program(idl, CANDY_MACHINE_ADDRESS, this.program.provider); 146 | this.candyProgram = program; 147 | return program; 148 | } 149 | 150 | async initialize( 151 | args = {}, 152 | accounts: InitializeAccounts 153 | ): Promise<{ 154 | transactions: { instructions: TransactionInstruction[]; signers: Keypair[] }[]; 155 | rpc: () => Promise<{ number: number; txs: { txid: string; slot: number }[] }>; 156 | }> { 157 | const { transactions } = await this.instruction.initialize(args, accounts); 158 | return { 159 | transactions, 160 | rpc: async () => 161 | await sendTransactions( 162 | (this.program.provider as Provider).connection, 163 | (this.program.provider as Provider).wallet, 164 | transactions.map((t) => t.instructions), 165 | transactions.map((t) => t.signers) 166 | ), 167 | }; 168 | } 169 | 170 | async addOrRefillTag( 171 | args: AddOrRefillTagParams, 172 | accounts: AddOrRefillTagAccounts 173 | ): Promise<{ 174 | transactions: { instructions: TransactionInstruction[]; signers: Keypair[] }[]; 175 | 176 | rpc: () => Promise<{ number: number; txs: { txid: string; slot: number }[] }>; 177 | }> { 178 | if ( 179 | (accounts.whitelistMint == undefined || 180 | accounts.paymentTokenMint == undefined || 181 | args.pricePerMint == undefined) && 182 | accounts.candyMachine 183 | ) { 184 | const candyProgram = await this.getCandyProgram(); 185 | 186 | const cm = await candyProgram.account.candyMachine.fetch(accounts.candyMachine); 187 | 188 | //@ts-ignore 189 | if (cm.data.whitelistMintSettings) { 190 | //@ts-ignore 191 | accounts.whitelistMint = cm.data.whitelistMintSettings.mint; 192 | //@ts-ignore 193 | if (cm.data.whitelistMintSettings.mode.burnEveryTime) { 194 | args.whitelistBurn = true; 195 | } 196 | } 197 | 198 | //@ts-ignore 199 | if (cm.tokenMint && !cm.tokenMint.equals(WRAPPED_SOL_MINT)) { 200 | //@ts-ignore 201 | accounts.paymentTokenMint = cm.tokenMint; 202 | } 203 | 204 | //@ts-ignore 205 | args.pricePerMint = cm.data.price; 206 | } else if (!accounts.candyMachine) { 207 | const mintInfo = await getMint(this.program.provider.connection, new PublicKey(accounts.tokenMint)); 208 | 209 | const mantissa = 10 ** mintInfo.decimals; 210 | 211 | args.perUser = new BN(args.perUser.toNumber() * mantissa); 212 | args.numClaims = new BN(args.numClaims.toNumber() * mantissa); 213 | } 214 | 215 | const { transactions } = await this.instruction.addOrRefillTag(args, accounts); 216 | 217 | return { 218 | transactions, 219 | rpc: async () => 220 | await sendTransactions( 221 | (this.program.provider as Provider).connection, 222 | (this.program.provider as Provider).wallet, 223 | transactions.map((t) => t.instructions), 224 | transactions.map((t) => t.signers) 225 | ), 226 | }; 227 | } 228 | 229 | async claimTag(args: ClaimTagParams, accounts: ClaimTagAccounts): 230 | Promise<{ 231 | transactions: { instructions: TransactionInstruction[]; signers: Keypair[]; feePayer: PublicKey }[]; 232 | rpc: () => Promise<{ number: number; txs: { txid: string; slot: number }[] }>; 233 | }> 234 | { 235 | const tag = (await this.program.account.tag.fetch(accounts.tag)) as Tag; 236 | const config = (await this.program.account.config.fetch(tag.config)) as Config; 237 | 238 | let createAta = false; 239 | let nextEdition = undefined; 240 | 241 | args.creatorBump = 0; 242 | args.minterPays = tag.minterPays; 243 | const candyProgram = await this.getCandyProgram(); 244 | const user = 245 | accounts.user || accounts.userKeypair?.publicKey || (this.program.provider as Provider).wallet.publicKey; 246 | 247 | if (tag.tagType.walletRestrictedFungible || tag.tagType.refillable1Of1 || tag.tagType.singleUse1Of1) { 248 | const userAta = await getAssociatedTokenAddress(tag.tokenMint, user); 249 | const exists = await this.program.provider.connection.getAccountInfo(userAta); 250 | if (!exists) createAta = true; 251 | } else if (tag.tagType.limitedOrOpenEdition) { 252 | const masterEdition = await this.program.provider.connection.getAccountInfo( 253 | await getMasterEdition(tag.tokenMint) 254 | ); 255 | 256 | const metadata = await this.program.provider.connection.getAccountInfo(await getMetadata(tag.tokenMint)); 257 | 258 | const meObj = MasterEditionV2.fromAccountInfo(masterEdition)[0]; 259 | const mdObj = Metadata.fromAccountInfo(metadata)[0]; 260 | nextEdition = (new BN(meObj.supply)).toNumber() + 1; 261 | accounts.updateAuthority = mdObj.updateAuthority; 262 | } else if (tag.tagType.candyMachineDrop) { 263 | const candyMachine = await candyProgram.account.candyMachine.fetch(tag.candyMachine); 264 | 265 | accounts.candyMachineWallet = candyMachine.wallet; 266 | accounts.candyMachine = tag.candyMachine; 267 | 268 | accounts.candyMachineAuthority = candyMachine.authority; 269 | 270 | args.creatorBump = (await getCandyMachineCreator(tag.candyMachine))[1]; 271 | const collectionPDA = (await getCollectionPDA(tag.candyMachine))[0]; 272 | const collectionPDAAccount = await this.program.provider.connection.getAccountInfo(collectionPDA); 273 | 274 | if (collectionPDAAccount && candyMachine.data.retainAuthority) { 275 | const collectionPdaData = (await candyProgram.coder.accounts.decodeUnchecked( 276 | 'CollectionPDA', 277 | collectionPDAAccount.data 278 | )) as { 279 | mint: PublicKey; 280 | }; 281 | const collectionMint = collectionPdaData.mint; 282 | const collectionAuthorityRecord = (await getCollectionAuthorityRecordPDA(collectionMint, collectionPDA))[0]; 283 | 284 | if (collectionMint) { 285 | const collectionMetadata = await getMetadata(collectionMint); 286 | const collectionMasterEdition = await getMasterEdition(collectionMint); 287 | console.log('Collection PDA: ', collectionPDA.toBase58()); 288 | console.log('Authority: ', candyMachine.authority.toBase58()); 289 | 290 | accounts.collectionMint = collectionMint; 291 | accounts.collectionMetadata = collectionMetadata; 292 | accounts.collectionMasterEdition = collectionMasterEdition; 293 | accounts.collectionAuthorityRecord = collectionAuthorityRecord; 294 | } 295 | } 296 | } else if (tag.tagType.hotPotato) { 297 | args.creatorBump = (await getUserHotPotatoToken(this.program, tag.uid, config.authority, user, tag.tokenMint))[1]; 298 | } 299 | 300 | const addArgs: ClaimTagAdditionalArgs = { 301 | tag, 302 | config: (await this.program.account.config.fetch(tag.config)) as Config, 303 | createAta, 304 | nextEdition, 305 | candyProgram, 306 | }; 307 | 308 | const { transactions } = await this.instruction.claimTag(args, accounts, addArgs); 309 | 310 | return { 311 | transactions, 312 | rpc: async () => 313 | await sendTransactions( 314 | (this.program.provider as Provider).connection, 315 | (this.program.provider as Provider).wallet, 316 | transactions.map((t) => t.instructions), 317 | transactions.map((t) => t.signers), 318 | SequenceType.StopOnFailure, 319 | transactions.length > 1 ? 'finalized' : 'single', 320 | transactions[0].feePayer 321 | ), 322 | }; 323 | } 324 | } 325 | 326 | export async function getCupcakeProgram( 327 | anchorWallet: NodeWallet | Keypair, 328 | env: string, 329 | customRpcUrl: string 330 | ): Promise { 331 | if (customRpcUrl) console.log('USING CUSTOM URL', customRpcUrl); 332 | 333 | const solConnection = new Connection(customRpcUrl || getCluster(env)); 334 | 335 | if (anchorWallet instanceof Keypair) anchorWallet = new NodeWallet(anchorWallet); 336 | 337 | const provider = new Provider(solConnection, anchorWallet, { 338 | preflightCommitment: 'recent', 339 | }); 340 | 341 | const idl = await Program.fetchIdl(CUPCAKE_PROGRAM_ID, provider); 342 | 343 | const program = new Program(idl, CUPCAKE_PROGRAM_ID, provider); 344 | 345 | return new CupcakeProgram({ 346 | id: CUPCAKE_PROGRAM_ID, 347 | program, 348 | }); 349 | } 350 | -------------------------------------------------------------------------------- /solana/packages/sdk/src/pda.ts: -------------------------------------------------------------------------------- 1 | import { BN, Program } from "@project-serum/anchor"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | import { PREFIX } from "./cupcake_program"; 4 | import * as TokenMetadata from "@metaplex-foundation/mpl-token-metadata"; 5 | 6 | export const getConfig = async (program: Program, authority: PublicKey) => { 7 | return await PublicKey.findProgramAddress([Buffer.from(PREFIX), authority.toBuffer()], program.programId); 8 | }; 9 | 10 | export const getTag = async (program: Program, tagUID: BN, authority: PublicKey) => { 11 | return await PublicKey.findProgramAddress( 12 | [Buffer.from(PREFIX), authority.toBuffer(), tagUID.toBuffer('le', 8)], 13 | program.programId 14 | ); 15 | }; 16 | 17 | export const getUserInfo = async (program: Program, tagUID: BN, authority: PublicKey, user: PublicKey) => { 18 | return await PublicKey.findProgramAddress( 19 | [Buffer.from(PREFIX), authority.toBuffer(), tagUID.toBuffer('le', 8), user.toBuffer()], 20 | program.programId 21 | ); 22 | }; 23 | 24 | export const getUserHotPotatoToken = async ( 25 | program: Program, 26 | tagUID: BN, 27 | authority: PublicKey, 28 | user: PublicKey, 29 | tokenMint: PublicKey 30 | ) => { 31 | return await PublicKey.findProgramAddress( 32 | [Buffer.from(PREFIX), authority.toBuffer(), tagUID.toBuffer('le', 8), user.toBuffer(), tokenMint.toBuffer()], 33 | program.programId 34 | ); 35 | }; 36 | 37 | export async function getTokenRecordPDA(tokenMint: PublicKey, associatedToken: PublicKey) { 38 | return PublicKey.findProgramAddress( 39 | [ 40 | Buffer.from("metadata"), 41 | TokenMetadata.PROGRAM_ID.toBuffer(), 42 | tokenMint.toBuffer(), 43 | Buffer.from("token_record"), 44 | associatedToken.toBuffer() 45 | ], 46 | TokenMetadata.PROGRAM_ID, 47 | )[0] 48 | } -------------------------------------------------------------------------------- /solana/packages/sdk/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "outDir": "./../dist", 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "noImplicitAny": false, 9 | "removeComments": false, 10 | "isolatedModules": false, 11 | "experimentalDecorators": true, 12 | "downlevelIteration": true, 13 | "emitDecoratorMetadata": true, 14 | "noLib": false, 15 | "preserveConstEnums": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "resolveJsonModule": true, 18 | "lib": ["dom", "es2019"], 19 | "types": ["node"] 20 | }, 21 | "exclude": ["node_modules", "typings/browser", "typings/browser.d.ts"], 22 | "atom": { 23 | "rewriteTsconfig": false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /solana/packages/sdk/src/utils/mpl.ts: -------------------------------------------------------------------------------- 1 | import { Keypair, Connection, PublicKey, SystemProgram } from '@solana/web3.js'; 2 | import { createMint, createAssociatedTokenAccount, mintTo } from '@solana/spl-token'; 3 | import { 4 | PROGRAM_ADDRESS as METADATA_PROGRAM_ADDRESS, 5 | Creator, 6 | createCreateMetadataAccountV2Instruction, 7 | createCreateMasterEditionV3Instruction, 8 | } from '@metaplex-foundation/mpl-token-metadata'; 9 | import { BN, Program } from '@project-serum/anchor'; 10 | import { constructAndSendTx } from './solana'; 11 | import { TOKEN_METADATA_PROGRAM_ID } from '../cupcake_program'; 12 | 13 | export const MPL_METADATA_PROGRAM_ADDRESS = new PublicKey(METADATA_PROGRAM_ADDRESS); 14 | export const CANDY_MACHINE_ADDRESS = new PublicKey('DsRmdpRZJwagptu4MMN7GJWaPuwPgStWPUSbfAinYCg9'); 15 | 16 | export const MAX_NAME_LENGTH = 32; 17 | export const MAX_URI_LENGTH = 200; 18 | export const MAX_SYMBOL_LENGTH = 10; 19 | export const MAX_CREATOR_LEN = 32 + 1 + 1; 20 | export const MAX_CREATOR_LIMIT = 5; 21 | export const CONFIG_LINE_SIZE = 4 + 32 + 4 + 200; 22 | export const CONFIG_ARRAY_START = 23 | 8 + // key 24 | 32 + // authority 25 | 32 + //wallet 26 | 33 + // token mint 27 | 4 + 28 | 6 + // uuid 29 | 8 + // price 30 | 8 + // items available 31 | 9 + // go live 32 | 10 + // end settings 33 | 4 + 34 | MAX_SYMBOL_LENGTH + // u32 len + symbol 35 | 2 + // seller fee basis points 36 | 4 + 37 | MAX_CREATOR_LIMIT * MAX_CREATOR_LEN + // optional + u32 len + actual vec 38 | 8 + //max supply 39 | 1 + // is mutable 40 | 1 + // retain authority 41 | 1 + // option for hidden setting 42 | 4 + 43 | MAX_NAME_LENGTH + // name length, 44 | 4 + 45 | MAX_URI_LENGTH + // uri length, 46 | 32 + // hash 47 | 4 + // max number of lines; 48 | 8 + // items redeemed 49 | 1 + // whitelist option 50 | 1 + // whitelist mint mode 51 | 1 + // allow presale 52 | 9 + // discount price 53 | 32 + // mint key for whitelist 54 | 1 + 55 | 32 + 56 | 1; // gatekeeper 57 | 58 | export const getMetadata = async (mint: PublicKey) => { 59 | return ( 60 | await PublicKey.findProgramAddress( 61 | [Buffer.from('metadata'), MPL_METADATA_PROGRAM_ADDRESS.toBuffer(), mint.toBuffer()], 62 | MPL_METADATA_PROGRAM_ADDRESS 63 | ) 64 | )[0]; 65 | }; 66 | 67 | export const getMasterEdition = async (mint: PublicKey) => { 68 | return ( 69 | await PublicKey.findProgramAddress( 70 | [Buffer.from('metadata'), MPL_METADATA_PROGRAM_ADDRESS.toBuffer(), mint.toBuffer(), Buffer.from('edition')], 71 | MPL_METADATA_PROGRAM_ADDRESS 72 | ) 73 | )[0]; 74 | }; 75 | 76 | export const getCandyMachineCreator = async (candyMachine: PublicKey): Promise<[PublicKey, number]> => { 77 | return await PublicKey.findProgramAddress( 78 | [Buffer.from('candy_machine'), candyMachine.toBuffer()], 79 | CANDY_MACHINE_ADDRESS 80 | ); 81 | }; 82 | 83 | export const getCollectionPDA = async (candyMachineAddress: PublicKey): Promise<[PublicKey, number]> => { 84 | return await PublicKey.findProgramAddress( 85 | [Buffer.from('collection'), candyMachineAddress.toBuffer()], 86 | CANDY_MACHINE_ADDRESS 87 | ); 88 | }; 89 | 90 | export const getCollectionAuthorityRecordPDA = async ( 91 | mint: PublicKey, 92 | newAuthority: PublicKey 93 | ): Promise<[PublicKey, number]> => { 94 | return await PublicKey.findProgramAddress( 95 | [ 96 | Buffer.from('metadata'), 97 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 98 | mint.toBuffer(), 99 | Buffer.from('collection_authority'), 100 | newAuthority.toBuffer(), 101 | ], 102 | TOKEN_METADATA_PROGRAM_ID 103 | ); 104 | }; 105 | 106 | export const getEditionMarkPda = async (mint: PublicKey, edition: number): Promise => { 107 | const editionNumber = Math.floor(edition / 248); 108 | return ( 109 | await PublicKey.findProgramAddress( 110 | [ 111 | Buffer.from('metadata'), 112 | TOKEN_METADATA_PROGRAM_ID.toBuffer(), 113 | mint.toBuffer(), 114 | Buffer.from('edition'), 115 | Buffer.from(editionNumber.toString()), 116 | ], 117 | TOKEN_METADATA_PROGRAM_ID 118 | ) 119 | )[0]; 120 | }; 121 | 122 | export const getEdition = async (mint: PublicKey, number: Number) => { 123 | return ( 124 | await PublicKey.findProgramAddress( 125 | [ 126 | Buffer.from('metadata'), 127 | MPL_METADATA_PROGRAM_ADDRESS.toBuffer(), 128 | mint.toBuffer(), 129 | Buffer.from('edition'), 130 | Buffer.from(number.toString()), 131 | ], 132 | MPL_METADATA_PROGRAM_ADDRESS 133 | ) 134 | )[0]; 135 | }; 136 | 137 | export const createCreateMetadataAccountAccounts = async (keypair: Keypair, mint: PublicKey) => { 138 | return { 139 | metadata: await getMetadata(mint), 140 | mint: mint, 141 | mintAuthority: keypair.publicKey, 142 | payer: keypair.publicKey, 143 | updateAuthority: keypair.publicKey, 144 | }; 145 | }; 146 | 147 | export const createCreateMetadataAccountArgs = async ( 148 | uri: string, 149 | name: string, 150 | symbol: string, 151 | creators: Creator[], 152 | sellerFeeBasisPoints: number, 153 | isMutable: boolean 154 | ) => { 155 | return { 156 | createMetadataAccountArgsV2: { 157 | data: { 158 | collection: null, 159 | creators, 160 | name, 161 | sellerFeeBasisPoints, 162 | symbol, 163 | uri, 164 | uses: null, 165 | }, 166 | isMutable, 167 | }, 168 | }; 169 | }; 170 | 171 | export const createCreateMasterEditionAccountAccounts = async (keypair: Keypair, mint: PublicKey) => { 172 | return { 173 | edition: await getMasterEdition(mint), 174 | metadata: await getMetadata(mint), 175 | mint: mint, 176 | mintAuthority: keypair.publicKey, 177 | payer: keypair.publicKey, 178 | updateAuthority: keypair.publicKey, 179 | }; 180 | }; 181 | 182 | export const createCreateMasterEditionAccountArgs = async (maxSupply: number) => { 183 | return { 184 | createMasterEditionArgs: { 185 | maxSupply: new BN(maxSupply), 186 | }, 187 | }; 188 | }; 189 | 190 | export const createMetadataAccount = async ( 191 | connection: Connection, 192 | keypair: Keypair, 193 | mint: PublicKey, 194 | uri: string, 195 | name: string, 196 | symbol: string, 197 | creatorPubkeys: PublicKey[], 198 | creatorShares: number[], 199 | royaltyPercentage: number, 200 | isMutable: boolean 201 | ) => { 202 | return await constructAndSendTx( 203 | connection, 204 | [ 205 | createCreateMetadataAccountV2Instruction( 206 | await createCreateMetadataAccountAccounts(keypair, mint), 207 | await createCreateMetadataAccountArgs( 208 | uri, 209 | name, 210 | symbol, 211 | constructCreatorsArray(creatorPubkeys, creatorShares, keypair), 212 | royaltyPercentage, 213 | isMutable 214 | ) 215 | ), 216 | ], 217 | [keypair] 218 | ); 219 | }; 220 | 221 | export const createMasterEditionAccount = async ( 222 | connection: Connection, 223 | keypair: Keypair, 224 | mint: PublicKey, 225 | maxSupply: number 226 | ) => { 227 | return await constructAndSendTx( 228 | connection, 229 | [ 230 | createCreateMasterEditionV3Instruction( 231 | await createCreateMasterEditionAccountAccounts(keypair, mint), 232 | await createCreateMasterEditionAccountArgs(maxSupply) 233 | ), 234 | ], 235 | [keypair] 236 | ); 237 | }; 238 | 239 | export const constructCreatorsArray = (creators: PublicKey[], shares: number[], keypair: Keypair) => { 240 | return creators.map((c, i) => { 241 | return { 242 | address: c, 243 | share: shares[i], 244 | verified: c === keypair.publicKey, 245 | }; 246 | }); 247 | }; 248 | 249 | export const calcCandyAccountSize = (candyData: any) => { 250 | return ( 251 | CONFIG_ARRAY_START + 4 + 10 * CONFIG_LINE_SIZE + 8 + 2 * (Math.floor(candyData.itemsAvailable.toNumber() / 8) + 1) 252 | ); 253 | }; 254 | 255 | export const createCandyMachine = async (program: Program, candyData: any, keypair: Keypair) => { 256 | const candyAccount = Keypair.generate(); 257 | candyData.uuid = candyAccount.publicKey.toBase58().slice(0, 6); 258 | const size = calcCandyAccountSize(candyData); 259 | const createCandyAccountTx = SystemProgram.createAccount({ 260 | fromPubkey: keypair.publicKey, 261 | newAccountPubkey: candyAccount.publicKey, 262 | space: size, 263 | lamports: await program.provider.connection.getMinimumBalanceForRentExemption(size), 264 | programId: CANDY_MACHINE_ADDRESS, 265 | }); 266 | await program.methods 267 | .initializeCandyMachine(candyData) 268 | .accounts({ 269 | candyMachine: candyAccount.publicKey, 270 | wallet: keypair.publicKey, 271 | authority: keypair.publicKey, 272 | payer: keypair.publicKey, 273 | }) 274 | .preInstructions([createCandyAccountTx]) 275 | .signers([keypair, candyAccount]) 276 | .rpc(); 277 | return candyAccount.publicKey; 278 | }; 279 | 280 | export const mintNFT = async ( 281 | connection: Connection, 282 | keypair: Keypair, 283 | uri: string, 284 | totalSupply: number, 285 | creators: PublicKey[], 286 | shares: number[], 287 | royaltyPercentage: number, 288 | name: string, 289 | symbol: string, 290 | isMutable: boolean 291 | ) => { 292 | const mint = await createMint(connection, keypair, keypair.publicKey, null, 0); 293 | const ata = await createAssociatedTokenAccount(connection, keypair, mint, keypair.publicKey); 294 | await mintTo(connection, keypair, mint, ata, keypair, 1); 295 | const createMetadataAccountTx = await createMetadataAccount( 296 | connection, 297 | keypair, 298 | mint, 299 | uri, 300 | name, 301 | symbol, 302 | creators, 303 | shares, 304 | royaltyPercentage, 305 | isMutable 306 | ); 307 | const createMasterEditionAccountTx = await createMasterEditionAccount(connection, keypair, mint, totalSupply); 308 | return { mint, ata }; 309 | }; 310 | -------------------------------------------------------------------------------- /solana/packages/sdk/src/utils/solana.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Keypair, 3 | Connection, 4 | PublicKey, 5 | Transaction, 6 | TransactionInstruction, 7 | sendAndConfirmTransaction, 8 | SystemProgram, 9 | clusterApiUrl, 10 | } from "@solana/web3.js"; 11 | import { Provider, Program } from "@project-serum/anchor"; 12 | 13 | export const WRAPPED_SOL_MINT = new PublicKey( 14 | "So11111111111111111111111111111111111111112" 15 | ); 16 | 17 | export const constructAndSendTx = async ( 18 | connection: Connection, 19 | instructions: TransactionInstruction[], 20 | signers: Keypair[] 21 | ) => { 22 | const tx = new Transaction(); 23 | instructions.map((i) => tx.add(i)); 24 | return await sendAndConfirmTransaction(connection, tx, signers); 25 | }; 26 | 27 | export const sendSOL = async ( 28 | connection: Connection, 29 | from: Keypair, 30 | to: PublicKey, 31 | lamports: number 32 | ) => { 33 | return await constructAndSendTx( 34 | connection, 35 | [ 36 | SystemProgram.transfer({ 37 | fromPubkey: from.publicKey, 38 | toPubkey: to, 39 | lamports, 40 | }), 41 | ], 42 | [from] 43 | ); 44 | }; 45 | 46 | export const loadAnchorProgram = async ( 47 | programId: PublicKey, 48 | idlFile?: string, 49 | provider?: Provider 50 | ) => { 51 | let idl: any; 52 | if (!idlFile) { 53 | idl = await Program.fetchIdl(programId, provider); 54 | } else { 55 | idl = JSON.parse(await (await fetch(idlFile)).text()); 56 | } 57 | return new Program(idl, programId, provider); 58 | }; 59 | 60 | type Cluster = { 61 | name: string; 62 | url: string; 63 | }; 64 | export const CLUSTERS: Cluster[] = [ 65 | { 66 | name: "mainnet-beta", 67 | url: "https://api.metaplex.solana.com/", 68 | }, 69 | { 70 | name: "testnet", 71 | url: clusterApiUrl("testnet"), 72 | }, 73 | { 74 | name: "devnet", 75 | url: clusterApiUrl("devnet"), 76 | }, 77 | ]; 78 | export const DEFAULT_CLUSTER = CLUSTERS[2]; 79 | export function getCluster(name: string): string { 80 | for (const cluster of CLUSTERS) { 81 | if (cluster.name === name) { 82 | return cluster.url; 83 | } 84 | } 85 | return DEFAULT_CLUSTER.url; 86 | } 87 | -------------------------------------------------------------------------------- /solana/packages/sdk/src/utils/transaction.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from '@project-serum/anchor/dist/cjs/provider'; 2 | import { 3 | Blockhash, 4 | Commitment, 5 | Connection, 6 | FeeCalculator, 7 | Keypair, 8 | PublicKey, 9 | RpcResponseAndContext, 10 | SignatureStatus, 11 | SimulatedTransactionResponse, 12 | Transaction, 13 | TransactionInstruction, 14 | TransactionSignature, 15 | } from '@solana/web3.js'; 16 | import log from 'loglevel'; 17 | 18 | export const DEFAULT_TIMEOUT = 15000; 19 | 20 | export const getUnixTs = () => { 21 | return new Date().getTime() / 1000; 22 | }; 23 | 24 | export function sleep(ms: number): Promise { 25 | return new Promise((resolve) => setTimeout(resolve, ms)); 26 | } 27 | 28 | interface BlockhashAndFeeCalculator { 29 | blockhash: Blockhash; 30 | feeCalculator: FeeCalculator; 31 | } 32 | 33 | export const sendTransactionWithRetryWithKeypair = async ( 34 | connection: Connection, 35 | wallet: Keypair, 36 | instructions: TransactionInstruction[], 37 | signers: Keypair[], 38 | commitment: Commitment = 'singleGossip', 39 | includesFeePayer: boolean = false, 40 | block?: BlockhashAndFeeCalculator, 41 | beforeSend?: () => void 42 | ) => { 43 | const transaction = new Transaction(); 44 | instructions.forEach((instruction) => transaction.add(instruction)); 45 | transaction.recentBlockhash = (block || (await connection.getRecentBlockhash(commitment))).blockhash; 46 | 47 | if (includesFeePayer) { 48 | transaction.setSigners(...signers.map((s) => s.publicKey)); 49 | } else { 50 | transaction.setSigners( 51 | // fee payed by the wallet owner 52 | wallet.publicKey, 53 | ...signers.map((s) => s.publicKey) 54 | ); 55 | } 56 | 57 | if (signers.length > 0) { 58 | transaction.sign(...[wallet, ...signers]); 59 | } else { 60 | transaction.sign(wallet); 61 | } 62 | 63 | if (beforeSend) { 64 | beforeSend(); 65 | } 66 | 67 | const { txid, slot } = await sendSignedTransaction({ 68 | connection, 69 | signedTransaction: transaction, 70 | }); 71 | 72 | return { txid, slot }; 73 | }; 74 | 75 | export async function sendTransactionWithRetry( 76 | connection: Connection, 77 | wallet: Wallet, 78 | instructions: Array, 79 | signers: Array, 80 | commitment: Commitment = 'singleGossip' 81 | ): Promise { 82 | const transaction = new Transaction(); 83 | instructions.forEach((instruction) => transaction.add(instruction)); 84 | // @ts-ignore 85 | const recentBlockhash = await connection._recentBlockhash( 86 | // @ts-ignore 87 | provider.connection._disableBlockhashCaching 88 | ) 89 | 90 | transaction.recentBlockhash = recentBlockhash; 91 | 92 | transaction.feePayer = wallet.publicKey; 93 | 94 | if (signers.length > 0) { 95 | transaction.partialSign(...signers); 96 | } 97 | 98 | wallet.signTransaction(transaction); 99 | 100 | return sendSignedTransaction({ 101 | connection, 102 | signedTransaction: transaction, 103 | }); 104 | } 105 | 106 | export async function sendSignedTransaction({ 107 | signedTransaction, 108 | connection, 109 | timeout = DEFAULT_TIMEOUT, 110 | }: { 111 | signedTransaction: Transaction; 112 | connection: Connection; 113 | sendingMessage?: string; 114 | sentMessage?: string; 115 | successMessage?: string; 116 | timeout?: number; 117 | }): Promise<{ txid: string; slot: number }> { 118 | const rawTransaction = signedTransaction.serialize(); 119 | const startTime = getUnixTs(); 120 | let slot = 0; 121 | const txid: TransactionSignature = await connection.sendRawTransaction(rawTransaction, { 122 | skipPreflight: true, 123 | }); 124 | 125 | log.debug('Started awaiting confirmation for', txid); 126 | 127 | let done = false; 128 | (async () => { 129 | while (!done && getUnixTs() - startTime < timeout) { 130 | connection.sendRawTransaction(rawTransaction, { 131 | skipPreflight: true, 132 | }); 133 | await sleep(500); 134 | } 135 | })(); 136 | try { 137 | const confirmation = await awaitTransactionSignatureConfirmation(txid, timeout, connection, 'confirmed', true); 138 | 139 | if (!confirmation) throw new Error('Timed out awaiting confirmation on transaction'); 140 | 141 | if (confirmation.err) { 142 | log.error(confirmation.err); 143 | throw new Error('Transaction failed: Custom instruction error'); 144 | } 145 | 146 | slot = confirmation?.slot || 0; 147 | } catch (err) { 148 | log.error('Timeout Error caught', err); 149 | if (err.timeout) { 150 | throw new Error('Timed out awaiting confirmation on transaction'); 151 | } 152 | let simulateResult: SimulatedTransactionResponse | null = null; 153 | try { 154 | simulateResult = (await simulateTransaction(connection, signedTransaction, 'single')).value; 155 | } catch (e) { 156 | log.error('Simulate Transaction error', e); 157 | } 158 | if (simulateResult && simulateResult.err) { 159 | if (simulateResult.logs) { 160 | for (let i = simulateResult.logs.length - 1; i >= 0; --i) { 161 | const line = simulateResult.logs[i]; 162 | if (line.startsWith('Program log: ')) { 163 | throw new Error('Transaction failed: ' + line.slice('Program log: '.length)); 164 | } 165 | } 166 | } 167 | throw new Error(JSON.stringify(simulateResult.err)); 168 | } 169 | log.error('Got this far.'); 170 | // throw new Error('Transaction failed'); 171 | } finally { 172 | done = true; 173 | } 174 | 175 | log.debug('Latency (ms)', txid, getUnixTs() - startTime); 176 | return { txid, slot }; 177 | } 178 | 179 | async function simulateTransaction( 180 | connection: Connection, 181 | transaction: Transaction, 182 | commitment: Commitment 183 | ): Promise> { 184 | // @ts-ignore 185 | const recentBlockhash = await connection._recentBlockhash( 186 | // @ts-ignore 187 | provider.connection._disableBlockhashCaching 188 | ) 189 | transaction.recentBlockhash = recentBlockhash; 190 | 191 | const signData = transaction.serializeMessage(); 192 | // @ts-ignore 193 | const wireTransaction = transaction._serialize(signData); 194 | const encodedTransaction = wireTransaction.toString('base64'); 195 | const config: any = { encoding: 'base64', commitment }; 196 | const args = [encodedTransaction, config]; 197 | 198 | // @ts-ignore 199 | const res = await connection._rpcRequest('simulateTransaction', args); 200 | if (res.error) { 201 | throw new Error('failed to simulate transaction: ' + res.error.message); 202 | } 203 | return res.result; 204 | } 205 | 206 | async function awaitTransactionSignatureConfirmation( 207 | txid: TransactionSignature, 208 | timeout: number, 209 | connection: Connection, 210 | commitment: Commitment = 'recent', 211 | queryStatus = false 212 | ): Promise { 213 | let done = false; 214 | let status: SignatureStatus | null | void = { 215 | slot: 0, 216 | confirmations: 0, 217 | err: null, 218 | }; 219 | let subId = 0; 220 | // eslint-disable-next-line no-async-promise-executor 221 | status = await new Promise(async (resolve, reject) => { 222 | setTimeout(() => { 223 | if (done) { 224 | return; 225 | } 226 | done = true; 227 | log.warn('Rejecting for timeout...'); 228 | reject({ timeout: true }); 229 | }, timeout); 230 | try { 231 | subId = connection.onSignature( 232 | txid, 233 | (result, context) => { 234 | done = true; 235 | status = { 236 | err: result.err, 237 | slot: context.slot, 238 | confirmations: 0, 239 | }; 240 | if (result.err) { 241 | log.warn('Rejected via websocket', result.err); 242 | reject(status); 243 | } else { 244 | log.debug('Resolved via websocket', result); 245 | resolve(status); 246 | } 247 | }, 248 | commitment 249 | ); 250 | } catch (e) { 251 | done = true; 252 | log.error('WS error in setup', txid, e); 253 | } 254 | while (!done && queryStatus) { 255 | // eslint-disable-next-line no-loop-func 256 | (async () => { 257 | try { 258 | const signatureStatuses = await connection.getSignatureStatuses([txid]); 259 | status = signatureStatuses && signatureStatuses.value[0]; 260 | if (!done) { 261 | if (!status) { 262 | log.debug('REST null result for', txid, status); 263 | } else if (status.err) { 264 | log.error('REST error for', txid, status); 265 | done = true; 266 | reject(status.err); 267 | } else if (!status.confirmations) { 268 | log.debug('REST no confirmations for', txid, status); 269 | } else { 270 | log.debug('REST confirmation for', txid, status); 271 | done = true; 272 | resolve(status); 273 | } 274 | } 275 | } catch (e) { 276 | if (!done) { 277 | log.error('REST connection error: txid', txid, e); 278 | } 279 | } 280 | })(); 281 | await sleep(2000); 282 | } 283 | }); 284 | 285 | //@ts-ignore 286 | if (connection._subscriptionsByHash[subId]) connection.removeSignatureListener(subId); 287 | done = true; 288 | log.debug('Returning status', status); 289 | return status; 290 | } 291 | 292 | export enum SequenceType { 293 | Sequential, 294 | Parallel, 295 | StopOnFailure, 296 | } 297 | 298 | export const sendTransactions = async ( 299 | connection: Connection, 300 | wallet: any, 301 | instructionSet: TransactionInstruction[][], 302 | signersSet: Keypair[][], 303 | sequenceType: SequenceType = SequenceType.Parallel, 304 | commitment: Commitment = 'singleGossip', 305 | feePayer: PublicKey = wallet.publicKey, 306 | successCallback: (txid: string, ind: number) => void = (txid, ind) => {}, 307 | failCallback: (reason: string, ind: number) => boolean = (txid, ind) => false, 308 | block?: BlockhashAndFeeCalculator, 309 | beforeTransactions: Transaction[] = [], 310 | afterTransactions: Transaction[] = [] 311 | ): Promise<{ number: number; txs: { txid: string; slot: number }[] }> => { 312 | if (!wallet.publicKey) throw new Error('Wallet not connected'); 313 | 314 | const unsignedTxns: Transaction[] = beforeTransactions; 315 | 316 | if (!block) { 317 | block = await connection.getRecentBlockhash(commitment); 318 | } 319 | 320 | for (let i = 0; i < instructionSet.length; i++) { 321 | const instructions = instructionSet[i]; 322 | const signers = signersSet[i]; 323 | 324 | if (instructions.length === 0) { 325 | continue; 326 | } 327 | 328 | let transaction = new Transaction(); 329 | instructions.forEach((instruction) => transaction.add(instruction)); 330 | const walletUsed = !!instructions.find((i) => i.keys.find((k) => k.isSigner && k.pubkey.equals(wallet.publicKey))); 331 | transaction.recentBlockhash = block.blockhash; 332 | transaction.setSigners( 333 | // fee payed by the wallet owner 334 | ...(walletUsed ? [wallet.publicKey] : []), 335 | ...signers.map((s) => s.publicKey) 336 | ); 337 | transaction.feePayer = feePayer; 338 | 339 | if (signers.length > 0) { 340 | transaction.partialSign(...signers); 341 | } 342 | 343 | unsignedTxns.push(transaction); 344 | } 345 | unsignedTxns.push(...afterTransactions); 346 | 347 | const partiallySignedTransactions = unsignedTxns.filter((t) => 348 | t.signatures.find((sig) => sig.publicKey.equals(wallet.publicKey)) 349 | ); 350 | const fullySignedTransactions = unsignedTxns.filter( 351 | (t) => !t.signatures.find((sig) => sig.publicKey.equals(wallet.publicKey)) 352 | ); 353 | let signedTxns = await wallet.signAllTransactions(partiallySignedTransactions); 354 | signedTxns = fullySignedTransactions.concat(signedTxns); 355 | return await sendPreppedTransactions(connection, signedTxns, sequenceType, successCallback, failCallback); 356 | }; 357 | 358 | export const sendPreppedTransactions = async ( 359 | connection: Connection, 360 | signedTxns: Transaction[], 361 | sequenceType: SequenceType = SequenceType.Parallel, 362 | successCallback: (txid: string, ind: number) => void = (txid, ind) => {}, 363 | failCallback: (reason: string, ind: number) => boolean = (txid, ind) => false 364 | ): Promise<{ number: number; txs: { txid: string; slot: number }[] }> => { 365 | const pendingTxns: Promise<{ txid: string; slot: number }>[] = []; 366 | 367 | for (let i = 0; i < signedTxns.length; i++) { 368 | const signedTxnPromise = sendSignedTransaction({ 369 | connection, 370 | signedTransaction: signedTxns[i], 371 | }); 372 | 373 | if (sequenceType !== SequenceType.Parallel) { 374 | try { 375 | await signedTxnPromise.then(({ txid, slot }) => successCallback(txid, i)); 376 | pendingTxns.push(signedTxnPromise); 377 | } catch (e) { 378 | console.log('Failed at txn index:', i); 379 | console.log('Caught failure:', e); 380 | 381 | failCallback(e, i); 382 | if (sequenceType === SequenceType.StopOnFailure) { 383 | return { 384 | number: i, 385 | txs: await Promise.all(pendingTxns), 386 | }; 387 | } 388 | } 389 | } else { 390 | pendingTxns.push(signedTxnPromise); 391 | } 392 | } 393 | 394 | if (sequenceType !== SequenceType.Parallel) { 395 | const result = await Promise.all(pendingTxns); 396 | return { number: signedTxns.length, txs: result }; 397 | } 398 | 399 | return { number: signedTxns.length, txs: await Promise.all(pendingTxns) }; 400 | }; 401 | -------------------------------------------------------------------------------- /solana/programs/cupcake/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cupcake" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "cupcake" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | no-idl = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | 18 | [dependencies] 19 | anchor-lang = { version = "0.27.0", features = ["init-if-needed"] } 20 | anchor-spl = "0.27.0" 21 | mpl-token-metadata = { version = "1.9.1", features = ["no-entrypoint"] } 22 | spl-associated-token-account = {version = "1.1.3", features = ["no-entrypoint"]} 23 | mpl-token-auth-rules = { version = "1.3.0", features = ["no-entrypoint"] } 24 | spl-token = { version = "3.5.0", features = ["no-entrypoint"] } 25 | arrayref = "0.3.6" 26 | rmp-serde = "1.1.1" -------------------------------------------------------------------------------- /solana/programs/cupcake/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/errors.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum ErrorCode { 5 | #[msg("The given tag cannot output any more tokens.")] 6 | TagDepleted, 7 | 8 | #[msg("The given user has already claimed the maximum amount of tokens from this tag.")] 9 | ClaimLimitExceeded, 10 | 11 | #[msg("The given tag can not be refilled.")] 12 | NotRefillable, 13 | 14 | #[msg("Must use candy machine specific actions")] 15 | CannotUseCandyMachineWithThisAction, 16 | 17 | #[msg("Numerical overflow")] 18 | NumericalOverflowError, 19 | 20 | #[msg("Key mismatch")] 21 | PublicKeyMismatch, 22 | 23 | #[msg("ATA should not have delegate")] 24 | AtaShouldNotHaveDelegate, 25 | 26 | #[msg("This ATA should have this config as delegate")] 27 | AtaDelegateShouldBeConfig, 28 | 29 | #[msg("Incorrect owner")] 30 | IncorrectOwner, 31 | 32 | #[msg("Uninitialized")] 33 | Uninitialized, 34 | 35 | #[msg("Cannot create a tag that does not have a whitelist token deposit if user is not required to provide it")] 36 | MustProvideWhitelistTokenIfMinterIsNotProvidingIt, 37 | 38 | #[msg("Must provide payment account if minter is not providing it")] 39 | MustProvidePaymentAccountIfMinterIsNotProviding, 40 | 41 | #[msg("Must use config as payer")] 42 | MustUseConfigAsPayer, 43 | 44 | #[msg("Single use 1/1s are not reconfigurable")] 45 | SingleUseIsImmutable, 46 | 47 | #[msg("This tag requires that someone other than config authority pay for the mint")] 48 | AuthorityShouldNotBePayer, 49 | 50 | #[msg("Hot potato is immutable unless the token is in an ATA on the config authority wallet.")] 51 | CanOnlyMutateHotPotatoWhenAtHome, 52 | 53 | #[msg("This pNFT rule is not supported by Cupcake yet.")] 54 | ProgrammableRuleNotSupported, 55 | 56 | #[msg("Hot Potatos can not be pNFTs")] 57 | HotPotatoCanNotBeProgrammable, 58 | 59 | #[msg("Invalid seeds provided")] 60 | InvalidSeeds, 61 | 62 | #[msg("Cannot claim during this state")] 63 | InvalidListingState, 64 | 65 | #[msg("Cannot change price setting during this state")] 66 | CannotChangePriceSettingsInThisState, 67 | 68 | #[msg("If you are going to use a price_mint, you need to send up the listing token account to modify this listing")] 69 | MustSendUpListingTokenAccount, 70 | 71 | #[msg("If you are going to use a price_mint, you need to send up the price mint to modify this listing")] 72 | MustSendUpPriceMint, 73 | 74 | #[msg("Numerical Overflow")] 75 | NumericalOverflow, 76 | 77 | #[msg("Listing has been scanned and is now frozen")] 78 | ListingFrozen, 79 | 80 | #[msg("Can only accept a bid from the accept endpoint")] 81 | CannotAcceptFromModify, 82 | 83 | #[msg("Can only change price, not its mint type, while in for sale mode")] 84 | CannotChangePriceMintInThisState, 85 | 86 | #[msg("Can only scan from claim")] 87 | CannotScanFromModify, 88 | 89 | #[msg("No payer token account present")] 90 | NoPayerTokenPresent, 91 | 92 | #[msg("No buyer present")] 93 | NoBuyerPresent, 94 | 95 | #[msg("No transfer authority present")] 96 | NoTransferAuthorityPresent, 97 | 98 | #[msg("No price mint present")] 99 | NoPriceMintPresent, 100 | 101 | #[msg("No token metadata for this tag present but is required for this transaction")] 102 | NoTokenMetadataPresent, 103 | 104 | #[msg("No seller ata present")] 105 | NoSellerAtaPresent, 106 | 107 | #[msg("No ata program present")] 108 | NoAtaProgramPresent, 109 | 110 | #[msg("Price mint mismatch")] 111 | PriceMintMismatch, 112 | 113 | #[msg("Payer must sign when listing is active")] 114 | PayerMustSign, 115 | 116 | #[msg("Cannot delete listing unless it is cancelled or accepted")] 117 | CannotDeleteListingInThisState, 118 | 119 | #[msg("Cannot close listing if token account has a balance greater than zero")] 120 | ListingTokenHasBalance, 121 | 122 | #[msg("Seller must be token holder")] 123 | SellerMustBeLister, 124 | 125 | #[msg("Must hold token to sell")] 126 | MustHoldTokenToSell, 127 | 128 | #[msg("User must be a signer to use hot potato mode")] 129 | UserMustSign, 130 | 131 | #[msg("Need agreed price to goto scanned")] 132 | NeedAgreedPrice, 133 | 134 | #[msg("Need chosen buyer")] 135 | NeedBuyer, 136 | 137 | #[msg("Seller must initiate the listing")] 138 | SellerMustInitiateSale, 139 | 140 | #[msg("Listing not for sale")] 141 | ListingNotForSale, 142 | 143 | #[msg("Must bid at least 0.001 SOL")] 144 | MinimumOffer, 145 | 146 | #[msg("Must use seller as payer")] 147 | MustUseSellerAsPayer, 148 | 149 | #[msg("Seller does not match")] 150 | SellerMismatch, 151 | 152 | #[msg("Cannot claim a vaulted token")] 153 | CannotClaimVaulted, 154 | 155 | #[msg("Cannot return to these states once buyer is set")] 156 | ChosenBuyerSet, 157 | 158 | #[msg("Invalid NFT Vault Transition")] 159 | InvalidVaultTransition, 160 | 161 | #[msg("The only use who can claim while in transit is the vault authority")] 162 | CanOnlyClaimAsVaultAuthority, 163 | 164 | #[msg("Cannot move to this state without a chosen buyer")] 165 | MustChooseBuyer, 166 | 167 | #[msg("Offer not accepted")] 168 | OfferNotAccepted, 169 | 170 | #[msg("Not vault authority")] 171 | NotVaultAuthority, 172 | 173 | #[msg("Not vaulted")] 174 | NotVaulted, 175 | } 176 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/instructions/accept_offer/mod.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_spl::associated_token::AssociatedToken; 3 | use anchor_spl::token::{Token, Mint}; 4 | use crate::state::{PDA_PREFIX, LISTING, Listing, ListingState, Offer, TOKEN, OFFER}; 5 | use crate::state::{bakery::*, sprinkle::*}; 6 | use crate::utils::{empty_offer_escrow_to_seller, EmptyOfferEscrowToSellerArgs}; 7 | use crate::errors::ErrorCode; 8 | 9 | 10 | #[derive(Accounts)] 11 | pub struct AcceptOffer<'info> { 12 | /// Account that is signing off on the offer acceptance. Can be seller, buyer, or a third party. 13 | /// Can be seller in any circumstance. 14 | /// Can be buyer if the offer is above or at the set_price. 15 | /// Can be anybody if the offer is above or at the set_price. 16 | pub signer: Signer<'info>, 17 | 18 | /// PDA which stores token approvals for a Bakery, and executes the transfer during claims. 19 | #[account(mut)] 20 | pub config: Box>, 21 | 22 | /// PDA which stores data about the state of a Sprinkle. 23 | #[account(mut)] 24 | pub tag: Box>, 25 | 26 | /// PDA which stores data about the state of a listing. 27 | #[account(mut, 28 | constraint=signer.key() == listing.seller || (listing.set_price.is_some() && offer.offer_amount >= listing.set_price.unwrap()), 29 | seeds = [ 30 | PDA_PREFIX, 31 | config.authority.key().as_ref(), 32 | &tag.uid.to_le_bytes(), 33 | LISTING 34 | ], 35 | bump=listing.bump)] 36 | pub listing: Box>, 37 | 38 | #[account(mut, 39 | seeds=[ 40 | PDA_PREFIX, 41 | config.authority.key().as_ref(), 42 | &tag.uid.to_le_bytes(), 43 | LISTING, 44 | OFFER, 45 | buyer.key().as_ref() 46 | ], 47 | bump=offer.bump 48 | )] 49 | pub offer: Box>, 50 | 51 | /// Buyer 52 | /// CHECK: this is safe 53 | #[account(mut)] 54 | pub buyer: UncheckedAccount<'info>, 55 | 56 | /// Buyer 57 | /// CHECK: this is safe 58 | #[account(mut, constraint=seller.key() == listing.seller)] 59 | pub seller: UncheckedAccount<'info>, 60 | 61 | /// Original fee payer, to receive lamports back 62 | /// CHECK: this is safe 63 | #[account(mut, constraint=original_fee_payer.key() == offer.fee_payer)] 64 | pub original_fee_payer: UncheckedAccount<'info>, 65 | 66 | 67 | /// CHECK: this is safe 68 | #[account(mut, 69 | seeds=[ 70 | PDA_PREFIX, 71 | config.authority.key().as_ref(), 72 | &tag.uid.to_le_bytes(), 73 | LISTING, 74 | OFFER, 75 | buyer.key().as_ref(), 76 | TOKEN 77 | ], bump)] 78 | pub offer_token: UncheckedAccount<'info>, 79 | 80 | // ATA of seller for price mint, if necessary 81 | #[account(mut)] 82 | pub seller_ata: Option>, 83 | 84 | 85 | /// Mint of type of money you want to be accepted for this listing 86 | pub price_mint: Option>, 87 | 88 | 89 | /// CHECK: No 90 | pub token_metadata: UncheckedAccount<'info>, 91 | 92 | pub ata_program: Program<'info, AssociatedToken>, 93 | 94 | /// SPL System Program, required for account allocation. 95 | pub system_program: Program<'info, System>, 96 | 97 | /// SPL Token Program, required for transferring tokens. 98 | pub token_program: Program<'info, Token>, 99 | 100 | /// Rent 101 | pub rent: Sysvar<'info, Rent>, 102 | 103 | // Remaining accounts: 104 | // OUR_ADDRESS (w) sol account for collecting fees 105 | // OUR_ADDRESS (w) ata account for collecting price mint fees 106 | // royalty sol account (w) followed by royalty ata (w) pair from NFT (derive ATA from royalty accounts) [up to 5] 107 | // Note you only need to pass the ata account after the sol account IF using a price mint, otherwise its only the sol accounts 108 | } 109 | 110 | 111 | 112 | pub fn handler<'a, 'b, 'c, 'info>( 113 | ctx: Context<'a, 'b, 'c, 'info, AcceptOffer<'info>> 114 | ) -> Result<()> { 115 | let config = &mut ctx.accounts.config; 116 | let tag = &mut ctx.accounts.tag; 117 | let listing = &mut ctx.accounts.listing; 118 | let offer = &mut ctx.accounts.offer; 119 | let original_fee_payer = &ctx.accounts.original_fee_payer; 120 | let system_program = &ctx.accounts.system_program; 121 | let buyer = &ctx.accounts.buyer; 122 | let token_metadata = &ctx.accounts.token_metadata; 123 | let ata_program = &ctx.accounts.ata_program; 124 | let price_mint = &ctx.accounts.price_mint; 125 | let offer_token = &ctx.accounts.offer_token; 126 | let signer = &ctx.accounts.signer; 127 | let seller = &ctx.accounts.seller; 128 | let seller_ata = &ctx.accounts.seller_ata; 129 | let authority = config.authority.key(); 130 | let buyer_key = buyer.key(); 131 | let offer_seeds = &[ 132 | PDA_PREFIX, 133 | authority.as_ref(), 134 | &tag.uid.to_le_bytes(), 135 | LISTING, 136 | OFFER, 137 | buyer_key.as_ref(), 138 | &[offer.bump] 139 | ]; 140 | 141 | let offer_token_seeds = &[ 142 | PDA_PREFIX, 143 | authority.as_ref(), 144 | &tag.uid.to_le_bytes(), 145 | LISTING, 146 | OFFER, 147 | buyer_key.as_ref(), 148 | TOKEN, 149 | &[*ctx.bumps.get("offer_token").unwrap()] 150 | ]; 151 | 152 | listing.agreed_price = Some(offer.offer_amount); 153 | listing.chosen_buyer = Some(offer.buyer); 154 | 155 | tag.vault_authority = Some(buyer_key); 156 | 157 | listing.state = ListingState::Accepted; 158 | 159 | if listing.vaulted_preferred { 160 | // Need to add shifting logic here. 161 | // It remains vaulted, but the buyer now becomes the holder. 162 | tag.vault_state = VaultState::Vaulted; 163 | } else { 164 | // Moves to accepted on the way to Shipped -> Scanned.. 165 | // Presumably a memo will be in the txn to indicate the shipping address. 166 | // If we want to get super-semantic we can check for it. 167 | tag.vault_state = VaultState::UnvaultingRequested; 168 | } 169 | 170 | let tm = token_metadata.to_account_info(); 171 | let pm = if listing.price_mint.is_none() { 172 | system_program.to_account_info() 173 | } else { 174 | price_mint.clone().unwrap().to_account_info() 175 | }; 176 | let ap = ata_program.to_account_info(); 177 | let rent = ctx.accounts.rent.to_account_info(); 178 | 179 | require!(listing.price_mint.is_none() || Some(pm.key()) == listing.price_mint, ErrorCode::PriceMintMismatch); 180 | 181 | empty_offer_escrow_to_seller(EmptyOfferEscrowToSellerArgs { 182 | remaining_accounts: ctx.remaining_accounts, 183 | config, 184 | tag, 185 | listing: &listing, 186 | offer: &offer, 187 | offer_token_account: &offer_token.to_account_info(), 188 | offer_seeds, 189 | offer_token_seeds, 190 | token_metadata: &tm, 191 | payer: &signer, 192 | price_mint: &pm, 193 | ata_program: &ap, 194 | token_program: &ctx.accounts.token_program, 195 | system_program: &ctx.accounts.system_program, 196 | rent: &rent, 197 | seller: &seller, 198 | seller_ata: match seller_ata { 199 | Some(val) => val, 200 | None => &seller, 201 | }, 202 | program_id: ctx.program_id, 203 | })?; 204 | 205 | offer.close(original_fee_payer.to_account_info())?; 206 | Ok(()) 207 | } 208 | 209 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/instructions/cancel_offer/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | use anchor_lang::prelude::*; 3 | use anchor_spl::token::{self, Token, TokenAccount, Mint}; 4 | use crate::errors::ErrorCode; 5 | use crate::state::{PDA_PREFIX, LISTING, Listing, Offer, TOKEN, OFFER, ListingState}; 6 | use crate::state::{bakery::*, sprinkle::*}; 7 | use crate::utils::{ 8 | assert_is_ata, 9 | }; 10 | 11 | 12 | #[derive(Accounts)] 13 | #[instruction(buyer: Pubkey)] 14 | pub struct CancelOffer<'info> { 15 | /// Account which will receive lamports back from the offer. 16 | /// CHECK: this is safe 17 | #[account(mut, constraint=offer.fee_payer==fee_payer.key())] 18 | pub fee_payer: UncheckedAccount<'info>, 19 | 20 | /// CHECK: this is safe 21 | #[account(mut, constraint=offer.payer==payer.key())] 22 | pub payer: UncheckedAccount<'info>, 23 | 24 | /// PDA which stores token approvals for a Bakery, and executes the transfer during claims. 25 | pub config: Box>, 26 | 27 | /// PDA which stores data about the state of a Sprinkle. 28 | pub tag: Box>, 29 | 30 | /// PDA which stores data about the state of a listing. 31 | /// CHECK: this is safe 32 | #[account(seeds = [ 33 | PDA_PREFIX, 34 | config.authority.key().as_ref(), 35 | &tag.uid.to_le_bytes(), 36 | LISTING 37 | ], 38 | bump)] 39 | pub listing: UncheckedAccount<'info>, 40 | 41 | 42 | #[account(mut, 43 | seeds=[ 44 | PDA_PREFIX, 45 | config.authority.key().as_ref(), 46 | &tag.uid.to_le_bytes(), 47 | LISTING, 48 | OFFER, 49 | buyer.as_ref() 50 | ], 51 | bump=offer.bump, 52 | )] 53 | pub offer: Box>, 54 | 55 | /// SPL System Program, required for account allocation. 56 | pub system_program: Program<'info, System>, 57 | 58 | /// SPL Token Program, required for transferring tokens. 59 | pub token_program: Program<'info, Token>, 60 | 61 | /// CHECK: this is safe 62 | #[account(mut, 63 | seeds=[ 64 | PDA_PREFIX, 65 | config.authority.key().as_ref(), 66 | &tag.uid.to_le_bytes(), 67 | LISTING, 68 | OFFER, 69 | buyer.key().as_ref(), 70 | TOKEN 71 | ], bump)] 72 | pub offer_token: UncheckedAccount<'info>, 73 | 74 | /// Buyer's token account, if they are using a token to pay for this listing 75 | #[account(mut)] 76 | pub payer_token: Option>, 77 | 78 | /// Price mint, if needed 79 | pub price_mint: Option>, 80 | } 81 | 82 | 83 | 84 | pub fn handler<'a, 'b, 'c, 'info>( 85 | ctx: Context<'a, 'b, 'c, 'info, CancelOffer<'info>>, 86 | buyer: Pubkey 87 | ) -> Result<()> { 88 | let config = &mut ctx.accounts.config; 89 | let tag = &mut ctx.accounts.tag; 90 | let listing = &mut ctx.accounts.listing; 91 | let offer_token = &ctx.accounts.offer_token; 92 | let payer_token = &ctx.accounts.payer_token; 93 | let offer = &mut ctx.accounts.offer; 94 | let payer = &mut ctx.accounts.payer; 95 | let fee_payer = &mut ctx.accounts.fee_payer; 96 | let price_mint = &ctx.accounts.price_mint; 97 | let token_program = &ctx.accounts.token_program; 98 | 99 | let authority = config.authority.key(); 100 | let offer_token_seeds = &[ 101 | PDA_PREFIX, 102 | authority.as_ref(), 103 | &tag.uid.to_le_bytes(), 104 | LISTING, 105 | OFFER, 106 | buyer.as_ref(), 107 | TOKEN, 108 | &[*ctx.bumps.get("offer_token").unwrap()] 109 | ]; 110 | let offer_seeds = &[ 111 | PDA_PREFIX, 112 | authority.as_ref(), 113 | &tag.uid.to_le_bytes(), 114 | LISTING, 115 | OFFER, 116 | buyer.as_ref(), 117 | &[offer.bump] 118 | ]; 119 | 120 | if !listing.data_is_empty() { 121 | let real_listing: Account = Account::try_from(&listing)?; 122 | if real_listing.state != ListingState::UserCanceled && 123 | real_listing.state != ListingState::CupcakeCanceled && 124 | real_listing.state != ListingState::Accepted { 125 | require!(payer.is_signer, ErrorCode::PayerMustSign); 126 | } 127 | } 128 | 129 | 130 | if let Some(mint) = offer.offer_mint { 131 | require!(payer_token.is_some(), ErrorCode::NoPayerTokenPresent); 132 | require!(price_mint.is_some(), ErrorCode::NoPriceMintPresent); 133 | 134 | let payer_token_acct = payer_token.clone().unwrap(); 135 | assert_is_ata( 136 | &payer_token_acct.to_account_info(), 137 | &payer.key(), 138 | &mint, 139 | None)?; 140 | 141 | let cpi_accounts = token::Transfer { 142 | from: offer_token.to_account_info(), 143 | to: payer_token_acct.to_account_info(), 144 | authority: offer.to_account_info(), 145 | }; 146 | let context = 147 | CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts); 148 | token::transfer(context.with_signer(&[&offer_seeds[..]]), offer.offer_amount)?; 149 | 150 | let cpi_accounts = token::CloseAccount { 151 | account: offer_token.to_account_info(), 152 | destination: fee_payer.to_account_info(), 153 | authority: offer.to_account_info(), 154 | }; 155 | let context = CpiContext::new(token_program.to_account_info(), cpi_accounts); 156 | token::close_account(context.with_signer(&[&offer_seeds[..]]))?; 157 | } else { 158 | let ix = anchor_lang::solana_program::system_instruction::transfer( 159 | &offer_token.key(), 160 | &payer.key(), 161 | offer.offer_amount, 162 | ); 163 | 164 | anchor_lang::solana_program::program::invoke_signed( 165 | &ix, 166 | &[ 167 | offer_token.to_account_info(), 168 | payer.to_account_info(), 169 | ], 170 | &[&offer_token_seeds[..]] 171 | )?; 172 | } 173 | 174 | offer.close(ctx.accounts.fee_payer.to_account_info())?; 175 | Ok(()) 176 | } 177 | 178 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/instructions/claim_bought_nft/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::ErrorCode; 2 | use crate::state::PDA_PREFIX; 3 | use crate::state::{bakery::*, sprinkle::*}; 4 | use crate::utils::{move_hot_potato, MoveHotPotatoArgs}; 5 | use anchor_lang::prelude::*; 6 | use anchor_spl::token::Token; 7 | 8 | #[derive(Accounts)] 9 | pub struct ClaimBoughtNFT<'info> { 10 | /// PDA which stores token approvals for a Bakery, and executes the transfer during claims. 11 | #[account(mut)] 12 | pub config: Box>, 13 | 14 | /// PDA which stores data about the state of a Sprinkle. 15 | #[account(mut)] 16 | pub tag: Box>, 17 | 18 | /// Buyer 19 | /// CHECK: this is safe 20 | #[account(mut)] 21 | pub buyer: Signer<'info>, 22 | 23 | /// All of these accounts get checked in util function because of older code in 24 | /// claim_sprinkle, so we avoid doing redundant checks here, making them UncheckedAccounts. 25 | /// These all correspond to identical fields from claim_sprinkle. 26 | 27 | /// CHECK: No 28 | pub token_metadata_program: UncheckedAccount<'info>, 29 | /// CHECK: No 30 | pub token_mint: UncheckedAccount<'info>, 31 | /// CHECK: No 32 | pub edition: UncheckedAccount<'info>, 33 | /// CHECK: No 34 | #[account(mut)] 35 | pub user_token_account: UncheckedAccount<'info>, 36 | /// CHECK: No 37 | #[account(mut)] 38 | pub token: UncheckedAccount<'info>, 39 | 40 | /// SPL System Program, required for account allocation. 41 | pub system_program: Program<'info, System>, 42 | 43 | /// SPL Token Program, required for transferring tokens. 44 | pub token_program: Program<'info, Token>, 45 | 46 | /// Rent 47 | pub rent: Sysvar<'info, Rent>, 48 | } 49 | 50 | /// Sort of a trimmed down version of claim-sprinkle, only for hot potato NFTs, 51 | /// in the case for whena buyer just won a bid. Doesn't require tag authority to sign off like 52 | /// claim does. That means you'd need to go through a lamdba, and we want programmatic access 53 | /// to these contracts to work for liquidity purposes. 54 | /// 55 | /// In actuality this handler could be used multiple times. If you are vault authority, 56 | /// you should be able to reclaim your NFT at any time, but that is the most salient case. 57 | 58 | pub fn handler<'a, 'b, 'c, 'info>( 59 | ctx: Context<'a, 'b, 'c, 'info, ClaimBoughtNFT<'info>>, 60 | creator_bump: u8, // Ignored except in hotpotato use. In hotpotato is used to make the token account. 61 | ) -> Result<()> { 62 | let config = &mut ctx.accounts.config; 63 | let tag = &mut ctx.accounts.tag; 64 | let token_metadata_program = &ctx.accounts.token_metadata_program; 65 | let token_mint = &ctx.accounts.token_mint; 66 | let edition = &ctx.accounts.edition; 67 | let user_token_account = &ctx.accounts.user_token_account; 68 | let token = &ctx.accounts.token; 69 | let buyer = &ctx.accounts.buyer; 70 | 71 | let config_seeds = &[ 72 | &PDA_PREFIX[..], 73 | &config.authority.as_ref()[..], 74 | &[config.bump], 75 | ]; 76 | 77 | require!( 78 | tag.vault_authority == Some(buyer.key()), 79 | ErrorCode::NotVaultAuthority 80 | ); 81 | require!( 82 | tag.vault_state == VaultState::Vaulted 83 | || tag.vault_state == VaultState::InTransit 84 | || tag.vault_state == VaultState::UnvaultingRequested, 85 | ErrorCode::NotVaulted 86 | ); 87 | move_hot_potato(MoveHotPotatoArgs { 88 | token_metadata_program: &token_metadata_program.to_account_info(), 89 | token_mint: &token_mint.to_account_info(), 90 | edition: &edition.to_account_info(), 91 | user_token_account: &user_token_account.to_account_info(), 92 | token: &token.to_account_info(), 93 | tag, 94 | config, 95 | user: &buyer.to_account_info(), 96 | rent: &ctx.accounts.rent, 97 | system_program: &ctx.accounts.system_program, 98 | token_program: &ctx.accounts.token_program, 99 | payer: buyer, 100 | creator_bump, 101 | config_seeds, 102 | })?; 103 | 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/instructions/create_bakery.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::state::{PDA_PREFIX, bakery::*}; 3 | 4 | #[derive(Accounts)] 5 | pub struct Initialize<'info> { 6 | /// Account which has the authority to create/update sprinkles for this Bakery. 7 | #[account(mut)] 8 | pub authority: Signer<'info>, 9 | 10 | /// Account which pays the network and rent fees, for this transaction only. 11 | #[account(mut)] 12 | pub payer: Signer<'info>, 13 | 14 | /// PDA which stores token approvals for this Bakery, and executes the transfer during claims. 15 | #[account(init, 16 | payer = payer, 17 | space = Config::SIZE, 18 | seeds = [ 19 | PDA_PREFIX, 20 | authority.key().as_ref() 21 | ], 22 | bump)] 23 | pub config: Account<'info, Config>, 24 | 25 | /// SPL System Program, required for account allocation. 26 | pub system_program: Program<'info, System>, 27 | 28 | /// SPL Rent Sysvar, required for account allocation. 29 | pub rent: Sysvar<'info, Rent>, 30 | } 31 | 32 | pub fn handler<'a, 'b, 'c, 'info>(ctx: Context>) -> Result<()> { 33 | // Now that a fresh Bakery PDA has been created by the Anchor constraints, 34 | // we can store the authority account's address and the PDA bump inside. 35 | ctx.accounts.config.authority = *ctx.accounts.authority.to_account_info().key; 36 | ctx.accounts.config.bump = *ctx.bumps.get("config").unwrap(); 37 | 38 | Ok(()) 39 | } -------------------------------------------------------------------------------- /solana/programs/cupcake/src/instructions/delete_listing/mod.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use crate::errors::ErrorCode; 3 | use crate::state::{PDA_PREFIX, LISTING, Listing, ListingState}; 4 | use crate::state::{bakery::*, sprinkle::*}; 5 | 6 | 7 | 8 | #[derive(Accounts)] 9 | pub struct DeleteListing<'info> { 10 | /// Account which pays the network and rent fees, for this transaction only. 11 | /// CHECK: this is safe 12 | #[account(mut, constraint=payer.key() == listing.fee_payer)] 13 | pub payer: UncheckedAccount<'info>, 14 | 15 | #[account(constraint=authority.key() == config.authority)] 16 | pub authority: Signer<'info>, 17 | 18 | /// PDA which stores token approvals for a Bakery, and executes the transfer during claims. 19 | pub config: Box>, 20 | 21 | /// PDA which stores data about the state of a Sprinkle. 22 | #[account( 23 | seeds = [ 24 | PDA_PREFIX, 25 | config.authority.key().as_ref(), 26 | &tag.uid.to_le_bytes() 27 | ], 28 | bump = tag.bump)] 29 | pub tag: Box>, 30 | 31 | /// PDA which stores data about the state of a Sprinkle. 32 | #[account(mut, 33 | seeds = [ 34 | PDA_PREFIX, 35 | config.authority.key().as_ref(), 36 | &tag.uid.to_le_bytes(), 37 | LISTING 38 | ], 39 | bump = listing.bump)] 40 | pub listing: Box>, 41 | 42 | /// SPL System Program, required for account allocation. 43 | pub system_program: Program<'info, System>, 44 | } 45 | 46 | pub fn handler<'a, 'b, 'c, 'info>( 47 | ctx: Context<'a, 'b, 'c, 'info, DeleteListing<'info>> 48 | ) -> Result<()> { 49 | let payer = &ctx.accounts.payer; 50 | let listing = &mut ctx.accounts.listing; 51 | require!(listing.state == ListingState::UserCanceled || 52 | listing.state == ListingState::CupcakeCanceled || 53 | listing.state == ListingState::Accepted, ErrorCode::CannotDeleteListingInThisState); 54 | 55 | 56 | listing.close(payer.to_account_info())?; 57 | 58 | Ok(()) 59 | } 60 | 61 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/instructions/make_offer/mod.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_spl::token::{self, Token, TokenAccount, Mint}; 3 | use crate::errors::ErrorCode; 4 | use crate::state::{PDA_PREFIX, LISTING, Listing, Offer, TOKEN, OFFER, ListingState}; 5 | use crate::state::{bakery::*, sprinkle::*}; 6 | use crate::utils::{ 7 | assert_is_ata, 8 | create_program_token_account_if_not_present 9 | }; 10 | 11 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq)] 12 | pub struct MakeOfferArgs { 13 | offer_amount: u64, 14 | buyer: Pubkey 15 | } 16 | 17 | #[derive(Accounts)] 18 | #[instruction(args: MakeOfferArgs)] 19 | pub struct MakeOffer<'info> { 20 | /// Account which will receive lamports back from the offer. 21 | #[account(mut)] 22 | pub fee_payer: Signer<'info>, 23 | 24 | /// Account which will actually pay for the offer. 25 | #[account(mut)] 26 | pub payer: Signer<'info>, 27 | 28 | /// PDA which stores token approvals for a Bakery, and executes the transfer during claims. 29 | pub config: Box>, 30 | 31 | /// PDA which stores data about the state of a Sprinkle. 32 | pub tag: Box>, 33 | 34 | /// PDA which stores data about the state of a listing. 35 | #[account(seeds = [ 36 | PDA_PREFIX, 37 | config.authority.key().as_ref(), 38 | &tag.uid.to_le_bytes(), 39 | LISTING 40 | ], 41 | bump=listing.bump)] 42 | pub listing: Box>, 43 | 44 | #[account(init, 45 | seeds=[ 46 | PDA_PREFIX, 47 | config.authority.key().as_ref(), 48 | &tag.uid.to_le_bytes(), 49 | LISTING, 50 | OFFER, 51 | args.buyer.key().as_ref() 52 | ], 53 | bump, 54 | payer=fee_payer, 55 | space=Offer::SIZE 56 | )] 57 | pub offer: Box>, 58 | 59 | /// SPL System Program, required for account allocation. 60 | pub system_program: Program<'info, System>, 61 | 62 | /// SPL Token Program, required for transferring tokens. 63 | pub token_program: Program<'info, Token>, 64 | 65 | /// SPL Rent Sysvar, required for account allocation. 66 | pub rent: Sysvar<'info, Rent>, 67 | 68 | /// Either the token account or the SOL account 69 | /// CHECK: this is safe 70 | #[account(mut, 71 | seeds=[ 72 | PDA_PREFIX, 73 | config.authority.key().as_ref(), 74 | &tag.uid.to_le_bytes(), 75 | LISTING, 76 | OFFER, 77 | args.buyer.key().as_ref(), 78 | TOKEN 79 | ], bump)] 80 | pub offer_token: UncheckedAccount<'info>, 81 | 82 | /// Buyer's token account, if they are using a token to pay for this listing 83 | #[account(mut)] 84 | pub payer_token: Option>, 85 | 86 | /// Transfer authority to move out of buyer token account 87 | pub transfer_authority: Option>, 88 | 89 | /// Price mint 90 | pub price_mint: Option>, 91 | } 92 | 93 | 94 | 95 | pub fn handler<'a, 'b, 'c, 'info>( 96 | ctx: Context<'a, 'b, 'c, 'info, MakeOffer<'info>>, 97 | args: MakeOfferArgs 98 | ) -> Result<()> { 99 | let buyer = args.buyer; 100 | let config = &ctx.accounts.config; 101 | let tag = &ctx.accounts.tag; 102 | let listing = &mut ctx.accounts.listing; 103 | let offer_token = &ctx.accounts.offer_token; 104 | let payer_token = &ctx.accounts.payer_token; 105 | let offer = &mut ctx.accounts.offer; 106 | let transfer_authority = &ctx.accounts.transfer_authority; 107 | let system_program = &ctx.accounts.system_program; 108 | let rent = &ctx.accounts.rent; 109 | let token_program = &ctx.accounts.token_program; 110 | let payer = &mut ctx.accounts.payer; 111 | let price_mint = &ctx.accounts.price_mint; 112 | let authority = config.authority.key(); 113 | let offer_token_seeds = &[ 114 | PDA_PREFIX, 115 | authority.as_ref(), 116 | &tag.uid.to_le_bytes(), 117 | LISTING, 118 | OFFER, 119 | buyer.as_ref(), 120 | TOKEN, 121 | &[*ctx.bumps.get("offer_token").unwrap()] 122 | ]; 123 | 124 | require!(listing.price_mint.is_some() || args.offer_amount >= 1000000, ErrorCode::MinimumOffer); 125 | require!(listing.state == ListingState::ForSale, ErrorCode::ListingNotForSale); 126 | 127 | offer.bump = *ctx.bumps.get("offer").unwrap(); 128 | 129 | offer.buyer = buyer; 130 | 131 | offer.payer = payer.key(); 132 | 133 | offer.fee_payer = ctx.accounts.fee_payer.key(); 134 | 135 | offer.offer_amount = args.offer_amount; 136 | 137 | offer.offer_mint = listing.price_mint; 138 | 139 | offer.tag = tag.key(); 140 | 141 | if let Some(mint) = listing.price_mint { 142 | require!(payer_token.is_some(), ErrorCode::NoPayerTokenPresent); 143 | require!(price_mint.is_some(), ErrorCode::NoPriceMintPresent); 144 | 145 | let payer_token_acct = payer_token.clone().unwrap(); 146 | if transfer_authority.is_some() { 147 | let tfer_auth = transfer_authority.clone().unwrap(); 148 | assert_is_ata( 149 | &payer_token_acct.to_account_info(), 150 | &payer.key(), 151 | &mint, 152 | Some(&tfer_auth.key()))?; 153 | } else { 154 | assert_is_ata( 155 | &payer_token_acct.to_account_info(), 156 | &payer.key(), 157 | &mint, 158 | None)?; 159 | } 160 | 161 | create_program_token_account_if_not_present( 162 | &offer_token, 163 | system_program, 164 | &ctx.accounts.fee_payer, 165 | token_program, 166 | &price_mint.clone().unwrap(), 167 | &offer.to_account_info(), 168 | rent, 169 | offer_token_seeds 170 | )?; 171 | 172 | let cpi_accounts = token::Transfer { 173 | from: payer_token_acct.to_account_info(), 174 | to: offer_token.to_account_info(), 175 | authority: transfer_authority.clone().unwrap().to_account_info(), 176 | }; 177 | let context = 178 | CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts); 179 | token::transfer(context, args.offer_amount)?; 180 | } else { 181 | let ix = anchor_lang::solana_program::system_instruction::transfer( 182 | &payer.key(), 183 | &offer_token.key(), 184 | args.offer_amount, 185 | ); 186 | 187 | anchor_lang::solana_program::program::invoke( 188 | &ix, 189 | &[ 190 | payer.to_account_info(), 191 | offer_token.to_account_info(), 192 | ], 193 | 194 | )?; 195 | } 196 | Ok(()) 197 | } 198 | 199 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod accept_offer; 2 | pub mod bake_sprinkle; 3 | pub mod cancel_offer; 4 | pub mod claim_bought_nft; 5 | pub mod claim_sprinkle; 6 | pub mod create_bakery; 7 | pub mod delete_listing; 8 | pub mod make_offer; 9 | pub mod modify_listing; 10 | pub mod toggle_vault_nft; 11 | 12 | pub use accept_offer::*; 13 | pub use bake_sprinkle::*; 14 | pub use cancel_offer::CancelOffer; 15 | pub use cancel_offer::*; 16 | pub use claim_bought_nft::*; 17 | pub use claim_sprinkle::*; 18 | pub use create_bakery::*; 19 | pub use delete_listing::*; 20 | pub use make_offer::*; 21 | pub use modify_listing::*; 22 | pub use toggle_vault_nft::ToggleVaultNFT; 23 | pub use toggle_vault_nft::*; 24 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/instructions/modify_listing/mod.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_spl::token::{TokenAccount}; 3 | use crate::errors::ErrorCode; 4 | use crate::state::{PDA_PREFIX, LISTING, Listing, ListingState, ListingVersion}; 5 | use crate::state::{bakery::*, sprinkle::*}; 6 | 7 | 8 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq)] 9 | pub struct PriceSettings { 10 | /// Set to None to use SOL 11 | price_mint: Option, 12 | /// Set to None if you only want to use Offers, if set to something, any offer at or above price 13 | /// will auto accept. Like buy it now 14 | set_price: Option, 15 | } 16 | 17 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq)] 18 | pub struct ModifyListingArgs { 19 | /// Price Settings 20 | pub price_settings: Option, 21 | /// Unchecked collection of NFT. Used to rpc filter on listings. 22 | pub collection: Option, 23 | 24 | pub vaulted_preferred: Option, 25 | 26 | /// New state to go to 27 | pub next_state: Option, 28 | 29 | } 30 | 31 | #[derive(Accounts)] 32 | pub struct ModifyListing<'info> { 33 | /// Account which pays the network and rent fees, for this transaction only. 34 | #[account(mut, constraint=payer.key() == config.authority || payer.key() == seller.key())] 35 | pub payer: Signer<'info>, 36 | 37 | /// The seller. 38 | /// CHECK: No check. 39 | pub seller: UncheckedAccount<'info>, 40 | 41 | // The hot potato token account of the seller. 42 | #[account( 43 | seeds=[ 44 | PDA_PREFIX, 45 | config.authority.as_ref(), 46 | &tag.uid.to_le_bytes(), 47 | seller.key().as_ref(), 48 | tag.token_mint.as_ref()], bump)] 49 | pub seller_token: Account<'info, TokenAccount>, 50 | 51 | /// PDA which stores token approvals for a Bakery, and executes the transfer during claims. 52 | pub config: Box>, 53 | 54 | /// PDA which stores data about the state of a Sprinkle. 55 | #[account( 56 | seeds = [ 57 | PDA_PREFIX, 58 | config.authority.key().as_ref(), 59 | &tag.uid.to_le_bytes() 60 | ], 61 | bump = tag.bump)] 62 | pub tag: Box>, 63 | 64 | /// PDA which stores data about the state of a Sprinkle. 65 | #[account(init_if_needed, 66 | seeds = [ 67 | PDA_PREFIX, 68 | config.authority.key().as_ref(), 69 | &tag.uid.to_le_bytes(), 70 | LISTING 71 | ], 72 | space = Listing::SIZE, 73 | constraint=listing.version == ListingVersion::Unset || listing.seller == seller.key(), 74 | payer=payer, 75 | bump)] 76 | pub listing: Box>, 77 | 78 | /// SPL System Program, required for account allocation. 79 | pub system_program: Program<'info, System>, 80 | } 81 | 82 | 83 | 84 | pub fn handler<'a, 'b, 'c, 'info>( 85 | ctx: Context<'a, 'b, 'c, 'info, ModifyListing<'info>>, 86 | args: ModifyListingArgs 87 | ) -> Result<()> { 88 | let config = &ctx.accounts.config; 89 | let payer = &ctx.accounts.payer; 90 | let seller = &ctx.accounts.seller; 91 | let sprinkle = &ctx.accounts.tag; 92 | let listing = &mut ctx.accounts.listing; 93 | let seller_token = &ctx.accounts.seller_token; 94 | 95 | // tested 96 | if listing.version == ListingVersion::Unset { 97 | listing.bump = *ctx.bumps.get("listing").unwrap(); 98 | listing.fee_payer = payer.key(); 99 | listing.seller = seller.key(); 100 | // Redundant check but just in case. 101 | require!(seller_token.owner == seller.key(), ErrorCode::SellerMustBeLister); 102 | require!(seller_token.amount > 0, ErrorCode::MustHoldTokenToSell); 103 | require!(payer.key() == seller.key(), ErrorCode::SellerMustInitiateSale); 104 | } 105 | 106 | if payer.key() != config.authority && 107 | listing.state != ListingState::ForSale { 108 | // Don't let a user do anything to an order that isnt for salke 109 | return Err(ErrorCode::MustUseConfigAsPayer.into()); 110 | } 111 | 112 | // User can only create the listing or cancel it, after that, cupcake must do the rest. 113 | if payer.key() != config.authority && 114 | args.next_state != Some(ListingState::ForSale) && 115 | args.next_state != Some(ListingState::UserCanceled) && 116 | !args.next_state.is_none() { 117 | return Err(ErrorCode::MustUseConfigAsPayer.into()); 118 | } 119 | 120 | if let Some(settings) = args.price_settings { 121 | // Can only change the price mint or price during initialization. 122 | // Once it is for sale, there will be bids with escrowed coins potentially of wrong mints or amounts that could be accepted. 123 | // After, to make things easier, we allow price changes, but not mint changes. However, bids that do become eligible for auto-acceptance 124 | // won't be - they will need to be closed and remade, or accepted manually by the seller. 125 | if listing.version == ListingVersion::Unset { 126 | listing.price_mint = settings.price_mint; 127 | listing.set_price = settings.set_price; 128 | } else if listing.state == ListingState::ForSale { 129 | // Regardless of what state you are transitioning to, if you are in ForSale, you can only change the price, for simplicity. 130 | //tested 131 | listing.set_price = settings.set_price; 132 | 133 | require!(listing.price_mint == settings.price_mint, ErrorCode::CannotChangePriceMintInThisState); 134 | } else { 135 | return Err(ErrorCode::CannotChangePriceSettingsInThisState.into()); 136 | } 137 | } 138 | 139 | // Change filtering collection whenever you want. 140 | if let Some(collection) = args.collection { 141 | listing.collection = collection; 142 | } 143 | 144 | if let Some(vaulted_preferred) = args.vaulted_preferred { 145 | listing.vaulted_preferred = vaulted_preferred; 146 | } 147 | 148 | // Accepted is a frozen endpoint, cannot move from here. 149 | require!(listing.state != ListingState::Accepted, ErrorCode::ListingFrozen); 150 | 151 | 152 | if let Some(next_state) = args.next_state { 153 | // Basically you can only go from ForSale to a form of cancelled, 154 | // and from cancelled to returned (or for sale to returned). 155 | // Those are the transitions. 156 | 157 | if next_state == ListingState::ForSale || next_state == ListingState::UserCanceled || 158 | next_state == ListingState::CupcakeCanceled{ 159 | require!(listing.chosen_buyer.is_none(), ErrorCode::ChosenBuyerSet); 160 | } 161 | 162 | 163 | // tested 164 | // To move into the accepted state, please use the accept offer instruction as seller, 165 | // or as buyer, make bid that is above or at asking price. 166 | require!(next_state != ListingState::Accepted, ErrorCode::CannotAcceptFromModify); 167 | 168 | if next_state != ListingState::CupcakeCanceled && next_state != ListingState::UserCanceled && 169 | next_state != ListingState::ForSale && listing.chosen_buyer.is_none() { 170 | return Err(ErrorCode::MustChooseBuyer.into()); 171 | } 172 | 173 | // Cannot claim to have user cancel if you are not user 174 | // Conversely, cannot cancel as cupcake if you are not cupcake. 175 | if next_state == ListingState::CupcakeCanceled { 176 | require!(payer.key() == config.authority, ErrorCode::MustUseConfigAsPayer); 177 | } else if next_state == ListingState::UserCanceled { 178 | require!(payer.key() == listing.seller, ErrorCode::MustUseSellerAsPayer); 179 | } 180 | } 181 | 182 | listing.state = args.next_state.unwrap_or(listing.state); 183 | listing.sprinkle = sprinkle.key(); 184 | // Set at the bottom so we can have one run through where we can check 185 | // if this is the first time through. 186 | listing.version = ListingVersion::V1; 187 | 188 | Ok(()) 189 | } 190 | 191 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/instructions/toggle_vault_nft/mod.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_spl::token::{ TokenAccount}; 3 | use crate::errors::ErrorCode; 4 | use crate::state::{PDA_PREFIX}; 5 | use crate::state::{bakery::*, sprinkle::*}; 6 | 7 | 8 | 9 | #[derive(Accounts)] 10 | #[instruction(user: Pubkey)] 11 | pub struct ToggleVaultNFT<'info> { 12 | /// Either the bakery owner or the user holding the token (to change from InTransit to Unvaulted) 13 | #[account(mut, constraint=payer.key() == config.authority || payer.key() == user)] 14 | pub payer: Signer<'info>, 15 | 16 | /// CHECK: this is safe 17 | #[account(constraint=authority.key() == config.authority)] 18 | pub authority: UncheckedAccount<'info>, 19 | 20 | /// PDA which stores token approvals for a Bakery, and executes the transfer during claims. 21 | pub config: Box>, 22 | 23 | /// PDA which stores data about the state of a Sprinkle. 24 | #[account( 25 | mut, 26 | seeds = [ 27 | PDA_PREFIX, 28 | config.authority.key().as_ref(), 29 | &tag.uid.to_le_bytes() 30 | ], 31 | bump = tag.bump)] 32 | pub tag: Box>, 33 | 34 | #[account(mut, seeds=[ 35 | PDA_PREFIX, 36 | config.authority.key().as_ref(), 37 | &tag.uid.to_le_bytes(), 38 | user.as_ref(), 39 | tag.token_mint.as_ref() 40 | ], 41 | constraint=hot_potato_token.amount == 1, 42 | token::mint = tag.token_mint, 43 | bump)] 44 | pub hot_potato_token: Account<'info, TokenAccount>, 45 | } 46 | 47 | pub fn handler<'a, 'b, 'c, 'info>( 48 | ctx: Context<'a, 'b, 'c, 'info, ToggleVaultNFT<'info>>, 49 | user: Pubkey, 50 | desired_state: VaultState 51 | ) -> Result<()> { 52 | let tag = &mut ctx.accounts.tag; 53 | let payer = &ctx.accounts.payer; 54 | let authority = &ctx.accounts.authority; 55 | 56 | if payer.key() != authority.key() { 57 | require!(tag.vault_state == VaultState::Vaulted || tag.vault_state == VaultState::InTransit, ErrorCode::InvalidVaultTransition); 58 | require!((tag.vault_state == VaultState::Vaulted && 59 | desired_state == VaultState::UnvaultingRequested) || 60 | (tag.vault_state == VaultState::InTransit && 61 | desired_state == VaultState::Unvaulted), ErrorCode::InvalidVaultTransition); 62 | 63 | }; 64 | 65 | tag.vault_state = desired_state; 66 | tag.vault_authority = Some(user); 67 | 68 | Ok(()) 69 | } 70 | 71 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod errors; 4 | pub mod instructions; 5 | pub mod state; 6 | pub mod utils; 7 | 8 | use instructions::*; 9 | use state::VaultState; 10 | 11 | declare_id!("cakeGJxEdGpZ3MJP8sM3QypwzuzZpko1ueonUQgKLPE"); 12 | 13 | #[program] 14 | pub mod cupcake { 15 | 16 | use super::*; 17 | 18 | /// Create a new Bakery, managed by a provided account. 19 | pub fn initialize(ctx: Context) -> Result<()> { 20 | instructions::create_bakery::handler(ctx) 21 | } 22 | 23 | /// Create a new Sprinkle for a Bakery, or update an existing one. 24 | /// BakeryAuthority must be a signer. 25 | pub fn add_or_refill_tag<'a, 'b, 'c, 'info>( 26 | ctx: Context<'a, 'b, 'c, 'info, AddOrRefillTag<'info>>, 27 | tag_params: AddOrRefillTagParams, 28 | ) -> Result<()> { 29 | instructions::bake_sprinkle::handler(ctx, tag_params) 30 | } 31 | 32 | /// Execute the claim method of a Sprinkle for a provided account. 33 | /// SprinkleAuthority must be a signer. 34 | pub fn claim_tag<'a, 'b, 'c, 'info>( 35 | ctx: Context<'a, 'b, 'c, 'info, ClaimTag<'info>>, 36 | creator_bump: u8, 37 | ) -> Result<()> { 38 | instructions::claim_sprinkle::handler(ctx, creator_bump) 39 | } 40 | 41 | /// Modify or create a new listing 42 | pub fn modify_listing<'a, 'b, 'c, 'info>( 43 | ctx: Context<'a, 'b, 'c, 'info, ModifyListing<'info>>, 44 | args: ModifyListingArgs, 45 | ) -> Result<()> { 46 | instructions::modify_listing::handler(ctx, args) 47 | } 48 | 49 | /// Create a new offer 50 | pub fn make_offer<'a, 'b, 'c, 'info>( 51 | ctx: Context<'a, 'b, 'c, 'info, MakeOffer<'info>>, 52 | args: MakeOfferArgs, 53 | ) -> Result<()> { 54 | instructions::make_offer::handler(ctx, args) 55 | } 56 | 57 | /// Cancel an offer 58 | pub fn cancel_offer<'a, 'b, 'c, 'info>( 59 | ctx: Context<'a, 'b, 'c, 'info, CancelOffer<'info>>, 60 | buyer: Pubkey, 61 | ) -> Result<()> { 62 | instructions::cancel_offer::handler(ctx, buyer) 63 | } 64 | 65 | /// Delete listing 66 | pub fn delete_listing<'a, 'b, 'c, 'info>( 67 | ctx: Context<'a, 'b, 'c, 'info, DeleteListing<'info>>, 68 | ) -> Result<()> { 69 | instructions::delete_listing::handler(ctx) 70 | } 71 | 72 | /// Accept an new offer 73 | pub fn accept_offer<'a, 'b, 'c, 'info>( 74 | ctx: Context<'a, 'b, 'c, 'info, AcceptOffer<'info>>, 75 | ) -> Result<()> { 76 | instructions::accept_offer::handler(ctx) 77 | } 78 | 79 | /// Buyer can claim the NFT they just bought and it will be moved to their wallet 80 | pub fn claim_bought_nft<'a, 'b, 'c, 'info>( 81 | ctx: Context<'a, 'b, 'c, 'info, ClaimBoughtNFT<'info>>, 82 | bump: u8, 83 | ) -> Result<()> { 84 | instructions::claim_bought_nft::handler(ctx, bump) 85 | } 86 | 87 | /// Toggle Vault States 88 | pub fn toggle_vault_nft<'a, 'b, 'c, 'info>( 89 | ctx: Context<'a, 'b, 'c, 'info, ToggleVaultNFT<'info>>, 90 | user: Pubkey, 91 | desired_state: VaultState, 92 | ) -> Result<()> { 93 | instructions::toggle_vault_nft::handler(ctx, user, desired_state) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/state/bakery.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | /// PDA created for each Bakery. 4 | /// Stores information about the authorizing account within its' state. 5 | /// Collects and executes token approvals for Sprinkle claims. 6 | #[account] 7 | pub struct Config { 8 | /// Account which has the authority to create/update sprinkles for this Bakery. 9 | pub authority: Pubkey, 10 | 11 | /// Bump value used in the PDA generation for this Bakery. 12 | pub bump: u8, 13 | } 14 | 15 | impl Config { 16 | /// The minimum required account size for a Bakery PDA. 17 | pub const SIZE: usize = 18 | 8 + // Anchor discriminator 19 | 32 + // BakeryAuthority pubkey 20 | 1; // PDA bump 21 | } -------------------------------------------------------------------------------- /solana/programs/cupcake/src/state/marketplace.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | /// Different types of claim methods that can be assigned to a Sprinkle. 4 | /// Note: Accepted state can be permissionlessly cancelled after a time period 5 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq, Debug)] 6 | pub enum ListingState { 7 | /// No offer made yet 8 | ForSale, 9 | 10 | /// If this is true for two weeks, we cancel and return item. 11 | /// At two weeks out, this PDA can be permissionlessly destroyed and cleaned up for lamports 12 | CupcakeCanceled, 13 | 14 | /// User cancels, we need to be notified by backend app to return item 15 | /// At two weeks out, this PDA can be permissionlessly destroyed and cleaned up for lamports 16 | UserCanceled, 17 | 18 | /// Listing has been accepted, the seller has been paid, and the buyer receives 19 | /// the NFT. Immediately moves to Vaulted if a vaulted sale, otherwise later 20 | /// moves to Shipped. 21 | Accepted, 22 | } 23 | 24 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq, Debug)] 25 | pub enum ListingVersion { 26 | Unset, 27 | V1, 28 | } 29 | 30 | /// PDA created for each sale on the Hot potato market place 31 | /// Seed of [prefix, bakery, tag, "listing"] 32 | /// On creation, this Listing will make make further claims of the hot potato by others impossible. It 33 | /// will truly be frozen in the user's wallet, even from others scanning the sticker. 34 | /// 35 | /// This system could be gamed to prevent someone from ever scanning a sticker - not sure how to get around 36 | /// this except with listing limits. Perhaps you can only have a listing up for 2 weeks at a time with a 1 week 37 | /// cool down. We will add this if it becomes an issue. 38 | /// 39 | /// Further scans on the hot potato sticker will fail 40 | /// until the Listing is in the Shipped state, when a scan by the buyer (and only them) will trigger 41 | /// the release of the NFT, and the transfer of the tokens to the seller in a secondary async txn. 42 | /// 43 | /// A listing can either have an offer made and accepted in one go (no offer PDA created) 44 | /// or it is made and then accepted (offer is collapsed), either way, listing will end up 45 | /// with the tokens escrowed on it until its time to give them to the seller when the buyer scans. 46 | /// 47 | /// The listing will be able to give them to the seller permissionlessly on a successful buyer scan, 48 | /// and we can do this with a clockwork lever of some kind, a worker, or they can push a website claim button. 49 | #[account] 50 | pub struct Listing { 51 | /// A version identifier for this model, which we can use to cycle out the model if we need. 52 | pub version: ListingVersion, 53 | 54 | /// Account which created and can destroy this listing. 55 | /// also used for give me all listings for this seller memcmp call 56 | pub seller: Pubkey, 57 | 58 | /// Collection of the tag. Not enforced. Used for quick rpc lookups. You can stick anything here to index on. 59 | pub collection: Pubkey, 60 | 61 | pub state: ListingState, 62 | 63 | /// Original payer of the token account and this account 64 | pub fee_payer: Pubkey, 65 | 66 | pub chosen_buyer: Option, 67 | 68 | /// Default false, set to true if current buyer wants to vault NFT. 69 | pub vaulted_preferred: bool, 70 | 71 | /// If unset, assumed to be SOL 72 | pub price_mint: Option, 73 | 74 | /// If unset, only taking offers, if set, and an offer is made at or above this price, it is auto-accepted 75 | pub set_price: Option, 76 | 77 | /// Agreed upon price 78 | pub agreed_price: Option, 79 | 80 | /// Bump value used in the PDA generation for this Listing. 81 | pub bump: u8, 82 | 83 | // PDA address of the hot-potato sprinkle being listed 84 | pub sprinkle: Pubkey, 85 | } 86 | 87 | /// Offer with seed [cupcake, bakery, tag, buyer] 88 | /// Can only make one offer at a time, offer can be cancelled by buyer or seller. 89 | /// Offer escrows payment from buyer as an ATA owned by the Offer OR as SOL in the offer PDA. 90 | #[account] 91 | pub struct Offer { 92 | /// A version identifier for this model, which we can use to cycle out the model if we need. 93 | pub version: u8, 94 | 95 | /// Account which receives the NFT if the offer is accepted. 96 | /// Can cancel the offer, but only receives funds if is also the payer. 97 | /// also used for give me all offers for this buyer memcmp call 98 | pub buyer: Pubkey, 99 | 100 | /// Tag lookup for front end 101 | pub tag: Pubkey, 102 | 103 | /// Original fee payer of the token account and this account 104 | pub fee_payer: Pubkey, 105 | 106 | /// You can make an offer on behalf of someone else. 107 | /// If the offer gets cancelled, you get the money back, 108 | /// not them! 109 | pub payer: Pubkey, 110 | 111 | /// If you prefer to vault the NFT vs have it shipped 112 | pub vaulted_preferred: bool, 113 | 114 | /// If unset, assumed to be SOL, duplicative with the Listing but makes for easier data lookup by front end 115 | /// Also its possible for user or cupcake to have changed mint type since listing was made 116 | pub offer_mint: Option, 117 | 118 | /// Offer amount 119 | pub offer_amount: u64, 120 | 121 | /// If true, offer has been accepted and NFT can be claimed. 122 | pub offer_accepted: bool, 123 | 124 | /// Bump value used in the PDA generation for this Offer. 125 | pub bump: u8, 126 | 127 | /// Bump value used in the PDA generation for this Listing. 128 | pub token_bump: u8, 129 | } 130 | 131 | impl Listing { 132 | /// The minimum required account size for a Bakery PDA. 133 | pub const SIZE: usize = 8 + // Anchor discriminator 134 | 1 + // version 135 | 32 + // collection pubkey 136 | 1 + // listing state 137 | 32 + // original fee payer pubkey 138 | 33 + // chosen buyer 139 | 1 + // vaulted preferred 140 | 33 + // price mint 141 | 9 + // price 142 | 9 + // agreed price 143 | 1 + // PDA bump 144 | 32 + // sprinkle PDA 145 | 18; // buffer 146 | } 147 | 148 | impl Offer { 149 | /// The minimum required account size for a Bakery PDA. 150 | pub const SIZE: usize = 8 + // Anchor discriminator 151 | 1 + // version 152 | 32 + // Tag pubkey 153 | 32 + // fee payer pubkey 154 | 32 + // payer pubkey 155 | 1 + // vaulted preferred 156 | 33 + // offer mint 157 | 8 + // offer amount 158 | 1 + // offer accepted 159 | 2 + // PDA bump 160 | 50; // buffer 161 | } 162 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bakery; 2 | pub mod marketplace; 3 | pub mod sprinkle; 4 | pub mod user_info; 5 | 6 | pub use bakery::*; 7 | pub use marketplace::*; 8 | pub use sprinkle::*; 9 | pub use user_info::*; 10 | 11 | /// String used as the first seed for all Cupcake Protocol PDAs. 12 | pub const PDA_PREFIX: &[u8] = b"cupcake"; 13 | pub const LISTING: &[u8] = b"listing"; 14 | pub const TOKEN: &[u8] = b"token"; 15 | pub const OFFER: &[u8] = b"offer"; 16 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/state/sprinkle.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | /// Different types of claim methods that can be assigned to a Sprinkle. 4 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq, Debug)] 5 | pub enum TagType { 6 | /// Prints identical copies of a Master Edition NFT to each claimer. 7 | LimitedOrOpenEdition, 8 | 9 | /// Transfers a single NFT one time, then exists immutably forever. 10 | SingleUse1Of1, 11 | 12 | /// Mints one NFT from a Candy Machine to each claimer. 13 | /// Can optionally accept and use a whitelist token. 14 | CandyMachineDrop, 15 | 16 | /// Transfers a single NFT one time, then can be refilled by the Bakery. 17 | Refillable1Of1, 18 | 19 | /// Transfers a set amount of fungible tokens to each claimer. 20 | WalletRestrictedFungible, 21 | 22 | /// Passes a single frozen NFT between claimers. 23 | HotPotato, 24 | 25 | /// Acts as a Refillable1Of1 for ProgrammableNonFungible tokens (pNFTs) 26 | ProgrammableUnique, 27 | } 28 | 29 | // Type of vault state 30 | #[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, Eq, Debug)] 31 | pub enum VaultState { 32 | /// Sitting in someone's house or on someone's person, anybody can take it with a scan 33 | Unvaulted, 34 | /// Sitting in our warehouse, it is unscannable 35 | Vaulted, 36 | /// Unvaulting requested and address encrypted in a memo txn. 37 | UnvaultingRequested, 38 | /// In transit to the current vault authority, it is unscannable except by the vault authority 39 | InTransit, 40 | } 41 | 42 | /// PDA created for each Sprinkle. 43 | /// Stores information about the assigned NFT/Candy Machine/etc and claim method. 44 | /// Maintains a counter of the total number of claims executed. 45 | #[account] 46 | pub struct Tag { 47 | /// The unique identifier for this Sprinkle, used in PDA generation. 48 | pub uid: u64, 49 | 50 | /// The claim method this Sprinkle will use. 51 | pub tag_type: TagType, 52 | 53 | /// The address of the account which must sign to approve claims on this Sprinkle. 54 | pub tag_authority: Pubkey, 55 | 56 | /// The address of the Bakery PDA which owns this Sprinkle. 57 | pub config: Pubkey, 58 | 59 | /// The total amount of claims that can be executed from this Sprinkle. 60 | pub total_supply: u64, 61 | 62 | /// A counter tracking the current number of claims executed from this Sprinkle. 63 | pub num_claimed: u64, 64 | 65 | /// If this is true, claimers must pay the Candy Machine mint fees. 66 | pub minter_pays: bool, 67 | 68 | /// The total number of claims an individual user can execute from this Sprinkle. 69 | pub per_user: u64, 70 | 71 | /// The mint address of the SPL token custodied by this Sprinkle. 72 | pub token_mint: Pubkey, 73 | 74 | // I dont trust candy machine structure not to change so we pre-cache settings here 75 | // to avoid attempting to deserialize structure that might shift 76 | // I do expect them to stick to their interfaces though 77 | /// The address of the Candy Machine assigned to this sprinkle, if any. 78 | pub candy_machine: Pubkey, 79 | 80 | /// The mint address of the whitelist token for the Candy Machine assigned to this Sprinkle, if any. 81 | pub whitelist_mint: Pubkey, 82 | 83 | /// If this is true, whitelist tokens will be burnt after being used to mint from the Candy Machine. 84 | pub whitelist_burn: bool, 85 | 86 | /// Bump value used in the PDA generation for this Sprinkle. 87 | pub bump: u8, 88 | 89 | /// Address of the account currently holding the Hot-Potato'd token in this Sprinkle, if any. 90 | pub current_token_location: Pubkey, 91 | 92 | /// A vaulted hot potato can move without going through the claim endpoint 93 | /// that requires a lambda and tag authority sign off. Can just use 94 | /// vault authority, or current_token_location as signer in a different 95 | /// endpoint. 96 | pub vault_state: VaultState, 97 | 98 | /// If vaulted, who can move this token around remotely. 99 | /// Memcmp-able by frontend to find tokens I own. 100 | pub vault_authority: Option, 101 | } 102 | 103 | impl Tag { 104 | /// The minimum required account size for a Sprinkle PDA. 105 | pub const SIZE: usize = 8 + // Anchor discriminator 106 | 8 + // UID 107 | 1 + // SprinkleType 108 | 32 + // TagAuthority pubkey 109 | 32 + // Bakery pubkey 110 | 8 + // TotalSupply 111 | 8 + // NumClaimed 112 | 8 + // PerUser 113 | 1 + // Minter pays? 114 | 32 + // TokenMint pubkey 115 | // Dont use option here so we can do offset memcmp lookups 116 | 8 + // Pricer per mint 117 | 32 + // CandyMachine pubkey 118 | 32 + // WhitelistToken pubkey 119 | 1 + // PDA bump 120 | 32 + // HotPotato location pubkey 121 | 1 + // Vaulted 122 | 33 + // VaultAuthority 123 | 16; // ~ Padding ~ 124 | } 125 | -------------------------------------------------------------------------------- /solana/programs/cupcake/src/state/user_info.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | /// PDA, associated with a user, created for each unique Sprinkle they claim. 4 | /// Maintains a counter of the total number of claims by the user for that Sprinkle. 5 | #[account] 6 | #[derive(Default)] 7 | pub struct UserInfo { 8 | /// The number of claims this user has executed from this Sprinkle. 9 | pub num_claimed: u64, 10 | 11 | /// Bump value used in the PDA generation for this UserInfo. 12 | pub bump: u8, 13 | } 14 | 15 | impl UserInfo { 16 | /// The minimum required account size for a UserInfo PDA. 17 | pub const SIZE: usize = 18 | 8 + // Anchor discriminator 19 | 8 + // NumClaimed 20 | 1; // PDA bump 21 | } 22 | -------------------------------------------------------------------------------- /solana/tests/programmable/amount.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from '@project-serum/anchor'; 2 | import { Program } from '@project-serum/anchor'; 3 | import { LAMPORTS_PER_SOL } from '@solana/web3.js'; 4 | import { Cupcake } from '../../target/types/cupcake'; 5 | import { CupcakeProgram } from '../../wip_sdk/cucpakeProgram'; 6 | import { SolanaClient } from '../../site/cupcake-data/clients/solana/SolanaClient'; 7 | import { createProgrammableNFT, createRuleSetAccount, mintNFT } from '../../wip_sdk/programmableAssets'; 8 | import { Bakery } from '../../wip_sdk/state/bakery'; 9 | 10 | describe('Programmable with `Amount` RuleSet', async () => { 11 | const admin = anchor.web3.Keypair.generate(); 12 | const user = anchor.web3.Keypair.generate(); 13 | 14 | const cupcakeProgram = anchor.workspace.Cupcake as Program; 15 | const cupcakeProgramClient = new CupcakeProgram(cupcakeProgram, admin); 16 | 17 | const bakeryPDA = await Bakery.PDA(admin.publicKey, cupcakeProgram.programId); 18 | 19 | it('Should fund test wallets', async () => { 20 | let sig = await cupcakeProgram.provider.connection.requestAirdrop(admin.publicKey, LAMPORTS_PER_SOL * 10); 21 | await cupcakeProgram.provider.connection.confirmTransaction(sig, 'singleGossip'); 22 | 23 | let sig2 = await cupcakeProgram.provider.connection.requestAirdrop(user.publicKey, LAMPORTS_PER_SOL * 10); 24 | await cupcakeProgram.provider.connection.confirmTransaction(sig2, 'singleGossip'); 25 | }); 26 | 27 | it('Should create a Bakery', async () => { 28 | const createBakeryTxHash = await SolanaClient.runCreateBakeryTxn( 29 | cupcakeProgramClient.bakeryAuthorityKeypair, 30 | admin, 31 | cupcakeProgram.provider.connection.rpcEndpoint 32 | ); 33 | console.log('createBakeryTxHash', createBakeryTxHash); 34 | }); 35 | 36 | it('Tests for pNFTs with Amount rules', async () => { 37 | const sprinkleUID = '66556455251101'; 38 | const sprinkleAuthority = anchor.web3.Keypair.generate(); 39 | 40 | const createRuleSetAccountTxHash = await createRuleSetAccount( 41 | 'cupcake-ruleset', 42 | admin, 43 | { 44 | 'Delegate:Transfer': { 45 | Amount: [69, 'Lt', 'Amount'], 46 | }, 47 | 'Transfer:TransferDelegate': { 48 | Amount: [69, 'Lt', 'Amount'], 49 | }, 50 | }, 51 | cupcakeProgramClient.program.provider 52 | ); 53 | console.log('createRuleSetAccountTxHash', createRuleSetAccountTxHash); 54 | 55 | const programmableNFTMint = await createProgrammableNFT( 56 | cupcakeProgramClient.program.provider, 57 | admin, 58 | admin.publicKey, 59 | 0, 60 | admin.publicKey, 61 | 'cupcake-ruleset' 62 | ); 63 | console.log('programmableNFTMint', programmableNFTMint.toString()); 64 | 65 | try { 66 | const bakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 67 | 'refillable1Of1', 68 | sprinkleUID, 69 | programmableNFTMint, 70 | 1, 71 | 1, 72 | sprinkleAuthority 73 | ); 74 | console.log('bakeSprinkleTxHash', bakeSprinkleTxHash); 75 | 76 | const claimSprinkleTxHash = await cupcakeProgramClient.claimSprinkle( 77 | sprinkleUID, 78 | user.publicKey, 79 | sprinkleAuthority 80 | ); 81 | console.log('claimSprinkleTxHash', claimSprinkleTxHash); 82 | } catch (e) { 83 | console.warn(e); 84 | } 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /solana/tests/programmable/noRuleset.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from '@project-serum/anchor'; 2 | import { Program } from '@project-serum/anchor'; 3 | import { LAMPORTS_PER_SOL } from '@solana/web3.js'; 4 | import { Cupcake } from '../../target/types/cupcake'; 5 | import { CupcakeProgram } from '../../wip_sdk/cucpakeProgram'; 6 | import { createProgrammableNFT, createRuleSetAccount, mintNFT } from '../../wip_sdk/programmableAssets'; 7 | import { Bakery } from '../../wip_sdk/state/bakery'; 8 | 9 | describe('Programmable with no RuleSet', async () => { 10 | const admin = anchor.web3.Keypair.generate(); 11 | const user = anchor.web3.Keypair.generate(); 12 | 13 | const cupcakeProgram = anchor.workspace.Cupcake as Program; 14 | const cupcakeProgramClient = new CupcakeProgram(cupcakeProgram, admin); 15 | 16 | const bakeryPDA = await Bakery.PDA(admin.publicKey, cupcakeProgram.programId); 17 | 18 | it('Should fund test wallets', async () => { 19 | let sig = await cupcakeProgram.provider.connection.requestAirdrop(admin.publicKey, LAMPORTS_PER_SOL * 10); 20 | await cupcakeProgram.provider.connection.confirmTransaction(sig, 'singleGossip'); 21 | 22 | let sig2 = await cupcakeProgram.provider.connection.requestAirdrop(user.publicKey, LAMPORTS_PER_SOL * 10); 23 | await cupcakeProgram.provider.connection.confirmTransaction(sig2, 'singleGossip'); 24 | }); 25 | 26 | it('Should create a Bakery', async () => { 27 | const createBakeryTxHash = await cupcakeProgramClient.createBakery(); 28 | console.log('createBakeryTxHash', createBakeryTxHash); 29 | }); 30 | 31 | it('Tests for pNFTs with no ruleset', async () => { 32 | const sprinkleUID = '66557433221590'; 33 | const sprinkleAuthority = anchor.web3.Keypair.generate(); 34 | 35 | const programmableNFTMint = await createProgrammableNFT( 36 | cupcakeProgramClient.program.provider, 37 | admin, 38 | admin.publicKey, 39 | 0, 40 | null, 41 | null 42 | ); 43 | console.log('programmableNFTMint', programmableNFTMint.toString()); 44 | 45 | try { 46 | const bakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 47 | 'refillable1Of1', 48 | sprinkleUID, 49 | programmableNFTMint, 50 | 1, 51 | 1, 52 | sprinkleAuthority 53 | ); 54 | console.log('bakeSprinkleTxHash', bakeSprinkleTxHash); 55 | 56 | const claimSprinkleTxHash = await cupcakeProgramClient.claimSprinkle( 57 | sprinkleUID, 58 | user.publicKey, 59 | sprinkleAuthority 60 | ); 61 | console.log('claimSprinkleTxHash', claimSprinkleTxHash); 62 | } catch (e) { 63 | console.warn(e); 64 | } 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /solana/tests/programmable/pass.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from '@project-serum/anchor'; 2 | import { Program } from '@project-serum/anchor'; 3 | import { LAMPORTS_PER_SOL } from '@solana/web3.js'; 4 | import { Cupcake } from '../../target/types/cupcake'; 5 | import { CupcakeProgram } from '../../wip_sdk/cucpakeProgram'; 6 | import { createProgrammableNFT, createRuleSetAccount, mintNFT } from '../../wip_sdk/programmableAssets'; 7 | import { Bakery } from '../../wip_sdk/state/bakery'; 8 | 9 | describe('Programmable with `Pass` RuleSet', async () => { 10 | const admin = anchor.web3.Keypair.generate(); 11 | const user = anchor.web3.Keypair.generate(); 12 | 13 | const cupcakeProgram = anchor.workspace.Cupcake as Program; 14 | const cupcakeProgramClient = new CupcakeProgram(cupcakeProgram, admin); 15 | 16 | const bakeryPDA = await Bakery.PDA(admin.publicKey, cupcakeProgram.programId); 17 | 18 | it('Should fund test wallets', async () => { 19 | let sig = await cupcakeProgram.provider.connection.requestAirdrop(admin.publicKey, LAMPORTS_PER_SOL * 10); 20 | await cupcakeProgram.provider.connection.confirmTransaction(sig, 'singleGossip'); 21 | 22 | let sig2 = await cupcakeProgram.provider.connection.requestAirdrop(user.publicKey, LAMPORTS_PER_SOL * 10); 23 | await cupcakeProgram.provider.connection.confirmTransaction(sig2, 'singleGossip'); 24 | }); 25 | 26 | it('Should create a Bakery', async () => { 27 | const createBakeryTxHash = await cupcakeProgramClient.createBakery(); 28 | console.log('createBakeryTxHash', createBakeryTxHash); 29 | }); 30 | 31 | it('Tests for pNFTs with Pass rules', async () => { 32 | const sprinkleUID = '66557433221100'; 33 | const sprinkleAuthority = anchor.web3.Keypair.generate(); 34 | 35 | const createRuleSetAccountTxHash = await createRuleSetAccount( 36 | 'cupcake-ruleset', 37 | admin, 38 | { 39 | 'Delegate:Transfer': 'Pass', 40 | 'Transfer:TransferDelegate': 'Pass', 41 | }, 42 | cupcakeProgramClient.program.provider 43 | ); 44 | console.log('createRuleSetAccountTxHash', createRuleSetAccountTxHash); 45 | 46 | const programmableNFTMint = await createProgrammableNFT( 47 | cupcakeProgramClient.program.provider, 48 | admin, 49 | admin.publicKey, 50 | 0, 51 | admin.publicKey, 52 | 'cupcake-ruleset' 53 | ); 54 | console.log('programmableNFTMint', programmableNFTMint.toString()); 55 | 56 | try { 57 | const bakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 58 | 'refillable1Of1', 59 | sprinkleUID, 60 | programmableNFTMint, 61 | 1, 62 | 1, 63 | sprinkleAuthority 64 | ); 65 | console.log('bakeSprinkleTxHash', bakeSprinkleTxHash); 66 | 67 | const reBakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 68 | 'refillable1Of1', 69 | sprinkleUID, 70 | programmableNFTMint, 71 | 1, 72 | 1, 73 | sprinkleAuthority 74 | ); 75 | console.log('reBakeSprinkleTxHash', reBakeSprinkleTxHash); 76 | 77 | const claimSprinkleTxHash = await cupcakeProgramClient.claimSprinkle( 78 | sprinkleUID, 79 | user.publicKey, 80 | sprinkleAuthority 81 | ); 82 | console.log('claimSprinkleTxHash', claimSprinkleTxHash); 83 | } catch (e) { 84 | console.warn(e); 85 | } 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /solana/tests/programmable/programOwned.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from '@project-serum/anchor'; 2 | import { Program } from '@project-serum/anchor'; 3 | import { LAMPORTS_PER_SOL } from '@solana/web3.js'; 4 | import { Cupcake } from '../../target/types/cupcake'; 5 | import { CupcakeProgram } from '../../wip_sdk/cucpakeProgram'; 6 | import { createProgrammableNFT, createRuleSetAccount, mintNFT } from '../../wip_sdk/programmableAssets'; 7 | import { Bakery } from '../../wip_sdk/state/bakery'; 8 | 9 | describe('Programmable with `ProgramOwned` RuleSet', async () => { 10 | const admin = anchor.web3.Keypair.generate(); 11 | const user = anchor.web3.Keypair.generate(); 12 | 13 | const cupcakeProgram = anchor.workspace.Cupcake as Program; 14 | const cupcakeProgramClient = new CupcakeProgram(cupcakeProgram, admin); 15 | 16 | const bakeryPDA = await Bakery.PDA(admin.publicKey, cupcakeProgram.programId); 17 | 18 | it('Should fund test wallets', async () => { 19 | let sig = await cupcakeProgram.provider.connection.requestAirdrop(admin.publicKey, LAMPORTS_PER_SOL * 10); 20 | await cupcakeProgram.provider.connection.confirmTransaction(sig, 'singleGossip'); 21 | 22 | let sig2 = await cupcakeProgram.provider.connection.requestAirdrop(user.publicKey, LAMPORTS_PER_SOL * 10); 23 | await cupcakeProgram.provider.connection.confirmTransaction(sig2, 'singleGossip'); 24 | }); 25 | 26 | it('Should create a Bakery', async () => { 27 | const createBakeryTxHash = await cupcakeProgramClient.createBakery(); 28 | console.log('createBakeryTxHash', createBakeryTxHash); 29 | }); 30 | 31 | it('Tests for pNFTs with ProgramOwned rules', async () => { 32 | const sprinkleUID = '66552255221441'; 33 | const sprinkleAuthority = anchor.web3.Keypair.generate(); 34 | 35 | const createRuleSetAccountTxHash = await createRuleSetAccount( 36 | 'cupcake-ruleset', 37 | admin, 38 | { 39 | 'Delegate:Transfer': { 40 | ProgramOwned: [Array.from(cupcakeProgram.programId.toBytes()), 'Delegate'], 41 | }, 42 | 'Transfer:TransferDelegate': 'Pass', 43 | }, 44 | cupcakeProgramClient.program.provider 45 | ); 46 | console.log('createRuleSetAccountTxHash', createRuleSetAccountTxHash); 47 | 48 | const programmableNFTMint = await createProgrammableNFT( 49 | cupcakeProgramClient.program.provider, 50 | admin, 51 | admin.publicKey, 52 | 0, 53 | admin.publicKey, 54 | 'cupcake-ruleset' 55 | ); 56 | console.log('programmableNFTMint', programmableNFTMint.toString()); 57 | 58 | const bakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 59 | 'refillable1Of1', 60 | sprinkleUID, 61 | programmableNFTMint, 62 | 1, 63 | 1, 64 | sprinkleAuthority 65 | ); 66 | console.log('bakeSprinkleTxHash', bakeSprinkleTxHash); 67 | 68 | const claimSprinkleTxHash = await cupcakeProgramClient.claimSprinkle( 69 | sprinkleUID, 70 | user.publicKey, 71 | sprinkleAuthority 72 | ); 73 | console.log('claimSprinkleTxHash', claimSprinkleTxHash); 74 | }); 75 | 76 | it('Tests for pNFTs with ProgramOwnedList rules failing', async () => { 77 | const sprinkleUID = '66554455221101'; 78 | const sprinkleAuthority = anchor.web3.Keypair.generate(); 79 | 80 | const createRuleSetAccountTxHash = await createRuleSetAccount( 81 | 'cupcake-ruleset', 82 | admin, 83 | { 84 | 'Delegate:Transfer': { 85 | ProgramOwnedList: [[Array.from(cupcakeProgram.programId.toBytes())], 'Delegate'], 86 | }, 87 | 'Transfer:TransferDelegate': 'Pass', 88 | }, 89 | cupcakeProgramClient.program.provider 90 | ); 91 | console.log('createRuleSetAccountTxHash', createRuleSetAccountTxHash); 92 | 93 | const programmableNFTMint = await createProgrammableNFT( 94 | cupcakeProgramClient.program.provider, 95 | admin, 96 | admin.publicKey, 97 | 0, 98 | admin.publicKey, 99 | 'cupcake-ruleset' 100 | ); 101 | console.log('programmableNFTMint', programmableNFTMint.toString()); 102 | 103 | const bakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 104 | 'refillable1Of1', 105 | sprinkleUID, 106 | programmableNFTMint, 107 | 1, 108 | 1, 109 | sprinkleAuthority 110 | ); 111 | console.log('bakeSprinkleTxHash', bakeSprinkleTxHash); 112 | 113 | const claimSprinkleTxHash = await cupcakeProgramClient.claimSprinkle( 114 | sprinkleUID, 115 | user.publicKey, 116 | sprinkleAuthority 117 | ); 118 | console.log('claimSprinkleTxHash', claimSprinkleTxHash); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /solana/tests/programmable/pubkeyMatch.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from '@project-serum/anchor'; 2 | import { Program } from '@project-serum/anchor'; 3 | import { LAMPORTS_PER_SOL } from '@solana/web3.js'; 4 | import { Cupcake } from '../../target/types/cupcake'; 5 | import { CupcakeProgram } from '../../wip_sdk/cucpakeProgram'; 6 | import { createProgrammableNFT, createRuleSetAccount, mintNFT } from '../../wip_sdk/programmableAssets'; 7 | import { Bakery } from '../../wip_sdk/state/bakery'; 8 | 9 | describe('Programmable with `Pubkey` RuleSet', async () => { 10 | const admin = anchor.web3.Keypair.generate(); 11 | const user = anchor.web3.Keypair.generate(); 12 | 13 | const cupcakeProgram = anchor.workspace.Cupcake as Program; 14 | const cupcakeProgramClient = new CupcakeProgram(cupcakeProgram, admin); 15 | 16 | const bakeryPDA = await Bakery.PDA(admin.publicKey, cupcakeProgram.programId); 17 | 18 | it('Should fund test wallets', async () => { 19 | let sig = await cupcakeProgram.provider.connection.requestAirdrop(admin.publicKey, LAMPORTS_PER_SOL * 10); 20 | await cupcakeProgram.provider.connection.confirmTransaction(sig, 'singleGossip'); 21 | 22 | let sig2 = await cupcakeProgram.provider.connection.requestAirdrop(user.publicKey, LAMPORTS_PER_SOL * 10); 23 | await cupcakeProgram.provider.connection.confirmTransaction(sig2, 'singleGossip'); 24 | }); 25 | 26 | it('Should create a Bakery', async () => { 27 | const createBakeryTxHash = await cupcakeProgramClient.createBakery(); 28 | console.log('createBakeryTxHash', createBakeryTxHash); 29 | }); 30 | 31 | it('Tests for pNFTs with PubkeyMatch rules', async () => { 32 | const sprinkleUID = '66554433221100'; 33 | const sprinkleAuthority = anchor.web3.Keypair.generate(); 34 | 35 | const createRuleSetAccountTxHash = await createRuleSetAccount( 36 | 'cupcake-ruleset', 37 | admin, 38 | { 39 | 'Delegate:Transfer': { 40 | PubkeyMatch: [Array.from(bakeryPDA.toBytes()), 'Delegate'], 41 | }, 42 | 'Transfer:TransferDelegate': { 43 | PubkeyMatch: [Array.from(user.publicKey.toBytes()), 'Destination'], 44 | }, 45 | }, 46 | cupcakeProgramClient.program.provider 47 | ); 48 | console.log('createRuleSetAccountTxHash', createRuleSetAccountTxHash); 49 | 50 | const programmableNFTMint = await createProgrammableNFT( 51 | cupcakeProgramClient.program.provider, 52 | admin, 53 | admin.publicKey, 54 | 0, 55 | admin.publicKey, 56 | 'cupcake-ruleset' 57 | ); 58 | console.log('programmableNFTMint', programmableNFTMint.toString()); 59 | 60 | try { 61 | const bakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 62 | 'refillable1Of1', 63 | sprinkleUID, 64 | programmableNFTMint, 65 | 1, 66 | 1, 67 | sprinkleAuthority 68 | ); 69 | console.log('bakeSprinkleTxHash', bakeSprinkleTxHash); 70 | 71 | const claimSprinkleTxHash = await cupcakeProgramClient.claimSprinkle( 72 | sprinkleUID, 73 | user.publicKey, 74 | sprinkleAuthority 75 | ); 76 | console.log('claimSprinkleTxHash', claimSprinkleTxHash); 77 | } catch (e) { 78 | console.warn(e); 79 | } 80 | }); 81 | 82 | it('Tests for pNFTs with PubkeyListMatch rules', async () => { 83 | const sprinkleUID = '66554433221101'; 84 | const sprinkleAuthority = anchor.web3.Keypair.generate(); 85 | 86 | const createRuleSetAccountTxHash = await createRuleSetAccount( 87 | 'cupcake-ruleset', 88 | admin, 89 | { 90 | 'Delegate:Transfer': { 91 | PubkeyListMatch: [[Array.from(bakeryPDA.toBytes())], 'Delegate'], 92 | }, 93 | 'Transfer:TransferDelegate': { 94 | PubkeyListMatch: [[Array.from(user.publicKey.toBytes())], 'Destination'], 95 | }, 96 | }, 97 | cupcakeProgramClient.program.provider 98 | ); 99 | console.log('createRuleSetAccountTxHash', createRuleSetAccountTxHash); 100 | 101 | const programmableNFTMint = await createProgrammableNFT( 102 | cupcakeProgramClient.program.provider, 103 | admin, 104 | admin.publicKey, 105 | 0, 106 | admin.publicKey, 107 | 'cupcake-ruleset' 108 | ); 109 | console.log('programmableNFTMint', programmableNFTMint.toString()); 110 | 111 | const bakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 112 | 'refillable1Of1', 113 | sprinkleUID, 114 | programmableNFTMint, 115 | 1, 116 | 1, 117 | sprinkleAuthority 118 | ); 119 | console.log('bakeSprinkleTxHash', bakeSprinkleTxHash); 120 | 121 | const claimSprinkleTxHash = await cupcakeProgramClient.claimSprinkle( 122 | sprinkleUID, 123 | user.publicKey, 124 | sprinkleAuthority 125 | ); 126 | console.log('claimSprinkleTxHash', claimSprinkleTxHash); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /solana/tests/refillable/refillable1Of1.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from '@project-serum/anchor'; 2 | import { Program } from '@project-serum/anchor'; 3 | import { LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; 4 | import { Cupcake } from '../../target/types/cupcake'; 5 | import { CupcakeProgram } from '../../wip_sdk/cucpakeProgram'; 6 | import { createProgrammableNFT, createRuleSetAccount, mintNFT } from '../../wip_sdk/programmableAssets'; 7 | import { Bakery } from '../../wip_sdk/state/bakery'; 8 | 9 | describe('`Refillable1Of1` Sprinkle', async () => { 10 | const admin = anchor.web3.Keypair.generate(); 11 | const user = anchor.web3.Keypair.generate(); 12 | 13 | let nftMint: PublicKey | undefined = undefined; 14 | let nftMint2: PublicKey | undefined = undefined; 15 | 16 | const cupcakeProgram = anchor.workspace.Cupcake as Program; 17 | const cupcakeProgramClient = new CupcakeProgram(cupcakeProgram, admin); 18 | 19 | const bakeryPDA = await Bakery.PDA(admin.publicKey, cupcakeProgram.programId); 20 | 21 | const sprinkleUID = '66554433221155'; 22 | const sprinkleAuthority = anchor.web3.Keypair.generate(); 23 | 24 | it('Should create a bakery', async () => { 25 | let sig = await cupcakeProgram.provider.connection.requestAirdrop(admin.publicKey, LAMPORTS_PER_SOL * 10); 26 | console.log('Admin key', admin.publicKey.toBase58()); 27 | await cupcakeProgram.provider.connection.confirmTransaction(sig, 'singleGossip'); 28 | let sig2 = await cupcakeProgram.provider.connection.requestAirdrop(user.publicKey, LAMPORTS_PER_SOL * 10); 29 | await cupcakeProgram.provider.connection.confirmTransaction(sig2, 'singleGossip'); 30 | 31 | const createBakeryTxHash = await cupcakeProgramClient.createBakery(); 32 | console.log('createBakeryTxHash', createBakeryTxHash); 33 | }); 34 | 35 | it('Should mint 2 non-programmable NFTs', async () => { 36 | nftMint = await mintNFT(cupcakeProgramClient.program.provider, admin, admin.publicKey, 0); 37 | console.log('nftMint', nftMint.toString()); 38 | 39 | nftMint2 = await mintNFT(cupcakeProgramClient.program.provider, admin, admin.publicKey, 0); 40 | console.log('nftMint2', nftMint2.toString()); 41 | }); 42 | 43 | it('Should bake a `Refillable1Of1` Sprinkle with the first mint', async () => { 44 | try { 45 | const bakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 46 | 'refillable1Of1', 47 | sprinkleUID, 48 | nftMint, 49 | 1, 50 | 1, 51 | sprinkleAuthority 52 | ); 53 | console.log('bakeSprinkleTxHash', bakeSprinkleTxHash); 54 | } catch (e) { 55 | console.warn(e); 56 | } 57 | }); 58 | 59 | it('Should claim the `Refillable1Of1` Sprinkle', async () => { 60 | try { 61 | const claimSprinkleTxHash = await cupcakeProgramClient.claimSprinkle( 62 | sprinkleUID, 63 | user.publicKey, 64 | sprinkleAuthority 65 | ); 66 | console.log('claimSprinkleTxHash', claimSprinkleTxHash); 67 | } catch (e) { 68 | console.warn(e); 69 | } 70 | }); 71 | 72 | it('Should rebake the `Refillable1Of1` Sprinkle with the 2nd mint', async () => { 73 | try { 74 | const bakeSprinkleTxHash = await cupcakeProgramClient.bakeSprinkle( 75 | 'refillable1Of1', 76 | sprinkleUID, 77 | nftMint2, 78 | 1, 79 | 2, 80 | sprinkleAuthority 81 | ); 82 | console.log('bakeSprinkleTxHash', bakeSprinkleTxHash); 83 | } catch (e) { 84 | console.warn(e); 85 | } 86 | }); 87 | 88 | it('Should claim the rebaked `Refillable1Of1` Sprinkle', async () => { 89 | try { 90 | const claimSprinkleTxHash = await cupcakeProgramClient.claimSprinkle( 91 | sprinkleUID, 92 | user.publicKey, 93 | sprinkleAuthority 94 | ); 95 | console.log('claimSprinkleTxHash', claimSprinkleTxHash); 96 | } catch (e) { 97 | console.warn(e); 98 | } 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /solana/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true, 9 | "composite": true, 10 | } 11 | } -------------------------------------------------------------------------------- /solana/wip_sdk/cucpakeProgram.ts: -------------------------------------------------------------------------------- 1 | import { Program, BN } from "@project-serum/anchor"; 2 | import { Keypair, PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY } from "@solana/web3.js"; 3 | import { Cupcake } from '../target/types/cupcake'; 4 | import * as TokenAuth from "@metaplex-foundation/mpl-token-auth-rules" 5 | import * as TokenMetadata from "@metaplex-foundation/mpl-token-metadata" 6 | import { ASSOCIATED_TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, getAssociatedTokenAddressSync } from "@solana/spl-token"; 7 | import { getTokenRecordPDA } from "./programmableAssets"; 8 | import { Bakery } from "./state/bakery"; 9 | import { Sprinkle } from "./state/sprinkle"; 10 | import { UserInfo } from "./state/userInfo"; 11 | 12 | export const PDA_PREFIX = 'cupcake'; 13 | 14 | export async function getMetadataPDA(tokenMint: PublicKey) { 15 | return (await PublicKey.findProgramAddress( 16 | [ 17 | Buffer.from("metadata"), 18 | TokenMetadata.PROGRAM_ID.toBuffer(), 19 | tokenMint.toBuffer() 20 | ], 21 | TokenMetadata.PROGRAM_ID 22 | ))[0] 23 | } 24 | 25 | export async function getMasterEditionPDA(tokenMint: PublicKey) { 26 | return (await PublicKey.findProgramAddress( 27 | [ 28 | Buffer.from("metadata"), 29 | TokenMetadata.PROGRAM_ID.toBuffer(), 30 | tokenMint.toBuffer(), 31 | Buffer.from("edition") 32 | ], 33 | TokenMetadata.PROGRAM_ID 34 | ))[0] 35 | } 36 | 37 | export class CupcakeProgram { 38 | program: Program; 39 | bakeryAuthorityKeypair: Keypair; 40 | bakeryPDA: PublicKey; 41 | 42 | constructor(program: Program, bakeryAuthorityKeypair: Keypair) { 43 | this.program = program; 44 | this.bakeryAuthorityKeypair = bakeryAuthorityKeypair; 45 | this.bakeryPDA = Bakery.PDA(bakeryAuthorityKeypair.publicKey, program.programId) 46 | } 47 | 48 | async createBakery() { 49 | return this.program.methods 50 | .initialize() 51 | .accounts({ 52 | authority: this.bakeryAuthorityKeypair.publicKey, 53 | payer: this.bakeryAuthorityKeypair.publicKey, 54 | config: this.bakeryPDA, 55 | }) 56 | .signers([this.bakeryAuthorityKeypair]) 57 | .rpc() 58 | } 59 | 60 | async bakeSprinkle(sprinkleType: string, uid: string, tokenMint: PublicKey, numClaims: number, perUser: number, sprinkleAuthority: Keypair) { 61 | const sprinkleUID = new BN(`CC${uid}`, "hex"); 62 | const sprinklePDA = await Sprinkle.PDA( 63 | this.bakeryAuthorityKeypair.publicKey, 64 | sprinkleUID, 65 | this.program.programId 66 | ); 67 | 68 | const bakeryTokenATA = getAssociatedTokenAddressSync( 69 | tokenMint, 70 | this.bakeryAuthorityKeypair.publicKey 71 | ); 72 | 73 | const metadataPDA = await getMetadataPDA(tokenMint); 74 | const masterEditionPDA = await getMasterEditionPDA(tokenMint); 75 | const tokenRecordPDA = await getTokenRecordPDA(tokenMint, bakeryTokenATA); 76 | 77 | const metadata = await TokenMetadata.Metadata.fromAccountAddress( 78 | this.program.provider.connection, 79 | metadataPDA 80 | ); 81 | const isProgrammable = !!metadata.programmableConfig 82 | const hasRuleset = !!metadata.programmableConfig?.ruleSet 83 | console.log(isProgrammable, hasRuleset, "baking") 84 | 85 | return this.program.methods 86 | .addOrRefillTag({ 87 | uid: sprinkleUID, 88 | numClaims: new BN(numClaims), 89 | perUser: new BN(perUser), 90 | minterPays: false, 91 | pricePerMint: null, 92 | whitelistBurn: false, 93 | tagType: { [sprinkleType]: true } 94 | } as any) 95 | .accounts({ 96 | authority: this.bakeryAuthorityKeypair.publicKey, 97 | payer: this.bakeryAuthorityKeypair.publicKey, 98 | config: this.bakeryPDA, 99 | tagAuthority: sprinkleAuthority.publicKey, 100 | tag: sprinklePDA 101 | }) 102 | .remainingAccounts([ 103 | { pubkey: tokenMint, isWritable: false, isSigner: false }, 104 | { pubkey: bakeryTokenATA, isWritable: true, isSigner: false }, 105 | { pubkey: metadataPDA, isWritable: true, isSigner: false }, 106 | { pubkey: masterEditionPDA, isWritable: false, isSigner: false }, 107 | { pubkey: tokenRecordPDA, isWritable: true, isSigner: false }, 108 | { 109 | pubkey: hasRuleset ? metadata.programmableConfig!.ruleSet : TokenMetadata.PROGRAM_ID, 110 | isWritable: false, 111 | isSigner: false 112 | }, 113 | { pubkey: TokenAuth.PROGRAM_ID, isWritable: false, isSigner: false }, 114 | { pubkey: TokenMetadata.PROGRAM_ID, isWritable: false, isSigner: false }, 115 | { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isWritable: false, isSigner: false }, 116 | ]) 117 | .signers([this.bakeryAuthorityKeypair]) 118 | .rpc() 119 | } 120 | 121 | async claimSprinkle(uid: string, user: PublicKey, sprinkleAuthorityKeypair: Keypair) { 122 | const sprinkleUID = new BN(`CC${uid}`, "hex"); 123 | const sprinklePDA = await Sprinkle.PDA( 124 | this.bakeryAuthorityKeypair.publicKey, 125 | sprinkleUID, 126 | this.program.programId 127 | ); 128 | const sprinkleState = await this.program.account.tag.fetch(await sprinklePDA); 129 | const token = getAssociatedTokenAddressSync( 130 | sprinkleState.tokenMint, 131 | this.bakeryAuthorityKeypair.publicKey 132 | ); 133 | const userATA = getAssociatedTokenAddressSync( 134 | sprinkleState.tokenMint, 135 | user 136 | ); 137 | const userInfoPDA = await UserInfo.PDA( 138 | this.bakeryAuthorityKeypair.publicKey, 139 | sprinkleUID, 140 | user, 141 | this.program.programId 142 | ); 143 | const metadataPDA = await getMetadataPDA(sprinkleState.tokenMint); 144 | const masterEditionPDA = await getMasterEditionPDA(sprinkleState.tokenMint); 145 | const tokenRecordPDA = await getTokenRecordPDA(sprinkleState.tokenMint, token); 146 | const destinationTokenRecordPDA = await getTokenRecordPDA(sprinkleState.tokenMint, userATA); 147 | 148 | const metadata = await TokenMetadata.Metadata.fromAccountAddress( 149 | this.program.provider.connection, 150 | await metadataPDA 151 | ); 152 | const isProgrammable = !!metadata.programmableConfig 153 | const hasRuleset = !!metadata.programmableConfig?.ruleSet 154 | 155 | return this.program.methods 156 | .claimTag(0) 157 | .accounts({ 158 | user, 159 | authority: this.bakeryAuthorityKeypair.publicKey, 160 | payer: this.bakeryAuthorityKeypair.publicKey, 161 | config: this.bakeryPDA, 162 | tagAuthority: sprinkleAuthorityKeypair.publicKey, 163 | tag: sprinklePDA, 164 | userInfo: userInfoPDA, 165 | }) 166 | .remainingAccounts([ 167 | // Base transfer accounts 168 | { pubkey: token, isWritable: true, isSigner: false }, 169 | { pubkey: userATA, isWritable: true, isSigner: false }, 170 | // Bakery auth 171 | { pubkey: this.bakeryAuthorityKeypair.publicKey, isWritable: false, isSigner: false }, 172 | // Mint 173 | { pubkey: sprinkleState.tokenMint, isWritable: false, isSigner: false }, 174 | // Metadata + edition 175 | { pubkey: metadataPDA, isWritable: true, isSigner: false }, 176 | { pubkey: masterEditionPDA, isWritable: true, isSigner: false }, 177 | // Current token location record 178 | { 179 | pubkey: isProgrammable ? tokenRecordPDA : TokenMetadata.PROGRAM_ID, 180 | isWritable: isProgrammable, 181 | isSigner: false 182 | }, 183 | // Destination token record 184 | { 185 | pubkey: isProgrammable ? destinationTokenRecordPDA : TokenMetadata.PROGRAM_ID, 186 | isWritable: isProgrammable, 187 | isSigner: false 188 | }, 189 | // Token ruleset 190 | { 191 | pubkey: hasRuleset ? metadata.programmableConfig!.ruleSet : TokenMetadata.PROGRAM_ID, 192 | isWritable: false, 193 | isSigner: false 194 | }, 195 | // Programs / Sysvars 196 | { pubkey: TokenAuth.PROGRAM_ID, isWritable: false, isSigner: false }, 197 | { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isWritable: false, isSigner: false }, 198 | { pubkey: TokenMetadata.PROGRAM_ID, isWritable: false, isSigner: false }, 199 | { pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, isWritable: false, isSigner: false }, 200 | ]) 201 | .preInstructions([ 202 | createAssociatedTokenAccountInstruction( 203 | this.bakeryAuthorityKeypair.publicKey, 204 | userATA, 205 | user, 206 | sprinkleState.tokenMint 207 | ) 208 | ]) 209 | .signers([this.bakeryAuthorityKeypair, sprinkleAuthorityKeypair]) 210 | .rpc() 211 | } 212 | 213 | 214 | } -------------------------------------------------------------------------------- /solana/wip_sdk/index.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | 3 | export const CUPCAKE_PROGRAM_ID = new PublicKey("cakeGJxEdGpZ3MJP8sM3QypwzuzZpko1ueonUQgKLPE") -------------------------------------------------------------------------------- /solana/wip_sdk/programmableAssets.ts: -------------------------------------------------------------------------------- 1 | import { Keypair, PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY, Transaction } from '@solana/web3.js'; 2 | import { BN, Provider } from '@project-serum/anchor'; 3 | import * as TokenAuth from '@metaplex-foundation/mpl-token-auth-rules'; 4 | import * as TokenMetadata from '@metaplex-foundation/mpl-token-metadata'; 5 | import { decode, encode } from '@msgpack/msgpack'; 6 | import { createAssociatedTokenAccount, createMint, mintTo, TOKEN_PROGRAM_ID } from '@solana/spl-token'; 7 | import { getMasterEditionPDA, getMetadataPDA } from './cucpakeProgram'; 8 | import { 9 | createCreateInstruction, 10 | createCreateMasterEditionV3Instruction, 11 | createCreateMetadataAccountV3Instruction, 12 | createMintInstruction, 13 | MasterEditionHasPrintsError, 14 | TokenStandard, 15 | } from '@metaplex-foundation/mpl-token-metadata'; 16 | import { ASSOCIATED_PROGRAM_ID } from '@project-serum/anchor/dist/cjs/utils/token'; 17 | 18 | export async function getTokenRecordPDA(tokenMint: PublicKey, associatedToken: PublicKey) { 19 | return ( 20 | await PublicKey.findProgramAddress( 21 | [ 22 | Buffer.from('metadata'), 23 | TokenMetadata.PROGRAM_ID.toBuffer(), 24 | tokenMint.toBuffer(), 25 | Buffer.from('token_record'), 26 | associatedToken.toBuffer(), 27 | ], 28 | TokenMetadata.PROGRAM_ID 29 | ) 30 | )[0]; 31 | } 32 | 33 | export async function createRuleSetAccount(name: string, owner: Keypair, rules: any, provider: Provider) { 34 | const encoded = encode([1, Array.from(owner.publicKey.toBytes()), name, rules]); 35 | const rulesetPDA = (await TokenAuth.findRuleSetPDA(owner.publicKey, 'cupcake-ruleset'))[0]; 36 | const createTokenAuthRuleSetIx = TokenAuth.createCreateOrUpdateInstruction( 37 | { 38 | ruleSetPda: rulesetPDA, 39 | payer: owner.publicKey, 40 | }, 41 | { 42 | createOrUpdateArgs: { 43 | __kind: 'V1', 44 | serializedRuleSet: encoded, 45 | }, 46 | } 47 | ); 48 | const txn = new Transaction().add(createTokenAuthRuleSetIx); 49 | txn.recentBlockhash = (await provider.connection.getRecentBlockhash()).blockhash; 50 | txn.feePayer = provider.wallet.publicKey; 51 | const signedTxn = await provider.wallet.signTransaction(txn); 52 | return (await provider.sendAll([{ tx: signedTxn, signers: [owner] }]))[0]; 53 | } 54 | 55 | export async function mintNFT( 56 | provider: Provider, 57 | payer: Keypair, 58 | creator: PublicKey, 59 | totalSupply: number, 60 | creator2?: PublicKey 61 | ) { 62 | // Initialize the token mint. 63 | const tokenMint = await createMint(provider.connection, payer, creator, creator, totalSupply); 64 | console.log('created mint'); 65 | 66 | // Create an ATA for the mint owned by admin. 67 | const token = await createAssociatedTokenAccount(provider.connection, payer, tokenMint, creator); 68 | console.log('created token'); 69 | 70 | await mintTo(provider.connection, payer, tokenMint, token, payer, 1); 71 | console.log('minted to ata'); 72 | 73 | const metadataPDA = await getMetadataPDA(tokenMint); 74 | const masterEditionPDA = await getMasterEditionPDA(tokenMint); 75 | 76 | const createMetadataIx = createCreateMetadataAccountV3Instruction( 77 | { 78 | payer: payer.publicKey, 79 | metadata: metadataPDA, 80 | mint: tokenMint, 81 | mintAuthority: payer.publicKey, 82 | updateAuthority: payer.publicKey, 83 | }, 84 | { 85 | createMetadataAccountArgsV3: { 86 | data: { 87 | name: 'CupcakeNFT', 88 | symbol: 'cNFT', 89 | uri: 'https://cupcake.com/collection.json', 90 | sellerFeeBasisPoints: 1000, 91 | creators: creator2 92 | ? [ 93 | { address: creator, share: 50, verified: true }, 94 | { address: creator2, share: 50, verified: false }, 95 | ] 96 | : [{ address: creator, share: 100, verified: true }], 97 | uses: null, 98 | collection: null, 99 | }, 100 | isMutable: true, 101 | collectionDetails: null, 102 | }, 103 | } 104 | ); 105 | 106 | const createMasterEditionIx = createCreateMasterEditionV3Instruction( 107 | { 108 | edition: masterEditionPDA, 109 | mint: tokenMint, 110 | updateAuthority: payer.publicKey, 111 | mintAuthority: payer.publicKey, 112 | payer: payer.publicKey, 113 | metadata: metadataPDA, 114 | }, 115 | { 116 | createMasterEditionArgs: { 117 | maxSupply: 0, 118 | }, 119 | } 120 | ); 121 | 122 | // Pack both instructions into a transaction and send/confirm it. 123 | const txn = new Transaction().add(createMetadataIx, createMasterEditionIx); 124 | txn.recentBlockhash = (await provider.connection.getRecentBlockhash()).blockhash; 125 | txn.feePayer = provider.wallet.publicKey; 126 | const signedTxn = await provider.wallet.signTransaction(txn); 127 | const txHash = (await provider.sendAll([{ tx: signedTxn, signers: [payer] }]))[0]; 128 | console.log(txHash); 129 | return tokenMint; 130 | } 131 | 132 | export async function createProgrammableNFT( 133 | provider: Provider, 134 | payer: Keypair, 135 | creator: PublicKey, 136 | totalSupply: number, 137 | ruleSetOwner?: PublicKey, 138 | ruleSetName?: string 139 | ) { 140 | const hasRuleset = !!ruleSetOwner || !!ruleSetName; 141 | 142 | // Initialize the token mint. 143 | const tokenMint = await createMint(provider.connection, payer, creator, creator, totalSupply); 144 | 145 | // Create an ATA for the mint owned by admin. 146 | const token = await createAssociatedTokenAccount(provider.connection, payer, tokenMint, creator); 147 | 148 | const rulesetPDA = hasRuleset ? (await TokenAuth.findRuleSetPDA(ruleSetOwner, ruleSetName))[0] : undefined; 149 | const metadataPDA = await getMetadataPDA(tokenMint); 150 | const masterEditionPDA = await getMasterEditionPDA(tokenMint); 151 | const tokenRecordPDA = await getTokenRecordPDA(tokenMint, token); 152 | 153 | // Create the Create instruction. 154 | const createCreateIx = createCreateInstruction( 155 | { 156 | metadata: metadataPDA, 157 | masterEdition: masterEditionPDA, 158 | mint: tokenMint, 159 | authority: creator, 160 | payer: payer.publicKey, 161 | updateAuthority: creator, 162 | sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, 163 | splTokenProgram: TOKEN_PROGRAM_ID, 164 | }, 165 | { 166 | createArgs: { 167 | __kind: 'V1', 168 | assetData: { 169 | name: 'CupcakeNFT', 170 | symbol: 'cNFT', 171 | uri: 'https://cupcake.com/collection.json', 172 | sellerFeeBasisPoints: 0, 173 | creators: [{ address: creator, share: 100, verified: true }], 174 | uses: null, 175 | collection: null, 176 | isMutable: true, 177 | primarySaleHappened: false, 178 | tokenStandard: TokenStandard.ProgrammableNonFungible, 179 | collectionDetails: null, 180 | ruleSet: rulesetPDA, 181 | }, 182 | decimals: 0, 183 | printSupply: { __kind: 'Zero' }, 184 | }, 185 | } 186 | ); 187 | 188 | // Create the Mint instruction. 189 | const createMintIx = createMintInstruction( 190 | { 191 | token, 192 | tokenOwner: creator, 193 | metadata: metadataPDA, 194 | masterEdition: masterEditionPDA, 195 | tokenRecord: tokenRecordPDA, 196 | mint: tokenMint, 197 | authority: creator, 198 | payer: payer.publicKey, 199 | sysvarInstructions: SYSVAR_INSTRUCTIONS_PUBKEY, 200 | splTokenProgram: TOKEN_PROGRAM_ID, 201 | splAtaProgram: ASSOCIATED_PROGRAM_ID, 202 | }, 203 | { 204 | mintArgs: { 205 | __kind: 'V1', 206 | amount: 1, 207 | authorizationData: null, 208 | }, 209 | } 210 | ); 211 | 212 | // Pack both instructions into a transaction and send/confirm it. 213 | const txn = new Transaction().add(createCreateIx, createMintIx); 214 | txn.recentBlockhash = (await provider.connection.getRecentBlockhash()).blockhash; 215 | txn.feePayer = provider.wallet.publicKey; 216 | const signedTxn = await provider.wallet.signTransaction(txn); 217 | const txHash = (await provider.sendAll([{ tx: signedTxn, signers: [payer] }]))[0]; 218 | console.log(txHash); 219 | return tokenMint; 220 | } 221 | -------------------------------------------------------------------------------- /solana/wip_sdk/state/bakery.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { PDA_PREFIX } from "../cucpakeProgram"; 3 | import { CUPCAKE_PROGRAM_ID } from ".."; 4 | 5 | export class Bakery { 6 | bakeryAuthority: PublicKey 7 | 8 | constructor() { 9 | 10 | } 11 | 12 | static PDA(bakeryAuthority: PublicKey, programId = CUPCAKE_PROGRAM_ID) { 13 | return PublicKey.findProgramAddressSync( 14 | [ 15 | Buffer.from(PDA_PREFIX), 16 | bakeryAuthority.toBuffer(), 17 | ], 18 | programId 19 | )[0] 20 | } 21 | } -------------------------------------------------------------------------------- /solana/wip_sdk/state/sprinkle.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { PDA_PREFIX } from "../cucpakeProgram"; 3 | import { BN } from "@project-serum/anchor"; 4 | import { CUPCAKE_PROGRAM_ID } from ".."; 5 | 6 | export enum SprinkleType { 7 | 8 | } 9 | 10 | export class Sprinkle { 11 | uid: BN 12 | sprinkleType: SprinkleType 13 | bakeryAuthority: PublicKey 14 | sprinkleAuthority: PublicKey 15 | 16 | constructor() { 17 | 18 | } 19 | 20 | static async PDA(bakeryAuthority: PublicKey, sprinkleUID: BN, programId = CUPCAKE_PROGRAM_ID) { 21 | return (await PublicKey.findProgramAddress( 22 | [ 23 | Buffer.from(PDA_PREFIX), 24 | bakeryAuthority.toBuffer(), 25 | sprinkleUID.toBuffer('le', 8) 26 | ], 27 | programId 28 | ))[0] 29 | } 30 | } -------------------------------------------------------------------------------- /solana/wip_sdk/state/userInfo.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | import { PDA_PREFIX } from "../cucpakeProgram"; 3 | import { BN } from "@project-serum/anchor"; 4 | import { CUPCAKE_PROGRAM_ID } from ".."; 5 | 6 | export class UserInfo { 7 | bakeryAuthority: PublicKey 8 | sprinkleUID: BN 9 | user: PublicKey 10 | 11 | 12 | constructor() { 13 | 14 | } 15 | 16 | static async PDA(bakeryAuthority: PublicKey, sprinkleUID: BN, user: PublicKey, programId = CUPCAKE_PROGRAM_ID) { 17 | return (await PublicKey.findProgramAddress( 18 | [ 19 | Buffer.from(PDA_PREFIX), 20 | bakeryAuthority.toBuffer(), 21 | sprinkleUID.toBuffer('le', 8), 22 | user.toBuffer() 23 | ], 24 | programId 25 | ))[0] 26 | } 27 | } --------------------------------------------------------------------------------