@witnet/ethers
Version:
Wit/Oracle SDK Framework package for Solidity projects
1,148 lines (1,056 loc) • 45 kB
JavaScript
require("dotenv").config()
const moment = require("moment")
const { execSync, spawn } = require("node:child_process")
const os = require("os")
const prompt = require("inquirer").createPromptModule()
const { WrappedWIT } = require("witnet-wrapped-wit")
const { Witnet } = require("@witnet/sdk")
const { ethers, utils, KermitClient } = require("../../dist/src/lib")
const helpers = require("./helpers")
const { green, yellow, lwhite } = helpers.colors
/// CONSTANTS AND GLOBALS =============================================================================================
const settings = {
flags: {
help: "Show usage information for a specific command.",
verbose: "Outputs detailed information.",
version: "Print the CLI name and version.",
check: "See if cross-chain transactions have been consolidated.",
mints: "Include mint transactions, if any.",
burns: "Include burn transactions, if any.",
testnets: "List supported EVM testnets, instead of mainnets.",
},
options: {
offset: {
hint: "Skip the first output records when using --fromBlock (default: 0)",
param: "OFFSET",
},
limit: {
hint: "Limit number of output records (default: 64).",
param: "LIMIT",
},
since: {
hint: "Process events starting from the given EVM block number.",
param: "EVM_BLOCK",
},
from: {
hint: "Filter events by sender address.",
param: "EVM|WIT_ADDRESS",
},
into: {
hint: "Filter events by recipient address (or recipient of --value, when specified).",
param: "EVM|WIT_ADDRESS",
},
"vtt-hash": {
hint: "The hash of some finalized wrap transfer transaction in Witnet, pending to be verified.",
param: "WIT_VTT_HASH",
},
value: {
hint: "Send this amount of wrapped Wits to the specified recipient (requires --into).",
param: "WIT_COINS",
},
gasPrice: {
hint: "Specify the EVM transaction gas price to pay for.",
param: "EVM_GAS_PRICE",
},
port: {
hint: "Port on which the local ETH/RPC signing gateway is expected to be listening (default: 8545).",
param: "HTTP_PORT",
},
witnet: {
hint: "Wit/Oracle RPC provider to connect to, other than default.",
param: "URL",
},
kermit: {
hint: "Wit/Kermit REST-API provider to connect to, other than default.",
param: "URL",
},
},
envars: {
ETHRPC_PRIVATE_KEYS: "=> Private keys used by the ETH/RPC gateway for signing EVM transactions.",
WITNET_KERMIT_PROVIDER_URL: "=> Wit/Kermit REST-API provider to connect to, if no otherwise specified.",
WITNET_SDK_PROVIDER_URL: "=> Wit/Oracle RPC provider(s) to connect to, if no otherwise specified.",
WITNET_SDK_WALLET_MASTER_KEY: "=> Wallet's master key in XPRV format, as exported from either a node, Sheikah or myWitWallet.",
},
}
/// MAIN WORKFLOW =====================================================================================================
main()
async function main() {
let ethRpcPort = 8545
if (process.argv.indexOf("--port") >= 0) {
ethRpcPort = parseInt(process.argv[process.argv.indexOf("--port") + 1])
}
let ethRpcProvider, ethRpcNetwork, ethRpcError
try {
ethRpcProvider = new ethers.JsonRpcProvider(`http://127.0.0.1:${ethRpcPort}`)
ethRpcNetwork = utils.getEvmNetworkByChainId((await ethRpcProvider.getNetwork()).chainId)
} catch (err) {
ethRpcError = err
}
const router = {
...(WrappedWIT.isNetworkSupported(ethRpcNetwork)
? {
accounts: {
hint: `Show EVM native and Wrapped/WIT balances for all available signing accounts on ${helpers.colors.mcyan(ethRpcNetwork.toUpperCase())
}.`,
options: [
"port",
],
envars: [],
},
contract: {
hint: `Show Wrapped/WIT contract address on ${helpers.colors.mcyan(ethRpcNetwork.toUpperCase())}.`,
flags: [
"verbose",
],
},
supplies: {
hint: `Show relevant token-related supplies on ${helpers.colors.mcyan(ethRpcNetwork.toUpperCase())}.`,
options: [
"kermit",
"witnet",
"port",
...(WrappedWIT.isNetworkCanonical(ethRpcNetwork)
? ["witnet"]
: []
),
"from",
"gasPrice",
],
envars: [
...(WrappedWIT.isNetworkCanonical(ethRpcNetwork)
? ["WITNET_KERMIT_PROVIDER_URL", "WITNET_SDK_PROVIDER_URL", "WITNET_SDK_WALLET_MASTER_KEY"]
: []
),
],
},
transfers: {
hint: `Show transfers of Wrapped/WIT tokens on ${helpers.colors.mcyan(ethRpcNetwork.toUpperCase())}.`,
flags: [
"burns",
"mints",
],
options: [
"port",
"limit",
"offset",
"since",
"from",
"into",
"value",
"gasPrice",
],
},
}
: {}),
...(WrappedWIT.isNetworkCanonical(ethRpcNetwork)
? {
wrappings: {
hint: `Show wrapping transactions into ${helpers.colors.mcyan(ethRpcNetwork.toUpperCase())}.`,
flags: [
"check",
],
options: [
"witnet",
"port",
"limit",
"offset",
"since",
"from",
"into",
"value",
"vtt-hash",
"gasPrice",
],
envars: [
"WITNET_SDK_PROVIDER_URL",
"WITNET_SDK_WALLET_MASTER_KEY",
],
},
unwrappings: {
hint: `Show unwrapping transactions from ${helpers.colors.mcyan(ethRpcNetwork.toUpperCase())}.`,
flags: [
"check",
],
options: [
"witnet",
"port",
"limit",
"offset",
"since",
"from",
"into",
"value",
"gasPrice",
],
envars: [
"WITNET_SDK_PROVIDER_URL",
],
},
}
: {}),
gateway: {
hint: "Launch a local ETH/RPC signing gateway connected to some specific EVM network.",
params: ["EVM_NETWORK"],
options: [
"port",
],
envars: [
"ETHRPC_PRIVATE_KEYS",
"ETHRPC_PROVIDER_URL",
],
},
networks: {
hint: "List EVM networks where the Wrapped/WIT token is available.",
params: "[EVM_ECOSYSTEM]",
flags: [
"testnets",
],
},
commands: {
accounts: balance,
contract,
gateway,
networks,
supplies,
transfers,
unwrappings,
wrappings,
},
}
let [args, flags] = helpers.extractFlagsFromArgs(process.argv.slice(2), Object.keys(settings.flags))
if (flags.version) {
console.info(`${helpers.colors.lwhite(`Wrapped/WIT Ethers CLI v${require("../../package.json").version}`)}`)
}
let options;[args, options] = helpers.extractOptionsFromArgs(args, Object.keys(settings.options))
if (args[0] && router.commands[args[0]] && router[args[0]]) {
const cmd = args[0]
if (flags.help) {
showCommandUsage(router, cmd, router[cmd])
} else {
try {
await router.commands[cmd]({
...settings,
...flags,
...options,
provider: ethRpcProvider,
network: ethRpcNetwork,
}, args.slice(1))
} catch (e) {
showUsageError(router, cmd, router[cmd], e)
}
}
} else {
showMainUsage(router, [
...(!process.env.ETHRPC_PRIVATE_KEYS ? ["ETHRPC_PRIVATE_KEYS"] : []),
...(!process.env.WITNET_SDK_WALLET_MASTER_KEY ? ["WITNET_SDK_WALLET_MASTER_KEY"] : []),
...(!process.env.WITNET_SDK_PROVIDER_URL ? ["WITNET_SDK_PROVIDER_URL"] : []),
])
if (!args[0] && ethRpcError) {
console.info(helpers.colors.mred(`\nNo ETH/RPC gateway running on port ${ethRpcPort}.`))
}
}
}
function showMainUsage(router, envars) {
showUsageHeadline(router)
showUsageFlags(["help", "version"])
showUsageOptions(["port"])
console.info("\nCOMMANDS:")
const maxLength = Object.keys(router.commands).map(key => key.length).reduce((prev, curr) => curr > prev ? curr : prev)
Object.keys(router.commands).forEach(cmd => {
if (router[cmd]) console.info(" ", `${cmd}${" ".repeat(maxLength - cmd.length)}`, " ", router[cmd]?.hint)
})
showUsageEnvars(envars)
}
function showCommandUsage(router, cmd, specs) {
showUsageHeadline(router, cmd, specs)
showUsageFlags(specs?.flags || [])
showUsageOptions(specs?.options || [])
showUsageEnvars(specs?.envars || [])
}
function showUsageEnvars(envars) {
if (envars.length > 0) {
console.info("\nENVARS:")
const maxWidth = envars.map(envar => envar.length).reduce((curr, prev) => curr > prev ? curr : prev)
envars.forEach(envar => {
if (envar.toUpperCase().indexOf("KEY") < 0 && process.env[envar]) {
console.info(" ", `${yellow(envar.toUpperCase())}${" ".repeat(maxWidth - envar.length)}`, ` => Settled to "${process.env[envar]}"`)
} else {
console.info(" ", `${yellow(envar.toUpperCase())}${" ".repeat(maxWidth - envar.length)}`, ` ${settings.envars[envar]}`)
}
})
}
}
function showUsageError(router, cmd, specs, error) {
showCommandUsage(router, cmd, specs)
if (error) {
console.info()
console.info(error)
// console.error(error?.stack?.split("\n")[0] || error)
}
}
function showUsageFlags(flags) {
if (flags.length > 0) {
const maxWidth = flags.map(flag => flag.length).reduce((curr, prev) => curr > prev ? curr : prev)
console.info("\nFLAGS:")
flags.forEach(flag => {
if (settings.flags[flag]) {
console.info(` --${flag}${" ".repeat(maxWidth - flag.length)} ${settings.flags[flag]}`)
}
})
}
}
function showUsageHeadline(router, cmd, specs) {
console.info("USAGE:")
const flags = cmd && (!specs?.flags || specs.flags.length === 0) ? "" : "[FLAGS] "
const options = specs?.options && specs.options.length > 0 ? "[OPTIONS] " : ""
if (cmd) {
if (specs?.params) {
let params
const optionalize = (str) => str.endsWith(" ...]")
? `[<${str.slice(1, -5)}> ...]`
: (
str[0] === "[" ? `[<${str.slice(1, -1)}>]` : `<${str}>`
)
if (Array.isArray(specs?.params)) {
params = specs.params.map(param => optionalize(param)).join(" ") + " "
} else {
params = optionalize(specs?.params) + " "
}
console.info(` ${lwhite(`npx wrapped-wit ${cmd}`)} ${params ? green(params) : ""}${flags}${options}`)
} else {
console.info(` ${lwhite(`npx wrapped-wit ${cmd}`)} ${flags}${options}`)
}
console.info("\nDESCRIPTION:")
console.info(` ${router[cmd].hint}`)
} else {
console.info(` ${lwhite("npx wrapped-wit")} <COMMAND> ${flags}${options}`)
}
}
function showUsageOptions(options) {
if (options.length > 0) {
console.info("\nOPTIONS:")
const maxLength = options
.map(option => settings.options[option].param
? settings.options[option].param.length + option.length + 3
: option.length
)
.reduce((prev, curr) => curr > prev ? curr : prev)
options.forEach(option => {
if (settings.options[option].hint) {
const str = `${option}${settings.options[option].param ? helpers.colors.gray(` <${settings.options[option].param}>`) : ""}`
console.info(" ", `--${str}${" ".repeat(maxLength - helpers.colorstrip(str).length)}`, " ", settings.options[option].hint)
}
})
}
}
/// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
async function balance(flags = {}) {
const { provider, network } = flags
const contract = await WrappedWIT.fetchContractFromEthersProvider(provider)
helpers.traceHeader(network.toUpperCase(), helpers.colors.lcyan)
const signers = await provider.listAccounts()
const records = []
let totalWit = 0n; let totalEth = 0n
records.push(
...await Promise.all(signers.map(async signer => {
const eth = await provider.getBalance(signer.address)
const wit = BigInt(await contract.balanceOf.staticCall(signer.address))
totalEth += eth
totalWit += wit
return [signer.address, eth, wit]
}))
)
records.push(["", totalEth, totalWit])
helpers.traceTable(
records.map(([address, eth, wit], index) => {
totalWit += wit
eth = (Number(eth / BigInt(10 ** 15)) / 1000).toFixed(3)
wit = (Number(wit / BigInt(10 ** 8)) / 10).toFixed(1)
return [
address !== "" ? index : "",
address,
address !== "" ? helpers.colors.blue(helpers.commas(eth)) : helpers.colors.lblue(helpers.commas(eth) + " ETH"),
address !== "" ? helpers.colors.yellow(helpers.commas(wit)) : helpers.colors.lyellow(helpers.commas(wit) + " WIT"),
]
}), {
headlines: ["INDEX", "EVM ADDRESS", `${helpers.colors.lwhite("ETH")} BALANCE`, `${helpers.colors.lwhite("WIT")} BALANCE`],
humanizers: [helpers.commas, , ,],
colors: [, helpers.colors.mblue],
}
)
}
async function gateway(flags = {}, args = []) {
[args] = helpers.deleteExtraFlags(args)
const network = args[0]
if (!network) {
throw new Error("No EVM network was specified.")
} else if (network && !WrappedWIT.isNetworkSupported(network)) {
throw new Error(`Unsupported network "${network}"`)
} else {
const shell = spawn(
os.type() === "Windows_NT" ? "npx.cmd" : "npx", [
"ethrpc",
network,
flags?.port || 8545,
],
{ shell: true }
)
shell.stdout.on("data", (x) => {
process.stdout.write(x.toString())
})
shell.stderr.on("data", (x) => {
process.stderr.write(x.toString())
})
}
}
async function networks(flags = {}) {
const { testnets } = flags
const networks = Object.fromEntries(WrappedWIT.getSupportedNetworks()
.filter(network => {
const settings = WrappedWIT.getNetworkSettings(network)
const address = WrappedWIT.getNetworkAddresses(network)[settings.contract]
return address && address !== "" && !(!WrappedWIT.isNetworkMainnet(network) ^ testnets)
})
.map(network => {
return [
network, {
"Chain id": WrappedWIT.getNetworkChainId(network),
Mainnet: WrappedWIT.isNetworkMainnet(network),
Canonical: WrappedWIT.isNetworkCanonical(network),
},
]
})
)
console.table(networks)
}
async function contract(flags = {}) {
const { network, provider, verbose } = flags
const contract = await WrappedWIT.fetchContractFromEthersProvider(provider)
const settings = WrappedWIT.getNetworkSettings(network)
const records = []
records.push(["Contract address", helpers.colors.lblue(await contract.getAddress())])
if (verbose) {
records.push(["Curator address", helpers.colors.blue(await contract.evmCurator())])
records.push(["Wit/Oracle address", helpers.colors.mblue(await contract.witOracle())])
records.push(["Wit/Oracle PoI's CCDR template", helpers.colors.blue(await contract.witOracleCrossChainProofOfInclusionTemplate())])
records.push(["Wit/Oracle PoR's Radon hash", helpers.colors.cyan((await contract.witOracleProofOfReserveRadonHash()).slice(2))])
}
if (verbose) {
const providers = await contract.witOracleCrossChainRpcProviders()
records.push(["Wit/Oracle Cross-chain Wit/RPC providers", helpers.colors.gray(providers[0])])
providers.slice(1).forEach(provider => records.push(["", helpers.colors.gray(provider)]))
}
records.push(["Wit/Custodian recipient address", helpers.colors.mcyan(await contract.witCustodianWrapper())])
if (verbose) {
records.push(["Wit/Custodian sender address", helpers.colors.mmagenta(await contract.witCustodianUnwrapper())]);
}
WrappedWIT.
records.push(["Total wrap transactions", helpers.colors.white(helpers.commas(await contract.totalWraps()))])
records.push(["Total unwrap transactions", helpers.colors.white(helpers.commas(await contract.totalUnwraps()))])
helpers.traceTable(records, {
headlines: [
`:${helpers.colors.lcyan(network.replace(":", " ").toUpperCase())}`,
`:${helpers.colors.lwhite(settings.contract + " contract")}`,
],
})
}
async function supplies(flags = {}) {
const { network, provider, from, gasPrice } = flags
const contract = await WrappedWIT.fetchContractFromEthersProvider(provider)
const records = []
const totalSupply = Witnet.Coins.fromPedros(await contract.totalSupply())
if (WrappedWIT.isNetworkCanonical(network)) {
// connect to Witnet
const witnet = await Witnet.JsonRpcProvider.fromEnv(flags?.witnet)
const witnetSupply = await witnet.supplyInfo()
const totalReserveSupply = Witnet.Coins.fromPedros(await contract.totalReserveSupply())
records.push([
"Currently tradeable token supply",
totalReserveSupply.pedros <= totalSupply.pedros
? helpers.colors.lyellow(helpers.commas(totalSupply.wits.toFixed(2)))
: helpers.colors.yellow(helpers.commas(totalSupply.wits.toFixed(2))),
])
records.push([
"Max. supply that can be unwrapped",
totalReserveSupply.pedros >= totalSupply.pedros
? helpers.colors.myellow(helpers.commas(totalSupply.wits.toFixed(2)))
: helpers.colors.yellow(helpers.commas(totalReserveSupply.wits.toFixed(2))),
])
const witCustodianWrapper = await contract.witCustodianWrapper()
const witCustodianUnwrapper = await contract.witCustodianUnwrapper()
let witnetBalance = 0n
const custodianBalance = await witnet.getBalance(witCustodianWrapper)
witnetBalance += custodianBalance.locked + custodianBalance.staked + custodianBalance.unlocked
// if (witnetBalance > totalSupply.pedros) {
// records.push([
// "Pending to-be-wrapped token supply",
// helpers.colors.magenta(helpers.commas(Witnet.Coins.fromPedros(witnetBalance - totalSupply.pedros).wits.toFixed(2))),
// ])
// }
if (witCustodianUnwrapper !== witCustodianWrapper) {
const unwrapperBalance = await witnet.getBalance(witCustodianUnwrapper)
witnetBalance += unwrapperBalance.locked + unwrapperBalance.staked + unwrapperBalance.unlocked
}
records.push([
"Token under-custody supply on Witnet",
witnetBalance >= totalReserveSupply.pedros
? helpers.colors.mmagenta(helpers.commas(Witnet.Coins.fromPedros(witnetBalance).wits.toFixed(2)))
: helpers.colors.magenta(helpers.commas(Witnet.Coins.fromPedros(witnetBalance).wits.toFixed(2))),
])
records.push([
"Potentially wrappable supply on Witnet",
helpers.colors.lmagenta(helpers.commas(
Witnet.Coins.fromPedros(BigInt(witnetSupply.current_unlocked_supply) - witnetBalance).wits.toFixed(2)
)),
])
records.push([
"Currently locked/staked supply on Witnet",
helpers.colors.gray(helpers.commas(
Witnet.Coins.fromPedros(
BigInt(witnetSupply.current_staked_supply) + BigInt(witnetSupply.current_locked_supply)
).wits.toFixed(2)
)),
])
if (witnetBalance !== totalReserveSupply.pedros) {
const user = await prompt([{
message: "On-chain under-custody supply is outdated! Shall we report a fresh new Proof-of-Reserve ?",
name: "continue",
type: "confirm",
default: false,
}])
if (user.continue) {
helpers.traceHeader(network.toUpperCase(), helpers.colors.lcyan)
// create Witnet Wallet
const wallet = await Witnet.Wallet.fromEnv({ provider: witnet, strategy: "slim-first" })
// fetch proof-of-reserve radon bytecode from the token contract
const bytecode = await contract.witOracleProofOfReserveRadonBytecode()
// fetch Wit/Oracle Query settings from the token contract
const querySettings = await contract.witOracleQuerySettings()
// create Witnet Radon request
const request = Witnet.Radon.RadonRequest.fromBytecode(bytecode)
// print dry-run report on console
console.info(helpers.colors.lwhite("> Notarizing Proof-of-Reserve on Witnet ..."))
execSync(
`npx witsdk radon dry-run ${bytecode} --verbose --indent 2 --headline "WRAPPED / WIT PROOF-OF-RESERVE DRY-RUN REPORT"`,
{ stdio: "inherit", stdout: "inherit" },
)
console.info()
// create and transmit Witnet Data Request Transaction (DRT)
const PoRs = Witnet.DataRequests.from(wallet, request)
let tx = await PoRs.sendTransaction({
fees: Witnet.TransactionPriority.Medium,
witnesses: querySettings.minWitnesses,
maxResultSize: 256,
})
// await inclusion of the DRT in Witnet
console.info(` - DRO hash: ${helpers.colors.green(tx.droHash)}`)
console.info(` - DRT hash: ${helpers.colors.lwhite(tx.hash)}`)
console.info(` - DRT cost: ${helpers.colors.mmagenta(ethers.formatUnits(tx.fees.nanowits + tx.value?.nanowits, 9) + " WIT")}`)
tx = await PoRs.confirmTransaction(tx.hash, {
confirmations: 0,
onStatusChange: () => console.info(` - DRT status: ${tx.status}`),
})
// await resolution of the DRT in Witnet
let status = tx.status
do {
const report = await witnet.getDataRequest(tx.hash, "ethereal")
if (report.status !== status) {
status = report.status
console.info(` - DRT status: ${report.status}`)
}
if (report.status === "solved" && report?.result) {
console.info(` - DRT result: ${utils.cbor.decode(utils.fromHexString(report.result.cbor_bytes))}`)
break
}
const delay = ms => new Promise(_resolve => setTimeout(_resolve, ms))
await helpers.prompter(delay(5000))
} while (status !== "solved")
// retrieve data push report from Wit/Kermit:
console.info(
helpers.colors.lwhite("\n> Pushing Proof-of-Reserve report into ") +
helpers.colors.mblue(WrappedWIT.getNetworkContractAddress(network)) +
helpers.colors.lwhite(" ...")
)
const kermit = await KermitClient.fromEnv(flags?.kermit)
console.info(` - Wit/Kermit provider: ${kermit.url}`)
const report = await kermit.getDataPushReport(tx.hash, network)
const message = utils.abiEncodeDataPushReportMessage(report)
const digest = utils.abiEncodeDataPushReportDigest(report)
helpers.traceData(" - Push data report: ", message.slice(2), 64, "\x1b[90m")
console.info(` - Push data digest: ${digest.slice(2)}`)
console.info(` - Push data proof: ${report?.evm_proof.slice(2)}`)
// push data report into the consumer contract:
if (!from) from = (await provider.listAccounts())[0].address;
contract = contract.connect(await provider.getSigner(from))
console.info(` - EVM data pusher: ${from}`)
await contract
.pushDataReport
.send(
utils.abiEncodeDataPushReport(report),
report.evm_proof,
{ gasPrice }
)
.then(async (tx) => {
console.info(` - Transaction hash: ${tx.hash}`)
return helpers.prompter(tx.wait(confirmations || 1 ))
})
.then(receipt => {
console.info(` - Block number: ${helpers.commas(receipt.blockNumber)}`)
console.info(` - Gas price: ${helpers.commas(receipt.gasPrice)}`)
console.info(` - Gas used: ${helpers.commas(receipt.gasUsed)}`)
console.info(` - Transaction cost: ${ethers.formatEther(receipt.gasPrice * receipt.gasUsed)} ETH`)
return receipt
})
}
}
} else {
records.push([
"Currently tradeable token supply",
helpers.colors.myellow(helpers.commas(totalSupply.wits.toFixed(2))),
])
}
helpers.traceTable(records, {
headlines: [
`${helpers.colors.lcyan(network.replace(":", " ").toUpperCase())}`,
"Available ($WIT)",
],
})
process.exit(0)
}
async function transfers(flags = {}) {
let { provider, network, from, into, value, since, gasPrice, confirmations } = flags
let contract = await WrappedWIT.fetchContractFromEthersProvider(provider)
helpers.traceHeader(network.toUpperCase(), helpers.colors.lcyan)
if (value) {
value = Witnet.Coins.fromWits(value)
if (!into) {
throw new Error("--into must be specified.")
} else if (!from) {
from = (await provider.listAccounts())[0].address
}
contract = contract.connect(await provider.getSigner(from))
console.info(helpers.colors.lwhite(`> Transferring ${ethers.formatUnits(value.pedros, 9)} WIT ...`))
console.info(` - EVM sender: ${from}`)
console.info(` - EVM recipient: ${into}`)
await contract
.transfer
.send(into, value.pedros, { gasPrice })
.then(async (tx) => {
console.info(` - Transaction hash: ${tx.hash}`)
return helpers.prompter(tx.wait(confirmations || 1))
})
.then(receipt => {
console.info(` - Block number: ${helpers.commas(receipt.blockNumber)}`)
console.info(` - Gas price: ${helpers.commas(receipt.gasPrice)}`)
console.info(` - Gas used: ${helpers.commas(receipt.gasUsed)}`)
console.info(` - Transaction cost: ${ethers.formatEther(receipt.gasPrice * receipt.gasUsed)} ETH`)
return receipt
})
}
const fromBlock = since ? BigInt(flags.since) : undefined
let events = (fromBlock !== undefined && !value
? (await contract.queryFilter("Transfer", fromBlock)).slice(flags?.offset || 0, flags?.limit || 64)
: (await contract.queryFilter("Transfer")).reverse().slice(0, flags?.limit || 64)
)
if (from) events = events.filter(event => event.args[0].toLowerCase().indexOf(from.toLowerCase()) > -1)
if (into && !value) events = events.filter(event => event.args[1].toLowerCase().indexOf(into.toLowerCase()) > -1)
if (!flags?.mints) events = events.filter(event => event.args[0] !== "0x0000000000000000000000000000000000000000")
if (!flags?.burns) events = events.filter(event => event.args[1] !== "0x0000000000000000000000000000000000000000")
if (events.length > 0) {
helpers.traceTable(
events.map(event => {
const sender = `${event.args[0].slice(0, 7)}...${event.args[0].slice(-7)}`
const recipient = `${event.args[1].slice(0, 7)}...${event.args[1].slice(-7)}`
return [
event.blockNumber,
event.transactionHash,
event.args[0] === "0x0000000000000000000000000000000000000000" ? helpers.colors.blue(sender) : helpers.colors.mblue(sender),
event.args[1] === "0x0000000000000000000000000000000000000000" ? helpers.colors.blue(recipient) : helpers.colors.mblue(recipient),
event.args[2],
]
}), {
headlines: ["BLOCK NUMBER", "EVM TRANSACTION HASH", "EVM SENDER", "EVM RECIPIENT", `VALUE (${helpers.colors.lwhite("$pedros")})`],
humanizers: [helpers.commas, , , , helpers.commas],
colors: [, helpers.colors.gray, , , helpers.colors.yellow],
}
)
} else {
console.info(`^ No transfers found ${from ? `from "${from}" ` : ""}${into ? `into "${into}"` : ""}.`)
}
}
async function unwrappings(flags = {}) {
let { check, provider, network, from, into, value, since, offset, limit, gasPrice, confirmations } = flags
let contract = await WrappedWIT.fetchContractFromEthersProvider(provider)
helpers.traceHeader(network.toUpperCase(), helpers.colors.lcyan)
if (from && !ethers.isAddress(from)) {
throw new Error("--from must specify some valid <EVM_ADDRESS>.")
} else if (into) {
try {
Witnet.PublicKeyHash.fromBech32(into)
} catch {
throw new Error("--into must specify some valid <WIT_ADDRESS>.")
}
}
if (value) {
value = Witnet.Coins.fromWits(value)
if (!into) {
throw new Error("--into <WIT_ADDRESS> must be specified.")
} else if (!from) {
from = (await provider.listAccounts())[0].address
}
contract = contract.connect(await provider.getSigner(from))
console.info(helpers.colors.lwhite(`> Unwrapping ${ethers.formatUnits(value.pedros, 9)} WIT ...`))
console.info(` - EVM sender: ${from}`)
console.info(` - WITNET recipient: ${into}`)
await contract
.unwrap
.send(value.pedros, into, { gasPrice }) // swap method's parameters order
.then(async (tx) => {
console.info(` - Transaction hash: ${tx.hash}`)
return await helpers.prompter(tx.wait(confirmations || 1))
})
.then(receipt => {
console.info(` - Transaction cost: ${ethers.formatEther(receipt.gasPrice * receipt.gasUsed)} ETH`)
console.info(` - Block number: ${helpers.commas(receipt.blockNumber)}`)
console.info(` - Gas price: ${helpers.commas(receipt.gasPrice)}`)
console.info(` - Gas used: ${helpers.commas(receipt.gasUsed)}`)
return receipt
})
}
const fromBlock = since ? BigInt(flags.since) : undefined
let events = (fromBlock !== undefined && !value
? (await contract.queryFilter("Unwrapped", fromBlock)).slice(offset || 0, limit || 64)
: (await contract.queryFilter("Unwrapped")).reverse().slice(0, limit || 64)
)
if (from) events = events.filter(event => event.args[0].toLowerCase().indexOf(from.toLowerCase()) > -1)
if (into && !value) events = events.filter(event => event.args[1].toLowerCase().indexOf(into.toLowerCase()) > -1)
if (check) {
const witnet = await Witnet.JsonRpcProvider.fromEnv(flags?.witnet)
const records = await helpers.prompter(
Promise.all(events.map(async event => {
const ethBlock = await provider.getBlock(event.blockNumber)
const witUnwrapTx = await WrappedWIT.findUnwrapTransactionFromWitnetProvider(
witnet,
network,
event.blockNumber,
event.args[3], // nonce
event.args[0], // from
event.args[1], // to
event.args[2] // value
)
return [
{ blockNumber: event.blockNumber, hash: event.transactionHash, timestamp: ethBlock.timestamp },
witUnwrapTx,
witUnwrapTx ? moment.duration(moment.unix(witUnwrapTx.timestamp).diff(moment.unix(ethBlock.timestamp))).humanize() : "",
]
}))
)
helpers.traceTable(
records.map(([evm, wit, timediff]) => [
evm.blockNumber,
evm.hash,
wit?.hash,
timediff,
]), {
headlines: [
"BLOCK NUMBER",
"EVM UNWRAP TRANSACTION HASH",
`VALUE TRANSFER TRANSACTION HASH ON WITNET ${witnet.network.toUpperCase()}`,
":TIME DIFF",
],
humanizers: [helpers.commas, , ,],
colors: [, helpers.colors.gray, helpers.colors.magenta],
}
)
} else {
if (events.length > 0) {
helpers.traceTable(
events.map(event => {
const sender = `${event.args[0].slice(0, 7)}...${event.args[0].slice(-7)}`
return [
event.blockNumber,
event.transactionHash,
sender,
event.args[1],
event.args[2],
]
}), {
headlines: [
"BLOCK NUMBER",
"EVM UNWRAP TRANSACTION HASH",
"EVM UNWRAPPER",
`WIT RECIPIENT ON WITNET ${WrappedWIT.isNetworkMainnet(network) ? "MAINNET" : "TESTNET"}`,
`VALUE (${helpers.colors.lwhite("$pedros")})`,
],
humanizers: [helpers.commas, , , , helpers.commas],
colors: [, helpers.colors.gray, helpers.colors.mblue, helpers.colors.mmagenta, helpers.colors.yellow],
}
)
} else {
console.info(`^ No unwrappings found ${from ? `from "${from}" ` : ""}${into ? `into "${into}"` : ""}.`)
}
}
process.exit(0)
}
async function wrappings(flags = {}) {
let { provider, network, from, into, value, since, offset, limit, gasPrice, confirmations, check } = flags
let contract = await WrappedWIT.fetchContractFromEthersProvider(provider)
const witnet = await Witnet.JsonRpcProvider.fromEnv(flags?.witnet)
helpers.traceHeader(network.toUpperCase(), helpers.colors.lcyan)
if (into && !ethers.isAddress(into)) {
throw new Error("--into must specify some valid <EVM_ADDRESS>.")
} else if (from) {
try {
Witnet.PublicKeyHash.fromBech32(from)
} catch {
throw new Error("--from must specify some valid <WIT_ADDRESS>.")
}
}
let wallet, ledger
if (value || flags["vtt-hash"]) {
// create local wallet
wallet = await Witnet.Wallet.fromEnv({ provider: witnet, strategy: "slim-first", onlyWithFunds: false })
// select account/signer address from witnet wallet
ledger = from ? wallet.getAccount(from) : wallet
if (!ledger) {
throw new Error("--from <WIT_ADDRESS> not found on wallet.")
}
}
// read Wit/ custodian address from token contract
const witCustodianWrapper = await contract.witCustodianWrapper()
if (value && witnet) {
value = Witnet.Coins.fromWits(value)
if ((await ledger.getBalance()).unlocked < value.pedros) {
throw new Error(`Insufficient funds on ${ledger.pkh}`)
}
const user = await prompt([{
message: `Transfer ${value.toString(2)} into custodian address at ${witCustodianWrapper} ?`,
name: "continue",
type: "confirm",
default: false,
}])
if (user.continue) {
// create and send to Witnet a new value transfer transaction with metadata tag
const VTTs = Witnet.ValueTransfers.from(ledger)
const metadata = Witnet.PublicKeyHash.fromHexString(into).toBech32(witnet.network)
let tx = await VTTs.sendTransaction({
recipients: [
[witCustodianWrapper, value],
[metadata, Witnet.Coins.fromPedros(1n)],
],
})
console.info(` - From: ${helpers.colors.mmagenta(ledger.pkh)}`)
console.info(` - Into: ${helpers.colors.mcyan(witCustodianWrapper)}`)
console.info(` - Value: ${helpers.colors.myellow(`${value.wits.toFixed(2)} WIT`)}`)
console.info(` - Fees: ${helpers.colors.yellow(tx.fees.toString())}`)
console.info(` - VTT hash: ${helpers.colors.lwhite(tx.hash)}`)
// await inclusion of the VTT in Witnet
tx = await VTTs.confirmTransaction(tx.hash, {
confirmations: 0,
onStatusChange: () => console.info(` - VTT status: ${tx.status}`),
})
console.info(helpers.colors.myellow("\n => Please, wait a few minutes until the transaction gets finalized on Witnet,"))
console.info(helpers.colors.myellow(" before querying cross-chain verification.\n"))
// remove --into filter
into = undefined
}
}
if (flags["vtt-hash"] && witnet) {
const vttHash = flags["vtt-hash"].startsWith("0x") ? flags["vtt-hash"].slice(2) : flags["vtt-hash"]
if (!utils.isHexStringOfLength(vttHash, 32)) {
throw new Error("--vtt-hash must specify a valid Witnet transaction hash.")
}
// check the VTT exists, and fetch transaction etheral report
const vtt = await witnet.getValueTransfer(vttHash, "ethereal")
// check if the VTT is a valid transfer to the custodian address:
if (vtt.recipient !== witCustodianWrapper) {
throw new Error(`Transaction ${vttHash} transfers no value to custodian address at ${witCustodianWrapper}.`)
} else if (!vtt.metadata) {
throw new Error(`Transaction ${vttHash} transfers value to custodian address at ${witCustodianWrapper}, but specifies no EVM recipient.`)
}
let proceed, wrapEvent, user
// check VTT's current wrapping status
switch (await contract.getWrapTransactionStatus(`0x${vttHash}`)) {
case 3n: // Done
proceed = false
wrapEvent = (await contract.queryFilter("Wrapped")).find(event => event.args[3] === `0x${vttHash.toLowerCase()}`)
if (wrapEvent) {
since = wrapEvent.blockNumber; offset = 0; limit = 1
from = wrapEvent.args[0]; into = wrapEvent.args[1]
console.info(helpers.colors.mgreen(" => This Witnet wrap transaction has already been verified and minted:"))
}
break
case 0n: // Unknown
case 2n: // Retry
proceed = true
break
case 1n: // Awaiting
user = await prompt([{
message: "The specified VTT hash is currently being verified. Shall we retry, anyways ?",
name: "continue",
type: "confirm",
default: false,
}])
proceed = user.continue
break
default:
proceed = false
}
if (proceed && vtt.finalized !== 1) {
console.info(helpers.colors.mred("\n => Sorry, wait a few minutes until the transaction gets finalized on Witnet,"))
console.info(helpers.colors.mred(" before querying cross-chain verification.\n"))
proceed = false
}
if (proceed) {
if (!gasPrice) gasPrice = (await provider.getFeeData()).gasPrice
const fee = await contract.witOracleEstimateWrappingFee(gasPrice)
const gas = await contract.wrap.estimateGas(`0x${vttHash}`, { value: fee, gasPrice })
const cost = fee + gasPrice * gas
user = await prompt([{
message: `Verification is expected to cost ${ethers.formatEther(cost)} ETH. Shall we proceed ?`,
name: "continue",
type: "confirm",
default: true,
}])
if (user.continue) {
// connect contract to the eth/rpc provider's default signer
contract = contract.connect(await provider.getSigner())
console.info(helpers.colors.lwhite(`> Wrapping ${ethers.formatUnits(vtt.value, 9)} WIT ...`))
console.info(` - WITNET sender: ${vtt.sender}`)
console.info(` - EVM recipient: ${ethers.getAddress(`0x${vtt.metadata}`)}`)
console.info(` - EVM signer: ${(await provider.getSigner()).address}`)
console.info(` - Wit/Oracle fee: ${ethers.formatEther(fee)} ETH`)
await contract
.wrap
.send(`0x${vttHash}`, { value: fee, gasPrice })
.then(async (tx) => {
console.info(` - EVM tx hash: ${tx.hash}`)
return await helpers.prompter(tx.wait(confirmations || 1))
})
.then(receipt => {
console.info(` - EVM tx cost: ${ethers.formatEther(fee + receipt.gasPrice * receipt.gasUsed)} ETH`)
console.info(` - Gas price: ${helpers.commas(receipt.gasPrice)}`)
console.info(` - Gas used: ${helpers.commas(receipt.gasUsed)}`)
console.info(` - Block number: ${helpers.commas(receipt.blockNumber)}`)
return receipt
})
// settle --from filter
from = vtt.sender
// delete --into filter
into = undefined
}
}
}
const fromBlock = since ? BigInt(since) : undefined
let events = []
events.push(...(fromBlock !== undefined && !value
? (await contract.queryFilter("Wrapped", fromBlock)).slice(offset || 0, limit || 64)
: (await contract.queryFilter("Wrapped")).reverse().slice(0, limit || 64)
))
if (from) events = events.filter(event => event.args[0].toLowerCase().indexOf(from.toLowerCase()) > -1)
if (into) events = events.filter(event => event.args[1].toLowerCase().indexOf(into.toLowerCase()) > -1)
// insert pending to-be-validated wrap transactions, only if --from or --into are specified:
{
const utxos = await witnet.getUtxos(witCustodianWrapper)
const hashes = [...new Set(utxos.map(utxo => utxo.output_pointer.split(":")[0]))]
for (let index = 0; index < hashes.length; index++) {
const vtt = await witnet.getValueTransfer(hashes[index], "ethereal")
if (!events.find(event => event.args[3] === `0x${hashes[index]}`)) {
if (vtt?.metadata && (!from || from.toLowerCase() === vtt.sender) && (!into || into.toLowerCase() === `0x${vtt.metadata}`)) {
let status = ""
switch ((await contract.getWrapTransactionStatus(`0x${hashes[index]}`))) {
case 0n:
status = (vtt.finalized === 1) ? "(finalized on Witnet)" : "(awaiting finalization on Witnet)"
break
case 1n:
status = "(awaiting cross-chain verification)"
break
case 2n:
status = "(cross-chain verification failed)"
break
}
events.push({
blockNumber: undefined,
transactionHash: status,
args: [vtt.sender, ethers.getAddress(`0x${vtt.metadata}`), vtt.value, `0x${hashes[index]}`],
})
}
}
}
}
if (check) {
const records = []
records.push(...await helpers.prompter(
Promise.all(events.map(async event => {
const ethBlock = event.blockNumber ? await provider.getBlock(event.blockNumber) : undefined
const ethTxHash = event.transactionHash
const witTxHash = event.args[3]
const witTx = await witnet.getValueTransfer(witTxHash.slice(2), "ethereal")
return [
{ hash: witTxHash.slice(2), timestamp: witTx.timestamp },
{ blockNumber: event?.blockNumber, hash: ethTxHash, timestamp: ethBlock?.timestamp },
ethBlock
? moment.duration(moment.unix(ethBlock.timestamp).diff(moment.unix(witTx.timestamp))).humanize()
: moment.unix(witTx.timestamp).fromNow(),
]
}))
))
if (records.length > 0) {
helpers.traceTable(
records.map(([wit, evm, timediff]) => {
return [
evm?.blockNumber ? helpers.commas(evm.blockNumber) : "",
wit.hash,
evm.hash.startsWith("0x") ? helpers.colors.gray(evm.hash) : helpers.colors.red(evm.hash),
timediff,
]
}), {
headlines: [
"BLOCK NUMBER",
`VALUE TRANSFER TRANSACTION HASH ON WITNET ${witnet.network.toUpperCase()}`,
"ERC-20 WRAP VALIDATING TRANSACTION HASH",
":TIME DIFF",
],
colors: [, helpers.colors.magenta],
}
)
} else {
console.info(`^ No verified wrappings found so far ${from ? `from "${from}" ` : ""}${into ? `into "${into}"` : ""}.`)
}
} else {
const records = events.map(event => ({
blockNumber: event.blockNumber,
sender: event.args[0],
transactionHash: event.transactionHash,
recipient: `${event.args[1].slice(0, 7)}...${event.args[1].slice(-7)}`,
value: event.args[2],
}))
if (records.length > 0) {
helpers.traceTable(
records.map(record => {
return [
record?.blockNumber ? helpers.commas(record.blockNumber) : "",
record.sender,
record.transactionHash.startsWith("0x") ? helpers.colors.gray(record.transactionHash) : helpers.colors.red(record.transactionHash),
record.recipient,
record.value,
]
}), {
headlines: [
"BLOCK NUMBER",
`WIT SENDER ON WITNET ${WrappedWIT.isNetworkMainnet(network) ? "MAINNET" : "TESTNET"}`,
"ERC-20 WRAP VALIDATING TRANSACTION HASH",
"EVM RECIPIENT",
`VALUE (${helpers.colors.lwhite("$pedros")})`,
],
humanizers: [, , , , helpers.commas],
colors: [, helpers.colors.mmagenta, , helpers.colors.mblue, helpers.colors.yellow],
}
)
} else {
console.info(`^ No verified wrappings found so far ${from ? `from "${from}" ` : ""}${into ? `into "${into}"` : ""}.`)
}
}
process.exit(0)
}