working prototype and updated README
This commit is contained in:
@@ -1,26 +1,57 @@
|
||||
<div class="main-container">
|
||||
<header class="header-2">
|
||||
<div class="branding">
|
||||
<a class="nav-link">
|
||||
<cds-icon shape="home" size="lg"></cds-icon>
|
||||
<span class="title">Clarity</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-nav">
|
||||
<a class="active nav-link nav-text">Home</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content-container">
|
||||
<div class="content-area">
|
||||
<h3>Clarity Starter Instructions:</h3>
|
||||
<header class="header-2">
|
||||
<div class="branding">
|
||||
<a class="nav-link">
|
||||
<cds-icon shape="home" size="lg"></cds-icon>
|
||||
<span class="title">httXrp</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-nav">
|
||||
<a class="active nav-link nav-text">Shoutbox</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content-container">
|
||||
<div class="content-area">
|
||||
<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>
|
||||
<li>Start by clicking Fork in the toolbar above.</li>
|
||||
<li>Implement the problem in the new editor.</li>
|
||||
<li>
|
||||
Save the result, and share the url in a GitHub or StackOverflow issue.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="card-block">
|
||||
<clr-input-container>
|
||||
<input clrInput [disabled]="sending" required placeholder="Title" type="text" [(ngModel)]="newShout.title" name="title" />
|
||||
</clr-input-container>
|
||||
<textarea clrTextarea [disabled]="sending" required placeholder="Shout Body" type="text" [(ngModel)]="newShout.body"
|
||||
name="body"></textarea>
|
||||
<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>
|
||||
@@ -1,10 +1,41 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ShoutboxDataService } from './services/ShoutboxData.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'angular-cli';
|
||||
export class AppComponent implements OnInit{
|
||||
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 { AppRoutingModule } from './app-routing.module';
|
||||
@@ -7,11 +7,27 @@ import { CdsModule } from '@cds/angular';
|
||||
import { ClarityModule } from '@clr/angular';
|
||||
|
||||
import { ClarityIcons, homeIcon } from '@cds/core/icon';
|
||||
import { ShoutboxDataService, initShoutboxSvc } from './services/ShoutboxData.service';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [BrowserModule, AppRoutingModule, ClarityModule, CdsModule],
|
||||
providers: [],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
ClarityModule,
|
||||
CdsModule,
|
||||
FormsModule
|
||||
],
|
||||
providers: [
|
||||
ShoutboxDataService,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initShoutboxSvc,
|
||||
deps: [ShoutboxDataService],
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
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">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>AngularCli</title>
|
||||
<title>httXrp</title>
|
||||
<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/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">
|
||||
|
||||
Reference in New Issue
Block a user