Skip to content

Commit 049cf5a

Browse files
committed
feat: add chronik handler
1 parent e792d7a commit 049cf5a

File tree

7 files changed

+385
-14
lines changed

7 files changed

+385
-14
lines changed

react/lib/components/PayButton/PayButton.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
getCurrencyObject,
1818
isPropsTrue,
1919
setupAltpaymentSocket,
20-
setupTxsSocket,
20+
setupChronikWebSocket,
2121
CryptoCurrency,
2222
ButtonSize
2323
} from '../../util';
@@ -184,7 +184,7 @@ export const PayButton = (props: PayButtonProps): React.ReactElement => {
184184
(async () => {
185185
if (txsSocket === undefined) {
186186
const expectedAmount = currencyObj ? currencyObj?.float : undefined
187-
await setupTxsSocket({
187+
await setupChronikWebSocket({
188188
address: to,
189189
txsSocket,
190190
apiBaseUrl,
@@ -221,10 +221,10 @@ export const PayButton = (props: PayButtonProps): React.ReactElement => {
221221
})()
222222

223223
return () => {
224-
if (txsSocket !== undefined) {
225-
txsSocket.disconnect();
226-
setTxsSocket(undefined);
227-
}
224+
// if (txsSocket !== undefined) {
225+
// txsSocket.disconnect();
226+
//setTxsSocket(undefined);
227+
// }
228228
if (altpaymentSocket !== undefined) {
229229
altpaymentSocket.disconnect();
230230
setAltpaymentSocket(undefined);

react/lib/components/Widget/Widget.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
CURRENCY_PREFIXES_MAP,
3131
CRYPTO_CURRENCIES,
3232
isPropsTrue,
33-
setupTxsSocket,
33+
setupChronikWebSocket,
3434
setupAltpaymentSocket,
3535
CryptoCurrency,
3636
} from '../../util';
@@ -463,7 +463,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
463463
useEffect(() => {
464464
(async () => {
465465
if (isChild !== true) {
466-
await setupTxsSocket({
466+
await setupChronikWebSocket({
467467
address: to,
468468
txsSocket: thisTxsSocket,
469469
apiBaseUrl,
@@ -489,10 +489,10 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
489489
})()
490490

491491
return () => {
492-
if (thisTxsSocket !== undefined) {
493-
thisTxsSocket.disconnect();
494-
setThisTxsSocket(undefined);
495-
}
492+
// if (thisTxsSocket !== undefined) {
493+
//thisTxsSocket.disconnect();
494+
//setThisTxsSocket(undefined);
495+
// }
496496
if (thisAltpaymentSocket !== undefined) {
497497
thisAltpaymentSocket.disconnect();
498498
setThisAltpaymentSocket(undefined);

react/lib/util/chronik.ts

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { ChronikClient, WsEndpoint, Tx} from 'chronik-client-cashtokens';
2+
import { encodeCashAddress, decodeCashAddress } from 'ecashaddrjs'
3+
import { AddressType } from 'ecashaddrjs/dist/types'
4+
import xecaddr from 'xecaddrjs'
5+
import { parseOpReturnData } from './opReturn';
6+
import Decimal from 'decimal.js'
7+
import { Buffer } from 'buffer';
8+
import { Transaction } from './types';
9+
import { getAddressPrefix } from './address';
10+
11+
const decoder = new TextDecoder()
12+
export interface OpReturnData {
13+
rawMessage: string
14+
message: string
15+
paymentId: string
16+
}
17+
18+
export function getNullDataScriptData (outputScript: string): OpReturnData | null {
19+
if (outputScript.length < 2 || outputScript.length % 2 !== 0) {
20+
throw new Error(`Invalid outputScript length`)
21+
}
22+
const opReturnCode = '6a'
23+
const encodedProtocolPushData = '04' // '\x04'
24+
const encodedProtocol = '50415900' // 'PAY\x00'
25+
26+
const prefixLen = (
27+
opReturnCode.length +
28+
encodedProtocolPushData.length +
29+
encodedProtocol.length +
30+
2 // version byte
31+
)
32+
33+
const regexPattern = new RegExp(
34+
`${opReturnCode}${encodedProtocolPushData}${encodedProtocol}.{2}`,
35+
'i'
36+
)
37+
38+
if (!regexPattern.test(outputScript.slice(0, prefixLen))) {
39+
return null
40+
}
41+
42+
let dataStartIndex = prefixLen + 2
43+
44+
if (outputScript.length < dataStartIndex) {
45+
return null
46+
}
47+
48+
let dataPushDataHex = outputScript.slice(prefixLen, dataStartIndex)
49+
if (dataPushDataHex.toLowerCase() === '4c') {
50+
dataStartIndex = dataStartIndex + 2
51+
dataPushDataHex = outputScript.slice(prefixLen + 2, dataStartIndex)
52+
}
53+
const dataPushData = parseInt(dataPushDataHex, 16)
54+
if (outputScript.length < dataStartIndex + dataPushData * 2) {
55+
return null
56+
}
57+
58+
const dataHexBuffer = Buffer.from(
59+
outputScript.slice(dataStartIndex, dataStartIndex + dataPushData * 2),
60+
'hex'
61+
)
62+
const dataString = decoder.decode(dataHexBuffer)
63+
64+
const ret: OpReturnData = {
65+
rawMessage: dataString,
66+
message: parseOpReturnData(dataString),
67+
paymentId: ''
68+
}
69+
70+
const paymentIdPushDataIndex = dataStartIndex + dataPushData * 2
71+
const paymentIdStartIndex = paymentIdPushDataIndex + 2
72+
const hasPaymentId = outputScript.length >= paymentIdStartIndex
73+
if (!hasPaymentId) {
74+
return ret
75+
}
76+
77+
const paymentIdPushDataHex = outputScript.slice(paymentIdPushDataIndex, paymentIdStartIndex)
78+
const paymentIdPushData = parseInt(paymentIdPushDataHex, 16)
79+
let paymentIdString = ''
80+
if (outputScript.length < paymentIdStartIndex + paymentIdPushData * 2) {
81+
return ret
82+
}
83+
for (let i = 0; i < paymentIdPushData; i++) {
84+
const hexByte = outputScript.slice(paymentIdStartIndex + (i * 2), paymentIdStartIndex + (i * 2) + 2)
85+
// we don't decode the hex for the paymentId, since those are just random bytes.
86+
paymentIdString += hexByte
87+
}
88+
ret.paymentId = paymentIdString
89+
90+
return ret
91+
}
92+
93+
export function toHash160 (address: string): {type: AddressType, hash160: string} {
94+
try {
95+
const { type, hash } = decodeCashAddress(address)
96+
return { type, hash160: hash }
97+
} catch (err) {
98+
console.log('[CHRONIK]: Error converting address to hash160')
99+
throw err
100+
}
101+
}
102+
export async function satoshisToUnit(satoshis: bigint, networkFormat: string): Promise<string> {
103+
const decimal = new Decimal(satoshis.toString())
104+
105+
if (networkFormat === xecaddr.Format.Xecaddr) {
106+
return decimal.div(1e2).toString()
107+
} else if (networkFormat === xecaddr.Format.Cashaddr) {
108+
return decimal.div(1e8).toString()
109+
}
110+
111+
throw new Error('Invalid address')
112+
}
113+
const getTransactionAmountAndData = async (transaction: Tx, addressString: string): Promise<{amount: string, opReturn: string}> => {
114+
let totalOutput = BigInt(0);
115+
let totalInput = BigInt(0);
116+
const addressFormat = xecaddr.detectAddressFormat(addressString)
117+
const script = toHash160(addressString).hash160
118+
let opReturn = ''
119+
120+
for (const output of transaction.outputs) {
121+
if (output.outputScript.includes(script)) {
122+
totalOutput += output.sats
123+
}
124+
if (opReturn === '') {
125+
const nullScriptData = getNullDataScriptData(output.outputScript)
126+
if (nullScriptData !== null) {
127+
opReturn = JSON.stringify(
128+
nullScriptData
129+
)
130+
}
131+
}
132+
}
133+
for (const input of transaction.inputs) {
134+
if (input?.outputScript?.includes(script) === true) {
135+
totalInput += input.sats
136+
}
137+
}
138+
const satoshis = totalOutput - totalInput
139+
return {
140+
amount: await satoshisToUnit(satoshis, addressFormat),
141+
opReturn
142+
}
143+
}
144+
145+
const getTransactionFromChronikTransaction = async (transaction: Tx, address: string): Promise<Transaction> => {
146+
const { amount, opReturn } = await getTransactionAmountAndData(transaction, address)
147+
const parsedOpReturn = resolveOpReturn(opReturn)
148+
return {
149+
hash: transaction.txid,
150+
amount,
151+
address,
152+
timestamp: transaction.block !== undefined ? transaction.block.timestamp : transaction.timeFirstSeen,
153+
confirmed: transaction.block !== undefined,
154+
opReturn,
155+
paymentId: parsedOpReturn?.paymentId ?? '',
156+
message: parsedOpReturn?.message ?? '',
157+
rawMessage: parsedOpReturn?.rawMessage ?? '',
158+
}
159+
}
160+
export const fromHash160 = (networkSlug: string, type: AddressType, hash160: string): string => {
161+
const buffer = Buffer.from(hash160, 'hex')
162+
163+
// Because ecashaddrjs only accepts Uint8Array as input type, convert
164+
const hash160ArrayBuffer = new ArrayBuffer(buffer.length)
165+
const hash160Uint8Array = new Uint8Array(hash160ArrayBuffer)
166+
for (let i = 0; i < hash160Uint8Array.length; i += 1) {
167+
hash160Uint8Array[i] = buffer[i]
168+
}
169+
170+
return encodeCashAddress(
171+
networkSlug,
172+
type,
173+
hash160Uint8Array
174+
)
175+
}
176+
177+
export function outputScriptToAddress (networkSlug: string, outputScript: string | undefined): string | undefined {
178+
if (outputScript === undefined) return undefined
179+
180+
const typeTestSlice = outputScript.slice(0, 4)
181+
let addressType
182+
let hash160
183+
switch (typeTestSlice) {
184+
case '76a9':
185+
addressType = 'p2pkh'
186+
hash160 = outputScript.substring(
187+
outputScript.indexOf('76a914') + '76a914'.length,
188+
outputScript.lastIndexOf('88ac')
189+
)
190+
break
191+
case 'a914':
192+
addressType = 'p2sh'
193+
hash160 = outputScript.substring(
194+
outputScript.indexOf('a914') + 'a914'.length,
195+
outputScript.lastIndexOf('87')
196+
)
197+
break
198+
default:
199+
return undefined
200+
}
201+
202+
if (hash160.length !== 40) return undefined
203+
204+
return fromHash160(networkSlug, addressType as AddressType, hash160)
205+
}
206+
207+
const getRelatedAddressesForTransaction = (transaction: Tx, networkSlug: string): (string | undefined)[] => {
208+
const inputAddresses = transaction.inputs.map(inp => outputScriptToAddress(networkSlug, inp.outputScript))
209+
const outputAddresses = transaction.outputs.map(out => outputScriptToAddress(networkSlug, out.outputScript))
210+
return [...inputAddresses, ...outputAddresses].filter(a => a !== undefined)
211+
}
212+
213+
export const getAddressesForTransaction = async (transaction: Tx, networkSlug: string): Promise<any[]> => {
214+
const relatedAddresses = getRelatedAddressesForTransaction(transaction, networkSlug)
215+
const addressesWithTransactions: any = await Promise.all(relatedAddresses.map(
216+
async address => {
217+
return {
218+
address,
219+
transaction: await getTransactionFromChronikTransaction(transaction, address ?? '')
220+
}
221+
}
222+
))
223+
return addressesWithTransactions
224+
}
225+
226+
const resolveOpReturn = (opReturn: string): OpReturnData | null => {
227+
try {
228+
return opReturn === '' ? null : JSON.parse(opReturn)
229+
} catch (e) {
230+
return null
231+
}
232+
}
233+
234+
export const parseWebsocketMessage = async (
235+
wsMsg: any,
236+
onTransaction: Function,
237+
chronik: ChronikClient,
238+
networkslug: string,
239+
) => {
240+
const { type } = wsMsg;
241+
if (type === 'Error') {
242+
return;
243+
}
244+
const { msgType } = wsMsg;
245+
246+
switch (msgType) {
247+
case 'TX_ADDED_TO_MEMPOOL': {
248+
const rawTransaction = await chronik.tx(wsMsg.txid);
249+
const addressesWithTransactions = await getAddressesForTransaction(rawTransaction, networkslug)
250+
for (const addressWithTransaction of addressesWithTransactions) {
251+
if (addressWithTransaction.transaction.amount > Decimal(0)) {
252+
onTransaction([addressWithTransaction.transaction]);
253+
}
254+
}
255+
}
256+
default:
257+
return;
258+
}
259+
};
260+
261+
export const initializeChronikWebsocket = async (
262+
address: string,
263+
onTransaction: Function
264+
): Promise<WsEndpoint> => {
265+
const chronik = new ChronikClient(['https://chronik.e.cash']);
266+
const networSlug = getAddressPrefix(address)
267+
const ws = chronik.ws({
268+
onMessage: async (msg: any) => {
269+
await parseWebsocketMessage(
270+
msg,
271+
onTransaction,
272+
chronik,
273+
networSlug
274+
);
275+
},
276+
});
277+
await ws.waitForOpen();
278+
ws.subscribeToAddress(address);
279+
280+
return ws;
281+
};

0 commit comments

Comments
 (0)