1+ import { getAppDataDir , getAppRootDir } from 'byteballcore/desktop_app'
12import { EventEmitter } from 'events'
3+ import { dirname } from 'path'
24import Typed from 'strict-event-emitter-types'
35
46import shimmer from './event-handler'
57import { IBot , IDatabase , IPairing , IPayment } from './event-types'
6- import setOption , { IOption } from './options'
8+ import { getOption , IOption , LIVENET , setOption , TESTNET } from './options'
9+ import { transformExpression } from './utils/template-literals'
710
811type EventType < T > = Typed < EventEmitter , T >
912
1013// TODO: map to https://github.com/byteball/reddit-attestation/blob/master/index.js
1114
1215export { LIVENET , TESTNET } from './options'
16+ const trimTrailingSpace = ( str : string ) => str . trimLeft ( ) . trimRight ( )
1317
1418export class Bot extends ( EventEmitter as new ( ) => EventType < IBot > ) {
1519 static get instance ( ) { return shimmer }
16- static setOption = setOption
20+ static get explorerSite ( ) { return getOption ( ) . testnet ? TESTNET . explorer : LIVENET . explorer }
1721
1822 get database ( ) : EventType < IDatabase > { return this . _ev }
1923 get pairing ( ) : EventType < IPairing > { return this . _ev }
2024 get payment ( ) : EventType < IPayment > { return this . _ev }
21- private readonly _ev = new EventEmitter ( )
25+ static setOption = setOption
2226
27+ // mimick https://github.com/electron/electron/blob/master/docs/api/app.md#appgetpathname
28+ static getPath ( name : 'userData' | 'appData' | 'package.json' ) : string | never {
29+ if ( name === 'userData' ) return getAppDataDir ( )
30+ else if ( name === 'package.json' ) return getAppRootDir ( )
31+ else if ( name === 'appData' ) return dirname ( getAppDataDir ( ) )
32+ else throw new Error ( "name must be one of these: 'userData', 'appData', or 'package.json" )
33+ }
34+
35+ private readonly _ev = new EventEmitter ( )
36+ private address ?: string
2337 private core ?: {
2438 device : Device
2539 wallet : Wallet
2640 composer : LazyImport < any >
2741 network : LazyImport < any >
42+ // db: LazyImport<any> // TODO: use it if there is side-effect
2843 }
2944
45+ private readonly db = require ( 'byteballcore/db' )
46+
3047 constructor ( option ?: Partial < IOption > ) {
3148 super ( )
3249 if ( option ) Bot . setOption ( option )
3350
3451 this . init ( ) . then ( events => { // mapping events from https://developer.byteball.org/list-of-events
52+ // TODO: this._ev = events
3553
54+ //#region IDatabase
3655 events . on ( 'ready' , ( ) => this . database . emit ( 'cordova:ready' ) )
3756 events . on ( 'started_db_upgrade' , ( ) => this . database . emit ( 'upgrade:start' ) )
3857 events . on ( 'finished_db_upgrade' , ( ) => this . database . emit ( 'upgrade:finish' ) )
58+ //#endregion
3959
60+ //#region IPairing
4061 events . on ( 'pairing_attempt' , ( ...r ) => this . pairing . emit ( 'attempt' , r [ 0 ] , r [ 1 ] ) )
4162 events . on ( 'paired' , ( ...r ) => this . pairing . emit ( 'success' , r [ 0 ] , r [ 1 ] ) )
4263 events . on ( 'removed_paired_device' , ( ...r ) => this . pairing . emit ( 'remove' , r [ 0 ] ) )
64+ //#endregion
4365
44- events . on ( 'new_private_payment' , ( ...r ) => this . payment . emit ( 'recieve' , r [ 0 ] ) )
45- events . on ( 'my_transactions_became_stable' , ( ...r ) => this . payment . emit ( 'confirm' , r [ 0 ] ) )
46- events . on ( 'received_payment' , ( ...r ) => this . payment . emit ( 'private:recieve' , r [ 0 ] , r [ 1 ] , r [ 2 ] , r [ 3 ] , r [ 4 ] ) )
66+ //#region IPayment
67+ events . on ( 'received_payment' , ( ...r ) => this . payment . emit ( 'private:receive' , r [ 0 ] , r [ 1 ] , r [ 2 ] , r [ 3 ] , r [ 4 ] ) )
4768 events . on ( 'unhandled_private_payments_left' , ( ...r ) => this . payment . emit ( 'private:unhandle' , r [ 0 ] ) )
69+ events . on ( 'new_my_transactions' , arrUnits => this . getPaymentDetails ( [ 'asset' , 'amount' , 'address' ] , arrUnits )
70+ . then ( row => this . payment . emit ( 'receive' , row . address as string , row . amount as number , arrUnits ) )
71+ )
72+ events . on ( 'my_transactions_became_stable' , arrUnits => this . getPaymentDetails ( [ 'asset' , 'amount' , 'address' ] , arrUnits )
73+ . then ( row => this . payment . emit ( 'confirm' , row . address as string , row . amount as number , arrUnits ) )
74+ )
75+ //#endregion
4876
4977 events . on ( 'text' , ( ...r ) => this . emit ( 'message' , {
5078 from_address : r [ 0 ] ,
@@ -67,19 +95,17 @@ export class Bot extends (EventEmitter as new() => EventType<IBot>) {
6795 ifError : rejects ,
6896 ifOk : ( joint : any ) => { // https://api.byteball.co/joint/oj8yEksX9Ubq7lLc+p6F2uyHUuynugeVq4+ikT67X6E=
6997 this . core ! . network . broadcastJoint ( joint )
70- resolve ( joint )
98+ resolve ( joint . unit . unit )
7199 }
72100 } )
73101
74- this . core . wallet . readSingleAddress ( address => {
75- this . core ! . composer . composeAttestationJoint (
76- address , // attestor address
77- subject_address , // address of the person being attested (subject)
78- profile , // attested profile
79- this . core ! . wallet . signer ,
80- callbacks
81- )
82- } )
102+ this . core ! . composer . composeAttestationJoint (
103+ this . address , // attestor address
104+ trimTrailingSpace ( subject_address ) , // address of the person being attested (subject)
105+ profile , // attested profile
106+ this . core ! . wallet . signer ,
107+ callbacks
108+ )
83109 } )
84110 }
85111
@@ -88,27 +114,81 @@ export class Bot extends (EventEmitter as new() => EventType<IBot>) {
88114 this . core . device . sendMessageToDevice ( to_address , 'text' , message )
89115 }
90116
117+ /**
118+ * @param text_or_amount is callback that must return a string which will be sended @to_address
119+ * @param address where the payment should be send
120+ * @arg message is a tagged template literals for transforming number into "send-link"
121+ */
122+ requestPayment (
123+ to_address : string ,
124+ text_or_amount : ( ( message : TaggedTemplateLiterals ) => string ) | number ,
125+ address ?: string | string [ ] // TODO: support {user1: addr1, user2:addr2}
126+ ) {
127+ if ( ! this . core ) throw new Error ( 'Bot not ready' )
128+
129+ const requestToBePayedTo = ( addr : string [ ] | string ) => {
130+ this . sendMessage ( to_address , typeof text_or_amount === 'function' ? text_or_amount (
131+ transformExpression (
132+ ( expr , _ , length ) => { // transform expr in `${expr}` to readable nominal
133+ if ( Array . isArray ( addr ) && length !== addr . length ) throw new Error ( 'number of amount not equal to address' )
134+ if ( typeof expr === 'number' ) {
135+ if ( expr < 1_000 ) return `${ expr } bytes`
136+ else if ( expr < 1_000_000 ) return `${ expr / 1_000 } KB`
137+ else if ( expr < 1_000_000_000 ) return `${ expr / 1_000_000 } MB`
138+ else return `${ expr / 1_000_000_000 } GB`
139+
140+ } else return expr
141+ } ,
142+ // add "Request Payments" URI link at the end of the message
143+ expr => expr . reduce (
144+ ( acc , curr , idx ) => `${ acc } \n[nyaa](byteball:${ Array . isArray ( addr ) ? addr [ idx ] : addr } ?amount=${ curr } )` , ''
145+ )
146+ )
147+ ) : `[woof](byteball:${ Array . isArray ( addr ) ? addr [ 0 ] : addr } ?amount=${ text_or_amount } )` )
148+ }
149+
150+ if ( ! address ) this . core . wallet . readSingleAddress ( bot_address => requestToBePayedTo ( bot_address ) )
151+ else requestToBePayedTo ( address )
152+ }
153+
91154 sendPayment ( recipient_address : string , amount : number ) {
92155 if ( ! this . core ) throw new Error ( 'Bot not ready' )
93- this . core . wallet . issueChangeAddressAndSendPayment ( null , amount , recipient_address , undefined ,
156+ this . core . wallet . issueChangeAddressAndSendPayment ( null , amount , trimTrailingSpace ( recipient_address ) , undefined ,
94157 ( err : Error , unit : number ) => this . payment . emit ( 'send' , unit , err )
95158 )
96159 }
97160
161+ private getPaymentDetails ( selector : string [ ] , units : string [ ] ) {
162+ return new Promise < PlainObject < string | number > > ( resolve => {
163+ this . db . query ( `SELECT ${ selector . join ( ',' ) } FROM outputs
164+ WHERE unit IN(?)` , [ units ] ,
165+ ( rows : PlainObject < any > [ ] ) => rows . forEach ( row => { // WARNING: has potential for duplicated messages
166+ if ( row . address !== this . address ) {
167+ if ( row . asset !== null ) this . payment . emit ( 'reject' , row . address , 'Received payment in wrong asset' )
168+ else resolve ( row )
169+ }
170+ } )
171+ )
172+ } )
173+ }
174+
98175 private async init ( ) {
99176 /// use of `await import` is for module thaat immediately run a service 😓
100177 this . core = {
101- wallet : await import ( 'headless-byteball' ) as unknown as Wallet ,
102- device : await import ( 'byteballcore/device' ) as unknown as Device ,
178+ wallet : await import ( 'headless-byteball' ) as Wallet ,
179+ device : await import ( 'byteballcore/device' ) as Device ,
103180 composer : await import ( 'byteballcore/composer' ) ,
104181 network : await import ( 'byteballcore/network' )
105182 }
106183 const eventBus = require ( 'byteballcore/event_bus' )
107184 return new Promise < EventEmitter > ( async resolve => {
108185 eventBus . once ( 'headless_wallet_ready' , ( ) => {
109186 this . core ! . wallet . setupChatEventHandlers ( )
110- this . emit ( 'ready' , this . core ! . device )
111- resolve ( eventBus )
187+ this . core ! . wallet . readFirstAddress ( address => {
188+ this . address = address
189+ this . emit ( 'ready' , this . core ! . device )
190+ resolve ( eventBus )
191+ } )
112192 } )
113193 } )
114194 }
0 commit comments