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