diff --git a/src/commands/operator/deposit.ts b/src/commands/operator/deposit.ts index b9fac42..9729a6f 100644 --- a/src/commands/operator/deposit.ts +++ b/src/commands/operator/deposit.ts @@ -1,16 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { - formatRecord, - formatUSDC, - getContract, - getSigner, - parseHash, - parseTokens, - permit, - pretty, - run, -} from "../../helpers"; +import { approve, getContract, getSigner, parseHash, parseTokens, permit, pretty, run } from "../../helpers"; export default class OperatorDeposit extends TransactionCommand { static summary = "Deposit USDC to operator earned balance."; @@ -26,17 +16,15 @@ export default class OperatorDeposit extends TransactionCommand { const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); 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 = parseHash(args.ID); const amount = parseTokens(args.USDC); - const deadline = Math.floor(Date.now() / 1000) + 3600; - const sig = await permit(signer, usdc, operators, amount, deadline); - const oldBalance = await usdc.balanceOf(address); + + const output = []; + const { tx: approveTx, deadline, sig } = await approve(signer, usdc, operators, amount); + if (approveTx) output.push(await run(approveTx, signer, [usdc])); 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 usdc.balanceOf(address); + output.push(await run(tx, signer, [operators])); this.log(pretty(output)); - this.log(pretty(formatRecord({ address, oldBalance: formatUSDC(oldBalance), newBalance: formatUSDC(newBalance) }))); return output; } } diff --git a/src/commands/operator/stake.ts b/src/commands/operator/stake.ts index ef2faa1..077a7b3 100644 --- a/src/commands/operator/stake.ts +++ b/src/commands/operator/stake.ts @@ -1,16 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { - formatRecord, - formatTokens, - getContract, - getSigner, - parseHash, - parseTokens, - permit, - pretty, - run, -} from "../../helpers"; +import { approve, getContract, getSigner, parseHash, parseTokens, pretty, run } from "../../helpers"; export default class OperatorStake extends TransactionCommand { static summary = "Deposit Armada tokens to operator stake."; @@ -26,19 +16,15 @@ export default class OperatorStake extends TransactionCommand { 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 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 output = []; + const { tx: approveTx, deadline, sig } = await approve(signer, token, operators, amount); + if (approveTx) output.push(await run(approveTx, signer, [token])); 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); + output.push(await run(tx, signer, [operators])); 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 index 2c782a5..cc992d5 100644 --- a/src/commands/operator/unstake.ts +++ b/src/commands/operator/unstake.ts @@ -1,6 +1,18 @@ +import { AddressZero } from "@ethersproject/constants"; +import { Flags } from "@oclif/core"; import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { formatRecord, formatTokens, getContract, getSigner, parseHash, parseTokens, pretty, run } from "../../helpers"; +import { + formatRecord, + formatTokens, + getContract, + getSigner, + parseAddress, + parseHash, + parseTokens, + pretty, + run, +} from "../../helpers"; export default class OperatorUnstake extends TransactionCommand { static summary = "Withdraw Armada tokens from operator stake."; @@ -13,23 +25,30 @@ export default class OperatorUnstake extends TransactionCommand { { 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 }, ]; + static flags = { + recipient: Flags.string({ description: "[default: caller] The recipient address for tokens.", helpValue: "ADDR" }), + }; public async run(): Promise { const { args, flags } = await this.parse(OperatorUnstake); + if (flags.signer === "raw" && parseAddress(flags.recipient) === AddressZero) { + this.error("Must specify --recipient when using raw signer."); + } + 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 operators = await getContract(flags.network, flags.abi, "ArmadaOperators", signer); - const address = await signer.getAddress(); + const recipient = flags.recipient ? parseAddress(flags.recipient) : 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 oldBalance = await token.balanceOf(recipient); + const tx = await operators.populateTransaction.withdrawOperatorStake(operatorId, amount, recipient); const output = await run(tx, signer, [operators]); - const newBalance = await token.balanceOf(address); + const newBalance = await token.balanceOf(recipient); this.log(pretty(output)); this.log( - pretty(formatRecord({ address, oldBalance: formatTokens(oldBalance), newBalance: formatTokens(newBalance) })) + pretty(formatRecord({ recipient, oldBalance: formatTokens(oldBalance), newBalance: formatTokens(newBalance) })) ); return output; } diff --git a/src/commands/operator/withdraw.ts b/src/commands/operator/withdraw.ts index ab34427..24f5b1a 100644 --- a/src/commands/operator/withdraw.ts +++ b/src/commands/operator/withdraw.ts @@ -1,6 +1,18 @@ +import { AddressZero } from "@ethersproject/constants"; +import { Flags } from "@oclif/core"; import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { formatRecord, formatUSDC, getContract, getSigner, parseHash, parseTokens, pretty, run } from "../../helpers"; +import { + formatRecord, + formatUSDC, + getContract, + getSigner, + parseAddress, + parseHash, + parseTokens, + pretty, + run, +} from "../../helpers"; export default class OperatorWithdraw extends TransactionCommand { static summary = "Withdraw USDC from operator earned balance."; @@ -11,22 +23,31 @@ export default class OperatorWithdraw extends TransactionCommand { { 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 }, ]; + static flags = { + recipient: Flags.string({ description: "[default: caller] The recipient address for tokens.", helpValue: "ADDR" }), + }; public async run(): Promise { const { args, flags } = await this.parse(OperatorWithdraw); + if (flags.signer === "raw" && parseAddress(flags.recipient) === AddressZero) { + this.error("Must specify --recipient when using raw signer."); + } + const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); 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 recipient = flags.recipient ? parseAddress(flags.recipient) : await signer.getAddress(); 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 oldBalance = await usdc.balanceOf(recipient); + const tx = await operators.populateTransaction.withdrawOperatorBalance(operatorId, amount, recipient); const output = await run(tx, signer, [operators]); - const newBalance = await usdc.balanceOf(address); + const newBalance = await usdc.balanceOf(recipient); this.log(pretty(output)); - this.log(pretty(formatRecord({ address, oldBalance: formatUSDC(oldBalance), newBalance: formatUSDC(newBalance) }))); + this.log( + pretty(formatRecord({ recipient, oldBalance: formatUSDC(oldBalance), newBalance: formatUSDC(newBalance) })) + ); return output; } } diff --git a/src/commands/project/create.ts b/src/commands/project/create.ts index a52bde8..e411eff 100644 --- a/src/commands/project/create.ts +++ b/src/commands/project/create.ts @@ -1,3 +1,4 @@ +import { AddressZero } from "@ethersproject/constants"; import { Flags } from "@oclif/core"; import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; @@ -22,6 +23,9 @@ export default class ProjectCreate extends TransactionCommand { if (!!args.URL !== !!args.SHA) { this.error("Can only specify URL and SHA together."); } + if (flags.signer === "raw" && parseAddress(flags.owner) === AddressZero) { + this.error("Must specify --owner when using raw signer."); + } 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); diff --git a/src/commands/project/deposit.ts b/src/commands/project/deposit.ts index f00a947..6ced5de 100644 --- a/src/commands/project/deposit.ts +++ b/src/commands/project/deposit.ts @@ -1,16 +1,6 @@ import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { - formatRecord, - formatUSDC, - getContract, - getSigner, - parseHash, - parseUSDC, - permit, - pretty, - run, -} from "../../helpers"; +import { approve, getContract, getSigner, parseHash, parseUSDC, permit, pretty, run } from "../../helpers"; export default class ProjectDeposit extends TransactionCommand { static summary = "Deposit Armada tokens to project escrow."; @@ -26,17 +16,15 @@ export default class ProjectDeposit extends TransactionCommand { const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); 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 = parseHash(args.ID); const amount = parseUSDC(args.USDC); - const deadline = Math.floor(Date.now() / 1000) + 3600; - const sig = await permit(signer, usdc, projects, amount, deadline); - const oldBalance = await usdc.balanceOf(address); + + const output = []; + const { tx: approveTx, deadline, sig } = await approve(signer, usdc, projects, amount); + if (approveTx) output.push(await run(approveTx, signer, [usdc])); 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 usdc.balanceOf(address); + output.push(await run(tx, signer, [projects])); this.log(pretty(output)); - this.log(pretty(formatRecord({ address, oldBalance: formatUSDC(oldBalance), newBalance: formatUSDC(newBalance) }))); return output; } } diff --git a/src/commands/project/withdraw.ts b/src/commands/project/withdraw.ts index 97b16ae..ac35743 100644 --- a/src/commands/project/withdraw.ts +++ b/src/commands/project/withdraw.ts @@ -1,6 +1,18 @@ +import { AddressZero } from "@ethersproject/constants"; +import { Flags } from "@oclif/core"; import { Arg } from "@oclif/core/lib/interfaces"; import { TransactionCommand } from "../../base"; -import { formatRecord, formatUSDC, getContract, getSigner, parseHash, parseUSDC, pretty, run } from "../../helpers"; +import { + formatRecord, + formatUSDC, + getContract, + getSigner, + parseAddress, + parseHash, + parseUSDC, + pretty, + run, +} from "../../helpers"; export default class ProjectWithdraw extends TransactionCommand { static summary = "Withdraw USDC from project escrow."; @@ -13,22 +25,29 @@ export default class ProjectWithdraw extends TransactionCommand { { name: "ID", description: "The ID of the project to withdraw escrow from.", required: true }, { name: "USDC", description: "The USDC amount to withdraw (e.g. 1.0).", required: true }, ]; + static flags = { + recipient: Flags.string({ description: "[default: caller] The recipient address for tokens.", helpValue: "ADDR" }), + }; public async run(): Promise { const { args, flags } = await this.parse(ProjectWithdraw); + if (flags.signer === "raw" && parseAddress(flags.recipient) === AddressZero) { + this.error("Must specify --recipient when using raw signer."); + } + const signer = await getSigner(flags.network, flags.rpc, flags.address, flags.signer, flags.key, flags.account); 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 recipient = flags.recipient ? parseAddress(flags.recipient) : await signer.getAddress(); 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 oldEscrow = await usdc.balanceOf(recipient); + const tx = await projects.populateTransaction.withdrawProjectEscrow(projectId, amount, recipient); const output = await run(tx, signer, [projects]); - const newEscrow = await usdc.balanceOf(address); + const newEscrow = await usdc.balanceOf(recipient); this.log(pretty(output)); - this.log(pretty(formatRecord({ address, oldEscrow: formatUSDC(oldEscrow), newEscrow: formatUSDC(newEscrow) }))); + this.log(pretty(formatRecord({ recipient, oldEscrow: formatUSDC(oldEscrow), newEscrow: formatUSDC(newEscrow) }))); return output; } } diff --git a/src/helpers.ts b/src/helpers.ts index 66e8837..9b34bb8 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -44,6 +44,16 @@ export type TransactionLog = { args: Record; }; +const EmptySignature: Signature = { + r: HashZero, + s: HashZero, + v: 0, + _vs: "", + recoveryParam: 0, + yParityAndS: "", + compact: "", +}; + export const Permit: Record> = { Permit: [ { name: "owner", type: "address" }, @@ -63,6 +73,26 @@ 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`; +// Creates token transfer allowance. +// Returns either a token approve transaction (tx), or a gassless permit (deadline+sig) if the signer supports it. +export async function approve( + signer: Signer, + token: Contract, + spender: Contract, + amount: BigNumber +): Promise<{ tx: PopulatedTransaction | undefined; deadline: number; sig: Signature }> { + let tx = undefined; + let deadline = 0; + let sig = EmptySignature; + if (signer instanceof VoidSigner || signer instanceof LedgerSigner) { + tx = await token.populateTransaction.approve(spender.address, amount); + } else { + deadline = Math.floor(Date.now() / 1000) + 3600; + sig = await permit(signer, token, spender, amount, deadline); + } + return { tx, deadline, sig }; +} + // Signs a permit to transfer tokens. export async function permit( signer: Signer, @@ -89,7 +119,6 @@ export async function permit( // Signs and executes the transaction and returns its emitted events, if a signer is provided. // Otherwise, builds and returns a raw unsigned transaction string, if VoidSigner is provided. // Pass the contracts parameter to decode the corresponding events. No other events are returned. -// The first of the passed contracts will be used to lookup the abi used for the raw transaction. export async function run( tx: PopulatedTransaction, signer: Signer, @@ -97,9 +126,10 @@ export async function run( ): Promise { if (signer instanceof VoidSigner) { if (!tx.to || !tx.data) return undefined; - delete tx.from; // Raw tx must have "from" field + delete tx.from; // Raw tx must not have "from" field const sighash = tx.data.slice(0, 10); - const fragment = getFragment(contracts[0].interface, sighash); + const interfaces = contracts.map((c) => c.interface); + const fragment = getFragment(interfaces, sighash); if (!fragment) return undefined; const abi = `[${fragment.format("json")}]`; const raw = ethers.utils.serializeTransaction(tx); @@ -118,10 +148,12 @@ export async function run( return events; } -export function getFragment(interface_: Interface, sighash: string): FunctionFragment | undefined { - for (const fragment of Object.values(interface_.functions)) { - if (sighash === interface_.getSighash(fragment)) { - return fragment; +export function getFragment(interfaces: Interface[], sighash: string): FunctionFragment | undefined { + for (const interface_ of interfaces) { + for (const fragment of Object.values(interface_.functions)) { + if (sighash === interface_.getSighash(fragment)) { + return fragment; + } } } } @@ -203,7 +235,7 @@ export async function getSigner( let wallet: Signer; if (signer === "raw") { - wallet = new VoidSigner(AddressZero); + wallet = new VoidSigner(AddressZero, provider); } else if (signer === "ledger") { // Use stderr to not interfere with --json flag console.warn("> Make sure that Ledger is unlocked and the Ethereum app is open");