diff --git a/404.html b/404.html new file mode 100644 index 0000000..f8414f0 --- /dev/null +++ b/404.html @@ -0,0 +1,3 @@ + +404 Not Found +

404 Not Found

diff --git a/css/dark.css b/css/dark.css new file mode 100644 index 0000000..54615aa --- /dev/null +++ b/css/dark.css @@ -0,0 +1,181 @@ +body { + color: white; + background-color: #202124; +} + +::-moz-selection { + background: blue; + color: #fff; + text-shadow: none; +} + +::selection { + background: red; + color: #fff; + text-shadow: none; +} + +hr { + border-top: 3px dotted blue; +} + +code { + background-color: lightblue; + color: black; + text-decoration: bold; + padding: 0.1em 0.2em; +} + +pre { + background-color: #272822; + line-height: 1.4; + overflow-x: auto; + padding: 1em; +} + +blockquote { + border-color: blue; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #ddd; +} + +h1::before { + color: var(--darkMaincolor); + content: "# "; +} + +h2::before { + color: var(--darkMaincolor); + content: "## "; +} + +h3::before { + color: var(--darkMaincolor); + content: "### "; +} + +h4::before { + color: var(--darkMaincolor); + content: "#### "; +} + +h5::before { + color: var(--darkMaincolor); + content: "##### "; +} + +h6::before { + color: var(--darkMaincolor); + content: "###### "; +} + +a { + border-bottom: 3px solid var(--darkMaincolor); + color: inherit; +} + +a:hover { + background-color: var(--darkMaincolor); + color: black; +} + +.site-description a { + color: #ddd; +} + +.site-description a:hover { + color: black; +} + +.tags a { + border-bottom: 3px solid var(--darkMaincolor); +} + +.tags a:hover { + background-color: var(--darkMaincolor); + color: black; +} + +.site-title a { + color: white; + text-decoration: none !important; +} + +.header nav, +.footer { + border-color: #333; +} + +.highlight { + background-color: #333; +} + +.soc:hover { + color: black; +} + +.draft-label { + color: var(--darkMaincolor); + background-color: blue; +} + +.highlight pre code[class=language-javaScript]::before, +.highlight pre code[class="language-js"]::before { + content: "js"; + background: #f7df1e; + color: black; +} + +.highlight pre code[class*='language-yml']::before, +.highlight pre code[class*='language-yaml']::before { + content: 'yaml'; + background: #f71e6a; + color: white; +} + +.highlight pre code[class*='language-shell']::before, +.highlight pre code[class*='language-bash']::before, +.highlight pre code[class*='language-sh']::before { + content: 'shell'; + background: green; + color: white +} + +.highlight pre code[class*='language-json']::before { + content: 'json'; + background: dodgerblue; + color: #000000 +} + +.highlight pre code[class*='language-python']::before, +.highlight pre code[class*='language-py']::before { + content: 'py'; + background: blue; + color: yellow; +} + +.highlight pre code[class*='language-css']::before { + content: 'css'; + background: cyan; + color: black; +} + +.highlight pre code[class*='language-go']::before { + content: 'Go'; + background: cyan; + color: royalblue; +} + +.highlight pre code[class*='language-md']::before, +.highlight pre code[class*='language-md']::before { + content: 'Markdown'; + background: royalblue; + color: whitesmoke; +} \ No newline at end of file diff --git a/css/fonts.css b/css/fonts.css new file mode 100644 index 0000000..156ee63 --- /dev/null +++ b/css/fonts.css @@ -0,0 +1,38 @@ +/* fira-sans-regular - latin */ +@font-face { + font-family: 'Fira Sans'; + font-style: normal; + font-weight: 400; + src: url('../fonts/fira-sans-v10-latin-regular.eot'); /* IE9 Compat Modes */ + src: local('Fira Sans Regular'), local('FiraSans-Regular'), + url('../fonts/fira-sans-v10-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/fira-sans-v10-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('../fonts/fira-sans-v10-latin-regular.woff') format('woff'), /* Modern Browsers */ + url('../fonts/fira-sans-v10-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ + url('../fonts/fira-sans-v10-latin-regular.svg#FiraSans') format('svg'); /* Legacy iOS */ +} +/* roboto-mono-regular - latin */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + src: url('../fonts/roboto-mono-v12-latin-regular.eot'); /* IE9 Compat Modes */ + src: url('../fonts/roboto-mono-v12-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/roboto-mono-v12-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('../fonts/roboto-mono-v12-latin-regular.woff') format('woff'), /* Modern Browsers */ + url('../fonts/roboto-mono-v12-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ + url('../fonts/roboto-mono-v12-latin-regular.svg#RobotoMono') format('svg'); /* Legacy iOS */ +} +/* ibm-plex-mono-500italic - latin */ +@font-face { + font-family: 'IBM Plex Mono'; + font-style: italic; + font-weight: 500; + src: url('../fonts/ibm-plex-mono-v6-latin-500italic.eot'); /* IE9 Compat Modes */ + src: local('IBM Plex Mono Medium Italic'), local('IBMPlexMono-MediumItalic'), + url('../fonts/ibm-plex-mono-v6-latin-500italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/ibm-plex-mono-v6-latin-500italic.woff2') format('woff2'), /* Super Modern Browsers */ + url('../fonts/ibm-plex-mono-v6-latin-500italic.woff') format('woff'), /* Modern Browsers */ + url('../fonts/ibm-plex-mono-v6-latin-500italic.ttf') format('truetype'), /* Safari, Android, iOS */ + url('../fonts/ibm-plex-mono-v6-latin-500italic.svg#IBMPlexMono') format('svg'); /* Legacy iOS */ +} diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..76d0647 --- /dev/null +++ b/css/main.css @@ -0,0 +1,430 @@ +/* Markdown */ +:root { + --maincolor: red; + --bordercl: rebeccapurple; + --callouctcolor: dodgerblue; + --hovercolor: navy; + --darkMaincolor: #50fa7b; +} + +html { + color: #232333; + font-family: "Roboto Mono", monospace; + font-size: 15px; + line-height: 1.6em; +} + +body { + display: block; + margin: 8px; +} + +* { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +@keyframes intro { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.content { + animation: intro 0.3s both; + animation-delay: 0.15s; +} + +::selection { + background: var(--maincolor); + color: #fff; +} + +p { + /* font-family: 'Fira Sans', sans-serif; */ + line-height: 1.5; +} + +hr { + border: 0; + border-top: 3px dotted var(--bordercl); + margin: 1em 0; +} + +blockquote { + border-left: 3px solid var(--bordercl); + color: #737373; + margin: 0; + padding-left: 1em; +} + +a { + border-bottom: 3px solid var(--maincolor); + color: inherit; + text-decoration: none; +} + +a:hover { + background-color: var(--hovercolor); + color: #fff; +} + +ul { + list-style: none; + padding-left: 2ch; +} + +ul li { + text-indent: -2ch; +} + +ul > li::before { + content: "* "; + font-weight: bold; +} + +/* Images */ +img { + border: 3px solid #ececec; + max-width: 100%; +} + +figure { + box-sizing: border-box; + display: inline-block; + margin: 0; + max-width: 100%; +} + +figure img { + max-height: 500px; +} + +@media screen and (min-width: 600px) { + figure { + padding: 0 40px; + } +} + +figure h4 { + font-size: 1rem; + margin: 0; + margin-bottom: 1em; +} + +figure h4::before { + content: "↳ "; +} + +/* Code blocks */ +code { + background-color: #f1f1f1; + padding: 0.1em 0.2em; +} + +pre { + background-color: #ececec; + line-height: 1.4; + overflow-x: auto; + padding: 1em; +} + +.highlight pre ::selection { + background: rgba(255, 255, 255, 0.2); + color: inherit; +} + +pre code { + background-color: transparent; + color: inherit; + font-size: 100%; + padding: 0; +} + +/* Containers */ +.content { + margin-bottom: 4em; + margin-left: auto; + margin-right: auto; + max-width: 800px; + padding: 0 1ch; + word-wrap: break-word; +} + +/* Header */ +header { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin: 1em 0; +} + +header .main { + font-size: 1.5rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: 1.2rem; + margin-top: 2em; +} + +h1::before { + color: var(--maincolor); + content: "# "; +} + +h2::before { + color: var(--maincolor); + content: "## "; +} + +h3::before { + color: var(--maincolor); + content: "### "; +} + +h4::before { + color: var(--maincolor); + content: "#### "; +} + +h5::before { + color: var(--maincolor); + content: "##### "; +} + +h6::before { + color: var(--maincolor); + content: "###### "; +} + +.meta { + color: #999; + letter-spacing: -0.5px; +} + +/* Footer */ +footer { + display: flex; + align-items: center; + border-top: 0.4rem dotted var(--bordercl); + padding: 2rem 0rem; + margin-top: 2rem; +} + +.soc { + display: flex; + align-items: center; + padding-right: 1rem; + margin-right: 1rem; + border-right: 2px solid; + border-bottom: none; +} + +.footer-info { + padding: var(--footer-padding); +} + +#main_title { + margin-bottom: 10px; +} + +/* Common */ +.title h1 { + margin-bottom: 0; +} + +time { + color: grey; +} + +/* Posts */ +article .title { + margin-bottom: 1em; +} + +/* Callout */ +.callout { + background-color: var(--callouctcolor); + color: #fff; + padding: 1em; +} + +.callout p { + /* font-family: 'IBM Plex Mono', monospace; */ + margin: 0; +} + +.callout a { + border-bottom: 3px solid #fff; +} + +.callout a:hover { + background-color: #fff; + color: var(--callouctcolor); +} + +.site-description { + display: flex; + justify-content: space-between; +} + +.tags li::before { + content: "🏷 "; +} + +.tags a { + border-bottom: 3px solid var(--maincolor); +} + +.tags a:hover { + color: white; + background-color: var(--hovercolor); +} + +svg { + max-height: 15px; +} + +.soc:hover { + color: white; +} + +.draft-label { + color: var(--bordercl); + text-decoration: none; + padding: 2px 4px; + border-radius: 4px; + margin-left: 6px; + background-color: #f9f2f4; +} + +pre { + font-family: "Roboto Mono", monospace; + position: relative; + -webkit-overflow-scrolling: touch; +} + +pre code[class*="language-"] { + -webkit-overflow-scrolling: touch; +} + +pre code[class*="language-"]::before { + background: black; + border-radius: 0 0 0.25rem 0.25rem; + color: white; + font-size: 12px; + letter-spacing: 0.025rem; + padding: 0.1rem 0.5rem; + position: absolute; + right: 1rem; + text-align: right; + text-transform: uppercase; + top: 0; +} + +pre code[class="language-javaScript"]::before, +pre code[class="language-js"]::before { + content: "js"; + background: #f7df1e; + color: black; +} + +pre code[class="language-typescript"]::before, +pre code[class="language-ts"]::before { + content: "ts"; + background: dodgerblue; + color: black; +} + +pre code[class*="language-yml"]::before, +pre code[class*="language-yaml"]::before { + content: "yaml"; + background: #f71e6a; + color: white; +} + +pre code[class*="language-shell"]::before, +pre code[class*="language-bash"]::before, +pre code[class*="language-sh"]::before { + content: "shell"; + background: green; + color: white; +} + +pre code[class*="language-json"]::before { + content: "json"; + background: dodgerblue; + color: #000000; +} + +pre code[class*="language-python"]::before, +pre code[class*="language-py"]::before { + content: "py"; + background: blue; + color: yellow; +} + +pre code[class*="language-css"]::before { + content: "css"; + background: cyan; + color: black; +} + +pre code[class*="language-go"]::before { + content: "Go"; + background: cyan; + color: royalblue; +} + +pre code[class*="language-md"]::before, +pre code[class*="language-md"]::before { + content: "Markdown"; + background: royalblue; + color: whitesmoke; +} + +pre code[class*="language-rust"]::before, +pre code[class*="language-rs"]::before { + content: "rust"; + background: #fff8f6; + color: #ff4647; +} + +pre code[class*="language-cpp"]::before, +pre code[class*="language-c++"]::before { + content: "cpp"; + background: #5e97d0; + color: #044f88; +} + +/* table */ +table { + border-spacing: 0; + border-collapse: collapse; +} + +table th { + padding: 6px 13px; + border: 1px solid #dfe2e5; + font-size: large; +} + +table td { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.footnote-definition { + display: flex; + align-items: center; + grid-column-gap: 10px; +} diff --git a/fonts/fira-sans-v10-latin-regular.eot b/fonts/fira-sans-v10-latin-regular.eot new file mode 100644 index 0000000..7abf4c2 Binary files /dev/null and b/fonts/fira-sans-v10-latin-regular.eot differ diff --git a/fonts/fira-sans-v10-latin-regular.svg b/fonts/fira-sans-v10-latin-regular.svg new file mode 100644 index 0000000..1e52097 --- /dev/null +++ b/fonts/fira-sans-v10-latin-regular.svg @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/fira-sans-v10-latin-regular.ttf b/fonts/fira-sans-v10-latin-regular.ttf new file mode 100644 index 0000000..572e442 Binary files /dev/null and b/fonts/fira-sans-v10-latin-regular.ttf differ diff --git a/fonts/fira-sans-v10-latin-regular.woff b/fonts/fira-sans-v10-latin-regular.woff new file mode 100644 index 0000000..d99ba57 Binary files /dev/null and b/fonts/fira-sans-v10-latin-regular.woff differ diff --git a/fonts/fira-sans-v10-latin-regular.woff2 b/fonts/fira-sans-v10-latin-regular.woff2 new file mode 100644 index 0000000..9bb5760 Binary files /dev/null and b/fonts/fira-sans-v10-latin-regular.woff2 differ diff --git a/fonts/ibm-plex-mono-v6-latin-500italic.eot b/fonts/ibm-plex-mono-v6-latin-500italic.eot new file mode 100644 index 0000000..62b89b3 Binary files /dev/null and b/fonts/ibm-plex-mono-v6-latin-500italic.eot differ diff --git a/fonts/ibm-plex-mono-v6-latin-500italic.svg b/fonts/ibm-plex-mono-v6-latin-500italic.svg new file mode 100644 index 0000000..6423805 --- /dev/null +++ b/fonts/ibm-plex-mono-v6-latin-500italic.svg @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/ibm-plex-mono-v6-latin-500italic.ttf b/fonts/ibm-plex-mono-v6-latin-500italic.ttf new file mode 100644 index 0000000..e4d1ddf Binary files /dev/null and b/fonts/ibm-plex-mono-v6-latin-500italic.ttf differ diff --git a/fonts/ibm-plex-mono-v6-latin-500italic.woff b/fonts/ibm-plex-mono-v6-latin-500italic.woff new file mode 100644 index 0000000..4504b41 Binary files /dev/null and b/fonts/ibm-plex-mono-v6-latin-500italic.woff differ diff --git a/fonts/ibm-plex-mono-v6-latin-500italic.woff2 b/fonts/ibm-plex-mono-v6-latin-500italic.woff2 new file mode 100644 index 0000000..489745d Binary files /dev/null and b/fonts/ibm-plex-mono-v6-latin-500italic.woff2 differ diff --git a/fonts/roboto-mono-v12-latin-regular.eot b/fonts/roboto-mono-v12-latin-regular.eot new file mode 100644 index 0000000..8c56483 Binary files /dev/null and b/fonts/roboto-mono-v12-latin-regular.eot differ diff --git a/fonts/roboto-mono-v12-latin-regular.svg b/fonts/roboto-mono-v12-latin-regular.svg new file mode 100644 index 0000000..1864328 --- /dev/null +++ b/fonts/roboto-mono-v12-latin-regular.svg @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/roboto-mono-v12-latin-regular.ttf b/fonts/roboto-mono-v12-latin-regular.ttf new file mode 100644 index 0000000..d5dee83 Binary files /dev/null and b/fonts/roboto-mono-v12-latin-regular.ttf differ diff --git a/fonts/roboto-mono-v12-latin-regular.woff b/fonts/roboto-mono-v12-latin-regular.woff new file mode 100644 index 0000000..f319fbf Binary files /dev/null and b/fonts/roboto-mono-v12-latin-regular.woff differ diff --git a/fonts/roboto-mono-v12-latin-regular.woff2 b/fonts/roboto-mono-v12-latin-regular.woff2 new file mode 100644 index 0000000..ed384d2 Binary files /dev/null and b/fonts/roboto-mono-v12-latin-regular.woff2 differ diff --git a/icon/favicon.png b/icon/favicon.png new file mode 100644 index 0000000..cce24c3 Binary files /dev/null and b/icon/favicon.png differ diff --git a/imgs/arch-01.png b/imgs/arch-01.png new file mode 100644 index 0000000..fd77eb1 Binary files /dev/null and b/imgs/arch-01.png differ diff --git a/imgs/arch-02.png b/imgs/arch-02.png new file mode 100644 index 0000000..bc40158 Binary files /dev/null and b/imgs/arch-02.png differ diff --git a/imgs/demo-01.png b/imgs/demo-01.png new file mode 100644 index 0000000..d624c44 Binary files /dev/null and b/imgs/demo-01.png differ diff --git a/imgs/star-history.png b/imgs/star-history.png new file mode 100644 index 0000000..389a6c9 Binary files /dev/null and b/imgs/star-history.png differ diff --git a/imgs/vs-01.png b/imgs/vs-01.png new file mode 100644 index 0000000..55b32b2 Binary files /dev/null and b/imgs/vs-01.png differ diff --git a/imgs/vs-02.png b/imgs/vs-02.png new file mode 100644 index 0000000..bab32c0 Binary files /dev/null and b/imgs/vs-02.png differ diff --git a/imgs/vs-03.png b/imgs/vs-03.png new file mode 100644 index 0000000..857249d Binary files /dev/null and b/imgs/vs-03.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..faeff69 --- /dev/null +++ b/index.html @@ -0,0 +1,182 @@ + + + + + + + + + + + Teun van Wezel's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + +
+
+ +
+ + + +
+ + + + + + + + + + + +
+ + + diff --git a/js/feather.min.js b/js/feather.min.js new file mode 100644 index 0000000..d229492 --- /dev/null +++ b/js/feather.min.js @@ -0,0 +1,13 @@ +!function(e,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.feather=n():e.feather=n()}("undefined"!=typeof self?self:this,function(){return function(e){var n={};function i(l){if(n[l])return n[l].exports;var t=n[l]={i:l,l:!1,exports:{}};return e[l].call(t.exports,t,t.exports,i),t.l=!0,t.exports}return i.m=e,i.c=n,i.d=function(e,n,l){i.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:l})},i.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},i.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(n,"a",n),n},i.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},i.p="",i(i.s=61)}([function(e,n,i){var l=i(20)("wks"),t=i(11),r=i(1).Symbol,o="function"==typeof r;(e.exports=function(e){return l[e]||(l[e]=o&&r[e]||(o?r:t)("Symbol."+e))}).store=l},function(e,n){var i=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=i)},function(e,n){var i=e.exports={version:"2.5.6"};"number"==typeof __e&&(__e=i)},function(e,n){var i={}.hasOwnProperty;e.exports=function(e,n){return i.call(e,n)}},function(e,n,i){e.exports=!i(27)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,n,i){var l=i(13);e.exports=function(e){if(!l(e))throw TypeError(e+" is not an object!");return e}},function(e,n,i){var l=i(5),t=i(56),r=i(55),o=Object.defineProperty;n.f=i(4)?Object.defineProperty:function(e,n,i){if(l(e),n=r(n,!0),l(i),t)try{return o(e,n,i)}catch(e){}if("get"in i||"set"in i)throw TypeError("Accessors not supported!");return"value"in i&&(e[n]=i.value),e}},function(e,n,i){var l=i(6),t=i(12);e.exports=i(4)?function(e,n,i){return l.f(e,n,t(1,i))}:function(e,n,i){return e[n]=i,e}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var l=o(i(35)),t=o(i(33)),r=o(i(32));function o(e){return e&&e.__esModule?e:{default:e}}n.default=Object.keys(t.default).map(function(e){return new l.default(e,t.default[e],r.default[e])}).reduce(function(e,n){return e[n.name]=n,e},{})},function(e,n,i){var l=i(20)("keys"),t=i(11);e.exports=function(e){return l[e]||(l[e]=t(e))}},function(e,n){e.exports={}},function(e,n){var i=0,l=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++i+l).toString(36))}},function(e,n){e.exports=function(e,n){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:n}}},function(e,n){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,n){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,n){var i=Math.ceil,l=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?l:i)(e)}},function(e,n,i){var l; +/*! + Copyright (c) 2016 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames +*/ +/*! + Copyright (c) 2016 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames +*/ +!function(){"use strict";var i=function(){function e(){}function n(e,n){for(var i=n.length,l=0;l0?t(l(e),9007199254740991):0}},function(e,n){var i={}.toString;e.exports=function(e){return i.call(e).slice(8,-1)}},function(e,n,i){var l=i(48),t=i(14);e.exports=function(e){return l(t(e))}},function(e,n,i){var l=i(54);e.exports=function(e,n,i){if(l(e),void 0===n)return e;switch(i){case 1:return function(i){return e.call(n,i)};case 2:return function(i,l){return e.call(n,i,l)};case 3:return function(i,l,t){return e.call(n,i,l,t)}}return function(){return e.apply(n,arguments)}}},function(e,n,i){var l=i(1),t=i(7),r=i(3),o=i(11)("src"),a=Function.toString,c=(""+a).split("toString");i(2).inspectSource=function(e){return a.call(e)},(e.exports=function(e,n,i,a){var y="function"==typeof i;y&&(r(i,"name")||t(i,"name",n)),e[n]!==i&&(y&&(r(i,o)||t(i,o,e[n]?""+e[n]:c.join(String(n)))),e===l?e[n]=i:a?e[n]?e[n]=i:t(e,n,i):(delete e[n],t(e,n,i)))})(Function.prototype,"toString",function(){return"function"==typeof this&&this[o]||a.call(this)})},function(e,n,i){var l=i(13),t=i(1).document,r=l(t)&&l(t.createElement);e.exports=function(e){return r?t.createElement(e):{}}},function(e,n){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,n,i){var l=i(1),t=i(2),r=i(7),o=i(25),a=i(24),c=function(e,n,i){var y,p,h,x,s=e&c.F,u=e&c.G,d=e&c.S,f=e&c.P,v=e&c.B,g=u?l:d?l[n]||(l[n]={}):(l[n]||{}).prototype,m=u?t:t[n]||(t[n]={}),M=m.prototype||(m.prototype={});for(y in u&&(i=n),i)h=((p=!s&&g&&void 0!==g[y])?g:i)[y],x=v&&p?a(h,l):f&&"function"==typeof h?a(Function.call,h):h,g&&o(g,y,h,e&c.U),m[y]!=h&&r(m,y,x),f&&M[y]!=h&&(M[y]=h)};l.core=t,c.F=1,c.G=2,c.S=4,c.P=8,c.B=16,c.W=32,c.U=64,c.R=128,e.exports=c},function(e,n){e.exports=!1},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var l=Object.assign||function(e){for(var n=1;n0&&void 0!==arguments[0]?arguments[0]:{};if("undefined"==typeof document)throw new Error("`feather.replace()` only works in a browser environment.");var n=document.querySelectorAll("[data-feather]");Array.from(n).forEach(function(n){return function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=function(e){return Array.from(e.attributes).reduce(function(e,n){return e[n.name]=n.value,e},{})}(e),o=i["data-feather"];delete i["data-feather"];var a=r.default[o].toSvg(l({},n,i,{class:(0,t.default)(n.class,i.class)})),c=(new DOMParser).parseFromString(a,"image/svg+xml").querySelector("svg");e.parentNode.replaceChild(c,e)}(n,e)})}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var l,t=i(8),r=(l=t)&&l.__esModule?l:{default:l};n.default=function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(console.warn("feather.toSvg() is deprecated. Please use feather.icons[name].toSvg() instead."),!e)throw new Error("The required `key` (icon name) parameter is missing.");if(!r.default[e])throw new Error("No icon matching '"+e+"'. See the complete list of icons at https://feathericons.com");return r.default[e].toSvg(n)}},function(e){e.exports={activity:["pulse","health","action","motion"],airplay:["stream","cast","mirroring"],"alert-circle":["warning"],"alert-octagon":["warning"],"alert-triangle":["warning"],"at-sign":["mention"],award:["achievement","badge"],aperture:["camera","photo"],bell:["alarm","notification"],"bell-off":["alarm","notification","silent"],bluetooth:["wireless"],"book-open":["read"],book:["read","dictionary","booklet","magazine"],bookmark:["read","clip","marker","tag"],briefcase:["work","bag","baggage","folder"],clipboard:["copy"],clock:["time","watch","alarm"],"cloud-drizzle":["weather","shower"],"cloud-lightning":["weather","bolt"],"cloud-rain":["weather"],"cloud-snow":["weather","blizzard"],cloud:["weather"],codepen:["logo"],codesandbox:["logo"],coffee:["drink","cup","mug","tea","cafe","hot","beverage"],command:["keyboard","cmd"],compass:["navigation","safari","travel"],copy:["clone","duplicate"],"corner-down-left":["arrow"],"corner-down-right":["arrow"],"corner-left-down":["arrow"],"corner-left-up":["arrow"],"corner-right-down":["arrow"],"corner-right-up":["arrow"],"corner-up-left":["arrow"],"corner-up-right":["arrow"],"credit-card":["purchase","payment","cc"],crop:["photo","image"],crosshair:["aim","target"],database:["storage"],delete:["remove"],disc:["album","cd","dvd","music"],"dollar-sign":["currency","money","payment"],droplet:["water"],edit:["pencil","change"],"edit-2":["pencil","change"],"edit-3":["pencil","change"],eye:["view","watch"],"eye-off":["view","watch"],"external-link":["outbound"],facebook:["logo"],"fast-forward":["music"],figma:["logo","design","tool"],film:["movie","video"],"folder-minus":["directory"],"folder-plus":["directory"],folder:["directory"],frown:["emoji","face","bad","sad","emotion"],gift:["present","box","birthday","party"],"git-branch":["code","version control"],"git-commit":["code","version control"],"git-merge":["code","version control"],"git-pull-request":["code","version control"],github:["logo","version control"],gitlab:["logo","version control"],global:["world","browser","language","translate"],"hard-drive":["computer","server"],hash:["hashtag","number","pound"],headphones:["music","audio"],heart:["like","love"],"help-circle":["question mark"],hexagon:["shape","node.js","logo"],home:["house"],image:["picture"],inbox:["email"],instagram:["logo","camera"],key:["password","login","authentication"],"life-bouy":["help","life ring","support"],linkedin:["logo"],lock:["security","password"],"log-in":["sign in","arrow"],"log-out":["sign out","arrow"],mail:["email"],"map-pin":["location","navigation","travel","marker"],map:["location","navigation","travel"],maximize:["fullscreen"],"maximize-2":["fullscreen","arrows"],meh:["emoji","face","neutral","emotion"],menu:["bars","navigation","hamburger"],"message-circle":["comment","chat"],"message-square":["comment","chat"],"mic-off":["record"],mic:["record"],minimize:["exit fullscreen"],"minimize-2":["exit fullscreen","arrows"],monitor:["tv"],moon:["dark","night"],"more-horizontal":["ellipsis"],"more-vertical":["ellipsis"],"mouse-pointer":["arrow","cursor"],move:["arrows"],navigation:["location","travel"],"navigation-2":["location","travel"],octagon:["stop"],package:["box"],paperclip:["attachment"],pause:["music","stop"],"pause-circle":["music","stop"],"pen-tool":["vector","drawing"],play:["music","start"],"play-circle":["music","start"],plus:["add","new"],"plus-circle":["add","new"],"plus-square":["add","new"],pocket:["logo","save"],power:["on","off"],radio:["signal"],rewind:["music"],rss:["feed","subscribe"],save:["floppy disk"],search:["find","magnifier","magnifying glass"],send:["message","mail","paper airplane"],settings:["cog","edit","gear","preferences"],shield:["security"],"shield-off":["security"],"shopping-bag":["ecommerce","cart","purchase","store"],"shopping-cart":["ecommerce","cart","purchase","store"],shuffle:["music"],"skip-back":["music"],"skip-forward":["music"],slash:["ban","no"],sliders:["settings","controls"],smile:["emoji","face","happy","good","emotion"],speaker:["music"],star:["bookmark","favorite","like"],sun:["brightness","weather","light"],sunrise:["weather"],sunset:["weather"],tag:["label"],target:["bullseye"],terminal:["code","command line"],"thumbs-down":["dislike","bad"],"thumbs-up":["like","good"],"toggle-left":["on","off","switch"],"toggle-right":["on","off","switch"],trash:["garbage","delete","remove"],"trash-2":["garbage","delete","remove"],triangle:["delta"],truck:["delivery","van","shipping"],twitter:["logo"],umbrella:["rain","weather"],"video-off":["camera","movie","film"],video:["camera","movie","film"],voicemail:["phone"],volume:["music","sound","mute"],"volume-1":["music","sound"],"volume-2":["music","sound"],"volume-x":["music","sound","mute"],watch:["clock","time"],wind:["weather","air"],"x-circle":["cancel","close","delete","remove","times"],"x-octagon":["delete","stop","alert","warning","times"],"x-square":["cancel","close","delete","remove","times"],x:["cancel","close","delete","remove","times"],youtube:["logo","video","play"],"zap-off":["flash","camera","lightning"],zap:["flash","camera","lightning"]}},function(e){e.exports={activity:'',airplay:'',"alert-circle":'',"alert-octagon":'',"alert-triangle":'',"align-center":'',"align-justify":'',"align-left":'',"align-right":'',anchor:'',aperture:'',archive:'',"arrow-down-circle":'',"arrow-down-left":'',"arrow-down-right":'',"arrow-down":'',"arrow-left-circle":'',"arrow-left":'',"arrow-right-circle":'',"arrow-right":'',"arrow-up-circle":'',"arrow-up-left":'',"arrow-up-right":'',"arrow-up":'',"at-sign":'',award:'',"bar-chart-2":'',"bar-chart":'',"battery-charging":'',battery:'',"bell-off":'',bell:'',bluetooth:'',bold:'',"book-open":'',book:'',bookmark:'',box:'',briefcase:'',calendar:'',"camera-off":'',camera:'',cast:'',"check-circle":'',"check-square":'',check:'',"chevron-down":'',"chevron-left":'',"chevron-right":'',"chevron-up":'',"chevrons-down":'',"chevrons-left":'',"chevrons-right":'',"chevrons-up":'',chrome:'',circle:'',clipboard:'',clock:'',"cloud-drizzle":'',"cloud-lightning":'',"cloud-off":'',"cloud-rain":'',"cloud-snow":'',cloud:'',code:'',codepen:'',codesandbox:'',coffee:'',columns:'',command:'',compass:'',copy:'',"corner-down-left":'',"corner-down-right":'',"corner-left-down":'',"corner-left-up":'',"corner-right-down":'',"corner-right-up":'',"corner-up-left":'',"corner-up-right":'',cpu:'',"credit-card":'',crop:'',crosshair:'',database:'',delete:'',disc:'',"dollar-sign":'',"download-cloud":'',download:'',droplet:'',"edit-2":'',"edit-3":'',edit:'',"external-link":'',"eye-off":'',eye:'',facebook:'',"fast-forward":'',feather:'',figma:'',"file-minus":'',"file-plus":'',"file-text":'',file:'',film:'',filter:'',flag:'',"folder-minus":'',"folder-plus":'',folder:'',frown:'',gift:'',"git-branch":'',"git-commit":'',"git-merge":'',"git-pull-request":'',github:'',gitlab:'',globe:'',grid:'',"hard-drive":'',hash:'',headphones:'',heart:'',"help-circle":'',hexagon:'',home:'',image:'',inbox:'',info:'',instagram:'',italic:'',key:'',layers:'',layout:'',"life-buoy":'',"link-2":'',link:'',linkedin:'',list:'',loader:'',lock:'',"log-in":'',"log-out":'',mail:'',"map-pin":'',map:'',"maximize-2":'',maximize:'',meh:'',menu:'',"message-circle":'',"message-square":'',"mic-off":'',mic:'',"minimize-2":'',minimize:'',"minus-circle":'',"minus-square":'',minus:'',monitor:'',moon:'',"more-horizontal":'',"more-vertical":'',"mouse-pointer":'',move:'',music:'',"navigation-2":'',navigation:'',octagon:'',package:'',paperclip:'',"pause-circle":'',pause:'',"pen-tool":'',percent:'',"phone-call":'',"phone-forwarded":'',"phone-incoming":'',"phone-missed":'',"phone-off":'',"phone-outgoing":'',phone:'',"pie-chart":'',"play-circle":'',play:'',"plus-circle":'',"plus-square":'',plus:'',pocket:'',power:'',printer:'',radio:'',"refresh-ccw":'',"refresh-cw":'',repeat:'',rewind:'',"rotate-ccw":'',"rotate-cw":'',rss:'',save:'',scissors:'',search:'',send:'',server:'',settings:'',"share-2":'',share:'',"shield-off":'',shield:'',"shopping-bag":'',"shopping-cart":'',shuffle:'',sidebar:'',"skip-back":'',"skip-forward":'',slack:'',slash:'',sliders:'',smartphone:'',smile:'',speaker:'',square:'',star:'',"stop-circle":'',sun:'',sunrise:'',sunset:'',tablet:'',tag:'',target:'',terminal:'',thermometer:'',"thumbs-down":'',"thumbs-up":'',"toggle-left":'',"toggle-right":'',"trash-2":'',trash:'',trello:'',"trending-down":'',"trending-up":'',triangle:'',truck:'',tv:'',twitter:'',type:'',umbrella:'',underline:'',unlock:'',"upload-cloud":'',upload:'',"user-check":'',"user-minus":'',"user-plus":'',"user-x":'',user:'',users:'',"video-off":'',video:'',voicemail:'',"volume-1":'',"volume-2":'',"volume-x":'',volume:'',watch:'',"wifi-off":'',wifi:'',wind:'',"x-circle":'',"x-octagon":'',"x-square":'',x:'',youtube:'',"zap-off":'',zap:'',"zoom-in":'',"zoom-out":''}},function(e){e.exports={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var l=Object.assign||function(e){for(var n=1;n2&&void 0!==arguments[2]?arguments[2]:[];!function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this,e),this.name=n,this.contents=i,this.tags=t,this.attrs=l({},o.default,{class:"feather feather-"+n})}return t(e,[{key:"toSvg",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return""+this.contents+""}},{key:"toString",value:function(){return this.contents}}]),e}();n.default=c},function(e,n,i){"use strict";var l=o(i(8)),t=o(i(31)),r=o(i(30));function o(e){return e&&e.__esModule?e:{default:e}}e.exports={icons:l.default,toSvg:t.default,replace:r.default}},function(e,n,i){var l=i(0)("iterator"),t=!1;try{var r=[7][l]();r.return=function(){t=!0},Array.from(r,function(){throw 2})}catch(e){}e.exports=function(e,n){if(!n&&!t)return!1;var i=!1;try{var r=[7],o=r[l]();o.next=function(){return{done:i=!0}},r[l]=function(){return o},e(r)}catch(e){}return i}},function(e,n,i){var l=i(22),t=i(0)("toStringTag"),r="Arguments"==l(function(){return arguments}());e.exports=function(e){var n,i,o;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(i=function(e,n){try{return e[n]}catch(e){}}(n=Object(e),t))?i:r?l(n):"Object"==(o=l(n))&&"function"==typeof n.callee?"Arguments":o}},function(e,n,i){var l=i(38),t=i(0)("iterator"),r=i(10);e.exports=i(2).getIteratorMethod=function(e){if(void 0!=e)return e[t]||e["@@iterator"]||r[l(e)]}},function(e,n,i){"use strict";var l=i(6),t=i(12);e.exports=function(e,n,i){n in e?l.f(e,n,t(0,i)):e[n]=i}},function(e,n,i){var l=i(10),t=i(0)("iterator"),r=Array.prototype;e.exports=function(e){return void 0!==e&&(l.Array===e||r[t]===e)}},function(e,n,i){var l=i(5);e.exports=function(e,n,i,t){try{return t?n(l(i)[0],i[1]):n(i)}catch(n){var r=e.return;throw void 0!==r&&l(r.call(e)),n}}},function(e,n,i){"use strict";var l=i(24),t=i(28),r=i(17),o=i(42),a=i(41),c=i(21),y=i(40),p=i(39);t(t.S+t.F*!i(37)(function(e){Array.from(e)}),"Array",{from:function(e){var n,i,t,h,x=r(e),s="function"==typeof this?this:Array,u=arguments.length,d=u>1?arguments[1]:void 0,f=void 0!==d,v=0,g=p(x);if(f&&(d=l(d,u>2?arguments[2]:void 0,2)),void 0==g||s==Array&&a(g))for(i=new s(n=c(x.length));n>v;v++)y(i,v,f?d(x[v],v):x[v]);else for(h=g.call(x),i=new s;!(t=h.next()).done;v++)y(i,v,f?o(h,d,[t.value,v],!0):t.value);return i.length=v,i}})},function(e,n,i){var l=i(3),t=i(17),r=i(9)("IE_PROTO"),o=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=t(e),l(e,r)?e[r]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?o:null}},function(e,n,i){var l=i(1).document;e.exports=l&&l.documentElement},function(e,n,i){var l=i(15),t=Math.max,r=Math.min;e.exports=function(e,n){return(e=l(e))<0?t(e+n,0):r(e,n)}},function(e,n,i){var l=i(23),t=i(21),r=i(46);e.exports=function(e){return function(n,i,o){var a,c=l(n),y=t(c.length),p=r(o,y);if(e&&i!=i){for(;y>p;)if((a=c[p++])!=a)return!0}else for(;y>p;p++)if((e||p in c)&&c[p]===i)return e||p||0;return!e&&-1}}},function(e,n,i){var l=i(22);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==l(e)?e.split(""):Object(e)}},function(e,n,i){var l=i(3),t=i(23),r=i(47)(!1),o=i(9)("IE_PROTO");e.exports=function(e,n){var i,a=t(e),c=0,y=[];for(i in a)i!=o&&l(a,i)&&y.push(i);for(;n.length>c;)l(a,i=n[c++])&&(~r(y,i)||y.push(i));return y}},function(e,n,i){var l=i(49),t=i(19);e.exports=Object.keys||function(e){return l(e,t)}},function(e,n,i){var l=i(6),t=i(5),r=i(50);e.exports=i(4)?Object.defineProperties:function(e,n){t(e);for(var i,o=r(n),a=o.length,c=0;a>c;)l.f(e,i=o[c++],n[i]);return e}},function(e,n,i){var l=i(5),t=i(51),r=i(19),o=i(9)("IE_PROTO"),a=function(){},c=function(){var e,n=i(26)("iframe"),l=r.length;for(n.style.display="none",i(45).appendChild(n),n.src="javascript:",(e=n.contentWindow.document).open(),e.write(" + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + + +

All articles

+ + + + + + + + + + + + +
+ + + diff --git a/posts/iota-move-raffle-tutorial/claim-prize-money.png b/posts/iota-move-raffle-tutorial/claim-prize-money.png new file mode 100644 index 0000000..e01c6dc Binary files /dev/null and b/posts/iota-move-raffle-tutorial/claim-prize-money.png differ diff --git a/posts/iota-move-raffle-tutorial/index.html b/posts/iota-move-raffle-tutorial/index.html new file mode 100644 index 0000000..09edd48 --- /dev/null +++ b/posts/iota-move-raffle-tutorial/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + IOTA Rebased - Writing a Raffle Smart Contract With Move + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + + +
+
+
+

IOTA Rebased - Writing a Raffle Smart Contract With Move

+
+ + on 2024-12-23 + + +
+
+ + + +
+

IOTA Raffle Ticket

+

In this article, we'll see how you can create a raffle smart contract with Move on IOTA Rebased. This smart contract will let users create raffles, and sell tickets for them. The goal of this article is to teach you about Move smart contracts by guiding you through writing one yourself. There are exercises at the end of this article to challenge you to expand the smart contract in your own way.

+

The source code for this project can be found here on GitHub.

+

Prerequisites

+

Though this is a tutorial for Move beginners, if you have never written or deployed a Move smart contract on IOTA Rebased, it's recommended to read and work through this article first. The reader is expected to have some programming knowledge.

+

To write, test, and deploy our smart contract, we will need:

+ +

Raffle functionality description

+

Before we get started, we should specify what exactly we want our raffle smart contract to do. To start off:

+
    +
  • It should let users create new raffles. Users should be able to set a ticket price (in a token of their choice), and a resolution time after which the winner of the raffle can claim the prize money;
  • +
  • It should let users buy tickets to any existing raffles;
  • +
  • It should allow users to "resolve" the raffle after the resolution time, and claim the prize money if they won.
  • +
+

You might be able to come up with some more useful functionality - we'll keep it simple here though as to not make this article too long.

+

Writing the raffle smart contract

+

Starting out: Creating the Raffle and RaffleTicket structs

+

Let's get to writing the raffle smart contract. Create a new IOTA Move project, and open up the newly created folder in your code editor.

+
$ iota move new raffle
+$ cd raffle
+$ code .
+
+

Let's start out by defining a Raffle struct and a RaffleTicket struct in sources/raffle.move. Make sure to add the use imports at the top, we will need those later.

+
// sources/raffle.move
+module raffle::raffle {
+
+    // We will need these later on
+    use iota::balance::{Self, Balance};
+    use iota::coin::{Coin};
+    use iota::clock::{Clock};
+    use iota::random::{Self, Random};
+
+    /// A raffle. Token `T` will be what is used to buy tickets for that raffle.
+    public struct Raffle<phantom T> has key, store {
+        id: UID,
+        ticket_price: u64,
+        redemption_timestamp_ms: u64,
+        prize_money: Balance<T>,
+        sold_tickets: vector<ID>,
+        winning_ticket: Option<ID> // set when the raffle is resolved
+    }
+
+    /// A struct representing a ticket in a specific raffle.
+    public struct RaffleTicket has key, store {
+        id: UID
+    }
+}
+
+

A few things might not be immediately clear here. Hopefully the following notes will help:

+
    +
  • The redemption_timestamp_ms is the timestamp after which the raffle can be resolved (ended). We will set this on creating the Raffle based on the wanted raffle duration. When the raffle is resolved, a winning ticket is picked automatically and ticket sales are halted.
  • +
  • To keep track of all the tickets that are sold, each raffle holds a vector holding the sold ticket ID's. This allows the smart contract to pick a winning ticket when the raffle is resolved.
  • +
  • To allow users to choose the token used to pay for raffle tickets, we have to make the Raffle type generic over the token type T, which is why the raffle type is defined as Raffle<phantom T>.
  • +
  • The RaffleTicket doesn't really "do" anything. The power of defining this struct lies in Move's type system: our smart contract will be the only one able to create these RaffleTickets, so that we can be sure no one else can create them (without paying the ticket price).
  • +
+

Raffle creation

+

Next, let's add a function for creating raffles. All we need to create an instance of our Raffle struct is calculating the redemption timestamp. The function will need to take a reference to the shared Clock to get the current time. (We'll see how to pass a reference to Clock as an argument at the end.)

+
// sources/raffle.move
+module raffle::raffle {    
+    ...
+
+    /// Create a raffle
+    entry fun create_raffle<T>(ticket_price: u64, duration_s: u64, clock: &Clock, ctx: &mut TxContext) {
+        // Calculate the redemption timestamp from `duration_s`
+        let redemption_timestamp_ms = clock.timestamp_ms() + 1000 * duration_s;
+        // Create the new raffle
+        let raffle = Raffle<T> {
+            id: object::new(ctx),
+            ticket_price,
+            redemption_timestamp_ms,
+            prize_money: balance::zero<T>(),
+            sold_tickets: vector[],
+            winning_ticket: option::none()
+        };
+        // Make the raffle public by sharing it, so that people can buy tickets
+        transfer::share_object(raffle);
+    }
+}
+
+

We "publicize" the raffle by calling transfer::share_object. This call gives all network participants mutable access to raffle. This may seem worrying at first: doesn't that mean that anyone can take out all of the prize_money from a Raffle? No, because the MoveVM only lets structs be directly mutated in their defining modules. Which means that people can only mutate Raffles in ways that we let them, as defined through functions exposed in our raffle module.

+

To be technical, the transfer::share_object function only allows network participants to get a mutable reference to the Raffle - which allows mutation within the defining module, but not anywhere else. In contrast, there also exists a transfer::freeze_object function which only allows for getting immutable references. Objects that are "frozen" this way can't be mutated, not even by their defining modules.

+

Ticket sales

+

Let's continue by adding a function for buying tickets. The process starts by removing ticket_price tokens from the payment coin. We add this amount to the prize_money. Then we create the ticket. First, generate a new UID for the ticket - and add it to the list of sold_tickets. With our new ticket_id, we create an instance of our RaffleTicket struct and transfer it to whomever called the buy_ticket function.

+
// sources/raffle.move
+module raffle::raffle {    
+    ...
+    
+    /// Buy a ticket to a raffle
+    public fun buy_ticket<T>(raffle: &mut Raffle<T>, payment: &mut  Coin<T>, ctx: &mut TxContext) {
+        // Add payment to the prize money
+        raffle.prize_money.join(payment.split(raffle.ticket_price, ctx).into_balance());
+        
+        // Create and transfer ticket
+        let ticket_id = object::new(ctx);
+        raffle.sold_tickets.push_back(ticket_id.to_inner());
+        let ticket = RaffleTicket { id: ticket_id };
+        transfer::transfer(ticket, ctx.sender());
+    }
+}
+
+

Since UIDs have to be unique by definition, Move's type system won't let us create a copy of ticket_id directly. Instead we have to call to_inner() to get an ID (which basically functions as an object pointer, which MoveVM is fine with us copying).

+

Maybe you've already noticed that there's a problem with our function. In fact there's two. The first is that we don't check if there's enough tokens in payment to fulfill the ticket_price. The second is that our buy_ticket function lets people buy tickets to raffles that have already been resolved (i.e. raffles for which a winner has already been picked).

+

The first problem we don't have to worry about: Luckily for us, calling coin.split(amount) aborts automatically if the coin doesn't contain at least amount tokens.

+

The second problem can be solved by checking if a winning ticket has been picked, before we complete a ticket sale. Let's create a helper function is_resolved, and update our ticket buying function to abort if the raffle has already been resolved:

+
// sources/raffle.move
+module raffle::raffle {
+    const ERaffleAlreadyResolved: u64 = 0;
+
+    ...
+    // Check if a raffle has resolved yet
+    public fun is_resolved<T>(raffle: &Raffle<T>): bool {
+        raffle.winning_ticket.is_some()
+    }
+
+    /// Buy a ticket to a raffle
+    public fun buy_ticket<T>(raffle: &mut Raffle<T>, payment: &mut  Coin<T>, ctx: &mut TxContext) {
+        // Cancel ticket sale if the raffle has been resolved already
+        assert!(!raffle.is_resolved(), ERaffleAlreadyResolved);
+
+        // Add payment to the prize money
+        raffle.prize_money.join(payment.split(raffle.ticket_price, ctx).into_balance());
+        
+        // Create and transfer ticket
+        let ticket_id = object::new(ctx);
+        raffle.sold_tickets.push_back(ticket_id.to_inner());
+        let ticket = RaffleTicket { id: ticket_id };
+        transfer::transfer(ticket, ctx.sender());
+    }
+}
+
+

Make sure to add the error code at the top of your module const ERaffleAlreadyResolved: u64 = 0;. We will add a few more of these error codes. It's a good habit to add meaningful error codes to your Move smart contracts. They help users know what exactly went wrong when a transaction fails.

+

Resolving raffles: picking a winner

+

Once the raffle has ended, we want to have our smart contract pick a winner at random. Let's create a function resolve for this. We will use the random module to get a random ticket. We set raffle.winning_ticket, so that the winner can claim their prize money later on.

+

Before we pick a winner, the function needs to check that the raffle hasn't been resolved already, and that the required time has passed.

+
// sources/raffle.move
+module raffle::raffle {
+    const ERaffleAlreadyResolved: u64 = 0;
+    const ERaffleNotResolvableYet: u64 = 1; // new
+    const ERaffleNotResolved: u64 = 2;      // new too
+    ...
+
+    /// Resolve the raffle (decide who wins)
+    entry fun resolve<T>(raffle: &mut Raffle<T>, clock: &Clock, r: &Random, ctx: &mut TxContext) {
+        // Can't resolve twice
+        assert!(!raffle.is_resolved(), ERaffleAlreadyResolved);
+
+        // Make sure that the raffle is ready to be resolved
+        let current_timestamp_ms = clock.timestamp_ms();
+        assert!(current_timestamp_ms >= raffle.redemption_timestamp_ms, ERaffleNotResolvableYet);
+
+        // Pick a winner at random
+        let tickets_sold = raffle.sold_tickets.length();
+        let winner_idx = random::new_generator(r, ctx).generate_u64_in_range(0, tickets_sold - 1);
+        raffle.winning_ticket = option::some(raffle.sold_tickets[winner_idx]);
+    }
+}
+
+

Similarly to Clock, we will need a reference to the shared Random object to generate a random number. Lastly, again make sure to add the error codes ERaffleAlreadyResolved and ERaffleNotResolvableYet at the top of your module.

+

Paying out the prize money

+

We're almost done now. Note that the resolve function doesn't pay out the prize money - we will keep that functionality separate. For this purpose, we will create a dedicated function to claim the prize money. We'll need to make sure the raffle is resolved (i.e. a winner has been picked) and that the function caller has the winning ticket.

+
module raffle::raffle {
+    const ERaffleAlreadyResolved: u64 = 0;
+    const ERaffleNotResolvableYet: u64 = 1;
+    const ERaffleNotResolved: u64 = 2;
+    const ETicketDidNotWin: u64 = 3; // new
+    ...
+
+    /// Claim the prize money using the winning RaffleTicket
+    public fun claim_prize_money<T>(raffle: &mut Raffle<T>, ticket: RaffleTicket, ctx: &mut TxContext) {
+        assert!(raffle.is_resolved(), ERaffleNotResolved);
+
+        let RaffleTicket { id: winning_ticket_id } = ticket;
+        assert!(raffle.winning_ticket == option::some(*winning_ticket_id.as_inner()),
+            ETicketDidNotWin
+        );
+
+        // Delete winning ticket
+        object::delete(winning_ticket_id);
+
+        // Send full prize_money balance to winner
+        let payout_coin = raffle.prize_money.withdraw_all().into_coin(ctx);
+        transfer::public_transfer(payout_coin, ctx.sender());
+    }
+}
+
+

We delete the ticket to guarantee that this function can be called successfully only once. (Though there currently is no way to add to a Raffle's prize money balance after it's been resolved, it's still a good safety measure.)

+

Done!

+

And that's it! Your smart contract should now be complete. If you want to check your code, the full source code can be found here.

+

Deploying our smart contract

+

Now that we've built our smart contract, we want to make sure it works as intended. Make sure your IOTA Client CLI is configured to the IOTA Rebased testnet (see here) and publish (deploy) the smart contract to by calling (inside the raffle directory):

+
$ iota client publish
+
+

Under 'Object Changes'-'Published Objects', you should see your package with Modules: raffle. Copy the PackageID and export it as an environment variable. In bash:

+
export PACKAGE_ID=your_package_id_here
+
+

Using our smart contract

+

Now let's see the smart contract in action! We will use the IOTA Client PTB CLI to call the functions in our smart contract. We will create a raffle with tickets paid for in IOTA. To keep things tidy, start out by defining all the relevant environment variables in your shell:

+
export TICKET_PRICE_NANO=100000000 COIN_ID=0x1234 RAFFLE_DURATION_S=120
+
+

You can change these values to your liking of course. If you need a coin address, you can get the ID of all of your coins by calling iota client gas, just use whichever one has enough IOTA to cover the TICKET_PRICE (note that 1 IOTA = 1.000.000.000 nanos).

+

Now let's create a raffle using these variables. Creating a raffle is done as follows, with '<0x2::iota::IOTA>' as a type argument for the token type T:

+
$ iota client ptb --move-call $PACKAGE_ID::raffle::create_raffle \
+    '<0x2::iota::IOTA>' $TICKET_PRICE_NANO $RAFFLE_DURATION_S @0x6
+
+

This command should show you that the Raffle has been created. Make sure to export the Raffle's ObjectID (see image below) as an environment variable RAFFLE_ID. We will need it in the next few commands.

+

The Raffle ObjectID in the command output

+

Now, to buy a ticket for your raffle, call the buy_ticket as follows:

+
$ iota client ptb --move-call $PACKAGE_ID::raffle::buy_ticket \
+    '<0x2::iota::IOTA>' @$RAFFLE_ID @$COIN_ID
+
+

You can buy multiple tickets by running this command repeatedly.

+

The output of this command should show you that a RaffleTicket was created. Export an environment variable TICKET_ID with the RaffleTicket's ObjectID (see image below).

+

The RaffleTicket ObjectID in the command output

+

Then, to determine who's won the raffle (it's probably you!), resolve the raffle by calling the resolve function. If you do this fast enough (or if you set a really high raffle duration), you will get an error with code 0 = ERaffleNotResolvableYet. This is good; it means that the raffle is working as intended! Wait a little longer and try again. (Or create a new raffle that has a shorter duration.)

+
$ iota client ptb --move-call $PACKAGE_ID::raffle::resolve \
+    '<0x2::iota::IOTA>' @$RAFFLE_ID @0x6 @0x8
+
+

Now that the raffle is resolved, the winner (you) can claim their prize money with claim_prize_money. (If you bought multiple tickets for your raffle, make sure to check out your Raffle object on the IOTA Rebased explorer by searching for its ObjectID. You will be able to see the ObjectID of the winning ticket under the winning_ticket field of your Raffle. Export this value to the TICKET_ID environment variable.)

+

Here goes:

+
$ iota client ptb --move-call $PACKAGE_ID::raffle::claim_prize_money \
+    '<0x2::iota::IOTA>' @$RAFFLE_ID @$TICKET_ID
+
+

And from the output of this command, you should see that you got the tokens you paid for the tickets back (minus some gas), as shown below.

+

Claim prize money command output

+

Congratulations! You've now successfully created and used your own raffle smart contract.

+

Exercises

+

If you've completed the above tutorial, and want to challenge yourself, try to add the following functionality to the raffle smart contract. You can do just one, or all of them if you'd like.

+
    +
  • A raffle creator supposes that by increasing the payout on a raffle, more people will be interested in buying a ticket. To support this idea, try adding a function to deposit funds into the prize pool that doesn't give the user a ticket in return.
  • +
  • The raffle creator's raffles have been generating a lot of interest in the IOTA community. He thinks it's time to capitalize on that interest by starting to charge an "administrative fee" on his raffle ticket sales. Try adding fees to the ticket sale function. You will have to keep track of the creator of each raffle to pay out these "administrative fees" (for example by adding a creator field to the Raffle struct).
  • +
  • The raffle creator raked in a large chunk of money with the "administrative fees" on his raffles. He wants to give back to the IOTA community by giving away a part of his earnings to one lucky winner. Try changing the smart contract to allow for this giveaway. Consider that a giveaway is basically a raffle where you can get a ticket for free - but you can't get more than one ticket. One approach would be to add a maxTickets to the Raffle struct.
  • +
+

Thanks for reading!

+ +
+ + + +
+
+ + + + + + + + + +
+ + + diff --git a/posts/iota-move-raffle-tutorial/iota-raffle-ticket.jpg b/posts/iota-move-raffle-tutorial/iota-raffle-ticket.jpg new file mode 100644 index 0000000..ace4d3a Binary files /dev/null and b/posts/iota-move-raffle-tutorial/iota-raffle-ticket.jpg differ diff --git a/posts/iota-move-raffle-tutorial/raffle-id.png b/posts/iota-move-raffle-tutorial/raffle-id.png new file mode 100644 index 0000000..840da60 Binary files /dev/null and b/posts/iota-move-raffle-tutorial/raffle-id.png differ diff --git a/posts/iota-move-raffle-tutorial/raffle-ticket-id.png b/posts/iota-move-raffle-tutorial/raffle-ticket-id.png new file mode 100644 index 0000000..4420c2e Binary files /dev/null and b/posts/iota-move-raffle-tutorial/raffle-ticket-id.png differ diff --git a/posts/iota-rebased-sc/confirmed-whale-frontend.png b/posts/iota-rebased-sc/confirmed-whale-frontend.png new file mode 100644 index 0000000..e0c17a6 Binary files /dev/null and b/posts/iota-rebased-sc/confirmed-whale-frontend.png differ diff --git a/posts/iota-rebased-sc/index.html b/posts/iota-rebased-sc/index.html new file mode 100644 index 0000000..45e30e6 --- /dev/null +++ b/posts/iota-rebased-sc/index.html @@ -0,0 +1,476 @@ + + + + + + + + + + + Smart Contracts on IOTA Rebased - an Introduction + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + + +
+
+
+

Smart Contracts on IOTA Rebased - an Introduction

+
+ + on 2024-12-09 + + +
+
+ + + +
+

IOTA Rebased logo

+

With the upcoming IOTA Rebased network upgrade, the Move smart contract programming language is coming to IOTA. The goal of this article is to show you how you can get a simple dApp (decentralized application) written in Move up and running quickly. We will write and publish a Move smart contract on the IOTA Rebased testnet, and create a frontend to interact with it using Svelte.

+

The code for this article can be found here.

+

Who is this article for?

+

This article is for developers who want to get a feeling for writing smart contracts with Move on IOTA. Some previous programming experience is required; familiarity with Rust (similar to Move) and TypeScript (for the frontend) will help a lot.

+

Prerequisites

+

Before we can get started, make sure you have installed the following:

+ +

Writing a smart contract with Move

+

Alright, let's get to writing a smart contract. To keep things very simple, we will create a smart contract that sends a ConfirmedWhaleNFT to anyone that interacts with the smart contract and proves that they have more than 5 IOTA.

+

The smart contract will work as follows: the user can send a Coin<IOTA> object to the smart contract, which the smart contract will then immediately return to the sender. (In IOTA Move, IOTA tokens are represented as Coin<IOTA> objects, which can hold any number of IOTA tokens.) If the Coin object contains at least 5 IOTA, we will also create and send a ConfirmedWhaleNFT to the user. If the Coin object contains too little funds, the user will not receive a ConfirmedWhaleNFT. You can think of this NFT as a "proof of funds" NFT.

+

Let's start out by creating our project using the iota CLI. We'll call our package confirmed_whale_nft.

+
$ iota move new confirmed_whale_nft
+
+

This should create a folder confirmed_whale_nft. Open this folder in your preferred code editor. Now open up the file sources/confirmed_whale_nft.move. This is where we will be writing our smart contract. Let's start out by defining the ConfirmedWhaleNFT object that we will be sending to users.

+
// confirmed_whale_nft.move
+module confirmed_whale_nft::confirmed_whale_nft {
+    public struct ConfirmedWhaleNFT has key {
+        id: UID,
+        owner: address
+    }
+}
+
+

This should look pretty familiar if you've ever worked with a typed programming language before. The expression has key let's the MoveVM know that the confirmed_whale_nft is a Move object. Because it's a Move object, its first field needs to be of type UID, which is a globally unique object ID.

+

Next, we have to add a way for users to get one of these NFT's. Let's create a function that lets the user do just that. We will need to import a few things from the standard library.

+
// confirmed_whale_nft.move
+module confirmed_whale_nft::confirmed_whale_nft {
+    use iota::iota::{IOTA};
+    use iota::coin::{Coin};
+
+    public struct ConfirmedWhaleNFT has key {...}
+
+    // Send a ConfirmedWhaleNFT to the caller if they send along a coin with at
+    // least 5 IOTA.
+    public fun confirm_whale(coin: Coin<IOTA>, ctx: &mut TxContext) {
+        let coin_value_iota = coin.value() / 1_000_000_000;
+        let caller = tx_context::sender(ctx);
+        if (coin_value_iota >= 5) {
+            let nft = ConfirmedWhaleNFT {
+                id: object::new(ctx),
+                owner: caller
+            };
+            transfer::transfer(nft, caller);
+        };
+        transfer::public_transfer(coin, caller);
+    }
+}
+
+

That's all for now! Simple right?

+

A few notes on this code:

+
    +
  • The base unit for IOTA Rebased is a NANO, which is one billionth of an IOTA (i.e. 1 IOTA = 1.000.000.000 NANO). That's why we have to divide the coin value by a billion to get the value in IOTA.
  • +
  • The object::new(ctx) is used to create a new globally unique object ID for our NFT. This is done at runtime when the transaction is executed.
  • +
  • There are quite a few modules imported by default, like iota::tx_context, iota::transfer, and iota::object, which is why we can use them without having to import them.
  • +
+

You can make sure that there are no errors in your code by building your smart contract:

+
$ iota move build
+
+

Publishing the smart contract

+

Now that we've written our first smart contract, we will deploy it to the IOTA Rebased testnet, so that we can start seeing our code in action. To get started, we will first add and switch to the testnet environment for our IOTA Client CLI.

+

Add the testnet environment:

+
$ iota client new-env --alias=testnet --rpc https://api.testnet.iota.cafe:443
+Added new IOTA env [testnet] to config.
+
+

Switching to the newly added testnet environment:

+
$ iota client switch --env testnet
+Active environment switched to [testnet]
+
+

Now, to deploy the smart contract, we'll need some IOTA as gas. We can get some IOTA for free from the testnet faucet by calling:

+
$ iota client faucet
+Request successful. It can take up to 1 minute to get the coin. Run iota client gas to check your gas coins.
+
+

Now, to deploy our smart contract, in the folder confirmed_whale_nft:

+
$ iota client publish --gas-budget 100000000
+
+

If everything went well, the above command should print the results of the publishing our package. Under 'Object Changes'-'Published Objects', you should see your package with Modules: confirmed_whale_nft (see image below). Make sure to copy the PackageID, since we will need it later to interact with our smart contract.

+

PackageID in command output

+

Creating a frontend

+

Now, let's create a frontend to interact with our smart contract. We will use Svelte/SvelteKit. The frontend will be very minimal, but it should illustrate how a frontend that interacts with a Move smart contract.

+

Setup

+

Let's start by creating the frontend project using npx. The recommended installation options are shown below.

+
$ npx sv create confirmed_whale_nft_frontend
+  Which template would you like?
+  SvelteKit minimal
+│
+  Add type checking with Typescript?
+  Yes, using Typescript syntax
+│
+  Project created
+│
+  What would you like to add to your project? (use arrow keys / space bar)
+  tailwindcss
+│
+  Which plugins would you like to add?
+  none
+│
+  Which package manager do you want to install dependencies with?
+  npm
+  ...
+  You're all set!
+
+$ cd confirmed_whale_nft_frontend
+
+

Let's install some dependencies. We will need the iota-sdk to interact with the IOTA Rebased testnet, and to interact with the smart contract. We will need the Mysten wallet-standard to interact with end-user wallets.

+
$ npm install @iota/iota-sdk @mysten/wallet-standard 
+
+

Now, there's one last thing that we have to do before we can start creating our frontend. Create a component src/lib/ConfirmedWhaleInterface.svelte, and then replace the contents inside src/routes/+page.svelte so that it only holds this component:

+
<!-- src/routes/+page.svelte -->
+<script>
+    import ConfirmedWhaleInterface from "$lib/ConfirmedWhaleInterface.svelte";
+</script>
+
+<ConfirmedWhaleInterface />
+
+

This ConfirmedWhaleInterface component will hold all the HTML and TypeScript for our frontend.

+

Creating the frontend

+

To make things easier, make sure to start up a live server.

+
npm run dev -- --open
+
+

Let's start out by defining a few visual components using HTML / Svelte in our ConfirmedWhaleInterface.

+
<!-- src/lib/ConfirmedWhaleInterface.svelte -->
+
+<div class="m-4">
+
+    <h1>
+        Welcome to the Confirmed Whale dApp!
+    </h1>
+
+    <!-- Button for connecting to an IOTA wallet -->
+    <button 
+        onclick={connectWallet}
+        class="bg-blue-500 p-2"
+    >
+        {activeWallet ? 'Connected' : 'Connect wallet'}
+    </button>
+
+    <!-- Display for the balance of the active walletAccount -->
+    <p>{activeWalletAccount ? `Balance: ${activeWalletAccountBalance}` : 'Connect a wallet to show balance'}</p>
+
+    <hr/>
+
+    <!-- Input for how many NANOs to send to the smart contract -->
+    <h3>Send amount (need at least 5000000000 to get ConfirmedWhaleNFT)</h3>
+    <input bind:value={sendAmountNanos} type="number" lang="en" step="1"/>
+
+    <!-- Button that allows for interaction with the smart contract -->
+    <button 
+        onclick={confirmWhale}
+        class="bg-blue-500 p-2"
+        disabled={!activeWalletAccount}
+    >
+        {activeWalletAccount ? "call confirm_whale" : "Connect a wallet to interact with smart contract."}
+    </button>
+
+    <!-- Display of the result of the transaction -->
+    <p>{resultText}</p>
+    <a 
+        href={explorerUrl} target="_blank"
+        class="bg-green-300"
+    >
+        {explorerUrl == "" ? "" : "Check out the NFT on the IOTA Rebased explorer"}
+    </a>
+
+</div>
+
+

The HTML is (hopefully) pretty self-explanatory. Now let's look at the relevant TypeScript and see how the actual logic for interacting with our smart contract is defined. We begin by importing the relevant libraries, defining the relevant constants, and defining the state variables used in our app. Next important thing is to initialize the IOTA Client for the testnet. We will use it to send transactions.

+
<!-- src/lib/ConfirmedWhaleInterface.svelte -->
+
+<script lang="ts">
+    import type { Wallet, WalletAccount } from '@mysten/wallet-standard';
+    import { getWallets } from '@mysten/wallet-standard';
+    import { getFullnodeUrl, IotaClient } from '@iota/iota-sdk/client';
+    import { Transaction } from '@iota/iota-sdk/transactions';
+    
+    let PACKAGE_ID = "YOUR_PACKAGE_ID_HERE";
+    let MODULE_NAME = "confirmed_whale_nft";
+    let FUNCTION_NAME = "confirm_whale";
+    let GAS_BUDGET = 100_000_000;
+    
+    let activeWallet = $state(null);
+    let activeWalletAccount = $state(null);
+    let activeWalletAccountBalance = $state(0);
+    let sendAmountNanos = $state(0);
+    let resultText = $state("");
+    let explorerUrl = $state("");
+
+    const iotaClient = new IotaClient({ url: getFullnodeUrl('testnet') });
+    ...
+</script>
+
+

Make sure you paste your PackageId in PACKAGE_ID. Of interest here may be the hardcoded GAS_BUDGET, set to 0.1 IOTA for simplicity. You can lower this if you want, but if you set the gas budget too low, transactions may fail.

+

Additionally, you may wonder what the difference is between a Wallet and a WalletAccount. You can think of a Wallet as any app (browser extension or desktop app) that can manage your assets - and WalletAccounts as the addresses that are managed by a Wallet. A Wallet can have many different WalletAccounts.

+

Next, let's see how we can connect to an IOTA wallet.

+
<!-- src/lib/ConfirmedWhaleInterface.svelte -->
+
+<script>
+    ...
+
+    const iotaClient = new IotaClient({ url: getFullnodeUrl('testnet') });
+
+    async function connectWallet() {
+        let wallets = getWallets().get();
+        if (wallets.length == 0) {
+            console.log("No wallets found to connect to. Make sure you installed an IOTA web wallet.");
+            return;
+        }
+        // Make sure we get the right wallet
+        activeWallet = wallets.find((w) => w.name == "IOTA Wallet");
+        if (!activeWallet) {
+            console.log("No IOTA wallets found to connect to. Make sure you installed an IOTA web wallet.");
+            return;
+        }
+        activeWallet.features['standard:connect'].connect();
+        activeWallet.features['standard:events'].on("change", () => {
+            activeWalletAccount = activeWallet.accounts[0];
+            updateBalance();
+        });
+    }
+
+    // Helper function to update the balance of the activeWalletAccount
+    async function updateBalance() {
+        if (activeWalletAccount) {
+            activeWalletAccountBalance = (await iotaClient.getBalance({owner: activeWalletAccount.address})).totalBalance;
+        }
+    }
+
+    ...
+</script>
+
+

When an error occurs while trying to connect to a wallet, we will just write to the console what went wrong and return. There's are a few things that can go wrong in the process of connecting - in a real dApp you would probably want to do more robust error handling. For now we will keep things simple.

+

When we call activeWallet.features['standard:connect'].connect(), the wallet will present the user with the connection request and ask them to approve it. We don't know how long the user will take to approve the request, so we add the event listener that sets activeWalletAccount and updates the balance when any 'change' happens on the wallet. The wallet successfully connecting is such a 'change'.

+

Now we're almost done - the only thing left to do is to add the logic for calling our smart contract function. Let's walk through it.

+

We will start out by creating the transaction and creating a Coin<IOTA> object by splitting it from the gas Coin.

+
<!-- src/lib/ConfirmedWhaleInterface.svelte -->
+
+<script>
+    ...
+
+    // Send a transaction to the smart contract, calling `confirm_whale`.
+    async function confirmWhale() {
+
+        // Set up the transaction
+        let tx = new Transaction();
+        tx.setGasBudget(GAS_BUDGET);
+
+        let [coin] = tx.splitCoins(tx.gas, [sendAmountNanos]);
+        tx.moveCall({
+            package: PACKAGE_ID,
+            module: MODULE_NAME,
+            function: FUNCTION_NAME,
+            arguments: [tx.object(coin)],
+        });
+    }
+    ...
+</script>
+
+

The meat of the function is in the tx.moveCall(). This is saying that we want to call the confirm_whale Move function with our coin as an argument.

+

Now that we defined what we want the transaction to do, we need the user to sign the transaction with the wallet, so that it can be executed.

+
// Send a transaction to the smart contract, calling `confirm_whale`.
+async function confirmWhale() {
+    ...
+
+    // Request the wallet to sign the transaction
+    let {bytes, signature} = 
+    await (activeWallet.features['iota:signTransaction']).signTransaction({
+        transaction: tx, 
+        account: activeWalletAccount,
+    });
+
+    // Send signed transaction to the network for execution
+    let transactionResult = await iotaClient.executeTransactionBlock({
+        transactionBlock: bytes,
+        signature: signature,
+    })
+}
+
+

Here again, in a real dApp you would probably want to add proper error handling. For example, the user refusing to sign the transaction should be handled gracefully. But that's outside the scope of this article.

+

The last part of this function deals with the result of executing the transaction. We will have to wait for the transaction to be final on the network, after which we can get the relevant transactionBlock using the transaction digest. The last part of the code makes sure that the result of the transaction is shown to the user. The explorerUrl is updated so that a link to the created ConfirmedWhaleNFT is shown.

+
// Send a transaction to the smart contract, calling `confirm_whale`.
+async function confirmWhale() {
+    ...
+
+    // Wait for transaction to complete and parse results
+    await iotaClient.waitForTransaction({ digest: transactionResult.digest });
+
+    let transactionBlock = await iotaClient.getTransactionBlock({
+        digest: transactionResult.digest,
+        options: {
+            showEffects: true,
+            showObjectChanges: true,
+        }
+    });
+
+    // Show transaction results, if the transaction was successful
+    if (transactionBlock.effects?.status.status == 'success'){
+        let confirmedWhaleNftCreated = transactionBlock.objectChanges?.find((change) => {
+            return change.objectType.endsWith("::confirmed_whale_nft::ConfirmedWhaleNFT")
+        });
+        if (confirmedWhaleNftCreated) {
+            resultText = `Transaction success! ConfirmedWhaleNFT was created with id ${confirmedWhaleNftCreated.objectId}.`;
+            explorerUrl = `https://explorer.rebased.iota.org/object/${confirmedWhaleNftCreated.objectId}`;
+        } else {
+            resultText = "Transaction succeeded, but ConfirmedWhaleNFT was not created.";
+        }
+    }
+}
+
+

That's it! Our smart contract with frontend is now complete. Your frontend should look something like this:

+

ConfirmedWhaleNFT Frontend

+

Of course there are a bunch of things that could be improved: the frontend design, improved smart contract functionality, error handling, testing, etc.. But that's for another article.

+

For now, I hope that you learned something and are ready to write your own dApps. If you want to continue learning, consider taking a look at this article, which will show you how to create a raffle smart contract with Move.

+

Thanks for reading!

+

For the full code, check out the accompanying github repository.

+ +
+ + + +
+
+ + + + + + + + + +
+ + + diff --git a/posts/iota-rebased-sc/iota-rebased.jpg b/posts/iota-rebased-sc/iota-rebased.jpg new file mode 100644 index 0000000..db86aa0 Binary files /dev/null and b/posts/iota-rebased-sc/iota-rebased.jpg differ diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..030cce6 --- /dev/null +++ b/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Disallow: +Allow: / +Sitemap: https://teunvw14.github.io/sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..3f7d57d --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,17 @@ + + + + https://teunvw14.github.io/ + + + https://teunvw14.github.io/posts/ + + + https://teunvw14.github.io/posts/iota-move-raffle-tutorial/ + 2024-12-23 + + + https://teunvw14.github.io/posts/iota-rebased-sc/ + 2024-12-09 + +