├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── dist ├── nftsandbox.es.js ├── nftsandbox.es.min.js ├── nftsandbox.umd.js └── nftsandbox.umd.min.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public ├── code.html ├── index.html ├── nft_json.json └── owner_properties.json ├── rollup.config.js ├── scripts └── build-srcdoc.js ├── src ├── Builder │ ├── index.js │ ├── srcdoc │ │ ├── index.html │ │ └── index.js │ └── utils.js ├── Output │ ├── Proxy.js │ └── Viewer.svelte ├── Sandbox.svelte ├── conf │ └── link.js └── main.js └── test └── test-build.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/dist/ 3 | 4 | .DS_Store 5 | 6 | .ideas -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .ideas 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2020 Simon Fremaux, https://www.dievardump.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BeyondNFT - Interactive NFT Sandbox 2 | 3 | *This was developed for the Untitled NFT Hackaton while working on a safe and hopefully in the future standard way to create Interactive NFTs.* 4 | 5 | This project is the open source Sandbox for executing and viewing Interactive NFTs. 6 | 7 | If you're just looking to easily embed NFTs, you might be looking for the [BeyondNFT/embeddable](https://github.com/BeyondNFT/embeddable) project. 8 | 9 | The Sandbox has no idea about the existence of the Blockchain (it works with JSON already loaded), when [BeyondNFT/embeddable](https://github.com/BeyondNFT/embeddable) makes direct calls to the smart contracts to get all the data needed. 10 | 11 | This Sandbox project is more an "in deep" presentation of what are Interactive NFTs and how they work. 12 | 13 | This is the good place to see [the end schema](#usage) of Interactive NFT's JSON, which can guide platforms into creating their own Sandbox if they do not trust this one. (you could for example run the NFTs in an iframe that you host on a subdomain of your website if srcdoc iframes are not your thing) 14 | 15 | ## "Glossary" 16 | 17 | **Creator**: the entity (Artist, Developer, Platform, ...) who created the NFT. 18 | **Owner**: the current NFT owner. 19 | **Viewer**: the person viewing the NFT (might be any user, including Creator or Owner). 20 | 21 | 22 | ## Disclaimer 23 | 24 | What follows in mainly for developers to know what the Sandbox expects for properties to work and to participate to the development of this (maybe?) new standard. 25 | 26 | As a **Creator** or an **Owner**, you should probably never have to edit any of those values by hand. The tools provided by the platform you used to create Interactive NFTs should be enough. 27 | 28 | If like me, you prefer reading code with comments better than long walls of text, just [jump to Usage](#usage) 29 | 30 | ## Descriptions 31 | 32 | ### Interactive NFTs 33 | 34 | Interactive NFT is a project that aims to: 35 | - Allow NFTs to be dynamic and/or interactives to the **Viewer** (procedural art with js, html, external data call, music player, video player...). 36 | - Allow a **Creator** to declare some values "configurable/variables" and an **Owner** to configure those values, making the NFT evolve. (a bit like Async, but because the NFT is code running, it can go much deeper) 37 | 38 | This way, an Artist could for example create a procedural piece of art, and allow the future **Owners** to set some key values used during the art rendering, thus making the Art evolutive. 39 | The **Owner** could for example edit colors, animation durations, texts, textures or anything that the **Creator** declared as editable. 40 | 41 | Another example would be a Card on which the **Viewer** could click; the card would then flip and present its attributes (that are stored in the NFT's JSON or even retrieven with an ajax call). All this directly in a Gallery / Website / Marketplace. 42 | 43 | ### Sandbox 44 | 45 | This Sandbox aims to display the NFT code in a safe way for the **Viewer**. 46 | For Security reasons, the NFT Code is sandboxed in an iframe, using srcdoc. 47 | By default, only "allow-script", "allow-pointer-lock", "allow-downloads" and "allow-popups" are enabled. So no access to parent context or same origin stuffs (cookies, localStorage & co). 48 | `eval` and `alert` are also disabled. 49 | 50 | - [MDN iframe (see sandbox)](https://developer.mozilla.org/fr/docs/Web/HTML/Element/iframe) 51 | - [Play safely in a sandbox](https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/) 52 | 53 | _Idea (but only idea, this is risky and would need to be handled with a lot of care): Create a system of Permissions, allowing **Creators** to request some permissions that the **Viewer** would have to accept or decline._ 54 | 55 | 56 | ## Dynamic and Interactive how? 57 | 58 | ### "Hey I'm interactive" 59 | 60 | The idea is to have a property `interactive_nft` in the NFT's JSON that declares the NFT as an Interactive NFT. 61 | This propery references where to find the code, the dependencies, the default configuration (if any) etc... 62 | 63 | #### Version 64 | 65 | Under `interactive_nft.version` must be declared the version of the Interactive NFT used. This in order to help Sandboxed in the futur to know how to render the NFT, if the "standard" comes to evolve. 66 | 67 | #### Code 68 | 69 | The code of the NFT. It is expected to be **VALID HTML** that will be inserted at the end of the iframe's `body` tag. 70 | 71 | There are 2 ways to declare code in the `interactive_nft`. 72 | 73 | - **(recommended)** as an URI under `interactive_nft.code_uri`(1) . The Sandbox will detect this property, fetch the URI as a text file and use the content as the NFT code. 74 | - (not recommended) As a string under `interactive_nft.code`. The Sandbox will also use this code as the NFT code. However, because the code is HTML and can contain special characters that are not playing well with JSON, it is recommended to not save the code directly into the NFT JSON, and to use the `code_uri` property instead. `interactive_nft.code` is mainly here to be used when creating the NFTs, because the code won't be hosted already. 75 | 76 | This gives a huge flexibility to **Creators**. They can then add HTML, JavaScript and CSS to the NFT. 77 | 78 | ```html 79 | 80 |
81 | 84 | ``` 85 | 86 | is a perfectly fine NFT code. 87 | 88 | (1) preferably hosted somewhere on a decentralized host (IPFS, Arweave or the like). 89 | 90 | ##### Code Signature 91 | 92 | Recommended: some platforms have requested that the `code_uri` (or `code` if used) was *signed* (using the Wallet, i.e `web3.eth.sign(message, account)`) by the **Creator**. 93 | 94 | This, allowing **Viewer** to be able to define an "allow-list" of creators they want to execute the content automatically. 95 | Else, the platform will use an "Execute this interactive NFT" button, to protect user from possible non expected rendering. 96 | 97 | Therefore, if you can, before saving the JSON, ask creators to sign `code_uri` (and save the result signature in `code_uri_signature`) or `code` (and save in `code_signature`) 98 | 99 | ** This is not used internally by the Sandbox, it's mostly to have a proof of who created the NFT, allowing more control for the Viewer ** 100 | 101 | #### Dependencies 102 | 103 | Dependencies are declared under `interactive_nft.dependencies`. 104 | It is an array of object of the form : { url: String, type: String } 105 | When loading the NFT, the Sandbox will add the dependencies into `script` and `style` tags in the iframe before the NFT code. 106 | 107 | JSON Schema 108 | ```json 109 | { 110 | "title": "Interactive NFT Dependencies", 111 | "type": "array", 112 | "items": { 113 | "type": "object", 114 | "properties": { 115 | "required": ["type", "url"], 116 | "type": { 117 | "type": "string", 118 | "enum": ["script", "style"], 119 | "description": "Type of the dependency (script or style tag)." 120 | }, 121 | "url": { 122 | "type": "string", 123 | "description": "URL of the dependency." 124 | } 125 | } 126 | } 127 | } 128 | ``` 129 | 130 | Example: 131 | 132 | ```json 133 | { 134 | "name": "Interactive NFT #1", 135 | "description": "The first of its kind.", 136 | "image": "http://gateway.ipfs.io/Qxn...", 137 | "interactive_nft": { 138 | "code_uri": "http://gateway.ipfs.io/Qxn...", 139 | "code_uri_signature": "0x0123456789abcdef...", 140 | "dependencies": [{ 141 | "type": "script", 142 | "url": "https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js" 143 | }, { 144 | "type": "style", 145 | "url": "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" 146 | }] 147 | } 148 | } 149 | ``` 150 | 151 | #### Configurable 152 | 153 | ***Contract implementing configurable Interactive NFTs are expected to provide a public method `interactiveConfURI(_tokenId, _owner) public returns (string)` which works for both ERC721 and ERC1155, and returns the conf URI for a tokenId (if set by the Owner). 154 | It should also be paired with a setter method `setInteractiveConfURI(_tokenId, _uri)` for **Owners** to be able to set the URI*** [see ERC721Configurable](#erc721configurable) 155 | 156 | If the **Creator** declared some configurable properties, they **MUST** have a default value: this to be able to reset configuration if anything is wrongly configured. 157 | 158 | Configurable properties are declared under `interactive_nft.properties`. 159 | It is an Array of Objects of the form { name: String, type: String, value: String|Number|Array|Object } 160 | 161 | JSON Schema 162 | ```json 163 | { 164 | "title": "Interactive NFT Configurable Properties", 165 | "type": "array", 166 | "items": { 167 | "type": "object", 168 | "properties": { 169 | "required": ["name", "type", "value"], 170 | "name": { 171 | "type": "string", 172 | "description": "Name of the property.", 173 | }, 174 | "type": { 175 | "type": "string", 176 | "enum": ["string", "number", "array", "object"], 177 | "description": "Type of the property.", 178 | }, 179 | "value": { 180 | "type": ["string", "number", "array", "object"], 181 | "description": "This is the default value. May be a string, number, object or array.", 182 | }, 183 | "description": { 184 | "type": "string", 185 | "description": "Description of what the property is used for. Might be needed if name not explicit enough.", 186 | } 187 | } 188 | } 189 | } 190 | ``` 191 | 192 | Example: 193 | 194 | ```json 195 | { 196 | "name": "Interactive NFT #1", 197 | "description": "The first of its kind.", 198 | "image": "http://gateway.ipfs.io/Qxn...", 199 | "interactive_nft": { 200 | "code_uri": "http://gateway.ipfs.io/Qxn...", 201 | "code_uri_signature": "0x0123456789abcdef...", 202 | "properties": [{ 203 | "name": "duration", 204 | "type": "number", 205 | "value": 500, 206 | "description": "Animation duration" 207 | }, 208 | { 209 | "name": "name", 210 | "type": "string", 211 | "value": "John Doe", 212 | "description": "Your cat name", 213 | }, 214 | { 215 | "name": "fruits", 216 | "type": "array", 217 | "value": ["orange", "banane"], 218 | "description": "Your favorite fruits", 219 | }, 220 | { 221 | "name": "fullname", 222 | "type": "object", 223 | "value": { 224 | "name": "Doe", 225 | "surname": "John" 226 | }, 227 | "description": "Your cat fullname", 228 | }] 229 | } 230 | } 231 | ``` 232 | 233 | _(What follows is automatically done when using [BeyondNFT/embeddable](https://github.com/BeyondNFT/embeddable) to show Interactive NFTs to **Viewers**.)_ 234 | 235 | When loading the NFT Metadata, the platform showing the NFT to the **Viewers** should check the existence of this `interactive_nft.properties`, and, if defined, should call the NFT contract `interactiveConfURI(_tokenId, _owner)` to see if the current NFT owner has a configuration file for this NFT. 236 | 237 | If a configuration file exists, **its content as a JavaScript object** will be expected to be passed to the sandbox under `owner_properties` ([see usage](#usage)) 238 | 239 | When detecting `interactive_nft.properties`, the Sandbox will automatically search for `owner_properties`. 240 | It will then override `interactive_nft.properties` with `owner_properties` and write the whole properties object to `window.context.properties`. It will then be accessible in the js code using `const propValue = window.context.properties[propertyName];` 241 | 242 | There is nothing that forces the configuration file to be on IPFS or any decentralized network. 243 | Platform could just mint the NFT with an URI to an empty configuration JSON that they host on their server (e.g `http://mynftplatform.com/tokens/0xcontract/{id}.config.json`), or **Owners** could just set the configuration file to any raw gist. 244 | This way, when the **Owner** wants to edit some values, they can do it without having to do a new transaction because they already have control over the configuration file (either through gist or through the platform editing the NFTs). 245 | It is sure less decentralized, but it saves the cost of transactions and the **Owner** can always set the URI stored in the contract to another URI, having full control over the file. 246 | 247 | ##### Configurable Types 248 | 249 | As of now, accepted types are 'string', 'number', 'object' and 'array' (see [Usage](#usage)) 250 | 251 | ## Usage 252 | 253 | This Sandbox is to be used as follow: 254 | 255 | `new SandBox({ target, props })` 256 | 257 | Construction parameter is an Object with two required properties : 258 | 259 | `target`: an HTML element where to render the Sandbox 260 | `props`: An object of properties read by the Sandbox 261 | 262 | Props Schema : 263 | 264 | ```json 265 | { 266 | "title": "Sandbox Props definition", 267 | "type": "object", 268 | "properties": { 269 | "required": ["data"], 270 | "data": { 271 | "type": ["object", "string"], 272 | "description": "NFT's JSON URI or Object (result of JSON.parse of the tokenURI -erc721- or uri -erc1155-)." 273 | }, 274 | "owner": { 275 | "type": "string", 276 | "description": "Address of the current token owner. Default to solidity's address(0)." 277 | }, 278 | "owner_properties": { 279 | "type": ["object", "string"], 280 | "description": "Owner configuration JSON URI or Object (result of JSON.parse of the interactiveConfURI contract method)." 281 | }, 282 | "ipfsGateway": { 283 | "type": ["string"], 284 | "description": "Gateway used to replace ipfs:// links in the NFT Metadata (i.e: 'https://gateway.ipfs.io/')." 285 | } 286 | } 287 | } 288 | ``` 289 | 290 | 291 | Full example of usage, because code is ten times better than words (You can also see [./public/index.html](./public/index.html) to see another one) 292 | 293 | ```js 294 | import Sandbox from 'https://cdn.jsdelivr.net/npm/@beyondnft/sandbox@0.0.9/dist/nftsandbox.es.min.js'; 295 | const sandbox = new SandBox({ 296 | target: document.querySelector('#viewer'), 297 | props: { 298 | // data: required | Mixed 299 | // This is either the tokenURI (string) or the content (object - JSON.parsed) of tokenURI|uri 300 | // 301 | // The Sandbox will look for the `interactive_nft` property in thos object 302 | // Else it will show the `image` property 303 | // 304 | // This whole JSON will also be available in the iframe as a JavaScript object 305 | // under `window.context.nft_json` so the code can read data from it 306 | // (for example to get attributes or the NFT name) 307 | data: { 308 | name: 'Interactive NFT #1', 309 | description: "The first of its kind.",, 310 | image: "http://gateway.ipfs.io/Qxn...", 311 | 312 | // interactive_nft: required (for Interactive NFT) 313 | // this is the property the Sandbox will look for in order to render the NFT 314 | interactive_nft: { 315 | // code_uri (optional) - URI where to find the code to execute 316 | code_uri: 'ipfs://Qx....', 317 | 318 | // the code_uri_signature contains the signature of the URI by the Creator, proving that they 319 | // are the one that created this code 320 | code_uri_signature: "0x0123456789abcdef...", 321 | 322 | // code (optional) (non recommended, mostly used when creating the NFT in a codepen-like env) 323 | code: '', 324 | 325 | // code_signature (optional) 326 | // the code_signature is used when using `code` and not `code_uri` 327 | // it contains the signature of the `code` by the Creator, proving that they 328 | // are the one that created this code 329 | code_signature: "0x0123456789abcdef...", 330 | 331 | // version: required 332 | // version of the Sandbox used to create this Interactive NFT, might be important at some point 333 | // if the way the sandbox works changes a lot, we will need to know what Sandbox to use to load 334 | // the NFT 335 | version: '0.0.13', 336 | 337 | // dependencies: optional 338 | // Array of dependencies that the Sandbox should load before executing the NFT code. 339 | // 340 | // @dev When a creator makes an NFT with dependencies, please show a reminder that if the 341 | // dependencies break (404, cdn stops working, ...) the NFT will not work anymore 342 | // Maybe offer to host those on IPFS?! 343 | dependencies: [ 344 | { 345 | // type: required (script|style) 346 | // type of the dependency, will define how it is handled by the Sandbox 347 | type: 'script', 348 | 349 | // url: required 350 | // url where to find the dependency 351 | url: 'https://cdn.jsdelivr.net/npm/p5@1.1.9/lib/p5.js', 352 | }, 353 | { 354 | type: 'style', 355 | url: 'https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap', 356 | }, 357 | // ... 358 | ], 359 | 360 | // properties: optional 361 | // an array of configurable properties 362 | // those properties are here to configure the NFT rendering 363 | // because those are the one the Owner of the NFT can modify and store somewhere 364 | // for them to be loaded when users are viewing their NFT 365 | // These values will be set in `window.context.properties` and be accessed in the NFT Javascript as follow 366 | // `const propertyValue = window.context.properties[propertyName]`; 367 | properties: [{ 368 | // name: required 369 | // name of the property 370 | name: "duration", 371 | 372 | // type: required 373 | // type of the property, can be number, string, array or object 374 | // mainly for configuration editors to know what they deal with 375 | type: "number", 376 | 377 | 378 | // value: required 379 | // this is the default value of the property. 380 | // @dev if is very important to have a value here, because if an owner "breaks" their NFT by saving a boggus configuration file, this value can be used to recreate a default configuration file that won't break the NFT. even empty values ('', 0, [], {}) should be enough 381 | value: 500, 382 | 383 | // description: optional 384 | // small text describing what this value is used for. 385 | // might be needed if the name is not explicit enough 386 | description: "Duration of the animation" 387 | }, 388 | { 389 | name: "name", 390 | type: "string", 391 | value: "John Doe" 392 | }, 393 | { 394 | name: "fruits", 395 | type: "array", 396 | value: ["orange", "banane"] 397 | }, 398 | { 399 | name: "fullname", 400 | type: "object", 401 | value: { 402 | name: "Doe", 403 | surname: "John" 404 | } 405 | } 406 | // ... 407 | ] 408 | } 409 | }, 410 | // owner: optional (but should be set when possible - defaults to 0x0000000000000000000000000000000000000000) 411 | // Address of the current owner/holder. Accessible in the code under `window.context.owner` 412 | owner: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', 413 | 414 | // owner_properties: optional | Mixed 415 | // This is either the uri where to find the configuration file (string) or the content of this file (object - JSON.parsed) 416 | // if it's an URI the sandbox will first retrieve the data and JSON.parse it 417 | // 418 | // object containing all the properties the Owner configured 419 | // this should be the content of the NFT's stored configuration file (if set by the owner and NFT is configurable) 420 | // the Sandbox will override the default props with the values in here, making the NFT 421 | // configurable by its owner 422 | owner_properties: { 423 | duration: 3000, 424 | name: 'Jane Doe', 425 | fruits: ['kiwi', 'strawberries'], 426 | fullname: { 427 | name: "Doe", 428 | surname: "Jane" 429 | } 430 | }, 431 | 432 | // ipfsGateway: optional (Default: "https://gateway.ipfs.io") 433 | // Some nfts have ipfs links as image, code etc... 434 | // The Sandbox will replace "ipfs://" in all links by the value of this property before loading those 435 | ipfsGateway: "https://gateway.ipfs.io", 436 | } 437 | }); 438 | ``` 439 | 440 | #### Process of the Sandbox 441 | 442 | The Sandbox internal processus is as follow : 443 | - Sandbox checks if data is set 444 | - if it's a string, it will try to fetch it and JSON.parse 445 | - else it should be a JavaScript Object, content of the NFT JSON 446 | - Sandbox checks if `owner_properties` is set 447 | - if it's a string, it will try to fetch it and JSON.parse the result 448 | - else it should be a JavaScript Object, content of the configuration JSON 449 | - Sandbox checks if code is provided in `data.interactive_nft.code` (not recommended - only use in dev mode) 450 | - If not Sandbox will try to fetch code from `data.interactive_nft.code_uri` property 451 | - else it won't run and trhow an error 452 | - Sandbox will load `data.interactive_nft.dependencies` in respective script and style tags 453 | - Sandbox will assign `owner_properties` to `data.interactive_nft.properties` 454 | - the result will be assigned to `window.context.properties` making it accessible to the javascript. 455 | - Sandbox will set `window.context.owner` to the provided owner property (or default to address(0)) 456 | - - Sandbox will assign to `window.context.nft_json` the value of the current json file 457 | - Sandbox will execute the code 458 | 459 | #### Data access for dynamisme of the NFTs 460 | 461 | This way in your code's JavaScript you can access data as follow: 462 | - `window.context.nft_json` contains all the NFT data (name, description, attributes, image, ...) 463 | - `window.context.owner` contains the NFT Owner address 464 | - `window.context.properties` contains the configurable properties 465 | 466 | 467 | ## Events 468 | 469 | The Sandbox dispatch events to let you know what happens inside. 470 | You can listen to those events using `sandboxInstance.$on(eventName, fn)` 471 | 472 | For the moment, events are: 473 | 474 | `loaded`: when the Sandbox loaded all dependencies, created the configuration object and added the code to the iframe (and it didn't throw an error) 475 | 476 | `error`: When any `unhandledrejection` happens in the iframe. The iframe content will blur and stop any interaction if that happens (@TODO: reflect whether to make this configurable directly when instantiating the sandbox?!) 477 | 478 | `warning`: is emitted when something odd happens, but is not blocking the rendering (for example if the owner_properties is a string that doesn't resolve to a valid JSON file when fetched) 479 | 480 | ## User Warning 481 | 482 | Executing "user submitted" code is always a bit tricky. 483 | 484 | Using a Sandboxed iframe should already help to stop a lot of problem that can happen, but still, users should be warned to not do anything that seems suspicious when the NFT runs. 485 | 486 | Therefore, if you use this Sandbox on your website, before the first rendering it would be nice to tell users that they will be shown an Interactive NFT and that they must be carefull because you do not have control over the code itself. 487 | 488 | @TODO: Should we set that directly in the Sandbox? 489 | @TODO: Research if there is a way to create an AST of all the javascript code in the NFT code, and add things to it, to prevent for example infitinie loop or other things 490 | ## Development 491 | 492 | Feel free to help develop or correct bug as this is highly POC for the moment. 493 | 494 | The Sandbox is created using [Svelte](http://svelte.dev) (because... what else?). 495 | 496 | `npm run build` to build the Sandbox code 497 | 498 | `npm run dev` to edit with a watch (autoreload and so on). 499 | 500 | `npm run srcdoc` need to be run after modifying [the iframe html](./src/Output/srcdoc/index.html) in dev mode (automatically done before a build) 501 | 502 | `npm run start` will serve the files under public, which let you play a bit with the Sandbox if you edit [./public/index.html](./public/index.html) 503 | 504 | ## Errors 505 | 506 | The Sandbox will dispatch [event](#events) in case of an error occuring in the iframe. 507 | It will also show a message to the **Viewer** and blur the Sandbox. 508 | 509 | TODO: Make this configurable with a prop `showerror`? 510 | 511 | ## ERC721Configurable & ERC1155configurable 512 | 513 | Here are the Configurable contracts for ERC721 and ERC1155 tokens 514 | 515 | ERC721 516 | 517 | ```solidity 518 | // SPDX-License-Identifier: MIT 519 | pragma solidity ^0.6.0; 520 | 521 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 522 | 523 | abstract contract ERC721Configurable is ERC721 { 524 | // map of tokenId => interactiveConfURI. 525 | mapping(uint256 => string) private _interactiveConfURIs; 526 | 527 | function _setInteractiveConfURI( 528 | uint256 tokenId, 529 | string calldata _interactiveConfURI 530 | ) internal virtual { 531 | require( 532 | _exists(tokenId), 533 | "ERC721Configurable: Configuration URI for unknown token" 534 | ); 535 | _interactiveConfURIs[tokenId] = _interactiveConfURI; 536 | } 537 | 538 | /** 539 | * Configuration uri for tokenId 540 | */ 541 | function interactiveConfURI(uint256 tokenId) 542 | public 543 | virtual 544 | view 545 | returns (string memory) 546 | { 547 | require( 548 | _exists(tokenId), 549 | "ERC721Configurable: Configuration URI query for unknown token" 550 | ); 551 | return _interactiveConfURIs[tokenId]; 552 | } 553 | } 554 | ``` 555 | 556 | ERC1155 557 | 558 | ```solidity 559 | // SPDX-License-Identifier: MIT 560 | pragma solidity ^0.6.0; 561 | 562 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 563 | 564 | abstract contract ERC1155Configurable is ERC1155 { 565 | // map of tokenId => interactiveConfURI. 566 | mapping(uint256 => mapping(address => string)) private _interactiveConfURIs; 567 | 568 | function _setInteractiveConfURI( 569 | uint256 _tokenId, 570 | address _owner, 571 | string calldata _interactiveConfURI 572 | ) internal virtual { 573 | _interactiveConfURIs[_tokenId][_owner] = _interactiveConfURI; 574 | } 575 | 576 | /** 577 | * Configuration uri for tokenId 578 | */ 579 | function interactiveConfURI(uint256 _tokenId, address _owner) 580 | public 581 | virtual 582 | view 583 | returns (string memory) 584 | { 585 | return _interactiveConfURIs[_tokenId][_owner]; 586 | } 587 | } 588 | ``` 589 | -------------------------------------------------------------------------------- /dist/nftsandbox.es.js: -------------------------------------------------------------------------------- 1 | function mitt(n){return {all:n=n||new Map,on:function(t,e){var i=n.get(t);i&&i.push(e)||n.set(t,[e]);},off:function(t,e){var i=n.get(t);i&&i.splice(i.indexOf(e)>>>0,1);},emit:function(t,e){(n.get(t)||[]).slice().map(function(n){n(e);}),(n.get("*")||[]).slice().map(function(n){n(t,e);});}}} 2 | 3 | let ipfsGateway = ''; 4 | var IPFS = { 5 | init(gateway) { 6 | ipfsGateway = gateway; 7 | }, 8 | 9 | process(link) { 10 | return link 11 | .replace('ipfs://ipfs/', 'ipfs://') 12 | .replace('ipfs://', ipfsGateway); 13 | }, 14 | }; 15 | 16 | function makeDependencies(dependencies) { 17 | let result = ''; 18 | if (Array.isArray(dependencies)) { 19 | for (const dependency of dependencies) { 20 | const type = dependency.type; 21 | if (type === 'script') { 22 | result += ``; 23 | } else if (type === 'style') { 24 | result += ``; 32 | } else { 33 | console.log(`Unknown dependency type ${type}`); 34 | } 35 | } 36 | } 37 | 38 | return result; 39 | } 40 | 41 | function scriptify(script) { 42 | return ``; 43 | } 44 | 45 | var srcdoc = "\n\n \n \n\n \n \n \n \n \n \n\n"; 46 | 47 | const emitter = mitt(); 48 | 49 | var Builder = { 50 | emitter, 51 | json: {}, 52 | code: '', 53 | owner_properties: {}, 54 | owner: '', 55 | async init(json, code, owner_properties, owner, ipfsGateway, fetch) { 56 | if (ipfsGateway) { 57 | IPFS.init(ipfsGateway); 58 | } 59 | 60 | if ('string' === typeof json) { 61 | try { 62 | json = JSON.parse(json); 63 | } catch (e) { 64 | json = IPFS.process(json); 65 | await fetch(json) 66 | .then((res) => res.json()) 67 | .then((_data) => (json = _data)) 68 | .catch((e) => { 69 | emitter.emit( 70 | 'warning', 71 | new Error(`Error while fetching NFT's JSON at ${json}`), 72 | ); 73 | json = null; 74 | }); 75 | } 76 | } 77 | 78 | if (!json) { 79 | emitter.emit( 80 | 'error', 81 | new Error(`You need to provide a json property. 82 | Either a valid uri to the NFT JSON or the parsed NFT JSON.`), 83 | ); 84 | return; 85 | } 86 | 87 | // first fetch owner_properties if it's an URI 88 | if (owner_properties) { 89 | if ('string' === typeof owner_properties) { 90 | await fetch(IPFS.process(owner_properties)) 91 | .then((res) => res.json()) 92 | .then((_owner_properties) => (owner_properties = _owner_properties)) 93 | .catch((e) => { 94 | emitter.emit( 95 | 'warning', 96 | `Error while fetching owner_properties on ${owner_properties}. 97 | Setting owner_properties to default.`, 98 | ); 99 | owner_properties = {}; 100 | }); 101 | } 102 | } 103 | 104 | // get code from interactive_nft 105 | if (!code && json.interactive_nft) { 106 | if (json.interactive_nft.code) { 107 | code = json.interactive_nft.code; 108 | // if the code is in the interactive_nft property (not recommended) 109 | // we delete it because it might be a problem when we pass this object to the iframe 110 | // because we have to stringify it 111 | json.interactive_nft.code = null; 112 | } else if (json.interactive_nft.code_uri) { 113 | await fetch(IPFS.process(json.interactive_nft.code_uri)) 114 | .then((res) => res.text()) 115 | .then((_code) => (code = _code)) 116 | .catch((e) => { 117 | emitter.emit( 118 | 'Error', 119 | new Error( 120 | `Error while fetching ${json.interactive_nft.code_uri}`, 121 | ), 122 | ); 123 | }); 124 | } 125 | } 126 | 127 | if (!code) { 128 | emitter.emit( 129 | 'Error', 130 | new Error('You need to provide code for this NFT to run'), 131 | ); 132 | } 133 | 134 | this.json = json; 135 | this.code = code; 136 | this.owner_properties = owner_properties; 137 | this.owner = owner; 138 | }, 139 | 140 | build() { 141 | return this.replaceCode(srcdoc); 142 | }, 143 | 144 | makeDependencies() { 145 | if (!this.json.interactive_nft) { 146 | return ''; 147 | } 148 | return makeDependencies(this.json.interactive_nft.dependencies); 149 | }, 150 | 151 | loadProps() { 152 | const props = {}; 153 | if (this.json.interactive_nft) { 154 | if (Array.isArray(this.json.interactive_nft.properties)) { 155 | let overrider = {}; 156 | if ( 157 | this.owner_properties && 158 | 'object' === typeof this.owner_properties 159 | ) { 160 | overrider = this.owner_properties; 161 | } 162 | 163 | // no Object.assign because we only want declared props to be set 164 | for (const prop of this.json.interactive_nft.properties) { 165 | props[prop.name] = prop.value; 166 | if (undefined !== overrider[prop.name]) { 167 | props[prop.name] = overrider[prop.name]; 168 | } 169 | } 170 | } 171 | } 172 | 173 | return props; 174 | }, 175 | 176 | replaceCode(srcdoc) { 177 | let content = this.makeDependencies(); 178 | 179 | const props = this.loadProps(); 180 | 181 | content += scriptify(` 182 | // specific p5 because it's causing troubles. 183 | if (typeof p5 !== 'undefined' && p5.disableFriendlyErrors) { 184 | p5.disableFriendlyErrors = true; 185 | new p5(); 186 | } 187 | 188 | window.context = { 189 | get owner() { 190 | let owner = owner; 191 | if (window.location?.search) { 192 | const params = new URLSearchParams(window.location.search); 193 | owner = params.get('owner') || owner; 194 | } 195 | return owner; 196 | }, 197 | nft_json: JSON.parse(${JSON.stringify(JSON.stringify(this.json))}), 198 | properties: JSON.parse('${JSON.stringify(props)}'), 199 | }; 200 | `); 201 | 202 | content += this.code; 203 | 204 | return srcdoc.replace('', content); 205 | }, 206 | }; 207 | 208 | function noop() { } 209 | function run(fn) { 210 | return fn(); 211 | } 212 | function blank_object() { 213 | return Object.create(null); 214 | } 215 | function run_all(fns) { 216 | fns.forEach(run); 217 | } 218 | function is_function(thing) { 219 | return typeof thing === 'function'; 220 | } 221 | function safe_not_equal(a, b) { 222 | return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); 223 | } 224 | function is_empty(obj) { 225 | return Object.keys(obj).length === 0; 226 | } 227 | 228 | function append(target, node) { 229 | target.appendChild(node); 230 | } 231 | function insert(target, node, anchor) { 232 | target.insertBefore(node, anchor || null); 233 | } 234 | function detach(node) { 235 | node.parentNode.removeChild(node); 236 | } 237 | function element(name) { 238 | return document.createElement(name); 239 | } 240 | function text(data) { 241 | return document.createTextNode(data); 242 | } 243 | function space() { 244 | return text(' '); 245 | } 246 | function empty() { 247 | return text(''); 248 | } 249 | function attr(node, attribute, value) { 250 | if (value == null) 251 | node.removeAttribute(attribute); 252 | else if (node.getAttribute(attribute) !== value) 253 | node.setAttribute(attribute, value); 254 | } 255 | function children(element) { 256 | return Array.from(element.childNodes); 257 | } 258 | function toggle_class(element, name, toggle) { 259 | element.classList[toggle ? 'add' : 'remove'](name); 260 | } 261 | function custom_event(type, detail) { 262 | const e = document.createEvent('CustomEvent'); 263 | e.initCustomEvent(type, false, false, detail); 264 | return e; 265 | } 266 | 267 | let current_component; 268 | function set_current_component(component) { 269 | current_component = component; 270 | } 271 | function get_current_component() { 272 | if (!current_component) 273 | throw new Error('Function called outside component initialization'); 274 | return current_component; 275 | } 276 | function onMount(fn) { 277 | get_current_component().$$.on_mount.push(fn); 278 | } 279 | function createEventDispatcher() { 280 | const component = get_current_component(); 281 | return (type, detail) => { 282 | const callbacks = component.$$.callbacks[type]; 283 | if (callbacks) { 284 | // TODO are there situations where events could be dispatched 285 | // in a server (non-DOM) environment? 286 | const event = custom_event(type, detail); 287 | callbacks.slice().forEach(fn => { 288 | fn.call(component, event); 289 | }); 290 | } 291 | }; 292 | } 293 | // TODO figure out if we still want to support 294 | // shorthand events, or if we want to implement 295 | // a real bubbling mechanism 296 | function bubble(component, event) { 297 | const callbacks = component.$$.callbacks[event.type]; 298 | if (callbacks) { 299 | callbacks.slice().forEach(fn => fn(event)); 300 | } 301 | } 302 | 303 | const dirty_components = []; 304 | const binding_callbacks = []; 305 | const render_callbacks = []; 306 | const flush_callbacks = []; 307 | const resolved_promise = Promise.resolve(); 308 | let update_scheduled = false; 309 | function schedule_update() { 310 | if (!update_scheduled) { 311 | update_scheduled = true; 312 | resolved_promise.then(flush); 313 | } 314 | } 315 | function add_render_callback(fn) { 316 | render_callbacks.push(fn); 317 | } 318 | function add_flush_callback(fn) { 319 | flush_callbacks.push(fn); 320 | } 321 | let flushing = false; 322 | const seen_callbacks = new Set(); 323 | function flush() { 324 | if (flushing) 325 | return; 326 | flushing = true; 327 | do { 328 | // first, call beforeUpdate functions 329 | // and update components 330 | for (let i = 0; i < dirty_components.length; i += 1) { 331 | const component = dirty_components[i]; 332 | set_current_component(component); 333 | update(component.$$); 334 | } 335 | set_current_component(null); 336 | dirty_components.length = 0; 337 | while (binding_callbacks.length) 338 | binding_callbacks.pop()(); 339 | // then, once components are updated, call 340 | // afterUpdate functions. This may cause 341 | // subsequent updates... 342 | for (let i = 0; i < render_callbacks.length; i += 1) { 343 | const callback = render_callbacks[i]; 344 | if (!seen_callbacks.has(callback)) { 345 | // ...so guard against infinite loops 346 | seen_callbacks.add(callback); 347 | callback(); 348 | } 349 | } 350 | render_callbacks.length = 0; 351 | } while (dirty_components.length); 352 | while (flush_callbacks.length) { 353 | flush_callbacks.pop()(); 354 | } 355 | update_scheduled = false; 356 | flushing = false; 357 | seen_callbacks.clear(); 358 | } 359 | function update($$) { 360 | if ($$.fragment !== null) { 361 | $$.update(); 362 | run_all($$.before_update); 363 | const dirty = $$.dirty; 364 | $$.dirty = [-1]; 365 | $$.fragment && $$.fragment.p($$.ctx, dirty); 366 | $$.after_update.forEach(add_render_callback); 367 | } 368 | } 369 | const outroing = new Set(); 370 | let outros; 371 | function group_outros() { 372 | outros = { 373 | r: 0, 374 | c: [], 375 | p: outros // parent group 376 | }; 377 | } 378 | function check_outros() { 379 | if (!outros.r) { 380 | run_all(outros.c); 381 | } 382 | outros = outros.p; 383 | } 384 | function transition_in(block, local) { 385 | if (block && block.i) { 386 | outroing.delete(block); 387 | block.i(local); 388 | } 389 | } 390 | function transition_out(block, local, detach, callback) { 391 | if (block && block.o) { 392 | if (outroing.has(block)) 393 | return; 394 | outroing.add(block); 395 | outros.c.push(() => { 396 | outroing.delete(block); 397 | if (callback) { 398 | if (detach) 399 | block.d(1); 400 | callback(); 401 | } 402 | }); 403 | block.o(local); 404 | } 405 | } 406 | 407 | function bind(component, name, callback) { 408 | const index = component.$$.props[name]; 409 | if (index !== undefined) { 410 | component.$$.bound[index] = callback; 411 | callback(component.$$.ctx[index]); 412 | } 413 | } 414 | function create_component(block) { 415 | block && block.c(); 416 | } 417 | function mount_component(component, target, anchor, customElement) { 418 | const { fragment, on_mount, on_destroy, after_update } = component.$$; 419 | fragment && fragment.m(target, anchor); 420 | if (!customElement) { 421 | // onMount happens before the initial afterUpdate 422 | add_render_callback(() => { 423 | const new_on_destroy = on_mount.map(run).filter(is_function); 424 | if (on_destroy) { 425 | on_destroy.push(...new_on_destroy); 426 | } 427 | else { 428 | // Edge case - component was destroyed immediately, 429 | // most likely as a result of a binding initialising 430 | run_all(new_on_destroy); 431 | } 432 | component.$$.on_mount = []; 433 | }); 434 | } 435 | after_update.forEach(add_render_callback); 436 | } 437 | function destroy_component(component, detaching) { 438 | const $$ = component.$$; 439 | if ($$.fragment !== null) { 440 | run_all($$.on_destroy); 441 | $$.fragment && $$.fragment.d(detaching); 442 | // TODO null out other refs, including component.$$ (but need to 443 | // preserve final state?) 444 | $$.on_destroy = $$.fragment = null; 445 | $$.ctx = []; 446 | } 447 | } 448 | function make_dirty(component, i) { 449 | if (component.$$.dirty[0] === -1) { 450 | dirty_components.push(component); 451 | schedule_update(); 452 | component.$$.dirty.fill(0); 453 | } 454 | component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); 455 | } 456 | function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) { 457 | const parent_component = current_component; 458 | set_current_component(component); 459 | const $$ = component.$$ = { 460 | fragment: null, 461 | ctx: null, 462 | // state 463 | props, 464 | update: noop, 465 | not_equal, 466 | bound: blank_object(), 467 | // lifecycle 468 | on_mount: [], 469 | on_destroy: [], 470 | on_disconnect: [], 471 | before_update: [], 472 | after_update: [], 473 | context: new Map(parent_component ? parent_component.$$.context : []), 474 | // everything else 475 | callbacks: blank_object(), 476 | dirty, 477 | skip_bound: false 478 | }; 479 | let ready = false; 480 | $$.ctx = instance 481 | ? instance(component, options.props || {}, (i, ret, ...rest) => { 482 | const value = rest.length ? rest[0] : ret; 483 | if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { 484 | if (!$$.skip_bound && $$.bound[i]) 485 | $$.bound[i](value); 486 | if (ready) 487 | make_dirty(component, i); 488 | } 489 | return ret; 490 | }) 491 | : []; 492 | $$.update(); 493 | ready = true; 494 | run_all($$.before_update); 495 | // `false` as a special case of no DOM component 496 | $$.fragment = create_fragment ? create_fragment($$.ctx) : false; 497 | if (options.target) { 498 | if (options.hydrate) { 499 | const nodes = children(options.target); 500 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 501 | $$.fragment && $$.fragment.l(nodes); 502 | nodes.forEach(detach); 503 | } 504 | else { 505 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 506 | $$.fragment && $$.fragment.c(); 507 | } 508 | if (options.intro) 509 | transition_in(component.$$.fragment); 510 | mount_component(component, options.target, options.anchor, options.customElement); 511 | flush(); 512 | } 513 | set_current_component(parent_component); 514 | } 515 | /** 516 | * Base class for Svelte components. Used when dev=false. 517 | */ 518 | class SvelteComponent { 519 | $destroy() { 520 | destroy_component(this, 1); 521 | this.$destroy = noop; 522 | } 523 | $on(type, callback) { 524 | const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); 525 | callbacks.push(callback); 526 | return () => { 527 | const index = callbacks.indexOf(callback); 528 | if (index !== -1) 529 | callbacks.splice(index, 1); 530 | }; 531 | } 532 | $set($$props) { 533 | if (this.$$set && !is_empty($$props)) { 534 | this.$$.skip_bound = true; 535 | this.$$set($$props); 536 | this.$$.skip_bound = false; 537 | } 538 | } 539 | } 540 | 541 | let uid = 1; 542 | 543 | function handle_command_message(cmd_data) { 544 | let action = cmd_data.action; 545 | let id = cmd_data.cmd_id; 546 | let handler = this.pending_cmds.get(id); 547 | 548 | if (handler) { 549 | this.pending_cmds.delete(id); 550 | if (action === 'cmd_error') { 551 | let { message, stack } = cmd_data; 552 | let e = new Error(message); 553 | e.stack = stack; 554 | handler.reject(e); 555 | } 556 | 557 | if (action === 'cmd_ok') { 558 | handler.resolve(cmd_data.args || 'ok'); 559 | } 560 | } else { 561 | console.error('command not found', id, cmd_data, [ 562 | ...this.pending_cmds.keys(), 563 | ]); 564 | } 565 | } 566 | 567 | function handle_repl_message(event) { 568 | if (event.source !== this.iframe.contentWindow) return; 569 | const { action, args } = event.data; 570 | 571 | switch (action) { 572 | case 'cmd_error': 573 | case 'cmd_ok': 574 | return handle_command_message.call(this, event.data); 575 | case 'fetch_progress': 576 | return this.handlers.on_fetch_progress(args.remaining); 577 | case 'error': 578 | return this.handlers.on_error(event.data); 579 | case 'unhandledrejection': 580 | return this.handlers.on_unhandled_rejection(event.data); 581 | case 'console': 582 | return this.handlers.on_console(event.data); 583 | case 'console_group': 584 | return this.handlers.on_console_group(event.data); 585 | case 'console_group_collapsed': 586 | return this.handlers.on_console_group_collapsed(event.data); 587 | case 'console_group_end': 588 | return this.handlers.on_console_group_end(event.data); 589 | default: 590 | const handler = `on_${action}`; 591 | if ('function' === typeof this.handlers[handler]) { 592 | this.handlers[handler](event.data); 593 | } 594 | } 595 | } 596 | 597 | class Proxy { 598 | constructor(iframe, handlers) { 599 | this.iframe = iframe; 600 | this.handlers = handlers; 601 | 602 | this.pending_cmds = new Map(); 603 | 604 | this.handle_event = handle_repl_message.bind(this); 605 | window.addEventListener('message', this.handle_event, false); 606 | } 607 | 608 | destroy() { 609 | window.removeEventListener('message', this.handle_event); 610 | } 611 | 612 | iframe_command(action, args) { 613 | return new Promise((resolve, reject) => { 614 | const cmd_id = uid++; 615 | 616 | this.pending_cmds.set(cmd_id, { resolve, reject }); 617 | 618 | this.iframe.contentWindow.postMessage({ action, cmd_id, args }, '*'); 619 | }); 620 | } 621 | 622 | size() { 623 | return this.iframe_command('size'); 624 | } 625 | 626 | eval(script) { 627 | return this.iframe_command('eval', { script }); 628 | } 629 | 630 | add_script(script) { 631 | return this.iframe_command('add_script', script); 632 | } 633 | 634 | add_script_content(script) { 635 | return this.iframe_command('add_script_content', script); 636 | } 637 | 638 | add_style(style) { 639 | return this.iframe_command('add_style', style); 640 | } 641 | 642 | add_asset(asset) { 643 | return this.iframe_command('add_asset', asset); 644 | } 645 | 646 | handle_links() { 647 | return this.iframe_command('catch_clicks', {}); 648 | } 649 | } 650 | 651 | /* src/Output/Viewer.svelte generated by Svelte v3.35.0 */ 652 | 653 | function add_css() { 654 | var style = element("style"); 655 | style.id = "svelte-uaiew6-style"; 656 | style.textContent = ".beyondnft__sandbox.svelte-uaiew6{background-color:white;border:none;width:100%;height:100%;position:relative}iframe.svelte-uaiew6{min-width:100%;min-height:100%;border:none;display:block}.greyed-out.svelte-uaiew6{filter:grayscale(50%) blur(1px);opacity:0.25}.beyondnft__sandbox__error.svelte-uaiew6{font-size:0.9em;position:absolute;top:0;left:0;padding:5px}"; 657 | append(document.head, style); 658 | } 659 | 660 | // (120:2) {#if error} 661 | function create_if_block$1(ctx) { 662 | let strong; 663 | 664 | return { 665 | c() { 666 | strong = element("strong"); 667 | strong.innerHTML = `Sorry, an error occured while executing the NFT.`; 668 | attr(strong, "class", "beyondnft__sandbox__error svelte-uaiew6"); 669 | }, 670 | m(target, anchor) { 671 | insert(target, strong, anchor); 672 | }, 673 | d(detaching) { 674 | if (detaching) detach(strong); 675 | } 676 | }; 677 | } 678 | 679 | function create_fragment$1(ctx) { 680 | let div; 681 | let iframe_1; 682 | let iframe_1_sandbox_value; 683 | let t; 684 | let if_block = /*error*/ ctx[4] && create_if_block$1(); 685 | 686 | return { 687 | c() { 688 | div = element("div"); 689 | iframe_1 = element("iframe"); 690 | t = space(); 691 | if (if_block) if_block.c(); 692 | attr(iframe_1, "title", "Sandbox"); 693 | attr(iframe_1, "sandbox", iframe_1_sandbox_value = `allow-scripts allow-pointer-lock allow-popups allow-downloads ${/*sandbox_props*/ ctx[1]}`); 694 | attr(iframe_1, "allow", "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; camera; microphone; xr-spatial-tracking; geolocation; fullscreen; magnetometer; midi; vr;"); 695 | attr(iframe_1, "srcdoc", /*src*/ ctx[0]); 696 | attr(iframe_1, "class", "svelte-uaiew6"); 697 | toggle_class(iframe_1, "greyed-out", /*error*/ ctx[4] || pending || /*pending_imports*/ ctx[3]); 698 | attr(div, "class", "beyondnft__sandbox svelte-uaiew6"); 699 | }, 700 | m(target, anchor) { 701 | insert(target, div, anchor); 702 | append(div, iframe_1); 703 | /*iframe_1_binding*/ ctx[6](iframe_1); 704 | append(div, t); 705 | if (if_block) if_block.m(div, null); 706 | }, 707 | p(ctx, [dirty]) { 708 | if (dirty & /*sandbox_props*/ 2 && iframe_1_sandbox_value !== (iframe_1_sandbox_value = `allow-scripts allow-pointer-lock allow-popups allow-downloads ${/*sandbox_props*/ ctx[1]}`)) { 709 | attr(iframe_1, "sandbox", iframe_1_sandbox_value); 710 | } 711 | 712 | if (dirty & /*src*/ 1) { 713 | attr(iframe_1, "srcdoc", /*src*/ ctx[0]); 714 | } 715 | 716 | if (dirty & /*error, pending, pending_imports*/ 24) { 717 | toggle_class(iframe_1, "greyed-out", /*error*/ ctx[4] || pending || /*pending_imports*/ ctx[3]); 718 | } 719 | 720 | if (/*error*/ ctx[4]) { 721 | if (if_block) ; else { 722 | if_block = create_if_block$1(); 723 | if_block.c(); 724 | if_block.m(div, null); 725 | } 726 | } else if (if_block) { 727 | if_block.d(1); 728 | if_block = null; 729 | } 730 | }, 731 | i: noop, 732 | o: noop, 733 | d(detaching) { 734 | if (detaching) detach(div); 735 | /*iframe_1_binding*/ ctx[6](null); 736 | if (if_block) if_block.d(); 737 | } 738 | }; 739 | } 740 | 741 | let pending = false; 742 | 743 | function instance$1($$self, $$props, $$invalidate) { 744 | let { proxy } = $$props; 745 | let { src } = $$props; 746 | let { sandbox_props = "" } = $$props; 747 | const dispatch = createEventDispatcher(); 748 | let iframe; 749 | let pending_imports = 0; 750 | let error; 751 | let logs = []; 752 | let log_group_stack = []; 753 | let current_log_group = logs; 754 | let last_console_event; 755 | 756 | onMount(() => { 757 | $$invalidate(5, proxy = new Proxy(iframe, 758 | { 759 | on_fetch_progress: progress => { 760 | $$invalidate(3, pending_imports = progress); 761 | }, 762 | on_error: event => { 763 | push_logs({ level: "error", args: [event.value] }); 764 | show_error(event.value); 765 | }, 766 | on_unhandled_rejection: event => { 767 | let error = event.value; 768 | if (typeof error === "string") error = { message: error }; 769 | error.message = "Uncaught (in promise): " + error.message; 770 | push_logs({ level: "error", args: [error] }); 771 | show_error(error); 772 | }, 773 | on_console: log => { 774 | if (log.level === "clear") { 775 | clear_logs(); 776 | push_logs(log); 777 | } else if (log.duplicate) { 778 | increment_duplicate_log(); 779 | } else { 780 | push_logs(log); 781 | } 782 | }, 783 | on_console_group: action => { 784 | group_logs(action.label, false); 785 | }, 786 | on_console_group_end: () => { 787 | ungroup_logs(); 788 | }, 789 | on_console_group_collapsed: action => { 790 | group_logs(action.label, true); 791 | } 792 | })); 793 | 794 | iframe.addEventListener("load", () => { 795 | proxy.handle_links(); 796 | !error && dispatch("loaded"); 797 | }); 798 | 799 | return () => { 800 | proxy.destroy(); 801 | }; 802 | }); 803 | 804 | function show_error(e) { 805 | $$invalidate(4, error = e); 806 | dispatch("error", e); 807 | } 808 | 809 | function push_logs(log) { 810 | current_log_group.push(last_console_event = log); 811 | logs = logs; 812 | } 813 | 814 | function group_logs(label, collapsed) { 815 | const group_log = { 816 | level: "group", 817 | label, 818 | collapsed, 819 | logs: [] 820 | }; 821 | 822 | current_log_group.push(group_log); 823 | log_group_stack.push(current_log_group); 824 | current_log_group = group_log.logs; 825 | logs = logs; 826 | } 827 | 828 | function ungroup_logs() { 829 | current_log_group = log_group_stack.pop(); 830 | } 831 | 832 | function increment_duplicate_log() { 833 | const last_log = current_log_group[current_log_group.length - 1]; 834 | 835 | if (last_log) { 836 | last_log.count = (last_log.count || 1) + 1; 837 | logs = logs; 838 | } else { 839 | last_console_event.count = 1; 840 | push_logs(last_console_event); 841 | } 842 | } 843 | 844 | function clear_logs() { 845 | current_log_group = logs = []; 846 | } 847 | 848 | function iframe_1_binding($$value) { 849 | binding_callbacks[$$value ? "unshift" : "push"](() => { 850 | iframe = $$value; 851 | $$invalidate(2, iframe); 852 | }); 853 | } 854 | 855 | $$self.$$set = $$props => { 856 | if ("proxy" in $$props) $$invalidate(5, proxy = $$props.proxy); 857 | if ("src" in $$props) $$invalidate(0, src = $$props.src); 858 | if ("sandbox_props" in $$props) $$invalidate(1, sandbox_props = $$props.sandbox_props); 859 | }; 860 | 861 | return [src, sandbox_props, iframe, pending_imports, error, proxy, iframe_1_binding]; 862 | } 863 | 864 | class Viewer extends SvelteComponent { 865 | constructor(options) { 866 | super(); 867 | if (!document.getElementById("svelte-uaiew6-style")) add_css(); 868 | init(this, options, instance$1, create_fragment$1, safe_not_equal, { proxy: 5, src: 0, sandbox_props: 1 }); 869 | } 870 | } 871 | 872 | /* src/Sandbox.svelte generated by Svelte v3.35.0 */ 873 | 874 | function create_else_block(ctx) { 875 | let t; 876 | 877 | return { 878 | c() { 879 | t = text("Loading..."); 880 | }, 881 | m(target, anchor) { 882 | insert(target, t, anchor); 883 | }, 884 | p: noop, 885 | i: noop, 886 | o: noop, 887 | d(detaching) { 888 | if (detaching) detach(t); 889 | } 890 | }; 891 | } 892 | 893 | // (38:0) {#if ready} 894 | function create_if_block(ctx) { 895 | let viewer; 896 | let updating_proxy; 897 | let current; 898 | 899 | function viewer_proxy_binding(value) { 900 | /*viewer_proxy_binding*/ ctx[11](value); 901 | } 902 | 903 | let viewer_props = { 904 | src: Builder.build(), 905 | sandbox_props: /*sandbox_props*/ ctx[0] 906 | }; 907 | 908 | if (/*proxy*/ ctx[1] !== void 0) { 909 | viewer_props.proxy = /*proxy*/ ctx[1]; 910 | } 911 | 912 | viewer = new Viewer({ props: viewer_props }); 913 | binding_callbacks.push(() => bind(viewer, "proxy", viewer_proxy_binding)); 914 | viewer.$on("loaded", /*loaded_handler*/ ctx[12]); 915 | viewer.$on("error", /*error_handler*/ ctx[13]); 916 | viewer.$on("warning", /*warning_handler*/ ctx[14]); 917 | 918 | return { 919 | c() { 920 | create_component(viewer.$$.fragment); 921 | }, 922 | m(target, anchor) { 923 | mount_component(viewer, target, anchor); 924 | current = true; 925 | }, 926 | p(ctx, dirty) { 927 | const viewer_changes = {}; 928 | if (dirty & /*sandbox_props*/ 1) viewer_changes.sandbox_props = /*sandbox_props*/ ctx[0]; 929 | 930 | if (!updating_proxy && dirty & /*proxy*/ 2) { 931 | updating_proxy = true; 932 | viewer_changes.proxy = /*proxy*/ ctx[1]; 933 | add_flush_callback(() => updating_proxy = false); 934 | } 935 | 936 | viewer.$set(viewer_changes); 937 | }, 938 | i(local) { 939 | if (current) return; 940 | transition_in(viewer.$$.fragment, local); 941 | current = true; 942 | }, 943 | o(local) { 944 | transition_out(viewer.$$.fragment, local); 945 | current = false; 946 | }, 947 | d(detaching) { 948 | destroy_component(viewer, detaching); 949 | } 950 | }; 951 | } 952 | 953 | function create_fragment(ctx) { 954 | let current_block_type_index; 955 | let if_block; 956 | let if_block_anchor; 957 | let current; 958 | const if_block_creators = [create_if_block, create_else_block]; 959 | const if_blocks = []; 960 | 961 | function select_block_type(ctx, dirty) { 962 | if (/*ready*/ ctx[2]) return 0; 963 | return 1; 964 | } 965 | 966 | current_block_type_index = select_block_type(ctx); 967 | if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); 968 | 969 | return { 970 | c() { 971 | if_block.c(); 972 | if_block_anchor = empty(); 973 | }, 974 | m(target, anchor) { 975 | if_blocks[current_block_type_index].m(target, anchor); 976 | insert(target, if_block_anchor, anchor); 977 | current = true; 978 | }, 979 | p(ctx, [dirty]) { 980 | let previous_block_index = current_block_type_index; 981 | current_block_type_index = select_block_type(ctx); 982 | 983 | if (current_block_type_index === previous_block_index) { 984 | if_blocks[current_block_type_index].p(ctx, dirty); 985 | } else { 986 | group_outros(); 987 | 988 | transition_out(if_blocks[previous_block_index], 1, 1, () => { 989 | if_blocks[previous_block_index] = null; 990 | }); 991 | 992 | check_outros(); 993 | if_block = if_blocks[current_block_type_index]; 994 | 995 | if (!if_block) { 996 | if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); 997 | if_block.c(); 998 | } else { 999 | if_block.p(ctx, dirty); 1000 | } 1001 | 1002 | transition_in(if_block, 1); 1003 | if_block.m(if_block_anchor.parentNode, if_block_anchor); 1004 | } 1005 | }, 1006 | i(local) { 1007 | if (current) return; 1008 | transition_in(if_block); 1009 | current = true; 1010 | }, 1011 | o(local) { 1012 | transition_out(if_block); 1013 | current = false; 1014 | }, 1015 | d(detaching) { 1016 | if_blocks[current_block_type_index].d(detaching); 1017 | if (detaching) detach(if_block_anchor); 1018 | } 1019 | }; 1020 | } 1021 | 1022 | function getBuilder() { 1023 | return builder; 1024 | } 1025 | 1026 | function instance($$self, $$props, $$invalidate) { 1027 | const dispatch = createEventDispatcher(); 1028 | let { data = {} } = $$props; 1029 | let { code = "" } = $$props; 1030 | let { owner_properties = {} } = $$props; 1031 | let { owner = "0x0000000000000000000000000000000000000000" } = $$props; 1032 | let { sandbox_props = "" } = $$props; 1033 | let { ipfsGateway = "https://gateway.ipfs.io/ipfs/" } = $$props; 1034 | const version = "0.0.16"; 1035 | let proxy = null; 1036 | let ready = false; 1037 | 1038 | function getProxy() { 1039 | return proxy; 1040 | } 1041 | 1042 | Builder.emitter.on("warning", e => dispatch("warning", e.detail)); 1043 | Builder.emitter.on("error", e => dispatch("error", e.detail)); 1044 | 1045 | onMount(async () => { 1046 | await Builder.init(data, code, owner_properties, owner, ipfsGateway, fetch); 1047 | $$invalidate(2, ready = true); 1048 | }); 1049 | 1050 | function viewer_proxy_binding(value) { 1051 | proxy = value; 1052 | $$invalidate(1, proxy); 1053 | } 1054 | 1055 | function loaded_handler(event) { 1056 | bubble($$self, event); 1057 | } 1058 | 1059 | function error_handler(event) { 1060 | bubble($$self, event); 1061 | } 1062 | 1063 | function warning_handler(event) { 1064 | bubble($$self, event); 1065 | } 1066 | 1067 | $$self.$$set = $$props => { 1068 | if ("data" in $$props) $$invalidate(3, data = $$props.data); 1069 | if ("code" in $$props) $$invalidate(4, code = $$props.code); 1070 | if ("owner_properties" in $$props) $$invalidate(5, owner_properties = $$props.owner_properties); 1071 | if ("owner" in $$props) $$invalidate(6, owner = $$props.owner); 1072 | if ("sandbox_props" in $$props) $$invalidate(0, sandbox_props = $$props.sandbox_props); 1073 | if ("ipfsGateway" in $$props) $$invalidate(7, ipfsGateway = $$props.ipfsGateway); 1074 | }; 1075 | 1076 | return [ 1077 | sandbox_props, 1078 | proxy, 1079 | ready, 1080 | data, 1081 | code, 1082 | owner_properties, 1083 | owner, 1084 | ipfsGateway, 1085 | version, 1086 | getProxy, 1087 | getBuilder, 1088 | viewer_proxy_binding, 1089 | loaded_handler, 1090 | error_handler, 1091 | warning_handler 1092 | ]; 1093 | } 1094 | 1095 | class Sandbox extends SvelteComponent { 1096 | constructor(options) { 1097 | super(); 1098 | 1099 | init(this, options, instance, create_fragment, safe_not_equal, { 1100 | data: 3, 1101 | code: 4, 1102 | owner_properties: 5, 1103 | owner: 6, 1104 | sandbox_props: 0, 1105 | ipfsGateway: 7, 1106 | version: 8, 1107 | getProxy: 9, 1108 | getBuilder: 10 1109 | }); 1110 | } 1111 | 1112 | get version() { 1113 | return this.$$.ctx[8]; 1114 | } 1115 | 1116 | get getProxy() { 1117 | return this.$$.ctx[9]; 1118 | } 1119 | 1120 | get getBuilder() { 1121 | return getBuilder; 1122 | } 1123 | } 1124 | 1125 | Sandbox.Builder = Builder; 1126 | 1127 | export default Sandbox; 1128 | -------------------------------------------------------------------------------- /dist/nftsandbox.es.min.js: -------------------------------------------------------------------------------- 1 | let n="";var e={init(e){n=e},process:e=>e.replace("ipfs://ipfs/","ipfs://").replace("ipfs://",n)};const t={all:o=o||new Map,on:function(n,e){var t=o.get(n);t&&t.push(e)||o.set(n,[e])},off:function(n,e){var t=o.get(n);t&&t.splice(t.indexOf(e)>>>0,1)},emit:function(n,e){(o.get(n)||[]).slice().map((function(n){n(e)})),(o.get("*")||[]).slice().map((function(t){t(n,e)}))}};var o,r={emitter:t,json:{},code:"",owner_properties:{},owner:"",async init(n,o,r,s,i,a){if(i&&e.init(i),"string"==typeof n)try{n=JSON.parse(n)}catch(o){n=e.process(n),await a(n).then((n=>n.json())).then((e=>n=e)).catch((e=>{t.emit("warning",new Error(`Error while fetching NFT's JSON at ${n}`)),n=null}))}n?(r&&"string"==typeof r&&await a(e.process(r)).then((n=>n.json())).then((n=>r=n)).catch((n=>{t.emit("warning",`Error while fetching owner_properties on ${r}.\n\t\t\t\t\t\tSetting owner_properties to default.`),r={}})),!o&&n.interactive_nft&&(n.interactive_nft.code?(o=n.interactive_nft.code,n.interactive_nft.code=null):n.interactive_nft.code_uri&&await a(e.process(n.interactive_nft.code_uri)).then((n=>n.text())).then((n=>o=n)).catch((e=>{t.emit("Error",new Error(`Error while fetching ${n.interactive_nft.code_uri}`))}))),o||t.emit("Error",new Error("You need to provide code for this NFT to run")),this.json=n,this.code=o,this.owner_properties=r,this.owner=s):t.emit("error",new Error("You need to provide a json property.\n\t\t\tEither a valid uri to the NFT JSON or the parsed NFT JSON."))},build(){return this.replaceCode("\n\n \n \n\n `; 29 | } else if (type === 'style') { 30 | result += ``; 38 | } else { 39 | console.log(`Unknown dependency type ${type}`); 40 | } 41 | } 42 | } 43 | 44 | return result; 45 | } 46 | 47 | function scriptify(script) { 48 | return ``; 49 | } 50 | 51 | var srcdoc = "\n\n \n \n\n \n \n \n \n \n \n\n"; 52 | 53 | const emitter = mitt(); 54 | 55 | var Builder = { 56 | emitter, 57 | json: {}, 58 | code: '', 59 | owner_properties: {}, 60 | owner: '', 61 | async init(json, code, owner_properties, owner, ipfsGateway, fetch) { 62 | if (ipfsGateway) { 63 | IPFS.init(ipfsGateway); 64 | } 65 | 66 | if ('string' === typeof json) { 67 | try { 68 | json = JSON.parse(json); 69 | } catch (e) { 70 | json = IPFS.process(json); 71 | await fetch(json) 72 | .then((res) => res.json()) 73 | .then((_data) => (json = _data)) 74 | .catch((e) => { 75 | emitter.emit( 76 | 'warning', 77 | new Error(`Error while fetching NFT's JSON at ${json}`), 78 | ); 79 | json = null; 80 | }); 81 | } 82 | } 83 | 84 | if (!json) { 85 | emitter.emit( 86 | 'error', 87 | new Error(`You need to provide a json property. 88 | Either a valid uri to the NFT JSON or the parsed NFT JSON.`), 89 | ); 90 | return; 91 | } 92 | 93 | // first fetch owner_properties if it's an URI 94 | if (owner_properties) { 95 | if ('string' === typeof owner_properties) { 96 | await fetch(IPFS.process(owner_properties)) 97 | .then((res) => res.json()) 98 | .then((_owner_properties) => (owner_properties = _owner_properties)) 99 | .catch((e) => { 100 | emitter.emit( 101 | 'warning', 102 | `Error while fetching owner_properties on ${owner_properties}. 103 | Setting owner_properties to default.`, 104 | ); 105 | owner_properties = {}; 106 | }); 107 | } 108 | } 109 | 110 | // get code from interactive_nft 111 | if (!code && json.interactive_nft) { 112 | if (json.interactive_nft.code) { 113 | code = json.interactive_nft.code; 114 | // if the code is in the interactive_nft property (not recommended) 115 | // we delete it because it might be a problem when we pass this object to the iframe 116 | // because we have to stringify it 117 | json.interactive_nft.code = null; 118 | } else if (json.interactive_nft.code_uri) { 119 | await fetch(IPFS.process(json.interactive_nft.code_uri)) 120 | .then((res) => res.text()) 121 | .then((_code) => (code = _code)) 122 | .catch((e) => { 123 | emitter.emit( 124 | 'Error', 125 | new Error( 126 | `Error while fetching ${json.interactive_nft.code_uri}`, 127 | ), 128 | ); 129 | }); 130 | } 131 | } 132 | 133 | if (!code) { 134 | emitter.emit( 135 | 'Error', 136 | new Error('You need to provide code for this NFT to run'), 137 | ); 138 | } 139 | 140 | this.json = json; 141 | this.code = code; 142 | this.owner_properties = owner_properties; 143 | this.owner = owner; 144 | }, 145 | 146 | build() { 147 | return this.replaceCode(srcdoc); 148 | }, 149 | 150 | makeDependencies() { 151 | if (!this.json.interactive_nft) { 152 | return ''; 153 | } 154 | return makeDependencies(this.json.interactive_nft.dependencies); 155 | }, 156 | 157 | loadProps() { 158 | const props = {}; 159 | if (this.json.interactive_nft) { 160 | if (Array.isArray(this.json.interactive_nft.properties)) { 161 | let overrider = {}; 162 | if ( 163 | this.owner_properties && 164 | 'object' === typeof this.owner_properties 165 | ) { 166 | overrider = this.owner_properties; 167 | } 168 | 169 | // no Object.assign because we only want declared props to be set 170 | for (const prop of this.json.interactive_nft.properties) { 171 | props[prop.name] = prop.value; 172 | if (undefined !== overrider[prop.name]) { 173 | props[prop.name] = overrider[prop.name]; 174 | } 175 | } 176 | } 177 | } 178 | 179 | return props; 180 | }, 181 | 182 | replaceCode(srcdoc) { 183 | let content = this.makeDependencies(); 184 | 185 | const props = this.loadProps(); 186 | 187 | content += scriptify(` 188 | // specific p5 because it's causing troubles. 189 | if (typeof p5 !== 'undefined' && p5.disableFriendlyErrors) { 190 | p5.disableFriendlyErrors = true; 191 | new p5(); 192 | } 193 | 194 | window.context = { 195 | get owner() { 196 | let owner = owner; 197 | if (window.location?.search) { 198 | const params = new URLSearchParams(window.location.search); 199 | owner = params.get('owner') || owner; 200 | } 201 | return owner; 202 | }, 203 | nft_json: JSON.parse(${JSON.stringify(JSON.stringify(this.json))}), 204 | properties: JSON.parse('${JSON.stringify(props)}'), 205 | }; 206 | `); 207 | 208 | content += this.code; 209 | 210 | return srcdoc.replace('', content); 211 | }, 212 | }; 213 | 214 | function noop() { } 215 | function run(fn) { 216 | return fn(); 217 | } 218 | function blank_object() { 219 | return Object.create(null); 220 | } 221 | function run_all(fns) { 222 | fns.forEach(run); 223 | } 224 | function is_function(thing) { 225 | return typeof thing === 'function'; 226 | } 227 | function safe_not_equal(a, b) { 228 | return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); 229 | } 230 | function is_empty(obj) { 231 | return Object.keys(obj).length === 0; 232 | } 233 | 234 | function append(target, node) { 235 | target.appendChild(node); 236 | } 237 | function insert(target, node, anchor) { 238 | target.insertBefore(node, anchor || null); 239 | } 240 | function detach(node) { 241 | node.parentNode.removeChild(node); 242 | } 243 | function element(name) { 244 | return document.createElement(name); 245 | } 246 | function text(data) { 247 | return document.createTextNode(data); 248 | } 249 | function space() { 250 | return text(' '); 251 | } 252 | function empty() { 253 | return text(''); 254 | } 255 | function attr(node, attribute, value) { 256 | if (value == null) 257 | node.removeAttribute(attribute); 258 | else if (node.getAttribute(attribute) !== value) 259 | node.setAttribute(attribute, value); 260 | } 261 | function children(element) { 262 | return Array.from(element.childNodes); 263 | } 264 | function toggle_class(element, name, toggle) { 265 | element.classList[toggle ? 'add' : 'remove'](name); 266 | } 267 | function custom_event(type, detail) { 268 | const e = document.createEvent('CustomEvent'); 269 | e.initCustomEvent(type, false, false, detail); 270 | return e; 271 | } 272 | 273 | let current_component; 274 | function set_current_component(component) { 275 | current_component = component; 276 | } 277 | function get_current_component() { 278 | if (!current_component) 279 | throw new Error('Function called outside component initialization'); 280 | return current_component; 281 | } 282 | function onMount(fn) { 283 | get_current_component().$$.on_mount.push(fn); 284 | } 285 | function createEventDispatcher() { 286 | const component = get_current_component(); 287 | return (type, detail) => { 288 | const callbacks = component.$$.callbacks[type]; 289 | if (callbacks) { 290 | // TODO are there situations where events could be dispatched 291 | // in a server (non-DOM) environment? 292 | const event = custom_event(type, detail); 293 | callbacks.slice().forEach(fn => { 294 | fn.call(component, event); 295 | }); 296 | } 297 | }; 298 | } 299 | // TODO figure out if we still want to support 300 | // shorthand events, or if we want to implement 301 | // a real bubbling mechanism 302 | function bubble(component, event) { 303 | const callbacks = component.$$.callbacks[event.type]; 304 | if (callbacks) { 305 | callbacks.slice().forEach(fn => fn(event)); 306 | } 307 | } 308 | 309 | const dirty_components = []; 310 | const binding_callbacks = []; 311 | const render_callbacks = []; 312 | const flush_callbacks = []; 313 | const resolved_promise = Promise.resolve(); 314 | let update_scheduled = false; 315 | function schedule_update() { 316 | if (!update_scheduled) { 317 | update_scheduled = true; 318 | resolved_promise.then(flush); 319 | } 320 | } 321 | function add_render_callback(fn) { 322 | render_callbacks.push(fn); 323 | } 324 | function add_flush_callback(fn) { 325 | flush_callbacks.push(fn); 326 | } 327 | let flushing = false; 328 | const seen_callbacks = new Set(); 329 | function flush() { 330 | if (flushing) 331 | return; 332 | flushing = true; 333 | do { 334 | // first, call beforeUpdate functions 335 | // and update components 336 | for (let i = 0; i < dirty_components.length; i += 1) { 337 | const component = dirty_components[i]; 338 | set_current_component(component); 339 | update(component.$$); 340 | } 341 | set_current_component(null); 342 | dirty_components.length = 0; 343 | while (binding_callbacks.length) 344 | binding_callbacks.pop()(); 345 | // then, once components are updated, call 346 | // afterUpdate functions. This may cause 347 | // subsequent updates... 348 | for (let i = 0; i < render_callbacks.length; i += 1) { 349 | const callback = render_callbacks[i]; 350 | if (!seen_callbacks.has(callback)) { 351 | // ...so guard against infinite loops 352 | seen_callbacks.add(callback); 353 | callback(); 354 | } 355 | } 356 | render_callbacks.length = 0; 357 | } while (dirty_components.length); 358 | while (flush_callbacks.length) { 359 | flush_callbacks.pop()(); 360 | } 361 | update_scheduled = false; 362 | flushing = false; 363 | seen_callbacks.clear(); 364 | } 365 | function update($$) { 366 | if ($$.fragment !== null) { 367 | $$.update(); 368 | run_all($$.before_update); 369 | const dirty = $$.dirty; 370 | $$.dirty = [-1]; 371 | $$.fragment && $$.fragment.p($$.ctx, dirty); 372 | $$.after_update.forEach(add_render_callback); 373 | } 374 | } 375 | const outroing = new Set(); 376 | let outros; 377 | function group_outros() { 378 | outros = { 379 | r: 0, 380 | c: [], 381 | p: outros // parent group 382 | }; 383 | } 384 | function check_outros() { 385 | if (!outros.r) { 386 | run_all(outros.c); 387 | } 388 | outros = outros.p; 389 | } 390 | function transition_in(block, local) { 391 | if (block && block.i) { 392 | outroing.delete(block); 393 | block.i(local); 394 | } 395 | } 396 | function transition_out(block, local, detach, callback) { 397 | if (block && block.o) { 398 | if (outroing.has(block)) 399 | return; 400 | outroing.add(block); 401 | outros.c.push(() => { 402 | outroing.delete(block); 403 | if (callback) { 404 | if (detach) 405 | block.d(1); 406 | callback(); 407 | } 408 | }); 409 | block.o(local); 410 | } 411 | } 412 | 413 | function bind(component, name, callback) { 414 | const index = component.$$.props[name]; 415 | if (index !== undefined) { 416 | component.$$.bound[index] = callback; 417 | callback(component.$$.ctx[index]); 418 | } 419 | } 420 | function create_component(block) { 421 | block && block.c(); 422 | } 423 | function mount_component(component, target, anchor, customElement) { 424 | const { fragment, on_mount, on_destroy, after_update } = component.$$; 425 | fragment && fragment.m(target, anchor); 426 | if (!customElement) { 427 | // onMount happens before the initial afterUpdate 428 | add_render_callback(() => { 429 | const new_on_destroy = on_mount.map(run).filter(is_function); 430 | if (on_destroy) { 431 | on_destroy.push(...new_on_destroy); 432 | } 433 | else { 434 | // Edge case - component was destroyed immediately, 435 | // most likely as a result of a binding initialising 436 | run_all(new_on_destroy); 437 | } 438 | component.$$.on_mount = []; 439 | }); 440 | } 441 | after_update.forEach(add_render_callback); 442 | } 443 | function destroy_component(component, detaching) { 444 | const $$ = component.$$; 445 | if ($$.fragment !== null) { 446 | run_all($$.on_destroy); 447 | $$.fragment && $$.fragment.d(detaching); 448 | // TODO null out other refs, including component.$$ (but need to 449 | // preserve final state?) 450 | $$.on_destroy = $$.fragment = null; 451 | $$.ctx = []; 452 | } 453 | } 454 | function make_dirty(component, i) { 455 | if (component.$$.dirty[0] === -1) { 456 | dirty_components.push(component); 457 | schedule_update(); 458 | component.$$.dirty.fill(0); 459 | } 460 | component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); 461 | } 462 | function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) { 463 | const parent_component = current_component; 464 | set_current_component(component); 465 | const $$ = component.$$ = { 466 | fragment: null, 467 | ctx: null, 468 | // state 469 | props, 470 | update: noop, 471 | not_equal, 472 | bound: blank_object(), 473 | // lifecycle 474 | on_mount: [], 475 | on_destroy: [], 476 | on_disconnect: [], 477 | before_update: [], 478 | after_update: [], 479 | context: new Map(parent_component ? parent_component.$$.context : []), 480 | // everything else 481 | callbacks: blank_object(), 482 | dirty, 483 | skip_bound: false 484 | }; 485 | let ready = false; 486 | $$.ctx = instance 487 | ? instance(component, options.props || {}, (i, ret, ...rest) => { 488 | const value = rest.length ? rest[0] : ret; 489 | if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { 490 | if (!$$.skip_bound && $$.bound[i]) 491 | $$.bound[i](value); 492 | if (ready) 493 | make_dirty(component, i); 494 | } 495 | return ret; 496 | }) 497 | : []; 498 | $$.update(); 499 | ready = true; 500 | run_all($$.before_update); 501 | // `false` as a special case of no DOM component 502 | $$.fragment = create_fragment ? create_fragment($$.ctx) : false; 503 | if (options.target) { 504 | if (options.hydrate) { 505 | const nodes = children(options.target); 506 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 507 | $$.fragment && $$.fragment.l(nodes); 508 | nodes.forEach(detach); 509 | } 510 | else { 511 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 512 | $$.fragment && $$.fragment.c(); 513 | } 514 | if (options.intro) 515 | transition_in(component.$$.fragment); 516 | mount_component(component, options.target, options.anchor, options.customElement); 517 | flush(); 518 | } 519 | set_current_component(parent_component); 520 | } 521 | /** 522 | * Base class for Svelte components. Used when dev=false. 523 | */ 524 | class SvelteComponent { 525 | $destroy() { 526 | destroy_component(this, 1); 527 | this.$destroy = noop; 528 | } 529 | $on(type, callback) { 530 | const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); 531 | callbacks.push(callback); 532 | return () => { 533 | const index = callbacks.indexOf(callback); 534 | if (index !== -1) 535 | callbacks.splice(index, 1); 536 | }; 537 | } 538 | $set($$props) { 539 | if (this.$$set && !is_empty($$props)) { 540 | this.$$.skip_bound = true; 541 | this.$$set($$props); 542 | this.$$.skip_bound = false; 543 | } 544 | } 545 | } 546 | 547 | let uid = 1; 548 | 549 | function handle_command_message(cmd_data) { 550 | let action = cmd_data.action; 551 | let id = cmd_data.cmd_id; 552 | let handler = this.pending_cmds.get(id); 553 | 554 | if (handler) { 555 | this.pending_cmds.delete(id); 556 | if (action === 'cmd_error') { 557 | let { message, stack } = cmd_data; 558 | let e = new Error(message); 559 | e.stack = stack; 560 | handler.reject(e); 561 | } 562 | 563 | if (action === 'cmd_ok') { 564 | handler.resolve(cmd_data.args || 'ok'); 565 | } 566 | } else { 567 | console.error('command not found', id, cmd_data, [ 568 | ...this.pending_cmds.keys(), 569 | ]); 570 | } 571 | } 572 | 573 | function handle_repl_message(event) { 574 | if (event.source !== this.iframe.contentWindow) return; 575 | const { action, args } = event.data; 576 | 577 | switch (action) { 578 | case 'cmd_error': 579 | case 'cmd_ok': 580 | return handle_command_message.call(this, event.data); 581 | case 'fetch_progress': 582 | return this.handlers.on_fetch_progress(args.remaining); 583 | case 'error': 584 | return this.handlers.on_error(event.data); 585 | case 'unhandledrejection': 586 | return this.handlers.on_unhandled_rejection(event.data); 587 | case 'console': 588 | return this.handlers.on_console(event.data); 589 | case 'console_group': 590 | return this.handlers.on_console_group(event.data); 591 | case 'console_group_collapsed': 592 | return this.handlers.on_console_group_collapsed(event.data); 593 | case 'console_group_end': 594 | return this.handlers.on_console_group_end(event.data); 595 | default: 596 | const handler = `on_${action}`; 597 | if ('function' === typeof this.handlers[handler]) { 598 | this.handlers[handler](event.data); 599 | } 600 | } 601 | } 602 | 603 | class Proxy { 604 | constructor(iframe, handlers) { 605 | this.iframe = iframe; 606 | this.handlers = handlers; 607 | 608 | this.pending_cmds = new Map(); 609 | 610 | this.handle_event = handle_repl_message.bind(this); 611 | window.addEventListener('message', this.handle_event, false); 612 | } 613 | 614 | destroy() { 615 | window.removeEventListener('message', this.handle_event); 616 | } 617 | 618 | iframe_command(action, args) { 619 | return new Promise((resolve, reject) => { 620 | const cmd_id = uid++; 621 | 622 | this.pending_cmds.set(cmd_id, { resolve, reject }); 623 | 624 | this.iframe.contentWindow.postMessage({ action, cmd_id, args }, '*'); 625 | }); 626 | } 627 | 628 | size() { 629 | return this.iframe_command('size'); 630 | } 631 | 632 | eval(script) { 633 | return this.iframe_command('eval', { script }); 634 | } 635 | 636 | add_script(script) { 637 | return this.iframe_command('add_script', script); 638 | } 639 | 640 | add_script_content(script) { 641 | return this.iframe_command('add_script_content', script); 642 | } 643 | 644 | add_style(style) { 645 | return this.iframe_command('add_style', style); 646 | } 647 | 648 | add_asset(asset) { 649 | return this.iframe_command('add_asset', asset); 650 | } 651 | 652 | handle_links() { 653 | return this.iframe_command('catch_clicks', {}); 654 | } 655 | } 656 | 657 | /* src/Output/Viewer.svelte generated by Svelte v3.35.0 */ 658 | 659 | function add_css() { 660 | var style = element("style"); 661 | style.id = "svelte-uaiew6-style"; 662 | style.textContent = ".beyondnft__sandbox.svelte-uaiew6{background-color:white;border:none;width:100%;height:100%;position:relative}iframe.svelte-uaiew6{min-width:100%;min-height:100%;border:none;display:block}.greyed-out.svelte-uaiew6{filter:grayscale(50%) blur(1px);opacity:0.25}.beyondnft__sandbox__error.svelte-uaiew6{font-size:0.9em;position:absolute;top:0;left:0;padding:5px}"; 663 | append(document.head, style); 664 | } 665 | 666 | // (120:2) {#if error} 667 | function create_if_block$1(ctx) { 668 | let strong; 669 | 670 | return { 671 | c() { 672 | strong = element("strong"); 673 | strong.innerHTML = `Sorry, an error occured while executing the NFT.`; 674 | attr(strong, "class", "beyondnft__sandbox__error svelte-uaiew6"); 675 | }, 676 | m(target, anchor) { 677 | insert(target, strong, anchor); 678 | }, 679 | d(detaching) { 680 | if (detaching) detach(strong); 681 | } 682 | }; 683 | } 684 | 685 | function create_fragment$1(ctx) { 686 | let div; 687 | let iframe_1; 688 | let iframe_1_sandbox_value; 689 | let t; 690 | let if_block = /*error*/ ctx[4] && create_if_block$1(); 691 | 692 | return { 693 | c() { 694 | div = element("div"); 695 | iframe_1 = element("iframe"); 696 | t = space(); 697 | if (if_block) if_block.c(); 698 | attr(iframe_1, "title", "Sandbox"); 699 | attr(iframe_1, "sandbox", iframe_1_sandbox_value = `allow-scripts allow-pointer-lock allow-popups allow-downloads ${/*sandbox_props*/ ctx[1]}`); 700 | attr(iframe_1, "allow", "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; camera; microphone; xr-spatial-tracking; geolocation; fullscreen; magnetometer; midi; vr;"); 701 | attr(iframe_1, "srcdoc", /*src*/ ctx[0]); 702 | attr(iframe_1, "class", "svelte-uaiew6"); 703 | toggle_class(iframe_1, "greyed-out", /*error*/ ctx[4] || pending || /*pending_imports*/ ctx[3]); 704 | attr(div, "class", "beyondnft__sandbox svelte-uaiew6"); 705 | }, 706 | m(target, anchor) { 707 | insert(target, div, anchor); 708 | append(div, iframe_1); 709 | /*iframe_1_binding*/ ctx[6](iframe_1); 710 | append(div, t); 711 | if (if_block) if_block.m(div, null); 712 | }, 713 | p(ctx, [dirty]) { 714 | if (dirty & /*sandbox_props*/ 2 && iframe_1_sandbox_value !== (iframe_1_sandbox_value = `allow-scripts allow-pointer-lock allow-popups allow-downloads ${/*sandbox_props*/ ctx[1]}`)) { 715 | attr(iframe_1, "sandbox", iframe_1_sandbox_value); 716 | } 717 | 718 | if (dirty & /*src*/ 1) { 719 | attr(iframe_1, "srcdoc", /*src*/ ctx[0]); 720 | } 721 | 722 | if (dirty & /*error, pending, pending_imports*/ 24) { 723 | toggle_class(iframe_1, "greyed-out", /*error*/ ctx[4] || pending || /*pending_imports*/ ctx[3]); 724 | } 725 | 726 | if (/*error*/ ctx[4]) { 727 | if (if_block) ; else { 728 | if_block = create_if_block$1(); 729 | if_block.c(); 730 | if_block.m(div, null); 731 | } 732 | } else if (if_block) { 733 | if_block.d(1); 734 | if_block = null; 735 | } 736 | }, 737 | i: noop, 738 | o: noop, 739 | d(detaching) { 740 | if (detaching) detach(div); 741 | /*iframe_1_binding*/ ctx[6](null); 742 | if (if_block) if_block.d(); 743 | } 744 | }; 745 | } 746 | 747 | let pending = false; 748 | 749 | function instance$1($$self, $$props, $$invalidate) { 750 | let { proxy } = $$props; 751 | let { src } = $$props; 752 | let { sandbox_props = "" } = $$props; 753 | const dispatch = createEventDispatcher(); 754 | let iframe; 755 | let pending_imports = 0; 756 | let error; 757 | let logs = []; 758 | let log_group_stack = []; 759 | let current_log_group = logs; 760 | let last_console_event; 761 | 762 | onMount(() => { 763 | $$invalidate(5, proxy = new Proxy(iframe, 764 | { 765 | on_fetch_progress: progress => { 766 | $$invalidate(3, pending_imports = progress); 767 | }, 768 | on_error: event => { 769 | push_logs({ level: "error", args: [event.value] }); 770 | show_error(event.value); 771 | }, 772 | on_unhandled_rejection: event => { 773 | let error = event.value; 774 | if (typeof error === "string") error = { message: error }; 775 | error.message = "Uncaught (in promise): " + error.message; 776 | push_logs({ level: "error", args: [error] }); 777 | show_error(error); 778 | }, 779 | on_console: log => { 780 | if (log.level === "clear") { 781 | clear_logs(); 782 | push_logs(log); 783 | } else if (log.duplicate) { 784 | increment_duplicate_log(); 785 | } else { 786 | push_logs(log); 787 | } 788 | }, 789 | on_console_group: action => { 790 | group_logs(action.label, false); 791 | }, 792 | on_console_group_end: () => { 793 | ungroup_logs(); 794 | }, 795 | on_console_group_collapsed: action => { 796 | group_logs(action.label, true); 797 | } 798 | })); 799 | 800 | iframe.addEventListener("load", () => { 801 | proxy.handle_links(); 802 | !error && dispatch("loaded"); 803 | }); 804 | 805 | return () => { 806 | proxy.destroy(); 807 | }; 808 | }); 809 | 810 | function show_error(e) { 811 | $$invalidate(4, error = e); 812 | dispatch("error", e); 813 | } 814 | 815 | function push_logs(log) { 816 | current_log_group.push(last_console_event = log); 817 | logs = logs; 818 | } 819 | 820 | function group_logs(label, collapsed) { 821 | const group_log = { 822 | level: "group", 823 | label, 824 | collapsed, 825 | logs: [] 826 | }; 827 | 828 | current_log_group.push(group_log); 829 | log_group_stack.push(current_log_group); 830 | current_log_group = group_log.logs; 831 | logs = logs; 832 | } 833 | 834 | function ungroup_logs() { 835 | current_log_group = log_group_stack.pop(); 836 | } 837 | 838 | function increment_duplicate_log() { 839 | const last_log = current_log_group[current_log_group.length - 1]; 840 | 841 | if (last_log) { 842 | last_log.count = (last_log.count || 1) + 1; 843 | logs = logs; 844 | } else { 845 | last_console_event.count = 1; 846 | push_logs(last_console_event); 847 | } 848 | } 849 | 850 | function clear_logs() { 851 | current_log_group = logs = []; 852 | } 853 | 854 | function iframe_1_binding($$value) { 855 | binding_callbacks[$$value ? "unshift" : "push"](() => { 856 | iframe = $$value; 857 | $$invalidate(2, iframe); 858 | }); 859 | } 860 | 861 | $$self.$$set = $$props => { 862 | if ("proxy" in $$props) $$invalidate(5, proxy = $$props.proxy); 863 | if ("src" in $$props) $$invalidate(0, src = $$props.src); 864 | if ("sandbox_props" in $$props) $$invalidate(1, sandbox_props = $$props.sandbox_props); 865 | }; 866 | 867 | return [src, sandbox_props, iframe, pending_imports, error, proxy, iframe_1_binding]; 868 | } 869 | 870 | class Viewer extends SvelteComponent { 871 | constructor(options) { 872 | super(); 873 | if (!document.getElementById("svelte-uaiew6-style")) add_css(); 874 | init(this, options, instance$1, create_fragment$1, safe_not_equal, { proxy: 5, src: 0, sandbox_props: 1 }); 875 | } 876 | } 877 | 878 | /* src/Sandbox.svelte generated by Svelte v3.35.0 */ 879 | 880 | function create_else_block(ctx) { 881 | let t; 882 | 883 | return { 884 | c() { 885 | t = text("Loading..."); 886 | }, 887 | m(target, anchor) { 888 | insert(target, t, anchor); 889 | }, 890 | p: noop, 891 | i: noop, 892 | o: noop, 893 | d(detaching) { 894 | if (detaching) detach(t); 895 | } 896 | }; 897 | } 898 | 899 | // (38:0) {#if ready} 900 | function create_if_block(ctx) { 901 | let viewer; 902 | let updating_proxy; 903 | let current; 904 | 905 | function viewer_proxy_binding(value) { 906 | /*viewer_proxy_binding*/ ctx[11](value); 907 | } 908 | 909 | let viewer_props = { 910 | src: Builder.build(), 911 | sandbox_props: /*sandbox_props*/ ctx[0] 912 | }; 913 | 914 | if (/*proxy*/ ctx[1] !== void 0) { 915 | viewer_props.proxy = /*proxy*/ ctx[1]; 916 | } 917 | 918 | viewer = new Viewer({ props: viewer_props }); 919 | binding_callbacks.push(() => bind(viewer, "proxy", viewer_proxy_binding)); 920 | viewer.$on("loaded", /*loaded_handler*/ ctx[12]); 921 | viewer.$on("error", /*error_handler*/ ctx[13]); 922 | viewer.$on("warning", /*warning_handler*/ ctx[14]); 923 | 924 | return { 925 | c() { 926 | create_component(viewer.$$.fragment); 927 | }, 928 | m(target, anchor) { 929 | mount_component(viewer, target, anchor); 930 | current = true; 931 | }, 932 | p(ctx, dirty) { 933 | const viewer_changes = {}; 934 | if (dirty & /*sandbox_props*/ 1) viewer_changes.sandbox_props = /*sandbox_props*/ ctx[0]; 935 | 936 | if (!updating_proxy && dirty & /*proxy*/ 2) { 937 | updating_proxy = true; 938 | viewer_changes.proxy = /*proxy*/ ctx[1]; 939 | add_flush_callback(() => updating_proxy = false); 940 | } 941 | 942 | viewer.$set(viewer_changes); 943 | }, 944 | i(local) { 945 | if (current) return; 946 | transition_in(viewer.$$.fragment, local); 947 | current = true; 948 | }, 949 | o(local) { 950 | transition_out(viewer.$$.fragment, local); 951 | current = false; 952 | }, 953 | d(detaching) { 954 | destroy_component(viewer, detaching); 955 | } 956 | }; 957 | } 958 | 959 | function create_fragment(ctx) { 960 | let current_block_type_index; 961 | let if_block; 962 | let if_block_anchor; 963 | let current; 964 | const if_block_creators = [create_if_block, create_else_block]; 965 | const if_blocks = []; 966 | 967 | function select_block_type(ctx, dirty) { 968 | if (/*ready*/ ctx[2]) return 0; 969 | return 1; 970 | } 971 | 972 | current_block_type_index = select_block_type(ctx); 973 | if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); 974 | 975 | return { 976 | c() { 977 | if_block.c(); 978 | if_block_anchor = empty(); 979 | }, 980 | m(target, anchor) { 981 | if_blocks[current_block_type_index].m(target, anchor); 982 | insert(target, if_block_anchor, anchor); 983 | current = true; 984 | }, 985 | p(ctx, [dirty]) { 986 | let previous_block_index = current_block_type_index; 987 | current_block_type_index = select_block_type(ctx); 988 | 989 | if (current_block_type_index === previous_block_index) { 990 | if_blocks[current_block_type_index].p(ctx, dirty); 991 | } else { 992 | group_outros(); 993 | 994 | transition_out(if_blocks[previous_block_index], 1, 1, () => { 995 | if_blocks[previous_block_index] = null; 996 | }); 997 | 998 | check_outros(); 999 | if_block = if_blocks[current_block_type_index]; 1000 | 1001 | if (!if_block) { 1002 | if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); 1003 | if_block.c(); 1004 | } else { 1005 | if_block.p(ctx, dirty); 1006 | } 1007 | 1008 | transition_in(if_block, 1); 1009 | if_block.m(if_block_anchor.parentNode, if_block_anchor); 1010 | } 1011 | }, 1012 | i(local) { 1013 | if (current) return; 1014 | transition_in(if_block); 1015 | current = true; 1016 | }, 1017 | o(local) { 1018 | transition_out(if_block); 1019 | current = false; 1020 | }, 1021 | d(detaching) { 1022 | if_blocks[current_block_type_index].d(detaching); 1023 | if (detaching) detach(if_block_anchor); 1024 | } 1025 | }; 1026 | } 1027 | 1028 | function getBuilder() { 1029 | return builder; 1030 | } 1031 | 1032 | function instance($$self, $$props, $$invalidate) { 1033 | const dispatch = createEventDispatcher(); 1034 | let { data = {} } = $$props; 1035 | let { code = "" } = $$props; 1036 | let { owner_properties = {} } = $$props; 1037 | let { owner = "0x0000000000000000000000000000000000000000" } = $$props; 1038 | let { sandbox_props = "" } = $$props; 1039 | let { ipfsGateway = "https://gateway.ipfs.io/ipfs/" } = $$props; 1040 | const version = "0.0.16"; 1041 | let proxy = null; 1042 | let ready = false; 1043 | 1044 | function getProxy() { 1045 | return proxy; 1046 | } 1047 | 1048 | Builder.emitter.on("warning", e => dispatch("warning", e.detail)); 1049 | Builder.emitter.on("error", e => dispatch("error", e.detail)); 1050 | 1051 | onMount(async () => { 1052 | await Builder.init(data, code, owner_properties, owner, ipfsGateway, fetch); 1053 | $$invalidate(2, ready = true); 1054 | }); 1055 | 1056 | function viewer_proxy_binding(value) { 1057 | proxy = value; 1058 | $$invalidate(1, proxy); 1059 | } 1060 | 1061 | function loaded_handler(event) { 1062 | bubble($$self, event); 1063 | } 1064 | 1065 | function error_handler(event) { 1066 | bubble($$self, event); 1067 | } 1068 | 1069 | function warning_handler(event) { 1070 | bubble($$self, event); 1071 | } 1072 | 1073 | $$self.$$set = $$props => { 1074 | if ("data" in $$props) $$invalidate(3, data = $$props.data); 1075 | if ("code" in $$props) $$invalidate(4, code = $$props.code); 1076 | if ("owner_properties" in $$props) $$invalidate(5, owner_properties = $$props.owner_properties); 1077 | if ("owner" in $$props) $$invalidate(6, owner = $$props.owner); 1078 | if ("sandbox_props" in $$props) $$invalidate(0, sandbox_props = $$props.sandbox_props); 1079 | if ("ipfsGateway" in $$props) $$invalidate(7, ipfsGateway = $$props.ipfsGateway); 1080 | }; 1081 | 1082 | return [ 1083 | sandbox_props, 1084 | proxy, 1085 | ready, 1086 | data, 1087 | code, 1088 | owner_properties, 1089 | owner, 1090 | ipfsGateway, 1091 | version, 1092 | getProxy, 1093 | getBuilder, 1094 | viewer_proxy_binding, 1095 | loaded_handler, 1096 | error_handler, 1097 | warning_handler 1098 | ]; 1099 | } 1100 | 1101 | class Sandbox extends SvelteComponent { 1102 | constructor(options) { 1103 | super(); 1104 | 1105 | init(this, options, instance, create_fragment, safe_not_equal, { 1106 | data: 3, 1107 | code: 4, 1108 | owner_properties: 5, 1109 | owner: 6, 1110 | sandbox_props: 0, 1111 | ipfsGateway: 7, 1112 | version: 8, 1113 | getProxy: 9, 1114 | getBuilder: 10 1115 | }); 1116 | } 1117 | 1118 | get version() { 1119 | return this.$$.ctx[8]; 1120 | } 1121 | 1122 | get getProxy() { 1123 | return this.$$.ctx[9]; 1124 | } 1125 | 1126 | get getBuilder() { 1127 | return getBuilder; 1128 | } 1129 | } 1130 | 1131 | Sandbox.Builder = Builder; 1132 | 1133 | return Sandbox; 1134 | 1135 | }))); 1136 | -------------------------------------------------------------------------------- /dist/nftsandbox.umd.min.js: -------------------------------------------------------------------------------- 1 | !function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(n="undefined"!=typeof globalThis?globalThis:n||self).Sandbox=e()}(this,(function(){"use strict";let n="";var e={init(e){n=e},process:e=>e.replace("ipfs://ipfs/","ipfs://").replace("ipfs://",n)};const t={all:o=o||new Map,on:function(n,e){var t=o.get(n);t&&t.push(e)||o.set(n,[e])},off:function(n,e){var t=o.get(n);t&&t.splice(t.indexOf(e)>>>0,1)},emit:function(n,e){(o.get(n)||[]).slice().map((function(n){n(e)})),(o.get("*")||[]).slice().map((function(t){t(n,e)}))}};var o,r={emitter:t,json:{},code:"",owner_properties:{},owner:"",async init(n,o,r,s,i,a){if(i&&e.init(i),"string"==typeof n)try{n=JSON.parse(n)}catch(o){n=e.process(n),await a(n).then((n=>n.json())).then((e=>n=e)).catch((e=>{t.emit("warning",new Error(`Error while fetching NFT's JSON at ${n}`)),n=null}))}n?(r&&"string"==typeof r&&await a(e.process(r)).then((n=>n.json())).then((n=>r=n)).catch((n=>{t.emit("warning",`Error while fetching owner_properties on ${r}.\n\t\t\t\t\t\tSetting owner_properties to default.`),r={}})),!o&&n.interactive_nft&&(n.interactive_nft.code?(o=n.interactive_nft.code,n.interactive_nft.code=null):n.interactive_nft.code_uri&&await a(e.process(n.interactive_nft.code_uri)).then((n=>n.text())).then((n=>o=n)).catch((e=>{t.emit("Error",new Error(`Error while fetching ${n.interactive_nft.code_uri}`))}))),o||t.emit("Error",new Error("You need to provide code for this NFT to run")),this.json=n,this.code=o,this.owner_properties=r,this.owner=s):t.emit("error",new Error("You need to provide a json property.\n\t\t\tEither a valid uri to the NFT JSON or the parsed NFT JSON."))},build(){return this.replaceCode("\n\n \n \n\n 64 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BeyondNFT - Sandbox Example 7 | 17 | 18 | 19 | 20 |
21 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /public/nft_json.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NFT #1", 3 | "description": "yes.", 4 | "attributes": { 5 | "processing": "p5js" 6 | }, 7 | "interactive_nft": { 8 | "code_uri": "./code.html", 9 | "dependencies": [{ 10 | "url": "https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js", 11 | "type": "script" 12 | }], 13 | "properties": [{ 14 | "name": "color", 15 | "type": "string", 16 | "value": "#000000" 17 | }] 18 | } 19 | } -------------------------------------------------------------------------------- /public/owner_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": "blue" 3 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import replace from '@rollup/plugin-replace'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | import pkg from './package.json'; 8 | 9 | const name = pkg.name 10 | .replace(/^(@\S+\/)?(beyondnft)?(\S+)/, '$3') 11 | .replace(/^\w/, (m) => m.toUpperCase()) 12 | .replace(/-\w/g, (m) => m[1].toUpperCase()); 13 | 14 | const production = !process.env.ROLLUP_WATCH; 15 | 16 | function serve() { 17 | let server; 18 | 19 | function toExit() { 20 | if (server) server.kill(0); 21 | } 22 | 23 | return { 24 | writeBundle() { 25 | if (server) return; 26 | server = require('child_process').spawn( 27 | 'npm', 28 | ['run', 'start', '--', '--dev'], 29 | { 30 | stdio: ['ignore', 'inherit', 'inherit'], 31 | shell: true, 32 | } 33 | ); 34 | 35 | process.on('SIGTERM', toExit); 36 | process.on('exit', toExit); 37 | }, 38 | }; 39 | } 40 | const output = [ 41 | { file: pkg.module.replace('.min.js', '.js'), format: 'es' }, 42 | { file: pkg.main.replace('.min.js', '.js'), format: 'umd', name }, 43 | ]; 44 | 45 | if (!production) { 46 | output.push({ 47 | file: 'public/' + pkg.module.replace('.min.js', '.js'), 48 | format: 'es', 49 | }); 50 | } else { 51 | output.push( 52 | { 53 | file: pkg.module, 54 | format: 'es', 55 | plugins: [terser()], 56 | }, 57 | { 58 | file: pkg.main, 59 | format: 'umd', 60 | name, 61 | plugins: [terser()], 62 | } 63 | ); 64 | } 65 | export default { 66 | input: 'src/main.js', 67 | output, 68 | plugins: [ 69 | replace({ 70 | 'process.env.NODE_ENV': JSON.stringify( 71 | production ? 'production' : 'development' 72 | ), 73 | 'process.env.npm_package_version': JSON.stringify( 74 | process.env.npm_package_version 75 | ), 76 | }), 77 | svelte({ 78 | dev: !production, 79 | emitCss: false, 80 | }), 81 | resolve({ 82 | browser: true, 83 | dedupe: ['svelte'], 84 | }), 85 | commonjs(), 86 | 87 | // In dev mode, call `npm run start` once 88 | // the bundle has been generated 89 | !production && serve(), 90 | 91 | // Watch the `public` directory and refresh the 92 | // browser on changes when not in production 93 | !production && livereload('public'), 94 | ], 95 | watch: { 96 | clearScreen: false, 97 | }, 98 | }; 99 | -------------------------------------------------------------------------------- /scripts/build-srcdoc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | let css = ''; 4 | try { 5 | css = fs.readFileSync('src/Builder/srcdoc/styles.css', 'utf-8'); 6 | } catch (e) {} 7 | const html = fs.readFileSync('src/Builder/srcdoc/index.html', 'utf-8'); 8 | 9 | fs.writeFileSync( 10 | 'src/Builder/srcdoc/index.js', 11 | `export default ${JSON.stringify(html.replace('/* STYLES */', css))};`, 12 | ); 13 | -------------------------------------------------------------------------------- /src/Builder/index.js: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | import IPFS from '../conf/link'; 3 | 4 | import * as utils from './utils.js'; 5 | import srcdoc from './srcdoc/index.js'; 6 | 7 | const emitter = mitt(); 8 | 9 | export default { 10 | emitter, 11 | json: {}, 12 | code: '', 13 | owner_properties: {}, 14 | owner: '', 15 | async init(json, code, owner_properties, owner, ipfsGateway, fetch) { 16 | if (ipfsGateway) { 17 | IPFS.init(ipfsGateway); 18 | } 19 | 20 | if ('string' === typeof json) { 21 | try { 22 | json = JSON.parse(json); 23 | } catch (e) { 24 | json = IPFS.process(json); 25 | await fetch(json) 26 | .then((res) => res.json()) 27 | .then((_data) => (json = _data)) 28 | .catch((e) => { 29 | emitter.emit( 30 | 'warning', 31 | new Error(`Error while fetching NFT's JSON at ${json}`), 32 | ); 33 | json = null; 34 | }); 35 | } 36 | } 37 | 38 | if (!json) { 39 | emitter.emit( 40 | 'error', 41 | new Error(`You need to provide a json property. 42 | Either a valid uri to the NFT JSON or the parsed NFT JSON.`), 43 | ); 44 | return; 45 | } 46 | 47 | // first fetch owner_properties if it's an URI 48 | if (owner_properties) { 49 | if ('string' === typeof owner_properties) { 50 | await fetch(IPFS.process(owner_properties)) 51 | .then((res) => res.json()) 52 | .then((_owner_properties) => (owner_properties = _owner_properties)) 53 | .catch((e) => { 54 | emitter.emit( 55 | 'warning', 56 | `Error while fetching owner_properties on ${owner_properties}. 57 | Setting owner_properties to default.`, 58 | ); 59 | owner_properties = {}; 60 | }); 61 | } 62 | } 63 | 64 | // get code from interactive_nft 65 | if (!code && json.interactive_nft) { 66 | if (json.interactive_nft.code) { 67 | code = json.interactive_nft.code; 68 | // if the code is in the interactive_nft property (not recommended) 69 | // we delete it because it might be a problem when we pass this object to the iframe 70 | // because we have to stringify it 71 | json.interactive_nft.code = null; 72 | } else if (json.interactive_nft.code_uri) { 73 | await fetch(IPFS.process(json.interactive_nft.code_uri)) 74 | .then((res) => res.text()) 75 | .then((_code) => (code = _code)) 76 | .catch((e) => { 77 | emitter.emit( 78 | 'Error', 79 | new Error( 80 | `Error while fetching ${json.interactive_nft.code_uri}`, 81 | ), 82 | ); 83 | }); 84 | } 85 | } 86 | 87 | if (!code) { 88 | emitter.emit( 89 | 'Error', 90 | new Error('You need to provide code for this NFT to run'), 91 | ); 92 | } 93 | 94 | this.json = json; 95 | this.code = code; 96 | this.owner_properties = owner_properties; 97 | this.owner = owner; 98 | }, 99 | 100 | build() { 101 | return this.replaceCode(srcdoc); 102 | }, 103 | 104 | makeDependencies() { 105 | if (!this.json.interactive_nft) { 106 | return ''; 107 | } 108 | return utils.makeDependencies(this.json.interactive_nft.dependencies); 109 | }, 110 | 111 | loadProps() { 112 | const props = {}; 113 | if (this.json.interactive_nft) { 114 | if (Array.isArray(this.json.interactive_nft.properties)) { 115 | let overrider = {}; 116 | if ( 117 | this.owner_properties && 118 | 'object' === typeof this.owner_properties 119 | ) { 120 | overrider = this.owner_properties; 121 | } 122 | 123 | // no Object.assign because we only want declared props to be set 124 | for (const prop of this.json.interactive_nft.properties) { 125 | props[prop.name] = prop.value; 126 | if (undefined !== overrider[prop.name]) { 127 | props[prop.name] = overrider[prop.name]; 128 | } 129 | } 130 | } 131 | } 132 | 133 | return props; 134 | }, 135 | 136 | replaceCode(srcdoc) { 137 | let content = this.makeDependencies(); 138 | 139 | const props = this.loadProps(); 140 | 141 | content += utils.scriptify(` 142 | // specific p5 because it's causing troubles. 143 | if (typeof p5 !== 'undefined' && p5.disableFriendlyErrors) { 144 | p5.disableFriendlyErrors = true; 145 | new p5(); 146 | } 147 | 148 | window.context = { 149 | get owner() { 150 | let owner = owner; 151 | if (window.location?.search) { 152 | const params = new URLSearchParams(window.location.search); 153 | owner = params.get('owner') || owner; 154 | } 155 | return owner; 156 | }, 157 | nft_json: JSON.parse(${JSON.stringify(JSON.stringify(this.json))}), 158 | properties: JSON.parse('${JSON.stringify(props)}'), 159 | }; 160 | `); 161 | 162 | content += this.code; 163 | 164 | return srcdoc.replace('', content); 165 | }, 166 | }; 167 | -------------------------------------------------------------------------------- /src/Builder/srcdoc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 313 | 328 | 329 | 330 | 331 | 332 | 333 | -------------------------------------------------------------------------------- /src/Builder/srcdoc/index.js: -------------------------------------------------------------------------------- 1 | export default "\n\n \n \n\n \n \n \n \n \n \n\n"; -------------------------------------------------------------------------------- /src/Builder/utils.js: -------------------------------------------------------------------------------- 1 | export function makeDependencies(dependencies) { 2 | let result = ''; 3 | if (Array.isArray(dependencies)) { 4 | for (const dependency of dependencies) { 5 | const type = dependency.type; 6 | if (type === 'script') { 7 | result += ``; 8 | } else if (type === 'style') { 9 | result += ``; 17 | } else { 18 | console.log(`Unknown dependency type ${type}`); 19 | } 20 | } 21 | } 22 | 23 | return result; 24 | } 25 | 26 | export function scriptify(script) { 27 | return ``; 28 | } 29 | -------------------------------------------------------------------------------- /src/Output/Proxy.js: -------------------------------------------------------------------------------- 1 | let uid = 1; 2 | 3 | function handle_command_message(cmd_data) { 4 | let action = cmd_data.action; 5 | let id = cmd_data.cmd_id; 6 | let handler = this.pending_cmds.get(id); 7 | 8 | if (handler) { 9 | this.pending_cmds.delete(id); 10 | if (action === 'cmd_error') { 11 | let { message, stack } = cmd_data; 12 | let e = new Error(message); 13 | e.stack = stack; 14 | handler.reject(e); 15 | } 16 | 17 | if (action === 'cmd_ok') { 18 | handler.resolve(cmd_data.args || 'ok'); 19 | } 20 | } else { 21 | console.error('command not found', id, cmd_data, [ 22 | ...this.pending_cmds.keys(), 23 | ]); 24 | } 25 | } 26 | 27 | function handle_repl_message(event) { 28 | if (event.source !== this.iframe.contentWindow) return; 29 | const { action, args } = event.data; 30 | 31 | switch (action) { 32 | case 'cmd_error': 33 | case 'cmd_ok': 34 | return handle_command_message.call(this, event.data); 35 | case 'fetch_progress': 36 | return this.handlers.on_fetch_progress(args.remaining); 37 | case 'error': 38 | return this.handlers.on_error(event.data); 39 | case 'unhandledrejection': 40 | return this.handlers.on_unhandled_rejection(event.data); 41 | case 'console': 42 | return this.handlers.on_console(event.data); 43 | case 'console_group': 44 | return this.handlers.on_console_group(event.data); 45 | case 'console_group_collapsed': 46 | return this.handlers.on_console_group_collapsed(event.data); 47 | case 'console_group_end': 48 | return this.handlers.on_console_group_end(event.data); 49 | default: 50 | const handler = `on_${action}`; 51 | if ('function' === typeof this.handlers[handler]) { 52 | this.handlers[handler](event.data); 53 | } 54 | } 55 | } 56 | 57 | export default class Proxy { 58 | constructor(iframe, handlers) { 59 | this.iframe = iframe; 60 | this.handlers = handlers; 61 | 62 | this.pending_cmds = new Map(); 63 | 64 | this.handle_event = handle_repl_message.bind(this); 65 | window.addEventListener('message', this.handle_event, false); 66 | } 67 | 68 | destroy() { 69 | window.removeEventListener('message', this.handle_event); 70 | } 71 | 72 | iframe_command(action, args) { 73 | return new Promise((resolve, reject) => { 74 | const cmd_id = uid++; 75 | 76 | this.pending_cmds.set(cmd_id, { resolve, reject }); 77 | 78 | this.iframe.contentWindow.postMessage({ action, cmd_id, args }, '*'); 79 | }); 80 | } 81 | 82 | size() { 83 | return this.iframe_command('size'); 84 | } 85 | 86 | eval(script) { 87 | return this.iframe_command('eval', { script }); 88 | } 89 | 90 | add_script(script) { 91 | return this.iframe_command('add_script', script); 92 | } 93 | 94 | add_script_content(script) { 95 | return this.iframe_command('add_script_content', script); 96 | } 97 | 98 | add_style(style) { 99 | return this.iframe_command('add_style', style); 100 | } 101 | 102 | add_asset(asset) { 103 | return this.iframe_command('add_asset', asset); 104 | } 105 | 106 | handle_links() { 107 | return this.iframe_command('catch_clicks', {}); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Output/Viewer.svelte: -------------------------------------------------------------------------------- 1 | 110 | 111 |
112 |