diff --git a/abi/staging/ArmadaOperators.json b/abi/staging/ArmadaOperators.json index 9026562..57e75eb 100644 --- a/abi/staging/ArmadaOperators.json +++ b/abi/staging/ArmadaOperators.json @@ -88,6 +88,44 @@ "name": "OperatorStakeChanged", "type": "event" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "operatorId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "depositOperatorBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -162,6 +200,11 @@ "internalType": "uint256", "name": "stake", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" } ], "internalType": "struct ArmadaOperator", @@ -213,6 +256,11 @@ "internalType": "uint256", "name": "stake", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" } ], "internalType": "struct ArmadaOperator[]", @@ -264,6 +312,29 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "operatorId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "withdrawOperatorBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/abi/staging/ArmadaProjects.json b/abi/staging/ArmadaProjects.json index 51369a3..e876bee 100644 --- a/abi/staging/ArmadaProjects.json +++ b/abi/staging/ArmadaProjects.json @@ -510,6 +510,11 @@ "internalType": "uint256", "name": "amount", "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" } ], "name": "withdrawProjectEscrow", diff --git a/abi/staging/USDC.json b/abi/staging/USDC.json new file mode 100644 index 0000000..5cd7ad7 --- /dev/null +++ b/abi/staging/USDC.json @@ -0,0 +1,363 @@ +{ + "address": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + } + ] +} \ No newline at end of file diff --git a/abi/testnet/ArmadaOperators.json b/abi/testnet/ArmadaOperators.json index e0ab4d4..4b3a363 100644 --- a/abi/testnet/ArmadaOperators.json +++ b/abi/testnet/ArmadaOperators.json @@ -88,6 +88,44 @@ "name": "OperatorStakeChanged", "type": "event" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "operatorId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "depositOperatorBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -162,6 +200,11 @@ "internalType": "uint256", "name": "stake", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" } ], "internalType": "struct ArmadaOperator", @@ -213,6 +256,11 @@ "internalType": "uint256", "name": "stake", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" } ], "internalType": "struct ArmadaOperator[]", @@ -265,6 +313,29 @@ "type": "function" }, { + "inputs": [ + { + "internalType": "bytes32", + "name": "operatorId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "withdrawOperatorBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [ { "internalType": "bytes32", diff --git a/abi/testnet/ArmadaProjects.json b/abi/testnet/ArmadaProjects.json index 02fa05f..1e9af9e 100644 --- a/abi/testnet/ArmadaProjects.json +++ b/abi/testnet/ArmadaProjects.json @@ -510,6 +510,11 @@ "internalType": "uint256", "name": "amount", "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" } ], "name": "withdrawProjectEscrow", diff --git a/abi/testnet/USDC.json b/abi/testnet/USDC.json new file mode 100644 index 0000000..5cd7ad7 --- /dev/null +++ b/abi/testnet/USDC.json @@ -0,0 +1,363 @@ +{ + "address": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + } + ] +} \ No newline at end of file diff --git a/src/commands/node/create.ts b/src/commands/node/create.ts index a1fdbbd..8ff17aa 100644 --- a/src/commands/node/create.ts +++ b/src/commands/node/create.ts @@ -1,10 +1,10 @@ import { Arg } from "@oclif/core/lib/interfaces"; -import { parseUnits } from "ethers/lib/utils"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, parseUSDC, pretty, run } from "../../helpers"; export default class NodeCreate extends TransactionCommand { static summary = `Register content nodes on the Armada Network.`; + static description = "Node prices (PRICE) are expressed in USDC."; static examples = [ "<%= config.bin %> <%= command.id %> 0x123abc... host1.com:na,host2.com:eu", "<%= config.bin %> <%= command.id %> 0x123abc... host1.com,host2.com na:true:1.5", @@ -26,7 +26,7 @@ export default class NodeCreate extends TransactionCommand { public async run(): Promise { const { args, flags } = await this.parse(NodeCreate); - const operatorId = normalizeHash(args.ID); + const operatorId = parseHash(args.ID); const values: string[] = args.VALUES.split(","); const defaults: string[] = (args.DEFAULTS ?? "").split(":"); if (defaults.length > 3) { @@ -60,7 +60,7 @@ export default class NodeCreate extends TransactionCommand { region: region || defaultRegion, topology: false, disabled: enabled === "false" || (!enabled && defaultEnabled !== "true"), - price: parseUnits(price || defaultPrice || "0", 18), + price: parseUSDC(price || defaultPrice || "0"), }; }) ); diff --git a/src/commands/node/delete.ts b/src/commands/node/delete.ts index e996497..3c8ebe3 100644 --- a/src/commands/node/delete.ts +++ b/src/commands/node/delete.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, pretty, run } from "../../helpers"; export default class NodeDelete extends TransactionCommand { static summary = "Delete content nodes from the Armada Network."; @@ -15,7 +15,7 @@ export default class NodeDelete extends TransactionCommand { public async run(): Promise { const { args, flags } = await this.parse(NodeDelete); - const nodeIds = args.IDS.split(",").map((id: string) => normalizeHash(id)); + const nodeIds = args.IDS.split(",").map((id: string) => parseHash(id)); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const nodes = await getContract(flags.network, flags.abi, "ArmadaNodes", signer); const operatorId = (await nodes.getNode(nodeIds[0])).operatorId; diff --git a/src/commands/node/enable.ts b/src/commands/node/enable.ts index 0b830ab..eec2628 100644 --- a/src/commands/node/enable.ts +++ b/src/commands/node/enable.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, pretty, run } from "../../helpers"; export default class NodeEnable extends TransactionCommand { static summary = "Change enabled state of content nodes."; @@ -22,7 +22,7 @@ export default class NodeEnable extends TransactionCommand { const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const nodes = await getContract(flags.network, flags.abi, "ArmadaNodes", signer); - const nodeIds = args.IDS.split(",").map((id: string) => normalizeHash(id)); + const nodeIds = args.IDS.split(",").map((id: string) => parseHash(id)); const disables = nodeIds.map(() => args.BOOL !== "true"); const operatorId = (await nodes.getNode(nodeIds[0])).operatorId; const tx = await nodes.populateTransaction.setNodeDisabled(operatorId, nodeIds, disables); diff --git a/src/commands/node/host.ts b/src/commands/node/host.ts index f393c35..1d4716f 100644 --- a/src/commands/node/host.ts +++ b/src/commands/node/host.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getProvider, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getProvider, getSigner, parseHash, pretty, run } from "../../helpers"; export default class NodeHost extends TransactionCommand { static summary = "Change hosts and regions of content nodes."; @@ -45,7 +45,7 @@ export default class NodeHost extends TransactionCommand { const node = fetchOldValues ? await nodesView.getNode(nodeId) : undefined; return { - nodeId: normalizeHash(nodeId), + nodeId: parseHash(nodeId), host: host || node.host, region: region || defaultRegion || node.region, }; diff --git a/src/commands/node/list.ts b/src/commands/node/list.ts index 2611dcb..d8631ae 100644 --- a/src/commands/node/list.ts +++ b/src/commands/node/list.ts @@ -2,7 +2,7 @@ import { HashZero } from "@ethersproject/constants"; import { Flags } from "@oclif/core"; import { Result } from "ethers/lib/utils"; import { BlockchainCommand } from "../../base"; -import { getAll, getContract, getProvider, normalizeHash, normalizeRecords, pretty } from "../../helpers"; +import { formatNode, getAll, getContract, getProvider, parseHash, pretty } from "../../helpers"; export default class NodeList extends BlockchainCommand { static summary = "List content nodes on the Armada Network."; @@ -23,7 +23,7 @@ export default class NodeList extends BlockchainCommand { const { flags } = await this.parse(NodeList); const provider = await getProvider(flags.network, flags.rpc); const nodes = await getContract(flags.network, flags.abi, "ArmadaNodes", provider); - const operatorId = normalizeHash(flags.operator); + const operatorId = parseHash(flags.operator); const blockTag = await provider.getBlockNumber(); let results: Result[] = await getAll(flags.page, async (i, n) => { return await nodes.getNodes(operatorId, flags.topology, i, n, { blockTag }); @@ -44,7 +44,7 @@ export default class NodeList extends BlockchainCommand { }); const records = results.slice(flags.skip, flags.skip + flags.size); - const output = normalizeRecords(records); + const output = records.map((r) => formatNode(r)); this.log(pretty(output)); return output; } diff --git a/src/commands/node/price.ts b/src/commands/node/price.ts index de32298..697c50f 100644 --- a/src/commands/node/price.ts +++ b/src/commands/node/price.ts @@ -1,8 +1,7 @@ import { Flags } from "@oclif/core"; import { Arg } from "@oclif/core/lib/interfaces"; -import { parseUnits } from "ethers/lib/utils"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, parseUSDC, pretty, run } from "../../helpers"; export default class NodePrice extends TransactionCommand { static summary = "Change prices of content nodes."; @@ -18,8 +17,8 @@ export default class NodePrice extends TransactionCommand { static usage = "<%= command.id %> ID[:PRICE?],... [DEFAULT_PRICE] [--spot] [--renew]"; static aliases = ["node:price", "node:prices"]; static args: Arg[] = [ - { name: "ID:PRICE", description: "The comma separated price values for the nodes.", required: true }, - { name: "DEFAULT_PRICE", description: "The default price, if omitted in the values.", required: false }, + { name: "ID:PRICE", description: "The comma separated USDC price values for the nodes.", required: true }, + { name: "DEFAULT_PRICE", description: "The default USDC price, if omitted in the values.", required: false }, ]; static flags = { spot: Flags.boolean({ description: "Change price in the current epoch only (nodes must not be reserved)." }), @@ -50,8 +49,8 @@ export default class NodePrice extends TransactionCommand { } return { - nodeId: normalizeHash(nodeId), - price: parseUnits(price || defaultPrice, 18), + nodeId: parseHash(nodeId), + price: parseUSDC(price || defaultPrice), }; }) ); diff --git a/src/commands/node/show.ts b/src/commands/node/show.ts index 8f28a2a..bbfeed1 100644 --- a/src/commands/node/show.ts +++ b/src/commands/node/show.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { BlockchainCommand } from "../../base"; -import { getContract, getProvider, normalizeHash, normalizeRecord, pretty } from "../../helpers"; +import { formatNode, getContract, getProvider, parseHash, pretty } from "../../helpers"; export default class NodeShow extends BlockchainCommand { static summary = "Show details of an Armada Network node."; @@ -12,9 +12,9 @@ export default class NodeShow extends BlockchainCommand { const { args, flags } = await this.parse(NodeShow); const provider = await getProvider(flags.network, flags.rpc); const nodes = await getContract(flags.network, flags.abi, "ArmadaNodes", provider); - const nodeId = normalizeHash(args.ID); + const nodeId = parseHash(args.ID); const record = await nodes.getNode(nodeId); - const output = normalizeRecord(record); + const output = formatNode(record); this.log(pretty(output)); return output; } diff --git a/src/commands/operator/deposit.ts b/src/commands/operator/deposit.ts index 3c55520..b9fac42 100644 --- a/src/commands/operator/deposit.ts +++ b/src/commands/operator/deposit.ts @@ -1,33 +1,42 @@ import { Arg } from "@oclif/core/lib/interfaces"; -import { parseUnits } from "ethers/lib/utils"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, normalizeRecord, permit, pretty, run } from "../../helpers"; +import { + formatRecord, + formatUSDC, + getContract, + getSigner, + parseHash, + parseTokens, + permit, + pretty, + run, +} from "../../helpers"; export default class OperatorDeposit extends TransactionCommand { - static summary = "Deposit Armada tokens to operator escrow."; + static summary = "Deposit USDC to operator earned balance."; static examples = ["<%= config.bin %> <%= command.id %> 0x123abc... 1.0"]; - static usage = "<%= command.id %> ID TOKENS"; + static usage = "<%= command.id %> ID USDC"; static args: Arg[] = [ - { name: "ID", description: "The ID of the operator to deposit escrow for.", required: true }, - { name: "TOKENS", description: "The Armada token amount to deposit (e.g. 1.0).", required: true }, + { name: "ID", description: "The ID of the operator to deposit balance for.", required: true }, + { name: "USDC", description: "The USDC amount to deposit (e.g. 1.0).", required: true }, ]; public async run(): Promise { const { args, flags } = await this.parse(OperatorDeposit); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); - const token = await getContract(flags.network, flags.abi, "ArmadaToken", signer); + const usdc = await getContract(flags.network, flags.abi, "USDC", signer); const operators = await getContract(flags.network, flags.abi, "ArmadaOperators", signer); const address = await signer.getAddress(); - const id = normalizeHash(args.ID); - const amount = parseUnits(args.TOKENS, 18); + const id = parseHash(args.ID); + const amount = parseTokens(args.USDC); const deadline = Math.floor(Date.now() / 1000) + 3600; - const sig = await permit(signer, token, operators, amount, deadline); - const oldBalance = await token.balanceOf(address); - const tx = await operators.populateTransaction.depositOperatorStake(id, amount, deadline, sig.v, sig.r, sig.s); + const sig = await permit(signer, usdc, operators, amount, deadline); + const oldBalance = await usdc.balanceOf(address); + const tx = await operators.populateTransaction.depositOperatorBalance(id, amount, deadline, sig.v, sig.r, sig.s); const output = await run(tx, signer, [operators]); - const newBalance = await token.balanceOf(address); + const newBalance = await usdc.balanceOf(address); this.log(pretty(output)); - this.log(pretty(normalizeRecord({ address, oldBalance, newBalance }))); + this.log(pretty(formatRecord({ address, oldBalance: formatUSDC(oldBalance), newBalance: formatUSDC(newBalance) }))); return output; } } diff --git a/src/commands/operator/list.ts b/src/commands/operator/list.ts index d87ef22..0d06147 100644 --- a/src/commands/operator/list.ts +++ b/src/commands/operator/list.ts @@ -1,7 +1,7 @@ import { Flags } from "@oclif/core"; import { Result } from "ethers/lib/utils"; import { BlockchainCommand } from "../../base"; -import { getAll, getContract, getProvider, normalizeRecords, pretty } from "../../helpers"; +import { formatOperator, getAll, getContract, getProvider, pretty } from "../../helpers"; export default class OperatorList extends BlockchainCommand { static summary = "List operators on the Armada Network."; @@ -22,7 +22,8 @@ export default class OperatorList extends BlockchainCommand { return await operators.getOperators(i, n, { blockTag }); }); - const output = normalizeRecords(results.slice(flags.skip, flags.skip + flags.size)); + const records = results.slice(flags.skip, flags.skip + flags.size); + const output = records.map((r) => formatOperator(r)); this.log(pretty(output)); return output; } diff --git a/src/commands/operator/owner.ts b/src/commands/operator/owner.ts index f1931aa..e9d0e88 100644 --- a/src/commands/operator/owner.ts +++ b/src/commands/operator/owner.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, pretty, run } from "../../helpers"; export default class OperatorOwner extends TransactionCommand { static summary = "Transfer ownership of an operator."; @@ -16,7 +16,7 @@ export default class OperatorOwner extends TransactionCommand { const { args, flags } = await this.parse(OperatorOwner); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const operators = await getContract(flags.network, flags.abi, "ArmadaOperators", signer); - const operatorId = normalizeHash(args.ID); + const operatorId = parseHash(args.ID); const tx = await operators.populateTransaction.setOperatorOwner(operatorId, args.ADDR); const output = await run(tx, signer, [operators]); this.log(pretty(output)); diff --git a/src/commands/operator/props.ts b/src/commands/operator/props.ts index b569332..2f0b9d5 100644 --- a/src/commands/operator/props.ts +++ b/src/commands/operator/props.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, pretty, run } from "../../helpers"; export default class OperatorProps extends TransactionCommand { static summary = "Change detailed properties of an operator."; @@ -16,7 +16,7 @@ export default class OperatorProps extends TransactionCommand { const { args, flags } = await this.parse(OperatorProps); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const operators = await getContract(flags.network, flags.abi, "ArmadaOperators", signer); - const operatorId = normalizeHash(args.ID); + const operatorId = parseHash(args.ID); const tx = await operators.populateTransaction.setOperatorProps(operatorId, args.NAME, args.EMAIL); const output = await run(tx, signer, [operators]); this.log(pretty(output)); diff --git a/src/commands/operator/show.ts b/src/commands/operator/show.ts index 5cf4f9f..7bd5cfa 100644 --- a/src/commands/operator/show.ts +++ b/src/commands/operator/show.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { BlockchainCommand } from "../../base"; -import { getContract, getProvider, normalizeHash, normalizeRecord, pretty } from "../../helpers"; +import { formatOperator, getContract, getProvider, parseHash, pretty } from "../../helpers"; export default class OperatorShow extends BlockchainCommand { static summary = "Show details of an Armada Network operator."; @@ -12,9 +12,9 @@ export default class OperatorShow extends BlockchainCommand { const { args, flags } = await this.parse(OperatorShow); const provider = await getProvider(flags.network, flags.rpc); const operators = await getContract(flags.network, flags.abi, "ArmadaOperators", provider); - const operatorId = normalizeHash(args.ID); + const operatorId = parseHash(args.ID); const record = await operators.getOperator(operatorId); - const output = normalizeRecord(record); + const output = formatOperator(record); this.log(pretty(output)); return output; } diff --git a/src/commands/operator/stake.ts b/src/commands/operator/stake.ts new file mode 100644 index 0000000..5645775 --- /dev/null +++ b/src/commands/operator/stake.ts @@ -0,0 +1,44 @@ +import { Arg } from "@oclif/core/lib/interfaces"; +import { TransactionCommand } from "../../base"; +import { + formatRecord, + formatTokens, + getContract, + getSigner, + parseHash, + parseTokens, + permit, + pretty, + run, +} from "../../helpers"; + +export default class OperatorStake extends TransactionCommand { + static summary = "Deposit Armada tokens to operator stake."; + static examples = ["<%= config.bin %> <%= command.id %> 0x123abc... 1.0"]; + static usage = "<%= command.id %> ID TOKENS"; + static args: Arg[] = [ + { name: "ID", description: "The ID of the operator to deposit stake for.", required: true }, + { name: "TOKENS", description: "The Armada token amount to deposit (e.g. 1.0).", required: true }, + ]; + + public async run(): Promise { + const { args, flags } = await this.parse(OperatorStake); + const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key); + const token = await getContract(flags.network, flags.abi, "ArmadaToken", signer); + const operators = await getContract(flags.network, flags.abi, "ArmadaOperators", signer); + const address = await signer.getAddress(); + const id = parseHash(args.ID); + const amount = parseTokens(args.TOKENS); + const deadline = Math.floor(Date.now() / 1000) + 3600; + const sig = await permit(signer, token, operators, amount, deadline); + const oldBalance = await token.balanceOf(address); + const tx = await operators.populateTransaction.depositOperatorStake(id, amount, deadline, sig.v, sig.r, sig.s); + const output = await run(tx, signer, [operators]); + const newBalance = await token.balanceOf(address); + this.log(pretty(output)); + this.log( + pretty(formatRecord({ address, oldBalance: formatTokens(oldBalance), newBalance: formatTokens(newBalance) })) + ); + return output; + } +} diff --git a/src/commands/operator/unstake.ts b/src/commands/operator/unstake.ts new file mode 100644 index 0000000..3c5d1b8 --- /dev/null +++ b/src/commands/operator/unstake.ts @@ -0,0 +1,36 @@ +import { Arg } from "@oclif/core/lib/interfaces"; +import { TransactionCommand } from "../../base"; +import { formatRecord, formatTokens, getContract, getSigner, parseHash, parseTokens, pretty, run } from "../../helpers"; + +export default class OperatorUnstake extends TransactionCommand { + static summary = "Withdraw Armada tokens from operator stake."; + static description = + "The tokens are sent to the operator owner. Only the part of tokens not needed to " + + "stake the nodes can be withdrawn. In order to withdraw more, nodes can be deleted."; + static examples = ["<%= config.bin %> <%= command.id %> 0x123abc... 1.0"]; + static usage = "<%= command.id %> ID TOKENS"; + static args: Arg[] = [ + { name: "ID", description: "The ID of the operator to withdraw stake from.", required: true }, + { name: "TOKENS", description: "The Armada token amount to withdraw (e.g. 1.0).", required: true }, + ]; + + public async run(): Promise { + const { args, flags } = await this.parse(OperatorUnstake); + const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key); + const token = await getContract(flags.network, flags.abi, "ArmadaToken", signer); + const operators = await getContract(flags.network, flags.abi, "ArmadaOperators", signer); + const address = await signer.getAddress(); + const operatorId = parseHash(args.ID); + const amount = parseTokens(args.TOKENS); + if (amount.lte(0)) this.error("A positive amount required."); + const oldBalance = await token.balanceOf(address); + const tx = await operators.populateTransaction.withdrawOperatorStake(operatorId, amount, address); + const output = await run(tx, signer, [operators]); + const newBalance = await token.balanceOf(address); + this.log(pretty(output)); + this.log( + pretty(formatRecord({ address, oldBalance: formatTokens(oldBalance), newBalance: formatTokens(newBalance) })) + ); + return output; + } +} diff --git a/src/commands/operator/withdraw.ts b/src/commands/operator/withdraw.ts index 73bbaa3..ab34427 100644 --- a/src/commands/operator/withdraw.ts +++ b/src/commands/operator/withdraw.ts @@ -1,35 +1,32 @@ import { Arg } from "@oclif/core/lib/interfaces"; -import { parseUnits } from "ethers/lib/utils"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, normalizeRecord, pretty, run } from "../../helpers"; +import { formatRecord, formatUSDC, getContract, getSigner, parseHash, parseTokens, pretty, run } from "../../helpers"; export default class OperatorWithdraw extends TransactionCommand { - static summary = "Withdraw Armada tokens from operator escrow."; - static description = - "The tokens are sent to the operator owner. Only the part of tokens not needed to " + - "stake the nodes can be withdrawn. In order to withdraw more, nodes can be deleted."; + static summary = "Withdraw USDC from operator earned balance."; + static description = "The tokens are sent to the operator owner."; static examples = ["<%= config.bin %> <%= command.id %> 0x123abc... 1.0"]; - static usage = "<%= command.id %> ID TOKENS"; + static usage = "<%= command.id %> ID USDC"; static args: Arg[] = [ - { name: "ID", description: "The ID of the operator to withdraw escrow from.", required: true }, - { name: "TOKENS", description: "The Armada token amount to withdraw (e.g. 1.0).", required: true }, + { name: "ID", description: "The ID of the operator to withdraw balance from.", required: true }, + { name: "USDC", description: "The USDC amount to withdraw (e.g. 1.0).", required: true }, ]; public async run(): Promise { const { args, flags } = await this.parse(OperatorWithdraw); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); - const token = await getContract(flags.network, flags.abi, "ArmadaToken", signer); + const usdc = await getContract(flags.network, flags.abi, "USDC", signer); const operators = await getContract(flags.network, flags.abi, "ArmadaOperators", signer); const address = await signer.getAddress(); - const operatorId = normalizeHash(args.ID); - const tokens = parseUnits(args.TOKENS, 18); - if (tokens.lte(0)) this.error("A positive amount required."); - const oldBalance = await token.balanceOf(address); - const tx = await operators.populateTransaction.withdrawOperatorStake(operatorId, tokens, address); + const operatorId = parseHash(args.ID); + const amount = parseTokens(args.USDC); + if (amount.lte(0)) this.error("A positive amount required."); + const oldBalance = await usdc.balanceOf(address); + const tx = await operators.populateTransaction.withdrawOperatorBalance(operatorId, amount, address); const output = await run(tx, signer, [operators]); - const newBalance = await token.balanceOf(address); + const newBalance = await usdc.balanceOf(address); this.log(pretty(output)); - this.log(pretty(normalizeRecord({ address, oldBalance, newBalance }))); + this.log(pretty(formatRecord({ address, oldBalance: formatUSDC(oldBalance), newBalance: formatUSDC(newBalance) }))); return output; } } diff --git a/src/commands/project/content.ts b/src/commands/project/content.ts index a2ce404..c36f01f 100644 --- a/src/commands/project/content.ts +++ b/src/commands/project/content.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, pretty, run } from "../../helpers"; export default class ProjectContent extends TransactionCommand { static summary = "Publish the provided bundle on the network."; @@ -24,8 +24,8 @@ export default class ProjectContent extends TransactionCommand { const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const projects = await getContract(flags.network, flags.abi, "ArmadaProjects", signer); - const projectId = normalizeHash(args.ID); - const bundleSha = normalizeHash(args.SHA); + const projectId = parseHash(args.ID); + const bundleSha = parseHash(args.SHA); const tx = await projects.populateTransaction.setProjectContent(projectId, args.URL, bundleSha); const output = await run(tx, signer, [projects]); this.log(pretty(output)); diff --git a/src/commands/project/create.ts b/src/commands/project/create.ts index b5a871c..a52bde8 100644 --- a/src/commands/project/create.ts +++ b/src/commands/project/create.ts @@ -1,7 +1,7 @@ import { Flags } from "@oclif/core"; import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeAddress, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseAddress, parseHash, pretty, run } from "../../helpers"; export default class ProjectCreate extends TransactionCommand { static summary = "Register a new project on the Armada Network."; @@ -25,8 +25,8 @@ export default class ProjectCreate extends TransactionCommand { const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const projects = await getContract(flags.network, flags.abi, "ArmadaProjects", signer); - const owner = flags.owner ? normalizeAddress(flags.owner) : await signer.getAddress(); - const bundleSha = normalizeHash(args.SHA); + const owner = flags.owner ? parseAddress(flags.owner) : await signer.getAddress(); + const bundleSha = parseHash(args.SHA); const tx = await projects.populateTransaction.createProject([owner, args.NAME, args.EMAIL, args.URL, bundleSha]); const output = await run(tx, signer, [projects]); this.log(pretty(output)); diff --git a/src/commands/project/delete.ts b/src/commands/project/delete.ts index 9ae9cca..ca13e0f 100644 --- a/src/commands/project/delete.ts +++ b/src/commands/project/delete.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, pretty, run } from "../../helpers"; export default class ProjectDelete extends TransactionCommand { static summary = "Delete a project from the Armada Network."; @@ -12,7 +12,7 @@ export default class ProjectDelete extends TransactionCommand { const { args, flags } = await this.parse(ProjectDelete); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const projects = await getContract(flags.network, flags.abi, "ArmadaProjects", signer); - const projectId = normalizeHash(args.ID); + const projectId = parseHash(args.ID); const tx = await projects.populateTransaction.deleteProject(projectId); const output = await run(tx, signer, [projects]); this.log(pretty(output)); diff --git a/src/commands/project/deposit.ts b/src/commands/project/deposit.ts index b767cca..f00a947 100644 --- a/src/commands/project/deposit.ts +++ b/src/commands/project/deposit.ts @@ -1,7 +1,16 @@ import { Arg } from "@oclif/core/lib/interfaces"; -import { parseUnits } from "ethers/lib/utils"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, normalizeRecord, permit, pretty, run } from "../../helpers"; +import { + formatRecord, + formatUSDC, + getContract, + getSigner, + parseHash, + parseUSDC, + permit, + pretty, + run, +} from "../../helpers"; export default class ProjectDeposit extends TransactionCommand { static summary = "Deposit Armada tokens to project escrow."; @@ -9,25 +18,25 @@ export default class ProjectDeposit extends TransactionCommand { static usage = "<%= command.id %> ID TOKENS"; static args: Arg[] = [ { name: "ID", description: "The ID of the project to deposit escrow for.", required: true }, - { name: "TOKENS", description: "The Armada token amount to deposit (e.g. 1.0).", required: true }, + { name: "USDC", description: "The USDC amount to deposit (e.g. 1.0).", required: true }, ]; public async run(): Promise { const { args, flags } = await this.parse(ProjectDeposit); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); - const token = await getContract(flags.network, flags.abi, "ArmadaToken", signer); + const usdc = await getContract(flags.network, flags.abi, "USDC", signer); const projects = await getContract(flags.network, flags.abi, "ArmadaProjects", signer); const address = await signer.getAddress(); - const id = normalizeHash(args.ID); - const amount = parseUnits(args.TOKENS, 18); + const id = parseHash(args.ID); + const amount = parseUSDC(args.USDC); const deadline = Math.floor(Date.now() / 1000) + 3600; - const sig = await permit(signer, token, projects, amount, deadline); - const oldBalance = await token.balanceOf(address); + const sig = await permit(signer, usdc, projects, amount, deadline); + const oldBalance = await usdc.balanceOf(address); const tx = await projects.populateTransaction.depositProjectEscrow(id, amount, deadline, sig.v, sig.r, sig.s); const output = await run(tx, signer, [projects]); - const newBalance = await token.balanceOf(address); + const newBalance = await usdc.balanceOf(address); this.log(pretty(output)); - this.log(pretty(normalizeRecord({ address, oldBalance, newBalance }))); + this.log(pretty(formatRecord({ address, oldBalance: formatUSDC(oldBalance), newBalance: formatUSDC(newBalance) }))); return output; } } diff --git a/src/commands/project/list.ts b/src/commands/project/list.ts index 0afbd80..43dee20 100644 --- a/src/commands/project/list.ts +++ b/src/commands/project/list.ts @@ -1,7 +1,7 @@ import { Flags } from "@oclif/core"; import { Result } from "ethers/lib/utils"; import { BlockchainCommand } from "../../base"; -import { getAll, getContract, getProvider, normalizeAddress, normalizeRecords, pretty } from "../../helpers"; +import { formatProject, getAll, getContract, getProvider, parseAddress, pretty } from "../../helpers"; export default class ProjectList extends BlockchainCommand { static summary = "List projects on the Armada Network."; @@ -18,7 +18,7 @@ export default class ProjectList extends BlockchainCommand { const { flags } = await this.parse(ProjectList); const provider = await getProvider(flags.network, flags.rpc); const projects = await getContract(flags.network, flags.abi, "ArmadaProjects", provider); - const owner = normalizeAddress(flags.owner); + const owner = parseAddress(flags.owner); const blockTag = await provider.getBlockNumber(); let results: Result[] = await getAll(flags.page, async (i, n) => { return await projects.getProjects(i, n, { blockTag }); @@ -27,7 +27,8 @@ export default class ProjectList extends BlockchainCommand { results = results.filter((v) => v.owner.toLowerCase() === owner.toLowerCase()); } - const output = normalizeRecords(results.slice(flags.skip, flags.skip + flags.size)); + const records = results.slice(flags.skip, flags.skip + flags.size); + const output = records.map((r) => formatProject(r)); this.log(pretty(output)); return output; } diff --git a/src/commands/project/owner.ts b/src/commands/project/owner.ts index 94532ab..11ce75a 100644 --- a/src/commands/project/owner.ts +++ b/src/commands/project/owner.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, pretty, run } from "../../helpers"; export default class ProjectOwner extends TransactionCommand { static summary = "Transfer ownership of a project."; @@ -16,7 +16,7 @@ export default class ProjectOwner extends TransactionCommand { const { args, flags } = await this.parse(ProjectOwner); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const projects = await getContract(flags.network, flags.abi, "ArmadaProjects", signer); - const projectId = normalizeHash(args.ID); + const projectId = parseHash(args.ID); const tx = await projects.populateTransaction.setProjectOwner(projectId, args.ADDR); const output = await run(tx, signer, [projects]); this.log(pretty(output)); diff --git a/src/commands/project/props.ts b/src/commands/project/props.ts index dd2b603..6785f31 100644 --- a/src/commands/project/props.ts +++ b/src/commands/project/props.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, pretty, run } from "../../helpers"; export default class ProjectProps extends TransactionCommand { static summary = "Change detailed properties of a project."; @@ -16,7 +16,7 @@ export default class ProjectProps extends TransactionCommand { const { args, flags } = await this.parse(ProjectProps); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const projects = await getContract(flags.network, flags.abi, "ArmadaProjects", signer); - const projectId = normalizeHash(args.ID); + const projectId = parseHash(args.ID); const tx = await projects.populateTransaction.setProjectProps(projectId, args.NAME, args.EMAIL); const output = await run(tx, signer, [projects]); this.log(pretty(output)); diff --git a/src/commands/project/show.ts b/src/commands/project/show.ts index 29866e1..53c17ee 100644 --- a/src/commands/project/show.ts +++ b/src/commands/project/show.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { BlockchainCommand } from "../../base"; -import { getContract, getProvider, normalizeHash, normalizeRecord, pretty } from "../../helpers"; +import { formatProject, getContract, getProvider, parseHash, pretty } from "../../helpers"; export default class ProjectShow extends BlockchainCommand { static summary = "Show details of an Armada Network project."; @@ -12,9 +12,9 @@ export default class ProjectShow extends BlockchainCommand { const { args, flags } = await this.parse(ProjectShow); const provider = await getProvider(flags.network, flags.rpc); const projects = await getContract(flags.network, flags.abi, "ArmadaProjects", provider); - const projectId = normalizeHash(args.ID); + const projectId = parseHash(args.ID); const record = await projects.getProject(projectId); - const output = normalizeRecord(record); + const output = formatProject(record); this.log(pretty(output)); return output; } diff --git a/src/commands/project/withdraw.ts b/src/commands/project/withdraw.ts index 05acc65..97b16ae 100644 --- a/src/commands/project/withdraw.ts +++ b/src/commands/project/withdraw.ts @@ -1,35 +1,34 @@ import { Arg } from "@oclif/core/lib/interfaces"; -import { parseUnits } from "ethers/lib/utils"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, normalizeRecord, pretty, run } from "../../helpers"; +import { formatRecord, formatUSDC, getContract, getSigner, parseHash, parseUSDC, pretty, run } from "../../helpers"; export default class ProjectWithdraw extends TransactionCommand { - static summary = "Withdraw Armada tokens from project escrow."; + static summary = "Withdraw USDC from project escrow."; static description = "The tokens are sent to the project owner. Only the part of tokens not needed to reserve " + "the nodes can be withdrawn. In order to withdraw more, reservations can be deleted."; static examples = ["<%= config.bin %> <%= command.id %> 0x123abc... 1.0"]; - static usage = "<%= command.id %> ID TOKENS"; + static usage = "<%= command.id %> ID USDC"; static args: Arg[] = [ { name: "ID", description: "The ID of the project to withdraw escrow from.", required: true }, - { name: "TOKENS", description: "The Armada token amount to withdraw (e.g. 1.0).", required: true }, + { name: "USDC", description: "The USDC amount to withdraw (e.g. 1.0).", required: true }, ]; public async run(): Promise { const { args, flags } = await this.parse(ProjectWithdraw); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); - const token = await getContract(flags.network, flags.abi, "ArmadaToken", signer); + const usdc = await getContract(flags.network, flags.abi, "USDC", signer); const projects = await getContract(flags.network, flags.abi, "ArmadaProjects", signer); const address = await signer.getAddress(); - const projectId = normalizeHash(args.ID); - const tokens = parseUnits(args.TOKENS, 18); - if (tokens.lte(0)) this.error("A positive amount required."); - const oldBalance = await token.balanceOf(address); - const tx = await projects.populateTransaction.withdrawProjectEscrow(projectId, tokens, address); + const projectId = parseHash(args.ID); + const amount = parseUSDC(args.USDC); + if (amount.lte(0)) this.error("A positive amount required."); + const oldEscrow = await usdc.balanceOf(address); + const tx = await projects.populateTransaction.withdrawProjectEscrow(projectId, amount, address); const output = await run(tx, signer, [projects]); - const newBalance = await token.balanceOf(address); + const newEscrow = await usdc.balanceOf(address); this.log(pretty(output)); - this.log(pretty(normalizeRecord({ address, oldBalance, newBalance }))); + this.log(pretty(formatRecord({ address, oldEscrow: formatUSDC(oldEscrow), newEscrow: formatUSDC(newEscrow) }))); return output; } } diff --git a/src/commands/reservation/create.ts b/src/commands/reservation/create.ts index 93f48b1..f3b1edf 100644 --- a/src/commands/reservation/create.ts +++ b/src/commands/reservation/create.ts @@ -1,8 +1,7 @@ import { Flags } from "@oclif/core"; import { Arg } from "@oclif/core/lib/interfaces"; -import { parseUnits } from "ethers/lib/utils"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, parseUSDC, pretty, run } from "../../helpers"; export default class ReservationCreate extends TransactionCommand { static summary = "Reserve content nodes for a project."; @@ -28,11 +27,11 @@ export default class ReservationCreate extends TransactionCommand { this.error("Must provide at least one of --spot and/or --renew."); } - const nodeIds = args.IDS.split(",").map((id: string) => normalizeHash(id)); + const nodeIds = args.IDS.split(",").map((id: string) => parseHash(id)); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const reservations = await getContract(flags.network, flags.abi, "ArmadaReservations", signer); - const projectId = normalizeHash(args.ID); - const prices = nodeIds.map(() => parseUnits("1", 18)); + const projectId = parseHash(args.ID); + const prices = nodeIds.map(() => parseUSDC("1")); const slot = { last: !!flags.spot, next: !!flags.renew }; const tx = await reservations.populateTransaction.createReservations(projectId, nodeIds, prices, slot); const output = await run(tx, signer, [reservations]); diff --git a/src/commands/reservation/delete.ts b/src/commands/reservation/delete.ts index 2a737ef..02e84d8 100644 --- a/src/commands/reservation/delete.ts +++ b/src/commands/reservation/delete.ts @@ -1,6 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { getContract, getSigner, normalizeHash, pretty, run } from "../../helpers"; +import { getContract, getSigner, parseHash, pretty, run } from "../../helpers"; export default class ReservationDelete extends TransactionCommand { static summary = "Unreserve content nodes from a project."; @@ -14,10 +14,10 @@ export default class ReservationDelete extends TransactionCommand { public async run(): Promise { const { args, flags } = await this.parse(ReservationDelete); - const nodeIds = args.IDS.split(",").map((id: string) => normalizeHash(id)); + const nodeIds = args.IDS.split(",").map((id: string) => parseHash(id)); const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); const reservations = await getContract(flags.network, flags.abi, "ArmadaReservations", signer); - const projectId = normalizeHash(args.ID); + const projectId = parseHash(args.ID); const slot = { last: false, next: true }; const tx = await reservations.populateTransaction.deleteReservations(projectId, nodeIds, slot); const output = await run(tx, signer, [reservations]); diff --git a/src/commands/reservation/list.ts b/src/commands/reservation/list.ts index 20a9200..a4adcf5 100644 --- a/src/commands/reservation/list.ts +++ b/src/commands/reservation/list.ts @@ -2,7 +2,7 @@ import { Flags } from "@oclif/core"; import { Arg } from "@oclif/core/lib/interfaces"; import { Result } from "ethers/lib/utils"; import { BlockchainCommand } from "../../base"; -import { getAll, getContract, getProvider, normalizeHash, normalizeRecords, pretty } from "../../helpers"; +import { formatNode, getAll, getContract, getProvider, parseHash, pretty } from "../../helpers"; export default class ReservationList extends BlockchainCommand { static summary = "List node reservations by a project."; @@ -19,13 +19,14 @@ export default class ReservationList extends BlockchainCommand { const { args, flags } = await this.parse(ReservationList); const provider = await getProvider(flags.network, flags.rpc); const reservations = await getContract(flags.network, flags.abi, "ArmadaReservations", provider); - const projectId = normalizeHash(args.ID); + const projectId = parseHash(args.ID); const blockTag = await provider.getBlockNumber(); const results: Result[] = await getAll(flags.page, async (i, n) => { return await reservations.getReservations(projectId, i, n, { blockTag }); }); - const output = normalizeRecords(results.slice(flags.skip, flags.skip + flags.size)); + const records = results.slice(flags.skip, flags.skip + flags.size); + const output = records.map((r) => formatNode(r)); this.log(pretty(output)); return output; } diff --git a/src/contracts.ts b/src/contracts.ts index 340ed22..4c8bc55 100644 --- a/src/contracts.ts +++ b/src/contracts.ts @@ -9,13 +9,23 @@ import "../abi/staging/ArmadaNodes.json"; import "../abi/staging/ArmadaOperators.json"; import "../abi/staging/ArmadaProjects.json"; import "../abi/staging/ArmadaReservations.json"; +import "../abi/staging/USDC.json"; + +// These imports are necessary to pull these files into dist/ import "../abi/testnet/ArmadaToken.json"; import "../abi/testnet/ArmadaNodes.json"; import "../abi/testnet/ArmadaOperators.json"; import "../abi/testnet/ArmadaProjects.json"; import "../abi/testnet/ArmadaReservations.json"; +import "../abi/testnet/USDC.json"; -export type ContractName = "ArmadaToken" | "ArmadaNodes" | "ArmadaOperators" | "ArmadaProjects" | "ArmadaReservations"; +export type ContractName = + | "ArmadaToken" + | "ArmadaNodes" + | "ArmadaOperators" + | "ArmadaProjects" + | "ArmadaReservations" + | "USDC"; export interface ContractInfo { address: string; diff --git a/src/helpers.ts b/src/helpers.ts index 9edd304..66e8837 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -14,8 +14,9 @@ import { VoidSigner, type PopulatedTransaction, type Signature, + type BigNumberish, } from "ethers"; -import { formatUnits, FunctionFragment, getAddress, Interface, Result } from "ethers/lib/utils"; +import { formatUnits, FunctionFragment, getAddress, Interface, parseUnits, Result } from "ethers/lib/utils"; import inquirer from "inquirer"; import keytar from "keytar"; import { ContractName, loadAbi } from "./contracts"; @@ -57,6 +58,11 @@ export function pretty(value: unknown): string { return typeof value === "string" ? value : inspect(value, { depth: 10 }); } +export const parseUSDC = (value: string): BigNumber => parseUnits(value, 6); +export const parseTokens = (value: string): BigNumber => parseUnits(value, 18); +export const formatUSDC = (value: BigNumberish): string => `${formatUnits(value, 6)} USDC`; +export const formatTokens = (value: BigNumberish): string => `${formatUnits(value, 18)} ARMADA`; + // Signs a permit to transfer tokens. export async function permit( signer: Signer, @@ -71,7 +77,8 @@ export async function permit( if (!provider) throw Error(""); const chainId = (await provider.getNetwork()).chainId; const nonces = await token.nonces(address); - const domain = { name: await token.name(), version: "1", chainId, verifyingContract: token.address }; + const version = token.version !== undefined ? await token.version() : "1"; + const domain = { name: await token.name(), version, chainId, verifyingContract: token.address }; const values = { owner: address, spender: spender.address, value: amount, nonce: nonces, deadline }; const signature = await (signer as unknown as TypedDataSigner)._signTypedData(domain, Permit, values); const sig = ethers.utils.splitSignature(signature); @@ -125,32 +132,44 @@ export function getTxUrl(tx: Transaction): string { return `https://${prefix}etherscan.io/tx/${tx.hash}`; } +export function formatNode(r: Record | Result): Record { + return formatRecord({ ...r, prices: r.prices.map((p: BigNumber) => formatUSDC(p)) }); +} + +export function formatOperator(r: Record | Result): Record { + return formatRecord({ ...r, stake: formatTokens(r.stake), balance: formatUSDC(r.balance) }); +} + +export function formatProject(r: Record | Result): Record { + return formatRecord({ ...r, escrow: formatUSDC(r.escrow), reserve: formatUSDC(r.reserve) }); +} + +export function formatBigNumber(n: BigNumber): string { + return `BigNumber ${n.toString()} / ${formatUnits(n, 6)} / ${formatUnits(n, 18)}`; +} + // Converts union objects returned by ethers to plain objects. -export function normalizeRecord(r: Record | Result): Record { +export function formatRecord(r: Record | Result): Record { return Object.fromEntries( Object.keys(r) .filter((k) => isNaN(Number(k))) .map((k) => { - return [k, normalizeRecordValue(r[k])]; + return [k, formatRecordValue(r[k])]; }) ); } -export function normalizeRecords(rs: (Record | Result)[]): Record[] { - return rs.map((r) => normalizeRecord(r)); -} - -function normalizeRecordValue(val: unknown): unknown { +function formatRecordValue(val: unknown): unknown { if (Array.isArray(val)) { - return val.map(normalizeRecordValue); + return val.map(formatRecordValue); } if (val instanceof BigNumber) { - return normalizeBigNumber(val); + return formatBigNumber(val); } return val; } -export function normalizeHash(s: string | undefined): string { +export function parseHash(s: string | undefined): string { if (!s?.length) { return HashZero; } @@ -160,19 +179,11 @@ export function normalizeHash(s: string | undefined): string { return s.startsWith("0x") ? s : "0x" + s; } -export function normalizeAddress(s: string | undefined): string { +export function parseAddress(s: string | undefined): string { // getAddress throws on invalid input which is what we want here return !s?.length ? AddressZero : getAddress(s); } -function normalizeBigNumber(n: BigNumber): string { - try { - return n.toNumber().toString(); - } catch { - return formatUnits(n, 18); - } -} - export async function getProvider(network: NetworkName, rpcUrl: string | undefined): Promise { const url = rpcUrl ?? Networks[network].url; const provider = new ethers.providers.JsonRpcProvider(url); @@ -195,11 +206,8 @@ export async function getSigner( wallet = new VoidSigner(AddressZero); } else if (signer === "ledger") { // Use stderr to not interfere with --json flag - console.warn("> Make sure the Ledger wallet is unlocked and the Ethereum application is open"); - if (!account) { - account = "0"; - } - wallet = new LedgerSigner(provider, "default", account); + console.warn("> Make sure that Ledger is unlocked and the Ethereum app is open"); + wallet = new LedgerSigner(provider, "default", account ?? "0"); const address = await wallet.getAddress(); // Use stderr to not interfere with --json flag console.warn(`> Using Ledger wallet ${address}`); @@ -280,7 +288,7 @@ export async function decodeEvents(receipt: TransactionReceipt, contracts: Contr } catch { continue; } - results.push({ event: frag.name, args: normalizeRecord(args) }); + results.push({ event: frag.name, args: formatRecord(args) }); } } } diff --git a/src/keystore.ts b/src/keystore.ts index 584554f..a60b94e 100644 --- a/src/keystore.ts +++ b/src/keystore.ts @@ -4,13 +4,13 @@ import path from "path"; import { encryptKeystore } from "@ethersproject/json-wallets"; import { Wallet } from "ethers"; import keytar from "keytar"; -import { normalizeHash } from "./helpers"; +import { parseHash } from "./helpers"; const homedir = os.homedir(); const keyStoreFolderPath = path.join(homedir, ".armada/keystore"); export async function saveWallet(privateKey: string, password: string, description: string): Promise { - privateKey = normalizeHash(privateKey); + privateKey = parseHash(privateKey); const wallet = new Wallet(privateKey); const address = wallet.address; const text = await encryptKeystore({ address, privateKey }, password);