├── .gitignore ├── README.md ├── app ├── .env-sample ├── .eslintrc.cjs ├── README.md ├── assets │ ├── add-article-form.png │ ├── editor.png │ └── home-page.png ├── index.html ├── lua │ ├── cms.lua │ └── proxy.lua ├── package-lock.json ├── package.json ├── public │ ├── ao.svg │ ├── arweave.png │ ├── atomic-asset.lua │ ├── atomic-note.lua │ ├── atomic-notes.png │ ├── collection.lua │ ├── cover.png │ ├── github.svg │ ├── icon.png │ ├── logo.png │ ├── x.png │ └── x.svg ├── src │ ├── App.css │ ├── App.jsx │ ├── components │ │ ├── Header.jsx │ │ ├── NoteCard.jsx │ │ └── NotebookCard.jsx │ ├── github-markdown.css │ ├── lib │ │ ├── svgs.jsx │ │ └── utils.js │ ├── main.jsx │ └── pages │ │ ├── Book.jsx │ │ ├── CreateBook.jsx │ │ ├── CreateNote.jsx │ │ ├── Note.jsx │ │ └── User.jsx ├── vite.config.js └── yarn.lock ├── assets ├── cover.png └── logo.png ├── logo ├── .gitignore ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── ao.svg │ ├── cover.png │ ├── icon.png │ └── svg-export.min.js ├── src │ ├── App.jsx │ ├── font.js │ └── main.jsx ├── vite.config.js └── yarn.lock └── sdk ├── .babelrc-cjs ├── .babelrc-esm ├── .gitignore ├── .npmignore ├── README.md ├── make.js ├── package.json ├── scripts └── upload_library.js ├── src ├── asset.js ├── assets │ ├── banner.png │ └── thumbnail.png ├── collection.js ├── dirname.js ├── helpers.js ├── index.js ├── lua │ ├── aos-sqlite.wasm │ ├── aos.wasm │ ├── aos2.lua │ ├── aos2_0_1.wasm │ ├── atomic-asset.lua │ ├── atomic-note-library.lua │ ├── atomic-note.lua │ ├── collection-registry.lua │ ├── collection.lua │ ├── notebook.lua │ ├── profile.lua │ ├── profile000.lua │ ├── proxy.lua │ ├── registry.lua │ └── registry000.lua ├── note.js ├── notebook.js ├── profile.js └── utils.js ├── test ├── README.md └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atomic Notes 2 | 3 | ![](./assets/cover.png) 4 | 5 | Atomic Note ia a new social primitive on AO, and a building block for horizontally scalable fully decentralized social applications. 6 | 7 | - [aoNote SDK Reference](./sdk) 8 | - [Testing Framework](./sdk/test) 9 | -------------------------------------------------------------------------------- /app/.env-sample: -------------------------------------------------------------------------------- 1 | VITE_PROCESS_ID=6Z6aOJ7N2IJsVd7yNJrdw5eH_Ccy06cc7lWtu3SvhSA 2 | VITE_TITLE=Tomo | Permaweb Hacker 3 | VITE_DESCRIPTION=I hack, therefore I am. 4 | VITE_IMAGE=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/cover.png 5 | VITE_ICON=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/tomo.png 6 | VITE_PROFILE_NAME=Tomo 7 | VITE_PROFILE_DESCRIPTION=Permaweb Hacker 8 | VITE_PROFILE_IMAGE=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/tomo.png 9 | VITE_PROFILE_X=0xTomo 10 | VITE_PROFILE_GITHUB=ocrybit -------------------------------------------------------------------------------- /app/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # PermaCMS on Arweave/AO 2 | 3 | This is a permanent CMS deployed on Arweave and managed by AO. 4 | 5 | The SPA instance needs to be deployed once and articles will be updated by the assigned AO process. 6 | 7 | ***This is the very first alpha release, please expect bugs and use it with caution and managed.**** 8 | 9 | ## Clone the Repo 10 | 11 | ```bash 12 | git clone https://github.com/ocrybit/perma-cms.git 13 | cd perma-cms 14 | npm install 15 | ``` 16 | 17 | ## Prepare AOS Process 18 | 19 | In your terminal, go to the `lua` directory and start AOS. 20 | 21 | ```bash 22 | cd lua 23 | aos cms 24 | ``` 25 | 26 | Then load the lua script to add handlers to your process. 27 | 28 | ```bash 29 | .load cms.lua 30 | ``` 31 | You need to take note of the `ao process` txid and you need to make the same Arweave owner account available in your browser. The keyfile should be located at `.aos.json` in your home directory. You can import this to the [Arconnect](https://www.arconnect.io/) browser extension. 32 | 33 | you can quit the process with `Ctrl-C`. Now you are all set to run the CMS. 34 | 35 | ## `.env` 36 | 37 | You could update the site owner profile later, but due to the static website nature, predefining site details in `.env` will allow social media / search engines to get and display the site information. 38 | 39 | ```bash 40 | VITE_PROCESS_ID=6Z6aOJ7N2IJsVd7yNJrdw5eH_Ccy06cc7lWtu3SvhSA 41 | VITE_TITLE=Tomo | Permaweb Hacker 42 | VITE_DESCRIPTION=I hack, therefore I am. 43 | VITE_IMAGE=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/cover.png 44 | VITE_ICON=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/tomo.png 45 | VITE_PROFILE_NAME=Tomo 46 | VITE_PROFILE_DESCRIPTION=Permaweb Hacker 47 | VITE_PROFILE_IMAGE=https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/tomo.png 48 | VITE_PROFILE_X=0xTomo 49 | VITE_PROFILE_GITHUB=ocrybit 50 | ``` 51 | 52 | ## Deploy on Arweave 53 | 54 | Check if everything works. 55 | 56 | ```bash 57 | yarn dev 58 | ``` 59 | 60 | You could create and update articles at [http://localhost:5173/#/admin](http://localhost:5173/#/admin), or you can do that later. 61 | 62 | If everything is fine, build and deploy the app on Arweave. 63 | 64 | ```bash 65 | yarn build 66 | arkb deploy dist -w path_to_keyfile --auto-confirm 67 | ``` 68 | 69 | Now you get the app URL like [https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U](https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U). 70 | 71 | ## Manage Articles with Built-in Editor 72 | 73 | You can go to the admin page at [https://your.app/#/admin](https://your.app/#/admin), and connect the owner wallet, and start writing articles with the simple built-in editor. You can perform the following actions. 74 | 75 | - Download the MD file 76 | - Upload articles to Arweave 77 | - Add articles to AO (CMS) 78 | - [bonus] Update your profile 79 | 80 | ![](./assets/editor.png) 81 | 82 | ## Upload Markdown Files to Arewave 83 | 84 | In case of using the built-in editor, just hit the `Upload to Arweave` button. 85 | 86 | You could also import markdown files created with external editors like [HackMD](https://hackmd.io), and upload them to Arweave. 87 | 88 | You can use `arkb` to do so. 89 | 90 | ```bash 91 | arkb deploy doc.md -w path_to_keyfile --auto-confirm 92 | ``` 93 | 94 | ## Add Articles to AO 95 | 96 | Then go to the admin page at [https://your.app/#/admin](https://your.app/#/admin) and add the `TxID` to your AO process with a `Title` and an arbitrary `Page ID`. 97 | 98 | Note that only the AO process owner can update, so you need to connect the same account that deployed the process. 99 | 100 | ![](./assets/add-article-form.png) 101 | 102 | Now you can access your article at [https://arweave.net/3W4...6U/#/a/3](https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U/#/a/3). 103 | 104 | Also, it will be listed on the top page at [https://arweave.net/3W4...6U](https://arweave.net/3W4l7Q_w7r7bYlXH9MXAu2lascJm5YsPoCXn6BXGJ6U). 105 | 106 | ![](./assets/home-page.png) 107 | -------------------------------------------------------------------------------- /app/assets/add-article-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/assets/add-article-form.png -------------------------------------------------------------------------------- /app/assets/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/assets/editor.png -------------------------------------------------------------------------------- /app/assets/home-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/assets/home-page.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %VITE_TITLE% 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/lua/cms.lua: -------------------------------------------------------------------------------- 1 | local json = require("json") 2 | local ao = require('ao') 3 | 4 | Articles = Articles or {} 5 | Profile = Profile or nil 6 | 7 | Handlers.add( 8 | "Set-Profile", 9 | Handlers.utils.hasMatchingTag("Action", "Set-Profile"), 10 | function (msg) 11 | assert(msg.From == Owner, 'only owner can execute!') 12 | assert(type(msg.name) == 'string', 'name is required!') 13 | local profile = { name = msg.name } 14 | if msg.description then 15 | profile.description = msg.description 16 | end 17 | if msg.x then 18 | profile.x = msg.x 19 | end 20 | if msg.github then 21 | profile.github = msg.github 22 | end 23 | if msg.image then 24 | profile.image = msg.image 25 | end 26 | if msg.cover then 27 | profile.cover = msg.cover 28 | end 29 | Profile = profile 30 | Handlers.utils.reply("profile updated!")(msg) 31 | end 32 | ) 33 | 34 | Handlers.add( 35 | "Get-Profile", 36 | Handlers.utils.hasMatchingTag("Action", "Get-Profile"), 37 | function (msg) 38 | ao.send({ 39 | Target = msg.From, 40 | Article = json.encode(Profile) 41 | }) 42 | end 43 | ) 44 | 45 | Handlers.add( 46 | "Add", 47 | Handlers.utils.hasMatchingTag("Action", "Add"), 48 | function (msg) 49 | assert(msg.From == Owner, 'only owner can execute!') 50 | assert(type(msg.title) == 'string', 'title is required!') 51 | assert(type(msg.id) == 'string', 'id is required!') 52 | assert(type(msg.txid) == 'string', 'txid is required!') 53 | assert(0 < tonumber(msg.date), 'date must be greater than 0') 54 | assert(not Articles[msg.id], 'article exists!') 55 | local article = { 56 | title = msg.title, 57 | txid = msg.txid, 58 | id = msg.id, 59 | date = tonumber(msg.date) 60 | } 61 | 62 | Articles[msg.id] = article 63 | Handlers.utils.reply("article added!")(msg) 64 | end 65 | ) 66 | 67 | Handlers.add( 68 | "Update", 69 | Handlers.utils.hasMatchingTag("Action", "Update"), 70 | function (msg) 71 | assert(msg.From == Owner, 'only owner can execute!') 72 | assert(type(msg.txid) == 'string', 'txid is required!') 73 | assert(type(msg.id) == 'string', 'id is required!') 74 | assert(Articles[msg.id], 'article does not exist!') 75 | assert(0 < tonumber(msg.date), 'date must be greater than 0') 76 | local article = Articles[msg.id] 77 | if msg.title then 78 | assert(type(msg.title) == 'string', 'title must be string!') 79 | article.title = msg.title 80 | end 81 | article.txid = msg.txid 82 | article.update = tonumber(msg.date) 83 | Articles[msg.id] = article 84 | Handlers.utils.reply("article updated!")(msg) 85 | end 86 | ) 87 | 88 | Handlers.add( 89 | "Delete", 90 | Handlers.utils.hasMatchingTag("Action", "Delete"), 91 | function (msg) 92 | assert(msg.From == Owner, 'only owner can execute!') 93 | assert(type(msg.id) == 'string', 'id is required!') 94 | assert(Articles[msg.id], 'article doesn\'t exist!') 95 | Articles[msg.id] = nil 96 | Handlers.utils.reply("article deleted!")(msg) 97 | end 98 | ) 99 | 100 | Handlers.add( 101 | "List", 102 | Handlers.utils.hasMatchingTag("Action", "List"), 103 | function (msg) 104 | local arr = {} 105 | for _, article in pairs(Articles) do 106 | table.insert(arr, article) 107 | end 108 | 109 | local order = msg.order or "desc" 110 | local limit = msg.limit or #arr 111 | local skip = msg.skip or 0 112 | 113 | table.sort(arr, function(a, b) 114 | if order == "asc" then 115 | return a.date < b.date 116 | else 117 | return a.date > b.date 118 | end 119 | end) 120 | 121 | local start = skip + 1 122 | local finish = start + limit - 1 123 | finish = math.min(finish, #arr) 124 | 125 | local slicedArr = {} 126 | for i = start, finish do 127 | table.insert(slicedArr, arr[i]) 128 | end 129 | local isNext = (finish < #arr) and "true" or "false" 130 | ao.send({ 131 | Target = msg.From, 132 | Articles = json.encode(slicedArr), 133 | Count = tostring(#arr), 134 | Next = isNext 135 | }) 136 | end 137 | ) 138 | 139 | Handlers.add( 140 | "Get", 141 | Handlers.utils.hasMatchingTag("Action", "Get"), 142 | function (msg) 143 | assert(type(msg.id) == 'string', 'id is required!') 144 | ao.send({ 145 | Target = msg.From, 146 | Article = json.encode(Articles[msg.id]) 147 | }) 148 | end 149 | ) 150 | 151 | -------------------------------------------------------------------------------- /app/lua/proxy.lua: -------------------------------------------------------------------------------- 1 | local ao = require("ao") 2 | Handlers.add( 3 | "allow", 4 | Handlers.utils.hasMatchingTag("Action", "Allow"), 5 | function (msg) 6 | ao.addAssignable({ From = msg.From }) 7 | Handlers.utils.reply("allowed!")(msg) 8 | end 9 | ) 10 | 11 | Handlers.add( 12 | "assign", 13 | Handlers.utils.hasMatchingTag("Type", "Process"), 14 | function (msg) 15 | assert(msg.From == msg.Owner, 'only process owner can execute!') 16 | assert(msg.Tags["Content-Type"] == "text/markdown" or msg.Tags["Content-Type"] == "text/plain", 'only markdown and text are allowed!') 17 | ao.send({ 18 | Target = msg.Id, 19 | Data = msg.Data, 20 | Tags = { Action = "Assigned" } 21 | }) 22 | end 23 | ) 24 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tomo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/icons": "^2.1.1", 14 | "@chakra-ui/react": "^2.8.2", 15 | "@emotion/react": "^11.11.4", 16 | "@emotion/styled": "^11.11.5", 17 | "@fortawesome/free-brands-svg-icons": "^6.6.0", 18 | "@fortawesome/react-fontawesome": "^0.2.2", 19 | "@mdxeditor/editor": "^3.8.0", 20 | "@permaweb/aoconnect": "^0.0.56", 21 | "@wooorm/starry-night": "^3.4.0", 22 | "aonote": "^0.8.1", 23 | "arweave": "^1.15.1", 24 | "dayjs": "^1.11.11", 25 | "framer-motion": "^11.3.0", 26 | "hast-util-to-html": "^9.0.1", 27 | "localforage": "^1.10.0", 28 | "markdown-it": "^14.1.0", 29 | "marked": "^13.0.2", 30 | "react": "^18.3.1", 31 | "react-dom": "^18.3.1", 32 | "react-helmet": "^6.1.0", 33 | "react-router-dom": "^6.24.1", 34 | "vite-plugin-node-polyfills": "^0.22.0" 35 | }, 36 | "devDependencies": { 37 | "@types/react": "^18.3.3", 38 | "@types/react-dom": "^18.3.0", 39 | "@vitejs/plugin-react-swc": "^3.5.0", 40 | "eslint": "^8.57.0", 41 | "eslint-plugin-react": "^7.34.2", 42 | "eslint-plugin-react-hooks": "^4.6.2", 43 | "eslint-plugin-react-refresh": "^0.4.7", 44 | "vite": "^5.3.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/public/ao.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/public/arweave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/arweave.png -------------------------------------------------------------------------------- /app/public/atomic-asset.lua: -------------------------------------------------------------------------------- 1 | local bint = require('.bint')(256) 2 | local json = require('json') 3 | 4 | if Name ~= '' then Name = '' end 5 | if Description ~= '' then Description = '' end 6 | if Thumbnail ~= '' then Thumbnail = '' end 7 | --if Collection ~= '' then Collection = '' end 8 | if Creator ~= '' then Creator = '' end 9 | if Ticker ~= '' then Ticker = '' end 10 | if Denomination ~= '' then Denomination = '' end 11 | if not Balances then Balances = { [''] = '' } end 12 | if DateCreated ~= '' then DateCreated = '' end 13 | if not Collections then Collections = {} end 14 | 15 | Transferable = true 16 | 17 | local function checkValidAddress(address) 18 | if not address or type(address) ~= 'string' then 19 | return false 20 | end 21 | 22 | return string.match(address, "^[%w%-_]+$") ~= nil and #address == 43 23 | end 24 | 25 | local function checkValidAmount(data) 26 | return (math.type(tonumber(data)) == 'integer' or math.type(tonumber(data)) == 'float') and bint(data) > 0 27 | end 28 | 29 | local function decodeMessageData(data) 30 | local status, decodedData = pcall(json.decode, data) 31 | 32 | if not status or type(decodedData) ~= 'table' then 33 | return false, nil 34 | end 35 | 36 | return true, decodedData 37 | end 38 | 39 | -- Read process state 40 | Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg) 41 | ao.send({ 42 | Target = msg.From, 43 | Action = 'Read-Success', 44 | Data = json.encode({ 45 | Name = Name, 46 | Description = Description, 47 | Ticker = Ticker, 48 | Denomination = Denomination, 49 | Balances = Balances, 50 | Transferable = Transferable, 51 | Thumbnail = Thumbnail, 52 | Collections = Collections 53 | }) 54 | }) 55 | end) 56 | 57 | -- Transfer balance to recipient (Data - { Recipient, Quantity }) 58 | Handlers.add('Transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), function(msg) 59 | if not Transferable then 60 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Transfers are not allowed' } }) 61 | return 62 | end 63 | 64 | local data = { 65 | Recipient = msg.Tags.Recipient, 66 | Quantity = msg.Tags.Quantity 67 | } 68 | 69 | if checkValidAddress(data.Recipient) and checkValidAmount(data.Quantity) then 70 | -- Transfer is valid, calculate balances 71 | if not Balances[data.Recipient] then 72 | Balances[data.Recipient] = '0' 73 | end 74 | 75 | Balances[msg.From] = tostring(bint(Balances[msg.From]) - bint(data.Quantity)) 76 | Balances[data.Recipient] = tostring(bint(Balances[data.Recipient]) + bint(data.Quantity)) 77 | 78 | -- If new balance zeroes out then remove it from the table 79 | if bint(Balances[msg.From]) <= 0 then 80 | Balances[msg.From] = nil 81 | end 82 | if bint(Balances[data.Recipient]) <= 0 then 83 | Balances[data.Recipient] = nil 84 | end 85 | 86 | local debitNoticeTags = { 87 | Status = 'Success', 88 | Message = 'Balance transferred, debit notice issued', 89 | Recipient = msg.Tags.Recipient, 90 | Quantity = msg.Tags.Quantity, 91 | } 92 | 93 | local creditNoticeTags = { 94 | Status = 'Success', 95 | Message = 'Balance transferred, credit notice issued', 96 | Sender = msg.From, 97 | Quantity = msg.Tags.Quantity, 98 | } 99 | 100 | for tagName, tagValue in pairs(msg) do 101 | if string.sub(tagName, 1, 2) == 'X-' then 102 | debitNoticeTags[tagName] = tagValue 103 | creditNoticeTags[tagName] = tagValue 104 | end 105 | end 106 | 107 | -- Send a debit notice to the sender 108 | ao.send({ 109 | Target = msg.From, 110 | Action = 'Debit-Notice', 111 | Tags = debitNoticeTags, 112 | Data = json.encode({ 113 | Recipient = data.Recipient, 114 | Quantity = tostring(data.Quantity) 115 | }) 116 | }) 117 | 118 | -- Send a credit notice to the recipient 119 | ao.send({ 120 | Target = data.Recipient, 121 | Action = 'Credit-Notice', 122 | Tags = creditNoticeTags, 123 | Data = json.encode({ 124 | Sender = msg.From, 125 | Quantity = tostring(data.Quantity) 126 | }) 127 | }) 128 | end 129 | end) 130 | 131 | -- Mint new tokens (Data - { Quantity }) 132 | Handlers.add('Mint', Handlers.utils.hasMatchingTag('Action', 'Mint'), function(msg) 133 | local decodeCheck, data = decodeMessageData(msg.Data) 134 | 135 | if decodeCheck and data then 136 | -- Check if quantity is present 137 | if not data.Quantity then 138 | ao.send({ Target = msg.From, Action = 'Input-Error', Tags = { Status = 'Error', Message = 'Invalid arguments, required { Quantity }' } }) 139 | return 140 | end 141 | 142 | -- Check if quantity is a valid integer greater than zero 143 | if not checkValidAmount(data.Quantity) then 144 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Quantity must be an integer greater than zero' } }) 145 | return 146 | end 147 | 148 | -- Check if owner is sender 149 | if msg.From ~= Owner then 150 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Only the process owner can mint new tokens' } }) 151 | return 152 | end 153 | 154 | -- Mint request is valid, add tokens to the pool 155 | if not Balances[Owner] then 156 | Balances[Owner] = '0' 157 | end 158 | 159 | Balances[Owner] = tostring(bint(Balances[Owner]) + bint(data.Quantity)) 160 | 161 | ao.send({ Target = msg.From, Action = 'Mint-Success', Tags = { Status = 'Success', Message = 'Tokens minted' } }) 162 | else 163 | ao.send({ 164 | Target = msg.From, 165 | Action = 'Input-Error', 166 | Tags = { 167 | Status = 'Error', 168 | Message = string.format('Failed to parse data, received: %s. %s', msg.Data, 169 | 'Data must be an object - { Quantity }') 170 | } 171 | }) 172 | end 173 | end) 174 | 175 | -- Read balance (Data - { Target }) 176 | Handlers.add('Balance', Handlers.utils.hasMatchingTag('Action', 'Balance'), function(msg) 177 | local decodeCheck, data = decodeMessageData(msg.Data) 178 | 179 | if decodeCheck and data then 180 | -- Check if target is present 181 | if not data.Target then 182 | ao.send({ Target = msg.From, Action = 'Input-Error', Tags = { Status = 'Error', Message = 'Invalid arguments, required { Target }' } }) 183 | return 184 | end 185 | 186 | -- Check if target is a valid address 187 | if not checkValidAddress(data.Target) then 188 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Target is not a valid address' } }) 189 | return 190 | end 191 | 192 | -- Check if target has a balance 193 | if not Balances[data.Target] then 194 | ao.send({ Target = msg.From, Action = 'Read-Error', Tags = { Status = 'Error', Message = 'Target does not have a balance' } }) 195 | return 196 | end 197 | 198 | ao.send({ 199 | Target = msg.From, 200 | Action = 'Read-Success', 201 | Tags = { Status = 'Success', Message = 'Balance received' }, 202 | Data = 203 | Balances[data.Target] 204 | }) 205 | else 206 | ao.send({ 207 | Target = msg.From, 208 | Action = 'Input-Error', 209 | Tags = { 210 | Status = 'Error', 211 | Message = string.format('Failed to parse data, received: %s. %s', msg.Data, 212 | 'Data must be an object - { Target }') 213 | } 214 | }) 215 | end 216 | end) 217 | 218 | -- Read balances 219 | Handlers.add('Balances', Handlers.utils.hasMatchingTag('Action', 'Balances'), 220 | function(msg) ao.send({ Target = msg.From, Action = 'Read-Success', Data = json.encode(Balances) }) end) 221 | 222 | -- Initialize a request to add the uploaded asset to a profile 223 | Handlers.add('Add-Asset-To-Profile', Handlers.utils.hasMatchingTag('Action', 'Add-Asset-To-Profile'), function(msg) 224 | if checkValidAddress(msg.Tags.ProfileProcess) then 225 | -- ao.assign({ Processes = { msg.Tags.ProfileProcess }, Message = ao.id }) 226 | ao.send({ 227 | Target = msg.Tags.ProfileProcess, 228 | Action = 'Add-Uploaded-Asset', 229 | Data = json.encode({ 230 | Id = ao.id, 231 | Quantity = msg.Tags.Quantity or '0' 232 | }) 233 | }) 234 | else 235 | ao.send({ 236 | Target = msg.From, 237 | Action = 'Input-Error', 238 | Tags = { 239 | Status = 'Error', 240 | Message = 'ProfileProcess tag not specified or not a valid Process ID' 241 | } 242 | }) 243 | end 244 | end) 245 | 246 | Handlers.add('Add-To-Collection-Success', Handlers.utils.hasMatchingTag('Action', 'Add-To-Collection-Success'), function(msg) 247 | local exists = false 248 | for i, id in ipairs(Collections) do 249 | if id == msg.From then 250 | exists = true 251 | break 252 | end 253 | end 254 | 255 | if not exists then 256 | table.insert(Collections, msg.From) 257 | end 258 | end) 259 | 260 | Handlers.add('Remove-From-Collection-Success', Handlers.utils.hasMatchingTag('Action', 'Remove-From-Collection-Success'), function(msg) 261 | for i, id in ipairs(Collections) do 262 | if id == msg.From then 263 | table.remove(Collections, i) 264 | break 265 | end 266 | end 267 | end) 268 | -------------------------------------------------------------------------------- /app/public/atomic-notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/atomic-notes.png -------------------------------------------------------------------------------- /app/public/collection.lua: -------------------------------------------------------------------------------- 1 | local json = require('json') 2 | 3 | if Name ~= '' then Name = '' end 4 | if Description ~= '' then Description = '' end 5 | if Creator ~= '' then Creator = '' end 6 | if Banner ~= '' then Banner = '' end 7 | if Thumbnail ~= '' then Thumbnail = '' end 8 | 9 | if DateCreated ~= '' then DateCreated = '' end 10 | if LastUpdate ~= '' then LastUpdate = '' end 11 | 12 | -- Assets: Id[] 13 | if not Assets then Assets = {} end 14 | 15 | local function decodeMessageData(data) 16 | local status, decodedData = pcall(json.decode, data) 17 | 18 | if not status or type(decodedData) ~= 'table' then 19 | return false, nil 20 | end 21 | 22 | return true, decodedData 23 | end 24 | 25 | local function assetExists(assetId) 26 | for _, id in ipairs(Assets) do 27 | if id == assetId then 28 | return true 29 | end 30 | end 31 | return false 32 | end 33 | 34 | local function checkValidAddress(address) 35 | if not address or type(address) ~= 'string' then 36 | return false 37 | end 38 | 39 | return string.match(address, "^[%w%-_]+$") ~= nil and #address == 43 40 | end 41 | 42 | Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg) 43 | ao.send({ 44 | Target = msg.From, 45 | Data = json.encode({ 46 | Name = Name, 47 | Description = Description, 48 | Creator = Creator, 49 | Banner = Banner, 50 | Thumbnail = Thumbnail, 51 | DateCreated = DateCreated, 52 | Assets = Assets 53 | }) 54 | }) 55 | end) 56 | 57 | -- Add or remove assets 58 | Handlers.add('Update-Assets', Handlers.utils.hasMatchingTag('Action', 'Update-Assets'), function(msg) 59 | if msg.From ~= Owner and msg.From ~= ao.id and msg.From ~= Creator then 60 | ao.send({ 61 | Target = msg.From, 62 | Action = 'Authorization-Error', 63 | Tags = { 64 | Status = 'Error', 65 | Message = 'Unauthorized to access this handler' 66 | } 67 | }) 68 | return 69 | end 70 | 71 | local decodeCheck, data = decodeMessageData(msg.Data) 72 | 73 | if decodeCheck and data then 74 | if not data.AssetIds or type(data.AssetIds) ~= 'table' or #data.AssetIds == 0 then 75 | ao.send({ 76 | Target = msg.From, 77 | Action = 'Action-Response', 78 | Tags = { 79 | Status = 'Error', 80 | Message = 'Invalid or empty AssetIds list' 81 | } 82 | }) 83 | return 84 | end 85 | 86 | if not data.UpdateType or (data.UpdateType ~= 'Add' and data.UpdateType ~= 'Remove') then 87 | ao.send({ 88 | Target = msg.From, 89 | Action = 'Action-Response', 90 | Tags = { 91 | Status = 'Error', 92 | Message = 'UpdateType argument required (Add | Remove)' 93 | } 94 | }) 95 | return 96 | end 97 | 98 | if data.UpdateType == 'Add' then 99 | for _, assetId in ipairs(data.AssetIds) do 100 | if not assetExists(assetId) then 101 | table.insert(Assets, assetId) 102 | ao.send({ 103 | Target = assetId, 104 | Action = 'Add-To-Collection-Success', 105 | Tags = {} 106 | }) 107 | end 108 | end 109 | end 110 | 111 | if data.UpdateType == 'Remove' then 112 | for _, assetId in ipairs(data.AssetIds) do 113 | for i, id in ipairs(Assets) do 114 | if id == assetId then 115 | table.remove(Assets, i) 116 | ao.send({ 117 | Target = assetId, 118 | Action = 'Remove-From-Collection-Success', 119 | Tags = {} 120 | }) 121 | break 122 | end 123 | end 124 | end 125 | end 126 | 127 | LastUpdate = msg.Timestamp 128 | 129 | ao.send({ 130 | Target = msg.From, 131 | Action = 'Action-Response', 132 | Tags = { 133 | Status = 'Success', 134 | Message = 'Assets updated successfully' 135 | } 136 | }) 137 | else 138 | ao.send({ 139 | Target = msg.From, 140 | Action = 'Input-Error', 141 | Tags = { 142 | Status = 'Error', 143 | Message = string.format('Failed to parse data, received: %s. %s', 144 | msg.Data, 145 | 'Data must be an object - { AssetIds: [], UpdateType }') 146 | } 147 | }) 148 | end 149 | end) 150 | 151 | -- Initialize a request to add the uploaded asset to a profile 152 | Handlers.add('Add-Collection-To-Profile', Handlers.utils.hasMatchingTag('Action', 'Add-Collection-To-Profile'), function(msg) 153 | if checkValidAddress(msg.Tags.ProfileProcess) then 154 | -- ao.assign({Processes = {msg.Tags.ProfileProcess}, Message = ao.id}) 155 | ao.send({ 156 | Target = msg.Tags.ProfileProcess, 157 | Action = 'Add-Collection', 158 | Data = json.encode({ 159 | Id = ao.id, 160 | Name = Name 161 | }) 162 | }) 163 | else 164 | ao.send({ 165 | Target = msg.From, 166 | Action = 'Input-Error', 167 | Tags = { 168 | Status = 'Error', 169 | Message = 'ProfileProcess tag not specified or not a valid Process ID' 170 | } 171 | }) 172 | end 173 | end) 174 | -------------------------------------------------------------------------------- /app/public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/cover.png -------------------------------------------------------------------------------- /app/public/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/icon.png -------------------------------------------------------------------------------- /app/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/logo.png -------------------------------------------------------------------------------- /app/public/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/app/public/x.png -------------------------------------------------------------------------------- /app/public/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/App.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height:100%; 3 | coloir:#222326; 4 | } 5 | 6 | #root { 7 | height:100%; 8 | min-width: 100%; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /app/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { Image, Button, Flex, Box, useToast } from "@chakra-ui/react" 3 | import { Link } from "react-router-dom" 4 | import { map } from "ramda" 5 | import { getAddr, getProf } from "./lib/utils" 6 | 7 | import Header from "./components/Header" 8 | const roboto = { 9 | fontFamily: `"Roboto Mono", monospace`, 10 | fontOpticalSizing: "auto", 11 | fontWeight: 600, 12 | fontStyle: "normal", 13 | } 14 | function App() { 15 | const [address, setAddress] = useState(null) 16 | const [profile, setProfile] = useState(null) 17 | const [init, setInit] = useState(false) 18 | const t = useToast() 19 | const boxW = ["100%", null, "50%", 1 / 3] 20 | useEffect(() => getAddr({ setAddress, setInit, t }), []) 21 | useEffect( 22 | () => getProf({ address, setProfile, setInit, setAddress, t }), 23 | [address], 24 | ) 25 | 26 | return ( 27 | <> 28 |
31 | 32 | 33 | 34 | 35 | Atomic Notes 36 | 37 | A New Decentralized Social Primitive. 38 | 39 | 40 | 41 | 52 | 53 | 57 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 82 | 83 | Atomic Assets 84 | 85 | 86 | Data, licenses and smart contracts are all stored together on 87 | Arweave as tradable atomic assets on UCMs like{" "} 88 | 89 | 90 | BazAR 91 | 92 | 93 | . 94 | 95 | 96 | 97 | 98 | 105 | 106 | Universal Data License 107 | 108 | 109 | Profit sharing and royalty distribution are to be automated by 110 | Universal Content Marketplaces with onchain UDLs. 111 | 112 | 113 | 114 | 115 | 122 | 123 | Editable Notes on AO 124 | 125 | 126 | AO enables delta updates on permanent data, co-authoring, and 127 | semantic version control with AO processes / smart contracts. 128 | 129 | 130 | 131 | 132 | 139 | 140 | Built for Developers 141 | 142 | 143 | Atomic notes introduce a new social primitive built with a 144 | developer-first approach, providing an SDK and APIs. 145 | 146 | 147 | 148 | 149 | 156 | 157 | ArNS Integration 158 | 159 | 160 | 161 | 162 | ArNS 163 | 164 | 165 | domains provide censorship-resistant access to your content 166 | through thousands of decentralized gateways. 167 | 168 | 169 | 170 | 171 | 178 | 179 | Atomic Timelines 180 | 181 | 182 | Atomic Timelines are horizontally scalable social timelines 183 | built with Atomic Notes, distributing rewards to content 184 | creators. 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 196 | 201 | 202 | 203 | 204 | 209 | 210 | 211 | 212 | 213 | ONLY POSSIBLE ON ⓐ ARWEAVE 214 | 215 | 216 | 217 | 218 | 219 | 220 | ) 221 | } 222 | 223 | export default App 224 | -------------------------------------------------------------------------------- /app/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Spinner, 4 | Image, 5 | Flex, 6 | Box, 7 | Menu, 8 | MenuButton, 9 | MenuList, 10 | MenuItem, 11 | MenuItemOption, 12 | MenuGroup, 13 | MenuOptionGroup, 14 | MenuDivider, 15 | } from "@chakra-ui/react" 16 | import { 17 | msg, 18 | err, 19 | ao, 20 | getAoProfile, 21 | getAddr, 22 | getProf, 23 | getPFP, 24 | } from "../lib/utils" 25 | import { useParams } from "react-router-dom" 26 | import { Link } from "react-router-dom" 27 | import { useState, useEffect } from "react" 28 | import lf from "localforage" 29 | 30 | function Header({ 31 | children, 32 | address, 33 | profile, 34 | init, 35 | setAddress, 36 | setProfile, 37 | setInit, 38 | t, 39 | }) { 40 | return ( 41 | 54 | 58 | 59 | 67 | {children ? ( 68 | children 69 | ) : ( 70 | 71 | 72 | 73 | Atomic Notes 74 | 75 | 76 | )} 77 | 78 | {!init ? ( 79 | 80 | ) : profile ? ( 81 | <> 82 | 83 | 88 | <> 89 | {profile ? ( 90 | 91 | 92 | {profile.DisplayName} 93 | 100 | 101 | 102 | ) : ( 103 | 114 | {address.slice(0, 5)}...{address.slice(-5)} 115 | 116 | )} 117 | 118 | 119 | 120 | 121 | 122 | 123 | Profile 124 | 125 | 126 | 127 | 128 | 129 | 130 | Create Note 131 | 132 | 133 | 134 | 135 | 136 | 137 | Create Notebook 138 | 139 | 140 | 141 | { 143 | setAddress(null) 144 | setProfile(null) 145 | await lf.removeItem("address") 146 | await lf.removeItem(`profile-${address}`) 147 | msg(t, "Wallet Disconnected!", null, "info") 148 | }} 149 | > 150 | 151 | Sign Out 152 | 153 | 154 | 155 | 156 | 157 | ) : ( 158 | 182 | )} 183 | 184 | 185 | ) 186 | } 187 | 188 | export default Header 189 | -------------------------------------------------------------------------------- /app/src/components/NoteCard.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Tag, 3 | IconButton, 4 | Menu, 5 | MenuButton, 6 | MenuList, 7 | MenuItem, 8 | Button, 9 | Tabs, 10 | TabList, 11 | Tab, 12 | TabPanels, 13 | TabPanel, 14 | Text, 15 | Flex, 16 | Input, 17 | Box, 18 | Image, 19 | Card, 20 | CardHeader, 21 | Heading, 22 | } from "@chakra-ui/react" 23 | import { validAddress, getPFP, gateway_url } from "../lib/utils" 24 | import { Link } from "react-router-dom" 25 | import { DeleteIcon, AddIcon, EditIcon } from "@chakra-ui/icons" 26 | import dayjs from "dayjs" 27 | import { map } from "ramda" 28 | 29 | const NoteCard = ({ 30 | note, 31 | bazar = false, 32 | addToNotebook = () => {}, 33 | deleteFromNotebook, 34 | profile, 35 | notebooks = [], 36 | nolinks, 37 | variant = "enclosed", 38 | navigate, 39 | diff = [], 40 | isCreator, 41 | bookmap = {}, 42 | fileInputRef, 43 | thumb64, 44 | onChange, 45 | }) => { 46 | const props = 47 | variant === "enclosed" 48 | ? { 49 | mt: 6, 50 | mb: 2, 51 | p: 4, 52 | sx: { border: "1px solid #222326", borderRadius: "5px" }, 53 | } 54 | : { 55 | p: 4, 56 | sx: { 57 | borderBottom: "1px solid rgb(226,232,240)", 58 | cursor: "pointer", 59 | }, 60 | } 61 | return ( 62 | { 66 | if (!nolinks) navigate(`/n/${note.id}`) 67 | }} 68 | > 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {note.title ?? "No Title"} 77 | 78 | {note.description} 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {dayjs((note.date ?? Date.now()) * 1).format("MMM DD")} 88 | 89 | { 94 | e.stopPropagation() 95 | if (!nolinks) navigate(`/u/${profile.ProfileId}`) 96 | }} 97 | > 98 | 99 | 100 | {profile.DisplayName} 101 | 102 | 103 | 104 | {map(v => ( 105 | { 114 | e.stopPropagation() 115 | if (!nolinks) navigate(`/b/${v.id}`) 116 | }} 117 | > 118 | {v.Name} 119 | 120 | ))(notebooks)} 121 | 122 | 123 | {nolinks || !isCreator || diff.length === 0 ? null : ( 124 | 125 | e.stopPropagation()} 127 | size="xs" 128 | colorScheme="gray" 129 | variant="outline" 130 | as={IconButton} 131 | icon={} 132 | sx={{ 133 | border: "1px solid #222326", 134 | ":hover": { 135 | cursor: "pointer", 136 | opacity: 0.75, 137 | }, 138 | }} 139 | /> 140 | e.stopPropagation()}> 141 | {map(v3 => { 142 | const v2 = bookmap[v3] 143 | return !v2 ? null : ( 144 | 145 | 146 | {v2.Name} 147 | 148 | 149 | ) 150 | })(diff)} 151 | 152 | 153 | )} 154 | {!bazar ? null : ( 155 | <> 156 | 160 | 179 | 180 | 181 | 200 | 201 | 205 | 224 | 225 | 226 | )} 227 | {nolinks || !isCreator ? null : ( 228 | 248 | )} 249 | {nolinks || !isCreator || !deleteFromNotebook ? null : ( 250 | 267 | )} 268 | 269 | 270 | {onChange ? ( 271 | fileInputRef.current.click()} 287 | size="xl" 288 | align="center" 289 | justify="center" 290 | > 291 | 298 | {note.thumb64 || validAddress(note.thumbnail) ? null : ( 299 | 300 | )} 301 | 302 | ) : !note.thumbnail ? null : ( 303 | 317 | )} 318 | 319 | ) 320 | } 321 | 322 | export default NoteCard 323 | -------------------------------------------------------------------------------- /app/src/components/NotebookCard.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Tag, 3 | IconButton, 4 | Menu, 5 | MenuButton, 6 | MenuList, 7 | MenuItem, 8 | Button, 9 | Tabs, 10 | TabList, 11 | Tab, 12 | TabPanels, 13 | TabPanel, 14 | Text, 15 | Flex, 16 | Input, 17 | Box, 18 | Image, 19 | Card, 20 | CardHeader, 21 | Avatar, 22 | Heading, 23 | } from "@chakra-ui/react" 24 | import { validAddress, gateway_url, getThumb } from "../lib/utils" 25 | import { Link } from "react-router-dom" 26 | import { DeleteIcon, AddIcon, EditIcon } from "@chakra-ui/icons" 27 | import dayjs from "dayjs" 28 | import { map } from "ramda" 29 | 30 | const NotebookCard = ({ 31 | bmap, 32 | note, 33 | bazar = false, 34 | addToNotebook = () => {}, 35 | deleteFromNotebook, 36 | profile, 37 | notebooks = [], 38 | nolinks, 39 | variant = "enclosed", 40 | navigate, 41 | diff = [], 42 | isCreator, 43 | bookmap = {}, 44 | fileInputRef, 45 | thumb64, 46 | onChange, 47 | }) => { 48 | const props = 49 | variant === "enclosed" 50 | ? { 51 | mt: 6, 52 | mb: 2, 53 | p: 4, 54 | sx: { border: "1px solid #222326", borderRadius: "5px" }, 55 | } 56 | : { 57 | p: 4, 58 | sx: { 59 | borderBottom: "1px solid rgb(226,232,240)", 60 | cursor: "pointer", 61 | }, 62 | } 63 | return ( 64 | 65 | 66 | 67 | 68 | {onChange ? ( 69 | fileInputRef.current.click()} 86 | size="xl" 87 | align="center" 88 | justify="center" 89 | > 90 | 97 | {note.thumb64 || validAddress(note.thumbnail) ? null : ( 98 | 99 | )} 100 | 101 | ) : !note.thumbnail ? null : ( 102 | 103 | )} 104 | 105 | 106 | 107 | {note.title} 108 | 109 | {note.description} 110 | 111 | 112 | {dayjs((note.date ?? Date.now()) * 1).format("MMM DD")} 113 | 114 | 120 | {note.assets?.length ?? 0} Notes 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | ) 130 | } 131 | 132 | export default NotebookCard 133 | -------------------------------------------------------------------------------- /app/src/lib/svgs.jsx: -------------------------------------------------------------------------------- 1 | const circleNotch = ( 2 | 8 | 18 | 26 | 27 | 28 | ) 29 | 30 | export { circleNotch } 31 | -------------------------------------------------------------------------------- /app/src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import { dryrun } from "@permaweb/aoconnect" 2 | import lf from "localforage" 3 | import { fromPairs, map, prop, includes, clone } from "ramda" 4 | import { AR, Profile, Notebook, Note } from "aonote" 5 | 6 | let graphql_url = "https://arweave.net/graphql" 7 | let default_thumbnail = "9v2GrtXpVpPWf9KBuTBdClARjjcDA3NqxFn8Kbn1f2M" 8 | let default_banner = "UuEwLRmuNmqLTDcKqgcxDEV1CWIR_uZ6rxzmKjODlrg" 9 | let gateway_url = "https://arweave.net" 10 | 11 | const genOpt = () => { 12 | let env = { ar: {}, ao: {}, profile: {}, note: {}, notebook: {} } 13 | for (const k in import.meta.env) { 14 | if (k.match(/^VITE_/)) { 15 | const k2 = k.replace(/^VITE_/, "").toLowerCase() 16 | const k3 = k2.split("_") 17 | if ( 18 | includes(k3[0], ["ar", "ao", "profile", "note", "notebook"]) && 19 | import.meta.env[k].match(/^\s*$/) === null 20 | ) { 21 | env[k3[0]][k3.slice(1).join("_")] = import.meta.env[k] 22 | } 23 | } 24 | } 25 | let link = { 26 | ar: [], 27 | ao: ["module", "scheduler"], 28 | profile: ["registry", "src"], 29 | note: ["proxy", "src", "lib_src"], 30 | notebook: ["registry", "src"], 31 | } 32 | let namemap = { 33 | ar: {}, 34 | ao: {}, 35 | profile: { src: "profile_src" }, 36 | note: { src: "note_src", lib_src: "notelib_src" }, 37 | notebook: { src: "notebook_src" }, 38 | } 39 | let opt = { ar: {}, ao: {}, profile: {}, note: {}, notebook: {} } 40 | opt.ar = env.ar 41 | for (const k in link) { 42 | for (const v of link[k]) { 43 | if (env[k][v]) opt[k][namemap[k][v] ?? v] = env[k][v] 44 | } 45 | } 46 | if (env.ao.mu || env.ao.cu || env.ao.gateway) { 47 | opt.ao.aoconnect = {} 48 | if (env.ao.mu) opt.ao.aoconnect.MU_URL = env.ao.mu 49 | if (env.ao.cu) opt.ao.aoconnect.CU_URL = env.ao.cu 50 | if (env.ao.gateway) opt.ao.aoconnect.GATEWAY_URL = env.ao.gateway 51 | } 52 | if (import.meta.env.VITE_THUMBNAIL) { 53 | opt.note.thumbnail = import.meta.env.VITE_THUMBNAIL 54 | opt.notebook.thumbnail = import.meta.env.VITE_THUMBNAIL 55 | } 56 | if (import.meta.env.VITE_BANNER) { 57 | opt.note.banner = import.meta.env.VITE_BANNER 58 | opt.notebook.banner = import.meta.env.VITE_BANNER 59 | } 60 | 61 | const _ar = clone(opt.ar) 62 | const _ao = clone(opt.ao) 63 | const _profile = clone(opt.profile) 64 | const _note = clone(opt.ntoe) 65 | const _notebook = clone(opt.ntoebook) 66 | 67 | opt.ao.ar = _ar 68 | 69 | opt.profile.ao = _ao 70 | opt.profile.ar = _ar 71 | 72 | opt.notebook.ao = _ao 73 | opt.notebook.ar = _ar 74 | opt.notebook.profile = _profile 75 | 76 | opt.note.ao = _ao 77 | opt.note.ar = _ar 78 | opt.note.profile = _profile 79 | return opt 80 | } 81 | 82 | const opt = genOpt() 83 | 84 | const _nb = new Notebook(opt.notebook) 85 | 86 | graphql_url = `${_nb.ar.protocol}://${_nb.ar.host}:${_nb.ar.port}/graphql` 87 | gateway_url = `${_nb.ar.protocol}://${_nb.ar.host}:${_nb.ar.port}` 88 | default_thumbnail = _nb.thumbnail 89 | default_banner = _nb.banner 90 | 91 | const getArticles = async ({ limit, skip } = {}) => { 92 | let tags = [{ name: "Action", value: "List" }] 93 | if (limit) tags.push({ name: "limit", value: limit.toString() }) 94 | if (skip) tags.push({ name: "skip", value: skip.toString() }) 95 | const result = await dryrun({ 96 | process: import.meta.env.VITE_PROCESS_ID, 97 | tags, 98 | }) 99 | return { 100 | articles: JSON.parse(result.Messages[0].Tags[6].value), 101 | next: JSON.parse(result.Messages[0].Tags[7].value), 102 | count: JSON.parse(result.Messages[0].Tags[8].value), 103 | } 104 | } 105 | 106 | const getProfile = async () => { 107 | const result = await dryrun({ 108 | process: import.meta.env.VITE_PROCESS_ID, 109 | tags: [{ name: "Action", value: "Get-Profile" }], 110 | }) 111 | return JSON.parse(result.Messages[0].Tags[6].value) 112 | } 113 | 114 | const ao = 115 | "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMTEuOTciIHZpZXdCb3g9IjAgMCA0MjkgMjE0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMCAyMTRINzEuMzc2M0w4NS45NDI5IDE3NC42MUw1My4xNjgxIDEwNy41TDAgMjE0WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTE4OS4zNjYgMTYwLjc1TDEwOS45NzggMUw4NS45NDI5IDU1LjcwODlMMTYwLjk2MSAyMTRIMjE1TDE4OS4zNjYgMTYwLjc1WiIgZmlsbD0iYmxhY2siLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zMjIgMjE0QzM4MS4wOTQgMjE0IDQyOSAxNjYuMDk0IDQyOSAxMDdDNDI5IDQ3LjkwNTUgMzgxLjA5NCAwIDMyMiAwQzI2Mi45MDYgMCAyMTUgNDcuOTA1NSAyMTUgMTA3QzIxNSAxNjYuMDk0IDI2Mi45MDYgMjE0IDMyMiAyMTRaTTMyMiAxNzJDMzU3Ljg5OSAxNzIgMzg3IDE0Mi44OTkgMzg3IDEwN0MzODcgNzEuMTAxNSAzNTcuODk5IDQyIDMyMiA0MkMyODYuMTAxIDQyIDI1NyA3MS4xMDE1IDI1NyAxMDdDMjU3IDE0Mi44OTkgMjg2LjEwMSAxNzIgMzIyIDE3MloiIGZpbGw9ImJsYWNrIi8+Cjwvc3ZnPg==" 116 | 117 | const arweave_logo = 118 | "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMzQuOSAxMzUuNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTM0LjkgMTM1LjQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojMjIyMzI2O30KCS5zdDF7ZmlsbDpub25lO3N0cm9rZTojMjIyMzI2O3N0cm9rZS13aWR0aDo5LjY1MjE7c3Ryb2tlLW1pdGVybGltaXQ6MTA7fQo8L3N0eWxlPgo8Zz4KCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik03Ny42LDkxLjVjLTAuMy0wLjYtMC42LTEuMy0wLjgtMi4xYy0wLjItMC44LTAuNC0xLjYtMC41LTIuNWMtMC43LDAuOC0xLjUsMS41LTIuNCwyLjIKCQljLTAuOSwwLjctMS45LDEuMy0zLDEuOGMtMS4xLDAuNS0yLjMsMC45LTMuNywxLjJjLTEuMywwLjMtMi44LDAuNC00LjQsMC40Yy0yLjUsMC00LjktMC40LTctMS4xYy0yLjEtMC43LTMuOS0xLjgtNS41LTMuMQoJCWMtMS41LTEuMy0yLjctMi45LTMuNi00LjdjLTAuOS0xLjgtMS4zLTMuOC0xLjMtNS45YzAtNS4yLDEuOS05LjMsNS44LTEyLjFjMy45LTIuOSw5LjctNC4zLDE3LjQtNC4zaDcuMXYtMi45CgkJYzAtMi40LTAuOC00LjMtMi4zLTUuN2MtMS42LTEuNC0zLjgtMi4xLTYuNy0yLjFjLTIuNiwwLTQuNSwwLjYtNS43LDEuN2MtMS4yLDEuMS0xLjgsMi42LTEuOCw0LjVINDYuNWMwLTIuMSwwLjUtNC4xLDEuNC02CgkJYzAuOS0xLjksMi4zLTMuNiw0LjEtNWMxLjgtMS40LDQtMi42LDYuNi0zLjRjMi42LTAuOCw1LjUtMS4zLDguOS0xLjNjMywwLDUuOCwwLjQsOC40LDEuMWMyLjYsMC43LDQuOCwxLjgsNi43LDMuMwoJCWMxLjksMS40LDMuNCwzLjIsNC40LDUuNGMxLjEsMi4yLDEuNiw0LjcsMS42LDcuNnYyMS4zYzAsMi43LDAuMiw0LjksMC41LDYuNmMwLjMsMS43LDAuOCwzLjIsMS41LDQuNXYwLjhINzcuNnogTTY1LjUsODIuNgoJCWMxLjMsMCwyLjUtMC4yLDMuNi0wLjVjMS4xLTAuMywyLjEtMC43LDMtMS4yYzAuOS0wLjUsMS42LTEsMi4zLTEuN2MwLjYtMC42LDEuMS0xLjMsMS41LTEuOXYtOC41aC02LjVjLTIsMC0zLjcsMC4yLTUuMSwwLjYKCQljLTEuNCwwLjQtMi42LDAuOS0zLjQsMS42Yy0wLjksMC43LTEuNSwxLjUtMiwyLjVjLTAuNCwxLTAuNiwyLTAuNiwzLjFjMCwxLjcsMC42LDMuMSwxLjgsNC4zQzYxLjIsODIsNjMsODIuNiw2NS41LDgyLjZ6Ii8+CjwvZz4KPGNpcmNsZSBjbGFzcz0ic3QxIiBjeD0iNjcuMyIgY3k9IjY4LjEiIHI9IjYxLjciLz4KPC9zdmc+Cg==" 119 | 120 | const defaultProfile = profile => { 121 | return ( 122 | profile ?? { 123 | name: import.meta.env.VITE_PROFILE_NAME ?? "John Doe", 124 | description: 125 | import.meta.env.VITE_PROFILE_DESCRIPTION ?? "Set up your profile", 126 | image: import.meta.env.VITE_PROFILE_IMAGE ?? arweave_logo, 127 | x: import.meta.env.VITE_PROFILE_X ?? null, 128 | github: import.meta.env.VITE_PROFILE_GITHUB ?? null, 129 | } 130 | ) 131 | } 132 | const action = value => tag("Action", value) 133 | const tag = (name, value) => ({ name, value }) 134 | const wait = ms => new Promise(res => setTimeout(() => res(), ms)) 135 | 136 | const getInfo = async prid => { 137 | const prof = new Profile(opt.profile) 138 | return await prof.info({ id: prid }) 139 | } 140 | 141 | const getBookInfo = async pid => { 142 | const book = new Notebook({ pid, ...opt.notebook }) 143 | return await book.info() 144 | } 145 | 146 | const getAoProfile = async address => { 147 | let prof = null 148 | const prid = await getProfileId(address) 149 | if (prid) { 150 | const _prof = new Profile(opt.profile) 151 | prof = await _prof.profile({ id: prid }) 152 | } 153 | return prof 154 | } 155 | 156 | const getProfileId = async address => { 157 | const prof = new Profile(opt.profile) 158 | return (await prof.ids({ addr: address }))[0] ?? null 159 | } 160 | 161 | const getAoProfiles = async ids => { 162 | const prof = new Profile({ ...opt.profile }) 163 | return await prof.profiles({ ids }) 164 | } 165 | 166 | const getProf = ({ address, setAddress, setProfile, setInit, t }) => { 167 | ;(async () => { 168 | if (address) { 169 | let _profile = await lf.getItem(`profile-${address}`) 170 | let isCache = false 171 | if (_profile) { 172 | setProfile(_profile) 173 | setInit(true) 174 | isCache = true 175 | } 176 | _profile = await getAoProfile(address) 177 | setProfile(_profile) 178 | if (_profile) { 179 | await lf.setItem(`profile-${address}`, _profile) 180 | if (!isCache) msg(t, "Wallet Connected!") 181 | } else { 182 | const prof = await new Profile(opt.profile).init() 183 | err( 184 | t, 185 | "No Profile Found!", 186 | "You have no AO profile. Create one on BazAR.", 187 | ) 188 | await lf.removeItem(`address`) 189 | setAddress(null) 190 | } 191 | setInit(true) 192 | } 193 | })() 194 | } 195 | const getAddr = ({ setAddress, setInit }) => { 196 | ;(async () => { 197 | const userAddress = await lf.getItem("address") 198 | if (userAddress) { 199 | setAddress(userAddress) 200 | } else { 201 | setInit(true) 202 | } 203 | })() 204 | } 205 | 206 | const getNotes = async pids => { 207 | const query = `query { 208 | transactions(ids: [${map(v => `"${v}"`)(pids).join(",")}], tags: { name: "Asset-Type", values: ["Atomic-Note"]}) { 209 | edges { 210 | node { 211 | id 212 | owner { address } 213 | tags { name value } 214 | } 215 | } 216 | } 217 | }` 218 | const res = await fetch(graphql_url, { 219 | method: "POST", 220 | headers: { "Content-Type": "application/json" }, 221 | body: JSON.stringify({ query }), 222 | }).then(r => r.json()) 223 | return map(prop("node"))(res?.data?.transactions?.edges ?? []) 224 | } 225 | const getBooks = async pids => { 226 | const query = `query { 227 | transactions(ids: [${map(v => `"${v}"`)(pids).join(",")}], tags: { name: "Collection-Type", values: ["Atomic-Notes"]}) { 228 | edges { 229 | node { 230 | id 231 | owner { address } 232 | tags { name value } 233 | } 234 | } 235 | } 236 | }` 237 | const res = await fetch(graphql_url, { 238 | method: "POST", 239 | headers: { "Content-Type": "application/json" }, 240 | body: JSON.stringify({ query }), 241 | }).then(r => r.json()) 242 | return map(prop("node"))(res?.data?.transactions?.edges ?? []) 243 | } 244 | 245 | const tags = tags => fromPairs(map(v => [v.name, v.value])(tags)) 246 | const ltags = tags => fromPairs(map(v => [v.name.toLowerCase(), v.value])(tags)) 247 | const badWallet = async (t, addr) => { 248 | let isValid = false 249 | try { 250 | await window.arweaveWallet.connect(["ACCESS_ADDRESS", "SIGN_TRANSACTION"]) 251 | const addr2 = await window.arweaveWallet.getActiveAddress() 252 | isValid = addr === addr2 253 | } catch (e) {} 254 | if (!isValid) 255 | err(t, "The wrong wallet", `use ${addr} or reconnect the wallet.`) 256 | return !isValid 257 | } 258 | const validAddress = addr => /^[a-zA-Z0-9_-]{43}$/.test(addr) 259 | 260 | const gTag = (_tags, name) => { 261 | const __tags = tags(_tags) 262 | return __tags[name] ?? null 263 | } 264 | 265 | const tagEq = (tags, name, val = null) => { 266 | const _tags = gTag(tags, name) 267 | return _tags === val 268 | } 269 | 270 | const searchTag = (res, name, val) => { 271 | for (let v of res.Messages || []) { 272 | if (tagEq(v.Tags || {}, name, val)) return v 273 | } 274 | return null 275 | } 276 | 277 | const msg = (t, title = "Success!", description, status = "success") => { 278 | t({ 279 | title, 280 | description, 281 | status, 282 | isClosable: true, 283 | }) 284 | } 285 | 286 | const err = ( 287 | t, 288 | title = "something went wrong!", 289 | description, 290 | status = "success", 291 | ) => msg(t, title, description, "error") 292 | 293 | const getPFP = profile => 294 | profile.ProfileImage === "None" 295 | ? "./logo.png" 296 | : `${gateway_url}/${profile.ProfileImage}` 297 | const getThumb = book => 298 | book.Thumbnail === "None" || !book.Thumbnail === "" || !book.Thumbnail 299 | ? "/logo.png" 300 | : `${gateway_url}/${book.Thumbnail}` 301 | 302 | const note_src_data = `if Name ~= '' then Name = '' end 303 | if Description ~= '' then Description = '' end 304 | if Thumbnail ~= '' then Thumbnail = '' end 305 | if Creator ~= '' then Creator = '' end 306 | if Ticker ~= '' then Ticker = '' end 307 | if Denomination ~= '' then Denomination = '' end 308 | if not Balances then Balances = { [''] = '' } end 309 | if DateCreated ~= '' then DateCreated = '' end 310 | if not Collections then Collections = {} end 311 | 312 | ao.addAssignable("LIBRARY", { Id = '' }) 313 | ` 314 | export { 315 | note_src_data, 316 | getThumb, 317 | default_thumbnail, 318 | default_banner, 319 | opt, 320 | err, 321 | msg, 322 | gTag, 323 | tagEq, 324 | searchTag, 325 | validAddress, 326 | badWallet, 327 | getBooks, 328 | ltags, 329 | tags, 330 | getNotes, 331 | getAddr, 332 | getAoProfile, 333 | getProf, 334 | wait, 335 | getArticles, 336 | getProfile, 337 | getProfileId, 338 | defaultProfile, 339 | ao, 340 | arweave_logo, 341 | action, 342 | tag, 343 | getInfo, 344 | getBookInfo, 345 | getPFP, 346 | graphql_url, 347 | gateway_url, 348 | getAoProfiles, 349 | } 350 | -------------------------------------------------------------------------------- /app/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { RouterProvider, createHashRouter } from "react-router-dom" 3 | import { ChakraProvider } from "@chakra-ui/react" 4 | import ReactDOM from "react-dom/client" 5 | import App from "./App.jsx" 6 | import Note from "./pages/Note" 7 | import User from "./pages/User" 8 | import CreateNote from "./pages/CreateNote" 9 | import Book from "./pages/Book" 10 | import CreateBook from "./pages/CreateBook" 11 | 12 | const router = createHashRouter([ 13 | { 14 | path: "/", 15 | element: , 16 | }, 17 | { 18 | path: "/u/:id", 19 | element: , 20 | }, 21 | { 22 | path: "/n/:id", 23 | element: , 24 | }, 25 | { 26 | path: "/b/:id", 27 | element: , 28 | }, 29 | { 30 | path: "/n/:pid/edit", 31 | element: , 32 | }, 33 | { 34 | path: "/b/:pid/edit", 35 | element: , 36 | }, 37 | ]) 38 | 39 | ReactDOM.createRoot(document.getElementById("root")).render( 40 | 41 | 42 | 43 | 44 | , 45 | ) 46 | -------------------------------------------------------------------------------- /app/src/pages/Book.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigate, useParams } from "react-router-dom" 2 | import { Link } from "react-router-dom" 3 | import { useState, useEffect } from "react" 4 | import { Notebook } from "aonote" 5 | import Header from "../components/Header" 6 | import NoteCard from "../components/NoteCard" 7 | import { Helmet } from "react-helmet" 8 | import { 9 | getInfo, 10 | getNotes, 11 | getBookInfo, 12 | getAddr, 13 | getProf, 14 | ltags, 15 | tags, 16 | badWallet, 17 | msg, 18 | err, 19 | getPFP, 20 | opt, 21 | gateway_url, 22 | getThumb, 23 | } from "../lib/utils" 24 | import dayjs from "dayjs" 25 | import { 26 | Button, 27 | Tabs, 28 | TabList, 29 | Tab, 30 | TabPanels, 31 | TabPanel, 32 | Text, 33 | Flex, 34 | Box, 35 | Image, 36 | Card, 37 | CardHeader, 38 | Avatar, 39 | Heading, 40 | useToast, 41 | } from "@chakra-ui/react" 42 | import { o, sortBy, map, pluck, fromPairs, clone, reject } from "ramda" 43 | 44 | function User({}) { 45 | const { id } = useParams() 46 | const navigate = useNavigate() 47 | const t = useToast() 48 | const [address, setAddress] = useState(null) 49 | const [profile, setProfile] = useState(null) 50 | const [init, setInit] = useState(false) 51 | const [user, setUser] = useState(null) 52 | const [notes, setNotes] = useState([]) 53 | const [book, setBook] = useState(null) 54 | const [assetmap, setAssetMap] = useState({}) 55 | 56 | useEffect(() => getAddr({ setAddress, setInit, t }), []) 57 | useEffect( 58 | () => getProf({ address, setProfile, setInit, setAddress, t }), 59 | [address], 60 | ) 61 | 62 | useEffect(() => { 63 | ;(async () => { 64 | const info = await getBookInfo(id) 65 | if (info) { 66 | setBook(info) 67 | setNotes( 68 | o( 69 | sortBy(v => v["date-created"] * -1), 70 | map(v => { 71 | return { ...ltags(v.tags), id: v.id, owner: v.owner.address } 72 | }), 73 | )(await getNotes(info.Assets)), 74 | ) 75 | let _user = await getInfo(info.Creator) 76 | if (_user) setUser({ ..._user.Profile, id: info.Creator }) 77 | } 78 | })() 79 | }, [id]) 80 | const isCreator = book && book?.Creator === profile?.ProfileId 81 | return ( 82 | <> 83 |
86 | {!book ? null : ( 87 | 88 | 89 | {book.Name} 90 | {!user ? "" : ` | ${user.DisplayName}`} 91 | 92 | 93 | 94 | 98 | 99 | 103 | 107 | 108 | 112 | 113 | )} 114 | 115 | {!book ? null : ( 116 | 117 | 118 | <> 119 | {!book.Banner ? null : ( 120 | 131 | )} 132 | 133 | 134 | 135 | 136 | 142 | 143 | {book.Name} 144 | 145 | {book.Description} 146 | 147 | {!user ? null : ( 148 | 149 | 150 | 151 | {dayjs(book["DateCreated"] * 1).format( 152 | "MMM DD", 153 | )} 154 | 155 | 156 | {user.DisplayName} 157 | 158 | 159 | )} 160 | 161 | 162 | 166 | 185 | 186 | {!isCreator ? null : ( 187 | 188 | 203 | 204 | )} 205 | 206 | 207 | 208 | 209 | 210 | 211 | Notes 212 | 213 | 214 | 215 | {map(v => { 216 | let _note = v 217 | if (assetmap[v.id]) { 218 | _note.thumbnail = assetmap[v.id].Thumbnail 219 | } 220 | 221 | const deleteFromNotebook = v => async e => { 222 | e.preventDefault() 223 | e.stopPropagation() 224 | if (await badWallet(t, address)) return 225 | if ( 226 | confirm( 227 | "Would you like to remove the note from this notebook?", 228 | ) 229 | ) { 230 | const book = await new Notebook({ 231 | ...opt.notebook, 232 | pid: id, 233 | }).init() 234 | const { err: _err } = await book.removeNote( 235 | v.id, 236 | true, 237 | ) 238 | if (!_err) { 239 | let _notes = clone(notes) 240 | setNotes(reject(v2 => v2.id === v.id)(_notes)) 241 | msg(t, "Note removed!") 242 | } else { 243 | err(t, "something went wrong") 244 | } 245 | } 246 | } 247 | return ( 248 | 257 | ) 258 | })(notes)} 259 | 260 | {book.Description} 261 | 262 | 263 | 264 | 265 | 266 | )} 267 | 268 | ) 269 | } 270 | 271 | export default User 272 | -------------------------------------------------------------------------------- /app/src/pages/Note.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react" 2 | import { useNavigate, useParams } from "react-router-dom" 3 | import { useToast, Flex, Box, Image, Spinner } from "@chakra-ui/react" 4 | import markdownIt from "markdown-it" 5 | import { Profile, Note } from "aonote" 6 | import { toHtml } from "hast-util-to-html" 7 | import "../github-markdown.css" 8 | import { common, createStarryNight } from "@wooorm/starry-night" 9 | import { Link } from "react-router-dom" 10 | import Header from "../components/Header" 11 | import NoteCard from "../components/NoteCard" 12 | import { msg, err, getNotes, tags, getAddr, getProf, opt } from "../lib/utils" 13 | import { Helmet } from "react-helmet" 14 | 15 | function Article(a) { 16 | const { id } = useParams() 17 | const navigate = useNavigate() 18 | const t = useToast() 19 | const [address, setAddress] = useState(null) 20 | const [profile, setProfile] = useState(null) 21 | const [init, setInit] = useState(false) 22 | const [error, setError] = useState(false) 23 | const [md, setMD] = useState(null) 24 | const [note, setNote] = useState(null) 25 | const [user, setUser] = useState(null) 26 | const [initNote, setInitNote] = useState(false) 27 | const [pubmap, setPubmap] = useState({}) 28 | useEffect(() => getAddr({ setAddress, setInit, t }), []) 29 | useEffect( 30 | () => getProf({ address, setProfile, setInit, setAddress, t }), 31 | [address], 32 | ) 33 | useEffect(() => { 34 | ;(async () => { 35 | const _note = new Note({ ...opt.note, pid: id }) 36 | const res = await _note.info() 37 | if (res) { 38 | setNote({ 39 | id: id, 40 | title: res.Name, 41 | description: res.Description, 42 | thumbnail: res.Thumbnail, 43 | collections: res.Collections || [], 44 | }) 45 | let pubmap = {} 46 | for (let v of res.Collections || []) { 47 | const nb = new Note({ ...opt.note, pid: v }) 48 | const info = await nb.info() 49 | if (info) pubmap[v] = info 50 | } 51 | setPubmap(pubmap) 52 | } 53 | })() 54 | }, [md]) 55 | 56 | useEffect(() => { 57 | ;(async () => { 58 | try { 59 | const note = new Note({ pid: id, ...opt.note }) 60 | const out = await note.get() 61 | if (out) { 62 | const _article = out.data 63 | try { 64 | const creator = tags((await getNotes([id]))[0]?.tags).Creator 65 | const _prof = new Profile(opt.profile) 66 | setUser(await _prof.profile({ id: creator })) 67 | } catch (e) {} 68 | const text = _article 69 | const starryNight = await createStarryNight(common) 70 | const markdownItInstance = markdownIt({ 71 | highlight(value, lang) { 72 | const scope = starryNight.flagToScope(lang) 73 | return toHtml({ 74 | type: "element", 75 | tagName: "pre", 76 | properties: { 77 | className: scope 78 | ? [ 79 | "highlight", 80 | "highlight-" + 81 | scope.replace(/^source\./, "").replace(/\./g, "-"), 82 | ] 83 | : undefined, 84 | }, 85 | children: scope 86 | ? /** @type {Array} */ ( 87 | starryNight.highlight(value, scope).children 88 | ) 89 | : [{ type: "text", value }], 90 | }) 91 | }, 92 | }) 93 | const html = markdownItInstance.render(text) 94 | setMD(html) 95 | setError(false) 96 | } 97 | } catch (e) { 98 | setError(true) 99 | console.log(e) 100 | } 101 | setInitNote(true) 102 | })() 103 | }, [id]) 104 | const isCreator = user && user?.ProfileId === profile?.ProfileId 105 | let notebooks = [] 106 | for (let v of note?.collections || []) { 107 | if (pubmap[v]) notebooks.push({ ...pubmap[v], id: v }) 108 | } 109 | return ( 110 | <> 111 |
114 | {!note ? null : ( 115 | 116 | 117 | {note.title} 118 | {!user ? "" : ` | ${user.DisplayName}`} 119 | 120 | 121 | 122 | 126 | 127 | 131 | 135 | 136 | 140 | 141 | )} 142 | 150 | 151 | {error ? ( 152 | 160 | 161 | something went wrong 162 | 163 | ) : !initNote ? ( 164 | 172 | 173 | Fetching Note... 174 | 175 | ) : ( 176 | 183 | )} 184 | {!note || !user ? null : ( 185 | 186 | 194 | 195 | )} 196 | 197 | 198 | 199 | ) 200 | } 201 | 202 | export default Article 203 | -------------------------------------------------------------------------------- /app/src/pages/User.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigate, useParams } from "react-router-dom" 2 | import { Notebook } from "aonote" 3 | import { Link } from "react-router-dom" 4 | import { useState, useEffect } from "react" 5 | import Header from "../components/Header" 6 | import NoteCard from "../components/NoteCard" 7 | import lf from "localforage" 8 | import { 9 | getBooks, 10 | getNotes, 11 | getInfo, 12 | getBookInfo, 13 | getAddr, 14 | getProf, 15 | ltags, 16 | tags, 17 | badWallet, 18 | msg, 19 | err, 20 | getPFP, 21 | opt, 22 | gateway_url, 23 | getThumb, 24 | } from "../lib/utils" 25 | import dayjs from "dayjs" 26 | import { 27 | useToast, 28 | Tag, 29 | IconButton, 30 | Menu, 31 | MenuButton, 32 | MenuList, 33 | MenuItem, 34 | Button, 35 | Tabs, 36 | TabList, 37 | Tab, 38 | TabPanels, 39 | TabPanel, 40 | Text, 41 | Flex, 42 | Box, 43 | Image, 44 | Card, 45 | CardHeader, 46 | Avatar, 47 | Heading, 48 | } from "@chakra-ui/react" 49 | import { 50 | indexOf, 51 | without, 52 | o, 53 | sortBy, 54 | map, 55 | pluck, 56 | fromPairs, 57 | clone, 58 | difference, 59 | } from "ramda" 60 | 61 | function User({}) { 62 | const { id } = useParams() 63 | const navigate = useNavigate() 64 | const t = useToast() 65 | const [address, setAddress] = useState(null) 66 | const [profile, setProfile] = useState(null) 67 | const [init, setInit] = useState(false) 68 | const [user, setUser] = useState(null) 69 | const [notes, setNotes] = useState([]) 70 | const [books, setBooks] = useState([]) 71 | const [bookmap, setBookMap] = useState({}) 72 | const [notemap, setNoteMap] = useState({}) 73 | const [assetmap, setAssetMap] = useState({}) 74 | 75 | useEffect(() => getAddr({ setAddress, setInit, t }), []) 76 | useEffect( 77 | () => getProf({ address, setProfile, setInit, setAddress, t }), 78 | [address], 79 | ) 80 | 81 | useEffect(() => { 82 | ;(async () => { 83 | const info = await getInfo(id) 84 | if (info) { 85 | setUser(info.Profile) 86 | setNotes( 87 | o( 88 | sortBy(v => v["date-created"] * -1), 89 | map(v => { 90 | return { ...ltags(v.tags), id: v.id, owner: v.owner.address } 91 | }), 92 | )(await getNotes(pluck("Id", info.Assets))), 93 | ) 94 | setBooks( 95 | o( 96 | sortBy(v => v["date-created"] * -1), 97 | map(v => { 98 | return { ...ltags(v.tags), id: v.id, owner: v.owner.address } 99 | }), 100 | )(await getBooks(pluck("Id", info.Collections))), 101 | ) 102 | } 103 | })() 104 | }, [id]) 105 | 106 | useEffect(() => { 107 | ;(async () => { 108 | let bookmap = {} 109 | let notemap = {} 110 | for (let v of books) { 111 | const _info = await lf.getItem(`notebook-${v.id}`) 112 | if (_info) { 113 | bookmap[v.id] = _info 114 | for (let v2 of bookmap[v.id].Assets || []) { 115 | notemap[v2] ??= [] 116 | notemap[v2].push(v.id) 117 | setNoteMap(notemap) 118 | } 119 | } 120 | setBookMap(bookmap) 121 | } 122 | for (let v of books) { 123 | const info = await getBookInfo(v.id) 124 | if (info) await lf.setItem(`notebook-${v.id}`, info) 125 | let exists = [] 126 | for (let k in notemap) { 127 | for (let v2 of notemap[k]) { 128 | if (v2 === v.id) exists.push(k) 129 | } 130 | } 131 | const diff = difference(exists, info.Assets) 132 | bookmap[v.id] = info 133 | for (let v2 of diff) { 134 | notemap[v2] ??= [] 135 | notemap[v2] = without([v.id], notemap[v2]) 136 | } 137 | for (let v2 of bookmap[v.id].Assets || []) { 138 | notemap[v2] ??= [] 139 | if (indexOf(v.id, notemap[v2]) === -1) { 140 | notemap[v2].push(v.id) 141 | setNoteMap(notemap) 142 | } 143 | } 144 | setBookMap(bookmap) 145 | } 146 | })() 147 | }, [books]) 148 | const isCreator = id === profile?.ProfileId 149 | return ( 150 | <> 151 |
154 | {!user ? null : ( 155 | 156 | 157 | <> 158 | 159 | 160 | 161 | 162 | 167 | 168 | 169 | {user.DisplayName} 170 | {user.Description} 171 | 172 | 173 | {!isCreator ? null : ( 174 | 178 | 193 | 194 | )} 195 | 196 | 197 | 198 | 199 | 200 | 201 | Notes 202 | Notebooks 203 | 204 | 205 | 206 | {map(v => { 207 | const _books = notemap[v.id] ?? [] 208 | const bids = pluck("id", books) 209 | const diff = difference(bids, _books) 210 | let notebooks = [] 211 | for (let v2 of notemap[v.id] ?? []) { 212 | if (bookmap[v2]) 213 | notebooks.push({ id: v2, ...bookmap[v2] }) 214 | } 215 | let _note = v 216 | if (assetmap[v.id]) { 217 | _note.thumbnail = assetmap[v.id].Thumbnail 218 | } 219 | const addToNotebook = v3 => async () => { 220 | if (await badWallet(t, address)) return 221 | const book = await new Notebook({ 222 | ...opt.notebook, 223 | pid: v3, 224 | }).init() 225 | 226 | const { err: _err } = await book.addNote(v.id) 227 | if (!_err) { 228 | let _map = clone(notemap) 229 | let _bmap = clone(bookmap) 230 | _map[v.id] ??= [] 231 | _map[v.id].push(v3) 232 | setNoteMap(_map) 233 | if (_bmap[v3]) { 234 | _bmap[v3].Assets.push(v.id) 235 | setBookMap(_bmap) 236 | } 237 | msg(t, "Note added!") 238 | } else { 239 | err(t, "something went wrong") 240 | } 241 | } 242 | return ( 243 | <> 244 | 255 | 256 | ) 257 | })(notes)} 258 | 259 | 260 | {map(v => { 261 | let bmap = bookmap[v.id] 262 | return ( 263 | 264 | 272 | 273 | 274 | 275 | {!bmap ? null : ( 276 | 281 | )} 282 | 288 | 289 | 290 | {v.title} 291 | 292 | {v.description} 293 | 294 | 295 | {dayjs(v["date-created"] * 1).format( 296 | "MMM DD", 297 | )} 298 | 299 | 300 | {!bmap ? null : ( 301 | 307 | {bmap.Assets.length} Notes 308 | 309 | )} 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | ) 319 | })(books)} 320 | 321 | 322 | 323 | 324 | 325 | 326 | )} 327 | 328 | ) 329 | } 330 | 331 | export default User 332 | -------------------------------------------------------------------------------- /app/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import { nodePolyfills } from "vite-plugin-node-polyfills" 3 | import react from "@vitejs/plugin-react-swc" 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | base: "./", 8 | plugins: [nodePolyfills(), react()], 9 | build: { chunkSizeWarningLimit: 10000 }, 10 | }) 11 | -------------------------------------------------------------------------------- /assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/assets/cover.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/assets/logo.png -------------------------------------------------------------------------------- /logo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .vercel 26 | -------------------------------------------------------------------------------- /logo/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { 9 | files: ['**/*.{js,jsx}'], 10 | ignores: ['dist'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /logo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Logo | Arweave Japan 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /logo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/react": "^2.8.2", 14 | "@emotion/react": "^11.13.0", 15 | "@emotion/styled": "^11.13.0", 16 | "framer-motion": "^11.3.24", 17 | "ramda": "^0.30.1", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "react-router-dom": "^6.26.1" 21 | }, 22 | "devDependencies": { 23 | "@eslint/js": "^9.8.0", 24 | "@types/react": "^18.3.3", 25 | "@types/react-dom": "^18.3.0", 26 | "@vitejs/plugin-react-swc": "^3.5.0", 27 | "eslint": "^9.8.0", 28 | "eslint-plugin-react": "^7.35.0", 29 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 30 | "eslint-plugin-react-refresh": "^0.4.9", 31 | "globals": "^15.9.0", 32 | "vite": "^5.4.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /logo/public/ao.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /logo/public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/logo/public/cover.png -------------------------------------------------------------------------------- /logo/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/logo/public/icon.png -------------------------------------------------------------------------------- /logo/public/svg-export.min.js: -------------------------------------------------------------------------------- 1 | (function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):(t="undefined"!=typeof globalThis?globalThis:t||self,e(t.svgExport=t.svgExport||{}))})(this,function(t){"use strict";function e(t){void 0!==typeof console&&"function"==typeof console.warn&&console.warn(t)}function n(t){var n=document.createElement("div");if(n.className="tempdiv-svg-exportJS","string"==typeof t&&(n.insertAdjacentHTML("beforeend",t.trim()),t=n.firstChild),!t.nodeType||1!==t.nodeType)return e("Error svg-export: The input svg was not recognized"),null;var i=t.cloneNode(!0);return i.style.display=null,n.appendChild(i),n.style.visibility="hidden",n.style.display="table",n.style.position="absolute",document.body.appendChild(n),i}function i(t){t&&t.pdfOptions&&(Object.keys(y.pdfOptions).forEach(function(e){if(t.pdfOptions.hasOwnProperty(e)&&typeof t.pdfOptions[e]==typeof y.pdfOptions[e]){if(""===t.pdfOptions[e])return;y.pdfOptions[e]=t.pdfOptions[e]}}),y.pdfOptions.pageLayout.margin||(y.pdfOptions.pageLayout.margin=50),y.pdfOptions.pageLayout.margins||(y.pdfOptions.pageLayout.margins={})),y.pdfOptions.pageLayout.margins.top=y.pdfOptions.pageLayout.margins.top||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.bottom=y.pdfOptions.pageLayout.margins.bottom||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.left=y.pdfOptions.pageLayout.margins.left||y.pdfOptions.pageLayout.margin,y.pdfOptions.pageLayout.margins.right=y.pdfOptions.pageLayout.margins.top||y.pdfOptions.pageLayout.margin,delete y.pdfOptions.pageLayout.margin,t&&y.pdfOptions.pageLayout.size||(y.pdfOptions.pageLayout.size=[Math.max(300,y.width)+y.pdfOptions.pageLayout.margins.left+y.pdfOptions.pageLayout.margins.right,Math.max(300,y.height)+y.pdfOptions.pageLayout.margins.top+y.pdfOptions.pageLayout.margins.bottom+(y.pdfOptions.addTitleToPage?2*y.pdfOptions.pdfTitleFontSize+10:0)+(""!==y.pdfOptions.chartCaption?4*y.pdfOptions.pdfCaptionFontSize+10:0)])}function o(t,e){y={originalWidth:100,originalHeight:100,originalMinXViewBox:0,originalMinYViewBox:0,width:100,height:100,scale:1,useCSS:!0,transparentBackgroundReplace:"white",allowCrossOriginImages:!1,elementsToExclude:[],pdfOptions:{customFonts:[],pageLayout:{margin:50,margins:{}},addTitleToPage:!0,chartCaption:"",pdfTextFontFamily:"Helvetica",pdfTitleFontSize:20,pdfCaptionFontSize:14},onDone:null},y.originalHeight=-1!==t.style.getPropertyValue("height").indexOf("%")||t.getAttribute("height")&&-1!==t.getAttribute("height").indexOf("%")?t.getBBox().height*y.scale:t.getBoundingClientRect().height*y.scale,y.originalWidth=-1!==t.style.getPropertyValue("width").indexOf("%")||t.getAttribute("width")&&-1!==t.getAttribute("width").indexOf("%")?t.getBBox().width*y.scale:t.getBoundingClientRect().width*y.scale,y.originalMinXViewBox=t.getAttribute("viewBox")?t.getAttribute("viewBox").split(/\s/)[0]:0,y.originalMinYViewBox=t.getAttribute("viewBox")?t.getAttribute("viewBox").split(/\s/)[1]:0,e&&e.scale&&"number"==typeof e.scale&&(y.scale=e.scale),e&&e.height?"number"==typeof e.height&&(y.height=e.height*y.scale):y.height=y.originalHeight*y.scale,e&&e.width?"number"==typeof e.width&&(y.width=e.width*y.scale):y.width=y.originalWidth*y.scale,e&&!1===e.useCSS&&(y.useCSS=!1),e&&e.transparentBackgroundReplace&&(y.transparentBackgroundReplace=e.transparentBackgroundReplace),e&&e.allowCrossOriginImages&&(y.allowCrossOriginImages=e.allowCrossOriginImages),e&&e.excludeByCSSSelector&&"string"==typeof e.excludeByCSSSelector&&(y.elementsToExclude=t.querySelectorAll(e.excludeByCSSSelector)),e&&e.onDone&&"function"==typeof e.onDone&&(y.onDone=e.onDone),i(e)}function a(t,n){if("function"==typeof getComputedStyle){for(var i=0;i0)for(const t of o)-1===["width","height","inline-size","block-size"].indexOf(t)&&n.style.setProperty(t,o.getPropertyValue(t));t.childNodes.forEach(function(t,e){1===t.nodeType&&a(t,n.childNodes[parseInt(e,10)])})}else e("Warning svg-export: this browser is not able to get computed styles")}function r(t,e,n){void 0===n&&(n=!0),y.useCSS&&"object"==typeof e&&(a(e,t),t.style.display=null),y.elementsToExclude.forEach(function(t){t.remove()}),t.style.width=null,t.style.height=null,t.setAttribute("width",y.width),t.setAttribute("height",y.height),t.setAttribute("preserveAspectRatio","none"),t.setAttribute("viewBox",y.originalMinXViewBox+" "+y.originalMinYViewBox+" "+y.originalWidth+" "+y.originalHeight);for(var i=document.getElementsByClassName("tempdiv-svg-exportJS");i.length>0;)i[0].parentNode.removeChild(i[0]);if(n){var o=new XMLSerializer,r=o.serializeToString(t).replace(/currentColor/g,"black");return r.match(/^]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)||(r=r.replace(/^]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)||(r=r.replace(/^]/g,"_"),navigator.msSaveBlob){for(var i=decodeURIComponent(t.split(",")[1]),o=[],a=t.split(",")[0].split(":")[1].split(";")[0],r=0;r\r\n'+n;var i="data:image/svg+xml;charset=utf-8,"+encodeURIComponent(n);l(i,e+".svg")})}}function d(t,i,a,s){if("object"==typeof canvg){s=s.toLowerCase().replace("jpg","jpeg"),"png"!==s&&"jpeg"!==s&&(s="png");var p=n(t);if(p){null==i&&(i="chart");var g=document.createElement("canvas");a&&(a.width||a.height)||(a||(a={}),a.scale=10),o(p,a);var d=r(p,t);"jpeg"===s&&(d=d.replace(">",'>'));var f=g.getContext("2d"),u=canvg.Canvg.fromString(f,d,{anonymousCrossOrigin:y.allowCrossOriginImages});u.start(),u.ready().then(function(){var t=g.toDataURL("image/"+s);l(t,i+"."+s,g)})}}else e("Error svg-export: PNG/JPEG export requires Canvg.js")}function f(t,e,n){d(t,e,n,"png")}function u(t,e,n){d(t,e,n,"jpeg")}function h(t,e,n){y.pdfOptions.addTitleToPage&&t.font(y.pdfOptions.pdfTextFontFamily).fontSize(y.pdfOptions.pdfTitleFontSize).text(e,{width:y.pdfOptions.pageLayout.size[0]-y.pdfOptions.pageLayout.margins.left-y.pdfOptions.pageLayout.margins.right}),SVGtoPDF(t,n,y.pdfOptions.pageLayout.margins.left,t.y+10,{width:y.width,height:y.height,preserveAspectRatio:"none",useCSS:y.useCSS}),""!==y.pdfOptions.chartCaption&&t.font(y.pdfOptions.pdfTextFontFamily).fontSize(y.pdfOptions.pdfCaptionFontSize).text(y.pdfOptions.chartCaption,y.pdfOptions.pageLayout.margins.left,y.pdfOptions.pageLayout.size[1]-y.pdfOptions.pageLayout.margins.bottom-4*y.pdfOptions.pdfCaptionFontSize,{width:y.pdfOptions.pageLayout.size[0]-y.pdfOptions.pageLayout.margins.left-y.pdfOptions.pageLayout.margins.right})}function c(t,i,a){if("function"==typeof PDFDocument&&"function"==typeof SVGtoPDF&&"function"==typeof blobStream){var g=n(t);if(g){null==i&&(i="chart"),o(g,a);var d=r(g,t,!1),f=new PDFDocument(y.pdfOptions.pageLayout),u=f.pipe(blobStream()),c=g.getElementsByTagName("image"),m=[];if(c)for(var w of c)(w.getAttribute("href")&&-1===w.getAttribute("href").indexOf("data:")||w.getAttribute("xlink:href")&&-1===w.getAttribute("xlink:href").indexOf("data:"))&&m.push(s(w));Promise.all(m).then(function(){if(y.pdfOptions.customFonts.length>0){var t=p(y.pdfOptions.customFonts.map(function(t){return t.url}));Promise.all(t).then(function(t){t.forEach(function(t,e){var n=y.pdfOptions.customFonts[parseInt(e,10)],i=d.querySelectorAll('[style*="'+n.fontName+'"]');i.forEach(function(t){t.style.fontFamily=n.fontName}),-1===n.url.indexOf(".ttc")&&-1===n.url.indexOf(".dfont")||!n.styleName?f.registerFont(n.fontName,t):f.registerFont(n.fontName,t,n.styleName)}),h(f,i,d),f.end()})}else h(f,i,d),f.end()}),u.on("finish",function(){var t=u.toBlobURL("application/pdf");l(t,i+".pdf")})}}else e("Error svg-export: PDF export requires PDFKit.js, blob-stream and SVG-to-PDFKit")}var m="1.2.0",y={};t.version=m,t.downloadSvg=g,t.downloadPng=f,t.downloadJpeg=u,t.downloadPdf=c,Object.defineProperty(t,"__esModule",{value:!0})}); -------------------------------------------------------------------------------- /logo/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react" 2 | import ReactDOM from "react-dom/client" 3 | import { RouterProvider, createHashRouter } from "react-router-dom" 4 | import { createRoot } from "react-dom/client" 5 | import App from "./App.jsx" 6 | import { ChakraProvider } from "@chakra-ui/react" 7 | 8 | const router = createHashRouter([ 9 | { 10 | path: "/", 11 | element: , 12 | }, 13 | ]) 14 | 15 | ReactDOM.createRoot(document.getElementById("root")).render( 16 | 17 | 18 | 19 | 20 | , 21 | ) 22 | -------------------------------------------------------------------------------- /logo/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /sdk/.babelrc-cjs: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": "commonjs" }] 4 | ] 5 | } -------------------------------------------------------------------------------- /sdk/.babelrc-esm: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": false }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /sdk/.gitignore: -------------------------------------------------------------------------------- 1 | keyfile.json -------------------------------------------------------------------------------- /sdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | npm-debug.log* 3 | 4 | # Coverage directory used by tools like istanbul 5 | coverage 6 | .nyc_output 7 | 8 | # Dependency directories 9 | node_modules 10 | 11 | # npm package lock 12 | package-lock.json 13 | yarn.lock 14 | 15 | # project files 16 | examples 17 | CHANGELOG.md 18 | .travis.yml 19 | .editorconfig 20 | .eslintignore 21 | .eslintrc 22 | .babelrc 23 | .gitignore -------------------------------------------------------------------------------- /sdk/make.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync, readFileSync } from "fs" 2 | import { resolve } from "path" 3 | 4 | const packageJson = resolve(import.meta.dirname, "package.json") 5 | const packageJsonDist = resolve(import.meta.dirname, "dist/package.json") 6 | const packageJsonDist2 = resolve(import.meta.dirname, "dist/esm/package.json") 7 | const json = JSON.parse(readFileSync(packageJson, "utf8")) 8 | delete json.type 9 | delete json.devDependencies 10 | delete json.scripts 11 | json.main = "cjs/index.js" 12 | json.module = "esm/index.js" 13 | 14 | console.log(json) 15 | writeFileSync(packageJsonDist, JSON.stringify(json, undefined, 2)) 16 | 17 | const json2 = { 18 | type: "module", 19 | } 20 | writeFileSync(packageJsonDist2, JSON.stringify(json2, undefined, 2)) 21 | -------------------------------------------------------------------------------- /sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aonote", 3 | "version": "0.11.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "dist/cjs/index.js", 7 | "module": "dist/src/index.js", 8 | "scripts": { 9 | "build:cjs": "babel src --out-dir dist/cjs --config-file ./.babelrc-cjs", 10 | "build": "rm -rf dist && npm run build:cjs && cp src -rf dist/esm && node make.js && cp .npmignore dist/ && cp src/lua dist/cjs/lua -rf", 11 | "test": "mocha --node-option=experimental-wasm-memory64" 12 | }, 13 | "engines": { 14 | "node": ">=20.0.0" 15 | }, 16 | "exports": { 17 | ".": { 18 | "require": "./cjs/index.js", 19 | "import": "./esm/index.js" 20 | }, 21 | "./utils": { 22 | "require": "./cjs/utils.js", 23 | "import": "./esm/utils.js" 24 | }, 25 | "./test": { 26 | "require": "./cjs/helpers.js", 27 | "import": "./esm/helpers.js" 28 | } 29 | }, 30 | "author": "", 31 | "license": "MIT", 32 | "dependencies": { 33 | "@babel/plugin-transform-modules-commonjs": "^7.24.8", 34 | "@permaweb/ao-loader": "^0.0.43", 35 | "@permaweb/aoconnect": "^0.0.61", 36 | "arbundles": "^0.11.1", 37 | "arweave": "^1.15.1", 38 | "base64url": "^3.0.1", 39 | "ramda": "^0.30.1", 40 | "test": "^3.3.0", 41 | "wao": "^0.2.2" 42 | }, 43 | "devDependencies": { 44 | "@babel/cli": "^7.24.8", 45 | "@babel/core": "^7.25.2", 46 | "@babel/preset-env": "^7.25.3", 47 | "chai": "^5.1.1", 48 | "mocha": "^10.7.3", 49 | "yargs": "^17.7.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /sdk/scripts/upload_library.js: -------------------------------------------------------------------------------- 1 | import { AO } from "../src/index.js" 2 | import { resolve } from "path" 3 | import { readFileSync } from "fs" 4 | import { srcs } from "../src/utils.js" 5 | 6 | const jwk = JSON.parse( 7 | readFileSync(resolve(import.meta.dirname, "keyfile.json"), "utf8"), 8 | ) 9 | 10 | const main = async () => { 11 | const ao = await new AO({}).init(jwk) 12 | const { pid } = await ao.spwn({ 13 | tags: { Library: "Atomic-Notes", Version: "1.0.0" }, 14 | }) 15 | await ao.wait({ pid }) 16 | const { mid } = await ao.load({ src: srcs.notelib_src, pid }) 17 | console.log(mid) 18 | } 19 | 20 | main() 21 | -------------------------------------------------------------------------------- /sdk/src/asset.js: -------------------------------------------------------------------------------- 1 | import { srcs, wait, udl } from "./utils.js" 2 | import Profile from "./profile.js" 3 | import { getTagVal } from "./utils.js" 4 | import { mergeLeft } from "ramda" 5 | 6 | class Asset { 7 | constructor({ 8 | asset_src = srcs.asset_src, 9 | pid, 10 | profile = {}, 11 | ao = {}, 12 | ar = {}, 13 | } = {}) { 14 | this.__type__ = "asset" 15 | if (profile?.__type__ === "profile") { 16 | this.profile = profile 17 | } else { 18 | let _profile = typeof profile === "object" ? profile : {} 19 | if (!_profile.ao) _profile.ao = ao 20 | if (!_profile.ar) _profile.ar = ar 21 | this.profile = new Profile(profile) 22 | } 23 | this.ao = this.profile.ao 24 | this.ar = this.ao.ar 25 | this.asset_src = asset_src 26 | this.pid = pid 27 | } 28 | 29 | async init(jwk) { 30 | await this.profile.init(jwk) 31 | return this 32 | } 33 | 34 | async create({ 35 | jwk, 36 | src = this.asset_src, 37 | data, 38 | fills = {}, 39 | tags = {}, 40 | content_type, 41 | info: { title, description }, 42 | token: { fraction = "1" }, 43 | udl: { payment, access, derivations, commercial, training }, 44 | cb, 45 | }) { 46 | const creator = this.profile.id 47 | if (!creator) return { err: "no ao profile id" } 48 | const date = Date.now() 49 | let _tags = { 50 | Action: "Add-Uploaded-Asset", 51 | Title: title, 52 | Description: description, 53 | "Date-Created": Number(date).toString(), 54 | Implements: "ANS-110", 55 | Type: "image", 56 | "Content-Type": content_type, 57 | ...udl({ payment, access, derivations, commercial, training }), 58 | ...tags, 59 | } 60 | if (creator) _tags["Creator"] = creator 61 | const balance = 62 | typeof fraction === "number" ? Number(fraction * 1).toString() : fraction 63 | const _fills = mergeLeft(fills, { 64 | NAME: title, 65 | CREATOR: creator, 66 | TICKER: "ATOMIC", 67 | DENOMINATION: "1", 68 | BALANCE: balance, 69 | }) 70 | 71 | let fns = [ 72 | { 73 | fn: "deploy", 74 | args: { src, fills: _fills, tags: _tags, data }, 75 | then: ({ pid }) => { 76 | this.pid = pid 77 | }, 78 | }, 79 | { fn: "add", bind: this, args: { id: creator } }, 80 | ] 81 | 82 | return await this.ao.pipe({ jwk, fns, cb }) 83 | } 84 | 85 | async info() { 86 | const { err, out } = await this.ao.dry({ 87 | pid: this.pid, 88 | act: "Info", 89 | checkData: true, 90 | get: { data: true, json: true }, 91 | }) 92 | return out ?? null 93 | } 94 | 95 | async add({ id }) { 96 | return await this.ao.msg({ 97 | pid: this.pid, 98 | act: "Add-Asset-To-Profile", 99 | tags: { ProfileProcess: id }, 100 | check: { Action: "Add-Uploaded-Asset" }, 101 | }) 102 | } 103 | 104 | async mint({ quantity }) { 105 | return await this.ao.msg({ 106 | pid: this.pid, 107 | act: "Mint", 108 | data: JSON.stringify({ Quantity: quantity }), 109 | check: { Action: "Mint-Success" }, 110 | }) 111 | } 112 | 113 | async transfer({ recipient, quantity, profile = false }) { 114 | if (profile) { 115 | return await this.ao.msg({ 116 | pid: this.profile.id, 117 | act: "Transfer", 118 | tags: { Target: this.pid, Recipient: recipient, Quantity: quantity }, 119 | check: { Action: "Transfer" }, 120 | }) 121 | } else { 122 | return await this.ao.msg({ 123 | pid: this.pid, 124 | act: "Transfer", 125 | tags: { Recipient: recipient, Quantity: quantity }, 126 | check: { Status: "Success" }, 127 | }) 128 | } 129 | } 130 | 131 | async balance({ target }) { 132 | const res = await this.ao.dry({ 133 | pid: this.pid, 134 | act: "Balance", 135 | data: JSON.stringify({ Target: target }), 136 | check: { Action: "Read-Success" }, 137 | get: { data: true }, 138 | }) 139 | const tags = getTagVal( 140 | { obj: { action: "Action", status: "Status", message: "Message" } }, 141 | res.res, 142 | ) 143 | if (tags.action === "Read-Error") { 144 | res.out = "0" 145 | res.err = null 146 | } 147 | return res 148 | } 149 | 150 | async balances() { 151 | return await this.ao.dry({ 152 | pid: this.pid, 153 | act: "Balances", 154 | check: { Action: "Read-Success" }, 155 | get: { data: true, json: true }, 156 | }) 157 | } 158 | } 159 | 160 | export default Asset 161 | -------------------------------------------------------------------------------- /sdk/src/assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/sdk/src/assets/banner.png -------------------------------------------------------------------------------- /sdk/src/assets/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/sdk/src/assets/thumbnail.png -------------------------------------------------------------------------------- /sdk/src/collection.js: -------------------------------------------------------------------------------- 1 | import { srcs } from "./utils.js" 2 | import { is } from "ramda" 3 | import Profile from "./profile.js" 4 | import { mergeLeft } from "ramda" 5 | 6 | class Collection { 7 | constructor({ 8 | registry = srcs.bookreg, 9 | registry_src = srcs.bookreg_src, 10 | thumbnail = srcs.thumb, 11 | banner = srcs.banner, 12 | collection_src = srcs.collection_src, 13 | pid, 14 | profile = {}, 15 | ao = {}, 16 | ar = {}, 17 | } = {}) { 18 | this.__type__ = "collection" 19 | if (profile?.__type__ === "profile") { 20 | this.profile = profile 21 | } else { 22 | let _profile = typeof profile === "object" ? profile : {} 23 | if (!_profile.ao) _profile.ao = ao 24 | if (!_profile.ar) _profile.ar = ar 25 | this.profile = new Profile(profile) 26 | } 27 | this.ao = this.profile.ao 28 | this.ar = this.ao.ar 29 | this.registry = registry 30 | this.pid = pid 31 | this.registry_src = registry_src 32 | this.thumbnail = thumbnail 33 | this.banner = banner 34 | this.collection_src = collection_src 35 | } 36 | 37 | async init(jwk) { 38 | await this.profile.init(jwk) 39 | return this 40 | } 41 | 42 | async createRegistry({ jwk } = {}) { 43 | const fn = { 44 | fn: "deploy", 45 | args: { src: this.registry_src }, 46 | then: ({ pid }) => { 47 | this.registry = pid 48 | }, 49 | } 50 | return await this.ao.pipe({ jwk, fns: [fn] }) 51 | } 52 | 53 | async create({ 54 | src = this.collection_src, 55 | fills = {}, 56 | info: { 57 | title, 58 | description, 59 | thumbnail, 60 | banner, 61 | thumbnail_data, 62 | thumbnail_type, 63 | banner_data, 64 | banner_type, 65 | } = {}, 66 | bazar = false, 67 | jwk, 68 | cb, 69 | }) { 70 | const profileId = this.profile.id 71 | if (!profileId) return { err: "no ao profile id" } 72 | const date = Date.now() 73 | let tags = { 74 | Action: "Add-Collection", 75 | Title: title, 76 | Description: description, 77 | "Date-Created": Number(date).toString(), 78 | "Profile-Creator": profileId, 79 | Creator: this.ar.addr, 80 | "Collection-Type": "Atomic-Notes", 81 | } 82 | if (thumbnail && !/^\s*$/.test(thumbnail)) { 83 | tags["Thumbnail"] = thumbnail 84 | } 85 | if (banner && !/^\s*$/.test(banner)) tags["Banner"] = banner 86 | let fns = [ 87 | { 88 | fn: "deploy", 89 | args: { 90 | tags, 91 | src, 92 | fills: mergeLeft(fills, { 93 | NAME: title, 94 | DESCRIPTION: description, 95 | DATECREATED: date, 96 | LASTUPDATE: date, 97 | CREATOR: profileId, 98 | BANNER: banner ?? "None", 99 | THUMBNAIL: thumbnail ?? "None", 100 | }), 101 | }, 102 | then: ({ pid }) => { 103 | this.pid = pid 104 | }, 105 | }, 106 | { 107 | fn: this.add, 108 | bind: this, 109 | args: { id: profileId }, 110 | then: { "args.collectionId": "pid" }, 111 | }, 112 | ] 113 | if (bazar) { 114 | fns.push({ 115 | fn: this.register, 116 | bind: this, 117 | args: { 118 | name: title, 119 | description, 120 | thumbnail, 121 | banner, 122 | date, 123 | creator: profileId, 124 | }, 125 | }) 126 | } 127 | this.addImages({ 128 | fns, 129 | thumbnail, 130 | thumbnail_data, 131 | thumbnail_type, 132 | banner, 133 | banner_data, 134 | banner_type, 135 | }) 136 | return await this.ao.pipe({ jwk, fns, cb }) 137 | } 138 | addImages({ 139 | fns, 140 | thumbnail, 141 | thumbnail_data, 142 | thumbnail_type, 143 | banner, 144 | banner_data, 145 | banner_type, 146 | }) { 147 | let images = 0 148 | if (!thumbnail && thumbnail_data && thumbnail_type) { 149 | images++ 150 | fns.unshift({ 151 | fn: "post", 152 | args: { 153 | data: new Uint8Array(thumbnail_data), 154 | tags: { "Content-Type": thumbnail_type }, 155 | }, 156 | then: ({ args, id, out }) => { 157 | images-- 158 | out.thumbnail = id 159 | if (images === 0) { 160 | args.fills.THUMBNAIL = id 161 | args.tags.Thumbnail = id 162 | } 163 | if (out.banner) { 164 | args.fills.BANNER = out.banner 165 | args.tags.Banner = out.banner 166 | } 167 | }, 168 | }) 169 | } 170 | 171 | if (!banner && banner_data && banner_type) { 172 | images++ 173 | fns.unshift({ 174 | fn: "post", 175 | args: { 176 | data: new Uint8Array(banner_data), 177 | tags: { "Content-Type": banner_type }, 178 | }, 179 | then: ({ args, id, out }) => { 180 | images-- 181 | out.banner = id 182 | if (images === 0) { 183 | args.fills.BANNER = id 184 | args.tags.Banner = id 185 | } 186 | }, 187 | }) 188 | } 189 | } 190 | async updateInfo({ 191 | title, 192 | description, 193 | jwk, 194 | thumbnail, 195 | thumbnail_data, 196 | thumbnail_type, 197 | banner, 198 | banner_data, 199 | banner_type, 200 | cb, 201 | }) { 202 | let info_map = { 203 | Name: title, 204 | Description: description, 205 | Thumbnail: thumbnail, 206 | Banner: banner, 207 | } 208 | let new_info = [] 209 | for (const k in info_map) { 210 | if (info_map[k]) 211 | new_info.push(`${k} = '${info_map[k].replace(/'/g, "\\'")}'`) 212 | } 213 | const isThumbnail = !thumbnail && thumbnail_data && thumbnail_type 214 | const isBanner = !banner && banner_data && banner_type 215 | if (new_info.length === 0 && !isThumbnail && !isBanner) { 216 | return { err: "empty info" } 217 | } 218 | let fns = [ 219 | { fn: "eval", args: { pid: this.pid, data: new_info.join("\n") } }, 220 | ] 221 | let images = 0 222 | if (isThumbnail) { 223 | images++ 224 | fns.unshift({ 225 | fn: "post", 226 | args: { 227 | data: new Uint8Array(thumbnail_data), 228 | tags: { "Content-Type": thumbnail_type }, 229 | }, 230 | then: ({ args, id, out }) => { 231 | images-- 232 | out.thumbnail = id 233 | if (images === 0) args.data += `\nThumbnail = '${id}'` 234 | if (out.banner) args.data += `\nBanner = '${out.banner}'` 235 | }, 236 | }) 237 | } 238 | if (isBanner) { 239 | images++ 240 | fns.unshift({ 241 | fn: "post", 242 | args: { 243 | data: new Uint8Array(banner_data), 244 | tags: { "Content-Type": banner_type }, 245 | }, 246 | then: ({ args, id, out }) => { 247 | images-- 248 | out.banner = id 249 | if (images === 0) args.data += `\nBanner = '${out.banner}'` 250 | }, 251 | }) 252 | } 253 | return await this.ao.pipe({ jwk, fns, cb }) 254 | } 255 | 256 | async info(pid = this.pid) { 257 | const { err, out } = await this.ao.dry({ 258 | pid, 259 | act: "Info", 260 | checkData: true, 261 | get: { data: true, json: true }, 262 | }) 263 | return out ?? null 264 | } 265 | 266 | async get(creator) { 267 | const { err, out } = await this.ao.dry({ 268 | pid: this.registry, 269 | act: "Get-Collections-By-User", 270 | tags: { Creator: creator }, 271 | checkData: true, 272 | get: { data: true, json: true }, 273 | }) 274 | return out ?? null 275 | } 276 | 277 | async add({ id }) { 278 | return await this.ao.msg({ 279 | pid: this.pid, 280 | act: "Add-Collection-To-Profile", 281 | tags: { ProfileProcess: id }, 282 | check: { Action: "Add-Collection" }, 283 | }) 284 | } 285 | 286 | async register({ 287 | name, 288 | description, 289 | thumbnail = this.thumbnail, 290 | banner = this.banner, 291 | date, 292 | creator, 293 | collectionId, 294 | }) { 295 | let tags = { 296 | Name: name, 297 | Description: description, 298 | Thumbnail: thumbnail, 299 | Banner: banner, 300 | DateCreated: Number(date).toString(), 301 | Creator: creator, 302 | CollectionId: collectionId, 303 | } 304 | return await this.ao.msg({ 305 | act: "Add-Collection", 306 | pid: this.registry, 307 | tags: tags, 308 | check: { Status: "Success" }, 309 | }) 310 | } 311 | 312 | async addAsset(asset_pid) { 313 | return await this.update(asset_pid) 314 | } 315 | 316 | async removeAsset(asset_pid) { 317 | return await this.update(asset_pid, true) 318 | } 319 | 320 | async addAssets(asset_pids) { 321 | return await this.update(asset_pids) 322 | } 323 | 324 | async removeAssets(asset_pids) { 325 | return await this.update(asset_pids, true) 326 | } 327 | 328 | async update(asset_pid, remove) { 329 | let ids = asset_pid 330 | if (!is(Array, ids)) ids = [ids] 331 | return this.ao.msg({ 332 | pid: this.pid, 333 | act: "Update-Assets", 334 | data: JSON.stringify({ 335 | AssetIds: ids, 336 | UpdateType: remove ? "Remove" : "Add", 337 | }), 338 | check: { Status: "Success" }, 339 | }) 340 | } 341 | } 342 | 343 | export default Collection 344 | -------------------------------------------------------------------------------- /sdk/src/dirname.js: -------------------------------------------------------------------------------- 1 | export default import.meta.dirname 2 | -------------------------------------------------------------------------------- /sdk/src/helpers.js: -------------------------------------------------------------------------------- 1 | import { Note, Profile, AR, AO, Collection, Notebook } from "./index.js" 2 | import assert from "assert" 3 | import { createDataItemSigner, connect } from "@permaweb/aoconnect" 4 | import { dirname as _dirname, resolve } from "path" 5 | import { mkdirSync, existsSync, writeFileSync, readFileSync } from "fs" 6 | import yargs from "yargs" 7 | 8 | let { 9 | reset = false, 10 | cache = false, 11 | auth = null, 12 | } = yargs(process.argv.slice(2)).argv 13 | 14 | const dirname = async () => 15 | typeof __dirname != "undefined" 16 | ? __dirname 17 | : (await import("./dirname.js")).default 18 | 19 | export class Src { 20 | constructor({ ar, dir } = {}) { 21 | this.ar = ar 22 | this.dir = dir 23 | if (!dir) dirname().then(v => (this.dir = v)) 24 | } 25 | data(file, ext = "lua") { 26 | return readFileSync( 27 | `${this.dir}/${file}.${ext}`, 28 | ext === "wasm" ? null : "utf8", 29 | ) 30 | } 31 | async upload(file, ext = "lua") { 32 | const res = await this.ar.post({ data: this.data(file, ext) }) 33 | return res.err ? null : res.id 34 | } 35 | } 36 | 37 | export const setup = async ({ 38 | aoconnect, 39 | arweave, 40 | cacheDir = ".cache", 41 | targets = { profile: false, note: false, asset: false }, 42 | } = {}) => { 43 | if (targets.asset || targets.note) targets.profile = true 44 | let opt = null 45 | console.error = () => {} 46 | console.warn = () => {} 47 | const dir = resolve(await dirname(), "lua") 48 | const thumbnail = readFileSync(`${dir}/../assets/thumbnail.png`) 49 | const banner = readFileSync(resolve(`${dir}/../assets/banner.png`)) 50 | const _cacheDir = resolve(await dirname(), cacheDir) 51 | const optPath = `${_cacheDir}/opt.json` 52 | if (cache && !reset) { 53 | try { 54 | if (existsSync(optPath)) { 55 | opt = JSON.parse(readFileSync(optPath, "utf8")) 56 | } else { 57 | console.log("cache doesn't exist:", optPath) 58 | } 59 | } catch (e) { 60 | console.log(e) 61 | } 62 | } 63 | 64 | if (opt) { 65 | const ar = await new AR(opt.ar).init(opt.jwk) 66 | const src = new Src({ ar, readFileSync, dir }) 67 | const ao = await new AO(opt.ao).init(opt.jwk) 68 | const ao2 = await new AO(opt.ao2).init(opt.jwk) 69 | const profile = opt.targets.profile 70 | ? await new Profile({ ...opt.profile, ao }).init(opt.jwk) 71 | : null 72 | console.log("cache:\t", optPath) 73 | console.log("addr:\t", ar.addr) 74 | return { opt, thumbnail, banner, ar, ao2, ao, profile, src } 75 | } 76 | 77 | // ar 78 | arweave ??= { port: 4000 } 79 | aoconnect ??= { 80 | MU_URL: "http://localhost:4002", 81 | CU_URL: "http://localhost:4004", 82 | GATEWAY_URL: "http://localhost:4000", 83 | } 84 | const ar = new AR(arweave) 85 | await ar.gen("10") 86 | const src = new Src({ ar, readFileSync, dir }) 87 | opt = { ar: { ...arweave }, jwk: ar.jwk } 88 | if (!auth && /localhost/.test(aoconnect?.CU_URL ?? "")) { 89 | auth = (await fetch(aoconnect.CU_URL).then(r => r.json())).address 90 | } 91 | 92 | // ao 93 | const wasm = await src.upload("aos-sqlite", "wasm") 94 | const wasm2 = await src.upload("aos", "wasm") 95 | const wasm_aos2 = await src.upload("aos2_0_1", "wasm") 96 | 97 | const ao = new AO({ aoconnect, ar, authority: auth }) 98 | const { id: module_aos2 } = await ao.postModule({ 99 | data: await ar.data(wasm_aos2), 100 | }) 101 | 102 | const { id: module_sqlite } = await ao.postModule({ 103 | data: await ar.data(wasm), 104 | overwrite: true, 105 | }) 106 | 107 | const { id: module } = await ao.postModule({ 108 | data: await ar.data(wasm2), 109 | overwrite: true, 110 | }) 111 | 112 | const { scheduler } = await ao.postScheduler({ 113 | url: "http://su", 114 | overwrite: true, 115 | }) 116 | 117 | opt.ao = { 118 | module: module_sqlite, 119 | scheduler, 120 | aoconnect, 121 | ar: opt.ar, 122 | authority: auth, 123 | } 124 | 125 | const ao2 = await new AO({ 126 | aoconnect, 127 | ar, 128 | authority: auth, 129 | module: module_aos2, 130 | scheduler, 131 | }).init(ar.jwk) 132 | 133 | opt.ao2 = { 134 | module: module_aos2, 135 | scheduler, 136 | aoconnect, 137 | ar: opt.ar, 138 | authority: auth, 139 | } 140 | 141 | if (auth) opt.ao.authority = auth 142 | opt.authority = auth 143 | opt.targets = targets 144 | opt.modules = { 145 | aos2: module_aos2, 146 | aos1: module, 147 | sqlite: module_sqlite, 148 | } 149 | let profile = null 150 | if (targets.profile) { 151 | // profile 152 | const registry_src = await src.upload("registry000") 153 | const profile_src = await src.upload("profile000") 154 | profile = new Profile({ profile_src, registry_src, ao }) 155 | await profile.createRegistry({}) 156 | 157 | opt.profile = { 158 | module: module_sqlite, 159 | registry_src, 160 | registry: profile.registry, 161 | profile_src, 162 | ao: opt.ao, 163 | } 164 | if (targets.asset || targets.note) { 165 | const collection_registry_src = await src.upload("collection-registry") 166 | const collection_src = await src.upload("collection") 167 | const notebook_src = await src.upload("notebook") 168 | if (targets.note) { 169 | // notebook 170 | const notebook = new Notebook({ 171 | notebook_src, 172 | registry_src: collection_registry_src, 173 | profile, 174 | }) 175 | await notebook.createRegistry() 176 | opt.notebook = { 177 | notebook_src, 178 | registry: notebook.registry, 179 | registry_src: collection_registry_src, 180 | profile: opt.profile, 181 | } 182 | 183 | // note 184 | const notelib_src = await src.upload("atomic-note-library") 185 | const note_src = await src.upload("atomic-note") 186 | const proxy = await src.upload("proxy") 187 | const { pid: proxy_pid } = await ao.deploy({ src: proxy, module }) 188 | opt.note = { 189 | proxy: proxy_pid, 190 | note_src, 191 | profile: opt.profile, 192 | } 193 | const { mid } = await ao.deploy({ 194 | tags: { Library: "Atomic-Notes", Version: "1.0.0" }, 195 | src: notelib_src, 196 | }) 197 | opt.note.notelib_mid = mid 198 | } 199 | if (targets.asset) { 200 | // collection 201 | const collection = new Collection({ 202 | collection_src: notebook_src, 203 | registry_src: collection_registry_src, 204 | profile, 205 | }) 206 | await collection.createRegistry() 207 | 208 | opt.collection = { 209 | collection_src, 210 | registry: collection.registry, 211 | registry_src: collection_registry_src, 212 | profile: opt.profile, 213 | } 214 | 215 | // asset 216 | const asset_src = await src.upload("atomic-asset") 217 | opt.asset = { asset_src, profile: opt.profile } 218 | } 219 | } 220 | } 221 | if (cache) { 222 | if (!existsSync(_cacheDir)) mkdirSync(_cacheDir) 223 | writeFileSync(optPath, JSON.stringify(opt)) 224 | } 225 | return { opt, profile, ao, ar, thumbnail, banner, src, ao2 } 226 | } 227 | 228 | export const ok = obj => { 229 | if (obj.err) console.log(obj.err) 230 | assert.equal(obj.err, null) 231 | return obj 232 | } 233 | 234 | export const fail = obj => { 235 | if (!obj.err) console.log(obj.res) 236 | assert.notEqual(obj.err, null) 237 | return obj 238 | } 239 | -------------------------------------------------------------------------------- /sdk/src/index.js: -------------------------------------------------------------------------------- 1 | import Note from "./note.js" 2 | import Notebook from "./notebook.js" 3 | import { AR, AO } from "wao" 4 | import Profile from "./profile.js" 5 | import Collection from "./collection.js" 6 | import Asset from "./asset.js" 7 | 8 | export { Note, Notebook, Profile, AO, AR, Collection, Asset } 9 | -------------------------------------------------------------------------------- /sdk/src/lua/aos-sqlite.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/sdk/src/lua/aos-sqlite.wasm -------------------------------------------------------------------------------- /sdk/src/lua/aos.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/sdk/src/lua/aos.wasm -------------------------------------------------------------------------------- /sdk/src/lua/aos2.lua: -------------------------------------------------------------------------------- 1 | local count = 0 2 | 3 | Handlers.add( 4 | "Print", 5 | "Print", 6 | function (msg) 7 | print('Hello World!') 8 | local name = Send({Target = msg.Addr, Action = "Get", Tags = { Origin = msg.From, To = msg.Addr, ID = "1" } }).receive().Data 9 | local name2 = Send({Target = msg.Addr2, Action = "Get2", Tags = { Origin = msg.From, To2 = msg.Addr2 } }).receive().Data 10 | local name3 = Send({Target = msg.Addr, Action = "Get", Tags = { Origin = msg.From, To = msg.Addr, ID = "3" } }).receive().Data 11 | msg.reply({ Data = name3 .. " printed!"}) 12 | local name4 = Send({Target = msg.Addr2, Action = "Get2", Tags = { Origin = msg.From, To2 = msg.Addr2 } }).receive().Data 13 | msg.reply({ Data = name4 .. " printed!"}) 14 | end 15 | ) 16 | 17 | Handlers.add( 18 | "Get", 19 | "Get", 20 | function (msg) 21 | count = count + 1 22 | msg.reply({ Data = "Bob" .. count, Tags = { Ret = msg.From, Origin = msg.Origin, To = msg.To } }) 23 | end 24 | ) 25 | 26 | Handlers.add( 27 | "Get2", 28 | "Get2", 29 | function (msg) 30 | count = count + 1 31 | msg.reply({ Data = "Alice" .. count, Tags = { Ret = msg.From, Origin = msg.Origin, To = msg.To } }) 32 | end 33 | ) 34 | -------------------------------------------------------------------------------- /sdk/src/lua/aos2_0_1.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weavedb/atomic-notes/f8d00ce10c483db1d408a887e0343646fcc3fa12/sdk/src/lua/aos2_0_1.wasm -------------------------------------------------------------------------------- /sdk/src/lua/atomic-asset.lua: -------------------------------------------------------------------------------- 1 | local bint = require('.bint')(256) 2 | local json = require('json') 3 | local base64 = require(".base64") 4 | local ao = require("ao") 5 | 6 | if Name ~= '' then Name = '' end 7 | if Creator ~= '' then Creator = '' end 8 | if Ticker ~= '' then Ticker = '' end 9 | if Denomination ~= '' then Denomination = '' end 10 | if not Balances then Balances = { [''] = '' } end 11 | 12 | Transferable = true 13 | 14 | local function checkValidAddress(address) 15 | if not address or type(address) ~= 'string' then 16 | return false 17 | end 18 | 19 | return string.match(address, "^[%w%-_]+$") ~= nil and #address == 43 20 | end 21 | 22 | local function checkValidAmount(data) 23 | return (math.type(tonumber(data)) == 'integer' or math.type(tonumber(data)) == 'float') and bint(data) > 0 24 | end 25 | 26 | local function decodeMessageData(data) 27 | local status, decodedData = pcall(json.decode, data) 28 | 29 | if not status or type(decodedData) ~= 'table' then 30 | return false, nil 31 | end 32 | 33 | return true, decodedData 34 | end 35 | 36 | -- Read process state 37 | Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg) 38 | ao.send({ 39 | Target = msg.From, 40 | Action = 'Read-Success', 41 | Data = json.encode({ 42 | Name = Name, 43 | Ticker = Ticker, 44 | Denomination = Denomination, 45 | Balances = Balances, 46 | Transferable = Transferable, 47 | }) 48 | }) 49 | end) 50 | 51 | -- Transfer balance to recipient (Data - { Recipient, Quantity }) 52 | Handlers.add('Transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), function(msg) 53 | if not Transferable then 54 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Transfers are not allowed' } }) 55 | return 56 | end 57 | 58 | local data = { 59 | Recipient = msg.Tags.Recipient, 60 | Quantity = msg.Tags.Quantity 61 | } 62 | 63 | if checkValidAddress(data.Recipient) and checkValidAmount(data.Quantity) and bint(data.Quantity) <= bint(Balances[msg.From]) then 64 | -- Transfer is valid, calculate balances 65 | if not Balances[data.Recipient] then 66 | Balances[data.Recipient] = '0' 67 | end 68 | 69 | Balances[msg.From] = tostring(bint(Balances[msg.From]) - bint(data.Quantity)) 70 | Balances[data.Recipient] = tostring(bint(Balances[data.Recipient]) + bint(data.Quantity)) 71 | 72 | -- If new balance zeroes out then remove it from the table 73 | if bint(Balances[msg.From]) <= 0 then 74 | Balances[msg.From] = nil 75 | end 76 | if bint(Balances[data.Recipient]) <= 0 then 77 | Balances[data.Recipient] = nil 78 | end 79 | 80 | local debitNoticeTags = { 81 | Status = 'Success', 82 | Message = 'Balance transferred, debit notice issued', 83 | Recipient = msg.Tags.Recipient, 84 | Quantity = msg.Tags.Quantity, 85 | } 86 | 87 | local creditNoticeTags = { 88 | Status = 'Success', 89 | Message = 'Balance transferred, credit notice issued', 90 | Sender = msg.From, 91 | Quantity = msg.Tags.Quantity, 92 | } 93 | 94 | for tagName, tagValue in pairs(msg) do 95 | if string.sub(tagName, 1, 2) == 'X-' then 96 | debitNoticeTags[tagName] = tagValue 97 | creditNoticeTags[tagName] = tagValue 98 | end 99 | end 100 | 101 | -- Send a debit notice to the sender 102 | ao.send({ 103 | Target = msg.From, 104 | Action = 'Debit-Notice', 105 | Tags = debitNoticeTags, 106 | Data = json.encode({ 107 | Recipient = data.Recipient, 108 | Quantity = tostring(data.Quantity) 109 | }) 110 | }) 111 | 112 | -- Send a credit notice to the recipient 113 | ao.send({ 114 | Target = data.Recipient, 115 | Action = 'Credit-Notice', 116 | Tags = creditNoticeTags, 117 | Data = json.encode({ 118 | Sender = msg.From, 119 | Quantity = tostring(data.Quantity) 120 | }) 121 | }) 122 | end 123 | end) 124 | 125 | -- Mint new tokens (Data - { Quantity }) 126 | Handlers.add('Mint', Handlers.utils.hasMatchingTag('Action', 'Mint'), function(msg) 127 | local decodeCheck, data = decodeMessageData(msg.Data) 128 | 129 | if decodeCheck and data then 130 | -- Check if quantity is present 131 | if not data.Quantity then 132 | ao.send({ Target = msg.From, Action = 'Input-Error', Tags = { Status = 'Error', Message = 'Invalid arguments, required { Quantity }' } }) 133 | return 134 | end 135 | 136 | -- Check if quantity is a valid integer greater than zero 137 | if not checkValidAmount(data.Quantity) then 138 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Quantity must be an integer greater than zero' } }) 139 | return 140 | end 141 | 142 | -- Check if owner is sender 143 | if msg.From ~= Owner then 144 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Only the process owner can mint new tokens' } }) 145 | return 146 | end 147 | 148 | -- Mint request is valid, add tokens to the pool 149 | if not Balances[Owner] then 150 | Balances[Owner] = '0' 151 | end 152 | 153 | Balances[Owner] = tostring(bint(Balances[Owner]) + bint(data.Quantity)) 154 | 155 | ao.send({ Target = msg.From, Action = 'Mint-Success', Tags = { Status = 'Success', Message = 'Tokens minted' } }) 156 | else 157 | ao.send({ 158 | Target = msg.From, 159 | Action = 'Input-Error', 160 | Tags = { 161 | Status = 'Error', 162 | Message = string.format('Failed to parse data, received: %s. %s', msg.Data, 163 | 'Data must be an object - { Quantity }') 164 | } 165 | }) 166 | end 167 | end) 168 | 169 | -- Read balance (Data - { Target }) 170 | Handlers.add('Balance', Handlers.utils.hasMatchingTag('Action', 'Balance'), function(msg) 171 | local decodeCheck, data = decodeMessageData(msg.Data) 172 | 173 | if decodeCheck and data then 174 | -- Check if target is present 175 | if not data.Target then 176 | ao.send({ Target = msg.From, Action = 'Input-Error', Tags = { Status = 'Error', Message = 'Invalid arguments, required { Target }' } }) 177 | return 178 | end 179 | 180 | -- Check if target is a valid address 181 | if not checkValidAddress(data.Target) then 182 | ao.send({ Target = msg.From, Action = 'Validation-Error', Tags = { Status = 'Error', Message = 'Target is not a valid address' } }) 183 | return 184 | end 185 | 186 | -- Check if target has a balance 187 | if not Balances[data.Target] then 188 | ao.send({ Target = msg.From, Action = 'Read-Error', Tags = { Status = 'Error', Message = 'Target does not have a balance' } }) 189 | return 190 | end 191 | 192 | ao.send({ 193 | Target = msg.From, 194 | Action = 'Read-Success', 195 | Tags = { Status = 'Success', Message = 'Balance received' }, 196 | Data = 197 | Balances[data.Target] 198 | }) 199 | else 200 | ao.send({ 201 | Target = msg.From, 202 | Action = 'Input-Error', 203 | Tags = { 204 | Status = 'Error', 205 | Message = string.format('Failed to parse data, received: %s. %s', msg.Data, 206 | 'Data must be an object - { Target }') 207 | } 208 | }) 209 | end 210 | end) 211 | 212 | -- Read balances 213 | Handlers.add('Balances', Handlers.utils.hasMatchingTag('Action', 'Balances'), 214 | function(msg) ao.send({ Target = msg.From, Action = 'Read-Success', Data = json.encode(Balances) }) end) 215 | 216 | -- Initialize a request to add the uploaded asset to a profile 217 | Handlers.add('Add-Asset-To-Profile', Handlers.utils.hasMatchingTag('Action', 'Add-Asset-To-Profile'), function(msg) 218 | if checkValidAddress(msg.Tags.ProfileProcess) then 219 | -- ao.assign({ Processes = { msg.Tags.ProfileProcess }, Message = ao.id }) 220 | ao.send({ 221 | Target = msg.Tags.ProfileProcess, 222 | Action = 'Add-Uploaded-Asset', 223 | Data = json.encode({ 224 | Id = ao.id, 225 | Quantity = msg.Tags.Quantity or '0' 226 | }) 227 | }) 228 | else 229 | ao.send({ 230 | Target = msg.From, 231 | Action = 'Input-Error', 232 | Tags = { 233 | Status = 'Error', 234 | Message = 'ProfileProcess tag not specified or not a valid Process ID' 235 | } 236 | }) 237 | end 238 | end) 239 | -------------------------------------------------------------------------------- /sdk/src/lua/atomic-note.lua: -------------------------------------------------------------------------------- 1 | if Name ~= '' then Name = '' end 2 | if Description ~= '' then Description = '' end 3 | if Thumbnail ~= '' then Thumbnail = '' end 4 | if Creator ~= '' then Creator = '' end 5 | if Ticker ~= '' then Ticker = '' end 6 | if Denomination ~= '' then Denomination = '' end 7 | if not Balances then Balances = { [''] = '' } end 8 | if DateCreated ~= '' then DateCreated = '' end 9 | if not Collections then Collections = {} end 10 | 11 | ao.addAssignable("LIBRARY", { Id = '' }) 12 | -------------------------------------------------------------------------------- /sdk/src/lua/collection-registry.lua: -------------------------------------------------------------------------------- 1 | local json = require('json') 2 | 3 | -- Collections { Id, Name, Description, Creator, DateCreated, Banner, Thumbnail }[] 4 | if not Collections then Collections = {} end 5 | 6 | -- CollectionsByUser: { Creator: { CollectionIds } } 7 | if not CollectionsByUser then CollectionsByUser = {} end 8 | 9 | -- Add collection to registry 10 | Handlers.add('Add-Collection', Handlers.utils.hasMatchingTag('Action', 'Add-Collection'), function(msg) 11 | local data = { 12 | Id = msg.Tags.CollectionId, 13 | Name = msg.Tags.Name, 14 | Description = msg.Tags.Description, 15 | Creator = msg.Tags.Creator, 16 | DateCreated = msg.Tags.DateCreated, 17 | Banner = msg.Tags.Banner, 18 | Thumbnail = msg.Tags.Thumbnail 19 | } 20 | 21 | local requiredFields = { 22 | { key = 'Id', name = 'CollectionId' }, 23 | { key = 'Name', name = 'Name' }, 24 | { key = 'Creator', name = 'Creator' } 25 | } 26 | 27 | for _, field in ipairs(requiredFields) do 28 | if not data[field.key] or data[field.key] == '' then 29 | ao.send({ 30 | Target = msg.From, 31 | Action = 'Action-Response', 32 | Tags = { 33 | Status = 'Error', 34 | Message = 'Invalid or missing ' .. field.name 35 | } 36 | }) 37 | return 38 | end 39 | end 40 | 41 | for _, collection in ipairs(Collections) do 42 | if collection.Id == data.Id then 43 | ao.send({ 44 | Target = msg.From, 45 | Action = 'Action-Response', 46 | Tags = { 47 | Status = 'Error', 48 | Message = 'Collection with this ID already exists' 49 | } 50 | }) 51 | return 52 | end 53 | end 54 | 55 | table.insert(Collections, { 56 | Id = data.Id, 57 | Name = data.Name, 58 | Description = data.Description, 59 | Creator = data.Creator, 60 | DateCreated = data.DateCreated, 61 | Banner = data.Banner, 62 | Thumbnail = data.Thumbnail 63 | }) 64 | 65 | if not CollectionsByUser[data.Creator] then 66 | CollectionsByUser[data.Creator] = {} 67 | end 68 | table.insert(CollectionsByUser[data.Creator], data.Id) 69 | 70 | ao.send({ 71 | Target = msg.From, 72 | Action = 'Action-Response', 73 | Tags = { 74 | Status = 'Success', 75 | Message = 'Collection added successfully' 76 | } 77 | }) 78 | end) 79 | 80 | -- Get collections by user 81 | Handlers.add('Get-Collections', Handlers.utils.hasMatchingTag('Action', 'Get-Collections'), function(msg) 82 | ao.send({ 83 | Target = msg.From, 84 | Action = 'Action-Response', 85 | Tags = { 86 | Status = 'Success', 87 | Message = 'Collections fetched successfully' 88 | }, 89 | Data = json.encode({ Collections = Collections }) 90 | }) 91 | end) 92 | 93 | -- Get collections by user 94 | Handlers.add('Get-Collections-By-User', Handlers.utils.hasMatchingTag('Action', 'Get-Collections-By-User'), function(msg) 95 | local creator = msg.Tags.Creator 96 | 97 | if not creator or creator == '' then 98 | ao.send({ 99 | Target = msg.From, 100 | Action = 'Action-Response', 101 | Tags = { 102 | Status = 'Error', 103 | Message = 'Invalid or missing Creator' 104 | } 105 | }) 106 | return 107 | end 108 | 109 | local collectionIds = CollectionsByUser[creator] or {} 110 | local userCollections = {} 111 | 112 | for _, collectionId in ipairs(collectionIds) do 113 | for _, collection in ipairs(Collections) do 114 | if collection.Id == collectionId then 115 | table.insert(userCollections, collection) 116 | break 117 | end 118 | end 119 | end 120 | 121 | ao.send({ 122 | Target = msg.From, 123 | Action = 'Action-Response', 124 | Tags = { 125 | Status = 'Success', 126 | Message = 'Collections fetched successfully' 127 | }, 128 | Data = json.encode({ 129 | Creator = creator, 130 | Collections = userCollections 131 | }) 132 | }) 133 | end) 134 | 135 | -- Remove collection by ID 136 | Handlers.add('Remove-Collection', Handlers.utils.hasMatchingTag('Action', 'Remove-Collection'), function(msg) 137 | if msg.From ~= Owner and msg.From ~= ao.id then 138 | ao.send({ 139 | Target = msg.From, 140 | Action = 'Authorization-Error', 141 | Tags = { 142 | Status = 'Error', 143 | Message = 'Unauthorized to access this handler' 144 | } 145 | }) 146 | return 147 | end 148 | 149 | local collectionId = msg.Tags.CollectionId 150 | 151 | if not collectionId or collectionId == '' then 152 | ao.send({ 153 | Target = msg.From, 154 | Action = 'Action-Response', 155 | Tags = { 156 | Status = 'Error', 157 | Message = 'Invalid or missing CollectionId' 158 | } 159 | }) 160 | return 161 | end 162 | 163 | local collectionIndex = nil 164 | local collectionOwner = nil 165 | 166 | for index, collection in ipairs(Collections) do 167 | if collection.Id == collectionId then 168 | collectionIndex = index 169 | collectionOwner = collection.Creator 170 | break 171 | end 172 | end 173 | 174 | if not collectionIndex then 175 | ao.send({ 176 | Target = msg.From, 177 | Action = 'Action-Response', 178 | Tags = { 179 | Status = 'Error', 180 | Message = 'Collection not found' 181 | } 182 | }) 183 | return 184 | end 185 | 186 | table.remove(Collections, collectionIndex) 187 | for i, id in ipairs(CollectionsByUser[collectionOwner]) do 188 | if id == collectionId then 189 | table.remove(CollectionsByUser[collectionOwner], i) 190 | break 191 | end 192 | end 193 | 194 | ao.send({ 195 | Target = msg.From, 196 | Action = 'Action-Response', 197 | Tags = { 198 | Status = 'Success', 199 | Message = 'Collection removed successfully' 200 | } 201 | }) 202 | end) 203 | -------------------------------------------------------------------------------- /sdk/src/lua/collection.lua: -------------------------------------------------------------------------------- 1 | local json = require('json') 2 | 3 | if Name ~= '' then Name = '' end 4 | if Description ~= '' then Description = '' end 5 | if Creator ~= '' then Creator = '' end 6 | if Banner ~= '' then Banner = '' end 7 | if Thumbnail ~= '' then Thumbnail = '' end 8 | 9 | if DateCreated ~= '' then DateCreated = '' end 10 | if LastUpdate ~= '' then LastUpdate = '' end 11 | 12 | -- Assets: Id[] 13 | if not Assets then Assets = {} end 14 | 15 | local function decodeMessageData(data) 16 | local status, decodedData = pcall(json.decode, data) 17 | 18 | if not status or type(decodedData) ~= 'table' then 19 | return false, nil 20 | end 21 | 22 | return true, decodedData 23 | end 24 | 25 | local function assetExists(assetId) 26 | for _, id in ipairs(Assets) do 27 | if id == assetId then 28 | return true 29 | end 30 | end 31 | return false 32 | end 33 | 34 | local function checkValidAddress(address) 35 | if not address or type(address) ~= 'string' then 36 | return false 37 | end 38 | 39 | return string.match(address, "^[%w%-_]+$") ~= nil and #address == 43 40 | end 41 | 42 | Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg) 43 | ao.send({ 44 | Target = msg.From, 45 | Data = json.encode({ 46 | Name = Name, 47 | Description = Description, 48 | Creator = Creator, 49 | Banner = Banner, 50 | Thumbnail = Thumbnail, 51 | DateCreated = DateCreated, 52 | Assets = Assets 53 | }) 54 | }) 55 | end) 56 | 57 | -- Add or remove assets 58 | Handlers.add('Update-Assets', Handlers.utils.hasMatchingTag('Action', 'Update-Assets'), function(msg) 59 | if msg.From ~= Owner and msg.From ~= ao.id and msg.From ~= Creator then 60 | ao.send({ 61 | Target = msg.From, 62 | Action = 'Authorization-Error', 63 | Tags = { 64 | Status = 'Error', 65 | Message = 'Unauthorized to access this handler' 66 | } 67 | }) 68 | return 69 | end 70 | 71 | local decodeCheck, data = decodeMessageData(msg.Data) 72 | 73 | if decodeCheck and data then 74 | if not data.AssetIds or type(data.AssetIds) ~= 'table' or #data.AssetIds == 0 then 75 | ao.send({ 76 | Target = msg.From, 77 | Action = 'Action-Response', 78 | Tags = { 79 | Status = 'Error', 80 | Message = 'Invalid or empty AssetIds list' 81 | } 82 | }) 83 | return 84 | end 85 | 86 | if not data.UpdateType or (data.UpdateType ~= 'Add' and data.UpdateType ~= 'Remove') then 87 | ao.send({ 88 | Target = msg.From, 89 | Action = 'Action-Response', 90 | Tags = { 91 | Status = 'Error', 92 | Message = 'UpdateType argument required (Add | Remove)' 93 | } 94 | }) 95 | return 96 | end 97 | 98 | if data.UpdateType == 'Add' then 99 | for _, assetId in ipairs(data.AssetIds) do 100 | if not assetExists(assetId) then 101 | table.insert(Assets, assetId) 102 | ao.send({ 103 | Target = assetId, 104 | Action = 'Add-To-Collection-Success', 105 | Tags = {} 106 | }) 107 | end 108 | end 109 | end 110 | 111 | if data.UpdateType == 'Remove' then 112 | for _, assetId in ipairs(data.AssetIds) do 113 | for i, id in ipairs(Assets) do 114 | if id == assetId then 115 | table.remove(Assets, i) 116 | ao.send({ 117 | Target = assetId, 118 | Action = 'Remove-From-Collection-Success', 119 | Tags = {} 120 | }) 121 | break 122 | end 123 | end 124 | end 125 | end 126 | 127 | LastUpdate = msg.Timestamp 128 | 129 | ao.send({ 130 | Target = msg.From, 131 | Action = 'Action-Response', 132 | Tags = { 133 | Status = 'Success', 134 | Message = 'Assets updated successfully' 135 | } 136 | }) 137 | else 138 | ao.send({ 139 | Target = msg.From, 140 | Action = 'Input-Error', 141 | Tags = { 142 | Status = 'Error', 143 | Message = string.format('Failed to parse data, received: %s. %s', 144 | msg.Data, 145 | 'Data must be an object - { AssetIds: [], UpdateType }') 146 | } 147 | }) 148 | end 149 | end) 150 | 151 | -- Initialize a request to add the uploaded asset to a profile 152 | Handlers.add('Add-Collection-To-Profile', Handlers.utils.hasMatchingTag('Action', 'Add-Collection-To-Profile'), function(msg) 153 | if checkValidAddress(msg.Tags.ProfileProcess) then 154 | -- ao.assign({Processes = {msg.Tags.ProfileProcess}, Message = ao.id}) 155 | ao.send({ 156 | Target = msg.Tags.ProfileProcess, 157 | Action = 'Add-Collection', 158 | Data = json.encode({ 159 | Id = ao.id, 160 | Name = Name 161 | }) 162 | }) 163 | else 164 | ao.send({ 165 | Target = msg.From, 166 | Action = 'Input-Error', 167 | Tags = { 168 | Status = 'Error', 169 | Message = 'ProfileProcess tag not specified or not a valid Process ID' 170 | } 171 | }) 172 | end 173 | end) 174 | -------------------------------------------------------------------------------- /sdk/src/lua/notebook.lua: -------------------------------------------------------------------------------- 1 | local json = require('json') 2 | 3 | if Name ~= '' then Name = '' end 4 | if Description ~= '' then Description = '' end 5 | if Creator ~= '' then Creator = '' end 6 | if Banner ~= '' then Banner = '' end 7 | if Thumbnail ~= '' then Thumbnail = '' end 8 | 9 | if DateCreated ~= '' then DateCreated = '' end 10 | if LastUpdate ~= '' then LastUpdate = '' end 11 | 12 | -- Assets: Id[] 13 | if not Assets then Assets = {} end 14 | 15 | local function decodeMessageData(data) 16 | local status, decodedData = pcall(json.decode, data) 17 | 18 | if not status or type(decodedData) ~= 'table' then 19 | return false, nil 20 | end 21 | 22 | return true, decodedData 23 | end 24 | 25 | local function assetExists(assetId) 26 | for _, id in ipairs(Assets) do 27 | if id == assetId then 28 | return true 29 | end 30 | end 31 | return false 32 | end 33 | 34 | local function checkValidAddress(address) 35 | if not address or type(address) ~= 'string' then 36 | return false 37 | end 38 | 39 | return string.match(address, "^[%w%-_]+$") ~= nil and #address == 43 40 | end 41 | 42 | Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg) 43 | ao.send({ 44 | Target = msg.From, 45 | Data = json.encode({ 46 | Name = Name, 47 | Description = Description, 48 | Creator = Creator, 49 | Banner = Banner, 50 | Thumbnail = Thumbnail, 51 | DateCreated = DateCreated, 52 | Assets = Assets 53 | }) 54 | }) 55 | end) 56 | 57 | -- Add or remove assets 58 | Handlers.add('Update-Assets', Handlers.utils.hasMatchingTag('Action', 'Update-Assets'), function(msg) 59 | if msg.From ~= Owner and msg.From ~= ao.id and msg.From ~= Creator then 60 | ao.send({ 61 | Target = msg.From, 62 | Action = 'Authorization-Error', 63 | Tags = { 64 | Status = 'Error', 65 | Message = 'Unauthorized to access this handler' 66 | } 67 | }) 68 | return 69 | end 70 | 71 | local decodeCheck, data = decodeMessageData(msg.Data) 72 | 73 | if decodeCheck and data then 74 | if not data.AssetIds or type(data.AssetIds) ~= 'table' or #data.AssetIds == 0 then 75 | ao.send({ 76 | Target = msg.From, 77 | Action = 'Action-Response', 78 | Tags = { 79 | Status = 'Error', 80 | Message = 'Invalid or empty AssetIds list' 81 | } 82 | }) 83 | return 84 | end 85 | 86 | if not data.UpdateType or (data.UpdateType ~= 'Add' and data.UpdateType ~= 'Remove') then 87 | ao.send({ 88 | Target = msg.From, 89 | Action = 'Action-Response', 90 | Tags = { 91 | Status = 'Error', 92 | Message = 'UpdateType argument required (Add | Remove)' 93 | } 94 | }) 95 | return 96 | end 97 | 98 | if data.UpdateType == 'Add' then 99 | for _, assetId in ipairs(data.AssetIds) do 100 | if not assetExists(assetId) then 101 | table.insert(Assets, assetId) 102 | ao.send({ 103 | Target = assetId, 104 | Action = 'Add-To-Collection-Success', 105 | Tags = {} 106 | }) 107 | end 108 | end 109 | end 110 | 111 | if data.UpdateType == 'Remove' then 112 | for _, assetId in ipairs(data.AssetIds) do 113 | for i, id in ipairs(Assets) do 114 | if id == assetId then 115 | table.remove(Assets, i) 116 | ao.send({ 117 | Target = assetId, 118 | Action = 'Remove-From-Collection-Success', 119 | Tags = {} 120 | }) 121 | break 122 | end 123 | end 124 | end 125 | end 126 | 127 | LastUpdate = msg.Timestamp 128 | 129 | ao.send({ 130 | Target = msg.From, 131 | Action = 'Action-Response', 132 | Tags = { 133 | Status = 'Success', 134 | Message = 'Assets updated successfully' 135 | } 136 | }) 137 | else 138 | ao.send({ 139 | Target = msg.From, 140 | Action = 'Input-Error', 141 | Tags = { 142 | Status = 'Error', 143 | Message = string.format('Failed to parse data, received: %s. %s', 144 | msg.Data, 145 | 'Data must be an object - { AssetIds: [], UpdateType }') 146 | } 147 | }) 148 | end 149 | end) 150 | 151 | -- Initialize a request to add the uploaded asset to a profile 152 | Handlers.add('Add-Collection-To-Profile', Handlers.utils.hasMatchingTag('Action', 'Add-Collection-To-Profile'), function(msg) 153 | if checkValidAddress(msg.Tags.ProfileProcess) then 154 | -- ao.assign({Processes = {msg.Tags.ProfileProcess}, Message = ao.id}) 155 | ao.send({ 156 | Target = msg.Tags.ProfileProcess, 157 | Action = 'Add-Collection', 158 | Data = json.encode({ 159 | Id = ao.id, 160 | Name = Name 161 | }) 162 | }) 163 | else 164 | ao.send({ 165 | Target = msg.From, 166 | Action = 'Input-Error', 167 | Tags = { 168 | Status = 'Error', 169 | Message = 'ProfileProcess tag not specified or not a valid Process ID' 170 | } 171 | }) 172 | end 173 | end) 174 | -------------------------------------------------------------------------------- /sdk/src/lua/proxy.lua: -------------------------------------------------------------------------------- 1 | local ao = require("ao") 2 | 3 | Handlers.add( 4 | "allow", 5 | Handlers.utils.hasMatchingTag("Action", "Allow"), 6 | function (msg) 7 | ao.addAssignable({ From = msg.From }) 8 | Handlers.utils.reply("allowed!")(msg) 9 | end 10 | ) 11 | 12 | Handlers.add( 13 | "assign", 14 | Handlers.utils.hasMatchingTag("Type", "Process"), 15 | function (msg) 16 | assert(msg.From == msg.Owner, 'only process owner can execute!') 17 | assert(msg.Tags["Content-Type"] == "text/markdown" or msg.Tags["Content-Type"] == "text/plain", 'only markdown and text are allowed!') 18 | ao.send({ 19 | Target = msg.Id, 20 | Data = msg.Data, 21 | Tags = { Action = "Assigned" } 22 | }) 23 | end 24 | ) 25 | -------------------------------------------------------------------------------- /sdk/src/note.js: -------------------------------------------------------------------------------- 1 | import { srcs, wait, udl } from "./utils.js" 2 | import Profile from "./profile.js" 3 | import Asset from "./asset.js" 4 | import { mergeLeft } from "ramda" 5 | 6 | class Note extends Asset { 7 | constructor({ 8 | proxy = srcs.proxy, 9 | render_with = srcs.render, 10 | note_src = srcs.note_src, 11 | notelib_mid = srcs.notelib_mid, 12 | pid, 13 | profile = {}, 14 | ao = {}, 15 | ar = {}, 16 | } = {}) { 17 | super({ proxy, render_with, asset_src: note_src, pid, profile, ao, ar }) 18 | this.__type__ = "note" 19 | this.note_src = note_src 20 | this.render_with = render_with 21 | this.proxy = proxy 22 | this.notelib_mid = notelib_mid 23 | } 24 | 25 | async create({ 26 | jwk, 27 | content_type = "text/markdown", 28 | src = this.note_src, 29 | src_data, 30 | data, 31 | fills = {}, 32 | tags = {}, 33 | info: { title, description, thumbnail, thumbnail_data, thumbnail_type }, 34 | token: { fraction = "1" }, 35 | udl: { payment, access, derivations, commercial, training }, 36 | cb, 37 | }) { 38 | const creator = this.profile.id 39 | if (!creator) return { err: "no ao profile id" } 40 | const date = Date.now() 41 | let _tags = { 42 | Action: "Add-Uploaded-Asset", 43 | Title: title, 44 | Description: description, 45 | "Date-Created": Number(date).toString(), 46 | Implements: "ANS-110", 47 | Type: "blog-post", 48 | "Asset-Type": "Atomic-Note", 49 | "Render-With": this.render_with, 50 | "Content-Type": content_type, 51 | ...udl({ payment, access, derivations, commercial, training }), 52 | ...tags, 53 | } 54 | if (!/^\s*$/.test(thumbnail)) _tags["Thumbnail"] = thumbnail 55 | if (creator) _tags["Creator"] = creator 56 | const balance = 57 | typeof fraction === "number" ? Number(fraction * 1).toString() : fraction 58 | fills = mergeLeft(fills, { 59 | NAME: title, 60 | CREATOR: creator, 61 | TICKER: "ATOMIC", 62 | DENOMINATION: "1", 63 | DESCRIPTION: description, 64 | THUMBNAIL: thumbnail ?? "None", 65 | DATECREATED: date, 66 | BALANCE: balance, 67 | LIBRARY: this.notelib_mid, 68 | }) 69 | let fns = [ 70 | { 71 | fn: "deploy", 72 | args: { src_data, src, fills, tags: _tags, data }, 73 | then: ({ pid, args }) => { 74 | this.pid = pid 75 | args.pid = pid 76 | }, 77 | }, 78 | { 79 | fn: "asgn", 80 | args: { mid: this.notelib_mid }, 81 | err: ({ args, res }) => typeof res?.Output?.data !== "object", 82 | }, 83 | { fn: "allow", bind: this }, 84 | { fn: "assignData", bind: this }, 85 | { fn: "add", bind: this, args: { id: creator } }, 86 | ] 87 | if (!thumbnail && thumbnail_data && thumbnail_type) { 88 | fns.unshift({ 89 | fn: "post", 90 | args: { 91 | data: new Uint8Array(thumbnail_data), 92 | tags: { "Content-Type": thumbnail_type }, 93 | }, 94 | then: ({ out, args, id }) => { 95 | out.thumbnail = id 96 | args.fills.THUMBNAIL = id 97 | args.tags.Thumbnail = id 98 | }, 99 | }) 100 | } 101 | return await this.ao.pipe({ jwk, fns, cb }) 102 | } 103 | 104 | async updateInfo({ 105 | title, 106 | description, 107 | thumbnail, 108 | thumbnail_data, 109 | thumbnail_type, 110 | jwk, 111 | cb, 112 | }) { 113 | let info_map = { 114 | Name: title, 115 | Description: description, 116 | Thumbnail: thumbnail, 117 | } 118 | let new_info = [] 119 | for (const k in info_map) { 120 | if (info_map[k]) 121 | new_info.push(`${k} = '${info_map[k].replace(/'/g, "\\'")}'`) 122 | } 123 | const isThumbnail = !thumbnail && thumbnail_data && thumbnail_type 124 | if (new_info.length === 0 && !isThumbnail) return { err: "empty info" } 125 | let fns = [ 126 | { fn: "eval", args: { pid: this.pid, data: new_info.join("\n") } }, 127 | ] 128 | let images = 0 129 | if (isThumbnail) { 130 | images++ 131 | fns.unshift({ 132 | fn: "post", 133 | args: { 134 | data: new Uint8Array(thumbnail_data), 135 | tags: { "Content-Type": thumbnail_type }, 136 | }, 137 | then: ({ args, id, out }) => { 138 | images-- 139 | out.thumbnail = id 140 | if (images === 0) args.data += `\nThumbnail = '${id}'` 141 | }, 142 | }) 143 | } 144 | return await this.ao.pipe({ jwk, fns, cb }) 145 | } 146 | 147 | async allow() { 148 | return await this.ao.msg({ 149 | pid: this.proxy, 150 | act: "Allow", 151 | checkData: "allowed!", 152 | }) 153 | } 154 | 155 | async assignData() { 156 | return await this.ao.asgn({ 157 | pid: this.proxy, 158 | mid: this.pid, 159 | check: { Action: "Assigned" }, 160 | }) 161 | } 162 | 163 | async get(version) { 164 | let tags = {} 165 | if (version) tags.Version = version 166 | const { err, out } = await this.ao.dry({ 167 | act: "Get", 168 | pid: this.pid, 169 | check: { Data: true }, 170 | tags, 171 | get: { obj: { version: "Version", data: "Data", date: "Date" } }, 172 | }) 173 | return out ?? null 174 | } 175 | 176 | async list() { 177 | const { err, out } = await this.ao.dry({ 178 | pid: this.pid, 179 | act: "List", 180 | check: { Versions: true }, 181 | get: { name: "Versions", json: true }, 182 | }) 183 | return out ?? null 184 | } 185 | 186 | async update(data, version) { 187 | let err = null 188 | let res = null 189 | const patches = await this.patches(data) 190 | if (!patches) { 191 | err = "something went wrong" 192 | } else { 193 | const { res: _res, err: _err } = await this.updateVersion( 194 | patches, 195 | version, 196 | ) 197 | if (_err) err = _err 198 | res = _res 199 | } 200 | return { err, res } 201 | } 202 | 203 | async patches(data) { 204 | const { err, out, res } = await this.ao.dry({ 205 | pid: this.pid, 206 | act: "Patches", 207 | data, 208 | check: { Patches: true }, 209 | get: "Patches", 210 | }) 211 | return out ?? null 212 | } 213 | 214 | async updateVersion(patches, version) { 215 | return await this.ao.msg({ 216 | pid: this.pid, 217 | act: "Update", 218 | tags: { Version: version }, 219 | data: patches, 220 | checkData: "updated!", 221 | }) 222 | } 223 | 224 | async editors() { 225 | const { err, out } = await this.ao.dry({ 226 | pid: this.pid, 227 | act: "Editors", 228 | check: { Editors: true }, 229 | get: { name: "Editors", json: true }, 230 | }) 231 | return err ? null : out 232 | } 233 | 234 | async addEditor(editor) { 235 | return await this.ao.msg({ 236 | pid: this.pid, 237 | act: "Add-Editor", 238 | tags: { Editor: editor }, 239 | checkData: "editor added!", 240 | get: { name: "Editors", json: true }, 241 | }) 242 | } 243 | 244 | async removeEditor(editor) { 245 | return await this.ao.msg({ 246 | pid: this.pid, 247 | act: "Remove-Editor", 248 | tags: { Editor: editor }, 249 | checkData: "editor removed!", 250 | get: { name: "Editors", json: true }, 251 | }) 252 | } 253 | } 254 | 255 | export default Note 256 | -------------------------------------------------------------------------------- /sdk/src/notebook.js: -------------------------------------------------------------------------------- 1 | import { srcs } from "./utils.js" 2 | import { is } from "ramda" 3 | import Profile from "./profile.js" 4 | import Collection from "./collection.js" 5 | 6 | class Notebook extends Collection { 7 | constructor({ 8 | registry = srcs.bookreg, 9 | registry_src = srcs.bookreg_src, 10 | thumbnail = srcs.thumb, 11 | banner = srcs.banner, 12 | notebook_src = srcs.notebook_src, 13 | pid, 14 | profile = {}, 15 | ao = {}, 16 | ar = {}, 17 | } = {}) { 18 | super({ 19 | registry, 20 | registry_src, 21 | thumbnail, 22 | banner, 23 | collection_src: notebook_src, 24 | pid, 25 | profile, 26 | ao, 27 | ar, 28 | }) 29 | this.__type__ = "notebook" 30 | } 31 | async addNote(note_pid) { 32 | return await this.update(note_pid) 33 | } 34 | 35 | async removeNote(note_pid) { 36 | return await this.update(note_pid, true) 37 | } 38 | 39 | async addNotes(note_pids) { 40 | return await this.update(note_pids) 41 | } 42 | 43 | async removeNotes(note_pids) { 44 | return await this.update(note_pids, true) 45 | } 46 | } 47 | 48 | export default Notebook 49 | -------------------------------------------------------------------------------- /sdk/src/profile.js: -------------------------------------------------------------------------------- 1 | import { AO } from "wao" 2 | import { map, prop, assoc } from "ramda" 3 | 4 | import { 5 | searchTag, 6 | wait, 7 | query, 8 | isLocalhost, 9 | tag, 10 | action, 11 | getTag, 12 | isData, 13 | getTagVal, 14 | srcs, 15 | } from "./utils.js" 16 | 17 | class Profile { 18 | constructor({ 19 | registry = srcs.registry, 20 | registry_src = srcs.registry_src, 21 | profile_src = srcs.profile, 22 | module, 23 | scheduler, 24 | ar = {}, 25 | ao = {}, 26 | } = {}) { 27 | this.__type__ = "profile" 28 | if (ao?.__type__ === "ao") { 29 | this.ao = ao 30 | } else { 31 | let _ao = typeof ao === "object" ? ao : {} 32 | _ao.ar ??= ar 33 | this.ao = new AO(ao) 34 | } 35 | this.ar = this.ao.ar 36 | this.profile_src = profile_src 37 | this.registry_src = registry_src 38 | this.registry = registry 39 | this.module = module 40 | this.scheduler = scheduler 41 | } 42 | 43 | async init(jwk) { 44 | await this.ao.init(jwk) 45 | await this.profile() 46 | return this 47 | } 48 | 49 | async createRegistry({ jwk } = {}) { 50 | const fns = [ 51 | { 52 | fn: this.ao.deploy, 53 | args: { src: this.registry_src }, 54 | then: ({ pid }) => { 55 | this.registry = pid 56 | }, 57 | }, 58 | { fn: this.initRegistry, bind: this }, 59 | ] 60 | return await this.ao.pipe({ jwk, fns }) 61 | } 62 | 63 | async initRegistry({ registry = this.registry, jwk } = {}) { 64 | const fn = { 65 | args: { 66 | pid: registry, 67 | act: "Prepare-Database", 68 | check: { Status: "Success" }, 69 | }, 70 | then: () => { 71 | this.registry ??= registry 72 | }, 73 | } 74 | return await this.ao.pipe({ jwk, fns: [fn] }) 75 | } 76 | 77 | async updateProfile({ jwk, profile, id }) { 78 | let err = null 79 | ;({ jwk, err } = await this.ar.checkWallet({ jwk })) 80 | if (err) return { err } 81 | id ??= this.id ?? (await this.ids())[0] 82 | if (!id) return { err: "no profile id" } 83 | return await this.ao.msg({ 84 | pid: id, 85 | jwk, 86 | data: JSON.stringify(profile), 87 | act: "Update-Profile", 88 | }) 89 | } 90 | 91 | async ids({ registry = this.registry, addr = this.ar.addr, jwk } = {}) { 92 | const fn = { 93 | fn: this.ao.dry, 94 | args: { 95 | pid: registry, 96 | act: "Get-Profiles-By-Delegate", 97 | data: JSON.stringify({ Address: addr }), 98 | get: { data: true, json: true }, 99 | }, 100 | then: ({ inp, args }) => { 101 | const _ids = map(prop("ProfileId"), inp ?? []) 102 | if (_ids[0] && addr === this.ar.addr) this.id = _ids[0] 103 | return _ids 104 | }, 105 | } 106 | return await this.ao.pipe({ jwk, fns: [fn] }) 107 | } 108 | 109 | async profile({ registry = this.registry, id, jwk } = {}) { 110 | let err = null 111 | ;({ jwk, err } = await this.ar.checkWallet({ jwk })) 112 | if (err) return null 113 | id ??= this.id ?? (await this.ids())[0] 114 | if (!id) return null 115 | const profiles = await this.profiles({ registry, ids: [id], jwk }) 116 | return !profiles ? null : (profiles[0] ?? null) 117 | } 118 | 119 | async profiles({ registry = this.registry, ids, jwk } = {}) { 120 | let err = null 121 | ;({ jwk, err } = await this.ar.checkWallet({ jwk })) 122 | if (err) return null 123 | if (!ids) ids = await this.ids() 124 | if (ids.length === []) return null 125 | const fn = { 126 | fn: this.ao.dry, 127 | args: { 128 | pid: registry, 129 | act: "Get-Metadata-By-ProfileIds", 130 | data: JSON.stringify({ ProfileIds: ids }), 131 | get: { data: true, json: true }, 132 | }, 133 | then: ({ inp }) => inp, 134 | } 135 | return await this.ao.pipe({ jwk, fns: [fn] }) 136 | } 137 | 138 | async info({ id, registry = this.registry, jwk } = {}) { 139 | let err = null 140 | ;({ jwk, err } = await this.ar.checkWallet({ jwk })) 141 | if (err) return null 142 | if (!id) id = this.id ?? (await this.ids())[0] 143 | if (!id) return null 144 | const fn = { 145 | fn: this.ao.dry, 146 | args: { pid: id, act: "Info", get: { json: true, data: true } }, 147 | then: ({ inp: profile }) => (profile ? assoc("Id", id, profile) : null), 148 | } 149 | return await this.ao.pipe({ jwk, fns: [fn] }) 150 | } 151 | 152 | async checkProfile({ jwk }) { 153 | let out = null 154 | let err = null 155 | let attempts = 5 156 | while (attempts > 0) { 157 | await wait(1000) 158 | out = await this.profile({ jwk }) 159 | attempts -= 1 160 | if (out || attempts === 0) break 161 | } 162 | if (!out) err = "no profile found on registry" 163 | return { err, out } 164 | } 165 | 166 | async createProfile({ 167 | registry = this.registry, 168 | profile_src = this.profile_src, 169 | profile, 170 | jwk, 171 | }) { 172 | const fns = [ 173 | { 174 | fn: this.ao.deploy, 175 | args: { 176 | src: profile_src, 177 | fills: { REGISTRY: registry }, 178 | module: this.module, 179 | scheduler: this.scheduler, 180 | }, 181 | then: ({ pid, args }) => { 182 | this.id = pid 183 | args.id = pid 184 | }, 185 | }, 186 | { fn: this.updateProfile, bind: this, args: { profile } }, 187 | { fn: this.checkProfile, bind: this, then: { "_.profile": "in" } }, 188 | ] 189 | return await this.ao.pipe({ jwk, fns }) 190 | } 191 | } 192 | 193 | export default Profile 194 | -------------------------------------------------------------------------------- /sdk/src/utils.js: -------------------------------------------------------------------------------- 1 | import { clone, is, includes, fromPairs, map, isNil } from "ramda" 2 | 3 | const allows = [ 4 | { key: "allowed", val: "Allowed" }, 5 | { key: "disallowed", val: "Disallowed" }, 6 | ] 7 | const allowsMap = fromPairs(allows.map(({ key, val }) => [key, val])) 8 | const accesses = [ 9 | { key: "none", val: "None" }, 10 | { key: "one-time", val: "One-Time" }, 11 | ] 12 | const accessesMap = fromPairs(accesses.map(({ key, val }) => [key, val])) 13 | const payments = [ 14 | { key: "single", val: "Single" }, 15 | { key: "random", val: "Random" }, 16 | { key: "global", val: "Global" }, 17 | ] 18 | const paymentsMap = fromPairs(payments.map(({ key, val }) => [key, val])) 19 | const dTerms = [ 20 | { key: "credit", val: "With Credit" }, 21 | { key: "indication", val: "With Indication" }, 22 | { key: "passthrough", val: "With License Passthrough" }, 23 | { key: "revenue", val: "With Revenue Share" }, 24 | { key: "monthly", val: "With Monthly Fee" }, 25 | { key: "one-time", val: "With One-Time Fee" }, 26 | ] 27 | const dtMap = fromPairs(dTerms.map(({ key, val }) => [key, val])) 28 | const cTerms = [ 29 | { key: "revenue", val: "With Revenue Share" }, 30 | { key: "monthly", val: "With Monthly Fee" }, 31 | { key: "one-time", val: "With One-Time Fee" }, 32 | ] 33 | const ctMap = fromPairs(cTerms.map(({ key, val }) => [key, val])) 34 | const tTerms = [ 35 | { key: "monthly", val: "With Monthly Fee" }, 36 | { key: "one-time", val: "With One-Time Fee" }, 37 | ] 38 | const ttMap = fromPairs(tTerms.map(({ key, val }) => [key, val])) 39 | 40 | const action = value => tag("Action", value) 41 | const tag = (name, value) => ({ name, value: jsonToStr(value) }) 42 | 43 | const wait = ms => new Promise(res => setTimeout(() => res(), ms)) 44 | 45 | const tags = tags => fromPairs(map(v => [v.name, v.value])(tags)) 46 | const ltags = tags => fromPairs(map(v => [v.name.toLowerCase(), v.value])(tags)) 47 | 48 | const validAddress = addr => /^[a-zA-Z0-9_-]{43}$/.test(addr) 49 | 50 | const isRegExp = obj => obj instanceof RegExp 51 | 52 | const getTag = (_tags, name) => tags(_tags)[name] ?? null 53 | 54 | const tagEq = (tags, name, val = null) => { 55 | const tag = getTag(tags, name) 56 | if (val === true) { 57 | return tag !== null 58 | } else if (isRegExp(val)) { 59 | let ok = false 60 | try { 61 | ok = val.test(tag) 62 | } catch (e) {} 63 | return ok 64 | } else return tag === val 65 | } 66 | 67 | const searchTag = (res, name, val) => { 68 | for (let v of res.Messages || []) { 69 | if (tagEq(v.Tags || {}, name, val)) return v 70 | } 71 | return null 72 | } 73 | 74 | const checkTag = (res, name, val) => { 75 | for (let v of res.Messages || []) { 76 | if (tagEq(v.Tags || {}, name, val)) return true 77 | } 78 | return false 79 | } 80 | 81 | const isData = (data, res) => { 82 | for (const v of res.Messages ?? []) { 83 | if (isRegExp(data)) { 84 | try { 85 | if (data.test(v.Data)) return true 86 | } catch (e) {} 87 | } else { 88 | if (data === true || v.Data === data) return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | const query = txid => `query { 95 | transactions(ids: ["${txid}"]) { 96 | edges { node { id tags { name value } owner { address } } } 97 | } 98 | }` 99 | 100 | const queries = to => `query { 101 | transactions (recipients: ["${to}"]){ 102 | edges { node { id recipient tags { name value } owner { address } } } 103 | } 104 | }` 105 | 106 | const isLocalhost = v => includes(v, ["localhost", "127.0.0.1"]) 107 | 108 | const udl = ({ payment, access, derivations, commercial, training }) => { 109 | let tags = { 110 | License: "dE0rmDfl9_OWjkDznNEXHaSO_JohJkRolvMzaCroUdw", 111 | Currency: "xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10", 112 | } 113 | tags["Payment-Mode"] = paymentsMap[payment.mode] 114 | if (payment.mode === "single") tags["Payment-Address"] = payment.recipient 115 | let _access = accessesMap[access.mode] 116 | if (access.mode === "one-time") _access += "-" + access.fee 117 | tags["Access-Fee"] = _access 118 | 119 | let _derivations = allowsMap[derivations.mode] 120 | if (derivations.mode === "allowed") { 121 | if (derivations.term === "revenue") { 122 | _derivations += `-${dtMap[derivations.term].split(" ").join("-")}-${derivations.share}` 123 | } else if ( 124 | derivations.term === "monthly" || 125 | derivations.term === "one-time" 126 | ) { 127 | _derivations += `-${dtMap[derivations.term].split(" ").join("-")}-${derivations.fee}` 128 | } else { 129 | _derivations += `-${dtMap[derivations.term].split(" ").join("-")}-0` 130 | } 131 | } 132 | tags["Derivations"] = _derivations 133 | let _commercial = allowsMap[commercial.mode] 134 | if (commercial.mode === "allowed") { 135 | if (commercial.term === "revenue") { 136 | _commercial += `-${ctMap[commercial.term].split(" ").join("-")}-${commercial.share}` 137 | } else { 138 | _commercial += `-${ctMap[commercial.term].split(" ").join("-")}-${commercial.fee}` 139 | } 140 | } 141 | tags["Commercial-Use"] = _commercial 142 | let _training = allowsMap[training.mode] 143 | if (training.mode === "allowed") { 144 | _training += `-${ttMap[training.term].split(" ").join("-")}-${training.fee}` 145 | } 146 | tags["Data-Model-Training"] = _training 147 | return tags 148 | } 149 | 150 | const modGet = get => { 151 | let _get = clone(get) 152 | if (is(Array, get)) { 153 | _get = { obj: {} } 154 | for (const v of get) { 155 | if (typeof v === "string") _get.obj[v] = v 156 | else if (is(Array, v)) _get.obj[v[0]] = v[1] 157 | else if (is(Object, v)) for (const k in v) _get.obj[k] = v[k] 158 | } 159 | } else if ( 160 | is(Object, get) && 161 | isNil(get.data) && 162 | isNil(get.json) && 163 | isNil(get.name) && 164 | isNil(get.obj) 165 | ) { 166 | _get = { obj: get } 167 | } 168 | return _get 169 | } 170 | 171 | const _getTagVal = (get, res) => { 172 | let out = null 173 | const _get = modGet(get) 174 | if (typeof _get === "object" && _get.obj) { 175 | out = {} 176 | for (const k in _get.obj ?? {}) out[k] = _getTagVal(_get.obj[k], res) 177 | } else { 178 | for (const v of res.Messages ?? []) { 179 | if ( 180 | (typeof _get === "object" && _get.data) || 181 | typeof _get === "boolean" 182 | ) { 183 | if (v.Data) out = v.Data 184 | try { 185 | if (_get.json || _get === true) out = JSON.parse(out) 186 | } catch (e) {} 187 | } else if (typeof _get === "object" && typeof _get.name === "string") { 188 | out = getTag(v.Tags ?? [], _get.name) 189 | try { 190 | if (_get.json) out = JSON.parse(out) 191 | } catch (e) {} 192 | } else out = getTag(v.Tags ?? [], _get) 193 | if (out) break 194 | } 195 | } 196 | return out 197 | } 198 | 199 | const getTagVal = (get, res) => { 200 | const _get = modGet(get) 201 | return _getTagVal(_get, res) 202 | } 203 | 204 | const srcs = { 205 | module: "cNlipBptaF9JeFAf4wUmpi43EojNanIBos3EfNrEOWo", 206 | module_sqlite: "ghSkge2sIUD_F00ym5sEimC63BDBuBrq4b5OcwxOjiw", 207 | module_aos2: "Do_Uc2Sju_ffp6Ev0AnLVdPtot15rvMjP-a9VVaA5fM", 208 | 209 | notelib_src: "h35YJqJ0ve_2pxO5uV1tKTQ44k9WpTBOzk5lGpYcEWs", 210 | note_src: "6BrngB9N_ujSka4BmsZ2HuRbbqePQFkNXQ-tATp03ZU", 211 | notebook_src: "NKISXnq5XseLQd_u-lfO6ThBLuikLoontY47UlONrB4", 212 | asset_src: "CT5qN5e97Fr0wJ8VVu_TRj6qPNWped52IPsJMJ2pd08", 213 | collection_src: "cLzVDfhmC0JAADYyFkdLQbtEMtL4VxbeGv98TADbbRk", 214 | bookreg_src: "4Bm1snpCEHIxYMDdAxiFf6ar81gKQHvElDFeDZbSnJU", 215 | registry_src: "kBk-wRbK5aIZVqDJEzWhjYb5gnydHafrFG3wgItBvuI", 216 | 217 | scheduler: "_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA", 218 | authority: "fcoN_xJeisVsPXA-trzVAuIiqO3ydLQxM-L4XbrQKzY", 219 | 220 | notelib_mid: "ls7QnjCPXDeAF6w7mA0cNAwv9hY7r-W1T31X1iEJMJU", 221 | bookreg: "TFWDmf8a3_nw43GCm_CuYlYoylHAjCcFGbgHfDaGcsg", 222 | thumb: "9v2GrtXpVpPWf9KBuTBdClARjjcDA3NqxFn8Kbn1f2M", 223 | banner: "UuEwLRmuNmqLTDcKqgcxDEV1CWIR_uZ6rxzmKjODlrg", 224 | proxy: "0uboI80S6vMxJD9Yn41Wdwnp9uAHEi4XLGQhBrp3qSQ", 225 | render: "yXXAop3Yxm8QlZRzP46oRxZjCBp88YTpoSTPlTr4TcQ", 226 | registry: "SNy4m-DrqxWl01YqGM4sxI8qCni-58re8uuJLvZPypY", 227 | profile: "uEtSHyK9yDBABomez6ts3LI_8ULvO-rANSgDN_9OzEc", 228 | } 229 | 230 | const buildTags = (act, tags) => { 231 | let _tags = [] 232 | if (act) _tags.push(action(act)) 233 | for (const k in tags) { 234 | if (is(Array)(tags[k])) for (const v of tags[k]) _tags.push(tag(k, v)) 235 | else _tags.push(tag(k, tags[k])) 236 | } 237 | return _tags 238 | } 239 | 240 | const mergeOut = (out, out2, get) => { 241 | const _get = modGet(get) 242 | if (_get.obj) { 243 | for (const k in out2 ?? {}) { 244 | if (isNil(out?.[k])) { 245 | if (!out) out = {} 246 | out[k] = out2[k] 247 | } 248 | } 249 | return out 250 | } else return out2 251 | } 252 | 253 | const mergeChecks = (check1, check2, check) => { 254 | if (!isRegExp(check) && !includes(typeof check)(["string", "boolean"])) { 255 | for (const k in check2 ?? {}) { 256 | if (!check1) check1 = {} 257 | check1[k] = check1[k] || check2[k] 258 | } 259 | return check1 260 | } else return check1 || check2 261 | } 262 | 263 | const isOutComplete = (out, get) => { 264 | if (isNil(out)) return false 265 | const _get = modGet(get) 266 | if (_get.obj) { 267 | for (const k in out ?? {}) { 268 | if (isNil(out[k])) return false 269 | } 270 | } 271 | return true 272 | } 273 | 274 | const isCheckComplete = (checks, check) => { 275 | let i = 0 276 | for (const v of checks) { 277 | if ( 278 | isRegExp(check[i]) || 279 | includes(typeof check[i])(["string", "boolean"]) 280 | ) { 281 | if (!v) return false 282 | } else { 283 | for (const k in v) { 284 | if (!v[k]) return false 285 | } 286 | } 287 | i++ 288 | } 289 | return true 290 | } 291 | 292 | function isJSON(obj) { 293 | if (obj === null || obj === undefined) return false 294 | if ( 295 | typeof obj !== "object" || 296 | obj instanceof Buffer || 297 | obj instanceof ArrayBuffer || 298 | Array.isArray(obj) 299 | ) { 300 | return false 301 | } 302 | 303 | try { 304 | const str = JSON.stringify(obj) 305 | const parsed = JSON.parse(str) 306 | const isjson = typeof parsed === "object" && parsed !== null 307 | return isjson ? str : false 308 | } catch (e) { 309 | return false 310 | } 311 | } 312 | 313 | const jsonToStr = obj => 314 | isJSON(obj) || (is(Number, obj) ? Number(obj).toString() : obj) 315 | 316 | export { 317 | jsonToStr, 318 | mergeChecks, 319 | isCheckComplete, 320 | mergeOut, 321 | isOutComplete, 322 | isRegExp, 323 | buildTags, 324 | srcs, 325 | getTagVal, 326 | isData, 327 | query, 328 | queries, 329 | getTag, 330 | tagEq, 331 | searchTag, 332 | checkTag, 333 | validAddress, 334 | ltags, 335 | tags, 336 | wait, 337 | action, 338 | tag, 339 | isLocalhost, 340 | udl, 341 | isJSON, 342 | } 343 | -------------------------------------------------------------------------------- /sdk/test/README.md: -------------------------------------------------------------------------------- 1 | # Local Testing Atomic Notes 2 | 3 | ## Install AO Localnet 4 | 5 | Make sure you have NodeJS v.22+ and Docker installed. 6 | 7 | ```bash 8 | git clone -b hotfix https://github.com/weavedb/ao-localnet.git 9 | cd ao-localnet/wallets && ./generateAll.sh 10 | cd ../ && sudo docker compose --profile explorer up 11 | ``` 12 | 13 | - ArLocal : [localhost:4000](http://localhost:4000) 14 | - GraphQL : [localhost:4000/graphql](http://localhost:4000/graphql) 15 | - Scar : [localhost:4006](http://localhost:4006) 16 | - MU : [localhost:4002](http://localhost:4002) 17 | - SU : [localhost:4003](http://localhost:4003) 18 | - CU : [localhost:4004](http://localhost:4004) 19 | 20 | In another terminal, set up the AOS environment. 21 | 22 | ```bash 23 | cd ao-localnet/seed && ./download-aos-module.sh 24 | ./seed-for-aos.sh 25 | cd ../wallets && node printWalletAddresses.mjs 26 | ``` 27 | 28 | ## Testing with Mocha 29 | 30 | ```js 31 | mkdir aonote-test && cd aonote-test && mkdir test && touch test/test.js && npm init 32 | ``` 33 | 34 | Edit `package.json` to add `test` command and `"type": "module"` to run ESM scripts. 35 | 36 | ```json 37 | { 38 | "name": "aonote-test", 39 | "version": "1.0.0", 40 | "type": "module", 41 | "scripts": { 42 | "test": "mocha" 43 | } 44 | } 45 | ``` 46 | 47 | Install the dependencies. 48 | ```js 49 | npm i aonote && npm i mocha chai --dev 50 | ``` 51 | 52 | Edit`test/test.js`. 53 | 54 | ```js 55 | import { setup, ok, fail } from "aonote/test/helpers.js" 56 | import { expect } from "chai" 57 | import { AR, AO, Profile, Note, Notebook } from "aonote" 58 | 59 | describe("Atomic Notes", function () { 60 | this.timeout(0) 61 | let ao, opt, profile, ar, thumbnail, banner 62 | 63 | before(async () => { 64 | ;({ thumbnail, banner, opt, ao, ar, profile } = await setup({})) 65 | }) 66 | 67 | it("should create an AO profile", async () => { 68 | const my_profile = { 69 | DisplayName: "Tomo", 70 | UserName: "0xtomo", 71 | Description: "The Permaweb Hacker", 72 | } 73 | const { pid } = ok(await profile.createProfile({ profile: my_profile })) 74 | expect((await profile.profile()).DisplayName).to.eql("Tomo") 75 | }) 76 | }) 77 | ``` 78 | 79 | Run the tests. 80 | 81 | ```bash 82 | npm test 83 | ``` 84 | 85 | ## aoNote Test Helpers 86 | 87 | The `setup` function will 88 | 89 | - generate an Arweave wallet 90 | - deploy Wasm modules and Lua sources to Arweave 91 | - deploy the AOS module 92 | - upload a scheduler 93 | - create a profile registry 94 | - ccreate a collection registry 95 | - deploy a note proxy 96 | 97 | and returns 98 | 99 | - `ar` : AR initialized by the generated wallet 100 | - `ao` : AO initialized by the AR 101 | - `profile` : Profile initialized by the AO 102 | - `thumbnail`: a sample thumbnail binary data 103 | - `banner` : a sample banner binary data 104 | - `opt` : parameters for AR/AO/Profile/Note/Notebook to pass to instantiate 105 | 106 | 107 | Use `opt` to instantiate the aoNote classes. 108 | 109 | ```js 110 | const ar2 = new AR(opt.ar) 111 | const ao2 = new AO(opt.ao) 112 | const profile2 = new Profile(opt.profile) 113 | const notebook = new Notebook(opt.notebook) 114 | const note = new Note(opt.note) 115 | ``` 116 | 117 | If you want to initialize them with the same wallet used for the setup, use `ar.jwk` with `init`. 118 | 119 | ```js 120 | it("should instantiate AR with the setup wallet", async () => { 121 | const ar2 = await new AR(opt.ar).init(jwk) 122 | expect(ar2.addr).to.eql(ar.addr) 123 | }) 124 | ``` 125 | 126 | `ok` and `fail` can check if the function call is successful or not. 127 | 128 | ```js 129 | it("should succeed", async () => { 130 | ok(await profile.createProfile({ profile: my_profile })) 131 | }) 132 | 133 | it("should fail", async () => { 134 | fail(await profile.createProfile({ profile: {} })) 135 | }) 136 | ``` 137 | -------------------------------------------------------------------------------- /sdk/test/index.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai" 2 | import { resolve } from "path" 3 | import { Asset, AR, Notebook, Note, Profile } from "../src/index.js" 4 | import { Src } from "wao/test" 5 | import { AO } from "wao/test" 6 | import { wait } from "../src/utils.js" 7 | import { o, map, indexBy, prop } from "ramda" 8 | import { setup, ok, fail } from "wao/test" 9 | 10 | const v1 = "# this is markdown 1" 11 | const v2 = "# this is markdown 2" 12 | const v3 = "# this is markdown 3" 13 | const v4 = "# this is markdown 4" 14 | const v5 = "# this is markdown 5" 15 | const v6 = "# this is markdown 6" 16 | 17 | const note_tags = { title: "title", description: "desc" } 18 | 19 | const prof = { 20 | DisplayName: "Atom", 21 | UserName: "Atom", 22 | ProfileImage: "None", 23 | Description: "The Permaweb Hacker", 24 | CoverImage: "None", 25 | } 26 | 27 | const genUDL = recipient => { 28 | return { 29 | payment: { mode: "single", recipient }, 30 | access: { mode: "none" }, 31 | derivations: { mode: "allowed", term: "one-time", fee: "0" }, 32 | commercial: { mode: "allowed", term: "revenue", fee: "5" }, 33 | training: { mode: "disallowed" }, 34 | } 35 | } 36 | 37 | describe("Atomic Notes", function () { 38 | this.timeout(0) 39 | let ao, ao2, opt, profile, ar, thumbnail, banner, src 40 | let profile_pid, notebook, notebook_pid, note, note_pid, ar2, note2 41 | 42 | before(async () => { 43 | ;({ thumbnail, banner, opt, ao, ao2, ar, profile } = await setup({ 44 | aoconnect: { local: true }, 45 | })) 46 | src = new Src({ ar, dir: resolve(import.meta.dirname, "../src/lua") }) 47 | }) 48 | 49 | it.skip("should deploy aos2.0 with On-Boot", async () => { 50 | const ao2 = new AO() // mainnet 51 | const { pid } = ok( 52 | await ao2.deploy({ 53 | src_data: src.data("aos2_1_0"), 54 | boot: true, 55 | }), 56 | ) 57 | ok(await ao2.wait({ pid })) 58 | const { out } = await ao2.dry({ 59 | pid, 60 | act: "Get", 61 | get: { data: true }, 62 | check: true, 63 | }) 64 | expect(out).to.eql("Bob1") 65 | }) 66 | 67 | it.skip("should spawn aos2.0 with On-Boot", async () => { 68 | const ao2 = new AO() // mainnet 69 | const { pid } = ok( 70 | await ao2.spwn({ 71 | boot: "Y0FZa1vyn-Azx0o48odlw8UJxVT5XmggZqJa8Jw9RW8", 72 | }), 73 | ) 74 | ok(await ao2.wait({ pid })) 75 | const { out } = await ao2.dry({ 76 | pid, 77 | act: "Get", 78 | get: { data: true }, 79 | check: true, 80 | }) 81 | expect(out).to.eql("Bob") 82 | }) 83 | 84 | it.only("should spawn aos2.0", async () => { 85 | const ao2 = new AO({}) 86 | const { pid: pid3 } = ok(await ao2.deploy({ src_data: src.data("aos2") })) 87 | return 88 | const { pid } = ok(await ao2.deploy({ src_data: src.data("aos2") })) 89 | const { pid: pid2 } = ok(await ao2.deploy({ src_data: src.data("aos2") })) 90 | const ar2 = new AR(opt.ar) 91 | await ar2.gen() 92 | const a = ao2.p(pid) 93 | const a2 = ao2.p(pid2) 94 | const { 95 | mid, 96 | out: out2, 97 | res, 98 | } = ok( 99 | await a.msg( 100 | "Print", 101 | { Addr: pid2, Addr2: pid3 }, 102 | { 103 | get: ["To", { print: false }], 104 | check: [ 105 | /printed/, 106 | /Bob/, 107 | /Alice/, 108 | { To2: pid3, To: true, Origin: true }, 109 | ], 110 | }, 111 | ), 112 | ) 113 | expect(out2).to.eql({ print: "Bob2 printed!", To: pid2 }) 114 | expect(res.Output.data).to.eql("Hello World!") 115 | const out = await a2.d("Get", null, { get: false, check: true }) 116 | expect(out).to.eql("Bob3") 117 | const out3 = await a2.d("Get2", null, { get: false, check: true }) 118 | expect(out3).to.eql("Alice3") 119 | console.log("here....", Date.now()) 120 | await ao.asgn({ pid: pid2, mid }) 121 | }) 122 | 123 | it("should upload atomic assets", async () => { 124 | const asset = new Asset(opt.asset) 125 | await asset.ar.gen("100") 126 | const { pid: profile_pid } = ok( 127 | await asset.profile.createProfile({ profile: prof }), 128 | ) 129 | expect((await asset.profile.profile()).DisplayName).to.eql(prof.DisplayName) 130 | ok( 131 | await asset.create({ 132 | data: thumbnail, 133 | content_type: "image/png", 134 | info: note_tags, 135 | token: { fraction: "100" }, 136 | udl: genUDL(asset.ar.addr), 137 | }), 138 | ) 139 | }) 140 | 141 | it("should auto-load ArConnect wallet", async () => { 142 | const _jwk = ar.jwk 143 | const arconnect = new AR(opt.ar) 144 | const { addr, jwk, pub } = await arconnect.gen("10") 145 | globalThis.window = { 146 | arweaveWallet: { 147 | walletName: "ArConnect", 148 | test: true, 149 | jwk, 150 | connect: async () => {}, 151 | getActiveAddress: async () => addr, 152 | getActivePublicKey: async () => pub, 153 | sign: async tx => { 154 | await arconnect.arweave.transactions.sign(tx, jwk) 155 | return tx 156 | }, 157 | }, 158 | } 159 | globalThis.arweaveWallet = globalThis.window.arweaveWallet 160 | 161 | const ar2 = await new AR(opt.ar).init() 162 | expect((await ar2.checkWallet()).addr).to.eql(addr) 163 | 164 | const ar3 = new AR(opt.ar) 165 | const { addr: addr2, jwk: jwk2, pub: pub2 } = await ar3.gen() 166 | 167 | const ar4 = await new AR(opt.ar).init() 168 | expect((await ar4.balance()) * 1).to.eql(10) 169 | 170 | const ar5 = await new AR(opt.ar).init() 171 | await ar5.transfer("5", ar3.addr) 172 | expect((await ar5.balance()) * 1).to.eql(5) 173 | 174 | const pr6 = await new Profile({ ...opt.profile, ao }).init() 175 | await pr6.createProfile({ profile: prof }) 176 | expect((await pr6.profile()).DisplayName).to.eql(prof.DisplayName) 177 | 178 | const pr7 = await new Profile({ ...opt.profile, ao }).init() 179 | globalThis.window = { 180 | arweaveWallet: { 181 | walletName: "ArConnect", 182 | test: true, 183 | jwk, 184 | connect: async () => {}, 185 | getActiveAddress: async () => addr2, 186 | getActivePublicKey: async () => pub2, 187 | sign: async tx => { 188 | await arconnect.arweave.transactions.sign(tx, jwk2) 189 | return tx 190 | }, 191 | }, 192 | } 193 | globalThis.arweaveWallet = globalThis.window.arweaveWallet 194 | expect((await pr7.createProfile({ profile: prof })).err).to.eql( 195 | "the wrong wallet", 196 | ) 197 | await pr7.init(arweaveWallet) 198 | expect((await pr7.createProfile({ profile: prof })).err).to.eql(null) 199 | await ar.init(_jwk) 200 | }) 201 | 202 | it("should create an AO profile", async () => { 203 | ;({ pid: profile_pid } = ok(await profile.createProfile({ profile: prof }))) 204 | expect((await profile.profile()).DisplayName).to.eql(prof.DisplayName) 205 | }) 206 | 207 | it("should create a notebook", async () => { 208 | notebook = new Notebook({ ...opt.notebook, profile }) 209 | ;({ pid: notebook_pid } = ok( 210 | await notebook.create({ 211 | info: { 212 | title: "title", 213 | description: "desc", 214 | thumbnail_data: thumbnail, 215 | thumbnail_type: "image/png", 216 | banner_data: banner, 217 | banner_type: "image/png", 218 | }, 219 | bazar: true, 220 | }), 221 | )) 222 | expect((await notebook.get(profile.id)).Collections[0].Id).to.eql( 223 | notebook_pid, 224 | ) 225 | expect((await notebook.info()).Name).to.eql("title") 226 | }) 227 | 228 | it("should update a notebook", async () => { 229 | ok( 230 | await notebook.updateInfo({ 231 | title: "title2", 232 | thumbnail_data: thumbnail, 233 | thumbnail_type: "image/png", 234 | banner_data: banner, 235 | banner_type: "image/png", 236 | }), 237 | ) 238 | expect((await notebook.info()).Name).to.eql("title2") 239 | }) 240 | 241 | it("should create a note", async () => { 242 | let res = null 243 | note = new Note({ ...opt.note, profile }) 244 | const src_data = src.data("atomic-note") 245 | ;({ pid: note_pid, res } = ok( 246 | await note.create({ 247 | src_data, 248 | data: v1, 249 | info: { 250 | ...note_tags, 251 | thumbnail_data: thumbnail, 252 | thumbnail_type: "image/png", 253 | }, 254 | token: { fraction: "100" }, 255 | udl: { 256 | payment: { mode: "single", recipient: ao.ar.addr }, 257 | access: { mode: "one-time", fee: "1.3" }, 258 | derivations: { 259 | mode: "allowed", 260 | term: "one-time", 261 | share: "5.0", 262 | fee: "1.0", 263 | }, 264 | commercial: { 265 | mode: "allowed", 266 | term: "one-time", 267 | share: "5.0", 268 | fee: "1.0", 269 | }, 270 | training: { mode: "allowed", term: "one-time", fee: "0.1" }, 271 | }, 272 | }), 273 | )) 274 | expect((await note.info()).Name).to.eql("title") 275 | }) 276 | 277 | it("should update a note", async () => { 278 | ok( 279 | await note.updateInfo({ 280 | title: "title2", 281 | thumbnail_data: thumbnail, 282 | thumbnail_type: "image/png", 283 | }), 284 | ) 285 | expect((await note.info()).Name).to.eql("title2") 286 | }) 287 | 288 | it("should add a note to a notebook", async () => { 289 | ok(await notebook.addNote(note.pid)) 290 | expect((await notebook.info()).Assets).to.eql([note.pid]) 291 | }) 292 | 293 | it("should remove a note from a notebook", async () => { 294 | ok(await notebook.removeNote(note.pid)) 295 | expect((await notebook.info()).Assets).to.eql([]) 296 | }) 297 | 298 | it("should add notes to a notebook", async () => { 299 | ok(await notebook.addNotes([note.pid])) 300 | expect((await notebook.info()).Assets).to.eql([note.pid]) 301 | }) 302 | 303 | it("should remove notes from a notebook", async () => { 304 | ok(await notebook.removeNotes([note.pid])) 305 | expect((await notebook.info()).Assets).to.eql([]) 306 | }) 307 | 308 | it("should update the version with new content", async () => { 309 | expect((await note.get()).data).to.eql(v1) 310 | expect((await note.list())[0].version).to.eql("0.0.1") 311 | ok(await note.update(v2, "0.0.2")) 312 | expect((await note.get()).data).to.eql(v2) 313 | expect((await note.list())[1].version).to.eql("0.0.2") 314 | }) 315 | 316 | it("should add an editor", async () => { 317 | expect((await note.get("0.0.1")).data).to.eql(v1) 318 | expect(await note.editors()).to.eql([ar.addr]) 319 | ar2 = new AR(opt.ar) 320 | await ar2.gen("10") 321 | await ar2.transfer("5", ar.addr) 322 | const _ao2 = new AO({ ...opt.ao, ar: ar2 }) 323 | const _pr2 = new Profile({ ...opt.profile, ao: _ao2 }) 324 | note2 = new Note({ pid: note_pid, ...opt.note, profile: _pr2 }) 325 | ok(await note.addEditor(ar2.addr)) 326 | expect(await note.editors()).to.eql([ar.addr, ar2.addr]) 327 | ok(await note2.update(v3, "0.0.3")) 328 | expect((await note.get()).data).to.eql(v3) 329 | expect((await note.list())[2].version).to.eql("0.0.3") 330 | }) 331 | 332 | it("should remove an editor", async () => { 333 | ok(await note.removeEditor(ar2.addr)) 334 | expect(await note.editors()).to.eql([ar.addr]) 335 | fail(await note2.update(v4, "0.0.4")) 336 | expect((await note.get()).data).to.eql(v3) 337 | }) 338 | 339 | it("should bump with major/minor/patch", async () => { 340 | ok(await note.update(v4, "minor")) 341 | expect((await note.get()).version).to.eql("0.1.0") 342 | ok(await note.update(v5, "patch")) 343 | expect((await note.get()).version).to.eql("0.1.1") 344 | ok(await note.update(v6, "major")) 345 | expect((await note.get()).version).to.eql("1.0.0") 346 | }) 347 | 348 | it("should return the correct notebook info", async () => { 349 | const info = await profile.info() 350 | expect(info.Collections[0].Id).to.eql(notebook_pid) 351 | expect(info.Assets[0].Id).to.eql(note_pid) 352 | expect(info.Owner).to.eql(ar.addr) 353 | expect(info.Id).to.eql(profile.id) 354 | }) 355 | 356 | it("should init AR with an existing jwk", async () => { 357 | const _ar = await new AR(opt.ar).init(ar.jwk) 358 | expect(_ar.jwk).to.eql(ar.jwk) 359 | expect(_ar.addr).to.eql(ar.addr) 360 | const _ao = new AO({ ...opt.ao, ar: _ar }) 361 | expect(_ao.ar.jwk).to.eql(ar.jwk) 362 | expect(_ao.ar.addr).to.eql(ar.addr) 363 | const _pr = new Profile({ ...opt.pr, ao: _ao }) 364 | expect(_pr.ar.jwk).to.eql(ar.jwk) 365 | expect(_pr.ar.addr).to.eql(ar.addr) 366 | }) 367 | 368 | it("should transfer tokens", async () => { 369 | const { out: balances } = await note.balances() 370 | const { out: balance } = await note.balance({ target: note.profile.id }) 371 | expect(balance).to.eql("100") 372 | expect(balances).to.eql({ [note.profile.id]: "100" }) 373 | ok(await note.mint({ quantity: "100" })) 374 | ok(await note.transfer({ recipient: note.profile.id, quantity: "10" })) 375 | const { out: balances2 } = await note.balances() 376 | expect(balances2).to.eql({ 377 | [note.profile.id]: "110", 378 | [note.ar.addr]: "90", 379 | }) 380 | const acc = new AR() 381 | await acc.gen("100") 382 | const { out: balance3 } = await note.balance({ target: acc.addr }) 383 | expect(balance3).to.eql("0") 384 | ok( 385 | await note.transfer({ 386 | recipient: note.ar.addr, 387 | quantity: "10", 388 | profile: true, 389 | }), 390 | ) 391 | await wait(1000) 392 | const { out: balances3 } = await note.balances() 393 | expect(balances3).to.eql({ 394 | [note.profile.id]: "100", 395 | [note.ar.addr]: "100", 396 | }) 397 | }) 398 | }) 399 | --------------------------------------------------------------------------------