import Arweave from 'arweave' ; import Blockweave from 'blockweave' ; import { fieldType } from './faces/fields' ; import GQLResultInterface , { GQLEdgeBlockInterface , GQLEdgeTransactionInterface } from './faces/gql' ; import { IGlobalOptions , RequestType } from './faces/options' ; import ArdbBlock from './models/block' ; import ArdbTransaction from './models/transaction' ; import { Log , log , LOGS } from './utils/log' ; /**
 * Arweave as a database.
 * To easily interact with Arweave's graphql endpoint.
 */ export default class ArDB { private arweave : Arweave | Blockweave ; private reqType : RequestType = 'transactions' ; private options : IGlobalOptions = { } ; private after : string = '' ; private readonly afterRegex = /after: *"([^"]*)"/gi ; private readonly emptyLinesRegex = /^\s*[\r\n]/gm ; private readonly fields : fieldType [ ] = [ 'id' , 'anchor' , 'signature' , 'recipient' , 'owner' , 'owner.address' , 'owner.key' , 'fee' , 'fee.winston' , 'fee.ar' , 'quantity' , 'quantity.winston' , 'quantity.ar' , 'data' , 'data.size' , 'data.type' , 'tags' , 'tags.name' , 'tags.value' , 'block' , 'block.id' , 'block.timestamp' , 'block.height' , 'block.previous' , 'parent' , 'parent.id' , ] ; private includes : Set < fieldType > = new Set ( ) ; private log : Log ; param arweave param logLevel constructor ( arweave , logLevel = LOGS . ARWEAVE ) { this . arweave = arweave ; log . init ( logLevel , arweave ) ; this . includes = new Set ( this . fields ) ; } returns getCursor ( ) { return this . after ; } param type returns search ( type = 'transactions' ) { this . reqType = type ; this . options = { } ; this . after = '' ; return this ; } param id returns id ( id ) { this . checkSearchType ( ) ; this . options . id = id ; this . options . ids = [ id ] ; return this ; } param ids returns ids ( ids ) { this . checkSearchType ( ) ; this . options . ids = ids ; this . options . id = ids [ 0 ] ; return this ; } param name returns appName ( name ) { this . checkSearchType ( ) ; this . tag ( 'App-Name' , [ name ] ) ; return this ; } param type returns type ( type ) { this . checkSearchType ( ) ; this . tag ( 'Content-Type' , [ type ] ) ; return this ; } param tags returns tags ( tags ) { this . checkSearchType ( ) ; const ts = [ ] ; for ( const tag of tags ) { const values = typeof tag . values === 'string' ? [ tag . values ] : tag . values ; ts . push ( { name : tag . name , values , } ) ; } this . options . tags = ts ; return this ; } param name param values returns tag ( name , values ) { this . checkSearchType ( ) ; if ( ! this . options . tags ) { this . options . tags = [ ] ; } if ( typeof values === 'string' ) { values = [ values ] ; } this . options . tags . push ( { name , values } ) ; return this ; } param owners returns from ( owners ) { this . checkSearchType ( ) ; if ( typeof owners === 'string' ) { owners = [ owners ] ; } this . options . owners = owners ; return this ; } param recipients returns to ( recipients ) { this . checkSearchType ( ) ; if ( typeof recipients === 'string' ) { recipients = [ recipients ] ; } this . options . recipients = recipients ; return this ; } param min returns min ( min ) { this . checkSearchType ( ) ; if ( ! this . options . block ) { this . options . block = { } ; } this . options . block . min = min ; return this ; } param max returns max ( max ) { this . checkSearchType ( ) ; if ( ! this . options . block ) { this . options . block = { } ; } this . options . block . max = max ; return this ; } param limit returns limit ( limit ) { this . checkSearchType ( ) ; if ( limit < 1 ) { console . warn ( 'Limit cannot be less than 1, setting it to 1.' ) ; limit = 1 ; } else if ( limit > 100 ) { console . warn ( "Arweave GQL won't return more than 100 entries at once." ) ; } this . options . first = limit ; return this ; } param sort returns sort ( sort ) { this . options . sort = sort ; return this ; } param after returns cursor ( after ) { this . checkSearchType ( ) ; this . options . after = after ; return this ; } param fields returns only ( fields ) { this . includes = new Set ( ) ; if ( typeof fields === 'string' && this . fields . indexOf ( fields ) !== - 1 ) { this . includes . add ( fields ) ; this . validateIncludes ( ) ; return this ; } const toInclude = [ ] ; for ( const field of fields ) { if ( this . fields . indexOf ( field ) !== - 1 ) { toInclude . push ( field ) ; } } if ( toInclude . length ) { this . includes = new Set ( toInclude ) ; } this . validateIncludes ( ) ; return this ; } param fields returns exclude ( fields ) { this . includes = new Set ( this . fields ) ; if ( typeof fields === 'string' ) { this . includes . delete ( fields ) ; this . validateIncludes ( ) ; return this ; } for ( const field of fields ) { this . includes . delete ( field ) ; } this . validateIncludes ( ) ; return this ; } param filters returns async find ( filters = { } ) { this . checkSearchType ( ) ; for ( const filter of Object . keys ( filters ) ) { this . options [ filter ] = filters [ filter ] ; } if ( ! this . options . first ) { this . options . first = 10 ; } const query = this . construct ( ) ; return this . run ( query ) ; } param filters returns async findOne ( filters = { } ) { this . checkSearchType ( ) ; for ( const filter of Object . keys ( filters ) ) { this . options [ filter ] = filters [ filter ] ; } this . options . first = 1 ; const query = this . construct ( ) ; const txs = await this . run ( query ) ; return txs . length ? txs [ 0 ] : null ; } async findAll ( filters = { } ) { this . checkSearchType ( ) ; for ( const filter of Object . keys ( filters ) ) { this . options [ filter ] = filters [ filter ] ; } this . options . first = 100 ; const query = this . construct ( ) ; return this . runAll ( query ) ; } /**
     * To run with the cursor
     */ async next ( ) { if ( ! this . after || ! this . after . length ) { log . show ( 'next(): Nothing more to search.' ) ; return ; } const query = this . construct ( ) . replace ( this . afterRegex , `after: "${ this . after }"` ) ; const result = await this . run ( query ) ; return this . options . first === 1 ? ( result . length ? result [ 0 ] : null ) : result ; } async run ( query ) { log . show ( 'Running query:' ) ; log . show ( query ) ; const res = await this . get ( query ) ; if ( ! res ) return [ ] ; if ( res . transaction ) { return [ new ArdbTransaction ( res . transaction , this . arweave ) ] ; } else if ( res . block ) { return [ new ArdbBlock ( res . block ) ] ; } else if ( res . transactions ) { const edges = res . transactions . edges ; if ( edges && edges . length ) { this . after = edges [ edges . length - 1 ] . cursor ; } else { this . after = '' ; } return edges . map ( ( edge ) => new ArdbTransaction ( edge . node , this . arweave ) ) ; } else if ( res . blocks ) { const edges = res . blocks . edges ; if ( edges && edges . length ) { this . after = edges [ edges . length - 1 ] . cursor ; } else { this . after = '' ; } return edges . map ( ( edge ) => new ArdbBlock ( edge . node ) ) ; } } async runAll ( query ) { let hasNextPage = true ; let edges = [ ] ; let cursor = this . options . after || '' ; let isTx = true ; while ( hasNextPage ) { log . show ( 'Running query:' ) ; log . show ( query ) ; const res = await this . get ( query ) ; if ( ! res ) { return [ ] ; } if ( res . transaction ) { return [ new ArdbTransaction ( res . transaction , this . arweave ) ] ; } else if ( res . block ) { return [ new ArdbBlock ( res . block ) ] ; } else if ( res . transactions ) { const r = res . transactions ; if ( r . edges && r . edges . length ) { edges = edges . concat ( r . edges ) ; cursor = r . edges [ r . edges . length - 1 ] . cursor ; query = query . replace ( this . afterRegex , `after: "${ cursor }"` ) ; } hasNextPage = r . pageInfo . hasNextPage ; } else if ( res . blocks ) { isTx = false ; const r = res . blocks ; if ( r . edges && r . edges . length ) { edges = edges . concat ( r . edges ) ; cursor = r . edges [ r . edges . length - 1 ] . cursor ; query = query . replace ( this . afterRegex , `after: "${ cursor }"` ) ; } hasNextPage = r . pageInfo . hasNextPage ; } } if ( isTx ) { return edges . map ( ( edge ) => new ArdbTransaction ( edge . node , this . arweave ) ) ; } else { return edges . map ( ( edge ) => new ArdbBlock ( edge . node ) ) ; } } /** Helpers */ private checkSearchType ( ) { if ( ! this . reqType || ( this . reqType !== 'transaction' && this . reqType !== 'block' && this . reqType !== 'transactions' && this . reqType !== 'blocks' ) ) { throw new Error ( 'Invalid search type. Must provide one and it must be either "transaction", "transactions", "block" or "blocks"' ) ; } } private async get ( query ) { const res = await this . arweave . api . post ( 'graphql' , { query } , { headers : { 'content-type' : 'application/json' } } ) ; log . show ( 'Returned result: ' ) ; log . show ( res . data . data ) ; return res . data . data ; } private construct ( ) { if ( this . reqType === 'transactions' || this . reqType === 'blocks' ) { delete this . options . id ; if ( this . reqType === 'transactions' ) { delete this . options . height ; } else { delete this . options . owners ; delete this . options . recipients ; delete this . options . tags ; delete this . options . block ; } if ( ! this . options . after ) { this . options . after = '' ; } } else { this . options = { id : this . options . id } ; } let params = JSON . stringify ( this . options , null , 2 ) . replace ( /"([^"]+)":/gm , '$1: ' ) . replace ( '"HEIGHT_DESC"' , 'HEIGHT_DESC' ) . replace ( '"HEIGHT_ASC"' , 'HEIGHT_ASC' ) ; params = params . substring ( 1 , params . length - 1 ) ; let fields = '' ; if ( this . reqType === 'transaction' || this . reqType === 'transactions' ) { let owner = '' ; if ( this . includes . has ( 'owner' ) ) { owner = `owner {
          ${ this . includes . has ( 'owner.address' ) ? 'address' : '' }
          ${ this . includes . has ( 'owner.key' ) ? 'key' : '' }
        }` ; } let fee = '' ; if ( this . includes . has ( 'fee' ) ) { fee = `fee {
          ${ this . includes . has ( 'fee.winston' ) ? 'winston' : '' }
          ${ this . includes . has ( 'fee.ar' ) ? 'ar' : '' }
        }` ; } let quantity = '' ; if ( this . includes . has ( 'quantity' ) ) { quantity = `quantity {
          ${ this . includes . has ( 'quantity.winston' ) ? 'winston' : '' }
          ${ this . includes . has ( 'quantity.ar' ) ? 'ar' : '' }
        }` ; } let data = '' ; if ( this . includes . has ( 'data' ) ) { data = `data {
          ${ this . includes . has ( 'data.size' ) ? 'size' : '' }
          ${ this . includes . has ( 'data.type' ) ? 'type' : '' }
        }` ; } let tags = '' ; if ( this . includes . has ( 'tags' ) ) { tags = `tags {
          ${ this . includes . has ( 'tags.name' ) ? 'name' : '' }
          ${ this . includes . has ( 'tags.value' ) ? 'value' : '' }
        }` ; } let block = '' ; if ( this . includes . has ( 'block' ) ) { block = `block {
          ${ this . includes . has ( 'block.id' ) ? 'id' : '' }
          ${ this . includes . has ( 'block.timestamp' ) ? 'timestamp' : '' }
          ${ this . includes . has ( 'block.height' ) ? 'height' : '' }
          ${ this . includes . has ( 'block.previous' ) ? 'previous' : '' }
        }` ; } let parent = '' ; if ( this . includes . has ( 'parent' ) || this . includes . has ( 'parent.id' ) ) { parent = `parent {
          id
        }` ; } fields = `
      ${ this . includes . has ( 'id' ) ? 'id' : '' }
      ${ this . includes . has ( 'anchor' ) ? 'anchor' : '' }
      ${ this . includes . has ( 'signature' ) ? 'signature' : '' }
      ${ this . includes . has ( 'recipient' ) ? 'recipient' : '' }
      ${ owner }
      ${ fee }
      ${ quantity }
      ${ data }
      ${ tags }
      ${ block }
      ${ parent }
      ` ; fields = fields . replace ( this . emptyLinesRegex , '' ) . trim ( ) ; if ( ! fields . length ) { fields = `
        id
        anchor
        signature
        recipient
        owner {
          address
          key
        }
        fee {
          winston
          ar
        }
        quantity {
          winston
          ar
        }
        data {
          size
          type
        }
        tags {
          name
          value
        }
        block {
          id
          timestamp
          height
          previous
        }
        parent {
          id
        }` ; } } else { fields = `
      ${ this . includes . has ( 'block.id' ) ? 'id' : '' }
      ${ this . includes . has ( 'block.timestamp' ) ? 'timestamp' : '' }
      ${ this . includes . has ( 'block.height' ) ? 'height' : '' }
      ${ this . includes . has ( 'block.previous' ) ? 'previous' : '' }
      ` ; fields = fields . replace ( this . emptyLinesRegex , '' ) . trim ( ) ; if ( ! fields . length ) { fields = `
        id
        timestamp
        height
        previous` ; } } if ( this . reqType === 'transactions' || this . reqType === 'blocks' ) { fields = `
      pageInfo {
        hasNextPage
      }
      edges { 
        cursor
        node { 
          ${ fields }
        } 
      }` ; } if ( ! this . reqType || ! params ) { throw new Error ( 'Invalid options. You need to first set your options!' ) ; } return `query {
      ${ this . reqType }(
        ${ params }
      ){
        ${ fields }
      }
    }` . replace ( /(\r\n|\n|\r)/gm , '' ) ; } private validateIncludes ( ) { if ( this . includes . has ( 'owner' ) && ! this . includes . has ( 'owner.address' ) && ! this . includes . has ( 'owner.key' ) ) { this . includes . add ( 'owner.address' ) ; this . includes . add ( 'owner.key' ) ; } if ( this . includes . has ( 'fee' ) && ! this . includes . has ( 'fee.winston' ) && ! this . includes . has ( 'fee.ar' ) ) { this . includes . add ( 'fee.winston' ) ; this . includes . add ( 'fee.ar' ) ; } if ( this . includes . has ( 'quantity' ) && ! this . includes . has ( 'quantity.winston' ) && ! this . includes . has ( 'quantity.ar' ) ) { this . includes . add ( 'quantity.winston' ) ; this . includes . add ( 'quantity.ar' ) ; } if ( this . includes . has ( 'data' ) && ! this . includes . has ( 'data.size' ) && ! this . includes . has ( 'data.type' ) ) { this . includes . add ( 'data.size' ) ; this . includes . add ( 'data.type' ) ; } if ( this . includes . has ( 'tags' ) && ! this . includes . has ( 'tags.name' ) && ! this . includes . has ( 'tags.value' ) ) { this . includes . add ( 'tags.name' ) ; this . includes . add ( 'tags.value' ) ; } if ( this . includes . has ( 'block' ) && ! this . includes . has ( 'block.timestamp' ) && ! this . includes . has ( 'block.height' ) && ! this . includes . has ( 'block.previous' ) ) { this . includes . add ( 'block.id' ) ; this . includes . add ( 'block.timestamp' ) ; this . includes . add ( 'block.height' ) ; this . includes . add ( 'block.previous' ) ; } if ( ! this . includes . has ( 'owner' ) && ( this . includes . has ( 'owner.address' ) || this . includes . has ( 'owner.key' ) ) ) { this . includes . add ( 'owner' ) ; } if ( ! this . includes . has ( 'fee' ) && ( this . includes . has ( 'fee.winston' ) || this . includes . has ( 'fee.ar' ) ) ) { this . includes . add ( 'fee' ) ; } if ( ! this . includes . has ( 'quantity' ) && ( this . includes . has ( 'quantity.winston' ) || this . includes . has ( 'quantity.ar' ) ) ) { this . includes . add ( 'quantity' ) ; } if ( ! this . includes . has ( 'data' ) && ( this . includes . has ( 'data.size' ) || this . includes . has ( 'data.type' ) ) ) { this . includes . add ( 'data' ) ; } if ( ! this . includes . has ( 'tags' ) && ( this . includes . has ( 'tags.name' ) || this . includes . has ( 'tags.value' ) ) ) { this . includes . add ( 'tags' ) ; } if ( ! this . includes . has ( 'block' ) && ( this . includes . has ( 'block.timestamp' ) || this . includes . has ( 'block.height' ) || this . includes . has ( 'block.previous' ) ) ) { this . includes . add ( 'block' ) ; } } }