Compare commits

..
10 Commits
Author SHA1 Message Date
nitowa e9045436f3 add dates to frontend 2024-10-13 16:08:17 +02:00
nitowa ec746cd1a2 frontend work 2024-10-13 14:45:00 +02:00
nitowa 9d64d43230 Work after api break 2024-10-11 19:21:08 +02:00
nitowa b78464beab update to new CLR version, refine deployment script with better log output 2023-10-09 11:57:18 +02:00
nitowa 0c25da5c16 remove confusing readme 2023-08-15 22:38:30 +02:00
nitowa bbe32c2c2d init 2023-08-15 22:37:40 +02:00
nitowa d1d576edfa fix typo 2022-12-11 00:31:48 +01:00
nitowa 515b45de7c update README some more again 2022-11-23 11:17:01 +01:00
nitowa 604f3ecabc update README some more again 2022-11-23 11:14:51 +01:00
nitowa ad503bcabd update README some more again 2022-11-23 11:02:40 +01:00
33 changed files with 11633 additions and 8746 deletions
-75
View File
@@ -1,75 +0,0 @@
# Overview
httXrp is a proof of concept for a truly serverless web architecture. If serverless simply means "a server owned by someone else", httXrp pushes that definition to its limit -or- perhaps its logical conclusion: What if that "someone else" never even intended that server to be used that way but can't do anything about it?
# How it works
## 1: Getting data into and out of the blockchain
Transactions on the ripple blockchain are allowed to carry up to 1kB of arbitrary data via the memo field.
We can use this to store data of any size by building a tree of references between these transactions that can then be reassembled by reading them back from the blockchain.
In order to generate these transactions a library called [xrpio](https://gitea.nitowa.xyz/npm-packages/xrpio.git) is used to send minimum-denomination transactions between two user controlled wallets.
Highly simplified, you can visualize the process like this:
<img src="https://i.imgur.com/G2HofSE.gif" alt="xrpio" width="650"/>
## 2: Abstracting the webserver away from the web
Using tools like `webpack`, it is possible to condense even modern complex single-page-applications into a single html file. As `xrpio` is written in JavaScript, it is even possible to embed it into such an HTML file, the use of which will become more clear a few paragraphs below.
Since such a condensed HTML file is effectively nothing more than a long string it is possible to use `xrpio` to store them into the ripple blockchain and to retrieve them via a single identifying hash.
<img src="https://i.imgur.com/Rwo37xJ.gif" alt="serverless web" width="650"/>
## 3: Backendless dynamic web applications: Databases without databases
Superficially, this technique is limited to serving static webpages, as there can be no backend communicating with these pages without betraying the serverless premise. However, since it is possible to embed `xrpio` into such a "static" page, it is possible to listen for transactions on the blockchain containing valid xrpio hashes and to dynamically update the webpage's content based on the stored data.
All necessary mechanisms can easily be embedded within that webpage, which allows us to build complex webapplications without any need for a backend server.
To prove the feasibility of this approach, this project contains a small example application in the form of a shoutbox:
<img src="https://i.imgur.com/5gYLuYc.png" alt="shoutbox" width="450"/>
The exact procedure is more easily explained in code than visually. The presented code snippets should be considered pseudocode, but if you're interested in the exact steps please take a look into [ShoutboxData.service.ts](https://gitea.nitowa.xyz/nitowa/httxrp/src/branch/master/src/frontend/src/app/services/ShoutboxData.service.ts). The actual implementation isn't any more complex than the steps below but they were altered for readability reasons.
### Submitting a new shout to the shoutbox
```js
//When submitting a new shout, first the user creates a xrpio write between two of their own wallets
submitShout = async (shout: any) => {
const shoutHash = await xrpio.treeWrite(shout, userWallet1.address, userWallet2.secret)
return await submit(shoutHash)
}
//After the shout has been written to the blockchain,
//the hash pointing to the data is sent to the address keeping track of the application's state
submit = async (shoutHash: string) => {
return await xrpio.writeRaw({ data: shoutHash }, shoutboxAddress, userWallet1.secret)
}
```
### Loading the application state and live updating it
```js
//Loading old data is as easy as parsing the historical transactions of the shoutboxAddress
loadHistory = async () => {
const raw_txs = await getTransactions(shoutboxAddress)
//Extracts hashes from memos and reads them with xrpio
const shouts = await parseMemos(raw_txs.map(getMemo))
history = shouts
}
//Fetching new data as it comes in is also possible by simply subscribing to new transactions for the shoutboxAddress
listen = async () => {
await subscribeTxs(async (raw_tx: any) => {
//Extracts hashes from memos and reads them with xrpio
const shout = await parseMemos(getMemo(raw_tx))
history.push(shout)
})
}
```
# Credits
- This project was originally inspired by indImm (https://ndm-inf.github.io/ndm/main), a ripple-based file storage using IPFS.
- xrpio (https://gitea.nitowa.xyz/npm-packages/xrpio.git) is heavily used in the technical architecture of this project. It is also written and maintained by me.
+5 -2
View File
@@ -1,15 +1,18 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<script src="https://bundle.run/browserify-zlib@0.2.0"></script> <script src="https://bundle.run/browserify-zlib@0.2.0"></script>
<script src="https://bundle.run/buffer@6.0.3"></script> <script src="https://bundle.run/buffer@6.0.3"></script>
<script src="https://cdn.jsdelivr.net/npm/ripple-lib@1.10.0/build/ripple-latest-min.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/ripple-lib@1.10.0/build/ripple-latest-min.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xrpl@2.1.1"></script> <script src="https://cdn.jsdelivr.net/npm/xrpl@2.1.1"></script>
<script src="https://cdn.jsdelivr.net/npm/xrpio@0.1.7/lib/browser/xrpio.browser.js"></script> <script src="https://cdn.jsdelivr.net/npm/xrpio@0.1.7/lib/browser/xrpio.browser.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" integrity="sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm" crossorigin="anonymous"/>
</head> </head>
<body> <body>
Loading ... <div style="text-align: center;">Downloading the application from the blockchain</div>
<script src="main.js"></script>
</body> </body>
<script src="main.js"></script>
</html> </html>
+1343 -809
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -12,7 +12,8 @@
"build-gui": "cd src/frontend && npm run build", "build-gui": "cd src/frontend && npm run build",
"copy-gui": "cp src/frontend/build/index.html gui", "copy-gui": "cp src/frontend/build/index.html gui",
"webpack-gateway": "webpack --config webpack.gateway.js --progress && cp build/gateway/main.js gateway", "webpack-gateway": "webpack --config webpack.gateway.js --progress && cp build/gateway/main.js gateway",
"deploy": "node ./lib/Deploy.js" "deploy": "node ./lib/Deploy.js",
"github": "cp gateway/index.html ~/caisar.github.io && cp gateway/main.js ~/caisar.github.io && cd ~/caisar.github.io && git checkout master && git add -A && git commit -m \"update\" --allow-empty && git push origin master --force && git checkout main && git add -A && git commit -m \"update\" --allow-empty && git push origin main --force"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@@ -20,8 +21,9 @@
"buffer": "^6.0.3", "buffer": "^6.0.3",
"chai": "^4.3.4", "chai": "^4.3.4",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"xrpio": "^0.1.7", "rjsvm": "^0.3.2",
"xrpl": "^2.6.0-beta.0" "xrpio": "^0.3.0",
"xrpl": "^2.7.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.11.9", "@types/node": "^18.11.9",
@@ -35,8 +37,7 @@
"typescript": "^4.9.3", "typescript": "^4.9.3",
"url": "^0.11.0", "url": "^0.11.0",
"utf8": "^3.0.0", "utf8": "^3.0.0",
"webpack": "^5.75.0", "webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.7.0", "webpack-cli": "^5.1.4"
"webpack-cli": "^5.0.0"
} }
} }
+3 -3
View File
@@ -1,8 +1,8 @@
export const config = { export const config = {
"oracle": true, "oracle": true,
"secret": "sEdTP9Wkb4FSeaFhBavaob336sUTnqy", "secret": "sEd7WCM4AD8r19eyPfiG7nx6Vkx1bbU",
"treasuryAddress": "rffH6RPq8xcKAWVvdYsiGEFrz1mz2iH3oj", "treasuryAddress": "rhfkTrKqRNKFyY3wWqaHLSxgwfc1eMfvWz",
"contractAddress": "rDXi28Ud76cz9LtR2vAwsmGfqMcnfCTm8g", //pk: sEdSMv9Ztd2njbAhL3tTYqVRTKMQEME "contractAddress": "rrha5qSA3KUxwLYCBtm1z13KMPwsPFpvQ3", //pk: sEdVr3HmJooxVApTdqCKp4gVHrBBqUr
"rippleNode": "wss://s.altnet.rippletest.net:51233", "rippleNode": "wss://s.altnet.rippletest.net:51233",
} }
+4 -3
View File
@@ -11,10 +11,11 @@ const path = require('path');
console.log("Getting html from "+htmlPath) console.log("Getting html from "+htmlPath)
const htmlBuf = await fs.readFile(htmlPath) const htmlBuf = await fs.readFile(htmlPath)
const html = htmlBuf.toString('ascii') const html = htmlBuf.toString('ascii')
console.log(`Writing ${html.length} bytes to blockchain...`)
const dataHash = await api.treeWrite(html, config.contractAddress, config.secret) const dataHash = await api.treeWrite(html, config.contractAddress, config.secret)
await api.disconnect() await api.disconnect()
console.log("Done. Your upload is at: ", dataHash); console.log("Done. Your upload is at: ", dataHash);
console.log("Building gateway ...") console.log("Building gateway...")
child_process.exec(`DATA_HASH=${dataHash} npm run webpack-gateway`, ()=>{})
child_process.exec(`DATA_HASH=${dataHash} npm run webpack-gateway`, console.log) console.log(`Success. You can now open the application under ${path.join(__dirname, '..', 'gateway', 'index.html')}`)
})() })()
-1
View File
@@ -7,6 +7,5 @@ declare const DATA_HASH: string
const api = new xrpIO("wss://s.altnet.rippletest.net:51233") const api = new xrpIO("wss://s.altnet.rippletest.net:51233")
api.connect().then(async _ => { api.connect().then(async _ => {
const data = await api.treeRead([DATA_HASH]); const data = await api.treeRead([DATA_HASH]);
alert("Writing to document now")
document.write(data) document.write(data)
}) })
-17
View File
@@ -1,17 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
+16
View File
@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
+38
View File
@@ -0,0 +1,38 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode
.history
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
+27
View File
@@ -0,0 +1,27 @@
# AngularClr
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.3.0.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
+24 -17
View File
@@ -1,9 +1,12 @@
{ {
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"cli": {
"analytics": false
},
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"angular-cli": { "angular-clr": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
@@ -20,29 +23,29 @@
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:browser",
"options": { "options": {
"outputPath": "dist/angular-cli", "outputPath": "dist/angular-clr",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"], "assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [ "styles": [
"./node_modules/@cds/city/css/bundles/default.min.css",
"./node_modules/@cds/core/global.min.css",
"./node_modules/normalize.css/normalize.css",
"node_modules/@clr/icons/clr-icons.min.css",
"node_modules/@clr/ui/clr-ui.min.css",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": [
"node_modules/buffer/index.js"
]
}, },
"configurations": { "configurations": {
"production": { "production": {
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "3mb", "maximumWarning": "500kb",
"maximumError": "10mb" "maximumError": "10mb"
}, },
{ {
@@ -74,10 +77,10 @@
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "angular-cli:build:production" "browserTarget": "angular-clr:build:production"
}, },
"development": { "development": {
"browserTarget": "angular-cli:build:development" "browserTarget": "angular-clr:build:development"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
@@ -85,7 +88,7 @@
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "angular-cli:build" "browserTarget": "angular-clr:build"
} }
}, },
"test": { "test": {
@@ -96,13 +99,17 @@
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js", "karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"], "assets": [
"styles": ["src/styles.scss"], "src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [] "scripts": []
} }
} }
} }
} }
}, }
"defaultProject": "angular-cli"
} }
+44
View File
@@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/angular-clr'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
+9729 -7600
View File
File diff suppressed because it is too large Load Diff
+45 -34
View File
@@ -1,55 +1,66 @@
{ {
"name": "angular-cli", "name": "angular-clr",
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"postinstall": "npm run start",
"start": "ng serve", "start": "ng serve",
"build": "npm run build-aot && npm run bundle", "build": "npm run build-aot && npm run bundle",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"build-aot": "ng build --aot --optimization --build-optimizer",
"bundle": "webpack --config webpack-bundle.config.js --progress", "bundle": "webpack --config webpack-bundle.config.js --progress",
"clean": "rm -rf dist/* build/*" "build-aot": "ng build --aot --optimization --build-optimizer",
"watch": "ng build --watch --configuration development",
"test": "ng test"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "~14.0.0", "@angular/animations": "^15.2.9",
"@angular/common": "~14.0.0", "@angular/cdk": "^15.2.9",
"@angular/compiler": "~14.0.0", "@angular/common": "^15.2.9",
"@angular/core": "~14.0.0", "@angular/compiler": "^15.2.9",
"@angular/forms": "~14.0.0", "@angular/core": "^15.2.9",
"@angular/platform-browser": "~14.0.0", "@angular/forms": "^15.2.9",
"@angular/platform-browser-dynamic": "~14.0.0", "@angular/platform-browser": "^15.2.9",
"@angular/router": "~14.0.0", "@angular/platform-browser-dynamic": "^15.2.9",
"@cds/angular": "^6.0.0", "@angular/router": "^15.2.9",
"@cds/angular": "6.6.2",
"@cds/city": "^1.1.0", "@cds/city": "^1.1.0",
"@cds/core": "^6.0.0", "@cds/core": "6.6.2",
"@clr/angular": "~13.0.0", "@clr/angular": "^15.12.0",
"@clr/icons": "~13.0.0", "@clr/ui": "^15.12.0",
"@clr/ui": "~13.0.0", "axios": "^1.7.7",
"mini-css-extract-plugin": "^1.6.0", "buffer": "^6.0.3",
"normalize.css": "^8.0.1", "modern-normalize": "^1.1.0",
"rxjs": "~6.6.0", "process": "^0.11.10",
"style-loader": "^3.3.1", "rjsvm": "^0.3.2",
"tslib": "^2.1.0", "rxjs": "~7.5.0",
"tslib": "^2.3.0",
"xrpio": "^0.3.0",
"zod": "^3.22.4",
"zone.js": "~0.11.4" "zone.js": "~0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~14.0.0", "@angular-devkit/build-angular": "^15.2.9",
"@angular/cli": "~14.0.0", "@angular/cli": "^15.2.9",
"@angular/compiler-cli": "~14.0.0", "@angular/compiler-cli": "^15.2.9",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.10.0",
"@types/node": "^12.11.1", "@types/node": "^12.11.1",
"css-loader": "^6.7.2", "browserify-zlib": "^0.2.0",
"html-webpack-plugin": "^5.5.0", "crypto-browserify": "^3.12.0",
"glob": "^10.3.10",
"html-webpack-plugin": "^5.5.3",
"https-browserify": "^1.0.0",
"inline-chunk-html-plugin": "^1.1.1", "inline-chunk-html-plugin": "^1.1.1",
"jasmine-core": "~3.7.0", "jasmine-core": "~4.0.0",
"karma": "~6.3.0", "karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3", "karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.7.0" "purgecss-webpack-plugin": "^5.0.0",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"style-loader": "^3.3.3",
"typescript": "~4.9.5",
"url": "^0.11.3",
"webpack-cli": "^5.1.4"
} }
} }
+1 -1
View File
@@ -7,4 +7,4 @@ const routes: Routes = [];
imports: [RouterModule.forRoot(routes)], imports: [RouterModule.forRoot(routes)],
exports: [RouterModule] exports: [RouterModule]
}) })
export class AppRoutingModule {} export class AppRoutingModule { }
+93 -11
View File
@@ -1,46 +1,128 @@
<div class="main-container"> <div class="main-container">
<header class="header-2"> <header class="header-2">
<div class="branding"> <div class="branding">
<a class="nav-link"> <a class="nav-link" target="_blank" [href]="'https://testnet.xrpl.org/accounts/'+contractAddress">
<cds-icon shape="home" size="lg"></cds-icon> <cds-icon shape="home" size="lg"></cds-icon>
<span class="title">httXrp</span> <span class="title">{{contractAddress}}</span>
</a> </a>
</div> </div>
<div class="header-nav"> <div class="header-nav">
<a class="active nav-link nav-text">Shoutbox</a> <a class="active nav-link nav-text">Shoutbox</a>
</div> </div>
</header> </header>
<div class="content-container"> <div class="content-container">
<div class="content-area"> <div class="content-area">
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-lg-5 clr-col-md-8 clr-col-12"> <div class="clr-col-lg-6 clr-col-md-8 clr-col-12">
<div class="card">
<h3 class="card-header">Wallets in use</h3>
<div class="clr-row">
<div class="clr-col-lg-6 clr-col-md-8 clr-col-12">
<div class="card-block">
<clr-input-container>
<label>User Wallet 1 Address</label>
<input clrInput [disabled]="true" required type="text" [(ngModel)]="userWallet.address"
name="title" size="29" />
</clr-input-container>
<clr-input-container>
<label>Secret</label>
<input clrInput [disabled]="true" required type="text" [(ngModel)]="userWallet.secret"
name="title" size="29" />
</clr-input-container>
</div>
</div>
<div class="clr-col-lg-6 clr-col-md-8 clr-col-12">
<div class="card-block">
<clr-input-container>
<label>User Wallet 2 Address</label>
<input clrInput [disabled]="true" required type="text" [(ngModel)]="drainWallet.address"
name="title" size="29" />
</clr-input-container>
<clr-input-container>
<label>Secret</label>
<input clrInput [disabled]="true" required type="text" [(ngModel)]="drainWallet.secret"
name="title" size="29" />
</clr-input-container>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="clr-row">
<div class="clr-col-lg-6 clr-col-md-8 clr-col-12">
<div class="card"> <div class="card">
<h3 class="card-header">Shout Something</h3> <h3 class="card-header">Shout Something</h3>
<form clrForm #loginForm="ngForm"> <form clrForm #loginForm="ngForm">
<div class="card-block"> <div class="card-block">
<clr-input-container> <clr-input-container>
<input clrInput [disabled]="sending" required placeholder="Title" type="text" [(ngModel)]="newShout.title" name="title" /> <input clrInput [disabled]="sending" required placeholder="Title" type="text"
[(ngModel)]="newShout.title" name="title" size="45" />
</clr-input-container> </clr-input-container>
<textarea clrTextarea [disabled]="sending" required placeholder="Shout Body" type="text" [(ngModel)]="newShout.body" <textarea clrTextarea [disabled]="sending" required placeholder="Shout Body" type="text"
name="body"></textarea> [(ngModel)]="newShout.body" name="body" cols="43" rows="15"></textarea>
<clr-input-container> <clr-input-container>
<input clrInput [disabled]="sending" required placeholder="Your Name" type="text" [(ngModel)]="newShout.from" name="from" /> <input clrInput [disabled]="sending" required placeholder="Your Name" type="text"
[(ngModel)]="newShout.from" name="from" size="45" />
</clr-input-container> </clr-input-container>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<button class="btn btn-success" (click)="submitShout()" <button class="btn btn-success" (click)="submitShout()"
[disabled]="!loginForm.form.valid || sending">Shout</button> <span *ngIf="sending">Sending...</span> [disabled]="!loginForm.form.valid || sending">Shout</button>
<span *ngIf="sending">Sending...</span>
<span *ngIf="waiting">
TX <a [href]="'https://testnet.xrpl.org/transactions/'+lastMessageHash+'/detailed'" target="_blank">
<cds-icon shape="link"></cds-icon>
</a>
<br />
It may take up to 30 seconds for the new entry to persist
</span>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div class="clr-row" *ngFor="let shout of shouts"> <div class="clr-row" *ngIf="initializing">
<div class="clr-col-lg-5 clr-col-md-8 clr-col-12"> <div class="clr-col-lg-6 clr-col-md-8 clr-col-12">
<div class="card"> <div class="card">
<h3 class="card-header">{{shout.title}}</h3> <h3 class="card-header">Syncing application state</h3>
<div class="card-block">
<div class="card-text">
<span class="spinner spinner-inline">Loading...</span>
<span>Loading...</span>
</div>
</div>
</div>
</div>
</div>
<div class="clr-row" *ngFor="let shout of shouts">
<div class="clr-col-lg-6 clr-col-md-8 clr-col-12">
<div class="card">
<h3 class="card-header">
{{shout.title}}
<span class="p3">
<br />
({{shout.date}}
<a [href]="'https://testnet.xrpl.org/transactions/'+shout.hash+'/detailed'" target="_blank">
<cds-icon shape="link"></cds-icon>
</a>
)
</span>
</h3>
<div class="card-block"> <div class="card-block">
<div class="card-text"> <div class="card-text">
{{shout.body}} {{shout.body}}
+53 -18
View File
@@ -1,41 +1,76 @@
import { Component, OnInit } from '@angular/core'; import { Component, NgZone, OnInit } from '@angular/core';
import { ShoutboxDataService } from './services/ShoutboxData.service'; import { ShoutboxDataService } from './services/ShoutboxData.service';
import { makeTestnetWallet } from './util/TestnetUtils';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
}) })
export class AppComponent implements OnInit{ export class AppComponent implements OnInit {
title = 'httXrp'; lastMessageHash = ""
contractAddress = "";
initializing = true
waiting = false
title = 'httXrp Shoutbox';
shouts: any[] = [] shouts: any[] = []
sending = false sending = false
newShout = { newShout = {
title: "", title: "",
body: "", body: "",
from: "" from: "",
hash: "",
date: "",
}
userWallet = {
address: "",
secret: ""
}
drainWallet = {
address: "",
secret: ""
} }
constructor( constructor(
private dataService: ShoutboxDataService private dataService: ShoutboxDataService,
){} private zone: NgZone
) {
this.contractAddress = dataService.getContractAddress()
}
submitShout = () => { submitShout = () => {
this.sending = true this.sending = true
this.dataService.submitShout(this.newShout) this.dataService.submitShout(this.newShout)
.then(() => { .then((hash) => {
this.newShout = { this.newShout = {
title: "", title: "",
body: "", body: "",
from: "" from: "",
} hash: "",
}) date: "",
.finally(() => { }
this.sending = false this.lastMessageHash = hash
}) })
.finally(() => {
this.sending = false
this.waiting = true
setTimeout(() => { this.waiting = false }, 20000)
})
} }
ngOnInit(){ ngOnInit() {
this.shouts = this.dataService.history Promise.all([makeTestnetWallet(), makeTestnetWallet()])
.then(async ([userWallet, drainWallet]) => {
this.userWallet = userWallet
this.drainWallet = drainWallet
await this.dataService.initialize((shout: any) => {
this.zone.run(_ => {
this.shouts.unshift(shout)
console.log(shout)
})
}, userWallet, drainWallet)
this.initializing = false
})
} }
} }
+3 -8
View File
@@ -6,8 +6,8 @@ import { AppComponent } from './app.component';
import { CdsModule } from '@cds/angular'; import { CdsModule } from '@cds/angular';
import { ClarityModule } from '@clr/angular'; import { ClarityModule } from '@clr/angular';
import { ClarityIcons, homeIcon } from '@cds/core/icon'; import { ClarityIcons, homeIcon, linkIcon } from '@cds/core/icon';
import { ShoutboxDataService, initShoutboxSvc } from './services/ShoutboxData.service'; import { ShoutboxDataService } from './services/ShoutboxData.service';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@NgModule({ @NgModule({
@@ -21,17 +21,12 @@ import { FormsModule } from '@angular/forms';
], ],
providers: [ providers: [
ShoutboxDataService, ShoutboxDataService,
{
provide: APP_INITIALIZER,
useFactory: initShoutboxSvc,
deps: [ShoutboxDataService],
multi: true
}
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule { export class AppModule {
constructor() { constructor() {
ClarityIcons.addIcons(homeIcon); ClarityIcons.addIcons(homeIcon);
ClarityIcons.addIcons(linkIcon);
} }
} }
@@ -1,104 +1,103 @@
import { Injectable } from "@angular/core"; import { ChangeDetectorRef, Injectable, NgZone } from "@angular/core";
import { DataParser } from "../util/Dataparser"; import { z } from 'zod';
import { makeTestnetWallet } from "../util/TestnetUtils";
declare const xrpIO: any declare const RJSVM: any;
declare const xrpl: any declare const RJSVM_Builder: any;
declare const Datawriter: any;
const xrpNode = "wss://s.altnet.rippletest.net:51233" const xrpNode = "wss://s.altnet.rippletest.net:51233"
const listeningAddress = "rMBYWyxGx1b5zEjJKF18TTwF5X3vP2WyjR"
@Injectable() @Injectable()
export class ShoutboxDataService { export class ShoutboxDataService {
public history: any[] = [] private dataWriter = undefined as any
private userWallet1: any constructor(
private userWallet2: any ) { }
private shoutboxAddress = "rBnbBMZrbWEVHsyi1EWxv3gidzbreJzbgC"
private rippleApi: any;
private xrpio: any;
private getTransactions = async () => {
const resp = await this.rippleApi.request({
command: "account_tx",
account: this.shoutboxAddress,
forward: false,
})
return resp.result.transactions.map((entry: any) => entry.tx)
}
private submit = async (shoutHash: string) => {
return await this.xrpio.writeRaw({ data: shoutHash }, this.shoutboxAddress, this.userWallet1.secret)
}
public submitShout = async (shout: any) => { public submitShout = async (shout: any) => {
const shoutHash = await this.xrpio.treeWrite(JSON.stringify(shout), this.userWallet1.address, this.userWallet2.secret) return await this.dataWriter.callEndpoint('submit', shout, 10)
return await this.submit(shoutHash)
} }
private parseMemos = async (memos: any) => { public getContractAddress = () => listeningAddress
const shouts = await Promise.all(memos
.map((memo: any) => {
if (!memo.Memo || !memo.Memo.MemoData)
return
try { initialize = async (onData: Function, userWallet?: any, drainWallet?: any) => {
return DataParser.parse('TxHash', hex_to_ascii(memo.Memo.MemoData))
} catch (e) {
return
}
})
.filter((hash: string) => hash != undefined)
.map((root_hash: string) => this.xrpio.treeRead([root_hash]))
)
return shouts.map((jsonStr: string) => JSON.parse(jsonStr))
}
private loadHistory = async () => { this.dataWriter = new Datawriter({
const raw_txs = await this.getTransactions() receiveAddress: drainWallet.address,
return await this.parseMemos(raw_txs.flatMap((htx: any) => htx.Memos)) sendWallet: userWallet,
} xrpNode: xrpNode,
contractAddress: listeningAddress
private subscribeTxs = async (callback: Function) => {
this.rippleApi.on('transaction', (tx: any) => callback(tx))
await this.rippleApi.connection.request({
command: 'subscribe',
accounts: [this.shoutboxAddress]
}) })
}
private listen = async () => { const shoutSchema = z.object({
await this.subscribeTxs(async (raw_tx: any) => { title: z.string(),
const shouts = await this.parseMemos(raw_tx.transaction.Memos) body: z.string(),
this.history.unshift(...shouts) from: z.string(),
hash: z.optional(z.string()),
date: z.optional(z.string()),
id: z.optional(z.string())
}) })
type Shout = z.infer<typeof shoutSchema>
type State = {}
// #########################
// Define endpoints
// #########################
type RJSVM_Endpoints = {
submit: (data: Shout) => void
}
// #########################
// Define init state
// #########################
abstract class RJSVM_Base
extends RJSVM<State, RJSVM_Endpoints>
{
owner = userWallet.address
state: State = {}
}
// #########################
// Implement logic
// #########################
const RJSVM_Contract = {
submit: {
implementation: function (env: any, shout: Shout) {
shout.hash = env.hash;
const d = new Date("2000-01-01");
d.setSeconds(d.getSeconds() + env.date)
shout.date = d.toLocaleString("de-DE")
},
visibility: 'public',
fee: 10,
parameterSchema: shoutSchema
}
}
// #########################
// Build and connect
// #########################
const Rjsvm = RJSVM_Builder.from(RJSVM_Base, RJSVM_Contract);
const conf = {
listeningAddress: listeningAddress,
rippleNode: xrpNode
}
const rjsvm = new Rjsvm(conf)
await rjsvm.connect()
rjsvm.on('error', console.log)
rjsvm.on('submit', onData)
} }
initialize = async () => {
this.userWallet1 = await makeTestnetWallet()
this.userWallet2 = await makeTestnetWallet()
this.rippleApi = new xrpl.Client(xrpNode)
await this.rippleApi.connect()
this.xrpio = new xrpIO(xrpNode);
await this.xrpio.connect()
this.history = await this.loadHistory()
await this.listen();
}
}
export function initShoutboxSvc(svc: ShoutboxDataService): () => Promise<any> {
return svc.initialize;
}
function hex_to_ascii(input: any) {
var hex = input.toString();
var str = '';
for (var n = 0; n < hex.length; n += 2) {
str += String.fromCharCode(parseInt(hex.substr(n, 2), 16));
}
return str;
} }
+1 -1
View File
@@ -66,7 +66,7 @@ export class DataParser {
private static parseTxHash(input: any): TxHash{ private static parseTxHash(input: any): TxHash{
if(typeof input !== 'string' || !NON_ZERO_TX_HASH.test(input)){ if(typeof input !== 'string' || !NON_ZERO_TX_HASH.test(input)){
throw new Error('Input is not a trasnaction hash') throw new Error(`Input '${input}'is not a trasnaction hash`)
} }
return input return input
} }
+14 -14
View File
@@ -1,14 +1,14 @@
export const makeTestnetWallet = () : Promise<{ secret: string, address: string }> => fetch('https://faucet.altnet.rippletest.net/accounts', { import axios from 'axios';
method: 'POST',
headers: { export const makeTestnetWallet = async () : Promise<any> => {
'Accept': 'application/json', try{
'Content-Type': 'application/json' const response = await axios.post('https://faucet.altnet.rippletest.net/accounts', {})
}, return ({
}).then((raw:any) => { secret: response.data.seed,
return raw.json().then((content:any) => { address: response.data.account.address
return({ })
secret: content.account.secret,
address: content.account.address }catch(e){
}); console.log(e)
}) }
}); }
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

+16 -13
View File
@@ -1,16 +1,19 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <script>
<title>httXrp</title> window.process = { env: { NODE_ENV: 'production' } }
<base href="" /> </script>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script src="https://cdn.jsdelivr.net/npm/xrpl@2.1.1"></script>
<script src="https://cdn.jsdelivr.net/npm/xrpio@0.1.7/lib/browser/xrpio.browser.js"></script>
</head>
<body cds-text="body"> <meta charset="utf-8">
<app-root></app-root> <title>AngularClr</title>
</body> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<script src="https://cdn.jsdelivr.net/npm/rjsvm@0.3.2/lib/browser/rjsvm.browser.js"></script>
</head>
<body cds-text="body" cds-theme="dark">
<app-root></app-root>
</body>
</html> </html>
+1 -2
View File
@@ -8,6 +8,5 @@ if (environment.production) {
enableProdMode(); enableProdMode();
} }
platformBrowserDynamic() platformBrowserDynamic().bootstrapModule(AppModule)
.bootstrapModule(AppModule)
.catch(err => console.error(err)); .catch(err => console.error(err));
+9 -15
View File
@@ -8,8 +8,8 @@
* file. * file.
* *
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * automatically update themselves. This includes recent versions of Safari, Chrome (including
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * Opera), Edge on the desktop, and iOS and Chrome on mobile.
* *
* Learn more in https://angular.io/guide/browser-support * Learn more in https://angular.io/guide/browser-support
*/ */
@@ -18,18 +18,6 @@
* BROWSER POLYFILLS * BROWSER POLYFILLS
*/ */
/**
* IE11 requires the following for NgClass support on SVG elements
*/
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** /**
* By default, zone.js will patch all possible macroTask and DomEvents * By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags * user can disable parts of macroTask/DomEvents patch by setting following flags
@@ -57,7 +45,13 @@
/*************************************************************************************************** /***************************************************************************************************
* Zone JS is required by default for Angular itself. * Zone JS is required by default for Angular itself.
*/ */
import 'zone.js'; // Included with Angular CLI. import 'zone.js'; // Included with Angular CLI.
import * as process from 'process';
(window as any)['process'] = process;
(window as any)['buffer'] = (window as any).Buffer || require('buffer').Buffer;
(window as any)['Buffer'] = (window as any).Buffer || require('buffer').Buffer;
(window as any)['global'] = (window as any);
/*************************************************************************************************** /***************************************************************************************************
* APPLICATION IMPORTS * APPLICATION IMPORTS
+11 -4
View File
@@ -1,4 +1,11 @@
@import '~modern-normalize/modern-normalize.css'; @import 'modern-normalize/modern-normalize.css';
@import '~@cds/core/global.min.css'; @import '@cds/core/global.min.css';
@import '~@cds/city/css/bundles/default.min.css'; @import '@cds/core/styles/theme.dark.min.css';
@import '~@cds/core/styles/theme.dark.min.css'; @import '@cds/city/css/bundles/default.css';
@import '@clr/ui/shim.cds-core.min.css';
@import '@clr/ui/clr-ui.min.css';
[cds-card-remove-margin] {
--color: var(--cds-alias-object-container-border-color);
}
+14
View File
@@ -0,0 +1,14 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);
+18 -2
View File
@@ -6,6 +6,8 @@
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"strict": true, "strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"sourceMap": true, "sourceMap": true,
@@ -14,9 +16,23 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"target": "ES2020", "target": "ES2022",
"module": "es2020", "module": "es2020",
"lib": ["es2018", "dom"] "lib": [
"es2020",
"dom"
],
"useDefineForClassFields": false,
"paths": {
"stream": ["./node_modules/stream-browserify"],
"http": ["./node_modules/stream-http"],
"crypto": ["./node_modules/crypto-browserify"],
"zlib": ["./node_modules/browserify-zlib"],
"https": ["./node_modules/https-browserify"],
"url": ["./node_modules/url/"],
"buffer": ["./node_modules/buffer/"],
"Buffer": ["./node_modules/buffer/"],
}
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
+18
View File
@@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
+12 -4
View File
@@ -4,10 +4,11 @@ const webpack = require('webpack')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
const distDir = path.resolve(__dirname, 'dist', 'angular-cli') const distDir = path.resolve(__dirname, 'dist', 'angular-clr')
const buildDir = path.resolve(__dirname, 'build') const buildDir = path.resolve(__dirname, 'build')
const srcDir = path.resolve(__dirname, 'src') const srcDir = path.resolve(__dirname, 'src')
module.exports = { module.exports = {
mode: 'production', mode: 'production',
target: 'web', target: 'web',
@@ -21,13 +22,21 @@ module.exports = {
}, },
resolve: { resolve: {
extensions: ['', '.js', '.jsx', '.css'], extensions: ['', '.js', '.jsx', '.css'],
fallback: {
}
}, },
plugins: [ plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
new webpack.ProvidePlugin({
process: 'process/browser',
}),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: "body", inject: "body",
template: path.resolve(srcDir, 'index.html'), template: path.resolve(srcDir, 'index.html'),
}), }),
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/.*/]), new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/.*/])
], ],
optimization: { optimization: {
}, },
@@ -39,6 +48,5 @@ module.exports = {
}, },
], ],
}, },
externals: { externals: {},
},
} }
-1
View File
@@ -1,6 +1,5 @@
const path = require('path'); const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
if(!process.env['DATA_HASH']){ if(!process.env['DATA_HASH']){
console.log("Environment DATA_HASH not set") console.log("Environment DATA_HASH not set")