@@ -1,7 +1,7 @@
< template >
< div >
< div class = "text-h5 text-bold q-py-md q-px-sm full-width flex row justify-start" >
< h1 > Nostr Relay Registry < / h1 >
< h1 > Nostr Relay Status < sup > alpha < / sup > < / h1 >
< span > Next ping in { { nextPing } } seconds < / span > |
< span v-if = "relays.filter((url) => status[url] && !status[url].complete).length > 0" > Processing {{ relays.filter ( ( url ) = > status [ url ] . complete ) . length } } / { { relays . length } } < / span >
< / div >
@@ -10,39 +10,40 @@
< column :xs = "12" :md = "12" :lg = "6" >
< div >
< h2 > < span class = "indicator badge readwrite" > { { query ( 'public' ) . length } } < / span > Public < / h2 >
< table class = "online" v-if = "query('public').length > 0" >
< table class = "online public " v-if = "query('public').length > 0" >
< tr >
< th > < / th >
< th > < / th >
< th > ℹ ️ < / th >
< th > 🔌 < / th >
< th > 👁 ️ 🗨 ️ < / th >
< th > ✏ ️ < / th >
< th > < span class = "verified-shape-wrapper" > < span class = "shape verified" > < / span > < / span > < / th >
< th > 🌎 < / th >
<!-- < td > wl < / td >
< td > nip - 05 > < td > -- >
< th > ⌛ ️ < / th >
< th > ℹ ️ < / th >
< th > NIP - 15 < / th >
< th > NIP - 20 < / th >
< th > FILTER : LIMIT < / th >
< / tr >
< tr v-for = "relay in query('public')" :key="{relay}" :class="getLoadingClass(relay)" >
< td : key = "generateKey(relay, 'aggregate')" > < span :class = "getAggregateStatusClass(relay)" > < / span > < / td >
< td class = "left-align relay-url" @click ="copy(relay)" > {{ relay }} < / td >
< td >
< ul v-if = "Object.keys(status[relay].notes).length" >
< li v -tooltip :left.tooltip = "key" v-for = "(message, key) in status[relay].notes" :key="generateKey(relay, key)" > ✉ ️ < / li >
< / ul >
< / td >
< td : key = "generateKey(relay, 'didConnect')" > < span : class = "getStatusClass(relay, 'didConnect')" > < / span > < / td >
< td : key = "generateKey(relay, 'didRead')" > < span : class = "getStatusClass(relay, 'didRead')" > < / span > < / td >
< td : key = "generateKey(relay, 'didWrite')" > < span : class = "getStatusClass(relay, 'didWrite')" > < / span > < / td >
< td > < / td >
< td > { { status [ relay ] . flag } } < / td >
< td > < span v-if = "status[relay].didConnect">{{ status[relay].latency }}<span v-if="status[relay].latency" > ms < / span > < / span > < / td >
< td >
< Popper v-if = "Object.keys(status[relay].messages).length" >
{{ status [ relay ].type }}
< button @mouseover ="showPopper" > log < / button >
< template # content >
< ul >
< li v-for = "(message, key) in status[relay].messages" :key="generateKey(relay, key)" > {{ key }} < / li >
< / ul >
< / template >
< / Popper >
< / td >
< td > { { setCheck ( status [ relay ] . didNip15 ) } } < / td >
< td > { { setCheck ( status [ relay ] . didNip20 ) } } < / td >
< td > { { setCaution ( status [ relay ]. didSubscribeFilterLimit ) } } < / td >
< / tr >
< / table >
< / div >
@@ -71,14 +72,9 @@
< td > { { status [ relay ] . flag } } < / td >
< td > < span v-if = "status[relay].didConnect">{{ status[relay].latency }}<span v-if="status[relay].latency" > ms < / span > < / span > < / td >
< td >
< Popper v-if = "Object.keys(status[relay].messag es).length" >
< button @mouseover ="showPopper" > log < / button >
< template # content >
< ul >
< li v-for = "(message, key) in status[relay].messages" :key="generateKey(relay, key)" > {{ key }} < / li >
< / ul >
< / template >
< / Popper >
< ul v-if = "Object.keys(status[relay].not es).length" >
< li v -tooltip :left.tooltip = "key" v-for = "(message, key) in status[relay].notes" :key="generateKey(relay, key)" > ✉ ️ < / li >
< / ul >
< / td >
< / tr >
< / table >
@@ -91,7 +87,7 @@
< th > 🔌 < / th >
< th > 👁 ️ 🗨 ️ < / th >
< th > ✏ ️ < / th >
< th > msg < / th >
< th > ℹ ️ < / th >
< / tr >
< tr v-for = "relay in query('offline')" :key="{relay}" :class="getLoadingClass(relay)" >
< td : key = "generateKey(relay, 'aggregate')" > < span :class = "getAggregateStatusClass(relay)" > < / span > < / td >
@@ -100,14 +96,9 @@
< td : key = "generateKey(relay, 'didRead')" > < span : class = "getStatusClass(relay, 'didRead')" > < / span > < / td >
< td : key = "generateKey(relay, 'didWrite')" > < span : class = "getStatusClass(relay, 'didWrite')" > < / span > < / td >
< td >
< Popper v-if = "Object.keys(status[relay].messag es).length" >
< button > log < / button >
< template # content >
< ul >
< li v-for = "(message, key) in status[relay].messages" :key="generateKey(relay, key)" > {{ key }} < / li >
< / ul >
< / template >
< / Popper >
< ul v-if = "Object.keys(status[relay].not es).length" >
< li v -tooltip :left.tooltip = "key" v-for = "(message, key) in status[relay].notes" :key="generateKey(relay, key)" > ✉ ️ < / li >
< / ul >
< / td >
< / tr >
< / table >
@@ -116,7 +107,7 @@
< / row >
< ! - - < h2 > Processing < / h2 >
< table v-if = "relays.filter((url) => !status[url].complete).length > 0" >
< table v-if = "relays.filter((url) => status[url] && !status[url].complete).length > 0" >
< tr >
< th > < / th >
< / tr >
@@ -125,12 +116,20 @@
< / tr >
< / table >
< a href = "./relays/" > JSON API < / a > -- >
< a class = "credit" href = "http://sandwich.farm" > Another 🥪 by sandwich . farm < / a >
< / div >
< / template >
< script >
/* eslint-disable */
import { defineComponent } from 'vue'
import TooltipComponent from './TooltipComponent.vue'
// import nip04 from 'nostr-tools/nip04'
// import nip05 from '../../node_modules/nostr-tools/nip05'
// import nip06 from 'nostr-tools/nip06'
// import { relayConnect } from 'nostr-tools/relay'
import { RelayPool , Relay } from 'nostr'
@@ -152,7 +151,7 @@ export default defineComponent({
components : {
Row ,
Column ,
Popper
TooltipComponent
} ,
data ( ) {
@@ -164,11 +163,156 @@ export default defineComponent({
connections : { } ,
latency : { } ,
pool : null ,
timeouts : { } ,
nips : { } ,
}
} ,
mounted ( ) {
this . relays . forEach ( async url => {
this . status [ url ] = { }
await this . testRelay ( url )
} )
let latencyIntVal
let counterIntVal
// eslint-disable-next-line
let latencyTimeout = setTimeout ( ( ) => {
this . testRelayLatency ( )
// eslint-disable-next-line
latencyIntVal = setInterval ( ( ) => { this . testRelayLatency ( ) } , refreshMillis )
// eslint-disable-next-line
latencyIntVal = setInterval ( ( ) => { this . nextPing = Math . round ( ( this . lastPing + refreshMillis - Date . now ( ) ) / 1000 ) } , 1000 )
} , 10000 )
} ,
methods : {
hardFail ( url ) {
if ( ! this . status [ url ] ) this . status [ url ] = { }
this . status [ url ] . didConnect = false
this . status [ url ] . didRead = false
this . status [ url ] . didWrite = false
this . tryComplete ( url )
if ( this . connections [ url ] . close ) this . connections [ url ] . close ( )
} ,
async testRelay ( url ) {
this . lastPing = Date . now ( )
this . latency [ url ] = { }
this . timeouts [ url ] = { }
this . status [ url ] . notes = { }
this . status [ url ] . state = 'pending'
this . nips [ url ] = new Array ( 99 ) . fill ( null ) ;
this . status [ url ] . readEventCount = 0
this . status [ url ] . writeEventCount = 0
this . status [ url ] . latencyEventCount = 0
this . status [ url ] . didNip15 = false
this . status [ url ] . didNip20 = false
this . timeouts [ url ] . didConnect = setTimeout ( ( ) => {
console . log ( url , "TIMEOUT" )
if ( Object . keys ( this . status [ url ] . notes ) . length == 0 ) this . status [ url ] . notes [ 'Reason: Timeout' ] = { }
this . hardFail ( url )
} , 20000 )
let relay = Relay ( url , { reconnect : false } )
relay . on ( 'open' , e => {
console . log ( url , "OPEN" )
clearTimeout ( this . timeouts [ url ] . didConnect )
this . status [ url ] . didConnect = true
this . testRead ( url , "testRead" )
this . testWrite ( url , "testWrite" )
this . tryComplete ( url )
console . log ( url , "did connect" , this . status [ url ] . didConnect )
} )
relay . on ( 'eose' , e => {
//console.log('EOSE', e)
// relay.close()
this . tryComplete ( url )
this . status [ url ] . didNip15 = true
} )
relay . on ( 'error' , ( e ) => {
//console.log('ERROR', e)
clearTimeout ( this . timeouts [ url ] . didConnect )
this . status [ url ] . notes [ 'Reason: Error' ] = { }
this . hardFail ( url )
} )
relay . on ( 'ok' , ( ) => {
this . status [ url ] . didNip20 = true
} )
relay . on ( 'close' , ( e ) => {
console . log ( 'close' , e )
// console.dir(arguments)
} )
relay . on ( 'other' , ( e ) => {
// console.log('OTHER!!!!', e)
} )
relay . on ( 'event' , ( sub _id , ev ) => {
// console.log('event', sub_id, ev)
if ( sub _id == this . getID ( url , "testRead" ) ) {
// console.log("SUCCESS:", "READ")
this . status [ url ] . readEventCount ++
this . status [ url ] . didRead = true
clearTimeout ( this . timeouts [ url ] . testRead )
this . connections [ url ] . unsubscribe ( sub _id )
// this.tryComplete(url)
this . tryComplete ( url )
}
if ( sub _id == this . getID ( url , "testWrite" ) ) {
if ( this . status [ url ] . writeEventCount < 1 ) {
// console.log("SUCCESS:", "WRITE")
this . status [ url ] . didWrite = true
console . log ( ev )
// this.tryComplete(url)
//console.log(url, this.timeouts[url].testWrite)
clearTimeout ( this . timeouts [ url ] . testWrite )
this . tryComplete ( url )
}
this . status [ url ] . writeEventCount ++
}
if ( sub _id == this . getID ( url , "testLatency" ) ) {
if ( this . status [ url ] . latencyEventCount < 1 ) {
console . log ( url , "SUCCESS:" , "test latency" )
clearTimeout ( this . timeouts [ url ] . testLatency )
console . log ( this . latency [ url ] . start , this . latency [ url ] . final )
this . latency [ url ] . final = Date . now ( ) - this . latency [ url ] . start
this . setLatency ( url )
}
this . status [ url ] . latencyEventCount ++
// this.tryComplete(url)
//console.log(url, this.timeouts[url].testRead)
}
// relay.unsubscribe(sub_id)
} )
relay . on ( 'message' , ( message ) => {
// console.log('message', message)
// console.dir(arguments)
} )
relay . on ( 'notice' , ( message ) => {
const hash = this . sha1 ( message )
let message _obj = RELAY _MESSAGES [ hash ]
let code _obj = RELAY _CODES [ message _obj . code ]
message _obj . type = code _obj . type
message _obj . hash = hash
this . status [ url ] . notes [ code _obj . description ] = message _obj
// this.adjustStatus(url, hash)
} )
this . connections [ url ] = relay
await this . getIP ( url )
await this . setGeo ( url )
this . setFlag ( url )
// this.setNip05(url)
} ,
// query (group, filterType) {
query ( group ) {
let unordered ,
@@ -214,10 +358,10 @@ export default defineComponent({
getStatusClass ( url , key ) {
let status = this . status ? . [ url ] ? . [ key ] === true
? 'green '
? 'success '
: this . status ? . [ url ] ? . [ key ] === false
? 'red '
: 'silver '
? 'failu re'
: 'pending '
return ` indicator ${ status } `
} ,
@@ -225,6 +369,14 @@ export default defineComponent({
return this . status ? . [ url ] ? . complete ? "relay loaded" : "relay"
} ,
// setNip05(url){
// const data = nip05(url.replace('wss://', ''))
// if(data.length) {
// this.nips[url][5] = data
// this.status[url].nip05 = true
// }
// },
setAggregateStatus ( url ) {
let aggregateTally = 0
aggregateTally += this . status ? . [ url ] ? . didConnect ? 1 : 0
@@ -249,123 +401,43 @@ export default defineComponent({
}
} ,
set Complete ( url ) {
try Complete ( url ) {
let connect = typeof this . status ? . [ url ] ? . didConnect !== 'undefined' ,
read = typeof this . status ? . [ url ] ? . didRead !== 'undefined' ,
write = typeof this . status ? . [ url ] ? . didWrite !== 'undefined'
// console.log(connect, read, write)
this . setAggregateStatus ( url )
console. log ( url , 'trying complete' , connect , read , write)
if ( connect && read && write ) {
console . log ( url , 'did complete' )
this . setAggregateStatus ( url )
this . adjustStatus ( url )
this . status [ url ] . complete = true
this . connection s[ url ] . close ( )
this . statu s[ url ] . testing = false
if ( this . status [ url ] . readEventCount > 1 ) {
this . status [ url ] . didSubscribeFilterLimit = false
} else {
this . status [ url ] . didSubscribeFilterLimit = true
}
}
} ,
generateKey ( url , key ) {
return ` ${ url } _ ${ key } `
} ,
testConnect ( url ) {
// console.log(url, "CONNECT", "TEST")
// const self = this
// this.connections[url] = Relay(url)
// console.log(this.connections[url])
// this.connections[url]
// .on('open', (e) => {
// console.log('open', e)
// self.testWrite(url)
// self.testRead(url)
// })
// .on('error', (e) => {
// console.log('error', e)
// })
//
// .on('message', (message) => {
// console.log('message', message)
//
// // console.log(url, "CONNECT", "SUCCESS")
// // const hash = this.sha1(message)
// // let message_obj = RELAY_MESSAGES[hash]
// // let code_obj = RELAY_CODES[message_obj.code]
// //
// // message_obj.type = code_obj.type
// // this.status[url].messages[message] = message_obj
// // this.adjustStatus(url, hash)
// })
// .on('event', (relay, sub_id, ev) => {
// console.log('event', relay, sub_id, ev)
// })
// .on('notice', (message) => {
// console.log('notice', message)
// })
// .on('close', (e) => {
// console.log('close', e)
// })
// // () => {},
// (message) => {
//
//
// console.log(hash)
// console.dir(message_obj)
// console.dir(code_obj)
//
//
// // console.log("RECIEVED MESSAGE!")
// // console.dir(this.status[url].messages)
// },
// () => {
// console.log(url, "CONNECT", "FAILURE")
// this.status[url].didConnect = false
// this.status[url].didRead = false
// this.status[url].didWrite = false
// this.setComplete(url)
// }
// )
// this.status[url].didConnect = true
async testRead ( url , id , benchmark ) {
const subid = this . getID ( url , id )
if ( benchmark ) this . latency [ url ] . start = Date . now ( )
if ( benchmark ) console . log ( url , subid , this . latency [ url ] . start )
this . connections [ url ] . subscribe ( subid , { limit : 1 , kinds : [ 1 ] } )
this . timeouts [ url ] [ id ] = setTimeout ( ( ) => {
if ( ! benchmark ) this . status [ url ] . didRead = false
this . tryComplete ( url )
} , 3000 )
} ,
async testRead ( url ) {
// console.log(url, "READ ", "TEST")
this . connections [ url ] . subscribe ( this . getID ( url , "read" ) , { limit : 1 , kinds : [ 1 ] } )
// console.dir(this.connections[url])
// // console.log(this.connections[url]['get status']())
// console.log(url, "READ", "TEST")
// let start
// start = Date.now();
//
//
// let {unsub} = await this.connections[url].sub(
// {
// cb: () => {
// console.log(url, "READ", "SUCCESS")
// this.status[url].didRead = true
// this.setComplete(url)
// this.latency[url].read = Date.now() - start;
// unsub()
// clearTimeout(willUnsub)
// },
// filter: {
// ids: [
// '41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0'
// ]
// }
// },
// 'nostr-registry'
// )
// let willUnsub = setTimeout(() => {
// unsub()
// console.log(url, "READ", "FAILURE")
// if(!this.status[url].maybe_public) this.status[url].didRead = false
// this.setComplete(url)
// }, 10000)
} ,
async testWrite ( url ) {
async testWrite ( url , id , benchmark ) {
// console.log(url, "WRITE ", "TEST")
const message = {
id : '41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0' ,
pubkey :
@@ -377,97 +449,39 @@ export default defineComponent({
sig : '08e6303565e9282f32bed41eee4136f45418f366c0ec489ef4f90d13de1b3b9fb45e14c74f926441f8155236fb2f6fef5b48a5c52b19298a0585a2c06afe39ed'
}
this . connections [ url ] . send ( [ "EVENT" , message ] )
this . connections [ url ] . subscribe ( this . getID ( url , "write" ) , { limit : 1 , kinds : [ 1 ] , ids : [ '41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0' ] } )
this . connections [ url ] . subscribe ( this . getID ( url , id ) , { limit : 1 , kinds : [ 1 ] , ids : [ '41ce9bc50da77dda5542f020370ecc2b056d8f2be93c1cedf1bf57efcab095b0' ] } )
this . timeouts [ url ] [ id ] = setTimeout ( ( ) => {
console . log ( url , "did write" , id , false )
if ( ! benchmark ) this . status [ url ] . didWrite = false
this . tryComplete ( url )
} , 10000 )
} ,
getID ( url , keyword ) {
return ` ${ keyword } _ ${ url } `
} ,
async testRelay ( url ) {
this . lastPing = Date . now ( )
this . latency [ url ] = { }
this . status [ url ] . messages = { }
let relay
relay = Relay ( url )
relay . on ( 'open' , e => {
// console.log(url, "OPEN")
this . status [ url ] . didConnect = true
this . testRead ( url )
this . testWrite ( url )
this . setComplete ( url )
} )
relay . on ( 'eose' , e => {
// relay.close()
} )
relay . on ( 'ok' , ( ) => {
// console.log('ok')
// console.dir(arguments)
} )
relay . on ( 'error' , ( e , b ) => {
// console.log(b, e)
this . status [ url ] . didConnect = false
} )
relay . on ( 'close' , ( e ) => {
// console.log('close', e)
// console.dir(arguments)
} )
relay . on ( 'other' , ( e ) => {
// console.log('OTHER!!!!', e)
} )
relay . on ( 'event' , ( sub _id , ev ) => {
// console.log('event', sub_id, ev)
if ( sub _id == this . getID ( url , "read" ) ) {
// console.log("SUCCESS:", "READ")
this . status [ url ] . didRead = true
this . setComplete ( url )
}
if ( sub _id == this . getID ( url , "write" ) ) {
// console.log("SUCCESS:", "WRITE")
this . status [ url ] . didWrite = true
this . setComplete ( url )
}
// relay.unsubscribe(sub_id)
} )
relay . on ( 'message' , ( message ) => {
// console.log('message', message)
// console.dir(arguments)
} )
relay . on ( 'notice' , ( message ) => {
console . log ( 'notice' , message )
// console.dir(arguments)
} )
this . connections [ url ] = relay
this . setLatency ( url )
// await this.getIP(url)
// await this.setGeo(url)
this . setFlag ( url )
} ,
isOnion ( url ) {
return onionRegex ( ) . test ( url )
} ,
setLatency ( url ) {
this . status [ url ] . latency = this . latency [ url ] . read
// console.log(this.status[url].didConnect === true, this.status[url]. latency,this.latency[url].final)
if ( this . status [ url ] . didConnect === true ) this . status [ url ] . latency = this . latency [ url ] . final
// console.log(this.status[url].didConnect === true, this.status[url]. latency,this.latency[url].final)
console . log ( "latency" , this . latency [ url ] )
} ,
testRelayLatency ( ) {
// console.log( 'testing latency')
console. log ( 'testing latency')
this . relays . forEach ( url => {
// this.testWrite(url, true)
this . testRead ( url , true )
this . setLatency ( url )
console . log ( url , 'did read' , this . status [ url ] . didRead )
if ( this. status [ url ] . didRead ) {
console . log ( url , 'testing read' )
this . testRead ( url , "testLatency" , true )
}
this . lastPing = Date . now ( )
} )
this . lastPing = Date . now ( )
} ,
async getIP ( url ) {
@@ -491,19 +505,35 @@ export default defineComponent({
this . status [ url ] . flag = this . status [ url ] . geo ? . countryCode ? countryCodeEmoji ( this . status [ url ] . geo . countryCode ) : emoji . get ( 'shrug' ) ;
} ,
adjustStatus ( url , hash ) {
let code = RELAY _MESSAGES [ hash ] . code ,
type = RELAY _CODES [ code ] . type
setCheck ( bool ) {
return bool ? '✅ ' : ''
} ,
this . status [ url ] [ type ] = code
if ( type == "maybe_public" ) {
this . status [ url ] . didWrite = true
this . status [ url ] . didRead = true
}
if ( type == "write_restricted" ) {
this . status [ url ] . didWrite = false
}
setCross ( bool ) {
return ! bool ? '❌' : ''
} ,
setCaution ( bool ) {
return ! bool ? '⚠️' : ''
} ,
adjustStatus ( url ) {
Object . entries ( this . status [ url ] . notes ) . forEach ( ( [ key , value ] ) => {
if ( ! value . hasOwnProperty ( "hash" ) ) return
let code = RELAY _MESSAGES [ value . hash ] . code ,
type = RELAY _CODES [ code ] . type
this . status [ url ] [ type ] = code
if ( type == "maybe_public" ) {
this . status [ url ] . didWrite = false
this . status [ url ] . aggregate = 'public'
}
if ( type == "write_restricted" ) {
this . status [ url ] . didWrite = false
}
} )
} ,
sha1 ( message ) {
@@ -513,29 +543,6 @@ export default defineComponent({
} ,
} ,
mounted ( ) {
this . relays . forEach ( async url => {
this . status [ url ] = { } //statusInterface
if ( this . isOnion ( url ) ) {
url = ` ${ url } .to ` //add proxy
}
await this . testRelay ( url )
} )
// // eslint-disable-next-line
// let latencyTimeout = setTimeout(() => { this.testRelayLatency() }, 10000)
//
// // eslint-disable-next-line
// let latencyIntVal = setInterval(() => { this.testRelayLatency() }, refreshMillis)
// // eslint-disable-next-line
// let counterIntVal = setInterval(() => {
// this.nextPing = Math.round((this.lastPing + refreshMillis - Date.now())/1000)
// }, 1000)
} ,
} )
< / script >
@@ -589,7 +596,8 @@ tr.relay.loaded td {
}
. badge . write - only ,
. badge . read - only {
. badge . read - only ,
table . online . public . indicator . failure {
background - color : orange ! important ;
}
@@ -599,22 +607,22 @@ tr.relay.loaded td {
border - style : solid ;
}
. indicator . silver {
. indicator . pernding {
background - color : # c0c0c0 ;
border - color : rgba ( 55 , 55 , 55 , 0.5 ) ;
}
. indicator . green {
. indicator . success {
background - color : green ;
border - color : rgba ( 0 , 255 , 0 , 0.5 ) ;
}
. indicator . red {
. indicator . failu re {
background - color : red ;
border - color : rgba ( 255 , 0 , 0 , 0.5 ) ;
}
. indicator . orange {
. indicator . caution {
background - color : orange ;
border - color : rgba ( 255 , 191 , 0 , 0.5 ) ;
}
@@ -678,4 +686,50 @@ table.online .relay-url {
cursor : pointer ;
}
. verified - shape - wrapper {
display : inline - block ;
width : 16 px ;
height : 16 px ;
}
. shape . verified {
background : blue ;
width : 16 px ;
height : 16 px ;
position : relative ;
top : 5 px ;
left : - 5 px ;
text - align : center ;
}
. shape . verified : before ,
. shape . verified : after {
content : "" ;
position : absolute ;
top : 0 ;
left : 0 ;
height : 13 px ;
width : 13 px ;
background : blue ;
}
. shape . verified : before {
transform : rotate ( 30 deg ) ;
}
. shape . verified : after {
transform : rotate ( 75 deg ) ;
}
sup {
color : # c0c0c0 ;
font - size : 15 px ;
}
. credit {
display : inline - block ;
text - decoration : none ;
color : # 333 ;
text - transform : uppercase ;
font - size : 12 px ;
margin - top : 25 px ;
}
< / style >