Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make all commands work with raw signer #53

Merged
merged 1 commit into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 6 additions & 18 deletions src/commands/operator/deposit.ts
Original file line number Diff line number Diff line change
@@ -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.";
Expand All @@ -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;
}
}
26 changes: 6 additions & 20 deletions src/commands/operator/stake.ts
Original file line number Diff line number Diff line change
@@ -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.";
Expand All @@ -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;
}
}
31 changes: 25 additions & 6 deletions src/commands/operator/unstake.ts
Original file line number Diff line number Diff line change
@@ -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.";
Expand All @@ -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<unknown> {
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;
}
Expand Down
33 changes: 27 additions & 6 deletions src/commands/operator/withdraw.ts
Original file line number Diff line number Diff line change
@@ -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.";
Expand All @@ -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<unknown> {
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;
}
}
4 changes: 4 additions & 0 deletions src/commands/project/create.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Expand Down
24 changes: 6 additions & 18 deletions src/commands/project/deposit.ts
Original file line number Diff line number Diff line change
@@ -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.";
Expand All @@ -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;
}
}
31 changes: 25 additions & 6 deletions src/commands/project/withdraw.ts
Original file line number Diff line number Diff line change
@@ -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.";
Expand All @@ -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<unknown> {
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;
}
}
48 changes: 40 additions & 8 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ export type TransactionLog = {
args: Record<string, unknown>;
};

const EmptySignature: Signature = {
r: HashZero,
s: HashZero,
v: 0,
_vs: "",
recoveryParam: 0,
yParityAndS: "",
compact: "",
};

export const Permit: Record<string, Array<TypedDataField>> = {
Permit: [
{ name: "owner", type: "address" },
Expand All @@ -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,
Expand All @@ -89,17 +119,17 @@ 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,
contracts: Contract[]
): Promise<RawTransaction | TransactionLog[] | undefined> {
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);
Expand All @@ -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;
}
}
}
}
Expand Down Expand Up @@ -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");
Expand Down