Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
33e63455
Unverified
Commit
33e63455
authored
Feb 28, 2023
by
mergify[bot]
Committed by
GitHub
Feb 28, 2023
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #4960 from ethereum-optimism/willc/atst-final
✨
Add atst CLI
parents
904b2c5e
8762e9a0
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
482 additions
and
0 deletions
+482
-0
logger.spec.ts.snap
packages/atst/src/__snapshots__/logger.spec.ts.snap
+11
-0
cli.ts
packages/atst/src/cli.ts
+106
-0
read.spec.ts
packages/atst/src/commands/read.spec.ts
+28
-0
read.ts
packages/atst/src/commands/read.ts
+73
-0
write.spec.ts
packages/atst/src/commands/write.spec.ts
+44
-0
write.ts
packages/atst/src/commands/write.ts
+101
-0
castAsDataType.ts
packages/atst/src/lib/castAsDataType.ts
+21
-0
logger.spec.ts
packages/atst/src/lib/logger.spec.ts
+28
-0
logger.ts
packages/atst/src/lib/logger.ts
+38
-0
watchConsole.ts
packages/atst/src/test/watchConsole.ts
+32
-0
No files found.
packages/atst/src/__snapshots__/logger.spec.ts.snap
0 → 100644
View file @
33e63455
// Vitest Snapshot v1
exports[`logger > \${level}() > logs message "error" 1`] = `"[31merror[39m"`;
exports[`logger > \${level}() > logs message "info" 1`] = `"[34minfo[39m"`;
exports[`logger > \${level}() > logs message "log" 1`] = `"[37mlog[39m"`;
exports[`logger > \${level}() > logs message "success" 1`] = `"[32msuccess[39m"`;
exports[`logger > \${level}() > logs message "warn" 1`] = `"[33mwarn[39m"`;
packages/atst/src/cli.ts
0 → 100644
View file @
33e63455
#!/usr/bin/env node
import
{
cac
}
from
'
cac
'
import
type
{
Address
}
from
'
@wagmi/core
'
import
{
readOptionsValidators
,
ReadOptions
}
from
'
./commands/read
'
import
*
as
logger
from
'
./lib/logger
'
// @ts-ignore it's mad about me importing something not in tsconfig.includes
import
packageJson
from
'
../package.json
'
import
{
WriteOptions
,
writeOptionsValidators
}
from
'
./commands/write
'
const
cli
=
cac
(
'
atst
'
)
cli
.
command
(
'
read
'
,
'
read an attestation
'
)
.
option
(
'
--creator <string>
'
,
readOptionsValidators
.
creator
.
description
!
)
.
option
(
'
--about <string>
'
,
readOptionsValidators
.
about
.
description
!
)
.
option
(
'
--key <string>
'
,
readOptionsValidators
.
key
.
description
!
)
.
option
(
'
--data-type [string]
'
,
readOptionsValidators
.
dataType
.
description
!
,
{
default
:
readOptionsValidators
.
dataType
.
parse
(
undefined
),
})
.
option
(
'
--rpc-url [url]
'
,
readOptionsValidators
.
rpcUrl
.
description
!
,
{
default
:
readOptionsValidators
.
rpcUrl
.
parse
(
undefined
),
})
.
option
(
'
--contract [address]
'
,
readOptionsValidators
.
contract
.
description
!
,
{
default
:
readOptionsValidators
.
contract
.
parse
(
undefined
),
})
.
example
(
()
=>
`atst read --key "optimist.base-uri" --about 0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5 --creator 0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3`
)
.
action
(
async
(
options
:
ReadOptions
)
=>
{
const
{
read
}
=
await
import
(
'
./commands/read
'
)
// TODO use the native api to do this instead of parsing the raw args
// by default options parses addresses as numbers without precision
// we should use the args parsing library to do this directly
// but for now I didn't bother to figure out how to do that
const
{
rawArgs
}
=
cli
const
about
=
rawArgs
[
rawArgs
.
indexOf
(
'
--about
'
)
+
1
]
as
Address
const
creator
=
rawArgs
[
rawArgs
.
indexOf
(
'
--creator
'
)
+
1
]
as
Address
const
contract
=
rawArgs
.
includes
(
'
--contract
'
)
?
(
rawArgs
[
rawArgs
.
indexOf
(
'
--contract
'
)
+
1
]
as
Address
)
:
options
.
contract
await
read
({
...
options
,
about
,
creator
,
contract
})
})
cli
.
command
(
'
write
'
,
'
write an attestation
'
)
.
option
(
'
--private-key <string>
'
,
writeOptionsValidators
.
privateKey
.
description
!
)
.
option
(
'
--data-type [string]
'
,
readOptionsValidators
.
dataType
.
description
!
,
{
default
:
writeOptionsValidators
.
dataType
.
parse
(
undefined
),
})
.
option
(
'
--about <string>
'
,
writeOptionsValidators
.
about
.
description
!
)
.
option
(
'
--key <string>
'
,
writeOptionsValidators
.
key
.
description
!
)
.
option
(
'
--value <string>
'
,
writeOptionsValidators
.
value
.
description
!
)
.
option
(
'
--rpc-url [url]
'
,
writeOptionsValidators
.
rpcUrl
.
description
!
,
{
default
:
writeOptionsValidators
.
rpcUrl
.
parse
(
undefined
),
})
.
option
(
'
--contract [address]
'
,
writeOptionsValidators
.
contract
.
description
!
,
{
default
:
writeOptionsValidators
.
contract
.
parse
(
undefined
),
}
)
.
example
(
()
=>
`atst write --key "optimist.base-uri" --about 0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5 --value "my attestation" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --rpc-url http://localhost:8545`
)
.
action
(
async
(
options
:
WriteOptions
)
=>
{
const
{
write
}
=
await
import
(
'
./commands/write
'
)
// TODO use the native api to do this instead of parsing the raw args
// by default options parses addresses as numbers without precision
// we should use the args parsing library to do this directly
// but for now I didn't bother to figure out how to do that
const
{
rawArgs
}
=
cli
const
privateKey
=
rawArgs
[
rawArgs
.
indexOf
(
'
--private-key
'
)
+
1
]
as
Address
const
about
=
rawArgs
[
rawArgs
.
indexOf
(
'
--about
'
)
+
1
]
as
Address
const
contract
=
rawArgs
.
includes
(
'
--contract
'
)
?
(
rawArgs
[
rawArgs
.
indexOf
(
'
--contract
'
)
+
1
]
as
Address
)
:
options
.
contract
await
write
({
...
options
,
about
,
privateKey
,
contract
})
})
cli
.
help
()
cli
.
version
(
packageJson
.
version
)
void
(
async
()
=>
{
try
{
// Parse CLI args without running command
cli
.
parse
(
process
.
argv
,
{
run
:
false
})
if
(
!
cli
.
matchedCommand
&&
cli
.
args
.
length
===
0
)
{
cli
.
outputHelp
()
}
await
cli
.
runMatchedCommand
()
}
catch
(
error
)
{
logger
.
error
(
`\n
${(
error
as
Error
).
message
}
`
)
process
.
exit
(
1
)
}
})()
packages/atst/src/commands/read.spec.ts
0 → 100644
View file @
33e63455
import
{
describe
,
expect
,
it
}
from
'
vitest
'
import
{
ATTESTATION_STATION_ADDRESS
}
from
'
../constants/attestationStationAddress
'
import
{
watchConsole
}
from
'
../test/watchConsole
'
import
{
read
}
from
'
./read
'
describe
(
`cli:
${
read
.
name
}
`
,
()
=>
{
it
(
'
should read attestation
'
,
async
()
=>
{
const
creator
=
'
0x60c5C9c98bcBd0b0F2fD89B24c16e533BaA8CdA3
'
const
about
=
'
0x2335022c740d17c2837f9C884Bfe4fFdbf0A95D5
'
const
key
=
'
optimist.base-uri
'
const
dataType
=
'
string
'
const
consoleUtil
=
watchConsole
()
await
read
({
creator
,
about
,
key
,
dataType
,
contract
:
ATTESTATION_STATION_ADDRESS
,
rpcUrl
:
'
http://localhost:8545
'
,
})
expect
(
consoleUtil
.
formatted
).
toMatchInlineSnapshot
(
'
"[37mhttps://assets.optimism.io/4a609661-6774-441f-9fdb-453fdbb89931-bucket/optimist-nft/attributes[39m"
'
)
})
})
packages/atst/src/commands/read.ts
0 → 100644
View file @
33e63455
import
{
Address
,
createClient
}
from
'
@wagmi/core
'
import
{
isAddress
}
from
'
ethers/lib/utils.js
'
import
{
z
}
from
'
zod
'
import
{
providers
}
from
'
ethers
'
import
*
as
logger
from
'
../lib/logger
'
import
{
dataTypeOptionValidator
}
from
'
../types/DataTypeOption
'
import
type
{
WagmiBytes
}
from
'
../types/WagmiBytes
'
import
{
ATTESTATION_STATION_ADDRESS
}
from
'
../constants/attestationStationAddress
'
import
{
DEFAULT_RPC_URL
}
from
'
../constants/defaultRpcUrl
'
import
{
readAttestation
}
from
'
../lib/readAttestation
'
const
zodAddress
=
()
=>
z
.
string
()
.
transform
((
addr
)
=>
addr
as
Address
)
.
refine
(
isAddress
,
{
message
:
'
Invalid address
'
})
export
const
readOptionsValidators
=
{
creator
:
zodAddress
().
describe
(
'
Address of the creator of the attestation
'
),
about
:
zodAddress
().
describe
(
'
Address of the subject of the attestation
'
),
key
:
z
.
string
()
.
describe
(
'
Key of the attestation either as string or hex number
'
),
dataType
:
dataTypeOptionValidator
,
rpcUrl
:
z
.
string
()
.
url
()
.
optional
()
.
default
(
DEFAULT_RPC_URL
)
.
describe
(
'
Rpc url to use
'
),
contract
:
zodAddress
()
.
optional
()
.
default
(
ATTESTATION_STATION_ADDRESS
)
.
describe
(
'
Contract address to read from
'
),
}
const
validators
=
z
.
object
(
readOptionsValidators
)
export
type
ReadOptions
=
z
.
infer
<
typeof
validators
>
export
const
read
=
async
(
options
:
ReadOptions
)
=>
{
// TODO make these errors more user friendly
const
parsedOptions
=
await
validators
.
parseAsync
(
options
).
catch
((
e
)
=>
{
logger
.
error
(
e
)
process
.
exit
(
1
)
})
const
provider
=
new
providers
.
JsonRpcProvider
({
url
:
parsedOptions
.
rpcUrl
,
headers
:
{
'
User-Agent
'
:
'
@eth-optimism/atst
'
,
},
})
createClient
({
provider
,
})
try
{
const
result
=
await
readAttestation
(
parsedOptions
.
creator
,
parsedOptions
.
about
,
parsedOptions
.
key
as
WagmiBytes
,
parsedOptions
.
dataType
,
parsedOptions
.
contract
)
logger
.
log
(
result
?.
toString
())
return
result
?.
toString
()
}
catch
(
e
)
{
logger
.
error
(
'
Unable to read attestation
'
,
e
)
process
.
exit
(
1
)
}
}
packages/atst/src/commands/write.spec.ts
0 → 100644
View file @
33e63455
import
{
Address
}
from
'
@wagmi/core
'
import
{
Wallet
}
from
'
ethers
'
import
{
describe
,
expect
,
it
}
from
'
vitest
'
import
{
ATTESTATION_STATION_ADDRESS
}
from
'
../constants/attestationStationAddress
'
import
{
read
}
from
'
./read
'
import
{
write
}
from
'
./write
'
describe
(
`cli:
${
write
.
name
}
`
,
()
=>
{
it
(
'
should write attestation
'
,
async
()
=>
{
// Anvil account[0]
const
privateKey
=
'
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
'
const
publicKey
=
'
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
'
const
about
=
Wallet
.
createRandom
().
address
as
Address
const
key
=
'
key
'
const
value
=
'
value
'
const
rpcUrl
=
'
http://localhost:8545
'
const
txHash
=
await
write
({
privateKey
,
about
,
key
,
value
,
contract
:
ATTESTATION_STATION_ADDRESS
,
rpcUrl
,
})
expect
(
txHash
.
startsWith
(
'
0x
'
)).
toBe
(
true
)
// check that attestation was written
const
attestation
=
await
read
({
creator
:
publicKey
,
about
,
key
,
dataType
:
'
string
'
,
contract
:
ATTESTATION_STATION_ADDRESS
,
rpcUrl
,
})
expect
(
attestation
).
toBe
(
value
)
})
})
packages/atst/src/commands/write.ts
0 → 100644
View file @
33e63455
import
{
Address
,
connect
,
createClient
}
from
'
@wagmi/core
'
import
{
isAddress
}
from
'
ethers/lib/utils.js
'
import
{
z
}
from
'
zod
'
import
{
providers
,
Wallet
}
from
'
ethers
'
import
{
MockConnector
}
from
'
@wagmi/core/connectors/mock
'
import
*
as
logger
from
'
../lib/logger
'
import
{
ATTESTATION_STATION_ADDRESS
}
from
'
../constants/attestationStationAddress
'
import
{
DEFAULT_RPC_URL
}
from
'
../constants/defaultRpcUrl
'
import
{
prepareWriteAttestation
}
from
'
../lib/prepareWriteAttestation
'
import
{
writeAttestation
}
from
'
../lib/writeAttestation
'
import
{
castAsDataType
}
from
'
../lib/castAsDataType
'
import
{
dataTypeOptionValidator
}
from
'
../types/DataTypeOption
'
const
zodAddress
=
()
=>
z
.
string
()
.
transform
((
addr
)
=>
addr
as
Address
)
.
refine
(
isAddress
,
{
message
:
'
Invalid address
'
})
const
zodWallet
=
()
=>
z
.
string
().
refine
((
key
)
=>
new
Wallet
(
key
))
const
zodAttestation
=
()
=>
z
.
union
([
z
.
string
(),
z
.
number
(),
z
.
boolean
()])
export
const
writeOptionsValidators
=
{
privateKey
:
zodWallet
().
describe
(
'
Address of the creator of the attestation
'
),
about
:
zodAddress
().
describe
(
'
Address of the subject of the attestation
'
),
key
:
z
.
string
()
.
describe
(
'
Key of the attestation either as string or hex number
'
),
value
:
zodAttestation
().
describe
(
'
Attestation value
'
).
default
(
''
),
dataType
:
dataTypeOptionValidator
,
rpcUrl
:
z
.
string
()
.
url
()
.
optional
()
.
default
(
DEFAULT_RPC_URL
)
.
describe
(
'
Rpc url to use
'
),
contract
:
zodAddress
()
.
optional
()
.
default
(
ATTESTATION_STATION_ADDRESS
)
.
describe
(
'
Contract address to read from
'
),
}
const
validators
=
z
.
object
(
writeOptionsValidators
)
export
type
WriteOptions
=
z
.
infer
<
typeof
validators
>
export
const
write
=
async
(
options
:
WriteOptions
)
=>
{
// TODO make these errors more user friendly
const
parsedOptions
=
await
validators
.
parseAsync
(
options
).
catch
((
e
)
=>
{
logger
.
error
(
e
)
process
.
exit
(
1
)
})
const
provider
=
new
providers
.
JsonRpcProvider
({
url
:
parsedOptions
.
rpcUrl
,
headers
:
{
'
User-Agent
'
:
'
@eth-optimism/atst
'
,
},
})
createClient
({
provider
,
})
const
network
=
await
provider
.
getNetwork
()
if
(
!
network
)
{
logger
.
error
(
'
Unable to detect chainId
'
)
process
.
exit
(
1
)
}
await
connect
({
// MockConnector is actually a vanilla connector
// it's called mockConnector because normally they
// expect us to connect with metamask or something
// but we're just using a private key
connector
:
new
MockConnector
({
options
:
{
chainId
:
network
.
chainId
,
signer
:
new
Wallet
(
parsedOptions
.
privateKey
,
provider
),
},
}),
})
try
{
const
preparedTx
=
await
prepareWriteAttestation
(
parsedOptions
.
about
,
parsedOptions
.
key
,
castAsDataType
(
parsedOptions
.
value
,
parsedOptions
.
dataType
),
network
.
chainId
)
const
result
=
await
writeAttestation
(
preparedTx
)
await
result
.
wait
()
logger
.
log
(
`txHash:
${
result
.
hash
}
`
)
return
result
.
hash
}
catch
(
e
)
{
logger
.
error
(
'
Unable to read attestation
'
,
e
)
process
.
exit
(
1
)
}
}
packages/atst/src/lib/castAsDataType.ts
0 → 100644
View file @
33e63455
import
{
DataTypeOption
}
from
'
../types/DataTypeOption
'
/**
* @internal
* Takes a datatype and returns the value casted to that type
*/
export
const
castAsDataType
=
(
value
:
any
,
dataType
:
DataTypeOption
)
=>
{
if
(
dataType
===
'
string
'
)
{
return
value
}
else
if
(
dataType
===
'
number
'
)
{
return
Number
(
value
)
}
else
if
(
dataType
===
'
bool
'
)
{
return
Boolean
(
value
)
}
else
if
(
dataType
===
'
bytes
'
)
{
return
value
}
else
if
(
dataType
===
'
address
'
)
{
return
value
}
else
{
throw
new
Error
(
`Unrecognized data type
${
dataType
satisfies
never
}
`
)
}
}
packages/atst/src/lib/logger.spec.ts
0 → 100644
View file @
33e63455
import
{
afterEach
,
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
import
*
as
logger
from
'
./logger
'
import
{
watchConsole
}
from
'
../test/watchConsole
'
describe
(
'
logger
'
,
()
=>
{
afterEach
(()
=>
{
vi
.
restoreAllMocks
()
})
describe
.
each
([
{
level
:
'
success
'
},
{
level
:
'
info
'
},
{
level
:
'
log
'
},
{
level
:
'
warn
'
},
{
level
:
'
error
'
},
// eslint-disable-next-line no-template-curly-in-string
])(
'
${level}()
'
,
({
level
})
=>
{
it
(
`logs message "
${
level
}
"`
,
()
=>
{
const
spy
=
vi
.
spyOn
(
logger
,
level
as
'
info
'
)
const
consoleUtil
=
watchConsole
()
const
loggerFn
=
logger
[
level
]
loggerFn
(
level
)
expect
(
spy
).
toHaveBeenCalledWith
(
level
)
expect
(
consoleUtil
.
formatted
).
toMatchSnapshot
()
})
})
})
packages/atst/src/lib/logger.ts
0 → 100644
View file @
33e63455
import
util
from
'
util
'
import
ora
from
'
ora
'
import
pc
from
'
picocolors
'
const
format
=
(
args
:
any
[])
=>
{
return
util
.
format
(...
args
)
.
split
(
'
\n
'
)
.
join
(
'
\n
'
)
}
export
const
success
=
(...
args
:
Array
<
any
>
)
=>
{
console
.
log
(
pc
.
green
(
format
(
args
)))
}
export
const
info
=
(...
args
:
Array
<
any
>
)
=>
{
console
.
info
(
pc
.
blue
(
format
(
args
)))
}
export
const
log
=
(...
args
:
Array
<
any
>
)
=>
{
console
.
log
(
pc
.
white
(
format
(
args
)))
}
export
const
warn
=
(...
args
:
Array
<
any
>
)
=>
{
console
.
warn
(
pc
.
yellow
(
format
(
args
)))
}
export
const
error
=
(...
args
:
Array
<
any
>
)
=>
{
console
.
error
(
pc
.
red
(
format
(
args
)))
}
export
const
spinner
=
()
=>
{
return
ora
({
color
:
'
gray
'
,
spinner
:
'
dots8Bit
'
,
})
}
packages/atst/src/test/watchConsole.ts
0 → 100644
View file @
33e63455
import
{
vi
}
from
'
vitest
'
/**
* A test util for watching console output
*/
export
const
watchConsole
=
()
=>
{
type
Console
=
'
info
'
|
'
log
'
|
'
warn
'
|
'
error
'
const
output
:
{
[
_
in
Console
|
'
all
'
]:
string
[]
}
=
{
info
:
[],
log
:
[],
warn
:
[],
error
:
[],
all
:
[],
}
const
handleOutput
=
(
method
:
Console
)
=>
{
return
(
message
:
string
)
=>
{
output
[
method
].
push
(
message
)
output
.
all
.
push
(
message
)
}
}
return
{
debug
:
console
.
debug
,
info
:
vi
.
spyOn
(
console
,
'
info
'
).
mockImplementation
(
handleOutput
(
'
info
'
)),
log
:
vi
.
spyOn
(
console
,
'
log
'
).
mockImplementation
(
handleOutput
(
'
log
'
)),
warn
:
vi
.
spyOn
(
console
,
'
warn
'
).
mockImplementation
(
handleOutput
(
'
warn
'
)),
error
:
vi
.
spyOn
(
console
,
'
error
'
).
mockImplementation
(
handleOutput
(
'
error
'
)),
output
,
get
formatted
()
{
return
output
.
all
.
join
(
'
\n
'
)
},
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment