0.1 release, add .on(...) and .once(...) to RJSVM, add tests

This commit is contained in:
nitowa
2023-10-06 23:55:09 +02:00
parent 1dae68b1c7
commit 270c41a3dc
45 changed files with 5610 additions and 779 deletions
+48
View File
@@ -0,0 +1,48 @@
import { xrpIO } from "xrpio";
type Wallet = {
address: string,
secret: string
}
type DatawriterConfig = {
receiveAddress: string
sendWallet: Wallet
xrpNode: string
contractAddress: string
}
export class Datawriter {
constructor(private config: DatawriterConfig) {
}
async callEndpoint(endpointName: string, parameter: any, fee?: number) {
const xrpio: xrpIO = new xrpIO(this.config.xrpNode)
await xrpio.connect()
try{
const dataHash = await xrpio.treeWrite(
JSON.stringify(parameter),
this.config.receiveAddress,
this.config.sendWallet.secret
)
const hash = await xrpio.writeRaw(
{
data: JSON.stringify({
endpoint: endpointName,
data: dataHash
})
},
this.config.contractAddress,
this.config.sendWallet.secret,
undefined,
fee ? String(fee) : undefined
)
}catch(e){
console.log(e)
}finally{
await xrpio.disconnect()
}
}
}
+78 -11
View File
@@ -1,8 +1,10 @@
import { PaymentTx_T, ParameterizedFunction, Payload, RJSVM, RJSVM_Config, RJSVM_Endpoint, RJSVM_Implementations, payloadSchema, Function_Map, Endpoints_Of, State_Of, Generic_Ctor_ReturnType } from "./types"
import { PaymentTx_T, ParameterizedFunction, Payload, RJSVM, RJSVM_Config, RJSVM_Implementations, Function_Map, Endpoints_Of, State_Of, Generic_Ctor_ReturnType, RJSVM_Endpoint } from "./types"
import { Client as Xrpl } from 'xrpl';
import { xrpIO } from 'xrpio';
import { DataParser } from "../../util/dataparser";
import { XRP_ADDRESS } from "../../util/protocol.constants";
import { payloadSchema } from "./schemas";
import { InsufficientFeeError, RestrictedAccessError } from "../../util/errors";
export abstract class RJSVM_Builder {
@@ -18,6 +20,10 @@ export abstract class RJSVM_Builder {
private rippleApi: Xrpl
private xrpIO: xrpIO
private syncTimeout: NodeJS.Timeout
private subscribers = {}
private onceSubscribers = {}
public readonly definitions: Impl
constructor(
@@ -27,7 +33,9 @@ export abstract class RJSVM_Builder {
if(!XRP_ADDRESS.test(this.owner)){
throw new Error(`Inavlid owner address ${this.owner}`)
const err = new Error(`Inavlid owner address ${this.owner}`)
this.emit('error', err)
throw err
}
@@ -45,18 +53,72 @@ export abstract class RJSVM_Builder {
await this.xrpIO.connect()
await this.sync()
/*
prototype for a new-block listener. would call on every closed ledger (i.e. new block)
could be used to implement limited lifetime states without requiring new events to trigger
this.rippleApi.on('ledgerClosed', (ledger: any) => {
/*
console.log("---- Ledger ----")
console.log("index",ledger.ledger_index)
console.log("hash", ledger.ledger_hash)
console.log("---- /Ledger ----")
*/
})
await this.rippleApi.request({
command: 'subscribe',
streams: ['ledger']
})
*/
}
public disconnect = async () => {
if(this.syncTimeout){
clearTimeout(this.syncTimeout)
this.syncTimeout = undefined
}
if(this.xrpIO){
await this.xrpIO.disconnect()
this.xrpIO = undefined
}
if(this.rippleApi){
await this.rippleApi.disconnect()
this.rippleApi = undefined
}
this.subscribers = {}
}
public on = (event: string, handler: ParameterizedFunction) => {
const availableEvents = ['error', ...Object.keys(this.definitions)]
if(!availableEvents.includes(event))
return
if(!this.subscribers[event])
this.subscribers[event] = []
this.subscribers[event].push(handler)
}
public once = (event: string, handler: ParameterizedFunction) => {
const availableEvents = ['error', ...Object.keys(this.definitions)]
if(!availableEvents.includes(event))
return
if(!this.onceSubscribers[event])
this.onceSubscribers[event] = []
this.onceSubscribers[event].push(handler)
}
private emit = (event: string, payload: any) => {
if(this.subscribers[event])
this.subscribers[event].forEach(handler => handler(payload))
if(this.onceSubscribers[event]){
this.onceSubscribers[event].forEach(handler => handler(payload))
this.onceSubscribers[event] = []
}
}
private handlePayload = async (tx: PaymentTx_T, payload: Payload) => {
@@ -65,15 +127,16 @@ export abstract class RJSVM_Builder {
}
const endpointDef: RJSVM_Endpoint<RJSVM, any> = this.definitions[payload.endpoint]
if(endpointDef.visibility === 'owner' && tx.Account !== this.owner){
console.log(`owner restricted endpoint "${payload.endpoint}" called from ${tx.hash}. But ${tx.Account} != ${this.owner}`)
const err = new RestrictedAccessError(payload.endpoint, tx.hash, tx.Account, this.owner)
this.emit('error', err)
return
}
if(endpointDef.fee && Number(tx.Amount) < endpointDef.fee){
console.log(`Insufficient fee ${tx.hash}. Required ${endpointDef.fee}, was ${tx.Amount}`)
const err = new InsufficientFeeError(tx.hash, Number(endpointDef.fee), Number(tx.Amount))
this.emit('error', err)
return
}
@@ -82,8 +145,9 @@ export abstract class RJSVM_Builder {
const jsonData = JSON.parse(data)
endpointDef.parameterSchema.parse(jsonData)
this[payload.endpoint].apply(this, [tx, jsonData])
this.emit(payload.endpoint, jsonData)
} catch (err) {
console.log(err)
this.emit('error', err)
return
}
}
@@ -97,7 +161,8 @@ export abstract class RJSVM_Builder {
try {
const data = DataParser.hex_to_ascii(memo.Memo.MemoData)
return JSON.parse(data)
} catch (e) {
} catch (err) {
this.emit('error', err)
return
}
})
@@ -106,7 +171,9 @@ export abstract class RJSVM_Builder {
try {
const parsedPayload = payloadSchema.parse(payload)
this.handlePayload(tx, parsedPayload)
} catch (e) {
} catch (err) {
this.emit('error', err)
return
}
})
}
@@ -140,7 +207,7 @@ export abstract class RJSVM_Builder {
//presence of no marker means we caught up to current block height
this.sync_block_height = resp.result.ledger_index_max + 1
//schedule the next sync
setTimeout(this.sync, 10000)
this.syncTimeout = setTimeout(this.sync, 10000)
}
}
}
@@ -1,6 +1,10 @@
import { z } from "zod";
import { XRP_ADDRESS } from "./protocol.constants";
import { NON_ZERO_TX_HASH } from "xrpio";
import { NON_ZERO_TX_HASH, XRP_ADDRESS } from "../../util/protocol.constants";
export const payloadSchema = z.object({
endpoint: z.string(),
data: z.string()
})
export const xrp_address_schema = z.string().regex(XRP_ADDRESS, "Not a valid XRP address")
export const xrp_transaction_hash_schema = z.string().regex(NON_ZERO_TX_HASH, "Not a valid XRP transaction hash")
+8 -13
View File
@@ -1,12 +1,16 @@
import { z } from "zod";
import { payloadSchema } from "./schemas";
export abstract class RJSVM<State_T = any, Definitions_T extends Function_Map = Function_Map>{
owner: string
state: State_T
sync_block_height: number
config: RJSVM_Config
definitions: RJSVM_Implementations<any, Definitions_T>
definitions: RJSVM_Implementations<RJSVM<any, Definitions_T>, Definitions_T>
connect: () => Promise<void>
disconnect: () => Promise<void>
on: (event: string, handler: ParameterizedFunction) => void
once: (event: string, handler: ParameterizedFunction) => void
}
export type Generic_Ctor_ReturnType<Ctor>
@@ -14,8 +18,8 @@ export type Generic_Ctor_ReturnType<Ctor>
: Ctor extends abstract new (...args:any) => infer A ? A
: never
export type State_Of<T extends RJSVM<any>> = T extends RJSVM<infer State_T> ? State_T : any
export type Endpoints_Of<T extends RJSVM<any, any>> = T extends RJSVM<any, infer Endpoints_T> ? Endpoints_T : any
export type State_Of<T extends RJSVM> = T extends RJSVM<infer State_T> ? State_T : any
export type Endpoints_Of<T extends RJSVM> = T extends RJSVM<any, infer Endpoints_T> ? Endpoints_T : any
export type PaymentTx_T = {
@@ -72,24 +76,15 @@ export type RJSVM_Interface<
State_T = State_Of<RJSVM_T>,
Definitions_T extends Function_Map = Endpoints_Of<RJSVM_T>,
> = {
owner: string
state: State_T
} & {
[K in keyof Definitions_T]?: RJSVM_EndpointHandler<RJSVM_T, Definitions_T, Definitions_T[K]>
}
export type RJSVM_InitState<State_T> = {
owner: string,
state: State_T
}
export type RJSVM_Config = {
rippleNode: string,
listeningAddress: string
}
export const payloadSchema = z.object({
endpoint: z.string(),
data: z.string()
})
export type Payload = z.infer<typeof payloadSchema>
-79
View File
@@ -1,79 +0,0 @@
import { xrpIO } from "xrpio";
type Wallet = {
address: string,
secret: string
}
type DatawriterConfig = {
receiveAddress: string
sendWallet: Wallet
xrpNode: string
contractAddress: string
}
export class Datawriter {
constructor(private config: DatawriterConfig) {
}
async callEndpoint(endpointName: string, parameter: any, fee?: number) {
const xrpio: xrpIO = new xrpIO(this.config.xrpNode)
await xrpio.connect()
try{
const dataHash = await xrpio.treeWrite(
JSON.stringify(parameter),
this.config.receiveAddress,
this.config.sendWallet.secret
)
await xrpio.writeRaw(
{
data: JSON.stringify({
endpoint: endpointName,
data: dataHash
})
},
this.config.contractAddress,
this.config.sendWallet.secret,
undefined,
fee ? String(fee) : undefined
)
}catch(e){
console.log(e)
}finally{
await xrpio.disconnect()
}
}
}
(async () => {
const owner_dw = new Datawriter({
receiveAddress: "rDuXvMYNCEJCYDMFQykcafXhgi2NvMWqR",
sendWallet: {
address: "rUsPfG1hn6w6is28p5FUDWdMwvCo1iHrYq",
secret: "sEdTMicaTVmfsVMLhxrufyAzEQSnsaP"
},
xrpNode: "wss://s.altnet.rippletest.net:51233",
contractAddress: 'rLaXUiYvW1EMns69PsAfwSLb2VgNtPpZwq'
})
//await owner_dw.callEndpoint('submit', { title: "1", body: "2", from: "3" }, 100)
const user_dw = new Datawriter({
receiveAddress: "rwqCiEr3SLF43rAduhCChkR2K1XDhiqx5g",
sendWallet: {
address: "rHWN4X3hbodryX8H1EoPvmMV7AFHngEiBe",
secret: "sEdTdLPWvz69UAUmt1zYijTyWheER9u"
},
xrpNode: "wss://s.altnet.rippletest.net:51233",
contractAddress: 'rLaXUiYvW1EMns69PsAfwSLb2VgNtPpZwq'
})
//await owner_dw.callEndpoint('submit', { title: "1", body: "2", from: "3" }, 100)
//await user_dw.callEndpoint('restricted', { title: "user", body: "user", from: "user" })
//await owner_dw.callEndpoint('restricted', { title: "owner", body: "owner", from: "owner" })
await user_dw.callEndpoint('setTns', { hash: '01708abcF00636CE10E191FD782DBDC8F4076F28404BD88EBC31DE42DD084C0944B', name: 'test'})
})()
+127 -65
View File
@@ -1,78 +1,140 @@
import { z } from "zod";
import { RJSVM_Implementations, RJSVM_Interface, RJSVM, RJSVM_Config, RJSVM_InitState } from "./RJSVM/framework/types"
import { RJSVM_Builder } from "./main";
import { xrp_transaction_hash_schema } from "./util/schemas";
import { RJSVM_Implementations, RJSVM_Interface, RJSVM, RJSVM_Config } from "../src/RJSVM/framework/types"
import { RJSVM_Builder } from "../src/main";
import { Wallet } from "xrpio";
import { Datawriter } from "./RJSVM/datawriter/datawriter";
import { makeTestnetWallet } from "../test/tools";
import { xrp_transaction_hash_schema } from "./RJSVM/framework/schemas";
const shoutSchema = z.object({
title: z.string(),
body: z.string(),
from: z.string(),
id: z.optional(z.string())
})
type Shout = z.infer<typeof shoutSchema>
const xrpNode = "wss://s.altnet.rippletest.net:51233"
const tnsEntrySchema = z.object({
hash: xrp_transaction_hash_schema,
name: z.string(),
})
type TnsEntry = z.infer<typeof tnsEntrySchema>
let ownerWallet: Wallet //the RJSVM owner
let userWallet: Wallet //a RJSVM user
let listeningWallet: Wallet //wallet the RJSVM listens to
let drainWallet: Wallet //random wallet to send stuff to, could be anything
let rjsvm: RJSVM
type State = {
shouts: Shout[]
}
let user_datawriter: Datawriter
let owner_datawriter: Datawriter
type RJSVM_Endpoints = {
submit: (data: Shout) => void
restricted: (data: Shout) => void
setTns: (entry: TnsEntry) => void
}
abstract class RJSVM_Base
extends RJSVM<State, RJSVM_Endpoints>
implements RJSVM_Interface<RJSVM_Base> {
owner = "rUsPfG1hn6w6is28p5FUDWdMwvCo1iHrYq"
const setup = async () => {
state: State = {
shouts: []
[ownerWallet, listeningWallet, userWallet, drainWallet] = await Promise.all([makeTestnetWallet(),makeTestnetWallet(),makeTestnetWallet(),makeTestnetWallet()])
owner_datawriter = new Datawriter({
receiveAddress: drainWallet.address,
sendWallet: ownerWallet,
xrpNode: xrpNode,
contractAddress: listeningWallet.address
})
user_datawriter = new Datawriter({
receiveAddress: drainWallet.address,
sendWallet: userWallet,
xrpNode: xrpNode,
contractAddress: listeningWallet.address
})
//await owner_dw.callEndpoint('submit', { title: "1", body: "2", from: "3" }, 100)
//await owner_dw.callEndpoint('submit', { title: "1", body: "2", from: "3" }, 100)
//await user_dw.callEndpoint('restricted', { title: "user", body: "user", from: "user" })
//await owner_dw.callEndpoint('restricted', { title: "owner", body: "owner", from: "owner" })
// #########################
// Define parameter types
// #########################
const shoutSchema = z.object({
title: z.string(),
body: z.string(),
from: z.string(),
id: z.optional(z.string())
})
type Shout = z.infer<typeof shoutSchema>
const tnsEntrySchema = z.object({
hash: xrp_transaction_hash_schema,
name: z.string(),
})
type TnsEntry = z.infer<typeof tnsEntrySchema>
type State = {
shouts: Shout[]
}
}
const RJSVM_Contract: RJSVM_Implementations<RJSVM_Base> = {
submit: {
implementation: function (env, shout) {
this.state.shouts.unshift(shout)
console.log(shout)
},
visibility: 'public',
fee: 10,
parameterSchema: shoutSchema
},
// #########################
// Define endpoints
// #########################
restricted: {
implementation: function (env, shout) {
this.state.shouts.unshift(shout)
console.log(shout)
},
visibility: 'owner',
parameterSchema: shoutSchema
},
setTns: {
implementation: function (env, tnsEntry) {
console.log(tnsEntry)
},
visibility: 'public',
parameterSchema: tnsEntrySchema,
type RJSVM_Endpoints = {
submit: (data: Shout) => void
restricted: (data: Shout) => void
setTns: (entry: TnsEntry) => void
}
// #########################
// Define init state
// #########################
abstract class RJSVM_Base
extends RJSVM<State, RJSVM_Endpoints>
implements RJSVM_Interface<RJSVM_Base> {
owner = ownerWallet.address
state: State = {
shouts: []
}
}
// #########################
// Implement logic
// #########################
const RJSVM_Contract: RJSVM_Implementations<RJSVM_Base> = {
submit: {
implementation: function (env, shout) {
this.state.shouts.unshift(shout)
console.log(shout)
},
visibility: 'public',
fee: 10,
parameterSchema: shoutSchema
},
restricted: {
implementation: function (env, shout) {
this.state.shouts.unshift(shout)
console.log(shout)
},
visibility: 'owner',
parameterSchema: shoutSchema
},
setTns: {
implementation: function (env, tnsEntry) {
console.log(tnsEntry)
},
visibility: 'public',
parameterSchema: tnsEntrySchema,
}
}
// #########################
// Build and connect
// #########################
const Rjsvm = RJSVM_Builder.from(RJSVM_Base, RJSVM_Contract);
const conf: RJSVM_Config = {
listeningAddress: listeningWallet.address,
rippleNode: "wss://s.altnet.rippletest.net:51233"
}
rjsvm = new Rjsvm(conf)
await rjsvm.connect()
}
const Rjsvm = RJSVM_Builder.from(RJSVM_Base, RJSVM_Contract);
const conf: RJSVM_Config = {
listeningAddress: "rLaXUiYvW1EMns69PsAfwSLb2VgNtPpZwq",
rippleNode: "wss://s.altnet.rippletest.net:51233"
}
const rjsvm = new Rjsvm(conf)
rjsvm.connect()
(async () => {
await setup()
await owner_datawriter.callEndpoint('submit', { title: "1", body: "2", from: "3" }, 100)
})()
+11
View File
@@ -0,0 +1,11 @@
export class RestrictedAccessError extends Error{
constructor(endpoint:string, hash:string, callee:string, expecedCallee:string){
super(`Restricted endpoint "${endpoint}" called in ${hash}. But callee ${callee} is not owner ${expecedCallee}`)
}
}
export class InsufficientFeeError extends Error{
constructor(hash: string, requiredFee:number, suppliedFee:number){
super(`Insufficient fee in ${hash}. Required fee is ${requiredFee}, but was ${suppliedFee}`)
}
}