Skip to content

Commit

Permalink
Feature/saga phone deposit (#871)
Browse files Browse the repository at this point in the history
* saga phone purchase

* fix close aob account in mango withdraw + saga phone inst fixes

* decode instruction + rebuy fixes

* fixes

* fix build

* fix updateQuantity

* transfer rent

* fix rent

* rent size fix

* show usdc account instand of sol account
  • Loading branch information
abrzezinski94 authored Jul 21, 2022
1 parent 0b72233 commit 5e77d7e
Show file tree
Hide file tree
Showing 13 changed files with 1,173 additions and 14 deletions.
6 changes: 5 additions & 1 deletion Strategies/components/WithdrawModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,11 @@ const WithdrawModal = ({
chunkBy: 1,
}
proposalInstructions.push(instructionData)
if (wrappedSolAccount) {
if (
wrappedSolAccount &&
(selectedMangoAccount.owner.toBase58() === form.withdrawAddress ||
selectedMangoAccount.owner.toBase58() === governance.pubkey.toBase58())
) {
const closeAobInstruction = closeAccount({
source: wrappedSolAccount.publicKey,
destination: new PublicKey(form.withdrawAddress),
Expand Down
14 changes: 8 additions & 6 deletions actions/dryRunInstruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { WalletAdapter } from '@solana/wallet-adapter-base'
export async function dryRunInstruction(
connection: Connection,
wallet: WalletAdapter,
instructionData: InstructionData,
instructionData: InstructionData | null,
prerequisiteInstructionsToRun?: TransactionInstruction[] | undefined,
additionalInstructions?: InstructionData[]
) {
Expand All @@ -29,11 +29,13 @@ export async function dryRunInstruction(
}
}

transaction.add({
keys: instructionData.accounts,
programId: instructionData.programId,
data: Buffer.from(instructionData.data),
})
if (instructionData) {
transaction.add({
keys: instructionData.accounts,
programId: instructionData.programId,
data: Buffer.from(instructionData.data),
})
}

const result = await simulateTransaction(connection, transaction, 'single')

Expand Down
60 changes: 60 additions & 0 deletions components/instructions/programs/SagaPhone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
AnchorProvider,
BorshInstructionCoder,
Program,
} from '@project-serum/anchor'
import { Connection, Keypair } from '@solana/web3.js'
import { MORTAR_PROGRAM_ID } from '@tools/sagaPhone/mortar'
import { IDL, Mortar } from '@tools/sagaPhone/schema'

export const SAGA_PHONE = {
[MORTAR_PROGRAM_ID.toBase58()]: {
194: {
name: 'Purchase',
accounts: [
{ name: 'Issuer' },
{ name: 'Purchaser' },
{ name: 'Payer' },
{ name: 'Receipt' },
{ name: 'Receipt Tokens' },
{ name: 'Purchaser Tokens' },
],
getDataUI: async (connection: Connection, data: Uint8Array) => {
try {
return <div></div>
} catch (e) {
console.log(e)
return <div>{JSON.stringify(data)}</div>
}
},
},
102: {
name: 'Update Quantity',
accounts: [
{ name: 'Issuer' },
{ name: 'Purchaser' },
{ name: 'Receipt' },
{ name: 'Receipt Tokens' },
{ name: 'Purchaser Tokens' },
],
getDataUI: async (connection: Connection, data: Uint8Array) => {
try {
const program = new Program<Mortar>(
IDL,
MORTAR_PROGRAM_ID,
new AnchorProvider(null as any, Keypair.generate() as any, {})
)
const decodedInstructionData = new BorshInstructionCoder(
program.idl
).decode(Buffer.from(data))?.data as any
return (
<div>quantity: {decodedInstructionData.newQuantity.toNumber()}</div>
)
} catch (e) {
console.log(e)
return <div>{JSON.stringify(data)}</div>
}
},
},
},
}
2 changes: 2 additions & 0 deletions components/instructions/tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ConnectionContext } from '@utils/connection'
import { NFT_VOTER_INSTRUCTIONS } from './programs/nftVotingClient'
import { PROGRAM_IDS } from '@castlefinance/vault-sdk'
import { FORESIGHT_INSTRUCTIONS } from './programs/foresight'
import { SAGA_PHONE } from './programs/SagaPhone'
import { LIDO_INSTRUCTIONS } from './programs/lido'
/**
* Default governance program id instance
Expand Down Expand Up @@ -250,6 +251,7 @@ export const INSTRUCTION_DESCRIPTORS = {
...SYSTEM_INSTRUCTIONS,
...VOTE_STAKE_REGISTRY_INSTRUCTIONS,
...NFT_VOTER_INSTRUCTIONS,
...SAGA_PHONE,
}

export async function getInstructionDescriptor(
Expand Down
5 changes: 5 additions & 0 deletions hooks/useGovernanceAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ export default function useGovernanceAssets() {
name: 'Close token account',
isVisible: canUseTransferInstruction,
},
{
id: Instructions.SagaPreOrder,
name: 'Pre-order Saga Phone',
isVisible: canUseTokenTransferInstruction,
},
{
id: Instructions.None,
name: 'None',
Expand Down
5 changes: 1 addition & 4 deletions pages/dao/[symbol]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,7 @@ const REALM = () => {
<div>
{realmInfo?.bannerImage ? (
<>
<img
className="mb-10"
src={realmInfo?.bannerImage}
></img>
<img className="mb-10" src={realmInfo?.bannerImage}></img>
{/* temp. setup for Ukraine.SOL */}
{realmInfo.sharedWalletId && (
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ const DryRunInstructionBtn = ({
const result = await dryRunInstruction(
connection.current,
wallet!,
getInstructionDataFromBase64(instructionData?.serializedInstruction),
instructionData?.serializedInstruction
? getInstructionDataFromBase64(instructionData?.serializedInstruction)
: null,
prerequisiteInstructionsToRun,
additionalInstructions?.map((x) => getInstructionDataFromBase64(x))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface InstructionInput {
hide?: boolean | (() => boolean)
validateMinMax?: boolean
precision?: number
additionalComponent?: JSX.Element
additionalComponent?: JSX.Element | null
}

const InstructionForm = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { useContext, useEffect, useState } from 'react'
import useRealm from '@hooks/useRealm'
import {
PublicKey,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js'
import * as yup from 'yup'
import { isFormValid } from '@utils/formValidation'
import {
UiInstruction,
SagaPhoneForm,
} from '@utils/uiTypes/proposalCreationTypes'
import { NewProposalContext } from '../../../../new'
import useGovernanceAssets from '@hooks/useGovernanceAssets'
import { Governance } from '@solana/spl-governance'
import { ProgramAccount } from '@solana/spl-governance'
import useWalletStore from 'stores/useWalletStore'
import { serializeInstructionToBase64 } from '@solana/spl-governance'
import { AccountType } from '@utils/uiTypes/assets'
import InstructionForm, {
InstructionInput,
InstructionInputType,
} from '../../FormCreator'
import { BN } from '@project-serum/anchor'
import { USDC_MINT } from 'Strategies/protocols/mango/tools'
import {
DEVNET_ISSUER,
getPurchaseInstructions,
MAINNET_ISSUER,
USDC_DEVNET,
} from '@tools/sagaPhone/mortar'
import { abbreviateAddress } from '@utils/formatting'

const SagaPreOrder = ({
index,
governance,
}: {
index: number
governance: ProgramAccount<Governance> | null
}) => {
const wallet = useWalletStore((s) => s.current)
const { connection } = useWalletStore()
const { realmInfo } = useRealm()
const { assetAccounts } = useGovernanceAssets()
const isDevnet = connection.cluster === 'devnet'
const ISSUER = isDevnet ? DEVNET_ISSUER : MAINNET_ISSUER
const SALE_MINT = isDevnet ? USDC_DEVNET : new PublicKey(USDC_MINT)
const onePhoneDepositPrice = isDevnet ? 250 : 100
const governedSolAccounts = assetAccounts.filter(
(solAcc) =>
solAcc.type === AccountType.SOL &&
assetAccounts.find(
(tokenAcc) =>
tokenAcc.extensions.token?.account.owner.toBase58() ===
solAcc.extensions.transferAddress?.toBase58() &&
tokenAcc.extensions.mint?.publicKey.toBase58() ===
SALE_MINT.toBase58()
)
)
const governedUSDCAccounts = assetAccounts.filter(
(token) =>
token.isToken &&
token.extensions.mint?.publicKey.toBase58() === SALE_MINT.toBase58() &&
governedSolAccounts.find(
(solAcc) =>
solAcc.extensions.transferAddress?.toBase58() ===
token.extensions.token?.account.owner.toBase58()
)
)
const shouldBeGoverned = index !== 0 && governance
const programId: PublicKey | undefined = realmInfo?.programId
const [form, setForm] = useState<SagaPhoneForm>({
governedAccount: null,
quantity: 1,
})
const [formErrors, setFormErrors] = useState({})
const { handleSetInstructions } = useContext(NewProposalContext)
const handleSetForm = ({ propertyName, value }) => {
setFormErrors({})
setForm({ ...form, [propertyName]: value })
}
const validateInstruction = async (): Promise<boolean> => {
const { isValid, validationErrors } = await isFormValid(schema, form)
setFormErrors(validationErrors)
return isValid
}
async function getInstruction(): Promise<UiInstruction> {
const isValid = await validateInstruction()
let serializedInstructions: string[] = []
const prequisiteInstructions: TransactionInstruction[] = []
if (
isValid &&
programId &&
form.governedAccount?.governance?.account &&
wallet?.publicKey
) {
//size of ata
const size = 165
const rent = await connection.current.getMinimumBalanceForRentExemption(
size
)
const transferRentIx = SystemProgram.transfer({
fromPubkey: wallet.publicKey!,
toPubkey: form.governedAccount.extensions.token!.account.owner!,
lamports: rent,
})
prequisiteInstructions.push(transferRentIx)
//Mango instruction call and serialize
const instructions = await getPurchaseInstructions(
form.governedAccount.extensions.token!.account.owner!,
ISSUER,
form.governedAccount.extensions.token!.account.owner!,
SALE_MINT,
new BN(form.quantity),
connection.current
)

serializedInstructions = instructions.map((x) =>
serializeInstructionToBase64(x)
)
}
const obj: UiInstruction = {
prerequisiteInstructions: prequisiteInstructions,
additionalSerializedInstructions: serializedInstructions,
serializedInstruction: '',
isValid,
governance: form.governedAccount?.governance,
}
return obj
}
useEffect(() => {
handleSetForm({
propertyName: 'programId',
value: programId?.toString(),
})
}, [realmInfo?.programId])
useEffect(() => {
handleSetInstructions(
{ governedAccount: form.governedAccount?.governance, getInstruction },
index
)
}, [form])
const schema = yup.object().shape({
quantity: yup.number().min(1).required('Mango group is required'),
governedAccount: yup
.object()
.nullable()
.required('Program governed account is required'),
})
const inputs: InstructionInput[] = [
{
label: 'USDC account owned by SOL account',
initialValue: form.governedAccount,
name: 'governedAccount',
type: InstructionInputType.GOVERNED_ACCOUNT,
shouldBeGoverned: shouldBeGoverned as any,
governance: governance,
options: governedUSDCAccounts,
additionalComponent: form.governedAccount?.extensions.token ? (
<div>
SOL account:{' '}
<small>
{abbreviateAddress(
form.governedAccount?.extensions.token?.account.owner
)}
</small>
</div>
) : null,
},
{
label: 'Quantity',
initialValue: form.quantity,
type: InstructionInputType.INPUT,
validateMinMax: true,
name: 'quantity',
inputType: 'number',
min: 1,
additionalComponent: (
<div>
To Deposit:{' '}
{form.quantity ? Number(form.quantity) * onePhoneDepositPrice : 0}
</div>
),
},
]

return (
<>
{form && (
<InstructionForm
outerForm={form}
setForm={setForm}
inputs={inputs}
setFormErrors={setFormErrors}
formErrors={formErrors}
></InstructionForm>
)}
</>
)
}

export default SagaPreOrder
6 changes: 5 additions & 1 deletion pages/dao/[symbol]/proposal/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import MakeRemoveSpotMarket from './components/instructions/Mango/MakeRemoveSpot
import MakeRemovePerpMarket from './components/instructions/Mango/MakeRemovePerpMarket'
import MakeSwapSpotMarket from './components/instructions/Mango/MakeSwapSpotMarket'
import MakeRemoveOracle from './components/instructions/Mango/MakeRemoveOracle'
import SagaPreOrder from './components/instructions/Solana/SagaPhone/SagaPreOrder'

const TITLE_LENGTH_LIMIT = 130

Expand Down Expand Up @@ -241,7 +242,8 @@ const New = () => {
? getTimestampFromDays(instruction.customHoldUpTime)
: selectedGovernance?.account?.config
.minInstructionHoldUpTime,
prerequisiteInstructions: [],
prerequisiteInstructions:
instruction.prerequisiteInstructions || [],
chunkSplitByDefault: instruction.chunkSplitByDefault || false,
signers: instruction.signers,
shouldSplitIntoSeparateTxs:
Expand Down Expand Up @@ -343,6 +345,8 @@ const New = () => {
)
case Instructions.Mint:
return <Mint index={idx} governance={governance}></Mint>
case Instructions.SagaPreOrder:
return <SagaPreOrder index={idx} governance={governance}></SagaPreOrder>
case Instructions.Base64:
return <CustomBase64 index={idx} governance={governance}></CustomBase64>
case Instructions.None:
Expand Down
Loading

1 comment on commit 5e77d7e

@vercel
Copy link

@vercel vercel bot commented on 5e77d7e Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.