working prototype and updated README
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Overview
|
# 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.
|
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
|
# How it works
|
||||||
|
|
||||||
@@ -8,11 +8,11 @@ httXrp is a proof of concept for a truly serverless web architecture. If serverl
|
|||||||
|
|
||||||
Transactions on the ripple blockchain are allowed to carry up to 1kB of arbitrary data via the memo field.
|
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.
|
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.
|
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:
|
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
|
## 2: Abstracting the webserver away from the web
|
||||||
|
|
||||||
@@ -20,11 +20,53 @@ Using tools like `webpack`, it is possible to condense even modern complex singl
|
|||||||
|
|
||||||
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.
|
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: Dynamic web applications without a backend
|
## 3: Backendless dynamic web applications: Databases without databases
|
||||||
|
|
||||||
Superficially, this technique is limited to serving static webpages, as there is no *real* backend serving these pages. However, since it is possible to embed `xrpio` into such a "static" page, it is possible to listen for transactions on the Ripple blockchain containing valid xrpio hashes and to dynamically update the webpage's content based on the stored data.
|
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 `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
|
# Credits
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,57 @@
|
|||||||
<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">
|
||||||
<cds-icon shape="home" size="lg"></cds-icon>
|
<cds-icon shape="home" size="lg"></cds-icon>
|
||||||
<span class="title">Clarity</span>
|
<span class="title">httXrp</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-nav">
|
<div class="header-nav">
|
||||||
<a class="active nav-link nav-text">Home</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">
|
||||||
<h3>Clarity Starter Instructions:</h3>
|
<div class="clr-row">
|
||||||
|
<div class="clr-col-lg-5 clr-col-md-8 clr-col-12">
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header">Shout Something</h3>
|
||||||
|
<form clrForm #loginForm="ngForm">
|
||||||
|
|
||||||
<ul>
|
<div class="card-block">
|
||||||
<li>Start by clicking Fork in the toolbar above.</li>
|
<clr-input-container>
|
||||||
<li>Implement the problem in the new editor.</li>
|
<input clrInput [disabled]="sending" required placeholder="Title" type="text" [(ngModel)]="newShout.title" name="title" />
|
||||||
<li>
|
</clr-input-container>
|
||||||
Save the result, and share the url in a GitHub or StackOverflow issue.
|
<textarea clrTextarea [disabled]="sending" required placeholder="Shout Body" type="text" [(ngModel)]="newShout.body"
|
||||||
</li>
|
name="body"></textarea>
|
||||||
</ul>
|
<clr-input-container>
|
||||||
|
<input clrInput [disabled]="sending" required placeholder="Your Name" type="text" [(ngModel)]="newShout.from" name="from" />
|
||||||
|
</clr-input-container>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button class="btn btn-success" (click)="submitShout()"
|
||||||
|
[disabled]="!loginForm.form.valid || sending">Shout</button> <span *ngIf="sending">Sending...</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-row" *ngFor="let shout of shouts">
|
||||||
|
<div class="clr-col-lg-5 clr-col-md-8 clr-col-12">
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header">{{shout.title}}</h3>
|
||||||
|
<div class="card-block">
|
||||||
|
<div class="card-text">
|
||||||
|
{{shout.body}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
By: {{shout.from}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,10 +1,41 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ShoutboxDataService } from './services/ShoutboxData.service';
|
||||||
|
|
||||||
@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 {
|
export class AppComponent implements OnInit{
|
||||||
title = 'angular-cli';
|
title = 'httXrp';
|
||||||
|
shouts: any[] = []
|
||||||
|
sending = false
|
||||||
|
newShout = {
|
||||||
|
title: "",
|
||||||
|
body: "",
|
||||||
|
from: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dataService: ShoutboxDataService
|
||||||
|
){}
|
||||||
|
|
||||||
|
submitShout = () => {
|
||||||
|
this.sending = true
|
||||||
|
this.dataService.submitShout(this.newShout)
|
||||||
|
.then(() => {
|
||||||
|
this.newShout = {
|
||||||
|
title: "",
|
||||||
|
body: "",
|
||||||
|
from: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.sending = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(){
|
||||||
|
this.shouts = this.dataService.history
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
@@ -7,11 +7,27 @@ 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 } from '@cds/core/icon';
|
||||||
|
import { ShoutboxDataService, initShoutboxSvc } from './services/ShoutboxData.service';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
imports: [BrowserModule, AppRoutingModule, ClarityModule, CdsModule],
|
imports: [
|
||||||
providers: [],
|
BrowserModule,
|
||||||
|
AppRoutingModule,
|
||||||
|
ClarityModule,
|
||||||
|
CdsModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ShoutboxDataService,
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: initShoutboxSvc,
|
||||||
|
deps: [ShoutboxDataService],
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { DataParser } from "../util/Dataparser";
|
||||||
|
import { makeTestnetWallet } from "../util/TestnetUtils";
|
||||||
|
|
||||||
|
declare const xrpIO: any
|
||||||
|
declare const xrpl: any
|
||||||
|
|
||||||
|
const xrpNode = "wss://s.altnet.rippletest.net:51233"
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ShoutboxDataService {
|
||||||
|
|
||||||
|
public history: any[] = []
|
||||||
|
|
||||||
|
private userWallet1: any
|
||||||
|
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) => {
|
||||||
|
const shoutHash = await this.xrpio.treeWrite(JSON.stringify(shout), this.userWallet1.address, this.userWallet2.secret)
|
||||||
|
return await this.submit(shoutHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseMemos = async (memos: any) => {
|
||||||
|
const shouts = await Promise.all(memos
|
||||||
|
.map((memo: any) => {
|
||||||
|
if (!memo.Memo || !memo.Memo.MemoData)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
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 () => {
|
||||||
|
const raw_txs = await this.getTransactions()
|
||||||
|
return await this.parseMemos(raw_txs.flatMap((htx: any) => htx.Memos))
|
||||||
|
}
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
await this.subscribeTxs(async (raw_tx: any) => {
|
||||||
|
const shouts = await this.parseMemos(raw_tx.transaction.Memos)
|
||||||
|
this.history.unshift(...shouts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { AMOUNT_DECIMALS, AMOUNT_FORMAT, NON_ZERO_TX_HASH } from "./protocol.constants"
|
||||||
|
import { Amount, ArgumentType, TxHash } from "./types"
|
||||||
|
|
||||||
|
export class DataParser {
|
||||||
|
public static parse(type: ArgumentType | string[], value: any){
|
||||||
|
if(typeof type === 'object' && type[0] != null){ //enum value type
|
||||||
|
if(! (type as string[]).includes(value)){
|
||||||
|
throw new Error(`Invalid enum value type: ${value} is not included in ${type}`)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}else if(typeof type === 'string'){ //known defined type
|
||||||
|
if(!this.typeMap[type]){
|
||||||
|
throw new Error('Unknown parameter type'+type)
|
||||||
|
}
|
||||||
|
return this.typeMap[type](value)
|
||||||
|
}else{
|
||||||
|
throw new Error('FRAMEWORK_ERR: Datachecker.check(..): unknown argument type '+String(type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static typeMap : {[key in ArgumentType] : Function} = {
|
||||||
|
Amount: DataParser.parseAmount,
|
||||||
|
Boolean: DataParser.parseBoolean,
|
||||||
|
String: DataParser.parseString,
|
||||||
|
TxHash: DataParser.parseTxHash,
|
||||||
|
any: DataParser.parseAny
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static parseAmount(input: any): Amount{
|
||||||
|
if(typeof input === 'string' && typeof input !== 'number')
|
||||||
|
throw new Error('Input is not a number')
|
||||||
|
|
||||||
|
if(typeof input === 'string')
|
||||||
|
input = Number.parseFloat(input)
|
||||||
|
|
||||||
|
if(! AMOUNT_FORMAT.test(''+input)){
|
||||||
|
throw new Error('Input did not match the specification for `Amount`. The maximum number of decimals is '+AMOUNT_DECIMALS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static parseString(input: any): string{
|
||||||
|
if(typeof input !== 'string')
|
||||||
|
throw new Error('Input is not a string')
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseBoolean(input: any): boolean{
|
||||||
|
switch (typeof input) {
|
||||||
|
case 'boolean': return input;
|
||||||
|
case 'string': {
|
||||||
|
if(input === 'true') return true;
|
||||||
|
if(input === 'false') return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Input is not a boolean')
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseAny(input: any): any{
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseTxHash(input: any): TxHash{
|
||||||
|
if(typeof input !== 'string' || !NON_ZERO_TX_HASH.test(input)){
|
||||||
|
throw new Error('Input is not a trasnaction hash')
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export const makeTestnetWallet = () : Promise<{ secret: string, address: string }> => fetch('https://faucet.altnet.rippletest.net/accounts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}).then((raw:any) => {
|
||||||
|
return raw.json().then((content:any) => {
|
||||||
|
return({
|
||||||
|
secret: content.account.secret,
|
||||||
|
address: content.account.address
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export const MSG_DELIM: string = ' '
|
||||||
|
export const MSG_DATA_MAX: number = 925
|
||||||
|
export const PUBKEY_LEN: number = 66
|
||||||
|
export const NON_ZERO_TX_HASH = new RegExp(`[0-9A-F]{64}`)
|
||||||
|
export const PTR_FORMAT = new RegExp(`^((${NON_ZERO_TX_HASH.source})|0)`)
|
||||||
|
export const DATA_FORMAT = new RegExp(`(.{1,${MSG_DATA_MAX}})`)
|
||||||
|
export const SIGNATURE_FORMAT = new RegExp(`(\\S{140}|\\S{142})$`)
|
||||||
|
export const SIGNER_FORMAT = new RegExp(`(\\S{${PUBKEY_LEN}})`)
|
||||||
|
export const MSG_FORMAT = new RegExp(`${PTR_FORMAT.source}${MSG_DELIM}${DATA_FORMAT.source}`, 'm')
|
||||||
|
export const AMOUNT_DECIMALS = 18
|
||||||
|
export const MAX_SUPPLY = 20_000_000
|
||||||
|
export const AMOUNT_FORMAT = new RegExp(`\d+(\.\d{1,${AMOUNT_DECIMALS}})?`)
|
||||||
|
export const MIN_XRP_FEE = "0.00001"
|
||||||
|
export const MIN_XRP_TX_VALUE = "0.000001"
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
export type Memo = {
|
||||||
|
type?: string
|
||||||
|
format?: string
|
||||||
|
data?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Signature = {signature: string, signer: PublicKey}
|
||||||
|
|
||||||
|
export type Environment = {
|
||||||
|
msg: {
|
||||||
|
sender: Address,
|
||||||
|
value: number,
|
||||||
|
data: XrpTransaction,
|
||||||
|
rawTx: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Transition<Model> = {
|
||||||
|
call: keyof Model,
|
||||||
|
params: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ArgumentType = 'String' | 'Amount' | 'TxHash' | 'Boolean' | 'any'
|
||||||
|
export type ReturnType = ArgumentType | 'Void'
|
||||||
|
export type MutableStateOperation = 'ARRAY_PUSH' | 'ARRAY_UNSHIFT' | 'ARRAY_SHIFT' | 'ARRAY_POP' | 'VALUE_SET' | 'VALUE_DELETE'
|
||||||
|
export const MutableStateOperationStrings = ['ARRAY_PUSH', 'ARRAY_UNSHIFT', 'ARRAY_SHIFT', 'ARRAY_POP', 'VALUE_SET', 'VALUE_DELETE']
|
||||||
|
export type ArgumentDefiniton = {
|
||||||
|
type: ArgumentType | string[],
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
export type ContractFunctionSignature<Of = any> = {
|
||||||
|
name: keyof Of
|
||||||
|
argTypes: ArgumentDefiniton[]
|
||||||
|
returnType: ReturnType
|
||||||
|
documentation?: string,
|
||||||
|
modifier?: Modifier[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Modifier = "OWNER_ONLY" | "PAYABLE"
|
||||||
|
export type TransferListener = (transfer : { _from: Address, _to: Address, _value: Amount}) => void
|
||||||
|
export type ApprovalListener = (approval : { _from: Address, _spender: Address, _value: Amount}) => void
|
||||||
|
|
||||||
|
export type Address = string
|
||||||
|
export type Secret = string
|
||||||
|
export type PublicKey = string
|
||||||
|
export type Amount = number
|
||||||
|
export type TxHash = string
|
||||||
|
|
||||||
|
export type XrpTransaction = {
|
||||||
|
hash: TxHash,
|
||||||
|
sender: Address,
|
||||||
|
receiver: Address,
|
||||||
|
value: Amount,
|
||||||
|
fee: Amount,
|
||||||
|
ledger_index: number
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>AngularCli</title>
|
<title>httXrp</title>
|
||||||
<base href="" />
|
<base href="" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
<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>
|
</head>
|
||||||
|
|
||||||
<body cds-text="body">
|
<body cds-text="body">
|
||||||
|
|||||||
Reference in New Issue
Block a user