Skip to content
This repository was archived by the owner on Jun 19, 2021. It is now read-only.

Commit df02201

Browse files
committed
feat(lib): add payment related event & function 💵
- requestPayment() can utilize template literal - event 'receive' & 'confirm' expose: - single address (not device address) - nominal amount in `number` - array of unit transaction
1 parent 1c36781 commit df02201

File tree

4 files changed

+121
-24
lines changed

4 files changed

+121
-24
lines changed

src/event-types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ export interface IPairing {
66

77
export interface IPayment {
88
send(unit: number, error: Error): void
9-
recieve(arrNewUnits: number[]): void // #new_private_payment
10-
confirm(arrUnits: number[]): void // #my_transactions_became_stable
11-
'private:recieve'(// #received_payment
9+
reject(from_address: string, error: string): void
10+
receive(from_address: string, amount: number, arrNewUnits: string[]): void // #new_my_transactions
11+
confirm(from_address: string, amount: number, arrUnits: string[]): void // #my_transactions_became_stable
12+
'private:receive'(// #received_payment
1213
payer_device_address: string,
1314
assocAmountsByAsset: string,
1415
asset: string,

src/index.ts

Lines changed: 101 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,78 @@
1+
import {getAppDataDir, getAppRootDir} from 'byteballcore/desktop_app'
12
import {EventEmitter} from 'events'
3+
import {dirname} from 'path'
24
import Typed from 'strict-event-emitter-types'
35

46
import shimmer from './event-handler'
57
import {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

811
type EventType<T> = Typed<EventEmitter, T>
912

1013
// TODO: map to https://github.com/byteball/reddit-attestation/blob/master/index.js
1114

1215
export {LIVENET, TESTNET} from './options'
16+
const trimTrailingSpace = (str: string) => str.trimLeft().trimRight()
1317

1418
export 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
}

src/shim.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
//#region helpers type
12
type PlainObject<T> = {
23
// constructor: ObjectConstructor
34
[key: string]: T
45
}
56
type LazyImport<T> = { [key: string]: T }
67
type AnyFunc = (...args: any) => any
78
type NonFunc = string | number | string[] | number[] | PlainObject<NonFunc>
9+
type TaggedTemplateLiterals = (str: TemplateStringsArray, ...keys: any[]) => string
10+
//#endregion
811

912
interface Wallet {
1013
signer: string
@@ -17,6 +20,7 @@ interface Wallet {
1720
calback: (err: Error, unit: number) => void
1821
)
1922
readSingleAddress(cb: (address: string) => void)
23+
readFirstAddress(cb: (address: string) => void)
2024
}
2125
declare module 'headless-byteball' {
2226
export default Wallet
@@ -35,6 +39,11 @@ declare module 'byteballcore/device' {
3539
export default Device
3640
}
3741

42+
declare module 'byteballcore/desktop_app' {
43+
export const getAppDataDir: () => string
44+
export const getAppRootDir: () => string
45+
}
46+
3847
declare module 'byteballcore/event_bus' {
3948
import {EventEmitter} from 'events'
4049
const event: EventEmitter

src/utils/template-literals.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function transformExpression(
2+
arg: (expr: any, index: number, length: number) => string,
3+
suffix?: (expr: any[]) => string
4+
): TaggedTemplateLiterals {
5+
return (str, ...keys) => str.reduce((acc, curr, idx) => acc + arg(keys[idx - 1], idx, keys.length) + curr)
6+
+ (suffix ? suffix(keys) : '')
7+
}

0 commit comments

Comments
 (0)