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

feat/proposal creation improvements #234

Merged
merged 4 commits into from
Sep 5, 2024
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
9 changes: 0 additions & 9 deletions lib/client/indexer/markets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,6 @@ export class FutarchyIndexerMarketsClient implements FutarchyMarketsClient {
market_acct: { _eq: request.marketKey.toBase58() }
}
},
tokenAcctByBidsTokenAcct: {
token: {
decimals: true,
image_url: true,
symbol: true,
name: true,
mint_acct: true
}
},
token: {
decimals: true,
image_url: true,
Expand Down
91 changes: 65 additions & 26 deletions lib/client/indexer/proposals.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Keypair, PublicKey } from "@solana/web3.js";
import { PublicKey } from "@solana/web3.js";
import {
Proposal,
DaoAggregate,
Expand All @@ -17,9 +17,7 @@ import {
ProposalRanking,
ProposalRequestConfig,
ProposalDaoConfiguration,
ProposalInstruction,
VaultAccountWithKey,
VaultAccount
NextProposalValues
} from "@/types";
import { FutarchyProposalsClient } from "@/client";
import { FutarchyRPCProposalsClient } from "@/client/rpc";
Expand Down Expand Up @@ -717,28 +715,69 @@ export class FutarchyIndexerProposalsClient implements FutarchyProposalsClient {
}

// we should really probably change this column to a sequence so we don't have to manually fetch this OR we should just get rid of the column
async getProposalIdAndEndSlot(): Promise<[number, number]> {
const result = await this.getGQLClient({}).query({
proposal_details: {
__args: {
order_by: [{ proposal_id: "desc" }],
limit: 1
},
proposal: {
dao: {
slots_per_proposal: true
}
},
proposal_id: true
}
});
async getNextProposalValues(
proposalSlug: string
): Promise<NextProposalValues> {
const [latestProposalResult, matchingSlugProposals] = await Promise.all([
this.getGQLClient({}).query({
proposal_details: {
__args: {
order_by: [{ proposal_id: "desc" }],
limit: 1
},
proposal: {
dao: {
slots_per_proposal: true
}
},
proposal_id: true
}
}),
this.getGQLClient({}).query({
proposal_details: {
__args: {
where: {
slug: { _ilike: proposalSlug }
}
},
slug: true
}
})
]);

const existingSlugs = matchingSlugProposals.proposal_details.map(
(p) => p.slug ?? ""
);

const latestProposal = result.proposal_details[0];
const latestProposal = latestProposalResult.proposal_details[0];
const proposalId = latestProposal ? latestProposal.proposal_id + 1 : 0;
const slotsPerProposal =
latestProposal.proposal?.dao.slots_per_proposal ?? 0;

return [proposalId, slotsPerProposal];
return {
proposalId,
proposalDuration: slotsPerProposal,
proposalSlug: this.getNewProposalSlug(existingSlugs, proposalSlug)
};
}

private getNewProposalSlug(
existingSlugs: string[],
desiredSlug: string
): string {
if (!existingSlugs.includes(desiredSlug)) {
return desiredSlug;
}

let counter = 1;
let newSlug = `${desiredSlug}-${counter}`;

while (existingSlugs.includes(newSlug)) {
counter++;
newSlug = `${desiredSlug}-${counter}`;
}

return newSlug;
}

async saveProposalDetails(
Expand All @@ -762,8 +801,8 @@ export class FutarchyIndexerProposalsClient implements FutarchyProposalsClient {
} = proposalDetails;

try {
const [latestPropID, slotsPerProposal] =
await this.getProposalIdAndEndSlot();
const { proposalId, proposalSlug, proposalDuration } =
await this.getNextProposalValues(slug);

const proposalOnChain = await this.rpcProposalsClient.fetchProposal(
dao,
Expand Down Expand Up @@ -845,7 +884,7 @@ export class FutarchyIndexerProposalsClient implements FutarchyProposalsClient {
updated_at: new Date(),
initial_slot: proposalOnChain.account.slotEnqueued.toNumber(),
end_slot: proposalOnChain.account.slotEnqueued
.add(new BN(slotsPerProposal))
.add(new BN(proposalDuration))
.toNumber(),
pass_threshold_bps: dao.daoAccount.passThresholdBps,
duration_in_slots: dao.daoAccount.slotsPerProposal?.toNumber(),
Expand Down Expand Up @@ -873,14 +912,14 @@ export class FutarchyIndexerProposalsClient implements FutarchyProposalsClient {
insert_proposal_details_one: {
__args: {
object: {
proposal_id: latestPropID,
proposal_id: proposalId,
proposal_acct:
proposalsResult.insert_proposals_one?.proposal_acct,
title: title,
description: description,
categories: categories,
content: content,
slug: slug,
slug: proposalSlug,
proposer_acct: proposerAcct.toBase58(),
base_cond_vault_acct: baseCondVaultAcct.toBase58(),
quote_cond_vault_acct: quoteCondVaultAcct.toBase58(),
Expand Down
7 changes: 3 additions & 4 deletions lib/client/rpc/balances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,11 @@ export class FutarchyRPCBalancesClient implements FutarchyBalancesClient {
tokenWithPDA.pda,
(accountInfo) => {
const accountData = AccountLayout.decode(accountInfo.data);
const dividedTokenAmount = new BN(Number(accountData.amount)).div(
new BN(10).pow(new BN(tokenWithPDA.token.decimals))
);
const dividedTokenAmount = Number(accountData.amount) /
10 ** tokenWithPDA.token.decimals;
// this is bugged if value is decimal/between 0 and 1
const tokenVal: TokenWithBalance = {
balance: dividedTokenAmount.toNumber(),
balance: dividedTokenAmount,
token: tokenWithPDA.token
};
subscriber.next(tokenVal);
Expand Down
5 changes: 2 additions & 3 deletions lib/client/rpc/market-clients/openbookMarkets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ import { enrichTokenMetadata } from "@/tokens";
import { getTwapMarketKey } from "@/openbookTwap";
import { BASE_FORMAT, MAX_MARKET_PRICE, NUMERAL_FORMAT } from "@/constants";
import { shortKey } from "@/utils";
import { utf8 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import { SendTransactionResponse } from "@/types/transactions";
import utf8 from 'utf8';

export class FutarchyOpenbookMarketsRPCClient
implements FutarchyOrderbookMarketsClient<OpenbookMarket, OpenbookOrder>
Expand Down Expand Up @@ -91,7 +90,7 @@ export class FutarchyOpenbookMarketsRPCClient
);

const marketName = utf8
.decode(new Uint8Array(obMarket.account.name))
.decode(new Uint8Array(obMarket.account.name).toString())
.split("\x00")[0];

const baseTokenWithSymbol = !baseToken.isFallback
Expand Down
144 changes: 144 additions & 0 deletions lib/instructions/coder/idl-coder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { IdlField, IdlType, IdlTypeDef } from '@/types';
import { IdlError } from '@coral-xyz/anchor';
import * as borsh from '@coral-xyz/borsh';
import { Layout } from '@coral-xyz/borsh';
import camelCase from 'camelcase';

export class IdlCoder {
static fieldLayout(field: {
name?: string;
} & Pick<IdlField, "type">, types?: IdlTypeDef[]): Layout<Object | null> {
const fieldName = field.name !== undefined ? camelCase(field.name) : undefined;
switch (field.type) {
case "bool": {
return borsh.bool(fieldName);
}
case "u8": {
return borsh.u8(fieldName);
}
case "i8": {
return borsh.i8(fieldName);
}
case "u16": {
return borsh.u16(fieldName);
}
case "i16": {
return borsh.i16(fieldName);
}
case "u32": {
return borsh.u32(fieldName);
}
case "i32": {
return borsh.i32(fieldName);
}
case "f32": {
return borsh.f32(fieldName);
}
case "u64": {
return borsh.u64(fieldName);
}
case "i64": {
return borsh.i64(fieldName);
}
case "f64": {
return borsh.f64(fieldName);
}
case "u128": {
return borsh.u128(fieldName);
}
case "i128": {
return borsh.i128(fieldName);
}
case "u256": {
return borsh.u256(fieldName);
}
case "i256": {
return borsh.i256(fieldName);
}
case "bytes": {
return borsh.vecU8(fieldName);
}
case "string": {
return borsh.str(fieldName);
}
case "publicKey": {
return borsh.publicKey(fieldName);
}
default: {
if ("vec" in field.type) {
return borsh.vec(IdlCoder.fieldLayout({
name: undefined,
type: field.type.vec,
}, types), fieldName);
}
else if ("option" in field.type) {
return borsh.option(IdlCoder.fieldLayout({
name: undefined,
type: field.type.option,
}, types), fieldName);
}
else if ("defined" in field.type) {
const defined = field.type.defined;
// User defined type.
if (types === undefined) {
throw new IdlError("User defined types not provided");
}
const filtered = types.filter((t) => t.name === defined);
if (filtered.length !== 1) {
throw new IdlError(`Type not found: ${JSON.stringify(field)}`);
}
return IdlCoder.typeDefLayout(filtered[0], types, fieldName);
}
else if ("array" in field.type) {
let arrayTy = field.type.array[0];
let arrayLen = field.type.array[1];
let innerLayout = IdlCoder.fieldLayout({
name: undefined,
type: arrayTy,
}, types);
return borsh.array(innerLayout, arrayLen, fieldName);
}
else {
throw new Error(`Not yet implemented: ${field}`);
}
}
}
}
static typeDefLayout(typeDef: IdlTypeDef, types?: IdlTypeDef[], name?: string) {
if (typeDef.type.kind === "struct") {
const fieldLayouts = typeDef.type.fields.map((field) => {
const x = IdlCoder.fieldLayout(field, types);
return x;
});
return borsh.struct(fieldLayouts, name);
}
else if (typeDef.type.kind === "enum") {
let variants = typeDef.type.variants.map((variant) => {
const name = camelCase(variant.name);
if (variant.fields === undefined) {
return borsh.struct([], name);
}
const fieldLayouts = variant.fields.map((f, i) => {
if (!f.hasOwnProperty("name")) {
return IdlCoder.fieldLayout({ type: f as IdlType, name: i.toString() }, types);
}
// this typescript conversion is ok
// because if f were of type IdlType
// (that does not have a name property)
// the check before would've errored
return IdlCoder.fieldLayout(f as IdlField, types);
});
return borsh.struct(fieldLayouts, name);
});
if (name !== undefined) {
// Buffer-layout lib requires the name to be null (on construction)
// when used as a field.
return borsh.rustEnum(variants).replicate(name);
}
return borsh.rustEnum(variants, name);
}
else {
throw new Error(`Unknown type kint: ${typeDef}`);
}
}
}
7 changes: 2 additions & 5 deletions lib/instructions/coder/memoInstructionDecoder.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { Idl } from '@coral-xyz/anchor';
import {
Instruction,
InstructionDisplay,
} from '@coral-xyz/anchor/dist/cjs/coder/borsh/instruction';
import { Idl, Instruction } from '@coral-xyz/anchor';
import { AccountMeta } from '@solana/web3.js';
import bs58 from 'bs58';
import { Buffer } from 'buffer';
import { DecodeInstructionCoder } from '@/types/instructions';
import { InstructionDisplay } from '@/types';

export class MemoInstructionDecoder implements DecodeInstructionCoder {
public constructor(private _idl: Idl) {}
Expand Down
11 changes: 5 additions & 6 deletions lib/instructions/coder/nativeInstructionDecoder.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Idl, Instruction } from '@coral-xyz/anchor';
import { IdlCoder } from '@coral-xyz/anchor/dist/cjs/coder/borsh/idl';
import { IdlField } from '@coral-xyz/anchor/dist/cjs/idl';
import * as borsh from '@coral-xyz/borsh';
import { AccountMeta } from '@solana/web3.js';
import bs58 from 'bs58';
import { Buffer } from 'buffer';
import { InstructionDisplay } from '@coral-xyz/anchor/dist/cjs/coder/borsh/instruction';
import { Layout } from '@solana/buffer-layout';
import camelCase from 'camelcase';

import { InstructionFormatter } from '../formatter';
import { DecodeInstructionCoder } from '@/types/instructions';
import { IdlField, IdlInstruction, IdlTypeDef, InstructionDisplay } from '@/types';
import { IdlCoder } from './idl-coder';

/**
* this is a stripped down version of the BorshInstructionCoder (1). All we care about for incompatible (aka non-anchor) programs
Expand Down Expand Up @@ -71,9 +70,9 @@ export class NativeInstructionDecoder implements DecodeInstructionCoder {
// given an instruction layout, derive the layout that can be used to decode each respective field
private static parseIxLayout(idl: Idl): Map<string, Layout<Object>> {

const ixLayouts = idl.instructions?.map((m: any): [string, Layout<Object>] => {
const fieldLayouts = m.args.map((arg: IdlField) =>
IdlCoder.fieldLayout(arg, [...idl.types || []]),
const ixLayouts = idl.instructions?.map((m: IdlInstruction): [string, Layout<Object>] => {
const fieldLayouts = m.args.map((arg) =>
IdlCoder.fieldLayout(arg, [...idl.types || []] as IdlTypeDef[]),
);
const name = camelCase(m.name);
return [name, borsh.struct(fieldLayouts, name)];
Expand Down
Loading
Loading