Skip to content

Commit

Permalink
Airdrop by dual finance (#7) (solana-labs#1422)
Browse files Browse the repository at this point in the history
* Name token only after the mint has been initialized

* Merkle airdrop in realms

* Allow hex input

* Fix validation

* Fix validation

* yarn lock

* yarn lock

* Change label for amount
  • Loading branch information
brittcyr committed Feb 24, 2023
1 parent 9a12fb1 commit ff36c64
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 1 deletion.
5 changes: 5 additions & 0 deletions hooks/useGovernanceAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,11 @@ export default function useGovernanceAssets() {
isVisible: canUseTransferInstruction,
packageId: PackageEnum.Dual,
},
[Instructions.DualFinanceAirdrop]: {
name: 'Airdrop',
isVisible: canUseTransferInstruction,
packageId: PackageEnum.Dual,
},

/*
███████ ██ ██ ███████ ██████ ██ ███████ ███ ██ ██████
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@coral-xyz/borsh": "0.26.0",
"@dialectlabs/react-sdk-blockchain-solana": "1.0.0-beta.3",
"@dialectlabs/react-ui": "1.1.0-beta.5",
"@dual-finance/airdrop": "0.0.4",
"@dual-finance/staking-options": "0.0.17",
"@emotion/react": "11.9.0",
"@emotion/styled": "11.8.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { useContext, useEffect, useState } from 'react'
import { ProgramAccount, Governance } from '@solana/spl-governance'
import {
UiInstruction,
DualFinanceAirdropForm,
} from '@utils/uiTypes/proposalCreationTypes'
import { NewProposalContext } from '../../../new'
import GovernedAccountSelect from '../../GovernedAccountSelect'
import useGovernanceAssets from '@hooks/useGovernanceAssets'
import Input from '@components/inputs/Input'
import { getAirdropInstruction } from '@utils/instructions/Dual/airdrop'
import useWalletStore from 'stores/useWalletStore'
import { getDualFinanceAirdropSchema } from '@utils/validations'
import Tooltip from '@components/Tooltip'

const DualAirdrop = ({
index,
governance,
}: {
index: number
governance: ProgramAccount<Governance> | null
}) => {
const [form, setForm] = useState<DualFinanceAirdropForm>({
root: '',
amount: 0,
treasury: undefined,
})
const connection = useWalletStore((s) => s.connection)
const wallet = useWalletStore((s) => s.current)
const shouldBeGoverned = !!(index !== 0 && governance)
const { assetAccounts } = useGovernanceAssets()
const [governedAccount, setGovernedAccount] = useState<
ProgramAccount<Governance> | undefined
>(undefined)
const [formErrors, setFormErrors] = useState({})
const { handleSetInstructions } = useContext(NewProposalContext)
const handleSetForm = ({ propertyName, value }) => {
setFormErrors({})
setForm({ ...form, [propertyName]: value })
}
function getInstruction(): Promise<UiInstruction> {
return getAirdropInstruction({
connection,
form,
schema,
setFormErrors,
wallet,
})
}
useEffect(() => {
handleSetInstructions(
{ governedAccount: governedAccount, getInstruction },
index
)
}, [form])
useEffect(() => {
setGovernedAccount(form.treasury?.governance)
}, [form.treasury])
const schema = getDualFinanceAirdropSchema()

return (
<>
<Tooltip content="Merkle root of the airdrop. https://github.com/Dual-Finance/airdrop-sdk/blob/97d97492bdb926f150a6436a68a77eda35fc7095/src/utils/balance_tree.ts#L40">
<Input
label="Root"
value={form.root}
type="text"
onChange={(evt) =>
handleSetForm({
value: evt.target.value,
propertyName: 'root',
})
}
error={formErrors['root']}
/>
</Tooltip>
<Input
label="Total number of tokens"
value={form.amount}
type="text"
onChange={(evt) =>
handleSetForm({
value: evt.target.value,
propertyName: 'amount',
})
}
error={formErrors['amount']}
/>
<GovernedAccountSelect
label="Treasury"
governedAccounts={assetAccounts}
onChange={(value) => {
handleSetForm({ value, propertyName: 'treasury' })
}}
value={form.treasury}
error={formErrors['treasury']}
shouldBeGoverned={shouldBeGoverned}
governance={governance}
type="token"
></GovernedAccountSelect>
</>
)
}

export default DualAirdrop
2 changes: 2 additions & 0 deletions pages/dao/[symbol]/proposal/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ import AddKeyToDID from './components/instructions/Identity/AddKeyToDID'
import RemoveKeyFromDID from './components/instructions/Identity/RemoveKeyFromDID'
import AddServiceToDID from './components/instructions/Identity/AddServiceToDID'
import RemoveServiceFromDID from './components/instructions/Identity/RemoveServiceFromDID'
import DualAirdrop from './components/instructions/Dual/DualAirdrop'
import DualWithdraw from './components/instructions/Dual/DualWithdraw'
import DualExercise from './components/instructions/Dual/DualExercise'
import PsyFinanceMintAmericanOptions from './components/instructions/PsyFinance/MintAmericanOptions'
Expand Down Expand Up @@ -434,6 +435,7 @@ const New = () => {
[Instructions.ClaimPendingDeposit]: FriktionClaimPendingDeposit,
[Instructions.ClaimPendingWithdraw]: FriktionClaimPendingWithdraw,
[Instructions.DepositIntoCastle]: CastleDeposit,
[Instructions.DualFinanceAirdrop]: DualAirdrop,
[Instructions.DualFinanceStakingOption]: StakingOption,
[Instructions.DualFinanceWithdraw]: DualWithdraw,
[Instructions.DualFinanceExercise]: DualExercise,
Expand Down
80 changes: 80 additions & 0 deletions utils/instructions/Dual/airdrop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
serializeInstructionToBase64,
} from '@solana/spl-governance'

import { ConnectionContext } from '@utils/connection'
import { validateInstruction } from '@utils/instructionTools'
import {
DualFinanceAirdropForm,
UiInstruction,
} from '@utils/uiTypes/proposalCreationTypes'
import { WalletAdapter } from '@solana/wallet-adapter-base'
import { Airdrop, AirdropConfigureContext } from '@dual-finance/airdrop'
import { Keypair } from '@solana/web3.js'
import { BN } from '@coral-xyz/anchor'
import { getMintNaturalAmountFromDecimalAsBN } from '@tools/sdk/units'

interface AirdropArgs {
connection: ConnectionContext
form: DualFinanceAirdropForm
setFormErrors: any
schema: any
wallet: WalletAdapter | undefined
}

export async function getAirdropInstruction({
connection,
wallet,
form,
schema,
setFormErrors,
}: AirdropArgs): Promise<UiInstruction> {
const isValid = await validateInstruction({ schema, form, setFormErrors })

const serializedInstruction = ''
const additionalSerializedInstructions: string[] = []
if (isValid && form.treasury && wallet?.publicKey && form.treasury.extensions.mint?.account.decimals) {
const airdrop = new Airdrop(connection.endpoint)

const amountNatural: BN = getMintNaturalAmountFromDecimalAsBN(
form.amount,
form.treasury.extensions.mint?.account.decimals
);

let root: number[] = [];
try {
root = Array.from(Uint8Array.from(Buffer.from(form.root, 'hex')));
} catch (err) {
root = form.root.split(',').map(function(item) {
return parseInt(item, 10);
});
}
const airdropTransactionContext: AirdropConfigureContext = await airdrop.createConfigMerkleTransactionFromRoot(
form.treasury.pubkey, // source
form.treasury.extensions.token!.account.owner!, // authority
amountNatural,
root,
);

for (const instruction of airdropTransactionContext.transaction.instructions) {
additionalSerializedInstructions.push(
serializeInstructionToBase64(instruction)
)
}

return {
serializedInstruction,
additionalSerializedInstructions,
signers: airdropTransactionContext.signers as Keypair[],
isValid: true,
governance: form.treasury?.governance,
}
}

return {
serializedInstruction,
isValid: false,
governance: form.treasury?.governance,
additionalSerializedInstructions: [],
}
}
7 changes: 7 additions & 0 deletions utils/uiTypes/proposalCreationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ export enum Instructions {
DepositIntoVolt,
DepositReserveLiquidityAndObligationCollateral,
DifferValidatorStake,
DualFinanceAirdrop,
DualFinanceExercise,
DualFinanceStakingOption,
DualFinanceWithdraw,
Expand Down Expand Up @@ -716,6 +717,12 @@ export interface ValidatorWithdrawStakeForm {
amount: number
}

export interface DualFinanceAirdropForm {
root: string
amount: number
treasury: AssetAccount | undefined
}

export interface DualFinanceStakingOptionForm {
strike: number
soName: string | undefined
Expand Down
54 changes: 54 additions & 0 deletions utils/validations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,60 @@ export const getFriktionWithdrawSchema = () => {
})
}

export const getDualFinanceAirdropSchema = () => {
return yup.object().shape({
root: yup.string().required('Root is required')
.test(
'destination',
'Account validation error',
async function (val: string) {
if (val) {
try {
const arr = Uint8Array.from(Buffer.from(val, 'hex'));
if (arr.length !== 32) {
return this.createError({
message: 'Expected 32 bytes'
})
}
return true;
} catch (e) {
console.log(e)
}
try {
const root = val.split(',').map(function(item) {
return parseInt(item, 10);
});
if (root.length !== 32) {
return this.createError({
message: 'Expected 32 bytes'
})
}
for (const byte of root) {
if (byte < 0 || byte >= 256) {
return this.createError({
message: 'Invalid byte'
})
}
}
return true
} catch (e) {
console.log(e)
}
return this.createError({
message: `Could not parse`,
});
} else {
return this.createError({
message: `Root is required`,
})
}
}
),
treasury: yup.object().typeError('Treasury is required'),
amount: yup.number().typeError('Amount is required')
})
}

export const getDualFinanceStakingOptionSchema = () => {
return yup.object().shape({
soName: yup.string().required('Staking option name is required'),
Expand Down
12 changes: 11 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,16 @@
js-sha256 "^0.9.0"
tweetnacl "1.0.3"

"@dual-finance/airdrop@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@dual-finance/airdrop/-/airdrop-0.0.4.tgz#12ee6f1a99f52725eaa783c522efd7021981db3c"
integrity sha512-eLBC1pzJwqhniVvHm3b26RWcLEVcFRL3OA94FsagUiWzSY7uYLigo5phBvN153dviaI+AiNPc86aQ85IlY+AAA==
dependencies:
"@coral-xyz/anchor" "^0.26.0"
"@solana/spl-token" "^0.3.6"
"@solana/web3.js" "^1.73.2"
js-sha3 "^0.8.0"

"@dual-finance/staking-options@0.0.17":
version "0.0.17"
resolved "https://registry.yarnpkg.com/@dual-finance/staking-options/-/staking-options-0.0.17.tgz#e56f7eb8be7d1364ac7fac6e0d0e8b79cab833cf"
Expand Down Expand Up @@ -5011,7 +5021,7 @@
"@wallet-standard/app" "^1.0.0"
"@wallet-standard/base" "^1.0.0"

"@solana/web3.js@1.42.0", "@solana/web3.js@1.56.0", "@solana/web3.js@1.66.2", "@solana/web3.js@^1.16.1", "@solana/web3.js@^1.17.0", "@solana/web3.js@^1.20.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.22.0", "@solana/web3.js@^1.24.1", "@solana/web3.js@^1.29.2", "@solana/web3.js@^1.30.2", "@solana/web3.js@^1.31.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.35.0", "@solana/web3.js@^1.35.1", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.37.1", "@solana/web3.js@^1.42.0", "@solana/web3.js@^1.44.3", "@solana/web3.js@^1.48.0", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.59.1", "@solana/web3.js@^1.63.0", "@solana/web3.js@^1.63.1", "@solana/web3.js@^1.66.2", "@solana/web3.js@^1.68.0":
"@solana/web3.js@1.42.0", "@solana/web3.js@1.56.0", "@solana/web3.js@1.66.2", "@solana/web3.js@^1.16.1", "@solana/web3.js@^1.17.0", "@solana/web3.js@^1.20.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.22.0", "@solana/web3.js@^1.24.1", "@solana/web3.js@^1.29.2", "@solana/web3.js@^1.30.2", "@solana/web3.js@^1.31.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.35.0", "@solana/web3.js@^1.35.1", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.37.1", "@solana/web3.js@^1.42.0", "@solana/web3.js@^1.43.5", "@solana/web3.js@^1.44.3", "@solana/web3.js@^1.48.0", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.59.1", "@solana/web3.js@^1.63.0", "@solana/web3.js@^1.63.1", "@solana/web3.js@^1.66.2", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.73.2":
version "1.66.2"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.66.2.tgz#80b43c5868b846124fe3ebac7d3943930c3fa60c"
integrity sha512-RyaHMR2jGmaesnYP045VLeBGfR/gAW3cvZHzMFGg7bkO+WOYOYp1nEllf0/la4U4qsYGKCsO9eEevR5fhHiVHg==
Expand Down

0 comments on commit ff36c64

Please sign in to comment.