0.1 release, add .on(...) and .once(...) to RJSVM, add tests
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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>
|
||||
@@ -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
@@ -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)
|
||||
})()
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user